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 1 month ago committed by GitHub
parent
commit
afe99e26c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 382
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue
  2. 1
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilterMenu.vue
  3. 23
      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

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

@ -11,6 +11,7 @@ interface Props {
modelValue?: undefined | Filter[]
webHook?: boolean
draftFilter?: Partial<FilterType>
isOpen?: boolean
}
const props = withDefaults(defineProps<Props>(), {
@ -27,6 +28,7 @@ const emit = defineEmits(['update:filtersLength', 'update:draftFilter', 'update:
const excludedFilterColUidt = [UITypes.QrCode, UITypes.Barcode]
const draftFilter = useVModel(props, 'draftFilter', emit)
const modelValue = useVModel(props, 'modelValue', emit)
const { nestedLevel, parentId, autoSave, hookId, showLoading, webHook } = toRefs(props)
@ -386,6 +388,15 @@ watch(
immediate: true,
},
)
const addFilterBtnRef = ref()
watchEffect(() => {
if (props.isOpen && !nested.value && addFilterBtnRef.value) {
setTimeout(() => {
addFilterBtnRef.value?.$el?.focus()
}, 10)
}
})
</script>
<template>
@ -394,13 +405,63 @@ watch(
:class="{
'max-h-[max(80vh,500px)] min-w-112 py-2 pl-4': !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
v-if="filters && filters.length"
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 }"
@click.stop
>
@ -408,46 +469,7 @@ watch(
<template v-if="filter.status !== 'delete'">
<template v-if="filter.is_group">
<div class="flex flex-col w-full gap-y-2">
<div class="flex flex-row w-full justify-between items-center">
<span v-if="!i" class="flex items-center ml-2">{{ $t('labels.where') }}</span>
<div v-else :key="`${i}nested`" class="flex nc-filter-logical-op">
<NcSelect
v-model:value="filter.logical_op"
v-e="['c:filter:logical-op:select']"
:dropdown-match-select-width="false"
class="min-w-20 capitalize"
placeholder="Group op"
dropdown-class-name="nc-dropdown-filter-logical-op-group"
:disabled="visibleFilters.indexOf(filter) > 1 && !isLogicalOpChangeAllowed"
@click.stop
@change="onLogicalOpUpdate(filter, i)"
>
<a-select-option v-for="op in logicalOps" :key="op.value" :value="op.value">
<div class="flex items-center w-full justify-between w-full gap-2">
<div class="truncate flex-1 capitalize">{{ op.value }}</div>
<component
:is="iconMap.check"
v-if="filter.logical_op === op.value"
id="nc-selected-item-icon"
class="text-primary w-4 h-4"
/>
</div>
</a-select-option>
</NcSelect>
</div>
<NcButton
v-if="!filter.readOnly"
:key="i"
v-e="['c:filter:delete']"
type="text"
size="small"
class="nc-filter-item-remove-btn cursor-pointer"
@click.stop="deleteFilter(filter, i)"
>
<component :is="iconMap.deleteListItem" />
</NcButton>
</div>
<div class="flex border-1 rounded-lg p-2 w-full" :class="nestedLevel % 2 !== 0 ? 'bg-white' : 'bg-gray-100'">
<div class="flex rounded-lg p-2 w-full border-1" :class="[`nc-filter-nested-level-${nestedLevel}`]">
<LazySmartsheetToolbarColumnFilter
v-if="filter.id || filter.children || !autoSave"
:key="filter.id ?? i"
@ -457,22 +479,71 @@ watch(
: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">
<NcSelect
v-model:value="filter.logical_op"
v-e="['c:filter:logical-op:select']"
:dropdown-match-select-width="false"
class="min-w-18 max-w-18 capitalize"
placeholder="Group op"
dropdown-class-name="nc-dropdown-filter-logical-op-group"
:disabled="i > 1 && !isLogicalOpChangeAllowed"
:class="{ 'nc-disabled-logical-op': filter.readOnly || (i > 1 && !isLogicalOpChangeAllowed) }"
@click.stop
@change="onLogicalOpUpdate(filter, i)"
>
<a-select-option v-for="op in logicalOps" :key="op.value" :value="op.value">
<div class="flex items-center w-full justify-between w-full gap-2">
<div class="truncate flex-1 capitalize">{{ op.value }}</div>
<component
:is="iconMap.check"
v-if="filter.logical_op === op.value"
id="nc-selected-item-icon"
class="text-primary w-4 h-4"
/>
</div>
</a-select-option>
</NcSelect>
</div>
</template>
<template #end>
<NcButton
v-if="!filter.readOnly"
:key="i"
v-e="['c:filter:delete']"
type="text"
size="small"
class="nc-filter-item-remove-btn cursor-pointer"
@click.stop="deleteFilter(filter, i)"
>
<component :is="iconMap.deleteListItem" />
</NcButton>
</template>
</LazySmartsheetToolbarColumnFilter>
</div>
</div>
</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
v-else
v-model:value="filter.logical_op"
v-e="['c:filter:logical-op:select']"
: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
:disabled="filter.readOnly || (visibleFilters.indexOf(filter) > 1 && !isLogicalOpChangeAllowed)"
dropdown-class-name="nc-dropdown-filter-logical-op"
:class="{
'nc-disabled-logical-op': filter.readOnly || (visibleFilters.indexOf(filter) > 1 && !isLogicalOpChangeAllowed),
}"
@change="onLogicalOpUpdate(filter, i)"
@click.stop
>
@ -488,6 +559,7 @@ watch(
</div>
</a-select-option>
</NcSelect>
<SmartsheetToolbarFieldListAutoCompleteDropdown
:key="`${i}_6`"
v-model="filter.fk_column_id"
@ -497,6 +569,7 @@ watch(
@click.stop
@change="selectFilterField(filter, i)"
/>
<NcSelect
v-model:value="filter.comparison_op"
v-e="['c:filter:comparison-op:select']"
@ -529,6 +602,7 @@ watch(
</NcSelect>
<div v-if="['blank', 'notblank'].includes(filter.comparison_op)" class="flex flex-grow"></div>
<NcSelect
v-else-if="isDateType(types[filter.fk_column_id])"
v-model:value="filter.comparison_sub_op"
@ -567,6 +641,7 @@ watch(
</a-select-option>
</template>
</NcSelect>
<a-checkbox
v-if="filter.field && types[filter.field] === 'boolean'"
v-model:checked="filter.value"
@ -583,6 +658,7 @@ watch(
@update-filter-value="(value) => updateFilterValue(value, filter, i)"
@click.stop
/>
<div v-else-if="!isDateType(types[filter.fk_column_id])" class="flex-grow"></div>
<NcButton
@ -600,56 +676,64 @@ watch(
</template>
</div>
<template v-if="isEeUI && !isPublic">
<div
v-if="filtersCount < getPlanLimit(PlanLimitTypes.FILTER_LIMIT)"
ref="addFiltersRowDomRef"
class="flex gap-2"
:class="{
'mt-1 mb-2': filters.length,
}"
>
<NcButton size="small" type="text" class="!text-brand-500" @click.stop="addFilter()">
<div class="flex items-center gap-1">
<component :is="iconMap.plus" />
<!-- Add Filter -->
{{ $t('activity.addFilter') }}
</div>
</NcButton>
<template v-if="!nested">
<template v-if="isEeUI && !isPublic">
<div
v-if="filtersCount < getPlanLimit(PlanLimitTypes.FILTER_LIMIT)"
class="flex gap-2"
:class="{
'mt-1 mb-2': filters.length,
}"
>
<NcButton :ref="addFilterBtnRef" size="small" type="text" class="nc-btn-focus" @click.stop="addFilter()">
<div class="flex items-center gap-1">
<component :is="iconMap.plus" />
<!-- Add Filter -->
{{ $t('activity.addFilter') }}
</div>
</NcButton>
<NcButton v-if="nestedLevel < 5" type="text" size="small" @click.stop="addFilterGroup()">
<div class="flex items-center gap-1">
<!-- Add Filter Group -->
<component :is="iconMap.plus" />
{{ $t('activity.addFilterGroup') }}
</div>
</NcButton>
</div>
</template>
<template v-else>
<div
ref="addFiltersRowDomRef"
class="flex gap-2"
:class="{
'mt-1 mb-2': filters.length,
}"
>
<NcButton size="small" type="text" class="!text-brand-500" @click.stop="addFilter()">
<div class="flex items-center gap-1">
<component :is="iconMap.plus" />
<!-- Add Filter -->
{{ $t('activity.addFilter') }}
</div>
</NcButton>
<NcButton v-if="nestedLevel < 5" class="nc-btn-focus" type="text" size="small" @click.stop="addFilterGroup()">
<div class="flex items-center gap-1">
<!-- Add Filter Group -->
<component :is="iconMap.plus" />
{{ $t('activity.addFilterGroup') }}
</div>
</NcButton>
</div>
</template>
<NcButton v-if="!webHook && nestedLevel < 5" type="text" size="small" @click.stop="addFilterGroup()">
<div class="flex items-center gap-1">
<!-- Add Filter Group -->
<component :is="iconMap.plus" />
{{ $t('activity.addFilterGroup') }}
</div>
</NcButton>
</div>
<template v-else>
<div
ref="addFiltersRowDomRef"
class="flex gap-2"
:class="{
'mt-1 mb-2': filters.length,
}"
>
<NcButton ref="addFilterBtnRef" class="nc-btn-focus" size="small" type="text" @click.stop="addFilter()">
<div class="flex items-center gap-1">
<component :is="iconMap.plus" />
<!-- Add Filter -->
{{ $t('activity.addFilter') }}
</div>
</NcButton>
<NcButton
v-if="!webHook && nestedLevel < 5"
class="nc-btn-focus"
type="text"
size="small"
@click.stop="addFilterGroup()"
>
<div class="flex items-center gap-1">
<!-- Add Filter Group -->
<component :is="iconMap.plus" />
{{ $t('activity.addFilterGroup') }}
</div>
</NcButton>
</div>
</template>
</template>
<div
v-if="!filters.length"
@ -666,7 +750,7 @@ watch(
</div>
</template>
<style scoped>
<style scoped lang="scss">
.nc-filter-item-remove-btn {
@apply text-gray-600 hover:text-gray-800;
}
@ -680,6 +764,112 @@ watch(
}
: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>

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

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

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

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

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) =>
isComparisonOpAllowed({ fk_column_id: options.value?.[0].id }, compOp),
)?.[0].value as FilterType['comparison_op'],
value: '',
value: null,
status: 'create',
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();
const filterDropdown = this.get().locator('.menu-filter-dropdown').nth(filterGroupIndex);
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 selectOperation = filterDropdown.locator('.nc-filter-operation-select').last();
const selectValue = filterDropdown.locator('.nc-filter-value-select > input').last();

Loading…
Cancel
Save