2 Commits

Author SHA1 Message Date
Francisco Gaona
6593fecca7 WIP - saving expires at for sharing records 2025-12-31 05:01:27 +01:00
Francisco Gaona
75b7325cea WIP - use objection for record shares 2025-12-30 21:46:37 +01:00
3 changed files with 66 additions and 33 deletions

View File

@@ -21,13 +21,13 @@ export class RecordShare extends BaseModel {
}; };
} }
// Override BaseModel hooks to prevent automatic timestamp handling // Don't auto-set timestamps - let DB defaults handle them
$beforeInsert(queryContext: any) { $beforeInsert() {
// Don't set timestamps - let database defaults handle it // Don't call super - skip BaseModel's timestamp logic
} }
$beforeUpdate(opt: any, queryContext: any) { $beforeUpdate() {
// Don't set timestamps - let database defaults handle it // Don't call super - skip BaseModel's timestamp logic
} }
id!: string; id!: string;

View File

@@ -147,39 +147,43 @@ export class RecordSharingController {
if (existingShare) { if (existingShare) {
// Update existing share // Update existing share
await knex('record_shares') const updated = await RecordShare.query(knex)
.where({ id: existingShare.id }) .patchAndFetchById(existingShare.id, {
.update({ accessLevel: {
accessLevel: JSON.stringify({
canRead: data.canRead, canRead: data.canRead,
canEdit: data.canEdit, canEdit: data.canEdit,
canDelete: data.canDelete, canDelete: data.canDelete,
}), },
expiresAt: data.expiresAt ? data.expiresAt : null, // Convert ISO string to MySQL datetime format
updatedAt: knex.fn.now(), expiresAt: data.expiresAt
}); ? knex.raw('?', [new Date(data.expiresAt).toISOString().slice(0, 19).replace('T', ' ')])
: null,
} as any);
return RecordShare.query(knex) return RecordShare.query(knex)
.findById(existingShare.id) .findById(updated.id)
.withGraphFetched('[granteeUser]'); .withGraphFetched('[granteeUser]');
} }
// Create new share // Create new share
const [shareId] = await knex('record_shares').insert({ const share = await RecordShare.query(knex).insertAndFetch({
objectDefinitionId: objectDef.id, objectDefinitionId: objectDef.id,
recordId, recordId,
granteeUserId: data.granteeUserId, granteeUserId: data.granteeUserId,
grantedByUserId: currentUser.userId, grantedByUserId: currentUser.userId,
accessLevel: JSON.stringify({ accessLevel: {
canRead: data.canRead, canRead: data.canRead,
canEdit: data.canEdit, canEdit: data.canEdit,
canDelete: data.canDelete, canDelete: data.canDelete,
}), },
expiresAt: data.expiresAt ? data.expiresAt : null, // Convert ISO string to MySQL datetime format: YYYY-MM-DD HH:MM:SS
}); expiresAt: data.expiresAt
? knex.raw('?', [new Date(data.expiresAt).toISOString().slice(0, 19).replace('T', ' ')])
: null,
} as any);
return RecordShare.query(knex) return RecordShare.query(knex)
.findById(shareId) .findById(share.id)
.withGraphFetched('[granteeUser]'); .withGraphFetched('[granteeUser]');
} }
@@ -235,11 +239,9 @@ export class RecordSharingController {
} }
// Revoke the share (soft delete) // Revoke the share (soft delete)
await knex('record_shares') await RecordShare.query(knex)
.where({ id: shareId }) .patchAndFetchById(shareId, {
.update({ revokedAt: knex.fn.now() as any,
revokedAt: knex.fn.now(),
updatedAt: knex.fn.now(),
}); });
return { success: true }; return { success: true };

View File

@@ -146,12 +146,13 @@
<div class="space-y-2"> <div class="space-y-2">
<Label for="expiresAt">Expires At (Optional)</Label> <Label for="expiresAt">Expires At (Optional)</Label>
<Input <div class="flex gap-2">
id="expiresAt" <DatePicker
v-model="newShare.expiresAt" v-model="expiresDate"
type="datetime-local" placeholder="Select date"
placeholder="Never" class="flex-1"
/> />
</div>
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
@@ -178,6 +179,7 @@ import { Input } from '~/components/ui/input';
import { Label } from '~/components/ui/label'; import { Label } from '~/components/ui/label';
import { Badge } from '~/components/ui/badge'; import { Badge } from '~/components/ui/badge';
import Checkbox from '~/components/ui/checkbox.vue'; import Checkbox from '~/components/ui/checkbox.vue';
import DatePicker from '~/components/ui/date-picker/DatePicker.vue';
import { UserPlus, Trash2, Users } from 'lucide-vue-next'; import { UserPlus, Trash2, Users } from 'lucide-vue-next';
interface Props { interface Props {
@@ -206,6 +208,24 @@ const newShare = ref({
expiresAt: '', expiresAt: '',
}); });
const expiresDate = ref<Date | null>(null);
const expiresTime = ref('');
// Computed property to combine date and time into ISO string
const combinedExpiresAt = computed(() => {
if (!expiresDate.value) return '';
const date = new Date(expiresDate.value);
if (expiresTime.value) {
const [hours, minutes] = expiresTime.value.split(':');
date.setHours(parseInt(hours), parseInt(minutes), 0, 0);
} else {
date.setHours(23, 59, 59, 999); // Default to end of day
}
return date.toISOString();
});
// Filter out users who already have shares // Filter out users who already have shares
const availableUsers = computed(() => { const availableUsers = computed(() => {
const sharedUserIds = new Set(shares.value.map(s => s.granteeUserId)); const sharedUserIds = new Set(shares.value.map(s => s.granteeUserId));
@@ -244,6 +264,10 @@ const loadUsers = async () => {
const createShare = async () => { const createShare = async () => {
try { try {
sharing.value = true; sharing.value = true;
const expiresAtValue = combinedExpiresAt.value;
console.log('Creating share, expiresAt value:', expiresAtValue);
const payload: any = { const payload: any = {
granteeUserId: newShare.value.userId, granteeUserId: newShare.value.userId,
canRead: newShare.value.canRead, canRead: newShare.value.canRead,
@@ -252,10 +276,15 @@ const createShare = async () => {
}; };
// Only include expiresAt if it has a value // Only include expiresAt if it has a value
if (newShare.value.expiresAt && newShare.value.expiresAt.trim()) { if (expiresAtValue) {
payload.expiresAt = newShare.value.expiresAt; payload.expiresAt = expiresAtValue;
console.log('Including expiresAt in payload:', payload.expiresAt);
} else {
console.log('Skipping expiresAt - no date selected');
} }
console.log('Final payload:', payload);
await api.post( await api.post(
`/runtime/objects/${props.objectApiName}/records/${props.recordId}/shares`, `/runtime/objects/${props.objectApiName}/records/${props.recordId}/shares`,
payload payload
@@ -269,6 +298,8 @@ const createShare = async () => {
canDelete: false, canDelete: false,
expiresAt: '', expiresAt: '',
}; };
expiresDate.value = null;
expiresTime.value = '';
await loadShares(); await loadShares();
} catch (e: any) { } catch (e: any) {
console.error('Failed to share record:', e); console.error('Failed to share record:', e);