mirror of
https://github.com/oxynote/oxynote.git
synced 2026-10-18 03:19:19 +02:00
438 lines
13 KiB
Vue
438 lines
13 KiB
Vue
<script lang="ts" setup>
|
|
import { cn } from "~/lib/utils"
|
|
import type { DocumentBreadcrumb } from "./sidebar"
|
|
import { showToastMessage } from "./toast"
|
|
|
|
const props = defineProps<{
|
|
allInitialSectionsLoaded: boolean
|
|
breadcrumbs: DocumentBreadcrumb[] | null // null indicates that the document is still loading or missing
|
|
timestamps: DocumentTimestamps | null // null indicates that the document is still loading or missing
|
|
}>()
|
|
const emit = defineEmits<{
|
|
(e: "delete-document" | "duplicate-document"): void
|
|
}>()
|
|
|
|
const {
|
|
isEditable,
|
|
isBranchProtected,
|
|
toggleIsEditable,
|
|
isCompactView,
|
|
toggleCompactView,
|
|
} = useEditorMeta()
|
|
const editorStore = useEditorStore()
|
|
const { isAssistantEnabled } = useCapabilitiesAPI()
|
|
const { useFetchDocumentBranchesByDocId } = useDocumentAPI()
|
|
const fetchBranches = useFetchDocumentBranchesByDocId(
|
|
() => editorStore.activeDocumentId,
|
|
)
|
|
const header = useTemplateRef("header")
|
|
const { height } = useElementSize(header)
|
|
const { t } = useI18n({ useScope: "global" })
|
|
const { updateDocumentBranch, createDocumentBranch, deleteDocumentBranch } =
|
|
useDocumentAPI()
|
|
const isReviewable = computed(() => {
|
|
// NOTE this will be removed in the future
|
|
//
|
|
// a branch that is still being created has a temporary id and does not
|
|
// count. A switch to it would be undone once the server answers
|
|
const branches = fetchBranches.state.value.data
|
|
return !!branches && branches.filter((b) => isXid(b.branchId)).length > 1
|
|
})
|
|
const modeTooltip = computed(() => {
|
|
if (isBranchProtected.value) {
|
|
return [
|
|
t("editor.navbar.mode-tooltip.protected-line-1"),
|
|
t("editor.navbar.mode-tooltip.protected-line-2"),
|
|
]
|
|
}
|
|
|
|
if (isEditable.value) {
|
|
return [
|
|
t("editor.navbar.mode-tooltip.edit-line-1"),
|
|
t("editor.navbar.mode-tooltip.edit-line-2"),
|
|
]
|
|
}
|
|
|
|
return [
|
|
t("editor.navbar.mode-tooltip.read-line-1"),
|
|
t("editor.navbar.mode-tooltip.read-line-2"),
|
|
]
|
|
})
|
|
const activeBranchMode = computed(() => {
|
|
// NOTE this will be removed in the future
|
|
const branches = fetchBranches.state.value.data
|
|
if (!branches) {
|
|
return "main"
|
|
}
|
|
|
|
const activeBranch = branches.find(
|
|
(b) => b.branchId === editorStore.activeBranchId,
|
|
)
|
|
if (!activeBranch) {
|
|
return "main"
|
|
}
|
|
|
|
return activeBranch.default ? "main" : "draft"
|
|
})
|
|
|
|
if (import.meta.client) {
|
|
watchImmediate(height, (v) => {
|
|
document.documentElement.style.setProperty(
|
|
"--document-header-height",
|
|
`${v}px`,
|
|
)
|
|
})
|
|
}
|
|
|
|
async function toggleReviewability() {
|
|
if (!editorStore.activeDocumentId || !editorStore.mappedDefaultBranchId) {
|
|
return
|
|
}
|
|
|
|
// NOTE: for now we can only have 1 or 2 branches
|
|
|
|
try {
|
|
if (!isReviewable.value) {
|
|
await createDocumentBranch.mutateAsync({
|
|
docId: editorStore.activeDocumentId,
|
|
req: {
|
|
branch: DOCUMENT_DRAFT_BRANCH_NAME,
|
|
sourceBranchId: editorStore.mappedDefaultBranchId,
|
|
},
|
|
})
|
|
await updateDocumentBranch.mutateAsync({
|
|
id: editorStore.activeDocumentId,
|
|
branchId: editorStore.mappedDefaultBranchId,
|
|
protectedMode: true,
|
|
})
|
|
} else {
|
|
// read once here: the narrowing from the guard above does not reach
|
|
// inside the map callback
|
|
const docId = editorStore.activeDocumentId
|
|
|
|
await Promise.all(
|
|
fetchBranches.state.value.data
|
|
?.filter((b) => !b.default)
|
|
.map((b) =>
|
|
deleteDocumentBranch.mutateAsync({
|
|
docId,
|
|
branchId: b.branchId,
|
|
}),
|
|
) ?? [],
|
|
)
|
|
|
|
await updateDocumentBranch.mutateAsync({
|
|
id: editorStore.activeDocumentId,
|
|
branchId: editorStore.mappedDefaultBranchId,
|
|
protectedMode: false,
|
|
})
|
|
}
|
|
} catch {
|
|
showToastMessage("error", t("editor.errors.reviewability-update-failed"))
|
|
}
|
|
}
|
|
|
|
function activateBranch(branch: "default" | "draft") {
|
|
if (!editorStore.activeDocumentId) {
|
|
return
|
|
}
|
|
|
|
const branches = fetchBranches.state.value.data
|
|
const defaultBranch = branches?.find((b) => b.default)
|
|
const draftBranch = branches?.find((b) => !b.default)
|
|
|
|
if (branch === "default" && defaultBranch) {
|
|
editorStore.updateActiveBranchId(defaultBranch.branchId)
|
|
editorStore.updateTargetBranchId(null)
|
|
} else if (branch === "draft" && draftBranch && defaultBranch) {
|
|
editorStore.updateActiveBranchId(draftBranch.branchId)
|
|
editorStore.updateTargetBranchId(defaultBranch.branchId)
|
|
}
|
|
}
|
|
</script>
|
|
<template>
|
|
<header
|
|
ref="header"
|
|
class="sticky top-0 z-navbar h-10 w-full border-b border-border bg-background pr-2 pl-2"
|
|
>
|
|
<Transition v-bind="defaultTransitionProps">
|
|
<div
|
|
v-if="props.allInitialSectionsLoaded"
|
|
class="flex h-full w-full items-center justify-between gap-4"
|
|
>
|
|
<div class="flex min-w-0 items-center gap-3">
|
|
<ShortcutTooltip
|
|
side="bottom"
|
|
:shortcut="SHORTCUT_ACTIONS.toggleSidebar"
|
|
>
|
|
<ShadcnUiSidebarTrigger />
|
|
</ShortcutTooltip>
|
|
<HeaderBreadcrumbs
|
|
v-if="editorStore.activeDocumentId && props.breadcrumbs"
|
|
:breadcrumbs="props.breadcrumbs"
|
|
/>
|
|
</div>
|
|
<div
|
|
v-if="editorStore.activeDocumentId && editorStore.activeBranchId"
|
|
class="flex items-center gap-2"
|
|
>
|
|
<ShadcnUiTooltip
|
|
v-if="props.timestamps?.[editorStore.activeBranchId]"
|
|
>
|
|
<ShadcnUiTooltipTrigger as-child>
|
|
<div
|
|
class="hidden text-sm whitespace-nowrap text-muted-foreground lg:block"
|
|
>
|
|
{{
|
|
$t("editor.navbar.timestamp.short-edited", {
|
|
time: $d(
|
|
props.timestamps[editorStore.activeBranchId]!.updated.at,
|
|
"month-day-short",
|
|
),
|
|
})
|
|
}}
|
|
</div>
|
|
</ShadcnUiTooltipTrigger>
|
|
<ShadcnUiTooltipContent side="bottom" align="end">
|
|
<div class="flex flex-col gap-1">
|
|
<i18n-t
|
|
scope="global"
|
|
keypath="editor.navbar.timestamp.full-edited"
|
|
tag="div"
|
|
>
|
|
<template #user>
|
|
<span class="font-bold">
|
|
{{
|
|
props.timestamps[editorStore.activeBranchId]!.updated
|
|
.user?.name || $t("general.deleted-user")
|
|
}}
|
|
</span>
|
|
</template>
|
|
<template #time>
|
|
{{
|
|
$d(
|
|
props.timestamps[editorStore.activeBranchId]!.updated
|
|
.at,
|
|
"month-day-short",
|
|
)
|
|
}}
|
|
</template>
|
|
</i18n-t>
|
|
<i18n-t
|
|
scope="global"
|
|
keypath="editor.navbar.timestamp.full-created"
|
|
tag="div"
|
|
>
|
|
<template #user>
|
|
<span class="font-bold">
|
|
{{
|
|
props.timestamps[editorStore.activeBranchId]!.created
|
|
.user?.name || $t("general.deleted-user")
|
|
}}
|
|
</span>
|
|
</template>
|
|
<template #time>
|
|
{{
|
|
$d(
|
|
props.timestamps[editorStore.activeBranchId]!.created
|
|
.at,
|
|
"month-day-short",
|
|
)
|
|
}}
|
|
</template>
|
|
</i18n-t>
|
|
</div>
|
|
</ShadcnUiTooltipContent>
|
|
</ShadcnUiTooltip>
|
|
<div class="flex items-center gap-px">
|
|
<ShadcnUiDropdownMenu v-if="isReviewable">
|
|
<ShadcnUiDropdownMenuTrigger as-child>
|
|
<ShadcnUiButton
|
|
variant="ghost"
|
|
size="sm"
|
|
:class="cn('h-fit gap-0.75 px-2 py-1')"
|
|
>
|
|
{{ $t(`general.branch-labels.${activeBranchMode}`) }}
|
|
<Icon name="lucide:chevron-down" class="mt-0.5" />
|
|
</ShadcnUiButton>
|
|
</ShadcnUiDropdownMenuTrigger>
|
|
<ShadcnUiDropdownMenuContent
|
|
side="bottom"
|
|
align="end"
|
|
loop
|
|
class="max-w-75"
|
|
>
|
|
<ShadcnUiDropdownMenuItem @click="activateBranch('default')">
|
|
<div class="flex flex-col gap-0.5">
|
|
<div class="text-2sm font-medium">
|
|
{{ $t(`general.branch-labels.main`) }}
|
|
</div>
|
|
<div class="text-xs text-muted-foreground">
|
|
{{ $t(`editor.navbar.document-modes.main.description`) }}
|
|
</div>
|
|
</div>
|
|
</ShadcnUiDropdownMenuItem>
|
|
<div class="mx-1.75 my-0.5 h-px bg-border last:hidden" />
|
|
<ShadcnUiDropdownMenuItem @click="activateBranch('draft')">
|
|
<div class="flex flex-col gap-0.5">
|
|
<div class="text-2sm font-medium">
|
|
{{ $t(`general.branch-labels.draft`) }}
|
|
</div>
|
|
<div class="text-xs text-muted-foreground">
|
|
{{ $t(`editor.navbar.document-modes.draft.description`) }}
|
|
</div>
|
|
</div>
|
|
</ShadcnUiDropdownMenuItem>
|
|
</ShadcnUiDropdownMenuContent>
|
|
</ShadcnUiDropdownMenu>
|
|
<ShadcnUiTooltip :delay-duration="600">
|
|
<ShadcnUiTooltipTrigger as-child>
|
|
<ShadcnUiButton
|
|
variant="ghost"
|
|
size="icon"
|
|
:class="
|
|
cn(
|
|
'h-7 w-7 cursor-pointer hover:bg-accent/50 active:bg-accent',
|
|
)
|
|
"
|
|
:disabled="isBranchProtected"
|
|
@click="toggleIsEditable"
|
|
>
|
|
<Icon
|
|
:name="
|
|
!isEditable ? 'lucide:book-open' : 'lucide:pencil-line'
|
|
"
|
|
/>
|
|
<span class="sr-only">
|
|
{{
|
|
!isEditable
|
|
? $t("editor.navbar.toggle-read-mode")
|
|
: $t("editor.navbar.toggle-edit-mode")
|
|
}}
|
|
</span>
|
|
</ShadcnUiButton>
|
|
</ShadcnUiTooltipTrigger>
|
|
<ShadcnUiTooltipContent side="bottom" align="end">
|
|
<div class="flex max-w-48 flex-col gap-2">
|
|
<div
|
|
v-for="line in modeTooltip"
|
|
:key="line"
|
|
class="break-normal whitespace-normal"
|
|
>
|
|
{{ line }}
|
|
</div>
|
|
</div>
|
|
</ShadcnUiTooltipContent>
|
|
</ShadcnUiTooltip>
|
|
<ShadcnUiDropdownMenu>
|
|
<ShadcnUiDropdownMenuTrigger as-child>
|
|
<ShadcnUiButton
|
|
variant="ghost"
|
|
size="icon"
|
|
:class="
|
|
cn(
|
|
'h-7 w-7 cursor-pointer hover:bg-accent/50 active:bg-accent',
|
|
)
|
|
"
|
|
>
|
|
<Icon name="lucide:ellipsis" />
|
|
<span class="sr-only">
|
|
{{ $t("editor.navbar.open-document-options") }}
|
|
</span>
|
|
</ShadcnUiButton>
|
|
</ShadcnUiDropdownMenuTrigger>
|
|
<ShadcnUiDropdownMenuContent side="bottom" align="end" loop>
|
|
<ShadcnUiDropdownMenuItem
|
|
v-if="editorStore.compactViewAvailable"
|
|
@click="toggleCompactView"
|
|
>
|
|
<Icon
|
|
:name="
|
|
isCompactView ? 'lucide:maximize-2' : 'lucide:minimize-2'
|
|
"
|
|
/>
|
|
<span>
|
|
{{
|
|
isCompactView
|
|
? $t("editor.navbar.document-options.view.full-title")
|
|
: $t(
|
|
"editor.navbar.document-options.view.compact-title",
|
|
)
|
|
}}
|
|
</span>
|
|
</ShadcnUiDropdownMenuItem>
|
|
<ShadcnUiDropdownMenuItem @click="toggleReviewability">
|
|
<Icon
|
|
:name="
|
|
isReviewable
|
|
? 'lucide:message-square-off'
|
|
: 'lucide:message-square-diff'
|
|
"
|
|
/>
|
|
<span>
|
|
{{
|
|
isReviewable
|
|
? $t(
|
|
"editor.navbar.document-options.review-workflow.disable-title",
|
|
)
|
|
: $t(
|
|
"editor.navbar.document-options.review-workflow.enable-title",
|
|
)
|
|
}}
|
|
</span>
|
|
</ShadcnUiDropdownMenuItem>
|
|
<ShadcnUiDropdownMenuItem @click="emit('duplicate-document')">
|
|
<Icon name="mingcute:copy-2-line" />
|
|
<span>
|
|
{{ $t("editor.navbar.document-options.duplicate.title") }}
|
|
</span>
|
|
</ShadcnUiDropdownMenuItem>
|
|
<ShadcnUiDropdownMenuItem @click="emit('delete-document')">
|
|
<Icon name="lucide:trash-2" />
|
|
<span>
|
|
{{ $t("editor.navbar.document-options.delete.title") }}
|
|
</span>
|
|
</ShadcnUiDropdownMenuItem>
|
|
</ShadcnUiDropdownMenuContent>
|
|
</ShadcnUiDropdownMenu>
|
|
<template v-if="isAssistantEnabled">
|
|
<div class="mx-2 h-5.5 w-px bg-border" />
|
|
<ShadcnUiTooltip :delay-duration="600">
|
|
<ShadcnUiTooltipTrigger as-child>
|
|
<ShadcnUiButton
|
|
variant="ghost"
|
|
size="icon"
|
|
:class="
|
|
cn(
|
|
'h-7 w-7 cursor-pointer hover:bg-accent/50 active:bg-accent',
|
|
editorStore.aiAssistantOpen && 'bg-accent/50',
|
|
)
|
|
"
|
|
@click="editorStore.toggleAiAssistantOpen"
|
|
>
|
|
<Icon name="hugeicons:rubber-duck" class="size-4" />
|
|
<span class="sr-only">
|
|
{{
|
|
editorStore.aiAssistantOpen
|
|
? $t("editor.navbar.close-ai-assistant")
|
|
: $t("editor.navbar.open-ai-assistant")
|
|
}}
|
|
</span>
|
|
</ShadcnUiButton>
|
|
</ShadcnUiTooltipTrigger>
|
|
<ShadcnUiTooltipContent side="bottom" align="end">
|
|
{{
|
|
editorStore.aiAssistantOpen
|
|
? $t("editor.navbar.close-ai-assistant")
|
|
: $t("editor.navbar.open-ai-assistant")
|
|
}}
|
|
</ShadcnUiTooltipContent>
|
|
</ShadcnUiTooltip>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</header>
|
|
</template>
|