多维表格
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

446 lines
14 KiB

<script lang="ts" setup>
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
import InboxIcon from '~icons/nc-icons/inbox'
import {
ColumnInj,
IsPublicInj,
SaveRowInj,
computed,
inject,
ref,
useLTARStoreOrThrow,
useSmartsheetRowStoreOrThrow,
useVModel,
} from '#imports'
const props = defineProps<{ modelValue: boolean; column: any }>()
const emit = defineEmits(['update:modelValue', 'addNewRecord'])
const vModel = useVModel(props, 'modelValue', emit)
const { isMobileMode } = useGlobal()
const injectedColumn = inject(ColumnInj)
const { isSharedBase } = storeToRefs(useBase())
const filterQueryRef = ref<HTMLInputElement>()
const { t } = useI18n()
const { $e } = useNuxtApp()
const {
childrenExcludedList,
isChildrenExcludedListLinked,
childrenExcludedOffsetCount,
childrenListOffsetCount,
isChildrenExcludedListLoading,
isChildrenExcludedLoading,
childrenListCount,
loadChildrenExcludedList,
loadChildrenList,
childrenExcludedListPagination,
relatedTableDisplayValueProp,
displayValueTypeAndFormatProp,
link,
relatedTableMeta,
meta,
unlink,
row,
headerDisplayValue,
} = useLTARStoreOrThrow()
const { addLTARRef, isNew, removeLTARRef, state: rowState } = useSmartsheetRowStoreOrThrow()
const isPublic = inject(IsPublicInj, ref(false))
const isExpandedFormCloseAfterSave = ref(false)
isChildrenExcludedLoading.value = true
const isForm = inject(IsFormInj, ref(false))
const saveRow = inject(SaveRowInj, () => {})
const linkRow = async (row: Record<string, any>, id: number) => {
if (isNew.value) {
addLTARRef(row, injectedColumn?.value as ColumnType)
isChildrenExcludedListLinked.value[id] = true
saveRow!()
$e('a:links:link')
} else {
await link(row, {}, false, id)
}
}
const unlinkRow = async (row: Record<string, any>, id: number) => {
if (isNew.value) {
removeLTARRef(row, injectedColumn?.value as ColumnType)
isChildrenExcludedListLinked.value[id] = false
saveRow!()
$e('a:links:unlink')
} else {
await unlink(row, {}, false, id)
}
}
/** reload list on modal open */
watch(
vModel,
(nextVal, prevVal) => {
if (nextVal && !prevVal) {
/** reset query and limit */
childrenExcludedListPagination.query = ''
childrenExcludedListPagination.page = 1
if (!isForm.value) {
loadChildrenList()
}
loadChildrenExcludedList(rowState.value)
}
},
{
immediate: true,
},
)
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 {}
const colOpt = (injectedColumn?.value as ColumnType)?.colOptions as LinkToAnotherRecordType
const colInRelatedTable: ColumnType | undefined = relatedTableMeta?.value?.columns?.find((col) => {
// Links as for the case of 'mm' we need the 'Links' column
if (!isLinksOrLTAR(col)) return false
const colOpt1 = col?.colOptions as LinkToAnotherRecordType
if (colOpt1?.fk_related_model_id !== meta.value.id) return false
if (colOpt.type === RelationTypes.MANY_TO_MANY && colOpt1?.type === RelationTypes.MANY_TO_MANY) {
return (
colOpt.fk_parent_column_id === colOpt1.fk_child_column_id && colOpt.fk_child_column_id === colOpt1.fk_parent_column_id
)
} else {
return (
colOpt.fk_parent_column_id === colOpt1.fk_parent_column_id && colOpt.fk_child_column_id === colOpt1.fk_child_column_id
)
}
})
if (!colInRelatedTable) return {}
const relatedTableColOpt = colInRelatedTable?.colOptions as LinkToAnotherRecordType
if (!relatedTableColOpt) return {}
if (relatedTableColOpt.type === RelationTypes.BELONGS_TO) {
return {
[colInRelatedTable.title as string]: row?.value?.row,
}
} else {
return {
[colInRelatedTable.title as string]: row?.value && [row.value.row],
}
}
})
const attachmentCol = computedInject(FieldsInj, (_fields) => {
return (relatedTableMeta.value.columns ?? []).filter((col) => isAttachment(col))[0]
})
const fields = computedInject(FieldsInj, (_fields) => {
return (relatedTableMeta.value.columns ?? [])
.filter((col) => !isSystemColumn(col) && !isPrimary(col) && !isLinksOrLTAR(col) && !isAttachment(col))
.slice(0, isMobileMode.value ? 1 : 4)
})
const relation = computed(() => {
return injectedColumn!.value?.colOptions?.type
})
watch(expandedFormDlg, () => {
if (!expandedFormDlg.value) {
isExpandedFormCloseAfterSave.value = false
if (!isForm.value) {
loadChildrenList()
}
loadChildrenExcludedList(rowState.value)
}
childrenExcludedOffsetCount.value = 0
childrenListOffsetCount.value = 0
})
watch(filterQueryRef, () => {
filterQueryRef.value?.focus()
})
const onClick = (refRow: any, id: string) => {
if (isSharedBase.value) return
if (isChildrenExcludedListLinked.value[Number.parseInt(id)]) {
unlinkRow(refRow, Number.parseInt(id))
} else {
linkRow(refRow, Number.parseInt(id))
}
}
const addNewRecord = () => {
expandedFormRow.value = {}
expandedFormDlg.value = true
isExpandedFormCloseAfterSave.value = true
}
const onCreatedRecord = (record: any) => {
const msgVNode = h(
'div',
{
class: 'ml-1 inline-flex flex-col gap-1 items-start',
},
[
h(
'span',
{
class: 'font-semibold',
},
t('activity.recordCreatedLinked'),
),
h(
'span',
{
class: 'text-gray-500',
},
t('activity.gotSavedLinkedSuccessfully', {
tableName: relatedTableMeta.value?.title,
recordTitle: record[relatedTableDisplayValueProp.value],
}),
),
],
)
message.success(msgVNode)
}
const linkedShortcuts = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
vModel.value = false
} else if (e.key === 'ArrowDown') {
e.preventDefault()
try {
e.target?.nextElementSibling?.focus()
} catch (e) {}
} else if (e.key === 'ArrowUp') {
e.preventDefault()
try {
e.target?.previousElementSibling?.focus()
} catch (e) {}
} else if (!expandedFormDlg.value && e.key !== 'Tab' && e.key !== 'Shift' && e.key !== 'Enter' && e.key !== ' ') {
try {
filterQueryRef.value?.focus()
} catch (e) {}
}
}
const childrenExcludedListRef = ref<HTMLDivElement>()
watch(childrenExcludedListPagination, () => {
childrenExcludedListRef.value?.scrollTo({ top: 0, behavior: 'smooth' })
})
onMounted(() => {
window.addEventListener('keydown', linkedShortcuts)
})
onUnmounted(() => {
childrenExcludedListPagination.query = ''
window.removeEventListener('keydown', linkedShortcuts)
})
</script>
<template>
<NcModal
v-model:visible="vModel"
:body-style="{ 'max-height': '640px', 'height': '85vh' }"
:class="{ active: vModel }"
:closable="false"
:footer="null"
:width="isForm ? 600 : 800"
wrap-class-name="nc-modal-link-record"
>
<LazyVirtualCellComponentsHeader
v-if="!isForm"
:display-value="headerDisplayValue"
:header="$t('activity.addNewLink')"
:related-table-title="relatedTableMeta?.title"
:relation="relation"
:table-title="meta?.title"
/>
<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 !focus-within:border-primary">
<MdiMagnify class="w-5 h-5 ml-2 text-gray-500" />
<a-input
ref="filterQueryRef"
v-model:value="childrenExcludedListPagination.query"
:bordered="false"
:placeholder="`${$t('general.searchIn')} ${relatedTableMeta?.title}`"
class="w-full !rounded-md nc-excluded-search xs:min-h-8"
size="small"
@change="childrenExcludedListPagination.page = 1"
@keydown.capture.stop="
(e) => {
if (e.key === 'Escape') {
filterQueryRef?.blur()
}
}
"
>
</a-input>
</div>
<div class="flex-1" />
<!-- Add new record -->
<NcButton
v-if="!isPublic"
v-e="['c:row-expand:open']"
:size="isMobileMode ? 'medium' : 'small'"
class="!text-brand-500"
type="secondary"
@click="addNewRecord"
>
<div class="flex items-center gap-1 px-4"><MdiPlus v-if="!isMobileMode" /> {{ $t('activity.newRecord') }}</div>
</NcButton>
</div>
<template v-if="childrenExcludedList?.pageInfo?.totalRows">
<div ref="childrenExcludedListRef" class="overflow-scroll nc-scrollbar-md pr-1 cursor-pointer flex flex-col flex-grow">
<template v-if="isChildrenExcludedLoading">
<div
v-for="(_x, i) in Array.from({ length: 10 })"
:key="i"
class="!border-2 flex flex-row gap-2 mb-2 transition-all !rounded-xl relative !border-gray-200 hover:bg-gray-50"
>
<a-skeleton-image class="h-24 w-24 !rounded-xl" />
<div class="flex flex-col m-[.5rem] gap-2 flex-grow justify-center">
<a-skeleton-input active class="!xs:w-30 !w-48 !rounded-xl" size="small" />
<div class="flex flex-row gap-6 w-10/12">
<div class="flex flex-col gap-0.5">
<a-skeleton-input active class="!h-4 !w-12" size="small" />
<a-skeleton-input active class="!xs:hidden !h-4 !w-24" size="small" />
</div>
<div class="flex flex-col gap-0.5">
<a-skeleton-input active class="!h-4 !w-12" size="small" />
<a-skeleton-input active class="!xs:hidden !h-4 !w-24" size="small" />
</div>
<div class="flex flex-col gap-0.5">
<a-skeleton-input active class="!h-4 !w-12" size="small" />
<a-skeleton-input active class="!xs:hidden !h-4 !w-24" size="small" />
</div>
<div class="flex flex-col gap-0.5">
<a-skeleton-input active class="!h-4 !w-12" size="small" />
<a-skeleton-input active class="!xs:hidden !h-4 !w-24" size="small" />
</div>
</div>
</div>
</div>
</template>
<template v-else>
<LazyVirtualCellComponentsListItem
v-for="(refRow, id) in childrenExcludedList?.list ?? []"
:key="id"
:attachment="attachmentCol"
:display-value-type-and-format-prop="displayValueTypeAndFormatProp"
:fields="fields"
:is-linked="isChildrenExcludedListLinked[Number.parseInt(id)]"
:is-loading="isChildrenExcludedListLoading[Number.parseInt(id)]"
:related-table-display-value-prop="relatedTableDisplayValueProp"
:row="refRow"
data-testid="nc-excluded-list-item"
@click="() => onClick(refRow, id)"
@expand="
() => {
expandedFormRow = refRow
expandedFormDlg = true
}
"
@keydown.space.prevent="() => onClick(refRow, id)"
@keydown.enter.prevent="() => onClick(refRow, id)"
/>
</template>
</div>
</template>
<div v-else class="my-auto py-2 flex flex-col gap-3 items-center justify-center text-gray-500">
<InboxIcon class="w-16 h-16 mx-auto" />
<p>
{{ $t('msg.thereAreNoRecordsInTable') }}
{{ relatedTableMeta?.title }}
</p>
</div>
<div v-if="isMobileMode" class="flex flex-row justify-center items-center w-full my-2">
<NcPagination
v-if="childrenExcludedList?.pageInfo"
v-model:current="childrenExcludedListPagination.page"
v-model:page-size="childrenExcludedListPagination.size"
:total="+childrenExcludedList?.pageInfo?.totalRows"
entity-name="links-excluded-list"
/>
</div>
<div class="mb-2 bg-gray-50 border-gray-50 border-b-2"></div>
<div class="flex flex-row justify-between items-center bg-white relative pt-1">
<div v-if="!isForm" class="flex items-center justify-center px-2 rounded-md text-gray-500 bg-brand-50 h-9.5">
{{ relation === 'bt' ? (row.row[relatedTableMeta?.title] ? '1' : 0) : childrenListCount ?? 'No' }}
{{ !isMobileMode ? $t('objects.records') : '' }} {{ !isMobileMode && childrenListCount !== 0 ? 'are' : '' }}
{{ $t('general.linked') }}
</div>
<div class="!xs:hidden flex absolute -mt-0.75 items-center py-2 justify-center w-full">
<NcPagination
v-if="childrenExcludedList?.pageInfo"
v-model:current="childrenExcludedListPagination.page"
v-model:page-size="childrenExcludedListPagination.size"
:total="+childrenExcludedList?.pageInfo?.totalRows"
entity-name="links-excluded-list"
mode="simple"
/>
</div>
<NcButton class="nc-close-btn ml-auto" type="ghost" @click="vModel = false"> {{ $t('general.finish') }} </NcButton>
</div>
<Suspense>
<LazySmartsheetExpandedForm
v-if="expandedFormDlg"
v-model="expandedFormDlg"
:close-after-save="isExpandedFormCloseAfterSave"
:meta="relatedTableMeta"
:new-record-header="
isExpandedFormCloseAfterSave
? $t('activity.tableNameCreateNewRecord', {
tableName: relatedTableMeta?.title,
})
: undefined
"
:row="{
row: expandedFormRow,
oldRow: {},
rowMeta:
Object.keys(expandedFormRow).length > 0
? {}
: {
new: true,
},
}"
:row-id="extractPkFromRow(expandedFormRow, relatedTableMeta.columns as ColumnType[])"
:state="newRowState"
use-meta-fields
@created-record="onCreatedRecord"
/>
</Suspense>
</NcModal>
</template>
<style lang="scss">
.nc-modal-link-record > .ant-modal > .ant-modal-content {
@apply !p-0;
}
</style>