diff --git a/src/app/(main)/boards/BoardEditForm.tsx b/src/app/(main)/boards/BoardEditForm.tsx index 9360c036d..ec405ff35 100644 --- a/src/app/(main)/boards/BoardEditForm.tsx +++ b/src/app/(main)/boards/BoardEditForm.tsx @@ -10,7 +10,6 @@ import { Select, TextField, } from '@umami/react-zen'; -import { useEffect, useState } from 'react'; import { useBoardQuery, useMessages, useNavigation, useUpdateQuery } from '@/components/hooks'; import { LinkSelect } from '@/components/input/LinkSelect'; import { PixelSelect } from '@/components/input/PixelSelect'; @@ -58,7 +57,7 @@ export function BoardEditForm({ const { t, labels, messages, getErrorMessage } = useMessages(); const { teamId: navigationTeamId } = useNavigation(); const resolvedTeamId = teamId ?? navigationTeamId; - const { data, isLoading } = useBoardQuery(boardId || ''); + const { data: board, isLoading } = useBoardQuery(boardId || ''); const { mutateAsync, error, isPending, touch, toast } = useUpdateQuery( boardId ? `/boards/${boardId}` : '/boards', { @@ -66,20 +65,14 @@ export function BoardEditForm({ teamId: resolvedTeamId, }, ); - const [values, setValues] = useState(getDefaultValues()); + const values = getDefaultValues(board); - useEffect(() => { - if (data) { - setValues(getDefaultValues(data)); - } - }, [data]); - - const handleSubmit = async () => { + const handleSubmit = async (data: BoardFormValues) => { const result = await mutateAsync({ - name: values.name, - description: values.description, - type: values.type, - parameters: setBoardEntity({}, values.type, values.entityId || undefined), + name: data.name, + description: data.description, + type: data.type, + parameters: setBoardEntity(board?.parameters, data.type, data.entityId || undefined), }); toast(t(messages.saved)); @@ -89,111 +82,100 @@ export function BoardEditForm({ onClose?.(); }; - const handleNameChange = (name: string) => { - setValues(current => ({ ...current, name })); - }; - - const handleDescriptionChange = (description: string) => { - setValues(current => ({ ...current, description })); - }; - - const handleTypeChange = (type: string) => { - setValues(current => ({ - ...current, - type: type as BoardType, - entityId: '', - })); - }; - - const handleEntityChange = (entityId: string) => { - setValues(current => ({ ...current, entityId })); - }; - if (boardId && isLoading) { return ; } - const entityLabel = - values.type === BOARD_TYPES.pixel - ? t(labels.pixel) - : values.type === BOARD_TYPES.link - ? t(labels.link) - : t(labels.website); - return (
- - - - - - - - - - - - {requiresBoardEntity(values.type) && ( - - - {values.type === BOARD_TYPES.website ? ( - - ) : values.type === BOARD_TYPES.pixel ? ( - - ) : ( - { + const type = watch('type') as BoardType; + const entityId = watch('entityId') as string; + const entityLabel = + type === BOARD_TYPES.pixel + ? t(labels.pixel) + : type === BOARD_TYPES.link + ? t(labels.link) + : t(labels.website); + + const handleTypeChange = (value: string) => { + setValue('type', value as BoardType, { shouldDirty: true }); + setValue('entityId', '', { shouldDirty: true }); + }; + + const handleEntityChange = (value: string) => { + setValue('entityId', value, { shouldDirty: true }); + }; + + return ( + <> + + + + + + + + + + + + {requiresBoardEntity(type) && ( + + + {type === BOARD_TYPES.website ? ( + + ) : type === BOARD_TYPES.pixel ? ( + + ) : ( + + )} + + )} - - - )} - - {onClose && ( - - )} - {t(labels.save)} - + + {onClose && ( + + )} + {t(labels.save)} + + + ); + }}
); } diff --git a/src/app/(main)/boards/[boardId]/BoardShareDialog.tsx b/src/app/(main)/boards/[boardId]/BoardShareDialog.tsx index 108df7c64..ba7a84774 100644 --- a/src/app/(main)/boards/[boardId]/BoardShareDialog.tsx +++ b/src/app/(main)/boards/[boardId]/BoardShareDialog.tsx @@ -30,13 +30,13 @@ function BoardShareDialogContent({ }) { const { t, labels, messages } = useMessages(); const [isCreating, setIsCreating] = useState(false); - const showCreateForm = !hasShares || isCreating; + const showCreateForm = isCreating; return ( {t(labels.share)} - {hasShares && !isCreating && ( + {!isCreating && ( @@ -46,17 +46,18 @@ function BoardShareDialogContent({ setIsCreating(false)} - onCancel={hasShares ? () => setIsCreating(false) : undefined} + onCancel={() => setIsCreating(false)} /> )} - {hasShares ? ( - <> - {t(messages.shareUrl)} - - - ) : ( - !showCreateForm && {t(messages.noDataAvailable)} + {hasShares && ( + {t(messages.shareUrl)} )} + {!showCreateForm && + (hasShares ? ( + + ) : ( + {t(messages.noDataAvailable)} + ))} ); } diff --git a/src/app/(main)/boards/[boardId]/edit/BoardEditPage.tsx b/src/app/(main)/boards/[boardId]/edit/BoardEditPage.tsx index 7c6568185..ff46863fc 100644 --- a/src/app/(main)/boards/[boardId]/edit/BoardEditPage.tsx +++ b/src/app/(main)/boards/[boardId]/edit/BoardEditPage.tsx @@ -20,7 +20,8 @@ export function BoardEditPage({ boardId }: { boardId: string }) { <> diff --git a/src/app/(main)/links/[linkId]/LinkShareForm.tsx b/src/app/(main)/links/[linkId]/LinkShareForm.tsx index 6e6c4f8dc..3486a71ea 100644 --- a/src/app/(main)/links/[linkId]/LinkShareForm.tsx +++ b/src/app/(main)/links/[linkId]/LinkShareForm.tsx @@ -30,13 +30,13 @@ function LinkShareFormContent({ }) { const { t, labels, messages } = useMessages(); const [isCreating, setIsCreating] = useState(false); - const showCreateForm = !hasShares || isCreating; + const showCreateForm = isCreating; return ( {t(labels.share)} - {hasShares && !isCreating && ( + {!isCreating && ( @@ -46,17 +46,18 @@ function LinkShareFormContent({ setIsCreating(false)} - onCancel={hasShares ? () => setIsCreating(false) : undefined} + onCancel={() => setIsCreating(false)} /> )} - {hasShares ? ( - <> - {t(messages.shareUrl)} - - - ) : ( - !showCreateForm && {t(messages.noDataAvailable)} + {hasShares && ( + {t(messages.shareUrl)} )} + {!showCreateForm && + (hasShares ? ( + + ) : ( + {t(messages.noDataAvailable)} + ))} ); } diff --git a/src/app/(main)/pixels/[pixelId]/PixelShareForm.tsx b/src/app/(main)/pixels/[pixelId]/PixelShareForm.tsx index 43f4d3169..4c31bbd74 100644 --- a/src/app/(main)/pixels/[pixelId]/PixelShareForm.tsx +++ b/src/app/(main)/pixels/[pixelId]/PixelShareForm.tsx @@ -30,13 +30,13 @@ function PixelShareFormContent({ }) { const { t, labels, messages } = useMessages(); const [isCreating, setIsCreating] = useState(false); - const showCreateForm = !hasShares || isCreating; + const showCreateForm = isCreating; return ( {t(labels.share)} - {hasShares && !isCreating && ( + {!isCreating && ( @@ -46,17 +46,18 @@ function PixelShareFormContent({ setIsCreating(false)} - onCancel={hasShares ? () => setIsCreating(false) : undefined} + onCancel={() => setIsCreating(false)} /> )} - {hasShares ? ( - <> - {t(messages.shareUrl)} - - - ) : ( - !showCreateForm && {t(messages.noDataAvailable)} + {hasShares && ( + {t(messages.shareUrl)} )} + {!showCreateForm && + (hasShares ? ( + + ) : ( + {t(messages.noDataAvailable)} + ))} ); } diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx index 53b4cd9c7..d3d5f4f0f 100644 --- a/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx @@ -7,7 +7,7 @@ import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider'; export function WebsiteSettingsPage({ websiteId }: { websiteId: string }) { return ( - + diff --git a/src/lib/__tests__/boards.test.ts b/src/lib/__tests__/boards.test.ts index 5b43ba428..4d6a8f435 100644 --- a/src/lib/__tests__/boards.test.ts +++ b/src/lib/__tests__/boards.test.ts @@ -2,6 +2,10 @@ import { BOARD_ENTITY_TYPES, isBoardComponentSupported, } from '../boards'; +import { + BOARD_COMPONENT_COMPATIBILITY_MATRIX, + getSupportedBoardComponentEntityTypes, +} from '../boardComponentCompatibility'; test('isBoardComponentSupported allows events chart on website boards', () => { expect(isBoardComponentSupported('EventsChart', BOARD_ENTITY_TYPES.website)).toBe(true); @@ -12,6 +16,13 @@ test('isBoardComponentSupported rejects events chart on pixel and link boards', expect(isBoardComponentSupported('EventsChart', BOARD_ENTITY_TYPES.link)).toBe(false); }); +test('board component compatibility matrix defines website-only components explicitly', () => { + expect(BOARD_COMPONENT_COMPATIBILITY_MATRIX.EventsChart).toEqual([BOARD_ENTITY_TYPES.website]); + expect(getSupportedBoardComponentEntityTypes('EventsChart')).toEqual([ + BOARD_ENTITY_TYPES.website, + ]); +}); + test('isBoardComponentSupported leaves other components available for all board entities', () => { expect(isBoardComponentSupported('WebsiteChart', BOARD_ENTITY_TYPES.pixel)).toBe(true); expect(isBoardComponentSupported('TextBlock', BOARD_ENTITY_TYPES.link)).toBe(true); diff --git a/src/lib/boardComponentCompatibility.ts b/src/lib/boardComponentCompatibility.ts new file mode 100644 index 000000000..943701962 --- /dev/null +++ b/src/lib/boardComponentCompatibility.ts @@ -0,0 +1,22 @@ +import type { BoardEntityType } from './boards'; + +export const BOARD_COMPONENT_COMPATIBILITY_MATRIX = { + EventsChart: ['website'], +} as const satisfies Partial>; + +export function getSupportedBoardComponentEntityTypes(componentType: string) { + return BOARD_COMPONENT_COMPATIBILITY_MATRIX[componentType]; +} + +export function isBoardComponentSupportedByEntityType( + componentType: string, + entityType?: BoardEntityType, +) { + const supportedEntityTypes = getSupportedBoardComponentEntityTypes(componentType); + + if (!entityType || !supportedEntityTypes) { + return true; + } + + return supportedEntityTypes.includes(entityType); +} diff --git a/src/lib/boards.ts b/src/lib/boards.ts index 7fdae85be..e0087c16c 100644 --- a/src/lib/boards.ts +++ b/src/lib/boards.ts @@ -1,4 +1,5 @@ import type { Board, BoardComponentConfig, BoardParameters } from './types'; +import { isBoardComponentSupportedByEntityType } from './boardComponentCompatibility'; export const BOARD_TYPES = { dashboard: 'dashboard', @@ -18,9 +19,6 @@ export type BoardType = (typeof BOARD_TYPES)[keyof typeof BOARD_TYPES]; export type BoardEntityType = (typeof BOARD_ENTITY_TYPES)[keyof typeof BOARD_ENTITY_TYPES]; const boardTypes = new Set(Object.values(BOARD_TYPES)); -const boardComponentEntityTypes: Partial> = { - EventsChart: [BOARD_ENTITY_TYPES.website], -}; export function getLegacyBoardType(parameters?: BoardParameters): BoardType { if (parameters?.pixelId) { @@ -147,13 +145,7 @@ export function isBoardComponentSupported( componentType: string, entityType?: BoardEntityType, ) { - const supportedEntityTypes = boardComponentEntityTypes[componentType]; - - if (!entityType || !supportedEntityTypes) { - return true; - } - - return supportedEntityTypes.includes(entityType); + return isBoardComponentSupportedByEntityType(componentType, entityType); } export function getFirstBoardComponentEntity(