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
4 changes: 4 additions & 0 deletions src/components/container-form/attribute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ const typeOptions = [
label: <FormattedMessage {...messages.richTextLabel} />,
value: TYPES.RichText,
},
{
label: <FormattedMessage {...messages.assetLabel} />,
value: TYPES.Asset,
},
];

type Props = {
Expand Down
5 changes: 5 additions & 0 deletions src/components/container-form/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,4 +192,9 @@ export default defineMessages({
description: 'Label for attributes rich text value',
defaultMessage: 'Rich Text',
},
assetLabel: {
id: 'Container.form.type.label.asset',
description: 'Asset type label',
defaultMessage: 'Asset',
},
});
123 changes: 123 additions & 0 deletions src/components/custom-object-form/asset-input/asset-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// src/components/attribute-input/asset-input.js

import Spacings from '@commercetools-uikit/spacings';
import TextInput from '@commercetools-uikit/text-input';
import LocalizedTextInput from '@commercetools-uikit/localized-text-input';
import { useApplicationContext } from '@commercetools-frontend/application-shell-connectors';
import Text from '@commercetools-uikit/text';
import { FC } from 'react';
import SourceArrayInput from './source-array-input';
import { Asset, LocalizedString, Source } from './types';

type Props = {
name: string;
value?: any;
touched?: any;
errors?: any;
onChange: (...args: any[]) => void;
onBlur: (...args: any[]) => void;
};

const AssetInput: FC<Props> = ({
name,
value = {},
onChange,
touched,
errors,
}) => {
const { dataLocale } = useApplicationContext((context) => ({
dataLocale: context.dataLocale ?? '',
}));

const triggerChange = (updatedValue: Partial<Asset>) => {
onChange({ target: { name, value: updatedValue } });
};

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name: fieldName, value: fieldValue } = e.target;
triggerChange({ ...value, [fieldName]: fieldValue });
};

const handleLocalizedChange = (
localizedValue: LocalizedString,
fieldName: string
) => {
triggerChange({ ...value, [fieldName]: localizedValue });
};

const handleSourcesChange = (sources: Source[]) => {
triggerChange({ ...value, sources });
};

const handleTagsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newTags = e.target.value
? e.target.value.split(',').map((tag) => tag.trim())
: [];
triggerChange({ ...value, tags: newTags });
};

return (
<Spacings.Stack scale="l">
<TextInput
name="key"
placeholder="Key (publicId)"
value={value?.key || ''}
onChange={handleChange}
/>
<Text.Body>
<span className="text" style={{ margin: '0px' }}>
Name:
</span>
</Text.Body>
<LocalizedTextInput
name="name"
placeholder={'Asset Name' as unknown as Record<string, string>}
value={value?.name || {}}
selectedLanguage={dataLocale}
onChange={(event) =>
handleLocalizedChange(
event.target.value as unknown as LocalizedString,
'name'
)
}
hasError={!!(LocalizedTextInput.isTouched(touched) && errors)}
/>
<Text.Body>Description:</Text.Body>
<LocalizedTextInput
name="description"
placeholder={'Asset Description' as unknown as Record<string, string>}
value={value?.description || {}}
selectedLanguage={dataLocale}
onChange={(event) =>
handleLocalizedChange(
event.target.value as unknown as LocalizedString,
'description'
)
}
hasError={!!(LocalizedTextInput.isTouched(touched) && errors)}
/>
<TextInput
name="tags"
placeholder="Tags (comma-separated)"
value={value?.tags?.join(', ') || ''}
onChange={handleTagsChange}
/>
<TextInput
name="folder"
placeholder="Folder"
value={value?.folder || ''}
onChange={handleChange}
/>

<Spacings.Stack scale="s">
<Text.Headline>Sources</Text.Headline>
<SourceArrayInput
value={value?.sources || []}
onChange={handleSourcesChange}
/>
</Spacings.Stack>
</Spacings.Stack>
);
};

export default AssetInput;
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// src/components/attribute-input/source-array-input.tsx

import React from 'react';
import Spacings from '@commercetools-uikit/spacings';
import SecondaryButton from '@commercetools-uikit/secondary-button';
import Constraints from '@commercetools-uikit/constraints';
import SourceInput from './source-input';
import type { Source } from './types';

type Props = {
value: Source[];
onChange: (value: Source[]) => void;
};

const SourceArrayInput: React.FC<Props> = ({ value = [], onChange }) => {
const handleItemChange = (index: number, itemValue: Source) => {
const newSources = value.map((item, i) => (i === index ? itemValue : item));
onChange(newSources);
};

const handleAddItem = () => {
const newSources = [...value, { key: '', uri: '', contentType: '' }];
onChange(newSources);
};

const handleRemoveItem = (index: number) => {
const newSources = value.filter((_, i) => i !== index);
onChange(newSources);
};

return (
<Spacings.Stack scale="m">
{value.map((source, index) => (
<SourceInput
key={index}
index={index}
value={source}
onChange={handleItemChange}
onRemove={handleRemoveItem}
/>
))}
<Constraints.Horizontal max="scale">
<SecondaryButton
size="small"
label="Add Source"
onClick={handleAddItem}
/>
</Constraints.Horizontal>
</Spacings.Stack>
);
};

