mirror of
https://github.com/docmost/docmost.git
synced 2026-08-29 18:14:58 +08:00
feat(base): restore property type change with confirmation panel
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
|||||||
ActionIcon,
|
ActionIcon,
|
||||||
Divider,
|
Divider,
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
|
Loader,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
IconTrash,
|
IconTrash,
|
||||||
@@ -16,15 +17,22 @@ import {
|
|||||||
IconChevronRight,
|
IconChevronRight,
|
||||||
IconSettings,
|
IconSettings,
|
||||||
} from "@tabler/icons-react";
|
} 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 { useAtom } from "jotai";
|
||||||
import { propertyMenuCloseRequestAtomFamily } from "@/features/base/atoms/base-atoms";
|
import { propertyMenuCloseRequestAtomFamily } from "@/features/base/atoms/base-atoms";
|
||||||
import {
|
import {
|
||||||
useUpdatePropertyMutation,
|
useUpdatePropertyMutation,
|
||||||
useDeletePropertyMutation,
|
useDeletePropertyMutation,
|
||||||
} from "@/features/base/queries/base-property-query";
|
} 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 { PropertyOptions } from "./property-options";
|
||||||
|
import {
|
||||||
|
conversionWarning,
|
||||||
|
NON_USER_TARGET_TYPES,
|
||||||
|
} from "./conversion-warning";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { isSystemPropertyType } from "@/features/base/hooks/use-base-table";
|
import { isSystemPropertyType } from "@/features/base/hooks/use-base-table";
|
||||||
import cellClasses from "@/features/base/styles/cells.module.css";
|
import cellClasses from "@/features/base/styles/cells.module.css";
|
||||||
@@ -37,7 +45,14 @@ type PropertyMenuContentProps = {
|
|||||||
pageId: string;
|
pageId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MenuPanel = "main" | "rename" | "options" | "confirmDelete" | "confirmDiscard";
|
type MenuPanel =
|
||||||
|
| "main"
|
||||||
|
| "rename"
|
||||||
|
| "options"
|
||||||
|
| "changeType"
|
||||||
|
| "confirmTypeChange"
|
||||||
|
| "confirmDelete"
|
||||||
|
| "confirmDiscard";
|
||||||
|
|
||||||
export function PropertyMenuContent({
|
export function PropertyMenuContent({
|
||||||
property,
|
property,
|
||||||
@@ -51,6 +66,7 @@ export function PropertyMenuContent({
|
|||||||
const [renameValue, setRenameValue] = useState(property.name);
|
const [renameValue, setRenameValue] = useState(property.name);
|
||||||
const renameInputRef = useRef<HTMLInputElement>(null);
|
const renameInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [optionsDirty, setOptionsDirty] = useState(false);
|
const [optionsDirty, setOptionsDirty] = useState(false);
|
||||||
|
const [pendingTargetType, setPendingTargetType] = useState<BasePropertyType | null>(null);
|
||||||
const pendingActionRef = useRef<"back" | "close" | null>(null);
|
const pendingActionRef = useRef<"back" | "close" | null>(null);
|
||||||
const sourcePanelRef = useRef<"rename" | "options" | null>(null);
|
const sourcePanelRef = useRef<"rename" | "options" | null>(null);
|
||||||
const [closeRequest] = useAtom(propertyMenuCloseRequestAtomFamily(pageId)) as unknown as [number];
|
const [closeRequest] = useAtom(propertyMenuCloseRequestAtomFamily(pageId)) as unknown as [number];
|
||||||
@@ -66,6 +82,7 @@ export function PropertyMenuContent({
|
|||||||
setPanel("main");
|
setPanel("main");
|
||||||
setRenameValue(property.name);
|
setRenameValue(property.name);
|
||||||
setOptionsDirty(false);
|
setOptionsDirty(false);
|
||||||
|
setPendingTargetType(null);
|
||||||
}
|
}
|
||||||
}, [opened, property.name]);
|
}, [opened, property.name]);
|
||||||
|
|
||||||
@@ -148,6 +165,35 @@ export function PropertyMenuContent({
|
|||||||
[property, updatePropertyMutation],
|
[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(() => {
|
const handleDelete = useCallback(() => {
|
||||||
deletePropertyMutation.mutate({
|
deletePropertyMutation.mutate({
|
||||||
propertyId: property.id,
|
propertyId: property.id,
|
||||||
@@ -201,6 +247,7 @@ export function PropertyMenuContent({
|
|||||||
<MainPanel
|
<MainPanel
|
||||||
property={property}
|
property={property}
|
||||||
onRename={() => setPanel("rename")}
|
onRename={() => setPanel("rename")}
|
||||||
|
onChangeType={() => setPanel("changeType")}
|
||||||
onOptions={() => setPanel("options")}
|
onOptions={() => setPanel("options")}
|
||||||
onDelete={() => setPanel("confirmDelete")}
|
onDelete={() => setPanel("confirmDelete")}
|
||||||
/>
|
/>
|
||||||
@@ -232,6 +279,61 @@ export function PropertyMenuContent({
|
|||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
{panel === "changeType" && (
|
||||||
|
<Stack gap={0} p={4}>
|
||||||
|
<Group gap="xs" px="sm" py={6}>
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
color="gray"
|
||||||
|
size="xs"
|
||||||
|
onClick={() => setPanel("main")}
|
||||||
|
>
|
||||||
|
<IconChevronRight
|
||||||
|
size={14}
|
||||||
|
style={{ transform: "rotate(180deg)" }}
|
||||||
|
/>
|
||||||
|
</ActionIcon>
|
||||||
|
<Text size="xs" fw={600} c="dimmed">
|
||||||
|
{t("Change type")}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<ScrollArea.Autosize mah={300} scrollbarSize={6} offsetScrollbars>
|
||||||
|
<PropertyTypePicker
|
||||||
|
onSelect={handleTypeSelect}
|
||||||
|
currentType={property.type}
|
||||||
|
excludeTypes={NON_USER_TARGET_TYPES}
|
||||||
|
showSearch
|
||||||
|
/>
|
||||||
|
</ScrollArea.Autosize>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
{panel === "confirmTypeChange" && pendingTargetType && (
|
||||||
|
<Stack gap="xs" p="sm">
|
||||||
|
<Text size="sm" fw={600}>
|
||||||
|
{t("Change type to {{label}}?", {
|
||||||
|
label: t(
|
||||||
|
propertyTypes.find((pt) => pt.type === pendingTargetType)
|
||||||
|
?.labelKey ?? pendingTargetType,
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{t(conversionWarning(property.type, pendingTargetType))}
|
||||||
|
</Text>
|
||||||
|
<Group gap="xs" justify="flex-end">
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="xs"
|
||||||
|
onClick={() => setPanel("main")}
|
||||||
|
>
|
||||||
|
{t("Cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button size="xs" onClick={handleApplyTypeChange}>
|
||||||
|
{t("Apply")}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
{(panel === "options" || panel === "confirmDiscard") && (
|
{(panel === "options" || panel === "confirmDiscard") && (
|
||||||
<Stack gap="xs" p="sm" style={panel === "confirmDiscard" ? { display: "none" } : undefined}>
|
<Stack gap="xs" p="sm" style={panel === "confirmDiscard" ? { display: "none" } : undefined}>
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
@@ -351,25 +453,29 @@ function MenuItem({
|
|||||||
function MainPanel({
|
function MainPanel({
|
||||||
property,
|
property,
|
||||||
onRename,
|
onRename,
|
||||||
|
onChangeType,
|
||||||
onOptions,
|
onOptions,
|
||||||
onDelete,
|
onDelete,
|
||||||
}: {
|
}: {
|
||||||
property: IBaseProperty;
|
property: IBaseProperty;
|
||||||
onRename: () => void;
|
onRename: () => void;
|
||||||
|
onChangeType: () => void;
|
||||||
onOptions: () => void;
|
onOptions: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const isSystem = isSystemPropertyType(property.type);
|
const isSystem = isSystemPropertyType(property.type);
|
||||||
|
const isPending = property.pendingType != null;
|
||||||
|
|
||||||
const hasOptions =
|
const hasOptions =
|
||||||
!isSystem &&
|
!isSystem &&
|
||||||
|
!isPending &&
|
||||||
(property.type === "select" ||
|
(property.type === "select" ||
|
||||||
property.type === "multiSelect" ||
|
property.type === "multiSelect" ||
|
||||||
property.type === "status" ||
|
property.type === "status" ||
|
||||||
property.type === "number" ||
|
property.type === "number" ||
|
||||||
property.type === "date");
|
property.type === "date");
|
||||||
|
|
||||||
const typeDef = propertyTypes.find((pt) => pt.type === property.type);
|
const typeDef = propertyTypes.find((pt) => pt.type === property.type);
|
||||||
const TypeIcon = typeDef?.icon;
|
const TypeIcon = typeDef?.icon;
|
||||||
@@ -381,17 +487,27 @@ function MainPanel({
|
|||||||
label={t("Rename")}
|
label={t("Rename")}
|
||||||
onClick={onRename}
|
onClick={onRename}
|
||||||
/>
|
/>
|
||||||
{!isSystem && (
|
{isPending && (
|
||||||
<Stack gap={4} px="sm" py={6}>
|
<Group gap={8} px="sm" py={8}>
|
||||||
<Text size="xs" c="dimmed">{t("Type")}</Text>
|
<Loader size={12} />
|
||||||
<TextInput
|
<Text size="sm" c="dimmed">
|
||||||
size="xs"
|
{t("Converting…")}
|
||||||
value={typeDef ? t(typeDef.labelKey) : property.type}
|
</Text>
|
||||||
disabled
|
</Group>
|
||||||
leftSection={TypeIcon ? <TypeIcon size={14} /> : null}
|
)}
|
||||||
readOnly
|
{!isSystem && !isPending && !property.isPrimary && (
|
||||||
/>
|
<UnstyledButton
|
||||||
</Stack>
|
className={cellClasses.menuItem}
|
||||||
|
onClick={onChangeType}
|
||||||
|
>
|
||||||
|
<Group gap={8} wrap="nowrap" style={{ flex: 1 }}>
|
||||||
|
{TypeIcon ? <TypeIcon size={14} /> : null}
|
||||||
|
<Text size="sm">
|
||||||
|
{typeDef ? t(typeDef.labelKey) : property.type}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<IconChevronRight size={14} />
|
||||||
|
</UnstyledButton>
|
||||||
)}
|
)}
|
||||||
{hasOptions && (
|
{hasOptions && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@@ -401,7 +517,7 @@ function MainPanel({
|
|||||||
onClick={onOptions}
|
onClick={onOptions}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!property.isPrimary && (
|
{!property.isPrimary && !isPending && (
|
||||||
<>
|
<>
|
||||||
<Divider my={4} />
|
<Divider my={4} />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
|||||||
Reference in New Issue
Block a user