Compare commits

..
Author SHA1 Message Date
Palanikannan1437 69034187be fix: error handling editor 2024-04-24 14:18:37 +05:30
39 changed files with 324 additions and 407 deletions
+2 -2
View File
@@ -282,7 +282,7 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
)
if serializer.is_valid():
if (
request.data.get("external_id")
str(request.data.get("external_id"))
and (issue.external_id != str(request.data.get("external_id")))
and Issue.objects.filter(
project_id=project_id,
@@ -437,7 +437,7 @@ class LabelAPIEndpoint(BaseAPIView):
serializer = LabelSerializer(label, data=request.data, partial=True)
if serializer.is_valid():
if (
request.data.get("external_id")
str(request.data.get("external_id"))
and (label.external_id != str(request.data.get("external_id")))
and Issue.objects.filter(
project_id=project_id,
@@ -56,6 +56,8 @@ export const useEditor = ({
mentionHandler,
placeholder,
}: CustomEditorProps) => {
const [error, setError] = useState<string | null>(null);
const editor = useCustomEditor({
editorProps: {
...CoreEditorProps(editorClassName),
@@ -86,6 +88,10 @@ export const useEditor = ({
setSavedSelection(editor.state.selection);
},
onUpdate: async ({ editor }) => {
const shouldError = Math.random() < 0.05; // 10% chance to generate an error
if (shouldError) {
setError("Randomly generated error for testing purposes.");
}
onChange?.(editor.getJSON(), getTrimmedHTML(editor.getHTML()));
},
onDestroy: async () => {
@@ -203,6 +209,11 @@ export const useEditor = ({
[editorRef, savedSelection, uploadFile]
);
if (error) {
console.log("threw error from core");
throw new Error(error);
}
if (!editor) {
return null;
}
@@ -49,7 +49,6 @@ const RichTextEditor = (props: IRichTextEditor) => {
containerClassName,
editorClassName = "",
forwardedRef,
// rerenderOnPropsChange,
id = "",
placeholder,
tabIndex,
@@ -58,8 +57,6 @@ const RichTextEditor = (props: IRichTextEditor) => {
const [hideDragHandleOnMouseLeave, setHideDragHandleOnMouseLeave] = React.useState<() => void>(() => {});
// this essentially sets the hideDragHandle function from the DragAndDrop extension as the Plugin
// loads such that we can invoke it from react when the cursor leaves the container
const setHideDragHandleFunction = (hideDragHandlerFromDragDrop: () => void) => {
setHideDragHandleOnMouseLeave(() => hideDragHandlerFromDragDrop);
};
@@ -75,7 +72,6 @@ const RichTextEditor = (props: IRichTextEditor) => {
initialValue,
value,
forwardedRef,
// rerenderOnPropsChange,
extensions: RichTextEditorExtensions({
uploadFile: fileHandler.upload,
dragDropEnabled,
+3 -1
View File
@@ -27,7 +27,9 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
: mode === "true-transparent"
? "rounded border-none bg-transparent ring-0"
: ""
} ${hasError ? "border-red-500" : ""} ${inputSize === "sm" ? "px-3 py-2" : inputSize === "md" ? "p-3" : ""}`,
} ${hasError ? "border-red-500" : ""} ${hasError && mode === "primary" ? "bg-red-500/20" : ""} ${
inputSize === "sm" ? "px-3 py-2" : inputSize === "md" ? "p-3" : ""
}`,
className
)}
{...rest}
+37 -50
View File
@@ -46,10 +46,10 @@ export const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
}`}`}
target={activity.issue === null ? "_self" : "_blank"}
rel={activity.issue === null ? "" : "noopener noreferrer"}
className="inline items-center gap-1 font-medium text-custom-text-100 hover:underline"
className="font-medium text-custom-text-100 hover:underline"
>
<span className="whitespace-nowrap">{`${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}`}</span>{" "}
<span className="font-normal break-all">{activity.issue_detail?.name}</span>
<span className="font-normal">{activity.issue_detail?.name}</span>
</a>
) : (
<span className="inline-flex items-center gap-1 font-medium text-custom-text-100 whitespace-nowrap">
@@ -105,7 +105,7 @@ const EstimatePoint = observer((props: { point: string }) => {
const estimateValue = getEstimatePointValue(Number(point), null);
return (
<span className="font-medium text-custom-text-100 whitespace-nowrap">
<span className="font-medium text-custom-text-100">
{areEstimatesEnabledForCurrentProject
? estimateValue
: `${currentPoint} ${currentPoint > 1 ? "points" : "point"}`}
@@ -300,13 +300,11 @@ const activityDetails: {
message: (activity, showIssue, workspaceSlug) => {
if (activity.old_value === "")
return (
<span className="overflow-hidden">
<>
added a new label{" "}
<span className="inline-flex items-center gap-2 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
<span className="inline-flex w-min items-center gap-2 truncate whitespace-nowrap rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
<LabelPill labelId={activity.new_identifier ?? ""} workspaceSlug={workspaceSlug} />
<span className="flex-shrink font-medium text-custom-text-100 break-all line-clamp-1">
{activity.new_value}
</span>
<span className="flex-shrink truncate font-medium text-custom-text-100">{activity.new_value}</span>
</span>
{showIssue && (
<span className="">
@@ -314,17 +312,15 @@ const activityDetails: {
to <IssueLink activity={activity} />
</span>
)}
</span>
</>
);
else
return (
<>
removed the label{" "}
<span className="inline-flex items-center gap-2 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
<span className="inline-flex w-min items-center gap-2 truncate whitespace-nowrap rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
<LabelPill labelId={activity.old_identifier ?? ""} workspaceSlug={workspaceSlug} />
<span className="flex-shrink font-medium text-custom-text-100 break-all line-clamp-1">
{activity.old_value}
</span>
<span className="flex-shrink truncate font-medium text-custom-text-100">{activity.old_value}</span>
</span>
{showIssue && (
<span>
@@ -408,30 +404,29 @@ const activityDetails: {
return (
<>
<span className="flex-shrink-0">
added {showIssue ? <IssueLink activity={activity} /> : "this issue"}{" "}
<span className="whitespace-nowrap">to the cycle</span>{" "}
added {showIssue ? <IssueLink activity={activity} /> : "this issue"} to the cycle{" "}
</span>
<a
href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.new_identifier}`}
target="_blank"
rel="noopener noreferrer"
className="inline items-center gap-1 font-medium text-custom-text-100 hover:underline"
className="inline-flex items-center gap-1 truncate font-medium text-custom-text-100 hover:underline"
>
<span className="break-all">{activity.new_value}</span>
<span className="truncate">{activity.new_value}</span>
</a>
</>
);
else if (activity.verb === "updated")
return (
<>
<span className="flex-shrink-0 whitespace-nowrap">set the cycle to </span>
<span className="flex-shrink-0">set the cycle to </span>
<a
href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.new_identifier}`}
target="_blank"
rel="noopener noreferrer"
className="inline items-center gap-1 font-medium text-custom-text-100 hover:underline"
className="inline-flex items-center gap-1 truncate font-medium text-custom-text-100 hover:underline"
>
<span className="break-all">{activity.new_value}</span>
<span className="truncate">{activity.new_value}</span>
</a>
</>
);
@@ -443,9 +438,9 @@ const activityDetails: {
href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.old_identifier}`}
target="_blank"
rel="noopener noreferrer"
className="inline items-center gap-1 font-medium text-custom-text-100 hover:underline"
className="inline-flex items-center gap-1 truncate font-medium text-custom-text-100 hover:underline"
>
<span className="break-all">{activity.old_value}</span>
<span className="truncate">{activity.old_value}</span>
</a>
</>
);
@@ -462,9 +457,9 @@ const activityDetails: {
href={`/${workspaceSlug}/projects/${activity.project}/modules/${activity.new_identifier}`}
target="_blank"
rel="noopener noreferrer"
className="inline items-center gap-1 font-medium text-custom-text-100 hover:underline"
className="inline-flex items-center gap-1 truncate font-medium text-custom-text-100 hover:underline"
>
<span className="break-all">{activity.new_value}</span>
<span className="truncate">{activity.new_value}</span>
</a>
</>
);
@@ -476,9 +471,9 @@ const activityDetails: {
href={`/${workspaceSlug}/projects/${activity.project}/modules/${activity.new_identifier}`}
target="_blank"
rel="noopener noreferrer"
className="inline items-center gap-1 font-medium text-custom-text-100 hover:underline"
className="inline-flex items-center gap-1 truncate font-medium text-custom-text-100 hover:underline"
>
<span className="break-all">{activity.new_value}</span>
<span className="truncate">{activity.new_value}</span>
</a>
</>
);
@@ -490,9 +485,9 @@ const activityDetails: {
href={`/${workspaceSlug}/projects/${activity.project}/modules/${activity.old_identifier}`}
target="_blank"
rel="noopener noreferrer"
className="inline items-center gap-1 font-medium text-custom-text-100 hover:underline"
className="inline-flex items-center gap-1 truncate font-medium text-custom-text-100 hover:underline"
>
<span className="break-all">{activity.old_value}</span>
<span className="truncate">{activity.old_value}</span>
</a>
</>
);
@@ -502,7 +497,7 @@ const activityDetails: {
name: {
message: (activity, showIssue) => (
<>
set the name to <span className="break-all">{activity.new_value}</span>
<span className="truncate">set the name to {activity.new_value}</span>
{showIssue && (
<>
{" "}
@@ -518,8 +513,7 @@ const activityDetails: {
if (!activity.new_value)
return (
<>
removed the parent{" "}
<span className="font-medium text-custom-text-100 whitespace-nowrap">{activity.old_value}</span>
removed the parent <span className="font-medium text-custom-text-100">{activity.old_value}</span>
{showIssue && (
<>
{" "}
@@ -531,8 +525,7 @@ const activityDetails: {
else
return (
<>
set the parent to{" "}
<span className="font-medium text-custom-text-100 whitespace-nowrap">{activity.new_value}</span>
set the parent to <span className="font-medium text-custom-text-100">{activity.new_value}</span>
{showIssue && (
<>
{" "}
@@ -567,14 +560,13 @@ const activityDetails: {
return (
<>
marked that {showIssue ? <IssueLink activity={activity} /> : "this issue"} relates to{" "}
<span className="font-medium text-custom-text-100 whitespace-nowrap">{activity.new_value}</span>.
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
</>
);
else
return (
<>
removed the relation from{" "}
<span className="font-medium text-custom-text-100 whitespace-nowrap">{activity.old_value}</span>.
removed the relation from <span className="font-medium text-custom-text-100">{activity.old_value}</span>.
</>
);
},
@@ -586,14 +578,13 @@ const activityDetails: {
return (
<>
marked {showIssue ? <IssueLink activity={activity} /> : "this issue"} is blocking issue{" "}
<span className="font-medium text-custom-text-100 whitespace-nowrap">{activity.new_value}</span>.
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
</>
);
else
return (
<>
removed the blocking issue{" "}
<span className="font-medium text-custom-text-100 whitespace-nowrap">{activity.old_value}</span>.
removed the blocking issue <span className="font-medium text-custom-text-100">{activity.old_value}</span>.
</>
);
},
@@ -605,14 +596,14 @@ const activityDetails: {
return (
<>
marked {showIssue ? <IssueLink activity={activity} /> : "this issue"} is being blocked by{" "}
<span className="font-medium text-custom-text-100 whitespace-nowrap">{activity.new_value}</span>.
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
</>
);
else
return (
<>
removed {showIssue ? <IssueLink activity={activity} /> : "this issue"} being blocked by issue{" "}
<span className="font-medium text-custom-text-100 whitespace-nowrap">{activity.old_value}</span>.
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
</>
);
},
@@ -624,14 +615,14 @@ const activityDetails: {
return (
<>
marked {showIssue ? <IssueLink activity={activity} /> : "this issue"} as duplicate of{" "}
<span className="font-medium text-custom-text-100 whitespace-nowrap">{activity.new_value}</span>.
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
</>
);
else
return (
<>
removed {showIssue ? <IssueLink activity={activity} /> : "this issue"} as a duplicate of{" "}
<span className="font-medium text-custom-text-100 whitespace-nowrap">{activity.old_value}</span>.
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
</>
);
},
@@ -640,7 +631,7 @@ const activityDetails: {
state: {
message: (activity, showIssue) => (
<>
set the state to <span className="font-medium text-custom-text-100 break-all">{activity.new_value}</span>
set the state to <span className="font-medium text-custom-text-100">{activity.new_value}</span>
{showIssue && (
<>
{" "}
@@ -669,9 +660,7 @@ const activityDetails: {
return (
<>
set the start date to{" "}
<span className="font-medium text-custom-text-100 whitespace-nowrap">
{renderFormattedDate(activity.new_value)}
</span>
<span className="font-medium text-custom-text-100">{renderFormattedDate(activity.new_value)}</span>
{showIssue && (
<>
{" "}
@@ -701,9 +690,7 @@ const activityDetails: {
return (
<>
set the due date to{" "}
<span className="font-medium text-custom-text-100 whitespace-nowrap">
{renderFormattedDate(activity.new_value)}
</span>
<span className="font-medium text-custom-text-100">{renderFormattedDate(activity.new_value)}</span>
{showIssue && (
<>
<IssueLink activity={activity} />
@@ -1,5 +1,7 @@
import React from "react";
import { CircularProgressIndicator } from "@plane/ui";
type TSingleProgressStatsProps = {
title: any;
completed: number;
@@ -24,6 +26,9 @@ export const SingleProgressStats: React.FC<TSingleProgressStatsProps> = ({
<div className="w-1/2">{title}</div>
<div className="flex w-1/2 items-center justify-end gap-1 px-2">
<div className="flex h-5 items-center justify-center gap-1">
<span className="h-4 w-4">
<CircularProgressIndicator percentage={(completed / total) * 100} size={14} strokeWidth={2} />
</span>
<span className="w-8 text-right">
{isNaN(Math.round((completed / total) * 100)) ? "0" : Math.round((completed / total) * 100)}%
</span>
+2 -3
View File
@@ -77,7 +77,7 @@ export const CycleForm: React.FC<Props> = (props) => {
</div>
<div className="space-y-3">
<div className="mt-2 space-y-3">
<div className="flex flex-col gap-1">
<div>
<Controller
name="name"
control={control}
@@ -85,7 +85,7 @@ export const CycleForm: React.FC<Props> = (props) => {
required: "Name is required",
maxLength: {
value: 255,
message: "Title should be less than 255 characters",
message: "Name should be less than 255 characters",
},
}}
render={({ field: { value, onChange } }) => (
@@ -103,7 +103,6 @@ export const CycleForm: React.FC<Props> = (props) => {
/>
)}
/>
<span className="text-xs text-red-500">{errors?.name?.message}</span>
</div>
<div>
<Controller
@@ -42,7 +42,7 @@ export const DashboardWidgets = observer(() => {
if (!workspaceSlug || !homeDashboardId) return null;
return (
<div className="relative flex flex-col lg:grid lg:grid-cols-2 gap-7">
<div className="grid lg:grid-cols-2 gap-7">
{Object.entries(WIDGETS_LIST).map(([key, widget]) => {
const WidgetComponent = widget.component;
// if the widget doesn't exist, return null
@@ -68,8 +68,8 @@ export const RecentActivityWidget: React.FC<WidgetProps> = observer((props) => {
</div>
)}
</div>
<div className="-mt-2 break-words">
<p className="inline text-sm text-custom-text-200">
<div className="-mt-1 break-words">
<p className="text-sm text-custom-text-200">
<span className="font-medium text-custom-text-100">
{currentUser?.id === activity.actor_detail.id ? "You" : activity.actor_detail?.display_name}{" "}
</span>
@@ -81,9 +81,7 @@ export const RecentActivityWidget: React.FC<WidgetProps> = observer((props) => {
</span>
)}
</p>
<p className="text-xs text-custom-text-200 whitespace-nowrap">
{calculateTimeAgo(activity.created_at)}
</p>
<p className="text-xs text-custom-text-200">{calculateTimeAgo(activity.created_at)}</p>
</div>
</div>
))}
@@ -1,4 +1,4 @@
import React, { forwardRef } from "react";
import { forwardRef } from "react";
// editor
import { EditorRefApi, IRichTextEditor, RichTextEditorWithRef } from "@plane/rich-text-editor";
// types
@@ -9,6 +9,7 @@ import { cn } from "@/helpers/common.helper";
import { useMember, useMention, useUser } from "@/hooks/store";
// services
import { FileService } from "@/services/file.service";
import ErrorBoundaryWithFallback from "./rich-text-fallback";
interface RichTextEditorWrapperProps extends Omit<IRichTextEditor, "fileHandler" | "mentionHandler"> {
workspaceSlug: string;
@@ -20,6 +21,7 @@ const fileService = new FileService();
export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProps>((props, ref) => {
const { containerClassName, workspaceSlug, workspaceId, projectId, ...rest } = props;
console.log("eeeeeeee", rest.value);
// store hooks
const { currentUser } = useUser();
const {
@@ -38,21 +40,23 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
});
return (
<RichTextEditorWithRef
ref={ref}
fileHandler={{
upload: fileService.getUploadFileFunction(workspaceSlug),
delete: fileService.getDeleteImageFunction(workspaceId),
restore: fileService.getRestoreImageFunction(workspaceId),
cancel: fileService.cancelUpload,
}}
mentionHandler={{
highlights: mentionHighlights,
suggestions: mentionSuggestions,
}}
{...rest}
containerClassName={cn(containerClassName, "relative min-h-[150px] border border-custom-border-200 p-3")}
/>
<ErrorBoundaryWithFallback>
<RichTextEditorWithRef
ref={ref}
fileHandler={{
upload: fileService.getUploadFileFunction(workspaceSlug),
delete: fileService.getDeleteImageFunction(workspaceId),
restore: fileService.getRestoreImageFunction(workspaceId),
cancel: fileService.cancelUpload,
}}
mentionHandler={{
highlights: mentionHighlights,
suggestions: mentionSuggestions,
}}
{...rest}
containerClassName={cn(containerClassName, "relative min-h-[150px] border border-custom-border-200 p-3")}
/>
</ErrorBoundaryWithFallback>
);
});
@@ -0,0 +1,41 @@
import React, { useEffect } from "react";
import { ErrorBoundary } from "react-error-boundary";
interface FallbackProps {
error: Error;
resetErrorBoundary: () => void;
}
const Fallback: React.FC<FallbackProps> = ({ error, resetErrorBoundary }) => {
useEffect(() => {
const timer = setTimeout(() => {
resetErrorBoundary();
}, 1000);
return () => clearTimeout(timer);
}, [resetErrorBoundary]);
return (
<div role="alert">
<p>Something went wrong:</p>
<pre style={{ color: "red" }}>{error.message}</pre>
</div>
);
};
interface ErrorBoundaryWithFallbackProps {
children: React.ReactNode;
}
const ErrorBoundaryWithFallback: React.FC<ErrorBoundaryWithFallbackProps> = ({ children }) => {
return (
<ErrorBoundary
FallbackComponent={Fallback}
onError={(error, errorInfo) => console.log("Error occurred:", error, errorInfo)}
>
{children}
</ErrorBoundary>
);
};
export default ErrorBoundaryWithFallback;
+5 -8
View File
@@ -51,19 +51,16 @@ export const ProjectsHeader = observer(() => {
const handleFilters = useCallback(
(key: keyof TProjectFilters, value: string | string[]) => {
if (!workspaceSlug) return;
let newValues = filters?.[key] ?? [];
if (Array.isArray(value)) {
if (key === "created_at" && newValues.find((v) => v.includes("custom"))) newValues = [];
const newValues = filters?.[key] ?? [];
if (Array.isArray(value))
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
else newValues.splice(newValues.indexOf(val), 1);
});
} else {
else {
if (filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else {
if (key === "created_at") newValues = [value];
else newValues.push(value);
}
else newValues.push(value);
}
updateFilters(workspaceSlug, { [key]: newValues });
@@ -6,7 +6,7 @@ import { Avatar, Loader } from "@plane/ui";
// components
import { FilterHeader, FilterOption } from "@/components/issues";
// hooks
import { useMember, useProjectInbox, useUser } from "@/hooks/store";
import { useMember, useProjectInbox } from "@/hooks/store";
type Props = {
filterKey: TInboxIssueFilterMemberKeys;
@@ -20,7 +20,6 @@ export const FilterMember: FC<Props> = observer((props: Props) => {
// hooks
const { inboxFilters, handleInboxIssueFilters } = useProjectInbox();
const { getUserDetails } = useMember();
const { currentUser } = useUser();
// states
const [itemsToRender, setItemsToRender] = useState(5);
const [previewEnabled, setPreviewEnabled] = useState(true);
@@ -35,7 +34,6 @@ export const FilterMember: FC<Props> = observer((props: Props) => {
return sortBy(filteredOptions, [
(memberId) => !filterValue.includes(memberId),
(memberId) => memberId !== currentUser?.id,
(memberId) => getUserDetails(memberId)?.display_name.toLowerCase(),
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -73,7 +71,7 @@ export const FilterMember: FC<Props> = observer((props: Props) => {
isChecked={filterValue?.includes(member.id) ? true : false}
onClick={() => handleInboxIssueFilters(filterKey, handleFilterValue(member.id))}
icon={<Avatar name={member.display_name} src={member.avatar} showTooltip={false} size="md" />}
title={currentUser?.id === member.id ? "You" : member?.display_name}
title={member.display_name}
/>
);
})}
@@ -116,16 +116,10 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
setFormSubmitting(false);
};
const isTitleLengthMoreThan255Character = formData?.name ? formData.name.length > 255 : false;
if (!workspaceSlug || !projectId || !workspaceId) return <></>;
return (
<form className="relative space-y-4" onSubmit={handleFormSubmit}>
<InboxIssueTitle
data={formData}
handleData={handleFormData}
isTitleLengthMoreThan255Character={isTitleLengthMoreThan255Character}
/>
<InboxIssueTitle data={formData} handleData={handleFormData} />
<InboxIssueDescription
workspaceSlug={workspaceSlug}
projectId={projectId}
@@ -144,13 +138,7 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
<Button variant="neutral-primary" size="sm" type="button" onClick={handleModalClose}>
Discard
</Button>
<Button
variant="primary"
size="sm"
type="submit"
loading={formSubmitting}
disabled={isTitleLengthMoreThan255Character}
>
<Button variant="primary" size="sm" type="submit" loading={formSubmitting}>
{formSubmitting ? "Adding Issue..." : "Add Issue"}
</Button>
</div>
@@ -121,16 +121,10 @@ export const InboxIssueEditRoot: FC<TInboxIssueEditRoot> = observer((props) => {
setFormSubmitting(false);
};
const isTitleLengthMoreThan255Character = formData?.name ? formData.name.length > 255 : false;
if (!workspaceSlug || !projectId || !workspaceId || !formData) return <></>;
return (
<div className="relative space-y-4">
<InboxIssueTitle
data={formData}
handleData={handleFormData}
isTitleLengthMoreThan255Character={isTitleLengthMoreThan255Character}
/>
<InboxIssueTitle data={formData} handleData={handleFormData} />
<InboxIssueDescription
workspaceSlug={workspaceSlug}
projectId={projectId}
@@ -144,14 +138,7 @@ export const InboxIssueEditRoot: FC<TInboxIssueEditRoot> = observer((props) => {
<Button variant="neutral-primary" size="sm" type="button" onClick={handleModalClose}>
Cancel
</Button>
<Button
variant="primary"
size="sm"
type="button"
loading={formSubmitting}
disabled={isTitleLengthMoreThan255Character}
onClick={handleFormSubmit}
>
<Button variant="primary" size="sm" type="button" loading={formSubmitting} onClick={handleFormSubmit}>
{formSubmitting ? "Adding..." : "Add to project"}
</Button>
</div>
@@ -6,11 +6,10 @@ import { Input } from "@plane/ui";
type TInboxIssueTitle = {
data: Partial<TIssue>;
handleData: (issueKey: keyof Partial<TIssue>, issueValue: Partial<TIssue>[keyof Partial<TIssue>]) => void;
isTitleLengthMoreThan255Character?: boolean;
};
export const InboxIssueTitle: FC<TInboxIssueTitle> = observer((props) => {
const { data, handleData, isTitleLengthMoreThan255Character } = props;
const { data, handleData } = props;
return (
<div className="relative flex flex-wrap gap-2 items-center">
@@ -22,11 +21,9 @@ export const InboxIssueTitle: FC<TInboxIssueTitle> = observer((props) => {
onChange={(e) => handleData("name", e.target.value)}
placeholder="Title"
className="w-full resize-none text-xl"
maxLength={255}
required
/>
{isTitleLengthMoreThan255Character && (
<span className="text-xs text-red-500">Title should be less than 255 characters</span>
)}
</div>
);
});
+7 -22
View File
@@ -13,7 +13,6 @@ import {
CopyPlus,
CalendarClock,
CalendarCheck2,
UserCircle2,
} from "lucide-react";
// hooks
// components
@@ -37,7 +36,6 @@ import {
} from "@/components/dropdowns";
// ui
// helpers
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
import {
DeleteIssueModal,
IssueLinkRoot,
@@ -56,7 +54,7 @@ import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
import { copyTextToClipboard } from "@/helpers/string.helper";
// types
import { useEstimate, useIssueDetail, useMember, useProject, useProjectState, useUser } from "@/hooks/store";
import { useEstimate, useIssueDetail, useProject, useProjectState, useUser } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
// components
import type { TIssueOperations } from "./root";
@@ -90,12 +88,9 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
} = useIssueDetail();
const { getStateById } = useProjectState();
const { isMobile } = usePlatformOS();
const { getUserDetails } = useMember();
const issue = getIssueById(issueId);
if (!issue) return <></>;
const createdByDetails = getUserDetails(issue.created_by);
const handleCopyText = () => {
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`).then(() => {
@@ -262,21 +257,6 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
/>
</div>
{createdByDetails && (
<div className="flex h-8 items-center gap-2">
<div className="flex w-2/5 flex-shrink-0 items-center gap-1 text-sm text-custom-text-300">
<UserCircle2 className="h-4 w-4 flex-shrink-0" />
<span>Created by</span>
</div>
<Tooltip tooltipContent={createdByDetails?.display_name} isMobile={isMobile}>
<div className="h-full flex items-center gap-1.5 rounded px-2 py-0.5 text-sm justify-between cursor-default">
<ButtonAvatars showTooltip={false} userIds={createdByDetails.id} />
<span className="flex-grow truncate text-xs leading-5">{createdByDetails?.display_name}</span>
</div>
</Tooltip>
</div>
)}
<div className="flex h-8 items-center gap-2">
<div className="flex w-2/5 flex-shrink-0 items-center gap-1 text-sm text-custom-text-300">
<CalendarClock className="h-4 w-4 flex-shrink-0" />
@@ -480,7 +460,12 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
</div>
</div>
<IssueLinkRoot workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} disabled={!isEditable} />
<IssueLinkRoot
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
disabled={!isEditable}
/>
</div>
</div>
</>
@@ -50,7 +50,7 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
const isEditingAllowed = alwaysAllowEditing || (currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER);
return (
<div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100 truncate">
<div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100">
{Object.entries(appliedFilters).map(([key, value]) => {
const filterKey = key as keyof IIssueFilterOptions;
@@ -60,9 +60,9 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
return (
<div
key={filterKey}
className="flex flex-wrap items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 capitalize truncate"
className="flex flex-wrap items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 capitalize w-full"
>
<div className="flex flex-wrap items-center gap-1.5 truncate">
<div className="flex flex-wrap items-center gap-1.5 w-full">
<span className="text-xs text-custom-text-300">{replaceUnderscoreIfSnakeCase(filterKey)}</span>
{membersFilters.includes(filterKey) && (
<AppliedMembersFilters
@@ -141,7 +141,7 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
<button
type="button"
onClick={handleClearAllFilters}
className="flex items-center gap-2 flex-shrink-0 rounded-md border border-custom-border-200 px-2 py-1 text-xs text-custom-text-300 hover:text-custom-text-200"
className="flex items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 text-xs text-custom-text-300 hover:text-custom-text-200"
>
Clear all
<X size={12} strokeWidth={2} />
@@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite";
// hooks
import { Avatar, Loader } from "@plane/ui";
import { FilterHeader, FilterOption } from "@/components/issues";
import { useMember, useUser } from "@/hooks/store";
import { useMember } from "@/hooks/store";
// components
// ui
@@ -22,18 +22,16 @@ export const FilterAssignees: React.FC<Props> = observer((props: Props) => {
const [previewEnabled, setPreviewEnabled] = useState(true);
// store hooks
const { getUserDetails } = useMember();
const { currentUser } = useUser();
const appliedFiltersCount = appliedFilters?.length ?? 0;
const sortedOptions = useMemo(() => {
const filteredOptions = (memberIds || []).filter((memberId) =>
getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase())
const filteredOptions = (memberIds || []).filter(
(memberId) => getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase())
);
return sortBy(filteredOptions, [
(memberId) => !(appliedFilters ?? []).includes(memberId),
(memberId) => memberId !== currentUser?.id,
(memberId) => getUserDetails(memberId)?.display_name.toLowerCase(),
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -68,7 +66,7 @@ export const FilterAssignees: React.FC<Props> = observer((props: Props) => {
isChecked={appliedFilters?.includes(member.id) ? true : false}
onClick={() => handleUpdate(member.id)}
icon={<Avatar name={member.display_name} src={member.avatar} showTooltip={false} size="md" />}
title={currentUser?.id === member.id ? "You" : member?.display_name}
title={member.display_name}
/>
);
})}
@@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite";
// hooks
import { Avatar, Loader } from "@plane/ui";
import { FilterHeader, FilterOption } from "@/components/issues";
import { useMember, useUser } from "@/hooks/store";
import { useMember } from "@/hooks/store";
// components
// ui
@@ -22,16 +22,14 @@ export const FilterCreatedBy: React.FC<Props> = observer((props: Props) => {
const [previewEnabled, setPreviewEnabled] = useState(true);
// store hooks
const { getUserDetails } = useMember();
const { currentUser } = useUser();
const sortedOptions = useMemo(() => {
const filteredOptions = (memberIds || []).filter((memberId) =>
getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase())
const filteredOptions = (memberIds || []).filter(
(memberId) => getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase())
);
return sortBy(filteredOptions, [
(memberId) => !(appliedFilters ?? []).includes(memberId),
(memberId) => memberId !== currentUser?.id,
(memberId) => getUserDetails(memberId)?.display_name.toLowerCase(),
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -68,7 +66,7 @@ export const FilterCreatedBy: React.FC<Props> = observer((props: Props) => {
isChecked={appliedFilters?.includes(member.id) ? true : false}
onClick={() => handleUpdate(member.id)}
icon={<Avatar name={member.display_name} src={member.avatar} size="md" />}
title={currentUser?.id === member.id ? "You" : member?.display_name}
title={member.display_name}
/>
);
})}
@@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite";
// hooks
import { Loader, Avatar } from "@plane/ui";
import { FilterHeader, FilterOption } from "@/components/issues";
import { useMember, useUser } from "@/hooks/store";
import { useMember } from "@/hooks/store";
// components
// ui
@@ -22,18 +22,16 @@ export const FilterMentions: React.FC<Props> = observer((props: Props) => {
const [previewEnabled, setPreviewEnabled] = useState(true);
// store hooks
const { getUserDetails } = useMember();
const { currentUser } = useUser();
const appliedFiltersCount = appliedFilters?.length ?? 0;
const sortedOptions = useMemo(() => {
const filteredOptions = (memberIds || []).filter((memberId) =>
getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase())
const filteredOptions = (memberIds || []).filter(
(memberId) => getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase())
);
return sortBy(filteredOptions, [
(memberId) => !(appliedFilters ?? []).includes(memberId),
(memberId) => memberId !== currentUser?.id,
(memberId) => getUserDetails(memberId)?.display_name.toLowerCase(),
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -68,7 +66,7 @@ export const FilterMentions: React.FC<Props> = observer((props: Props) => {
isChecked={appliedFilters?.includes(member.id) ? true : false}
onClick={() => handleUpdate(member.id)}
icon={<Avatar name={member?.display_name} src={member?.avatar} showTooltip={false} size={"md"} />}
title={currentUser?.id === member.id ? "You" : member?.display_name}
title={member.display_name}
/>
);
})}
@@ -391,8 +391,6 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
/>
)}
/>
<span className="text-xs text-red-500">{errors?.name?.message}</span>
<div className="relative">
{data?.description_html === undefined ? (
<Loader className="min-h-[7rem] space-y-2 overflow-hidden rounded-md border border-custom-border-200 p-2 py-2">
@@ -10,11 +10,10 @@ import {
XCircle,
CalendarClock,
CalendarCheck2,
UserCircle2,
} from "lucide-react";
// hooks
// ui icons
import { DiceIcon, DoubleCircleIcon, UserGroupIcon, ContrastIcon, RelatedIcon, Tooltip } from "@plane/ui";
import { DiceIcon, DoubleCircleIcon, UserGroupIcon, ContrastIcon, RelatedIcon } from "@plane/ui";
// components
import {
DateDropdown,
@@ -23,7 +22,6 @@ import {
MemberDropdown,
StateDropdown,
} from "@/components/dropdowns";
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
import {
IssueLinkRoot,
IssueCycleSelect,
@@ -37,8 +35,7 @@ import {
import { cn } from "@/helpers/common.helper";
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
import { useIssueDetail, useMember, useProject, useProjectState } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
import { useIssueDetail, useProject, useProjectState } from "@/hooks/store";
interface IPeekOverviewProperties {
workspaceSlug: string;
@@ -56,12 +53,9 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
issue: { getIssueById },
} = useIssueDetail();
const { getStateById } = useProjectState();
const { getUserDetails } = useMember();
const { isMobile } = usePlatformOS();
// derived values
const issue = getIssueById(issueId);
if (!issue) return <></>;
const createdByDetails = getUserDetails(issue?.created_by);
const projectDetails = getProjectById(issue.project_id);
const isEstimateEnabled = projectDetails?.estimate;
const stateDetails = getStateById(issue.state_id);
@@ -137,22 +131,6 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
/>
</div>
{/* created by */}
{createdByDetails && (
<div className="flex w-full items-center gap-3 h-8">
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
<UserCircle2 className="h-4 w-4 flex-shrink-0" />
<span>Created by</span>
</div>
<Tooltip tooltipContent={createdByDetails?.display_name} isMobile={isMobile}>
<div className="h-full flex items-center gap-1.5 rounded px-2 py-0.5 text-sm justify-between cursor-default">
<ButtonAvatars showTooltip={false} userIds={createdByDetails?.id} />
<span className="flex-grow truncate text-xs leading-5">{createdByDetails?.display_name}</span>
</div>
</Tooltip>
</div>
)}
{/* start date */}
<div className="flex w-full items-center gap-3 h-8">
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
+2 -3
View File
@@ -90,7 +90,7 @@ export const ModuleForm: React.FC<Props> = (props) => {
</div>
<div className="space-y-3">
<div className="flex flex-col gap-1">
<div>
<Controller
control={control}
name="name"
@@ -109,14 +109,13 @@ export const ModuleForm: React.FC<Props> = (props) => {
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors?.name)}
hasError={Boolean(errors.name)}
placeholder="Module Title"
className="w-full resize-none placeholder:text-sm placeholder:font-medium focus:border-blue-400"
tabIndex={1}
/>
)}
/>
<span className="text-xs text-red-500">{errors?.name?.message}</span>
</div>
<div>
<Controller
+92 -90
View File
@@ -135,101 +135,103 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
const completedModuleCheck = moduleDetails.status === "completed";
return (
<div className="group flex w-full flex-col items-center justify-between gap-5 border-b border-custom-border-100 bg-custom-background-100 px-5 py-6 text-sm hover:bg-custom-background-90 sm:flex-row">
<Link
href={`/${workspaceSlug}/projects/${moduleDetails.project_id}/modules/${moduleDetails.id}`}
onClick={(e) => {
if (isArchived) {
openModuleOverview(e);
}
}}
className="relative flex w-full items-center justify-between gap-3 overflow-hidden"
>
<div className="relative flex w-full items-center gap-3 overflow-hidden">
<div className="flex items-center gap-4 truncate">
<span className="flex-shrink-0">
<CircularProgressIndicator size={38} percentage={progress}>
{completedModuleCheck ? (
progress === 100 ? (
<Link
href={`/${workspaceSlug}/projects/${moduleDetails.project_id}/modules/${moduleDetails.id}`}
onClick={(e) => {
if (isArchived) {
openModuleOverview(e);
}
}}
>
<div className="group flex w-full flex-col items-center justify-between gap-5 border-b border-custom-border-100 bg-custom-background-100 px-5 py-6 text-sm hover:bg-custom-background-90 sm:flex-row">
<div className="relative flex w-full items-center justify-between gap-3 overflow-hidden">
<div className="relative flex w-full items-center gap-3 overflow-hidden">
<div className="flex items-center gap-4 truncate">
<span className="flex-shrink-0">
<CircularProgressIndicator size={38} percentage={progress}>
{completedModuleCheck ? (
progress === 100 ? (
<Check className="h-3 w-3 stroke-[2] text-custom-primary-100" />
) : (
<span className="text-sm text-custom-primary-100">{`!`}</span>
)
) : progress === 100 ? (
<Check className="h-3 w-3 stroke-[2] text-custom-primary-100" />
) : (
<span className="text-sm text-custom-primary-100">{`!`}</span>
)
) : progress === 100 ? (
<Check className="h-3 w-3 stroke-[2] text-custom-primary-100" />
) : (
<span className="text-xs text-custom-text-300">{`${progress}%`}</span>
)}
</CircularProgressIndicator>
</span>
<Tooltip tooltipContent={moduleDetails.name} position="top" isMobile={isMobile}>
<span className="truncate text-base font-medium">{moduleDetails.name}</span>
</Tooltip>
</div>
<button onClick={openModuleOverview} className="z-[5] hidden flex-shrink-0 group-hover:flex">
<Info className="h-4 w-4 text-custom-text-400" />
</button>
</div>
<div className="flex flex-shrink-0 items-center justify-center">
{moduleStatus && (
<span
className="flex h-6 w-20 flex-shrink-0 items-center justify-center rounded-sm text-center text-xs"
style={{
color: moduleStatus.color,
backgroundColor: `${moduleStatus.color}20`,
}}
>
{moduleStatus.label}
</span>
)}
</div>
</Link>
<div className="relative flex w-full items-center justify-between gap-2.5 sm:w-auto sm:flex-shrink-0 sm:justify-end ">
<div className="text-xs text-custom-text-300">
{renderDate && (
<span className=" text-xs text-custom-text-300">
{renderFormattedDate(startDate) ?? "_ _"} - {renderFormattedDate(endDate) ?? "_ _"}
</span>
)}
</div>
<div className="relative flex flex-shrink-0 items-center gap-3">
<Tooltip tooltipContent={`${moduleDetails?.member_ids?.length || 0} Members`} isMobile={isMobile}>
<div className="flex w-10 cursor-default items-center justify-center gap-1">
{moduleDetails.member_ids.length > 0 ? (
<AvatarGroup showTooltip={false}>
{moduleDetails.member_ids.map((member_id) => {
const member = getUserDetails(member_id);
return <Avatar key={member?.id} name={member?.display_name} src={member?.avatar} />;
})}
</AvatarGroup>
) : (
<span className="flex h-5 w-5 items-end justify-center rounded-full border border-dashed border-custom-text-400 bg-custom-background-80">
<User2 className="h-4 w-4 text-custom-text-400" />
</span>
)}
<span className="text-xs text-custom-text-300">{`${progress}%`}</span>
)}
</CircularProgressIndicator>
</span>
<Tooltip tooltipContent={moduleDetails.name} position="top" isMobile={isMobile}>
<span className="truncate text-base font-medium">{moduleDetails.name}</span>
</Tooltip>
</div>
</Tooltip>
<button onClick={openModuleOverview} className="z-[5] hidden flex-shrink-0 group-hover:flex">
<Info className="h-4 w-4 text-custom-text-400" />
</button>
</div>
<div className="flex flex-shrink-0 items-center justify-center">
{moduleStatus && (
<span
className="flex h-6 w-20 flex-shrink-0 items-center justify-center rounded-sm text-center text-xs"
style={{
color: moduleStatus.color,
backgroundColor: `${moduleStatus.color}20`,
}}
>
{moduleStatus.label}
</span>
)}
</div>
</div>
{isEditingAllowed && !isArchived && (
<FavoriteStar
onClick={(e) => {
if (moduleDetails.is_favorite) handleRemoveFromFavorites(e);
else handleAddToFavorites(e);
}}
selected={moduleDetails.is_favorite}
/>
)}
{workspaceSlug && projectId && (
<ModuleQuickActions
moduleId={moduleId}
projectId={projectId.toString()}
workspaceSlug={workspaceSlug.toString()}
isArchived={isArchived}
/>
)}
<div className="relative flex w-full items-center justify-between gap-2.5 sm:w-auto sm:flex-shrink-0 sm:justify-end ">
<div className="text-xs text-custom-text-300">
{renderDate && (
<span className=" text-xs text-custom-text-300">
{renderFormattedDate(startDate) ?? "_ _"} - {renderFormattedDate(endDate) ?? "_ _"}
</span>
)}
</div>
<div className="relative flex flex-shrink-0 items-center gap-3">
<Tooltip tooltipContent={`${moduleDetails?.member_ids?.length || 0} Members`} isMobile={isMobile}>
<div className="flex w-10 cursor-default items-center justify-center gap-1">
{moduleDetails.member_ids.length > 0 ? (
<AvatarGroup showTooltip={false}>
{moduleDetails.member_ids.map((member_id) => {
const member = getUserDetails(member_id);
return <Avatar key={member?.id} name={member?.display_name} src={member?.avatar} />;
})}
</AvatarGroup>
) : (
<span className="flex h-5 w-5 items-end justify-center rounded-full border border-dashed border-custom-text-400 bg-custom-background-80">
<User2 className="h-4 w-4 text-custom-text-400" />
</span>
)}
</div>
</Tooltip>
{isEditingAllowed && !isArchived && (
<FavoriteStar
onClick={(e) => {
if (moduleDetails.is_favorite) handleRemoveFromFavorites(e);
else handleAddToFavorites(e);
}}
selected={moduleDetails.is_favorite}
/>
)}
{workspaceSlug && projectId && (
<ModuleQuickActions
moduleId={moduleId}
projectId={projectId.toString()}
workspaceSlug={workspaceSlug.toString()}
isArchived={isArchived}
/>
)}
</div>
</div>
</div>
</div>
</Link>
);
});
@@ -1,5 +1,6 @@
import { useEffect } from "react";
import { observer } from "mobx-react-lite";
// hooks
// components
import { Spinner } from "@plane/ui";
import { DashboardWidgets } from "@/components/dashboard";
@@ -11,11 +12,7 @@ import { UserGreetingsView } from "@/components/user";
// constants
import { EmptyStateType } from "@/constants/empty-state";
import { PRODUCT_TOUR_COMPLETED } from "@/constants/event-tracker";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useApplication, useEventTracker, useDashboard, useProject, useUser } from "@/hooks/store";
import useSize from "@/hooks/use-window-size";
export const WorkspaceDashboardView = observer(() => {
// store hooks
@@ -28,8 +25,6 @@ export const WorkspaceDashboardView = observer(() => {
const { homeDashboardId, fetchHomeDashboardWidgets } = useDashboard();
const { joinedProjectIds } = useProject();
const [windowWidth] = useSize();
const handleTourCompleted = () => {
updateTourCompleted()
.then(() => {
@@ -62,14 +57,7 @@ export const WorkspaceDashboardView = observer(() => {
{joinedProjectIds.length > 0 ? (
<>
<IssuePeekOverview />
<div
className={cn(
"space-y-7 md:p-7 p-3 bg-custom-background-90 h-full w-full flex flex-col overflow-y-auto",
{
"vertical-scrollbar scrollbar-lg": windowWidth >= 768,
}
)}
>
<div className="space-y-7 p-7 bg-custom-background-90 h-full w-full flex flex-col overflow-y-auto vertical-scrollbar scrollbar-lg">
{currentUser && <UserGreetingsView user={currentUser} />}
<DashboardWidgets />
@@ -97,10 +97,10 @@ export const ActivityList: React.FC<Props> = observer((props) => {
return (
<li key={activityItem.id}>
<div className="relative pb-1">
<div className="relative flex items-start space-x-2">
<div className="relative flex items-center space-x-2">
<>
<div>
<div className="relative px-1.5 mt-4">
<div className="relative px-1.5">
<div className="mt-1.5">
<div className="flex h-6 w-6 items-center justify-center">
{activityItem.field ? (
@@ -127,7 +127,7 @@ export const ActivityList: React.FC<Props> = observer((props) => {
</div>
</div>
<div className="min-w-0 flex-1 border-b border-custom-border-100 py-4">
<div className="break-words text-sm text-custom-text-200">
<div className="flex gap-1 break-words text-sm text-custom-text-200">
{activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? (
<span className="text-gray font-medium">Plane</span>
) : activityItem.actor_detail.is_bot ? (
@@ -135,7 +135,6 @@ export const ActivityList: React.FC<Props> = observer((props) => {
) : (
<Link
href={`/${activityItem.workspace_detail?.slug}/profile/${activityItem.actor_detail.id}`}
className="inline"
>
<span className="text-gray font-medium">
{currentUser?.id === activityItem.actor_detail.id
@@ -144,7 +143,7 @@ export const ActivityList: React.FC<Props> = observer((props) => {
</span>
</Link>
)}{" "}
<div className="inline gap-1">
<div className="flex gap-1 truncate">
{message}{" "}
<span className="flex-shrink-0 whitespace-nowrap">
{calculateTimeAgo(activityItem.created_at)}
@@ -120,10 +120,10 @@ export const ProfileActivityListPage: React.FC<Props> = observer((props) => {
return (
<li key={activityItem.id}>
<div className="relative pb-1">
<div className="relative flex items-start space-x-2">
<div className="relative flex items-center space-x-2">
<>
<div>
<div className="relative px-1.5 mt-4">
<div className="relative px-1.5">
<div className="mt-1.5">
<div className="flex h-6 w-6 items-center justify-center">
{activityItem.field ? (
@@ -150,7 +150,7 @@ export const ProfileActivityListPage: React.FC<Props> = observer((props) => {
</div>
</div>
<div className="min-w-0 flex-1 border-b border-custom-border-100 py-4">
<div className="break-words text-sm text-custom-text-200">
<div className="flex gap-1 break-words text-sm text-custom-text-200">
{activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? (
<span className="text-gray font-medium">Plane</span>
) : activityItem.actor_detail.is_bot ? (
@@ -158,7 +158,6 @@ export const ProfileActivityListPage: React.FC<Props> = observer((props) => {
) : (
<Link
href={`/${activityItem.workspace_detail.slug}/profile/${activityItem.actor_detail.id}`}
className="inline"
>
<span className="text-gray font-medium">
{currentUser?.id === activityItem.actor_detail.id
@@ -167,7 +166,7 @@ export const ProfileActivityListPage: React.FC<Props> = observer((props) => {
</span>
</Link>
)}{" "}
<div className="inline gap-1">
<div className="flex gap-1 truncate">
{message}{" "}
<span className="flex-shrink-0 whitespace-nowrap">
{calculateTimeAgo(activityItem.created_at)}
+2 -2
View File
@@ -61,7 +61,7 @@ export const ProfileActivity = observer(() => {
)}
</div>
<div className="-mt-1 w-4/5 break-words">
<p className="inline text-sm text-custom-text-200">
<p className="text-sm text-custom-text-200">
<span className="font-medium text-custom-text-100">
{currentUser?.id === activity.actor_detail?.id ? "You" : activity.actor_detail?.display_name}{" "}
</span>
@@ -73,7 +73,7 @@ export const ProfileActivity = observer(() => {
</span>
)}
</p>
<p className="text-xs text-custom-text-200 whitespace-nowrap ">{calculateTimeAgo(activity.created_at)}</p>
<p className="text-xs text-custom-text-200">{calculateTimeAgo(activity.created_at)}</p>
</div>
</div>
))}
@@ -1,7 +1,7 @@
import { observer } from "mobx-react-lite";
import { X } from "lucide-react";
// helpers
import { PROJECT_CREATED_AT_FILTER_OPTIONS } from "@/constants/filters";
import { DATE_BEFORE_FILTER_OPTIONS } from "@/constants/filters";
import { renderFormattedDate } from "@/helpers/date-time.helper";
import { capitalizeFirstLetter } from "@/helpers/string.helper";
// constants
@@ -18,7 +18,7 @@ export const AppliedDateFilters: React.FC<Props> = observer((props) => {
const getDateLabel = (value: string): string => {
let dateLabel = "";
const dateDetails = PROJECT_CREATED_AT_FILTER_OPTIONS.find((d) => d.value === value);
const dateDetails = DATE_BEFORE_FILTER_OPTIONS.find((d) => d.value === value);
if (dateDetails) dateLabel = dateDetails.name;
else {
@@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite";
import { DateFilterModal } from "@/components/core";
import { FilterHeader, FilterOption } from "@/components/issues";
// constants
import { PROJECT_CREATED_AT_FILTER_OPTIONS } from "@/constants/filters";
import { DATE_BEFORE_FILTER_OPTIONS } from "@/constants/filters";
// helpers
import { isInDateFormat } from "@/helpers/date-time.helper";
@@ -16,12 +16,13 @@ type Props = {
export const FilterCreatedDate: React.FC<Props> = observer((props) => {
const { appliedFilters, handleUpdate, searchQuery } = props;
// state
const [previewEnabled, setPreviewEnabled] = useState(true);
const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false);
// derived values
const appliedFiltersCount = appliedFilters?.length ?? 0;
const filteredOptions = PROJECT_CREATED_AT_FILTER_OPTIONS.filter((d) =>
const filteredOptions = DATE_BEFORE_FILTER_OPTIONS.filter((d) =>
d.name.toLowerCase().includes(searchQuery.toLowerCase())
);
@@ -61,15 +62,10 @@ export const FilterCreatedDate: React.FC<Props> = observer((props) => {
isChecked={appliedFilters?.includes(option.value) ? true : false}
onClick={() => handleUpdate(option.value)}
title={option.name}
multiple={false}
multiple
/>
))}
<FilterOption
isChecked={isCustomDateSelected()}
onClick={handleCustomDate}
title="Custom"
multiple={false}
/>
<FilterOption isChecked={isCustomDateSelected()} onClick={handleCustomDate} title="Custom" multiple />
</>
) : (
<p className="text-xs italic text-custom-text-400">No matches found</p>
+3 -5
View File
@@ -216,10 +216,6 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
name="name"
rules={{
required: "Name is required",
maxLength: {
value: 255,
message: "Project name should be less than 255 characters",
},
}}
render={({ field: { value, onChange, ref } }) => (
<Input
@@ -236,7 +232,9 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
/>
)}
/>
<span className="text-xs text-red-500">{errors?.name?.message}</span>
<span className="text-xs text-red-500">
<>{errors?.name?.message}</>
</span>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Description</h4>
+3 -4
View File
@@ -112,7 +112,7 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
<div className="space-y-5">
<h3 className="text-lg font-medium leading-6 text-custom-text-100">{data ? "Update" : "Create"} View</h3>
<div className="space-y-3">
<div className="flex flex-col gap-1">
<div>
<Controller
control={control}
name="name"
@@ -137,7 +137,6 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
/>
)}
/>
<span className="text-xs text-red-500">{errors?.name?.message}</span>
</div>
<div>
<Controller
@@ -216,8 +215,8 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
? "Updating View..."
: "Update View"
: isSubmitting
? "Creating View..."
: "Create View"}
? "Creating View..."
: "Create View"}
</Button>
</div>
</form>
@@ -125,38 +125,35 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
Workspace Name
<span className="ml-0.5 text-red-500">*</span>
</label>
<div className="flex flex-col gap-1">
<Controller
control={control}
name="name"
rules={{
required: "Workspace name is required",
validate: (value) =>
/^[\w\s-]*$/.test(value) || `Name can only contain (" "), ( - ), ( _ ) & alphanumeric characters.`,
maxLength: {
value: 80,
message: "Workspace name should not exceed 80 characters",
},
}}
render={({ field: { value, ref, onChange } }) => (
<Input
id="workspaceName"
type="text"
value={value}
onChange={(e) => {
onChange(e.target.value);
setValue("name", e.target.value);
setValue("slug", e.target.value.toLocaleLowerCase().trim().replace(/ /g, "-"));
}}
ref={ref}
hasError={Boolean(errors.name)}
placeholder="Enter workspace name..."
className="w-full"
/>
)}
/>
<span className="text-xs text-red-500">{errors?.name?.message}</span>
</div>
<Controller
control={control}
name="name"
rules={{
required: "Workspace name is required",
validate: (value) =>
/^[\w\s-]*$/.test(value) || `Name can only contain (" "), ( - ), ( _ ) & alphanumeric characters.`,
maxLength: {
value: 80,
message: "Workspace name should not exceed 80 characters",
},
}}
render={({ field: { value, ref, onChange } }) => (
<Input
id="workspaceName"
type="text"
value={value}
onChange={(e) => {
onChange(e.target.value);
setValue("name", e.target.value);
setValue("slug", e.target.value.toLocaleLowerCase().trim().replace(/ /g, "-"));
}}
ref={ref}
hasError={Boolean(errors.name)}
placeholder="Enter workspace name..."
className="w-full"
/>
)}
/>
</div>
<div className="space-y-1 text-sm">
<label htmlFor="workspaceUrl">
-19
View File
@@ -31,22 +31,3 @@ export const DATE_BEFORE_FILTER_OPTIONS = [
value: "1_months;before;fromnow",
},
];
export const PROJECT_CREATED_AT_FILTER_OPTIONS = [
{
name: "Today",
value: "today;custom;custom",
},
{
name: "Yesterday",
value: "yesterday;custom;custom",
},
{
name: "Last 7 days",
value: "last_7_days;custom;custom",
},
{
name: "Last 30 days",
value: "last_30_days;custom;custom",
},
];
-8
View File
@@ -36,14 +36,6 @@ export const satisfiesDateFilter = (date: Date, filter: string): boolean => {
const [value, operator, from] = filter.split(";");
const dateValue = getDate(value);
if (operator === "custom" && from === "custom") {
if (value === "today") return differenceInCalendarDays(date, new Date()) === 0;
if (value === "yesterday") return differenceInCalendarDays(date, new Date()) === -1;
if (value === "last_7_days") return differenceInCalendarDays(date, new Date()) >= -7;
if (value === "last_30_days") return differenceInCalendarDays(date, new Date()) >= -30;
}
if (!from && dateValue) {
if (operator === "after") return date >= dateValue;
if (operator === "before") return date <= dateValue;
+1 -1
View File
@@ -54,13 +54,13 @@
"react-day-picker": "^8.10.0",
"react-dom": "18.2.0",
"react-dropzone": "^14.2.3",
"react-error-boundary": "^4.0.13",
"react-hook-form": "^7.38.0",
"react-markdown": "^8.0.7",
"react-popper": "^2.3.0",
"sharp": "^0.32.1",
"swr": "^2.1.3",
"tailwind-merge": "^2.0.0",
"use-debounce": "^9.0.4",
"uuid": "^9.0.0"
},
"devDependencies": {
+2 -2
View File
@@ -46,8 +46,8 @@ const ProfileActivityPage: NextPageWithLayout = observer(() => {
return (
<>
<PageHead title="Profile - Activity" />
<section className="mx-auto mt-5 md:mt-16 h-full w-full flex flex-col overflow-hidden px-5 md:px-8 pb-8 lg:w-3/5">
<div className="flex items-center border-b border-custom-border-100 gap-4 pb-3.5">
<section className="mx-auto h-full w-full flex flex-col overflow-hidden px-8 pb-8 lg:w-3/5">
<div className="flex items-center border-b border-custom-border-100 gap-4 pb-3.5 mt-5 md:mt-16">
<SidebarHamburgerToggle onClick={() => themeStore.toggleSidebar()} />
<h3 className="text-xl font-medium">Activity</h3>
</div>
+8 -6
View File
@@ -2755,7 +2755,7 @@
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@18.2.42", "@types/react@^18.2.42":
"@types/react@*", "@types/react@^18.2.42":
version "18.2.42"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.42.tgz#6f6b11a904f6d96dda3c2920328a97011a00aba7"
integrity sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA==
@@ -7194,6 +7194,13 @@ react-dropzone@^14.2.3:
file-selector "^0.6.0"
prop-types "^15.8.1"
react-error-boundary@^4.0.13:
version "4.0.13"
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-4.0.13.tgz#80386b7b27b1131c5fbb7368b8c0d983354c7947"
integrity sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==
dependencies:
"@babel/runtime" "^7.12.5"
react-fast-compare@^3.0.1:
version "3.2.2"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49"
@@ -8628,11 +8635,6 @@ use-callback-ref@^1.3.0:
dependencies:
tslib "^2.0.0"
use-debounce@^9.0.4:
version "9.0.4"
resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-9.0.4.tgz#51d25d856fbdfeb537553972ce3943b897f1ac85"
integrity sha512-6X8H/mikbrt0XE8e+JXRtZ8yYVvKkdYRfmIhWZYsP8rcNs9hk3APV8Ua2mFkKRLcJKVdnX2/Vwrmg2GWKUQEaQ==
use-memo-one@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99"