From 3405f2e732a3c32929d126612a076cc89828b0e9 Mon Sep 17 00:00:00 2001 From: Amr Mohammed El-Sheraey <141947355+AmrMsCLL@users.noreply.github.com> Date: Mon, 5 Oct 2026 18:37:23 +0300 Subject: [PATCH] feat: start pages from templates and capture quick notes --- app/frontend/src/components/SearchPalette.tsx | 18 ++++ app/frontend/src/dashboard/capture-widget.tsx | 61 +++++++++++++ app/frontend/src/dashboard/widgets.tsx | 4 +- app/frontend/src/notes.ts | 8 ++ app/frontend/src/pages/NotePage.tsx | 25 +++++- app/frontend/src/pages/spaces/SpaceNotes.tsx | 89 ++++++++++++------- app/frontend/test/dashboard.spec.tsx | 55 ++++++++++++ app/frontend/test/notes.spec.tsx | 45 +++++++++- app/frontend/test/search.spec.tsx | 35 ++++++++ .../migration.sql | 1 + app/server/prisma/schema.prisma | 1 + .../src/dashboards/dto/dashboards.dto.ts | 2 +- app/server/src/notes/dto/notes.dto.ts | 20 ++++- app/server/src/notes/notes.service.ts | 27 +++++- app/server/src/notes/templates.ts | 87 ++++++++++++++++++ app/server/test/integration/notes.spec.ts | 29 ++++++ 16 files changed, 468 insertions(+), 39 deletions(-) create mode 100644 app/frontend/src/dashboard/capture-widget.tsx create mode 100644 app/server/prisma/migrations/20261005060000_note_templates/migration.sql create mode 100644 app/server/src/notes/templates.ts diff --git a/app/frontend/src/components/SearchPalette.tsx b/app/frontend/src/components/SearchPalette.tsx index 3e81cb5..9da6fa4 100644 --- a/app/frontend/src/components/SearchPalette.tsx +++ b/app/frontend/src/components/SearchPalette.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router'; import { CommandPalette, isAbortError, type CommandItem } from '@project-graphite/ui'; import { useAuth } from '../auth'; import { itemHref, useSpaces, type Tag } from '../spaces'; @@ -27,6 +28,8 @@ const pages = [ export function SearchPalette({ onClose }: { onClose: () => void }) { const { request, user } = useAuth(); const spaces = useSpaces(); + const navigate = useNavigate(); + const personal = spaces.data?.find((space) => space.kind === 'personal'); const [query, setQuery] = useState(''); const [results, setResults] = useState(); const text = query.trim(); @@ -53,7 +56,22 @@ export function SearchPalette({ onClose }: { onClose: () => void }) { const matches = (label: string) => label.toLowerCase().includes(text.toLowerCase()); const spaceName = (spaceId: string) => spaces.data?.find((space) => space.id === spaceId)?.name; + const actions: CommandItem[] = personal + ? [ + { + group: 'Create', + id: 'new-page', + label: 'New page', + onSelect: () => + void request<{ id: string }>(`/spaces/${personal.id}/notes`, { method: 'POST', body: JSON.stringify({}) }).then((created) => + navigate(`/notes/${created.id}`), + ), + }, + { group: 'Create', href: `/spaces/${personal.id}/tasks`, id: 'new-task', label: 'New task' }, + ] + : []; const items: CommandItem[] = [ + ...actions.filter((action) => !text || matches(action.label)), ...(results?.items ?? []).map((item) => ({ group: 'Items', hint: `${item.kind} · ${item.spaceName}${item.archived ? ' · archived' : ''}`, diff --git a/app/frontend/src/dashboard/capture-widget.tsx b/app/frontend/src/dashboard/capture-widget.tsx new file mode 100644 index 0000000..bd8ddd8 --- /dev/null +++ b/app/frontend/src/dashboard/capture-widget.tsx @@ -0,0 +1,61 @@ +import { useState } from 'react'; +import { useAuth } from '../auth'; +import { useSpaces } from '../spaces'; +import { useAction } from '../useAction'; +import { Empty } from './widget-parts'; + +export function Capture() { + const auth = useAuth(); + const spaces = useSpaces(); + const saving = useAction(); + const [text, setText] = useState(''); + const [spaceId, setSpaceId] = useState(''); + const editable = (spaces.data ?? []).filter((space) => space.role !== 'viewer'); + const target = spaceId || editable[0]?.id; + + if (spaces.data && editable.length === 0) return You can only view your spaces, so there is nowhere to save.; + + function save(kind: 'note' | 'task') { + const content = text.trim(); + if (!content || !target) return; + void saving.run(async () => { + if (kind === 'note') { + await auth.request(`/spaces/${target}/notes`, { method: 'POST', body: JSON.stringify({ text: content }) }); + } else { + await auth.request(`/spaces/${target}/tasks`, { method: 'POST', body: JSON.stringify({ title: content.split(/\r?\n/)[0]!.slice(0, 200) }) }); + } + setText(''); + return kind === 'note' ? 'Saved as a page.' : 'Saved as a task.'; + }, 'Could not save that'); + } + + return ( +
+