Browse Source

feat: links modal revamp

pull/6360/head
DarkPhoenix2704 10 months ago
parent
commit
a988acbfc7
  1. BIN
      packages/nc-gui/assets/icons/FileIconImageBox.png
  2. 16
      packages/nc-gui/assets/nc-icons/belongsto.svg
  3. 7
      packages/nc-gui/assets/nc-icons/file.svg
  4. 16
      packages/nc-gui/assets/nc-icons/hasmany.svg
  5. 12
      packages/nc-gui/assets/nc-icons/info.svg
  6. 10
      packages/nc-gui/assets/nc-icons/manytomany.svg
  7. 6
      packages/nc-gui/assets/nc-icons/maximize.svg
  8. 8
      packages/nc-gui/assets/nc-icons/multi-file.svg
  9. 5
      packages/nc-gui/assets/nc-icons/onetoone.svg
  10. 47
      packages/nc-gui/assets/style.scss
  11. 5
      packages/nc-gui/components/smartsheet/Cell.vue
  12. 6
      packages/nc-gui/components/smartsheet/expanded-form/Header.vue
  13. 5
      packages/nc-gui/components/smartsheet/header/Cell.vue
  14. 4
      packages/nc-gui/components/smartsheet/header/VirtualCell.vue
  15. 47
      packages/nc-gui/components/virtual-cell/Links.vue
  16. 114
      packages/nc-gui/components/virtual-cell/components/Header.vue
  17. 239
      packages/nc-gui/components/virtual-cell/components/ListChildItems.vue
  18. 122
      packages/nc-gui/components/virtual-cell/components/ListItem.vue
  19. 295
      packages/nc-gui/components/virtual-cell/components/ListItems.vue
  20. 25
      packages/nc-gui/composables/useExpandedFormStore.ts
  21. 86
      packages/nc-gui/composables/useLTARStore.ts
  22. 48
      packages/nc-gui/windi.config.ts
  23. 2
      tests/playwright/pages/Dashboard/Grid/Column/LTAR/LinkRecord.ts
  24. 6
      tests/playwright/tests/db/features/undo-redo.spec.ts

BIN
packages/nc-gui/assets/icons/FileIconImageBox.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 6.2 KiB

16
packages/nc-gui/assets/nc-icons/belongsto.svg

@ -0,0 +1,16 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_222_2345)">
<path d="M13 10C11.8954 10 11 9.10457 11 8C11 6.89543 11.8954 6 13 6C14.1046 6 15 6.89543 15 8C15 9.10457 14.1046 10 13 10Z" stroke="#EDF9FF" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 10C1.89543 10 1 9.10457 1 8C1 6.89543 1.89543 6 3 6C4.10457 6 5 6.89543 5 8C5 9.10457 4.10457 10 3 10Z" stroke="#EDF9FF" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 15C5.89543 15 5 14.1046 5 13C5 11.8954 5.89543 11 7 11C8.10457 11 9 11.8954 9 13C9 14.1046 8.10457 15 7 15Z" stroke="#EDF9FF" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 5C5.89543 5 5 4.10457 5 3C5 1.89543 5.89543 1 7 1C8.10457 1 9 1.89543 9 3C9 4.10457 8.10457 5 7 5Z" stroke="#EDF9FF" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 8L11 8" stroke="#EDF9FF" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 4L11 6" stroke="#EDF9FF" stroke-width="1.33333" stroke-linecap="square" stroke-linejoin="round"/>
<path d="M9 12L11 10" stroke="#EDF9FF" stroke-width="1.33333" stroke-linecap="square" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_222_2345">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

7
packages/nc-gui/assets/nc-icons/file.svg

@ -0,0 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.33341 1.33333H4.00008C3.64646 1.33333 3.30732 1.4738 3.05727 1.72385C2.80722 1.9739 2.66675 2.31304 2.66675 2.66666V13.3333C2.66675 13.6869 2.80722 14.0261 3.05727 14.2761C3.30732 14.5262 3.64646 14.6667 4.00008 14.6667H12.0001C12.3537 14.6667 12.6928 14.5262 12.9429 14.2761C13.1929 14.0261 13.3334 13.6869 13.3334 13.3333V5.33333L9.33341 1.33333Z" stroke="#4A5268" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.6666 11.3333H5.33325" stroke="#4A5268" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.6666 8.66667H5.33325" stroke="#4A5268" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.66659 6H5.99992H5.33325" stroke="#4A5268" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.33325 1.33333V5.33333H13.3333" stroke="#4A5268" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

16
packages/nc-gui/assets/nc-icons/hasmany.svg

@ -0,0 +1,16 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_102_2604)">
<path d="M3 10C4.10457 10 5 9.10457 5 8C5 6.89543 4.10457 6 3 6C1.89543 6 1 6.89543 1 8C1 9.10457 1.89543 10 3 10Z" stroke="white" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13 10C14.1046 10 15 9.10457 15 8C15 6.89543 14.1046 6 13 6C11.8954 6 11 6.89543 11 8C11 9.10457 11.8954 10 13 10Z" stroke="white" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 15C10.1046 15 11 14.1046 11 13C11 11.8954 10.1046 11 9 11C7.89543 11 7 11.8954 7 13C7 14.1046 7.89543 15 9 15Z" stroke="white" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 5C10.1046 5 11 4.10457 11 3C11 1.89543 10.1046 1 9 1C7.89543 1 7 1.89543 7 3C7 4.10457 7.89543 5 9 5Z" stroke="white" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11 8L5 8" stroke="white" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 4L5 6" stroke="white" stroke-width="1.33333" stroke-linecap="square" stroke-linejoin="round"/>
<path d="M7 12L5 10" stroke="white" stroke-width="1.33333" stroke-linecap="square" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_102_2604">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

12
packages/nc-gui/assets/nc-icons/info.svg

@ -0,0 +1,12 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_63_136093)">
<path d="M8.00004 14.6667C11.6819 14.6667 14.6667 11.6819 14.6667 7.99999C14.6667 4.3181 11.6819 1.33333 8.00004 1.33333C4.31814 1.33333 1.33337 4.3181 1.33337 7.99999C1.33337 11.6819 4.31814 14.6667 8.00004 14.6667Z" stroke="#6A7184" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 10.6667V8" stroke="#6A7184" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 5.33333H8.00667" stroke="#6A7184" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_63_136093">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 786 B

10
packages/nc-gui/assets/nc-icons/manytomany.svg

@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 6C10.8954 6 10 5.10457 10 4C10 2.89543 10.8954 2 12 2C13.1046 2 14 2.89543 14 4C14 5.10457 13.1046 6 12 6Z" stroke="#FFEEFB" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 14C10.8954 14 10 13.1046 10 12C10 10.8954 10.8954 10 12 10C13.1046 10 14 10.8954 14 12C14 13.1046 13.1046 14 12 14Z" stroke="#FFEEFB" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 14C2.89543 14 2 13.1046 2 12C2 10.8954 2.89543 10 4 10C5.10457 10 6 10.8954 6 12C6 13.1046 5.10457 14 4 14Z" stroke="#FFEEFB" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 6C2.89543 6 2 5.10457 2 4C2 2.89543 2.89543 2 4 2C5.10457 2 6 2.89543 6 4C6 5.10457 5.10457 6 4 6Z" stroke="#FFEEFB" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.5 10.5L10.5 5.5" stroke="#FFEEFB" stroke-width="1.33333" stroke-linecap="square" stroke-linejoin="round"/>
<path d="M5.5 5.5L10.5 10.5" stroke="#FFEEFB" stroke-width="1.33333" stroke-linecap="square" stroke-linejoin="round"/>
<path d="M6 4L10 4" stroke="#FFEEFB" stroke-width="1.33333" stroke-linecap="square" stroke-linejoin="round"/>
<path d="M6 12L10 12" stroke="#FFEEFB" stroke-width="1.33333" stroke-linecap="square" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

