refactor(review): rollback-only flow and compact change summary
This commit is contained in:
@@ -1624,7 +1624,6 @@ watchEffect(() => {
|
|||||||
const changeActionBusy = ref(false);
|
const changeActionBusy = ref(false);
|
||||||
const activeChangeSetId = ref("");
|
const activeChangeSetId = ref("");
|
||||||
const activeChangeStep = ref(0);
|
const activeChangeStep = ref(0);
|
||||||
const changeSelectionByItemId = ref<Record<string, boolean>>({});
|
|
||||||
const focusedCalendarEventId = ref("");
|
const focusedCalendarEventId = ref("");
|
||||||
const uiPathSyncLocked = ref(false);
|
const uiPathSyncLocked = ref(false);
|
||||||
let popstateHandler: (() => void) | null = null;
|
let popstateHandler: (() => void) | null = null;
|
||||||
@@ -1672,11 +1671,7 @@ const activeChangeItem = computed(() => {
|
|||||||
|
|
||||||
const reviewActive = computed(() => Boolean(activeChangeSetId.value.trim() && activeChangeItems.value.length > 0));
|
const reviewActive = computed(() => Boolean(activeChangeSetId.value.trim() && activeChangeItems.value.length > 0));
|
||||||
const activeChangeStepNumber = computed(() => activeChangeIndex.value + 1);
|
const activeChangeStepNumber = computed(() => activeChangeIndex.value + 1);
|
||||||
const activeChangeApproved = computed(() => {
|
const rollbackableCount = computed(() => activeChangeItems.value.filter((item) => !item.rolledBack).length);
|
||||||
const item = activeChangeItem.value;
|
|
||||||
if (!item || item.rolledBack) return true;
|
|
||||||
return changeSelectionByItemId.value[item.id] !== false;
|
|
||||||
});
|
|
||||||
const activeReviewCalendarEventId = computed(() => {
|
const activeReviewCalendarEventId = computed(() => {
|
||||||
const item = activeChangeItem.value;
|
const item = activeChangeItem.value;
|
||||||
if (!item || item.entity !== "calendar_event" || !item.entityId) return "";
|
if (!item || item.entity !== "calendar_event" || !item.entityId) return "";
|
||||||
@@ -1706,21 +1701,6 @@ const activeReviewContactDiff = computed(() => {
|
|||||||
after: normalizeChangeText(item.after),
|
after: normalizeChangeText(item.after),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const selectedRollbackItemIds = computed(() =>
|
|
||||||
activeChangeItems.value
|
|
||||||
.filter((item) => !item.rolledBack && changeSelectionByItemId.value[item.id] === false)
|
|
||||||
.map((item) => item.id),
|
|
||||||
);
|
|
||||||
const selectedRollbackCount = computed(() => selectedRollbackItemIds.value.length);
|
|
||||||
|
|
||||||
function setReviewApprovalForAll(approved: boolean) {
|
|
||||||
const next: Record<string, boolean> = {};
|
|
||||||
for (const item of activeChangeItems.value) {
|
|
||||||
next[item.id] = item.rolledBack ? true : approved;
|
|
||||||
}
|
|
||||||
changeSelectionByItemId.value = next;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeChangeText(raw: string | null | undefined) {
|
function normalizeChangeText(raw: string | null | undefined) {
|
||||||
const text = String(raw ?? "").trim();
|
const text = String(raw ?? "").trim();
|
||||||
if (!text) return "";
|
if (!text) return "";
|
||||||
@@ -1753,32 +1733,6 @@ function describeChangeAction(action: string) {
|
|||||||
return action || "changed";
|
return action || "changed";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isReviewItemApproved(item: PilotChangeItem | null | undefined) {
|
|
||||||
if (!item || item.rolledBack) return true;
|
|
||||||
return changeSelectionByItemId.value[item.id] !== false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setReviewItemApproval(itemId: string, approved: boolean) {
|
|
||||||
const target = activeChangeItems.value.find((item) => item.id === itemId);
|
|
||||||
if (!target || target.rolledBack) return;
|
|
||||||
changeSelectionByItemId.value = {
|
|
||||||
...changeSelectionByItemId.value,
|
|
||||||
[itemId]: approved,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function onReviewItemApprovalInput(itemId: string, event: Event) {
|
|
||||||
const input = event.target as HTMLInputElement | null;
|
|
||||||
setReviewItemApproval(itemId, Boolean(input?.checked));
|
|
||||||
}
|
|
||||||
|
|
||||||
function onActiveReviewApprovalInput(event: Event) {
|
|
||||||
const item = activeChangeItem.value;
|
|
||||||
if (!item) return;
|
|
||||||
const input = event.target as HTMLInputElement | null;
|
|
||||||
setReviewItemApproval(item.id, Boolean(input?.checked));
|
|
||||||
}
|
|
||||||
|
|
||||||
function calendarCursorToken(date: Date) {
|
function calendarCursorToken(date: Date) {
|
||||||
const y = date.getFullYear();
|
const y = date.getFullYear();
|
||||||
const m = String(date.getMonth() + 1).padStart(2, "0");
|
const m = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
@@ -1880,19 +1834,6 @@ function syncPathFromUi(push = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureChangeSelectionSeeded(message: PilotMessage | null | undefined) {
|
|
||||||
if (!message?.changeItems?.length) {
|
|
||||||
changeSelectionByItemId.value = {};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const next: Record<string, boolean> = {};
|
|
||||||
for (const item of message.changeItems) {
|
|
||||||
const prev = changeSelectionByItemId.value[item.id];
|
|
||||||
next[item.id] = typeof prev === "boolean" ? prev : true;
|
|
||||||
}
|
|
||||||
changeSelectionByItemId.value = next;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setPeopleLeftMode(mode: PeopleLeftMode, push = false) {
|
function setPeopleLeftMode(mode: PeopleLeftMode, push = false) {
|
||||||
selectedTab.value = "communications";
|
selectedTab.value = "communications";
|
||||||
peopleLeftMode.value = mode;
|
peopleLeftMode.value = mode;
|
||||||
@@ -1906,7 +1847,6 @@ function openChangeReview(changeSetId: string, step = 0, push = true) {
|
|||||||
activeChangeSetId.value = targetId;
|
activeChangeSetId.value = targetId;
|
||||||
const items = activeChangeMessage.value?.changeItems ?? [];
|
const items = activeChangeMessage.value?.changeItems ?? [];
|
||||||
activeChangeStep.value = items.length ? Math.max(0, Math.min(step, items.length - 1)) : 0;
|
activeChangeStep.value = items.length ? Math.max(0, Math.min(step, items.length - 1)) : 0;
|
||||||
ensureChangeSelectionSeeded(activeChangeMessage.value);
|
|
||||||
applyReviewStepToUi(push);
|
applyReviewStepToUi(push);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1922,7 +1862,6 @@ function applyPathToUi(pathname: string, search = "") {
|
|||||||
} else {
|
} else {
|
||||||
activeChangeSetId.value = "";
|
activeChangeSetId.value = "";
|
||||||
activeChangeStep.value = 0;
|
activeChangeStep.value = 0;
|
||||||
changeSelectionByItemId.value = {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const calendarEventMatch = path.match(/^\/calendar\/event\/([^/]+)\/?$/i);
|
const calendarEventMatch = path.match(/^\/calendar\/event\/([^/]+)\/?$/i);
|
||||||
@@ -2074,7 +2013,7 @@ async function rollbackLatestChangeSet() {
|
|||||||
|
|
||||||
async function rollbackSelectedChangeItems() {
|
async function rollbackSelectedChangeItems() {
|
||||||
const targetChangeSetId = activeChangeMessage.value?.changeSetId?.trim() || activeChangeSetId.value.trim();
|
const targetChangeSetId = activeChangeMessage.value?.changeSetId?.trim() || activeChangeSetId.value.trim();
|
||||||
const itemIds = selectedRollbackItemIds.value;
|
const itemIds = activeChangeItems.value.filter((item) => !item.rolledBack).map((item) => item.id);
|
||||||
if (changeActionBusy.value || !targetChangeSetId || itemIds.length === 0) return;
|
if (changeActionBusy.value || !targetChangeSetId || itemIds.length === 0) return;
|
||||||
|
|
||||||
changeActionBusy.value = true;
|
changeActionBusy.value = true;
|
||||||
@@ -2084,7 +2023,23 @@ async function rollbackSelectedChangeItems() {
|
|||||||
itemIds,
|
itemIds,
|
||||||
});
|
});
|
||||||
await Promise.all([loadPilotMessages(), refreshCrmData(), loadChatConversations()]);
|
await Promise.all([loadPilotMessages(), refreshCrmData(), loadChatConversations()]);
|
||||||
ensureChangeSelectionSeeded(activeChangeMessage.value);
|
} finally {
|
||||||
|
changeActionBusy.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rollbackChangeItemById(itemId: string) {
|
||||||
|
const item = activeChangeItems.value.find((entry) => entry.id === itemId);
|
||||||
|
const targetChangeSetId = activeChangeMessage.value?.changeSetId?.trim() || activeChangeSetId.value.trim();
|
||||||
|
if (!item || item.rolledBack || !targetChangeSetId || changeActionBusy.value) return;
|
||||||
|
|
||||||
|
changeActionBusy.value = true;
|
||||||
|
try {
|
||||||
|
await gqlFetch<{ rollbackChangeSetItems: { ok: boolean } }>(rollbackChangeSetItemsMutation, {
|
||||||
|
changeSetId: targetChangeSetId,
|
||||||
|
itemIds: [itemId],
|
||||||
|
});
|
||||||
|
await Promise.all([loadPilotMessages(), refreshCrmData(), loadChatConversations()]);
|
||||||
} finally {
|
} finally {
|
||||||
changeActionBusy.value = false;
|
changeActionBusy.value = false;
|
||||||
}
|
}
|
||||||
@@ -2206,7 +2161,6 @@ function applyReviewStepToUi(push = false) {
|
|||||||
function finishReview(push = true) {
|
function finishReview(push = true) {
|
||||||
activeChangeSetId.value = "";
|
activeChangeSetId.value = "";
|
||||||
activeChangeStep.value = 0;
|
activeChangeStep.value = 0;
|
||||||
changeSelectionByItemId.value = {};
|
|
||||||
syncPathFromUi(push);
|
syncPathFromUi(push);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2214,7 +2168,6 @@ watch(
|
|||||||
() => activeChangeMessage.value?.changeSetId,
|
() => activeChangeMessage.value?.changeSetId,
|
||||||
() => {
|
() => {
|
||||||
if (!activeChangeSetId.value.trim()) return;
|
if (!activeChangeSetId.value.trim()) return;
|
||||||
ensureChangeSelectionSeeded(activeChangeMessage.value);
|
|
||||||
const maxIndex = Math.max(0, (activeChangeItems.value.length || 1) - 1);
|
const maxIndex = Math.max(0, (activeChangeItems.value.length || 1) - 1);
|
||||||
if (activeChangeStep.value > maxIndex) activeChangeStep.value = maxIndex;
|
if (activeChangeStep.value > maxIndex) activeChangeStep.value = maxIndex;
|
||||||
applyReviewStepToUi(false);
|
applyReviewStepToUi(false);
|
||||||
@@ -5506,22 +5459,17 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
|||||||
:active-change-step-number="activeChangeStepNumber"
|
:active-change-step-number="activeChangeStepNumber"
|
||||||
:active-change-items="activeChangeItems"
|
:active-change-items="activeChangeItems"
|
||||||
:active-change-item="activeChangeItem"
|
:active-change-item="activeChangeItem"
|
||||||
:active-change-approved="activeChangeApproved"
|
|
||||||
:active-change-index="activeChangeIndex"
|
:active-change-index="activeChangeIndex"
|
||||||
:selected-rollback-count="selectedRollbackCount"
|
:rollbackable-count="rollbackableCount"
|
||||||
:change-action-busy="changeActionBusy"
|
:change-action-busy="changeActionBusy"
|
||||||
:describe-change-entity="describeChangeEntity"
|
:describe-change-entity="describeChangeEntity"
|
||||||
:describe-change-action="describeChangeAction"
|
:describe-change-action="describeChangeAction"
|
||||||
:is-review-item-approved="isReviewItemApproved"
|
|
||||||
@close="finishReview(true)"
|
@close="finishReview(true)"
|
||||||
@active-approval-change="onActiveReviewApprovalInput"
|
|
||||||
@item-approval-change="onReviewItemApprovalInput($event.itemId, $event.event)"
|
|
||||||
@open-item-target="openChangeItemTarget"
|
@open-item-target="openChangeItemTarget"
|
||||||
|
@rollback-item="rollbackChangeItemById"
|
||||||
|
@rollback-all="rollbackSelectedChangeItems"
|
||||||
@prev-step="goToPreviousChangeStep"
|
@prev-step="goToPreviousChangeStep"
|
||||||
@next-step="goToNextChangeStep"
|
@next-step="goToNextChangeStep"
|
||||||
@approve-all="setReviewApprovalForAll(true)"
|
|
||||||
@mark-all-rollback="setReviewApprovalForAll(false)"
|
|
||||||
@rollback-selected="rollbackSelectedChangeItems"
|
|
||||||
@done="finishReview(true)"
|
@done="finishReview(true)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -128,33 +128,19 @@ defineProps<{
|
|||||||
<p class="text-xs font-semibold text-amber-100">
|
<p class="text-xs font-semibold text-amber-100">
|
||||||
{{ message.changeSummary || "Technical change summary" }}
|
{{ message.changeSummary || "Technical change summary" }}
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-2 overflow-x-auto">
|
<div class="mt-2 flex flex-wrap gap-1.5 text-[10px]">
|
||||||
<table class="w-full min-w-[340px] text-left text-[11px] text-white/85">
|
<span class="rounded border border-white/25 px-2 py-0.5 text-white/85">
|
||||||
<thead>
|
total {{ message.changeItems?.length || 0 }}
|
||||||
<tr class="text-white/60">
|
</span>
|
||||||
<th class="py-1 pr-2 font-medium">Metric</th>
|
<span class="rounded border border-white/25 px-2 py-0.5 text-white/85">
|
||||||
<th class="py-1 pr-2 font-medium">Value</th>
|
created {{ summarizeChangeActions(message.changeItems).created }}
|
||||||
</tr>
|
</span>
|
||||||
</thead>
|
<span class="rounded border border-white/25 px-2 py-0.5 text-white/85">
|
||||||
<tbody>
|
updated {{ summarizeChangeActions(message.changeItems).updated }}
|
||||||
<tr>
|
</span>
|
||||||
<td class="py-1 pr-2">Total changes</td>
|
<span class="rounded border border-white/25 px-2 py-0.5 text-white/85">
|
||||||
<td class="py-1 pr-2">{{ message.changeItems?.length || 0 }}</td>
|
archived {{ summarizeChangeActions(message.changeItems).deleted }}
|
||||||
</tr>
|
</span>
|
||||||
<tr>
|
|
||||||
<td class="py-1 pr-2">Created</td>
|
|
||||||
<td class="py-1 pr-2">{{ summarizeChangeActions(message.changeItems).created }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="py-1 pr-2">Updated</td>
|
|
||||||
<td class="py-1 pr-2">{{ summarizeChangeActions(message.changeItems).updated }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="py-1 pr-2">Archived</td>
|
|
||||||
<td class="py-1 pr-2">{{ summarizeChangeActions(message.changeItems).deleted }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="summarizeChangeEntities(message.changeItems).length" class="mt-2 flex flex-wrap gap-1.5">
|
<div v-if="summarizeChangeEntities(message.changeItems).length" class="mt-2 flex flex-wrap gap-1.5">
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -12,25 +12,20 @@ const props = defineProps<{
|
|||||||
activeChangeStepNumber: number;
|
activeChangeStepNumber: number;
|
||||||
activeChangeItems: ChangeItem[];
|
activeChangeItems: ChangeItem[];
|
||||||
activeChangeItem: ChangeItem | null;
|
activeChangeItem: ChangeItem | null;
|
||||||
activeChangeApproved: boolean;
|
|
||||||
activeChangeIndex: number;
|
activeChangeIndex: number;
|
||||||
selectedRollbackCount: number;
|
rollbackableCount: number;
|
||||||
changeActionBusy: boolean;
|
changeActionBusy: boolean;
|
||||||
describeChangeEntity: (entity: string) => string;
|
describeChangeEntity: (entity: string) => string;
|
||||||
describeChangeAction: (action: string) => string;
|
describeChangeAction: (action: string) => string;
|
||||||
isReviewItemApproved: (item: ChangeItem | null | undefined) => boolean;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "close"): void;
|
(e: "close"): void;
|
||||||
(e: "active-approval-change", event: Event): void;
|
|
||||||
(e: "item-approval-change", payload: { itemId: string; event: Event }): void;
|
|
||||||
(e: "open-item-target", item: ChangeItem): void;
|
(e: "open-item-target", item: ChangeItem): void;
|
||||||
|
(e: "rollback-item", itemId: string): void;
|
||||||
|
(e: "rollback-all"): void;
|
||||||
(e: "prev-step"): void;
|
(e: "prev-step"): void;
|
||||||
(e: "next-step"): void;
|
(e: "next-step"): void;
|
||||||
(e: "approve-all"): void;
|
|
||||||
(e: "mark-all-rollback"): void;
|
|
||||||
(e: "rollback-selected"): void;
|
|
||||||
(e: "done"): void;
|
(e: "done"): void;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
@@ -58,23 +53,13 @@ const emit = defineEmits<{
|
|||||||
{{ props.describeChangeEntity(props.activeChangeItem.entity) }}
|
{{ props.describeChangeEntity(props.activeChangeItem.entity) }}
|
||||||
{{ props.describeChangeAction(props.activeChangeItem.action) }}
|
{{ props.describeChangeAction(props.activeChangeItem.action) }}
|
||||||
</p>
|
</p>
|
||||||
<label class="mt-1 inline-flex items-center gap-2 text-xs">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
class="checkbox checkbox-xs"
|
|
||||||
:checked="props.activeChangeApproved"
|
|
||||||
:disabled="props.activeChangeItem.rolledBack"
|
|
||||||
@change="emit('active-approval-change', $event)"
|
|
||||||
>
|
|
||||||
<span>{{ props.activeChangeItem.rolledBack ? "Already rolled back" : "Approve this step" }}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-2 max-h-40 space-y-1 overflow-y-auto pr-1">
|
<div class="mt-2 max-h-40 space-y-1 overflow-y-auto pr-1">
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in props.activeChangeItems"
|
v-for="(item, index) in props.activeChangeItems"
|
||||||
:key="`review-step-${item.id}`"
|
:key="`review-step-${item.id}`"
|
||||||
class="flex items-center gap-2 rounded-lg border px-2 py-1"
|
class="group flex items-center gap-2 rounded-lg border px-2 py-1"
|
||||||
:class="index === props.activeChangeIndex ? 'border-primary/45 bg-primary/10' : 'border-base-300 bg-base-100'"
|
:class="index === props.activeChangeIndex ? 'border-primary/45 bg-primary/10' : 'border-base-300 bg-base-100'"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@@ -88,13 +73,15 @@ const emit = defineEmits<{
|
|||||||
{{ props.describeChangeEntity(item.entity) }}
|
{{ props.describeChangeEntity(item.entity) }}
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
<input
|
<button
|
||||||
type="checkbox"
|
v-if="!item.rolledBack"
|
||||||
class="checkbox checkbox-xs"
|
class="btn btn-ghost btn-xs opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100"
|
||||||
:checked="props.isReviewItemApproved(item)"
|
:disabled="props.changeActionBusy"
|
||||||
:disabled="item.rolledBack"
|
@click="emit('rollback-item', item.id)"
|
||||||
@change="emit('item-approval-change', { itemId: item.id, event: $event })"
|
|
||||||
>
|
>
|
||||||
|
Rollback
|
||||||
|
</button>
|
||||||
|
<span v-else class="text-[10px] font-medium uppercase tracking-wide text-warning">Rolled back</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -116,19 +103,17 @@ const emit = defineEmits<{
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-[11px] text-base-content/70">
|
<p class="text-[11px] text-base-content/70">
|
||||||
Rollback marked: {{ props.selectedRollbackCount }}
|
Rollback available: {{ props.rollbackableCount }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-2 flex flex-wrap gap-2">
|
<div class="mt-2 flex flex-wrap gap-2">
|
||||||
<button class="btn btn-xs btn-outline" @click="emit('approve-all')">Approve all</button>
|
|
||||||
<button class="btn btn-xs btn-outline" @click="emit('mark-all-rollback')">Mark all rollback</button>
|
|
||||||
<button
|
<button
|
||||||
class="btn btn-xs btn-warning"
|
class="btn btn-xs btn-warning"
|
||||||
:disabled="props.changeActionBusy || props.selectedRollbackCount === 0"
|
:disabled="props.changeActionBusy || props.rollbackableCount === 0"
|
||||||
@click="emit('rollback-selected')"
|
@click="emit('rollback-all')"
|
||||||
>
|
>
|
||||||
{{ props.changeActionBusy ? "Applying..." : "Rollback selected" }}
|
{{ props.changeActionBusy ? "Applying..." : "Rollback all" }}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-xs btn-primary ml-auto" @click="emit('done')">Done</button>
|
<button class="btn btn-xs btn-primary ml-auto" @click="emit('done')">Done</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user