Skip to content

Commit

Permalink
add document tagging
Browse files Browse the repository at this point in the history
adds a basic interface for tagging documents, searching by tags, and quick-links for available tags / search to the sidebar
  • Loading branch information
cloverich committed Jun 15, 2024
1 parent 2b9037c commit 061619d
Show file tree
Hide file tree
Showing 24 changed files with 589 additions and 264 deletions.
2 changes: 2 additions & 0 deletions scripts/dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ async function watchRenderer() {
bundle: true,
platform: "browser",
plugins: [startElectronPlugin("renderer")],
sourcemap: true,
});

await ctxRenderer.watch();
Expand All @@ -109,6 +110,7 @@ async function watchPreload() {
platform: "node",
external: ["knex", "electron", "electron-store", "better-sqlite3"],
plugins: [startElectronPlugin("preload")],
sourcemap: true,
});

await ctxPreload.watch();
Expand Down
16 changes: 11 additions & 5 deletions src/electron/migrations/20211005142122.sql
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,17 @@ CREATE TABLE IF NOT EXISTS "documents" (
FOREIGN KEY ("journalId") REFERENCES "journals" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);

-- CreateIndex
CREATE UNIQUE INDEX IF NOT EXISTS "journals_name_uniq" ON "journals"("name");

-- CreateIndex
CREATE INDEX IF NOT EXISTS "documents_title_idx" ON "documents"("title");
-- CreateTable
CREATE TABLE IF NOT EXISTS "document_tags" (
"documentId" TEXT NOT NULL,
"tag" TEXT NOT NULL,
FOREIGN KEY ("documentId") REFERENCES "documents" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY ("documentId", "tag")
);

-- CreateIndex

CREATE UNIQUE INDEX IF NOT EXISTS "journals_name_uniq" ON "journals"("name");
CREATE INDEX IF NOT EXISTS "documents_title_idx" ON "documents"("title");
CREATE INDEX IF NOT EXISTS "documents_createdat_idx" ON "documents"("createdAt");
CREATE INDEX IF NOT EXISTS "tags_name_idx" ON "document_tags"("tag");
39 changes: 39 additions & 0 deletions src/hooks/useTags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from "react";
import useClient from "./useClient";

/**
* Hook for loading tags.
*/
export function useTags() {
const [loading, setLoading] = React.useState(true);
const [tags, setTags] = React.useState<string[]>([]);
const [error, setError] = React.useState(null);
const client = useClient();

React.useEffect(() => {
let isEffectMounted = true;
setLoading(true);

async function load() {
try {
const tags = await client.tags.all();
if (!isEffectMounted) return;

setTags(tags);
setLoading(false);
} catch (err: any) {
if (!isEffectMounted) return;

setError(err);
setLoading(false);
}
}

load();
return () => {
isEffectMounted = false;
};
}, []);

return { loading, tags, error };
}
14 changes: 14 additions & 0 deletions src/markdown/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ import { Node as SNode } from "slate";

export * from "ts-mdast";

// I usually forget how unified works, so just leaving some notes for reference
// https://github.com/orgs/unifiedjs/discussions/113
// | ........................ process ........................... |
// | .......... parse ... | ... run ... | ... stringify ..........|
//
// +--------+ +----------+
// Input ->- | Parser | ->- Syntax Tree ->- | Compiler | ->- Output
// +--------+ | +----------+
// X
// |
// +--------------+
// | Transformers |
// +--------------+

const stringifier = unified().use(remarkStringify);
const parser = unified().use(remarkParse).use(remarkGfm);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { mdastToSlate } from "../transformers/mdast-to-slate";

