Browse Source

Nc feat/filter UI (#8429)

* feat: hide arrow if it's in readonly mode

* fix: apply locking logic with filter group

* fix: remove border and padding between filter options

* fix: spacing and font-size corrections

* fix: missing separator line

* feat: use different background in each level

* feat: remove border for `where` label

* feat: keep border for `where` label

* feat: filter ui

* refactor: update spacing

* feat: suggested ui changes

* fix: disabled text color correction

* feat: focus add button by default and on reopening

* test: fix filter group tests

* fix: typo correction

* refactor: spacing and height correction

* refactor: set darker border color to make visible in inner most group

* refactor: font color and column order

* fix: filter sql error

* Update packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* refactor: improved version for focusing button

Signed-off-by: Pranav C <pranavxc@gmail.com>

* refactor: replace button with menuitem

Signed-off-by: Pranav C <pranavxc@gmail.com>

---------

Signed-off-by: Pranav C <pranavxc@gmail.com>
Co-authored-by: Pranav C <pranavxc@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
pull/8535/head
Raju Udava 6 months ago committed by GitHub
parent
commit
afe99e26c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 246
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue
  2. 1
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilterMenu.vue
  3. 9
      packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue
  4. 2
      packages/nc-gui/composables/useViewFilters.ts
  5. 9
      tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts

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

@ -11,6 +11,7 @@ interface Props {
modelValue?: undefined | Filter[] modelValue?: undefined | Filter[]
webHook?: boolean webHook?: boolean
draftFilter?: Partial<FilterType> draftFilter?: Partial<FilterType>
isOpen?: boolean
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@ -27,6 +28,7 @@ const emit = defineEmits(['update:filtersLength', 'update:draftFilter', 'update:
const excludedFilterColUidt = [UITypes.QrCode, UITypes.Barcode] const excludedFilterColUidt = [UITypes.QrCode, UITypes.Barcode]
const draftFilter = useVModel(props, 'draftFilter', emit) const draftFilter = useVModel(props, 'draftFilter', emit)
const modelValue = useVModel(props, 'modelValue', emit) const modelValue = useVModel(props, 'modelValue', emit)
const { nestedLevel, parentId, autoSave, hookId, showLoading, webHook } = toRefs(props) const { nestedLevel, parentId, autoSave, hookId, showLoading, webHook } = toRefs(props)
@ -386,6 +388,15 @@ watch(
immediate: true, immediate: true,
}, },
) )
const addFilterBtnRef = ref()
watchEffect(() => {
if (props.isOpen && !nested.value && addFilterBtnRef.value) {
setTimeout(() => {
addFilterBtnRef.value?.$el?.focus()
}, 10)
}
})
</script> </script>
<template> <template>
@ -394,13 +405,63 @@ watch(
:class="{ :class="{
'max-h-[max(80vh,500px)] min-w-112 py-2 pl-4': !nested, 'max-h-[max(80vh,500px)] min-w-112 py-2 pl-4': !nested,
'w-full ': nested, 'w-full ': nested,
'py-4': !filters.length,
}" }"
> >
<div v-if="nested" class="flex w-full items-center mb-2">
<div :class="[`nc-filter-logical-op-level-${nestedLevel}`]"><slot name="start"></slot></div>
<div class="flex-grow"></div>
<NcDropdown :trigger="['hover']" overlay-class-name="nc-dropdown-filter-group-sub-menu">
<GeneralIcon icon="plus" class="cursor-pointer" />
<template #overlay>
<NcMenu>
<template v-if="isEeUI && !isPublic">
<template v-if="filtersCount < getPlanLimit(PlanLimitTypes.FILTER_LIMIT)">
<NcMenuItem @click.stop="addFilter()">
<div class="flex items-center gap-1">
<component :is="iconMap.plus" />
<!-- Add Filter -->
{{ $t('activity.addFilter') }}
</div>
</NcMenuItem>
<NcMenuItem v-if="nestedLevel < 5" @click.stop="addFilterGroup()">
<div class="flex items-center gap-1">
<!-- Add Filter Group -->
<component :is="iconMap.plusSquare" />
{{ $t('activity.addFilterGroup') }}
</div>
</NcMenuItem>
</template>
</template>
<template v-else>
<NcMenuItem @click.stop="addFilter()">
<div class="flex items-center gap-1">
<component :is="iconMap.plus" />
<!-- Add Filter -->
{{ $t('activity.addFilter') }}
</div>
</NcMenuItem>
<NcMenuItem v-if="!webHook && nestedLevel < 5" @click.stop="addFilterGroup()">
<div class="flex items-center gap-1">
<!-- Add Filter Group -->
<component :is="iconMap.plusSquare" />
{{ $t('activity.addFilterGroup') }}
</div>
</NcMenuItem>
</template>
</NcMenu>
</template>
</NcDropdown>
<div>
<slot name="end"></slot>
</div>
</div>
<div <div
v-if="filters && filters.length" v-if="filters && filters.length"
ref="wrapperDomRef" ref="wrapperDomRef"
class="flex flex-col gap-y-3 nc-filter-grid w-full" class="flex flex-col gap-y-1.5 nc-filter-grid w-full"
:class="{ 'max-h-420px nc-scrollbar-thin nc-filter-top-wrapper pr-4 my-2 py-1': !nested }" :class="{ 'max-h-420px nc-scrollbar-thin nc-filter-top-wrapper pr-4 my-2 py-1': !nested }"
@click.stop @click.stop
> >
@ -408,17 +469,29 @@ watch(
<template v-if="filter.status !== 'delete'"> <template v-if="filter.status !== 'delete'">
<template v-if="filter.is_group"> <template v-if="filter.is_group">
<div class="flex flex-col w-full gap-y-2"> <div class="flex flex-col w-full gap-y-2">
<div class="flex flex-row w-full justify-between items-center"> <div class="flex rounded-lg p-2 w-full border-1" :class="[`nc-filter-nested-level-${nestedLevel}`]">
<span v-if="!i" class="flex items-center ml-2">{{ $t('labels.where') }}</span> <LazySmartsheetToolbarColumnFilter
v-if="filter.id || filter.children || !autoSave"
:key="filter.id ?? i"
ref="localNestedFilters"
v-model="filter.children"
:nested-level="nestedLevel + 1"
:parent-id="filter.id"
:auto-save="autoSave"
:web-hook="webHook"
>
<template #start>
<span v-if="!i" class="flex items-center nc-filter-where-label ml-1">{{ $t('labels.where') }}</span>
<div v-else :key="`${i}nested`" class="flex nc-filter-logical-op"> <div v-else :key="`${i}nested`" class="flex nc-filter-logical-op">
<NcSelect <NcSelect
v-model:value="filter.logical_op" v-model:value="filter.logical_op"
v-e="['c:filter:logical-op:select']" v-e="['c:filter:logical-op:select']"
:dropdown-match-select-width="false" :dropdown-match-select-width="false"
class="min-w-20 capitalize" class="min-w-18 max-w-18 capitalize"
placeholder="Group op" placeholder="Group op"
dropdown-class-name="nc-dropdown-filter-logical-op-group" dropdown-class-name="nc-dropdown-filter-logical-op-group"
:disabled="visibleFilters.indexOf(filter) > 1 && !isLogicalOpChangeAllowed" :disabled="i > 1 && !isLogicalOpChangeAllowed"
:class="{ 'nc-disabled-logical-op': filter.readOnly || (i > 1 && !isLogicalOpChangeAllowed) }"
@click.stop @click.stop
@change="onLogicalOpUpdate(filter, i)" @change="onLogicalOpUpdate(filter, i)"
> >
@ -435,6 +508,8 @@ watch(
</a-select-option> </a-select-option>
</NcSelect> </NcSelect>
</div> </div>
</template>
<template #end>
<NcButton <NcButton
v-if="!filter.readOnly" v-if="!filter.readOnly"
:key="i" :key="i"
@ -446,33 +521,29 @@ watch(
> >
<component :is="iconMap.deleteListItem" /> <component :is="iconMap.deleteListItem" />
</NcButton> </NcButton>
</div> </template>
<div class="flex border-1 rounded-lg p-2 w-full" :class="nestedLevel % 2 !== 0 ? 'bg-white' : 'bg-gray-100'"> </LazySmartsheetToolbarColumnFilter>
<LazySmartsheetToolbarColumnFilter
v-if="filter.id || filter.children || !autoSave"
:key="filter.id ?? i"
ref="localNestedFilters"
v-model="filter.children"
:nested-level="nestedLevel + 1"
:parent-id="filter.id"
:auto-save="autoSave"
:web-hook="webHook"
/>
</div> </div>
</div> </div>
</template> </template>
<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> <div v-else class="flex flex-row gap-x-0 w-full nc-filter-wrapper" :class="`nc-filter-wrapper-${filter.fk_column_id}`">
<div v-if="!i" class="flex items-center !min-w-18 !max-w-18 pl-3 nc-filter-where-label">
{{ $t('labels.where') }}
</div>
<NcSelect <NcSelect
v-else v-else
v-model:value="filter.logical_op" v-model:value="filter.logical_op"
v-e="['c:filter:logical-op:select']" v-e="['c:filter:logical-op:select']"
:dropdown-match-select-width="false" :dropdown-match-select-width="false"
class="h-full !min-w-20 !max-w-20 capitalize" class="h-full !min-w-18 !max-w-18 capitalize"
hide-details hide-details
:disabled="filter.readOnly || (visibleFilters.indexOf(filter) > 1 && !isLogicalOpChangeAllowed)" :disabled="filter.readOnly || (visibleFilters.indexOf(filter) > 1 && !isLogicalOpChangeAllowed)"
dropdown-class-name="nc-dropdown-filter-logical-op" dropdown-class-name="nc-dropdown-filter-logical-op"
:class="{
'nc-disabled-logical-op': filter.readOnly || (visibleFilters.indexOf(filter) > 1 && !isLogicalOpChangeAllowed),
}"
@change="onLogicalOpUpdate(filter, i)" @change="onLogicalOpUpdate(filter, i)"
@click.stop @click.stop
> >
@ -488,6 +559,7 @@ watch(
</div> </div>
</a-select-option> </a-select-option>
</NcSelect> </NcSelect>
<SmartsheetToolbarFieldListAutoCompleteDropdown <SmartsheetToolbarFieldListAutoCompleteDropdown
:key="`${i}_6`" :key="`${i}_6`"
v-model="filter.fk_column_id" v-model="filter.fk_column_id"
@ -497,6 +569,7 @@ watch(
@click.stop @click.stop
@change="selectFilterField(filter, i)" @change="selectFilterField(filter, i)"
/> />
<NcSelect <NcSelect
v-model:value="filter.comparison_op" v-model:value="filter.comparison_op"
v-e="['c:filter:comparison-op:select']" v-e="['c:filter:comparison-op:select']"
@ -529,6 +602,7 @@ watch(
</NcSelect> </NcSelect>
<div v-if="['blank', 'notblank'].includes(filter.comparison_op)" class="flex flex-grow"></div> <div v-if="['blank', 'notblank'].includes(filter.comparison_op)" class="flex flex-grow"></div>
<NcSelect <NcSelect
v-else-if="isDateType(types[filter.fk_column_id])" v-else-if="isDateType(types[filter.fk_column_id])"
v-model:value="filter.comparison_sub_op" v-model:value="filter.comparison_sub_op"
@ -567,6 +641,7 @@ watch(
</a-select-option> </a-select-option>
</template> </template>
</NcSelect> </NcSelect>
<a-checkbox <a-checkbox
v-if="filter.field && types[filter.field] === 'boolean'" v-if="filter.field && types[filter.field] === 'boolean'"
v-model:checked="filter.value" v-model:checked="filter.value"
@ -583,6 +658,7 @@ watch(
@update-filter-value="(value) => updateFilterValue(value, filter, i)" @update-filter-value="(value) => updateFilterValue(value, filter, i)"
@click.stop @click.stop
/> />
<div v-else-if="!isDateType(types[filter.fk_column_id])" class="flex-grow"></div> <div v-else-if="!isDateType(types[filter.fk_column_id])" class="flex-grow"></div>
<NcButton <NcButton
@ -600,16 +676,16 @@ watch(
</template> </template>
</div> </div>
<template v-if="!nested">
<template v-if="isEeUI && !isPublic"> <template v-if="isEeUI && !isPublic">
<div <div
v-if="filtersCount < getPlanLimit(PlanLimitTypes.FILTER_LIMIT)" v-if="filtersCount < getPlanLimit(PlanLimitTypes.FILTER_LIMIT)"
ref="addFiltersRowDomRef"
class="flex gap-2" class="flex gap-2"
:class="{ :class="{
'mt-1 mb-2': filters.length, 'mt-1 mb-2': filters.length,
}" }"
> >
<NcButton size="small" type="text" class="!text-brand-500" @click.stop="addFilter()"> <NcButton :ref="addFilterBtnRef" size="small" type="text" class="nc-btn-focus" @click.stop="addFilter()">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<component :is="iconMap.plus" /> <component :is="iconMap.plus" />
<!-- Add Filter --> <!-- Add Filter -->
@ -617,7 +693,7 @@ watch(
</div> </div>
</NcButton> </NcButton>
<NcButton v-if="nestedLevel < 5" type="text" size="small" @click.stop="addFilterGroup()"> <NcButton v-if="nestedLevel < 5" class="nc-btn-focus" type="text" size="small" @click.stop="addFilterGroup()">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<!-- Add Filter Group --> <!-- Add Filter Group -->
<component :is="iconMap.plus" /> <component :is="iconMap.plus" />
@ -626,6 +702,7 @@ watch(
</NcButton> </NcButton>
</div> </div>
</template> </template>
<template v-else> <template v-else>
<div <div
ref="addFiltersRowDomRef" ref="addFiltersRowDomRef"
@ -634,7 +711,7 @@ watch(
'mt-1 mb-2': filters.length, 'mt-1 mb-2': filters.length,
}" }"
> >
<NcButton size="small" type="text" class="!text-brand-500" @click.stop="addFilter()"> <NcButton ref="addFilterBtnRef" class="nc-btn-focus" size="small" type="text" @click.stop="addFilter()">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<component :is="iconMap.plus" /> <component :is="iconMap.plus" />
<!-- Add Filter --> <!-- Add Filter -->
@ -642,7 +719,13 @@ watch(
</div> </div>
</NcButton> </NcButton>
<NcButton v-if="!webHook && nestedLevel < 5" type="text" size="small" @click.stop="addFilterGroup()"> <NcButton
v-if="!webHook && nestedLevel < 5"
class="nc-btn-focus"
type="text"
size="small"
@click.stop="addFilterGroup()"
>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<!-- Add Filter Group --> <!-- Add Filter Group -->
<component :is="iconMap.plus" /> <component :is="iconMap.plus" />
@ -651,6 +734,7 @@ watch(
</NcButton> </NcButton>
</div> </div>
</template> </template>
</template>
<div <div
v-if="!filters.length" v-if="!filters.length"
class="flex flex-row text-gray-400 mt-2" class="flex flex-row text-gray-400 mt-2"
@ -666,7 +750,7 @@ watch(
</div> </div>
</template> </template>
<style scoped> <style scoped lang="scss">
.nc-filter-item-remove-btn { .nc-filter-item-remove-btn {
@apply text-gray-600 hover:text-gray-800; @apply text-gray-600 hover:text-gray-800;
} }
@ -680,6 +764,112 @@ watch(
} }
:deep(.ant-select-selector) { :deep(.ant-select-selector) {
@apply !min-h-8.25; @apply !min-h-8;
}
.nc-disabled-logical-op :deep(.ant-select-arrow) {
@apply hidden;
}
.nc-filter-wrapper {
@apply bg-white !rounded-lg border-1px border-[#E7E7E9];
& > * {
@apply !border-none;
}
& > * > :deep(.ant-select-selector) {
border: none !important;
box-shadow: none !important;
}
& > :not(:last-child):not(:empty) {
border-right: 1px solid #eee !important;
border-bottom-right-radius: 0 !important;
border-top-right-radius: 0 !important;
}
& > :not(:first-child) {
border-bottom-left-radius: 0 !important;
border-top-left-radius: 0 !important;
}
& > :last-child {
@apply relative;
&::after {
content: '';
@apply absolute h-full w-1px bg-[#eee] -left-1px top-0;
}
}
:deep(::placeholder) {
@apply text-sm tracking-normal;
}
:deep(::-ms-input-placeholder) {
@apply text-sm tracking-normal;
}
:deep(input) {
@apply text-sm;
}
:deep(.nc-select:not(.nc-disabled-logical-op):hover) {
&,
.ant-select-selector {
@apply bg-gray-50;
}
}
}
.nc-filter-nested-level-0 {
@apply bg-[#f9f9fa];
}
.nc-filter-nested-level-1,
.nc-filter-nested-level-3 {
@apply bg-gray-[#f4f4f5];
}
.nc-filter-nested-level-2,
.nc-filter-nested-level-4 {
@apply bg-gray-[#e7e7e9];
}
.nc-filter-logical-op-level-3,
.nc-filter-logical-op-level-5 {
:deep(.nc-select.ant-select .ant-select-selector) {
@apply border-[#d9d9d9];
}
}
.nc-filter-where-label {
@apply text-gray-400;
}
:deep(.ant-select-disabled.ant-select:not(.ant-select-customize-input) .ant-select-selector) {
@apply bg-transparent text-gray-400;
}
:deep(.nc-filter-logical-op .nc-select.ant-select .ant-select-selector) {
@apply shadow-none;
}
:deep(.nc-select-expand-btn) {
@apply text-gray-500;
}
.menu-filter-dropdown {
input:not(:disabled),
select:not(:disabled),
.ant-select:not(.ant-select-disabled) {
@apply text-[#4A5268];
}
}
.nc-filter-input-wrapper :deep(input) {
@apply !px-2;
}
.nc-btn-focus:focus {
@apply !text-brand-500 !shadow-none;
} }
</style> </style>

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

@ -86,6 +86,7 @@ eventBus.on(async (event, column: ColumnType) => {
:auto-save="true" :auto-save="true"
data-testid="nc-filter-menu" data-testid="nc-filter-menu"
@update:filters-length="filtersLength = $event" @update:filters-length="filtersLength = $event"
:is-open="open"
> >
</SmartsheetToolbarColumnFilter> </SmartsheetToolbarColumnFilter>
</template> </template>

9
packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue

@ -63,14 +63,15 @@ const options = computed<SelectProps['options']>(() =>
return !isVirtualSystemField return !isVirtualSystemField
} }
}) })
)?.map((c: ColumnType) => ({ )
// sort and keep system columns at the end
?.sort((field1, field2) => +isSystemColumn(field2) - +isSystemColumn(field1))
?.map((c: ColumnType) => ({
value: c.id, value: c.id,
label: c.title, label: c.title,
icon: h( icon: h(
isVirtualCol(c) ? resolveComponent('SmartsheetHeaderVirtualCellIcon') : resolveComponent('SmartsheetHeaderCellIcon'), isVirtualCol(c) ? resolveComponent('SmartsheetHeaderVirtualCellIcon') : resolveComponent('SmartsheetHeaderCellIcon'),
{ { columnMeta: c },
columnMeta: c,
},
), ),
c, c,
})), })),

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

@ -188,7 +188,7 @@ export function useViewFilters(
comparison_op: comparisonOpList(options.value?.[0].uidt as UITypes).filter((compOp) => comparison_op: comparisonOpList(options.value?.[0].uidt as UITypes).filter((compOp) =>
isComparisonOpAllowed({ fk_column_id: options.value?.[0].id }, compOp), isComparisonOpAllowed({ fk_column_id: options.value?.[0].id }, compOp),
)?.[0].value as FilterType['comparison_op'], )?.[0].value as FilterType['comparison_op'],
value: '', value: null,
status: 'create', status: 'create',
logical_op: logicalOps.size === 1 ? logicalOps.values().next().value : 'and', logical_op: logicalOps.size === 1 ? logicalOps.values().next().value : 'and',
} }

9
tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts

@ -65,7 +65,14 @@ export class ToolbarFilterPage extends BasePage {
await this.get().locator(`button:has-text("Add Filter Group")`).last().click(); await this.get().locator(`button:has-text("Add Filter Group")`).last().click();
const filterDropdown = this.get().locator('.menu-filter-dropdown').nth(filterGroupIndex); const filterDropdown = this.get().locator('.menu-filter-dropdown').nth(filterGroupIndex);
await filterDropdown.waitFor({ state: 'visible' }); await filterDropdown.waitFor({ state: 'visible' });
await filterDropdown.locator(`button:has-text("Add Filter")`).first().click(); const ADD_BUTTON_SELECTOR = `span:has-text("add")`;
const FILTER_GROUP_SUB_MENU_SELECTOR = `.nc-dropdown-filter-group-sub-menu`;
const ADD_FILTER_SELECTOR = `.nc-menu-item:has-text("Add Filter")`;
await filterDropdown.locator(ADD_BUTTON_SELECTOR).first().click();
const filterGroupSubMenu = this.rootPage.locator(FILTER_GROUP_SUB_MENU_SELECTOR).last();
await filterGroupSubMenu.waitFor({ state: 'visible' });
await filterGroupSubMenu.locator(ADD_FILTER_SELECTOR).first().click();
const selectField = filterDropdown.locator('.nc-filter-field-select').last(); const selectField = filterDropdown.locator('.nc-filter-field-select').last();
const selectOperation = filterDropdown.locator('.nc-filter-operation-select').last(); const selectOperation = filterDropdown.locator('.nc-filter-operation-select').last();
const selectValue = filterDropdown.locator('.nc-filter-value-select > input').last(); const selectValue = filterDropdown.locator('.nc-filter-value-select > input').last();

Loading…
Cancel
Save