From 59a17971fb3a9644335f446c5fadf5d97f62e95e Mon Sep 17 00:00:00 2001 From: Andrei Zhaleznichenka Date: Fri, 2 Dec 2022 16:24:35 +0100 Subject: [PATCH] feat: Allows labelling property filter options (#497) --- pages/property-filter/common-props.tsx | 15 +- ...plit-panel-app-layout-integration.page.tsx | 11 +- pages/property-filter/table.data.ts | 130 +++++++------ .../__snapshots__/documenter.test.ts.snap | 15 ++ .../__tests__/property-filter.test.tsx | 177 ++++++++++++++++-- src/property-filter/controller.ts | 22 +-- src/property-filter/filter-options.ts | 7 +- src/property-filter/index.tsx | 14 +- src/property-filter/interfaces.ts | 15 ++ src/property-filter/token-editor.tsx | 10 +- src/property-filter/utils.ts | 16 +- 11 files changed, 324 insertions(+), 108 deletions(-) diff --git a/pages/property-filter/common-props.tsx b/pages/property-filter/common-props.tsx index 7b7a0afe7a..8db350af5b 100644 --- a/pages/property-filter/common-props.tsx +++ b/pages/property-filter/common-props.tsx @@ -1,9 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { PropertyFilterProps } from '~components/property-filter'; -import { TableItem } from './table.data'; +import { states, TableItem } from './table.data'; import { DateForm, DateTimeForm, formatDateTime, YesNoForm, yesNoFormat } from './custom-forms'; +const getStateLabel = (value: TableItem['state']) => (value !== undefined && states[value]) ?? 'Unknown'; + export const columnDefinitions = [ { id: 'instanceid', @@ -17,9 +19,10 @@ export const columnDefinitions = [ id: 'state', sortingField: 'state', header: 'State', - type: 'text', + type: 'enum', + getLabel: getStateLabel, propertyLabel: 'State', - cell: (item: TableItem) => item.state, + cell: (item: TableItem) => getStateLabel(item.state), }, { id: 'stopped', @@ -27,7 +30,7 @@ export const columnDefinitions = [ header: 'Stopped', type: 'boolean', propertyLabel: 'Stopped', - cell: (item: TableItem) => item.state === 'Stopped', + cell: (item: TableItem) => item.state === 0, }, { id: 'instancetype', @@ -161,6 +164,10 @@ export const filteringProperties: readonly PropertyFilterProps.FilteringProperty let defaultOperator: PropertyFilterProps.ComparisonOperator = '='; let groupValuesLabel = `${def.propertyLabel} values`; + if (def.type === 'enum') { + operators = ['=', '!='].map(operator => ({ operator, format: def.getLabel })); + } + if (def.type === 'text') { operators = ['=', '!=', ':', '!:']; } diff --git a/pages/property-filter/split-panel-app-layout-integration.page.tsx b/pages/property-filter/split-panel-app-layout-integration.page.tsx index 9b7ab63686..ded3101fa8 100644 --- a/pages/property-filter/split-panel-app-layout-integration.page.tsx +++ b/pages/property-filter/split-panel-app-layout-integration.page.tsx @@ -12,7 +12,7 @@ import ScreenshotArea from '../utils/screenshot-area'; import { Navigation, Tools, Breadcrumbs } from '../app-layout/utils/content-blocks'; import * as toolsContent from '../app-layout/utils/tools-content'; import labels from '../app-layout/utils/labels'; -import { allItems, TableItem } from './table.data'; +import { allItems, states, TableItem } from './table.data'; import { columnDefinitions, i18nStrings, filteringProperties } from './common-props'; import { useCollection } from '@cloudscape-design/collection-hooks'; @@ -40,6 +40,14 @@ export default function () { }, sorting: {}, }); + + const filteringOptions = propertyFilterProps.filteringOptions.map(option => { + if (option.propertyKey === 'state') { + option.label = states[parseInt(option.value)]; + } + return option; + }); + return ( = { + 0: 'Stopped', + 1: 'Stopping', + 2: 'Pending', +}; + export const allItems: TableItem[] = [ { instanceid: 'i-2dc5ce28a0328391', - state: 'Stopped', + state: 0, instancetype: 't3.small', averagelatency: 771, availablestorage: 8.9, @@ -35,7 +41,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-d0312e022392efa0', - state: 'Stopped', + state: 0, instancetype: 't2.small', averagelatency: 216, availablestorage: 7.99, @@ -49,7 +55,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-070eef935c1301e6', - state: 'Stopped', + state: 0, instancetype: 't3.nano', averagelatency: 352, availablestorage: 8.2, @@ -63,7 +69,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-0eeaae622e074e21', - state: 'Stopped', + state: 0, instancetype: 't2.medium', averagelatency: 895, availablestorage: 4.23, @@ -77,7 +83,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-799860926d39b2a3', - state: 'Stopped', + state: 0, instancetype: 't2.medium', averagelatency: 600, availablestorage: 1.71, @@ -91,7 +97,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-f64935677691848b', - state: 'Stopped', + state: 0, instancetype: 't3.medium', averagelatency: 461, availablestorage: 2.71, @@ -105,7 +111,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-d5b5e73917af69a8', - state: 'Stopped', + state: 0, instancetype: 't2.large', averagelatency: 153, availablestorage: 9.56, @@ -119,7 +125,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-2a5bdc6c48fa8e5c', - state: 'Stopped', + state: 0, instancetype: 't2.nano', averagelatency: 107, availablestorage: 3.54, @@ -133,7 +139,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-393c4d4a25ca3dba', - state: 'Stopped', + state: 0, instancetype: 't2.micro', averagelatency: 53, availablestorage: 3.27, @@ -147,7 +153,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-c28fbdbb073ec4de', - state: 'Stopped', + state: 0, instancetype: 't2.medium', averagelatency: 724, availablestorage: 5.2, @@ -161,7 +167,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-e876fb65dc771c53', - state: 'Stopped', + state: 0, instancetype: 't2.large', averagelatency: 981, availablestorage: 6.86, @@ -175,7 +181,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-d92d0e29fa3a0eda', - state: 'Stopped', + state: 0, instancetype: 't3.small', averagelatency: 303, availablestorage: 6.07, @@ -189,7 +195,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-7911f4562405cb04', - state: 'Stopped', + state: 0, instancetype: 't3.small', averagelatency: 718, availablestorage: 2.9, @@ -203,7 +209,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-d3e9e4c068d4df6a', - state: 'Stopped', + state: 0, instancetype: 't3.large', averagelatency: 44, availablestorage: 6.1, @@ -217,7 +223,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-9966b9f79fc2afac', - state: 'Stopped', + state: 0, instancetype: 't3.nano', averagelatency: 652, availablestorage: 0.31, @@ -231,7 +237,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-f202265d52b3d9b6', - state: 'Stopping', + state: 1, instancetype: 't2.micro', averagelatency: 743, availablestorage: 3.69, @@ -245,7 +251,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-a6b569bc16be3756', - state: 'Stopped', + state: 0, instancetype: 't2.micro', averagelatency: 304, availablestorage: 6.93, @@ -259,7 +265,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-f8e3d9fffd82bd62', - state: 'Stopping', + state: 1, instancetype: 't2.large', averagelatency: 339, availablestorage: 0.68, @@ -273,7 +279,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-d1f8d3023c360cb4', - state: 'Stopping', + state: 1, instancetype: 't2.large', averagelatency: 945, availablestorage: 7.26, @@ -287,7 +293,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-64ed85f898d0a950', - state: 'Stopped', + state: 0, instancetype: 't2.large', averagelatency: 402, availablestorage: 4.17, @@ -301,7 +307,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-6afd6b0ebae03bd6', - state: 'Stopped', + state: 0, instancetype: 't2.small', averagelatency: 845, availablestorage: 9.73, @@ -315,7 +321,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-068993f1f76b09e1', - state: 'Stopped', + state: 0, instancetype: 't2.large', averagelatency: 184, availablestorage: 5.2, @@ -329,7 +335,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-1cd4a64fc26a8fe9', - state: 'Stopping', + state: 1, instancetype: 't3.nano', averagelatency: 995, availablestorage: 0.24, @@ -343,7 +349,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-835d004c46769aed', - state: 'Stopped', + state: 0, instancetype: 't3.small', averagelatency: 800, availablestorage: 4.03, @@ -357,7 +363,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-5441bf2e91565e24', - state: 'Stopped', + state: 0, instancetype: 't3.medium', averagelatency: 283, availablestorage: 7.38, @@ -371,7 +377,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-a57d0a6a6d20d73b', - state: 'Stopping', + state: 1, instancetype: 't2.small', averagelatency: 705, availablestorage: 6.78, @@ -385,7 +391,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-0b6646fde4598f8d', - state: 'Stopped', + state: 0, instancetype: 't2.nano', averagelatency: 375, availablestorage: 3.41, @@ -399,7 +405,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-5313687f75346895', - state: 'Stopped', + state: 0, instancetype: 't2.nano', averagelatency: 219, availablestorage: 1.97, @@ -413,7 +419,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-7173f776b4b30c05', - state: 'Stopped', + state: 0, instancetype: 't3.large', averagelatency: 17, availablestorage: 8.63, @@ -427,7 +433,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-fcfc136cb96b30c4', - state: 'Stopped', + state: 0, instancetype: 't2.nano', averagelatency: 835, availablestorage: 7.17, @@ -441,7 +447,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-8c6a328351a438ff', - state: 'Stopped', + state: 0, instancetype: 't2.nano', averagelatency: 73, availablestorage: 6.02, @@ -455,7 +461,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-cd323ed6664c7dc5', - state: 'Stopped', + state: 0, instancetype: 't2.large', averagelatency: 875, availablestorage: 7.42, @@ -469,7 +475,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-a7251b1805f9df3d', - state: 'Stopped', + state: 0, instancetype: 't2.nano', averagelatency: 342, availablestorage: 9.64, @@ -483,7 +489,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-9f11e46b6c54a2a1', - state: 'Stopped', + state: 0, instancetype: 't3.nano', averagelatency: 792, availablestorage: 6.99, @@ -497,7 +503,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-6f733cc0de79c2cc', - state: 'Stopped', + state: 0, instancetype: 't2.large', averagelatency: 782, availablestorage: 7.1, @@ -511,7 +517,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-b994cfe3823d78b4', - state: 'Stopped', + state: 0, instancetype: 't2.small', averagelatency: 242, availablestorage: 2.71, @@ -525,7 +531,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-82b8f4ef5d25d17c', - state: 'Pending', + state: 2, instancetype: 't2.large', averagelatency: 497, availablestorage: 2.03, @@ -539,7 +545,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-1d9468c0fdc6d337', - state: 'Stopped', + state: 0, instancetype: 't3.large', averagelatency: 219, availablestorage: 2.34, @@ -553,7 +559,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-1d56f94cf858be59', - state: 'Stopped', + state: 0, instancetype: 't3.micro', averagelatency: 45, availablestorage: 0.57, @@ -567,7 +573,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-59129472a88790c4', - state: 'Stopped', + state: 0, instancetype: 't3.nano', averagelatency: 154, availablestorage: 3.45, @@ -581,7 +587,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-b761d2801b356d89', - state: 'Stopping', + state: 1, instancetype: 't2.medium', averagelatency: 885, availablestorage: 4.06, @@ -595,7 +601,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-9b50eea20c1d2813', - state: 'Stopped', + state: 0, instancetype: 't3.large', averagelatency: 458, availablestorage: 3.06, @@ -609,7 +615,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-a19a9633e1c5ba8f', - state: 'Stopping', + state: 1, instancetype: 't2.nano', averagelatency: 30, availablestorage: 4.64, @@ -623,7 +629,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-85c31338cf96a6b9', - state: 'Stopped', + state: 0, instancetype: 't3.medium', averagelatency: 420, availablestorage: 4.66, @@ -637,7 +643,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-2a63773bd3bbc950', - state: 'Stopping', + state: 1, instancetype: 't2.micro', averagelatency: 685, availablestorage: 9.49, @@ -651,7 +657,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-c71adc85f62dc441', - state: 'Stopped', + state: 0, instancetype: 't2.medium', averagelatency: 639, availablestorage: 9.37, @@ -665,7 +671,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-7d78145659f6edf8', - state: 'Stopped', + state: 0, instancetype: 't2.micro', averagelatency: 141, availablestorage: 3.37, @@ -679,7 +685,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-d64836cefed723f9', - state: 'Stopped', + state: 0, instancetype: 't2.micro', averagelatency: 259, availablestorage: 2.31, @@ -693,7 +699,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-0854aa3ca406d6de', - state: 'Stopped', + state: 0, instancetype: 't3.large', averagelatency: 672, availablestorage: 2.09, @@ -707,7 +713,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-d50d96196e1da02f', - state: 'Stopped', + state: 0, instancetype: 't3.medium', averagelatency: 636, availablestorage: 7.76, @@ -721,7 +727,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-bf46a8fa4f894969', - state: 'Stopping', + state: 1, instancetype: 't2.medium', averagelatency: 236, availablestorage: 9.14, @@ -735,7 +741,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-a3358a2493af65da', - state: 'Stopped', + state: 0, instancetype: 't2.micro', averagelatency: 478, availablestorage: 8.13, @@ -749,7 +755,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-1ca6c551cd98b0bc', - state: 'Stopped', + state: 0, instancetype: 't3.small', averagelatency: 29, availablestorage: 3.5, @@ -763,7 +769,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-78363f3b35fe1638', - state: 'Stopping', + state: 1, instancetype: 't2.nano', averagelatency: 74, availablestorage: 6.37, @@ -777,7 +783,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-36cb4d638866ef4b', - state: 'Stopped', + state: 0, instancetype: 't2.nano', averagelatency: 819, availablestorage: 3.16, @@ -791,7 +797,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-dde47b18e8183070', - state: 'Stopped', + state: 0, instancetype: 't2.large', averagelatency: 305, availablestorage: 8.34, @@ -805,7 +811,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-86f3902256c13e75', - state: 'Stopped', + state: 0, instancetype: 't2.large', averagelatency: 521, availablestorage: 5.05, @@ -819,7 +825,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-64329dd06110114b', - state: 'Stopped', + state: 0, instancetype: 't2.nano', averagelatency: 478, availablestorage: 7.33, @@ -833,7 +839,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-36b91360e881739d', - state: 'Stopped', + state: 0, instancetype: 't3.micro', averagelatency: 733, availablestorage: 3.26, @@ -847,7 +853,7 @@ export const allItems: TableItem[] = [ }, { instanceid: 'i-3b44795b1fea36ac', - state: 'Stopped', + state: 0, instancetype: 't3.large', averagelatency: 636, availablestorage: 3.57, @@ -862,7 +868,7 @@ export const allItems: TableItem[] = [ ].map((item, indx) => ({ order: indx, ...item, - stopped: item.state === 'Stopped', + stopped: item.state === 0, releasedate: new Date(new Date(item.launchdate).getTime() - getRandomTimeHours(10, 2000)), launchdate: new Date(item.launchdate), lasteventat: new Date(item.lasteventat), diff --git a/src/__tests__/__snapshots__/documenter.test.ts.snap b/src/__tests__/__snapshots__/documenter.test.ts.snap index 91994b1539..f8a138ec2f 100644 --- a/src/__tests__/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/__snapshots__/documenter.test.ts.snap @@ -8827,6 +8827,21 @@ in a slight, visible lag when scrolling complex pages.", "description": "An array of possible values of the individual \`filteringProperties\`. Each element has the following properties: * \`propertyKey\` [string]: The key of the corresponding filtering property in the \`filteringProperties\` array. * \`value\` [string]: The value that will be used as a suggestion when creating or modifying a filtering token. +* \`label\` [string]: Optional suggestion label to be matched instead of the value. + +Filtering options that require labels can only use \`=\` and \`!=\` operators. The token value must be labelled separately, for example: +\`\`\` +const filteringProperty = { + key: 'state', + propertyLabel: 'State', + operators: ['=', '!='].map(operator => ({ operator, format: getStateLabel })) +} +const filteringOptions = [ + { propertyKey: 'state', value: 'STOPPED', label: getStateLabel('STOPPED') }, + { propertyKey: 'state', value: 'STOPPING', label: getStateLabel('STOPPING') }, + { propertyKey: 'state', value: 'RUNNING', label: getStateLabel('RUNNING') }, +] +\`\`\` ", "name": "filteringOptions", "optional": true, diff --git a/src/property-filter/__tests__/property-filter.test.tsx b/src/property-filter/__tests__/property-filter.test.tsx index 74105e9d3e..cbbf490cc4 100644 --- a/src/property-filter/__tests__/property-filter.test.tsx +++ b/src/property-filter/__tests__/property-filter.test.tsx @@ -11,13 +11,24 @@ import createWrapper, { PopoverWrapper, } from '../../../lib/components/test-utils/dom'; import PropertyFilter from '../../../lib/components/property-filter'; -import { FilteringProperty } from '../../../lib/components/property-filter/interfaces'; import styles from '../../../lib/components/property-filter/styles.selectors.js'; import { KeyCode } from '@cloudscape-design/test-utils-core/dist/utils'; -import { FilteringOption, PropertyFilterProps, Ref } from '../../../lib/components/property-filter/interfaces'; +import { + FilteringProperty, + FilteringOption, + PropertyFilterProps, + Ref, +} from '../../../lib/components/property-filter/interfaces'; -const filteringProperties = [ +const states: Record = { + 0: 'Stopped', + 1: 'Stopping', + 2: 'Running', +}; +const getStateLabel = (value: string) => (value !== undefined ? states[value] : 'Unknown'); + +const filteringProperties: readonly FilteringProperty[] = [ { key: 'string', propertyLabel: 'string', @@ -52,15 +63,13 @@ const filteringProperties = [ operators: ['!:', ':', '=', '!='], groupValuesLabel: 'Edge case values', }, -] as const; - -function openEditor(wrapper: PropertyFilterWrapper, index = 0) { - const tokenWrapper = createWrapper(wrapper.findTokens()![index].getElement()); - const popoverWrapper = tokenWrapper.findPopover()!; - act(() => popoverWrapper.findTrigger().click()); - const contentWrapper = popoverWrapper.findContent()!; - return [contentWrapper, popoverWrapper] as const; -} + { + key: 'state', + propertyLabel: 'state', + operators: [{ operator: '=', format: getStateLabel }], + groupValuesLabel: 'State values', + }, +]; const filteringOptions: readonly FilteringOption[] = [ { propertyKey: 'string', value: 'value1' }, @@ -71,7 +80,10 @@ const filteringOptions: readonly FilteringOption[] = [ { propertyKey: 'other-string', value: 'value2' }, { propertyKey: 'missing-property', value: 'value' }, { propertyKey: 'default-operator', value: 'value' }, -] as const; + { propertyKey: 'state', value: '0', label: getStateLabel('0') }, + { propertyKey: 'state', value: '1', label: getStateLabel('1') }, + { propertyKey: 'state', value: '2', label: getStateLabel('2') }, +]; const defaultProps: PropertyFilterProps = { filteringProperties, @@ -177,7 +189,7 @@ describe('property filter parts', () => { .findDropdown() .findOptions() .map(optionWrapper => optionWrapper.getElement().textContent) - ).toEqual(['string', 'string-other', 'default', 'string!=', 'range']); + ).toEqual(['string', 'string-other', 'default', 'string!=', 'state', 'range']); // property and value suggestions act(() => wrapper.setInputValue('a')); expect( @@ -187,6 +199,7 @@ describe('property filter parts', () => { .map(optionWrapper => optionWrapper.getElement().textContent) ).toEqual([ 'default', + 'state', 'range', 'string = value1', 'string-other = value1', @@ -531,7 +544,7 @@ describe('property filter parts', () => { .findDropdown() .findOptions()! .map(optionWrapper => optionWrapper.getElement().textContent) - ).toEqual(['All properties', 'string', 'string-other', 'default', 'string!=', 'range']); + ).toEqual(['All properties', 'string', 'string-other', 'default', 'string!=', 'state', 'range']); const operatorSelectWrapper = findOperatorSelector(contentWrapper); act(() => operatorSelectWrapper.openDropdown()); @@ -567,7 +580,7 @@ describe('property filter parts', () => { .findDropdown() .findOptions()! .map(optionWrapper => optionWrapper.getElement().textContent) - ).toEqual(['string', 'string-other', 'default', 'string!=', 'range']); + ).toEqual(['string', 'string-other', 'default', 'string!=', 'state', 'range']); }); test('preserves fields, when one is edited', () => { const { propertyFilterWrapper: wrapper } = renderComponent({ @@ -817,8 +830,8 @@ describe('property filter parts', () => { }, }); expect(wrapper.findTokens()).toHaveLength(2); - expect(openEditor(wrapper, 0)[0].find('[data-testid="change+"]')).not.toBe(null); - expect(openEditor(wrapper, 1)[0].find('[data-testid="change-"]')).not.toBe(null); + expect(openTokenEditor(wrapper, 0)[0].find('[data-testid="change+"]')).not.toBe(null); + expect(openTokenEditor(wrapper, 1)[0].find('[data-testid="change-"]')).not.toBe(null); }); test('extended operator form takes value/onChange state', () => { @@ -1093,6 +1106,134 @@ describe('property filter parts', () => { }); }); + describe('labelled values', () => { + test('tokens are labelled', () => { + const { propertyFilterWrapper: wrapper } = renderComponent({ + query: { tokens: [{ propertyKey: 'state', value: '0', operator: '=' }], operation: 'or' }, + }); + + expect(wrapper.findTokens()[0].getElement()).toHaveTextContent('state = Stopped'); + }); + + test('token editor values are labelled', () => { + const { propertyFilterWrapper: wrapper } = renderComponent({ + query: { tokens: [{ propertyKey: 'state', value: '0', operator: '=' }], operation: 'or' }, + }); + + const [contentWrapper] = openTokenEditor(wrapper); + const valueSelectWrapper = findValueSelector(contentWrapper); + act(() => valueSelectWrapper.focus()); + expect( + valueSelectWrapper + .findDropdown() + .findOptions()! + .map(optionWrapper => optionWrapper.getElement().textContent) + ).toEqual(['Stopped', 'Stopping', 'Running']); + }); + + test('property value suggestions are labelled', () => { + const { propertyFilterWrapper: wrapper } = renderComponent(); + + act(() => wrapper.setInputValue('state=')); + expect( + wrapper + .findDropdown() + .findOptions() + .map(optionWrapper => optionWrapper.getElement().textContent) + ).toEqual(['state = Stopped', 'state = Stopping', 'state = Running']); + }); + + test('matches property values', () => { + const { propertyFilterWrapper: wrapper } = renderComponent(); + + // By label + act(() => wrapper.setInputValue('state=Stopp')); + expect( + wrapper + .findDropdown() + .findOptions() + .map(optionWrapper => optionWrapper.getElement().textContent) + ).toEqual(['state = Stopped', 'state = Stopping']); + + // By value + act(() => wrapper.setInputValue('state=2')); + expect( + wrapper + .findDropdown() + .findOptions() + .map(optionWrapper => optionWrapper.getElement().textContent) + ).toEqual(['state = Running']); + }); + + test('matches all values', () => { + const { propertyFilterWrapper: wrapper } = renderComponent(); + + // By label + act(() => wrapper.setInputValue('Stopp')); + expect( + wrapper + .findDropdown() + .findOptions() + .map(optionWrapper => optionWrapper.getElement().textContent) + ).toEqual(['state = Stopped', 'state = Stopping']); + + // By value + act(() => wrapper.setInputValue('0')); + expect( + wrapper + .findDropdown() + .findOptions() + .map(optionWrapper => optionWrapper.getElement().textContent) + ).toEqual(['state = Stopped']); + }); + + test('query is created with actual value when clicking on option', () => { + const onChange = jest.fn(); + const { propertyFilterWrapper: wrapper } = renderComponent({ onChange }); + + // Selecting matched option from the list + act(() => wrapper.setInputValue('state=Stopp')); + act(() => wrapper.selectSuggestion(1)); + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + detail: { + tokens: [{ propertyKey: 'state', value: '0', operator: '=' }], + operation: 'and', + }, + }) + ); + }); + + test('query is created with actual value when pressing enter', () => { + const onChange = jest.fn(); + const { propertyFilterWrapper: wrapper } = renderComponent({ onChange }); + + // Entering full label + act(() => wrapper.setInputValue('state=Stopping')); + act(() => wrapper.findNativeInput().keydown(KeyCode.enter)); + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + detail: { + tokens: [{ propertyKey: 'state', value: '1', operator: '=' }], + operation: 'and', + }, + }) + ); + + // Entering full value + act(() => wrapper.setInputValue('state=2')); + act(() => wrapper.findNativeInput().keydown(KeyCode.enter)); + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + detail: { + tokens: [{ propertyKey: 'state', value: '2', operator: '=' }], + operation: 'and', + }, + }) + ); + }); + }); + test('property filter input can be found with autosuggest selector', () => { const { container } = renderComponent(); expect(createWrapper(container).findAutosuggest()!.getElement()).not.toBe(null); diff --git a/src/property-filter/controller.ts b/src/property-filter/controller.ts index 5dd33f8223..6293e383e8 100644 --- a/src/property-filter/controller.ts +++ b/src/property-filter/controller.ts @@ -103,16 +103,12 @@ export const parseText = ( if (operator) { const operatorLastIndex = textWithoutProperty.indexOf(operator) + operator.length; const textWithoutPropertyAndOperator = textWithoutProperty.slice(operatorLastIndex); - return { - step: 'property', - property, - operator, - // We need to remove the first leading space in case the user presses space - // after the operator, for example: Owner: admin, will result in value of ` admin` - // and we need to remove the first space, if the user added any more spaces only the - // first one will be removed. - value: trimFirstSpace(textWithoutPropertyAndOperator), - }; + // We need to remove the first leading space in case the user presses space + // after the operator, for example: Owner: admin, will result in value of ` admin` + // and we need to remove the first space, if the user added any more spaces only the + // first one will be removed. + const value = trimFirstSpace(textWithoutPropertyAndOperator); + return { step: 'property', property, operator, value }; } const operatorPrefix = matchOperatorPrefix(allowedOps, trimStart(textWithoutProperty)); @@ -173,7 +169,7 @@ export const getAllValueSuggestions = ( const propertyGroup = property.group ? customGroups[property.group] : defaultGroup; propertyGroup.options.push({ value: property.propertyLabel + ' ' + (operator || '=') + ' ' + filteringOption.value, - label: filteringOption.value, + label: filteringOption.label ?? filteringOption.value, __labelPrefix: property.propertyLabel + ' ' + (operator || '='), }); }); @@ -288,9 +284,9 @@ export const getAutosuggestOptions = ( filterText: parsedText.value, options: [ { - options: options.map(({ value }) => ({ + options: options.map(({ label, value }) => ({ value: propertyLabel + ' ' + parsedText.operator + ' ' + value, - label: value, + label: label ?? value, __labelPrefix: propertyLabel + ' ' + parsedText.operator, })), label: groupValuesLabel, diff --git a/src/property-filter/filter-options.ts b/src/property-filter/filter-options.ts index f54dac719f..d41fdd9b19 100644 --- a/src/property-filter/filter-options.ts +++ b/src/property-filter/filter-options.ts @@ -31,5 +31,10 @@ function isGroup(optionOrGroup: AutosuggestProps.Option): optionOrGroup is Autos } function matchSingleOption(option: OptionDefinition, searchText: string): boolean { - return (option.label ?? '').toLowerCase().indexOf(searchText.toLowerCase()) !== -1; + searchText = searchText.toLowerCase(); + + const label = (option.label ?? '').toLowerCase(); + const labelPrefix = option.__labelPrefix ?? ''; + const value = (option.value ? option.value.slice(labelPrefix.length) : '').toLowerCase(); + return label.indexOf(searchText) !== -1 || value.indexOf(searchText) !== -1; } diff --git a/src/property-filter/index.tsx b/src/property-filter/index.tsx index a3d2ca83bd..3e1cf46f8b 100644 --- a/src/property-filter/index.tsx +++ b/src/property-filter/index.tsx @@ -27,6 +27,7 @@ import useBaseComponent from '../internal/hooks/use-base-component'; import PropertyFilterAutosuggest, { PropertyFilterAutosuggestProps } from './property-filter-autosuggest'; import { PropertyEditor } from './property-editor'; import { AutosuggestInputRef } from '../internal/components/autosuggest-input'; +import { matchTokenValue } from './utils'; export { PropertyFilterProps }; @@ -85,11 +86,14 @@ const PropertyFilter = React.forwardRef( let newToken: Token; switch (parsedText.step) { case 'property': { - newToken = { - propertyKey: parsedText.property.key, - operator: parsedText.operator, - value: parsedText.value, - }; + newToken = matchTokenValue( + { + propertyKey: parsedText.property.key, + operator: parsedText.operator, + value: parsedText.value, + }, + filteringOptions + ); break; } case 'free-text': { diff --git a/src/property-filter/interfaces.ts b/src/property-filter/interfaces.ts index dfd9c169e8..12802df457 100644 --- a/src/property-filter/interfaces.ts +++ b/src/property-filter/interfaces.ts @@ -77,6 +77,21 @@ export interface PropertyFilterProps extends BaseComponentProps, ExpandToViewpor * * * `propertyKey` [string]: The key of the corresponding filtering property in the `filteringProperties` array. * * `value` [string]: The value that will be used as a suggestion when creating or modifying a filtering token. + * * `label` [string]: Optional suggestion label to be matched instead of the value. + * + * Filtering options that require labels can only use `=` and `!=` operators. The token value must be labelled separately, for example: + * ``` + * const filteringProperty = { + * key: 'state', + * propertyLabel: 'State', + * operators: ['=', '!='].map(operator => ({ operator, format: getStateLabel })) + * } + * const filteringOptions = [ + * { propertyKey: 'state', value: 'STOPPED', label: getStateLabel('STOPPED') }, + * { propertyKey: 'state', value: 'STOPPING', label: getStateLabel('STOPPING') }, + * { propertyKey: 'state', value: 'RUNNING', label: getStateLabel('RUNNING') }, + * ] + * ``` */ filteringOptions?: ReadonlyArray; /** diff --git a/src/property-filter/token-editor.tsx b/src/property-filter/token-editor.tsx index ac636900a9..129044e58f 100644 --- a/src/property-filter/token-editor.tsx +++ b/src/property-filter/token-editor.tsx @@ -30,6 +30,7 @@ import { NonCancelableEventHandler } from '../internal/events'; import { DropdownStatusProps } from '../internal/components/dropdown-status/interfaces'; import InternalButton from '../button/internal'; import InternalFormField from '../form-field/internal'; +import { matchTokenValue } from './utils'; const freeTextOperators: ComparisonOperator[] = [':', '!:']; @@ -168,11 +169,14 @@ function ValueInput({ i18nStrings, }: ValueInputProps) { const property = propertyKey !== undefined ? getPropertyByKey(filteringProperties, propertyKey) : undefined; - const valueOptions = property ? getPropertyOptions(property, filteringOptions).map(({ value }) => ({ value })) : []; + const valueOptions = property + ? getPropertyOptions(property, filteringOptions).map(({ label, value }) => ({ label, value })) + : []; const valueAutosuggestHandlers = useLoadItems(onLoadItems, '', property); const asyncValueAutosuggesProps = propertyKey ? { ...valueAutosuggestHandlers, ...asyncProps } : { empty: asyncProps.empty }; + const [mathedOption] = valueOptions.filter(option => option.value === value); const OperatorForm = propertyKey && operator && getExtendedOperator(filteringProperties, propertyKey, operator)?.form; @@ -181,7 +185,7 @@ function ValueInput({ ) : ( onChangeValue(e.detail.value)} disabled={!operator} options={valueOptions} @@ -310,7 +314,7 @@ export function TokenEditor({ { - setToken(temporaryToken); + setToken(matchTokenValue(temporaryToken, filteringOptions)); closePopover(); }} > diff --git a/src/property-filter/utils.ts b/src/property-filter/utils.ts index abefe11b87..8e237ee220 100644 --- a/src/property-filter/utils.ts +++ b/src/property-filter/utils.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { ComparisonOperator, FilteringProperty } from './interfaces'; +import { ComparisonOperator, FilteringOption, FilteringProperty, Token } from './interfaces'; // Finds the longest property the filtering text starts from. export function matchFilteringProperty( @@ -59,6 +59,20 @@ export function matchOperatorPrefix( return null; } +export function matchTokenValue(token: Token, filteringOptions: readonly FilteringOption[]): Token { + const value = token.value.toLowerCase(); + + const propertyOptions = filteringOptions.filter(option => option.propertyKey === token.propertyKey); + for (const option of propertyOptions) { + const optionText = (option.label ?? option.value ?? '').toLowerCase(); + if (optionText === value) { + return { ...token, value: option.value }; + } + } + + return token; +} + export function trimStart(source: string): string { let spacesLength = 0; for (let i = 0; i < source.length; i++) {