feat(notes): vues structurées tableau/kanban, flashcards et MCP robuste
Ajoute la base organisable par carnet (schéma, champs partagés, valeurs par note) avec activation guidée, tableau éditable, kanban et suppression de colonnes. Corrige le multiselect en vue tableau et enrichit sidebar, grille et i18n FR/EN. Inclut aussi les améliorations flashcards SM-2, l'audit consentement IA et la robustesse du serveur MCP (config, validation, rate-limit, métriques). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,10 +5,12 @@ export interface DeckSummary {
|
||||
id: string
|
||||
name: string
|
||||
notebookId: string | null
|
||||
notebookName: string | null
|
||||
totalCards: number
|
||||
dueCount: number
|
||||
masteredCount: number
|
||||
lastReviewedAt: string | null
|
||||
nextReviewAt: string | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
@@ -17,6 +19,7 @@ export async function listDeckSummaries(userId: string): Promise<DeckSummary[]>
|
||||
const decks = await prisma.flashcardDeck.findMany({
|
||||
where: { userId },
|
||||
include: {
|
||||
notebook: { select: { name: true } },
|
||||
flashcards: {
|
||||
select: {
|
||||
id: true,
|
||||
@@ -36,19 +39,27 @@ export async function listDeckSummaries(userId: string): Promise<DeckSummary[]>
|
||||
return decks.map((deck) => {
|
||||
const totalCards = deck.flashcards.length
|
||||
const dueCount = deck.flashcards.filter((c) => c.nextReviewAt <= now).length
|
||||
const masteredCount = deck.flashcards.filter((c) => isCardMastered(c.interval)).length
|
||||
// Maîtrisée = interval >= 7 jours (une semaine de bonne mémorisation)
|
||||
const masteredCount = deck.flashcards.filter((c) => c.interval >= 7).length
|
||||
const lastReview = deck.flashcards
|
||||
.flatMap((c) => c.reviews.map((r) => r.reviewedAt))
|
||||
.sort((a, b) => b.getTime() - a.getTime())[0]
|
||||
|
||||
// Date de la prochaine carte à réviser (la plus proche dans le futur)
|
||||
const nextReviewAt = deck.flashcards
|
||||
.map((c) => c.nextReviewAt)
|
||||
.sort((a, b) => a.getTime() - b.getTime())[0] ?? null
|
||||
|
||||
return {
|
||||
id: deck.id,
|
||||
name: deck.name,
|
||||
notebookId: deck.notebookId,
|
||||
notebookName: deck.notebook?.name ?? null,
|
||||
totalCards,
|
||||
dueCount,
|
||||
masteredCount,
|
||||
lastReviewedAt: lastReview ? lastReview.toISOString() : null,
|
||||
nextReviewAt: nextReviewAt ? nextReviewAt.toISOString() : null,
|
||||
createdAt: deck.createdAt.toISOString(),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { Prisma } from '@prisma/client'
|
||||
|
||||
export async function getOrCreateDeckForNotebook(params: {
|
||||
userId: string
|
||||
@@ -28,6 +29,14 @@ export async function getOrCreateDeckForNotebook(params: {
|
||||
notebookId,
|
||||
name: notebook.name,
|
||||
},
|
||||
}).catch(async (error) => {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') {
|
||||
const raced = await prisma.flashcardDeck.findFirst({
|
||||
where: { userId, notebookId },
|
||||
})
|
||||
if (raced) return raced
|
||||
}
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,15 @@ export const NOTES_VIEW_TYPE_COOKIE = 'memento-notes-view-type'
|
||||
export const NOTES_LAYOUT_STORAGE_KEY = 'memento-notes-layout'
|
||||
export const NOTES_VIEW_TYPE_STORAGE_KEY = 'memento-notes-view-type'
|
||||
|
||||
const LAYOUT_VALUES: NotesLayoutMode[] = ['grid', 'list', 'table']
|
||||
const LAYOUT_VALUES: NotesLayoutMode[] = ['grid', 'list', 'table', 'kanban', 'gallery']
|
||||
const VIEW_TYPE_VALUES: NotesViewType[] = ['notes', 'tasks']
|
||||
|
||||
export function parseNotesLayoutMode(value: string | undefined | null): NotesLayoutMode {
|
||||
if (value && (LAYOUT_VALUES as string[]).includes(value)) return value as NotesLayoutMode
|
||||
if (value && (LAYOUT_VALUES as string[]).includes(value)) {
|
||||
const mode = value as NotesLayoutMode
|
||||
if (mode === 'gallery') return 'grid'
|
||||
return mode
|
||||
}
|
||||
return 'list'
|
||||
}
|
||||
|
||||
|
||||
@@ -10,19 +10,27 @@ const prismaClientSingleton = () => {
|
||||
})
|
||||
}
|
||||
|
||||
/** Dev hot-reload can keep an old PrismaClient missing newly generated models. */
|
||||
function needsFreshPrismaClient(client: PrismaClient | undefined): boolean {
|
||||
if (!client) return true
|
||||
return typeof (client as PrismaClient & { flashcard?: unknown }).flashcard === 'undefined'
|
||||
}
|
||||
|
||||
declare const globalThis: {
|
||||
prismaGlobal: ReturnType<typeof prismaClientSingleton>;
|
||||
} & typeof global;
|
||||
|
||||
const prisma = globalThis.prismaGlobal ?? prismaClientSingleton()
|
||||
let prisma = globalThis.prismaGlobal ?? prismaClientSingleton()
|
||||
|
||||
// Log current model keys to verify availability
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (needsFreshPrismaClient(globalThis.prismaGlobal)) {
|
||||
prisma = prismaClientSingleton()
|
||||
}
|
||||
globalThis.prismaGlobal = prisma
|
||||
|
||||
const models = Object.keys(prisma).filter(k => !k.startsWith('_') && !k.startsWith('$'))
|
||||
console.log('[Prisma] Models loaded:', models.join(', '))
|
||||
}
|
||||
|
||||
export { prisma }
|
||||
export default prisma
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { NotebookSchemaPayload } from '@/lib/structured-views/types'
|
||||
|
||||
export type BootstrapStructuredTarget = 'table' | 'kanban'
|
||||
|
||||
export type BootstrapStructuredLabels = {
|
||||
statusName: string
|
||||
statusOptions: string[]
|
||||
}
|
||||
|
||||
export type BootstrapStructuredActions = {
|
||||
getSchema: () => NotebookSchemaPayload | null
|
||||
enableStructuredMode: () => Promise<NotebookSchemaPayload | null>
|
||||
addProperty: (name: string, type: string, options?: string[]) => Promise<NotebookSchemaPayload | null>
|
||||
setKanbanGroupProperty: (propertyId: string | null) => Promise<void>
|
||||
}
|
||||
|
||||
function pickGroupProperty(schema: NotebookSchemaPayload, statusName: string) {
|
||||
return (
|
||||
schema.properties.find((p) => p.type === 'select' && p.name === statusName) ??
|
||||
schema.properties.find((p) => p.type === 'select') ??
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
/** Active une base organisable avec champs par défaut (Statut) si nécessaire. */
|
||||
export async function bootstrapStructuredNotebook(
|
||||
target: BootstrapStructuredTarget,
|
||||
labels: BootstrapStructuredLabels,
|
||||
actions: BootstrapStructuredActions,
|
||||
): Promise<NotebookSchemaPayload> {
|
||||
let schema = actions.getSchema()
|
||||
if (!schema) {
|
||||
schema = await actions.enableStructuredMode()
|
||||
}
|
||||
if (!schema) {
|
||||
throw new Error('enable_failed')
|
||||
}
|
||||
|
||||
const selectProps = schema.properties.filter((p) => p.type === 'select')
|
||||
const needsDefaultStatus =
|
||||
target === 'kanban' ? selectProps.length === 0 : schema.properties.length === 0
|
||||
|
||||
if (needsDefaultStatus) {
|
||||
schema =
|
||||
(await actions.addProperty(labels.statusName, 'select', labels.statusOptions)) ?? schema
|
||||
}
|
||||
|
||||
if (target === 'kanban') {
|
||||
const groupProp = pickGroupProperty(schema, labels.statusName)
|
||||
if (!groupProp) {
|
||||
throw new Error('kanban_needs_select')
|
||||
}
|
||||
if (schema.viewSettings.kanbanGroupPropertyId !== groupProp.id) {
|
||||
await actions.setKanbanGroupProperty(groupProp.id)
|
||||
schema = {
|
||||
...schema,
|
||||
viewSettings: { ...schema.viewSettings, kanbanGroupPropertyId: groupProp.id },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
/** Ajoute un champ Statut + colonnes kanban sur une base déjà active. */
|
||||
export async function ensureKanbanStatusField(
|
||||
labels: BootstrapStructuredLabels,
|
||||
actions: BootstrapStructuredActions,
|
||||
): Promise<NotebookSchemaPayload> {
|
||||
return bootstrapStructuredNotebook('kanban', labels, actions)
|
||||
}
|
||||
22
memento-note/lib/structured-views/preferences.ts
Normal file
22
memento-note/lib/structured-views/preferences.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { StructuredViewMode } from './types'
|
||||
|
||||
const MODES: StructuredViewMode[] = ['list', 'table', 'kanban', 'gallery']
|
||||
|
||||
export function structuredViewStorageKey(notebookId: string) {
|
||||
return `memento-structured-view-${notebookId}`
|
||||
}
|
||||
|
||||
export function parseStructuredViewMode(value: string | null | undefined): StructuredViewMode {
|
||||
if (value && (MODES as string[]).includes(value)) return value as StructuredViewMode
|
||||
return 'list'
|
||||
}
|
||||
|
||||
export function getStructuredViewPreference(notebookId: string): StructuredViewMode {
|
||||
if (typeof window === 'undefined') return 'list'
|
||||
return parseStructuredViewMode(localStorage.getItem(structuredViewStorageKey(notebookId)))
|
||||
}
|
||||
|
||||
export function setStructuredViewPreference(notebookId: string, mode: StructuredViewMode) {
|
||||
if (typeof window === 'undefined') return
|
||||
localStorage.setItem(structuredViewStorageKey(notebookId), mode)
|
||||
}
|
||||
167
memento-note/lib/structured-views/property-utils.ts
Normal file
167
memento-note/lib/structured-views/property-utils.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import type {
|
||||
ColumnFilter,
|
||||
ColumnSort,
|
||||
NotePropertyValues,
|
||||
PropertyType,
|
||||
SchemaProperty,
|
||||
} from './types'
|
||||
|
||||
export function parsePropertyOptions(raw: string | null | undefined): string[] {
|
||||
if (!raw) return []
|
||||
try {
|
||||
const parsed = JSON.parse(raw)
|
||||
return Array.isArray(parsed) ? parsed.filter((v): v is string => typeof v === 'string') : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export function serializePropertyValue(type: PropertyType, value: unknown): string | null {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return type === 'checkbox' ? JSON.stringify(false) : null
|
||||
}
|
||||
if (type === 'checkbox') {
|
||||
return JSON.stringify(Boolean(value))
|
||||
}
|
||||
if (type === 'number') {
|
||||
const n = typeof value === 'number' ? value : Number(value)
|
||||
return Number.isFinite(n) ? JSON.stringify(n) : null
|
||||
}
|
||||
if (type === 'multiselect') {
|
||||
const arr = Array.isArray(value) ? value : []
|
||||
return JSON.stringify(arr.filter((v) => typeof v === 'string'))
|
||||
}
|
||||
return JSON.stringify(String(value))
|
||||
}
|
||||
|
||||
export function parseStoredPropertyValue(type: PropertyType, raw: string | null | undefined): unknown {
|
||||
if (raw == null || raw === '') {
|
||||
if (type === 'checkbox') return false
|
||||
if (type === 'multiselect') return []
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(raw)
|
||||
if (type === 'checkbox') return Boolean(parsed)
|
||||
if (type === 'number') return typeof parsed === 'number' ? parsed : Number(parsed)
|
||||
if (type === 'multiselect') return Array.isArray(parsed) ? parsed : []
|
||||
return parsed
|
||||
} catch {
|
||||
return raw
|
||||
}
|
||||
}
|
||||
|
||||
export function formatPropertyDisplay(type: PropertyType, value: unknown): string {
|
||||
if (value == null || value === '') return '—'
|
||||
if (type === 'checkbox') return value ? '✓' : '—'
|
||||
if (type === 'multiselect') {
|
||||
return Array.isArray(value) ? value.join(', ') : String(value)
|
||||
}
|
||||
if (type === 'date' && typeof value === 'string') {
|
||||
const d = new Date(value)
|
||||
return Number.isNaN(d.getTime()) ? String(value) : d.toLocaleDateString()
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
|
||||
export function comparePropertyValues(
|
||||
type: PropertyType,
|
||||
a: unknown,
|
||||
b: unknown,
|
||||
direction: 'asc' | 'desc',
|
||||
): number {
|
||||
const emptyA = a == null || a === '' || (Array.isArray(a) && a.length === 0)
|
||||
const emptyB = b == null || b === '' || (Array.isArray(b) && b.length === 0)
|
||||
if (emptyA && emptyB) return 0
|
||||
if (emptyA) return direction === 'asc' ? 1 : -1
|
||||
if (emptyB) return direction === 'asc' ? -1 : 1
|
||||
|
||||
let cmp = 0
|
||||
if (type === 'number') {
|
||||
cmp = Number(a) - Number(b)
|
||||
} else if (type === 'date') {
|
||||
cmp = new Date(String(a)).getTime() - new Date(String(b)).getTime()
|
||||
} else if (type === 'checkbox') {
|
||||
cmp = Number(Boolean(a)) - Number(Boolean(b))
|
||||
} else if (type === 'multiselect') {
|
||||
cmp = formatPropertyDisplay(type, a).localeCompare(formatPropertyDisplay(type, b))
|
||||
} else {
|
||||
cmp = String(a).localeCompare(String(b), undefined, { sensitivity: 'base' })
|
||||
}
|
||||
return direction === 'asc' ? cmp : -cmp
|
||||
}
|
||||
|
||||
export function matchesFilter(
|
||||
type: PropertyType,
|
||||
value: unknown,
|
||||
filter: ColumnFilter,
|
||||
): boolean {
|
||||
const { operator, value: filterValue } = filter
|
||||
const empty = value == null || value === '' || (Array.isArray(value) && value.length === 0)
|
||||
|
||||
if (operator === 'empty') return empty
|
||||
if (empty) return false
|
||||
|
||||
const haystack = formatPropertyDisplay(type, value).toLowerCase()
|
||||
const needle = (filterValue ?? '').toLowerCase()
|
||||
|
||||
if (operator === 'equals') {
|
||||
if (type === 'multiselect' && Array.isArray(value)) {
|
||||
return value.some((v) => String(v).toLowerCase() === needle)
|
||||
}
|
||||
return haystack === needle
|
||||
}
|
||||
return haystack.includes(needle)
|
||||
}
|
||||
|
||||
export function sortNotesWithProperties<T extends { id: string; title?: string | null; updatedAt: string | Date }>(
|
||||
notes: T[],
|
||||
valuesByNote: Record<string, NotePropertyValues>,
|
||||
sort: ColumnSort,
|
||||
properties: SchemaProperty[],
|
||||
): T[] {
|
||||
const prop = properties.find((p) => p.id === sort.propertyId)
|
||||
const copy = [...notes]
|
||||
copy.sort((a, b) => {
|
||||
if (sort.propertyId === 'title') {
|
||||
const ta = (a.title ?? '').toLowerCase()
|
||||
const tb = (b.title ?? '').toLowerCase()
|
||||
const cmp = ta.localeCompare(tb)
|
||||
return sort.direction === 'asc' ? cmp : -cmp
|
||||
}
|
||||
if (sort.propertyId === 'updatedAt') {
|
||||
const cmp = new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime()
|
||||
return sort.direction === 'asc' ? cmp : -cmp
|
||||
}
|
||||
if (!prop) return 0
|
||||
const va = valuesByNote[a.id]?.[prop.id]
|
||||
const vb = valuesByNote[b.id]?.[prop.id]
|
||||
return comparePropertyValues(prop.type, va, vb, sort.direction)
|
||||
})
|
||||
return copy
|
||||
}
|
||||
|
||||
export function filterNotesWithProperties<T extends { id: string }>(
|
||||
notes: T[],
|
||||
valuesByNote: Record<string, NotePropertyValues>,
|
||||
filters: ColumnFilter[],
|
||||
properties: SchemaProperty[],
|
||||
): T[] {
|
||||
if (filters.length === 0) return notes
|
||||
return notes.filter((note) => {
|
||||
const vals = valuesByNote[note.id] ?? {}
|
||||
return filters.every((filter) => {
|
||||
if (filter.propertyId === 'title') {
|
||||
const title = (note as { title?: string | null }).title ?? ''
|
||||
return matchesFilter('text', title, filter)
|
||||
}
|
||||
const prop = properties.find((p) => p.id === filter.propertyId)
|
||||
if (!prop) return true
|
||||
return matchesFilter(prop.type, vals[prop.id], filter)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function isValidPropertyType(value: string): value is PropertyType {
|
||||
return ['text', 'number', 'date', 'select', 'multiselect', 'checkbox'].includes(value)
|
||||
}
|
||||
68
memento-note/lib/structured-views/schema-serialize.ts
Normal file
68
memento-note/lib/structured-views/schema-serialize.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import type { NotebookViewSettings, NotebookSchemaPayload, SchemaProperty } from './types'
|
||||
import { parsePropertyOptions, parseStoredPropertyValue } from './property-utils'
|
||||
import { isValidPropertyType } from './property-utils'
|
||||
|
||||
type RawSchema = {
|
||||
id: string
|
||||
notebookId: string
|
||||
viewSettings: string | null
|
||||
properties: Array<{
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
options: string | null
|
||||
position: number
|
||||
}>
|
||||
}
|
||||
|
||||
export function parseViewSettings(raw: string | null | undefined): NotebookViewSettings {
|
||||
if (!raw) return {}
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as NotebookViewSettings
|
||||
return {
|
||||
kanbanGroupPropertyId: parsed.kanbanGroupPropertyId ?? null,
|
||||
}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export function serializeSchema(raw: RawSchema | null): NotebookSchemaPayload | null {
|
||||
if (!raw) return null
|
||||
const properties: SchemaProperty[] = raw.properties
|
||||
.filter((p) => isValidPropertyType(p.type))
|
||||
.sort((a, b) => a.position - b.position)
|
||||
.map((p) => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
type: p.type as SchemaProperty['type'],
|
||||
options: parsePropertyOptions(p.options),
|
||||
position: p.position,
|
||||
}))
|
||||
|
||||
return {
|
||||
id: raw.id,
|
||||
notebookId: raw.notebookId,
|
||||
viewSettings: parseViewSettings(raw.viewSettings),
|
||||
properties,
|
||||
}
|
||||
}
|
||||
|
||||
export function buildNoteValuesMap(
|
||||
noteIds: string[],
|
||||
rows: Array<{ noteId: string; propertyId: string; value: string | null; property: { type: string } }>,
|
||||
): Record<string, Record<string, unknown>> {
|
||||
const map: Record<string, Record<string, unknown>> = {}
|
||||
for (const id of noteIds) {
|
||||
map[id] = {}
|
||||
}
|
||||
for (const row of rows) {
|
||||
if (!map[row.noteId]) map[row.noteId] = {}
|
||||
if (!isValidPropertyType(row.property.type)) continue
|
||||
map[row.noteId][row.propertyId] = parseStoredPropertyValue(
|
||||
row.property.type,
|
||||
row.value,
|
||||
)
|
||||
}
|
||||
return map
|
||||
}
|
||||
55
memento-note/lib/structured-views/types.ts
Normal file
55
memento-note/lib/structured-views/types.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
export const PROPERTY_TYPES = [
|
||||
'text',
|
||||
'number',
|
||||
'date',
|
||||
'select',
|
||||
'multiselect',
|
||||
'checkbox',
|
||||
] as const
|
||||
|
||||
export type PropertyType = (typeof PROPERTY_TYPES)[number]
|
||||
|
||||
export const MAX_PROPERTIES_PER_NOTEBOOK = 15
|
||||
|
||||
export type StructuredViewMode = 'list' | 'table' | 'kanban' | 'gallery'
|
||||
|
||||
export type NotebookViewSettings = {
|
||||
kanbanGroupPropertyId?: string | null
|
||||
}
|
||||
|
||||
export type SchemaProperty = {
|
||||
id: string
|
||||
name: string
|
||||
type: PropertyType
|
||||
options: string[]
|
||||
position: number
|
||||
}
|
||||
|
||||
export type NotebookSchemaPayload = {
|
||||
id: string
|
||||
notebookId: string
|
||||
viewSettings: NotebookViewSettings
|
||||
properties: SchemaProperty[]
|
||||
}
|
||||
|
||||
export type NotePropertyValues = Record<string, unknown>
|
||||
|
||||
export type StructuredNotebookData = {
|
||||
schema: NotebookSchemaPayload | null
|
||||
noteValues: Record<string, NotePropertyValues>
|
||||
}
|
||||
|
||||
export type ColumnFilterOperator = 'contains' | 'equals' | 'empty'
|
||||
|
||||
export type ColumnFilter = {
|
||||
propertyId: string
|
||||
operator: ColumnFilterOperator
|
||||
value?: string
|
||||
}
|
||||
|
||||
export type SortDirection = 'asc' | 'desc'
|
||||
|
||||
export type ColumnSort = {
|
||||
propertyId: 'title' | 'updatedAt' | string
|
||||
direction: SortDirection
|
||||
}
|
||||
39
memento-note/lib/structured-views/wizard-templates.ts
Normal file
39
memento-note/lib/structured-views/wizard-templates.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { NotesLayoutMode } from '@/components/notes-list-views'
|
||||
import type { PropertyType } from '@/lib/structured-views/types'
|
||||
|
||||
export type WizardGoal = 'tasks' | 'learning' | 'reading' | 'simple'
|
||||
|
||||
export type WizardFieldId = 'status' | 'dueDate' | 'level' | 'lastReview' | 'read' | 'source'
|
||||
|
||||
export type WizardFieldDef = {
|
||||
id: WizardFieldId
|
||||
type: PropertyType
|
||||
hasOptions?: boolean
|
||||
}
|
||||
|
||||
export const WIZARD_GOALS: WizardGoal[] = ['tasks', 'learning', 'reading', 'simple']
|
||||
|
||||
export const WIZARD_FIELDS_BY_GOAL: Record<WizardGoal, WizardFieldDef[]> = {
|
||||
tasks: [
|
||||
{ id: 'status', type: 'select', hasOptions: true },
|
||||
{ id: 'dueDate', type: 'date' },
|
||||
],
|
||||
learning: [
|
||||
{ id: 'level', type: 'select', hasOptions: true },
|
||||
{ id: 'lastReview', type: 'date' },
|
||||
],
|
||||
reading: [
|
||||
{ id: 'read', type: 'checkbox' },
|
||||
{ id: 'source', type: 'text' },
|
||||
],
|
||||
simple: [],
|
||||
}
|
||||
|
||||
export const WIZARD_DEFAULT_VIEW: Record<WizardGoal, NotesLayoutMode> = {
|
||||
tasks: 'kanban',
|
||||
learning: 'gallery',
|
||||
reading: 'gallery',
|
||||
simple: 'list',
|
||||
}
|
||||
|
||||
export const KANBAN_GROUP_FIELD_ID: WizardFieldId = 'status'
|
||||
Reference in New Issue
Block a user