diff --git a/packages/nc-gui/components/cell/Email.vue b/packages/nc-gui/components/cell/Email.vue index 65ec91122e..d8831e794d 100644 --- a/packages/nc-gui/components/cell/Email.vue +++ b/packages/nc-gui/components/cell/Email.vue @@ -109,7 +109,7 @@ watch( gridWrapper: Ref }) => { - const { eventBus } = useSmartsheetStoreOrThrow() + const { eventBus, isDefaultView, meta } = useSmartsheetStoreOrThrow() const { addUndo, defineViewScope } = useUndoRedo() const { activeView } = storeToRefs(useViewsStore()) @@ -22,6 +22,24 @@ export const useColumnDrag = ({ const dragColPlaceholderDomRef = ref(null) const toBeDroppedColId = ref(null) + const updateDefaultViewColumnOrder = (columnId: string, order: number) => { + if (!meta.value?.columns) return + + const colIndex = meta.value.columns.findIndex((c) => c.id === columnId) + if (colIndex !== -1) { + meta.value.columns[colIndex].meta = { ...(meta.value.columns[colIndex].meta || {}), defaultViewColOrder: order } + meta.value.columns = (meta.value.columns || []).map((c) => { + if (c.id !== columnId) return c + + c.meta = { ...(c.meta || {}), defaultViewColOrder: order } + return c + }) + } + if (meta.value.columnsById[columnId]) { + meta.value.columnsById[columnId].meta = { ...(meta.value.columnsById[columnId] || {}), defaultViewColOrder: order } + } + } + const reorderColumn = async (colId: string, toColId: string) => { const toBeReorderedViewCol = gridViewCols.value[colId] @@ -46,12 +64,19 @@ export const useColumnDrag = ({ toBeReorderedViewCol.order = newOrder + if (isDefaultView.value && toBeReorderedViewCol.fk_column_id) { + updateDefaultViewColumnOrder(toBeReorderedViewCol.fk_column_id, newOrder) + } + addUndo({ undo: { fn: async () => { if (!fields.value) return toBeReorderedViewCol.order = oldOrder + if (isDefaultView.value) { + updateDefaultViewColumnOrder(toBeReorderedViewCol.fk_column_id, oldOrder) + } await updateGridViewColumn(colId, { order: oldOrder } as any) eventBus.emit(SmartsheetStoreEvents.FIELD_RELOAD) @@ -63,6 +88,9 @@ export const useColumnDrag = ({ if (!fields.value) return toBeReorderedViewCol.order = newOrder + if (isDefaultView.value) { + updateDefaultViewColumnOrder(toBeReorderedViewCol.fk_column_id, newOrder) + } await updateGridViewColumn(colId, { order: newOrder } as any) eventBus.emit(SmartsheetStoreEvents.FIELD_RELOAD) diff --git a/packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue b/packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue index d72330aefa..17ce92c284 100644 --- a/packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue +++ b/packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue @@ -54,7 +54,7 @@ const { toggleFieldVisibility, } = useViewColumnsOrThrow() -const { eventBus } = useSmartsheetStoreOrThrow() +const { eventBus, isDefaultView } = useSmartsheetStoreOrThrow() const { addUndo, defineViewScope } = useUndoRedo() @@ -127,7 +127,7 @@ const onMove = async (_event: { moved: { newIndex: number; oldIndex: number } }, fields.value.map(async (field, index) => { if (field.order !== index + 1) { field.order = index + 1 - await saveOrUpdate(field, index, true) + await saveOrUpdate(field, index, true, !!isDefaultView.value) } }), ) diff --git a/packages/nc-gui/components/tabs/Smartsheet.vue b/packages/nc-gui/components/tabs/Smartsheet.vue index d35c300cf4..edc64d18fc 100644 --- a/packages/nc-gui/components/tabs/Smartsheet.vue +++ b/packages/nc-gui/components/tabs/Smartsheet.vue @@ -21,6 +21,7 @@ import { provide, ref, toRef, + useExpandedFormDetachedProvider, useMetas, useProvideCalendarViewStore, useProvideKanbanViewStore, @@ -83,6 +84,7 @@ provide( ReadonlyInj, computed(() => !isUIAllowed('dataEdit')), ) +useExpandedFormDetachedProvider() useProvideViewColumns(activeView, meta, () => reloadViewDataEventHook?.trigger()) useProvideViewGroupBy(activeView, meta, xWhere) diff --git a/packages/nc-gui/components/virtual-cell/BelongsTo.vue b/packages/nc-gui/components/virtual-cell/BelongsTo.vue index 6604907b84..ef79796c27 100644 --- a/packages/nc-gui/components/virtual-cell/BelongsTo.vue +++ b/packages/nc-gui/components/virtual-cell/BelongsTo.vue @@ -40,6 +40,8 @@ const { isUIAllowed } = useRoles() const listItemsDlg = ref(false) +const isOpen = ref(false) + const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow() const { relatedTableMeta, loadRelatedTableMeta, relatedTableDisplayValueProp, relatedTableDisplayValuePropId, unlink } = @@ -47,8 +49,6 @@ const { relatedTableMeta, loadRelatedTableMeta, relatedTableDisplayValueProp, re await loadRelatedTableMeta() -const addIcon = computed(() => (cellValue?.value ? 'expand' : 'plus')) - const value = computed(() => { if (cellValue?.value) { return cellValue?.value @@ -80,53 +80,70 @@ const belongsToColumn = computed( relatedTableMeta.value?.columns?.find((c: any) => c.title === relatedTableDisplayValueProp.value) as ColumnType | undefined, ) -const plusBtnRef = ref(null) +watch(listItemsDlg, () => { + isOpen.value = listItemsDlg.value +}) + +// When isOpen is false, ensure the listItemsDlg is also closed. +watch( + isOpen, + (next) => { + if (!next) { + listItemsDlg.value = false + } + }, + { flush: 'post' }, +) -watch([listItemsDlg], () => { - if (!listItemsDlg.value) { - plusBtnRef.value?.focus() +watch(value, (next) => { + if (next) { + isOpen.value = false } }) diff --git a/packages/nc-gui/components/virtual-cell/HasMany.vue b/packages/nc-gui/components/virtual-cell/HasMany.vue index 5ce0888c06..707ef50804 100644 --- a/packages/nc-gui/components/virtual-cell/HasMany.vue +++ b/packages/nc-gui/components/virtual-cell/HasMany.vue @@ -37,6 +37,10 @@ const listItemsDlg = ref(false) const childListDlg = ref(false) +const isOpen = ref(false) + +const hideBackBtn = ref(false) + const { isUIAllowed } = useRoles() const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow() @@ -85,6 +89,31 @@ const hasManyColumn = computed( const onAttachRecord = () => { childListDlg.value = false listItemsDlg.value = true + hideBackBtn.value = false +} + +const onAttachLinkedRecord = () => { + listItemsDlg.value = false + childListDlg.value = true +} + +const openChildList = () => { + if (isUnderLookup.value) return + + childListDlg.value = true + listItemsDlg.value = false + + isOpen.value = true + hideBackBtn.value = false +} + +const openListDlg = () => { + if (isUnderLookup.value) return + + listItemsDlg.value = true + childListDlg.value = false + isOpen.value = true + hideBackBtn.value = true } useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEvent) => { @@ -95,53 +124,75 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEven break } }) + +watch([childListDlg, listItemsDlg], () => { + isOpen.value = childListDlg.value || listItemsDlg.value +}) + +watch( + isOpen, + (next) => { + if (!next) { + listItemsDlg.value = false + childListDlg.value = false + } + }, + { flush: 'post' }, +) + diff --git a/packages/nc-gui/components/virtual-cell/components/LinkedItems.vue b/packages/nc-gui/components/virtual-cell/components/LinkedItems.vue index 5762c2c0d6..6be91be7a1 100644 --- a/packages/nc-gui/components/virtual-cell/components/LinkedItems.vue +++ b/packages/nc-gui/components/virtual-cell/components/LinkedItems.vue @@ -1,5 +1,6 @@ diff --git a/packages/nc-gui/components/virtual-cell/components/ListItem.vue b/packages/nc-gui/components/virtual-cell/components/ListItem.vue index c4b210e10a..26d1798ea4 100644 --- a/packages/nc-gui/components/virtual-cell/components/ListItem.vue +++ b/packages/nc-gui/components/virtual-cell/components/ListItem.vue @@ -16,19 +16,23 @@ import { useVModel, } from '#imports' import MaximizeIcon from '~icons/nc-icons/maximize' -import LinkIcon from '~icons/nc-icons/link' -const props = defineProps<{ - row: any - fields: any[] - attachment: any - relatedTableDisplayValueProp: string - displayValueTypeAndFormatProp: { type: string; format: string } - isLoading: boolean - isLinked: boolean -}>() +const props = withDefaults( + defineProps<{ + row: any + fields: any[] + attachment: any + relatedTableDisplayValueProp: string + displayValueTypeAndFormatProp: { type: string; format: string } + isLoading: boolean + isLinked: boolean + }>(), + { + isLoading: false, + }, +) -defineEmits(['expand']) +defineEmits(['expand', 'linkOrUnlink']) provide(IsExpandedFormOpenInj, ref(true)) @@ -88,116 +92,198 @@ const displayValue = computed(() => { + diff --git a/packages/nc-gui/composables/useExpandedFormDetached/index.ts b/packages/nc-gui/composables/useExpandedFormDetached/index.ts index 6dfc6b6fa7..f7cfe33b10 100644 --- a/packages/nc-gui/composables/useExpandedFormDetached/index.ts +++ b/packages/nc-gui/composables/useExpandedFormDetached/index.ts @@ -19,6 +19,8 @@ const [setup, use] = useInjectionState(() => { return ref([]) }) +export { setup as useExpandedFormDetachedProvider } + export function useExpandedFormDetached() { let states = use()! diff --git a/packages/nc-gui/composables/useLTARStore.ts b/packages/nc-gui/composables/useLTARStore.ts index 4ece4be094..45d7ee0b8a 100644 --- a/packages/nc-gui/composables/useLTARStore.ts +++ b/packages/nc-gui/composables/useLTARStore.ts @@ -12,9 +12,11 @@ import { IsPublicInj, Modal, NOCO, + NcErrorType, SharedViewPasswordInj, computed, extractSdkResponseErrorMsg, + extractSdkResponseErrorMsgv2, inject, message, parseProp, @@ -188,15 +190,16 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState( return row.value.row[displayValueProp.value] }) - const loadChildrenExcludedList = async (activeState?: any) => { + const loadChildrenExcludedList = async (activeState?: any, resetOffset: boolean = false) => { if (activeState) newRowState.state = activeState try { let offset = childrenExcludedListPagination.size * (childrenExcludedListPagination.page - 1) - childrenExcludedOffsetCount.value - if (offset < 0) { + if (offset < 0 || resetOffset) { offset = 0 childrenExcludedOffsetCount.value = 0 + childrenExcludedListPagination.page = 1 } isChildrenExcludedLoading.value = true if (isPublic.value) { @@ -284,27 +287,29 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState( } } catch (e: any) { // temporary fix to handle when offset is beyond limit - if ((await extractSdkResponseErrorMsg(e)) === 'Offset is beyond the total number of records') { + const error = await extractSdkResponseErrorMsgv2(e) + + if (error.error === NcErrorType.INVALID_OFFSET_VALUE) { childrenExcludedListPagination.page = 0 - return loadChildrenExcludedList(activeState) + return loadChildrenExcludedList(activeState, true) } - message.error(`${t('msg.error.failedToLoadList')}: ${await extractSdkResponseErrorMsg(e)}`) + message.error(`${t('msg.error.failedToLoadList')}: ${error.message}`) } finally { isChildrenExcludedLoading.value = false } } - const loadChildrenList = async () => { + const loadChildrenList = async (resetOffset: boolean = false) => { try { isChildrenLoading.value = true if ([RelationTypes.BELONGS_TO, RelationTypes.ONE_TO_ONE].includes(colOptions.value.type)) return if (!rowId.value || !column.value) return let offset = childrenListPagination.size * (childrenListPagination.page - 1) + childrenListOffsetCount.value - - if (offset < 0) { + if (offset < 0 || resetOffset) { offset = 0 childrenListOffsetCount.value = 0 + childrenListPagination.page = 1 } else if (offset >= childrenListCount.value) { offset = 0 } @@ -347,6 +352,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState( isChildrenListLinked.value[index] = true isChildrenListLoading.value[index] = false }) + if (!childrenListPagination.query) { childrenListCount.value = childrenList.value?.pageInfo.totalRows ?? 0 } diff --git a/packages/nc-gui/composables/useSmartsheetStore.ts b/packages/nc-gui/composables/useSmartsheetStore.ts index 48be07d8be..38f8a97e67 100644 --- a/packages/nc-gui/composables/useSmartsheetStore.ts +++ b/packages/nc-gui/composables/useSmartsheetStore.ts @@ -38,6 +38,7 @@ const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState( const isKanban = computed(() => view.value?.type === ViewTypes.KANBAN) const isMap = computed(() => view.value?.type === ViewTypes.MAP) const isSharedForm = computed(() => isForm.value && shared) + const isDefaultView = computed(() => view.value?.is_default) const xWhere = computed(() => { let where const col = @@ -100,6 +101,7 @@ const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState( eventBus, sqlUi, allFilters, + isDefaultView, } }, 'smartsheet-store', diff --git a/packages/nc-gui/composables/useViewColumns.ts b/packages/nc-gui/composables/useViewColumns.ts index edaf8adb19..ffcffd867b 100644 --- a/packages/nc-gui/composables/useViewColumns.ts +++ b/packages/nc-gui/composables/useViewColumns.ts @@ -159,7 +159,12 @@ const [useProvideViewColumns, useViewColumns] = useInjectionState( $e('a:fields:show-all') } - const saveOrUpdate = async (field: any, index: number, disableDataReload: boolean = false) => { + const saveOrUpdate = async ( + field: any, + index: number, + disableDataReload: boolean = false, + updateDefaultViewColumnOrder: boolean = false, + ) => { if (isLocalMode.value && fields.value) { fields.value[index] = field meta.value!.columns = meta.value!.columns?.map((column: ColumnType) => { @@ -168,6 +173,7 @@ const [useProvideViewColumns, useViewColumns] = useInjectionState( ...column, ...field, id: field.fk_column_id, + ...(updateDefaultViewColumnOrder ? { meta: { ...parseProp(column.meta), defaultViewColOrder: field.order } } : {}), } } return column diff --git a/packages/nc-gui/lang/en.json b/packages/nc-gui/lang/en.json index f94af5d57a..790bca109b 100644 --- a/packages/nc-gui/lang/en.json +++ b/packages/nc-gui/lang/en.json @@ -1181,7 +1181,7 @@ "tooltip_desc": "A single record from table ", "tooltip_desc2": " can be linked with a single record from table " }, - "clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.", + "clickLinkRecordsToAddLinkFromTable": "Looks like no records have been linked yet.", "noRecordsLinked": "No records linked", "noLinkedRecords": "No linked records", "recordsLinked": "records linked", diff --git a/packages/nocodb/src/models/Column.ts b/packages/nocodb/src/models/Column.ts index cb7911001b..1d22995207 100644 --- a/packages/nocodb/src/models/Column.ts +++ b/packages/nocodb/src/models/Column.ts @@ -486,8 +486,10 @@ export default class Column implements ColumnType { public static async list( { fk_model_id, + fk_default_view_id, }: { fk_model_id: string; + fk_default_view_id?: string; }, ncMeta = Noco.ncMeta, ): Promise { @@ -505,9 +507,27 @@ export default class Column implements ColumnType { order: 'asc', }, }); + const defaultViewColumns = fk_default_view_id + ? await View.getColumns(fk_default_view_id) + : []; + + const defaultViewColumnOrderMap = defaultViewColumns.reduce( + (acc, col) => { + acc[col.fk_column_id] = col.order; + return acc; + }, + {} as Record, + ); columnsList.forEach((column) => { column.meta = parseMetaProp(column); + + if (defaultViewColumns.length) { + column.meta = { + ...column.meta, + defaultViewColOrder: defaultViewColumnOrderMap[column.id], + }; + } }); await NocoCache.setList(CacheScope.COLUMN, [fk_model_id], columnsList); diff --git a/packages/nocodb/src/models/Model.ts b/packages/nocodb/src/models/Model.ts index 1148dbe6f5..8f62c1b173 100644 --- a/packages/nocodb/src/models/Model.ts +++ b/packages/nocodb/src/models/Model.ts @@ -65,10 +65,14 @@ export default class Model implements TableType { Object.assign(this, data); } - public async getColumns(ncMeta = Noco.ncMeta): Promise { + public async getColumns( + ncMeta = Noco.ncMeta, + defaultViewId = undefined, + ): Promise { this.columns = await Column.list( { fk_model_id: this.id, + fk_default_view_id: defaultViewId, }, ncMeta, ); @@ -391,8 +395,13 @@ export default class Model implements TableType { } if (modelData) { const m = new Model(modelData); - const columns = await m.getColumns(ncMeta); + await m.getViews(false, ncMeta); + + const defaultViewId = m.views.find((view) => view.is_default).id; + + const columns = await m.getColumns(ncMeta, defaultViewId); + m.columnsById = columns.reduce((agg, c) => ({ ...agg, [c.id]: c }), {}); return m; } diff --git a/tests/playwright/pages/Dashboard/ExpandedForm/index.ts b/tests/playwright/pages/Dashboard/ExpandedForm/index.ts index 8ad6d110a1..dad5affa5c 100644 --- a/tests/playwright/pages/Dashboard/ExpandedForm/index.ts +++ b/tests/playwright/pages/Dashboard/ExpandedForm/index.ts @@ -84,7 +84,7 @@ export class ExpandedFormPage extends BasePage { case 'belongsTo': await field.locator('.nc-virtual-cell').hover(); await field.locator('.nc-action-icon').click(); - await this.dashboard.linkRecord.select(value); + await this.dashboard.linkRecord.select(value, false); break; case 'hasMany': case 'manyToMany': diff --git a/tests/playwright/pages/Dashboard/Grid/Column/LTAR/ChildList.ts b/tests/playwright/pages/Dashboard/Grid/Column/LTAR/ChildList.ts index 012de5a532..7de4f16eb7 100644 --- a/tests/playwright/pages/Dashboard/Grid/Column/LTAR/ChildList.ts +++ b/tests/playwright/pages/Dashboard/Grid/Column/LTAR/ChildList.ts @@ -22,7 +22,7 @@ export class ChildList extends BasePage { // child list body validation (card count, card title) const cardCount = cardTitle.length; - await this.get().locator('.ant-modal-content').waitFor(); + await this.get().locator('.nc-dropdown-link-record-header').waitFor(); { let isOk = false; let count = 0; @@ -54,7 +54,8 @@ export class ChildList extends BasePage { } async close() { - await this.get().locator(`.nc-close-btn`).click(); + // await this.get().locator(`.nc-close-btn`).click(); + await this.rootPage.keyboard.press('Escape'); await this.get().waitFor({ state: 'hidden' }); } diff --git a/tests/playwright/pages/Dashboard/Grid/Column/LTAR/LinkRecord.ts b/tests/playwright/pages/Dashboard/Grid/Column/LTAR/LinkRecord.ts index 352b786063..4c31ae4eab 100644 --- a/tests/playwright/pages/Dashboard/Grid/Column/LTAR/LinkRecord.ts +++ b/tests/playwright/pages/Dashboard/Grid/Column/LTAR/LinkRecord.ts @@ -33,14 +33,22 @@ export class LinkRecord extends BasePage { } } - async select(cardTitle: string) { + async select(cardTitle: string, close = true) { await this.rootPage.waitForTimeout(100); - await this.get().locator(`.ant-card:has-text("${cardTitle}"):visible`).click(); - await this.close(); + await this.get() + .locator(`.ant-card:has-text("${cardTitle}"):visible`) + .locator('button.nc-list-item-link-unlink-btn') + .click(); + + // explicitly close dropdown (auto closes for belongs to) + if (close) { + await this.close(); + } } async close() { - await this.get().locator('.nc-close-btn').last().click(); + await this.get().getByTestId('nc-link-count-info').click(); + await this.rootPage.keyboard.press('Escape'); await this.get().last().waitFor({ state: 'hidden' }); } diff --git a/tests/playwright/pages/Dashboard/common/Cell/index.ts b/tests/playwright/pages/Dashboard/common/Cell/index.ts index 24495b8a8b..7244a28c65 100644 --- a/tests/playwright/pages/Dashboard/common/Cell/index.ts +++ b/tests/playwright/pages/Dashboard/common/Cell/index.ts @@ -347,7 +347,9 @@ export class CellPageObject extends BasePage { await expect.poll(() => this.rootPage.locator('.ant-card:visible').count()).toBe(count); // close child list - await this.rootPage.locator('.nc-modal-child-list').locator('.nc-close-btn').last().click(); + // await this.rootPage.locator('.nc-modal-child-list').locator('.nc-close-btn').last().click(); + await this.rootPage.locator('.nc-modal-child-list').getByTestId('nc-link-count-info').click(); + await this.rootPage.keyboard.press('Escape'); } } } @@ -373,12 +375,17 @@ export class CellPageObject extends BasePage { await this.waitForResponse({ uiAction: () => - this.rootPage.locator(`[data-testid="nc-child-list-item"]`).last().click({ force: true, timeout: 3000 }), + this.rootPage + .locator(`[data-testid="nc-child-list-item"]`) + .last() + .locator('button.nc-list-item-link-unlink-btn') + .click({ force: true, timeout: 3000 }), requestUrlPathToMatch: '/api/v1/db/data/noco', httpMethodsToMatch: ['GET'], }); await this.rootPage.keyboard.press('Escape'); + await this.rootPage.keyboard.press('Escape'); } } diff --git a/tests/playwright/pages/SharedForm/index.ts b/tests/playwright/pages/SharedForm/index.ts index fc3fe7115e..252beb1dc1 100644 --- a/tests/playwright/pages/SharedForm/index.ts +++ b/tests/playwright/pages/SharedForm/index.ts @@ -40,7 +40,8 @@ export class SharedFormPage extends BasePage { } async closeLinkToChildList() { - await this.get().locator('.nc-close-btn').click(); + // await this.get().locator('.nc-close-btn').click(); + await this.rootPage.keyboard.press('Escape'); } async verifyChildList(cardTitle?: string[]) { @@ -69,6 +70,9 @@ export class SharedFormPage extends BasePage { } async selectChildList(cardTitle: string) { - await this.get().locator(`.ant-card:has-text("${cardTitle}"):visible`).click(); + await this.get() + .locator(`.ant-card:has-text("${cardTitle}"):visible`) + .locator('.nc-list-item-link-unlink-btn') + .click(); } } diff --git a/tests/playwright/tests/db/columns/columnLinkToAnotherRecord.spec.ts b/tests/playwright/tests/db/columns/columnLinkToAnotherRecord.spec.ts index 9b932c6d26..ec65868aa9 100644 --- a/tests/playwright/tests/db/columns/columnLinkToAnotherRecord.spec.ts +++ b/tests/playwright/tests/db/columns/columnLinkToAnotherRecord.spec.ts @@ -90,7 +90,7 @@ test.describe('LTAR create & update', () => { // In cell insert await dashboard.grid.addNewRow({ index: 1, value: '2b' }); await dashboard.grid.cell.inCellAdd({ index: 1, columnHeader: 'Sheet1' }); - await dashboard.linkRecord.select('1b'); + await dashboard.linkRecord.select('1b', false); await dashboard.grid.cell.inCellAdd({ index: 1, columnHeader: 'Sheet1s',