mirror of
https://github.com/makeplane/plane.git
synced 2026-02-24 04:00:14 +01:00
fix: merge conflicts resolved
This commit is contained in:
@@ -1668,15 +1668,9 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
Issue.objects.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(
|
||||
parent=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
Issue.objects.filter(
|
||||
project_id=self.kwargs.get("project_id")
|
||||
)
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(is_draft=True)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
@@ -1710,7 +1704,7 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
)
|
||||
).distinct()
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
def list(self, request, slug, project_id):
|
||||
@@ -1832,7 +1826,10 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
issue = (
|
||||
self.get_queryset().filter(pk=serializer.data["id"]).first()
|
||||
)
|
||||
return Response(IssueSerializer(issue).data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
@@ -1868,10 +1865,13 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def retrieve(self, request, slug, project_id, pk=None):
|
||||
issue = Issue.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk, is_draft=True
|
||||
issue = self.get_queryset().filter(pk=pk).first()
|
||||
return Response(
|
||||
IssueSerializer(
|
||||
issue, fields=self.fields, expand=self.expand
|
||||
).data,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
|
||||
|
||||
def destroy(self, request, slug, project_id, pk=None):
|
||||
issue = Issue.objects.get(
|
||||
|
||||
33
apiserver/plane/db/migrations/0059_auto_20240208_0957.py
Normal file
33
apiserver/plane/db/migrations/0059_auto_20240208_0957.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 4.2.7 on 2024-02-08 09:57
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def widgets_filter_change(apps, schema_editor):
|
||||
Widget = apps.get_model("db", "Widget")
|
||||
widgets_to_update = []
|
||||
|
||||
# Define the filter dictionaries for each widget key
|
||||
filters_mapping = {
|
||||
"assigned_issues": {"duration": "none", "tab": "pending"},
|
||||
"created_issues": {"duration": "none", "tab": "pending"},
|
||||
"issues_by_state_groups": {"duration": "none"},
|
||||
"issues_by_priority": {"duration": "none"},
|
||||
}
|
||||
|
||||
# Iterate over widgets and update filters if applicable
|
||||
for widget in Widget.objects.all():
|
||||
if widget.key in filters_mapping:
|
||||
widget.filters = filters_mapping[widget.key]
|
||||
widgets_to_update.append(widget)
|
||||
|
||||
# Bulk update the widgets
|
||||
Widget.objects.bulk_update(widgets_to_update, ["filters"], batch_size=10)
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('db', '0058_alter_moduleissue_issue_and_more'),
|
||||
]
|
||||
operations = [
|
||||
migrations.RunPython(widgets_filter_change)
|
||||
]
|
||||
@@ -66,7 +66,7 @@
|
||||
style="margin-left: 30px; margin-bottom: 20px; margin-top: 20px"
|
||||
>
|
||||
<img
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/plane-logo.webp"
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/plane-logo.png"
|
||||
width="130"
|
||||
height="40"
|
||||
border="0"
|
||||
@@ -280,7 +280,7 @@
|
||||
<tr>
|
||||
<td>
|
||||
<img
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/due-date.webp"
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/due-date.png"
|
||||
width="12"
|
||||
height="12"
|
||||
border="0"
|
||||
@@ -341,7 +341,7 @@
|
||||
<tr>
|
||||
<td>
|
||||
<img
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/duplicate.webp"
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/duplicate.png"
|
||||
width="12"
|
||||
height="12"
|
||||
border="0"
|
||||
@@ -436,7 +436,7 @@
|
||||
<tr>
|
||||
<td valign="top" style="white-space: nowrap; padding: 0px;">
|
||||
<img
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/assignee.webp"
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/assignee.png"
|
||||
width="12"
|
||||
height="12"
|
||||
border="0"
|
||||
@@ -532,7 +532,7 @@
|
||||
<tr>
|
||||
<td valign="top" style="white-space: nowrap; padding: 0px;">
|
||||
<img
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/labels.webp"
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/labels.png"
|
||||
width="12"
|
||||
height="12"
|
||||
border="0"
|
||||
@@ -629,7 +629,7 @@
|
||||
<tr>
|
||||
<td>
|
||||
<img
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/state.webp"
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/state.png"
|
||||
width="12"
|
||||
height="12"
|
||||
border="0"
|
||||
@@ -647,15 +647,17 @@
|
||||
State:
|
||||
</p>
|
||||
</td>
|
||||
<td >
|
||||
{% if update.changes.state.old_value.0 == 'Backlog' or update.changes.state.old_value.0 == 'In Progress' or update.changes.state.old_value.0 == 'Done' or update.changes.state.old_value.0 == 'Cancelled' %}
|
||||
<td>
|
||||
<img
|
||||
src="{% if update.changes.state.old_value.0 == 'Backlog' %}https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/backlog.webp{% endif %}{% if update.changes.state.old_value.0 == 'In Progress' %}https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/in-progress.webp{% endif %}{% if update.changes.state.old_value.0 == 'Done' %}https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/done.webp{% endif %}{% if update.changes.state.old_value.0 == 'Cancelled' %}https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/cancelled.webp{% endif %}"
|
||||
src="{% if update.changes.state.old_value.0 == 'Backlog' %}https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/backlog.png{% endif %}{% if update.changes.state.old_value.0 == 'In Progress' %}https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/in-progress.png{% endif %}{% if update.changes.state.old_value.0 == 'Done' %}https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/done.png{% endif %}{% if update.changes.state.old_value.0 == 'Cancelled' %}https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/cancelled.png{% endif %}"
|
||||
width="12"
|
||||
height="12"
|
||||
border="0"
|
||||
style="display: block; margin-left: 5px;"
|
||||
/>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
<p
|
||||
style="
|
||||
@@ -669,22 +671,24 @@
|
||||
</td>
|
||||
<td style="padding-left: 10px; padding-right: 10px;">
|
||||
<img
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/forward-arrow.webp"
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/forward-arrow.png"
|
||||
width="16"
|
||||
height="16"
|
||||
border="0"
|
||||
style="display: block;"
|
||||
/>
|
||||
</td>
|
||||
<td >
|
||||
{% if update.changes.state.new_value|last == 'Backlog' or update.changes.state.new_value|last == 'In Progress' or update.changes.state.new_value|last == 'Done' or update.changes.state.new_value|last == 'Cancelled' %}
|
||||
<td>
|
||||
<img
|
||||
src="{% if update.changes.state.new_value|last == 'Backlog' %}https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/backlog.webp{% elif update.changes.state.new_value|last == 'In Progress' %}https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/in-progress.webp{% elif update.changes.state.new_value|last == 'Todo' %}https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/todo.webp{% elif update.changes.state.new_value|last == 'Done' %}https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/done.webp{% elif update.changes.state.new_value|last == 'Cancelled' %}https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/cancelled.webp{% endif %}"
|
||||
src="{% if update.changes.state.new_value|last == 'Backlog' %}https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/backlog.png{% elif update.changes.state.new_value|last == 'In Progress' %}https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/in-progress.png{% elif update.changes.state.new_value|last == 'Todo' %}https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/todo.png{% elif update.changes.state.new_value|last == 'Done' %}https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/done.png{% elif update.changes.state.new_value|last == 'Cancelled' %}https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/cancelled.png{% endif %}"
|
||||
width="12"
|
||||
height="12"
|
||||
border="0"
|
||||
style="display: block;"
|
||||
/>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
<p
|
||||
style="
|
||||
@@ -707,7 +711,7 @@
|
||||
<tr>
|
||||
<td valign="top">
|
||||
<img
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/link.webp"
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/link.png"
|
||||
width="12"
|
||||
height="12"
|
||||
border="0"
|
||||
@@ -768,7 +772,7 @@
|
||||
<tr>
|
||||
<td>
|
||||
<img
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/priority.webp"
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/priority.png"
|
||||
width="12"
|
||||
height="12"
|
||||
border="0"
|
||||
@@ -808,7 +812,7 @@
|
||||
</td>
|
||||
<td style="padding-left: 10px; padding-right: 10px;">
|
||||
<img
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/forward-arrow.webp"
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/forward-arrow.png"
|
||||
width="16"
|
||||
height="16"
|
||||
border="0"
|
||||
@@ -846,7 +850,7 @@
|
||||
<tr style="overflow-wrap: break-word;">
|
||||
<td>
|
||||
<img
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/blocking.webp"
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/blocking.png"
|
||||
width="12"
|
||||
height="12"
|
||||
border="0"
|
||||
|
||||
8
packages/types/src/dashboard.d.ts
vendored
8
packages/types/src/dashboard.d.ts
vendored
@@ -24,21 +24,21 @@ export type TDurationFilterOptions =
|
||||
|
||||
// widget filters
|
||||
export type TAssignedIssuesWidgetFilters = {
|
||||
target_date?: TDurationFilterOptions;
|
||||
duration?: TDurationFilterOptions;
|
||||
tab?: TIssuesListTypes;
|
||||
};
|
||||
|
||||
export type TCreatedIssuesWidgetFilters = {
|
||||
target_date?: TDurationFilterOptions;
|
||||
duration?: TDurationFilterOptions;
|
||||
tab?: TIssuesListTypes;
|
||||
};
|
||||
|
||||
export type TIssuesByStateGroupsWidgetFilters = {
|
||||
target_date?: TDurationFilterOptions;
|
||||
duration?: TDurationFilterOptions;
|
||||
};
|
||||
|
||||
export type TIssuesByPriorityWidgetFilters = {
|
||||
target_date?: TDurationFilterOptions;
|
||||
duration?: TDurationFilterOptions;
|
||||
};
|
||||
|
||||
export type TWidgetFiltersFormData =
|
||||
|
||||
@@ -10,7 +10,7 @@ type BreadcrumbsProps = {
|
||||
const Breadcrumbs = ({ children }: BreadcrumbsProps) => (
|
||||
<div className="flex items-center space-x-2">
|
||||
{React.Children.map(children, (child, index) => (
|
||||
<div key={index} className="flex flex-wrap items-center gap-2.5">
|
||||
<div key={index} className="flex items-center gap-2.5">
|
||||
{child}
|
||||
{index !== React.Children.count(children) - 1 && (
|
||||
<ChevronRight className="h-3.5 w-3.5 flex-shrink-0 text-custom-text-400" aria-hidden="true" />
|
||||
|
||||
@@ -48,7 +48,13 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
if (referenceElement) referenceElement.focus();
|
||||
};
|
||||
const closeDropdown = () => setIsOpen(false);
|
||||
|
||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
||||
|
||||
const handleOnClick = () => {
|
||||
if (closeOnSelect) closeDropdown();
|
||||
};
|
||||
|
||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||
|
||||
let menuItems = (
|
||||
@@ -90,6 +96,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
tabIndex={tabIndex}
|
||||
className={cn("relative w-min text-left", className)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
@@ -98,7 +105,8 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openDropdown();
|
||||
if (menuButtonOnClick) menuButtonOnClick();
|
||||
}}
|
||||
@@ -114,7 +122,8 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openDropdown();
|
||||
if (menuButtonOnClick) menuButtonOnClick();
|
||||
}}
|
||||
@@ -138,7 +147,8 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openDropdown();
|
||||
if (menuButtonOnClick) menuButtonOnClick();
|
||||
}}
|
||||
|
||||
@@ -163,6 +163,8 @@ export const CommandPalette: FC = observer(() => {
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [handleKeyDown]);
|
||||
|
||||
const isDraftIssue = router?.asPath?.includes("draft-issues") || false;
|
||||
|
||||
if (!currentUser) return null;
|
||||
|
||||
return (
|
||||
@@ -217,6 +219,7 @@ export const CommandPalette: FC = observer(() => {
|
||||
onClose={() => toggleCreateIssueModal(false)}
|
||||
data={cycleId ? { cycle_id: cycleId.toString() } : moduleId ? { module_ids: [moduleId.toString()] } : undefined}
|
||||
storeType={createIssueStoreType}
|
||||
isDraft={isDraftIssue}
|
||||
/>
|
||||
|
||||
{workspaceSlug && projectId && issueId && issueDetails && (
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Menu } from "lucide-react";
|
||||
import { useApplication } from "hooks/store";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
export const SidebarHamburgerToggle: FC = observer (() => {
|
||||
export const SidebarHamburgerToggle: FC = observer(() => {
|
||||
const { theme: themStore } = useApplication();
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -149,7 +149,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
|
||||
color: group.color,
|
||||
}));
|
||||
|
||||
const daysLeft = findHowManyDaysLeft(activeCycle.end_date ?? new Date());
|
||||
const daysLeft = findHowManyDaysLeft(activeCycle.end_date) ?? 0;
|
||||
|
||||
return (
|
||||
<div className="grid-row-2 grid divide-y rounded-[10px] border border-custom-border-200 bg-custom-background-100 shadow">
|
||||
|
||||
@@ -137,7 +137,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const daysLeft = findHowManyDaysLeft(cycleDetails.end_date ?? new Date());
|
||||
const daysLeft = findHowManyDaysLeft(cycleDetails.end_date) ?? 0;
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -140,7 +140,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
||||
|
||||
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
|
||||
|
||||
const daysLeft = findHowManyDaysLeft(cycleDetails.end_date ?? new Date());
|
||||
const daysLeft = findHowManyDaysLeft(cycleDetails.end_date) ?? 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { FC } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { KeyedMutator } from "swr";
|
||||
// hooks
|
||||
import { useCycle, useUser } from "hooks/store";
|
||||
// services
|
||||
import { CycleService } from "services/cycle.service";
|
||||
// components
|
||||
import { GanttChartRoot, IBlockUpdateData, CycleGanttSidebar } from "components/gantt-chart";
|
||||
import { CycleGanttBlock } from "components/cycles";
|
||||
@@ -17,14 +14,10 @@ import { EUserProjectRoles } from "constants/project";
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
cycleIds: string[];
|
||||
mutateCycles?: KeyedMutator<ICycle[]>;
|
||||
};
|
||||
|
||||
// services
|
||||
const cycleService = new CycleService();
|
||||
|
||||
export const CyclesListGanttChartView: FC<Props> = observer((props) => {
|
||||
const { cycleIds, mutateCycles } = props;
|
||||
const { cycleIds } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
@@ -32,38 +25,15 @@ export const CyclesListGanttChartView: FC<Props> = observer((props) => {
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
const { getCycleById } = useCycle();
|
||||
const { getCycleById, updateCycleDetails } = useCycle();
|
||||
|
||||
const handleCycleUpdate = (cycle: ICycle, payload: IBlockUpdateData) => {
|
||||
if (!workspaceSlug) return;
|
||||
mutateCycles &&
|
||||
mutateCycles((prevData: any) => {
|
||||
if (!prevData) return prevData;
|
||||
const handleCycleUpdate = async (cycle: ICycle, data: IBlockUpdateData) => {
|
||||
if (!workspaceSlug || !cycle) return;
|
||||
|
||||
const newList = prevData.map((p: any) => ({
|
||||
...p,
|
||||
...(p.id === cycle.id
|
||||
? {
|
||||
start_date: payload.start_date ? payload.start_date : p.start_date,
|
||||
target_date: payload.target_date ? payload.target_date : p.end_date,
|
||||
sort_order: payload.sort_order ? payload.sort_order.newSortOrder : p.sort_order,
|
||||
}
|
||||
: {}),
|
||||
}));
|
||||
const payload: any = { ...data };
|
||||
if (data.sort_order) payload.sort_order = data.sort_order.newSortOrder;
|
||||
|
||||
if (payload.sort_order) {
|
||||
const removedElement = newList.splice(payload.sort_order.sourceIndex, 1)[0];
|
||||
newList.splice(payload.sort_order.destinationIndex, 0, removedElement);
|
||||
}
|
||||
|
||||
return newList;
|
||||
}, false);
|
||||
|
||||
const newPayload: any = { ...payload };
|
||||
|
||||
if (newPayload.sort_order && payload.sort_order) newPayload.sort_order = payload.sort_order.newSortOrder;
|
||||
|
||||
cycleService.patchCycle(workspaceSlug.toString(), cycle.project, cycle.id, newPayload);
|
||||
await updateCycleDetails(workspaceSlug.toString(), cycle.project, cycle.id, payload);
|
||||
};
|
||||
|
||||
const blockFormat = (blocks: (ICycle | null)[]) => {
|
||||
|
||||
@@ -318,6 +318,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
|
||||
const issueCount =
|
||||
cycleDetails.total_issues === 0 ? "0 Issue" : `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
|
||||
const daysLeft = findHowManyDaysLeft(cycleDetails.end_date);
|
||||
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
@@ -375,8 +376,8 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
backgroundColor: `${currentCycle.color}20`,
|
||||
}}
|
||||
>
|
||||
{currentCycle.value === "current"
|
||||
? `${findHowManyDaysLeft(cycleDetails.end_date ?? new Date())} ${currentCycle.label}`
|
||||
{currentCycle.value === "current" && daysLeft !== undefined
|
||||
? `${daysLeft} ${currentCycle.label}`
|
||||
: `${currentCycle.label}`}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
WidgetProps,
|
||||
} from "components/dashboard/widgets";
|
||||
// helpers
|
||||
import { getCustomDates, getRedirectionFilters } from "helpers/dashboard.helper";
|
||||
import { getCustomDates, getRedirectionFilters, getTabKey } from "helpers/dashboard.helper";
|
||||
// types
|
||||
import { TAssignedIssuesWidgetFilters, TAssignedIssuesWidgetResponse } from "@plane/types";
|
||||
// constants
|
||||
@@ -30,8 +30,8 @@ export const AssignedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
|
||||
// derived values
|
||||
const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY);
|
||||
const widgetStats = getWidgetStats<TAssignedIssuesWidgetResponse>(workspaceSlug, dashboardId, WIDGET_KEY);
|
||||
const selectedTab = widgetDetails?.widget_filters.tab ?? "pending";
|
||||
const selectedDurationFilter = widgetDetails?.widget_filters.target_date ?? "none";
|
||||
const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? "none";
|
||||
const selectedTab = getTabKey(selectedDurationFilter, widgetDetails?.widget_filters.tab);
|
||||
|
||||
const handleUpdateFilters = async (filters: Partial<TAssignedIssuesWidgetFilters>) => {
|
||||
if (!widgetDetails) return;
|
||||
@@ -43,7 +43,7 @@ export const AssignedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
|
||||
filters,
|
||||
});
|
||||
|
||||
const filterDates = getCustomDates(filters.target_date ?? selectedDurationFilter);
|
||||
const filterDates = getCustomDates(filters.duration ?? selectedDurationFilter);
|
||||
fetchWidgetStats(workspaceSlug, dashboardId, {
|
||||
widget_key: WIDGET_KEY,
|
||||
issue_type: filters.tab ?? selectedTab,
|
||||
@@ -86,19 +86,19 @@ export const AssignedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
|
||||
|
||||
// switch to pending tab if target date is changed to none
|
||||
if (val === "none" && selectedTab !== "completed") {
|
||||
handleUpdateFilters({ target_date: val, tab: "pending" });
|
||||
handleUpdateFilters({ duration: val, tab: "pending" });
|
||||
return;
|
||||
}
|
||||
// switch to upcoming tab if target date is changed to other than none
|
||||
if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed") {
|
||||
handleUpdateFilters({
|
||||
target_date: val,
|
||||
duration: val,
|
||||
tab: "upcoming",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handleUpdateFilters({ target_date: val });
|
||||
handleUpdateFilters({ duration: val });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
WidgetProps,
|
||||
} from "components/dashboard/widgets";
|
||||
// helpers
|
||||
import { getCustomDates, getRedirectionFilters } from "helpers/dashboard.helper";
|
||||
import { getCustomDates, getRedirectionFilters, getTabKey } from "helpers/dashboard.helper";
|
||||
// types
|
||||
import { TCreatedIssuesWidgetFilters, TCreatedIssuesWidgetResponse } from "@plane/types";
|
||||
// constants
|
||||
@@ -30,8 +30,8 @@ export const CreatedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
|
||||
// derived values
|
||||
const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY);
|
||||
const widgetStats = getWidgetStats<TCreatedIssuesWidgetResponse>(workspaceSlug, dashboardId, WIDGET_KEY);
|
||||
const selectedTab = widgetDetails?.widget_filters.tab ?? "pending";
|
||||
const selectedDurationFilter = widgetDetails?.widget_filters.target_date ?? "none";
|
||||
const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? "none";
|
||||
const selectedTab = getTabKey(selectedDurationFilter, widgetDetails?.widget_filters.tab);
|
||||
|
||||
const handleUpdateFilters = async (filters: Partial<TCreatedIssuesWidgetFilters>) => {
|
||||
if (!widgetDetails) return;
|
||||
@@ -43,7 +43,7 @@ export const CreatedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
|
||||
filters,
|
||||
});
|
||||
|
||||
const filterDates = getCustomDates(filters.target_date ?? selectedDurationFilter);
|
||||
const filterDates = getCustomDates(filters.duration ?? selectedDurationFilter);
|
||||
fetchWidgetStats(workspaceSlug, dashboardId, {
|
||||
widget_key: WIDGET_KEY,
|
||||
issue_type: filters.tab ?? selectedTab,
|
||||
@@ -83,19 +83,19 @@ export const CreatedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
|
||||
|
||||
// switch to pending tab if target date is changed to none
|
||||
if (val === "none" && selectedTab !== "completed") {
|
||||
handleUpdateFilters({ target_date: val, tab: "pending" });
|
||||
handleUpdateFilters({ duration: val, tab: "pending" });
|
||||
return;
|
||||
}
|
||||
// switch to upcoming tab if target date is changed to other than none
|
||||
if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed") {
|
||||
handleUpdateFilters({
|
||||
target_date: val,
|
||||
duration: val,
|
||||
tab: "upcoming",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handleUpdateFilters({ target_date: val });
|
||||
handleUpdateFilters({ duration: val });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -83,7 +83,7 @@ export const AssignedOverdueIssueListItem: React.FC<IssueListItemProps> = observ
|
||||
const blockedByIssueProjectDetails =
|
||||
blockedByIssues.length === 1 ? getProjectById(blockedByIssues[0]?.project_id ?? "") : null;
|
||||
|
||||
const dueBy = findTotalDaysInRange(new Date(issueDetails.target_date ?? ""), new Date(), false);
|
||||
const dueBy = findTotalDaysInRange(new Date(issueDetails.target_date ?? ""), new Date(), false) ?? 0;
|
||||
|
||||
return (
|
||||
<ControlLink
|
||||
@@ -212,7 +212,7 @@ export const CreatedOverdueIssueListItem: React.FC<IssueListItemProps> = observe
|
||||
|
||||
const projectDetails = getProjectById(issue.project_id);
|
||||
|
||||
const dueBy = findTotalDaysInRange(new Date(issue.target_date ?? ""), new Date(), false);
|
||||
const dueBy = findTotalDaysInRange(new Date(issue.target_date ?? ""), new Date(), false) ?? 0;
|
||||
|
||||
return (
|
||||
<ControlLink
|
||||
|
||||
@@ -16,42 +16,40 @@ export const TabsList: React.FC<Props> = observer((props) => {
|
||||
const { durationFilter, selectedTab } = props;
|
||||
|
||||
const tabsList = durationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST;
|
||||
const selectedTabIndex = tabsList.findIndex((tab) => tab.key === (selectedTab ?? "pending"));
|
||||
const selectedTabIndex = tabsList.findIndex((tab) => tab.key === selectedTab);
|
||||
|
||||
return (
|
||||
<Tab.List
|
||||
as="div"
|
||||
className="relative border-[0.5px] border-custom-border-200 rounded bg-custom-background-80 grid"
|
||||
className="relative border-[0.5px] border-custom-border-200 rounded bg-custom-background-80 p-[1px] grid"
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${tabsList.length}, 1fr)`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn("absolute bg-custom-background-100 rounded transition-all duration-500 ease-in-out", {
|
||||
// right shadow
|
||||
"shadow-[2px_0_8px_rgba(167,169,174,0.15)]": selectedTabIndex !== tabsList.length - 1,
|
||||
// left shadow
|
||||
"shadow-[-2px_0_8px_rgba(167,169,174,0.15)]": selectedTabIndex !== 0,
|
||||
})}
|
||||
className={cn(
|
||||
"absolute top-1/2 left-[1px] bg-custom-background-100 rounded-[3px] transition-all duration-500 ease-in-out",
|
||||
{
|
||||
// right shadow
|
||||
"shadow-[2px_0_8px_rgba(167,169,174,0.15)]": selectedTabIndex !== tabsList.length - 1,
|
||||
// left shadow
|
||||
"shadow-[-2px_0_8px_rgba(167,169,174,0.15)]": selectedTabIndex !== 0,
|
||||
}
|
||||
)}
|
||||
style={{
|
||||
height: "calc(100% - 1px)",
|
||||
width: `${100 / tabsList.length}%`,
|
||||
transform: `translateX(${selectedTabIndex * 100}%)`,
|
||||
height: "calc(100% - 2px)",
|
||||
width: `calc(${100 / tabsList.length}% - 1px)`,
|
||||
transform: `translate(${selectedTabIndex * 100}%, -50%)`,
|
||||
}}
|
||||
/>
|
||||
{tabsList.map((tab) => (
|
||||
<Tab
|
||||
key={tab.key}
|
||||
className={cn(
|
||||
"relative z-[1] font-semibold text-xs rounded py-1.5 text-custom-text-400 focus:outline-none",
|
||||
"transition duration-500",
|
||||
"relative z-[1] font-semibold text-xs rounded-[3px] py-1.5 text-custom-text-400 focus:outline-none transition duration-500",
|
||||
{
|
||||
"text-custom-text-100 bg-custom-background-100": selectedTab === tab.key,
|
||||
"hover:text-custom-text-300": selectedTab !== tab.key,
|
||||
// // right shadow
|
||||
// "shadow-[2px_0_8px_rgba(167,169,174,0.15)]": selectedTabIndex !== tabsList.length - 1,
|
||||
// // left shadow
|
||||
// "shadow-[-2px_0_8px_rgba(167,169,174,0.15)]": selectedTabIndex !== 0,
|
||||
}
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -73,8 +73,10 @@ export const IssuesByPriorityWidget: React.FC<WidgetProps> = observer((props) =>
|
||||
const { dashboardId, workspaceSlug } = props;
|
||||
// store hooks
|
||||
const { fetchWidgetStats, getWidgetDetails, getWidgetStats, updateDashboardWidgetFilters } = useDashboard();
|
||||
// derived values
|
||||
const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY);
|
||||
const widgetStats = getWidgetStats<TIssuesByPriorityWidgetResponse[]>(workspaceSlug, dashboardId, WIDGET_KEY);
|
||||
const selectedDuration = widgetDetails?.widget_filters.duration ?? "none";
|
||||
|
||||
const handleUpdateFilters = async (filters: Partial<TIssuesByPriorityWidgetFilters>) => {
|
||||
if (!widgetDetails) return;
|
||||
@@ -84,7 +86,7 @@ export const IssuesByPriorityWidget: React.FC<WidgetProps> = observer((props) =>
|
||||
filters,
|
||||
});
|
||||
|
||||
const filterDates = getCustomDates(filters.target_date ?? widgetDetails.widget_filters.target_date ?? "none");
|
||||
const filterDates = getCustomDates(filters.duration ?? selectedDuration);
|
||||
fetchWidgetStats(workspaceSlug, dashboardId, {
|
||||
widget_key: WIDGET_KEY,
|
||||
...(filterDates.trim() !== "" ? { target_date: filterDates } : {}),
|
||||
@@ -92,7 +94,7 @@ export const IssuesByPriorityWidget: React.FC<WidgetProps> = observer((props) =>
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const filterDates = getCustomDates(widgetDetails?.widget_filters.target_date ?? "none");
|
||||
const filterDates = getCustomDates(selectedDuration);
|
||||
fetchWidgetStats(workspaceSlug, dashboardId, {
|
||||
widget_key: WIDGET_KEY,
|
||||
...(filterDates.trim() !== "" ? { target_date: filterDates } : {}),
|
||||
@@ -139,10 +141,10 @@ export const IssuesByPriorityWidget: React.FC<WidgetProps> = observer((props) =>
|
||||
Assigned by priority
|
||||
</Link>
|
||||
<DurationFilterDropdown
|
||||
value={widgetDetails.widget_filters.target_date ?? "none"}
|
||||
value={selectedDuration}
|
||||
onChange={(val) =>
|
||||
handleUpdateFilters({
|
||||
target_date: val,
|
||||
duration: val,
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -34,6 +34,7 @@ export const IssuesByStateGroupWidget: React.FC<WidgetProps> = observer((props)
|
||||
// derived values
|
||||
const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY);
|
||||
const widgetStats = getWidgetStats<TIssuesByStateGroupsWidgetResponse[]>(workspaceSlug, dashboardId, WIDGET_KEY);
|
||||
const selectedDuration = widgetDetails?.widget_filters.duration ?? "none";
|
||||
|
||||
const handleUpdateFilters = async (filters: Partial<TIssuesByStateGroupsWidgetFilters>) => {
|
||||
if (!widgetDetails) return;
|
||||
@@ -43,7 +44,7 @@ export const IssuesByStateGroupWidget: React.FC<WidgetProps> = observer((props)
|
||||
filters,
|
||||
});
|
||||
|
||||
const filterDates = getCustomDates(filters.target_date ?? widgetDetails.widget_filters.target_date ?? "none");
|
||||
const filterDates = getCustomDates(filters.duration ?? selectedDuration);
|
||||
fetchWidgetStats(workspaceSlug, dashboardId, {
|
||||
widget_key: WIDGET_KEY,
|
||||
...(filterDates.trim() !== "" ? { target_date: filterDates } : {}),
|
||||
@@ -52,7 +53,7 @@ export const IssuesByStateGroupWidget: React.FC<WidgetProps> = observer((props)
|
||||
|
||||
// fetch widget stats
|
||||
useEffect(() => {
|
||||
const filterDates = getCustomDates(widgetDetails?.widget_filters.target_date ?? "none");
|
||||
const filterDates = getCustomDates(selectedDuration);
|
||||
fetchWidgetStats(workspaceSlug, dashboardId, {
|
||||
widget_key: WIDGET_KEY,
|
||||
...(filterDates.trim() !== "" ? { target_date: filterDates } : {}),
|
||||
@@ -138,10 +139,10 @@ export const IssuesByStateGroupWidget: React.FC<WidgetProps> = observer((props)
|
||||
Assigned by state
|
||||
</Link>
|
||||
<DurationFilterDropdown
|
||||
value={widgetDetails.widget_filters.target_date ?? "none"}
|
||||
value={selectedDuration}
|
||||
onChange={(val) =>
|
||||
handleUpdateFilters({
|
||||
target_date: val,
|
||||
duration: val,
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -3,8 +3,7 @@ import { TIssue } from "@plane/types";
|
||||
import { IGanttBlock } from "components/gantt-chart";
|
||||
|
||||
export const renderIssueBlocksStructure = (blocks: TIssue[]): IGanttBlock[] =>
|
||||
blocks &&
|
||||
blocks.map((block) => ({
|
||||
blocks?.map((block) => ({
|
||||
data: block,
|
||||
id: block.id,
|
||||
sort_order: block.sort_order,
|
||||
|
||||
@@ -93,7 +93,7 @@ export const CycleGanttSidebar: React.FC<Props> = (props) => {
|
||||
<>
|
||||
{blocks ? (
|
||||
blocks.map((block, index) => {
|
||||
const duration = findTotalDaysInRange(block.start_date ?? "", block.target_date ?? "");
|
||||
const duration = findTotalDaysInRange(block.start_date, block.target_date);
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
@@ -130,9 +130,11 @@ export const CycleGanttSidebar: React.FC<Props> = (props) => {
|
||||
<div className="flex-grow truncate">
|
||||
<CycleGanttSidebarBlock data={block.data} />
|
||||
</div>
|
||||
<div className="flex-shrink-0 text-sm text-custom-text-200">
|
||||
{duration} day{duration > 1 ? "s" : ""}
|
||||
</div>
|
||||
{duration !== undefined && (
|
||||
<div className="flex-shrink-0 text-sm text-custom-text-200">
|
||||
{duration} day{duration > 1 ? "s" : ""}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -93,7 +93,7 @@ export const ModuleGanttSidebar: React.FC<Props> = (props) => {
|
||||
<>
|
||||
{blocks ? (
|
||||
blocks.map((block, index) => {
|
||||
const duration = findTotalDaysInRange(block.start_date ?? "", block.target_date ?? "");
|
||||
const duration = findTotalDaysInRange(block.start_date, block.target_date);
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
@@ -130,9 +130,11 @@ export const ModuleGanttSidebar: React.FC<Props> = (props) => {
|
||||
<div className="flex-grow truncate">
|
||||
<ModuleGanttSidebarBlock data={block.data} />
|
||||
</div>
|
||||
<div className="flex-shrink-0 text-sm text-custom-text-200">
|
||||
{duration} day{duration > 1 ? "s" : ""}
|
||||
</div>
|
||||
{duration !== undefined && (
|
||||
<div className="flex-shrink-0 text-sm text-custom-text-200">
|
||||
{duration} day{duration > 1 ? "s" : ""}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -94,7 +94,7 @@ export const ProjectViewGanttSidebar: React.FC<Props> = (props) => {
|
||||
<>
|
||||
{blocks ? (
|
||||
blocks.map((block, index) => {
|
||||
const duration = findTotalDaysInRange(block.start_date ?? "", block.target_date ?? "");
|
||||
const duration = findTotalDaysInRange(block.start_date, block.target_date);
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
@@ -131,9 +131,11 @@ export const ProjectViewGanttSidebar: React.FC<Props> = (props) => {
|
||||
<div className="flex-grow truncate">
|
||||
<IssueGanttSidebarBlock data={block.data} />
|
||||
</div>
|
||||
<div className="flex-shrink-0 text-sm text-custom-text-200">
|
||||
{duration} day{duration > 1 ? "s" : ""}
|
||||
</div>
|
||||
{duration !== undefined && (
|
||||
<div className="flex-shrink-0 text-sm text-custom-text-200">
|
||||
{duration} day{duration > 1 ? "s" : ""}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -119,10 +119,7 @@ export const IssueGanttSidebar: React.FC<Props> = (props) => {
|
||||
// hide the block if it doesn't have start and target dates and showAllBlocks is false
|
||||
if (!showAllBlocks && !isBlockVisibleOnSidebar) return;
|
||||
|
||||
const duration =
|
||||
!block.start_date || !block.target_date
|
||||
? null
|
||||
: findTotalDaysInRange(block.start_date, block.target_date);
|
||||
const duration = findTotalDaysInRange(block.start_date, block.target_date);
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
@@ -166,13 +163,13 @@ export const IssueGanttSidebar: React.FC<Props> = (props) => {
|
||||
<div className="flex-grow truncate">
|
||||
<IssueGanttSidebarBlock data={block.data} />
|
||||
</div>
|
||||
<div className="flex-shrink-0 text-sm text-custom-text-200">
|
||||
{duration && (
|
||||
{duration !== undefined && (
|
||||
<div className="flex-shrink-0 text-sm text-custom-text-200">
|
||||
<span>
|
||||
{duration} day{duration > 1 ? "s" : ""}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -103,7 +103,7 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<BreadcrumbLink label="Inbox Issues" icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />} />
|
||||
<BreadcrumbLink label="Draft Issues" icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />} />
|
||||
}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
|
||||
@@ -200,8 +200,8 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
|
||||
{currentProjectDetails?.inbox_view && inboxDetails && (
|
||||
</div>
|
||||
{currentProjectDetails?.inbox_view && inboxDetails && (
|
||||
<Link href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxDetails?.id}`}>
|
||||
<span>
|
||||
<Button variant="neutral-primary" size="sm" className="relative">
|
||||
@@ -214,9 +214,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
</Button>
|
||||
</span>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)}
|
||||
{canUserCreateIssue && (
|
||||
<>
|
||||
<Button className="hidden md:block" onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||
|
||||
@@ -1,18 +1,78 @@
|
||||
// ui
|
||||
import { Breadcrumbs } from "@plane/ui";
|
||||
import { Breadcrumbs, CustomMenu } from "@plane/ui";
|
||||
import { BreadcrumbLink } from "components/common";
|
||||
// components
|
||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
||||
import { cn } from "helpers/common.helper";
|
||||
import { FC } from "react";
|
||||
import { useApplication, useUser } from "hooks/store";
|
||||
import { ChevronDown, PanelRight } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "constants/profile";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export const UserProfileHeader = () => (
|
||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||
type TUserProfileHeader = {
|
||||
type?: string | undefined
|
||||
}
|
||||
|
||||
export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
|
||||
const { type = undefined } = props
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, userId } = router.query;
|
||||
|
||||
const AUTHORIZED_ROLES = [20, 15, 10];
|
||||
const {
|
||||
membership: { currentWorkspaceRole },
|
||||
} = useUser();
|
||||
|
||||
if (!currentWorkspaceRole) return null;
|
||||
|
||||
const isAuthorized = AUTHORIZED_ROLES.includes(currentWorkspaceRole);
|
||||
const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB;
|
||||
|
||||
const { theme: themStore } = useApplication();
|
||||
|
||||
return (<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||
<SidebarHamburgerToggle />
|
||||
<div>
|
||||
<div className="flex justify-between w-full">
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink href="/profile" label="Activity Overview" />} />
|
||||
</Breadcrumbs>
|
||||
<div className="flex gap-4 md:hidden">
|
||||
<CustomMenu
|
||||
maxHeight={"md"}
|
||||
className="flex flex-grow justify-center text-custom-text-200 text-sm"
|
||||
placement="bottom-start"
|
||||
customButton={
|
||||
<div className="flex gap-2 items-center px-2 py-1.5 border border-custom-border-400 rounded-md">
|
||||
<span className="flex flex-grow justify-center text-custom-text-200 text-sm">{type}</span>
|
||||
<ChevronDown className="w-4 h-4 text-custom-text-400" />
|
||||
</div>
|
||||
}
|
||||
customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm"
|
||||
closeOnSelect
|
||||
>
|
||||
<></>
|
||||
{tabsList.map((tab) => (
|
||||
<CustomMenu.MenuItem
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Link key={tab.route} href={`/${workspaceSlug}/profile/${userId}/${tab.route}`} className="text-custom-text-300 w-full">{tab.label}</Link>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
<button className="transition-all block md:hidden" onClick={() => { themStore.toggleProfileSidebar(); console.log(themStore.profileSidebarCollapsed) }}>
|
||||
<PanelRight className={
|
||||
cn("w-4 h-4 block md:hidden", !themStore.profileSidebarCollapsed ? "text-[#3E63DD]" : "text-custom-text-200")
|
||||
} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
</div>)
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useState } from "react";
|
||||
import { LayoutGrid, Zap } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useTheme } from "next-themes";
|
||||
@@ -6,18 +5,16 @@ import { useTheme } from "next-themes";
|
||||
import githubBlackImage from "/public/logos/github-black.png";
|
||||
import githubWhiteImage from "/public/logos/github-white.png";
|
||||
// components
|
||||
import { BreadcrumbLink, ProductUpdatesModal } from "components/common";
|
||||
import { BreadcrumbLink } from "components/common";
|
||||
import { Breadcrumbs } from "@plane/ui";
|
||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
||||
|
||||
export const WorkspaceDashboardHeader = () => {
|
||||
const [isProductUpdatesModalOpen, setIsProductUpdatesModalOpen] = useState(false);
|
||||
// hooks
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProductUpdatesModal isOpen={isProductUpdatesModalOpen} setIsOpen={setIsProductUpdatesModalOpen} />
|
||||
<div className="relative z-[15] flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||
<SidebarHamburgerToggle />
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export * from "./attachment";
|
||||
export * from "./issue-modal";
|
||||
export * from "./view-select";
|
||||
export * from "./delete-issue-modal";
|
||||
export * from "./description-form";
|
||||
export * from "./issue-layouts";
|
||||
|
||||
@@ -66,16 +66,22 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((prop
|
||||
</div>
|
||||
</WithDisplayPropertiesHOC>
|
||||
|
||||
<ControlLink
|
||||
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
|
||||
target="_blank"
|
||||
onClick={() => handleIssuePeekOverview(issue)}
|
||||
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
|
||||
>
|
||||
{issue?.is_draft ? (
|
||||
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
||||
<span>{issue.name}</span>
|
||||
</Tooltip>
|
||||
</ControlLink>
|
||||
) : (
|
||||
<ControlLink
|
||||
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
|
||||
target="_blank"
|
||||
onClick={() => handleIssuePeekOverview(issue)}
|
||||
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
|
||||
>
|
||||
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
||||
<span>{issue.name}</span>
|
||||
</Tooltip>
|
||||
</ControlLink>
|
||||
)}
|
||||
|
||||
<IssueProperties
|
||||
className="flex flex-wrap items-center gap-2 whitespace-nowrap"
|
||||
|
||||
@@ -79,21 +79,14 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{isDraftIssue ? (
|
||||
<CreateUpdateDraftIssueModal
|
||||
isOpen={isOpen}
|
||||
handleClose={() => setIsOpen(false)}
|
||||
prePopulateData={issuePayload}
|
||||
fieldsToShow={["all"]}
|
||||
/>
|
||||
) : (
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
data={issuePayload}
|
||||
storeType={storeType}
|
||||
/>
|
||||
)}
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
data={issuePayload}
|
||||
storeType={storeType}
|
||||
isDraft={isDraftIssue}
|
||||
/>
|
||||
|
||||
{renderExistingIssueModal && (
|
||||
<ExistingIssuesListModal
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
|
||||
@@ -69,16 +69,22 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
|
||||
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
|
||||
)}
|
||||
|
||||
<ControlLink
|
||||
href={`/${workspaceSlug}/projects/${projectId}/issues/${issueId}`}
|
||||
target="_blank"
|
||||
onClick={() => handleIssuePeekOverview(issue)}
|
||||
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
|
||||
>
|
||||
{issue?.is_draft ? (
|
||||
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
||||
<span>{issue.name}</span>
|
||||
</Tooltip>
|
||||
</ControlLink>
|
||||
) : (
|
||||
<ControlLink
|
||||
href={`/${workspaceSlug}/projects/${projectId}/issues/${issueId}`}
|
||||
target="_blank"
|
||||
onClick={() => handleIssuePeekOverview(issue)}
|
||||
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
|
||||
>
|
||||
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
||||
<span>{issue.name}</span>
|
||||
</Tooltip>
|
||||
</ControlLink>
|
||||
)}
|
||||
|
||||
<div className="ml-auto flex flex-shrink-0 items-center gap-2">
|
||||
{!issue?.tempId ? (
|
||||
|
||||
@@ -109,21 +109,13 @@ export const HeaderGroupByCard = observer(
|
||||
</div>
|
||||
))}
|
||||
|
||||
{isDraftIssue ? (
|
||||
<CreateUpdateDraftIssueModal
|
||||
isOpen={isOpen}
|
||||
handleClose={() => setIsOpen(false)}
|
||||
prePopulateData={issuePayload}
|
||||
fieldsToShow={["all"]}
|
||||
/>
|
||||
) : (
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
data={issuePayload}
|
||||
storeType={storeType}
|
||||
/>
|
||||
)}
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
data={issuePayload}
|
||||
storeType={storeType}
|
||||
isDraft={isDraftIssue}
|
||||
/>
|
||||
|
||||
{renderExistingIssueModal && (
|
||||
<ExistingIssuesListModal
|
||||
|
||||
@@ -54,6 +54,8 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
};
|
||||
delete duplicateIssuePayload.id;
|
||||
|
||||
const isDraftIssue = router?.asPath?.includes("draft-issues") || false;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeleteIssueModal
|
||||
@@ -62,6 +64,7 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
handleClose={() => setDeleteIssueModal(false)}
|
||||
onSubmit={handleDelete}
|
||||
/>
|
||||
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={createUpdateIssueModal}
|
||||
onClose={() => {
|
||||
@@ -73,7 +76,9 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data });
|
||||
}}
|
||||
storeType={EIssuesStoreType.PROJECT}
|
||||
isDraft={isDraftIssue}
|
||||
/>
|
||||
|
||||
<CustomMenu
|
||||
placement="bottom-start"
|
||||
customButton={customActionButton}
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface DraftIssueProps {
|
||||
onClose: (saveDraftIssueInLocalStorage?: boolean) => void;
|
||||
onSubmit: (formData: Partial<TIssue>) => Promise<void>;
|
||||
projectId: string;
|
||||
isDraft: boolean;
|
||||
}
|
||||
|
||||
const issueDraftService = new IssueDraftService();
|
||||
@@ -35,6 +36,7 @@ export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
|
||||
projectId,
|
||||
isCreateMoreToggleEnabled,
|
||||
onCreateMoreToggleChange,
|
||||
isDraft,
|
||||
} = props;
|
||||
// states
|
||||
const [issueDiscardModal, setIssueDiscardModal] = useState(false);
|
||||
@@ -107,6 +109,7 @@ export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
|
||||
onClose={handleClose}
|
||||
onSubmit={onSubmit}
|
||||
projectId={projectId}
|
||||
isDraft={isDraft}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, useState, useRef, useEffect } from "react";
|
||||
import React, { FC, useState, useRef, useEffect, Fragment } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
@@ -55,8 +55,9 @@ export interface IssueFormProps {
|
||||
onCreateMoreToggleChange: (value: boolean) => void;
|
||||
onChange?: (formData: Partial<TIssue> | null) => void;
|
||||
onClose: () => void;
|
||||
onSubmit: (values: Partial<TIssue>) => Promise<void>;
|
||||
onSubmit: (values: Partial<TIssue>, is_draft_issue?: boolean) => Promise<void>;
|
||||
projectId: string;
|
||||
isDraft: boolean;
|
||||
}
|
||||
|
||||
// services
|
||||
@@ -72,6 +73,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
projectId: defaultProjectId,
|
||||
isCreateMoreToggleEnabled,
|
||||
onCreateMoreToggleChange,
|
||||
isDraft,
|
||||
} = props;
|
||||
// states
|
||||
const [labelModal, setLabelModal] = useState(false);
|
||||
@@ -137,8 +139,8 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
|
||||
const issueName = watch("name");
|
||||
|
||||
const handleFormSubmit = async (formData: Partial<TIssue>) => {
|
||||
await onSubmit(formData);
|
||||
const handleFormSubmit = async (formData: Partial<TIssue>, is_draft_issue = false) => {
|
||||
await onSubmit(formData, is_draft_issue);
|
||||
|
||||
setGptAssistantModal(false);
|
||||
|
||||
@@ -248,7 +250,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<form onSubmit={handleSubmit((data) => handleFormSubmit(data))}>
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center gap-x-2">
|
||||
{/* Don't show project selection if editing an issue */}
|
||||
@@ -670,7 +672,34 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
<Button variant="neutral-primary" size="sm" onClick={onClose} tabIndex={17}>
|
||||
Discard
|
||||
</Button>
|
||||
<Button type="submit" variant="primary" size="sm" loading={isSubmitting} tabIndex={18}>
|
||||
|
||||
{isDraft && (
|
||||
<Fragment>
|
||||
{data?.id ? (
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
size="sm"
|
||||
loading={isSubmitting}
|
||||
onClick={handleSubmit((data) => handleFormSubmit({ ...data, is_draft: false }))}
|
||||
tabIndex={18}
|
||||
>
|
||||
{isSubmitting ? "Moving" : "Move from draft"}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
size="sm"
|
||||
loading={isSubmitting}
|
||||
onClick={handleSubmit((data) => handleFormSubmit(data, true))}
|
||||
tabIndex={18}
|
||||
>
|
||||
{isSubmitting ? "Saving" : "Save as draft"}
|
||||
</Button>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
<Button variant="primary" type="submit" size="sm" loading={isSubmitting} tabIndex={isDraft ? 19 : 18}>
|
||||
{data?.id ? (isSubmitting ? "Updating" : "Update issue") : isSubmitting ? "Creating" : "Create issue"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -20,10 +20,19 @@ export interface IssuesModalProps {
|
||||
onSubmit?: (res: TIssue) => Promise<void>;
|
||||
withDraftIssueWrapper?: boolean;
|
||||
storeType?: TCreateModalStoreTypes;
|
||||
isDraft?: boolean;
|
||||
}
|
||||
|
||||
export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((props) => {
|
||||
const { data, isOpen, onClose, onSubmit, withDraftIssueWrapper = true, storeType = EIssuesStoreType.PROJECT } = props;
|
||||
const {
|
||||
data,
|
||||
isOpen,
|
||||
onClose,
|
||||
onSubmit,
|
||||
withDraftIssueWrapper = true,
|
||||
storeType = EIssuesStoreType.PROJECT,
|
||||
isDraft = false,
|
||||
} = props;
|
||||
// states
|
||||
const [changesMade, setChangesMade] = useState<Partial<TIssue> | null>(null);
|
||||
const [createMore, setCreateMore] = useState(false);
|
||||
@@ -42,6 +51,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
||||
const { issues: cycleIssues } = useIssues(EIssuesStoreType.CYCLE);
|
||||
const { issues: viewIssues } = useIssues(EIssuesStoreType.PROJECT_VIEW);
|
||||
const { issues: profileIssues } = useIssues(EIssuesStoreType.PROFILE);
|
||||
const { issues: draftIssueStore } = useIssues(EIssuesStoreType.DRAFT);
|
||||
// store mapping based on current store
|
||||
const issueStores = {
|
||||
[EIssuesStoreType.PROJECT]: {
|
||||
@@ -122,11 +132,16 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleCreateIssue = async (payload: Partial<TIssue>): Promise<TIssue | undefined> => {
|
||||
const handleCreateIssue = async (
|
||||
payload: Partial<TIssue>,
|
||||
is_draft_issue: boolean = false
|
||||
): Promise<TIssue | undefined> => {
|
||||
if (!workspaceSlug || !payload.project_id) return;
|
||||
|
||||
try {
|
||||
const response = await currentIssueStore.createIssue(workspaceSlug, payload.project_id, payload, viewId);
|
||||
const response = is_draft_issue
|
||||
? await draftIssueStore.createIssue(workspaceSlug, payload.project_id, payload)
|
||||
: await currentIssueStore.createIssue(workspaceSlug, payload.project_id, payload, viewId);
|
||||
if (!response) throw new Error();
|
||||
|
||||
currentIssueStore.fetchIssues(workspaceSlug, payload.project_id, "mutation", viewId);
|
||||
@@ -213,7 +228,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (formData: Partial<TIssue>) => {
|
||||
const handleFormSubmit = async (formData: Partial<TIssue>, is_draft_issue: boolean = false) => {
|
||||
if (!workspaceSlug || !formData.project_id || !storeType) return;
|
||||
|
||||
const payload: Partial<TIssue> = {
|
||||
@@ -222,7 +237,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
||||
};
|
||||
|
||||
let response: TIssue | undefined = undefined;
|
||||
if (!data?.id) response = await handleCreateIssue(payload);
|
||||
if (!data?.id) response = await handleCreateIssue(payload, is_draft_issue);
|
||||
else response = await handleUpdateIssue(payload);
|
||||
|
||||
if (response != undefined && onSubmit) await onSubmit(response);
|
||||
@@ -274,6 +289,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
||||
projectId={activeProjectId}
|
||||
isCreateMoreToggleEnabled={createMore}
|
||||
onCreateMoreToggleChange={handleCreateMoreToggleChange}
|
||||
isDraft={isDraft}
|
||||
/>
|
||||
) : (
|
||||
<IssueFormRoot
|
||||
@@ -287,6 +303,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
||||
onCreateMoreToggleChange={handleCreateMoreToggleChange}
|
||||
onSubmit={handleFormSubmit}
|
||||
projectId={activeProjectId}
|
||||
isDraft={isDraft}
|
||||
/>
|
||||
)}
|
||||
</Dialog.Panel>
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
// ui
|
||||
import { CustomDatePicker } from "components/ui";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { CalendarCheck } from "lucide-react";
|
||||
// helpers
|
||||
import { findHowManyDaysLeft, renderFormattedDate } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
issue: TIssue;
|
||||
onChange: (date: string | null) => void;
|
||||
handleOnOpen?: () => void;
|
||||
handleOnClose?: () => void;
|
||||
tooltipPosition?: "top" | "bottom";
|
||||
className?: string;
|
||||
noBorder?: boolean;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export const ViewDueDateSelect: React.FC<Props> = ({
|
||||
issue,
|
||||
onChange,
|
||||
handleOnOpen,
|
||||
handleOnClose,
|
||||
tooltipPosition = "top",
|
||||
className = "",
|
||||
noBorder = false,
|
||||
disabled,
|
||||
}) => {
|
||||
const minDate = issue.start_date ? new Date(issue.start_date) : null;
|
||||
minDate?.setDate(minDate.getDate());
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
tooltipHeading="Due date"
|
||||
tooltipContent={issue.target_date ? renderFormattedDate(issue.target_date) ?? "N/A" : "N/A"}
|
||||
position={tooltipPosition}
|
||||
>
|
||||
<div
|
||||
className={`group max-w-[6.5rem] flex-shrink-0 ${className} ${
|
||||
issue.target_date === null
|
||||
? ""
|
||||
: issue.target_date < new Date().toISOString()
|
||||
? "text-red-600"
|
||||
: findHowManyDaysLeft(issue.target_date) <= 3 && "text-orange-400"
|
||||
}`}
|
||||
>
|
||||
<CustomDatePicker
|
||||
value={issue?.target_date}
|
||||
onChange={onChange}
|
||||
className={`bg-transparent ${issue?.target_date ? "w-[6.5rem]" : "w-[5rem] text-center"}`}
|
||||
customInput={
|
||||
<div
|
||||
className={`flex items-center justify-center gap-2 rounded border border-custom-border-200 px-2 py-1 text-xs shadow-sm duration-200 hover:bg-custom-background-80 ${
|
||||
issue.target_date ? "pr-6 text-custom-text-300" : "text-custom-text-400"
|
||||
} ${disabled ? "cursor-not-allowed" : "cursor-pointer"}`}
|
||||
>
|
||||
{issue.target_date ? (
|
||||
<>
|
||||
<CalendarCheck className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<span>{renderFormattedDate(issue.target_date) ?? "_ _"}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CalendarCheck className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<span>Due Date</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
minDate={minDate ?? undefined}
|
||||
noBorder={noBorder}
|
||||
handleOnOpen={handleOnOpen}
|
||||
handleOnClose={handleOnClose}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -1,64 +0,0 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Triangle } from "lucide-react";
|
||||
import sortBy from "lodash/sortBy";
|
||||
// store hooks
|
||||
import { useEstimate } from "hooks/store";
|
||||
// ui
|
||||
import { CustomSelect, Tooltip } from "@plane/ui";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
issue: TIssue;
|
||||
onChange: (data: number) => void;
|
||||
tooltipPosition?: "top" | "bottom";
|
||||
customButton?: boolean;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export const ViewEstimateSelect: React.FC<Props> = observer((props) => {
|
||||
const { issue, onChange, tooltipPosition = "top", customButton = false, disabled } = props;
|
||||
const { areEstimatesEnabledForCurrentProject, activeEstimateDetails, getEstimatePointValue } = useEstimate();
|
||||
|
||||
const estimateValue = getEstimatePointValue(issue.estimate_point, issue.project_id);
|
||||
|
||||
const estimateLabels = (
|
||||
<Tooltip tooltipHeading="Estimate" tooltipContent={estimateValue} position={tooltipPosition}>
|
||||
<div className="flex items-center gap-1 text-custom-text-200">
|
||||
<Triangle className="h-3 w-3" />
|
||||
{estimateValue ?? "None"}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
if (!areEstimatesEnabledForCurrentProject) return null;
|
||||
|
||||
return (
|
||||
<CustomSelect
|
||||
value={issue.estimate_point}
|
||||
onChange={onChange}
|
||||
{...(customButton ? { customButton: estimateLabels } : { label: estimateLabels })}
|
||||
maxHeight="md"
|
||||
noChevron
|
||||
disabled={disabled}
|
||||
>
|
||||
<CustomSelect.Option value={null}>
|
||||
<>
|
||||
<span>
|
||||
<Triangle className="h-3 w-3" />
|
||||
</span>
|
||||
None
|
||||
</>
|
||||
</CustomSelect.Option>
|
||||
{sortBy(activeEstimateDetails?.points, "key")?.map((estimate) => (
|
||||
<CustomSelect.Option key={estimate.id} value={estimate.key}>
|
||||
<>
|
||||
<Triangle className="h-3 w-3" />
|
||||
{estimate.value}
|
||||
</>
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
);
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from "./due-date";
|
||||
export * from "./estimate";
|
||||
export * from "./start-date";
|
||||
@@ -1,72 +0,0 @@
|
||||
// ui
|
||||
import { CustomDatePicker } from "components/ui";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { CalendarClock } from "lucide-react";
|
||||
// helpers
|
||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
issue: TIssue;
|
||||
onChange: (date: string | null) => void;
|
||||
handleOnOpen?: () => void;
|
||||
handleOnClose?: () => void;
|
||||
tooltipPosition?: "top" | "bottom";
|
||||
className?: string;
|
||||
noBorder?: boolean;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export const ViewStartDateSelect: React.FC<Props> = ({
|
||||
issue,
|
||||
onChange,
|
||||
handleOnOpen,
|
||||
handleOnClose,
|
||||
tooltipPosition = "top",
|
||||
className = "",
|
||||
noBorder = false,
|
||||
disabled,
|
||||
}) => {
|
||||
const maxDate = issue.target_date ? new Date(issue.target_date) : null;
|
||||
maxDate?.setDate(maxDate.getDate());
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
tooltipHeading="Start date"
|
||||
tooltipContent={issue.start_date ? renderFormattedDate(issue.start_date) ?? "N/A" : "N/A"}
|
||||
position={tooltipPosition}
|
||||
>
|
||||
<div className={`group max-w-[6.5rem] flex-shrink-0 ${className}`}>
|
||||
<CustomDatePicker
|
||||
value={issue?.start_date}
|
||||
onChange={onChange}
|
||||
maxDate={maxDate ?? undefined}
|
||||
noBorder={noBorder}
|
||||
handleOnOpen={handleOnOpen}
|
||||
customInput={
|
||||
<div
|
||||
className={`flex items-center justify-center gap-2 rounded border border-custom-border-200 px-2 py-1 text-xs shadow-sm duration-200 hover:bg-custom-background-80 ${
|
||||
issue?.start_date ? "pr-6 text-custom-text-300" : "text-custom-text-400"
|
||||
} ${disabled ? "cursor-not-allowed" : "cursor-pointer"}`}
|
||||
>
|
||||
{issue?.start_date ? (
|
||||
<>
|
||||
<CalendarClock className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<span>{renderFormattedDate(issue?.start_date ?? "_ _")}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CalendarClock className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<span>Start Date</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
handleOnClose={handleOnClose}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -13,37 +13,32 @@ export const ModulesListGanttChartView: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
// store
|
||||
const { projectModuleIds, moduleMap } = useModule();
|
||||
const { currentProjectDetails } = useProject();
|
||||
const { projectModuleIds, moduleMap, updateModuleDetails } = useModule();
|
||||
|
||||
const handleModuleUpdate = (module: IModule, payload: IBlockUpdateData) => {
|
||||
if (!workspaceSlug) return;
|
||||
// FIXME
|
||||
//updateModuleGanttStructure(workspaceSlug.toString(), module.project, module, payload);
|
||||
const handleModuleUpdate = async (module: IModule, data: IBlockUpdateData) => {
|
||||
if (!workspaceSlug || !module) return;
|
||||
|
||||
const payload: any = { ...data };
|
||||
if (data.sort_order) payload.sort_order = data.sort_order.newSortOrder;
|
||||
|
||||
await updateModuleDetails(workspaceSlug.toString(), module.project, module.id, payload);
|
||||
};
|
||||
|
||||
const blockFormat = (blocks: string[]) =>
|
||||
blocks && blocks.length > 0
|
||||
? blocks
|
||||
.filter((blockId) => {
|
||||
const block = moduleMap[blockId];
|
||||
return block.start_date && block.target_date && new Date(block.start_date) <= new Date(block.target_date);
|
||||
})
|
||||
.map((blockId) => {
|
||||
const block = moduleMap[blockId];
|
||||
return {
|
||||
data: block,
|
||||
id: block.id,
|
||||
sort_order: block.sort_order,
|
||||
start_date: new Date(block.start_date ?? ""),
|
||||
target_date: new Date(block.target_date ?? ""),
|
||||
};
|
||||
})
|
||||
: [];
|
||||
blocks?.map((blockId) => {
|
||||
const block = moduleMap[blockId];
|
||||
return {
|
||||
data: block,
|
||||
id: block.id,
|
||||
sort_order: block.sort_order,
|
||||
start_date: block.start_date ? new Date(block.start_date) : null,
|
||||
target_date: block.target_date ? new Date(block.target_date) : null,
|
||||
};
|
||||
});
|
||||
|
||||
const isAllowed = currentProjectDetails?.member_role === 20 || currentProjectDetails?.member_role === 15;
|
||||
|
||||
const modules = projectModuleIds;
|
||||
return (
|
||||
<div className="h-full w-full overflow-y-auto">
|
||||
<GanttChartRoot
|
||||
@@ -57,6 +52,7 @@ export const ModulesListGanttChartView: React.FC = observer(() => {
|
||||
enableBlockRightResize={isAllowed}
|
||||
enableBlockMove={isAllowed}
|
||||
enableReorder={isAllowed}
|
||||
showAllBlocks
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -77,7 +77,7 @@ export const ModuleMobileHeader = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="block md:hidden">
|
||||
<ProjectAnalyticsModal
|
||||
isOpen={analyticsModal}
|
||||
onClose={() => setAnalyticsModal(false)}
|
||||
@@ -157,6 +157,6 @@ export const ModuleMobileHeader = () => {
|
||||
Analytics
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -208,9 +208,6 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
<Tooltip tooltipContent="Snooze">
|
||||
<CustomMenu
|
||||
className="flex items-center"
|
||||
menuButtonOnClick={(e: { stopPropagation: () => void }) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
customButton={
|
||||
<div className="flex w-full items-center gap-x-2 rounded bg-custom-background-80 p-0.5 text-sm hover:bg-custom-background-100">
|
||||
<Clock className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
|
||||
@@ -22,16 +22,15 @@ export const ProfileNavbar: React.FC<Props> = (props) => {
|
||||
const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB;
|
||||
|
||||
return (
|
||||
<div className="sticky -top-0.5 z-10 flex items-center justify-between gap-4 border-b border-custom-border-300 bg-custom-background-100 px-4 sm:px-5 md:static">
|
||||
<div className="sticky -top-0.5 z-10 hidden md:flex items-center justify-between gap-4 border-b border-custom-border-300 bg-custom-background-100 px-4 sm:px-5 md:static">
|
||||
<div className="flex items-center overflow-x-scroll">
|
||||
{tabsList.map((tab) => (
|
||||
<Link key={tab.route} href={`/${workspaceSlug}/profile/${userId}/${tab.route}`}>
|
||||
<span
|
||||
className={`flex whitespace-nowrap border-b-2 p-4 text-sm font-medium outline-none ${
|
||||
router.pathname === tab.selected
|
||||
? "border-custom-primary-100 text-custom-primary-100"
|
||||
: "border-transparent"
|
||||
}`}
|
||||
className={`flex whitespace-nowrap border-b-2 p-4 text-sm font-medium outline-none ${router.pathname === tab.selected
|
||||
? "border-custom-primary-100 text-custom-primary-100"
|
||||
: "border-transparent"
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</span>
|
||||
|
||||
@@ -4,7 +4,7 @@ import useSWR from "swr";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useUser } from "hooks/store";
|
||||
import { useApplication, useUser } from "hooks/store";
|
||||
// services
|
||||
import { UserService } from "services/user.service";
|
||||
// components
|
||||
@@ -18,6 +18,8 @@ import { renderFormattedDate } from "helpers/date-time.helper";
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
// fetch-keys
|
||||
import { USER_PROFILE_PROJECT_SEGREGATION } from "constants/fetch-keys";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
// services
|
||||
const userService = new UserService();
|
||||
@@ -28,6 +30,8 @@ export const ProfileSidebar = observer(() => {
|
||||
const { workspaceSlug, userId } = router.query;
|
||||
// store hooks
|
||||
const { currentUser } = useUser();
|
||||
const { theme: themStore } = useApplication();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { data: userProjectsData } = useSWR(
|
||||
workspaceSlug && userId ? USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug.toString(), userId.toString()) : null,
|
||||
@@ -36,6 +40,14 @@ export const ProfileSidebar = observer(() => {
|
||||
: null
|
||||
);
|
||||
|
||||
useOutsideClickDetector(ref, () => {
|
||||
if (themStore.profileSidebarCollapsed === false) {
|
||||
if (window.innerWidth < 768) {
|
||||
themStore.toggleProfileSidebar();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const userDetails = [
|
||||
{
|
||||
label: "Joined on",
|
||||
@@ -47,8 +59,26 @@ export const ProfileSidebar = observer(() => {
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const handleToggleProfileSidebar = () => {
|
||||
if (window && window.innerWidth < 768) {
|
||||
themStore.toggleProfileSidebar(true);
|
||||
}
|
||||
if (window && themStore.profileSidebarCollapsed && window.innerWidth >= 768) {
|
||||
themStore.toggleProfileSidebar(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleToggleProfileSidebar);
|
||||
handleToggleProfileSidebar();
|
||||
return () => window.removeEventListener("resize", handleToggleProfileSidebar);
|
||||
}, [themStore]);
|
||||
|
||||
return (
|
||||
<div className="w-full flex-shrink-0 overflow-y-auto shadow-custom-shadow-sm md:h-full md:w-80 border-l border-custom-border-100">
|
||||
<div
|
||||
className={`flex-shrink-0 overflow-hidden overflow-y-auto shadow-custom-shadow-sm border-l border-custom-border-100 bg-custom-sidebar-background-100 h-full z-[5] fixed md:relative transition-all w-full md:w-[300px]`}
|
||||
style={themStore.profileSidebarCollapsed ? { marginLeft: `${window?.innerWidth || 0}px` } : {}}
|
||||
>
|
||||
{userProjectsData ? (
|
||||
<>
|
||||
<div className="relative h-32">
|
||||
@@ -132,13 +162,12 @@ export const ProfileSidebar = observer(() => {
|
||||
{project.assigned_issues > 0 && (
|
||||
<Tooltip tooltipContent="Completion percentage" position="left">
|
||||
<div
|
||||
className={`rounded px-1 py-0.5 text-xs font-medium ${
|
||||
completedIssuePercentage <= 35
|
||||
? "bg-red-500/10 text-red-500"
|
||||
: completedIssuePercentage <= 70
|
||||
className={`rounded px-1 py-0.5 text-xs font-medium ${completedIssuePercentage <= 35
|
||||
? "bg-red-500/10 text-red-500"
|
||||
: completedIssuePercentage <= 70
|
||||
? "bg-yellow-500/10 text-yellow-500"
|
||||
: "bg-green-500/10 text-green-500"
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
{completedIssuePercentage}%
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,10 @@ import { renderFormattedPayloadDate } from "./date-time.helper";
|
||||
// types
|
||||
import { TDurationFilterOptions, TIssuesListTypes } from "@plane/types";
|
||||
|
||||
/**
|
||||
* @description returns date range based on the duration filter
|
||||
* @param duration
|
||||
*/
|
||||
export const getCustomDates = (duration: TDurationFilterOptions): string => {
|
||||
const today = new Date();
|
||||
let firstDay, lastDay;
|
||||
@@ -30,6 +34,10 @@ export const getCustomDates = (duration: TDurationFilterOptions): string => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description returns redirection filters for the issues list
|
||||
* @param type
|
||||
*/
|
||||
export const getRedirectionFilters = (type: TIssuesListTypes): string => {
|
||||
const today = renderFormattedPayloadDate(new Date());
|
||||
|
||||
@@ -44,3 +52,20 @@ export const getRedirectionFilters = (type: TIssuesListTypes): string => {
|
||||
|
||||
return filterParams;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description returns the tab key based on the duration filter
|
||||
* @param duration
|
||||
* @param tab
|
||||
*/
|
||||
export const getTabKey = (duration: TDurationFilterOptions, tab: TIssuesListTypes | undefined): TIssuesListTypes => {
|
||||
if (!tab) return "completed";
|
||||
|
||||
if (tab === "completed") return tab;
|
||||
|
||||
if (duration === "none") return "pending";
|
||||
else {
|
||||
if (["upcoming", "overdue"].includes(tab)) return tab;
|
||||
else return "upcoming";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -87,11 +87,11 @@ export const renderFormattedTime = (date: string | Date, timeFormat: "12-hour" |
|
||||
* @example checkIfStringIsDate("2021-01-01", "2021-01-08") // 8
|
||||
*/
|
||||
export const findTotalDaysInRange = (
|
||||
startDate: Date | string,
|
||||
endDate: Date | string,
|
||||
startDate: Date | string | undefined | null,
|
||||
endDate: Date | string | undefined | null,
|
||||
inclusive: boolean = true
|
||||
): number => {
|
||||
if (!startDate || !endDate) return 0;
|
||||
): number | undefined => {
|
||||
if (!startDate || !endDate) return undefined;
|
||||
// Parse the dates to check if they are valid
|
||||
const parsedStartDate = new Date(startDate);
|
||||
const parsedEndDate = new Date(endDate);
|
||||
@@ -110,8 +110,11 @@ export const findTotalDaysInRange = (
|
||||
* @param {boolean} inclusive (optional) // default true
|
||||
* @example findHowManyDaysLeft("2024-01-01") // 3
|
||||
*/
|
||||
export const findHowManyDaysLeft = (date: string | Date, inclusive: boolean = true): number => {
|
||||
if (!date) return 0;
|
||||
export const findHowManyDaysLeft = (
|
||||
date: Date | string | undefined | null,
|
||||
inclusive: boolean = true
|
||||
): number | undefined => {
|
||||
if (!date) return undefined;
|
||||
// Pass the date to findTotalDaysInRange function to find the total number of days in range from today
|
||||
return findTotalDaysInRange(new Date(), date, inclusive);
|
||||
};
|
||||
|
||||
@@ -1,26 +1,41 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
const useReloadConfirmations = (message?: string) => {
|
||||
//TODO: remove temp flag isActive later and use showAlert as the source of truth
|
||||
const useReloadConfirmations = (isActive = true) => {
|
||||
const [showAlert, setShowAlert] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const handleBeforeUnload = useCallback(
|
||||
(event: BeforeUnloadEvent) => {
|
||||
if (!isActive || !showAlert) return;
|
||||
event.preventDefault();
|
||||
event.returnValue = "";
|
||||
return message ?? "Are you sure you want to leave?";
|
||||
},
|
||||
[message]
|
||||
[isActive, showAlert]
|
||||
);
|
||||
|
||||
const handleRouteChangeStart = useCallback(
|
||||
(url: string) => {
|
||||
if (!isActive || !showAlert) return;
|
||||
const leave = confirm("Are you sure you want to leave? Changes you made may not be saved.");
|
||||
if (!leave) {
|
||||
router.events.emit("routeChangeError");
|
||||
throw `Route change to "${url}" was aborted (this error can be safely ignored).`;
|
||||
}
|
||||
},
|
||||
[isActive, showAlert, router.events]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showAlert) {
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
}, [handleBeforeUnload, showAlert]);
|
||||
router.events.on("routeChangeStart", handleRouteChangeStart);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
router.events.off("routeChangeStart", handleRouteChangeStart);
|
||||
};
|
||||
}, [handleBeforeUnload, handleRouteChangeStart, router.events]);
|
||||
|
||||
return { setShowAlert };
|
||||
};
|
||||
|
||||
@@ -2,6 +2,11 @@ import { FC, ReactNode } from "react";
|
||||
// layout
|
||||
import { ProfileSettingsLayout } from "layouts/settings-layout";
|
||||
import { ProfilePreferenceSettingsSidebar } from "./sidebar";
|
||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
interface IProfilePreferenceSettingsLayout {
|
||||
children: ReactNode;
|
||||
@@ -10,9 +15,57 @@ interface IProfilePreferenceSettingsLayout {
|
||||
|
||||
export const ProfilePreferenceSettingsLayout: FC<IProfilePreferenceSettingsLayout> = (props) => {
|
||||
const { children, header } = props;
|
||||
const router = useRouter();
|
||||
|
||||
const showMenuItem = () => {
|
||||
const item = router.asPath.split('/');
|
||||
let splittedItem = item[item.length - 1];
|
||||
splittedItem = splittedItem.replace(splittedItem[0], splittedItem[0].toUpperCase());
|
||||
console.log(splittedItem);
|
||||
return splittedItem;
|
||||
}
|
||||
|
||||
const profilePreferenceLinks: Array<{
|
||||
label: string;
|
||||
href: string;
|
||||
}> = [
|
||||
{
|
||||
label: "Theme",
|
||||
href: `/profile/preferences/theme`,
|
||||
},
|
||||
{
|
||||
label: "Email",
|
||||
href: `/profile/preferences/email`,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ProfileSettingsLayout>
|
||||
<ProfileSettingsLayout header={
|
||||
<div className="md:hidden flex flex-shrink-0 gap-4 items-center justify-start border-b border-custom-border-200 p-4">
|
||||
<SidebarHamburgerToggle />
|
||||
<CustomMenu
|
||||
maxHeight={"md"}
|
||||
className="flex flex-grow justify-center text-custom-text-200 text-sm"
|
||||
placement="bottom-start"
|
||||
customButton={
|
||||
<div className="flex gap-2 items-center px-2 py-1.5 border rounded-md border-custom-border-400">
|
||||
<span className="flex flex-grow justify-center text-custom-text-200 text-sm">{showMenuItem()}</span>
|
||||
<ChevronDown className="w-4 h-4 text-custom-text-400" />
|
||||
</div>
|
||||
}
|
||||
customButtonClassName="flex flex-grow justify-start text-custom-text-200 text-sm"
|
||||
>
|
||||
<></>
|
||||
{profilePreferenceLinks.map((link) => (
|
||||
<CustomMenu.MenuItem
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Link key={link.href} href={link.href} className="text-custom-text-300 w-full">{link.label}</Link>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
}>
|
||||
<div className="relative flex h-screen w-full overflow-hidden">
|
||||
<ProfilePreferenceSettingsSidebar />
|
||||
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">
|
||||
|
||||
@@ -9,28 +9,27 @@ export const ProfilePreferenceSettingsSidebar = () => {
|
||||
label: string;
|
||||
href: string;
|
||||
}> = [
|
||||
{
|
||||
label: "Theme",
|
||||
href: `/profile/preferences/theme`,
|
||||
},
|
||||
{
|
||||
label: "Email",
|
||||
href: `/profile/preferences/email`,
|
||||
},
|
||||
];
|
||||
{
|
||||
label: "Theme",
|
||||
href: `/profile/preferences/theme`,
|
||||
},
|
||||
{
|
||||
label: "Email",
|
||||
href: `/profile/preferences/email`,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className="flex w-96 flex-col gap-6 px-8 py-12">
|
||||
<div className="hidden md:flex w-96 flex-col gap-6 px-8 py-12">
|
||||
<div className="flex flex-col gap-4">
|
||||
<span className="text-xs font-semibold text-custom-text-400">Preference</span>
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
{profilePreferenceLinks.map((link) => (
|
||||
<Link key={link.href} href={link.href}>
|
||||
<div
|
||||
className={`rounded-md px-4 py-2 text-sm font-medium ${
|
||||
(link.label === "Import" ? router.asPath.includes(link.href) : router.asPath === link.href)
|
||||
? "bg-custom-primary-100/10 text-custom-primary-100"
|
||||
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
|
||||
}`}
|
||||
className={`rounded-md px-4 py-2 text-sm font-medium ${(link.label === "Import" ? router.asPath.includes(link.href) : router.asPath === link.href)
|
||||
? "bg-custom-primary-100/10 text-custom-primary-100"
|
||||
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
|
||||
}`}
|
||||
>
|
||||
{link.label}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { mutate } from "swr";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -12,6 +12,7 @@ import useToast from "hooks/use-toast";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// constants
|
||||
import { PROFILE_ACTION_LINKS } from "constants/profile";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
|
||||
const WORKSPACE_ACTION_LINKS = [
|
||||
{
|
||||
@@ -52,6 +53,35 @@ export const ProfileLayoutSidebar = observer(() => {
|
||||
currentUserSettings?.workspace?.fallback_workspace_slug ||
|
||||
"";
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useOutsideClickDetector(ref, () => {
|
||||
if (sidebarCollapsed === false) {
|
||||
if (window.innerWidth < 768) {
|
||||
toggleSidebar();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (window.innerWidth <= 768) {
|
||||
toggleSidebar(true);
|
||||
}
|
||||
};
|
||||
handleResize();
|
||||
window.addEventListener("resize", handleResize);
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, [toggleSidebar]);
|
||||
|
||||
const handleItemClick = () => {
|
||||
if (window.innerWidth < 768) {
|
||||
toggleSidebar();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSignOut = async () => {
|
||||
setIsSigningOut(true);
|
||||
|
||||
@@ -73,16 +103,18 @@ export const ProfileLayoutSidebar = observer(() => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-custom-sidebar-border-200 bg-custom-sidebar-background-100 duration-300 md:relative ${
|
||||
sidebarCollapsed ? "" : "md:w-[280px]"
|
||||
} ${sidebarCollapsed ? "left-0" : "-left-full md:left-0"}`}
|
||||
className={`fixed inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-custom-sidebar-border-200 bg-custom-sidebar-background-100 duration-300 md:relative
|
||||
${sidebarCollapsed ? "-ml-[280px]" : ""}
|
||||
sm:${sidebarCollapsed ? "-ml-[280px]" : ""}
|
||||
md:ml-0 ${sidebarCollapsed ? "w-[80px]" : "w-[280px]"}
|
||||
lg:ml-0 ${sidebarCollapsed ? "w-[80px]" : "w-[280px]"}
|
||||
`}
|
||||
>
|
||||
<div className="flex h-full w-full flex-col gap-y-4">
|
||||
<div ref={ref} className="flex h-full w-full flex-col gap-y-4">
|
||||
<Link href={`/${redirectWorkspaceSlug}`}>
|
||||
<div
|
||||
className={`flex flex-shrink-0 items-center gap-2 truncate px-4 pt-4 ${
|
||||
sidebarCollapsed ? "justify-center" : ""
|
||||
}`}
|
||||
className={`flex flex-shrink-0 items-center gap-2 truncate px-4 pt-4 ${sidebarCollapsed ? "justify-center" : ""
|
||||
}`}
|
||||
>
|
||||
<span className="grid h-5 w-5 flex-shrink-0 place-items-center">
|
||||
<ChevronLeft className="h-5 w-5" strokeWidth={1} />
|
||||
@@ -101,14 +133,13 @@ export const ProfileLayoutSidebar = observer(() => {
|
||||
if (link.key === "change-password" && currentUser?.is_password_autoset) return null;
|
||||
|
||||
return (
|
||||
<Link key={link.key} href={link.href} className="block w-full">
|
||||
<Link key={link.key} href={link.href} className="block w-full" onClick={handleItemClick}>
|
||||
<Tooltip tooltipContent={link.label} position="right" className="ml-2" disabled={!sidebarCollapsed}>
|
||||
<div
|
||||
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
|
||||
link.highlight(router.pathname)
|
||||
? "bg-custom-primary-100/10 text-custom-primary-100"
|
||||
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
|
||||
} ${sidebarCollapsed ? "justify-center" : ""}`}
|
||||
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${link.highlight(router.pathname)
|
||||
? "bg-custom-primary-100/10 text-custom-primary-100"
|
||||
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
|
||||
} ${sidebarCollapsed ? "justify-center" : ""}`}
|
||||
>
|
||||
{<link.Icon className="h-4 w-4" />}
|
||||
{!sidebarCollapsed && link.label}
|
||||
@@ -129,19 +160,17 @@ export const ProfileLayoutSidebar = observer(() => {
|
||||
<Link
|
||||
key={workspace.id}
|
||||
href={`/${workspace.slug}`}
|
||||
className={`flex flex-grow cursor-pointer select-none items-center truncate text-left text-sm font-medium ${
|
||||
sidebarCollapsed ? "justify-center" : `justify-between`
|
||||
}`}
|
||||
className={`flex flex-grow cursor-pointer select-none items-center truncate text-left text-sm font-medium ${sidebarCollapsed ? "justify-center" : `justify-between`
|
||||
}`}
|
||||
onClick={handleItemClick}
|
||||
>
|
||||
<span
|
||||
className={`flex w-full flex-grow items-center gap-x-2 truncate rounded-md px-3 py-1 hover:bg-custom-sidebar-background-80 ${
|
||||
sidebarCollapsed ? "justify-center" : ""
|
||||
}`}
|
||||
className={`flex w-full flex-grow items-center gap-x-2 truncate rounded-md px-3 py-1 hover:bg-custom-sidebar-background-80 ${sidebarCollapsed ? "justify-center" : ""
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`relative flex h-6 w-6 flex-shrink-0 items-center justify-center p-2 text-xs uppercase ${
|
||||
!workspace?.logo && "rounded bg-custom-primary-500 text-white"
|
||||
}`}
|
||||
className={`relative flex h-6 w-6 flex-shrink-0 items-center justify-center p-2 text-xs uppercase ${!workspace?.logo && "rounded bg-custom-primary-500 text-white"
|
||||
}`}
|
||||
>
|
||||
{workspace?.logo && workspace.logo !== "" ? (
|
||||
<img
|
||||
@@ -163,12 +192,11 @@ export const ProfileLayoutSidebar = observer(() => {
|
||||
)}
|
||||
<div className="mt-1.5">
|
||||
{WORKSPACE_ACTION_LINKS.map((link) => (
|
||||
<Link className="block w-full" key={link.key} href={link.href}>
|
||||
<Link className="block w-full" key={link.key} href={link.href} onClick={handleItemClick}>
|
||||
<Tooltip tooltipContent={link.label} position="right" className="ml-2" disabled={!sidebarCollapsed}>
|
||||
<div
|
||||
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium text-custom-sidebar-text-200 outline-none hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80 ${
|
||||
sidebarCollapsed ? "justify-center" : ""
|
||||
}`}
|
||||
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium text-custom-sidebar-text-200 outline-none hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80 ${sidebarCollapsed ? "justify-center" : ""
|
||||
}`}
|
||||
>
|
||||
{<link.Icon className="h-4 w-4" />}
|
||||
{!sidebarCollapsed && link.label}
|
||||
@@ -180,9 +208,8 @@ export const ProfileLayoutSidebar = observer(() => {
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 flex-grow items-end px-6 py-2">
|
||||
<div
|
||||
className={`flex w-full ${
|
||||
sidebarCollapsed ? "flex-col justify-center gap-2" : "items-center justify-between gap-2"
|
||||
}`}
|
||||
className={`flex w-full ${sidebarCollapsed ? "flex-col justify-center gap-2" : "items-center justify-between gap-2"
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@@ -202,9 +229,8 @@ export const ProfileLayoutSidebar = observer(() => {
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`ml-auto hidden place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 md:grid ${
|
||||
sidebarCollapsed ? "w-full" : ""
|
||||
}`}
|
||||
className={`ml-auto hidden place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 md:grid ${sidebarCollapsed ? "w-full" : ""
|
||||
}`}
|
||||
onClick={() => toggleSidebar()}
|
||||
>
|
||||
<MoveLeft className={`h-3.5 w-3.5 duration-300 ${sidebarCollapsed ? "rotate-180" : ""}`} />
|
||||
|
||||
@@ -28,9 +28,8 @@ export const ProfileAuthWrapper: React.FC<Props> = observer((props) => {
|
||||
const isAuthorizedPath = router.pathname.includes("assigned" || "created" || "subscribed");
|
||||
|
||||
return (
|
||||
<div className="h-full w-full md:flex md:flex-row-reverse md:overflow-hidden">
|
||||
<ProfileSidebar />
|
||||
<div className="flex w-full flex-col md:h-full md:overflow-hidden">
|
||||
<div className="h-full w-full realtive flex flex-row">
|
||||
<div className="w-full realtive flex flex-col">
|
||||
<ProfileNavbar isAuthorized={isAuthorized} showProfileIssuesFilter={showProfileIssuesFilter} />
|
||||
{isAuthorized || !isAuthorizedPath ? (
|
||||
<div className={`w-full overflow-hidden md:h-full ${className}`}>{children}</div>
|
||||
@@ -40,6 +39,8 @@ export const ProfileAuthWrapper: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ProfileSidebar />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ const ProfileAssignedIssuesPage: NextPageWithLayout = () => <ProfileIssuesPage t
|
||||
|
||||
ProfileAssignedIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<AppLayout header={<UserProfileHeader />}>
|
||||
<AppLayout header={<UserProfileHeader type="Assigned" />}>
|
||||
<ProfileAuthWrapper showProfileIssuesFilter>{page}</ProfileAuthWrapper>
|
||||
</AppLayout>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ const ProfileCreatedIssuesPage: NextPageWithLayout = () => <ProfileIssuesPage ty
|
||||
|
||||
ProfileCreatedIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<AppLayout header={<UserProfileHeader />}>
|
||||
<AppLayout header={<UserProfileHeader type="Created" />}>
|
||||
<ProfileAuthWrapper showProfileIssuesFilter>{page}</ProfileAuthWrapper>
|
||||
</AppLayout>
|
||||
);
|
||||
|
||||
@@ -56,7 +56,7 @@ const ProfileOverviewPage: NextPageWithLayout = () => {
|
||||
|
||||
ProfileOverviewPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<AppLayout header={<UserProfileHeader />}>
|
||||
<AppLayout header={<UserProfileHeader type='Summary' />}>
|
||||
<ProfileAuthWrapper>{page}</ProfileAuthWrapper>
|
||||
</AppLayout>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ const ProfileSubscribedIssuesPage: NextPageWithLayout = () => <ProfileIssuesPage
|
||||
|
||||
ProfileSubscribedIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<AppLayout header={<UserProfileHeader />}>
|
||||
<AppLayout header={<UserProfileHeader type="Subscribed" />}>
|
||||
<ProfileAuthWrapper showProfileIssuesFilter>{page}</ProfileAuthWrapper>
|
||||
</AppLayout>
|
||||
);
|
||||
|
||||
@@ -57,9 +57,6 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
//TODO:fix reload confirmations, with mobx
|
||||
const { setShowAlert } = useReloadConfirmations();
|
||||
|
||||
const { handleSubmit, setValue, watch, getValues, control, reset } = useForm<IPage>({
|
||||
defaultValues: { name: "", description_html: "" },
|
||||
});
|
||||
@@ -92,6 +89,8 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
|
||||
|
||||
const pageStore = usePage(pageId as string);
|
||||
|
||||
const { setShowAlert } = useReloadConfirmations(pageStore?.isSubmitting === "submitting");
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (pageStore) {
|
||||
|
||||
@@ -50,16 +50,6 @@ class MyDocument extends Document {
|
||||
src="https://plausible.io/js/script.js"
|
||||
/>
|
||||
)}
|
||||
{process.env.NEXT_PUBLIC_POSTHOG_KEY && process.env.NEXT_PUBLIC_POSTHOG_HOST && (
|
||||
<Script id="posthog-tracking">
|
||||
{`!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
|
||||
posthog.init('${process.env.NEXT_PUBLIC_POSTHOG_KEY}',{api_host:'${process.env.NEXT_PUBLIC_POSTHOG_HOST}'})`}
|
||||
</Script>
|
||||
)}
|
||||
<Script
|
||||
type="text/javascript"
|
||||
src="https://api.useberry.com/integrations/liveUrl/scripts/useberryScript.js"
|
||||
/>
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
|
||||
@@ -21,6 +21,7 @@ import { USER_ACTIVITY } from "constants/fetch-keys";
|
||||
import { calculateTimeAgo } from "helpers/date-time.helper";
|
||||
// type
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
||||
|
||||
const userService = new UserService();
|
||||
|
||||
@@ -30,8 +31,10 @@ const ProfileActivityPage: NextPageWithLayout = observer(() => {
|
||||
const { currentUser } = useUser();
|
||||
|
||||
return (
|
||||
<section className="mx-auto mt-16 flex h-full w-full flex-col overflow-hidden px-8 pb-8 lg:w-3/5">
|
||||
<div className="flex items-center border-b border-custom-border-100 pb-3.5">
|
||||
|
||||
<section className="mx-auto mt-5 md:mt-16 flex h-full w-full flex-col overflow-hidden px-8 pb-8 lg:w-3/5">
|
||||
<div className="flex items-center border-b border-custom-border-100 gap-4 pb-3.5">
|
||||
<SidebarHamburgerToggle />
|
||||
<h3 className="text-xl font-medium">Activity</h3>
|
||||
</div>
|
||||
{userActivity ? (
|
||||
@@ -94,12 +97,12 @@ const ProfileActivityPage: NextPageWithLayout = observer(() => {
|
||||
|
||||
const message =
|
||||
activityItem.verb === "created" &&
|
||||
activityItem.field !== "cycles" &&
|
||||
activityItem.field !== "modules" &&
|
||||
activityItem.field !== "attachment" &&
|
||||
activityItem.field !== "link" &&
|
||||
activityItem.field !== "estimate" &&
|
||||
!activityItem.field ? (
|
||||
activityItem.field !== "cycles" &&
|
||||
activityItem.field !== "modules" &&
|
||||
activityItem.field !== "attachment" &&
|
||||
activityItem.field !== "link" &&
|
||||
activityItem.field !== "estimate" &&
|
||||
!activityItem.field ? (
|
||||
<span>
|
||||
created <IssueLink activity={activityItem} />
|
||||
</span>
|
||||
@@ -187,6 +190,7 @@ const ProfileActivityPage: NextPageWithLayout = observer(() => {
|
||||
</Loader>
|
||||
)}
|
||||
</section>
|
||||
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ProfileSettingsLayout } from "layouts/settings-layout";
|
||||
import { Button, Input, Spinner } from "@plane/ui";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
||||
|
||||
interface FormValues {
|
||||
old_password: string;
|
||||
@@ -86,6 +87,10 @@ const ChangePasswordPage: NextPageWithLayout = observer(() => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="block md:hidden flex-shrink-0 border-b border-custom-border-200 p-4">
|
||||
<SidebarHamburgerToggle />
|
||||
</div>
|
||||
<form
|
||||
onSubmit={handleSubmit(handleChangePassword)}
|
||||
className="mx-auto mt-16 flex h-full w-full flex-col gap-8 px-8 pb-8 lg:w-3/5"
|
||||
@@ -168,6 +173,7 @@ const ChangePasswordPage: NextPageWithLayout = observer(() => {
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import type { NextPageWithLayout } from "lib/types";
|
||||
// constants
|
||||
import { USER_ROLES } from "constants/workspace";
|
||||
import { TIME_ZONES } from "constants/timezones";
|
||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
||||
|
||||
const defaultValues: Partial<IUser> = {
|
||||
avatar: "",
|
||||
@@ -56,7 +57,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
|
||||
// store hooks
|
||||
const { currentUser: myProfile, updateCurrentUser, currentUserLoader } = useUser();
|
||||
// custom hooks
|
||||
const {} = useUserAuth({ user: myProfile, isLoading: currentUserLoader });
|
||||
const { } = useUserAuth({ user: myProfile, isLoading: currentUserLoader });
|
||||
|
||||
useEffect(() => {
|
||||
reset({ ...defaultValues, ...myProfile });
|
||||
@@ -136,304 +137,310 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Controller
|
||||
control={control}
|
||||
name="avatar"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<UserImageUploadModal
|
||||
isOpen={isImageUploadModalOpen}
|
||||
onClose={() => setIsImageUploadModalOpen(false)}
|
||||
isRemoving={isRemoving}
|
||||
handleDelete={() => handleDelete(myProfile?.avatar, true)}
|
||||
onSuccess={(url) => {
|
||||
onChange(url);
|
||||
handleSubmit(onSubmit)();
|
||||
setIsImageUploadModalOpen(false);
|
||||
}}
|
||||
value={value && value.trim() !== "" ? value : null}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<DeactivateAccountModal isOpen={deactivateAccountModal} onClose={() => setDeactivateAccountModal(false)} />
|
||||
<div className="mx-auto flex h-full w-full flex-col space-y-10 overflow-y-auto pt-16 px-8 pb-8 lg:w-3/5">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="flex w-full flex-col gap-8">
|
||||
<div className="relative h-44 w-full">
|
||||
<img
|
||||
src={watch("cover_image") ?? "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"}
|
||||
className="h-44 w-full rounded-lg object-cover"
|
||||
alt={myProfile?.first_name ?? "Cover image"}
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="block md:hidden flex-shrink-0 border-b border-custom-border-200 p-4">
|
||||
<SidebarHamburgerToggle />
|
||||
</div>
|
||||
<div className="overflow-hidden">
|
||||
<Controller
|
||||
control={control}
|
||||
name="avatar"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<UserImageUploadModal
|
||||
isOpen={isImageUploadModalOpen}
|
||||
onClose={() => setIsImageUploadModalOpen(false)}
|
||||
isRemoving={isRemoving}
|
||||
handleDelete={() => handleDelete(myProfile?.avatar, true)}
|
||||
onSuccess={(url) => {
|
||||
onChange(url);
|
||||
handleSubmit(onSubmit)();
|
||||
setIsImageUploadModalOpen(false);
|
||||
}}
|
||||
value={value && value.trim() !== "" ? value : null}
|
||||
/>
|
||||
<div className="absolute -bottom-6 left-8 flex items-end justify-between">
|
||||
<div className="flex gap-3">
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-lg bg-custom-background-90">
|
||||
<button type="button" onClick={() => setIsImageUploadModalOpen(true)}>
|
||||
{!watch("avatar") || watch("avatar") === "" ? (
|
||||
<div className="h-16 w-16 rounded-md bg-custom-background-80 p-2">
|
||||
<User2 className="h-full w-full text-custom-text-200" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative h-16 w-16 overflow-hidden">
|
||||
<img
|
||||
src={watch("avatar")}
|
||||
className="absolute left-0 top-0 h-full w-full rounded-lg object-cover"
|
||||
onClick={() => setIsImageUploadModalOpen(true)}
|
||||
alt={myProfile.display_name}
|
||||
role="button"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<DeactivateAccountModal isOpen={deactivateAccountModal} onClose={() => setDeactivateAccountModal(false)} />
|
||||
<div className="mx-auto flex h-full w-full flex-col space-y-10 overflow-y-auto pt-10 md:pt-16 px-8 pb-8 lg:w-3/5">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="flex w-full flex-col gap-8">
|
||||
<div className="relative h-44 w-full">
|
||||
<img
|
||||
src={watch("cover_image") ?? "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"}
|
||||
className="h-44 w-full rounded-lg object-cover"
|
||||
alt={myProfile?.first_name ?? "Cover image"}
|
||||
/>
|
||||
<div className="absolute -bottom-6 left-8 flex items-end justify-between">
|
||||
<div className="flex gap-3">
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-lg bg-custom-background-90">
|
||||
<button type="button" onClick={() => setIsImageUploadModalOpen(true)}>
|
||||
{!watch("avatar") || watch("avatar") === "" ? (
|
||||
<div className="h-16 w-16 rounded-md bg-custom-background-80 p-2">
|
||||
<User2 className="h-full w-full text-custom-text-200" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative h-16 w-16 overflow-hidden">
|
||||
<img
|
||||
src={watch("avatar")}
|
||||
className="absolute left-0 top-0 h-full w-full rounded-lg object-cover"
|
||||
onClick={() => setIsImageUploadModalOpen(true)}
|
||||
alt={myProfile.display_name}
|
||||
role="button"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-3 right-3 flex">
|
||||
<Controller
|
||||
control={control}
|
||||
name="cover_image"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<ImagePickerPopover
|
||||
label={"Change cover"}
|
||||
onChange={(imageUrl) => onChange(imageUrl)}
|
||||
control={control}
|
||||
value={value ?? "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-3 right-3 flex">
|
||||
<Controller
|
||||
control={control}
|
||||
name="cover_image"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<ImagePickerPopover
|
||||
label={"Change cover"}
|
||||
onChange={(imageUrl) => onChange(imageUrl)}
|
||||
control={control}
|
||||
value={value ?? "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="item-center mt-4 flex justify-between px-8">
|
||||
<div className="flex flex-col">
|
||||
<div className="item-center flex text-lg font-semibold text-custom-text-100">
|
||||
<span>{`${watch("first_name")} ${watch("last_name")}`}</span>
|
||||
</div>
|
||||
<span className="text-sm tracking-tight">{watch("email")}</span>
|
||||
</div>
|
||||
|
||||
<div className="item-center mt-4 flex justify-between px-8">
|
||||
<div className="flex flex-col">
|
||||
<div className="item-center flex text-lg font-semibold text-custom-text-100">
|
||||
<span>{`${watch("first_name")} ${watch("last_name")}`}</span>
|
||||
</div>
|
||||
<span className="text-sm tracking-tight">{watch("email")}</span>
|
||||
</div>
|
||||
|
||||
{/* <Link href={`/profile/${myProfile.id}`}>
|
||||
{/* <Link href={`/profile/${myProfile.id}`}>
|
||||
<span className="flex item-center gap-1 text-sm text-custom-primary-100 underline font-medium">
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
Activity Overview
|
||||
</span>
|
||||
</Link> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 px-8 lg:grid-cols-2 2xl:grid-cols-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">
|
||||
First name<span className="text-red-500">*</span>
|
||||
</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="first_name"
|
||||
rules={{
|
||||
required: "First name is required.",
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="first_name"
|
||||
<div className="grid grid-cols-1 gap-6 px-8 lg:grid-cols-2 2xl:grid-cols-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">
|
||||
First name<span className="text-red-500">*</span>
|
||||
</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="first_name"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.first_name)}
|
||||
placeholder="Enter your first name"
|
||||
className={`w-full rounded-md ${errors.first_name ? "border-red-500" : ""}`}
|
||||
rules={{
|
||||
required: "First name is required.",
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="first_name"
|
||||
name="first_name"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.first_name)}
|
||||
placeholder="Enter your first name"
|
||||
className={`w-full rounded-md ${errors.first_name ? "border-red-500" : ""}`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.first_name && <span className="text-xs text-red-500">Please enter first name</span>}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">Last name</h4>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="last_name"
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="last_name"
|
||||
name="last_name"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.last_name)}
|
||||
placeholder="Enter your last name"
|
||||
className="w-full rounded-md"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">
|
||||
Email<span className="text-red-500">*</span>
|
||||
</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="email"
|
||||
rules={{
|
||||
required: "Email is required.",
|
||||
}}
|
||||
render={({ field: { value, ref } }) => (
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={value}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.email)}
|
||||
placeholder="Enter your email"
|
||||
className={`w-full rounded-md cursor-not-allowed !bg-custom-background-80 ${
|
||||
errors.email ? "border-red-500" : ""
|
||||
}`}
|
||||
disabled
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">
|
||||
Role<span className="text-red-500">*</span>
|
||||
</h4>
|
||||
<Controller
|
||||
name="role"
|
||||
control={control}
|
||||
rules={{ required: "Role is required." }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
label={value ? value.toString() : "Select your role"}
|
||||
buttonClassName={errors.role ? "border-red-500" : "border-none"}
|
||||
className="rounded-md border-[0.5px] !border-custom-border-200"
|
||||
optionsClassName="w-full"
|
||||
input
|
||||
>
|
||||
{USER_ROLES.map((item) => (
|
||||
<CustomSelect.Option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
{errors.role && <span className="text-xs text-red-500">Please select a role</span>}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">
|
||||
Display name<span className="text-red-500">*</span>
|
||||
</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="display_name"
|
||||
rules={{
|
||||
required: "Display name is required.",
|
||||
validate: (value) => {
|
||||
if (value.trim().length < 1) return "Display name can't be empty.";
|
||||
|
||||
if (value.split(" ").length > 1) return "Display name can't have two consecutive spaces.";
|
||||
|
||||
if (value.replace(/\s/g, "").length < 1)
|
||||
return "Display name must be at least 1 characters long.";
|
||||
|
||||
if (value.replace(/\s/g, "").length > 20)
|
||||
return "Display name must be less than 20 characters long.";
|
||||
|
||||
return true;
|
||||
},
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="display_name"
|
||||
name="display_name"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.display_name)}
|
||||
placeholder="Enter your display name"
|
||||
className={`w-full ${errors.display_name ? "border-red-500" : ""}`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.display_name && <span className="text-xs text-red-500">Please enter display name</span>}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">
|
||||
Timezone<span className="text-red-500">*</span>
|
||||
</h4>
|
||||
|
||||
<Controller
|
||||
name="user_timezone"
|
||||
control={control}
|
||||
rules={{ required: "Time zone is required" }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CustomSearchSelect
|
||||
value={value}
|
||||
label={value ? TIME_ZONES.find((t) => t.value === value)?.label ?? value : "Select a timezone"}
|
||||
options={timeZoneOptions}
|
||||
onChange={onChange}
|
||||
optionsClassName="w-full"
|
||||
buttonClassName={errors.user_timezone ? "border-red-500" : "border-none"}
|
||||
className="rounded-md border-[0.5px] !border-custom-border-200"
|
||||
input
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.role && <span className="text-xs text-red-500">Please select a time zone</span>}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<Button variant="primary" type="submit" loading={isLoading}>
|
||||
{isLoading ? "Saving..." : "Save changes"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<Disclosure as="div" className="border-t border-custom-border-100 px-8">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Disclosure.Button as="button" type="button" className="flex w-full items-center justify-between py-4">
|
||||
<span className="text-lg tracking-tight">Deactivate account</span>
|
||||
<ChevronDown className={`h-5 w-5 transition-all ${open ? "rotate-180" : ""}`} />
|
||||
</Disclosure.Button>
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform opacity-0"
|
||||
enterTo="transform opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform opacity-100"
|
||||
leaveTo="transform opacity-0"
|
||||
>
|
||||
<Disclosure.Panel>
|
||||
<div className="flex flex-col gap-8">
|
||||
<span className="text-sm tracking-tight">
|
||||
The danger zone of the profile page is a critical area that requires careful consideration and
|
||||
attention. When deactivating an account, all of the data and resources within that account will be
|
||||
permanently removed and cannot be recovered.
|
||||
</span>
|
||||
<div>
|
||||
<Button variant="danger" onClick={() => setDeactivateAccountModal(true)}>
|
||||
Deactivate account
|
||||
</Button>
|
||||
</div>
|
||||
{errors.first_name && <span className="text-xs text-red-500">Please enter first name</span>}
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">Last name</h4>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="last_name"
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="last_name"
|
||||
name="last_name"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.last_name)}
|
||||
placeholder="Enter your last name"
|
||||
className="w-full rounded-md"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">
|
||||
Email<span className="text-red-500">*</span>
|
||||
</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="email"
|
||||
rules={{
|
||||
required: "Email is required.",
|
||||
}}
|
||||
render={({ field: { value, ref } }) => (
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={value}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.email)}
|
||||
placeholder="Enter your email"
|
||||
className={`w-full rounded-md cursor-not-allowed !bg-custom-background-80 ${errors.email ? "border-red-500" : ""
|
||||
}`}
|
||||
disabled
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">
|
||||
Role<span className="text-red-500">*</span>
|
||||
</h4>
|
||||
<Controller
|
||||
name="role"
|
||||
control={control}
|
||||
rules={{ required: "Role is required." }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
label={value ? value.toString() : "Select your role"}
|
||||
buttonClassName={errors.role ? "border-red-500" : "border-none"}
|
||||
className="rounded-md border-[0.5px] !border-custom-border-200"
|
||||
optionsClassName="w-full"
|
||||
input
|
||||
>
|
||||
{USER_ROLES.map((item) => (
|
||||
<CustomSelect.Option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
{errors.role && <span className="text-xs text-red-500">Please select a role</span>}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">
|
||||
Display name<span className="text-red-500">*</span>
|
||||
</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="display_name"
|
||||
rules={{
|
||||
required: "Display name is required.",
|
||||
validate: (value) => {
|
||||
if (value.trim().length < 1) return "Display name can't be empty.";
|
||||
|
||||
if (value.split(" ").length > 1) return "Display name can't have two consecutive spaces.";
|
||||
|
||||
if (value.replace(/\s/g, "").length < 1)
|
||||
return "Display name must be at least 1 characters long.";
|
||||
|
||||
if (value.replace(/\s/g, "").length > 20)
|
||||
return "Display name must be less than 20 characters long.";
|
||||
|
||||
return true;
|
||||
},
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="display_name"
|
||||
name="display_name"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.display_name)}
|
||||
placeholder="Enter your display name"
|
||||
className={`w-full ${errors.display_name ? "border-red-500" : ""}`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.display_name && <span className="text-xs text-red-500">Please enter display name</span>}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">
|
||||
Timezone<span className="text-red-500">*</span>
|
||||
</h4>
|
||||
|
||||
<Controller
|
||||
name="user_timezone"
|
||||
control={control}
|
||||
rules={{ required: "Time zone is required" }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CustomSearchSelect
|
||||
value={value}
|
||||
label={value ? TIME_ZONES.find((t) => t.value === value)?.label ?? value : "Select a timezone"}
|
||||
options={timeZoneOptions}
|
||||
onChange={onChange}
|
||||
optionsClassName="w-full"
|
||||
buttonClassName={errors.user_timezone ? "border-red-500" : "border-none"}
|
||||
className="rounded-md border-[0.5px] !border-custom-border-200"
|
||||
input
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.role && <span className="text-xs text-red-500">Please select a time zone</span>}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<Button variant="primary" type="submit" loading={isLoading}>
|
||||
{isLoading ? "Saving..." : "Save changes"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<Disclosure as="div" className="border-t border-custom-border-100 px-8">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Disclosure.Button as="button" type="button" className="flex w-full items-center justify-between py-4">
|
||||
<span className="text-lg tracking-tight">Deactivate account</span>
|
||||
<ChevronDown className={`h-5 w-5 transition-all ${open ? "rotate-180" : ""}`} />
|
||||
</Disclosure.Button>
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform opacity-0"
|
||||
enterTo="transform opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform opacity-100"
|
||||
leaveTo="transform opacity-0"
|
||||
>
|
||||
<Disclosure.Panel>
|
||||
<div className="flex flex-col gap-8">
|
||||
<span className="text-sm tracking-tight">
|
||||
The danger zone of the profile page is a critical area that requires careful consideration and
|
||||
attention. When deactivating an account, all of the data and resources within that account will be
|
||||
permanently removed and cannot be recovered.
|
||||
</span>
|
||||
<div>
|
||||
<Button variant="danger" onClick={() => setDeactivateAccountModal(true)}>
|
||||
Deactivate account
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -48,7 +48,7 @@ const ProfilePreferencesThemePage: NextPageWithLayout = observer(() => {
|
||||
return (
|
||||
<>
|
||||
{currentUser ? (
|
||||
<div className="mx-auto mt-14 h-full w-full overflow-y-auto px-6 lg:px-20 pb-8">
|
||||
<div className="mx-auto mt-10 md:mt-14 h-full w-full overflow-y-auto px-6 lg:px-20 pb-8">
|
||||
<div className="flex items-center border-b border-custom-border-100 pb-3.5">
|
||||
<h3 className="text-xl font-medium">Preferences</h3>
|
||||
</div>
|
||||
|
||||
@@ -7,15 +7,18 @@ export interface IThemeStore {
|
||||
// observables
|
||||
theme: string | null;
|
||||
sidebarCollapsed: boolean | undefined;
|
||||
profileSidebarCollapsed: boolean | undefined;
|
||||
// actions
|
||||
toggleSidebar: (collapsed?: boolean) => void;
|
||||
setTheme: (theme: any) => void;
|
||||
toggleProfileSidebar: (collapsed?: boolean) => void;
|
||||
}
|
||||
|
||||
export class ThemeStore implements IThemeStore {
|
||||
// observables
|
||||
sidebarCollapsed: boolean | undefined = undefined;
|
||||
theme: string | null = null;
|
||||
profileSidebarCollapsed: boolean | undefined = undefined;
|
||||
// root store
|
||||
rootStore;
|
||||
|
||||
@@ -24,9 +27,11 @@ export class ThemeStore implements IThemeStore {
|
||||
// observable
|
||||
sidebarCollapsed: observable.ref,
|
||||
theme: observable.ref,
|
||||
profileSidebarCollapsed: observable.ref,
|
||||
// action
|
||||
toggleSidebar: action,
|
||||
setTheme: action,
|
||||
toggleProfileSidebar: action,
|
||||
// computed
|
||||
});
|
||||
// root store
|
||||
@@ -46,6 +51,19 @@ export class ThemeStore implements IThemeStore {
|
||||
localStorage.setItem("app_sidebar_collapsed", this.sidebarCollapsed.toString());
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle the profile sidebar collapsed state
|
||||
* @param collapsed
|
||||
*/
|
||||
toggleProfileSidebar = (collapsed?: boolean) => {
|
||||
if (collapsed === undefined) {
|
||||
this.profileSidebarCollapsed = !this.profileSidebarCollapsed;
|
||||
} else {
|
||||
this.profileSidebarCollapsed = collapsed;
|
||||
}
|
||||
localStorage.setItem("profile_sidebar_collapsed", this.profileSidebarCollapsed.toString());
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the user theme and applies it to the platform
|
||||
* @param _theme
|
||||
|
||||
@@ -103,7 +103,7 @@ export class CycleStore implements ICycleStore {
|
||||
const projectId = this.rootStore.app.router.projectId;
|
||||
if (!projectId || !this.fetchedMap[projectId]) return null;
|
||||
let allCycles = Object.values(this.cycleMap ?? {}).filter((c) => c?.project === projectId);
|
||||
allCycles = sortBy(allCycles, [(c) => !c.is_favorite, (c) => c.name.toLowerCase()]);
|
||||
allCycles = sortBy(allCycles, [(c) => c.sort_order]);
|
||||
const allCycleIds = allCycles.map((c) => c.id);
|
||||
return allCycleIds;
|
||||
}
|
||||
@@ -118,7 +118,7 @@ export class CycleStore implements ICycleStore {
|
||||
const hasEndDatePassed = isPast(new Date(c.end_date ?? ""));
|
||||
return c.project === projectId && hasEndDatePassed;
|
||||
});
|
||||
completedCycles = sortBy(completedCycles, [(c) => !c.is_favorite, (c) => c.name.toLowerCase()]);
|
||||
completedCycles = sortBy(completedCycles, [(c) => c.sort_order]);
|
||||
const completedCycleIds = completedCycles.map((c) => c.id);
|
||||
return completedCycleIds;
|
||||
}
|
||||
@@ -133,7 +133,7 @@ export class CycleStore implements ICycleStore {
|
||||
const isStartDateUpcoming = isFuture(new Date(c.start_date ?? ""));
|
||||
return c.project === projectId && isStartDateUpcoming;
|
||||
});
|
||||
upcomingCycles = sortBy(upcomingCycles, [(c) => !c.is_favorite, (c) => c.name.toLowerCase()]);
|
||||
upcomingCycles = sortBy(upcomingCycles, [(c) => c.sort_order]);
|
||||
const upcomingCycleIds = upcomingCycles.map((c) => c.id);
|
||||
return upcomingCycleIds;
|
||||
}
|
||||
@@ -148,7 +148,7 @@ export class CycleStore implements ICycleStore {
|
||||
const hasEndDatePassed = isPast(new Date(c.end_date ?? ""));
|
||||
return c.project === projectId && !hasEndDatePassed;
|
||||
});
|
||||
incompleteCycles = sortBy(incompleteCycles, [(c) => !c.is_favorite, (c) => c.name.toLowerCase()]);
|
||||
incompleteCycles = sortBy(incompleteCycles, [(c) => c.sort_order]);
|
||||
const incompleteCycleIds = incompleteCycles.map((c) => c.id);
|
||||
return incompleteCycleIds;
|
||||
}
|
||||
@@ -162,7 +162,7 @@ export class CycleStore implements ICycleStore {
|
||||
let draftCycles = Object.values(this.cycleMap ?? {}).filter(
|
||||
(c) => c.project === projectId && !c.start_date && !c.end_date
|
||||
);
|
||||
draftCycles = sortBy(draftCycles, [(c) => !c.is_favorite, (c) => c.name.toLowerCase()]);
|
||||
draftCycles = sortBy(draftCycles, [(c) => c.sort_order]);
|
||||
const draftCycleIds = draftCycles.map((c) => c.id);
|
||||
return draftCycleIds;
|
||||
}
|
||||
@@ -203,7 +203,7 @@ export class CycleStore implements ICycleStore {
|
||||
if (!this.fetchedMap[projectId]) return null;
|
||||
|
||||
let cycles = Object.values(this.cycleMap ?? {}).filter((c) => c.project === projectId);
|
||||
cycles = sortBy(cycles, [(c) => !c.is_favorite, (c) => c.name.toLowerCase()]);
|
||||
cycles = sortBy(cycles, [(c) => c.sort_order]);
|
||||
const cycleIds = cycles.map((c) => c.id);
|
||||
return cycleIds || null;
|
||||
});
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { action, observable, makeObservable, computed, runInAction } from "mobx";
|
||||
import set from "lodash/set";
|
||||
import update from "lodash/update";
|
||||
import uniq from "lodash/uniq";
|
||||
import concat from "lodash/concat";
|
||||
import pull from "lodash/pull";
|
||||
// base class
|
||||
import { IssueHelperStore } from "../helpers/issue-helper.store";
|
||||
// services
|
||||
@@ -123,7 +127,7 @@ export class DraftIssues extends IssueHelperStore implements IDraftIssues {
|
||||
const response = await this.issueDraftService.createDraftIssue(workspaceSlug, projectId, data);
|
||||
|
||||
runInAction(() => {
|
||||
this.issues[projectId].push(response.id);
|
||||
update(this.issues, [projectId], (issueIds = []) => uniq(concat(issueIds, response.id)));
|
||||
});
|
||||
|
||||
this.rootStore.issues.addIssue([response]);
|
||||
@@ -136,8 +140,17 @@ export class DraftIssues extends IssueHelperStore implements IDraftIssues {
|
||||
|
||||
updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
|
||||
try {
|
||||
this.rootStore.issues.updateIssue(issueId, data);
|
||||
const response = await this.issueDraftService.updateDraftIssue(workspaceSlug, projectId, issueId, data);
|
||||
const response = await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data);
|
||||
|
||||
if (data.hasOwnProperty("is_draft") && data?.is_draft === false) {
|
||||
runInAction(() => {
|
||||
update(this.issues, [projectId], (issueIds = []) => {
|
||||
if (issueIds.includes(issueId)) pull(issueIds, issueId);
|
||||
return issueIds;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
this.fetchIssues(workspaceSlug, projectId, "mutation");
|
||||
@@ -147,15 +160,14 @@ export class DraftIssues extends IssueHelperStore implements IDraftIssues {
|
||||
|
||||
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
try {
|
||||
const response = await this.issueDraftService.deleteDraftIssue(workspaceSlug, projectId, issueId);
|
||||
const response = await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId);
|
||||
|
||||
const issueIndex = this.issues[projectId].findIndex((_issueId) => _issueId === issueId);
|
||||
if (issueIndex >= 0)
|
||||
runInAction(() => {
|
||||
this.issues[projectId].splice(issueIndex, 1);
|
||||
runInAction(() => {
|
||||
update(this.issues, [projectId], (issueIds = []) => {
|
||||
if (issueIds.includes(issueId)) pull(issueIds, issueId);
|
||||
return issueIds;
|
||||
});
|
||||
|
||||
this.rootStore.issues.removeIssue(issueId);
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
|
||||
@@ -100,7 +100,7 @@ export class ModulesStore implements IModuleStore {
|
||||
const projectId = this.rootStore.app.router.projectId;
|
||||
if (!projectId || !this.fetchedMap[projectId]) return null;
|
||||
let projectModules = Object.values(this.moduleMap).filter((m) => m.project === projectId);
|
||||
projectModules = sortBy(projectModules, [(m) => !m.is_favorite, (m) => m.name.toLowerCase()]);
|
||||
projectModules = sortBy(projectModules, [(m) => m.sort_order]);
|
||||
const projectModuleIds = projectModules.map((m) => m.id);
|
||||
return projectModuleIds || null;
|
||||
}
|
||||
@@ -120,7 +120,7 @@ export class ModulesStore implements IModuleStore {
|
||||
if (!this.fetchedMap[projectId]) return null;
|
||||
|
||||
let projectModules = Object.values(this.moduleMap).filter((m) => m.project === projectId);
|
||||
projectModules = sortBy(projectModules, [(m) => !m.is_favorite, (m) => m.name.toLowerCase()]);
|
||||
projectModules = sortBy(projectModules, [(m) => m.sort_order]);
|
||||
const projectModuleIds = projectModules.map((m) => m.id);
|
||||
return projectModuleIds;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user