Browse Source

Nc feat: Prefill links using form builder (#9254)

* fix(nc-gui): prefill virtual fields on clear form

* feat(nc-gui): prefill links using form builder

* chore(nc-gui): lint

* fix(nc-gui): In prefill link field if we remove one linked item, it removes all linked items

* fix(nc-gui): record count in linked record modal is 0 if it new row/form

* fix(nc-gui): oo link count update issue in new form row

* chore(nc-gui): lint
pull/9273/head
Ramesh Mane 3 months ago committed by GitHub
parent
commit
b3820868fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 17
      packages/nc-gui/components/nc/List.vue
  2. 99
      packages/nc-gui/components/smartsheet/Form.vue
  3. 2
      packages/nc-gui/components/virtual-cell/components/ListItem.vue
  4. 26
      packages/nc-gui/components/virtual-cell/components/UnLinkedItems.vue
  5. 24
      packages/nc-gui/composables/useSharedFormViewStore.ts

17
packages/nc-gui/components/nc/List.vue

@ -1,6 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useVirtualList } from '@vueuse/core' import { useVirtualList } from '@vueuse/core'
export type Placement = 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topRight'
export type RawValueType = string | number export type RawValueType = string | number
@ -30,12 +29,20 @@ interface Props {
optionLabelKey?: string optionLabelKey?: string
/** Whether the list is open or closed */ /** Whether the list is open or closed */
open?: boolean open?: boolean
/** Whether to close the list after an item is selected */ /**
* Whether to close the list after an item is selected
* @default true
*/
closeOnSelect?: boolean closeOnSelect?: boolean
/** Placeholder text for the search input */ /** Placeholder text for the search input */
searchInputPlaceholder?: string searchInputPlaceholder?: string
/** Whether to show the currently selected option */ /** Whether to show the currently selected option */
showSelectedOption?: boolean showSelectedOption?: boolean
/**
* The height of each item in the list, used for virtual list rendering.
* @default 38
*/
itemHeight?: number
/** Custom filter function for list items */ /** Custom filter function for list items */
filterOption?: (input: string, option: ListItem, index: Number) => boolean filterOption?: (input: string, option: ListItem, index: Number) => boolean
} }
@ -49,9 +56,11 @@ interface Emits {
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
open: false, open: false,
closeOnSelect: true, closeOnSelect: true,
searchInputPlaceholder: '',
showSelectedOption: true, showSelectedOption: true,
optionValueKey: 'value', optionValueKey: 'value',
optionLabelKey: 'label', optionLabelKey: 'label',
itemHeight: 38,
}) })
const emits = defineEmits<Emits>() const emits = defineEmits<Emits>()
@ -102,7 +111,7 @@ const {
wrapperProps, wrapperProps,
scrollTo, scrollTo,
} = useVirtualList(list, { } = useVirtualList(list, {
itemHeight: 38, itemHeight: props.itemHeight,
}) })
/** /**
@ -253,7 +262,7 @@ watch(
<a-input <a-input
ref="inputRef" ref="inputRef"
v-model:value="searchQuery" v-model:value="searchQuery"
:placeholder="searchInputPlaceholder || $t('placeholder.searchFields')" :placeholder="searchInputPlaceholder"
class="nc-toolbar-dropdown-search-field-input !pl-2 !pr-1.5" class="nc-toolbar-dropdown-search-field-input !pl-2 !pr-1.5"
allow-clear allow-clear
:bordered="false" :bordered="false"

99
packages/nc-gui/components/smartsheet/Form.vue

@ -6,6 +6,8 @@ import 'splitpanes/dist/splitpanes.css'
import { import {
type AttachmentResType, type AttachmentResType,
type ColumnType,
type LinkToAnotherRecordType,
ProjectRoles, ProjectRoles,
RelationTypes, RelationTypes,
UITypes, UITypes,
@ -55,6 +57,8 @@ const { $api, $e } = useNuxtApp()
const { isUIAllowed } = useRoles() const { isUIAllowed } = useRoles()
const { metas, getMeta } = useMetas()
const { base } = storeToRefs(useBase()) const { base } = storeToRefs(useBase())
const { getPossibleAttachmentSrc } = useAttachment() const { getPossibleAttachmentSrc } = useAttachment()
@ -94,7 +98,7 @@ const { preFillFormSearchParams } = storeToRefs(useViewsStore())
const reloadEventHook = inject(ReloadViewDataHookInj, createEventHook()) const reloadEventHook = inject(ReloadViewDataHookInj, createEventHook())
reloadEventHook.on(async () => { reloadEventHook.on(async () => {
await loadFormView() await Promise.all([loadFormView(), loadReleatedMetas()])
setFormData() setFormData()
}) })
@ -184,6 +188,41 @@ const onVisibilityChange = (state: 'showAddColumn' | 'showEditColumn') => {
const getFormLogoSrc = computed(() => getPossibleAttachmentSrc(parseProp(formViewData.value?.logo_url))) const getFormLogoSrc = computed(() => getPossibleAttachmentSrc(parseProp(formViewData.value?.logo_url)))
const getPrefillValue = (c: ColumnType, value: any) => {
let preFillValue: any
switch (c.uidt) {
case UITypes.LinkToAnotherRecord:
case UITypes.Links: {
const values = Array.isArray(value) ? value : [value]
const fk_related_model_id = (c?.colOptions as LinkToAnotherRecordType)?.fk_related_model_id
if (!fk_related_model_id) return
const rowIds = values
.map((row) => {
return extractPkFromRow(row, metas.value[fk_related_model_id].columns || [])
})
.filter((rowId) => !!rowId)
.join(',')
preFillValue = rowIds || undefined
// if bt/oo then extract object from array
if (c.colOptions?.type === RelationTypes.BELONGS_TO || c.colOptions?.type === RelationTypes.ONE_TO_ONE) {
preFillValue = rowIds[0]
}
break
}
default: {
return value
}
}
return preFillValue
}
const updatePreFillFormSearchParams = useDebounceFn(() => { const updatePreFillFormSearchParams = useDebounceFn(() => {
if (isLocked.value || !isUIAllowed('dataInsert')) return if (isLocked.value || !isUIAllowed('dataInsert')) return
@ -192,8 +231,20 @@ const updatePreFillFormSearchParams = useDebounceFn(() => {
const searchParams = new URLSearchParams() const searchParams = new URLSearchParams()
for (const c of visibleColumns.value) { for (const c of visibleColumns.value) {
if (c.title && preFilledData[c.title] && !isVirtualCol(c) && !(UITypes.Attachment === c.uidt)) { if (
searchParams.append(c.title, preFilledData[c.title]) !c.title ||
!isValidValue(preFilledData[c.title]) ||
(isVirtualCol(c) && !isLinksOrLTAR(c)) ||
isAttachment(c) ||
c.uidt === UITypes.SpecificDBType
) {
continue
}
const preFillValue = getPrefillValue(c, preFilledData[c.title])
if (preFillValue !== undefined) {
searchParams.append(c.title, preFillValue)
} }
} }
@ -562,6 +613,31 @@ const updateFieldTitle = (value: string) => {
} }
} }
const handleAutoScrollFormField = (title: string, isSidebar: boolean) => {
const field = document.querySelector(
`${isSidebar ? '.nc-form-field-item-' : '.nc-form-drag-'}${CSS.escape(title?.replaceAll(' ', ''))}`,
)
if (field) {
setTimeout(() => {
field?.scrollIntoView({ behavior: 'smooth', block: 'center' })
}, 50)
}
}
async function loadReleatedMetas() {
await Promise.all(
(localColumns.value || []).map(async (c: ColumnType) => {
const fk_related_model_id = (c?.colOptions as LinkToAnotherRecordType)?.fk_related_model_id
if (isVirtualCol(c) && isLinksOrLTAR(c) && fk_related_model_id) {
await getMeta(fk_related_model_id)
}
return c
}),
)
}
onMounted(async () => { onMounted(async () => {
if (imageCropperData.value.src) { if (imageCropperData.value.src) {
URL.revokeObjectURL(imageCropperData.value.imageConfig.src) URL.revokeObjectURL(imageCropperData.value.imageConfig.src)
@ -570,7 +646,9 @@ onMounted(async () => {
preFillFormSearchParams.value = '' preFillFormSearchParams.value = ''
isLoadingFormView.value = true isLoadingFormView.value = true
await loadFormView()
await Promise.all([loadFormView(), loadReleatedMetas()])
setFormData() setFormData()
isLoadingFormView.value = false isLoadingFormView.value = false
}) })
@ -600,7 +678,6 @@ watch(
for (const virtualField in state.value) { for (const virtualField in state.value) {
formState.value[virtualField] = state.value[virtualField] formState.value[virtualField] = state.value[virtualField]
} }
updatePreFillFormSearchParams() updatePreFillFormSearchParams()
try { try {
@ -616,18 +693,6 @@ watch(
}, },
) )
const handleAutoScrollFormField = (title: string, isSidebar: boolean) => {
const field = document.querySelector(
`${isSidebar ? '.nc-form-field-item-' : '.nc-form-drag-'}${CSS.escape(title?.replaceAll(' ', ''))}`,
)
if (field) {
setTimeout(() => {
field?.scrollIntoView({ behavior: 'smooth', block: 'center' })
}, 50)
}
}
watch(activeField, (newValue, oldValue) => { watch(activeField, (newValue, oldValue) => {
if (newValue && autoScrollFormField.value) { if (newValue && autoScrollFormField.value) {
nextTick(() => { nextTick(() => {

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

@ -28,6 +28,8 @@ const isForm = inject(IsFormInj, ref(false))
const row = useVModel(props, 'row') const row = useVModel(props, 'row')
const { isLinked, isLoading } = toRefs(props)
const isPublic = inject(IsPublicInj, ref(false)) const isPublic = inject(IsPublicInj, ref(false))
const readOnly = inject(ReadonlyInj, ref(false)) const readOnly = inject(ReadonlyInj, ref(false))

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

@ -60,10 +60,26 @@ const reloadTrigger = inject(ReloadRowDataHookInj, createEventHook())
const reloadViewDataTrigger = inject(ReloadViewDataHookInj, createEventHook()) const reloadViewDataTrigger = inject(ReloadViewDataHookInj, createEventHook())
const relation = computed(() => {
return injectedColumn!.value?.colOptions?.type
})
const linkRow = async (row: Record<string, any>, id: number) => { const linkRow = async (row: Record<string, any>, id: number) => {
if (isNew.value) { if (isNew.value) {
addLTARRef(row, injectedColumn?.value as ColumnType) addLTARRef(row, injectedColumn?.value as ColumnType)
isChildrenExcludedListLinked.value[id] = true if (relation.value === 'oo' || relation.value === 'bt') {
isChildrenExcludedListLinked.value.forEach((isLinked, idx) => {
if (isLinked) {
isChildrenExcludedListLinked.value[idx] = false
}
if (id === idx) {
isChildrenExcludedListLinked.value[idx] = true
}
})
} else {
isChildrenExcludedListLinked.value[id] = true
}
saveRow!() saveRow!()
$e('a:links:link') $e('a:links:link')
@ -157,12 +173,12 @@ const fields = computedInject(FieldsInj, (_fields) => {
.slice(0, isMobileMode.value ? 1 : 3) .slice(0, isMobileMode.value ? 1 : 3)
}) })
const relation = computed(() => {
return injectedColumn!.value?.colOptions?.type
})
const totalItemsToShow = computed(() => { const totalItemsToShow = computed(() => {
if (isForm.value || isNew.value) { if (isForm.value || isNew.value) {
if (relation.value === 'bt' || relation.value === 'oo') {
return rowState.value?.[injectedColumn!.value?.title] ? 1 : 0
}
return rowState.value?.[injectedColumn!.value?.title]?.length ?? 0 return rowState.value?.[injectedColumn!.value?.title]?.length ?? 0
} }

24
packages/nc-gui/composables/useSharedFormViewStore.ts

@ -63,6 +63,8 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
const preFilledformState = ref<Record<string, any>>({}) const preFilledformState = ref<Record<string, any>>({})
const preFilledAdditionalState = ref<Record<string, any>>({})
const preFilledDefaultValueformState = ref<Record<string, any>>({}) const preFilledDefaultValueformState = ref<Record<string, any>>({})
useProvideSmartsheetLtarHelpers(meta) useProvideSmartsheetLtarHelpers(meta)
@ -339,7 +341,10 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
const clearForm = async () => { const clearForm = async () => {
formResetHook.trigger() formResetHook.trigger()
additionalState.value = {} additionalState.value = {
...(sharedViewMeta.value.preFillEnabled ? preFilledAdditionalState.value : {}),
}
formState.value = { formState.value = {
...preFilledDefaultValueformState.value, ...preFilledDefaultValueformState.value,
...(sharedViewMeta.value.preFillEnabled ? preFilledformState.value : {}), ...(sharedViewMeta.value.preFillEnabled ? preFilledformState.value : {}),
@ -372,14 +377,21 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
if (preFillValue !== undefined) { if (preFillValue !== undefined) {
if (isLinksOrLTAR(c)) { if (isLinksOrLTAR(c)) {
// Prefill Link to another record / Links form state // Prefill Link to another record / Links form state
additionalState.value[c.title] = preFillValue
additionalState.value = {
...(additionalState.value || {}),
[c.title]: preFillValue,
}
// preFilledAdditionalState will be used in clear form to fill the prefilled data
preFilledAdditionalState.value[c.title] = preFillValue
} else { } else {
// Prefill form state // Prefill form state
formState.value[c.title] = preFillValue formState.value[c.title] = preFillValue
}
// preFilledformState will be used in clear form to fill the prefilled data // preFilledformState will be used in clear form to fill the prefilled data
preFilledformState.value[c.title] = preFillValue preFilledformState.value[c.title] = preFillValue
}
// Update column // Update column
switch (sharedViewMeta.value.preFilledMode) { switch (sharedViewMeta.value.preFilledMode) {
@ -404,7 +416,7 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
return (c?.source_id ? sqlUis.value[c?.source_id] : Object.values(sqlUis.value)[0])?.getAbstractType(c) return (c?.source_id ? sqlUis.value[c?.source_id] : Object.values(sqlUis.value)[0])?.getAbstractType(c)
} }
async function getPreFillValue(c: ColumnType, value: string) { async function getPreFillValue(c: ColumnType, value: string | string[]) {
let preFillValue: any let preFillValue: any
switch (c.uidt) { switch (c.uidt) {
case UITypes.SingleSelect: case UITypes.SingleSelect:

Loading…
Cancel
Save