diff --git a/apps/lfx-one/package.json b/apps/lfx-one/package.json index 9a14b90c..e1c7bdf3 100644 --- a/apps/lfx-one/package.json +++ b/apps/lfx-one/package.json @@ -53,6 +53,7 @@ "ngx-cookie-service-ssr": "^19.1.2", "pino-http": "^10.5.0", "primeng": "^20.4.0", + "quill": "^2.0.3", "rxjs": "~7.8.2", "snowflake-sdk": "^2.3.1", "tslib": "^2.8.1" diff --git a/apps/lfx-one/src/app/modules/mailing-lists/components/mailing-list-basic-info/mailing-list-basic-info.component.html b/apps/lfx-one/src/app/modules/mailing-lists/components/mailing-list-basic-info/mailing-list-basic-info.component.html index bd500dda..0b1c4bac 100644 --- a/apps/lfx-one/src/app/modules/mailing-lists/components/mailing-list-basic-info/mailing-list-basic-info.component.html +++ b/apps/lfx-one/src/app/modules/mailing-lists/components/mailing-list-basic-info/mailing-list-basic-info.component.html @@ -11,7 +11,7 @@
- This mailing list belongs to: {{ projectName() }} + This mailing list belongs to: {{ projectName() }} - {{ service()?.domain || '' }}
This helps members understand when to use this list.
@if (form().get('description')?.errors?.['required'] && form().get('description')?.touched) {Description is required
- } - @if (form().get('description')?.errors?.['minlength'] && form().get('description')?.touched) { + } @else if (form().get('description')?.errors?.['minlength'] && form().get('description')?.touched) {Description must be at least 11 characters
- } - @if (form().get('description')?.errors?.['maxlength'] && form().get('description')?.touched) { + } @else if (form().get('description')?.errors?.['maxlength'] && form().get('description')?.touched) {Description cannot exceed 500 characters
}Step 3: People & Groups Component
-Coming next
-Step 3: People & Groups Component
+Coming next
+Hello & World
') + * // Returns: "Hello & World" + * + * stripHtml(null) + * // Returns: "" + * ``` + */ +export function stripHtml(html: string | null | undefined): string { + if (!html) return ''; + + return ( + html + // Remove HTML tags + .replace(/<[^>]*>/g, '') + // Decode common HTML entities + .replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/'/g, "'") + .replace(/'/g, "'") + // Trim whitespace + .trim() + ); +} diff --git a/packages/shared/src/utils/index.ts b/packages/shared/src/utils/index.ts index d5eeb19a..e10418b8 100644 --- a/packages/shared/src/utils/index.ts +++ b/packages/shared/src/utils/index.ts @@ -5,6 +5,7 @@ export * from './color.utils'; export * from './date-time.utils'; export * from './file.utils'; export * from './form.utils'; +export * from './html-utils'; export * from './meeting.utils'; export * from './rsvp-calculator.util'; export * from './string.utils'; diff --git a/packages/shared/src/validators/mailing-list.validators.ts b/packages/shared/src/validators/mailing-list.validators.ts index e6afd4d4..b90ea328 100644 --- a/packages/shared/src/validators/mailing-list.validators.ts +++ b/packages/shared/src/validators/mailing-list.validators.ts @@ -4,6 +4,7 @@ import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; import { MailingListType } from '../enums/mailing-list.enum'; +import { stripHtml } from '../utils/html-utils'; /** * Validator to ensure announcement mailing lists have public visibility @@ -22,3 +23,65 @@ export function announcementVisibilityValidator(): ValidatorFn { return null; }; } + +/** + * Validator for minimum length of HTML content (strips tags before counting) + * @param minLength - Minimum character count for plain text content + */ +export function htmlMinLengthValidator(minLength: number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const value = control.value; + if (!value) return null; // Let required validator handle empty values + + const plainText = stripHtml(value); + if (plainText.length < minLength) { + return { + minlength: { + requiredLength: minLength, + actualLength: plainText.length, + }, + }; + } + + return null; + }; +} + +/** + * Validator for maximum length of HTML content (strips tags before counting) + * @param maxLength - Maximum character count for plain text content + */ +export function htmlMaxLengthValidator(maxLength: number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const value = control.value; + if (!value) return null; + + const plainText = stripHtml(value); + if (plainText.length > maxLength) { + return { + maxlength: { + requiredLength: maxLength, + actualLength: plainText.length, + }, + }; + } + + return null; + }; +} + +/** + * Validator for required HTML content (checks if plain text is not empty) + */ +export function htmlRequiredValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const value = control.value; + const plainText = stripHtml(value); + + if (!plainText || plainText.length === 0) { + return { required: true }; + } + + return null; + }; +} diff --git a/yarn.lock b/yarn.lock index d610fb03..08df7fdf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9547,6 +9547,13 @@ __metadata: languageName: node linkType: hard +"fast-diff@npm:^1.3.0": + version: 1.3.0 + resolution: "fast-diff@npm:1.3.0" + checksum: 10c0/5c19af237edb5d5effda008c891a18a585f74bf12953be57923f17a3a4d0979565fc64dbc73b9e20926b9d895f5b690c618cbb969af0cf022e3222471220ad29 + languageName: node + linkType: hard + "fast-glob@npm:3.3.3, fast-glob@npm:^3.3.2": version: 3.3.3 resolution: "fast-glob@npm:3.3.3" @@ -11759,6 +11766,7 @@ __metadata: prettier-plugin-organize-imports: "npm:^4.2.0" prettier-plugin-tailwindcss: "npm:^0.6.14" primeng: "npm:^20.4.0" + quill: "npm:^2.0.3" rxjs: "npm:~7.8.2" snowflake-sdk: "npm:^2.3.1" tailwindcss: "npm:^3.4.17" @@ -11945,6 +11953,13 @@ __metadata: languageName: node linkType: hard +"lodash-es@npm:^4.17.21": + version: 4.17.22 + resolution: "lodash-es@npm:4.17.22" + checksum: 10c0/5f28a262183cca43e08c580622557f393cb889386df2d8adf7c852bfdff7a84c5e629df5aa6c5c6274e83b38172f239d3e4e72e1ad27352d9ae9766627338089 + languageName: node + linkType: hard + "lodash.camelcase@npm:^4.3.0": version: 4.3.0 resolution: "lodash.camelcase@npm:4.3.0" @@ -11952,6 +11967,13 @@ __metadata: languageName: node linkType: hard +"lodash.clonedeep@npm:^4.5.0": + version: 4.5.0 + resolution: "lodash.clonedeep@npm:4.5.0" + checksum: 10c0/2caf0e4808f319d761d2939ee0642fa6867a4bbf2cfce43276698828380756b99d4c4fa226d881655e6ac298dd453fe12a5ec8ba49861777759494c534936985 + languageName: node + linkType: hard + "lodash.debounce@npm:^4.0.8": version: 4.0.8 resolution: "lodash.debounce@npm:4.0.8" @@ -11980,6 +12002,13 @@ __metadata: languageName: node linkType: hard +"lodash.isequal@npm:^4.5.0": + version: 4.5.0 + resolution: "lodash.isequal@npm:4.5.0" + checksum: 10c0/dfdb2356db19631a4b445d5f37868a095e2402292d59539a987f134a8778c62a2810c2452d11ae9e6dcac71fc9de40a6fedcb20e2952a15b431ad8b29e50e28f + languageName: node + linkType: hard + "lodash.isinteger@npm:^4.0.4": version: 4.0.4 resolution: "lodash.isinteger@npm:4.0.4" @@ -13358,6 +13387,13 @@ __metadata: languageName: node linkType: hard +"parchment@npm:^3.0.0": + version: 3.0.0 + resolution: "parchment@npm:3.0.0" + checksum: 10c0/83cc55756a899d6769e42b345ae55738f7e93f9027bb0095f518bc75b81d44ba2b69a76bc25f259974d972f8e250066419c3f92e3ded7518f144fc9c1b47430d + languageName: node + linkType: hard + "parent-module@npm:^1.0.0": version: 1.0.1 resolution: "parent-module@npm:1.0.1" @@ -14198,6 +14234,29 @@ __metadata: languageName: node linkType: hard +"quill-delta@npm:^5.1.0": + version: 5.1.0 + resolution: "quill-delta@npm:5.1.0" + dependencies: + fast-diff: "npm:^1.3.0" + lodash.clonedeep: "npm:^4.5.0" + lodash.isequal: "npm:^4.5.0" + checksum: 10c0/a99462b96177f4559e5a659be0f51bbfe090c11b61c53aa19afabd3fdf8a6495173bbacd84b75acce680ed7c157a024907e74ff077ddd6a135b4da15bf71ada2 + languageName: node + linkType: hard + +"quill@npm:^2.0.3": + version: 2.0.3 + resolution: "quill@npm:2.0.3" + dependencies: + eventemitter3: "npm:^5.0.1" + lodash-es: "npm:^4.17.21" + parchment: "npm:^3.0.0" + quill-delta: "npm:^5.1.0" + checksum: 10c0/9897468b3e2b0fbf9c91471deea745d7b6494f866cb8caace63267769b2c4c6128e49da0988c4ed64f1a91a171fbf91c84009b663b1256a3caca0755204bb6b5 + languageName: node + linkType: hard + "randombytes@npm:^2.1.0": version: 2.1.0 resolution: "randombytes@npm:2.1.0"