fix: temporarily lock tasker dark theme
This commit is contained in:
parent
3f86b73d5c
commit
6e74964853
|
|
@ -10,7 +10,7 @@ import { Links, Meta, Outlet, Scripts } from "react-router";
|
||||||
import type { LinksFunction } from "react-router";
|
import type { LinksFunction } from "react-router";
|
||||||
import { ThemeProvider } from "next-themes";
|
import { ThemeProvider } from "next-themes";
|
||||||
// plane imports
|
// plane imports
|
||||||
import { SITE_DESCRIPTION, SITE_NAME } from "@plane/constants";
|
import { NODEDC_FORCED_THEME, NODEDC_THEME_LOCK_ENABLED, SITE_DESCRIPTION, SITE_NAME, THEMES } from "@plane/constants";
|
||||||
import { cn } from "@plane/utils";
|
import { cn } from "@plane/utils";
|
||||||
// types
|
// types
|
||||||
// assets
|
// assets
|
||||||
|
|
@ -24,9 +24,12 @@ import { NodeDCSessionSync } from "@/components/auth-screens/nodedc-session-sync
|
||||||
import { CustomErrorComponent } from "./error";
|
import { CustomErrorComponent } from "./error";
|
||||||
import { AppProvider } from "./provider";
|
import { AppProvider } from "./provider";
|
||||||
// fonts
|
// fonts
|
||||||
|
// oxlint-disable-next-line import/no-unassigned-import
|
||||||
import "@fontsource-variable/inter";
|
import "@fontsource-variable/inter";
|
||||||
import interVariableWoff2 from "@fontsource-variable/inter/files/inter-latin-wght-normal.woff2?url";
|
import interVariableWoff2 from "@fontsource-variable/inter/files/inter-latin-wght-normal.woff2?url";
|
||||||
|
// oxlint-disable-next-line import/no-unassigned-import
|
||||||
import "@fontsource/material-symbols-rounded";
|
import "@fontsource/material-symbols-rounded";
|
||||||
|
// oxlint-disable-next-line import/no-unassigned-import
|
||||||
import "@fontsource/ibm-plex-mono";
|
import "@fontsource/ibm-plex-mono";
|
||||||
|
|
||||||
const APP_TITLE = "NODE.DC | Self-hosted task management workspace.";
|
const APP_TITLE = "NODE.DC | Self-hosted task management workspace.";
|
||||||
|
|
@ -110,15 +113,20 @@ export function Layout({ children }: { children: ReactNode }) {
|
||||||
const isSessionRecorderEnabled = parseInt(process.env.VITE_ENABLE_SESSION_RECORDER || "0");
|
const isSessionRecorderEnabled = parseInt(process.env.VITE_ENABLE_SESSION_RECORDER || "0");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning style={designConfigStyle}>
|
<html
|
||||||
|
lang="en"
|
||||||
|
data-theme={NODEDC_THEME_LOCK_ENABLED ? NODEDC_FORCED_THEME : undefined}
|
||||||
|
suppressHydrationWarning
|
||||||
|
style={designConfigStyle}
|
||||||
|
>
|
||||||
<head>
|
<head>
|
||||||
<meta charSet="utf-8" />
|
<meta charSet="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#fff" />
|
<meta name="theme-color" content={NODEDC_THEME_LOCK_ENABLED ? "#111113" : "#fff"} />
|
||||||
{/* Meta info for PWA */}
|
{/* Meta info for PWA */}
|
||||||
<meta name="application-name" content="NODE.DC" />
|
<meta name="application-name" content="NODE.DC" />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
<meta name="apple-mobile-web-app-status-bar-style" content={NODEDC_THEME_LOCK_ENABLED ? "black" : "default"} />
|
||||||
<meta name="apple-mobile-web-app-title" content={SITE_NAME} />
|
<meta name="apple-mobile-web-app-title" content={SITE_NAME} />
|
||||||
<meta name="format-detection" content="telephone=no" />
|
<meta name="format-detection" content="telephone=no" />
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
|
|
@ -128,7 +136,14 @@ export function Layout({ children }: { children: ReactNode }) {
|
||||||
<body suppressHydrationWarning>
|
<body suppressHydrationWarning>
|
||||||
<div id="context-menu-portal" />
|
<div id="context-menu-portal" />
|
||||||
<div id="editor-portal" />
|
<div id="editor-portal" />
|
||||||
<ThemeProvider themes={["light", "dark", "light-contrast", "dark-contrast", "custom"]} defaultTheme="system">
|
<ThemeProvider
|
||||||
|
attribute="data-theme"
|
||||||
|
themes={THEMES}
|
||||||
|
defaultTheme={NODEDC_THEME_LOCK_ENABLED ? NODEDC_FORCED_THEME : "system"}
|
||||||
|
forcedTheme={NODEDC_THEME_LOCK_ENABLED ? NODEDC_FORCED_THEME : undefined}
|
||||||
|
enableSystem={!NODEDC_THEME_LOCK_ENABLED}
|
||||||
|
disableTransitionOnChange
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
<Scripts />
|
<Scripts />
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { observer } from "mobx-react";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
// plane imports
|
// plane imports
|
||||||
import type { I_THEME_OPTION } from "@plane/constants";
|
import type { I_THEME_OPTION } from "@plane/constants";
|
||||||
import { THEME_OPTIONS } from "@plane/constants";
|
import { NODEDC_FORCED_THEME, NODEDC_THEME_LOCK_ENABLED, THEME_OPTIONS } from "@plane/constants";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { setPromiseToast } from "@plane/propel/toast";
|
import { setPromiseToast } from "@plane/propel/toast";
|
||||||
import { applyCustomTheme } from "@plane/utils";
|
import { applyCustomTheme } from "@plane/utils";
|
||||||
|
|
@ -35,18 +35,20 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
// derived values
|
// derived values
|
||||||
const currentTheme = useMemo(() => {
|
const currentTheme = useMemo(() => {
|
||||||
const userThemeOption = THEME_OPTIONS.find((t) => t.value === userProfile?.theme?.theme);
|
const themeValue = NODEDC_THEME_LOCK_ENABLED ? NODEDC_FORCED_THEME : userProfile?.theme?.theme;
|
||||||
return userThemeOption || null;
|
return THEME_OPTIONS.find((themeOption) => themeOption.value === themeValue) || null;
|
||||||
}, [userProfile?.theme?.theme]);
|
}, [userProfile?.theme?.theme]);
|
||||||
|
|
||||||
const handleThemeChange = useCallback(
|
const handleThemeChange = useCallback(
|
||||||
async (themeOption: I_THEME_OPTION) => {
|
async (themeOption: I_THEME_OPTION) => {
|
||||||
try {
|
try {
|
||||||
setTheme(themeOption.value);
|
const nextTheme = NODEDC_THEME_LOCK_ENABLED ? NODEDC_FORCED_THEME : themeOption.value;
|
||||||
|
setTheme(nextTheme);
|
||||||
|
|
||||||
// If switching to custom theme and user has saved custom colors, apply them immediately
|
// If switching to custom theme and user has saved custom colors, apply them immediately
|
||||||
if (
|
if (
|
||||||
themeOption.value === "custom" &&
|
!NODEDC_THEME_LOCK_ENABLED &&
|
||||||
|
nextTheme === "custom" &&
|
||||||
userProfile?.theme?.primary &&
|
userProfile?.theme?.primary &&
|
||||||
userProfile?.theme?.background &&
|
userProfile?.theme?.background &&
|
||||||
userProfile?.theme?.darkPalette !== undefined
|
userProfile?.theme?.darkPalette !== undefined
|
||||||
|
|
@ -58,7 +60,7 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatePromise = updateUserTheme({ theme: themeOption.value });
|
const updatePromise = updateUserTheme({ theme: nextTheme });
|
||||||
setPromiseToast(updatePromise, {
|
setPromiseToast(updatePromise, {
|
||||||
loading: "Updating theme...",
|
loading: "Updating theme...",
|
||||||
success: {
|
success: {
|
||||||
|
|
@ -96,7 +98,7 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: {
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{userProfile.theme?.theme === "custom" && <CustomThemeSelector />}
|
{!NODEDC_THEME_LOCK_ENABLED && userProfile.theme?.theme === "custom" && <CustomThemeSelector />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { observer } from "mobx-react";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
// plane imports
|
// plane imports
|
||||||
import type { I_THEME_OPTION } from "@plane/constants";
|
import type { I_THEME_OPTION } from "@plane/constants";
|
||||||
import { THEME_OPTIONS } from "@plane/constants";
|
import { NODEDC_FORCED_THEME, NODEDC_THEME_LOCK_ENABLED, THEME_OPTIONS } from "@plane/constants";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { setPromiseToast } from "@plane/propel/toast";
|
import { setPromiseToast } from "@plane/propel/toast";
|
||||||
// components
|
// components
|
||||||
|
|
@ -34,15 +34,16 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
// derived values
|
// derived values
|
||||||
const currentTheme = useMemo(() => {
|
const currentTheme = useMemo(() => {
|
||||||
const userThemeOption = THEME_OPTIONS.find((t) => t.value === userProfile?.theme?.theme);
|
const themeValue = NODEDC_THEME_LOCK_ENABLED ? NODEDC_FORCED_THEME : userProfile?.theme?.theme;
|
||||||
return userThemeOption || null;
|
return THEME_OPTIONS.find((themeOption) => themeOption.value === themeValue) || null;
|
||||||
}, [userProfile?.theme?.theme]);
|
}, [userProfile?.theme?.theme]);
|
||||||
|
|
||||||
const handleThemeChange = useCallback(
|
const handleThemeChange = useCallback(
|
||||||
(themeOption: I_THEME_OPTION) => {
|
(themeOption: I_THEME_OPTION) => {
|
||||||
try {
|
try {
|
||||||
setTheme(themeOption.value);
|
const nextTheme = NODEDC_THEME_LOCK_ENABLED ? NODEDC_FORCED_THEME : themeOption.value;
|
||||||
const updatePromise = updateUserTheme({ theme: themeOption.value });
|
setTheme(nextTheme);
|
||||||
|
const updatePromise = updateUserTheme({ theme: nextTheme });
|
||||||
setPromiseToast(updatePromise, {
|
setPromiseToast(updatePromise, {
|
||||||
loading: "Updating theme...",
|
loading: "Updating theme...",
|
||||||
success: {
|
success: {
|
||||||
|
|
@ -58,7 +59,7 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: {
|
||||||
console.error("Error updating theme:", error);
|
console.error("Error updating theme:", error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[updateUserTheme]
|
[setTheme, updateUserTheme]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!userProfile) return null;
|
if (!userProfile) return null;
|
||||||
|
|
@ -70,7 +71,7 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: {
|
||||||
description={t(props.option.description)}
|
description={t(props.option.description)}
|
||||||
control={<ThemeSwitch value={currentTheme} onChange={handleThemeChange} />}
|
control={<ThemeSwitch value={currentTheme} onChange={handleThemeChange} />}
|
||||||
/>
|
/>
|
||||||
{userProfile.theme?.theme === "custom" && <CustomThemeSelector />}
|
{!NODEDC_THEME_LOCK_ENABLED && userProfile.theme?.theme === "custom" && <CustomThemeSelector />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
// plane imports
|
// plane imports
|
||||||
import type { I_THEME_OPTION } from "@plane/constants";
|
import type { I_THEME_OPTION } from "@plane/constants";
|
||||||
import { THEME_OPTIONS } from "@plane/constants";
|
import { NODEDC_ACTIVE_THEME_OPTIONS } from "@plane/constants";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
// constants
|
// constants
|
||||||
import { SelectionDropdown } from "@/components/common/selection-dropdown";
|
import { SelectionDropdown } from "@/components/common/selection-dropdown";
|
||||||
|
|
@ -17,34 +17,34 @@ type Props = {
|
||||||
onChange: (value: I_THEME_OPTION) => void;
|
onChange: (value: I_THEME_OPTION) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderThemeSwatch = (themeOption: I_THEME_OPTION) => (
|
||||||
|
<div
|
||||||
|
className="relative flex h-4 w-4 rotate-45 transform items-center justify-center rounded-full border-1"
|
||||||
|
style={{
|
||||||
|
borderColor: themeOption.icon.border,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="h-full w-1/2 rounded-l-full"
|
||||||
|
style={{
|
||||||
|
background: themeOption.icon.color1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="h-full w-1/2 rounded-r-full border-l"
|
||||||
|
style={{
|
||||||
|
borderLeftColor: themeOption.icon.border,
|
||||||
|
background: themeOption.icon.color2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
export function ThemeSwitch(props: Props) {
|
export function ThemeSwitch(props: Props) {
|
||||||
const { value, onChange } = props;
|
const { value, onChange } = props;
|
||||||
// translation
|
// translation
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const renderThemeSwatch = (themeOption: I_THEME_OPTION) => (
|
|
||||||
<div
|
|
||||||
className="relative flex h-4 w-4 rotate-45 transform items-center justify-center rounded-full border-1"
|
|
||||||
style={{
|
|
||||||
borderColor: themeOption.icon.border,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="h-full w-1/2 rounded-l-full"
|
|
||||||
style={{
|
|
||||||
background: themeOption.icon.color1,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="h-full w-1/2 rounded-r-full border-l"
|
|
||||||
style={{
|
|
||||||
borderLeftColor: themeOption.icon.border,
|
|
||||||
background: themeOption.icon.color2,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectionDropdown
|
<SelectionDropdown
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
|
|
@ -59,7 +59,7 @@ export function ThemeSwitch(props: Props) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
menuButtonWrapperClassName="flex w-full items-center justify-between rounded-full border border-subtle-1 px-3 py-2 text-13"
|
menuButtonWrapperClassName="flex w-full items-center justify-between rounded-full border border-subtle-1 px-3 py-2 text-13"
|
||||||
options={THEME_OPTIONS.map((themeOption) => ({
|
options={NODEDC_ACTIVE_THEME_OPTIONS.map((themeOption) => ({
|
||||||
key: themeOption.value,
|
key: themeOption.value,
|
||||||
isChecked: value?.value === themeOption.value,
|
isChecked: value?.value === themeOption.value,
|
||||||
onClick: () => onChange(themeOption),
|
onClick: () => onChange(themeOption),
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import React, { useState } from "react";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { ArrowRightLeft } from "lucide-react";
|
import { ArrowRightLeft } from "lucide-react";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
import { NODEDC_FORCED_THEME, NODEDC_THEME_LOCK_ENABLED } from "@plane/constants";
|
||||||
// ui
|
// ui
|
||||||
import { Button } from "@plane/propel/button";
|
import { Button } from "@plane/propel/button";
|
||||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||||
|
|
@ -42,9 +43,10 @@ export function SwitchAccountModal(props: Props) {
|
||||||
|
|
||||||
await signOut()
|
await signOut()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setTheme("system");
|
setTheme(NODEDC_THEME_LOCK_ENABLED ? NODEDC_FORCED_THEME : "system");
|
||||||
router.push("/");
|
router.push("/");
|
||||||
handleClose();
|
handleClose();
|
||||||
|
return undefined;
|
||||||
})
|
})
|
||||||
.catch(() =>
|
.catch(() =>
|
||||||
setToast({
|
setToast({
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { useCallback } from "react";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { Calendar, Earth, Languages, Palette } from "lucide-react";
|
import { Calendar, Earth, Languages, Palette } from "lucide-react";
|
||||||
// plane imports
|
// plane imports
|
||||||
|
import { NODEDC_FORCED_THEME, NODEDC_THEME_LOCK_ENABLED } from "@plane/constants";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { setToast, TOAST_TYPE } from "@plane/propel/toast";
|
import { setToast, TOAST_TYPE } from "@plane/propel/toast";
|
||||||
import type { EStartOfTheWeek, TUserProfile } from "@plane/types";
|
import type { EStartOfTheWeek, TUserProfile } from "@plane/types";
|
||||||
|
|
@ -29,8 +30,9 @@ export const usePowerKPreferencesCommands = (): TPowerKCommandConfig[] => {
|
||||||
|
|
||||||
const handleUpdateTheme = useCallback(
|
const handleUpdateTheme = useCallback(
|
||||||
async (newTheme: string) => {
|
async (newTheme: string) => {
|
||||||
setTheme(newTheme);
|
const nextTheme = NODEDC_THEME_LOCK_ENABLED ? NODEDC_FORCED_THEME : newTheme;
|
||||||
return updateUserTheme({ theme: newTheme })
|
setTheme(nextTheme);
|
||||||
|
return updateUserTheme({ theme: nextTheme })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.SUCCESS,
|
type: TOAST_TYPE.SUCCESS,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import React, { useEffect, useState } from "react";
|
||||||
import { Command } from "cmdk";
|
import { Command } from "cmdk";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// plane imports
|
// plane imports
|
||||||
import { THEME_OPTIONS } from "@plane/constants";
|
import { NODEDC_ACTIVE_THEME_OPTIONS } from "@plane/constants";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
// local imports
|
// local imports
|
||||||
import { PowerKModalCommandItem } from "../../modal/command-item";
|
import { PowerKModalCommandItem } from "../../modal/command-item";
|
||||||
|
|
@ -33,7 +33,7 @@ export const PowerKPreferencesThemesMenu = observer(function PowerKPreferencesTh
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Command.Group>
|
<Command.Group>
|
||||||
{THEME_OPTIONS.map((theme) => (
|
{NODEDC_ACTIVE_THEME_OPTIONS.map((theme) => (
|
||||||
<PowerKModalCommandItem key={theme.value} onSelect={() => onSelect(theme.value)} label={t(theme.i18n_label)} />
|
<PowerKModalCommandItem key={theme.value} onSelect={() => onSelect(theme.value)} label={t(theme.i18n_label)} />
|
||||||
))}
|
))}
|
||||||
</Command.Group>
|
</Command.Group>
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,10 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const workspaceProjectGroups = projectAccessPayload?.workspaces ?? [];
|
const workspaceProjectGroups = useMemo(
|
||||||
|
() => projectAccessPayload?.workspaces ?? [],
|
||||||
|
[projectAccessPayload?.workspaces]
|
||||||
|
);
|
||||||
const projectOptions = useMemo(() => flattenProjectAccessOptions(workspaceProjectGroups), [workspaceProjectGroups]);
|
const projectOptions = useMemo(() => flattenProjectAccessOptions(workspaceProjectGroups), [workspaceProjectGroups]);
|
||||||
const effectiveSelectedProjectKey = selectedProjectKey || projectOptions[0]?.key || "";
|
const effectiveSelectedProjectKey = selectedProjectKey || projectOptions[0]?.key || "";
|
||||||
const setupCards = useMemo(
|
const setupCards = useMemo(
|
||||||
|
|
@ -735,6 +738,10 @@ function CodexConnectionGuide(props: TCodexConnectionGuideProps) {
|
||||||
В строке <code>Authorization</code> замените только <code>ВАШ_УНИКАЛЬНЫЙ_ТОКЕН</code> на значение из{" "}
|
В строке <code>Authorization</code> замените только <code>ВАШ_УНИКАЛЬНЫЙ_ТОКЕН</code> на значение из{" "}
|
||||||
<code>Agent token</code>. <code>Bearer</code> и <code>ndcag_</code> оставьте как есть.
|
<code>Agent token</code>. <code>Bearer</code> и <code>ndcag_</code> оставьте как есть.
|
||||||
</p>
|
</p>
|
||||||
|
<p className="mt-3">
|
||||||
|
Не используйте <code>bearer_token_env_var</code>, если переменная окружения не добавлена в macOS launchd:
|
||||||
|
Codex Desktop не увидит обычный <code>export</code> из shell-профиля.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="nodedc-settings-input min-h-36 px-4 py-4 text-13 leading-5 text-secondary">
|
<div className="nodedc-settings-input min-h-36 px-4 py-4 text-13 leading-5 text-secondary">
|
||||||
|
|
@ -743,6 +750,10 @@ function CodexConnectionGuide(props: TCodexConnectionGuideProps) {
|
||||||
Сохраните <code>config.toml</code> и перезапустите локальный Codex. После старта попросите Codex проверить
|
Сохраните <code>config.toml</code> и перезапустите локальный Codex. После старта попросите Codex проверить
|
||||||
доступные проекты через <code>tasker_list_projects</code>.
|
доступные проекты через <code>tasker_list_projects</code>.
|
||||||
</p>
|
</p>
|
||||||
|
<p className="mt-3">
|
||||||
|
Если токен или endpoint временно недоступны, чат Codex все равно откроется, но MCP tools появятся только
|
||||||
|
после исправления подключения.
|
||||||
|
</p>
|
||||||
<p className="mt-3">Правила работы и grants Codex получит сам через MCP по токену.</p>
|
<p className="mt-3">Правила работы и grants Codex получит сам через MCP по токену.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -775,13 +786,13 @@ function buildCodexConfigSnippet(endpoint: string): string {
|
||||||
return `[mcp_servers.${CODEX_MCP_SERVER_NAME}]
|
return `[mcp_servers.${CODEX_MCP_SERVER_NAME}]
|
||||||
url = "${endpoint}"
|
url = "${endpoint}"
|
||||||
enabled = true
|
enabled = true
|
||||||
required = true
|
required = false
|
||||||
startup_timeout_sec = 20
|
startup_timeout_sec = 20
|
||||||
tool_timeout_sec = 60
|
tool_timeout_sec = 60
|
||||||
|
|
||||||
[mcp_servers.${CODEX_MCP_SERVER_NAME}.headers]
|
[mcp_servers.${CODEX_MCP_SERVER_NAME}.headers]
|
||||||
Authorization = "Bearer ndcag_ВАШ_УНИКАЛЬНЫЙ_ТОКЕН"
|
Authorization = "Bearer ndcag_ВАШ_УНИКАЛЬНЫЙ_ТОКЕН"
|
||||||
Accept = "application/json, text/event-stream"
|
Accept = "application/json"
|
||||||
"MCP-Protocol-Version" = "2025-06-18"`;
|
"MCP-Protocol-Version" = "2025-06-18"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -995,6 +1006,7 @@ function mergeTokens(primaryTokens: TCodexAgentToken[], secondaryTokens: TCodexA
|
||||||
const tokensById = new Map<string, TCodexAgentToken>();
|
const tokensById = new Map<string, TCodexAgentToken>();
|
||||||
for (const token of primaryTokens) tokensById.set(token.id, token);
|
for (const token of primaryTokens) tokensById.set(token.id, token);
|
||||||
for (const token of secondaryTokens) tokensById.set(token.id, tokensById.get(token.id) ?? token);
|
for (const token of secondaryTokens) tokensById.set(token.id, tokensById.get(token.id) ?? token);
|
||||||
|
// oxlint-disable-next-line unicorn/no-array-sort
|
||||||
return Array.from(tokensById.values()).sort(
|
return Array.from(tokensById.values()).sort(
|
||||||
(leftToken, rightToken) => new Date(rightToken.created_at).getTime() - new Date(leftToken.created_at).getTime()
|
(leftToken, rightToken) => new Date(rightToken.created_at).getTime() - new Date(leftToken.created_at).getTime()
|
||||||
);
|
);
|
||||||
|
|
@ -1072,13 +1084,16 @@ function buildProjectGrantPayload(grantKeys: string[]): { workspace_slug: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGrantedProjectKeys(grants: TCodexAgentGrant[]): string[] {
|
function getGrantedProjectKeys(grants: TCodexAgentGrant[]): string[] {
|
||||||
return [
|
const grantedProjectKeys = [
|
||||||
...new Set(
|
...new Set(
|
||||||
grants
|
grants
|
||||||
.filter((grant) => grant.project_id)
|
.filter((grant) => grant.project_id)
|
||||||
.map((grant) => buildProjectGrantKey(grant.workspace_slug, String(grant.project_id)))
|
.map((grant) => buildProjectGrantKey(grant.workspace_slug, String(grant.project_id)))
|
||||||
),
|
),
|
||||||
].sort();
|
];
|
||||||
|
|
||||||
|
// oxlint-disable-next-line unicorn/no-array-sort
|
||||||
|
return grantedProjectKeys.sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
type TMergeAgentGrantsOptions = {
|
type TMergeAgentGrantsOptions = {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { useEffect, useRef } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
|
import { NODEDC_FORCED_THEME, NODEDC_THEME_LOCK_ENABLED } from "@plane/constants";
|
||||||
import type { TLanguage } from "@plane/i18n";
|
import type { TLanguage } from "@plane/i18n";
|
||||||
import { DEFAULT_LANGUAGE, useTranslation } from "@plane/i18n";
|
import { DEFAULT_LANGUAGE, useTranslation } from "@plane/i18n";
|
||||||
// helpers
|
// helpers
|
||||||
|
|
@ -57,16 +58,13 @@ function StoreWrapper(props: TStoreWrapper) {
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const localValue = localStorage && localStorage.getItem("app_sidebar_collapsed");
|
const localValue = localStorage && localStorage.getItem("app_sidebar_collapsed");
|
||||||
const localBoolValue = localValue ? (localValue === "true" ? true : false) : false;
|
const localBoolValue = localValue === "true";
|
||||||
if (localValue && sidebarCollapsed === undefined) toggleSidebar(localBoolValue);
|
if (localValue && sidebarCollapsed === undefined) toggleSidebar(localBoolValue);
|
||||||
}, [sidebarCollapsed, setTheme, toggleSidebar]);
|
}, [sidebarCollapsed, setTheme, toggleSidebar]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Effect 1: Initial theme sync from server (one-time only)
|
* Temporary NODE.DC theme lock: keep the operational dark palette active
|
||||||
*
|
* until the light theme gets a full pass.
|
||||||
* This effect runs ONCE per user session to load theme from server.
|
|
||||||
* After initial load, all theme changes are localStorage-driven (next-themes).
|
|
||||||
* This prevents a feedback loop where server updates trigger UI updates in a cycle.
|
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const userId = userProfile?.id;
|
const userId = userProfile?.id;
|
||||||
|
|
@ -79,6 +77,16 @@ function StoreWrapper(props: TStoreWrapper) {
|
||||||
currentUserIdRef.current = userId;
|
currentUserIdRef.current = userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (NODEDC_THEME_LOCK_ENABLED) {
|
||||||
|
if (!userProfile?.id || hasInitializedThemeRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTheme(NODEDC_FORCED_THEME);
|
||||||
|
hasInitializedThemeRef.current = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Only initialize theme from server on FIRST load for this user
|
// Only initialize theme from server on FIRST load for this user
|
||||||
if (!userProfile?.theme?.theme || hasInitializedThemeRef.current) {
|
if (!userProfile?.theme?.theme || hasInitializedThemeRef.current) {
|
||||||
return; // Skip if already initialized or no profile data
|
return; // Skip if already initialized or no profile data
|
||||||
|
|
@ -89,7 +97,7 @@ function StoreWrapper(props: TStoreWrapper) {
|
||||||
|
|
||||||
// Mark as initialized - prevents future syncs from server
|
// Mark as initialized - prevents future syncs from server
|
||||||
hasInitializedThemeRef.current = true;
|
hasInitializedThemeRef.current = true;
|
||||||
}, [userProfile?.theme?.theme, setTheme]);
|
}, [userProfile?.id, userProfile?.theme?.theme, setTheme]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Effect 2: Custom theme CSS application (runs on every change)
|
* Effect 2: Custom theme CSS application (runs on every change)
|
||||||
|
|
@ -98,12 +106,21 @@ function StoreWrapper(props: TStoreWrapper) {
|
||||||
* the theme changes. It runs independently of the initial sync effect.
|
* the theme changes. It runs independently of the initial sync effect.
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!userProfile?.theme?.theme) return;
|
if (NODEDC_THEME_LOCK_ENABLED) {
|
||||||
|
clearCustomTheme();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userProfile?.theme?.theme) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const currentTheme = userProfile?.theme?.theme;
|
const currentTheme = userProfile?.theme?.theme;
|
||||||
const previousTheme = previousThemeRef.current;
|
const previousTheme = previousThemeRef.current;
|
||||||
const themeData = userProfile?.theme;
|
const themeData = userProfile?.theme;
|
||||||
|
|
||||||
|
if (!currentTheme) return;
|
||||||
|
|
||||||
// Apply custom theme if current theme is custom
|
// Apply custom theme if current theme is custom
|
||||||
if (currentTheme === "custom" && themeData.primary && themeData.background && themeData.darkPalette !== undefined) {
|
if (currentTheme === "custom" && themeData.primary && themeData.background && themeData.darkPalette !== undefined) {
|
||||||
applyCustomTheme(themeData.primary, themeData.background, themeData.darkPalette ? "dark" : "light");
|
applyCustomTheme(themeData.primary, themeData.background, themeData.darkPalette ? "dark" : "light");
|
||||||
|
|
@ -116,7 +133,7 @@ function StoreWrapper(props: TStoreWrapper) {
|
||||||
|
|
||||||
// Update previous theme for next comparison
|
// Update previous theme for next comparison
|
||||||
previousThemeRef.current = currentTheme;
|
previousThemeRef.current = currentTheme;
|
||||||
}, [userProfile?.theme]);
|
}, [userProfile?.id, userProfile?.theme]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!userProfile?.id) return;
|
if (!userProfile?.id) return;
|
||||||
|
|
@ -138,7 +155,7 @@ function StoreWrapper(props: TStoreWrapper) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!userProfile?.id) return;
|
if (!userProfile?.id) return;
|
||||||
changeLanguage((userProfile?.language as TLanguage) || DEFAULT_LANGUAGE);
|
changeLanguage((userProfile?.language as TLanguage) || DEFAULT_LANGUAGE);
|
||||||
}, [userProfile?.language, changeLanguage]);
|
}, [userProfile?.id, userProfile?.language, changeLanguage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!params) return;
|
if (!params) return;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@
|
||||||
* See the LICENSE file for details.
|
* See the LICENSE file for details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const THEMES = ["light", "dark", "light-contrast", "dark-contrast", "custom"];
|
export const NODEDC_FORCED_THEME = "dark";
|
||||||
|
export const NODEDC_THEME_LOCK_ENABLED = true;
|
||||||
|
export const THEMES = ["light", NODEDC_FORCED_THEME, "light-contrast", "dark-contrast", "custom"];
|
||||||
|
|
||||||
export interface I_THEME_OPTION {
|
export interface I_THEME_OPTION {
|
||||||
key: string;
|
key: string;
|
||||||
|
|
@ -43,7 +45,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "dark",
|
key: "dark",
|
||||||
value: "dark",
|
value: NODEDC_FORCED_THEME,
|
||||||
i18n_label: "Dark",
|
i18n_label: "Dark",
|
||||||
type: "dark",
|
type: "dark",
|
||||||
icon: {
|
icon: {
|
||||||
|
|
@ -86,3 +88,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const NODEDC_ACTIVE_THEME_OPTIONS = NODEDC_THEME_LOCK_ENABLED
|
||||||
|
? THEME_OPTIONS.filter((themeOption) => themeOption.value === NODEDC_FORCED_THEME)
|
||||||
|
: THEME_OPTIONS;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue