mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-17 15:17:40 +02:00
185 lines
6.2 KiB
TypeScript
185 lines
6.2 KiB
TypeScript
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
|
import { API_BASE } from '@/shared/config';
|
|
|
|
const DASHBOARDS_API = `${API_BASE}/dashboards`;
|
|
|
|
export interface Dashboard {
|
|
id: string;
|
|
name: string;
|
|
auto_named: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
thumbnail?: string | null;
|
|
/** Bumped only on a real screenshot save; sidebar/grids sort by this so opening a dashboard doesn't reorder it. */
|
|
preview_updated_at?: string | null;
|
|
/** Sorted card-id set at last screenshot; compared on exit to decide if cards changed. */
|
|
preview_signature?: string | null;
|
|
}
|
|
|
|
interface DashboardsState {
|
|
items: Record<string, Dashboard>;
|
|
loading: boolean;
|
|
}
|
|
|
|
const initialState: DashboardsState = {
|
|
items: {},
|
|
loading: false,
|
|
};
|
|
|
|
export const fetchDashboards = createAsyncThunk('dashboards/fetchAll', async () => {
|
|
const res = await fetch(`${DASHBOARDS_API}/list`);
|
|
const data = await res.json();
|
|
return data.dashboards as Dashboard[];
|
|
});
|
|
|
|
export const createDashboard = createAsyncThunk(
|
|
'dashboards/create',
|
|
async (name: string) => {
|
|
const res = await fetch(`${DASHBOARDS_API}/create`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name }),
|
|
});
|
|
return (await res.json()) as Dashboard;
|
|
},
|
|
);
|
|
|
|
export const renameDashboard = createAsyncThunk(
|
|
'dashboards/rename',
|
|
async ({ id, name }: { id: string; name: string; previousName?: string }) => {
|
|
const res = await fetch(`${DASHBOARDS_API}/${id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name }),
|
|
});
|
|
if (!res.ok) throw new Error(`rename failed: ${res.status}`);
|
|
return (await res.json()) as Dashboard;
|
|
},
|
|
);
|
|
|
|
export const deleteDashboard = createAsyncThunk(
|
|
'dashboards/delete',
|
|
async (id: string) => {
|
|
await fetch(`${DASHBOARDS_API}/${id}`, { method: 'DELETE' });
|
|
return id;
|
|
},
|
|
);
|
|
|
|
export const duplicateDashboard = createAsyncThunk(
|
|
'dashboards/duplicate',
|
|
async (id: string) => {
|
|
const res = await fetch(`${DASHBOARDS_API}/${id}/duplicate`, { method: 'POST' });
|
|
return (await res.json()) as Dashboard;
|
|
},
|
|
);
|
|
|
|
export const updateDashboardThumbnail = createAsyncThunk(
|
|
'dashboards/updateThumbnail',
|
|
// `signature` is the card-id set the shot was taken at; '' thumbnail clears the preview (empty dashboard).
|
|
async ({ id, thumbnail, signature }: { id: string; thumbnail: string; signature: string }) => {
|
|
const res = await fetch(`${DASHBOARDS_API}/${id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ thumbnail, preview_signature: signature }),
|
|
});
|
|
if (!res.ok) throw new Error(`Thumbnail update failed: ${res.status}`);
|
|
const data = await res.json();
|
|
return {
|
|
id,
|
|
thumbnail: data.thumbnail as string | null,
|
|
updated_at: data.updated_at as string,
|
|
preview_updated_at: data.preview_updated_at as string | null,
|
|
preview_signature: data.preview_signature as string | null,
|
|
};
|
|
},
|
|
);
|
|
|
|
export const generateDashboardName = createAsyncThunk(
|
|
'dashboards/generateName',
|
|
async (dashboardId: string) => {
|
|
const res = await fetch(`${DASHBOARDS_API}/${dashboardId}/generate-name`, {
|
|
method: 'POST',
|
|
});
|
|
const data = await res.json();
|
|
return { id: dashboardId, name: data.name as string, auto_named: data.auto_named as boolean };
|
|
},
|
|
);
|
|
|
|
const dashboardsSlice = createSlice({
|
|
name: 'dashboards',
|
|
initialState,
|
|
reducers: {},
|
|
extraReducers: (builder) => {
|
|
builder
|
|
.addCase(fetchDashboards.pending, (state) => {
|
|
state.loading = true;
|
|
})
|
|
.addCase(fetchDashboards.fulfilled, (state, action) => {
|
|
state.loading = false;
|
|
// This replaces the whole list, so a wrong-shaped answer must not be read as "you have no
|
|
// dashboards" any more than it should throw inside immer. Keep what we had.
|
|
if (!Array.isArray(action.payload)) return;
|
|
const items: Record<string, Dashboard> = {};
|
|
for (const d of action.payload) {
|
|
items[d.id] = d;
|
|
}
|
|
state.items = items;
|
|
})
|
|
.addCase(fetchDashboards.rejected, (state) => {
|
|
state.loading = false;
|
|
})
|
|
.addCase(createDashboard.fulfilled, (state, action) => {
|
|
state.items[action.payload.id] = action.payload;
|
|
})
|
|
// Optimistic rename: swap label on dispatch; .rejected rolls back to previousName.
|
|
.addCase(renameDashboard.pending, (state, action) => {
|
|
const { id, name } = action.meta.arg;
|
|
if (state.items[id]) {
|
|
state.items[id].name = name;
|
|
state.items[id].auto_named = false;
|
|
}
|
|
})
|
|
.addCase(renameDashboard.fulfilled, (state, action) => {
|
|
const d = action.payload;
|
|
if (state.items[d.id]) {
|
|
state.items[d.id] = {
|
|
...state.items[d.id],
|
|
name: d.name,
|
|
auto_named: d.auto_named ?? false,
|
|
updated_at: d.updated_at,
|
|
};
|
|
}
|
|
})
|
|
.addCase(renameDashboard.rejected, (state, action) => {
|
|
const { id, previousName } = action.meta.arg;
|
|
if (state.items[id] && previousName !== undefined) {
|
|
state.items[id].name = previousName;
|
|
}
|
|
})
|
|
.addCase(deleteDashboard.fulfilled, (state, action) => {
|
|
delete state.items[action.payload];
|
|
})
|
|
.addCase(duplicateDashboard.fulfilled, (state, action) => {
|
|
state.items[action.payload.id] = action.payload;
|
|
})
|
|
.addCase(generateDashboardName.fulfilled, (state, action) => {
|
|
const { id, name, auto_named } = action.payload;
|
|
if (state.items[id]) {
|
|
state.items[id].name = name;
|
|
state.items[id].auto_named = auto_named;
|
|
}
|
|
})
|
|
.addCase(updateDashboardThumbnail.fulfilled, (state, action) => {
|
|
const { id, thumbnail, updated_at, preview_updated_at, preview_signature } = action.payload;
|
|
if (state.items[id]) {
|
|
state.items[id].thumbnail = thumbnail;
|
|
state.items[id].updated_at = updated_at;
|
|
state.items[id].preview_updated_at = preview_updated_at;
|
|
state.items[id].preview_signature = preview_signature;
|
|
}
|
|
});
|
|
},
|
|
});
|
|
|
|
export default dashboardsSlice.reducer;
|