mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
feat: update memo editor
This commit is contained in:
@ -18,11 +18,11 @@ const getCellAdditionalStyles = (count: number, maxCount: number) => {
|
|||||||
|
|
||||||
const ratio = count / maxCount;
|
const ratio = count / maxCount;
|
||||||
if (ratio > 0.7) {
|
if (ratio > 0.7) {
|
||||||
return "bg-teal-600 text-gray-100 dark:opacity-80";
|
return "bg-teal-700 text-gray-100 dark:opacity-80";
|
||||||
} else if (ratio > 0.4) {
|
} else if (ratio > 0.4) {
|
||||||
return "bg-teal-400 text-gray-100 dark:opacity-80";
|
return "bg-teal-600 text-gray-100 dark:opacity-80";
|
||||||
} else {
|
} else {
|
||||||
return "bg-teal-300 text-gray-100 dark:opacity-80";
|
return "bg-teal-500 text-gray-100 dark:opacity-70";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -58,15 +58,15 @@ const ActivityCalendar = (props: Props) => {
|
|||||||
const tooltipText = count ? t("memo.count-memos-in-date", { count: count, date: date }) : date;
|
const tooltipText = count ? t("memo.count-memos-in-date", { count: count, date: date }) : date;
|
||||||
const isSelected = new Date(props.selectedDate).toDateString() === new Date(date).toDateString();
|
const isSelected = new Date(props.selectedDate).toDateString() === new Date(date).toDateString();
|
||||||
return day ? (
|
return day ? (
|
||||||
|
count > 0 ? (
|
||||||
<Tooltip className="shrink-0" key={`${date}-${index}`} title={tooltipText} placement="top" arrow>
|
<Tooltip className="shrink-0" key={`${date}-${index}`} title={tooltipText} placement="top" arrow>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-6 h-6 text-xs rounded-xl flex justify-center items-center border",
|
"w-6 h-6 text-xs rounded-xl flex justify-center items-center border cursor-default",
|
||||||
getCellAdditionalStyles(count, maxCount),
|
getCellAdditionalStyles(count, maxCount),
|
||||||
isToday && "border-gray-600 dark:border-zinc-300",
|
isToday && "border-zinc-400 dark:border-zinc-300",
|
||||||
isSelected && "font-bold border-gray-600 dark:border-zinc-300",
|
isSelected && "font-bold border-zinc-400 dark:border-zinc-300",
|
||||||
!isToday && !isSelected && "border-transparent",
|
!isToday && !isSelected && "border-transparent",
|
||||||
count > 0 ? "cursor-pointer" : "cursor-default",
|
|
||||||
)}
|
)}
|
||||||
onClick={() => count && onClick && onClick(new Date(date).toDateString())}
|
onClick={() => count && onClick && onClick(new Date(date).toDateString())}
|
||||||
>
|
>
|
||||||
@ -77,10 +77,17 @@ const ActivityCalendar = (props: Props) => {
|
|||||||
<div
|
<div
|
||||||
key={`${date}-${index}`}
|
key={`${date}-${index}`}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"shrink-0 opacity-30 w-6 h-6 rounded-xl flex justify-center items-center border border-transparent",
|
"w-6 h-6 text-xs rounded-xl flex justify-center items-center border cursor-default",
|
||||||
getCellAdditionalStyles(count, maxCount),
|
"bg-gray-100 text-gray-400 dark:bg-zinc-800 dark:text-gray-500",
|
||||||
|
isToday && "border-zinc-400 dark:border-zinc-500",
|
||||||
|
!isToday && !isSelected && "border-transparent",
|
||||||
)}
|
)}
|
||||||
></div>
|
>
|
||||||
|
{day}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div key={`${date}-${index}`} className={clsx("shrink-0 w-6 h-6 opacity-0", getCellAdditionalStyles(count, maxCount))}></div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,7 +9,6 @@ import { useMemoStore } from "@/store/v1";
|
|||||||
import { RowStatus } from "@/types/proto/api/v1/common";
|
import { RowStatus } from "@/types/proto/api/v1/common";
|
||||||
import { Memo } from "@/types/proto/api/v1/memo_service";
|
import { Memo } from "@/types/proto/api/v1/memo_service";
|
||||||
import { useTranslate } from "@/utils/i18n";
|
import { useTranslate } from "@/utils/i18n";
|
||||||
import showMemoEditorDialog from "./MemoEditor/MemoEditorDialog";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
memo: Memo;
|
memo: Memo;
|
||||||
@ -55,12 +54,6 @@ const MemoActionMenu = (props: Props) => {
|
|||||||
props.onEdit();
|
props.onEdit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: remove me later.
|
|
||||||
showMemoEditorDialog({
|
|
||||||
memoName: memo.name,
|
|
||||||
cacheKey: `${memo.name}-${memo.updateTime}`,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleMemoStatusClick = async () => {
|
const handleToggleMemoStatusClick = async () => {
|
||||||
@ -125,7 +118,7 @@ const MemoActionMenu = (props: Props) => {
|
|||||||
{memo.pinned ? t("common.unpin") : t("common.pin")}
|
{memo.pinned ? t("common.unpin") : t("common.pin")}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
{!hiddenActions?.includes("edit") && (
|
{!hiddenActions?.includes("edit") && props.onEdit && (
|
||||||
<MenuItem onClick={handleEditMemoClick}>
|
<MenuItem onClick={handleEditMemoClick}>
|
||||||
<Icon.Edit3 className="w-4 h-auto" />
|
<Icon.Edit3 className="w-4 h-auto" />
|
||||||
{t("common.edit")}
|
{t("common.edit")}
|
||||||
|
@ -1,103 +0,0 @@
|
|||||||
import { IconButton } from "@mui/joy";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import { useEffect, useRef } from "react";
|
|
||||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
|
||||||
import useDateTime from "@/hooks/useDateTime";
|
|
||||||
import { useMemoStore, useTagStore } from "@/store/v1";
|
|
||||||
import { Memo } from "@/types/proto/api/v1/memo_service";
|
|
||||||
import MemoEditor, { Props as MemoEditorProps } from ".";
|
|
||||||
import { generateDialog } from "../Dialog";
|
|
||||||
import Icon from "../Icon";
|
|
||||||
|
|
||||||
interface Props extends DialogProps, MemoEditorProps {}
|
|
||||||
|
|
||||||
const MemoEditorDialog: React.FC<Props> = ({
|
|
||||||
memoName,
|
|
||||||
parentMemoName,
|
|
||||||
placeholder,
|
|
||||||
cacheKey,
|
|
||||||
relationList,
|
|
||||||
onConfirm,
|
|
||||||
destroy,
|
|
||||||
}: Props) => {
|
|
||||||
const tagStore = useTagStore();
|
|
||||||
const memoStore = useMemoStore();
|
|
||||||
const { setDateTime, displayDateTime, datePickerDateTime } = useDateTime(memoStore.getMemoByName(memoName || "")?.displayTime);
|
|
||||||
const memoPatchRef = useRef<Partial<Memo>>({
|
|
||||||
displayTime: memoStore.getMemoByName(memoName || "")?.displayTime,
|
|
||||||
});
|
|
||||||
const user = useCurrentUser();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
tagStore.fetchTags({ user }, { skipCache: false });
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const updateDisplayTime = (displayTime: string) => {
|
|
||||||
setDateTime(displayTime);
|
|
||||||
memoPatchRef.current.displayTime = new Date(displayTime);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCloseBtnClick = () => {
|
|
||||||
destroy();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleConfirm = (memoName: string) => {
|
|
||||||
handleCloseBtnClick();
|
|
||||||
if (onConfirm) {
|
|
||||||
onConfirm(memoName);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="w-full flex flex-row justify-between items-center">
|
|
||||||
<div className={clsx("flex flex-row justify-start items-center", !displayDateTime && "mb-2")}>
|
|
||||||
{displayDateTime ? (
|
|
||||||
<div className="relative">
|
|
||||||
<span className="cursor-pointer text-gray-500 dark:text-gray-400">{displayDateTime}</span>
|
|
||||||
<input
|
|
||||||
className="inset-0 absolute z-1 opacity-0"
|
|
||||||
type="datetime-local"
|
|
||||||
value={datePickerDateTime}
|
|
||||||
onFocus={(e: any) => e.target.showPicker()}
|
|
||||||
onChange={(e) => updateDisplayTime(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<img className="w-6 h-auto rounded-full shadow" src={"/full-logo.webp"} alt="" />
|
|
||||||
<p className="ml-1 text-lg opacity-80 dark:text-gray-300">Memos</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<IconButton size="sm" onClick={handleCloseBtnClick}>
|
|
||||||
<Icon.X className="w-5 h-auto" />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col justify-start items-start max-w-full w-[40rem]">
|
|
||||||
<MemoEditor
|
|
||||||
className="border-none !p-0 -mb-2"
|
|
||||||
cacheKey={`memo-editor-${cacheKey || memoName}`}
|
|
||||||
memoName={memoName}
|
|
||||||
parentMemoName={parentMemoName}
|
|
||||||
placeholder={placeholder}
|
|
||||||
relationList={relationList}
|
|
||||||
memoPatchRef={memoPatchRef}
|
|
||||||
onConfirm={handleConfirm}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function showMemoEditorDialog(props: Partial<Props> = {}): void {
|
|
||||||
generateDialog(
|
|
||||||
{
|
|
||||||
className: "memo-editor-dialog",
|
|
||||||
dialogName: "memo-editor-dialog",
|
|
||||||
},
|
|
||||||
MemoEditorDialog,
|
|
||||||
props,
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,4 +1,5 @@
|
|||||||
import { Select, Option, Button, Divider } from "@mui/joy";
|
import { Select, Option, Button, Divider } from "@mui/joy";
|
||||||
|
import { isEqual } from "lodash-es";
|
||||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@ -6,6 +7,7 @@ import useLocalStorage from "react-use/lib/useLocalStorage";
|
|||||||
import { memoServiceClient } from "@/grpcweb";
|
import { memoServiceClient } from "@/grpcweb";
|
||||||
import { TAB_SPACE_WIDTH } from "@/helpers/consts";
|
import { TAB_SPACE_WIDTH } from "@/helpers/consts";
|
||||||
import { isValidUrl } from "@/helpers/utils";
|
import { isValidUrl } from "@/helpers/utils";
|
||||||
|
import useAsyncEffect from "@/hooks/useAsyncEffect";
|
||||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||||
import { useMemoStore, useResourceStore, useUserStore, useWorkspaceSettingStore } from "@/store/v1";
|
import { useMemoStore, useResourceStore, useUserStore, useWorkspaceSettingStore } from "@/store/v1";
|
||||||
import { MemoRelation, MemoRelation_Type } from "@/types/proto/api/v1/memo_relation_service";
|
import { MemoRelation, MemoRelation_Type } from "@/types/proto/api/v1/memo_relation_service";
|
||||||
@ -32,11 +34,11 @@ export interface Props {
|
|||||||
className?: string;
|
className?: string;
|
||||||
cacheKey?: string;
|
cacheKey?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
// The name of the memo to be edited.
|
||||||
memoName?: string;
|
memoName?: string;
|
||||||
|
// The name of the parent memo if the memo is a comment.
|
||||||
parentMemoName?: string;
|
parentMemoName?: string;
|
||||||
relationList?: MemoRelation[];
|
|
||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
memoPatchRef?: React.MutableRefObject<Partial<Memo>>;
|
|
||||||
onConfirm?: (memoName: string) => void;
|
onConfirm?: (memoName: string) => void;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
}
|
}
|
||||||
@ -62,11 +64,12 @@ const MemoEditor = (props: Props) => {
|
|||||||
const [state, setState] = useState<State>({
|
const [state, setState] = useState<State>({
|
||||||
memoVisibility: Visibility.PRIVATE,
|
memoVisibility: Visibility.PRIVATE,
|
||||||
resourceList: [],
|
resourceList: [],
|
||||||
relationList: props.relationList ?? [],
|
relationList: [],
|
||||||
isUploadingResource: false,
|
isUploadingResource: false,
|
||||||
isRequesting: false,
|
isRequesting: false,
|
||||||
isComposing: false,
|
isComposing: false,
|
||||||
});
|
});
|
||||||
|
const [displayTime, setDisplayTime] = useState<Date | undefined>();
|
||||||
const [hasContent, setHasContent] = useState<boolean>(false);
|
const [hasContent, setHasContent] = useState<boolean>(false);
|
||||||
const editorRef = useRef<EditorRefActions>(null);
|
const editorRef = useRef<EditorRefActions>(null);
|
||||||
const userSetting = userStore.userSetting as UserSetting;
|
const userSetting = userStore.userSetting as UserSetting;
|
||||||
@ -102,9 +105,12 @@ const MemoEditor = (props: Props) => {
|
|||||||
}));
|
}));
|
||||||
}, [userSetting.memoVisibility, workspaceMemoRelatedSetting.disallowPublicVisibility]);
|
}, [userSetting.memoVisibility, workspaceMemoRelatedSetting.disallowPublicVisibility]);
|
||||||
|
|
||||||
useEffect(() => {
|
useAsyncEffect(async () => {
|
||||||
if (memoName) {
|
if (!memoName) {
|
||||||
memoStore.getOrFetchMemoByName(memoName).then((memo) => {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const memo = await memoStore.getOrFetchMemoByName(memoName);
|
||||||
if (memo) {
|
if (memo) {
|
||||||
handleEditorFocus();
|
handleEditorFocus();
|
||||||
setState((prevState) => ({
|
setState((prevState) => ({
|
||||||
@ -113,12 +119,11 @@ const MemoEditor = (props: Props) => {
|
|||||||
resourceList: memo.resources,
|
resourceList: memo.resources,
|
||||||
relationList: memo.relations,
|
relationList: memo.relations,
|
||||||
}));
|
}));
|
||||||
|
setDisplayTime(memo.displayTime);
|
||||||
if (!contentCache) {
|
if (!contentCache) {
|
||||||
editorRef.current?.setContent(memo.content ?? "");
|
editorRef.current?.setContent(memo.content ?? "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [memoName]);
|
}, [memoName]);
|
||||||
|
|
||||||
const handleCompositionStart = () => {
|
const handleCompositionStart = () => {
|
||||||
@ -289,18 +294,16 @@ const MemoEditor = (props: Props) => {
|
|||||||
const prevMemo = await memoStore.getOrFetchMemoByName(memoName);
|
const prevMemo = await memoStore.getOrFetchMemoByName(memoName);
|
||||||
if (prevMemo) {
|
if (prevMemo) {
|
||||||
const updateMask = ["content", "visibility"];
|
const updateMask = ["content", "visibility"];
|
||||||
if (props.memoPatchRef?.current?.displayTime) {
|
const memoPatch: Partial<Memo> = {
|
||||||
updateMask.push("display_time");
|
|
||||||
}
|
|
||||||
const memo = await memoStore.updateMemo(
|
|
||||||
{
|
|
||||||
name: prevMemo.name,
|
name: prevMemo.name,
|
||||||
content,
|
content,
|
||||||
visibility: state.memoVisibility,
|
visibility: state.memoVisibility,
|
||||||
...props.memoPatchRef?.current,
|
};
|
||||||
},
|
if (!isEqual(displayTime, prevMemo.displayTime)) {
|
||||||
updateMask,
|
updateMask.push("display_time");
|
||||||
);
|
memoPatch.displayTime = displayTime;
|
||||||
|
}
|
||||||
|
const memo = await memoStore.updateMemo(memoPatch, updateMask);
|
||||||
await memoServiceClient.setMemoResources({
|
await memoServiceClient.setMemoResources({
|
||||||
name: memo.name,
|
name: memo.name,
|
||||||
resources: state.resourceList,
|
resources: state.resourceList,
|
||||||
@ -409,6 +412,18 @@ const MemoEditor = (props: Props) => {
|
|||||||
onCompositionStart={handleCompositionStart}
|
onCompositionStart={handleCompositionStart}
|
||||||
onCompositionEnd={handleCompositionEnd}
|
onCompositionEnd={handleCompositionEnd}
|
||||||
>
|
>
|
||||||
|
{memoName && displayTime && (
|
||||||
|
<div className="relative text-sm">
|
||||||
|
<span className="cursor-pointer text-gray-400 dark:text-gray-500">{displayTime.toLocaleString()}</span>
|
||||||
|
<input
|
||||||
|
className="inset-0 absolute z-1 opacity-0"
|
||||||
|
type="datetime-local"
|
||||||
|
value={displayTime.toLocaleString()}
|
||||||
|
onFocus={(e: any) => e.target.showPicker()}
|
||||||
|
onChange={(e) => setDisplayTime(new Date(e.target.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Editor ref={editorRef} {...editorConfig} />
|
<Editor ref={editorRef} {...editorConfig} />
|
||||||
<ResourceListView resourceList={state.resourceList} setResourceList={handleSetResourceList} />
|
<ResourceListView resourceList={state.resourceList} setResourceList={handleSetResourceList} />
|
||||||
<RelationListView relationList={referenceRelations} setRelationList={handleSetRelationList} />
|
<RelationListView relationList={referenceRelations} setRelationList={handleSetRelationList} />
|
||||||
|
@ -44,7 +44,7 @@ const MemoRelationListView = (props: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex flex-col justify-start items-start w-full px-2 pt-2 pb-1 bg-zinc-50 dark:bg-zinc-900 rounded-lg border border-gray-200 dark:border-zinc-700">
|
<div className="relative flex flex-col justify-start items-start w-full px-2 pt-2 pb-1.5 bg-zinc-50 dark:bg-zinc-900 rounded-lg border border-gray-200 dark:border-zinc-700">
|
||||||
<div className="w-full flex flex-row justify-start items-center mb-1 gap-3 opacity-60">
|
<div className="w-full flex flex-row justify-start items-center mb-1 gap-3 opacity-60">
|
||||||
{referencingMemoList.length > 0 && (
|
{referencingMemoList.length > 0 && (
|
||||||
<button
|
<button
|
||||||
@ -56,6 +56,7 @@ const MemoRelationListView = (props: Props) => {
|
|||||||
>
|
>
|
||||||
<Icon.Link className="w-3 h-auto shrink-0 opacity-70" />
|
<Icon.Link className="w-3 h-auto shrink-0 opacity-70" />
|
||||||
<span>Referencing</span>
|
<span>Referencing</span>
|
||||||
|
<span className="opacity-80">({referencingMemoList.length})</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{referencedMemoList.length > 0 && (
|
{referencedMemoList.length > 0 && (
|
||||||
@ -68,6 +69,7 @@ const MemoRelationListView = (props: Props) => {
|
|||||||
>
|
>
|
||||||
<Icon.Milestone className="w-3 h-auto shrink-0 opacity-70" />
|
<Icon.Milestone className="w-3 h-auto shrink-0 opacity-70" />
|
||||||
<span>Referenced by</span>
|
<span>Referenced by</span>
|
||||||
|
<span className="opacity-80">({referencedMemoList.length})</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -81,7 +83,7 @@ const MemoRelationListView = (props: Props) => {
|
|||||||
to={`/m/${memo.uid}`}
|
to={`/m/${memo.uid}`}
|
||||||
unstable_viewTransition
|
unstable_viewTransition
|
||||||
>
|
>
|
||||||
<Icon.Dot className="shrink-0 -ml-1 opacity-60" />
|
<Icon.Dot className="shrink-0 w-4 h-auto opacity-40" />
|
||||||
<span className="truncate">{memo.snippet}</span>
|
<span className="truncate">{memo.snippet}</span>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
@ -98,7 +100,7 @@ const MemoRelationListView = (props: Props) => {
|
|||||||
to={`/m/${memo.uid}`}
|
to={`/m/${memo.uid}`}
|
||||||
unstable_viewTransition
|
unstable_viewTransition
|
||||||
>
|
>
|
||||||
<Icon.Dot className="shrink-0 -ml-1 opacity-60" />
|
<Icon.Dot className="shrink-0 w-4 h-auto opacity-40" />
|
||||||
<span className="truncate">{memo.snippet}</span>
|
<span className="truncate">{memo.snippet}</span>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
@ -106,6 +106,17 @@ const MemoView: React.FC<Props> = (props: Props) => {
|
|||||||
)}
|
)}
|
||||||
ref={memoContainerRef}
|
ref={memoContainerRef}
|
||||||
>
|
>
|
||||||
|
{showEditor ? (
|
||||||
|
<MemoEditor
|
||||||
|
autoFocus
|
||||||
|
className="border-none !p-0 -mb-2"
|
||||||
|
cacheKey={`inline-memo-editor-${memo.name}`}
|
||||||
|
memoName={memo.name}
|
||||||
|
onConfirm={() => setShowEditor(false)}
|
||||||
|
onCancel={() => setShowEditor(false)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<div className="w-full flex flex-row justify-between items-center gap-2">
|
<div className="w-full flex flex-row justify-between items-center gap-2">
|
||||||
<div className="w-auto max-w-[calc(100%-8rem)] grow flex flex-row justify-start items-center">
|
<div className="w-auto max-w-[calc(100%-8rem)] grow flex flex-row justify-start items-center">
|
||||||
{props.showCreator && creator ? (
|
{props.showCreator && creator ? (
|
||||||
@ -130,12 +141,14 @@ const MemoView: React.FC<Props> = (props: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full text-sm leading-tight text-gray-400 dark:text-gray-500 select-none" onClick={handleGotoMemoDetailPage}>
|
<div
|
||||||
|
className="w-full text-sm leading-tight text-gray-400 dark:text-gray-500 select-none"
|
||||||
|
onClick={handleGotoMemoDetailPage}
|
||||||
|
>
|
||||||
{displayTime}
|
{displayTime}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!showEditor && (
|
|
||||||
<div className="flex flex-row justify-end items-center select-none shrink-0 gap-2">
|
<div className="flex flex-row justify-end items-center select-none shrink-0 gap-2">
|
||||||
<div className="w-auto invisible group-hover:visible flex flex-row justify-between items-center gap-2">
|
<div className="w-auto invisible group-hover:visible flex flex-row justify-between items-center gap-2">
|
||||||
{props.showVisibility && memo.visibility !== Visibility.PRIVATE && (
|
{props.showVisibility && memo.visibility !== Visibility.PRIVATE && (
|
||||||
@ -174,20 +187,7 @@ const MemoView: React.FC<Props> = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showEditor ? (
|
|
||||||
<MemoEditor
|
|
||||||
autoFocus
|
|
||||||
className="border-none !p-0 -mb-2"
|
|
||||||
cacheKey={`inline-memo-editor-${memo.name}`}
|
|
||||||
memoName={memo.name}
|
|
||||||
onConfirm={() => setShowEditor(false)}
|
|
||||||
onCancel={() => setShowEditor(false)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<MemoContent
|
<MemoContent
|
||||||
key={`${memo.name}-${memo.updateTime}`}
|
key={`${memo.name}-${memo.updateTime}`}
|
||||||
memoName={memo.name}
|
memoName={memo.name}
|
||||||
|
@ -12,6 +12,7 @@ import { useMemoStore } from "@/store/v1";
|
|||||||
import { useTranslate } from "@/utils/i18n";
|
import { useTranslate } from "@/utils/i18n";
|
||||||
import ActivityCalendar from "./ActivityCalendar";
|
import ActivityCalendar from "./ActivityCalendar";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "./ui/Popover";
|
||||||
|
|
||||||
interface UserMemoStats {
|
interface UserMemoStats {
|
||||||
link: number;
|
link: number;
|
||||||
@ -26,15 +27,14 @@ const UserStatisticsView = () => {
|
|||||||
const memoStore = useMemoStore();
|
const memoStore = useMemoStore();
|
||||||
const filterStore = useFilterStore();
|
const filterStore = useFilterStore();
|
||||||
const [memoAmount, setMemoAmount] = useState(0);
|
const [memoAmount, setMemoAmount] = useState(0);
|
||||||
const [isRequesting, setIsRequesting] = useState(false);
|
|
||||||
const [memoStats, setMemoStats] = useState<UserMemoStats>({ link: 0, taskList: 0, code: 0, incompleteTasks: 0 });
|
const [memoStats, setMemoStats] = useState<UserMemoStats>({ link: 0, taskList: 0, code: 0, incompleteTasks: 0 });
|
||||||
const [activityStats, setActivityStats] = useState<Record<string, number>>({});
|
const [activityStats, setActivityStats] = useState<Record<string, number>>({});
|
||||||
const monthString = dayjs(new Date().toDateString()).format("YYYY-MM");
|
const [selectedDate] = useState(new Date());
|
||||||
|
const [monthString, setMonthString] = useState(dayjs(selectedDate.toDateString()).format("YYYY-MM"));
|
||||||
const days = Math.ceil((Date.now() - currentUser.createTime!.getTime()) / 86400000);
|
const days = Math.ceil((Date.now() - currentUser.createTime!.getTime()) / 86400000);
|
||||||
const filter = filterStore.state;
|
const filter = filterStore.state;
|
||||||
|
|
||||||
useAsyncEffect(async () => {
|
useAsyncEffect(async () => {
|
||||||
setIsRequesting(true);
|
|
||||||
const { properties } = await memoServiceClient.listMemoProperties({
|
const { properties } = await memoServiceClient.listMemoProperties({
|
||||||
name: `memos/-`,
|
name: `memos/-`,
|
||||||
});
|
});
|
||||||
@ -62,7 +62,6 @@ const UserStatisticsView = () => {
|
|||||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
filter: filters.join(" && "),
|
filter: filters.join(" && "),
|
||||||
});
|
});
|
||||||
|
|
||||||
setActivityStats(
|
setActivityStats(
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
Object.entries(stats).filter(([date]) => {
|
Object.entries(stats).filter(([date]) => {
|
||||||
@ -70,7 +69,6 @@ const UserStatisticsView = () => {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
setIsRequesting(false);
|
|
||||||
}, [memoStore.stateId]);
|
}, [memoStore.stateId]);
|
||||||
|
|
||||||
const handleRebuildMemoTags = async () => {
|
const handleRebuildMemoTags = async () => {
|
||||||
@ -83,39 +81,42 @@ const UserStatisticsView = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group w-full border mt-2 py-2 px-3 rounded-lg space-y-0.5 text-gray-500 dark:text-gray-400 bg-zinc-50 dark:bg-zinc-900 dark:border-zinc-800">
|
<div className="group w-full border mt-2 py-2 px-3 rounded-lg space-y-0.5 text-gray-500 dark:text-gray-400 bg-zinc-50 dark:bg-zinc-900 dark:border-zinc-800">
|
||||||
<div className="w-full mb-1 flex flex-row justify-between items-center">
|
<div className="w-full mb-2 flex flex-row justify-between items-center">
|
||||||
<p className="text-sm font-medium leading-6 dark:text-gray-400">
|
<div className="relative text-base font-medium leading-6 flex flex-row items-center dark:text-gray-400">
|
||||||
{new Date().toLocaleDateString(i18n.language, { month: "long", day: "numeric" })}
|
<Icon.CalendarDays className="w-5 h-auto mr-1 opacity-60" strokeWidth={1.5} />
|
||||||
</p>
|
<span>{new Date(monthString).toLocaleString(i18n.language, { year: "numeric", month: "long" })}</span>
|
||||||
<div className="group-hover:block hidden">
|
<input
|
||||||
<Tooltip title={"Refresh"} placement="top">
|
className="inset-0 absolute z-1 opacity-0"
|
||||||
<Icon.RefreshCcw
|
type="month"
|
||||||
className="text-gray-400 w-4 h-auto cursor-pointer opacity-60 hover:opacity-100"
|
value={monthString}
|
||||||
onClick={handleRebuildMemoTags}
|
onFocus={(e: any) => e.target.showPicker()}
|
||||||
|
onChange={(e) => setMonthString(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</div>
|
||||||
|
<div className="invisible group-hover:visible flex justify-end items-center">
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Icon.MoreVertical className="w-4 h-auto shrink-0 opacity-60" />
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent>
|
||||||
|
<button className="w-auto flex flex-row justify-between items-center gap-2 hover:opacity-80" onClick={handleRebuildMemoTags}>
|
||||||
|
<Icon.RefreshCcw className="text-gray-400 w-4 h-auto cursor-pointer opacity-60" />
|
||||||
|
<span className="text-sm shrink-0 text-gray-500 dark:text-gray-400">Refresh</span>
|
||||||
|
</button>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full pb-2">
|
<div className="w-full">
|
||||||
<ActivityCalendar month={monthString} selectedDate={new Date().toDateString()} data={activityStats} />
|
<ActivityCalendar month={monthString} selectedDate={selectedDate.toDateString()} data={activityStats} />
|
||||||
|
{memoAmount > 0 && (
|
||||||
|
<p className="mt-1 w-full text-xs italic opacity-80">
|
||||||
|
<span>{memoAmount}</span> memos in <span>{days}</span> days
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full grid grid-cols-1 gap-x-4">
|
<Divider className="!my-2 opacity-50" />
|
||||||
<div className="w-full flex justify-between items-center">
|
<div className="w-full flex flex-row justify-start items-center gap-x-2 gap-y-1 flex-wrap">
|
||||||
<div className="w-auto flex justify-start items-center">
|
|
||||||
<Icon.CalendarDays className="w-4 h-auto mr-1" />
|
|
||||||
<span className="block text-base sm:text-sm">Days</span>
|
|
||||||
</div>
|
|
||||||
<span>{days}</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-full flex justify-between items-center">
|
|
||||||
<div className="w-auto flex justify-start items-center">
|
|
||||||
<Icon.Library className="w-4 h-auto mr-1" />
|
|
||||||
<span className="block text-base sm:text-sm">Memos</span>
|
|
||||||
</div>
|
|
||||||
{isRequesting ? <Icon.Loader className="animate-spin w-4 h-auto text-gray-400" /> : <span className="">{memoAmount}</span>}
|
|
||||||
</div>
|
|
||||||
<Divider className="!my-1 opacity-50" />
|
|
||||||
<div className="w-full mt-1 flex flex-row justify-start items-center gap-x-2 gap-y-1 flex-wrap">
|
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-auto border dark:border-zinc-800 pl-1 pr-1.5 rounded-md flex justify-between items-center cursor-pointer hover:shadow",
|
"w-auto border dark:border-zinc-800 pl-1 pr-1.5 rounded-md flex justify-between items-center cursor-pointer hover:shadow",
|
||||||
@ -171,7 +172,6 @@ const UserStatisticsView = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,4 +4,3 @@ export * from "./useNavigateTo";
|
|||||||
export * from "./useAsyncEffect";
|
export * from "./useAsyncEffect";
|
||||||
export * from "./useFilterWithUrlParams";
|
export * from "./useFilterWithUrlParams";
|
||||||
export * from "./useResponsiveWidth";
|
export * from "./useResponsiveWidth";
|
||||||
export * from "./useDateTime";
|
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
const useDateTime = (initalState?: Date) => {
|
|
||||||
const [dateTime, setDateTimeInternal] = useState<Date | undefined>(initalState && new Date(initalState));
|
|
||||||
|
|
||||||
return {
|
|
||||||
setDateTime: (dateTimeString: string) => setDateTimeInternal(new Date(dateTimeString)),
|
|
||||||
displayDateTime: dateTime && dateTime.toLocaleString(),
|
|
||||||
datePickerDateTime: dateTime && new Date(dateTime.getTime() - dateTime.getTimezoneOffset() * 60000).toISOString().split(".")[0],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useDateTime;
|
|
Reference in New Issue
Block a user