From 5530d25ec37aa8bb0c6846cef5b72254312d06d6 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Wed, 22 May 2024 15:54:25 -0500 Subject: [PATCH 1/3] Add entry getter to enums to get a member entry by value --- src/common/make-enum.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/common/make-enum.ts b/src/common/make-enum.ts index 38559c5d67..bf1b258861 100644 --- a/src/common/make-enum.ts +++ b/src/common/make-enum.ts @@ -1,5 +1,5 @@ import { registerEnumType } from '@nestjs/graphql'; -import { cleanJoin, nonEnumerable, setHas } from '@seedcompany/common'; +import { cleanJoin, mapKeys, nonEnumerable } from '@seedcompany/common'; import { inspect, InspectOptionsStylized } from 'util'; export type EnumType = Enum extends MadeEnum @@ -82,6 +82,7 @@ export const makeEnum = < (value: EnumValueDeclarationShape): EnumValueDeclarationObjectShape => typeof value === 'string' ? { value } : value, ); + const entryMap = mapKeys.fromList(entries, (e) => e.value).asMap; const object = Object.fromEntries(entries.map((v) => [v.value, v.value])); @@ -92,7 +93,14 @@ export const makeEnum = < entries, [Symbol.iterator]: () => values.values(), // @ts-expect-error Ignoring generics for implementation. - has: (value: string) => setHas(values, value), + has: (value: string) => entryMap.has(value), + entry: (value: string) => { + const entry = entryMap.get(value); + if (!entry) { + throw new Error(`${name ?? 'Enum'} does not have member: "${value}"`); + } + return entry; + }, [inspect.custom]: ( depth: number, options: InspectOptionsStylized, @@ -192,6 +200,7 @@ type NormalizedValueDeclaration = interface EnumHelpers { readonly values: ReadonlySet; readonly entries: ReadonlyArray>; + readonly entry: (value: Values) => Readonly; readonly has: ( value: In & {}, ) => value is In & Values & {}; From 60bf359e5bbb7e9733c6d3db63cde4d1b9a461c8 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Wed, 22 May 2024 15:56:30 -0500 Subject: [PATCH 2/3] Default all enum member (human) labels if not declared This heuristic matches what the UI does too. Not declaring these to GQL schema though, so only handwritten labels are provided there. --- package.json | 1 + src/common/make-enum.ts | 36 +++++++++++++++++++++++------------- yarn.lock | 8 ++++++++ 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index c83bc52410..231ed89b2d 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "rimraf": "^5.0.5", "rxjs": "^7.8.1", "sanitize-filename": "^1.6.3", + "title-case": "^4.3.1", "ts-essentials": "^9.4.1", "winston": "^3.11.0", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.0/xlsx-0.20.0.tgz", diff --git a/src/common/make-enum.ts b/src/common/make-enum.ts index bf1b258861..679d6835d0 100644 --- a/src/common/make-enum.ts +++ b/src/common/make-enum.ts @@ -1,5 +1,8 @@ import { registerEnumType } from '@nestjs/graphql'; import { cleanJoin, mapKeys, nonEnumerable } from '@seedcompany/common'; +import { lowerCase } from 'lodash'; +import { titleCase } from 'title-case'; +import { SetRequired } from 'type-fest'; import { inspect, InspectOptionsStylized } from 'util'; export type EnumType = Enum extends MadeEnum @@ -145,6 +148,11 @@ export const makeEnum = < registerEnumType(object, { name, description, valuesMap }); } + for (const entry of entries) { + // @ts-expect-error ignoring immutable here. + entry.label ??= titleCase(lowerCase(entry.value)).replace(/ and /g, ' & '); + } + return object as any; }; @@ -183,19 +191,21 @@ type ValuesOfDeclarations = * properties as optional. */ type NormalizedValueDeclaration = - // For values that are objects, accept them as they are... - | (Extract & - // plus all the normal object keys - EnumValueDeclarationObjectShape>) - // For values that are strings, convert them to the standard shape... - | (EnumValueDeclarationObjectShape> & - // and include all the extra keys as optional - Partial< - Omit< - Extract, - keyof EnumValueDeclarationObjectShape - > - >); + SetRequired, 'label'>, 'label'> & + // For values that are objects, accept them as they are... + (| (Extract & + // plus all the normal object keys + EnumValueDeclarationObjectShape>) + // For values that are strings, convert them to the standard shape... + | (EnumValueDeclarationObjectShape> & + // and include all the extra keys as optional + Partial< + Omit< + Extract, + keyof EnumValueDeclarationObjectShape + > + >) + ); interface EnumHelpers { readonly values: ReadonlySet; diff --git a/yarn.lock b/yarn.lock index 8c10ed1268..37a525cdca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5442,6 +5442,7 @@ __metadata: rimraf: "npm:^5.0.5" rxjs: "npm:^7.8.1" sanitize-filename: "npm:^1.6.3" + title-case: "npm:^4.3.1" ts-essentials: "npm:^9.4.1" ts-jest: "npm:^29.1.1" ts-morph: "npm:^19.0.0" @@ -12385,6 +12386,13 @@ __metadata: languageName: node linkType: hard +"title-case@npm:^4.3.1": + version: 4.3.1 + resolution: "title-case@npm:4.3.1" + checksum: 10c0/10bb67f3ae5cf4043b4050f5c4b3ae7fdaeef6c55bcb646d08a3567d4233f02d10f3ea55add7d8dbf55e1a320b49b79cb0b5d4ce39701a500de62cc8860d9bc8 + languageName: node + linkType: hard + "tmp@npm:^0.0.33": version: 0.0.33 resolution: "tmp@npm:0.0.33" From decbf15aa1cb6e3239e62a3d78512ebd1c7849b8 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Wed, 22 May 2024 15:58:45 -0500 Subject: [PATCH 3/3] Convert to using enum labels --- src/components/engagement/engagement.rules.ts | 8 ++++---- .../workflow/progress-report-workflow.flowchart.ts | 5 ++--- .../progress-report-status-changed.template.tsx | 9 ++++++--- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/components/engagement/engagement.rules.ts b/src/components/engagement/engagement.rules.ts index 8488e3a420..1bafa60e6b 100644 --- a/src/components/engagement/engagement.rules.ts +++ b/src/components/engagement/engagement.rules.ts @@ -1,7 +1,7 @@ /* eslint-disable no-case-declarations */ import { Injectable } from '@nestjs/common'; import { node, relation } from 'cypher-query-builder'; -import { first, intersection, startCase } from 'lodash'; +import { first, intersection } from 'lodash'; import { ID, Role, @@ -384,9 +384,9 @@ export class EngagementRules { ); if (!validNextStatus) { throw new UnauthorizedException( - `One or more engagements cannot be changed to ${startCase( - nextStatus, - )}. Please check engagement statuses.`, + `One or more engagements cannot be changed to ${ + EngagementStatus.entry(nextStatus).label + }. Please check engagement statuses.`, 'engagement.status', ); } diff --git a/src/components/progress-report/workflow/progress-report-workflow.flowchart.ts b/src/components/progress-report/workflow/progress-report-workflow.flowchart.ts index 92da686dab..2757044562 100644 --- a/src/components/progress-report/workflow/progress-report-workflow.flowchart.ts +++ b/src/components/progress-report/workflow/progress-report-workflow.flowchart.ts @@ -1,5 +1,4 @@ import { Injectable } from '@nestjs/common'; -import { startCase } from 'lodash'; import open from 'open'; import pako from 'pako'; import { ProgressReportStatus as Status } from '../dto'; @@ -37,8 +36,8 @@ export class ProgressReportWorkflowFlowchart { return str ? `classDef ${type} ${str}` : []; }), '', - ...Object.keys(Status).map( - (status) => `${status}(${startCase(status)}):::State`, + ...Status.entries.map( + (status) => `${status.value}(${status.label}):::State`, ), '', ...Object.values(Transitions).flatMap((t) => { diff --git a/src/core/email/templates/progress-report-status-changed.template.tsx b/src/core/email/templates/progress-report-status-changed.template.tsx index 2a132b25e2..8a809c0ff4 100644 --- a/src/core/email/templates/progress-report-status-changed.template.tsx +++ b/src/core/email/templates/progress-report-status-changed.template.tsx @@ -4,7 +4,6 @@ import { Section, Text, } from '@seedcompany/nestjs-email/templates'; -import { startCase } from 'lodash'; import { fiscalQuarter, fiscalYear } from '~/common'; import { Language } from '../../../components/language/dto'; import { PeriodicReport } from '../../../components/periodic-report/dto'; @@ -49,8 +48,12 @@ export function ProgressReportStatusChanged({ report.start, )} FY${fiscalYear(report.start)}`; - const oldStatus = startCase(previousStatusVal) || undefined; - const newStatus = startCase(newStatusVal) || undefined; + const oldStatus = previousStatusVal + ? ProgressReportStatus.entry(previousStatusVal).label + : undefined; + const newStatus = newStatusVal + ? ProgressReportStatus.entry(newStatusVal).label + : undefined; return (