Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions apps/apollo-vertex/.oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"typescript/explicit-module-boundary-types": "off",
"react/jsx-filename-extension": ["error", { "extensions": [".tsx"] }],
"import/no-unassigned-import": ["error", { "allow": ["**/*.css"] }],
"eslint/no-warning-comments": "off",
"eslint/no-alert": "error",
"eslint/no-console": "error",
"eslint/no-empty": "error",
Expand All @@ -33,7 +34,7 @@
"eslint/no-sequences": "error",
"eslint/no-undefined": "error",
"eslint/no-var": "error",
"eslint/no-void": "error",
"eslint/no-void": ["error", { "allowAsStatement": true }],
"import/no-amd": "error",
"import/no-commonjs": "error",
"import/no-cycle": "error",
Expand All @@ -49,7 +50,9 @@
"react/no-unknown-property": "error",
"react/only-export-components": "error",
"typescript/use-unknown-in-catch-callback-variable": "error",
"typescript/promise-function-async": "error",
"typescript/no-confusing-void-expression": "off",
"typescript/strict-boolean-expressions": "off",
"typescript/promise-function-async": "off",
"typescript/prefer-literal-enum-member": "error",
"typescript/no-var-requires": "error",
"typescript/no-require-imports": "error",
Expand Down
15 changes: 11 additions & 4 deletions apps/apollo-vertex/app/components/theme-switcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import {
SelectTrigger,
SelectValue,
} from "@/registry/select/select";
import { hasCustomTheme, type ThemeName, themes } from "../themes";
import {
hasCustomTheme,
type ThemeName,
ThemeNameSchema,
themes,
} from "../themes";

const THEME_STORAGE_KEY = "apollo-vertex-theme";

