Skip to content

Commit

Permalink
chore: Property filter token groups internal feature (#2627)
Browse files Browse the repository at this point in the history
  • Loading branch information
pan-kot authored Sep 4, 2024
1 parent 67017e5 commit 09aa16f
Show file tree
Hide file tree
Showing 21 changed files with 1,198 additions and 667 deletions.
6 changes: 4 additions & 2 deletions pages/property-filter/common-props.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { PropertyFilterProps } from '~components/property-filter';
import { I18nStringsExt } from '~components/property-filter/i18n-utils';
import { I18nStringsTokenGroups } from '~components/property-filter/interfaces';

import {
DateForm,
Expand Down Expand Up @@ -146,7 +146,7 @@ export const labels = {
filteringPlaceholder: 'Search',
};

export const i18nStrings: PropertyFilterProps.I18nStrings & I18nStringsExt = {
export const i18nStrings: PropertyFilterProps.I18nStrings = {
dismissAriaLabel: 'Dismiss',

groupValuesText: 'Values',
Expand Down Expand Up @@ -184,7 +184,9 @@ export const i18nStrings: PropertyFilterProps.I18nStrings & I18nStringsExt = {

formatToken,
removeTokenButtonAriaLabel: token => `Remove token, ${formatToken(token)}`,
};

export const i18nStringsTokenGroups: I18nStringsTokenGroups = {
groupEditAriaLabel: group => `Edit group with ${group.tokens.length} tokens`,
tokenEditorTokenActionsAriaLabel: token => `Filter remove actions for ${formatToken(token)}`,
tokenEditorTokenRemoveAriaLabel: token => `Remove filter, ${formatToken(token)}`,
Expand Down
5 changes: 3 additions & 2 deletions pages/property-filter/custom-forms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import React, { useEffect, useState } from 'react';
import { DatePicker, FormField, RadioGroup, TimeInput, TimeInputProps } from '~components';
import Calendar, { CalendarProps } from '~components/calendar';
import DateInput from '~components/date-input';
import Multiselect from '~components/multiselect';
import InternalMultiselect from '~components/multiselect/internal';
import { ExtendedOperatorFormProps } from '~components/property-filter/interfaces';

import { allItems } from './table.data';
Expand Down Expand Up @@ -224,7 +224,7 @@ export function OwnerMultiSelectForm({ value, onChange }: ExtendedOperatorFormPr
value = value && Array.isArray(value) ? value : [];
return (
<FormField stretch={true}>
<Multiselect
<InternalMultiselect
options={allOwners.map(owner => ({ value: owner, label: owner }))}
selectedOptions={value.map(owner => ({ value: owner, label: owner })) ?? []}
onChange={event =>
Expand All @@ -235,6 +235,7 @@ export function OwnerMultiSelectForm({ value, onChange }: ExtendedOperatorFormPr
)
}
expandToViewport={true}
inlineTokens={true}
/>
</FormField>
);
Expand Down
25 changes: 12 additions & 13 deletions pages/property-filter/property-filter-editor-permutations.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,9 @@ const defaultProps: Omit<TokenEditorProps, 'i18nStrings'> = {
filteringOptions: [],
onSubmit: () => {},
onDismiss: () => {},
standaloneTokens: [],
onChangeStandalone: () => {},
tokensToCapture: [],
onTokenCapture: () => {},
onTokenRelease: () => {},
tempGroup: [{ property: null, operator: ':', value: 'search text' }],
onChangeTempGroup: () => {},
};
Expand Down Expand Up @@ -118,37 +119,35 @@ const tokenPermutations = createPermutations<Partial<TokenEditorProps>>([
{ property: nameProperty, operator: '=', value: 'Jack' },
],
],
standaloneTokens: [
tokensToCapture: [
[
{ property: dateProperty, operator: '=', value: new Date('2020-01-01') },
{ property: dateProperty, operator: '=', value: new Date('2020-01-02') },
{ property: dateProperty, operator: '=', value: new Date('2020-01-03') },
{ property: dateProperty, operator: '=', value: new Date('2020-01-04') },
{ property: dateProperty, operator: '=', value: new Date('2020-01-05') },
{ standaloneIndex: 0, property: dateProperty, operator: '=', value: new Date('2020-01-01') },
{ standaloneIndex: 1, property: dateProperty, operator: '=', value: new Date('2020-01-02') },
{ standaloneIndex: 2, property: dateProperty, operator: '=', value: new Date('2020-01-03') },
{ standaloneIndex: 3, property: dateProperty, operator: '=', value: new Date('2020-01-04') },
{ standaloneIndex: 4, property: dateProperty, operator: '=', value: new Date('2020-01-05') },
],
],
},
]);

function TokenEditorStateful(props: Omit<TokenEditorProps, 'i18nStrings'>) {
const [tempGroup, setTempGroup] = useState(props.tempGroup);
const [standaloneTokens, setStandaloneTokens] = useState(props.standaloneTokens);
const capturedTokenIndices = tempGroup.map(token => token.standaloneIndex).filter(Boolean);
const tokensToCapture = props.tokensToCapture.filter((_, index) => !capturedTokenIndices.includes(index));
const i18nStringsInternal = usePropertyFilterI18n(i18nStrings);
return (
<TokenEditor
{...props}
i18nStrings={i18nStringsInternal}
standaloneTokens={standaloneTokens}
onChangeStandalone={setStandaloneTokens}
tokensToCapture={tokensToCapture}
tempGroup={tempGroup}
onChangeTempGroup={setTempGroup}
onDismiss={() => {
setTempGroup(props.tempGroup);
setStandaloneTokens(props.standaloneTokens);
}}
onSubmit={() => {
setTempGroup(props.tempGroup);
setStandaloneTokens(props.standaloneTokens);
}}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Button from '~components/button';
import Header from '~components/header';
import I18nProvider from '~components/i18n';
import messages from '~components/i18n/messages/all.en';
import PropertyFilter from '~components/property-filter';
import PropertyFilter from '~components/property-filter/internal';
import SplitPanel from '~components/split-panel';
import Table from '~components/table';

Expand Down Expand Up @@ -97,8 +97,11 @@ export default function () {
filteringOptions={filteringOptions}
virtualScroll={true}
countText={`${items.length} matches`}
enableTokenGroups={true}
expandToViewport={true}
filteringEmpty="No properties"
customGroupsText={[]}
disableFreeTextFiltering={false}
/>
}
columnDefinitions={columnDefinitions.slice(0, 2)}
Expand Down
2 changes: 2 additions & 0 deletions pages/property-filter/token-editor.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
columnDefinitions,
filteringProperties as commonFilteringProperties,
i18nStrings,
i18nStringsTokenGroups,
labels,
} from './common-props';

Expand All @@ -27,6 +28,7 @@ const commonProps = {
filteringProperties,
filteringOptions: [],
i18nStrings,
i18nStringsTokenGroups,
countText: '5 matches',
disableFreeTextFiltering: false,
virtualScroll: true,
Expand Down
28 changes: 3 additions & 25 deletions src/__tests__/__snapshots__/documenter.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -12068,18 +12068,7 @@ Object {
"description": "Fired when the \`query\` gets changed. Filter the dataset in response to this event using the values in the \`detail\` object.",
"detailInlineType": Object {
"name": "PropertyFilterProps.Query",
"properties": Array [
Object {
"name": "operation",
"optional": false,
"type": "PropertyFilterProps.JoinOperation",
},
Object {
"name": "tokens",
"optional": false,
"type": "ReadonlyArray<PropertyFilterProps.Token>",
},
],
"properties": Array [],
"type": "object",
},
"detailType": "PropertyFilterProps.Query",
Expand Down Expand Up @@ -12165,7 +12154,7 @@ To use it correctly, define an ID for the element you want to use as label and s
"type": "string",
},
Object {
"description": "Set \`asyncProperties\` if you need to load \`filteringProperties\` asynchronousely. This would cause extra \`onLoadMore\`
"description": "Set \`asyncProperties\` if you need to load \`filteringProperties\` asynchronously. This would cause extra \`onLoadMore\`
events to fire calling for more properties.",
"name": "asyncProperties",
"optional": true,
Expand Down Expand Up @@ -12557,18 +12546,7 @@ Each token has the following properties:
",
"inlineType": Object {
"name": "PropertyFilterProps.Query",
"properties": Array [
Object {
"name": "operation",
"optional": false,
"type": "PropertyFilterProps.JoinOperation",
},
Object {
"name": "tokens",
"optional": false,
"type": "ReadonlyArray<PropertyFilterProps.Token>",
},
],
"properties": Array [],
"type": "object",
},
"name": "query",
Expand Down
1 change: 1 addition & 0 deletions src/autosuggest/options-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export const useAutosuggestItems = ({
const enteredItemLabel = i18n('enteredTextLabel', enteredTextLabel?.(filterValue), format =>
format({ value: filterValue })
);

if (!enteredItemLabel) {
warnOnce('Autosuggest', 'A value for enteredTextLabel must be provided.');
}
Expand Down
77 changes: 72 additions & 5 deletions src/property-filter/__tests__/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,17 @@
import React, { useState } from 'react';

import PropertyFilter from '../../../lib/components/property-filter';
import { FilteringProperty, InternalFilteringProperty, PropertyFilterProps, Token } from '../interfaces';
import PropertyFilterInternal, { PropertyFilterInternalProps } from '../../../lib/components/property-filter/internal';
import {
FilteringProperty,
I18nStrings,
I18nStringsTokenGroups,
InternalFilteringProperty,
PropertyFilterProps,
Token,
} from '../interfaces';

export const i18nStrings = {
export const i18nStrings: I18nStrings = {
dismissAriaLabel: 'Dismiss',

groupValuesText: 'Values',
Expand Down Expand Up @@ -37,10 +45,64 @@ export const i18nStrings = {
tokenLimitShowFewer: 'Show fewer',
clearFiltersText: 'Clear filters',
tokenOperatorAriaLabel: 'Boolean Operator',
removeTokenButtonAriaLabel: (token: Token) =>
'Remove token ' + token.propertyKey + ' ' + token.operator + ' ' + token.value,
enteredTextLabel: (text: string) => `Use: "${text}"`,
} as const;

formatToken: token => `${token.propertyLabel} ${formatOperator(token.operator)} ${token.value}`,
removeTokenButtonAriaLabel: (token: Token) =>
'Remove token ' + token.propertyKey + ' ' + formatOperator(token.operator) + ' ' + token.value,
};

export const i18nStringsTokenGroups: I18nStringsTokenGroups = {
groupEditAriaLabel: group =>
'Edit filter, ' +
group.tokens
.map(token => `${token.propertyLabel} ${formatOperator(token.operator)} ${token.value}`)
.join(` ${group.operationLabel} `),
tokenEditorTokenActionsAriaLabel: token =>
`Remove actions, ${token.propertyLabel} ${formatOperator(token.operator)} ${token.value}`,
tokenEditorTokenRemoveAriaLabel: token =>
`Remove filter, ${token.propertyLabel} ${formatOperator(token.operator)} ${token.value}`,
tokenEditorTokenRemoveLabel: 'Remove filter',
tokenEditorTokenRemoveFromGroupLabel: 'Remove filter from group',
tokenEditorAddNewTokenLabel: 'Add new filter',
tokenEditorAddTokenActionsAriaLabel: 'Add filter actions',
tokenEditorAddExistingTokenAriaLabel: token =>
`Add filter ${token.propertyLabel} ${formatOperator(token.operator)} ${token.value} to group`,
tokenEditorAddExistingTokenLabel: token =>
`Add filter ${token.propertyLabel} ${token.operator} ${token.value} to group`,
};

export const providedI18nStrings = {
'property-filter': {
'i18nStrings.editTokenHeader': 'Edit token',
'i18nStrings.propertyText': 'Property',
'i18nStrings.operatorText': 'Operator',
'i18nStrings.valueText': 'Value',
'i18nStrings.cancelActionText': 'Cancel',
'i18nStrings.applyActionText': 'Apply',
'i18nStrings.formatToken': '{token__propertyLabel} {token__operator} {token__value}',
'i18nStrings.tokenEditorTokenActionsAriaLabel': 'Remove actions, {token__formattedText}',
'i18nStrings.tokenEditorTokenRemoveAriaLabel': 'Remove filter, {token__formattedText}',
'i18nStrings.tokenEditorTokenRemoveLabel': 'Remove filter',
'i18nStrings.tokenEditorTokenRemoveFromGroupLabel': 'Remove filter from group',
'i18nStrings.tokenEditorAddNewTokenLabel': 'Add new filter',
'i18nStrings.tokenEditorAddTokenActionsAriaLabel': 'Add filter actions',
'i18nStrings.tokenEditorAddExistingTokenAriaLabel': 'Add filter {token__formattedText} to group',
'i18nStrings.tokenEditorAddExistingTokenLabel':
'Add filter {token__propertyLabel} {token__operator} {token__value} to group',
},
};

function formatOperator(operator: string) {
switch (operator) {
case '=':
return 'equals';
case '!=':
return 'does_not_equal';
default:
return operator;
}
}

export const createDefaultProps = (
filteringProperties: PropertyFilterProps['filteringProperties'],
Expand Down Expand Up @@ -75,3 +137,8 @@ export function StatefulPropertyFilter(props: Omit<PropertyFilterProps, 'onChang
const [query, setQuery] = useState<PropertyFilterProps.Query>(props.query);
return <PropertyFilter {...props} query={query} onChange={e => setQuery(e.detail)} />;
}

export function StatefulInternalPropertyFilter(props: Omit<PropertyFilterInternalProps, 'onChange'>) {
const [query, setQuery] = useState<PropertyFilterInternalProps['query']>(props.query);
return <PropertyFilterInternal {...props} query={query} onChange={e => setQuery(e.detail)} />;
}
72 changes: 72 additions & 0 deletions src/property-filter/__tests__/property-filter-i18n.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import { act, render } from '@testing-library/react';

import TestI18nProvider from '../../../lib/components/i18n/testing';
import PropertyFilter from '../../../lib/components/property-filter';
import InternalPropertyFilter from '../../../lib/components/property-filter/internal';
import createWrapper, { ElementWrapper, PropertyFilterWrapper } from '../../../lib/components/test-utils/dom';
import { PropertyFilterWrapperInternal } from '../../../lib/components/test-utils/dom/property-filter/index.js';
import { createDefaultProps } from './common';

import styles from '../../../lib/components/property-filter/styles.selectors.js';
Expand Down Expand Up @@ -251,6 +253,76 @@ describe('i18n', () => {
expect(findSubmitButton(popoverContent).getElement()).toHaveTextContent('Custom Apply');
});

it('uses token group edit label from i18n provider', () => {
const { container } = render(
<TestI18nProvider
messages={{
'property-filter': {
'i18nStrings.operationAndText': '&',
'i18nStrings.operationOrText': '|',
'i18nStrings.formatToken': `{token__operator, select,
equals {{token__propertyLabel} eq {token__value}}
not_equals {{token__propertyLabel} neq {token__value}}
other {}}`,
'i18nStrings.groupEditAriaLabel': `{group__formattedTokens__length, select,
2 {Edit filter group {group__formattedTokens0__formattedText} {group__operationLabel} {group__formattedTokens1__formattedText}}
3 {Edit filter group {group__formattedTokens0__formattedText} {group__operationLabel} {group__formattedTokens1__formattedText} {group__operationLabel} {group__formattedTokens2__formattedText}}
4 {Edit filter group {group__formattedTokens0__formattedText} {group__operationLabel} {group__formattedTokens1__formattedText} {group__operationLabel} {group__formattedTokens2__formattedText} {group__operationLabel} {group__formattedTokens3__formattedText}}
5 {Edit filter group {group__formattedTokens0__formattedText} {group__operationLabel} {group__formattedTokens1__formattedText} {group__operationLabel} {group__formattedTokens2__formattedText} {group__operationLabel} {group__formattedTokens3__formattedText} {group__operationLabel} 1 more}
other {Edit filter group {group__formattedTokens0__formattedText} {group__operationLabel} {group__formattedTokens1__formattedText} {group__operationLabel} {group__formattedTokens2__formattedText} {group__operationLabel} {group__formattedTokens3__formattedText} {group__operationLabel} more}}`,
'i18nStrings.removeTokenButtonAriaLabel': `Remove filter, {token__formattedText}`,
},
}}
>
<InternalPropertyFilter
{...defaultProps}
i18nStrings={{}}
query={{
operation: 'and',
tokenGroups: [
{
operation: 'or',
tokens: [
{ propertyKey: 'string', operator: '=', value: 'value1' },
{ propertyKey: 'string', operator: '=', value: 'value2' },
{ propertyKey: 'string', operator: '=', value: 'value3' },
{ propertyKey: 'string', operator: '=', value: 'value4' },
{ propertyKey: 'string', operator: '=', value: 'value5' },
],
},
],
tokens: [],
}}
enableTokenGroups={true}
filteringOptions={[]}
customGroupsText={[]}
disableFreeTextFiltering={false}
/>
</TestI18nProvider>
);
const wrapper = new PropertyFilterWrapperInternal(createWrapper(container).findPropertyFilter()!.getElement());
const token = (index: number) => wrapper.findTokens()[index];
const groupToken = (index: number, inGroupIndex: number) => token(index).findGroupTokens()[inGroupIndex];

// 1st nested token
expect(groupToken(0, 0).getElement()).toHaveAccessibleName('String eq value1');
expect(groupToken(0, 0).findTokenOperation()).toBe(null);
expect(groupToken(0, 0).findRemoveButton().getElement()).toHaveAccessibleName('Remove filter, String eq value1');

// 1nd nested token
expect(groupToken(0, 1).getElement()).toHaveAccessibleName('String eq value2');
expect(groupToken(0, 1).findTokenOperation()!.getElement()).toHaveTextContent('|');
expect(groupToken(0, 1).findRemoveButton().getElement()).toHaveAccessibleName('Remove filter, String eq value2');

// Token group
expect(token(0).getElement()).toHaveAccessibleName(
'String eq value1 | String eq value2 | String eq value3 | String eq value4 | String eq value5'
);
expect(token(0).findEditButton()!.getElement()).toHaveAccessibleName(
'Edit filter group String eq value1 | String eq value2 | String eq value3 | String eq value4 | 1 more'
);
});

it('uses formatted token for removeTokenButtonAriaLabel', () => {
const { container } = render(
<PropertyFilter
Expand Down
Loading

0 comments on commit 09aa16f

Please sign in to comment.