From 16fa6c9c57b84716682abba2712d545e9ba556f3 Mon Sep 17 00:00:00 2001 From: Norbert Csaba Herczeg Date: Sat, 7 Sep 2024 01:35:07 +0200 Subject: [PATCH] JNG-5914 table tag representation --- .../Galaxy/View/ViewGalaxyView.tsx.snapshot | 15 +++++- .../react/UiPageContainerHelper.java | 2 +- .../ui/generator/react/UiTableHelper.java | 4 ++ .../ui/generator/react/UiWidgetHelper.java | 18 ++++++++ .../actor/src/components/widgets/Tags.tsx.hbs | 25 ++++------ .../containers/components/tag/index.tsx.hbs | 46 +++++++++++++------ .../containers/components/tag/types.ts.hbs | 7 +++ .../src/containers/widget-fragments/table.hbs | 13 +++++- .../AutocompleteAddAction.fragment.hbs | 13 ++++++ .../AutocompleteRangeAction.fragment.hbs | 9 ++++ .../actor/src/utilities/filter-helper.ts.hbs | 7 +-- .../actor/src/utilities/helper.ts.hbs | 38 +++++++++++++-- pom.xml | 2 +- 13 files changed, 155 insertions(+), 44 deletions(-) create mode 100644 judo-ui-react/src/main/resources/actor/src/pages/actions/AutocompleteAddAction.fragment.hbs diff --git a/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/containers/View/Galaxy/View/ViewGalaxyView.tsx.snapshot b/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/containers/View/Galaxy/View/ViewGalaxyView.tsx.snapshot index 9660e752..e1298368 100644 --- a/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/containers/View/Galaxy/View/ViewGalaxyView.tsx.snapshot +++ b/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/containers/View/Galaxy/View/ViewGalaxyView.tsx.snapshot @@ -159,7 +159,20 @@ export default function ViewGalaxyView(props: ViewGalaxyViewProps) { alignItems="stretch" justifyContent="flex-start" > - {'LMAO'} + diff --git a/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiPageContainerHelper.java b/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiPageContainerHelper.java index 5a8e7663..0d8c7002 100644 --- a/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiPageContainerHelper.java +++ b/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiPageContainerHelper.java @@ -61,7 +61,7 @@ public static List getTablesForPageContainers(Application application) { public static List
getTagsForPageContainers(Application application) { return application.getPageContainers().stream().flatMap(c -> ((List
) c.getTables()).stream()) - .filter(t -> TableRepresentation.TAG.equals(t.getRepresentationComponent())) + .filter(UiWidgetHelper::isTableTag) .collect(Collectors.toList()); } diff --git a/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiTableHelper.java b/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiTableHelper.java index 1a546948..6b43fee6 100644 --- a/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiTableHelper.java +++ b/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiTableHelper.java @@ -302,4 +302,8 @@ public static List customizableColumns(Table table) { public static boolean checkboxSelectionEnabled(Table table) { return table.getCheckboxSelection() == null || table.getCheckboxSelection() != CheckboxSelection.DISABLED; } + + public static boolean isTableTag(Table table) { + return TableRepresentation.TAG.equals(table.getRepresentationComponent()); + } } diff --git a/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiWidgetHelper.java b/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiWidgetHelper.java index 26c234b2..b1253242 100644 --- a/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiWidgetHelper.java +++ b/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiWidgetHelper.java @@ -217,6 +217,13 @@ public static Column getFirstAutocompleteColumnForLink(Link link) { return column.orElse(null); } + public static Column getFirstAutocompleteColumnForTable(Table table) { + Optional column = table.getColumns().stream() + .filter(c -> c.getAttributeType().getDataType() instanceof StringType && !c.getAttributeType().getIsMemberTypeTransient()) + .findFirst(); + return column.orElse(null); + } + public static boolean isAutocompleteAvailable(Link link) { if (link.getParts().isEmpty()) { return false; @@ -445,6 +452,10 @@ public static Integer calculateLinkAutocompleteRows(Link link) { return defaultValue != null ? defaultValue : 10; } + public static Integer calculateTableAutocompleteRows(Table table) { + return 10; + } + public static boolean flexParentIsNotTab(Flex flex) { return !(flex.eContainer() instanceof Tab); } @@ -468,6 +479,13 @@ public static Column getSortColumnForLink(Link link) { return getFirstAutocompleteColumnForLink(link); } + public static Column getSortColumnForTable(Table table) { + if (table.getDefaultSortColumn() != null) { + return table.getDefaultSortColumn(); + } + return getFirstAutocompleteColumnForTable(table); + } + public static boolean isLinkAssociation(Link link) { return link.getRelationType().getIsRelationKindAssociation(); } diff --git a/judo-ui-react/src/main/resources/actor/src/components/widgets/Tags.tsx.hbs b/judo-ui-react/src/main/resources/actor/src/components/widgets/Tags.tsx.hbs index 954fbcd2..6e22e7cb 100644 --- a/judo-ui-react/src/main/resources/actor/src/components/widgets/Tags.tsx.hbs +++ b/judo-ui-react/src/main/resources/actor/src/components/widgets/Tags.tsx.hbs @@ -27,10 +27,10 @@ export interface TagsProps { editMode?: boolean; autoCompleteAttribute: keyof T; identifierAttribute: string | keyof T; - onAutoCompleteSearch: (searchText: string, preparedQueryCustomizer: QueryCustomizer) => Promise; + onAutoCompleteSearch?: (searchText: string, preparedQueryCustomizer: QueryCustomizer) => Promise; additionalMaskAttributes?: string[]; limitOptions?: number; - onValueChange: (target: T[]) => Promise; + onValueChange?: (target: T[]) => Promise; onItemClick?: (target: T) => void; onSearchDialogsClick?: () => void; searchDialogTitle?: string; @@ -76,7 +76,6 @@ export function Tags(props: TagsProps) { } = props; const [options, setOptions] = useState([]); const [loading, setLoading] = useState(false); - const prevValues = useRef(ownerData[name] as T[]); const handleSearch = async (searchText: string) => { try { @@ -101,7 +100,7 @@ export function Tags(props: TagsProps) { limit: limitOptions, }, }; - const response = await onAutoCompleteSearch(searchText, queryCustomizer); + const response = await onAutoCompleteSearch!(searchText, queryCustomizer); setOptions(response); } catch (error) { console.error(error); @@ -123,9 +122,9 @@ export function Tags(props: TagsProps) { const onChange = useCallback( (event: SyntheticEvent, value: (string | any)[]) => { - // prevent useEffect below from triggering recursion - prevValues.current = value; - onValueChange(value as any); + if (typeof onValueChange === 'function') { + onValueChange(value as any); + } }, [ownerData, onValueChange], ); @@ -143,14 +142,6 @@ export function Tags(props: TagsProps) { [ownerData, onItemClick], ); - useEffect(() => { - // prevent recursion - if (prevValues.current !== ownerData[name]) { - prevValues.current = ownerData[name] as T[]; - onValueChange(ownerData[name] as any); - } - }, [ownerData[name]]); - return ( (props: TagsProps) { getOptionKey={(option) => option[identifierAttribute]} getOptionLabel={(option) => option[autoCompleteAttribute] ?? ''} isOptionEqualToValue={(option, value) => option[identifierAttribute] === value[identifierAttribute]} - onOpen={ () => { + onOpen={ onAutoCompleteSearch ? () => { setOptions([]); // always start with a clean slate handleSearch(''); - } } + } : undefined } onInputChange={onInputChange} onChange={onChange} renderInput={(params) => ( diff --git a/judo-ui-react/src/main/resources/actor/src/containers/components/tag/index.tsx.hbs b/judo-ui-react/src/main/resources/actor/src/containers/components/tag/index.tsx.hbs index 2666aeaf..63926c0a 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/components/tag/index.tsx.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/components/tag/index.tsx.hbs @@ -63,6 +63,8 @@ import { useL10N } from '~/l10n/l10n-context'; import { TABLE_COLUMN_CUSTOMIZER_HOOK_INTERFACE_KEY, randomUtils, + createActionName, + actionsHasCapability, {{# if container.isSelector }} isRowSelectable, {{ else }} @@ -117,30 +119,48 @@ export function {{ componentName table }}(props: {{ componentName table }}Props) ownerData, editMode, isFormUpdateable, + storeDiff, } = props; + const { t } = useTranslation(); + + const hasRangeCapability = useMemo(() => { + return actionsHasCapability(actions, '{{ table.relationName }}', 'AutocompleteRangeAction'); + }, [actions]); + const hasAutocompleteAddCapability = useMemo(() => { + return actionsHasCapability(actions, '{{ table.relationName }}', 'AutocompleteAddAction'); + }, [actions]); + const hasOpenAddCapability = useMemo(() => { + return actionsHasCapability(actions, '{{ table.relationName }}', 'OpenAddSelectorAction'); + }, [actions]); + const hasCreateCapability = useMemo(() => { + return actionsHasCapability(actions, '{{ table.relationName }}', 'OpenCreateFormAction'); + }, [actions]); + return (
id="{{ getXMIID table }}" - label={'KUKAC'} + label={ {{# if (elementHasLabel table) }}t('{{ getTranslationKeyForVisualElement table }}', { defaultValue: '{{ table.label }}' }){{ else }}'{{ table.relationName }}'{{/ if }} } helperText={validationError} error={!!validationError} editMode={editMode} ownerData={ownerData} name="{{ table.relationName }}" - disabled={!isFormUpdateable()} - autoCompleteAttribute="{{ table.columns.[0].attributeType.name }}" - onAutoCompleteSearch={ async (searchText, preparedQueryCustomizer) => { - alert('lol'); - return Promise.resolve([]); - } } - onValueChange={ async (values) => { - console.log(values); - } } - onItemClick={(target) => alert('A')} - onSearchDialogsClick={() => alert('B')} - onClearDialogsClick={() => alert('C')} + disabled={ {{# if table.enabledBy }}!ownerData.{{ table.enabledBy.name }} || {{/ if }}isOwnerLoading || !isFormUpdateable()} + readOnly={ {{ boolValue table.relationType.isReadOnly }} || !isFormUpdateable() } + {{# with (getFirstAutocompleteColumnForTable table) as |column| }} + autoCompleteAttribute="{{ column.attributeType.name }}" + {{/ with }} + onAutoCompleteSearch={ hasRangeCapability ? async (searchText, preparedQueryCustomizer) => { + const values = await (actions as any)[createActionName('{{ table.relationName }}', 'AutocompleteRangeAction')](preparedQueryCustomizer); + return values; + } : undefined } + onSearchDialogsClick={hasOpenAddCapability ? () => (actions as any)[createActionName('{{ table.relationName }}', 'OpenAddSelectorAction')]() : undefined} + onValueChange={async (values) => storeDiff('{{ table.relationName }}', values)} + onItemClick={actions.{{ table.relationName }}OpenPageAction ? (target) => actions.{{ table.relationName }}OpenPageAction!(target) : undefined} + onCreateDialogsClick={hasCreateCapability ? () => (actions as any)[createActionName('{{ table.relationName }}', 'OpenCreateFormAction')]() : undefined} + onClearDialogsClick={() => storeDiff('{{ table.relationName }}', [])} identifierAttribute="__identifier" />
diff --git a/judo-ui-react/src/main/resources/actor/src/containers/components/tag/types.ts.hbs b/judo-ui-react/src/main/resources/actor/src/containers/components/tag/types.ts.hbs index 9b520054..e532dc55 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/components/tag/types.ts.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/components/tag/types.ts.hbs @@ -18,6 +18,12 @@ import type { DialogResult } from '~/utilities'; export interface {{ componentName table }}ActionDefinitions { get{{ firstToUpper table.relationName }}Mask?: () => string; get{{ firstToUpper table.relationName }}RowRepresentation?: (row: {{ classDataName (getReferenceClassType table) 'Stored' }}) => string; + {{# if table.autocompleteRangeActionDefinition }} + {{ simpleActionDefinitionName table.autocompleteRangeActionDefinition }}?: (queryCustomizer: {{ classDataName (getReferenceClassType table) 'QueryCustomizer' }}) => Promise<{{ classDataName (getReferenceClassType table) 'Stored' }}[]>; + {{/ if }} + {{# if table.autocompleteAddActionDefinition }} + {{ simpleActionDefinitionName table.autocompleteAddActionDefinition }}?: (values: {{ classDataName (getReferenceClassType table) 'Stored' }}[]) => Promise; + {{/ if }} {{# each table.tableActionDefinitions as |actionDefinition| }} {{# if actionDefinition.isFilterAction }} {{ simpleActionDefinitionName actionDefinition }}?: (id: string, filterOptions: FilterOption[], model?: GridFilterModel, filters?: Filter[]) => Promise<{ model?: GridFilterModel; filters?: Filter[] }>; @@ -69,4 +75,5 @@ export interface {{ componentName table }}Props { ownerData: {{ classDataName container.dataElement 'Stored' }}; editMode: boolean; isFormUpdateable: () => boolean; + storeDiff: (attributeName: keyof {{ classDataName container.dataElement '' }}, value: any) => void, } diff --git a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/table.hbs b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/table.hbs index 2136a6eb..e3831a51 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/table.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/table.hbs @@ -54,7 +54,18 @@ isOwnerLoading={isLoading} /> {{ else }} - {'LMAO'} + <{{ componentName child }} + uniqueId={'{{ getXMIID child }}'} + actions={actions} + dataPath={dataPath ? (dataPath + '.{{ child.dataElement.name }}') : '{{ child.dataElement.name }}'} + isOwnerLoading={isLoading} + isDraft={isDraft} + validationError={validation.get('{{ child.dataElement.name }}')} + ownerData={data} + editMode={editMode} + isFormUpdateable={isFormUpdateable} + storeDiff={storeDiff} + /> {{/ unless }} diff --git a/judo-ui-react/src/main/resources/actor/src/pages/actions/AutocompleteAddAction.fragment.hbs b/judo-ui-react/src/main/resources/actor/src/pages/actions/AutocompleteAddAction.fragment.hbs new file mode 100644 index 00000000..0570013f --- /dev/null +++ b/judo-ui-react/src/main/resources/actor/src/pages/actions/AutocompleteAddAction.fragment.hbs @@ -0,0 +1,13 @@ +// AutocompleteAddAction: {{ getXMIID action }} +const {{ simpleActionDefinitionName action.actionDefinition }} = async (values: {{ classDataName action.actionDefinition.targetType 'Stored' }}[]) => { + {{# with (getTableParentForActionDefinition action.actionDefinition) as |table| }} + try { + {{# if table.isEager }} + storeDiff('{{ table.relationName }}', values); + {{/ if }} + } catch (error) { + handleError(error); + return Promise.reject(error); + } + {{/ with }} +}; diff --git a/judo-ui-react/src/main/resources/actor/src/pages/actions/AutocompleteRangeAction.fragment.hbs b/judo-ui-react/src/main/resources/actor/src/pages/actions/AutocompleteRangeAction.fragment.hbs index 334987f3..26facf18 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/actions/AutocompleteRangeAction.fragment.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/actions/AutocompleteRangeAction.fragment.hbs @@ -9,4 +9,13 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async (queryCus return Promise.resolve([]); } {{/ with }} + {{# with (getTableParentForActionDefinition action.actionDefinition) as |table| }} + try { + const { data: result } = await {{ getServiceImplForPage page }}.getRange{{# if (isActionOnOperationInput action) }}On{{ firstToUpper (getOperationNameForActionOnInput action) }}{{/ if }}For{{ firstToUpper table.relationName }}(data, queryCustomizer); + return result; + } catch (error: any) { + handleError(error); + return Promise.resolve([]); + } + {{/ with }} }; diff --git a/judo-ui-react/src/main/resources/actor/src/utilities/filter-helper.ts.hbs b/judo-ui-react/src/main/resources/actor/src/utilities/filter-helper.ts.hbs index edf58eb1..323bade9 100644 --- a/judo-ui-react/src/main/resources/actor/src/utilities/filter-helper.ts.hbs +++ b/judo-ui-react/src/main/resources/actor/src/utilities/filter-helper.ts.hbs @@ -6,7 +6,7 @@ import { isEqual, compareAsc } from 'date-fns'; import type { Filter, FilterOption, Operation } from '../components-api'; import { FilterType } from '../components-api'; import type { Serializer } from '~/services/data-api/common/Serializer'; -import { simpleCloneDeep } from './helper'; +import { simpleCloneDeep, ucFirst } from './helper'; type FilterBy = { value: any; @@ -101,11 +101,6 @@ export const isFilterWithoutValue = (filter: Filter) => { return ['isNotEmpty', 'isEmpty'].includes(getOperationEnumValue(filter, filter.filterBy.operator)); }; -export const ucFirst = (str: string): string => { - if (!str) return str; - return str.charAt(0).toUpperCase() + str.slice(1); -} - export const mapFiltersToQueryCustomizerProperty = (filters: Filter[], property: string): FilterBy[] | undefined => { if (!filters.some((filter) => filter.filterOption.attributeName === property)) return undefined; diff --git a/judo-ui-react/src/main/resources/actor/src/utilities/helper.ts.hbs b/judo-ui-react/src/main/resources/actor/src/utilities/helper.ts.hbs index 461f5329..0c0fc49e 100644 --- a/judo-ui-react/src/main/resources/actor/src/utilities/helper.ts.hbs +++ b/judo-ui-react/src/main/resources/actor/src/utilities/helper.ts.hbs @@ -12,25 +12,42 @@ import type { RandomUtils } from './interfaces'; export const exists = (element: any) => element !== undefined && element !== null; -export const simpleCloneDeep = (input: T): T => { +export const ucFirst = (str: string): string => { + if (!str) return str; + return str.charAt(0).toUpperCase() + str.slice(1); +} + +export const lcFirst = (str: string): string => { + if (!str) return str; + return str.charAt(0).toLowerCase() + str.slice(1); +} + +export const simpleCloneDeep = (input: T, excludeKeys: string[] = []): T => { if (input === null || input === undefined) { return input; } else if (Array.isArray(input)) { - return input.map(simpleCloneDeep) as T; + return input.map((v) => simpleCloneDeep(v, excludeKeys)) as T; } else if (input instanceof Date) { return input; } else if (input instanceof Set) { - return new Set(Array.from(input).map(simpleCloneDeep)) as T; + return new Set(Array.from(input).map((v) => simpleCloneDeep(v, excludeKeys))) as T; } else if (typeof input === 'object') { let payload: Record = {}; for (const key in input) { - payload[key] = simpleCloneDeep(input[key]); + if (!excludeKeys.includes(key)) { + payload[key] = simpleCloneDeep(input[key], excludeKeys); + } } return payload as T; } return input; }; +export const deepEquals = (a: any, b: any, excludeKeys: string[] = []) => { + // This is a pretty resource intensive function, consider using other methods for large inputs! + return JSON.stringify(simpleCloneDeep(a, excludeKeys)) === JSON.stringify(simpleCloneDeep(b, excludeKeys)); +}; + export const stringToBooleanSelect = (booleanString?: string | null): boolean | null => { if (!booleanString || !booleanString.trim()) { return null; @@ -136,3 +153,16 @@ export function setValue(target: any, path: string, value: any): void { } } +export type reservedActionSuffixes = 'OpenCreateFormAction' | 'OpenAddSelectorAction' | 'AutocompleteAddAction' | 'AutocompleteRangeAction'; + +export const createActionName = (relationName: string, suffix: reservedActionSuffixes) => { + return relationName + (relationName ? ucFirst(suffix) : lcFirst(suffix)); +}; + +export const actionsHasCapability = (actions: any, relationName: string, suffix: reservedActionSuffixes) => { + try { + return typeof actions[createActionName(relationName, suffix)] === 'function'; + } catch (_) { + return false; + } +}; diff --git a/pom.xml b/pom.xml index 3aef845a..46d386f6 100644 --- a/pom.xml +++ b/pom.xml @@ -55,7 +55,7 @@ 18.14.2 8.9.2 - 1.1.0.20240905_151408_4d3b9bd0_feature_JNG_5914_table_tag_representation + 1.1.0-SNAPSHOT 1.0.0.20240712_085217_8319ce27_develop 1.0.0.20240823_165725_58575058_feature_JNG_5888_filter_context