0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-02-23 10:45:28 +01:00

Use camel case in typescript

This commit is contained in:
Lunny Xiao 2025-12-29 22:31:39 -08:00
parent a6eb361a23
commit 1a045fa62e
No known key found for this signature in database
GPG Key ID: C3B7C91B632F738A
5 changed files with 173 additions and 119 deletions

View File

@ -22,6 +22,7 @@
"add-asset-webpack-plugin": "3.1.1",
"ansi_up": "6.0.6",
"asciinema-player": "3.13.5",
"camelcase-keys": "10.0.1",
"chart.js": "4.5.1",
"chartjs-adapter-dayjs-4": "1.0.4",
"chartjs-plugin-zoom": "2.2.0",

38
pnpm-lock.yaml generated
View File

@ -74,6 +74,9 @@ importers:
asciinema-player:
specifier: 3.13.5
version: 3.13.5
camelcase-keys:
specifier: 10.0.1
version: 10.0.1
chart.js:
specifier: 4.5.1
version: 4.5.1
@ -1917,6 +1920,14 @@ packages:
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
engines: {node: '>= 6'}
camelcase-keys@10.0.1:
resolution: {integrity: sha512-kIH5nQUKB8ORZrwjk40GUgnhzuzxwKb6n5L+anOVmVSrjgix1pfNqVNl0tEcOP4AaFYdke3NNpNiGDSD112lcA==}
engines: {node: '>=20'}
camelcase@8.0.0:
resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==}
engines: {node: '>=16'}
caniuse-lite@1.0.30001760:
resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==}
@ -3194,6 +3205,10 @@ packages:
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
map-obj@5.0.2:
resolution: {integrity: sha512-K6K2NgKnTXimT3779/4KxSvobxOtMmx1LBZ3NwRxT/MDIR3Br/fQ4Q+WCX5QxjyUR8zg5+RV9Tbf2c5pAWTD2A==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
markdown-it@14.1.0:
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
hasBin: true
@ -3709,6 +3724,10 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
quick-lru@7.3.0:
resolution: {integrity: sha512-k9lSsjl36EJdK7I06v7APZCbyGT2vMTsYSRX1Q2nbYmnkBqgUhRkAuzH08Ciotteu/PLJmIF2+tti7o3C/ts2g==}
engines: {node: '>=18'}
randombytes@2.1.0:
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
@ -4159,6 +4178,10 @@ packages:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
type-fest@4.41.0:
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
engines: {node: '>=16'}
typescript-eslint@8.50.0:
resolution: {integrity: sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -5868,6 +5891,15 @@ snapshots:
camelcase-css@2.0.1: {}
camelcase-keys@10.0.1:
dependencies:
camelcase: 8.0.0
map-obj: 5.0.2
quick-lru: 7.3.0
type-fest: 4.41.0
camelcase@8.0.0: {}
caniuse-lite@1.0.30001760: {}
chai@6.2.1: {}
@ -7221,6 +7253,8 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
map-obj@5.0.2: {}
markdown-it@14.1.0:
dependencies:
argparse: 2.0.1
@ -7818,6 +7852,8 @@ snapshots:
queue-microtask@1.2.3: {}
quick-lru@7.3.0: {}
randombytes@2.1.0:
dependencies:
safe-buffer: '@nolyfill/safe-buffer@1.0.44'
@ -8331,6 +8367,8 @@ snapshots:
dependencies:
prelude-ls: 1.2.1
type-fest@4.41.0: {}
typescript-eslint@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
dependencies:
'@typescript-eslint/eslint-plugin': 8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)

View File

@ -176,7 +176,7 @@ type WorkflowConfig struct {
Actions []project_model.WorkflowAction `json:"actions"`
Summary string `json:"summary"` // Human readable filter description
Enabled bool `json:"enabled"`
IsConfigured bool `json:"isConfigured"` // Whether this workflow is configured/saved
IsConfigured bool `json:"is_configured"` // Whether this workflow is configured/saved
}
func WorkflowsEvents(ctx *context.Context, project *project_model.Project) {
@ -447,7 +447,7 @@ func WorkflowsPost(ctx *context.Context) {
workflowSummary := project_service.GetWorkflowSummary(ctx, wf)
ctx.JSON(http.StatusOK, map[string]any{
"success": true,
"workflows": WorkflowConfig{
"workflow": WorkflowConfig{
ID: wf.ID,
EventID: strconv.FormatInt(wf.ID, 10),
DisplayName: string(ctx.Tr(wf.WorkflowEvent.LangKey())),

View File

@ -84,22 +84,22 @@ const setEditMode = (enabled: boolean) => {
const showCancelButton = computed(() => {
if (!store.selectedWorkflow) return false;
if (store.selectedWorkflow.id > 0) return true;
const eventId = store.selectedWorkflow.event_id ?? '';
const eventId = store.selectedWorkflow.eventId ?? '';
return typeof eventId === 'string' && eventId.startsWith('clone-');
});
const isTemporaryWorkflow = (workflow?: WorkflowEvent | null) => {
if (!workflow) return false;
if (workflow.id > 0) return false;
const eventId = typeof workflow.event_id === 'string' ? workflow.event_id : '';
const eventId = typeof workflow.eventId === 'string' ? workflow.eventId : '';
return eventId.startsWith('clone-') || eventId.startsWith('new-');
};
const removeTemporaryWorkflow = (workflow?: WorkflowEvent | null) => {
if (!workflow || !isTemporaryWorkflow(workflow)) return;
const eventId = workflow.event_id;
const tempIndex = store.workflowEvents.findIndex((w: WorkflowEvent) => w.event_id === eventId);
const eventId = workflow.eventId;
const tempIndex = store.workflowEvents.findIndex((w: WorkflowEvent) => w.eventId === eventId);
if (tempIndex >= 0) {
store.workflowEvents.splice(tempIndex, 1);
}
@ -125,20 +125,20 @@ const toggleEditMode = () => {
store.selectedItem = previousSelection.value.selectedItem;
store.selectedWorkflow = previousSelection.value.selectedWorkflow;
if (previousSelection.value.selectedWorkflow) {
store.loadWorkflowData(previousSelection.value.selectedWorkflow.event_id);
store.loadWorkflowData(previousSelection.value.selectedWorkflow.eventId);
}
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: WorkflowEvent) => {
if (!canceledWorkflow) return false;
const baseType = canceledWorkflow.workflow_event;
return baseType && (w.workflow_event === baseType || w.event_id === baseType);
const baseType = canceledWorkflow.workflowEvent;
return baseType && (w.workflowEvent === baseType || w.eventId === baseType);
}) || store.workflowEvents[0];
if (fallback) {
store.selectedItem = fallback.event_id;
store.selectedItem = fallback.eventId;
store.selectedWorkflow = fallback;
store.loadWorkflowData(fallback.event_id);
store.loadWorkflowData(fallback.eventId);
} else {
store.selectedItem = null;
store.selectedWorkflow = null;
@ -174,7 +174,7 @@ const deleteWorkflow = async () => {
// If deleting a temporary workflow (new or cloned, unsaved), just remove from list
if (currentSelection.id === 0) {
const tempIndex = store.workflowEvents.findIndex((w: WorkflowEvent) =>
w.event_id === currentSelection.event_id,
w.eventId === currentSelection.eventId,
);
if (tempIndex >= 0) {
store.workflowEvents.splice(tempIndex, 1);
@ -188,7 +188,7 @@ const deleteWorkflow = async () => {
// Find workflows for the same base event type
const sameEventWorkflows = store.workflowEvents.filter((w: WorkflowEvent) =>
(w.workflow_event === currentSelection.workflow_event)
(w.workflowEvent === currentSelection.workflowEvent)
);
let workflowToSelect: WorkflowListItem | null = null;
@ -231,18 +231,18 @@ const cloneWorkflow = (sourceWorkflow?: WorkflowEvent | null) => {
if (!sourceWorkflow) return;
// Generate a unique temporary ID for the cloned workflow
const tempId = `${sourceWorkflow.workflow_event}`;
const tempId = `${sourceWorkflow.workflowEvent}`;
// Extract base name without any parenthetical descriptions
const baseName = (sourceWorkflow.display_name || sourceWorkflow.workflow_event || sourceWorkflow.event_id)
const baseName = (sourceWorkflow.displayName || sourceWorkflow.workflowEvent || sourceWorkflow.eventId)
.replace(/\s*\([^)]*\)\s*/g, '');
// Create a new workflow object based on the source
const clonedWorkflow = {
id: 0, // New workflow
event_id: tempId,
display_name: `${baseName} (Copy)`,
workflow_event: sourceWorkflow.workflow_event,
eventId: tempId,
displayName: `${baseName} (Copy)`,
workflowEvent: sourceWorkflow.workflowEvent,
capabilities: sourceWorkflow.capabilities,
filters: JSON.parse(JSON.stringify(sourceWorkflow.filters || [])), // Deep clone
actions: JSON.parse(JSON.stringify(sourceWorkflow.actions || [])), // Deep clone
@ -251,7 +251,7 @@ const cloneWorkflow = (sourceWorkflow?: WorkflowEvent | null) => {
};
// Insert cloned workflow right after the source workflow (keep same type together)
const sourceIndex = store.workflowEvents.findIndex((w: WorkflowEvent) => w.event_id === sourceWorkflow.event_id);
const sourceIndex = store.workflowEvents.findIndex((w: WorkflowEvent) => w.eventId === sourceWorkflow.eventId);
if (sourceIndex >= 0) {
store.workflowEvents.splice(sourceIndex + 1, 0, clonedWorkflow);
} else {
@ -285,22 +285,22 @@ const selectWorkflowEvent = async (event: WorkflowEvent) => {
if (store.loading) return;
// If already selected, do nothing (keep selection active)
if (store.selectedItem === event.event_id) {
if (store.selectedItem === event.eventId) {
return;
}
try {
store.selectedItem = event.event_id;
store.selectedItem = event.eventId;
store.selectedWorkflow = event;
// Wait for DOM update before proceeding
await nextTick();
await store.loadWorkflowData(event.event_id);
await store.loadWorkflowData(event.eventId);
// Update URL without page reload
const newUrl = `${props.projectLink}/workflows/${event.event_id}`;
window.history.pushState({eventId: event.event_id}, '', newUrl);
const newUrl = `${props.projectLink}/workflows/${event.eventId}`;
window.history.pushState({eventId: event.eventId}, '', newUrl);
} catch (error) {
console.error('Error selecting workflow event:', error);
// On error, try to select the first available workflow instead of clearing
@ -322,7 +322,7 @@ const saveWorkflow = async () => {
const isWorkflowConfigured = (event: WorkflowEvent) => {
// Check if the event_id is a number (saved workflow ID) or if it has id > 0
return !Number.isNaN(parseInt(event.event_id)) || (event.id !== undefined && event.id > 0);
return !Number.isNaN(parseInt(event.eventId)) || (event.id !== undefined && event.id > 0);
};
// Get flat list of all workflows - use cached data to prevent frequent recomputation
@ -336,7 +336,7 @@ const workflowList = computed<WorkflowListItem[]>(() => {
return workflows.map((workflow: WorkflowEvent) => ({
...workflow,
isConfigured: isWorkflowConfigured(workflow),
display_name: workflow.display_name || workflow.workflow_event || workflow.event_id,
displayName: workflow.displayName || workflow.workflowEvent || workflow.eventId,
}));
});
@ -363,28 +363,28 @@ const selectWorkflowItem = async (item: WorkflowListItem) => {
} else {
// This is an unconfigured event - check if we already have a workflow object for it
const existingWorkflow = store.workflowEvents.find((w: WorkflowEvent) =>
w.id === 0 && w.workflow_event === item.workflow_event,
w.id === 0 && w.workflowEvent === item.workflowEvent,
);
const workflowToSelect = existingWorkflow || item;
await selectWorkflowEvent(workflowToSelect);
// Update URL for workflow
const newUrl = `${props.projectLink}/workflows/${item.workflow_event}`;
window.history.pushState({eventId: item.workflow_event}, '', newUrl);
const newUrl = `${props.projectLink}/workflows/${item.workflowEvent}`;
window.history.pushState({eventId: item.workflowEvent}, '', newUrl);
}
};
const hasAvailableFilters = computed(() => {
return (store.selectedWorkflow?.capabilities?.available_filters?.length ?? 0) > 0;
return (store.selectedWorkflow?.capabilities?.availableFilters?.length ?? 0) > 0;
});
const hasFilter = (filterType: any) => {
return store.selectedWorkflow?.capabilities?.available_filters?.includes(filterType);
return store.selectedWorkflow?.capabilities?.availableFilters?.includes(filterType);
};
const hasAction = (actionType: any) => {
return store.selectedWorkflow?.capabilities?.available_actions?.includes(actionType);
return store.selectedWorkflow?.capabilities?.availableActions?.includes(actionType);
};
// Toggle label selection for add_labels, remove_labels, or filter_labels
@ -392,8 +392,10 @@ const toggleLabel = (type: string, labelId: any) => {
let labels;
if (type === 'filter_labels') {
labels = store.workflowFilters.labels;
} else {
labels = (store.workflowActions as any)[type];
} else if (type === 'add_labels') {
labels = (store.workflowActions as any)['addLabels'];
} else if (type === 'remove_labels') {
labels = (store.workflowActions as any)['removeLabels'];
}
const index = labels.indexOf(labelId);
if (index > -1) {
@ -426,20 +428,20 @@ const isItemSelected = (item: WorkflowListItem) => {
if (item.isConfigured || item.id === 0) {
// For configured workflows or temporary workflows (new), match by event_id
return store.selectedItem === item.event_id;
return store.selectedItem === item.eventId;
}
// For unconfigured events, match by workflow_event
return store.selectedItem === item.workflow_event;
return store.selectedItem === item.workflowEvent;
};
// Get display name for workflow with numbering for same types
const getWorkflowDisplayName = (item: WorkflowListItem, _index: number) => {
const list = workflowList.value;
const displayName = item.display_name || item.workflow_event || item.event_id || '';
const displayName = item.displayName || item.workflowEvent || item.eventId || '';
// Find all workflows of the same type
const sameTypeWorkflows = list.filter((w: WorkflowListItem) =>
w.workflow_event === item.workflow_event &&
w.workflowEvent === item.workflowEvent &&
(w.isConfigured || w.id === 0) // Only count configured workflows
);
@ -449,7 +451,7 @@ const getWorkflowDisplayName = (item: WorkflowListItem, _index: number) => {
}
// Find the index of this workflow among same-type workflows
const sameTypeIndex = sameTypeWorkflows.findIndex((w: WorkflowListItem) => w.event_id === item.event_id);
const sameTypeIndex = sameTypeWorkflows.findIndex((w: WorkflowListItem) => w.eventId === item.eventId);
// Extract base name without filter summary (remove anything in parentheses)
const baseName = displayName.replace(/\s*\([^)]*\)\s*$/g, '');
@ -461,7 +463,7 @@ const getWorkflowDisplayName = (item: WorkflowListItem, _index: number) => {
const getCurrentDraftKey = () => {
if (!store.selectedWorkflow) return null;
return store.selectedWorkflow.event_id || store.selectedWorkflow.workflow_event;
return store.selectedWorkflow.eventId || store.selectedWorkflow.workflowEvent;
};
const persistDraftState = () => {
@ -533,7 +535,7 @@ onMounted(async () => {
// Auto-select logic
if (props.eventID) {
// If eventID is provided in URL, try to find and select it
const selectedEvent = store.workflowEvents.find((e: WorkflowEvent) => e.event_id === props.eventID);
const selectedEvent = store.workflowEvents.find((e: WorkflowEvent) => e.eventId === props.eventID);
if (selectedEvent) {
// Found existing configured workflow
store.selectedItem = props.eventID;
@ -543,7 +545,7 @@ onMounted(async () => {
// Check if eventID matches a base event type (unconfigured workflow)
const items = workflowList.value;
const matchingUnconfigured = items.find((item: WorkflowListItem) =>
!item.isConfigured && (item.workflow_event === props.eventID || item.event_id === props.eventID),
!item.isConfigured && (item.workflowEvent === props.eventID || item.eventId === props.eventID),
);
if (matchingUnconfigured) {
// Select the placeholder workflow for this base event type
@ -587,14 +589,14 @@ onMounted(async () => {
const popstateHandler = (e: PopStateEvent) => {
if (e.state?.eventId) {
// Handle browser back/forward navigation
const event = store.workflowEvents.find((ev: WorkflowEvent) => ev.event_id === e.state.eventId);
const event = store.workflowEvents.find((ev: WorkflowEvent) => ev.eventId === e.state.eventId);
if (event) {
void selectWorkflowEvent(event);
} else {
// Check if it's a base event type
const items = workflowList.value;
const matchingUnconfigured = items.find((item: WorkflowListItem) =>
!item.isConfigured && (item.workflow_event === e.state.eventId || item.event_id === e.state.eventId),
!item.isConfigured && (item.workflowEvent === e.state.eventId || item.eventId === e.state.eventId),
);
if (matchingUnconfigured) {
void selectWorkflowEvent(matchingUnconfigured);
@ -635,7 +637,7 @@ onUnmounted(() => {
<div class="workflow-items">
<div
v-for="(item, index) in workflowList"
:key="`workflow-${item.event_id}-${item.isConfigured ? 'configured' : 'unconfigured'}`"
:key="`workflow-${item.eventId}-${item.isConfigured ? 'configured' : 'unconfigured'}`"
class="workflow-item"
:class="{ active: isItemSelected(item) }"
:data-workflow-item="JSON.stringify(item)"
@ -678,7 +680,7 @@ onUnmounted(() => {
<div class="editor-title">
<h2>
<i class="settings icon"/>
{{ store.selectedWorkflow.display_name }}
{{ store.selectedWorkflow.displayName }}
<span
v-if="store.selectedWorkflow.id > 0 && !isInEditMode"
class="workflow-status"
@ -765,7 +767,7 @@ onUnmounted(() => {
<label>{{ locale.when }}</label>
<div class="segment">
<div class="description">
{{ locale.runWhen }}<strong>{{ store.selectedWorkflow.display_name }}</strong>
{{ locale.runWhen }}<strong>{{ store.selectedWorkflow.displayName }}</strong>
</div>
</div>
</div>
@ -779,15 +781,15 @@ onUnmounted(() => {
<select
v-if="isInEditMode"
class="column-select"
v-model="store.workflowFilters.issue_type"
v-model="store.workflowFilters.issueType"
>
<option value="">{{ locale.issuesAndPullRequests }}</option>
<option value="issue">{{ locale.issuesOnly }}</option>
<option value="pull_request">{{ locale.pullRequestsOnly }}</option>
</select>
<div v-else class="readonly-value">
{{ store.workflowFilters.issue_type === 'issue' ? locale.issuesOnly :
store.workflowFilters.issue_type === 'pull_request' ? locale.pullRequestsOnly :
{{ store.workflowFilters.issueType === 'issue' ? locale.issuesOnly :
store.workflowFilters.issueType === 'pull_request' ? locale.pullRequestsOnly :
locale.issuesAndPullRequests }}
</div>
</div>
@ -796,7 +798,7 @@ onUnmounted(() => {
<label>{{ locale.whenMovedFromColumn }}</label>
<select
v-if="isInEditMode"
v-model="store.workflowFilters.source_column"
v-model="store.workflowFilters.sourceColumn"
class="column-select"
>
<option value="">{{ locale.anyColumn }}</option>
@ -805,7 +807,7 @@ onUnmounted(() => {
</option>
</select>
<div v-else class="readonly-value">
{{ store.projectColumns.find(c => String(c.id) === store.workflowFilters.source_column)?.title || locale.anyColumn }}
{{ store.projectColumns.find(c => String(c.id) === store.workflowFilters.sourceColumn)?.title || locale.anyColumn }}
</div>
</div>
@ -813,7 +815,7 @@ onUnmounted(() => {
<label>{{ locale.whenMovedToColumn }}</label>
<select
v-if="isInEditMode"
v-model="store.workflowFilters.target_column"
v-model="store.workflowFilters.targetColumn"
class="column-select"
>
<option value="">{{ locale.anyColumn }}</option>
@ -822,7 +824,7 @@ onUnmounted(() => {
</option>
</select>
<div v-else class="readonly-value">
{{ store.projectColumns.find(c => String(c.id) === store.workflowFilters.target_column)?.title || locale.anyColumn }}
{{ store.projectColumns.find(c => String(c.id) === store.workflowFilters.targetColumn)?.title || locale.anyColumn }}
</div>
</div>
@ -894,13 +896,13 @@ onUnmounted(() => {
<div class="field" v-if="hasAction('add_labels')">
<label>{{ locale.addLabels }}</label>
<div v-if="isInEditMode" class="ui fluid multiple search selection dropdown label-dropdown">
<input type="hidden" :value="store.workflowActions.add_labels.join(',')">
<input type="hidden" :value="store.workflowActions.addLabels.join(',')">
<i class="dropdown icon"/>
<div class="text" :class="{ default: !store.workflowActions.add_labels?.length }">
<span v-if="!store.workflowActions.add_labels?.length">{{ locale.none }}</span>
<div class="text" :class="{ default: !store.workflowActions.addLabels?.length }">
<span v-if="!store.workflowActions.addLabels?.length">{{ locale.none }}</span>
<template v-else>
<span
v-for="labelId in store.workflowActions.add_labels" :key="labelId"
v-for="labelId in store.workflowActions.addLabels" :key="labelId"
class="ui label"
:style="`background-color: ${store.projectLabels.find(l => String(l.id) === labelId)?.color}; color: ${getLabelTextColor(store.projectLabels.find(l => String(l.id) === labelId)?.color)}`"
>
@ -913,7 +915,7 @@ onUnmounted(() => {
class="item" v-for="label in store.projectLabels" :key="label.id"
:data-value="String(label.id)"
@click.prevent="toggleLabel('add_labels', String(label.id))"
:class="{ active: store.workflowActions.add_labels.includes(String(label.id)), selected: store.workflowActions.add_labels.includes(String(label.id)) }"
:class="{ active: store.workflowActions.addLabels.includes(String(label.id)), selected: store.workflowActions.addLabels.includes(String(label.id)) }"
>
<span class="ui label" :style="`background-color: ${label.color}; color: ${getLabelTextColor(label.color)}`">
{{ label.name }}
@ -922,9 +924,9 @@ onUnmounted(() => {
</div>
</div>
<div v-else class="ui labels">
<span v-if="!store.workflowActions.add_labels?.length" class="text-muted">{{ locale.none }}</span>
<span v-if="!store.workflowActions.addLabels?.length" class="text-muted">{{ locale.none }}</span>
<span
v-for="labelId in store.workflowActions.add_labels" :key="labelId"
v-for="labelId in store.workflowActions.addLabels" :key="labelId"
class="ui label"
:style="`background-color: ${store.projectLabels.find(l => String(l.id) === labelId)?.color}; color: ${getLabelTextColor(store.projectLabels.find(l => String(l.id) === labelId)?.color)}`"
>
@ -936,13 +938,13 @@ onUnmounted(() => {
<div class="field" v-if="hasAction('remove_labels')">
<label>{{ locale.removeLabels }}</label>
<div v-if="isInEditMode" class="ui fluid multiple search selection dropdown label-dropdown">
<input type="hidden" :value="store.workflowActions.remove_labels.join(',')">
<input type="hidden" :value="store.workflowActions.removeLabels.join(',')">
<i class="dropdown icon"/>
<div class="text" :class="{ default: !store.workflowActions.remove_labels?.length }">
<span v-if="!store.workflowActions.remove_labels?.length">{{ locale.none }}</span>
<div class="text" :class="{ default: !store.workflowActions.removeLabels?.length }">
<span v-if="!store.workflowActions.removeLabels?.length">{{ locale.none }}</span>
<template v-else>
<span
v-for="labelId in store.workflowActions.remove_labels" :key="labelId"
v-for="labelId in store.workflowActions.removeLabels" :key="labelId"
class="ui label"
:style="`background-color: ${store.projectLabels.find(l => String(l.id) === labelId)?.color}; color: ${getLabelTextColor(store.projectLabels.find(l => String(l.id) === labelId)?.color)}`"
>
@ -955,7 +957,7 @@ onUnmounted(() => {
class="item" v-for="label in store.projectLabels" :key="label.id"
:data-value="String(label.id)"
@click.prevent="toggleLabel('remove_labels', String(label.id))"
:class="{ active: store.workflowActions.remove_labels.includes(String(label.id)), selected: store.workflowActions.remove_labels.includes(String(label.id)) }"
:class="{ active: store.workflowActions.removeLabels.includes(String(label.id)), selected: store.workflowActions.removeLabels.includes(String(label.id)) }"
>
<span class="ui label" :style="`background-color: ${label.color}; color: ${getLabelTextColor(label.color)}`">
{{ label.name }}
@ -964,9 +966,9 @@ onUnmounted(() => {
</div>
</div>
<div v-else class="ui labels">
<span v-if="!store.workflowActions.remove_labels?.length" class="text-muted">{{ locale.none }}</span>
<span v-if="!store.workflowActions.removeLabels?.length" class="text-muted">{{ locale.none }}</span>
<span
v-for="labelId in store.workflowActions.remove_labels" :key="labelId"
v-for="labelId in store.workflowActions.removeLabels" :key="labelId"
class="ui label"
:style="`background-color: ${store.projectLabels.find(l => String(l.id) === labelId)?.color}; color: ${getLabelTextColor(store.projectLabels.find(l => String(l.id) === labelId)?.color)}`"
>
@ -981,15 +983,15 @@ onUnmounted(() => {
v-if="isInEditMode"
id="issue-state-action"
class="column-select"
v-model="store.workflowActions.issue_state"
v-model="store.workflowActions.issueState"
>
<option value="">{{ locale.noChange }}</option>
<option value="close">{{ locale.closeIssue }}</option>
<option value="reopen">{{ locale.reopenIssue }}</option>
</select>
<div v-else class="readonly-value">
{{ store.workflowActions.issue_state === 'close' ? locale.closeIssue :
store.workflowActions.issue_state === 'reopen' ? locale.reopenIssue : locale.noChange }}
{{ store.workflowActions.issueState === 'close' ? locale.closeIssue :
store.workflowActions.issueState === 'reopen' ? locale.reopenIssue : locale.noChange }}
</div>
</div>
</div>

View File

@ -1,11 +1,12 @@
import {reactive} from 'vue';
import {GET, POST} from '../../modules/fetch.ts';
import {showErrorToast} from '../../modules/toast.ts';
import camelcaseKeys from 'camelcase-keys';
type WorkflowFilters = {
issue_type: string;
source_column: string;
target_column: string;
issueType: string;
sourceColumn: string;
targetColumn: string;
labels: string[];
};
@ -13,9 +14,9 @@ type WorkflowIssueStateAction = '' | 'close' | 'reopen';
type WorkflowActions = {
column: string;
add_labels: string[];
remove_labels: string[];
issue_state: WorkflowIssueStateAction;
addLabels: string[];
removeLabels: string[];
issueState: WorkflowIssueStateAction;
};
type WorkflowDraftState = {
@ -35,15 +36,15 @@ export type ProjectLabel = {
};
type WorkflowCapabilities = {
available_filters?: string[];
available_actions?: string[];
availableFilters?: string[];
availableActions?: string[];
};
export type WorkflowEvent = {
id: number;
event_id: string;
workflow_event?: string;
display_name?: string;
eventId: string;
workflowEvent?: string;
displayName?: string;
summary?: string;
enabled?: boolean;
capabilities?: WorkflowCapabilities;
@ -79,19 +80,29 @@ type WorkflowStoreState = {
deleteWorkflow(): Promise<void>;
};
const createDefaultFilters = (): WorkflowFilters => ({issue_type: '', source_column: '', target_column: '', labels: []});
const createDefaultActions = (): WorkflowActions => ({column: '', add_labels: [], remove_labels: [], issue_state: ''});
const createDefaultFilters = (): WorkflowFilters => ({issueType: '', sourceColumn: '', targetColumn: '', labels: []});
const createDefaultActions = (): WorkflowActions => ({column: '', addLabels: [], removeLabels: [], issueState: ''});
const camelToSnake = (key: string): string => key.replace(/([A-Z])/g, '_$1').toLowerCase();
function convertKeysToSnakeCase<T extends Record<string, unknown>>(obj: T): Record<string, unknown> {
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) {
result[camelToSnake(key)] = value;
}
return result;
}
function convertFilters(workflow: any): WorkflowFilters {
const filters = createDefaultFilters();
if (workflow?.filters && Array.isArray(workflow.filters)) {
for (const filter of workflow.filters) {
if (filter.type === 'issue_type') {
filters.issue_type = filter.value;
filters.issueType = filter.value;
} else if (filter.type === 'source_column') {
filters.source_column = filter.value;
filters.sourceColumn = filter.value;
} else if (filter.type === 'target_column') {
filters.target_column = filter.value;
filters.targetColumn = filter.value;
} else if (filter.type === 'labels') {
filters.labels.push(filter.value);
}
@ -110,12 +121,12 @@ function convertActions(workflow: any): WorkflowActions {
actions.column = action.value;
} else if (action.type === 'add_labels') {
// Backend returns string, keep as string to match label.id type
actions.add_labels.push(action.value);
actions.addLabels.push(action.value);
} else if (action.type === 'remove_labels') {
// Backend returns string, keep as string to match label.id type
actions.remove_labels.push(action.value);
actions.removeLabels.push(action.value);
} else if (action.type === 'issue_state') {
actions.issue_state = action.value as WorkflowIssueStateAction;
actions.issueState = action.value as WorkflowIssueStateAction;
}
}
}
@ -123,17 +134,17 @@ function convertActions(workflow: any): WorkflowActions {
}
const cloneFilters = (filters: WorkflowFilters): WorkflowFilters => ({
issue_type: filters.issue_type,
source_column: filters.source_column,
target_column: filters.target_column,
issueType: filters.issueType,
sourceColumn: filters.sourceColumn,
targetColumn: filters.targetColumn,
labels: Array.from(filters.labels),
});
const cloneActions = (actions: WorkflowActions): WorkflowActions => ({
column: actions.column,
add_labels: Array.from(actions.add_labels),
remove_labels: Array.from(actions.remove_labels),
issue_state: actions.issue_state,
addLabels: Array.from(actions.addLabels),
removeLabels: Array.from(actions.removeLabels),
issueState: actions.issueState,
});
export function createWorkflowStore(props: any): WorkflowStoreState {
@ -170,7 +181,8 @@ export function createWorkflowStore(props: any): WorkflowStoreState {
async loadEvents(): Promise<WorkflowEvent[]> {
const response = await GET(`${props.projectLink}/workflows/events`);
store.workflowEvents = await response.json() as WorkflowEvent[];
const data = await response.json();
store.workflowEvents = camelcaseKeys(data, {deep: true}) as WorkflowEvent[];
return store.workflowEvents;
},
@ -199,7 +211,7 @@ export function createWorkflowStore(props: any): WorkflowStoreState {
}
// Find the workflow from existing workflowEvents
const workflow = store.workflowEvents.find((e: WorkflowEvent) => e.event_id === eventId);
const workflow = store.workflowEvents.find((e: WorkflowEvent) => e.eventId === eventId);
store.workflowFilters = convertFilters(workflow);
store.workflowActions = convertActions(workflow);
@ -223,7 +235,7 @@ export function createWorkflowStore(props: any): WorkflowStoreState {
store.workflowFilters = createDefaultFilters();
store.workflowActions = createDefaultActions();
const currentEventId = store.selectedWorkflow?.event_id;
const currentEventId = store.selectedWorkflow?.eventId;
if (currentEventId) {
store.updateDraft(currentEventId, store.workflowFilters, store.workflowActions);
}
@ -235,9 +247,9 @@ export function createWorkflowStore(props: any): WorkflowStoreState {
// Validate: at least one action must be configured
const hasAtLeastOneAction = Boolean(
store.workflowActions.column ||
store.workflowActions.add_labels.length > 0 ||
store.workflowActions.remove_labels.length > 0 ||
store.workflowActions.issue_state,
store.workflowActions.addLabels.length > 0 ||
store.workflowActions.removeLabels.length > 0 ||
store.workflowActions.issueState,
);
if (!hasAtLeastOneAction) {
@ -248,13 +260,13 @@ export function createWorkflowStore(props: any): WorkflowStoreState {
store.saving = true;
try {
// For new workflows, use the base event type
const eventId = store.selectedWorkflow.event_id;
const eventId = store.selectedWorkflow.eventId;
// Convert frontend data format to backend JSON format
const postData = {
event_id: eventId,
filters: store.workflowFilters,
actions: store.workflowActions,
filters: convertKeysToSnakeCase(store.workflowFilters),
actions: convertKeysToSnakeCase(store.workflowActions),
};
const response = await POST(`${props.projectLink}/workflows/${eventId}`, {
@ -280,45 +292,46 @@ export function createWorkflowStore(props: any): WorkflowStoreState {
return;
}
const result = await response.json();
const data = await response.json();
const result = camelcaseKeys(data, {deep: true});
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 eventKey = typeof store.selectedWorkflow.eventId === 'string' ? store.selectedWorkflow.eventId : '';
const wasNewWorkflow = store.selectedWorkflow.id === 0 ||
eventKey.startsWith('new-') ||
eventKey.startsWith('clone-');
if (wasNewWorkflow && store.selectedWorkflow.workflow_event) {
store.clearDraft(store.selectedWorkflow.workflow_event);
if (wasNewWorkflow && store.selectedWorkflow.workflowEvent) {
store.clearDraft(store.selectedWorkflow.workflowEvent);
}
// 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: WorkflowEvent) => w.event_id === result.workflow.event_id);
const reloadedWorkflow = store.workflowEvents.find((w: WorkflowEvent) => w.eventId === result.workflow.eventId);
if (reloadedWorkflow) {
// Use the reloaded workflow as it has all the necessary fields
store.selectedWorkflow = reloadedWorkflow;
store.selectedItem = reloadedWorkflow.event_id;
store.selectedItem = reloadedWorkflow.eventId;
} else {
// Fallback: use the result from backend (shouldn't normally happen)
store.selectedWorkflow = result.workflow;
store.selectedItem = result.workflow.event_id;
store.selectedItem = result.workflow.eventId;
}
store.workflowFilters = convertFilters(store.selectedWorkflow);
store.workflowActions = convertActions(store.selectedWorkflow);
if (store.selectedWorkflow?.event_id) {
store.updateDraft(store.selectedWorkflow.event_id, store.workflowFilters, store.workflowActions);
if (store.selectedWorkflow?.eventId) {
store.updateDraft(store.selectedWorkflow.eventId, store.workflowFilters, store.workflowActions);
}
// Update URL to use the new workflow ID
if (wasNewWorkflow && store.selectedWorkflow?.event_id) {
const newUrl = `${props.projectLink}/workflows/${store.selectedWorkflow.event_id}`;
window.history.replaceState({eventId: store.selectedWorkflow.event_id}, '', newUrl);
if (wasNewWorkflow && store.selectedWorkflow?.eventId) {
const newUrl = `${props.projectLink}/workflows/${store.selectedWorkflow.eventId}`;
window.history.replaceState({eventId: store.selectedWorkflow.eventId}, '', newUrl);
}
} else {
console.error('Unexpected response format:', result);
@ -361,7 +374,7 @@ export function createWorkflowStore(props: any): WorkflowStoreState {
const result = await response.json();
if (result.success) {
// Update workflow in the list
const existingIndex = store.workflowEvents.findIndex((e: WorkflowEvent) => e.event_id === selected.event_id);
const existingIndex = store.workflowEvents.findIndex((e: WorkflowEvent) => e.eventId === selected.eventId);
if (existingIndex >= 0) {
store.workflowEvents[existingIndex].enabled = desiredEnabled;
}
@ -399,7 +412,7 @@ export function createWorkflowStore(props: any): WorkflowStoreState {
const result = await response.json();
if (result.success) {
// Remove workflow from the list
const existingIndex = store.workflowEvents.findIndex((e: WorkflowEvent) => e.event_id === selected.event_id);
const existingIndex = store.workflowEvents.findIndex((e: WorkflowEvent) => e.eventId === selected.eventId);
if (existingIndex >= 0) {
store.workflowEvents.splice(existingIndex, 1);
}