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 there is no default state assign any random state
if default_state is None: if default_state is None:
default_state = State.objects.filter( default_state = State.objects.filter(
~Q(name="Triage"), sproject_id=project_id ~Q(name="Triage"), project_id=project_id
).first() ).first()
# Get the maximum sequence_id # Get the maximum sequence_id

View File

@ -75,6 +75,7 @@ from plane.db.models import (
Label, Label,
WorkspaceMember, WorkspaceMember,
CycleIssue, CycleIssue,
IssueReaction,
) )
from plane.api.permissions import ( from plane.api.permissions import (
WorkSpaceBasePermission, WorkSpaceBasePermission,
@ -1321,6 +1322,12 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
) )
.select_related("project", "workspace", "state", "parent") .select_related("project", "workspace", "state", "parent")
.prefetch_related("assignees", "labels") .prefetch_related("assignees", "labels")
.prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related("actor"),
)
)
.order_by("-created_at") .order_by("-created_at")
.annotate( .annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id")) 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 { THEMES_OBJ } from "constants/themes";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { SettingIcon } from "components/icons"; import { SettingIcon } from "components/icons";
import userService from "services/user.service";
import useUser from "hooks/use-user";
type Props = { type Props = {
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>; setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
@ -12,24 +14,50 @@ type Props = {
export const ChangeInterfaceTheme: React.FC<Props> = ({ setIsPaletteOpen }) => { export const ChangeInterfaceTheme: React.FC<Props> = ({ setIsPaletteOpen }) => {
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const { setTheme } = useTheme(); 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 only runs on the client, so now we can safely show the UI
useEffect(() => { useEffect(() => {
setMounted(true); setMounted(true);
}, []); }, []);
if (!mounted) { if (!mounted) return null;
return null;
}
return ( return (
<> <>
{THEMES_OBJ.map((theme) => ( {THEMES_OBJ.filter((t) => t.value !== "custom").map((theme) => (
<Command.Item <Command.Item
key={theme.value} key={theme.value}
onSelect={() => { onSelect={() => {
setTheme(theme.value); updateUserTheme(theme.value);
setIsPaletteOpen(false); setIsPaletteOpen(false);
}} }}
className="focus:outline-none" 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 "./theme";
export * from "./views"; export * from "./views";
export * from "./activity"; export * from "./activity";
export * from "./feeds";
export * from "./reaction-selector"; export * from "./reaction-selector";
export * from "./image-picker-popover"; export * from "./image-picker-popover";

View File

@ -28,6 +28,7 @@ const defaultValues: ICustomTheme = {
sidebarText: "#c5c5c5", sidebarText: "#c5c5c5",
darkPalette: false, darkPalette: false,
palette: "", palette: "",
theme: "custom",
}; };
export const CustomThemeSelector: React.FC<Props> = ({ preLoadedData }) => { export const CustomThemeSelector: React.FC<Props> = ({ preLoadedData }) => {
@ -56,6 +57,7 @@ export const CustomThemeSelector: React.FC<Props> = ({ preLoadedData }) => {
sidebarText: formData.sidebarText, sidebarText: formData.sidebarText,
darkPalette: darkPalette, darkPalette: darkPalette,
palette: `${formData.background},${formData.text},${formData.primary},${formData.sidebarBackground},${formData.sidebarText}`, palette: `${formData.background},${formData.text},${formData.primary},${formData.sidebarBackground},${formData.sidebarText}`,
theme: "custom",
}; };
await userService 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"; import { useTheme } from "next-themes";
// services
import userService from "services/user.service";
// hooks
import useUser from "hooks/use-user";
// constants // constants
import { THEMES_OBJ } from "constants/themes"; import { THEMES_OBJ } from "constants/themes";
// ui // ui
import { CustomSelect } from "components/ui"; import { CustomSelect } from "components/ui";
// types // types
import { ICustomTheme, IUser } from "types"; import { ICustomTheme } from "types";
import { unsetCustomCssVariables } from "helpers/theme.helper";
type Props = { type Props = {
user: IUser | undefined; setPreLoadedData: React.Dispatch<React.SetStateAction<ICustomTheme | null>>;
setPreLoadedData: Dispatch<SetStateAction<ICustomTheme | null>>;
customThemeSelectorOptions: boolean; customThemeSelectorOptions: boolean;
setCustomThemeSelectorOptions: Dispatch<SetStateAction<boolean>>; setCustomThemeSelectorOptions: React.Dispatch<React.SetStateAction<boolean>>;
}; };
export const ThemeSwitch: React.FC<Props> = ({ export const ThemeSwitch: React.FC<Props> = ({
user,
setPreLoadedData, setPreLoadedData,
customThemeSelectorOptions, customThemeSelectorOptions,
setCustomThemeSelectorOptions, setCustomThemeSelectorOptions,
}) => { }) => {
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const { theme, setTheme } = useTheme(); 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 only runs on the client, so now we can safely show the UI
useEffect(() => { useEffect(() => {
setMounted(true); setMounted(true);
}, []); }, []);
if (!mounted) { if (!mounted) return null;
return null;
}
const currentThemeObj = THEMES_OBJ.find((t) => t.value === theme); const currentThemeObj = THEMES_OBJ.find((t) => t.value === theme);
return ( return (
<> <CustomSelect
<CustomSelect value={theme}
value={theme} label={
label={ currentThemeObj ? (
currentThemeObj ? ( <div className="flex items-center gap-2">
<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 <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={{ style={{
borderColor: currentThemeObj.icon.border, background: currentThemeObj.icon.color1,
}} }}
> />
<div <div
className="h-full w-1/2 rounded-l-full" className="h-full w-1/2 rounded-r-full border-l"
style={{ style={{
background: currentThemeObj.icon.color1, borderLeftColor: currentThemeObj.icon.border,
}} background: currentThemeObj.icon.color2,
/> }}
<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> </div>
) : ( {currentThemeObj.label}
"Select your theme" </div>
) ) : (
} "Select your theme"
onChange={({ value, type }: { value: string; type: string }) => { )
if (value === "custom") { }
if (user?.theme.palette) { onChange={({ value, type }: { value: string; type: string }) => {
setPreLoadedData({ if (value === "custom") {
background: user.theme.background !== "" ? user.theme.background : "#0d101b", if (user?.theme.palette) {
text: user.theme.text !== "" ? user.theme.text : "#c5c5c5", setPreLoadedData({
primary: user.theme.primary !== "" ? user.theme.primary : "#3f76ff", background: user.theme.background !== "" ? user.theme.background : "#0d101b",
sidebarBackground: text: user.theme.text !== "" ? user.theme.text : "#c5c5c5",
user.theme.sidebarBackground !== "" ? user.theme.sidebarBackground : "#0d101b", primary: user.theme.primary !== "" ? user.theme.primary : "#3f76ff",
sidebarText: user.theme.sidebarText !== "" ? user.theme.sidebarText : "#c5c5c5", sidebarBackground:
darkPalette: false, user.theme.sidebarBackground !== "" ? user.theme.sidebarBackground : "#0d101b",
palette: sidebarText: user.theme.sidebarText !== "" ? user.theme.sidebarText : "#c5c5c5",
user.theme.palette !== ",,,," darkPalette: false,
? user.theme.palette palette:
: "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5", user.theme.palette !== ",,,,"
}); ? user.theme.palette
} : "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
theme: "custom",
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}`);
}
} }
setTheme(value); if (!customThemeSelectorOptions) setCustomThemeSelectorOptions(true);
document.documentElement.style.setProperty("color-scheme", type); } else {
}} if (customThemeSelectorOptions) setCustomThemeSelectorOptions(false);
input unsetCustomCssVariables();
width="w-full" }
position="right"
> updateUserTheme(value);
{THEMES_OBJ.map(({ value, label, type, icon }) => ( document.documentElement.style.setProperty("--color-scheme", type);
<CustomSelect.Option key={value} value={{ value, type }}> }}
<div className="flex items-center gap-2"> 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 <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={{ style={{
borderColor: icon.border, background: icon.color1,
}} }}
> />
<div <div
className="h-full w-1/2 rounded-l-full" className="h-full w-1/2 rounded-r-full border-l"
style={{ style={{
background: icon.color1, borderLeftColor: icon.border,
}} background: icon.color2,
/> }}
<div />
className="h-full w-1/2 rounded-r-full border-l"
style={{
borderLeftColor: icon.border,
background: icon.color2,
}}
/>
</div>
{label}
</div> </div>
</CustomSelect.Option> {label}
))} </div>
</CustomSelect> </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"; import { useRouter } from "next/router";
@ -98,20 +98,36 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
assignees: [...(prePopulateData?.assignees ?? []), user?.id ?? ""], assignees: [...(prePopulateData?.assignees ?? []), user?.id ?? ""],
}; };
const onClose = useCallback(() => {
handleClose();
setActiveProject(null);
}, [handleClose]);
useEffect(() => { 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) { if (data && data.project) {
setActiveProject(data.project); setActiveProject(data.project);
return; 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) if (projects && projects.length > 0 && !activeProject)
setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null); setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null);
}, [activeProject, data, projectId, projects]); }, [activeProject, data, projectId, projects, isOpen]);
useEffect(() => { useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") { if (e.key === "Escape") {
handleClose(); onClose();
} }
}; };
@ -119,7 +135,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
return () => { return () => {
window.removeEventListener("keydown", handleKeyDown); window.removeEventListener("keydown", handleKeyDown);
}; };
}, [handleClose]); }, [onClose]);
const addIssueToCycle = async (issueId: string, cycleId: string) => { const addIssueToCycle = async (issueId: string, cycleId: string) => {
if (!workspaceSlug || !activeProject) return; 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>) => { 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.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle);
if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module); if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module);
if (!createMore) handleClose(); if (!createMore) onClose();
setToastAlert({ setToastAlert({
type: "success", type: "success",
@ -324,7 +340,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <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 <Transition.Child
as={React.Fragment} as={React.Fragment}
enter="ease-out duration-300" enter="ease-out duration-300"
@ -354,7 +370,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
initialData={data ?? prePopulateData} initialData={data ?? prePopulateData}
createMore={createMore} createMore={createMore}
setCreateMore={setCreateMore} setCreateMore={setCreateMore}
handleClose={handleClose} handleClose={onClose}
projectId={activeProject ?? ""} projectId={activeProject ?? ""}
setActiveProject={setActiveProject} setActiveProject={setActiveProject}
status={data ? true : false} status={data ? true : false}

View File

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

View File

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

View File

@ -130,7 +130,7 @@ export const ProjectSidebarList: FC = () => {
data={projectToDelete} data={projectToDelete}
user={user} 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}> <DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="favorite-projects"> <Droppable droppableId="favorite-projects">
{(provided) => ( {(provided) => (

View File

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

View File

@ -7,7 +7,7 @@ import { PrimaryButton } from "components/ui";
type Props = { type Props = {
title: string; title: string;
description: React.ReactNode | string; description?: React.ReactNode;
image: any; image: any;
primaryButton?: { primaryButton?: {
icon?: any; icon?: any;
@ -34,7 +34,7 @@ export const EmptyState: React.FC<Props> = ({
<div className="text-center flex flex-col items-center w-full"> <div className="text-center flex flex-col items-center w-full">
<Image src={image} className="w-52 sm:w-60" alt={primaryButton?.text} /> <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> <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"> <div className="flex items-center gap-4">
{primaryButton && ( {primaryButton && (
<PrimaryButton className="flex items-center gap-1.5" onClick={primaryButton.onClick}> <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 { useRouter } from "next/router";
import Link from "next/link"; import Link from "next/link";
// next-themes
import { useTheme } from "next-themes";
// headless ui // headless ui
import { Menu, Transition } from "@headlessui/react"; import { Menu, Transition } from "@headlessui/react";
// next-themes
import { useTheme } from "next-themes";
// hooks // hooks
import useUser from "hooks/use-user"; import useUser from "hooks/use-user";
import useThemeHook from "hooks/use-theme"; import useThemeHook from "hooks/use-theme";
@ -91,7 +91,7 @@ export const WorkspaceSidebarDropdown = () => {
.then(() => { .then(() => {
mutateUser(undefined); mutateUser(undefined);
router.push("/"); router.push("/");
setTheme("dark"); setTheme("system");
}) })
.catch(() => .catch(() =>
setToastAlert({ setToastAlert({

View File

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

View File

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

View File

@ -60,6 +60,7 @@ const calculateShades = (hexValue: string): TShades => {
}; };
export const applyTheme = (palette: string, isDarkPalette: boolean) => { export const applyTheme = (palette: string, isDarkPalette: boolean) => {
const dom = document?.querySelector<HTMLElement>("[data-theme='custom']");
// palette: [bg, text, primary, sidebarBg, sidebarText] // palette: [bg, text, primary, sidebarBg, sidebarText]
const values: string[] = palette.split(","); const values: string[] = palette.split(",");
values.push(isDarkPalette ? "dark" : "light"); 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 sidebarBackgroundRgbValues = `${sidebarBackgroundShades[shade].r}, ${sidebarBackgroundShades[shade].g}, ${sidebarBackgroundShades[shade].b}`;
const sidebarTextRgbValues = `${sidebarTextShades[shade].r}, ${sidebarTextShades[shade].g}, ${sidebarTextShades[shade].b}`; const sidebarTextRgbValues = `${sidebarTextShades[shade].r}, ${sidebarTextShades[shade].g}, ${sidebarTextShades[shade].b}`;
document dom?.style.setProperty(`--color-background-${shade}`, bgRgbValues);
.querySelector<HTMLElement>("[data-theme='custom']") dom?.style.setProperty(`--color-text-${shade}`, textRgbValues);
?.style.setProperty(`--color-background-${shade}`, bgRgbValues); dom?.style.setProperty(`--color-primary-${shade}`, primaryRgbValues);
document dom?.style.setProperty(`--color-sidebar-background-${shade}`, sidebarBackgroundRgbValues);
.querySelector<HTMLElement>("[data-theme='custom']") dom?.style.setProperty(`--color-sidebar-text-${shade}`, sidebarTextRgbValues);
?.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);
if (i >= 100 && i <= 400) { if (i >= 100 && i <= 400) {
const borderShade = i === 100 ? 70 : i === 200 ? 80 : i === 300 ? 90 : 100; const borderShade = i === 100 ? 70 : i === 200 ? 80 : i === 300 ? 90 : 100;
document dom?.style.setProperty(
.querySelector<HTMLElement>("[data-theme='custom']") `--color-border-${shade}`,
?.style.setProperty( `${bgShades[borderShade].r}, ${bgShades[borderShade].g}, ${bgShades[borderShade].b}`
`--color-border-${shade}`, );
`${bgShades[borderShade].r}, ${bgShades[borderShade].g}, ${bgShades[borderShade].b}` dom?.style.setProperty(
); `--color-sidebar-border-${shade}`,
document `${sidebarBackgroundShades[borderShade].r}, ${sidebarBackgroundShades[borderShade].g}, ${sidebarBackgroundShades[borderShade].b}`
.querySelector<HTMLElement>("[data-theme='custom']") );
?.style.setProperty(
`--color-sidebar-border-${shade}`,
`${sidebarBackgroundShades[borderShade].r}, ${sidebarBackgroundShades[borderShade].g}, ${sidebarBackgroundShades[borderShade].b}`
);
} }
} }
document dom?.style.setProperty("--color-scheme", values[5]);
.querySelector<HTMLElement>("[data-theme='custom']") };
?.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( mutateCommentReactions(
(prevData) => (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( 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) => { const markNotificationArchivedStatus = async (notificationId: string) => {
if (!workspaceSlug) return; if (!workspaceSlug) return;
const isArchived = const isArchived =
@ -283,6 +303,7 @@ const useUserNotification = () => {
hasMore, hasMore,
isRefreshing, isRefreshing,
setFetchNotifications, setFetchNotifications,
markNotificationAsRead,
}; };
}; };

View File

@ -1,21 +1,43 @@
import useSWR from "swr"; import useSWR from "swr";
import { useRouter } from "next/router";
import Link from "next/link";
// services // services
import userService from "services/user.service"; import userService from "services/user.service";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import SettingsNavbar from "layouts/settings-navbar"; import SettingsNavbar from "layouts/settings-navbar";
// components // 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 // ui
import { Loader } from "components/ui"; import { Icon, Loader } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// fetch-keys // fetch-keys
import { USER_ACTIVITY } from "constants/fetch-keys"; import { USER_ACTIVITY } from "constants/fetch-keys";
// helper
import { timeAgo } from "helpers/date-time.helper";
const ProfileActivity = () => { const ProfileActivity = () => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { data: userActivity } = useSWR(USER_ACTIVITY, () => userService.getUserActivity()); 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 ( return (
<WorkspaceAuthorizationLayout <WorkspaceAuthorizationLayout
breadcrumbs={ breadcrumbs={
@ -34,17 +56,176 @@ const ProfileActivity = () => {
</div> </div>
<SettingsNavbar profilePage /> <SettingsNavbar profilePage />
</div> </div>
{userActivity ? ( {userActivity && userActivity.results.length > 0 && (
userActivity.results.length > 0 ? ( <div>
<Feeds activities={userActivity.results} /> <ul role="list" className="-mb-4">
) : null {userActivity.results.map((activityItem: any, activityIdx: number) => {
) : ( if (activityItem.field === "comment") {
<Loader className="space-y-5"> return (
<Loader.Item height="40px" /> <div key={activityItem.id} className="mt-2">
<Loader.Item height="40px" /> <div className="relative flex items-start space-x-3">
<Loader.Item height="40px" /> <div className="relative px-1">
<Loader.Item height="40px" /> {activityItem.field ? (
</Loader> 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> </div>
</WorkspaceAuthorizationLayout> </WorkspaceAuthorizationLayout>

View File

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

View File

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

View File

@ -15,8 +15,10 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
// components // components
import { IssueDetailsSidebar, IssueMainContent } from "components/issues"; import { IssueDetailsSidebar, IssueMainContent } from "components/issues";
// ui // ui
import { Loader } from "components/ui"; import { EmptyState, Loader } from "components/ui";
import { Breadcrumbs } from "components/breadcrumbs"; import { Breadcrumbs } from "components/breadcrumbs";
// images
import emptyIssue from "public/empty-state/issue.svg";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
@ -45,7 +47,11 @@ const IssueDetailsPage: NextPage = () => {
const { user } = useUserAuth(); 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 ? ISSUE_DETAILS(issueId as string) : null,
workspaceSlug && projectId && issueId workspaceSlug && projectId && issueId
? () => ? () =>
@ -125,7 +131,17 @@ const IssueDetailsPage: NextPage = () => {
</Breadcrumbs> </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="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"> <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} /> <IssueMainContent issueDetails={issueDetails} submitChanges={submitChanges} />

View File

@ -20,8 +20,10 @@ import { ExistingIssuesListModal, IssuesFilterView, IssuesView } from "component
import { ModuleDetailsSidebar } from "components/modules"; import { ModuleDetailsSidebar } from "components/modules";
import { AnalyticsProjectModal } from "components/analytics"; import { AnalyticsProjectModal } from "components/analytics";
// ui // ui
import { CustomMenu, SecondaryButton } from "components/ui"; import { CustomMenu, EmptyState, SecondaryButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// images
import emptyModule from "public/empty-state/module.svg";
// helpers // helpers
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
// types // types
@ -60,7 +62,7 @@ const SingleModule: React.FC = () => {
: null : null
); );
const { data: moduleDetails } = useSWR( const { data: moduleDetails, error } = useSWR(
moduleId ? MODULE_DETAILS(moduleId as string) : null, moduleId ? MODULE_DETAILS(moduleId as string) : null,
workspaceSlug && projectId workspaceSlug && projectId
? () => ? () =>
@ -162,22 +164,37 @@ const SingleModule: React.FC = () => {
</div> </div>
} }
> >
<AnalyticsProjectModal isOpen={analyticsModal} onClose={() => setAnalyticsModal(false)} /> {error ? (
<EmptyState
<div image={emptyModule}
className={`h-full flex flex-col ${moduleSidebar ? "mr-[24rem]" : ""} ${ title="Module does not exist"
analyticsModal ? "mr-[50%]" : "" description="The module you are looking for does not exist or has been deleted."
} duration-300`} primaryButton={{
> text: "View other modules",
<IssuesView openIssuesListModal={openIssuesListModal} /> onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/modules`),
</div> }}
/>
<ModuleDetailsSidebar ) : (
module={moduleDetails} <>
isOpen={moduleSidebar} <AnalyticsProjectModal
moduleIssues={moduleIssues} isOpen={analyticsModal}
user={user} 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> </ProjectAuthorizationWrapper>
</IssueViewContextProvider> </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"; import { useRouter } from "next/router";
@ -28,7 +28,16 @@ import { CreateLabelModal } from "components/labels";
import { CreateBlock } from "components/pages/create-block"; import { CreateBlock } from "components/pages/create-block";
// ui // ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; 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 // icons
import { import {
ArrowLeftIcon, ArrowLeftIcon,
@ -40,7 +49,7 @@ import {
XMarkIcon, XMarkIcon,
ChevronDownIcon, ChevronDownIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { ColorPalletteIcon, ClipboardIcon } from "components/icons"; import { ColorPalletteIcon } from "components/icons";
// helpers // helpers
import { render24HourFormatTime, renderShortDate } from "helpers/date-time.helper"; import { render24HourFormatTime, renderShortDate } from "helpers/date-time.helper";
import { copyTextToClipboard, truncateText } from "helpers/string.helper"; import { copyTextToClipboard, truncateText } from "helpers/string.helper";
@ -82,7 +91,7 @@ const SinglePage: NextPage = () => {
: null : null
); );
const { data: pageDetails } = useSWR( const { data: pageDetails, error } = useSWR(
workspaceSlug && projectId && pageId ? PAGE_DETAILS(pageId as string) : null, workspaceSlug && projectId && pageId ? PAGE_DETAILS(pageId as string) : null,
workspaceSlug && projectId workspaceSlug && projectId
? () => ? () =>
@ -267,13 +276,6 @@ const SinglePage: NextPage = () => {
); );
}; };
const handleNewBlock = useCallback(() => {
setCreateBlockForm(true);
scrollToRef.current?.scrollIntoView({
behavior: "smooth",
});
}, [setCreateBlockForm, scrollToRef]);
const handleShowBlockToggle = async () => { const handleShowBlockToggle = async () => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
@ -311,22 +313,21 @@ const SinglePage: NextPage = () => {
}); });
}; };
const options = const options = labels?.map((label) => ({
labels?.map((label) => ({ value: label.id,
value: label.id, query: label.name,
query: label.name, content: (
content: ( <div className="flex items-center gap-2">
<div className="flex items-center gap-2"> <span
<span className="h-2 w-2 flex-shrink-0 rounded-full"
className="h-2 w-2 flex-shrink-0 rounded-full" style={{
style={{ backgroundColor: label.color && label.color !== "" ? label.color : "#000000",
backgroundColor: label.color && label.color !== "" ? label.color : "#000000", }}
}} />
/> {label.name}
{label.name} </div>
</div> ),
), }));
})) ?? [];
useEffect(() => { useEffect(() => {
if (!pageDetails) return; if (!pageDetails) return;
@ -346,11 +347,21 @@ const SinglePage: NextPage = () => {
breadcrumbs={ breadcrumbs={
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} /> <BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project",32)} Pages`} /> <BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project", 32)} Pages`} />
</Breadcrumbs> </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="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="h-full w-full overflow-y-auto">
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">

View File

@ -12,11 +12,13 @@ import { IssueViewContextProvider } from "contexts/issue-view.context";
// components // components
import { IssuesFilterView, IssuesView } from "components/core"; import { IssuesFilterView, IssuesView } from "components/core";
// ui // ui
import { CustomMenu, PrimaryButton } from "components/ui"; import { CustomMenu, EmptyState, PrimaryButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons // icons
import { PlusIcon } from "@heroicons/react/24/outline"; import { PlusIcon } from "@heroicons/react/24/outline";
import { StackedLayersIcon } from "components/icons"; import { StackedLayersIcon } from "components/icons";
// images
import emptyView from "public/empty-state/view.svg";
// helpers // helpers
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
// fetch-keys // fetch-keys
@ -40,7 +42,7 @@ const SingleView: React.FC = () => {
: null : null
); );
const { data: viewDetails } = useSWR( const { data: viewDetails, error } = useSWR(
workspaceSlug && projectId && viewId ? VIEW_DETAILS(viewId as string) : null, workspaceSlug && projectId && viewId ? VIEW_DETAILS(viewId as string) : null,
workspaceSlug && projectId && viewId workspaceSlug && projectId && viewId
? () => ? () =>
@ -101,9 +103,21 @@ const SingleView: React.FC = () => {
</div> </div>
} }
> >
<div className="h-full w-full flex flex-col"> {error ? (
<IssuesView /> <EmptyState
</div> 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> </ProjectAuthorizationWrapper>
</IssueViewContextProvider> </IssueViewContextProvider>
); );

View File

@ -45,7 +45,7 @@ Router.events.on("routeChangeComplete", NProgress.done);
function MyApp({ Component, pageProps }: AppProps) { function MyApp({ Component, pageProps }: AppProps) {
return ( return (
// <UserProvider> // <UserProvider>
<ThemeProvider themes={THEMES} defaultTheme="dark"> <ThemeProvider themes={THEMES} defaultTheme="system">
<ToastContextProvider> <ToastContextProvider>
<ThemeContextProvider> <ThemeContextProvider>
<CrispWithNoSSR /> <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"; import Image from "next/image";
@ -22,6 +22,8 @@ import {
import { Spinner } from "components/ui"; import { Spinner } from "components/ui";
// images // images
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
import { useTheme } from "next-themes";
import { ICurrentUserResponse, IUser } from "types";
// types // types
type EmailPasswordFormValues = { type EmailPasswordFormValues = {
email: string; email: string;
@ -34,6 +36,12 @@ const HomePage: NextPage = () => {
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const { setTheme } = useTheme();
const changeTheme = (user: IUser) => {
setTheme(user.theme.theme ?? "system");
};
const handleGoogleSignIn = async ({ clientId, credential }: any) => { const handleGoogleSignIn = async ({ clientId, credential }: any) => {
try { try {
if (clientId && credential) { if (clientId && credential) {
@ -43,7 +51,10 @@ const HomePage: NextPage = () => {
clientId, clientId,
}; };
const response = await authenticationService.socialAuth(socialAuthPayload); const response = await authenticationService.socialAuth(socialAuthPayload);
if (response && response?.user) mutateUser(); if (response && response?.user) {
mutateUser();
changeTheme(response.user);
}
} else { } else {
throw Error("Cant find credentials"); throw Error("Cant find credentials");
} }
@ -66,7 +77,10 @@ const HomePage: NextPage = () => {
clientId: process.env.NEXT_PUBLIC_GITHUB_ID, clientId: process.env.NEXT_PUBLIC_GITHUB_ID,
}; };
const response = await authenticationService.socialAuth(socialAuthPayload); const response = await authenticationService.socialAuth(socialAuthPayload);
if (response && response?.user) mutateUser(); if (response && response?.user) {
mutateUser();
changeTheme(response.user);
}
} else { } else {
throw Error("Cant find credentials"); throw Error("Cant find credentials");
} }
@ -85,7 +99,10 @@ const HomePage: NextPage = () => {
.emailLogin(formData) .emailLogin(formData)
.then((response) => { .then((response) => {
try { try {
if (response) mutateUser(); if (response) {
mutateUser();
changeTheme(response.user);
}
} catch (err: any) { } catch (err: any) {
setToastAlert({ setToastAlert({
type: "error", type: "error",
@ -109,7 +126,10 @@ const HomePage: NextPage = () => {
const handleEmailCodeSignIn = async (response: any) => { const handleEmailCodeSignIn = async (response: any) => {
try { try {
if (response) mutateUser(); if (response) {
mutateUser();
changeTheme(response.user);
}
} catch (err: any) { } catch (err: any) {
setToastAlert({ setToastAlert({
type: "error", type: "error",
@ -120,6 +140,10 @@ const HomePage: NextPage = () => {
} }
}; };
useEffect(() => {
setTheme("system");
}, [setTheme]);
return ( return (
<DefaultLayout> <DefaultLayout>
{isLoading ? ( {isLoading ? (

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
// services // services
import APIService from "services/api.service"; import APIService from "services/api.service";
import { ICurrentUserResponse } from "types";
const { NEXT_PUBLIC_API_BASE_URL } = process.env; 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: {} }) return this.post("/api/social-auth/", data, { headers: {} })
.then((response) => { .then((response) => {
this.setAccessToken(response?.data?.access_token); this.setAccessToken(response?.data?.access_token);

View File

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