6
packages/nc-gui/assets/nc-icons/maximize.svg

@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 14H2V10" stroke="#374151" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 14L6.66667 9.33337" stroke="#374151" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 2H14V6" stroke="#4A5268" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.9999 2L9.33325 6.66667" stroke="#4A5268" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 570 B

8
packages/nc-gui/assets/nc-icons/multi-file.svg

@ -0,0 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.25 3H5.25C4.91848 3 4.60054 3.12643 4.36612 3.35147C4.1317 3.57652 4 3.88174 4 4.2V13.8C4 14.1183 4.1317 14.4235 4.36612 14.6485C4.60054 14.8736 4.91848 15 5.25 15H12.75C13.0815 15 13.3995 14.8736 13.6339 14.6485C13.8683 14.4235 14 14.1183 14 13.8V6.6L10.25 3Z" stroke="#FC3AC6" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 13H3.25C2.91848 13 2.60054 12.8736 2.36612 12.6485C2.1317 12.4235 2 12.1183 2 11.8V2.2C2 1.88174 2.1317 1.57652 2.36612 1.35147C2.60054 1.12643 2.91848 1 3.25 1H8.25L10.5 3" stroke="#FC3AC6" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.6666 11.3333H6.33325" stroke="#FC3AC6" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.6666 8.66669H6.33325" stroke="#FC3AC6" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.66659 6H6.99992H6.33325" stroke="#FC3AC6" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.3333 3.33331V7H13.4999" stroke="#FC3AC6" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

5
packages/nc-gui/assets/nc-icons/onetoone.svg

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 10C11.8954 10 11 9.10457 11 8C11 6.89543 11.8954 6 13 6C14.1046 6 15 6.89543 15 8C15 9.10457 14.1046 10 13 10Z" stroke="#F3ECFA" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 10C1.89543 10 1 9.10457 1 8C1 6.89543 1.89543 6 3 6C4.10457 6 5 6.89543 5 8C5 9.10457 4.10457 10 3 10Z" stroke="#F3ECFA" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 8L11 8" stroke="#F3ECFA" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 631 B

47
packages/nc-gui/assets/style.scss

