mirror of
https://gitlab.com/bramw/baserow.git
synced 2024-11-24 16:36:46 +00:00
542 lines
16 KiB
Vue
542 lines
16 KiB
Vue
<template>
|
|
<div>
|
|
<header
|
|
ref="header"
|
|
class="layout__col-2-1 header"
|
|
:class="{ 'header--overflow': headerOverflow }"
|
|
>
|
|
<div v-show="tableLoading" class="header__loading"></div>
|
|
<ul v-if="!tableLoading" class="header__filter">
|
|
<li v-if="showLogo" class="header__filter-item">
|
|
<ExternalLinkBaserowLogo class="header__filter-logo" />
|
|
</li>
|
|
<li class="header__filter-item header__filter-item--grids">
|
|
<a
|
|
ref="viewsSelectToggle"
|
|
class="header__filter-link"
|
|
:class="{ 'header__filter-link--disabled': views === null }"
|
|
@click="
|
|
views !== null &&
|
|
$refs.viewsContext.toggle(
|
|
$refs.viewsSelectToggle,
|
|
'bottom',
|
|
'left',
|
|
4
|
|
)
|
|
"
|
|
>
|
|
<template v-if="hasSelectedView">
|
|
<i
|
|
class="header__filter-icon header-filter-icon--view"
|
|
:class="`${view._.type.colorClass} ${view._.type.iconClass}`"
|
|
></i>
|
|
<span class="header__filter-name header__filter-name--forced">
|
|
<EditableViewName ref="rename" :view="view"></EditableViewName>
|
|
</span>
|
|
<i class="header__sub-icon iconoir-nav-arrow-down"></i>
|
|
</template>
|
|
<template v-else-if="view !== null">
|
|
{{ $t('table.chooseView') }}
|
|
<i class="header__sub-icon iconoir-nav-arrow-down"></i>
|
|
</template>
|
|
</a>
|
|
<ViewsContext
|
|
v-if="views !== null"
|
|
ref="viewsContext"
|
|
:database="database"
|
|
:table="table"
|
|
:views="views"
|
|
:read-only="readOnly"
|
|
:header-overflow="headerOverflow"
|
|
@selected-view="$emit('selected-view', $event)"
|
|
></ViewsContext>
|
|
</li>
|
|
<li
|
|
v-if="hasSelectedView && !readOnly && showViewContext"
|
|
class="header__filter-item header__filter-item--no-margin-left"
|
|
>
|
|
<a
|
|
class="header__filter-link"
|
|
@click="
|
|
$refs.viewContext.toggle(
|
|
$event.currentTarget,
|
|
'bottom',
|
|
'left',
|
|
4
|
|
)
|
|
"
|
|
>
|
|
<i class="header__filter-icon baserow-icon-more-vertical"></i>
|
|
</a>
|
|
<ViewContext
|
|
ref="viewContext"
|
|
:database="database"
|
|
:view="view"
|
|
:table="table"
|
|
@enable-rename="$refs.rename.edit()"
|
|
>
|
|
</ViewContext>
|
|
</li>
|
|
<li
|
|
v-if="
|
|
hasSelectedView &&
|
|
view._.type.canFilter &&
|
|
(adhocFiltering ||
|
|
$hasPermission(
|
|
'database.table.view.create_filter',
|
|
view,
|
|
database.workspace.id
|
|
))
|
|
"
|
|
class="header__filter-item"
|
|
>
|
|
<ViewFilter
|
|
:view="view"
|
|
:is-public-view="isPublic"
|
|
:fields="fields"
|
|
:read-only="adhocFiltering"
|
|
:disable-filter="disableFilter"
|
|
@changed="refresh()"
|
|
></ViewFilter>
|
|
</li>
|
|
<li
|
|
v-if="
|
|
hasSelectedView &&
|
|
view._.type.canSort &&
|
|
(adhocSorting ||
|
|
$hasPermission(
|
|
'database.table.view.create_sort',
|
|
view,
|
|
database.workspace.id
|
|
))
|
|
"
|
|
class="header__filter-item"
|
|
>
|
|
<ViewSort
|
|
:view="view"
|
|
:fields="fields"
|
|
:read-only="adhocSorting"
|
|
:disable-sort="disableSort"
|
|
@changed="refresh()"
|
|
></ViewSort>
|
|
</li>
|
|
<li
|
|
v-if="
|
|
hasSelectedView &&
|
|
view._.type.canGroupBy &&
|
|
(readOnly ||
|
|
$hasPermission(
|
|
'database.table.view.create_group_by',
|
|
view,
|
|
database.workspace.id
|
|
))
|
|
"
|
|
class="header__filter-item"
|
|
>
|
|
<ViewGroupBy
|
|
:view="view"
|
|
:fields="fields"
|
|
:read-only="readOnly"
|
|
:disable-group-by="disableGroupBy"
|
|
@changed="refresh()"
|
|
></ViewGroupBy>
|
|
</li>
|
|
<li
|
|
v-if="
|
|
hasSelectedView &&
|
|
view._.type.canShare &&
|
|
!readOnly &&
|
|
$hasPermission(
|
|
'database.table.view.update_slug',
|
|
view,
|
|
database.workspace.id
|
|
)
|
|
"
|
|
class="header__filter-item"
|
|
>
|
|
<ShareViewLink :view="view" :read-only="readOnly"></ShareViewLink>
|
|
</li>
|
|
<li
|
|
v-if="
|
|
hasCompatibleDecorator &&
|
|
!readOnly &&
|
|
$hasPermission(
|
|
'database.table.view.decoration.update',
|
|
view,
|
|
database.workspace.id
|
|
)
|
|
"
|
|
class="header__filter-item"
|
|
>
|
|
<ViewDecoratorMenu
|
|
:database="database"
|
|
:view="view"
|
|
:table="table"
|
|
:fields="fields"
|
|
:read-only="readOnly"
|
|
:disable-sort="disableSort"
|
|
@changed="refresh()"
|
|
></ViewDecoratorMenu>
|
|
</li>
|
|
</ul>
|
|
<component
|
|
:is="getViewHeaderComponent(view)"
|
|
v-if="!tableLoading && hasSelectedView"
|
|
:database="database"
|
|
:table="table"
|
|
:view="view"
|
|
:fields="fields"
|
|
:read-only="readOnly"
|
|
:store-prefix="storePrefix"
|
|
@refresh="refresh"
|
|
/>
|
|
</header>
|
|
<div class="layout__col-2-2 content">
|
|
<component
|
|
:is="getViewComponent(view)"
|
|
v-if="hasSelectedView && !tableLoading"
|
|
ref="view"
|
|
:database="database"
|
|
:table="table"
|
|
:view="view"
|
|
:loading="viewLoading"
|
|
:fields="fields"
|
|
:read-only="readOnly"
|
|
:store-prefix="storePrefix"
|
|
@refresh="refresh"
|
|
@selected-row="$emit('selected-row', $event)"
|
|
@navigate-previous="
|
|
(row, activeSearchTerm) =>
|
|
$emit('navigate-previous', row, activeSearchTerm)
|
|
"
|
|
@navigate-next="
|
|
(row, activeSearchTerm) =>
|
|
$emit('navigate-next', row, activeSearchTerm)
|
|
"
|
|
/>
|
|
<div v-if="viewLoading" class="loading-overlay"></div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { mapGetters } from 'vuex'
|
|
import ResizeObserver from 'resize-observer-polyfill'
|
|
|
|
import { RefreshCancelledError } from '@baserow/modules/core/errors'
|
|
import { notifyIf } from '@baserow/modules/core/utils/error'
|
|
import ViewsContext from '@baserow/modules/database/components/view/ViewsContext'
|
|
import ViewContext from '@baserow/modules/database/components/view/ViewContext'
|
|
import ViewFilter from '@baserow/modules/database/components/view/ViewFilter'
|
|
import ViewSort from '@baserow/modules/database/components/view/ViewSort'
|
|
import ViewDecoratorMenu from '@baserow/modules/database/components/view/ViewDecoratorMenu'
|
|
import ViewSearch from '@baserow/modules/database/components/view/ViewSearch'
|
|
import EditableViewName from '@baserow/modules/database/components/view/EditableViewName'
|
|
import ShareViewLink from '@baserow/modules/database/components/view/ShareViewLink'
|
|
import ExternalLinkBaserowLogo from '@baserow/modules/core/components/ExternalLinkBaserowLogo'
|
|
import ViewGroupBy from '@baserow/modules/database/components/view/ViewGroupBy.vue'
|
|
|
|
/**
|
|
* This page component is the skeleton for a table. Depending on the selected view it
|
|
* will load the correct components into the header and body.
|
|
*/
|
|
export default {
|
|
components: {
|
|
ViewGroupBy,
|
|
ExternalLinkBaserowLogo,
|
|
ShareViewLink,
|
|
EditableViewName,
|
|
ViewsContext,
|
|
ViewDecoratorMenu,
|
|
ViewFilter,
|
|
ViewSort,
|
|
ViewSearch,
|
|
ViewContext,
|
|
},
|
|
/**
|
|
* Because there is no hook that is called before the route changes, we need the
|
|
* tableLoading middleware to change the table loading state. This change will get
|
|
* rendered right away. This allows us to have a custom loading animation when
|
|
* switching views.
|
|
*/
|
|
middleware: ['tableLoading'],
|
|
props: {
|
|
database: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
table: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
fields: {
|
|
type: Array,
|
|
required: true,
|
|
},
|
|
views: {
|
|
required: false,
|
|
validator: (prop) => typeof prop === 'object' || prop === undefined,
|
|
default: null,
|
|
},
|
|
view: {
|
|
required: true,
|
|
validator: (prop) => typeof prop === 'object' || prop === undefined,
|
|
},
|
|
tableLoading: {
|
|
type: Boolean,
|
|
required: true,
|
|
},
|
|
readOnly: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false,
|
|
},
|
|
disableFilter: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false,
|
|
},
|
|
disableSort: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false,
|
|
},
|
|
disableGroupBy: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false,
|
|
},
|
|
storePrefix: {
|
|
type: String,
|
|
required: false,
|
|
default: '',
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
// Shows a small spinning loading animation when the view is being refreshed.
|
|
viewLoading: false,
|
|
// Indicates if the elements within the header are overflowing. In case of true,
|
|
// we can hide certain values to make sure that it fits within the header.
|
|
headerOverflow: false,
|
|
}
|
|
},
|
|
computed: {
|
|
/**
|
|
* Indicates if there is a selected view by checking if the view object has been
|
|
* populated.
|
|
*/
|
|
hasSelectedView() {
|
|
return (
|
|
this.view !== undefined &&
|
|
Object.prototype.hasOwnProperty.call(this.view, '_')
|
|
)
|
|
},
|
|
hasCompatibleDecorator() {
|
|
return (
|
|
this.view !== undefined &&
|
|
this.$registry
|
|
.getOrderedList('viewDecorator')
|
|
.some((deco) => deco.isCompatible(this.view))
|
|
)
|
|
},
|
|
showLogo() {
|
|
return this.view?.show_logo && this.isPublic
|
|
},
|
|
showViewContext() {
|
|
return (
|
|
this.$hasPermission(
|
|
'database.table.run_export',
|
|
this.table,
|
|
this.database.workspace.id
|
|
) ||
|
|
this.$hasPermission(
|
|
'database.table.import_rows',
|
|
this.table,
|
|
this.database.workspace.id
|
|
) ||
|
|
this.someViewHasPermission(
|
|
'database.table.view.duplicate',
|
|
this.table,
|
|
this.database.workspace.id
|
|
) ||
|
|
this.someViewHasPermission(
|
|
'database.table.view.update',
|
|
this.table,
|
|
this.database.workspace.id
|
|
) ||
|
|
this.someViewHasPermission(
|
|
'database.table.view.delete',
|
|
this.table,
|
|
this.database.workspace.id
|
|
) ||
|
|
this.$hasPermission(
|
|
'database.table.create_webhook',
|
|
this.table,
|
|
this.database.workspace.id
|
|
)
|
|
)
|
|
},
|
|
adhocFiltering() {
|
|
if (this.readOnly) {
|
|
return true
|
|
}
|
|
|
|
return (
|
|
this.$hasPermission(
|
|
'database.table.view.list_filter',
|
|
this.view,
|
|
this.database.workspace.id
|
|
) &&
|
|
!this.$hasPermission(
|
|
'database.table.view.create_filter',
|
|
this.view,
|
|
this.database.workspace.id
|
|
)
|
|
)
|
|
},
|
|
adhocSorting() {
|
|
if (this.readOnly) {
|
|
return true
|
|
}
|
|
|
|
return (
|
|
this.$hasPermission(
|
|
'database.table.view.list_sort',
|
|
this.view,
|
|
this.database.workspace.id
|
|
) &&
|
|
!this.$hasPermission(
|
|
'database.table.view.create_sort',
|
|
this.view,
|
|
this.database.workspace.id
|
|
)
|
|
)
|
|
},
|
|
...mapGetters({
|
|
isPublic: 'page/view/public/getIsPublic',
|
|
}),
|
|
},
|
|
watch: {
|
|
tableLoading(value) {
|
|
if (!value) {
|
|
this.$nextTick(() => {
|
|
this.checkHeaderOverflow()
|
|
})
|
|
}
|
|
},
|
|
},
|
|
beforeMount() {
|
|
this.$bus.$on('table-refresh', this.refresh)
|
|
},
|
|
mounted() {
|
|
this.$el.resizeObserver = new ResizeObserver(this.checkHeaderOverflow)
|
|
this.$el.resizeObserver.observe(this.$el)
|
|
},
|
|
beforeDestroy() {
|
|
this.$bus.$off('table-refresh', this.refresh)
|
|
this.$el.resizeObserver.unobserve(this.$el)
|
|
},
|
|
methods: {
|
|
getViewComponent(view) {
|
|
const type = this.$registry.get('view', view.type)
|
|
return type.getComponent()
|
|
},
|
|
getViewHeaderComponent(view) {
|
|
const type = this.$registry.get('view', view.type)
|
|
return type.getHeaderComponent()
|
|
},
|
|
/**
|
|
* When the window resizes, we want to check if the content of the header is
|
|
* overflowing. If that is the case, we want to make some space by removing some
|
|
* content. We do this by copying the header content into a new HTMLElement and
|
|
* check if the elements still fit within the header. We copy the html because we
|
|
* want to measure the header in the full width state.
|
|
*/
|
|
checkHeaderOverflow() {
|
|
const header = this.$refs.header
|
|
const width = header.getBoundingClientRect().width
|
|
const wrapper = document.createElement('div')
|
|
wrapper.innerHTML = header.outerHTML
|
|
const el = wrapper.childNodes[0]
|
|
el.style = `position: absolute; left: 0; top: 0; width: ${width}px; overflow: auto;`
|
|
el.classList.remove('header--overflow')
|
|
document.body.appendChild(el)
|
|
this.headerOverflow =
|
|
el.clientWidth < el.scrollWidth || el.clientHeight < el.scrollHeight
|
|
document.body.removeChild(el)
|
|
},
|
|
/**
|
|
* Refreshes the whole view. All data will be reloaded and it will visually look
|
|
* the same as seeing the view for the first time.
|
|
*/
|
|
async refresh(event) {
|
|
// If could be that the refresh event is for a specific table and in table case
|
|
// we check if the refresh event is related to this table and stop if that is not
|
|
// the case.
|
|
if (
|
|
typeof event === 'object' &&
|
|
Object.prototype.hasOwnProperty.call(event, 'tableId') &&
|
|
event.tableId !== this.table.id
|
|
) {
|
|
return
|
|
}
|
|
|
|
const includeFieldOptions =
|
|
typeof event === 'object' ? event.includeFieldOptions : false
|
|
|
|
const fieldsToRefresh =
|
|
typeof event === 'object' && event.newField
|
|
? [...this.fields, event.newField]
|
|
: this.fields
|
|
|
|
this.viewLoading = true
|
|
const type = this.$registry.get('view', this.view.type)
|
|
try {
|
|
await type.refresh(
|
|
{ store: this.$store, app: this },
|
|
this.database,
|
|
this.view,
|
|
fieldsToRefresh,
|
|
this.storePrefix,
|
|
includeFieldOptions,
|
|
event?.sourceEvent
|
|
)
|
|
} catch (error) {
|
|
if (error instanceof RefreshCancelledError) {
|
|
// Multiple refresh calls have been made and the view has indicated that
|
|
// this particular one should be cancelled. However we do not want to
|
|
// set viewLoading back to false as the other non cancelled call/s might
|
|
// still be loading.
|
|
return
|
|
} else {
|
|
notifyIf(error)
|
|
}
|
|
}
|
|
if (
|
|
Object.prototype.hasOwnProperty.call(this.$refs, 'view') &&
|
|
// TODO crash here can't convert undefined to object
|
|
Object.prototype.hasOwnProperty.call(this.$refs.view, 'refresh')
|
|
) {
|
|
await this.$refs.view.refresh()
|
|
}
|
|
// It might be possible that the event has a callback that needs to be called
|
|
// after the rows are refreshed. This is for example the case when a field has
|
|
// changed. In that case we want to update the field in the store after the rows
|
|
// have been refreshed to prevent incompatible values in field types.
|
|
if (event && Object.prototype.hasOwnProperty.call(event, 'callback')) {
|
|
await event.callback()
|
|
}
|
|
this.$nextTick(() => {
|
|
this.viewLoading = false
|
|
})
|
|
},
|
|
someViewHasPermission(op) {
|
|
return this.views.some((v) =>
|
|
this.$hasPermission(op, v, this.database.workspace.id)
|
|
)
|
|
},
|
|
},
|
|
}
|
|
</script>
|