Browse Source

Nc fix: linked records dropdown followup (#8415)

* fix(nc-gui): update link record dropdown border radius

* fix(nc-gui): move link/unlink btn to the right side

* fix(nc-gui): update placeholder text color

* fix(nc-gui): reduce link record dropdown bottom bar padding x

* fix(nc-gui): update expanded form save btn text in create new record from link record

* fix(nc-gui): grid active cell border issue

* fix(nc-gui): link record dropdown trigger & dropdown spacing

* fix(nc-gui): link record footer btn size

* fix(nc-gui): link record loading state

* fix(nc-gui): update link record search input icon

* fix(nc-gui): update link record item skeleton

* fix(nc-gui): update grid row hover & selected state bg color

* fix(nc-gui): add tooltip on hover over expand icon

* fix(nc-gui): link record tooltip issue

* fix(nc-gui): update search query empty state status

* fix(nc-gui): bt cell ui fixes

* feat(nc-gui): resizable link record dropdown

* fix(nc-gui): external DB, fields re-order to re-align sub fields needs a fix

* chore(nc-gui): lint

* fix(nc-gui): bt & oto cell unlink & link btn visibility issue

* fix(nc-gui): pr review changes

* fix: docs

---------

Co-authored-by: Raju Udava <86527202+dstala@users.noreply.github.com>
pull/8433/head
Ramesh Mane 2 months ago committed by GitHub
parent
commit
d4fa6a8174
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  2. 20
      packages/nc-gui/components/smartsheet/grid/Table.vue
  3. 4
      packages/nc-gui/components/virtual-cell/BelongsTo.vue
  4. 2
      packages/nc-gui/components/virtual-cell/HasMany.vue
  5. 2
      packages/nc-gui/components/virtual-cell/Links.vue
  6. 2
      packages/nc-gui/components/virtual-cell/ManyToMany.vue
  7. 10
      packages/nc-gui/components/virtual-cell/OneToOne.vue
  8. 67
      packages/nc-gui/components/virtual-cell/components/ItemChip.vue
  9. 13
      packages/nc-gui/components/virtual-cell/components/LinkRecordDropdown.vue
  10. 49
      packages/nc-gui/components/virtual-cell/components/LinkedItems.vue
  11. 73
      packages/nc-gui/components/virtual-cell/components/ListItem.vue
  12. 52
      packages/nc-gui/components/virtual-cell/components/UnLinkedItems.vue
  13. 1
      packages/nc-gui/lang/en.json
  14. 8
      packages/noco-docs/docs/070.fields/040.field-types/040.links-based/010.links.md
  15. BIN
      packages/noco-docs/static/img/v2/fields/add-link-modal.png
  16. BIN
      packages/noco-docs/static/img/v2/fields/linked-record-modal.png
  17. 37
      packages/nocodb/src/models/Column.ts

3
packages/nc-gui/components/smartsheet/expanded-form/index.vue

@ -26,6 +26,7 @@ interface Props {
closeAfterSave?: boolean closeAfterSave?: boolean
newRecordHeader?: string newRecordHeader?: string
skipReload?: boolean skipReload?: boolean
newRecordSubmitBtnText?: string
} }
const props = defineProps<Props>() const props = defineProps<Props>()
@ -882,7 +883,7 @@ export default {
size="medium" size="medium"
@click="save" @click="save"
> >
<div class="xs:px-1">Save</div> <div class="xs:px-1">{{ newRecordSubmitBtnText ?? 'Save' }}</div>
</NcButton> </NcButton>
</div> </div>
</div> </div>

20
packages/nc-gui/components/smartsheet/grid/Table.vue

@ -427,8 +427,8 @@ const cellMeta = computed(() => {
const colMeta = computed(() => { const colMeta = computed(() => {
return fields.value.map((col) => { return fields.value.map((col) => {
return { return {
isLookup: isLinksOrLTAR(col), isLookup: isLookup(col),
isRollup: isBt(col), isRollup: isRollup(col),
isFormula: isFormula(col), isFormula: isFormula(col),
isCreatedOrLastModifiedTimeCol: isCreatedOrLastModifiedTimeCol(col), isCreatedOrLastModifiedTimeCol: isCreatedOrLastModifiedTimeCol(col),
isCreatedOrLastModifiedByCol: isCreatedOrLastModifiedByCol(col), isCreatedOrLastModifiedByCol: isCreatedOrLastModifiedByCol(col),
@ -857,7 +857,7 @@ onClickOutside(tableBodyEl, (e) => {
// ignore unselecting if clicked inside or on the picker(Date, Time, DateTime, Year) // ignore unselecting if clicked inside or on the picker(Date, Time, DateTime, Year)
// or single/multi select options // or single/multi select options
const activePickerOrDropdownEl = document.querySelector( const activePickerOrDropdownEl = document.querySelector(
'.nc-picker-datetime.active,.nc-dropdown-single-select-cell.active,.nc-dropdown-multi-select-cell.active,.nc-dropdown-user-select-cell.active,.nc-picker-date.active,.nc-picker-year.active,.nc-picker-time.active', '.nc-picker-datetime.active,.nc-dropdown-single-select-cell.active,.nc-dropdown-multi-select-cell.active,.nc-dropdown-user-select-cell.active,.nc-picker-date.active,.nc-picker-year.active,.nc-picker-time.active,.nc-link-dropdown-root',
) )
if ( if (
e.target && e.target &&
@ -1892,6 +1892,7 @@ onKeyStroke('ArrowDown', onDown)
:class="{ :class="{
'active-row': activeCell.row === rowIndex || selectedRange._start?.row === rowIndex, 'active-row': activeCell.row === rowIndex || selectedRange._start?.row === rowIndex,
'mouse-down': isGridCellMouseDown || isFillMode, 'mouse-down': isGridCellMouseDown || isFillMode,
'selected-row': row.rowMeta.selected,
}" }"
:style="{ height: rowHeight ? `${rowHeightInPx[`${rowHeight}`]}px` : `${rowHeightInPx['1']}px` }" :style="{ height: rowHeight ? `${rowHeightInPx[`${rowHeight}`]}px` : `${rowHeightInPx['1']}px` }"
:data-testid="`grid-row-${rowIndex}`" :data-testid="`grid-row-${rowIndex}`"
@ -2430,7 +2431,7 @@ onKeyStroke('ArrowDown', onDown)
td, td,
th { th {
@apply border-gray-100 border-solid border-r bg-gray-50 p-0; @apply border-gray-100 border-solid border-r bg-gray-100 p-0;
min-height: 32px !important; min-height: 32px !important;
height: 32px !important; height: 32px !important;
position: relative; position: relative;
@ -2685,9 +2686,18 @@ onKeyStroke('ArrowDown', onDown)
@apply !xs:hidden flex; @apply !xs:hidden flex;
} }
&:not(.selected-row) {
td.nc-grid-cell:not(.active),
td:nth-child(2):not(.active) {
@apply !bg-gray-50 border-b-gray-200 border-r-gray-200;
}
}
}
&.selected-row {
td.nc-grid-cell:not(.active), td.nc-grid-cell:not(.active),
td:nth-child(2):not(.active) { td:nth-child(2):not(.active) {
@apply !bg-gray-50 border-b-gray-200 border-r-gray-200; @apply !bg-[#F0F3FF] border-b-gray-200 border-r-gray-200;
} }
} }
} }

4
packages/nc-gui/components/virtual-cell/BelongsTo.vue

@ -87,8 +87,8 @@ watch(value, (next) => {
<template> <template>
<div class="flex w-full chips-wrapper items-center" :class="{ active }"> <div class="flex w-full chips-wrapper items-center" :class="{ active }">
<LazyVirtualCellComponentsLinkRecordDropdown v-model:is-open="isOpen"> <LazyVirtualCellComponentsLinkRecordDropdown v-model:is-open="isOpen">
<div class="flex items-center w-full"> <div class="flex items-center w-full min-h-7.7">
<div class="nc-cell-field chips flex items-center flex-1"> <div class="nc-cell-field chips flex items-center flex-1 max-w-[calc(100%_-_16px)]">
<template v-if="value && (relatedTableDisplayValueProp || relatedTableDisplayValuePropId)"> <template v-if="value && (relatedTableDisplayValueProp || relatedTableDisplayValuePropId)">
<VirtualCellComponentsItemChip <VirtualCellComponentsItemChip
:item="value" :item="value"

2
packages/nc-gui/components/virtual-cell/HasMany.vue

@ -127,7 +127,7 @@ watch(
<template> <template>
<LazyVirtualCellComponentsLinkRecordDropdown v-model:is-open="isOpen"> <LazyVirtualCellComponentsLinkRecordDropdown v-model:is-open="isOpen">
<div class="flex items-center gap-1 w-full chips-wrapper"> <div class="flex items-center gap-1 w-full chips-wrapper min-h-7.7">
<div class="chips flex items-center img-container flex-1 hm-items flex-nowrap min-w-0 overflow-hidden"> <div class="chips flex items-center img-container flex-1 hm-items flex-nowrap min-w-0 overflow-hidden">
<template v-if="cells"> <template v-if="cells">
<VirtualCellComponentsItemChip <VirtualCellComponentsItemChip

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

@ -139,7 +139,7 @@ watch(
<template> <template>
<div class="nc-cell-field flex w-full group items-center nc-links-wrapper py-1" @dblclick.stop="openChildList"> <div class="nc-cell-field flex w-full group items-center nc-links-wrapper py-1" @dblclick.stop="openChildList">
<LazyVirtualCellComponentsLinkRecordDropdown v-model:is-open="isOpen"> <LazyVirtualCellComponentsLinkRecordDropdown v-model:is-open="isOpen">
<div class="flex w-full group items-center"> <div class="flex w-full group items-center min-h-7.7">
<div class="block flex-shrink truncate"> <div class="block flex-shrink truncate">
<component <component
:is="isUnderLookup ? 'span' : 'a'" :is="isUnderLookup ? 'span' : 'a'"

2
packages/nc-gui/components/virtual-cell/ManyToMany.vue

@ -126,7 +126,7 @@ watch(
<template> <template>
<LazyVirtualCellComponentsLinkRecordDropdown v-model:is-open="isOpen"> <LazyVirtualCellComponentsLinkRecordDropdown v-model:is-open="isOpen">
<div class="flex items-center gap-1 w-full chips-wrapper"> <div class="flex items-center gap-1 w-full chips-wrapper min-h-7.7">
<div class="chips flex items-center img-container flex-1 hm-items flex-nowrap min-w-0 overflow-hidden"> <div class="chips flex items-center img-container flex-1 hm-items flex-nowrap min-w-0 overflow-hidden">
<template v-if="cells"> <template v-if="cells">
<VirtualCellComponentsItemChip <VirtualCellComponentsItemChip

10
packages/nc-gui/components/virtual-cell/OneToOne.vue

@ -84,8 +84,8 @@ watch(
<template> <template>
<LazyVirtualCellComponentsLinkRecordDropdown v-model:is-open="isOpen"> <LazyVirtualCellComponentsLinkRecordDropdown v-model:is-open="isOpen">
<div class="flex w-full chips-wrapper items-center" :class="{ active }"> <div class="flex w-full chips-wrapper items-center min-h-7.7" :class="{ active }">
<div class="nc-cell-field chips flex items-center flex-1"> <div class="nc-cell-field chips flex items-center flex-1 max-w-[calc(100%_-_16px)]">
<template v-if="value && (relatedTableDisplayValueProp || relatedTableDisplayValuePropId)"> <template v-if="value && (relatedTableDisplayValueProp || relatedTableDisplayValuePropId)">
<VirtualCellComponentsItemChip <VirtualCellComponentsItemChip
:item="value" :item="value"
@ -109,7 +109,11 @@ watch(
> >
<GeneralIcon <GeneralIcon
:icon="addIcon" :icon="addIcon"
class="select-none !text-md text-gray-700 nc-action-icon nc-plus invisible group-hover:visible group-focus:visible" class="select-none text-gray-700 nc-action-icon nc-plus invisible group-hover:visible group-focus:visible"
:class="{
'!text-[14px]': addIcon === 'expand',
'!text-md': addIcon !== 'expand',
}"
@click.stop="listItemsDlg = true" @click.stop="listItemsDlg = true"
/> />
</div> </div>

67
packages/nc-gui/components/virtual-cell/components/ItemChip.vue

@ -28,6 +28,8 @@ const isForm = inject(IsFormInj)!
const { open } = useExpandedFormDetached() const { open } = useExpandedFormDetached()
function openExpandedForm() { function openExpandedForm() {
if (!active.value) return
const rowId = extractPkFromRow(item, relatedTableMeta.value.columns as ColumnType[]) const rowId = extractPkFromRow(item, relatedTableMeta.value.columns as ColumnType[])
if (!readOnly.value && !readonlyProp && rowId) { if (!readOnly.value && !readonlyProp && rowId) {
open({ open({
@ -50,44 +52,50 @@ export default {
<template> <template>
<div <div
v-e="['c:row-expand:open']" v-e="['c:row-expand:open']"
class="chip group mr-1 my-1 flex items-center rounded-[2px] flex-row" class="chip group mr-1 my-1 flex items-center rounded-[2px] flex-row truncate"
:class="{ active, 'border-1 py-1 px-2': isAttachment(column) }" :class="{ active, 'border-1 py-1 px-2': isAttachment(column) }"
@click="openExpandedForm" @click="openExpandedForm"
> >
<span class="name"> <div class="text-ellipsis overflow-hidden">
<!-- Render virtual cell --> <span class="name">
<div v-if="isVirtualCol(column)"> <!-- Render virtual cell -->
<template v-if="column.uidt === UITypes.LinkToAnotherRecord"> <div v-if="isVirtualCol(column)">
<LazySmartsheetVirtualCell :edit-enabled="false" :model-value="value" :column="column" :read-only="true" /> <template v-if="column.uidt === UITypes.LinkToAnotherRecord">
</template> <LazySmartsheetVirtualCell :edit-enabled="false" :model-value="value" :column="column" :read-only="true" />
</template>
<LazySmartsheetVirtualCell v-else :edit-enabled="false" :read-only="true" :model-value="value" :column="column" />
</div> <LazySmartsheetVirtualCell v-else :edit-enabled="false" :read-only="true" :model-value="value" :column="column" />
<!-- Render normal cell -->
<template v-else>
<div v-if="isAttachment(column) && value && !Array.isArray(value) && typeof value === 'object'">
<LazySmartsheetCell :model-value="value" :column="column" :edit-enabled="false" :read-only="true" />
</div> </div>
<!-- For attachment cell avoid adding chip style --> <!-- Render normal cell -->
<template v-else> <template v-else>
<div <div v-if="isAttachment(column) && value && !Array.isArray(value) && typeof value === 'object'">
class="min-w-max" <LazySmartsheetCell :model-value="value" :column="column" :edit-enabled="false" :read-only="true" />
:class="{
'px-1 rounded-full flex-1': !isAttachment(column),
'border-gray-200 rounded border-1':
border && ![UITypes.Attachment, UITypes.MultiSelect, UITypes.SingleSelect].includes(column.uidt),
}"
>
<LazySmartsheetCell :model-value="value" :column="column" :edit-enabled="false" :virtual="true" :read-only="true" />
</div> </div>
<!-- For attachment cell avoid adding chip style -->
<template v-else>
<div
class="min-w-max"
:class="{
'px-1 rounded-full flex-1': !isAttachment(column),
'border-gray-200 rounded border-1':
border && ![UITypes.Attachment, UITypes.MultiSelect, UITypes.SingleSelect].includes(column.uidt),
}"
>
<LazySmartsheetCell :model-value="value" :column="column" :edit-enabled="false" :virtual="true" :read-only="true" />
</div>
</template>
</template> </template>
</template> </span>
</span> </div>
<div v-show="active || isForm" v-if="showUnlinkButton && !readOnly && isUIAllowed('dataEdit')" class="flex items-center"> <div
v-show="active || isForm"
v-if="showUnlinkButton && !readOnly && isUIAllowed('dataEdit')"
class="flex items-center cursor-pointer"
>
<component <component
:is="iconMap.closeThick" :is="iconMap.closeThick"
class="nc-icon unlink-icon text-xs text-gray-500/50 group-hover:text-gray-500" class="nc-icon unlink-icon text-gray-500/50 group-hover:text-gray-500 ml-0.5 cursor-pointer"
@click.stop="emit('unlink')" @click.stop="emit('unlink')"
/> />
</div> </div>
@ -99,9 +107,8 @@ export default {
max-width: max(100%, 60px); max-width: max(100%, 60px);
.name { .name {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap; white-space: nowrap;
word-break: keep-all;
} }
} }
</style> </style>

13
packages/nc-gui/components/virtual-cell/components/LinkRecordDropdown.vue

@ -68,7 +68,7 @@ watch([ncLinksDropdownRef, isOpen], () => {
> >
<slot /> <slot />
<template #overlay> <template #overlay>
<div ref="ncLinksDropdownRef" class="h-[412px] w-[540px]" :class="`${randomClass}`"> <div ref="ncLinksDropdownRef" class="h-[412px] w-[540px] nc-links-dropdown-wrapper" :class="`${randomClass}`">
<slot name="overlay" /> <slot name="overlay" />
</div> </div>
</template> </template>
@ -77,9 +77,20 @@ watch([ncLinksDropdownRef, isOpen], () => {
<style lang="scss"> <style lang="scss">
.nc-links-dropdown { .nc-links-dropdown {
@apply rounded-xl !border-gray-200;
z-index: 1000 !important; z-index: 1000 !important;
} }
.nc-link-dropdown-root { .nc-link-dropdown-root {
z-index: 1000; z-index: 1000;
} }
.nc-links-dropdown-wrapper {
@apply h-[412px] w-[540px];
overflow-y: auto;
overflow-x: hidden;
resize: vertical;
min-height: 412px;
max-height: 700px;
max-width: 540px;
}
</style> </style>

49
packages/nc-gui/components/virtual-cell/components/LinkedItems.vue

@ -306,15 +306,14 @@ const onFilterChange = () => {
<template> <template>
<div class="nc-modal-child-list h-full w-full" :class="{ active: vModel }" @keydown.enter.stop> <div class="nc-modal-child-list h-full w-full" :class="{ active: vModel }" @keydown.enter.stop>
<div class="flex flex-col h-full"> <div class="flex flex-col h-full">
<div class="nc-dropdown-link-record-header bg-gray-100 py-2 rounded-t-md flex justify-between pl-3 pr-2 gap-2"> <div class="nc-dropdown-link-record-header bg-gray-100 py-2 rounded-t-xl flex justify-between pl-3 pr-2 gap-2">
<div v-if="!isForm" class="flex-1 nc-dropdown-link-record-search-wrapper flex items-center py-0.5 rounded-md"> <div v-if="!isForm" class="flex-1 nc-dropdown-link-record-search-wrapper flex items-center py-0.5 rounded-md">
<MdiMagnify class="nc-search-icon w-5 h-5" />
<a-input <a-input
ref="filterQueryRef" ref="filterQueryRef"
v-model:value="childrenListPagination.query" v-model:value="childrenListPagination.query"
:bordered="false" :bordered="false"
placeholder="Search linked records..." placeholder="Search linked records..."
class="w-full min-h-4" class="w-full min-h-4 !pl-0"
size="small" size="small"
@change="onFilterChange" @change="onFilterChange"
@keydown.capture.stop=" @keydown.capture.stop="
@ -325,6 +324,9 @@ const onFilterChange = () => {
} }
" "
> >
<template #prefix>
<GeneralIcon icon="search" class="nc-search-icon mr-2 h-4 w-4 text-gray-500" />
</template>
</a-input> </a-input>
</div> </div>
<div v-else>&nbsp;</div> <div v-else>&nbsp;</div>
@ -343,26 +345,21 @@ const onFilterChange = () => {
<div <div
v-for="(_x, i) in Array.from({ length: skeletonCount })" v-for="(_x, i) in Array.from({ length: skeletonCount })"
:key="i" :key="i"
class="flex flex-row gap-2 mb-2 transition-all relative !border-gray-200 hover:bg-gray-50" class="flex flex-row gap-3 px-3 py-2 transition-all relative border-b-1 border-gray-200 hover:bg-gray-50"
> >
<div class="flex items-center"> <div class="flex items-center">
<a-skeleton-image class="h-14 w-14 !rounded-xl children:!h-full" /> <a-skeleton-image class="!h-11 !w-11 !rounded-md overflow-hidden children:(!h-full !w-full)" />
</div> </div>
<div class="flex flex-col gap-2 flex-grow justify-center"> <div class="flex flex-col gap-2 flex-grow justify-center">
<a-skeleton-input active class="h-3 !w-48 !rounded-xl" size="small" /> <a-skeleton-input active class="h-4 !w-48 !rounded-md overflow-hidden" size="small" />
<div class="flex flex-row gap-6 w-10/12"> <div class="flex flex-row gap-6 w-10/12">
<div class="flex flex-col gap-0.5"> <a-skeleton-input
<a-skeleton-input active class="!h-2 !w-12" size="small" /> v-for="idx of [1, 2, 3]"
<a-skeleton-input active class="!h-2 !w-24" size="small" /> :key="idx"
</div> active
<div class="flex flex-col gap-0.5"> class="!h-3 !w-24 !rounded-md overflow-hidden"
<a-skeleton-input active class="!h-2 !w-12" size="small" /> size="small"
<a-skeleton-input active class="!h-2 !w-24" size="small" /> />
</div>
<div class="flex flex-col gap-0.5">
<a-skeleton-input active class="!h-2 !w-12" size="small" />
<a-skeleton-input active class="!h-2 !w-24" size="small" />
</div>
</div> </div>
</div> </div>
</div> </div>
@ -410,13 +407,13 @@ const onFilterChange = () => {
</div> </div>
</div> </div>
<div class="bg-gray-100 px-3 py-2 rounded-b-md flex items-center justify-between gap-3 min-h-12"> <div class="nc-dropdown-link-record-footer bg-gray-100 p-2 rounded-b-xl flex items-center justify-between gap-3 min-h-11">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<NcButton <NcButton
v-if="!isPublic" v-if="!isPublic"
v-e="['c:row-expand:open']" v-e="['c:row-expand:open']"
size="small" size="small"
class="!hover:(bg-white text-brand-500)" class="!hover:(bg-white text-brand-500) !h-7 !text-small"
type="secondary" type="secondary"
@click="addNewRecord" @click="addNewRecord"
> >
@ -428,7 +425,7 @@ const onFilterChange = () => {
v-if="!readOnly && (childrenListCount > 0 || (childrenList?.list ?? state?.[colTitle] ?? []).length > 0)" v-if="!readOnly && (childrenListCount > 0 || (childrenList?.list ?? state?.[colTitle] ?? []).length > 0)"
v-e="['c:links:link']" v-e="['c:links:link']"
data-testid="nc-child-list-button-link-to" data-testid="nc-child-list-button-link-to"
class="!hover:(bg-white text-brand-500)" class="!hover:(bg-white text-brand-500) !h-7 !text-small"
size="small" size="small"
type="secondary" type="secondary"
@click="emit('attachRecord')" @click="emit('attachRecord')"
@ -478,6 +475,7 @@ const onFilterChange = () => {
:state="newRowState" :state="newRowState"
:row-id="extractPkFromRow(expandedFormRow, relatedTableMeta.columns as ColumnType[])" :row-id="extractPkFromRow(expandedFormRow, relatedTableMeta.columns as ColumnType[])"
use-meta-fields use-meta-fields
new-record-submit-btn-text="Create & Link"
@created-record="onCreatedRecord" @created-record="onCreatedRecord"
/> />
</Suspense> </Suspense>
@ -493,8 +491,8 @@ const onFilterChange = () => {
@apply !p-0; @apply !p-0;
} }
:deep(.ant-skeleton-element .ant-skeleton-image) { :deep(.ant-skeleton-element .ant-skeleton-image-svg) {
@apply !h-full; @apply !w-7;
} }
</style> </style>
@ -509,5 +507,10 @@ const onFilterChange = () => {
@apply text-gray-600; @apply text-gray-600;
} }
} }
input {
&::placeholder {
@apply text-gray-500;
}
}
} }
</style> </style>

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

@ -90,25 +90,6 @@ const displayValue = computed(() => {
:hoverable="false" :hoverable="false"
> >
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div v-if="isLoading" class="flex">
<MdiLoading class="flex-none w-7 h-7 !text-brand-500 animate-spin" />
</div>
<NcTooltip v-else class="z-10 flex">
<template #title> {{ isLinked ? 'Unlink' : 'Link' }}</template>
<button
tabindex="-1"
class="nc-list-item-link-unlink-btn p-1.5 flex rounded-lg transition-all"
:class="{
'bg-red-100 text-red-500 hover:bg-red-200': isLinked,
'bg-green-100 text-green-500 hover:bg-green-200': !isLinked,
}"
@click="$emit('linkOrUnlink')"
>
<GeneralIcon :icon="isLinked ? 'minus' : 'plus'" class="flex-none w-4 h-4 !font-extrabold" />
</button>
</NcTooltip>
<template v-if="attachment"> <template v-if="attachment">
<div v-if="attachments && attachments.length"> <div v-if="attachments && attachments.length">
<a-carousel autoplay class="!w-11 !h-11 !max-h-11 !max-w-11"> <a-carousel autoplay class="!w-11 !h-11 !max-h-11 !max-w-11">
@ -144,15 +125,20 @@ const displayValue = computed(() => {
> >
<div v-for="field in fields" :key="field.id" class="sm:(w-1/3 max-w-1/3 overflow-hidden)"> <div v-for="field in fields" :key="field.id" class="sm:(w-1/3 max-w-1/3 overflow-hidden)">
<div v-if="!isRowEmpty(row, field)" class="flex flex-col gap-[-1]"> <div v-if="!isRowEmpty(row, field)" class="flex flex-col gap-[-1]">
<NcTooltip class="z-10 flex" placement="bottom"> <NcTooltip class="z-10 flex" placement="bottomLeft" :arrow-point-at-center="false">
<template #title> <template #title>
<LazySmartsheetHeaderVirtualCell <LazySmartsheetHeaderVirtualCell
v-if="isVirtualCol(field)" v-if="isVirtualCol(field)"
class="!scale-60 text-gray-100 !text-sm" class="text-gray-100 !text-sm nc-link-record-cell-tooltip"
:column="field"
:hide-menu="true"
/>
<LazySmartsheetHeaderCell
v-else
class="text-gray-100 !text-sm nc-link-record-cell-tooltip"
:column="field" :column="field"
:hide-menu="true" :hide-menu="true"
/> />
<LazySmartsheetHeaderCell v-else class="!scale-70 text-gray-100 !text-sm" :column="field" :hide-menu="true" />
</template> </template>
<div class="nc-link-record-cell flex w-full max-w-full"> <div class="nc-link-record-cell flex w-full max-w-full">
<LazySmartsheetVirtualCell v-if="isVirtualCol(field)" v-model="row[field.title]" :row="row" :column="field" /> <LazySmartsheetVirtualCell v-if="isVirtualCol(field)" v-model="row[field.title]" :row="row" :column="field" />
@ -171,15 +157,38 @@ const displayValue = computed(() => {
</div> </div>
</div> </div>
<div v-if="!isForm && !isPublic && !readOnly" class="flex-none flex items-center w-7"> <div v-if="!isForm && !isPublic && !readOnly" class="flex-none flex items-center w-7">
<NcTooltip class="flex">
<template #title>{{ $t('title.expand') }}</template>
<button
v-e="['c:row-expand:open']"
:tabindex="-1"
class="z-10 flex items-center justify-center nc-expand-item !group-hover:visible !invisible !h-7 !w-7 transition-all !hover:children:(w-4.5 h-4.5)"
@click.stop="$emit('expand', row)"
>
<MaximizeIcon class="flex-none w-4 h-4 scale-125" />
</button>
</NcTooltip>
</div>
<NcTooltip class="z-10 flex">
<template #title> {{ isLinked ? 'Unlink' : 'Link' }}</template>
<button <button
v-e="['c:row-expand:open']" tabindex="-1"
:tabindex="-1" class="nc-list-item-link-unlink-btn p-1.5 flex rounded-lg transition-all"
class="z-10 flex items-center justify-center nc-expand-item !group-hover:visible !invisible !h-7 !w-7 transition-all !hover:children:(w-4.5 h-4.5)" :class="{
@click.stop="$emit('expand', row)" 'bg-gray-200 text-gray-800 hover:(bg-red-100 text-red-500)': isLinked,
'bg-green-[#D4F7E0] text-[#17803D] hover:bg-green-200': !isLinked,
}"
@click="$emit('linkOrUnlink')"
> >
<MaximizeIcon class="flex-none w-4 h-4 scale-125" /> <div v-if="isLoading" class="flex">
<MdiLoading class="flex-none w-4 h-4 !text-brand-500 animate-spin" />
</div>
<GeneralIcon v-else :icon="isLinked ? 'minus' : 'plus'" class="flex-none w-4 h-4 !font-extrabold" />
</button> </button>
</div> </NcTooltip>
</div> </div>
</a-card> </a-card>
</div> </div>
@ -260,6 +269,14 @@ const displayValue = computed(() => {
} }
} }
} }
.nc-link-record-cell-tooltip {
:deep(.nc-cell-icon) {
@apply !ml-0;
}
:deep(.name) {
@apply !text-small;
}
}
</style> </style>
<style lang="scss"> <style lang="scss">

52
packages/nc-gui/components/virtual-cell/components/UnLinkedItems.vue

@ -295,7 +295,7 @@ const onFilterChange = () => {
<template> <template>
<div class="nc-modal-link-record h-full w-full overflow-hidden" :class="{ active: vModel }" @keydown.enter.stop> <div class="nc-modal-link-record h-full w-full overflow-hidden" :class="{ active: vModel }" @keydown.enter.stop>
<div class="flex flex-col h-full"> <div class="flex flex-col h-full">
<div class="nc-dropdown-link-record-header bg-gray-100 py-2 rounded-t-md flex justify-between pl-3 pr-2 gap-2"> <div class="nc-dropdown-link-record-header bg-gray-100 py-2 rounded-t-xl flex justify-between pl-3 pr-2 gap-2">
<div class="flex-1 gap-2 flex items-center"> <div class="flex-1 gap-2 flex items-center">
<button <button
v-if="!hideBackBtn" v-if="!hideBackBtn"
@ -306,13 +306,12 @@ const onFilterChange = () => {
</button> </button>
<div class="flex-1 nc-dropdown-link-record-search-wrapper flex items-center py-0.5 rounded-md"> <div class="flex-1 nc-dropdown-link-record-search-wrapper flex items-center py-0.5 rounded-md">
<MdiMagnify class="nc-search-icon w-5 h-5" />
<a-input <a-input
ref="filterQueryRef" ref="filterQueryRef"
v-model:value="childrenExcludedListPagination.query" v-model:value="childrenExcludedListPagination.query"
:bordered="false" :bordered="false"
placeholder="Search records to link..." placeholder="Search records to link..."
class="w-full nc-excluded-search min-h-4" class="w-full nc-excluded-search min-h-4 !pl-0"
size="small" size="small"
@change="onFilterChange" @change="onFilterChange"
@keydown.capture.stop=" @keydown.capture.stop="
@ -323,6 +322,9 @@ const onFilterChange = () => {
} }
" "
> >
<template #prefix>
<GeneralIcon icon="search" class="nc-search-icon mr-2 h-4 w-4 text-gray-500" />
</template>
</a-input> </a-input>
</div> </div>
</div> </div>
@ -341,26 +343,21 @@ const onFilterChange = () => {
<div <div
v-for="(_x, i) in Array.from({ length: 10 })" v-for="(_x, i) in Array.from({ length: 10 })"
:key="i" :key="i"
class="flex flex-row gap-2 mb-2 transition-all relative !border-gray-200 hover:bg-gray-50" class="flex flex-row gap-3 px-3 py-2 transition-all relative border-b-1 border-gray-200 hover:bg-gray-50"
> >
<div class="flex items-center"> <div class="flex items-center">
<a-skeleton-image class="h-14 w-14 !rounded-xl children:!h-full" /> <a-skeleton-image class="!h-11 !w-11 !rounded-md overflow-hidden children:(!h-full !w-full)" />
</div> </div>
<div class="flex flex-col gap-2 flex-grow justify-center"> <div class="flex flex-col gap-2 flex-grow justify-center">
<a-skeleton-input active class="h-3 !w-48 !rounded-xl" size="small" /> <a-skeleton-input active class="h-4 !w-48 !rounded-md overflow-hidden" size="small" />
<div class="flex flex-row gap-6 w-10/12"> <div class="flex flex-row gap-6 w-10/12">
<div class="flex flex-col gap-0.5"> <a-skeleton-input
<a-skeleton-input active class="!h-2 !w-12" size="small" /> v-for="idx of [1, 2, 3]"
<a-skeleton-input active class="!h-2 !w-24" size="small" /> :key="idx"
</div> active
<div class="flex flex-col gap-0.5"> class="!h-3 !w-24 !rounded-md overflow-hidden"
<a-skeleton-input active class="!h-2 !w-12" size="small" /> size="small"
<a-skeleton-input active class="!h-2 !w-24" size="small" /> />
</div>
<div class="flex flex-col gap-0.5">
<a-skeleton-input active class="!h-2 !w-12" size="small" />
<a-skeleton-input active class="!h-2 !w-24" size="small" />
</div>
</div> </div>
</div> </div>
</div> </div>
@ -392,19 +389,21 @@ const onFilterChange = () => {
</template> </template>
<div v-else class="h-full my-auto py-2 flex flex-col gap-3 items-center justify-center text-gray-500"> <div v-else class="h-full my-auto py-2 flex flex-col gap-3 items-center justify-center text-gray-500">
<InboxIcon class="w-16 h-16 mx-auto" /> <InboxIcon class="w-16 h-16 mx-auto" />
<p>
<p v-if="childrenExcludedListPagination.query">{{ $t('msg.noRecordsMatchYourSearchQuery') }}</p>
<p v-else>
{{ $t('msg.thereAreNoRecordsInTable') }} {{ $t('msg.thereAreNoRecordsInTable') }}
{{ relatedTableMeta?.title }} {{ relatedTableMeta?.title }}
</p> </p>
</div> </div>
</div> </div>
<div class="bg-gray-100 px-3 py-2 rounded-b-md flex items-center justify-between min-h-12"> <div class="nc-dropdown-link-record-footer bg-gray-100 p-2 rounded-b-xl flex items-center justify-between min-h-11">
<div class="flex"> <div class="flex">
<NcButton <NcButton
v-if="!isPublic" v-if="!isPublic"
v-e="['c:row-expand:open']" v-e="['c:row-expand:open']"
size="small" size="small"
class="!hover:(bg-white text-brand-500)" class="!hover:(bg-white text-brand-500) !h-7 !text-small"
type="secondary" type="secondary"
@click="addNewRecord" @click="addNewRecord"
> >
@ -463,6 +462,7 @@ const onFilterChange = () => {
:state="newRowState" :state="newRowState"
use-meta-fields use-meta-fields
:skip-reload="true" :skip-reload="true"
new-record-submit-btn-text="Create & Link"
@created-record="onCreatedRecord" @created-record="onCreatedRecord"
/> />
</Suspense> </Suspense>
@ -470,8 +470,8 @@ const onFilterChange = () => {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
:deep(.ant-skeleton-element .ant-skeleton-image) { :deep(.ant-skeleton-element .ant-skeleton-image-svg) {
@apply !h-full; @apply !w-7;
} }
</style> </style>
@ -486,5 +486,11 @@ const onFilterChange = () => {
@apply text-gray-600; @apply text-gray-600;
} }
} }
input {
&::placeholder {
@apply text-gray-500;
}
}
} }
</style> </style>

1
packages/nc-gui/lang/en.json

@ -1200,6 +1200,7 @@
"noNewNotifications": "You have no new notifications", "noNewNotifications": "You have no new notifications",
"noRecordFound": "Record not found", "noRecordFound": "Record not found",
"noRecordsFound": "No records found", "noRecordsFound": "No records found",
"noRecordsMatchYourSearchQuery": "No records match your search query",
"rowDeleted": "Record deleted", "rowDeleted": "Record deleted",
"saveChanges": "Do you want to save the changes?", "saveChanges": "Do you want to save the changes?",
"tooLargeFieldEntity": "The field is too large to be converted to {entity}", "tooLargeFieldEntity": "The field is too large to be converted to {entity}",

8
packages/noco-docs/docs/070.fields/040.field-types/040.links-based/010.links.md

@ -47,8 +47,8 @@ List of linked records will appear as a dropdown containing linked records as ca
1. Search bar, to narrow down the list of linked records displayed 1. Search bar, to narrow down the list of linked records displayed
2. Icon represents `Many-to-Many` relation. The count indicates the number of linked records 2. Icon represents `Many-to-Many` relation. The count indicates the number of linked records
3. List (cards) of linked records 3. List (cards) of linked records
4. Click on the `-` icon to unlink the record 4. To view additional information (expanded record), hover on the card & click on the `<>` icon
5. To view additional information (expanded record), hover on the card & click on the `<>` icon 5. Click on the `-` icon to unlink the record
6. Click on `+ New record` button to create and link a new record to the current one 6. Click on `+ New record` button to create and link a new record to the current one
7. Click on `Link more records` to link an existing record [Read more](#link-new-records) 7. Click on `Link more records` to link an existing record [Read more](#link-new-records)
8. Pagination bar 8. Pagination bar
@ -65,8 +65,8 @@ A brief note about the modal components:
1. Search bar, to narrow down the list of records displayed 1. Search bar, to narrow down the list of records displayed
2. Icon represents `Many-to-Many` relation. The count indicates the number of linked records 2. Icon represents `Many-to-Many` relation. The count indicates the number of linked records
3. List (cards) of records available for linking 3. List (cards) of records available for linking
4. Click on the `+` icon to link the record 4. To view additional information (expanded record) before linking, hover on the card & click on the `<>` icon
5. To view additional information (expanded record) before linking, hover on the card & click on the `<>` icon 5. Click on the `+` icon to link the record
6. Click on `+ New record` button to create and link a new record to the current one 6. Click on `+ New record` button to create and link a new record to the current one
7. Pagination bar 7. Pagination bar

BIN
packages/noco-docs/static/img/v2/fields/add-link-modal.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 292 KiB

After

Width:  |  Height:  |  Size: 362 KiB

BIN
packages/noco-docs/static/img/v2/fields/linked-record-modal.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

After

Width:  |  Height:  |  Size: 368 KiB

37
packages/nocodb/src/models/Column.ts

@ -498,6 +498,16 @@ export default class Column<T = any> implements ColumnType {
]); ]);
let { list: columnsList } = cachedList; let { list: columnsList } = cachedList;
const { isNoneList } = cachedList; const { isNoneList } = cachedList;
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<string, number>);
if (!isNoneList && !columnsList.length) { if (!isNoneList && !columnsList.length) {
columnsList = await ncMeta.metaList2(null, null, MetaTable.COLUMNS, { columnsList = await ncMeta.metaList2(null, null, MetaTable.COLUMNS, {
condition: { condition: {
@ -507,38 +517,29 @@ export default class Column<T = any> implements ColumnType {
order: 'asc', 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<string, number>,
);
columnsList.forEach((column) => { columnsList.forEach((column) => {
column.meta = parseMetaProp(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); await NocoCache.setList(CacheScope.COLUMN, [fk_model_id], columnsList);
} }
columnsList.sort( columnsList.sort(
(a, b) => (a, b) =>
(a.order != null ? a.order : Infinity) - (a.order != null ? a.order : Infinity) -
(b.order != null ? b.order : Infinity), (b.order != null ? b.order : Infinity),
); );
return Promise.all( return Promise.all(
columnsList.map(async (m) => { columnsList.map(async (m) => {
if (defaultViewColumns.length) {
m.meta = {
...parseMetaProp(m),
defaultViewColOrder: defaultViewColumnOrderMap[m.id],
};
}
const column = new Column(m); const column = new Column(m);
await column.getColOptions(ncMeta); await column.getColOptions(ncMeta);
return column; return column;

Loading…
Cancel
Save