-
Recent Emojis
-
+ {/*
Recent Emojis */}
+
{recentEmojis.map((emoji) => (
{
onChange(emoji);
@@ -97,13 +113,14 @@ const EmojiIconPicker: React.FC = ({ label, value, onChange }) => {
)}
+
-
All Emojis
-
+ {/*
All Emojis */}
+
{emojis.map((emoji) => (
{
onChange(emoji);
@@ -117,9 +134,76 @@ const EmojiIconPicker: React.FC = ({ label, value, onChange }) => {
-
- Coming Soon...
-
+
+
+
+ {[
+ "#D687FF",
+ "#F7AE59",
+ "#FF6B00",
+ "#8CC1FF",
+ "#FCBE1D",
+ "#18904F",
+ "#ADF672",
+ "#05C3FF",
+ "#000000",
+ ].map((curCol) => (
+ setActiveColor(curCol)}
+ />
+ ))}
+ setOpenColorPicker((prev) => !prev)}
+ className="flex items-center gap-1"
+ >
+
+
+
+
+ {
+ setActiveColor(color.hex);
+ if (onIconColorChange) onIconColorChange(color.hex);
+ }}
+ triangle="hide"
+ width="205px"
+ />
+
+
+
+
+
+ {icons.material_rounded.map((icon) => (
+ {
+ if (onIconsClick) onIconsClick(icon.name);
+ setIsOpen(false);
+ }}
+ >
+
+ {icon.name}
+
+
+ ))}
+
+
+
diff --git a/apps/app/components/emoji-icon-picker/types.d.ts b/apps/app/components/emoji-icon-picker/types.d.ts
index 2d3e26353..0162640f5 100644
--- a/apps/app/components/emoji-icon-picker/types.d.ts
+++ b/apps/app/components/emoji-icon-picker/types.d.ts
@@ -2,4 +2,6 @@ export type Props = {
label: string | React.ReactNode;
value: any;
onChange: (data: any) => void;
+ onIconsClick?: (data: any) => void;
+ onIconColorChange?: (data: any) => void;
};
diff --git a/apps/app/components/issues/comment/add-comment.tsx b/apps/app/components/issues/comment/add-comment.tsx
index 426a222ac..307b4b0b4 100644
--- a/apps/app/components/issues/comment/add-comment.tsx
+++ b/apps/app/components/issues/comment/add-comment.tsx
@@ -96,7 +96,7 @@ export const AddComment: React.FC = () => {
setValue("comment_json", jsonValue);
setValue("comment_html", htmlValue);
}}
- // placeholder="Enter Your comment..."
+ placeholder="Enter your comment..."
/>
)}
/>
@@ -104,7 +104,7 @@ export const AddComment: React.FC = () => {
{isSubmitting ? "Adding..." : "Comment"}
diff --git a/apps/app/components/issues/form.tsx b/apps/app/components/issues/form.tsx
index 800630c15..b37ba8db5 100644
--- a/apps/app/components/issues/form.tsx
+++ b/apps/app/components/issues/form.tsx
@@ -6,6 +6,10 @@ import { useRouter } from "next/router";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
+// services
+import aiService from "services/ai.service";
+// hooks
+import useToast from "hooks/use-toast";
// components
import { GptAssistantModal } from "components/core";
import {
@@ -83,10 +87,13 @@ export const IssueForm: FC
= ({
const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false);
const [gptAssistantModal, setGptAssistantModal] = useState(false);
+ const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
const router = useRouter();
const { workspaceSlug } = router.query;
+ const { setToastAlert } = useToast();
+
const {
register,
formState: { errors, isSubmitting },
@@ -102,6 +109,8 @@ export const IssueForm: FC = ({
reValidateMode: "onChange",
});
+ const issueName = watch("name");
+
const handleTitleChange = (e: ChangeEvent) => {
const value = e.target.value;
const similarIssue = issues?.find((i: IIssue) => cosineSimilarity(i.name, value) > 0.7);
@@ -126,6 +135,44 @@ export const IssueForm: FC = ({
setValue("description_html", `${watch("description_html")}${response}
`);
};
+ const handelAutoGenerateDescription = async () => {
+ if (!workspaceSlug || !projectId) return;
+
+ setIAmFeelingLucky(true);
+
+ aiService
+ .createGptTask(workspaceSlug as string, projectId as string, {
+ prompt: issueName,
+ task: "Generate a proper description for this issue in context of a project management software.",
+ })
+ .then((res) => {
+ if (res.response === "")
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message:
+ "Issue title isn't informative enough to generate the description. Please try with a different title.",
+ });
+ else handleAiAssistance(res.response_html);
+ })
+ .catch((err) => {
+ if (err.status === 429)
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message:
+ "You have reached the maximum number of requests of 50 requests per month per user.",
+ });
+ else
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message: "Some error occurred. Please try again.",
+ });
+ })
+ .finally(() => setIAmFeelingLucky(false));
+ };
+
useEffect(() => {
setFocus("name");
@@ -245,10 +292,28 @@ export const IssueForm: FC = ({
)}
-
+
+ {issueName && issueName !== "" && (
+
+ {iAmFeelingLucky ? (
+ "Generating response..."
+ ) : (
+ <>
+ I{"'"}m feeling lucky
+ >
+ )}
+
+ )}
setGptAssistantModal((prevData) => !prevData)}
>
@@ -267,7 +332,7 @@ export const IssueForm: FC = ({
}
onJSONChange={(jsonValue) => setValue("description", jsonValue)}
onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)}
- placeholder="Description"
+ placeholder="Describe the issue..."
/>
)}
/>
diff --git a/apps/app/components/issues/sidebar.tsx b/apps/app/components/issues/sidebar.tsx
index d39b4158a..bda5cdd01 100644
--- a/apps/app/components/issues/sidebar.tsx
+++ b/apps/app/components/issues/sidebar.tsx
@@ -153,11 +153,20 @@ export const IssueDetailsSidebar: React.FC = ({
await issuesService
.createIssueLink(workspaceSlug as string, projectId as string, issueDetail.id, payload)
- .then((res) => {
- mutate(ISSUE_DETAILS(issueDetail.id));
- })
+ .then(() => mutate(ISSUE_DETAILS(issueDetail.id)))
.catch((err) => {
- console.log(err);
+ if (err.status === 400)
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message: "This URL already exists for this issue.",
+ });
+ else
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message: "Something went wrong. Please try again.",
+ });
});
};
diff --git a/apps/app/components/modules/sidebar.tsx b/apps/app/components/modules/sidebar.tsx
index f1a174692..4ecf8cdd2 100644
--- a/apps/app/components/modules/sidebar.tsx
+++ b/apps/app/components/modules/sidebar.tsx
@@ -107,15 +107,20 @@ export const ModuleDetailsSidebar: React.FC = ({
await modulesService
.createModuleLink(workspaceSlug as string, projectId as string, moduleId as string, payload)
- .then((res) => {
- mutate(MODULE_DETAILS(moduleId as string));
- })
+ .then(() => mutate(MODULE_DETAILS(moduleId as string)))
.catch((err) => {
- setToastAlert({
- type: "error",
- title: "Error!",
- message: "Couldn't create the link. Please try again.",
- });
+ if (err.status === 400)
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message: "This URL already exists for this module.",
+ });
+ else
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message: "Something went wrong. Please try again.",
+ });
});
};
diff --git a/apps/app/components/onboarding/user-details.tsx b/apps/app/components/onboarding/user-details.tsx
index febfc0a39..ab7af9ab3 100644
--- a/apps/app/components/onboarding/user-details.tsx
+++ b/apps/app/components/onboarding/user-details.tsx
@@ -22,9 +22,10 @@ const defaultValues: Partial = {
type Props = {
user?: IUser;
setStep: React.Dispatch>;
+ setUserRole: React.Dispatch>;
};
-export const UserDetails: React.FC = ({ user, setStep }) => {
+export const UserDetails: React.FC = ({ user, setStep, setUserRole }) => {
const { setToastAlert } = useToast();
const {
@@ -53,13 +54,15 @@ export const UserDetails: React.FC = ({ user, setStep }) => {
};
useEffect(() => {
- if (user)
+ if (user) {
reset({
first_name: user.first_name,
last_name: user.last_name,
role: user.role,
});
- }, [user, reset]);
+ setUserRole(user.role);
+ }
+ }, [user, reset, setUserRole]);
return (
@@ -202,6 +230,7 @@ export const PagesView: React.FC
= ({ pages, viewType }) => {
handleDeletePage={() => handleDeletePage(page)}
handleAddToFavorites={() => handleAddToFavorites(page)}
handleRemoveFromFavorites={() => handleRemoveFromFavorites(page)}
+ partialUpdatePage={partialUpdatePage}
/>
))}
diff --git a/apps/app/components/pages/single-page-block.tsx b/apps/app/components/pages/single-page-block.tsx
index f138c39f8..4ab8e81ed 100644
--- a/apps/app/components/pages/single-page-block.tsx
+++ b/apps/app/components/pages/single-page-block.tsx
@@ -8,20 +8,29 @@ import { mutate } from "swr";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
+// react-beautiful-dnd
+import { Draggable } from "react-beautiful-dnd";
// services
import pagesService from "services/pages.service";
import issuesService from "services/issues.service";
+import aiService from "services/ai.service";
// hooks
import useToast from "hooks/use-toast";
// components
-import { CreateUpdateIssueModal } from "components/issues";
import { GptAssistantModal } from "components/core";
+import { CreateUpdateBlockInline } from "components/pages";
// ui
-import { CustomMenu, Input, Loader, TextArea } from "components/ui";
+import { CustomMenu, Loader } from "components/ui";
// icons
import { LayerDiagonalIcon } from "components/icons";
import { ArrowPathIcon } from "@heroicons/react/20/solid";
-import { BoltIcon, CheckIcon, SparklesIcon } from "@heroicons/react/24/outline";
+import {
+ BoltIcon,
+ CheckIcon,
+ EllipsisVerticalIcon,
+ PencilIcon,
+ SparklesIcon,
+} from "@heroicons/react/24/outline";
// helpers
import { copyTextToClipboard } from "helpers/string.helper";
// types
@@ -32,6 +41,8 @@ import { PAGE_BLOCKS_LIST } from "constants/fetch-keys";
type Props = {
block: IPageBlock;
projectDetails: IProject | undefined;
+ index: number;
+ handleNewBlock: () => void;
};
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
@@ -43,9 +54,15 @@ const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor
),
});
-export const SinglePageBlock: React.FC
= ({ block, projectDetails }) => {
- const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
+export const SinglePageBlock: React.FC = ({
+ block,
+ projectDetails,
+ index,
+ handleNewBlock,
+}) => {
const [isSyncing, setIsSyncing] = useState(false);
+ const [createBlockForm, setCreateBlockForm] = useState(false);
+ const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
const [gptAssistantModal, setGptAssistantModal] = useState(false);
@@ -54,7 +71,7 @@ export const SinglePageBlock: React.FC = ({ block, projectDetails }) => {
const { setToastAlert } = useToast();
- const { handleSubmit, watch, reset, setValue, control } = useForm({
+ const { handleSubmit, watch, reset, setValue, register } = useForm({
defaultValues: {
name: "",
description: {},
@@ -136,10 +153,6 @@ export const SinglePageBlock: React.FC = ({ block, projectDetails }) => {
});
};
- const editAndPushBlockIntoIssues = async () => {
- setCreateUpdateIssueModal(true);
- };
-
const deletePageBlock = async () => {
if (!workspaceSlug || !projectId || !pageId) return;
@@ -160,6 +173,44 @@ export const SinglePageBlock: React.FC = ({ block, projectDetails }) => {
});
};
+ const handelAutoGenerateDescription = async () => {
+ if (!workspaceSlug || !projectId) return;
+
+ setIAmFeelingLucky(true);
+
+ aiService
+ .createGptTask(workspaceSlug as string, projectId as string, {
+ prompt: block.name,
+ task: "Generate a proper description for this issue in context of a project management software.",
+ })
+ .then((res) => {
+ if (res.response === "")
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message:
+ "Block title isn't informative enough to generate the description. Please try with a different title.",
+ });
+ else handleAiAssistance(res.response_html);
+ })
+ .catch((err) => {
+ if (err.status === 429)
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message:
+ "You have reached the maximum number of requests of 50 requests per month per user.",
+ });
+ else
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message: "Some error occurred. Please try again.",
+ });
+ })
+ .finally(() => setIAmFeelingLucky(false));
+ };
+
const handleAiAssistance = async (response: string) => {
if (!workspaceSlug || !projectId) return;
@@ -228,110 +279,145 @@ export const SinglePageBlock: React.FC = ({ block, projectDetails }) => {
reset({ ...block });
}, [reset, block]);
+ useEffect(() => {
+ window.addEventListener("keydown", (e: KeyboardEvent) => {
+ if (e.key === "Enter" && !createBlockForm) handleNewBlock();
+ });
+
+ return () => {
+ window.removeEventListener("keydown", (e: KeyboardEvent) => {
+ if (e.key === "Enter" && !createBlockForm) handleNewBlock();
+ });
+ };
+ }, [handleNewBlock, createBlockForm]);
+
return (
-
-
setCreateUpdateIssueModal(false)}
- prePopulateData={{
- name: watch("name"),
- description: watch("description"),
- description_html: watch("description_html"),
- }}
- />
-
-
setValue("name", e.target.value)}
- required={true}
- className="min-h-10 block w-full resize-none overflow-hidden border-none bg-transparent py-1 text-base font-medium ring-0 focus:ring-1 focus:ring-gray-200"
- role="textbox"
- />
-
- {block.issue && block.sync && (
-
- {isSyncing ? (
-
- ) : (
-
- )}
- {isSyncing ? "Syncing..." : "Synced"}
+
+ {(provided, snapshot) => (
+ <>
+ {createBlockForm ? (
+
+ setCreateBlockForm(false)}
+ data={block}
+ setIsSyncing={setIsSyncing}
+ />
+
+ ) : (
+
+
+
+
+
+
+ {block.issue && block.sync && (
+
+ {isSyncing ? (
+
+ ) : (
+
+ )}
+ {isSyncing ? "Syncing..." : "Synced"}
+
+ )}
+
+ {iAmFeelingLucky ? (
+ "Generating response..."
+ ) : (
+ <>
+ I{"'"}m feeling lucky
+ >
+ )}
+
+
setGptAssistantModal((prevData) => !prevData)}
+ >
+
+ AI
+
+
setCreateBlockForm(true)}
+ >
+
+
+
} noBorder noChevron>
+ {block.issue ? (
+ <>
+
+ <>Turn sync {block.sync ? "off" : "on"}>
+
+
+ Copy issue link
+
+ >
+ ) : (
+
+ Push into issues
+
+ )}
+
Delete block
+
+
+
+
setGptAssistantModal(false)}
+ inset="top-8 left-0"
+ content={block.description_stripped}
+ htmlContent={block.description_html}
+ onResponse={handleAiAssistance}
+ projectId={projectId as string}
+ />
)}
- {block.issue && (
-
-
-
- {projectDetails?.identifier}-{block.issue_detail?.sequence_id}
-
-
- )}
- setGptAssistantModal((prevData) => !prevData)}
- >
-
- AI
-
- } noBorder noChevron>
- {block.issue ? (
- <>
-
- <>Turn sync {block.sync ? "off" : "on"}>
-
- Copy issue link
- >
- ) : (
- <>
-
- Push into issues
-
- {/*
- Edit and push into issues
- */}
- >
- )}
- Delete block
-
-
-
-
- (
- setValue("description", jsonValue)}
- onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)}
- placeholder="Block description..."
- customClassName="border border-transparent"
- noBorder
- borderOnFocus
- />
- )}
- />
- setGptAssistantModal(false)}
- inset="top-2 left-0"
- content={block.description_stripped}
- htmlContent={block.description_html}
- onResponse={handleAiAssistance}
- projectId={projectId as string}
- />
-
-
+ >
+ )}
+
);
};
diff --git a/apps/app/components/pages/single-page-detailed-item.tsx b/apps/app/components/pages/single-page-detailed-item.tsx
index 47156ad92..e53a1c78b 100644
--- a/apps/app/components/pages/single-page-detailed-item.tsx
+++ b/apps/app/components/pages/single-page-detailed-item.tsx
@@ -5,9 +5,15 @@ import { useRouter } from "next/router";
import dynamic from "next/dynamic";
// ui
-import { CustomMenu, Loader } from "components/ui";
+import { CustomMenu, Loader, Tooltip } from "components/ui";
// icons
-import { PencilIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline";
+import {
+ LockClosedIcon,
+ LockOpenIcon,
+ PencilIcon,
+ StarIcon,
+ TrashIcon,
+} from "@heroicons/react/24/outline";
// helpers
import { truncateText } from "helpers/string.helper";
import { renderShortTime } from "helpers/date-time.helper";
@@ -20,6 +26,7 @@ type TSingleStatProps = {
handleDeletePage: () => void;
handleAddToFavorites: () => void;
handleRemoveFromFavorites: () => void;
+ partialUpdatePage: (page: IPage, formData: Partial) => void;
};
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
@@ -37,90 +44,126 @@ export const SinglePageDetailedItem: React.FC = ({
handleDeletePage,
handleAddToFavorites,
handleRemoveFromFavorites,
+ partialUpdatePage,
}) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
return (
-
-
);
};
diff --git a/apps/app/components/pages/single-page-list-item.tsx b/apps/app/components/pages/single-page-list-item.tsx
index e4cc69147..db3d1d95f 100644
--- a/apps/app/components/pages/single-page-list-item.tsx
+++ b/apps/app/components/pages/single-page-list-item.tsx
@@ -6,7 +6,14 @@ import { useRouter } from "next/router";
// ui
import { CustomMenu, Tooltip } from "components/ui";
// icons
-import { DocumentTextIcon, PencilIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline";
+import {
+ DocumentTextIcon,
+ LockClosedIcon,
+ LockOpenIcon,
+ PencilIcon,
+ StarIcon,
+ TrashIcon,
+} from "@heroicons/react/24/outline";
// helpers
import { truncateText } from "helpers/string.helper";
import { renderShortDate, renderShortTime } from "helpers/date-time.helper";
@@ -19,6 +26,7 @@ type TSingleStatProps = {
handleDeletePage: () => void;
handleAddToFavorites: () => void;
handleRemoveFromFavorites: () => void;
+ partialUpdatePage: (page: IPage, formData: Partial
) => void;
};
export const SinglePageListItem: React.FC = ({
@@ -27,6 +35,7 @@ export const SinglePageListItem: React.FC = ({
handleDeletePage,
handleAddToFavorites,
handleRemoveFromFavorites,
+ partialUpdatePage,
}) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@@ -94,6 +103,29 @@ export const SinglePageListItem: React.FC = ({
)}
+
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ partialUpdatePage(page, { access: page.access ? 0 : 1 });
+ }}
+ >
+ {page.access ? (
+
+ ) : (
+
+ )}
+
+
{
diff --git a/apps/app/components/rich-text-editor/index.tsx b/apps/app/components/rich-text-editor/index.tsx
index 93ce1a60b..4c44650b9 100644
--- a/apps/app/components/rich-text-editor/index.tsx
+++ b/apps/app/components/rich-text-editor/index.tsx
@@ -185,25 +185,23 @@ const RemirrorRichTextEditor: FC = (props) => {
};
return (
-
+
{
onBlur(jsonValue, htmlValue);
}}
>
- {/* {(!value || value === "" || value?.content?.[0]?.content === undefined) && (
-
- {placeholder || "Enter text..."}
-
- )} */}
+ {(!value || value === "" || value?.content?.[0]?.content === undefined) && placeholder && (
+ {placeholder}
+ )}
{imageLoader && (
diff --git a/apps/app/components/ui/empty-state.tsx b/apps/app/components/ui/empty-state.tsx
index 108c9c133..5c1b7eb8b 100644
--- a/apps/app/components/ui/empty-state.tsx
+++ b/apps/app/components/ui/empty-state.tsx
@@ -27,6 +27,10 @@ export const EmptyState: React.FC = ({ type, title, description, imgURL,
return "P";
case "issue":
return "C";
+ case "view":
+ return "V";
+ case "page":
+ return "D"
default:
return null;
}
diff --git a/apps/app/components/views/single-view-item.tsx b/apps/app/components/views/single-view-item.tsx
index 5b8bab19e..931435a79 100644
--- a/apps/app/components/views/single-view-item.tsx
+++ b/apps/app/components/views/single-view-item.tsx
@@ -81,8 +81,8 @@ export const SingleViewItem: React.FC = ({ view, setSelectedView }) => {
};
return (
- <>
-
+
+
@@ -137,7 +137,7 @@ export const SingleViewItem: React.FC
= ({ view, setSelectedView }) => {
)}
-
- >
+
+
);
};
diff --git a/apps/app/components/workspace/create-workspace-form.tsx b/apps/app/components/workspace/create-workspace-form.tsx
index 3f63dbf92..af98c00f3 100644
--- a/apps/app/components/workspace/create-workspace-form.tsx
+++ b/apps/app/components/workspace/create-workspace-form.tsx
@@ -27,6 +27,15 @@ type Props = {
setDefaultValues: Dispatch
>;
};
+const restrictedUrls = [
+ "create-workspace",
+ "error",
+ "invitations",
+ "magic-sign-in",
+ "onboarding",
+ "signin",
+];
+
export const CreateWorkspaceForm: React.FC = ({
onSubmit,
defaultValues,
@@ -49,7 +58,7 @@ export const CreateWorkspaceForm: React.FC = ({
await workspaceService
.workspaceSlugCheck(formData.slug)
.then(async (res) => {
- if (res.status === true) {
+ if (res.status === true && !restrictedUrls.includes(formData.slug)) {
setSlugError(false);
await workspaceService
.createWorkspace(formData)
diff --git a/apps/app/constants/issue.ts b/apps/app/constants/issue.ts
index 83541541c..a9a39620a 100644
--- a/apps/app/constants/issue.ts
+++ b/apps/app/constants/issue.ts
@@ -80,11 +80,8 @@ export const handleIssuesMutation: THandleIssuesMutation = (
let newGroup: IIssue[] = [];
- if (selectedGroupBy === "priority") {
- newGroup = prevData[formData.priority ?? ""] ?? [];
- } else if (selectedGroupBy === "state") {
- newGroup = prevData[formData.state ?? ""] ?? [];
- }
+ if (selectedGroupBy === "priority") newGroup = prevData[formData.priority ?? ""] ?? [];
+ else if (selectedGroupBy === "state") newGroup = prevData[formData.state ?? ""] ?? [];
const updatedIssue = {
...oldGroup[issueIndex],
diff --git a/apps/app/hooks/use-issues-view.tsx b/apps/app/hooks/use-issues-view.tsx
index 377f078e1..cb3ddb43e 100644
--- a/apps/app/hooks/use-issues-view.tsx
+++ b/apps/app/hooks/use-issues-view.tsx
@@ -125,7 +125,15 @@ const useIssuesView = () => {
return issuesToGroup ? Object.assign(emptyStatesObject, issuesToGroup) : undefined;
return issuesToGroup;
- }, [projectIssues, cycleIssues, moduleIssues, groupByProperty, cycleId, moduleId]);
+ }, [
+ projectIssues,
+ cycleIssues,
+ moduleIssues,
+ groupByProperty,
+ cycleId,
+ moduleId,
+ emptyStatesObject,
+ ]);
const isEmpty =
Object.values(groupedByIssues ?? {}).every((group) => group.length === 0) ||
diff --git a/apps/app/layouts/settings-navbar.tsx b/apps/app/layouts/settings-navbar.tsx
index 0aa413892..b990eaf48 100644
--- a/apps/app/layouts/settings-navbar.tsx
+++ b/apps/app/layouts/settings-navbar.tsx
@@ -29,10 +29,10 @@ const SettingsNavbar: React.FC = ({ profilePage = false }) => {
label: "Integrations",
href: `/${workspaceSlug}/settings/integrations`,
},
- {
- label: "Import/Export",
- href: `/${workspaceSlug}/settings/import-export`,
- },
+ // {
+ // label: "Import/Export",
+ // href: `/${workspaceSlug}/settings/import-export`,
+ // },
];
const projectLinks: Array<{
diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx
index fff14ef06..c840b40d1 100644
--- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx
+++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from "react";
+import React, { useEffect, useRef, useState } from "react";
import { useRouter } from "next/router";
@@ -10,6 +10,9 @@ import { useForm } from "react-hook-form";
import { Popover, Transition } from "@headlessui/react";
// react-color
import { TwitterPicker } from "react-color";
+// react-beautiful-dnd
+import { DragDropContext, DropResult } from "react-beautiful-dnd";
+import StrictModeDroppable from "components/dnd/StrictModeDroppable";
// lib
import { requiredAdmin, requiredAuth } from "lib/auth";
// services
@@ -21,16 +24,24 @@ import useToast from "hooks/use-toast";
// layouts
import AppLayout from "layouts/app-layout";
// components
-import { SinglePageBlock } from "components/pages";
+import { CreateUpdateBlockInline, SinglePageBlock } from "components/pages";
// ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
import { CustomSearchSelect, Loader, PrimaryButton, TextArea, Tooltip } from "components/ui";
// icons
-import { ArrowLeftIcon, PlusIcon, ShareIcon, StarIcon } from "@heroicons/react/24/outline";
+import {
+ ArrowLeftIcon,
+ LockClosedIcon,
+ LockOpenIcon,
+ PlusIcon,
+ ShareIcon,
+ StarIcon,
+} from "@heroicons/react/24/outline";
import { ColorPalletteIcon } from "components/icons";
// helpers
import { renderShortTime } from "helpers/date-time.helper";
import { copyTextToClipboard } from "helpers/string.helper";
+import { orderArrayBy } from "helpers/array.helper";
// types
import type { NextPage, GetServerSidePropsContext } from "next";
import { IIssueLabels, IPage, IPageBlock, UserAuth } from "types";
@@ -43,14 +54,16 @@ import {
} from "constants/fetch-keys";
const SinglePage: NextPage = (props) => {
- const [isAddingBlock, setIsAddingBlock] = useState(false);
+ const [createBlockForm, setCreateBlockForm] = useState(false);
+
+ const scrollToRef = useRef(null);
const router = useRouter();
const { workspaceSlug, projectId, pageId } = router.query;
const { setToastAlert } = useToast();
- const { handleSubmit, reset, watch, setValue, control } = useForm({
+ const { handleSubmit, reset, watch, setValue } = useForm({
defaultValues: { name: "" },
});
@@ -65,11 +78,11 @@ const SinglePage: NextPage = (props) => {
workspaceSlug && projectId && pageId ? PAGE_DETAILS(pageId as string) : null,
workspaceSlug && projectId
? () =>
- pagesService.getPageDetails(
- workspaceSlug as string,
- projectId as string,
- pageId as string
- )
+ pagesService.getPageDetails(
+ workspaceSlug as string,
+ projectId as string,
+ pageId as string
+ )
: null
);
@@ -77,11 +90,11 @@ const SinglePage: NextPage = (props) => {
workspaceSlug && projectId && pageId ? PAGE_BLOCKS_LIST(pageId as string) : null,
workspaceSlug && projectId
? () =>
- pagesService.listPageBlocks(
- workspaceSlug as string,
- projectId as string,
- pageId as string
- )
+ pagesService.listPageBlocks(
+ workspaceSlug as string,
+ projectId as string,
+ pageId as string
+ )
: null
);
@@ -131,34 +144,6 @@ const SinglePage: NextPage = (props) => {
});
};
- const createPageBlock = async () => {
- if (!workspaceSlug || !projectId || !pageId) return;
-
- setIsAddingBlock(true);
-
- await pagesService
- .createPageBlock(workspaceSlug as string, projectId as string, pageId as string, {
- name: "New block",
- })
- .then((res) => {
- mutate(
- PAGE_BLOCKS_LIST(pageId as string),
- (prevData) => [...(prevData as IPageBlock[]), res],
- false
- );
- })
- .catch(() => {
- setToastAlert({
- type: "error",
- title: "Error!",
- message: "Page could not be created. Please try again.",
- });
- })
- .finally(() => {
- setIsAddingBlock(false);
- });
- };
-
const handleAddToFavorites = () => {
if (!workspaceSlug || !projectId || !pageId) return;
@@ -195,6 +180,50 @@ const SinglePage: NextPage = (props) => {
);
};
+ const handleOnDragEnd = (result: DropResult) => {
+ if (!result.destination || !workspaceSlug || !projectId || !pageId || !pageBlocks) return;
+
+ const { source, destination } = result;
+
+ let newSortOrder = pageBlocks.find((p) => p.id === result.draggableId)?.sort_order ?? 65535;
+
+ if (destination.index === 0) newSortOrder = pageBlocks[0].sort_order - 10000;
+ else if (destination.index === pageBlocks.length - 1)
+ newSortOrder = pageBlocks[pageBlocks.length - 1].sort_order + 10000;
+ else {
+ if (destination.index > source.index)
+ newSortOrder =
+ (pageBlocks[destination.index].sort_order +
+ pageBlocks[destination.index + 1].sort_order) /
+ 2;
+ else if (destination.index < source.index)
+ newSortOrder =
+ (pageBlocks[destination.index - 1].sort_order +
+ pageBlocks[destination.index].sort_order) /
+ 2;
+ }
+
+ const newBlocksList = pageBlocks.map((p) => ({
+ ...p,
+ sort_order: p.id === result.draggableId ? newSortOrder : p.sort_order,
+ }));
+ mutate(
+ PAGE_BLOCKS_LIST(pageId as string),
+ orderArrayBy(newBlocksList, "sort_order", "ascending"),
+ false
+ );
+
+ pagesService.patchPageBlock(
+ workspaceSlug as string,
+ projectId as string,
+ pageId as string,
+ result.draggableId,
+ {
+ sort_order: newSortOrder,
+ }
+ );
+ };
+
const handleCopyText = () => {
const originURL =
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
@@ -210,6 +239,13 @@ const SinglePage: NextPage = (props) => {
);
};
+ const handleNewBlock = () => {
+ setCreateBlockForm(true);
+ scrollToRef.current?.scrollIntoView({
+ behavior: "smooth",
+ });
+ };
+
const options =
labels?.map((label) => ({
value: label.id,
@@ -272,8 +308,9 @@ const SinglePage: NextPage = (props) => {
key={label.id}
className="group flex items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs"
style={{
- backgroundColor: `${label?.color && label.color !== "" ? label.color : "#000000"
- }20`,
+ backgroundColor: `${
+ label?.color && label.color !== "" ? label.color : "#000000"
+ }20`,
}}
>
= (props) => {
<>
{watch("color") && watch("color") !== "" ? (
= (props) => {
}}
/>
) : (
-
+
)}
@@ -376,6 +414,19 @@ const SinglePage: NextPage = (props) => {
)}
+ {pageDetails.access ? (
+ partialUpdatePage({ access: 0 })} className="z-10">
+
+
+ ) : (
+ partialUpdatePage({ access: 1 })}
+ type="button"
+ className="z-10"
+ >
+
+
+ )}
{pageDetails.is_favorite ? (
@@ -403,34 +454,44 @@ const SinglePage: NextPage = (props) => {
{pageBlocks ? (
<>
- {pageBlocks.length !== 0 && (
-
- {pageBlocks.map((block, index) => (
- <>
-
- >
- ))}
+
+ {pageBlocks.length !== 0 && (
+
+ {(provided) => (
+
+ {pageBlocks.map((block, index) => (
+
+ ))}
+ {provided.placeholder}
+
+ )}
+
+ )}
+
+ {!createBlockForm && (
+
+
+ Add new block
+
+ )}
+ {createBlockForm && (
+
+ setCreateBlockForm(false)}
+ focus="name"
+ />
)}
-
- {isAddingBlock ? (
- "Adding block..."
- ) : (
- <>
-
- Add new block
- >
- )}
-
>
) : (
diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx
index ca12e9143..b70ad37a3 100644
--- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx
+++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx
@@ -167,7 +167,10 @@ const ProjectPages: NextPage = (props) => {
right={
setCreateUpdatePageModal(true)}
+ onClick={() => {
+ const e = new KeyboardEvent("keydown", { key: "d" });
+ document.dispatchEvent(e);
+ }}
>
Create Page
diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx
index 534409ec4..013f74a38 100644
--- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx
+++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx
@@ -46,7 +46,7 @@ const LabelsSettings: NextPage = (props) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
- const scollToRef = useRef(null);
+ const scrollToRef = useRef(null);
const { data: projectDetails } = useSWR(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
@@ -130,7 +130,7 @@ const LabelsSettings: NextPage = (props) => {
setLabelForm={setLabelForm}
isUpdating={isUpdating}
labelToUpdate={labelToUpdate}
- ref={scollToRef}
+ ref={scrollToRef}
/>
)}
<>
@@ -147,7 +147,7 @@ const LabelsSettings: NextPage = (props) => {
addLabelToGroup={() => addLabelToGroup(label)}
editLabel={(label) => {
editLabel(label);
- scollToRef.current?.scrollIntoView({
+ scrollToRef.current?.scrollIntoView({
behavior: "smooth",
});
}}
@@ -163,7 +163,7 @@ const LabelsSettings: NextPage = (props) => {
addLabelToGroup={addLabelToGroup}
editLabel={(label) => {
editLabel(label);
- scollToRef.current?.scrollIntoView({
+ scrollToRef.current?.scrollIntoView({
behavior: "smooth",
});
}}
diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx
index 1ba31d5fa..3c1b0e398 100644
--- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx
+++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx
@@ -67,7 +67,14 @@ const ProjectViews: NextPage = (props) => {
}
right={
-
setIsCreateViewModalOpen(true)}>
+ {
+ const e = new KeyboardEvent("keydown", { key: "v" });
+ document.dispatchEvent(e);
+ }}
+ >
Create View
@@ -100,7 +107,6 @@ const ProjectViews: NextPage = (props) => {
title="Create New View"
description="Views aid in saving your issues by applying various filters and grouping options."
imgURL={emptyView}
- action={() => setIsCreateViewModalOpen(true)}
/>
)
) : (
diff --git a/apps/app/pages/api/track-event.ts b/apps/app/pages/api/track-event.ts
index 9112faeea..c9255d215 100644
--- a/apps/app/pages/api/track-event.ts
+++ b/apps/app/pages/api/track-event.ts
@@ -37,15 +37,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// TODO: cache user info
jitsu
- .id(
- {
- id: user.id,
- email: user.email,
- first_name: user.first_name,
- last_name: user.last_name,
- },
- true
- )
+ .id({
+ id: user.id,
+ email: user.email,
+ first_name: user.first_name,
+ last_name: user.last_name,
+ })
.then(() => {
jitsu.track(eventName, {
...extra,
diff --git a/apps/app/pages/onboarding.tsx b/apps/app/pages/onboarding.tsx
index b2a7e987f..0c1c95326 100644
--- a/apps/app/pages/onboarding.tsx
+++ b/apps/app/pages/onboarding.tsx
@@ -24,6 +24,7 @@ import type { NextPage, GetServerSidePropsContext } from "next";
const Onboarding: NextPage = () => {
const [step, setStep] = useState(1);
+ const [userRole, setUserRole] = useState(null);
const [workspace, setWorkspace] = useState();
@@ -40,7 +41,7 @@ const Onboarding: NextPage = () => {
{step === 1 ? (
-
+
) : step === 2 ? (
) : (
@@ -69,7 +70,7 @@ const Onboarding: NextPage = () => {
onClick={() => {
if (step === 8) {
userService
- .updateUserOnBoard()
+ .updateUserOnBoard({ userRole })
.then(() => {
router.push("/");
})
diff --git a/apps/app/services/issues.service.ts b/apps/app/services/issues.service.ts
index e6848f7b5..d23ff33f2 100644
--- a/apps/app/services/issues.service.ts
+++ b/apps/app/services/issues.service.ts
@@ -343,7 +343,7 @@ class ProjectIssuesServices extends APIService {
)
.then((response) => response?.data)
.catch((error) => {
- throw error?.response?.data;
+ throw error?.response;
});
}
diff --git a/apps/app/services/modules.service.ts b/apps/app/services/modules.service.ts
index 419d6033c..1fe3aaac5 100644
--- a/apps/app/services/modules.service.ts
+++ b/apps/app/services/modules.service.ts
@@ -175,7 +175,7 @@ class ProjectIssuesServices extends APIService {
)
.then((response) => response?.data)
.catch((error) => {
- throw error?.response?.data;
+ throw error?.response;
});
}
diff --git a/apps/app/services/track-event.service.ts b/apps/app/services/track-event.service.ts
index a0f50afac..73d79483d 100644
--- a/apps/app/services/track-event.service.ts
+++ b/apps/app/services/track-event.service.ts
@@ -14,6 +14,7 @@ import type {
IPageBlock,
IProject,
IState,
+ IView,
IWorkspace,
} from "types";
@@ -37,6 +38,8 @@ type ModuleEventType = "MODULE_CREATE" | "MODULE_UPDATE" | "MODULE_DELETE";
type PagesEventType = "PAGE_CREATE" | "PAGE_UPDATE" | "PAGE_DELETE";
+type ViewEventType = "VIEW_CREATE" | "VIEW_UPDATE" | "VIEW_DELETE";
+
type PageBlocksEventType =
| "PAGE_BLOCK_CREATE"
| "PAGE_BLOCK_UPDATE"
@@ -365,6 +368,30 @@ class TrackEventServices extends APIService {
},
});
}
+
+ async trackViewEvent(data: IView, eventName: ViewEventType): Promise {
+ let payload: any;
+ if (eventName === "VIEW_DELETE") payload = data;
+ else
+ payload = {
+ labels: Boolean(data.query_data.labels),
+ assignees: Boolean(data.query_data.assignees),
+ priority: Boolean(data.query_data.priority),
+ state: Boolean(data.query_data.state),
+ created_by: Boolean(data.query_data.created_by),
+ };
+
+ return this.request({
+ url: "/api/track-event",
+ method: "POST",
+ data: {
+ eventName,
+ extra: {
+ ...payload,
+ },
+ },
+ });
+ }
}
const trackEventServices = new TrackEventServices();
diff --git a/apps/app/services/user.service.ts b/apps/app/services/user.service.ts
index 95f42554d..cab79ab3a 100644
--- a/apps/app/services/user.service.ts
+++ b/apps/app/services/user.service.ts
@@ -47,10 +47,16 @@ class UserService extends APIService {
});
}
- async updateUserOnBoard(): Promise {
- return this.patch("/api/users/me/onboard/", { is_onboarded: true })
+ async updateUserOnBoard({ userRole }: any): Promise {
+ return this.patch("/api/users/me/onboard/", {
+ is_onboarded: true,
+ })
.then((response) => {
- if (trackEvent) trackEventServices.trackUserOnboardingCompleteEvent(response.data);
+ if (trackEvent)
+ trackEventServices.trackUserOnboardingCompleteEvent({
+ ...response.data,
+ user_role: userRole ?? "None",
+ });
return response?.data;
})
.catch((error) => {
diff --git a/apps/app/services/views.service.ts b/apps/app/services/views.service.ts
index 80699d994..af2426a7c 100644
--- a/apps/app/services/views.service.ts
+++ b/apps/app/services/views.service.ts
@@ -1,10 +1,15 @@
// services
import APIService from "services/api.service";
+import trackEventServices from "services/track-event.service";
+
// types
import { IView } from "types/views";
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
+const trackEvent =
+ process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
+
class ViewServices extends APIService {
constructor() {
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
@@ -12,7 +17,10 @@ class ViewServices extends APIService {
async createView(workspaceSlug: string, projectId: string, data: IView): Promise {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/`, data)
- .then((response) => response?.data)
+ .then((response) => {
+ if (trackEvent) trackEventServices.trackViewEvent(response?.data, "VIEW_CREATE");
+ return response?.data;
+ })
.catch((error) => {
throw error?.response?.data;
});
@@ -25,7 +33,10 @@ class ViewServices extends APIService {
data: IView
): Promise {
return this.put(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`, data)
- .then((response) => response?.data)
+ .then((response) => {
+ if (trackEvent) trackEventServices.trackViewEvent(response?.data, "VIEW_UPDATE");
+ return response?.data;
+ })
.catch((error) => {
throw error?.response?.data;
});
@@ -41,7 +52,10 @@ class ViewServices extends APIService {
`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`,
data
)
- .then((response) => response?.data)
+ .then((response) => {
+ if (trackEvent) trackEventServices.trackViewEvent(response?.data, "VIEW_UPDATE");
+ return response?.data;
+ })
.catch((error) => {
throw error?.response?.data;
});
@@ -49,7 +63,10 @@ class ViewServices extends APIService {
async deleteView(workspaceSlug: string, projectId: string, viewId: string): Promise {
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`)
- .then((response) => response?.data)
+ .then((response) => {
+ if (trackEvent) trackEventServices.trackViewEvent(response?.data, "VIEW_DELETE");
+ return response?.data;
+ })
.catch((error) => {
throw error?.response?.data;
});
@@ -111,7 +128,6 @@ class ViewServices extends APIService {
throw error?.response?.data;
});
}
-
}
export default new ViewServices();
diff --git a/apps/app/styles/command-pallette.css b/apps/app/styles/command-pallette.css
index 00f4e6483..86fcb51da 100644
--- a/apps/app/styles/command-pallette.css
+++ b/apps/app/styles/command-pallette.css
@@ -31,5 +31,9 @@
}
[cmdk-item]:hover {
- background-color: rgb(243 244 246);
+ background-color: rgb(229 231 235);
+}
+
+[cmdk-item][aria-selected="true"] {
+ background-color: rgb(229 231 235);
}
diff --git a/apps/app/styles/editor.css b/apps/app/styles/editor.css
index 55cb8a8e1..c9d58dfaf 100644
--- a/apps/app/styles/editor.css
+++ b/apps/app/styles/editor.css
@@ -363,6 +363,10 @@ img.ProseMirror-separator {
min-height: 50px;
}
+.remirror-section .remirror-editor-wrapper .remirror-editor {
+ min-height: 0 !important;
+}
+
.remirror-editor-wrapper {
padding-top: 8px;
}
diff --git a/apps/app/styles/globals.css b/apps/app/styles/globals.css
index f4dd63e48..eeed87dd4 100644
--- a/apps/app/styles/globals.css
+++ b/apps/app/styles/globals.css
@@ -128,4 +128,9 @@
.react-datepicker-popper {
z-index: 30 !important;
}
+
+.conical-gradient{
+ background: conic-gradient(from 180deg at 50% 50%, #FF6B00 0deg, #F7AE59 70.5deg, #3F76FF 151.12deg, #05C3FF 213deg, #18914F 289.87deg, #F6F172 329.25deg, #FF6B00 360deg);
+}
+
/* end react datepicker styling */