mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 12:17:45 +02:00
[hAIk]: refactor: wire dashboardsSlice through backend-bridge, remove inline fetch calls from slice and update all 6 consumer files
This commit is contained in:
@@ -8,7 +8,7 @@ import RestartAltIcon from '@mui/icons-material/RestartAlt';
|
||||
import SystemUpdateAltIcon from '@mui/icons-material/SystemUpdateAlt';
|
||||
import Settings from '@/app/pages/Settings/Settings';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { fetchDashboards } from '@/shared/state/dashboardsSlice';
|
||||
import { LIST_DASHBOARDS } from '@/shared/backend-bridge/apps/dashboards';
|
||||
import { fetchOutputs } from '@/shared/state/outputsSlice';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { useUpdateNotification } from './hooks/useUpdateNotification';
|
||||
@@ -40,7 +40,7 @@ const AppShell: React.FC = () => {
|
||||
useUrlInterception(dashboardList);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchDashboards());
|
||||
dispatch(LIST_DASHBOARDS());
|
||||
dispatch(fetchOutputs());
|
||||
}, [dispatch]);
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import AddIcon from '@mui/icons-material/Add';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import ExtensionIcon from '@mui/icons-material/Extension';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { createDashboard, renameDashboard } from '@/shared/state/dashboardsSlice';
|
||||
import { CREATE_DASHBOARD, UPDATE_DASHBOARD } from '@/shared/backend-bridge/apps/dashboards';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
|
||||
const CUSTOMIZATION_ITEMS = [
|
||||
@@ -65,13 +65,13 @@ const Sidebar: React.FC<SidebarProps> = ({ showUpdateDot }) => {
|
||||
const handleStartRename = (id: string, name: string) => { setRenamingId(id); setRenameValue(name); };
|
||||
const handleRenameSubmit = (id: string) => {
|
||||
const t = renameValue.trim();
|
||||
if (t && t !== dashboardItems[id]?.name) dispatch(renameDashboard({ id, name: t }));
|
||||
if (t && t !== dashboardItems[id]?.name) dispatch(UPDATE_DASHBOARD({ dashboardId: id, name: t }));
|
||||
setRenamingId(null);
|
||||
};
|
||||
const handleCreateDash = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
const r = await dispatch(createDashboard('Untitled Dashboard'));
|
||||
if (createDashboard.fulfilled.match(r)) navigate(`/dashboard/${r.payload.id}`);
|
||||
const r = await dispatch(CREATE_DASHBOARD('Untitled Dashboard'));
|
||||
if (CREATE_DASHBOARD.fulfilled.match(r)) navigate(`/dashboard/${r.payload.id}`);
|
||||
};
|
||||
const handleAppsClick = () => {
|
||||
if (isAppsRoute && location.pathname === '/apps') setAppsExpanded((p) => !p);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useAppDispatch } from '@/shared/hooks';
|
||||
import { createDashboard } from '@/shared/state/dashboardsSlice';
|
||||
import { CREATE_DASHBOARD } from '@/shared/backend-bridge/apps/dashboards';
|
||||
import { addBrowserCard, addBrowserTab } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { setPendingBrowserUrl } from '@/shared/state/tempStateSlice';
|
||||
import { findBrowserByWebContentsId } from '@/shared/browserRegistry';
|
||||
@@ -30,8 +30,8 @@ export function useUrlInterception(dashboardList: { id: string }[]) {
|
||||
if (targetId) {
|
||||
navigate(`/dashboard/${targetId}`);
|
||||
} else {
|
||||
dispatch(createDashboard('Untitled Dashboard')).then((result: any) => {
|
||||
if (createDashboard.fulfilled.match(result)) {
|
||||
dispatch(CREATE_DASHBOARD('Untitled Dashboard')).then((result: any) => {
|
||||
if (CREATE_DASHBOARD.fulfilled.match(result)) {
|
||||
navigate(`/dashboard/${result.payload.id}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useCallback, useRef } from 'react';
|
||||
import type { RefObject } from 'react';
|
||||
import { store } from '@/shared/state/store';
|
||||
import { updateDashboardThumbnail } from '@/shared/state/dashboardsSlice';
|
||||
import { UPDATE_DASHBOARD } from '@/shared/backend-bridge/apps/dashboards';
|
||||
import { captureDashboardThumbnail } from '../captureDashboardThumbnail';
|
||||
|
||||
export function useDashboardThumbnail(
|
||||
@@ -45,7 +45,7 @@ export function useDashboardThumbnail(
|
||||
return () => {
|
||||
const thumbnail = pendingThumbnailRef.current;
|
||||
if (thumbnail) {
|
||||
store.dispatch(updateDashboardThumbnail({ id: exitingId, thumbnail }));
|
||||
store.dispatch(UPDATE_DASHBOARD({ dashboardId: exitingId, thumbnail }));
|
||||
pendingThumbnailRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
ViewCardPosition,
|
||||
BrowserCardPosition
|
||||
} from '@/shared/state/dashboardLayoutSlice';
|
||||
import { generateDashboardName } from '@/shared/state/dashboardsSlice';
|
||||
import { GENERATE_DASHBOARD_NAME } from '@/shared/backend-bridge/apps/dashboards';
|
||||
import type { ContextPath } from '@/app/components/DirectoryBrowser';
|
||||
import type { CanvasActions } from '../useCanvasControls';
|
||||
|
||||
@@ -150,7 +150,7 @@ export function useToolbarActions(deps: ToolbarDeps) {
|
||||
currentDash &&
|
||||
(currentDash.auto_named || currentDash.name === 'Untitled Dashboard');
|
||||
if (NAME_GEN_TRIGGERS.includes(agentCount) && canAutoName) {
|
||||
dispatch(generateDashboardName(dashboardId));
|
||||
dispatch(GENERATE_DASHBOARD_NAME(dashboardId));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -15,13 +15,13 @@ import EditIcon from '@mui/icons-material/Edit';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import {
|
||||
fetchDashboards,
|
||||
createDashboard,
|
||||
deleteDashboard,
|
||||
duplicateDashboard,
|
||||
renameDashboard,
|
||||
Dashboard,
|
||||
} from '@/shared/state/dashboardsSlice';
|
||||
LIST_DASHBOARDS,
|
||||
CREATE_DASHBOARD,
|
||||
DELETE_DASHBOARD,
|
||||
DUPLICATE_DASHBOARD,
|
||||
UPDATE_DASHBOARD,
|
||||
} from '@/shared/backend-bridge/apps/dashboards';
|
||||
import type { Dashboard } from '@/shared/state/dashboardsSlice';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import DashboardCard from './DashboardCard';
|
||||
|
||||
@@ -39,7 +39,7 @@ const DashboardSelection: React.FC = () => {
|
||||
const [renameValue, setRenameValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchDashboards());
|
||||
dispatch(LIST_DASHBOARDS());
|
||||
}, [dispatch]);
|
||||
|
||||
const dashboards = useMemo(() => {
|
||||
@@ -52,8 +52,8 @@ const DashboardSelection: React.FC = () => {
|
||||
}, [items, search]);
|
||||
|
||||
const handleCreate = async () => {
|
||||
const result = await dispatch(createDashboard('Untitled Dashboard'));
|
||||
if (createDashboard.fulfilled.match(result)) {
|
||||
const result = await dispatch(CREATE_DASHBOARD('Untitled Dashboard'));
|
||||
if (CREATE_DASHBOARD.fulfilled.match(result)) {
|
||||
navigate(`/dashboard/${result.payload.id}`);
|
||||
}
|
||||
};
|
||||
@@ -70,12 +70,12 @@ const DashboardSelection: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (menuDashboard) dispatch(deleteDashboard(menuDashboard.id));
|
||||
if (menuDashboard) dispatch(DELETE_DASHBOARD(menuDashboard.id));
|
||||
handleCloseMenu();
|
||||
};
|
||||
|
||||
const handleDuplicate = () => {
|
||||
if (menuDashboard) dispatch(duplicateDashboard(menuDashboard.id));
|
||||
if (menuDashboard) dispatch(DUPLICATE_DASHBOARD(menuDashboard.id));
|
||||
handleCloseMenu();
|
||||
};
|
||||
|
||||
@@ -93,7 +93,7 @@ const DashboardSelection: React.FC = () => {
|
||||
const handleRenameSubmit = (id: string) => {
|
||||
const trimmed = renameValue.trim();
|
||||
if (trimmed && trimmed !== items[id]?.name) {
|
||||
dispatch(renameDashboard({ id, name: trimmed }));
|
||||
dispatch(UPDATE_DASHBOARD({ dashboardId: id, name: trimmed }));
|
||||
}
|
||||
setRenamingId(null);
|
||||
};
|
||||
|
||||
@@ -3,19 +3,28 @@ import { API_BASE } from '@/shared/backend-bridge/base_routes';
|
||||
|
||||
const DASHBOARDS_API: string = `${API_BASE}/dashboards`;
|
||||
|
||||
export interface Dashboard {
|
||||
id: string;
|
||||
name: string;
|
||||
auto_named: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
thumbnail?: string | null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dashboard CRUD
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
const list_dashboards_endpoint: string = `${DASHBOARDS_API}/list`;
|
||||
async function list_dashboards_function(): Promise<Record<string, unknown>[]> {
|
||||
async function list_dashboards_function(): Promise<Dashboard[]> {
|
||||
const res = await fetch(list_dashboards_endpoint, {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
const data = await res.json();
|
||||
return data.dashboards as Record<string, unknown>[];
|
||||
return data.dashboards as Dashboard[];
|
||||
}
|
||||
export const LIST_DASHBOARDS = createAsyncThunk(
|
||||
list_dashboards_endpoint,
|
||||
@@ -24,14 +33,14 @@ export const LIST_DASHBOARDS = createAsyncThunk(
|
||||
|
||||
|
||||
const create_dashboard_endpoint: string = `${DASHBOARDS_API}/create`;
|
||||
async function create_dashboard_function(name: string = 'Untitled Dashboard'): Promise<Record<string, unknown>> {
|
||||
async function create_dashboard_function(name: string = 'Untitled Dashboard'): Promise<Dashboard> {
|
||||
const res = await fetch(create_dashboard_endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
const data = await res.json();
|
||||
return data as Record<string, unknown>;
|
||||
return data as Dashboard;
|
||||
}
|
||||
export const CREATE_DASHBOARD = createAsyncThunk(
|
||||
create_dashboard_endpoint,
|
||||
@@ -40,13 +49,13 @@ export const CREATE_DASHBOARD = createAsyncThunk(
|
||||
|
||||
|
||||
const generate_dashboard_name_endpoint: string = `${DASHBOARDS_API}/generate-name`;
|
||||
async function generate_dashboard_name_function(dashboardId: string): Promise<{ name: string; auto_named: boolean }> {
|
||||
async function generate_dashboard_name_function(dashboardId: string): Promise<{ id: string; name: string; auto_named: boolean }> {
|
||||
const res = await fetch(`${DASHBOARDS_API}/${dashboardId}/generate-name`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
const data = await res.json();
|
||||
return data as { name: string; auto_named: boolean };
|
||||
return { id: dashboardId, name: data.name as string, auto_named: data.auto_named as boolean };
|
||||
}
|
||||
export const GENERATE_DASHBOARD_NAME = createAsyncThunk(
|
||||
generate_dashboard_name_endpoint,
|
||||
@@ -55,13 +64,13 @@ export const GENERATE_DASHBOARD_NAME = createAsyncThunk(
|
||||
|
||||
|
||||
const get_dashboard_endpoint: string = `${DASHBOARDS_API}/get`;
|
||||
async function get_dashboard_function(dashboardId: string): Promise<Record<string, unknown>> {
|
||||
async function get_dashboard_function(dashboardId: string): Promise<Dashboard> {
|
||||
const res = await fetch(`${DASHBOARDS_API}/${dashboardId}`, {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
const data = await res.json();
|
||||
return data as Record<string, unknown>;
|
||||
return data as Dashboard;
|
||||
}
|
||||
export const GET_DASHBOARD = createAsyncThunk(
|
||||
get_dashboard_endpoint,
|
||||
@@ -75,15 +84,16 @@ async function update_dashboard_function(args: {
|
||||
name?: string;
|
||||
layout?: Record<string, unknown>;
|
||||
thumbnail?: string;
|
||||
}): Promise<Record<string, unknown>> {
|
||||
}): Promise<Dashboard> {
|
||||
const { dashboardId, ...updates } = args;
|
||||
const res = await fetch(`${DASHBOARDS_API}/${dashboardId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(updates),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Dashboard update failed: ${res.status}`);
|
||||
const data = await res.json();
|
||||
return data as Record<string, unknown>;
|
||||
return data as Dashboard;
|
||||
}
|
||||
export const UPDATE_DASHBOARD = createAsyncThunk(
|
||||
update_dashboard_endpoint,
|
||||
@@ -92,13 +102,13 @@ export const UPDATE_DASHBOARD = createAsyncThunk(
|
||||
|
||||
|
||||
const delete_dashboard_endpoint: string = `${DASHBOARDS_API}/delete`;
|
||||
async function delete_dashboard_function(dashboardId: string): Promise<{ ok: boolean }> {
|
||||
async function delete_dashboard_function(dashboardId: string): Promise<string> {
|
||||
const res = await fetch(`${DASHBOARDS_API}/${dashboardId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
const data = await res.json();
|
||||
return data as { ok: boolean };
|
||||
await res.json();
|
||||
return dashboardId;
|
||||
}
|
||||
export const DELETE_DASHBOARD = createAsyncThunk(
|
||||
delete_dashboard_endpoint,
|
||||
@@ -107,13 +117,13 @@ export const DELETE_DASHBOARD = createAsyncThunk(
|
||||
|
||||
|
||||
const duplicate_dashboard_endpoint: string = `${DASHBOARDS_API}/duplicate`;
|
||||
async function duplicate_dashboard_function(dashboardId: string): Promise<Record<string, unknown>> {
|
||||
async function duplicate_dashboard_function(dashboardId: string): Promise<Dashboard> {
|
||||
const res = await fetch(`${DASHBOARDS_API}/${dashboardId}/duplicate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
const data = await res.json();
|
||||
return data as Record<string, unknown>;
|
||||
return data as Dashboard;
|
||||
}
|
||||
export const DUPLICATE_DASHBOARD = createAsyncThunk(
|
||||
duplicate_dashboard_endpoint,
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { API_BASE } from '@/shared/config';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import {
|
||||
LIST_DASHBOARDS,
|
||||
CREATE_DASHBOARD,
|
||||
UPDATE_DASHBOARD,
|
||||
DELETE_DASHBOARD,
|
||||
DUPLICATE_DASHBOARD,
|
||||
GENERATE_DASHBOARD_NAME,
|
||||
} from '@/shared/backend-bridge/apps/dashboards';
|
||||
import type { Dashboard } from '@/shared/backend-bridge/apps/dashboards';
|
||||
|
||||
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;
|
||||
}
|
||||
export type { Dashboard };
|
||||
|
||||
interface DashboardsState {
|
||||
items: Record<string, Dashboard>;
|
||||
@@ -22,87 +21,16 @@ const initialState: DashboardsState = {
|
||||
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 }) => {
|
||||
const res = await fetch(`${DASHBOARDS_API}/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
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',
|
||||
async ({ id, thumbnail }: { id: string; thumbnail: string }) => {
|
||||
const res = await fetch(`${DASHBOARDS_API}/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ thumbnail }),
|
||||
});
|
||||
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 };
|
||||
},
|
||||
);
|
||||
|
||||
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) => {
|
||||
.addCase(LIST_DASHBOARDS.pending, (state) => {
|
||||
state.loading = true;
|
||||
})
|
||||
.addCase(fetchDashboards.fulfilled, (state, action) => {
|
||||
.addCase(LIST_DASHBOARDS.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
const items: Record<string, Dashboard> = {};
|
||||
for (const d of action.payload) {
|
||||
@@ -110,42 +38,30 @@ const dashboardsSlice = createSlice({
|
||||
}
|
||||
state.items = items;
|
||||
})
|
||||
.addCase(fetchDashboards.rejected, (state) => {
|
||||
.addCase(LIST_DASHBOARDS.rejected, (state) => {
|
||||
state.loading = false;
|
||||
})
|
||||
.addCase(createDashboard.fulfilled, (state, action) => {
|
||||
.addCase(CREATE_DASHBOARD.fulfilled, (state, action) => {
|
||||
state.items[action.payload.id] = action.payload;
|
||||
})
|
||||
.addCase(renameDashboard.fulfilled, (state, action) => {
|
||||
.addCase(UPDATE_DASHBOARD.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,
|
||||
};
|
||||
state.items[d.id] = { ...state.items[d.id], ...d };
|
||||
}
|
||||
})
|
||||
.addCase(deleteDashboard.fulfilled, (state, action) => {
|
||||
.addCase(DELETE_DASHBOARD.fulfilled, (state, action) => {
|
||||
delete state.items[action.payload];
|
||||
})
|
||||
.addCase(duplicateDashboard.fulfilled, (state, action) => {
|
||||
.addCase(DUPLICATE_DASHBOARD.fulfilled, (state, action) => {
|
||||
state.items[action.payload.id] = action.payload;
|
||||
})
|
||||
.addCase(generateDashboardName.fulfilled, (state, action) => {
|
||||
.addCase(GENERATE_DASHBOARD_NAME.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 } = action.payload;
|
||||
if (state.items[id]) {
|
||||
state.items[id].thumbnail = thumbnail;
|
||||
state.items[id].updated_at = updated_at;
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user