merge conflicts resolved

This commit is contained in:
sriramveeraghanta 2023-08-16 13:00:58 +05:30
commit 9003c58d89
360 changed files with 13916 additions and 7344 deletions

View file

@ -22,7 +22,6 @@ const MyIssuesPage: NextPage = () => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { projects } = useProjects();
const { user } = useUser();
const { filters, setFilters } = useMyIssuesFilters(workspaceSlug?.toString());
@ -30,23 +29,37 @@ const MyIssuesPage: NextPage = () => {
const tabsList = [
{
key: "assigned",
label: "Assigned to me",
label: "Assigned",
selected: (filters?.assignees ?? []).length > 0,
onClick: () => {
setFilters({
assignees: [user?.id ?? ""],
created_by: null,
subscriber: null,
});
},
},
{
key: "created",
label: "Created by me",
label: "Created",
selected: (filters?.created_by ?? []).length > 0,
onClick: () => {
setFilters({
created_by: [user?.id ?? ""],
assignees: null,
created_by: [user?.id ?? ""],
subscriber: null,
});
},
},
{
key: "subscribed",
label: "Subscribed",
selected: (filters?.subscriber ?? []).length > 0,
onClick: () => {
setFilters({
assignees: null,
created_by: null,
subscriber: [user?.id ?? ""],
});
},
},
@ -55,7 +68,7 @@ const MyIssuesPage: NextPage = () => {
useEffect(() => {
if (!filters || !user) return;
if (!filters.assignees && !filters.created_by) {
if (!filters.assignees && !filters.created_by && !filters.subscriber) {
setFilters({
assignees: [user.id],
});

View file

@ -10,7 +10,7 @@ import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import SettingsNavbar from "layouts/settings-navbar";
// components
import { ActivityIcon, ActivityMessage } from "components/core";
import RemirrorRichTextEditor from "components/rich-text-editor";
import Tiptap, { ITiptapRichTextEditor } from "components/tiptap";
// icons
import { ArrowTopRightOnSquareIcon, ChatBubbleLeftEllipsisIcon } from "@heroicons/react/24/outline";
// ui
@ -73,7 +73,7 @@ const ProfileActivity = () => {
activityItem.actor_detail.avatar !== "" ? (
<img
src={activityItem.actor_detail.avatar}
alt={activityItem.actor_detail.first_name}
alt={activityItem.actor_detail.display_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"
@ -82,7 +82,7 @@ const ProfileActivity = () => {
<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)}
{activityItem.actor_detail.display_name?.charAt(0)}
</div>
)}
@ -96,25 +96,25 @@ const ProfileActivity = () => {
<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}
? activityItem.actor_detail.first_name + " Bot"
: activityItem.actor_detail.display_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
<Tiptap
value={
activityItem.new_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"
noBorder
borderOnFocus={false}
editable={false}
/>
</div>
</div>
@ -176,7 +176,7 @@ const ProfileActivity = () => {
activityItem.actor_detail.avatar !== "" ? (
<img
src={activityItem.actor_detail.avatar}
alt={activityItem.actor_detail.first_name}
alt={activityItem.actor_detail.display_name}
height={24}
width={24}
className="rounded-full"
@ -185,7 +185,7 @@ const ProfileActivity = () => {
<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)}
{activityItem.actor_detail.display_name?.charAt(0)}
</div>
)}
</div>
@ -206,8 +206,7 @@ const ProfileActivity = () => {
href={`/${workspaceSlug}/profile/${activityItem.actor_detail.id}`}
>
<a className="text-gray font-medium">
{activityItem.actor_detail.first_name}{" "}
{activityItem.actor_detail.last_name}
{activityItem.actor_detail.display_name}
</a>
</Link>
)}{" "}

View file

@ -12,7 +12,7 @@ import useToast from "hooks/use-toast";
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import SettingsNavbar from "layouts/settings-navbar";
// components
import { ImageUploadModal } from "components/core";
import { ImagePickerPopover, ImageUploadModal } from "components/core";
// ui
import { CustomSelect, DangerButton, Input, SecondaryButton, Spinner } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
@ -26,6 +26,7 @@ import { USER_ROLES } from "constants/workspace";
const defaultValues: Partial<IUser> = {
avatar: "",
cover_image: "",
first_name: "",
last_name: "",
email: "",
@ -68,13 +69,15 @@ const Profile: NextPage = () => {
first_name: formData.first_name,
last_name: formData.last_name,
avatar: formData.avatar,
cover_image: formData.cover_image,
role: formData.role,
display_name: formData.display_name,
};
await userService
.updateUser(payload)
.then((res) => {
mutateUser((prevData) => {
mutateUser((prevData: any) => {
if (!prevData) return prevData;
return { ...prevData, ...res };
@ -109,7 +112,7 @@ const Profile: NextPage = () => {
title: "Success!",
message: "Profile picture removed successfully.",
});
mutateUser((prevData) => {
mutateUser((prevData: any) => {
if (!prevData) return prevData;
return { ...prevData, avatar: "" };
}, false);
@ -176,7 +179,7 @@ const Profile: NextPage = () => {
src={watch("avatar")}
className="absolute top-0 left-0 h-full w-full object-cover rounded-md"
onClick={() => setIsImageUploadModalOpen(true)}
alt={myProfile.first_name}
alt={myProfile.display_name}
/>
</div>
)}
@ -203,11 +206,42 @@ const Profile: NextPage = () => {
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold text-custom-text-100">Full Name</h4>
<h4 className="text-lg font-semibold">Cover Photo</h4>
<p className="text-sm text-custom-text-200">
This name will be reflected on all the projects you are working on.
Select your cover photo from the given library.
</p>
</div>
<div className="col-span-12 sm:col-span-6">
<div className="h-32 w-full rounded border border-custom-border-200 p-1">
<div className="relative h-full w-full rounded">
<img
src={
watch("cover_image") ??
"https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"
}
className="absolute top-0 left-0 h-full w-full object-cover rounded"
alt={myProfile?.name ?? "Cover image"}
/>
<div className="absolute bottom-0 flex w-full justify-end">
<ImagePickerPopover
label={"Change cover"}
onChange={(imageUrl) => {
setValue("cover_image", imageUrl);
}}
value={
watch("cover_image") ??
"https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"
}
/>
</div>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold text-custom-text-100">Full Name</h4>
</div>
<div className="col-span-12 flex items-center gap-2 sm:col-span-6">
<Input
name="first_name"
@ -227,6 +261,43 @@ const Profile: NextPage = () => {
/>
</div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold text-custom-text-100">Display Name</h4>
<p className="text-sm text-custom-text-200">
This could be your first name, or a nickname however you{"'"}d like people to
refer to you in Plane.
</p>
</div>
<div className="col-span-12 sm:col-span-6">
<Input
id="display_name"
name="display_name"
autoComplete="off"
register={register}
error={errors.display_name}
className="w-full"
placeholder="Enter your display name"
validations={{
required: "Display name is required.",
validate: (value) => {
if (value.trim().length < 1) return "Display name can't be empty.";
if (value.split(" ").length > 1)
return "Display name can't have two consecutive spaces.";
if (value.replace(/\s/g, "").length < 1)
return "Display name must be at least 1 characters long.";
if (value.replace(/\s/g, "").length > 20)
return "Display name must be less than 20 characters long.";
return true;
},
}}
/>
</div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold text-custom-text-100">Email</h4>

View file

@ -1,7 +1,4 @@
import { useEffect, useState } from "react";
// next-themes
import { useTheme } from "next-themes";
// hooks
import useUserAuth from "hooks/use-user-auth";
// layouts
@ -14,37 +11,47 @@ import { Spinner } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// types
import { ICustomTheme } from "types";
// mobx react lite
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// next themes
import { useTheme } from "next-themes";
const ProfilePreferences = () => {
const [customThemeSelectorOptions, setCustomThemeSelectorOptions] = useState(false);
const [preLoadedData, setPreLoadedData] = useState<ICustomTheme | null>(null);
const { theme } = useTheme();
const ProfilePreferences = observer(() => {
const { user: myProfile } = useUserAuth();
const store: any = useMobxStore();
const { theme } = useTheme();
console.log("store", store?.theme?.theme);
console.log("theme", theme);
const [customThemeSelectorOptions, setCustomThemeSelectorOptions] = useState(false);
const [preLoadedData, setPreLoadedData] = useState<ICustomTheme | null>(null);
useEffect(() => {
if (theme === "custom") {
if (myProfile?.theme.palette)
if (store?.user && store?.theme?.theme === "custom") {
const currentTheme = store?.user?.currentUserSettings?.theme;
if (currentTheme.palette)
setPreLoadedData({
background: myProfile.theme.background !== "" ? myProfile.theme.background : "#0d101b",
text: myProfile.theme.text !== "" ? myProfile.theme.text : "#c5c5c5",
primary: myProfile.theme.primary !== "" ? myProfile.theme.primary : "#3f76ff",
background: currentTheme.background !== "" ? currentTheme.background : "#0d101b",
text: currentTheme.text !== "" ? currentTheme.text : "#c5c5c5",
primary: currentTheme.primary !== "" ? currentTheme.primary : "#3f76ff",
sidebarBackground:
myProfile.theme.sidebarBackground !== ""
? myProfile.theme.sidebarBackground
: "#0d101b",
sidebarText: myProfile.theme.sidebarText !== "" ? myProfile.theme.sidebarText : "#c5c5c5",
currentTheme.sidebarBackground !== "" ? currentTheme.sidebarBackground : "#0d101b",
sidebarText: currentTheme.sidebarText !== "" ? currentTheme.sidebarText : "#c5c5c5",
darkPalette: false,
palette:
myProfile.theme.palette !== ",,,,"
? myProfile.theme.palette
currentTheme.palette !== ",,,,"
? currentTheme.palette
: "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
theme: "custom",
});
if (!customThemeSelectorOptions) setCustomThemeSelectorOptions(true);
setCustomThemeSelectorOptions((prevData) => true);
}
}, [myProfile, theme, customThemeSelectorOptions]);
}, [store, store?.theme?.theme]);
return (
<WorkspaceAuthorizationLayout
@ -91,6 +98,6 @@ const ProfilePreferences = () => {
)}
</WorkspaceAuthorizationLayout>
);
};
});
export default ProfilePreferences;

View file

@ -28,22 +28,24 @@ import { PROJECT_ISSUES_ACTIVITY, ISSUE_DETAILS } from "constants/fetch-keys";
import { truncateText } from "helpers/string.helper";
const defaultValues = {
name: "",
assignees_list: [],
description: "",
description_html: "",
estimate_point: null,
state: "",
assignees_list: [],
priority: "low",
target_date: new Date().toString(),
issue_cycle: null,
issue_module: null,
labels_list: [],
name: "",
priority: "low",
start_date: null,
state: "",
target_date: null,
};
const IssueDetailsPage: NextPage = () => {
const router = useRouter();
const { workspaceSlug, projectId, issueId } = router.query;
// console.log(workspaceSlug, "workspaceSlug")
const { user } = useUserAuth();

View file

@ -63,7 +63,7 @@ const ProjectIssues: NextPage = () => {
<IssuesFilterView />
<SecondaryButton
onClick={() => setAnalyticsModal(true)}
className="!py-1.5 rounded-md font-normal text-custom-sidebar-text-200 border-custom-sidebar-border-200 hover:text-custom-text-100 hover:bg-custom-sidebar-background-90"
className="!py-1.5 rounded-md font-normal text-custom-sidebar-text-200 border-custom-border-200 hover:text-custom-text-100 hover:bg-custom-sidebar-background-90"
outline
>
Analytics
@ -72,7 +72,7 @@ const ProjectIssues: NextPage = () => {
<Link href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxList?.[0]?.id}`}>
<a>
<SecondaryButton
className="relative !py-1.5 rounded-md font-normal text-custom-sidebar-text-200 border-custom-sidebar-border-200 hover:text-custom-text-100 hover:bg-custom-sidebar-background-90"
className="relative !py-1.5 rounded-md font-normal text-custom-sidebar-text-200 border-custom-border-200 hover:text-custom-text-100 hover:bg-custom-sidebar-background-90"
outline
>
<span>Inbox</span>
@ -97,6 +97,7 @@ const ProjectIssues: NextPage = () => {
</PrimaryButton>
</div>
}
bg="secondary"
>
<AnalyticsProjectModal isOpen={analyticsModal} onClose={() => setAnalyticsModal(false)} />
<div className="h-full w-full flex flex-col">

View file

@ -50,7 +50,7 @@ const ProjectModules: NextPage = () => {
: null
);
const { data: modules } = useSWR<IModule[]>(
const { data: modules, mutate: mutateModules } = useSWR(
workspaceSlug && projectId ? MODULE_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => modulesService.getModules(workspaceSlug as string, projectId as string)
@ -139,7 +139,9 @@ const ProjectModules: NextPage = () => {
</div>
</div>
)}
{modulesView === "gantt_chart" && <ModulesListGanttChartView modules={modules} />}
{modulesView === "gantt_chart" && (
<ModulesListGanttChartView modules={modules} mutateModules={mutateModules} />
)}
</div>
) : (
<EmptyState

View file

@ -629,17 +629,19 @@ const SinglePage: NextPage = () => {
ref={provided.innerRef}
{...provided.droppableProps}
>
{pageBlocks.map((block, index) => (
<SinglePageBlock
key={block.id}
block={block}
projectDetails={projectDetails}
showBlockDetails={showBlock}
index={index}
user={user}
/>
))}
{provided.placeholder}
<>
{pageBlocks.map((block, index) => (
<SinglePageBlock
key={block.id}
block={block}
projectDetails={projectDetails}
showBlockDetails={showBlock}
index={index}
user={user}
/>
))}
{provided.placeholder}
</>
</div>
)}
</StrictModeDroppable>

View file

@ -131,7 +131,7 @@ const ControlSettings: NextPage = () => {
{...field}
label={
people?.find((person) => person.member.id === field.value)?.member
.first_name ?? <span className="text-custom-text-200">Select lead</span>
.display_name ?? <span className="text-custom-text-200">Select lead</span>
}
width="w-full"
input
@ -153,14 +153,10 @@ const ControlSettings: NextPage = () => {
</div>
) : (
<div className="grid h-4 w-4 flex-shrink-0 place-items-center rounded-full bg-gray-700 capitalize text-white">
{person.member.first_name && person.member.first_name !== ""
? person.member.first_name.charAt(0)
: person.member.email.charAt(0)}
{person.member.display_name?.charAt(0)}
</div>
)}
{person.member.first_name !== ""
? person.member.first_name
: person.member.email}
{person.member.display_name}
</div>
</CustomSelect.Option>
))}
@ -190,7 +186,7 @@ const ControlSettings: NextPage = () => {
<CustomSelect
{...field}
label={
people?.find((p) => p.member.id === field.value)?.member.first_name ?? (
people?.find((p) => p.member.id === field.value)?.member.display_name ?? (
<span className="text-custom-text-200">Select default assignee</span>
)
}
@ -214,14 +210,10 @@ const ControlSettings: NextPage = () => {
</div>
) : (
<div className="grid h-4 w-4 flex-shrink-0 place-items-center rounded-full bg-gray-700 capitalize text-white">
{person.member.first_name && person.member.first_name !== ""
? person.member.first_name.charAt(0)
: person.member.email.charAt(0)}
{person.member.display_name?.charAt(0)}
</div>
)}
{person.member.first_name !== ""
? person.member.first_name
: person.member.email}
{person.member.display_name}
</div>
</CustomSelect.Option>
))}

View file

@ -34,7 +34,7 @@ import { truncateText } from "helpers/string.helper";
import { IProject, IWorkspace } from "types";
import type { NextPage } from "next";
// fetch-keys
import { PROJECTS_LIST, PROJECT_DETAILS } from "constants/fetch-keys";
import { PROJECTS_LIST, PROJECT_DETAILS, USER_PROJECT_VIEW } from "constants/fetch-keys";
// constants
import { NETWORK_CHOICES } from "constants/project";
@ -62,6 +62,13 @@ const GeneralSettings: NextPage = () => {
: null
);
const { data: memberDetails, error } = useSWR(
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId.toString()) : null,
workspaceSlug && projectId
? () => projectService.projectMemberMe(workspaceSlug.toString(), projectId.toString())
: null
);
const {
register,
handleSubmit,
@ -157,6 +164,8 @@ const GeneralSettings: NextPage = () => {
const currentNetwork = NETWORK_CHOICES.find((n) => n.key === projectDetails?.network);
const isAdmin = memberDetails?.role === 20;
return (
<ProjectAuthorizationWrapper
breadcrumbs={
@ -355,7 +364,7 @@ const GeneralSettings: NextPage = () => {
</div>
<div className="sm:text-right">
{projectDetails ? (
<SecondaryButton type="submit" loading={isSubmitting}>
<SecondaryButton type="submit" loading={isSubmitting} disabled={!isAdmin}>
{isSubmitting ? "Updating Project..." : "Update Project"}
</SecondaryButton>
) : (
@ -364,32 +373,34 @@ const GeneralSettings: NextPage = () => {
</Loader>
)}
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold">Danger Zone</h4>
<p className="text-sm text-custom-text-200">
The danger zone of the project delete page is a critical area that requires careful
consideration and attention. When deleting a project, all of the data and resources
within that project will be permanently removed and cannot be recovered.
</p>
{memberDetails?.role === 20 && (
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold">Danger Zone</h4>
<p className="text-sm text-custom-text-200">
The danger zone of the project delete page is a critical area that requires
careful consideration and attention. When deleting a project, all of the data and
resources within that project will be permanently removed and cannot be recovered.
</p>
</div>
<div className="col-span-12 sm:col-span-6">
{projectDetails ? (
<div>
<DangerButton
onClick={() => setSelectedProject(projectDetails.id ?? null)}
outline
>
Delete Project
</DangerButton>
</div>
) : (
<Loader className="mt-2 w-full">
<Loader.Item height="46px" width="100px" />
</Loader>
)}
</div>
</div>
<div className="col-span-12 sm:col-span-6">
{projectDetails ? (
<div>
<DangerButton
onClick={() => setSelectedProject(projectDetails.id ?? null)}
outline
>
Delete Project
</DangerButton>
</div>
) : (
<Loader className="mt-2 w-full">
<Loader.Item height="46px" width="100px" />
</Loader>
)}
</div>
</div>
)}
</div>
</form>
</ProjectAuthorizationWrapper>

View file

@ -133,6 +133,11 @@ const LabelsSettings: NextPage = () => {
setLabelForm={setLabelForm}
isUpdating={isUpdating}
labelToUpdate={labelToUpdate}
onClose={() => {
setLabelForm(false);
setIsUpdating(false);
setLabelToUpdate(null);
}}
ref={scrollToRef}
/>
)}

View file

@ -10,8 +10,9 @@ import projectService from "services/project.service";
import workspaceService from "services/workspace.service";
// hooks
import useToast from "hooks/use-toast";
import useProjectDetails from "hooks/use-project-details";
import useUser from "hooks/use-user";
import useProjectMembers from "hooks/use-project-members";
import useProjectDetails from "hooks/use-project-details";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
// components
@ -26,7 +27,11 @@ import { PlusIcon, XMarkIcon } from "@heroicons/react/24/outline";
// types
import type { NextPage } from "next";
// fetch-keys
import { PROJECT_INVITATIONS, PROJECT_MEMBERS, WORKSPACE_DETAILS } from "constants/fetch-keys";
import {
PROJECT_INVITATIONS_WITH_EMAIL,
PROJECT_MEMBERS_WITH_EMAIL,
WORKSPACE_DETAILS,
} from "constants/fetch-keys";
// constants
import { ROLE } from "constants/workspace";
// helper
@ -44,6 +49,11 @@ const MembersSettings: NextPage = () => {
const { user } = useUser();
const { projectDetails } = useProjectDetails();
const { isOwner } = useProjectMembers(
workspaceSlug?.toString(),
projectId?.toString(),
Boolean(workspaceSlug && projectId)
);
const { data: activeWorkspace } = useSWR(
workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null,
@ -51,16 +61,21 @@ const MembersSettings: NextPage = () => {
);
const { data: projectMembers, mutate: mutateMembers } = useSWR(
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
? PROJECT_MEMBERS_WITH_EMAIL(workspaceSlug.toString(), projectId.toString())
: null,
workspaceSlug && projectId
? () => projectService.projectMembersWithEmail(workspaceSlug as string, projectId as string)
: null
);
const { data: projectInvitations, mutate: mutateInvitations } = useSWR(
workspaceSlug && projectId ? PROJECT_INVITATIONS : null,
workspaceSlug && projectId
? () => projectService.projectInvitations(workspaceSlug as string, projectId as string)
? PROJECT_INVITATIONS_WITH_EMAIL(workspaceSlug.toString(), projectId.toString())
: null,
workspaceSlug && projectId
? () =>
projectService.projectInvitationsWithEmail(workspaceSlug as string, projectId as string)
: null
);
@ -72,6 +87,7 @@ const MembersSettings: NextPage = () => {
first_name: item.member?.first_name,
last_name: item.member?.last_name,
email: item.member?.email,
display_name: item.member?.display_name,
role: item.role,
status: true,
member: true,
@ -83,6 +99,7 @@ const MembersSettings: NextPage = () => {
first_name: item.first_name ?? item.email,
last_name: item.last_name ?? "",
email: item.email,
display_name: item.email,
role: item.role,
status: item.accepted,
member: false,
@ -122,7 +139,7 @@ const MembersSettings: NextPage = () => {
selectedRemoveMember
);
mutateMembers(
(prevData) => prevData?.filter((item: any) => item.id !== selectedRemoveMember),
(prevData: any) => prevData?.filter((item: any) => item.id !== selectedRemoveMember),
false
);
}
@ -133,7 +150,8 @@ const MembersSettings: NextPage = () => {
selectedInviteRemoveMember
);
mutateInvitations(
(prevData) => prevData?.filter((item: any) => item.id !== selectedInviteRemoveMember),
(prevData: any) =>
prevData?.filter((item: any) => item.id !== selectedInviteRemoveMember),
false
);
}
@ -177,32 +195,41 @@ const MembersSettings: NextPage = () => {
? members.map((member) => (
<div key={member.id} className="flex items-center justify-between py-6">
<div className="flex items-center gap-x-6 gap-y-2">
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg bg-gray-700 p-4 capitalize text-white">
{member.avatar && member.avatar !== "" ? (
{member.avatar && member.avatar !== "" ? (
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize text-white">
<img
src={member.avatar}
alt={member.first_name}
alt={member.display_name}
className="absolute top-0 left-0 h-full w-full object-cover rounded-lg"
/>
) : member.first_name !== "" ? (
member.first_name.charAt(0)
) : (
member.email.charAt(0)
)}
</div>
</div>
) : member.display_name || member.email ? (
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg bg-gray-700 p-4 capitalize text-white">
{(member.display_name || member.email)?.charAt(0)}
</div>
) : (
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg bg-gray-700 p-4 capitalize text-white">
?
</div>
)}
<div>
{member.member ? (
<Link href={`/${workspaceSlug}/profile/${member.memberId}`}>
<a className="text-sm">
{member.first_name} {member.last_name}
<span>
{member.first_name} {member.last_name}
</span>
<span className="text-custom-text-300 text-sm ml-2">
({member.display_name})
</span>
</a>
</Link>
) : (
<h4 className="text-sm">
{member.first_name} {member.last_name}
</h4>
<h4 className="text-sm">{member.display_name || member.email}</h4>
)}
{isOwner && (
<p className="mt-0.5 text-xs text-custom-text-200">{member.email}</p>
)}
<p className="mt-0.5 text-xs text-custom-text-200">{member.email}</p>
</div>
</div>
<div className="flex items-center gap-2 text-xs">

View file

@ -16,7 +16,7 @@ import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { JoinProjectModal } from "components/project/join-project-modal";
import { DeleteProjectModal, SingleProjectCard } from "components/project";
// ui
import { EmptyState, Loader, PrimaryButton } from "components/ui";
import { EmptyState, Icon, Loader, PrimaryButton } from "components/ui";
import { Breadcrumbs, BreadcrumbItem } from "components/breadcrumbs";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
@ -34,6 +34,8 @@ const ProjectsPage: NextPage = () => {
const router = useRouter();
const { workspaceSlug } = router.query;
const [query, setQuery] = useState("");
const { user } = useUserAuth();
// context data
const { activeWorkspace } = useWorkspaces();
@ -42,6 +44,15 @@ const ProjectsPage: NextPage = () => {
const [deleteProject, setDeleteProject] = useState<string | null>(null);
const [selectedProjectToJoin, setSelectedProjectToJoin] = useState<string | null>(null);
const filteredProjectList =
query === ""
? projects
: projects?.filter(
(project) =>
project.name.toLowerCase().includes(query.toLowerCase()) ||
project.identifier.toLowerCase().includes(query.toLowerCase())
);
return (
<WorkspaceAuthorizationLayout
breadcrumbs={
@ -53,16 +64,28 @@ const ProjectsPage: NextPage = () => {
</Breadcrumbs>
}
right={
<PrimaryButton
className="flex items-center gap-2"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "p" });
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-4 w-4" />
Add Project
</PrimaryButton>
<div className="flex items-center gap-3">
<div className="flex w-full gap-1 items-center justify-start rounded-md px-2 py-1.5 border border-custom-border-300 bg-custom-background-90">
<Icon iconName="search" className="!text-xl !leading-5 !text-custom-sidebar-text-400" />
<input
className="w-full border-none bg-transparent text-xs text-custom-text-200 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
/>
</div>
<PrimaryButton
className="flex items-center gap-2 flex-shrink-0"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "p" });
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-4 w-4" />
Add Project
</PrimaryButton>
</div>
}
>
<JoinProjectModal
@ -91,12 +114,12 @@ const ProjectsPage: NextPage = () => {
data={projects?.find((item) => item.id === deleteProject) ?? null}
user={user}
/>
{projects ? (
{filteredProjectList ? (
<div className="h-full w-full overflow-hidden">
{projects.length > 0 ? (
{filteredProjectList.length > 0 ? (
<div className="h-full p-8 overflow-y-auto">
<div className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
{projects.map((project) => (
{filteredProjectList.map((project) => (
<SingleProjectCard
key={project.id}
project={project}

View file

@ -8,7 +8,7 @@ import workspaceService from "services/workspace.service";
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { SettingsHeader } from "components/workspace";
// components
import IntegrationGuide from "components/integration/guide";
import ExportGuide from "components/exporter/guide";
import { IntegrationAndImportExportBanner } from "components/ui";
// ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
@ -37,14 +37,14 @@ const ImportExport: NextPage = () => {
link={`/${workspaceSlug}`}
linkTruncate
/>
<BreadcrumbItem title="Import/ Export Settings" unshrinkTitle />
<BreadcrumbItem title="Export Settings" unshrinkTitle />
</Breadcrumbs>
}
>
<div className="p-8 space-y-4">
<SettingsHeader />
<IntegrationAndImportExportBanner bannerName="Import/ Export" />
<IntegrationGuide />
<IntegrationAndImportExportBanner bannerName="Export" />
<ExportGuide />
</div>
</WorkspaceAuthorizationLayout>
);

View file

@ -0,0 +1,58 @@
import { useRouter } from "next/router";
import useSWR from "swr";
// services
import workspaceService from "services/workspace.service";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { SettingsHeader } from "components/workspace";
// components
import IntegrationGuide from "components/integration/guide";
import { IntegrationAndImportExportBanner } from "components/ui";
// ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// types
import type { NextPage } from "next";
// fetch-keys
import { WORKSPACE_DETAILS } from "constants/fetch-keys";
// helper
import { truncateText } from "helpers/string.helper";
const ImportExport: NextPage = () => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { data: activeWorkspace } = useSWR(
workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null,
() => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null)
);
return (
<WorkspaceAuthorizationLayout
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem
title={`${truncateText(activeWorkspace?.name ?? "Workspace", 32)}`}
link={`/${workspaceSlug}`}
linkTruncate
/>
<BreadcrumbItem title="Import/ Export Settings" unshrinkTitle />
</Breadcrumbs>
}
>
<div className="p-8 space-y-4">
<SettingsHeader />
<IntegrationAndImportExportBanner
bannerName="Import/ Export"
description="Integrations and importers are only available on the cloud version. We plan to open-source
our SDKs in the near future so that the community can request or contribute integrations as
needed."
/>
<IntegrationGuide />
</div>
</WorkspaceAuthorizationLayout>
);
};
export default ImportExport;

View file

@ -28,7 +28,7 @@ import { copyTextToClipboard, truncateText } from "helpers/string.helper";
import type { IWorkspace } from "types";
import type { NextPage } from "next";
// fetch-keys
import { WORKSPACE_DETAILS, USER_WORKSPACES } from "constants/fetch-keys";
import { WORKSPACE_DETAILS, USER_WORKSPACES, WORKSPACE_MEMBERS_ME } from "constants/fetch-keys";
// constants
import { ORGANIZATION_SIZE } from "constants/workspace";
@ -50,6 +50,11 @@ const WorkspaceSettings: NextPage = () => {
const { user } = useUserAuth();
const { data: memberDetails } = useSWR(
workspaceSlug ? WORKSPACE_MEMBERS_ME(workspaceSlug.toString()) : null,
workspaceSlug ? () => workspaceService.workspaceMemberMe(workspaceSlug.toString()) : null
);
const { setToastAlert } = useToast();
const { data: activeWorkspace } = useSWR(
@ -142,6 +147,8 @@ const WorkspaceSettings: NextPage = () => {
});
};
const isAdmin = memberDetails?.role === 20;
return (
<WorkspaceAuthorizationLayout
breadcrumbs={
@ -314,26 +321,32 @@ const WorkspaceSettings: NextPage = () => {
</div>
</div>
<div className="sm:text-right">
<SecondaryButton onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
<SecondaryButton
onClick={handleSubmit(onSubmit)}
loading={isSubmitting}
disabled={!isAdmin}
>
{isSubmitting ? "Updating..." : "Update Workspace"}
</SecondaryButton>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold">Danger Zone</h4>
<p className="text-sm text-custom-text-200">
The danger zone of the workspace delete page is a critical area that requires
careful consideration and attention. When deleting a workspace, all of the data
and resources within that workspace will be permanently removed and cannot be
recovered.
</p>
{memberDetails?.role === 20 && (
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold">Danger Zone</h4>
<p className="text-sm text-custom-text-200">
The danger zone of the workspace delete page is a critical area that requires
careful consideration and attention. When deleting a workspace, all of the data
and resources within that workspace will be permanently removed and cannot be
recovered.
</p>
</div>
<div className="col-span-12 sm:col-span-6">
<DangerButton onClick={() => setIsOpen(true)} outline>
Delete the workspace
</DangerButton>
</div>
</div>
<div className="col-span-12 sm:col-span-6">
<DangerButton onClick={() => setIsOpen(true)} outline>
Delete the workspace
</DangerButton>
</div>
</div>
)}
</div>
) : (
<div className="grid h-full w-full place-items-center px-4 sm:px-0">

View file

@ -10,6 +10,7 @@ import workspaceService from "services/workspace.service";
// hooks
import useToast from "hooks/use-toast";
import useUser from "hooks/use-user";
import useWorkspaceMembers from "hooks/use-workspace-members";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { SettingsHeader } from "components/workspace";
@ -24,7 +25,11 @@ import { PlusIcon } from "@heroicons/react/24/outline";
// types
import type { NextPage } from "next";
// fetch-keys
import { WORKSPACE_DETAILS, WORKSPACE_INVITATIONS, WORKSPACE_MEMBERS } from "constants/fetch-keys";
import {
WORKSPACE_DETAILS,
WORKSPACE_INVITATION_WITH_EMAIL,
WORKSPACE_MEMBERS_WITH_EMAIL,
} from "constants/fetch-keys";
// constants
import { ROLE } from "constants/workspace";
// helper
@ -42,19 +47,25 @@ const MembersSettings: NextPage = () => {
const { user } = useUser();
const { isOwner } = useWorkspaceMembers(workspaceSlug?.toString(), Boolean(workspaceSlug));
const { data: activeWorkspace } = useSWR(
workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug.toString()) : null,
() => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug.toString()) : null)
);
const { data: workspaceMembers, mutate: mutateMembers } = useSWR(
workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug.toString()) : null,
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug.toString()) : null
workspaceSlug ? WORKSPACE_MEMBERS_WITH_EMAIL(workspaceSlug.toString()) : null,
workspaceSlug
? () => workspaceService.workspaceMembersWithEmail(workspaceSlug.toString())
: null
);
const { data: workspaceInvitations, mutate: mutateInvitations } = useSWR(
workspaceSlug ? WORKSPACE_INVITATIONS : null,
workspaceSlug ? () => workspaceService.workspaceInvitations(workspaceSlug.toString()) : null
workspaceSlug ? WORKSPACE_INVITATION_WITH_EMAIL(workspaceSlug.toString()) : null,
workspaceSlug
? () => workspaceService.workspaceInvitationsWithEmail(workspaceSlug.toString())
: null
);
const members = [
@ -65,6 +76,7 @@ const MembersSettings: NextPage = () => {
first_name: item.member?.first_name,
last_name: item.member?.last_name,
email: item.member?.email,
display_name: item.member?.display_name,
role: item.role,
status: true,
member: true,
@ -77,6 +89,7 @@ const MembersSettings: NextPage = () => {
first_name: item.email,
last_name: "",
email: item.email,
display_name: item.email,
role: item.role,
status: item.accepted,
member: false,
@ -126,14 +139,15 @@ const MembersSettings: NextPage = () => {
});
})
.finally(() => {
mutateMembers((prevData) =>
prevData?.filter((item) => item.id !== selectedRemoveMember)
mutateMembers((prevData: any) =>
prevData?.filter((item: any) => item.id !== selectedRemoveMember)
);
});
}
if (selectedInviteRemoveMember) {
mutateInvitations(
(prevData) => prevData?.filter((item) => item.id !== selectedInviteRemoveMember),
(prevData: any) =>
prevData?.filter((item: any) => item.id !== selectedInviteRemoveMember),
false
);
workspaceService
@ -194,32 +208,41 @@ const MembersSettings: NextPage = () => {
? members.map((member) => (
<div key={member.id} className="flex items-center justify-between py-6">
<div className="flex items-center gap-x-8 gap-y-2">
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg bg-gray-700 p-4 capitalize text-white">
{member.avatar && member.avatar !== "" ? (
{member.avatar && member.avatar !== "" ? (
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize text-white">
<img
src={member.avatar}
className="absolute top-0 left-0 h-full w-full object-cover rounded-lg"
alt={member.first_name}
alt={member.display_name || member.email}
/>
) : member.first_name !== "" ? (
member.first_name.charAt(0)
) : (
member.email.charAt(0)
)}
</div>
</div>
) : member.display_name || member.email ? (
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize bg-gray-700 text-white">
{(member.display_name || member.email)?.charAt(0)}
</div>
) : (
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize bg-gray-700 text-white">
?
</div>
)}
<div>
{member.member ? (
<Link href={`/${workspaceSlug}/profile/${member.memberId}`}>
<a className="text-sm">
{member.first_name} {member.last_name}
<span>
{member.first_name} {member.last_name}
</span>
<span className="text-custom-text-300 text-sm ml-2">
({member.display_name})
</span>
</a>
</Link>
) : (
<h4 className="text-sm">
{member.first_name} {member.last_name}
</h4>
<h4 className="text-sm">{member.display_name || member.email}</h4>
)}
{isOwner && (
<p className="text-xs text-custom-text-200">{member.email}</p>
)}
<p className="text-xs text-custom-text-200">{member.email}</p>
</div>
</div>
<div className="flex items-center gap-2 text-xs">
@ -240,8 +263,8 @@ const MembersSettings: NextPage = () => {
if (!workspaceSlug) return;
mutateMembers(
(prevData) =>
prevData?.map((m) =>
(prevData: any) =>
prevData?.map((m: any) =>
m.id === member.id ? { ...m, role: value } : m
),
false

View file

@ -33,6 +33,9 @@ import {
SITE_KEYWORDS,
SITE_TITLE,
} from "constants/seo-variables";
// mobx store provider
import { MobxStoreProvider } from "lib/mobx/store-provider";
import MobxStoreInit from "lib/mobx/store-init";
const CrispWithNoSSR = dynamic(() => import("constants/crisp"), { ssr: false });
@ -45,9 +48,10 @@ Router.events.on("routeChangeComplete", NProgress.done);
function MyApp({ Component, pageProps }: AppProps) {
return (
// <UserProvider>
<ThemeProvider themes={THEMES} defaultTheme="system">
<ToastContextProvider>
<ThemeContextProvider>
// mobx root provider
<MobxStoreProvider {...pageProps}>
<ThemeProvider themes={THEMES} defaultTheme="system">
<ToastContextProvider>
<CrispWithNoSSR />
<Head>
<title>{SITE_TITLE}</title>
@ -64,10 +68,11 @@ function MyApp({ Component, pageProps }: AppProps) {
<link rel="manifest" href="/site.webmanifest.json" />
<link rel="shortcut icon" href="/favicon/favicon.ico" />
</Head>
<MobxStoreInit />
<Component {...pageProps} />
</ThemeContextProvider>
</ToastContextProvider>
</ThemeProvider>
</ToastContextProvider>
</ThemeProvider>
</MobxStoreProvider>
// </UserProvider>
);
}

View file

@ -26,6 +26,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
email: user.email,
first_name: user.first_name,
last_name: user.last_name,
display_name: user?.display_name,
})
.then(() => {
jitsu.track(eventName, {

View file

@ -22,8 +22,14 @@ import {
import { Spinner } from "components/ui";
// images
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
// mobx react lite
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// next themes
import { useTheme } from "next-themes";
import { ICurrentUserResponse, IUser } from "types";
import { IUser } from "types";
// types
type EmailPasswordFormValues = {
email: string;
@ -31,15 +37,18 @@ type EmailPasswordFormValues = {
medium?: string;
};
const HomePage: NextPage = () => {
const HomePage: NextPage = observer(() => {
const store: any = useMobxStore();
const { setTheme } = useTheme();
const { isLoading, mutateUser } = useUserAuth("sign-in");
const { setToastAlert } = useToast();
const { setTheme } = useTheme();
const changeTheme = (user: IUser) => {
setTheme(user.theme.theme ?? "system");
const handleTheme = (user: IUser) => {
const currentTheme = user.theme.theme ?? "system";
setTheme(currentTheme);
store?.user?.setCurrentUserSettings();
};
const handleGoogleSignIn = async ({ clientId, credential }: any) => {
@ -53,7 +62,7 @@ const HomePage: NextPage = () => {
const response = await authenticationService.socialAuth(socialAuthPayload);
if (response && response?.user) {
mutateUser();
changeTheme(response.user);
handleTheme(response?.user);
}
} else {
throw Error("Cant find credentials");
@ -79,7 +88,7 @@ const HomePage: NextPage = () => {
const response = await authenticationService.socialAuth(socialAuthPayload);
if (response && response?.user) {
mutateUser();
changeTheme(response.user);
handleTheme(response?.user);
}
} else {
throw Error("Cant find credentials");
@ -101,7 +110,7 @@ const HomePage: NextPage = () => {
try {
if (response) {
mutateUser();
changeTheme(response.user);
handleTheme(response?.user);
}
} catch (err: any) {
setToastAlert({
@ -128,7 +137,7 @@ const HomePage: NextPage = () => {
try {
if (response) {
mutateUser();
changeTheme(response.user);
handleTheme(response?.user);
}
} catch (err: any) {
setToastAlert({
@ -202,6 +211,6 @@ const HomePage: NextPage = () => {
)}
</DefaultLayout>
);
};
});
export default HomePage;