import { type ColumnType, CommonAggregations, type GridColumnReqType, type GridColumnType, type MapType, type TableType, type ViewType, } from 'nocodb-sdk' import { ViewTypes, isHiddenCol, isSystemColumn } from 'nocodb-sdk' import type { ComputedRef, Ref } from 'vue' const [useProvideViewColumns, useViewColumns] = useInjectionState( ( view: Ref, meta: Ref | ComputedRef, reloadData?: (params?: { shouldShowLoading?: boolean }) => void, isPublic = false, ) => { const rootFields = ref([]) const fields = ref() const fieldsMap = computed(() => (fields.value || []).reduce>((acc, curr) => { if (curr.fk_column_id) { acc[curr.fk_column_id] = curr } return acc }, {}), ) const filterQuery = ref('') const { $api, $e } = useNuxtApp() const { isUIAllowed } = useRoles() const { isSharedBase } = storeToRefs(useBase()) const isViewColumnsLoading = ref(false) const { addUndo, defineViewScope } = useUndoRedo() const isLocalMode = computed(() => isPublic || !isUIAllowed('viewFieldEdit') || isSharedBase.value) const localChanges = ref>({}) const isColumnViewEssential = (column: ColumnType) => { // TODO: consider at some point ti delegate this via a cleaner design pattern to view specific check logic // which could be inside of a view specific helper class (and generalized via an interface) // (on the other hand, the logic complexity is still very low atm - might be overkill) return view.value?.type === ViewTypes.MAP && (view.value?.view as MapType)?.fk_geo_data_col_id === column.id } const metaColumnById = computed>(() => { if (!meta.value?.columns) return {} return (meta.value.columns as ColumnType[]).reduce( (acc, curr) => ({ ...acc, [curr.id!]: curr, }), {}, ) as Record }) const gridViewCols = ref>({}) const loadViewColumns = async () => { if (!meta || !view) return let order = 1 if (view.value?.id) { const data = (isPublic ? meta.value?.columns : (await $api.dbViewColumn.list(view.value.id)).list) as any[] const fieldById = data.reduce>((acc, curr) => { curr.show = !!curr.show return { ...acc, [curr.fk_column_id]: curr, } }, {}) fields.value = meta.value?.columns ?.filter((column: ColumnType) => { // filter created by and last modified by system columns if (isHiddenCol(column, meta.value)) return false return true }) .map((column: ColumnType) => { const currentColumnField = fieldById[column.id!] || {} return { title: column.title, fk_column_id: column.id, ...currentColumnField, show: currentColumnField.show || isColumnViewEssential(currentColumnField), order: currentColumnField.order || order++, aggregation: currentColumnField?.aggregation ?? CommonAggregations.None, system: isSystemColumn(metaColumnById?.value?.[currentColumnField.fk_column_id!]), isViewEssentialField: isColumnViewEssential(column), initialShow: currentColumnField.show || isColumnViewEssential(currentColumnField) || (currentColumnField as GridColumnType)?.group_by, } }) .sort((a: Field, b: Field) => a.order - b.order) if (isLocalMode.value && fields.value) { for (const key in localChanges.value) { const fieldIndex = fields.value.findIndex((f) => f.fk_column_id === key) if (fieldIndex !== undefined && fieldIndex > -1) { fields.value[fieldIndex] = localChanges.value[key] fields.value = fields.value.sort((a: Field, b: Field) => a.order - b.order) } } } const colsData: GridColumnType[] = (isPublic.value ? view.value?.columns : fields.value) ?? [] gridViewCols.value = colsData.reduce>( (o, col) => ({ ...o, [col.fk_column_id as string]: col, }), {}, ) } } const showAll = async (ignoreIds?: any) => { if (isLocalMode.value) { const fieldById = (fields.value || []).reduce>((acc, curr) => { if (curr.fk_column_id) { curr.show = !!curr.initialShow acc[curr.fk_column_id] = curr } return acc }, {}) fields.value = (fields.value || [])?.map((field: Field) => { const updateField = { ...field, show: fieldById[field.fk_column_id!]?.show, } localChanges.value[field.fk_column_id!] = field return updateField }) meta.value!.columns = meta.value!.columns?.map((column: ColumnType) => { if (fieldById[column.id!]) { return { ...column, ...fieldById[column.id!], id: fieldById[column.id!].fk_column_id, } } return column }) reloadData?.() return } if (view?.value?.id) { if (ignoreIds) { await $api.dbView.showAllColumn(view.value.id, { ignoreIds, }) } else { await $api.dbView.showAllColumn(view.value.id) } } await loadViewColumns() reloadData?.() $e('a:fields:show-all') } const hideAll = async (ignoreIds?: any) => { if (isLocalMode.value) { const fieldById = (fields.value || []).reduce>((acc, curr) => { if (curr.fk_column_id) { curr.show = !!metaColumnById?.value?.[curr.fk_column_id!]?.pv || !!curr.isViewEssentialField acc[curr.fk_column_id] = curr } return acc }, {}) fields.value = (fields.value || [])?.map((field: Field) => { const updateField = { ...field, show: fieldById[field.fk_column_id!]?.show, } localChanges.value[field.fk_column_id!] = field return updateField }) meta.value!.columns = meta.value!.columns?.map((column: ColumnType) => { if (fieldById[column.id!]) { return { ...column, ...fieldById[column.id!], id: fieldById[column.id!].fk_column_id, } } return column }) reloadData?.() return } if (view?.value?.id) { if (ignoreIds) { await $api.dbView.hideAllColumn(view.value.id, { ignoreIds, }) } else { await $api.dbView.hideAllColumn(view.value.id) } } await loadViewColumns() reloadData?.() $e('a:fields:show-all') } const saveOrUpdate = async ( field: any, index: number, disableDataReload: boolean = false, updateDefaultViewColumnOrder: boolean = false, ) => { if (isLocalMode.value && fields.value) { fields.value[index] = field meta.value!.columns = meta.value!.columns?.map((column: ColumnType) => { if (column.id === field.fk_column_id) { return { ...column, ...field, id: field.fk_column_id, ...(updateDefaultViewColumnOrder ? { meta: { ...parseProp(column.meta), defaultViewColOrder: field.order } } : {}), } } return column }) localChanges.value[field.fk_column_id] = field } if (isUIAllowed('viewFieldEdit')) { if (field.id && view?.value?.id) { await $api.dbViewColumn.update(view.value.id, field.id, field) } else if (view.value?.id) { const insertedField = (await $api.dbViewColumn.create(view.value.id, field)) as any /** update the field in fields if defined */ if (fields.value) fields.value[index] = insertedField return insertedField } } if (!disableDataReload) { await loadViewColumns() reloadData?.({ shouldShowLoading: false }) } } const showSystemFields = computed({ get() { return (view.value?.show_system_fields as boolean) || false }, set(v: boolean) { if (view?.value?.id) { if (!isLocalMode.value) { $api.dbView .update(view.value.id, { show_system_fields: v, }) .finally(() => { loadViewColumns() reloadData?.() }) } view.value.show_system_fields = v } $e('a:fields:system-fields') }, }) const filteredFieldList = computed(() => { return ( fields.value?.filter((field: Field) => { if (!field.initialShow && isLocalMode.value) { return false } if ( metaColumnById?.value?.[field.fk_column_id!]?.pv && (!filterQuery.value || field.title.toLowerCase().includes(filterQuery.value.toLowerCase())) ) { return true } // hide system columns if not enabled if (!showSystemFields.value && isSystemColumn(metaColumnById?.value?.[field.fk_column_id!])) { return false } if (filterQuery.value === '') { return true } else { return field.title.toLowerCase().includes(filterQuery.value.toLowerCase()) } }) || [] ) }) const numberOfHiddenFields = computed(() => { return (fields.value || []) ?.filter((field: Field) => { if (!field.initialShow && isLocalMode.value) { return false } if (metaColumnById?.value?.[field.fk_column_id!]?.pv) { return true } // hide system columns if not enabled if (!showSystemFields.value && isSystemColumn(metaColumnById?.value?.[field.fk_column_id!])) { return false } return true }) .filter((field) => !field.show)?.length }) const sortedAndFilteredFields = computed(() => { return (fields?.value ?.filter((field: Field) => { // hide system columns if not enabled if ( !showSystemFields.value && metaColumnById.value && metaColumnById?.value?.[field.fk_column_id!] && isSystemColumn(metaColumnById.value?.[field.fk_column_id!]) && !metaColumnById.value?.[field.fk_column_id!]?.pv ) { return false } return field.show && metaColumnById?.value?.[field.fk_column_id!] }) ?.sort((a: Field, b: Field) => a.order - b.order) ?.map((field: Field) => metaColumnById?.value?.[field.fk_column_id!]) || []) as ColumnType[] }) const toggleFieldVisibility = (checked: boolean, field: any) => { const fieldIndex = fields.value?.findIndex((f) => f.fk_column_id === field.fk_column_id) if (!fieldIndex && fieldIndex !== 0) return addUndo({ undo: { fn: (v: boolean) => { field.show = !v saveOrUpdate(field, fieldIndex) }, args: [checked], }, redo: { fn: (v: boolean) => { field.show = v saveOrUpdate(field, fieldIndex) }, args: [checked], }, scope: defineViewScope({ view: view.value }), }) saveOrUpdate(field, fieldIndex, !checked) } const toggleFieldStyles = (field: any, style: 'underline' | 'bold' | 'italic', status: boolean) => { const fieldIndex = fields.value?.findIndex((f) => f.fk_column_id === field.fk_column_id) if (!fieldIndex && fieldIndex !== 0) return field[style] = status $e('a:fields:style', { style, status }) saveOrUpdate(field, fieldIndex, true) } // reload view columns when active view changes // or when columns changes(delete/add) watch( [() => view?.value?.id, () => meta.value?.columns], async ([newViewId]) => { // reload only if view belongs to current table if (newViewId && view.value?.fk_model_id === meta.value?.id) { isViewColumnsLoading.value = true try { await loadViewColumns() } catch (e) { console.error(e) } isViewColumnsLoading.value = false } }, { immediate: true }, ) const resizingColOldWith = ref('180px') const updateGridViewColumn = async (id: string, props: Partial, undo = false) => { if (!undo) { const oldProps = Object.keys(props).reduce>((o: any, k) => { if (gridViewCols.value[id][k as keyof GridColumnType]) { if (k === 'width') o[k] = `${resizingColOldWith.value}px` else o[k] = gridViewCols.value[id][k as keyof GridColumnType] } return o }, {}) addUndo({ redo: { fn: (w: Partial) => updateGridViewColumn(id, w, true), args: [props], }, undo: { fn: (w: Partial) => updateGridViewColumn(id, w, true), args: [oldProps], }, scope: defineViewScope({ view: view.value }), }) } try { // sync with server if allowed if (!isPublic.value && isUIAllowed('viewFieldEdit') && gridViewCols.value[id]?.id) { await $api.dbView.gridColumnUpdate(gridViewCols.value[id].id as string, { ...props, }) } if (gridViewCols.value?.[id]) { Object.assign(gridViewCols.value[id], { ...gridViewCols.value[id], ...props, }) } else { // fallback to reload await loadViewColumns() } } catch (e) { // this could happen if user doesn't have permission to update view columns // todo: find out root cause and handle with isUIAllowed console.error(e) } } watch( sortedAndFilteredFields, (v) => { if (rootFields) rootFields.value = v || [] }, { immediate: true }, ) provide(FieldsInj, rootFields) return { fields, fieldsMap, loadViewColumns, filteredFieldList, numberOfHiddenFields, filterQuery, showAll, hideAll, saveOrUpdate, sortedAndFilteredFields, showSystemFields, metaColumnById, toggleFieldVisibility, toggleFieldStyles, isViewColumnsLoading, updateGridViewColumn, gridViewCols, resizingColOldWith, isLocalMode, } }, 'useViewColumnsOrThrow', ) export { useProvideViewColumns } export function useViewColumnsOrThrow() { const viewColumns = useViewColumns() if (viewColumns == null) throw new Error('Please call `useProvideViewColumns` on the appropriate parent component') return viewColumns }