Skip to content

Commit ddb006b

Browse files
committed
bug #166
#166 supression de masse correction page export qui ne s affichait pas
1 parent d57c9ba commit ddb006b

5 files changed

Lines changed: 188 additions & 60 deletions

File tree

‎apps/api/src/core/backends/backends.service.ts‎

Lines changed: 49 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -315,46 +315,65 @@ export class BackendsService extends AbstractQueueProcessor {
315315

316316
if (!payload.length) throw new BadRequestException('No identities to disable');
317317

318+
const result = {};
319+
318320
for (const key of payload) {
319-
const identity = await this.identitiesService.findById<any>(key);
320-
if (identity.primaryEmployeeNumber !== null && identity.primaryEmployeeNumber !== '') {
321-
identity.inetOrgPerson.employeeNumber = identity.primaryEmployeeNumber;
322-
} else {
323-
//on prend la premiere pour envoyer une chaine et non un tableau pour la compatibilité ldap
324-
identity.inetOrgPerson.employeeNumber = identity.inetOrgPerson.employeeNumber[0];
325-
}
326-
if (!identity.lastBackendSync) {
327-
// l identité n'a jamais été symchronisée on la soft delete
328-
await this.identitiesService.model.findByIdAndUpdate(key, {
329-
$set: {
330-
state: IdentityState.DONT_SYNC,
331-
deletedFlag: true,
332-
},
321+
try {
322+
const identity = await this.identitiesService.findById<any>(key);
323+
if (!identity) {
324+
result[key] = { error: `Identity ${key} not found` };
325+
continue;
326+
}
327+
if (identity.primaryEmployeeNumber !== null && identity.primaryEmployeeNumber !== '') {
328+
identity.inetOrgPerson.employeeNumber = identity.primaryEmployeeNumber;
329+
} else if (Array.isArray(identity.inetOrgPerson?.employeeNumber)) {
330+
//on prend la premiere pour envoyer une chaine et non un tableau pour la compatibilité ldap
331+
identity.inetOrgPerson.employeeNumber = identity.inetOrgPerson.employeeNumber[0];
332+
}
333+
if (!identity.lastBackendSync) {
334+
// l identité n'a jamais été symchronisée on la soft delete
335+
// puis on poursuit avec le reste de la sélection (suppression en masse)
336+
await this.identitiesService.model.findByIdAndUpdate(key, {
337+
$set: {
338+
state: IdentityState.DONT_SYNC,
339+
deletedFlag: true,
340+
},
341+
});
342+
result[key] = { softDeleted: true };
343+
continue;
344+
}
345+
identities.push({
346+
action: ActionType.IDENTITY_DELETE,
347+
identity,
333348
});
334-
return [];
349+
} catch (error) {
350+
// une identité en erreur ne doit pas interrompre la suppression des autres
351+
this.logger.error(`Unable to prepare deletion of identity ${key}: ${error?.message}`, error?.stack);
352+
result[key] = { error: error?.message ?? 'Unknown error' };
335353
}
336-
identities.push({
337-
action: ActionType.IDENTITY_DELETE,
338-
identity,
339-
});
340354
}
341355

356+
if (!identities.length) return result;
357+
342358
const task: Document<Tasks> = await this.tasksService.create<Tasks>({
343359
jobs: identities.map((identity) => identity.identity._id),
344360
});
345361

346-
const result = {};
347362
for (const identity of identities) {
348-
const [executedJob] = await this.executeJob(identity.action, identity.identity._id, identity.identity, {
349-
...options,
350-
updateStatus: true,
351-
switchToProcessing: false,
352-
targetState: IdentityState.DONT_SYNC,
353-
dataState: DataStatusEnum.DELETED,
354-
task: task._id as unknown as Types.ObjectId,
355-
});
356-
result[identity.identity._id] = executedJob;
357-
// console.log(res);
363+
try {
364+
const [executedJob] = await this.executeJob(identity.action, identity.identity._id, identity.identity, {
365+
...options,
366+
updateStatus: true,
367+
switchToProcessing: false,
368+
targetState: IdentityState.DONT_SYNC,
369+
dataState: DataStatusEnum.DELETED,
370+
task: task._id as unknown as Types.ObjectId,
371+
});
372+
result[`${identity.identity._id}`] = executedJob;
373+
} catch (error) {
374+
this.logger.error(`Unable to delete identity ${identity.identity._id}: ${error?.message}`, error?.stack);
375+
result[`${identity.identity._id}`] = { error: error?.message ?? 'Unknown error' };
376+
}
358377
}
359378
return result;
360379
}

‎apps/web/src/components/core/twopan.vue‎

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -250,8 +250,23 @@ export default defineNuxtComponent({
250250
this.syncSelectionWithRows()
251251
},
252252
},
253+
// La sélection survit à un changement de page, mais pas à un changement de
254+
// filtre / recherche : les lignes conservées ne feraient plus partie du jeu affiché.
255+
filtersSignature: {
256+
handler(next: string, previous: string) {
257+
if (next !== previous) this.clearSelection()
258+
},
259+
},
253260
},
254261
computed: {
262+
/** Empreinte des filtres actifs, hors pagination / tri / route de détail. */
263+
filtersSignature(): string {
264+
const query = this.$route?.query || {}
265+
const entries = Object.entries(query)
266+
.filter(([key]) => !['limit', 'skip', 'page', 'read'].includes(key) && !key.startsWith('sort'))
267+
.sort(([a], [b]) => a.localeCompare(b))
268+
return JSON.stringify(entries)
269+
},
255270
visibleColumnsSelected(): QTableProps['visibleColumns'] {
256271
if (!this.visibleColumns || this.visibleColumns.length === 0) {
257272
return (this.columns || []).map((column) => column.name)
@@ -290,21 +305,29 @@ export default defineNuxtComponent({
290305
},
291306
},
292307
methods: {
293-
/**
294-
* Garde uniquement les lignes encore présentes dans `rows` (même clé `rowKey`)
295-
* et réaligne sur les objets courants. Sinon la sélection peut rester bloquée
296-
* sur d’anciennes lignes après changement de route / filtres / rechargement.
297-
*/
298308
tableRowKey(row: Record<string, unknown>): string {
299309
const rk = this.rowKey
300310
return typeof rk === 'function' ? String(rk(row)) : String(row?.[rk as string] ?? '')
301311
},
312+
/**
313+
* Réaligne la sélection sur les objets de la page courante (sinon Quasar ne
314+
* reconnaît plus les lignes cochées après un rechargement) tout en conservant
315+
* les lignes sélectionnées sur les autres pages : la sélection doit survivre
316+
* à un changement de page. Les doublons de clé sont éliminés.
317+
*/
302318
syncSelectionWithRows() {
303319
if (!this.selection || this.selection === 'none') return
304320
if (!Array.isArray(this.selected) || this.selected.length === 0) return
305321
const rows = this.rows || []
306322
const rowByKey = new Map(rows.map((r) => [this.tableRowKey(r), r]))
307-
const next = this.selected.map((s) => rowByKey.get(this.tableRowKey(s))).filter((r) => r !== undefined)
323+
const seen = new Set<string>()
324+
const next = [] as typeof this.selected
325+
for (const s of this.selected) {
326+
const key = this.tableRowKey(s)
327+
if (seen.has(key)) continue
328+
seen.add(key)
329+
next.push(rowByKey.get(key) ?? s)
330+
}
308331
if (next.length !== this.selected.length || next.some((r, i) => r !== this.selected[i])) {
309332
this.selected = next
310333
}

‎apps/web/src/components/pages/identities/modals/delete-many.vue‎

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ q-dialog(
3838
q-item-section
3939
q-item-label.text-weight-medium(lines="2") {{ item.label }}
4040
q-item-label.text-caption.text-grey-6(lines="1" style="font-family: ui-monospace, monospace") {{ item.idShort }}
41+
.identity-modal-body-footer(v-if="allIdentitiesCount > selectedRows.length")
42+
q-separator.q-my-md
43+
q-checkbox(
44+
dense
45+
color="negative"
46+
v-model="deleteAllIdentities",
47+
:label="checkboxLabel",
48+
)
4149
q-card-actions.identity-modal-actions(align="right")
4250
q-btn.identity-modal-btn-cancel(
4351
outline
@@ -54,20 +62,24 @@ q-dialog(
5462
color="negative"
5563
icon-right="mdi-check"
5664
label="Supprimer"
57-
:disable="selectedRows.length === 0"
65+
:disable="selectedRows.length === 0 && !deleteAllIdentities"
5866
@click="syncIdentities"
5967
)
6068
</template>
6169

6270
<script lang="ts" setup>
63-
import { computed } from 'vue'
71+
import { computed, ref } from 'vue'
6472
import { useDialogPluginComponent, useQuasar } from 'quasar'
6573
6674
const props = defineProps({
6775
selectedIdentities: {
6876
type: Array,
6977
default: () => [],
7078
},
79+
allIdentitiesCount: {
80+
type: Number,
81+
default: 0,
82+
},
7183
})
7284
7385
defineEmits([...useDialogPluginComponent.emits])
@@ -159,15 +171,24 @@ const identityListItems = computed(() => {
159171
})
160172
})
161173
162-
const mainText = computed(
163-
() =>
164-
`Vous allez supprimer ${selectedRows.value.length} identité${selectedRows.value.length > 1 ? 's' : ''}. Cette action est irréversible. Vérifiez la liste puis confirmez.`,
174+
const deleteAllIdentities = ref(false)
175+
176+
const mainText = computed(() => {
177+
if (deleteAllIdentities.value) {
178+
return `Vous allez supprimer les ${props.allIdentitiesCount} identité${props.allIdentitiesCount > 1 ? 's' : ''} correspondant au filtre actuel, et pas seulement la sélection ci-dessous. Cette action est irréversible.`
179+
}
180+
181+
return `Vous allez supprimer ${selectedRows.value.length} identité${selectedRows.value.length > 1 ? 's' : ''}. Cette action est irréversible. Vérifiez la liste puis confirmez.`
182+
})
183+
184+
const checkboxLabel = computed(
185+
() => `Supprimer toutes les identités du filtre actuel (${props.allIdentitiesCount} identité${props.allIdentitiesCount > 1 ? 's' : ''})`,
165186
)
166187
167188
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
168189
169190
const syncIdentities = () => {
170-
onDialogOK({ success: true })
191+
onDialogOK({ success: true, deleteAllIdentities: deleteAllIdentities.value })
171192
}
172193
173194
const cancelSync = () => {
@@ -210,6 +231,10 @@ const cancelSync = () => {
210231
flex-shrink: 0;
211232
}
212233
234+
.identity-modal-body-footer {
235+
flex-shrink: 0;
236+
}
237+
213238
.identity-modal-lede {
214239
line-height: 1.55;
215240
margin-bottom: 1rem;

‎apps/web/src/pages/identities/export.vue‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ q-page.container.q-pa-sm
6565
q-table(
6666
:rows-per-page-options="[20,50,0]"
6767
:columns="exportColumns"
68-
:rows="identities"
68+
:rows="identities || []"
6969
row-key="_id"
7070
flat
7171
dense
@@ -74,6 +74,8 @@ q-page.container.q-pa-sm
7474

7575
<script lang="ts">
7676
import type { LocationQueryValue } from 'vue-router'
77+
// Import explicite : l'auto-import d'unimport ne détecte pas cette constante dans ce fichier.
78+
import { DEFAULT_IDENTITY_FILTER_FIELD_PATHS } from '~/composables/useFilterFieldOptions'
7779
7880
export default defineNuxtComponent({
7981
name: 'IdentitiesExportPage',
@@ -95,11 +97,13 @@ export default defineNuxtComponent({
9597
9698
let rowsData = null
9799
const queryWithoutRead = computed(() => {
98-
const { read, ...rest } = route.query
100+
// L'export porte sur tout le résultat filtré : la pagination de la table
101+
// (`limit`/`page`, présents dans l'URL au retour de /identities/table) ne doit pas être reprise.
102+
const { read, limit, page, ...rest } = route.query
99103
return {
100-
limit: 9999,
101104
...getSearchFieldsQuery(),
102105
...rest,
106+
limit: 9999,
103107
}
104108
})
105109
@@ -110,8 +114,11 @@ export default defineNuxtComponent({
110114
} = await useHttp('/management/identities/validation', {
111115
method: 'GET',
112116
transform: (result: any) => {
113-
const allFields = result.data.flatMap((enr) => {
114-
return Object.keys(enr[enr.name].properties)
117+
const schemas = Array.isArray(result?.data) ? result.data : []
118+
const allFields = schemas.flatMap((enr) => {
119+
// Un schéma YAML vide ou sans `properties` ne doit pas casser la page.
120+
const properties = enr?.[enr?.name]?.properties
121+
return properties ? Object.keys(properties) : []
115122
})
116123
117124
const columns = allFields.map((enr) => {

0 commit comments

Comments
 (0)