chore: merge origin/main with local test additions
All checks were successful
CI / verify (push) Successful in 10m1s
All checks were successful
CI / verify (push) Successful in 10m1s
Resolves conflict in repo.spec.ts by combining getRepoPermission tests from main with the getFiles and queryFileContent tests from the local test pass. Adds getRepoPermission to the userRepo.store spec mocks so the new permission probe added upstream doesn't break the store tests.
This commit is contained in:
@@ -3,6 +3,7 @@ import { onBeforeMount, ref } from "vue"
|
|||||||
import { useRoute, useRouter } from "vue-router"
|
import { useRoute, useRouter } from "vue-router"
|
||||||
|
|
||||||
import { useGitHubLogin } from "@/hooks/useGitHubLogin.hook"
|
import { useGitHubLogin } from "@/hooks/useGitHubLogin.hook"
|
||||||
|
import { consumeGithubOAuthReturnPath } from "@/modules/user/service/oauthReturnPath"
|
||||||
import { signIn } from "@/modules/user/service/signIn"
|
import { signIn } from "@/modules/user/service/signIn"
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -22,7 +23,13 @@ onBeforeMount(async () => {
|
|||||||
await saveCredentials(token)
|
await saveCredentials(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
router.replace({ name: "Home" })
|
const returnPath = consumeGithubOAuthReturnPath()
|
||||||
|
|
||||||
|
if (!hasError.value && returnPath) {
|
||||||
|
router.replace(returnPath)
|
||||||
|
} else {
|
||||||
|
router.replace({ name: "Home" })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import { onMounted, ref, watch } from "vue"
|
|||||||
const props = defineProps<{ open: boolean }>()
|
const props = defineProps<{ open: boolean }>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "discard"): void
|
discard: []
|
||||||
(e: "overwrite"): void
|
overwrite: []
|
||||||
(e: "cancel"): void
|
cancel: []
|
||||||
(e: "update:open", value: boolean): void
|
"update:open": [value: boolean]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const dialogRef = ref<HTMLDialogElement | null>(null)
|
const dialogRef = ref<HTMLDialogElement | null>(null)
|
||||||
@@ -17,8 +17,16 @@ const close = () => {
|
|||||||
emit("update:open", false)
|
emit("update:open", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const choose = (action: "discard" | "overwrite" | "cancel") => {
|
const onDiscard = () => {
|
||||||
emit(action)
|
emit("discard")
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
const onOverwrite = () => {
|
||||||
|
emit("overwrite")
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
const onCancel = () => {
|
||||||
|
emit("cancel")
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +50,7 @@ onMounted(() => {
|
|||||||
ref="dialogRef"
|
ref="dialogRef"
|
||||||
class="modal"
|
class="modal"
|
||||||
@close="emit('update:open', false)"
|
@close="emit('update:open', false)"
|
||||||
@cancel.prevent="choose('cancel')"
|
@cancel.prevent="onCancel()"
|
||||||
>
|
>
|
||||||
<div class="modal-box">
|
<div class="modal-box">
|
||||||
<h3 class="text-lg font-bold">GitHub has a newer version of this note</h3>
|
<h3 class="text-lg font-bold">GitHub has a newer version of this note</h3>
|
||||||
@@ -55,28 +63,28 @@ onMounted(() => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-ghost"
|
class="btn btn-ghost"
|
||||||
@click="choose('cancel')"
|
@click="onCancel()"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-warning"
|
class="btn btn-warning"
|
||||||
@click="choose('overwrite')"
|
@click="onOverwrite()"
|
||||||
>
|
>
|
||||||
Save anyway (overwrite)
|
Save anyway (overwrite)
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
@click="choose('discard')"
|
@click="onDiscard()"
|
||||||
>
|
>
|
||||||
Discard my edits, pull latest
|
Discard my edits, pull latest
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form method="dialog" class="modal-backdrop">
|
<form method="dialog" class="modal-backdrop">
|
||||||
<button type="submit" @click="choose('cancel')">close</button>
|
<button type="submit" @click="onCancel()">close</button>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,20 +1,34 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { useRoute } from "vue-router"
|
||||||
|
|
||||||
|
import { GITHUB_OAUTH_RETURN_PATH_KEY } from "@/modules/user/service/oauthReturnPath"
|
||||||
|
|
||||||
const GITHUB_URL = "https://github.com/login/oauth/authorize"
|
const GITHUB_URL = "https://github.com/login/oauth/authorize"
|
||||||
|
|
||||||
const CLIENT_ID = "Iv1.12dc43d013ce3623"
|
const CLIENT_ID = "Iv1.12dc43d013ce3623"
|
||||||
const SCOPE = "repo%20workflow"
|
const SCOPE = "repo%20workflow"
|
||||||
const REDIRECT_URI = window.location.origin
|
const REDIRECT_URI = window.location.origin
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
const url = new URL(GITHUB_URL)
|
const url = new URL(GITHUB_URL)
|
||||||
url.searchParams.set("client_id", CLIENT_ID)
|
url.searchParams.set("client_id", CLIENT_ID)
|
||||||
url.searchParams.set("scope", SCOPE)
|
url.searchParams.set("scope", SCOPE)
|
||||||
url.searchParams.set("redirect_uri", REDIRECT_URI)
|
url.searchParams.set("redirect_uri", REDIRECT_URI)
|
||||||
|
|
||||||
const href = url.toString()
|
const href = url.toString()
|
||||||
|
|
||||||
|
const saveReturnPath = () => {
|
||||||
|
sessionStorage.setItem(GITHUB_OAUTH_RETURN_PATH_KEY, route.fullPath)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<a :href="href" class="sign-in-github btn btn-sm btn-primary">
|
<a
|
||||||
|
:href="href"
|
||||||
|
class="sign-in-github btn btn-sm btn-primary"
|
||||||
|
@click="saveReturnPath"
|
||||||
|
>
|
||||||
Sign in with
|
Sign in with
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { useEditionMode } from "@/hooks/useEditionMode"
|
import { useEditionMode } from "@/hooks/useEditionMode"
|
||||||
import { useFile } from "@/hooks/useFile.hook"
|
import { useFile } from "@/hooks/useFile.hook"
|
||||||
import { useGitHubContent } from "@/hooks/useGitHubContent.hook"
|
import { useGitHubContent } from "@/hooks/useGitHubContent.hook"
|
||||||
|
import { useImageUpload } from "@/hooks/useImageUpload.hook"
|
||||||
import { useLinks } from "@/hooks/useLinks.hook"
|
import { useLinks } from "@/hooks/useLinks.hook"
|
||||||
import { renderCodeFile } from "@/hooks/useMarkdown.hook"
|
import { renderCodeFile } from "@/hooks/useMarkdown.hook"
|
||||||
import { useMarkdownPostRender } from "@/hooks/useMarkdownPostRender.hook"
|
import { useMarkdownPostRender } from "@/hooks/useMarkdownPostRender.hook"
|
||||||
@@ -99,6 +100,7 @@ useTitleNotes(repo)
|
|||||||
|
|
||||||
const store = useUserRepoStore()
|
const store = useUserRepoStore()
|
||||||
const hasBacklinks = computed(() => store.userSettings?.backlink)
|
const hasBacklinks = computed(() => store.userSettings?.backlink)
|
||||||
|
const canPush = computed(() => store.canPush)
|
||||||
|
|
||||||
const { displayNoteOverlay } = useNoteOverlay(className.value, index)
|
const { displayNoteOverlay } = useNoteOverlay(className.value, index)
|
||||||
const displayedTitle = computed(() => filenameToNoteTitle(props.title ?? ""))
|
const displayedTitle = computed(() => filenameToNoteTitle(props.title ?? ""))
|
||||||
@@ -109,6 +111,34 @@ const { updateFile } = useGitHubContent({
|
|||||||
repo: repo.value
|
repo: repo.value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { uploadImage } = useImageUpload({
|
||||||
|
user: user.value,
|
||||||
|
repo: repo.value,
|
||||||
|
notePath: path
|
||||||
|
})
|
||||||
|
|
||||||
|
const fileInput = ref<HTMLInputElement | null>(null)
|
||||||
|
const editKey = ref(0)
|
||||||
|
const isUploading = ref(false)
|
||||||
|
|
||||||
|
const onImagePicked = async (e: Event) => {
|
||||||
|
const input = e.target as HTMLInputElement
|
||||||
|
const file = input.files?.[0]
|
||||||
|
input.value = ""
|
||||||
|
if (!file || !path.value) return
|
||||||
|
isUploading.value = true
|
||||||
|
try {
|
||||||
|
const result = await uploadImage(file)
|
||||||
|
if (!result) return
|
||||||
|
const trimmed = rawContent.value.replace(/\n+$/, "")
|
||||||
|
const prefix = trimmed ? `${trimmed}\n\n` : ""
|
||||||
|
rawContent.value = `${prefix}\n`
|
||||||
|
editKey.value++
|
||||||
|
} finally {
|
||||||
|
isUploading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
status: freshnessStatus,
|
status: freshnessStatus,
|
||||||
lastCheckedAt,
|
lastCheckedAt,
|
||||||
@@ -207,10 +237,10 @@ watch(mode, async (newMode) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const onConflictDiscard = async () => {
|
const onConflictDiscard = async () => {
|
||||||
const newRaw = await pullLatest()
|
const { raw } = await pullLatest()
|
||||||
if (newRaw !== null) {
|
if (raw !== null) {
|
||||||
rawContent.value = newRaw
|
rawContent.value = raw
|
||||||
initialRawContent.value = newRaw
|
initialRawContent.value = raw
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,13 +270,13 @@ const onBadgeClick = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const newRaw = await pullLatest()
|
const { raw, failureStatus } = await pullLatest()
|
||||||
if (newRaw !== null) {
|
if (raw !== null) {
|
||||||
rawContent.value = newRaw
|
rawContent.value = raw
|
||||||
initialRawContent.value = newRaw
|
initialRawContent.value = raw
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (freshnessStatus.value === "unauthorized") {
|
if (failureStatus === "unauthorized") {
|
||||||
errorMessage("🔐 GitHub auth expired — please sign in again")
|
errorMessage("🔐 GitHub auth expired — please sign in again")
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -274,7 +304,7 @@ const onBadgeClick = async () => {
|
|||||||
class="action"
|
class="action"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="isMarkdown"
|
v-if="isMarkdown && canPush"
|
||||||
class="action button is-text is-light"
|
class="action button is-text is-light"
|
||||||
:class="{ 'is-link': mode === 'edit' }"
|
:class="{ 'is-link': mode === 'edit' }"
|
||||||
:style="mode === 'edit' ? 'color: var(--color-primary)' : ''"
|
:style="mode === 'edit' ? 'color: var(--color-primary)' : ''"
|
||||||
@@ -323,6 +353,48 @@ const onBadgeClick = async () => {
|
|||||||
<path d="M14 4l0 4l-6 0l0 -4" />
|
<path d="M14 4l0 4l-6 0l0 -4" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="isMarkdown && mode === 'edit' && canPush"
|
||||||
|
class="action button is-text is-light"
|
||||||
|
:title="isUploading ? 'Uploading…' : 'Upload image'"
|
||||||
|
:disabled="isUploading"
|
||||||
|
@click="fileInput?.click()"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="isUploading"
|
||||||
|
class="loading loading-spinner loading-sm"
|
||||||
|
></span>
|
||||||
|
<svg
|
||||||
|
v-else
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="icon icon-tabler icon-tabler-photo-plus"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M15 8h.01" />
|
||||||
|
<path
|
||||||
|
d="M12.5 21h-6.5a3 3 0 0 1 -3 -3v-12a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v6.5"
|
||||||
|
/>
|
||||||
|
<path d="M3 16l5 -5c.928 -.893 2.072 -.893 3 0l4 4" />
|
||||||
|
<path d="M14 14l1 -1c.928 -.893 2.072 -.893 3 0l2 2" />
|
||||||
|
<path d="M16 19h6" />
|
||||||
|
<path d="M19 16v6" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
ref="fileInput"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
class="hidden-input"
|
||||||
|
@change="onImagePicked"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
class="title-stacked-note-link"
|
class="title-stacked-note-link"
|
||||||
@@ -337,7 +409,7 @@ const onBadgeClick = async () => {
|
|||||||
</div>
|
</div>
|
||||||
<section class="text-content">
|
<section class="text-content">
|
||||||
<div v-if="mode === 'edit' && isMarkdown" class="edit">
|
<div v-if="mode === 'edit' && isMarkdown" class="edit">
|
||||||
<edit-note v-model="rawContent" />
|
<edit-note :key="editKey" v-model="rawContent" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="mode === 'read'"
|
v-if="mode === 'read'"
|
||||||
@@ -410,6 +482,10 @@ $border-color: rgba(18, 19, 58, 0.2);
|
|||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hidden-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.action {
|
.action {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ export const useCheckboxCommit = ({
|
|||||||
initialContent,
|
initialContent,
|
||||||
initialSha,
|
initialSha,
|
||||||
containerSelector,
|
containerSelector,
|
||||||
debounceMs = 1000
|
debounceMs = 1000,
|
||||||
|
enabled = true
|
||||||
}: {
|
}: {
|
||||||
user: string
|
user: string
|
||||||
repo: string
|
repo: string
|
||||||
@@ -44,6 +45,7 @@ export const useCheckboxCommit = ({
|
|||||||
initialSha: Ref<string> | string
|
initialSha: Ref<string> | string
|
||||||
containerSelector: string
|
containerSelector: string
|
||||||
debounceMs?: number
|
debounceMs?: number
|
||||||
|
enabled?: Ref<boolean> | boolean
|
||||||
}) => {
|
}) => {
|
||||||
const { updateFile } = useGitHubContent({ user, repo })
|
const { updateFile } = useGitHubContent({ user, repo })
|
||||||
|
|
||||||
@@ -91,6 +93,8 @@ export const useCheckboxCommit = ({
|
|||||||
const debouncedCommit = useDebounceFn(commitChanges, debounceMs)
|
const debouncedCommit = useDebounceFn(commitChanges, debounceMs)
|
||||||
|
|
||||||
const handleCheckboxChange = (event: Event) => {
|
const handleCheckboxChange = (event: Event) => {
|
||||||
|
if (!toValue(enabled)) return
|
||||||
|
|
||||||
const target = event.target as HTMLInputElement
|
const target = event.target as HTMLInputElement
|
||||||
|
|
||||||
if (target.tagName !== "INPUT" || target.type !== "checkbox") {
|
if (target.tagName !== "INPUT" || target.type !== "checkbox") {
|
||||||
@@ -128,8 +132,16 @@ export const useCheckboxCommit = ({
|
|||||||
const listenToCheckboxes = () => {
|
const listenToCheckboxes = () => {
|
||||||
removeListeners()
|
removeListeners()
|
||||||
const container = document.querySelector(containerSelector)
|
const container = document.querySelector(containerSelector)
|
||||||
if (container) {
|
if (!container) return
|
||||||
|
|
||||||
|
if (toValue(enabled)) {
|
||||||
container.addEventListener("change", handleCheckboxChange)
|
container.addEventListener("change", handleCheckboxChange)
|
||||||
|
} else {
|
||||||
|
container
|
||||||
|
.querySelectorAll<HTMLInputElement>('input[type="checkbox"]')
|
||||||
|
.forEach((checkbox) => {
|
||||||
|
checkbox.disabled = true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,14 +37,22 @@ export const useGitHubContent = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const putFile = async ({
|
const putRaw = async ({
|
||||||
content,
|
contentBase64,
|
||||||
path,
|
path,
|
||||||
sha
|
sha,
|
||||||
|
message,
|
||||||
|
successMessage,
|
||||||
|
conflictMessage,
|
||||||
|
failureMessage
|
||||||
}: {
|
}: {
|
||||||
content: string
|
contentBase64: string
|
||||||
path: string
|
path: string
|
||||||
sha?: string
|
sha?: string
|
||||||
|
message: string
|
||||||
|
successMessage: string
|
||||||
|
conflictMessage: string
|
||||||
|
failureMessage: string
|
||||||
}): Promise<{ sha: string | null; conflict: boolean }> => {
|
}): Promise<{ sha: string | null; conflict: boolean }> => {
|
||||||
try {
|
try {
|
||||||
const octokit = await getOctokit()
|
const octokit = await getOctokit()
|
||||||
@@ -55,28 +63,63 @@ export const useGitHubContent = ({
|
|||||||
owner: user,
|
owner: user,
|
||||||
repo,
|
repo,
|
||||||
path,
|
path,
|
||||||
message: `Updating ${path} from Remanso`,
|
message,
|
||||||
content: encodeUTF8ToBase64(content),
|
content: contentBase64,
|
||||||
sha
|
sha
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
confirmMessage("✅ Note saved")
|
confirmMessage(successMessage)
|
||||||
|
|
||||||
return { sha: response?.data.content?.sha ?? null, conflict: false }
|
return { sha: response?.data.content?.sha ?? null, conflict: false }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const status = (error as { status?: number })?.status
|
const status = (error as { status?: number })?.status
|
||||||
if (status && isConflictStatus(status)) {
|
if (status && isConflictStatus(status)) {
|
||||||
errorMessage("⚠ Conflict: this note changed on GitHub")
|
errorMessage(conflictMessage)
|
||||||
console.warn(error)
|
console.warn(error)
|
||||||
return { sha: null, conflict: true }
|
return { sha: null, conflict: true }
|
||||||
}
|
}
|
||||||
errorMessage("❌ Note could not be saved")
|
errorMessage(failureMessage)
|
||||||
console.warn(error)
|
console.warn(error)
|
||||||
return { sha: null, conflict: false }
|
return { sha: null, conflict: false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const putFile = async ({
|
||||||
|
content,
|
||||||
|
path,
|
||||||
|
sha
|
||||||
|
}: {
|
||||||
|
content: string
|
||||||
|
path: string
|
||||||
|
sha?: string
|
||||||
|
}): Promise<{ sha: string | null; conflict: boolean }> =>
|
||||||
|
putRaw({
|
||||||
|
contentBase64: encodeUTF8ToBase64(content),
|
||||||
|
path,
|
||||||
|
sha,
|
||||||
|
message: `Updating ${path} from Remanso`,
|
||||||
|
successMessage: "✅ Note saved",
|
||||||
|
conflictMessage: "⚠ Conflict: this note changed on GitHub",
|
||||||
|
failureMessage: "❌ Note could not be saved"
|
||||||
|
})
|
||||||
|
|
||||||
|
const uploadBinaryFile = async ({
|
||||||
|
base64,
|
||||||
|
path
|
||||||
|
}: {
|
||||||
|
base64: string
|
||||||
|
path: string
|
||||||
|
}): Promise<{ sha: string | null; conflict: boolean }> =>
|
||||||
|
putRaw({
|
||||||
|
contentBase64: base64,
|
||||||
|
path,
|
||||||
|
message: `Uploading ${path} from Remanso`,
|
||||||
|
successMessage: "✅ Image uploaded",
|
||||||
|
conflictMessage: "⚠ A file already exists at this path on GitHub",
|
||||||
|
failureMessage: "❌ Image could not be uploaded"
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fetchLatestSha,
|
fetchLatestSha,
|
||||||
updateFile: async (props: {
|
updateFile: async (props: {
|
||||||
@@ -85,6 +128,7 @@ export const useGitHubContent = ({
|
|||||||
sha: string
|
sha: string
|
||||||
}) => putFile(props),
|
}) => putFile(props),
|
||||||
createFile: async (props: { content: string; path: string }) =>
|
createFile: async (props: { content: string; path: string }) =>
|
||||||
putFile(props)
|
putFile(props),
|
||||||
|
uploadBinaryFile
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
104
src/hooks/useImageUpload.hook.ts
Normal file
104
src/hooks/useImageUpload.hook.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { Ref, toValue } from "vue"
|
||||||
|
|
||||||
|
import { useGitHubContent } from "@/hooks/useGitHubContent.hook"
|
||||||
|
import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
|
||||||
|
import { errorMessage } from "@/utils/notif"
|
||||||
|
import { uniqueFilename } from "@/utils/uniqueFilename"
|
||||||
|
|
||||||
|
const arrayBufferToBase64 = (buffer: ArrayBuffer): string => {
|
||||||
|
const bytes = new Uint8Array(buffer)
|
||||||
|
const chunkSize = 0x8000
|
||||||
|
let binary = ""
|
||||||
|
for (let i = 0; i < bytes.length; i += chunkSize) {
|
||||||
|
const chunk = bytes.subarray(i, i + chunkSize)
|
||||||
|
binary += String.fromCharCode(...chunk)
|
||||||
|
}
|
||||||
|
return btoa(binary)
|
||||||
|
}
|
||||||
|
|
||||||
|
const splitPath = (fullPath: string): { directory: string; filename: string } => {
|
||||||
|
const lastSlash = fullPath.lastIndexOf("/")
|
||||||
|
if (lastSlash === -1) return { directory: "", filename: fullPath }
|
||||||
|
return {
|
||||||
|
directory: fullPath.slice(0, lastSlash),
|
||||||
|
filename: fullPath.slice(lastSlash + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stripMarkdownExtension = (filename: string): string =>
|
||||||
|
filename.replace(/\.(md|markdown|mdx)$/i, "")
|
||||||
|
|
||||||
|
const extractExtension = (filename: string): string => {
|
||||||
|
const dot = filename.lastIndexOf(".")
|
||||||
|
if (dot <= 0) return ".png"
|
||||||
|
return filename.slice(dot).toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useImageUpload = ({
|
||||||
|
user,
|
||||||
|
repo,
|
||||||
|
notePath
|
||||||
|
}: {
|
||||||
|
user: string
|
||||||
|
repo: string
|
||||||
|
notePath: Ref<string | undefined> | string | undefined
|
||||||
|
}) => {
|
||||||
|
const store = useUserRepoStore()
|
||||||
|
const { uploadBinaryFile } = useGitHubContent({ user, repo })
|
||||||
|
|
||||||
|
const uploadImage = async (
|
||||||
|
file: File
|
||||||
|
): Promise<{ filename: string } | null> => {
|
||||||
|
const currentNotePath = toValue(notePath)
|
||||||
|
if (!currentNotePath) {
|
||||||
|
errorMessage("❌ Image upload failed")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { directory, filename: noteFilename } = splitPath(currentNotePath)
|
||||||
|
const basename = stripMarkdownExtension(noteFilename)
|
||||||
|
const extension = extractExtension(file.name)
|
||||||
|
|
||||||
|
const existingPaths = store.files
|
||||||
|
.map((f) => f.path)
|
||||||
|
.filter((p): p is string => typeof p === "string")
|
||||||
|
|
||||||
|
const filename = uniqueFilename({
|
||||||
|
basename,
|
||||||
|
extension,
|
||||||
|
existingPaths,
|
||||||
|
directory
|
||||||
|
})
|
||||||
|
|
||||||
|
const targetPath = directory ? `${directory}/${filename}` : filename
|
||||||
|
|
||||||
|
const buffer = await file.arrayBuffer()
|
||||||
|
const base64 = arrayBufferToBase64(buffer)
|
||||||
|
|
||||||
|
const { sha, conflict } = await uploadBinaryFile({
|
||||||
|
base64,
|
||||||
|
path: targetPath
|
||||||
|
})
|
||||||
|
|
||||||
|
if (conflict || !sha) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
store.registerUploadedFile({
|
||||||
|
path: targetPath,
|
||||||
|
sha,
|
||||||
|
type: "blob",
|
||||||
|
size: file.size
|
||||||
|
})
|
||||||
|
|
||||||
|
return { filename }
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("image upload failed", error)
|
||||||
|
errorMessage("❌ Image upload failed")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { uploadImage }
|
||||||
|
}
|
||||||
@@ -77,14 +77,17 @@ export const useNoteFreshness = ({
|
|||||||
return { sha: result.sha, failureStatus: null }
|
return { sha: result.sha, failureStatus: null }
|
||||||
}
|
}
|
||||||
|
|
||||||
const pullLatest = async (): Promise<string | null> => {
|
const pullLatest = async (): Promise<{
|
||||||
if (!path.value) return null
|
raw: string | null
|
||||||
|
failureStatus: FreshnessStatus | null
|
||||||
|
}> => {
|
||||||
|
if (!path.value) return { raw: null, failureStatus: null }
|
||||||
const usedCachedSha = latestSha.value !== null
|
const usedCachedSha = latestSha.value !== null
|
||||||
const { sha: remoteSha, failureStatus } = await resolveRemoteSha(path.value)
|
const { sha: remoteSha, failureStatus } = await resolveRemoteSha(path.value)
|
||||||
if (!remoteSha) {
|
if (!remoteSha) {
|
||||||
console.warn("pullLatest: could not resolve remote sha", { path: path.value })
|
console.warn("pullLatest: could not resolve remote sha", { path: path.value })
|
||||||
if (failureStatus) status.value = failureStatus
|
if (failureStatus) status.value = failureStatus
|
||||||
return null
|
return { raw: null, failureStatus }
|
||||||
}
|
}
|
||||||
const fileContent = await queryFileContent(user, repo, remoteSha)
|
const fileContent = await queryFileContent(user, repo, remoteSha)
|
||||||
if (!fileContent) {
|
if (!fileContent) {
|
||||||
@@ -96,7 +99,7 @@ export const useNoteFreshness = ({
|
|||||||
// Cached SHA may be stale — clear so the next click re-resolves it.
|
// Cached SHA may be stale — clear so the next click re-resolves it.
|
||||||
if (usedCachedSha) latestSha.value = null
|
if (usedCachedSha) latestSha.value = null
|
||||||
status.value = "offline"
|
status.value = "offline"
|
||||||
return null
|
return { raw: null, failureStatus: "offline" }
|
||||||
}
|
}
|
||||||
const { saveCacheNote } = prepareNoteCache(sha.value, path.value)
|
const { saveCacheNote } = prepareNoteCache(sha.value, path.value)
|
||||||
await saveCacheNote(fileContent, {
|
await saveCacheNote(fileContent, {
|
||||||
@@ -108,7 +111,7 @@ export const useNoteFreshness = ({
|
|||||||
lastCheckedAt.value = new Date()
|
lastCheckedAt.value = new Date()
|
||||||
status.value = "verified"
|
status.value = "verified"
|
||||||
const { getRawContent } = markdownBuilder(sha.value)
|
const { getRawContent } = markdownBuilder(sha.value)
|
||||||
return getRawContent(fileContent)
|
return { raw: getRawContent(fileContent), failureStatus: null }
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
||||||
|
|
||||||
|
const { reposGet } = vi.hoisted(() => ({ reposGet: vi.fn() }))
|
||||||
|
|
||||||
vi.mock("./octo", () => ({
|
vi.mock("./octo", () => ({
|
||||||
getOctokit: vi.fn(),
|
getOctokit: vi.fn().mockResolvedValue({
|
||||||
|
repos: { get: reposGet }
|
||||||
|
}),
|
||||||
runWithAuthRetry: vi.fn()
|
runWithAuthRetry: vi.fn()
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -18,12 +22,52 @@ vi.mock("@/modules/note/cache/prepareNoteCache", () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
import { getOctokit, runWithAuthRetry } from "./octo"
|
import { getOctokit, runWithAuthRetry } from "./octo"
|
||||||
import { getFiles, queryFileContent } from "./repo"
|
import {
|
||||||
|
getFiles,
|
||||||
|
getRepoPermission,
|
||||||
|
queryFileContent
|
||||||
|
} from "./repo"
|
||||||
|
|
||||||
const makeOctokitWithRequest = (impl: (route: string, params: unknown) => unknown) => ({
|
const makeOctokitWithRequest = (impl: (route: string, params: unknown) => unknown) => ({
|
||||||
request: vi.fn(impl)
|
request: vi.fn(impl)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("getRepoPermission", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
reposGet.mockReset()
|
||||||
|
vi.mocked(getOctokit).mockResolvedValue({
|
||||||
|
repos: { get: reposGet }
|
||||||
|
} as never)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns true when permissions.push is true", async () => {
|
||||||
|
reposGet.mockResolvedValue({
|
||||||
|
data: { permissions: { push: true } }
|
||||||
|
})
|
||||||
|
await expect(getRepoPermission("owner", "repo")).resolves.toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns false when permissions.push is false", async () => {
|
||||||
|
reposGet.mockResolvedValue({
|
||||||
|
data: { permissions: { push: false } }
|
||||||
|
})
|
||||||
|
await expect(getRepoPermission("owner", "repo")).resolves.toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns false when permissions is missing (anonymous request)", async () => {
|
||||||
|
reposGet.mockResolvedValue({
|
||||||
|
data: {}
|
||||||
|
})
|
||||||
|
await expect(getRepoPermission("owner", "repo")).resolves.toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns false when owner or repo is empty", async () => {
|
||||||
|
await expect(getRepoPermission("", "repo")).resolves.toBe(false)
|
||||||
|
await expect(getRepoPermission("owner", "")).resolves.toBe(false)
|
||||||
|
expect(reposGet).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("getFiles", () => {
|
describe("getFiles", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.mocked(getOctokit).mockReset()
|
vi.mocked(getOctokit).mockReset()
|
||||||
|
|||||||
@@ -4,6 +4,18 @@ import { RepoFile } from "@/modules/repo/interfaces/RepoFile"
|
|||||||
import { UserSettings } from "@/modules/repo/interfaces/UserSettings"
|
import { UserSettings } from "@/modules/repo/interfaces/UserSettings"
|
||||||
import { getOctokit, runWithAuthRetry } from "@/modules/repo/services/octo"
|
import { getOctokit, runWithAuthRetry } from "@/modules/repo/services/octo"
|
||||||
|
|
||||||
|
export const getRepoPermission = async (
|
||||||
|
owner: string,
|
||||||
|
repo: string
|
||||||
|
): Promise<boolean> => {
|
||||||
|
if (!owner || !repo) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const octokit = await getOctokit()
|
||||||
|
const { data } = await octokit.repos.get({ owner, repo })
|
||||||
|
return data.permissions?.push ?? false
|
||||||
|
}
|
||||||
|
|
||||||
export const getFiles = async (
|
export const getFiles = async (
|
||||||
owner: string,
|
owner: string,
|
||||||
repo: string
|
repo: string
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ vi.mock("@/modules/repo/services/repo", () => ({
|
|||||||
getFiles: vi.fn().mockResolvedValue([]),
|
getFiles: vi.fn().mockResolvedValue([]),
|
||||||
getMainReadme: vi.fn().mockResolvedValue(null),
|
getMainReadme: vi.fn().mockResolvedValue(null),
|
||||||
getCachedMainReadme: vi.fn().mockResolvedValue(null),
|
getCachedMainReadme: vi.fn().mockResolvedValue(null),
|
||||||
getUserSettingsContent: vi.fn().mockResolvedValue(null)
|
getUserSettingsContent: vi.fn().mockResolvedValue(null),
|
||||||
|
getRepoPermission: vi.fn().mockResolvedValue(false)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock("@/modules/user/service/signIn", () => ({
|
vi.mock("@/modules/user/service/signIn", () => ({
|
||||||
@@ -26,6 +27,7 @@ import {
|
|||||||
getCachedMainReadme,
|
getCachedMainReadme,
|
||||||
getFiles,
|
getFiles,
|
||||||
getMainReadme,
|
getMainReadme,
|
||||||
|
getRepoPermission,
|
||||||
getUserSettingsContent
|
getUserSettingsContent
|
||||||
} from "@/modules/repo/services/repo"
|
} from "@/modules/repo/services/repo"
|
||||||
|
|
||||||
@@ -145,6 +147,7 @@ describe("userRepo store — setUserRepo", () => {
|
|||||||
vi.mocked(getMainReadme).mockResolvedValue(null)
|
vi.mocked(getMainReadme).mockResolvedValue(null)
|
||||||
vi.mocked(getCachedMainReadme).mockResolvedValue(null)
|
vi.mocked(getCachedMainReadme).mockResolvedValue(null)
|
||||||
vi.mocked(getUserSettingsContent).mockResolvedValue(null)
|
vi.mocked(getUserSettingsContent).mockResolvedValue(null)
|
||||||
|
vi.mocked(getRepoPermission).mockResolvedValue(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("sets user and repo immediately and clears loadError", async () => {
|
it("sets user and repo immediately and clears loadError", async () => {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
getCachedMainReadme,
|
getCachedMainReadme,
|
||||||
getFiles,
|
getFiles,
|
||||||
getMainReadme,
|
getMainReadme,
|
||||||
|
getRepoPermission,
|
||||||
getUserSettingsContent
|
getUserSettingsContent
|
||||||
} from "@/modules/repo/services/repo"
|
} from "@/modules/repo/services/repo"
|
||||||
import { refreshToken } from "@/modules/user/service/signIn"
|
import { refreshToken } from "@/modules/user/service/signIn"
|
||||||
@@ -24,6 +25,7 @@ interface State {
|
|||||||
userSettings?: UserSettings | null
|
userSettings?: UserSettings | null
|
||||||
needToLogin: boolean
|
needToLogin: boolean
|
||||||
loadError: RepoLoadError
|
loadError: RepoLoadError
|
||||||
|
canPush: boolean
|
||||||
_requestId: number
|
_requestId: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +48,7 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
|
|||||||
userSettings: undefined,
|
userSettings: undefined,
|
||||||
needToLogin: false,
|
needToLogin: false,
|
||||||
loadError: null,
|
loadError: null,
|
||||||
|
canPush: false,
|
||||||
_requestId: 0
|
_requestId: 0
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
@@ -78,6 +81,7 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
|
|||||||
this.user = user
|
this.user = user
|
||||||
this.repo = repo
|
this.repo = repo
|
||||||
this.loadError = null
|
this.loadError = null
|
||||||
|
this.canPush = false
|
||||||
|
|
||||||
let lsLayout: Partial<UserSettings> = {}
|
let lsLayout: Partial<UserSettings> = {}
|
||||||
try {
|
try {
|
||||||
@@ -120,6 +124,17 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
|
|||||||
|
|
||||||
if (requestId !== this._requestId) return
|
if (requestId !== this._requestId) return
|
||||||
|
|
||||||
|
getRepoPermission(user, repo)
|
||||||
|
.then((canPush) => {
|
||||||
|
if (requestId !== this._requestId) return
|
||||||
|
this.canPush = canPush
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
if (requestId !== this._requestId) return
|
||||||
|
console.warn("getRepoPermission failed", error)
|
||||||
|
this.canPush = false
|
||||||
|
})
|
||||||
|
|
||||||
getFiles(user, repo)
|
getFiles(user, repo)
|
||||||
.then(async (files) => {
|
.then(async (files) => {
|
||||||
if (requestId !== this._requestId) return
|
if (requestId !== this._requestId) return
|
||||||
@@ -230,6 +245,28 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
|
|||||||
})
|
})
|
||||||
this.files = newFiles
|
this.files = newFiles
|
||||||
},
|
},
|
||||||
|
registerUploadedFile(file: RepoFile) {
|
||||||
|
if (!file.path) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedRepoId = generateId(
|
||||||
|
DataType.SavedRepo,
|
||||||
|
`${this.user}-${this.repo}`
|
||||||
|
)
|
||||||
|
const newFiles = [
|
||||||
|
...toRaw(this.files).filter((f) => f.path !== file.path),
|
||||||
|
toRaw(file)
|
||||||
|
]
|
||||||
|
data.update<DataType.SavedRepo, SavedRepo>({
|
||||||
|
_id: savedRepoId,
|
||||||
|
$type: DataType.SavedRepo,
|
||||||
|
repo: this.repo,
|
||||||
|
user: this.user,
|
||||||
|
files: newFiles
|
||||||
|
})
|
||||||
|
this.files = newFiles
|
||||||
|
},
|
||||||
resetUserRepo() {
|
resetUserRepo() {
|
||||||
this.user = ""
|
this.user = ""
|
||||||
this.repo = ""
|
this.repo = ""
|
||||||
@@ -239,6 +276,7 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
|
|||||||
resetFiles() {
|
resetFiles() {
|
||||||
this.files = []
|
this.files = []
|
||||||
this.readme = null
|
this.readme = null
|
||||||
|
this.canPush = false
|
||||||
},
|
},
|
||||||
setFontFamily(fontFamily: string) {
|
setFontFamily(fontFamily: string) {
|
||||||
if (!this.userSettings) {
|
if (!this.userSettings) {
|
||||||
|
|||||||
7
src/modules/user/service/oauthReturnPath.ts
Normal file
7
src/modules/user/service/oauthReturnPath.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const GITHUB_OAUTH_RETURN_PATH_KEY = "github-oauth-return-path"
|
||||||
|
|
||||||
|
export const consumeGithubOAuthReturnPath = (): string | null => {
|
||||||
|
const path = sessionStorage.getItem(GITHUB_OAUTH_RETURN_PATH_KEY)
|
||||||
|
sessionStorage.removeItem(GITHUB_OAUTH_RETURN_PATH_KEY)
|
||||||
|
return path
|
||||||
|
}
|
||||||
71
src/utils/uniqueFilename.spec.ts
Normal file
71
src/utils/uniqueFilename.spec.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { describe, expect, it } from "vitest"
|
||||||
|
|
||||||
|
import { uniqueFilename } from "./uniqueFilename"
|
||||||
|
|
||||||
|
describe("uniqueFilename", () => {
|
||||||
|
it("returns basename.ext when nothing collides", () => {
|
||||||
|
expect(
|
||||||
|
uniqueFilename({
|
||||||
|
basename: "myfile",
|
||||||
|
extension: ".png",
|
||||||
|
existingPaths: ["docs/notes/myfile.md"],
|
||||||
|
directory: "docs/notes"
|
||||||
|
})
|
||||||
|
).toEqual("myfile.png")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("appends -2 on first collision (skips -1)", () => {
|
||||||
|
expect(
|
||||||
|
uniqueFilename({
|
||||||
|
basename: "myfile",
|
||||||
|
extension: ".png",
|
||||||
|
existingPaths: ["docs/notes/myfile.png"],
|
||||||
|
directory: "docs/notes"
|
||||||
|
})
|
||||||
|
).toEqual("myfile-2.png")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("appends -3 when both base and -2 exist", () => {
|
||||||
|
expect(
|
||||||
|
uniqueFilename({
|
||||||
|
basename: "myfile",
|
||||||
|
extension: ".png",
|
||||||
|
existingPaths: ["docs/notes/myfile.png", "docs/notes/myfile-2.png"],
|
||||||
|
directory: "docs/notes"
|
||||||
|
})
|
||||||
|
).toEqual("myfile-3.png")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("fills gaps when -3 exists but -2 does not", () => {
|
||||||
|
expect(
|
||||||
|
uniqueFilename({
|
||||||
|
basename: "myfile",
|
||||||
|
extension: ".png",
|
||||||
|
existingPaths: ["docs/notes/myfile.png", "docs/notes/myfile-3.png"],
|
||||||
|
directory: "docs/notes"
|
||||||
|
})
|
||||||
|
).toEqual("myfile-2.png")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handles repo-root directory", () => {
|
||||||
|
expect(
|
||||||
|
uniqueFilename({
|
||||||
|
basename: "myfile",
|
||||||
|
extension: ".jpg",
|
||||||
|
existingPaths: ["myfile.jpg"],
|
||||||
|
directory: ""
|
||||||
|
})
|
||||||
|
).toEqual("myfile-2.jpg")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("does not collide with a different extension", () => {
|
||||||
|
expect(
|
||||||
|
uniqueFilename({
|
||||||
|
basename: "myfile",
|
||||||
|
extension: ".png",
|
||||||
|
existingPaths: ["docs/myfile.jpg"],
|
||||||
|
directory: "docs"
|
||||||
|
})
|
||||||
|
).toEqual("myfile.png")
|
||||||
|
})
|
||||||
|
})
|
||||||
21
src/utils/uniqueFilename.ts
Normal file
21
src/utils/uniqueFilename.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
export const uniqueFilename = ({
|
||||||
|
basename,
|
||||||
|
extension,
|
||||||
|
existingPaths,
|
||||||
|
directory
|
||||||
|
}: {
|
||||||
|
basename: string
|
||||||
|
extension: string
|
||||||
|
existingPaths: string[]
|
||||||
|
directory: string
|
||||||
|
}): string => {
|
||||||
|
const prefix = directory ? `${directory}/` : ""
|
||||||
|
const candidate = (n: number) =>
|
||||||
|
n === 1 ? `${basename}${extension}` : `${basename}-${n}${extension}`
|
||||||
|
const taken = new Set(existingPaths)
|
||||||
|
let n = 1
|
||||||
|
while (taken.has(`${prefix}${candidate(n)}`)) {
|
||||||
|
n = n === 1 ? 2 : n + 1
|
||||||
|
}
|
||||||
|
return candidate(n)
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hoo
|
|||||||
import { prepareNoteCache } from "@/modules/note/cache/prepareNoteCache"
|
import { prepareNoteCache } from "@/modules/note/cache/prepareNoteCache"
|
||||||
import EditNote from "@/modules/note/components/EditNote.vue"
|
import EditNote from "@/modules/note/components/EditNote.vue"
|
||||||
import { useFolderNotes } from "@/modules/note/hooks/useFolderNotes"
|
import { useFolderNotes } from "@/modules/note/hooks/useFolderNotes"
|
||||||
|
import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
|
||||||
import { encodeUTF8ToBase64 } from "@/utils/decodeBase64ToUTF8"
|
import { encodeUTF8ToBase64 } from "@/utils/decodeBase64ToUTF8"
|
||||||
import { confirmMessage, errorMessage } from "@/utils/notif"
|
import { confirmMessage, errorMessage } from "@/utils/notif"
|
||||||
import { extractYouTubeId } from "@/utils/youtube"
|
import { extractYouTubeId } from "@/utils/youtube"
|
||||||
@@ -76,6 +77,9 @@ const { createFile } = useGitHubContent({
|
|||||||
|
|
||||||
const hasTodayNote = computed(() => content.value.includes(today))
|
const hasTodayNote = computed(() => content.value.includes(today))
|
||||||
|
|
||||||
|
const store = useUserRepoStore()
|
||||||
|
const canPush = computed(() => store.canPush)
|
||||||
|
|
||||||
watch(mode, async (newMode) => {
|
watch(mode, async (newMode) => {
|
||||||
if (newMode === "read" && newContent.value.trim() !== initialContent) {
|
if (newMode === "read" && newContent.value.trim() !== initialContent) {
|
||||||
const content = `# ${new Date().toLocaleDateString()}\n\n${
|
const content = `# ${new Date().toLocaleDateString()}\n\n${
|
||||||
@@ -114,13 +118,15 @@ watch(mode, async (newMode) => {
|
|||||||
<h3 class="subtitle">Inbox</h3>
|
<h3 class="subtitle">Inbox</h3>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button
|
<button
|
||||||
v-if="!hasTodayNote"
|
v-if="!hasTodayNote && canPush"
|
||||||
class="btn btn-secondary"
|
class="btn btn-secondary"
|
||||||
@click="toggleMode"
|
@click="toggleMode"
|
||||||
>
|
>
|
||||||
new fleeting note
|
new fleeting note
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline" @click="handleYouTube">YouTube</button>
|
<button v-if="canPush" class="btn btn-outline" @click="handleYouTube">
|
||||||
|
YouTube
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="mode === 'edit'">
|
<div v-if="mode === 'edit'">
|
||||||
|
|||||||
@@ -26,9 +26,10 @@ const todoNote = computed(() =>
|
|||||||
const sha = computed(() => todoNote.value?.sha ?? "")
|
const sha = computed(() => todoNote.value?.sha ?? "")
|
||||||
const path = computed(() => todoNote.value?.path)
|
const path = computed(() => todoNote.value?.path)
|
||||||
|
|
||||||
|
const canPush = computed(() => store.canPush)
|
||||||
|
|
||||||
const { toHTML } = markdownBuilder(repo)
|
const { toHTML } = markdownBuilder(repo)
|
||||||
|
|
||||||
// Setup checkbox commit handler
|
|
||||||
const { pendingContent, syncContent, listenToCheckboxes, hasPendingChanges } =
|
const { pendingContent, syncContent, listenToCheckboxes, hasPendingChanges } =
|
||||||
useCheckboxCommit({
|
useCheckboxCommit({
|
||||||
user: props.user,
|
user: props.user,
|
||||||
@@ -37,7 +38,8 @@ const { pendingContent, syncContent, listenToCheckboxes, hasPendingChanges } =
|
|||||||
initialContent: "",
|
initialContent: "",
|
||||||
initialSha: sha,
|
initialSha: sha,
|
||||||
containerSelector: ".todo-notes .note-display",
|
containerSelector: ".todo-notes .note-display",
|
||||||
debounceMs: 1000
|
debounceMs: 1000,
|
||||||
|
enabled: canPush
|
||||||
})
|
})
|
||||||
|
|
||||||
// Render pending content to HTML for display
|
// Render pending content to HTML for display
|
||||||
@@ -64,9 +66,9 @@ watch(
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
// Setup checkbox listeners when content renders
|
// Setup checkbox listeners when content renders or canPush changes
|
||||||
watch(
|
watch(
|
||||||
renderedContent,
|
[renderedContent, canPush],
|
||||||
async () => {
|
async () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
listenToCheckboxes()
|
listenToCheckboxes()
|
||||||
|
|||||||
Reference in New Issue
Block a user