diff --git a/.eslintignore b/.eslintignore index 369d6fc81..ee6d74864 100644 --- a/.eslintignore +++ b/.eslintignore @@ -272,6 +272,7 @@ packages/app-desktop/gui/NoteList/NoteList2.js packages/app-desktop/gui/NoteList/NoteListSource.js packages/app-desktop/gui/NoteList/commands/focusElementNoteList.js packages/app-desktop/gui/NoteList/commands/index.js +packages/app-desktop/gui/NoteList/defaultItemRenderer.js packages/app-desktop/gui/NoteList/types.js packages/app-desktop/gui/NoteListControls/NoteListControls.js packages/app-desktop/gui/NoteListControls/commands/focusSearch.js diff --git a/.gitignore b/.gitignore index 477b900e8..9b7a88454 100644 --- a/.gitignore +++ b/.gitignore @@ -258,6 +258,7 @@ packages/app-desktop/gui/NoteList/NoteList2.js packages/app-desktop/gui/NoteList/NoteListSource.js packages/app-desktop/gui/NoteList/commands/focusElementNoteList.js packages/app-desktop/gui/NoteList/commands/index.js +packages/app-desktop/gui/NoteList/defaultItemRenderer.js packages/app-desktop/gui/NoteList/types.js packages/app-desktop/gui/NoteListControls/NoteListControls.js packages/app-desktop/gui/NoteListControls/commands/focusSearch.js diff --git a/packages/app-desktop/gui/NoteList/NoteList2.tsx b/packages/app-desktop/gui/NoteList/NoteList2.tsx index 333ebf186..e70cdef7f 100644 --- a/packages/app-desktop/gui/NoteList/NoteList2.tsx +++ b/packages/app-desktop/gui/NoteList/NoteList2.tsx @@ -1,21 +1,16 @@ import * as React from 'react'; import { useMemo, useState, useCallback, memo } from 'react'; import { AppState } from '../../app.reducer'; -// import { _ } from '@joplin/lib/locale'; import BaseModel, { ModelType } from '@joplin/lib/BaseModel'; const { connect } = require('react-redux'); -import { Props } from './types'; +import { ItemFlow, ItemRendererDepependency, Props } from './types'; import { itemIsReadOnlySync, ItemSlice } from '@joplin/lib/models/utils/readOnly'; import { FolderEntity, NoteEntity } from '@joplin/lib/services/database/types'; import ItemChange from '@joplin/lib/models/ItemChange'; import { Size } from '@joplin/utils/types'; -import { htmlentities } from '@joplin/utils/html'; import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect'; - -// enum ItemFlow { -// TopToBottom = 'topToBottom', -// LeftToRight = 'leftToRight', -// } +import defaultItemRenderer from './defaultItemRenderer'; +import * as Mustache from 'mustache'; interface RenderedNote { id: string; @@ -32,36 +27,52 @@ const useRenderedNotes = (notes: NoteEntity[], selectedNoteIds: string[], itemSi const [renderedNotes, setRenderedNotes] = useState(initialValue); + const prepareViewProps = async (dependencies: ItemRendererDepependency[], note: NoteEntity, itemSize: Size, selected: boolean) => { + const output: any = {}; + for (const dep of dependencies) { + + if (dep.startsWith('note.')) { + const splitted = dep.split('.'); + if (splitted.length !== 2) throw new Error(`Invalid dependency name: ${dep}`); + const propName = splitted.pop(); + if (!output.note) output.note = {}; + if (!(propName in note)) throw new Error(`Invalid dependency name: ${dep}`); + output.note[propName] = (note as any)[propName]; + } + + if (dep.startsWith('item.size.')) { + const splitted = dep.split('.'); + if (splitted.length !== 3) throw new Error(`Invalid dependency name: ${dep}`); + const propName = splitted.pop(); + if (!output.item) output.item = {}; + if (!output.item.size) output.item.size = {}; + if (!(propName in itemSize)) throw new Error(`Invalid dependency name: ${dep}`); + output.item.size[propName] = (itemSize as any)[propName]; + } + + if (dep === 'item.selected') { + if (!output.item) output.item = {}; + output.item.selected = selected; + } + } + + return output; + }; + useAsyncEffect(async (event) => { const newRenderedNotes: RenderedNote[] = []; - const renderCheckbox = (itemHeight: number) => { - return ` -
- -
- `; - }; - - const renderTitle = (noteId: string, title: string) => { - return ` - - ${htmlentities(title)} - `; - }; - for (const note of notes) { - const selected = selectedNoteIds.includes(note.id); + const view = await defaultItemRenderer.onRenderNote(await prepareViewProps( + defaultItemRenderer.dependencies, + note, + itemSize, + selectedNoteIds.includes(note.id) + )); newRenderedNotes.push({ id: note.id, - html: ` -
- ${renderCheckbox(itemSize.height)} - ${renderTitle(note.id, note.title)} -
- `, + html: Mustache.render(defaultItemRenderer.itemTemplate, view), }); } @@ -90,8 +101,10 @@ const NoteItem = memo((props: NoteItemProps) => { >; }); -const NoteListComponent = (props: Props) => { - // const itemDirection:ItemFlow = ItemFlow.TopToBottom; +const NoteList = (props: Props) => { + const itemFlow = ItemFlow.TopToBottom; + + if (itemFlow !== ItemFlow.TopToBottom) throw new Error('Not implemented'); const itemSize: Size = useMemo(() => { return { @@ -191,4 +204,4 @@ const mapStateToProps = (state: AppState) => { }; }; -export default connect(mapStateToProps)(NoteListComponent); +export default connect(mapStateToProps)(NoteList); diff --git a/packages/app-desktop/gui/NoteList/defaultItemRenderer.ts b/packages/app-desktop/gui/NoteList/defaultItemRenderer.ts new file mode 100644 index 000000000..25d479e64 --- /dev/null +++ b/packages/app-desktop/gui/NoteList/defaultItemRenderer.ts @@ -0,0 +1,51 @@ +import { ItemFlow, ItemRenderer } from './types'; + +interface Props { + note: { + id: string; + title: string; + is_todo: number; + todo_completed: number; + }; + item: { + size: { + height: number; + }; + selected: boolean; + }; +} + +const defaultItemRenderer: ItemRenderer = { + flow: ItemFlow.TopToBottom, + + itemSize: { + width: 0, + height: 34, + }, + + dependencies: [ + 'note.id', + 'note.title', + 'note.is_todo', + 'note.todo_completed', + 'item.size.height', + 'item.selected', + ], + + itemTemplate: ` +
+
+ +
+
+ {{note.title}} +
+ `, + + onRenderNote: async (props: Props) => { + return props; + }, +}; + +export default defaultItemRenderer; diff --git a/packages/app-desktop/gui/NoteList/types.ts b/packages/app-desktop/gui/NoteList/types.ts index 2f9758b58..0fab7acff 100644 --- a/packages/app-desktop/gui/NoteList/types.ts +++ b/packages/app-desktop/gui/NoteList/types.ts @@ -1,5 +1,6 @@ -import { FolderEntity, NoteEntity } from '@joplin/lib/services/database/types'; +import { FolderEntity, ItemRendererDatabaseDependency, NoteEntity } from '@joplin/lib/services/database/types'; import { PluginStates } from '@joplin/lib/services/plugins/reducer'; +import { Size } from '@joplin/utils/types'; export interface Props { themeId: any; @@ -27,3 +28,22 @@ export interface Props { focusedField: string; parentFolderIsReadOnly: boolean; } + +export enum ItemFlow { + TopToBottom = 'topToBottom', + LeftToRight = 'leftToRight', +} + +export type RenderNoteView = Record; + +export type OnRenderNoteHandler = (props: any)=> Promise; + +export type ItemRendererDepependency = ItemRendererDatabaseDependency | 'item.size.width' | 'item.size.height' | 'item.selected'; + +export interface ItemRenderer { + flow: ItemFlow; + itemSize: Size; + dependencies: ItemRendererDepependency[]; + itemTemplate: string; + onRenderNote: OnRenderNoteHandler; +} diff --git a/packages/app-desktop/package.json b/packages/app-desktop/package.json index 240756565..8ec7ff47e 100644 --- a/packages/app-desktop/package.json +++ b/packages/app-desktop/package.json @@ -139,6 +139,7 @@ "@joplin/lib": "~2.12", "@joplin/renderer": "~2.12", "@joplin/utils": "~2.12", + "@types/mustache": "4.2.2", "async-mutex": "0.4.0", "codemirror": "5.65.9", "color": "3.2.1", @@ -154,6 +155,7 @@ "mark.js": "8.11.1", "md5": "2.3.0", "moment": "2.29.4", + "mustache": "4.2.0", "node-fetch": "2.6.7", "node-notifier": "10.0.1", "node-rsa": "1.1.1", diff --git a/packages/lib/services/database/types.ts b/packages/lib/services/database/types.ts index 02443c588..d1bc56da1 100644 --- a/packages/lib/services/database/types.ts +++ b/packages/lib/services/database/types.ts @@ -46,6 +46,34 @@ export interface UserDataValue { export type UserData = Record>; +interface DatabaseTableColumn { + type: string; +} + +interface DatabaseTable { + [key: string]: DatabaseTableColumn; +} + +interface DatabaseTables { + [key: string]: DatabaseTable; +} + + + + + + + + + + + + + + + + + @@ -300,3 +328,233 @@ export interface VersionEntity { 'version'?: number; 'type_'?: number; } + + +export const databaseSchema: DatabaseTables = { + folders: { + created_time: { type: 'number' }, + encryption_applied: { type: 'number' }, + encryption_cipher_text: { type: 'string' }, + icon: { type: 'string' }, + id: { type: 'string' }, + is_shared: { type: 'number' }, + master_key_id: { type: 'string' }, + parent_id: { type: 'string' }, + share_id: { type: 'string' }, + title: { type: 'string' }, + updated_time: { type: 'number' }, + user_created_time: { type: 'number' }, + user_updated_time: { type: 'number' }, + type_: { type: 'number' }, + }, + tags: { + created_time: { type: 'number' }, + encryption_applied: { type: 'number' }, + encryption_cipher_text: { type: 'string' }, + id: { type: 'string' }, + is_shared: { type: 'number' }, + parent_id: { type: 'string' }, + title: { type: 'string' }, + updated_time: { type: 'number' }, + user_created_time: { type: 'number' }, + user_updated_time: { type: 'number' }, + type_: { type: 'number' }, + }, + note_tags: { + created_time: { type: 'number' }, + encryption_applied: { type: 'number' }, + encryption_cipher_text: { type: 'string' }, + id: { type: 'string' }, + is_shared: { type: 'number' }, + note_id: { type: 'string' }, + tag_id: { type: 'string' }, + updated_time: { type: 'number' }, + user_created_time: { type: 'number' }, + user_updated_time: { type: 'number' }, + type_: { type: 'number' }, + }, + table_fields: { + field_default: { type: 'string' }, + field_name: { type: 'string' }, + field_type: { type: 'number' }, + id: { type: 'number' }, + table_name: { type: 'string' }, + type_: { type: 'number' }, + }, + sync_items: { + force_sync: { type: 'number' }, + id: { type: 'number' }, + item_id: { type: 'string' }, + item_location: { type: 'number' }, + item_type: { type: 'number' }, + sync_disabled: { type: 'number' }, + sync_disabled_reason: { type: 'string' }, + sync_target: { type: 'number' }, + sync_time: { type: 'number' }, + type_: { type: 'number' }, + }, + version: { + table_fields_version: { type: 'number' }, + version: { type: 'number' }, + type_: { type: 'number' }, + }, + deleted_items: { + deleted_time: { type: 'number' }, + id: { type: 'number' }, + item_id: { type: 'string' }, + item_type: { type: 'number' }, + sync_target: { type: 'number' }, + type_: { type: 'number' }, + }, + settings: { + key: { type: 'string' }, + value: { type: 'string' }, + type_: { type: 'number' }, + }, + alarms: { + id: { type: 'number' }, + note_id: { type: 'string' }, + trigger_time: { type: 'number' }, + type_: { type: 'number' }, + }, + item_changes: { + before_change_item: { type: 'string' }, + created_time: { type: 'number' }, + id: { type: 'number' }, + item_id: { type: 'string' }, + item_type: { type: 'number' }, + source: { type: 'number' }, + type: { type: 'number' }, + type_: { type: 'number' }, + }, + note_resources: { + id: { type: 'number' }, + is_associated: { type: 'number' }, + last_seen_time: { type: 'number' }, + note_id: { type: 'string' }, + resource_id: { type: 'string' }, + type_: { type: 'number' }, + }, + resource_local_states: { + fetch_error: { type: 'string' }, + fetch_status: { type: 'number' }, + id: { type: 'number' }, + resource_id: { type: 'string' }, + type_: { type: 'number' }, + }, + resources: { + created_time: { type: 'number' }, + encryption_applied: { type: 'number' }, + encryption_blob_encrypted: { type: 'number' }, + encryption_cipher_text: { type: 'string' }, + file_extension: { type: 'string' }, + filename: { type: 'string' }, + id: { type: 'string' }, + is_shared: { type: 'number' }, + master_key_id: { type: 'string' }, + mime: { type: 'string' }, + share_id: { type: 'string' }, + size: { type: 'number' }, + title: { type: 'string' }, + updated_time: { type: 'number' }, + user_created_time: { type: 'number' }, + user_updated_time: { type: 'number' }, + type_: { type: 'number' }, + }, + revisions: { + body_diff: { type: 'string' }, + created_time: { type: 'number' }, + encryption_applied: { type: 'number' }, + encryption_cipher_text: { type: 'string' }, + id: { type: 'string' }, + item_id: { type: 'string' }, + item_type: { type: 'number' }, + item_updated_time: { type: 'number' }, + metadata_diff: { type: 'string' }, + parent_id: { type: 'string' }, + title_diff: { type: 'string' }, + updated_time: { type: 'number' }, + type_: { type: 'number' }, + }, + migrations: { + created_time: { type: 'number' }, + id: { type: 'number' }, + number: { type: 'number' }, + updated_time: { type: 'number' }, + type_: { type: 'number' }, + }, + resources_to_download: { + created_time: { type: 'number' }, + id: { type: 'number' }, + resource_id: { type: 'string' }, + updated_time: { type: 'number' }, + type_: { type: 'number' }, + }, + key_values: { + id: { type: 'number' }, + key: { type: 'string' }, + type: { type: 'number' }, + updated_time: { type: 'number' }, + value: { type: 'string' }, + type_: { type: 'number' }, + }, + notes: { + altitude: { type: 'number' }, + application_data: { type: 'string' }, + author: { type: 'string' }, + body: { type: 'string' }, + conflict_original_id: { type: 'string' }, + created_time: { type: 'number' }, + encryption_applied: { type: 'number' }, + encryption_cipher_text: { type: 'string' }, + id: { type: 'string' }, + is_conflict: { type: 'number' }, + is_shared: { type: 'number' }, + is_todo: { type: 'number' }, + latitude: { type: 'number' }, + longitude: { type: 'number' }, + markup_language: { type: 'number' }, + master_key_id: { type: 'string' }, + order: { type: 'number' }, + parent_id: { type: 'string' }, + share_id: { type: 'string' }, + source: { type: 'string' }, + source_application: { type: 'string' }, + source_url: { type: 'string' }, + title: { type: 'string' }, + todo_completed: { type: 'number' }, + todo_due: { type: 'number' }, + updated_time: { type: 'number' }, + user_created_time: { type: 'number' }, + user_data: { type: 'string' }, + user_updated_time: { type: 'number' }, + type_: { type: 'number' }, + }, + notes_normalized: { + altitude: { type: 'number' }, + body: { type: 'string' }, + id: { type: 'string' }, + is_todo: { type: 'number' }, + latitude: { type: 'number' }, + longitude: { type: 'number' }, + parent_id: { type: 'string' }, + source_url: { type: 'string' }, + title: { type: 'string' }, + todo_completed: { type: 'number' }, + todo_due: { type: 'number' }, + user_created_time: { type: 'number' }, + user_updated_time: { type: 'number' }, + type_: { type: 'number' }, + }, + tags_with_note_count: { + created_time: { type: 'number' }, + id: { type: 'string' }, + note_count: { type: 'any' }, + title: { type: 'string' }, + todo_completed_count: { type: 'any' }, + updated_time: { type: 'number' }, + type_: { type: 'number' }, + }, +}; + +export type ItemRendererDatabaseDependency = 'folder.created_time' | 'folder.encryption_applied' | 'folder.encryption_cipher_text' | 'folder.icon' | 'folder.id' | 'folder.is_shared' | 'folder.master_key_id' | 'folder.parent_id' | 'folder.share_id' | 'folder.title' | 'folder.updated_time' | 'folder.user_created_time' | 'folder.user_updated_time' | 'folder.type_' | 'note.altitude' | 'note.application_data' | 'note.author' | 'note.body' | 'note.conflict_original_id' | 'note.created_time' | 'note.encryption_applied' | 'note.encryption_cipher_text' | 'note.id' | 'note.is_conflict' | 'note.is_shared' | 'note.is_todo' | 'note.latitude' | 'note.longitude' | 'note.markup_language' | 'note.master_key_id' | 'note.order' | 'note.parent_id' | 'note.share_id' | 'note.source' | 'note.source_application' | 'note.source_url' | 'note.title' | 'note.todo_completed' | 'note.todo_due' | 'note.updated_time' | 'note.user_created_time' | 'note.user_data' | 'note.user_updated_time' | 'note.type_'; \ No newline at end of file diff --git a/packages/tools/generate-database-types.ts b/packages/tools/generate-database-types.ts index ef3afdc80..461d710ef 100644 --- a/packages/tools/generate-database-types.ts +++ b/packages/tools/generate-database-types.ts @@ -4,6 +4,37 @@ import { rootDir } from './tool-utils'; const sqlts = require('@rmp135/sql-ts').default; const fs = require('fs-extra'); +function createRuntimeObject(table: any) { + const colStrings = []; + for (const col of table.columns) { + const name = col.propertyName; + const type = col.propertyType; + colStrings.push(`\t\t${name}: { type: '${type}' },`); + } + + return `\t${table.name}: {\n${colStrings.join('\n')}\n\t},`; +} + +const stringToSingular = (word: string) => { + if (word.endsWith('s')) return word.substring(0, word.length - 1); + return word; +}; + +const generateItemRenderDependencyType = (tables: any[]) => { + const output: string[] = []; + + for (const table of tables) { + if (!['notes', 'folders'].includes(table.name)) continue; + + for (const col of table.columns) { + const name = col.propertyName; + output.push(`'${stringToSingular(table.name)}.${name}'`); + } + } + + return output.join(' | '); +}; + async function main() { // Run the CLI app once so as to generate the database file process.chdir(`${rootDir}/packages/app-cli`); @@ -54,6 +85,11 @@ async function main() { return table; }); + const tableStrings = []; + for (const table of definitions.tables) { + tableStrings.push(createRuntimeObject(table)); + } + const tsString = sqlts.fromObject(definitions, sqlTsConfig) .replace(/": /g, '"?: '); const header = `// AUTO-GENERATED BY ${__filename.substr(rootDir.length + 1)}`; @@ -65,7 +101,11 @@ async function main() { const splitted = existingContent.split('// AUTO-GENERATED BY'); const staticContent = splitted[0]; - await fs.writeFile(targetFile, `${staticContent}\n\n${header}\n\n${tsString}`, 'utf8'); + const runtimeContent = `export const databaseSchema: DatabaseTables = {\n${tableStrings.join('\n')}\n};`; + + const itemRendererDependency = `export type ItemRendererDatabaseDependency = ${generateItemRenderDependencyType(definitions.tables)};`; + + await fs.writeFile(targetFile, `${staticContent}\n\n${header}\n\n${tsString}\n\n${runtimeContent}\n\n${itemRendererDependency}`, 'utf8'); } main().catch((error) => { diff --git a/yarn.lock b/yarn.lock index 11b710f40..0b3eb8cec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4435,6 +4435,7 @@ __metadata: "@joplin/utils": ~2.12 "@testing-library/react-hooks": 8.0.1 "@types/jest": 29.5.3 + "@types/mustache": 4.2.2 "@types/node": 18.16.18 "@types/react": 18.0.24 "@types/react-redux": 7.1.25 @@ -4461,6 +4462,7 @@ __metadata: mark.js: 8.11.1 md5: 2.3.0 moment: 2.29.4 + mustache: 4.2.0 nan: 2.17.0 node-fetch: 2.6.7 node-notifier: 10.0.1