UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: канонизация статусов и системы оценок в настройках проекта
This commit is contained in:
parent
b8f2654e80
commit
b7b0388dc1
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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} />}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -70,12 +72,12 @@ export const StateDelete = observer(function StateDelete(props: TStateDelete) {
|
|||
handleSubmit={handleDeleteState}
|
||||
isSubmitting={isDelete}
|
||||
isOpen={isDeleteModal}
|
||||
title={t("entity.delete.label", { entity: t("common.state") })}
|
||||
content={t("entity.delete.confirmation", {
|
||||
entity: t("common.state").toLowerCase(),
|
||||
identifier: state?.name ? `"${state.name}"` : "",
|
||||
})}
|
||||
/>
|
||||
title={t("entity.delete.label", { entity: t("common.state") })}
|
||||
content={t("entity.delete.confirmation", {
|
||||
entity: t("common.state").toLowerCase(),
|
||||
identifier: localizedStateName ? `"${localizedStateName}"` : "",
|
||||
})}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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: "Автоматизация",
|
||||
|
|
|
|||
Loading…
Reference in New Issue