mirror of
https://github.com/oxynote/oxynote.git
synced 2026-10-17 02:49:18 +02:00
add tag deletion modal
This commit is contained in:
parent
b126237a98
commit
70728413a7
|
|
@ -400,6 +400,25 @@ describe("<AppSidebar>", { concurrent: false }, () => {
|
|||
expect(sidebar(wrapper).emitted("initial-load-complete")).toHaveLength(1)
|
||||
})
|
||||
|
||||
it("announces that every section has loaded with every tag hidden", async ({
|
||||
expect,
|
||||
}) => {
|
||||
// a reload hands the cache its answers before the sidebar is built,
|
||||
// so the tags section is there from the first render — and with the
|
||||
// last visible tag hidden it has no rows to report
|
||||
stubQueries()
|
||||
seedQueryData(["documents", "tree"], [treeElement()])
|
||||
seedQueryData(
|
||||
["tags", "tree"],
|
||||
[{ ...tagElement(TAG_A, "Production", "#22c55e"), hidden: true }],
|
||||
)
|
||||
|
||||
const wrapper = await mountSidebar()
|
||||
await settleMutations()
|
||||
|
||||
expect(sidebar(wrapper).emitted("initial-load-complete")).toHaveLength(1)
|
||||
})
|
||||
|
||||
describe("next steps", { concurrent: false }, () => {
|
||||
it("invites the user to add team members while alone", async ({
|
||||
expect,
|
||||
|
|
@ -714,7 +733,9 @@ describe("<AppSidebar>", { concurrent: false }, () => {
|
|||
expect(calls[0]?.body).toEqual({ hidden: true })
|
||||
})
|
||||
|
||||
it("deletes a tag from its options menu", async ({ expect }) => {
|
||||
it("asks the page to delete a tag from its options menu", async ({
|
||||
expect,
|
||||
}) => {
|
||||
stubQueries({ tags: seededTags() })
|
||||
const calls = mockEndpoint("DELETE", `/api/tags/${TAG_A}`, () => ({}))
|
||||
const wrapper = await mountSidebar()
|
||||
|
|
@ -724,7 +745,12 @@ describe("<AppSidebar>", { concurrent: false }, () => {
|
|||
menuItem(t("sidebar.item-dropdown-menu-buttons.delete-tag")).click()
|
||||
await settleMutations()
|
||||
|
||||
expect(calls).toHaveLength(1)
|
||||
// the modal outlives the sidebar's own drawer on a narrow screen,
|
||||
// so the page owns it and the deletion with it
|
||||
expect(sidebar(wrapper).emitted("delete-tag")).toEqual([
|
||||
[{ id: TAG_A, name: "Production" }],
|
||||
])
|
||||
expect(calls).toHaveLength(0)
|
||||
})
|
||||
|
||||
// the tree lists a document under a tag by its default branch, so
|
||||
|
|
|
|||
|
|
@ -21,18 +21,14 @@ const emit = defineEmits<{
|
|||
(e: "initial-load-complete" | "toggle-notifications"): void
|
||||
(e: "open-settings", target: "org-members" | null): void
|
||||
(e: "delete-document" | "duplicate-document", id: string): void
|
||||
(e: "delete-tag", tag: { id: string; name: string }): void
|
||||
(e: "create-document", parentId: string | null): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n({ useScope: "global" })
|
||||
const { fetchDocumentTree, updateDocumentTree } = useDocumentAPI()
|
||||
const {
|
||||
fetchTagTree,
|
||||
updateTagTree,
|
||||
updateTagVisibility,
|
||||
deleteTag,
|
||||
unassignBranchTag,
|
||||
} = useTagAPI()
|
||||
const { fetchTagTree, updateTagTree, updateTagVisibility, unassignBranchTag } =
|
||||
useTagAPI()
|
||||
const { fetchOrganization, safeSignOut } = useAuthSession()
|
||||
const { fetchGitHubConnectionStatus, gitHubConfigured, fetchGitHubInstallURL } =
|
||||
useGitHubAPI()
|
||||
|
|
@ -131,6 +127,9 @@ const sections = computed<Section[]>(() => {
|
|||
rootItems.push({
|
||||
id: "tags",
|
||||
heading: t("sidebar.sections.tags.heading"),
|
||||
// the tree has answered by the time this section exists, so no
|
||||
// rows means every tag is hidden rather than still on its way
|
||||
isEmptyAfterLoad: true,
|
||||
emptyMessage: t("sidebar.sections.tags.all-hidden"),
|
||||
items: tagItems,
|
||||
onUpdateLocation: handleTagLocationUpdate,
|
||||
|
|
@ -351,8 +350,8 @@ function tagActions(tag: TagTreeElement): SidebarItemAction[] {
|
|||
id: "delete-tag",
|
||||
name: t("sidebar.item-dropdown-menu-buttons.delete-tag"),
|
||||
icon: "lucide:trash-2",
|
||||
fn: async () => {
|
||||
await handleTagDelete(tag.id)
|
||||
fn: () => {
|
||||
emit("delete-tag", { id: tag.id, name: tag.tagName })
|
||||
},
|
||||
},
|
||||
]
|
||||
|
|
@ -383,14 +382,6 @@ async function handleTagVisibilityUpdate(id: string, req: { hidden: boolean }) {
|
|||
}
|
||||
}
|
||||
|
||||
async function handleTagDelete(id: string) {
|
||||
try {
|
||||
await deleteTag.mutateAsync(id)
|
||||
} catch {
|
||||
showToastMessage("error", t("sidebar.errors.delete-tag-failed"))
|
||||
}
|
||||
}
|
||||
|
||||
// the tree lists a document under a tag by its default branch, so that is
|
||||
// the branch the row's action detaches
|
||||
async function handleDocumentTagRemoval(
|
||||
|
|
@ -506,7 +497,7 @@ async function installSlack() {
|
|||
<ShadcnUiSidebarGroupContent>
|
||||
<div
|
||||
v-if="section.emptyMessage && !section.items.length"
|
||||
class="px-2 py-1.25 text-2sm text-sidebar-foreground/60"
|
||||
class="flex h-7 items-center px-2 text-2sm text-sidebar-foreground/60"
|
||||
>
|
||||
{{ section.emptyMessage }}
|
||||
</div>
|
||||
|
|
|
|||
114
web/app/components/TagDeletionModal.nuxt.test.ts
Normal file
114
web/app/components/TagDeletionModal.nuxt.test.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import { mountSuspended } from "@nuxt/test-utils/runtime"
|
||||
import { beforeEach, describe, it, vi } from "vitest"
|
||||
import TagDeletionModal from "./TagDeletionModal.vue"
|
||||
import { clearTeleportedOverlays, t, teleportedButton } from "./test-helpers"
|
||||
|
||||
function mountModal(
|
||||
target: { deleteTag: () => Promise<void>; name: string } | null,
|
||||
) {
|
||||
return mountSuspended(TagDeletionModal, {
|
||||
props: { modelValue: target },
|
||||
})
|
||||
}
|
||||
|
||||
// the dialog body is teleported into <body>, so it is the only place the
|
||||
// modal's own markup can be read from
|
||||
function dialog() {
|
||||
return document.body.querySelector("[data-slot='dialog-content']")
|
||||
}
|
||||
|
||||
// the dialog body is teleported into the shared <body> and the delete flow
|
||||
// is driven by the global fake timers, so these tests cannot interleave
|
||||
// t() reaches for the nuxt app context, so the label can only be
|
||||
// resolved inside a test — not once at module scope
|
||||
function confirmButton() {
|
||||
return teleportedButton(t("sidebar.tag-deletion-modal.confirm-button"))
|
||||
}
|
||||
|
||||
describe("<TagDeletionModal>", { concurrent: false }, () => {
|
||||
beforeEach(clearTeleportedOverlays)
|
||||
|
||||
it("stays closed while there is no deletion target", async ({ expect }) => {
|
||||
await mountModal(null)
|
||||
|
||||
expect(dialog()).toBeNull()
|
||||
})
|
||||
|
||||
it("names the tag it is about to delete", async ({ expect }) => {
|
||||
await mountModal({ name: "Production", deleteTag: vi.fn() })
|
||||
|
||||
expect(dialog()?.textContent).toContain("Production")
|
||||
})
|
||||
|
||||
it("deletes the tag when the confirm button is pressed", async ({
|
||||
expect,
|
||||
}) => {
|
||||
vi.useFakeTimers()
|
||||
const deleteTag = vi.fn().mockResolvedValue(undefined)
|
||||
const wrapper = await mountModal({ name: "Production", deleteTag })
|
||||
|
||||
confirmButton().click()
|
||||
await vi.advanceTimersByTimeAsync(300)
|
||||
|
||||
expect(deleteTag).toHaveBeenCalledTimes(1)
|
||||
expect(wrapper.emitted("update:modelValue")?.at(-1)).toEqual([null])
|
||||
})
|
||||
|
||||
it("shows a spinner and disables both buttons while deleting", async ({
|
||||
expect,
|
||||
}) => {
|
||||
vi.useFakeTimers()
|
||||
const deleteTag = vi.fn().mockResolvedValue(undefined)
|
||||
await mountModal({ name: "Production", deleteTag })
|
||||
|
||||
confirmButton().click()
|
||||
await nextTick()
|
||||
|
||||
expect(
|
||||
dialog()?.querySelector(".i-svg-spinners\\:blocks-shuffle-3"),
|
||||
).not.toBeNull()
|
||||
expect(confirmButton().disabled).toBe(true)
|
||||
expect(
|
||||
teleportedButton(t("sidebar.tag-deletion-modal.cancel-button")).disabled,
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it("closes without deleting when the cancel button is pressed", async ({
|
||||
expect,
|
||||
}) => {
|
||||
const deleteTag = vi.fn()
|
||||
const wrapper = await mountModal({ name: "Production", deleteTag })
|
||||
|
||||
teleportedButton(t("sidebar.tag-deletion-modal.cancel-button")).click()
|
||||
await nextTick()
|
||||
|
||||
expect(deleteTag).toHaveBeenCalledTimes(0)
|
||||
expect(wrapper.emitted("update:modelValue")?.at(-1)).toEqual([null])
|
||||
})
|
||||
|
||||
it("closes without deleting when the close button is pressed", async ({
|
||||
expect,
|
||||
}) => {
|
||||
const deleteTag = vi.fn()
|
||||
const wrapper = await mountModal({ name: "Production", deleteTag })
|
||||
|
||||
teleportedButton(t("general.modal-close-screen-reader-hint")).click()
|
||||
await nextTick()
|
||||
|
||||
expect(deleteTag).toHaveBeenCalledTimes(0)
|
||||
expect(wrapper.emitted("update:modelValue")?.at(-1)).toEqual([null])
|
||||
})
|
||||
|
||||
it("keeps showing the tag name while the dialog animates shut", async ({
|
||||
expect,
|
||||
}) => {
|
||||
const wrapper = await mountModal({
|
||||
name: "Production",
|
||||
deleteTag: vi.fn(),
|
||||
})
|
||||
|
||||
await wrapper.setProps({ modelValue: null })
|
||||
|
||||
expect(document.body.textContent).toContain("Production")
|
||||
})
|
||||
})
|
||||
104
web/app/components/TagDeletionModal.vue
Normal file
104
web/app/components/TagDeletionModal.vue
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
<script lang="ts" setup>
|
||||
const open = defineModel<{
|
||||
deleteTag: () => Promise<void>
|
||||
name: string
|
||||
} | null>({
|
||||
default: null,
|
||||
})
|
||||
|
||||
const lastValidTarget = useLastValidRef(open)
|
||||
const loading = ref(false)
|
||||
|
||||
async function deleteTag() {
|
||||
if (!lastValidTarget.value || !open.value) {
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
await delay(300) // show loading spinner for at least a moment
|
||||
await lastValidTarget.value.deleteTag()
|
||||
open.value = null
|
||||
|
||||
setTimeout(() => {
|
||||
// just to ensure that the spinner is visible until the modal closes
|
||||
loading.value = false
|
||||
}, 500)
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<ShadcnUiDialog
|
||||
:open="!!open"
|
||||
@update:open="
|
||||
(v) => {
|
||||
if (!v) {
|
||||
open = null
|
||||
}
|
||||
}
|
||||
"
|
||||
>
|
||||
<ShadcnUiDialogContent
|
||||
class="max-h-[90dvh] w-[85dvw] overflow-y-auto p-0 text-foreground sm:w-110 md:max-h-[80dvh]"
|
||||
>
|
||||
<div class="flex flex-col gap-8 p-6">
|
||||
<div class="flex min-h-0 flex-col gap-3">
|
||||
<ShadcnUiDialogHeader>
|
||||
<ShadcnUiDialogTitle class="text-base">
|
||||
{{ $t("sidebar.tag-deletion-modal.title") }}
|
||||
</ShadcnUiDialogTitle>
|
||||
<ShadcnUiButton
|
||||
variant="ghost-plain"
|
||||
class="absolute top-1/2 right-0 shrink-0 -translate-y-1/2 p-0"
|
||||
@click="open = null"
|
||||
>
|
||||
<Icon name="lucide:x" size="1rem" />
|
||||
<span class="sr-only">
|
||||
{{ $t("general.modal-close-screen-reader-hint") }}
|
||||
</span>
|
||||
</ShadcnUiButton>
|
||||
</ShadcnUiDialogHeader>
|
||||
<div class="flex flex-col gap-2 self-stretch">
|
||||
<i18n-t
|
||||
scope="global"
|
||||
keypath="sidebar.tag-deletion-modal.description"
|
||||
tag="p"
|
||||
class="text-2sm text-muted-foreground"
|
||||
>
|
||||
<template #name>
|
||||
<span class="font-medium break-all">
|
||||
{{ lastValidTarget?.name }}
|
||||
</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
<div class="flex gap-2 self-stretch">
|
||||
<ShadcnUiButton
|
||||
type="button"
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
:disabled="loading"
|
||||
class="text-2sm"
|
||||
@click="deleteTag"
|
||||
>
|
||||
<Icon
|
||||
v-show="loading"
|
||||
name="svg-spinners:blocks-shuffle-3"
|
||||
class="size-3"
|
||||
/>
|
||||
{{ $t("sidebar.tag-deletion-modal.confirm-button") }}
|
||||
</ShadcnUiButton>
|
||||
<ShadcnUiButton
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
:disabled="loading"
|
||||
class="text-2sm"
|
||||
@click="open = null"
|
||||
>
|
||||
{{ $t("sidebar.tag-deletion-modal.cancel-button") }}
|
||||
</ShadcnUiButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ShadcnUiDialogContent>
|
||||
</ShadcnUiDialog>
|
||||
</template>
|
||||
|
|
@ -4,6 +4,7 @@ import {
|
|||
clearQueryCache,
|
||||
disposeMockEndpoints,
|
||||
makeXid,
|
||||
mockDeferredEndpoint,
|
||||
mockEndpoint,
|
||||
runInApp,
|
||||
} from "~/composables/api/test-helpers"
|
||||
|
|
@ -447,6 +448,22 @@ describe("<DocumentTagList>", { concurrent: false }, () => {
|
|||
expect(pickerRows().filter((row) => row.includes("Production"))).toEqual([])
|
||||
})
|
||||
|
||||
it("reopens with the search box empty", async ({ expect }) => {
|
||||
stubTags([
|
||||
makeTag(TAG_A, "Production", "#1a9e4a"),
|
||||
makeTag(TAG_B, "Staging", "#e8760c"),
|
||||
])
|
||||
const wrapper = await mountTags()
|
||||
await openPicker(wrapper)
|
||||
await search("stag")
|
||||
|
||||
await openPicker(wrapper)
|
||||
await openPicker(wrapper)
|
||||
|
||||
expect(pickerInput().value).toBe("")
|
||||
expect(pickerRows()).toEqual(["Production", "Staging"])
|
||||
})
|
||||
|
||||
it("hints at creation while nothing is typed", async ({ expect }) => {
|
||||
stubTags([makeTag(TAG_A, "Production", "#1a9e4a")])
|
||||
const wrapper = await mountTags()
|
||||
|
|
@ -512,6 +529,43 @@ describe("<DocumentTagList>", { concurrent: false }, () => {
|
|||
expect(assigned[0]?.body).toEqual({ tagId: TAG_B })
|
||||
})
|
||||
|
||||
it("keeps the other tags listed while the new one is still in flight", async ({
|
||||
expect,
|
||||
}) => {
|
||||
// the create settles only once the refetch behind it has, so a slow
|
||||
// request leaves the list on screen for as long as it takes
|
||||
stubTags([makeTag(TAG_A, "Production", "#1a9e4a")])
|
||||
const created = mockDeferredEndpoint("POST", "/api/tags")
|
||||
const wrapper = await mountTags()
|
||||
await openPicker(wrapper)
|
||||
await search("Rollout")
|
||||
|
||||
pressEnter()
|
||||
await created.reached
|
||||
await nextTick()
|
||||
|
||||
expect(pickerRows()).toEqual(["Production", "Rollout"])
|
||||
})
|
||||
|
||||
it("scrolls the list down to the tag it just appended", async ({
|
||||
expect,
|
||||
}) => {
|
||||
// happy-dom lays nothing out, so the list reports a height for the
|
||||
// scroll to aim at
|
||||
vi.spyOn(HTMLElement.prototype, "scrollHeight", "get").mockReturnValue(500)
|
||||
stubTags([makeTag(TAG_A, "Production", "#1a9e4a")])
|
||||
mockEndpoint("POST", "/api/tags", () => ({ id: TAG_B }))
|
||||
mockEndpoint("POST", BRANCH_TAGS_URL, () => ({}))
|
||||
const wrapper = await mountTags()
|
||||
await openPicker(wrapper)
|
||||
await search("Rollout")
|
||||
|
||||
pressEnter()
|
||||
await nextTick()
|
||||
|
||||
expect(pickerItems()[0]?.parentElement?.scrollTop).toBe(500)
|
||||
})
|
||||
|
||||
it("creates nothing on enter for a name that already exists", async ({
|
||||
expect,
|
||||
}) => {
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ const documentTags = computed(() => {
|
|||
|
||||
const rowElem = useTemplateRef<HTMLElement>("tag-row")
|
||||
const triggerElem = useTemplateRef<HTMLElement>("tag-trigger")
|
||||
const listElem = useTemplateRef<HTMLElement>("tag-list")
|
||||
const rowParentElem = computed(() => rowElem.value?.parentElement ?? null)
|
||||
// the count settles by trying one more pill until the row overflows, then
|
||||
// stepping back. tooMany remembers the count that overflowed, so the two
|
||||
|
|
@ -115,14 +116,16 @@ useResizeObserver([rowElem, rowParentElem], () => {
|
|||
void measureOnResize()
|
||||
})
|
||||
|
||||
// the search is emptied on the way in rather than on the way out: the
|
||||
// list is still on screen while the menu fades, and clearing it then
|
||||
// shows the rows springing back to their full length mid-close
|
||||
watch(pickerOpen, (isOpen) => {
|
||||
if (isOpen) {
|
||||
newColor.value = suggestedColor()
|
||||
|
||||
if (!isOpen) {
|
||||
return
|
||||
}
|
||||
|
||||
query.value = ""
|
||||
newColor.value = suggestedColor()
|
||||
})
|
||||
|
||||
// the swatches come from theme variables, which resolve to oklch, while a
|
||||
|
|
@ -205,21 +208,31 @@ async function toggleTag(tag: TagTreeElement) {
|
|||
async function createAndAssign() {
|
||||
const documentId = editorStore.activeDocumentId
|
||||
const branchId = editorStore.activeBranchId
|
||||
if (!documentId || !branchId || !trimmedQuery.value) {
|
||||
const tagName = trimmedQuery.value
|
||||
if (!documentId || !branchId || !tagName) {
|
||||
return
|
||||
}
|
||||
|
||||
// the search box is emptied before the request rather than after it.
|
||||
// mutateAsync settles only once the refetch its success triggers has,
|
||||
// and until then the typed name narrows the list to the one tag that
|
||||
// matches it — the one just created
|
||||
query.value = ""
|
||||
|
||||
try {
|
||||
const created = await createTag.mutateAsync({
|
||||
tagName: trimmedQuery.value,
|
||||
const creating = createTag.mutateAsync({
|
||||
tagName: tagName,
|
||||
// the swatches resolve from theme variables, which are oklch; the
|
||||
// stored colour is hex so it survives a theme change
|
||||
color: colorToHex(
|
||||
newColor.value ?? chartStyles().selectableColors.default,
|
||||
),
|
||||
})
|
||||
// the tag is appended, so the row it adds is below the fold on a
|
||||
// list long enough to scroll. onMutate has already put it there
|
||||
void nextTick(scrollListToEnd)
|
||||
|
||||
query.value = ""
|
||||
const created = await creating
|
||||
// the tag just created holds a colour now, so the next one drawn has
|
||||
// to weigh it in — the menu stays open across several creations
|
||||
newColor.value = suggestedColor()
|
||||
|
|
@ -234,6 +247,15 @@ async function createAndAssign() {
|
|||
}
|
||||
}
|
||||
|
||||
function scrollListToEnd() {
|
||||
const list = listElem.value
|
||||
if (!list) {
|
||||
return
|
||||
}
|
||||
|
||||
list.scrollTop = list.scrollHeight
|
||||
}
|
||||
|
||||
// the create row is the only thing enter can act on: an existing tag is
|
||||
// toggled from its own row, and a name already taken leaves nothing to
|
||||
// create
|
||||
|
|
@ -307,7 +329,7 @@ function handleSearchEnter() {
|
|||
/>
|
||||
<ShadcnUiDropdownMenuSeparator />
|
||||
<template v-if="matchingTags.length">
|
||||
<div class="max-h-47.5 overflow-y-auto">
|
||||
<div ref="tag-list" class="max-h-47.5 overflow-y-auto">
|
||||
<ShadcnUiDropdownMenuItem
|
||||
v-for="tag in matchingTags"
|
||||
:key="tag.id"
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ const {
|
|||
duplicateDocument,
|
||||
useFetchDocumentBranchesByDocId,
|
||||
} = useDocumentAPI()
|
||||
const { deleteTag } = useTagAPI()
|
||||
const { fetchOrganization } = useAuthSession()
|
||||
const { t } = useI18n({ useScope: "global" })
|
||||
const { setEditable } = useEditorMeta()
|
||||
|
|
@ -150,6 +151,10 @@ const pendingDocDeletion = ref<{
|
|||
deleteDocument: () => Promise<void>
|
||||
name: string
|
||||
} | null>(null)
|
||||
const pendingTagDeletion = ref<{
|
||||
deleteTag: () => Promise<void>
|
||||
name: string
|
||||
} | null>(null)
|
||||
const notificationSidebarOpen = ref(false)
|
||||
const contentEditorRef = shallowRef<Editor | null>(null)
|
||||
const nameEditorRef = shallowRef<Editor | null>(null)
|
||||
|
|
@ -509,6 +514,19 @@ function handleDocumentDeletion(id: string | null | undefined) {
|
|||
}
|
||||
}
|
||||
|
||||
function handleTagDeletion(tag: { id: string; name: string }) {
|
||||
pendingTagDeletion.value = {
|
||||
name: tag.name,
|
||||
deleteTag: async () => {
|
||||
try {
|
||||
await deleteTag.mutateAsync(tag.id)
|
||||
} catch {
|
||||
showToastMessage("error", t("sidebar.errors.delete-tag-failed"))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDocumentDuplication(id?: string) {
|
||||
if (!id) {
|
||||
id = activeDocMetadata.value?.id
|
||||
|
|
@ -586,6 +604,7 @@ function clearLinkHighlightNodeOnce() {
|
|||
@create-document="handleDocumentCreation"
|
||||
@duplicate-document="handleDocumentDuplication"
|
||||
@delete-document="handleDocumentDeletion"
|
||||
@delete-tag="handleTagDeletion"
|
||||
@open-settings="
|
||||
(target) =>
|
||||
target === 'org-members'
|
||||
|
|
@ -610,6 +629,7 @@ function clearLinkHighlightNodeOnce() {
|
|||
v-if="activeDocMetadata && allSectionsLoaded"
|
||||
v-model="pendingDocDeletion"
|
||||
/>
|
||||
<TagDeletionModal v-model="pendingTagDeletion" />
|
||||
<main class="w-full min-w-0 bg-background">
|
||||
<EditorIconPickerProvider>
|
||||
<DocumentHeader
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
"tags": {
|
||||
"heading": "Tags",
|
||||
"visibility-action-title": "Choose Visible Tags",
|
||||
"all-hidden": "Every tag is hidden"
|
||||
"all-hidden": "All tags are hidden"
|
||||
},
|
||||
"next-steps": {
|
||||
"heading": "Next Steps",
|
||||
|
|
@ -52,6 +52,12 @@
|
|||
},
|
||||
"item-placeholder-name": "Add Page",
|
||||
"toggle-sidebar": "Toggle Sidebar",
|
||||
"tag-deletion-modal": {
|
||||
"title": "Delete Tag",
|
||||
"description": "Are you sure you want to delete {name}? It will be removed from every page using it. This action cannot be undone.",
|
||||
"confirm-button": "Delete tag",
|
||||
"cancel-button": "Cancel"
|
||||
},
|
||||
"errors": {
|
||||
"create-document-failed": "Failed to create a page. Please try again",
|
||||
"update-tree-failed": "Failed to update the page location. Please try again",
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user