Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat(contact-tagging): Add support tagable contacts with bulk action #63

Merged
merged 56 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
fbeb394
chore(cleanup): Removed unused isPartial tsdoc property
JumpLink Oct 29, 2024
1415ca7
feat(contact-tagging): Frontend: Contacts made selectable
JumpLink Oct 29, 2024
5b43972
feat(contact-tagging): Frontend: Initial tag manager page
JumpLink Oct 29, 2024
c27bc03
feat(contact-tagging): Tag translations centralised
JumpLink Oct 30, 2024
566c104
feat(contact-tagging): Moved pages/admin/callouts/ToggleTagButton.vue…
JumpLink Oct 30, 2024
75b40e3
chore(cleanup): Split utils index into multiple files
JumpLink Oct 30, 2024
d813187
feat(contact-tagging): Frontend: Add general tag manager
JumpLink Oct 30, 2024
1b7e661
feat(contact-tagging): Frontend: Show selected contact count
JumpLink Oct 31, 2024
d231a73
chore(doc): Update documentaion for new typeorm migration path
JumpLink Oct 31, 2024
3b18ba9
feat(contact-tagging): Backend: TypeORM contact model changes tags
JumpLink Oct 31, 2024
571f7eb
feat(contact-tagging): Backend: Adjust TypeORM migration file to migr…
JumpLink Oct 31, 2024
9c8cb2d
feat(contact-tagging): Backend: Initial working contact tag implement…
JumpLink Oct 31, 2024
1a1a0ce
Merge branch 'main' into feat/contact-tagging
JumpLink Nov 5, 2024
b1d46e7
fix(type): Due to the new rule ‘--isolatedDeclarations’
JumpLink Nov 5, 2024
7e9f917
feat(contact-tagging): Add model for ContactTagAssignment for more co…
JumpLink Nov 5, 2024
3f2ff9c
chore(doc): Moved deploy beabee on the server to root README.md
JumpLink Nov 6, 2024
85e711a
feat(contact-tagging): Frontend: Add support for tag filters
JumpLink Nov 6, 2024
1c6423c
feat(contact-tagging): Backend: Tag Controller with get, post and patch
JumpLink Nov 6, 2024
701cc5a
feat(contact-tagging): Backend: ContactTagService and delete controll…
JumpLink Nov 6, 2024
4773853
feat(contact-tagging): Merged TagTransformers
JumpLink Nov 7, 2024
8af97a6
feat(contact-tagging): Backend: Consolidate tag loading logic
JumpLink Nov 7, 2024
08f63bb
feat(contact-tagging): Backend: Moved loadEntityTags method to TagTra…
JumpLink Nov 7, 2024
6ea07e5
feat(contact-tagging): Backend: Moved ContactTagService methods to Ta…
JumpLink Nov 7, 2024
ee6947e
feat(contact-tagging): Backend: Adapt import steady script for the ne…
JumpLink Nov 7, 2024
c7377dd
chore(doc): Fixed some documentations
JumpLink Nov 7, 2024
49e16ce
feat(contact-tagging): Backend: Removed contact profile tag prop / col
JumpLink Nov 7, 2024
cc8d8af
chore(doc): Fixed formatting
JumpLink Nov 7, 2024
04768b8
feat(contact-tagging): Get batch contacts tags update working 🎉
JumpLink Nov 8, 2024
07b1ff9
feat(contact-tagging): Fixed contacts tags filter handler 😑
JumpLink Nov 8, 2024
aedb972
chore(doc): Add documentation for filter handlers
JumpLink Nov 8, 2024
d2a08d8
feat(contact-tagging): Backend: Tag filter handlers unified
JumpLink Nov 8, 2024
2227e2f
fix: Circular Dependencies
JumpLink Nov 8, 2024
b6c23c9
feat(contact-tagging): Backend: Cleanup tag filter handler method
JumpLink Nov 12, 2024
1925031
fix(api): Replace Partial<DTO> with concrete PartialDTO class
JumpLink Nov 12, 2024
f964c4f
feat(contact-tagging): Backend: Cleanup: Removed empty rules
JumpLink Nov 12, 2024
7267ac5
fix(api): Fix type checking for createPartialDTO<T>
JumpLink Nov 12, 2024
7a2c0c4
feat(contact-tagging): Frontend: Overview view for tags with the call…
JumpLink Nov 12, 2024
94a1816
fix(contact-tagging): Revert createPartialDTO and use a normal class …
JumpLink Nov 12, 2024
f43dbd2
feat(contact-tagging): Frontend: Tag toggle button aligned with exist…
JumpLink Nov 12, 2024
91d9a05
chore(contact-tagging): Resolved merge conflict
JumpLink Nov 12, 2024
efd03fa
fix(contact-tagging): Try to fix latest migration file
JumpLink Nov 13, 2024
5f9c356
feat(contact-tagging): Frontend: Make tags clickable
JumpLink Nov 13, 2024
471c0d8
chore(contact-tagging): Frontend: Cleanups
JumpLink Nov 13, 2024
c29392b
chore(contact-tagging): Backend: Cleanups
JumpLink Nov 13, 2024
e356e5a
review(contact-tagging): Cleanups and small fixes
JumpLink Nov 14, 2024
29440a7
fix(contact-tagging): Add error message for existing tags + translation
JumpLink Nov 14, 2024
d196736
fix(contact-tagging): Seperate tag remove warnings for contact and ca…
JumpLink Nov 14, 2024
1cc78b3
fix(contact-tagging): Tag Manager route from detail pages
JumpLink Nov 14, 2024
39951ba
fix(contact-tagging): Always enable ToggleTagButton to reach the Tag …
JumpLink Nov 14, 2024
a3b8abe
fix(contact-tagging): Frontend: Readd callout filter group
JumpLink Nov 14, 2024
268b8d4
fix(contact-tagging): Backend make DuplicateTagNameError more robust
JumpLink Nov 14, 2024
f1b37cf
fix(contact-tagging): Frontend: When the dropdown is open, the button…
JumpLink Nov 14, 2024
546c75d
fix(contact-tagging): Frontend: Restore selected items after toggled tag
JumpLink Nov 14, 2024
43e3e0f
fix(contact-tagging): Frontend: Wrap tags if there is not enough space
JumpLink Nov 14, 2024
06d0dd5
Merge branch 'main' into feat/contact-tagging
JumpLink Nov 14, 2024
418f3c3
feat(contact-tagging): Fetch tags ordered by name
JumpLink Nov 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 2 additions & 10 deletions apps/backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ changes

