Skip to content

Commit

Permalink
wip: Property filter uses internal embedded multiselect
Browse files Browse the repository at this point in the history
  • Loading branch information
pan-kot committed Oct 10, 2024
1 parent 5b0e892 commit a8f6987
Show file tree
Hide file tree
Showing 27 changed files with 962 additions and 145 deletions.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"start:watch": "gulp watch",
"start:dev": "cross-env NODE_ENV=development webpack serve --config pages/webpack.config.js",
"start:integ": "cross-env NODE_ENV=development webpack serve --config pages/webpack.config.integ.js",
"prepare": "husky"
"prepare": "husky",
"postinstall": "node ./scripts/install-peer-dependency.js collection-hooks:feat-property-filter-enum-props-3"
},
"dependencies": {
"@cloudscape-design/collection-hooks": "^1.0.0",
Expand Down
7 changes: 4 additions & 3 deletions pages/common/options-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ export function useOptionsLoader<Item>({ pageSize = 25, timeout = 1000, randomEr
if (randomErrors && Math.random() < 0.3) {
reject();
} else {
const nextItems = sourceItems.slice(pageNumber * pageSize, (pageNumber + 1) * pageSize);
resolve({ items: nextItems, hasNextPage: items.length + nextItems.length < sourceItems.length });
const newItems = sourceItems.slice(pageNumber * pageSize, (pageNumber + 1) * pageSize);
const nextItems = [...items, ...newItems];
resolve({ items: nextItems, hasNextPage: nextItems.length < sourceItems.length });
}
}, timeout)
);
Expand Down Expand Up @@ -100,7 +101,7 @@ export function useOptionsLoader<Item>({ pageSize = 25, timeout = 1000, randomEr
request.promise
.then(response => {
if (!request.cancelled) {
setItems(prev => [...prev, ...(response.items as Item[])]);
setItems(response.items as Item[]);
setStatus(response.hasNextPage ? 'pending' : 'finished');
}
})
Expand Down
2 changes: 1 addition & 1 deletion pages/multiselect/multiselect.async.example.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ function EmbeddedMultiselectIntegration(props: EmbeddedMultiselectProps) {
filteringPlaceholder="Find security group"
/>

<div style={{ maxBlockSize: 400, display: 'flex' }}>
<div style={{ maxBlockSize: 400, display: 'flex', flexDirection: 'column' }}>
<EmbeddedMultiselect {...props} filteringText={filteringText} />
</div>
</SpaceBetween>
Expand Down
8 changes: 4 additions & 4 deletions pages/property-filter/async-loading.integ.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,10 @@ export default function () {
filteringProperties={filteringProperties}
filteringOptions={optionsLoader.items}
filteringStatusType={status}
filteringLoadingText={'loading text'}
filteringErrorText={'error text'}
filteringRecoveryText={'recovery text'}
filteringFinishedText={'finished text'}
filteringLoadingText="loading text"
filteringErrorText="error text"
filteringRecoveryText="recovery text"
filteringFinishedText="finished text"
onLoadItems={handleLoadItems}
asyncProperties={urlParams.asyncProperties}
virtualScroll={true}
Expand Down
77 changes: 50 additions & 27 deletions pages/property-filter/common-props.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,11 @@ import React from 'react';
import { Badge, SpaceBetween } from '~components';
import { PropertyFilterProps } from '~components/property-filter';

import {
DateForm,
DateTimeForm,
DateTimeFormLegacy,
formatDateTime,
formatOwners,
OwnerMultiSelectForm,
YesNoForm,
yesNoFormat,
} from './custom-forms';
import { DateForm, DateTimeForm, DateTimeFormLegacy, formatDateTime, YesNoForm, yesNoFormat } from './custom-forms';
import { states, TableItem } from './table.data';

const getStateLabel = (value: TableItem['state']) => (value !== undefined && states[value]) || 'Unknown';
const getStateLabel = (value: TableItem['state'], fallback = 'Invalid value') =>
(value !== undefined && states[value]) || fallback;

export const columnDefinitions = [
{
Expand All @@ -33,7 +25,8 @@ export const columnDefinitions = [
sortingField: 'state',
header: 'State',
type: 'enum',
getLabel: getStateLabel,
getLabel: (value: any) =>
Array.isArray(value) ? value.map(v => getStateLabel(v)).join(', ') : getStateLabel(value, value),
propertyLabel: 'State',
cell: (item: TableItem) => getStateLabel(item.state),
},
Expand Down Expand Up @@ -73,7 +66,7 @@ export const columnDefinitions = [
id: 'owner',
sortingField: 'owner',
header: 'Owner',
type: 'text',
type: 'enum',
propertyLabel: 'Owner',
cell: (item: TableItem) => item.owner,
},
Expand Down Expand Up @@ -254,7 +247,18 @@ export const filteringProperties: readonly PropertyFilterProps.FilteringProperty
let groupValuesLabel = `${def.propertyLabel} values`;

if (def.type === 'enum') {
operators = ['=', '!='].map(operator => ({ operator, format: def.getLabel }));
operators = [
...['=', '!='].map(operator => ({ operator, format: def.getLabel, tokenType: 'enum' })),
...[':', '!:'].map(operator => ({ operator, format: def.getLabel, tokenType: 'value' })),
];
}
if (def.id === 'tags') {
operators = [
{ operator: '=', tokenType: 'enum', match: (v: unknown[], t: unknown[]) => checkArrayMatches(v, t) },
{ operator: '!=', tokenType: 'enum', match: (v: unknown[], t: unknown[]) => !checkArrayMatches(v, t) },
{ operator: ':', tokenType: 'enum', match: (v: unknown[], t: unknown[]) => checkArrayContains(v, t) },
{ operator: '!:', tokenType: 'enum', match: (v: unknown[], t: unknown[]) => !checkArrayContains(v, t) },
];
}

if (def.type === 'text') {
Expand Down Expand Up @@ -297,19 +301,6 @@ export const filteringProperties: readonly PropertyFilterProps.FilteringProperty
];
}

// This is not recommended as it nests
if (def.id === 'owner') {
operators = [
{
operator: '=',
form: OwnerMultiSelectForm,
format: formatOwners,
match: (itemValue: string, tokenValue: string[]) =>
Array.isArray(tokenValue) && tokenValue.some(value => itemValue === value),
},
];
}

return {
key: def.id,
operators: operators,
Expand All @@ -318,3 +309,35 @@ export const filteringProperties: readonly PropertyFilterProps.FilteringProperty
groupValuesLabel,
};
});

function checkArrayMatches(value: unknown[], token: unknown[]) {
if (!Array.isArray(value) || !Array.isArray(token) || value.length !== token.length) {
return false;
}
const valuesMap = value.reduce<Map<unknown, number>>(
(map, value) => map.set(value, (map.get(value) ?? 0) + 1),
new Map()
);
for (const tokenEntry of token) {
const count = valuesMap.get(tokenEntry);
if (count) {
count === 1 ? valuesMap.delete(tokenEntry) : valuesMap.set(tokenEntry, count - 1);
} else {
return false;
}
}
return valuesMap.size === 0;
}

function checkArrayContains(value: unknown[], token: unknown[]) {
if (!Array.isArray(value) || !Array.isArray(token)) {
return false;
}
const valuesSet = new Set(value);
for (const tokenEntry of token) {
if (!valuesSet.has(tokenEntry)) {
return false;
}
}
return true;
}
59 changes: 0 additions & 59 deletions pages/property-filter/custom-forms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,8 @@ 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 EmbeddedMultiselect from '~components/multiselect/embedded';
import InternalMultiselect from '~components/multiselect/internal';
import { ExtendedOperatorFormProps } from '~components/property-filter/interfaces';

import { allItems } from './table.data';

import styles from './custom-forms.scss';

export function YesNoForm({ value, onChange }: ExtendedOperatorFormProps<boolean>) {
Expand Down Expand Up @@ -218,58 +214,3 @@ function formatTimezoneOffset(isoDate: string, offsetInMinutes?: number) {
.padStart(2, '0');
return `${sign}${hoursOffset}:${minuteOffset}`;
}

const allOwners = [...new Set(allItems.map(({ owner }) => owner))];

export function OwnerMultiSelectForm({ value, onChange, filter }: ExtendedOperatorFormProps<string[]>) {
value = value && Array.isArray(value) ? value : [];

if (typeof filter !== 'undefined') {
return (
<EmbeddedMultiselect
options={allOwners.map(owner => ({ value: owner, label: owner }))}
selectedOptions={value.map(owner => ({ value: owner, label: owner })) ?? []}
onChange={event =>
onChange(
event.detail.selectedOptions
.map(({ value }) => value)
.filter((value): value is string => typeof value !== 'undefined')
)
}
filteringText={filter}
statusType="finished"
filteringType="auto"
empty="No options available"
noMatch="No options matched"
/>
);
}

return (
<div className={styles['multiselect-form']}>
<FormField stretch={true}>
<InternalMultiselect
options={allOwners.map(owner => ({ value: owner, label: owner }))}
selectedOptions={value.map(owner => ({ value: owner, label: owner })) ?? []}
onChange={event =>
onChange(
event.detail.selectedOptions
.map(({ value }) => value)
.filter((value): value is string => typeof value !== 'undefined')
)
}
statusType="finished"
filteringType="none"
expandToViewport={true}
keepOpen={true}
hideTokens={false}
inlineTokens={true}
/>
</FormField>
</div>
);
}

export function formatOwners(owners: string[]) {
return owners && Array.isArray(owners) ? owners.join(', ') : '';
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const nameProperty: InternalFilteringProperty = {
groupValuesLabel: 'Name values',
operators: ['=', '!='],
defaultOperator: '=',
getTokenType: () => 'value',
getValueFormatter: () => null,
getValueFormRenderer: () => null,
externalProperty,
Expand All @@ -43,6 +44,7 @@ const dateProperty: InternalFilteringProperty = {
groupValuesLabel: 'Date values',
operators: ['=', '!='],
defaultOperator: '=',
getTokenType: () => 'value',
getValueFormatter: () => (value: Date) => (value ? format(value, 'yyyy-MM-dd') : ''),
getValueFormRenderer:
() =>
Expand All @@ -60,6 +62,7 @@ const dateTimeProperty: InternalFilteringProperty = {
groupValuesLabel: 'Date time values',
operators: ['=', '!='],
defaultOperator: '=',
getTokenType: () => 'value',
getValueFormatter: () => (value: Date) => (value ? format(value, 'yyyy-MM-dd hh:mm') : ''),
getValueFormRenderer:
() =>
Expand Down
Loading

0 comments on commit a8f6987

Please sign in to comment.