export default SourceArrayInput;
83 changes: 83 additions & 0 deletions src/components/custom-object-form/asset-input/source-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// src/components/attribute-input/source-input.tsx

import React from 'react';
import Grid from '@commercetools-uikit/grid';
import TextInput from '@commercetools-uikit/text-input';
import NumberInput from '@commercetools-uikit/number-input';
import { CloseIcon } from '@commercetools-uikit/icons';
import Text from '@commercetools-uikit/text';
import SecondaryButton from '@commercetools-uikit/secondary-button';
import Spacings from '@commercetools-uikit/spacings';
import Card from '@commercetools-uikit/card';
import type { Source } from './types';

type Props = {
value: Source;
index: number;
onChange: (index: number, value: Source) => void;
onRemove: (index: number) => void;
};

const SourceInput: React.FC<Props> = ({ value, index, onChange, onRemove }) => {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value: fieldValue } = e.target;
onChange(index, { ...value, [name]: fieldValue });
};

const handleNumberChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value: fieldValue } = e.target;
onChange(index, {
...value,
[name]: fieldValue ? parseInt(fieldValue, 10) : null,
});
};

return (
<Card type='raised'>
<Spacings.Inline justifyContent="space-between" alignItems="center">
<Text.Subheadline as="h4">Sources {index + 1}</Text.Subheadline>
<SecondaryButton
iconLeft={<CloseIcon size="medium" />}
label="Remove Source"
size="small"
data-testid={`remove-source-${index}`}
onClick={() => onRemove(index)}
/>
</Spacings.Inline>
<Grid gridGap="16px" gridTemplateColumns="repeat(2, 1fr)">
<TextInput
name="key"
placeholder="Source Key"
value={value.key || ''}
onChange={handleChange}
/>
<TextInput
name="uri"
placeholder="Source URI"
value={value.uri || ''}
onChange={handleChange}
/>
<TextInput
name="contentType"
placeholder="Content Type (e.g., image/jpeg)"
value={value.contentType || ''}
onChange={handleChange}
/>
<NumberInput
name="width"
placeholder="Width"
value={value.width || ''}
onChange={handleNumberChange}
/>
<NumberInput
name="height"
placeholder="Height"
value={value.height || ''}
onChange={handleNumberChange}
/>
</Grid>
</Card>
);
};

export default SourceInput;
18 changes: 18 additions & 0 deletions src/components/custom-object-form/asset-input/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export type LocalizedString = Record<string, string>;

export interface Source {
key: string;
uri: string;
contentType: string;
width?: number | null;
height?: number | null;
}

export interface Asset {
key?: string;
name: LocalizedString;
description?: LocalizedString;
tags?: string[];
folder?: string;
sources?: Source[];
}
24 changes: 23 additions & 1 deletion src/components/custom-object-form/attribute-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { TYPES } from '../../constants';
import nestedStyles from '../container-form/nested-attributes.module.css';
import AttributeField from './attribute-field'; // eslint-disable-line import/no-cycle
import LexicalEditorField from './lexical-editor-field';
import AssetInput from './asset-input/asset-input';

type Props = {
type: string;
Expand Down Expand Up @@ -271,7 +272,9 @@ const AttributeInput: FC<Props> = ({
}}
/>
{touched && errors && (
<ErrorMessage data-testid="field-error-richtext">{errors}</ErrorMessage>
<ErrorMessage data-testid="field-error-richtext">
{errors}
</ErrorMessage>
)}
</Spacings.Stack>
);
Expand Down Expand Up @@ -307,6 +310,25 @@ const AttributeInput: FC<Props> = ({
</div>
);

case TYPES.Asset:
return (
<Spacings.Stack scale="xs">
<AssetInput
name={name}
value={value}
touched={touched}
errors={errors}
onChange={onChange}
onBlur={onBlur}
/>
{touched && errors && (
<ErrorMessage data-testid="field-error">
{typeof errors === 'string' ? errors : 'Invalid Asset data'}
</ErrorMessage>
)}
</Spacings.Stack>
);

default:
return null;
}
Expand Down
2 changes: 2 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const TYPES = {
Object: 'Object',
Reference: 'Reference',
RichText: 'RichText',
Asset: 'Asset',
};

export enum TYPES_ENUM {
Expand All @@ -53,6 +54,7 @@ export enum TYPES_ENUM {
Object = 'Object',
Reference = 'Reference',
RichText = 'RichText',
Asset= 'Asset'
}

export const REFERENCE_BY = {
Expand Down
Loading