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:
@@ -35,7 +35,8 @@ export const useCheckboxCommit = ({
|
||||
initialContent,
|
||||
initialSha,
|
||||
containerSelector,
|
||||
debounceMs = 1000
|
||||
debounceMs = 1000,
|
||||
enabled = true
|
||||
}: {
|
||||
user: string
|
||||
repo: string
|
||||
@@ -44,6 +45,7 @@ export const useCheckboxCommit = ({
|
||||
initialSha: Ref<string> | string
|
||||
containerSelector: string
|
||||
debounceMs?: number
|
||||
enabled?: Ref<boolean> | boolean
|
||||
}) => {
|
||||
const { updateFile } = useGitHubContent({ user, repo })
|
||||
|
||||
@@ -91,6 +93,8 @@ export const useCheckboxCommit = ({
|
||||
const debouncedCommit = useDebounceFn(commitChanges, debounceMs)
|
||||
|
||||
const handleCheckboxChange = (event: Event) => {
|
||||
if (!toValue(enabled)) return
|
||||
|
||||
const target = event.target as HTMLInputElement
|
||||
|
||||
if (target.tagName !== "INPUT" || target.type !== "checkbox") {
|
||||
@@ -128,8 +132,16 @@ export const useCheckboxCommit = ({
|
||||
const listenToCheckboxes = () => {
|
||||
removeListeners()
|
||||
const container = document.querySelector(containerSelector)
|
||||
if (container) {
|
||||
if (!container) return
|
||||
|
||||
if (toValue(enabled)) {
|
||||
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 ({
|
||||
content,
|
||||
const putRaw = async ({
|
||||
contentBase64,
|
||||
path,
|
||||
sha
|
||||
sha,
|
||||
message,
|
||||
successMessage,
|
||||
conflictMessage,
|
||||
failureMessage
|
||||
}: {
|
||||
content: string
|
||||
contentBase64: string
|
||||
path: string
|
||||
sha?: string
|
||||
message: string
|
||||
successMessage: string
|
||||
conflictMessage: string
|
||||
failureMessage: string
|
||||
}): Promise<{ sha: string | null; conflict: boolean }> => {
|
||||
try {
|
||||
const octokit = await getOctokit()
|
||||
@@ -55,28 +63,63 @@ export const useGitHubContent = ({
|
||||
owner: user,
|
||||
repo,
|
||||
path,
|
||||
message: `Updating ${path} from Remanso`,
|
||||
content: encodeUTF8ToBase64(content),
|
||||
message,
|
||||
content: contentBase64,
|
||||
sha
|
||||
}
|
||||
)
|
||||
|
||||
confirmMessage("✅ Note saved")
|
||||
confirmMessage(successMessage)
|
||||
|
||||
return { sha: response?.data.content?.sha ?? null, conflict: false }
|
||||
} catch (error) {
|
||||
const status = (error as { status?: number })?.status
|
||||
if (status && isConflictStatus(status)) {
|
||||
errorMessage("⚠ Conflict: this note changed on GitHub")
|
||||
errorMessage(conflictMessage)
|
||||
console.warn(error)
|
||||
return { sha: null, conflict: true }
|
||||
}
|
||||
errorMessage("❌ Note could not be saved")
|
||||
errorMessage(failureMessage)
|
||||
console.warn(error)
|
||||
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 {
|
||||
fetchLatestSha,
|
||||
updateFile: async (props: {
|
||||
@@ -85,6 +128,7 @@ export const useGitHubContent = ({
|
||||
sha: string
|
||||
}) => putFile(props),
|
||||
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 }
|
||||
}
|
||||
|
||||
const pullLatest = async (): Promise<string | null> => {
|
||||
if (!path.value) return null
|
||||
const pullLatest = async (): Promise<{
|
||||
raw: string | null
|
||||
failureStatus: FreshnessStatus | null
|
||||
}> => {
|
||||
if (!path.value) return { raw: null, failureStatus: null }
|
||||
const usedCachedSha = latestSha.value !== null
|
||||
const { sha: remoteSha, failureStatus } = await resolveRemoteSha(path.value)
|
||||
if (!remoteSha) {
|
||||
console.warn("pullLatest: could not resolve remote sha", { path: path.value })
|
||||
if (failureStatus) status.value = failureStatus
|
||||
return null
|
||||
return { raw: null, failureStatus }
|
||||
}
|
||||
const fileContent = await queryFileContent(user, repo, remoteSha)
|
||||
if (!fileContent) {
|
||||
@@ -96,7 +99,7 @@ export const useNoteFreshness = ({
|
||||
// Cached SHA may be stale — clear so the next click re-resolves it.
|
||||
if (usedCachedSha) latestSha.value = null
|
||||
status.value = "offline"
|
||||
return null
|
||||
return { raw: null, failureStatus: "offline" }
|
||||
}
|
||||
const { saveCacheNote } = prepareNoteCache(sha.value, path.value)
|
||||
await saveCacheNote(fileContent, {
|
||||
@@ -108,7 +111,7 @@ export const useNoteFreshness = ({
|
||||
lastCheckedAt.value = new Date()
|
||||
status.value = "verified"
|
||||
const { getRawContent } = markdownBuilder(sha.value)
|
||||
return getRawContent(fileContent)
|
||||
return { raw: getRawContent(fileContent), failureStatus: null }
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user