1
0
mirror of https://gitlab.com/bramw/baserow.git synced 2024-11-24 16:36:46 +00:00
bramw_baserow/web-frontend/modules/database/components/table/Table.vue
2024-10-14 06:59:49 +00:00

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>