Skip to content

Commit b6f2414

Browse files
authored
Merge pull request #1434 from IFRCGo/feature/dref-import-template-improvements
Feature/dref import template improvements
2 parents 399232d + 20dfeb3 commit b6f2414

File tree

10 files changed

+1280
-688
lines changed

10 files changed

+1280
-688
lines changed

.changeset/nasty-jars-happen.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"go-web-app": patch
3+
---
4+
5+
Update DREF import template
6+
7+
- Update guidance
8+
- Improve template stylings
9+
- Update message in error popup when import fails

app/index.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&display=swap" rel="stylesheet">
2828
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
2929

30-
<!-- link href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400&family=Montserrat:wght@300;400;600;700&family=OpenSans:wght@300;400;600;700&display=swap" rel="stylesheet" -->
3130
<style>
3231
html, body {
3332
margin: 0;

app/src/utils/importTemplate.ts

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ import {
77
mapToMap,
88
randomString,
99
} from '@togglecorp/fujs';
10+
import { type CellRichTextValue } from 'exceljs';
11+
12+
import {
13+
type ParsePlugin,
14+
parsePseudoHtml,
15+
} from '#utils/richText';
1016

1117
type ValidationType = string | number | boolean | 'textArea';
1218
type TypeToLiteral<T extends ValidationType> = T extends string
@@ -56,6 +62,7 @@ interface ListField<
5662
// TODO: Make this more strict
5763
optionsKey: keyof OPTIONS_MAPPING;
5864
keyFieldName?: string;
65+
hiddenLabel?: boolean;
5966
children: TemplateSchema<
6067
VALUE,
6168
OPTIONS_MAPPING
@@ -75,6 +82,7 @@ interface ObjectField<VALUE, OPTIONS_MAPPING extends TemplateFieldOptionsMapping
7582
export interface TemplateOptionItem<T extends ValidationType> {
7683
key: T;
7784
label: string;
85+
description?: string;
7886
}
7987

8088
export interface TemplateFieldOptionsMapping {
@@ -98,23 +106,26 @@ export type TemplateSchema<
98106
| SelectField<ExtractValidation<VALUE>, OPTIONS_MAPPING>)
99107
);
100108

109+
// NOTE: Not adding richtext support on heading
101110
interface HeadingTemplateField {
102111
type: 'heading';
103112
name: string | number | boolean;
104113
label: string;
105114
outlineLevel: number;
106115
description?: string;
116+
context: { field: string, key: string }[],
107117
}
108118

109119
type ObjectKey = string | number | symbol;
110120

