diff --git a/backend/src/ai-assistant/ai-assistant.service.ts b/backend/src/ai-assistant/ai-assistant.service.ts index 865eb03..80cbfb0 100644 --- a/backend/src/ai-assistant/ai-assistant.service.ts +++ b/backend/src/ai-assistant/ai-assistant.service.ts @@ -694,8 +694,6 @@ export class AiAssistantService { type: field.type, })); - console.log('fields:',fields); - const formatInstructions = parser.getFormatInstructions(); const today = new Date().toISOString(); diff --git a/backend/src/object/object.service.ts b/backend/src/object/object.service.ts index 6ead21f..dd3a1b1 100644 --- a/backend/src/object/object.service.ts +++ b/backend/src/object/object.service.ts @@ -1284,10 +1284,23 @@ export class ObjectService { if (missingIds.length > 0) { throw new NotFoundException(`Records not found: ${missingIds.join(', ')}`); } - - // Check if user can delete each record + + const deletableIds: string[] = []; + const deniedIds: string[] = []; + for (const record of records) { - await this.authService.assertCanPerformAction('delete', objectDefModel, record, user, knex); + const canDelete = await this.authService.canPerformAction( + 'delete', + objectDefModel, + record, + user, + knex, + ); + if (canDelete) { + deletableIds.push(record.id); + } else { + deniedIds.push(record.id); + } } // Ensure model is registered @@ -1295,14 +1308,23 @@ export class ObjectService { // Use Objection model const boundModel = await this.modelService.getBoundModel(resolvedTenantId, objectApiName); - await boundModel.query().whereIn('id', recordIds).delete(); + if (deletableIds.length > 0) { + await boundModel.query().whereIn('id', deletableIds).delete(); + } // Remove from search index await Promise.all( - recordIds.map((id) => this.removeIndexedRecord(resolvedTenantId, objectApiName, id)), + deletableIds.map((id) => + this.removeIndexedRecord(resolvedTenantId, objectApiName, id), + ), ); - return { success: true, deleted: recordIds.length }; + return { + success: true, + deleted: deletableIds.length, + deletedIds: deletableIds, + deniedIds, + }; } private async indexRecord( diff --git a/frontend/components/ui/checkbox/Checkbox.vue b/frontend/components/ui/checkbox/Checkbox.vue index 0909b8e..07a5337 100644 --- a/frontend/components/ui/checkbox/Checkbox.vue +++ b/frontend/components/ui/checkbox/Checkbox.vue @@ -18,7 +18,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits) diff --git a/frontend/components/views/ListView.vue b/frontend/components/views/ListView.vue index 23f43a6..2de24be 100644 --- a/frontend/components/views/ListView.vue +++ b/frontend/components/views/ListView.vue @@ -12,6 +12,7 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Badge } from '@/components/ui/badge' import { Checkbox } from '@/components/ui/checkbox' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import FieldRenderer from '@/components/fields/FieldRenderer.vue' import { ListViewConfig, ViewMode, FieldType } from '@/types/field-types' import { ChevronDown, ChevronUp, Search, Plus, Download, Trash2, Edit } from 'lucide-vue-next' @@ -49,11 +50,13 @@ const emit = defineEmits<{ }>() // State -const selectedRows = ref>(new Set()) +const normalizeId = (id: any) => String(id) +const selectedRowIds = ref([]) const searchQuery = ref('') const sortField = ref('') const sortDirection = ref<'asc' | 'desc'>('asc') const currentPage = ref(1) +const bulkAction = ref('delete') // Computed const visibleFields = computed(() => @@ -94,27 +97,39 @@ const showLoadMore = computed(() => ( )) const allSelected = computed({ - get: () => props.data.length > 0 && selectedRows.value.size === props.data.length, + get: () => props.data.length > 0 && selectedRowIds.value.length === props.data.length, set: (val: boolean) => { if (val) { - selectedRows.value = new Set(props.data.map(row => row.id)) + selectedRowIds.value = props.data.map(row => normalizeId(row.id)) } else { - selectedRows.value.clear() + selectedRowIds.value = [] } emit('row-select', getSelectedRows()) }, }) const getSelectedRows = () => { - return props.data.filter(row => selectedRows.value.has(row.id)) + const idSet = new Set(selectedRowIds.value) + return props.data.filter(row => idSet.has(normalizeId(row.id))) } const toggleRowSelection = (rowId: string) => { - if (selectedRows.value.has(rowId)) { - selectedRows.value.delete(rowId) + const normalizedId = normalizeId(rowId) + const nextSelection = new Set(selectedRowIds.value) + nextSelection.has(normalizedId) ? nextSelection.delete(normalizedId) : nextSelection.add(normalizedId) + selectedRowIds.value = Array.from(nextSelection) + emit('row-select', getSelectedRows()) +} + +const setRowSelection = (rowId: string, checked: boolean) => { + const normalizedId = normalizeId(rowId) + const nextSelection = new Set(selectedRowIds.value) + if (checked) { + nextSelection.add(normalizedId) } else { - selectedRows.value.add(rowId) + nextSelection.delete(normalizedId) } + selectedRowIds.value = Array.from(nextSelection) emit('row-select', getSelectedRows()) } @@ -136,6 +151,14 @@ const handleAction = (actionId: string) => { emit('action', actionId, getSelectedRows()) } +const handleBulkAction = () => { + if (bulkAction.value === 'delete') { + emit('delete', getSelectedRows()) + return + } + emit('action', bulkAction.value, getSelectedRows()) +} + const goToPage = (page: number) => { const nextPage = Math.min(Math.max(page, 1), availablePages.value) if (nextPage !== currentPage.value) { @@ -157,6 +180,19 @@ watch( } } ) + +watch( + () => props.data, + (rows) => { + const rowIds = new Set(rows.map(row => normalizeId(row.id))) + const nextSelection = selectedRowIds.value.filter(id => rowIds.has(id)) + if (nextSelection.length !== selectedRowIds.value.length) { + selectedRowIds.value = nextSelection + emit('row-select', getSelectedRows()) + } + }, + { deep: true } +)