[WEB-5127] chore: added readonly properties + fixed state dropdown + added additional props to issue modal (#7949)
* chore: added readonly properties + fixed state dropdown + added addditional props to issue modal * fix: refactor * fix: refactors * fix: build
This commit is contained in:
parent
2c17f8ad72
commit
70be4a4ace
13 changed files with 511 additions and 65 deletions
|
|
@ -28,7 +28,7 @@ export const StateDropdown: React.FC<TWorkItemStateDropdownProps> = observer((pr
|
|||
|
||||
// fetch states if not provided
|
||||
const onDropdownOpen = async () => {
|
||||
if (stateIds === undefined && workspaceSlug && projectId) {
|
||||
if ((stateIds === undefined || stateIds.length === 0) && workspaceSlug && projectId) {
|
||||
setStateLoader(true);
|
||||
await fetchProjectStates(workspaceSlug.toString(), projectId);
|
||||
setStateLoader(false);
|
||||
|
|
|
|||
|
|
@ -366,7 +366,6 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
|||
isDuplicateModalOpen: isDuplicateModalOpen,
|
||||
handleDuplicateIssueModal: handleDuplicateIssueModal,
|
||||
isProjectSelectionDisabled: isProjectSelectionDisabled,
|
||||
storeType: storeType,
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -67,7 +67,8 @@ export interface IssueFormProps {
|
|||
handleDuplicateIssueModal: (isOpen: boolean) => void;
|
||||
handleDraftAndClose?: () => void;
|
||||
isProjectSelectionDisabled?: boolean;
|
||||
storeType: EIssuesStoreType;
|
||||
showActionButtons?: boolean;
|
||||
dataResetProperties?: any[];
|
||||
}
|
||||
|
||||
export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
|
|
@ -93,7 +94,8 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||
handleDuplicateIssueModal,
|
||||
handleDraftAndClose,
|
||||
isProjectSelectionDisabled = false,
|
||||
storeType,
|
||||
showActionButtons = true,
|
||||
dataResetProperties = [],
|
||||
} = props;
|
||||
|
||||
// states
|
||||
|
|
@ -178,6 +180,14 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [projectId]);
|
||||
|
||||
// Reset form when data prop changes
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset({ ...DEFAULT_WORK_ITEM_FORM_VALUES, project_id: projectId, ...data });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [...dataResetProperties]);
|
||||
|
||||
// Update the issue type id when the project id changes
|
||||
useEffect(() => {
|
||||
const issueTypeId = watch("type_id");
|
||||
|
|
@ -377,7 +387,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||
disabled={!!data?.id || !!data?.sourceIssueId || isProjectSelectionDisabled}
|
||||
handleFormChange={handleFormChange}
|
||||
/>
|
||||
{projectId && storeType !== EIssuesStoreType.EPIC && (
|
||||
{projectId && (
|
||||
<IssueTypeSelect
|
||||
control={control}
|
||||
projectId={projectId}
|
||||
|
|
@ -477,7 +487,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||
activeAdditionalPropertiesLength > 0 && "shadow-custom-shadow-xs"
|
||||
)}
|
||||
>
|
||||
<div className="pb-3 border-b-[0.5px] border-custom-border-200">
|
||||
<div>
|
||||
<IssueDefaultProperties
|
||||
control={control}
|
||||
id={data?.id}
|
||||
|
|
@ -492,67 +502,69 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||
setSelectedParentIssue={setSelectedParentIssue}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-4 py-3" tabIndex={getIndex("create_more")}>
|
||||
{!data?.id && (
|
||||
<div
|
||||
className="inline-flex items-center gap-1.5 cursor-pointer"
|
||||
onClick={() => onCreateMoreToggleChange(!isCreateMoreToggleEnabled)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") onCreateMoreToggleChange(!isCreateMoreToggleEnabled);
|
||||
}}
|
||||
role="button"
|
||||
>
|
||||
<ToggleSwitch value={isCreateMoreToggleEnabled} onChange={() => {}} size="sm" />
|
||||
<span className="text-xs">{t("create_more")}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<div tabIndex={getIndex("discard_button")}>
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (editorRef.current?.isEditorReadyToDiscard()) {
|
||||
onClose();
|
||||
} else {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Editor is still processing changes. Please wait before proceeding.",
|
||||
});
|
||||
}
|
||||
{showActionButtons && (
|
||||
<div className="flex items-center justify-end gap-4 py-3" tabIndex={getIndex("create_more")}>
|
||||
{!data?.id && (
|
||||
<div
|
||||
className="inline-flex items-center gap-1.5 cursor-pointer"
|
||||
onClick={() => onCreateMoreToggleChange(!isCreateMoreToggleEnabled)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") onCreateMoreToggleChange(!isCreateMoreToggleEnabled);
|
||||
}}
|
||||
role="button"
|
||||
>
|
||||
{t("discard")}
|
||||
</Button>
|
||||
</div>
|
||||
<div tabIndex={isDraft ? getIndex("submit_button") : getIndex("draft_button")}>
|
||||
<Button
|
||||
variant={moveToIssue ? "neutral-primary" : "primary"}
|
||||
type="submit"
|
||||
size="sm"
|
||||
ref={submitBtnRef}
|
||||
loading={isSubmitting}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{isSubmitting ? primaryButtonText.loading : primaryButtonText.default}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{moveToIssue && (
|
||||
<Button
|
||||
variant="primary"
|
||||
type="button"
|
||||
size="sm"
|
||||
loading={isMoving}
|
||||
onClick={handleMoveToProjects}
|
||||
disabled={isMoving}
|
||||
>
|
||||
{t("add_to_project")}
|
||||
</Button>
|
||||
<ToggleSwitch value={isCreateMoreToggleEnabled} onChange={() => {}} size="sm" />
|
||||
<span className="text-xs">{t("create_more")}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<div tabIndex={getIndex("discard_button")}>
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (editorRef.current?.isEditorReadyToDiscard()) {
|
||||
onClose();
|
||||
} else {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Editor is still processing changes. Please wait before proceeding.",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("discard")}
|
||||
</Button>
|
||||
</div>
|
||||
<div tabIndex={isDraft ? getIndex("submit_button") : getIndex("draft_button")}>
|
||||
<Button
|
||||
variant={moveToIssue ? "neutral-primary" : "primary"}
|
||||
type="submit"
|
||||
size="sm"
|
||||
ref={submitBtnRef}
|
||||
loading={isSubmitting}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{isSubmitting ? primaryButtonText.loading : primaryButtonText.default}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{moveToIssue && (
|
||||
<Button
|
||||
variant="primary"
|
||||
type="button"
|
||||
size="sm"
|
||||
loading={isMoving}
|
||||
onClick={handleMoveToProjects}
|
||||
disabled={isMoving}
|
||||
>
|
||||
{t("add_to_project")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,9 +18,11 @@ type Props = {
|
|||
isMobile: boolean;
|
||||
isChangeInIdentifierRequired: boolean;
|
||||
setIsChangeInIdentifierRequired: (value: boolean) => void;
|
||||
handleFormOnChange?: () => void;
|
||||
};
|
||||
const ProjectCommonAttributes: React.FC<Props> = (props) => {
|
||||
const { setValue, isMobile, isChangeInIdentifierRequired, setIsChangeInIdentifierRequired } = props;
|
||||
const { setValue, isMobile, isChangeInIdentifierRequired, setIsChangeInIdentifierRequired, handleFormOnChange } =
|
||||
props;
|
||||
const {
|
||||
formState: { errors },
|
||||
control,
|
||||
|
|
@ -37,6 +39,7 @@ const ProjectCommonAttributes: React.FC<Props> = (props) => {
|
|||
if (e.target.value === "") setValue("identifier", "");
|
||||
else setValue("identifier", projectIdentifierSanitizer(e.target.value).substring(0, 5));
|
||||
onChange(e);
|
||||
handleFormOnChange?.();
|
||||
};
|
||||
|
||||
const handleIdentifierChange = (onChange: any) => (e: ChangeEvent<HTMLInputElement>) => {
|
||||
|
|
@ -44,6 +47,7 @@ const ProjectCommonAttributes: React.FC<Props> = (props) => {
|
|||
const alphanumericValue = projectIdentifierSanitizer(value);
|
||||
setIsChangeInIdentifierRequired(false);
|
||||
onChange(alphanumericValue);
|
||||
handleFormOnChange?.();
|
||||
};
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-x-2 gap-y-3 md:grid-cols-4">
|
||||
|
|
@ -128,7 +132,10 @@ const ProjectCommonAttributes: React.FC<Props> = (props) => {
|
|||
name="description"
|
||||
value={value}
|
||||
placeholder={t("description")}
|
||||
onChange={onChange}
|
||||
onChange={(e) => {
|
||||
onChange(e);
|
||||
handleFormOnChange?.();
|
||||
}}
|
||||
className="!h-24 text-sm focus:border-blue-400"
|
||||
hasError={Boolean(errors?.description)}
|
||||
tabIndex={getIndex("description")}
|
||||
|
|
|
|||
40
apps/web/core/components/readonly/cycle.tsx
Normal file
40
apps/web/core/components/readonly/cycle.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ContrastIcon } from "@plane/propel/icons";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useCycle } from "@/hooks/store/use-cycle";
|
||||
|
||||
export type TReadonlyCycleProps = {
|
||||
className?: string;
|
||||
hideIcon?: boolean;
|
||||
value: string | null;
|
||||
placeholder?: string;
|
||||
projectId: string | undefined;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const ReadonlyCycle: React.FC<TReadonlyCycleProps> = observer((props) => {
|
||||
const { className, hideIcon = false, value, placeholder, projectId, workspaceSlug } = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { getCycleNameById, fetchAllCycles } = useCycle();
|
||||
const cycleName = value ? getCycleNameById(value) : null;
|
||||
|
||||
useEffect(() => {
|
||||
if (projectId) {
|
||||
fetchAllCycles(workspaceSlug, projectId);
|
||||
}
|
||||
}, [projectId, workspaceSlug]);
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center gap-1 text-sm", className)}>
|
||||
{!hideIcon && <ContrastIcon className="size-4 flex-shrink-0" />}
|
||||
<span className="flex-grow truncate">{cycleName ?? placeholder ?? t("common.none")}</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
29
apps/web/core/components/readonly/date.tsx
Normal file
29
apps/web/core/components/readonly/date.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { Calendar } from "lucide-react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { cn, renderFormattedDate, getDate } from "@plane/utils";
|
||||
|
||||
export type TReadonlyDateProps = {
|
||||
className?: string;
|
||||
hideIcon?: boolean;
|
||||
value: Date | string | null;
|
||||
placeholder?: string;
|
||||
formatToken?: string;
|
||||
};
|
||||
|
||||
export const ReadonlyDate: React.FC<TReadonlyDateProps> = observer((props) => {
|
||||
const { className, hideIcon = false, value, placeholder, formatToken } = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const formattedDate = value ? renderFormattedDate(getDate(value), formatToken) : null;
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center gap-1 text-sm", className)}>
|
||||
{!hideIcon && <Calendar className="size-4 flex-shrink-0" />}
|
||||
<span className="flex-grow truncate">{formattedDate ?? placeholder ?? t("common.none")}</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
53
apps/web/core/components/readonly/estimate.tsx
Normal file
53
apps/web/core/components/readonly/estimate.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Triangle } from "lucide-react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { EEstimateSystem } from "@plane/types";
|
||||
import { cn, convertMinutesToHoursMinutesString } from "@plane/utils";
|
||||
// hooks
|
||||
import { useProjectEstimates } from "@/hooks/store/estimates";
|
||||
import { useEstimate } from "@/hooks/store/estimates/use-estimate";
|
||||
|
||||
export type TReadonlyEstimateProps = {
|
||||
className?: string;
|
||||
hideIcon?: boolean;
|
||||
value: string | undefined | null;
|
||||
placeholder?: string;
|
||||
projectId: string | undefined;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const ReadonlyEstimate: React.FC<TReadonlyEstimateProps> = observer((props) => {
|
||||
const { className, hideIcon = false, value, placeholder, projectId, workspaceSlug } = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { currentActiveEstimateIdByProjectId, getEstimateById, getProjectEstimates } = useProjectEstimates();
|
||||
|
||||
const currentActiveEstimateId = projectId ? currentActiveEstimateIdByProjectId(projectId) : undefined;
|
||||
const currentActiveEstimate = currentActiveEstimateId ? getEstimateById(currentActiveEstimateId) : undefined;
|
||||
const { estimatePointById } = useEstimate(currentActiveEstimateId);
|
||||
|
||||
const estimatePoint = value ? estimatePointById(value) : null;
|
||||
|
||||
const displayValue = estimatePoint
|
||||
? currentActiveEstimate?.type === EEstimateSystem.TIME
|
||||
? convertMinutesToHoursMinutesString(Number(estimatePoint.value))
|
||||
: estimatePoint.value
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
if (projectId) {
|
||||
getProjectEstimates(workspaceSlug, projectId);
|
||||
}
|
||||
}, [projectId, workspaceSlug]);
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center gap-1 text-sm", className)}>
|
||||
{!hideIcon && <Triangle className="size-4 flex-shrink-0" />}
|
||||
<span className="flex-grow truncate">{displayValue ?? placeholder ?? t("common.none")}</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
10
apps/web/core/components/readonly/index.tsx
Normal file
10
apps/web/core/components/readonly/index.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
// Readonly components for displaying single values instead of interactive dropdowns
|
||||
// These components handle their own data fetching using internal hooks
|
||||
export { ReadonlyState, type TReadonlyStateProps } from "./state";
|
||||
export { ReadonlyPriority, type TReadonlyPriorityProps } from "./priority";
|
||||
export { ReadonlyMember, type TReadonlyMemberProps } from "./member";
|
||||
export { ReadonlyLabels, type TReadonlyLabelsProps } from "./labels";
|
||||
export { ReadonlyCycle, type TReadonlyCycleProps } from "./cycle";
|
||||
export { ReadonlyDate, type TReadonlyDateProps } from "./date";
|
||||
export { ReadonlyEstimate, type TReadonlyEstimateProps } from "./estimate";
|
||||
export { ReadonlyModule, type TReadonlyModuleProps } from "./module";
|
||||
57
apps/web/core/components/readonly/labels.tsx
Normal file
57
apps/web/core/components/readonly/labels.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useLabel } from "@/hooks/store/use-label";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
|
||||
export type TReadonlyLabelsProps = {
|
||||
className?: string;
|
||||
hideIcon?: boolean;
|
||||
value: string[];
|
||||
placeholder?: string;
|
||||
projectId: string | undefined;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const ReadonlyLabels: React.FC<TReadonlyLabelsProps> = observer((props) => {
|
||||
const { className, value, projectId, workspaceSlug } = props;
|
||||
|
||||
const { getLabelById, fetchProjectLabels } = useLabel();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const labels = value
|
||||
.map((labelId) => getLabelById(labelId))
|
||||
.filter((label): label is NonNullable<typeof label> => Boolean(label));
|
||||
|
||||
useEffect(() => {
|
||||
if (projectId) {
|
||||
fetchProjectLabels(workspaceSlug?.toString(), projectId);
|
||||
}
|
||||
}, [projectId, workspaceSlug]);
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center gap-2 text-sm", className)}>
|
||||
{labels && (
|
||||
<>
|
||||
<Tooltip
|
||||
position="top"
|
||||
tooltipHeading="Labels"
|
||||
tooltipContent={labels.map((l) => l?.name).join(", ")}
|
||||
isMobile={isMobile}
|
||||
disabled={labels.length === 0}
|
||||
>
|
||||
<div className="h-full flex items-center gap-1 rounded py-1 text-sm">
|
||||
<span className="h-2 w-2 flex-shrink-0 rounded-full bg-custom-primary" />
|
||||
<span>{value.length}</span>
|
||||
<span>Labels</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
64
apps/web/core/components/readonly/member.tsx
Normal file
64
apps/web/core/components/readonly/member.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { LucideIcon } from "lucide-react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
||||
// hooks
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
|
||||
export type TReadonlyMemberProps = {
|
||||
className?: string;
|
||||
icon?: LucideIcon;
|
||||
hideIcon?: boolean;
|
||||
value: string | string[];
|
||||
placeholder?: string;
|
||||
multiple?: boolean;
|
||||
projectId?: string;
|
||||
};
|
||||
|
||||
export const ReadonlyMember: React.FC<TReadonlyMemberProps> = observer((props) => {
|
||||
const { className, icon: Icon, hideIcon = false, value, placeholder, multiple = false } = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { getUserDetails } = useMember();
|
||||
const memberIds = Array.isArray(value) ? value : value ? [value] : [];
|
||||
const members = memberIds.map((id) => getUserDetails(id)).filter(Boolean);
|
||||
|
||||
if (members.length === 0) {
|
||||
return (
|
||||
<div className={cn("flex items-center gap-1 text-sm", className)}>
|
||||
{<ButtonAvatars showTooltip={false} userIds={value} icon={Icon} />}
|
||||
<span className="flex-grow truncate">{placeholder ?? t("common.none")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (multiple) {
|
||||
return (
|
||||
<div className={cn("flex items-center gap-1 text-sm", className)}>
|
||||
{!hideIcon && Icon && <Icon className="h-3 w-3 flex-shrink-0" />}
|
||||
<ButtonAvatars showTooltip={false} userIds={memberIds} size="sm" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const member = members[0];
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center gap-1 text-sm", className)}>
|
||||
{!hideIcon && Icon && <Icon className="size-4 flex-shrink-0" />}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="size-4 rounded-full bg-custom-background-80 flex items-center justify-center">
|
||||
<span className="text-sm font-medium">
|
||||
{member?.display_name?.charAt(0) ?? member?.email?.charAt(0) ?? "?"}
|
||||
</span>
|
||||
</div>
|
||||
<span className="flex-grow truncate">{member?.display_name ?? member?.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
75
apps/web/core/components/readonly/module.tsx
Normal file
75
apps/web/core/components/readonly/module.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Layers } from "lucide-react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useModule } from "@/hooks/store/use-module";
|
||||
|
||||
export type TReadonlyModuleProps = {
|
||||
className?: string;
|
||||
hideIcon?: boolean;
|
||||
value: string | string[] | null;
|
||||
placeholder?: string;
|
||||
projectId: string | undefined;
|
||||
multiple?: boolean;
|
||||
showCount?: boolean;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const ReadonlyModule: React.FC<TReadonlyModuleProps> = observer((props) => {
|
||||
const {
|
||||
className,
|
||||
hideIcon = false,
|
||||
value,
|
||||
placeholder,
|
||||
multiple = false,
|
||||
showCount = true,
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { getModuleById, fetchModules } = useModule();
|
||||
|
||||
const moduleIds = Array.isArray(value) ? value : value ? [value] : [];
|
||||
const modules = moduleIds.map((id) => getModuleById(id)).filter(Boolean);
|
||||
|
||||
useEffect(() => {
|
||||
if (moduleIds.length > 0 && projectId) {
|
||||
fetchModules(workspaceSlug, projectId);
|
||||
}
|
||||
}, [value, projectId, workspaceSlug]);
|
||||
|
||||
if (modules.length === 0) {
|
||||
return (
|
||||
<div className={cn("flex items-center gap-1 text-sm", className)}>
|
||||
{!hideIcon && <Layers className="size-4 flex-shrink-0" />}
|
||||
<span className="flex-grow truncate">{placeholder ?? t("common.none")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (multiple) {
|
||||
const displayText =
|
||||
showCount && modules.length > 1 ? `${modules[0]?.name} +${modules.length - 1}` : modules[0]?.name;
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center gap-1 text-sm", className)}>
|
||||
{!hideIcon && <Layers className="size-4 flex-shrink-0" />}
|
||||
<span className="flex-grow truncate">{displayText}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const moduleItem = modules[0];
|
||||
return (
|
||||
<div className={cn("flex items-center gap-2 text-sm", className)}>
|
||||
{!hideIcon && <Layers className="size-4 flex-shrink-0" />}
|
||||
<span className="flex-grow truncate">{moduleItem?.name}</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
30
apps/web/core/components/readonly/priority.tsx
Normal file
30
apps/web/core/components/readonly/priority.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { ISSUE_PRIORITIES } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { PriorityIcon } from "@plane/propel/icons";
|
||||
import { TIssuePriorities } from "@plane/types";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
export type TReadonlyPriorityProps = {
|
||||
className?: string;
|
||||
hideIcon?: boolean;
|
||||
value: TIssuePriorities | undefined | null;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
export const ReadonlyPriority: React.FC<TReadonlyPriorityProps> = observer((props) => {
|
||||
const { className, hideIcon = false, value, placeholder } = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === value);
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center gap-1 text-sm", className)}>
|
||||
{!hideIcon && <PriorityIcon priority={value ?? "none"} size={12} className="flex-shrink-0" withContainer />}
|
||||
<span className="flex-grow truncate">{priorityDetails?.title ?? placeholder ?? t("common.none")}</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
70
apps/web/core/components/readonly/state.tsx
Normal file
70
apps/web/core/components/readonly/state.tsx
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { StateGroupIcon } from "@plane/propel/icons";
|
||||
import { Loader } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||
|
||||
export type TReadonlyStateProps = {
|
||||
className?: string;
|
||||
iconSize?: string;
|
||||
hideIcon?: boolean;
|
||||
value: string | undefined | null;
|
||||
placeholder?: string;
|
||||
projectId: string | undefined;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const ReadonlyState: React.FC<TReadonlyStateProps> = observer((props) => {
|
||||
const { className, iconSize = "size-4", hideIcon = false, value, placeholder, projectId, workspaceSlug } = props;
|
||||
// states
|
||||
const [stateLoader, setStateLoader] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const { getStateById, getProjectStateIds, fetchProjectStates } = useProjectState();
|
||||
// derived values
|
||||
const stateIds = getProjectStateIds(projectId);
|
||||
const state = getStateById(value);
|
||||
|
||||
// fetch states if not provided
|
||||
const fetchStates = async () => {
|
||||
if ((stateIds === undefined || stateIds.length === 0) && projectId) {
|
||||
setStateLoader(true);
|
||||
try {
|
||||
await fetchProjectStates(workspaceSlug, projectId);
|
||||
} finally {
|
||||
setStateLoader(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchStates();
|
||||
}, [projectId, workspaceSlug]);
|
||||
|
||||
if (stateLoader) {
|
||||
return (
|
||||
<Loader className={cn("flex items-center gap-1 text-sm", className)}>
|
||||
<Loader.Item height="16px" width="16px" className="rounded-full" />
|
||||
<Loader.Item height="16px" width="50px" />
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center gap-1 text-sm", className)}>
|
||||
{!hideIcon && (
|
||||
<StateGroupIcon
|
||||
stateGroup={state?.group ?? "backlog"}
|
||||
className={cn(iconSize, "flex-shrink-0")}
|
||||
color={state?.color}
|
||||
/>
|
||||
)}
|
||||
<span className="flex-grow truncate">{state?.name ?? placeholder ?? t("common.none")}</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue