Skip to content

Commit

Permalink
Merge pull request #1434 from IFRCGo/feature/dref-import-template-imp…
Browse files Browse the repository at this point in the history
…rovements

Feature/dref import template improvements
  • Loading branch information
frozenhelium authored Nov 5, 2024
2 parents 399232d + 20dfeb3 commit b6f2414
Show file tree
Hide file tree
Showing 10 changed files with 1,280 additions and 688 deletions.
9 changes: 9 additions & 0 deletions .changeset/nasty-jars-happen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"go-web-app": patch
---

Update DREF import template

- Update guidance
- Improve template stylings
- Update message in error popup when import fails
1 change: 0 additions & 1 deletion app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">

<!-- 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" -->
<style>
html, body {
margin: 0;
Expand Down
73 changes: 57 additions & 16 deletions app/src/utils/importTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import {
mapToMap,
randomString,
} from '@togglecorp/fujs';
import { type CellRichTextValue } from 'exceljs';

import {
type ParsePlugin,
parsePseudoHtml,
} from '#utils/richText';

type ValidationType = string | number | boolean | 'textArea';
type TypeToLiteral<T extends ValidationType> = T extends string
Expand Down Expand Up @@ -56,6 +62,7 @@ interface ListField<
// TODO: Make this more strict
optionsKey: keyof OPTIONS_MAPPING;
keyFieldName?: string;
hiddenLabel?: boolean;
children: TemplateSchema<
VALUE,
OPTIONS_MAPPING
Expand All @@ -75,6 +82,7 @@ interface ObjectField<VALUE, OPTIONS_MAPPING extends TemplateFieldOptionsMapping
export interface TemplateOptionItem<T extends ValidationType> {
key: T;
label: string;
description?: string;
}

export interface TemplateFieldOptionsMapping {
Expand All @@ -98,23 +106,26 @@ export type TemplateSchema<
| SelectField<ExtractValidation<VALUE>, OPTIONS_MAPPING>)
);

// NOTE: Not adding richtext support on heading
interface HeadingTemplateField {
type: 'heading';
name: string | number | boolean;
label: string;
outlineLevel: number;
description?: string;
context: { field: string, key: string }[],
}

type ObjectKey = string | number | symbol;

type InputTemplateField = {
type: 'input';
name: string | number | boolean;
label: string;
label: string | CellRichTextValue;
outlineLevel: number;
description?: string;
description?: string | CellRichTextValue;
headingBefore?: string;
context: { field: string, key: string }[],
} & ({
dataValidation: 'list';
optionsKey: ObjectKey;
Expand All @@ -136,7 +147,30 @@ export function getCombinedKey(

export type TemplateField = HeadingTemplateField | InputTemplateField;

// TODO: add test
function createInsPlugin(
optionsMap: TemplateFieldOptionsMapping,
context: { field: string, key: string }[],
): ParsePlugin {
return {
tag: 'ins',
transformer: (token, richText) => {
const [optionField, valueField] = token.split('.');
const currOptions = context?.find((item) => item.field === optionField);
const selectedOption = currOptions
? optionsMap?.[optionField]?.find(
(option) => String(option.key) === currOptions?.key,
)
: undefined;

return {
...richText,
// FIXME: Need to add mechanism to identify if we have error for mapping
text: selectedOption?.[valueField as 'description'] ?? '',
};
},
};
}

export function createImportTemplate<
TEMPLATE_SCHEMA,
OPTIONS_MAPPING extends TemplateFieldOptionsMapping
Expand All @@ -145,6 +179,7 @@ export function createImportTemplate<
optionsMap: OPTIONS_MAPPING,
fieldName: string | undefined = undefined,
outlineLevel = -1,
context: { field: string, key: string }[] = [],
): TemplateField[] {
if (schema.type === 'object') {
return [
Expand All @@ -158,6 +193,7 @@ export function createImportTemplate<
optionsMap,
getCombinedKey(key, fieldName),
outlineLevel + 1,
context,
);

return newFields;
Expand All @@ -177,22 +213,26 @@ export function createImportTemplate<
if (isDefined(schema.headingBefore)) {
fields.push({
type: 'heading',
name: getCombinedKey('headingBefore', fieldName),
name: getCombinedKey('heading_before', fieldName),
label: schema.headingBefore,
outlineLevel,
context,
} satisfies HeadingTemplateField);
}

const insPlugin = createInsPlugin(optionsMap, context);

if (schema.type === 'input') {
const field = {
type: 'input',
name: fieldName,
label: schema.label,
description: schema.description,
label: parsePseudoHtml(schema.label, [insPlugin]),
description: parsePseudoHtml(schema.description, [insPlugin]),
dataValidation: (schema.validation === 'number' || schema.validation === 'date' || schema.validation === 'integer' || schema.validation === 'textArea')
? schema.validation
: undefined,
outlineLevel,
context,
} satisfies InputTemplateField;

fields.push(field);
Expand All @@ -203,11 +243,12 @@ export function createImportTemplate<
const field = {
type: 'input',
name: fieldName,
label: schema.label,
description: schema.description,
label: parsePseudoHtml(schema.label, [insPlugin]),
description: parsePseudoHtml(schema.description, [insPlugin]),
outlineLevel,
dataValidation: 'list',
optionsKey: schema.optionsKey,
context,
} satisfies InputTemplateField;

fields.push(field);
Expand All @@ -220,28 +261,29 @@ export function createImportTemplate<
label: schema.label,
description: schema.description,
outlineLevel,
context,
} satisfies HeadingTemplateField;

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

const optionFields = options.flatMap((option) => {
const subHeadingField = {
type: 'heading',
// name: option.key,
name: getCombinedKey(option.key, fieldName),
label: option.label,
outlineLevel: outlineLevel + 1,
// description: schema.description,
context,
} satisfies HeadingTemplateField;

const combinedKey = getCombinedKey(option.key, fieldName);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const newFields = createImportTemplate<any, OPTIONS_MAPPING>(
schema.children,
optionsMap,
// undefined,
getCombinedKey(option.key, fieldName),
combinedKey,
outlineLevel + 1,
[...context, { field: String(schema.optionsKey), key: String(option.key) }],
);

return [
Expand All @@ -252,16 +294,15 @@ export function createImportTemplate<

return [
...fields,
headingField,
!schema.hiddenLabel ? headingField : undefined,
...optionFields,
];
].filter(isDefined);
}

function addClientId(item: object): object {
return { ...item, clientId: randomString() };
}

// TODO: add test
export function getValueFromImportTemplate<
TEMPLATE_SCHEMA,
OPTIONS_MAPPING extends TemplateFieldOptionsMapping,
Expand Down Expand Up @@ -348,6 +389,7 @@ export function getValueFromImportTemplate<
return listValue;
}

/*
type TemplateName = 'dref-application' | 'dref-operational-update' | 'dref-final-report';
export interface ImportTemplateDescription<FormFields> {
Expand All @@ -359,7 +401,6 @@ export interface ImportTemplateDescription<FormFields> {
fieldNameToTabNameMap: Record<string, string>,
}
/*
function isValidTemplate(templateName: unknown): templateName is TemplateName {
const templateNameMap: Record<TemplateName, boolean> = {
'dref-application': true,
Expand Down
110 changes: 110 additions & 0 deletions app/src/utils/richText.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {
isDefined,
isNotDefined,
} from '@togglecorp/fujs';
import { type CellRichTextValue } from 'exceljs';

export interface ParsePlugin {
tag: string,
transformer: (token: string, richText: CellRichTextValue['richText'][number]) => CellRichTextValue['richText'][number],
}

const boldPlugin: ParsePlugin = {
tag: 'b',
transformer: (_: string, richText) => ({
...richText,
font: {
...richText.font,
bold: true,
},
}),
};
const italicsPlugin: ParsePlugin = {
tag: 'i',
transformer: (_: string, richText) => ({
...richText,
font: {
...richText.font,
italic: true,
},
}),
};
const underlinePlugin: ParsePlugin = {
tag: 'u',
transformer: (_: string, richText) => ({
...richText,
font: {
...richText.font,
underline: true,
},
}),
};

/**
* Convert subset of html into excel's richtext format
* @param value string with or without html tags
*/
export function parsePseudoHtml(
value: undefined,
extraPlugins?: ParsePlugin[],
): undefined;
export function parsePseudoHtml(
value: string,
extraPlugins?: ParsePlugin[],
): string | CellRichTextValue
export function parsePseudoHtml(
value: string | undefined,
extraPlugins?: ParsePlugin[],
): string | CellRichTextValue | undefined
export function parsePseudoHtml(
value: string | undefined,
extraPlugins: ParsePlugin[] = [],
): string | CellRichTextValue | undefined {
if (isNotDefined(value)) {
return value;
}

const plugins: ParsePlugin[] = [
boldPlugin,
italicsPlugin,
underlinePlugin,
...extraPlugins,
];

const supportedTags = plugins.map((p) => p.tag).join('|');

const tagRegex = RegExp(`(</?(?:${supportedTags})>)`);
const tokens = value.split(tagRegex);
if (tokens.length === 1) {
return value;
}

const openTagRegex = RegExp(`<(?:${supportedTags})>`);
const closeTagRegex = RegExp(`</(?:${supportedTags})>`);

const stack: string[] = [];
const richText = tokens.map((token) => {
if (token.match(openTagRegex)) {
stack.push(token);
return undefined;
}
if (token.match(closeTagRegex)) {
// TODO: Check correctness by checking closeTag with last openTag
stack.pop();
return undefined;
}

const applicablePlugins = plugins
.filter((plugin) => stack.includes(`<${plugin.tag}>`));

const richTextItem: CellRichTextValue['richText'][number] = applicablePlugins
.reduce(
(acc, plugin) => plugin.transformer(token, acc),
{ text: token },
);
return richTextItem;
}).filter(isDefined);

// TODO: Check correctness to check that stack is empty
return { richText };
}
Loading

0 comments on commit b6f2414

Please sign in to comment.