mirror of https://github.com/usememos/memos
feat: surface newly created memo above pinned list
parent
deddf71d7b
commit
6eb17864df
@ -0,0 +1,29 @@
|
||||
import { createContext, type ReactNode, useContext, useMemo, useState } from "react";
|
||||
|
||||
interface NewMemoContextValue {
|
||||
// Name of the most recently created memo in this view, or null. Only one
|
||||
// memo is ever "new" at a time: creating another overwrites it, which clears
|
||||
// the previous one for free.
|
||||
newMemoName: string | null;
|
||||
markNewMemo: (name: string) => void;
|
||||
}
|
||||
|
||||
// Default is a safe no-op so MemoEditor/MemoView rendered outside a provider
|
||||
// (dialogs, detail pages, other lists) keep working without a "new" marker.
|
||||
const NewMemoContext = createContext<NewMemoContextValue>({
|
||||
newMemoName: null,
|
||||
markNewMemo: () => {},
|
||||
});
|
||||
|
||||
export function NewMemoProvider({ children }: { children: ReactNode }) {
|
||||
const [newMemoName, setNewMemoName] = useState<string | null>(null);
|
||||
// setNewMemoName is identity-stable, so the value only changes with newMemoName.
|
||||
// Memoize it so a Home re-render doesn't cascade into every subscribed memo row.
|
||||
const value = useMemo(() => ({ newMemoName, markNewMemo: setNewMemoName }), [newMemoName]);
|
||||
|
||||
return <NewMemoContext.Provider value={value}>{children}</NewMemoContext.Provider>;
|
||||
}
|
||||
|
||||
export function useNewMemo() {
|
||||
return useContext(NewMemoContext);
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { hoistMemoToFront } from "@/hooks/useMemoSorting";
|
||||
import type { Memo } from "@/types/proto/api/v1/memo_service_pb";
|
||||
|
||||
const memo = (name: string) => ({ name }) as Memo;
|
||||
|
||||
describe("hoistMemoToFront", () => {
|
||||
it("moves the named memo to the front, above everything else", () => {
|
||||
const list = [memo("memos/pin1"), memo("memos/pin2"), memo("memos/new")];
|
||||
expect(hoistMemoToFront(list, "memos/new").map((m) => m.name)).toEqual(["memos/new", "memos/pin1", "memos/pin2"]);
|
||||
});
|
||||
|
||||
it("returns the list unchanged when the name is null", () => {
|
||||
const list = [memo("memos/a"), memo("memos/b")];
|
||||
expect(hoistMemoToFront(list, null)).toBe(list);
|
||||
});
|
||||
|
||||
it("returns the list unchanged when the name is absent", () => {
|
||||
const list = [memo("memos/a"), memo("memos/b")];
|
||||
expect(hoistMemoToFront(list, "memos/missing")).toBe(list);
|
||||
});
|
||||
|
||||
it("returns the list unchanged when the memo is already first", () => {
|
||||
const list = [memo("memos/a"), memo("memos/b")];
|
||||
expect(hoistMemoToFront(list, "memos/a")).toBe(list);
|
||||
});
|
||||
|
||||
it("does not mutate the input list", () => {
|
||||
const list = [memo("memos/a"), memo("memos/new")];
|
||||
hoistMemoToFront(list, "memos/new");
|
||||
expect(list.map((m) => m.name)).toEqual(["memos/a", "memos/new"]);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,27 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
COMPACT_MODE_CONFIG,
|
||||
getCompactTriggerHeightPx,
|
||||
getPreviewMaxHeightPx,
|
||||
shouldCompactContent,
|
||||
} from "@/components/MemoContent/constants";
|
||||
|
||||
describe("compact folded preview sizing", () => {
|
||||
it("clamps the collapsed preview to fewer rows than the fold trigger", () => {
|
||||
// Preview must be shorter than the trigger, otherwise folding shows everything.
|
||||
expect(COMPACT_MODE_CONFIG.previewRows).toBeLessThan(COMPACT_MODE_CONFIG.triggerRows);
|
||||
expect(getPreviewMaxHeightPx()).toBeLessThan(getCompactTriggerHeightPx());
|
||||
});
|
||||
|
||||
it("folds only when content is taller than the trigger height", () => {
|
||||
const trigger = getCompactTriggerHeightPx();
|
||||
expect(shouldCompactContent(trigger + 1, trigger)).toBe(true);
|
||||
});
|
||||
|
||||
it("leaves content at or below the trigger height fully expanded", () => {
|
||||
const trigger = getCompactTriggerHeightPx();
|
||||
// A memo a row or two over the preview but within the buffer stays open.
|
||||
expect(shouldCompactContent(getPreviewMaxHeightPx() + 1, trigger)).toBe(false);
|
||||
expect(shouldCompactContent(trigger, trigger)).toBe(false);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue