diff --git a/packages/nc-gui/assets/nc-icons/link.svg b/packages/nc-gui/assets/nc-icons/link.svg new file mode 100644 index 0000000000..1207f4b97f --- /dev/null +++ b/packages/nc-gui/assets/nc-icons/link.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/nc-gui/assets/style.scss b/packages/nc-gui/assets/style.scss index 1986dd926c..5dee33bf42 100644 --- a/packages/nc-gui/assets/style.scss +++ b/packages/nc-gui/assets/style.scss @@ -33,7 +33,7 @@ } .anticon-check-circle { - @apply !relative top-[-1px] left-0; + @apply !relative top-[-1px] left-0; } html, @@ -580,10 +580,6 @@ input[type='number'] { @apply !block; } -.ant-card-body { - @apply !p-2; -} - .ant-pagination .ant-pagination-item-link-icon { @apply !block !py-1.5; } diff --git a/packages/nc-gui/components.d.ts b/packages/nc-gui/components.d.ts index ec39c7fb6f..0e78a9c6a6 100644 --- a/packages/nc-gui/components.d.ts +++ b/packages/nc-gui/components.d.ts @@ -159,6 +159,7 @@ declare module '@vue/runtime-core' { MdiWhatsapp: typeof import('~icons/mdi/whatsapp')['default'] MiCircleWarning: typeof import('~icons/mi/circle-warning')['default'] NcIconsInbox: typeof import('~icons/nc-icons/inbox')['default'] + PhLink: typeof import('~icons/ph/link')['default'] PhMagnifyingGlassBold: typeof import('~icons/ph/magnifying-glass-bold')['default'] PhTriangleFill: typeof import('~icons/ph/triangle-fill')['default'] RiExternalLinkLine: typeof import('~icons/ri/external-link-line')['default'] diff --git a/packages/nc-gui/components/virtual-cell/components/Header.vue b/packages/nc-gui/components/virtual-cell/components/Header.vue index da78143a2c..286eb3b9da 100644 --- a/packages/nc-gui/components/virtual-cell/components/Header.vue +++ b/packages/nc-gui/components/virtual-cell/components/Header.vue @@ -49,9 +49,9 @@ const relationMeta = computed(() => { - + diff --git a/packages/nc-gui/components/virtual-cell/components/ListChildItems.vue b/packages/nc-gui/components/virtual-cell/components/ListChildItems.vue index 766df7082d..a09db62757 100644 --- a/packages/nc-gui/components/virtual-cell/components/ListChildItems.vue +++ b/packages/nc-gui/components/virtual-cell/components/ListChildItems.vue @@ -2,7 +2,6 @@ import { type ColumnType, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk' import type { Row } from '#imports' import InboxIcon from '~icons/nc-icons/inbox' -import ColumnIcon from '~icons/nc-icons/column' import { ColumnInj, @@ -12,6 +11,7 @@ import { computed, inject, isPrimary, + onKeyStroke, ref, useLTARStoreOrThrow, useSmartsheetRowStoreOrThrow, @@ -41,6 +41,7 @@ const { unlink, isChildrenListLoading, isChildrenListLinked, + isChildrenLoading, relatedTableMeta, row, link, @@ -112,7 +113,13 @@ watch( ) watch(expandedFormDlg, () => { - loadChildrenList() + if (!expandedFormDlg.value) { + loadChildrenList() + } +}) + +onKeyStroke('Escape', () => { + vModel.value = false }) @@ -131,6 +138,7 @@ watch(expandedFormDlg, () => { :relation="relation" :linked-records="childrenListCount" :table-title="meta?.title" + :show-header="true" :related-table-title="relatedTableMeta?.title" :display-value="row.row[displayValueProp]" /> @@ -152,6 +160,7 @@ watch(expandedFormDlg, () => { @focus="isFocused = true" @blur="isFocused = false" @keydown.capture.stop + @change="childrenListPagination.page = 1" > @@ -166,28 +175,60 @@ watch(expandedFormDlg, () => { }" class="overflow-scroll nc-scrollbar-md cursor-pointer pr-1" > - + + @@ -202,10 +243,7 @@ watch(expandedFormDlg, () => {

No records are linked from table - - - {{ relatedTableMeta?.title }} - + {{ relatedTableMeta?.title }}

{
-
+
{{ childrenListCount || 0 }} records {{ childrenListCount !== 0 ? 'are' : '' }} linked
-
+
{{ state?.[colTitle]?.length || 0 }} records {{ state?.[colTitle]?.length !== 0 ? 'are' : '' }} linked
@@ -238,8 +276,8 @@ watch(expandedFormDlg, () => { show-less-items />
-
- Cancel +
+ Finish { -
- +
+ -
+
-
+
- {{ row[relatedTableDisplayValueProp] }} - {{ row[relatedTableDisplayValueProp] }} +
+ > + + Linked +
0 && !isPublic && !isForm" class="flex flex-row gap-4 w-10/12"> +
{ v-if="!isForm && !isPublic" type="text" size="lg" - class="!px-2 nc-expand-item !group-hover:block !hidden !absolute right-1 bottom-1" + class="!px-2 nc-expand-item !group-hover:block !hidden !border-1 !shadow-sm !border-gray-200 !bg-white !absolute right-3 bottom-3" + :class="{ + '!group-hover:right-1.8 !group-hover:bottom-1.7': fields.length === 0, + }" @click.stop="$emit('expand', row)" > diff --git a/packages/nc-gui/components/virtual-cell/components/ListItems.vue b/packages/nc-gui/components/virtual-cell/components/ListItems.vue index d4a828e3ca..84d93e7a4e 100644 --- a/packages/nc-gui/components/virtual-cell/components/ListItems.vue +++ b/packages/nc-gui/components/virtual-cell/components/ListItems.vue @@ -2,13 +2,13 @@ import { RelationTypes, UITypes, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk' import InboxIcon from '~icons/nc-icons/inbox' -import ColumnIcon from '~icons/nc-icons/column' import { ColumnInj, IsPublicInj, SaveRowInj, computed, inject, + onKeyStroke, ref, useLTARStoreOrThrow, useSmartsheetRowStoreOrThrow, @@ -30,6 +30,7 @@ const { isChildrenExcludedListLinked, isChildrenExcludedListLoading, displayValueProp, + isChildrenExcludedLoading, childrenListCount, loadChildrenExcludedList, loadChildrenList, @@ -138,7 +139,13 @@ const relation = computed(() => { }) watch(expandedFormDlg, () => { - loadChildrenExcludedList(rowState.value) + if (!expandedFormDlg.value) { + loadChildrenExcludedList(rowState.value) + } +}) + +onKeyStroke('Escape', () => { + vModel.value = false }) @@ -176,6 +183,7 @@ watch(expandedFormDlg, () => { @focus="isFocused = true" @blur="isFocused = false" @keydown.capture.stop + @change="childrenExcludedListPagination.page = 1" >
@@ -202,29 +210,61 @@ watch(expandedFormDlg, () => { @@ -232,16 +272,13 @@ watch(expandedFormDlg, () => {

There are no records in table - - - {{ relatedTableMeta?.title }} - + {{ relatedTableMeta?.title }}

-
+
{{ relation === 'bt' ? (row.row[relatedTableMeta?.title] ? '1' : 0) : childrenListCount ?? 'No' }} records {{ childrenListCount !== 0 ? 'are' : '' }} linked
@@ -258,7 +295,7 @@ watch(expandedFormDlg, () => { show-less-items />
- Cancel + Finish
>([]) const isChildrenListLinked = ref>([]) const isChildrenExcludedListLoading = ref>([]) + const isChildrenExcludedLoading = ref(false) + const isChildrenExcludedListLinked = ref>([]) const newRowState = reactive({ @@ -127,6 +131,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState( const loadChildrenExcludedList = async (activeState?: any) => { if (activeState) newRowState.state = activeState try { + isChildrenExcludedLoading.value = true if (isPublic.value) { const router = useRouter() @@ -212,11 +217,14 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState( } } catch (e: any) { message.error(`${t('msg.error.failedToLoadList')}: ${await extractSdkResponseErrorMsg(e)}`) + } finally { + isChildrenExcludedLoading.value = false } } const loadChildrenList = async () => { try { + isChildrenLoading.value = true if (colOptions.value.type === 'bt') return if (!rowId.value || !column.value) return if (isPublic.value) { @@ -262,6 +270,8 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState( } } catch (e: any) { message.error(`${t('msg.error.failedToLoadChildrenList')}: ${await extractSdkResponseErrorMsg(e)}`) + } finally { + isChildrenLoading.value = false } } @@ -357,8 +367,11 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState( } catch (e: any) { message.error(`${t('msg.error.unlinkFailed')}: ${await extractSdkResponseErrorMsg(e)}`) } finally { - isChildrenExcludedListLoading.value[index] = false - isChildrenListLoading.value[index] = false + // To Keep the Loading State for Minimum 600ms + setTimeout(() => { + isChildrenExcludedListLoading.value[index] = false + isChildrenListLoading.value[index] = false + }, 600) } reloadData?.(false) @@ -422,8 +435,12 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState( } catch (e: any) { message.error(`Linking failed: ${await extractSdkResponseErrorMsg(e)}`) } finally { - isChildrenExcludedListLoading.value[index] = false - isChildrenListLoading.value[index] = false + // To Keep the Loading State for Minimum 600ms + + setTimeout(() => { + isChildrenExcludedListLoading.value[index] = false + isChildrenListLoading.value[index] = false + }, 600) } reloadData?.(false) @@ -466,6 +483,8 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState( isChildrenListLoading, isChildrenExcludedListLoading, row, + isChildrenLoading, + isChildrenExcludedLoading, deleteRelatedRow, getRelatedTableRowId, } diff --git a/tests/playwright/pages/Dashboard/Grid/Column/LTAR/LinkRecord.ts b/tests/playwright/pages/Dashboard/Grid/Column/LTAR/LinkRecord.ts index 0a95f771af..6ea0a20344 100644 --- a/tests/playwright/pages/Dashboard/Grid/Column/LTAR/LinkRecord.ts +++ b/tests/playwright/pages/Dashboard/Grid/Column/LTAR/LinkRecord.ts @@ -24,8 +24,7 @@ export class LinkRecord extends BasePage { { const childList = linkRecord.getByTestId(`nc-excluded-list-item`); - const childCards = await childList.count(); - expect(childCards).toEqual(cardTitle.length); + expect.poll(() => linkRecord.getByTestId(`nc-excluded-list-item`).count()).toBe(cardTitle.length); for (let i = 0; i < cardTitle.length; i++) { await childList.nth(i).locator('.nc-display-value').scrollIntoViewIfNeeded(); await childList.nth(i).locator('.nc-display-value').waitFor({ state: 'visible' }); diff --git a/tests/playwright/pages/Dashboard/common/Cell/index.ts b/tests/playwright/pages/Dashboard/common/Cell/index.ts index db02bb0f1a..f04090517b 100644 --- a/tests/playwright/pages/Dashboard/common/Cell/index.ts +++ b/tests/playwright/pages/Dashboard/common/Cell/index.ts @@ -341,8 +341,7 @@ export class CellPageObject extends BasePage { await this.rootPage.waitForSelector('.nc-modal-child-list:visible'); // verify child list count & contents - const childList = this.rootPage.locator('.ant-card:visible'); - expect(await childList.count()).toBe(count); + 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(); @@ -364,14 +363,21 @@ export class CellPageObject extends BasePage { // For HM/MM columns else { await cell.locator('.nc-datatype-link').click(); + await this.rootPage + .locator(`[data-testid="nc-child-list-item"]`) + .last() + .waitFor({ state: 'visible', timeout: 3000 }); + await this.waitForResponse({ uiAction: async () => - this.rootPage.locator(`[data-testid="nc-child-list-item"]`).last().click({ - force: true, - }), + await this.rootPage + .locator(`[data-testid="nc-child-list-item"]`) + .last() + .click({ force: true, timeout: 3000 }), requestUrlPathToMatch: '/api/v1/db/data/noco/', httpMethodsToMatch: ['GET'], }); + await this.rootPage.keyboard.press('Escape'); } } diff --git a/tests/playwright/pages/SharedForm/index.ts b/tests/playwright/pages/SharedForm/index.ts index 756e08319e..cb2ce79ca3 100644 --- a/tests/playwright/pages/SharedForm/index.ts +++ b/tests/playwright/pages/SharedForm/index.ts @@ -58,8 +58,7 @@ export class SharedFormPage extends BasePage { { const childList = linkRecord.locator(`.ant-card`); - const childCards = await childList.count(); - expect(childCards).toEqual(cardTitle.length); + expect.poll(() => linkRecord.locator(`.ant-card`).count()).toBe(cardTitle.length); for (let i = 0; i < cardTitle.length; i++) { expect(await childList.nth(i).textContent()).toContain(cardTitle[i]); }