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 ────────────────────────────────────────────────────────────────────
|
// ── Shares ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function createShare(docId, recipientHandle) {
|
export function createShare(docId, recipientHandle, permission = 'view') {
|
||||||
return request('/api/shares', {
|
return request('/api/shares', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
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>
|
</div>
|
||||||
|
|
||||||
<!-- Shared indicator pill -->
|
<!-- 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>
|
<span class="bg-indigo-50 text-indigo-600 text-xs font-medium px-2 py-1 rounded-full">Shared</span>
|
||||||
</div>
|
</div>
|
||||||
</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"
|
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"
|
@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
|
<button
|
||||||
@click="submitShare"
|
@click="submitShare"
|
||||||
:disabled="submitting || !handle.trim()"
|
:disabled="submitting || !handle.trim()"
|
||||||
@@ -51,6 +59,7 @@
|
|||||||
|
|
||||||
<!-- Error -->
|
<!-- Error -->
|
||||||
<p v-if="error" class="text-xs text-red-600 mt-2">{{ error }}</p>
|
<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 -->
|
<!-- Separator -->
|
||||||
<div class="border-t border-gray-100 my-4"></div>
|
<div class="border-t border-gray-100 my-4"></div>
|
||||||
@@ -72,7 +81,24 @@
|
|||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-sm text-gray-900">{{ share.recipient_handle }}</span>
|
<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>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="handleRevoke(share.id)"
|
@click="handleRevoke(share.id)"
|
||||||
@@ -102,10 +128,13 @@ const emit = defineEmits(['close'])
|
|||||||
const docsStore = useDocumentsStore()
|
const docsStore = useDocumentsStore()
|
||||||
|
|
||||||
const handle = ref('')
|
const handle = ref('')
|
||||||
|
const permission = ref('view')
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const error = ref(null)
|
const error = ref(null)
|
||||||
|
const permissionError = ref(null)
|
||||||
const shares = ref([])
|
const shares = ref([])
|
||||||
const loadingShares = ref(false)
|
const loadingShares = ref(false)
|
||||||
|
const updatingPermission = ref(new Set())
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
loadingShares.value = true
|
loadingShares.value = true
|
||||||
@@ -127,9 +156,10 @@ async function submitShare() {
|
|||||||
error.value = null
|
error.value = null
|
||||||
|
|
||||||
try {
|
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)
|
shares.value.push(newShare)
|
||||||
handle.value = ''
|
handle.value = ''
|
||||||
|
permission.value = 'view'
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.status === 404) {
|
if (e.status === 404) {
|
||||||
error.value = 'User not found. Check the handle and try again.'
|
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) {
|
async function handleRevoke(shareId) {
|
||||||
// Optimistic removal
|
// Optimistic removal
|
||||||
const removedIdx = shares.value.findIndex(s => s.id === shareId)
|
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)
|
total.value = Math.max(0, total.value - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function shareDocument(docId, recipientHandle) {
|
async function shareDocument(docId, recipientHandle, permission = 'view') {
|
||||||
try { return await api.createShare(docId, recipientHandle) } catch (e) { throw e }
|
try { return await api.createShare(docId, recipientHandle, permission) } catch (e) { throw e }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function revokeShare(shareId) {
|
async function revokeShare(shareId) {
|
||||||
@@ -168,5 +168,9 @@ export const useDocumentsStore = defineStore('documents', () => {
|
|||||||
try { return await api.listShares(docId) } catch (e) { throw e }
|
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