Expand All @@ -22,8 +27,9 @@ export function ThemeSwitcher() {

// Load saved theme from localStorage
const savedTheme = localStorage.getItem(THEME_STORAGE_KEY);
if (savedTheme && (savedTheme in themes || savedTheme === "custom")) {
setSelectedTheme(savedTheme as ThemeName);
const { data, success } = ThemeNameSchema.safeParse(savedTheme);
if (success) {
setSelectedTheme(data);
}

// Listen for custom theme changes
Expand All @@ -42,7 +48,8 @@ export function ThemeSwitcher() {
}, []);

const handleThemeChange = (value: string) => {
const themeName = value as ThemeName;
const { data: themeName, success } = ThemeNameSchema.safeParse(value);
if (!success) return;
setSelectedTheme(themeName);
localStorage.setItem(THEME_STORAGE_KEY, themeName);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,8 @@ export function CollapsibleAlertExample() {
useEffect(() => {
const el = measureRef.current;
if (!el) return;
const firstChild = el.firstElementChild as HTMLElement | null;
if (!firstChild) return;
const firstChild = el.firstElementChild;
if (!(firstChild instanceof HTMLElement)) return;
const singleRow = firstChild.offsetHeight + 6;
setRowHeight(singleRow);
setOverflows(el.scrollHeight > singleRow);
Expand Down
15 changes: 12 additions & 3 deletions apps/apollo-vertex/app/themes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ThemeConfig } from "../registry/shell/shell-theme-provider";
import { z } from "zod";
import { type ThemeConfig, ThemeConfigSchema } from "@/lib/schemas/theme";

export const themes = {
default: {
Expand Down Expand Up @@ -202,7 +203,12 @@ export const themes = {
},
} as const;

export type ThemeName = keyof typeof themes | "custom";
const themeNames = [...Object.keys(themes), "custom"] as const;
export const ThemeNameSchema = z.enum(
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion)
themeNames as unknown as [string, ...string[]],
);
export type ThemeName = z.infer<typeof ThemeNameSchema>;

const CUSTOM_THEME_STORAGE_KEY = "apollo-vertex-custom-theme";

Expand All @@ -213,7 +219,10 @@ export function getCustomTheme(): ThemeConfig | null {
if (!savedTheme) return null;

try {
return JSON.parse(savedTheme);
const { data, success } = ThemeConfigSchema.safeParse(
JSON.parse(savedTheme),
);
return success ? data : null;
} catch {
return null;
}
Expand Down
61 changes: 37 additions & 24 deletions apps/apollo-vertex/app/themes/customize/theme-customizer.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"use client";
import { type ChangeEvent, startTransition, useEffect, useState } from "react";
import { type ThemeConfig, ThemeConfigSchema } from "@/lib/schemas/theme";
import { Button } from "@/registry/button/button";
import { Card } from "@/registry/card/card";
import { Input } from "@/registry/input/input";
import { Label } from "@/registry/label/label";
import type { ThemeConfig } from "@/registry/shell/shell-theme-provider";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/registry/tabs/tabs";
import { themes } from "../../themes";

Expand Down Expand Up @@ -116,8 +116,10 @@ export function ThemeCustomizer() {
const savedTheme = localStorage.getItem(CUSTOM_THEME_STORAGE_KEY);
if (savedTheme) {
try {
const parsed = JSON.parse(savedTheme);
setCustomTheme(parsed);
const { data, success } = ThemeConfigSchema.safeParse(
JSON.parse(savedTheme),
);
if (success) setCustomTheme(data);
} catch {
// Ignore invalid saved theme
}
Expand Down Expand Up @@ -179,24 +181,32 @@ export function ThemeCustomizer() {
const file = event.target.files?.[0];
if (!file) return;

file.text().then((content) => {
try {
const imported = JSON.parse(content);
setCustomTheme(imported);

localStorage.setItem(
CUSTOM_THEME_STORAGE_KEY,
JSON.stringify(imported),
);
localStorage.setItem(THEME_STORAGE_KEY, "custom");

window.dispatchEvent(
new CustomEvent("theme-change", { detail: "custom" }),
);
} catch {
// Invalid theme file
}
});
void file
.text()
.then((content) => {
try {
const { data: imported, success } = ThemeConfigSchema.safeParse(
JSON.parse(content),
);
if (!success) return;
setCustomTheme(imported);

localStorage.setItem(
CUSTOM_THEME_STORAGE_KEY,
JSON.stringify(imported),
);
localStorage.setItem(THEME_STORAGE_KEY, "custom");

window.dispatchEvent(
new CustomEvent("theme-change", { detail: "custom" }),
);
} catch {
// Invalid theme file
}
})
.catch(() => {
// File read failed
});
};

const handleLoadPreset = (presetName: keyof typeof themes) => {
Expand Down Expand Up @@ -253,6 +263,7 @@ export function ThemeCustomizer() {
key={key}
variant="outline"
size="sm"
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Object.entries loses keyof; themes is a closed object
onClick={() => handleLoadPreset(key as keyof typeof themes)}
>
{theme.name}
Expand All @@ -263,7 +274,9 @@ export function ThemeCustomizer() {

<Tabs
value={activeTab}
onValueChange={(v) => setActiveTab(v as "light" | "dark")}
onValueChange={(v) => {
if (v === "light" || v === "dark") setActiveTab(v);
}}
>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="light">Light Mode</TabsTrigger>
Expand All @@ -274,7 +287,7 @@ export function ThemeCustomizer() {
<div className="grid grid-cols-2 gap-4">
{colorFields.map(({ key, label }) => {
const themeValue =
currentModeTheme?.[key as keyof typeof currentModeTheme] ||
currentModeTheme?.[key as keyof typeof currentModeTheme] ??
"";
const isOklch = themeValue.includes("oklch");
const hexValue = isOklch
Expand Down Expand Up @@ -316,7 +329,7 @@ export function ThemeCustomizer() {
<div className="grid grid-cols-2 gap-4">
{colorFields.map(({ key, label }) => {
const themeValue =
currentModeTheme?.[key as keyof typeof currentModeTheme] ||
currentModeTheme?.[key as keyof typeof currentModeTheme] ??
"";
const isOklch = themeValue.includes("oklch");
const hexValue = isOklch
Expand Down
34 changes: 18 additions & 16 deletions apps/apollo-vertex/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@ const STORAGE_KEYS = {
STATE: "uipath_state",
} as const;

const TokenDataSchema = z.object({
const TokenResponseSchema = z.object({
access_token: z.string(),
id_token: z.string(),
refresh_token: z.string(),
expires_in: z.number(),
token_type: z.string(),
});

const TokenDataSchema = TokenResponseSchema.extend({
expiresAt: z.number(),
});

Expand Down Expand Up @@ -74,19 +77,15 @@ const fetchTokenData = async (baseUrl: string, body?: URLSearchParams) => {
throw new Error(`Token refresh failed: ${errorText}`);
}

const data = await response.json();
const responseData = TokenResponseSchema.parse(await response.json());

return TokenDataSchema.parse({
access_token: data.access_token,
id_token: data.id_token,
refresh_token: data.refresh_token,
expires_in: data.expires_in,
token_type: data.token_type,
expiresAt: Date.now() + data.expires_in * 1000,
...responseData,
expiresAt: Date.now() + responseData.expires_in * 1000,
});
};

const refreshAccessToken = async (
const refreshAccessToken = (
refreshToken: string,
clientId: string,
baseUrl: string,
Expand All @@ -97,7 +96,7 @@ const refreshAccessToken = async (
client_id: clientId,
});

return await fetchTokenData(baseUrl, body);
return fetchTokenData(baseUrl, body);
};

const refreshTokenIfNeeded = async (
Expand All @@ -123,7 +122,7 @@ const refreshTokenIfNeeded = async (
}
};

const exchangeCodeForToken = async (
const exchangeCodeForToken = (
code: string,
codeVerifier: string,
clientId: string,
Expand All @@ -137,7 +136,7 @@ const exchangeCodeForToken = async (
code_verifier: codeVerifier,
});

return await fetchTokenData(baseUrl, body);
return fetchTokenData(baseUrl, body);
};

const handleOAuthCallback = async (
Expand Down Expand Up @@ -180,7 +179,9 @@ const handleOAuthCallback = async (
window.history.replaceState({}, document.title, window.location.pathname);
} catch (authError) {
clearTokenData();
toast.error(`Authentication failed: ${authError}`);
const message =
authError instanceof Error ? authError.message : "Unknown error";
toast.error(`Authentication failed: ${message}`);
throw authError;
}
};
Expand All @@ -196,7 +197,7 @@ export const ensureValidToken = async (
if (isInOAuthCallback) {
try {
await handleOAuthCallback(clientId, baseUrl);
queryClient.invalidateQueries({ queryKey: TOKEN_QUERY_KEY });
await queryClient.invalidateQueries({ queryKey: TOKEN_QUERY_KEY });
} catch {
queryClient.setQueryData(TOKEN_QUERY_KEY, null);
}
Expand All @@ -207,8 +208,9 @@ export const ensureValidToken = async (
return null;
}

const parsed = JSON.parse(tokenDataStr);
const { data: tokenData, success } = TokenDataSchema.safeParse(parsed);
const { data: tokenData, success } = TokenDataSchema.safeParse(
JSON.parse(tokenDataStr),
);

if (!success) {
clearTokenData();
Expand Down
15 changes: 11 additions & 4 deletions apps/apollo-vertex/lib/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const localeLoaders = {
} as const satisfies Record<string, LocaleLoader>;

export type SupportedLocale = keyof typeof localeLoaders;
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Object.keys returns string[]; keyof narrowing is safe here since localeLoaders is a closed object
export const SUPPORTED_LOCALES = Object.keys(
localeLoaders,
) as SupportedLocale[];
Expand All @@ -33,16 +34,22 @@ export const configurei18n = async () => {
.use({
type: "backend",
read(language: string, _namespace: Namespace, callback: ReadCallback) {
const loadLocale = localeLoaders[language as SupportedLocale];

if (!loadLocale) {
if (!(language in localeLoaders)) {
callback(new Error(`Locale not found: ${language}`), null);
return;
}

// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- `in` guard above validates membership but TS can't narrow string to keyof
const loadLocale = localeLoaders[language as SupportedLocale];

loadLocale()
.then((module) => callback(null, module.default))
.catch((error) => callback(error, null));
.catch((error: unknown) =>
callback(
error instanceof Error ? error : new Error(String(error)),
null,
),
);
},
})
.use(initReactI18next)
Expand Down
Loading
Loading