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:
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 }
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user