Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 51 additions & 13 deletions src/components/custom-object-form/attribute-field.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -7,7 +7,12 @@ 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,
EyeIcon,
PlusBoldIcon,
SearchIcon,
} from '@commercetools-uikit/icons';
import Spacings from '@commercetools-uikit/spacings';
import {
closestCenter,
Expand All @@ -30,6 +35,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 = {
Expand Down Expand Up @@ -73,6 +79,8 @@ const AttributeField: FC<Props> = ({
dataLocale: context.dataLocale ?? '',
})
);
const [searchModalIndex, setSearchModalIndex] = useState<number | null>(null);

const emptyValue = getValueByType(
type,
attributes,
Expand All @@ -82,19 +90,15 @@ const AttributeField: FC<Props> = ({
);
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(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
);

return (
Expand All @@ -103,6 +107,27 @@ const AttributeField: FC<Props> = ({
<FieldArray
name={name}
render={({ push, remove, form }) => {
const handleSelect = (selectedId: string) => {
const newItem = {
typeId: 'key-value-document',
key: selectedId,
};

const updatedValue = [...(value || [])];
const emptyIndex = updatedValue.findIndex(
(item: any) => item?.key === ''
);

if (emptyIndex !== -1) {
updatedValue[emptyIndex] = newItem;
} else {
updatedValue.push(newItem);
}

form.setFieldValue(name, updatedValue);
setSearchModalIndex(null);
};

const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over?.id) {
Expand Down Expand Up @@ -142,10 +167,10 @@ const AttributeField: FC<Props> = ({
onDragEnd={handleDragEnd}
>
<SortableContext
items={value.map((_: any, i: number) => `${i}`)}
items={value?.map((_: any, i: number) => `${i}`)}
strategy={verticalListSortingStrategy}
>
{value.map((val: any, index: number) => (
{value?.map((val: any, index: number) => (
<SortableItem key={index} id={`${index}`}>
<Card
theme={isNestedSet ? 'light' : 'dark'}
Expand All @@ -171,6 +196,11 @@ const AttributeField: FC<Props> = ({
options={selectOptions}
/>
</div>
<SecondaryIconButton
icon={val.key ? <EyeIcon /> : <SearchIcon />}
label="Search"
onClick={() => setSearchModalIndex(index)}
/>
<SecondaryIconButton
data-testid={`remove-attribute-${index}`}
icon={<BinLinearIcon />}
Expand All @@ -184,6 +214,14 @@ const AttributeField: FC<Props> = ({
))}
</SortableContext>
</DndContext>
{searchModalIndex !== null && (
<CustomObjectsModal
isOpen={searchModalIndex !== null}
close={() => setSearchModalIndex(null)}
handleSelect={handleSelect}
objectId={value?.[searchModalIndex]?.key}
/>
)}
</Spacings.Stack>
);
}}
Expand Down
205 changes: 205 additions & 0 deletions src/components/custom-object-form/custom-objects-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import { useEffect, 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 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(
containers.map((item) => item.key)[0] || ''
);
const [key, setKey] = useState('');
const tableSorting = useDataTableSortingState({ key: 'key', order: 'asc' });

// 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,
value: containerKey,
}));

function filterByContainer(event: any) {
const { value } = event.target;
setContainer(value);
}

const handleSelection = (value: string) => {
handleSelect(value);
};

const renderSingleObject = () => {
if (singleLoading) return <LoadingSpinner />;
if (singleError || !customObject) return <PageNotFound />;

return (
<div>
{renderObject(customObject)}
<Spacings.Stack scale="l">
<Spacings.Inline justifyContent="flex-end">
<SecondaryButton
data-testid="edit-custom-object"
iconLeft={<EditIcon />}
as="a"
onClick={() => {
const baseUrl = match.url.split('/').slice(0, -1).join('/');
const newUrl = `${baseUrl}/${customObject.id}`;

replace(newUrl);
}}
label="Edit Custom Object"
/>
</Spacings.Inline>
</Spacings.Stack>
</div>
);
};

const renderList = () => {
if (
!loading &&
!hasContainers &&
(!customObjectsPaginatedResult || !customObjectsPaginatedResult.results)
) {
return <PageNotFound />;
}

if (loading) return <LoadingSpinner />;

return (
<Spacings.Stack scale="l">
<Card theme="dark" type="flat">
<Spacings.Inline scale="m" alignItems="center">
<Text.Body intlMessage={customObjectsMessages.filter} />
<Constraints.Horizontal max={'scale'}>
<SelectInput
data-testid="container-filter"
name="container"
placeholder={intl.formatMessage(
customObjectsMessages.container
)}
value={container}
options={containerOptions}
onChange={filterByContainer}
/>
</Constraints.Horizontal>
<Constraints.Horizontal max={'scale'}>
<TextFilter
placeholder={intl.formatMessage(customObjectsMessages.key)}
value={key}
onChange={setKey}
onSubmit={setKey}
/>
</Constraints.Horizontal>
</Spacings.Inline>
</Card>
<Grid
gridGap={customProperties.spacingM}
gridAutoColumns="1fr"
gridTemplateColumns={`repeat(auto-fill, minmax(${customProperties.constraint6}, 1fr))`}
>
{customObjectsPaginatedResult?.results &&
customObjectsPaginatedResult?.results.map(({ id, key, value }) => {
return (
<Card key={id} theme="dark" onClick={() => handleSelection(id)}>
<Spacings.Inline>
<Text.Body data-testid="container-key" truncate={true}>
{key}
</Text.Body>
<Text.Body>
<FormattedMessage
data-testid="container-attributes"
values={{
total: (value.attributes as Array<any>)?.length,
}}
{...messages.attributesLabel}
/>
</Text.Body>
</Spacings.Inline>
</Card>
);
})}
</Grid>
</Spacings.Stack>
);
};

return (
<InfoModalPage
title={objectId ? String(customObject?.key) : 'Select Custom Object'}
subtitle={
objectId
? 'Custom Object Detail'
: `Results: ${customObjectsPaginatedResult?.total ?? 0}`
}
isOpen={isOpen}
onClose={close}
>
{objectId ? renderSingleObject() : renderList()}
</InfoModalPage>
);
};
Loading
Loading