mirror of https://github.com/nocodb/nocodb
Raju Udava
2 years ago
committed by
GitHub
47 changed files with 2617 additions and 271 deletions
@ -0,0 +1,194 @@
|
||||
<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>> = $computed(() => { |
||||
return { |
||||
// use MultiSelect for SingleSelect columns for anyof / nanyof filters |
||||
isSingleSelect: ['anyof', 'nanyof'].includes(props.filter.comparison_op!) ? MultiSelect : 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" |
||||
placeholder="Enter a value" |
||||
:column="column" |
||||
class="flex" |
||||
v-bind="componentProps" |
||||
/> |
||||
</div> |
||||
</template> |
@ -0,0 +1,358 @@
|
||||
import { NcUpgraderCtx } from './NcUpgrader'; |
||||
import { MetaTable } from '../utils/globals'; |
||||
import NcMetaIO from '../meta/NcMetaIO'; |
||||
import Column from '../models/Column'; |
||||
import Filter from '../models/Filter'; |
||||
import Project from '../models/Project'; |
||||
import { UITypes, SelectOptionsType } from 'nocodb-sdk'; |
||||
|
||||
// as of 0.104.3, almost all filter operators are available to all column types
|
||||
// while some of them aren't supposed to be shown
|
||||
// this upgrader is to remove those unsupported filters / migrate to the correct filter
|
||||
|
||||
// Change Summary:
|
||||
// - Text-based columns:
|
||||
// - remove `>`, `<`, `>=`, `<=`
|
||||
// - Numeric-based / SingleSelect columns:
|
||||
// - remove `like`
|
||||
// - migrate `null`, and `empty` to `blank`
|
||||
// - Checkbox columns:
|
||||
// - remove `equal`
|
||||
// - migrate `empty` and `null` to `notchecked`
|
||||
// - MultiSelect columns:
|
||||
// - remove `like`
|
||||
// - migrate `equal`, `null`, `empty`
|
||||
// - Attachment columns:
|
||||
// - remove `>`, `<`, `>=`, `<=`, `equal`
|
||||
// - migrate `empty`, `null` to `blank`
|
||||
// - LTAR columns:
|
||||
// - remove `>`, `<`, `>=`, `<=`
|
||||
// - migrate `empty`, `null` to `blank`
|
||||
// - Lookup columns:
|
||||
// - migrate `empty`, `null` to `blank`
|
||||
// - Duration columns:
|
||||
// - remove `like`
|
||||
// - migrate `empty`, `null` to `blank`
|
||||
|
||||
const removeEqualFilters = (filter, ncMeta) => { |
||||
let actions = []; |
||||
// remove `is equal`, `is not equal`
|
||||
if (['eq', 'neq'].includes(filter.comparison_op)) { |
||||
actions.push(Filter.delete(filter.id, ncMeta)); |
||||
} |
||||
return actions; |
||||
}; |
||||
|
||||
const removeArithmeticFilters = (filter, ncMeta) => { |
||||
let actions = []; |
||||
// remove `>`, `<`, `>=`, `<=`
|
||||
if (['gt', 'lt', 'gte', 'lte'].includes(filter.comparison_op)) { |
||||
actions.push(Filter.delete(filter.id, ncMeta)); |
||||
} |
||||
return actions; |
||||
}; |
||||
|
||||
const removeLikeFilters = (filter, ncMeta) => { |
||||
let actions = []; |
||||
// remove `is like`, `is not like`
|
||||
if (['like', 'nlike'].includes(filter.comparison_op)) { |
||||
actions.push(Filter.delete(filter.id, ncMeta)); |
||||
} |
||||
return actions; |
||||
}; |
||||
|
||||
const migrateNullAndEmptyToBlankFilters = (filter, ncMeta) => { |
||||
let actions = []; |
||||
if (['empty', 'null'].includes(filter.comparison_op)) { |
||||
// migrate to blank
|
||||
actions.push( |
||||
Filter.update( |
||||
filter.id, |
||||
{ |
||||
comparison_op: 'blank', |
||||
}, |
||||
ncMeta |
||||
) |
||||
); |
||||
} else if (['notempty', 'notnull'].includes(filter.comparison_op)) { |
||||
// migrate to not blank
|
||||
actions.push( |
||||
Filter.update( |
||||
filter.id, |
||||
{ |
||||
comparison_op: 'notblank', |
||||
}, |
||||
ncMeta |
||||
) |
||||
); |
||||
} |
||||
return actions; |
||||
}; |
||||
|
||||
const migrateMultiSelectEq = async (filter, col: Column, ncMeta) => { |
||||
// only allow eq / neq
|
||||
if (!['eq', 'neq'].includes(filter.comparison_op)) return; |
||||
// if there is no value -> delete this filter
|
||||
if (!filter.value) { |
||||
return await Filter.delete(filter.id, ncMeta); |
||||
} |
||||
// options inputted from users
|
||||
const options = filter.value.split(','); |
||||
// retrieve the possible col options
|
||||
const colOptions = (await col.getColOptions()) as SelectOptionsType; |
||||
// only include valid options as the input value becomes dropdown type now
|
||||
let validOptions = []; |
||||
for (const option of options) { |
||||
if (colOptions.options.includes(option)) { |
||||
validOptions.push(option); |
||||
} |
||||
} |
||||
const newFilterValue = validOptions.join(','); |
||||
// if all inputted options are invalid -> delete this filter
|
||||
if (!newFilterValue) { |
||||
return await Filter.delete(filter.id, ncMeta); |
||||
} |
||||
let actions = []; |
||||
if (filter.comparison_op === 'eq') { |
||||
// migrate to `contains all of`
|
||||
actions.push( |
||||
Filter.update( |
||||
filter.id, |
||||
{ |
||||
comparison_op: 'anyof', |
||||
value: newFilterValue, |
||||
}, |
||||
ncMeta |
||||
) |
||||
); |
||||
} else if (filter.comparison_op === 'neq') { |
||||
// migrate to `doesn't contain all of`
|
||||
actions.push( |
||||
Filter.update( |
||||
filter.id, |
||||
{ |
||||
comparison_op: 'nanyof', |
||||
value: newFilterValue, |
||||
}, |
||||
ncMeta |
||||
) |
||||
); |
||||
} |
||||
return await Promise.all(actions); |
||||
}; |
||||
|
||||
const migrateToCheckboxFilter = (filter, ncMeta) => { |
||||
let actions = []; |
||||
const possibleTrueValues = ['true', 'True', '1', 'T', 'Y']; |
||||
const possibleFalseValues = ['false', 'False', '0', 'F', 'N']; |
||||
if (['empty', 'null'].includes(filter.comparison_op)) { |
||||
// migrate to not checked
|
||||
actions.push( |
||||
Filter.update( |
||||
filter.id, |
||||
{ |
||||
comparison_op: 'notchecked', |
||||
}, |
||||
ncMeta |
||||
) |
||||
); |
||||
} else if (['notempty', 'notnull'].includes(filter.comparison_op)) { |
||||
// migrate to checked
|
||||
actions.push( |
||||
Filter.update( |
||||
filter.id, |
||||
{ |
||||
comparison_op: 'checked', |
||||
}, |
||||
ncMeta |
||||
) |
||||
); |
||||
} else if (filter.comparison_op === 'eq') { |
||||
if (possibleTrueValues.includes(filter.value)) { |
||||
// migrate to checked
|
||||
actions.push( |
||||
Filter.update( |
||||
filter.id, |
||||
{ |
||||
comparison_op: 'checked', |
||||
value: '', |
||||
}, |
||||
ncMeta |
||||
) |
||||
); |
||||
} else if (possibleFalseValues.includes(filter.value)) { |
||||
// migrate to notchecked
|
||||
actions.push( |
||||
Filter.update( |
||||
filter.id, |
||||
{ |
||||
comparison_op: 'notchecked', |
||||
value: '', |
||||
}, |
||||
ncMeta |
||||
) |
||||
); |
||||
} else { |
||||
// invalid value - good to delete
|
||||
actions.push(Filter.delete(filter.id, ncMeta)); |
||||
} |
||||
} else if (filter.comparison_op === 'neq') { |
||||
if (possibleFalseValues.includes(filter.value)) { |
||||
// migrate to checked
|
||||
actions.push( |
||||
Filter.update( |
||||
filter.id, |
||||
{ |
||||
comparison_op: 'checked', |
||||
value: '', |
||||
}, |
||||
ncMeta |
||||
) |
||||
); |
||||
} else if (possibleTrueValues.includes(filter.value)) { |
||||
// migrate to not checked
|
||||
actions.push( |
||||
Filter.update( |
||||
filter.id, |
||||
{ |
||||
comparison_op: 'notchecked', |
||||
value: '', |
||||
}, |
||||
ncMeta |
||||
) |
||||
); |
||||
} else { |
||||
// invalid value - good to delete
|
||||
actions.push(Filter.delete(filter.id, ncMeta)); |
||||
} |
||||
} |
||||
return actions; |
||||
}; |
||||
|
||||
async function migrateFilters(ncMeta: NcMetaIO) { |
||||
const filters = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP); |
||||
for (const filter of filters) { |
||||
if (!filter.fk_column_id || filter.is_group) { |
||||
continue; |
||||
} |
||||
const col = await Column.get({ colId: filter.fk_column_id }, ncMeta); |
||||
if ( |
||||
[ |
||||
UITypes.SingleLineText, |
||||
UITypes.LongText, |
||||
UITypes.PhoneNumber, |
||||
UITypes.Email, |
||||
UITypes.URL, |
||||
].includes(col.uidt) |
||||
) { |
||||
await Promise.all(removeArithmeticFilters(filter, ncMeta)); |
||||
} else if ( |
||||
[ |
||||
// numeric fields
|
||||
UITypes.Duration, |
||||
UITypes.Currency, |
||||
UITypes.Percent, |
||||
UITypes.Number, |
||||
UITypes.Decimal, |
||||
UITypes.Rating, |
||||
UITypes.Rollup, |
||||
// select fields
|
||||
UITypes.SingleSelect, |
||||
].includes(col.uidt) |
||||
) { |
||||
await Promise.all([ |
||||
...removeLikeFilters(filter, ncMeta), |
||||
...migrateNullAndEmptyToBlankFilters(filter, ncMeta), |
||||
]); |
||||
} else if (col.uidt === UITypes.Checkbox) { |
||||
await Promise.all(migrateToCheckboxFilter(filter, ncMeta)); |
||||
} else if (col.uidt === UITypes.MultiSelect) { |
||||
await Promise.all([ |
||||
...removeLikeFilters(filter, ncMeta), |
||||
...migrateNullAndEmptyToBlankFilters(filter, ncMeta), |
||||
]); |
||||
await migrateMultiSelectEq(filter, col, ncMeta); |
||||
} else if (col.uidt === UITypes.Attachment) { |
||||
await Promise.all([ |
||||
...removeArithmeticFilters(filter, ncMeta), |
||||
...removeEqualFilters(filter, ncMeta), |
||||
...migrateNullAndEmptyToBlankFilters(filter, ncMeta), |
||||
]); |
||||
} else if (col.uidt === UITypes.LinkToAnotherRecord) { |
||||
await Promise.all([ |
||||
...removeArithmeticFilters(filter, ncMeta), |
||||
...migrateNullAndEmptyToBlankFilters(filter, ncMeta), |
||||
]); |
||||
} else if (col.uidt === UITypes.Lookup) { |
||||
await Promise.all([ |
||||
...removeArithmeticFilters(filter, ncMeta), |
||||
...migrateNullAndEmptyToBlankFilters(filter, ncMeta), |
||||
]); |
||||
} else if (col.uidt === UITypes.Duration) { |
||||
await Promise.all([ |
||||
...removeLikeFilters(filter, ncMeta), |
||||
...migrateNullAndEmptyToBlankFilters(filter, ncMeta), |
||||
]); |
||||
} |
||||
} |
||||
} |
||||
|
||||
async function updateProjectMeta(ncMeta: NcMetaIO) { |
||||
const projectHasEmptyOrFilters: Record<string, boolean> = {}; |
||||
|
||||
const filters = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP); |
||||
|
||||
let actions = []; |
||||
|
||||
for (const filter of filters) { |
||||
if ( |
||||
['notempty', 'notnull', 'empty', 'null'].includes(filter.comparison_op) |
||||
) { |
||||
projectHasEmptyOrFilters[filter.project_id] = true; |
||||
} |
||||
} |
||||
|
||||
const projects = await ncMeta.metaList2(null, null, MetaTable.PROJECT); |
||||
|
||||
const defaultProjectMeta = { |
||||
showNullAndEmptyInFilter: false, |
||||
}; |
||||
|
||||
for (const project of projects) { |
||||
const oldProjectMeta = project.meta; |
||||
let newProjectMeta = defaultProjectMeta; |
||||
try { |
||||
newProjectMeta = |
||||
(typeof oldProjectMeta === 'string' |
||||
? JSON.parse(oldProjectMeta) |
||||
: oldProjectMeta) ?? defaultProjectMeta; |
||||
} catch {} |
||||
|
||||
newProjectMeta = { |
||||
...newProjectMeta, |
||||
showNullAndEmptyInFilter: projectHasEmptyOrFilters[project.id] ?? false, |
||||
}; |
||||
|
||||
actions.push( |
||||
Project.update( |
||||
project.id, |
||||
{ |
||||
meta: JSON.stringify(newProjectMeta), |
||||
}, |
||||
ncMeta |
||||
) |
||||
); |
||||
} |
||||
await Promise.all(actions); |
||||
} |
||||
|
||||
export default async function ({ ncMeta }: NcUpgraderCtx) { |
||||
// fix the existing filter behaviours or
|
||||
// migrate `null` or `empty` filters to `blank`
|
||||
await migrateFilters(ncMeta); |
||||
// enrich `showNullAndEmptyInFilter` in project meta
|
||||
// if there is empty / null filters in existing projects,
|
||||
// then set `showNullAndEmptyInFilter` to true
|
||||
// else set to false
|
||||
await updateProjectMeta(ncMeta); |
||||
} |
@ -0,0 +1,135 @@
|
||||
import { ColumnType, UITypes } from 'nocodb-sdk'; |
||||
|
||||
const rowMixedValue = (column: ColumnType, index: number) => { |
||||
// Array of country names
|
||||
const countries = [ |
||||
'Afghanistan', |
||||
'Albania', |
||||
'', |
||||
'Andorra', |
||||
'Angola', |
||||
'Antigua and Barbuda', |
||||
'Argentina', |
||||
null, |
||||
'Armenia', |
||||
'Australia', |
||||
'Austria', |
||||
'', |
||||
null, |
||||
]; |
||||
|
||||
// Array of sample random paragraphs (comma separated list of cities and countries). Not more than 200 characters
|
||||
const longText = [ |
||||
'Aberdeen, United Kingdom', |
||||
'Abidjan, Côte d’Ivoire', |
||||
'Abuja, Nigeria', |
||||
'', |
||||
'Addis Ababa, Ethiopia', |
||||
'Adelaide, Australia', |
||||
'Ahmedabad, India', |
||||
'Albuquerque, United States', |
||||
null, |
||||
'Alexandria, Egypt', |
||||
'Algiers, Algeria', |
||||
'Allahabad, India', |
||||
'', |
||||
null, |
||||
]; |
||||
|
||||
// Array of random integers, not more than 10000
|
||||
const numbers = [33, null, 456, 333, 267, 34, 8754, 3234, 44, 33, null]; |
||||
const decimals = [33.3, 456.34, 333.3, null, 267.5674, 34.0, 8754.0, 3234.547, 44.2647, 33.98, null]; |
||||
const duration = [60, 120, 180, 3600, 3660, 3720, null, 3780, 60, 120, null]; |
||||
const rating = [0, 1, 2, 3, null, 0, 4, 5, 0, 1, null]; |
||||
|
||||
// Array of random sample email strings (not more than 100 characters)
|
||||
const emails = [ |
||||
'jbutt@gmail.com', |
||||
'josephine_darakjy@darakjy.org', |
||||
'art@venere.org', |
||||
'', |
||||
null, |
||||
'donette.foller@cox.net', |
||||
'simona@morasca.com', |
||||
'mitsue_tollner@yahoo.com', |
||||
'leota@hotmail.com', |
||||
'sage_wieser@cox.net', |
||||
'', |
||||
null, |
||||
]; |
||||
|
||||
// Array of random sample phone numbers
|
||||
const phoneNumbers = [ |
||||
'1-541-754-3010', |
||||
'504-621-8927', |
||||
'810-292-9388', |
||||
'856-636-8749', |
||||
'907-385-4412', |
||||
'513-570-1893', |
||||
'419-503-2484', |
||||
'773-573-6914', |
||||
'', |
||||
null, |
||||
]; |
||||
|
||||
// Array of random sample URLs
|
||||
const urls = [ |
||||
'https://www.google.com', |
||||
'https://www.facebook.com', |
||||
'https://www.youtube.com', |
||||
'https://www.amazon.com', |
||||
'https://www.wikipedia.org', |
||||
'https://www.twitter.com', |
||||
'https://www.instagram.com', |
||||
'https://www.linkedin.com', |
||||
'https://www.reddit.com', |
||||
'https://www.tiktok.com', |
||||
'https://www.pinterest.com', |
||||
'https://www.netflix.com', |
||||
'https://www.microsoft.com', |
||||
'https://www.apple.com', |
||||
'', |
||||
null, |
||||
]; |
||||
|
||||
const singleSelect = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec', null]; |
||||
|
||||
const multiSelect = ['jan,feb,mar', 'apr,may,jun', 'jul,aug,sep', 'oct,nov,dec', 'jan,feb,mar', null]; |
||||
|
||||
const checkbox = [true, false, false, true, false, true, false, false, true, true]; |
||||
|
||||
switch (column.uidt) { |
||||
case UITypes.Checkbox: |
||||
return checkbox[index % checkbox.length]; |
||||
case UITypes.Number: |
||||
case UITypes.Percent: |
||||
return numbers[index % numbers.length]; |
||||
case UITypes.Decimal: |
||||
case UITypes.Currency: |
||||
return decimals[index % decimals.length]; |
||||
case UITypes.Duration: |
||||
return duration[index % duration.length]; |
||||
case UITypes.Rating: |
||||
return rating[index % rating.length]; |
||||
case UITypes.SingleLineText: |
||||
return countries[index % countries.length]; |
||||
case UITypes.Email: |
||||
return emails[index % emails.length]; |
||||
case UITypes.PhoneNumber: |
||||
return phoneNumbers[index % phoneNumbers.length]; |
||||
case UITypes.LongText: |
||||
return longText[index % longText.length]; |
||||
case UITypes.Date: |
||||
return '2020-01-01'; |
||||
case UITypes.URL: |
||||
return urls[index % urls.length]; |
||||
case UITypes.SingleSelect: |
||||
return singleSelect[index % singleSelect.length]; |
||||
case UITypes.MultiSelect: |
||||
return multiSelect[index % multiSelect.length]; |
||||
default: |
||||
return `test-${index}`; |
||||
} |
||||
}; |
||||
|
||||
export { rowMixedValue }; |
@ -0,0 +1,770 @@
|
||||
import { expect, test } from '@playwright/test'; |
||||
import { DashboardPage } from '../pages/Dashboard'; |
||||
import setup from '../setup'; |
||||
import { ToolbarPage } from '../pages/Dashboard/common/Toolbar'; |
||||
import { UITypes } from 'nocodb-sdk'; |
||||
import { Api } from 'nocodb-sdk'; |
||||
import { rowMixedValue } from '../setup/xcdb-records'; |
||||
|
||||
let dashboard: DashboardPage, toolbar: ToolbarPage; |
||||
let context: any; |
||||
let api: Api<any>; |
||||
let records = []; |
||||
|
||||
const skipList = { |
||||
Number: ['is null', 'is not null'], |
||||
Decimal: ['is null', 'is not null'], |
||||
Percent: ['is null', 'is not null'], |
||||
Currency: ['is null', 'is not null'], |
||||
Rating: ['is null', 'is not null', 'is blank', 'is not blank'], |
||||
Duration: ['is null', 'is not null'], |
||||
SingleLineText: [], |
||||
MultiLineText: [], |
||||
Email: [], |
||||
PhoneNumber: [], |
||||
URL: [], |
||||
SingleSelect: ['contains all of', 'does not contain all of'], |
||||
MultiSelect: ['is', 'is not'], |
||||
}; |
||||
|
||||
async function verifyFilterOperatorList(param: { column: string; opType: string[] }) { |
||||
await toolbar.clickFilter({ networkValidation: false }); |
||||
const opList = await toolbar.filter.columnOperatorList({ |
||||
columnTitle: param.column, |
||||
}); |
||||
await toolbar.clickFilter({ networkValidation: false }); |
||||
await toolbar.filter.reset({ networkValidation: false }); |
||||
|
||||
expect(opList).toEqual(param.opType); |
||||
} |
||||
|
||||
// define validateRowArray function
|
||||
async function validateRowArray(param) { |
||||
const { rowCount } = param; |
||||
await dashboard.grid.verifyTotalRowCount({ count: rowCount }); |
||||
} |
||||
|
||||
async function verifyFilter(param: { |
||||
column: string; |
||||
opType: string; |
||||
value?: string; |
||||
result: { rowCount: number }; |
||||
dataType?: string; |
||||
}) { |
||||
// if opType was included in skip list, skip it
|
||||
if (skipList[param.column]?.includes(param.opType)) { |
||||
return; |
||||
} |
||||
|
||||
await toolbar.clickFilter({ networkValidation: false }); |
||||
await toolbar.filter.add({ |
||||
columnTitle: param.column, |
||||
opType: param.opType, |
||||
value: param.value, |
||||
isLocallySaved: false, |
||||
dataType: param?.dataType, |
||||
}); |
||||
await toolbar.clickFilter({ networkValidation: false }); |
||||
|
||||
// verify filtered rows
|
||||
await validateRowArray({ |
||||
rowCount: param.result.rowCount, |
||||
}); |
||||
|
||||
// Reset filter
|
||||
await toolbar.filter.reset({ networkValidation: false }); |
||||
} |
||||
|
||||
// Number based filters
|
||||
//
|
||||
|
||||
test.describe('Filter Tests: Numerical', () => { |
||||
async function numBasedFilterTest(dataType, eqString, isLikeString) { |
||||
await dashboard.closeTab({ title: 'Team & Auth' }); |
||||
await dashboard.treeView.openTable({ title: 'numberBased' }); |
||||
|
||||
// Enable NULL & EMPTY filters
|
||||
await dashboard.gotoSettings(); |
||||
await dashboard.settings.toggleNullEmptyFilters(); |
||||
|
||||
let eqStringDerived = eqString; |
||||
let isLikeStringDerived = isLikeString; |
||||
if (dataType === 'Duration') { |
||||
// convert from hh:mm to seconds
|
||||
eqStringDerived = parseInt(eqString.split(':')[0]) * 3600 + parseInt(eqString.split(':')[1]) * 60; |
||||
isLikeStringDerived = parseInt(isLikeString.split(':')[0]) * 3600 + parseInt(isLikeString.split(':')[1]) * 60; |
||||
} |
||||
|
||||
const filterList = [ |
||||
{ |
||||
op: '=', |
||||
value: eqString, |
||||
rowCount: records.list.filter(r => parseFloat(r[dataType]) === parseFloat(eqStringDerived)).length, |
||||
}, |
||||
{ |
||||
op: '!=', |
||||
value: eqString, |
||||
rowCount: records.list.filter(r => parseFloat(r[dataType]) !== parseFloat(eqStringDerived)).length, |
||||
}, |
||||
{ |
||||
op: 'is null', |
||||
value: '', |
||||
rowCount: records.list.filter(r => r[dataType] === null).length, |
||||
}, |
||||
{ |
||||
op: 'is not null', |
||||
value: '', |
||||
rowCount: records.list.filter(r => r[dataType] !== null).length, |
||||
}, |
||||
{ |
||||
op: 'is blank', |
||||
value: '', |
||||
rowCount: records.list.filter(r => r[dataType] === null).length, |
||||
}, |
||||
{ |
||||
op: 'is not blank', |
||||
value: '', |
||||
rowCount: records.list.filter(r => r[dataType] !== null).length, |
||||
}, |
||||
{ |
||||
op: '>', |
||||
value: isLikeString, |
||||
rowCount: records.list.filter( |
||||
r => parseFloat(r[dataType]) > parseFloat(isLikeStringDerived) && r[dataType] != null |
||||
).length, |
||||
}, |
||||
{ |
||||
op: '>=', |
||||
value: isLikeString, |
||||
rowCount: records.list.filter( |
||||
r => parseFloat(r[dataType]) >= parseFloat(isLikeStringDerived) && r[dataType] != null |
||||
).length, |
||||
}, |
||||
{ |
||||
op: '<', |
||||
value: isLikeString, |
||||
rowCount: |
||||
dataType === 'Rating' |
||||
? records.list.filter( |
||||
r => parseFloat(r[dataType]) < parseFloat(isLikeStringDerived) || r[dataType] === null |
||||
).length |
||||
: records.list.filter(r => parseFloat(r[dataType]) < parseFloat(isLikeStringDerived) && r[dataType] != null) |
||||
.length, |
||||
}, |
||||
{ |
||||
op: '<=', |
||||
value: isLikeString, |
||||
rowCount: |
||||
dataType === 'Rating' |
||||
? records.list.filter( |
||||
r => parseFloat(r[dataType]) <= parseFloat(isLikeStringDerived) || r[dataType] === null |
||||
).length |
||||
: records.list.filter( |
||||
r => parseFloat(r[dataType]) <= parseFloat(isLikeStringDerived) && r[dataType] != null |
||||
).length, |
||||
}, |
||||
]; |
||||
|
||||
for (let i = 0; i < filterList.length; i++) { |
||||
await verifyFilter({ |
||||
column: dataType, |
||||
opType: filterList[i].op, |
||||
value: filterList[i].value, |
||||
result: { rowCount: filterList[i].rowCount }, |
||||
dataType: dataType, |
||||
}); |
||||
} |
||||
} |
||||
|
||||
test.beforeEach(async ({ page }) => { |
||||
context = await setup({ page }); |
||||
dashboard = new DashboardPage(page, context.project); |
||||
toolbar = dashboard.grid.toolbar; |
||||
|
||||
api = new Api({ |
||||
baseURL: `http://localhost:8080/`, |
||||
headers: { |
||||
'xc-auth': context.token, |
||||
}, |
||||
}); |
||||
|
||||
const columns = [ |
||||
{ |
||||
column_name: 'Id', |
||||
title: 'Id', |
||||
uidt: UITypes.ID, |
||||
}, |
||||
{ |
||||
column_name: 'Number', |
||||
title: 'Number', |
||||
uidt: UITypes.Number, |
||||
}, |
||||
{ |
||||
column_name: 'Decimal', |
||||
title: 'Decimal', |
||||
uidt: UITypes.Decimal, |
||||
}, |
||||
{ |
||||
column_name: 'Currency', |
||||
title: 'Currency', |
||||
uidt: UITypes.Currency, |
||||
}, |
||||
{ |
||||
column_name: 'Percent', |
||||
title: 'Percent', |
||||
uidt: UITypes.Percent, |
||||
}, |
||||
{ |
||||
column_name: 'Duration', |
||||
title: 'Duration', |
||||
uidt: UITypes.Duration, |
||||
}, |
||||
{ |
||||
column_name: 'Rating', |
||||
title: 'Rating', |
||||
uidt: UITypes.Rating, |
||||
}, |
||||
]; |
||||
|
||||
try { |
||||
const project = await api.project.read(context.project.id); |
||||
const table = await api.base.tableCreate(context.project.id, project.bases?.[0].id, { |
||||
table_name: 'numberBased', |
||||
title: 'numberBased', |
||||
columns: columns, |
||||
}); |
||||
|
||||
const rowAttributes = []; |
||||
for (let i = 0; i < 400; i++) { |
||||
const row = { |
||||
Number: rowMixedValue(columns[1], i), |
||||
Decimal: rowMixedValue(columns[2], i), |
||||
Currency: rowMixedValue(columns[3], i), |
||||
Percent: rowMixedValue(columns[4], i), |
||||
Duration: rowMixedValue(columns[5], i), |
||||
Rating: rowMixedValue(columns[6], i), |
||||
}; |
||||
rowAttributes.push(row); |
||||
} |
||||
|
||||
await api.dbTableRow.bulkCreate('noco', context.project.id, table.id, rowAttributes); |
||||
records = await api.dbTableRow.list('noco', context.project.id, table.id, { limit: 400 }); |
||||
} catch (e) { |
||||
console.error(e); |
||||
} |
||||
}); |
||||
|
||||
test('Filter: Number', async () => { |
||||
await numBasedFilterTest('Number', '33', '44'); |
||||
}); |
||||
|
||||
test('Filter: Decimal', async () => { |
||||
await numBasedFilterTest('Decimal', '33.3', '44.26'); |
||||
}); |
||||
|
||||
test('Filter: Percent', async () => { |
||||
await numBasedFilterTest('Percent', '33', '44'); |
||||
}); |
||||
|
||||
test('Filter: Currency', async () => { |
||||
await numBasedFilterTest('Currency', '33.3', '44.26'); |
||||
}); |
||||
|
||||
test('Filter: Rating', async () => { |
||||
await numBasedFilterTest('Rating', '3', '2'); |
||||
}); |
||||
|
||||
test('Filter: Duration', async () => { |
||||
await numBasedFilterTest('Duration', '00:01', '01:03'); |
||||
}); |
||||
}); |
||||
|
||||
// Text based filters
|
||||
//
|
||||
|
||||
test.describe('Filter Tests: Text based', () => { |
||||
async function textBasedFilterTest(dataType, eqString, isLikeString) { |
||||
await dashboard.closeTab({ title: 'Team & Auth' }); |
||||
await dashboard.treeView.openTable({ title: 'textBased' }); |
||||
|
||||
// Enable NULL & EMPTY filters
|
||||
await dashboard.gotoSettings(); |
||||
await dashboard.settings.toggleNullEmptyFilters(); |
||||
|
||||
const filterList = [ |
||||
{ |
||||
op: 'is equal', |
||||
value: eqString, |
||||
rowCount: records.list.filter(r => r[dataType] === eqString).length, |
||||
}, |
||||
{ |
||||
op: 'is not equal', |
||||
value: eqString, |
||||
rowCount: records.list.filter(r => r[dataType] !== eqString).length, |
||||
}, |
||||
{ |
||||
op: 'is null', |
||||
value: '', |
||||
rowCount: records.list.filter(r => r[dataType] === null).length, |
||||
}, |
||||
{ |
||||
op: 'is not null', |
||||
value: '', |
||||
rowCount: records.list.filter(r => r[dataType] !== null).length, |
||||
}, |
||||
{ |
||||
op: 'is empty', |
||||
value: '', |
||||
rowCount: records.list.filter(r => r[dataType] === '').length, |
||||
}, |
||||
{ |
||||
op: 'is not empty', |
||||
value: '', |
||||
rowCount: records.list.filter(r => r[dataType] !== '').length, |
||||
}, |
||||
{ |
||||
op: 'is blank', |
||||
value: '', |
||||
rowCount: records.list.filter(r => r[dataType] === '' || r[dataType] === null).length, |
||||
}, |
||||
{ |
||||
op: 'is not blank', |
||||
value: '', |
||||
rowCount: records.list.filter(r => r[dataType] !== '' && r[dataType] !== null).length, |
||||
}, |
||||
{ |
||||
op: 'is like', |
||||
value: isLikeString, |
||||
rowCount: records.list.filter(r => r[dataType]?.includes(isLikeString)).length, |
||||
}, |
||||
{ |
||||
op: 'is not like', |
||||
value: isLikeString, |
||||
rowCount: records.list.filter(r => !r[dataType]?.includes(isLikeString)).length, |
||||
}, |
||||
]; |
||||
|
||||
for (let i = 0; i < filterList.length; i++) { |
||||
await verifyFilter({ |
||||
column: dataType, |
||||
opType: filterList[i].op, |
||||
value: filterList[i].value, |
||||
result: { rowCount: filterList[i].rowCount }, |
||||
}); |
||||
} |
||||
} |
||||
|
||||
test.beforeEach(async ({ page }) => { |
||||
context = await setup({ page }); |
||||
dashboard = new DashboardPage(page, context.project); |
||||
toolbar = dashboard.grid.toolbar; |
||||
|
||||
api = new Api({ |
||||
baseURL: `http://localhost:8080/`, |
||||
headers: { |
||||
'xc-auth': context.token, |
||||
}, |
||||
}); |
||||
|
||||
const columns = [ |
||||
{ |
||||
column_name: 'Id', |
||||
title: 'Id', |
||||
uidt: UITypes.ID, |
||||
}, |
||||
{ |
||||
column_name: 'SingleLineText', |
||||
title: 'SingleLineText', |
||||
uidt: UITypes.SingleLineText, |
||||
}, |
||||
{ |
||||
column_name: 'MultiLineText', |
||||
title: 'MultiLineText', |
||||
uidt: UITypes.LongText, |
||||
}, |
||||
{ |
||||
column_name: 'Email', |
||||
title: 'Email', |
||||
uidt: UITypes.Email, |
||||
}, |
||||
{ |
||||
column_name: 'PhoneNumber', |
||||
title: 'PhoneNumber', |
||||
uidt: UITypes.PhoneNumber, |
||||
}, |
||||
{ |
||||
column_name: 'URL', |
||||
title: 'URL', |
||||
uidt: UITypes.URL, |
||||
}, |
||||
]; |
||||
|
||||
try { |
||||
const project = await api.project.read(context.project.id); |
||||
const table = await api.base.tableCreate(context.project.id, project.bases?.[0].id, { |
||||
table_name: 'textBased', |
||||
title: 'textBased', |
||||
columns: columns, |
||||
}); |
||||
|
||||
const rowAttributes = []; |
||||
for (let i = 0; i < 400; i++) { |
||||
const row = { |
||||
SingleLineText: rowMixedValue(columns[1], i), |
||||
MultiLineText: rowMixedValue(columns[2], i), |
||||
Email: rowMixedValue(columns[3], i), |
||||
PhoneNumber: rowMixedValue(columns[4], i), |
||||
URL: rowMixedValue(columns[5], i), |
||||
}; |
||||
rowAttributes.push(row); |
||||
} |
||||
|
||||
await api.dbTableRow.bulkCreate('noco', context.project.id, table.id, rowAttributes); |
||||
records = await api.dbTableRow.list('noco', context.project.id, table.id, { limit: 400 }); |
||||
} catch (e) { |
||||
console.error(e); |
||||
} |
||||
}); |
||||
|
||||
test('Filter: Single Line Text', async () => { |
||||
await textBasedFilterTest('SingleLineText', 'Afghanistan', 'Au'); |
||||
}); |
||||
|
||||
test('Filter: Long Text', async () => { |
||||
await textBasedFilterTest('MultiLineText', 'Aberdeen, United Kingdom', 'abad'); |
||||
}); |
||||
|
||||
test('Filter: Email', async () => { |
||||
await textBasedFilterTest('Email', 'leota@hotmail.com', 'cox.net'); |
||||
}); |
||||
|
||||
test('Filter: PhoneNumber', async () => { |
||||
await textBasedFilterTest('PhoneNumber', '504-621-8927', '504'); |
||||
}); |
||||
|
||||
test('Filter: URL', async () => { |
||||
await textBasedFilterTest('URL', 'https://www.youtube.com', 'e.com'); |
||||
}); |
||||
}); |
||||
|
||||
// Select Based
|
||||
//
|
||||
|
||||
test.describe('Filter Tests: Select based', () => { |
||||
async function selectBasedFilterTest(dataType, is, anyof, allof) { |
||||
await dashboard.closeTab({ title: 'Team & Auth' }); |
||||
await dashboard.treeView.openTable({ title: 'selectBased' }); |
||||
|
||||
// Enable NULL & EMPTY filters
|
||||
await dashboard.gotoSettings(); |
||||
await dashboard.settings.toggleNullEmptyFilters(); |
||||
|
||||
const filterList = [ |
||||
{ |
||||
op: 'is', |
||||
value: is, |
||||
rowCount: records.list.filter(r => r[dataType] === is).length, |
||||
}, |
||||
{ |
||||
op: 'is not', |
||||
value: is, |
||||
rowCount: records.list.filter(r => r[dataType] !== is).length, |
||||
}, |
||||
{ |
||||
op: 'contains any of', |
||||
value: anyof, |
||||
rowCount: records.list.filter(r => { |
||||
const values = anyof.split(','); |
||||
const recordValue = r[dataType]?.split(','); |
||||
return values.some(value => recordValue?.includes(value)); |
||||
}).length, |
||||
}, |
||||
{ |
||||
op: 'contains all of', |
||||
value: allof, |
||||
rowCount: records.list.filter(r => { |
||||
const values = allof.split(','); |
||||
return values.every(value => r[dataType]?.includes(value)); |
||||
}).length, |
||||
}, |
||||
{ |
||||
op: 'does not contain any of', |
||||
value: anyof, |
||||
rowCount: records.list.filter(r => { |
||||
const values = anyof.split(','); |
||||
const recordValue = r[dataType]?.split(','); |
||||
return !values.some(value => recordValue?.includes(value)); |
||||
}).length, |
||||
}, |
||||
{ |
||||
op: 'does not contain all of', |
||||
value: allof, |
||||
rowCount: records.list.filter(r => { |
||||
const values = allof.split(','); |
||||
return !values.every(value => r[dataType]?.includes(value)); |
||||
}).length, |
||||
}, |
||||
{ |
||||
op: 'is blank', |
||||
value: '', |
||||
rowCount: records.list.filter(r => r[dataType] === '' || r[dataType] === null).length, |
||||
}, |
||||
{ |
||||
op: 'is not blank', |
||||
value: '', |
||||
rowCount: records.list.filter(r => r[dataType] !== '' && r[dataType] !== null).length, |
||||
}, |
||||
]; |
||||
|
||||
for (let i = 0; i < filterList.length; i++) { |
||||
await verifyFilter({ |
||||
column: dataType, |
||||
opType: filterList[i].op, |
||||
value: filterList[i].value, |
||||
result: { rowCount: filterList[i].rowCount }, |
||||
dataType: dataType, |
||||
}); |
||||
} |
||||
} |
||||
test.beforeEach(async ({ page }) => { |
||||
context = await setup({ page }); |
||||
dashboard = new DashboardPage(page, context.project); |
||||
toolbar = dashboard.grid.toolbar; |
||||
|
||||
api = new Api({ |
||||
baseURL: `http://localhost:8080/`, |
||||
headers: { |
||||
'xc-auth': context.token, |
||||
}, |
||||
}); |
||||
|
||||
const columns = [ |
||||
{ |
||||
column_name: 'Id', |
||||
title: 'Id', |
||||
uidt: UITypes.ID, |
||||
}, |
||||
{ |
||||
column_name: 'SingleSelect', |
||||
title: 'SingleSelect', |
||||
uidt: UITypes.SingleSelect, |
||||
dtxp: "'jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec'", |
||||
}, |
||||
{ |
||||
column_name: 'MultiSelect', |
||||
title: 'MultiSelect', |
||||
uidt: UITypes.MultiSelect, |
||||
dtxp: "'jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec'", |
||||
}, |
||||
]; |
||||
|
||||
try { |
||||
const project = await api.project.read(context.project.id); |
||||
const table = await api.base.tableCreate(context.project.id, project.bases?.[0].id, { |
||||
table_name: 'selectBased', |
||||
title: 'selectBased', |
||||
columns: columns, |
||||
}); |
||||
|
||||
const rowAttributes = []; |
||||
for (let i = 0; i < 400; i++) { |
||||
const row = { |
||||
SingleSelect: rowMixedValue(columns[1], i), |
||||
MultiSelect: rowMixedValue(columns[2], i), |
||||
}; |
||||
rowAttributes.push(row); |
||||
} |
||||
|
||||
await api.dbTableRow.bulkCreate('noco', context.project.id, table.id, rowAttributes); |
||||
records = await api.dbTableRow.list('noco', context.project.id, table.id, { limit: 400 }); |
||||
} catch (e) { |
||||
console.error(e); |
||||
} |
||||
}); |
||||
|
||||
test('Filter: Single Select', async () => { |
||||
await selectBasedFilterTest('SingleSelect', 'jan', 'jan,feb,mar', ''); |
||||
}); |
||||
|
||||
test('Filter: Multi Select', async () => { |
||||
await selectBasedFilterTest('MultiSelect', '', 'jan,feb,mar', 'jan,feb,mar'); |
||||
}); |
||||
}); |
||||
|
||||
// Misc : Checkbox
|
||||
//
|
||||
|
||||
test.describe('Filter Tests: AddOn', () => { |
||||
async function addOnFilterTest(dataType) { |
||||
await dashboard.closeTab({ title: 'Team & Auth' }); |
||||
await dashboard.treeView.openTable({ title: 'addOnTypes', networkResponse: false }); |
||||
|
||||
// Enable NULL & EMPTY filters
|
||||
await dashboard.gotoSettings(); |
||||
await dashboard.settings.toggleNullEmptyFilters(); |
||||
|
||||
const filterList = [ |
||||
{ |
||||
op: 'is checked', |
||||
value: null, |
||||
rowCount: records.list.filter(r => { |
||||
return r[dataType] === (context.dbType === 'pg' ? true : 1); |
||||
}).length, |
||||
}, |
||||
{ |
||||
op: 'is not checked', |
||||
value: null, |
||||
rowCount: records.list.filter(r => { |
||||
return r[dataType] !== (context.dbType === 'pg' ? true : 1); |
||||
}).length, |
||||
}, |
||||
]; |
||||
|
||||
for (let i = 0; i < filterList.length; i++) { |
||||
await verifyFilter({ |
||||
column: dataType, |
||||
opType: filterList[i].op, |
||||
value: filterList[i].value, |
||||
result: { rowCount: filterList[i].rowCount }, |
||||
dataType: dataType, |
||||
}); |
||||
} |
||||
} |
||||
test.beforeEach(async ({ page }) => { |
||||
context = await setup({ page }); |
||||
dashboard = new DashboardPage(page, context.project); |
||||
toolbar = dashboard.grid.toolbar; |
||||
|
||||
api = new Api({ |
||||
baseURL: `http://localhost:8080/`, |
||||
headers: { |
||||
'xc-auth': context.token, |
||||
}, |
||||
}); |
||||
|
||||
const columns = [ |
||||
{ |
||||
column_name: 'Id', |
||||
title: 'Id', |
||||
uidt: UITypes.ID, |
||||
}, |
||||
{ |
||||
column_name: 'SingleLineText', |
||||
title: 'SingleLineText', |
||||
uidt: UITypes.SingleLineText, |
||||
}, |
||||
{ |
||||
column_name: 'Checkbox', |
||||
title: 'Checkbox', |
||||
uidt: UITypes.Checkbox, |
||||
}, |
||||
]; |
||||
|
||||
try { |
||||
const project = await api.project.read(context.project.id); |
||||
const table = await api.base.tableCreate(context.project.id, project.bases?.[0].id, { |
||||
table_name: 'addOnTypes', |
||||
title: 'addOnTypes', |
||||
columns: columns, |
||||
}); |
||||
|
||||
const rowAttributes = []; |
||||
for (let i = 0; i < 400; i++) { |
||||
const row = { |
||||
SingleLineText: rowMixedValue(columns[1], i), |
||||
Checkbox: rowMixedValue(columns[2], i), |
||||
}; |
||||
rowAttributes.push(row); |
||||
} |
||||
|
||||
await api.dbTableRow.bulkCreate('noco', context.project.id, table.id, rowAttributes); |
||||
records = await api.dbTableRow.list('noco', context.project.id, table.id, { limit: 400 }); |
||||
} catch (e) { |
||||
console.error(e); |
||||
} |
||||
}); |
||||
|
||||
test('Filter: Checkbox', async () => { |
||||
await addOnFilterTest('Checkbox'); |
||||
}); |
||||
}); |
||||
|
||||
// Rest of tests
|
||||
//
|
||||
|
||||
test.describe('Filter Tests: Toggle button', () => { |
||||
/** |
||||
* Steps |
||||
* |
||||
* 1. Open table |
||||
* 2. Verify filter options : should not include NULL & EMPTY options |
||||
* 3. Enable `Show NULL & EMPTY in Filter` in Project Settings |
||||
* 4. Verify filter options : should include NULL & EMPTY options |
||||
* 5. Add NULL & EMPTY filters |
||||
* 6. Disable `Show NULL & EMPTY in Filter` in Project Settings : should not be allowed |
||||
* 7. Remove the NULL & EMPTY filters |
||||
* 8. Disable `Show NULL & EMPTY in Filter` in Project Settings again : should be allowed |
||||
* |
||||
*/ |
||||
|
||||
test.beforeEach(async ({ page }) => { |
||||
context = await setup({ page }); |
||||
dashboard = new DashboardPage(page, context.project); |
||||
toolbar = dashboard.grid.toolbar; |
||||
}); |
||||
|
||||
test('Filter: Toggle NULL & EMPTY button', async () => { |
||||
await dashboard.closeTab({ title: 'Team & Auth' }); |
||||
await dashboard.treeView.openTable({ title: 'Country', networkResponse: false }); |
||||
|
||||
// Verify filter options
|
||||
await verifyFilterOperatorList({ |
||||
column: 'Country', |
||||
opType: ['is equal', 'is not equal', 'is like', 'is not like', 'is blank', 'is not blank'], |
||||
}); |
||||
|
||||
// Enable NULL & EMPTY button
|
||||
await dashboard.gotoSettings(); |
||||
await dashboard.settings.toggleNullEmptyFilters(); |
||||
|
||||
// Verify filter options
|
||||
await verifyFilterOperatorList({ |
||||
column: 'Country', |
||||
opType: [ |
||||
'is equal', |
||||
'is not equal', |
||||
'is like', |
||||
'is not like', |
||||
'is empty', |
||||
'is not empty', |
||||
'is null', |
||||
'is not null', |
||||
'is blank', |
||||
'is not blank', |
||||
], |
||||
}); |
||||
|
||||
await toolbar.clickFilter({ networkValidation: false }); |
||||
await toolbar.filter.add({ |
||||
columnTitle: 'Country', |
||||
opType: 'is null', |
||||
value: null, |
||||
isLocallySaved: false, |
||||
dataType: 'SingleLineText', |
||||
}); |
||||
await toolbar.clickFilter({ networkValidation: false }); |
||||
|
||||
// Disable NULL & EMPTY button
|
||||
await dashboard.gotoSettings(); |
||||
await dashboard.settings.toggleNullEmptyFilters(); |
||||
// wait for toast message
|
||||
await dashboard.verifyToast({ message: 'Null / Empty filters exist. Please remove them first.' }); |
||||
|
||||
// remove filter
|
||||
await toolbar.filter.reset({ networkValidation: false }); |
||||
|
||||
// Disable NULL & EMPTY button
|
||||
await dashboard.gotoSettings(); |
||||
await dashboard.settings.toggleNullEmptyFilters(); |
||||
}); |
||||
}); |
Loading…
Reference in new issue