From adbeaf390aed7035ca5e9834ac2cbca4c9c3400e Mon Sep 17 00:00:00 2001 From: Mitch Goudy Date: Thu, 2 Apr 2026 13:30:11 -0600 Subject: [PATCH] fix: prevent duplicate color values in config [] --- .../src/components/SwatchEditor.tsx | 13 +++++- .../src/locations/ConfigScreen.tsx | 40 +++++++++++++++++-- 2 files changed, 48 insertions(+), 5 deletions(-) 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) => ( ))}