Browse Source

Merge pull request #8309 from nocodb/nc-fix/field-header-context-menu

Nc fix/field header context menu
pull/8319/head
Ramesh Mane 7 months ago committed by GitHub
parent
commit
4c774715cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 96
      packages/nc-gui/components/smartsheet/header/Menu.vue
  2. 62
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue
  3. 18
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilterMenu.vue
  4. 75
      packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue
  5. 3
      packages/nc-gui/composables/useSmartsheetStore.ts
  6. 71
      packages/nc-gui/composables/useViewFilters.ts
  7. 83
      packages/nc-gui/composables/useViewGroupBy.ts
  8. 2
      packages/nc-gui/lib/constants.ts
  9. 3
      packages/nc-gui/lib/enums.ts

96
packages/nc-gui/components/smartsheet/header/Menu.vue

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ColumnReqType } from 'nocodb-sdk' import type { ColumnReqType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isLinksOrLTAR } from 'nocodb-sdk' import { PlanLimitTypes, RelationTypes, UITypes, isLinksOrLTAR } from 'nocodb-sdk'
import { computed } from 'vue' import { computed } from 'vue'
import { import {
ActiveViewInj, ActiveViewInj,
@ -28,7 +28,7 @@ const virtual = toRef(props, 'virtual')
const isOpen = useVModel(props, 'isOpen', emit) const isOpen = useVModel(props, 'isOpen', emit)
const { eventBus } = useSmartsheetStoreOrThrow() const { eventBus, allFilters } = useSmartsheetStoreOrThrow()
const column = inject(ColumnInj) const column = inject(ColumnInj)
@ -38,10 +38,12 @@ const meta = inject(MetaInj, ref())
const view = inject(ActiveViewInj, ref()) const view = inject(ActiveViewInj, ref())
const { insertSort } = useViewSorts(view, () => reloadDataHook?.trigger())
const isLocked = inject(IsLockedInj) const isLocked = inject(IsLockedInj)
const isPublic = inject(IsPublicInj, ref(false))
const { insertSort } = useViewSorts(view, () => reloadDataHook?.trigger())
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
const { t } = useI18n() const { t } = useI18n()
@ -52,6 +54,10 @@ const { addUndo, defineModelScope, defineViewScope } = useUndoRedo()
const showDeleteColumnModal = ref(false) const showDeleteColumnModal = ref(false)
const { gridViewCols } = useViewColumnsOrThrow()
const { fieldsToGroupBy, groupByLimit } = useViewGroupBy(view)
const setAsDisplayValue = async () => { const setAsDisplayValue = async () => {
try { try {
const currentDisplayValue = meta?.value?.columns?.find((f) => f.pv) const currentDisplayValue = meta?.value?.columns?.find((f) => f.pv)
@ -295,6 +301,33 @@ const isDeleteAllowed = computed(() => {
const isDuplicateAllowed = computed(() => { const isDuplicateAllowed = computed(() => {
return column?.value && !column.value.system return column?.value && !column.value.system
}) })
const isFilterSupported = computed(
() =>
!!(meta.value?.columns || []).find((f) => f.id === column?.value?.id && ![UITypes.QrCode, UITypes.Barcode].includes(f.uidt)),
)
const { getPlanLimit } = useWorkspace()
const isFilterLimitExceeded = computed(
() =>
allFilters.value.filter((f) => !(f.is_group || f.status === 'delete')).length >= getPlanLimit(PlanLimitTypes.FILTER_LIMIT),
)
const isGroupedByThisField = computed(() => !!gridViewCols.value[column?.value?.id]?.group_by)
const isGroupBySupported = computed(() => !!(fieldsToGroupBy.value || []).find((f) => f.id === column?.value?.id))
const isGroupByLimitExceeded = computed(() => {
const groupBy = Object.values(gridViewCols.value).filter((c) => c.group_by)
return !(fieldsToGroupBy.value.length && fieldsToGroupBy.value.length > groupBy.length && groupBy.length < groupByLimit)
})
const filterOrGroupByThisField = (event: SmartsheetStoreEvents) => {
if (column?.value) {
eventBus.emit(event, column.value)
}
isOpen.value = false
}
</script> </script>
<template> <template>
@ -362,7 +395,55 @@ const isDuplicateAllowed = computed(() => {
</NcMenuItem> </NcMenuItem>
</template> </template>
<a-divider v-if="!column?.pk" class="!my-0" /> <a-divider class="!my-0" />
<NcTooltip :disabled="isFilterSupported && !isFilterLimitExceeded">
<template #title>
{{
!isFilterSupported
? "This field type doesn't support filtering"
: isFilterLimitExceeded
? 'Filter by limit exceeded'
: ''
}}
</template>
<NcMenuItem
:disabled="!isFilterSupported || isFilterLimitExceeded"
@click="filterOrGroupByThisField(SmartsheetStoreEvents.FILTER_ADD)"
>
<div v-e="['a:field:add:filter']" class="nc-column-filter nc-header-menu-item">
<component :is="iconMap.filter" class="text-gray-700" />
<!-- Filter by this field -->
Filter by this field
</div>
</NcMenuItem>
</NcTooltip>
<NcTooltip :disabled="(isGroupBySupported && !isGroupByLimitExceeded) || isGroupedByThisField || !(isEeUI && !isPublic)">
<template #title>{{
!isGroupBySupported
? "This field type doesn't support grouping"
: isGroupByLimitExceeded
? 'Group by limit exceeded'
: ''
}}</template>
<NcMenuItem
:disabled="isEeUI && !isPublic && (!isGroupBySupported || isGroupByLimitExceeded) && !isGroupedByThisField"
@click="
filterOrGroupByThisField(
isGroupedByThisField ? SmartsheetStoreEvents.GROUP_BY_REMOVE : SmartsheetStoreEvents.GROUP_BY_ADD,
)
"
>
<div v-e="['a:field:add:groupby']" class="nc-column-groupby nc-header-menu-item">
<component :is="iconMap.group" class="text-gray-700" />
<!-- Group by this field -->
{{ isGroupedByThisField ? "Don't group by this field" : 'Group by this field' }}
</div>
</NcMenuItem>
</NcTooltip>
<a-divider class="!my-0" />
<NcMenuItem v-if="!column?.pk" :disabled="!isDuplicateAllowed" @click="openDuplicateDlg"> <NcMenuItem v-if="!column?.pk" :disabled="!isDuplicateAllowed" @click="openDuplicateDlg">
<div v-e="['a:field:duplicate']" class="nc-column-duplicate nc-header-menu-item"> <div v-e="['a:field:duplicate']" class="nc-column-duplicate nc-header-menu-item">
@ -418,7 +499,10 @@ const isDuplicateAllowed = computed(() => {
} }
} }
:deep(.ant-dropdown-menu-item) { :deep(.ant-dropdown-menu-item:not(.ant-dropdown-menu-item-disabled)) {
@apply !hover:text-black text-gray-700; @apply !hover:text-black text-gray-700;
} }
:deep(.ant-dropdown-menu-item.ant-dropdown-menu-item-disabled .nc-icon) {
@apply text-current;
}
</style> </style>

62
packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue

@ -27,8 +27,8 @@ interface Props {
showLoading?: boolean showLoading?: boolean
modelValue?: undefined | Filter[] modelValue?: undefined | Filter[]
webHook?: boolean webHook?: boolean
draftFilter?: Partial<FilterType>
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
nestedLevel: 0, nestedLevel: 0,
autoSave: true, autoSave: true,
@ -38,7 +38,11 @@ const props = withDefaults(defineProps<Props>(), {
webHook: false, webHook: false,
}) })
const emit = defineEmits(['update:filtersLength']) const emit = defineEmits(['update:filtersLength', 'update:draftFilter'])
const excludedFilterColUidt = [UITypes.QrCode, UITypes.Barcode]
const draftFilter = useVModel(props, 'draftFilter', emit)
const { nestedLevel, parentId, autoSave, hookId, modelValue, showLoading, webHook } = toRefs(props) const { nestedLevel, parentId, autoSave, hookId, modelValue, showLoading, webHook } = toRefs(props)
@ -94,8 +98,12 @@ const localNestedFilters = ref()
const wrapperDomRef = ref<HTMLElement>() const wrapperDomRef = ref<HTMLElement>()
const addFiltersRowDomRef = ref<HTMLElement>() const addFiltersRowDomRef = ref<HTMLElement>()
const isMounted = ref(false)
const columns = computed(() => meta.value?.columns) const columns = computed(() => meta.value?.columns)
const fieldsToFilter = computed(() => (columns.value || []).filter((c) => !excludedFilterColUidt.includes(c.uidt as UITypes)))
const getColumn = (filter: Filter) => { const getColumn = (filter: Filter) => {
// extract looked up column if available // extract looked up column if available
return btLookupTypesMap.value[filter.fk_column_id] || columns.value?.find((col: ColumnType) => col.id === filter.fk_column_id) return btLookupTypesMap.value[filter.fk_column_id] || columns.value?.find((col: ColumnType) => col.id === filter.fk_column_id)
@ -279,8 +287,12 @@ const scrollDownIfNeeded = () => {
} }
} }
const addFilter = async () => { const addFilter = async (filter?: Partial<FilterType>) => {
await _addFilter() await _addFilter(false, filter)
if (filter) {
selectFilterField(filters.value[filters.value.length - 1], filters.value.length - 1)
}
if (!nested.value) { if (!nested.value) {
// if nested, scroll to bottom // if nested, scroll to bottom
@ -316,12 +328,9 @@ const showFilterInput = (filter: Filter) => {
} }
} }
onMounted(() => {
loadFilters(hookId?.value, webHook.value)
})
onMounted(async () => { onMounted(async () => {
await loadBtLookupTypes() await Promise.all([loadFilters(hookId?.value, webHook.value), loadBtLookupTypes()])
isMounted.value = true
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
@ -331,6 +340,37 @@ onBeforeUnmount(() => {
function isDateType(uidt: UITypes) { function isDateType(uidt: UITypes) {
return [UITypes.Date, UITypes.DateTime, UITypes.CreatedTime, UITypes.LastModifiedTime].includes(uidt) return [UITypes.Date, UITypes.DateTime, UITypes.CreatedTime, UITypes.LastModifiedTime].includes(uidt)
} }
watch(
[draftFilter, isMounted],
async () => {
if (!isMounted.value || !draftFilter.value?.fk_column_id) return
await addFilter(draftFilter.value)
await nextTick()
scrollToBottom()
const filterWrapper = document.querySelectorAll(`.nc-filter-wrapper-${draftFilter.value.fk_column_id}`)
draftFilter.value = {}
if (!filterWrapper.length) return
const filterInputElement =
filterWrapper[filterWrapper.length - 1]?.querySelector<HTMLInputElement>('.nc-filter-value-select input')
if (filterInputElement) {
setTimeout(() => {
filterInputElement?.focus?.()
filterInputElement?.click?.()
}, 100)
}
},
{
deep: true,
immediate: true,
},
)
</script> </script>
<template> <template>
@ -405,7 +445,7 @@ function isDateType(uidt: UITypes) {
</div> </div>
</div> </div>
</template> </template>
<div v-else class="flex flex-row gap-x-2 w-full"> <div v-else class="flex flex-row gap-x-2 w-full" :class="`nc-filter-wrapper-${filter.fk_column_id}`">
<span v-if="!i" class="flex items-center ml-2 mr-7.35">{{ $t('labels.where') }}</span> <span v-if="!i" class="flex items-center ml-2 mr-7.35">{{ $t('labels.where') }}</span>
<NcSelect <NcSelect
@ -436,7 +476,7 @@ function isDateType(uidt: UITypes) {
:key="`${i}_6`" :key="`${i}_6`"
v-model="filter.fk_column_id" v-model="filter.fk_column_id"
class="nc-filter-field-select min-w-32 max-w-32 max-h-8" class="nc-filter-field-select min-w-32 max-w-32 max-h-8"
:columns="columns" :columns="fieldsToFilter"
:disabled="filter.readOnly" :disabled="filter.readOnly"
@click.stop @click.stop
@change="selectFilterField(filter, i)" @change="selectFilterField(filter, i)"

18
packages/nc-gui/components/smartsheet/toolbar/ColumnFilterMenu.vue

@ -1,8 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import { import {
ActiveViewInj, ActiveViewInj,
AllFiltersInj, AllFiltersInj,
IsLockedInj, IsLockedInj,
SmartsheetStoreEvents,
computed, computed,
iconMap, iconMap,
inject, inject,
@ -22,7 +24,7 @@ const { isMobileMode } = useGlobal()
const filterComp = ref<typeof ColumnFilter>() const filterComp = ref<typeof ColumnFilter>()
const { nestedFilters } = useSmartsheetStoreOrThrow() const { nestedFilters, eventBus } = useSmartsheetStoreOrThrow()
// todo: avoid duplicate api call by keeping a filter store // todo: avoid duplicate api call by keeping a filter store
const { nonDeletedFilters, loadFilters } = useViewFilters( const { nonDeletedFilters, loadFilters } = useViewFilters(
@ -40,7 +42,7 @@ watch(
() => activeView?.value?.id, () => activeView?.value?.id,
async (viewId) => { async (viewId) => {
if (viewId) { if (viewId) {
await loadFilters() await loadFilters(undefined, false, true)
filtersLength.value = nonDeletedFilters.value.length || 0 filtersLength.value = nonDeletedFilters.value.length || 0
} }
}, },
@ -54,6 +56,17 @@ const allFilters = ref({})
provide(AllFiltersInj, allFilters) provide(AllFiltersInj, allFilters)
useMenuCloseOnEsc(open) useMenuCloseOnEsc(open)
const draftFilter = ref({})
eventBus.on(async (event, column: ColumnType) => {
if (!column) return
if (event === SmartsheetStoreEvents.FILTER_ADD) {
draftFilter.value = { fk_column_id: column.id }
open.value = true
}
})
</script> </script>
<template> <template>
@ -78,6 +91,7 @@ useMenuCloseOnEsc(open)
<template #overlay> <template #overlay>
<SmartsheetToolbarColumnFilter <SmartsheetToolbarColumnFilter
ref="filterComp" ref="filterComp"
v-model:draft-filter="draftFilter"
class="nc-table-toolbar-menu" class="nc-table-toolbar-menu"
:auto-save="true" :auto-save="true"
data-testid="nc-filter-menu" data-testid="nc-filter-menu"

75
packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ColumnType, LinkToAnotherRecordType, LookupType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk' import { RelationTypes, UITypes, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
import { import {
ActiveViewInj, ActiveViewInj,
@ -8,30 +8,26 @@ import {
computed, computed,
getSortDirectionOptions, getSortDirectionOptions,
inject, inject,
onMounted,
ref, ref,
useMenuCloseOnEsc, useMenuCloseOnEsc,
useMetas,
useNuxtApp, useNuxtApp,
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
useViewColumnsOrThrow, useViewColumnsOrThrow,
watch, watch,
} from '#imports' } from '#imports'
const excludedGroupingUidt = [UITypes.Attachment]
const meta = inject(MetaInj, ref()) const meta = inject(MetaInj, ref())
const view = inject(ActiveViewInj, ref()) const view = inject(ActiveViewInj, ref())
const isLocked = inject(IsLockedInj, ref(false)) const isLocked = inject(IsLockedInj, ref(false))
const { gridViewCols, updateGridViewColumn, metaColumnById, showSystemFields } = useViewColumnsOrThrow() const { gridViewCols, updateGridViewColumn, metaColumnById, showSystemFields } = useViewColumnsOrThrow()
const { fieldsToGroupBy, groupByLimit } = useViewGroupBy(view)
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
const _groupBy = ref<{ fk_column_id?: string; sort: string; order: number }[]>([]) const _groupBy = ref<{ fk_column_id?: string; sort: string; order: number }[]>([])
const { getMeta } = useMetas()
const groupBy = computed<{ fk_column_id?: string; sort: string; order: number }[]>(() => { const groupBy = computed<{ fk_column_id?: string; sort: string; order: number }[]>(() => {
const tempGroupBy: { fk_column_id?: string; sort: string; order: number }[] = [] const tempGroupBy: { fk_column_id?: string; sort: string; order: number }[] = []
Object.values(gridViewCols.value).forEach((col) => { Object.values(gridViewCols.value).forEach((col) => {
@ -53,24 +49,8 @@ const { eventBus } = useSmartsheetStoreOrThrow()
const { isMobileMode } = useGlobal() const { isMobileMode } = useGlobal()
const supportedLookups = ref<string[]>([])
const showCreateGroupBy = ref(false) const showCreateGroupBy = ref(false)
const fieldsToGroupBy = computed(() => {
const fields = meta.value?.columns || []
return fields.filter((field) => {
if (excludedGroupingUidt.includes(field.uidt as UITypes)) return false
if (field.uidt === UITypes.Lookup) {
return field.id && supportedLookups.value.includes(field.id)
}
return true
})
})
const columns = computed(() => meta.value?.columns || []) const columns = computed(() => meta.value?.columns || [])
const columnByID = computed(() => const columnByID = computed(() =>
@ -174,49 +154,18 @@ watch(open, () => {
} }
}) })
const loadAllowedLookups = async () => { eventBus.on(async (event, column) => {
const filteredLookupCols = [] if (!column?.id) return
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( if (event === SmartsheetStoreEvents.GROUP_BY_ADD) {
(c) => c.id === ((nextCol?.colOptions as LookupType).fk_lookup_column_id as string), addFieldToGroupBy(column)
) as ColumnType } else if (event === SmartsheetStoreEvents.GROUP_BY_REMOVE) {
if (groupedByColumnIds.value.length === 0) return
// if next column is same as root lookup column then break the loop _groupBy.value = _groupBy.value.filter((g) => g.fk_column_id !== column.id)
// 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) await saveGroupBy()
}
supportedLookups.value = filteredLookupCols
} catch (e) {
console.error(e)
} }
}
onMounted(async () => {
await loadAllowedLookups()
})
watch(meta, async () => {
await loadAllowedLookups()
}) })
</script> </script>
@ -305,7 +254,7 @@ watch(meta, async () => {
</template> </template>
</div> </div>
<NcDropdown <NcDropdown
v-if="availableColumns.length && fieldsToGroupBy.length > _groupBy.length && _groupBy.length < 3" v-if="availableColumns.length && fieldsToGroupBy.length > _groupBy.length && _groupBy.length < groupByLimit"
v-model:visible="showCreateGroupBy" v-model:visible="showCreateGroupBy"
:trigger="['click']" :trigger="['click']"
overlay-class-name="nc-toolbar-dropdown" overlay-class-name="nc-toolbar-dropdown"

3
packages/nc-gui/composables/useSmartsheetStore.ts

@ -58,6 +58,8 @@ const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState(
const sorts = ref<SortType[]>(unref(initialSorts) ?? []) const sorts = ref<SortType[]>(unref(initialSorts) ?? [])
const nestedFilters = ref<FilterType[]>(unref(initialFilters) ?? []) const nestedFilters = ref<FilterType[]>(unref(initialFilters) ?? [])
const allFilters = ref<Filter[]>([])
watch( watch(
sorts, sorts,
() => { () => {
@ -97,6 +99,7 @@ const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState(
isSqlView, isSqlView,
eventBus, eventBus,
sqlUi, sqlUi,
allFilters,
} }
}, },
'smartsheet-store', 'smartsheet-store',

71
packages/nc-gui/composables/useViewFilters.ts

@ -43,7 +43,7 @@ export function useViewFilters(
const reloadHook = inject(ReloadViewDataHookInj) const reloadHook = inject(ReloadViewDataHookInj)
const { nestedFilters } = useSmartsheetStoreOrThrow() const { nestedFilters, allFilters } = useSmartsheetStoreOrThrow()
const { baseMeta } = storeToRefs(useBase()) const { baseMeta } = storeToRefs(useBase())
@ -208,7 +208,35 @@ export function useViewFilters(
} }
} }
const loadFilters = async (hookId?: string, isWebhook = false) => { const loadAllChildFilters = async (filters: Filter[]) => {
// Array to store promises of child filter loading
const promises = []
// Array to store all child filters
const allChildFilters: Filter[] = []
// Iterate over all filters
for (const filter of filters) {
// Check if the filter is a group
if (filter.id && filter.is_group) {
// Load children filters from the backend
const childFilterPromise = $api.dbTableFilter.childrenRead(filter.id).then((response) => {
const childFilters = response.list as Filter[]
allChildFilters.push(...childFilters)
return loadAllChildFilters(childFilters)
})
promises.push(childFilterPromise)
}
}
// Wait for all promises to resolve
await Promise.all(promises)
// Push all child filters into the allFilters array
allFilters.value.push(...allChildFilters)
}
const loadFilters = async (hookId?: string, isWebhook = false, loadAllFilters = false) => {
if (!view.value?.id) return if (!view.value?.id) return
if (nestedMode.value) { if (nestedMode.value) {
@ -228,6 +256,10 @@ export function useViewFilters(
filters.value = (await $api.dbTableFilter.childrenRead(parentId)).list as Filter[] filters.value = (await $api.dbTableFilter.childrenRead(parentId)).list as Filter[]
} else { } else {
filters.value = (await $api.dbTableFilter.read(view.value!.id!)).list as Filter[] filters.value = (await $api.dbTableFilter.read(view.value!.id!)).list as Filter[]
if (loadAllFilters) {
allFilters.value = [...filters.value]
await loadAllChildFilters(allFilters.value)
}
} }
} }
} catch (e: any) { } catch (e: any) {
@ -241,6 +273,11 @@ export function useViewFilters(
for (const [i, filter] of Object.entries(filters.value)) { for (const [i, filter] of Object.entries(filters.value)) {
if (filter.status === 'delete') { if (filter.status === 'delete') {
await $api.dbTableFilter.delete(filter.id as string) await $api.dbTableFilter.delete(filter.id as string)
if (filter.is_group) {
deleteFilterGroupFromAllFilters(filter)
} else {
allFilters.value = allFilters.value.filter((f) => f.id !== filter.id)
}
} else if (filter.status === 'update') { } else if (filter.status === 'update') {
await $api.dbTableFilter.update(filter.id as string, { await $api.dbTableFilter.update(filter.id as string, {
...filter, ...filter,
@ -258,6 +295,8 @@ export function useViewFilters(
fk_parent_id: parentId, fk_parent_id: parentId,
}) })
} }
allFilters.value.push(filters.value[+i])
} }
} }
@ -323,6 +362,8 @@ export function useViewFilters(
...filter, ...filter,
fk_parent_id: parentId, fk_parent_id: parentId,
}) })
allFilters.value.push(filters.value[+i])
} }
} catch (e: any) { } catch (e: any) {
console.log(e) console.log(e)
@ -334,6 +375,21 @@ export function useViewFilters(
if (!isWebhook) reloadData?.() if (!isWebhook) reloadData?.()
} }
function deleteFilterGroupFromAllFilters(filter: Filter) {
// Find all child filters of the specified parentId
const childFilters = allFilters.value.filter((f) => f.fk_parent_id === filter.id)
// Recursively delete child filter of child filter
childFilters.forEach((childFilter) => {
if (childFilter.is_group) {
deleteFilterGroupFromAllFilters(childFilter)
}
})
// Remove the parent object and its children from the array
allFilters.value = allFilters.value.filter((f) => f.id !== filter.id && f.fk_parent_id !== filter.id)
}
const deleteFilter = async (filter: Filter, i: number, undo = false) => { const deleteFilter = async (filter: Filter, i: number, undo = false) => {
if (!undo && !filter.is_group) { if (!undo && !filter.is_group) {
addUndo({ addUndo({
@ -369,6 +425,7 @@ export function useViewFilters(
} else { } else {
try { try {
await $api.dbTableFilter.delete(filter.id) await $api.dbTableFilter.delete(filter.id)
if (!isWebhook) reloadData?.() if (!isWebhook) reloadData?.()
filters.value.splice(i, 1) filters.value.splice(i, 1)
} catch (e: any) { } catch (e: any) {
@ -382,12 +439,18 @@ export function useViewFilters(
} }
$e('a:filter:delete', { length: nonDeletedFilters.value.length }) $e('a:filter:delete', { length: nonDeletedFilters.value.length })
} }
if (filter.is_group) {
deleteFilterGroupFromAllFilters(filter)
} else {
allFilters.value = allFilters.value.filter((f) => f.id !== filter.id)
}
} }
const saveOrUpdateDebounced = useDebounceFn(saveOrUpdate, 500) const saveOrUpdateDebounced = useDebounceFn(saveOrUpdate, 500)
const addFilter = async (undo = false) => { const addFilter = async (undo = false, draftFilter: Partial<FilterType> = {}) => {
filters.value.push(placeholderFilter()) filters.value.push(draftFilter?.fk_column_id ? { ...placeholderFilter(), ...draftFilter } : placeholderFilter())
if (!undo) { if (!undo) {
addUndo({ addUndo({
undo: { undo: {

83
packages/nc-gui/composables/useViewGroupBy.ts

@ -1,11 +1,16 @@
import { type ColumnType, type SelectOptionsType, UITypes, type ViewType } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType, LookupType, SelectOptionsType, ViewType } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { extractSdkResponseErrorMsg } from '../utils' import { extractSdkResponseErrorMsg } from '../utils'
import { GROUP_BY_VARS, ref, storeToRefs, useApi, useBase, useViewColumnsOrThrow } from '#imports' import { GROUP_BY_VARS, ref, storeToRefs, useApi, useBase, useMetas, useViewColumnsOrThrow } from '#imports'
import type { Group, GroupNestedIn, Row } from '#imports' import type { Group, GroupNestedIn, Row } from '#imports'
const excludedGroupingUidt = [UITypes.Attachment, UITypes.QrCode, UITypes.Barcode]
export const useViewGroupBy = (view: Ref<ViewType | undefined>, where?: ComputedRef<string | undefined>) => { export const useViewGroupBy = (view: Ref<ViewType | undefined>, where?: ComputedRef<string | undefined>) => {
const groupByLimit: number = 3
const { api } = useApi() const { api } = useApi()
const { appInfo } = useGlobal() const { appInfo } = useGlobal()
@ -18,6 +23,8 @@ export const useViewGroupBy = (view: Ref<ViewType | undefined>, where?: Computed
const { gridViewCols } = useViewColumnsOrThrow() const { gridViewCols } = useViewColumnsOrThrow()
const { getMeta } = useMetas()
const groupBy = computed<{ column: ColumnType; sort: string; order?: number }[]>(() => { const groupBy = computed<{ column: ColumnType; sort: string; order?: number }[]>(() => {
const tempGroupBy: { column: ColumnType; sort: string; order?: number }[] = [] const tempGroupBy: { column: ColumnType; sort: string; order?: number }[] = []
Object.values(gridViewCols.value).forEach((col) => { Object.values(gridViewCols.value).forEach((col) => {
@ -54,6 +61,20 @@ export const useViewGroupBy = (view: Ref<ViewType | undefined>, where?: Computed
return appInfo.value.defaultGroupByLimit?.limitRecord || 10 return appInfo.value.defaultGroupByLimit?.limitRecord || 10
}) })
const supportedLookups = ref<string[]>([])
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)
}
return true
}),
)
const rootGroup = ref<Group>({ const rootGroup = ref<Group>({
key: 'root', key: 'root',
color: 'root', color: 'root',
@ -464,5 +485,61 @@ export const useViewGroupBy = (view: Ref<ViewType | undefined>, where?: Computed
} }
} }
return { rootGroup, groupBy, isGroupBy, loadGroups, loadGroupData, loadGroupPage, groupWrapperChangePage, redistributeRows } 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)
}
supportedLookups.value = filteredLookupCols
} catch (e) {
console.error(e)
}
}
onMounted(async () => {
await loadAllowedLookups()
})
watch(meta, async () => {
await loadAllowedLookups()
})
return {
rootGroup,
groupBy,
isGroupBy,
fieldsToGroupBy,
groupByLimit,
loadGroups,
loadGroupData,
loadGroupPage,
groupWrapperChangePage,
redistributeRows,
}
} }

2
packages/nc-gui/lib/constants.ts

@ -17,4 +17,4 @@ export const GROUP_BY_VARS = {
__nc_true__: 'Checked', __nc_true__: 'Checked',
__nc_false__: 'Unchecked', __nc_false__: 'Unchecked',
} as Record<string, string>, } as Record<string, string>,
} }

3
packages/nc-gui/lib/enums.ts

@ -81,6 +81,9 @@ export enum SmartsheetStoreEvents {
FIELD_ADD = 'field-add', FIELD_ADD = 'field-add',
MAPPED_BY_COLUMN_CHANGE = 'mapped-by-column-change', MAPPED_BY_COLUMN_CHANGE = 'mapped-by-column-change',
CLEAR_NEW_ROW = 'clear-new-row', CLEAR_NEW_ROW = 'clear-new-row',
GROUP_BY_ADD = 'group-by-add',
GROUP_BY_REMOVE = 'group-by-remove',
FILTER_ADD = 'filter-add',
} }
export enum DataSourcesSubTab { export enum DataSourcesSubTab {

Loading…
Cancel
Save