From 5bd87d10a14bc44e5224c9e62fc639172acbc924 Mon Sep 17 00:00:00 2001 From: braxtonhall Date: Thu, 12 Jan 2023 12:21:15 +0100 Subject: [PATCH 01/15] Create the tag insertion dialog outline --- src/ts/common/ui/dialog.ts | 57 ++++++++++++++++++++++++++++ src/ts/common/ui/modal.ts | 17 +++++---- src/ts/content/dialogs/insertTags.ts | 44 +++++++++++++++++++++ src/ts/content/index.ts | 15 ++++++++ 4 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 src/ts/common/ui/dialog.ts create mode 100644 src/ts/content/dialogs/insertTags.ts diff --git a/src/ts/common/ui/dialog.ts b/src/ts/common/ui/dialog.ts new file mode 100644 index 00000000..a98b599c --- /dev/null +++ b/src/ts/common/ui/dialog.ts @@ -0,0 +1,57 @@ +import {createModal, ModalButton, ModalInput} from "./modal"; +import {UIColour} from "./colour"; + +interface DialogOptions { + text: string; + subText?: string[]; + elements: E; + colour: UIColour; +} + +type DialogButton = Omit & {id: string}; +type DialogInput = Omit & {id: string}; +type DialogElement = DialogButton | DialogInput; + +type SelectionResponse = Selection extends DialogInput + ? {kind: DialogResponseKind.Input; id: string; value: string} + : Selection extends DialogButton + ? {kind: DialogResponseKind.Button; id: string} + : {kind: DialogResponseKind}; + +type DefaultResponse = {kind: DialogResponseKind.Cancelled}; + +enum DialogResponseKind { + Cancelled = "cancelled", + Button = "button", + Input = "input", +} + +const getButtonHandler = (resolve: (response) => void, id: string) => ({ + onClick: async () => resolve({kind: DialogResponseKind.Button, id}), +}); + +const getInputHandler = (resolve: (response) => void, id: string) => ({ + onSelect: async (userText: string) => resolve({kind: DialogResponseKind.Input, id, value: userText}), +}); + +const createModalElement = + (resolve: (response) => void) => + (element: DialogElement): ModalInput | ModalButton => { + return { + ...element, + ...(element.kind === "button" && getButtonHandler(resolve, element.id)), + ...(element.kind === "input" && getInputHandler(resolve, element.id)), + }; + }; + +const dialog = (options: DialogOptions) => + new Promise | DefaultResponse>((resolve) => + createModal({ + ...options, + elements: options.elements.map(createModalElement(resolve)), + onCancel: async () => resolve({kind: DialogResponseKind.Cancelled}), + }) + ); + +export {DialogButton, DialogInput}; +export {dialog}; diff --git a/src/ts/common/ui/modal.ts b/src/ts/common/ui/modal.ts index 394cf42c..4ff265b6 100644 --- a/src/ts/common/ui/modal.ts +++ b/src/ts/common/ui/modal.ts @@ -100,15 +100,17 @@ const addOnClick = (element: HTMLElement, exit: () => void, onClick?: () => Prom const dismissModals = (): void => activeOverlays.forEach((modal) => modal.dispatchEvent(new Event("click"))); -const createModal = ({text, subText, elements, onCancel, colour}: ModalOptions): void => { - const exit = () => { - activeOverlays.delete(overlay); - overlay.remove(); - }; +const createDismiss = (overlay) => () => { + activeOverlays.delete(overlay); + overlay.remove(); +}; +const createModal = ({text, subText, elements, onCancel, colour}: ModalOptions): void => { const overlay = createOverlay(); + const dismiss = createDismiss(overlay); + overlay.classList.add("modal"); - addOnClick(overlay, exit, onCancel); + addOnClick(overlay, dismiss, onCancel); activeOverlays.add(overlay); const modal = createWithClass("div", `${MODAL_CLASS_NAME} ${colour}`); @@ -117,7 +119,7 @@ const createModal = ({text, subText, elements, onCancel, colour}: ModalOptions): const textContainer = createTextContainer(text, subText); const elementContainer = createWithClass("div", MODAL_ELEMENT_CONTAINER_CLASS_NAME); - const modalElements = elements.map(createModalElement(exit)); + const modalElements = elements.map(createModalElement(dismiss)); elementContainer.append(...modalElements); modal.append(textContainer, elementContainer); @@ -125,4 +127,5 @@ const createModal = ({text, subText, elements, onCancel, colour}: ModalOptions): document.body.appendChild(overlay); }; +export type {ModalButton, ModalInput}; export {createModal, dismissModals}; diff --git a/src/ts/content/dialogs/insertTags.ts b/src/ts/content/dialogs/insertTags.ts new file mode 100644 index 00000000..7f0d4022 --- /dev/null +++ b/src/ts/content/dialogs/insertTags.ts @@ -0,0 +1,44 @@ +import {createModal} from "../../common/ui/modal"; +import {UIColour} from "../../common/ui/colour"; + +const insertTags = async (tags: string[]): Promise => { + if (tags.length === 0) { + return Promise.resolve(true); + } else { + return new Promise((resolve) => + createModal({ + text: "Which tag would you like to insert?", + elements: tags.map((tag) => ({ + kind: "button", + colour: UIColour.BLUE, + text: tag, + onClick: async () => { + resolve(insertTag(tag, tags).then(insertTags)); + }, + })), + colour: UIColour.BLUE, + onCancel: async () => resolve(false), + }) + ); + } +}; + +const insertTag = (tag: string, remainingTags: string[]): Promise => + new Promise((resolve) => + createModal({ + text: "Where would you like to insert this tag?", + elements: [ + { + kind: "button", + text: "Here", + colour: UIColour.GREEN, + onClick: async () => resolve(remainingTags.filter((otherTag) => otherTag !== tag)), + }, + {kind: "button", text: "Not Here", colour: UIColour.RED, onClick: async () => resolve(remainingTags)}, + ], + colour: UIColour.BLUE, + onCancel: async () => resolve(remainingTags), + }) + ); + +export {insertTags}; diff --git a/src/ts/content/index.ts b/src/ts/content/index.ts index 4d3ea459..3ec7074e 100644 --- a/src/ts/content/index.ts +++ b/src/ts/content/index.ts @@ -10,3 +10,18 @@ import "./extensions/resize"; import "./extensions/tags"; import "./extensions/sort"; import "./extensions/enforceTagIndexAccess"; + +// import {insertTags} from "./dialogs/insertTags"; + +// insertTags([ +// "Tag 0", +// "Tag 1", +// "Tag 2", +// "Tag 3", +// "Tag 4", +// "Tag 5", +// "Tag 6", +// "Tag 7", +// "Tag 8", +// "Tag 9", +// ]).then(() => alert("done")); From 9a487ae99db7d606296da2090aee0c53ce29232f Mon Sep 17 00:00:00 2001 From: braxtonhall Date: Thu, 12 Jan 2023 22:04:20 +0100 Subject: [PATCH 02/15] #212 Hook up the tag insertion dialog to tag validation --- src/ts/content/dialogs/insertTags.ts | 23 +++++++++++-------- .../content/extensions/util/tagValidation.ts | 10 +++++++- src/ts/content/index.ts | 15 ------------ 3 files changed, 22 insertions(+), 26 deletions(-) diff --git a/src/ts/content/dialogs/insertTags.ts b/src/ts/content/dialogs/insertTags.ts index 7f0d4022..1e24bb89 100644 --- a/src/ts/content/dialogs/insertTags.ts +++ b/src/ts/content/dialogs/insertTags.ts @@ -1,21 +1,24 @@ import {createModal} from "../../common/ui/modal"; import {UIColour} from "../../common/ui/colour"; -const insertTags = async (tags: string[]): Promise => { +const toInsertionButton = (remainingTags: string[], resolve: (value) => void) => (tag: string) => ({ + kind: "button" as const, + colour: UIColour.BLUE, + text: tag, + onClick: async () => resolve(insertTag(tag, remainingTags).then(insertTagsDialogs)), +}); + +const insertTagsDialogs = async (tags: string[]): Promise => { if (tags.length === 0) { return Promise.resolve(true); } else { return new Promise((resolve) => createModal({ text: "Which tag would you like to insert?", - elements: tags.map((tag) => ({ - kind: "button", - colour: UIColour.BLUE, - text: tag, - onClick: async () => { - resolve(insertTag(tag, tags).then(insertTags)); - }, - })), + elements: [ + ...tags.map(toInsertionButton(tags, resolve)), + {kind: "button", colour: UIColour.RED, text: "Back", onClick: async () => resolve(false)}, + ], colour: UIColour.BLUE, onCancel: async () => resolve(false), }) @@ -41,4 +44,4 @@ const insertTag = (tag: string, remainingTags: string[]): Promise => }) ); -export {insertTags}; +export {insertTagsDialogs}; diff --git a/src/ts/content/extensions/util/tagValidation.ts b/src/ts/content/extensions/util/tagValidation.ts index c457f0e1..ac6cb5f6 100644 --- a/src/ts/content/extensions/util/tagValidation.ts +++ b/src/ts/content/extensions/util/tagValidation.ts @@ -2,9 +2,10 @@ import {getTagsFromElement, getTagTrees, TagTrees} from "../../adapters/tags"; import {createModal} from "../../../common/ui/modal"; import {loaderOverlaid} from "../../../common/ui/loadingIndicator"; import {UIColour} from "../../../common/ui/colour"; -import {OnSave, OffSave} from "../../entities/bookForm"; +import {OffSave, OnSave} from "../../entities/bookForm"; import {Highlight, Highlightable, highlighted} from "../../../common/ui/highlighter"; import {getSheetLink} from "../../../common/entities/spreadsheet"; +import {insertTagsDialogs} from "../../dialogs/insertTags"; type GetTagsOptions = {noCache: boolean}; @@ -47,6 +48,13 @@ const getUserAcceptance = ( colour: UIColour.GREY, onClick: async () => resolve(getSecondaryAcceptance(saveHandler)), }, + { + kind: "button", + text: "Insert Tags", + colour: UIColour.GREY, + onClick: async () => + resolve(insertTagsDialogs(invalidTags).then(() => saveHandler({noCache: true}))), + }, {kind: "button", text: "Save anyway", colour: UIColour.GREY, onClick: async () => resolve(true)}, {kind: "button", text: "Cancel", colour: UIColour.PURPLE, onClick: async () => resolve(false)}, ], diff --git a/src/ts/content/index.ts b/src/ts/content/index.ts index 3ec7074e..4d3ea459 100644 --- a/src/ts/content/index.ts +++ b/src/ts/content/index.ts @@ -10,18 +10,3 @@ import "./extensions/resize"; import "./extensions/tags"; import "./extensions/sort"; import "./extensions/enforceTagIndexAccess"; - -// import {insertTags} from "./dialogs/insertTags"; - -// insertTags([ -// "Tag 0", -// "Tag 1", -// "Tag 2", -// "Tag 3", -// "Tag 4", -// "Tag 5", -// "Tag 6", -// "Tag 7", -// "Tag 8", -// "Tag 9", -// ]).then(() => alert("done")); From 40d78cbd06687f51529c1bee00c74fea2916bf14 Mon Sep 17 00:00:00 2001 From: braxtonhall Date: Thu, 12 Jan 2023 22:06:51 +0100 Subject: [PATCH 03/15] #212 Avoid naming collision with author page tag insertion --- src/ts/content/extensions/author/authorPage/authorUI.ts | 4 ++-- src/ts/content/extensions/author/authorPage/index.ts | 4 ++-- src/ts/content/extensions/author/authorPage/pull.ts | 6 +++--- src/ts/content/extensions/author/authorPage/util.ts | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/ts/content/extensions/author/authorPage/authorUI.ts b/src/ts/content/extensions/author/authorPage/authorUI.ts index 842f8d96..3e254a31 100644 --- a/src/ts/content/extensions/author/authorPage/authorUI.ts +++ b/src/ts/content/extensions/author/authorPage/authorUI.ts @@ -118,7 +118,7 @@ const appendUI = (container: Element, handlers: ButtonHandlers, getTagsCallback: viewExistingTags(); }; -const insertTags = (tags: string[]) => { +const renderAuthorTags = (tags: string[]) => { const list = document.getElementById(TAG_LIST_ID); const input = document.getElementById(AUTHOR_TAG_INPUT_ID) as HTMLInputElement; if (tags.length === 0) { @@ -144,4 +144,4 @@ const getInputElement = () => document.getElementById(AUTHOR_TAG_INPUT_ID) as HT const viewExistingTags = toggleViews(TAG_LIST_CONTAINER_ID, TAG_INPUT_CONTAINER_ID); const viewTagEditor = toggleViews(TAG_INPUT_CONTAINER_ID, TAG_LIST_CONTAINER_ID); -export {appendUI, insertTags, getInput, viewExistingTags, viewTagEditor, AUTHOR_TAG_INPUT_ID}; +export {appendUI, renderAuthorTags, getInput, viewExistingTags, viewTagEditor, AUTHOR_TAG_INPUT_ID}; diff --git a/src/ts/content/extensions/author/authorPage/index.ts b/src/ts/content/extensions/author/authorPage/index.ts index 443bf1c4..2479d1b1 100644 --- a/src/ts/content/extensions/author/authorPage/index.ts +++ b/src/ts/content/extensions/author/authorPage/index.ts @@ -1,5 +1,5 @@ import Author, {AuthorRecord} from "../../../adapters/author"; -import {appendUI, getInput, insertTags, AUTHOR_TAG_INPUT_ID, viewExistingTags, viewTagEditor} from "./authorUI"; +import {appendUI, getInput, renderAuthorTags, AUTHOR_TAG_INPUT_ID, viewExistingTags, viewTagEditor} from "./authorUI"; import {loaderOverlaid} from "../../../../common/ui/loadingIndicator"; import Book from "../../../adapters/book"; import {createPushBookTags, createSyncBookTags} from "../util/bookEditor"; @@ -18,7 +18,7 @@ const onEdit = () => { const onBackToExistingTags = (getAuthor: () => Promise) => () => loaderOverlaid(async () => { const author = await getAuthor(); - author && insertTags(author.tags); + author && renderAuthorTags(author.tags); viewExistingTags(); }); diff --git a/src/ts/content/extensions/author/authorPage/pull.ts b/src/ts/content/extensions/author/authorPage/pull.ts index 4d4e5373..e88747fa 100644 --- a/src/ts/content/extensions/author/authorPage/pull.ts +++ b/src/ts/content/extensions/author/authorPage/pull.ts @@ -1,5 +1,5 @@ import {createModal} from "../../../../common/ui/modal"; -import {getInput, insertTags} from "./authorUI"; +import {getInput, renderAuthorTags} from "./authorUI"; import {showToast, ToastType} from "../../../../common/ui/toast"; import {loaderOverlaid} from "../../../../common/ui/loadingIndicator"; import {authorTagsFromBooksWhere, getAuthorInfo} from "./util"; @@ -30,7 +30,7 @@ const uncertainTagModal = kind: "button", text: `Add${strings.all}`, colour: UIColour.RED, - onClick: async () => insertTags([...certainTags, ...uncertainTags]), + onClick: async () => renderAuthorTags([...certainTags, ...uncertainTags]), }, {kind: "button", text: "Back", colour: UIColour.BLUE}, ], @@ -63,7 +63,7 @@ const onPull = async () => const multiAuthorTags = authorTagsFromBooksWhere(books, (book) => book.authorIds.length !== 1); const certainTags = new Set([...getInput(), ...(author?.tags ?? []), ...singleAuthorTags]); const uncertainTags = new Set(multiAuthorTags.filter((tag) => !certainTags.has(tag))); - insertTags([...certainTags]); + renderAuthorTags([...certainTags]); return {certainTags, uncertainTags, name}; }).then(finishPull); diff --git a/src/ts/content/extensions/author/authorPage/util.ts b/src/ts/content/extensions/author/authorPage/util.ts index 43fe20b3..1cf96c83 100644 --- a/src/ts/content/extensions/author/authorPage/util.ts +++ b/src/ts/content/extensions/author/authorPage/util.ts @@ -1,5 +1,5 @@ import Author from "../../../adapters/author"; -import {insertTags} from "./authorUI"; +import {renderAuthorTags} from "./authorUI"; import {filterAuthorTags} from "../../../util/filterAuthorTags"; import {BookRecord} from "../../../adapters/book"; @@ -11,7 +11,7 @@ const getAuthorInfo = () => { const getTags = async () => { const author = await Author.getAuthor(getAuthorInfo().uuid); - insertTags(author?.tags ?? []); + renderAuthorTags(author?.tags ?? []); }; const authorTagsFromBooksWhere = (books: BookRecord[], where: (book: BookRecord) => boolean) => From ce88e0fbf588554cef21027a068edfdad84cf43e Mon Sep 17 00:00:00 2001 From: braxtonhall Date: Thu, 12 Jan 2023 22:26:43 +0100 Subject: [PATCH 04/15] #212 Move some files around for cohesion --- .../appendTagValidator.ts} | 66 ++----------------- .../util/tagValidation}/dialogs/insertTags.ts | 10 +-- .../tagValidation/dialogs/userAcceptance.ts | 56 ++++++++++++++++ .../extensions/util/tagValidation/index.ts | 3 + .../extensions/util/tagValidation/types.ts | 3 + 5 files changed, 73 insertions(+), 65 deletions(-) rename src/ts/content/extensions/util/{tagValidation.ts => tagValidation/appendTagValidator.ts} (52%) rename src/ts/content/{ => extensions/util/tagValidation}/dialogs/insertTags.ts (84%) create mode 100644 src/ts/content/extensions/util/tagValidation/dialogs/userAcceptance.ts create mode 100644 src/ts/content/extensions/util/tagValidation/index.ts create mode 100644 src/ts/content/extensions/util/tagValidation/types.ts diff --git a/src/ts/content/extensions/util/tagValidation.ts b/src/ts/content/extensions/util/tagValidation/appendTagValidator.ts similarity index 52% rename from src/ts/content/extensions/util/tagValidation.ts rename to src/ts/content/extensions/util/tagValidation/appendTagValidator.ts index ac6cb5f6..e00b53ff 100644 --- a/src/ts/content/extensions/util/tagValidation.ts +++ b/src/ts/content/extensions/util/tagValidation/appendTagValidator.ts @@ -1,13 +1,9 @@ -import {getTagsFromElement, getTagTrees, TagTrees} from "../../adapters/tags"; -import {createModal} from "../../../common/ui/modal"; -import {loaderOverlaid} from "../../../common/ui/loadingIndicator"; -import {UIColour} from "../../../common/ui/colour"; -import {OffSave, OnSave} from "../../entities/bookForm"; -import {Highlight, Highlightable, highlighted} from "../../../common/ui/highlighter"; -import {getSheetLink} from "../../../common/entities/spreadsheet"; -import {insertTagsDialogs} from "../../dialogs/insertTags"; - -type GetTagsOptions = {noCache: boolean}; +import {getTagsFromElement, getTagTrees, TagTrees} from "../../../adapters/tags"; +import {loaderOverlaid} from "../../../../common/ui/loadingIndicator"; +import {OffSave, OnSave} from "../../../entities/bookForm"; +import {Highlight, Highlightable, highlighted} from "../../../../common/ui/highlighter"; +import {getUserAcceptance} from "./dialogs/userAcceptance"; +import {GetTagsOptions} from "./types"; const applyHighlights = async (text: string): Promise => { const validTags = await getTagTrees().catch(() => new Map()); @@ -33,56 +29,6 @@ const applyHighlights = async (text: string): Promise => { const getInvalidTags = (tags: string[], trees: TagTrees): string[] => tags.filter((tag) => !trees.has(tag.toLowerCase())); -const getUserAcceptance = ( - invalidTags: string[], - saveHandler: (options: GetTagsOptions) => Promise -): Promise => - new Promise((resolve) => - createModal({ - text: "Are you sure? The following tags are not in the Tag Index", - subText: invalidTags, - elements: [ - { - kind: "button", - text: "Open the Tag Index", - colour: UIColour.GREY, - onClick: async () => resolve(getSecondaryAcceptance(saveHandler)), - }, - { - kind: "button", - text: "Insert Tags", - colour: UIColour.GREY, - onClick: async () => - resolve(insertTagsDialogs(invalidTags).then(() => saveHandler({noCache: true}))), - }, - {kind: "button", text: "Save anyway", colour: UIColour.GREY, onClick: async () => resolve(true)}, - {kind: "button", text: "Cancel", colour: UIColour.PURPLE, onClick: async () => resolve(false)}, - ], - colour: UIColour.PURPLE, - onCancel: async () => resolve(false), - }) - ); - -const getSecondaryAcceptance = async (saveHandler: (options: GetTagsOptions) => Promise): Promise => { - window.open(await getSheetLink()); - return new Promise((resolve) => - createModal({ - text: "Did you put the new tags in the Tag Index?", - elements: [ - { - kind: "button", - text: "Yes!", - colour: UIColour.GREY, - onClick: async () => resolve(saveHandler({noCache: true})), - }, - {kind: "button", text: "Cancel", colour: UIColour.PURPLE, onClick: async () => resolve(false)}, - ], - colour: UIColour.PURPLE, - onCancel: async () => resolve(false), - }) - ); -}; - const fixTagsCase = (tags: string[], trees: TagTrees): string[] => tags.map((tag) => trees.get(tag.toLowerCase())?.tag ?? tag); diff --git a/src/ts/content/dialogs/insertTags.ts b/src/ts/content/extensions/util/tagValidation/dialogs/insertTags.ts similarity index 84% rename from src/ts/content/dialogs/insertTags.ts rename to src/ts/content/extensions/util/tagValidation/dialogs/insertTags.ts index 1e24bb89..ec488d72 100644 --- a/src/ts/content/dialogs/insertTags.ts +++ b/src/ts/content/extensions/util/tagValidation/dialogs/insertTags.ts @@ -1,14 +1,14 @@ -import {createModal} from "../../common/ui/modal"; -import {UIColour} from "../../common/ui/colour"; +import {UIColour} from "../../../../../common/ui/colour"; +import {createModal} from "../../../../../common/ui/modal"; const toInsertionButton = (remainingTags: string[], resolve: (value) => void) => (tag: string) => ({ kind: "button" as const, colour: UIColour.BLUE, text: tag, - onClick: async () => resolve(insertTag(tag, remainingTags).then(insertTagsDialogs)), + onClick: async () => resolve(insertTag(tag, remainingTags).then(insertTags)), }); -const insertTagsDialogs = async (tags: string[]): Promise => { +const insertTags = async (tags: string[]): Promise => { if (tags.length === 0) { return Promise.resolve(true); } else { @@ -44,4 +44,4 @@ const insertTag = (tag: string, remainingTags: string[]): Promise => }) ); -export {insertTagsDialogs}; +export {insertTags}; diff --git a/src/ts/content/extensions/util/tagValidation/dialogs/userAcceptance.ts b/src/ts/content/extensions/util/tagValidation/dialogs/userAcceptance.ts new file mode 100644 index 00000000..37515e51 --- /dev/null +++ b/src/ts/content/extensions/util/tagValidation/dialogs/userAcceptance.ts @@ -0,0 +1,56 @@ +import {createModal} from "../../../../../common/ui/modal"; +import {UIColour} from "../../../../../common/ui/colour"; +import {insertTags} from "./insertTags"; +import {getSheetLink} from "../../../../../common/entities/spreadsheet"; +import {GetTagsOptions} from "../types"; + +const getUserAcceptance = ( + invalidTags: string[], + saveHandler: (options: GetTagsOptions) => Promise +): Promise => + new Promise((resolve) => + createModal({ + text: "Are you sure? The following tags are not in the Tag Index", + subText: invalidTags, + elements: [ + { + kind: "button", + text: "Open the Tag Index", + colour: UIColour.GREY, + onClick: async () => resolve(getSecondaryAcceptance(saveHandler)), + }, + { + kind: "button", + text: "Insert Tags", + colour: UIColour.GREY, + onClick: async () => resolve(insertTags(invalidTags).then(() => saveHandler({noCache: true}))), + }, + {kind: "button", text: "Save anyway", colour: UIColour.GREY, onClick: async () => resolve(true)}, + {kind: "button", text: "Cancel", colour: UIColour.PURPLE, onClick: async () => resolve(false)}, + ], + colour: UIColour.PURPLE, + onCancel: async () => resolve(false), + }) + ); + +const getSecondaryAcceptance = async (saveHandler: (options: GetTagsOptions) => Promise): Promise => { + window.open(await getSheetLink()); + return new Promise((resolve) => + createModal({ + text: "Did you put the new tags in the Tag Index?", + elements: [ + { + kind: "button", + text: "Yes!", + colour: UIColour.GREY, + onClick: async () => resolve(saveHandler({noCache: true})), + }, + {kind: "button", text: "Cancel", colour: UIColour.PURPLE, onClick: async () => resolve(false)}, + ], + colour: UIColour.PURPLE, + onCancel: async () => resolve(false), + }) + ); +}; + +export {getUserAcceptance}; diff --git a/src/ts/content/extensions/util/tagValidation/index.ts b/src/ts/content/extensions/util/tagValidation/index.ts new file mode 100644 index 00000000..fd416a65 --- /dev/null +++ b/src/ts/content/extensions/util/tagValidation/index.ts @@ -0,0 +1,3 @@ +import {appendTagValidator} from "./appendTagValidator"; + +export {appendTagValidator}; diff --git a/src/ts/content/extensions/util/tagValidation/types.ts b/src/ts/content/extensions/util/tagValidation/types.ts new file mode 100644 index 00000000..9b67c734 --- /dev/null +++ b/src/ts/content/extensions/util/tagValidation/types.ts @@ -0,0 +1,3 @@ +type GetTagsOptions = {noCache: boolean}; + +export type {GetTagsOptions}; From e8be0b6e30ebababb757e0b547d852e34fdc93a9 Mon Sep 17 00:00:00 2001 From: braxtonhall Date: Thu, 12 Jan 2023 22:28:05 +0100 Subject: [PATCH 05/15] #212 Make insert tags the first option when there are bad tags found --- .../extensions/util/tagValidation/dialogs/insertTags.ts | 2 +- .../util/tagValidation/dialogs/userAcceptance.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ts/content/extensions/util/tagValidation/dialogs/insertTags.ts b/src/ts/content/extensions/util/tagValidation/dialogs/insertTags.ts index ec488d72..4461b823 100644 --- a/src/ts/content/extensions/util/tagValidation/dialogs/insertTags.ts +++ b/src/ts/content/extensions/util/tagValidation/dialogs/insertTags.ts @@ -17,7 +17,7 @@ const insertTags = async (tags: string[]): Promise => { text: "Which tag would you like to insert?", elements: [ ...tags.map(toInsertionButton(tags, resolve)), - {kind: "button", colour: UIColour.RED, text: "Back", onClick: async () => resolve(false)}, + {kind: "button", colour: UIColour.RED, text: "Done", onClick: async () => resolve(false)}, ], colour: UIColour.BLUE, onCancel: async () => resolve(false), diff --git a/src/ts/content/extensions/util/tagValidation/dialogs/userAcceptance.ts b/src/ts/content/extensions/util/tagValidation/dialogs/userAcceptance.ts index 37515e51..3f76e05e 100644 --- a/src/ts/content/extensions/util/tagValidation/dialogs/userAcceptance.ts +++ b/src/ts/content/extensions/util/tagValidation/dialogs/userAcceptance.ts @@ -15,15 +15,15 @@ const getUserAcceptance = ( elements: [ { kind: "button", - text: "Open the Tag Index", + text: "Insert Tags", colour: UIColour.GREY, - onClick: async () => resolve(getSecondaryAcceptance(saveHandler)), + onClick: async () => resolve(insertTags(invalidTags).then(() => saveHandler({noCache: true}))), }, { kind: "button", - text: "Insert Tags", + text: "Open the Tag Index", colour: UIColour.GREY, - onClick: async () => resolve(insertTags(invalidTags).then(() => saveHandler({noCache: true}))), + onClick: async () => resolve(getSecondaryAcceptance(saveHandler)), }, {kind: "button", text: "Save anyway", colour: UIColour.GREY, onClick: async () => resolve(true)}, {kind: "button", text: "Cancel", colour: UIColour.PURPLE, onClick: async () => resolve(false)}, From ce52e708c2c5d0568e00cf8e0ae239eefce4a9d8 Mon Sep 17 00:00:00 2001 From: braxtonhall Date: Thu, 12 Jan 2023 23:24:50 +0100 Subject: [PATCH 06/15] #212 Make the tag parser return all the sheet metadata it can --- src/ts/content/adapters/tags/getTags.ts | 55 +++++++++---------- src/ts/content/adapters/tags/index.ts | 13 +++-- src/ts/content/adapters/tags/parseTags.ts | 33 ++++++----- src/ts/content/adapters/tags/types.ts | 19 +++++-- .../extensions/util/contentWarningCheck.ts | 8 +-- .../util/tagValidation/appendTagValidator.ts | 14 ++--- 6 files changed, 80 insertions(+), 62 deletions(-) diff --git a/src/ts/content/adapters/tags/getTags.ts b/src/ts/content/adapters/tags/getTags.ts index 004d0e2b..1d94b0be 100644 --- a/src/ts/content/adapters/tags/getTags.ts +++ b/src/ts/content/adapters/tags/getTags.ts @@ -1,13 +1,10 @@ import {makeCache} from "../../../common/util/cache"; -import {TagSearchOptions, TagTrees, WarnedTag} from "./types"; -import Sheets, {Range, ValueRange} from "../sheets"; -import {parseTree} from "./parseTags"; +import {TagSearchOptions, WarnedTag, TagSheetDescriptor, TagMapper, RawTagSheet, Tags} from "./types"; +import Sheets, {Range, ValueRange, Values} from "../sheets"; +import {parseTags} from "./parseTags"; import {incrementColumnBy} from "../sheets/util"; import {getSheetId} from "../../../common/entities/spreadsheet"; -type TagMapper = `${string}$TAG${string}`; -type MappedRange = {range: Range; mapper: TagMapper; name: string; cwRange?: Range}; - const META_TAG_SHEET = "Tag Index Index"; /** * The Meta Tag Sheet stores information about where tags are in the spreadsheet @@ -27,12 +24,12 @@ const META_TAG_SHEET = "Tag Index Index"; * The first tag in this table is at A2 */ -const {asyncCached, setCache} = makeCache(); +const {asyncCached, setCache} = makeCache(); const rowIsRange = ([, topLeft, width]: string[]): boolean => topLeft && width && /^[A-Z]+[0-9]+$/.test(topLeft) && /^[0-9]+$/.test(width); -const rowToMappedRange = ([sheet, topLeft, width, cwColumn, userMapper, as]: string[]): MappedRange => { +const rowToSheetDescriptor = ([sheet, topLeft, width, cwColumn, userMapper, as]: string[]): TagSheetDescriptor => { // `left` is a column, for example "B" const left = topLeft.match(/^[A-Z]+/)[0]; // `top` is a row, for example "4" @@ -46,49 +43,51 @@ const rowToMappedRange = ([sheet, topLeft, width, cwColumn, userMapper, as]: str return {range, mapper, cwRange, name: as ?? sheet}; }; -const getTagRanges = async (): Promise => { +const getSheetDescriptors = async (): Promise => { const range = Sheets.createRange(META_TAG_SHEET, "A", "E"); const response = await Sheets.readRanges(await getSheetId(), [range]); - return response?.[0].values.filter(rowIsRange).map(rowToMappedRange) ?? []; + return response?.[0].values.filter(rowIsRange).map(rowToSheetDescriptor) ?? []; }; -const extractRanges = (ranges: MappedRange[]): Set => +const extractRanges = (ranges: TagSheetDescriptor[]): Set => ranges.reduce( (acc, {cwRange, range}) => (cwRange ? acc.add(cwRange).add(range) : acc.add(range)), new Set() ); -const nameResponses = (ranges: Range[], response: ValueRange[] | null): Map => { +const nameResponses = (ranges: Range[], response: ValueRange[] | null): Map => { const groupedResponses = new Map(); - response?.forEach((valueRange, index) => groupedResponses.set(ranges[index], valueRange)); + response?.forEach((valueRange, index) => groupedResponses.set(ranges[index], valueRange?.values)); return groupedResponses; }; -const annotateWithContentWarnings = (tags: string[][], contentWarnings: ValueRange | undefined): WarnedTag[][] => +const annotateWithContentWarnings = (tags: string[][], contentWarnings: Values | undefined): WarnedTag[][] => tags.map((row, rowIndex) => row.map((cell) => ({tag: cell, warning: !!contentWarnings?.values?.[rowIndex]?.[0]}))); -const toWarningTags = (namedResponses: Map) => (mappedRange: MappedRange) => { - const tags = namedResponses.get(mappedRange.range); - const mappedTags = mapTags(tags, mappedRange.mapper); - const contentWarnings = namedResponses.get(mappedRange.cwRange); - return annotateWithContentWarnings(mappedTags, contentWarnings); -}; +const toTagSheet = + (namedResponses: Map) => + (descriptor: TagSheetDescriptor): RawTagSheet => { + const tags = namedResponses.get(descriptor.range); + const mappedTags = mapTags(tags, descriptor.mapper); + const contentWarnings = namedResponses.get(descriptor.cwRange); + const values = annotateWithContentWarnings(mappedTags, contentWarnings); + return {...descriptor, values}; + }; -const getSheetsTags = async (): Promise => { - const mappedRanges = await getTagRanges(); +const getSheetsTags = async (): Promise => { + const mappedRanges = await getSheetDescriptors(); const ranges = [...extractRanges(mappedRanges)]; // We might have duplicate ranges, so we use a set when extracting - const response = await Sheets.readRanges(await getSheetId(), [...ranges]); + const response = await Sheets.readRanges(await getSheetId(), ranges); const namedResponses = nameResponses(ranges, response); - return mappedRanges.flatMap(toWarningTags(namedResponses)); + return mappedRanges.map(toTagSheet(namedResponses)); }; -const mapTags = (valueRange: ValueRange | undefined, mapper: TagMapper): string[][] => { - const values = valueRange?.values ?? []; - return values.map((row) => row.map((value) => mapper.replaceAll("$TAG", value))); +const mapTags = (values: Values | undefined, mapper: TagMapper): string[][] => { + return values?.map((row) => row.map((value) => mapper.replaceAll("$TAG", value))) ?? []; }; const getTagTrees = async ({noCache}: TagSearchOptions = {noCache: false}) => { - const implementation = async () => parseTree(await getSheetsTags()); + const implementation = async () => parseTags(await getSheetsTags()); if (noCache) { return implementation().then((tree) => setCache("", tree)); } else { diff --git a/src/ts/content/adapters/tags/index.ts b/src/ts/content/adapters/tags/index.ts index 582f4e4d..0db66652 100644 --- a/src/ts/content/adapters/tags/index.ts +++ b/src/ts/content/adapters/tags/index.ts @@ -1,17 +1,18 @@ -import {TagSearchOptions, TagTrees} from "./types"; +import {TagSearchOptions, TagNodes, TagTree} from "./types"; import {getTagTrees} from "./getTags"; const getAncestry = async (tag: string): Promise => { - const trees = await getTagTrees(); + const {nodes} = await getTagTrees(); const ancestry = []; - for (let node = trees.get(tag.toLowerCase()); node; node = node.parent) { - ancestry.push(node.tag); + for (let node: TagTree["parent"] = nodes.get(tag.toLowerCase()); node && "parent" in node; node = node.parent) { + // TODO maybe we should detect collisions here and then create a modal? + "tag" in node && ancestry.push(node.tag); } return ancestry; }; const getTagList = async (options: TagSearchOptions = {noCache: false}) => { - const nodes = (await getTagTrees(options)).values(); + const nodes = (await getTagTrees(options)).nodes.values(); const tags = [...nodes].map((node) => node.tag); return new Set(tags); }; @@ -36,5 +37,5 @@ const getTagsFromElement = (element: HTMLTextAreaElement | HTMLInputElement): st .map((tag) => tag.trim()) .filter((tag) => !!tag) ?? []; -export type {TagTrees}; +export type {TagNodes}; export {getAncestry, getTagList, getTagsIncluding, getTagTrees, getTagsFromElement}; diff --git a/src/ts/content/adapters/tags/parseTags.ts b/src/ts/content/adapters/tags/parseTags.ts index 47fbff6e..4f18213f 100644 --- a/src/ts/content/adapters/tags/parseTags.ts +++ b/src/ts/content/adapters/tags/parseTags.ts @@ -1,22 +1,24 @@ -import {TagTree, TagTrees, WarnedTag} from "./types"; +import {TagTree, TagNodes, WarnedTag, RawTagSheet, TagRoot} from "./types"; interface ParserOptions { rows: WarnedTag[][]; fromRow: number; depth: number; - trees: TagTrees; - parent?: TagTree; + nodes: TagNodes; + parent: TagTree | TagRoot; } -const parseRows = ({rows, fromRow, depth, trees, parent}: ParserOptions): number => { +const parseRows = ({rows, fromRow, depth, nodes, parent}: ParserOptions): number => { let row = fromRow; while (row < rows.length) { const {tag, warning} = rows[row][depth] ?? {}; if (tag) { - const node = {tag, parent, warning}; - // Keys in the tag trees map are lowercase for ez lookup later - trees.set(tag.toLowerCase(), node); - row = parseRows({rows, fromRow: row + 1, depth: depth + 1, trees, parent: node}); + const node = {tag, parent, warning, children: []}; + parent.children.push(node); + // TODO if nodes already contains this tag, we need to emit a warning somehow!!! see #214 + // Keys in the tag nodes map are lowercase for ez lookup later + nodes.set(tag.toLowerCase(), node); + row = parseRows({rows, fromRow: row + 1, depth: depth + 1, nodes, parent: node}); } else { break; } @@ -24,10 +26,15 @@ const parseRows = ({rows, fromRow, depth, trees, parent}: ParserOptions): number return row; }; -const parseTree = (rows: WarnedTag[][]) => { - const trees: TagTrees = new Map(); - for (let fromRow = 0; fromRow < rows.length; fromRow = parseRows({rows, fromRow, depth: 0, trees}) + 1); - return trees; +const parseTags = (sheets: RawTagSheet[]) => { + const nodes: TagNodes = new Map(); + const roots = sheets.map((sheet) => { + const rows = sheet.values; + const parent = {...sheet, children: []}; + for (let fromRow = 0; fromRow < rows.length; fromRow = parseRows({rows, fromRow, depth: 0, nodes, parent}) + 1); + return parent; + }); + return {nodes, roots}; }; -export {parseTree}; +export {parseTags}; diff --git a/src/ts/content/adapters/tags/types.ts b/src/ts/content/adapters/tags/types.ts index 8c539a32..72117172 100644 --- a/src/ts/content/adapters/tags/types.ts +++ b/src/ts/content/adapters/tags/types.ts @@ -1,14 +1,25 @@ +import {Range} from "../sheets"; + +type TagMapper = `${string}$TAG${string}`; +type TagSheetDescriptor = {range: Range; mapper: TagMapper; name: string; cwRange?: Range}; + +type RawTagSheet = TagSheetDescriptor & {values: WarnedTag[][]}; + +type TagRoot = RawTagSheet & {children: TagTree[]}; + /** * A tag tree contains the properly cased tag at the root, * and a pointer to its parent in the tree */ -type TagTree = {tag: string; parent?: TagTree; warning: boolean}; +type TagTree = {tag: string; parent: TagTree | TagRoot; warning: boolean; children: TagTree[]}; /** - * TagTrees is a map of lowercase tag to the tag subtree at that tag + * TagNodes is a map of lowercase tag to the tag subtree at that tag * It contains every subtree of the complete tag tree */ -type TagTrees = Map; +type TagNodes = Map; + +type Tags = {nodes: TagNodes; roots: TagRoot[]}; interface TagSearchOptions { noCache: boolean; @@ -16,4 +27,4 @@ interface TagSearchOptions { type WarnedTag = {tag: string; warning: boolean}; -export {WarnedTag, TagTree, TagTrees, TagSearchOptions}; +export {WarnedTag, TagTree, TagRoot, TagNodes, TagSearchOptions, RawTagSheet, TagSheetDescriptor, TagMapper, Tags}; diff --git a/src/ts/content/extensions/util/contentWarningCheck.ts b/src/ts/content/extensions/util/contentWarningCheck.ts index 68cc3eef..d5932248 100644 --- a/src/ts/content/extensions/util/contentWarningCheck.ts +++ b/src/ts/content/extensions/util/contentWarningCheck.ts @@ -1,6 +1,6 @@ import {OffSave, OnSave} from "../../entities/bookForm"; import {loaderOverlaid} from "../../../common/ui/loadingIndicator"; -import {getTagsFromElement, getTagTrees, TagTrees} from "../../adapters/tags"; +import {getTagsFromElement, getTagTrees, TagNodes} from "../../adapters/tags"; import {createModal} from "../../../common/ui/modal"; import {UIColour} from "../../../common/ui/colour"; @@ -53,7 +53,7 @@ const handleContentWarning = async ( return true; } }; -const getWarningRequiredTags = (commentsTextArea: HTMLTextAreaElement, tags: string[], trees: TagTrees): string[] => { +const getWarningRequiredTags = (commentsTextArea: HTMLTextAreaElement, tags: string[], trees: TagNodes): string[] => { if (contentWarningIsPresent(commentsTextArea)) { return []; } else { @@ -63,9 +63,9 @@ const getWarningRequiredTags = (commentsTextArea: HTMLTextAreaElement, tags: str const checkTags = async (tagInput: HTMLTextAreaElement, commentsTextArea: HTMLTextAreaElement): Promise => loaderOverlaid(async () => { - const trees = await getTagTrees(); + const {nodes} = await getTagTrees(); const userTags = getTagsFromElement(tagInput); - return getWarningRequiredTags(commentsTextArea, userTags, trees); + return getWarningRequiredTags(commentsTextArea, userTags, nodes); }); const handleSave = (tagsTextArea: HTMLTextAreaElement, commentsTextArea: HTMLTextAreaElement): Promise => diff --git a/src/ts/content/extensions/util/tagValidation/appendTagValidator.ts b/src/ts/content/extensions/util/tagValidation/appendTagValidator.ts index e00b53ff..1246dfaf 100644 --- a/src/ts/content/extensions/util/tagValidation/appendTagValidator.ts +++ b/src/ts/content/extensions/util/tagValidation/appendTagValidator.ts @@ -1,4 +1,4 @@ -import {getTagsFromElement, getTagTrees, TagTrees} from "../../../adapters/tags"; +import {getTagsFromElement, getTagTrees, TagNodes} from "../../../adapters/tags"; import {loaderOverlaid} from "../../../../common/ui/loadingIndicator"; import {OffSave, OnSave} from "../../../entities/bookForm"; import {Highlight, Highlightable, highlighted} from "../../../../common/ui/highlighter"; @@ -6,7 +6,7 @@ import {getUserAcceptance} from "./dialogs/userAcceptance"; import {GetTagsOptions} from "./types"; const applyHighlights = async (text: string): Promise => { - const validTags = await getTagTrees().catch(() => new Map()); + const {nodes: validTags} = await getTagTrees().catch(() => ({nodes: new Map()})); return text .split(",") .flatMap((part) => { @@ -26,10 +26,10 @@ const applyHighlights = async (text: string): Promise => { .slice(0, -1); // Remove the trailing comma }; -const getInvalidTags = (tags: string[], trees: TagTrees): string[] => +const getInvalidTags = (tags: string[], trees: TagNodes): string[] => tags.filter((tag) => !trees.has(tag.toLowerCase())); -const fixTagsCase = (tags: string[], trees: TagTrees): string[] => +const fixTagsCase = (tags: string[], trees: TagNodes): string[] => tags.map((tag) => trees.get(tag.toLowerCase())?.tag ?? tag); const setTags = (tagInput: Highlightable, tags: Iterable) => { @@ -39,11 +39,11 @@ const setTags = (tagInput: Highlightable, tags: Iterable) => { const checkTags = async (tagInput: Highlightable, options: GetTagsOptions) => loaderOverlaid(async () => { - const trees = await getTagTrees(options); + const {nodes} = await getTagTrees(options); const userTags = getTagsFromElement(tagInput); - const properCaseTags = fixTagsCase(userTags, trees); + const properCaseTags = fixTagsCase(userTags, nodes); setTags(tagInput, properCaseTags); - return getInvalidTags(properCaseTags, trees); + return getInvalidTags(properCaseTags, nodes); }); const handleSave = (tagInput: Highlightable, options: GetTagsOptions) => { From 4cb452281caa5345d0ce3e55c76fd8bc227741d9 Mon Sep 17 00:00:00 2001 From: braxtonhall Date: Fri, 13 Jan 2023 00:01:04 +0100 Subject: [PATCH 07/15] #212 Implement nesting with the dialog --- src/ts/content/adapters/tags/getTags.ts | 2 +- src/ts/content/adapters/tags/index.ts | 4 +- src/ts/content/adapters/tags/parseTags.ts | 2 +- src/ts/content/adapters/tags/types.ts | 2 +- .../util/tagValidation/appendTagValidator.ts | 2 +- .../util/tagValidation/dialogs/insertTags.ts | 54 +++++++++++++++---- 6 files changed, 50 insertions(+), 16 deletions(-) diff --git a/src/ts/content/adapters/tags/getTags.ts b/src/ts/content/adapters/tags/getTags.ts index 1d94b0be..5f1b88b7 100644 --- a/src/ts/content/adapters/tags/getTags.ts +++ b/src/ts/content/adapters/tags/getTags.ts @@ -44,7 +44,7 @@ const rowToSheetDescriptor = ([sheet, topLeft, width, cwColumn, userMapper, as]: }; const getSheetDescriptors = async (): Promise => { - const range = Sheets.createRange(META_TAG_SHEET, "A", "E"); + const range = Sheets.createRange(META_TAG_SHEET, "A", "F"); const response = await Sheets.readRanges(await getSheetId(), [range]); return response?.[0].values.filter(rowIsRange).map(rowToSheetDescriptor) ?? []; }; diff --git a/src/ts/content/adapters/tags/index.ts b/src/ts/content/adapters/tags/index.ts index 0db66652..be0df7b8 100644 --- a/src/ts/content/adapters/tags/index.ts +++ b/src/ts/content/adapters/tags/index.ts @@ -6,14 +6,14 @@ const getAncestry = async (tag: string): Promise => { const ancestry = []; for (let node: TagTree["parent"] = nodes.get(tag.toLowerCase()); node && "parent" in node; node = node.parent) { // TODO maybe we should detect collisions here and then create a modal? - "tag" in node && ancestry.push(node.tag); + "tag" in node && ancestry.push(node.name); } return ancestry; }; const getTagList = async (options: TagSearchOptions = {noCache: false}) => { const nodes = (await getTagTrees(options)).nodes.values(); - const tags = [...nodes].map((node) => node.tag); + const tags = [...nodes].map((node) => node.name); return new Set(tags); }; diff --git a/src/ts/content/adapters/tags/parseTags.ts b/src/ts/content/adapters/tags/parseTags.ts index 4f18213f..3c8ecd51 100644 --- a/src/ts/content/adapters/tags/parseTags.ts +++ b/src/ts/content/adapters/tags/parseTags.ts @@ -13,7 +13,7 @@ const parseRows = ({rows, fromRow, depth, nodes, parent}: ParserOptions): number while (row < rows.length) { const {tag, warning} = rows[row][depth] ?? {}; if (tag) { - const node = {tag, parent, warning, children: []}; + const node = {name: tag, parent, warning, children: []}; parent.children.push(node); // TODO if nodes already contains this tag, we need to emit a warning somehow!!! see #214 // Keys in the tag nodes map are lowercase for ez lookup later diff --git a/src/ts/content/adapters/tags/types.ts b/src/ts/content/adapters/tags/types.ts index 72117172..a7cc9df7 100644 --- a/src/ts/content/adapters/tags/types.ts +++ b/src/ts/content/adapters/tags/types.ts @@ -11,7 +11,7 @@ type TagRoot = RawTagSheet & {children: TagTree[]}; * A tag tree contains the properly cased tag at the root, * and a pointer to its parent in the tree */ -type TagTree = {tag: string; parent: TagTree | TagRoot; warning: boolean; children: TagTree[]}; +type TagTree = {name: string; parent: TagTree | TagRoot; warning: boolean; children: TagTree[]}; /** * TagNodes is a map of lowercase tag to the tag subtree at that tag diff --git a/src/ts/content/extensions/util/tagValidation/appendTagValidator.ts b/src/ts/content/extensions/util/tagValidation/appendTagValidator.ts index 1246dfaf..f7707d04 100644 --- a/src/ts/content/extensions/util/tagValidation/appendTagValidator.ts +++ b/src/ts/content/extensions/util/tagValidation/appendTagValidator.ts @@ -30,7 +30,7 @@ const getInvalidTags = (tags: string[], trees: TagNodes): string[] => tags.filter((tag) => !trees.has(tag.toLowerCase())); const fixTagsCase = (tags: string[], trees: TagNodes): string[] => - tags.map((tag) => trees.get(tag.toLowerCase())?.tag ?? tag); + tags.map((tag) => trees.get(tag.toLowerCase())?.name ?? tag); const setTags = (tagInput: Highlightable, tags: Iterable) => { tagInput.value = [...tags].join(", "); diff --git a/src/ts/content/extensions/util/tagValidation/dialogs/insertTags.ts b/src/ts/content/extensions/util/tagValidation/dialogs/insertTags.ts index 4461b823..a068a836 100644 --- a/src/ts/content/extensions/util/tagValidation/dialogs/insertTags.ts +++ b/src/ts/content/extensions/util/tagValidation/dialogs/insertTags.ts @@ -1,14 +1,17 @@ import {UIColour} from "../../../../../common/ui/colour"; -import {createModal} from "../../../../../common/ui/modal"; +import {createModal, ModalButton} from "../../../../../common/ui/modal"; +import {getTagTrees} from "../../../../adapters/tags"; +import {loaderOverlaid} from "../../../../../common/ui/loadingIndicator"; +import {TagRoot, TagTree} from "../../../../adapters/tags/types"; -const toInsertionButton = (remainingTags: string[], resolve: (value) => void) => (tag: string) => ({ +const tagToInsertionButton = (remainingTags: string[], resolve: (value) => void) => (tag: string) => ({ kind: "button" as const, colour: UIColour.BLUE, text: tag, onClick: async () => resolve(insertTag(tag, remainingTags).then(insertTags)), }); -const insertTags = async (tags: string[]): Promise => { +const insertTags = (tags: string[]): Promise => { if (tags.length === 0) { return Promise.resolve(true); } else { @@ -16,8 +19,8 @@ const insertTags = async (tags: string[]): Promise => { createModal({ text: "Which tag would you like to insert?", elements: [ - ...tags.map(toInsertionButton(tags, resolve)), - {kind: "button", colour: UIColour.RED, text: "Done", onClick: async () => resolve(false)}, + ...tags.map(tagToInsertionButton(tags, resolve)), + {kind: "button", colour: UIColour.RED, text: "Back", onClick: async () => resolve(false)}, ], colour: UIColour.BLUE, onCancel: async () => resolve(false), @@ -26,22 +29,53 @@ const insertTags = async (tags: string[]): Promise => { } }; -const insertTag = (tag: string, remainingTags: string[]): Promise => +const nodeToInsertionButton = + (tag: string, remainingTags: string[], resolve: (value) => void) => + (node: TagRoot | TagTree): ModalButton => ({ + kind: "button", + text: node.name, + colour: UIColour.BLUE, + onClick: async () => resolve(confirm(tag, remainingTags, node)), + }); + +const confirm = (tag: string, remainingTags: string[], node: TagRoot | TagTree) => new Promise((resolve) => createModal({ - text: "Where would you like to insert this tag?", + text: `Insert "${tag}" under "${node.name}"?`, elements: [ { kind: "button", - text: "Here", - colour: UIColour.GREEN, + text: "Yes", + colour: UIColour.GREY, onClick: async () => resolve(remainingTags.filter((otherTag) => otherTag !== tag)), }, - {kind: "button", text: "Not Here", colour: UIColour.RED, onClick: async () => resolve(remainingTags)}, + { + kind: "button", + text: "Deeper", + colour: UIColour.BLUE, + onClick: async () => resolve(insertTagIntoOneOf(tag, remainingTags, node.children)), + }, + {kind: "button", text: "Back", colour: UIColour.RED, onClick: async () => resolve(remainingTags)}, + ], + colour: UIColour.BLUE, + onCancel: async () => resolve(remainingTags), + }) + ); + +const insertTagIntoOneOf = (tag: string, remainingTags: string[], options: Array) => + new Promise((resolve) => + createModal({ + text: "Where would you like to insert this tag?", + elements: [ + ...options.map(nodeToInsertionButton(tag, remainingTags, resolve)), + {kind: "button", text: "Back", colour: UIColour.RED, onClick: async () => resolve(remainingTags)}, ], colour: UIColour.BLUE, onCancel: async () => resolve(remainingTags), }) ); +const insertTag = (tag: string, remainingTags: string[]): Promise => + loaderOverlaid(getTagTrees).then(({roots}) => insertTagIntoOneOf(tag, remainingTags, roots)); + export {insertTags}; From a6573204cea923e4617dc90897255846a80425ae Mon Sep 17 00:00:00 2001 From: braxtonhall Date: Fri, 13 Jan 2023 00:35:09 +0100 Subject: [PATCH 08/15] #212 Make the modal scrollable --- src/sass/modal.sass | 2 ++ .../util/tagValidation/dialogs/insertTags.ts | 16 ++++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/sass/modal.sass b/src/sass/modal.sass index 095cea63..6b6e5ffa 100644 --- a/src/sass/modal.sass +++ b/src/sass/modal.sass @@ -43,6 +43,8 @@ $modal-background-colour: colours.$white margin-bottom: 0 .better-library-thing-modal-element-container + overflow-y: auto + max-height: 60vh width: 100% margin-top: 20px diff --git a/src/ts/content/extensions/util/tagValidation/dialogs/insertTags.ts b/src/ts/content/extensions/util/tagValidation/dialogs/insertTags.ts index a068a836..47cdc9fe 100644 --- a/src/ts/content/extensions/util/tagValidation/dialogs/insertTags.ts +++ b/src/ts/content/extensions/util/tagValidation/dialogs/insertTags.ts @@ -49,12 +49,16 @@ const confirm = (tag: string, remainingTags: string[], node: TagRoot | TagTree) colour: UIColour.GREY, onClick: async () => resolve(remainingTags.filter((otherTag) => otherTag !== tag)), }, - { - kind: "button", - text: "Deeper", - colour: UIColour.BLUE, - onClick: async () => resolve(insertTagIntoOneOf(tag, remainingTags, node.children)), - }, + ...(node.children.length // TODO this kinda but better and checking for depth + ? [ + { + kind: "button" as const, + text: "Deeper", + colour: UIColour.BLUE, + onClick: async () => resolve(insertTagIntoOneOf(tag, remainingTags, node.children)), + }, + ] + : []), {kind: "button", text: "Back", colour: UIColour.RED, onClick: async () => resolve(remainingTags)}, ], colour: UIColour.BLUE, From 2adcc24609b4da388713ef4b77aa2db019e27026 Mon Sep 17 00:00:00 2001 From: braxtonhall Date: Fri, 13 Jan 2023 16:06:11 +0100 Subject: [PATCH 09/15] #212 Complete the insert tag dialog --- src/ts/content/adapters/tags/getTags.ts | 2 +- src/ts/content/adapters/tags/parseTags.ts | 6 ++--- src/ts/content/adapters/tags/types.ts | 6 ++--- .../util/tagValidation/dialogs/insertTags.ts | 26 +++++++++---------- 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/ts/content/adapters/tags/getTags.ts b/src/ts/content/adapters/tags/getTags.ts index 5f1b88b7..c61c36a5 100644 --- a/src/ts/content/adapters/tags/getTags.ts +++ b/src/ts/content/adapters/tags/getTags.ts @@ -40,7 +40,7 @@ const rowToSheetDescriptor = ([sheet, topLeft, width, cwColumn, userMapper, as]: const range: Range = `${sheet}!${topLeft}:${right}`; const cwRange: Range = cwColumn ? `${sheet}!${cwColumn}${top}:${cwColumn}` : undefined; const mapper: TagMapper = (userMapper ?? "").includes("$TAG") ? (userMapper as TagMapper) : "$TAG"; - return {range, mapper, cwRange, name: as ?? sheet}; + return {range, mapper, cwRange, height: Number(width), name: as ?? sheet}; }; const getSheetDescriptors = async (): Promise => { diff --git a/src/ts/content/adapters/tags/parseTags.ts b/src/ts/content/adapters/tags/parseTags.ts index 3c8ecd51..f2fd05fe 100644 --- a/src/ts/content/adapters/tags/parseTags.ts +++ b/src/ts/content/adapters/tags/parseTags.ts @@ -13,7 +13,7 @@ const parseRows = ({rows, fromRow, depth, nodes, parent}: ParserOptions): number while (row < rows.length) { const {tag, warning} = rows[row][depth] ?? {}; if (tag) { - const node = {name: tag, parent, warning, children: []}; + const node = {name: tag, parent, warning, children: [], height: parent.height - 1}; parent.children.push(node); // TODO if nodes already contains this tag, we need to emit a warning somehow!!! see #214 // Keys in the tag nodes map are lowercase for ez lookup later @@ -29,8 +29,8 @@ const parseRows = ({rows, fromRow, depth, nodes, parent}: ParserOptions): number const parseTags = (sheets: RawTagSheet[]) => { const nodes: TagNodes = new Map(); const roots = sheets.map((sheet) => { - const rows = sheet.values; - const parent = {...sheet, children: []}; + const {values: rows, ...rest} = sheet; + const parent: TagRoot = {...rest, children: []}; for (let fromRow = 0; fromRow < rows.length; fromRow = parseRows({rows, fromRow, depth: 0, nodes, parent}) + 1); return parent; }); diff --git a/src/ts/content/adapters/tags/types.ts b/src/ts/content/adapters/tags/types.ts index a7cc9df7..94586c1d 100644 --- a/src/ts/content/adapters/tags/types.ts +++ b/src/ts/content/adapters/tags/types.ts @@ -1,17 +1,17 @@ import {Range} from "../sheets"; type TagMapper = `${string}$TAG${string}`; -type TagSheetDescriptor = {range: Range; mapper: TagMapper; name: string; cwRange?: Range}; +type TagSheetDescriptor = {range: Range; mapper: TagMapper; name: string; cwRange?: Range; height: number}; type RawTagSheet = TagSheetDescriptor & {values: WarnedTag[][]}; -type TagRoot = RawTagSheet & {children: TagTree[]}; +type TagRoot = TagSheetDescriptor & {children: TagTree[]}; /** * A tag tree contains the properly cased tag at the root, * and a pointer to its parent in the tree */ -type TagTree = {name: string; parent: TagTree | TagRoot; warning: boolean; children: TagTree[]}; +type TagTree = {name: string; parent: TagTree | TagRoot; warning: boolean; children: TagTree[]; height: number}; /** * TagNodes is a map of lowercase tag to the tag subtree at that tag diff --git a/src/ts/content/extensions/util/tagValidation/dialogs/insertTags.ts b/src/ts/content/extensions/util/tagValidation/dialogs/insertTags.ts index 47cdc9fe..3b12da3f 100644 --- a/src/ts/content/extensions/util/tagValidation/dialogs/insertTags.ts +++ b/src/ts/content/extensions/util/tagValidation/dialogs/insertTags.ts @@ -38,33 +38,31 @@ const nodeToInsertionButton = onClick: async () => resolve(confirm(tag, remainingTags, node)), }); -const confirm = (tag: string, remainingTags: string[], node: TagRoot | TagTree) => - new Promise((resolve) => +const confirm = (tag: string, remainingTags: string[], node: TagRoot | TagTree) => { + const hasSubTag = node.height > 1 && node.children.length > 0; + const hasMultipleSubTags = hasSubTag && node.children.length > 1; + const oneOf = hasMultipleSubTags ? "one of " : ""; + const s = hasMultipleSubTags ? "s" : ""; + const subTagMessage = `Or as a sub tag under ${oneOf}the following existing tag${s}?`; + return new Promise((resolve) => createModal({ text: `Insert "${tag}" under "${node.name}"?`, + subText: hasSubTag ? [subTagMessage] : [], elements: [ { kind: "button", - text: "Yes", - colour: UIColour.GREY, + text: `Yes! Insert it under "${node.name}"`, + colour: UIColour.GREEN, onClick: async () => resolve(remainingTags.filter((otherTag) => otherTag !== tag)), }, - ...(node.children.length // TODO this kinda but better and checking for depth - ? [ - { - kind: "button" as const, - text: "Deeper", - colour: UIColour.BLUE, - onClick: async () => resolve(insertTagIntoOneOf(tag, remainingTags, node.children)), - }, - ] - : []), + ...(hasSubTag ? node.children.map(nodeToInsertionButton(tag, remainingTags, resolve)) : []), {kind: "button", text: "Back", colour: UIColour.RED, onClick: async () => resolve(remainingTags)}, ], colour: UIColour.BLUE, onCancel: async () => resolve(remainingTags), }) ); +}; const insertTagIntoOneOf = (tag: string, remainingTags: string[], options: Array) => new Promise((resolve) => From 6dc2a19f52c5bb0a7ed62aa8c0fbad8ead1e1efb Mon Sep 17 00:00:00 2001 From: braxtonhall Date: Fri, 13 Jan 2023 16:08:47 +0100 Subject: [PATCH 10/15] Delete unused Dialog ui element --- src/ts/common/ui/dialog.ts | 57 -------------------------------------- 1 file changed, 57 deletions(-) delete mode 100644 src/ts/common/ui/dialog.ts diff --git a/src/ts/common/ui/dialog.ts b/src/ts/common/ui/dialog.ts deleted file mode 100644 index a98b599c..00000000 --- a/src/ts/common/ui/dialog.ts +++ /dev/null @@ -1,57 +0,0 @@ -import {createModal, ModalButton, ModalInput} from "./modal"; -import {UIColour} from "./colour"; - -interface DialogOptions { - text: string; - subText?: string[]; - elements: E; - colour: UIColour; -} - -type DialogButton = Omit & {id: string}; -type DialogInput = Omit & {id: string}; -type DialogElement = DialogButton | DialogInput; - -type SelectionResponse = Selection extends DialogInput - ? {kind: DialogResponseKind.Input; id: string; value: string} - : Selection extends DialogButton - ? {kind: DialogResponseKind.Button; id: string} - : {kind: DialogResponseKind}; - -type DefaultResponse = {kind: DialogResponseKind.Cancelled}; - -enum DialogResponseKind { - Cancelled = "cancelled", - Button = "button", - Input = "input", -} - -const getButtonHandler = (resolve: (response) => void, id: string) => ({ - onClick: async () => resolve({kind: DialogResponseKind.Button, id}), -}); - -const getInputHandler = (resolve: (response) => void, id: string) => ({ - onSelect: async (userText: string) => resolve({kind: DialogResponseKind.Input, id, value: userText}), -}); - -const createModalElement = - (resolve: (response) => void) => - (element: DialogElement): ModalInput | ModalButton => { - return { - ...element, - ...(element.kind === "button" && getButtonHandler(resolve, element.id)), - ...(element.kind === "input" && getInputHandler(resolve, element.id)), - }; - }; - -const dialog = (options: DialogOptions) => - new Promise | DefaultResponse>((resolve) => - createModal({ - ...options, - elements: options.elements.map(createModalElement(resolve)), - onCancel: async () => resolve({kind: DialogResponseKind.Cancelled}), - }) - ); - -export {DialogButton, DialogInput}; -export {dialog}; From 1612057255c08d52506967f203181178a3469d63 Mon Sep 17 00:00:00 2001 From: braxtonhall Date: Fri, 13 Jan 2023 16:16:22 +0100 Subject: [PATCH 11/15] DP-750: Inline some code that's only called once --- .../util/tagValidation/dialogs/insertTags.ts | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/src/ts/content/extensions/util/tagValidation/dialogs/insertTags.ts b/src/ts/content/extensions/util/tagValidation/dialogs/insertTags.ts index 3b12da3f..f6010b04 100644 --- a/src/ts/content/extensions/util/tagValidation/dialogs/insertTags.ts +++ b/src/ts/content/extensions/util/tagValidation/dialogs/insertTags.ts @@ -64,20 +64,25 @@ const confirm = (tag: string, remainingTags: string[], node: TagRoot | TagTree) ); }; -const insertTagIntoOneOf = (tag: string, remainingTags: string[], options: Array) => - new Promise((resolve) => - createModal({ - text: "Where would you like to insert this tag?", - elements: [ - ...options.map(nodeToInsertionButton(tag, remainingTags, resolve)), - {kind: "button", text: "Back", colour: UIColour.RED, onClick: async () => resolve(remainingTags)}, - ], - colour: UIColour.BLUE, - onCancel: async () => resolve(remainingTags), - }) - ); - const insertTag = (tag: string, remainingTags: string[]): Promise => - loaderOverlaid(getTagTrees).then(({roots}) => insertTagIntoOneOf(tag, remainingTags, roots)); + loaderOverlaid(getTagTrees).then( + ({roots}) => + new Promise((resolve) => + createModal({ + text: "Where would you like to insert this tag?", + elements: [ + ...roots.map(nodeToInsertionButton(tag, remainingTags, resolve)), + { + kind: "button", + text: "Back", + colour: UIColour.RED, + onClick: async () => resolve(remainingTags), + }, + ], + colour: UIColour.BLUE, + onCancel: async () => resolve(remainingTags), + }) + ) + ); export {insertTags}; From 7cd39e9b33265b2439ae25f7310d884f242174f8 Mon Sep 17 00:00:00 2001 From: braxtonhall Date: Sun, 15 Jan 2023 00:53:21 +0100 Subject: [PATCH 12/15] Trim tags that you read from sheets --- src/ts/content/adapters/tags/parseTags.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ts/content/adapters/tags/parseTags.ts b/src/ts/content/adapters/tags/parseTags.ts index f2fd05fe..b36f3cb2 100644 --- a/src/ts/content/adapters/tags/parseTags.ts +++ b/src/ts/content/adapters/tags/parseTags.ts @@ -13,11 +13,12 @@ const parseRows = ({rows, fromRow, depth, nodes, parent}: ParserOptions): number while (row < rows.length) { const {tag, warning} = rows[row][depth] ?? {}; if (tag) { - const node = {name: tag, parent, warning, children: [], height: parent.height - 1}; + const trimmedTag = tag.trim(); + const node = {name: trimmedTag, parent, warning, children: [], height: parent.height - 1}; parent.children.push(node); // TODO if nodes already contains this tag, we need to emit a warning somehow!!! see #214 // Keys in the tag nodes map are lowercase for ez lookup later - nodes.set(tag.toLowerCase(), node); + nodes.set(trimmedTag.toLowerCase(), node); row = parseRows({rows, fromRow: row + 1, depth: depth + 1, nodes, parent: node}); } else { break; From 6c02912488dcfdeec067113ac8cc0326cad2ff1a Mon Sep 17 00:00:00 2001 From: braxtonhall Date: Sun, 15 Jan 2023 00:54:12 +0100 Subject: [PATCH 13/15] Comment out tag insertion for now --- .../util/tagValidation/dialogs/userAcceptance.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ts/content/extensions/util/tagValidation/dialogs/userAcceptance.ts b/src/ts/content/extensions/util/tagValidation/dialogs/userAcceptance.ts index 3f76e05e..ea8e71bb 100644 --- a/src/ts/content/extensions/util/tagValidation/dialogs/userAcceptance.ts +++ b/src/ts/content/extensions/util/tagValidation/dialogs/userAcceptance.ts @@ -1,8 +1,8 @@ import {createModal} from "../../../../../common/ui/modal"; import {UIColour} from "../../../../../common/ui/colour"; -import {insertTags} from "./insertTags"; import {getSheetLink} from "../../../../../common/entities/spreadsheet"; import {GetTagsOptions} from "../types"; +// import {insertTags} from "./insertTags"; const getUserAcceptance = ( invalidTags: string[], @@ -13,12 +13,12 @@ const getUserAcceptance = ( text: "Are you sure? The following tags are not in the Tag Index", subText: invalidTags, elements: [ - { - kind: "button", - text: "Insert Tags", - colour: UIColour.GREY, - onClick: async () => resolve(insertTags(invalidTags).then(() => saveHandler({noCache: true}))), - }, + // { + // kind: "button", + // text: "Insert Tags", + // colour: UIColour.GREY, + // onClick: async () => resolve(insertTags(invalidTags).then(() => saveHandler({noCache: true}))), + // }, { kind: "button", text: "Open the Tag Index", From 097991e8283907dfa4bc97c6e90b42698c0cbd6e Mon Sep 17 00:00:00 2001 From: braxtonhall Date: Sun, 15 Jan 2023 00:55:04 +0100 Subject: [PATCH 14/15] #212 Rename the confirmation method becuase it doesn't just do confirmation --- .../extensions/util/tagValidation/dialogs/insertTags.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ts/content/extensions/util/tagValidation/dialogs/insertTags.ts b/src/ts/content/extensions/util/tagValidation/dialogs/insertTags.ts index f6010b04..de181a54 100644 --- a/src/ts/content/extensions/util/tagValidation/dialogs/insertTags.ts +++ b/src/ts/content/extensions/util/tagValidation/dialogs/insertTags.ts @@ -35,10 +35,10 @@ const nodeToInsertionButton = kind: "button", text: node.name, colour: UIColour.BLUE, - onClick: async () => resolve(confirm(tag, remainingTags, node)), + onClick: async () => resolve(insertTagUnder(tag, remainingTags, node)), }); -const confirm = (tag: string, remainingTags: string[], node: TagRoot | TagTree) => { +const insertTagUnder = (tag: string, remainingTags: string[], node: TagRoot | TagTree) => { const hasSubTag = node.height > 1 && node.children.length > 0; const hasMultipleSubTags = hasSubTag && node.children.length > 1; const oneOf = hasMultipleSubTags ? "one of " : ""; From 32a2ba4f5c7d68fdc49cbafc92d5b44916dd8abc Mon Sep 17 00:00:00 2001 From: braxtonhall Date: Sun, 15 Jan 2023 11:03:43 +0100 Subject: [PATCH 15/15] #267 Remove unused code --- .../util/tagValidation/dialogs/userAcceptance.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/ts/content/extensions/util/tagValidation/dialogs/userAcceptance.ts b/src/ts/content/extensions/util/tagValidation/dialogs/userAcceptance.ts index ea8e71bb..f1591f6c 100644 --- a/src/ts/content/extensions/util/tagValidation/dialogs/userAcceptance.ts +++ b/src/ts/content/extensions/util/tagValidation/dialogs/userAcceptance.ts @@ -2,7 +2,6 @@ import {createModal} from "../../../../../common/ui/modal"; import {UIColour} from "../../../../../common/ui/colour"; import {getSheetLink} from "../../../../../common/entities/spreadsheet"; import {GetTagsOptions} from "../types"; -// import {insertTags} from "./insertTags"; const getUserAcceptance = ( invalidTags: string[], @@ -13,12 +12,6 @@ const getUserAcceptance = ( text: "Are you sure? The following tags are not in the Tag Index", subText: invalidTags, elements: [ - // { - // kind: "button", - // text: "Insert Tags", - // colour: UIColour.GREY, - // onClick: async () => resolve(insertTags(invalidTags).then(() => saveHandler({noCache: true}))), - // }, { kind: "button", text: "Open the Tag Index",