UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: дизайн и русификация God Mode
This commit is contained in:
parent
5d336039ba
commit
7f47b85c36
|
|
@ -41,17 +41,17 @@ export function InstanceAIForm(props: IInstanceAIForm) {
|
||||||
{
|
{
|
||||||
key: "LLM_MODEL",
|
key: "LLM_MODEL",
|
||||||
type: "text",
|
type: "text",
|
||||||
label: "LLM Model",
|
label: "LLM-модель",
|
||||||
description: (
|
description: (
|
||||||
<>
|
<>
|
||||||
Choose an OpenAI engine.{" "}
|
Выберите модель OpenAI.{" "}
|
||||||
<a
|
<a
|
||||||
href="https://platform.openai.com/docs/models/overview"
|
href="https://platform.openai.com/docs/models/overview"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-accent-primary hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
Learn more
|
Подробнее
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
|
@ -62,17 +62,17 @@ export function InstanceAIForm(props: IInstanceAIForm) {
|
||||||
{
|
{
|
||||||
key: "LLM_API_KEY",
|
key: "LLM_API_KEY",
|
||||||
type: "password",
|
type: "password",
|
||||||
label: "API key",
|
label: "API-ключ",
|
||||||
description: (
|
description: (
|
||||||
<>
|
<>
|
||||||
You will find your API key{" "}
|
API-ключ находится{" "}
|
||||||
<a
|
<a
|
||||||
href="https://platform.openai.com/api-keys"
|
href="https://platform.openai.com/api-keys"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-accent-primary hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
here.
|
здесь.
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
|
@ -89,8 +89,8 @@ export function InstanceAIForm(props: IInstanceAIForm) {
|
||||||
.then(() =>
|
.then(() =>
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.SUCCESS,
|
type: TOAST_TYPE.SUCCESS,
|
||||||
title: "Success",
|
title: "Сохранено",
|
||||||
message: "AI Settings updated successfully",
|
message: "ИИ-настройки обновлены",
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.catch((err) => console.error(err));
|
.catch((err) => console.error(err));
|
||||||
|
|
@ -101,7 +101,7 @@ export function InstanceAIForm(props: IInstanceAIForm) {
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="pb-1 text-18 font-medium text-primary">OpenAI</div>
|
<div className="pb-1 text-18 font-medium text-primary">OpenAI</div>
|
||||||
<div className="text-13 font-regular text-tertiary">If you use ChatGPT, this is for you.</div>
|
<div className="text-13 font-regular text-tertiary">Используется для встроенных функций на базе OpenAI.</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid-col grid w-full grid-cols-1 items-center justify-between gap-x-12 gap-y-8 lg:grid-cols-3">
|
<div className="grid-col grid w-full grid-cols-1 items-center justify-between gap-x-12 gap-y-8 lg:grid-cols-3">
|
||||||
{aiFormFields.map((field) => (
|
{aiFormFields.map((field) => (
|
||||||
|
|
@ -122,15 +122,15 @@ export function InstanceAIForm(props: IInstanceAIForm) {
|
||||||
|
|
||||||
<div className="flex flex-col items-start gap-4">
|
<div className="flex flex-col items-start gap-4">
|
||||||
<Button variant="primary" size="lg" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
<Button variant="primary" size="lg" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
||||||
{isSubmitting ? "Saving" : "Save changes"}
|
{isSubmitting ? "Сохранение" : "Сохранить изменения"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="relative inline-flex items-center gap-1.5 rounded-sm border border-accent-subtle bg-accent-subtle px-4 py-2 text-caption-sm-regular text-accent-secondary">
|
<div className="nodedc-settings-note relative inline-flex max-w-2xl items-center gap-2 px-4 py-3 text-12 font-regular">
|
||||||
<Lightbulb className="size-4" />
|
<Lightbulb className="size-4 shrink-0 text-tertiary" />
|
||||||
<div>
|
<div>
|
||||||
If you have a preferred AI models vendor, please get in{" "}
|
Если нужен другой провайдер ИИ-моделей, свяжитесь{" "}
|
||||||
<a className="font-medium underline" href="https://plane.so/contact">
|
<a className="font-medium underline" href="https://plane.so/contact">
|
||||||
touch with us.
|
с командой поддержки.
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,8 @@ const InstanceAIPage = observer(function InstanceAIPage(_props: Route.ComponentP
|
||||||
return (
|
return (
|
||||||
<PageWrapper
|
<PageWrapper
|
||||||
header={{
|
header={{
|
||||||
title: "AI features for all your workspaces",
|
title: "ИИ-функции для всех воркспейсов",
|
||||||
description: "Configure your AI API credentials so Plane AI features are turned on for all your workspaces.",
|
description: "Настройте API-ключ и модель, чтобы включить ИИ-возможности во всех рабочих пространствах.",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{formattedConfig ? (
|
{formattedConfig ? (
|
||||||
|
|
@ -45,6 +45,6 @@ const InstanceAIPage = observer(function InstanceAIPage(_props: Route.ComponentP
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const meta: Route.MetaFunction = () => [{ title: "Artificial Intelligence Settings - God Mode" }];
|
export const meta: Route.MetaFunction = () => [{ title: "ИИ-настройки - NODE.DC" }];
|
||||||
|
|
||||||
export default InstanceAIPage;
|
export default InstanceAIPage;
|
||||||
|
|
|
||||||
|
|
@ -58,9 +58,9 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||||
{
|
{
|
||||||
key: "GITEA_HOST",
|
key: "GITEA_HOST",
|
||||||
type: "text",
|
type: "text",
|
||||||
label: "Gitea Host",
|
label: "Хост Gitea",
|
||||||
description: (
|
description: (
|
||||||
<>Use the URL of your Gitea instance. For the official Gitea instance, use "https://gitea.com".</>
|
<>Укажите URL вашего Gitea-инстанса. Для официального сервиса используйте "https://gitea.com".</>
|
||||||
),
|
),
|
||||||
placeholder: "https://gitea.com",
|
placeholder: "https://gitea.com",
|
||||||
error: Boolean(errors.GITEA_HOST),
|
error: Boolean(errors.GITEA_HOST),
|
||||||
|
|
@ -72,7 +72,7 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||||
label: "Client ID",
|
label: "Client ID",
|
||||||
description: (
|
description: (
|
||||||
<>
|
<>
|
||||||
You will get this from your{" "}
|
Возьмите значение в настройках{" "}
|
||||||
<a
|
<a
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
href="https://gitea.com/user/settings/applications"
|
href="https://gitea.com/user/settings/applications"
|
||||||
|
|
@ -80,7 +80,7 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||||
className="text-accent-primary hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
Gitea OAuth application settings.
|
Gitea OAuth-приложения.
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
|
@ -94,7 +94,7 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||||
label: "Client secret",
|
label: "Client secret",
|
||||||
description: (
|
description: (
|
||||||
<>
|
<>
|
||||||
Your client secret is also found in your{" "}
|
Секрет клиента находится в настройках{" "}
|
||||||
<a
|
<a
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
href="https://gitea.com/user/settings/applications"
|
href="https://gitea.com/user/settings/applications"
|
||||||
|
|
@ -102,7 +102,7 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||||
className="text-accent-primary hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
Gitea OAuth application settings.
|
Gitea OAuth-приложения.
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
|
@ -124,8 +124,8 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||||
url: `${originURL}/auth/gitea/callback/`,
|
url: `${originURL}/auth/gitea/callback/`,
|
||||||
description: (
|
description: (
|
||||||
<>
|
<>
|
||||||
We will auto-generate this. Paste this into your <CodeBlock darkerShade>Authorized Callback URI</CodeBlock>{" "}
|
Значение сформировано автоматически. Вставьте его в поле{" "}
|
||||||
field{" "}
|
<CodeBlock darkerShade>Authorized Callback URI</CodeBlock>{" "}
|
||||||
<a
|
<a
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
href={`${control._formValues.GITEA_HOST || "https://gitea.com"}/user/settings/applications`}
|
href={`${control._formValues.GITEA_HOST || "https://gitea.com"}/user/settings/applications`}
|
||||||
|
|
@ -133,7 +133,7 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||||
className="text-accent-primary hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
here.
|
здесь.
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
|
@ -147,8 +147,8 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||||
const response = await updateInstanceConfigurations(payload);
|
const response = await updateInstanceConfigurations(payload);
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.SUCCESS,
|
type: TOAST_TYPE.SUCCESS,
|
||||||
title: "Done!",
|
title: "Готово",
|
||||||
message: "Your Gitea authentication is configured. You should test it now.",
|
message: "Аутентификация через Gitea настроена. Проверьте вход перед включением в проде.",
|
||||||
});
|
});
|
||||||
reset({
|
reset({
|
||||||
GITEA_HOST: response.find((item) => item.key === "GITEA_HOST")?.value,
|
GITEA_HOST: response.find((item) => item.key === "GITEA_HOST")?.value,
|
||||||
|
|
@ -178,7 +178,7 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
<div className="grid w-full grid-cols-2 gap-x-12 gap-y-8">
|
<div className="grid w-full grid-cols-2 gap-x-12 gap-y-8">
|
||||||
<div className="col-span-2 flex flex-col gap-y-4 pt-1 md:col-span-1">
|
<div className="col-span-2 flex flex-col gap-y-4 pt-1 md:col-span-1">
|
||||||
<div className="pt-2.5 text-18 font-medium">Gitea-provided details for Plane</div>
|
<div className="pt-2.5 text-18 font-medium">Данные Gitea для NODE.DC</div>
|
||||||
{GITEA_FORM_FIELDS.map((field) => (
|
{GITEA_FORM_FIELDS.map((field) => (
|
||||||
<ControllerInput
|
<ControllerInput
|
||||||
key={field.key}
|
key={field.key}
|
||||||
|
|
@ -202,17 +202,17 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
disabled={!isDirty}
|
disabled={!isDirty}
|
||||||
>
|
>
|
||||||
{isSubmitting ? "Saving" : "Save changes"}
|
{isSubmitting ? "Сохранение" : "Сохранить изменения"}
|
||||||
</Button>
|
</Button>
|
||||||
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
|
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
|
||||||
Go back
|
Назад
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 md:col-span-1">
|
<div className="col-span-2 md:col-span-1">
|
||||||
<div className="flex flex-col gap-y-4 rounded-lg bg-layer-1 px-6 pt-1.5 pb-4">
|
<div className="nodedc-settings-helper-card flex flex-col gap-y-4 px-6 pt-1.5 pb-4">
|
||||||
<div className="pt-2 text-18 font-medium">Plane-provided details for Gitea</div>
|
<div className="pt-2 text-18 font-medium">Данные NODE.DC для Gitea</div>
|
||||||
{GITEA_SERVICE_FIELD.map((field) => (
|
{GITEA_SERVICE_FIELD.map((field) => (
|
||||||
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -41,14 +41,14 @@ const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthentic
|
||||||
const updateConfigPromise = updateInstanceConfigurations(payload);
|
const updateConfigPromise = updateInstanceConfigurations(payload);
|
||||||
|
|
||||||
setPromiseToast(updateConfigPromise, {
|
setPromiseToast(updateConfigPromise, {
|
||||||
loading: "Saving Configuration",
|
loading: "Сохранение конфигурации",
|
||||||
success: {
|
success: {
|
||||||
title: "Configuration saved",
|
title: "Конфигурация сохранена",
|
||||||
message: () => `Gitea authentication is now ${value === "1" ? "active" : "disabled"}.`,
|
message: () => `Вход через Gitea ${value === "1" ? "включен" : "отключен"}.`,
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
title: "Error",
|
title: "Ошибка",
|
||||||
message: () => "Failed to save configuration",
|
message: () => "Не удалось сохранить конфигурацию",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -69,7 +69,7 @@ const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthentic
|
||||||
customHeader={
|
customHeader={
|
||||||
<AuthenticationMethodCard
|
<AuthenticationMethodCard
|
||||||
name="Gitea"
|
name="Gitea"
|
||||||
description="Allow members to login or sign up to plane with their Gitea accounts."
|
description="Вход и регистрация пользователей через аккаунты Gitea."
|
||||||
icon={<img src={giteaLogo} height={24} width={24} alt="Gitea Logo" />}
|
icon={<img src={giteaLogo} height={24} width={24} alt="Gitea Logo" />}
|
||||||
config={
|
config={
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
|
|
@ -100,6 +100,6 @@ const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthentic
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
export const meta: Route.MetaFunction = () => [{ title: "Gitea Authentication - God Mode" }];
|
export const meta: Route.MetaFunction = () => [{ title: "Gitea OAuth - NODE.DC" }];
|
||||||
|
|
||||||
export default InstanceGiteaAuthenticationPage;
|
export default InstanceGiteaAuthenticationPage;
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||||
label: "Client ID",
|
label: "Client ID",
|
||||||
description: (
|
description: (
|
||||||
<>
|
<>
|
||||||
You will get this from your{" "}
|
Возьмите значение в настройках{" "}
|
||||||
<a
|
<a
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
href="https://github.com/settings/applications/new"
|
href="https://github.com/settings/applications/new"
|
||||||
|
|
@ -70,7 +70,7 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||||
className="text-accent-primary hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
GitHub OAuth application settings.
|
GitHub OAuth-приложения.
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
|
@ -84,7 +84,7 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||||
label: "Client secret",
|
label: "Client secret",
|
||||||
description: (
|
description: (
|
||||||
<>
|
<>
|
||||||
Your client secret is also found in your{" "}
|
Секрет клиента находится в настройках{" "}
|
||||||
<a
|
<a
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
href="https://github.com/settings/applications/new"
|
href="https://github.com/settings/applications/new"
|
||||||
|
|
@ -92,7 +92,7 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||||
className="text-accent-primary hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
GitHub OAuth application settings.
|
GitHub OAuth-приложения.
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
|
@ -103,8 +103,8 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||||
{
|
{
|
||||||
key: "GITHUB_ORGANIZATION_ID",
|
key: "GITHUB_ORGANIZATION_ID",
|
||||||
type: "text",
|
type: "text",
|
||||||
label: "Organization ID",
|
label: "ID организации",
|
||||||
description: <>The organization github ID.</>,
|
description: <>ID организации GitHub.</>,
|
||||||
placeholder: "123456789",
|
placeholder: "123456789",
|
||||||
error: Boolean(errors.GITHUB_ORGANIZATION_ID),
|
error: Boolean(errors.GITHUB_ORGANIZATION_ID),
|
||||||
required: false,
|
required: false,
|
||||||
|
|
@ -123,7 +123,8 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||||
url: originURL,
|
url: originURL,
|
||||||
description: (
|
description: (
|
||||||
<>
|
<>
|
||||||
We will auto-generate this. Paste this into the <CodeBlock darkerShade>Authorized origin URL</CodeBlock> field{" "}
|
Значение сформировано автоматически. Вставьте его в поле{" "}
|
||||||
|
<CodeBlock darkerShade>Authorized origin URL</CodeBlock>{" "}
|
||||||
<a
|
<a
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
href="https://github.com/settings/applications/new"
|
href="https://github.com/settings/applications/new"
|
||||||
|
|
@ -131,7 +132,7 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||||
className="text-accent-primary hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
here.
|
здесь.
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
|
@ -145,8 +146,8 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||||
url: `${originURL}/auth/github/callback/`,
|
url: `${originURL}/auth/github/callback/`,
|
||||||
description: (
|
description: (
|
||||||
<>
|
<>
|
||||||
We will auto-generate this. Paste this into your <CodeBlock darkerShade>Authorized Callback URI</CodeBlock>{" "}
|
Значение сформировано автоматически. Вставьте его в поле{" "}
|
||||||
field{" "}
|
<CodeBlock darkerShade>Authorized Callback URI</CodeBlock>{" "}
|
||||||
<a
|
<a
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
href="https://github.com/settings/applications/new"
|
href="https://github.com/settings/applications/new"
|
||||||
|
|
@ -154,7 +155,7 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||||
className="text-accent-primary hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
here.
|
здесь.
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
|
@ -168,8 +169,8 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||||
const response = await updateInstanceConfigurations(payload);
|
const response = await updateInstanceConfigurations(payload);
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.SUCCESS,
|
type: TOAST_TYPE.SUCCESS,
|
||||||
title: "Done!",
|
title: "Готово",
|
||||||
message: "Your GitHub authentication is configured. You should test it now.",
|
message: "Аутентификация через GitHub настроена. Проверьте вход перед включением в проде.",
|
||||||
});
|
});
|
||||||
reset({
|
reset({
|
||||||
GITHUB_CLIENT_ID: response.find((item) => item.key === "GITHUB_CLIENT_ID")?.value,
|
GITHUB_CLIENT_ID: response.find((item) => item.key === "GITHUB_CLIENT_ID")?.value,
|
||||||
|
|
@ -199,7 +200,7 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
<div className="grid w-full grid-cols-2 gap-x-12 gap-y-8">
|
<div className="grid w-full grid-cols-2 gap-x-12 gap-y-8">
|
||||||
<div className="col-span-2 flex flex-col gap-y-4 pt-1 md:col-span-1">
|
<div className="col-span-2 flex flex-col gap-y-4 pt-1 md:col-span-1">
|
||||||
<div className="pt-2.5 text-18 font-medium">GitHub-provided details for Plane</div>
|
<div className="pt-2.5 text-18 font-medium">Данные GitHub для NODE.DC</div>
|
||||||
{GITHUB_FORM_FIELDS.map((field) => (
|
{GITHUB_FORM_FIELDS.map((field) => (
|
||||||
<ControllerInput
|
<ControllerInput
|
||||||
key={field.key}
|
key={field.key}
|
||||||
|
|
@ -223,32 +224,32 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
disabled={!isDirty}
|
disabled={!isDirty}
|
||||||
>
|
>
|
||||||
{isSubmitting ? "Saving" : "Save changes"}
|
{isSubmitting ? "Сохранение" : "Сохранить изменения"}
|
||||||
</Button>
|
</Button>
|
||||||
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
|
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
|
||||||
Go back
|
Назад
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 flex flex-col gap-y-6 md:col-span-1">
|
<div className="col-span-2 flex flex-col gap-y-6 md:col-span-1">
|
||||||
<div className="pt-2 text-18 font-medium">Plane-provided details for GitHub</div>
|
<div className="pt-2 text-18 font-medium">Данные NODE.DC для GitHub</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-y-4">
|
<div className="flex flex-col gap-y-4">
|
||||||
{/* common service details */}
|
{/* common service details */}
|
||||||
<div className="flex flex-col gap-y-4 rounded-lg bg-layer-1 px-6 py-4">
|
<div className="nodedc-settings-helper-card flex flex-col gap-y-4 px-6 py-4">
|
||||||
{GITHUB_COMMON_SERVICE_DETAILS.map((field) => (
|
{GITHUB_COMMON_SERVICE_DETAILS.map((field) => (
|
||||||
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* web service details */}
|
{/* web service details */}
|
||||||
<div className="flex flex-col overflow-hidden rounded-lg">
|
<div className="nodedc-settings-helper-card flex flex-col overflow-hidden">
|
||||||
<div className="flex items-center gap-x-3 bg-layer-3 px-6 py-3 text-11 font-medium text-secondary uppercase">
|
<div className="nodedc-settings-helper-card-header flex items-center gap-x-3 px-6 py-3 text-11 font-medium uppercase">
|
||||||
<Monitor className="h-3 w-3" />
|
<Monitor className="h-3 w-3" />
|
||||||
Web
|
Веб
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-y-4 bg-layer-1 px-6 py-4">
|
<div className="flex flex-col gap-y-4 px-6 py-4">
|
||||||
{GITHUB_SERVICE_DETAILS.map((field) => (
|
{GITHUB_SERVICE_DETAILS.map((field) => (
|
||||||
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -49,14 +49,14 @@ const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthent
|
||||||
const updateConfigPromise = updateInstanceConfigurations(payload);
|
const updateConfigPromise = updateInstanceConfigurations(payload);
|
||||||
|
|
||||||
setPromiseToast(updateConfigPromise, {
|
setPromiseToast(updateConfigPromise, {
|
||||||
loading: "Saving Configuration",
|
loading: "Сохранение конфигурации",
|
||||||
success: {
|
success: {
|
||||||
title: "Configuration saved",
|
title: "Конфигурация сохранена",
|
||||||
message: () => `GitHub authentication is now ${value === "1" ? "active" : "disabled"}.`,
|
message: () => `Вход через GitHub ${value === "1" ? "включен" : "отключен"}.`,
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
title: "Error",
|
title: "Ошибка",
|
||||||
message: () => "Failed to save configuration",
|
message: () => "Не удалось сохранить конфигурацию",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -77,7 +77,7 @@ const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthent
|
||||||
customHeader={
|
customHeader={
|
||||||
<AuthenticationMethodCard
|
<AuthenticationMethodCard
|
||||||
name="GitHub"
|
name="GitHub"
|
||||||
description="Allow members to login or sign up to plane with their GitHub accounts."
|
description="Вход и регистрация пользователей через аккаунты GitHub."
|
||||||
icon={
|
icon={
|
||||||
<img
|
<img
|
||||||
src={resolveGeneralTheme(resolvedTheme) === "dark" ? githubDarkModeImage : githubLightModeImage}
|
src={resolveGeneralTheme(resolvedTheme) === "dark" ? githubDarkModeImage : githubLightModeImage}
|
||||||
|
|
@ -116,6 +116,6 @@ const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthent
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const meta: Route.MetaFunction = () => [{ title: "GitHub Authentication - God Mode" }];
|
export const meta: Route.MetaFunction = () => [{ title: "GitHub OAuth - NODE.DC" }];
|
||||||
|
|
||||||
export default InstanceGithubAuthenticationPage;
|
export default InstanceGithubAuthenticationPage;
|
||||||
|
|
|
||||||
|
|
@ -58,10 +58,10 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||||
{
|
{
|
||||||
key: "GITLAB_HOST",
|
key: "GITLAB_HOST",
|
||||||
type: "text",
|
type: "text",
|
||||||
label: "Host",
|
label: "Хост",
|
||||||
description: (
|
description: (
|
||||||
<>
|
<>
|
||||||
This is either https://gitlab.com or the <CodeBlock>domain.tld</CodeBlock> where you host GitLab.
|
Укажите https://gitlab.com или <CodeBlock>domain.tld</CodeBlock>, если GitLab развернут у вас.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
placeholder: "https://gitlab.com",
|
placeholder: "https://gitlab.com",
|
||||||
|
|
@ -74,7 +74,7 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||||
label: "Application ID",
|
label: "Application ID",
|
||||||
description: (
|
description: (
|
||||||
<>
|
<>
|
||||||
Get this from your{" "}
|
Возьмите значение в настройках{" "}
|
||||||
<a
|
<a
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
|
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
|
||||||
|
|
@ -82,7 +82,7 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||||
className="text-accent-primary hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
GitLab OAuth application settings
|
GitLab OAuth-приложения
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
</>
|
</>
|
||||||
|
|
@ -97,7 +97,7 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||||
label: "Secret",
|
label: "Secret",
|
||||||
description: (
|
description: (
|
||||||
<>
|
<>
|
||||||
The client secret is also found in your{" "}
|
Секрет клиента находится в настройках{" "}
|
||||||
<a
|
<a
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
|
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
|
||||||
|
|
@ -105,7 +105,7 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||||
className="text-accent-primary hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
GitLab OAuth application settings
|
GitLab OAuth-приложения
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
</>
|
</>
|
||||||
|
|
@ -128,7 +128,7 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||||
url: `${originURL}/auth/gitlab/callback/`,
|
url: `${originURL}/auth/gitlab/callback/`,
|
||||||
description: (
|
description: (
|
||||||
<>
|
<>
|
||||||
We will auto-generate this. Paste this into the <CodeBlock darkerShade>Redirect URI</CodeBlock> field of your{" "}
|
Значение сформировано автоматически. Вставьте его в поле <CodeBlock darkerShade>Redirect URI</CodeBlock>{" "}
|
||||||
<a
|
<a
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
|
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
|
||||||
|
|
@ -136,7 +136,7 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||||
className="text-accent-primary hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
GitLab OAuth application
|
GitLab OAuth-приложения
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
</>
|
</>
|
||||||
|
|
@ -151,8 +151,8 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||||
const response = await updateInstanceConfigurations(payload);
|
const response = await updateInstanceConfigurations(payload);
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.SUCCESS,
|
type: TOAST_TYPE.SUCCESS,
|
||||||
title: "Done!",
|
title: "Готово",
|
||||||
message: "Your GitLab authentication is configured. You should test it now.",
|
message: "Аутентификация через GitLab настроена. Проверьте вход перед включением в проде.",
|
||||||
});
|
});
|
||||||
reset({
|
reset({
|
||||||
GITLAB_HOST: response.find((item) => item.key === "GITLAB_HOST")?.value,
|
GITLAB_HOST: response.find((item) => item.key === "GITLAB_HOST")?.value,
|
||||||
|
|
@ -182,7 +182,7 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
<div className="grid w-full grid-cols-2 gap-x-12 gap-y-8">
|
<div className="grid w-full grid-cols-2 gap-x-12 gap-y-8">
|
||||||
<div className="col-span-2 flex flex-col gap-y-4 pt-1 md:col-span-1">
|
<div className="col-span-2 flex flex-col gap-y-4 pt-1 md:col-span-1">
|
||||||
<div className="pt-2.5 text-18 font-medium">GitLab-provided details for Plane</div>
|
<div className="pt-2.5 text-18 font-medium">Данные GitLab для NODE.DC</div>
|
||||||
{GITLAB_FORM_FIELDS.map((field) => (
|
{GITLAB_FORM_FIELDS.map((field) => (
|
||||||
<ControllerInput
|
<ControllerInput
|
||||||
key={field.key}
|
key={field.key}
|
||||||
|
|
@ -206,17 +206,17 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
disabled={!isDirty}
|
disabled={!isDirty}
|
||||||
>
|
>
|
||||||
{isSubmitting ? "Saving" : "Save changes"}
|
{isSubmitting ? "Сохранение" : "Сохранить изменения"}
|
||||||
</Button>
|
</Button>
|
||||||
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
|
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
|
||||||
Go back
|
Назад
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 md:col-span-1">
|
<div className="col-span-2 md:col-span-1">
|
||||||
<div className="flex flex-col gap-y-4 rounded-lg bg-layer-3 px-6 pt-1.5 pb-4">
|
<div className="nodedc-settings-helper-card flex flex-col gap-y-4 px-6 pt-1.5 pb-4">
|
||||||
<div className="pt-2 text-18 font-medium">Plane-provided details for GitLab</div>
|
<div className="pt-2 text-18 font-medium">Данные NODE.DC для GitLab</div>
|
||||||
{GITLAB_SERVICE_FIELD.map((field) => (
|
{GITLAB_SERVICE_FIELD.map((field) => (
|
||||||
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -43,14 +43,14 @@ const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthent
|
||||||
const updateConfigPromise = updateInstanceConfigurations(payload);
|
const updateConfigPromise = updateInstanceConfigurations(payload);
|
||||||
|
|
||||||
setPromiseToast(updateConfigPromise, {
|
setPromiseToast(updateConfigPromise, {
|
||||||
loading: "Saving Configuration",
|
loading: "Сохранение конфигурации",
|
||||||
success: {
|
success: {
|
||||||
title: "Configuration saved",
|
title: "Конфигурация сохранена",
|
||||||
message: () => `GitLab authentication is now ${value === "1" ? "active" : "disabled"}.`,
|
message: () => `Вход через GitLab ${value === "1" ? "включен" : "отключен"}.`,
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
title: "Error",
|
title: "Ошибка",
|
||||||
message: () => "Failed to save configuration",
|
message: () => "Не удалось сохранить конфигурацию",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -68,7 +68,7 @@ const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthent
|
||||||
customHeader={
|
customHeader={
|
||||||
<AuthenticationMethodCard
|
<AuthenticationMethodCard
|
||||||
name="GitLab"
|
name="GitLab"
|
||||||
description="Allow members to login or sign up to plane with their GitLab accounts."
|
description="Вход и регистрация пользователей через аккаунты GitLab."
|
||||||
icon={<img src={GitlabLogo} height={24} width={24} alt="GitLab Logo" />}
|
icon={<img src={GitlabLogo} height={24} width={24} alt="GitLab Logo" />}
|
||||||
config={
|
config={
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
|
|
@ -104,6 +104,6 @@ const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthent
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const meta: Route.MetaFunction = () => [{ title: "GitLab Authentication - God Mode" }];
|
export const meta: Route.MetaFunction = () => [{ title: "GitLab OAuth - NODE.DC" }];
|
||||||
|
|
||||||
export default InstanceGitlabAuthenticationPage;
|
export default InstanceGitlabAuthenticationPage;
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||||
label: "Client ID",
|
label: "Client ID",
|
||||||
description: (
|
description: (
|
||||||
<>
|
<>
|
||||||
Your client ID lives in your Google API Console.{" "}
|
Client ID находится в Google API Console.{" "}
|
||||||
<a
|
<a
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
href="https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#creatingcred"
|
href="https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#creatingcred"
|
||||||
|
|
@ -69,7 +69,7 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||||
className="text-accent-primary hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
Learn more
|
Подробнее
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
|
@ -83,7 +83,7 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||||
label: "Client secret",
|
label: "Client secret",
|
||||||
description: (
|
description: (
|
||||||
<>
|
<>
|
||||||
Your client secret should also be in your Google API Console.{" "}
|
Client secret также находится в Google API Console.{" "}
|
||||||
<a
|
<a
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
href="https://developers.google.com/identity/oauth2/web/guides/get-google-api-clientid"
|
href="https://developers.google.com/identity/oauth2/web/guides/get-google-api-clientid"
|
||||||
|
|
@ -91,7 +91,7 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||||
className="text-accent-primary hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
Learn more
|
Подробнее
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
|
@ -113,15 +113,15 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||||
url: originURL,
|
url: originURL,
|
||||||
description: (
|
description: (
|
||||||
<p>
|
<p>
|
||||||
We will auto-generate this. Paste this into your{" "}
|
Значение сформировано автоматически. Вставьте его в поле{" "}
|
||||||
<CodeBlock darkerShade>Authorized JavaScript origins</CodeBlock> field. For this OAuth client{" "}
|
<CodeBlock darkerShade>Authorized JavaScript origins</CodeBlock> для OAuth-клиента{" "}
|
||||||
<a
|
<a
|
||||||
href="https://console.cloud.google.com/apis/credentials/oauthclient"
|
href="https://console.cloud.google.com/apis/credentials/oauthclient"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-accent-primary hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
here.
|
здесь.
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
),
|
),
|
||||||
|
|
@ -135,15 +135,15 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||||
url: `${originURL}/auth/google/callback/`,
|
url: `${originURL}/auth/google/callback/`,
|
||||||
description: (
|
description: (
|
||||||
<p>
|
<p>
|
||||||
We will auto-generate this. Paste this into your <CodeBlock darkerShade>Authorized Redirect URI</CodeBlock>{" "}
|
Значение сформировано автоматически. Вставьте его в поле{" "}
|
||||||
field. For this OAuth client{" "}
|
<CodeBlock darkerShade>Authorized Redirect URI</CodeBlock> для OAuth-клиента{" "}
|
||||||
<a
|
<a
|
||||||
href="https://console.cloud.google.com/apis/credentials/oauthclient"
|
href="https://console.cloud.google.com/apis/credentials/oauthclient"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-accent-primary hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
here.
|
здесь.
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
),
|
),
|
||||||
|
|
@ -157,8 +157,8 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||||
const response = await updateInstanceConfigurations(payload);
|
const response = await updateInstanceConfigurations(payload);
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.SUCCESS,
|
type: TOAST_TYPE.SUCCESS,
|
||||||
title: "Done!",
|
title: "Готово",
|
||||||
message: "Your Google authentication is configured. You should test it now.",
|
message: "Аутентификация через Google настроена. Проверьте вход перед включением в проде.",
|
||||||
});
|
});
|
||||||
reset({
|
reset({
|
||||||
GOOGLE_CLIENT_ID: response.find((item) => item.key === "GOOGLE_CLIENT_ID")?.value,
|
GOOGLE_CLIENT_ID: response.find((item) => item.key === "GOOGLE_CLIENT_ID")?.value,
|
||||||
|
|
@ -187,7 +187,7 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
<div className="grid w-full grid-cols-2 gap-x-12 gap-y-8">
|
<div className="grid w-full grid-cols-2 gap-x-12 gap-y-8">
|
||||||
<div className="col-span-2 flex flex-col gap-y-4 pt-1 md:col-span-1">
|
<div className="col-span-2 flex flex-col gap-y-4 pt-1 md:col-span-1">
|
||||||
<div className="pt-2.5 text-18 font-medium">Google-provided details for Plane</div>
|
<div className="pt-2.5 text-18 font-medium">Данные Google для NODE.DC</div>
|
||||||
{GOOGLE_FORM_FIELDS.map((field) => (
|
{GOOGLE_FORM_FIELDS.map((field) => (
|
||||||
<ControllerInput
|
<ControllerInput
|
||||||
key={field.key}
|
key={field.key}
|
||||||
|
|
@ -211,32 +211,32 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
disabled={!isDirty}
|
disabled={!isDirty}
|
||||||
>
|
>
|
||||||
{isSubmitting ? "Saving" : "Save changes"}
|
{isSubmitting ? "Сохранение" : "Сохранить изменения"}
|
||||||
</Button>
|
</Button>
|
||||||
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
|
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
|
||||||
Go back
|
Назад
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 flex flex-col gap-y-6 md:col-span-1">
|
<div className="col-span-2 flex flex-col gap-y-6 md:col-span-1">
|
||||||
<div className="pt-2 text-18 font-medium">Plane-provided details for Google</div>
|
<div className="pt-2 text-18 font-medium">Данные NODE.DC для Google</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-y-4">
|
<div className="flex flex-col gap-y-4">
|
||||||
{/* common service details */}
|
{/* common service details */}
|
||||||
<div className="flex flex-col gap-y-4 rounded-lg bg-layer-1 px-6 py-4">
|
<div className="nodedc-settings-helper-card flex flex-col gap-y-4 px-6 py-4">
|
||||||
{GOOGLE_COMMON_SERVICE_DETAILS.map((field) => (
|
{GOOGLE_COMMON_SERVICE_DETAILS.map((field) => (
|
||||||
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* web service details */}
|
{/* web service details */}
|
||||||
<div className="flex flex-col overflow-hidden rounded-lg">
|
<div className="nodedc-settings-helper-card flex flex-col overflow-hidden">
|
||||||
<div className="flex items-center gap-x-3 bg-layer-3 px-6 py-3 text-11 font-medium text-secondary uppercase">
|
<div className="nodedc-settings-helper-card-header flex items-center gap-x-3 px-6 py-3 text-11 font-medium uppercase">
|
||||||
<Monitor className="h-3 w-3" />
|
<Monitor className="h-3 w-3" />
|
||||||
Web
|
Веб
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-y-4 bg-layer-1 px-6 py-4">
|
<div className="flex flex-col gap-y-4 px-6 py-4">
|
||||||
{GOOGLE_SERVICE_DETAILS.map((field) => (
|
{GOOGLE_SERVICE_DETAILS.map((field) => (
|
||||||
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -43,14 +43,14 @@ const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthent
|
||||||
const updateConfigPromise = updateInstanceConfigurations(payload);
|
const updateConfigPromise = updateInstanceConfigurations(payload);
|
||||||
|
|
||||||
setPromiseToast(updateConfigPromise, {
|
setPromiseToast(updateConfigPromise, {
|
||||||
loading: "Saving Configuration",
|
loading: "Сохранение конфигурации",
|
||||||
success: {
|
success: {
|
||||||
title: "Configuration saved",
|
title: "Конфигурация сохранена",
|
||||||
message: () => `Google authentication is now ${value === "1" ? "active" : "disabled"}.`,
|
message: () => `Вход через Google ${value === "1" ? "включен" : "отключен"}.`,
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
title: "Error",
|
title: "Ошибка",
|
||||||
message: () => "Failed to save configuration",
|
message: () => "Не удалось сохранить конфигурацию",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -68,8 +68,7 @@ const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthent
|
||||||
customHeader={
|
customHeader={
|
||||||
<AuthenticationMethodCard
|
<AuthenticationMethodCard
|
||||||
name="Google"
|
name="Google"
|
||||||
description="Allow members to login or sign up to plane with their Google
|
description="Вход и регистрация пользователей через аккаунты Google."
|
||||||
accounts."
|
|
||||||
icon={<img src={GoogleLogo} height={24} width={24} alt="Google Logo" />}
|
icon={<img src={GoogleLogo} height={24} width={24} alt="Google Logo" />}
|
||||||
config={
|
config={
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
|
|
@ -105,6 +104,6 @@ const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthent
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const meta: Route.MetaFunction = () => [{ title: "Google Authentication - God Mode" }];
|
export const meta: Route.MetaFunction = () => [{ title: "Google OAuth - NODE.DC" }];
|
||||||
|
|
||||||
export default InstanceGoogleAuthenticationPage;
|
export default InstanceGoogleAuthenticationPage;
|
||||||
|
|
|
||||||
|
|
@ -55,9 +55,8 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
|
||||||
if (!canDisable) {
|
if (!canDisable) {
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.ERROR,
|
type: TOAST_TYPE.ERROR,
|
||||||
title: "Cannot disable authentication",
|
title: "Нельзя отключить вход",
|
||||||
message:
|
message: "Должен остаться хотя бы один способ входа. Сначала включите другой способ аутентификации.",
|
||||||
"At least one authentication method must remain enabled. Please enable another method before disabling this one.",
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -74,14 +73,14 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
|
||||||
const updateConfigPromise = updateInstanceConfigurations(payload);
|
const updateConfigPromise = updateInstanceConfigurations(payload);
|
||||||
|
|
||||||
setPromiseToast(updateConfigPromise, {
|
setPromiseToast(updateConfigPromise, {
|
||||||
loading: "Saving configuration",
|
loading: "Сохранение конфигурации",
|
||||||
success: {
|
success: {
|
||||||
title: "Success",
|
title: "Сохранено",
|
||||||
message: () => "Configuration saved successfully",
|
message: () => "Конфигурация обновлена",
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
title: "Error",
|
title: "Ошибка",
|
||||||
message: () => "Failed to save configuration",
|
message: () => "Не удалось сохранить конфигурацию",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -111,8 +110,8 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
|
||||||
return (
|
return (
|
||||||
<PageWrapper
|
<PageWrapper
|
||||||
header={{
|
header={{
|
||||||
title: "Manage authentication modes for your instance",
|
title: "Способы входа в инстанс",
|
||||||
description: "Configure authentication modes for your team and restrict sign-ups to be invite only.",
|
description: "Настройте email, пароль, OAuth-провайдеры и правила регистрации пользователей.",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{formattedConfig ? (
|
{formattedConfig ? (
|
||||||
|
|
@ -120,9 +119,9 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
|
||||||
<div className={cn("flex w-full items-center gap-14 rounded-sm")}>
|
<div className={cn("flex w-full items-center gap-14 rounded-sm")}>
|
||||||
<div className="flex grow items-center gap-4">
|
<div className="flex grow items-center gap-4">
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
<div className="pb-1 text-16 font-medium">Allow anyone to sign up even without an invite</div>
|
<div className="pb-1 text-16 font-medium">Разрешить регистрацию без приглашения</div>
|
||||||
<div className={cn("text-11 leading-5 font-regular text-tertiary")}>
|
<div className={cn("text-11 leading-5 font-regular text-tertiary")}>
|
||||||
Toggling this off will only let users sign up when they are invited.
|
Если выключить, новые пользователи смогут зарегистрироваться только по приглашению.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -143,7 +142,7 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-lg pt-6 font-medium">Available authentication modes</div>
|
<div className="text-lg pt-6 font-medium">Доступные способы входа</div>
|
||||||
{authenticationModes.map((method) => (
|
{authenticationModes.map((method) => (
|
||||||
<AuthenticationMethodCard
|
<AuthenticationMethodCard
|
||||||
key={method.key}
|
key={method.key}
|
||||||
|
|
@ -169,6 +168,6 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const meta: Route.MetaFunction = () => [{ title: "Authentication Settings - Plane Web" }];
|
export const meta: Route.MetaFunction = () => [{ title: "Аутентификация - NODE.DC" }];
|
||||||
|
|
||||||
export default InstanceAuthenticationPage;
|
export default InstanceAuthenticationPage;
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ type TEmailSecurityKeys = "EMAIL_USE_TLS" | "EMAIL_USE_SSL" | "NONE";
|
||||||
const EMAIL_SECURITY_OPTIONS: { [key in TEmailSecurityKeys]: string } = {
|
const EMAIL_SECURITY_OPTIONS: { [key in TEmailSecurityKeys]: string } = {
|
||||||
EMAIL_USE_TLS: "TLS",
|
EMAIL_USE_TLS: "TLS",
|
||||||
EMAIL_USE_SSL: "SSL",
|
EMAIL_USE_SSL: "SSL",
|
||||||
NONE: "No email security",
|
NONE: "Без шифрования",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function InstanceEmailForm(props: IInstanceEmailForm) {
|
export function InstanceEmailForm(props: IInstanceEmailForm) {
|
||||||
|
|
@ -63,7 +63,7 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
|
||||||
{
|
{
|
||||||
key: "EMAIL_HOST",
|
key: "EMAIL_HOST",
|
||||||
type: "text",
|
type: "text",
|
||||||
label: "Host",
|
label: "Хост",
|
||||||
placeholder: "email.google.com",
|
placeholder: "email.google.com",
|
||||||
error: Boolean(errors.EMAIL_HOST),
|
error: Boolean(errors.EMAIL_HOST),
|
||||||
required: true,
|
required: true,
|
||||||
|
|
@ -71,7 +71,7 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
|
||||||
{
|
{
|
||||||
key: "EMAIL_PORT",
|
key: "EMAIL_PORT",
|
||||||
type: "text",
|
type: "text",
|
||||||
label: "Port",
|
label: "Порт",
|
||||||
placeholder: "8080",
|
placeholder: "8080",
|
||||||
error: Boolean(errors.EMAIL_PORT),
|
error: Boolean(errors.EMAIL_PORT),
|
||||||
required: true,
|
required: true,
|
||||||
|
|
@ -79,9 +79,9 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
|
||||||
{
|
{
|
||||||
key: "EMAIL_FROM",
|
key: "EMAIL_FROM",
|
||||||
type: "text",
|
type: "text",
|
||||||
label: "Sender's email address",
|
label: "Email отправителя",
|
||||||
description:
|
description:
|
||||||
"This is the email address your users will see when getting emails from this instance. You will need to verify this address.",
|
"Этот адрес будут видеть пользователи в письмах от инстанса. Адрес нужно подтвердить на стороне SMTP.",
|
||||||
placeholder: "no-reply@projectplane.so",
|
placeholder: "no-reply@projectplane.so",
|
||||||
error: Boolean(errors.EMAIL_FROM),
|
error: Boolean(errors.EMAIL_FROM),
|
||||||
required: true,
|
required: true,
|
||||||
|
|
@ -92,7 +92,7 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
|
||||||
{
|
{
|
||||||
key: "EMAIL_HOST_USER",
|
key: "EMAIL_HOST_USER",
|
||||||
type: "text",
|
type: "text",
|
||||||
label: "Username",
|
label: "Имя пользователя",
|
||||||
placeholder: "getitdone@projectplane.so",
|
placeholder: "getitdone@projectplane.so",
|
||||||
error: Boolean(errors.EMAIL_HOST_USER),
|
error: Boolean(errors.EMAIL_HOST_USER),
|
||||||
required: false,
|
required: false,
|
||||||
|
|
@ -100,8 +100,8 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
|
||||||
{
|
{
|
||||||
key: "EMAIL_HOST_PASSWORD",
|
key: "EMAIL_HOST_PASSWORD",
|
||||||
type: "password",
|
type: "password",
|
||||||
label: "Password",
|
label: "Пароль",
|
||||||
placeholder: "Password",
|
placeholder: "Пароль",
|
||||||
error: Boolean(errors.EMAIL_HOST_PASSWORD),
|
error: Boolean(errors.EMAIL_HOST_PASSWORD),
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
|
|
@ -114,8 +114,8 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
|
||||||
.then(() =>
|
.then(() =>
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.SUCCESS,
|
type: TOAST_TYPE.SUCCESS,
|
||||||
title: "Success",
|
title: "Сохранено",
|
||||||
message: "Email Settings updated successfully",
|
message: "Настройки почты обновлены",
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.catch((err) => console.error(err));
|
.catch((err) => console.error(err));
|
||||||
|
|
@ -163,12 +163,12 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h4 className="text-13 text-tertiary">Email security</h4>
|
<h4 className="text-13 text-tertiary">Защита соединения</h4>
|
||||||
<CustomSelect
|
<CustomSelect
|
||||||
value={emailSecurityKey}
|
value={emailSecurityKey}
|
||||||
label={EMAIL_SECURITY_OPTIONS[emailSecurityKey]}
|
label={EMAIL_SECURITY_OPTIONS[emailSecurityKey]}
|
||||||
onChange={handleEmailSecurityChange}
|
onChange={handleEmailSecurityChange}
|
||||||
buttonClassName="rounded-md border-subtle"
|
buttonClassName="nodedc-settings-select rounded-md border-subtle"
|
||||||
input
|
input
|
||||||
>
|
>
|
||||||
{Object.entries(EMAIL_SECURITY_OPTIONS).map(([key, value]) => (
|
{Object.entries(EMAIL_SECURITY_OPTIONS).map(([key, value]) => (
|
||||||
|
|
@ -183,9 +183,9 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
|
||||||
<div className="flex w-full max-w-xl flex-col gap-y-10 px-1">
|
<div className="flex w-full max-w-xl flex-col gap-y-10 px-1">
|
||||||
<div className="mr-8 flex items-center gap-10 pt-4">
|
<div className="mr-8 flex items-center gap-10 pt-4">
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
<div className="text-13 font-medium text-primary">Authentication</div>
|
<div className="text-13 font-medium text-primary">Аутентификация</div>
|
||||||
<div className="text-11 font-regular text-tertiary">
|
<div className="text-11 font-regular text-tertiary">
|
||||||
This is optional, but we recommend setting up a username and a password for your SMTP server.
|
Необязательно, но для SMTP-сервера обычно нужны имя пользователя и пароль.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -215,7 +215,7 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
disabled={!isValid || !isDirty}
|
disabled={!isValid || !isDirty}
|
||||||
>
|
>
|
||||||
{isSubmitting ? "Saving" : "Save changes"}
|
{isSubmitting ? "Сохранение" : "Сохранить изменения"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
|
@ -224,7 +224,7 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
disabled={!isValid}
|
disabled={!isValid}
|
||||||
>
|
>
|
||||||
Send test email
|
Отправить тестовое письмо
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -34,14 +34,14 @@ const InstanceEmailPage = observer(function InstanceEmailPage(_props: Route.Comp
|
||||||
await disableEmail();
|
await disableEmail();
|
||||||
setIsSMTPEnabled(false);
|
setIsSMTPEnabled(false);
|
||||||
setToast({
|
setToast({
|
||||||
title: "Email feature disabled",
|
title: "Почта отключена",
|
||||||
message: "Email feature has been disabled",
|
message: "Отправка писем через SMTP отключена",
|
||||||
type: TOAST_TYPE.SUCCESS,
|
type: TOAST_TYPE.SUCCESS,
|
||||||
});
|
});
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
setToast({
|
setToast({
|
||||||
title: "Error disabling email",
|
title: "Не удалось отключить почту",
|
||||||
message: "Failed to disable email feature. Please try again.",
|
message: "Повторите попытку.",
|
||||||
type: TOAST_TYPE.ERROR,
|
type: TOAST_TYPE.ERROR,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -60,13 +60,13 @@ const InstanceEmailPage = observer(function InstanceEmailPage(_props: Route.Comp
|
||||||
return (
|
return (
|
||||||
<PageWrapper
|
<PageWrapper
|
||||||
header={{
|
header={{
|
||||||
title: "Secure emails from your own instance",
|
title: "Письма от вашего инстанса",
|
||||||
description: (
|
description: (
|
||||||
<>
|
<>
|
||||||
Plane can send useful emails to you and your users from your own instance without talking to the Internet.
|
NODE.DC может отправлять системные письма пользователям через ваш SMTP-сервер.
|
||||||
<div className="text-13 font-regular text-tertiary">
|
<div className="text-13 font-regular text-tertiary">
|
||||||
Set it up below and please test your settings before you save them.
|
Заполните параметры ниже и проверьте отправку перед сохранением.
|
||||||
<span className="text-danger-primary">Misconfigs can lead to email bounces and errors.</span>
|
<span className="text-danger-primary">Ошибки в конфигурации приводят к отказам доставки.</span>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
|
@ -98,6 +98,6 @@ const InstanceEmailPage = observer(function InstanceEmailPage(_props: Route.Comp
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const meta: Route.MetaFunction = () => [{ title: "Email Settings - God Mode" }];
|
export const meta: Route.MetaFunction = () => [{ title: "Настройки почты - NODE.DC" }];
|
||||||
|
|
||||||
export default InstanceEmailPage;
|
export default InstanceEmailPage;
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ export function SendTestEmailModal(props: Props) {
|
||||||
setSendEmailStep(ESendEmailSteps.SUCCESS);
|
setSendEmailStep(ESendEmailSteps.SUCCESS);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
setError(error?.error || "Failed to send email");
|
setError(error?.error || "Не удалось отправить письмо");
|
||||||
setSendEmailStep(ESendEmailSteps.FAILED);
|
setSendEmailStep(ESendEmailSteps.FAILED);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|
@ -91,13 +91,13 @@ export function SendTestEmailModal(props: Props) {
|
||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative w-full transform rounded-lg bg-surface-1 p-5 px-4 text-left shadow-raised-200 transition-all sm:max-w-xl">
|
<Dialog.Panel className="nodedc-glass-modal relative w-full transform rounded-[1.75rem] bg-surface-1 p-5 px-4 text-left shadow-raised-200 transition-all sm:max-w-xl">
|
||||||
<h3 className="text-16 leading-6 font-medium text-primary">
|
<h3 className="text-16 leading-6 font-medium text-primary">
|
||||||
{sendEmailStep === ESendEmailSteps.SEND_EMAIL
|
{sendEmailStep === ESendEmailSteps.SEND_EMAIL
|
||||||
? "Send test email"
|
? "Отправить тестовое письмо"
|
||||||
: sendEmailStep === ESendEmailSteps.SUCCESS
|
: sendEmailStep === ESendEmailSteps.SUCCESS
|
||||||
? "Email send"
|
? "Письмо отправлено"
|
||||||
: "Failed"}{" "}
|
: "Ошибка отправки"}{" "}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="pt-6 pb-2">
|
<div className="pt-6 pb-2">
|
||||||
{sendEmailStep === ESendEmailSteps.SEND_EMAIL && (
|
{sendEmailStep === ESendEmailSteps.SEND_EMAIL && (
|
||||||
|
|
@ -106,28 +106,25 @@ export function SendTestEmailModal(props: Props) {
|
||||||
type="email"
|
type="email"
|
||||||
value={receiverEmail}
|
value={receiverEmail}
|
||||||
onChange={(e) => setReceiverEmail(e.target.value)}
|
onChange={(e) => setReceiverEmail(e.target.value)}
|
||||||
placeholder="Receiver email"
|
placeholder="Email получателя"
|
||||||
className="w-full resize-none text-16"
|
className="nodedc-settings-input w-full resize-none text-16"
|
||||||
tabIndex={1}
|
tabIndex={1}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{sendEmailStep === ESendEmailSteps.SUCCESS && (
|
{sendEmailStep === ESendEmailSteps.SUCCESS && (
|
||||||
<div className="flex flex-col gap-y-4 text-13">
|
<div className="flex flex-col gap-y-4 text-13">
|
||||||
<p>
|
<p>Тестовое письмо отправлено на {receiverEmail}. Если письма нет во входящих, проверьте спам.</p>
|
||||||
We have sent the test email to {receiverEmail}. Please check your spam folder if you cannot find
|
<p>Если письмо не пришло, проверьте SMTP-настройки и отправьте тест заново.</p>
|
||||||
it.
|
|
||||||
</p>
|
|
||||||
<p>If you still cannot find it, recheck your SMTP configuration and trigger a new test email.</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{sendEmailStep === ESendEmailSteps.FAILED && <div className="text-13">{error}</div>}
|
{sendEmailStep === ESendEmailSteps.FAILED && <div className="text-13">{error}</div>}
|
||||||
<div className="mt-5 flex items-center justify-end gap-2">
|
<div className="mt-5 flex items-center justify-end gap-2">
|
||||||
<Button variant="secondary" size="lg" onClick={handleClose} tabIndex={2}>
|
<Button variant="secondary" size="lg" onClick={handleClose} tabIndex={2}>
|
||||||
{sendEmailStep === ESendEmailSteps.SEND_EMAIL ? "Cancel" : "Close"}
|
{sendEmailStep === ESendEmailSteps.SEND_EMAIL ? "Отмена" : "Закрыть"}
|
||||||
</Button>
|
</Button>
|
||||||
{sendEmailStep === ESendEmailSteps.SEND_EMAIL && (
|
{sendEmailStep === ESendEmailSteps.SEND_EMAIL && (
|
||||||
<Button variant="primary" size="lg" loading={isLoading} onClick={handleSubmit} tabIndex={3}>
|
<Button variant="primary" size="lg" loading={isLoading} onClick={handleSubmit} tabIndex={3}>
|
||||||
{isLoading ? "Sending email" : "Send email"}
|
{isLoading ? "Отправка" : "Отправить"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -60,8 +60,8 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
|
||||||
.then(() =>
|
.then(() =>
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.SUCCESS,
|
type: TOAST_TYPE.SUCCESS,
|
||||||
title: "Success",
|
title: "Сохранено",
|
||||||
message: "Settings updated successfully",
|
message: "Основные настройки обновлены",
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.catch((err) => console.error(err));
|
.catch((err) => console.error(err));
|
||||||
|
|
@ -70,41 +70,41 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="text-16 font-medium text-primary">Instance details</div>
|
<div className="text-16 font-medium text-primary">Данные инстанса</div>
|
||||||
<div className="grid-col grid w-full grid-cols-1 items-center justify-between gap-8 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid-col grid w-full grid-cols-1 items-center justify-between gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||||
<ControllerInput
|
<ControllerInput
|
||||||
key="instance_name"
|
key="instance_name"
|
||||||
name="instance_name"
|
name="instance_name"
|
||||||
control={control}
|
control={control}
|
||||||
type="text"
|
type="text"
|
||||||
label="Name of instance"
|
label="Название инстанса"
|
||||||
placeholder="Instance name"
|
placeholder="Название инстанса"
|
||||||
error={Boolean(errors.instance_name)}
|
error={Boolean(errors.instance_name)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h4 className="text-13 text-tertiary">Email</h4>
|
<h4 className="text-13 text-tertiary">Email администратора</h4>
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
name="email"
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
value={instanceAdmins[0]?.user_detail?.email ?? ""}
|
value={instanceAdmins[0]?.user_detail?.email ?? ""}
|
||||||
placeholder="Admin email"
|
placeholder="Email администратора"
|
||||||
className="w-full cursor-not-allowed !text-placeholder"
|
className="nodedc-settings-input w-full cursor-not-allowed !text-placeholder"
|
||||||
autoComplete="on"
|
autoComplete="on"
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h4 className="text-13 text-tertiary">Instance ID</h4>
|
<h4 className="text-13 text-tertiary">ID инстанса</h4>
|
||||||
<Input
|
<Input
|
||||||
id="instance_id"
|
id="instance_id"
|
||||||
name="instance_id"
|
name="instance_id"
|
||||||
type="text"
|
type="text"
|
||||||
value={instance.instance_id}
|
value={instance.instance_id}
|
||||||
className="w-full cursor-not-allowed rounded-md font-medium !text-placeholder"
|
className="nodedc-settings-input w-full cursor-not-allowed rounded-md font-medium !text-placeholder"
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -112,7 +112,7 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="border-b border-subtle pb-1.5 text-16 font-medium text-primary">Chat + telemetry</div>
|
<div className="border-b border-subtle pb-1.5 text-16 font-medium text-primary">Чат и телеметрия</div>
|
||||||
<IntercomConfig isTelemetryEnabled={watch("is_telemetry_enabled") ?? false} />
|
<IntercomConfig isTelemetryEnabled={watch("is_telemetry_enabled") ?? false} />
|
||||||
<div className="flex items-center gap-14">
|
<div className="flex items-center gap-14">
|
||||||
<div className="flex grow items-center gap-4">
|
<div className="flex grow items-center gap-4">
|
||||||
|
|
@ -122,17 +122,17 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
<div className="text-13 leading-5 font-medium text-primary">Let Plane collect anonymous usage data</div>
|
<div className="text-13 leading-5 font-medium text-primary">Разрешить анонимную телеметрию</div>
|
||||||
<div className="text-11 leading-5 font-regular text-tertiary">
|
<div className="text-11 leading-5 font-regular text-tertiary">
|
||||||
No PII is collected.This anonymized data is used to understand how you use Plane and build new features
|
Персональные данные не собираются. Анонимные события помогают понимать, как используется система, с
|
||||||
in line with{" "}
|
учетом{" "}
|
||||||
<a
|
<a
|
||||||
href="https://developers.plane.so/self-hosting/telemetry"
|
href="https://developers.plane.so/self-hosting/telemetry"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-accent-primary hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
our Telemetry Policy.
|
политики телеметрии.
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -158,7 +158,7 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
|
||||||
}}
|
}}
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
>
|
>
|
||||||
{isSubmitting ? "Saving" : "Save changes"}
|
{isSubmitting ? "Сохранение" : "Сохранить изменения"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -64,10 +64,10 @@ export const IntercomConfig = observer(function IntercomConfig(props: TIntercomC
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
<div className="text-13 leading-5 font-medium text-primary">Chat with us</div>
|
<div className="text-13 leading-5 font-medium text-primary">Встроенный чат поддержки</div>
|
||||||
<div className="text-11 leading-5 font-regular text-tertiary">
|
<div className="text-11 leading-5 font-regular text-tertiary">
|
||||||
Let your users chat with us via Intercom or another service. Toggling Telemetry off turns this off
|
Разрешает пользователям писать в поддержку через Intercom или аналогичный сервис. При отключении
|
||||||
automatically.
|
телеметрии чат отключается автоматически.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,8 @@ function GeneralPage() {
|
||||||
return (
|
return (
|
||||||
<PageWrapper
|
<PageWrapper
|
||||||
header={{
|
header={{
|
||||||
title: "General settings",
|
title: "Основные настройки",
|
||||||
description:
|
description: "Имя инстанса, служебные идентификаторы, email администратора и режимы телеметрии.",
|
||||||
"Change the name of your instance and instance admin e-mail addresses. Enable or disable telemetry in your instance.",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{instance && instanceAdmins && <GeneralConfigurationForm instance={instance} instanceAdmins={instanceAdmins} />}
|
{instance && instanceAdmins && <GeneralConfigurationForm instance={instance} instanceAdmins={instanceAdmins} />}
|
||||||
|
|
@ -30,6 +29,6 @@ function GeneralPage() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const meta: Route.MetaFunction = () => [{ title: "General Settings - God Mode" }];
|
export const meta: Route.MetaFunction = () => [{ title: "Основные настройки - NODE.DC" }];
|
||||||
|
|
||||||
export default observer(GeneralPage);
|
export default observer(GeneralPage);
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,8 @@ export function InstanceImageConfigForm(props: IInstanceImageConfigForm) {
|
||||||
.then(() =>
|
.then(() =>
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.SUCCESS,
|
type: TOAST_TYPE.SUCCESS,
|
||||||
title: "Success",
|
title: "Сохранено",
|
||||||
message: "Image Configuration Settings updated successfully",
|
message: "Настройки изображений обновлены",
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.catch((err) => console.error(err));
|
.catch((err) => console.error(err));
|
||||||
|
|
@ -55,17 +55,17 @@ export function InstanceImageConfigForm(props: IInstanceImageConfigForm) {
|
||||||
control={control}
|
control={control}
|
||||||
type="password"
|
type="password"
|
||||||
name="UNSPLASH_ACCESS_KEY"
|
name="UNSPLASH_ACCESS_KEY"
|
||||||
label="Access key from your Unsplash account"
|
label="Access key аккаунта Unsplash"
|
||||||
description={
|
description={
|
||||||
<>
|
<>
|
||||||
You will find your access key in your Unsplash developer console.
|
Ключ доступа находится в консоли разработчика Unsplash.
|
||||||
<a
|
<a
|
||||||
href="https://unsplash.com/documentation#creating-a-developer-account"
|
href="https://unsplash.com/documentation#creating-a-developer-account"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-accent-primary hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
Learn more.
|
Подробнее.
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
@ -77,7 +77,7 @@ export function InstanceImageConfigForm(props: IInstanceImageConfigForm) {
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Button variant="primary" size="lg" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
<Button variant="primary" size="lg" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
||||||
{isSubmitting ? "Saving" : "Save changes"}
|
{isSubmitting ? "Сохранение" : "Сохранить изменения"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,8 @@ const InstanceImagePage = observer(function InstanceImagePage(_props: Route.Comp
|
||||||
return (
|
return (
|
||||||
<PageWrapper
|
<PageWrapper
|
||||||
header={{
|
header={{
|
||||||
title: "Third-party image libraries",
|
title: "Внешние библиотеки изображений",
|
||||||
description: "Let your users search and choose images from third-party libraries",
|
description: "Разрешите пользователям искать и выбирать изображения из внешних библиотек.",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{formattedConfig ? (
|
{formattedConfig ? (
|
||||||
|
|
@ -41,6 +41,6 @@ const InstanceImagePage = observer(function InstanceImagePage(_props: Route.Comp
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const meta: Route.MetaFunction = () => [{ title: "Images Settings - God Mode" }];
|
export const meta: Route.MetaFunction = () => [{ title: "Изображения - NODE.DC" }];
|
||||||
|
|
||||||
export default InstanceImagePage;
|
export default InstanceImagePage;
|
||||||
|
|
|
||||||
|
|
@ -38,9 +38,9 @@ function AdminLayout(_props: Route.ComponentProps) {
|
||||||
|
|
||||||
if (isUserLoggedIn) {
|
if (isUserLoggedIn) {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-screen w-screen overflow-hidden">
|
<div className="nodedc-admin-shell relative flex h-screen w-screen overflow-hidden">
|
||||||
<AdminSidebar />
|
<AdminSidebar />
|
||||||
<main className="relative flex h-full w-full flex-col overflow-hidden bg-surface-1">
|
<main className="nodedc-admin-main relative flex h-full w-full flex-col overflow-hidden bg-surface-1">
|
||||||
<AdminHeader />
|
<AdminHeader />
|
||||||
<div className="vertical-scrollbar scrollbar-md h-full w-full overflow-hidden overflow-y-scroll">
|
<div className="vertical-scrollbar scrollbar-md h-full w-full overflow-hidden overflow-y-scroll">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
|
|
||||||
|
|
@ -36,11 +36,13 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
|
||||||
|
|
||||||
const handleSignOut = () => signOut();
|
const handleSignOut = () => signOut();
|
||||||
|
|
||||||
const getSidebarMenuItems = () => (
|
const getSidebarMenuItems = (align: "left" | "right" = "left") => (
|
||||||
<Menu.Items
|
<Menu.Items
|
||||||
className={cn(
|
className={cn(
|
||||||
"shadow-lg absolute left-0 z-20 mt-1.5 flex w-52 flex-col divide-y divide-subtle rounded-md border border-subtle bg-surface-1 px-1 py-2 text-11 outline-none",
|
"nodedc-glass-popup-surface absolute z-20 mt-1.5 flex w-56 flex-col divide-y divide-white/6 px-2 py-2 text-11 outline-none",
|
||||||
{
|
{
|
||||||
|
"left-0": align === "left",
|
||||||
|
"right-0": align === "right",
|
||||||
"left-4": isSidebarCollapsed,
|
"left-4": isSidebarCollapsed,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
|
@ -52,11 +54,11 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
as="button"
|
as="button"
|
||||||
type="button"
|
type="button"
|
||||||
className="flex w-full items-center gap-2 rounded-sm px-2 py-1 hover:bg-layer-1-hover"
|
className="nodedc-settings-sidebar-item flex w-full items-center gap-2 px-2 py-1 text-left"
|
||||||
onClick={handleThemeSwitch}
|
onClick={handleThemeSwitch}
|
||||||
>
|
>
|
||||||
<Palette className="h-4 w-4 stroke-[1.5]" />
|
<Palette className="h-4 w-4 stroke-[1.5]" />
|
||||||
Switch to {resolvedTheme === "dark" ? "light" : "dark"} mode
|
{resolvedTheme === "dark" ? "Светлая тема" : "Темная тема"}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</div>
|
</div>
|
||||||
<div className="py-2">
|
<div className="py-2">
|
||||||
|
|
@ -65,10 +67,10 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
as="button"
|
as="button"
|
||||||
type="submit"
|
type="submit"
|
||||||
className="flex w-full items-center gap-2 rounded-sm px-2 py-1 hover:bg-layer-1-hover"
|
className="nodedc-settings-sidebar-item flex w-full items-center gap-2 px-2 py-1 text-left"
|
||||||
>
|
>
|
||||||
<LogOut className="h-4 w-4 stroke-[1.5]" />
|
<LogOut className="h-4 w-4 stroke-[1.5]" />
|
||||||
Sign out
|
Выйти
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -81,10 +83,10 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
|
||||||
}, [csrfToken]);
|
}, [csrfToken]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex max-h-header items-center gap-x-5 gap-y-2 border-b border-subtle px-4 py-2.5">
|
<div className="px-3 pt-4 pb-2">
|
||||||
<div className="h-full w-full truncate">
|
<div className="h-full w-full truncate">
|
||||||
<div
|
<div
|
||||||
className={`flex flex-grow items-center gap-x-2 truncate rounded-sm ${
|
className={`nodedc-admin-sidebar-profile flex flex-grow items-center gap-x-3 truncate px-3 py-3 ${
|
||||||
isSidebarCollapsed ? "justify-center" : ""
|
isSidebarCollapsed ? "justify-center" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|
@ -94,8 +96,8 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
|
||||||
"cursor-default": !isSidebarCollapsed,
|
"cursor-default": !isSidebarCollapsed,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className="flex size-8 flex-shrink-0 items-center justify-center rounded-sm bg-layer-1">
|
<div className="nodedc-admin-sidebar-avatar-button flex size-10 flex-shrink-0 items-center justify-center">
|
||||||
<UserCog2 className="size-5 text-primary" />
|
<UserCog2 className="size-5 text-[rgb(var(--nodedc-card-active-rgb))]" />
|
||||||
</div>
|
</div>
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
{isSidebarCollapsed && (
|
{isSidebarCollapsed && (
|
||||||
|
|
@ -114,38 +116,39 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
{!isSidebarCollapsed && (
|
{!isSidebarCollapsed && (
|
||||||
<div className="flex w-full gap-2">
|
<div className="min-w-0 flex-1">
|
||||||
<h4 className="grow truncate text-body-md-medium text-primary">Instance admin</h4>
|
<h4 className="truncate text-15 font-medium text-primary">Глобальный админ</h4>
|
||||||
|
<div className="truncate text-11 font-medium text-tertiary">Супер-администратор</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!isSidebarCollapsed && currentUser && (
|
||||||
|
<Menu as="div" className="relative flex-shrink-0">
|
||||||
|
<Menu.Button className="nodedc-admin-sidebar-action grid size-8 place-items-center outline-none">
|
||||||
|
<Avatar
|
||||||
|
name={currentUser.display_name}
|
||||||
|
src={getFileURL(currentUser.avatar_url)}
|
||||||
|
size={24}
|
||||||
|
shape="square"
|
||||||
|
className="!text-body-sm-medium"
|
||||||
|
/>
|
||||||
|
</Menu.Button>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
{getSidebarMenuItems("right")}
|
||||||
|
</Transition>
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isSidebarCollapsed && currentUser && (
|
|
||||||
<Menu as="div" className="relative flex-shrink-0">
|
|
||||||
<Menu.Button className="grid place-items-center outline-none">
|
|
||||||
<Avatar
|
|
||||||
name={currentUser.display_name}
|
|
||||||
src={getFileURL(currentUser.avatar_url)}
|
|
||||||
size={24}
|
|
||||||
shape="square"
|
|
||||||
className="!text-body-sm-medium"
|
|
||||||
/>
|
|
||||||
</Menu.Button>
|
|
||||||
|
|
||||||
<Transition
|
|
||||||
as={Fragment}
|
|
||||||
enter="transition ease-out duration-100"
|
|
||||||
enterFrom="transform opacity-0 scale-95"
|
|
||||||
enterTo="transform opacity-100 scale-100"
|
|
||||||
leave="transition ease-in duration-75"
|
|
||||||
leaveFrom="transform opacity-100 scale-100"
|
|
||||||
leaveTo="transform opacity-0 scale-95"
|
|
||||||
>
|
|
||||||
{getSidebarMenuItems()}
|
|
||||||
</Transition>
|
|
||||||
</Menu>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -20,17 +20,17 @@ import { useInstance, useTheme } from "@/hooks/store";
|
||||||
|
|
||||||
const helpOptions = [
|
const helpOptions = [
|
||||||
{
|
{
|
||||||
name: "Documentation",
|
name: "Документация",
|
||||||
href: "https://docs.plane.so/",
|
href: "https://docs.plane.so/",
|
||||||
Icon: PageIcon,
|
Icon: PageIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Join our Forum",
|
name: "Форум Plane",
|
||||||
href: "https://forum.plane.so",
|
href: "https://forum.plane.so",
|
||||||
Icon: MessageSquare,
|
Icon: MessageSquare,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Report a bug",
|
name: "Сообщить об ошибке",
|
||||||
href: "https://github.com/makeplane/plane/issues/new/choose",
|
href: "https://github.com/makeplane/plane/issues/new/choose",
|
||||||
Icon: GithubIcon,
|
Icon: GithubIcon,
|
||||||
},
|
},
|
||||||
|
|
@ -50,26 +50,26 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-14 w-full flex-shrink-0 items-center justify-between gap-1 self-baseline border-t border-subtle bg-surface-1 px-4",
|
"flex h-16 w-full flex-shrink-0 items-center justify-between gap-1 self-baseline border-t border-white/6 px-3",
|
||||||
{
|
{
|
||||||
"h-auto flex-col py-1.5": isSidebarCollapsed,
|
"h-auto flex-col py-1.5": isSidebarCollapsed,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className={`flex items-center gap-1 ${isSidebarCollapsed ? "flex-col justify-center" : "w-full"}`}>
|
<div className={`flex items-center gap-1 ${isSidebarCollapsed ? "flex-col justify-center" : "w-full"}`}>
|
||||||
<Tooltip tooltipContent="Redirect to Plane" position="right" className="ml-4" disabled={!isSidebarCollapsed}>
|
<Tooltip tooltipContent="Перейти в приложение" position="right" className="ml-4" disabled={!isSidebarCollapsed}>
|
||||||
<a
|
<a
|
||||||
href={redirectionLink}
|
href={redirectionLink}
|
||||||
className={`relative flex items-center gap-1 rounded-sm bg-layer-1 px-2 py-1 text-body-xs-medium whitespace-nowrap text-secondary`}
|
className="nodedc-admin-sidebar-action relative flex items-center gap-1 px-3 py-1 text-12 font-medium whitespace-nowrap"
|
||||||
>
|
>
|
||||||
<NewTabIcon width={14} height={14} />
|
<NewTabIcon width={14} height={14} />
|
||||||
{!isSidebarCollapsed && "Redirect to Plane"}
|
{!isSidebarCollapsed && "В приложение"}
|
||||||
</a>
|
</a>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip tooltipContent="Help" position={isSidebarCollapsed ? "right" : "top"} className="ml-4">
|
<Tooltip tooltipContent="Помощь" position={isSidebarCollapsed ? "right" : "top"} className="ml-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`ml-auto grid place-items-center rounded-md p-1.5 text-secondary outline-none hover:bg-layer-1-hover hover:text-primary ${
|
className={`nodedc-admin-sidebar-action ml-auto grid place-items-center p-1.5 outline-none ${
|
||||||
isSidebarCollapsed ? "w-full" : ""
|
isSidebarCollapsed ? "w-full" : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
|
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
|
||||||
|
|
@ -77,10 +77,10 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
|
||||||
<HelpCircle className="size-4" />
|
<HelpCircle className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip tooltipContent="Toggle sidebar" position={isSidebarCollapsed ? "right" : "top"} className="ml-4">
|
<Tooltip tooltipContent="Свернуть меню" position={isSidebarCollapsed ? "right" : "top"} className="ml-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`grid place-items-center rounded-md p-1.5 text-secondary outline-none hover:bg-layer-1-hover hover:text-primary ${
|
className={`nodedc-admin-sidebar-action grid place-items-center p-1.5 outline-none ${
|
||||||
isSidebarCollapsed ? "w-full" : ""
|
isSidebarCollapsed ? "w-full" : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => toggleSidebar(!isSidebarCollapsed)}
|
onClick={() => toggleSidebar(!isSidebarCollapsed)}
|
||||||
|
|
@ -103,7 +103,7 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
|
||||||
<div
|
<div
|
||||||
className={`absolute bottom-2 z-[15] min-w-[10rem] ${
|
className={`absolute bottom-2 z-[15] min-w-[10rem] ${
|
||||||
isSidebarCollapsed ? "left-full" : "-left-[75px]"
|
isSidebarCollapsed ? "left-full" : "-left-[75px]"
|
||||||
} divide-y divide-subtle-1 rounded-sm bg-surface-1 p-1 whitespace-nowrap shadow-raised-100`}
|
} nodedc-glass-popup-surface divide-y divide-subtle-1 rounded-sm bg-surface-1 p-1 whitespace-nowrap shadow-raised-100`}
|
||||||
ref={helpOptionsRef}
|
ref={helpOptionsRef}
|
||||||
>
|
>
|
||||||
<div className="space-y-1 pb-2">
|
<div className="space-y-1 pb-2">
|
||||||
|
|
@ -134,7 +134,7 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="px-2 pt-2 pb-1 text-10">Version: v{instance?.current_version}</div>
|
<div className="px-2 pt-2 pb-1 text-10">Версия: v{instance?.current_version}</div>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,10 @@ export const AdminSidebarMenu = observer(function AdminSidebarMenu() {
|
||||||
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
||||||
// derived values
|
// derived values
|
||||||
const sidebarMenu = useSidebarMenu();
|
const sidebarMenu = useSidebarMenu();
|
||||||
|
const sidebarMenuGroups = [
|
||||||
|
{ label: "ИНСТАНС", items: sidebarMenu.slice(0, 4) },
|
||||||
|
{ label: "ВОЗМОЖНОСТИ", items: sidebarMenu.slice(4) },
|
||||||
|
];
|
||||||
|
|
||||||
const handleItemClick = () => {
|
const handleItemClick = () => {
|
||||||
if (window.innerWidth < 768) {
|
if (window.innerWidth < 768) {
|
||||||
|
|
@ -29,36 +33,33 @@ export const AdminSidebarMenu = observer(function AdminSidebarMenu() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="vertical-scrollbar flex scrollbar-sm h-full w-full flex-col gap-2.5 overflow-y-scroll px-4 py-4">
|
<div className="vertical-scrollbar flex scrollbar-sm h-full w-full flex-col overflow-y-scroll px-3 py-4">
|
||||||
{sidebarMenu.map((item, index) => {
|
{sidebarMenuGroups.map((group) => (
|
||||||
const isActive = item.href === pathName || pathName?.includes(item.href);
|
<div key={group.label} className="shrink-0 border-b border-white/6 py-3 first:pt-0 last:border-b-0 last:pb-0">
|
||||||
return (
|
{!isSidebarCollapsed && <div className="nodedc-admin-sidebar-section-label">{group.label}</div>}
|
||||||
<Link key={index} href={item.href} onClick={handleItemClick}>
|
<div className="flex flex-col gap-1">
|
||||||
<div>
|
{group.items.map((item) => {
|
||||||
<Tooltip tooltipContent={item.name} position="right" className="ml-2" disabled={!isSidebarCollapsed}>
|
const isActive = item.href === pathName || Boolean(pathName?.startsWith(item.href));
|
||||||
<div
|
return (
|
||||||
className={cn(
|
<Link key={item.href} href={item.href} onClick={handleItemClick}>
|
||||||
"group flex w-full items-center gap-3 rounded-md px-3 py-2 transition-colors outline-none",
|
<Tooltip tooltipContent={item.name} position="right" className="ml-2" disabled={!isSidebarCollapsed}>
|
||||||
{
|
<div
|
||||||
"!bg-layer-transparent-active text-primary": isActive,
|
className={cn(
|
||||||
"text-secondary hover:bg-layer-transparent-hover active:bg-layer-transparent-active": !isActive,
|
"nodedc-settings-sidebar-item group flex w-full items-center gap-3 px-3 py-2 text-14 font-medium transition-colors outline-none",
|
||||||
},
|
isSidebarCollapsed ? "justify-center" : "w-full"
|
||||||
isSidebarCollapsed ? "justify-center" : "w-[260px]"
|
)}
|
||||||
)}
|
data-active={isActive}
|
||||||
>
|
>
|
||||||
{<item.Icon className="h-4 w-4 flex-shrink-0" />}
|
{<item.Icon className="h-4 w-4 flex-shrink-0" />}
|
||||||
{!isSidebarCollapsed && (
|
{!isSidebarCollapsed && <div className="min-w-0 truncate transition-colors">{item.name}</div>}
|
||||||
<div className="w-full">
|
|
||||||
<div className={cn(`text-body-xs-medium transition-colors`)}>{item.name}</div>
|
|
||||||
<div className={cn(`text-caption-sm-regular transition-colors`)}>{item.description}</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</Tooltip>
|
||||||
</div>
|
</Link>
|
||||||
</Tooltip>
|
);
|
||||||
</div>
|
})}
|
||||||
</Link>
|
</div>
|
||||||
);
|
</div>
|
||||||
})}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ export const AdminSidebar = observer(function AdminSidebar() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`fixed inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-subtle bg-surface-1 duration-300 md:relative ${isSidebarCollapsed ? "-ml-[290px]" : ""} sm:${isSidebarCollapsed ? "-ml-[290px]" : ""} md:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[290px]"} lg:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[290px]"} `}
|
className={`nodedc-glass-sidebar fixed inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-subtle bg-surface-1 duration-300 md:relative ${isSidebarCollapsed ? "-ml-[290px]" : ""} sm:${isSidebarCollapsed ? "-ml-[290px]" : ""} md:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[290px]"} lg:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[290px]"} `}
|
||||||
>
|
>
|
||||||
<div ref={ref} className="flex h-full w-full flex-1 flex-col">
|
<div ref={ref} className="flex h-full w-full flex-1 flex-col">
|
||||||
<AdminSidebarDropdown />
|
<AdminSidebarDropdown />
|
||||||
|
|
|
||||||
|
|
@ -56,16 +56,16 @@ export function WorkspaceCreateForm() {
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.SUCCESS,
|
type: TOAST_TYPE.SUCCESS,
|
||||||
title: "Success!",
|
title: "Готово",
|
||||||
message: "Workspace created successfully.",
|
message: "Воркспейс создан.",
|
||||||
});
|
});
|
||||||
router.push(`/workspace`);
|
router.push(`/workspace`);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.ERROR,
|
type: TOAST_TYPE.ERROR,
|
||||||
title: "Error!",
|
title: "Ошибка",
|
||||||
message: "Workspace could not be created. Please try again.",
|
message: "Не удалось создать воркспейс. Попробуйте еще раз.",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else setSlugError(true);
|
} else setSlugError(true);
|
||||||
|
|
@ -73,8 +73,8 @@ export function WorkspaceCreateForm() {
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.ERROR,
|
type: TOAST_TYPE.ERROR,
|
||||||
title: "Error!",
|
title: "Ошибка",
|
||||||
message: "Some error occurred while creating workspace. Please try again.",
|
message: "При создании воркспейса произошла ошибка. Попробуйте еще раз.",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -91,7 +91,7 @@ export function WorkspaceCreateForm() {
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div className="grid-col grid w-full max-w-4xl grid-cols-1 items-start justify-between gap-x-10 gap-y-6 lg:grid-cols-2">
|
<div className="grid-col grid w-full max-w-4xl grid-cols-1 items-start justify-between gap-x-10 gap-y-6 lg:grid-cols-2">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h4 className="text-13 text-tertiary">Name your workspace</h4>
|
<h4 className="text-13 text-tertiary">Название воркспейса</h4>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
|
|
@ -113,8 +113,8 @@ export function WorkspaceCreateForm() {
|
||||||
}}
|
}}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
hasError={Boolean(errors.name)}
|
hasError={Boolean(errors.name)}
|
||||||
placeholder="Something familiar and recognizable is always best."
|
placeholder="Короткое понятное название"
|
||||||
className="w-full"
|
className="nodedc-settings-input w-full"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -122,8 +122,8 @@ export function WorkspaceCreateForm() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h4 className="text-13 text-tertiary">Set your workspace's URL</h4>
|
<h4 className="text-13 text-tertiary">URL воркспейса</h4>
|
||||||
<div className="flex w-full items-center gap-0.5 rounded-md border-[0.5px] border-subtle px-3">
|
<div className="nodedc-settings-input flex w-full items-center gap-0.5 rounded-md border-[0.5px] border-subtle px-3">
|
||||||
<span className="text-13 whitespace-nowrap text-secondary">{workspaceBaseURL}</span>
|
<span className="text-13 whitespace-nowrap text-secondary">{workspaceBaseURL}</span>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
|
|
@ -149,29 +149,29 @@ export function WorkspaceCreateForm() {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{slugError && <p className="text-13 text-danger-primary">This URL is taken. Try something else.</p>}
|
{slugError && <p className="text-13 text-danger-primary">Этот URL уже занят. Выберите другой.</p>}
|
||||||
{invalidSlug && (
|
{invalidSlug && (
|
||||||
<p className="text-13 text-danger-primary">{`URLs can contain only ( - ), ( _ ) and alphanumeric characters.`}</p>
|
<p className="text-13 text-danger-primary">{`URL может содержать только латинские буквы, цифры, "-" и "_".`}</p>
|
||||||
)}
|
)}
|
||||||
{errors.slug && <span className="text-11 text-danger-primary">{errors.slug.message}</span>}
|
{errors.slug && <span className="text-11 text-danger-primary">{errors.slug.message}</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h4 className="text-13 text-tertiary">How many people will use this workspace?</h4>
|
<h4 className="text-13 text-tertiary">Сколько людей будет работать в воркспейсе?</h4>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Controller
|
<Controller
|
||||||
name="organization_size"
|
name="organization_size"
|
||||||
control={control}
|
control={control}
|
||||||
rules={{ required: "This is a required field." }}
|
rules={{ required: "Это обязательное поле." }}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<CustomSelect
|
<CustomSelect
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
label={
|
label={
|
||||||
ORGANIZATION_SIZE.find((c) => c === value) ?? (
|
ORGANIZATION_SIZE.find((c) => c === value) ?? (
|
||||||
<span className="text-placeholder">Select a range</span>
|
<span className="text-placeholder">Выберите диапазон</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
buttonClassName="!border-[0.5px] !border-subtle !shadow-none"
|
buttonClassName="nodedc-settings-select !border-[0.5px] !border-subtle !shadow-none"
|
||||||
input
|
input
|
||||||
>
|
>
|
||||||
{ORGANIZATION_SIZE.map((item) => (
|
{ORGANIZATION_SIZE.map((item) => (
|
||||||
|
|
@ -196,10 +196,10 @@ export function WorkspaceCreateForm() {
|
||||||
disabled={!isValid}
|
disabled={!isValid}
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
>
|
>
|
||||||
{isSubmitting ? "Creating workspace" : "Create workspace"}
|
{isSubmitting ? "Создание" : "Создать воркспейс"}
|
||||||
</Button>
|
</Button>
|
||||||
<Link className={getButtonStyling("secondary", "lg")} href="/workspace">
|
<Link className={getButtonStyling("secondary", "lg")} href="/workspace">
|
||||||
Go back
|
Назад
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ const WorkspaceCreatePage = observer(function WorkspaceCreatePage(_props: Route.
|
||||||
return (
|
return (
|
||||||
<PageWrapper
|
<PageWrapper
|
||||||
header={{
|
header={{
|
||||||
title: "Create a new workspace on this instance.",
|
title: "Создать новый воркспейс",
|
||||||
description: "You will need to invite users from Workspace Settings after you create this workspace.",
|
description: "После создания пригласите пользователей в настройках рабочего пространства.",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<WorkspaceCreateForm />
|
<WorkspaceCreateForm />
|
||||||
|
|
@ -25,6 +25,6 @@ const WorkspaceCreatePage = observer(function WorkspaceCreatePage(_props: Route.
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const meta: Route.MetaFunction = () => [{ title: "Create Workspace - God Mode" }];
|
export const meta: Route.MetaFunction = () => [{ title: "Создание воркспейса - NODE.DC" }];
|
||||||
|
|
||||||
export default WorkspaceCreatePage;
|
export default WorkspaceCreatePage;
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,16 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { PlaneLockup } from "@plane/propel/icons";
|
|
||||||
|
|
||||||
export function AuthHeader() {
|
export function AuthHeader() {
|
||||||
return (
|
return (
|
||||||
<div className="sticky top-0 flex w-full flex-shrink-0 items-center justify-between gap-6">
|
<div className="sticky top-0 flex w-full flex-shrink-0 items-center justify-between gap-6">
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<PlaneLockup height={20} width={95} className="text-primary" />
|
<span className="tracking-normal text-16 font-semibold text-primary">NODE.DC</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
<span className="rounded-full bg-white/6 px-3 py-1 text-11 font-medium text-secondary">
|
||||||
|
Глобальное администрирование
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,56 +22,56 @@ const errorCodeMessages: {
|
||||||
} = {
|
} = {
|
||||||
// admin
|
// admin
|
||||||
[EAdminAuthErrorCodes.ADMIN_ALREADY_EXIST]: {
|
[EAdminAuthErrorCodes.ADMIN_ALREADY_EXIST]: {
|
||||||
title: `Admin already exists`,
|
title: `Администратор уже существует`,
|
||||||
message: () => `Admin already exists. Please try again.`,
|
message: () => `Администратор уже существует. Попробуйте еще раз.`,
|
||||||
},
|
},
|
||||||
[EAdminAuthErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME]: {
|
[EAdminAuthErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME]: {
|
||||||
title: `Email, password and first name required`,
|
title: `Нужны email, пароль и имя`,
|
||||||
message: () => `Email, password and first name required. Please try again.`,
|
message: () => `Укажите email, пароль и имя. Попробуйте еще раз.`,
|
||||||
},
|
},
|
||||||
[EAdminAuthErrorCodes.INVALID_ADMIN_EMAIL]: {
|
[EAdminAuthErrorCodes.INVALID_ADMIN_EMAIL]: {
|
||||||
title: `Invalid admin email`,
|
title: `Некорректный email администратора`,
|
||||||
message: () => `Invalid admin email. Please try again.`,
|
message: () => `Некорректный email администратора. Попробуйте еще раз.`,
|
||||||
},
|
},
|
||||||
[EAdminAuthErrorCodes.INVALID_ADMIN_PASSWORD]: {
|
[EAdminAuthErrorCodes.INVALID_ADMIN_PASSWORD]: {
|
||||||
title: `Invalid admin password`,
|
title: `Некорректный пароль администратора`,
|
||||||
message: () => `Invalid admin password. Please try again.`,
|
message: () => `Некорректный пароль администратора. Попробуйте еще раз.`,
|
||||||
},
|
},
|
||||||
[EAdminAuthErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD]: {
|
[EAdminAuthErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD]: {
|
||||||
title: `Email and password required`,
|
title: `Нужны email и пароль`,
|
||||||
message: () => `Email and password required. Please try again.`,
|
message: () => `Укажите email и пароль. Попробуйте еще раз.`,
|
||||||
},
|
},
|
||||||
[EAdminAuthErrorCodes.ADMIN_AUTHENTICATION_FAILED]: {
|
[EAdminAuthErrorCodes.ADMIN_AUTHENTICATION_FAILED]: {
|
||||||
title: `Authentication failed`,
|
title: `Ошибка входа`,
|
||||||
message: () => `Authentication failed. Please try again.`,
|
message: () => `Не удалось войти. Проверьте данные и попробуйте еще раз.`,
|
||||||
},
|
},
|
||||||
[EAdminAuthErrorCodes.ADMIN_USER_ALREADY_EXIST]: {
|
[EAdminAuthErrorCodes.ADMIN_USER_ALREADY_EXIST]: {
|
||||||
title: `Admin user already exists`,
|
title: `Администратор уже существует`,
|
||||||
message: () => (
|
message: () => (
|
||||||
<div>
|
<div>
|
||||||
Admin user already exists.
|
Администратор уже существует.
|
||||||
<Link className="font-medium underline underline-offset-4 transition-all hover:font-bold" href={`/admin`}>
|
<Link className="font-medium underline underline-offset-4 transition-all hover:font-bold" href={`/admin`}>
|
||||||
Sign In
|
Войти
|
||||||
</Link>
|
</Link>
|
||||||
now.
|
сейчас.
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
[EAdminAuthErrorCodes.ADMIN_USER_DOES_NOT_EXIST]: {
|
[EAdminAuthErrorCodes.ADMIN_USER_DOES_NOT_EXIST]: {
|
||||||
title: `Admin user does not exist`,
|
title: `Администратор не найден`,
|
||||||
message: () => (
|
message: () => (
|
||||||
<div>
|
<div>
|
||||||
Admin user does not exist.
|
Администратор не найден.
|
||||||
<Link className="font-medium underline underline-offset-4 transition-all hover:font-bold" href={`/admin`}>
|
<Link className="font-medium underline underline-offset-4 transition-all hover:font-bold" href={`/admin`}>
|
||||||
Sign In
|
Войти
|
||||||
</Link>
|
</Link>
|
||||||
now.
|
сейчас.
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
[EAdminAuthErrorCodes.ADMIN_USER_DEACTIVATED]: {
|
[EAdminAuthErrorCodes.ADMIN_USER_DEACTIVATED]: {
|
||||||
title: `User account deactivated`,
|
title: `Аккаунт деактивирован`,
|
||||||
message: () => `User account deactivated. Please contact ${SUPPORT_EMAIL ? SUPPORT_EMAIL : "administrator"}.`,
|
message: () => `Аккаунт деактивирован. Свяжитесь с ${SUPPORT_EMAIL ? SUPPORT_EMAIL : "администратором"}.`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -92,8 +92,8 @@ export const authErrorHandler = (errorCode: EAdminAuthErrorCodes, email?: string
|
||||||
return {
|
return {
|
||||||
type: EErrorAlertType.BANNER_ALERT,
|
type: EErrorAlertType.BANNER_ALERT,
|
||||||
code: errorCode,
|
code: errorCode,
|
||||||
title: errorCodeMessages[errorCode]?.title || "Error",
|
title: errorCodeMessages[errorCode]?.title || "Ошибка",
|
||||||
message: errorCodeMessages[errorCode]?.message(email) || "Something went wrong. Please try again.",
|
message: errorCodeMessages[errorCode]?.message(email) || "Что-то пошло не так. Попробуйте еще раз.",
|
||||||
};
|
};
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ function RootLayout() {
|
||||||
}, [replace, isUserLoggedIn]);
|
}, [replace, isUserLoggedIn]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex h-screen w-screen flex-col items-center overflow-hidden overflow-y-auto bg-surface-1 px-8 pt-6 pb-10">
|
<div className="nodedc-auth-shell relative z-10 flex h-screen w-screen flex-col items-center overflow-hidden overflow-y-auto bg-surface-1 px-8 pt-6 pb-10">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,6 @@ function HomePage() {
|
||||||
export default observer(HomePage);
|
export default observer(HomePage);
|
||||||
|
|
||||||
export const meta: Route.MetaFunction = () => [
|
export const meta: Route.MetaFunction = () => [
|
||||||
{ title: "Admin – Instance Setup & Sign-In" },
|
{ title: "NODE.DC - вход в админ-панель" },
|
||||||
{ name: "description", content: "Configure your Plane instance or sign in to the admin portal." },
|
{ name: "description", content: "Настройка инстанса NODE.DC и вход в глобальную админ-панель." },
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -112,10 +112,10 @@ export function InstanceSignInForm() {
|
||||||
<>
|
<>
|
||||||
<AuthHeader />
|
<AuthHeader />
|
||||||
<div className="mt-10 flex w-full flex-grow flex-col items-center justify-center py-6">
|
<div className="mt-10 flex w-full flex-grow flex-col items-center justify-center py-6">
|
||||||
<div className="relative flex w-full max-w-[22.5rem] flex-col gap-6">
|
<div className="nodedc-auth-card relative flex w-full max-w-[22.5rem] flex-col gap-6">
|
||||||
<FormHeader
|
<FormHeader
|
||||||
heading="Manage your Plane instance"
|
heading="Управление инстансом NODE.DC"
|
||||||
subHeading="Configure instance-wide settings to secure your instance"
|
subHeading="Войдите, чтобы менять глобальные настройки системы"
|
||||||
/>
|
/>
|
||||||
<form
|
<form
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
|
|
@ -135,10 +135,10 @@ export function InstanceSignInForm() {
|
||||||
|
|
||||||
<div className="w-full space-y-1">
|
<div className="w-full space-y-1">
|
||||||
<label className="text-13 font-medium text-tertiary" htmlFor="email">
|
<label className="text-13 font-medium text-tertiary" htmlFor="email">
|
||||||
Email <span className="text-danger-primary">*</span>
|
Электронная почта <span className="text-danger-primary">*</span>
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
className="nodedc-settings-input w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||||
id="email"
|
id="email"
|
||||||
name="email"
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
|
|
@ -153,16 +153,16 @@ export function InstanceSignInForm() {
|
||||||
|
|
||||||
<div className="w-full space-y-1">
|
<div className="w-full space-y-1">
|
||||||
<label className="text-13 font-medium text-tertiary" htmlFor="password">
|
<label className="text-13 font-medium text-tertiary" htmlFor="password">
|
||||||
Password <span className="text-danger-primary">*</span>
|
Пароль <span className="text-danger-primary">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
className="nodedc-settings-input w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
type={showPassword ? "text" : "password"}
|
type={showPassword ? "text" : "password"}
|
||||||
inputSize="md"
|
inputSize="md"
|
||||||
placeholder="Enter your password"
|
placeholder="Введите пароль"
|
||||||
value={formData.password}
|
value={formData.password}
|
||||||
onChange={(e) => handleFormChange("password", e.target.value)}
|
onChange={(e) => handleFormChange("password", e.target.value)}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
|
|
@ -188,7 +188,7 @@ export function InstanceSignInForm() {
|
||||||
</div>
|
</div>
|
||||||
<div className="py-2">
|
<div className="py-2">
|
||||||
<Button type="submit" size="xl" className="w-full" disabled={isButtonDisabled}>
|
<Button type="submit" size="xl" className="w-full" disabled={isButtonDisabled}>
|
||||||
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Sign in"}
|
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Войти"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -17,19 +17,16 @@ function PageNotFound() {
|
||||||
<div className="grid h-full place-items-center p-4">
|
<div className="grid h-full place-items-center p-4">
|
||||||
<div className="space-y-8 text-center">
|
<div className="space-y-8 text-center">
|
||||||
<div className="relative mx-auto h-60 w-60 lg:h-80 lg:w-80">
|
<div className="relative mx-auto h-60 w-60 lg:h-80 lg:w-80">
|
||||||
<img src={Image404} alt="404 - Page not found" className="h-full w-full object-contain" />
|
<img src={Image404} alt="404 - страница не найдена" className="h-full w-full object-contain" />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h3 className="text-16 font-semibold">Oops! Something went wrong.</h3>
|
<h3 className="text-16 font-semibold">Страница не найдена</h3>
|
||||||
<p className="text-13 text-secondary">
|
<p className="text-13 text-secondary">Похоже, раздел был удален, переименован или временно недоступен.</p>
|
||||||
Sorry, the page you are looking for cannot be found. It may have been removed, had its name changed, or is
|
|
||||||
temporarily unavailable.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<Link to="/general/">
|
<Link to="/general/">
|
||||||
<span className="flex justify-center py-4">
|
<span className="flex justify-center py-4">
|
||||||
<Button variant="secondary" size="lg">
|
<Button variant="secondary" size="lg">
|
||||||
Go to general settings
|
Перейти в основные настройки
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,8 @@ import interVariableWoff2 from "@fontsource-variable/inter/files/inter-latin-wgh
|
||||||
import "@fontsource/material-symbols-rounded";
|
import "@fontsource/material-symbols-rounded";
|
||||||
import "@fontsource/ibm-plex-mono";
|
import "@fontsource/ibm-plex-mono";
|
||||||
|
|
||||||
const APP_TITLE = "Plane | Simple, extensible, open-source project management tool.";
|
const APP_TITLE = "NODE.DC | Глобальное администрирование";
|
||||||
const APP_DESCRIPTION =
|
const APP_DESCRIPTION = "Панель глобального администрирования инстанса NODE.DC.";
|
||||||
"Open-source project management tool to manage work items, sprints, and product roadmaps with peace of mind.";
|
|
||||||
|
|
||||||
export const links: LinksFunction = () => [
|
export const links: LinksFunction = () => [
|
||||||
{ rel: "apple-touch-icon", sizes: "180x180", href: appleTouchIcon },
|
{ rel: "apple-touch-icon", sizes: "180x180", href: appleTouchIcon },
|
||||||
|
|
@ -43,7 +42,7 @@ export const links: LinksFunction = () => [
|
||||||
|
|
||||||
export function Layout({ children }: { children: ReactNode }) {
|
export function Layout({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="ru" suppressHydrationWarning>
|
||||||
<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" />
|
||||||
|
|
@ -66,15 +65,14 @@ export const meta: Route.MetaFunction = () => [
|
||||||
{ property: "og:url", content: "https://plane.so/" },
|
{ property: "og:url", content: "https://plane.so/" },
|
||||||
{
|
{
|
||||||
name: "keywords",
|
name: "keywords",
|
||||||
content:
|
content: "NODE.DC, администрирование, рабочие пространства, проекты, пользователи, настройки инстанса",
|
||||||
"software development, customer feedback, software, accelerate, code management, release management, project management, work items tracking, agile, scrum, kanban, collaboration",
|
|
||||||
},
|
},
|
||||||
{ name: "twitter:site", content: "@planepowers" },
|
{ name: "twitter:site", content: "@nodedc" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Root() {
|
export default function Root() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-canvas">
|
<div className="nodedc-admin-root min-h-screen bg-canvas">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -91,7 +89,7 @@ export function HydrateFallback() {
|
||||||
export function ErrorBoundary({ error: _error }: Route.ErrorBoundaryProps) {
|
export function ErrorBoundary({ error: _error }: Route.ErrorBoundaryProps) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p>Something went wrong.</p>
|
<p>Что-то пошло не так.</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ export function AuthenticationMethodCard(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("flex w-full items-center gap-14 rounded-lg bg-layer-2", {
|
className={cn("nodedc-settings-card flex w-full items-center gap-14 rounded-lg bg-layer-2", {
|
||||||
"border border-subtle px-4 py-3": withBorder,
|
"border border-subtle px-4 py-3": withBorder,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ export const GiteaConfiguration = observer(function GiteaConfiguration(props: Pr
|
||||||
{GiteaConfigured ? (
|
{GiteaConfigured ? (
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Link href="/authentication/gitea" className={cn(getButtonStyling("link", "base"), "font-medium")}>
|
<Link href="/authentication/gitea" className={cn(getButtonStyling("link", "base"), "font-medium")}>
|
||||||
Edit
|
Изменить
|
||||||
</Link>
|
</Link>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
value={Boolean(parseInt(GiteaConfig))}
|
value={Boolean(parseInt(GiteaConfig))}
|
||||||
|
|
@ -51,7 +51,7 @@ export const GiteaConfiguration = observer(function GiteaConfiguration(props: Pr
|
||||||
) : (
|
) : (
|
||||||
<Link href="/authentication/gitea" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}>
|
<Link href="/authentication/gitea" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}>
|
||||||
<Settings2 className="h-4 w-4 p-0.5 text-tertiary" />
|
<Settings2 className="h-4 w-4 p-0.5 text-tertiary" />
|
||||||
Configure
|
Настроить
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ export const GithubConfiguration = observer(function GithubConfiguration(props:
|
||||||
{isGithubConfigured ? (
|
{isGithubConfigured ? (
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Link href="/authentication/github" className={cn(getButtonStyling("link", "base"), "font-medium")}>
|
<Link href="/authentication/github" className={cn(getButtonStyling("link", "base"), "font-medium")}>
|
||||||
Edit
|
Изменить
|
||||||
</Link>
|
</Link>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
value={Boolean(parseInt(enableGithubConfig))}
|
value={Boolean(parseInt(enableGithubConfig))}
|
||||||
|
|
@ -49,7 +49,7 @@ export const GithubConfiguration = observer(function GithubConfiguration(props:
|
||||||
) : (
|
) : (
|
||||||
<Link href="/authentication/github" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}>
|
<Link href="/authentication/github" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}>
|
||||||
<Settings2 className="h-4 w-4 p-0.5 text-tertiary" />
|
<Settings2 className="h-4 w-4 p-0.5 text-tertiary" />
|
||||||
Configure
|
Настроить
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ export const GitlabConfiguration = observer(function GitlabConfiguration(props:
|
||||||
{isGitlabConfigured ? (
|
{isGitlabConfigured ? (
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Link href="/authentication/gitlab" className={cn(getButtonStyling("link", "base"), "font-medium")}>
|
<Link href="/authentication/gitlab" className={cn(getButtonStyling("link", "base"), "font-medium")}>
|
||||||
Edit
|
Изменить
|
||||||
</Link>
|
</Link>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
value={Boolean(parseInt(enableGitlabConfig))}
|
value={Boolean(parseInt(enableGitlabConfig))}
|
||||||
|
|
@ -49,7 +49,7 @@ export const GitlabConfiguration = observer(function GitlabConfiguration(props:
|
||||||
) : (
|
) : (
|
||||||
<Link href="/authentication/gitlab" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}>
|
<Link href="/authentication/gitlab" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}>
|
||||||
<Settings2 className="h-4 w-4 p-0.5 text-tertiary" />
|
<Settings2 className="h-4 w-4 p-0.5 text-tertiary" />
|
||||||
Configure
|
Настроить
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ export const GoogleConfiguration = observer(function GoogleConfiguration(props:
|
||||||
{isGoogleConfigured ? (
|
{isGoogleConfigured ? (
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Link href="/authentication/google" className={cn(getButtonStyling("link", "base"), "font-medium")}>
|
<Link href="/authentication/google" className={cn(getButtonStyling("link", "base"), "font-medium")}>
|
||||||
Edit
|
Изменить
|
||||||
</Link>
|
</Link>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
value={Boolean(parseInt(enableGoogleConfig))}
|
value={Boolean(parseInt(enableGoogleConfig))}
|
||||||
|
|
@ -49,7 +49,7 @@ export const GoogleConfiguration = observer(function GoogleConfiguration(props:
|
||||||
) : (
|
) : (
|
||||||
<Link href="/authentication/google" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}>
|
<Link href="/authentication/google" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}>
|
||||||
<Settings2 className="h-4 w-4 p-0.5 text-tertiary" />
|
<Settings2 className="h-4 w-4 p-0.5 text-tertiary" />
|
||||||
Configure
|
Настроить
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -11,26 +11,35 @@ type Props = {
|
||||||
label?: string;
|
label?: string;
|
||||||
href?: string;
|
href?: string;
|
||||||
icon?: React.ReactNode | undefined;
|
icon?: React.ReactNode | undefined;
|
||||||
|
isCurrent?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function BreadcrumbLink(props: Props) {
|
export function BreadcrumbLink(props: Props) {
|
||||||
const { href, label, icon } = props;
|
const { href, label, icon, isCurrent = false } = props;
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<>
|
||||||
|
{icon && <div className="flex h-5 w-5 items-center justify-center overflow-hidden !text-16">{icon}</div>}
|
||||||
|
<div className="relative line-clamp-1 block max-w-[150px] truncate overflow-hidden">{label}</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip tooltipContent={label} position="bottom">
|
<Tooltip tooltipContent={label} position="bottom">
|
||||||
<li className="flex items-center space-x-2" tabIndex={-1}>
|
<li className="flex items-center" tabIndex={-1}>
|
||||||
<div className="flex flex-wrap items-center gap-2.5">
|
{href && !isCurrent ? (
|
||||||
{href ? (
|
<Link className="nodedc-admin-breadcrumb-pill flex items-center gap-1.5 text-13 font-medium" href={href}>
|
||||||
<Link className="flex items-center gap-1 text-13 font-medium text-tertiary hover:text-primary" href={href}>
|
{content}
|
||||||
{icon && <div className="flex h-5 w-5 items-center justify-center overflow-hidden !text-16">{icon}</div>}
|
</Link>
|
||||||
<div className="relative line-clamp-1 block max-w-[150px] truncate overflow-hidden">{label}</div>
|
) : (
|
||||||
</Link>
|
<div
|
||||||
) : (
|
className="nodedc-admin-breadcrumb-pill flex cursor-default items-center gap-1.5 text-13 font-medium"
|
||||||
<div className="flex cursor-default items-center gap-1 text-13 font-medium text-primary">
|
data-current={isCurrent}
|
||||||
{icon && <div className="flex h-5 w-5 items-center justify-center overflow-hidden">{icon}</div>}
|
aria-current={isCurrent ? "page" : undefined}
|
||||||
<div className="relative line-clamp-1 block max-w-[150px] truncate overflow-hidden">{label}</div>
|
>
|
||||||
</div>
|
{content}
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</li>
|
</li>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export function CodeBlock({ children, className, darkerShade }: TProps) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-md border border-subtle bg-surface-2 px-0.5 text-11 font-semibold text-tertiary",
|
"nodedc-code-chip rounded-md border border-subtle bg-surface-2 px-0.5 text-11 font-semibold text-tertiary",
|
||||||
{
|
{
|
||||||
"border-subtle bg-layer-1 text-secondary": darkerShade,
|
"border-subtle bg-layer-1 text-secondary": darkerShade,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -45,27 +45,25 @@ export function ConfirmDiscardModal(props: Props) {
|
||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-surface-1 text-left shadow-raised-200 transition-all sm:my-8 sm:w-[30rem]">
|
<Dialog.Panel className="nodedc-glass-modal relative transform overflow-hidden rounded-[1.75rem] bg-surface-1 text-left shadow-raised-200 transition-all sm:my-8 sm:w-[30rem]">
|
||||||
<div className="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
<div className="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||||
<div className="sm:flex sm:items-start">
|
<div className="sm:flex sm:items-start">
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:text-left">
|
<div className="mt-3 text-center sm:mt-0 sm:text-left">
|
||||||
<Dialog.Title as="h3" className="text-16 leading-6 font-medium text-tertiary">
|
<Dialog.Title as="h3" className="text-16 leading-6 font-medium text-tertiary">
|
||||||
You have unsaved changes
|
Есть несохраненные изменения
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<p className="text-13 text-placeholder">
|
<p className="text-13 text-placeholder">Если уйти назад, текущие правки будут потеряны.</p>
|
||||||
Changes you made will be lost if you go back. Do you wish to go back?
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-end gap-2 p-4 sm:px-6">
|
<div className="flex items-center justify-end gap-2 p-4 sm:px-6">
|
||||||
<Button variant="secondary" size="lg" onClick={handleClose}>
|
<Button variant="secondary" size="lg" onClick={handleClose}>
|
||||||
Keep editing
|
Продолжить редактирование
|
||||||
</Button>
|
</Button>
|
||||||
<Link href={onDiscardHref} className={getButtonStyling("primary", "base")}>
|
<Link href={onDiscardHref} className={getButtonStyling("primary", "base")}>
|
||||||
Go back
|
Уйти назад
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ export function ControllerInput(props: Props) {
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={name}
|
name={name}
|
||||||
rules={{ required: required ? `${label} is required.` : false }}
|
rules={{ required: required ? `Поле "${label}" обязательно.` : false }}
|
||||||
render={({ field: { value, onChange, ref } }) => (
|
render={({ field: { value, onChange, ref } }) => (
|
||||||
<Input
|
<Input
|
||||||
id={name}
|
id={name}
|
||||||
|
|
@ -57,7 +57,7 @@ export function ControllerInput(props: Props) {
|
||||||
ref={ref}
|
ref={ref}
|
||||||
hasError={error}
|
hasError={error}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
className={cn("w-full rounded-md font-medium", {
|
className={cn("nodedc-settings-input w-full rounded-md font-medium", {
|
||||||
"pr-10": type === "password",
|
"pr-10": type === "password",
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export function ControllerSwitch<T extends FieldValues>(props: Props<T>) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between gap-1">
|
<div className="flex items-center justify-between gap-1">
|
||||||
<h4 className="text-sm text-custom-text-300">Refresh user attributes from {label} during sign in</h4>
|
<h4 className="text-sm text-custom-text-300">Обновлять атрибуты пользователя из {label} при входе</h4>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
|
|
|
||||||
|
|
@ -32,18 +32,18 @@ export function CopyField(props: Props) {
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="flex items-center justify-between py-2"
|
className="nodedc-settings-secondary-button flex w-full items-center justify-between gap-3 py-2 text-left"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(url);
|
navigator.clipboard.writeText(url);
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.INFO,
|
type: TOAST_TYPE.INFO,
|
||||||
title: "Copied to clipboard",
|
title: "Скопировано",
|
||||||
message: `The ${label} has been successfully copied to your clipboard`,
|
message: `${label} скопировано в буфер обмена`,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p className="text-13 font-medium">{url}</p>
|
<p className="min-w-0 truncate text-13 font-medium">{url}</p>
|
||||||
<CopyIcon width={18} height={18} color="#B9B9B9" />
|
<CopyIcon width={18} height={18} color="#B9B9B9" className="shrink-0" />
|
||||||
</Button>
|
</Button>
|
||||||
<div className="text-11 text-tertiary">{description}</div>
|
<div className="text-11 text-tertiary">{description}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,15 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const CORE_HEADER_SEGMENT_LABELS: Record<string, string> = {
|
export const CORE_HEADER_SEGMENT_LABELS: Record<string, string> = {
|
||||||
general: "General",
|
general: "Основное",
|
||||||
ai: "Artificial Intelligence",
|
ai: "Искусственный интеллект",
|
||||||
email: "Email",
|
email: "Почта",
|
||||||
authentication: "Authentication",
|
authentication: "Аутентификация",
|
||||||
image: "Image",
|
image: "Изображения",
|
||||||
google: "Google",
|
google: "Google",
|
||||||
github: "GitHub",
|
github: "GitHub",
|
||||||
gitlab: "GitLab",
|
gitlab: "GitLab",
|
||||||
gitea: "Gitea",
|
gitea: "Gitea",
|
||||||
workspace: "Workspace",
|
workspace: "Воркспейсы",
|
||||||
create: "Create",
|
create: "Создание",
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,10 @@
|
||||||
* See the LICENSE file for details.
|
* See the LICENSE file for details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Fragment } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { Menu, Settings } from "lucide-react";
|
import { ChevronRight, Menu, Settings } from "lucide-react";
|
||||||
// icons
|
|
||||||
import { Breadcrumbs } from "@plane/ui";
|
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink } from "../breadcrumb-link";
|
import { BreadcrumbLink } from "../breadcrumb-link";
|
||||||
// hooks
|
// hooks
|
||||||
|
|
@ -22,6 +21,7 @@ export const HamburgerToggle = observer(function HamburgerToggle() {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="group flex size-7 cursor-pointer items-center justify-center rounded-sm bg-layer-1 transition-all hover:bg-layer-1-hover md:hidden"
|
className="group flex size-7 cursor-pointer items-center justify-center rounded-sm bg-layer-1 transition-all hover:bg-layer-1-hover md:hidden"
|
||||||
|
aria-label="Открыть меню"
|
||||||
onClick={() => toggleSidebar(!isSidebarCollapsed)}
|
onClick={() => toggleSidebar(!isSidebarCollapsed)}
|
||||||
>
|
>
|
||||||
<Menu size={14} className="text-secondary transition-all group-hover:text-primary" />
|
<Menu size={14} className="text-secondary transition-all group-hover:text-primary" />
|
||||||
|
|
@ -56,32 +56,28 @@ export const AdminHeader = observer(function AdminHeader() {
|
||||||
const breadcrumbItems = generateBreadcrumbItems(pathName || "");
|
const breadcrumbItems = generateBreadcrumbItems(pathName || "");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex h-header w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-subtle bg-surface-1 p-4">
|
<div className="nodedc-admin-header nodedc-glass-modal relative z-10 flex h-header w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-subtle bg-surface-1 p-4">
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
<HamburgerToggle />
|
<HamburgerToggle />
|
||||||
{breadcrumbItems.length >= 0 && (
|
{breadcrumbItems.length >= 0 && (
|
||||||
<div>
|
<nav className="min-w-0" aria-label="Навигация God Mode">
|
||||||
<Breadcrumbs>
|
<ol className="nodedc-admin-breadcrumbs">
|
||||||
<Breadcrumbs.Item
|
<BreadcrumbLink href="/general/" label="Настройки" icon={<Settings className="h-4 w-4" />} />
|
||||||
component={
|
{breadcrumbItems.map((item, index) => {
|
||||||
<BreadcrumbLink
|
if (!item.title) return null;
|
||||||
href="/general/"
|
const isCurrent = index === breadcrumbItems.length - 1;
|
||||||
label="Settings"
|
|
||||||
icon={<Settings className="h-4 w-4 text-tertiary" />}
|
return (
|
||||||
/>
|
<Fragment key={`${item.href}-${item.title}`}>
|
||||||
}
|
<li className="nodedc-admin-breadcrumb-separator" aria-hidden="true">
|
||||||
/>
|
<ChevronRight className="size-4" />
|
||||||
{breadcrumbItems.map(
|
</li>
|
||||||
(item) =>
|
<BreadcrumbLink href={item.href} label={item.title} isCurrent={isCurrent} />
|
||||||
item.title && (
|
</Fragment>
|
||||||
<Breadcrumbs.Item
|
);
|
||||||
key={item.title}
|
})}
|
||||||
component={<BreadcrumbLink href={item.href} label={item.title} />}
|
</ol>
|
||||||
/>
|
</nav>
|
||||||
)
|
|
||||||
)}
|
|
||||||
</Breadcrumbs>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -24,20 +24,19 @@ export const NewUserPopup = observer(function NewUserPopup() {
|
||||||
|
|
||||||
if (!isNewUserPopup) return <></>;
|
if (!isNewUserPopup) return <></>;
|
||||||
return (
|
return (
|
||||||
<div className="shadow-md absolute right-8 bottom-8 w-96 rounded-lg border border-subtle bg-surface-1 p-6">
|
<div className="nodedc-glass-modal shadow-md absolute right-8 bottom-8 w-96 rounded-[1.75rem] border border-subtle bg-surface-1 p-6">
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
<div className="text-14 font-semibold">Create workspace</div>
|
<div className="text-14 font-semibold">Создать воркспейс</div>
|
||||||
<div className="py-2 text-13 font-medium text-tertiary">
|
<div className="py-2 text-13 font-medium text-tertiary">
|
||||||
Instance setup done! Welcome to Plane instance portal. Start your journey with by creating your first
|
Настройка инстанса завершена. Создайте первое рабочее пространство, чтобы начать работу.
|
||||||
workspace.
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4 pt-2">
|
<div className="flex items-center gap-4 pt-2">
|
||||||
<Link href="/workspace/create" className={getButtonStyling("primary", "lg")}>
|
<Link href="/workspace/create" className={getButtonStyling("primary", "lg")}>
|
||||||
Create workspace
|
Создать воркспейс
|
||||||
</Link>
|
</Link>
|
||||||
<Button variant="secondary" size="lg" onClick={toggleNewUserPopup}>
|
<Button variant="secondary" size="lg" onClick={toggleNewUserPopup}>
|
||||||
Close
|
Закрыть
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -46,7 +45,7 @@ export const NewUserPopup = observer(function NewUserPopup() {
|
||||||
src={resolveGeneralTheme(resolvedTheme) === "dark" ? TakeoffIconDark : TakeoffIconLight}
|
src={resolveGeneralTheme(resolvedTheme) === "dark" ? TakeoffIconDark : TakeoffIconLight}
|
||||||
height={80}
|
height={80}
|
||||||
width={80}
|
width={80}
|
||||||
alt="Plane icon"
|
alt="NODE.DC"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ type TPageHeader = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function PageHeader(props: TPageHeader) {
|
export function PageHeader(props: TPageHeader) {
|
||||||
const { title = "God Mode - Plane", description = "Plane god mode" } = props;
|
const { title = "NODE.DC - глобальное администрирование", description = "Глобальное администрирование NODE.DC" } =
|
||||||
|
props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -24,16 +24,16 @@ export const PageWrapper = (props: TPageWrapperProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("mx-auto h-full w-full space-y-6 py-4", {
|
className={cn("nodedc-page mx-auto h-full w-full space-y-6 py-4", {
|
||||||
"max-w-[1000px] md:px-4 2xl:max-w-[1200px]": size === "md",
|
"max-w-[1000px] md:px-4 2xl:max-w-[1200px]": size === "md",
|
||||||
"px-4 lg:px-12": size === "lg",
|
"px-4 lg:px-12": size === "lg",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{customHeader ? (
|
{customHeader ? (
|
||||||
<div className="mx-4 shrink-0 space-y-1 border-b border-subtle py-4">{customHeader}</div>
|
<div className="nodedc-page-header mx-4 shrink-0 space-y-1 border-b border-subtle py-4">{customHeader}</div>
|
||||||
) : (
|
) : (
|
||||||
header && (
|
header && (
|
||||||
<div className="mx-4 flex shrink-0 items-center justify-between gap-4 space-y-1 border-b border-subtle py-4">
|
<div className="nodedc-page-header mx-4 flex shrink-0 items-center justify-between gap-4 space-y-1 border-b border-subtle py-4">
|
||||||
<div className={header.actions ? "flex flex-col gap-1" : "space-y-1"}>
|
<div className={header.actions ? "flex flex-col gap-1" : "space-y-1"}>
|
||||||
<div className="text-h5-semibold text-primary">{header.title}</div>
|
<div className="text-h5-semibold text-primary">{header.title}</div>
|
||||||
<div className="text-body-sm-regular text-secondary">{header.description}</div>
|
<div className="text-body-sm-regular text-secondary">{header.description}</div>
|
||||||
|
|
@ -42,7 +42,7 @@ export const PageWrapper = (props: TPageWrapperProps) => {
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
<div className="vertical-scrollbar scrollbar-sm flex-grow overflow-hidden overflow-y-scroll px-4 pb-4">
|
<div className="nodedc-page-body vertical-scrollbar scrollbar-sm flex-grow overflow-hidden overflow-y-scroll px-4 pb-4">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -27,15 +27,15 @@ export const InstanceFailureView = observer(function InstanceFailureView() {
|
||||||
<div className="mt-10 flex w-full flex-grow flex-col items-center justify-center py-6">
|
<div className="mt-10 flex w-full flex-grow flex-col items-center justify-center py-6">
|
||||||
<div className="relative flex w-full max-w-[22.5rem] flex-col gap-6">
|
<div className="relative flex w-full max-w-[22.5rem] flex-col gap-6">
|
||||||
<div className="relative flex flex-col items-center justify-center space-y-4">
|
<div className="relative flex flex-col items-center justify-center space-y-4">
|
||||||
<img src={instanceImage} alt="Instance failure illustration" />
|
<img src={instanceImage} alt="Ошибка загрузки инстанса" />
|
||||||
<h3 className="text-center text-20 font-medium text-on-color">Unable to fetch instance details.</h3>
|
<h3 className="text-center text-20 font-medium text-on-color">Не удалось загрузить данные инстанса.</h3>
|
||||||
<p className="text-center text-14 font-medium">
|
<p className="text-center text-14 font-medium">
|
||||||
We were unable to fetch the details of the instance. Fret not, it might just be a connectivity issue.
|
Проверьте соединение с API и попробуйте обновить страницу.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<Button size="lg" onClick={handleRetry}>
|
<Button size="lg" onClick={handleRetry}>
|
||||||
Retry
|
Повторить
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,15 @@ export function InstanceNotReady() {
|
||||||
<div className="relative container mx-auto flex h-full w-full items-center justify-center px-5">
|
<div className="relative container mx-auto flex h-full w-full items-center justify-center px-5">
|
||||||
<div className="relative w-auto max-w-2xl space-y-8 py-10">
|
<div className="relative w-auto max-w-2xl space-y-8 py-10">
|
||||||
<div className="relative flex flex-col items-center justify-center space-y-4">
|
<div className="relative flex flex-col items-center justify-center space-y-4">
|
||||||
<h1 className="pb-3 text-24 font-bold">Welcome aboard Plane!</h1>
|
<h1 className="pb-3 text-24 font-bold">Добро пожаловать в NODE.DC</h1>
|
||||||
<img src={PlaneTakeOffImage} alt="Plane Logo" />
|
<img src={PlaneTakeOffImage} alt="NODE.DC" />
|
||||||
<p className="text-14 font-medium text-placeholder">Get started by setting up your instance and workspace</p>
|
<p className="text-14 font-medium text-placeholder">Начните с настройки инстанса и первого воркспейса</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Link href={"/setup/?auth_enabled=0"}>
|
<Link href={"/setup/?auth_enabled=0"}>
|
||||||
<Button size="xl" className="w-full">
|
<Button size="xl" className="w-full">
|
||||||
Get started
|
Начать
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -140,10 +140,10 @@ export function InstanceSetupForm() {
|
||||||
<>
|
<>
|
||||||
<AuthHeader />
|
<AuthHeader />
|
||||||
<div className="mt-10 flex w-full flex-grow flex-col items-center justify-center py-6">
|
<div className="mt-10 flex w-full flex-grow flex-col items-center justify-center py-6">
|
||||||
<div className="relative flex w-full max-w-[22.5rem] flex-col gap-6">
|
<div className="nodedc-auth-card relative flex w-full max-w-[22.5rem] flex-col gap-6">
|
||||||
<FormHeader
|
<FormHeader
|
||||||
heading="Setup your Plane Instance"
|
heading="Настройка инстанса NODE.DC"
|
||||||
subHeading="Post setup you will be able to manage this Plane instance."
|
subHeading="После настройки вы получите доступ к глобальному администрированию."
|
||||||
/>
|
/>
|
||||||
{errorData.type &&
|
{errorData.type &&
|
||||||
errorData?.message &&
|
errorData?.message &&
|
||||||
|
|
@ -163,15 +163,15 @@ export function InstanceSetupForm() {
|
||||||
<div className="flex flex-col items-center gap-4 sm:flex-row">
|
<div className="flex flex-col items-center gap-4 sm:flex-row">
|
||||||
<div className="w-full space-y-1">
|
<div className="w-full space-y-1">
|
||||||
<label className="text-13 font-medium text-tertiary" htmlFor="first_name">
|
<label className="text-13 font-medium text-tertiary" htmlFor="first_name">
|
||||||
First name <span className="text-danger-primary">*</span>
|
Имя <span className="text-danger-primary">*</span>
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
className="nodedc-settings-input w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||||
id="first_name"
|
id="first_name"
|
||||||
name="first_name"
|
name="first_name"
|
||||||
type="text"
|
type="text"
|
||||||
inputSize="md"
|
inputSize="md"
|
||||||
placeholder="Wilber"
|
placeholder="Иван"
|
||||||
value={formData.first_name}
|
value={formData.first_name}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const validation = validatePersonName(e.target.value);
|
const validation = validatePersonName(e.target.value);
|
||||||
|
|
@ -186,15 +186,15 @@ export function InstanceSetupForm() {
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full space-y-1">
|
<div className="w-full space-y-1">
|
||||||
<label className="text-13 font-medium text-tertiary" htmlFor="last_name">
|
<label className="text-13 font-medium text-tertiary" htmlFor="last_name">
|
||||||
Last name <span className="text-danger-primary">*</span>
|
Фамилия <span className="text-danger-primary">*</span>
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
className="nodedc-settings-input w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||||
id="last_name"
|
id="last_name"
|
||||||
name="last_name"
|
name="last_name"
|
||||||
type="text"
|
type="text"
|
||||||
inputSize="md"
|
inputSize="md"
|
||||||
placeholder="Wright"
|
placeholder="Иванов"
|
||||||
value={formData.last_name}
|
value={formData.last_name}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const validation = validatePersonName(e.target.value);
|
const validation = validatePersonName(e.target.value);
|
||||||
|
|
@ -210,10 +210,10 @@ export function InstanceSetupForm() {
|
||||||
|
|
||||||
<div className="w-full space-y-1">
|
<div className="w-full space-y-1">
|
||||||
<label className="text-13 font-medium text-tertiary" htmlFor="email">
|
<label className="text-13 font-medium text-tertiary" htmlFor="email">
|
||||||
Email <span className="text-danger-primary">*</span>
|
Электронная почта <span className="text-danger-primary">*</span>
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
className="nodedc-settings-input w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||||
id="email"
|
id="email"
|
||||||
name="email"
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
|
|
@ -231,15 +231,15 @@ export function InstanceSetupForm() {
|
||||||
|
|
||||||
<div className="w-full space-y-1">
|
<div className="w-full space-y-1">
|
||||||
<label className="text-13 font-medium text-tertiary" htmlFor="company_name">
|
<label className="text-13 font-medium text-tertiary" htmlFor="company_name">
|
||||||
Company name <span className="text-danger-primary">*</span>
|
Название компании <span className="text-danger-primary">*</span>
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
className="nodedc-settings-input w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||||
id="company_name"
|
id="company_name"
|
||||||
name="company_name"
|
name="company_name"
|
||||||
type="text"
|
type="text"
|
||||||
inputSize="md"
|
inputSize="md"
|
||||||
placeholder="Company name"
|
placeholder="Название компании"
|
||||||
value={formData.company_name}
|
value={formData.company_name}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const validation = validateCompanyName(e.target.value, false);
|
const validation = validateCompanyName(e.target.value, false);
|
||||||
|
|
@ -253,16 +253,16 @@ export function InstanceSetupForm() {
|
||||||
|
|
||||||
<div className="w-full space-y-1">
|
<div className="w-full space-y-1">
|
||||||
<label className="text-13 font-medium text-tertiary" htmlFor="password">
|
<label className="text-13 font-medium text-tertiary" htmlFor="password">
|
||||||
Set a password <span className="text-danger-primary">*</span>
|
Задайте пароль <span className="text-danger-primary">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
className="nodedc-settings-input w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
type={showPassword.password ? "text" : "password"}
|
type={showPassword.password ? "text" : "password"}
|
||||||
inputSize="md"
|
inputSize="md"
|
||||||
placeholder="New password"
|
placeholder="Новый пароль"
|
||||||
value={formData.password}
|
value={formData.password}
|
||||||
onChange={(e) => handleFormChange("password", e.target.value)}
|
onChange={(e) => handleFormChange("password", e.target.value)}
|
||||||
hasError={errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD ? true : false}
|
hasError={errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD ? true : false}
|
||||||
|
|
@ -298,7 +298,7 @@ export function InstanceSetupForm() {
|
||||||
|
|
||||||
<div className="w-full space-y-1">
|
<div className="w-full space-y-1">
|
||||||
<label className="text-13 font-medium text-tertiary" htmlFor="confirm_password">
|
<label className="text-13 font-medium text-tertiary" htmlFor="confirm_password">
|
||||||
Confirm password <span className="text-danger-primary">*</span>
|
Подтвердите пароль <span className="text-danger-primary">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -308,8 +308,8 @@ export function InstanceSetupForm() {
|
||||||
inputSize="md"
|
inputSize="md"
|
||||||
value={formData.confirm_password}
|
value={formData.confirm_password}
|
||||||
onChange={(e) => handleFormChange("confirm_password", e.target.value)}
|
onChange={(e) => handleFormChange("confirm_password", e.target.value)}
|
||||||
placeholder="Confirm password"
|
placeholder="Повторите пароль"
|
||||||
className="w-full border border-subtle !bg-surface-1 pr-12 placeholder:text-placeholder"
|
className="nodedc-settings-input w-full border border-subtle !bg-surface-1 pr-12 placeholder:text-placeholder"
|
||||||
onFocus={() => setIsRetryPasswordInputFocused(true)}
|
onFocus={() => setIsRetryPasswordInputFocused(true)}
|
||||||
onBlur={() => setIsRetryPasswordInputFocused(false)}
|
onBlur={() => setIsRetryPasswordInputFocused(false)}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
|
|
@ -336,9 +336,7 @@ export function InstanceSetupForm() {
|
||||||
</div>
|
</div>
|
||||||
{!!formData.confirm_password &&
|
{!!formData.confirm_password &&
|
||||||
formData.password !== formData.confirm_password &&
|
formData.password !== formData.confirm_password &&
|
||||||
renderPasswordMatchError && (
|
renderPasswordMatchError && <span className="text-13 text-danger-primary">Пароли не совпадают</span>}
|
||||||
<span className="text-13 text-danger-primary">Passwords don{"'"}t match</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative flex gap-2">
|
<div className="relative flex gap-2">
|
||||||
|
|
@ -352,7 +350,7 @@ export function InstanceSetupForm() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<label className="cursor-pointer text-13 font-medium text-tertiary" htmlFor="is_telemetry_enabled">
|
<label className="cursor-pointer text-13 font-medium text-tertiary" htmlFor="is_telemetry_enabled">
|
||||||
Allow Plane to anonymously collect usage events.{" "}
|
Разрешить NODE.DC анонимно собирать события использования.{" "}
|
||||||
<a
|
<a
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
href="https://developers.plane.so/self-hosting/telemetry"
|
href="https://developers.plane.so/self-hosting/telemetry"
|
||||||
|
|
@ -360,14 +358,14 @@ export function InstanceSetupForm() {
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-blue-500 hover:text-blue-600 flex-shrink-0 text-13 font-medium"
|
className="text-blue-500 hover:text-blue-600 flex-shrink-0 text-13 font-medium"
|
||||||
>
|
>
|
||||||
See More
|
Подробнее
|
||||||
</a>
|
</a>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="py-2">
|
<div className="py-2">
|
||||||
<Button type="submit" size="xl" className="w-full" disabled={isButtonDisabled}>
|
<Button type="submit" size="xl" className="w-full" disabled={isButtonDisabled}>
|
||||||
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
|
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Продолжить"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -35,17 +35,16 @@ export const getCoreAuthenticationModesMap: (
|
||||||
}) => ({
|
}) => ({
|
||||||
"unique-codes": {
|
"unique-codes": {
|
||||||
key: "unique-codes",
|
key: "unique-codes",
|
||||||
name: "Unique codes",
|
name: "Одноразовые коды",
|
||||||
description:
|
description: "Вход и регистрация по кодам из email. Для этого способа нужен настроенный SMTP.",
|
||||||
"Log in or sign up for Plane using codes sent via email. You need to have set up SMTP to use this method.",
|
|
||||||
icon: <Mails className="h-6 w-6 p-0.5 text-tertiary" />,
|
icon: <Mails className="h-6 w-6 p-0.5 text-tertiary" />,
|
||||||
config: <EmailCodesConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
config: <EmailCodesConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||||
enabledConfigKey: "ENABLE_MAGIC_LINK_LOGIN",
|
enabledConfigKey: "ENABLE_MAGIC_LINK_LOGIN",
|
||||||
},
|
},
|
||||||
"passwords-login": {
|
"passwords-login": {
|
||||||
key: "passwords-login",
|
key: "passwords-login",
|
||||||
name: "Passwords",
|
name: "Пароли",
|
||||||
description: "Allow members to create accounts with passwords and use it with their email addresses to sign in.",
|
description: "Пользователи создают аккаунты с паролем и входят по email.",
|
||||||
icon: <KeyRound className="h-6 w-6 p-0.5 text-tertiary" />,
|
icon: <KeyRound className="h-6 w-6 p-0.5 text-tertiary" />,
|
||||||
config: <PasswordLoginConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
config: <PasswordLoginConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||||
enabledConfigKey: "ENABLE_EMAIL_PASSWORD",
|
enabledConfigKey: "ENABLE_EMAIL_PASSWORD",
|
||||||
|
|
@ -53,7 +52,7 @@ export const getCoreAuthenticationModesMap: (
|
||||||
google: {
|
google: {
|
||||||
key: "google",
|
key: "google",
|
||||||
name: "Google",
|
name: "Google",
|
||||||
description: "Allow members to log in or sign up for Plane with their Google accounts.",
|
description: "Вход и регистрация через аккаунты Google.",
|
||||||
icon: <img src={googleLogo} height={20} width={20} alt="Google Logo" />,
|
icon: <img src={googleLogo} height={20} width={20} alt="Google Logo" />,
|
||||||
config: <GoogleConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
config: <GoogleConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||||
enabledConfigKey: "IS_GOOGLE_ENABLED",
|
enabledConfigKey: "IS_GOOGLE_ENABLED",
|
||||||
|
|
@ -61,7 +60,7 @@ export const getCoreAuthenticationModesMap: (
|
||||||
github: {
|
github: {
|
||||||
key: "github",
|
key: "github",
|
||||||
name: "GitHub",
|
name: "GitHub",
|
||||||
description: "Allow members to log in or sign up for Plane with their GitHub accounts.",
|
description: "Вход и регистрация через аккаунты GitHub.",
|
||||||
icon: (
|
icon: (
|
||||||
<img
|
<img
|
||||||
src={resolvedTheme === "dark" ? githubDarkModeImage : githubLightModeImage}
|
src={resolvedTheme === "dark" ? githubDarkModeImage : githubLightModeImage}
|
||||||
|
|
@ -76,7 +75,7 @@ export const getCoreAuthenticationModesMap: (
|
||||||
gitlab: {
|
gitlab: {
|
||||||
key: "gitlab",
|
key: "gitlab",
|
||||||
name: "GitLab",
|
name: "GitLab",
|
||||||
description: "Allow members to log in or sign up to plane with their GitLab accounts.",
|
description: "Вход и регистрация через аккаунты GitLab.",
|
||||||
icon: <img src={gitlabLogo} height={20} width={20} alt="GitLab Logo" />,
|
icon: <img src={gitlabLogo} height={20} width={20} alt="GitLab Logo" />,
|
||||||
config: <GitlabConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
config: <GitlabConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||||
enabledConfigKey: "IS_GITLAB_ENABLED",
|
enabledConfigKey: "IS_GITLAB_ENABLED",
|
||||||
|
|
@ -84,7 +83,7 @@ export const getCoreAuthenticationModesMap: (
|
||||||
gitea: {
|
gitea: {
|
||||||
key: "gitea",
|
key: "gitea",
|
||||||
name: "Gitea",
|
name: "Gitea",
|
||||||
description: "Allow members to log in or sign up to plane with their Gitea accounts.",
|
description: "Вход и регистрация через аккаунты Gitea.",
|
||||||
icon: <img src={giteaLogo} height={20} width={20} alt="Gitea Logo" />,
|
icon: <img src={giteaLogo} height={20} width={20} alt="Gitea Logo" />,
|
||||||
config: <GiteaConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
config: <GiteaConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||||
enabledConfigKey: "IS_GITEA_ENABLED",
|
enabledConfigKey: "IS_GITEA_ENABLED",
|
||||||
|
|
|
||||||
|
|
@ -15,38 +15,38 @@ export type TCoreSidebarMenuKey = "general" | "email" | "workspace" | "authentic
|
||||||
export const coreSidebarMenuLinks: Record<TCoreSidebarMenuKey, TSidebarMenuItem> = {
|
export const coreSidebarMenuLinks: Record<TCoreSidebarMenuKey, TSidebarMenuItem> = {
|
||||||
general: {
|
general: {
|
||||||
Icon: Cog,
|
Icon: Cog,
|
||||||
name: "General",
|
name: "Основное",
|
||||||
description: "Identify your instances and get key details.",
|
description: "Имя инстанса, ID и телеметрия.",
|
||||||
href: `/general/`,
|
href: `/general/`,
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
Icon: Mail,
|
Icon: Mail,
|
||||||
name: "Email",
|
name: "Почта",
|
||||||
description: "Configure your SMTP controls.",
|
description: "SMTP и тестовая отправка.",
|
||||||
href: `/email/`,
|
href: `/email/`,
|
||||||
},
|
},
|
||||||
workspace: {
|
workspace: {
|
||||||
Icon: WorkspaceIcon,
|
Icon: WorkspaceIcon,
|
||||||
name: "Workspaces",
|
name: "Воркспейсы",
|
||||||
description: "Manage all workspaces on this instance.",
|
description: "Все рабочие пространства инстанса.",
|
||||||
href: `/workspace/`,
|
href: `/workspace/`,
|
||||||
},
|
},
|
||||||
authentication: {
|
authentication: {
|
||||||
Icon: LockIcon,
|
Icon: LockIcon,
|
||||||
name: "Authentication",
|
name: "Аутентификация",
|
||||||
description: "Configure authentication modes.",
|
description: "Вход, регистрация и OAuth.",
|
||||||
href: `/authentication/`,
|
href: `/authentication/`,
|
||||||
},
|
},
|
||||||
ai: {
|
ai: {
|
||||||
Icon: BrainCog,
|
Icon: BrainCog,
|
||||||
name: "Artificial intelligence",
|
name: "ИИ",
|
||||||
description: "Configure your OpenAI creds.",
|
description: "OpenAI модель и ключ API.",
|
||||||
href: `/ai/`,
|
href: `/ai/`,
|
||||||
},
|
},
|
||||||
image: {
|
image: {
|
||||||
Icon: Image,
|
Icon: Image,
|
||||||
name: "Images in Plane",
|
name: "Изображения",
|
||||||
description: "Allow third-party image libraries.",
|
description: "Внешние библиотеки изображений.",
|
||||||
href: `/image/`,
|
href: `/image/`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ const DEFAULT_SWR_CONFIG = {
|
||||||
|
|
||||||
export function CoreProviders({ children }: { children: React.ReactNode }) {
|
export function CoreProviders({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
|
<ThemeProvider themes={["light", "dark"]} defaultTheme="dark" enableSystem>
|
||||||
<AppProgressBar />
|
<AppProgressBar />
|
||||||
<ToastWithTheme />
|
<ToastWithTheme />
|
||||||
<SWRConfig value={DEFAULT_SWR_CONFIG}>
|
<SWRConfig value={DEFAULT_SWR_CONFIG}>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,27 @@
|
||||||
@import "@plane/tailwind-config/index.css";
|
@import "@plane/tailwind-config/index.css";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--nodedc-accent-rgb: 195 255 102;
|
||||||
|
--nodedc-on-accent-rgb: 11 17 23;
|
||||||
|
--nodedc-card-passive-rgb: 42 43 46;
|
||||||
|
--nodedc-on-card-passive-rgb: 245 247 251;
|
||||||
|
--nodedc-card-active-rgb: 195 255 102;
|
||||||
|
--nodedc-on-card-active-rgb: 11 17 23;
|
||||||
|
--brand-default: rgb(var(--nodedc-accent-rgb));
|
||||||
|
--brand-300: color-mix(in srgb, rgb(var(--nodedc-accent-rgb)) 65%, white);
|
||||||
|
--brand-700: color-mix(in srgb, rgb(var(--nodedc-accent-rgb)) 75%, black);
|
||||||
|
--bg-accent-primary: rgb(var(--nodedc-accent-rgb));
|
||||||
|
--bg-accent-primary-hover: color-mix(in srgb, rgb(var(--nodedc-accent-rgb)) 82%, white);
|
||||||
|
--bg-accent-primary-active: color-mix(in srgb, rgb(var(--nodedc-accent-rgb)) 90%, black);
|
||||||
|
--txt-on-color: rgb(var(--nodedc-on-accent-rgb));
|
||||||
|
--txt-icon-on-color: rgb(var(--nodedc-on-accent-rgb));
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
background: #050506;
|
||||||
|
}
|
||||||
|
|
||||||
.shadow-custom {
|
.shadow-custom {
|
||||||
box-shadow: 2px 2px 8px 2px rgba(234, 231, 250, 0.3); /* Convert #EAE7FA4D to rgba */
|
box-shadow: 2px 2px 8px 2px rgba(234, 231, 250, 0.3); /* Convert #EAE7FA4D to rgba */
|
||||||
}
|
}
|
||||||
|
|
@ -40,3 +62,463 @@
|
||||||
0 0 4px --alpha(var(--background-color-accent-primary) / 40%) !important;
|
0 0 4px --alpha(var(--background-color-accent-primary) / 40%) !important;
|
||||||
will-change: transform, opacity;
|
will-change: transform, opacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.nodedc-admin-root {
|
||||||
|
min-height: 100vh;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 12% -8%, rgba(var(--nodedc-accent-rgb), 0.18), transparent 32rem),
|
||||||
|
radial-gradient(circle at 88% 0%, rgba(var(--nodedc-card-active-rgb), 0.1), transparent 30rem), #050506;
|
||||||
|
color: var(--text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-shell {
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.018) 0%, rgba(255, 255, 255, 0.006) 100%), rgba(5, 5, 7, 0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-main {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-auth-shell {
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 20% 0%, rgba(var(--nodedc-accent-rgb), 0.2), transparent 30rem),
|
||||||
|
radial-gradient(circle at 78% 10%, rgba(var(--nodedc-card-active-rgb), 0.11), transparent 26rem), #050506 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-auth-card {
|
||||||
|
border-radius: 1.75rem;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.036) 0%, rgba(255, 255, 255, 0.012) 100%), rgba(8, 8, 11, 0.78);
|
||||||
|
padding: 1.5rem;
|
||||||
|
-webkit-backdrop-filter: blur(34px);
|
||||||
|
backdrop-filter: blur(34px);
|
||||||
|
box-shadow:
|
||||||
|
0 24px 64px rgba(0, 0, 0, 0.42),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.035);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-glass-sidebar {
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.024) 0%, rgba(255, 255, 255, 0.008) 100%), rgba(8, 8, 11, 0.9) !important;
|
||||||
|
border-right: 1px solid rgba(255, 255, 255, 0.07) !important;
|
||||||
|
-webkit-backdrop-filter: blur(28px);
|
||||||
|
backdrop-filter: blur(28px);
|
||||||
|
box-shadow:
|
||||||
|
inset -1px 0 0 rgba(255, 255, 255, 0.06),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.015),
|
||||||
|
0 18px 48px rgba(0, 0, 0, 0.26);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-glass-modal,
|
||||||
|
.nodedc-glass-surface {
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.036) 0%, rgba(255, 255, 255, 0.012) 100%), rgba(8, 8, 11, 0.88) !important;
|
||||||
|
border: 0 !important;
|
||||||
|
outline: none !important;
|
||||||
|
-webkit-backdrop-filter: blur(38px);
|
||||||
|
backdrop-filter: blur(38px);
|
||||||
|
box-shadow:
|
||||||
|
0 22px 58px rgba(0, 0, 0, 0.34),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.025);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-glass-popup-surface {
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.012) 100%), rgba(8, 8, 11, 0.93) !important;
|
||||||
|
border: 0 !important;
|
||||||
|
outline: none !important;
|
||||||
|
border-radius: 1.25rem !important;
|
||||||
|
-webkit-backdrop-filter: blur(42px);
|
||||||
|
backdrop-filter: blur(42px);
|
||||||
|
box-shadow: 0 22px 60px rgba(0, 0, 0, 0.36);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-header {
|
||||||
|
min-height: 4.25rem;
|
||||||
|
border: 0 !important;
|
||||||
|
border-radius: 0 0 1.35rem 1.35rem;
|
||||||
|
margin: 0.65rem 0.75rem 0;
|
||||||
|
width: calc(100% - 1.5rem) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-breadcrumbs {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-breadcrumb-pill {
|
||||||
|
min-height: 2.5rem;
|
||||||
|
border: 0 !important;
|
||||||
|
border-radius: 1.25rem !important;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.036) 0%, rgba(255, 255, 255, 0.014) 100%),
|
||||||
|
rgba(255, 255, 255, 0.04) !important;
|
||||||
|
color: rgba(255, 255, 255, 0.72) !important;
|
||||||
|
padding: 0.55rem 0.9rem !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
transition:
|
||||||
|
background-color 160ms ease,
|
||||||
|
color 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-breadcrumb-pill:hover {
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.018) 100%),
|
||||||
|
rgba(255, 255, 255, 0.065) !important;
|
||||||
|
color: var(--text-color-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-breadcrumb-pill[data-current="true"] {
|
||||||
|
color: rgb(var(--nodedc-accent-rgb)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-breadcrumb-pill[data-current="true"] * {
|
||||||
|
color: rgb(var(--nodedc-accent-rgb)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-breadcrumb-separator {
|
||||||
|
color: rgba(255, 255, 255, 0.32);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-page {
|
||||||
|
padding: 1rem 0 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-page-header {
|
||||||
|
border: 0 !important;
|
||||||
|
border-radius: 1.35rem;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.025) 0%, rgba(255, 255, 255, 0.01) 100%), rgba(255, 255, 255, 0.025);
|
||||||
|
padding: 1.1rem 1.25rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-page-body {
|
||||||
|
padding-top: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-settings-card {
|
||||||
|
border: 0 !important;
|
||||||
|
outline: none !important;
|
||||||
|
border-radius: 1.35rem !important;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.012) 100%),
|
||||||
|
rgba(255, 255, 255, 0.032) !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
-webkit-backdrop-filter: blur(18px);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-settings-card:hover {
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.04) 0%, rgba(255, 255, 255, 0.018) 100%), rgba(255, 255, 255, 0.046) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-sidebar-profile {
|
||||||
|
border: 0 !important;
|
||||||
|
border-radius: 1.35rem !important;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.044) 0%, rgba(255, 255, 255, 0.016) 100%),
|
||||||
|
rgba(255, 255, 255, 0.04) !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
-webkit-backdrop-filter: blur(18px);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-sidebar-avatar-button {
|
||||||
|
border: 0 !important;
|
||||||
|
border-radius: 0.65rem !important;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.04) 0%, rgba(255, 255, 255, 0.014) 100%),
|
||||||
|
rgba(0, 0, 0, 0.32) !important;
|
||||||
|
color: rgb(var(--nodedc-accent-rgb)) !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-sidebar-section-label {
|
||||||
|
padding: 0.55rem 0.9rem 0.45rem;
|
||||||
|
color: rgba(255, 255, 255, 0.54);
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-settings-sidebar-item {
|
||||||
|
min-height: 2.75rem;
|
||||||
|
border: 0 !important;
|
||||||
|
outline: none !important;
|
||||||
|
border-radius: 1.1rem !important;
|
||||||
|
background: transparent !important;
|
||||||
|
color: rgba(255, 255, 255, 0.76) !important;
|
||||||
|
padding-inline: 0.95rem !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-settings-sidebar-item:hover {
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.014) 100%), rgba(255, 255, 255, 0.03) !important;
|
||||||
|
color: var(--text-color-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-settings-sidebar-item[data-active="true"] {
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.04) 0%, rgba(255, 255, 255, 0.016) 100%), rgba(255, 255, 255, 0.045) !important;
|
||||||
|
color: rgb(var(--nodedc-accent-rgb)) !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-settings-sidebar-item[data-active="true"] * {
|
||||||
|
color: rgb(var(--nodedc-accent-rgb)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-sidebar-action {
|
||||||
|
min-height: 2.25rem;
|
||||||
|
border: 0 !important;
|
||||||
|
border-radius: 999px !important;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.036) 0%, rgba(255, 255, 255, 0.014) 100%),
|
||||||
|
rgba(255, 255, 255, 0.045) !important;
|
||||||
|
color: rgba(255, 255, 255, 0.7) !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-sidebar-action:hover {
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.052) 0%, rgba(255, 255, 255, 0.018) 100%),
|
||||||
|
rgba(255, 255, 255, 0.07) !important;
|
||||||
|
color: var(--text-color-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-settings-input,
|
||||||
|
.nodedc-admin-root input:not([type="checkbox"]) {
|
||||||
|
min-height: 3rem;
|
||||||
|
border: 0 !important;
|
||||||
|
outline: none !important;
|
||||||
|
border-radius: 1.25rem !important;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.012) 100%),
|
||||||
|
rgba(255, 255, 255, 0.032) !important;
|
||||||
|
color: var(--text-color-primary) !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
-webkit-backdrop-filter: blur(18px);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-settings-input:focus,
|
||||||
|
.nodedc-settings-input:focus-visible,
|
||||||
|
.nodedc-admin-root input:not([type="checkbox"]):focus,
|
||||||
|
.nodedc-admin-root input:not([type="checkbox"]):focus-visible {
|
||||||
|
outline: none !important;
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(var(--nodedc-accent-rgb), 0.28) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-settings-input input {
|
||||||
|
min-height: auto !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
background: transparent !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-settings-select {
|
||||||
|
min-height: 3rem !important;
|
||||||
|
border: 0 !important;
|
||||||
|
outline: none !important;
|
||||||
|
border-radius: 1.25rem !important;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.012) 100%),
|
||||||
|
rgba(255, 255, 255, 0.032) !important;
|
||||||
|
color: var(--text-color-primary) !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-root [data-slot="button"],
|
||||||
|
.nodedc-admin-root button,
|
||||||
|
.nodedc-admin-root a[class*="rounded"] {
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-settings-save-button,
|
||||||
|
.nodedc-admin-root [data-slot="button"].bg-accent-primary,
|
||||||
|
.nodedc-admin-root button.bg-accent-primary:not([role="switch"]),
|
||||||
|
.nodedc-admin-root a.bg-accent-primary {
|
||||||
|
min-height: 2.75rem;
|
||||||
|
border: 0 !important;
|
||||||
|
border-radius: 1.25rem !important;
|
||||||
|
background: rgb(var(--nodedc-card-active-rgb)) !important;
|
||||||
|
color: rgb(var(--nodedc-on-card-active-rgb)) !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
padding-inline: 1.35rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-settings-save-button:hover,
|
||||||
|
.nodedc-admin-root [data-slot="button"].bg-accent-primary:hover,
|
||||||
|
.nodedc-admin-root button.bg-accent-primary:not([role="switch"]):hover,
|
||||||
|
.nodedc-admin-root a.bg-accent-primary:hover {
|
||||||
|
background: color-mix(in srgb, rgb(var(--nodedc-card-active-rgb)) 82%, white) !important;
|
||||||
|
color: rgb(var(--nodedc-on-card-active-rgb)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-settings-save-button *,
|
||||||
|
.nodedc-admin-root [data-slot="button"].bg-accent-primary *,
|
||||||
|
.nodedc-admin-root button.bg-accent-primary:not([role="switch"]) *,
|
||||||
|
.nodedc-admin-root a.bg-accent-primary * {
|
||||||
|
color: rgb(var(--nodedc-on-card-active-rgb)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-root button[role="switch"] {
|
||||||
|
min-height: auto !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
border-radius: 9999px !important;
|
||||||
|
border-color: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
background: rgba(255, 255, 255, 0.13) !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-root button[role="switch"][aria-checked="true"] {
|
||||||
|
border-color: transparent !important;
|
||||||
|
background: rgb(var(--nodedc-card-active-rgb)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-root button[role="switch"]:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.17) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-root button[role="switch"][aria-checked="true"]:hover {
|
||||||
|
background: color-mix(in srgb, rgb(var(--nodedc-card-active-rgb)) 88%, white) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-root button[role="switch"] > span[aria-hidden="true"] {
|
||||||
|
background: rgba(255, 255, 255, 0.72) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-root button[role="switch"][aria-checked="true"] > span[aria-hidden="true"] {
|
||||||
|
background: rgba(9, 10, 9, 0.94) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-root input[type="checkbox"] {
|
||||||
|
min-height: 1rem !important;
|
||||||
|
width: 1rem !important;
|
||||||
|
height: 1rem !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
border-radius: 0.35rem !important;
|
||||||
|
border-color: rgba(255, 255, 255, 0.22) !important;
|
||||||
|
background: rgba(255, 255, 255, 0.06) !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-root input[type="checkbox"]:hover {
|
||||||
|
border-color: rgba(255, 255, 255, 0.36) !important;
|
||||||
|
background: rgba(255, 255, 255, 0.09) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-root input[type="checkbox"]:checked,
|
||||||
|
.nodedc-admin-root input[type="checkbox"]:indeterminate {
|
||||||
|
border-color: transparent !important;
|
||||||
|
background: rgb(var(--nodedc-card-active-rgb)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-root input[type="checkbox"]:focus-visible {
|
||||||
|
outline: none !important;
|
||||||
|
box-shadow: 0 0 0 4px rgba(var(--nodedc-accent-rgb), 0.18) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-settings-secondary-button {
|
||||||
|
min-height: 2.75rem;
|
||||||
|
border: 0 !important;
|
||||||
|
border-radius: 1.25rem !important;
|
||||||
|
background: rgba(255, 255, 255, 0.06) !important;
|
||||||
|
color: var(--text-color-primary) !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
padding-inline: 1.25rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-root [data-slot="button"].border-strong,
|
||||||
|
.nodedc-admin-root a.border-strong,
|
||||||
|
.nodedc-admin-root button.border-strong:not([role="switch"]) {
|
||||||
|
min-height: 2.75rem;
|
||||||
|
border: 0 !important;
|
||||||
|
border-radius: 1.25rem !important;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.022) 100%),
|
||||||
|
rgba(255, 255, 255, 0.055) !important;
|
||||||
|
color: rgba(255, 255, 255, 0.76) !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
padding-inline: 1.25rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-settings-secondary-button:hover,
|
||||||
|
.nodedc-admin-root [data-slot="button"].border-strong:hover,
|
||||||
|
.nodedc-admin-root a.border-strong:hover,
|
||||||
|
.nodedc-admin-root button.border-strong:not([role="switch"]):hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
color: var(--text-color-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-root [data-slot="button"]:disabled,
|
||||||
|
.nodedc-admin-root button:disabled:not([role="switch"]),
|
||||||
|
.nodedc-admin-root [aria-disabled="true"] {
|
||||||
|
border: 0 !important;
|
||||||
|
background: rgba(255, 255, 255, 0.035) !important;
|
||||||
|
color: rgba(255, 255, 255, 0.34) !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-settings-note {
|
||||||
|
border: 0 !important;
|
||||||
|
border-radius: 1.25rem !important;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.045) 0%, rgba(255, 255, 255, 0.018) 100%),
|
||||||
|
rgba(255, 255, 255, 0.045) !important;
|
||||||
|
color: rgba(255, 255, 255, 0.72) !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-settings-note a {
|
||||||
|
color: rgb(var(--nodedc-card-active-rgb)) !important;
|
||||||
|
text-decoration-color: rgba(var(--nodedc-card-active-rgb), 0.48) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-settings-helper-card {
|
||||||
|
border: 0 !important;
|
||||||
|
border-radius: 1.35rem !important;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.048) 0%, rgba(255, 255, 255, 0.018) 100%),
|
||||||
|
rgba(255, 255, 255, 0.045) !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
-webkit-backdrop-filter: blur(18px);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-settings-helper-card-header {
|
||||||
|
border: 0 !important;
|
||||||
|
background: rgba(255, 255, 255, 0.035) !important;
|
||||||
|
color: rgba(255, 255, 255, 0.72) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-root .bg-layer-1,
|
||||||
|
.nodedc-admin-root .bg-layer-2,
|
||||||
|
.nodedc-admin-root .bg-layer-3 {
|
||||||
|
background-color: rgba(255, 255, 255, 0.04) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-root .border-subtle,
|
||||||
|
.nodedc-admin-root .border-subtle-1 {
|
||||||
|
border-color: rgba(255, 255, 255, 0.07) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-root :focus-visible {
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-code-chip {
|
||||||
|
border: 0 !important;
|
||||||
|
border-radius: 0.65rem !important;
|
||||||
|
background: rgba(255, 255, 255, 0.08) !important;
|
||||||
|
color: rgb(var(--nodedc-card-active-rgb)) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,10 @@
|
||||||
* See the LICENSE file for details.
|
* See the LICENSE file for details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { Fragment, useState } from "react";
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
import type { Placement } from "@popperjs/core";
|
import type { Placement } from "@popperjs/core";
|
||||||
import { usePopper } from "react-popper";
|
import { usePopper } from "react-popper";
|
||||||
// headless ui
|
|
||||||
import { Popover, Portal, Transition } from "@headlessui/react";
|
|
||||||
// ui
|
// ui
|
||||||
import { Button } from "@plane/propel/button";
|
import { Button } from "@plane/propel/button";
|
||||||
|
|
||||||
|
|
@ -45,93 +44,132 @@ export function FiltersDropdown(props: Props) {
|
||||||
|
|
||||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | HTMLDivElement | null>(null);
|
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | HTMLDivElement | null>(null);
|
||||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||||
placement: placement ?? "auto",
|
placement: placement ?? "auto",
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
const toggleDropdown = useCallback(() => {
|
||||||
<Popover as="div">
|
if (!disabled) setIsOpen((current) => !current);
|
||||||
{({ open, close }) => (
|
}, [disabled]);
|
||||||
<>
|
|
||||||
<Popover.Button as={React.Fragment}>
|
const closeDropdown = useCallback(() => setIsOpen(false), []);
|
||||||
{menuButton ? (
|
|
||||||
<button
|
const handleTriggerKeyDown = useCallback(
|
||||||
type="button"
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
ref={setReferenceElement}
|
if (disabled) return;
|
||||||
className={menuButtonWrapperClassName}
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
disabled={disabled}
|
event.preventDefault();
|
||||||
>
|
toggleDropdown();
|
||||||
{typeof menuButton === "function" ? menuButton({ open }) : menuButton}
|
}
|
||||||
</button>
|
if (event.key === "Escape") closeDropdown();
|
||||||
) : (
|
},
|
||||||
<div ref={setReferenceElement}>
|
[closeDropdown, disabled, toggleDropdown]
|
||||||
<div className="hidden @4xl:flex">
|
);
|
||||||
<Button
|
|
||||||
disabled={disabled}
|
useEffect(() => {
|
||||||
variant="secondary"
|
if (!isOpen) return;
|
||||||
prependIcon={icon}
|
|
||||||
tabIndex={tabIndex}
|
const handleDocumentMouseDown = (event: MouseEvent) => {
|
||||||
className="nodedc-toolbar-pill nodedc-toolbar-pill-wide relative"
|
const target = event.target;
|
||||||
data-active={open}
|
if (!(target instanceof Node)) return;
|
||||||
size="lg"
|
|
||||||
>
|
if (referenceElement?.contains(target) || popperElement?.contains(target)) return;
|
||||||
<>
|
closeDropdown();
|
||||||
<div className={`${open ? "text-[rgb(var(--nodedc-accent-rgb))]" : "text-secondary"}`}>
|
};
|
||||||
<span>{title}</span>
|
|
||||||
</div>
|
const handleDocumentKeyDown = (event: KeyboardEvent) => {
|
||||||
{isFiltersApplied && (
|
if (event.key === "Escape") closeDropdown();
|
||||||
<span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-accent-primary" />
|
};
|
||||||
)}
|
|
||||||
</>
|
document.addEventListener("mousedown", handleDocumentMouseDown);
|
||||||
</Button>
|
document.addEventListener("keydown", handleDocumentKeyDown);
|
||||||
</div>
|
|
||||||
<div className="flex @4xl:hidden">
|
return () => {
|
||||||
<Button
|
document.removeEventListener("mousedown", handleDocumentMouseDown);
|
||||||
disabled={disabled}
|
document.removeEventListener("keydown", handleDocumentKeyDown);
|
||||||
ref={setReferenceElement}
|
};
|
||||||
variant="secondary"
|
}, [closeDropdown, isOpen, popperElement, referenceElement]);
|
||||||
tabIndex={tabIndex}
|
|
||||||
className="nodedc-toolbar-pill nodedc-toolbar-pill-wide"
|
const dropdownPanel =
|
||||||
size="lg"
|
isOpen &&
|
||||||
>
|
createPortal(
|
||||||
{miniIcon || title}
|
<div className="fixed z-[760] translate-y-0">
|
||||||
</Button>
|
<div
|
||||||
</div>
|
className={`nodedc-dropdown-surface my-1 overflow-hidden ${dropdownClassName ?? ""}`}
|
||||||
</div>
|
ref={setPopperElement}
|
||||||
)}
|
style={styles.popper}
|
||||||
</Popover.Button>
|
{...attributes.popper}
|
||||||
<Transition
|
>
|
||||||
as={Fragment}
|
<div
|
||||||
enter="transition ease-out duration-200"
|
className={`flex max-h-[30rem] w-[18.75rem] flex-col overflow-hidden lg:max-h-[37.5rem] ${
|
||||||
enterFrom="opacity-0 translate-y-1"
|
dropdownContentClassName ?? ""
|
||||||
enterTo="opacity-100 translate-y-0"
|
}`}
|
||||||
leave="transition ease-in duration-150"
|
|
||||||
leaveFrom="opacity-100 translate-y-0"
|
|
||||||
leaveTo="opacity-0 translate-y-1"
|
|
||||||
>
|
>
|
||||||
<Portal>
|
{typeof children === "function" ? children({ closeDropdown }) : children}
|
||||||
{/** translate-y-0 is a hack to create new stacking context. Required for safari */}
|
</div>
|
||||||
<Popover.Panel className="fixed z-[760] translate-y-0">
|
</div>
|
||||||
<div
|
</div>,
|
||||||
className={`nodedc-dropdown-surface my-1 overflow-hidden ${dropdownClassName ?? ""}`}
|
document.body
|
||||||
ref={setPopperElement}
|
);
|
||||||
style={styles.popper}
|
|
||||||
{...attributes.popper}
|
return (
|
||||||
>
|
<>
|
||||||
<div
|
{menuButton ? (
|
||||||
className={`flex max-h-[30rem] w-[18.75rem] flex-col overflow-hidden lg:max-h-[37.5rem] ${
|
<button
|
||||||
dropdownContentClassName ?? ""
|
type="button"
|
||||||
}`}
|
ref={setReferenceElement}
|
||||||
>
|
className={menuButtonWrapperClassName}
|
||||||
{typeof children === "function" ? children({ closeDropdown: close }) : children}
|
disabled={disabled}
|
||||||
</div>
|
tabIndex={tabIndex}
|
||||||
|
onClick={toggleDropdown}
|
||||||
|
>
|
||||||
|
{typeof menuButton === "function" ? menuButton({ open: isOpen }) : menuButton}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
ref={setReferenceElement}
|
||||||
|
role="button"
|
||||||
|
tabIndex={disabled ? -1 : tabIndex}
|
||||||
|
aria-disabled={disabled}
|
||||||
|
onClick={toggleDropdown}
|
||||||
|
onKeyDown={handleTriggerKeyDown}
|
||||||
|
>
|
||||||
|
<div className="hidden @4xl:flex">
|
||||||
|
<Button
|
||||||
|
disabled={disabled}
|
||||||
|
variant="secondary"
|
||||||
|
prependIcon={icon}
|
||||||
|
tabIndex={-1}
|
||||||
|
className="nodedc-toolbar-pill nodedc-toolbar-pill-wide relative"
|
||||||
|
data-active={isOpen}
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<div className={`${isOpen ? "text-[rgb(var(--nodedc-accent-rgb))]" : "text-secondary"}`}>
|
||||||
|
<span>{title}</span>
|
||||||
</div>
|
</div>
|
||||||
</Popover.Panel>
|
{isFiltersApplied && (
|
||||||
</Portal>
|
<span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-accent-primary" />
|
||||||
</Transition>
|
)}
|
||||||
</>
|
</>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex @4xl:hidden">
|
||||||
|
<Button
|
||||||
|
disabled={disabled}
|
||||||
|
variant="secondary"
|
||||||
|
tabIndex={-1}
|
||||||
|
className="nodedc-toolbar-pill nodedc-toolbar-pill-wide"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
{miniIcon || title}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</Popover>
|
{dropdownPanel}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue