mirror of
https://github.com/zadam/trilium.git
synced 2025-09-08 15:47:27 +08:00
refactor(react): fix a few rules of hooks violations
This commit is contained in:
parent
e386b03b90
commit
733ec2c145
9 changed files with 125 additions and 162 deletions
|
@ -5,12 +5,7 @@ import { dismissCallToAction, getCallToActions } from "./call_to_action_definiti
|
|||
import { t } from "../../services/i18n";
|
||||
|
||||
export default function CallToActionDialog() {
|
||||
const activeCallToActions = useMemo(() => getCallToActions(), []);
|
||||
|
||||
if (!activeCallToActions.length) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const activeCallToActions = useMemo(() => getCallToActions(), []);
|
||||
const [ activeIndex, setActiveIndex ] = useState(0);
|
||||
const [ shown, setShown ] = useState(true);
|
||||
const activeItem = activeCallToActions[activeIndex];
|
||||
|
@ -23,7 +18,7 @@ export default function CallToActionDialog() {
|
|||
}
|
||||
}
|
||||
|
||||
return (
|
||||
return (activeCallToActions.length &&
|
||||
<Modal
|
||||
className="call-to-action"
|
||||
size="md"
|
||||
|
|
|
@ -36,25 +36,23 @@ export default function NoteTypeChooserDialogComponent() {
|
|||
setShown(true);
|
||||
});
|
||||
|
||||
if (!noteTypes.length) {
|
||||
useEffect(() => {
|
||||
note_types.getNoteTypeItems().then(noteTypes => {
|
||||
let index = -1;
|
||||
useEffect(() => {
|
||||
note_types.getNoteTypeItems().then(noteTypes => {
|
||||
let index = -1;
|
||||
|
||||
setNoteTypes((noteTypes ?? []).map((item) => {
|
||||
if (item.title === "----") {
|
||||
index++;
|
||||
return {
|
||||
title: SEPARATOR_TITLE_REPLACEMENTS[index],
|
||||
enabled: false
|
||||
}
|
||||
setNoteTypes((noteTypes ?? []).map((item) => {
|
||||
if (item.title === "----") {
|
||||
index++;
|
||||
return {
|
||||
title: SEPARATOR_TITLE_REPLACEMENTS[index],
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
|
||||
return item;
|
||||
}));
|
||||
});
|
||||
return item;
|
||||
}));
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
function onNoteTypeSelected(value: string) {
|
||||
const [ noteType, templateNoteId ] = value.split(",");
|
||||
|
|
|
@ -18,34 +18,27 @@ import { useTriliumEvent } from "../react/hooks";
|
|||
export default function RecentChangesDialog() {
|
||||
const [ ancestorNoteId, setAncestorNoteId ] = useState<string>();
|
||||
const [ groupedByDate, setGroupedByDate ] = useState<Map<string, RecentChangeRow[]>>();
|
||||
const [ needsRefresh, setNeedsRefresh ] = useState(false);
|
||||
const [ refreshCounter, setRefreshCounter ] = useState(0);
|
||||
const [ shown, setShown ] = useState(false);
|
||||
|
||||
useTriliumEvent("showRecentChanges", ({ ancestorNoteId }) => {
|
||||
setNeedsRefresh(true);
|
||||
useTriliumEvent("showRecentChanges", ({ ancestorNoteId }) => {
|
||||
setAncestorNoteId(ancestorNoteId ?? hoisted_note.getHoistedNoteId());
|
||||
setShown(true);
|
||||
});
|
||||
|
||||
if (!groupedByDate || needsRefresh) {
|
||||
useEffect(() => {
|
||||
if (needsRefresh) {
|
||||
setNeedsRefresh(false);
|
||||
}
|
||||
useEffect(() => {
|
||||
server.get<RecentChangeRow[]>(`recent-changes/${ancestorNoteId}`)
|
||||
.then(async (recentChanges) => {
|
||||
// preload all notes into cache
|
||||
await froca.getNotes(
|
||||
recentChanges.map((r) => r.noteId),
|
||||
true
|
||||
);
|
||||
|
||||
server.get<RecentChangeRow[]>(`recent-changes/${ancestorNoteId}`)
|
||||
.then(async (recentChanges) => {
|
||||
// preload all notes into cache
|
||||
await froca.getNotes(
|
||||
recentChanges.map((r) => r.noteId),
|
||||
true
|
||||
);
|
||||
|
||||
const groupedByDate = groupByDate(recentChanges);
|
||||
setGroupedByDate(groupedByDate);
|
||||
});
|
||||
})
|
||||
}
|
||||
const groupedByDate = groupByDate(recentChanges);
|
||||
setGroupedByDate(groupedByDate);
|
||||
});
|
||||
}, [ shown, refreshCounter ])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
@ -60,7 +53,7 @@ export default function RecentChangesDialog() {
|
|||
style={{ padding: "0 10px" }}
|
||||
onClick={() => {
|
||||
server.post("notes/erase-deleted-notes-now").then(() => {
|
||||
setNeedsRefresh(true);
|
||||
setRefreshCounter(refreshCounter + 1);
|
||||
toast.showMessage(t("recent_changes.deleted_notes_message"));
|
||||
});
|
||||
}}
|
||||
|
@ -113,10 +106,6 @@ function RecentChangesTimeline({ groupedByDate, setShown }: { groupedByDate: Map
|
|||
}
|
||||
|
||||
function NoteLink({ notePath, title }: { notePath: string, title: string }) {
|
||||
if (!notePath || !title) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [ noteLink, setNoteLink ] = useState<JQuery<HTMLElement> | null>(null);
|
||||
useEffect(() => {
|
||||
link.createLink(notePath, {
|
||||
|
|
|
@ -201,17 +201,9 @@ function RevisionContent({ revisionItem, fullRevision }: { revisionItem?: Revisi
|
|||
return <></>;
|
||||
}
|
||||
|
||||
|
||||
switch (revisionItem.type) {
|
||||
case "text": {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
if (contentRef.current?.querySelector("span.math-tex")) {
|
||||
renderMathInElement(contentRef.current, { trust: true });
|
||||
}
|
||||
});
|
||||
return <div ref={contentRef} className="ck-content" dangerouslySetInnerHTML={{ __html: content as string }}></div>
|
||||
}
|
||||
case "text":
|
||||
return <RevisionContentText content={content} />
|
||||
case "code":
|
||||
return <pre style={CODE_STYLE}>{content}</pre>;
|
||||
case "image":
|
||||
|
@ -263,6 +255,16 @@ function RevisionContent({ revisionItem, fullRevision }: { revisionItem?: Revisi
|
|||
}
|
||||
}
|
||||
|
||||
function RevisionContentText({ content }: { content: string | Buffer<ArrayBufferLike> | undefined }) {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
if (contentRef.current?.querySelector("span.math-tex")) {
|
||||
renderMathInElement(contentRef.current, { trust: true });
|
||||
}
|
||||
}, [content]);
|
||||
return <div ref={contentRef} className="ck-content" dangerouslySetInnerHTML={{ __html: content as string }}></div>
|
||||
}
|
||||
|
||||
function RevisionFooter({ note }: { note?: FNote }) {
|
||||
if (!note) {
|
||||
return <></>;
|
||||
|
|
|
@ -23,12 +23,12 @@ export default function UploadAttachmentsDialog() {
|
|||
setShown(true);
|
||||
});
|
||||
|
||||
if (parentNoteId) {
|
||||
useEffect(() => {
|
||||
tree.getNoteTitle(parentNoteId).then((noteTitle) =>
|
||||
setDescription(t("upload_attachments.files_will_be_uploaded", { noteTitle })));
|
||||
}, [parentNoteId]);
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!parentNoteId) return;
|
||||
|
||||
tree.getNoteTitle(parentNoteId).then((noteTitle) =>
|
||||
setDescription(t("upload_attachments.files_will_be_uploaded", { noteTitle })));
|
||||
}, [parentNoteId]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
|
|
@ -71,29 +71,27 @@ export default function Modal({ children, className, size, title, header, footer
|
|||
const parentWidget = useContext(ParentComponent);
|
||||
const elementToFocus = useRef<Element | null>();
|
||||
|
||||
if (onShown || onHidden) {
|
||||
useEffect(() => {
|
||||
const modalElement = modalRef.current;
|
||||
if (!modalElement) {
|
||||
return;
|
||||
useEffect(() => {
|
||||
const modalElement = modalRef.current;
|
||||
if (!modalElement) {
|
||||
return;
|
||||
}
|
||||
if (onShown) {
|
||||
modalElement.addEventListener("shown.bs.modal", onShown);
|
||||
}
|
||||
modalElement.addEventListener("hidden.bs.modal", () => {
|
||||
onHidden();
|
||||
if (elementToFocus.current && "focus" in elementToFocus.current) {
|
||||
(elementToFocus.current as HTMLElement).focus();
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
if (onShown) {
|
||||
modalElement.addEventListener("shown.bs.modal", onShown);
|
||||
modalElement.removeEventListener("shown.bs.modal", onShown);
|
||||
}
|
||||
modalElement.addEventListener("hidden.bs.modal", () => {
|
||||
onHidden();
|
||||
if (elementToFocus.current && "focus" in elementToFocus.current) {
|
||||
(elementToFocus.current as HTMLElement).focus();
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
if (onShown) {
|
||||
modalElement.removeEventListener("shown.bs.modal", onShown);
|
||||
}
|
||||
modalElement.removeEventListener("hidden.bs.modal", onHidden);
|
||||
};
|
||||
}, [ ]);
|
||||
}
|
||||
modalElement.removeEventListener("hidden.bs.modal", onHidden);
|
||||
};
|
||||
}, [ onShown, onHidden ]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!parentWidget) {
|
||||
|
|
|
@ -216,15 +216,13 @@ export function useNoteContext() {
|
|||
setNote(noteContext?.note);
|
||||
});
|
||||
|
||||
useLegacyImperativeHandlers({
|
||||
setNoteContextEvent({ noteContext }: EventData<"setNoteContext">) {
|
||||
setNoteContext(noteContext);
|
||||
}
|
||||
}, true);
|
||||
const parentComponent = useContext(ParentComponent) as ReactWrappedWidget;
|
||||
(parentComponent as ReactWrappedWidget & { setNoteContextEvent: (data: EventData<"setNoteContext">) => void }).setNoteContextEvent = ({ noteContext }: EventData<"setNoteContext">) => {
|
||||
setNoteContext(noteContext);
|
||||
}
|
||||
|
||||
useDebugValue(() => `notePath=${notePath}, ntxId=${noteContext?.ntxId}`);
|
||||
|
||||
const parentComponent = useContext(ParentComponent) as ReactWrappedWidget;
|
||||
|
||||
return {
|
||||
note: note,
|
||||
|
@ -249,25 +247,21 @@ export function useNoteContext() {
|
|||
* @returns the value of the requested property.
|
||||
*/
|
||||
export function useNoteProperty<T extends keyof FNote>(note: FNote | null | undefined, property: T, componentId?: string) {
|
||||
if (!note) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [, setValue ] = useState<FNote[T]>(note[property]);
|
||||
const refreshValue = () => setValue(note[property]);
|
||||
const [, setValue ] = useState<FNote[T] | undefined>(note?.[property]);
|
||||
const refreshValue = () => setValue(note?.[property]);
|
||||
|
||||
// Watch for note changes.
|
||||
useEffect(() => refreshValue(), [ note, note[property] ]);
|
||||
useEffect(() => refreshValue(), [ note, note?.[property] ]);
|
||||
|
||||
// Watch for external changes.
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (loadResults.isNoteReloaded(note.noteId, componentId)) {
|
||||
if (loadResults.isNoteReloaded(note?.noteId, componentId)) {
|
||||
refreshValue();
|
||||
}
|
||||
});
|
||||
|
||||
useDebugValue(property);
|
||||
return note[property];
|
||||
return note?.[property];
|
||||
}
|
||||
|
||||
export function useNoteRelation(note: FNote | undefined | null, relationName: string): [string | null | undefined, (newValue: string) => void] {
|
||||
|
@ -362,10 +356,6 @@ export function useNoteLabelBoolean(note: FNote | undefined | null, labelName: s
|
|||
}
|
||||
|
||||
export function useNoteBlob(note: FNote | null | undefined): [ FBlob | null | undefined ] {
|
||||
if (!note) {
|
||||
return [ undefined ];
|
||||
}
|
||||
|
||||
const [ blob, setBlob ] = useState<FBlob | null>();
|
||||
|
||||
function refresh() {
|
||||
|
@ -379,7 +369,7 @@ export function useNoteBlob(note: FNote | null | undefined): [ FBlob | null | un
|
|||
}
|
||||
});
|
||||
|
||||
useDebugValue(note.noteId);
|
||||
useDebugValue(note?.noteId);
|
||||
|
||||
return [ blob ] as const;
|
||||
}
|
||||
|
@ -514,13 +504,9 @@ export function useTooltip(elRef: RefObject<HTMLElement>, config: Partial<Toolti
|
|||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
export function useLegacyImperativeHandlers(handlers: Record<string, Function>, force?: boolean) {
|
||||
export function useLegacyImperativeHandlers(handlers: Record<string, Function>) {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
if (!force) {
|
||||
useEffect(() => {
|
||||
Object.assign(parentComponent as never, handlers);
|
||||
}, [ handlers ])
|
||||
} else {
|
||||
useEffect(() => {
|
||||
Object.assign(parentComponent as never, handlers);
|
||||
}
|
||||
}, [ handlers ]);
|
||||
}
|
|
@ -20,16 +20,16 @@ interface NoteActionsProps {
|
|||
noteContext?: NoteContext;
|
||||
}
|
||||
|
||||
export default function NoteActions(props: NoteActionsProps) {
|
||||
export default function NoteActions({ note, noteContext }: NoteActionsProps) {
|
||||
return (
|
||||
<>
|
||||
<RevisionsButton {...props} />
|
||||
<NoteContextMenu {...props} />
|
||||
{note && <RevisionsButton note={note} />}
|
||||
{note && note.type !== "launcher" && <NoteContextMenu note={note as FNote} noteContext={noteContext}/>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function RevisionsButton({ note }: NoteActionsProps) {
|
||||
function RevisionsButton({ note }: { note: FNote }) {
|
||||
const isEnabled = !["launcher", "doc"].includes(note?.type ?? "");
|
||||
|
||||
return (isEnabled &&
|
||||
|
@ -42,12 +42,7 @@ function RevisionsButton({ note }: NoteActionsProps) {
|
|||
);
|
||||
}
|
||||
|
||||
function NoteContextMenu(props: NoteActionsProps) {
|
||||
const { note, noteContext } = props;
|
||||
if (!note || note.type === "launcher") {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment();
|
||||
const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(note.type);
|
||||
|
@ -65,7 +60,7 @@ function NoteContextMenu(props: NoteActionsProps) {
|
|||
hideToggleArrow
|
||||
noSelectButtonStyle
|
||||
>
|
||||
{canBeConvertedToAttachment && <ConvertToAttachment {...props} /> }
|
||||
{canBeConvertedToAttachment && <ConvertToAttachment note={note} /> }
|
||||
{note.type === "render" && <CommandItem command="renderActiveNote" icon="bx bx-extension" text={t("note_actions.re_render_note")} />}
|
||||
<CommandItem command="findInText" icon="bx bx-search" disabled={!isSearchable} text={t("note_actions.search_in_note")} />
|
||||
<CommandItem command="printActiveNote" icon="bx bx-printer" disabled={!isPrintable} text={t("note_actions.print_note")} />
|
||||
|
@ -110,7 +105,7 @@ function CommandItem({ icon, text, title, command, disabled }: { icon: string, t
|
|||
>{text}</FormListItem>
|
||||
}
|
||||
|
||||
function ConvertToAttachment({ note }: NoteActionsProps) {
|
||||
function ConvertToAttachment({ note }: { note: FNote }) {
|
||||
return (
|
||||
<FormListItem
|
||||
icon="bx bx-paperclip"
|
||||
|
|
|
@ -77,10 +77,6 @@ export default function EtapiSettings() {
|
|||
}
|
||||
|
||||
function TokenList({ tokens }: { tokens: EtapiToken[] }) {
|
||||
if (!tokens.length) {
|
||||
return <div>{t("etapi.no_tokens_yet")}</div>;
|
||||
}
|
||||
|
||||
const renameCallback = useCallback<RenameTokenCallback>(async (tokenId: string, oldName: string) => {
|
||||
const tokenName = await dialog.prompt({
|
||||
title: t("etapi.rename_token_title"),
|
||||
|
@ -104,41 +100,45 @@ function TokenList({ tokens }: { tokens: EtapiToken[] }) {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ overflow: "auto", height: "500px"}}>
|
||||
<table className="table table-stripped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("etapi.token_name")}</th>
|
||||
<th>{t("etapi.created")}</th>
|
||||
<th>{t("etapi.actions")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tokens.map(({ etapiTokenId, name, utcDateCreated}) => (
|
||||
tokens.length ? (
|
||||
<div style={{ overflow: "auto", height: "500px"}}>
|
||||
<table className="table table-stripped">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>{name}</td>
|
||||
<td>{formatDateTime(utcDateCreated)}</td>
|
||||
<td>
|
||||
{etapiTokenId && (
|
||||
<>
|
||||
<ActionButton
|
||||
icon="bx bx-edit-alt"
|
||||
text={t("etapi.rename_token")}
|
||||
onClick={() => renameCallback(etapiTokenId, name)}
|
||||
/>
|
||||
|
||||
<ActionButton
|
||||
icon="bx bx-trash"
|
||||
text={t("etapi.delete_token")}
|
||||
onClick={() => deleteCallback(etapiTokenId, name)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
<th>{t("etapi.token_name")}</th>
|
||||
<th>{t("etapi.created")}</th>
|
||||
<th>{t("etapi.actions")}</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
</thead>
|
||||
<tbody>
|
||||
{tokens.map(({ etapiTokenId, name, utcDateCreated}) => (
|
||||
<tr>
|
||||
<td>{name}</td>
|
||||
<td>{formatDateTime(utcDateCreated)}</td>
|
||||
<td>
|
||||
{etapiTokenId && (
|
||||
<>
|
||||
<ActionButton
|
||||
icon="bx bx-edit-alt"
|
||||
text={t("etapi.rename_token")}
|
||||
onClick={() => renameCallback(etapiTokenId, name)}
|
||||
/>
|
||||
|
||||
<ActionButton
|
||||
icon="bx bx-trash"
|
||||
text={t("etapi.delete_token")}
|
||||
onClick={() => deleteCallback(etapiTokenId, name)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<div>{t("etapi.no_tokens_yet")}</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue