mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: add "user_configs" db table (#16564)
This commit is contained in:
+47
-46
@@ -9,53 +9,54 @@
|
||||
-->
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Coder</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#17172E" />
|
||||
<meta name="application-name" content="{{ .ApplicationName }}" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="csrf-token" content="{{ .CSRF.Token }}" />
|
||||
<meta property="build-info" content="{{ .BuildInfo }}" />
|
||||
<meta property="user" content="{{ .User }}" />
|
||||
<meta property="entitlements" content="{{ .Entitlements }}" />
|
||||
<meta property="appearance" content="{{ .Appearance }}" />
|
||||
<meta property="experiments" content="{{ .Experiments }}" />
|
||||
<meta property="regions" content="{{ .Regions }}" />
|
||||
<meta property="docs-url" content="{{ .DocsURL }}" />
|
||||
<meta property="logo-url" content="{{ .LogoURL }}" />
|
||||
<!-- We need to set data-react-helmet to be able to override it in the workspace page -->
|
||||
<link
|
||||
rel="alternate icon"
|
||||
type="image/png"
|
||||
href="/favicons/favicon-light.png"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
data-react-helmet="true"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/svg+xml"
|
||||
href="/favicons/favicon-light.svg"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
data-react-helmet="true"
|
||||
/>
|
||||
<link
|
||||
rel="alternate icon"
|
||||
type="image/png"
|
||||
href="/favicons/favicon-dark.png"
|
||||
media="(prefers-color-scheme: light)"
|
||||
data-react-helmet="true"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/svg+xml"
|
||||
href="/favicons/favicon-dark.svg"
|
||||
media="(prefers-color-scheme: light)"
|
||||
data-react-helmet="true"
|
||||
/>
|
||||
<meta charset="utf-8" />
|
||||
<title>Coder</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#17172E" />
|
||||
<meta name="application-name" content="{{ .ApplicationName }}" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="csrf-token" content="{{ .CSRF.Token }}" />
|
||||
<meta property="build-info" content="{{ .BuildInfo }}" />
|
||||
<meta property="user" content="{{ .User }}" />
|
||||
<meta property="entitlements" content="{{ .Entitlements }}" />
|
||||
<meta property="appearance" content="{{ .Appearance }}" />
|
||||
<meta property="userAppearance" content="{{ .UserAppearance }}" />
|
||||
<meta property="experiments" content="{{ .Experiments }}" />
|
||||
<meta property="regions" content="{{ .Regions }}" />
|
||||
<meta property="docs-url" content="{{ .DocsURL }}" />
|
||||
<meta property="logo-url" content="{{ .LogoURL }}" />
|
||||
<!-- We need to set data-react-helmet to be able to override it in the workspace page -->
|
||||
<link
|
||||
rel="alternate icon"
|
||||
type="image/png"
|
||||
href="/favicons/favicon-light.png"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
data-react-helmet="true"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/svg+xml"
|
||||
href="/favicons/favicon-light.svg"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
data-react-helmet="true"
|
||||
/>
|
||||
<link
|
||||
rel="alternate icon"
|
||||
type="image/png"
|
||||
href="/favicons/favicon-dark.png"
|
||||
media="(prefers-color-scheme: light)"
|
||||
data-react-helmet="true"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/svg+xml"
|
||||
href="/favicons/favicon-dark.svg"
|
||||
media="(prefers-color-scheme: light)"
|
||||
data-react-helmet="true"
|
||||
/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./src/index.tsx"></script>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./src/index.tsx"></script>
|
||||
</body>
|
||||
|
||||
+29
-7
@@ -292,13 +292,14 @@ type htmlState struct {
|
||||
ApplicationName string
|
||||
LogoURL string
|
||||
|
||||
BuildInfo string
|
||||
User string
|
||||
Entitlements string
|
||||
Appearance string
|
||||
Experiments string
|
||||
Regions string
|
||||
DocsURL string
|
||||
BuildInfo string
|
||||
User string
|
||||
Entitlements string
|
||||
Appearance string
|
||||
UserAppearance string
|
||||
Experiments string
|
||||
Regions string
|
||||
DocsURL string
|
||||
}
|
||||
|
||||
type csrfState struct {
|
||||
@@ -426,12 +427,22 @@ func (h *Handler) renderHTMLWithState(r *http.Request, filePath string, state ht
|
||||
|
||||
var eg errgroup.Group
|
||||
var user database.User
|
||||
var themePreference string
|
||||
orgIDs := []uuid.UUID{}
|
||||
eg.Go(func() error {
|
||||
var err error
|
||||
user, err = h.opts.Database.GetUserByID(ctx, apiKey.UserID)
|
||||
return err
|
||||
})
|
||||
eg.Go(func() error {
|
||||
var err error
|
||||
themePreference, err = h.opts.Database.GetUserAppearanceSettings(ctx, apiKey.UserID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
themePreference = ""
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
})
|
||||
eg.Go(func() error {
|
||||
memberIDs, err := h.opts.Database.GetOrganizationIDsByMemberIDs(ctx, []uuid.UUID{apiKey.UserID})
|
||||
if errors.Is(err, sql.ErrNoRows) || len(memberIDs) == 0 {
|
||||
@@ -455,6 +466,17 @@ func (h *Handler) renderHTMLWithState(r *http.Request, filePath string, state ht
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
userAppearance, err := json.Marshal(codersdk.UserAppearanceSettings{
|
||||
ThemePreference: themePreference,
|
||||
})
|
||||
if err == nil {
|
||||
state.UserAppearance = html.EscapeString(string(userAppearance))
|
||||
}
|
||||
}()
|
||||
|
||||
if h.Entitlements != nil {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
|
||||
+8
-6
@@ -1340,14 +1340,16 @@ class ApiMethods {
|
||||
return response.data;
|
||||
};
|
||||
|
||||
getAppearanceSettings =
|
||||
async (): Promise<TypesGen.UserAppearanceSettings> => {
|
||||
const response = await this.axios.get("/api/v2/users/me/appearance");
|
||||
return response.data;
|
||||
};
|
||||
|
||||
updateAppearanceSettings = async (
|
||||
userId: string,
|
||||
data: TypesGen.UpdateUserAppearanceSettingsRequest,
|
||||
): Promise<TypesGen.User> => {
|
||||
const response = await this.axios.put(
|
||||
`/api/v2/users/${userId}/appearance`,
|
||||
data,
|
||||
);
|
||||
): Promise<TypesGen.UserAppearanceSettings> => {
|
||||
const response = await this.axios.put("/api/v2/users/me/appearance", data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import type {
|
||||
UpdateUserPasswordRequest,
|
||||
UpdateUserProfileRequest,
|
||||
User,
|
||||
UserAppearanceSettings,
|
||||
UsersRequest,
|
||||
ValidateUserPasswordRequest,
|
||||
} from "api/typesGenerated";
|
||||
import {
|
||||
type MetadataState,
|
||||
@@ -224,35 +224,39 @@ export const updateProfile = (userId: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
const myAppearanceKey = ["me", "appearance"];
|
||||
|
||||
export const appearanceSettings = (
|
||||
metadata: MetadataState<UserAppearanceSettings>,
|
||||
) => {
|
||||
return cachedQuery({
|
||||
metadata,
|
||||
queryKey: myAppearanceKey,
|
||||
queryFn: API.getAppearanceSettings,
|
||||
});
|
||||
};
|
||||
|
||||
export const updateAppearanceSettings = (
|
||||
userId: string,
|
||||
queryClient: QueryClient,
|
||||
): UseMutationOptions<
|
||||
User,
|
||||
UserAppearanceSettings,
|
||||
unknown,
|
||||
UpdateUserAppearanceSettingsRequest,
|
||||
unknown
|
||||
> => {
|
||||
return {
|
||||
mutationFn: (req) => API.updateAppearanceSettings(userId, req),
|
||||
mutationFn: (req) => API.updateAppearanceSettings(req),
|
||||
onMutate: async (patch) => {
|
||||
// Mutate the `queryClient` optimistically to make the theme switcher
|
||||
// more responsive.
|
||||
const me: User | undefined = queryClient.getQueryData(meKey);
|
||||
if (userId === "me" && me) {
|
||||
queryClient.setQueryData(meKey, {
|
||||
...me,
|
||||
theme_preference: patch.theme_preference,
|
||||
});
|
||||
}
|
||||
queryClient.setQueryData(myAppearanceKey, {
|
||||
theme_preference: patch.theme_preference,
|
||||
});
|
||||
},
|
||||
onSuccess: async () => {
|
||||
onSuccess: async () =>
|
||||
// Could technically invalidate more, but we only ever care about the
|
||||
// `theme_preference` for the `me` query.
|
||||
if (userId === "me") {
|
||||
await queryClient.invalidateQueries(meKey);
|
||||
}
|
||||
},
|
||||
await queryClient.invalidateQueries(myAppearanceKey),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Generated
+6
-1
@@ -1970,7 +1970,7 @@ export interface ReducedUser extends MinimalUser {
|
||||
readonly last_seen_at: string;
|
||||
readonly status: UserStatus;
|
||||
readonly login_type: LoginType;
|
||||
readonly theme_preference: string;
|
||||
readonly theme_preference?: string;
|
||||
}
|
||||
|
||||
// From codersdk/workspaceproxy.go
|
||||
@@ -2805,6 +2805,11 @@ export interface UserActivityInsightsResponse {
|
||||
readonly report: UserActivityInsightsReport;
|
||||
}
|
||||
|
||||
// From codersdk/users.go
|
||||
export interface UserAppearanceSettings {
|
||||
readonly theme_preference: string;
|
||||
}
|
||||
|
||||
// From codersdk/insights.go
|
||||
export interface UserLatency {
|
||||
readonly template_ids: readonly string[];
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { ThemeProvider } from "contexts/ThemeProvider";
|
||||
import { fireEvent, screen } from "@testing-library/react";
|
||||
import { renderComponent } from "testHelpers/renderHelpers";
|
||||
import { FileUpload } from "./FileUpload";
|
||||
|
||||
test("accepts files with the correct extension", async () => {
|
||||
const onUpload = jest.fn();
|
||||
|
||||
render(
|
||||
<ThemeProvider>
|
||||
<FileUpload
|
||||
isUploading={false}
|
||||
onUpload={onUpload}
|
||||
removeLabel="Remove file"
|
||||
title="Upload file"
|
||||
extensions={["tar", "zip"]}
|
||||
/>
|
||||
</ThemeProvider>,
|
||||
renderComponent(
|
||||
<FileUpload
|
||||
isUploading={false}
|
||||
onUpload={onUpload}
|
||||
removeLabel="Remove file"
|
||||
title="Upload file"
|
||||
extensions={["tar", "zip"]}
|
||||
/>,
|
||||
);
|
||||
|
||||
const dropZone = screen.getByTestId("drop-zone");
|
||||
|
||||
@@ -7,26 +7,27 @@ import {
|
||||
StyledEngineProvider,
|
||||
// biome-ignore lint/nursery/noRestrictedImports: we extend the MUI theme
|
||||
} from "@mui/material/styles";
|
||||
import { appearanceSettings } from "api/queries/users";
|
||||
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
|
||||
import {
|
||||
type FC,
|
||||
type PropsWithChildren,
|
||||
type ReactNode,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import themes, { DEFAULT_THEME, type Theme } from "theme";
|
||||
import { AuthContext } from "./auth/AuthProvider";
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export const ThemeProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
// We need to use the `AuthContext` directly, rather than the `useAuth` hook,
|
||||
// because Storybook and many tests depend on this component, but do not provide
|
||||
// an `AuthProvider`, and `useAuth` will throw in that case.
|
||||
const user = useContext(AuthContext)?.user;
|
||||
const { metadata } = useEmbeddedMetadata();
|
||||
const appearanceSettingsQuery = useQuery(
|
||||
appearanceSettings(metadata.userAppearance),
|
||||
);
|
||||
const themeQuery = useMemo(
|
||||
() => window.matchMedia?.("(prefers-color-scheme: light)"),
|
||||
[],
|
||||
@@ -53,7 +54,8 @@ export const ThemeProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
}, [themeQuery]);
|
||||
|
||||
// We might not be logged in yet, or the `theme_preference` could be an empty string.
|
||||
const themePreference = user?.theme_preference || DEFAULT_THEME;
|
||||
const themePreference =
|
||||
appearanceSettingsQuery.data?.theme_preference || DEFAULT_THEME;
|
||||
// The janky casting here is find because of the much more type safe fallback
|
||||
// We need to support `themePreference` being wrong anyway because the database
|
||||
// value could be anything, like an empty string.
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
*/
|
||||
import { act, renderHook, screen } from "@testing-library/react";
|
||||
import { GlobalSnackbar } from "components/GlobalSnackbar/GlobalSnackbar";
|
||||
import { ThemeProvider } from "contexts/ThemeProvider";
|
||||
import { ThemeOverride } from "contexts/ThemeProvider";
|
||||
import themes, { DEFAULT_THEME } from "theme";
|
||||
import {
|
||||
COPY_FAILED_MESSAGE,
|
||||
HTTP_FALLBACK_DATA_ID,
|
||||
@@ -121,10 +122,10 @@ function renderUseClipboard<TInput extends UseClipboardInput>(inputs: TInput) {
|
||||
initialProps: inputs,
|
||||
wrapper: ({ children }) => (
|
||||
// Need ThemeProvider because GlobalSnackbar uses theme
|
||||
<ThemeProvider>
|
||||
<ThemeOverride theme={themes[DEFAULT_THEME]}>
|
||||
{children}
|
||||
<GlobalSnackbar />
|
||||
</ThemeProvider>
|
||||
</ThemeOverride>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
MockEntitlements,
|
||||
MockExperiments,
|
||||
MockUser,
|
||||
MockUserAppearanceSettings,
|
||||
} from "testHelpers/entities";
|
||||
import {
|
||||
DEFAULT_METADATA_KEY,
|
||||
@@ -38,6 +39,7 @@ const mockDataForTags = {
|
||||
entitlements: MockEntitlements,
|
||||
experiments: MockExperiments,
|
||||
user: MockUser,
|
||||
userAppearance: MockUserAppearanceSettings,
|
||||
regions: MockRegions,
|
||||
} as const satisfies Record<MetadataKey, MetadataValue>;
|
||||
|
||||
@@ -66,6 +68,10 @@ const emptyMetadata: RuntimeHtmlMetadata = {
|
||||
available: false,
|
||||
value: undefined,
|
||||
},
|
||||
userAppearance: {
|
||||
available: false,
|
||||
value: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
const populatedMetadata: RuntimeHtmlMetadata = {
|
||||
@@ -93,6 +99,10 @@ const populatedMetadata: RuntimeHtmlMetadata = {
|
||||
available: true,
|
||||
value: MockUser,
|
||||
},
|
||||
userAppearance: {
|
||||
available: true,
|
||||
value: MockUserAppearanceSettings,
|
||||
},
|
||||
};
|
||||
|
||||
function seedInitialMetadata(metadataKey: string): () => void {
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
Experiments,
|
||||
Region,
|
||||
User,
|
||||
UserAppearanceSettings,
|
||||
} from "api/typesGenerated";
|
||||
import { useMemo, useSyncExternalStore } from "react";
|
||||
|
||||
@@ -25,6 +26,7 @@ type AvailableMetadata = Readonly<{
|
||||
user: User;
|
||||
experiments: Experiments;
|
||||
appearance: AppearanceConfig;
|
||||
userAppearance: UserAppearanceSettings;
|
||||
entitlements: Entitlements;
|
||||
regions: readonly Region[];
|
||||
"build-info": BuildInfoResponse;
|
||||
@@ -83,6 +85,8 @@ export class MetadataManager implements MetadataManagerApi {
|
||||
this.metadata = {
|
||||
user: this.registerValue<User>("user"),
|
||||
appearance: this.registerValue<AppearanceConfig>("appearance"),
|
||||
userAppearance:
|
||||
this.registerValue<UserAppearanceSettings>("userAppearance"),
|
||||
entitlements: this.registerValue<Entitlements>("entitlements"),
|
||||
experiments: this.registerValue<Experiments>("experiments"),
|
||||
"build-info": this.registerValue<BuildInfoResponse>("build-info"),
|
||||
|
||||
@@ -34,7 +34,7 @@ describe("appearance page", () => {
|
||||
|
||||
// Check if the API was called correctly
|
||||
expect(API.updateAppearanceSettings).toBeCalledTimes(1);
|
||||
expect(API.updateAppearanceSettings).toHaveBeenCalledWith("me", {
|
||||
expect(API.updateAppearanceSettings).toHaveBeenCalledWith({
|
||||
theme_preference: "light",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,19 +1,34 @@
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import { updateAppearanceSettings } from "api/queries/users";
|
||||
import { appearanceSettings } from "api/queries/users";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { useAuthenticated } from "contexts/auth/RequireAuth";
|
||||
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
|
||||
import type { FC } from "react";
|
||||
import { useMutation, useQueryClient } from "react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import { Section } from "../Section";
|
||||
import { AppearanceForm } from "./AppearanceForm";
|
||||
|
||||
export const AppearancePage: FC = () => {
|
||||
const { user: me } = useAuthenticated();
|
||||
const queryClient = useQueryClient();
|
||||
const updateAppearanceSettingsMutation = useMutation(
|
||||
updateAppearanceSettings("me", queryClient),
|
||||
updateAppearanceSettings(queryClient),
|
||||
);
|
||||
|
||||
const { metadata } = useEmbeddedMetadata();
|
||||
const appearanceSettingsQuery = useQuery(
|
||||
appearanceSettings(metadata.userAppearance),
|
||||
);
|
||||
|
||||
if (appearanceSettingsQuery.isLoading) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
if (!appearanceSettingsQuery.data) {
|
||||
return <ErrorAlert error={appearanceSettingsQuery.error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section
|
||||
@@ -30,7 +45,9 @@ export const AppearancePage: FC = () => {
|
||||
<AppearanceForm
|
||||
isUpdating={updateAppearanceSettingsMutation.isLoading}
|
||||
error={updateAppearanceSettingsMutation.error}
|
||||
initialValues={{ theme_preference: me.theme_preference }}
|
||||
initialValues={{
|
||||
theme_preference: appearanceSettingsQuery.data.theme_preference,
|
||||
}}
|
||||
onSubmit={updateAppearanceSettingsMutation.mutateAsync}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { API } from "api/api";
|
||||
import { workspaceByOwnerAndName } from "api/queries/workspaces";
|
||||
import { GlobalSnackbar } from "components/GlobalSnackbar/GlobalSnackbar";
|
||||
import { ThemeProvider } from "contexts/ThemeProvider";
|
||||
import dayjs from "dayjs";
|
||||
import { http, HttpResponse } from "msw";
|
||||
import type { FC } from "react";
|
||||
import { QueryClient, QueryClientProvider, useQuery } from "react-query";
|
||||
import { RouterProvider, createMemoryRouter } from "react-router-dom";
|
||||
import { useQuery } from "react-query";
|
||||
import { MockTemplate, MockWorkspace } from "testHelpers/entities";
|
||||
import { render } from "testHelpers/renderHelpers";
|
||||
import { server } from "testHelpers/server";
|
||||
import { WorkspaceScheduleControls } from "./WorkspaceScheduleControls";
|
||||
|
||||
@@ -45,16 +43,7 @@ const renderScheduleControls = async () => {
|
||||
});
|
||||
}),
|
||||
);
|
||||
render(
|
||||
<ThemeProvider>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<RouterProvider
|
||||
router={createMemoryRouter([{ path: "/", element: <Wrapper /> }])}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
<GlobalSnackbar />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
render(<Wrapper />);
|
||||
await screen.findByTestId("schedule-controls");
|
||||
expect(screen.getByText("Stop in 3 hours")).toBeInTheDocument();
|
||||
};
|
||||
|
||||
@@ -495,7 +495,6 @@ export const MockUser: TypesGen.User = {
|
||||
avatar_url: "https://avatars.githubusercontent.com/u/95932066?s=200&v=4",
|
||||
last_seen_at: "",
|
||||
login_type: "password",
|
||||
theme_preference: "",
|
||||
name: "",
|
||||
};
|
||||
|
||||
@@ -516,7 +515,6 @@ export const MockUser2: TypesGen.User = {
|
||||
avatar_url: "",
|
||||
last_seen_at: "2022-09-14T19:12:21Z",
|
||||
login_type: "oidc",
|
||||
theme_preference: "",
|
||||
name: "Mock User The Second",
|
||||
};
|
||||
|
||||
@@ -532,10 +530,13 @@ export const SuspendedMockUser: TypesGen.User = {
|
||||
avatar_url: "",
|
||||
last_seen_at: "",
|
||||
login_type: "password",
|
||||
theme_preference: "",
|
||||
name: "",
|
||||
};
|
||||
|
||||
export const MockUserAppearanceSettings: TypesGen.UserAppearanceSettings = {
|
||||
theme_preference: "dark",
|
||||
};
|
||||
|
||||
export const MockOrganizationMember: TypesGen.OrganizationMemberWithUserData = {
|
||||
organization_id: MockOrganization.id,
|
||||
user_id: MockUser.id,
|
||||
|
||||
@@ -162,6 +162,9 @@ export const handlers = [
|
||||
http.get("/api/v2/users/me", () => {
|
||||
return HttpResponse.json(M.MockUser);
|
||||
}),
|
||||
http.get("/api/v2/users/me/appearance", () => {
|
||||
return HttpResponse.json(M.MockUserAppearanceSettings);
|
||||
}),
|
||||
http.get("/api/v2/users/me/keys", () => {
|
||||
return HttpResponse.json(M.MockAPIKey);
|
||||
}),
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from "@testing-library/react";
|
||||
import { AppProviders } from "App";
|
||||
import type { ProxyProvider } from "contexts/ProxyContext";
|
||||
import { ThemeProvider } from "contexts/ThemeProvider";
|
||||
import { ThemeOverride } from "contexts/ThemeProvider";
|
||||
import { RequireAuth } from "contexts/auth/RequireAuth";
|
||||
import { DashboardLayout } from "modules/dashboard/DashboardLayout";
|
||||
import type { DashboardProvider } from "modules/dashboard/DashboardProvider";
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
RouterProvider,
|
||||
createMemoryRouter,
|
||||
} from "react-router-dom";
|
||||
import themes, { DEFAULT_THEME } from "theme";
|
||||
import { MockUser } from "./entities";
|
||||
|
||||
export function createTestQueryClient() {
|
||||
@@ -245,6 +246,8 @@ export const waitForLoaderToBeRemoved = async (): Promise<void> => {
|
||||
|
||||
export const renderComponent = (component: React.ReactElement) => {
|
||||
return testingLibraryRender(component, {
|
||||
wrapper: ({ children }) => <ThemeProvider>{children}</ThemeProvider>,
|
||||
wrapper: ({ children }) => (
|
||||
<ThemeOverride theme={themes[DEFAULT_THEME]}>{children}</ThemeOverride>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user