forked from github/plane
fix: build fixes (#2591)
This commit is contained in:
parent
d511799f31
commit
4fcc4b4a01
@ -168,7 +168,7 @@ export const FiltersList: React.FC<Props> = ({ filters, setFilters, clearAllFilt
|
||||
key={memberId}
|
||||
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1"
|
||||
>
|
||||
<Avatar user={member} />
|
||||
<Avatar name={member?.display_name} src={member?.avatar} showTooltip={false} />
|
||||
<span>{member?.display_name}</span>
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
|
@ -3,8 +3,7 @@ import React, { useState } from "react";
|
||||
// components
|
||||
import { FilterHeader, FilterOption } from "components/issues";
|
||||
// ui
|
||||
import { Avatar } from "components/ui";
|
||||
import { Loader } from "@plane/ui";
|
||||
import { Loader, Avatar } from "@plane/ui";
|
||||
// types
|
||||
import { IUserLite } from "types";
|
||||
|
||||
@ -45,7 +44,7 @@ export const FilterMentions: React.FC<Props> = (props) => {
|
||||
key={`mentions-${member.id}`}
|
||||
isChecked={appliedFilters?.includes(member.id) ? true : false}
|
||||
onClick={() => handleUpdate(member.id)}
|
||||
icon={<Avatar user={member} height="18px" width="18px" />}
|
||||
icon={<Avatar name={member?.display_name} src={member?.avatar} showTooltip={false} />}
|
||||
title={member.display_name}
|
||||
/>
|
||||
))}
|
||||
@ -65,4 +64,4 @@ export const FilterMentions: React.FC<Props> = (props) => {
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
@ -1,278 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
// services
|
||||
import { ProjectStateService, ProjectService } from "services/project";
|
||||
import { IssueLabelService } from "services/issue";
|
||||
// ui
|
||||
import { Avatar, MultiLevelDropdown } from "components/ui";
|
||||
// icons
|
||||
import { PriorityIcon, StateGroupIcon } from "@plane/ui";
|
||||
// helpers
|
||||
import { getStatesList } from "helpers/state.helper";
|
||||
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
||||
// types
|
||||
import { IIssueFilterOptions } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys";
|
||||
// constants
|
||||
import { PRIORITIES } from "constants/project";
|
||||
import { DATE_FILTER_OPTIONS } from "constants/filters";
|
||||
|
||||
type Props = {
|
||||
filters: Partial<IIssueFilterOptions>;
|
||||
onSelect: (option: any) => void;
|
||||
direction?: "left" | "right";
|
||||
height?: "sm" | "md" | "rg" | "lg";
|
||||
};
|
||||
|
||||
const projectService = new ProjectService();
|
||||
const projectStateService = new ProjectStateService();
|
||||
const issueLabelService = new IssueLabelService();
|
||||
|
||||
export const SelectFilters: React.FC<Props> = ({ filters, onSelect, direction = "right", height = "md" }) => {
|
||||
const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false);
|
||||
const [dateFilterType, setDateFilterType] = useState<{
|
||||
title: string;
|
||||
type: "start_date" | "target_date";
|
||||
}>({
|
||||
title: "",
|
||||
type: "start_date",
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { data: states } = useSWR(
|
||||
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => projectStateService.getStates(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
const statesList = getStatesList(states);
|
||||
|
||||
const { data: members } = useSWR(
|
||||
projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => projectService.fetchProjectMembers(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: issueLabels } = useSWR(
|
||||
projectId ? PROJECT_ISSUE_LABELS(projectId.toString()) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => issueLabelService.getProjectIssueLabels(workspaceSlug as string, projectId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const projectFilterOption = [
|
||||
{
|
||||
id: "priority",
|
||||
label: "Priority",
|
||||
value: PRIORITIES,
|
||||
hasChildren: true,
|
||||
children: PRIORITIES.map((priority) => ({
|
||||
id: priority === null ? "null" : priority,
|
||||
label: (
|
||||
<div className="flex items-center gap-2 capitalize">
|
||||
<PriorityIcon priority={priority} />
|
||||
{priority ?? "None"}
|
||||
</div>
|
||||
),
|
||||
value: {
|
||||
key: "priority",
|
||||
value: priority === null ? "null" : priority,
|
||||
},
|
||||
selected: filters?.priority?.includes(priority === null ? "null" : priority),
|
||||
})),
|
||||
},
|
||||
{
|
||||
id: "state",
|
||||
label: "State",
|
||||
value: statesList,
|
||||
hasChildren: true,
|
||||
children: statesList?.map((state) => ({
|
||||
id: state.id,
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} />
|
||||
{state.name}
|
||||
</div>
|
||||
),
|
||||
value: {
|
||||
key: "state",
|
||||
value: state.id,
|
||||
},
|
||||
selected: filters?.state?.includes(state.id),
|
||||
})),
|
||||
},
|
||||
{
|
||||
id: "assignees",
|
||||
label: "Assignees",
|
||||
value: members,
|
||||
hasChildren: true,
|
||||
children: members?.map((member) => ({
|
||||
id: member.member.id,
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={member.member} />
|
||||
{member.member.display_name}
|
||||
</div>
|
||||
),
|
||||
value: {
|
||||
key: "assignees",
|
||||
value: member.member.id,
|
||||
},
|
||||
selected: filters?.assignees?.includes(member.member.id),
|
||||
})),
|
||||
},
|
||||
{
|
||||
id: "mentions",
|
||||
label: "Mentions",
|
||||
value: members,
|
||||
hasChildren: true,
|
||||
children: members?.map((member) => ({
|
||||
id: member.member.id,
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={member.member} />
|
||||
{member.member.display_name}
|
||||
</div>
|
||||
),
|
||||
value: {
|
||||
key: "mentions",
|
||||
value: member.member.id,
|
||||
},
|
||||
selected: filters?.mentions?.includes(member.member.id),
|
||||
})),
|
||||
},
|
||||
{
|
||||
id: "created_by",
|
||||
label: "Created by",
|
||||
value: members,
|
||||
hasChildren: true,
|
||||
children: members?.map((member) => ({
|
||||
id: member.member.id,
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={member.member} />
|
||||
{member.member.display_name}
|
||||
</div>
|
||||
),
|
||||
value: {
|
||||
key: "created_by",
|
||||
value: member.member.id,
|
||||
},
|
||||
selected: filters?.created_by?.includes(member.member.id),
|
||||
})),
|
||||
},
|
||||
{
|
||||
id: "labels",
|
||||
label: "Labels",
|
||||
value: issueLabels,
|
||||
hasChildren: true,
|
||||
children: issueLabels?.map((label) => ({
|
||||
id: label.id,
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="h-2 w-2 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label.color && label.color !== "" ? label.color : "#000000",
|
||||
}}
|
||||
/>
|
||||
{label.name}
|
||||
</div>
|
||||
),
|
||||
value: {
|
||||
key: "labels",
|
||||
value: label.id,
|
||||
},
|
||||
selected: filters?.labels?.includes(label.id),
|
||||
})),
|
||||
},
|
||||
{
|
||||
id: "start_date",
|
||||
label: "Start date",
|
||||
value: DATE_FILTER_OPTIONS,
|
||||
hasChildren: true,
|
||||
children: [
|
||||
...DATE_FILTER_OPTIONS.map((option) => ({
|
||||
id: option.name,
|
||||
label: option.name,
|
||||
value: {
|
||||
key: "start_date",
|
||||
value: option.value,
|
||||
},
|
||||
selected: checkIfArraysHaveSameElements(filters?.start_date ?? [], [option.value]),
|
||||
})),
|
||||
{
|
||||
id: "custom",
|
||||
label: "Custom",
|
||||
value: "custom",
|
||||
element: (
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDateFilterModalOpen(true);
|
||||
setDateFilterType({
|
||||
title: "Start date",
|
||||
type: "start_date",
|
||||
});
|
||||
}}
|
||||
className="w-full rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
|
||||
>
|
||||
Custom
|
||||
</button>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "target_date",
|
||||
label: "Due date",
|
||||
value: DATE_FILTER_OPTIONS,
|
||||
hasChildren: true,
|
||||
children: [
|
||||
...DATE_FILTER_OPTIONS.map((option) => ({
|
||||
id: option.name,
|
||||
label: option.name,
|
||||
value: {
|
||||
key: "target_date",
|
||||
value: option.value,
|
||||
},
|
||||
selected: checkIfArraysHaveSameElements(filters?.target_date ?? [], [option.value]),
|
||||
})),
|
||||
{
|
||||
id: "custom",
|
||||
label: "Custom",
|
||||
value: "custom",
|
||||
element: (
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDateFilterModalOpen(true);
|
||||
setDateFilterType({
|
||||
title: "Due date",
|
||||
type: "target_date",
|
||||
});
|
||||
}}
|
||||
className="w-full rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
|
||||
>
|
||||
Custom
|
||||
</button>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<MultiLevelDropdown
|
||||
label="Filters"
|
||||
onSelect={onSelect}
|
||||
direction={direction}
|
||||
height={height}
|
||||
options={projectFilterOption}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,112 +0,0 @@
|
||||
import React from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
// hooks
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
// components
|
||||
import { LiteTextEditorWithRef } from "@plane/lite-text-editor";
|
||||
import { Button } from "@plane/ui";
|
||||
// icons
|
||||
import { Send } from "lucide-react";
|
||||
// types
|
||||
import type { IIssueComment } from "types";
|
||||
// services
|
||||
import { FileService } from "services/file.service";
|
||||
import useEditorSuggestions from "hooks/use-editor-suggestions";
|
||||
|
||||
const defaultValues: Partial<IIssueComment> = {
|
||||
access: "INTERNAL",
|
||||
comment_html: "",
|
||||
};
|
||||
|
||||
type Props = {
|
||||
disabled?: boolean;
|
||||
onSubmit: (data: IIssueComment) => Promise<void>;
|
||||
};
|
||||
|
||||
type commentAccessType = {
|
||||
icon: string;
|
||||
key: string;
|
||||
label: "Private" | "Public";
|
||||
};
|
||||
|
||||
const commentAccess: commentAccessType[] = [
|
||||
{
|
||||
icon: "lock",
|
||||
key: "INTERNAL",
|
||||
label: "Private",
|
||||
},
|
||||
{
|
||||
icon: "public",
|
||||
key: "EXTERNAL",
|
||||
label: "Public",
|
||||
},
|
||||
];
|
||||
|
||||
const fileService = new FileService();
|
||||
|
||||
export const AddComment: React.FC<Props> = ({ disabled = false, onSubmit }) => {
|
||||
const editorRef = React.useRef<any>(null);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { projectDetails } = useProjectDetails();
|
||||
|
||||
const editorSuggestions = useEditorSuggestions(workspaceSlug as string | undefined, projectDetails?.id)
|
||||
|
||||
const showAccessSpecifier = projectDetails?.is_deployed || false;
|
||||
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
reset,
|
||||
} = useForm<IIssueComment>({ defaultValues });
|
||||
|
||||
const handleAddComment = async (formData: IIssueComment) => {
|
||||
if (!formData.comment_html || isSubmitting) return;
|
||||
|
||||
await onSubmit(formData).then(() => {
|
||||
reset(defaultValues);
|
||||
editorRef.current?.clearEditor();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="w-full flex gap-x-2" onSubmit={handleSubmit(handleAddComment)}>
|
||||
<div className="relative flex-grow">
|
||||
<Controller
|
||||
name="access"
|
||||
control={control}
|
||||
render={({ field: { onChange: onAccessChange, value: accessValue } }) => (
|
||||
<Controller
|
||||
name="comment_html"
|
||||
control={control}
|
||||
render={({ field: { onChange: onCommentChange, value: commentValue } }) => (
|
||||
<LiteTextEditorWithRef
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||
deleteFile={fileService.deleteImage}
|
||||
ref={editorRef}
|
||||
value={!commentValue || commentValue === "" ? "<p></p>" : commentValue}
|
||||
customClassName="p-3 min-h-[100px] shadow-sm"
|
||||
debouncedUpdatesEnabled={false}
|
||||
mentionSuggestions={editorSuggestions.mentionSuggestions}
|
||||
mentionHighlights={editorSuggestions.mentionHighlights}
|
||||
onChange={(comment_json: Object, comment_html: string) => onCommentChange(comment_html)}
|
||||
commentAccessSpecifier={{ accessValue, onAccessChange, showAccessSpecifier, commentAccess }}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="inline">
|
||||
<Button variant="primary" type="submit" disabled={isSubmitting || disabled}>
|
||||
<Send className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
@ -1,193 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
// react-hook-form
|
||||
import { useForm } from "react-hook-form";
|
||||
// icons
|
||||
import { Check, Globe2, Lock, MessageSquare, Pencil, Trash2, X } from "lucide-react";
|
||||
// service
|
||||
import { FileService } from "services/file.service";
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { CommentReaction } from "components/issues";
|
||||
import { LiteTextEditorWithRef, LiteReadOnlyEditorWithRef } from "@plane/lite-text-editor";
|
||||
|
||||
// helpers
|
||||
import { timeAgo } from "helpers/date-time.helper";
|
||||
// types
|
||||
import type { IIssueComment } from "types";
|
||||
import useEditorSuggestions from "hooks/use-editor-suggestions";
|
||||
|
||||
type Props = {
|
||||
comment: IIssueComment;
|
||||
handleCommentDeletion: (comment: string) => void;
|
||||
onSubmit: (commentId: string, data: Partial<IIssueComment>) => void;
|
||||
showAccessSpecifier?: boolean;
|
||||
workspaceSlug: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
// services
|
||||
const fileService = new FileService();
|
||||
|
||||
export const CommentCard: React.FC<Props> = (props) => {
|
||||
const { comment, handleCommentDeletion, onSubmit, showAccessSpecifier = false, workspaceSlug, disabled } = props;
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const editorSuggestions = useEditorSuggestions(workspaceSlug, comment.project_detail.id)
|
||||
|
||||
const editorRef = React.useRef<any>(null);
|
||||
const showEditorRef = React.useRef<any>(null);
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const {
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
setFocus,
|
||||
watch,
|
||||
setValue,
|
||||
} = useForm<IIssueComment>({
|
||||
defaultValues: comment,
|
||||
});
|
||||
|
||||
const onEnter = (formData: Partial<IIssueComment>) => {
|
||||
if (isSubmitting) return;
|
||||
setIsEditing(false);
|
||||
|
||||
onSubmit(comment.id, formData);
|
||||
|
||||
editorRef.current?.setEditorValue(formData.comment_html);
|
||||
showEditorRef.current?.setEditorValue(formData.comment_html);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
isEditing && setFocus("comment");
|
||||
}, [isEditing, setFocus]);
|
||||
|
||||
return (
|
||||
<div className="relative flex items-start space-x-3">
|
||||
<div className="relative px-1">
|
||||
{comment.actor_detail.avatar && comment.actor_detail.avatar !== "" ? (
|
||||
<img
|
||||
src={comment.actor_detail.avatar}
|
||||
alt={
|
||||
comment.actor_detail.is_bot ? comment.actor_detail.first_name + " Bot" : comment.actor_detail.display_name
|
||||
}
|
||||
height={30}
|
||||
width={30}
|
||||
className="grid h-7 w-7 place-items-center rounded-full border-2 border-custom-border-200"
|
||||
/>
|
||||
) : (
|
||||
<div className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white`}>
|
||||
{comment.actor_detail.is_bot
|
||||
? comment.actor_detail.first_name.charAt(0)
|
||||
: comment.actor_detail.display_name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span className="absolute -bottom-0.5 -right-1 rounded-tl bg-custom-background-80 px-0.5 py-px">
|
||||
<MessageSquare className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div>
|
||||
<div className="text-xs">
|
||||
{comment.actor_detail.is_bot ? comment.actor_detail.first_name + " Bot" : comment.actor_detail.display_name}
|
||||
</div>
|
||||
<p className="mt-0.5 text-xs text-custom-text-200">commented {timeAgo(comment.created_at)}</p>
|
||||
</div>
|
||||
<div className="issue-comments-section p-0">
|
||||
<form className={`flex-col gap-2 ${isEditing ? "flex" : "hidden"}`}>
|
||||
<div>
|
||||
<LiteTextEditorWithRef
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
||||
deleteFile={fileService.deleteImage}
|
||||
onEnterKeyPress={handleSubmit(onEnter)}
|
||||
ref={editorRef}
|
||||
value={watch("comment_html")}
|
||||
debouncedUpdatesEnabled={false}
|
||||
customClassName="min-h-[50px] p-3 shadow-sm"
|
||||
onChange={(comment_json: Object, comment_html: string) => {
|
||||
setValue("comment_json", comment_json);
|
||||
setValue("comment_html", comment_html);
|
||||
}}
|
||||
mentionSuggestions={editorSuggestions.mentionSuggestions}
|
||||
mentionHighlights={editorSuggestions.mentionHighlights}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-1 self-end">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting || disabled}
|
||||
className="group rounded border border-green-500 bg-green-500/20 p-2 shadow-md duration-300 hover:bg-green-500"
|
||||
>
|
||||
<Check className="h-3 w-3 text-green-500 duration-300 group-hover:text-white" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="group rounded border border-red-500 bg-red-500/20 p-2 shadow-md duration-300 hover:bg-red-500"
|
||||
onClick={() => setIsEditing(false)}
|
||||
>
|
||||
<X className="h-3 w-3 text-red-500 duration-300 group-hover:text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div className={`relative ${isEditing ? "hidden" : ""}`}>
|
||||
{showAccessSpecifier && (
|
||||
<div className="absolute top-1 right-1.5 z-[1] text-custom-text-300">
|
||||
{comment.access === "INTERNAL" ? <Lock className="h-3 w-3" /> : <Globe2 className="h-3 w-3" />}
|
||||
</div>
|
||||
)}
|
||||
<LiteReadOnlyEditorWithRef
|
||||
ref={showEditorRef}
|
||||
value={comment.comment_html}
|
||||
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
|
||||
/>
|
||||
<CommentReaction readonly={disabled} projectId={comment.project} commentId={comment.id} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{user?.id === comment.actor && !disabled && (
|
||||
<CustomMenu ellipsis>
|
||||
<CustomMenu.MenuItem onClick={() => setIsEditing(true)} className="flex items-center gap-1">
|
||||
<Pencil className="h-3 w-3" />
|
||||
Edit comment
|
||||
</CustomMenu.MenuItem>
|
||||
{showAccessSpecifier && (
|
||||
<>
|
||||
{comment.access === "INTERNAL" ? (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => onSubmit(comment.id, { access: "EXTERNAL" })}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Globe2 className="h-3 w-3" />
|
||||
Switch to public comment
|
||||
</CustomMenu.MenuItem>
|
||||
) : (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => onSubmit(comment.id, { access: "INTERNAL" })}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Lock className="h-3 w-3" />
|
||||
Switch to private comment
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
handleCommentDeletion(comment.id);
|
||||
}}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
Delete comment
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,158 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { Controller } from "react-hook-form";
|
||||
|
||||
// services
|
||||
import { FileService } from "services/file.service";
|
||||
// hooks
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import useReloadConfirmations from "hooks/use-reload-confirmation";
|
||||
// ui
|
||||
import { TextArea } from "@plane/ui";
|
||||
// components
|
||||
import { RichTextEditor } from "@plane/rich-text-editor";
|
||||
import { Label } from "components/web-view";
|
||||
// types
|
||||
import type { IIssue } from "types";
|
||||
import useEditorSuggestions from "hooks/use-editor-suggestions";
|
||||
|
||||
type Props = {
|
||||
isAllowed: boolean;
|
||||
issueDetails: IIssue;
|
||||
submitChanges: (data: Partial<IIssue>) => Promise<void>;
|
||||
register: any;
|
||||
control: any;
|
||||
watch: any;
|
||||
handleSubmit: any;
|
||||
};
|
||||
|
||||
// services
|
||||
const fileService = new FileService();
|
||||
|
||||
export const IssueWebViewForm: React.FC<Props> = (props) => {
|
||||
const { isAllowed, issueDetails, submitChanges, control, watch, handleSubmit } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const [characterLimit, setCharacterLimit] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
||||
|
||||
const { setShowAlert } = useReloadConfirmations();
|
||||
|
||||
const editorSuggestion = useEditorSuggestions(workspaceSlug as string | undefined, issueDetails.project_detail.id)
|
||||
|
||||
useEffect(() => {
|
||||
if (isSubmitting === "submitted") {
|
||||
setShowAlert(false);
|
||||
setTimeout(async () => {
|
||||
setIsSubmitting("saved");
|
||||
}, 2000);
|
||||
} else if (isSubmitting === "submitting") {
|
||||
setShowAlert(true);
|
||||
}
|
||||
}, [isSubmitting, setShowAlert]);
|
||||
|
||||
const debouncedTitleSave = useDebouncedCallback(async () => {
|
||||
setTimeout(async () => {
|
||||
handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted"));
|
||||
}, 500);
|
||||
}, 1000);
|
||||
|
||||
const handleDescriptionFormSubmit = useCallback(
|
||||
async (formData: Partial<IIssue>) => {
|
||||
if (!formData?.name || formData?.name.length === 0 || formData?.name.length > 255) return;
|
||||
|
||||
await submitChanges({
|
||||
name: formData.name ?? "",
|
||||
description_html: formData.description_html ?? "<p></p>",
|
||||
});
|
||||
},
|
||||
[submitChanges]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col">
|
||||
<Label>Title</Label>
|
||||
<div className="relative">
|
||||
{isAllowed ? (
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
render={({ field: { value } }) => (
|
||||
<TextArea
|
||||
id="name"
|
||||
name="name"
|
||||
value={value}
|
||||
placeholder="Enter issue name"
|
||||
onFocus={() => setCharacterLimit(true)}
|
||||
onChange={() => {
|
||||
setCharacterLimit(false);
|
||||
setIsSubmitting("submitting");
|
||||
debouncedTitleSave();
|
||||
}}
|
||||
required={true}
|
||||
className="min-h-10 block w-full resize-none overflow-hidden rounded border bg-transparent px-3 py-2 text-xl outline-none ring-0 focus:ring-1 focus:ring-custom-primary"
|
||||
role="textbox"
|
||||
disabled={!isAllowed}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<h4 className="break-words text-2xl font-semibold">{issueDetails?.name}</h4>
|
||||
)}
|
||||
{characterLimit && isAllowed && (
|
||||
<div className="pointer-events-none absolute bottom-1 right-1 z-[2] rounded bg-custom-background-100 text-custom-text-200 p-0.5 text-xs">
|
||||
<span className={`${watch("name").length === 0 || watch("name").length > 255 ? "text-red-500" : ""}`}>
|
||||
{watch("name").length}
|
||||
</span>
|
||||
/255
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Description</Label>
|
||||
<div className="relative">
|
||||
<Controller
|
||||
name="description_html"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => {
|
||||
if(value==null)return <></>;
|
||||
return <RichTextEditor
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||
deleteFile={fileService.deleteImage}
|
||||
value={
|
||||
!value || value === "" || (typeof value === "object" && Object.keys(value).length === 0)
|
||||
? "<p></p>"
|
||||
: value
|
||||
}
|
||||
debouncedUpdatesEnabled={true}
|
||||
setShouldShowAlert={setShowAlert}
|
||||
setIsSubmitting={setIsSubmitting}
|
||||
customClassName={isAllowed ? "min-h-[150px] shadow-sm" : "!p-0 !pt-2 text-custom-text-200"}
|
||||
noBorder={!isAllowed}
|
||||
onChange={(description: Object, description_html: string) => {
|
||||
setShowAlert(true);
|
||||
setIsSubmitting("submitting");
|
||||
onChange(description_html);
|
||||
handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted"));
|
||||
}}
|
||||
mentionSuggestions={editorSuggestion.mentionSuggestions}
|
||||
mentionHighlights={editorSuggestion.mentionHighlights}
|
||||
/>
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={`absolute right-5 bottom-5 text-xs text-custom-text-200 border border-custom-border-400 rounded-xl w-[6.5rem] py-1 z-10 flex items-center justify-center ${
|
||||
isSubmitting === "saved" ? "fadeOut" : "fadeIn"
|
||||
}`}
|
||||
>
|
||||
{isSubmitting === "submitting" ? "Saving..." : "Saved"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue
Block a user