Files
KanbanGit/resources/js/lib/acceptance-criteria.ts
T
Koda 3ddfcc89f9
linter / quality (pull_request) Successful in 1m16s
tests / ci (8.4) (pull_request) Successful in 1m33s
Add checklist acceptance criteria
2026-05-18 23:24:52 +00:00

147 lines
3.7 KiB
TypeScript

import type {
AcceptanceCriteriaItemModel,
AcceptanceCriteriaModel,
} from '@/types/models/acceptance-criteria-model';
const MAX_INDENT_LEVEL = 8;
export type AcceptanceCriteriaEditorState = {
checklist: string;
summary: string;
};
export const emptyAcceptanceCriteria = (): AcceptanceCriteriaModel => ({
summary: null,
items: [],
});
export const normalizeAcceptanceCriteria = (
value: AcceptanceCriteriaModel | null | undefined,
): AcceptanceCriteriaModel => {
if (value === null || value === undefined) {
return emptyAcceptanceCriteria();
}
const summary = value.summary?.trim() ?? null;
const items = Array.isArray(value.items)
? value.items
.map((item) => ({
checked: item.checked === true,
indent: clampIndent(item.indent),
text: item.text.trim(),
}))
.filter((item) => item.text !== '')
: [];
return {
summary: summary === '' ? null : summary,
items,
};
};
export const parseChecklistLines = (
checklistInput: string,
): AcceptanceCriteriaItemModel[] => {
if (checklistInput.trim() === '') {
return [];
}
return checklistInput
.split(/\r?\n/u)
.map((line) => parseChecklistLine(line))
.filter(
(parsedLine): parsedLine is AcceptanceCriteriaItemModel =>
parsedLine !== null,
);
};
export const formatChecklistLines = (
items: AcceptanceCriteriaItemModel[],
): string => {
return items
.map((item) => {
const indentation = ' '.repeat(clampIndent(item.indent));
const marker = item.checked ? '[x]' : '[ ]';
return `${indentation}${marker} ${item.text}`;
})
.join('\n');
};
export const buildAcceptanceCriteria = (
summaryInput: string,
checklistInput: string,
): AcceptanceCriteriaModel => {
const summary = summaryInput.trim();
return normalizeAcceptanceCriteria({
summary: summary === '' ? null : summary,
items: parseChecklistLines(checklistInput),
});
};
export const acceptanceCriteriaToEditorState = (
value: AcceptanceCriteriaModel | null | undefined,
): AcceptanceCriteriaEditorState => {
const normalized = normalizeAcceptanceCriteria(value);
return {
checklist: formatChecklistLines(normalized.items),
summary: normalized.summary ?? '',
};
};
const parseChecklistLine = (
line: string,
): AcceptanceCriteriaItemModel | null => {
const normalizedLine = line.replaceAll('\t', ' ').trimEnd();
if (normalizedLine.trim() === '') {
return null;
}
const match = /^(?<indent>\s*)(?<content>.+)$/u.exec(normalizedLine);
if (match === null || match.groups === undefined) {
return null;
}
const indentLevel = clampIndent(
Math.floor(match.groups.indent.length / 2),
);
let content = match.groups.content.trim();
let checked = false;
const checkboxMatch =
/^(?:[-*+]\s+)?\[(?<state>[xX ])\]\s*(?<text>.+)$/u.exec(content);
if (checkboxMatch?.groups !== undefined) {
checked = checkboxMatch.groups.state.toLowerCase() === 'x';
content = checkboxMatch.groups.text.trim();
} else {
const bulletMatch = /^[-*+]\s+(?<text>.+)$/u.exec(content);
if (bulletMatch?.groups !== undefined) {
content = bulletMatch.groups.text.trim();
}
}
if (content === '') {
return null;
}
return {
checked,
indent: indentLevel,
text: content,
};
};
const clampIndent = (indent: number): number => {
if (!Number.isFinite(indent)) {
return 0;
}
return Math.max(0, Math.min(MAX_INDENT_LEVEL, Math.trunc(indent)));
};