[eric] workflows: choose where a schedule runs, next to the schedule, with the reason it cannot go up

This commit is contained in:
ciregenz
2026-07-31 23:39:45 -07:00
parent a828496c15
commit f8f043dea1
8 changed files with 587 additions and 7 deletions
+20 -3
View File
@@ -74,6 +74,9 @@ export interface Workflow {
steps: WorkflowStep[];
actions: ActionsConfig;
schedule: ScheduleConfig;
/** Where a SCHEDULED fire runs. Only the cloud_workflows routes may change it, and only once
* the cloud has actually taken (or released) the workflow. Manual runs are always local. */
execution_target?: 'device' | 'cloud';
permissions: PermissionTier[];
source_session_id?: string | null;
dashboard_id?: string | null;
@@ -200,12 +203,14 @@ interface State {
allRuns: WorkflowRun[];
allRunsLoading: boolean;
runningToast: RunningToast | null;
/** One-off explanation for something the user asked for that the server refused. */
noticeToast: string | null;
runControlPending: Record<string, WorkflowRunControlAction>;
deleted: Workflow[];
deletedLoading: boolean;
}
const initialState: State = { items: {}, runs: {}, openCards: {}, loaded: false, loading: false, paused: false, active: [], cloudSmsEnabled: false, allRuns: [], allRunsLoading: false, runningToast: null, runControlPending: {}, deleted: [], deletedLoading: false };
const initialState: State = { items: {}, runs: {}, openCards: {}, loaded: false, loading: false, paused: false, active: [], cloudSmsEnabled: false, allRuns: [], allRunsLoading: false, runningToast: null, noticeToast: null, runControlPending: {}, deleted: [], deletedLoading: false };
function mergeRunIntoState(state: State, r: WorkflowRun) {
const arr = state.runs[r.workflow_id] || [];
@@ -402,8 +407,13 @@ export const discardDraft = createAsyncThunk('workflows/discardDraft', async (id
return (await res.json()) as Workflow;
});
export const deleteWorkflow = createAsyncThunk('workflows/delete', async (id: string) => {
await fetch(`${API}/${id}`, { method: 'DELETE' });
export const deleteWorkflow = createAsyncThunk('workflows/delete', async (id: string, { rejectWithValue }) => {
const res = await fetch(`${API}/${id}`, { method: 'DELETE' });
// A refused delete must not remove the card: the workflow is still there, and if it is cloud-hosted it is still running.
if (!res.ok) {
const body = await res.json().catch(() => null);
return rejectWithValue(typeof body?.detail === 'string' ? body.detail : "Couldn't delete this workflow. Try again in a moment.");
}
return id;
});
@@ -570,6 +580,9 @@ const slice = createSlice({
dismissRunningToast(state) {
state.runningToast = null;
},
dismissNoticeToast(state) {
state.noticeToast = null;
},
},
extraReducers: (builder) => {
builder
@@ -591,6 +604,9 @@ const slice = createSlice({
.addCase(updateWorkflow.fulfilled, (state, action) => { state.items[action.payload.id] = action.payload; })
.addCase(commitDraft.fulfilled, (state, action) => { state.items[action.payload.id] = action.payload; })
.addCase(discardDraft.fulfilled, (state, action) => { state.items[action.payload.id] = action.payload; })
.addCase(deleteWorkflow.rejected, (state, action) => {
state.noticeToast = typeof action.payload === 'string' ? action.payload : "Couldn't delete this workflow. Try again in a moment.";
})
.addCase(deleteWorkflow.fulfilled, (state, action) => {
delete state.items[action.payload];
delete state.runs[action.payload];
@@ -670,5 +686,6 @@ export const {
upsertWorkflow,
removeWorkflow,
dismissRunningToast,
dismissNoticeToast,
} = slice.actions;
export default slice.reducer;