Browse Source

Merge branch 'enhancement/filters' into refactor/ui-improvements

pull/3774/head
Wing-Kam Wong 2 years ago
parent
commit
457f634cf5
  1. 5
      packages/nc-gui/components/cell/Duration.vue
  2. 11
      packages/nc-gui/components/cell/MultiSelect.vue
  3. 20
      packages/nc-gui/components/cell/SingleSelect.vue
  4. 33
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue
  5. 186
      packages/nc-gui/components/smartsheet/toolbar/FilterInput.vue
  6. 2
      tests/playwright/pages/Dashboard/WebhookForm/index.ts
  7. 1
      tests/playwright/pages/Dashboard/common/Cell/SelectOptionCell.ts
  8. 4
      tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts

5
packages/nc-gui/components/cell/Duration.vue

@ -13,9 +13,10 @@ import {
interface Props { interface Props {
modelValue: number | string | null | undefined modelValue: number | string | null | undefined
showValidationError: boolean
} }
const { modelValue } = defineProps<Props>() const { modelValue, showValidationError = true } = defineProps<Props>()
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
@ -99,7 +100,7 @@ const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
<span v-else> {{ localState }}</span> <span v-else> {{ localState }}</span>
<div v-if="showWarningMessage" class="duration-warning"> <div v-if="showWarningMessage && showValidationError" class="duration-warning">
<!-- TODO: i18n --> <!-- TODO: i18n -->
Please enter a number Please enter a number
</div> </div>

11
packages/nc-gui/components/cell/MultiSelect.vue

@ -28,9 +28,10 @@ import MdiCloseCircle from '~icons/mdi/close-circle'
interface Props { interface Props {
modelValue?: string | string[] modelValue?: string | string[]
rowIndex?: number rowIndex?: number
disableOptionCreation?: boolean
} }
const { modelValue } = defineProps<Props>() const { modelValue, disableOptionCreation } = defineProps<Props>()
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
@ -317,7 +318,13 @@ const onTagClick = (e: Event, onClose: Function) => {
</a-select-option> </a-select-option>
<a-select-option <a-select-option
v-if="searchVal && isOptionMissing && !isPublic && (hasRole('owner', true) || hasRole('creator', true))" v-if="
searchVal &&
isOptionMissing &&
!isPublic &&
!disableOptionCreation &&
(hasRole('owner', true) || hasRole('creator', true))
"
:key="searchVal" :key="searchVal"
:value="searchVal" :value="searchVal"
> >

20
packages/nc-gui/components/cell/SingleSelect.vue

@ -14,6 +14,7 @@ import {
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
inject, inject,
ref, ref,
useEventListener,
useRoles, useRoles,
useSelectedCellKeyupListener, useSelectedCellKeyupListener,
watch, watch,
@ -22,9 +23,10 @@ import {
interface Props { interface Props {
modelValue?: string | undefined modelValue?: string | undefined
rowIndex?: number rowIndex?: number
disableOptionCreation?: boolean
} }
const { modelValue } = defineProps<Props>() const { modelValue, disableOptionCreation } = defineProps<Props>()
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
@ -189,6 +191,14 @@ const toggleMenu = (e: Event) => {
} }
isOpen.value = editAllowed.value && !isOpen.value isOpen.value = editAllowed.value && !isOpen.value
} }
const handleClose = (e: MouseEvent) => {
if (aselect.value && !aselect.value.$el.contains(e.target)) {
isOpen.value = false
}
}
useEventListener(document, 'click', handleClose)
</script> </script>
<template> <template>
@ -231,7 +241,13 @@ const toggleMenu = (e: Event) => {
</a-tag> </a-tag>
</a-select-option> </a-select-option>
<a-select-option <a-select-option
v-if="searchVal && isOptionMissing && !isPublic && (hasRole('owner', true) || hasRole('creator', true))" v-if="
searchVal &&
isOptionMissing &&
!isPublic &&
!disableOptionCreation &&
(hasRole('owner', true) || hasRole('creator', true))
"
:key="searchVal" :key="searchVal"
:value="searchVal" :value="searchVal"
> >

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

@ -101,6 +101,15 @@ watch(
}, },
) )
const getColumn = (filter: Filter) => {
return columns.value?.find((col) => col.id === filter.fk_column_id)
}
const selectFilterField = (filter: Filter, index: number) => {
filter.value = null
saveOrUpdate(filter, index)
}
const applyChanges = async (hookId?: string, _nested = false) => { const applyChanges = async (hookId?: string, _nested = false) => {
await sync(hookId, _nested) await sync(hookId, _nested)
@ -214,7 +223,7 @@ defineExpose({
:columns="columns" :columns="columns"
:disabled="filter.readOnly" :disabled="filter.readOnly"
@click.stop @click.stop
@change="saveOrUpdate(filter, i)" @change="selectFilterField(filter, i)"
/> />
<a-select <a-select
@ -244,14 +253,26 @@ defineExpose({
:key="`span${i}`" :key="`span${i}`"
/> />
<a-input <a-checkbox
v-else-if="filter.field && types[filter.field] === 'boolean'"
v-model:checked="filter.value"
dense
:disabled="filter.readOnly"
@change="saveOrUpdate(filter, i)"
/>
<LazySmartsheetToolbarFilterInput
v-else v-else
:key="`${i}_7`"
v-model:value="filter.value"
class="nc-filter-value-select" class="nc-filter-value-select"
:disabled="filter.readOnly || !filter.fk_column_id" :column="getColumn(filter)"
:filter="filter"
@update-filter-value="
(value) => {
filter.value = value
saveOrUpdateDebounced(filter, i)
}
"
@click.stop @click.stop
@input="saveOrUpdateDebounced(filter, i)"
/> />
</template> </template>
</template> </template>

186
packages/nc-gui/components/smartsheet/toolbar/FilterInput.vue

@ -0,0 +1,186 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import {
ColumnInj,
EditModeInj,
ReadonlyInj,
computed,
isBoolean,
isCurrency,
isDate,
isDateTime,
isDecimal,
isDuration,
isFloat,
isInt,
isMultiSelect,
isPercent,
isRating,
isSingleSelect,
isTextArea,
isTime,
isYear,
provide,
ref,
toRef,
useProject,
} from '#imports'
import type { Filter } from '~/lib'
import SingleSelect from '~/components/cell/SingleSelect.vue'
import MultiSelect from '~/components/cell/MultiSelect.vue'
import DatePicker from '~/components/cell/DatePicker.vue'
import YearPicker from '~/components/cell/YearPicker.vue'
import DateTimePicker from '~/components/cell/DateTimePicker.vue'
import TimePicker from '~/components/cell/TimePicker.vue'
import Rating from '~/components/cell/Rating.vue'
import Duration from '~/components/cell/Duration.vue'
import Percent from '~/components/cell/Percent.vue'
import Currency from '~/components/cell/Currency.vue'
import Decimal from '~/components/cell/Decimal.vue'
import Integer from '~/components/cell/Integer.vue'
import Float from '~/components/cell/Float.vue'
import Text from '~/components/cell/Text.vue'
interface Props {
column: ColumnType
filter: Filter
}
interface Emits {
(event: 'updateFilterValue', model: any): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const column = toRef(props, 'column')
const editEnabled = ref(true)
provide(ColumnInj, column)
provide(EditModeInj, readonly(editEnabled))
provide(ReadonlyInj, ref(false))
const checkTypeFunctions = {
isSingleSelect,
isMultiSelect,
isDate,
isYear,
isDateTime,
isTime,
isRating,
isDuration,
isPercent,
isCurrency,
isDecimal,
isInt,
isFloat,
isTextArea,
}
type FilterType = keyof typeof checkTypeFunctions
const { sqlUi } = $(useProject())
const abstractType = $computed(() => (column.value?.dt && sqlUi ? sqlUi.getAbstractType(column.value) : null))
const checkType = (filterType: FilterType) => {
const checkTypeFunction = checkTypeFunctions[filterType]
if (!column.value || !checkTypeFunction) {
return false
}
return checkTypeFunction(column.value, abstractType)
}
const filterInput = computed({
get: () => {
return props.filter.value
},
set: (value) => {
emit('updateFilterValue', value)
},
})
const booleanOptions = [
{ value: true, label: 'true' },
{ value: false, label: 'false' },
{ value: null, label: 'unset' },
]
const componentMap: Partial<Record<FilterType, any>> = {
isSingleSelect: SingleSelect,
isMultiSelect: MultiSelect,
isDate: DatePicker,
isYear: YearPicker,
isDateTime: DateTimePicker,
isTime: TimePicker,
isRating: Rating,
isDuration: Duration,
isPercent: Percent,
isCurrency: Currency,
isDecimal: Decimal,
isInt: Integer,
isFloat: Float,
}
const filterType = $computed(() => {
return Object.keys(componentMap).find((key) => checkType(key as FilterType))
})
const componentProps = $computed(() => {
switch (filterType) {
case 'isSingleSelect':
case 'isMultiSelect': {
return { disableOptionCreation: true }
}
case 'isPercent':
case 'isDecimal':
case 'isFloat':
case 'isInt': {
return { class: 'h-32px' }
}
case 'isDuration': {
return { showValidationError: false }
}
default: {
return {}
}
}
})
const hasExtraPadding = $computed(() => {
return (
column.value &&
(isInt(column.value, abstractType) ||
isDate(column.value, abstractType) ||
isDateTime(column.value, abstractType) ||
isTime(column.value, abstractType) ||
isYear(column.value, abstractType))
)
})
</script>
<template>
<a-select
v-if="column && isBoolean(column, abstractType)"
v-model:value="filterInput"
:disabled="filter.readOnly"
:options="booleanOptions"
/>
<div
v-else
class="bg-white border-1 flex min-w-120px max-w-170px min-h-32px h-full"
:class="{ 'px-2': hasExtraPadding }"
@mouseup.stop
>
<component
:is="filterType ? componentMap[filterType] : Text"
v-model="filterInput"
:disabled="filter.readOnly"
:column="column"
class="flex"
v-bind="componentProps"
/>
</div>
</template>

2
tests/playwright/pages/Dashboard/WebhookForm/index.ts

@ -87,7 +87,7 @@ export class WebhookFormPage extends BasePage {
await this.rootPage.waitForTimeout(1500); await this.rootPage.waitForTimeout(1500);
if (operator != 'is null' && operator != 'is not null') { if (operator != 'is null' && operator != 'is not null') {
await modal.locator('input.nc-filter-value-select').fill(value); await modal.locator('.nc-filter-value-select > input').fill(value);
} }
if (save) { if (save) {

1
tests/playwright/pages/Dashboard/common/Cell/SelectOptionCell.ts

@ -65,7 +65,6 @@ export class SelectOptionCellPageObject extends BasePage {
await this.get({ index, columnHeader }).click(); await this.get({ index, columnHeader }).click();
await this.rootPage.locator('.ant-select-single > .ant-select-clear').click(); await this.rootPage.locator('.ant-select-single > .ant-select-clear').click();
await this.cell.get({ index, columnHeader }).click();
await this.rootPage.locator(`.nc-dropdown-single-select-cell`).waitFor({ state: 'hidden' }); await this.rootPage.locator(`.nc-dropdown-single-select-cell`).waitFor({ state: 'hidden' });
} }

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

@ -18,7 +18,7 @@ export class ToolbarFilterPage extends BasePage {
await expect(this.get().locator('.nc-filter-field-select').nth(index)).toHaveText(column); await expect(this.get().locator('.nc-filter-field-select').nth(index)).toHaveText(column);
await expect(this.get().locator('.nc-filter-operation-select').nth(index)).toHaveText(operator); await expect(this.get().locator('.nc-filter-operation-select').nth(index)).toHaveText(operator);
await expect await expect
.poll(async () => await this.get().locator('input.nc-filter-value-select').nth(index).inputValue()) .poll(async () => await this.get().locator('.nc-filter-value-select > input').nth(index).inputValue())
.toBe(value); .toBe(value);
} }
@ -71,7 +71,7 @@ export class ToolbarFilterPage extends BasePage {
// if value field was provided, fill it // if value field was provided, fill it
if (value) { if (value) {
const fillFilter = this.rootPage.locator('.nc-filter-value-select').last().fill(value); const fillFilter = this.rootPage.locator('.nc-filter-value-select > input').last().fill(value);
await this.waitForResponse({ await this.waitForResponse({
uiAction: fillFilter, uiAction: fillFilter,
httpMethodsToMatch: ['GET'], httpMethodsToMatch: ['GET'],

Loading…
Cancel
Save