Skip to content

Commit 40cb688

Browse files
AR-1205: asset input field
1 parent 039eafa commit 40cb688

File tree

5 files changed

+300
-1
lines changed

5 files changed

+300
-1
lines changed
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// src/components/attribute-input/asset-input.js
2+
3+
import Spacings from '@commercetools-uikit/spacings';
4+
import TextInput from '@commercetools-uikit/text-input';
5+
import LocalizedTextInput from '@commercetools-uikit/localized-text-input';
6+
import { useApplicationContext } from '@commercetools-frontend/application-shell-connectors';
7+
import Text from '@commercetools-uikit/text';
8+
import { FC } from 'react';
9+
import SourceArrayInput from './source-array-input';
10+
import { Asset, LocalizedString, Source } from './types';
11+
12+
type Props = {
13+
name: string;
14+
value?: any;
15+
touched?: any;
16+
errors?: any;
17+
onChange: (...args: any[]) => void;
18+
onBlur: (...args: any[]) => void;
19+
};
20+
21+
const AssetInput: FC<Props> = ({
22+
name,
23+
value = {},
24+
onChange,
25+
touched,
26+
errors,
27+
}) => {
28+
const { dataLocale } = useApplicationContext((context) => ({
29+
dataLocale: context.dataLocale ?? '',
30+
}));
31+
32+
const triggerChange = (updatedValue: Partial<Asset>) => {
33+
onChange({ target: { name, value: updatedValue } });
34+
};
35+
36+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
37+
const { name: fieldName, value: fieldValue } = e.target;
38+
triggerChange({ ...value, [fieldName]: fieldValue });
39+
};
40+
41+
const handleLocalizedChange = (
42+
localizedValue: LocalizedString,
43+
fieldName: string
44+
) => {
45+
triggerChange({ ...value, [fieldName]: localizedValue });
46+
};
47+
48+
const handleSourcesChange = (sources: Source[]) => {
49+
triggerChange({ ...value, sources });
50+
};
51+
52+
const handleTagsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
53+
const newTags = e.target.value
54+
? e.target.value.split(',').map((tag) => tag.trim())
55+
: [];
56+
triggerChange({ ...value, tags: newTags });
57+
};
58+
59+
return (
60+
<Spacings.Stack scale="l">
61+
<TextInput
62+
name="key"
63+
placeholder="Key (publicId)"
64+
value={value?.key || ''}
65+
onChange={handleChange}
66+
/>
67+
<Text.Body>
68+
<span className="text" style={{ margin: '0px' }}>
69+
Name:
70+
</span>
71+
</Text.Body>
72+
<LocalizedTextInput
73+
name="name"
74+
placeholder={'Asset Name' as unknown as Record<string, string>}
75+
value={value?.name || {}}
76+
selectedLanguage={dataLocale}
77+
onChange={(event) =>
78+
handleLocalizedChange(
79+
event.target.value as unknown as LocalizedString,
80+
'name'
81+
)
82+
}
83+
hasError={!!(LocalizedTextInput.isTouched(touched) && errors)}
84+
/>
85+
<Text.Body>Description:</Text.Body>
86+
<LocalizedTextInput
87+
name="description"
88+
placeholder={'Asset Description' as unknown as Record<string, string>}
89+
value={value?.description || {}}
90+
selectedLanguage={dataLocale}
91+
onChange={(event) =>
92+
handleLocalizedChange(
93+
event.target.value as unknown as LocalizedString,
94+
'description'
95+
)
96+
}
97+
hasError={!!(LocalizedTextInput.isTouched(touched) && errors)}
98+
/>
99+
<TextInput
100+
name="tags"
101+
placeholder="Tags (comma-separated)"
102+
value={value?.tags?.join(', ') || ''}
103+
onChange={handleTagsChange}
104+
/>
105+
<TextInput
106+
name="folder"
107+
placeholder="Folder"
108+
value={value?.folder || ''}
109+
onChange={handleChange}
110+
/>
111+
112+
<Spacings.Stack scale="s">
113+
<Text.Headline>Sources</Text.Headline>
114+
<SourceArrayInput
115+
value={value?.sources || []}
116+
onChange={handleSourcesChange}
117+
/>
118+
</Spacings.Stack>
119+
</Spacings.Stack>
120+
);
121+
};
122+
123+
export default AssetInput;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// src/components/attribute-input/source-array-input.tsx
2+
3+
import React from 'react';
4+
import Spacings from '@commercetools-uikit/spacings';
5+
import SecondaryButton from '@commercetools-uikit/secondary-button';
6+
import Constraints from '@commercetools-uikit/constraints';
7+
import SourceInput from './source-input';
8+
import type { Source } from './types';
9+
10+
type Props = {
11+
value: Source[];
12+
onChange: (value: Source[]) => void;
13+
};
14+
15+
const SourceArrayInput: React.FC<Props> = ({ value = [], onChange }) => {
16+
const handleItemChange = (index: number, itemValue: Source) => {
17+
const newSources = value.map((item, i) => (i === index ? itemValue : item));
18+
onChange(newSources);
19+
};
20+
21+
const handleAddItem = () => {
22+
const newSources = [...value, { key: '', uri: '', contentType: '' }];
23+
onChange(newSources);
24+
};
25+
26+
const handleRemoveItem = (index: number) => {
27+
const newSources = value.filter((_, i) => i !== index);
28+
onChange(newSources);
29+
};
30+
31+
return (
32+
<Spacings.Stack scale="m">
33+
{value.map((source, index) => (
34+
<SourceInput
35+
key={index}
36+
index={index}
37+
value={source}
38+
onChange={handleItemChange}
39+
onRemove={handleRemoveItem}
40+
/>
41+
))}
42+
<Constraints.Horizontal max="scale">
43+
<SecondaryButton
44+
size="small"
45+
label="Add Source"
46+
onClick={handleAddItem}
47+
/>
48+
</Constraints.Horizontal>
49+
</Spacings.Stack>
50+
);
51+
};
52+
53+
export default SourceArrayInput;
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// src/components/attribute-input/source-input.tsx
2+
3+
import React from 'react';
4+
import Grid from '@commercetools-uikit/grid';
5+
import TextInput from '@commercetools-uikit/text-input';
6+
import NumberInput from '@commercetools-uikit/number-input';
7+
import { CloseIcon } from '@commercetools-uikit/icons';
8+
import Text from '@commercetools-uikit/text';
9+
import SecondaryButton from '@commercetools-uikit/secondary-button';
10+
import Spacings from '@commercetools-uikit/spacings';
11+
import Card from '@commercetools-uikit/card';
12+
import type { Source } from './types';
13+
14+
type Props = {
15+
value: Source;
16+
index: number;
17+
onChange: (index: number, value: Source) => void;
18+
onRemove: (index: number) => void;
19+
};
20+
21+
const SourceInput: React.FC<Props> = ({ value, index, onChange, onRemove }) => {
22+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
23+
const { name, value: fieldValue } = e.target;
24+
onChange(index, { ...value, [name]: fieldValue });
25+
};
26+
27+
const handleNumberChange = (e: React.ChangeEvent<HTMLInputElement>) => {
28+
const { name, value: fieldValue } = e.target;
29+
onChange(index, {
30+
...value,
31+
[name]: fieldValue ? parseInt(fieldValue, 10) : null,
32+
});
33+
};
34+
35+
return (
36+
<Card type='raised'>
37+
<Spacings.Inline justifyContent="space-between" alignItems="center">
38+
<Text.Subheadline as="h4">Sources {index + 1}</Text.Subheadline>
39+
<SecondaryButton
40+
iconLeft={<CloseIcon size="medium" />}
41+
label="Remove Source"
42+
size="small"
43+
data-testid={`remove-source-${index}`}
44+
onClick={() => onRemove(index)}
45+
/>
46+
</Spacings.Inline>
47+
<Grid gridGap="16px" gridTemplateColumns="repeat(2, 1fr)">
48+
<TextInput
49+
name="key"
50+
placeholder="Source Key"
51+
value={value.key || ''}
52+
onChange={handleChange}
53+
/>
54+
<TextInput
55+
name="uri"
56+
placeholder="Source URI"
57+
value={value.uri || ''}
58+
onChange={handleChange}
59+
/>
60+
<TextInput
61+
name="contentType"
62+
placeholder="Content Type (e.g., image/jpeg)"
63+
value={value.contentType || ''}
64+
onChange={handleChange}
65+
/>
66+
<NumberInput
67+
name="width"
68+
placeholder="Width"
69+
value={value.width || ''}
70+
onChange={handleNumberChange}
71+
/>
72+
<NumberInput
73+
name="height"
74+
placeholder="Height"
75+
value={value.height || ''}
76+
onChange={handleNumberChange}
77+
/>
78+
</Grid>
79+
</Card>
80+
);
81+
};
82+
83+
export default SourceInput;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export type LocalizedString = Record<string, string>;
2+
3+
export interface Source {
4+
key: string;
5+
uri: string;
6+
contentType: string;
7+
width?: number | null;
8+
height?: number | null;
9+
}
10+
11+
export interface Asset {
12+
key?: string;
13+
name: LocalizedString;
14+
description?: LocalizedString;
15+
tags?: string[];
16+
folder?: string;
17+
sources?: Source[];
18+
}

