Skip to content

Commit

Permalink
fiche-action: bulk edit for libreTags
Browse files Browse the repository at this point in the history
  • Loading branch information
farnoux committed Dec 11, 2024
1 parent 0b531c2 commit 2fc7514
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 152 deletions.
87 changes: 87 additions & 0 deletions backend/src/fiches/bulk-edit/bulk-edit.router.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
getTestRouter,
} from '../../../test/common/app-utils';
import { AuthenticatedUser } from '../../auth/models/auth.models';
import { libreTagTable } from '../../taxonomie/models/libre-tag.table';
import { AppRouter, TrpcRouter } from '../../trpc/trpc.router';
import { ficheActionLibreTagTable } from '../models/fiche-action-libre-tag.table';
import { ficheActionPiloteTable } from '../models/fiche-action-pilote.table';
import {
FicheActionStatutsEnumType,
Expand Down Expand Up @@ -64,6 +66,17 @@ describe('BulkEditRouter', () => {
.groupBy(ficheActionPiloteTable.ficheId);
}

function getFichesWithLibreTags(ficheIds: number[]) {
return db.db
.select({
ficheId: ficheActionLibreTagTable.ficheId,
libreTagIds: sql`array_remove(array_agg(${ficheActionLibreTagTable.libreTagId}), NULL)`,
})
.from(ficheActionLibreTagTable)
.where(inArray(ficheActionLibreTagTable.ficheId, ficheIds))
.groupBy(ficheActionLibreTagTable.ficheId);
}

beforeAll(async () => {
const app = await getTestApp();
router = await getTestRouter(app);
Expand Down Expand Up @@ -167,6 +180,80 @@ describe('BulkEditRouter', () => {
});
});

test('authenticated, bulk edit `libreTags`', async () => {
function createLibreTagIds() {
return db.db
.insert(libreTagTable)
.values([
{
collectiviteId: COLLECTIVITE_ID,
nom: 'tag-1',
},
{
collectiviteId: COLLECTIVITE_ID,
nom: 'tag-2',
},
])
.returning()
.then((tags) => tags.map((tag) => tag.id));
}

const caller = router.createCaller({ user: yoloDodo });
const tagIds = await createLibreTagIds();

const input = {
ficheIds,
libreTags: {
add: [{ id: tagIds[0] }],
},
} satisfies Input;

const result = await caller.plans.fiches.bulkEdit(input);
expect(result).toBeUndefined();

// Verify that all fiches have been updated with libreTags
const fiches = await getFichesWithLibreTags(ficheIds);

for (const fiche of fiches) {
expect(fiche.libreTagIds).toContain(input.libreTags.add[0].id);
}

// Add again the same libreTags to check there is no conflict error
await caller.plans.fiches.bulkEdit(input);
expect(result).toBeUndefined();

// Remove one pilote and add another one
const input2 = {
ficheIds,
libreTags: {
add: [{ id: tagIds[1] }],
remove: [{ id: input.libreTags.add[0].id }],
},
} satisfies Input;

await caller.plans.fiches.bulkEdit(input2);
expect(result).toBeUndefined();

// Verify that all fiches have been updated with libreTags
const updatedFiches = await getFichesWithLibreTags(ficheIds);

for (const fiche of updatedFiches) {
expect(fiche.libreTagIds).toContain(input2.libreTags.add[0].id);
expect(fiche.libreTagIds).not.toContain(input.libreTags.add[0].id);
}

// Delete inserted or existing pilotes after test
onTestFinished(async () => {
await db.db
.delete(ficheActionLibreTagTable)
.where(inArray(ficheActionLibreTagTable.ficheId, ficheIds));

await db.db
.delete(libreTagTable)
.where(inArray(libreTagTable.id, tagIds));
});
});

test('authenticated, bulk edit `priorite`', async () => {
const caller = router.createCaller({ user: yoloDodo });

Expand Down
39 changes: 37 additions & 2 deletions backend/src/fiches/bulk-edit/bulk-edit.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { and, inArray, or } from 'drizzle-orm';
import z from 'zod';
import { AuthService } from '../../auth/services/auth.service';
import DatabaseService from '../../common/services/database.service';
import { ficheActionLibreTagTable } from '../models/fiche-action-libre-tag.table';
import { ficheActionPiloteTable } from '../models/fiche-action-pilote.table';
import {
ficheActionSchema,
Expand All @@ -28,9 +29,13 @@ export class BulkEditService {
dateFin: ficheActionSchema.shape.dateFin.optional(),
ameliorationContinue:
ficheActionSchema.shape.ameliorationContinue.optional(),

pilotes: listSchema(
updateFicheActionRequestSchema.shape.pilotes.unwrap().unwrap()
),
libreTags: listSchema(
updateFicheActionRequestSchema.shape.libresTag.unwrap().unwrap()
),
});

async bulkEdit(
Expand All @@ -52,7 +57,7 @@ export class BulkEditService {
NiveauAcces.EDITION
);

const { pilotes, ...plainValues } = params;
const { pilotes, libreTags, ...plainValues } = params;

await this.db.transaction(async (tx) => {
// Update plain values
Expand All @@ -63,7 +68,7 @@ export class BulkEditService {
.where(inArray(ficheActionTable.id, ficheIds));
}

// Update external relations
// Update external relation `pilotes`
if (pilotes !== undefined) {
if (pilotes.add?.length) {
const values = ficheIds.flatMap((ficheId) => {
Expand Down Expand Up @@ -101,6 +106,36 @@ export class BulkEditService {
);
}
}

// Update external relation `libreTags`
if (libreTags !== undefined) {
if (libreTags.add?.length) {
const values = ficheIds.flatMap((ficheId) => {
return (libreTags.add ?? []).map((tag) => ({
ficheId,
libreTagId: tag.id,
}));
});

await tx
.insert(ficheActionLibreTagTable)
.values(values)
.onConflictDoNothing();
}

if (libreTags.remove?.length) {
const ids = libreTags.remove.map((tag) => tag.id) as number[];

await tx
.delete(ficheActionLibreTagTable)
.where(
and(
inArray(ficheActionLibreTagTable.ficheId, ficheIds),
inArray(ficheActionLibreTagTable.libreTagId, ids)
)
);
}
}
});
}
}
Expand Down
11 changes: 2 additions & 9 deletions backend/src/fiches/models/update-fiche-action.request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,17 +80,10 @@ export const updateFicheActionRequestSchema = updateFicheActionSchema.extend({
actions: actionRelationSchema.pick({ id: true }).array().nullish(),
indicateurs: indicateurDefinitionSchema.pick({ id: true }).array().nullish(),
services: serviceTagSchema.pick({ id: true }).array().nullish(),
financeurs: z.array(financeurWithMontantSchema).nullish(),
financeurs: financeurWithMontantSchema.array().nullish(),
fichesLiees: ficheActionSchema.pick({ id: true }).array().nullish(),
resultatsAttendus: effetAttenduSchema.pick({ id: true }).array().nullish(),
libresTag: z
.array(
z.union([
libreTagSchema.pick({ id: true }),
libreTagSchema.pick({ nom: true }),
])
)
.nullish(),
libresTag: libreTagSchema.pick({ id: true }).array().nullish(),
});

export type UpdateFicheActionRequestType = z.infer<
Expand Down
102 changes: 3 additions & 99 deletions backend/src/fiches/services/fiches-action-update.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { ficheActionAxeTable } from '../models/fiche-action-axe.table';
import { ficheActionEffetAttenduTable } from '../models/fiche-action-effet-attendu.table';
import { ficheActionFinanceurTagTable } from '../models/fiche-action-financeur-tag.table';
import { ficheActionIndicateurTable } from '../models/fiche-action-indicateur.table';
import { ficheActionLibreTagTable } from '../models/fiche-action-libre-tag.table';
import { ficheActionLienTable } from '../models/fiche-action-lien.table';
import {
ficheActionNoteTable,
Expand All @@ -38,8 +39,6 @@ import {
} from '../models/fiche-action.table';
import { UpdateFicheActionRequestType } from '../models/update-fiche-action.request';
import FicheService from './fiche.service';
import { ficheActionLibreTagTable } from '../models/fiche-action-libre-tag.table';
import { libreTagTable } from '../../taxonomie/models/libre-tag.table';

type TxType = PgTransaction<
PostgresJsQueryResultHKT,
Expand Down Expand Up @@ -323,40 +322,17 @@ export default class FichesActionUpdateService {
}

if (libresTag !== undefined) {
const { collectiviteId } = await this.getCollectiviteIdForFiche(
tx,
ficheActionId
);

// tagMap = new Map([['Mobility', 1], ['Transport', 2], ['Bicycle', 3]]);
const tagMap = await this.getOrCreateLibreTag(
tx,
libresTag,
collectiviteId
);

// Example of libresTagWithResolvedIds:
// libresTagWithResolvedIds = [
// { id: 1 },
// { id: 2 },
// { id: 3 }
// ]
const libresTagWithResolvedIds = this.resolveLibreTagIds(
libresTag,
tagMap
);

// Delete existing relations
await tx
.delete(ficheActionLibreTagTable)
.where(eq(ficheActionLibreTagTable.ficheId, ficheActionId));

// Insert new relations
if (libresTagWithResolvedIds.length > 0) {
if (libresTag !== null && libresTag.length > 0) {
updatedLibresTag = await tx
.insert(ficheActionLibreTagTable)
.values(
libresTagWithResolvedIds.map((relation) => ({
libresTag.map((relation) => ({
ficheId: ficheActionId,
libreTagId: relation.id,
createdBy: tokenInfo.id,
Expand Down Expand Up @@ -480,78 +456,6 @@ export default class FichesActionUpdateService {
}));
}

private async getCollectiviteIdForFiche(tx: TxType, ficheActionId: number) {
return await tx
.select({ collectiviteId: ficheActionTable.collectiviteId })
.from(ficheActionTable)
.where(eq(ficheActionTable.id, ficheActionId))
.then((rows: { collectiviteId: number }[]) => rows[0]);
}

/**
* Create or get libre tags
* @param tx - current database transaction
* @param libresTag - array of libre tags to create or get
* @param collectiviteId - id of the collectivite
* @returns a map of name -> id for the newly created or existing libre tags
*/
private async getOrCreateLibreTag(
tx: TxType,
libresTag: { id?: number; nom?: string }[] | null,
collectiviteId: number
): Promise<Map<string, number>> {
const tagMap = new Map<string, number>();

if (!libresTag) {
return tagMap;
}

for (const tag of libresTag) {
if (this.isNewTag(tag)) {
const existingTag = await this.findExistingLibreTagByName(tx, tag.nom!);
if (existingTag) {
tagMap.set(tag.nom!, existingTag.id);
} else {
const [newTag] = await tx
.insert(libreTagTable)
.values({
nom: tag.nom!,
collectiviteId: collectiviteId,
})
.returning();
tagMap.set(tag.nom!, newTag.id);
}
}
}
return tagMap;
}

private isNewTag(tag: { id?: number; nom?: string }) {
return !tag.id && tag.nom;
}

private resolveLibreTagIds(
libresTag: { id?: number; nom?: string }[] | null,
tagMap: Map<string, number>
): { id: number }[] {
if (!libresTag) {
return [];
}
return libresTag
.map((tag) => ({
id: tag.id ?? tagMap.get(tag.nom ?? ''),
}))
.filter((tag): tag is { id: number } => tag.id !== undefined);
}

private async findExistingLibreTagByName(tx: TxType, nom: string) {
return tx
.select()
.from(libreTagTable)
.where(eq(libreTagTable.nom, nom))
.then((rows: { id: number; nom: string }[]) => rows[0]);
}

/** Insère ou met à jour des notes de suivi */
async upsertNotes(
ficheId: number,
Expand Down
Loading

0 comments on commit 2fc7514

Please sign in to comment.