From 304d7ffb8afac385db53ecdeeb45c44bd778cc84 Mon Sep 17 00:00:00 2001 From: NicoPaladiniAries Date: Wed, 18 Jun 2025 11:41:54 -0300 Subject: [PATCH 1/3] AR-1223: custom obj modal --- .../custom-object-form/attribute-field.tsx | 64 ++++++-- .../custom-objects-modal.tsx | 149 ++++++++++++++++++ 2 files changed, 201 insertions(+), 12 deletions(-) create mode 100644 src/components/custom-object-form/custom-objects-modal.tsx diff --git a/src/components/custom-object-form/attribute-field.tsx b/src/components/custom-object-form/attribute-field.tsx index d4e3745..cb0c3d3 100644 --- a/src/components/custom-object-form/attribute-field.tsx +++ b/src/components/custom-object-form/attribute-field.tsx @@ -1,4 +1,4 @@ -import { FC } from 'react'; +import { FC, useState } from 'react'; import get from 'lodash/get'; import { useIntl } from 'react-intl'; import { FieldArray } from 'formik'; @@ -7,7 +7,11 @@ import SecondaryButton from '@commercetools-uikit/secondary-button'; import SecondaryIconButton from '@commercetools-uikit/secondary-icon-button'; import Card from '@commercetools-uikit/card'; import Constraints from '@commercetools-uikit/constraints'; -import { BinLinearIcon, PlusBoldIcon } from '@commercetools-uikit/icons'; +import { + BinLinearIcon, + PlusBoldIcon, + SearchIcon, +} from '@commercetools-uikit/icons'; import Spacings from '@commercetools-uikit/spacings'; import { closestCenter, @@ -30,6 +34,7 @@ import AttributeLabel from './attribute-label'; import AttributeInput from './attribute-input'; import messages from './messages'; import { SortableItem } from './sortable-item'; +import { CustomObjectsModal } from './custom-objects-modal'; import styles from './attribute-field.module.css'; type Props = { @@ -73,6 +78,8 @@ const AttributeField: FC = ({ dataLocale: context.dataLocale ?? '', }) ); + const [isSearchOpen, setSearchOpen] = useState(false); + const emptyValue = getValueByType( type, attributes, @@ -83,18 +90,16 @@ const AttributeField: FC = ({ const selectOptions = type === TYPES.LocalizedEnum ? options?.map((option) => { - return { - value: option.value, - label: option.label[dataLocale], - }; - }) + return { + value: option.value, + label: option.label[dataLocale], + }; + }) : options; const sensors = useSensors( useSensor(PointerSensor), - useSensor(KeyboardSensor, { - coordinateGetter: sortableKeyboardCoordinates, - }) + useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) ); return ( @@ -103,6 +108,31 @@ const AttributeField: FC = ({ { + const handleSelect = (selectedId: string) => { + const newItem = { + typeId: 'key-value-document', + key: selectedId, + }; + + // clone to avoid direct mutation + const updatedValue = [...(value || [])]; + + // Find the first empty item + const emptyIndex = updatedValue.findIndex( + (item: any) => item?.key === '' + ); + + if (emptyIndex !== -1) { + // Replace the empty item + updatedValue[emptyIndex] = newItem; + } else { + // No empty item found — push a new one + updatedValue.push(newItem); + } + + form.setFieldValue(name, updatedValue); + setSearchOpen(false); + }; const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (active.id !== over?.id) { @@ -142,10 +172,10 @@ const AttributeField: FC = ({ onDragEnd={handleDragEnd} > `${i}`)} + items={value?.map((_: any, i: number) => `${i}`)} strategy={verticalListSortingStrategy} > - {value.map((val: any, index: number) => ( + {value?.map((val: any, index: number) => ( = ({ options={selectOptions} /> + } + label="Search" + onClick={() => setSearchOpen(true)} + /> } @@ -184,6 +219,11 @@ const AttributeField: FC = ({ ))} + setSearchOpen(false)} + handleSelect={handleSelect} + /> ); }} diff --git a/src/components/custom-object-form/custom-objects-modal.tsx b/src/components/custom-object-form/custom-objects-modal.tsx new file mode 100644 index 0000000..b106446 --- /dev/null +++ b/src/components/custom-object-form/custom-objects-modal.tsx @@ -0,0 +1,149 @@ +import { useState } from 'react'; +import { + InfoModalPage, + PageNotFound, +} from '@commercetools-frontend/application-components'; +import LoadingSpinner from '@commercetools-uikit/loading-spinner'; +import { + useDataTableSortingState, + usePaginationState, +} from '@commercetools-uikit/hooks'; +import Grid from '@commercetools-uikit/grid'; +import { customProperties } from '@commercetools-uikit/design-system'; +import Card from '@commercetools-uikit/card'; +import Spacings from '@commercetools-uikit/spacings'; +import Text from '@commercetools-uikit/text'; +import { FormattedMessage, useIntl } from 'react-intl'; +import Constraints from '@commercetools-uikit/constraints'; +import SelectInput from '@commercetools-uikit/select-input'; +import map from 'lodash/map'; +import { useCustomObjectsFetcher } from '../../hooks/use-custom-object-connector/use-custom-object-connector'; +import messages from '../container-list/messages'; +import customObjectsMessages from '../custom-objects-list/messages'; +import { useContainerContext } from '../../context/container-context'; +import TextFilter from '../custom-objects-list/text-filter'; + +export const CustomObjectsModal = ({ + isOpen, + close, + handleSelect, +}: { + isOpen: boolean; + close: () => void; + handleSelect: (value: string) => void; +}) => { + const { hasContainers, containers } = useContainerContext(); + const intl = useIntl(); + + const { page, perPage } = usePaginationState(); + const [container, setContainer] = useState( + containers.map((item) => item.key)[0] || '' + ); + const [key, setKey] = useState(''); + const tableSorting = useDataTableSortingState({ key: 'key', order: 'asc' }); + + const { customObjectsPaginatedResult, loading, error, refetch } = + useCustomObjectsFetcher({ + limit: perPage.value, + offset: (page.value - 1) * perPage.value, + sort: [`${tableSorting.value.key} ${tableSorting.value.order}`], + container: container, + where: key && key !== '' ? `key="${key}"` : undefined, + }); + + const containerOptions = map(containers, ({ key: containerKey }) => ({ + label: containerKey, + value: containerKey, + })); + + function filterByContainer(event: any) { + const { value } = event.target; + setContainer(value); + } + + const handleSelection = (value: string) => { + handleSelect(value); + }; + + const modalContent = () => { + if ( + !loading && + !hasContainers && + (!customObjectsPaginatedResult || !customObjectsPaginatedResult.results) + ) { + return ; + } + + if (loading) { + return ; + } + + return ( + + + + + + + + + + + + + + {customObjectsPaginatedResult?.results && + customObjectsPaginatedResult?.results.map(({ id, key, value }) => { + return ( + handleSelection(id)}> + + + {key} + + + )?.length, + }} + {...messages.attributesLabel} + /> + + + + ); + })} + + + ); + }; + + return ( + + {modalContent()} + + ); +}; From fc94f99b5325b3ec4681cfcad964d52c44bb3aad Mon Sep 17 00:00:00 2001 From: NicoPaladiniAries Date: Wed, 18 Jun 2025 12:47:45 -0300 Subject: [PATCH 2/3] AR-1224: export render obj function --- .../custom-objects-list.tsx | 72 +------------------ .../custom-objects-list/render-object.tsx | 70 ++++++++++++++++++ 2 files changed, 72 insertions(+), 70 deletions(-) create mode 100644 src/components/custom-objects-list/render-object.tsx diff --git a/src/components/custom-objects-list/custom-objects-list.tsx b/src/components/custom-objects-list/custom-objects-list.tsx index ce0c3de..878c606 100644 --- a/src/components/custom-objects-list/custom-objects-list.tsx +++ b/src/components/custom-objects-list/custom-objects-list.tsx @@ -1,12 +1,10 @@ import { lazy, useState } from 'react'; -import { FormattedDate, FormattedMessage, useIntl } from 'react-intl'; +import { FormattedMessage, useIntl } from 'react-intl'; import camelCase from 'lodash/camelCase'; import includes from 'lodash/includes'; import isEmpty from 'lodash/isEmpty'; import isPlainObject from 'lodash/isPlainObject'; -import isString from 'lodash/isString'; import map from 'lodash/map'; -import startCase from 'lodash/startCase'; import LoadingSpinner from '@commercetools-uikit/loading-spinner'; import { ContentNotification } from '@commercetools-uikit/notifications'; import SpacingsStack from '@commercetools-uikit/spacings-stack'; @@ -41,6 +39,7 @@ import { columnDefinitions, COLUMN_KEYS } from './column-definitions'; import messages from './messages'; import styles from './custom-objects-list.module.css'; import TextFilter from './text-filter'; +import { renderObject } from './render-object'; const CreateCustomObject = lazy(() => import('../create-custom-object')); @@ -96,73 +95,6 @@ const CustomObjectsList = () => { return ; } - function renderValue(value: any) { - if (isPlainObject(value)) { - return ( -
- {renderObject(value)} -
- ); - } - - if (Array.isArray(value)) { - return ( -
- {map(value, (val, index) => ( -
- {renderValue(val)} -
- ))} -
- ); - } - - const dateRegex = /\d{4}-\d{2}-\d{2}/; - if (isString(value) && value.match(dateRegex)) { - return value.indexOf('T') >= 0 ? ( - - ) : ( - - ); - } - - return value.toString(); - } - - function renderObject(value: { [key: string]: unknown }) { - const result = Object.entries(value).map(([key, value]) => { - return ( -
- - {startCase(key)}: - -   - {renderValue(value)} -
- ); - }); - - return result; - } - function getDisplayAttributes( attributes: Array ): Array { diff --git a/src/components/custom-objects-list/render-object.tsx b/src/components/custom-objects-list/render-object.tsx new file mode 100644 index 0000000..915a888 --- /dev/null +++ b/src/components/custom-objects-list/render-object.tsx @@ -0,0 +1,70 @@ +import { FormattedDate } from 'react-intl'; +import isPlainObject from 'lodash/isPlainObject'; +import isString from 'lodash/isString'; +import map from 'lodash/map'; +import startCase from 'lodash/startCase'; +import Text from '@commercetools-uikit/text'; +import styles from './custom-objects-list.module.css'; + +function renderValue(value: any) { + if (isPlainObject(value)) { + return ( +
+ {renderObject(value)} +
+ ); + } + + if (Array.isArray(value)) { + return ( +
+ {map(value, (val, index) => ( +
+ {renderValue(val)} +
+ ))} +
+ ); + } + + const dateRegex = /\d{4}-\d{2}-\d{2}/; + if (isString(value) && value.match(dateRegex)) { + return value.indexOf('T') >= 0 ? ( + + ) : ( + + ); + } + + return value.toString(); +} + +export function renderObject(value: { [key: string]: unknown }) { + const result = Object.entries(value).map(([key, value]) => { + return ( +
+ + {startCase(key)}: + +   + {renderValue(value)} +
+ ); + }); + + return result; +} From 79136397d2114d85af8458818627d2ceca02f090 Mon Sep 17 00:00:00 2001 From: NicoPaladiniAries Date: Wed, 18 Jun 2025 12:48:29 -0300 Subject: [PATCH 3/3] AR-1224: custom obj preview --- .../custom-object-form/attribute-field.tsx | 38 ++++---- .../custom-objects-modal.tsx | 90 +++++++++++++++---- 2 files changed, 91 insertions(+), 37 deletions(-) diff --git a/src/components/custom-object-form/attribute-field.tsx b/src/components/custom-object-form/attribute-field.tsx index cb0c3d3..fa609f7 100644 --- a/src/components/custom-object-form/attribute-field.tsx +++ b/src/components/custom-object-form/attribute-field.tsx @@ -9,6 +9,7 @@ import Card from '@commercetools-uikit/card'; import Constraints from '@commercetools-uikit/constraints'; import { BinLinearIcon, + EyeIcon, PlusBoldIcon, SearchIcon, } from '@commercetools-uikit/icons'; @@ -78,7 +79,7 @@ const AttributeField: FC = ({ dataLocale: context.dataLocale ?? '', }) ); - const [isSearchOpen, setSearchOpen] = useState(false); + const [searchModalIndex, setSearchModalIndex] = useState(null); const emptyValue = getValueByType( type, @@ -89,12 +90,10 @@ const AttributeField: FC = ({ ); const selectOptions = type === TYPES.LocalizedEnum - ? options?.map((option) => { - return { - value: option.value, - label: option.label[dataLocale], - }; - }) + ? options?.map((option) => ({ + value: option.value, + label: option.label[dataLocale], + })) : options; const sensors = useSensors( @@ -114,25 +113,21 @@ const AttributeField: FC = ({ key: selectedId, }; - // clone to avoid direct mutation const updatedValue = [...(value || [])]; - - // Find the first empty item const emptyIndex = updatedValue.findIndex( (item: any) => item?.key === '' ); if (emptyIndex !== -1) { - // Replace the empty item updatedValue[emptyIndex] = newItem; } else { - // No empty item found — push a new one updatedValue.push(newItem); } form.setFieldValue(name, updatedValue); - setSearchOpen(false); + setSearchModalIndex(null); }; + const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (active.id !== over?.id) { @@ -202,9 +197,9 @@ const AttributeField: FC = ({ /> } + icon={val.key ? : } label="Search" - onClick={() => setSearchOpen(true)} + onClick={() => setSearchModalIndex(index)} /> = ({ ))} - setSearchOpen(false)} - handleSelect={handleSelect} - /> + {searchModalIndex !== null && ( + setSearchModalIndex(null)} + handleSelect={handleSelect} + objectId={value?.[searchModalIndex]?.key} + /> + )} ); }} diff --git a/src/components/custom-object-form/custom-objects-modal.tsx b/src/components/custom-object-form/custom-objects-modal.tsx index b106446..3afd6ca 100644 --- a/src/components/custom-object-form/custom-objects-modal.tsx +++ b/src/components/custom-object-form/custom-objects-modal.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { InfoModalPage, PageNotFound, @@ -17,23 +17,34 @@ import { FormattedMessage, useIntl } from 'react-intl'; import Constraints from '@commercetools-uikit/constraints'; import SelectInput from '@commercetools-uikit/select-input'; import map from 'lodash/map'; -import { useCustomObjectsFetcher } from '../../hooks/use-custom-object-connector/use-custom-object-connector'; +import SecondaryButton from '@commercetools-uikit/secondary-button'; +import { EditIcon } from '@commercetools-uikit/icons'; +import { useHistory, useRouteMatch } from 'react-router-dom'; +import { + useCustomObjectsFetcher, + useCustomObjectFetcher, +} from '../../hooks/use-custom-object-connector/use-custom-object-connector'; import messages from '../container-list/messages'; import customObjectsMessages from '../custom-objects-list/messages'; import { useContainerContext } from '../../context/container-context'; import TextFilter from '../custom-objects-list/text-filter'; +import { renderObject } from '../custom-objects-list/render-object'; export const CustomObjectsModal = ({ isOpen, close, handleSelect, + objectId, }: { isOpen: boolean; close: () => void; handleSelect: (value: string) => void; + objectId?: string; }) => { const { hasContainers, containers } = useContainerContext(); const intl = useIntl(); + const match = useRouteMatch(); + const { replace } = useHistory(); const { page, perPage } = usePaginationState(); const [container, setContainer] = useState( @@ -42,14 +53,30 @@ export const CustomObjectsModal = ({ const [key, setKey] = useState(''); const tableSorting = useDataTableSortingState({ key: 'key', order: 'asc' }); - const { customObjectsPaginatedResult, loading, error, refetch } = - useCustomObjectsFetcher({ - limit: perPage.value, - offset: (page.value - 1) * perPage.value, - sort: [`${tableSorting.value.key} ${tableSorting.value.order}`], - container: container, - where: key && key !== '' ? `key="${key}"` : undefined, - }); + // FETCH FOR SINGLE OBJECT (IF objectId provided) + const { + customObject, + loading: singleLoading, + error: singleError, + refetch: refetchSingleObject, + } = useCustomObjectFetcher({ + id: objectId, + }); + + useEffect(() => { + if (objectId) { + refetchSingleObject(); + } + }, [objectId, refetchSingleObject]); + + // FETCH FOR LIST (if no objectId) + const { customObjectsPaginatedResult, loading } = useCustomObjectsFetcher({ + limit: perPage.value, + offset: (page.value - 1) * perPage.value, + sort: [`${tableSorting.value.key} ${tableSorting.value.order}`], + container: container, + where: key && key !== '' ? `key="${key}"` : undefined, + }); const containerOptions = map(containers, ({ key: containerKey }) => ({ label: containerKey, @@ -65,7 +92,34 @@ export const CustomObjectsModal = ({ handleSelect(value); }; - const modalContent = () => { + const renderSingleObject = () => { + if (singleLoading) return ; + if (singleError || !customObject) return ; + + return ( +
+ {renderObject(customObject)} + + + } + as="a" + onClick={() => { + const baseUrl = match.url.split('/').slice(0, -1).join('/'); + const newUrl = `${baseUrl}/${customObject.id}`; + + replace(newUrl); + }} + label="Edit Custom Object" + /> + + +
+ ); + }; + + const renderList = () => { if ( !loading && !hasContainers && @@ -74,9 +128,7 @@ export const CustomObjectsModal = ({ return ; } - if (loading) { - return ; - } + if (loading) return ; return ( @@ -138,12 +190,16 @@ export const CustomObjectsModal = ({ return ( - {modalContent()} + {objectId ? renderSingleObject() : renderList()} ); };