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 { 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}\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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user