mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Inline provider catalog in settings (#1423)
Replace the modal Add Provider flow with an inline Add provider section below the configured providers list. Makes new providers discoverable without opening a modal. - Remove add-provider-modal component - Add provider-catalog-list component with search + Add button - Remove duplicate ACP catalog entries for built-in providers - Add spacing between the two sections
This commit is contained in:
@@ -4,7 +4,7 @@ import { getServerId } from "./helpers/server-id";
|
||||
import {
|
||||
expectProviderInstalledInSettings,
|
||||
installAcpCatalogProvider,
|
||||
openAddProviderModal,
|
||||
openAddProviderArea,
|
||||
openSettingsHost,
|
||||
openSettingsHostSection,
|
||||
} from "./helpers/settings";
|
||||
@@ -21,7 +21,7 @@ test.describe("ACP provider catalog", () => {
|
||||
await openSettingsHost(page, getServerId());
|
||||
// Providers moved to their own host section; add-provider lives there now.
|
||||
await openSettingsHostSection(page, getServerId(), "providers");
|
||||
await openAddProviderModal(page);
|
||||
await openAddProviderArea(page);
|
||||
|
||||
await installAcpCatalogProvider(page, ACP_PROVIDER.name);
|
||||
await expectProviderInstalledInSettings(page, ACP_PROVIDER.name);
|
||||
|
||||
@@ -319,8 +319,8 @@ export async function serveJson(page: Page, url: string, body: unknown): Promise
|
||||
});
|
||||
}
|
||||
|
||||
export async function openAddProviderModal(page: Page): Promise<void> {
|
||||
await page.getByRole("button", { name: "Add provider", exact: true }).click();
|
||||
export async function openAddProviderArea(page: Page): Promise<void> {
|
||||
await page.getByTestId("host-page-add-provider-card").scrollIntoViewIfNeeded();
|
||||
await expect(page.getByRole("textbox", { name: "Search providers" })).toBeVisible();
|
||||
}
|
||||
|
||||
@@ -332,7 +332,6 @@ export async function findAcpCatalogProvider(page: Page, providerName: string):
|
||||
export async function installAcpCatalogProvider(page: Page, providerName: string): Promise<void> {
|
||||
await findAcpCatalogProvider(page, providerName);
|
||||
await page.getByRole("button", { name: "Add", exact: true }).click();
|
||||
await expect(page.getByRole("textbox", { name: "Search providers" })).toHaveCount(0);
|
||||
}
|
||||
|
||||
export async function expectProviderInstalledInSettings(
|
||||
|
||||
@@ -1,36 +1,24 @@
|
||||
import { useCallback, useMemo, useReducer, useState } from "react";
|
||||
import { Alert, Pressable, Text, View } from "react-native";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import { SvgXml } from "react-native-svg";
|
||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import { ExternalLink, PackagePlus, Search } from "lucide-react-native";
|
||||
import {
|
||||
AdaptiveModalSheet,
|
||||
AdaptiveTextInput,
|
||||
type SheetHeader,
|
||||
} from "@/components/adaptive-modal-sheet";
|
||||
import { AdaptiveTextInput } from "@/components/adaptive-modal-sheet";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
buildAcpProviderConfigPatch,
|
||||
useAcpProviderCatalog,
|
||||
type AcpProviderCatalogItem,
|
||||
} from "@/hooks/use-acp-provider-catalog";
|
||||
import { useDaemonConfig } from "@/hooks/use-daemon-config";
|
||||
import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
import { openExternalUrl } from "@/utils/open-external-url";
|
||||
|
||||
interface AddProviderModalProps {
|
||||
interface ProviderCatalogListProps {
|
||||
serverId: string;
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
installingProviderId: string | null;
|
||||
onInstall: (entry: AcpProviderCatalogItem) => Promise<void> | void;
|
||||
}
|
||||
|
||||
type InstallState = "installed" | "available";
|
||||
|
||||
const FLEX_ONE_STYLE = { flex: 1 } as const;
|
||||
const ACTION_BUTTON_STYLE = { width: 92 } as const;
|
||||
const MODAL_SNAP_POINTS = ["78%", "92%"];
|
||||
const ADD_PROVIDER_HEADER: SheetHeader = { title: "Add provider" };
|
||||
const SEARCH_ICON_SIZE = 16;
|
||||
const PROVIDER_FALLBACK_ICON_SIZE = 20;
|
||||
const PROVIDER_REMOTE_ICON_SIZE = 24;
|
||||
@@ -45,14 +33,6 @@ const foregroundMutedColorMapping = (theme: Theme) => ({
|
||||
color: theme.colors.foregroundMuted,
|
||||
});
|
||||
|
||||
function getInstallState(
|
||||
entry: AcpProviderCatalogItem,
|
||||
installedProviderIds: Set<string>,
|
||||
): InstallState {
|
||||
if (installedProviderIds.has(entry.id)) return "installed";
|
||||
return "available";
|
||||
}
|
||||
|
||||
function matchesSearch(entry: AcpProviderCatalogItem, query: string): boolean {
|
||||
const normalized = query.trim().toLowerCase();
|
||||
if (!normalized) return true;
|
||||
@@ -61,22 +41,13 @@ function matchesSearch(entry: AcpProviderCatalogItem, query: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
interface ProviderCatalogRowProps {
|
||||
interface CatalogRowProps {
|
||||
entry: AcpProviderCatalogItem;
|
||||
state: InstallState;
|
||||
installing: boolean;
|
||||
onInstall: (entry: AcpProviderCatalogItem) => void;
|
||||
}
|
||||
|
||||
function ProviderCatalogRow({ entry, state, installing, onInstall }: ProviderCatalogRowProps) {
|
||||
const isAvailable = state === "available";
|
||||
let actionLabel = "Add";
|
||||
if (installing) {
|
||||
actionLabel = "Adding";
|
||||
} else if (state === "installed") {
|
||||
actionLabel = "Installed";
|
||||
}
|
||||
|
||||
function CatalogRow({ entry, installing, onInstall }: CatalogRowProps) {
|
||||
const handleInstall = useCallback(() => {
|
||||
onInstall(entry);
|
||||
}, [entry, onInstall]);
|
||||
@@ -125,72 +96,43 @@ function ProviderCatalogRow({ entry, state, installing, onInstall }: ProviderCat
|
||||
</View>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={isAvailable ? "default" : "secondary"}
|
||||
disabled={!isAvailable || installing}
|
||||
variant="default"
|
||||
disabled={installing}
|
||||
loading={installing}
|
||||
onPress={handleInstall}
|
||||
style={ACTION_BUTTON_STYLE}
|
||||
style={styles.actionButton}
|
||||
testID={`install-provider-${entry.id}`}
|
||||
>
|
||||
{actionLabel}
|
||||
{installing ? "Adding" : "Add"}
|
||||
</Button>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export function AddProviderModal({ serverId, visible, onClose }: AddProviderModalProps) {
|
||||
const { entries } = useAcpProviderCatalog();
|
||||
const { entries: providerEntries, refresh } = useProvidersSnapshot(serverId);
|
||||
const { patchConfig } = useDaemonConfig(serverId);
|
||||
export function ProviderCatalogList({
|
||||
serverId,
|
||||
installingProviderId,
|
||||
onInstall,
|
||||
}: ProviderCatalogListProps) {
|
||||
const { entries: catalogEntries } = useAcpProviderCatalog();
|
||||
const { entries: providerEntries } = useProvidersSnapshot(serverId);
|
||||
const [search, setSearch] = useState("");
|
||||
const [searchResetKey, bumpSearchResetKey] = useReducer((key: number) => key + 1, 0);
|
||||
const [installingProviderId, setInstallingProviderId] = useState<string | null>(null);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setSearch("");
|
||||
bumpSearchResetKey();
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
const installedProviderIds = useMemo(
|
||||
const installedIds = useMemo(
|
||||
() => new Set(providerEntries?.map((entry) => entry.provider) ?? []),
|
||||
[providerEntries],
|
||||
);
|
||||
const filteredEntries = useMemo(
|
||||
() => entries.filter((entry) => matchesSearch(entry, search)),
|
||||
[entries, search],
|
||||
);
|
||||
|
||||
const handleInstall = useCallback(
|
||||
async (entry: AcpProviderCatalogItem) => {
|
||||
if (installingProviderId) return;
|
||||
|
||||
setInstallingProviderId(entry.id);
|
||||
try {
|
||||
await patchConfig(buildAcpProviderConfigPatch(entry));
|
||||
await refresh([entry.id]);
|
||||
handleClose();
|
||||
} catch (installError) {
|
||||
Alert.alert(
|
||||
"Unable to install provider",
|
||||
installError instanceof Error ? installError.message : String(installError),
|
||||
);
|
||||
} finally {
|
||||
setInstallingProviderId((current) => (current === entry.id ? null : current));
|
||||
}
|
||||
},
|
||||
[installingProviderId, handleClose, patchConfig, refresh],
|
||||
const availableEntries = useMemo(
|
||||
() =>
|
||||
catalogEntries
|
||||
.filter((entry) => !installedIds.has(entry.id))
|
||||
.filter((entry) => matchesSearch(entry, search)),
|
||||
[catalogEntries, installedIds, search],
|
||||
);
|
||||
|
||||
return (
|
||||
<AdaptiveModalSheet
|
||||
header={ADD_PROVIDER_HEADER}
|
||||
visible={visible}
|
||||
onClose={handleClose}
|
||||
desktopMaxWidth={680}
|
||||
snapPoints={MODAL_SNAP_POINTS}
|
||||
testID="add-provider-modal"
|
||||
>
|
||||
<View>
|
||||
<View style={styles.searchField}>
|
||||
<View style={styles.searchIcon}>
|
||||
<ThemedSearch size={SEARCH_ICON_SIZE} uniProps={foregroundMutedColorMapping} />
|
||||
@@ -198,8 +140,6 @@ export function AddProviderModal({ serverId, visible, onClose }: AddProviderModa
|
||||
<AdaptiveTextInput
|
||||
testID="provider-catalog-search"
|
||||
accessibilityLabel="Search providers"
|
||||
initialValue={search}
|
||||
resetKey={`provider-catalog-search-${searchResetKey}`}
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
placeholder="Search providers"
|
||||
@@ -209,32 +149,25 @@ export function AddProviderModal({ serverId, visible, onClose }: AddProviderModa
|
||||
/>
|
||||
</View>
|
||||
|
||||
{filteredEntries.length === 0 ? (
|
||||
{availableEntries.length === 0 ? (
|
||||
<View style={styles.stateBox}>
|
||||
<Text style={styles.stateText}>No providers found</Text>
|
||||
<Text style={styles.stateText}>
|
||||
{search.trim().length > 0 ? "No providers found" : "All providers are installed"}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{filteredEntries.length > 0 ? (
|
||||
) : (
|
||||
<View style={styles.list}>
|
||||
{filteredEntries.map((entry) => (
|
||||
<ProviderCatalogRow
|
||||
{availableEntries.map((entry) => (
|
||||
<CatalogRow
|
||||
key={entry.id}
|
||||
entry={entry}
|
||||
state={getInstallState(entry, installedProviderIds)}
|
||||
installing={installingProviderId === entry.id}
|
||||
onInstall={handleInstall}
|
||||
onInstall={onInstall}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<View style={styles.actions}>
|
||||
<Button style={FLEX_ONE_STYLE} variant="secondary" onPress={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</View>
|
||||
</AdaptiveModalSheet>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -248,6 +181,7 @@ const styles = StyleSheet.create((theme) => ({
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.border,
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
marginBottom: theme.spacing[3],
|
||||
},
|
||||
searchIcon: {
|
||||
width: 18,
|
||||
@@ -320,6 +254,10 @@ const styles = StyleSheet.create((theme) => ({
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.xs,
|
||||
},
|
||||
actionButton: {
|
||||
width: 92,
|
||||
flexShrink: 0,
|
||||
},
|
||||
stateBox: {
|
||||
minHeight: 96,
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
@@ -335,7 +273,4 @@ const styles = StyleSheet.create((theme) => ({
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.sm,
|
||||
},
|
||||
actions: {
|
||||
flexDirection: "row",
|
||||
},
|
||||
}));
|
||||
@@ -53,15 +53,6 @@ const CATALOG_DATA = [
|
||||
installLink: "https://www.autohand.ai/cli/",
|
||||
command: ["npx", "-y", "@autohandai/autohand-acp@0.2.1"],
|
||||
},
|
||||
{
|
||||
id: "claude-acp",
|
||||
title: "Claude Agent",
|
||||
description: "ACP wrapper for Anthropic's Claude",
|
||||
version: "0.42.0",
|
||||
iconId: "claude-acp",
|
||||
installLink: "https://github.com/agentclientprotocol/claude-agent-acp",
|
||||
command: ["npx", "-y", "@agentclientprotocol/claude-agent-acp@0.42.0"],
|
||||
},
|
||||
{
|
||||
id: "cline",
|
||||
title: "Cline",
|
||||
@@ -81,15 +72,6 @@ const CATALOG_DATA = [
|
||||
installLink: "https://www.codebuddy.cn/cli/",
|
||||
command: ["npx", "-y", "@tencent-ai/codebuddy-code@2.103.4", "--acp"],
|
||||
},
|
||||
{
|
||||
id: "codex-acp",
|
||||
title: "Codex CLI",
|
||||
description: "ACP adapter for OpenAI's coding assistant",
|
||||
version: "0.13.0",
|
||||
iconId: "codex-acp",
|
||||
installLink: "https://github.com/zed-industries/codex-acp",
|
||||
command: ["codex-acp"],
|
||||
},
|
||||
{
|
||||
id: "cortex-code",
|
||||
title: "Cortex Code",
|
||||
@@ -203,15 +185,6 @@ const CATALOG_DATA = [
|
||||
installLink: "https://geminicli.com",
|
||||
command: ["npx", "-y", "@google/gemini-cli@0.45.2", "--acp"],
|
||||
},
|
||||
{
|
||||
id: "github-copilot-cli",
|
||||
title: "GitHub Copilot",
|
||||
description: "GitHub's AI pair programmer",
|
||||
version: "1.0.60",
|
||||
iconId: "github-copilot-cli",
|
||||
installLink: "https://github.com/features/copilot/cli/",
|
||||
command: ["npx", "-y", "@github/copilot@1.0.60", "--acp"],
|
||||
},
|
||||
{
|
||||
id: "glm-acp-agent",
|
||||
title: "GLM Agent",
|
||||
@@ -314,24 +287,6 @@ const CATALOG_DATA = [
|
||||
installLink: "https://www.compassap.ai/portfolio/nova.html",
|
||||
command: ["npx", "-y", "@compass-ai/nova@1.1.15", "acp"],
|
||||
},
|
||||
{
|
||||
id: "opencode",
|
||||
title: "OpenCode",
|
||||
description: "The open source coding agent",
|
||||
version: "1.14.39",
|
||||
iconId: "opencode",
|
||||
installLink: "https://opencode.ai/docs/acp/",
|
||||
command: ["opencode", "acp"],
|
||||
},
|
||||
{
|
||||
id: "pi-acp",
|
||||
title: "pi ACP",
|
||||
description: "ACP adapter for pi coding agent",
|
||||
version: "0.0.27",
|
||||
iconId: "pi-acp",
|
||||
installLink: "https://github.com/svkozak/pi-acp",
|
||||
command: ["npx", "-y", "pi-acp@0.0.27"],
|
||||
},
|
||||
{
|
||||
id: "poolside",
|
||||
title: "Poolside",
|
||||
|
||||
@@ -97,7 +97,6 @@ vi.mock("lucide-react-native", () => {
|
||||
const icon = (name: string) => () => React.createElement("span", { "data-icon": name });
|
||||
return {
|
||||
ChevronRight: icon("ChevronRight"),
|
||||
Plus: icon("Plus"),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -143,8 +142,8 @@ vi.mock("@/stores/provider-settings-store", () => ({
|
||||
selector({ open: openProviderSettingsMock }),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/add-provider-modal", () => ({
|
||||
AddProviderModal: () => null,
|
||||
vi.mock("@/components/provider-catalog-list", () => ({
|
||||
ProviderCatalogList: () => null,
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/use-providers-snapshot", () => ({
|
||||
|
||||
@@ -6,13 +6,17 @@ import { useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||
import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot";
|
||||
import { useDaemonConfig } from "@/hooks/use-daemon-config";
|
||||
import { buildProviderDefinitions } from "@/utils/provider-definitions";
|
||||
import { AddProviderModal } from "@/components/add-provider-modal";
|
||||
import {
|
||||
buildAcpProviderConfigPatch,
|
||||
type AcpProviderCatalogItem,
|
||||
} from "@/hooks/use-acp-provider-catalog";
|
||||
import { ProviderCatalogList } from "@/components/provider-catalog-list";
|
||||
import { getProviderIcon } from "@/components/provider-icons";
|
||||
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { SettingsSection } from "@/screens/settings/settings-section";
|
||||
import { useProviderSettingsStore } from "@/stores/provider-settings-store";
|
||||
import { ChevronRight, Plus } from "lucide-react-native";
|
||||
import { ChevronRight } from "lucide-react-native";
|
||||
|
||||
type ProviderDefinition = ReturnType<typeof buildProviderDefinitions>[number];
|
||||
type ProviderEntry = NonNullable<ReturnType<typeof useProvidersSnapshot>["entries"]>[number];
|
||||
@@ -177,13 +181,12 @@ export interface ProvidersSectionProps {
|
||||
}
|
||||
|
||||
export function ProvidersSection({ serverId }: ProvidersSectionProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const isConnected = useHostRuntimeIsConnected(serverId);
|
||||
const { entries, isLoading } = useProvidersSnapshot(serverId);
|
||||
const { entries, isLoading, refresh } = useProvidersSnapshot(serverId);
|
||||
const { patchConfig } = useDaemonConfig(serverId);
|
||||
const openProviderSettings = useProviderSettingsStore((state) => state.open);
|
||||
const [isAddProviderOpen, setIsAddProviderOpen] = useState(false);
|
||||
const [pendingProviderId, setPendingProviderId] = useState<string | null>(null);
|
||||
const [installingProviderId, setInstallingProviderId] = useState<string | null>(null);
|
||||
|
||||
const providerDefinitions = useMemo(() => buildProviderDefinitions(entries), [entries]);
|
||||
const hasServer = serverId.length > 0;
|
||||
@@ -194,8 +197,7 @@ export function ProvidersSection({ serverId }: ProvidersSectionProps) {
|
||||
},
|
||||
[openProviderSettings, serverId],
|
||||
);
|
||||
const handleOpenAddProvider = useCallback(() => setIsAddProviderOpen(true), []);
|
||||
const handleCloseAddProvider = useCallback(() => setIsAddProviderOpen(false), []);
|
||||
|
||||
const handleToggleEnabled = useCallback(
|
||||
async (providerId: string, enabled: boolean) => {
|
||||
setPendingProviderId(providerId);
|
||||
@@ -213,37 +215,29 @@ export function ProvidersSection({ serverId }: ProvidersSectionProps) {
|
||||
[patchConfig],
|
||||
);
|
||||
|
||||
const headerActions = useMemo(
|
||||
() =>
|
||||
hasServer && isConnected ? (
|
||||
<View style={styles.headerActions}>
|
||||
<Pressable
|
||||
onPress={handleOpenAddProvider}
|
||||
hitSlop={8}
|
||||
style={settingsStyles.sectionHeaderLink}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Add provider"
|
||||
testID="add-provider-button"
|
||||
>
|
||||
<Plus size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||
<Text style={settingsStyles.sectionHeaderLinkText}>Add provider</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
) : undefined,
|
||||
[
|
||||
hasServer,
|
||||
isConnected,
|
||||
handleOpenAddProvider,
|
||||
theme.iconSize.sm,
|
||||
theme.colors.foregroundMuted,
|
||||
],
|
||||
const handleInstall = useCallback(
|
||||
async (entry: AcpProviderCatalogItem) => {
|
||||
if (installingProviderId) return;
|
||||
setInstallingProviderId(entry.id);
|
||||
try {
|
||||
await patchConfig(buildAcpProviderConfigPatch(entry));
|
||||
await refresh([entry.id]);
|
||||
} catch (error) {
|
||||
Alert.alert(
|
||||
"Unable to add provider",
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
} finally {
|
||||
setInstallingProviderId((current) => (current === entry.id ? null : current));
|
||||
}
|
||||
},
|
||||
[installingProviderId, patchConfig, refresh],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSection
|
||||
title="Providers"
|
||||
trailing={headerActions}
|
||||
testID="host-page-providers-card"
|
||||
style={styles.sectionSpacing}
|
||||
>
|
||||
@@ -279,8 +273,18 @@ export function ProvidersSection({ serverId }: ProvidersSectionProps) {
|
||||
) : null}
|
||||
</SettingsSection>
|
||||
|
||||
{hasServer && isConnected && isAddProviderOpen ? (
|
||||
<AddProviderModal serverId={serverId} visible onClose={handleCloseAddProvider} />
|
||||
{hasServer && isConnected ? (
|
||||
<SettingsSection
|
||||
title="Add provider"
|
||||
testID="host-page-add-provider-card"
|
||||
style={styles.addProviderSection}
|
||||
>
|
||||
<ProviderCatalogList
|
||||
serverId={serverId}
|
||||
installingProviderId={installingProviderId}
|
||||
onInstall={handleInstall}
|
||||
/>
|
||||
</SettingsSection>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
@@ -290,6 +294,9 @@ const styles = StyleSheet.create((theme) => ({
|
||||
sectionSpacing: {
|
||||
marginBottom: theme.spacing[4],
|
||||
},
|
||||
addProviderSection: {
|
||||
marginTop: theme.spacing[4],
|
||||
},
|
||||
emptyCard: {
|
||||
padding: theme.spacing[4],
|
||||
alignItems: "center",
|
||||
@@ -298,11 +305,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.sm,
|
||||
},
|
||||
headerActions: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[3],
|
||||
},
|
||||
row: {
|
||||
gap: theme.spacing[3],
|
||||
minHeight: 56,
|
||||
|
||||
Reference in New Issue
Block a user