Skip to content

Commit

Permalink
feat: implement parse user input and submit actions for repeater tckt…
Browse files Browse the repository at this point in the history
…-310
  • Loading branch information
kalasgarov committed Jan 6, 2025
1 parent 1c94e04 commit c74724d
Show file tree
Hide file tree
Showing 6 changed files with 386 additions and 106 deletions.
70 changes: 46 additions & 24 deletions packages/forms/src/pattern.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ export type GetPattern<T extends Pattern = Pattern> = (

export type ParseUserInput<Pattern, PatternOutput> = (
pattern: Pattern,
obj: unknown
obj: unknown,
config?: FormConfig,
form?: Blueprint
) => r.Result<PatternOutput, FormError>;

export type ParsePatternConfigData<PatternConfigData> = (
Expand Down Expand Up @@ -121,10 +123,6 @@ export const validatePattern = (
pattern: Pattern,
value: any
): r.Result<Pattern['data'], FormError> => {
/**
* TODO: maybe touch this file to see if there are fields that are part of the repeater
* that are being treated and a standalone thing. uuid.index.uuid -> ownedByRepeater
*/
if (!patternConfig.parseUserInput) {
return {
success: true,
Expand All @@ -138,15 +136,31 @@ export const validatePattern = (
return r.success(parseResult.data);
};

const setNestedValue = (
obj: Record<string, any>,
path: string[],
value: any
): void => {
path.reduce((acc, key, idx) => {
if (idx === path.length - 1) {
acc[key] = value;
} else {
if (!acc[key]) {
acc[key] = isNaN(Number(path[idx + 1])) ? {} : [];
}
}
return acc[key];
}, obj);
};

const aggregateValuesByPrefix = (
values: Record<string, string>
values: Record<string, any>
): Record<string, any> => {
const aggregatedValues: Record<string, any> = {};

for (const [key, value] of Object.entries(values)) {
set(aggregatedValues, key, value);
const keys = key.split('.');
setNestedValue(aggregatedValues, keys, value);
}

return aggregatedValues;
};

Expand All @@ -155,36 +169,44 @@ export const aggregatePatternSessionValues = (
form: Blueprint,
patternConfig: PatternConfig,
pattern: Pattern,
values: Record<string, string>,
values: Record<string, any>,
result: {
values: Record<PatternId, PatternValue>;
errors: Record<PatternId, FormError>;
}
) => {
const aggregatedValues = aggregateValuesByPrefix(values);

if (patternConfig.parseUserInput) {
const isRepeaterType = pattern.type === 'repeater';
const patternValues = aggregatedValues[pattern.id];
const parseResult = patternConfig.parseUserInput(pattern, patternValues);
let parseResult: any = patternConfig.parseUserInput(
pattern,
patternValues,
config,
form
);

if (parseResult.success) {
result.values[pattern.id] = parseResult.data;
delete result.errors[pattern.id];
} else {
result.values[pattern.id] = values[pattern.id];
result.values[pattern.id] = isRepeaterType
? parseResult.data
: values[pattern.id];
result.errors[pattern.id] = parseResult.error;
}
}
for (const child of patternConfig.getChildren(pattern, form.patterns)) {
const childPatternConfig = getPatternConfig(config, child.type);
aggregatePatternSessionValues(
config,
form,
childPatternConfig,
child,
values,
result
);
} else {
for (const child of patternConfig.getChildren(pattern, form.patterns)) {
const childPatternConfig = getPatternConfig(config, child.type);
aggregatePatternSessionValues(
config,
form,
childPatternConfig,
child,
values,
result
);
}
}
return result;
};
Expand Down
31 changes: 0 additions & 31 deletions packages/forms/src/patterns/repeater/config.ts

This file was deleted.

197 changes: 175 additions & 22 deletions packages/forms/src/patterns/repeater/index.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,203 @@
import { z } from 'zod';
import { type Result } from '@atj/common';
import { type FormError } from '../../error.js';
import {
type FormConfig,
ParseUserInput,
type Pattern,
type PatternConfig,
type PatternId,
getPatternConfig,
} from '../../pattern.js';
import { parseConfigData } from './config.js';
import { safeZodParseFormErrors } from '../../util/zod.js';
import { createPrompt } from './prompt.js';
import { Blueprint } from '../../types.js';

export type RepeaterPattern = Pattern<{
legend?: string;
showControls?: boolean;
patterns: PatternId[];
}>;

const PromptActionSchema = z.object({
type: z.literal('submit'),
submitAction: z.union([
z.literal('submit'),
z.literal('next'),
z.string().regex(/^action\/[^/]+\/[^/]+$/),
]),
text: z.string(),
});

const configSchema = z.object({
legend: z.string().min(1),
showControls: z.boolean().optional(),
patterns: z.union([
z.array(z.string()),
z
.string()
.transform(value =>
value
.split(',')
.map(String)
.filter(value => value)
)
.pipe(z.string().array()),
]),
actions: z.array(PromptActionSchema).default([
{
type: 'submit',
submitAction: 'submit',
text: 'Submit',
},
]),
});

export const parseConfigData = (obj: unknown) => {
return safeZodParseFormErrors(configSchema, obj);
};

interface RepeaterSuccess {
success: true;
data: {
[key: string]: Record<string, any>[];
};
}

interface RepeaterFailure {
success: false;
error: FormError;
data?: {
[key: string]: Record<string, any>[];
};
}

type RepeaterResult = RepeaterSuccess | RepeaterFailure;

export const repeaterConfig: PatternConfig<RepeaterPattern> = {
displayName: 'Repeater',
iconPath: 'block-icon.svg',
initial: {
legend: 'Default Heading',
patterns: [],
showControls: true,
},
parseConfigData,
getChildren(pattern, patterns) {
return pattern.data.patterns.map(
(patternId: string) => patterns[patternId]
);
},
/*
* TODO: this probably needs a parseUserInput method that maps over the repeater pattern and then
* gets all its child components in a new function. Dan suggested that this is a way to get the dynamic
* indexes working.
*
*/
removeChildPattern(pattern, patternId) {
const newPatterns = pattern.data.patterns.filter(
(id: string) => patternId !== id
);
if (newPatterns.length === pattern.data.patterns.length) {
return pattern;
// @ts-ignore
parseUserInput: ((
pattern: RepeaterPattern,
input: unknown,
config?: FormConfig<Pattern<any>, unknown>,
form?: any
): RepeaterResult => {
if (!config) {
return {
success: false,
error: {
type: 'custom',
message: 'Form configuration is required',
},
data: {
[pattern.id]: [],
},
};
}

const values = input as Array<Record<string, any>>;
if (!Array.isArray(values)) {
return {
success: false,
error: {
type: 'custom',
message: 'Invalid repeater input format',
},
data: {
[pattern.id]: [],
},
};
}

const errors: Record<string, FormError> = {};
const parsedValues: Array<Record<string, any>> = [];

// Get child patterns
const patternConfig = getPatternConfig(config, pattern.type);
const childPatterns = patternConfig.getChildren(pattern, form?.patterns);

values.forEach((repeaterItem, index) => {
const itemValues: Record<string, any> = {};

childPatterns.forEach((childPattern: Pattern<any>) => {
const childConfig = getPatternConfig(config, childPattern.type);
if (childConfig?.parseUserInput) {
const rawValue = repeaterItem[childPattern.id];

let childValue = rawValue;

if (typeof rawValue === 'string') {
const initialValue = childConfig.initial;

if (initialValue && typeof initialValue === 'object') {
const keys = Object.keys(initialValue);
// If initial value has a single key structure, use it as template
if (keys.length === 1) {
childValue = { [keys[0]]: rawValue };
}
}
} else if (rawValue && typeof rawValue === 'object') {
// Keep existing object structure
childValue = rawValue;
}

const parseResult = childConfig.parseUserInput(
childPattern,
childValue,
config,
form
);

// Store the value in its original format
itemValues[childPattern.id] = parseResult.success
? parseResult.data
: childValue;

if (!parseResult.success) {
errors[`${pattern.id}.${index}.${childPattern.id}`] =
parseResult.error;
}
}
});

parsedValues.push(itemValues);
});

const hasErrors = Object.keys(errors).length > 0;

if (hasErrors) {
return {
success: false,
error: {
type: 'custom',
message: 'Please ensure all fields are properly filled out.',
fields: errors,
},
data: {
[pattern.id]: parsedValues,
},
};
}

return {
...pattern,
success: true,
data: {
...pattern.data,
patterns: newPatterns,
[pattern.id]: parsedValues,
},
};
}) as unknown as ParseUserInput<RepeaterPattern, unknown>,
parseConfigData,
getChildren: (pattern, patterns) => {
return pattern.data.patterns
.map((patternId: string) => patterns[patternId])
.filter(Boolean);
},
createPrompt,
};
Loading

0 comments on commit c74724d

Please sign in to comment.