web: replace note creation date edit popover with dialog

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
This commit is contained in:
01zulfi
2026-01-14 14:10:44 +05:00
parent e7da0a35d3
commit 98557be52d
4 changed files with 210 additions and 256 deletions

View File

@@ -239,15 +239,15 @@ export class NotePropertiesModel extends BaseProperties {
const editIcon = this.page.locator(getTestId("edit-date-created"));
await editIcon.click();
const editDateCreatedPopup = this.page.locator(
getTestId("edit-date-created-popup")
const editDateCreatedDialog = this.page.locator(
getTestId("edit-note-creation-date-dialog")
);
await editDateCreatedPopup.waitFor({ state: "visible" });
await editDateCreatedDialog.waitFor({ state: "visible" });
const dateInput = editDateCreatedPopup.locator(
const dateInput = editDateCreatedDialog.locator(
getTestId("date-created-input")
);
const timeInput = editDateCreatedPopup.locator(
const timeInput = editDateCreatedDialog.locator(
getTestId("time-created-input")
);
@@ -255,7 +255,7 @@ export class NotePropertiesModel extends BaseProperties {
await dateInput.fill(dayjs(date).format("DD-MM-YYYY"));
await timeInput.fill(dayjs(date).format("hh:mm A"));
await editDateCreatedPopup.locator(getTestId("save-date-created")).click();
await confirmDialog(editDateCreatedDialog);
await this.close();
}

View File

@@ -24,7 +24,6 @@ import {
Unlock,
Readonly,
SyncOff,
ArrowLeft,
Circle,
Checkmark,
ChevronDown,
@@ -32,9 +31,10 @@ import {
LinkedTo,
ReferencedIn as ReferencedInIcon,
Note as NoteIcon,
Archive
Archive,
Edit
} from "../icons";
import { Box, Button, Flex, Text, FlexProps } from "@theme-ui/components";
import { Button, Flex, Text, FlexProps } from "@theme-ui/components";
import {
useEditorStore,
ReadonlyEditorSession,
@@ -45,13 +45,12 @@ import { useStore as useAppStore } from "../../stores/app-store";
import { useStore as useAttachmentStore } from "../../stores/attachment-store";
import { store as noteStore } from "../../stores/note-store";
import Toggle from "./toggle";
import { NoteDateCreatedEditor } from "./note-date-created-editor";
import { EditNoteCreationDateDialog } from "../../dialogs/edit-note-creation-date-dialog";
import ScrollContainer from "../scroll-container";
import {
getFormattedDate,
usePromise,
ResolvedItem,
useResolvedItem,
useUnresolvedItem
} from "@notesnook/common";
import { ScopedThemeProvider } from "../theme-provider";
@@ -60,7 +59,6 @@ import { VirtualizedList } from "../virtualized-list";
import { SessionItem } from "../session-item";
import {
ContentBlock,
InternalLink,
Note,
VirtualizedGrouping,
createInternalLink,
@@ -68,7 +66,6 @@ import {
} from "@notesnook/core";
import { VirtualizedTable } from "../virtualized-table";
import { TextSlice } from "@notesnook/core";
import { TITLE_BAR_HEIGHT } from "../title-bar";
import { strings } from "@notesnook/intl";
const tools = [
@@ -213,13 +210,30 @@ function EditorProperties(props: EditorPropertiesProps) {
>
{item.label}
</Text>
{item.key === "dateCreated" ? (
<NoteDateCreatedEditor
noteId={session.note.id}
dateCreated={session.note.dateCreated}
dateEdited={session.note.dateEdited}
displayValue={item.value(session.note[item.key])}
/>
<Flex sx={{ alignItems: "center", gap: 1 }}>
<Text
data-test-id="date-created"
className="selectable"
variant="subBody"
sx={{ fontSize: "body", flexShrink: 0 }}
>
{item.value(session.note[item.key])}
</Text>
<Edit
size={14}
sx={{ cursor: "pointer", color: "icon" }}
onClick={() => {
EditNoteCreationDateDialog.show({
noteId: session.note.id,
dateCreated: session.note.dateCreated,
dateEdited: session.note.dateEdited
});
}}
data-test-id="edit-date-created"
/>
</Flex>
) : (
<Text
className="selectable"

View File

@@ -1,237 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useState, useRef } from "react";
import { Button, Flex, Text } from "@theme-ui/components";
import { Calendar, Edit } from "../icons";
import Field from "../field";
import { DayPicker } from "../day-picker";
import { PopupPresenter } from "@notesnook/ui";
import { db } from "../../common/db";
import { showToast } from "../../utils/toast";
import { useStore as useThemeStore } from "../../stores/theme-store";
import { store as noteStore } from "../../stores/note-store";
import { getFormattedDate } from "@notesnook/common";
import { getTimeFormat } from "@notesnook/core";
import { setTimeOnly, setDateOnly } from "../../utils/date-time";
import dayjs from "dayjs";
import customParseFormat from "dayjs/plugin/customParseFormat";
import { strings } from "@notesnook/intl";
dayjs.extend(customParseFormat);
type NoteDateCreatedEditorProps = {
noteId: string;
dateCreated: number;
dateEdited: number;
displayValue: string;
};
export function NoteDateCreatedEditor({
noteId,
dateCreated,
dateEdited,
displayValue
}: NoteDateCreatedEditorProps) {
const [isEditing, setIsEditing] = useState(false);
const [showCalendar, setShowCalendar] = useState(false);
const [editedDate, setEditedDate] = useState<dayjs.Dayjs>(dayjs());
const dateTextRef = useRef<HTMLDivElement>(null);
const dateInputRef = useRef<HTMLInputElement>(null);
const theme = useThemeStore((store) => store.colorScheme);
return (
<>
<Flex sx={{ alignItems: "center", gap: 1 }}>
<Text
data-test-id="date-created"
ref={dateTextRef}
className="selectable"
variant="subBody"
sx={{ fontSize: "body", flexShrink: 0 }}
>
{displayValue}
</Text>
<Edit
size={14}
sx={{ cursor: "pointer", color: "icon" }}
onClick={() => {
setEditedDate(dayjs(dateCreated));
setIsEditing(true);
}}
data-test-id="edit-date-created"
/>
</Flex>
{isEditing && (
<PopupPresenter
isOpen={isEditing}
onClose={() => {
setIsEditing(false);
setShowCalendar(false);
}}
position={{
isTargetAbsolute: true,
target: dateTextRef.current,
location: "below"
}}
>
<Flex
sx={{
bg: "background",
p: 3,
boxShadow: `0px 0px 25px 5px ${
theme === "dark" ? "#000000aa" : "#0000004e"
}`,
borderRadius: "dialog",
flexDirection: "column",
gap: 2,
width: 350
}}
data-test-id="edit-date-created-popup"
>
<Text variant="subtitle">{strings.editCreationDate()}</Text>
<Flex sx={{ gap: 2, flexDirection: "column" }}>
<Field
id="date-created"
label={strings.date()}
required
inputRef={dateInputRef}
data-test-id="date-created-input"
helpText={`${db.settings.getDateFormat()}`}
action={{
icon: Calendar,
onClick() {
setShowCalendar(!showCalendar);
}
}}
validate={(t) =>
dayjs(t, db.settings.getDateFormat(), true).isValid()
}
defaultValue={editedDate.format(db.settings.getDateFormat())}
onChange={(e) =>
setEditedDate((d) => setDateOnly(e.target.value, d))
}
/>
<PopupPresenter
isOpen={showCalendar}
onClose={() => setShowCalendar(false)}
position={{
isTargetAbsolute: true,
target: dateInputRef.current,
location: "below"
}}
>
<DayPicker
sx={{
bg: "background",
p: 2,
boxShadow: `0px 0px 25px 5px ${
theme === "dark" ? "#000000aa" : "#0000004e"
}`,
borderRadius: "dialog",
width: 300
}}
selected={dayjs(editedDate).toDate()}
maxDate={new Date()}
onSelect={(day) => {
if (!day) return;
const date = getFormattedDate(day, "date");
setEditedDate((d) => setDateOnly(date, d));
if (dateInputRef.current) dateInputRef.current.value = date;
setShowCalendar(false);
}}
/>
</PopupPresenter>
<Field
id="time-created"
label={strings.time()}
required
data-test-id="time-created-input"
helpText={`${
db.settings.getTimeFormat() === "12-hour"
? "hh:mm AM/PM"
: "hh:mm"
}`}
validate={(t) => {
const format =
db.settings.getTimeFormat() === "12-hour"
? "hh:mm a"
: "HH:mm";
return dayjs(t.toLowerCase(), format, true).isValid();
}}
defaultValue={editedDate.format(
getTimeFormat(db.settings.getTimeFormat())
)}
onChange={(e) =>
setEditedDate((d) => setTimeOnly(e.target.value, d))
}
/>
</Flex>
<Flex sx={{ gap: 2, justifyContent: "flex-end" }}>
<Button
onClick={() => {
setIsEditing(false);
setShowCalendar(false);
}}
>
{strings.cancel()}
</Button>
<Button
variant="accentSecondary"
data-test-id="save-date-created"
onClick={async () => {
try {
if (editedDate.isAfter(dayjs())) {
showToast(
"error",
"Creation date cannot be in the future"
);
return;
}
if (dateEdited && editedDate.isAfter(dayjs(dateEdited))) {
showToast(
"error",
"Creation date cannot be after last edited date"
);
return;
}
await noteStore
.get()
.updateDateCreated(noteId, editedDate.valueOf());
setIsEditing(false);
setShowCalendar(false);
} catch (error) {
showToast(
"error",
"Failed to update creation date: " +
(error as Error).message
);
}
}}
>
{strings.save()}
</Button>
</Flex>
</Flex>
</PopupPresenter>
)}
</>
);
}

View File

@@ -0,0 +1,177 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { getFormattedDate } from "@notesnook/common";
import { getTimeFormat } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { PopupPresenter } from "@notesnook/ui";
import { Flex } from "@theme-ui/components";
import dayjs from "dayjs";
import customParseFormat from "dayjs/plugin/customParseFormat";
import { useRef, useState } from "react";
import { db } from "../common/db";
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
import { DayPicker } from "../components/day-picker";
import Dialog from "../components/dialog";
import Field from "../components/field";
import { Calendar } from "../components/icons";
import { store as noteStore } from "../stores/note-store";
import { useStore as useThemeStore } from "../stores/theme-store";
import { setDateOnly, setTimeOnly } from "../utils/date-time";
import { showToast } from "../utils/toast";
dayjs.extend(customParseFormat);
type EditNoteCreationDateDialogProps = BaseDialogProps<boolean> & {
noteId: string;
dateCreated: number;
dateEdited: number;
};
export const EditNoteCreationDateDialog = DialogManager.register(
function EditNoteCreationDateDialog({
noteId,
dateCreated,
dateEdited,
onClose
}: EditNoteCreationDateDialogProps) {
const [showCalendar, setShowCalendar] = useState(false);
const [date, setDate] = useState<dayjs.Dayjs>(dayjs(dateCreated));
const dateInputRef = useRef<HTMLInputElement>(null);
const theme = useThemeStore((store) => store.colorScheme);
return (
<Dialog
isOpen
testId="edit-note-creation-date-dialog"
onClose={() => {
setShowCalendar(false);
onClose(false);
}}
title={strings.editCreationDate()}
negativeButton={{
text: strings.cancel(),
onClick: () => {
setShowCalendar(false);
onClose(false);
}
}}
positiveButton={{
text: strings.save(),
onClick: async () => {
try {
if (date.isAfter(dayjs())) {
showToast("error", "Creation date cannot be in the future");
return;
}
if (dateEdited && date.isAfter(dayjs(dateEdited))) {
showToast(
"error",
"Creation date cannot be after last edited date"
);
return;
}
await noteStore.get().updateDateCreated(noteId, date.valueOf());
setShowCalendar(false);
onClose(false);
} catch (error) {
showToast(
"error",
"Failed to update creation date: " + (error as Error).message
);
}
}
}}
>
<Flex sx={{ gap: 2, flexDirection: "column" }}>
<Field
id="date-created"
label={strings.date()}
required
inputRef={dateInputRef}
data-test-id="date-created-input"
helpText={`${db.settings.getDateFormat()}`}
action={{
icon: Calendar,
onClick() {
setShowCalendar(!showCalendar);
}
}}
validate={(t) =>
dayjs(t, db.settings.getDateFormat(), true).isValid()
}
defaultValue={date.format(db.settings.getDateFormat())}
onChange={(e) => setDate((d) => setDateOnly(e.target.value, d))}
/>
<PopupPresenter
isOpen={showCalendar}
onClose={() => setShowCalendar(false)}
position={{
isTargetAbsolute: true,
target: dateInputRef.current,
location: "below"
}}
>
<DayPicker
sx={{
bg: "background",
p: 2,
boxShadow: `0px 0px 25px 5px ${
theme === "dark" ? "#000000aa" : "#0000004e"
}`,
borderRadius: "dialog",
width: 300
}}
selected={dayjs(date).toDate()}
maxDate={new Date()}
onSelect={(day) => {
if (!day) return;
const date = getFormattedDate(day, "date");
setDate((d) => setDateOnly(date, d));
if (dateInputRef.current) dateInputRef.current.value = date;
setShowCalendar(false);
}}
/>
</PopupPresenter>
<Field
id="time-created"
label={strings.time()}
required
data-test-id="time-created-input"
helpText={`${
db.settings.getTimeFormat() === "12-hour"
? "hh:mm AM/PM"
: "hh:mm"
}`}
validate={(t) => {
const format =
db.settings.getTimeFormat() === "12-hour" ? "hh:mm a" : "HH:mm";
return dayjs(t.toLowerCase(), format, true).isValid();
}}
defaultValue={date.format(
getTimeFormat(db.settings.getTimeFormat())
)}
onChange={(e) => setDate((d) => setTimeOnly(e.target.value, d))}
/>
</Flex>
</Dialog>
);
}
);