diff --git a/src/store/data.js b/src/store/data.js index 470bd9dc6c..ff5b41ccbd 100644 --- a/src/store/data.js +++ b/src/store/data.js @@ -47,6 +47,10 @@ export const useDataStore = defineStore('data', { const stateId = typeof elementId === 'string' && elementId.startsWith('public-') ? elementId : genStateKey(isView, elementId) return state.rows[stateId] ?? [] }, + hasRows: (state) => (isView, elementId) => { + const stateId = typeof elementId === 'string' && elementId.startsWith('public-') ? elementId : genStateKey(isView, elementId) + return Object.hasOwn(state.rows, stateId) + }, getRelations: (state) => (columnId) => { if (state.relations[columnId] === undefined) { state.relations[columnId] = {} @@ -104,27 +108,50 @@ export const useDataStore = defineStore('data', { return columns }, + async loadViewWithColumnSettings(view) { + if (!view || Array.isArray(view.columnSettings)) { + return view + } + + const cachedView = useTablesStore().getView(parseInt(view.id)) + if (cachedView && Array.isArray(cachedView.columnSettings)) { + return { ...view, ...cachedView } + } + + try { + const res = await axios.get(generateUrl('/apps/tables/view/' + view.id)) + return { ...view, ...res.data } + } catch (e) { + displayError(e, t('tables', 'Could not load view.')) + return view + } + }, + async loadColumnsFromBE({ view, tableId }) { - let allColumns = await this.getColumnsFromBE({ tableId, viewId: view?.id }) - if (view) { - // Transform array to object for faster access - const columnSettingsMap = view.columnSettings?.reduce((acc, item) => { + const viewWithColumnSettings = await this.loadViewWithColumnSettings(view) + let allColumns = await this.getColumnsFromBE({ tableId, viewId: viewWithColumnSettings?.id }) + if (viewWithColumnSettings) { + // Meta columns aren't real DB columns, so they never come back + // from the fetch above -- append any this view has settings for. + const columnSettingsMap = viewWithColumnSettings.columnSettings?.reduce((acc, item) => { acc[item.columnId] = item return acc }, {}) ?? {} - allColumns = allColumns.concat(MetaColumns.filter(col => columnSettingsMap[col.id])) - if (view.columnSettings) { - allColumns = allColumns.sort((a, b) => { - const orderA = columnSettingsMap[a.id]?.order ?? Number.MAX_SAFE_INTEGER - const orderB = columnSettingsMap[b.id]?.order ?? Number.MAX_SAFE_INTEGER - return orderA - orderB - }) - } + + // Real columns carry their own order via viewColumnInformation; + // meta columns fall back to columnSettingsMap since they were + // just concatenated above and never went through server-side + // enhancement. + allColumns = allColumns.sort((a, b) => { + const orderA = a.viewColumnInformation?.order ?? columnSettingsMap[a.id]?.order ?? Number.MAX_SAFE_INTEGER + const orderB = b.viewColumnInformation?.order ?? columnSettingsMap[b.id]?.order ?? Number.MAX_SAFE_INTEGER + return orderA - orderB + }) } else { // no view: keep the backend-ordered result (ColumnService::findAllByTable already applies columnOrder) } - const stateId = genStateKey(!!(view?.id), view?.id ?? tableId) + const stateId = genStateKey(!!(viewWithColumnSettings?.id), viewWithColumnSettings?.id ?? tableId) this.columns[stateId] = allColumns return true }, diff --git a/src/views/ContentReferenceWidget.vue b/src/views/ContentReferenceWidget.vue index f4749867d1..661c68990d 100644 --- a/src/views/ContentReferenceWidget.vue +++ b/src/views/ContentReferenceWidget.vue @@ -18,18 +18,19 @@
@@ -54,6 +55,7 @@ import { useResizeObserver } from '@vueuse/core' import { spawnDialog } from '@nextcloud/vue/functions/dialog' import { useTablesStore } from '../store/store.js' import { useDataStore } from '../store/data.js' +import { NODE_TYPE_VIEW } from '../shared/constants.ts' export default { @@ -86,6 +88,7 @@ export default { return { searchExp: null, localRows: [], // Keep as fallback only + localViewSetting: {}, showCopyRow: false, copyPrefillData: null, rowToDelete: null, @@ -95,6 +98,11 @@ export default { }, computed: { + isView() { + return this.richObject?.type === NODE_TYPE_VIEW + || this.richObject?.type === String(NODE_TYPE_VIEW) + || this.richObject?.type === 'view' + }, tablePermissions() { return { canCreateRows: this.canCreateRowInElement(this.richObject), @@ -124,18 +132,28 @@ export default { } }, getRows() { - return this.dataStore ? this.dataStore.getRows(false, this.richObject.id) : [] + return this.dataStore ? this.dataStore.getRows(this.isView, this.richObject.id) : [] }, // Use computed property to get rows from store or richObject rows() { - // First try to get from the store const storeRows = this.getRows - if (storeRows && storeRows.length > 0) { + if (this.dataStore?.hasRows(this.isView, this.richObject.id)) { return storeRows } // Fallback to richObject rows or local rows return this.richObject?.rows || this.localRows }, + getColumns() { + return this.dataStore ? this.dataStore.getColumns(this.isView, this.richObject.id) : [] + }, + // Prefer fresh store data over the (possibly stale) richObject snapshot + columns() { + const storeColumns = this.getColumns + if (storeColumns && storeColumns.length > 0) { + return storeColumns + } + return this.richObject?.columns || [] + }, }, watch: { @@ -173,10 +191,16 @@ export default { this.tablesStore = useTablesStore() this.dataStore = useDataStore() - await this.loadRows() + await Promise.all([this.loadRows(), this.loadColumns()]) }, methods: { + // { tableId } or { viewId } payload for loadRowsFromBE + elementIdPayload() { + return this.isView + ? { viewId: this.richObject.id } + : { tableId: this.richObject.id } + }, search(searchString) { this.searchExp = (searchString !== '') ? new RegExp(searchString.trim(), 'ig') @@ -186,28 +210,24 @@ export default { const { default: CreateRow } = await import('../modules/modals/CreateRow.vue') spawnDialog(CreateRow, { showModal: true, - columns: this.richObject.columns, - isView: Boolean(this.richObject.type), + columns: this.columns, + isView: this.isView, elementId: this.richObject.id, }, async () => { // Reload rows from the backend to get the latest data - await this.dataStore.loadRowsFromBE({ - tableId: this.richObject.id, - }) + await this.dataStore.loadRowsFromBE(this.elementIdPayload()) }) }, async editRow(rowId) { const { default: EditRow } = await import('../modules/modals/EditRow.vue') spawnDialog(EditRow, { showModal: true, - columns: this.richObject.columns, + columns: this.columns, row: this.getRow(rowId), - isView: Boolean(this.richObject.type), + isView: this.isView, element: this.richObject, }, async () => { - await this.dataStore.loadRowsFromBE({ - tableId: this.richObject.id, - }) + await this.dataStore.loadRowsFromBE(this.elementIdPayload()) }) }, copyRow(rowId) { @@ -223,25 +243,36 @@ export default { async loadRows() { if (!this.dataStore) return - if (this.richObject.rows) { + // Paint from cached snapshot immediately, but it can be stale -- + // always reconcile with the backend below. + if (Array.isArray(this.richObject.rows)) { this.localRows = this.richObject.rows this.dataStore.seedRows({ - isView: Boolean(this.richObject.type), + isView: this.isView, elementId: this.richObject.id, rows: this.richObject.rows, }) - return } try { - await this.dataStore.loadRowsFromBE({ - tableId: this.richObject.id, - }) + await this.dataStore.loadRowsFromBE(this.elementIdPayload()) // No need to set local rows as the computed property will use store data } catch (error) { console.error('Error loading rows:', error) } }, + async loadColumns() { + if (!this.dataStore) return + try { + if (this.isView) { + await this.dataStore.loadColumnsFromBE({ view: this.richObject }) + } else { + await this.dataStore.loadColumnsFromBE({ tableId: this.richObject.id }) + } + } catch (error) { + console.error('Error loading columns:', error) + } + }, }, } @@ -249,14 +280,18 @@ export default { .tables-content-widget { min-height: max(50vh, 200px); - height: 50vh; + height: auto; + max-height: calc(100dvh - 40px); overflow: scroll; + overscroll-behavior: contain; + isolation: isolate; & .header { position: sticky; top: 0; inset-inline-start: 0; - z-index: 1; + z-index: 7; + background-color: var(--color-main-background); :where(.options) { position: sticky; @@ -285,8 +320,11 @@ export default { .nc-table { min-width: var(--widget-content-width); - :where(.options.row) { - display: none; + :deep(.options.row) { + height: 0 !important; + overflow: hidden !important; + margin: 0 !important; + padding: 0 !important; } :where(thead) { diff --git a/src/views/partials/SearchAndSelectOption.vue b/src/views/partials/SearchAndSelectOption.vue index 764a247a34..bdf4a1c4e6 100644 --- a/src/views/partials/SearchAndSelectOption.vue +++ b/src/views/partials/SearchAndSelectOption.vue @@ -3,10 +3,10 @@ - SPDX-License-Identifier: AGPL-3.0-or-later -->