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}
|
key={memberId}
|
||||||
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1"
|
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>{member?.display_name}</span>
|
||||||
<span
|
<span
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
|
@ -3,8 +3,7 @@ import React, { useState } from "react";
|
|||||||
// components
|
// components
|
||||||
import { FilterHeader, FilterOption } from "components/issues";
|
import { FilterHeader, FilterOption } from "components/issues";
|
||||||
// ui
|
// ui
|
||||||
import { Avatar } from "components/ui";
|
import { Loader, Avatar } from "@plane/ui";
|
||||||
import { Loader } from "@plane/ui";
|
|
||||||
// types
|
// types
|
||||||
import { IUserLite } from "types";
|
import { IUserLite } from "types";
|
||||||
|
|
||||||
@ -45,7 +44,7 @@ export const FilterMentions: React.FC<Props> = (props) => {
|
|||||||
key={`mentions-${member.id}`}
|
key={`mentions-${member.id}`}
|
||||||
isChecked={appliedFilters?.includes(member.id) ? true : false}
|
isChecked={appliedFilters?.includes(member.id) ? true : false}
|
||||||
onClick={() => handleUpdate(member.id)}
|
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}
|
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