Merge pull request #1784 from makeplane/stage-release

promote: stage-release to master v0.10.1-patch
This commit is contained in:
Vamsi Kurama 2023-08-03 18:55:24 +05:30 committed by GitHub
commit 9ff8994c0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 738 additions and 785 deletions

View File

@ -332,7 +332,7 @@ class BulkImportIssuesEndpoint(BaseAPIView):
# if there is no default state assign any random state
if default_state is None:
default_state = State.objects.filter(
~Q(name="Triage"), sproject_id=project_id
~Q(name="Triage"), project_id=project_id
).first()
# Get the maximum sequence_id

View File

@ -75,6 +75,7 @@ from plane.db.models import (
Label,
WorkspaceMember,
CycleIssue,
IssueReaction,
)
from plane.api.permissions import (
WorkSpaceBasePermission,
@ -1321,6 +1322,12 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
)
.select_related("project", "workspace", "state", "parent")
.prefetch_related("assignees", "labels")
.prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related("actor"),
)
)
.order_by("-created_at")
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))

View File

@ -5,6 +5,8 @@ import { Command } from "cmdk";
import { THEMES_OBJ } from "constants/themes";
import { useTheme } from "next-themes";
import { SettingIcon } from "components/icons";
import userService from "services/user.service";
import useUser from "hooks/use-user";
type Props = {
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
@ -12,24 +14,50 @@ type Props = {
export const ChangeInterfaceTheme: React.FC<Props> = ({ setIsPaletteOpen }) => {
const [mounted, setMounted] = useState(false);
const { setTheme } = useTheme();
const { user, mutateUser } = useUser();
const updateUserTheme = (newTheme: string) => {
if (!user) return;
setTheme(newTheme);
mutateUser((prevData) => {
if (!prevData) return prevData;
return {
...prevData,
theme: {
...prevData.theme,
theme: newTheme,
},
};
}, false);
userService.updateUser({
theme: {
...user.theme,
theme: newTheme,
},
});
};
// useEffect only runs on the client, so now we can safely show the UI
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}
if (!mounted) return null;
return (
<>
{THEMES_OBJ.map((theme) => (
{THEMES_OBJ.filter((t) => t.value !== "custom").map((theme) => (
<Command.Item
key={theme.value}
onSelect={() => {
setTheme(theme.value);
updateUserTheme(theme.value);
setIsPaletteOpen(false);
}}
className="focus:outline-none"

View File

@ -1,342 +0,0 @@
import React from "react";
import { useRouter } from "next/router";
import Link from "next/link";
// icons
import {
ArrowTopRightOnSquareIcon,
ChatBubbleLeftEllipsisIcon,
Squares2X2Icon,
} from "@heroicons/react/24/outline";
import { BlockedIcon, BlockerIcon } from "components/icons";
import { Icon } from "components/ui";
// helpers
import { renderShortDateWithYearFormat, timeAgo } from "helpers/date-time.helper";
import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
import RemirrorRichTextEditor from "components/rich-text-editor";
const activityDetails: {
[key: string]: {
message?: string;
icon: JSX.Element;
};
} = {
assignee: {
message: "removed the assignee",
icon: <Icon iconName="group" className="!text-sm" aria-hidden="true" />,
},
assignees: {
message: "added a new assignee",
icon: <Icon iconName="group" className="!text-sm" aria-hidden="true" />,
},
blocks: {
message: "marked this issue being blocked by",
icon: <BlockedIcon height="12" width="12" color="#6b7280" />,
},
blocking: {
message: "marked this issue is blocking",
icon: <BlockerIcon height="12" width="12" color="#6b7280" />,
},
cycles: {
message: "set the cycle to",
icon: <Icon iconName="contrast" className="!text-sm" aria-hidden="true" />,
},
labels: {
icon: <Icon iconName="sell" className="!text-sm" aria-hidden="true" />,
},
modules: {
message: "set the module to",
icon: <Icon iconName="dataset" className="!text-sm" aria-hidden="true" />,
},
state: {
message: "set the state to",
icon: <Squares2X2Icon className="h-3 w-3 text-custom-text-200" aria-hidden="true" />,
},
priority: {
message: "set the priority to",
icon: <Icon iconName="signal_cellular_alt" className="!text-sm" aria-hidden="true" />,
},
name: {
message: "set the name to",
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
},
description: {
message: "updated the description.",
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
},
estimate_point: {
message: "set the estimate point to",
icon: <Icon iconName="change_history" className="!text-sm" aria-hidden="true" />,
},
target_date: {
message: "set the due date to",
icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />,
},
parent: {
message: "set the parent to",
icon: <Icon iconName="supervised_user_circle" className="!text-sm" aria-hidden="true" />,
},
issue: {
message: "deleted the issue.",
icon: <Icon iconName="delete" className="!text-sm" aria-hidden="true" />,
},
estimate: {
message: "updated the estimate",
icon: <Icon iconName="change_history" className="!text-sm" aria-hidden="true" />,
},
link: {
message: "updated the link",
icon: <Icon iconName="link" className="!text-sm" aria-hidden="true" />,
},
attachment: {
message: "updated the attachment",
icon: <Icon iconName="attach_file" className="!text-sm" aria-hidden="true" />,
},
archived_at: {
message: "archived",
icon: <Icon iconName="archive" className="!text-sm text-custom-text-200" aria-hidden="true" />,
},
};
export const Feeds: React.FC<any> = ({ activities }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
return (
<div>
<ul role="list" className="-mb-4">
{activities.map((activity: any, activityIdx: number) => {
// determines what type of action is performed
let action = activityDetails[activity.field as keyof typeof activityDetails]?.message;
if (activity.field === "labels") {
action = activity.new_value !== "" ? "added a new label" : "removed the label";
} else if (activity.field === "blocking") {
action =
activity.new_value !== ""
? "marked this issue is blocking"
: "removed the issue from blocking";
} else if (activity.field === "blocks") {
action =
activity.new_value !== "" ? "marked this issue being blocked by" : "removed blocker";
} else if (activity.field === "target_date") {
action =
activity.new_value && activity.new_value !== ""
? "set the due date to"
: "removed the due date";
} else if (activity.field === "parent") {
action =
activity.new_value && activity.new_value !== ""
? "set the parent to"
: "removed the parent";
} else if (activity.field === "priority") {
action =
activity.new_value && activity.new_value !== ""
? "set the priority to"
: "removed the priority";
} else if (activity.field === "description") {
action = "updated the";
} else if (activity.field === "attachment") {
action = `${activity.verb} the`;
} else if (activity.field === "link") {
action = `${activity.verb} the`;
} else if (activity.field === "archived_at") {
action =
activity.new_value && activity.new_value === "restore"
? "restored the issue"
: "archived the issue";
}
// for values that are after the action clause
let value: any = activity.new_value ? activity.new_value : activity.old_value;
if (
activity.verb === "created" &&
activity.field !== "cycles" &&
activity.field !== "modules" &&
activity.field !== "attachment" &&
activity.field !== "link" &&
activity.field !== "estimate"
) {
const { project, issue } = activity;
value = (
<span className="text-custom-text-200">
created{" "}
<Link href={`/${workspaceSlug}/projects/${project}/issues/${issue}`}>
<a className="inline-flex items-center hover:underline">
this issue. <ArrowTopRightOnSquareIcon className="ml-1 h-3.5 w-3.5" />
</a>
</Link>
</span>
);
} else if (activity.field === "state") {
value = activity.new_value ? addSpaceIfCamelCase(activity.new_value) : "None";
} else if (activity.field === "labels") {
let name;
let id = "#000000";
if (activity.new_value !== "") {
name = activity.new_value;
id = activity.new_identifier ? activity.new_identifier : id;
} else {
name = activity.old_value;
id = activity.old_identifier ? activity.old_identifier : id;
}
value = name;
} else if (activity.field === "assignees") {
value = activity.new_value;
} else if (activity.field === "target_date") {
const date =
activity.new_value && activity.new_value !== ""
? activity.new_value
: activity.old_value;
value = renderShortDateWithYearFormat(date as string);
} else if (activity.field === "description") {
value = "description";
} else if (activity.field === "attachment") {
value = "attachment";
} else if (activity.field === "link") {
value = "link";
} else if (activity.field === "estimate_point") {
value = activity.new_value
? activity.new_value +
` Point${parseInt(activity.new_value ?? "", 10) > 1 ? "s" : ""}`
: "None";
}
if (activity.field === "comment") {
return (
<div key={activity.id} className="mt-2">
<div className="relative flex items-start space-x-3">
<div className="relative px-1">
{activity.field ? (
activity.new_value === "restore" ? (
<Icon iconName="history" className="text-sm text-custom-text-200" />
) : (
activityDetails[activity.field as keyof typeof activityDetails]?.icon
)
) : activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? (
<img
src={activity.actor_detail.avatar}
alt={activity.actor_detail.first_name}
height={30}
width={30}
className="grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white"
/>
) : (
<div
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white`}
>
{activity.actor_detail.first_name.charAt(0)}
</div>
)}
<span className="absolute -bottom-0.5 -right-1 rounded-tl bg-custom-background-80 px-0.5 py-px">
<ChatBubbleLeftEllipsisIcon
className="h-3.5 w-3.5 text-custom-text-200"
aria-hidden="true"
/>
</span>
</div>
<div className="min-w-0 flex-1">
<div>
<div className="text-xs">
{activity.actor_detail.first_name}
{activity.actor_detail.is_bot
? "Bot"
: " " + activity.actor_detail.last_name}
</div>
<p className="mt-0.5 text-xs text-custom-text-200">
Commented {timeAgo(activity.created_at)}
</p>
</div>
<div className="issue-comments-section p-0">
<RemirrorRichTextEditor
value={
activity.new_value && activity.new_value !== ""
? activity.new_value
: activity.old_value
}
editable={false}
noBorder
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
/>
</div>
</div>
</div>
</div>
);
}
if ("field" in activity && activity.field !== "updated_by") {
return (
<li key={activity.id}>
<div className="relative pb-1">
{activities.length > 1 && activityIdx !== activities.length - 1 ? (
<span
className="absolute top-5 left-5 -ml-px h-full w-0.5 bg-custom-background-80"
aria-hidden="true"
/>
) : null}
<div className="relative flex items-start space-x-2">
<>
<div>
<div className="relative px-1.5">
<div className="mt-1.5">
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-custom-background-80 text-custom-text-200 ring-white">
{activity.field ? (
activityDetails[activity.field as keyof typeof activityDetails]
?.icon
) : activity.actor_detail.avatar &&
activity.actor_detail.avatar !== "" ? (
<img
src={activity.actor_detail.avatar}
alt={activity.actor_detail.first_name}
height={24}
width={24}
className="rounded-full"
/>
) : (
<div
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs text-white`}
>
{activity.actor_detail.first_name.charAt(0)}
</div>
)}
</div>
</div>
</div>
</div>
<div className="min-w-0 flex-1 py-3">
<div className="text-xs text-custom-text-200">
{activity.field === "archived_at" && activity.new_value !== "restore" ? (
<span className="text-gray font-medium">Plane</span>
) : (
<span className="text-gray font-medium">
{activity.actor_detail.first_name}
{activity.actor_detail.is_bot
? " Bot"
: " " + activity.actor_detail.last_name}
</span>
)}
<span> {action} </span>
{activity.field !== "archived_at" && (
<span className="text-xs font-medium text-custom-text-100">
{" "}
{value}{" "}
</span>
)}
<span className="whitespace-nowrap">{timeAgo(activity.created_at)}</span>
</div>
</div>
</>
</div>
</div>
</li>
);
}
})}
</ul>
</div>
);
};

View File

@ -4,6 +4,5 @@ export * from "./sidebar";
export * from "./theme";
export * from "./views";
export * from "./activity";
export * from "./feeds";
export * from "./reaction-selector";
export * from "./image-picker-popover";

View File

@ -28,6 +28,7 @@ const defaultValues: ICustomTheme = {
sidebarText: "#c5c5c5",
darkPalette: false,
palette: "",
theme: "custom",
};
export const CustomThemeSelector: React.FC<Props> = ({ preLoadedData }) => {
@ -56,6 +57,7 @@ export const CustomThemeSelector: React.FC<Props> = ({ preLoadedData }) => {
sidebarText: formData.sidebarText,
darkPalette: darkPalette,
palette: `${formData.background},${formData.text},${formData.primary},${formData.sidebarBackground},${formData.sidebarText}`,
theme: "custom",
};
await userService

View File

@ -1,142 +1,161 @@
import { useState, useEffect, Dispatch, SetStateAction } from "react";
import { useState, useEffect } from "react";
// next-themes
import { useTheme } from "next-themes";
// services
import userService from "services/user.service";
// hooks
import useUser from "hooks/use-user";
// constants
import { THEMES_OBJ } from "constants/themes";
// ui
import { CustomSelect } from "components/ui";
// types
import { ICustomTheme, IUser } from "types";
import { ICustomTheme } from "types";
import { unsetCustomCssVariables } from "helpers/theme.helper";
type Props = {
user: IUser | undefined;
setPreLoadedData: Dispatch<SetStateAction<ICustomTheme | null>>;
setPreLoadedData: React.Dispatch<React.SetStateAction<ICustomTheme | null>>;
customThemeSelectorOptions: boolean;
setCustomThemeSelectorOptions: Dispatch<SetStateAction<boolean>>;
setCustomThemeSelectorOptions: React.Dispatch<React.SetStateAction<boolean>>;
};
export const ThemeSwitch: React.FC<Props> = ({
user,
setPreLoadedData,
customThemeSelectorOptions,
setCustomThemeSelectorOptions,
}) => {
const [mounted, setMounted] = useState(false);
const { theme, setTheme } = useTheme();
const { user, mutateUser } = useUser();
const updateUserTheme = (newTheme: string) => {
if (!user) return;
setTheme(newTheme);
mutateUser((prevData) => {
if (!prevData) return prevData;
return {
...prevData,
theme: {
...prevData.theme,
theme: newTheme,
},
};
}, false);
userService.updateUser({
theme: {
...user.theme,
theme: newTheme,
},
});
};
// useEffect only runs on the client, so now we can safely show the UI
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}
if (!mounted) return null;
const currentThemeObj = THEMES_OBJ.find((t) => t.value === theme);
return (
<>
<CustomSelect
value={theme}
label={
currentThemeObj ? (
<div className="flex items-center gap-2">
<CustomSelect
value={theme}
label={
currentThemeObj ? (
<div className="flex items-center gap-2">
<div
className="border-1 relative flex h-4 w-4 rotate-45 transform items-center justify-center rounded-full border"
style={{
borderColor: currentThemeObj.icon.border,
}}
>
<div
className="border-1 relative flex h-4 w-4 rotate-45 transform items-center justify-center rounded-full border"
className="h-full w-1/2 rounded-l-full"
style={{
borderColor: currentThemeObj.icon.border,
background: currentThemeObj.icon.color1,
}}
>
<div
className="h-full w-1/2 rounded-l-full"
style={{
background: currentThemeObj.icon.color1,
}}
/>
<div
className="h-full w-1/2 rounded-r-full border-l"
style={{
borderLeftColor: currentThemeObj.icon.border,
background: currentThemeObj.icon.color2,
}}
/>
</div>
{currentThemeObj.label}
/>
<div
className="h-full w-1/2 rounded-r-full border-l"
style={{
borderLeftColor: currentThemeObj.icon.border,
background: currentThemeObj.icon.color2,
}}
/>
</div>
) : (
"Select your theme"
)
}
onChange={({ value, type }: { value: string; type: string }) => {
if (value === "custom") {
if (user?.theme.palette) {
setPreLoadedData({
background: user.theme.background !== "" ? user.theme.background : "#0d101b",
text: user.theme.text !== "" ? user.theme.text : "#c5c5c5",
primary: user.theme.primary !== "" ? user.theme.primary : "#3f76ff",
sidebarBackground:
user.theme.sidebarBackground !== "" ? user.theme.sidebarBackground : "#0d101b",
sidebarText: user.theme.sidebarText !== "" ? user.theme.sidebarText : "#c5c5c5",
darkPalette: false,
palette:
user.theme.palette !== ",,,,"
? user.theme.palette
: "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
});
}
if (!customThemeSelectorOptions) setCustomThemeSelectorOptions(true);
} else {
if (customThemeSelectorOptions) setCustomThemeSelectorOptions(false);
for (let i = 10; i <= 900; i >= 100 ? (i += 100) : (i += 10)) {
document.documentElement.style.removeProperty(`--color-background-${i}`);
document.documentElement.style.removeProperty(`--color-text-${i}`);
document.documentElement.style.removeProperty(`--color-border-${i}`);
document.documentElement.style.removeProperty(`--color-primary-${i}`);
document.documentElement.style.removeProperty(`--color-sidebar-background-${i}`);
document.documentElement.style.removeProperty(`--color-sidebar-text-${i}`);
document.documentElement.style.removeProperty(`--color-sidebar-border-${i}`);
}
{currentThemeObj.label}
</div>
) : (
"Select your theme"
)
}
onChange={({ value, type }: { value: string; type: string }) => {
if (value === "custom") {
if (user?.theme.palette) {
setPreLoadedData({
background: user.theme.background !== "" ? user.theme.background : "#0d101b",
text: user.theme.text !== "" ? user.theme.text : "#c5c5c5",
primary: user.theme.primary !== "" ? user.theme.primary : "#3f76ff",
sidebarBackground:
user.theme.sidebarBackground !== "" ? user.theme.sidebarBackground : "#0d101b",
sidebarText: user.theme.sidebarText !== "" ? user.theme.sidebarText : "#c5c5c5",
darkPalette: false,
palette:
user.theme.palette !== ",,,,"
? user.theme.palette
: "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
theme: "custom",
});
}
setTheme(value);
document.documentElement.style.setProperty("color-scheme", type);
}}
input
width="w-full"
position="right"
>
{THEMES_OBJ.map(({ value, label, type, icon }) => (
<CustomSelect.Option key={value} value={{ value, type }}>
<div className="flex items-center gap-2">
if (!customThemeSelectorOptions) setCustomThemeSelectorOptions(true);
} else {
if (customThemeSelectorOptions) setCustomThemeSelectorOptions(false);
unsetCustomCssVariables();
}
updateUserTheme(value);
document.documentElement.style.setProperty("--color-scheme", type);
}}
input
width="w-full"
position="right"
>
{THEMES_OBJ.map(({ value, label, type, icon }) => (
<CustomSelect.Option key={value} value={{ value, type }}>
<div className="flex items-center gap-2">
<div
className="border-1 relative flex h-4 w-4 rotate-45 transform items-center justify-center rounded-full border"
style={{
borderColor: icon.border,
}}
>
<div
className="border-1 relative flex h-4 w-4 rotate-45 transform items-center justify-center rounded-full border"
className="h-full w-1/2 rounded-l-full"
style={{
borderColor: icon.border,
background: icon.color1,
}}
>
<div
className="h-full w-1/2 rounded-l-full"
style={{
background: icon.color1,
}}
/>
<div
className="h-full w-1/2 rounded-r-full border-l"
style={{
borderLeftColor: icon.border,
background: icon.color2,
}}
/>
</div>
{label}
/>
<div
className="h-full w-1/2 rounded-r-full border-l"
style={{
borderLeftColor: icon.border,
background: icon.color2,
}}
/>
</div>
</CustomSelect.Option>
))}
</CustomSelect>
</>
{label}
</div>
</CustomSelect.Option>
))}
</CustomSelect>
);
};

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useState, useCallback } from "react";
import { useRouter } from "next/router";
@ -98,20 +98,36 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
assignees: [...(prePopulateData?.assignees ?? []), user?.id ?? ""],
};
const onClose = useCallback(() => {
handleClose();
setActiveProject(null);
}, [handleClose]);
useEffect(() => {
// if modal is closed, reset active project to null
// and return to avoid activeProject being set to some other project
if (!isOpen) {
setActiveProject(null);
return;
}
// if data is present, set active project to the project of the
// issue. This has more priority than the project in the url.
if (data && data.project) {
setActiveProject(data.project);
return;
}
// if data is not present, set active project to the project
// in the url. This has the least priority.
if (projects && projects.length > 0 && !activeProject)
setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null);
}, [activeProject, data, projectId, projects]);
}, [activeProject, data, projectId, projects, isOpen]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
handleClose();
onClose();
}
};
@ -119,7 +135,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [handleClose]);
}, [onClose]);
const addIssueToCycle = async (issueId: string, cycleId: string) => {
if (!workspaceSlug || !activeProject) return;
@ -267,7 +283,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
});
});
if (!createMore) handleClose();
if (!createMore) onClose();
};
const updateIssue = async (payload: Partial<IIssue>) => {
@ -286,7 +302,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle);
if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module);
if (!createMore) handleClose();
if (!createMore) onClose();
setToastAlert({
type: "success",
@ -324,7 +340,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={() => handleClose()}>
<Dialog as="div" className="relative z-20" onClose={onClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
@ -354,7 +370,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
initialData={data ?? prePopulateData}
createMore={createMore}
setCreateMore={setCreateMore}
handleClose={handleClose}
handleClose={onClose}
projectId={activeProject ?? ""}
setActiveProject={setActiveProject}
status={data ? true : false}

View File

@ -27,6 +27,7 @@ import { snoozeOptions } from "constants/notification";
type NotificationCardProps = {
notification: IUserNotification;
markNotificationReadStatus: (notificationId: string) => Promise<void>;
markNotificationReadStatusToggle: (notificationId: string) => Promise<void>;
markNotificationArchivedStatus: (notificationId: string) => Promise<void>;
setSelectedNotificationForSnooze: (notificationId: string) => void;
markSnoozeNotification: (notificationId: string, dateTime?: Date | undefined) => Promise<void>;
@ -36,6 +37,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
const {
notification,
markNotificationReadStatus,
markNotificationReadStatusToggle,
markNotificationArchivedStatus,
setSelectedNotificationForSnooze,
markSnoozeNotification,
@ -159,7 +161,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
name: notification.read_at ? "Mark as unread" : "Mark as read",
icon: "chat_bubble",
onClick: () => {
markNotificationReadStatus(notification.id).then(() => {
markNotificationReadStatusToggle(notification.id).then(() => {
setToastAlert({
title: notification.read_at
? "Notification marked as unread"

View File

@ -38,6 +38,7 @@ export const NotificationPopover = () => {
notificationMutate,
markNotificationArchivedStatus,
markNotificationReadStatus,
markNotificationAsRead,
markSnoozeNotification,
notificationCount,
totalNotificationCount,
@ -128,7 +129,8 @@ export const NotificationPopover = () => {
key={notification.id}
notification={notification}
markNotificationArchivedStatus={markNotificationArchivedStatus}
markNotificationReadStatus={markNotificationReadStatus}
markNotificationReadStatus={markNotificationAsRead}
markNotificationReadStatusToggle={markNotificationReadStatus}
setSelectedNotificationForSnooze={setSelectedNotificationForSnooze}
markSnoozeNotification={markSnoozeNotification}
/>

View File

@ -130,7 +130,7 @@ export const ProjectSidebarList: FC = () => {
data={projectToDelete}
user={user}
/>
<div className="h-full overflow-y-auto px-5 space-y-3 pt-3 border-t border-custom-sidebar-border-300">
<div className="h-full overflow-y-auto px-4 space-y-3 pt-3 border-t border-custom-sidebar-border-300">
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="favorite-projects">
{(provided) => (

View File

@ -137,21 +137,30 @@ export const SingleSidebarProject: React.FC<Props> = ({
{({ open }) => (
<>
<div
className={`group relative text-custom-sidebar-text-10 px-2 py-1 ml-1.5 w-full flex items-center hover:bg-custom-sidebar-background-80 rounded-md ${
className={`group relative text-custom-sidebar-text-10 px-2 py-1 w-full flex items-center hover:bg-custom-sidebar-background-80 rounded-md ${
snapshot?.isDragging ? "opacity-60" : ""
}`}
>
{provided && (
<button
type="button"
className={`absolute top-1/2 -translate-y-1/2 -left-4 hidden rounded p-0.5 ${
sidebarCollapse ? "" : "group-hover:!flex"
}`}
{...provided?.dragHandleProps}
<Tooltip
tooltipContent={
project.sort_order === null
? "Join the project to rearrange"
: "Drag to rearrange"
}
position="top-right"
>
<EllipsisVerticalIcon className="h-4" />
<EllipsisVerticalIcon className="-ml-5 h-4" />
</button>
<button
type="button"
className={`absolute top-1/2 -translate-y-1/2 -left-4 hidden rounded p-0.5 ${
sidebarCollapse ? "" : "group-hover:!flex"
} ${project.sort_order === null ? "opacity-60 cursor-not-allowed" : ""}`}
{...provided?.dragHandleProps}
>
<EllipsisVerticalIcon className="h-4" />
<EllipsisVerticalIcon className="-ml-5 h-4" />
</button>
</Tooltip>
)}
<Tooltip
tooltipContent={`${project.name}`}
@ -161,17 +170,21 @@ export const SingleSidebarProject: React.FC<Props> = ({
>
<Disclosure.Button
as="div"
className={`flex items-center w-full cursor-pointer select-none text-left text-sm font-medium ${
className={`flex items-center flex-grow truncate cursor-pointer select-none text-left text-sm font-medium ${
sidebarCollapse ? "justify-center" : `justify-between`
}`}
>
<div className="flex items-center gap-x-2">
<div
className={`flex items-center flex-grow w-full truncate gap-x-2 ${
sidebarCollapse ? "justify-center" : ""
}`}
>
{project.emoji ? (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
{renderEmoji(project.emoji)}
</span>
) : project.icon_prop ? (
<div className="h-7 w-7 grid place-items-center">
<div className="h-7 w-7 flex-shrink-0 grid place-items-center">
{renderEmoji(project.icon_prop)}
</div>
) : (
@ -181,19 +194,15 @@ export const SingleSidebarProject: React.FC<Props> = ({
)}
{!sidebarCollapse && (
<p
className={`overflow-hidden text-ellipsis ${
open ? "" : "text-custom-sidebar-text-200"
}`}
>
{truncateText(project.name, 15)}
<p className={`truncate ${open ? "" : "text-custom-sidebar-text-200"}`}>
{project.name}
</p>
)}
</div>
{!sidebarCollapse && (
<ExpandMoreOutlined
fontSize="small"
className={`${
className={`flex-shrink-0 ${
open ? "rotate-180" : ""
} !hidden group-hover:!block text-custom-sidebar-text-200 duration-300`}
/>
@ -202,7 +211,7 @@ export const SingleSidebarProject: React.FC<Props> = ({
</Tooltip>
{!sidebarCollapse && (
<CustomMenu className="hidden group-hover:block" ellipsis>
<CustomMenu className="hidden group-hover:block flex-shrink-0" ellipsis>
{!shortContextMenu && (
<CustomMenu.MenuItem onClick={handleDeleteProject}>
<span className="flex items-center justify-start gap-2 ">
@ -257,7 +266,7 @@ export const SingleSidebarProject: React.FC<Props> = ({
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
>
<Disclosure.Panel className={`space-y-2 ${sidebarCollapse ? "" : "ml-[2.25rem]"}`}>
<Disclosure.Panel className={`space-y-2 mt-1 ${sidebarCollapse ? "" : "ml-[2.25rem]"}`}>
{navigation(workspaceSlug as string, project?.id).map((item) => {
if (
(item.name === "Cycles" && !project.cycle_view) ||

View File

@ -7,7 +7,7 @@ import { PrimaryButton } from "components/ui";
type Props = {
title: string;
description: React.ReactNode | string;
description?: React.ReactNode;
image: any;
primaryButton?: {
icon?: any;
@ -34,7 +34,7 @@ export const EmptyState: React.FC<Props> = ({
<div className="text-center flex flex-col items-center w-full">
<Image src={image} className="w-52 sm:w-60" alt={primaryButton?.text} />
<h6 className="text-xl font-semibold mt-6 sm:mt-8 mb-3">{title}</h6>
<p className="text-custom-text-300 mb-7 sm:mb-8">{description}</p>
{description && <p className="text-custom-text-300 mb-7 sm:mb-8">{description}</p>}
<div className="flex items-center gap-4">
{primaryButton && (
<PrimaryButton className="flex items-center gap-1.5" onClick={primaryButton.onClick}>

View File

@ -3,10 +3,10 @@ import { Fragment } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
// next-themes
import { useTheme } from "next-themes";
// headless ui
import { Menu, Transition } from "@headlessui/react";
// next-themes
import { useTheme } from "next-themes";
// hooks
import useUser from "hooks/use-user";
import useThemeHook from "hooks/use-theme";
@ -91,7 +91,7 @@ export const WorkspaceSidebarDropdown = () => {
.then(() => {
mutateUser(undefined);
router.push("/");
setTheme("dark");
setTheme("system");
})
.catch(() =>
setToastAlert({

View File

@ -1,6 +1,16 @@
export const THEMES = ["light", "dark", "light-contrast", "dark-contrast", "custom"];
export const THEMES_OBJ = [
{
value: "system",
label: "System Preference",
type: "light",
icon: {
border: "#DEE2E6",
color1: "#FAFAFA",
color2: "#3F76FF",
},
},
{
value: "light",
label: "Light",

View File

@ -11,7 +11,7 @@ import projectService from "services/project.service";
// fetch-keys
import { USER_PROJECT_VIEW } from "constants/fetch-keys";
// helper
import { applyTheme } from "helpers/theme.helper";
import { applyTheme, unsetCustomCssVariables } from "helpers/theme.helper";
// constants
export const themeContext = createContext<ContextType>({} as ContextType);
@ -92,15 +92,18 @@ export const ThemeContextProvider: React.FC<{ children: React.ReactNode }> = ({
useEffect(() => {
const theme = localStorage.getItem("theme");
if (theme && theme === "custom") {
if (user && user.theme.palette) {
applyTheme(
user.theme.palette !== ",,,,"
? user.theme.palette
: "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
user.theme.darkPalette
);
}
if (theme) {
if (theme === "custom") {
if (user && user.theme.palette) {
applyTheme(
user.theme.palette !== ",,,,"
? user.theme.palette
: "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
user.theme.darkPalette
);
}
} else unsetCustomCssVariables();
}
}, [user]);

View File

@ -60,6 +60,7 @@ const calculateShades = (hexValue: string): TShades => {
};
export const applyTheme = (palette: string, isDarkPalette: boolean) => {
const dom = document?.querySelector<HTMLElement>("[data-theme='custom']");
// palette: [bg, text, primary, sidebarBg, sidebarText]
const values: string[] = palette.split(",");
values.push(isDarkPalette ? "dark" : "light");
@ -79,41 +80,40 @@ export const applyTheme = (palette: string, isDarkPalette: boolean) => {
const sidebarBackgroundRgbValues = `${sidebarBackgroundShades[shade].r}, ${sidebarBackgroundShades[shade].g}, ${sidebarBackgroundShades[shade].b}`;
const sidebarTextRgbValues = `${sidebarTextShades[shade].r}, ${sidebarTextShades[shade].g}, ${sidebarTextShades[shade].b}`;
document
.querySelector<HTMLElement>("[data-theme='custom']")
?.style.setProperty(`--color-background-${shade}`, bgRgbValues);
document
.querySelector<HTMLElement>("[data-theme='custom']")
?.style.setProperty(`--color-text-${shade}`, textRgbValues);
document
.querySelector<HTMLElement>("[data-theme='custom']")
?.style.setProperty(`--color-primary-${shade}`, primaryRgbValues);
document
.querySelector<HTMLElement>("[data-theme='custom']")
?.style.setProperty(`--color-sidebar-background-${shade}`, sidebarBackgroundRgbValues);
document
.querySelector<HTMLElement>("[data-theme='custom']")
?.style.setProperty(`--color-sidebar-text-${shade}`, sidebarTextRgbValues);
dom?.style.setProperty(`--color-background-${shade}`, bgRgbValues);
dom?.style.setProperty(`--color-text-${shade}`, textRgbValues);
dom?.style.setProperty(`--color-primary-${shade}`, primaryRgbValues);
dom?.style.setProperty(`--color-sidebar-background-${shade}`, sidebarBackgroundRgbValues);
dom?.style.setProperty(`--color-sidebar-text-${shade}`, sidebarTextRgbValues);
if (i >= 100 && i <= 400) {
const borderShade = i === 100 ? 70 : i === 200 ? 80 : i === 300 ? 90 : 100;
document
.querySelector<HTMLElement>("[data-theme='custom']")
?.style.setProperty(
`--color-border-${shade}`,
`${bgShades[borderShade].r}, ${bgShades[borderShade].g}, ${bgShades[borderShade].b}`
);
document
.querySelector<HTMLElement>("[data-theme='custom']")
?.style.setProperty(
`--color-sidebar-border-${shade}`,
`${sidebarBackgroundShades[borderShade].r}, ${sidebarBackgroundShades[borderShade].g}, ${sidebarBackgroundShades[borderShade].b}`
);
dom?.style.setProperty(
`--color-border-${shade}`,
`${bgShades[borderShade].r}, ${bgShades[borderShade].g}, ${bgShades[borderShade].b}`
);
dom?.style.setProperty(
`--color-sidebar-border-${shade}`,
`${sidebarBackgroundShades[borderShade].r}, ${sidebarBackgroundShades[borderShade].g}, ${sidebarBackgroundShades[borderShade].b}`
);
}
}
document
.querySelector<HTMLElement>("[data-theme='custom']")
?.style.setProperty("--color-scheme", values[5]);
dom?.style.setProperty("--color-scheme", values[5]);
};
export const unsetCustomCssVariables = () => {
for (let i = 10; i <= 900; i >= 100 ? (i += 100) : (i += 10)) {
const dom = document.querySelector<HTMLElement>("[data-theme='custom']");
dom?.style.removeProperty(`--color-background-${i}`);
dom?.style.removeProperty(`--color-text-${i}`);
dom?.style.removeProperty(`--color-border-${i}`);
dom?.style.removeProperty(`--color-primary-${i}`);
dom?.style.removeProperty(`--color-sidebar-background-${i}`);
dom?.style.removeProperty(`--color-sidebar-text-${i}`);
dom?.style.removeProperty(`--color-sidebar-border-${i}`);
dom?.style.removeProperty("--color-scheme");
}
};

View File

@ -70,7 +70,8 @@ const useCommentReaction = (
mutateCommentReactions(
(prevData) =>
prevData?.filter((r) => r.actor !== user?.user?.id || r.reaction !== reaction) || []
prevData?.filter((r) => r.actor !== user?.user?.id || r.reaction !== reaction) || [],
false
);
await reactionService.deleteIssueCommentReaction(

View File

@ -185,6 +185,26 @@ const useUserNotification = () => {
}
};
const markNotificationAsRead = async (notificationId: string) => {
if (!workspaceSlug) return;
const isRead =
notifications?.find((notification) => notification.id === notificationId)?.read_at !== null;
if (isRead) return;
mutateNotification(notificationId, { read_at: new Date() });
handleReadMutation("read");
await userNotificationServices
.markUserNotificationAsRead(workspaceSlug.toString(), notificationId)
.catch(() => {
throw new Error("Something went wrong");
});
mutateNotificationCount();
};
const markNotificationArchivedStatus = async (notificationId: string) => {
if (!workspaceSlug) return;
const isArchived =
@ -283,6 +303,7 @@ const useUserNotification = () => {
hasMore,
isRefreshing,
setFetchNotifications,
markNotificationAsRead,
};
};

View File

@ -1,21 +1,43 @@
import useSWR from "swr";
import { useRouter } from "next/router";
import Link from "next/link";
// services
import userService from "services/user.service";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import SettingsNavbar from "layouts/settings-navbar";
// components
import { Feeds } from "components/core";
import { ActivityIcon, ActivityMessage } from "components/core";
import RemirrorRichTextEditor from "components/rich-text-editor";
// icons
import { ArrowTopRightOnSquareIcon, ChatBubbleLeftEllipsisIcon } from "@heroicons/react/24/outline";
// ui
import { Loader } from "components/ui";
import { Icon, Loader } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// fetch-keys
import { USER_ACTIVITY } from "constants/fetch-keys";
// helper
import { timeAgo } from "helpers/date-time.helper";
const ProfileActivity = () => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { data: userActivity } = useSWR(USER_ACTIVITY, () => userService.getUserActivity());
if (!userActivity) {
return (
<Loader className="space-y-5">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
</Loader>
);
}
return (
<WorkspaceAuthorizationLayout
breadcrumbs={
@ -34,17 +56,176 @@ const ProfileActivity = () => {
</div>
<SettingsNavbar profilePage />
</div>
{userActivity ? (
userActivity.results.length > 0 ? (
<Feeds activities={userActivity.results} />
) : null
) : (
<Loader className="space-y-5">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
</Loader>
{userActivity && userActivity.results.length > 0 && (
<div>
<ul role="list" className="-mb-4">
{userActivity.results.map((activityItem: any, activityIdx: number) => {
if (activityItem.field === "comment") {
return (
<div key={activityItem.id} className="mt-2">
<div className="relative flex items-start space-x-3">
<div className="relative px-1">
{activityItem.field ? (
activityItem.new_value === "restore" && (
<Icon iconName="history" className="text-sm text-custom-text-200" />
)
) : activityItem.actor_detail.avatar &&
activityItem.actor_detail.avatar !== "" ? (
<img
src={activityItem.actor_detail.avatar}
alt={activityItem.actor_detail.first_name}
height={30}
width={30}
className="grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white"
/>
) : (
<div
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white`}
>
{activityItem.actor_detail.first_name.charAt(0)}
</div>
)}
<span className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-custom-background-80 text-custom-text-200 ring-white">
<ChatBubbleLeftEllipsisIcon
className="h-3.5 w-3.5 text-custom-text-200"
aria-hidden="true"
/>
</span>
</div>
<div className="min-w-0 flex-1">
<div>
<div className="text-xs">
{activityItem.actor_detail.first_name}
{activityItem.actor_detail.is_bot
? "Bot"
: " " + activityItem.actor_detail.last_name}
</div>
<p className="mt-0.5 text-xs text-custom-text-200">
Commented {timeAgo(activityItem.created_at)}
</p>
</div>
<div className="issue-comments-section p-0">
<RemirrorRichTextEditor
value={
activityItem.new_value && activityItem.new_value !== ""
? activityItem.new_value
: activityItem.old_value
}
editable={false}
noBorder
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
/>
</div>
</div>
</div>
</div>
);
}
const message =
activityItem.verb === "created" &&
activityItem.field !== "cycles" &&
activityItem.field !== "modules" &&
activityItem.field !== "attachment" &&
activityItem.field !== "link" &&
activityItem.field !== "estimate" ? (
<span className="text-custom-text-200">
created{" "}
<Link
href={`/${workspaceSlug}/projects/${activityItem.project}/issues/${activityItem.issue}`}
>
<a className="inline-flex items-center hover:underline">
this issue. <ArrowTopRightOnSquareIcon className="ml-1 h-3.5 w-3.5" />
</a>
</Link>
</span>
) : activityItem.field ? (
<ActivityMessage activity={activityItem} showIssue />
) : (
"created the issue."
);
if ("field" in activityItem && activityItem.field !== "updated_by") {
return (
<li key={activityItem.id}>
<div className="relative pb-1">
{userActivity.results.length > 1 &&
activityIdx !== userActivity.results.length - 1 ? (
<span
className="absolute top-5 left-5 -ml-px h-full w-0.5 bg-custom-background-80"
aria-hidden="true"
/>
) : null}
<div className="relative flex items-start space-x-2">
<>
<div>
<div className="relative px-1.5">
<div className="mt-1.5">
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-custom-background-80 text-custom-text-200 ring-white">
{activityItem.field ? (
activityItem.new_value === "restore" ? (
<Icon
iconName="history"
className="text-sm text-custom-text-200"
/>
) : (
<ActivityIcon activity={activityItem} />
)
) : activityItem.actor_detail.avatar &&
activityItem.actor_detail.avatar !== "" ? (
<img
src={activityItem.actor_detail.avatar}
alt={activityItem.actor_detail.first_name}
height={24}
width={24}
className="rounded-full"
/>
) : (
<div
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs text-white`}
>
{activityItem.actor_detail.first_name.charAt(0)}
</div>
)}
</div>
</div>
</div>
</div>
<div className="min-w-0 flex-1 py-3">
<div className="text-xs text-custom-text-200 break-words">
{activityItem.field === "archived_at" &&
activityItem.new_value !== "restore" ? (
<span className="text-gray font-medium">Plane</span>
) : activityItem.actor_detail.is_bot ? (
<span className="text-gray font-medium">
{activityItem.actor_detail.first_name} Bot
</span>
) : (
<Link
href={`/${workspaceSlug}/profile/${activityItem.actor_detail.id}`}
>
<a className="text-gray font-medium">
{activityItem.actor_detail.first_name}{" "}
{activityItem.actor_detail.last_name}
</a>
</Link>
)}{" "}
{message}{" "}
<span className="whitespace-nowrap">
{timeAgo(activityItem.created_at)}
</span>
</div>
</div>
</>
</div>
</div>
</li>
);
}
})}
</ul>
</div>
)}
</div>
</WorkspaceAuthorizationLayout>

View File

@ -1,6 +1,7 @@
import { useEffect, useState } from "react";
import { useTheme } from "next-themes";
// next-themes
import { useTheme } from "next-themes";
// hooks
import useUserAuth from "hooks/use-user-auth";
// layouts
@ -15,11 +16,13 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
import { ICustomTheme } from "types";
const ProfilePreferences = () => {
const { user: myProfile } = useUserAuth();
const { theme } = useTheme();
const [customThemeSelectorOptions, setCustomThemeSelectorOptions] = useState(false);
const [preLoadedData, setPreLoadedData] = useState<ICustomTheme | null>(null);
const { theme } = useTheme();
const { user: myProfile } = useUserAuth();
useEffect(() => {
if (theme === "custom") {
if (myProfile?.theme.palette)
@ -37,6 +40,7 @@ const ProfilePreferences = () => {
myProfile.theme.palette !== ",,,,"
? myProfile.theme.palette
: "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
theme: "custom",
});
if (!customThemeSelectorOptions) setCustomThemeSelectorOptions(true);
}
@ -71,7 +75,6 @@ const ProfilePreferences = () => {
</div>
<div className="col-span-12 sm:col-span-6">
<ThemeSwitch
user={myProfile}
setPreLoadedData={setPreLoadedData}
customThemeSelectorOptions={customThemeSelectorOptions}
setCustomThemeSelectorOptions={setCustomThemeSelectorOptions}

View File

@ -22,8 +22,10 @@ import useUserAuth from "hooks/use-user-auth";
// components
import { AnalyticsProjectModal } from "components/analytics";
// ui
import { CustomMenu, SecondaryButton } from "components/ui";
import { CustomMenu, EmptyState, SecondaryButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// images
import emptyCycle from "public/empty-state/cycle.svg";
// helpers
import { truncateText } from "helpers/string.helper";
import { getDateRangeStatus } from "helpers/date-time.helper";
@ -52,14 +54,14 @@ const SingleCycle: React.FC = () => {
: null
);
const { data: cycleDetails } = useSWR(
cycleId ? CYCLE_DETAILS(cycleId as string) : null,
const { data: cycleDetails, error } = useSWR(
workspaceSlug && projectId && cycleId ? CYCLE_DETAILS(cycleId.toString()) : null,
workspaceSlug && projectId && cycleId
? () =>
cycleServices.getCycleDetails(
workspaceSlug as string,
projectId as string,
cycleId as string
workspaceSlug.toString(),
projectId.toString(),
cycleId.toString()
)
: null
);
@ -159,31 +161,48 @@ const SingleCycle: React.FC = () => {
</div>
}
>
<TransferIssuesModal
handleClose={() => setTransferIssuesModal(false)}
isOpen={transferIssuesModal}
/>
<AnalyticsProjectModal isOpen={analyticsModal} onClose={() => setAnalyticsModal(false)} />
<div
className={`h-full flex flex-col ${cycleSidebar ? "mr-[24rem]" : ""} ${
analyticsModal ? "mr-[50%]" : ""
} duration-300`}
>
{cycleStatus === "completed" && (
<TransferIssues handleClick={() => setTransferIssuesModal(true)} />
)}
<IssuesView
openIssuesListModal={openIssuesListModal}
disableUserActions={cycleStatus === "completed" ?? false}
{error ? (
<EmptyState
image={emptyCycle}
title="Cycle does not exist"
description="The cycle you are looking for does not exist or has been deleted."
primaryButton={{
text: "View other cycles",
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/cycles`),
}}
/>
</div>
<CycleDetailsSidebar
cycleStatus={cycleStatus}
cycle={cycleDetails}
isOpen={cycleSidebar}
isCompleted={cycleStatus === "completed" ?? false}
user={user}
/>
) : (
<>
<TransferIssuesModal
handleClose={() => setTransferIssuesModal(false)}
isOpen={transferIssuesModal}
/>
<AnalyticsProjectModal
isOpen={analyticsModal}
onClose={() => setAnalyticsModal(false)}
/>
<div
className={`h-full flex flex-col ${cycleSidebar ? "mr-[24rem]" : ""} ${
analyticsModal ? "mr-[50%]" : ""
} duration-300`}
>
{cycleStatus === "completed" && (
<TransferIssues handleClick={() => setTransferIssuesModal(true)} />
)}
<IssuesView
openIssuesListModal={openIssuesListModal}
disableUserActions={cycleStatus === "completed" ?? false}
/>
</div>
<CycleDetailsSidebar
cycleStatus={cycleStatus}
cycle={cycleDetails}
isOpen={cycleSidebar}
isCompleted={cycleStatus === "completed" ?? false}
user={user}
/>
</>
)}
</ProjectAuthorizationWrapper>
</IssueViewContextProvider>
);

View File

@ -15,8 +15,10 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
// components
import { IssueDetailsSidebar, IssueMainContent } from "components/issues";
// ui
import { Loader } from "components/ui";
import { EmptyState, Loader } from "components/ui";
import { Breadcrumbs } from "components/breadcrumbs";
// images
import emptyIssue from "public/empty-state/issue.svg";
// types
import { IIssue } from "types";
import type { NextPage } from "next";
@ -45,7 +47,11 @@ const IssueDetailsPage: NextPage = () => {
const { user } = useUserAuth();
const { data: issueDetails, mutate: mutateIssueDetails } = useSWR<IIssue | undefined>(
const {
data: issueDetails,
mutate: mutateIssueDetails,
error,
} = useSWR(
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null,
workspaceSlug && projectId && issueId
? () =>
@ -125,7 +131,17 @@ const IssueDetailsPage: NextPage = () => {
</Breadcrumbs>
}
>
{issueDetails && projectId ? (
{error ? (
<EmptyState
image={emptyIssue}
title="Issue does not exist"
description="The issue you are looking for does not exist, has been archived, or has been deleted."
primaryButton={{
text: "View other issues",
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/issues`),
}}
/>
) : issueDetails && projectId ? (
<div className="flex h-full overflow-hidden">
<div className="w-2/3 h-full overflow-y-auto space-y-5 divide-y-2 divide-custom-border-300 p-5">
<IssueMainContent issueDetails={issueDetails} submitChanges={submitChanges} />

View File

@ -20,8 +20,10 @@ import { ExistingIssuesListModal, IssuesFilterView, IssuesView } from "component
import { ModuleDetailsSidebar } from "components/modules";
import { AnalyticsProjectModal } from "components/analytics";
// ui
import { CustomMenu, SecondaryButton } from "components/ui";
import { CustomMenu, EmptyState, SecondaryButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// images
import emptyModule from "public/empty-state/module.svg";
// helpers
import { truncateText } from "helpers/string.helper";
// types
@ -60,7 +62,7 @@ const SingleModule: React.FC = () => {
: null
);
const { data: moduleDetails } = useSWR(
const { data: moduleDetails, error } = useSWR(
moduleId ? MODULE_DETAILS(moduleId as string) : null,
workspaceSlug && projectId
? () =>
@ -162,22 +164,37 @@ const SingleModule: React.FC = () => {
</div>
}
>
<AnalyticsProjectModal isOpen={analyticsModal} onClose={() => setAnalyticsModal(false)} />
<div
className={`h-full flex flex-col ${moduleSidebar ? "mr-[24rem]" : ""} ${
analyticsModal ? "mr-[50%]" : ""
} duration-300`}
>
<IssuesView openIssuesListModal={openIssuesListModal} />
</div>
<ModuleDetailsSidebar
module={moduleDetails}
isOpen={moduleSidebar}
moduleIssues={moduleIssues}
user={user}
/>
{error ? (
<EmptyState
image={emptyModule}
title="Module does not exist"
description="The module you are looking for does not exist or has been deleted."
primaryButton={{
text: "View other modules",
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/modules`),
}}
/>
) : (
<>
<AnalyticsProjectModal
isOpen={analyticsModal}
onClose={() => setAnalyticsModal(false)}
/>
<div
className={`h-full flex flex-col ${moduleSidebar ? "mr-[24rem]" : ""} ${
analyticsModal ? "mr-[50%]" : ""
} duration-300`}
>
<IssuesView openIssuesListModal={openIssuesListModal} />
</div>
<ModuleDetailsSidebar
module={moduleDetails}
isOpen={moduleSidebar}
moduleIssues={moduleIssues}
user={user}
/>
</>
)}
</ProjectAuthorizationWrapper>
</IssueViewContextProvider>
);

View File

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { useRouter } from "next/router";
@ -28,7 +28,16 @@ import { CreateLabelModal } from "components/labels";
import { CreateBlock } from "components/pages/create-block";
// ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
import { CustomSearchSelect, Loader, TextArea, ToggleSwitch, Tooltip } from "components/ui";
import {
CustomSearchSelect,
EmptyState,
Loader,
TextArea,
ToggleSwitch,
Tooltip,
} from "components/ui";
// images
import emptyPage from "public/empty-state/page.svg";
// icons
import {
ArrowLeftIcon,
@ -40,7 +49,7 @@ import {
XMarkIcon,
ChevronDownIcon,
} from "@heroicons/react/24/outline";
import { ColorPalletteIcon, ClipboardIcon } from "components/icons";
import { ColorPalletteIcon } from "components/icons";
// helpers
import { render24HourFormatTime, renderShortDate } from "helpers/date-time.helper";
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
@ -82,7 +91,7 @@ const SinglePage: NextPage = () => {
: null
);
const { data: pageDetails } = useSWR(
const { data: pageDetails, error } = useSWR(
workspaceSlug && projectId && pageId ? PAGE_DETAILS(pageId as string) : null,
workspaceSlug && projectId
? () =>
@ -267,13 +276,6 @@ const SinglePage: NextPage = () => {
);
};
const handleNewBlock = useCallback(() => {
setCreateBlockForm(true);
scrollToRef.current?.scrollIntoView({
behavior: "smooth",
});
}, [setCreateBlockForm, scrollToRef]);
const handleShowBlockToggle = async () => {
if (!workspaceSlug || !projectId) return;
@ -311,22 +313,21 @@ const SinglePage: NextPage = () => {
});
};
const options =
labels?.map((label) => ({
value: label.id,
query: label.name,
content: (
<div className="flex items-center gap-2">
<span
className="h-2 w-2 flex-shrink-0 rounded-full"
style={{
backgroundColor: label.color && label.color !== "" ? label.color : "#000000",
}}
/>
{label.name}
</div>
),
})) ?? [];
const options = labels?.map((label) => ({
value: label.id,
query: label.name,
content: (
<div className="flex items-center gap-2">
<span
className="h-2 w-2 flex-shrink-0 rounded-full"
style={{
backgroundColor: label.color && label.color !== "" ? label.color : "#000000",
}}
/>
{label.name}
</div>
),
}));
useEffect(() => {
if (!pageDetails) return;
@ -346,11 +347,21 @@ const SinglePage: NextPage = () => {
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project",32)} Pages`} />
<BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project", 32)} Pages`} />
</Breadcrumbs>
}
>
{pageDetails ? (
{error ? (
<EmptyState
image={emptyPage}
title="Page does not exist"
description="The page you are looking for does not exist or has been deleted."
primaryButton={{
text: "View other pages",
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/pages`),
}}
/>
) : pageDetails ? (
<div className="flex h-full flex-col justify-between space-y-4 overflow-hidden p-4">
<div className="h-full w-full overflow-y-auto">
<div className="flex items-start justify-between gap-2">

View File

@ -12,11 +12,13 @@ import { IssueViewContextProvider } from "contexts/issue-view.context";
// components
import { IssuesFilterView, IssuesView } from "components/core";
// ui
import { CustomMenu, PrimaryButton } from "components/ui";
import { CustomMenu, EmptyState, PrimaryButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
import { StackedLayersIcon } from "components/icons";
// images
import emptyView from "public/empty-state/view.svg";
// helpers
import { truncateText } from "helpers/string.helper";
// fetch-keys
@ -40,7 +42,7 @@ const SingleView: React.FC = () => {
: null
);
const { data: viewDetails } = useSWR(
const { data: viewDetails, error } = useSWR(
workspaceSlug && projectId && viewId ? VIEW_DETAILS(viewId as string) : null,
workspaceSlug && projectId && viewId
? () =>
@ -101,9 +103,21 @@ const SingleView: React.FC = () => {
</div>
}
>
<div className="h-full w-full flex flex-col">
<IssuesView />
</div>
{error ? (
<EmptyState
image={emptyView}
title="View does not exist"
description="The view you are looking for does not exist or has been deleted."
primaryButton={{
text: "View other views",
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/views`),
}}
/>
) : (
<div className="h-full w-full flex flex-col">
<IssuesView />
</div>
)}
</ProjectAuthorizationWrapper>
</IssueViewContextProvider>
);

View File

@ -45,7 +45,7 @@ Router.events.on("routeChangeComplete", NProgress.done);
function MyApp({ Component, pageProps }: AppProps) {
return (
// <UserProvider>
<ThemeProvider themes={THEMES} defaultTheme="dark">
<ThemeProvider themes={THEMES} defaultTheme="system">
<ToastContextProvider>
<ThemeContextProvider>
<CrispWithNoSSR />

View File

@ -1,143 +0,0 @@
import React from "react";
// layouts
import DefaultLayout from "layouts/default-layout";
import { UserAuthorizationLayout } from "layouts/auth-layout/user-authorization-wrapper";
// types
import type { NextPage } from "next";
const Colors: NextPage = () => (
<UserAuthorizationLayout>
<DefaultLayout>
<div className="space-y-8 p-8">
<div>
Primary:
<div className="flex flex-wrap">
<div className="h-12 w-12 bg-custom-primary-0" />
<div className="h-12 w-12 bg-custom-primary-10" />
<div className="h-12 w-12 bg-custom-primary-20" />
<div className="h-12 w-12 bg-custom-primary-30" />
<div className="h-12 w-12 bg-custom-primary-40" />
<div className="h-12 w-12 bg-custom-primary-50" />
<div className="h-12 w-12 bg-custom-primary-60" />
<div className="h-12 w-12 bg-custom-primary-70" />
<div className="h-12 w-12 bg-custom-primary-80" />
<div className="h-12 w-12 bg-custom-primary-90" />
<div className="h-12 w-12 bg-custom-primary-100" />
<div className="h-12 w-12 bg-custom-primary-200" />
<div className="h-12 w-12 bg-custom-primary-300" />
<div className="h-12 w-12 bg-custom-primary-400" />
<div className="h-12 w-12 bg-custom-primary-500" />
<div className="h-12 w-12 bg-custom-primary-600" />
<div className="h-12 w-12 bg-custom-primary-700" />
<div className="h-12 w-12 bg-custom-primary-800" />
<div className="h-12 w-12 bg-custom-primary-900" />
<div className="h-12 w-12 bg-custom-primary-1000" />
</div>
</div>
<div>
Background:
<div className="flex flex-wrap">
<div className="h-12 w-12 bg-custom-background-0" />
<div className="h-12 w-12 bg-custom-background-10" />
<div className="h-12 w-12 bg-custom-background-20" />
<div className="h-12 w-12 bg-custom-background-30" />
<div className="h-12 w-12 bg-custom-background-40" />
<div className="h-12 w-12 bg-custom-background-50" />
<div className="h-12 w-12 bg-custom-background-60" />
<div className="h-12 w-12 bg-custom-background-70" />
<div className="h-12 w-12 bg-custom-background-80" />
<div className="h-12 w-12 bg-custom-background-90" />
<div className="h-12 w-12 bg-custom-background-100" />
<div className="h-12 w-12 bg-custom-background-200" />
<div className="h-12 w-12 bg-custom-background-300" />
<div className="h-12 w-12 bg-custom-background-400" />
<div className="h-12 w-12 bg-custom-background-500" />
<div className="h-12 w-12 bg-custom-background-600" />
<div className="h-12 w-12 bg-custom-background-700" />
<div className="h-12 w-12 bg-custom-background-800" />
<div className="h-12 w-12 bg-custom-background-900" />
<div className="h-12 w-12 bg-custom-background-1000" />
</div>
</div>
<div>
Text:
<div className="flex flex-wrap">
<div className="h-12 w-12 bg-custom-text-0" />
<div className="h-12 w-12 bg-custom-text-10" />
<div className="h-12 w-12 bg-custom-text-20" />
<div className="h-12 w-12 bg-custom-text-30" />
<div className="h-12 w-12 bg-custom-text-40" />
<div className="h-12 w-12 bg-custom-text-50" />
<div className="h-12 w-12 bg-custom-text-60" />
<div className="h-12 w-12 bg-custom-text-70" />
<div className="h-12 w-12 bg-custom-text-80" />
<div className="h-12 w-12 bg-custom-text-90" />
<div className="h-12 w-12 bg-custom-text-100" />
<div className="h-12 w-12 bg-custom-text-200" />
<div className="h-12 w-12 bg-custom-text-300" />
<div className="h-12 w-12 bg-custom-text-400" />
<div className="h-12 w-12 bg-custom-text-500" />
<div className="h-12 w-12 bg-custom-text-600" />
<div className="h-12 w-12 bg-custom-text-700" />
<div className="h-12 w-12 bg-custom-text-800" />
<div className="h-12 w-12 bg-custom-text-900" />
<div className="h-12 w-12 bg-custom-text-1000" />
</div>
</div>
<div>
Sidebar Background:
<div className="flex flex-wrap">
<div className="h-12 w-12 bg-custom-sidebar-background-0" />
<div className="h-12 w-12 bg-custom-sidebar-background-10" />
<div className="h-12 w-12 bg-custom-sidebar-background-20" />
<div className="h-12 w-12 bg-custom-sidebar-background-30" />
<div className="h-12 w-12 bg-custom-sidebar-background-40" />
<div className="h-12 w-12 bg-custom-sidebar-background-50" />
<div className="h-12 w-12 bg-custom-sidebar-background-60" />
<div className="h-12 w-12 bg-custom-sidebar-background-70" />
<div className="h-12 w-12 bg-custom-sidebar-background-80" />
<div className="h-12 w-12 bg-custom-sidebar-background-90" />
<div className="h-12 w-12 bg-custom-sidebar-background-100" />
<div className="h-12 w-12 bg-custom-sidebar-background-200" />
<div className="h-12 w-12 bg-custom-sidebar-background-300" />
<div className="h-12 w-12 bg-custom-sidebar-background-400" />
<div className="h-12 w-12 bg-custom-sidebar-background-500" />
<div className="h-12 w-12 bg-custom-sidebar-background-600" />
<div className="h-12 w-12 bg-custom-sidebar-background-700" />
<div className="h-12 w-12 bg-custom-sidebar-background-800" />
<div className="h-12 w-12 bg-custom-sidebar-background-900" />
<div className="h-12 w-12 bg-custom-sidebar-background-1000" />
</div>
</div>
<div>
Sidebar Text:
<div className="flex flex-wrap">
<div className="h-12 w-12 bg-custom-sidebar-text-0" />
<div className="h-12 w-12 bg-custom-sidebar-text-10" />
<div className="h-12 w-12 bg-custom-sidebar-text-20" />
<div className="h-12 w-12 bg-custom-sidebar-text-30" />
<div className="h-12 w-12 bg-custom-sidebar-text-40" />
<div className="h-12 w-12 bg-custom-sidebar-text-50" />
<div className="h-12 w-12 bg-custom-sidebar-text-60" />
<div className="h-12 w-12 bg-custom-sidebar-text-70" />
<div className="h-12 w-12 bg-custom-sidebar-text-80" />
<div className="h-12 w-12 bg-custom-sidebar-text-90" />
<div className="h-12 w-12 bg-custom-sidebar-text-100" />
<div className="h-12 w-12 bg-custom-sidebar-text-200" />
<div className="h-12 w-12 bg-custom-sidebar-text-300" />
<div className="h-12 w-12 bg-custom-sidebar-text-400" />
<div className="h-12 w-12 bg-custom-sidebar-text-500" />
<div className="h-12 w-12 bg-custom-sidebar-text-600" />
<div className="h-12 w-12 bg-custom-sidebar-text-700" />
<div className="h-12 w-12 bg-custom-sidebar-text-800" />
<div className="h-12 w-12 bg-custom-sidebar-text-900" />
<div className="h-12 w-12 bg-custom-sidebar-text-1000" />
</div>
</div>
</div>
</DefaultLayout>
</UserAuthorizationLayout>
);
export default Colors;

View File

@ -1,4 +1,4 @@
import React from "react";
import React, { useEffect } from "react";
import Image from "next/image";
@ -22,6 +22,8 @@ import {
import { Spinner } from "components/ui";
// images
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
import { useTheme } from "next-themes";
import { ICurrentUserResponse, IUser } from "types";
// types
type EmailPasswordFormValues = {
email: string;
@ -34,6 +36,12 @@ const HomePage: NextPage = () => {
const { setToastAlert } = useToast();
const { setTheme } = useTheme();
const changeTheme = (user: IUser) => {
setTheme(user.theme.theme ?? "system");
};
const handleGoogleSignIn = async ({ clientId, credential }: any) => {
try {
if (clientId && credential) {
@ -43,7 +51,10 @@ const HomePage: NextPage = () => {
clientId,
};
const response = await authenticationService.socialAuth(socialAuthPayload);
if (response && response?.user) mutateUser();
if (response && response?.user) {
mutateUser();
changeTheme(response.user);
}
} else {
throw Error("Cant find credentials");
}
@ -66,7 +77,10 @@ const HomePage: NextPage = () => {
clientId: process.env.NEXT_PUBLIC_GITHUB_ID,
};
const response = await authenticationService.socialAuth(socialAuthPayload);
if (response && response?.user) mutateUser();
if (response && response?.user) {
mutateUser();
changeTheme(response.user);
}
} else {
throw Error("Cant find credentials");
}
@ -85,7 +99,10 @@ const HomePage: NextPage = () => {
.emailLogin(formData)
.then((response) => {
try {
if (response) mutateUser();
if (response) {
mutateUser();
changeTheme(response.user);
}
} catch (err: any) {
setToastAlert({
type: "error",
@ -109,7 +126,10 @@ const HomePage: NextPage = () => {
const handleEmailCodeSignIn = async (response: any) => {
try {
if (response) mutateUser();
if (response) {
mutateUser();
changeTheme(response.user);
}
} catch (err: any) {
setToastAlert({
type: "error",
@ -120,6 +140,10 @@ const HomePage: NextPage = () => {
}
};
useEffect(() => {
setTheme("system");
}, [setTheme]);
return (
<DefaultLayout>
{isLoading ? (

View File

@ -1,6 +1,9 @@
import React, { useState, useEffect } from "react";
// next imports
import { useRouter } from "next/router";
// next-themes
import { useTheme } from "next-themes";
// layouts
import DefaultLayout from "layouts/default-layout";
// services
@ -17,11 +20,17 @@ const MagicSignIn: NextPage = () => {
const { setToastAlert } = useToast();
const { user, isLoading, mutateUser } = useUserAuth("sign-in");
const { setTheme } = useTheme();
const { mutateUser } = useUserAuth("sign-in");
const [isSigningIn, setIsSigningIn] = useState(false);
const [errorSigningIn, setErrorSignIn] = useState<string | undefined>();
useEffect(() => {
setTheme("system");
}, [setTheme]);
useEffect(() => {
setIsSigningIn(() => false);
setErrorSignIn(() => undefined);

View File

@ -1,6 +1,5 @@
import { useEffect, useState } from "react";
import Router from "next/router";
import Image from "next/image";
import useSWR, { mutate } from "swr";
@ -32,7 +31,7 @@ import { CURRENT_USER, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
const Onboarding: NextPage = () => {
const [step, setStep] = useState<number | null>(null);
const { theme } = useTheme();
const { theme, setTheme } = useTheme();
const { user, isLoading: userLoading } = useUserAuth("onboarding");
@ -117,6 +116,10 @@ const Onboarding: NextPage = () => {
await userService.updateUserOnBoard({ userRole: user.role }, user);
};
useEffect(() => {
setTheme("system");
}, [setTheme]);
useEffect(() => {
const handleStepChange = async () => {
if (!user || !invitations) return;

View File

@ -3,6 +3,8 @@ import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import Image from "next/image";
// next-themes
import { useTheme } from "next-themes";
// react-hook-form
import { useForm } from "react-hook-form";
// hooks
@ -31,6 +33,8 @@ const ResetPasswordPage: NextPage = () => {
const { setToastAlert } = useToast();
const { setTheme } = useTheme();
const {
register,
handleSubmit,
@ -76,6 +80,10 @@ const ResetPasswordPage: NextPage = () => {
);
};
useEffect(() => {
setTheme("system");
}, [setTheme]);
useEffect(() => {
if (parseInt(process.env.NEXT_PUBLIC_ENABLE_OAUTH || "0")) router.push("/");
else setIsLoading(false);

View File

@ -3,6 +3,8 @@ import React, { useEffect, useState } from "react";
import Image from "next/image";
import { useRouter } from "next/router";
// next-themes
import { useTheme } from "next-themes";
// services
import authenticationService from "services/authentication.service";
// hooks
@ -31,6 +33,8 @@ const SignUp: NextPage = () => {
const { setToastAlert } = useToast();
const { setTheme } = useTheme();
const { mutateUser } = useUserAuth("sign-in");
const handleSignUp = async (formData: EmailPasswordFormValues) => {
@ -62,6 +66,10 @@ const SignUp: NextPage = () => {
);
};
useEffect(() => {
setTheme("system");
}, [setTheme]);
useEffect(() => {
if (parseInt(process.env.NEXT_PUBLIC_ENABLE_OAUTH || "0")) router.push("/");
else setIsLoading(false);

View File

@ -1,5 +1,6 @@
// services
import APIService from "services/api.service";
import { ICurrentUserResponse } from "types";
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
@ -32,7 +33,11 @@ class AuthService extends APIService {
});
}
async socialAuth(data: any) {
async socialAuth(data: any): Promise<{
access_token: string;
refresh_toke: string;
user: ICurrentUserResponse;
}> {
return this.post("/api/social-auth/", data, { headers: {} })
.then((response) => {
this.setAccessToken(response?.data?.access_token);

View File

@ -46,6 +46,7 @@ export interface ICustomTheme {
sidebarText: string;
darkPalette: boolean;
palette: string;
theme: string;
}
export interface ICurrentUserResponse extends IUser {