Skip to content

Commit 3c08b11

Browse files
committed
Delete tags and search if not possible
1 parent 6c06e7a commit 3c08b11

File tree

7 files changed

+163
-58
lines changed

7 files changed

+163
-58
lines changed

src/common/components/Tag/Tag.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ export const Tag = (props: TagProps) => {
1010
const { name, hue, icon } = props;
1111
return (
1212
<span
13-
className="font-semibold tag w-fit text-sm"
13+
className="tag w-fit text-sm"
1414
data-icon={icon}
15+
data-type="tags"
1516
style={
1617
{
1718
"--tag-color": hue,

src/common/components/TextInput/TextInput.tsx

Lines changed: 62 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import "./TextInput.css";
44
import { parseSpark } from "../../../scripts/utils/stringUtils";
55
import { isUserSelectingTag } from "./TagList/TagList";
66
import { isUserSelectingExtension } from "./SparkExtensionList/SparkExtensionList";
7+
import { useEffect } from "react";
78

89
export type TextInputProps = {
910
onSubmit?: (plainText: string, html: string) => void;
@@ -16,11 +17,12 @@ export type TextInputProps = {
1617
placeholder?: string;
1718
content?: string;
1819
onEscape?: () => void;
20+
globalAccessorId?: string;
1921
};
2022

2123
const styleMap = {
2224
spark: "p-4 min-h-full block w-full bg-white border border-blue-300 rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400 dark:placeholder-neutral-500 dark:focus:ring-neutral-600",
23-
search: "px-4 py-2 block w-full bg-transparent hover:bg-white focus:bg-white border-b border-b-stone-200 rounded",
25+
search: "px-4 py-2 block w-full bg-transparent text-sm hover:bg-white focus:bg-white border-b border-b-stone-200 rounded",
2426
invisible:
2527
"p-1 w-full bg-transparent rounded-sm text-stone-500 focus:text-stone-700 text-sm max-w-72",
2628
};
@@ -37,53 +39,69 @@ export const TextInput = (props: TextInputProps) => {
3739
enableTags,
3840
enableExtension,
3941
onEscape,
42+
globalAccessorId,
4043
} = props;
41-
const editor = useEditor({
42-
content,
43-
extensions: getExtensions({
44-
parentWindow: parentWindow ?? window,
45-
allowAddingTags,
46-
placeholder,
47-
enableTags,
48-
enableExtension,
49-
}),
50-
editorProps: {
51-
attributes: {
52-
"aria-label": "Add a spark",
53-
role: "textbox",
54-
class: `${styleMap[style]}`,
55-
},
56-
handleKeyDown: (_view, event) => {
57-
if (!onSubmit) {
58-
return false;
59-
}
60-
if (isUserSelectingTag || isUserSelectingExtension) {
61-
// user currently has the selection open and might have pressed enter to select an item
62-
return false;
63-
}
64-
if (event.key !== "Enter" || event.shiftKey) {
65-
if (event.key === "Escape") {
66-
onEscape?.();
44+
const editor = useEditor(
45+
{
46+
content,
47+
extensions: getExtensions({
48+
parentWindow: parentWindow ?? window,
49+
allowAddingTags,
50+
placeholder,
51+
enableTags,
52+
enableExtension,
53+
}),
54+
editorProps: {
55+
attributes: {
56+
"aria-label": "Add a spark",
57+
role: "textbox",
58+
class: `${styleMap[style]}`,
59+
},
60+
handleKeyDown: (_view, event) => {
61+
if (!onSubmit) {
62+
return false;
63+
}
64+
if (isUserSelectingTag || isUserSelectingExtension) {
65+
// user currently has the selection open and might have pressed enter to select an item
66+
return false;
67+
}
68+
if (event.key !== "Enter" || event.shiftKey) {
69+
if (event.key === "Escape") {
70+
onEscape?.();
71+
}
72+
return false;
73+
}
74+
const html = editor?.getHTML().trim() ?? "";
75+
const plainText = editor?.getText().trim() ?? "";
76+
if (html === "") {
77+
return false;
6778
}
68-
return false;
69-
}
70-
const html = editor?.getHTML().trim() ?? "";
71-
const plainText = editor?.getText().trim() ?? "";
72-
if (html === "") {
73-
return false;
74-
}
75-
onSubmit(plainText, html);
76-
const { prefixTagsHtml } = parseSpark(plainText, html);
77-
editor?.commands.setContent(prefixTagsHtml, false, {
78-
preserveWhitespace: true,
79-
});
80-
return true;
79+
onSubmit(plainText, html);
80+
const { prefixTagsHtml } = parseSpark(plainText, html);
81+
editor?.commands.setContent(prefixTagsHtml, false, {
82+
preserveWhitespace: true,
83+
});
84+
return true;
85+
},
86+
},
87+
onUpdate: ({ editor }) => {
88+
onChange?.(editor.getHTML());
8189
},
8290
},
83-
onUpdate: ({ editor }) => {
84-
onChange?.(editor.getHTML());
85-
},
86-
});
91+
[content],
92+
);
93+
94+
useEffect(() => {
95+
if (!globalAccessorId) {
96+
return;
97+
}
98+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
99+
const win = window as any;
100+
if (!win.editor) {
101+
win.editor = {};
102+
}
103+
win.editor[globalAccessorId] = editor;
104+
}, [editor, globalAccessorId]);
87105

88106
return (
89107
<EditorContent

src/container/SearchInput/SearchInput.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { TextInput } from "../../common/components/TextInput/TextInput";
2-
import { updateQueryDebounced } from "../../scripts/store/queryStore";
3-
import { extractTags } from "../../scripts/utils/stringUtils";
2+
import {
3+
updateQueryDebounced,
4+
useQueryStore,
5+
} from "../../scripts/store/queryStore";
6+
7+
export const SearchInputEditorAccessorId = "search";
48

59
export const SearchInput = () => {
610
const handleChange = (htmlString: string) => {
@@ -9,6 +13,7 @@ export const SearchInput = () => {
913
return (
1014
<div>
1115
<TextInput
16+
globalAccessorId={SearchInputEditorAccessorId}
1217
allowAddingTags={false}
1318
onChange={handleChange}
1419
style="search"

src/container/SparkList/SparkList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ type DateSection = {
2424
type Section = SparkSection | DateSection;
2525

2626
export const SparkList = () => {
27-
const queryTags = useQueryStore((state) => state.context.query);
27+
const queryTags = useQueryStore((state) => state.context.tags);
2828
const sparksWithTags = useLiveQuery(
2929
() => sparkService.find(queryTags),
3030
[queryTags],

src/container/TagEditor/TagConfig/TagConfig.tsx

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ import {
1919
SelectValue,
2020
} from "../../../common/components/shadcn/select";
2121
import "./TagConfig.css";
22+
import { IconButton } from "../../../common/components/IconButton/IconButton";
23+
import { TrashIcon } from "../../../assets/icons/TrashIcon";
24+
import { sparkService } from "../../../scripts/db/SparkService";
25+
import { useToast } from "../../../common/hooks/use-toast";
26+
import { ToastAction } from "../../../common/components/shadcn/toast";
27+
import { SearchInputEditorAccessorId } from "../../SearchInput/SearchInput";
28+
import type { Editor } from "@tiptap/react";
2229

2330
type Props = {
2431
tag: Tag;
@@ -43,14 +50,64 @@ export const TagConfig = (props: Props) => {
4350
const { tag } = props;
4451
const [hue, setHue] = useState(tag.hue);
4552
const [icon, setIcon] = useState<TagIcon>(tag.icon ?? "hash");
53+
const { toast } = useToast();
4654

4755
const handleHueChange = (value: number) => {
4856
setHue(value);
4957
updateHueDebounced(tag.name, value);
5058
};
5159

60+
const searchTag = () => {
61+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
62+
const searchEditor: Editor | undefined = (window as any).editor?.[
63+
SearchInputEditorAccessorId
64+
];
65+
66+
if (!searchEditor) {
67+
return;
68+
}
69+
70+
searchEditor.commands.setContent(
71+
`
72+
<span
73+
data-type="tags"
74+
data-icon="${tag.icon}"
75+
data-id="${tag.name}"
76+
style="--tag-color: ${tag.hue}"
77+
class="tag"
78+
contenteditable="false"
79+
>${tag.name}</span>
80+
`,
81+
true,
82+
{ preserveWhitespace: false },
83+
);
84+
};
85+
86+
const handleDelete = async () => {
87+
const sparksWithTag = await sparkService.find([tag.name]);
88+
89+
if (sparksWithTag.length > 0) {
90+
toast({
91+
title: `Could not delete tag "${tag.name}"`,
92+
description: `The tag is still being used by ${sparksWithTag.length} sparks. Edit these sparks and remove the tag on each of them to delete this tag.`,
93+
variant: "destructive",
94+
action: (
95+
<ToastAction
96+
altText="View sparks using this tag"
97+
onClick={searchTag}
98+
>
99+
View
100+
</ToastAction>
101+
),
102+
});
103+
return;
104+
}
105+
106+
await tagService.deleteTag(tag.name);
107+
};
108+
52109
return (
53-
<div className="grid grid-cols-subgrid col-span-4 border-b border-stone-200 py-2 w-full items-center gap-4">
110+
<div className="grid grid-cols-subgrid col-span-5 border-b border-stone-200 py-2 w-full items-center gap-4">
54111
<div className="justify-self-center">
55112
<TagElement
56113
name={tag.name}
@@ -124,6 +181,14 @@ export const TagConfig = (props: Props) => {
124181
</PopoverContent>
125182
</Popover>
126183
</div>
184+
<div className="flex flex-row justify-center">
185+
<IconButton
186+
relevancy="secondary"
187+
onClick={handleDelete}
188+
>
189+
<TrashIcon />
190+
</IconButton>
191+
</div>
127192
</div>
128193
);
129194
};

src/scripts/db/TagService.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ export class TagService {
104104
this.tagMap.set(newTag.name, newTag);
105105
}
106106

107+
public async deleteTag(name: string) {
108+
await this.db.tags.delete(name);
109+
}
110+
107111
public async CAREFUL_deleteAllData() {
108112
await this.db.tags.clear();
109113
}

src/scripts/store/queryStore.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,36 +5,48 @@ import { useSelector } from "@xstate/store/react";
55

66
export const queryStore = createStore({
77
// Initial context
8-
context: { query: [] } as { query: string[] },
8+
context: { tags: [], queryHtml: "" } as {
9+
tags: string[];
10+
queryHtml: string;
11+
},
912
// Transitions
1013
on: {
11-
update: {
12-
query: (_context, event: { value: string[] }) => event.value,
14+
updateTags: {
15+
tags: (_context, event: { html: string }) => {
16+
return extractTags(event.html);
17+
},
18+
},
19+
updateQueryHtml: {
20+
queryHtml: (_context, event: { html: string }) => {
21+
return event.html;
22+
},
1323
},
1424
clear: {
15-
query: () => [],
25+
tags: () => [],
26+
queryHtml: () => "",
1627
},
1728
},
1829
});
1930

2031
const extractTagsAndUpdateDebounced = debounce((queryString: string) => {
21-
const newQuery = extractTags(queryString);
22-
console.log("extract", queryString, newQuery);
2332
queryStore.send({
24-
type: "update",
25-
value: newQuery,
33+
type: "updateTags",
34+
html: queryString,
2635
});
2736
}, 300);
2837

2938
export const updateQueryDebounced = (queryString?: string) => {
30-
console.log("u", queryString);
3139
if (!queryString || queryString.trim() === "") {
3240
queryStore.send({
3341
type: "clear",
3442
});
3543
return;
3644
}
3745

46+
queryStore.send({
47+
type: "updateQueryHtml",
48+
html: queryString,
49+
});
3850
extractTagsAndUpdateDebounced(queryString);
3951
};
4052

0 commit comments

Comments
 (0)