Skip to content

Instantly share code, notes, and snippets.

@sejas
Created January 16, 2025 09:29
Show Gist options
  • Select an option

  • Save sejas/3d6991ef4d60ab979cf82df5d7fdd674 to your computer and use it in GitHub Desktop.

Select an option

Save sejas/3d6991ef4d60ab979cf82df5d7fdd674 to your computer and use it in GitHub Desktop.
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import * as Sentry from '@sentry/electron/renderer';
import { getIpcApi } from '../lib/get-ipc-api';
import { DEFAULT_PHP_VERSION } from '../../vendor/wp-now/src/constants';
// ---- Types ----
interface WPCliItem {
name: string;
}
export interface SiteDetails {
id: string;
name: string;
port: number;
phpVersion?: string;
// add other fields as needed
}
interface SiteState {
currentURL: string;
pluginList: string[];
themeList: string[];
numberOfSites: number;
phpVersion: string;
siteName: string;
isSiteLoadedDict: Record<string, boolean>;
}
interface ThemeState {
themeName: string;
isBlockTheme: boolean;
}
interface SystemState {
os: string;
availableEditors: string[];
wpVersion: string;
}
export interface ChatState extends SiteState, ThemeState, SystemState {}
// ---- Helpers ----
const parseWpCliOutput = (stdout: string): string[] => {
try {
const parsed = JSON.parse(stdout) as WPCliItem[];
return parsed.map((item) => item.name) || [];
} catch (error) {
Sentry.captureException(error, { extra: { stdout } });
return [];
}
};
const fetchPluginList = async (siteId: string): Promise<string[]> => {
const { stdout, stderr } = await getIpcApi().executeWPCLiInline({
siteId,
args: 'plugin list --format=json --status=active',
skipPluginsAndThemes: true,
});
return stderr ? [] : parseWpCliOutput(stdout);
};
const fetchThemeList = async (siteId: string): Promise<string[]> => {
const { stdout, stderr } = await getIpcApi().executeWPCLiInline({
siteId,
args: 'theme list --format=json',
skipPluginsAndThemes: true,
});
return stderr ? [] : parseWpCliOutput(stdout);
};
// ---- Async thunks ----
export const updateFromSite = createAsyncThunk<
// Return type of the payload creator
{ siteId: string; plugins: string[]; themes: string[] },
// First argument to the payload creator
SiteDetails
>(
'chat/updateFromSite',
async (site: SiteDetails, { rejectWithValue }) => {
try {
const [plugins, themes] = await Promise.all([
fetchPluginList(site.id),
fetchThemeList(site.id),
]);
return { siteId: site.id, plugins, themes };
} catch (error) {
return rejectWithValue({ siteId: site.id, error });
}
}
);
// ---- Initial state ----
const initialState: ChatState = {
// SiteState
currentURL: '',
pluginList: [],
themeList: [],
numberOfSites: 0,
phpVersion: DEFAULT_PHP_VERSION,
siteName: '',
isSiteLoadedDict: {},
// ThemeState
themeName: '',
isBlockTheme: false,
// SystemState
os: window.appGlobals?.platform || '',
availableEditors: [],
wpVersion: '',
};
// ---- Slice definition ----
export const chatSlice = createSlice({
name: 'chat',
initialState,
reducers: {
// Synchronous reducers
setNumberOfSites(state, action: PayloadAction<number>) {
state.numberOfSites = action.payload;
},
updateFromTheme(state, action: PayloadAction<ThemeState>) {
const { themeName, isBlockTheme } = action.payload;
state.themeName = themeName;
state.isBlockTheme = isBlockTheme;
},
// You might want a synchronous action to initialize site details, etc.
setSiteDetails(state, action: PayloadAction<SiteDetails>) {
const { id, name, port, phpVersion } = action.payload;
state.currentURL = `http://localhost:${port}`;
state.phpVersion = phpVersion ?? DEFAULT_PHP_VERSION;
state.siteName = name;
// Mark site as "loading" or "loaded" initially
state.isSiteLoadedDict[id] = true;
},
},
extraReducers: (builder) => {
builder
.addCase(updateFromSite.fulfilled, (state, action) => {
const { siteId, plugins, themes } = action.payload;
state.pluginList = plugins;
state.themeList = themes;
// Mark site as loaded
state.isSiteLoadedDict[siteId] = true;
})
.addCase(updateFromSite.rejected, (state, action) => {
if (action.payload && typeof action.payload === 'object') {
const { siteId } = action.payload as { siteId: string; error: unknown };
// Mark site as not loaded
state.isSiteLoadedDict[siteId] = false;
}
})
.addCase(updateFromSite.pending, (state, action) => {
// Optionally mark site as "loading" if needed
// e.g. state.isSiteLoadedDict[siteId] = false;
});
},
});
// ---- Export actions from the slice ----
export const { setNumberOfSites, updateFromTheme, setSiteDetails } = chatSlice.actions;
// ---- Selectors ----
export const selectChatState = (state: { chat: ChatState }) => state.chat;
/**
* This selector recreates the `getContextForApi` format
*/
export const selectContextForApi = (state: { chat: ChatState }) => {
const {
currentURL,
numberOfSites,
wpVersion,
phpVersion,
pluginList,
themeList,
themeName,
isBlockTheme,
availableEditors,
siteName,
os,
} = state.chat;
return {
current_url: currentURL,
number_of_sites: numberOfSites,
wp_version: wpVersion,
php_version: phpVersion,
plugins: pluginList,
themes: themeList,
current_theme: themeName,
is_block_theme: isBlockTheme,
ide: availableEditors,
site_name: siteName,
os,
};
};
// ---- Reducer export ----
export default chatSlice.reducer;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment