diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index eca18163a..cfdd0dd9b 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -994,11 +994,11 @@ class UserWorkspaceDashboardEndpoint(BaseAPIView): upcoming_issues = Issue.issue_objects.filter( ~Q(state__group__in=["completed", "cancelled"]), - target_date__gte=timezone.now(), + start_date__gte=timezone.now(), workspace__slug=slug, assignees__in=[request.user], completed_at__isnull=True, - ).values("id", "name", "workspace__slug", "project_id", "target_date") + ).values("id", "name", "workspace__slug", "project_id", "start_date") return Response( { @@ -1083,6 +1083,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView): .filter(**filters) .values("priority") .annotate(priority_count=Count("priority")) + .filter(priority_count__gte=1) .annotate( priority_order=Case( *[ diff --git a/apiserver/plane/db/migrations/0041_cycle_sort_order_issuecomment_access_and_more.py b/apiserver/plane/db/migrations/0041_cycle_sort_order_issuecomment_access_and_more.py index bffa89d23..07c302c76 100644 --- a/apiserver/plane/db/migrations/0041_cycle_sort_order_issuecomment_access_and_more.py +++ b/apiserver/plane/db/migrations/0041_cycle_sort_order_issuecomment_access_and_more.py @@ -227,6 +227,11 @@ class Migration(migrations.Migration): 'unique_together': {('issue', 'actor')}, }, ), + migrations.AlterField( + model_name='modulelink', + name='title', + field=models.CharField(blank=True, max_length=255, null=True), + ), migrations.RunPython(generate_display_name), migrations.RunPython(rectify_field_issue_activity), migrations.RunPython(update_assignee_issue_activity), diff --git a/apiserver/plane/db/models/module.py b/apiserver/plane/db/models/module.py index ad1e16080..e286d297a 100644 --- a/apiserver/plane/db/models/module.py +++ b/apiserver/plane/db/models/module.py @@ -98,7 +98,7 @@ class ModuleIssue(ProjectBaseModel): class ModuleLink(ProjectBaseModel): - title = models.CharField(max_length=255, null=True) + title = models.CharField(max_length=255, blank=True, null=True) url = models.URLField() module = models.ForeignKey( Module, on_delete=models.CASCADE, related_name="link_module" diff --git a/apps/app/components/analytics/scope-and-demand/leaderboard.tsx b/apps/app/components/analytics/scope-and-demand/leaderboard.tsx index 156c09d4c..e52657c03 100644 --- a/apps/app/components/analytics/scope-and-demand/leaderboard.tsx +++ b/apps/app/components/analytics/scope-and-demand/leaderboard.tsx @@ -1,3 +1,8 @@ +// ui +import { ProfileEmptyState } from "components/ui"; +// image +import emptyUsers from "public/empty-state/empty_users.svg"; + type Props = { users: { avatar: string | null; @@ -8,10 +13,16 @@ type Props = { id: string; }[]; title: string; + emptyStateMessage: string; workspaceSlug: string; }; -export const AnalyticsLeaderboard: React.FC = ({ users, title, workspaceSlug }) => ( +export const AnalyticsLeaderboard: React.FC = ({ + users, + title, + emptyStateMessage, + workspaceSlug, +}) => (
{title}
{users.length > 0 ? ( @@ -47,7 +58,9 @@ export const AnalyticsLeaderboard: React.FC = ({ users, title, workspaceS ))}
) : ( -
No matching data found.
+
+ +
)} ); diff --git a/apps/app/components/analytics/scope-and-demand/scope-and-demand.tsx b/apps/app/components/analytics/scope-and-demand/scope-and-demand.tsx index 7f40ee79a..dc7e65515 100644 --- a/apps/app/components/analytics/scope-and-demand/scope-and-demand.tsx +++ b/apps/app/components/analytics/scope-and-demand/scope-and-demand.tsx @@ -63,6 +63,7 @@ export const ScopeAndDemand: React.FC = ({ fullScreen = true }) => { id: user?.created_by__id, }))} title="Most issues created" + emptyStateMessage="Co-workers and the number issues created by them appears here." workspaceSlug={workspaceSlug?.toString() ?? ""} /> = ({ fullScreen = true }) => { id: user?.assignees__id, }))} title="Most issues closed" + emptyStateMessage="Co-workers and the number issues closed by them appears here." workspaceSlug={workspaceSlug?.toString() ?? ""} />
diff --git a/apps/app/components/analytics/scope-and-demand/scope.tsx b/apps/app/components/analytics/scope-and-demand/scope.tsx index 15b9e18de..b01354b93 100644 --- a/apps/app/components/analytics/scope-and-demand/scope.tsx +++ b/apps/app/components/analytics/scope-and-demand/scope.tsx @@ -1,5 +1,7 @@ // ui -import { BarGraph } from "components/ui"; +import { BarGraph, ProfileEmptyState } from "components/ui"; +// image +import emptyBarGraph from "public/empty-state/empty_bar_graph.svg"; // types import { IDefaultAnalyticsResponse } from "types"; @@ -70,8 +72,12 @@ export const AnalyticsScope: React.FC = ({ defaultAnalytics }) => ( }} /> ) : ( -
- No matching data found. +
+
)}
diff --git a/apps/app/components/analytics/scope-and-demand/year-wise-issues.tsx b/apps/app/components/analytics/scope-and-demand/year-wise-issues.tsx index 621706f0e..87127ed60 100644 --- a/apps/app/components/analytics/scope-and-demand/year-wise-issues.tsx +++ b/apps/app/components/analytics/scope-and-demand/year-wise-issues.tsx @@ -1,5 +1,7 @@ // ui -import { LineGraph } from "components/ui"; +import { LineGraph, ProfileEmptyState } from "components/ui"; +// image +import emptyGraph from "public/empty-state/empty_graph.svg"; // types import { IDefaultAnalyticsResponse } from "types"; // constants @@ -48,7 +50,13 @@ export const AnalyticsYearWiseIssues: React.FC = ({ defaultAnalytics }) = enableArea /> ) : ( -
No matching data found.
+
+ +
)}
); diff --git a/apps/app/components/core/modals/gpt-assistant-modal.tsx b/apps/app/components/core/modals/gpt-assistant-modal.tsx index a3b748d66..35f282040 100644 --- a/apps/app/components/core/modals/gpt-assistant-modal.tsx +++ b/apps/app/components/core/modals/gpt-assistant-modal.tsx @@ -140,14 +140,14 @@ export const GptAssistantModal: React.FC = ({ return (
{((content && content !== "") || (htmlContent && htmlContent !== "

")) && (
Content: ${content}

`} customClassName="-m-3" noBorder @@ -161,6 +161,7 @@ export const GptAssistantModal: React.FC = ({
Response: ${response}

`} customClassName="-mx-3 -my-3" noBorder @@ -179,11 +180,10 @@ export const GptAssistantModal: React.FC = ({ type="text" name="task" register={register} - placeholder={`${ - content && content !== "" + placeholder={`${content && content !== "" ? "Tell AI what action to perform on this content..." : "Ask AI anything..." - }`} + }`} autoComplete="off" />
@@ -219,8 +219,8 @@ export const GptAssistantModal: React.FC = ({ {isSubmitting ? "Generating response..." : response === "" - ? "Generate response" - : "Generate again"} + ? "Generate response" + : "Generate again"}
diff --git a/apps/app/components/core/modals/link-modal.tsx b/apps/app/components/core/modals/link-modal.tsx index fc1641767..bed74fca0 100644 --- a/apps/app/components/core/modals/link-modal.tsx +++ b/apps/app/components/core/modals/link-modal.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect } from "react"; // react-hook-form import { useForm } from "react-hook-form"; @@ -7,12 +7,15 @@ import { Dialog, Transition } from "@headlessui/react"; // ui import { Input, PrimaryButton, SecondaryButton } from "components/ui"; // types -import type { IIssueLink, ModuleLink } from "types"; +import type { IIssueLink, linkDetails, ModuleLink } from "types"; type Props = { isOpen: boolean; handleClose: () => void; - onFormSubmit: (formData: IIssueLink | ModuleLink) => Promise; + data?: linkDetails | null; + status: boolean; + createIssueLink: (formData: IIssueLink | ModuleLink) => Promise; + updateIssueLink: (formData: IIssueLink | ModuleLink, linkId: string) => Promise; }; const defaultValues: IIssueLink | ModuleLink = { @@ -20,7 +23,14 @@ const defaultValues: IIssueLink | ModuleLink = { url: "", }; -export const LinkModal: React.FC = ({ isOpen, handleClose, onFormSubmit }) => { +export const LinkModal: React.FC = ({ + isOpen, + handleClose, + createIssueLink, + updateIssueLink, + status, + data, +}) => { const { register, formState: { errors, isSubmitting }, @@ -30,11 +40,6 @@ export const LinkModal: React.FC = ({ isOpen, handleClose, onFormSubmit } defaultValues, }); - const onSubmit = async (formData: IIssueLink | ModuleLink) => { - await onFormSubmit({ title: formData.title, url: formData.url }); - onClose(); - }; - const onClose = () => { handleClose(); const timeout = setTimeout(() => { @@ -43,6 +48,27 @@ export const LinkModal: React.FC = ({ isOpen, handleClose, onFormSubmit } }, 500); }; + const handleFormSubmit = async (formData: IIssueLink | ModuleLink) => { + if (!data) await createIssueLink({ title: formData.title, url: formData.url }); + else await updateIssueLink({ title: formData.title, url: formData.url }, data.id); + onClose(); + }; + + const handleCreateUpdatePage = async (formData: IIssueLink | ModuleLink) => { + await handleFormSubmit(formData); + + reset({ + ...defaultValues, + }); + }; + + useEffect(() => { + reset({ + ...defaultValues, + ...data, + }); + }, [data, reset]); + return ( @@ -70,14 +96,14 @@ export const LinkModal: React.FC = ({ isOpen, handleClose, onFormSubmit } leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > -
+
- Add Link + {status ? "Update Link" : "Add Link"}
@@ -113,7 +139,13 @@ export const LinkModal: React.FC = ({ isOpen, handleClose, onFormSubmit }
Cancel - {isSubmitting ? "Adding Link..." : "Add Link"} + {status + ? isSubmitting + ? "Updating Link..." + : "Update Link" + : isSubmitting + ? "Adding Link..." + : "Add Link"}
diff --git a/apps/app/components/core/sidebar/links-list.tsx b/apps/app/components/core/sidebar/links-list.tsx index af008fec4..fb8f3243d 100644 --- a/apps/app/components/core/sidebar/links-list.tsx +++ b/apps/app/components/core/sidebar/links-list.tsx @@ -1,25 +1,24 @@ // icons import { ArrowTopRightOnSquareIcon, LinkIcon, TrashIcon } from "@heroicons/react/24/outline"; +import { Icon } from "components/ui"; // helpers import { timeAgo } from "helpers/date-time.helper"; // types -import { IUserLite, UserAuth } from "types"; +import { linkDetails, UserAuth } from "types"; type Props = { - links: { - id: string; - created_at: Date; - created_by: string; - created_by_detail: IUserLite; - metadata: any; - title: string; - url: string; - }[]; + links: linkDetails[]; handleDeleteLink: (linkId: string) => void; + handleEditLink: (link: linkDetails) => void; userAuth: UserAuth; }; -export const LinksList: React.FC = ({ links, handleDeleteLink, userAuth }) => { +export const LinksList: React.FC = ({ + links, + handleDeleteLink, + handleEditLink, + userAuth, +}) => { const isNotAllowed = userAuth.isGuest || userAuth.isViewer; return ( @@ -28,6 +27,13 @@ export const LinksList: React.FC = ({ links, handleDeleteLink, userAuth }
{!isNotAllowed && (
+ = ({ }) => { // context menu const [contextMenu, setContextMenu] = useState(false); - const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 }); + const [contextMenuPosition, setContextMenuPosition] = useState(null); + const [isMenuActive, setIsMenuActive] = useState(false); + const [isDropdownActive, setIsDropdownActive] = useState(false); const actionSectionRef = useRef(null); @@ -200,7 +202,7 @@ export const SingleBoardIssue: React.FC = ({ return ( <> = ({ onContextMenu={(e) => { e.preventDefault(); setContextMenu(true); - setContextMenuPosition({ x: e.pageX, y: e.pageY }); + setContextMenuPosition(e); }} > -
+
{!isNotAllowed && (
= ({
)} -
+ {properties.key && ( -
+
{issue.project_detail.identifier}-{issue.sequence_id}
)}
{issue.name}
-
+
{properties.priority && ( = ({ setIsDropdownActive(true)} + handleOnClose={() => setIsDropdownActive(false)} user={user} isNotAllowed={isNotAllowed} /> @@ -335,6 +343,8 @@ export const SingleBoardIssue: React.FC = ({ setIsDropdownActive(true)} + handleOnClose={() => setIsDropdownActive(false)} user={user} isNotAllowed={isNotAllowed} /> diff --git a/apps/app/components/core/views/list-view/single-issue.tsx b/apps/app/components/core/views/list-view/single-issue.tsx index 7d1cea37e..eafe74612 100644 --- a/apps/app/components/core/views/list-view/single-issue.tsx +++ b/apps/app/components/core/views/list-view/single-issue.tsx @@ -33,7 +33,7 @@ import { } from "@heroicons/react/24/outline"; import { LayerDiagonalIcon } from "components/icons"; // helpers -import { copyTextToClipboard, truncateText } from "helpers/string.helper"; +import { copyTextToClipboard } from "helpers/string.helper"; import { handleIssuesMutation } from "constants/issue"; // types import { ICurrentUserResponse, IIssue, IIssueViewProps, ISubIssueResponse, UserAuth } from "types"; @@ -71,7 +71,7 @@ export const SingleListIssue: React.FC = ({ }) => { // context menu const [contextMenu, setContextMenu] = useState(false); - const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 }); + const [contextMenuPosition, setContextMenuPosition] = useState(null); const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId } = router.query; @@ -167,7 +167,7 @@ export const SingleListIssue: React.FC = ({ return ( <> = ({ onContextMenu={(e) => { e.preventDefault(); setContextMenu(true); - setContextMenuPosition({ x: e.pageX, y: e.pageY }); + setContextMenuPosition(e); }} >
diff --git a/apps/app/components/cycles/single-cycle-card.tsx b/apps/app/components/cycles/single-cycle-card.tsx index 8808940f1..0822ba26a 100644 --- a/apps/app/components/cycles/single-cycle-card.tsx +++ b/apps/app/components/cycles/single-cycle-card.tsx @@ -45,7 +45,6 @@ type TSingleStatProps = { handleDeleteCycle: () => void; handleAddToFavorites: () => void; handleRemoveFromFavorites: () => void; - isCompleted?: boolean; }; const stateGroups = [ @@ -82,7 +81,6 @@ export const SingleCycleCard: React.FC = ({ handleDeleteCycle, handleAddToFavorites, handleRemoveFromFavorites, - isCompleted = false, }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -90,6 +88,7 @@ export const SingleCycleCard: React.FC = ({ const { setToastAlert } = useToast(); const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date); + const isCompleted = cycleStatus === "completed"; const endDate = new Date(cycle.end_date ?? ""); const startDate = new Date(cycle.start_date ?? ""); diff --git a/apps/app/components/cycles/single-cycle-list.tsx b/apps/app/components/cycles/single-cycle-list.tsx index 813264986..7518568ed 100644 --- a/apps/app/components/cycles/single-cycle-list.tsx +++ b/apps/app/components/cycles/single-cycle-list.tsx @@ -34,7 +34,6 @@ type TSingleStatProps = { handleDeleteCycle: () => void; handleAddToFavorites: () => void; handleRemoveFromFavorites: () => void; - isCompleted?: boolean; }; const stateGroups = [ @@ -113,7 +112,6 @@ export const SingleCycleList: React.FC = ({ handleDeleteCycle, handleAddToFavorites, handleRemoveFromFavorites, - isCompleted = false, }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -121,6 +119,7 @@ export const SingleCycleList: React.FC = ({ const { setToastAlert } = useToast(); const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date); + const isCompleted = cycleStatus === "completed"; const endDate = new Date(cycle.end_date ?? ""); const startDate = new Date(cycle.start_date ?? ""); diff --git a/apps/app/components/inbox/inbox-main-content.tsx b/apps/app/components/inbox/inbox-main-content.tsx index af1234859..bd4f0ab01 100644 --- a/apps/app/components/inbox/inbox-main-content.tsx +++ b/apps/app/components/inbox/inbox-main-content.tsx @@ -293,6 +293,7 @@ export const InboxMainContent: React.FC = () => {
= ({ issueId, user }) => {
- {activityItem.actor_detail.display_name.charAt(0)} + {activityItem.actor_detail.is_bot + ? activityItem.actor_detail.first_name.charAt(0) + : activityItem.actor_detail.display_name.charAt(0)}
)}
@@ -153,7 +155,9 @@ export const IssueActivitySection: React.FC = ({ issueId, user }) => { ) : ( - {activityItem.actor_detail.display_name} + {activityItem.actor_detail.is_bot + ? activityItem.actor_detail.first_name + : activityItem.actor_detail.display_name} )}{" "} @@ -171,6 +175,7 @@ export const IssueActivitySection: React.FC = ({ issueId, user }) => { return (
= ({ issueId, user, disabled = false }) control={control} render={({ field: { value, onChange } }) => ( { onChange(comment_html); diff --git a/apps/app/components/issues/comment/comment-card.tsx b/apps/app/components/issues/comment/comment-card.tsx index 08ea0f1d5..06ef891a9 100644 --- a/apps/app/components/issues/comment/comment-card.tsx +++ b/apps/app/components/issues/comment/comment-card.tsx @@ -22,12 +22,13 @@ const TiptapEditor = React.forwardRef void; handleCommentDeletion: (comment: string) => void; }; -export const CommentCard: React.FC = ({ comment, onSubmit, handleCommentDeletion }) => { +export const CommentCard: React.FC = ({ comment, workspaceSlug, onSubmit, handleCommentDeletion }) => { const { user } = useUser(); const editorRef = React.useRef(null); @@ -65,7 +66,11 @@ export const CommentCard: React.FC = ({ comment, onSubmit, handleCommentD {comment.actor_detail.avatar && comment.actor_detail.avatar !== "" ? ( {comment.actor_detail.display_name} = ({ comment, onSubmit, handleCommentD
- {comment.actor_detail.display_name.charAt(0)} + {comment.actor_detail.is_bot + ? comment.actor_detail.first_name.charAt(0) + : comment.actor_detail.display_name.charAt(0)}
)} @@ -103,10 +110,11 @@ export const CommentCard: React.FC = ({ comment, onSubmit, handleCommentD >
{ setValue("comment_json", comment_json); setValue("comment_html", comment_html); @@ -132,6 +140,7 @@ export const CommentCard: React.FC = ({ comment, onSubmit, handleCommentD
Promise; isAllowed: boolean; } @@ -31,6 +32,7 @@ export interface IssueDetailsProps { export const IssueDescriptionForm: FC = ({ issue, handleFormSubmit, + workspaceSlug, isAllowed, }) => { const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); @@ -69,11 +71,15 @@ export const IssueDescriptionForm: FC = ({ useEffect(() => { if (isSubmitting === "submitted") { + setShowAlert(false); setTimeout(async () => { setIsSubmitting("saved"); }, 2000); + } else if (isSubmitting === "submitting") { + setShowAlert(true); } - }, [isSubmitting]); + }, [isSubmitting, setShowAlert]); + // reset form values useEffect(() => { @@ -112,9 +118,8 @@ export const IssueDescriptionForm: FC = ({ {characterLimit && (
255 ? "text-red-500" : "" - }`} + className={`${watch("name").length === 0 || watch("name").length > 255 ? "text-red-500" : "" + }`} > {watch("name").length} @@ -134,16 +139,19 @@ export const IssueDescriptionForm: FC = ({ { + setShowAlert(true); setIsSubmitting("submitting"); onChange(description_html); setValue("description", description); @@ -156,9 +164,8 @@ export const IssueDescriptionForm: FC = ({ }} />
{isSubmitting === "submitting" ? "Saving..." : "Saved"}
diff --git a/apps/app/components/issues/form.tsx b/apps/app/components/issues/form.tsx index 0e73ca349..c15c61d57 100644 --- a/apps/app/components/issues/form.tsx +++ b/apps/app/components/issues/form.tsx @@ -370,6 +370,7 @@ export const IssueForm: FC = ({ return ( = ({
) : null} = ({ projectId, value = [], on value={value} onChange={onChange} options={options} - label={ -
+ customButton={ +
{value && value.length > 0 && Array.isArray(value) ? ( -
+
) : ( -
- +
+ Assignee
)} diff --git a/apps/app/components/issues/select/date.tsx b/apps/app/components/issues/select/date.tsx index fb88a8937..01fbacda3 100644 --- a/apps/app/components/issues/select/date.tsx +++ b/apps/app/components/issues/select/date.tsx @@ -17,10 +17,10 @@ type Props = { export const IssueDateSelect: React.FC = ({ label, maxDate, minDate, onChange, value }) => ( - {({ open }) => ( + {({ close }) => ( <> - + {value ? ( <> {renderShortDateWithYearFormat(value)} @@ -52,6 +52,8 @@ export const IssueDateSelect: React.FC = ({ label, maxDate, minDate, onCh onChange={(val) => { if (!val) onChange(""); else onChange(renderDateFormat(val)); + + close(); }} dateFormat="dd-MM-yyyy" minDate={minDate} diff --git a/apps/app/components/issues/select/label.tsx b/apps/app/components/issues/select/label.tsx index 6d7e2f391..cf960b3a0 100644 --- a/apps/app/components/issues/select/label.tsx +++ b/apps/app/components/issues/select/label.tsx @@ -59,17 +59,17 @@ export const IssueLabelSelect: React.FC = ({ setIsOpen, value, onChange, > {({ open }: any) => ( <> - + {value && value.length > 0 ? ( - + issueLabels?.find((l) => l.id === v)?.color) ?? []} + labels={value.map((v) => issueLabels?.find((l) => l.id === v)) ?? []} length={3} showLength={true} /> ) : ( - + Label diff --git a/apps/app/components/issues/sidebar.tsx b/apps/app/components/issues/sidebar.tsx index c396750e5..3ac0eb943 100644 --- a/apps/app/components/issues/sidebar.tsx +++ b/apps/app/components/issues/sidebar.tsx @@ -37,7 +37,7 @@ import { LinkIcon, CalendarDaysIcon, TrashIcon, PlusIcon } from "@heroicons/reac // helpers import { copyTextToClipboard } from "helpers/string.helper"; // types -import type { ICycle, IIssue, IIssueLink, IModule } from "types"; +import type { ICycle, IIssue, IIssueLink, linkDetails, IModule } from "types"; // fetch-keys import { ISSUE_DETAILS } from "constants/fetch-keys"; @@ -77,6 +77,7 @@ export const IssueDetailsSidebar: React.FC = ({ }) => { const [deleteIssueModal, setDeleteIssueModal] = useState(false); const [linkModal, setLinkModal] = useState(false); + const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState(null); const router = useRouter(); const { workspaceSlug, projectId, issueId } = router.query; @@ -156,6 +157,43 @@ export const IssueDetailsSidebar: React.FC = ({ }); }; + const handleUpdateLink = async (formData: IIssueLink, linkId: string) => { + if (!workspaceSlug || !projectId || !issueDetail) return; + + const payload = { metadata: {}, ...formData }; + + const updatedLinks = issueDetail.issue_link.map((l) => + l.id === linkId + ? { + ...l, + title: formData.title, + url: formData.url, + } + : l + ); + + mutate( + ISSUE_DETAILS(issueDetail.id), + (prevData) => ({ ...(prevData as IIssue), issue_link: updatedLinks }), + false + ); + + await issuesService + .updateIssueLink( + workspaceSlug as string, + projectId as string, + issueDetail.id, + linkId, + payload + ) + .then((res) => { + mutate(ISSUE_DETAILS(issueDetail.id)); + }) + .catch((err) => { + console.log(err); + }); + }; + const handleDeleteLink = async (linkId: string) => { if (!workspaceSlug || !projectId || !issueDetail) return; @@ -220,14 +258,25 @@ export const IssueDetailsSidebar: React.FC = ({ const maxDate = targetDate ? new Date(targetDate) : null; maxDate?.setDate(maxDate.getDate()); + const handleEditLink = (link: linkDetails) => { + setSelectedLinkToUpdate(link); + setLinkModal(true); + }; + const isNotAllowed = memberRole.isGuest || memberRole.isViewer; return ( <> setLinkModal(false)} - onFormSubmit={handleCreateLink} + handleClose={() => { + setLinkModal(false); + setSelectedLinkToUpdate(null); + }} + data={selectedLinkToUpdate} + status={selectedLinkToUpdate ? true : false} + createIssueLink={handleCreateLink} + updateIssueLink={handleUpdateLink} /> setDeleteIssueModal(false)} @@ -396,7 +445,8 @@ export const IssueDetailsSidebar: React.FC = ({ start_date: val, }) } - className="bg-custom-background-90 w-full" + className="bg-custom-background-100" + wrapperClassName="w-full" maxDate={maxDate ?? undefined} disabled={isNotAllowed || uneditable} /> @@ -424,7 +474,8 @@ export const IssueDetailsSidebar: React.FC = ({ target_date: val, }) } - className="bg-custom-background-90 w-full" + className="bg-custom-background-100" + wrapperClassName="w-full" minDate={minDate ?? undefined} disabled={isNotAllowed || uneditable} /> @@ -488,6 +539,7 @@ export const IssueDetailsSidebar: React.FC = ({ ) : null} diff --git a/apps/app/components/issues/view-select/due-date.tsx b/apps/app/components/issues/view-select/due-date.tsx index 54b287a1e..5f5e5cbd0 100644 --- a/apps/app/components/issues/view-select/due-date.tsx +++ b/apps/app/components/issues/view-select/due-date.tsx @@ -13,6 +13,8 @@ import useIssuesView from "hooks/use-issues-view"; type Props = { issue: IIssue; partialUpdateIssue: (formData: Partial, issue: IIssue) => void; + handleOnOpen?: () => void; + handleOnClose?: () => void; tooltipPosition?: "top" | "bottom"; noBorder?: boolean; user: ICurrentUserResponse | undefined; @@ -22,6 +24,8 @@ type Props = { export const ViewDueDateSelect: React.FC = ({ issue, partialUpdateIssue, + handleOnOpen, + handleOnClose, tooltipPosition = "top", noBorder = false, user, @@ -80,6 +84,8 @@ export const ViewDueDateSelect: React.FC = ({ }`} minDate={minDate ?? undefined} noBorder={noBorder} + handleOnOpen={handleOnOpen} + handleOnClose={handleOnClose} disabled={isNotAllowed} />
diff --git a/apps/app/components/issues/view-select/start-date.tsx b/apps/app/components/issues/view-select/start-date.tsx index 29110eadb..8748567ae 100644 --- a/apps/app/components/issues/view-select/start-date.tsx +++ b/apps/app/components/issues/view-select/start-date.tsx @@ -13,6 +13,8 @@ import useIssuesView from "hooks/use-issues-view"; type Props = { issue: IIssue; partialUpdateIssue: (formData: Partial, issue: IIssue) => void; + handleOnOpen?: () => void; + handleOnClose?: () => void; tooltipPosition?: "top" | "bottom"; noBorder?: boolean; user: ICurrentUserResponse | undefined; @@ -22,6 +24,8 @@ type Props = { export const ViewStartDateSelect: React.FC = ({ issue, partialUpdateIssue, + handleOnOpen, + handleOnClose, tooltipPosition = "top", noBorder = false, user, @@ -72,6 +76,8 @@ export const ViewStartDateSelect: React.FC = ({ }`} maxDate={maxDate ?? undefined} noBorder={noBorder} + handleOnOpen={handleOnOpen} + handleOnClose={handleOnClose} disabled={isNotAllowed} />
diff --git a/apps/app/components/modules/form.tsx b/apps/app/components/modules/form.tsx index 0b36176c5..2715ba266 100644 --- a/apps/app/components/modules/form.tsx +++ b/apps/app/components/modules/form.tsx @@ -1,15 +1,11 @@ -import { useEffect, useState } from "react"; +import { useEffect } from "react"; // react-hook-form import { Controller, useForm } from "react-hook-form"; -// hooks -import useToast from "hooks/use-toast"; // components import { ModuleLeadSelect, ModuleMembersSelect, ModuleStatusSelect } from "components/modules"; // ui import { DateSelect, Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui"; -// helper -import { isDateRangeValid } from "helpers/date-time.helper"; // types import { IModule } from "types"; @@ -29,8 +25,6 @@ const defaultValues: Partial = { }; export const ModuleForm: React.FC = ({ handleFormSubmit, handleClose, status, data }) => { - const [isDateValid, setIsDateValid] = useState(true); - const { setToastAlert } = useToast(); const { register, formState: { errors, isSubmitting }, @@ -57,6 +51,15 @@ export const ModuleForm: React.FC = ({ handleFormSubmit, handleClose, sta }); }, [data, reset]); + const startDate = watch("start_date"); + const targetDate = watch("target_date"); + + const minDate = startDate ? new Date(startDate) : null; + minDate?.setDate(minDate.getDate()); + + const maxDate = targetDate ? new Date(targetDate) : null; + maxDate?.setDate(maxDate.getDate()); + return (
@@ -103,20 +106,8 @@ export const ModuleForm: React.FC = ({ handleFormSubmit, handleClose, sta value={value} onChange={(val) => { onChange(val); - if (val && watch("target_date")) { - if (isDateRangeValid(val, `${watch("target_date")}`)) { - setIsDateValid(true); - } else { - setIsDateValid(false); - setToastAlert({ - type: "error", - title: "Error!", - message: - "The date you have entered is invalid. Please check and enter a valid date.", - }); - } - } }} + maxDate={maxDate ?? undefined} /> )} /> @@ -129,20 +120,8 @@ export const ModuleForm: React.FC = ({ handleFormSubmit, handleClose, sta value={value} onChange={(val) => { onChange(val); - if (watch("start_date") && val) { - if (isDateRangeValid(`${watch("start_date")}`, val)) { - setIsDateValid(true); - } else { - setIsDateValid(false); - setToastAlert({ - type: "error", - title: "Error!", - message: - "The date you have entered is invalid. Please check and enter a valid date.", - }); - } - } }} + minDate={minDate ?? undefined} /> )} /> @@ -166,7 +145,7 @@ export const ModuleForm: React.FC = ({ handleFormSubmit, handleClose, sta
Cancel - + {status ? isSubmitting ? "Updating Module..." diff --git a/apps/app/components/modules/sidebar.tsx b/apps/app/components/modules/sidebar.tsx index 1e63f960a..0407b95aa 100644 --- a/apps/app/components/modules/sidebar.tsx +++ b/apps/app/components/modules/sidebar.tsx @@ -37,7 +37,7 @@ import { LinkIcon } from "@heroicons/react/20/solid"; import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper"; import { capitalizeFirstLetter, copyTextToClipboard } from "helpers/string.helper"; // types -import { ICurrentUserResponse, IIssue, IModule, ModuleLink } from "types"; +import { ICurrentUserResponse, IIssue, linkDetails, IModule, ModuleLink } from "types"; // fetch-keys import { MODULE_DETAILS } from "constants/fetch-keys"; // constant @@ -61,6 +61,7 @@ type Props = { export const ModuleDetailsSidebar: React.FC = ({ module, isOpen, moduleIssues, user }) => { const [moduleDeleteModal, setModuleDeleteModal] = useState(false); const [moduleLinkModal, setModuleLinkModal] = useState(false); + const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState(null); const router = useRouter(); const { workspaceSlug, projectId, moduleId } = router.query; @@ -115,6 +116,37 @@ export const ModuleDetailsSidebar: React.FC = ({ module, isOpen, moduleIs }); }; + const handleUpdateLink = async (formData: ModuleLink, linkId: string) => { + if (!workspaceSlug || !projectId || !module) return; + + const payload = { metadata: {}, ...formData }; + + const updatedLinks = module.link_module.map((l) => + l.id === linkId + ? { + ...l, + title: formData.title, + url: formData.url, + } + : l + ); + + mutate( + MODULE_DETAILS(module.id), + (prevData) => ({ ...(prevData as IModule), link_module: updatedLinks }), + false + ); + + await modulesService + .updateModuleLink(workspaceSlug as string, projectId as string, module.id, linkId, payload) + .then((res) => { + mutate(MODULE_DETAILS(module.id)); + }) + .catch((err) => { + console.log(err); + }); + }; + const handleDeleteLink = async (linkId: string) => { if (!workspaceSlug || !projectId || !module) return; @@ -170,12 +202,23 @@ export const ModuleDetailsSidebar: React.FC = ({ module, isOpen, moduleIs ? Math.round((module.completed_issues / module.total_issues) * 100) : null; + const handleEditLink = (link: linkDetails) => { + setSelectedLinkToUpdate(link); + setModuleLinkModal(true); + }; + return ( <> setModuleLinkModal(false)} - onFormSubmit={handleCreateLink} + handleClose={() => { + setModuleLinkModal(false); + setSelectedLinkToUpdate(null); + }} + data={selectedLinkToUpdate} + status={selectedLinkToUpdate ? true : false} + createIssueLink={handleCreateLink} + updateIssueLink={handleUpdateLink} /> = ({ module, isOpen, moduleIs
-
+

Links

diff --git a/apps/app/components/pages/single-page-block.tsx b/apps/app/components/pages/single-page-block.tsx index 8a13e3ea1..0c192990a 100644 --- a/apps/app/components/pages/single-page-block.tsx +++ b/apps/app/components/pages/single-page-block.tsx @@ -456,6 +456,7 @@ export const SinglePageBlock: React.FC = ({ {showBlockDetails ? block.description_html.length > 7 && ( = ({ userProfile }) => ( -
+

Issues by Priority

{userProfile ? ( -
+
{userProfile.priority_distribution.length > 0 ? ( ({ @@ -63,11 +63,11 @@ export const ProfilePriorityDistribution: React.FC = ({ userProfile }) => }} /> ) : ( -
+
)} diff --git a/apps/app/components/profile/overview/state-distribution.tsx b/apps/app/components/profile/overview/state-distribution.tsx index 5d283aeec..77ec8e596 100644 --- a/apps/app/components/profile/overview/state-distribution.tsx +++ b/apps/app/components/profile/overview/state-distribution.tsx @@ -16,9 +16,9 @@ export const ProfileStateDistribution: React.FC = ({ stateDistribution, u if (!userProfile) return null; return ( -
+

Issues by State

-
+
{userProfile.state_distribution.length > 0 ? (
diff --git a/apps/app/components/project/create-project-modal.tsx b/apps/app/components/project/create-project-modal.tsx index c48c8ec55..b6a2be8bb 100644 --- a/apps/app/components/project/create-project-modal.tsx +++ b/apps/app/components/project/create-project-modal.tsx @@ -42,6 +42,7 @@ import { NETWORK_CHOICES } from "constants/project"; type Props = { isOpen: boolean; setIsOpen: React.Dispatch>; + setToFavorite?: boolean; user: ICurrentUserResponse | undefined; }; @@ -74,7 +75,12 @@ const IsGuestCondition: React.FC<{ return null; }; -export const CreateProjectModal: React.FC = ({ isOpen, setIsOpen, user }) => { +export const CreateProjectModal: React.FC = ({ + isOpen, + setIsOpen, + setToFavorite = false, + user, +}) => { const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true); const { setToastAlert } = useToast(); @@ -104,6 +110,29 @@ export const CreateProjectModal: React.FC = ({ isOpen, setIsOpen, user }) reset(defaultValues); }; + const handleAddToFavorites = (projectId: string) => { + if (!workspaceSlug) return; + + mutate( + PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }), + (prevData) => + (prevData ?? []).map((p) => (p.id === projectId ? { ...p, is_favorite: true } : p)), + false + ); + + projectServices + .addProjectToFavorites(workspaceSlug as string, { + project: projectId, + }) + .catch(() => + setToastAlert({ + type: "error", + title: "Error!", + message: "Couldn't remove the project from favorites. Please try again.", + }) + ); + }; + const onSubmit = async (formData: IProject) => { if (!workspaceSlug) return; @@ -125,6 +154,9 @@ export const CreateProjectModal: React.FC = ({ isOpen, setIsOpen, user }) title: "Success!", message: "Project created successfully.", }); + if (setToFavorite) { + handleAddToFavorites(res.id); + } handleClose(); }) .catch((err) => { diff --git a/apps/app/components/project/delete-project-modal.tsx b/apps/app/components/project/delete-project-modal.tsx index 8edca368e..5b271d99d 100644 --- a/apps/app/components/project/delete-project-modal.tsx +++ b/apps/app/components/project/delete-project-modal.tsx @@ -1,9 +1,11 @@ -import React, { useEffect, useState } from "react"; +import React from "react"; import { useRouter } from "next/router"; import { mutate } from "swr"; +// react-hook-form +import { Controller, useForm } from "react-hook-form"; // headless ui import { Dialog, Transition } from "@headlessui/react"; // services @@ -27,6 +29,11 @@ type TConfirmProjectDeletionProps = { user: ICurrentUserResponse | undefined; }; +const defaultValues = { + projectName: "", + confirmDelete: "", +}; + export const DeleteProjectModal: React.FC = ({ isOpen, data, @@ -34,51 +41,41 @@ export const DeleteProjectModal: React.FC = ({ onSuccess, user, }) => { - const [isDeleteLoading, setIsDeleteLoading] = useState(false); - const [confirmProjectName, setConfirmProjectName] = useState(""); - const [confirmDeleteMyProject, setConfirmDeleteMyProject] = useState(false); - const [selectedProject, setSelectedProject] = useState(null); - const router = useRouter(); const { workspaceSlug } = router.query; const { setToastAlert } = useToast(); - const canDelete = confirmProjectName === data?.name && confirmDeleteMyProject; + const { + control, + formState: { isSubmitting }, + handleSubmit, + reset, + watch, + } = useForm({ defaultValues }); - useEffect(() => { - if (data) setSelectedProject(data); - else { - const timer = setTimeout(() => { - setSelectedProject(null); - clearTimeout(timer); - }, 300); - } - }, [data]); + const canDelete = + watch("projectName") === data?.name && watch("confirmDelete") === "delete my project"; const handleClose = () => { - setIsDeleteLoading(false); - const timer = setTimeout(() => { - setConfirmProjectName(""); - setConfirmDeleteMyProject(false); + reset(defaultValues); clearTimeout(timer); }, 350); + onClose(); }; - const handleDeletion = async () => { + const onSubmit = async () => { if (!data || !workspaceSlug || !canDelete) return; - setIsDeleteLoading(true); - await projectService - .deleteProject(workspaceSlug as string, data.id, user) + .deleteProject(workspaceSlug.toString(), data.id, user) .then(() => { handleClose(); mutate( - PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }), + PROJECTS_LIST(workspaceSlug.toString(), { is_favorite: "all" }), (prevData) => prevData?.filter((project: IProject) => project.id !== data.id), false ); @@ -91,8 +88,7 @@ export const DeleteProjectModal: React.FC = ({ title: "Error!", message: "Something went wrong. Please try again later.", }) - ) - .finally(() => setIsDeleteLoading(false)); + ); }; return ( @@ -122,7 +118,7 @@ export const DeleteProjectModal: React.FC = ({ leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > -
+
= ({

Are you sure you want to delete project{" "} - {selectedProject?.name}? - All of the data related to the project will be permanently removed. This - action cannot be undone + {data?.name}? All of the + data related to the project will be permanently removed. This action cannot be + undone

Enter the project name{" "} - - {selectedProject?.name} - {" "} - to continue: + {data?.name} to + continue:

- { - setConfirmProjectName(e.target.value); - }} + ( + + )} />
@@ -167,31 +164,27 @@ export const DeleteProjectModal: React.FC = ({ delete my project{" "} below:

- { - if (e.target.value === "delete my project") { - setConfirmDeleteMyProject(true); - } else { - setConfirmDeleteMyProject(false); - } - }} - name="typeDelete" + ( + + )} />
Cancel - - {isDeleteLoading ? "Deleting..." : "Delete Project"} + + {isSubmitting ? "Deleting..." : "Delete Project"}
-
+
diff --git a/apps/app/components/project/sidebar-list.tsx b/apps/app/components/project/sidebar-list.tsx index 29dafd6cf..266b05204 100644 --- a/apps/app/components/project/sidebar-list.tsx +++ b/apps/app/components/project/sidebar-list.tsx @@ -13,7 +13,7 @@ import useTheme from "hooks/use-theme"; import useUserAuth from "hooks/use-user-auth"; import useProjects from "hooks/use-projects"; // components -import { DeleteProjectModal, SingleSidebarProject } from "components/project"; +import { CreateProjectModal, DeleteProjectModal, SingleSidebarProject } from "components/project"; // services import projectService from "services/project.service"; // icons @@ -32,6 +32,7 @@ import { useMobxStore } from "lib/mobx/store-provider"; export const ProjectSidebarList: FC = () => { const store: any = useMobxStore(); + const [isProjectModalOpen, setIsProjectModalOpen] = useState(false); const [deleteProjectModal, setDeleteProjectModal] = useState(false); const [projectToDelete, setProjectToDelete] = useState(null); @@ -151,6 +152,12 @@ export const ProjectSidebarList: FC = () => { return ( <> + setDeleteProjectModal(false)} @@ -172,17 +179,25 @@ export const ProjectSidebarList: FC = () => { {({ open }) => ( <> {!store?.theme?.sidebarCollapsed && ( - - Favorites - - +
+ + Favorites + + + +
)} {orderedFavProjects.map((project, index) => ( @@ -241,10 +256,7 @@ export const ProjectSidebarList: FC = () => { diff --git a/apps/app/components/tiptap/bubble-menu/link-selector.tsx b/apps/app/components/tiptap/bubble-menu/link-selector.tsx index 4a2859155..1596870f7 100644 --- a/apps/app/components/tiptap/bubble-menu/link-selector.tsx +++ b/apps/app/components/tiptap/bubble-menu/link-selector.tsx @@ -1,17 +1,27 @@ import { Editor } from "@tiptap/core"; import { Check, Trash } from "lucide-react"; -import { Dispatch, FC, SetStateAction, useEffect, useRef } from "react"; +import { Dispatch, FC, SetStateAction, useCallback, useEffect, useRef } from "react"; import { cn } from "../utils"; - +import isValidHttpUrl from "./utils/link-validator"; interface LinkSelectorProps { editor: Editor; isOpen: boolean; setIsOpen: Dispatch>; } + export const LinkSelector: FC = ({ editor, isOpen, setIsOpen }) => { const inputRef = useRef(null); + const onLinkSubmit = useCallback(() => { + const input = inputRef.current; + const url = input?.value; + if (url && isValidHttpUrl(url)) { + editor.chain().focus().setLink({ href: url }).run(); + setIsOpen(false); + } + }, [editor, inputRef, setIsOpen]); + useEffect(() => { inputRef.current && inputRef.current?.focus(); }); @@ -38,15 +48,13 @@ export const LinkSelector: FC = ({ editor, isOpen, setIsOpen

{isOpen && ( -
{ - e.preventDefault(); - const form = e.target as HTMLFormElement; - const input = form.elements[0] as HTMLInputElement; - editor.chain().focus().setLink({ href: input.value }).run(); - setIsOpen(false); - }} +
{ + if (e.key === "Enter") { + e.preventDefault(); onLinkSubmit(); + } + }} > = ({ editor, isOpen, setIsOpen /> {editor.getAttributes("link").href ? ( ) : ( - )} - +
)}
); diff --git a/apps/app/components/tiptap/bubble-menu/utils/link-validator.tsx b/apps/app/components/tiptap/bubble-menu/utils/link-validator.tsx new file mode 100644 index 000000000..5b05811d6 --- /dev/null +++ b/apps/app/components/tiptap/bubble-menu/utils/link-validator.tsx @@ -0,0 +1,12 @@ +export default function isValidHttpUrl(string: string): boolean { + let url; + + try { + url = new URL(string); + } catch (_) { + return false; + } + + return url.protocol === "http:" || url.protocol === "https:"; +} + diff --git a/apps/app/components/tiptap/extensions/image-resize.tsx b/apps/app/components/tiptap/extensions/image-resize.tsx new file mode 100644 index 000000000..7b2d1a2d3 --- /dev/null +++ b/apps/app/components/tiptap/extensions/image-resize.tsx @@ -0,0 +1,57 @@ +import { Editor } from "@tiptap/react"; +import Moveable from "react-moveable"; + +export const ImageResizer = ({ editor }: { editor: Editor }) => { + const updateMediaSize = () => { + const imageInfo = document.querySelector( + ".ProseMirror-selectednode", + ) as HTMLImageElement; + if (imageInfo) { + const selection = editor.state.selection; + editor.commands.setImage({ + src: imageInfo.src, + width: Number(imageInfo.style.width.replace("px", "")), + height: Number(imageInfo.style.height.replace("px", "")), + } as any); + editor.commands.setNodeSelection(selection.from); + } + }; + + return ( + <> + { + delta[0] && (target!.style.width = `${width}px`); + delta[1] && (target!.style.height = `${height}px`); + }} + onResizeEnd={() => { + updateMediaSize(); + }} + scalable={true} + renderDirections={["w", "e"]} + onScale={({ + target, + transform, + }: + any) => { + target!.style.transform = transform; + }} + /> + + ); +}; + diff --git a/apps/app/components/tiptap/extensions/index.tsx b/apps/app/components/tiptap/extensions/index.tsx index 45dee0929..2c5ffd10a 100644 --- a/apps/app/components/tiptap/extensions/index.tsx +++ b/apps/app/components/tiptap/extensions/index.tsx @@ -1,7 +1,6 @@ import StarterKit from "@tiptap/starter-kit"; import HorizontalRule from "@tiptap/extension-horizontal-rule"; import TiptapLink from "@tiptap/extension-link"; -import TiptapImage from "@tiptap/extension-image"; import Placeholder from "@tiptap/extension-placeholder"; import TiptapUnderline from "@tiptap/extension-underline"; import TextStyle from "@tiptap/extension-text-style"; @@ -18,18 +17,13 @@ import { InputRule } from "@tiptap/core"; import ts from "highlight.js/lib/languages/typescript"; import "highlight.js/styles/github-dark.css"; -import UploadImagesPlugin from "../plugins/upload-image"; import UniqueID from "@tiptap-pro/extension-unique-id"; +import UpdatedImage from "./updated-image"; +import isValidHttpUrl from "../bubble-menu/utils/link-validator"; lowlight.registerLanguage("ts", ts); -const CustomImage = TiptapImage.extend({ - addProseMirrorPlugins() { - return [UploadImagesPlugin()]; - }, -}); - -export const TiptapExtensions = [ +export const TiptapExtensions = (workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) => [ StarterKit.configure({ bulletList: { HTMLAttributes: { @@ -93,13 +87,14 @@ export const TiptapExtensions = [ }, }), TiptapLink.configure({ + protocols: ["http", "https"], + validate: (url) => isValidHttpUrl(url), HTMLAttributes: { class: "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer", }, }), - CustomImage.configure({ - allowBase64: true, + UpdatedImage.configure({ HTMLAttributes: { class: "rounded-lg border border-custom-border-300", }, @@ -117,7 +112,7 @@ export const TiptapExtensions = [ UniqueID.configure({ types: ["image"], }), - SlashCommand, + SlashCommand(workspaceSlug, setIsSubmitting), TiptapUnderline, TextStyle, Color, diff --git a/apps/app/components/tiptap/extensions/updated-image.tsx b/apps/app/components/tiptap/extensions/updated-image.tsx new file mode 100644 index 000000000..01648dcd7 --- /dev/null +++ b/apps/app/components/tiptap/extensions/updated-image.tsx @@ -0,0 +1,22 @@ +import Image from "@tiptap/extension-image"; +import TrackImageDeletionPlugin from "../plugins/delete-image"; +import UploadImagesPlugin from "../plugins/upload-image"; + +const UpdatedImage = Image.extend({ + addProseMirrorPlugins() { + return [UploadImagesPlugin(), TrackImageDeletionPlugin()]; + }, + addAttributes() { + return { + ...this.parent?.(), + width: { + default: '35%', + }, + height: { + default: null, + }, + }; + }, +}); + +export default UpdatedImage; diff --git a/apps/app/components/tiptap/index.tsx b/apps/app/components/tiptap/index.tsx index 0ecaf69b0..418449c08 100644 --- a/apps/app/components/tiptap/index.tsx +++ b/apps/app/components/tiptap/index.tsx @@ -1,14 +1,10 @@ -// @ts-nocheck import { useEditor, EditorContent, Editor } from "@tiptap/react"; import { useDebouncedCallback } from "use-debounce"; import { EditorBubbleMenu } from "./bubble-menu"; import { TiptapExtensions } from "./extensions"; import { TiptapEditorProps } from "./props"; -import { Node } from "@tiptap/pm/model"; -import { Editor as CoreEditor } from "@tiptap/core"; -import { useCallback, useImperativeHandle, useRef } from "react"; -import { EditorState } from "@tiptap/pm/state"; -import fileService from "services/file.service"; +import { useImperativeHandle, useRef } from "react"; +import { ImageResizer } from "./extensions/image-resize"; export interface ITiptapRichTextEditor { value: string; @@ -18,6 +14,8 @@ export interface ITiptapRichTextEditor { editorContentCustomClassNames?: string; onChange?: (json: any, html: string) => void; setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void; + setShouldShowAlert?: (showAlert: boolean) => void; + workspaceSlug: string; editable?: boolean; forwardedRef?: any; debouncedUpdatesEnabled?: boolean; @@ -30,22 +28,24 @@ const Tiptap = (props: ITiptapRichTextEditor) => { forwardedRef, editable, setIsSubmitting, + setShouldShowAlert, editorContentCustomClassNames, value, noBorder, + workspaceSlug, borderOnFocus, customClassName, } = props; const editor = useEditor({ editable: editable ?? true, - editorProps: TiptapEditorProps, - extensions: TiptapExtensions, + editorProps: TiptapEditorProps(workspaceSlug, setIsSubmitting), + extensions: TiptapExtensions(workspaceSlug, setIsSubmitting), content: value, onUpdate: async ({ editor }) => { // for instant feedback loop setIsSubmitting?.("submitting"); - checkForNodeDeletions(editor); + setShouldShowAlert?.(true); if (debouncedUpdatesEnabled) { debouncedUpdates({ onChange, editor }); } else { @@ -65,45 +65,6 @@ const Tiptap = (props: ITiptapRichTextEditor) => { }, })); - const previousState = useRef(); - - const onNodeDeleted = useCallback(async (node: Node) => { - if (node.type.name === "image") { - const assetUrlWithWorkspaceId = new URL(node.attrs.src).pathname.substring(1); - const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId); - if (resStatus === 204) { - console.log("file deleted successfully"); - } - } - }, []); - - const checkForNodeDeletions = useCallback( - (editor: CoreEditor) => { - const prevNodesById: Record = {}; - previousState.current?.doc.forEach((node) => { - if (node.attrs.id) { - prevNodesById[node.attrs.id] = node; - } - }); - - const nodesById: Record = {}; - editor.state?.doc.forEach((node) => { - if (node.attrs.id) { - nodesById[node.attrs.id] = node; - } - }); - - previousState.current = editor.state; - - for (const [id, node] of Object.entries(prevNodesById)) { - if (nodesById[id] === undefined) { - onNodeDeleted(node); - } - } - }, - [onNodeDeleted] - ); - const debouncedUpdates = useDebouncedCallback(async ({ onChange, editor }) => { setTimeout(async () => { if (onChange) { @@ -112,10 +73,9 @@ const Tiptap = (props: ITiptapRichTextEditor) => { }, 500); }, 1000); - const editorClassNames = `relative w-full max-w-screen-lg sm:rounded-lg sm:shadow-lg mt-2 p-3 relative focus:outline-none rounded-md - ${noBorder ? "" : "border border-custom-border-200"} ${ - borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0" - } ${customClassName}`; + const editorClassNames = `relative w-full max-w-screen-lg sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md + ${noBorder ? "" : "border border-custom-border-200"} ${borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0" + } ${customClassName}`; if (!editor) return null; editorRef.current = editor; @@ -131,6 +91,7 @@ const Tiptap = (props: ITiptapRichTextEditor) => { {editor && }
+ {editor?.isActive("image") && }
); diff --git a/apps/app/components/tiptap/plugins/delete-image.tsx b/apps/app/components/tiptap/plugins/delete-image.tsx new file mode 100644 index 000000000..57ab65c63 --- /dev/null +++ b/apps/app/components/tiptap/plugins/delete-image.tsx @@ -0,0 +1,56 @@ +import { Plugin, PluginKey } from "@tiptap/pm/state"; +import { Node as ProseMirrorNode } from '@tiptap/pm/model'; +import fileService from "services/file.service"; + +const deleteKey = new PluginKey("delete-image"); + +const TrackImageDeletionPlugin = () => + new Plugin({ + key: deleteKey, + appendTransaction: (transactions, oldState, newState) => { + transactions.forEach((transaction) => { + if (!transaction.docChanged) return; + + const removedImages: ProseMirrorNode[] = []; + + oldState.doc.descendants((oldNode, oldPos) => { + if (oldNode.type.name !== 'image') return; + + if (!newState.doc.resolve(oldPos).parent) return; + const newNode = newState.doc.nodeAt(oldPos); + + // Check if the node has been deleted or replaced + if (!newNode || newNode.type.name !== 'image') { + // Check if the node still exists elsewhere in the document + let nodeExists = false; + newState.doc.descendants((node) => { + if (node.attrs.id === oldNode.attrs.id) { + nodeExists = true; + } + }); + + if (!nodeExists) { + removedImages.push(oldNode as ProseMirrorNode); + } + } + }); + + removedImages.forEach((node) => { + const src = node.attrs.src; + onNodeDeleted(src); + }); + }); + + return null; + }, + }); + +export default TrackImageDeletionPlugin; + +async function onNodeDeleted(src: string) { + const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1); + const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId); + if (resStatus === 204) { + console.log("Image deleted successfully"); + } +} diff --git a/apps/app/components/tiptap/plugins/upload-image.tsx b/apps/app/components/tiptap/plugins/upload-image.tsx index ed44aa379..0657bc82b 100644 --- a/apps/app/components/tiptap/plugins/upload-image.tsx +++ b/apps/app/components/tiptap/plugins/upload-image.tsx @@ -57,7 +57,7 @@ function findPlaceholder(state: EditorState, id: {}) { return found.length ? found[0].from : null; } -export async function startImageUpload(file: File, view: EditorView, pos: number) { +export async function startImageUpload(file: File, view: EditorView, pos: number, workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) { if (!file.type.includes("image/")) { return; } else if (file.size / 1024 / 1024 > 20) { @@ -82,7 +82,11 @@ export async function startImageUpload(file: File, view: EditorView, pos: number view.dispatch(tr); }; - const src = await UploadImageHandler(file); + if (!workspaceSlug) { + return; + } + setIsSubmitting?.("submitting") + const src = await UploadImageHandler(file, workspaceSlug); const { schema } = view.state; pos = findPlaceholder(view.state, id); @@ -96,7 +100,10 @@ export async function startImageUpload(file: File, view: EditorView, pos: number view.dispatch(transaction); } -const UploadImageHandler = (file: File): Promise => { +const UploadImageHandler = (file: File, workspaceSlug: string): Promise => { + if (!workspaceSlug) { + return Promise.reject("Workspace slug is missing"); + } try { const formData = new FormData(); formData.append("asset", file); @@ -104,7 +111,7 @@ const UploadImageHandler = (file: File): Promise => { return new Promise(async (resolve, reject) => { const imageUrl = await fileService - .uploadFile("plane", formData) + .uploadFile(workspaceSlug, formData) .then((response) => response.asset); const image = new Image(); diff --git a/apps/app/components/tiptap/props.tsx b/apps/app/components/tiptap/props.tsx index 1ffbebe6d..d50fc29b0 100644 --- a/apps/app/components/tiptap/props.tsx +++ b/apps/app/components/tiptap/props.tsx @@ -1,56 +1,56 @@ import { EditorProps } from "@tiptap/pm/view"; import { startImageUpload } from "./plugins/upload-image"; -export const TiptapEditorProps: EditorProps = { - attributes: { - class: `prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none`, - }, - handleDOMEvents: { - keydown: (_view, event) => { - // prevent default event listeners from firing when slash command is active - if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) { - const slashCommand = document.querySelector("#slash-command"); - if (slashCommand) { - return true; - } - } +export function TiptapEditorProps(workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void): EditorProps { + return { + attributes: { + class: `prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none`, }, - }, - handlePaste: (view, event) => { - if ( - event.clipboardData && - event.clipboardData.files && - event.clipboardData.files[0] - ) { - event.preventDefault(); - const file = event.clipboardData.files[0]; - const pos = view.state.selection.from; - - startImageUpload(file, view, pos); - return true; - } - return false; - }, - handleDrop: (view, event, _slice, moved) => { - if ( - !moved && - event.dataTransfer && - event.dataTransfer.files && - event.dataTransfer.files[0] - ) { - event.preventDefault(); - const file = event.dataTransfer.files[0]; - const coordinates = view.posAtCoords({ - left: event.clientX, - top: event.clientY, - }); - // here we deduct 1 from the pos or else the image will create an extra node - if (coordinates) { - startImageUpload(file, view, coordinates.pos - 1); + handleDOMEvents: { + keydown: (_view, event) => { + // prevent default event listeners from firing when slash command is active + if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) { + const slashCommand = document.querySelector("#slash-command"); + if (slashCommand) { + return true; + } + } + }, + }, + handlePaste: (view, event) => { + if ( + event.clipboardData && + event.clipboardData.files && + event.clipboardData.files[0] + ) { + event.preventDefault(); + const file = event.clipboardData.files[0]; + const pos = view.state.selection.from; + startImageUpload(file, view, pos, workspaceSlug, setIsSubmitting); + return true; } - return true; - } - return false; - }, -}; - + return false; + }, + handleDrop: (view, event, _slice, moved) => { + if ( + !moved && + event.dataTransfer && + event.dataTransfer.files && + event.dataTransfer.files[0] + ) { + event.preventDefault(); + const file = event.dataTransfer.files[0]; + const coordinates = view.posAtCoords({ + left: event.clientX, + top: event.clientY, + }); + // here we deduct 1 from the pos or else the image will create an extra node + if (coordinates) { + startImageUpload(file, view, coordinates.pos - 1, workspaceSlug, setIsSubmitting); + } + return true; + } + return false; + }, + }; +} diff --git a/apps/app/components/tiptap/slash-command/index.tsx b/apps/app/components/tiptap/slash-command/index.tsx index 7c686e06b..38f5c9c0a 100644 --- a/apps/app/components/tiptap/slash-command/index.tsx +++ b/apps/app/components/tiptap/slash-command/index.tsx @@ -52,7 +52,7 @@ const Command = Extension.create({ }, }); -const getSuggestionItems = ({ query }: { query: string }) => +const getSuggestionItems = (workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) => ({ query }: { query: string }) => [ { title: "Text", @@ -163,7 +163,7 @@ const getSuggestionItems = ({ query }: { query: string }) => if (input.files?.length) { const file = input.files[0]; const pos = editor.view.state.selection.from; - startImageUpload(file, editor.view, pos); + startImageUpload(file, editor.view, pos, workspaceSlug, setIsSubmitting); } }; input.click(); @@ -328,11 +328,12 @@ const renderItems = () => { }; }; -const SlashCommand = Command.configure({ - suggestion: { - items: getSuggestionItems, - render: renderItems, - }, -}); +export const SlashCommand = (workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) => + Command.configure({ + suggestion: { + items: getSuggestionItems(workspaceSlug, setIsSubmitting), + render: renderItems, + }, + }); export default SlashCommand; diff --git a/apps/app/components/ui/avatar.tsx b/apps/app/components/ui/avatar.tsx index 8cd09fe6e..d60baa56e 100644 --- a/apps/app/components/ui/avatar.tsx +++ b/apps/app/components/ui/avatar.tsx @@ -106,7 +106,7 @@ export const AssigneesList: React.FC = ({ ))} {users.length > length ? (
-
+
{users.length - length}
diff --git a/apps/app/components/ui/datepicker.tsx b/apps/app/components/ui/datepicker.tsx index 831baec51..3bb0d3a1d 100644 --- a/apps/app/components/ui/datepicker.tsx +++ b/apps/app/components/ui/datepicker.tsx @@ -8,10 +8,13 @@ type Props = { renderAs?: "input" | "button"; value: Date | string | null | undefined; onChange: (val: string | null) => void; + handleOnOpen?: () => void; + handleOnClose?: () => void; placeholder?: string; displayShortForm?: boolean; error?: boolean; noBorder?: boolean; + wrapperClassName?: string; className?: string; isClearable?: boolean; disabled?: boolean; @@ -23,10 +26,13 @@ export const CustomDatePicker: React.FC = ({ renderAs = "button", value, onChange, + handleOnOpen, + handleOnClose, placeholder = "Select date", displayShortForm = false, error = false, noBorder = false, + wrapperClassName = "", className = "", isClearable = true, disabled = false, @@ -40,6 +46,9 @@ export const CustomDatePicker: React.FC = ({ if (!val) onChange(null); else onChange(renderDateFormat(val)); }} + onCalendarOpen={handleOnOpen} + onCalendarClose={handleOnClose} + wrapperClassName={wrapperClassName} className={`${ renderAs === "input" ? "block px-2 py-2 text-sm focus:outline-none" diff --git a/apps/app/components/ui/dropdowns/context-menu.tsx b/apps/app/components/ui/dropdowns/context-menu.tsx index d7ecb4de7..78df25ec9 100644 --- a/apps/app/components/ui/dropdowns/context-menu.tsx +++ b/apps/app/components/ui/dropdowns/context-menu.tsx @@ -1,47 +1,76 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useRef } from "react"; import Link from "next/link"; +// hooks +import useOutsideClickDetector from "hooks/use-outside-click-detector"; + type Props = { - position: { - x: number; - y: number; - }; + clickEvent: React.MouseEvent | null; children: React.ReactNode; title?: string | JSX.Element; isOpen: boolean; setIsOpen: React.Dispatch>; }; -const ContextMenu = ({ position, children, title, isOpen, setIsOpen }: Props) => { +const ContextMenu = ({ clickEvent, children, title, isOpen, setIsOpen }: Props) => { + const contextMenuRef = useRef(null); + + // Close the context menu when clicked outside + useOutsideClickDetector(contextMenuRef, () => { + if (isOpen) setIsOpen(false); + }); + useEffect(() => { const hideContextMenu = () => { if (isOpen) setIsOpen(false); }; - window.addEventListener("click", hideContextMenu); - window.addEventListener("keydown", (e: KeyboardEvent) => { + const escapeKeyEvent = (e: KeyboardEvent) => { if (e.key === "Escape") hideContextMenu(); - }); + }; + + window.addEventListener("click", hideContextMenu); + window.addEventListener("keydown", escapeKeyEvent); return () => { window.removeEventListener("click", hideContextMenu); - window.removeEventListener("keydown", hideContextMenu); + window.removeEventListener("keydown", escapeKeyEvent); }; }, [isOpen, setIsOpen]); + useEffect(() => { + const contextMenu = contextMenuRef.current; + + if (contextMenu && isOpen) { + const contextMenuWidth = contextMenu.clientWidth; + const contextMenuHeight = contextMenu.clientHeight; + + const clickX = clickEvent?.pageX || 0; + const clickY = clickEvent?.pageY || 0; + + let top = clickY; + // check if there's enough space at the bottom, otherwise show at the top + if (clickY + contextMenuHeight > window.innerHeight) top = clickY - contextMenuHeight; + + // check if there's enough space on the right, otherwise show on the left + let left = clickX; + if (clickX + contextMenuWidth > window.innerWidth) left = clickX - contextMenuWidth; + + contextMenu.style.top = `${top}px`; + contextMenu.style.left = `${left}px`; + } + }, [clickEvent, isOpen]); + return (
{title && (

diff --git a/apps/app/components/ui/input/index.tsx b/apps/app/components/ui/input/index.tsx index d00d1386f..867712a29 100644 --- a/apps/app/components/ui/input/index.tsx +++ b/apps/app/components/ui/input/index.tsx @@ -29,9 +29,9 @@ export const Input: React.FC = ({ type={type} id={id} value={value} - {...(register && register(name, validations))} + {...(register && register(name ?? "", validations))} onChange={(e) => { - register && register(name).onChange(e); + register && register(name ?? "").onChange(e); onChange && onChange(e); }} className={`block rounded-md bg-transparent text-sm focus:outline-none placeholder-custom-text-400 ${ diff --git a/apps/app/components/ui/input/types.d.ts b/apps/app/components/ui/input/types.d.ts index 8207006df..77a48b4b9 100644 --- a/apps/app/components/ui/input/types.d.ts +++ b/apps/app/components/ui/input/types.d.ts @@ -3,7 +3,7 @@ import type { UseFormRegister, RegisterOptions } from "react-hook-form"; export interface Props extends React.ComponentPropsWithoutRef<"input"> { label?: string; - name: string; + name?: string; value?: string | number | readonly string[]; mode?: "primary" | "transparent" | "trueTransparent" | "secondary" | "disabled"; register?: UseFormRegister; diff --git a/apps/app/components/ui/labels-list.tsx b/apps/app/components/ui/labels-list.tsx index 90658dd62..ba130acca 100644 --- a/apps/app/components/ui/labels-list.tsx +++ b/apps/app/components/ui/labels-list.tsx @@ -1,7 +1,11 @@ import React from "react"; +// ui +import { Tooltip } from "components/ui"; +// types +import { IIssueLabels } from "types"; type IssueLabelsListProps = { - labels?: (string | undefined)[]; + labels?: (IIssueLabels | undefined)[]; length?: number; showLength?: boolean; }; @@ -14,18 +18,16 @@ export const IssueLabelsList: React.FC = ({ <> {labels && ( <> - {labels.slice(0, length).map((color, index) => ( -
- + l?.name).join(", ")} + > +
+ + {`${labels.length} Labels`}
- ))} - {labels.length > length ? +{labels.length - length} : null} +
)} diff --git a/apps/app/components/ui/profile-empty-state.tsx b/apps/app/components/ui/profile-empty-state.tsx index 12af25794..35ac66856 100644 --- a/apps/app/components/ui/profile-empty-state.tsx +++ b/apps/app/components/ui/profile-empty-state.tsx @@ -11,8 +11,8 @@ type Props = { export const ProfileEmptyState: React.FC = ({ title, description, image }) => (
-
- {title} +
+ {title}
{title}
{description &&

{description}

} diff --git a/apps/app/components/workspace/delete-workspace-modal.tsx b/apps/app/components/workspace/delete-workspace-modal.tsx index b896a87e7..7ba8c0eff 100644 --- a/apps/app/components/workspace/delete-workspace-modal.tsx +++ b/apps/app/components/workspace/delete-workspace-modal.tsx @@ -1,9 +1,11 @@ -import React, { useEffect, useState } from "react"; +import React from "react"; import { useRouter } from "next/router"; import { mutate } from "swr"; +// react-hook-form +import { Controller, useForm } from "react-hook-form"; // headless ui import { Dialog, Transition } from "@headlessui/react"; // services @@ -26,57 +28,63 @@ type Props = { user: ICurrentUserResponse | undefined; }; +const defaultValues = { + workspaceName: "", + confirmDelete: "", +}; + export const DeleteWorkspaceModal: React.FC = ({ isOpen, data, onClose, user }) => { - const [isDeleteLoading, setIsDeleteLoading] = useState(false); - - const [confirmWorkspaceName, setConfirmWorkspaceName] = useState(""); - const [confirmDeleteMyWorkspace, setConfirmDeleteMyWorkspace] = useState(false); - - const [selectedWorkspace, setSelectedWorkspace] = useState(null); - const router = useRouter(); + const { setToastAlert } = useToast(); - useEffect(() => { - if (data) setSelectedWorkspace(data); - else { - const timer = setTimeout(() => { - setSelectedWorkspace(null); - clearTimeout(timer); - }, 350); - } - }, [data]); + const { + control, + formState: { isSubmitting }, + handleSubmit, + reset, + watch, + } = useForm({ defaultValues }); - const canDelete = confirmWorkspaceName === data?.name && confirmDeleteMyWorkspace; + const canDelete = + watch("workspaceName") === data?.name && watch("confirmDelete") === "delete my workspace"; const handleClose = () => { - setIsDeleteLoading(false); - setConfirmWorkspaceName(""); - setConfirmDeleteMyWorkspace(false); + const timer = setTimeout(() => { + reset(defaultValues); + clearTimeout(timer); + }, 350); + onClose(); }; - const handleDeletion = async () => { - setIsDeleteLoading(true); + const onSubmit = async () => { if (!data || !canDelete) return; + await workspaceService .deleteWorkspace(data.slug, user) .then(() => { handleClose(); + router.push("/"); + mutate(USER_WORKSPACES, (prevData) => prevData?.filter((workspace) => workspace.id !== data.id) ); + setToastAlert({ type: "success", - message: "Workspace deleted successfully", - title: "Success", + title: "Success!", + message: "Workspace deleted successfully.", }); }) - .catch((error) => { - console.log(error); - setIsDeleteLoading(false); - }); + .catch(() => + setToastAlert({ + type: "error", + title: "Error!", + message: "Something went wrong. Please try again later.", + }) + ); }; return ( @@ -106,7 +114,7 @@ export const DeleteWorkspaceModal: React.FC = ({ isOpen, data, onClose, u leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > -
+
= ({ isOpen, data, onClose, u

Enter the workspace name{" "} - - {selectedWorkspace?.name} - {" "} - to continue: + {data?.name} to + continue:

- { - setConfirmWorkspaceName(e.target.value); - }} + ( + + )} />
@@ -154,28 +163,28 @@ export const DeleteWorkspaceModal: React.FC = ({ isOpen, data, onClose, u delete my workspace{" "} below:

- { - if (e.target.value === "delete my workspace") { - setConfirmDeleteMyWorkspace(true); - } else { - setConfirmDeleteMyWorkspace(false); - } - }} - name="typeDelete" + ( + + )} />
Cancel - - {isDeleteLoading ? "Deleting..." : "Delete Workspace"} + + {isSubmitting ? "Deleting..." : "Delete Workspace"}
-
+
diff --git a/apps/app/components/workspace/issues-list.tsx b/apps/app/components/workspace/issues-list.tsx index baf61da73..0cb8416c0 100644 --- a/apps/app/components/workspace/issues-list.tsx +++ b/apps/app/components/workspace/issues-list.tsx @@ -45,12 +45,14 @@ export const IssuesList: React.FC = ({ issues, type }) => { >

{type}

Issue

-

Due Date

+

{type === "overdue" ? "Due" : "Start"} Date

{issues.length > 0 ? ( issues.map((issue) => { - const dateDifference = getDateDifference(new Date(issue.target_date as string)); + const date = type === "overdue" ? issue.target_date : issue.start_date; + + const dateDifference = getDateDifference(new Date(date as string)); return ( = ({ issues, type }) => {

{truncateText(issue.name, 30)}
- {renderShortDateWithYearFormat(new Date(issue.target_date as string))} + {renderShortDateWithYearFormat(new Date(date?.toString() ?? ""))}
diff --git a/apps/app/constants/analytics.ts b/apps/app/constants/analytics.ts index d152e211b..709b5938a 100644 --- a/apps/app/constants/analytics.ts +++ b/apps/app/constants/analytics.ts @@ -42,10 +42,10 @@ export const ANALYTICS_X_AXIS_VALUES: { value: TXAxisValues; label: string }[] = value: "target_date", label: "Due date", }, - // { - // value: "start_date", - // label: "Start date", - // }, + { + value: "start_date", + label: "Start date", + }, { value: "created_at", label: "Created date", diff --git a/apps/app/contexts/issue-view.context.tsx b/apps/app/contexts/issue-view.context.tsx index dbf1d9a83..530dbee4e 100644 --- a/apps/app/contexts/issue-view.context.tsx +++ b/apps/app/contexts/issue-view.context.tsx @@ -362,7 +362,13 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> = }, }); - if (property === "kanban") { + const additionalProperties = { + groupByProperty: state.groupByProperty, + orderBy: state.orderBy, + }; + + if (property === "kanban" && state.groupByProperty === null) { + additionalProperties.groupByProperty = "state"; dispatch({ type: "SET_GROUP_BY_PROPERTY", payload: { @@ -371,6 +377,7 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> = }); } if (property === "calendar") { + additionalProperties.groupByProperty = null; dispatch({ type: "SET_GROUP_BY_PROPERTY", payload: { @@ -378,13 +385,22 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> = }, }); } + if (property === "gantt_chart") { + additionalProperties.orderBy = "sort_order"; + dispatch({ + type: "SET_ORDER_BY_PROPERTY", + payload: { + orderBy: "sort_order", + }, + }); + } if (!workspaceSlug || !projectId) return; saveDataToServer(workspaceSlug as string, projectId as string, { ...state, issueView: property, - groupByProperty: "state", + ...additionalProperties, }); }, [workspaceSlug, projectId, state] diff --git a/apps/app/hooks/use-outside-click-detector.tsx b/apps/app/hooks/use-outside-click-detector.tsx index f20666f8c..5331d11c8 100644 --- a/apps/app/hooks/use-outside-click-detector.tsx +++ b/apps/app/hooks/use-outside-click-detector.tsx @@ -8,10 +8,10 @@ const useOutsideClickDetector = (ref: React.RefObject, callback: () }; useEffect(() => { - document.addEventListener("click", handleClick); + document.addEventListener("mousedown", handleClick); return () => { - document.removeEventListener("click", handleClick); + document.removeEventListener("mousedown", handleClick); }; }); }; diff --git a/apps/app/package.json b/apps/app/package.json index 89b5de611..578a95716 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -52,10 +52,10 @@ "highlight.js": "^11.8.0", "js-cookie": "^3.0.1", "lodash.debounce": "^4.0.8", - "mobx": "^6.10.0", - "mobx-react-lite": "^4.0.3", "lowlight": "^2.9.0", "lucide-react": "^0.263.1", + "mobx": "^6.10.0", + "mobx-react-lite": "^4.0.3", "next": "12.3.2", "next-pwa": "^5.6.0", "next-themes": "^0.2.1", @@ -68,6 +68,7 @@ "react-dropzone": "^14.2.3", "react-hook-form": "^7.38.0", "react-markdown": "^8.0.7", + "react-moveable": "^0.54.1", "sharp": "^0.32.1", "sonner": "^0.6.2", "swr": "^2.1.3", diff --git a/apps/app/pages/[workspaceSlug]/me/profile/activity.tsx b/apps/app/pages/[workspaceSlug]/me/profile/activity.tsx index 67f3a77ee..148d738c0 100644 --- a/apps/app/pages/[workspaceSlug]/me/profile/activity.tsx +++ b/apps/app/pages/[workspaceSlug]/me/profile/activity.tsx @@ -106,6 +106,7 @@ const ProfileActivity = () => {
- - - + + + diff --git a/apps/app/public/empty-state/empty_graph.svg b/apps/app/public/empty-state/empty_graph.svg new file mode 100644 index 000000000..5eae523a5 --- /dev/null +++ b/apps/app/public/empty-state/empty_graph.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/apps/app/public/empty-state/empty_users.svg b/apps/app/public/empty-state/empty_users.svg new file mode 100644 index 000000000..7298e9e8f --- /dev/null +++ b/apps/app/public/empty-state/empty_users.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/app/public/empty-state/recent_activity.svg b/apps/app/public/empty-state/recent_activity.svg index 48ea2277c..bef318fae 100644 --- a/apps/app/public/empty-state/recent_activity.svg +++ b/apps/app/public/empty-state/recent_activity.svg @@ -1,3 +1,3 @@ - + diff --git a/apps/app/public/empty-state/state_graph.svg b/apps/app/public/empty-state/state_graph.svg index 7265784a8..07337991e 100644 --- a/apps/app/public/empty-state/state_graph.svg +++ b/apps/app/public/empty-state/state_graph.svg @@ -1,26 +1,26 @@ - + - - + + - - + + - - + + - - + + - - + + - + diff --git a/apps/app/services/issues.service.ts b/apps/app/services/issues.service.ts index ea92e6dec..53c6c1a2d 100644 --- a/apps/app/services/issues.service.ts +++ b/apps/app/services/issues.service.ts @@ -459,6 +459,29 @@ class ProjectIssuesServices extends APIService { }); } + async updateIssueLink( + workspaceSlug: string, + projectId: string, + issueId: string, + linkId: string, + data: { + metadata: any; + title: string; + url: string; + }, + + ): Promise { + return this.patch( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-links/${linkId}/`, + data + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async deleteIssueLink( workspaceSlug: string, projectId: string, diff --git a/apps/app/services/modules.service.ts b/apps/app/services/modules.service.ts index 357b92fa3..c70066a1e 100644 --- a/apps/app/services/modules.service.ts +++ b/apps/app/services/modules.service.ts @@ -212,6 +212,28 @@ class ProjectIssuesServices extends APIService { }); } + async updateModuleLink( + workspaceSlug: string, + projectId: string, + moduleId: string, + linkId: string, + data: { + metadata: any; + title: string; + url: string; + }, + + ): Promise { + return this.patch( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-links/${linkId}/`, + data + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + async deleteModuleLink( workspaceSlug: string, projectId: string, diff --git a/apps/app/styles/editor.css b/apps/app/styles/editor.css index 05817c0dc..57c23c911 100644 --- a/apps/app/styles/editor.css +++ b/apps/app/styles/editor.css @@ -126,3 +126,27 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p { transition: opacity 0.2s ease-out; } +.img-placeholder { + position: relative; + width: 35%; + + &:before { + content: ""; + box-sizing: border-box; + position: absolute; + top: 50%; + left: 45%; + width: 20px; + height: 20px; + border-radius: 50%; + border: 3px solid rgba(var(--color-text-200)); + border-top-color: rgba(var(--color-text-800)); + animation: spinning 0.6s linear infinite; + } +} + +@keyframes spinning { + to { + transform: rotate(360deg); + } +} diff --git a/apps/app/types/issues.d.ts b/apps/app/types/issues.d.ts index 683704f9f..9cbcef847 100644 --- a/apps/app/types/issues.d.ts +++ b/apps/app/types/issues.d.ts @@ -56,6 +56,16 @@ export interface IIssueLink { url: string; } +export interface linkDetails { + created_at: Date; + created_by: string; + created_by_detail: IUserLite; + id: string; + metadata: any; + title: string; + url: string; +} + export interface IIssue { archived_at: string; assignees: string[]; @@ -80,15 +90,7 @@ export interface IIssue { estimate_point: number | null; id: string; issue_cycle: IIssueCycle | null; - issue_link: { - created_at: Date; - created_by: string; - created_by_detail: IUserLite; - id: string; - metadata: any; - title: string; - url: string; - }[]; + issue_link: linkDetails[]; issue_module: IIssueModule | null; labels: string[]; label_details: any[]; @@ -205,7 +207,8 @@ export interface IIssueLite { id: string; name: string; project_id: string; - target_date: string; + start_date?: string | null; + target_date?: string | null; workspace__slug: string; } diff --git a/apps/app/types/modules.d.ts b/apps/app/types/modules.d.ts index eefd42788..e395f6f16 100644 --- a/apps/app/types/modules.d.ts +++ b/apps/app/types/modules.d.ts @@ -7,6 +7,7 @@ import type { IWorkspaceLite, IProjectLite, IIssueFilterOptions, + linkDetails, } from "types"; export interface IModule { @@ -26,15 +27,7 @@ export interface IModule { id: string; lead: string | null; lead_detail: IUserLite | null; - link_module: { - created_at: Date; - created_by: string; - created_by_detail: IUserLite; - id: string; - metadata: any; - title: string; - url: string; - }[]; + link_module: linkDetails[]; links_list: ModuleLink[]; members: string[]; members_list: string[]; diff --git a/setup.sh b/setup.sh index 8c1f81a48..a5a8e9b6a 100755 --- a/setup.sh +++ b/setup.sh @@ -1,5 +1,5 @@ #!/bin/bash -cp ./.env.example ./.env +# cp ./.env.example ./.env # Export for tr error in mac export LC_ALL=C @@ -14,3 +14,16 @@ echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./.en # WEB_URL for email redirection and image saving echo -e "WEB_URL=$1" >> ./.env + +# Generate Prompt for taking tiptap auth key +echo -e "\n\e[1;38m Instructions for generating TipTap Pro Extensions Auth Token \e[0m \n" + +echo -e "\e[1;38m 1. Head over to TipTap cloud's Pro Extensions Page, https://collab.tiptap.dev/pro-extensions \e[0m" +echo -e "\e[1;38m 2. Copy the token given to you under the first paragraph, after 'Here it is' \e[0m \n" + +read -p $'\e[1;32m Please Enter Your TipTap Pro Extensions Authentication Token: \e[0m \e[1;36m' authToken + + +echo "@tiptap-pro:registry=https://registry.tiptap.dev/ +//registry.tiptap.dev/:_authToken=${authToken}" > .npmrc + diff --git a/yarn.lock b/yarn.lock index b4cf55f2f..8bc1fec30 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1006,6 +1006,40 @@ react-popper "^2.3.0" tslib "~2.5.0" +"@cfcs/core@^0.0.6": + version "0.0.6" + resolved "https://registry.yarnpkg.com/@cfcs/core/-/core-0.0.6.tgz#9f8499dcd2ad29fd96d8fa72055411cd4a249121" + integrity sha512-FxfJMwoLB8MEMConeXUCqtMGqxdtePQxRBOiGip9ULcYYam3WfCgoY6xdnMaSkYvRvmosp5iuG+TiPofm65+Pw== + dependencies: + "@egjs/component" "^3.0.2" + +"@daybrush/utils@^1.1.1", "@daybrush/utils@^1.13.0", "@daybrush/utils@^1.4.0", "@daybrush/utils@^1.6.0", "@daybrush/utils@^1.7.1": + version "1.13.0" + resolved "https://registry.yarnpkg.com/@daybrush/utils/-/utils-1.13.0.tgz#ea70a60864130da476406fdd1d465e3068aea0ff" + integrity sha512-ALK12C6SQNNHw1enXK+UO8bdyQ+jaWNQ1Af7Z3FNxeAwjYhQT7do+TRE4RASAJ3ObaS2+TJ7TXR3oz2Gzbw0PQ== + +"@egjs/agent@^2.2.1": + version "2.4.3" + resolved "https://registry.yarnpkg.com/@egjs/agent/-/agent-2.4.3.tgz#6d44e2fb1ff7bab242c07f82732fe60305ac6f06" + integrity sha512-XvksSENe8wPeFlEVouvrOhKdx8HMniJ3by7sro2uPF3M6QqWwjzVcmvwoPtdjiX8O1lfRoLhQMp1a7NGlVTdIA== + +"@egjs/children-differ@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@egjs/children-differ/-/children-differ-1.0.1.tgz#5465fa80671d5ca3564ebe912f48b05b3e8a14fd" + integrity sha512-DRvyqMf+CPCOzAopQKHtW+X8iN6Hy6SFol+/7zCUiE5y4P/OB8JP8FtU4NxtZwtafvSL4faD5KoQYPj3JHzPFQ== + dependencies: + "@egjs/list-differ" "^1.0.0" + +"@egjs/component@^3.0.2": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@egjs/component/-/component-3.0.4.tgz#ad7b53794b2a612806179a188ad828acb9525f61" + integrity sha512-sXA7bGbIeLF2OAw/vpka66c6QBBUPcA4UUhR4WGJfnp2XWdiI8QrnJGJMr/UxpE/xnevX9tN3jvNPlW8WkHl3g== + +"@egjs/list-differ@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@egjs/list-differ/-/list-differ-1.0.1.tgz#5772b0f8b87973bb67827f6c7d7df8d7f64a22eb" + integrity sha512-OTFTDQcWS+1ZREOdCWuk5hCBgYO4OsD30lXcOCyVOAjXMhgL5rBRDnt/otb6Nz8CzU0L/igdcaQBDLWc4t9gvg== + "@emotion/babel-plugin@^11.11.0": version "11.11.0" resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz#c2d872b6a7767a9d176d007f5b31f7d504bb5d6c" @@ -1990,6 +2024,28 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.3.3.tgz#16ab6c727d8c2020a5b6e4a176a243ecd88d8d69" integrity sha512-0xd7qez0AQ+MbHatZTlI1gu5vkG8r7MYRUJAHPAHJBmGLs16zpkrpAVLvjQKQOqaXPDUBwOiJzNc00znHSCVBw== +"@scena/dragscroll@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@scena/dragscroll/-/dragscroll-1.4.0.tgz#220b2430c16119cd3e70044ee533a5b9a43cffd7" + integrity sha512-3O8daaZD9VXA9CP3dra6xcgt/qrm0mg0xJCwiX6druCteQ9FFsXffkF8PrqxY4Z4VJ58fFKEa0RlKqbsi/XnRA== + dependencies: + "@daybrush/utils" "^1.6.0" + "@scena/event-emitter" "^1.0.2" + +"@scena/event-emitter@^1.0.2", "@scena/event-emitter@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@scena/event-emitter/-/event-emitter-1.0.5.tgz#047e3acef93cf238d7ce3a8cc5a12ec6bd9c3bb1" + integrity sha512-AzY4OTb0+7ynefmWFQ6hxDdk0CySAq/D4efljfhtRHCOP7MBF9zUfhKG3TJiroVjASqVgkRJFdenS8ArZo6Olg== + dependencies: + "@daybrush/utils" "^1.1.1" + +"@scena/matrix@^1.0.0", "@scena/matrix@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@scena/matrix/-/matrix-1.1.1.tgz#5297f71825c72e2c2c8f802f924f482ed200c43c" + integrity sha512-JVKBhN0tm2Srl+Yt+Ywqu0oLgLcdemDQlD1OxmN9jaCTwaFPZ7tY8n6dhVgMEaR9qcR7r+kAlMXnSfNyYdE+Vg== + dependencies: + "@daybrush/utils" "^1.4.0" + "@sentry-internal/tracing@7.63.0": version "7.63.0" resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.63.0.tgz#58903b2205456034611cc5bc1b5b2479275f89c7" @@ -3462,6 +3518,21 @@ css-box-model@^1.2.0: dependencies: tiny-invariant "^1.0.6" +css-styled@^1.0.8, css-styled@~1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/css-styled/-/css-styled-1.0.8.tgz#c9c05dc4abdef5571033090bfb8cfc5e19429974" + integrity sha512-tCpP7kLRI8dI95rCh3Syl7I+v7PP+2JYOzWkl0bUEoSbJM+u8ITbutjlQVf0NC2/g4ULROJPi16sfwDIO8/84g== + dependencies: + "@daybrush/utils" "^1.13.0" + +css-to-mat@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/css-to-mat/-/css-to-mat-1.1.1.tgz#0dd10dcf9ec17df15708c8ff07a74fbd0b9a3fe5" + integrity sha512-kvpxFYZb27jRd2vium35G7q5XZ2WJ9rWjDUMNT36M3Hc41qCrLXFM5iEKMGXcrPsKfXEN+8l/riB4QzwwwiEyQ== + dependencies: + "@daybrush/utils" "^1.13.0" + "@scena/matrix" "^1.0.0" + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" @@ -4484,6 +4555,11 @@ fraction.js@^4.2.0: resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950" integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== +framework-utils@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/framework-utils/-/framework-utils-1.1.0.tgz#a3b528bce838dfd623148847dc92371b09d0da2d" + integrity sha512-KAfqli5PwpFJ8o3psRNs8svpMGyCSAe8nmGcjQ0zZBWN2H6dZDnq+ABp3N3hdUmFeMrLtjOCTXD4yplUJIWceg== + fs-constants@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" @@ -4539,6 +4615,14 @@ gensync@^1.0.0-beta.2: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== +gesto@^1.19.0, gesto@^1.19.1: + version "1.19.1" + resolved "https://registry.yarnpkg.com/gesto/-/gesto-1.19.1.tgz#b2a29730663eecf77b248982bbff929e79d4a461" + integrity sha512-ofWVEdqmnpFm3AFf7aoclhoayseb3OkwSiXbXusKYu/99iN5HgeWP+SWqdghQ5TFlOgP5Zlz+6SY8mP2V0kFaQ== + dependencies: + "@daybrush/utils" "^1.13.0" + "@scena/event-emitter" "^1.0.2" + get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" @@ -5244,6 +5328,21 @@ jsonpointer@^5.0.0: object.assign "^4.1.4" object.values "^1.1.6" +keycode@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.1.tgz#09c23b2be0611d26117ea2501c2c391a01f39eff" + integrity sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg== + +keycon@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/keycon/-/keycon-1.4.0.tgz#bf2a633f3c3b659ea564045938cff33e584cebd5" + integrity sha512-p1NAIxiRMH3jYfTeXRs2uWbVJ1WpEjpi8ktzUyBJsX7/wn2qu2VRXktneBLNtKNxJmlUYxRi9gOJt1DuthXR7A== + dependencies: + "@cfcs/core" "^0.0.6" + "@daybrush/utils" "^1.7.1" + "@scena/event-emitter" "^1.0.2" + keycode "^2.2.0" + kleur@^4.0.3: version "4.1.5" resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" @@ -6081,6 +6180,13 @@ orderedmap@^2.0.0: resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-2.1.1.tgz#61481269c44031c449915497bf5a4ad273c512d2" integrity sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g== +overlap-area@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/overlap-area/-/overlap-area-1.1.0.tgz#1fcaa21bdb9cb1ace973d9aa299ae6b56557a4c2" + integrity sha512-3dlJgJCaVeXH0/eZjYVJvQiLVVrPO4U1ZGqlATtx6QGO3b5eNM6+JgUKa7oStBTdYuGTk7gVoABCW6Tp+dhRdw== + dependencies: + "@daybrush/utils" "^1.7.1" + p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -6452,20 +6558,13 @@ prosemirror-menu@^1.2.1: prosemirror-history "^1.0.0" prosemirror-state "^1.0.0" -prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.18.1, prosemirror-model@^1.8.1: +prosemirror-model@1.18.1, prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.18.1, prosemirror-model@^1.19.0, prosemirror-model@^1.8.1: version "1.18.1" resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.18.1.tgz#1d5d6b6de7b983ee67a479dc607165fdef3935bd" integrity sha512-IxSVBKAEMjD7s3n8cgtwMlxAXZrC7Mlag7zYsAKDndAqnDScvSmp/UdnRTV/B33lTCVU3CCm7dyAn/rVVD0mcw== dependencies: orderedmap "^2.0.0" -prosemirror-model@^1.19.0: - version "1.19.3" - resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.19.3.tgz#f0d55285487fefd962d0ac695f716f4ec6705006" - integrity sha512-tgSnwN7BS7/UM0sSARcW+IQryx2vODKX4MI7xpqY2X+iaepJdKBPc7I4aACIsDV/LTaTjt12Z56MhDr9LsyuZQ== - dependencies: - orderedmap "^2.0.0" - prosemirror-schema-basic@^1.2.0: version "1.2.2" resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.2.tgz#6695f5175e4628aab179bf62e5568628b9cfe6c7" @@ -6603,6 +6702,14 @@ react-color@^2.19.3: reactcss "^1.2.0" tinycolor2 "^1.4.1" +react-css-styled@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/react-css-styled/-/react-css-styled-1.1.9.tgz#a7cc948e49f72b2f7fb1393bd85416a8293afab3" + integrity sha512-M7fJZ3IWFaIHcZEkoFOnkjdiUFmwd8d+gTh2bpqMOcnxy/0Gsykw4dsL4QBiKsxcGow6tETUa4NAUcmJF+/nfw== + dependencies: + css-styled "~1.0.8" + framework-utils "^1.1.0" + react-datepicker@^4.8.0: version "4.16.0" resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-4.16.0.tgz#b9dd389bb5611a1acc514bba1dd944be21dd877f" @@ -6683,6 +6790,25 @@ react-markdown@^8.0.7: unist-util-visit "^4.0.0" vfile "^5.0.0" +react-moveable@^0.54.1: + version "0.54.1" + resolved "https://registry.yarnpkg.com/react-moveable/-/react-moveable-0.54.1.tgz#3c69748c444184700e6999501b0da953c934205e" + integrity sha512-Kj2ifw9nk3LZvu7ezhst8Z5WBPRr+yVv9oROwrBirFlHmwGHHZXUGk5Gaezu+JGqqNRsQJncVMW5Uf68KSSOvg== + dependencies: + "@daybrush/utils" "^1.13.0" + "@egjs/agent" "^2.2.1" + "@egjs/children-differ" "^1.0.1" + "@egjs/list-differ" "^1.0.0" + "@scena/dragscroll" "^1.4.0" + "@scena/event-emitter" "^1.0.5" + "@scena/matrix" "^1.1.1" + css-to-mat "^1.1.1" + framework-utils "^1.1.0" + gesto "^1.19.0" + overlap-area "^1.1.0" + react-css-styled "^1.1.9" + react-selecto "^1.25.0" + react-onclickoutside@^6.12.2: version "6.13.0" resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz#e165ea4e5157f3da94f4376a3ab3e22a565f4ffc" @@ -6740,6 +6866,13 @@ react-remove-scroll@2.5.4: use-callback-ref "^1.3.0" use-sidecar "^1.1.2" +react-selecto@^1.25.0: + version "1.26.0" + resolved "https://registry.yarnpkg.com/react-selecto/-/react-selecto-1.26.0.tgz#9157ff0a732fc426602b30c08ec21b6ca0a9c472" + integrity sha512-aBTZEYA68uE+o8TytNjTb2GpIn4oKEv0U4LIow3cspJQlF/PdAnBwkq9UuiKVuFluu5kfLQ7Keu3S2Tihlmw0g== + dependencies: + selecto "~1.26.0" + react-style-singleton@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" @@ -7023,6 +7156,22 @@ schema-utils@^3.1.1: ajv "^6.12.5" ajv-keywords "^3.5.2" +selecto@~1.26.0: + version "1.26.0" + resolved "https://registry.yarnpkg.com/selecto/-/selecto-1.26.0.tgz#f3f04fb6409112b198243458f6c9963946d5ba2f" + integrity sha512-cEFKdv5rmkF6pf2OScQJllaNp4UJy/FvviB40ZaMSHrQCxC72X/Q6uhzW1tlb2RE+0danvUNJTs64cI9VXtUyg== + dependencies: + "@daybrush/utils" "^1.13.0" + "@egjs/children-differ" "^1.0.1" + "@scena/dragscroll" "^1.4.0" + "@scena/event-emitter" "^1.0.5" + css-styled "^1.0.8" + css-to-mat "^1.1.1" + framework-utils "^1.1.0" + gesto "^1.19.1" + keycon "^1.2.0" + overlap-area "^1.1.0" + semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"