diff --git a/src/store/data.js b/src/store/data.js
index 75fc23a92f..f17f2b1732 100644
--- a/src/store/data.js
+++ b/src/store/data.js
@@ -95,20 +95,29 @@ export const useDataStore = defineStore('data', {
async loadColumnsFromBE({ view, tableId }) {
let allColumns = await this.getColumnsFromBE({ tableId, viewId: view?.id })
if (view) {
- // Transform array to object for faster access
+ // Meta columns (e.g. id, created-by) aren't real DB columns and
+ // never come back from the backend fetch above -- append any
+ // that this view has settings for, same as before.
const columnSettingsMap = view.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
- })
- }
+
+ // Sort using each column's own viewColumnInformation (set
+ // server-side by ColumnService::enhanceColumn(), same field the
+ // public-share load path already relies on) as the primary
+ // source, falling back to columnSettingsMap for synthetic meta
+ // columns that were just concatenated above and never went
+ // through server-side enhancement. This keeps ordering correct
+ // even for callers that can't supply a full view.columnSettings
+ // array (e.g. the embedded reference widget), while preserving
+ // existing behavior for callers that can.
+ 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)
}
@@ -159,6 +168,9 @@ export const useDataStore = defineStore('data', {
this.columns[stateId].push(parseCol(res.data))
this.loading[stateId] = false
}
+
+ emit('tables:column:created', { isView, elementId, column: res.data })
+
return true
},
@@ -181,6 +193,8 @@ export const useDataStore = defineStore('data', {
this.columns[stateId][index] = parseCol(col)
}
+ emit('tables:column:updated', { isView, elementId, column: res.data })
+
return true
},
@@ -198,6 +212,8 @@ export const useDataStore = defineStore('data', {
this.columns[stateId] = filteredColumns
}
+ emit('tables:column:deleted', { isView, elementId, columnId: id })
+
return true
},
@@ -300,6 +316,8 @@ export const useDataStore = defineStore('data', {
await this.removeRowIfNotInView({ rowId: row?.id, viewId, stateId })
}
+ emit('tables:row:updated', { isView, elementId, row: res.data })
+
return true
},
@@ -340,6 +358,8 @@ export const useDataStore = defineStore('data', {
await this.removeRowIfNotInView({ rowId: row?.id, viewId, stateId })
}
+ emit('tables:row:created', { isView: !!viewId, elementId: viewId ?? tableId, row: res?.data?.ocs?.data })
+
return true
},
@@ -386,6 +406,9 @@ export const useDataStore = defineStore('data', {
const filteredRows = this.rows[stateId].filter(r => r.id !== rowId)
this.rows[stateId] = filteredRows
}
+
+ emit('tables:row:deleted', { isView, elementId, rowId })
+
return true
},
diff --git a/src/views/ContentReferenceWidget.vue b/src/views/ContentReferenceWidget.vue
index f4749867d1..0f9eac20fe 100644
--- a/src/views/ContentReferenceWidget.vue
+++ b/src/views/ContentReferenceWidget.vue
@@ -6,7 +6,7 @@
-
@@ -52,9 +52,24 @@ import permissionsMixin from '../shared/components/ncTable/mixins/permissionsMix
import { NcLoadingIcon } from '@nextcloud/vue'
import { useResizeObserver } from '@vueuse/core'
import { spawnDialog } from '@nextcloud/vue/functions/dialog'
+import { subscribe, unsubscribe } from '@nextcloud/event-bus'
import { useTablesStore } from '../store/store.js'
import { useDataStore } from '../store/data.js'
+// Row/column CRUD events emitted by data.js -- both the main Tables app and
+// this widget go through the same data.js actions (even though they run on
+// separate Pinia instances), so listening here gives real-time updates
+// whenever a row or column changes anywhere: from the main app's grid, from
+// another copy of this widget, etc. -- without polling or requiring a
+// manual refresh.
+// NOTE: these are past-tense ("created"/"updated"/"deleted") deliberately --
+// TableView.vue already emits 'tables:row:create'/'tables:row:delete'/
+// 'tables:column:create'/'tables:column:delete' as imperative "open the
+// modal" signals when the user clicks a button, with a different payload
+// shape. Don't reuse those names here.
+const ROW_EVENTS = ['tables:row:created', 'tables:row:updated', 'tables:row:deleted']
+const COLUMN_EVENTS = ['tables:column:created', 'tables:column:updated', 'tables:column:deleted']
+
export default {
components: {
@@ -85,16 +100,22 @@ export default {
data() {
return {
searchExp: null,
- localRows: [], // Keep as fallback only
showCopyRow: false,
copyPrefillData: null,
rowToDelete: null,
tablesStore: null,
dataStore: null,
+ // True once the initial backend fetch for rows+columns has completed.
+ // Nothing in this component reads richObject.rows/richObject.columns
+ // as data any more -- they're only ever used for id/type/title/emoji.
+ loaded: false,
}
},
computed: {
+ isView() {
+ return Boolean(this.richObject?.type)
+ },
tablePermissions() {
return {
canCreateRows: this.canCreateRowInElement(this.richObject),
@@ -123,41 +144,27 @@ export default {
return this.rows
}
},
- getRows() {
- return this.dataStore ? this.dataStore.getRows(false, this.richObject.id) : []
- },
- // Use computed property to get rows from store or richObject
+ // Store is the ONLY source of truth for rows/columns. No fallback to
+ // richObject.rows / richObject.columns -- those are just a snapshot frozen
+ // at reference-resolution time and can be arbitrarily stale (old column
+ // order, missing newly created rows, etc.).
rows() {
- // First try to get from the store
- const storeRows = this.getRows
- if (storeRows && storeRows.length > 0) {
- return storeRows
- }
- // Fallback to richObject rows or local rows
- return this.richObject?.rows || this.localRows
+ return this.dataStore ? this.dataStore.getRows(this.isView, this.richObject.id) : []
+ },
+ columns() {
+ return this.dataStore ? this.dataStore.getColumns(this.isView, this.richObject.id) : []
},
},
watch: {
+ // Covers "refresh": if the parent re-resolves the reference and swaps in
+ // a new richObject (same or different id) without remounting this
+ // component, refetch from the backend rather than trusting whatever
+ // richObject now contains.
richObject: {
deep: true,
- handler(newVal) {
- if (newVal && newVal.rows && this.localRows !== newVal.rows) {
- this.localRows = newVal.rows
- }
- },
- },
- rows: {
- deep: true,
- handler(newRows) {
- if (this.richObject && newRows) {
- /* eslint-disable vue/no-mutating-props */
- this.richObject.rows = newRows
- this.richObject.rowsCount = newRows.length
- /* eslint-enable vue/no-mutating-props */
- }
- // Force update of filteredRows when rows change
- this.search(this.searchExp ? this.searchExp.source : '')
+ handler() {
+ this.reload()
},
},
},
@@ -166,17 +173,50 @@ export default {
useResizeObserver(this.$el, (entries) => {
const entry = entries[0]
const { width } = entry.contentRect
- // In Vue 3 $el can be a fragment/comment node (no style), so guard it.
- this.$el?.style?.setProperty?.('--widget-content-width', `${width}px`)
+ this.$el.style.setProperty('--widget-content-width', `${width}px`)
})
this.tablesStore = useTablesStore()
this.dataStore = useDataStore()
- await this.loadRows()
+ ROW_EVENTS.forEach(event => subscribe(event, this.onRowChanged))
+ COLUMN_EVENTS.forEach(event => subscribe(event, this.onColumnChanged))
+
+ await this.reload()
+ },
+
+ // NOTE: if this component runs under Vue 3's Options API, rename this hook
+ // to `unmounted()`. Left as `beforeDestroy` to match the rest of this file's
+ // Vue 2-style lifecycle usage -- adjust if your build is Vue 3.
+ beforeDestroy() {
+ ROW_EVENTS.forEach(event => unsubscribe(event, this.onRowChanged))
+ COLUMN_EVENTS.forEach(event => unsubscribe(event, this.onColumnChanged))
},
methods: {
+ // Builds the { tableId } or { viewId } payload loadRowsFromBE expects,
+ // based on whether richObject is a table or a view.
+ elementIdPayload() {
+ return this.isView
+ ? { viewId: this.richObject.id }
+ : { tableId: this.richObject.id }
+ },
+ // True if an emitted row/column event refers to this exact table/view.
+ matchesThisElement(payload) {
+ return !!payload
+ && payload.isView === this.isView
+ && String(payload.elementId) === String(this.richObject.id)
+ },
+ onRowChanged(payload) {
+ if (this.matchesThisElement(payload)) {
+ this.loadRows()
+ }
+ },
+ onColumnChanged(payload) {
+ if (this.matchesThisElement(payload)) {
+ this.loadColumns()
+ }
+ },
search(searchString) {
this.searchExp = (searchString !== '')
? new RegExp(searchString.trim(), 'ig')
@@ -186,28 +226,26 @@ 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,
- })
+ // Reload rows from the backend to get the latest data. (data.js's
+ // insertNewRow also emits tables:row:created, so onRowChanged
+ // above will fire this again too -- harmless, just a duplicate fetch.)
+ 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) {
@@ -222,26 +260,29 @@ export default {
},
async loadRows() {
if (!this.dataStore) return
-
- if (this.richObject.rows) {
- this.localRows = this.richObject.rows
- this.dataStore.seedRows({
- isView: Boolean(this.richObject.type),
- elementId: this.richObject.id,
- rows: this.richObject.rows,
- })
- return
- }
-
try {
- await this.dataStore.loadRowsFromBE({
- tableId: this.richObject.id,
- })
- // No need to set local rows as the computed property will use store data
+ await this.dataStore.loadRowsFromBE(this.elementIdPayload())
} 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)
+ }
+ },
+ async reload() {
+ this.loaded = false
+ await Promise.all([this.loadRows(), this.loadColumns()])
+ this.loaded = true
+ },
},
}
@@ -249,14 +290,18 @@ export default {
.tables-content-widget {
min-height: max(50vh, 200px);
- height: 50vh;
+ height: 60vh;
+ 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 +330,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) {