Redesign ticket details with inline editing
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user