fix(explorer): live custom-prop metadata in detail panel, list-view delete

- CustomProps sidebar read a snapshot of file.metadata, so props added or
  removed from list-view cells never appeared in the open detail panel.
  Prefer the live fileManager list entry and include custom_props in the
  memo deps.
- Sync panel-initiated patches back into the fm list via fileUpdated so
  both views stay consistent.
- Add a hover Dismiss button on custom-prop list cells that removes the
  prop via patchCustomProp(remove: true), honoring read-only state.

Closes Dvorinka/cloudreve#94 (upstream cloudreve/cloudreve#3321).

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
pull/3582/head
Tomas Dvorak 2 weeks ago
parent 772eb1a56b
commit 325a3aa0a4

@ -1,4 +1,4 @@
import { Box, Fade, Grow, InputBase, PopoverProps, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material";
import { Box, Fade, Grow, IconButton, InputBase, PopoverProps, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material";
import React, { memo, useCallback, useContext, useEffect, useMemo, useState } from "react";
import { sizeToString } from "../../../../util";
import CrUri, { SearchParam } from "../../../../util/uri.ts";
@ -21,6 +21,7 @@ import { loadFileThumb, patchCustomProp } from "../../../../redux/thunks/file.ts
import AutoHeight from "../../../Common/AutoHeight.tsx";
import { NoWrapBox } from "../../../Common/StyledComponents.tsx";
import TimeBadge from "../../../Common/TimeBadge.tsx";
import Dismiss from "../../../Icons/Dismiss.tsx";
import Info from "../../../Icons/Info.tsx";
import { DisplayOption } from "../../ContextMenu/useActionDisplayOpt.ts";
import FileBadge from "../../FileBadge.tsx";
@ -392,6 +393,7 @@ const CustomPropsCell = memo(({ file, customProp, readOnly }: CustomPropsCellPro
const dispatch = useAppDispatch();
const fmIndex = useContext(FmIndexContext);
const [loading, setLoading] = useState(false);
const [mouseOver, setMouseOver] = useState(false);
const stopPropagation = useCallback((e: React.SyntheticEvent) => {
e.stopPropagation();
@ -407,6 +409,18 @@ const CustomPropsCell = memo(({ file, customProp, readOnly }: CustomPropsCellPro
[dispatch, fmIndex, file, customProp.id],
);
const onDelete = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation();
if (loading || readOnly) return;
setLoading(true);
dispatch(patchCustomProp(fmIndex, file, customProp.id, "", true)).finally(() => {
setLoading(false);
});
},
[dispatch, fmIndex, file, customProp.id, loading, readOnly],
);
return (
<Box
onClick={stopPropagation}
@ -414,9 +428,18 @@ const CustomPropsCell = memo(({ file, customProp, readOnly }: CustomPropsCellPro
onMouseDown={stopPropagation}
onMouseMove={stopPropagation}
onDragStart={stopPropagation}
sx={{ width: "100%" }}
onMouseEnter={() => setMouseOver(true)}
onMouseLeave={() => setMouseOver(false)}
sx={{ width: "100%", display: "flex", alignItems: "center" }}
>
{getPropsContent(customProp, onChange, loading, readOnly)}
<Box sx={{ flex: 1, minWidth: 0 }}>{getPropsContent(customProp, onChange, loading, readOnly)}</Box>
{!readOnly && (
<Grow in={mouseOver} unmountOnExit>
<IconButton size="small" onClick={onDelete} disabled={loading}>
<Dismiss fontSize="small" />
</IconButton>
</Grow>
)}
</Box>
);
});

@ -5,6 +5,8 @@ import { TransitionGroup } from "react-transition-group";
import { sendMetadataPatch } from "../../../../api/api.ts";
import { CustomProps as CustomPropsType, FileResponse } from "../../../../api/explorer.ts";
import { useAppDispatch, useAppSelector } from "../../../../redux/hooks.ts";
import { fileUpdated } from "../../../../redux/fileManagerSlice.ts";
import { FileManagerIndex } from "../../FileManager.tsx";
import { DisplayOption } from "../../ContextMenu/useActionDisplayOpt.ts";
import AddButton from "./AddButton.tsx";
import CustomPropsCard from "./CustomPropsItem.tsx";
@ -28,22 +30,28 @@ const CustomProps = ({ file, setTarget, targetDisplayOptions }: CustomPropsProps
const [loading, setLoading] = useState(false);
const dispatch = useAppDispatch();
const custom_props = useAppSelector((state) => state.siteConfig.explorer?.config?.custom_props);
// `file` is a snapshot taken when the sidebar opened; prefer live metadata
// from the file list so props patched from list view appear immediately.
const liveMetadata = useAppSelector(
(state) => state.fileManager[FileManagerIndex.main]?.list?.files.find((f) => f.path === file.path)?.metadata,
);
const metadata = liveMetadata ?? file.metadata;
const existingProps = useMemo(() => {
if (!file.metadata) {
if (!metadata) {
return [];
}
return Object.keys(file.metadata)
return Object.keys(metadata)
.filter((key) => key.startsWith(customPropsMetadataPrefix))
.map((key) => {
const propId = key.slice(customPropsMetadataPrefix.length);
return {
id: propId,
props: custom_props?.find((prop) => prop.id === propId),
value: file.metadata?.[key] ?? "",
value: metadata?.[key] ?? "",
} as CustomPropsItem;
});
}, [file.metadata]);
}, [metadata, custom_props]);
const existingPropIds = useMemo(() => {
return existingProps?.map((prop) => prop.id) ?? [];
@ -62,21 +70,22 @@ const CustomProps = ({ file, setTarget, targetDisplayOptions }: CustomPropsProps
}),
)
.then(() => {
if (remove) {
const newMetadata = { ...file.metadata };
props.forEach((prop) => {
const newMetadata = { ...metadata };
props.forEach((prop) => {
if (remove) {
delete newMetadata[customPropsMetadataPrefix + prop.id];
});
setTarget({ ...file, metadata: newMetadata });
} else {
setTarget({
...file,
metadata: {
...file.metadata,
...Object.assign({}, ...props.map((prop) => ({ [customPropsMetadataPrefix + prop.id]: prop.value }))),
},
});
}
} else {
newMetadata[customPropsMetadataPrefix + prop.id] = prop.value;
}
});
const updated = { ...file, metadata: newMetadata };
setTarget(updated);
dispatch(
fileUpdated({
index: FileManagerIndex.main,
value: [{ file: updated, oldPath: file.path, includeMetadata: true }],
}),
);
})
.finally(() => {
setLoading(false);

Loading…
Cancel
Save