@notis_ai/cli 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -5
- package/dist/scaffolds/notis-database/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-database/notis.config.ts +0 -1
- package/dist/scaffolds/notis-database/src/mock-runtime.ts +1 -0
- package/dist/scaffolds/notis-journal/CHANGELOG.md +25 -0
- package/dist/scaffolds/notis-journal/app/globals.css +37 -0
- package/dist/scaffolds/notis-journal/app/insights/page.tsx +513 -0
- package/dist/scaffolds/notis-journal/app/journal-core.tsx +362 -0
- package/dist/scaffolds/notis-journal/app/journal-ui.tsx +337 -0
- package/dist/scaffolds/notis-journal/app/layout.tsx +6 -0
- package/dist/scaffolds/notis-journal/app/page.tsx +485 -0
- package/dist/scaffolds/notis-journal/components/ui/badge.tsx +28 -0
- package/dist/scaffolds/notis-journal/components/ui/button.tsx +53 -0
- package/dist/scaffolds/notis-journal/components/ui/card.tsx +56 -0
- package/dist/scaffolds/notis-journal/components.json +20 -0
- package/dist/scaffolds/notis-journal/index.html +12 -0
- package/dist/scaffolds/notis-journal/lib/utils.ts +6 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-6.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-fixtures.json +132 -0
- package/dist/scaffolds/notis-journal/notis.config.ts +93 -0
- package/dist/scaffolds/notis-journal/package.json +34 -0
- package/dist/scaffolds/notis-journal/packages/sdk/package.json +36 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/Markdown.tsx +60 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +145 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +229 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +41 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotis.ts +34 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTool.ts +64 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTools.ts +56 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +83 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +43 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +220 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/styles.css +186 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/ui.ts +15 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/vite.ts +56 -0
- package/dist/scaffolds/notis-journal/packages/sdk/tsconfig.json +15 -0
- package/dist/scaffolds/notis-journal/postcss.config.mjs +8 -0
- package/dist/scaffolds/notis-journal/skills/journal-onboarding/SKILL.md +120 -0
- package/dist/scaffolds/notis-journal/src/dev-main.tsx +58 -0
- package/dist/scaffolds/notis-journal/src/mock-runtime.ts +197 -0
- package/dist/scaffolds/notis-journal/tailwind.config.ts +58 -0
- package/dist/scaffolds/notis-journal/tsconfig.json +23 -0
- package/dist/scaffolds/notis-journal/vite.config.ts +10 -0
- package/dist/scaffolds/notis-notes/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-notes/app/page.tsx +325 -543
- package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-notes/notis.config.ts +0 -1
- package/dist/scaffolds/notis-random/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-random/notis.config.ts +0 -1
- package/dist/scaffolds/notis-random/src/mock-runtime.ts +1 -0
- package/dist/scaffolds.json +11 -0
- package/package.json +3 -3
- package/skills/notis-apps/SKILL.md +64 -6
- package/skills/notis-apps/cli.md +38 -2
- package/skills/notis-cli/SKILL.md +20 -2
- package/skills/notis-query/cli.md +1 -1
- package/src/command-specs/apps.js +529 -16
- package/src/command-specs/index.js +1 -1
- package/src/command-specs/meta.js +8 -2
- package/src/command-specs/tools.js +50 -37
- package/src/runtime/agent-browser.js +273 -1
- package/src/runtime/app-changelog.js +79 -0
- package/src/runtime/app-dev-server.js +113 -6
- package/src/runtime/app-dev-sessions.js +176 -12
- package/src/runtime/app-platform.js +200 -39
- package/src/runtime/assets/store-screenshot-dark.png +0 -0
- package/src/runtime/desktop-auth.js +93 -0
- package/src/runtime/profiles.js +37 -15
- package/src/runtime/store-screenshot.js +138 -0
- package/src/runtime/transport.js +21 -82
- package/template/.harness/index.html.tmpl +128 -6
- package/template/CHANGELOG.md +5 -0
- package/template/app/page.tsx +11 -41
- package/template/notis.config.ts +0 -1
- package/template/package-lock.json +4137 -0
- package/template/package.json +1 -0
- package/template/packages/sdk/package.json +4 -0
- package/template/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/template/packages/sdk/src/components/Markdown.tsx +60 -0
- package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +7 -2
- package/template/packages/sdk/src/config.ts +74 -0
- package/template/packages/sdk/src/documents.ts +229 -0
- package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/template/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/template/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/template/packages/sdk/src/hooks/useMultiSelect.ts +38 -2
- package/template/packages/sdk/src/hooks/useUpsertDocument.ts +54 -9
- package/template/packages/sdk/src/index.ts +30 -1
- package/template/packages/sdk/src/runtime.ts +76 -17
- package/template/packages/sdk/src/styles.css +148 -0
- package/dist/scaffolds/notis-database/metadata/cover.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/cover.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-random/metadata/cover.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
- package/template/metadata/cover.png +0 -0
- package/template/packages/sdk/src/hooks/useDatabase.ts +0 -76
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import {
|
|
5
5
|
MultiSelectActionBar,
|
|
6
6
|
MultiSelectCheckbox,
|
|
7
7
|
MultiSelectDragOverlay,
|
|
8
8
|
useMultiSelect,
|
|
9
9
|
useBackend,
|
|
10
|
+
useDatabaseSchema,
|
|
11
|
+
useDocuments,
|
|
10
12
|
useNotis,
|
|
11
13
|
useNotisNavigation,
|
|
12
14
|
useTopBarSearch,
|
|
13
|
-
|
|
15
|
+
useUpsertDocument,
|
|
16
|
+
asRecord,
|
|
17
|
+
getDocumentPreview,
|
|
18
|
+
getRelationIds,
|
|
19
|
+
isPresentString,
|
|
20
|
+
optionalString,
|
|
21
|
+
type DatabaseProperty,
|
|
22
|
+
type DocumentRecord,
|
|
14
23
|
type MultiSelectController,
|
|
24
|
+
type UpsertDocumentArgs,
|
|
15
25
|
} from '@notis/sdk';
|
|
16
|
-
import {
|
|
26
|
+
import { ArrowUpRightIcon as ArrowUpRight, CubeIcon as Boxes, BookOpenIcon as BookOpen, BookOpenTextIcon as BookOpenText, CalendarIcon as CalendarDays, CaretDownIcon as ChevronDown, CaretLeftIcon as ChevronLeft, CaretRightIcon as ChevronRight, FileTextIcon as FileText, FolderIcon as Folder, FolderMinusIcon as FolderMinus, FolderOpenIcon as FolderOpen, FolderPlusIcon as FolderPlus, FoldersIcon as Folders, SquaresFourIcon as LayoutGrid, CircleNotchIcon as Loader2, MagnifyingGlassIcon as Search, NotePencilIcon as NotebookPen, PencilIcon as Pencil, PlusIcon as Plus, NoteIcon as StickyNote, TableIcon as Table2, TrashIcon as Trash, XIcon as X, type Icon } from '@phosphor-icons/react';
|
|
17
27
|
import * as PhosphorIcons from '@phosphor-icons/react';
|
|
18
28
|
|
|
19
29
|
import { Button } from '@/components/ui/button';
|
|
@@ -34,50 +44,6 @@ type FolderOption = {
|
|
|
34
44
|
pathLabel: string;
|
|
35
45
|
};
|
|
36
46
|
|
|
37
|
-
type DatabasePropertyType =
|
|
38
|
-
| 'title'
|
|
39
|
-
| 'rich_text'
|
|
40
|
-
| 'text'
|
|
41
|
-
| 'number'
|
|
42
|
-
| 'checkbox'
|
|
43
|
-
| 'date'
|
|
44
|
-
| 'select'
|
|
45
|
-
| 'multi_select'
|
|
46
|
-
| 'status'
|
|
47
|
-
| 'relation'
|
|
48
|
-
| 'formula'
|
|
49
|
-
| 'files';
|
|
50
|
-
|
|
51
|
-
type DatabasePropertyOption = {
|
|
52
|
-
id?: string | null;
|
|
53
|
-
name: string;
|
|
54
|
-
color?: string | null;
|
|
55
|
-
order?: number;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
type DatabaseProperty = {
|
|
59
|
-
id?: string | null;
|
|
60
|
-
name: string;
|
|
61
|
-
type: DatabasePropertyType;
|
|
62
|
-
description?: string | null;
|
|
63
|
-
options?: DatabasePropertyOption[];
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
type DocumentRecord = {
|
|
67
|
-
id: string;
|
|
68
|
-
title: string;
|
|
69
|
-
properties: Record<string, unknown>;
|
|
70
|
-
url?: string | null;
|
|
71
|
-
icon?: string | null;
|
|
72
|
-
cover?: string | null;
|
|
73
|
-
databaseSlug?: string;
|
|
74
|
-
contentBlocknote?: Array<Record<string, unknown>> | null;
|
|
75
|
-
contentMarkdown?: string | null;
|
|
76
|
-
plainText?: string | null;
|
|
77
|
-
createdAt?: string | null;
|
|
78
|
-
lastEditedTime?: string | null;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
47
|
type CollectionItemContext = {
|
|
82
48
|
id: string;
|
|
83
49
|
title: string;
|
|
@@ -85,72 +51,21 @@ type CollectionItemContext = {
|
|
|
85
51
|
properties: Record<string, unknown>;
|
|
86
52
|
};
|
|
87
53
|
|
|
88
|
-
type QueryFilter = {
|
|
89
|
-
filters?: Array<{
|
|
90
|
-
property: string;
|
|
91
|
-
operator: string;
|
|
92
|
-
type?: string;
|
|
93
|
-
value: unknown;
|
|
94
|
-
}>;
|
|
95
|
-
sorts?: Array<{
|
|
96
|
-
property: string;
|
|
97
|
-
direction: 'asc' | 'desc';
|
|
98
|
-
}>;
|
|
99
|
-
page_size?: number;
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
type QueryDatabaseArgs = {
|
|
103
|
-
database_slug: string;
|
|
104
|
-
query?: QueryFilter;
|
|
105
|
-
offset?: number;
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
type QueryDatabaseResult = {
|
|
109
|
-
documents?: unknown[];
|
|
110
|
-
message?: string;
|
|
111
|
-
error?: string;
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
type GetDatabaseArgs = {
|
|
115
|
-
database_slug: string;
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
type GetDatabaseResult = {
|
|
119
|
-
database?: {
|
|
120
|
-
schema?: {
|
|
121
|
-
properties?: unknown[];
|
|
122
|
-
};
|
|
123
|
-
};
|
|
124
|
-
message?: string;
|
|
125
|
-
error?: string;
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
type UpsertNoteArgs = {
|
|
129
|
-
document_id?: string;
|
|
130
|
-
title?: string;
|
|
131
|
-
content_blocknote?: Array<Record<string, unknown>> | null;
|
|
132
|
-
[propertyName: string]: unknown;
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
type UpsertNoteResult = {
|
|
136
|
-
status?: string;
|
|
137
|
-
document?: unknown;
|
|
138
|
-
message?: string;
|
|
139
|
-
error?: string;
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
type UpsertDocumentArgs = {
|
|
143
|
-
documentId?: string;
|
|
144
|
-
title?: string;
|
|
145
|
-
properties?: Record<string, unknown>;
|
|
146
|
-
contentBlocknote?: Array<Record<string, unknown>> | null;
|
|
147
|
-
};
|
|
148
|
-
|
|
149
54
|
const NOTE_DATABASE_SLUG = 'notes';
|
|
150
55
|
const FOLDER_DATABASE_SLUG = 'note_folders';
|
|
151
56
|
const DEFAULT_NOTE_TITLE = 'Untitled note';
|
|
152
57
|
const WEEKDAY_LABELS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
|
153
58
|
|
|
59
|
+
// Sentinel for the built-in "Created" date, sourced from each document's
|
|
60
|
+
// created_at timestamp rather than a user-defined date property.
|
|
61
|
+
const CREATED_DATE_FIELD = '__created__';
|
|
62
|
+
const CREATED_DATE_LABEL = 'Created';
|
|
63
|
+
|
|
64
|
+
type CalendarDateField = {
|
|
65
|
+
value: string;
|
|
66
|
+
label: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
154
69
|
const TABS: TabConfig[] = [
|
|
155
70
|
{ key: 'gallery', label: 'Gallery', icon: LayoutGrid },
|
|
156
71
|
{ key: 'table', label: 'Table', icon: Table2 },
|
|
@@ -239,112 +154,6 @@ const ALL_PHOSPHOR_ICON_NAMES = Object.keys(PhosphorIcons)
|
|
|
239
154
|
.map(toKebabCase)
|
|
240
155
|
.sort((a, b) => a.localeCompare(b));
|
|
241
156
|
|
|
242
|
-
function isPresentString(value: unknown): value is string {
|
|
243
|
-
return typeof value === 'string' && value.trim().length > 0;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
247
|
-
return value && typeof value === 'object' && !Array.isArray(value)
|
|
248
|
-
? (value as Record<string, unknown>)
|
|
249
|
-
: null;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function optionalString(value: unknown): string | null {
|
|
253
|
-
return isPresentString(value) ? value.trim() : null;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function extractRichText(value: unknown): string {
|
|
257
|
-
if (typeof value === 'string') return value;
|
|
258
|
-
if (!Array.isArray(value)) return '';
|
|
259
|
-
return value
|
|
260
|
-
.map((item) => {
|
|
261
|
-
const record = asRecord(item);
|
|
262
|
-
const text = asRecord(record?.text);
|
|
263
|
-
return optionalString(text?.content) ?? optionalString(record?.plain_text) ?? '';
|
|
264
|
-
})
|
|
265
|
-
.join('');
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function normalizePropertyValue(value: unknown): unknown {
|
|
269
|
-
const record = asRecord(value);
|
|
270
|
-
if (!record) return value;
|
|
271
|
-
|
|
272
|
-
const type = optionalString(record.type);
|
|
273
|
-
if (!type) return value;
|
|
274
|
-
|
|
275
|
-
if (type === 'title') return extractRichText(record.title);
|
|
276
|
-
if (type === 'rich_text') return extractRichText(record.rich_text);
|
|
277
|
-
if (type === 'select' || type === 'status') return optionalString(asRecord(record[type])?.name) ?? record[type] ?? null;
|
|
278
|
-
if (type === 'multi_select') {
|
|
279
|
-
const items = Array.isArray(record.multi_select) ? record.multi_select : [];
|
|
280
|
-
return items.map((item) => optionalString(asRecord(item)?.name) ?? item).filter(Boolean);
|
|
281
|
-
}
|
|
282
|
-
if (type === 'relation') {
|
|
283
|
-
const items = Array.isArray(record.relation) ? record.relation : [];
|
|
284
|
-
return items.map((item) => optionalString(asRecord(item)?.id) ?? item).filter(Boolean);
|
|
285
|
-
}
|
|
286
|
-
if (type === 'date') return optionalString(asRecord(record.date)?.start) ?? record.date ?? null;
|
|
287
|
-
if (type in record) return record[type];
|
|
288
|
-
return value;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function normalizeDocumentRecord(value: unknown): DocumentRecord {
|
|
292
|
-
const record = asRecord(value) ?? {};
|
|
293
|
-
const rawProperties = asRecord(record.properties) ?? {};
|
|
294
|
-
const properties: Record<string, unknown> = {};
|
|
295
|
-
for (const [key, propertyValue] of Object.entries(rawProperties)) {
|
|
296
|
-
properties[key] = normalizePropertyValue(propertyValue);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
return {
|
|
300
|
-
id: optionalString(record.id) ?? '',
|
|
301
|
-
title: optionalString(record.title) ?? DEFAULT_NOTE_TITLE,
|
|
302
|
-
url: optionalString(record.url),
|
|
303
|
-
properties,
|
|
304
|
-
icon: optionalString(record.icon),
|
|
305
|
-
cover: optionalString(record.cover),
|
|
306
|
-
databaseSlug: optionalString(record.databaseSlug) ?? optionalString(record.database_slug) ?? undefined,
|
|
307
|
-
contentBlocknote: Array.isArray(record.contentBlocknote)
|
|
308
|
-
? record.contentBlocknote as Array<Record<string, unknown>>
|
|
309
|
-
: Array.isArray(record.content_blocknote)
|
|
310
|
-
? record.content_blocknote as Array<Record<string, unknown>>
|
|
311
|
-
: null,
|
|
312
|
-
contentMarkdown: optionalString(record.contentMarkdown) ?? optionalString(record.content_markdown),
|
|
313
|
-
plainText: optionalString(record.plainText) ?? optionalString(record.plain_text),
|
|
314
|
-
createdAt: optionalString(record.createdAt) ?? optionalString(record.created_at) ?? optionalString(record.created_time),
|
|
315
|
-
lastEditedTime:
|
|
316
|
-
optionalString(record.lastEditedTime)
|
|
317
|
-
?? optionalString(record.last_edited_time)
|
|
318
|
-
?? optionalString(record.updated_at),
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function normalizeDatabaseProperty(value: unknown): DatabaseProperty | null {
|
|
323
|
-
const record = asRecord(value);
|
|
324
|
-
const name = optionalString(record?.name);
|
|
325
|
-
if (!record || !name) return null;
|
|
326
|
-
const rawOptions = Array.isArray(record.options) ? record.options : [];
|
|
327
|
-
const options = rawOptions.flatMap((option): DatabasePropertyOption[] => {
|
|
328
|
-
const optionRecord = asRecord(option);
|
|
329
|
-
const optionName = optionalString(optionRecord?.name);
|
|
330
|
-
if (!optionRecord || !optionName) return [];
|
|
331
|
-
return [{
|
|
332
|
-
id: optionalString(optionRecord.id),
|
|
333
|
-
name: optionName,
|
|
334
|
-
color: optionalString(optionRecord.color),
|
|
335
|
-
order: typeof optionRecord.order === 'number' ? optionRecord.order : undefined,
|
|
336
|
-
}];
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
return {
|
|
340
|
-
id: optionalString(record.id),
|
|
341
|
-
name,
|
|
342
|
-
type: (optionalString(record.type) ?? 'rich_text') as DatabasePropertyType,
|
|
343
|
-
description: optionalString(record.description),
|
|
344
|
-
options,
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
|
|
348
157
|
function normalizeCollectionItem(value: unknown): CollectionItemContext | null {
|
|
349
158
|
const record = asRecord(value);
|
|
350
159
|
const id = optionalString(record?.id);
|
|
@@ -357,104 +166,6 @@ function normalizeCollectionItem(value: unknown): CollectionItemContext | null {
|
|
|
357
166
|
};
|
|
358
167
|
}
|
|
359
168
|
|
|
360
|
-
function useNoteSchema(databaseSlug: string) {
|
|
361
|
-
const getDatabase = useTool<GetDatabaseArgs, GetDatabaseResult>('LOCAL_NOTIS_DATABASE_GET_DATABASE');
|
|
362
|
-
const [properties, setProperties] = useState<DatabaseProperty[]>([]);
|
|
363
|
-
const [loading, setLoading] = useState(true);
|
|
364
|
-
const [error, setError] = useState<Error | null>(null);
|
|
365
|
-
const [fetchKey, setFetchKey] = useState(0);
|
|
366
|
-
|
|
367
|
-
const refetch = useCallback(() => {
|
|
368
|
-
setFetchKey((key) => key + 1);
|
|
369
|
-
}, []);
|
|
370
|
-
|
|
371
|
-
useEffect(() => {
|
|
372
|
-
let cancelled = false;
|
|
373
|
-
setLoading(true);
|
|
374
|
-
setError(null);
|
|
375
|
-
|
|
376
|
-
getDatabase.call({ database_slug: databaseSlug })
|
|
377
|
-
.then((result) => {
|
|
378
|
-
if (cancelled) return;
|
|
379
|
-
const message = result.error ?? result.message;
|
|
380
|
-
if (!result.database && message) {
|
|
381
|
-
throw new Error(message);
|
|
382
|
-
}
|
|
383
|
-
const rawProperties = result.database?.schema?.properties ?? [];
|
|
384
|
-
setProperties(rawProperties.map(normalizeDatabaseProperty).filter((p): p is DatabaseProperty => Boolean(p)));
|
|
385
|
-
setLoading(false);
|
|
386
|
-
})
|
|
387
|
-
.catch((err) => {
|
|
388
|
-
if (cancelled) return;
|
|
389
|
-
setError(err instanceof Error ? err : new Error(String(err)));
|
|
390
|
-
setLoading(false);
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
return () => {
|
|
394
|
-
cancelled = true;
|
|
395
|
-
};
|
|
396
|
-
}, [databaseSlug, fetchKey, getDatabase.call]);
|
|
397
|
-
|
|
398
|
-
return { properties, loading, error, refetch };
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
function useQueryDocuments(databaseSlug: string, options: { filter?: QueryFilter; pageSize?: number; offset?: number } = {}) {
|
|
402
|
-
const queryTool = useTool<QueryDatabaseArgs, QueryDatabaseResult>('LOCAL_NOTIS_DATABASE_QUERY');
|
|
403
|
-
const [documents, setDocuments] = useState<DocumentRecord[]>([]);
|
|
404
|
-
const [loading, setLoading] = useState(true);
|
|
405
|
-
const [error, setError] = useState<Error | null>(null);
|
|
406
|
-
const [fetchKey, setFetchKey] = useState(0);
|
|
407
|
-
const filterKey = JSON.stringify(options.filter ?? null);
|
|
408
|
-
|
|
409
|
-
const refetch = useCallback(() => {
|
|
410
|
-
setFetchKey((key) => key + 1);
|
|
411
|
-
}, []);
|
|
412
|
-
|
|
413
|
-
useEffect(() => {
|
|
414
|
-
let cancelled = false;
|
|
415
|
-
setLoading(true);
|
|
416
|
-
setError(null);
|
|
417
|
-
|
|
418
|
-
queryTool.call({
|
|
419
|
-
database_slug: databaseSlug,
|
|
420
|
-
query: {
|
|
421
|
-
...(options.filter ?? {}),
|
|
422
|
-
page_size: options.pageSize,
|
|
423
|
-
},
|
|
424
|
-
offset: options.offset,
|
|
425
|
-
})
|
|
426
|
-
.then((result) => {
|
|
427
|
-
if (cancelled) return;
|
|
428
|
-
const message = result.error ?? result.message;
|
|
429
|
-
if (message && !result.documents) {
|
|
430
|
-
throw new Error(message);
|
|
431
|
-
}
|
|
432
|
-
setDocuments((result.documents ?? []).map(normalizeDocumentRecord).filter((doc) => doc.id));
|
|
433
|
-
setLoading(false);
|
|
434
|
-
})
|
|
435
|
-
.catch((err) => {
|
|
436
|
-
if (cancelled) return;
|
|
437
|
-
setError(err instanceof Error ? err : new Error(String(err)));
|
|
438
|
-
setLoading(false);
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
return () => {
|
|
442
|
-
cancelled = true;
|
|
443
|
-
};
|
|
444
|
-
}, [databaseSlug, fetchKey, filterKey, options.offset, options.pageSize, queryTool.call]);
|
|
445
|
-
|
|
446
|
-
return { documents, loading, error, refetch };
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
function buildUpsertNoteArgs(args: UpsertDocumentArgs): UpsertNoteArgs {
|
|
450
|
-
const payload: UpsertNoteArgs = {};
|
|
451
|
-
if (args.documentId) payload.document_id = args.documentId;
|
|
452
|
-
if (typeof args.title === 'string') payload.title = args.title;
|
|
453
|
-
if (args.properties) Object.assign(payload, args.properties);
|
|
454
|
-
if ('contentBlocknote' in args) payload.content_blocknote = args.contentBlocknote ?? null;
|
|
455
|
-
return payload;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
169
|
function pluralize(count: number, singular: string, plural = `${singular}s`): string {
|
|
459
170
|
return `${count} ${count === 1 ? singular : plural}`;
|
|
460
171
|
}
|
|
@@ -469,9 +180,9 @@ function getDateInputValue(value: unknown): string {
|
|
|
469
180
|
return getDateKey(value) ?? '';
|
|
470
181
|
}
|
|
471
182
|
|
|
472
|
-
function
|
|
473
|
-
if (
|
|
474
|
-
return
|
|
183
|
+
function getNoteDateValue(note: DocumentRecord, field: string): unknown {
|
|
184
|
+
if (field === CREATED_DATE_FIELD) return note.createdAt;
|
|
185
|
+
return note.properties[field];
|
|
475
186
|
}
|
|
476
187
|
|
|
477
188
|
function getFolderTitle(document: DocumentRecord): string {
|
|
@@ -484,45 +195,8 @@ function getNoteTitle(document: DocumentRecord): string {
|
|
|
484
195
|
return isPresentString(document.title) ? document.title : DEFAULT_NOTE_TITLE;
|
|
485
196
|
}
|
|
486
197
|
|
|
487
|
-
function getPlainTextFromBlockNote(value: unknown): string {
|
|
488
|
-
const segments: string[] = [];
|
|
489
|
-
|
|
490
|
-
function visit(node: unknown): void {
|
|
491
|
-
if (typeof node === 'string') {
|
|
492
|
-
if (node.trim()) segments.push(node.trim());
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
if (Array.isArray(node)) {
|
|
496
|
-
node.forEach(visit);
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
if (!node || typeof node !== 'object') return;
|
|
500
|
-
|
|
501
|
-
const record = node as Record<string, unknown>;
|
|
502
|
-
if (typeof record.text === 'string' && record.text.trim()) {
|
|
503
|
-
segments.push(record.text.trim());
|
|
504
|
-
}
|
|
505
|
-
visit(record.content);
|
|
506
|
-
visit(record.children);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
visit(value);
|
|
510
|
-
return segments.join(' ').replace(/\s+/g, ' ').trim();
|
|
511
|
-
}
|
|
512
|
-
|
|
513
198
|
function getNotePreviewText(document: DocumentRecord): string {
|
|
514
|
-
|
|
515
|
-
return document.plainText.replace(/\s+/g, ' ').trim();
|
|
516
|
-
}
|
|
517
|
-
if (isPresentString(document.contentMarkdown)) {
|
|
518
|
-
return document.contentMarkdown
|
|
519
|
-
.replace(/[#*_`>~[\]()]/g, ' ')
|
|
520
|
-
.replace(/\s+/g, ' ')
|
|
521
|
-
.trim();
|
|
522
|
-
}
|
|
523
|
-
const fromBlocks = getPlainTextFromBlockNote(document.contentBlocknote);
|
|
524
|
-
if (fromBlocks) return fromBlocks;
|
|
525
|
-
return getNoteTitle(document);
|
|
199
|
+
return getDocumentPreview(document);
|
|
526
200
|
}
|
|
527
201
|
|
|
528
202
|
function buildFolderOptions(documents: DocumentRecord[]): FolderOption[] {
|
|
@@ -784,7 +458,7 @@ function FolderIconPicker({
|
|
|
784
458
|
useEffect(() => {
|
|
785
459
|
if (!open) return;
|
|
786
460
|
|
|
787
|
-
function handlePointerDown(event:
|
|
461
|
+
function handlePointerDown(event: Event) {
|
|
788
462
|
const target = event.target;
|
|
789
463
|
if (target instanceof Node && containerRef.current?.contains(target)) {
|
|
790
464
|
return;
|
|
@@ -883,26 +557,6 @@ function FolderIconPicker({
|
|
|
883
557
|
);
|
|
884
558
|
}
|
|
885
559
|
|
|
886
|
-
function ToolbarIconButton({
|
|
887
|
-
icon: Icon,
|
|
888
|
-
label,
|
|
889
|
-
}: {
|
|
890
|
-
icon: Icon;
|
|
891
|
-
label: string;
|
|
892
|
-
}) {
|
|
893
|
-
return (
|
|
894
|
-
<button
|
|
895
|
-
type="button"
|
|
896
|
-
aria-label={label}
|
|
897
|
-
aria-disabled="true"
|
|
898
|
-
tabIndex={-1}
|
|
899
|
-
className="inline-flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted/40 hover:text-foreground"
|
|
900
|
-
>
|
|
901
|
-
<Icon className="h-3.5 w-3.5" />
|
|
902
|
-
</button>
|
|
903
|
-
);
|
|
904
|
-
}
|
|
905
|
-
|
|
906
560
|
function ViewPill({
|
|
907
561
|
config,
|
|
908
562
|
active,
|
|
@@ -948,6 +602,8 @@ function PageHeader({
|
|
|
948
602
|
onTitleDraftChange,
|
|
949
603
|
onSubmitTitleEdit,
|
|
950
604
|
onCancelTitleEdit,
|
|
605
|
+
onCreateNote,
|
|
606
|
+
creatingNote = false,
|
|
951
607
|
}: {
|
|
952
608
|
title: string;
|
|
953
609
|
icon: string | null;
|
|
@@ -966,6 +622,8 @@ function PageHeader({
|
|
|
966
622
|
onTitleDraftChange?: (title: string) => void;
|
|
967
623
|
onSubmitTitleEdit?: () => void;
|
|
968
624
|
onCancelTitleEdit?: () => void;
|
|
625
|
+
onCreateNote?: () => void;
|
|
626
|
+
creatingNote?: boolean;
|
|
969
627
|
}) {
|
|
970
628
|
return (
|
|
971
629
|
<div className="flex flex-col gap-3 px-6 pt-6">
|
|
@@ -1036,20 +694,14 @@ function PageHeader({
|
|
|
1036
694
|
))}
|
|
1037
695
|
</div>
|
|
1038
696
|
<div className="ml-auto flex items-center gap-0.5">
|
|
1039
|
-
<ToolbarIconButton icon={SlidersHorizontal} label="Filter" />
|
|
1040
|
-
<ToolbarIconButton icon={ArrowUpDown} label="Sort" />
|
|
1041
|
-
<ToolbarIconButton icon={Link2} label="Copy link" />
|
|
1042
|
-
<ToolbarIconButton icon={Search} label="Search" />
|
|
1043
|
-
<ToolbarIconButton icon={Eye} label="Views" />
|
|
1044
|
-
<ToolbarIconButton icon={Maximize2} label="Expand" />
|
|
1045
697
|
<button
|
|
1046
698
|
type="button"
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
className="ml-1 inline-flex h-7 items-center gap-1 rounded-md bg-primary px-2.5 text-[12.5px] font-semibold text-primary-foreground transition-colors hover:bg-primary/90"
|
|
699
|
+
onClick={onCreateNote}
|
|
700
|
+
disabled={!onCreateNote || creatingNote}
|
|
701
|
+
className="ml-1 inline-flex h-7 items-center gap-1 rounded-md bg-primary px-2.5 text-[12.5px] font-semibold text-primary-foreground transition-colors hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-60"
|
|
1051
702
|
>
|
|
1052
|
-
|
|
703
|
+
{creatingNote ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Plus className="h-3.5 w-3.5" />}
|
|
704
|
+
{creatingNote ? 'Creating…' : 'New'}
|
|
1053
705
|
</button>
|
|
1054
706
|
</div>
|
|
1055
707
|
</div>
|
|
@@ -1058,13 +710,13 @@ function PageHeader({
|
|
|
1058
710
|
}
|
|
1059
711
|
|
|
1060
712
|
export default function NotesPage() {
|
|
1061
|
-
const { app, route,
|
|
713
|
+
const { app, route, collectionItem: rawCollectionItem } = useNotis();
|
|
1062
714
|
const navigation = useNotisNavigation();
|
|
1063
715
|
const { request } = useBackend();
|
|
1064
|
-
const
|
|
716
|
+
const { upsert: upsertNoteDocument } = useUpsertDocument(NOTE_DATABASE_SLUG);
|
|
1065
717
|
|
|
1066
718
|
const [activeTab, setActiveTab] = useState<TabKey>('gallery');
|
|
1067
|
-
const [activeDateProperty, setActiveDateProperty] = useState(
|
|
719
|
+
const [activeDateProperty, setActiveDateProperty] = useState(CREATED_DATE_FIELD);
|
|
1068
720
|
const [visibleMonth, setVisibleMonth] = useState(() => new Date());
|
|
1069
721
|
const [creatingDocument, setCreatingDocument] = useState(false);
|
|
1070
722
|
const [savingNoteId, setSavingNoteId] = useState<string | null>(null);
|
|
@@ -1076,10 +728,13 @@ export default function NotesPage() {
|
|
|
1076
728
|
const [editingFolderTitle, setEditingFolderTitle] = useState(false);
|
|
1077
729
|
const [folderTitleOverride, setFolderTitleOverride] = useState<{ id: string; title: string } | null>(null);
|
|
1078
730
|
const [folderIconOverride, setFolderIconOverride] = useState<{ id: string; icon: string | null } | null>(null);
|
|
731
|
+
// Which bulk folder action is awaiting a target-folder pick, if any.
|
|
732
|
+
const [pendingFolderAction, setPendingFolderAction] = useState<'move' | 'add' | null>(null);
|
|
733
|
+
const [bulkRunning, setBulkRunning] = useState(false);
|
|
1079
734
|
const folderRenameSubmittingRef = useRef(false);
|
|
1080
735
|
|
|
1081
|
-
const collectionItem = normalizeCollectionItem(
|
|
1082
|
-
const noteSchema =
|
|
736
|
+
const collectionItem = normalizeCollectionItem(rawCollectionItem);
|
|
737
|
+
const noteSchema = useDatabaseSchema(NOTE_DATABASE_SLUG);
|
|
1083
738
|
const noteProperties = noteSchema.properties;
|
|
1084
739
|
const titleProperty = noteProperties.find((p) => p.type === 'title');
|
|
1085
740
|
const titlePropertyName = titleProperty?.name ?? 'Title';
|
|
@@ -1112,8 +767,8 @@ export default function NotesPage() {
|
|
|
1112
767
|
}
|
|
1113
768
|
: undefined;
|
|
1114
769
|
|
|
1115
|
-
const notesQuery =
|
|
1116
|
-
const foldersQuery =
|
|
770
|
+
const notesQuery = useDocuments(NOTE_DATABASE_SLUG, { filter: notesFilter, pageSize: 250 });
|
|
771
|
+
const foldersQuery = useDocuments(FOLDER_DATABASE_SLUG, { pageSize: 500 });
|
|
1117
772
|
|
|
1118
773
|
const { setLoading: setSearchLoading } = useTopBarSearch({
|
|
1119
774
|
value: searchQuery,
|
|
@@ -1140,19 +795,26 @@ export default function NotesPage() {
|
|
|
1140
795
|
const folderOptions = buildFolderOptions(folders);
|
|
1141
796
|
const folderNameById = new Map(folderOptions.map((f) => [f.id, f.title]));
|
|
1142
797
|
|
|
1143
|
-
|
|
1144
|
-
|
|
798
|
+
// The calendar always offers a built-in "Created" field (sourced from each
|
|
799
|
+
// note's created_at) plus any user-defined date properties on the database.
|
|
800
|
+
const calendarDateFields: CalendarDateField[] = [
|
|
801
|
+
{ value: CREATED_DATE_FIELD, label: CREATED_DATE_LABEL },
|
|
802
|
+
...dateProperties.map((p) => ({ value: p.name, label: p.name })),
|
|
803
|
+
];
|
|
804
|
+
const activeCalendarField = calendarDateFields.some((f) => f.value === activeDateProperty)
|
|
805
|
+
? activeDateProperty
|
|
806
|
+
: CREATED_DATE_FIELD;
|
|
1145
807
|
|
|
1146
808
|
const notesByDay = new Map<string, DocumentRecord[]>();
|
|
1147
809
|
for (const note of notes) {
|
|
1148
|
-
const key = getDateKey(note
|
|
810
|
+
const key = getDateKey(getNoteDateValue(note, activeCalendarField));
|
|
1149
811
|
if (!key) continue;
|
|
1150
812
|
const bucket = notesByDay.get(key) ?? [];
|
|
1151
813
|
bucket.push(note);
|
|
1152
814
|
notesByDay.set(key, bucket);
|
|
1153
815
|
}
|
|
1154
816
|
const scheduledNotesCount = notes.filter((n) =>
|
|
1155
|
-
|
|
817
|
+
Boolean(getDateKey(getNoteDateValue(n, activeCalendarField))),
|
|
1156
818
|
).length;
|
|
1157
819
|
|
|
1158
820
|
const monthDays = buildCalendarDays(visibleMonth);
|
|
@@ -1170,31 +832,16 @@ export default function NotesPage() {
|
|
|
1170
832
|
}, [activeFolderId]);
|
|
1171
833
|
|
|
1172
834
|
useEffect(() => {
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
if (!dateProperties.some((p) => p.name === activeDateProperty)) {
|
|
1178
|
-
setActiveDateProperty(dateProperties[0].name);
|
|
1179
|
-
}
|
|
835
|
+
const valid =
|
|
836
|
+
activeDateProperty === CREATED_DATE_FIELD ||
|
|
837
|
+
dateProperties.some((p) => p.name === activeDateProperty);
|
|
838
|
+
if (!valid) setActiveDateProperty(CREATED_DATE_FIELD);
|
|
1180
839
|
}, [activeDateProperty, dateProperties]);
|
|
1181
840
|
|
|
1182
841
|
async function openNote(document: DocumentRecord) {
|
|
1183
842
|
navigation.toDocument(document.id, document.title);
|
|
1184
843
|
}
|
|
1185
844
|
|
|
1186
|
-
const upsertNoteDocument = useCallback(
|
|
1187
|
-
async (args: UpsertDocumentArgs): Promise<DocumentRecord> => {
|
|
1188
|
-
const result = await upsertNote.call(buildUpsertNoteArgs(args));
|
|
1189
|
-
const message = result.error ?? result.message;
|
|
1190
|
-
if (!result.document) {
|
|
1191
|
-
throw new Error(message ?? 'Failed to save note');
|
|
1192
|
-
}
|
|
1193
|
-
return normalizeDocumentRecord(result.document);
|
|
1194
|
-
},
|
|
1195
|
-
[upsertNote.call],
|
|
1196
|
-
);
|
|
1197
|
-
|
|
1198
845
|
async function createDocument() {
|
|
1199
846
|
setCreatingDocument(true);
|
|
1200
847
|
setErrorMessage(null);
|
|
@@ -1323,38 +970,96 @@ export default function NotesPage() {
|
|
|
1323
970
|
}
|
|
1324
971
|
}
|
|
1325
972
|
|
|
1326
|
-
|
|
973
|
+
// Holds both table rows and gallery cards so Shift+Arrow can scroll the head
|
|
974
|
+
// into view regardless of the active view.
|
|
975
|
+
const rowRefs = useRef(new Map<string, HTMLElement>());
|
|
976
|
+
const selectionEnabled = activeTab !== 'calendar';
|
|
1327
977
|
const multiSelect = useMultiSelect<DocumentRecord>({
|
|
1328
978
|
items: notes,
|
|
1329
979
|
getId: (note) => note.id,
|
|
1330
|
-
bindKeyboardShortcuts:
|
|
1331
|
-
enableDragSelect:
|
|
980
|
+
bindKeyboardShortcuts: selectionEnabled,
|
|
981
|
+
enableDragSelect: selectionEnabled,
|
|
1332
982
|
onHeadChange: (id) => {
|
|
1333
983
|
if (!id) return;
|
|
1334
984
|
rowRefs.current.get(id)?.scrollIntoView({ block: 'nearest' });
|
|
1335
985
|
},
|
|
1336
986
|
});
|
|
1337
987
|
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
988
|
+
// Clear the selection when the visible set changes out from under it (folder
|
|
989
|
+
// switch) or when entering a view that can't act on a selection (calendar),
|
|
990
|
+
// so bulk actions never apply to off-screen notes.
|
|
991
|
+
const clearSelection = multiSelect.clear;
|
|
992
|
+
useEffect(() => {
|
|
993
|
+
clearSelection();
|
|
994
|
+
}, [activeFolderId, clearSelection]);
|
|
995
|
+
useEffect(() => {
|
|
996
|
+
if (!selectionEnabled) clearSelection();
|
|
997
|
+
}, [selectionEnabled, clearSelection]);
|
|
998
|
+
|
|
999
|
+
// Runs `apply` for each selected note in parallel, then clears + refetches.
|
|
1000
|
+
// `apply` receives the full note so handlers can read its current properties.
|
|
1001
|
+
async function runBulk(
|
|
1002
|
+
apply: (note: DocumentRecord) => UpsertDocumentArgs,
|
|
1003
|
+
failureMessage: string,
|
|
1004
|
+
) {
|
|
1005
|
+
const selected = multiSelect.getSelectedItems();
|
|
1006
|
+
if (selected.length === 0) return;
|
|
1007
|
+
setBulkRunning(true);
|
|
1341
1008
|
setErrorMessage(null);
|
|
1342
1009
|
try {
|
|
1343
|
-
await Promise.all(
|
|
1344
|
-
ids.map((documentId) =>
|
|
1345
|
-
upsertNoteDocument({
|
|
1346
|
-
documentId,
|
|
1347
|
-
properties: { [folderPropertyName]: [] },
|
|
1348
|
-
}),
|
|
1349
|
-
),
|
|
1350
|
-
);
|
|
1010
|
+
await Promise.all(selected.map((note) => upsertNoteDocument(apply(note))));
|
|
1351
1011
|
multiSelect.clear();
|
|
1352
1012
|
notesQuery.refetch();
|
|
1353
1013
|
} catch (error) {
|
|
1354
|
-
setErrorMessage(error instanceof Error ? error.message :
|
|
1014
|
+
setErrorMessage(error instanceof Error ? error.message : failureMessage);
|
|
1015
|
+
} finally {
|
|
1016
|
+
setBulkRunning(false);
|
|
1355
1017
|
}
|
|
1356
1018
|
}
|
|
1357
1019
|
|
|
1020
|
+
function bulkClearFolder() {
|
|
1021
|
+
return runBulk(
|
|
1022
|
+
(note) => ({ documentId: note.id, properties: { [folderPropertyName]: [] } }),
|
|
1023
|
+
'Failed to update notes',
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function bulkDelete() {
|
|
1028
|
+
return runBulk(
|
|
1029
|
+
(note) => ({ documentId: note.id, operation: 'archive' }),
|
|
1030
|
+
'Failed to delete notes',
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function bulkMoveToFolder(folderId: string) {
|
|
1035
|
+
return runBulk(
|
|
1036
|
+
(note) => ({ documentId: note.id, properties: { [folderPropertyName]: [folderId] } }),
|
|
1037
|
+
'Failed to move notes',
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function bulkAddToFolder(folderId: string) {
|
|
1042
|
+
return runBulk(
|
|
1043
|
+
(note) => ({
|
|
1044
|
+
documentId: note.id,
|
|
1045
|
+
properties: {
|
|
1046
|
+
[folderPropertyName]: Array.from(
|
|
1047
|
+
new Set([...getRelationIds(note.properties[folderPropertyName]), folderId]),
|
|
1048
|
+
),
|
|
1049
|
+
},
|
|
1050
|
+
}),
|
|
1051
|
+
'Failed to add notes to folder',
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
async function handleFolderPick(folderId: string) {
|
|
1056
|
+
const action = pendingFolderAction;
|
|
1057
|
+
setPendingFolderAction(null);
|
|
1058
|
+
if (!action) return;
|
|
1059
|
+
if (action === 'move') await bulkMoveToFolder(folderId);
|
|
1060
|
+
else await bulkAddToFolder(folderId);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1358
1063
|
return (
|
|
1359
1064
|
<main className="flex min-h-screen flex-col bg-background">
|
|
1360
1065
|
<PageHeader
|
|
@@ -1375,6 +1080,8 @@ export default function NotesPage() {
|
|
|
1375
1080
|
onCancelTitleEdit={cancelFolderTitleEdit}
|
|
1376
1081
|
activeTab={activeTab}
|
|
1377
1082
|
onTabChange={setActiveTab}
|
|
1083
|
+
onCreateNote={() => void createDocument()}
|
|
1084
|
+
creatingNote={creatingDocument}
|
|
1378
1085
|
/>
|
|
1379
1086
|
|
|
1380
1087
|
{topLevelError ? (
|
|
@@ -1394,6 +1101,8 @@ export default function NotesPage() {
|
|
|
1394
1101
|
creatingDocument={creatingDocument}
|
|
1395
1102
|
currentFolderLabel={currentFolderLabel}
|
|
1396
1103
|
hasCollectionItem={Boolean(activeFolderId)}
|
|
1104
|
+
multiSelect={multiSelect}
|
|
1105
|
+
rowRefs={rowRefs.current}
|
|
1397
1106
|
/>
|
|
1398
1107
|
) : null}
|
|
1399
1108
|
|
|
@@ -1417,35 +1126,63 @@ export default function NotesPage() {
|
|
|
1417
1126
|
|
|
1418
1127
|
{!isLoading && activeTab === 'calendar' ? (
|
|
1419
1128
|
<CalendarBody
|
|
1420
|
-
notes={notes}
|
|
1421
1129
|
monthDays={monthDays}
|
|
1422
1130
|
visibleMonth={visibleMonth}
|
|
1423
1131
|
setVisibleMonth={setVisibleMonth}
|
|
1424
1132
|
notesByDay={notesByDay}
|
|
1425
1133
|
statusPropertyName={statusPropertyName}
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
activeDateProperty={activeDateProperty}
|
|
1134
|
+
calendarDateFields={calendarDateFields}
|
|
1135
|
+
activeDateProperty={activeCalendarField}
|
|
1429
1136
|
setActiveDateProperty={setActiveDateProperty}
|
|
1430
1137
|
scheduledNotesCount={scheduledNotesCount}
|
|
1431
1138
|
onOpen={openNote}
|
|
1432
1139
|
/>
|
|
1433
1140
|
) : null}
|
|
1434
1141
|
|
|
1435
|
-
{
|
|
1142
|
+
{selectionEnabled ? (
|
|
1436
1143
|
<>
|
|
1437
1144
|
<MultiSelectDragOverlay rect={multiSelect.dragRect} />
|
|
1145
|
+
{pendingFolderAction ? (
|
|
1146
|
+
<BulkFolderPicker
|
|
1147
|
+
mode={pendingFolderAction}
|
|
1148
|
+
folderOptions={folderOptions}
|
|
1149
|
+
busy={bulkRunning}
|
|
1150
|
+
onPick={(folderId) => void handleFolderPick(folderId)}
|
|
1151
|
+
onClose={() => setPendingFolderAction(null)}
|
|
1152
|
+
/>
|
|
1153
|
+
) : null}
|
|
1438
1154
|
<MultiSelectActionBar
|
|
1439
1155
|
selectedCount={multiSelect.selectedCount}
|
|
1440
1156
|
itemLabel={{ singular: 'note', plural: 'notes' }}
|
|
1441
1157
|
actions={[
|
|
1158
|
+
{
|
|
1159
|
+
id: 'move-folder',
|
|
1160
|
+
label: 'Move to folder',
|
|
1161
|
+
shortcut: 'M',
|
|
1162
|
+
icon: <FolderOpen className="h-3.5 w-3.5" />,
|
|
1163
|
+
onRun: () => setPendingFolderAction('move'),
|
|
1164
|
+
},
|
|
1165
|
+
{
|
|
1166
|
+
id: 'add-folder',
|
|
1167
|
+
label: 'Add to folder',
|
|
1168
|
+
shortcut: 'F',
|
|
1169
|
+
icon: <FolderPlus className="h-3.5 w-3.5" />,
|
|
1170
|
+
onRun: () => setPendingFolderAction('add'),
|
|
1171
|
+
},
|
|
1442
1172
|
{
|
|
1443
1173
|
id: 'clear-folder',
|
|
1444
1174
|
label: 'Move out of folder',
|
|
1445
|
-
shortcut: 'M',
|
|
1446
1175
|
icon: <FolderMinus className="h-3.5 w-3.5" />,
|
|
1447
1176
|
onRun: bulkClearFolder,
|
|
1448
1177
|
},
|
|
1178
|
+
{
|
|
1179
|
+
id: 'delete',
|
|
1180
|
+
label: 'Delete',
|
|
1181
|
+
shortcut: '#',
|
|
1182
|
+
destructive: true,
|
|
1183
|
+
icon: <Trash className="h-3.5 w-3.5" />,
|
|
1184
|
+
onRun: bulkDelete,
|
|
1185
|
+
},
|
|
1449
1186
|
]}
|
|
1450
1187
|
/>
|
|
1451
1188
|
</>
|
|
@@ -1466,6 +1203,8 @@ function GalleryBody({
|
|
|
1466
1203
|
creatingDocument,
|
|
1467
1204
|
currentFolderLabel,
|
|
1468
1205
|
hasCollectionItem,
|
|
1206
|
+
multiSelect,
|
|
1207
|
+
rowRefs,
|
|
1469
1208
|
}: {
|
|
1470
1209
|
notes: DocumentRecord[];
|
|
1471
1210
|
statusPropertyName: string | null;
|
|
@@ -1474,6 +1213,8 @@ function GalleryBody({
|
|
|
1474
1213
|
creatingDocument: boolean;
|
|
1475
1214
|
currentFolderLabel: string;
|
|
1476
1215
|
hasCollectionItem: boolean;
|
|
1216
|
+
multiSelect: MultiSelectController<DocumentRecord>;
|
|
1217
|
+
rowRefs: Map<string, HTMLElement>;
|
|
1477
1218
|
}) {
|
|
1478
1219
|
if (!notes.length) {
|
|
1479
1220
|
return (
|
|
@@ -1501,13 +1242,26 @@ function GalleryBody({
|
|
|
1501
1242
|
}
|
|
1502
1243
|
|
|
1503
1244
|
return (
|
|
1504
|
-
<div
|
|
1245
|
+
<div
|
|
1246
|
+
{...multiSelect.getContainerProps()}
|
|
1247
|
+
className="grid grid-cols-1 gap-4 px-4 py-4 sm:grid-cols-2 sm:px-6 lg:grid-cols-3 xl:grid-cols-4"
|
|
1248
|
+
>
|
|
1505
1249
|
{notes.map((note) => (
|
|
1506
1250
|
<NoteCard
|
|
1507
1251
|
key={note.id}
|
|
1508
1252
|
note={note}
|
|
1509
1253
|
statusPropertyName={statusPropertyName}
|
|
1510
1254
|
onOpen={() => onOpenNote(note)}
|
|
1255
|
+
isSelected={multiSelect.isSelected(note.id)}
|
|
1256
|
+
itemProps={multiSelect.getItemProps(note.id)}
|
|
1257
|
+
checkboxProps={multiSelect.getCheckboxProps(note.id)}
|
|
1258
|
+
cardRef={(node) => {
|
|
1259
|
+
if (node) {
|
|
1260
|
+
rowRefs.set(note.id, node);
|
|
1261
|
+
} else {
|
|
1262
|
+
rowRefs.delete(note.id);
|
|
1263
|
+
}
|
|
1264
|
+
}}
|
|
1511
1265
|
/>
|
|
1512
1266
|
))}
|
|
1513
1267
|
</div>
|
|
@@ -1518,25 +1272,56 @@ function NoteCard({
|
|
|
1518
1272
|
note,
|
|
1519
1273
|
statusPropertyName,
|
|
1520
1274
|
onOpen,
|
|
1275
|
+
isSelected,
|
|
1276
|
+
itemProps,
|
|
1277
|
+
checkboxProps,
|
|
1278
|
+
cardRef,
|
|
1521
1279
|
}: {
|
|
1522
1280
|
note: DocumentRecord;
|
|
1523
1281
|
statusPropertyName: string | null;
|
|
1524
1282
|
onOpen: () => void;
|
|
1283
|
+
isSelected: boolean;
|
|
1284
|
+
itemProps: { 'data-notis-row-id': string; onMouseDown: (event: React.MouseEvent) => void };
|
|
1285
|
+
checkboxProps: { isSelected: boolean; onClick: (event: React.MouseEvent) => void };
|
|
1286
|
+
cardRef: (node: HTMLDivElement | null) => void;
|
|
1525
1287
|
}) {
|
|
1526
1288
|
const status = statusPropertyName ? getStatusLabel(note.properties[statusPropertyName]) : null;
|
|
1527
1289
|
const coverUrl = getCoverUrl(note);
|
|
1528
1290
|
const title = getNoteTitle(note);
|
|
1529
1291
|
const previewText = getNotePreviewText(note);
|
|
1530
1292
|
|
|
1293
|
+
// A plain div (no `role="button"`) so the SDK's drag-select can arm from a
|
|
1294
|
+
// card body; keyboard access is preserved via tabIndex + Enter.
|
|
1531
1295
|
return (
|
|
1532
|
-
<
|
|
1533
|
-
|
|
1296
|
+
<div
|
|
1297
|
+
{...itemProps}
|
|
1298
|
+
ref={cardRef}
|
|
1299
|
+
tabIndex={0}
|
|
1534
1300
|
onClick={onOpen}
|
|
1301
|
+
onKeyDown={(e) => {
|
|
1302
|
+
if (e.key === 'Enter') {
|
|
1303
|
+
e.preventDefault();
|
|
1304
|
+
onOpen();
|
|
1305
|
+
}
|
|
1306
|
+
}}
|
|
1535
1307
|
className={cn(
|
|
1536
|
-
'group flex h-full w-full flex-col overflow-hidden rounded-xl border
|
|
1308
|
+
'group relative flex h-full w-full cursor-pointer flex-col overflow-hidden rounded-xl border bg-card text-left',
|
|
1537
1309
|
'transition-colors hover:border-foreground/20 hover:shadow-sm',
|
|
1310
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
1311
|
+
isSelected ? 'border-primary ring-2 ring-primary' : 'border-border',
|
|
1538
1312
|
)}
|
|
1539
1313
|
>
|
|
1314
|
+
<div className="absolute left-2 top-2 z-10">
|
|
1315
|
+
<MultiSelectCheckbox
|
|
1316
|
+
{...checkboxProps}
|
|
1317
|
+
alwaysVisible={isSelected}
|
|
1318
|
+
ariaLabel={isSelected ? 'Deselect note' : 'Select note'}
|
|
1319
|
+
className={cn(
|
|
1320
|
+
'transition-opacity',
|
|
1321
|
+
!isSelected && 'opacity-0 group-hover:opacity-100 focus-visible:opacity-100',
|
|
1322
|
+
)}
|
|
1323
|
+
/>
|
|
1324
|
+
</div>
|
|
1540
1325
|
<div className="relative aspect-[16/10] w-full overflow-hidden">
|
|
1541
1326
|
{coverUrl ? (
|
|
1542
1327
|
// eslint-disable-next-line @next/next/no-img-element
|
|
@@ -1560,7 +1345,96 @@ function NoteCard({
|
|
|
1560
1345
|
{title}
|
|
1561
1346
|
</span>
|
|
1562
1347
|
</div>
|
|
1563
|
-
</
|
|
1348
|
+
</div>
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
/* -------------------------------------------------------------------------- */
|
|
1353
|
+
/* Bulk folder picker (Move to / Add to folder) */
|
|
1354
|
+
/* -------------------------------------------------------------------------- */
|
|
1355
|
+
|
|
1356
|
+
function BulkFolderPicker({
|
|
1357
|
+
mode,
|
|
1358
|
+
folderOptions,
|
|
1359
|
+
busy,
|
|
1360
|
+
onPick,
|
|
1361
|
+
onClose,
|
|
1362
|
+
}: {
|
|
1363
|
+
mode: 'move' | 'add';
|
|
1364
|
+
folderOptions: FolderOption[];
|
|
1365
|
+
busy: boolean;
|
|
1366
|
+
onPick: (folderId: string) => void;
|
|
1367
|
+
onClose: () => void;
|
|
1368
|
+
}) {
|
|
1369
|
+
const [query, setQuery] = useState('');
|
|
1370
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
1371
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
1372
|
+
const filtered = normalizedQuery
|
|
1373
|
+
? folderOptions.filter((f) => f.pathLabel.toLowerCase().includes(normalizedQuery))
|
|
1374
|
+
: folderOptions;
|
|
1375
|
+
|
|
1376
|
+
useEffect(() => {
|
|
1377
|
+
function handlePointerDown(event: Event) {
|
|
1378
|
+
const target = event.target;
|
|
1379
|
+
if (target instanceof Node && containerRef.current?.contains(target)) return;
|
|
1380
|
+
onClose();
|
|
1381
|
+
}
|
|
1382
|
+
const root = containerRef.current?.getRootNode();
|
|
1383
|
+
const eventTarget =
|
|
1384
|
+
root instanceof ShadowRoot || root instanceof Document ? root : document;
|
|
1385
|
+
eventTarget.addEventListener('pointerdown', handlePointerDown);
|
|
1386
|
+
return () => eventTarget.removeEventListener('pointerdown', handlePointerDown);
|
|
1387
|
+
}, [onClose]);
|
|
1388
|
+
|
|
1389
|
+
return (
|
|
1390
|
+
<div
|
|
1391
|
+
ref={containerRef}
|
|
1392
|
+
role="dialog"
|
|
1393
|
+
aria-label={mode === 'move' ? 'Move notes to folder' : 'Add notes to folder'}
|
|
1394
|
+
className="fixed bottom-16 left-1/2 z-[70] w-[320px] -translate-x-1/2 rounded-lg border border-border bg-popover p-3 text-popover-foreground shadow-xl"
|
|
1395
|
+
>
|
|
1396
|
+
<div className="mb-2 flex items-center justify-between">
|
|
1397
|
+
<span className="text-[12px] font-semibold text-foreground">
|
|
1398
|
+
{mode === 'move' ? 'Move to folder' : 'Add to folder'}
|
|
1399
|
+
</span>
|
|
1400
|
+
<button
|
|
1401
|
+
type="button"
|
|
1402
|
+
aria-label="Close"
|
|
1403
|
+
onClick={onClose}
|
|
1404
|
+
className="inline-flex size-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
1405
|
+
>
|
|
1406
|
+
<X className="h-3.5 w-3.5" />
|
|
1407
|
+
</button>
|
|
1408
|
+
</div>
|
|
1409
|
+
<div className="mb-2 flex items-center gap-2 rounded-md border border-border bg-background px-2">
|
|
1410
|
+
<Search className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
1411
|
+
<input
|
|
1412
|
+
autoFocus
|
|
1413
|
+
value={query}
|
|
1414
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
1415
|
+
placeholder="Search folders"
|
|
1416
|
+
className="h-8 min-w-0 flex-1 bg-transparent text-sm outline-none"
|
|
1417
|
+
/>
|
|
1418
|
+
</div>
|
|
1419
|
+
<div className="max-h-[220px] overflow-y-auto">
|
|
1420
|
+
{filtered.length ? (
|
|
1421
|
+
filtered.map((folder) => (
|
|
1422
|
+
<button
|
|
1423
|
+
key={folder.id}
|
|
1424
|
+
type="button"
|
|
1425
|
+
disabled={busy}
|
|
1426
|
+
onClick={() => onPick(folder.id)}
|
|
1427
|
+
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-[13px] text-foreground transition-colors hover:bg-muted disabled:pointer-events-none disabled:opacity-50"
|
|
1428
|
+
>
|
|
1429
|
+
<Folder className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
1430
|
+
<span className="truncate">{folder.pathLabel}</span>
|
|
1431
|
+
</button>
|
|
1432
|
+
))
|
|
1433
|
+
) : (
|
|
1434
|
+
<p className="py-6 text-center text-sm text-muted-foreground">No folders found.</p>
|
|
1435
|
+
)}
|
|
1436
|
+
</div>
|
|
1437
|
+
</div>
|
|
1564
1438
|
);
|
|
1565
1439
|
}
|
|
1566
1440
|
|
|
@@ -1595,7 +1469,7 @@ function TableBody({
|
|
|
1595
1469
|
currentFolderLabel: string;
|
|
1596
1470
|
hasCollectionItem: boolean;
|
|
1597
1471
|
multiSelect: MultiSelectController<DocumentRecord>;
|
|
1598
|
-
rowRefs: Map<string,
|
|
1472
|
+
rowRefs: Map<string, HTMLElement>;
|
|
1599
1473
|
}) {
|
|
1600
1474
|
const columns = metadataProperties.slice(0, 6);
|
|
1601
1475
|
const allSelected =
|
|
@@ -1610,21 +1484,6 @@ function TableBody({
|
|
|
1610
1484
|
|
|
1611
1485
|
return (
|
|
1612
1486
|
<div className="flex flex-1 flex-col gap-4 px-6 py-5">
|
|
1613
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
1614
|
-
<FilterChip icon={Settings2} label="Status is" value="Drafting, Review" />
|
|
1615
|
-
<FilterChip icon={CalendarDays} label="Due" value="this week" />
|
|
1616
|
-
<FilterChip icon={Rows3} label="Sort by" value="Updated ↓" />
|
|
1617
|
-
<button
|
|
1618
|
-
type="button"
|
|
1619
|
-
className="inline-flex items-center gap-1.5 rounded-full border border-dashed border-border px-2.5 py-1 text-[11px] font-medium text-muted-foreground transition-colors hover:border-border hover:text-foreground"
|
|
1620
|
-
>
|
|
1621
|
-
<Plus className="h-3 w-3" />
|
|
1622
|
-
Add filter
|
|
1623
|
-
</button>
|
|
1624
|
-
<div className="flex-1" />
|
|
1625
|
-
<Eyebrow>Hide · Tags, Author</Eyebrow>
|
|
1626
|
-
</div>
|
|
1627
|
-
|
|
1628
1487
|
{!notes.length ? (
|
|
1629
1488
|
<EmptyState
|
|
1630
1489
|
icon={Table2}
|
|
@@ -1697,45 +1556,10 @@ function TableBody({
|
|
|
1697
1556
|
</div>
|
|
1698
1557
|
</div>
|
|
1699
1558
|
)}
|
|
1700
|
-
|
|
1701
|
-
<div className="flex flex-wrap items-center gap-3 rounded-lg border border-border bg-card px-4 py-3">
|
|
1702
|
-
<div className="flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-md bg-muted">
|
|
1703
|
-
<Sparkles className="h-3.5 w-3.5 text-foreground" />
|
|
1704
|
-
</div>
|
|
1705
|
-
<div className="min-w-0 flex-1">
|
|
1706
|
-
<p className="text-[13px] font-semibold text-foreground">Need a different view?</p>
|
|
1707
|
-
<p className="text-[12px] text-muted-foreground">
|
|
1708
|
-
Ask Notis to add a column, group by folder, hide shipped rows, or sort differently. No settings maze.
|
|
1709
|
-
</p>
|
|
1710
|
-
</div>
|
|
1711
|
-
<div className="flex flex-wrap gap-2">
|
|
1712
|
-
<AgentHintChip>group by Folder</AgentHintChip>
|
|
1713
|
-
<AgentHintChip>hide Updated</AgentHintChip>
|
|
1714
|
-
<AgentHintChip>sort by Words</AgentHintChip>
|
|
1715
|
-
</div>
|
|
1716
|
-
</div>
|
|
1717
1559
|
</div>
|
|
1718
1560
|
);
|
|
1719
1561
|
}
|
|
1720
1562
|
|
|
1721
|
-
function FilterChip({ icon: Icon, label, value }: { icon: Icon; label: string; value: string }) {
|
|
1722
|
-
return (
|
|
1723
|
-
<span className="inline-flex items-center gap-1.5 rounded-full border border-border bg-background px-2.5 py-1 text-[11px]">
|
|
1724
|
-
<Icon className="h-3 w-3 text-muted-foreground" />
|
|
1725
|
-
<span className="text-muted-foreground">{label}</span>
|
|
1726
|
-
<span className="font-semibold text-foreground">{value}</span>
|
|
1727
|
-
</span>
|
|
1728
|
-
);
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
function AgentHintChip({ children }: { children: React.ReactNode }) {
|
|
1732
|
-
return (
|
|
1733
|
-
<span className="rounded-full bg-muted px-2.5 py-1 font-mono text-[10px] text-foreground/80">
|
|
1734
|
-
{children}
|
|
1735
|
-
</span>
|
|
1736
|
-
);
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
1563
|
function TableRow({
|
|
1740
1564
|
note,
|
|
1741
1565
|
columns,
|
|
@@ -1976,27 +1800,23 @@ function TableCell({
|
|
|
1976
1800
|
/* -------------------------------------------------------------------------- */
|
|
1977
1801
|
|
|
1978
1802
|
function CalendarBody({
|
|
1979
|
-
notes,
|
|
1980
1803
|
monthDays,
|
|
1981
1804
|
visibleMonth,
|
|
1982
1805
|
setVisibleMonth,
|
|
1983
1806
|
notesByDay,
|
|
1984
1807
|
statusPropertyName,
|
|
1985
|
-
|
|
1986
|
-
dateProperties,
|
|
1808
|
+
calendarDateFields,
|
|
1987
1809
|
activeDateProperty,
|
|
1988
1810
|
setActiveDateProperty,
|
|
1989
1811
|
scheduledNotesCount,
|
|
1990
1812
|
onOpen,
|
|
1991
1813
|
}: {
|
|
1992
|
-
notes: DocumentRecord[];
|
|
1993
1814
|
monthDays: Date[];
|
|
1994
1815
|
visibleMonth: Date;
|
|
1995
1816
|
setVisibleMonth: (d: Date) => void;
|
|
1996
1817
|
notesByDay: Map<string, DocumentRecord[]>;
|
|
1997
1818
|
statusPropertyName: string | null;
|
|
1998
|
-
|
|
1999
|
-
dateProperties: DatabaseProperty[];
|
|
1819
|
+
calendarDateFields: CalendarDateField[];
|
|
2000
1820
|
activeDateProperty: string;
|
|
2001
1821
|
setActiveDateProperty: (v: string) => void;
|
|
2002
1822
|
scheduledNotesCount: number;
|
|
@@ -2035,19 +1855,14 @@ function CalendarBody({
|
|
|
2035
1855
|
<span className="text-muted-foreground">Date field</span>
|
|
2036
1856
|
<select
|
|
2037
1857
|
className="appearance-none bg-transparent pr-3 font-semibold text-foreground outline-none"
|
|
2038
|
-
value={
|
|
1858
|
+
value={activeDateProperty}
|
|
2039
1859
|
onChange={(e) => setActiveDateProperty(e.target.value)}
|
|
2040
|
-
disabled={!dateProperties.length}
|
|
2041
1860
|
>
|
|
2042
|
-
{
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
))
|
|
2048
|
-
) : (
|
|
2049
|
-
<option value="">—</option>
|
|
2050
|
-
)}
|
|
1861
|
+
{calendarDateFields.map((field) => (
|
|
1862
|
+
<option key={field.value} value={field.value}>
|
|
1863
|
+
{field.label}
|
|
1864
|
+
</option>
|
|
1865
|
+
))}
|
|
2051
1866
|
</select>
|
|
2052
1867
|
<ChevronDown className="h-3 w-3 text-muted-foreground" />
|
|
2053
1868
|
</div>
|
|
@@ -2056,14 +1871,7 @@ function CalendarBody({
|
|
|
2056
1871
|
<Eyebrow>{pluralize(scheduledNotesCount, 'note')} scheduled</Eyebrow>
|
|
2057
1872
|
</div>
|
|
2058
1873
|
|
|
2059
|
-
|
|
2060
|
-
<EmptyState
|
|
2061
|
-
icon={CalendarDays}
|
|
2062
|
-
title="No date property yet"
|
|
2063
|
-
description="Add a date field to the notes database and this board will start plotting notes automatically."
|
|
2064
|
-
/>
|
|
2065
|
-
) : (
|
|
2066
|
-
<div className="overflow-hidden rounded-lg border border-border bg-card">
|
|
1874
|
+
<div className="overflow-hidden rounded-lg border border-border bg-card">
|
|
2067
1875
|
<div className="grid grid-cols-7 border-b border-border bg-muted/40">
|
|
2068
1876
|
{WEEKDAY_LABELS.map((label, i) => (
|
|
2069
1877
|
<div
|
|
@@ -2136,33 +1944,7 @@ function CalendarBody({
|
|
|
2136
1944
|
);
|
|
2137
1945
|
})}
|
|
2138
1946
|
</div>
|
|
2139
|
-
</div>
|
|
2140
|
-
)}
|
|
2141
|
-
|
|
2142
|
-
<div className="flex flex-wrap items-center gap-4 rounded-lg border border-border bg-card px-4 py-3">
|
|
2143
|
-
<Eyebrow>Legend</Eyebrow>
|
|
2144
|
-
<LegendItem color="bg-amber-500" label="Drafting" />
|
|
2145
|
-
<LegendItem color="bg-blue-500" label="Review" />
|
|
2146
|
-
<LegendItem color="bg-emerald-500" label="Shipped" />
|
|
2147
|
-
<LegendItem color="bg-stone-400" label="Idea" />
|
|
2148
|
-
<div className="flex-1" />
|
|
2149
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
2150
|
-
<Sparkles className="h-3.5 w-3.5 text-foreground" />
|
|
2151
|
-
<span className="text-[12px] text-muted-foreground">Ask Notis to</span>
|
|
2152
|
-
<AgentHintChip>use Publish date</AgentHintChip>
|
|
2153
|
-
<AgentHintChip>color by Folder</AgentHintChip>
|
|
2154
|
-
<AgentHintChip>switch to week view</AgentHintChip>
|
|
2155
|
-
</div>
|
|
2156
1947
|
</div>
|
|
2157
1948
|
</div>
|
|
2158
1949
|
);
|
|
2159
1950
|
}
|
|
2160
|
-
|
|
2161
|
-
function LegendItem({ color, label }: { color: string; label: string }) {
|
|
2162
|
-
return (
|
|
2163
|
-
<div className="flex items-center gap-1.5">
|
|
2164
|
-
<span className={cn('h-0.5 w-3 rounded', color)} />
|
|
2165
|
-
<span className="text-[11px] font-medium text-foreground/80">{label}</span>
|
|
2166
|
-
</div>
|
|
2167
|
-
);
|
|
2168
|
-
}
|