@ -422,7 +422,7 @@ a {
}
.ant-modal {
@apply !top-[50px];
@apply !top-[50px] !w-[700px];
}
.ant-modal-content {
@apply !p-6;
@ -542,3 +542,48 @@ a {
input[type='number'] {
@apply !outline-none !ring-0 !border-0;
}
.ant-pagination {
@apply !flex !flex-row !gap-1;
}
.ant-pagination .ant-pagination-prev {
@apply !border-1 !rounded-md !border-gray-200 transform scale-95;
}
.ant-pagination .ant-pagination-item {
@apply !bg-white !rounded-md !border-1 !scale-110 !border-gray-200 !flex !items-center !justify-center;
}
.ant-pagination .ant-pagination-item a {
@apply !no-underline !text-gray-700;
}
.ant-pagination .ant-pagination-item-active a {
@apply !text-brand-500;
}
.ant-pagination .ant-pagination-next {
@apply !border-1 !rounded-md !border-gray-200 scale-95;
}
.ant-pagination .ant-pagination-item-active {
@apply !bg-brand-50 !text-brand-500 !border-0 !scale-110;
}
.ant-pagination .ant-pagination-item-link {
@apply !flex !items-center !justify-center;
}
.ant-pagination .ant-pagination-jump-next-custom-icon .ant-pagination-item-link {
@apply !block;
}
.ant-pagination .ant-pagination-jump-prev-custom-icon .ant-pagination-item-link {
@apply !block;
}
.ant-card-body {
@apply !p-2;
}
.ant-pagination .ant-pagination-item-link-icon {
@apply !block !py-1.5;
}

5
packages/nc-gui/components/smartsheet/Cell.vue

@ -6,6 +6,7 @@ import {
ColumnInj,
EditColumnInj,
EditModeInj,
IsExpandedFormOpenInj,
IsFormInj,
IsLockedInj,
IsPublicInj,
@ -90,6 +91,8 @@ const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const isEditColumnMenu = inject(EditColumnInj, ref(false))
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))
const { currentRow } = useSmartsheetRowStoreOrThrow()
const { sqlUis } = storeToRefs(useProject())
@ -200,7 +203,7 @@ onUnmounted(() => {
class="nc-cell w-full h-full relative"
:class="[
`nc-cell-${(column?.uidt || 'default').toLowerCase()}`,
{ 'text-blue-600': isPrimary(column) && !props.virtual && !isForm },
{ 'text-brand-500': isPrimary(column) && !props.virtual && !isForm },
{ 'nc-grid-numeric-cell': isGrid && !isForm && isNumericField },
{ 'h-[40px]': !props.editEnabled && isForm && !isSurveyForm && !isAttachment(column) && !props.virtual },
]"

6
packages/nc-gui/components/smartsheet/expanded-form/Header.vue

@ -20,7 +20,7 @@ const route = useRoute()
const { meta, isSqlView } = useSmartsheetStoreOrThrow()
const { commentsDrawer, displayValue, primaryKey, save: _save, loadRow } = useExpandedFormStoreOrThrow()
const { commentsDrawer, displayValue, primaryKey, save: _save, loadRow, deleteRowById } = useExpandedFormStoreOrThrow()
const { isNew, syncLTARRefs, state } = useSmartsheetRowStoreOrThrow()
@ -72,8 +72,6 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
const showDeleteRowModal = ref(false)
const { deleteRowById } = useViewData(meta, ref(props.view))
const onDeleteRowClick = () => {
showDeleteRowModal.value = true
}
@ -81,7 +79,7 @@ const onDeleteRowClick = () => {
const onConfirmDeleteRowClick = async () => {
showDeleteRowModal.value = false
await deleteRowById(primaryKey.value)
reloadTrigger.trigger()
await reloadTrigger.trigger()
emit('cancel')
message.success('Row deleted')
}

5
packages/nc-gui/components/smartsheet/header/Cell.vue

@ -6,6 +6,7 @@ interface Props {
column: ColumnType
required?: boolean | number
hideMenu?: boolean
hideIcon?: boolean
}
const props = defineProps<Props>()
@ -53,7 +54,7 @@ const openHeaderMenu = () => {
@click.right="isDropDownOpen = !isDropDownOpen"
@click="isDropDownOpen = false"
>
<SmartsheetHeaderCellIcon v-if="column" />
<SmartsheetHeaderCellIcon v-if="column && !props.hideIcon" />
<div
v-if="column"
class="name pl-1 !truncate"
@ -103,7 +104,7 @@ const openHeaderMenu = () => {
<style scoped>
.name {
max-width: calc(100% - 40px);
max-width: calc(100% - 10px);
word-break: break-all;
}
</style>

4
packages/nc-gui/components/smartsheet/header/VirtualCell.vue

@ -22,7 +22,7 @@ import {
useUIPermission,
} from '#imports'
const props = defineProps<{ column: ColumnType; hideMenu?: boolean; required?: boolean | number }>()
const props = defineProps<{ column: ColumnType; hideMenu?: boolean; required?: boolean | number; hideIcon?: boolean }>()
const { t } = useI18n()
@ -126,7 +126,7 @@ const closeAddColumnDropdown = () => {
:class="{ 'h-full': column }"
@click.right="isDropDownOpen = !isDropDownOpen"
>
<LazySmartsheetHeaderVirtualCellIcon v-if="column" />
<LazySmartsheetHeaderVirtualCellIcon v-if="column && !props.hideIcon" />
<a-tooltip placement="bottom">
<template #title>

47
packages/nc-gui/components/virtual-cell/Links.vue

@ -46,7 +46,7 @@ const textVal = computed(() => {
const parsedValue = +value?.value || 0
if (!parsedValue) {
return 'Empty'
return 'No records linked'
} else if (parsedValue === 1) {
return `1 ${column.value?.meta?.singular || 'Link'}`
} else {
@ -84,30 +84,27 @@ const localCellValue = computed<any[]>(() => {
</script>
<template>
<div class="flex w-full items-center nc-links-wrapper" @dblclick.stop="openChildList">
<template v-if="!isForm">
<div class="block flex-shrink truncate">
<component
:is="isLocked || isUnderLookup ? 'span' : 'a'"
:title="textVal"
class="text-center pl-3 nc-datatype-link underline-transparent"
:class="{ '!text-gray-300': !value }"
@click.stop.prevent="openChildList"
>
{{ textVal }}
</component>
</div>
<div class="flex-grow" />
<div v-if="!isLocked && !isUnderLookup" class="flex justify-end gap-1 min-h-[30px] items-center">
<GeneralIcon
v-if="!readOnly && isUIAllowed('xcDatatableEditable')"
icon="plus"
class="nc-icon-transition select-none !text-xxl nc-action-icon text-gray-500/50 hover:text-gray-500 nc-plus hover:text-shadow-md"
@click.stop="listItemsDlg = true"
/>
</div>
</template>
<div class="flex w-full group items-center nc-links-wrapper" @dblclick.stop="openChildList">
<div class="block flex-shrink truncate">
<component
:is="isLocked || isUnderLookup ? 'span' : 'a'"
:title="textVal"
class="text-center pl-3 nc-datatype-link underline-transparent"
:class="{ '!text-gray-300': !value }"
@click.stop.prevent="openChildList"
>
{{ textVal }}
</component>
</div>
<div class="flex-grow" />
<div v-if="!isLocked && !isUnderLookup" class="flex justify-end hidden group-hover:flex items-center">
<MdiPlus
v-if="!readOnly && isUIAllowed('xcDatatableEditable')"
class="select-none !text-md text-gray-700 nc-action-icon nc-plus"
@click.stop="listItemsDlg = true"
/>
</div>
<LazyVirtualCellComponentsListItems v-model="listItemsDlg" :column="relatedTableDisplayColumn" />

114
packages/nc-gui/components/virtual-cell/components/Header.vue

@ -0,0 +1,114 @@
<script lang="ts" setup>
import HasManyIcon from '~icons/nc-icons/hasmany'
import ManytoManyIcon from '~icons/nc-icons/manytomany'
import OnetoOneIcon from '~icons/nc-icons/onetoone'
import BelongsToIcon from '~icons/nc-icons/belongsto'
import InfoIcon from '~icons/nc-icons/info'
import FileIcon from '~icons/nc-icons/file'
const { relation, relatedTableTitle, displayValue, showHeader, tableTitle } = defineProps<{
relation: string
showHeader?: boolean
tableTitle: string
relatedTableTitle: string
displayValue?: string
}>()
const relationMeta = computed(() => {
if (relation === 'hm') {
return {
title: 'Has Many Relation',
icon: HasManyIcon,
tooltip_desc: 'A single record from table ',
tooltip_desc2: ' can be linked with a multiple records from table ',
}
} else if (relation === 'mm') {
return {
title: 'Many to Many Relation',
icon: ManytoManyIcon,
tooltip_desc: 'Multiple records from table ',
tooltip_desc2: ' can be linked with multiple records from table ',
}
} else if (relation === 'bt') {
return {
title: 'Belongs to Relation',
icon: BelongsToIcon,
tooltip_desc: 'A single record from table ',
tooltip_desc2: ' can be linked with a record from table ',
}
} else {
return {
title: 'One to One Relation',
icon: OnetoOneIcon,
tooltip_desc: 'A single record from table ',
tooltip_desc2: ' can be linked with a single record from table ',
}
}
})
</script>
<template>
<div class="flex justify-between relative pb-2 items-center">
<h2 class="text-md font-semibold">
{{ showHeader ? 'Linked Records' : '' }}
</h2>
<div class="grid grid-cols-[1fr,auto,1fr] justify-center items-center gap-2 absolute inset-0 m-auto">
<div class="flex justify-end">
<div class="flex flex-shrink-0 rounded-md gap-1 text-brand-500 items-center bg-gray-100 px-2 py-1">
<FileIcon class="w-4 h-4" />
{{ displayValue }}
</div>
</div>
<NcTooltip class="flex-shrink-0">
<template #title> {{ relationMeta.title }} </template>
<component
:is="relationMeta.icon"
class="w-7 h-7 p-1 rounded-md"
:class="{
'!bg-orange-500': relation === 'hm',
'!bg-pink-500': relation === 'mm',
'!bg-blue-500': relation === 'bt',
}"
/>
</NcTooltip>
<div class="flex justify-start">
<div
class="flex rounded-md flex-shrink-0 gap-1 items-center px-2 py-1"
:class="{
'!bg-orange-50 !text-orange-500': relation === 'hm',
'!bg-pink-50 !text-pink-500': relation === 'mm',
'!bg-blue-50 !text-blue-500': relation === 'bt',
}"
>
<MdiFileDocumentMultipleOutline
class="w-4 h-4"
:class="{
'!text-orange-500': relation === 'hm',
'!text-pink-500': relation === 'mm',
'!text-blue-500': relation === 'bt',
}"
/>
{{ relatedTableTitle }} Records
</div>
</div>
</div>
<NcTooltip class="z-10" placement="bottom">
<template #title>
<div class="p-1">
<h1 class="text-white font-bold">{{ relationMeta.title }}</h1>
<div class="text-white">
{{ relationMeta.tooltip_desc }}
<span class="bg-gray-700 px-2 rounded-md">
{{ tableTitle }}
</span>
{{ relationMeta.tooltip_desc2 }}
<span class="bg-gray-700 px-2 rounded-md">
{{ relatedTableTitle }}
</span>
</div>
</div>
</template>
<InfoIcon class="w-4 h-4" />
</NcTooltip>
</div>
</template>

239
packages/nc-gui/components/virtual-cell/components/ListChildItems.vue

@ -1,16 +1,15 @@
<script lang="ts" setup>
import type { ColumnType } from 'nocodb-sdk'
import { type ColumnType, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
import type { Row } from '#imports'
import InboxIcon from '~icons/nc-icons/inbox'
import {
ColumnInj,
IsFormInj,
IsPublicInj,
Modal,
ReadonlyInj,
computed,
h,
iconMap,
inject,
isPrimary,
ref,
useLTARStoreOrThrow,
useSmartsheetRowStoreOrThrow,
@ -25,20 +24,24 @@ const vModel = useVModel(props, 'modelValue', emit)
const isForm = inject(IsFormInj, ref(false))
const isPublic = inject(IsPublicInj, ref(false))
const injectedColumn = inject(ColumnInj, ref())
const readonly = inject(ReadonlyInj, ref(false))
const {
childrenList,
deleteRelatedRow,
childrenListCount,
loadChildrenList,
childrenListPagination,
relatedTableDisplayValueProp,
unlink,
isChildrenListLoading,
isChildrenListLinked,
relatedTableMeta,
row,
link,
meta,
displayValueProp,
} = useLTARStoreOrThrow()
const { isNew, state, removeLTARRef } = useSmartsheetRowStoreOrThrow()
@ -53,153 +56,165 @@ watch(
{ immediate: true },
)
const unlinkRow = async (row: Record<string, any>) => {
const unlinkRow = async (row: Record<string, any>, id: number) => {
if (isNew.value) {
await removeLTARRef(row, injectedColumn?.value as ColumnType)
} else {
await unlink(row)
await loadChildrenList()
await unlink(row, {}, false, id)
}
}
const unlinkIfNewRow = async (row: Record<string, any>) => {
if (isNew.value) {
await removeLTARRef(row, injectedColumn?.value as ColumnType)
}
}
const attachmentCol = computedInject(FieldsInj, (_fields) => {
return (relatedTableMeta.value.columns ?? []).filter((col) => isAttachment(col))[0]
})
const container = computed(() =>
isForm.value
? h('div', {
class: 'w-full p-2',
})
: Modal,
)
const isFocused = ref(false)
const fields = computedInject(FieldsInj, (_fields) => {
return (relatedTableMeta.value.columns ?? [])
.filter((col) => !isSystemColumn(col) && !isPrimary(col) && !isLinksOrLTAR(col) && !isAttachment(col))
.slice(0, 4)
})
const expandedFormDlg = ref(false)
const expandedFormRow = ref()
const expandedFormRow = ref({})
const colTitle = computed(() => injectedColumn.value?.title || '')
/** reload children list whenever cell value changes and list is visible */
watch(
() => props.cellValue,
() => {
if (!isNew.value && vModel.value) loadChildrenList()
},
)
const onClick = (row: Row) => {
if (readonly.value) return
expandedFormRow.value = row
expandedFormDlg.value = true
}
const relation = computed(() => {
return injectedColumn!.value?.colOptions?.type
})
watch(expandedFormDlg, () => {
loadChildrenList()
})
</script>
<template>
<component
:is="container"
<a-modal
v-model:visible="vModel"
:class="{ active: vModel }"
:footer="null"
title="Child list"
:body-style="{ padding: 0 }"
:closable="false"
:width="840"
:body-style="{ 'padding': 0, 'margin': 0, 'min-height': '500px' }"
wrap-class-name="nc-modal-child-list"
>
<div class="py-6 nc-scrollbar-md">
<div class="flex mb-4 items-center gap-2 px-12">
<component
:is="iconMap.reload"
v-if="!isForm"
class="cursor-pointer text-gray-500"
data-testid="nc-child-list-reload"
@click="loadChildrenList"
/>
<a-button v-if="!readonly" type="primary" ghost data-testid="nc-child-list-button-link-to" @click="emit('attachRecord')">
<div class="flex items-center gap-1">
<component :is="iconMap.link" type="primary" />
Link to '
<GeneralTableIcon :meta="relatedTableMeta" class="-mx-1 w-5" />
{{ relatedTableMeta.title }}'
</div>
</a-button>
<LazyVirtualCellComponentsHeader
:relation="relation"
:linked-records="childrenListCount"
:table-title="meta?.title"
:related-table-title="relatedTableMeta?.title"
:display-value="row.row[displayValueProp]"
/>
<div class="m-4 bg-gray-50 border-gray-50 border-b-2"></div>
<div class="flex mt-2 mb-2 items-center gap-2">
<div
class="flex items-center border-1 p-1 rounded-md w-full border-gray-200"
:class="{ '!border-primary': childrenListPagination.query.length !== 0 || isFocused }"
>
<MdiMagnify class="w-5 h-5 ml-2" />
<a-input
ref="filterQueryRef"
v-model:value="childrenListPagination.query"
:placeholder="`Search in ${relatedTableMeta?.title}`"
class="w-full !rounded-md"
size="small"
:bordered="false"
@focus="isFocused = true"
@blur="isFocused = false"
@keydown.capture.stop
>
</a-input>
</div>
</div>
<template v-if="(isNew && state?.[colTitle]?.length) || childrenList?.pageInfo?.totalRows">
<div class="nc-scrollbar-md">
<div class="flex flex-col">
<div class="px-12 cursor-pointer">
<a-card
v-for="(row, i) of childrenList?.list ?? state?.[colTitle] ?? []"
:key="i"
class="nc-nested-list-item !my-2 hover:(!bg-gray-200/50 shadow-md)"
@click="onClick(row)"
>
<div class="flex items-center">
<div class="flex-1 overflow-hidden min-w-0">
<VirtualCellComponentsItemChip
:border="false"
:item="row"
:value="row[relatedTableDisplayValueProp]"
:column="props.column"
/>
</div>
<div v-if="!readonly" class="flex gap-2">
<component
:is="iconMap.linkRemove"
class="!text-base text-grey hover:(!text-red-500) cursor-pointer nc-icon-transition"
data-testid="nc-child-list-icon-unlink"
@click.stop="unlinkRow(row)"
/>
<component
:is="iconMap.delete"
v-if="!readonly && !isPublic"
class="!text-base text-grey hover:(!text-red-500) cursor-pointer nc-icon-transition"
data-testid="nc-child-list-icon-delete"
@click.stop="deleteRelatedRow(row, unlinkIfNewRow)"
/>
</div>
</div>
</a-card>
</div>
</div>
</div>
<div class="flex justify-center mt-6">
<a-pagination
v-if="!isNew && childrenList?.pageInfo"
v-model:current="childrenListPagination.page"
v-model:page-size="childrenListPagination.size"
class="mt-2 mx-auto"
size="small"
:total="+childrenList?.pageInfo.totalRows"
show-less-items
<template v-if="(isNew && state?.[colTitle]?.length) || childrenList?.pageInfo?.totalRows">
<div class="mt-2 mb-2">
<div class="h-[420px] overflow-scroll nc-scrollbar-md cursor-pointer pr-1">
<LazyVirtualCellComponentsListItem
v-for="(refRow, id) in childrenList?.list ?? state?.[colTitle] ?? []"
:key="id"
:row="refRow"
:fields="fields"
:attachment="attachmentCol"
:related-table-display-value-prop="relatedTableDisplayValueProp"
:is-linked="isChildrenListLinked[Number.parseInt(id)]"
:is-loading="isChildrenListLoading[Number.parseInt(id)]"
@expand="onClick(refRow)"
@click="
() => {
isChildrenListLinked[Number.parseInt(id)]
? unlinkRow(refRow, Number.parseInt(id))
: link(refRow, {}, false, Number.parseInt(id))
}
"
/>
</div>
</template>
</div>
</template>
<div v-else class="pt-1 h-[420px] flex flex-col items-center justify-center text-gray-500">
<InboxIcon class="w-16 h-16 mx-auto" />
<p>There are no records in table</p>
</div>
<div class="my-2 bg-gray-50 border-gray-50 border-b-2"></div>
<div v-else class="ml-12 text-gray-500">No Links</div>
<div class="flex flex-row justify-between bg-white relative pt-1">
<div class="flex items-center justify-center px-2 rounded-md text-brand-500 bg-brand-50">
{{ childrenListCount || 0 }} records {{ childrenListCount !== 0 ? 'are' : '' }} linked
</div>
<div class="flex absolute items-center py-2 justify-center w-full">
<a-pagination
v-if="!isNew && childrenList?.pageInfo"
v-model:current="childrenListPagination.page"
v-model:page-size="childrenListPagination.size"
:total="+childrenList.pageInfo.totalRows!"
:show-size-changer="false"
class="mt-2 mx-auto"
size="small"
hide-on-single-page
show-less-items
/>
</div>
<div class="flex flex-row gap-1">
<NcButton v-if="!readonly" type="ghost" data-testid="nc-child-list-button-link-to" @click="emit('attachRecord')">
<MdiPlus /> Link more records
</NcButton>
<NcButton type="primary" class="nc-close-btn" @click="vModel = false"> Close </NcButton>
</div>
</div>
<Suspense>
<LazySmartsheetExpandedForm
v-if="expandedFormRow && expandedFormDlg"
v-model="expandedFormDlg"
:row="{ row: expandedFormRow, oldRow: expandedFormRow, rowMeta: {} }"
:meta="relatedTableMeta"
load-row
:row="{
row: expandedFormRow,
oldRow: expandedFormRow,
rowMeta:
Object.keys(expandedFormRow).length > 0
? {}
: {
new: true,
},
}"
use-meta-fields
/>
</Suspense>
</component>
</a-modal>
</template>
<style scoped lang="scss">
:deep(.ant-pagination-item a) {
line-height: 21px !important;
}
:deep(.nc-nested-list-item .ant-card-body) {
@apply !px-1 !py-0;
}

122
packages/nc-gui/components/virtual-cell/components/ListItem.vue

@ -0,0 +1,122 @@
<script lang="ts" setup>
import { isVirtualCol } from 'nocodb-sdk'
import { isImage, useAttachment } from '#imports'
import MaximizeIcon from '~icons/nc-icons/maximize'
const { row, fields, relatedTableDisplayValueProp, isLoading, isLinked, attachment } = defineProps<{
row: any
fields: any[]
attachment: any
relatedTableDisplayValueProp: string
isLoading: boolean
isLinked: boolean
}>()
defineEmits(['expand'])
provide(IsExpandedFormOpenInj, ref(true))
const { getPossibleAttachmentSrc } = useAttachment()
interface Attachment {
url: string
title: string
type: string
mimetype: string
}
const attachments: Attachment[] = computed(() => {
try {
if (attachment && row[attachment.title]) {
return typeof row[attachment.title] === 'string' ? JSON.parse(row[attachment.title]) : row[attachment.title]
}
return []
} catch (e) {
return []
}
})
</script>
<template>
<a-card
class="!border-1 group transition-all !rounded-xl relative !mb-4 !border-gray-200 hover:!bg-gray-50 hover:!border-gray-400"
:class="{
'!bg-white !border-blue-500': isLoading,
'!border-brand-500 !bg-brand-50 !border-2': isLinked,
}"
:body-style="{ padding: 0 }"
:hoverable="false"
>
<div class="flex flex-row items-center gap-2 w-full">
<a-carousel
v-if="attachment && attachments && attachments.length"
autoplay
class="!w-24 border-1 bg-white border-gray-200 !rounded-md !h-24"
>
<template #customPaging> </template>
<template v-for="(attachmen, index) in attachments">
<LazyCellAttachmentImage
v-if="isImage(attachmen.title, attachmen.mimetype ?? attachmen.type)"
:key="`carousel-${attachmen.title}-${index}`"
class="!h-24 !w-24 object-cover !rounded-md"
:srcs="getPossibleAttachmentSrc(attachmen)"
/>
</template>
</a-carousel>
<div v-else-if="attachment" class="h-24 w-24 w-full !flex flex-row items-center justify-center">
<img class="object-contain h-24 w-24" src="~assets/icons/FileIconImageBox.png" />
</div>
<div class="flex flex-col gap-1 flex-grow justify-center">
<div class="flex justify-between">
<span class="font-bold text-gray-800 ml-1"> {{ row[relatedTableDisplayValueProp] }} </span>
<MdiCheck
v-if="isLinked"
:class="{
'!group-hover:mr-8': fields.length === 0,
}"
class="w-6 h-6 !text-brand-500"
/>
<MdiLoading
v-else-if="isLoading"
:class="{
'!group-hover:mr-8': fields.length === 0,
}"
class="w-6 h-6 !text-brand-500 animate-spin"
/>
</div>
<div v-if="fields.length > 0" class="flex flex-row gap-4 w-10/12">
<div v-for="field in fields" :key="field.id" :class="attachment ? 'w-1/3' : 'w-1/4'">
<div class="flex flex-col gap-[-1] max-w-72">
<LazySmartsheetHeaderVirtualCell
v-if="isVirtualCol(field)"
class="!scale-60"
:column="field"
:hide-menu="true"
:hide-icon="true"
/>
<LazySmartsheetHeaderCell v-else class="!scale-70" :column="field" :hide-menu="true" :hide-icon="true" />
<LazySmartsheetVirtualCell v-if="isVirtualCol(field)" v-model="row[field.title]" :row="row" :column="field" />
<LazySmartsheetCell
v-else
v-model="row[field.title]"
class="!text-gray-600 ml-1"
:column="field"
:edit-enabled="false"
:read-only="true"
/>
</div>
</div>
</div>
</div>
</div>
<NcButton
type="text"
size="lg"
class="!px-2 !group-hover:block !hidden !absolute right-1 bottom-1"
@click.stop="$emit('expand', row)"
>
<MaximizeIcon class="w-4 h-4" />
</NcButton>
</a-card>
</template>

295
packages/nc-gui/components/virtual-cell/components/ListItems.vue

@ -1,19 +1,15 @@
<script lang="ts" setup>
import type { Card } from 'ant-design-vue'
import { RelationTypes, UITypes } from 'nocodb-sdk'
import { RelationTypes, UITypes, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import InboxIcon from '~icons/nc-icons/inbox'
import {
ColumnInj,
Empty,
IsPublicInj,
SaveRowInj,
computed,
iconMap,
inject,
isDrawerExist,
ref,
useLTARStoreOrThrow,
useSelectedCellKeyupListener,
useSmartsheetRowStoreOrThrow,
useVModel,
} from '#imports'
@ -30,12 +26,18 @@ const filterQueryRef = ref()
const {
childrenExcludedList,
isChildrenExcludedListLinked,
isChildrenExcludedListLoading,
displayValueProp,
childrenListCount,
loadChildrenExcludedList,
loadChildrenList,
childrenExcludedListPagination,
relatedTableDisplayValueProp,
link,
relatedTableMeta,
meta,
unlink,
row,
} = useLTARStoreOrThrow()
@ -47,20 +49,14 @@ const saveRow = inject(SaveRowInj, () => {})
const selectedRowIndex = ref(0)
const isAltKeyDown = ref(false)
const isFocused = ref(false)
const linkRow = async (row: Record<string, any>) => {
childrenExcludedList.value?.list?.splice(selectedRowIndex.value, 1)
const linkRow = async (row: Record<string, any>, id: number) => {
if (isNew.value) {
addLTARRef(row, injectedColumn?.value as ColumnType)
saveRow!()
} else {
await link(row)
}
if (isAltKeyDown.value) {
if (!isNew.value) loadChildrenExcludedList()
} else {
vModel.value = false
await link(row, {}, false, id)
}
}
@ -70,6 +66,7 @@ watch(vModel, (nextVal, prevVal) => {
/** reset query and limit */
childrenExcludedListPagination.query = ''
childrenExcludedListPagination.page = 1
loadChildrenList()
loadChildrenExcludedList()
selectedRowIndex.value = 0
}
@ -77,6 +74,8 @@ watch(vModel, (nextVal, prevVal) => {
const expandedFormDlg = ref(false)
const expandedFormRow = ref({})
/** populate initial state for a new row which is parent/child of current record */
const newRowState = computed(() => {
if (isNew.value) return {}
@ -111,80 +110,22 @@ const newRowState = computed(() => {
}
})
// if it's an existing record close the list
// after new record creation since it's already linking while creating
watch(expandedFormDlg, (nexVal) => {
if (!nexVal && !isNew.value) vModel.value = false
const attachmentCol = computedInject(FieldsInj, (_fields) => {
return (relatedTableMeta.value.columns ?? []).filter((col) => isAttachment(col))[0]
})
useSelectedCellKeyupListener(vModel, (e: KeyboardEvent) => {
switch (e.key) {
case 'ArrowLeft':
e.stopPropagation()
e.preventDefault()
if (childrenExcludedListPagination.page > 1) childrenExcludedListPagination.page--
break
case 'ArrowRight':
e.stopPropagation()
e.preventDefault()
if (
childrenExcludedList.value?.pageInfo &&
childrenExcludedListPagination.page <
(childrenExcludedList.value.pageInfo.totalRows || 1) / childrenExcludedListPagination.size
)
childrenExcludedListPagination.page++
break
case 'ArrowUp':
selectedRowIndex.value = Math.max(0, selectedRowIndex.value - 1)
e.stopPropagation()
e.preventDefault()
break
case 'ArrowDown':
selectedRowIndex.value = Math.min(childrenExcludedList.value?.list?.length - 1, selectedRowIndex.value + 1)
e.stopPropagation()
e.preventDefault()
break
case 'Enter':
{
const selectedRow = childrenExcludedList.value?.list?.[selectedRowIndex.value]
if (selectedRow) {
linkRow(selectedRow)
e.stopPropagation()
e.preventDefault()
}
}
break
default: {
const el = filterQueryRef.value?.$el
if (el && !isDrawerExist()) {
filterQueryRef.value.$el.focus()
}
}
}
const fields = computedInject(FieldsInj, (_fields) => {
return (relatedTableMeta.value.columns ?? [])
.filter((col) => !isSystemColumn(col) && !isPrimary(col) && !isLinksOrLTAR(col) && !isAttachment(col))
.slice(0, 4)
})
const activeRow = (vNode?: InstanceType<typeof Card>) => {
vNode?.$el?.scrollIntoView({ block: 'nearest', inline: 'nearest' })
}
// set variable to true when alt key is pressed
const keyDownHandler = (e: KeyboardEvent) => {
isAltKeyDown.value = e.altKey
}
// set variable to false when key is released
const keyUpHandler = (e: KeyboardEvent) => {
isAltKeyDown.value = e.altKey
}
const relation = computed(() => {
return injectedColumn!.value?.colOptions?.type
})
// add event listeners when vModel is true and remove when false
watch(vModel, (nextVal) => {
if (nextVal) {
document.addEventListener('keydown', keyDownHandler)
document.addEventListener('keyup', keyUpHandler)
} else {
document.removeEventListener('keydown', keyDownHandler)
document.removeEventListener('keyup', keyUpHandler)
}
watch(expandedFormDlg, () => {
loadChildrenExcludedList()
})
</script>
@ -193,95 +134,131 @@ watch(vModel, (nextVal) => {
v-model:visible="vModel"
:class="{ active: vModel }"
:footer="null"
:title="$t('activity.linkRecord')"
:body-style="{ padding: 0 }"
:width="840"
:closable="false"
:body-style="{ 'padding': 0, 'margin': 0, 'min-height': '500px' }"
wrap-class-name="nc-modal-link-record"
>
<div class="h-[min(max(calc(100vh_-_300px)_,350px),540px)] flex flex-col py-6">
<div class="flex mb-4 items-center gap-2 px-12">
<LazyVirtualCellComponentsHeader
:relation="relation"
:linked-records="childrenListCount"
:table-title="meta?.title"
:related-table-title="relatedTableMeta?.title"
:display-value="row.row[displayValueProp]"
/>
<div class="m-4 bg-gray-50 border-gray-50 border-b-2"></div>
<div class="flex mt-2 mb-2 items-center gap-2">
<div
class="flex items-center border-1 p-1 rounded-md w-full border-gray-200"
:class="{ '!border-primary': childrenExcludedListPagination.query.length !== 0 || isFocused }"
>
<MdiMagnify class="w-5 h-5 ml-2" />
<a-input
ref="filterQueryRef"
v-model:value="childrenExcludedListPagination.query"
:placeholder="$t('placeholder.filterQuery')"
class="max-w-[200px]"
:placeholder="`Search in ${relatedTableMeta?.title}`"
class="w-full !rounded-md"
size="small"
:bordered="false"
@focus="isFocused = true"
@blur="isFocused = false"
@keydown.capture.stop
/>
<div class="flex-1" />
<component :is="iconMap.reload" class="cursor-pointer text-gray-500 nc-reload" @click="loadChildrenExcludedList" />
<!-- Add new record -->
<a-button v-if="!isPublic" type="primary" size="small" @click="expandedFormDlg = true">
{{ $t('activity.addNewRecord') }}
</a-button>
>
</a-input>
</div>
<template v-if="childrenExcludedList?.pageInfo?.totalRows">
<div class="flex-1 overflow-auto min-h-0 scrollbar-thin-dull px-12">
<a-card
v-for="(refRow, i) in childrenExcludedList?.list ?? []"
:key="i"
:ref="selectedRowIndex === i ? activeRow : null"
class="nc-nested-list-item !my-2 cursor-pointer hover:(!bg-gray-200/50 shadow-md) group"
:class="{ 'nc-selected-row': selectedRowIndex === i }"
@click="linkRow(refRow)"
>
<VirtualCellComponentsItemChip
:item="refRow"
:value="refRow[relatedTableDisplayValueProp]"
:column="props.column"
:show-unlink-button="false"
:border="false"
readonly
/>
</a-card>
</div>
<div class="flex-1" />
<!-- Add new record -->
<NcButton
v-if="!isPublic"
type="ghost"
size="xl"
class="!text-brand-500"
@click="
() => {
expandedFormRow = {}
expandedFormDlg = true
}
"
>
<MdiPlus class="w-4 h-4" />
New Record
</NcButton>
</div>
<div class="flex justify-center mt-6">
<a-pagination
v-if="childrenExcludedList?.pageInfo"
v-model:current="childrenExcludedListPagination.page"
v-model:page-size="childrenExcludedListPagination.size"
class="mt-2 !text-xs"
size="small"
:total="+childrenExcludedList.pageInfo.totalRows"
show-less-items
<template v-if="childrenExcludedList?.pageInfo?.totalRows">
<div class="pb-2 pt-1">
<div class="h-[420px] overflow-scroll nc-scrollbar-md pr-1 cursor-pointer">
<LazyVirtualCellComponentsListItem
v-for="(refRow, id) in childrenExcludedList?.list ?? []"
:key="id"
:row="refRow"
:fields="fields"
:attachment="attachmentCol"
:related-table-display-value-prop="relatedTableDisplayValueProp"
:is-loading="isChildrenExcludedListLoading[Number.parseInt(id)]"
:is-linked="isChildrenExcludedListLinked[Number.parseInt(id)]"
@expand="
() => {
expandedFormRow = refRow
expandedFormDlg = true
}
"
@click="
() => {
if (isChildrenExcludedListLinked[Number.parseInt(id)]) unlink(refRow, {}, false, Number.parseInt(id))
else linkRow(refRow, Number.parseInt(id))
}
"
/>
</div>
</div>
</template>
<div v-else class="py-2 h-[420px] flex flex-col items-center justify-center text-gray-500">
<InboxIcon class="w-16 h-16 mx-auto" />
<p>There are no records in table</p>
</div>
<div class="my-2 bg-gray-50 border-gray-50 border-b-2"></div>
<div class="text-xs text-gray-400 text-center px-2 mt-4 pb-0">
* Use <kbd>ALT</kbd> / <kbd>OPT</kbd> + <kbd>Click</kbd> to select multiple records
</div>
</template>
<a-empty v-else class="my-10" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
<Suspense>
<LazySmartsheetExpandedForm
v-if="expandedFormDlg"
v-model="expandedFormDlg"
:meta="relatedTableMeta"
:row="{ row: {}, oldRow: {}, rowMeta: { new: true } }"
:state="newRowState"
use-meta-fields
<div class="flex flex-row justify-between bg-white relative pt-1">
<div class="flex items-center justify-center px-2 rounded-md text-brand-500 bg-brand-50">
{{ relation === 'bt' ? (row.row[relatedTableMeta?.title] ? '1' : 0) : childrenListCount ?? 'No' }} records
{{ childrenListCount !== 0 ? 'are' : '' }} linked
</div>
<div class="flex absolute items-center py-2 justify-center w-full">
<a-pagination
v-if="childrenExcludedList?.pageInfo"
v-model:current="childrenExcludedListPagination.page"
v-model:page-size="childrenExcludedListPagination.size"
:total="+childrenExcludedList.pageInfo.totalRows"
:show-size-changer="false"
class="mt-2 mx-auto"
size="small"
hide-on-single-page
show-less-items
/>
</Suspense>
</div>
<NcButton class="nc-close-btn" @click="vModel = false"> Close </NcButton>
</div>
<Suspense>
<LazySmartsheetExpandedForm
v-if="expandedFormDlg"
v-model="expandedFormDlg"
:meta="relatedTableMeta"
:row="{
row: expandedFormRow,
oldRow: {},
rowMeta:
Object.keys(expandedFormRow).length > 0
? {}
: {
new: true,
},
}"
:state="newRowState"
use-meta-fields
/>
</Suspense>
</a-modal>
</template>
<style scoped>
:deep(.ant-pagination-item a) {
line-height: 21px !important;
}
:deep(.nc-selected-row) {
@apply !ring;
}
:deep(.nc-nested-list-item .ant-card-body) {
@apply !px-1 !py-0;
}
</style>

25
packages/nc-gui/composables/useExpandedFormStore.ts

@ -287,7 +287,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
NOCO,
// todo: project_id missing on view type
(project?.value?.id || (sharedView.value?.view as any)?.project_id) as string,
meta.value.id,
meta.value.id as string,
encodeURIComponent(rowId ?? extractPkFromRow(row.value.row, meta.value.columns as ColumnType[])),
{
getHiddenColumn: true,
@ -301,6 +301,28 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
})
}
const deleteRowById = async (rowId?: string) => {
try {
const res: { message?: string[] } | number = await $api.dbTableRow.delete(
NOCO,
project.value.id as string,
meta.value.id as string,
encodeURIComponent(rowId ?? extractPkFromRow(row.value.row, meta.value.columns as ColumnType[])),
)
if (res.message) {
message.info(
`Row delete failed: ${`Unable to delete row with ID ${rowId} because of the following:
\n${res.message.join('\n')}.\n
Clear the data first & try again`})}`,
)
return false
}
} catch (e: any) {
message.error(`${t('msg.error.deleteFailed')}: ${await extractSdkResponseErrorMsg(e)}`)
}
}
const updateComment = async (auditId: string, audit: Partial<AuditType>) => {
return await $api.utils.commentUpdate(auditId, audit)
}
@ -317,6 +339,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
isYou,
commentsDrawer,
row,
deleteRowById,
displayValue,
save,
changedColumns,

86
packages/nc-gui/composables/useLTARStore.ts

@ -64,6 +64,16 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
size: 10,
})
const isChildrenListLoading = ref<Array<boolean>>([])
const isChildrenListLinked = ref<Array<boolean>>([])
const isChildrenExcludedListLoading = ref<Array<boolean>>([])
const isChildrenExcludedListLinked = ref<Array<boolean>>([])
const childrenListCount = ref(0)
const { t } = useI18n()
const isPublic: Ref<boolean> = inject(IsPublicInj, ref(false))
@ -169,6 +179,10 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
} as any,
)
}
childrenExcludedList.value?.list.forEach((row: Record<string, any>, index: number) => {
isChildrenExcludedListLinked.value[index] = false
isChildrenExcludedListLoading.value[index] = false
})
} catch (e: any) {
message.error(`${t('msg.error.failedToLoadList')}: ${await extractSdkResponseErrorMsg(e)}`)
}
@ -212,6 +226,11 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
} as any,
)
}
childrenList.value?.list.forEach((row: Record<string, any>, index: number) => {
isChildrenListLinked.value[index] = true
isChildrenListLoading.value[index] = false
})
childrenListCount.value = childrenList.value?.pageInfo.totalRows ?? 0
} catch (e: any) {
message.error(`${t('msg.error.failedToLoadChildrenList')}: ${await extractSdkResponseErrorMsg(e)}`)
}
@ -254,7 +273,12 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
})
}
const unlink = async (row: Record<string, any>, { metaValue = meta.value }: { metaValue?: TableType } = {}, undo = false) => {
const unlink = async (
row: Record<string, any>,
{ metaValue = meta.value }: { metaValue?: TableType } = {},
undo = false,
index: number, // Index is For Loading and Linked State of Row
) => {
// const column = meta.columns.find(c => c.id === this.column.colOptions.fk_child_column_id);
// todo: handle if new record
// if (this.isNew) {
@ -270,6 +294,8 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
// }
try {
// todo: audit
isChildrenExcludedListLoading.value[index] = true
isChildrenListLoading.value[index] = true
await $api.dbTableRow.nestedRemove(
NOCO,
project.value.id as string,
@ -283,25 +309,38 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
if (!undo) {
addUndo({
redo: {
fn: (row: Record<string, any>) => unlink(row, {}, true),
fn: (row: Record<string, any>) => unlink(row, {}, true, index),
args: [clone(row)],
},
undo: {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
fn: (row: Record<string, any>) => link(row, {}, true),
fn: (row: Record<string, any>) => link(row, {}, true, index),
args: [clone(row)],
},
scope: defineViewScope({ view: activeView.value }),
})
}
isChildrenExcludedListLinked.value[index] = false
isChildrenListLinked.value[index] = false
if (colOptions.value.type !== 'bt') {
childrenListCount.value = childrenListCount.value - 1
}
} catch (e: any) {
message.error(`${t('msg.error.unlinkFailed')}: ${await extractSdkResponseErrorMsg(e)}`)
} finally {
isChildrenExcludedListLoading.value[index] = false
isChildrenListLoading.value[index] = false
}
reloadData?.(false)
}
const link = async (row: Record<string, any>, { metaValue = meta.value }: { metaValue?: TableType } = {}, undo = false) => {
const link = async (
row: Record<string, any>,
{ metaValue = meta.value }: { metaValue?: TableType } = {},
undo = false,
index: number,
) => {
// todo: handle new record
// const pid = this._extractRowId(parent, this.parentMeta);
// const id = this._extractRowId(this.row, this.meta);
@ -316,6 +355,9 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
// return;
// }
try {
isChildrenExcludedListLoading.value[index] = true
isChildrenListLoading.value[index] = true
await $api.dbTableRow.nestedAdd(
NOCO,
project.value.id as string,
@ -325,23 +367,34 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
column?.value?.id,
encodeURIComponent(getRelatedTableRowId(row) as string) as string,
)
await loadChildrenList()
// await loadChildrenList()
if (!undo) {
addUndo({
redo: {
fn: (row: Record<string, any>) => link(row, {}, true),
fn: (row: Record<string, any>) => link(row, {}, true, index),
args: [clone(row)],
},
undo: {
fn: (row: Record<string, any>) => unlink(row, {}, true),
fn: (row: Record<string, any>) => unlink(row, {}, true, index),
args: [clone(row)],
},
scope: defineViewScope({ view: activeView.value }),
})
}
isChildrenExcludedListLinked.value[index] = true
isChildrenListLinked.value[index] = true
if (colOptions.value.type !== 'bt') {
childrenListCount.value = childrenListCount.value + 1
} else {
isChildrenExcludedListLinked.value = Array(childrenExcludedList.value?.list.length).fill(false)
isChildrenExcludedListLinked.value[index] = true
}
} catch (e: any) {
message.error(`Linking failed: ${await extractSdkResponseErrorMsg(e)}`)
} finally {
isChildrenExcludedListLoading.value[index] = false
isChildrenListLoading.value[index] = false
}
reloadData?.(false)
@ -356,12 +409,27 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
await loadChildrenList()
})
watch(childrenExcludedList, async () => {
childrenExcludedList.value?.list.forEach((row: Record<string, any>, index: number) => {
isChildrenExcludedListLinked.value[index] = false
isChildrenExcludedListLoading.value[index] = false
})
})
watch(childrenList, async () => {
childrenList.value?.list.forEach((row: Record<string, any>, index: number) => {
isChildrenListLinked.value[index] = true
isChildrenListLoading.value[index] = false
})
})
return {
relatedTableMeta,
loadRelatedTableMeta,
relatedTableDisplayValueProp,
childrenExcludedList,
childrenList,
childrenListCount,
rowId,
childrenExcludedListPagination,
childrenListPagination,
@ -371,6 +439,10 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
link,
loadChildrenExcludedList,
loadChildrenList,
isChildrenExcludedListLinked,
isChildrenListLinked,
isChildrenListLoading,
isChildrenExcludedListLoading,
row,
deleteRelatedRow,
getRelatedTableRowId,

48
packages/nc-gui/windi.config.ts

@ -112,6 +112,54 @@ export default defineConfig({
800: '#B23830',
900: '#7D2721',
},
pink: {
50: '#FFEEFB',
100: '#FED8F4',
200: '#FEB0E8',
300: '#FD89DD',
400: '#FD61D1',
500: '#FC3AC6',
600: '#CA2E9E',
700: '#972377',
800: '#65174F',
900: '#320C28',
},
orange: {
50: '#FFF5EF',
100: '#FEE6D6',
200: '#FDCDAD',
300: '#FCB483',
400: '#FB9B5A',
500: '#FA8231',
600: '#E1752C',
700: '#C86827',
800: '#964E1D',
900: '#4B270F',
},
purple: {
50: '#F3ECFA',
100: '#E5D4F5',
200: '#CBA8EB',
300: '#B17DE1',
400: '#9751D7',
500: '#7D26CD',
600: '#641EA4',
700: '#4B177B',
800: '#320F52',
900: '#190829',
},
blue: {
50: '#EDF9FF',
100: '#D7F2FF',
200: '#AFE5FF',
300: '#86D9FF',
400: '#5ECCFF',
500: '#36BFFF',
600: '#2B99CC',
700: '#207399',
800: '#164C66',
900: '#0B2633',
},
primary: 'rgba(var(--color-primary), var(--tw-bg-opacity))',
accent: 'rgba(var(--color-accent), var(--tw-bg-opacity))',
dark: colors.dark,

2
tests/playwright/pages/Dashboard/Grid/Column/LTAR/LinkRecord.ts

@ -42,7 +42,7 @@ export class LinkRecord extends BasePage {
}
async close() {
await this.get().locator(`.ant-modal-close-x`).click();
await this.get().locator(`.nc-close-btn`).click();
await this.get().waitFor({ state: 'hidden' });
}

6
tests/playwright/tests/db/features/undo-redo.spec.ts

@ -609,12 +609,12 @@ test.describe('Undo Redo - LTAR', () => {
await grid.cell.inCellAdd({ index: 0, columnHeader: 'CityList' });
await dashboard.linkRecord.select('Mumbai');
await grid.cell.inCellAdd({ index: 0, columnHeader: 'CityList' });
await dashboard.linkRecord.select('Delhi');
await dashboard.linkRecord.close();
await grid.cell.unlinkVirtualCell({ index: 0, columnHeader: 'CityList' });
await grid.cell.unlinkVirtualCell({ index: 0, columnHeader: 'CityList' });
await grid.cell.unlinkVirtualCell({ index: 1, columnHeader: 'CityList' });
await verifyRecords([]);
await undo({ page, values: ['Delhi'] });

Loading…
Cancel
Save