diff --git a/apps/client/src/features/base/components/cells/cell-multi-select.tsx b/apps/client/src/features/base/components/cells/cell-multi-select.tsx index f76366f0..95e9d951 100644 --- a/apps/client/src/features/base/components/cells/cell-multi-select.tsx +++ b/apps/client/src/features/base/components/cells/cell-multi-select.tsx @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect, useCallback } from "react"; +import { useState, useRef, useEffect, useCallback, useMemo } from "react"; import { Popover, TextInput } from "@mantine/core"; import { IBaseProperty, @@ -6,8 +6,15 @@ import { Choice, } from "@/features/base/types/base.types"; import { choiceColor } from "@/features/base/components/cells/choice-color"; +import { useUpdatePropertyMutation } from "@/features/base/queries/base-property-query"; +import { v7 as uuid7 } from "uuid"; import cellClasses from "@/features/base/styles/cells.module.css"; +const CHOICE_COLORS = [ + "gray", "red", "pink", "grape", "violet", "indigo", + "blue", "cyan", "teal", "green", "lime", "yellow", "orange", +]; + type CellMultiSelectProps = { value: unknown; property: IBaseProperty; @@ -55,14 +62,55 @@ export function CellMultiSelect({ [selectedIds, selectedSet, onCommit], ); + const updatePropertyMutation = useUpdatePropertyMutation(); + + const trimmedSearch = search.trim(); + const hasExactMatch = useMemo( + () => + trimmedSearch.length > 0 && + choices.some((c) => c.name.toLowerCase() === trimmedSearch.toLowerCase()), + [choices, trimmedSearch], + ); + const showAddOption = trimmedSearch.length > 0 && !hasExactMatch; + + const addOptionColor = useMemo( + () => CHOICE_COLORS[choices.length % CHOICE_COLORS.length], + [choices.length], + ); + + const handleAddOption = useCallback(() => { + if (!trimmedSearch) return; + const newChoice: Choice = { + id: uuid7(), + name: trimmedSearch, + color: addOptionColor, + }; + const newChoices = [...choices, newChoice]; + updatePropertyMutation.mutate({ + propertyId: property.id, + baseId: property.baseId, + typeOptions: { + ...typeOptions, + choices: newChoices, + choiceOrder: newChoices.map((c) => c.id), + }, + }); + onCommit([...selectedIds, newChoice.id]); + setSearch(""); + }, [trimmedSearch, addOptionColor, choices, typeOptions, property, updatePropertyMutation, selectedIds, onCommit]); + const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === "Escape") { e.preventDefault(); onCancel(); } + if (e.key === "Enter" && showAddOption) { + e.preventDefault(); + handleAddOption(); + } }, - [onCancel], + [onCancel, showAddOption, handleAddOption], ); const MAX_VISIBLE = 3; @@ -110,6 +158,20 @@ export function CellMultiSelect({ ))} + {showAddOption && ( +
+ Add option: + + {trimmedSearch} + +
+ )} diff --git a/apps/client/src/features/base/components/cells/cell-select.tsx b/apps/client/src/features/base/components/cells/cell-select.tsx index 38c6361c..77096c89 100644 --- a/apps/client/src/features/base/components/cells/cell-select.tsx +++ b/apps/client/src/features/base/components/cells/cell-select.tsx @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect, useCallback } from "react"; +import { useState, useRef, useEffect, useCallback, useMemo } from "react"; import { Popover, TextInput } from "@mantine/core"; import { IBaseProperty, @@ -6,8 +6,15 @@ import { Choice, } from "@/features/base/types/base.types"; import { choiceColor } from "@/features/base/components/cells/choice-color"; +import { useUpdatePropertyMutation } from "@/features/base/queries/base-property-query"; +import { v7 as uuid7 } from "uuid"; import cellClasses from "@/features/base/styles/cells.module.css"; +const CHOICE_COLORS = [ + "gray", "red", "pink", "grape", "violet", "indigo", + "blue", "cyan", "teal", "green", "lime", "yellow", "orange", +]; + type CellSelectProps = { value: unknown; property: IBaseProperty; @@ -52,14 +59,54 @@ export function CellSelect({ [selectedId, onCommit], ); + const updatePropertyMutation = useUpdatePropertyMutation(); + + const trimmedSearch = search.trim(); + const hasExactMatch = useMemo( + () => + trimmedSearch.length > 0 && + choices.some((c) => c.name.toLowerCase() === trimmedSearch.toLowerCase()), + [choices, trimmedSearch], + ); + const showAddOption = trimmedSearch.length > 0 && !hasExactMatch; + + const addOptionColor = useMemo( + () => CHOICE_COLORS[choices.length % CHOICE_COLORS.length], + [choices.length], + ); + + const handleAddOption = useCallback(() => { + if (!trimmedSearch) return; + const newChoice: Choice = { + id: uuid7(), + name: trimmedSearch, + color: addOptionColor, + }; + const newChoices = [...choices, newChoice]; + updatePropertyMutation.mutate({ + propertyId: property.id, + baseId: property.baseId, + typeOptions: { + ...typeOptions, + choices: newChoices, + choiceOrder: newChoices.map((c) => c.id), + }, + }); + onCommit(newChoice.id); + }, [trimmedSearch, addOptionColor, choices, typeOptions, property, updatePropertyMutation, onCommit]); + const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === "Escape") { e.preventDefault(); onCancel(); } + if (e.key === "Enter" && showAddOption) { + e.preventDefault(); + handleAddOption(); + } }, - [onCancel], + [onCancel, showAddOption, handleAddOption], ); if (isEditing) { @@ -112,6 +159,20 @@ export function CellSelect({ ))} + {showAddOption && ( +
+ Add option: + + {trimmedSearch} + +
+ )} diff --git a/apps/client/src/features/base/components/property/property-options.tsx b/apps/client/src/features/base/components/property/property-options.tsx index 6754c059..37b6f962 100644 --- a/apps/client/src/features/base/components/property/property-options.tsx +++ b/apps/client/src/features/base/components/property/property-options.tsx @@ -167,6 +167,7 @@ function NumberOptions({