111121
type InputTemplateField = {
112122
type: 'input';
113123
name: string | number | boolean;
114-
label: string;
124+
label: string | CellRichTextValue;
115125
outlineLevel: number;
116-
description?: string;
126+
description?: string | CellRichTextValue;
117127
headingBefore?: string;
128+
context: { field: string, key: string }[],
118129
} & ({
119130
dataValidation: 'list';
120131
optionsKey: ObjectKey;
@@ -136,7 +147,30 @@ export function getCombinedKey(
136147

137148
export type TemplateField = HeadingTemplateField | InputTemplateField;
138149

139-
// TODO: add test
150+
function createInsPlugin(
151+
optionsMap: TemplateFieldOptionsMapping,
152+
context: { field: string, key: string }[],
153+
): ParsePlugin {
154+
return {
155+
tag: 'ins',
156+
transformer: (token, richText) => {
157+
const [optionField, valueField] = token.split('.');
158+
const currOptions = context?.find((item) => item.field === optionField);
159+
const selectedOption = currOptions
160+
? optionsMap?.[optionField]?.find(
161+
(option) => String(option.key) === currOptions?.key,
162+
)
163+
: undefined;
164+
165+
return {
166+
...richText,
167+
// FIXME: Need to add mechanism to identify if we have error for mapping
168+
text: selectedOption?.[valueField as 'description'] ?? '',
169+
};
170+
},
171+
};
172+
}
173+
140174
export function createImportTemplate<
141175
TEMPLATE_SCHEMA,
142176
OPTIONS_MAPPING extends TemplateFieldOptionsMapping
@@ -145,6 +179,7 @@ export function createImportTemplate<
145179
optionsMap: OPTIONS_MAPPING,
146180
fieldName: string | undefined = undefined,
147181
outlineLevel = -1,
182+
context: { field: string, key: string }[] = [],
148183
): TemplateField[] {
149184
if (schema.type === 'object') {
150185
return [
@@ -158,6 +193,7 @@ export function createImportTemplate<
158193
optionsMap,
159194
getCombinedKey(key, fieldName),
160195
outlineLevel + 1,
196+
context,
161197
);
162198

163199
return newFields;
@@ -177,22 +213,26 @@ export function createImportTemplate<
177213
if (isDefined(schema.headingBefore)) {
178214
fields.push({
179215
type: 'heading',
180-
name: getCombinedKey('headingBefore', fieldName),
216+
name: getCombinedKey('heading_before', fieldName),
181217
label: schema.headingBefore,
182218
outlineLevel,
219+
context,
183220
} satisfies HeadingTemplateField);
184221
}
185222

223+
const insPlugin = createInsPlugin(optionsMap, context);
224+
186225
if (schema.type === 'input') {
187226
const field = {
188227
type: 'input',
189228
name: fieldName,
190-
label: schema.label,
191-
description: schema.description,
229+
label: parsePseudoHtml(schema.label, [insPlugin]),
230+
description: parsePseudoHtml(schema.description, [insPlugin]),
192231
dataValidation: (schema.validation === 'number' || schema.validation === 'date' || schema.validation === 'integer' || schema.validation === 'textArea')
193232
? schema.validation
194233
: undefined,
195234
outlineLevel,
235+
context,
196236
} satisfies InputTemplateField;
197237

198238
fields.push(field);
@@ -203,11 +243,12 @@ export function createImportTemplate<
203243
const field = {
204244
type: 'input',
205245
name: fieldName,
206-
label: schema.label,
207-
description: schema.description,
246+
label: parsePseudoHtml(schema.label, [insPlugin]),
247+
description: parsePseudoHtml(schema.description, [insPlugin]),
208248
outlineLevel,
209249
dataValidation: 'list',
210250
optionsKey: schema.optionsKey,
251+
context,
211252
} satisfies InputTemplateField;
212253

213254
fields.push(field);
@@ -220,28 +261,29 @@ export function createImportTemplate<
220261
label: schema.label,
221262
description: schema.description,
222263
outlineLevel,
264+
context,
223265
} satisfies HeadingTemplateField;
224266

225-
// fields.push(headingField);
226267
const options = optionsMap[schema.optionsKey];
227268

228269
const optionFields = options.flatMap((option) => {
229270
const subHeadingField = {
230271
type: 'heading',
231-
// name: option.key,
232272
name: getCombinedKey(option.key, fieldName),
233273
label: option.label,
234274
outlineLevel: outlineLevel + 1,
235-
// description: schema.description,
275+
context,
236276
} satisfies HeadingTemplateField;
237277

278+
const combinedKey = getCombinedKey(option.key, fieldName);
238279
// eslint-disable-next-line @typescript-eslint/no-explicit-any
239280
const newFields = createImportTemplate<any, OPTIONS_MAPPING>(
240281
schema.children,
241282
optionsMap,
242283
// undefined,
243-
getCombinedKey(option.key, fieldName),
284+
combinedKey,
244285
outlineLevel + 1,
286+
[...context, { field: String(schema.optionsKey), key: String(option.key) }],
245287
);
246288

247289
return [
@@ -252,16 +294,15 @@ export function createImportTemplate<
252294

253295
return [
254296
...fields,
255-
headingField,
297+
!schema.hiddenLabel ? headingField : undefined,
256298
...optionFields,
257-
];
299+
].filter(isDefined);
258300
}
259301

260302
function addClientId(item: object): object {
261303
return { ...item, clientId: randomString() };
262304
}
263305

264-
// TODO: add test
265306
export function getValueFromImportTemplate<
266307
TEMPLATE_SCHEMA,
267308
OPTIONS_MAPPING extends TemplateFieldOptionsMapping,
@@ -348,6 +389,7 @@ export function getValueFromImportTemplate<
348389
return listValue;
349390
}
350391

392+
/*
351393
type TemplateName = 'dref-application' | 'dref-operational-update' | 'dref-final-report';
352394
353395
export interface ImportTemplateDescription<FormFields> {
@@ -359,7 +401,6 @@ export interface ImportTemplateDescription<FormFields> {
359401
fieldNameToTabNameMap: Record<string, string>,
360402
}
361403
362-
/*
363404
function isValidTemplate(templateName: unknown): templateName is TemplateName {
364405
const templateNameMap: Record<TemplateName, boolean> = {
365406
'dref-application': true,

app/src/utils/richText.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import {
2+
isDefined,
3+
isNotDefined,
4+
} from '@togglecorp/fujs';
5+
import { type CellRichTextValue } from 'exceljs';
6+
7+
export interface ParsePlugin {
8+
tag: string,
9+
transformer: (token: string, richText: CellRichTextValue['richText'][number]) => CellRichTextValue['richText'][number],
10+
}
11+
12+
const boldPlugin: ParsePlugin = {
13+
tag: 'b',
14+
transformer: (_: string, richText) => ({
15+
...richText,
16+
font: {
17+
...richText.font,
18+
bold: true,
19+
},
20+
}),
21+
};
22+
const italicsPlugin: ParsePlugin = {
23+
tag: 'i',
24+
transformer: (_: string, richText) => ({
25+
...richText,
26+
font: {
27+
...richText.font,
28+
italic: true,
29+
},
30+
}),
31+
};
32+
const underlinePlugin: ParsePlugin = {
33+
tag: 'u',
34+
transformer: (_: string, richText) => ({
35+
...richText,
36+
font: {
37+
...richText.font,
38+
underline: true,
39+
},
40+
}),
41+
};
42+
43+
/**
44+
* Convert subset of html into excel's richtext format
45+
* @param value string with or without html tags
46+
*/
47+
export function parsePseudoHtml(
48+
value: undefined,
49+
extraPlugins?: ParsePlugin[],
50+
): undefined;
51+
export function parsePseudoHtml(
52+
value: string,
53+
extraPlugins?: ParsePlugin[],
54+
): string | CellRichTextValue
55+
export function parsePseudoHtml(
56+
value: string | undefined,
57+
extraPlugins?: ParsePlugin[],
58+
): string | CellRichTextValue | undefined
59+
export function parsePseudoHtml(
60+
value: string | undefined,
61+
extraPlugins: ParsePlugin[] = [],
62+
): string | CellRichTextValue | undefined {
63+
if (isNotDefined(value)) {
64+
return value;
65+
}
66+
67+
const plugins: ParsePlugin[] = [
68+
boldPlugin,
69+
italicsPlugin,
70+
underlinePlugin,
71+
...extraPlugins,
72+
];
73+
74+
const supportedTags = plugins.map((p) => p.tag).join('|');
75+
76+
const tagRegex = RegExp(`(</?(?:${supportedTags})>)`);
77+
const tokens = value.split(tagRegex);
78+
if (tokens.length === 1) {
79+
return value;
80+
}
81+
82+
const openTagRegex = RegExp(`<(?:${supportedTags})>`);
83+
const closeTagRegex = RegExp(`</(?:${supportedTags})>`);
84+
85+
const stack: string[] = [];
86+
const richText = tokens.map((token) => {
87+
if (token.match(openTagRegex)) {
88+
stack.push(token);
89+
return undefined;
90+
}
91+
if (token.match(closeTagRegex)) {
92+
// TODO: Check correctness by checking closeTag with last openTag
93+
stack.pop();
94+
return undefined;
95+
}
96+
97+
const applicablePlugins = plugins
98+
.filter((plugin) => stack.includes(`<${plugin.tag}>`));
99+
100+
const richTextItem: CellRichTextValue['richText'][number] = applicablePlugins
101+
.reduce(
102+
(acc, plugin) => plugin.transformer(token, acc),
103+
{ text: token },
104+
);
105+
return richTextItem;
106+
}).filter(isDefined);
107+
108+
// TODO: Check correctness to check that stack is empty
109+
return { richText };
110+
}

0 commit comments

Comments
 (0)