export default function plugin() {
export default function remarkToSlatePlugin() {
// @ts-ignore
this.Compiler = function (node: any) {
return mdastToSlate(node);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import { slateToMdast } from "../transformers/slate-to-mdast";

type Settings = {};

const plugin: Plugin<[Settings?]> = function (settings?: Settings) {
const slateToRemarkPlugin: Plugin<[Settings?]> = function (
settings?: Settings,
) {
// @ts-ignore
return function (node: any) {
return slateToMdast(node);
};
};
export default plugin;

export default slateToRemarkPlugin;
164 changes: 103 additions & 61 deletions src/preload/client/documents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface GetDocumentResponse {
title?: string;
content: string;
journalId: string;
tags: string[];
}

/**
Expand Down Expand Up @@ -36,6 +37,11 @@ export interface SearchRequest {
*/
texts?: string[];

/**
* Search document #tags. ex: ['mytag', 'othertag']
*/
tags?: string[];

limit?: number;

nodeMatch?: {
Expand Down Expand Up @@ -64,7 +70,6 @@ export interface SearchItem {
journalId: string;
}

// Now straight up copying from the API layer
export interface SaveRawRequest {
journalName: string;
date: string;
Expand All @@ -84,6 +89,7 @@ export interface SaveRequest {
journalId: string;
content: string;
title?: string;
tags: string[];

// these included for override, originally,
// to support the import process
Expand All @@ -99,32 +105,38 @@ export class DocumentsClient {
private knex: Knex,
) {}

findById = ({
documentId,
}: {
documentId: string;
}): Promise<GetDocumentResponse> => {
const doc = this.db
.prepare("select * from documents where id = :id")
.get({ id: documentId });

return doc;
findById = ({ id }: { id: string }): Promise<GetDocumentResponse> => {
const document = this.db
.prepare(`SELECT * FROM documents WHERE id = :id`)
.get({ id });
const documentTags = this.db
.prepare(`SELECT tag FROM document_tags WHERE documentId = :documentId`)
.all({ documentId: id })
.map((row) => row.tag);
return {
...document,
tags: documentTags,
};
};

del = async (id: string) => {
this.db.prepare("delete from documents where id = :id").run({ id });
};

search = async (q?: SearchRequest): Promise<SearchResponse> => {
// todo: consider using raw and getting arrays of values rather than
// objects for each row, for performance
let query = this.knex("documents");

// filter by journal
if (q?.journals?.length) {
query = query.whereIn("journalId", q.journals);
}

if (q?.tags?.length) {
query = query
.join("document_tags", "documents.id", "document_tags.documentId")
.whereIn("document_tags.tag", q.tags);
}

// filter by title
if (q?.titles?.length) {
for (const title of q.titles) {
Expand Down Expand Up @@ -166,65 +178,95 @@ export class DocumentsClient {
return { data: [] };
};

save = ({
id,
/**
* Create or update a document and its tags
*
* todo: test; for tags: test prefix is removed, spaces are _, lowercased, max length
* todo: test description max length
*
* @returns - The document as it exists after the save
*/
save = (args: SaveRequest): Promise<GetDocumentResponse> => {
// de-dupe tags -- should happen before getting here.
args.tags = Array.from(new Set(args.tags));

const id = this.db.transaction(() => {
if (args.id) {
this.updateDocument(args);
return args.id;
} else {
return this.createDocument(args);
}
})();

return this.findById({ id });
};

private createDocument = ({
createdAt,
updatedAt,
journalId,
content,
title,
}: SaveRequest): Promise<GetDocumentResponse> => {
if (id) {
tags,
}: SaveRequest): string => {
const id = uuidv7();
this.db
.prepare(
`INSERT INTO documents (id, journalId, content, title, createdAt, updatedAt) VALUES (:id, :journalId, :content, :title, :createdAt, :updatedAt)`,
)
.run({
id,
journalId,
content,
title,
// allow passing createdAt to support backfilling prior notes
createdAt: createdAt || new Date().toISOString(),
updatedAt: new Date().toISOString(),
});

if (tags.length > 0) {
this.db
.prepare(
`
update documents set
journalId=:journalId,
content=:content,
title=:title,
updatedAt=:updatedAt,
createdAt=:createdAt
where
id=:id
`,
)
.run({
id,
content,
title,
journalId,
updatedAt: new Date().toISOString(),
createdAt,
});

return this.db
.prepare(
`select * from documents where id = :id order by "createdAt" desc`,
`INSERT INTO document_tags (documentId, tag) VALUES ${tags.map((tag) => `(:documentId, '${tag}')`).join(", ")}`,
)
.get({ id });
} else {
const id = uuidv7();
.run({ documentId: id });
}

return id;
};

private updateDocument = ({
id,
createdAt,
journalId,
content,
title,
tags,
}: SaveRequest): void => {
this.db
.prepare(
`UPDATE documents SET journalId=:journalId, content=:content, title=:title, updatedAt=:updatedAt, createdAt=:createdAt WHERE id=:id`,
)
.run({
id,
content,
title,
journalId,
updatedAt: new Date().toISOString(),
createdAt,
});

this.db
.prepare(`DELETE FROM document_tags WHERE documentId = :documentId`)
.run({ documentId: id });

if (tags.length > 0) {
this.db
.prepare(
`
insert into documents (id, journalId, content, title, createdAt, updatedAt)
values (:id, :journalId, :content, :title, :createdAt, :updatedAt)
`,
)
.run({
id,
journalId,
content,
title,
createdAt: createdAt || new Date().toISOString(),
updatedAt: updatedAt || new Date().toISOString(),
});

return this.db
.prepare(
`select * from documents where id = :id order by "createdAt" desc`,
`INSERT INTO document_tags (documentId, tag) VALUES ${tags.map((tag) => `(:documentId, '${tag}')`).join(", ")}`,
)
.get({ id });
.run({ documentId: id });
}
};

Expand Down
2 changes: 2 additions & 0 deletions src/preload/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { JournalsClient } from "./journals";
import { DocumentsClient } from "./documents";
import { TagsClient } from "./tags";
import { PreferencesClient } from "./preferences";
import { FilesClient } from "./files";
import { IClient } from "./types";
Expand Down Expand Up @@ -33,6 +34,7 @@ export function create(): IClient {
client = {
journals: new JournalsClient(db),
documents: new DocumentsClient(db, knex),
tags: new TagsClient(db, knex),
preferences: new PreferencesClient(settings),
files: new FilesClient(settings),
};
Expand Down
18 changes: 18 additions & 0 deletions src/preload/client/tags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Database } from "better-sqlite3";
import { Knex } from "knex";

export type ITagsClient = TagsClient;

export class TagsClient {
constructor(
private db: Database,
private knex: Knex,
) {}

all = async (): Promise<string[]> => {
return this.db
.prepare(`SELECT DISTINCT tag FROM document_tags`)
.all()
.map((row) => row.tag);
};
}
Loading

0 comments on commit 061619d

Please sign in to comment.