feat(06.2-02): frontend — is_shared badge fix + permission dropdown + View/Edit toggle

- DocumentCard.vue: fix Shared pill to read doc.is_shared (was doc.share_count > 0)
- ShareModal.vue: add permission select between handle input and submit button
- ShareModal.vue: replace static "view" span with View/Edit toggle group per share row
- ShareModal.vue: add handlePermissionChange with optimistic update + rollback on error
- documents.js: update shareDocument(docId, handle, permission='view') signature
- documents.js: add updateSharePermission(shareId, permission) action
- api/client.js: pass permission in createShare POST body
- api/client.js: add updateSharePermission PATCH helper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-05-31 15:07:04 +02:00
parent ea231853e9
commit 34b18a9f08
4 changed files with 69 additions and 8 deletions
+10 -2
View File
@@ -328,11 +328,19 @@ export function moveDocument(docId, folderId) {
// ── Shares ────────────────────────────────────────────────────────────────────
export function createShare(docId, recipientHandle) {
export function createShare(docId, recipientHandle, permission = 'view') {
return request('/api/shares', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ document_id: docId, recipient_handle: recipientHandle }),
body: JSON.stringify({ document_id: docId, recipient_handle: recipientHandle, permission }),
})
}
export function updateSharePermission(shareId, permission) {
return request(`/api/shares/${shareId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ permission }),
})
}
@@ -28,7 +28,7 @@
</div>
<!-- Shared indicator pill -->
<div v-if="doc.share_count > 0" class="mt-2">
<div v-if="doc.is_shared" class="mt-2">
<span class="bg-indigo-50 text-indigo-600 text-xs font-medium px-2 py-1 rounded-full">Shared</span>
</div>
</div>
+51 -2
View File
@@ -36,6 +36,14 @@
class="flex-1 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
@keydown.enter="submitShare"
/>
<select
v-model="permission"
aria-label="Permission level"
class="border border-gray-300 rounded-lg px-3 py-2 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-indigo-500 shrink-0"
>
<option value="view">Can view</option>
<option value="edit">Can edit</option>
</select>
<button
@click="submitShare"
:disabled="submitting || !handle.trim()"
@@ -51,6 +59,7 @@
<!-- Error -->
<p v-if="error" class="text-xs text-red-600 mt-2">{{ error }}</p>
<p v-if="permissionError" class="text-xs text-red-600 mt-2">{{ permissionError }}</p>
<!-- Separator -->
<div class="border-t border-gray-100 my-4"></div>
@@ -72,7 +81,24 @@
>
<div class="flex items-center gap-2">
<span class="text-sm text-gray-900">{{ share.recipient_handle }}</span>
<span class="text-xs bg-gray-100 text-gray-600 px-2 py-1 rounded-full font-medium">view</span>
<div
role="group"
:aria-label="`Permission for ${share.recipient_handle}`"
class="flex"
:class="{ 'opacity-50 pointer-events-none': updatingPermission.has(share.id) }"
>
<button
v-for="level in ['view', 'edit']"
:key="level"
:aria-pressed="share.permission === level"
:aria-label="`Change permission for ${share.recipient_handle} to ${level}`"
class="text-xs px-2 py-1 rounded-full font-medium transition-colors first:rounded-r-none last:rounded-l-none"
:class="share.permission === level
? 'bg-indigo-50 text-indigo-600 font-medium'
: 'bg-gray-100 text-gray-600'"
@click="share.permission !== level && handlePermissionChange(share.id, level)"
>{{ level === 'view' ? 'View' : 'Edit' }}</button>
</div>
</div>
<button
@click="handleRevoke(share.id)"
@@ -102,10 +128,13 @@ const emit = defineEmits(['close'])
const docsStore = useDocumentsStore()
const handle = ref('')
const permission = ref('view')
const submitting = ref(false)
const error = ref(null)
const permissionError = ref(null)
const shares = ref([])
const loadingShares = ref(false)
const updatingPermission = ref(new Set())
onMounted(async () => {
loadingShares.value = true
@@ -127,9 +156,10 @@ async function submitShare() {
error.value = null
try {
const newShare = await docsStore.shareDocument(props.doc.id, trimmed)
const newShare = await docsStore.shareDocument(props.doc.id, trimmed, permission.value)
shares.value.push(newShare)
handle.value = ''
permission.value = 'view'
} catch (e) {
if (e.status === 404) {
error.value = 'User not found. Check the handle and try again.'
@@ -143,6 +173,25 @@ async function submitShare() {
}
}
async function handlePermissionChange(shareId, newPermission) {
const share = shares.value.find(s => s.id === shareId)
if (!share) return
const oldPermission = share.permission
share.permission = newPermission
updatingPermission.value = new Set([...updatingPermission.value, shareId])
permissionError.value = null
try {
await docsStore.updateSharePermission(shareId, newPermission)
} catch (e) {
share.permission = oldPermission
permissionError.value = 'Failed to update permission.'
} finally {
const next = new Set(updatingPermission.value)
next.delete(shareId)
updatingPermission.value = next
}
}
async function handleRevoke(shareId) {
// Optimistic removal
const removedIdx = shares.value.findIndex(s => s.id === shareId)
+7 -3
View File
@@ -156,8 +156,8 @@ export const useDocumentsStore = defineStore('documents', () => {
total.value = Math.max(0, total.value - 1)
}
async function shareDocument(docId, recipientHandle) {
try { return await api.createShare(docId, recipientHandle) } catch (e) { throw e }
async function shareDocument(docId, recipientHandle, permission = 'view') {
try { return await api.createShare(docId, recipientHandle, permission) } catch (e) { throw e }
}
async function revokeShare(shareId) {
@@ -168,5 +168,9 @@ export const useDocumentsStore = defineStore('documents', () => {
try { return await api.listShares(docId) } catch (e) { throw e }
}
return { documents, total, loading, error, uploadProgress, currentFolderId, searchQuery, sortField, sortOrder, fetchDocuments, upload, remove, reclassify, moveToFolder, shareDocument, revokeShare, listShares }
async function updateSharePermission(shareId, permission) {
try { return await api.updateSharePermission(shareId, permission) } catch (e) { throw e }
}
return { documents, total, loading, error, uploadProgress, currentFolderId, searchQuery, sortField, sortOrder, fetchDocuments, upload, remove, reclassify, moveToFolder, shareDocument, revokeShare, listShares, updateSharePermission }
})