chore: merge origin/main with local test additions
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:
Julien Calixte
2026-06-06 22:17:10 +02:00
17 changed files with 522 additions and 50 deletions

View File

@@ -3,6 +3,7 @@ import { onBeforeMount, ref } from "vue"
import { useRoute, useRouter } from "vue-router"
import { useGitHubLogin } from "@/hooks/useGitHubLogin.hook"
import { consumeGithubOAuthReturnPath } from "@/modules/user/service/oauthReturnPath"
import { signIn } from "@/modules/user/service/signIn"
const route = useRoute()
@@ -22,7 +23,13 @@ onBeforeMount(async () => {
await saveCredentials(token)
}
router.replace({ name: "Home" })
const returnPath = consumeGithubOAuthReturnPath()
if (!hasError.value && returnPath) {
router.replace(returnPath)
} else {
router.replace({ name: "Home" })
}
}
})
</script>

View File

@@ -4,10 +4,10 @@ import { onMounted, ref, watch } from "vue"
const props = defineProps<{ open: boolean }>()
const emit = defineEmits<{
(e: "discard"): void
(e: "overwrite"): void
(e: "cancel"): void
(e: "update:open", value: boolean): void
discard: []
overwrite: []
cancel: []
"update:open": [value: boolean]
}>()
const dialogRef = ref<HTMLDialogElement | null>(null)
@@ -17,8 +17,16 @@ const close = () => {
emit("update:open", false)
}
const choose = (action: "discard" | "overwrite" | "cancel") => {
emit(action)
const onDiscard = () => {
emit("discard")
close()
}
const onOverwrite = () => {
emit("overwrite")
close()
}
const onCancel = () => {
emit("cancel")
close()
}
@@ -42,7 +50,7 @@ onMounted(() => {
ref="dialogRef"
class="modal"
@close="emit('update:open', false)"
@cancel.prevent="choose('cancel')"
@cancel.prevent="onCancel()"
>
<div class="modal-box">
<h3 class="text-lg font-bold">GitHub has a newer version of this note</h3>
@@ -55,28 +63,28 @@ onMounted(() => {
<button
type="button"
class="btn btn-ghost"
@click="choose('cancel')"
@click="onCancel()"
>
Cancel
</button>
<button
type="button"
class="btn btn-warning"
@click="choose('overwrite')"
@click="onOverwrite()"
>
Save anyway (overwrite)
</button>
<button
type="button"
class="btn btn-primary"
@click="choose('discard')"
@click="onDiscard()"
>
Discard my edits, pull latest
</button>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button type="submit" @click="choose('cancel')">close</button>
<button type="submit" @click="onCancel()">close</button>
</form>
</dialog>
</template>

View File

@@ -1,20 +1,34 @@
<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 CLIENT_ID = "Iv1.12dc43d013ce3623"
const SCOPE = "repo%20workflow"
const REDIRECT_URI = window.location.origin
const route = useRoute()
const url = new URL(GITHUB_URL)
url.searchParams.set("client_id", CLIENT_ID)
url.searchParams.set("scope", SCOPE)
url.searchParams.set("redirect_uri", REDIRECT_URI)
const href = url.toString()
const saveReturnPath = () => {
sessionStorage.setItem(GITHUB_OAUTH_RETURN_PATH_KEY, route.fullPath)
}
</script>
<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
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@@ -10,6 +10,7 @@ import {
import { useEditionMode } from "@/hooks/useEditionMode"
import { useFile } from "@/hooks/useFile.hook"
import { useGitHubContent } from "@/hooks/useGitHubContent.hook"
import { useImageUpload } from "@/hooks/useImageUpload.hook"
import { useLinks } from "@/hooks/useLinks.hook"
import { renderCodeFile } from "@/hooks/useMarkdown.hook"
import { useMarkdownPostRender } from "@/hooks/useMarkdownPostRender.hook"
@@ -99,6 +100,7 @@ useTitleNotes(repo)
const store = useUserRepoStore()
const hasBacklinks = computed(() => store.userSettings?.backlink)
const canPush = computed(() => store.canPush)
const { displayNoteOverlay } = useNoteOverlay(className.value, index)
const displayedTitle = computed(() => filenameToNoteTitle(props.title ?? ""))
@@ -109,6 +111,34 @@ const { updateFile } = useGitHubContent({
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}![](${result.filename})\n`
editKey.value++
} finally {
isUploading.value = false
}
}
const {
status: freshnessStatus,
lastCheckedAt,
@@ -207,10 +237,10 @@ watch(mode, async (newMode) => {
})
const onConflictDiscard = async () => {
const newRaw = await pullLatest()
if (newRaw !== null) {
rawContent.value = newRaw
initialRawContent.value = newRaw
const { raw } = await pullLatest()
if (raw !== null) {
rawContent.value = raw
initialRawContent.value = raw
}
}
@@ -240,13 +270,13 @@ const onBadgeClick = async () => {
return
}
const newRaw = await pullLatest()
if (newRaw !== null) {
rawContent.value = newRaw
initialRawContent.value = newRaw
const { raw, failureStatus } = await pullLatest()
if (raw !== null) {
rawContent.value = raw
initialRawContent.value = raw
return
}
if (freshnessStatus.value === "unauthorized") {
if (failureStatus === "unauthorized") {
errorMessage("🔐 GitHub auth expired — please sign in again")
}
} catch (error) {
@@ -274,7 +304,7 @@ const onBadgeClick = async () => {
class="action"
/>
<button
v-if="isMarkdown"
v-if="isMarkdown && canPush"
class="action button is-text is-light"
:class="{ 'is-link': mode === 'edit' }"
:style="mode === 'edit' ? 'color: var(--color-primary)' : ''"
@@ -323,6 +353,48 @@ const onBadgeClick = async () => {
<path d="M14 4l0 4l-6 0l0 -4" />
</svg>
</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>
<a
class="title-stacked-note-link"
@@ -337,7 +409,7 @@ const onBadgeClick = async () => {
</div>
<section class="text-content">
<div v-if="mode === 'edit' && isMarkdown" class="edit">
<edit-note v-model="rawContent" />
<edit-note :key="editKey" v-model="rawContent" />
</div>
<div
v-if="mode === 'read'"
@@ -410,6 +482,10 @@ $border-color: rgba(18, 19, 58, 0.2);
gap: 0.25rem;
}
.hidden-input {
display: none;
}
.action {
margin: 0;