Add checklist acceptance criteria
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
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)));
|
||||
};
|
||||
Reference in New Issue
Block a user