diff --git a/app/gui/src/project-view/components/visualizations/TableVisualization.vue b/app/gui/src/project-view/components/visualizations/TableVisualization.vue index 4e337e32ec61..280ca5c1fa2d 100644 --- a/app/gui/src/project-view/components/visualizations/TableVisualization.vue +++ b/app/gui/src/project-view/components/visualizations/TableVisualization.vue @@ -2,6 +2,7 @@ import icons from '@/assets/icons.svg' import AgGridTableView, { commonContextMenuActions } from '@/components/shared/AgGridTableView.vue' import { + GridFilterModel, useTableVizToolbar, type SortModel, } from '@/components/visualizations/TableVisualization/tableVizToolbar' @@ -123,7 +124,7 @@ const rowCount = ref(0) const showRowCount = ref(true) const isTruncated = ref(false) const isCreateNodeEnabled = ref(false) -const filterModel = ref({}) +const filterModel = ref([]) const sortModel = ref([]) const dataGroupingMap = shallowRef>() const defaultColDef: Ref = ref({ @@ -323,6 +324,16 @@ function getValueTypeIcon(valueType: string) { } } +function getFilterType(valueType: string) { + if (valueType === 'Date') { + return 'agDateColumnFilter' + } else if (isNumericType(valueType)) { + return 'agNumberColumnFilter' + } else { + return 'agSetColumnFilter' + } +} + /** * Generates the column definition for the table vizulization, including displaying the data value type and * data quality indicators. @@ -341,6 +352,7 @@ function toField( const displayValue = valueType ? valueType.display_text : null const icon = valueType ? getValueTypeIcon(valueType.constructor) : null + const filterType = valueType ? getFilterType(valueType.constructor) : null const dataQualityMetrics = typeof props.data === 'object' && 'data_quality_metrics' in props.data ? @@ -374,6 +386,10 @@ function toField( return { field: name, headerName: name, // AGGrid would demangle it its own way if not specified. + filter: filterType, + filterParams: { + maxNumConditions: 1, + }, headerComponentParams: { template, setAriaSort: () => {}, @@ -698,6 +714,7 @@ const createDateValue = (item: string, module: Ast.MutableModule) => { const dateOrTimePattern = Pattern.parseExpression('(Date.new __ __ __)') const dateTimeParts = item .match(/\d+/g)! + .filter((part, i) => i < 3) .map((part) => Ast.tryNumberToEnso(Number(part), module)!) return dateOrTimePattern.instantiateCopied([...dateTimeParts]) } @@ -710,7 +727,7 @@ function checkSortAndFilter(e: SortChangedEvent) { return } const colState = gridApi.getColumnState() - const filter = gridApi.getFilterModel() + const gridFilterModel = gridApi.getFilterModel() const sort = colState .map((cs) => { if (cs.sort) { @@ -722,14 +739,26 @@ function checkSortAndFilter(e: SortChangedEvent) { } }) .filter((sort) => sort) - if (sort.length || Object.keys(filter).length) { + const filter = Object.entries(gridFilterModel).map(([key, value]) => { + return { + columnName: key, + filterType: value.filterType, + filterAction: value.type, + filter: value.filter, + filterTo: value.filterTo, + dateFrom: value.dateFrom, + dateTo: value.dateTo, + values: value.values, + } + }) + if (sort.length || filter.length) { isCreateNodeEnabled.value = true sortModel.value = sort as SortModel[] filterModel.value = filter } else { isCreateNodeEnabled.value = false sortModel.value = [] - filterModel.value = {} + filterModel.value = [] } } diff --git a/app/gui/src/project-view/components/visualizations/TableVisualization/tableVizToolbar.ts b/app/gui/src/project-view/components/visualizations/TableVisualization/tableVizToolbar.ts index edf7366900dd..79a8d7328d80 100644 --- a/app/gui/src/project-view/components/visualizations/TableVisualization/tableVizToolbar.ts +++ b/app/gui/src/project-view/components/visualizations/TableVisualization/tableVizToolbar.ts @@ -13,14 +13,46 @@ export type SortModel = { sortDirection: SortDirection sortIndex: number } +type FilterType = 'number' | 'date' | 'set' + +/** + * Represents the value used for filtering. + * + * - For comparisons such as 'equals' or 'greater than,' the filter value is a single value (string). + * - For 'is in' filtering, the filter value is a list of strings. + * - For range filtering, the filter value consists of two values that define the range. + */ +type FilterValue = string | string[] | FilterValueRange + +const actionMap = { + equals: '..Equal', + notEqual: '..Not_Equal', + greaterThan: '..Greater', + greaterThanOrEqual: '..Equal_Or_Greater', + lessThan: '..Less', + lessThanOrEqual: '..Equal_Or_Less', + inRange: '..Between', + blank: '..Is_Nothing', + notBlank: '..Not_Nothing', +} +type FilterAction = keyof typeof actionMap +export type GridFilterModel = { + columnName: string + filterType: FilterType + filter?: string + filterTo?: string + dateFrom?: string + dateTo?: string + values?: string[] + filterAction?: FilterAction +} +type FilterValueRange = { + toValue: string + fromValue: string +} export interface SortFilterNodesButtonOptions { - filterModel: ToValue<{ - [key: string]: { - values: any[] - filterType: string - } - }> + filterModel: ToValue sortModel: ToValue isDisabled: ToValue isFilterSortNodeEnabled: ToValue @@ -65,6 +97,8 @@ function useSortFilterNodesButton({ } const filterPattern = computed(() => Pattern.parseExpression('__ (__ __)')!) + const filterBetweenPattern = computed(() => Pattern.parseExpression('__ (..Between __ __)')!) + const filterNothingPattern = computed(() => Pattern.parseExpression('__ __')!) function makeFilterPattern(module: Ast.MutableModule, columnName: string, items: string[]) { if ( @@ -97,6 +131,39 @@ function useSortFilterNodesButton({ ]) } + function makeNumericFilterPattern( + module: Ast.MutableModule, + columnName: string, + item: string | FilterValueRange, + filterAction: FilterAction, + ) { + const valueFormatter = getColumnValueToEnso(columnName) + if (filterAction === 'inRange' && typeof item === 'object') { + const filterToValue = valueFormatter(item.toValue, module) + const filterFromValue = valueFormatter(item.fromValue, module) + return filterBetweenPattern.value.instantiateCopied([ + Ast.TextLiteral.new(columnName), + filterFromValue as Expression | MutableExpression, + filterToValue as Expression | MutableExpression, + ]) + } + const filterValue = valueFormatter(item as string, module) + const action = actionMap[filterAction] + return filterPattern.value.instantiateCopied([ + Ast.TextLiteral.new(columnName), + Ast.parseExpression(action)!, + filterValue as Expression | MutableExpression, + ]) + } + + function makeNothingFilterPattern(columnName: string, filterAction: FilterAction) { + const action = actionMap[filterAction] + return filterNothingPattern.value.instantiateCopied([ + Ast.TextLiteral.new(columnName), + Ast.parseExpression(action)!, + ]) + } + function getAstPatternSort() { return Pattern.new((ast) => Ast.App.positional( @@ -106,22 +173,72 @@ function useSortFilterNodesButton({ ) } - function getAstPatternFilter(columnName: string, items: string[]) { + function getAstPatternFilter( + columnName: string, + items: string[] | string | FilterValue, + filterType: FilterType, + filterAction?: FilterAction, + ) { return Pattern.new((ast) => Ast.App.positional( Ast.PropertyAccess.new(ast.module, ast, Ast.identifier('filter')!), - makeFilterPattern(ast.module, columnName, items), + filterType === 'set' ? + makeFilterPattern(ast.module, columnName, items as string[]) + : makeNumericFilterPattern( + ast.module, + columnName, + items as string | FilterValueRange, + filterAction!, + ), ), ) } - function getAstPatternFilterAndSort(columnName: string, items: string[]) { + function getAstNothingPatternFilter(columnName: string, filterAction: FilterAction) { + return Pattern.new((ast) => + Ast.App.positional( + Ast.PropertyAccess.new(ast.module, ast, Ast.identifier('filter')!), + makeNothingFilterPattern(columnName, filterAction), + ), + ) + } + + function getAstPatternFilterAndSort( + columnName: string, + items: string[] | string | FilterValueRange, + filterType: FilterType, + filterAction?: FilterAction, + ) { return Pattern.new((ast) => Ast.OprApp.new( ast.module, Ast.App.positional( Ast.PropertyAccess.new(ast.module, ast, Ast.identifier('filter')!), - makeFilterPattern(ast.module, columnName, items), + filterType === 'set' ? + makeFilterPattern(ast.module, columnName, items as string[]) + : makeNumericFilterPattern( + ast.module, + columnName, + items as string | FilterValueRange, + filterAction!, + ), + ), + '.', + Ast.App.positional( + Ast.Ident.new(ast.module, Ast.identifier('sort')!), + makeSortPattern(ast.module), + ), + ), + ) + } + + function getAstNothingPatternFilterAndSort(columnName: string, filterAction: FilterAction) { + return Pattern.new((ast) => + Ast.OprApp.new( + ast.module, + Ast.App.positional( + Ast.PropertyAccess.new(ast.module, ast, Ast.identifier('filter')!), + makeNothingFilterPattern(columnName, filterAction), ), '.', Ast.App.positional( @@ -136,15 +253,44 @@ function useSortFilterNodesButton({ const patterns = new Array() const filterModelValue = toValue(filterModel) const sortModelValue = toValue(sortModel) - if (Object.keys(filterModelValue).length) { - for (const [columnName, columnFilter] of Object.entries(filterModelValue)) { - const items = columnFilter.values - const filterPatterns = - sortModelValue.length ? - getAstPatternFilterAndSort(columnName, items) - : getAstPatternFilter(columnName, items) - patterns.push(filterPatterns) - } + if (filterModelValue.length) { + filterModelValue.map((filterModel: GridFilterModel) => { + const columnName = filterModel.columnName + const filterAction = filterModel.filterAction + const filterType = filterModel.filterType + if (filterAction === 'blank' || filterAction === 'notBlank') { + const filterPatterns = + sortModelValue.length ? + getAstNothingPatternFilterAndSort(columnName, filterAction) + : getAstNothingPatternFilter(columnName, filterAction) + patterns.push(filterPatterns) + } + + let value: FilterValue + switch (filterType) { + case 'number': + value = + filterAction === 'inRange' ? + { toValue: filterModel.filterTo!, fromValue: filterModel.filter! } + : (filterModel.filter as FilterValue) + break + case 'date': + value = + filterAction === 'inRange' ? + { toValue: filterModel.dateTo!, fromValue: filterModel.dateFrom! } + : (filterModel.dateFrom as FilterValue) + break + default: + value = filterModel.values as FilterValue + } + if (value) { + const filterPatterns = + sortModelValue.length ? + getAstPatternFilterAndSort(columnName, value, filterType, filterAction) + : getAstPatternFilter(columnName, value, filterType, filterAction) + patterns.push(filterPatterns) + } + }) } else if (sortModelValue.length) { patterns.push(getAstPatternSort()) }