[WEB-2870]feat: language support (#6215)
* fix: adding language support package * fix: language support implementation using mobx * fix: adding more languages for support * fix: profile settings translations * feat: added language support for sidebar and user settings * feat: added language support for deactivation modal * fix: added project sync after transfer issues (#6200) * code refactor and improvement (#6203) * chore: package code refactoring * chore: component restructuring and refactor * chore: comment create improvement * refactor: enhance workspace and project wrapper modularity (#6207) * [WEB-2678]feat: added functionality to add labels directly from dropdown (#6211) * enhancement:added functionality to add features directly from dropdown * fix: fixed import order * fix: fixed lint errors * chore: added common component for project activity (#6212) * chore: added common component for project activity * fix: added enum * fix: added enum for initiatives * - Do not clear temp files that are locked. (#6214) - Handle edge cases in sync workspace * fix: labels empty state for drop down (#6216) * refactor: remove cn helper function from the editor package (#6217) * * feat: added language support to issue create modal in sidebar * fix: project activity type * * fix: added missing translations * fix: modified translation for plurals * fix: fixed spanish translation * dev: language type error in space user profile types * fix: type fixes * chore: added alpha tag --------- Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com> Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com> Co-authored-by: Akshita Goyal <36129505+gakshita@users.noreply.github.com> Co-authored-by: Satish Gandham <satish.iitg@gmail.com> Co-authored-by: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Co-authored-by: gurusinath <gurusainath007@gmail.com>
This commit is contained in:
parent
ade0aa1643
commit
873e4330bc
84 changed files with 2588 additions and 873 deletions
|
|
@ -3,6 +3,7 @@
|
|||
import React, { useState } from "react";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// hooks
|
||||
|
|
@ -18,6 +19,7 @@ export const DeactivateAccountModal: React.FC<Props> = (props) => {
|
|||
const router = useAppRouter();
|
||||
const { isOpen, onClose } = props;
|
||||
// hooks
|
||||
const { t } = useTranslation();
|
||||
const { deactivateAccount, signOut } = useUser();
|
||||
|
||||
// states
|
||||
|
|
@ -90,11 +92,10 @@ export const DeactivateAccountModal: React.FC<Props> = (props) => {
|
|||
</div>
|
||||
<div>
|
||||
<Dialog.Title as="h3" className="my-4 text-2xl font-medium leading-6 text-custom-text-100">
|
||||
Deactivate your account?
|
||||
{t("deactivate_your_account")}
|
||||
</Dialog.Title>
|
||||
<p className="mt-6 list-disc pr-4 text-base font-normal text-custom-text-200">
|
||||
Once deactivated, you can{"'"}t be assigned issues and be billed for your workspace.To
|
||||
reactivate your account, you will need an invite to a workspace at this email address.
|
||||
{t("deactivate_your_account_description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -102,10 +103,10 @@ export const DeactivateAccountModal: React.FC<Props> = (props) => {
|
|||
</div>
|
||||
<div className="mb-2 flex items-center justify-end gap-2 p-4 sm:px-6">
|
||||
<Button variant="neutral-primary" onClick={onClose}>
|
||||
Cancel
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button variant="danger" onClick={handleDeleteAccount}>
|
||||
{isDeactivating ? "Deactivating..." : "Confirm"}
|
||||
{isDeactivating ? t("deactivating") : t("confirm")}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { FC, useMemo } from "react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// import { CircleCheck } from "lucide-react";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
|
|
@ -17,6 +18,7 @@ type TPasswordStrengthMeter = {
|
|||
|
||||
export const PasswordStrengthMeter: FC<TPasswordStrengthMeter> = (props) => {
|
||||
const { password, isFocused = false } = props;
|
||||
const { t } = useTranslation();
|
||||
// derived values
|
||||
const strength = useMemo(() => getPasswordStrength(password), [password]);
|
||||
const strengthBars = useMemo(() => {
|
||||
|
|
@ -24,40 +26,40 @@ export const PasswordStrengthMeter: FC<TPasswordStrengthMeter> = (props) => {
|
|||
case E_PASSWORD_STRENGTH.EMPTY: {
|
||||
return {
|
||||
bars: [`bg-custom-text-100`, `bg-custom-text-100`, `bg-custom-text-100`],
|
||||
text: "Please enter your password.",
|
||||
text: t("please_enter_your_password"),
|
||||
textColor: "text-custom-text-100",
|
||||
};
|
||||
}
|
||||
case E_PASSWORD_STRENGTH.LENGTH_NOT_VALID: {
|
||||
return {
|
||||
bars: [`bg-red-500`, `bg-custom-text-100`, `bg-custom-text-100`],
|
||||
text: "Password length should me more than 8 characters.",
|
||||
text: t("password_length_should_me_more_than_8_characters"),
|
||||
textColor: "text-red-500",
|
||||
};
|
||||
}
|
||||
case E_PASSWORD_STRENGTH.STRENGTH_NOT_VALID: {
|
||||
return {
|
||||
bars: [`bg-red-500`, `bg-custom-text-100`, `bg-custom-text-100`],
|
||||
text: "Password is weak.",
|
||||
text: t("password_is_weak"),
|
||||
textColor: "text-red-500",
|
||||
};
|
||||
}
|
||||
case E_PASSWORD_STRENGTH.STRENGTH_VALID: {
|
||||
return {
|
||||
bars: [`bg-green-500`, `bg-green-500`, `bg-green-500`],
|
||||
text: "Password is strong.",
|
||||
text: t("password_is_strong"),
|
||||
textColor: "text-green-500",
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return {
|
||||
bars: [`bg-custom-text-100`, `bg-custom-text-100`, `bg-custom-text-100`],
|
||||
text: "Please enter your password.",
|
||||
text: t("please_enter_your_password"),
|
||||
textColor: "text-custom-text-100",
|
||||
};
|
||||
}
|
||||
}
|
||||
}, [strength]);
|
||||
}, [strength,t]);
|
||||
|
||||
const isPasswordMeterVisible = isFocused ? true : strength === E_PASSWORD_STRENGTH.STRENGTH_VALID ? false : true;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,29 +1,16 @@
|
|||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// types
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IUserTheme } from "@plane/types";
|
||||
// ui
|
||||
import { Button, InputColorPicker, setPromiseToast } from "@plane/ui";
|
||||
// hooks
|
||||
import { useUserProfile } from "@/hooks/store";
|
||||
|
||||
const inputRules = {
|
||||
minLength: {
|
||||
value: 7,
|
||||
message: "Enter a valid hex code of 6 characters",
|
||||
},
|
||||
maxLength: {
|
||||
value: 7,
|
||||
message: "Enter a valid hex code of 6 characters",
|
||||
},
|
||||
pattern: {
|
||||
value: /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/,
|
||||
message: "Enter a valid hex code of 6 characters",
|
||||
},
|
||||
};
|
||||
|
||||
type TCustomThemeSelector = {
|
||||
applyThemeChange: (theme: Partial<IUserTheme>) => void;
|
||||
};
|
||||
|
|
@ -32,7 +19,7 @@ export const CustomThemeSelector: React.FC<TCustomThemeSelector> = observer((pro
|
|||
const { applyThemeChange } = props;
|
||||
// hooks
|
||||
const { data: userProfile, updateUserTheme } = useUserProfile();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
control,
|
||||
formState: { errors, isSubmitting },
|
||||
|
|
@ -51,6 +38,24 @@ export const CustomThemeSelector: React.FC<TCustomThemeSelector> = observer((pro
|
|||
},
|
||||
});
|
||||
|
||||
const inputRules = useMemo(
|
||||
() => ({
|
||||
minLength: {
|
||||
value: 7,
|
||||
message: t("enter_a_valid_hex_code_of_6_characters"),
|
||||
},
|
||||
maxLength: {
|
||||
value: 7,
|
||||
message: t("enter_a_valid_hex_code_of_6_characters"),
|
||||
},
|
||||
pattern: {
|
||||
value: /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/,
|
||||
message: t("enter_a_valid_hex_code_of_6_characters"),
|
||||
},
|
||||
}),
|
||||
[t] // Empty dependency array since these rules never change
|
||||
);
|
||||
|
||||
const handleUpdateTheme = async (formData: Partial<IUserTheme>) => {
|
||||
const payload: IUserTheme = {
|
||||
background: formData.background,
|
||||
|
|
@ -66,14 +71,14 @@ export const CustomThemeSelector: React.FC<TCustomThemeSelector> = observer((pro
|
|||
|
||||
const updateCurrentUserThemePromise = updateUserTheme(payload);
|
||||
setPromiseToast(updateCurrentUserThemePromise, {
|
||||
loading: "Updating theme...",
|
||||
loading: t("updating_theme"),
|
||||
success: {
|
||||
title: "Success!",
|
||||
message: () => "Theme updated successfully!",
|
||||
title: t("success"),
|
||||
message: () => t("theme_updated_successfully"),
|
||||
},
|
||||
error: {
|
||||
title: "Error!",
|
||||
message: () => "Failed to Update the theme",
|
||||
title: t("error"),
|
||||
message: () => t("failed_to_update_the_theme"),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -91,16 +96,16 @@ export const CustomThemeSelector: React.FC<TCustomThemeSelector> = observer((pro
|
|||
return (
|
||||
<form onSubmit={handleSubmit(handleUpdateTheme)}>
|
||||
<div className="space-y-5">
|
||||
<h3 className="text-lg font-semibold text-custom-text-100">Customize your theme</h3>
|
||||
<h3 className="text-lg font-semibold text-custom-text-100">{t("customize_your_theme")}</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 gap-x-8 gap-y-4 sm:grid-cols-2 md:grid-cols-3">
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<h3 className="text-left text-sm font-medium text-custom-text-200">Background color</h3>
|
||||
<h3 className="text-left text-sm font-medium text-custom-text-200">{t("background_color")}</h3>
|
||||
<div className="w-full">
|
||||
<Controller
|
||||
control={control}
|
||||
name="background"
|
||||
rules={{ ...inputRules, required: "Background color is required" }}
|
||||
rules={{ ...inputRules, required: t("background_color_is_required") }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<InputColorPicker
|
||||
name="background"
|
||||
|
|
@ -121,12 +126,12 @@ export const CustomThemeSelector: React.FC<TCustomThemeSelector> = observer((pro
|
|||
</div>
|
||||
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<h3 className="text-left text-sm font-medium text-custom-text-200">Text color</h3>
|
||||
<h3 className="text-left text-sm font-medium text-custom-text-200">{t("text_color")}</h3>
|
||||
<div className="w-full">
|
||||
<Controller
|
||||
control={control}
|
||||
name="text"
|
||||
rules={{ ...inputRules, required: "Text color is required" }}
|
||||
rules={{ ...inputRules, required: t("text_color_is_required") }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<InputColorPicker
|
||||
name="text"
|
||||
|
|
@ -147,12 +152,12 @@ export const CustomThemeSelector: React.FC<TCustomThemeSelector> = observer((pro
|
|||
</div>
|
||||
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<h3 className="text-left text-sm font-medium text-custom-text-200">Primary(Theme) color</h3>
|
||||
<h3 className="text-left text-sm font-medium text-custom-text-200">{t("primary_color")}</h3>
|
||||
<div className="w-full">
|
||||
<Controller
|
||||
control={control}
|
||||
name="primary"
|
||||
rules={{ ...inputRules, required: "Primary color is required" }}
|
||||
rules={{ ...inputRules, required: t("primary_color_is_required") }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<InputColorPicker
|
||||
name="primary"
|
||||
|
|
@ -173,12 +178,12 @@ export const CustomThemeSelector: React.FC<TCustomThemeSelector> = observer((pro
|
|||
</div>
|
||||
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<h3 className="text-left text-sm font-medium text-custom-text-200">Sidebar background color</h3>
|
||||
<h3 className="text-left text-sm font-medium text-custom-text-200">{t("sidebar_background_color")}</h3>
|
||||
<div className="w-full">
|
||||
<Controller
|
||||
control={control}
|
||||
name="sidebarBackground"
|
||||
rules={{ ...inputRules, required: "Sidebar background color is required" }}
|
||||
rules={{ ...inputRules, required: t("sidebar_background_color_is_required") }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<InputColorPicker
|
||||
name="sidebarBackground"
|
||||
|
|
@ -201,12 +206,12 @@ export const CustomThemeSelector: React.FC<TCustomThemeSelector> = observer((pro
|
|||
</div>
|
||||
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<h3 className="text-left text-sm font-medium text-custom-text-200">Sidebar text color</h3>
|
||||
<h3 className="text-left text-sm font-medium text-custom-text-200">{t("sidebar_text_color")}</h3>
|
||||
<div className="w-full">
|
||||
<Controller
|
||||
control={control}
|
||||
name="sidebarText"
|
||||
rules={{ ...inputRules, required: "Sidebar text color is required" }}
|
||||
rules={{ ...inputRules, required: t("sidebar_text_color_is_required") }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<InputColorPicker
|
||||
name="sidebarText"
|
||||
|
|
@ -230,7 +235,7 @@ export const CustomThemeSelector: React.FC<TCustomThemeSelector> = observer((pro
|
|||
</div>
|
||||
<div className="mt-5 flex justify-end gap-2">
|
||||
<Button variant="primary" type="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? "Creating Theme..." : "Set Theme"}
|
||||
{isSubmitting ? t("creating_theme") : t("set_theme")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// constants
|
||||
import { CustomSelect } from "@plane/ui";
|
||||
import { THEME_OPTIONS, I_THEME_OPTION } from "@/constants/themes";
|
||||
|
|
@ -13,7 +14,7 @@ type Props = {
|
|||
|
||||
export const ThemeSwitch: FC<Props> = (props) => {
|
||||
const { value, onChange } = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
|
|
@ -40,10 +41,10 @@ export const ThemeSwitch: FC<Props> = (props) => {
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
{value.label}
|
||||
{t(value.key)}
|
||||
</div>
|
||||
) : (
|
||||
"Select your theme"
|
||||
t("select_your_theme")
|
||||
)
|
||||
}
|
||||
onChange={onChange}
|
||||
|
|
@ -72,7 +73,7 @@ export const ThemeSwitch: FC<Props> = (props) => {
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
{themeOption.label}
|
||||
{t(themeOption.key)}
|
||||
</div>
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { ChevronDown, LucideIcon } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { ComboDropDown } from "@plane/ui";
|
||||
// helpers
|
||||
|
|
@ -26,6 +27,7 @@ type Props = {
|
|||
} & MemberDropdownProps;
|
||||
|
||||
export const MemberDropdown: React.FC<Props> = observer((props) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
button,
|
||||
buttonClassName,
|
||||
|
|
@ -40,7 +42,7 @@ export const MemberDropdown: React.FC<Props> = observer((props) => {
|
|||
multiple,
|
||||
onChange,
|
||||
onClose,
|
||||
placeholder = "Members",
|
||||
placeholder = t("members"),
|
||||
tooltipContent,
|
||||
placement,
|
||||
projectId,
|
||||
|
|
@ -86,7 +88,7 @@ export const MemberDropdown: React.FC<Props> = observer((props) => {
|
|||
if (value.length === 1) {
|
||||
return getUserDetails(value[0])?.display_name || placeholder;
|
||||
} else {
|
||||
return showUserDetails ? `${value.length} members` : "";
|
||||
return showUserDetails ? `${value.length} ${t("members").toLocaleLowerCase()}` : "";
|
||||
}
|
||||
} else {
|
||||
return placeholder;
|
||||
|
|
@ -131,7 +133,9 @@ export const MemberDropdown: React.FC<Props> = observer((props) => {
|
|||
className={cn("text-xs", buttonClassName)}
|
||||
isActive={isOpen}
|
||||
tooltipHeading={placeholder}
|
||||
tooltipContent={tooltipContent ?? `${value?.length ?? 0} assignee${value?.length !== 1 ? "s" : ""}`}
|
||||
tooltipContent={
|
||||
tooltipContent ?? `${value?.length ?? 0} ${value?.length !== 1 ? t("assignees") : t("assignee")}`
|
||||
}
|
||||
showTooltip={showTooltip}
|
||||
variant={buttonVariant}
|
||||
renderToolTipByDefault={renderByDefault}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { createPortal } from "react-dom";
|
|||
import { usePopper } from "react-popper";
|
||||
import { Check, Search } from "lucide-react";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// plane ui
|
||||
import { Avatar } from "@plane/ui";
|
||||
// helpers
|
||||
|
|
@ -34,6 +35,7 @@ export const MemberOptions: React.FC<Props> = observer((props: Props) => {
|
|||
// refs
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
// store hooks
|
||||
const { t } = useTranslation();
|
||||
const { workspaceSlug } = useParams();
|
||||
const {
|
||||
getUserDetails,
|
||||
|
|
@ -85,7 +87,7 @@ export const MemberOptions: React.FC<Props> = observer((props: Props) => {
|
|||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar name={userDetails?.display_name} src={getFileURL(userDetails?.avatar_url ?? "")} />
|
||||
<span className="flex-grow truncate">{currentUser?.id === userId ? "You" : userDetails?.display_name}</span>
|
||||
<span className="flex-grow truncate">{currentUser?.id === userId ? t("you") : userDetails?.display_name}</span>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
|
@ -115,7 +117,7 @@ export const MemberOptions: React.FC<Props> = observer((props: Props) => {
|
|||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search"
|
||||
placeholder={t("search")}
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
onKeyDown={searchInputKeyDown}
|
||||
/>
|
||||
|
|
@ -142,10 +144,10 @@ export const MemberOptions: React.FC<Props> = observer((props: Props) => {
|
|||
</Combobox.Option>
|
||||
))
|
||||
) : (
|
||||
<p className="px-1.5 py-1 italic text-custom-text-400">No matching results</p>
|
||||
<p className="px-1.5 py-1 italic text-custom-text-400">{t("no_matching_results")}</p>
|
||||
)
|
||||
) : (
|
||||
<p className="px-1.5 py-1 italic text-custom-text-400">Loading...</p>
|
||||
<p className="px-1.5 py-1 italic text-custom-text-400">{t("loading")}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { useTheme } from "next-themes";
|
|||
import { usePopper } from "react-popper";
|
||||
import { Check, ChevronDown, Search, SignalHigh } from "lucide-react";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import { TIssuePriorities } from "@plane/types";
|
||||
// ui
|
||||
|
|
@ -71,11 +72,12 @@ const BorderButton = (props: ButtonProps) => {
|
|||
};
|
||||
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
tooltipHeading="Priority"
|
||||
tooltipContent={priorityDetails?.title ?? "None"}
|
||||
tooltipHeading={t("priority")}
|
||||
tooltipContent={t(priorityDetails?.key ?? "none")}
|
||||
disabled={!showTooltip}
|
||||
isMobile={isMobile}
|
||||
renderByDefault={renderToolTipByDefault}
|
||||
|
|
@ -119,7 +121,7 @@ const BorderButton = (props: ButtonProps) => {
|
|||
) : (
|
||||
<SignalHigh className="size-3" />
|
||||
))}
|
||||
{!hideText && <span className="flex-grow truncate">{priorityDetails?.title ?? placeholder}</span>}
|
||||
{!hideText && <span className="flex-grow truncate">{t(priorityDetails?.key ?? "priority") ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
|
|
@ -153,11 +155,12 @@ const BackgroundButton = (props: ButtonProps) => {
|
|||
};
|
||||
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
tooltipHeading="Priority"
|
||||
tooltipContent={priorityDetails?.title ?? "None"}
|
||||
tooltipHeading={t("priority")}
|
||||
tooltipContent={t(priorityDetails?.key ?? "none")}
|
||||
disabled={!showTooltip}
|
||||
isMobile={isMobile}
|
||||
renderByDefault={renderToolTipByDefault}
|
||||
|
|
@ -201,7 +204,7 @@ const BackgroundButton = (props: ButtonProps) => {
|
|||
) : (
|
||||
<SignalHigh className="size-3" />
|
||||
))}
|
||||
{!hideText && <span className="flex-grow truncate">{priorityDetails?.title ?? placeholder}</span>}
|
||||
{!hideText && <span className="flex-grow truncate">{t(priorityDetails?.key ?? "priority") ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
|
|
@ -236,11 +239,12 @@ const TransparentButton = (props: ButtonProps) => {
|
|||
};
|
||||
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
tooltipHeading="Priority"
|
||||
tooltipContent={priorityDetails?.title ?? "None"}
|
||||
tooltipHeading={t("priority")}
|
||||
tooltipContent={t(priorityDetails?.key ?? "none")}
|
||||
disabled={!showTooltip}
|
||||
isMobile={isMobile}
|
||||
renderByDefault={renderToolTipByDefault}
|
||||
|
|
@ -285,7 +289,7 @@ const TransparentButton = (props: ButtonProps) => {
|
|||
) : (
|
||||
<SignalHigh className="size-3" />
|
||||
))}
|
||||
{!hideText && <span className="flex-grow truncate">{priorityDetails?.title ?? placeholder}</span>}
|
||||
{!hideText && <span className="flex-grow truncate">{t(priorityDetails?.key ?? "priority") ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
|
|
@ -336,6 +340,8 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
|||
},
|
||||
],
|
||||
});
|
||||
//hooks
|
||||
const { t } = useTranslation();
|
||||
// next-themes
|
||||
// TODO: remove this after new theming implementation
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
|
@ -346,7 +352,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
|||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<PriorityIcon priority={priority.key} size={14} withContainer />
|
||||
<span className="flex-grow truncate">{priority.title}</span>
|
||||
<span className="flex-grow truncate">{t(priority.key)}</span>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
|
@ -456,7 +462,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
|||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search"
|
||||
placeholder={t("search")}
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
onKeyDown={searchInputKeyDown}
|
||||
/>
|
||||
|
|
@ -482,7 +488,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
|||
</Combobox.Option>
|
||||
))
|
||||
) : (
|
||||
<p className="text-custom-text-400 italic py-1 px-1.5">No matching results</p>
|
||||
<p className="text-custom-text-400 italic py-1 px-1.5">{t("no_matching_results")}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { usePopper } from "react-popper";
|
|||
import { Briefcase, Check, ChevronDown, Search } from "lucide-react";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
// ui
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ComboDropDown } from "@plane/ui";
|
||||
// components
|
||||
import { Logo } from "@/components/common";
|
||||
|
|
@ -86,7 +87,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
|||
});
|
||||
// store hooks
|
||||
const { joinedProjectIds, getProjectById } = useProject();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const options = joinedProjectIds?.map((projectId) => {
|
||||
const projectDetails = getProjectById(projectId);
|
||||
if (renderCondition && projectDetails && !renderCondition(projectDetails)) return;
|
||||
|
|
@ -238,7 +239,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
|||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search"
|
||||
placeholder={t("search")}
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
onKeyDown={searchInputKeyDown}
|
||||
/>
|
||||
|
|
@ -268,10 +269,10 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
|||
);
|
||||
})
|
||||
) : (
|
||||
<p className="text-custom-text-400 italic py-1 px-1.5">No matching results</p>
|
||||
<p className="text-custom-text-400 italic py-1 px-1.5">{t("no_matching_results")}</p>
|
||||
)
|
||||
) : (
|
||||
<p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
|
||||
<p className="text-custom-text-400 italic py-1 px-1.5">{t("loading")}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { useParams } from "next/navigation";
|
|||
import { usePopper } from "react-popper";
|
||||
import { ChevronDown, Search } from "lucide-react";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { ComboDropDown, Spinner, StateGroupIcon } from "@plane/ui";
|
||||
// helpers
|
||||
|
|
@ -82,6 +83,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
|||
],
|
||||
});
|
||||
// store hooks
|
||||
const { t } = useTranslation();
|
||||
const { workspaceSlug } = useParams();
|
||||
const { fetchProjectStates, getProjectStates, getStateById } = useProjectState();
|
||||
const statesList = stateIds
|
||||
|
|
@ -160,8 +162,8 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
|||
<DropdownButton
|
||||
className={buttonClassName}
|
||||
isActive={isOpen}
|
||||
tooltipHeading="State"
|
||||
tooltipContent={selectedState?.name ?? "State"}
|
||||
tooltipHeading={t("state")}
|
||||
tooltipContent={selectedState?.name ?? t("state")}
|
||||
showTooltip={showTooltip}
|
||||
variant={buttonVariant}
|
||||
renderToolTipByDefault={renderByDefault}
|
||||
|
|
@ -178,7 +180,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
|||
/>
|
||||
)}
|
||||
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
|
||||
<span className="flex-grow truncate">{selectedState?.name ?? "State"}</span>
|
||||
<span className="flex-grow truncate">{selectedState?.name ?? t("state")}</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
|
|
@ -239,10 +241,10 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
|||
/>
|
||||
))
|
||||
) : (
|
||||
<p className="px-1.5 py-1 italic text-custom-text-400">No matches found</p>
|
||||
<p className="px-1.5 py-1 italic text-custom-text-400">{t("no_matching_results")}</p>
|
||||
)
|
||||
) : (
|
||||
<p className="px-1.5 py-1 italic text-custom-text-400">Loading...</p>
|
||||
<p className="px-1.5 py-1 italic text-custom-text-400">{t("loading")}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import Image from "next/image";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { getButtonStyling } from "@plane/ui";
|
||||
// helpers
|
||||
|
|
@ -6,38 +7,40 @@ import { cn } from "@/helpers/common.helper";
|
|||
// assets
|
||||
import PlaneLogo from "@/public/plane-logos/blue-without-text.png";
|
||||
|
||||
export const ProductUpdatesFooter = () => (
|
||||
<div className="flex items-center justify-between flex-shrink-0 gap-4 m-6 mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<a
|
||||
export const ProductUpdatesFooter = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex items-center justify-between flex-shrink-0 gap-4 m-6 mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<a
|
||||
href="https://go.plane.so/p-docs"
|
||||
target="_blank"
|
||||
className="text-sm text-custom-text-200 hover:text-custom-text-100 hover:underline underline-offset-1 outline-none"
|
||||
>
|
||||
Docs
|
||||
</a>
|
||||
<svg viewBox="0 0 2 2" className="h-0.5 w-0.5 fill-current">
|
||||
<circle cx={1} cy={1} r={1} />
|
||||
>
|
||||
{t("docs")}
|
||||
</a>
|
||||
<svg viewBox="0 0 2 2" className="h-0.5 w-0.5 fill-current">
|
||||
<circle cx={1} cy={1} r={1} />
|
||||
</svg>
|
||||
<a
|
||||
href="https://go.plane.so/p-changelog"
|
||||
target="_blank"
|
||||
className="text-sm text-custom-text-200 hover:text-custom-text-100 hover:underline underline-offset-1 outline-none"
|
||||
>
|
||||
Full changelog
|
||||
</a>
|
||||
<svg viewBox="0 0 2 2" className="h-0.5 w-0.5 fill-current">
|
||||
<circle cx={1} cy={1} r={1} />
|
||||
>
|
||||
{t("full_changelog")}
|
||||
</a>
|
||||
<svg viewBox="0 0 2 2" className="h-0.5 w-0.5 fill-current">
|
||||
<circle cx={1} cy={1} r={1} />
|
||||
</svg>
|
||||
<a
|
||||
href="mailto:support@plane.so"
|
||||
target="_blank"
|
||||
className="text-sm text-custom-text-200 hover:text-custom-text-100 hover:underline underline-offset-1 outline-none"
|
||||
>
|
||||
Support
|
||||
</a>
|
||||
<svg viewBox="0 0 2 2" className="h-0.5 w-0.5 fill-current">
|
||||
<circle cx={1} cy={1} r={1} />
|
||||
>
|
||||
{t("support")}
|
||||
</a>
|
||||
<svg viewBox="0 0 2 2" className="h-0.5 w-0.5 fill-current">
|
||||
<circle cx={1} cy={1} r={1} />
|
||||
</svg>
|
||||
<a
|
||||
href="https://go.plane.so/p-discord"
|
||||
|
|
@ -55,8 +58,9 @@ export const ProductUpdatesFooter = () => (
|
|||
"flex gap-1.5 items-center text-center font-medium hover:underline underline-offset-2 outline-none"
|
||||
)}
|
||||
>
|
||||
<Image src={PlaneLogo} alt="Plane" width={12} height={12} />
|
||||
Powered by Plane Pages
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
<Image src={PlaneLogo} alt="Plane" width={12} height={12} />
|
||||
{t("powered_by_plane_pages")}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
|
||||
// components
|
||||
|
|
@ -16,7 +17,7 @@ export type ProductUpdatesModalProps = {
|
|||
|
||||
export const ProductUpdatesModal: FC<ProductUpdatesModalProps> = observer((props) => {
|
||||
const { isOpen, handleClose } = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { config } = useInstance();
|
||||
|
||||
return (
|
||||
|
|
@ -27,17 +28,17 @@ export const ProductUpdatesModal: FC<ProductUpdatesModalProps> = observer((props
|
|||
<iframe src={config?.instance_changelog_url} className="w-full h-full" />
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center w-full h-full mb-8">
|
||||
<div className="text-lg font-medium">We are having trouble fetching the updates.</div>
|
||||
<div className="text-lg font-medium">{t("we_are_having_trouble_fetching_the_updates")}</div>
|
||||
<div className="text-sm text-custom-text-200">
|
||||
Please visit{" "}
|
||||
{t("please_visit")}
|
||||
<a
|
||||
href="https://go.plane.so/p-changelog"
|
||||
target="_blank"
|
||||
className="text-sm text-custom-primary-100 font-medium hover:text-custom-primary-200 underline underline-offset-1 outline-none"
|
||||
>
|
||||
our changelogs
|
||||
{t("our_changelogs")}
|
||||
</a>{" "}
|
||||
for the latest updates.
|
||||
{t("for_the_latest_updates")}.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import { EIssuesStoreType } from "@plane/constants";
|
||||
import type { TBaseIssue, TIssue } from "@plane/types";
|
||||
|
|
@ -54,6 +55,7 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
|||
const [uploadedAssetIds, setUploadedAssetIds] = useState<string[]>([]);
|
||||
const [isDuplicateModalOpen, setIsDuplicateModalOpen] = useState(false);
|
||||
// store hooks
|
||||
const { t } = useTranslation();
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
const { workspaceSlug, projectId: routerProjectId, cycleId, moduleId } = useParams();
|
||||
const { projectsWithCreatePermissions } = useUser();
|
||||
|
|
@ -218,8 +220,8 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
|||
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: `${is_draft_issue ? "Draft created." : "Issue created successfully."} `,
|
||||
title: t("success"),
|
||||
message: `${is_draft_issue ? t("draft_created") : t("issue_created_successfully")} `,
|
||||
actionItems: !is_draft_issue && response?.project_id && (
|
||||
<CreateIssueToastActionItems
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
|
|
@ -241,8 +243,8 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
|||
} catch (error) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: `${is_draft_issue ? "Draft issue" : "Issue"} could not be created. Please try again.`,
|
||||
title: t("error"),
|
||||
message: t(is_draft_issue ? "draft_creation_failed" : "issue_creation_failed"),
|
||||
});
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_CREATED,
|
||||
|
|
@ -287,8 +289,8 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
|||
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Issue updated successfully.",
|
||||
title: t("success"),
|
||||
message: t("issue_updated_successfully"),
|
||||
});
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_UPDATED,
|
||||
|
|
@ -300,8 +302,8 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
|||
console.error(error);
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Issue could not be updated. Please try again.",
|
||||
title: t("error"),
|
||||
message: t("issue_could_not_be_updated"),
|
||||
});
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_UPDATED,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import React, { useState } from "react";
|
|||
import { observer } from "mobx-react";
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
import { LayoutPanelTop } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import { ISearchIssueResponse, TIssue } from "@plane/types";
|
||||
// ui
|
||||
|
|
@ -65,6 +66,7 @@ export const IssueDefaultProperties: React.FC<TIssueDefaultPropertiesProps> = ob
|
|||
// states
|
||||
const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false);
|
||||
// store hooks
|
||||
const { t } = useTranslation();
|
||||
const { areEstimateEnabledByProjectId } = useProjectEstimates();
|
||||
const { getProjectById } = useProject();
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
|
@ -133,7 +135,7 @@ export const IssueDefaultProperties: React.FC<TIssueDefaultPropertiesProps> = ob
|
|||
}}
|
||||
buttonVariant={value?.length > 0 ? "transparent-without-text" : "border-with-text"}
|
||||
buttonClassName={value?.length > 0 ? "hover:bg-transparent" : ""}
|
||||
placeholder="Assignees"
|
||||
placeholder={t("assignees")}
|
||||
multiple
|
||||
tabIndex={getIndex("assignee_ids")}
|
||||
/>
|
||||
|
|
@ -172,7 +174,7 @@ export const IssueDefaultProperties: React.FC<TIssueDefaultPropertiesProps> = ob
|
|||
}}
|
||||
buttonVariant="border-with-text"
|
||||
maxDate={maxDate ?? undefined}
|
||||
placeholder="Start date"
|
||||
placeholder={t("start_date")}
|
||||
tabIndex={getIndex("start_date")}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -191,7 +193,7 @@ export const IssueDefaultProperties: React.FC<TIssueDefaultPropertiesProps> = ob
|
|||
}}
|
||||
buttonVariant="border-with-text"
|
||||
minDate={minDate ?? undefined}
|
||||
placeholder="Due date"
|
||||
placeholder={t("due_date")}
|
||||
tabIndex={getIndex("target_date")}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -209,7 +211,7 @@ export const IssueDefaultProperties: React.FC<TIssueDefaultPropertiesProps> = ob
|
|||
onChange(cycleId);
|
||||
handleFormChange();
|
||||
}}
|
||||
placeholder="Cycle"
|
||||
placeholder={t("cycle")}
|
||||
value={value}
|
||||
buttonVariant="border-with-text"
|
||||
tabIndex={getIndex("cycle_id")}
|
||||
|
|
@ -231,7 +233,7 @@ export const IssueDefaultProperties: React.FC<TIssueDefaultPropertiesProps> = ob
|
|||
onChange(moduleIds);
|
||||
handleFormChange();
|
||||
}}
|
||||
placeholder="Modules"
|
||||
placeholder={t("modules")}
|
||||
buttonVariant="border-with-text"
|
||||
tabIndex={getIndex("module_ids")}
|
||||
multiple
|
||||
|
|
@ -256,7 +258,7 @@ export const IssueDefaultProperties: React.FC<TIssueDefaultPropertiesProps> = ob
|
|||
projectId={projectId}
|
||||
buttonVariant="border-with-text"
|
||||
tabIndex={getIndex("estimate_point")}
|
||||
placeholder="Estimate"
|
||||
placeholder={t("estimate")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -288,7 +290,7 @@ export const IssueDefaultProperties: React.FC<TIssueDefaultPropertiesProps> = ob
|
|||
>
|
||||
<>
|
||||
<CustomMenu.MenuItem className="!p-1" onClick={() => setParentIssueListModalOpen(true)}>
|
||||
Change parent issue
|
||||
{t("change_parent_issue")}
|
||||
</CustomMenu.MenuItem>
|
||||
<Controller
|
||||
control={control}
|
||||
|
|
@ -301,7 +303,7 @@ export const IssueDefaultProperties: React.FC<TIssueDefaultPropertiesProps> = ob
|
|||
handleFormChange();
|
||||
}}
|
||||
>
|
||||
Remove parent issue
|
||||
{t("remove_parent_issue")}
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
/>
|
||||
|
|
@ -314,7 +316,7 @@ export const IssueDefaultProperties: React.FC<TIssueDefaultPropertiesProps> = ob
|
|||
onClick={() => setParentIssueListModalOpen(true)}
|
||||
>
|
||||
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="whitespace-nowrap">Add parent</span>
|
||||
<span className="whitespace-nowrap">{t("add_parent")}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import React from "react";
|
|||
import { observer } from "mobx-react";
|
||||
import { Control, Controller, FieldErrors } from "react-hook-form";
|
||||
// types
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TIssue } from "@plane/types";
|
||||
// ui
|
||||
import { Input } from "@plane/ui";
|
||||
|
|
@ -25,12 +26,13 @@ export const IssueTitleInput: React.FC<TIssueTitleInputProps> = observer((props)
|
|||
const { control, issueTitleRef, errors, handleFormChange } = props;
|
||||
// store hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { getIndex } = getTabIndex(ETabIndices.ISSUE_FORM, isMobile);
|
||||
|
||||
const validateWhitespace = (value: string) => {
|
||||
if (value.trim() === "") {
|
||||
return "Title is required";
|
||||
return t("title_is_required");
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
|
@ -41,10 +43,10 @@ export const IssueTitleInput: React.FC<TIssueTitleInputProps> = observer((props)
|
|||
name="name"
|
||||
rules={{
|
||||
validate: validateWhitespace,
|
||||
required: "Title is required",
|
||||
required: t("title_is_required"),
|
||||
maxLength: {
|
||||
value: 255,
|
||||
message: "Title should be less than 255 characters",
|
||||
message: t("title_should_be_less_than_255_characters"),
|
||||
},
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
|
|
@ -59,7 +61,7 @@ export const IssueTitleInput: React.FC<TIssueTitleInputProps> = observer((props)
|
|||
}}
|
||||
ref={issueTitleRef || ref}
|
||||
hasError={Boolean(errors.name)}
|
||||
placeholder="Title"
|
||||
placeholder={t("title")}
|
||||
className="w-full text-base"
|
||||
tabIndex={getIndex("name")}
|
||||
autoFocus
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import { useForm } from "react-hook-form";
|
|||
// editor
|
||||
import { EIssuesStoreType } from "@plane/constants";
|
||||
import { EditorRefApi } from "@plane/editor";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import type { TIssue, ISearchIssueResponse, TWorkspaceDraftIssue } from "@plane/types";
|
||||
// hooks
|
||||
|
|
@ -77,6 +79,7 @@ export interface IssueFormProps {
|
|||
}
|
||||
|
||||
export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
data,
|
||||
issueTitleRef,
|
||||
|
|
@ -89,10 +92,10 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||
onCreateMoreToggleChange,
|
||||
isDraft,
|
||||
moveToIssue = false,
|
||||
modalTitle,
|
||||
modalTitle = `${data?.id ? t("update") : isDraft ? t("create_a_draft") : t("create_new_issue")}`,
|
||||
primaryButtonText = {
|
||||
default: `${data?.id ? "Update" : isDraft ? "Save to Drafts" : "Save"}`,
|
||||
loading: `${data?.id ? "Updating" : "Saving"}`,
|
||||
default: `${data?.id ? t("update") : isDraft ? t("save_to_drafts") : t("save")}`,
|
||||
loading: `${data?.id ? t("updating") : t("saving")}`,
|
||||
},
|
||||
isDuplicateModalOpen,
|
||||
handleDuplicateIssueModal,
|
||||
|
|
@ -198,8 +201,8 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||
if (!editorRef.current?.isEditorReadyToDiscard()) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Editor is not ready to discard changes.",
|
||||
title: t("error"),
|
||||
message: t("editor_is_not_ready_to_discard_changes"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
@ -391,7 +394,11 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||
<DeDupeButtonRoot
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
isDuplicateModalOpen={isDuplicateModalOpen}
|
||||
label={`${duplicateIssues.length} duplicate issue${duplicateIssues.length > 1 ? "s" : ""} found!`}
|
||||
label={
|
||||
duplicateIssues.length === 1
|
||||
? `${duplicateIssues.length} ${t("duplicate_issue_found")}`
|
||||
: `${duplicateIssues.length} ${t("duplicate_issues_found")}`
|
||||
}
|
||||
handleOnClick={() => handleDuplicateIssueModal(!isDuplicateModalOpen)}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -491,7 +498,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||
role="button"
|
||||
>
|
||||
<ToggleSwitch value={isCreateMoreToggleEnabled} onChange={() => {}} size="sm" />
|
||||
<span className="text-xs">Create more</span>
|
||||
<span className="text-xs">{t("create_more")}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -511,7 +518,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||
}}
|
||||
tabIndex={getIndex("discard_button")}
|
||||
>
|
||||
Discard
|
||||
{t("discard")}
|
||||
</Button>
|
||||
<Button
|
||||
variant={moveToIssue ? "neutral-primary" : "primary"}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import { useParams } from "next/navigation";
|
|||
import { usePopper } from "react-popper";
|
||||
import { Check, Component, Plus, Search, Tag } from "lucide-react";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
// plane helpers
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// components
|
||||
import { IssueLabelsList } from "@/components/ui";
|
||||
// helpers
|
||||
|
|
@ -39,6 +39,7 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
|||
createLabelEnabled = false,
|
||||
buttonClassName,
|
||||
} = props;
|
||||
const { t } = useTranslation();
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
|
|
@ -131,7 +132,7 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
|||
) : (
|
||||
<div className="h-full flex items-center justify-center gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1 text-xs hover:bg-custom-background-80">
|
||||
<Tag className="h-3 w-3 flex-shrink-0" />
|
||||
<span>Labels</span>
|
||||
<span>{t("labels")}</span>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
|
|
@ -152,7 +153,7 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
|||
ref={inputRef}
|
||||
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="Search"
|
||||
placeholder={t("search")}
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -232,10 +233,10 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
|||
);
|
||||
})
|
||||
) : (
|
||||
<p className="text-custom-text-400 italic py-1 px-1.5">No matching results</p>
|
||||
<p className="text-custom-text-400 italic py-1 px-1.5">{t("no_matching_results")}</p>
|
||||
)
|
||||
) : (
|
||||
<p className="text-custom-text-400 italic py-1 px-1.5">Loading...</p>
|
||||
<p className="text-custom-text-400 italic py-1 px-1.5">{t("loading")}</p>
|
||||
)}
|
||||
{createLabelEnabled && (
|
||||
<button
|
||||
|
|
@ -244,7 +245,7 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
|||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<Plus className="h-3 w-3" aria-hidden="true" />
|
||||
<span className="whitespace-nowrap">Create new label</span>
|
||||
<span className="whitespace-nowrap">{t("create_new_label")}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
463
web/core/components/profile/form.tsx
Normal file
463
web/core/components/profile/form.tsx
Normal file
|
|
@ -0,0 +1,463 @@
|
|||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { ChevronDown, CircleUserRound } from "lucide-react";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
import { useTranslation, SUPPORTED_LANGUAGES } from "@plane/i18n";
|
||||
import type { IUser, TUserProfile } from "@plane/types";
|
||||
import { Button, CustomSelect, Input, TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { DeactivateAccountModal } from "@/components/account";
|
||||
import { ImagePickerPopover, UserImageUploadModal } from "@/components/core";
|
||||
import { TimezoneSelect } from "@/components/global";
|
||||
// constants
|
||||
import { USER_ROLES } from "@/constants/workspace";
|
||||
// helpers
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
// hooks
|
||||
import { useUser, useUserProfile } from "@/hooks/store";
|
||||
|
||||
type TUserProfileForm = {
|
||||
avatar_url: string;
|
||||
cover_image: string;
|
||||
cover_image_asset: any;
|
||||
cover_image_url: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
display_name: string;
|
||||
email: string;
|
||||
role: string;
|
||||
language: string;
|
||||
user_timezone: string;
|
||||
};
|
||||
|
||||
export type TProfileFormProps = {
|
||||
user: IUser;
|
||||
profile: TUserProfile;
|
||||
};
|
||||
|
||||
export const ProfileForm = observer((props: TProfileFormProps) => {
|
||||
const { user, profile } = props;
|
||||
// states
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
|
||||
const [deactivateAccountModal, setDeactivateAccountModal] = useState(false);
|
||||
// language support
|
||||
const { t } = useTranslation();
|
||||
// form info
|
||||
const {
|
||||
handleSubmit,
|
||||
watch,
|
||||
control,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm<TUserProfileForm>({
|
||||
defaultValues: {
|
||||
avatar_url: user.avatar_url || "",
|
||||
cover_image_asset: null,
|
||||
cover_image_url: user.cover_image_url || "",
|
||||
first_name: user.first_name || "",
|
||||
last_name: user.last_name || "",
|
||||
display_name: user.display_name || "",
|
||||
email: user.email || "",
|
||||
role: profile.role || "Product / Project Manager",
|
||||
language: profile.language || "en",
|
||||
user_timezone: "Asia/Kolkata",
|
||||
},
|
||||
});
|
||||
// derived values
|
||||
const userAvatar = watch("avatar_url");
|
||||
const userCover = watch("cover_image_url");
|
||||
// store hooks
|
||||
const { data: currentUser, updateCurrentUser } = useUser();
|
||||
const { updateUserProfile } = useUserProfile();
|
||||
|
||||
const getLanguageLabel = (value: string) => {
|
||||
const selectedLanguage = SUPPORTED_LANGUAGES.find((l) => l.value === value);
|
||||
if (!selectedLanguage) return value;
|
||||
return selectedLanguage.label;
|
||||
};
|
||||
|
||||
const handleProfilePictureDelete = async (url: string | null | undefined) => {
|
||||
if (!url) return;
|
||||
await updateCurrentUser({
|
||||
avatar_url: "",
|
||||
})
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Profile picture deleted successfully.",
|
||||
});
|
||||
setValue("avatar_url", "");
|
||||
})
|
||||
.catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "There was some error in deleting your profile picture. Please try again.",
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setIsImageUploadModalOpen(false);
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = async (formData: TUserProfileForm) => {
|
||||
setIsLoading(true);
|
||||
const userPayload: Partial<IUser> = {
|
||||
first_name: formData.first_name,
|
||||
last_name: formData.last_name,
|
||||
avatar_url: formData.avatar_url,
|
||||
display_name: formData?.display_name,
|
||||
user_timezone: formData.user_timezone,
|
||||
};
|
||||
// if unsplash or a pre-defined image is uploaded, delete the old uploaded asset
|
||||
if (formData.cover_image_url?.startsWith("http")) {
|
||||
userPayload.cover_image = formData.cover_image_url;
|
||||
userPayload.cover_image_asset = null;
|
||||
}
|
||||
|
||||
const profilePayload: Partial<TUserProfile> = {
|
||||
role: formData.role,
|
||||
language: formData.language,
|
||||
};
|
||||
|
||||
const updateCurrentUserDetail = updateCurrentUser(userPayload).finally(() => setIsLoading(false));
|
||||
const updateCurrentUserProfile = updateUserProfile(profilePayload).finally(() => setIsLoading(false));
|
||||
|
||||
const promises = [updateCurrentUserDetail, updateCurrentUserProfile];
|
||||
const updateUserAndProfile = Promise.all(promises);
|
||||
|
||||
setPromiseToast(updateUserAndProfile, {
|
||||
loading: "Updating...",
|
||||
success: {
|
||||
title: "Success!",
|
||||
message: () => `Profile updated successfully.`,
|
||||
},
|
||||
error: {
|
||||
title: "Error!",
|
||||
message: () => `There was some error in updating your profile. Please try again.`,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeactivateAccountModal isOpen={deactivateAccountModal} onClose={() => setDeactivateAccountModal(false)} />
|
||||
<Controller
|
||||
control={control}
|
||||
name="avatar_url"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<UserImageUploadModal
|
||||
isOpen={isImageUploadModalOpen}
|
||||
onClose={() => setIsImageUploadModalOpen(false)}
|
||||
handleRemove={async () => await handleProfilePictureDelete(currentUser?.avatar_url)}
|
||||
onSuccess={(url) => {
|
||||
onChange(url);
|
||||
handleSubmit(onSubmit)();
|
||||
setIsImageUploadModalOpen(false);
|
||||
}}
|
||||
value={value && value.trim() !== "" ? value : null}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
<div className="relative h-44 w-full">
|
||||
<img
|
||||
src={userCover ? getFileURL(userCover) : "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"}
|
||||
className="h-44 w-full rounded-lg object-cover"
|
||||
alt={currentUser?.first_name ?? "Cover image"}
|
||||
/>
|
||||
<div className="absolute -bottom-6 left-6 flex items-end justify-between">
|
||||
<div className="flex gap-3">
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-lg bg-custom-background-90">
|
||||
<button type="button" onClick={() => setIsImageUploadModalOpen(true)}>
|
||||
{!userAvatar || userAvatar === "" ? (
|
||||
<div className="h-16 w-16 rounded-md bg-custom-background-80 p-2">
|
||||
<CircleUserRound className="h-full w-full text-custom-text-200" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative h-16 w-16 overflow-hidden">
|
||||
<img
|
||||
src={getFileURL(userAvatar)}
|
||||
className="absolute left-0 top-0 h-full w-full rounded-lg object-cover"
|
||||
onClick={() => setIsImageUploadModalOpen(true)}
|
||||
alt={currentUser?.display_name}
|
||||
role="button"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute bottom-3 right-3 flex">
|
||||
<Controller
|
||||
control={control}
|
||||
name="cover_image_url"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<ImagePickerPopover
|
||||
label={t("change_cover")}
|
||||
onChange={(imageUrl) => onChange(imageUrl)}
|
||||
control={control}
|
||||
value={value ?? "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"}
|
||||
isProfileCover
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="item-center mt-6 flex justify-between">
|
||||
<div className="flex flex-col">
|
||||
<div className="item-center flex text-lg font-medium text-custom-text-200">
|
||||
<span>{`${watch("first_name")} ${watch("last_name")}`}</span>
|
||||
</div>
|
||||
<span className="text-sm text-custom-text-300 tracking-tight">{watch("email")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-x-6 gap-y-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm font-medium text-custom-text-200">
|
||||
{t("first_name")}
|
||||
<span className="text-red-500">*</span>
|
||||
</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="first_name"
|
||||
rules={{
|
||||
required: "Please enter first name",
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="first_name"
|
||||
name="first_name"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.first_name)}
|
||||
placeholder="Enter your first name"
|
||||
className={`w-full rounded-md ${errors.first_name ? "border-red-500" : ""}`}
|
||||
maxLength={24}
|
||||
autoComplete="on"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.first_name && <span className="text-xs text-red-500">{errors.first_name.message}</span>}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm font-medium text-custom-text-200">{t("last_name")}</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="last_name"
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="last_name"
|
||||
name="last_name"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.last_name)}
|
||||
placeholder="Enter your last name"
|
||||
className="w-full rounded-md"
|
||||
maxLength={24}
|
||||
autoComplete="on"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm font-medium text-custom-text-200">
|
||||
{t("display_name")}
|
||||
<span className="text-red-500">*</span>
|
||||
</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="display_name"
|
||||
rules={{
|
||||
required: "Display name is required.",
|
||||
validate: (value) => {
|
||||
if (value.trim().length < 1) return "Display name can't be empty.";
|
||||
if (value.split(" ").length > 1) return "Display name can't have two consecutive spaces.";
|
||||
if (value.replace(/\s/g, "").length < 1) return "Display name must be at least 1 character long.";
|
||||
if (value.replace(/\s/g, "").length > 20)
|
||||
return "Display name must be less than 20 characters long.";
|
||||
return true;
|
||||
},
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="display_name"
|
||||
name="display_name"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors?.display_name)}
|
||||
placeholder="Enter your display name"
|
||||
className={`w-full ${errors?.display_name ? "border-red-500" : ""}`}
|
||||
maxLength={24}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors?.display_name && <span className="text-xs text-red-500">{errors?.display_name?.message}</span>}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm font-medium text-custom-text-200">
|
||||
{t("email")}
|
||||
<span className="text-red-500">*</span>
|
||||
</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="email"
|
||||
rules={{
|
||||
required: "Email is required.",
|
||||
}}
|
||||
render={({ field: { value, ref } }) => (
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={value}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.email)}
|
||||
placeholder="Enter your email"
|
||||
className={`w-full cursor-not-allowed rounded-md !bg-custom-background-90 ${
|
||||
errors.email ? "border-red-500" : ""
|
||||
}`}
|
||||
autoComplete="on"
|
||||
disabled
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm font-medium text-custom-text-200">
|
||||
{t("role")}
|
||||
<span className="text-red-500">*</span>
|
||||
</h4>
|
||||
<Controller
|
||||
name="role"
|
||||
control={control}
|
||||
rules={{ required: "Role is required." }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
label={value ? value.toString() : "Select your role"}
|
||||
buttonClassName={errors.role ? "border-red-500" : "border-none"}
|
||||
className="rounded-md border-[0.5px] !border-custom-border-200"
|
||||
optionsClassName="w-full"
|
||||
input
|
||||
>
|
||||
{USER_ROLES.map((item) => (
|
||||
<CustomSelect.Option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
{errors.role && <span className="text-xs text-red-500">Please select a role</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-x-6 gap-y-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm font-medium text-custom-text-200">
|
||||
{t("timezone")}
|
||||
<span className="text-red-500">*</span>
|
||||
</h4>
|
||||
<Controller
|
||||
name="user_timezone"
|
||||
control={control}
|
||||
rules={{ required: "Please select a timezone" }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<TimezoneSelect
|
||||
value={value}
|
||||
onChange={(value: string) => {
|
||||
onChange(value);
|
||||
}}
|
||||
error={Boolean(errors.user_timezone)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.user_timezone && <span className="text-xs text-red-500">{errors.user_timezone.message}</span>}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex gap-2 items-center">
|
||||
<h4 className="text-sm font-medium text-custom-text-200">{t("language")} </h4>
|
||||
<div className="w-fit cursor-pointer rounded-2xl text-custom-primary-200 bg-custom-primary-100/20 text-center font-medium outline-none text-xs px-2">
|
||||
Alpha
|
||||
</div>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="language"
|
||||
rules={{ required: "Please select a language" }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
label={value ? getLanguageLabel(value) : "Select a language"}
|
||||
onChange={onChange}
|
||||
buttonClassName={errors.language ? "border-red-500" : "border-none"}
|
||||
className="rounded-md border-[0.5px] !border-custom-border-200"
|
||||
optionsClassName="w-full"
|
||||
input
|
||||
>
|
||||
{SUPPORTED_LANGUAGES.map((item) => (
|
||||
<CustomSelect.Option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between pt-6 pb-8">
|
||||
<Button variant="primary" type="submit" loading={isLoading}>
|
||||
{isLoading ? t("saving...") : t("save_changes")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<Disclosure as="div" className="border-t border-custom-border-100">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Disclosure.Button as="button" type="button" className="flex w-full items-center justify-between py-4">
|
||||
<span className="text-lg font-medium tracking-tight">{t("deactivate_account")}</span>
|
||||
<ChevronDown className={`h-5 w-5 transition-all ${open ? "rotate-180" : ""}`} />
|
||||
</Disclosure.Button>
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform opacity-0"
|
||||
enterTo="transform opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform opacity-100"
|
||||
leaveTo="transform opacity-0"
|
||||
>
|
||||
<Disclosure.Panel>
|
||||
<div className="flex flex-col gap-8">
|
||||
<span className="text-sm tracking-tight">{t("deactivate_account_description")}</span>
|
||||
<div>
|
||||
<Button variant="danger" onClick={() => setDeactivateAccountModal(true)}>
|
||||
{t("deactivate_account")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -3,5 +3,6 @@ export * from "./overview";
|
|||
export * from "./profile-issues-filter";
|
||||
export * from "./sidebar";
|
||||
export * from "./time";
|
||||
export * from "./profile-setting-content-wrapper"
|
||||
export * from "./profile-setting-content-header"
|
||||
export * from "./profile-setting-content-wrapper";
|
||||
export * from "./profile-setting-content-header";
|
||||
export * from "./form";
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@
|
|||
|
||||
import React, { FC, useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IUserEmailNotificationSettings } from "@plane/types";
|
||||
// ui
|
||||
import { ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// services
|
||||
import { UserService } from "@/services/user.service";
|
||||
// types
|
||||
|
||||
interface IEmailNotificationFormProps {
|
||||
interface IEmailNotificationFormProps {
|
||||
data: IUserEmailNotificationSettings;
|
||||
}
|
||||
|
||||
|
|
@ -18,6 +18,7 @@ const userService = new UserService();
|
|||
|
||||
export const EmailNotificationForm: FC<IEmailNotificationFormProps> = (props) => {
|
||||
const { data } = props;
|
||||
const { t } = useTranslation();
|
||||
// form data
|
||||
const {
|
||||
control,
|
||||
|
|
@ -34,16 +35,16 @@ export const EmailNotificationForm: FC<IEmailNotificationFormProps> = (props) =>
|
|||
[key]: value,
|
||||
});
|
||||
setToast({
|
||||
title: "Success!",
|
||||
title: t("success"),
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
message: "Email notification setting updated successfully",
|
||||
message: t("email_notification_setting_updated_successfully"),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setToast({
|
||||
title: "Error!",
|
||||
title: t("error"),
|
||||
type: TOAST_TYPE.ERROR,
|
||||
message: "Failed to update email notification setting",
|
||||
message: t("failed_to_update_email_notification_setting"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -54,15 +55,13 @@ export const EmailNotificationForm: FC<IEmailNotificationFormProps> = (props) =>
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="pt-6 text-lg font-medium text-custom-text-100">Notify me when:</div>
|
||||
<div className="pt-6 text-lg font-medium text-custom-text-100">{t("notify_me_when")}:</div>
|
||||
{/* Notification Settings */}
|
||||
<div className="flex flex-col py-2">
|
||||
<div className="flex gap-2 items-center pt-6">
|
||||
<div className="grow">
|
||||
<div className="pb-1 text-base font-medium text-custom-text-100">Property changes</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
Notify me when issue's properties like assignees, priority, estimates or anything else changes.
|
||||
</div>
|
||||
<div className="pb-1 text-base font-medium text-custom-text-100">{t("property_changes")}</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">{t("property_changes_description")}</div>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<Controller
|
||||
|
|
@ -83,9 +82,9 @@ export const EmailNotificationForm: FC<IEmailNotificationFormProps> = (props) =>
|
|||
</div>
|
||||
<div className="flex gap-2 items-center pt-6 pb-2">
|
||||
<div className="grow">
|
||||
<div className="pb-1 text-base font-medium text-custom-text-100">State change</div>
|
||||
<div className="pb-1 text-base font-medium text-custom-text-100">{t("state_change")}</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
Notify me when the issues moves to a different state
|
||||
{t("state_change_description")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
|
|
@ -107,8 +106,8 @@ export const EmailNotificationForm: FC<IEmailNotificationFormProps> = (props) =>
|
|||
</div>
|
||||
<div className="flex gap-2 items-center border-0 border-l-[3px] border-custom-border-300 pl-3">
|
||||
<div className="grow">
|
||||
<div className="pb-1 text-base font-medium text-custom-text-100">Issue completed</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">Notify me only when an issue is completed</div>
|
||||
<div className="pb-1 text-base font-medium text-custom-text-100">{t("issue_completed")}</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">{t("issue_completed_description")}</div>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<Controller
|
||||
|
|
@ -129,9 +128,9 @@ export const EmailNotificationForm: FC<IEmailNotificationFormProps> = (props) =>
|
|||
</div>
|
||||
<div className="flex gap-2 items-center pt-6">
|
||||
<div className="grow">
|
||||
<div className="pb-1 text-base font-medium text-custom-text-100">Comments</div>
|
||||
<div className="pb-1 text-base font-medium text-custom-text-100">{t("comments")}</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
Notify me when someone leaves a comment on the issue
|
||||
{t("comments_description")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
|
|
@ -153,9 +152,9 @@ export const EmailNotificationForm: FC<IEmailNotificationFormProps> = (props) =>
|
|||
</div>
|
||||
<div className="flex gap-2 items-center pt-6">
|
||||
<div className="grow">
|
||||
<div className="pb-1 text-base font-medium text-custom-text-100">Mentions</div>
|
||||
<div className="pb-1 text-base font-medium text-custom-text-100">{t("mentions")}</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
Notify me only when someone mentions me in the comments or description
|
||||
{t("mentions_description")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { ChangeEvent } from "react";
|
||||
import { Controller, useFormContext, UseFormSetValue } from "react-hook-form";
|
||||
import { Info } from "lucide-react";
|
||||
// plane ui
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { Input, TextArea, Tooltip } from "@plane/ui";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
|
|
@ -27,6 +28,7 @@ const ProjectCommonAttributes: React.FC<Props> = (props) => {
|
|||
} = useFormContext<TProject>();
|
||||
|
||||
const { getIndex } = getTabIndex(ETabIndices.PROJECT_CREATE, isMobile);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleNameChange = (onChange: (...event: any[]) => void) => (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!isChangeInIdentifierRequired) {
|
||||
|
|
@ -51,10 +53,10 @@ const ProjectCommonAttributes: React.FC<Props> = (props) => {
|
|||
control={control}
|
||||
name="name"
|
||||
rules={{
|
||||
required: "Name is required",
|
||||
required: t("name_is_required"),
|
||||
maxLength: {
|
||||
value: 255,
|
||||
message: "Title should be less than 255 characters",
|
||||
message: t("title_should_be_less_than_255_characters"),
|
||||
},
|
||||
}}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
|
|
@ -65,7 +67,7 @@ const ProjectCommonAttributes: React.FC<Props> = (props) => {
|
|||
value={value}
|
||||
onChange={handleNameChange(onChange)}
|
||||
hasError={Boolean(errors.name)}
|
||||
placeholder="Project name"
|
||||
placeholder={t("project_name")}
|
||||
className="w-full focus:border-blue-400"
|
||||
tabIndex={getIndex("name")}
|
||||
/>
|
||||
|
|
@ -78,17 +80,17 @@ const ProjectCommonAttributes: React.FC<Props> = (props) => {
|
|||
control={control}
|
||||
name="identifier"
|
||||
rules={{
|
||||
required: "Project ID is required",
|
||||
required: t("project_id_is_required"),
|
||||
// allow only alphanumeric & non-latin characters
|
||||
validate: (value) =>
|
||||
/^[ÇŞĞIİÖÜA-Z0-9]+$/.test(value.toUpperCase()) || "Only Alphanumeric & Non-latin characters are allowed.",
|
||||
/^[ÇŞĞIİÖÜA-Z0-9]+$/.test(value.toUpperCase()) || t("only_alphanumeric_non_latin_characters_allowed"),
|
||||
minLength: {
|
||||
value: 1,
|
||||
message: "Project ID must at least be of 1 character",
|
||||
message: t("project_id_must_be_at_least_1_character"),
|
||||
},
|
||||
maxLength: {
|
||||
value: 5,
|
||||
message: "Project ID must at most be of 5 characters",
|
||||
message: t("project_id_must_be_at_most_5_characters"),
|
||||
},
|
||||
}}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
|
|
@ -99,7 +101,7 @@ const ProjectCommonAttributes: React.FC<Props> = (props) => {
|
|||
value={value}
|
||||
onChange={handleIdentifierChange(onChange)}
|
||||
hasError={Boolean(errors.identifier)}
|
||||
placeholder="Project ID"
|
||||
placeholder={t("project_id")}
|
||||
className={cn("w-full text-xs focus:border-blue-400 pr-7", {
|
||||
uppercase: value,
|
||||
})}
|
||||
|
|
@ -109,7 +111,7 @@ const ProjectCommonAttributes: React.FC<Props> = (props) => {
|
|||
/>
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent="Helps you identify issues in the project uniquely. Max 5 characters."
|
||||
tooltipContent={t("project_id_tooltip_content")}
|
||||
className="text-sm"
|
||||
position="right-top"
|
||||
>
|
||||
|
|
@ -126,7 +128,7 @@ const ProjectCommonAttributes: React.FC<Props> = (props) => {
|
|||
id="description"
|
||||
name="description"
|
||||
value={value}
|
||||
placeholder="Description..."
|
||||
placeholder={t("description")}
|
||||
onChange={onChange}
|
||||
className="!h-24 text-sm focus:border-blue-400"
|
||||
hasError={Boolean(errors?.description)}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useState } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { X } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// plane types
|
||||
import { IProject } from "@plane/types";
|
||||
// plane ui
|
||||
|
|
@ -21,6 +22,7 @@ type Props = {
|
|||
const ProjectCreateHeader: React.FC<Props> = (props) => {
|
||||
const { handleClose, isMobile = false } = props;
|
||||
const { watch, control } = useFormContext<IProject>();
|
||||
const { t } = useTranslation();
|
||||
// derived values
|
||||
const coverImage = watch("cover_image_url");
|
||||
|
||||
|
|
@ -33,7 +35,7 @@ const ProjectCreateHeader: React.FC<Props> = (props) => {
|
|||
<img
|
||||
src={getFileURL(coverImage)}
|
||||
className="absolute left-0 top-0 h-full w-full rounded-lg object-cover"
|
||||
alt="Project cover image"
|
||||
alt={t("project_cover_image_alt")}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
@ -48,7 +50,7 @@ const ProjectCreateHeader: React.FC<Props> = (props) => {
|
|||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<ImagePickerPopover
|
||||
label="Change Cover"
|
||||
label={t("change_cover")}
|
||||
onChange={onChange}
|
||||
control={control}
|
||||
value={value}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useFormContext } from "react-hook-form";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IProject } from "@plane/types";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
|
|
@ -13,6 +14,7 @@ type Props = {
|
|||
};
|
||||
|
||||
const ProjectCreateButtons: React.FC<Props> = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const { handleClose, isMobile = false } = props;
|
||||
const {
|
||||
formState: { isSubmitting },
|
||||
|
|
@ -23,10 +25,10 @@ const ProjectCreateButtons: React.FC<Props> = (props) => {
|
|||
return (
|
||||
<div className="flex justify-end gap-2 py-4 border-t border-custom-border-100">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={getIndex("cancel")}>
|
||||
Cancel
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button variant="primary" type="submit" size="sm" loading={isSubmitting} tabIndex={getIndex("submit")}>
|
||||
{isSubmitting ? "Creating" : "Create project"}
|
||||
{isSubmitting ? t("creating") : t("create_project")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { FC, useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Info, Lock } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// plane types
|
||||
import { IProject, IWorkspace } from "@plane/types";
|
||||
// plane ui
|
||||
|
|
@ -43,6 +44,7 @@ export interface IProjectDetailsForm {
|
|||
const projectService = new ProjectService();
|
||||
export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
||||
const { project, workspaceSlug, projectId, isAdmin } = props;
|
||||
const { t } = useTranslation();
|
||||
// states
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
|
@ -361,8 +363,8 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
|||
<div className="flex items-start gap-2">
|
||||
<network.icon className="h-3.5 w-3.5" />
|
||||
<div className="-mt-1">
|
||||
<p>{network.label}</p>
|
||||
<p className="text-xs text-custom-text-400">{network.description}</p>
|
||||
<p>{t(network.label)}</p>
|
||||
<p className="text-xs text-custom-text-400">{t(network.description)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CustomSelect.Option>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import React, { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { Button, getButtonStyling, Row } from "@plane/ui";
|
||||
// components
|
||||
|
|
@ -20,6 +21,7 @@ type Props = {
|
|||
export const ProjectFeatureUpdate: FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, onClose } = props;
|
||||
// store hooks
|
||||
const { t } = useTranslation();
|
||||
const { getProjectById } = useProject();
|
||||
|
||||
if (!workspaceSlug || !projectId) return null;
|
||||
|
|
@ -33,12 +35,12 @@ export const ProjectFeatureUpdate: FC<Props> = observer((props) => {
|
|||
</Row>
|
||||
<div className="flex items-center justify-between gap-2 mt-4 px-6 py-4 border-t border-custom-border-100">
|
||||
<div className="flex gap-1 text-sm text-custom-text-300 font-medium">
|
||||
Congrats! Project <Logo logo={currentProjectDetails.logo_props} />{" "}
|
||||
<p className="break-all">{currentProjectDetails.name}</p> created.
|
||||
{t("congrats")}! {t("project")} <Logo logo={currentProjectDetails.logo_props} />{" "}
|
||||
<p className="break-all">{currentProjectDetails.name}</p> {t("created").toLowerCase()}.
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={onClose} tabIndex={1}>
|
||||
Close
|
||||
{t("close")}
|
||||
</Button>
|
||||
<Link
|
||||
href={`/${workspaceSlug}/projects/${projectId}/issues`}
|
||||
|
|
@ -46,7 +48,7 @@ export const ProjectFeatureUpdate: FC<Props> = observer((props) => {
|
|||
className={getButtonStyling("primary", "sm")}
|
||||
tabIndex={2}
|
||||
>
|
||||
Open project
|
||||
{t("open_project")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IProject } from "@plane/types";
|
||||
import { ToggleSwitch, Tooltip, setPromiseToast } from "@plane/ui";
|
||||
// hooks
|
||||
|
|
@ -20,6 +21,7 @@ type Props = {
|
|||
export const ProjectFeaturesList: FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, isAdmin } = props;
|
||||
// store hooks
|
||||
const { t } = useTranslation();
|
||||
const { captureEvent } = useEventTracker();
|
||||
const { data: currentUser } = useUser();
|
||||
const { getProjectById, updateProject } = useProject();
|
||||
|
|
@ -62,8 +64,8 @@ export const ProjectFeaturesList: FC<Props> = observer((props) => {
|
|||
return (
|
||||
<div key={featureSectionKey} className="">
|
||||
<div className="flex flex-col justify-center pb-2 border-b border-custom-border-100">
|
||||
<h3 className="text-xl font-medium">{feature.title}</h3>
|
||||
<h4 className="text-sm leading-5 text-custom-text-200">{feature.description}</h4>
|
||||
<h3 className="text-xl font-medium">{t(feature.key)}</h3>
|
||||
<h4 className="text-sm leading-5 text-custom-text-200">{t(`${feature.key}_description`)}</h4>
|
||||
</div>
|
||||
{Object.keys(feature.featureList).map((featureItemKey) => {
|
||||
const featureItem = feature.featureList[featureItemKey];
|
||||
|
|
@ -79,7 +81,7 @@ export const ProjectFeaturesList: FC<Props> = observer((props) => {
|
|||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="text-sm font-medium leading-5">{featureItem.title}</h4>
|
||||
<h4 className="text-sm font-medium leading-5">{t(featureItem.key)}</h4>
|
||||
{featureItem.isPro && (
|
||||
<Tooltip tooltipContent="Pro feature" position="top">
|
||||
<UpgradeBadge />
|
||||
|
|
@ -87,7 +89,7 @@ export const ProjectFeaturesList: FC<Props> = observer((props) => {
|
|||
)}
|
||||
</div>
|
||||
<p className="text-sm leading-5 tracking-tight text-custom-text-300">
|
||||
{featureItem.description}
|
||||
{t(`${featureItem.key}_description`)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@
|
|||
import { Dispatch, SetStateAction, useEffect, useState, FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// constants
|
||||
import { ORGANIZATION_SIZE, RESTRICTED_URLS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// constants
|
||||
// types
|
||||
import { IWorkspace } from "@plane/types";
|
||||
// ui
|
||||
|
|
@ -35,14 +36,15 @@ type Props = {
|
|||
const workspaceService = new WorkspaceService();
|
||||
|
||||
export const CreateWorkspaceForm: FC<Props> = observer((props) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
onSubmit,
|
||||
defaultValues,
|
||||
setDefaultValues,
|
||||
secondaryButton,
|
||||
primaryButtonText = {
|
||||
loading: "Creating workspace",
|
||||
default: "Create workspace",
|
||||
loading: t("creating_workspace"),
|
||||
default: t("create_workspace"),
|
||||
},
|
||||
} = props;
|
||||
// states
|
||||
|
|
@ -76,13 +78,13 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
|
|||
payload: {
|
||||
...res,
|
||||
state: "SUCCESS",
|
||||
element: "Create workspace page",
|
||||
element: t("create_workspace_page"),
|
||||
},
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Workspace created successfully.",
|
||||
title: t("success"),
|
||||
message: t("workspace_created_successfully"),
|
||||
});
|
||||
|
||||
if (onSubmit) await onSubmit(res);
|
||||
|
|
@ -92,13 +94,13 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
|
|||
eventName: WORKSPACE_CREATED,
|
||||
payload: {
|
||||
state: "FAILED",
|
||||
element: "Create workspace page",
|
||||
element: t("create_workspace_page"),
|
||||
},
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Workspace could not be created. Please try again.",
|
||||
title: t("error"),
|
||||
message: t("workspace_could_not_be_created_please_try_again"),
|
||||
});
|
||||
});
|
||||
} else setSlugError(true);
|
||||
|
|
@ -106,8 +108,8 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
|
|||
.catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Some error occurred while creating workspace. Please try again.",
|
||||
title: t("error"),
|
||||
message: t("workspace_could_not_be_created_please_try_again"),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
@ -125,7 +127,7 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
|
|||
<div className="space-y-6 sm:space-y-7">
|
||||
<div className="space-y-1 text-sm">
|
||||
<label htmlFor="workspaceName">
|
||||
Name your workspace
|
||||
{t("name_your_workspace")}
|
||||
<span className="ml-0.5 text-red-500">*</span>
|
||||
</label>
|
||||
<div className="flex flex-col gap-1">
|
||||
|
|
@ -133,13 +135,13 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
|
|||
control={control}
|
||||
name="name"
|
||||
rules={{
|
||||
required: "This is a required field.",
|
||||
required: t("this_is_a_required_field"),
|
||||
validate: (value) =>
|
||||
/^[\w\s-]*$/.test(value) ||
|
||||
`Workspaces names can contain only (" "), ( - ), ( _ ) and alphanumeric characters.`,
|
||||
t("workspaces_names_can_contain_only_space_dash_and_alphanumeric_characters"),
|
||||
maxLength: {
|
||||
value: 80,
|
||||
message: "Limit your name to 80 characters.",
|
||||
message: t("limit_your_name_to_80_characters"),
|
||||
},
|
||||
}}
|
||||
render={({ field: { value, ref, onChange } }) => (
|
||||
|
|
@ -156,7 +158,7 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
|
|||
}}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.name)}
|
||||
placeholder="Something familiar and recognizable is always best."
|
||||
placeholder={t("something_familiar_and_recognizable_is_always_best")}
|
||||
className="w-full"
|
||||
/>
|
||||
)}
|
||||
|
|
@ -166,7 +168,7 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
|
|||
</div>
|
||||
<div className="space-y-1 text-sm">
|
||||
<label htmlFor="workspaceUrl">
|
||||
Set your workspace's URL
|
||||
{t("set_your_workspace_url")}
|
||||
<span className="ml-0.5 text-red-500">*</span>
|
||||
</label>
|
||||
<div className="flex w-full items-center rounded-md border-[0.5px] border-custom-border-200 px-3">
|
||||
|
|
@ -175,10 +177,10 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
|
|||
control={control}
|
||||
name="slug"
|
||||
rules={{
|
||||
required: "This is a required field.",
|
||||
required: t("this_is_a_required_field"),
|
||||
maxLength: {
|
||||
value: 48,
|
||||
message: "Limit your URL to 48 characters.",
|
||||
message: t("limit_your_url_to_48_characters"),
|
||||
},
|
||||
}}
|
||||
render={({ field: { onChange, value, ref } }) => (
|
||||
|
|
@ -193,34 +195,34 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
|
|||
}}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.slug)}
|
||||
placeholder="workspace-name"
|
||||
placeholder={t("workspace_name")}
|
||||
className="block w-full rounded-md border-none bg-transparent !px-0 py-2 text-sm"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{slugError && <p className="-mt-3 text-sm text-red-500">Workspace URL is already taken!</p>}
|
||||
{slugError && <p className="-mt-3 text-sm text-red-500">{t("workspace_url_is_already_taken")}</p>}
|
||||
{invalidSlug && (
|
||||
<p className="text-sm text-red-500">{`URLs can contain only ( - ), ( _ ) and alphanumeric characters.`}</p>
|
||||
<p className="text-sm text-red-500">{t("urls_can_contain_only_dash_and_alphanumeric_characters")}</p>
|
||||
)}
|
||||
{errors.slug && <span className="text-xs text-red-500">{errors.slug.message}</span>}
|
||||
</div>
|
||||
<div className="space-y-1 text-sm">
|
||||
<span>
|
||||
How many people will use this workspace?<span className="ml-0.5 text-red-500">*</span>
|
||||
{t("how_many_people_will_use_this_workspace")}<span className="ml-0.5 text-red-500">*</span>
|
||||
</span>
|
||||
<div className="w-full">
|
||||
<Controller
|
||||
name="organization_size"
|
||||
control={control}
|
||||
rules={{ required: "This is a required field." }}
|
||||
rules={{ required: t("this_is_a_required_field") }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
label={
|
||||
ORGANIZATION_SIZE.find((c) => c === value) ?? (
|
||||
<span className="text-custom-text-400">Select a range</span>
|
||||
<span className="text-custom-text-400">{t("select_a_range")}</span>
|
||||
)
|
||||
}
|
||||
buttonClassName="!border-[0.5px] !border-custom-border-200 !shadow-none"
|
||||
|
|
@ -249,7 +251,7 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
|
|||
</Button>
|
||||
{!secondaryButton && (
|
||||
<Button variant="neutral-primary" type="button" size="md" onClick={() => router.back()}>
|
||||
Go back
|
||||
{t("go_back")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { Fragment, Ref, useState } from "react";
|
||||
import { Fragment, Ref, useState, useMemo } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
|
|
@ -10,6 +10,7 @@ import { Check, ChevronDown, LogOut, Mails, PlusSquare, Settings } from "lucide-
|
|||
// ui
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
// types
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IWorkspace } from "@plane/types";
|
||||
// plane ui
|
||||
import { Avatar, Loader, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
|
|
@ -25,25 +26,29 @@ import { getIsWorkspaceCreationDisabled } from "@/plane-web/helpers/instance.hel
|
|||
// components
|
||||
import { WorkspaceLogo } from "../logo";
|
||||
|
||||
// Static Data
|
||||
const userLinks = (workspaceSlug: string) => [
|
||||
{
|
||||
key: "workspace_invites",
|
||||
name: "Workspace invites",
|
||||
href: "/invitations",
|
||||
icon: Mails,
|
||||
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
|
||||
},
|
||||
{
|
||||
key: "settings",
|
||||
name: "Workspace settings",
|
||||
href: `/${workspaceSlug}/settings`,
|
||||
icon: Settings,
|
||||
access: [EUserPermissions.ADMIN],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
export const SidebarDropdown = observer(() => {
|
||||
const { t } = useTranslation();
|
||||
const userLinks = useMemo(
|
||||
() => (workspaceSlug: string) => [
|
||||
{
|
||||
key: "workspace_invites",
|
||||
name: t("workspace_invites"),
|
||||
href: "/invitations",
|
||||
icon: Mails,
|
||||
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
|
||||
},
|
||||
{
|
||||
key: "settings",
|
||||
name: t("workspace_settings"),
|
||||
href: `/${workspaceSlug}/settings`,
|
||||
icon: Settings,
|
||||
access: [EUserPermissions.ADMIN],
|
||||
},
|
||||
],
|
||||
[t]
|
||||
);
|
||||
// router params
|
||||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
|
|
@ -86,8 +91,8 @@ export const SidebarDropdown = observer(() => {
|
|||
await signOut().catch(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Failed to sign out. Please try again.",
|
||||
title: t("error"),
|
||||
message: t("failed_to_sign_out_please_try_again"),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
@ -121,7 +126,7 @@ export const SidebarDropdown = observer(() => {
|
|||
<WorkspaceLogo logo={activeWorkspace?.logo_url} name={activeWorkspace?.name} />
|
||||
{!sidebarCollapsed && (
|
||||
<h4 className="truncate text-base font-medium text-custom-text-100">
|
||||
{activeWorkspace?.name ?? "Loading..."}
|
||||
{activeWorkspace?.name ?? t("loading")}
|
||||
</h4>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -177,7 +182,7 @@ export const SidebarDropdown = observer(() => {
|
|||
<img
|
||||
src={getFileURL(workspace.logo_url)}
|
||||
className="absolute left-0 top-0 h-full w-full rounded object-cover"
|
||||
alt="Workspace Logo"
|
||||
alt={t("workspace_logo")}
|
||||
/>
|
||||
) : (
|
||||
(workspace?.name?.[0] ?? "...")
|
||||
|
|
@ -217,7 +222,7 @@ export const SidebarDropdown = observer(() => {
|
|||
className="flex items-center gap-2 rounded px-2 py-1 text-sm font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
|
||||
>
|
||||
<PlusSquare strokeWidth={1.75} className="h-4 w-4 flex-shrink-0" />
|
||||
Create workspace
|
||||
{t("create_workspace")}
|
||||
</Menu.Item>
|
||||
</Link>
|
||||
)}
|
||||
|
|
@ -251,7 +256,7 @@ export const SidebarDropdown = observer(() => {
|
|||
onClick={handleSignOut}
|
||||
>
|
||||
<LogOut className="size-4 flex-shrink-0" />
|
||||
Sign out
|
||||
{t("sign_out")}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -293,7 +298,7 @@ export const SidebarDropdown = observer(() => {
|
|||
<Menu.Item as="div">
|
||||
<span className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80">
|
||||
<Settings className="h-4 w-4 stroke-[1.5]" />
|
||||
<span>Settings</span>
|
||||
<span>{t("settings")}</span>
|
||||
</span>
|
||||
</Menu.Item>
|
||||
</Link>
|
||||
|
|
@ -306,7 +311,7 @@ export const SidebarDropdown = observer(() => {
|
|||
onClick={handleSignOut}
|
||||
>
|
||||
<LogOut className="size-4 stroke-[1.5]" />
|
||||
Sign out
|
||||
{t("sign_out")}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
{isUserInstanceAdmin && (
|
||||
|
|
@ -314,7 +319,7 @@ export const SidebarDropdown = observer(() => {
|
|||
<Link href={GOD_MODE_URL}>
|
||||
<Menu.Item as="button" type="button" className="w-full">
|
||||
<span className="flex w-full items-center justify-center rounded bg-custom-primary-100/20 px-2 py-1 text-sm font-medium text-custom-primary-100 hover:bg-custom-primary-100/30 hover:text-custom-primary-200">
|
||||
Enter God Mode
|
||||
{t("enter_god_mode")}
|
||||
</span>
|
||||
</Menu.Item>
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { observer } from "mobx-react";
|
|||
import { useParams } from "next/navigation";
|
||||
import { ChevronRight, FolderPlus } from "lucide-react";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { IFavorite } from "@plane/types";
|
||||
import { setToast, TOAST_TYPE, Tooltip } from "@plane/ui";
|
||||
|
|
@ -38,6 +39,7 @@ export const SidebarFavoritesMenu = observer(() => {
|
|||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
// store hooks
|
||||
const { t } = useTranslation();
|
||||
const { sidebarCollapsed } = useAppTheme();
|
||||
const {
|
||||
favoriteIds,
|
||||
|
|
@ -65,8 +67,8 @@ export const SidebarFavoritesMenu = observer(() => {
|
|||
}).catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Failed to move favorite.",
|
||||
title: t("error"),
|
||||
message: t("failed_to_move_favorite"),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
@ -116,15 +118,15 @@ export const SidebarFavoritesMenu = observer(() => {
|
|||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Favorite removed successfully.",
|
||||
title: t("success"),
|
||||
message: t("favorite_removed_successfully"),
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Something went wrong!",
|
||||
title: t("error"),
|
||||
message: t("something_went_wrong"),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
@ -132,8 +134,8 @@ export const SidebarFavoritesMenu = observer(() => {
|
|||
removeFromFavoriteFolder(workspaceSlug.toString(), favoriteId).catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Failed to move favorite.",
|
||||
title: t("error"),
|
||||
message: t("failed_to_move_favorite"),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
@ -143,8 +145,8 @@ export const SidebarFavoritesMenu = observer(() => {
|
|||
reOrderFavorite(workspaceSlug.toString(), favoriteId, droppedFavId, edge).catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Failed reorder favorite",
|
||||
title: t("error"),
|
||||
message: t("failed_to_reorder_favorite"),
|
||||
});
|
||||
});
|
||||
},
|
||||
|
|
@ -198,10 +200,10 @@ export const SidebarFavoritesMenu = observer(() => {
|
|||
)}
|
||||
>
|
||||
<span onClick={() => toggleFavoriteMenu(!isFavoriteMenuOpen)} className="flex-1 text-start">
|
||||
YOUR FAVORITES
|
||||
{t("your_favorites").toUpperCase()}
|
||||
</span>
|
||||
<span className="flex flex-shrink-0 opacity-0 pointer-events-none group-hover/workspace-button:opacity-100 group-hover/workspace-button:pointer-events-auto rounded p-0.5 ">
|
||||
<Tooltip tooltipHeading="Create folder" tooltipContent="">
|
||||
<Tooltip tooltipHeading={t("create_folder")} tooltipContent="">
|
||||
<FolderPlus
|
||||
onClick={() => {
|
||||
setCreateNewFolder(true);
|
||||
|
|
@ -240,7 +242,9 @@ export const SidebarFavoritesMenu = observer(() => {
|
|||
{Object.keys(groupedFavorites).length === 0 ? (
|
||||
<>
|
||||
{!sidebarCollapsed && (
|
||||
<span className="text-custom-text-400 text-xs font-medium px-8 py-1.5">No favorites yet</span>
|
||||
<span className="text-custom-text-400 text-xs font-medium px-8 py-1.5">
|
||||
{t("no_favorites_yet")}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ import { useEffect, useRef } from "react";
|
|||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Controller, SubmitHandler, useForm } from "react-hook-form";
|
||||
// plane helpers
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// plane helpers
|
||||
// plane ui
|
||||
import { FavoriteFolderIcon, Input, setToast, TOAST_TYPE } from "@plane/ui";
|
||||
// hooks
|
||||
|
|
@ -24,6 +25,7 @@ type TProps = {
|
|||
};
|
||||
export const NewFavoriteFolder = observer((props: TProps) => {
|
||||
const { setCreateNewFolder, actionType, defaultName, favoriteId } = props;
|
||||
const { t } = useTranslation();
|
||||
const { workspaceSlug } = useParams();
|
||||
const { addFavorite, updateFavorite, existingFolders } = useFavorite();
|
||||
|
||||
|
|
@ -42,8 +44,8 @@ export const NewFavoriteFolder = observer((props: TProps) => {
|
|||
if (existingFolders.includes(formData.name))
|
||||
return setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Folder already exists",
|
||||
title: t("error"),
|
||||
message: t("folder_already_exists"),
|
||||
});
|
||||
formData = {
|
||||
entity_type: "folder",
|
||||
|
|
@ -56,23 +58,23 @@ export const NewFavoriteFolder = observer((props: TProps) => {
|
|||
if (formData.name === "")
|
||||
return setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Folder name cannot be empty",
|
||||
title: t("error"),
|
||||
message: t("folder_name_cannot_be_empty"),
|
||||
});
|
||||
|
||||
addFavorite(workspaceSlug.toString(), formData)
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Favorite created successfully.",
|
||||
title: t("success"),
|
||||
message: t("favorite_created_successfully"),
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Something went wrong!",
|
||||
title: t("error"),
|
||||
message: t("something_went_wrong"),
|
||||
});
|
||||
});
|
||||
setCreateNewFolder(false);
|
||||
|
|
@ -84,8 +86,8 @@ export const NewFavoriteFolder = observer((props: TProps) => {
|
|||
if (existingFolders.includes(formData.name))
|
||||
return setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Folder already exists",
|
||||
title: t("error"),
|
||||
message: t("folder_already_exists"),
|
||||
});
|
||||
const payload = {
|
||||
name: formData.name.trim(),
|
||||
|
|
@ -94,23 +96,23 @@ export const NewFavoriteFolder = observer((props: TProps) => {
|
|||
if (formData.name.trim() === "")
|
||||
return setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Folder name cannot be empty",
|
||||
title: t("error"),
|
||||
message: t("folder_name_cannot_be_empty"),
|
||||
});
|
||||
|
||||
updateFavorite(workspaceSlug.toString(), favoriteId, payload)
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Favorite updated successfully.",
|
||||
title: t("success"),
|
||||
message: t("favorite_updated_successfully"),
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Something went wrong!",
|
||||
title: t("error"),
|
||||
message: t("something_went_wrong"),
|
||||
});
|
||||
});
|
||||
setCreateNewFolder(false);
|
||||
|
|
@ -132,7 +134,7 @@ export const NewFavoriteFolder = observer((props: TProps) => {
|
|||
name="name"
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field }) => <Input className="w-full" placeholder="New folder" {...field} />}
|
||||
render={({ field }) => <Input className="w-full" placeholder={t("new_folder")} {...field} />}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import React, { useState } from "react";
|
|||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { FileText, HelpCircle, MessagesSquare, MoveLeft, User } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { CustomMenu, Tooltip, ToggleSwitch } from "@plane/ui";
|
||||
// components
|
||||
|
|
@ -24,6 +25,7 @@ export interface WorkspaceHelpSectionProps {
|
|||
export const SidebarHelpSection: React.FC<WorkspaceHelpSectionProps> = observer(() => {
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// store hooks
|
||||
const { t } = useTranslation();
|
||||
const { sidebarCollapsed, toggleSidebar } = useAppTheme();
|
||||
const { toggleShortcutModal } = useCommandPalette();
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
|
@ -83,7 +85,7 @@ export const SidebarHelpSection: React.FC<WorkspaceHelpSectionProps> = observer(
|
|||
className="flex items-center justify- gap-x-2 rounded text-xs hover:bg-custom-background-80"
|
||||
>
|
||||
<FileText className="h-3.5 w-3.5 text-custom-text-200" size={14} />
|
||||
<span className="text-xs">Documentation</span>
|
||||
<span className="text-xs">{t("documentation")}</span>
|
||||
</a>
|
||||
</CustomMenu.MenuItem>
|
||||
{config?.intercom_app_id && config?.is_intercom_enabled && (
|
||||
|
|
@ -94,7 +96,7 @@ export const SidebarHelpSection: React.FC<WorkspaceHelpSectionProps> = observer(
|
|||
className="flex w-full items-center gap-x-2 rounded text-xs hover:bg-custom-background-80"
|
||||
>
|
||||
<MessagesSquare className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
<span className="text-xs">Message support</span>
|
||||
<span className="text-xs">{t("message_support")}</span>
|
||||
</button>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
|
|
@ -105,7 +107,7 @@ export const SidebarHelpSection: React.FC<WorkspaceHelpSectionProps> = observer(
|
|||
className="flex items-center justify- gap-x-2 rounded text-xs hover:bg-custom-background-80"
|
||||
>
|
||||
<User className="h-3.5 w-3.5 text-custom-text-200" size={14} />
|
||||
<span className="text-xs">Contact sales</span>
|
||||
<span className="text-xs">{t("contact_sales")}</span>
|
||||
</a>
|
||||
</CustomMenu.MenuItem>
|
||||
<div className="my-1 border-t border-custom-border-200" />
|
||||
|
|
@ -117,7 +119,7 @@ export const SidebarHelpSection: React.FC<WorkspaceHelpSectionProps> = observer(
|
|||
}}
|
||||
className="flex w-full items-center justify-between text-xs hover:bg-custom-background-80"
|
||||
>
|
||||
<span className="racking-tight">Hyper Mode</span>
|
||||
<span className="racking-tight">{t("hyper_mode")}</span>
|
||||
<ToggleSwitch
|
||||
value={canUseLocalDB}
|
||||
onChange={() => toggleLocalDB(workspaceSlug?.toString(), projectId?.toString())}
|
||||
|
|
@ -130,7 +132,7 @@ export const SidebarHelpSection: React.FC<WorkspaceHelpSectionProps> = observer(
|
|||
onClick={() => toggleShortcutModal(true)}
|
||||
className="flex w-full items-center justify-start text-xs hover:bg-custom-background-80"
|
||||
>
|
||||
<span className="text-xs">Keyboard shortcuts</span>
|
||||
<span className="text-xs">{t("keyboard_shortcuts")}</span>
|
||||
</button>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem>
|
||||
|
|
@ -139,7 +141,7 @@ export const SidebarHelpSection: React.FC<WorkspaceHelpSectionProps> = observer(
|
|||
onClick={() => setProductUpdatesModalOpen(true)}
|
||||
className="flex w-full items-center justify-start text-xs hover:bg-custom-background-80"
|
||||
>
|
||||
<span className="text-xs">What's new</span>
|
||||
<span className="text-xs">{t("whats_new")}</span>
|
||||
</button>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { observer } from "mobx-react";
|
|||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { FileText, Layers } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// plane ui
|
||||
import { Tooltip, DiceIcon, ContrastIcon, LayersIcon, Intake } from "@plane/ui";
|
||||
// components
|
||||
|
|
@ -17,6 +18,7 @@ import { EUserPermissions } from "@/plane-web/constants";
|
|||
import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
|
||||
export type TNavigationItem = {
|
||||
key: string;
|
||||
name: string;
|
||||
href: string;
|
||||
icon: React.ElementType;
|
||||
|
|
@ -34,6 +36,7 @@ type TProjectItemsProps = {
|
|||
export const ProjectNavigation: FC<TProjectItemsProps> = observer((props) => {
|
||||
const { workspaceSlug, projectId, additionalNavigationItems } = props;
|
||||
// store hooks
|
||||
const { t } = useTranslation();
|
||||
const { sidebarCollapsed: isSidebarCollapsed, toggleSidebar } = useAppTheme();
|
||||
const { getProjectById } = useProject();
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
|
@ -54,6 +57,7 @@ export const ProjectNavigation: FC<TProjectItemsProps> = observer((props) => {
|
|||
const baseNavigation = useCallback(
|
||||
(workspaceSlug: string, projectId: string): TNavigationItem[] => [
|
||||
{
|
||||
key: "issues",
|
||||
name: "Issues",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/issues`,
|
||||
icon: LayersIcon,
|
||||
|
|
@ -62,6 +66,7 @@ export const ProjectNavigation: FC<TProjectItemsProps> = observer((props) => {
|
|||
sortOrder: 1,
|
||||
},
|
||||
{
|
||||
key: "cycles",
|
||||
name: "Cycles",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/cycles`,
|
||||
icon: ContrastIcon,
|
||||
|
|
@ -70,6 +75,7 @@ export const ProjectNavigation: FC<TProjectItemsProps> = observer((props) => {
|
|||
sortOrder: 2,
|
||||
},
|
||||
{
|
||||
key: "modules",
|
||||
name: "Modules",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/modules`,
|
||||
icon: DiceIcon,
|
||||
|
|
@ -78,6 +84,7 @@ export const ProjectNavigation: FC<TProjectItemsProps> = observer((props) => {
|
|||
sortOrder: 3,
|
||||
},
|
||||
{
|
||||
key: "views",
|
||||
name: "Views",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/views`,
|
||||
icon: Layers,
|
||||
|
|
@ -86,6 +93,7 @@ export const ProjectNavigation: FC<TProjectItemsProps> = observer((props) => {
|
|||
sortOrder: 4,
|
||||
},
|
||||
{
|
||||
key: "pages",
|
||||
name: "Pages",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/pages`,
|
||||
icon: FileText,
|
||||
|
|
@ -94,6 +102,7 @@ export const ProjectNavigation: FC<TProjectItemsProps> = observer((props) => {
|
|||
sortOrder: 5,
|
||||
},
|
||||
{
|
||||
key: "intake",
|
||||
name: "Intake",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/inbox`,
|
||||
icon: Intake,
|
||||
|
|
@ -137,7 +146,7 @@ export const ProjectNavigation: FC<TProjectItemsProps> = observer((props) => {
|
|||
<Tooltip
|
||||
key={item.name}
|
||||
isMobile={isMobile}
|
||||
tooltipContent={`${project?.name}: ${item.name}`}
|
||||
tooltipContent={`${project?.name}: ${t(item.key)}`}
|
||||
position="right"
|
||||
className="ml-2"
|
||||
disabled={!isSidebarCollapsed}
|
||||
|
|
@ -151,7 +160,7 @@ export const ProjectNavigation: FC<TProjectItemsProps> = observer((props) => {
|
|||
<item.icon
|
||||
className={`flex-shrink-0 size-4 ${item.name === "Intake" ? "stroke-1" : "stroke-[1.5]"}`}
|
||||
/>
|
||||
{!isSidebarCollapsed && <span className="text-xs font-medium">{item.name}</span>}
|
||||
{!isSidebarCollapsed && <span className="text-xs font-medium">{t(item.key)}</span>}
|
||||
</div>
|
||||
</SidebarNavItem>
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { LinkIcon, Star, Settings, Share2, LogOut, MoreHorizontal, ChevronRight
|
|||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
// plane helpers
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { CustomMenu, Tooltip, ArchiveIcon, setPromiseToast, DropIndicator, DragHandle, ControlLink } from "@plane/ui";
|
||||
// components
|
||||
|
|
@ -49,6 +50,7 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
props;
|
||||
// store hooks
|
||||
const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme();
|
||||
const { t } = useTranslation();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
const { addProjectToFavorites, removeProjectFromFavorites, getProjectById } = useProject();
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
|
@ -88,14 +90,14 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
|
||||
const addToFavoritePromise = addProjectToFavorites(workspaceSlug.toString(), project.id);
|
||||
setPromiseToast(addToFavoritePromise, {
|
||||
loading: "Adding project to favorites...",
|
||||
loading: t("adding_project_to_favorites"),
|
||||
success: {
|
||||
title: "Success!",
|
||||
message: () => "Project added to favorites.",
|
||||
title: t("success"),
|
||||
message: () => t("project_added_to_favorites"),
|
||||
},
|
||||
error: {
|
||||
title: "Error!",
|
||||
message: () => "Couldn't add the project to favorites. Please try again.",
|
||||
title: t("error"),
|
||||
message: () => t("couldnt_add_the_project_to_favorites"),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -105,14 +107,14 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
|
||||
const removeFromFavoritePromise = removeProjectFromFavorites(workspaceSlug.toString(), project.id);
|
||||
setPromiseToast(removeFromFavoritePromise, {
|
||||
loading: "Removing project from favorites...",
|
||||
loading: t("removing_project_from_favorites"),
|
||||
success: {
|
||||
title: "Success!",
|
||||
message: () => "Project removed from favorites.",
|
||||
title: t("success"),
|
||||
message: () => t("project_removed_from_favorites"),
|
||||
},
|
||||
error: {
|
||||
title: "Error!",
|
||||
message: () => "Couldn't remove the project from favorites. Please try again.",
|
||||
title: t("error"),
|
||||
message: () => t("couldnt_remove_the_project_from_favorites"),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -251,7 +253,9 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
{!disableDrag && (
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={project.sort_order === null ? "Join the project to rearrange" : "Drag to rearrange"}
|
||||
tooltipContent={
|
||||
project.sort_order === null ? t("join_the_project_to_rearrange") : t("drag_to_rearrange")
|
||||
}
|
||||
position="top-right"
|
||||
disabled={isDragging}
|
||||
>
|
||||
|
|
@ -343,7 +347,7 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
"fill-yellow-500 stroke-yellow-500": project.is_favorite,
|
||||
})}
|
||||
/>
|
||||
<span>{project.is_favorite ? "Remove from favorites" : "Add to favorites"}</span>
|
||||
<span>{project.is_favorite ? t("remove_from_favorites") : t("add_to_favorites")}</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
|
|
@ -355,7 +359,7 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
<div className="flex h-4 w-4 cursor-pointer items-center justify-center rounded text-custom-sidebar-text-200 transition-all duration-300 hover:bg-custom-sidebar-background-80">
|
||||
<Share2 className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
</div>
|
||||
<div>{project.anchor ? "Publish settings" : "Publish"}</div>
|
||||
<div>{project.anchor ? t("publish_settings") : t("publish")}</div>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
|
|
@ -372,7 +376,7 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<LinkIcon className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
<span>Copy link</span>
|
||||
<span>{t("copy_link")}</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
{isAuthorized && (
|
||||
|
|
@ -380,7 +384,7 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
<Link href={`/${workspaceSlug}/projects/${project?.id}/archives/issues`}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<ArchiveIcon className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
<span>Archives</span>
|
||||
<span>{t("archives")}</span>
|
||||
</div>
|
||||
</Link>
|
||||
</CustomMenu.MenuItem>
|
||||
|
|
@ -389,7 +393,7 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
<Link href={`/${workspaceSlug}/projects/${project?.id}/settings`}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<Settings className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
<span>Settings</span>
|
||||
<span>{t("settings")}</span>
|
||||
</div>
|
||||
</Link>
|
||||
</CustomMenu.MenuItem>
|
||||
|
|
@ -398,7 +402,7 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
<CustomMenu.MenuItem onClick={handleLeaveProject}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<LogOut className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
<span>Leave project</span>
|
||||
<span>{t("leave_project")}</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { observer } from "mobx-react";
|
|||
import { useParams, usePathname } from "next/navigation";
|
||||
import { Briefcase, ChevronRight, Plus } from "lucide-react";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
|
||||
// components
|
||||
|
|
@ -34,6 +35,7 @@ export const SidebarProjectsList: FC = observer(() => {
|
|||
// refs
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
// store hooks
|
||||
const { t } = useTranslation();
|
||||
const { toggleCreateProjectModal } = useCommandPalette();
|
||||
const { sidebarCollapsed } = useAppTheme();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
|
|
@ -54,8 +56,8 @@ export const SidebarProjectsList: FC = observer(() => {
|
|||
copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/issues`).then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Link Copied!",
|
||||
message: "Project link copied to clipboard.",
|
||||
title: t("link_copied"),
|
||||
message: t("project_link_copied_to_clipboard"),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
@ -84,8 +86,8 @@ export const SidebarProjectsList: FC = observer(() => {
|
|||
updateProjectView(workspaceSlug.toString(), sourceId, { sort_order: updatedSortOrder }).catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Something went wrong. Please try again.",
|
||||
title: t("error"),
|
||||
message: t("something_went_wrong"),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
@ -175,12 +177,17 @@ export const SidebarProjectsList: FC = observer(() => {
|
|||
)}
|
||||
onClick={() => toggleListDisclosure(!isAllProjectsListOpen)}
|
||||
>
|
||||
<Tooltip tooltipHeading="YOUR PROJECTS" tooltipContent="" position="right" disabled={!isCollapsed}>
|
||||
<Tooltip
|
||||
tooltipHeading={t("your_projects").toUpperCase()}
|
||||
tooltipContent=""
|
||||
position="right"
|
||||
disabled={!isCollapsed}
|
||||
>
|
||||
<>
|
||||
{isCollapsed ? (
|
||||
<Briefcase className="flex-shrink-0 size-3" />
|
||||
) : (
|
||||
<span className="text-xs font-semibold">YOUR PROJECTS</span>
|
||||
<span className="text-xs font-semibold">{t("your_projects").toUpperCase()}</span>
|
||||
)}
|
||||
</>
|
||||
</Tooltip>
|
||||
|
|
@ -188,7 +195,7 @@ export const SidebarProjectsList: FC = observer(() => {
|
|||
{!isCollapsed && (
|
||||
<div className="flex items-center opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto">
|
||||
{isAuthorizedUser && (
|
||||
<Tooltip tooltipHeading="Create project" tooltipContent="">
|
||||
<Tooltip tooltipHeading={t("create_project")} tooltipContent="">
|
||||
<button
|
||||
type="button"
|
||||
className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0"
|
||||
|
|
@ -265,7 +272,7 @@ export const SidebarProjectsList: FC = observer(() => {
|
|||
toggleCreateProjectModal(true);
|
||||
}}
|
||||
>
|
||||
{!isCollapsed && "Add project"}
|
||||
{!isCollapsed && t("add_project")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useRef, useState } from "react";
|
|||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { ChevronUp, PenSquare, Search } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
// components
|
||||
|
|
@ -15,6 +16,7 @@ import useLocalStorage from "@/hooks/use-local-storage";
|
|||
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
|
||||
export const SidebarQuickActions = observer(() => {
|
||||
const { t } = useTranslation();
|
||||
// states
|
||||
const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false);
|
||||
const [isDraftButtonOpen, setIsDraftButtonOpen] = useState(false);
|
||||
|
|
@ -92,7 +94,7 @@ export const SidebarQuickActions = observer(() => {
|
|||
disabled={disabled}
|
||||
>
|
||||
<PenSquare className="size-4" />
|
||||
{!isSidebarCollapsed && <span className="text-sm font-medium">New issue</span>}
|
||||
{!isSidebarCollapsed && <span className="text-sm font-medium">{t("new_issue")}</span>}
|
||||
</button>
|
||||
<button
|
||||
className={cn(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import React from "react";
|
|||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// components
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { SidebarNavItem } from "@/components/sidebar";
|
||||
|
|
@ -22,6 +23,7 @@ import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
|||
import { isUserFeatureEnabled } from "@/plane-web/helpers/dashboard.helper";
|
||||
|
||||
export const SidebarUserMenu = observer(() => {
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
|
||||
const { captureEvent } = useEventTracker();
|
||||
|
|
@ -62,28 +64,28 @@ export const SidebarUserMenu = observer(() => {
|
|||
})}
|
||||
>
|
||||
{SIDEBAR_USER_MENU_ITEMS.map((link) => {
|
||||
if (link.key === "drafts" && draftIssueCount === 0) return null;
|
||||
if (!isUserFeatureEnabled(link.key)) return null;
|
||||
if (link.value === "drafts" && draftIssueCount === 0) return null;
|
||||
if (!isUserFeatureEnabled(link.value)) return null;
|
||||
return (
|
||||
allowPermissions(link.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString()) && (
|
||||
<Tooltip
|
||||
key={link.key}
|
||||
tooltipContent={link.label}
|
||||
key={link.value}
|
||||
tooltipContent={t(link.key)}
|
||||
position="right"
|
||||
className="ml-2"
|
||||
disabled={!sidebarCollapsed}
|
||||
isMobile={isMobile}
|
||||
>
|
||||
<Link key={link.key} href={getHref(link)} onClick={() => handleLinkClick(link.key)}>
|
||||
<Link key={link.value} href={getHref(link)} onClick={() => handleLinkClick(link.value)}>
|
||||
<SidebarNavItem
|
||||
className={`${sidebarCollapsed ? "p-0 size-8 aspect-square justify-center mx-auto" : ""}`}
|
||||
isActive={link.highlight(pathname, `/${workspaceSlug}`, { userId: currentUser?.id })}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 py-[1px]">
|
||||
<link.Icon className="size-4 flex-shrink-0" />
|
||||
{!sidebarCollapsed && <p className="text-sm leading-5 font-medium">{link.label}</p>}
|
||||
{!sidebarCollapsed && <p className="text-sm leading-5 font-medium">{t(link.key)}</p>}
|
||||
</div>
|
||||
{link.key === "notifications" && notificationIndicatorElement}
|
||||
{link.value === "notifications" && notificationIndicatorElement}
|
||||
</SidebarNavItem>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import Link from "next/link";
|
|||
import { useParams, usePathname } from "next/navigation";
|
||||
import { ArchiveIcon, ChevronRight, MoreHorizontal, Settings } from "lucide-react";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
// plane helpers
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { CustomMenu, Tooltip } from "@plane/ui";
|
||||
// components
|
||||
|
|
@ -38,6 +38,7 @@ export const SidebarWorkspaceMenu = observer(() => {
|
|||
// pathname
|
||||
const pathname = usePathname();
|
||||
// store hooks
|
||||
const { t } = useTranslation();
|
||||
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
|
||||
const { captureEvent } = useEventTracker();
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
|
@ -85,7 +86,7 @@ export const SidebarWorkspaceMenu = observer(() => {
|
|||
className="flex-1 sticky top-0 z-10 w-full py-1.5 flex items-center justify-between gap-1 text-custom-sidebar-text-400 text-xs font-semibold"
|
||||
onClick={() => toggleWorkspaceMenu(!isWorkspaceMenuOpen)}
|
||||
>
|
||||
<span>WORKSPACE</span>
|
||||
<span>{t("workspace").toUpperCase()}</span>
|
||||
</Disclosure.Button>
|
||||
<CustomMenu
|
||||
customButton={
|
||||
|
|
@ -112,7 +113,7 @@ export const SidebarWorkspaceMenu = observer(() => {
|
|||
<Link href={`/${workspaceSlug}/projects/archives`}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<ArchiveIcon className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
<span>Archives</span>
|
||||
<span>{t("archives")}</span>
|
||||
</div>
|
||||
</Link>
|
||||
</CustomMenu.MenuItem>
|
||||
|
|
@ -122,7 +123,7 @@ export const SidebarWorkspaceMenu = observer(() => {
|
|||
<Link href={`/${workspaceSlug}/settings`}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<Settings className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
<span>Settings</span>
|
||||
<span>{t("settings")}</span>
|
||||
</div>
|
||||
</Link>
|
||||
</CustomMenu.MenuItem>
|
||||
|
|
@ -162,32 +163,32 @@ export const SidebarWorkspaceMenu = observer(() => {
|
|||
static
|
||||
>
|
||||
{SIDEBAR_WORKSPACE_MENU_ITEMS.map((link) => {
|
||||
if (!isWorkspaceFeatureEnabled(link.key, workspaceSlug.toString())) return null;
|
||||
if (!isWorkspaceFeatureEnabled(link.value, workspaceSlug.toString())) return null;
|
||||
return (
|
||||
allowPermissions(link.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString()) && (
|
||||
<Tooltip
|
||||
key={link.key}
|
||||
tooltipContent={link.label}
|
||||
key={link.value}
|
||||
tooltipContent={t(link.key)}
|
||||
position="right"
|
||||
className="ml-2"
|
||||
disabled={!sidebarCollapsed}
|
||||
isMobile={isMobile}
|
||||
>
|
||||
<Link href={`/${workspaceSlug}${link.href}`} onClick={() => handleLinkClick(link.key)}>
|
||||
<Link href={`/${workspaceSlug}${link.href}`} onClick={() => handleLinkClick(link.value)}>
|
||||
<SidebarNavItem
|
||||
key={link.key}
|
||||
key={link.value}
|
||||
className={`${sidebarCollapsed ? "p-0 size-8 aspect-square justify-center mx-auto" : ""}`}
|
||||
isActive={link.highlight(pathname, `/${workspaceSlug}`)}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 py-[1px]">
|
||||
<link.Icon
|
||||
className={cn("size-4", {
|
||||
"rotate-180": link.key === "active-cycles",
|
||||
"rotate-180": link.value === "active-cycles",
|
||||
})}
|
||||
/>
|
||||
{!sidebarCollapsed && <p className="text-sm leading-5 font-medium">{link.label}</p>}
|
||||
{!sidebarCollapsed && <p className="text-sm leading-5 font-medium">{t(link.key)}</p>}
|
||||
</div>
|
||||
{!sidebarCollapsed && link.key === "active-cycles" && indicatorElement}
|
||||
{!sidebarCollapsed && link.value === "active-cycles" && indicatorElement}
|
||||
</SidebarNavItem>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
|
|
|
|||
|
|
@ -93,34 +93,40 @@ export const CYCLE_STATUS: {
|
|||
|
||||
export const WORKSPACE_ACTIVE_CYCLES_DETAILS = [
|
||||
{
|
||||
key: "10000_feet_view",
|
||||
title: "10,000-feet view of all active cycles.",
|
||||
description:
|
||||
"Zoom out to see running cycles across all your projects at once instead of going from Cycle to Cycle in each project.",
|
||||
icon: Folder,
|
||||
},
|
||||
{
|
||||
key: "get_snapshot_of_each_active_cycle",
|
||||
title: "Get a snapshot of each active cycle.",
|
||||
description:
|
||||
"Track high-level metrics for all active cycles, see their state of progress, and get a sense of scope against deadlines.",
|
||||
icon: CircleDashed,
|
||||
},
|
||||
{
|
||||
key: "compare_burndowns",
|
||||
title: "Compare burndowns.",
|
||||
description: "Monitor how each of your teams are performing with a peek into each cycle’s burndown report.",
|
||||
icon: BarChart4,
|
||||
},
|
||||
{
|
||||
key: "quickly_see_make_or_break_issues",
|
||||
title: "Quickly see make-or-break issues. ",
|
||||
description:
|
||||
"Preview high-priority issues for each cycle against due dates. See all of them per cycle in one click.",
|
||||
icon: AlertOctagon,
|
||||
},
|
||||
{
|
||||
key: "zoom_into_cycles_that_need_attention",
|
||||
title: "Zoom into cycles that need attention. ",
|
||||
description: "Investigate the state of any cycle that doesn’t conform to expectations in one click.",
|
||||
icon: Search,
|
||||
},
|
||||
{
|
||||
key: "stay_ahead_of_blockers",
|
||||
title: "Stay ahead of blockers.",
|
||||
description:
|
||||
"Spot challenges from one project to another and see inter-cycle dependencies that aren’t obvious from any other view.",
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ export const PROFILE_ACTION_LINKS: {
|
|||
|
||||
export const PROFILE_VIEWER_TAB = [
|
||||
{
|
||||
key: "summary",
|
||||
route: "",
|
||||
label: "Summary",
|
||||
selected: "/",
|
||||
|
|
@ -56,6 +57,7 @@ export const PROFILE_VIEWER_TAB = [
|
|||
|
||||
export const PROFILE_ADMINS_TAB = [
|
||||
{
|
||||
key: "assigned",
|
||||
route: "assigned",
|
||||
label: "Assigned",
|
||||
selected: "/assigned/",
|
||||
|
|
@ -66,11 +68,13 @@ export const PROFILE_ADMINS_TAB = [
|
|||
selected: "/created/",
|
||||
},
|
||||
{
|
||||
key: "subscribed",
|
||||
route: "subscribed",
|
||||
label: "Subscribed",
|
||||
selected: "/subscribed/",
|
||||
},
|
||||
{
|
||||
key: "activity",
|
||||
route: "activity",
|
||||
label: "Activity",
|
||||
selected: "/activity/",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export const THEMES = ["light", "dark", "light-contrast", "dark-contrast", "custom"];
|
||||
|
||||
export interface I_THEME_OPTION {
|
||||
key: string;
|
||||
value: string;
|
||||
label: string;
|
||||
type: string;
|
||||
|
|
@ -13,6 +14,7 @@ export interface I_THEME_OPTION {
|
|||
|
||||
export const THEME_OPTIONS: I_THEME_OPTION[] = [
|
||||
{
|
||||
key: "system_preference",
|
||||
value: "system",
|
||||
label: "System preference",
|
||||
type: "light",
|
||||
|
|
@ -23,6 +25,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
|
|||
},
|
||||
},
|
||||
{
|
||||
key: "light",
|
||||
value: "light",
|
||||
label: "Light",
|
||||
type: "light",
|
||||
|
|
@ -33,6 +36,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
|
|||
},
|
||||
},
|
||||
{
|
||||
key: "dark",
|
||||
value: "dark",
|
||||
label: "Dark",
|
||||
type: "dark",
|
||||
|
|
@ -43,6 +47,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
|
|||
},
|
||||
},
|
||||
{
|
||||
key: "light_contrast",
|
||||
value: "light-contrast",
|
||||
label: "Light high contrast",
|
||||
type: "light",
|
||||
|
|
@ -53,6 +58,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
|
|||
},
|
||||
},
|
||||
{
|
||||
key: "dark_contrast",
|
||||
value: "dark-contrast",
|
||||
label: "Dark high contrast",
|
||||
type: "dark",
|
||||
|
|
@ -63,6 +69,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
|
|||
},
|
||||
},
|
||||
{
|
||||
key: "custom",
|
||||
value: "custom",
|
||||
label: "Custom theme",
|
||||
type: "light",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { ReactNode, useEffect, FC, useState } from "react";
|
||||
import { ReactNode, useEffect, FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useTranslation, Language } from "@plane/i18n";
|
||||
// helpers
|
||||
import { applyTheme, unsetCustomCssVariables } from "@/helpers/theme.helper";
|
||||
// hooks
|
||||
|
|
@ -21,6 +22,7 @@ const StoreWrapper: FC<TStoreWrapper> = observer((props) => {
|
|||
const { setQuery } = useRouterParams();
|
||||
const { sidebarCollapsed, toggleSidebar } = useAppTheme();
|
||||
const { data: userProfile } = useUserProfile();
|
||||
const { changeLanguage } = useTranslation();
|
||||
|
||||
/**
|
||||
* Sidebar collapsed fetching from local storage
|
||||
|
|
@ -28,7 +30,6 @@ const StoreWrapper: FC<TStoreWrapper> = observer((props) => {
|
|||
useEffect(() => {
|
||||
const localValue = localStorage && localStorage.getItem("app_sidebar_collapsed");
|
||||
const localBoolValue = localValue ? (localValue === "true" ? true : false) : false;
|
||||
|
||||
if (localValue && sidebarCollapsed === undefined) toggleSidebar(localBoolValue);
|
||||
}, [sidebarCollapsed, setTheme, toggleSidebar]);
|
||||
|
||||
|
|
@ -37,7 +38,6 @@ const StoreWrapper: FC<TStoreWrapper> = observer((props) => {
|
|||
*/
|
||||
useEffect(() => {
|
||||
if (!userProfile?.theme?.theme) return;
|
||||
|
||||
const currentTheme = userProfile?.theme?.theme || "system";
|
||||
const currentThemePalette = userProfile?.theme?.palette;
|
||||
if (currentTheme) {
|
||||
|
|
@ -51,6 +51,11 @@ const StoreWrapper: FC<TStoreWrapper> = observer((props) => {
|
|||
}
|
||||
}, [userProfile?.theme?.theme, userProfile?.theme?.palette, setTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!userProfile?.language) return;
|
||||
changeLanguage(userProfile?.language as Language);
|
||||
}, [userProfile?.language, changeLanguage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!params) return;
|
||||
setQuery(params);
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ export class ProfileStore implements IUserProfileStore {
|
|||
has_billing_address: false,
|
||||
created_at: "",
|
||||
updated_at: "",
|
||||
language: ""
|
||||
};
|
||||
|
||||
// services
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue