285 lines
8.4 KiB
Vue
285 lines
8.4 KiB
Vue
<template>
|
|
<Dialog :open="open" @update:open="handleClose">
|
|
<DialogContent class="max-w-2xl">
|
|
<DialogHeader>
|
|
<DialogTitle>Share Record</DialogTitle>
|
|
<DialogDescription>
|
|
Grant access to this record to other users
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div class="space-y-6 py-4">
|
|
<!-- Existing Shares -->
|
|
<div v-if="shares.length > 0" class="space-y-3">
|
|
<h3 class="text-sm font-semibold">Current Shares</h3>
|
|
<div
|
|
v-for="share in shares"
|
|
:key="share.id"
|
|
class="flex items-center justify-between p-3 border rounded-lg"
|
|
>
|
|
<div class="flex-1">
|
|
<div class="font-medium">{{ share.granteeUser?.email || 'Unknown User' }}</div>
|
|
<div class="text-sm text-muted-foreground">
|
|
Permissions: {{ share.actions.join(', ') }}
|
|
<span v-if="share.fields">(Limited fields)</span>
|
|
</div>
|
|
<div v-if="share.expiresAt" class="text-xs text-muted-foreground">
|
|
Expires: {{ formatDate(share.expiresAt) }}
|
|
</div>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
@click="handleRevokeShare(share.id)"
|
|
>
|
|
<X class="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add New Share Form -->
|
|
<div class="space-y-4 border-t pt-4">
|
|
<h3 class="text-sm font-semibold">Add New Share</h3>
|
|
|
|
<div class="space-y-2">
|
|
<Label>User Email</Label>
|
|
<Input
|
|
v-model="newShare.userEmail"
|
|
placeholder="user@example.com"
|
|
type="email"
|
|
/>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<Label>Permissions</Label>
|
|
<div class="space-y-2">
|
|
<div class="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="perm-read"
|
|
:checked="newShare.permissions.read"
|
|
@update:checked="(val) => newShare.permissions.read = val"
|
|
/>
|
|
<Label for="perm-read" class="cursor-pointer">Read</Label>
|
|
</div>
|
|
<div class="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="perm-update"
|
|
:checked="newShare.permissions.update"
|
|
@update:checked="(val) => newShare.permissions.update = val"
|
|
/>
|
|
<Label for="perm-update" class="cursor-pointer">Update</Label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<div class="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="field-scoped"
|
|
:checked="newShare.fieldScoped"
|
|
@update:checked="(val) => newShare.fieldScoped = val"
|
|
/>
|
|
<Label for="field-scoped" class="cursor-pointer">Limit to specific fields</Label>
|
|
</div>
|
|
|
|
<div v-if="newShare.fieldScoped" class="ml-6 space-y-2 border-l-2 pl-4">
|
|
<Label class="text-sm">Select Fields</Label>
|
|
<div class="space-y-1 max-h-48 overflow-y-auto">
|
|
<div
|
|
v-for="field in fields"
|
|
:key="field.apiName"
|
|
class="flex items-center space-x-2"
|
|
>
|
|
<Checkbox
|
|
:id="`field-${field.apiName}`"
|
|
:checked="newShare.selectedFields.includes(field.apiName)"
|
|
@update:checked="(val) => handleFieldToggle(field.apiName, val)"
|
|
/>
|
|
<Label :for="`field-${field.apiName}`" class="cursor-pointer text-sm">
|
|
{{ field.label }}
|
|
</Label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<div class="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="has-expiry"
|
|
:checked="newShare.hasExpiry"
|
|
@update:checked="(val) => newShare.hasExpiry = val"
|
|
/>
|
|
<Label for="has-expiry" class="cursor-pointer">Set expiration date</Label>
|
|
</div>
|
|
|
|
<Input
|
|
v-if="newShare.hasExpiry"
|
|
v-model="newShare.expiryDate"
|
|
type="datetime-local"
|
|
class="ml-6"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" @click="handleClose">Cancel</Button>
|
|
<Button @click="handleAddShare" :disabled="!canAddShare || saving">
|
|
{{ saving ? 'Sharing...' : 'Share' }}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { X } from 'lucide-vue-next'
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Label } from '@/components/ui/label'
|
|
import { Checkbox } from '@/components/ui/checkbox'
|
|
|
|
interface Props {
|
|
open: boolean
|
|
objectDefinitionId: string
|
|
recordId: string
|
|
fields?: any[]
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
fields: () => []
|
|
})
|
|
|
|
const emit = defineEmits(['close', 'shared'])
|
|
|
|
const { api } = useApi()
|
|
const { toast } = useToast()
|
|
|
|
const shares = ref<any[]>([])
|
|
const loading = ref(false)
|
|
const saving = ref(false)
|
|
|
|
const newShare = ref({
|
|
userEmail: '',
|
|
permissions: {
|
|
read: true,
|
|
update: false,
|
|
},
|
|
fieldScoped: false,
|
|
selectedFields: [] as string[],
|
|
hasExpiry: false,
|
|
expiryDate: '',
|
|
})
|
|
|
|
const canAddShare = computed(() => {
|
|
return newShare.value.userEmail && (newShare.value.permissions.read || newShare.value.permissions.update)
|
|
})
|
|
|
|
const fetchShares = async () => {
|
|
try {
|
|
loading.value = true
|
|
shares.value = await api.get(`/shares/record/${props.objectDefinitionId}/${props.recordId}`)
|
|
} catch (e: any) {
|
|
console.error('Error fetching shares:', e)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const handleFieldToggle = (fieldKey: string, checked: boolean) => {
|
|
if (checked) {
|
|
if (!newShare.value.selectedFields.includes(fieldKey)) {
|
|
newShare.value.selectedFields.push(fieldKey)
|
|
}
|
|
} else {
|
|
newShare.value.selectedFields = newShare.value.selectedFields.filter(f => f !== fieldKey)
|
|
}
|
|
}
|
|
|
|
const handleAddShare = async () => {
|
|
try {
|
|
saving.value = true
|
|
|
|
// First, find user by email (you'll need an endpoint for this)
|
|
// For now, we'll assume the email is actually a user ID
|
|
const actions = []
|
|
if (newShare.value.permissions.read) actions.push('read')
|
|
if (newShare.value.permissions.update) actions.push('update')
|
|
|
|
const payload: any = {
|
|
objectDefinitionId: props.objectDefinitionId,
|
|
recordId: props.recordId,
|
|
granteeUserId: newShare.value.userEmail, // Should be user ID, not email
|
|
actions,
|
|
}
|
|
|
|
if (newShare.value.fieldScoped && newShare.value.selectedFields.length > 0) {
|
|
payload.fields = newShare.value.selectedFields
|
|
}
|
|
|
|
if (newShare.value.hasExpiry && newShare.value.expiryDate) {
|
|
payload.expiresAt = new Date(newShare.value.expiryDate).toISOString()
|
|
}
|
|
|
|
await api.post('/shares', payload)
|
|
|
|
toast.success('Record shared successfully')
|
|
await fetchShares()
|
|
|
|
// Reset form
|
|
newShare.value = {
|
|
userEmail: '',
|
|
permissions: { read: true, update: false },
|
|
fieldScoped: false,
|
|
selectedFields: [],
|
|
hasExpiry: false,
|
|
expiryDate: '',
|
|
}
|
|
|
|
emit('shared')
|
|
} catch (e: any) {
|
|
console.error('Error creating share:', e)
|
|
toast.error('Failed to share record')
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
const handleRevokeShare = async (shareId: string) => {
|
|
if (!confirm('Are you sure you want to revoke this share?')) return
|
|
|
|
try {
|
|
await api.delete(`/shares/${shareId}`)
|
|
toast.success('Share revoked successfully')
|
|
await fetchShares()
|
|
emit('shared')
|
|
} catch (e: any) {
|
|
console.error('Error revoking share:', e)
|
|
toast.error('Failed to revoke share')
|
|
}
|
|
}
|
|
|
|
const formatDate = (date: string) => {
|
|
return new Date(date).toLocaleDateString()
|
|
}
|
|
|
|
const handleClose = () => {
|
|
emit('close')
|
|
}
|
|
|
|
watch(() => props.open, (isOpen) => {
|
|
if (isOpen) {
|
|
fetchShares()
|
|
}
|
|
})
|
|
</script>
|