diff --git a/apps/client/src/features/base/components/property/property-menu.tsx b/apps/client/src/features/base/components/property/property-menu.tsx index 7b2bb3353..9028538aa 100644 --- a/apps/client/src/features/base/components/property/property-menu.tsx +++ b/apps/client/src/features/base/components/property/property-menu.tsx @@ -9,6 +9,7 @@ import { ActionIcon, Divider, ScrollArea, + Loader, } from "@mantine/core"; import { IconTrash, @@ -16,15 +17,22 @@ import { IconChevronRight, IconSettings, } from "@tabler/icons-react"; -import { IBaseProperty } from "@/features/base/types/base.types"; +import { + IBaseProperty, + BasePropertyType, +} from "@/features/base/types/base.types"; import { useAtom } from "jotai"; import { propertyMenuCloseRequestAtomFamily } from "@/features/base/atoms/base-atoms"; import { useUpdatePropertyMutation, useDeletePropertyMutation, } from "@/features/base/queries/base-property-query"; -import { propertyTypes } from "./property-type-picker"; +import { PropertyTypePicker, propertyTypes } from "./property-type-picker"; import { PropertyOptions } from "./property-options"; +import { + conversionWarning, + NON_USER_TARGET_TYPES, +} from "./conversion-warning"; import { useTranslation } from "react-i18next"; import { isSystemPropertyType } from "@/features/base/hooks/use-base-table"; import cellClasses from "@/features/base/styles/cells.module.css"; @@ -37,7 +45,14 @@ type PropertyMenuContentProps = { pageId: string; }; -type MenuPanel = "main" | "rename" | "options" | "confirmDelete" | "confirmDiscard"; +type MenuPanel = + | "main" + | "rename" + | "options" + | "changeType" + | "confirmTypeChange" + | "confirmDelete" + | "confirmDiscard"; export function PropertyMenuContent({ property, @@ -51,6 +66,7 @@ export function PropertyMenuContent({ const [renameValue, setRenameValue] = useState(property.name); const renameInputRef = useRef(null); const [optionsDirty, setOptionsDirty] = useState(false); + const [pendingTargetType, setPendingTargetType] = useState(null); const pendingActionRef = useRef<"back" | "close" | null>(null); const sourcePanelRef = useRef<"rename" | "options" | null>(null); const [closeRequest] = useAtom(propertyMenuCloseRequestAtomFamily(pageId)) as unknown as [number]; @@ -66,6 +82,7 @@ export function PropertyMenuContent({ setPanel("main"); setRenameValue(property.name); setOptionsDirty(false); + setPendingTargetType(null); } }, [opened, property.name]); @@ -148,6 +165,35 @@ export function PropertyMenuContent({ [property, updatePropertyMutation], ); + const handleTypeSelect = useCallback( + (type: BasePropertyType) => { + if (type === property.type) { + onClose(); + return; + } + setPendingTargetType(type); + setPanel("confirmTypeChange"); + }, + [property.type, onClose], + ); + + const handleApplyTypeChange = useCallback(() => { + if (!pendingTargetType) return; + updatePropertyMutation.mutate({ + propertyId: property.id, + pageId: property.pageId, + type: pendingTargetType, + typeOptions: {}, + }); + onClose(); + }, [ + pendingTargetType, + property.id, + property.pageId, + updatePropertyMutation, + onClose, + ]); + const handleDelete = useCallback(() => { deletePropertyMutation.mutate({ propertyId: property.id, @@ -201,6 +247,7 @@ export function PropertyMenuContent({ setPanel("rename")} + onChangeType={() => setPanel("changeType")} onOptions={() => setPanel("options")} onDelete={() => setPanel("confirmDelete")} /> @@ -232,6 +279,61 @@ export function PropertyMenuContent({ )} + {panel === "changeType" && ( + + + setPanel("main")} + > + + + + {t("Change type")} + + + + + + + )} + {panel === "confirmTypeChange" && pendingTargetType && ( + + + {t("Change type to {{label}}?", { + label: t( + propertyTypes.find((pt) => pt.type === pendingTargetType) + ?.labelKey ?? pendingTargetType, + ), + })} + + + {t(conversionWarning(property.type, pendingTargetType))} + + + + + + + )} {(panel === "options" || panel === "confirmDiscard") && ( @@ -351,25 +453,29 @@ function MenuItem({ function MainPanel({ property, onRename, + onChangeType, onOptions, onDelete, }: { property: IBaseProperty; onRename: () => void; + onChangeType: () => void; onOptions: () => void; onDelete: () => void; }) { const { t } = useTranslation(); const isSystem = isSystemPropertyType(property.type); + const isPending = property.pendingType != null; const hasOptions = !isSystem && + !isPending && (property.type === "select" || - property.type === "multiSelect" || - property.type === "status" || - property.type === "number" || - property.type === "date"); + property.type === "multiSelect" || + property.type === "status" || + property.type === "number" || + property.type === "date"); const typeDef = propertyTypes.find((pt) => pt.type === property.type); const TypeIcon = typeDef?.icon; @@ -381,17 +487,27 @@ function MainPanel({ label={t("Rename")} onClick={onRename} /> - {!isSystem && ( - - {t("Type")} - : null} - readOnly - /> - + {isPending && ( + + + + {t("Converting…")} + + + )} + {!isSystem && !isPending && !property.isPrimary && ( + + + {TypeIcon ? : null} + + {typeDef ? t(typeDef.labelKey) : property.type} + + + + )} {hasOptions && ( )} - {!property.isPrimary && ( + {!property.isPrimary && !isPending && ( <>