```bash
docker compose -f ../../docker-compose.yml start db
docker compose -f ../../docker-compose.yml run app yarn typeorm migration:generate src/migrations/MigrationName && yarn format
yarn build
docker compose -f ../../docker-compose.yml run app yarn typeorm migration:generate /opt/packages/core/src/migrations/MigrationName && yarn format
yarn build # necessary for the new migration files to be found
docker compose -f ../../docker-compose.yml run app yarn typeorm migration:run
```

Expand Down Expand Up @@ -168,14 +168,6 @@ Various tools for administration, including nightly cron jobs
./src/tools
```

- **Database migrations**

Autogenerated by TypeORM

```
./src/migrations
```

#### 📡 Webhooks

Webhooks are handled by the `webhook_app` service. This is a separate service from the API to allow for scaling independently.
Expand Down
1 change: 1 addition & 0 deletions apps/backend/src/api/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ initApp()
log.notice("Bad request, probably a validation error", {
body: req.body,
query: req.query,
stack: error.stack,
error
});
}
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/api/controllers/CalloutController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ export class CalloutController {
@CurrentAuth({ required: true }) auth: AuthInfo,
@CalloutId() id: string,
@Param("tagId") tagId: string,
@PartialBody() data: CreateCalloutTagDto // Partial<CreateCalloutTagData>
@PartialBody() data: CreateCalloutTagDto // Partial<TagCreateData>
): Promise<GetCalloutTagDto | undefined> {
await getRepository(CalloutTag).update({ id: tagId, calloutId: id }, data);

Expand Down
43 changes: 35 additions & 8 deletions apps/backend/src/api/controllers/ContactController.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import {
ContributionPeriod,
NewsletterStatus,
GetContactWith
} from "@beabee/beabee-common";
import { ContributionPeriod, GetContactWith } from "@beabee/beabee-common";
import { getRepository } from "@beabee/core/database";
import { plainToInstance } from "class-transformer";
import { Response } from "express";
import {
Expand All @@ -23,14 +20,13 @@ import {
} from "routing-controllers";

import ContactsService from "@beabee/core/services/ContactsService";
import OptionsService from "@beabee/core/services/OptionsService";
import PaymentFlowService from "@beabee/core/services/PaymentFlowService";
import PaymentService from "@beabee/core/services/PaymentService";
import ContactMfaService from "@beabee/core/services/ContactMfaService";

import { generatePassword } from "@beabee/core/utils/auth";

import { Contact, JoinFlow } from "@beabee/core/models";
import { Contact, JoinFlow, ContactTag } from "@beabee/core/models";

import { GetExportQuery } from "@api/dto/BaseDto";
import {
Expand All @@ -41,6 +37,7 @@ import {
ListContactsDto,
UpdateContactDto
} from "@api/dto/ContactDto";
import { CreateContactTagDto, GetContactTagDto } from "@api/dto/ContactTagDto";
import {
CreateContactMfaDto,
DeleteContactMfaDto,
Expand Down Expand Up @@ -75,6 +72,7 @@ import ContactExporter from "@api/transformers/ContactExporter";
import ContactTransformer from "@api/transformers/ContactTransformer";
import ContactRoleTransformer from "@api/transformers/ContactRoleTransformer";
import PaymentTransformer from "@api/transformers/PaymentTransformer";
import ContactTagTransformer from "@api/transformers/ContactTagTransformer";

import { AuthInfo } from "@type/auth-info";

Expand Down Expand Up @@ -145,6 +143,35 @@ export class ContactController {
return res;
}

@Authorized("admin")
@Get("/tags")
JumpLink marked this conversation as resolved.
Show resolved Hide resolved
async getAllContactTags(
@CurrentAuth({ required: true }) auth: AuthInfo
): Promise<GetContactTagDto[]> {
const result = await ContactTagTransformer.fetch(auth, {
limit: -1,
rules: {
condition: "AND",
rules: []
}
});

return result.items;
}

@Authorized("admin")
@Post("/tags")
async createGlobalContactTag(
@Body() data: CreateContactTagDto
): Promise<GetContactTagDto> {
const tag = await getRepository(ContactTag).save({
name: data.name,
description: data.description
});

return ContactTagTransformer.convert(tag);
}

@Get("/:id")
async getContact(
@CurrentAuth({ required: true }) auth: AuthInfo,
Expand Down Expand Up @@ -174,7 +201,7 @@ export class ContactController {
if (data.profile) {
if (
!auth.roles.includes("admin") &&
(data.profile.tags || data.profile.notes || data.profile.description)
(data.profile.notes || data.profile.description)
) {
throw new UnauthorizedError();
}
Expand Down
9 changes: 9 additions & 0 deletions apps/backend/src/api/dto/ContactDto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
CreateContactRoleDto,
GetContactRoleDto
} from "@api/dto/ContactRoleDto";
import { GetContactTagDto } from "@api/dto/ContactTagDto";
import { ForceUpdateContributionDto } from "@api/dto/ContributionDto";

import IsPassword from "@api/validators/IsPassword";
Expand Down Expand Up @@ -157,6 +158,10 @@ export class GetContactDto extends BaseContactDto {
@IsOptional()
@ValidateNested()
roles?: GetContactRoleDto[];

@IsOptional()
@ValidateNested({ each: true })
tags?: GetContactTagDto[];
}

export class UpdateContactDto extends BaseContactDto {
Expand All @@ -180,6 +185,10 @@ export class CreateContactDto extends UpdateContactDto {
@ValidateNested({ each: true })
@Type(() => CreateContactRoleDto)
roles?: CreateContactRoleDto[];

@IsOptional()
@IsString({ each: true })
tags?: string[];
}

export interface ExportContactDto {
Expand Down
5 changes: 0 additions & 5 deletions apps/backend/src/api/dto/ContactProfileDto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,6 @@ export class UpdateContactProfileDto implements Partial<GetContactProfileDto> {
newsletterGroups?: string[];

// Admin only

@IsOptional()
@IsString({ each: true })
tags?: string[];

@IsOptional()
@IsString()
notes?: string;
Expand Down
24 changes: 24 additions & 0 deletions apps/backend/src/api/dto/ContactTagDto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { IsIn, IsString, IsUUID } from "class-validator";

import { GetPaginatedQuery } from "@api/dto/BaseDto";

export class GetContactTagDto {
@IsUUID()
id!: string;

@IsString()
name!: string;
}

export class CreateContactTagDto {
@IsString()
name!: string;

@IsString()
description!: string;
}

export class ListContactTagsDto extends GetPaginatedQuery {
@IsIn(["id", "name"])
sort?: string;
}
1 change: 1 addition & 0 deletions apps/backend/src/api/dto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export * from "./ContactDto.js";
export * from "./ContactMfaDto.js";
export * from "./ContactProfileDto.js";
export * from "./ContactRoleDto.js";
export * from "./ContactTagDto.js";
export * from "./ContentDto.js";
export * from "./ContentTelegramDto.js";
export * from "./ContributionDto.js";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ class ContactProfileTransformer extends BaseTransformer<
newsletterStatus: profile.newsletterStatus,
newsletterGroups: profile.newsletterGroups,
...(auth?.roles.includes("admin") && {
tags: profile.tags,
notes: profile.notes,
description: profile.description
})
Expand Down
30 changes: 30 additions & 0 deletions apps/backend/src/api/transformers/ContactTagTransformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ContactTag } from "@beabee/core/models";
import { GetContactTagDto } from "@api/dto/ContactTagDto";
import { BaseTransformer } from "./BaseTransformer";
import {
ContactTagFilterName,
contactTagFilters,
RoleType
} from "@beabee/beabee-common";
import { TransformPlainToInstance } from "class-transformer";

class ContactTagTransformer extends BaseTransformer<
ContactTag,
GetContactTagDto,
ContactTagFilterName
> {
protected model = ContactTag;
protected filters = contactTagFilters;

protected allowedRoles: RoleType[] = ["admin"];

@TransformPlainToInstance(GetContactTagDto)
convert(tag: ContactTag): GetContactTagDto {
return {
id: tag.id,
name: tag.name
};
}
}

export default new ContactTagTransformer();
10 changes: 10 additions & 0 deletions apps/backend/src/api/transformers/ContactTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import ContactProfileTransformer from "@api/transformers/ContactProfileTransform
import { mergeRules } from "@api/utils/rules";

import { AuthInfo } from "@type/auth-info";
import ContactTagTransformer from "./ContactTagTransformer";

class ContactTransformer extends BaseContactTransformer<
GetContactDto,
Expand Down Expand Up @@ -57,6 +58,9 @@ class ContactTransformer extends BaseContactTransformer<
}),
...(opts?.with?.includes(GetContactWith.Contribution) && {
contribution: contact.contributionInfo
}),
...(opts?.with?.includes(GetContactWith.Tags) && {
tags: contact.tags.map(ContactTagTransformer.convert)
})
};
}
Expand Down Expand Up @@ -88,6 +92,12 @@ class ContactTransformer extends BaseContactTransformer<
qb.innerJoinAndSelect(`${fieldPrefix}profile`, "profile");
}

if (query.with?.includes(GetContactWith.Tags)) {
JumpLink marked this conversation as resolved.
Show resolved Hide resolved
qb.leftJoinAndSelect(`${fieldPrefix}tags`, "tags");
}

console.debug("query.with", query.with);

switch (query.sort) {
// Add member role to allow sorting by membershipStarts and membershipExpires
case "membershipStarts":
Expand Down
1 change: 1 addition & 0 deletions apps/backend/src/api/transformers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export * from "./CalloutVariantTransformer.js";
export * from "./ContactExporter.js";
export * from "./ContactProfileTransformer.js";
export * from "./ContactRoleTransformer.js";
export * from "./ContactTagTransformer.js";
export * from "./ContactTransformer.js";
export * from "./ContentTransformer.js";
export * from "./NoticeTransformer.js";
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/barrelsby.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
"verbose": false,
"delete": true,
"local": true,
"directory": ["./src/type", "./src/enums", "./src/lib"]
"directory": ["./src/type", "./src/enums", "./src/lib", "./src/utils/api"]
}
21 changes: 13 additions & 8 deletions apps/frontend/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -462,30 +462,24 @@
"actions": {
"assignTo": "Zuweisen",
"moveBucket": "Antwort verschieben",
"toggleTag": "Tag hinzufügen / entfernen",
"viewOnMap": "Auf Karte anzeigen"
},
"editMode": "Vorsicht, du befindest dich im Bearbeitungsmodus!",
"manageFolders": "Ordner bearbeiten",
"manageReviewers": "Auswerter bearbeiten",
"manageTags": "Tags bearbeiten",
"notifications": {
"addedAssignee": "\"{assignee}\" zugewiesen",
"addedTag": "Tag \"{tag}\" hinzugefügt",
"movedToBucket": "Verschoben in {bucket}",
"removedAssignee": "Nicht mehr \"{assignee}\" zugewiesen",
"removedTag": "Tag \"{tag}\" entfernt"
"removedAssignee": "Nicht mehr \"{assignee}\" zugewiesen"
},
"responseOf": "Antwort {no} von {total} in {bucket}",
"selectedCount": "{n} Antwort ausgewählt | {n} Antworten ausgewählt"
},
"calloutResponsesPage": {
"moveToBucket": "Nach {bucket} verschieben",
"noTags": "Keine Tags",
"response": "Antwort",
"responseNo": "Antwort {no}",
"searchAssignee": "Zugewiesen an...",
"searchTag": "Getaggt mit...",
"showAnswer": "Antwort",
"showLatestComment": "Letzter Kommentar",
"showingOf": "Antworten {start} bis {end} von {total} werden angezeigt"
Expand Down Expand Up @@ -680,7 +674,8 @@
"noResults": "Keine Kontakte gefunden",
"numResults": "{n} Kontakt gefunden | {n} Kontakte gefunden",
"search": "Kontakte durchsuchen...",
"showingOf": "{start} to {end} von {total} Kontakten werden angezeigt."
"showingOf": "{start} to {end} von {total} Kontakten werden angezeigt.",
"selectedCount": "{n} Kontakt ausgewählt | {n} Kontakte ausgewählt"
},
"contribution": {
"adminCancelDescription": "Wenn du die Mitgliedschaft des Kontakts kündigst, endet sie am Ende des aktuellen Zahlungszyklus am {date}.",
Expand Down Expand Up @@ -1382,6 +1377,16 @@
"description": "Neues Passwort eingeben, um fortzufahren.",
"title": "Passwort vergeben"
},
"tags": {
"manageTags": "Tags bearbeiten",
"toggleTag": "Tag hinzufügen / entfernen",
"noTags": "Keine Tags",
"searchTag": "Getaggt mit...",
"notifications": {
"addedTag": "Tag \"{tag}\" hinzugefügt",
"removedTag": "Tag \"{tag}\" entfernt",
}
},
"tagEditor": {
"add": "Tag hinzufügen",
"addNewTag": "Tag hinzufügen",
Expand Down
Loading