diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index eca18163a..6cca91f63 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( { 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/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 && (
+ = ({ const [contextMenu, setContextMenu] = useState(false); const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 }); const [isMenuActive, setIsMenuActive] = useState(false); + const [isDropdownActive, setIsDropdownActive] = useState(false); const actionSectionRef = useRef(null); @@ -245,7 +246,7 @@ export const SingleBoardIssue: React.FC = ({ setContextMenuPosition({ x: e.pageX, y: e.pageY }); }} > -
+
{!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 +342,8 @@ export const SingleBoardIssue: React.FC = ({ setIsDropdownActive(true)} + handleOnClose={() => setIsDropdownActive(false)} user={user} isNotAllowed={isNotAllowed} /> 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/issues/comment/add-comment.tsx b/apps/app/components/issues/comment/add-comment.tsx index 4db300c0e..9d00bf784 100644 --- a/apps/app/components/issues/comment/add-comment.tsx +++ b/apps/app/components/issues/comment/add-comment.tsx @@ -102,7 +102,7 @@ export const AddComment: React.FC = ({ issueId, user, disabled = false }) ? watch("comment_html") : value } - customClassName="p-3 min-h-[50px]" + customClassName="p-3 min-h-[50px] shadow-sm" debouncedUpdatesEnabled={false} onChange={(comment_json: Object, comment_html: string) => { onChange(comment_html); diff --git a/apps/app/components/issues/comment/comment-card.tsx b/apps/app/components/issues/comment/comment-card.tsx index 8a969f318..ba2eaaab6 100644 --- a/apps/app/components/issues/comment/comment-card.tsx +++ b/apps/app/components/issues/comment/comment-card.tsx @@ -108,7 +108,7 @@ export const CommentCard: React.FC = ({ comment, workspaceSlug, onSubmit, ref={editorRef} value={watch("comment_html")} debouncedUpdatesEnabled={false} - customClassName="min-h-[50px] p-3" + customClassName="min-h-[50px] p-3 shadow-sm" onChange={(comment_json: Object, comment_html: string) => { setValue("comment_json", comment_json); setValue("comment_html", comment_html); diff --git a/apps/app/components/issues/description-form.tsx b/apps/app/components/issues/description-form.tsx index 75f7217e3..82d1d65b1 100644 --- a/apps/app/components/issues/description-form.tsx +++ b/apps/app/components/issues/description-form.tsx @@ -146,7 +146,7 @@ export const IssueDescriptionForm: FC = ({ debouncedUpdatesEnabled={true} setShouldShowAlert={setShowAlert} setIsSubmitting={setIsSubmitting} - customClassName="min-h-[150px]" + customClassName="min-h-[150px] shadow-sm" editorContentCustomClassNames="pb-9" onChange={(description: Object, description_html: string) => { setShowAlert(true); diff --git a/apps/app/components/issues/select/assignee.tsx b/apps/app/components/issues/select/assignee.tsx index 989d071ec..603483f0f 100644 --- a/apps/app/components/issues/select/assignee.tsx +++ b/apps/app/components/issues/select/assignee.tsx @@ -5,9 +5,7 @@ import useSWR from "swr"; // services import projectServices from "services/project.service"; // ui -import { AssigneesList, Avatar, CustomSearchSelect } from "components/ui"; -// icons -import { UserGroupIcon } from "@heroicons/react/24/outline"; +import { AssigneesList, Avatar, CustomSearchSelect, Icon } from "components/ui"; // fetch-keys import { PROJECT_MEMBERS } from "constants/fetch-keys"; @@ -44,15 +42,15 @@ export const IssueAssigneeSelect: React.FC = ({ 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/label.tsx b/apps/app/components/issues/select/label.tsx index 6d7e2f391..b15a6f9c6 100644 --- a/apps/app/components/issues/select/label.tsx +++ b/apps/app/components/issues/select/label.tsx @@ -59,9 +59,9 @@ export const IssueLabelSelect: React.FC = ({ setIsOpen, value, onChange, > {({ open }: any) => ( <> - + {value && value.length > 0 ? ( - + issueLabels?.find((l) => l.id === v)?.color) ?? []} length={3} @@ -69,7 +69,7 @@ export const IssueLabelSelect: React.FC = ({ setIsOpen, value, onChange, /> ) : ( - + 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/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/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/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/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/public/empty-state/priority_graph.svg b/apps/app/public/empty-state/empty_bar_graph.svg similarity index 58% rename from apps/app/public/empty-state/priority_graph.svg rename to apps/app/public/empty-state/empty_bar_graph.svg index 15d8f9b4c..7742a4238 100644 --- a/apps/app/public/empty-state/priority_graph.svg +++ b/apps/app/public/empty-state/empty_bar_graph.svg @@ -1,5 +1,5 @@ - - - + + + 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/types/issues.d.ts b/apps/app/types/issues.d.ts index 683704f9f..f035ae930 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[]; 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/apps/space/app/[workspace_slug]/[project_slug]/page.tsx b/apps/space/app/[workspace_slug]/[project_slug]/page.tsx index 0aa9b164d..f6ccf5081 100644 --- a/apps/space/app/[workspace_slug]/[project_slug]/page.tsx +++ b/apps/space/app/[workspace_slug]/[project_slug]/page.tsx @@ -29,15 +29,41 @@ const WorkspaceProjectPage = observer(() => { // updating default board view when we are in the issues page useEffect(() => { - if (workspace_slug && project_slug) { - if (!board) { - store.issue.setCurrentIssueBoardView("list"); - router.replace(`/${workspace_slug}/${project_slug}?board=${store?.issue?.currentIssueBoardView}`); - } else { - if (board != store?.issue?.currentIssueBoardView) store.issue.setCurrentIssueBoardView(board); + if (workspace_slug && project_slug && store?.project?.workspaceProjectSettings) { + const workspacePRojectSettingViews = store?.project?.workspaceProjectSettings?.views; + const userAccessViews: TIssueBoardKeys[] = []; + + Object.keys(workspacePRojectSettingViews).filter((_key) => { + if (_key === "list" && workspacePRojectSettingViews.list === true) userAccessViews.push(_key); + if (_key === "kanban" && workspacePRojectSettingViews.kanban === true) userAccessViews.push(_key); + if (_key === "calendar" && workspacePRojectSettingViews.calendar === true) userAccessViews.push(_key); + if (_key === "spreadsheet" && workspacePRojectSettingViews.spreadsheet === true) userAccessViews.push(_key); + if (_key === "gantt" && workspacePRojectSettingViews.gantt === true) userAccessViews.push(_key); + }); + + if (userAccessViews && userAccessViews.length > 0) { + if (!board) { + store.issue.setCurrentIssueBoardView(userAccessViews[0]); + router.replace(`/${workspace_slug}/${project_slug}?board=${userAccessViews[0]}`); + } else { + if (userAccessViews.includes(board)) { + if (store.issue.currentIssueBoardView === null) store.issue.setCurrentIssueBoardView(board); + else { + if (board === store.issue.currentIssueBoardView) + router.replace(`/${workspace_slug}/${project_slug}?board=${board}`); + else { + store.issue.setCurrentIssueBoardView(board); + router.replace(`/${workspace_slug}/${project_slug}?board=${board}`); + } + } + } else { + store.issue.setCurrentIssueBoardView(userAccessViews[0]); + router.replace(`/${workspace_slug}/${project_slug}?board=${userAccessViews[0]}`); + } + } } } - }, [workspace_slug, project_slug, board, router, store?.issue]); + }, [workspace_slug, project_slug, board, router, store?.issue, store?.project?.workspaceProjectSettings]); useEffect(() => { if (workspace_slug && project_slug) { diff --git a/apps/space/lib/mobx/store-init.tsx b/apps/space/lib/mobx/store-init.tsx index 2ba2f9024..4f4b6662c 100644 --- a/apps/space/lib/mobx/store-init.tsx +++ b/apps/space/lib/mobx/store-init.tsx @@ -24,11 +24,6 @@ const MobxStoreInit = () => { else localStorage.setItem("app_theme", _theme && _theme != "light" ? "dark" : "light"); }, [store?.theme]); - // updating default board view when we are in the issues page - useEffect(() => { - if (board && board != store?.issue?.currentIssueBoardView) store.issue.setCurrentIssueBoardView(board); - }, [board, store?.issue]); - return <>; }; 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 +