src/components/custom-object-form/attribute-input.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { TYPES } from '../../constants';
1919
import nestedStyles from '../container-form/nested-attributes.module.css';
2020
import AttributeField from './attribute-field'; // eslint-disable-line import/no-cycle
2121
import LexicalEditorField from './lexical-editor-field';
22+
import AssetInput from './asset-input/asset-input';
2223

2324
type Props = {
2425
type: string;
@@ -271,7 +272,9 @@ const AttributeInput: FC<Props> = ({
271272
}}
272273
/>
273274
{touched && errors && (
274-
<ErrorMessage data-testid="field-error-richtext">{errors}</ErrorMessage>
275+
<ErrorMessage data-testid="field-error-richtext">
276+
{errors}
277+
</ErrorMessage>
275278
)}
276279
</Spacings.Stack>
277280
);
@@ -307,6 +310,25 @@ const AttributeInput: FC<Props> = ({
307310
</div>
308311
);
309312

313+
case TYPES.Asset:
314+
return (
315+
<Spacings.Stack scale="xs">
316+
<AssetInput
317+
name={name}
318+
value={value}
319+
touched={touched}
320+
errors={errors}
321+
onChange={onChange}
322+
onBlur={onBlur}
323+
/>
324+
{touched && errors && (
325+
<ErrorMessage data-testid="field-error">
326+
{typeof errors === 'string' ? errors : 'Invalid Asset data'}
327+
</ErrorMessage>
328+
)}
329+
</Spacings.Stack>
330+
);
331+
310332
default:
311333
return null;
312334
}

0 commit comments

Comments
 (0)