Redesign ticket details with inline editing
linter / quality (pull_request) Successful in 1m23s
tests / ci (8.4) (pull_request) Successful in 1m49s

This commit is contained in:
2026-07-18 19:50:50 +02:00
parent 5f917e93eb
commit 8c70d094b8
11 changed files with 2712 additions and 1244 deletions
+221 -3
View File
@@ -96,6 +96,226 @@ export const syncAcceptanceCriteriaDescendantCheckedState = (
});
};
export const acceptanceCriteriaSubtreeEnd = (
items: AcceptanceCriteriaModel,
index: number,
): number => {
const indent = items[index]?.indent;
if (indent === undefined) {
return index;
}
let end = index + 1;
while (end < items.length && items[end].indent > indent) {
end++;
}
return end;
};
export const previousAcceptanceCriteriaSiblingIndex = (
items: AcceptanceCriteriaModel,
index: number,
): number | null => {
const indent = items[index]?.indent;
if (indent === undefined) {
return null;
}
for (
let candidateIndex = index - 1;
candidateIndex >= 0;
candidateIndex--
) {
const candidateIndent = items[candidateIndex].indent;
if (candidateIndent < indent) {
return null;
}
if (candidateIndent === indent) {
return candidateIndex;
}
}
return null;
};
export const nextAcceptanceCriteriaSiblingIndex = (
items: AcceptanceCriteriaModel,
index: number,
): number | null => {
const indent = items[index]?.indent;
const candidateIndex = acceptanceCriteriaSubtreeEnd(items, index);
if (
indent === undefined ||
candidateIndex >= items.length ||
items[candidateIndex].indent !== indent
) {
return null;
}
return candidateIndex;
};
export const addAcceptanceCriterionAfter = (
items: AcceptanceCriteriaModel,
index: number | null,
): { index: number; items: AcceptanceCriteriaModel } => {
const nextItems = items.map((item) => ({ ...item }));
const insertionIndex =
index === null
? nextItems.length
: acceptanceCriteriaSubtreeEnd(nextItems, index);
const indent = index === null ? 0 : (nextItems[index]?.indent ?? 0);
nextItems.splice(insertionIndex, 0, {
checked: false,
indent,
text: '',
});
return {
index: insertionIndex,
items: nextItems,
};
};
export const indentAcceptanceCriterionSubtree = (
items: AcceptanceCriteriaModel,
index: number,
): AcceptanceCriteriaModel => {
const previousSiblingIndex = previousAcceptanceCriteriaSiblingIndex(
items,
index,
);
const item = items[index];
if (
item === undefined ||
item.indent >= MAX_INDENT_LEVEL ||
previousSiblingIndex === null
) {
return items;
}
const subtreeEnd = acceptanceCriteriaSubtreeEnd(items, index);
return items.map((criterion, criterionIndex) => ({
...criterion,
indent:
criterionIndex >= index && criterionIndex < subtreeEnd
? criterion.indent + 1
: criterion.indent,
}));
};
export const outdentAcceptanceCriterionSubtree = (
items: AcceptanceCriteriaModel,
index: number,
): AcceptanceCriteriaModel => {
const item = items[index];
if (item === undefined || item.indent === 0) {
return items;
}
const subtreeEnd = acceptanceCriteriaSubtreeEnd(items, index);
return items.map((criterion, criterionIndex) => ({
...criterion,
indent:
criterionIndex >= index && criterionIndex < subtreeEnd
? criterion.indent - 1
: criterion.indent,
}));
};
export const removeAcceptanceCriterion = (
items: AcceptanceCriteriaModel,
index: number,
): AcceptanceCriteriaModel => {
const item = items[index];
if (item === undefined) {
return items;
}
const subtreeEnd = acceptanceCriteriaSubtreeEnd(items, index);
return items
.filter((_criterion, criterionIndex) => criterionIndex !== index)
.map((criterion, criterionIndex) => {
const originalIndex =
criterionIndex >= index ? criterionIndex + 1 : criterionIndex;
return {
...criterion,
indent:
originalIndex > index && originalIndex < subtreeEnd
? criterion.indent - 1
: criterion.indent,
};
});
};
export const moveAcceptanceCriterionSubtree = (
items: AcceptanceCriteriaModel,
index: number,
direction: 'down' | 'up',
): AcceptanceCriteriaModel => {
const subtreeEnd = acceptanceCriteriaSubtreeEnd(items, index);
const currentSubtree = items
.slice(index, subtreeEnd)
.map((item) => ({ ...item }));
if (direction === 'up') {
const previousSiblingIndex = previousAcceptanceCriteriaSiblingIndex(
items,
index,
);
if (previousSiblingIndex === null) {
return items;
}
return [
...items
.slice(0, previousSiblingIndex)
.map((item) => ({ ...item })),
...currentSubtree,
...items
.slice(previousSiblingIndex, index)
.map((item) => ({ ...item })),
...items.slice(subtreeEnd).map((item) => ({ ...item })),
];
}
const nextSiblingIndex = nextAcceptanceCriteriaSiblingIndex(items, index);
if (nextSiblingIndex === null) {
return items;
}
const nextSubtreeEnd = acceptanceCriteriaSubtreeEnd(
items,
nextSiblingIndex,
);
return [
...items.slice(0, index).map((item) => ({ ...item })),
...items
.slice(nextSiblingIndex, nextSubtreeEnd)
.map((item) => ({ ...item })),
...currentSubtree,
...items.slice(nextSubtreeEnd).map((item) => ({ ...item })),
];
};
export const acceptanceCriteriaToEditorState = (
value: AcceptanceCriteriaModel | null | undefined,
): AcceptanceCriteriaEditorState => {
@@ -121,9 +341,7 @@ const parseChecklistLine = (
return null;
}
const indentLevel = clampIndent(
Math.floor(match.groups.indent.length / 2),
);
const indentLevel = clampIndent(Math.floor(match.groups.indent.length / 2));
let content = match.groups.content.trim();
let checked = false;