diff --git a/apps/color-picker/src/components/SwatchEditor.tsx b/apps/color-picker/src/components/SwatchEditor.tsx
index a23350ceb7..ebefe49fa0 100644
--- a/apps/color-picker/src/components/SwatchEditor.tsx
+++ b/apps/color-picker/src/components/SwatchEditor.tsx
@@ -19,9 +19,15 @@ interface SwatchEditorProps {
swatch: Color;
onChange: (swatch: Color) => void;
onRemove: (swatch: Color) => void;
+ validationMessage?: string;
}
-export default function SwatchEditor({ swatch, onChange, onRemove }: SwatchEditorProps) {
+export default function SwatchEditor({
+ swatch,
+ onChange,
+ onRemove,
+ validationMessage,
+}: SwatchEditorProps) {
const {
attributes,
listeners,
@@ -39,7 +45,7 @@ export default function SwatchEditor({ swatch, onChange, onRemove }: SwatchEdito
return (
-
+
}
/>
+ {validationMessage ? (
+ {validationMessage}
+ ) : null}
);
diff --git a/apps/color-picker/src/locations/ConfigScreen.tsx b/apps/color-picker/src/locations/ConfigScreen.tsx
index 929cf012c6..04094a462d 100644
--- a/apps/color-picker/src/locations/ConfigScreen.tsx
+++ b/apps/color-picker/src/locations/ConfigScreen.tsx
@@ -27,7 +27,7 @@ import { ExternalLinkIcon, PlusIcon } from '@contentful/f36-icons';
import tokens from '@contentful/f36-tokens';
import { useSDK } from '@contentful/react-apps-toolkit';
import { css } from 'emotion';
-import { useCallback, useEffect, useState } from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
import SwatchEditor from '../components/SwatchEditor';
import { AppInstallationParameters, Color } from '../types';
@@ -49,6 +49,29 @@ const styles = {
}),
};
+const DUPLICATE_COLOR_MESSAGE = 'Each color value must be unique.';
+
+function normalizeHexValue(value: string) {
+ return value.trim().toLowerCase();
+}
+
+function getDuplicateColorIds(colors: Color[]) {
+ const idsByValue = new Map();
+
+ colors.forEach((color) => {
+ const normalizedValue = normalizeHexValue(color.value);
+ const ids = idsByValue.get(normalizedValue) ?? [];
+ ids.push(color.id);
+ idsByValue.set(normalizedValue, ids);
+ });
+
+ return new Set(
+ Array.from(idsByValue.values())
+ .filter((ids) => ids.length > 1)
+ .flat()
+ );
+}
+
const ConfigScreen = () => {
const [isInstalled, setIsInstalled] = useState(false);
const [parameters, setParameters] = useState({
@@ -61,6 +84,9 @@ const ConfigScreen = () => {
],
});
const sdk = useSDK();
+ const colors = parameters.themes[0].colors;
+
+ const duplicateColorIds = useMemo(() => getDuplicateColorIds(colors), [colors]);
const sensors = useSensors(
useSensor(PointerSensor, {
@@ -136,13 +162,18 @@ const ConfigScreen = () => {
};
const onConfigure = useCallback(async () => {
+ if (duplicateColorIds.size > 0) {
+ sdk.notifier.error('Duplicate color values are not allowed');
+ return false;
+ }
+
const currentState = await sdk.app.getCurrentState();
return {
parameters,
targetState: currentState,
};
- }, [parameters, sdk]);
+ }, [duplicateColorIds, parameters, sdk]);
useEffect(() => {
// its configuration.
@@ -205,12 +236,15 @@ const ConfigScreen = () => {
c.id)}
strategy={verticalListSortingStrategy}>
- {parameters.themes[0].colors.map((swatch) => (
+ {colors.map((swatch) => (
))}