Merge branches 'fix/pages-store' and 'develop' of github.com:makeplane/plane into develop

This commit is contained in:
sriram veeraghanta 2024-01-08 23:28:12 +05:30
commit c38e048ce8
19 changed files with 286 additions and 162 deletions

View File

@ -1,7 +1,7 @@
from django.urls import path
from plane.app.views import ConfigurationEndpoint
from plane.app.views import ConfigurationEndpoint, MobileConfigurationEndpoint
urlpatterns = [
path(
@ -9,4 +9,9 @@ urlpatterns = [
ConfigurationEndpoint.as_view(),
name="configuration",
),
path(
"mobile-configs/",
MobileConfigurationEndpoint.as_view(),
name="configuration",
),
]

View File

@ -165,7 +165,7 @@ from .notification import (
from .exporter import ExportIssuesEndpoint
from .config import ConfigurationEndpoint
from .config import ConfigurationEndpoint, MobileConfigurationEndpoint
from .webhook import (
WebhookEndpoint,

View File

@ -123,3 +123,113 @@ class ConfigurationEndpoint(BaseAPIView):
)
return Response(data, status=status.HTTP_200_OK)
class MobileConfigurationEndpoint(BaseAPIView):
permission_classes = [
AllowAny,
]
def get(self, request):
(
GOOGLE_CLIENT_ID,
GOOGLE_SERVER_CLIENT_ID,
GOOGLE_IOS_CLIENT_ID,
EMAIL_HOST_USER,
EMAIL_HOST_PASSWORD,
ENABLE_MAGIC_LINK_LOGIN,
ENABLE_EMAIL_PASSWORD,
POSTHOG_API_KEY,
POSTHOG_HOST,
UNSPLASH_ACCESS_KEY,
OPENAI_API_KEY,
) = get_configuration_value(
[
{
"key": "GOOGLE_CLIENT_ID",
"default": os.environ.get("GOOGLE_CLIENT_ID", None),
},
{
"key": "GOOGLE_SERVER_CLIENT_ID",
"default": os.environ.get("GOOGLE_SERVER_CLIENT_ID", None),
},
{
"key": "GOOGLE_IOS_CLIENT_ID",
"default": os.environ.get("GOOGLE_IOS_CLIENT_ID", None),
},
{
"key": "EMAIL_HOST_USER",
"default": os.environ.get("EMAIL_HOST_USER", None),
},
{
"key": "EMAIL_HOST_PASSWORD",
"default": os.environ.get("EMAIL_HOST_PASSWORD", None),
},
{
"key": "ENABLE_MAGIC_LINK_LOGIN",
"default": os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "1"),
},
{
"key": "ENABLE_EMAIL_PASSWORD",
"default": os.environ.get("ENABLE_EMAIL_PASSWORD", "1"),
},
{
"key": "POSTHOG_API_KEY",
"default": os.environ.get("POSTHOG_API_KEY", "1"),
},
{
"key": "POSTHOG_HOST",
"default": os.environ.get("POSTHOG_HOST", "1"),
},
{
"key": "UNSPLASH_ACCESS_KEY",
"default": os.environ.get("UNSPLASH_ACCESS_KEY", "1"),
},
{
"key": "OPENAI_API_KEY",
"default": os.environ.get("OPENAI_API_KEY", "1"),
},
]
)
data = {}
# Authentication
data["google_client_id"] = (
GOOGLE_CLIENT_ID if GOOGLE_CLIENT_ID and GOOGLE_CLIENT_ID != '""' else None
)
data["google_server_client_id"] = (
GOOGLE_SERVER_CLIENT_ID
if GOOGLE_SERVER_CLIENT_ID and GOOGLE_SERVER_CLIENT_ID != '""'
else None
)
data["google_ios_client_id"] = (
(GOOGLE_IOS_CLIENT_ID)[::-1] if GOOGLE_IOS_CLIENT_ID is not None else None
)
# Posthog
data["posthog_api_key"] = POSTHOG_API_KEY
data["posthog_host"] = POSTHOG_HOST
data["magic_login"] = (
bool(EMAIL_HOST_USER) and bool(EMAIL_HOST_PASSWORD)
) and ENABLE_MAGIC_LINK_LOGIN == "1"
data["email_password_login"] = ENABLE_EMAIL_PASSWORD == "1"
# Posthog
data["posthog_api_key"] = POSTHOG_API_KEY
data["posthog_host"] = POSTHOG_HOST
# Unsplash
data["has_unsplash_configured"] = bool(UNSPLASH_ACCESS_KEY)
# Open AI settings
data["has_openai_configured"] = bool(OPENAI_API_KEY)
# File size settings
data["file_size_limit"] = float(os.environ.get("FILE_SIZE_LIMIT", 5242880))
# is smtp configured
data["is_smtp_configured"] = not (
bool(EMAIL_HOST_USER) and bool(EMAIL_HOST_PASSWORD)
)
return Response(data, status=status.HTTP_200_OK)

View File

@ -2,6 +2,7 @@ import { useRouter } from "next/router";
import { Command } from "cmdk";
// icons
import { SettingIcon } from "components/icons";
import Link from "next/link";
type Props = {
closePalette: () => void;
@ -13,48 +14,55 @@ export const CommandPaletteWorkspaceSettingsActions: React.FC<Props> = (props) =
const router = useRouter();
const { workspaceSlug } = router.query;
const redirect = (path: string) => {
closePalette();
router.push(path);
};
return (
<>
<Command.Item onSelect={() => redirect(`/${workspaceSlug}/settings`)} className="focus:outline-none">
<div className="flex items-center gap-2 text-custom-text-200">
<SettingIcon className="h-4 w-4 text-custom-text-200" />
General
</div>
<Command.Item onSelect={closePalette} className="focus:outline-none">
<Link href={`/${workspaceSlug}/settings`}>
<div className="flex items-center gap-2 text-custom-text-200">
<SettingIcon className="h-4 w-4 text-custom-text-200" />
General
</div>
</Link>
</Command.Item>
<Command.Item onSelect={() => redirect(`/${workspaceSlug}/settings/members`)} className="focus:outline-none">
<div className="flex items-center gap-2 text-custom-text-200">
<SettingIcon className="h-4 w-4 text-custom-text-200" />
Members
</div>
<Command.Item onSelect={closePalette} className="focus:outline-none">
<Link href={`/${workspaceSlug}/settings/members`}>
<div className="flex items-center gap-2 text-custom-text-200">
<SettingIcon className="h-4 w-4 text-custom-text-200" />
Members
</div>
</Link>
</Command.Item>
<Command.Item onSelect={() => redirect(`/${workspaceSlug}/settings/billing`)} className="focus:outline-none">
<div className="flex items-center gap-2 text-custom-text-200">
<SettingIcon className="h-4 w-4 text-custom-text-200" />
Billing and Plans
</div>
<Command.Item onSelect={closePalette} className="focus:outline-none">
<Link href={`/${workspaceSlug}/settings/billing`}>
<div className="flex items-center gap-2 text-custom-text-200">
<SettingIcon className="h-4 w-4 text-custom-text-200" />
Billing and Plans
</div>
</Link>
</Command.Item>
<Command.Item onSelect={() => redirect(`/${workspaceSlug}/settings/integrations`)} className="focus:outline-none">
<div className="flex items-center gap-2 text-custom-text-200">
<SettingIcon className="h-4 w-4 text-custom-text-200" />
Integrations
</div>
<Command.Item onSelect={closePalette} className="focus:outline-none">
<Link href={`/${workspaceSlug}/settings/integrations`}>
<div className="flex items-center gap-2 text-custom-text-200">
<SettingIcon className="h-4 w-4 text-custom-text-200" />
Integrations
</div>
</Link>
</Command.Item>
<Command.Item onSelect={() => redirect(`/${workspaceSlug}/settings/imports`)} className="focus:outline-none">
<div className="flex items-center gap-2 text-custom-text-200">
<SettingIcon className="h-4 w-4 text-custom-text-200" />
Import
</div>
<Command.Item onSelect={closePalette} className="focus:outline-none">
<Link href={`/${workspaceSlug}/settings/imports`}>
<div className="flex items-center gap-2 text-custom-text-200">
<SettingIcon className="h-4 w-4 text-custom-text-200" />
Import
</div>
</Link>
</Command.Item>
<Command.Item onSelect={() => redirect(`/${workspaceSlug}/settings/exports`)} className="focus:outline-none">
<div className="flex items-center gap-2 text-custom-text-200">
<SettingIcon className="h-4 w-4 text-custom-text-200" />
Export
</div>
<Command.Item onSelect={closePalette} className="focus:outline-none">
<Link href={`/${workspaceSlug}/settings/exports`}>
<div className="flex items-center gap-2 text-custom-text-200">
<SettingIcon className="h-4 w-4 text-custom-text-200" />
Export
</div>
</Link>
</Command.Item>
</>
);

View File

@ -1,5 +1,5 @@
import React from "react";
import { eachDayOfInterval } from "date-fns";
import { eachDayOfInterval, isValid } from "date-fns";
// ui
import { LineGraph } from "components/ui";
// helpers
@ -47,7 +47,13 @@ const ProgressChart: React.FC<Props> = ({ distribution, startDate, endDate, tota
}));
const generateXAxisTickValues = () => {
const dates = eachDayOfInterval({ start: new Date(startDate), end: new Date(endDate) });
const start = new Date(startDate);
const end = new Date(endDate);
let dates: Date[] = [];
if (isValid(start) && isValid(end)) {
dates = eachDayOfInterval({ start, end });
}
const maxDates = 4;
const totalDates = dates.length;

View File

@ -33,6 +33,7 @@ import { truncateText } from "helpers/string.helper";
import { ICycle } from "@plane/types";
import { EIssuesStoreType } from "constants/issue";
import { ACTIVE_CYCLE_ISSUES } from "store/issue/cycle";
import { CYCLE_ISSUES_WITH_PARAMS } from "constants/fetch-keys";
const stateGroups = [
{
@ -73,7 +74,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
const { workspaceSlug, projectId } = props;
const {
issues: { issues },
issues: { issues, fetchActiveCycleIssues },
issueMap,
} = useIssues(EIssuesStoreType.CYCLE);
// store hooks
@ -99,13 +100,14 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
const activeCycle = currentProjectActiveCycleId ? getActiveCycleById(currentProjectActiveCycleId) : null;
const issueIds = issues?.[ACTIVE_CYCLE_ISSUES];
// useSWR(
// workspaceSlug && projectId && cycleId ? CYCLE_ISSUES_WITH_PARAMS(cycleId, { priority: "urgent,high" }) : null,
// workspaceSlug && projectId && cycleId
// ? () =>
// fetchActiveCycleIssues(workspaceSlug, projectId, )
// : null
// );
useSWR(
workspaceSlug && projectId && currentProjectActiveCycleId
? CYCLE_ISSUES_WITH_PARAMS(currentProjectActiveCycleId, { priority: "urgent,high" })
: null,
workspaceSlug && projectId && currentProjectActiveCycleId
? () => fetchActiveCycleIssues(workspaceSlug, projectId, currentProjectActiveCycleId)
: null
);
if (!activeCycle && isLoading)
return (
@ -382,9 +384,9 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
{issueIds ? (
issueIds.length > 0 ? (
issueIds.map((issue: any) => (
<div
<Link
key={issue.id}
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`)}
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
className="flex cursor-pointer flex-wrap items-center justify-between gap-2 rounded-md border border-custom-border-200 bg-custom-background-90 px-3 py-1.5"
>
<div className="flex flex-col gap-1">
@ -427,7 +429,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
)}
</div>
</div>
</div>
</Link>
))
) : (
<div className="grid place-items-center text-center text-sm text-custom-text-200">

View File

@ -14,6 +14,8 @@ import { EmptyState } from "components/common";
import emptyEstimate from "public/empty-state/estimate.svg";
// types
import { IEstimate } from "@plane/types";
// helpers
import { orderArrayBy } from "helpers/array.helper";
export const EstimatesList: React.FC = observer(() => {
// states
@ -31,7 +33,11 @@ export const EstimatesList: React.FC = observer(() => {
const editEstimate = (estimate: IEstimate) => {
setEstimateFormOpen(true);
setEstimateToUpdate(estimate);
// Order the points array by key before updating the estimate to update state
setEstimateToUpdate({
...estimate,
points: orderArrayBy(estimate.points, "key"),
});
};
const disableEstimates = () => {

View File

@ -1,6 +1,7 @@
import { useCallback, useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import Link from "next/link";
// hooks
import {
useApplication,
@ -41,14 +42,11 @@ const CycleDropdownOption: React.FC<{ cycleId: string }> = ({ cycleId }) => {
if (!cycle) return null;
return (
<CustomMenu.MenuItem
key={cycle.id}
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`)}
>
<div className="flex items-center gap-1.5">
<CustomMenu.MenuItem key={cycle.id}>
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`} className="flex items-center gap-1.5">
<ContrastIcon className="h-3 w-3" />
{truncateText(cycle.name, 40)}
</div>
</Link>
</CustomMenu.MenuItem>
);
};

View File

@ -1,6 +1,7 @@
import { useCallback, useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import Link from "next/link";
// hooks
import {
useApplication,
@ -41,14 +42,14 @@ const ModuleDropdownOption: React.FC<{ moduleId: string }> = ({ moduleId }) => {
if (!moduleDetail) return null;
return (
<CustomMenu.MenuItem
key={moduleDetail.id}
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/modules/${moduleDetail.id}`)}
>
<div className="flex items-center gap-1.5">
<CustomMenu.MenuItem key={moduleDetail.id}>
<Link
href={`/${workspaceSlug}/projects/${projectId}/modules/${moduleDetail.id}`}
className="flex items-center gap-1.5"
>
<DiceIcon className="h-3 w-3" />
{truncateText(moduleDetail.name, 40)}
</div>
</Link>
</CustomMenu.MenuItem>
);
};

View File

@ -2,6 +2,7 @@ import { useCallback } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { Plus } from "lucide-react";
import Link from "next/link";
// hooks
import {
useApplication,
@ -154,14 +155,14 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
if (!view) return;
return (
<CustomMenu.MenuItem
key={viewId}
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/views/${viewId}`)}
>
<div className="flex items-center gap-1.5">
<CustomMenu.MenuItem key={viewId}>
<Link
href={`/${workspaceSlug}/projects/${projectId}/views/${viewId}`}
className="flex items-center gap-1.5"
>
<PhotoFilterIcon height={12} width={12} />
{truncateText(view.name, 40)}
</div>
</Link>
</CustomMenu.MenuItem>
);
})}

View File

@ -2,6 +2,7 @@ import React from "react";
import Image from "next/image";
import { useRouter } from "next/router";
import { ArchiveRestore, Clock, MessageSquare, User2 } from "lucide-react";
import Link from "next/link";
// hooks
import useToast from "hooks/use-toast";
// icons
@ -10,17 +11,14 @@ import { ArchiveIcon, CustomMenu, Tooltip } from "@plane/ui";
import { snoozeOptions } from "constants/notification";
// helper
import { replaceUnderscoreIfSnakeCase, truncateText, stripAndTruncateHTML } from "helpers/string.helper";
import {
calculateTimeAgo,
renderFormattedTime,
renderFormattedDate,
} from "helpers/date-time.helper";
import { calculateTimeAgo, renderFormattedTime, renderFormattedDate } from "helpers/date-time.helper";
// type
import type { IUserNotification } from "@plane/types";
type NotificationCardProps = {
notification: IUserNotification;
isSnoozedTabOpen: boolean;
closePopover: () => void;
markNotificationReadStatus: (notificationId: string) => Promise<void>;
markNotificationReadStatusToggle: (notificationId: string) => Promise<void>;
markNotificationArchivedStatus: (notificationId: string) => Promise<void>;
@ -32,6 +30,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
const {
notification,
isSnoozedTabOpen,
closePopover,
markNotificationReadStatus,
markNotificationReadStatusToggle,
markNotificationArchivedStatus,
@ -47,15 +46,14 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
if (isSnoozedTabOpen && new Date(notification.snoozed_till!) < new Date()) return null;
return (
<div
<Link
onClick={() => {
markNotificationReadStatus(notification.id);
router.push(
`/${workspaceSlug}/projects/${notification.project}/${
notification.data.issue_activity.field === "archived_at" ? "archived-issues" : "issues"
}/${notification.data.issue.id}`
);
closePopover();
}}
href={`/${workspaceSlug}/projects/${notification.project}/${
notification.data.issue_activity.field === "archived_at" ? "archived-issues" : "issues"
}/${notification.data.issue.id}`}
className={`group relative flex w-full cursor-pointer items-center gap-4 p-3 pl-6 ${
notification.read_at === null ? "bg-custom-primary-70/5" : "hover:bg-custom-background-200"
}`}
@ -149,7 +147,8 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
<p className="flex flex-shrink-0 items-center justify-end gap-x-1 text-custom-text-300">
<Clock className="h-4 w-4" />
<span>
Till {renderFormattedDate(notification.snoozed_till)}, {renderFormattedTime(notification.snoozed_till, '12-hour')}
Till {renderFormattedDate(notification.snoozed_till)},{" "}
{renderFormattedTime(notification.snoozed_till, "12-hour")}
</span>
</p>
) : (
@ -195,6 +194,8 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
type="button"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
item.onClick();
}}
key={item.id}
@ -204,7 +205,6 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
</button>
</Tooltip>
))}
<Tooltip tooltipContent="Snooze">
<CustomMenu
className="flex items-center"
@ -223,6 +223,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
key={item.label}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
if (!item.value) {
setSelectedNotificationForSnooze(notification.id);
@ -243,6 +244,6 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
</CustomMenu>
</Tooltip>
</div>
</div>
</Link>
);
};

View File

@ -119,6 +119,7 @@ export const NotificationPopover = observer(() => {
<NotificationCard
key={notification.id}
isSnoozedTabOpen={snoozed}
closePopover={closePopover}
notification={notification}
markNotificationArchivedStatus={markNotificationArchivedStatus}
markNotificationReadStatus={markNotificationAsRead}

View File

@ -2,6 +2,7 @@ import React, { useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { LinkIcon, Lock, Pencil, Star } from "lucide-react";
import Link from "next/link";
// hooks
import { useProject } from "hooks/store";
import useToast from "hooks/use-toast";
@ -74,7 +75,7 @@ export const ProjectCard: React.FC<ProjectCardProps> = observer((props) => {
});
};
const projectMembersIds = project.members.map((member) => member.member_id);
const projectMembersIds = project.members?.map((member) => member.member_id);
return (
<>
@ -178,7 +179,7 @@ export const ProjectCard: React.FC<ProjectCardProps> = observer((props) => {
}
position="top"
>
{projectMembersIds.length > 0 ? (
{projectMembersIds && projectMembersIds.length > 0 ? (
<div className="flex cursor-pointer items-center gap-2 text-custom-text-200">
<AvatarGroup showTooltip={false}>
{projectMembersIds.map((memberId) => {
@ -195,17 +196,15 @@ export const ProjectCard: React.FC<ProjectCardProps> = observer((props) => {
)}
</Tooltip>
{(isOwner || isMember) && (
<button
<Link
className="flex items-center justify-center rounded p-1 text-custom-text-400 hover:bg-custom-background-80 hover:text-custom-text-200"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
router.push(`/${workspaceSlug}/projects/${project.id}/settings`);
}}
href={`/${workspaceSlug}/projects/${project.id}/settings`}
>
<Pencil className="h-3.5 w-3.5" />
</button>
</Link>
)}
{!project.is_member ? (

View File

@ -45,28 +45,39 @@ type EmptySpaceItemProps = {
title: string;
description?: React.ReactNode | string;
Icon: any;
action: () => void;
action?: () => void;
href?: string;
};
const EmptySpaceItem: React.FC<EmptySpaceItemProps> = ({ title, description, Icon, action }) => (
<>
<li className="cursor-pointer" onClick={action} role="button">
<div className={`group relative flex ${description ? "items-start" : "items-center"} space-x-3 py-4`}>
<div className="flex-shrink-0">
<span className="inline-flex h-10 w-10 items-center justify-center rounded-lg bg-custom-primary">
<Icon className="h-6 w-6 text-white" aria-hidden="true" />
</span>
</div>
<div className="min-w-0 flex-1 text-custom-text-200">
<div className="text-sm font-medium group-hover:text-custom-text-100">{title}</div>
{description ? <div className="text-sm">{description}</div> : null}
</div>
<div className="flex-shrink-0 self-center">
<ChevronRight className="h-5 w-5 text-custom-text-200 group-hover:text-custom-text-100" aria-hidden="true" />
</div>
const EmptySpaceItem: React.FC<EmptySpaceItemProps> = ({ title, description, Icon, action, href }) => {
let spaceItem = (
<div className={`group relative flex ${description ? "items-start" : "items-center"} space-x-3 py-4`}>
<div className="flex-shrink-0">
<span className="inline-flex h-10 w-10 items-center justify-center rounded-lg bg-custom-primary">
<Icon className="h-6 w-6 text-white" aria-hidden="true" />
</span>
</div>
</li>
</>
);
<div className="min-w-0 flex-1 text-custom-text-200">
<div className="text-sm font-medium group-hover:text-custom-text-100">{title}</div>
{description ? <div className="text-sm">{description}</div> : null}
</div>
<div className="flex-shrink-0 self-center">
<ChevronRight className="h-5 w-5 text-custom-text-200 group-hover:text-custom-text-100" aria-hidden="true" />
</div>
</div>
);
if (href) {
spaceItem = <Link href={href}>{spaceItem}</Link>;
}
return (
<>
<li className="cursor-pointer" onClick={action} role="button">
{spaceItem}
</li>
</>
);
};
export { EmptySpace, EmptySpaceItem };

View File

@ -1,6 +1,7 @@
import { FC, ReactNode } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import Link from "next/link";
// hooks
import { useUser } from "hooks/store";
// components
@ -31,14 +32,12 @@ export const ProjectSettingLayout: FC<IProjectSettingLayout> = observer((props)
<NotAuthorizedView
type="project"
actionButton={
<Button
variant="primary"
size="md"
prependIcon={<LayersIcon />}
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues`)}
>
Go to issues
</Button>
//TODO: Create a new component called Button Link to handle such scenarios
<Link href={`/${workspaceSlug}/projects/${projectId}/issues`}>
<Button variant="primary" size="md" prependIcon={<LayersIcon />}>
Go to issues
</Button>
</Link>
}
/>
) : (

View File

@ -3,6 +3,7 @@ import { useRouter } from "next/router";
import Image from "next/image";
import { useTheme } from "next-themes";
import { observer } from "mobx-react-lite";
import Link from "next/link";
// hooks
import { useUser } from "hooks/store";
// layouts
@ -39,9 +40,9 @@ const CreateWorkspacePage: NextPageWithLayout = observer(() => {
<div className="flex h-full flex-col gap-y-2 overflow-hidden sm:flex-row sm:gap-y-0">
<div className="relative h-1/6 flex-shrink-0 sm:w-2/12 md:w-3/12 lg:w-1/5">
<div className="absolute left-0 top-1/2 h-[0.5px] w-full -translate-y-1/2 border-b-[0.5px] border-custom-border-200 sm:left-1/2 sm:top-0 sm:h-screen sm:w-[0.5px] sm:-translate-x-1/2 sm:translate-y-0 sm:border-r-[0.5px] md:left-1/3" />
<button
<Link
className="absolute left-5 top-1/2 grid -translate-y-1/2 place-items-center bg-custom-background-100 px-3 sm:left-1/2 sm:top-12 sm:-translate-x-[15px] sm:translate-y-0 sm:px-0 sm:py-5 md:left-1/3"
onClick={() => router.push("/")}
href="/"
>
<div className="h-[30px] w-[133px]">
{theme === "light" ? (
@ -50,7 +51,7 @@ const CreateWorkspacePage: NextPageWithLayout = observer(() => {
<Image src={WhiteHorizontalLogo} alt="Plane white logo" />
)}
</div>
</button>
</Link>
<div className="absolute right-4 top-1/4 -translate-y-1/2 text-sm text-custom-text-100 sm:fixed sm:right-16 sm:top-12 sm:translate-y-0 sm:py-5">
{currentUser?.email}
</div>

View File

@ -81,7 +81,7 @@ const WorkspaceInvitationPage: NextPageWithLayout = observer(() => {
title={`You are already a member of ${invitationDetail.workspace.name}`}
description="Your workspace is where you'll create projects, collaborate on your issues, and organize different streams of work in your Plane account."
>
<EmptySpaceItem Icon={Boxes} title="Continue to Dashboard" action={() => router.push("/")} />
<EmptySpaceItem Icon={Boxes} title="Continue to Dashboard" href="/" />
</EmptySpace>
</>
) : (
@ -103,35 +103,15 @@ const WorkspaceInvitationPage: NextPageWithLayout = observer(() => {
link={{ text: "Or start from an empty project", href: "/" }}
>
{!currentUser ? (
<EmptySpaceItem
Icon={User2}
title="Sign in to continue"
action={() => {
router.push("/");
}}
/>
<EmptySpaceItem Icon={User2} title="Sign in to continue" href="/" />
) : (
<EmptySpaceItem
Icon={Boxes}
title="Continue to Dashboard"
action={() => {
router.push("/");
}}
/>
<EmptySpaceItem Icon={Boxes} title="Continue to Dashboard" href="/" />
)}
<EmptySpaceItem
Icon={Star}
title="Star us on GitHub"
action={() => {
router.push("https://github.com/makeplane");
}}
/>
<EmptySpaceItem Icon={Star} title="Star us on GitHub" href="https://github.com/makeplane" />
<EmptySpaceItem
Icon={Share2}
title="Join our community of active creators"
action={() => {
router.push("https://discord.com/invite/8SR2N9PAcJ");
}}
href="https://discord.com/invite/8SR2N9PAcJ"
/>
</EmptySpace>
) : (

View File

@ -14,7 +14,7 @@ import { CycleService } from "services/cycle.service";
export interface ICycleStore {
// observables
cycleMap: Record<string, ICycle>;
activeCycleMap: Record<string, ICycle>; // TODO: Merge these two into single map
activeCycleIdMap: Record<string, boolean>;
// computed
currentProjectCycleIds: string[] | null;
currentProjectCompletedCycleIds: string[] | null;
@ -49,7 +49,7 @@ export interface ICycleStore {
export class CycleStore implements ICycleStore {
// observables
cycleMap: Record<string, ICycle> = {};
activeCycleMap: Record<string, ICycle> = {};
activeCycleIdMap: Record<string, boolean> = {};
// root store
rootStore;
// services
@ -61,7 +61,7 @@ export class CycleStore implements ICycleStore {
makeObservable(this, {
// observables
cycleMap: observable,
activeCycleMap: observable,
activeCycleIdMap: observable,
// computed
currentProjectCycleIds: computed,
currentProjectCompletedCycleIds: computed,
@ -168,8 +168,8 @@ export class CycleStore implements ICycleStore {
get currentProjectActiveCycleId() {
const projectId = this.rootStore.app.router.projectId;
if (!projectId) return null;
const activeCycle = Object.keys(this.activeCycleMap ?? {}).find(
(cycleId) => this.activeCycleMap?.[cycleId]?.project === projectId
const activeCycle = Object.keys(this.activeCycleIdMap ?? {}).find(
(cycleId) => this.cycleMap?.[cycleId]?.project === projectId
);
return activeCycle || null;
}
@ -186,7 +186,8 @@ export class CycleStore implements ICycleStore {
* @param cycleId
* @returns
*/
getActiveCycleById = (cycleId: string): ICycle | null => this.activeCycleMap?.[cycleId] ?? null;
getActiveCycleById = (cycleId: string): ICycle | null =>
this.activeCycleIdMap?.[cycleId] && this.cycleMap?.[cycleId] ? this.cycleMap?.[cycleId] : null;
/**
* @description returns list of cycle ids of the project id passed as argument
@ -235,7 +236,8 @@ export class CycleStore implements ICycleStore {
await this.cycleService.getCyclesWithParams(workspaceSlug, projectId, "current").then((response) => {
runInAction(() => {
response.forEach((cycle) => {
set(this.activeCycleMap, [cycle.id], cycle);
set(this.activeCycleIdMap, [cycle.id], true);
set(this.cycleMap, [cycle.id], cycle);
});
});
return response;
@ -252,7 +254,6 @@ export class CycleStore implements ICycleStore {
await this.cycleService.getCycleDetails(workspaceSlug, projectId, cycleId).then((response) => {
runInAction(() => {
set(this.cycleMap, [response.id], { ...this.cycleMap?.[response.id], ...response });
set(this.activeCycleMap, [response.id], { ...this.activeCycleMap?.[response.id], ...response });
});
return response;
});
@ -268,7 +269,6 @@ export class CycleStore implements ICycleStore {
await this.cycleService.createCycle(workspaceSlug, projectId, data).then((response) => {
runInAction(() => {
set(this.cycleMap, [response.id], response);
set(this.activeCycleMap, [response.id], response);
});
return response;
});
@ -285,7 +285,6 @@ export class CycleStore implements ICycleStore {
try {
runInAction(() => {
set(this.cycleMap, [cycleId], { ...this.cycleMap?.[cycleId], ...data });
set(this.activeCycleMap, [cycleId], { ...this.activeCycleMap?.[cycleId], ...data });
});
const response = await this.cycleService.patchCycle(workspaceSlug, projectId, cycleId, data);
return response;
@ -307,7 +306,7 @@ export class CycleStore implements ICycleStore {
await this.cycleService.deleteCycle(workspaceSlug, projectId, cycleId).then(() => {
runInAction(() => {
delete this.cycleMap[cycleId];
delete this.activeCycleMap[cycleId];
delete this.activeCycleIdMap[cycleId];
});
});
@ -324,7 +323,6 @@ export class CycleStore implements ICycleStore {
try {
runInAction(() => {
if (currentCycle) set(this.cycleMap, [cycleId, "is_favorite"], true);
if (currentActiveCycle) set(this.activeCycleMap, [cycleId, "is_favorite"], true);
});
// updating through api.
const response = await this.cycleService.addCycleToFavorites(workspaceSlug, projectId, { cycle: cycleId });
@ -332,7 +330,6 @@ export class CycleStore implements ICycleStore {
} catch (error) {
runInAction(() => {
if (currentCycle) set(this.cycleMap, [cycleId, "is_favorite"], false);
if (currentActiveCycle) set(this.activeCycleMap, [cycleId, "is_favorite"], false);
});
throw error;
}
@ -351,14 +348,12 @@ export class CycleStore implements ICycleStore {
try {
runInAction(() => {
if (currentCycle) set(this.cycleMap, [cycleId, "is_favorite"], false);
if (currentActiveCycle) set(this.activeCycleMap, [cycleId, "is_favorite"], false);
});
const response = await this.cycleService.removeCycleFromFavorites(workspaceSlug, projectId, cycleId);
return response;
} catch (error) {
runInAction(() => {
if (currentCycle) set(this.cycleMap, [cycleId, "is_favorite"], true);
if (currentActiveCycle) set(this.activeCycleMap, [cycleId, "is_favorite"], true);
});
throw error;
}

View File

@ -172,7 +172,7 @@ export class EstimateStore implements IEstimateStore {
updateEstimate = async (workspaceSlug: string, projectId: string, estimateId: string, data: IEstimateFormData) =>
await this.estimateService.patchEstimate(workspaceSlug, projectId, estimateId, data).then((response) => {
const updatedEstimates = (this.estimates?.[projectId] ?? []).map((estimate) =>
estimate.id === estimateId ? { ...estimate, ...data.estimate } : estimate
estimate.id === estimateId ? { ...estimate, ...data.estimate, points: [...data.estimate_points] } : estimate
);
runInAction(() => {
set(this.estimates, projectId, updatedEstimates);