UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: канонизация статусов и системы оценок в настройках проекта

This commit is contained in:
DCCONSTRUCTIONS 2026-04-22 22:05:31 +03:00
parent b8f2654e80
commit b7b0388dc1
17 changed files with 292 additions and 63 deletions

View File

@ -18,6 +18,7 @@ import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
import { useProjectEstimates } from "@/hooks/store/estimates";
// local imports
import { EstimatePointCreateRoot } from "../points";
import { getLocalizedEstimateSystemName, getLocalizedEstimateTemplateValues } from "../helpers";
import { EstimateCreateStageOne } from "./stage-one";
type TCreateEstimateModal = {
@ -92,7 +93,7 @@ export const CreateEstimateModal = observer(function CreateEstimateModal(props:
setButtonLoader(true);
const payload: IEstimateFormData = {
estimate: {
name: ESTIMATE_SYSTEMS[estimateSystem]?.name,
name: getLocalizedEstimateSystemName(estimateSystem, t, ESTIMATE_SYSTEMS[estimateSystem]?.name),
type: estimateSystem,
last_used: true,
},
@ -142,15 +143,19 @@ export const CreateEstimateModal = observer(function CreateEstimateModal(props:
// }, [estimatePointError]);
return (
<ModalCore isOpen={isOpen} position={EModalPosition.TOP} width={EModalWidth.XXL}>
<div className="relative space-y-6 py-5">
<ModalCore
isOpen={isOpen}
position={EModalPosition.CENTER}
width={EModalWidth.XXL}
className="max-w-[52rem] overflow-hidden rounded-[1.85rem] p-0"
>
<div className="relative space-y-6 px-5 py-5">
{/* heading */}
<div className="relative flex items-center justify-between gap-2 px-5">
<div className="relative flex items-center justify-between gap-2">
<div className="relative flex items-center gap-1">
{estimatePoints && (
<div
onClick={() => {
setEstimateSystem(EEstimateSystem.POINTS);
handleUpdatePoints(undefined);
}}
className="flex h-5 w-5 flex-shrink-0 cursor-pointer items-center justify-center"
@ -169,13 +174,13 @@ export const CreateEstimateModal = observer(function CreateEstimateModal(props:
</div>
{/* estimate steps */}
<div className="px-5">
<div>
{!estimatePoints && (
<EstimateCreateStageOne
estimateSystem={estimateSystem}
handleEstimateSystem={setEstimateSystem}
handleEstimatePoints={(templateType: string) =>
handleUpdatePoints(ESTIMATE_SYSTEMS[estimateSystem].templates[templateType].values)
handleUpdatePoints(getLocalizedEstimateTemplateValues(estimateSystem, templateType, t))
}
/>
)}
@ -199,12 +204,24 @@ export const CreateEstimateModal = observer(function CreateEstimateModal(props:
)} */}
</div>
<div className="relative flex items-center justify-end gap-3 border-t border-subtle px-5 pt-5">
<Button variant="secondary" size="lg" onClick={handleClose} disabled={buttonLoader}>
<div className="relative flex items-center justify-end gap-3 border-t border-subtle pt-5">
<Button
variant="secondary"
size="lg"
onClick={handleClose}
disabled={buttonLoader}
className="nodedc-modal-secondary-button min-w-[8.75rem]"
>
{t("common.cancel")}
</Button>
{estimatePoints && (
<Button variant="primary" size="lg" onClick={handleCreateEstimate} disabled={buttonLoader}>
<Button
variant="primary"
size="lg"
onClick={handleCreateEstimate}
disabled={buttonLoader}
className="nodedc-modal-primary-button min-w-[10.5rem]"
>
{buttonLoader ? t("common.creating") : t("project_settings.estimates.create.label")}
</Button>
)}

View File

@ -16,6 +16,10 @@ import { convertMinutesToHoursMinutesString } from "@plane/utils";
import { isEstimateSystemEnabled } from "@/plane-web/components/estimates/helper";
import { UpgradeBadge } from "@/plane-web/components/workspace/upgrade-badge";
import { RadioInput } from "../radio-select";
import {
getLocalizedEstimatePointValue,
getLocalizedEstimateTemplateTitle,
} from "../helpers";
// local imports
type TEstimateCreateStageOne = {
@ -65,10 +69,10 @@ export function EstimateCreateStageOne(props: TEstimateCreateStageOne) {
.filter((option) => option !== null)}
name="estimate-radio-input"
label={t("project_settings.estimates.create.choose_estimate_system")}
labelClassName="text-13 font-medium text-secondary mb-1.5"
wrapperClassName="relative flex flex-wrap gap-14"
fieldClassName="relative flex items-center gap-1.5"
buttonClassName="size-4"
labelClassName="mb-1.5 text-13 font-medium text-secondary"
wrapperClassName="relative flex flex-wrap gap-x-10 gap-y-4"
fieldClassName="relative flex items-center gap-2.5 text-primary"
buttonClassName="size-[1.05rem]"
selected={estimateSystem}
onChange={(value) => handleEstimateSystem(value as TEstimateSystemKeys)}
/>
@ -80,13 +84,13 @@ export function EstimateCreateStageOne(props: TEstimateCreateStageOne) {
{t("project_settings.estimates.create.start_from_scratch")}
</div>
<button
className="block w-full space-y-1 rounded-md border border-subtle p-3 py-2.5 text-left hover:bg-layer-transparent-hover"
type="button"
className="nodedc-modal-field block w-full space-y-1.5 rounded-[1.35rem] px-4 py-4 text-left transition-colors hover:bg-white/8"
onClick={() => handleEstimatePoints("custom")}
>
<p className="text-14 font-medium">{t("project_settings.estimates.create.custom")}</p>
<p className="text-11 text-tertiary">
{/* TODO: Translate here */}
Add your own <span className="lowercase">{currentEstimateSystem.name}</span> from scratch.
{t(`project_settings.estimates.create.custom_description.${estimateSystem}`)}
</p>
</button>
</div>
@ -100,16 +104,17 @@ export function EstimateCreateStageOne(props: TEstimateCreateStageOne) {
currentEstimateSystem.templates[name]?.hide ? null : (
<button
key={name}
className="space-y-1 rounded-md border border-subtle p-3 py-2.5 text-left hover:bg-surface-2"
type="button"
className="nodedc-modal-field space-y-1.5 rounded-[1.35rem] px-4 py-4 text-left transition-colors hover:bg-white/8"
onClick={() => handleEstimatePoints(name)}
>
<p className="text-14 font-medium">{currentEstimateSystem.templates[name]?.title}</p>
<p className="text-14 font-medium">{getLocalizedEstimateTemplateTitle(estimateSystem, name, t)}</p>
<p className="text-11 text-tertiary">
{currentEstimateSystem.templates[name]?.values
?.map((template) =>
estimateSystem === (EEstimateSystem.TIME as TEstimateSystemKeys)
? convertMinutesToHoursMinutesString(Number(template.value)).trim()
: template.value
: getLocalizedEstimatePointValue(estimateSystem, template.value, t)
)
?.join(", ")}
</p>

View File

@ -14,6 +14,7 @@ import { AlertModalCore } from "@plane/ui";
import { useProjectEstimates } from "@/hooks/store/estimates";
import { useEstimate } from "@/hooks/store/estimates/use-estimate";
import { useProject } from "@/hooks/store/use-project";
import { getLocalizedEstimateDisplayName } from "../helpers";
type TDeleteEstimateModal = {
workspaceSlug: string;
@ -33,6 +34,8 @@ export const DeleteEstimateModal = observer(function DeleteEstimateModal(props:
const { updateProject } = useProject();
// states
const [buttonLoader, setButtonLoader] = useState(false);
const localizedEstimateName =
estimate?.type && estimate?.name ? getLocalizedEstimateDisplayName(estimate.name, estimate.type, t) : estimate?.name ?? "";
const handleDeleteEstimate = async () => {
try {
@ -66,7 +69,7 @@ export const DeleteEstimateModal = observer(function DeleteEstimateModal(props:
isSubmitting={buttonLoader}
isOpen={isOpen}
title={t("project_settings.estimates.delete_modal.title")}
content={t("project_settings.estimates.delete_modal.description", { value: estimate?.name ?? "" })}
content={t("project_settings.estimates.delete_modal.description", { value: localizedEstimateName })}
primaryButtonText={{
loading: t("project_settings.estimates.delete_modal.loading"),
default: t("project_settings.estimates.delete_modal.submit"),

View File

@ -7,9 +7,11 @@
import { observer } from "mobx-react";
// plane imports
import { EEstimateSystem } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { convertMinutesToHoursMinutesString } from "@plane/utils";
// components
import { SettingsBoxedControlItem } from "@/components/settings/boxed-control-item";
import { getLocalizedEstimateDisplayName, getLocalizedEstimatePointValue } from "./helpers";
// hooks
import { useProjectEstimates } from "@/hooks/store/estimates";
import { useEstimate } from "@/hooks/store/estimates/use-estimate";
@ -27,6 +29,7 @@ type TEstimateListItem = {
export const EstimateListItem = observer(function EstimateListItem(props: TEstimateListItem) {
const { estimateId } = props;
const { t } = useTranslation();
// store hooks
const { estimateById } = useProjectEstimates();
const { estimatePointIds, estimatePointById } = useEstimate(estimateId);
@ -41,13 +44,13 @@ export const EstimateListItem = observer(function EstimateListItem(props: TEstim
return (
<SettingsBoxedControlItem
title={currentEstimate.name}
title={getLocalizedEstimateDisplayName(currentEstimate.name, currentEstimate.type, t)}
description={estimatePointValues
?.map((estimatePointValue) => {
if (currentEstimate.type === EEstimateSystem.TIME) {
return convertMinutesToHoursMinutesString(Number(estimatePointValue));
}
return estimatePointValue;
return getLocalizedEstimatePointValue(currentEstimate.type, estimatePointValue || "", t);
})
.join(", ")}
control={<EstimateListItemButtons {...props} />}

View File

@ -0,0 +1,89 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { ESTIMATE_SYSTEMS, EEstimateSystem } from "@plane/constants";
import type { TEstimatePointsObject, TEstimateSystemKeys } from "@plane/types";
const CATEGORY_POINT_TRANSLATION_KEYS: Record<string, string> = {
Easy: "project_settings.estimates.values.easy",
Medium: "project_settings.estimates.values.medium",
Hard: "project_settings.estimates.values.hard",
"Very Hard": "project_settings.estimates.values.very_hard",
};
const ESTIMATE_ADD_LABELS: Record<TEstimateSystemKeys, string> = {
points: "project_settings.estimates.create.add_points",
categories: "project_settings.estimates.create.add_categories",
time: "project_settings.estimates.create.add_time",
};
export const getLocalizedEstimateSystemName = (
estimateType: TEstimateSystemKeys,
t: (key: string) => string,
fallbackName?: string
) => {
const localizedName = ESTIMATE_SYSTEMS[estimateType]?.i18n_name
? t(ESTIMATE_SYSTEMS[estimateType].i18n_name)
: undefined;
return localizedName || fallbackName || ESTIMATE_SYSTEMS[estimateType]?.name || "";
};
export const getLocalizedEstimateDisplayName = (
name: string | undefined,
estimateType: TEstimateSystemKeys,
t: (key: string) => string
) => {
if (!name) return "";
const defaultName = ESTIMATE_SYSTEMS[estimateType]?.name;
if (defaultName && name.trim().toLowerCase() === defaultName.trim().toLowerCase()) {
return getLocalizedEstimateSystemName(estimateType, t, name);
}
return name;
};
export const getLocalizedEstimateTemplateTitle = (
estimateType: TEstimateSystemKeys,
templateName: string,
t: (key: string) => string
) => {
const template = ESTIMATE_SYSTEMS[estimateType]?.templates?.[templateName];
if (!template) return templateName;
return template.i18n_title ? t(template.i18n_title) : template.title;
};
export const getLocalizedEstimatePointValue = (
estimateType: TEstimateSystemKeys,
value: string,
t: (key: string) => string
) => {
if (estimateType !== EEstimateSystem.CATEGORIES) return value;
const translationKey = CATEGORY_POINT_TRANSLATION_KEYS[value];
return translationKey ? t(translationKey) : value;
};
export const getLocalizedEstimateTemplateValues = (
estimateType: TEstimateSystemKeys,
templateName: string,
t: (key: string) => string
): TEstimatePointsObject[] => {
const template = ESTIMATE_SYSTEMS[estimateType]?.templates?.[templateName];
if (!template) return [];
return template.values.map((templateValue) => ({
...templateValue,
value: getLocalizedEstimatePointValue(estimateType, templateValue.value, t),
}));
};
export const getLocalizedEstimateAddLabel = (
estimateType: TEstimateSystemKeys,
t: (key: string) => string
) => t(ESTIMATE_ADD_LABELS[estimateType]);

View File

@ -9,11 +9,13 @@ import { useCallback, useState } from "react";
import { observer } from "mobx-react";
// plane imports
import { estimateCount } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { PlusIcon } from "@plane/propel/icons";
import type { TEstimatePointsObject, TEstimateSystemKeys, TEstimateTypeError } from "@plane/types";
import { Sortable } from "@plane/ui";
// local imports
import { getLocalizedEstimateAddLabel, getLocalizedEstimateSystemName } from "../helpers";
import { EstimatePointCreate } from "./create";
import { EstimatePointItemPreview } from "./preview";
@ -46,6 +48,7 @@ export const EstimatePointCreateRoot = observer(function EstimatePointCreateRoot
estimatePointError,
handleEstimatePointError,
} = props;
const { t } = useTranslation();
// states
const [estimatePointCreate, setEstimatePointCreate] = useState<TEstimatePointsObject[] | undefined>(undefined);
@ -115,8 +118,8 @@ export const EstimatePointCreateRoot = observer(function EstimatePointCreateRoot
if (!workspaceSlug || !projectId) return <></>;
return (
<div className="space-y-1">
<div className="text-13 font-medium text-secondary capitalize">{estimateType}</div>
<div className="space-y-3">
<div className="text-13 font-medium text-secondary">{getLocalizedEstimateSystemName(estimateType, t)}</div>
<div>
<Sortable
@ -171,8 +174,14 @@ export const EstimatePointCreateRoot = observer(function EstimatePointCreateRoot
/>
))}
{estimatePoints && estimatePoints.length + (estimatePointCreate?.length || 0) <= estimateCount.max - 1 && (
<Button variant="link" prependIcon={<PlusIcon />} onClick={handleCreate}>
Add {estimateType}
<Button
type="button"
variant="ghost"
prependIcon={<PlusIcon />}
onClick={handleCreate}
className="nodedc-modal-chip inline-flex w-fit items-center gap-2 px-4"
>
{getLocalizedEstimateAddLabel(estimateType, t)}
</Button>
)}
</div>

View File

@ -170,8 +170,8 @@ export const EstimatePointCreate = observer(function EstimatePointCreate(props:
<form onSubmit={handleCreate} className="relative flex items-center gap-2 pr-2.5 text-14">
<div
className={cn(
"relative my-1 flex w-full items-center rounded-sm border",
estimatePointError?.message ? `border-danger-strong` : `border-subtle`
"nodedc-modal-field relative my-1 flex min-h-[3.25rem] w-full items-center rounded-[1.25rem] pr-2",
estimatePointError?.message && "!bg-danger-500/10"
)}
>
<EstimateInputRoot
@ -191,7 +191,7 @@ export const EstimatePointCreate = observer(function EstimatePointCreate(props:
{estimateInputValue && estimateInputValue.length > 0 && (
<button
type="submit"
className="relative flex h-6 w-6 flex-shrink-0 cursor-pointer items-center justify-center rounded-xs text-success-primary transition-colors hover:bg-layer-1"
className="nodedc-settings-primary-button relative flex h-9 w-9 min-h-0 min-w-0 flex-shrink-0 cursor-pointer items-center justify-center rounded-full !px-0"
disabled={loader}
>
{loader ? <Spinner className="h-4 w-4" /> : <CheckIcon width={14} height={14} />}
@ -199,7 +199,7 @@ export const EstimatePointCreate = observer(function EstimatePointCreate(props:
)}
<button
type="button"
className="relative flex h-6 w-6 flex-shrink-0 cursor-pointer items-center justify-center rounded-xs transition-colors hover:bg-layer-1"
className="nodedc-settings-chip relative flex h-9 w-9 min-h-0 min-w-0 flex-shrink-0 cursor-pointer items-center justify-center rounded-full !px-0"
onClick={handleClose}
disabled={loader}
>

View File

@ -16,6 +16,7 @@ import { convertMinutesToHoursMinutesString } from "@plane/utils";
// plane web imports
import { EstimatePointDelete } from "@/plane-web/components/estimates";
// local imports
import { getLocalizedEstimatePointValue } from "../helpers";
import { EstimatePointUpdate } from "./update";
type TEstimatePointItemPreview = {
@ -62,26 +63,30 @@ export const EstimatePointItemPreview = observer(function EstimatePointItemPrevi
return (
<div>
{!estimatePointEditToggle && !estimatePointDeleteToggle && (
<div className="relative my-1 flex items-center gap-2 rounded-sm border border-subtle px-1 text-14">
<div className="relative flex h-6 w-6 flex-shrink-0 cursor-pointer items-center justify-center rounded-xs transition-colors hover:bg-layer-1">
<div className="nodedc-modal-field relative my-1 flex min-h-[3.25rem] items-center gap-2 rounded-[1.25rem] px-2 text-14">
<div className="nodedc-settings-chip relative flex h-9 w-9 min-h-0 min-w-0 flex-shrink-0 cursor-pointer items-center justify-center rounded-full !px-0">
<GripVertical size={14} className="text-secondary" />
</div>
<div ref={EstimatePointValueRef} className="w-full py-2 text-13">
<div ref={EstimatePointValueRef} className="w-full py-2 text-13 font-medium">
{estimatePoint?.value ? (
`${estimateType === EEstimateSystem.TIME ? convertMinutesToHoursMinutesString(Number(estimatePoint?.value)) : estimatePoint?.value}`
`${
estimateType === EEstimateSystem.TIME
? convertMinutesToHoursMinutesString(Number(estimatePoint?.value))
: getLocalizedEstimatePointValue(estimateType, estimatePoint?.value, t)
}`
) : (
<span className="text-placeholder">{t("project_settings.estimates.create.enter_estimate_point")}</span>
)}
</div>
<div
className="relative flex h-6 w-6 flex-shrink-0 cursor-pointer items-center justify-center rounded-xs transition-colors hover:bg-layer-1"
className="nodedc-settings-chip relative flex h-9 w-9 min-h-0 min-w-0 flex-shrink-0 cursor-pointer items-center justify-center rounded-full !px-0"
onClick={() => setEstimatePointEditToggle(true)}
>
<EditIcon width={14} height={14} className="text-secondary" />
</div>
{estimatePoints.length > estimateCount.min && (
<div
className="relative flex h-6 w-6 flex-shrink-0 cursor-pointer items-center justify-center rounded-xs transition-colors hover:bg-layer-1"
className="nodedc-settings-chip relative flex h-9 w-9 min-h-0 min-w-0 flex-shrink-0 cursor-pointer items-center justify-center rounded-full !px-0"
onClick={() =>
estimateId && estimatePointId
? setEstimatePointDeleteToggle(true)

View File

@ -175,8 +175,8 @@ export const EstimatePointUpdate = observer(function EstimatePointUpdate(props:
<form onSubmit={handleUpdate} className="relative flex items-center gap-2 pr-2.5 text-14">
<div
className={cn(
"relative my-1 flex w-full items-center rounded-sm border",
estimatePointError?.message ? `border-danger-strong` : `border-subtle`
"nodedc-modal-field relative my-1 flex min-h-[3.25rem] w-full items-center rounded-[1.25rem] pr-2",
estimatePointError?.message && "!bg-danger-500/10"
)}
>
<EstimateInputRoot
@ -205,7 +205,7 @@ export const EstimatePointUpdate = observer(function EstimatePointUpdate(props:
{estimateInputValue && estimateInputValue.length > 0 && (
<button
type="submit"
className="relative flex h-6 w-6 flex-shrink-0 cursor-pointer items-center justify-center rounded-xs text-success-primary transition-colors hover:bg-layer-1"
className="nodedc-settings-primary-button relative flex h-9 w-9 min-h-0 min-w-0 flex-shrink-0 cursor-pointer items-center justify-center rounded-full !px-0"
disabled={loader}
>
{loader ? <Spinner className="h-4 w-4" /> : <CheckIcon width={14} height={14} />}
@ -213,7 +213,7 @@ export const EstimatePointUpdate = observer(function EstimatePointUpdate(props:
)}
<button
type="button"
className="relative flex h-6 w-6 flex-shrink-0 cursor-pointer items-center justify-center rounded-xs transition-colors hover:bg-layer-1"
className="nodedc-settings-chip relative flex h-9 w-9 min-h-0 min-w-0 flex-shrink-0 cursor-pointer items-center justify-center rounded-full !px-0"
onClick={handleClose}
disabled={loader}
>

View File

@ -53,35 +53,43 @@ export function RadioInput({
return (
<div className={className}>
{inputLabel && <div className={cn(`mb-2`, inputLabelClassName)}>{inputLabel}</div>}
<div className={cn(`${wrapperClass}`, inputWrapperClassName)}>
<div className={cn(`${wrapperClass}`, inputWrapperClassName)} role="radiogroup" aria-label={aria}>
{options.map(({ value, label, disabled }, index) => (
<div
<button
key={index}
type="button"
onClick={() => !disabled && setSelected(value)}
className={cn(
"flex items-center gap-2 text-14",
disabled ? `cursor-not-allowed border-subtle bg-layer-1` : ``,
"flex items-center gap-2.5 text-14 transition-opacity",
disabled ? "cursor-not-allowed opacity-45" : "cursor-pointer",
inputFieldClassName
)}
role="radio"
aria-checked={selected === value}
aria-disabled={disabled}
disabled={disabled}
>
<input
id={`${name}_${index}`}
name={name}
className={cn(
`group flex size-5 flex-shrink-0 cursor-pointer items-center justify-center rounded-full border border-strong-1 bg-layer-2`,
selected === value ? `border-accent-strong bg-accent-primary/80` : ``,
disabled ? `cursor-not-allowed border-subtle bg-layer-1` : ``,
inputButtonClassName
)}
className="sr-only"
type="radio"
value={value}
disabled={disabled}
checked={selected === value}
onChange={() => !disabled && setSelected(value)}
/>
<label htmlFor={`${name}_${index}`} className="w-full cursor-pointer">
<span
className={cn(
"flex size-4 flex-shrink-0 items-center justify-center rounded-full border-0 transition-colors",
selected === value ? "bg-[rgb(var(--nodedc-accent-rgb))]" : "bg-white/14",
inputButtonClassName
)}
/>
<span className="w-full text-left">
{label}
</label>
</div>
</span>
</button>
))}
</div>
</div>

View File

@ -11,6 +11,7 @@ import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IState, TStateOperationsCallbacks } from "@plane/types";
// components
import { StateForm } from "@/components/project-states";
import { getLocalizedStateName } from "../helpers";
type TStateUpdate = {
state: IState;
@ -62,9 +63,14 @@ export const StateUpdate = observer(function StateUpdate(props: TStateUpdate) {
}
};
const localizedState = {
...state,
name: getLocalizedStateName(state.name, state.group, t),
};
return (
<StateForm
data={state}
data={localizedState}
onSubmit={onSubmit}
onCancel={onCancel}
buttonDisabled={loader}

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import type { TStateGroups } from "@plane/types";
const DEFAULT_STATE_KEYS_BY_GROUP: Record<TStateGroups, string> = {
backlog: "workspace_projects.state.backlog",
unstarted: "workspace_projects.state.unstarted",
started: "workspace_projects.state.started",
completed: "workspace_projects.state.completed",
cancelled: "workspace_projects.state.cancelled",
};
const DEFAULT_STATE_NAMES_BY_GROUP: Record<TStateGroups, string[]> = {
backlog: ["backlog"],
unstarted: ["todo", "to do", "unstarted"],
started: ["in progress", "in-progress", "started"],
completed: ["done", "completed"],
cancelled: ["cancelled", "canceled"],
};
export const getLocalizedStateName = (
name: string | undefined,
group: TStateGroups,
t: (key: string) => string
) => {
if (!name) return "";
const normalizedName = name.trim().toLowerCase();
if (DEFAULT_STATE_NAMES_BY_GROUP[group]?.includes(normalizedName)) {
return t(DEFAULT_STATE_KEYS_BY_GROUP[group]);
}
return name;
};

View File

@ -17,6 +17,7 @@ import { AlertModalCore } from "@plane/ui";
import { cn } from "@plane/utils";
// hooks
import { usePlatformOS } from "@/hooks/use-platform-os";
import { getLocalizedStateName } from "../helpers";
type TStateDelete = {
totalStates: number;
@ -35,6 +36,7 @@ export const StateDelete = observer(function StateDelete(props: TStateDelete) {
const [isDelete, setIsDelete] = useState(false);
// derived values
const isDeleteDisabled = state.default ? true : totalStates === 1 ? true : false;
const localizedStateName = getLocalizedStateName(state?.name, state.group, t);
const handleDeleteState = async () => {
if (isDeleteDisabled) return;
@ -73,7 +75,7 @@ export const StateDelete = observer(function StateDelete(props: TStateDelete) {
title={t("entity.delete.label", { entity: t("common.state") })}
content={t("entity.delete.confirmation", {
entity: t("common.state").toLowerCase(),
identifier: state?.name ? `"${state.name}"` : "",
identifier: localizedStateName ? `"${localizedStateName}"` : "",
})}
/>

View File

@ -7,6 +7,7 @@
import { useState } from "react";
import { observer } from "mobx-react";
// plane imports
import { useTranslation } from "@plane/i18n";
import type { TStateOperationsCallbacks } from "@plane/types";
import { cn } from "@plane/utils";
@ -18,6 +19,7 @@ type TStateMarksAsDefault = {
export const StateMarksAsDefault = observer(function StateMarksAsDefault(props: TStateMarksAsDefault) {
const { stateId, isDefault, markStateAsDefaultCallback } = props;
const { t } = useTranslation();
// states
const [isLoading, setIsLoading] = useState(false);
@ -43,7 +45,11 @@ export const StateMarksAsDefault = observer(function StateMarksAsDefault(props:
disabled={isDefault || isLoading}
onClick={handleMarkAsDefault}
>
{isLoading ? "Marking as default" : isDefault ? `Default` : `Mark as default`}
{isLoading
? t("project_settings.states.marking_as_default")
: isDefault
? t("project_settings.states.default_label")
: t("project_settings.states.mark_as_default")}
</button>
);
});

View File

@ -11,9 +11,11 @@ import { EIconSize, STATE_TRACKER_ELEMENTS } from "@plane/constants";
// plane imports
import { EditIcon, StateGroupIcon } from "@plane/propel/icons";
import type { IState, TStateOperationsCallbacks } from "@plane/types";
import { useTranslation } from "@plane/i18n";
// local imports
import { useProjectState } from "@/hooks/store/use-project-state";
import { StateDelete, StateMarksAsDefault } from "./options";
import { getLocalizedStateName } from "./helpers";
type TBaseStateItemTitleProps = {
stateCount: number;
@ -38,9 +40,11 @@ export const StateItemTitle = observer(function StateItemTitle(props: TStateItem
const { stateCount, setUpdateStateModal, disabled, state, shouldShowDescription = true } = props;
// store hooks
const { getStatePercentageInGroup } = useProjectState();
const { t } = useTranslation();
// derived values
const statePercentage = getStatePercentageInGroup(state.id);
const percentage = statePercentage ? statePercentage / 100 : undefined;
const stateTitle = getLocalizedStateName(state.name, state.group, t);
return (
<div className="flex w-full items-center justify-between gap-2">
@ -57,7 +61,7 @@ export const StateItemTitle = observer(function StateItemTitle(props: TStateItem
</div>
{/* state title and description */}
<div className="min-h-5 px-2 text-13">
<h6 className="text-13 font-medium">{state.name}</h6>
<h6 className="text-13 font-medium">{stateTitle}</h6>
{shouldShowDescription && <p className="text-11 text-secondary">{state.description}</p>}
</div>
</div>

View File

@ -1930,6 +1930,9 @@ export default {
heading: "States",
description: "Define and customize workflow states to track the progress of your work items.",
describe_this_state_for_your_members: "Describe this state for your members.",
default_label: "Default",
mark_as_default: "Mark as default",
marking_as_default: "Marking as default",
delete: {
blocked: "This state still contains work items. Move them to another state before deleting it.",
error: "State could not be deleted. Please try again.",
@ -1988,6 +1991,14 @@ export default {
enter_estimate_point: "Enter estimate",
step: "Step {step} of {total}",
label: "Create estimate",
add_points: "Add points",
add_categories: "Add categories",
add_time: "Add time",
custom_description: {
points: "Build your own point scale from scratch.",
categories: "Build your own category scale from scratch.",
time: "Build your own time scale from scratch.",
},
},
toasts: {
created: {
@ -2056,6 +2067,12 @@ export default {
hours: "Hours",
},
},
values: {
easy: "Easy",
medium: "Medium",
hard: "Hard",
very_hard: "Very hard",
},
},
automations: {
label: "Automations",

View File

@ -2089,6 +2089,9 @@ export default {
heading: "Статусы",
description: "Определяйте и настраивайте статусы рабочего процесса для отслеживания прогресса рабочих элементов.",
describe_this_state_for_your_members: "Опишите этот статус для участников",
default_label: "По умолчанию",
mark_as_default: "Сделать по умолчанию",
marking_as_default: "Назначение по умолчанию",
delete: {
blocked: "В этом статусе есть рабочие элементы. Переместите их в другой статус, чтобы удалить текущий.",
error: "Не удалось удалить статус. Попробуйте снова.",
@ -2147,6 +2150,14 @@ export default {
enter_estimate_point: "Ввести оценку",
step: "Шаг {step} из {total}",
label: "Создать оценку",
add_points: "Добавить баллы",
add_categories: "Добавить категории",
add_time: "Добавить время",
custom_description: {
points: "Создайте собственную шкалу баллов с нуля.",
categories: "Создайте собственную шкалу категорий с нуля.",
time: "Создайте собственную шкалу времени с нуля.",
},
},
toasts: {
created: {
@ -2216,6 +2227,12 @@ export default {
hours: "Часы",
},
},
values: {
easy: "Легко",
medium: "Средне",
hard: "Сложно",
very_hard: "Очень сложно",
},
},
automations: {
label: "Автоматизация",