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

Feature/dref import template improvements #1434

Merged
merged 8 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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