pull/30205/head
Lunny Xiao 2025-10-23 14:44:03 +07:00
parent 829fa15816
commit 7240b2b144
No known key found for this signature in database
GPG Key ID: C3B7C91B632F738A
2 changed files with 181 additions and 35 deletions

@ -30,6 +30,13 @@ const isInEditMode = computed(() => {
return store.selectedWorkflow._isEditing || false;
});
const showCancelButton = computed(() => {
if (!store.selectedWorkflow) return false;
if (store.selectedWorkflow.id > 0) return true;
const eventId = store.selectedWorkflow.event_id ?? '';
return typeof eventId === 'string' && eventId.startsWith('clone-');
});
// Helper to set edit mode for current workflow
const setEditMode = (enabled) => {
if (store.selectedWorkflow) {
@ -43,21 +50,39 @@ const setEditMode = (enabled) => {
// Store previous selection for cancel functionality
const isTemporaryWorkflow = (workflow) => {
if (!workflow) return false;
if (workflow.id > 0) return false;
const eventId = typeof workflow.event_id === 'string' ? workflow.event_id : '';
return eventId.startsWith('clone-') || eventId.startsWith('new-');
};
const removeTemporaryWorkflow = (workflow) => {
if (!isTemporaryWorkflow(workflow)) return;
const eventId = workflow.event_id;
const tempIndex = store.workflowEvents.findIndex((w) => w.event_id === eventId);
if (tempIndex >= 0) {
store.workflowEvents.splice(tempIndex, 1);
}
if (typeof store.clearDraft === 'function') {
store.clearDraft(eventId);
}
};
const toggleEditMode = () => {
if (isInEditMode.value) {
// Canceling edit mode
const canceledWorkflow = store.selectedWorkflow;
const hadTemporarySelection = isTemporaryWorkflow(canceledWorkflow);
if (hadTemporarySelection) {
removeTemporaryWorkflow(canceledWorkflow);
}
if (previousSelection.value) {
// If there was a previous selection, return to it
if (store.selectedWorkflow && store.selectedWorkflow.id === 0) {
// Remove temporary unsaved workflow (new or cloned) from list
const tempIndex = store.workflowEvents.findIndex((w) =>
w.event_id === store.selectedWorkflow.event_id,
);
if (tempIndex >= 0) {
store.workflowEvents.splice(tempIndex, 1);
}
}
// Restore previous selection
store.selectedItem = previousSelection.value.selectedItem;
store.selectedWorkflow = previousSelection.value.selectedWorkflow;
@ -65,6 +90,21 @@ const toggleEditMode = () => {
store.loadWorkflowData(previousSelection.value.selectedWorkflow.event_id);
}
previousSelection.value = null;
} else if (hadTemporarySelection) {
// If we removed a temporary item but have no previous selection, fall back to first workflow
const fallback = store.workflowEvents.find((w) => {
if (!canceledWorkflow) return false;
const baseType = canceledWorkflow.base_event_type || canceledWorkflow.workflow_event;
return baseType && (w.base_event_type === baseType || w.workflow_event === baseType || w.event_id === baseType);
}) || store.workflowEvents[0];
if (fallback) {
store.selectedItem = fallback.event_id;
store.selectedWorkflow = fallback;
store.loadWorkflowData(fallback.event_id);
} else {
store.selectedItem = null;
store.selectedWorkflow = null;
}
}
setEditMode(false);
} else {
@ -187,6 +227,12 @@ const cloneWorkflow = (sourceWorkflow) => {
store.workflowEvents.push(clonedWorkflow);
}
// Remember the source so cancel can return to it
previousSelection.value = {
selectedItem: store.selectedItem,
selectedWorkflow: store.selectedWorkflow ? {...store.selectedWorkflow} : {...sourceWorkflow},
};
// Select the cloned workflow and enter edit mode
store.selectedItem = tempId;
store.selectedWorkflow = clonedWorkflow;
@ -195,7 +241,6 @@ const cloneWorkflow = (sourceWorkflow) => {
store.loadWorkflowData(tempId);
// Enter edit mode
previousSelection.value = null; // No previous selection for cloned workflow
setEditMode(true);
// Update URL
@ -470,6 +515,25 @@ watch(isInEditMode, async (newVal) => {
}
});
const getCurrentDraftKey = () => {
if (!store.selectedWorkflow) return null;
return store.selectedWorkflow.event_id || store.selectedWorkflow.base_event_type;
};
const persistDraftState = () => {
const draftKey = getCurrentDraftKey();
if (!draftKey) return;
store.updateDraft(draftKey, store.workflowFilters, store.workflowActions);
};
watch(() => store.workflowFilters, () => {
persistDraftState();
}, {deep: true});
watch(() => store.workflowActions, () => {
persistDraftState();
}, {deep: true});
onMounted(async () => {
// Load all necessary data
store.workflowEvents = await store.loadEvents();
@ -666,6 +730,16 @@ onUnmounted(() => {
<div class="editor-actions-header">
<!-- Edit Mode Buttons -->
<template v-if="isInEditMode">
<!-- Cancel Button -->
<button
v-if="showCancelButton"
class="btn btn-outline-secondary"
@click="toggleEditMode"
>
<i class="times icon"/>
Cancel
</button>
<!-- Save Button -->
<button
class="btn btn-primary"
@ -676,15 +750,6 @@ onUnmounted(() => {
Save
</button>
<!-- Cancel Button -->
<button
class="btn btn-outline-secondary"
@click="toggleEditMode"
>
<i class="times icon"/>
Cancel
</button>
<!-- Delete Button (only for configured workflows) -->
<button
v-if="store.selectedWorkflow && store.selectedWorkflow.id > 0"

@ -2,7 +2,41 @@ import {reactive} from 'vue';
import {GET, POST} from '../../modules/fetch.ts';
import {showInfoToast, showErrorToast} from '../../modules/toast.ts';
export function createWorkflowStore(props: { projectLink: string, eventID: string}) {
type WorkflowFiltersState = {
issue_type: string;
column: string;
labels: string[];
};
type WorkflowActionsState = {
column: string;
add_labels: string[];
remove_labels: string[];
closeIssue: boolean;
};
type WorkflowDraftState = {
filters: WorkflowFiltersState;
actions: WorkflowActionsState;
};
const createDefaultFilters = (): WorkflowFiltersState => ({issue_type: '', column: '', labels: []});
const createDefaultActions = (): WorkflowActionsState => ({column: '', add_labels: [], remove_labels: [], closeIssue: false});
const cloneFilters = (filters: WorkflowFiltersState): WorkflowFiltersState => ({
issue_type: filters.issue_type,
column: filters.column,
labels: Array.from(filters.labels),
});
const cloneActions = (actions: WorkflowActionsState): WorkflowActionsState => ({
column: actions.column,
add_labels: Array.from(actions.add_labels),
remove_labels: Array.from(actions.remove_labels),
closeIssue: actions.closeIssue,
});
export function createWorkflowStore(props: {projectLink: string, eventID: string}) {
const store = reactive({
workflowEvents: [],
selectedItem: props.eventID,
@ -14,17 +48,25 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin
showCreateDialog: false, // For create workflow dialog
selectedEventType: null, // For workflow creation
workflowFilters: {
issue_type: '', // 'issue', 'pull_request', or ''
column: '', // target column ID for item_column_changed event
labels: [], // label IDs to filter by
workflowFilters: createDefaultFilters(),
workflowActions: createDefaultActions(),
workflowDrafts: {} as Record<string, WorkflowDraftState>,
getDraft(eventId: string): WorkflowDraftState | undefined {
return store.workflowDrafts[eventId];
},
workflowActions: {
column: '', // column ID to move to
add_labels: [], // selected label IDs
remove_labels: [], // selected label IDs to remove
closeIssue: false,
updateDraft(eventId: string, filters: WorkflowFiltersState, actions: WorkflowActionsState) {
store.workflowDrafts[eventId] = {
filters: cloneFilters(filters),
actions: cloneActions(actions),
};
},
clearDraft(eventId: string) {
delete store.workflowDrafts[eventId];
},
async loadEvents() {
@ -54,6 +96,13 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin
await store.loadProjectColumns();
await store.loadProjectLabels();
const draft = store.getDraft(eventId);
if (draft) {
store.workflowFilters = cloneFilters(draft.filters);
store.workflowActions = cloneActions(draft.actions);
return;
}
// Find the workflow from existing workflowEvents
const workflow = store.workflowEvents.find((e) => e.event_id === eventId);
console.log('[WorkflowStore] loadWorkflowData - eventId:', eventId);
@ -65,7 +114,7 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin
// Convert backend action format to frontend format
const frontendActions = {column: '', add_labels: [], remove_labels: [], closeIssue: false};
if (workflow) {
if (workflow?.filters && Array.isArray(workflow.filters)) {
for (const filter of workflow.filters) {
if (filter.type === 'issue_type') {
frontendFilters.issue_type = filter.value;
@ -76,6 +125,23 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin
}
}
if (workflow.actions && Array.isArray(workflow.actions)) {
for (const action of workflow.actions) {
if (action.type === 'column') {
// Backend returns string, keep as string to match column.id type
frontendActions.column = action.value;
} else if (action.type === 'add_labels') {
// Backend returns string, keep as string to match label.id type
frontendActions.add_labels.push(action.value);
} else if (action.type === 'remove_labels') {
// Backend returns string, keep as string to match label.id type
frontendActions.remove_labels.push(action.value);
} else if (action.type === 'close') {
frontendActions.closeIssue = action.value === 'true';
}
}
}
} else if (workflow?.actions && Array.isArray(workflow.actions)) {
for (const action of workflow.actions) {
if (action.type === 'column') {
// Backend returns string, keep as string to match column.id type
@ -94,6 +160,7 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin
store.workflowFilters = frontendFilters;
store.workflowActions = frontendActions;
store.updateDraft(eventId, frontendFilters, frontendActions);
} finally {
store.loading = false;
}
@ -110,8 +177,13 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin
},
resetWorkflowData() {
store.workflowFilters = {issue_type: '', column: '', labels: []};
store.workflowActions = {column: '', add_labels: [], remove_labels: [], closeIssue: false};
store.workflowFilters = createDefaultFilters();
store.workflowActions = createDefaultActions();
const currentEventId = store.selectedWorkflow?.event_id || store.selectedWorkflow?.base_event_type;
if (currentEventId) {
store.updateDraft(currentEventId, store.workflowFilters, store.workflowActions);
}
},
async saveWorkflow() {
@ -121,6 +193,7 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin
try {
// For new workflows, use the base event type
const eventId = store.selectedWorkflow.base_event_type || store.selectedWorkflow.event_id;
const previousDraftKey = store.selectedWorkflow.event_id || store.selectedWorkflow.base_event_type;
// Convert frontend data format to backend JSON format
const postData = {
@ -151,9 +224,14 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin
if (result.success && result.workflow) {
// Always reload the events list to get the updated structure
// This ensures we have both the base event and the new filtered event
const eventKey = typeof store.selectedWorkflow.event_id === 'string' ? store.selectedWorkflow.event_id : '';
const wasNewWorkflow = store.selectedWorkflow.id === 0 ||
store.selectedWorkflow.event_id.startsWith('new-') ||
store.selectedWorkflow.event_id.startsWith('clone-');
eventKey.startsWith('new-') ||
eventKey.startsWith('clone-');
if (wasNewWorkflow && previousDraftKey) {
store.clearDraft(previousDraftKey);
}
// Reload events from server to get the correct event structure
await store.loadEvents();
@ -204,6 +282,9 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin
store.workflowFilters = frontendFilters;
store.workflowActions = frontendActions;
if (store.selectedWorkflow?.event_id) {
store.updateDraft(store.selectedWorkflow.event_id, frontendFilters, frontendActions);
}
// Update URL to use the new workflow ID
if (wasNewWorkflow) {