diff --git a/packages/nc-gui/components/shared-view/Grid.vue b/packages/nc-gui/components/shared-view/Grid.vue index 747226cf83..7e14057284 100644 --- a/packages/nc-gui/components/shared-view/Grid.vue +++ b/packages/nc-gui/components/shared-view/Grid.vue @@ -23,7 +23,7 @@ const { signedIn } = useGlobal() const { loadProject } = useBase() -const { isLocked } = useProvideSmartsheetStore(sharedView, meta, true, ref([]), nestedFilters) +const { isLocked, xWhere } = useProvideSmartsheetStore(sharedView, meta, true, ref([]), nestedFilters) useProvideKanbanViewStore(meta, sharedView) useProvideCalendarViewStore(meta, sharedView) @@ -41,6 +41,7 @@ provide(IsPublicInj, ref(true)) provide(IsLockedInj, isLocked) useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true) +useProvideViewGroupBy(sharedView, meta, xWhere, true) if (signedIn.value) { try { diff --git a/packages/nc-gui/components/smartsheet/grid/GroupBy.vue b/packages/nc-gui/components/smartsheet/grid/GroupBy.vue index ac4105709c..0db4a55d09 100644 --- a/packages/nc-gui/components/smartsheet/grid/GroupBy.vue +++ b/packages/nc-gui/components/smartsheet/grid/GroupBy.vue @@ -37,6 +37,16 @@ const { isViewDataLoading, isPaginationLoading } = storeToRefs(useViewsStore()) const reloadViewDataHook = inject(ReloadViewDataHookInj, createEventHook()) +const _loadGroupData = async (group: Group, force?: boolean, params?: any) => { + isViewDataLoading.value = true + isPaginationLoading.value = true + + await props.loadGroupData(group, force, params) + + isViewDataLoading.value = false + isPaginationLoading.value = false +} + const _depth = props.depth ?? 0 const wrapper = ref() @@ -67,12 +77,12 @@ const findAndLoadSubGroup = (key: any) => { if (key.length > 0 && vGroup.value.children) { if (!oldActiveGroups.value.includes(key[key.length - 1])) { const k = key[key.length - 1].replace('group-panel-', '') - const grp = vGroup.value.children[k] + const grp = vGroup.value.children.find((g) => `${g.key}` === k) if (grp) { if (grp.nested) { if (!grp.children?.length) props.loadGroups({}, grp) } else { - if (!grp.rows?.length || grp.count !== grp.rows?.length) props.loadGroupData(grp) + if (!grp.rows?.length || grp.count !== grp.rows?.length) _loadGroupData(grp) } } } @@ -84,37 +94,35 @@ const reloadViewDataHandler = (params: void | { shouldShowLoading?: boolean | un if (vGroup.value.nested) { props.loadGroups({ ...(params?.offset !== undefined ? { offset: params.offset } : {}) }, vGroup.value) } else { - props.loadGroupData(vGroup.value, true, { + _loadGroupData(vGroup.value, true, { ...(params?.offset !== undefined ? { offset: params.offset } : {}), }) } } +onMounted(async () => { + reloadViewDataHook?.on(reloadViewDataHandler) +}) + onBeforeUnmount(async () => { reloadViewDataHook?.off(reloadViewDataHandler) }) -reloadViewDataHook?.on(reloadViewDataHandler) - -watch( - [() => vGroup.value.key], - async (n, o) => { - if (n !== o) { - isViewDataLoading.value = true - isPaginationLoading.value = true - - if (vGroup.value.nested) { - await props.loadGroups({}, vGroup.value) - } else { - await props.loadGroupData(vGroup.value, true) - } - - isViewDataLoading.value = false - isPaginationLoading.value = false +watch([() => vGroup.value.key], async (n, o) => { + if (n !== o) { + if (!vGroup.value.nested) { + await _loadGroupData(vGroup.value, true) + } else if (vGroup.value.nested) { + await props.loadGroups({}, vGroup.value) } - }, - { immediate: true }, -) + } +}) + +onMounted(async () => { + if (vGroup.value.root === true) { + await props.loadGroups({}, vGroup.value) + } +}) if (vGroup.value.root === true) provide(ScrollParentInj, wrapper) @@ -231,7 +239,7 @@ const shouldRenderCell = (column) => > @@ -328,7 +336,7 @@ const shouldRenderCell = (column) => v-if="!grp.nested && grp.rows" :group="grp" :load-groups="loadGroups" - :load-group-data="loadGroupData" + :load-group-data="_loadGroupData" :load-group-page="loadGroupPage" :group-wrapper-change-page="groupWrapperChangePage" :row-height="rowHeight" @@ -345,7 +353,7 @@ const shouldRenderCell = (column) => v-else :group="grp" :load-groups="loadGroups" - :load-group-data="loadGroupData" + :load-group-data="_loadGroupData" :load-group-page="loadGroupPage" :group-wrapper-change-page="groupWrapperChangePage" :row-height="rowHeight" diff --git a/packages/nc-gui/components/smartsheet/grid/index.vue b/packages/nc-gui/components/smartsheet/grid/index.vue index 85d1c64c8a..ac27c227ca 100644 --- a/packages/nc-gui/components/smartsheet/grid/index.vue +++ b/packages/nc-gui/components/smartsheet/grid/index.vue @@ -20,7 +20,7 @@ import { ref, useSmartsheetStoreOrThrow, useViewData, - useViewGroupBy, + useViewGroupByOrThrow, } from '#imports' import type { Row } from '#imports' @@ -166,7 +166,7 @@ const toggleOptimisedQuery = () => { } const { rootGroup, groupBy, isGroupBy, loadGroups, loadGroupData, loadGroupPage, groupWrapperChangePage, redistributeRows } = - useViewGroupBy(view, xWhere) + useViewGroupByOrThrow() const coreWrapperRef = ref() diff --git a/packages/nc-gui/components/smartsheet/header/Menu.vue b/packages/nc-gui/components/smartsheet/header/Menu.vue index 6b5069c16a..444d09452f 100644 --- a/packages/nc-gui/components/smartsheet/header/Menu.vue +++ b/packages/nc-gui/components/smartsheet/header/Menu.vue @@ -56,7 +56,7 @@ const showDeleteColumnModal = ref(false) const { gridViewCols } = useViewColumnsOrThrow() -const { fieldsToGroupBy, groupByLimit } = useViewGroupBy(view) +const { fieldsToGroupBy, groupByLimit } = useViewGroupByOrThrow(view) const setAsDisplayValue = async () => { try { diff --git a/packages/nc-gui/components/smartsheet/toolbar/CreateGroupBy.vue b/packages/nc-gui/components/smartsheet/toolbar/CreateGroupBy.vue index 98c9cfb82d..15c49464e9 100644 --- a/packages/nc-gui/components/smartsheet/toolbar/CreateGroupBy.vue +++ b/packages/nc-gui/components/smartsheet/toolbar/CreateGroupBy.vue @@ -18,7 +18,7 @@ const meta = inject(MetaInj, ref()) const { showSystemFields, metaColumnById } = useViewColumnsOrThrow() -const { groupBy } = useViewGroupBy(activeView) +const { groupBy } = useViewGroupByOrThrow() const options = computed( () => diff --git a/packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue b/packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue index d1b48414aa..443be829bb 100644 --- a/packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue +++ b/packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue @@ -22,7 +22,7 @@ const isLocked = inject(IsLockedInj, ref(false)) const { gridViewCols, updateGridViewColumn, metaColumnById, showSystemFields } = useViewColumnsOrThrow() -const { fieldsToGroupBy, groupByLimit } = useViewGroupBy(view) +const { fieldsToGroupBy, groupByLimit } = useViewGroupByOrThrow() const { $e } = useNuxtApp() diff --git a/packages/nc-gui/components/tabs/Smartsheet.vue b/packages/nc-gui/components/tabs/Smartsheet.vue index bc4e2a23b1..9e9efa2b85 100644 --- a/packages/nc-gui/components/tabs/Smartsheet.vue +++ b/packages/nc-gui/components/tabs/Smartsheet.vue @@ -54,7 +54,7 @@ const { handleSidebarOpenOnMobileForNonViews } = useConfigStore() const { activeTableId } = storeToRefs(useTablesStore()) const { activeView, openedViewsTab, activeViewTitleOrId } = storeToRefs(useViewsStore()) -const { isGallery, isGrid, isForm, isKanban, isLocked, isMap, isCalendar } = useProvideSmartsheetStore(activeView, meta) +const { isGallery, isGrid, isForm, isKanban, isLocked, isMap, isCalendar, xWhere } = useProvideSmartsheetStore(activeView, meta) useSqlEditor() @@ -86,6 +86,7 @@ provide( ) useProvideViewColumns(activeView, meta, () => reloadViewDataEventHook?.trigger()) +useProvideViewGroupBy(activeView, meta, xWhere) const grid = ref() diff --git a/packages/nc-gui/composables/useSharedFormViewStore.ts b/packages/nc-gui/composables/useSharedFormViewStore.ts index 1631c39fb7..1f18338155 100644 --- a/packages/nc-gui/composables/useSharedFormViewStore.ts +++ b/packages/nc-gui/composables/useSharedFormViewStore.ts @@ -217,7 +217,7 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share ((column.rqd && !column.cdf) || (column.pk && !(column.ai || column.cdf)) || column.required) ) { obj.localState[column.title!] = { - required: fieldRequired(undefined, column.uidt === UITypes.Checkbox && column.required ? true : false), + required: fieldRequired(undefined, !!(column.uidt === UITypes.Checkbox && column.required)), } } else if ( isLinksOrLTAR(column) && diff --git a/packages/nc-gui/composables/useViewGroupBy.ts b/packages/nc-gui/composables/useViewGroupBy.ts index d70e7d67d7..dafe5c8683 100644 --- a/packages/nc-gui/composables/useViewGroupBy.ts +++ b/packages/nc-gui/composables/useViewGroupBy.ts @@ -1,5 +1,5 @@ import { UITypes } from 'nocodb-sdk' -import type { ColumnType, LinkToAnotherRecordType, LookupType, SelectOptionsType, ViewType } from 'nocodb-sdk' +import type { ColumnType, LinkToAnotherRecordType, LookupType, SelectOptionsType, TableType, ViewType } from 'nocodb-sdk' import type { Ref } from 'vue' import { message } from 'ant-design-vue' import { extractSdkResponseErrorMsg } from '../utils' @@ -17,548 +17,557 @@ import type { Group, GroupNestedIn, Row } from '#imports' const excludedGroupingUidt = [UITypes.Attachment, UITypes.QrCode, UITypes.Barcode] -export const useViewGroupBy = (view: Ref, where?: ComputedRef) => { - const groupByLimit: number = 3 +const [useProvideViewGroupBy, useViewGroupBy] = useInjectionState( + ( + view: Ref, + meta: Ref | ComputedRef, + where?: ComputedRef, + isPublic = false, + ) => { + const groupByLimit: number = 3 - const { api } = useApi() + const { api } = useApi() - const { appInfo } = useGlobal() + const { appInfo } = useGlobal() - const { base } = storeToRefs(useBase()) + const { base } = storeToRefs(useBase()) - const { sharedView, fetchSharedViewData } = useSharedView() + const { sharedView, fetchSharedViewData } = useSharedView() - const meta = inject(MetaInj) + const { gridViewCols } = useViewColumnsOrThrow() - const { gridViewCols } = useViewColumnsOrThrow() + const { getMeta } = useMetas() - const { getMeta } = useMetas() + const sharedViewPassword = inject(SharedViewPasswordInj, ref(null)) - const sharedViewPassword = inject(SharedViewPasswordInj, ref(null)) - - const groupBy = computed<{ column: ColumnType; sort: string; order?: number }[]>(() => { - const tempGroupBy: { column: ColumnType; sort: string; order?: number }[] = [] - Object.values(gridViewCols.value).forEach((col) => { - if (col.group_by) { - const column = meta?.value?.columns?.find((f) => f.id === col.fk_column_id) - if (column) { - tempGroupBy.push({ - column, - sort: col.group_by_sort || 'asc', - order: col.group_by_order || 1, - }) + const groupBy = computed<{ column: ColumnType; sort: string; order?: number }[]>(() => { + const tempGroupBy: { column: ColumnType; sort: string; order?: number }[] = [] + Object.values(gridViewCols.value).forEach((col) => { + if (col.group_by) { + const column = meta?.value?.columns?.find((f) => f.id === col.fk_column_id) + if (column) { + tempGroupBy.push({ + column, + sort: col.group_by_sort || 'asc', + order: col.group_by_order || 1, + }) + } } - } + }) + tempGroupBy.sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity)) + return tempGroupBy }) - tempGroupBy.sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity)) - return tempGroupBy - }) - - const isGroupBy = computed(() => !!groupBy.value.length) - const { isUIAllowed } = useRoles() + const isGroupBy = computed(() => !!groupBy.value.length) - const { sorts, nestedFilters } = useSmartsheetStoreOrThrow() + const { isUIAllowed } = useRoles() - const isPublic = inject(IsPublicInj, ref(false)) + const { sorts, nestedFilters } = useSmartsheetStoreOrThrow() - const reloadViewDataHook = inject(ReloadViewDataHookInj, createEventHook()) + const reloadViewDataHook = inject(ReloadViewDataHookInj, createEventHook()) - const groupByGroupLimit = computed(() => { - return appInfo.value.defaultGroupByLimit?.limitGroup || 10 - }) + const groupByGroupLimit = computed(() => { + return appInfo.value.defaultGroupByLimit?.limitGroup || 10 + }) - const groupByRecordLimit = computed(() => { - return appInfo.value.defaultGroupByLimit?.limitRecord || 10 - }) + const groupByRecordLimit = computed(() => { + return appInfo.value.defaultGroupByLimit?.limitRecord || 10 + }) - const supportedLookups = ref([]) + const supportedLookups = ref([]) - const fieldsToGroupBy = computed(() => - (meta?.value?.columns || []).filter((field) => { - if (excludedGroupingUidt.includes(field.uidt as UITypes)) return false + const fieldsToGroupBy = computed(() => + (meta?.value?.columns || []).filter((field) => { + if (excludedGroupingUidt.includes(field.uidt as UITypes)) return false - if (field.uidt === UITypes.Lookup) { - return field.id && supportedLookups.value.includes(field.id) - } + if (field.uidt === UITypes.Lookup) { + return field.id && supportedLookups.value.includes(field.id) + } - return true - }), - ) - - const rootGroup = ref({ - key: 'root', - color: 'root', - count: 0, - column: {} as any, - nestedIn: [], - paginationData: { page: 1, pageSize: groupByGroupLimit.value }, - nested: true, - children: [], - root: true, - }) - - async function groupWrapperChangePage(page: number, groupWrapper?: Group) { - groupWrapper = groupWrapper || rootGroup.value - - if (!groupWrapper) return - - groupWrapper.paginationData.page = page - await loadGroups( - { - offset: (page - 1) * (groupWrapper.paginationData.pageSize || groupByGroupLimit.value), - } as any, - groupWrapper, + return true + }), ) - } - - const formatData = (list: Record[]) => - list.map((row) => ({ - row: { ...row }, - oldRow: { ...row }, - rowMeta: {}, - })) - - const valueToTitle = (value: string, col: ColumnType) => { - if (col.uidt === UITypes.Checkbox) { - return value ? GROUP_BY_VARS.TRUE : GROUP_BY_VARS.FALSE + + const rootGroup = ref({ + key: 'root', + color: 'root', + count: 0, + column: {} as any, + nestedIn: [], + paginationData: { page: 1, pageSize: groupByGroupLimit.value }, + nested: true, + children: [], + root: true, + }) + + async function groupWrapperChangePage(page: number, groupWrapper?: Group) { + groupWrapper = groupWrapper || rootGroup.value + + if (!groupWrapper) return + + groupWrapper.paginationData.page = page + await loadGroups( + { + offset: (page - 1) * (groupWrapper.paginationData.pageSize || groupByGroupLimit.value), + } as any, + groupWrapper, + ) } - if ([UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy].includes(col.uidt as UITypes)) { - if (!value) { - return GROUP_BY_VARS.NULL + const formatData = (list: Record[]) => + list.map((row) => ({ + row: { ...row }, + oldRow: { ...row }, + rowMeta: {}, + })) + + const valueToTitle = (value: string, col: ColumnType) => { + if (col.uidt === UITypes.Checkbox) { + return value ? GROUP_BY_VARS.TRUE : GROUP_BY_VARS.FALSE } - } - // convert to JSON string if non-string value - if (value && typeof value === 'object') { - value = JSON.stringify(value) - } + if ([UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy].includes(col.uidt as UITypes)) { + if (!value) { + return GROUP_BY_VARS.NULL + } + } - return value ?? GROUP_BY_VARS.NULL - } + // convert to JSON string if non-string value + if (value && typeof value === 'object') { + value = JSON.stringify(value) + } + + return value ?? GROUP_BY_VARS.NULL + } - const colors = ref(enumColor.light) + const colors = ref(enumColor.light) - const nextGroupColor = ref(colors.value[0]) + const nextGroupColor = ref(colors.value[0]) - const getNextColor = () => { - const tempColor = nextGroupColor.value - const index = colors.value.indexOf(nextGroupColor.value) - if (index === colors.value.length - 1) { - nextGroupColor.value = colors.value[0] - } else { - nextGroupColor.value = colors.value[index + 1] + const getNextColor = () => { + const tempColor = nextGroupColor.value + const index = colors.value.indexOf(nextGroupColor.value) + if (index === colors.value.length - 1) { + nextGroupColor.value = colors.value[0] + } else { + nextGroupColor.value = colors.value[index + 1] + } + return tempColor } - return tempColor - } - - const findKeyColor = (key?: string, col?: ColumnType): string => { - if (col) { - switch (col.uidt) { - case UITypes.MultiSelect: { - const keys = key?.split(',') || [] - const colors = [] - for (const k of keys) { - const option = (col.colOptions as SelectOptionsType).options?.find((o) => o.title === k) - if (option) { - colors.push(option.color) + + const findKeyColor = (key?: string, col?: ColumnType): string => { + if (col) { + switch (col.uidt) { + case UITypes.MultiSelect: { + const keys = key?.split(',') || [] + const colors = [] + for (const k of keys) { + const option = (col.colOptions as SelectOptionsType).options?.find((o) => o.title === k) + if (option) { + colors.push(option.color) + } } + return colors.join(',') } - return colors.join(',') - } - case UITypes.SingleSelect: { - const option = (col.colOptions as SelectOptionsType).options?.find((o) => o.title === key) - if (option) { - return option.color || getNextColor() + case UITypes.SingleSelect: { + const option = (col.colOptions as SelectOptionsType).options?.find((o) => o.title === key) + if (option) { + return option.color || getNextColor() + } + return 'gray' } - return 'gray' - } - case UITypes.Checkbox: { - if (key) { - return themeColors.success + case UITypes.Checkbox: { + if (key) { + return themeColors.success + } + return themeColors.error } - return themeColors.error + default: + return key ? getNextColor() : 'gray' } - default: - return key ? getNextColor() : 'gray' } + return key ? getNextColor() : 'gray' } - return key ? getNextColor() : 'gray' - } - - const calculateNestedWhere = (nestedIn: GroupNestedIn[], existing = '') => { - return nestedIn.reduce((acc, curr) => { - if (curr.key === GROUP_BY_VARS.NULL) { - acc += `${acc.length ? '~and' : ''}(${curr.title},gb_null)` - } else if (curr.column_uidt === UITypes.Checkbox) { - acc += `${acc.length ? '~and' : ''}(${curr.title},${curr.key === GROUP_BY_VARS.TRUE ? 'checked' : 'notchecked'})` - } else if ( - [UITypes.Date, UITypes.DateTime, UITypes.CreatedTime, UITypes.LastModifiedTime].includes(curr.column_uidt as UITypes) - ) { - acc += `${acc.length ? '~and' : ''}(${curr.title},eq,exactDate,${curr.key})` - } else if ([UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy].includes(curr.column_uidt as UITypes)) { - try { - const value = JSON.parse(curr.key) - acc += `${acc.length ? '~and' : ''}(${curr.title},gb_eq,${(Array.isArray(value) ? value : [value]) - .map((v: any) => v.id) - .join(',')})` - } catch (e) { - console.error(e) + + const calculateNestedWhere = (nestedIn: GroupNestedIn[], existing = '') => { + return nestedIn.reduce((acc, curr) => { + if (curr.key === GROUP_BY_VARS.NULL) { + acc += `${acc.length ? '~and' : ''}(${curr.title},gb_null)` + } else if (curr.column_uidt === UITypes.Checkbox) { + acc += `${acc.length ? '~and' : ''}(${curr.title},${curr.key === GROUP_BY_VARS.TRUE ? 'checked' : 'notchecked'})` + } else if ( + [UITypes.Date, UITypes.DateTime, UITypes.CreatedTime, UITypes.LastModifiedTime].includes(curr.column_uidt as UITypes) + ) { + acc += `${acc.length ? '~and' : ''}(${curr.title},eq,exactDate,${curr.key})` + } else if ([UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy].includes(curr.column_uidt as UITypes)) { + try { + const value = JSON.parse(curr.key) + acc += `${acc.length ? '~and' : ''}(${curr.title},gb_eq,${(Array.isArray(value) ? value : [value]) + .map((v: any) => v.id) + .join(',')})` + } catch (e) { + console.error(e) + } + } else { + acc += `${acc.length ? '~and' : ''}(${curr.title},gb_eq,${curr.key})` } - } else { - acc += `${acc.length ? '~and' : ''}(${curr.title},gb_eq,${curr.key})` - } - return acc - }, existing) - } + return acc + }, existing) + } - async function loadGroups(params: any = {}, group?: Group) { - try { - group = group || rootGroup.value + async function loadGroups(params: any = {}, group?: Group) { + try { + group = group || rootGroup.value - if (!base?.value?.id || !view.value?.id || !view.value?.fk_model_id || !group) return + if (!base?.value?.id || !view.value?.id || !view.value?.fk_model_id || !group) return - if (groupBy.value.length === 0) { - group.children = [] - return - } + if (groupBy.value.length === 0) { + group.children = [] + return + } - if (group.nestedIn.length > groupBy.value.length) return + if (group.nestedIn.length > groupBy.value.length) return - if (group.nestedIn.length === 0) nextGroupColor.value = colors.value[0] - const groupby = groupBy.value[group.nestedIn.length] + if (group.nestedIn.length === 0) nextGroupColor.value = colors.value[0] + const groupby = groupBy.value[group.nestedIn.length] - const nestedWhere = calculateNestedWhere(group.nestedIn, where?.value) - if (!groupby || !groupby.column.title) return + const nestedWhere = calculateNestedWhere(group.nestedIn, where?.value) + if (!groupby || !groupby.column.title) return - if (isPublic.value && !sharedView.value?.uuid) { - return - } + if (isPublic && !sharedView.value?.uuid) { + return + } - const response = !isPublic.value - ? await api.dbViewRow.groupBy('noco', base.value.id, view.value.fk_model_id, view.value.id, { - offset: ((group.paginationData.page ?? 0) - 1) * (group.paginationData.pageSize ?? groupByGroupLimit.value), - limit: group.paginationData.pageSize ?? groupByGroupLimit.value, - ...params, - ...(isUIAllowed('sortSync') ? {} : { sortArrJson: JSON.stringify(sorts.value) }), - ...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }), - where: `${nestedWhere}`, - sort: `${groupby.sort === 'desc' ? '-' : ''}${groupby.column.title}`, - column_name: groupby.column.title, - } as any) - : await api.public.dataGroupBy( - sharedView.value!.uuid!, - { + const response = !isPublic + ? await api.dbViewRow.groupBy('noco', base.value.id, view.value.fk_model_id, view.value.id, { offset: ((group.paginationData.page ?? 0) - 1) * (group.paginationData.pageSize ?? groupByGroupLimit.value), limit: group.paginationData.pageSize ?? groupByGroupLimit.value, ...params, - where: nestedWhere, + ...(isUIAllowed('sortSync') ? {} : { sortArrJson: JSON.stringify(sorts.value) }), + ...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }), + where: `${nestedWhere}`, sort: `${groupby.sort === 'desc' ? '-' : ''}${groupby.column.title}`, column_name: groupby.column.title, - sortsArr: sorts.value, - filtersArr: nestedFilters.value, - }, - { - headers: { - 'xc-password': sharedViewPassword.value, + } as any) + : await api.public.dataGroupBy( + sharedView.value!.uuid!, + { + offset: ((group.paginationData.page ?? 0) - 1) * (group.paginationData.pageSize ?? groupByGroupLimit.value), + limit: group.paginationData.pageSize ?? groupByGroupLimit.value, + ...params, + where: nestedWhere, + sort: `${groupby.sort === 'desc' ? '-' : ''}${groupby.column.title}`, + column_name: groupby.column.title, + sortsArr: sorts.value, + filtersArr: nestedFilters.value, }, - }, - ) + { + headers: { + 'xc-password': sharedViewPassword.value, + }, + }, + ) - const tempList: Group[] = response.list.reduce((acc: Group[], curr: Record) => { - const keyExists = acc.find( - (a) => a.key === valueToTitle(curr[groupby.column.column_name!] ?? curr[groupby.column.title!], groupby.column), - ) - if (keyExists) { - keyExists.count += +curr.count - keyExists.paginationData = { page: 1, pageSize: groupByGroupLimit.value, totalRows: keyExists.count } + const tempList: Group[] = response.list.reduce((acc: Group[], curr: Record) => { + const keyExists = acc.find( + (a) => a.key === valueToTitle(curr[groupby.column.column_name!] ?? curr[groupby.column.title!], groupby.column), + ) + if (keyExists) { + keyExists.count += +curr.count + keyExists.paginationData = { page: 1, pageSize: groupByGroupLimit.value, totalRows: keyExists.count } + return acc + } + if (groupby.column.title && groupby.column.uidt) { + acc.push({ + key: valueToTitle(curr[groupby.column.title!], groupby.column), + column: groupby.column, + count: +curr.count, + color: findKeyColor(curr[groupby.column.title!], groupby.column), + nestedIn: [ + ...group!.nestedIn, + { + title: groupby.column.title, + column_name: groupby.column.title!, + key: valueToTitle(curr[groupby.column.title!], groupby.column), + column_uidt: groupby.column.uidt, + }, + ], + paginationData: { + page: 1, + pageSize: group!.nestedIn.length < groupBy.value.length - 1 ? groupByGroupLimit.value : groupByRecordLimit.value, + totalRows: +curr.count, + }, + nested: group!.nestedIn.length < groupBy.value.length - 1, + }) + } return acc + }, []) + + if (!group.children) group.children = [] + + for (const temp of tempList) { + const keyExists = group.children?.find((a) => a.key === temp.key) + if (keyExists) { + temp.paginationData = { + page: keyExists.paginationData.page || temp.paginationData.page, + pageSize: keyExists.paginationData.pageSize || temp.paginationData.pageSize, + totalRows: temp.count, + } + temp.color = keyExists.color + // update group + Object.assign(keyExists, temp) + continue + } + group.children.push(temp) } - if (groupby.column.title && groupby.column.uidt) { - acc.push({ - key: valueToTitle(curr[groupby.column.title!], groupby.column), - column: groupby.column, - count: +curr.count, - color: findKeyColor(curr[groupby.column.title!], groupby.column), - nestedIn: [ - ...group!.nestedIn, - { - title: groupby.column.title, - column_name: groupby.column.title!, - key: valueToTitle(curr[groupby.column.title!], groupby.column), - column_uidt: groupby.column.uidt, - }, - ], - paginationData: { - page: 1, - pageSize: group!.nestedIn.length < groupBy.value.length - 1 ? groupByGroupLimit.value : groupByRecordLimit.value, - totalRows: +curr.count, - }, - nested: group!.nestedIn.length < groupBy.value.length - 1, + + // clear rest of the children + group.children = group.children.filter((c) => tempList.find((t) => t.key === c.key)) + + if (group.count <= (group.paginationData.pageSize ?? groupByGroupLimit.value)) { + group.children.sort((a, b) => { + const orderA = tempList.findIndex((t) => t.key === a.key) + const orderB = tempList.findIndex((t) => t.key === b.key) + return orderA - orderB }) } - return acc - }, []) - if (!group.children) group.children = [] + group.paginationData = response.pageInfo - for (const temp of tempList) { - const keyExists = group.children?.find((a) => a.key === temp.key) - if (keyExists) { - temp.paginationData = { - page: keyExists.paginationData.page || temp.paginationData.page, - pageSize: keyExists.paginationData.pageSize || temp.paginationData.pageSize, - totalRows: temp.count, - } - temp.color = keyExists.color - // update group - Object.assign(keyExists, temp) - continue + // to cater the case like when querying with a non-zero offset + // the result page may point to the target page where the actual returned data don't display on + const expectedPage = Math.max(1, Math.ceil(group.paginationData.totalRows! / group.paginationData.pageSize!)) + if (expectedPage < group.paginationData.page!) { + await groupWrapperChangePage(expectedPage, group) } - group.children.push(temp) + } catch (e) { + message.error(await extractSdkResponseErrorMsg(e)) } + } - // clear rest of the children - group.children = group.children.filter((c) => tempList.find((t) => t.key === c.key)) + async function loadGroupData(group: Group, force = false, params: any = {}) { + try { + if (!base?.value?.id || !view.value?.id || !view.value?.fk_model_id) return - if (group.count <= (group.paginationData.pageSize ?? groupByGroupLimit.value)) { - group.children.sort((a, b) => { - const orderA = tempList.findIndex((t) => t.key === a.key) - const orderB = tempList.findIndex((t) => t.key === b.key) - return orderA - orderB - }) - } + if (group.children && !force) return - group.paginationData = response.pageInfo + if (!group.paginationData) { + group.paginationData = { page: 1, pageSize: groupByRecordLimit.value } + } - // to cater the case like when querying with a non-zero offset - // the result page may point to the target page where the actual returned data don't display on - const expectedPage = Math.max(1, Math.ceil(group.paginationData.totalRows! / group.paginationData.pageSize!)) - if (expectedPage < group.paginationData.page!) { - await groupWrapperChangePage(expectedPage, group) - } - } catch (e) { - message.error(await extractSdkResponseErrorMsg(e)) - } - } + const nestedWhere = calculateNestedWhere(group.nestedIn, where?.value) - async function loadGroupData(group: Group, force = false, params: any = {}) { - try { - if (!base?.value?.id || !view.value?.id || !view.value?.fk_model_id) return + const query = { + offset: ((group.paginationData.page ?? 0) - 1) * (group.paginationData.pageSize ?? groupByRecordLimit.value), + limit: group.paginationData.pageSize ?? groupByRecordLimit.value, + where: `${nestedWhere}`, + } - if (group.children && !force) return + const response = !isPublic + ? await api.dbViewRow.list('noco', base.value.id, view.value.fk_model_id, view.value.id, { + ...query, + ...params, + ...(isUIAllowed('sortSync') ? {} : { sortArrJson: JSON.stringify(sorts.value) }), + ...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }), + } as any) + : await fetchSharedViewData({ sortsArr: sorts.value, filtersArr: nestedFilters.value, ...query }) + + group.count = response.pageInfo.totalRows ?? 0 + group.rows = formatData(response.list) + group.paginationData = response.pageInfo + } catch (e) { + message.error(await extractSdkResponseErrorMsg(e)) + } + } + const loadGroupPage = async (group: Group, p: number) => { if (!group.paginationData) { group.paginationData = { page: 1, pageSize: groupByRecordLimit.value } } + group.paginationData.page = p + await loadGroupData(group, true) + } - const nestedWhere = calculateNestedWhere(group.nestedIn, where?.value) + const refreshNested = (group?: Group, nestLevel = 0) => { + group = group || rootGroup.value + if (!group) return - const query = { - offset: ((group.paginationData.page ?? 0) - 1) * (group.paginationData.pageSize ?? groupByRecordLimit.value), - limit: group.paginationData.pageSize ?? groupByRecordLimit.value, - where: `${nestedWhere}`, + if (nestLevel < groupBy.value.length) { + group.nested = true + } else { + group.nested = false } - const response = !isPublic.value - ? await api.dbViewRow.list('noco', base.value.id, view.value.fk_model_id, view.value.id, { - ...query, - ...params, - ...(isUIAllowed('sortSync') ? {} : { sortArrJson: JSON.stringify(sorts.value) }), - ...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }), - } as any) - : await fetchSharedViewData({ sortsArr: sorts.value, filtersArr: nestedFilters.value, ...query }) - - group.count = response.pageInfo.totalRows ?? 0 - group.rows = formatData(response.list) - group.paginationData = response.pageInfo - } catch (e) { - message.error(await extractSdkResponseErrorMsg(e)) - } - } + if (group.nested) { + if (group?.rows) { + group.rows = [] + } + } else { + if (group?.children) { + group.children = [] + } + } - const loadGroupPage = async (group: Group, p: number) => { - if (!group.paginationData) { - group.paginationData = { page: 1, pageSize: groupByRecordLimit.value } - } - group.paginationData.page = p - await loadGroupData(group, true) - } - - const refreshNested = (group?: Group, nestLevel = 0) => { - group = group || rootGroup.value - if (!group) return - - if (nestLevel < groupBy.value.length) { - group.nested = true - } else { - group.nested = false - } + if (nestLevel > groupBy.value.length) return - if (group.nested) { - if (group?.rows) { - group.rows = [] - } - } else { - if (group?.children) { - group.children = [] + for (const child of group.children || []) { + refreshNested(child, nestLevel + 1) } } - if (nestLevel > groupBy.value.length) return + watch( + () => groupBy.value.length, + async () => { + if (groupBy.value.length > 0) { + rootGroup.value.paginationData = { page: 1, pageSize: groupByGroupLimit.value } + rootGroup.value.column = {} as any + refreshNested() + nextTick(() => reloadViewDataHook?.trigger()) + } + }, + ) - for (const child of group.children || []) { - refreshNested(child, nestLevel + 1) - } - } - - watch( - () => groupBy.value.length, - async () => { - if (groupBy.value.length > 0) { - rootGroup.value.paginationData = { page: 1, pageSize: groupByGroupLimit.value } - rootGroup.value.column = {} as any - await loadGroups() - refreshNested() - nextTick(() => reloadViewDataHook?.trigger()) - } - }, - { immediate: true }, - ) - - const findGroupByNestedIn = (nestedIn: GroupNestedIn[], group?: Group, nestLevel = 0): Group => { - group = group || rootGroup.value - if (nestLevel >= nestedIn.length) return group - const child = group.children?.find((g) => g.key === nestedIn[nestLevel].key) - if (child) { - if (child.nested) { - return findGroupByNestedIn(nestedIn, child, nestLevel + 1) + const findGroupByNestedIn = (nestedIn: GroupNestedIn[], group?: Group, nestLevel = 0): Group => { + group = group || rootGroup.value + if (nestLevel >= nestedIn.length) return group + const child = group.children?.find((g) => g.key === nestedIn[nestLevel].key) + if (child) { + if (child.nested) { + return findGroupByNestedIn(nestedIn, child, nestLevel + 1) + } + return child } - return child + return group + } + + const parentGroup = (group: Group) => { + const parent = findGroupByNestedIn(group.nestedIn.slice(0, -1)) + return parent } - return group - } - - const parentGroup = (group: Group) => { - const parent = findGroupByNestedIn(group.nestedIn.slice(0, -1)) - return parent - } - - const modifyCount = (group: Group, countEffect: number) => { - if (!group) return - group.count += countEffect - // remove group if count is 0 - if (group.count === 0) { - const parent = parentGroup(group) - if (parent) { - parent.children = parent.children?.filter((c) => c.key !== group.key) + + const modifyCount = (group: Group, countEffect: number) => { + if (!group) return + group.count += countEffect + // remove group if count is 0 + if (group.count === 0) { + const parent = parentGroup(group) + if (parent) { + parent.children = parent.children?.filter((c) => c.key !== group.key) + } } + if (group.root) return + modifyCount(parentGroup(group), countEffect) } - if (group.root) return - modifyCount(parentGroup(group), countEffect) - } - - const findGroupForRow = (row: Row, group?: Group, nestLevel = 0): { found: boolean; group: Group } => { - group = group || rootGroup.value - if (group.nested) { - const child = group.children?.find((g) => { - if (!groupBy.value[nestLevel].column.title) return undefined - return g.key === valueToTitle(row.row[groupBy.value[nestLevel].column.title!], groupBy.value[nestLevel].column) - }) - if (child) { - return findGroupForRow(row, child, nestLevel + 1) + + const findGroupForRow = (row: Row, group?: Group, nestLevel = 0): { found: boolean; group: Group } => { + group = group || rootGroup.value + if (group.nested) { + const child = group.children?.find((g) => { + if (!groupBy.value[nestLevel].column.title) return undefined + return g.key === valueToTitle(row.row[groupBy.value[nestLevel].column.title!], groupBy.value[nestLevel].column) + }) + if (child) { + return findGroupForRow(row, child, nestLevel + 1) + } + return { found: false, group } } - return { found: false, group } + return { found: true, group } } - return { found: true, group } - } - - const redistributeRows = (group?: Group) => { - group = group || rootGroup.value - if (!group) return - if (!group.nested && group.rows) { - group.rows.forEach((row) => { - const properGroup = findGroupForRow(row) - if (properGroup.found) { - if (properGroup.group !== group) { - if (properGroup.group) { - properGroup.group.rows?.push(row) - modifyCount(properGroup.group, 1) + + const redistributeRows = (group?: Group) => { + group = group || rootGroup.value + if (!group) return + if (!group.nested && group.rows) { + group.rows.forEach((row) => { + const properGroup = findGroupForRow(row) + if (properGroup.found) { + if (properGroup.group !== group) { + if (properGroup.group) { + properGroup.group.rows?.push(row) + modifyCount(properGroup.group, 1) + } + if (group) { + group.rows?.splice(group!.rows.indexOf(row), 1) + modifyCount(group, -1) + } } + } else { if (group) { group.rows?.splice(group!.rows.indexOf(row), 1) modifyCount(group, -1) } + if (properGroup.group?.children) loadGroups({}, properGroup.group) } - } else { - if (group) { - group.rows?.splice(group!.rows.indexOf(row), 1) - modifyCount(group, -1) - } - if (properGroup.group?.children) loadGroups({}, properGroup.group) - } - }) - } else { - group.children?.forEach((g) => redistributeRows(g)) + }) + } else { + group.children?.forEach((g) => redistributeRows(g)) + } } - } - - const loadAllowedLookups = async () => { - const filteredLookupCols = [] - try { - for (const col of meta?.value?.columns || []) { - if (col.uidt !== UITypes.Lookup) continue - - let nextCol: ColumnType = col - // check the lookup column is supported type or not - while (nextCol && nextCol.uidt === UITypes.Lookup) { - const lookupRelation = (await getMeta(nextCol.fk_model_id as string))?.columns?.find( - (c) => c.id === (nextCol?.colOptions as LookupType).fk_relation_column_id, - ) - const relatedTableMeta = await getMeta( - (lookupRelation?.colOptions as LinkToAnotherRecordType).fk_related_model_id as string, - ) - - nextCol = relatedTableMeta?.columns?.find( - (c) => c.id === ((nextCol?.colOptions as LookupType).fk_lookup_column_id as string), - ) as ColumnType - - // if next column is same as root lookup column then break the loop - // since it's going to be a circular loop, and ignore the column - if (nextCol?.id === col.id) { - break + const loadAllowedLookups = async () => { + const filteredLookupCols = [] + try { + for (const col of meta?.value?.columns || []) { + if (col.uidt !== UITypes.Lookup) continue + + let nextCol: ColumnType = col + // check the lookup column is supported type or not + while (nextCol && nextCol.uidt === UITypes.Lookup) { + const lookupRelation = (await getMeta(nextCol.fk_model_id as string))?.columns?.find( + (c) => c.id === (nextCol?.colOptions as LookupType).fk_relation_column_id, + ) + + const relatedTableMeta = await getMeta( + (lookupRelation?.colOptions as LinkToAnotherRecordType).fk_related_model_id as string, + ) + + nextCol = relatedTableMeta?.columns?.find( + (c) => c.id === ((nextCol?.colOptions as LookupType).fk_lookup_column_id as string), + ) as ColumnType + + // if next column is same as root lookup column then break the loop + // since it's going to be a circular loop, and ignore the column + if (nextCol?.id === col.id) { + break + } } + + if (nextCol?.uidt !== UITypes.Attachment && col.id) filteredLookupCols.push(col.id) } - if (nextCol?.uidt !== UITypes.Attachment && col.id) filteredLookupCols.push(col.id) + supportedLookups.value = filteredLookupCols + } catch (e) { + console.error(e) } + } - supportedLookups.value = filteredLookupCols - } catch (e) { - console.error(e) + 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) { + await loadAllowedLookups() + } + }) + + return { + rootGroup, + groupBy, + isGroupBy, + fieldsToGroupBy, + groupByLimit, + loadGroups, + loadGroupData, + loadGroupPage, + groupWrapperChangePage, + redistributeRows, } - } - - onMounted(async () => { - await loadAllowedLookups() - }) - - watch(meta, async () => { - await loadAllowedLookups() - }) - - return { - rootGroup, - groupBy, - isGroupBy, - fieldsToGroupBy, - groupByLimit, - loadGroups, - loadGroupData, - loadGroupPage, - groupWrapperChangePage, - redistributeRows, - } + }, + 'useViewGroupBy', +) + +export { useProvideViewGroupBy } + +export function useViewGroupByOrThrow() { + const viewColumns = useViewGroupBy() + if (viewColumns == null) throw new Error('Please call `useProvideViewGroupBy` on the appropriate parent component') + return viewColumns } diff --git a/tests/playwright/tests/db/general/toolbarOperations.spec.ts b/tests/playwright/tests/db/general/toolbarOperations.spec.ts index 6afa2b37ea..a7cb00afaf 100644 --- a/tests/playwright/tests/db/general/toolbarOperations.spec.ts +++ b/tests/playwright/tests/db/general/toolbarOperations.spec.ts @@ -230,7 +230,7 @@ test.describe('Toolbar operations (GRID)', () => { }); await toolbar.clickFilter(); - await dashboard.grid.groupPage.openGroup({ indexMap: [1, 0] }); + await dashboard.grid.groupPage.openGroup({ indexMap: [2, 0] }); await dashboard.grid.groupPage.verifyGroupHeader({ indexMap: [1, 0], @@ -377,16 +377,14 @@ test.describe('Toolbar operations (GRID)', () => { }); await toolbar.clickFilter(); - await dashboard.grid.groupPage.openGroup({ indexMap: [5, 0, 0] }); - await dashboard.grid.groupPage.verifyGroupHeader({ - indexMap: [5, 0, 0], + indexMap: [4, 0, 0], count: 2, title: 'ReleaseYear', }); await dashboard.grid.groupPage.verifyGroup({ - indexMap: [5, 0, 0], + indexMap: [4, 0, 0], value: '2006', });