refactor: extract SettingsPage GeneralSettingsTab
- Move GeneralSettingsTab to components/features/settings/ - Re-export from page for backward compatibility - Slim SettingsPage from 284 to ~150 lines Quality gates: tsc --noEmit passes, npm run build passes
This commit is contained in:
@@ -0,0 +1,149 @@
|
|||||||
|
import { useOutletContext } from "react-router-dom";
|
||||||
|
import { Icon } from "../../icon";
|
||||||
|
import type { UserConfig, UserConfigUpdate } from "../../../api/settings";
|
||||||
|
|
||||||
|
type SettingsOutletContext = {
|
||||||
|
config: UserConfig;
|
||||||
|
handleChange: (
|
||||||
|
key: keyof UserConfigUpdate,
|
||||||
|
value: string | string[] | null,
|
||||||
|
) => void;
|
||||||
|
handleSave: () => Promise<void>;
|
||||||
|
saveStatus: "idle" | "saving" | "saved" | "error";
|
||||||
|
};
|
||||||
|
|
||||||
|
const THEME_OPTIONS = [
|
||||||
|
{ value: "system", label: "System" },
|
||||||
|
{ value: "light", label: "Light" },
|
||||||
|
{ value: "dark", label: "Dark" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const TOAST_LEVEL_OPTIONS = [
|
||||||
|
{ value: "all", label: "All" },
|
||||||
|
{ value: "errors", label: "Errors only" },
|
||||||
|
{ value: "none", label: "None" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const MUTE_CATEGORIES = ["instance", "system", "health", "security"];
|
||||||
|
|
||||||
|
export const GeneralSettingsTab = () => {
|
||||||
|
const { config, handleChange, handleSave, saveStatus } =
|
||||||
|
useOutletContext<SettingsOutletContext>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="stack">
|
||||||
|
<h2>General</h2>
|
||||||
|
<label className="form-field">
|
||||||
|
Theme
|
||||||
|
<select
|
||||||
|
value={config.theme}
|
||||||
|
onChange={(e) => handleChange("theme", e.target.value)}
|
||||||
|
>
|
||||||
|
{THEME_OPTIONS.map((opt) => (
|
||||||
|
<option key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label className="form-field">
|
||||||
|
Git user name
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={config.git_user_name ?? ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleChange("git_user_name", e.target.value || null)
|
||||||
|
}
|
||||||
|
placeholder="Your git commit name"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="form-field">
|
||||||
|
Git user email
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={config.git_user_email ?? ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleChange("git_user_email", e.target.value || null)
|
||||||
|
}
|
||||||
|
placeholder="your.email@example.com"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="form-field">
|
||||||
|
Default editor
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={config.default_editor ?? ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleChange("default_editor", e.target.value || null)
|
||||||
|
}
|
||||||
|
placeholder="e.g., vscode, vim, cursor"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<h3>Notifications</h3>
|
||||||
|
<label className="form-field">
|
||||||
|
Toast level
|
||||||
|
<select
|
||||||
|
value={config.notification_toast_level ?? "all"}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleChange("notification_toast_level", e.target.value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{TOAST_LEVEL_OPTIONS.map((opt) => (
|
||||||
|
<option key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<fieldset className="form-field">
|
||||||
|
<legend>Mute categories</legend>
|
||||||
|
<div className="stack-sm">
|
||||||
|
{MUTE_CATEGORIES.map((cat) => (
|
||||||
|
<label
|
||||||
|
key={cat}
|
||||||
|
style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={(config.notification_mute_categories ?? []).includes(
|
||||||
|
cat,
|
||||||
|
)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const current = config.notification_mute_categories ?? [];
|
||||||
|
const next = e.target.checked
|
||||||
|
? [...current, cat]
|
||||||
|
: current.filter((c) => c !== cat);
|
||||||
|
handleChange("notification_mute_categories", next);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{cat}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<div className="settings-actions">
|
||||||
|
<button
|
||||||
|
className="primary-button"
|
||||||
|
onClick={() => void handleSave()}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{saveStatus === "saving" ? (
|
||||||
|
<>
|
||||||
|
<Icon name="loading" size="sm" /> Saving...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Icon name="save" size="sm" /> Save Settings
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{saveStatus === "saved" && (
|
||||||
|
<span className="success-text">Settings saved!</span>
|
||||||
|
)}
|
||||||
|
{saveStatus === "error" && (
|
||||||
|
<span className="error-text">Failed to save</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Link, Outlet, useLocation, useOutletContext } from "react-router-dom";
|
import { Link, Outlet, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getUserConfig,
|
getUserConfig,
|
||||||
updateUserConfig,
|
updateUserConfig,
|
||||||
@@ -8,7 +7,6 @@ import {
|
|||||||
type UserConfigUpdate,
|
type UserConfigUpdate,
|
||||||
} from "../api/settings";
|
} from "../api/settings";
|
||||||
import { ErrorState, LoadingState } from "../components/data-states";
|
import { ErrorState, LoadingState } from "../components/data-states";
|
||||||
import { Icon } from "../components/icon";
|
|
||||||
import { useAsyncData } from "../hooks/use-async-data";
|
import { useAsyncData } from "../hooks/use-async-data";
|
||||||
|
|
||||||
const TABS = [
|
const TABS = [
|
||||||
@@ -16,29 +14,7 @@ const TABS = [
|
|||||||
{ label: "SSH Keys", path: "ssh-keys" },
|
{ label: "SSH Keys", path: "ssh-keys" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const THEME_OPTIONS = [
|
export { GeneralSettingsTab } from "../components/features/settings/GeneralSettingsTab";
|
||||||
{ value: "system", label: "System" },
|
|
||||||
{ value: "light", label: "Light" },
|
|
||||||
{ value: "dark", label: "Dark" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const TOAST_LEVEL_OPTIONS = [
|
|
||||||
{ value: "all", label: "All" },
|
|
||||||
{ value: "errors", label: "Errors only" },
|
|
||||||
{ value: "none", label: "None" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const MUTE_CATEGORIES = ["instance", "system", "health", "security"];
|
|
||||||
|
|
||||||
type SettingsOutletContext = {
|
|
||||||
config: UserConfig;
|
|
||||||
handleChange: (
|
|
||||||
key: keyof UserConfigUpdate,
|
|
||||||
value: string | string[] | null,
|
|
||||||
) => void;
|
|
||||||
handleSave: () => Promise<void>;
|
|
||||||
saveStatus: "idle" | "saving" | "saved" | "error";
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SettingsPage = () => {
|
export const SettingsPage = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -60,7 +36,6 @@ export const SettingsPage = () => {
|
|||||||
"idle" | "saving" | "saved" | "error"
|
"idle" | "saving" | "saved" | "error"
|
||||||
>("idle");
|
>("idle");
|
||||||
|
|
||||||
// Sync loaded config into local editable state
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loadedConfig) {
|
if (loadedConfig) {
|
||||||
setConfig({
|
setConfig({
|
||||||
@@ -160,125 +135,3 @@ export const SettingsPage = () => {
|
|||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GeneralSettingsTab = () => {
|
|
||||||
const { config, handleChange, handleSave, saveStatus } =
|
|
||||||
useOutletContext<SettingsOutletContext>();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="stack">
|
|
||||||
<h2>General</h2>
|
|
||||||
<label className="form-field">
|
|
||||||
Theme
|
|
||||||
<select
|
|
||||||
value={config.theme}
|
|
||||||
onChange={(e) => handleChange("theme", e.target.value)}
|
|
||||||
>
|
|
||||||
{THEME_OPTIONS.map((opt) => (
|
|
||||||
<option key={opt.value} value={opt.value}>
|
|
||||||
{opt.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label className="form-field">
|
|
||||||
Git user name
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={config.git_user_name ?? ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleChange("git_user_name", e.target.value || null)
|
|
||||||
}
|
|
||||||
placeholder="Your git commit name"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="form-field">
|
|
||||||
Git user email
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
value={config.git_user_email ?? ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleChange("git_user_email", e.target.value || null)
|
|
||||||
}
|
|
||||||
placeholder="your.email@example.com"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="form-field">
|
|
||||||
Default editor
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={config.default_editor ?? ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleChange("default_editor", e.target.value || null)
|
|
||||||
}
|
|
||||||
placeholder="e.g., vscode, vim, cursor"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<h3>Notifications</h3>
|
|
||||||
<label className="form-field">
|
|
||||||
Toast level
|
|
||||||
<select
|
|
||||||
value={config.notification_toast_level ?? "all"}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleChange("notification_toast_level", e.target.value)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{TOAST_LEVEL_OPTIONS.map((opt) => (
|
|
||||||
<option key={opt.value} value={opt.value}>
|
|
||||||
{opt.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<fieldset className="form-field">
|
|
||||||
<legend>Mute categories</legend>
|
|
||||||
<div className="stack-sm">
|
|
||||||
{MUTE_CATEGORIES.map((cat) => (
|
|
||||||
<label
|
|
||||||
key={cat}
|
|
||||||
style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={(config.notification_mute_categories ?? []).includes(
|
|
||||||
cat,
|
|
||||||
)}
|
|
||||||
onChange={(e) => {
|
|
||||||
const current = config.notification_mute_categories ?? [];
|
|
||||||
const next = e.target.checked
|
|
||||||
? [...current, cat]
|
|
||||||
: current.filter((c) => c !== cat);
|
|
||||||
handleChange("notification_mute_categories", next);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{cat}
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
<div className="settings-actions">
|
|
||||||
<button
|
|
||||||
className="primary-button"
|
|
||||||
onClick={() => void handleSave()}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
{saveStatus === "saving" ? (
|
|
||||||
<>
|
|
||||||
<Icon name="loading" size="sm" /> Saving...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Icon name="save" size="sm" /> Save Settings
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
{saveStatus === "saved" && (
|
|
||||||
<span className="success-text">Settings saved!</span>
|
|
||||||
)}
|
|
||||||
{saveStatus === "error" && (
|
|
||||||
<span className="error-text">Failed to save</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
Reference in New Issue
Block a user