gitea/web_src/js/components/projects/WorkflowStore.ts

400 lines
16 KiB
TypeScript

import {reactive} from 'vue';
import {GET, POST} from '../../modules/fetch.ts';
import {showInfoToast, showErrorToast} from '../../modules/toast.ts';
type WorkflowFiltersState = {
issue_type: string;
column: string;
labels: string[];
};
type WorkflowIssueStateAction = '' | 'close' | 'reopen';
type WorkflowActionsState = {
column: string;
add_labels: string[];
remove_labels: string[];
issueState: WorkflowIssueStateAction;
};
type WorkflowDraftState = {
filters: WorkflowFiltersState;
actions: WorkflowActionsState;
};
const createDefaultFilters = (): WorkflowFiltersState => ({issue_type: '', column: '', labels: []});
const createDefaultActions = (): WorkflowActionsState => ({column: '', add_labels: [], remove_labels: [], issueState: ''});
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),
issueState: actions.issueState,
});
export function createWorkflowStore(props: {projectLink: string, eventID: string}) {
const store = reactive({
workflowEvents: [],
selectedItem: props.eventID,
selectedWorkflow: null,
projectColumns: [],
projectLabels: [], // Add labels data
saving: false,
loading: false, // Add loading state to prevent rapid clicks
showCreateDialog: false, // For create workflow dialog
selectedEventType: null, // For workflow creation
workflowFilters: createDefaultFilters(),
workflowActions: createDefaultActions(),
workflowDrafts: {} as Record<string, WorkflowDraftState>,
getDraft(eventId: string): WorkflowDraftState | undefined {
return store.workflowDrafts[eventId];
},
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() {
const response = await GET(`${props.projectLink}/workflows/events`);
store.workflowEvents = await response.json();
return store.workflowEvents;
},
async loadProjectColumns() {
try {
const response = await GET(`${props.projectLink}/workflows/columns`);
store.projectColumns = await response.json();
console.log('[WorkflowStore] Loaded columns:', store.projectColumns);
if (store.projectColumns.length > 0) {
console.log('[WorkflowStore] First column.id type:', typeof store.projectColumns[0].id, 'value:', store.projectColumns[0].id);
}
} catch (error) {
console.error('Failed to load project columns:', error);
store.projectColumns = [];
}
},
async loadWorkflowData(eventId: string) {
store.loading = true;
try {
// Load project columns and labels for the dropdowns
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);
console.log('[WorkflowStore] loadWorkflowData - found workflow:', workflow);
// Load existing configuration from the workflow data
// Convert backend filter format to frontend format
const frontendFilters = {issue_type: '', column: '', labels: []};
// Convert backend action format to frontend format
const frontendActions: WorkflowActionsState = {column: '', add_labels: [], remove_labels: [], issueState: ''};
if (workflow?.filters && Array.isArray(workflow.filters)) {
for (const filter of workflow.filters) {
if (filter.type === 'issue_type') {
frontendFilters.issue_type = filter.value;
} else if (filter.type === 'column') {
frontendFilters.column = filter.value;
} else if (filter.type === 'labels') {
frontendFilters.labels.push(filter.value);
}
}
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') {
if (action.value === 'reopen' || action.value === 'false') {
frontendActions.issueState = 'reopen';
} else if (action.value === 'true' || action.value === 'close') {
frontendActions.issueState = 'close';
}
}
}
}
} 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
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') {
if (action.value === 'reopen' || action.value === 'false') {
frontendActions.issueState = 'reopen';
} else if (action.value === 'true' || action.value === 'close') {
frontendActions.issueState = 'close';
}
}
}
}
store.workflowFilters = frontendFilters;
store.workflowActions = frontendActions;
store.updateDraft(eventId, frontendFilters, frontendActions);
} finally {
store.loading = false;
}
},
async loadProjectLabels() {
try {
const response = await GET(`${props.projectLink}/workflows/labels`);
store.projectLabels = await response.json();
} catch (error) {
console.error('Failed to load project labels:', error);
store.projectLabels = [];
}
},
resetWorkflowData() {
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() {
if (!store.selectedWorkflow) return;
store.saving = true;
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 = {
event_id: eventId,
filters: store.workflowFilters,
actions: store.workflowActions,
};
// Send workflow data
console.info('Sending workflow data:', postData);
const response = await POST(`${props.projectLink}/workflows/${eventId}`, {
data: postData,
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const errorText = await response.text();
console.error('Response error:', errorText);
showErrorToast(`Failed to save workflow: ${response.status} ${response.statusText}\n${errorText}`);
return;
}
const result = await response.json();
console.log('Response result:', result);
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 ||
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();
// Find the reloaded workflow which has complete data including capabilities
const reloadedWorkflow = store.workflowEvents.find((w) => w.event_id === result.workflow.event_id);
if (reloadedWorkflow) {
// Use the reloaded workflow as it has all the necessary fields
store.selectedWorkflow = reloadedWorkflow;
store.selectedItem = reloadedWorkflow.event_id;
} else {
// Fallback: use the result from backend (shouldn't normally happen)
store.selectedWorkflow = result.workflow;
store.selectedItem = result.workflow.event_id;
}
// Convert backend data to frontend format and update form
// Use the selectedWorkflow which now points to the reloaded workflow with complete data
const frontendFilters = {issue_type: '', column: '', labels: []};
const frontendActions: WorkflowActionsState = {column: '', add_labels: [], remove_labels: [], issueState: ''};
if (store.selectedWorkflow.filters && Array.isArray(store.selectedWorkflow.filters)) {
for (const filter of store.selectedWorkflow.filters) {
if (filter.type === 'issue_type') {
frontendFilters.issue_type = filter.value;
} else if (filter.type === 'column') {
frontendFilters.column = filter.value;
} else if (filter.type === 'labels') {
frontendFilters.labels.push(filter.value);
}
}
}
if (store.selectedWorkflow.actions && Array.isArray(store.selectedWorkflow.actions)) {
for (const action of store.selectedWorkflow.actions) {
if (action.type === 'column') {
frontendActions.column = action.value;
} else if (action.type === 'add_labels') {
frontendActions.add_labels.push(action.value);
} else if (action.type === 'remove_labels') {
frontendActions.remove_labels.push(action.value);
} else if (action.type === 'close') {
if (action.value === 'reopen' || action.value === 'false') {
frontendActions.issueState = 'reopen';
} else if (action.value === 'true' || action.value === 'close') {
frontendActions.issueState = 'close';
}
}
}
}
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) {
const newUrl = `${props.projectLink}/workflows/${store.selectedWorkflow.event_id}`;
window.history.replaceState({eventId: store.selectedWorkflow.event_id}, '', newUrl);
}
showInfoToast('Workflow saved successfully!');
} else {
console.error('Unexpected response format:', result);
showErrorToast('Failed to save workflow: Unexpected response format');
}
} catch (error) {
console.error('Error saving workflow:', error);
showErrorToast(`Error saving workflow: ${error.message}`);
} finally {
store.saving = false;
}
},
async saveWorkflowStatus() {
if (!store.selectedWorkflow || store.selectedWorkflow.id === 0) return;
try {
const formData = new FormData();
formData.append('enabled', store.selectedWorkflow.enabled.toString());
// Use workflow ID for status update
const workflowId = store.selectedWorkflow.id;
const response = await POST(`${props.projectLink}/workflows/${workflowId}/status`, {
data: formData,
});
if (!response.ok) {
const errorText = await response.text();
console.error('Failed to update workflow status:', errorText);
showErrorToast(`Failed to update workflow status: ${response.status} ${response.statusText}`);
// Revert the status change on error
store.selectedWorkflow.enabled = !store.selectedWorkflow.enabled;
return;
}
const result = await response.json();
if (result.success) {
// Update workflow in the list
const existingIndex = store.workflowEvents.findIndex((e) => e.event_id === store.selectedWorkflow.event_id);
if (existingIndex >= 0) {
store.workflowEvents[existingIndex].enabled = store.selectedWorkflow.enabled;
}
} else {
// Revert the status change on failure
store.selectedWorkflow.enabled = !store.selectedWorkflow.enabled;
showErrorToast('Failed to update workflow status');
}
} catch (error) {
console.error('Error updating workflow status:', error);
// Revert the status change on error
store.selectedWorkflow.enabled = !store.selectedWorkflow.enabled;
showErrorToast(`Error updating workflow status: ${error.message}`);
}
},
async deleteWorkflow() {
if (!store.selectedWorkflow || store.selectedWorkflow.id === 0) return;
try {
// Use workflow ID for deletion
const workflowId = store.selectedWorkflow.id;
const response = await POST(`${props.projectLink}/workflows/${workflowId}/delete`, {
data: new FormData(),
});
if (!response.ok) {
const errorText = await response.text();
console.error('Failed to delete workflow:', errorText);
showErrorToast(`Failed to delete workflow: ${response.status} ${response.statusText}`);
return;
}
const result = await response.json();
if (result.success) {
// Remove workflow from the list
const existingIndex = store.workflowEvents.findIndex((e) => e.event_id === store.selectedWorkflow.event_id);
if (existingIndex >= 0) {
store.workflowEvents.splice(existingIndex, 1);
}
} else {
showErrorToast('Failed to delete workflow');
}
} catch (error) {
console.error('Error deleting workflow:', error);
showErrorToast(`Error deleting workflow: ${error.message}`);
}
},
});
return store;
}