);
});
diff --git a/apps/space/components/issues/board-views/kanban/index.tsx b/space/components/issues/board-views/kanban/index.tsx
similarity index 69%
rename from apps/space/components/issues/board-views/kanban/index.tsx
rename to space/components/issues/board-views/kanban/index.tsx
index d716356ff..b45b037d2 100644
--- a/apps/space/components/issues/board-views/kanban/index.tsx
+++ b/space/components/issues/board-views/kanban/index.tsx
@@ -5,8 +5,10 @@ import { observer } from "mobx-react-lite";
// components
import { IssueListHeader } from "components/issues/board-views/kanban/header";
import { IssueListBlock } from "components/issues/board-views/kanban/block";
+// ui
+import { Icon } from "components/ui";
// interfaces
-import { IIssueState, IIssue } from "store/types/issue";
+import { IIssueState, IIssue } from "types/issue";
// mobx hook
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
@@ -19,21 +21,22 @@ export const IssueKanbanView = observer(() => {
{store?.issue?.states &&
store?.issue?.states.length > 0 &&
store?.issue?.states.map((_state: IIssueState) => (
-
+
-
+
{store.issue.getFilteredIssuesByState(_state.id) &&
store.issue.getFilteredIssuesByState(_state.id).length > 0 ? (
-
+
{store.issue.getFilteredIssuesByState(_state.id).map((_issue: IIssue) => (
-
+
))}
) : (
-
- No Issues are available.
+
+
+ No issues in this state
)}
diff --git a/space/components/issues/board-views/list/block.tsx b/space/components/issues/board-views/list/block.tsx
new file mode 100644
index 000000000..bdf39b84f
--- /dev/null
+++ b/space/components/issues/board-views/list/block.tsx
@@ -0,0 +1,84 @@
+import { FC } from "react";
+import { useRouter } from "next/router";
+import { observer } from "mobx-react-lite";
+// components
+import { IssueBlockPriority } from "components/issues/board-views/block-priority";
+import { IssueBlockState } from "components/issues/board-views/block-state";
+import { IssueBlockLabels } from "components/issues/board-views/block-labels";
+import { IssueBlockDueDate } from "components/issues/board-views/block-due-date";
+// mobx hook
+import { useMobxStore } from "lib/mobx/store-provider";
+// interfaces
+import { IIssue } from "types/issue";
+// store
+import { RootStore } from "store/root";
+
+export const IssueListBlock: FC<{ issue: IIssue }> = observer((props) => {
+ const { issue } = props;
+ // store
+ const { project: projectStore, issueDetails: issueDetailStore }: RootStore = useMobxStore();
+ // router
+ const router = useRouter();
+ const { workspace_slug, project_slug, board } = router.query;
+
+ const handleBlockClick = () => {
+ issueDetailStore.setPeekId(issue.id);
+ router.push(
+ {
+ pathname: `/${workspace_slug?.toString()}/${project_slug}`,
+ query: {
+ board: board?.toString(),
+ peekId: issue.id,
+ },
+ },
+ undefined,
+ { shallow: true }
+ );
+ // router.push(`/${workspace_slug?.toString()}/${project_slug}?board=${board?.toString()}&peekId=${issue.id}`);
+ };
+
+ return (
+
+
+ {/* id */}
+
+ {projectStore?.project?.identifier}-{issue?.sequence_id}
+
+ {/* name */}
+
+ {issue.name}
+
+
+
+
+ {/* priority */}
+ {issue?.priority && (
+
+
+
+ )}
+
+ {/* state */}
+ {issue?.state_detail && (
+
+
+
+ )}
+
+ {/* labels */}
+ {issue?.label_details && issue?.label_details.length > 0 && (
+
+
+
+ )}
+
+ {/* due date */}
+ {issue?.target_date && (
+
+
+
+ )}
+
+
+ );
+});
diff --git a/apps/space/components/issues/board-views/list/header.tsx b/space/components/issues/board-views/list/header.tsx
similarity index 50%
rename from apps/space/components/issues/board-views/list/header.tsx
rename to space/components/issues/board-views/list/header.tsx
index e87cac6f7..83312e7b9 100644
--- a/apps/space/components/issues/board-views/list/header.tsx
+++ b/space/components/issues/board-views/list/header.tsx
@@ -1,9 +1,9 @@
-"use client";
-
// mobx react lite
import { observer } from "mobx-react-lite";
// interfaces
-import { IIssueState } from "store/types/issue";
+import { IIssueState } from "types/issue";
+// icons
+import { StateGroupIcon } from "components/icons";
// constants
import { issueGroupFilter } from "constants/data";
// mobx hook
@@ -18,14 +18,12 @@ export const IssueListHeader = observer(({ state }: { state: IIssueState }) => {
if (stateGroup === null) return <>>;
return (
-
-
-
-
-
{state?.name}
-
- {store.issue.getCountOfIssuesByState(state.id)}
+
+
+
+
{state?.name}
+
{store.issue.getCountOfIssuesByState(state.id)}
);
});
diff --git a/space/components/issues/board-views/list/index.tsx b/space/components/issues/board-views/list/index.tsx
new file mode 100644
index 000000000..1c6900dd9
--- /dev/null
+++ b/space/components/issues/board-views/list/index.tsx
@@ -0,0 +1,38 @@
+import { observer } from "mobx-react-lite";
+// components
+import { IssueListHeader } from "components/issues/board-views/list/header";
+import { IssueListBlock } from "components/issues/board-views/list/block";
+// interfaces
+import { IIssueState, IIssue } from "types/issue";
+// mobx hook
+import { useMobxStore } from "lib/mobx/store-provider";
+// store
+import { RootStore } from "store/root";
+
+export const IssueListView = observer(() => {
+ const { issue: issueStore }: RootStore = useMobxStore();
+
+ return (
+ <>
+ {issueStore?.states &&
+ issueStore?.states.length > 0 &&
+ issueStore?.states.map((_state: IIssueState) => (
+
+
+ {issueStore.getFilteredIssuesByState(_state.id) &&
+ issueStore.getFilteredIssuesByState(_state.id).length > 0 ? (
+
+ {issueStore.getFilteredIssuesByState(_state.id).map((_issue: IIssue) => (
+
+ ))}
+
+ ) : (
+
+ No Issues are available.
+
+ )}
+
+ ))}
+ >
+ );
+});
diff --git a/apps/space/components/issues/board-views/spreadsheet/index.tsx b/space/components/issues/board-views/spreadsheet/index.tsx
similarity index 100%
rename from apps/space/components/issues/board-views/spreadsheet/index.tsx
rename to space/components/issues/board-views/spreadsheet/index.tsx
diff --git a/space/components/issues/filters-render/index.tsx b/space/components/issues/filters-render/index.tsx
new file mode 100644
index 000000000..d797d1506
--- /dev/null
+++ b/space/components/issues/filters-render/index.tsx
@@ -0,0 +1,53 @@
+import { useRouter } from "next/router";
+// mobx react lite
+import { observer } from "mobx-react-lite";
+// components
+import IssueStateFilter from "./state";
+import IssueLabelFilter from "./label";
+import IssuePriorityFilter from "./priority";
+// mobx hook
+import { useMobxStore } from "lib/mobx/store-provider";
+import { RootStore } from "store/root";
+
+const IssueFilter = observer(() => {
+ const store: RootStore = useMobxStore();
+
+ const router = useRouter();
+ const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string };
+
+ const clearAllFilters = () => {
+ // router.replace(
+ // store.issue.getURLDefinition(workspace_slug, project_slug, {
+ // key: "all",
+ // removeAll: true,
+ // })
+ // );
+ };
+
+ // if (store.issue.getIfFiltersIsEmpty()) return null;
+
+ return (
+
+
+ {/* state */}
+ {/* {store.issue.checkIfFilterExistsForKey("state") &&
} */}
+ {/* labels */}
+ {/* {store.issue.checkIfFilterExistsForKey("label") &&
} */}
+ {/* priority */}
+ {/* {store.issue.checkIfFilterExistsForKey("priority") &&
} */}
+ {/* clear all filters */}
+
+
Clear all filters
+
+ close
+
+
+
+
+ );
+});
+
+export default IssueFilter;
diff --git a/space/components/issues/filters-render/label/filter-label-block.tsx b/space/components/issues/filters-render/label/filter-label-block.tsx
new file mode 100644
index 000000000..a54fb65e4
--- /dev/null
+++ b/space/components/issues/filters-render/label/filter-label-block.tsx
@@ -0,0 +1,43 @@
+import { useRouter } from "next/router";
+// mobx react lite
+import { observer } from "mobx-react-lite";
+// mobx hook
+import { useMobxStore } from "lib/mobx/store-provider";
+// interfaces
+import { IIssueLabel } from "types/issue";
+
+export const RenderIssueLabel = observer(({ label }: { label: IIssueLabel }) => {
+ const store = useMobxStore();
+
+ const router = useRouter();
+ const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string };
+
+ const removeLabelFromFilter = () => {
+ // router.replace(
+ // store.issue.getURLDefinition(workspace_slug, project_slug, {
+ // key: "label",
+ // value: label?.id,
+ // })
+ // );
+ };
+
+ return (
+
+
+
+
{label?.name}
+
+ close
+
+
+ );
+});
diff --git a/space/components/issues/filters-render/label/index.tsx b/space/components/issues/filters-render/label/index.tsx
new file mode 100644
index 000000000..1d9a4f990
--- /dev/null
+++ b/space/components/issues/filters-render/label/index.tsx
@@ -0,0 +1,51 @@
+import { useRouter } from "next/router";
+// mobx react lite
+import { observer } from "mobx-react-lite";
+// components
+import { RenderIssueLabel } from "./filter-label-block";
+// interfaces
+import { IIssueLabel } from "types/issue";
+// mobx hook
+import { useMobxStore } from "lib/mobx/store-provider";
+import { RootStore } from "store/root";
+
+const IssueLabelFilter = observer(() => {
+ const store: RootStore = useMobxStore();
+
+ const router = useRouter();
+ const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string };
+
+ const clearLabelFilters = () => {
+ // router.replace(
+ // store.issue.getURLDefinition(workspace_slug, project_slug, {
+ // key: "label",
+ // removeAll: true,
+ // })
+ // );
+ };
+
+ return (
+ <>
+
+
Labels
+
+ {/* {store?.issue?.labels &&
+ store?.issue?.labels.map(
+ (_label: IIssueLabel, _index: number) =>
+ store.issue.getUserSelectedFilter("label", _label.id) && (
+
+ )
+ )} */}
+
+
+ close
+
+
+ >
+ );
+});
+
+export default IssueLabelFilter;
diff --git a/space/components/issues/filters-render/priority/filter-priority-block.tsx b/space/components/issues/filters-render/priority/filter-priority-block.tsx
new file mode 100644
index 000000000..5fd1ef1a7
--- /dev/null
+++ b/space/components/issues/filters-render/priority/filter-priority-block.tsx
@@ -0,0 +1,42 @@
+import { useRouter } from "next/router";
+// mobx react lite
+import { observer } from "mobx-react-lite";
+// mobx hook
+import { useMobxStore } from "lib/mobx/store-provider";
+// interfaces
+import { IIssuePriorityFilters } from "types/issue";
+
+export const RenderIssuePriority = observer(({ priority }: { priority: IIssuePriorityFilters }) => {
+ const store = useMobxStore();
+
+ const router = useRouter();
+ const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string };
+
+ const removePriorityFromFilter = () => {
+ // router.replace(
+ // store.issue.getURLDefinition(workspace_slug, project_slug, {
+ // key: "priority",
+ // value: priority?.key,
+ // })
+ // );
+ };
+
+ return (
+
+
+ {priority?.icon}
+
+
{priority?.title}
+
+ close
+
+
+ );
+});
diff --git a/space/components/issues/filters-render/priority/index.tsx b/space/components/issues/filters-render/priority/index.tsx
new file mode 100644
index 000000000..100ba1761
--- /dev/null
+++ b/space/components/issues/filters-render/priority/index.tsx
@@ -0,0 +1,53 @@
+import { useRouter } from "next/router";
+// mobx react lite
+import { observer } from "mobx-react-lite";
+// mobx hook
+import { useMobxStore } from "lib/mobx/store-provider";
+// components
+import { RenderIssuePriority } from "./filter-priority-block";
+// interfaces
+import { IIssuePriorityFilters } from "types/issue";
+// constants
+import { issuePriorityFilters } from "constants/data";
+
+const IssuePriorityFilter = observer(() => {
+ const store = useMobxStore();
+
+ const router = useRouter();
+ const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string };
+
+ const clearPriorityFilters = () => {
+ // router.replace(
+ // store.issue.getURLDefinition(workspace_slug, project_slug, {
+ // key: "priority",
+ // removeAll: true,
+ // })
+ // );
+ };
+
+ return (
+ <>
+
+
Priority
+
+ {/* {issuePriorityFilters.map(
+ (_priority: IIssuePriorityFilters, _index: number) =>
+ store.issue.getUserSelectedFilter("priority", _priority.key) && (
+
+ )
+ )} */}
+
+
{
+ clearPriorityFilters();
+ }}
+ >
+ close
+
+
+ >
+ );
+});
+
+export default IssuePriorityFilter;
diff --git a/space/components/issues/filters-render/state/filter-state-block.tsx b/space/components/issues/filters-render/state/filter-state-block.tsx
new file mode 100644
index 000000000..8445386a4
--- /dev/null
+++ b/space/components/issues/filters-render/state/filter-state-block.tsx
@@ -0,0 +1,43 @@
+import { useRouter } from "next/router";
+// mobx react lite
+import { observer } from "mobx-react-lite";
+// mobx hook
+import { useMobxStore } from "lib/mobx/store-provider";
+// interfaces
+import { IIssueState } from "types/issue";
+// constants
+import { issueGroupFilter } from "constants/data";
+
+export const RenderIssueState = observer(({ state }: { state: IIssueState }) => {
+ const store = useMobxStore();
+
+ const router = useRouter();
+ const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string };
+
+ const stateGroup = issueGroupFilter(state.group);
+
+ const removeStateFromFilter = () => {
+ // router.replace(
+ // store.issue.getURLDefinition(workspace_slug, project_slug, {
+ // key: "state",
+ // value: state?.id,
+ // })
+ // );
+ };
+
+ if (stateGroup === null) return <>>;
+ return (
+
+
+
+
+
{state?.name}
+
+ close
+
+
+ );
+});
diff --git a/space/components/issues/filters-render/state/index.tsx b/space/components/issues/filters-render/state/index.tsx
new file mode 100644
index 000000000..0198c5215
--- /dev/null
+++ b/space/components/issues/filters-render/state/index.tsx
@@ -0,0 +1,51 @@
+import { useRouter } from "next/router";
+// mobx react lite
+import { observer } from "mobx-react-lite";
+// components
+import { RenderIssueState } from "./filter-state-block";
+// interfaces
+import { IIssueState } from "types/issue";
+// mobx hook
+import { useMobxStore } from "lib/mobx/store-provider";
+import { RootStore } from "store/root";
+
+const IssueStateFilter = observer(() => {
+ const store: RootStore = useMobxStore();
+
+ const router = useRouter();
+ const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string };
+
+ const clearStateFilters = () => {
+ // router.replace(
+ // store.issue.getURLDefinition(workspace_slug, project_slug, {
+ // key: "state",
+ // removeAll: true,
+ // })
+ // );
+ };
+
+ return (
+ <>
+
+
State
+
+ {/* {store?.issue?.states &&
+ store?.issue?.states.map(
+ (_state: IIssueState, _index: number) =>
+ store.issue.getUserSelectedFilter("state", _state.id) && (
+
+ )
+ )} */}
+
+
+ close
+
+
+ >
+ );
+});
+
+export default IssueStateFilter;
diff --git a/space/components/issues/navbar/index.tsx b/space/components/issues/navbar/index.tsx
new file mode 100644
index 000000000..509d676b7
--- /dev/null
+++ b/space/components/issues/navbar/index.tsx
@@ -0,0 +1,121 @@
+import { useEffect } from "react";
+
+import Link from "next/link";
+import Image from "next/image";
+import { useRouter } from "next/router";
+
+// mobx
+import { observer } from "mobx-react-lite";
+// components
+import { NavbarSearch } from "./search";
+import { NavbarIssueBoardView } from "./issue-board-view";
+import { NavbarTheme } from "./theme";
+// ui
+import { PrimaryButton } from "components/ui";
+// lib
+import { useMobxStore } from "lib/mobx/store-provider";
+// store
+import { RootStore } from "store/root";
+
+const renderEmoji = (emoji: string | { name: string; color: string }) => {
+ if (!emoji) return;
+
+ if (typeof emoji === "object")
+ return (
+
+ {emoji.name}
+
+ );
+ else return isNaN(parseInt(emoji)) ? emoji : String.fromCodePoint(parseInt(emoji));
+};
+
+const IssueNavbar = observer(() => {
+ const { project: projectStore, user: userStore }: RootStore = useMobxStore();
+ // router
+ const router = useRouter();
+ const { workspace_slug, project_slug, board } = router.query;
+
+ const user = userStore?.currentUser;
+
+ useEffect(() => {
+ if (workspace_slug && project_slug) {
+ projectStore.fetchProjectSettings(workspace_slug.toString(), project_slug.toString());
+ }
+ }, [projectStore, workspace_slug, project_slug]);
+
+ useEffect(() => {
+ if (workspace_slug && project_slug) {
+ if (!board) {
+ router.push({
+ pathname: `/${workspace_slug}/${project_slug}`,
+ query: {
+ board: "list",
+ },
+ });
+ return projectStore.setActiveBoard("list");
+ }
+ projectStore.setActiveBoard(board.toString());
+ }
+ }, [board, workspace_slug, project_slug]);
+
+ return (
+
+ {/* project detail */}
+
+
+ {projectStore?.project && projectStore?.project?.emoji ? (
+ renderEmoji(projectStore?.project?.emoji)
+ ) : (
+
+ )}
+
+
+ {projectStore?.project?.name || `...`}
+
+
+
+ {/* issue search bar */}
+
+
+
+
+ {/* issue views */}
+
+
+
+
+ {/* theming */}
+
+
+
+
+ {user ? (
+
+ {user.avatar && user.avatar !== "" ? (
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+
+
+ ) : (
+
+ {(user.display_name ?? "A")[0]}
+
+ )}
+
{user.display_name}
+
+ ) : (
+
+ )}
+
+ );
+});
+
+export default IssueNavbar;
diff --git a/space/components/issues/navbar/issue-board-view.tsx b/space/components/issues/navbar/issue-board-view.tsx
new file mode 100644
index 000000000..0ae71e8ee
--- /dev/null
+++ b/space/components/issues/navbar/issue-board-view.tsx
@@ -0,0 +1,49 @@
+import { useRouter } from "next/router";
+import { observer } from "mobx-react-lite";
+// constants
+import { issueViews } from "constants/data";
+// mobx
+import { useMobxStore } from "lib/mobx/store-provider";
+import { RootStore } from "store/root";
+
+export const NavbarIssueBoardView = observer(() => {
+ const { project: projectStore, issue: issueStore }: RootStore = useMobxStore();
+
+ const router = useRouter();
+ const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string };
+
+ const handleCurrentBoardView = (boardView: string) => {
+ projectStore.setActiveBoard(boardView);
+ router.push(`/${workspace_slug}/${project_slug}?board=${boardView}`);
+ };
+
+ return (
+ <>
+ {projectStore?.viewOptions &&
+ Object.keys(projectStore?.viewOptions).map((viewKey: string) => {
+ if (projectStore?.viewOptions[viewKey]) {
+ return (
+
handleCurrentBoardView(viewKey)}
+ title={viewKey}
+ >
+
+ {issueViews[viewKey]?.icon}
+
+
+ );
+ }
+ })}
+ >
+ );
+});
diff --git a/space/components/issues/navbar/issue-filter.tsx b/space/components/issues/navbar/issue-filter.tsx
new file mode 100644
index 000000000..852121a5e
--- /dev/null
+++ b/space/components/issues/navbar/issue-filter.tsx
@@ -0,0 +1,111 @@
+import { useRouter } from "next/router";
+import { observer } from "mobx-react-lite";
+// icons
+import { ChevronDownIcon } from "@heroicons/react/20/solid";
+// mobx
+import { useMobxStore } from "lib/mobx/store-provider";
+import { RootStore } from "store/root";
+// components
+import { Dropdown } from "components/ui/dropdown";
+// constants
+import { issueGroupFilter } from "constants/data";
+
+const PRIORITIES = ["urgent", "high", "medium", "low"];
+
+export const NavbarIssueFilter = observer(() => {
+ const store: RootStore = useMobxStore();
+
+ const router = useRouter();
+ const pathName = router.asPath;
+
+ const handleOnSelect = (key: "states" | "labels" | "priorities", value: string) => {
+ // if (key === "states") {
+ // store.issue.userSelectedStates = store.issue.userSelectedStates.includes(value)
+ // ? store.issue.userSelectedStates.filter((s) => s !== value)
+ // : [...store.issue.userSelectedStates, value];
+ // } else if (key === "labels") {
+ // store.issue.userSelectedLabels = store.issue.userSelectedLabels.includes(value)
+ // ? store.issue.userSelectedLabels.filter((l) => l !== value)
+ // : [...store.issue.userSelectedLabels, value];
+ // } else if (key === "priorities") {
+ // store.issue.userSelectedPriorities = store.issue.userSelectedPriorities.includes(value)
+ // ? store.issue.userSelectedPriorities.filter((p) => p !== value)
+ // : [...store.issue.userSelectedPriorities, value];
+ // }
+ // const paramsCommaSeparated = `${`board=${store.issue.currentIssueBoardView || "list"}`}${
+ // store.issue.userSelectedPriorities.length > 0 ? `&priorities=${store.issue.userSelectedPriorities.join(",")}` : ""
+ // }${store.issue.userSelectedStates.length > 0 ? `&states=${store.issue.userSelectedStates.join(",")}` : ""}${
+ // store.issue.userSelectedLabels.length > 0 ? `&labels=${store.issue.userSelectedLabels.join(",")}` : ""
+ // }`;
+ // router.replace(`${pathName}?${paramsCommaSeparated}`);
+ };
+
+ return (
+
+ Filters
+
+ >
+ }
+ items={[
+ {
+ display: "Priority",
+ children: PRIORITIES.map((priority) => ({
+ display: (
+
+
+ {priority === "urgent"
+ ? "error"
+ : priority === "high"
+ ? "signal_cellular_alt"
+ : priority === "medium"
+ ? "signal_cellular_alt_2_bar"
+ : "signal_cellular_alt_1_bar"}
+
+ {priority}
+
+ ),
+ onClick: () => handleOnSelect("priorities", priority),
+ isSelected: store.issue.filteredPriorities.includes(priority),
+ })),
+ },
+ {
+ display: "State",
+ children: (store.issue.states || []).map((state) => {
+ const stateGroup = issueGroupFilter(state.group);
+
+ return {
+ display: (
+
+ {stateGroup && }
+ {state.name}
+
+ ),
+ onClick: () => handleOnSelect("states", state.id),
+ isSelected: store.issue.filteredStates.includes(state.id),
+ };
+ }),
+ },
+ {
+ display: "Labels",
+ children: [...(store.issue.labels || [])].map((label) => ({
+ display: (
+
+
+ {label.name}
+
+ ),
+ onClick: () => handleOnSelect("labels", label.id),
+ isSelected: store.issue.filteredLabels.includes(label.id),
+ })),
+ },
+ ]}
+ />
+ );
+});
diff --git a/apps/space/components/issues/navbar/issue-view.tsx b/space/components/issues/navbar/issue-view.tsx
similarity index 100%
rename from apps/space/components/issues/navbar/issue-view.tsx
rename to space/components/issues/navbar/issue-view.tsx
diff --git a/apps/space/components/issues/navbar/search.tsx b/space/components/issues/navbar/search.tsx
similarity index 100%
rename from apps/space/components/issues/navbar/search.tsx
rename to space/components/issues/navbar/search.tsx
diff --git a/space/components/issues/navbar/theme.tsx b/space/components/issues/navbar/theme.tsx
new file mode 100644
index 000000000..7efb561a4
--- /dev/null
+++ b/space/components/issues/navbar/theme.tsx
@@ -0,0 +1,32 @@
+// next theme
+import { useTheme } from "next-themes";
+
+// mobx react lite
+import { observer } from "mobx-react-lite";
+import { useEffect, useState } from "react";
+
+export const NavbarTheme = observer(() => {
+ const [appTheme, setAppTheme] = useState("light");
+
+ const { setTheme, theme } = useTheme();
+
+ const handleTheme = () => {
+ setTheme(theme === "light" ? "dark" : "light");
+ };
+
+ useEffect(() => {
+ if (!theme) return;
+
+ setAppTheme(theme);
+ }, [theme]);
+
+ return (
+
+ {appTheme === "light" ? "dark_mode" : "light_mode"}
+
+ );
+});
diff --git a/space/components/issues/peek-overview/comment/add-comment.tsx b/space/components/issues/peek-overview/comment/add-comment.tsx
new file mode 100644
index 000000000..3ea4308d7
--- /dev/null
+++ b/space/components/issues/peek-overview/comment/add-comment.tsx
@@ -0,0 +1,104 @@
+import React, { useRef } from "react";
+import { useRouter } from "next/router";
+import { observer } from "mobx-react-lite";
+import { useForm, Controller } from "react-hook-form";
+// lib
+import { useMobxStore } from "lib/mobx/store-provider";
+// hooks
+import useToast from "hooks/use-toast";
+// ui
+import { SecondaryButton } from "components/ui";
+// types
+import { Comment } from "types/issue";
+// components
+import { TipTapEditor } from "components/tiptap";
+
+const defaultValues: Partial = {
+ comment_html: "",
+};
+
+type Props = {
+ disabled?: boolean;
+};
+
+export const AddComment: React.FC = observer((props) => {
+ const { disabled = false } = props;
+
+ const {
+ handleSubmit,
+ control,
+ setValue,
+ watch,
+ formState: { isSubmitting },
+ reset,
+ } = useForm({ defaultValues });
+
+ const router = useRouter();
+ const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string };
+
+ const { user: userStore, issueDetails: issueDetailStore } = useMobxStore();
+
+ const issueId = issueDetailStore.peekId;
+
+ const editorRef = useRef(null);
+
+ const { setToastAlert } = useToast();
+
+ const onSubmit = async (formData: Comment) => {
+ if (!workspace_slug || !project_slug || !issueId || isSubmitting || !formData.comment_html) return;
+
+ await issueDetailStore
+ .addIssueComment(workspace_slug, project_slug, issueId, formData)
+ .then(() => {
+ reset(defaultValues);
+ editorRef.current?.clearEditor();
+ })
+ .catch(() => {
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message: "Comment could not be posted. Please try again.",
+ });
+ });
+ };
+
+ return (
+
+
+ (
+ {
+ onChange(comment_html);
+ }}
+ />
+ )}
+ />
+
+ {
+ userStore.requiredLogin(() => {
+ handleSubmit(onSubmit)(e);
+ });
+ }}
+ type="submit"
+ disabled={isSubmitting || disabled}
+ className="mt-2"
+ >
+ {isSubmitting ? "Adding..." : "Comment"}
+
+
+
+ );
+});
diff --git a/space/components/issues/peek-overview/comment/comment-detail-card.tsx b/space/components/issues/peek-overview/comment/comment-detail-card.tsx
new file mode 100644
index 000000000..0b4d2d5b0
--- /dev/null
+++ b/space/components/issues/peek-overview/comment/comment-detail-card.tsx
@@ -0,0 +1,204 @@
+import React, { useState } from "react";
+
+// mobx
+import { observer } from "mobx-react-lite";
+// react-hook-form
+import { Controller, useForm } from "react-hook-form";
+// headless ui
+import { Menu, Transition } from "@headlessui/react";
+// lib
+import { useMobxStore } from "lib/mobx/store-provider";
+// components
+import { TipTapEditor } from "components/tiptap";
+import { CommentReactions } from "components/issues/peek-overview";
+// icons
+import { ChatBubbleLeftEllipsisIcon, CheckIcon, XMarkIcon, EllipsisVerticalIcon } from "@heroicons/react/24/outline";
+// helpers
+import { timeAgo } from "helpers/date-time.helper";
+// types
+import { Comment } from "types/issue";
+
+type Props = {
+ workspaceSlug: string;
+ comment: Comment;
+};
+
+export const CommentCard: React.FC = observer((props) => {
+ const { comment, workspaceSlug } = props;
+ // store
+ const { user: userStore, issueDetails: issueDetailStore } = useMobxStore();
+ // states
+ const [isEditing, setIsEditing] = useState(false);
+
+ const editorRef = React.useRef(null);
+
+ const showEditorRef = React.useRef(null);
+ const {
+ control,
+ formState: { isSubmitting },
+ handleSubmit,
+ } = useForm({
+ defaultValues: { comment_html: comment.comment_html },
+ });
+
+ const handleDelete = () => {
+ if (!workspaceSlug || !issueDetailStore.peekId) return;
+ issueDetailStore.deleteIssueComment(workspaceSlug, comment.project, issueDetailStore.peekId, comment.id);
+ };
+
+ const handleCommentUpdate = async (formData: Comment) => {
+ if (!workspaceSlug || !issueDetailStore.peekId) return;
+ issueDetailStore.updateIssueComment(workspaceSlug, comment.project, issueDetailStore.peekId, comment.id, formData);
+ setIsEditing(false);
+
+ editorRef.current?.setEditorValue(formData.comment_html);
+ showEditorRef.current?.setEditorValue(formData.comment_html);
+ };
+
+ return (
+
+
+ {comment.actor_detail.avatar && comment.actor_detail.avatar !== "" ? (
+ // eslint-disable-next-line @next/next/no-img-element
+
+ ) : (
+
+ {comment.actor_detail.is_bot
+ ? comment?.actor_detail?.first_name?.charAt(0)
+ : comment?.actor_detail?.display_name?.charAt(0)}
+
+ )}
+
+
+
+
+
+
+
+
+ {comment.actor_detail.is_bot ? comment.actor_detail.first_name + " Bot" : comment.actor_detail.display_name}
+
+
+ <>commented {timeAgo(comment.created_at)}>
+
+
+
+
+
+ {userStore?.currentUser?.id === comment?.actor_detail?.id && (
+
+ {}}
+ className="relative grid place-items-center rounded p-1 text-custom-text-200 hover:text-custom-text-100 outline-none cursor-pointer hover:bg-custom-background-80"
+ >
+
+
+
+
+
+
+ {({ active }) => (
+
+ {
+ setIsEditing(true);
+ }}
+ className={`w-full select-none truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80 ${
+ active ? "bg-custom-background-80" : ""
+ }`}
+ >
+ Edit
+
+
+ )}
+
+
+ {({ active }) => (
+
+
+ Delete
+
+
+ )}
+
+
+
+
+ )}
+
+ );
+});
diff --git a/space/components/issues/peek-overview/comment/comment-reactions.tsx b/space/components/issues/peek-overview/comment/comment-reactions.tsx
new file mode 100644
index 000000000..4045d3edf
--- /dev/null
+++ b/space/components/issues/peek-overview/comment/comment-reactions.tsx
@@ -0,0 +1,131 @@
+import React from "react";
+
+import { useRouter } from "next/router";
+
+// mobx
+import { observer } from "mobx-react-lite";
+import { useMobxStore } from "lib/mobx/store-provider";
+// ui
+import { ReactionSelector, Tooltip } from "components/ui";
+// helpers
+import { groupReactions, renderEmoji } from "helpers/emoji.helper";
+
+type Props = {
+ commentId: string;
+ projectId: string;
+};
+
+export const CommentReactions: React.FC = observer((props) => {
+ const { commentId, projectId } = props;
+
+ const router = useRouter();
+ const { workspace_slug } = router.query;
+
+ const { issueDetails: issueDetailsStore, user: userStore } = useMobxStore();
+
+ const peekId = issueDetailsStore.peekId;
+ const user = userStore.currentUser;
+
+ const commentReactions = peekId
+ ? issueDetailsStore.details[peekId].comments.find((c) => c.id === commentId)?.comment_reactions
+ : [];
+ const groupedReactions = peekId ? groupReactions(commentReactions ?? [], "reaction") : {};
+
+ const userReactions = commentReactions?.filter((r) => r.actor_detail.id === user?.id);
+
+ const handleAddReaction = (reactionHex: string) => {
+ if (!workspace_slug || !projectId || !peekId) return;
+
+ issueDetailsStore.addCommentReaction(
+ workspace_slug.toString(),
+ projectId.toString(),
+ peekId,
+ commentId,
+ reactionHex
+ );
+ };
+
+ const handleRemoveReaction = (reactionHex: string) => {
+ if (!workspace_slug || !projectId || !peekId) return;
+
+ issueDetailsStore.removeCommentReaction(
+ workspace_slug.toString(),
+ projectId.toString(),
+ peekId,
+ commentId,
+ reactionHex
+ );
+ };
+
+ const handleReactionClick = (reactionHex: string) => {
+ const userReaction = userReactions?.find((r) => r.actor_detail.id === user?.id && r.reaction === reactionHex);
+
+ if (userReaction) handleRemoveReaction(reactionHex);
+ else handleAddReaction(reactionHex);
+ };
+
+ return (
+
+ {
+ userStore.requiredLogin(() => {
+ handleReactionClick(value);
+ });
+ }}
+ position="top"
+ selected={userReactions?.map((r) => r.reaction)}
+ size="md"
+ />
+
+ {Object.keys(groupedReactions || {}).map((reaction) => {
+ const reactions = groupedReactions?.[reaction] ?? [];
+ const REACTIONS_LIMIT = 1000;
+
+ if (reactions.length > 0)
+ return (
+
+ {reactions
+ .map((r) => r.actor_detail.display_name)
+ .splice(0, REACTIONS_LIMIT)
+ .join(", ")}
+ {reactions.length > REACTIONS_LIMIT && " and " + (reactions.length - REACTIONS_LIMIT) + " more"}
+
+ }
+ >
+ {
+ userStore.requiredLogin(() => {
+ handleReactionClick(reaction);
+ });
+ }}
+ className={`flex items-center gap-1 text-custom-text-100 text-sm h-full px-2 py-1 rounded-md ${
+ commentReactions?.some(
+ (r) => r.actor_detail.id === userStore.currentUser?.id && r.reaction === reaction
+ )
+ ? "bg-custom-primary-100/10"
+ : "bg-custom-background-80"
+ }`}
+ >
+ {renderEmoji(reaction)}
+ r.actor_detail.id === userStore.currentUser?.id && r.reaction === reaction
+ )
+ ? "text-custom-primary-100"
+ : ""
+ }
+ >
+ {groupedReactions?.[reaction].length}{" "}
+
+
+
+ );
+ })}
+
+ );
+});
diff --git a/space/components/issues/peek-overview/comment/index.ts b/space/components/issues/peek-overview/comment/index.ts
new file mode 100644
index 000000000..d217c0ddf
--- /dev/null
+++ b/space/components/issues/peek-overview/comment/index.ts
@@ -0,0 +1,3 @@
+export * from "./add-comment";
+export * from "./comment-detail-card";
+export * from "./comment-reactions";
diff --git a/space/components/issues/peek-overview/full-screen-peek-view.tsx b/space/components/issues/peek-overview/full-screen-peek-view.tsx
new file mode 100644
index 000000000..a40e6b16a
--- /dev/null
+++ b/space/components/issues/peek-overview/full-screen-peek-view.tsx
@@ -0,0 +1,71 @@
+import { useEffect } from "react";
+import { observer } from "mobx-react-lite";
+// lib
+import { useMobxStore } from "lib/mobx/store-provider";
+// components
+import {
+ PeekOverviewHeader,
+ PeekOverviewIssueActivity,
+ PeekOverviewIssueDetails,
+ PeekOverviewIssueProperties,
+} from "components/issues/peek-overview";
+// types
+import { Loader } from "components/ui/loader";
+import { IIssue } from "types/issue";
+
+type Props = {
+ handleClose: () => void;
+ issueDetails: IIssue | undefined;
+};
+
+export const FullScreenPeekView: React.FC
= observer((props) => {
+ const { handleClose, issueDetails } = props;
+
+ return (
+
+
+
+ {issueDetails ? (
+
+ {/* issue title and description */}
+
+ {/* divider */}
+
+ {/* issue activity/comments */}
+
+
+ ) : (
+
+
+
+
+
+
+
+
+ )}
+
+
+ {/* issue properties */}
+
+ {issueDetails ? (
+
+ ) : (
+
+
+
+
+
+
+ )}
+
+
+
+ );
+});
diff --git a/space/components/issues/peek-overview/header.tsx b/space/components/issues/peek-overview/header.tsx
new file mode 100644
index 000000000..7a0b43b98
--- /dev/null
+++ b/space/components/issues/peek-overview/header.tsx
@@ -0,0 +1,142 @@
+import React from "react";
+
+// mobx
+import { observer } from "mobx-react-lite";
+// headless ui
+import { Listbox, Transition } from "@headlessui/react";
+// hooks
+import useToast from "hooks/use-toast";
+// ui
+import { Icon } from "components/ui";
+// icons
+import { East } from "@mui/icons-material";
+// helpers
+import { copyTextToClipboard } from "helpers/string.helper";
+// store
+import { IPeekMode } from "store/issue_details";
+import { RootStore } from "store/root";
+// lib
+import { useMobxStore } from "lib/mobx/store-provider";
+// types
+import { IIssue } from "types/issue";
+
+type Props = {
+ handleClose: () => void;
+ issueDetails: IIssue | undefined;
+};
+
+const peekModes: {
+ key: IPeekMode;
+ icon: string;
+ label: string;
+}[] = [
+ { key: "side", icon: "side_navigation", label: "Side Peek" },
+ {
+ key: "modal",
+ icon: "dialogs",
+ label: "Modal Peek",
+ },
+ {
+ key: "full",
+ icon: "nearby",
+ label: "Full Screen Peek",
+ },
+];
+
+export const PeekOverviewHeader: React.FC = observer((props) => {
+ const { handleClose, issueDetails } = props;
+
+ const { issueDetails: issueDetailStore }: RootStore = useMobxStore();
+
+ const { setToastAlert } = useToast();
+
+ const handleCopyLink = () => {
+ const urlToCopy = window.location.href;
+
+ copyTextToClipboard(urlToCopy).then(() => {
+ setToastAlert({
+ type: "success",
+ title: "Link copied!",
+ message: "Issue link copied to clipboard",
+ });
+ });
+ };
+
+ return (
+ <>
+
+
+ {issueDetailStore.peekMode === "side" && (
+
+
+
+ )}
+
issueDetailStore.setPeekMode(val)}
+ className="relative flex-shrink-0 text-left"
+ >
+
+ m.key === issueDetailStore.peekMode)?.icon ?? ""} />
+
+
+
+
+
+ {peekModes.map((mode) => (
+
+ `cursor-pointer select-none truncate rounded px-1 py-1.5 ${
+ active || selected ? "bg-custom-background-80" : ""
+ } ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
+ }
+ >
+ {({ selected }) => (
+
+ )}
+
+ ))}
+
+
+
+
+
+ {(issueDetailStore.peekMode === "side" || issueDetailStore.peekMode === "modal") && (
+
+
+
+
+
+ )}
+
+ >
+ );
+});
diff --git a/space/components/issues/peek-overview/index.ts b/space/components/issues/peek-overview/index.ts
new file mode 100644
index 000000000..f42253e5e
--- /dev/null
+++ b/space/components/issues/peek-overview/index.ts
@@ -0,0 +1,11 @@
+export * from "./comment";
+export * from "./full-screen-peek-view";
+export * from "./header";
+export * from "./issue-activity";
+export * from "./issue-details";
+export * from "./issue-properties";
+export * from "./layout";
+export * from "./side-peek-view";
+export * from "./issue-reaction";
+export * from "./issue-vote-reactions";
+export * from "./issue-emoji-reactions";
diff --git a/space/components/issues/peek-overview/issue-activity.tsx b/space/components/issues/peek-overview/issue-activity.tsx
new file mode 100644
index 000000000..fde2fd878
--- /dev/null
+++ b/space/components/issues/peek-overview/issue-activity.tsx
@@ -0,0 +1,66 @@
+import React from "react";
+
+import Link from "next/link";
+import { useRouter } from "next/router";
+
+// mobx
+import { observer } from "mobx-react-lite";
+// lib
+import { useMobxStore } from "lib/mobx/store-provider";
+// components
+import { CommentCard, AddComment } from "components/issues/peek-overview";
+// ui
+import { Icon, PrimaryButton } from "components/ui";
+// types
+import { IIssue } from "types/issue";
+
+type Props = {
+ issueDetails: IIssue;
+};
+
+export const PeekOverviewIssueActivity: React.FC = observer((props) => {
+ const router = useRouter();
+ const { workspace_slug } = router.query;
+
+ const { issueDetails: issueDetailStore, project: projectStore, user: userStore } = useMobxStore();
+
+ const comments = issueDetailStore.details[issueDetailStore.peekId || ""]?.comments || [];
+
+ const user = userStore?.currentUser;
+
+ return (
+
+
Activity
+ {workspace_slug && (
+
+
+ {comments.map((comment: any) => (
+
+ ))}
+
+ {user ? (
+ <>
+ {projectStore.deploySettings?.comments && (
+
+ )}
+ >
+ ) : (
+
+
+
+ Sign in to add your comment
+
+
+
+ Sign in
+
+
+
+ )}
+
+ )}
+
+ );
+});
diff --git a/space/components/issues/peek-overview/issue-details.tsx b/space/components/issues/peek-overview/issue-details.tsx
new file mode 100644
index 000000000..0b329568c
--- /dev/null
+++ b/space/components/issues/peek-overview/issue-details.tsx
@@ -0,0 +1,40 @@
+import { IssueReactions } from "components/issues/peek-overview";
+import { TipTapEditor } from "components/tiptap";
+import { useRouter } from "next/router";
+// types
+import { IIssue } from "types/issue";
+
+type Props = {
+ issueDetails: IIssue;
+};
+
+export const PeekOverviewIssueDetails: React.FC = ({ issueDetails }) => {
+ const router = useRouter();
+ const { workspace_slug } = router.query;
+
+ return (
+
+
+ {issueDetails.project_detail.identifier}-{issueDetails.sequence_id}
+
+
{issueDetails.name}
+ {issueDetails.description_html !== "" && issueDetails.description_html !== "
" && (
+
"
+ : issueDetails.description_html
+ }
+ customClassName="p-3 min-h-[50px] shadow-sm"
+ debouncedUpdatesEnabled={false}
+ editable={false}
+ />
+ )}
+
+
+ );
+};
diff --git a/space/components/issues/peek-overview/issue-emoji-reactions.tsx b/space/components/issues/peek-overview/issue-emoji-reactions.tsx
new file mode 100644
index 000000000..b0c5b0361
--- /dev/null
+++ b/space/components/issues/peek-overview/issue-emoji-reactions.tsx
@@ -0,0 +1,109 @@
+import { useEffect } from "react";
+import { useRouter } from "next/router";
+import { observer } from "mobx-react-lite";
+// lib
+import { useMobxStore } from "lib/mobx/store-provider";
+// helpers
+import { groupReactions, renderEmoji } from "helpers/emoji.helper";
+// components
+import { ReactionSelector, Tooltip } from "components/ui";
+
+export const IssueEmojiReactions: React.FC = observer(() => {
+ // router
+ const router = useRouter();
+ const { workspace_slug, project_slug } = router.query;
+ // store
+ const { user: userStore, issueDetails: issueDetailsStore } = useMobxStore();
+
+ const user = userStore?.currentUser;
+ const issueId = issueDetailsStore.peekId;
+ const reactions = issueId ? issueDetailsStore.details[issueId]?.reactions || [] : [];
+ const groupedReactions = groupReactions(reactions, "reaction");
+
+ const userReactions = reactions?.filter((r) => r.actor_detail.id === user?.id);
+
+ const handleAddReaction = (reactionHex: string) => {
+ if (!workspace_slug || !project_slug || !issueId) return;
+
+ issueDetailsStore.addIssueReaction(workspace_slug.toString(), project_slug.toString(), issueId, reactionHex);
+ };
+
+ const handleRemoveReaction = (reactionHex: string) => {
+ if (!workspace_slug || !project_slug || !issueId) return;
+
+ issueDetailsStore.removeIssueReaction(workspace_slug.toString(), project_slug.toString(), issueId, reactionHex);
+ };
+
+ const handleReactionClick = (reactionHex: string) => {
+ const userReaction = userReactions?.find((r) => r.actor_detail.id === user?.id && r.reaction === reactionHex);
+
+ if (userReaction) handleRemoveReaction(reactionHex);
+ else handleAddReaction(reactionHex);
+ };
+
+ useEffect(() => {
+ if (user) return;
+ userStore.fetchCurrentUser();
+ }, [user, userStore]);
+
+ return (
+ <>
+ {
+ userStore.requiredLogin(() => {
+ handleReactionClick(value);
+ });
+ }}
+ selected={userReactions?.map((r) => r.reaction)}
+ size="md"
+ />
+
+ {Object.keys(groupedReactions || {}).map((reaction) => {
+ const reactions = groupedReactions?.[reaction] ?? [];
+ const REACTIONS_LIMIT = 1000;
+
+ if (reactions.length > 0)
+ return (
+
+ {reactions
+ .map((r) => r.actor_detail.display_name)
+ .splice(0, REACTIONS_LIMIT)
+ .join(", ")}
+ {reactions.length > REACTIONS_LIMIT && " and " + (reactions.length - REACTIONS_LIMIT) + " more"}
+
+ }
+ >
+ {
+ userStore.requiredLogin(() => {
+ handleReactionClick(reaction);
+ });
+ }}
+ className={`flex items-center gap-1 text-custom-text-100 text-sm h-full px-2 py-1 rounded-md ${
+ reactions?.some((r) => r.actor_detail.id === user?.id && r.reaction === reaction)
+ ? "bg-custom-primary-100/10"
+ : "bg-custom-background-80"
+ }`}
+ >
+ {renderEmoji(reaction)}
+ r.actor_detail.id === user?.id && r.reaction === reaction)
+ ? "text-custom-primary-100"
+ : ""
+ }
+ >
+ {groupedReactions?.[reaction].length}{" "}
+
+
+
+ );
+ })}
+
+ >
+ );
+});
diff --git a/space/components/issues/peek-overview/issue-properties.tsx b/space/components/issues/peek-overview/issue-properties.tsx
new file mode 100644
index 000000000..f7ccab18f
--- /dev/null
+++ b/space/components/issues/peek-overview/issue-properties.tsx
@@ -0,0 +1,123 @@
+// hooks
+import useToast from "hooks/use-toast";
+// icons
+import { Icon } from "components/ui";
+// helpers
+import { copyTextToClipboard, addSpaceIfCamelCase } from "helpers/string.helper";
+import { renderFullDate } from "helpers/date-time.helper";
+import { dueDateIconDetails } from "../board-views/block-due-date";
+// types
+import { IIssue } from "types/issue";
+import { IPeekMode } from "store/issue_details";
+// constants
+import { issueGroupFilter, issuePriorityFilter } from "constants/data";
+
+type Props = {
+ issueDetails: IIssue;
+ mode?: IPeekMode;
+};
+
+export const PeekOverviewIssueProperties: React.FC
= ({ issueDetails, mode }) => {
+ const { setToastAlert } = useToast();
+
+ const state = issueDetails.state_detail;
+ const stateGroup = issueGroupFilter(state.group);
+
+ const priority = issueDetails.priority ? issuePriorityFilter(issueDetails.priority) : null;
+
+ const dueDateIcon = dueDateIconDetails(issueDetails.target_date, state.group);
+
+ const handleCopyLink = () => {
+ const urlToCopy = window.location.href;
+
+ copyTextToClipboard(urlToCopy).then(() => {
+ setToastAlert({
+ type: "success",
+ title: "Link copied!",
+ message: "Issue link copied to clipboard",
+ });
+ });
+ };
+
+ return (
+
+ {mode === "full" && (
+
+
+ {issueDetails.project_detail.identifier}-{issueDetails.sequence_id}
+
+
+
+
+
+
+
+ )}
+
+
+
+
+ State
+
+
+ {stateGroup && (
+
+
+
+ {addSpaceIfCamelCase(state?.name ?? "")}
+
+
+ )}
+
+
+
+
+
+
+ Priority
+
+
+
+ {priority && (
+
+
+
+ )}
+ {priority?.title ?? "None"}
+
+
+
+
+
+
+ Due date
+
+
+ {issueDetails.target_date ? (
+
+
+ {dueDateIcon.iconName}
+
+ {renderFullDate(issueDetails.target_date)}
+
+ ) : (
+
Empty
+ )}
+
+
+
+
+ );
+};
diff --git a/space/components/issues/peek-overview/issue-reaction.tsx b/space/components/issues/peek-overview/issue-reaction.tsx
new file mode 100644
index 000000000..643ec7119
--- /dev/null
+++ b/space/components/issues/peek-overview/issue-reaction.tsx
@@ -0,0 +1,24 @@
+import { IssueEmojiReactions, IssueVotes } from "components/issues/peek-overview";
+import { useMobxStore } from "lib/mobx/store-provider";
+
+export const IssueReactions: React.FC = () => {
+ const { project: projectStore } = useMobxStore();
+
+ return (
+
+ {projectStore?.deploySettings?.votes && (
+ <>
+
+
+
+
+ >
+ )}
+ {projectStore?.deploySettings?.reactions && (
+
+
+
+ )}
+
+ );
+};
diff --git a/space/components/issues/peek-overview/issue-vote-reactions.tsx b/space/components/issues/peek-overview/issue-vote-reactions.tsx
new file mode 100644
index 000000000..ac20565ea
--- /dev/null
+++ b/space/components/issues/peek-overview/issue-vote-reactions.tsx
@@ -0,0 +1,129 @@
+import { useState, useEffect } from "react";
+
+import { useRouter } from "next/router";
+
+// mobx
+import { observer } from "mobx-react-lite";
+// lib
+import { useMobxStore } from "lib/mobx/store-provider";
+import { Tooltip } from "components/ui";
+
+export const IssueVotes: React.FC = observer(() => {
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const router = useRouter();
+
+ const { workspace_slug, project_slug } = router.query;
+
+ const { user: userStore, issueDetails: issueDetailsStore } = useMobxStore();
+
+ const user = userStore?.currentUser;
+ const issueId = issueDetailsStore.peekId;
+
+ const votes = issueId ? issueDetailsStore.details[issueId]?.votes : [];
+
+ const allUpVotes = votes?.filter((vote) => vote.vote === 1);
+ const allDownVotes = votes?.filter((vote) => vote.vote === -1);
+
+ const isUpVotedByUser = allUpVotes?.some((vote) => vote.actor === user?.id);
+ const isDownVotedByUser = allDownVotes?.some((vote) => vote.actor === user?.id);
+
+ const handleVote = async (e: any, voteValue: 1 | -1) => {
+ if (!workspace_slug || !project_slug || !issueId) return;
+
+ setIsSubmitting(true);
+
+ const actionPerformed = votes?.find((vote) => vote.actor === user?.id && vote.vote === voteValue);
+
+ if (actionPerformed)
+ await issueDetailsStore.removeIssueVote(workspace_slug.toString(), project_slug.toString(), issueId);
+ else
+ await issueDetailsStore.addIssueVote(workspace_slug.toString(), project_slug.toString(), issueId, {
+ vote: voteValue,
+ });
+
+ setIsSubmitting(false);
+ };
+
+ useEffect(() => {
+ if (user) return;
+
+ userStore.fetchCurrentUser();
+ }, [user, userStore]);
+
+ const VOTES_LIMIT = 1000;
+
+ return (
+
+ {/* upvote button 👇 */}
+
+ {allUpVotes.length > 0 ? (
+ <>
+ {allUpVotes
+ .map((r) => r.actor_detail.display_name)
+ .splice(0, VOTES_LIMIT)
+ .join(", ")}
+ {allUpVotes.length > VOTES_LIMIT && " and " + (allUpVotes.length - VOTES_LIMIT) + " more"}
+ >
+ ) : (
+ "No upvotes yet"
+ )}
+
+ }
+ >
+ {
+ userStore.requiredLogin(() => {
+ handleVote(e, 1);
+ });
+ }}
+ className={`flex items-center justify-center overflow-hidden px-2 gap-x-1 border rounded focus:outline-none ${
+ isUpVotedByUser ? "border-custom-primary-200 text-custom-primary-200" : "border-custom-border-300"
+ }`}
+ >
+ arrow_upward_alt
+ {allUpVotes.length}
+
+
+
+ {/* downvote button 👇 */}
+
+ {allDownVotes.length > 0 ? (
+ <>
+ {allDownVotes
+ .map((r) => r.actor_detail.display_name)
+ .splice(0, VOTES_LIMIT)
+ .join(", ")}
+ {allDownVotes.length > VOTES_LIMIT && " and " + (allDownVotes.length - VOTES_LIMIT) + " more"}
+ >
+ ) : (
+ "No downvotes yet"
+ )}
+
+ }
+ >
+
{
+ userStore.requiredLogin(() => {
+ handleVote(e, -1);
+ });
+ }}
+ className={`flex items-center justify-center overflow-hidden px-2 gap-x-1 border rounded focus:outline-none ${
+ isDownVotedByUser ? "border-red-600 text-red-600" : "border-custom-border-300"
+ }`}
+ >
+ arrow_downward_alt
+ {allDownVotes.length}
+
+
+
+ );
+});
diff --git a/space/components/issues/peek-overview/layout.tsx b/space/components/issues/peek-overview/layout.tsx
new file mode 100644
index 000000000..a3d7386eb
--- /dev/null
+++ b/space/components/issues/peek-overview/layout.tsx
@@ -0,0 +1,125 @@
+import React, { useEffect, useState } from "react";
+
+import { useRouter } from "next/router";
+
+// mobx
+import { observer } from "mobx-react-lite";
+// headless ui
+import { Dialog, Transition } from "@headlessui/react";
+// components
+import { FullScreenPeekView, SidePeekView } from "components/issues/peek-overview";
+// lib
+import { useMobxStore } from "lib/mobx/store-provider";
+
+type Props = {};
+
+export const IssuePeekOverview: React.FC
= observer((props) => {
+ const [isSidePeekOpen, setIsSidePeekOpen] = useState(false);
+ const [isModalPeekOpen, setIsModalPeekOpen] = useState(false);
+
+ // router
+ const router = useRouter();
+ const { workspace_slug, project_slug, peekId, board } = router.query;
+ // store
+ const { issueDetails: issueDetailStore, issue: issueStore } = useMobxStore();
+
+ const issueDetails = issueDetailStore.peekId && peekId ? issueDetailStore.details[peekId.toString()] : undefined;
+
+ useEffect(() => {
+ if (workspace_slug && project_slug && peekId && issueStore.issues && issueStore.issues.length > 0) {
+ if (!issueDetails) {
+ issueDetailStore.fetchIssueDetails(workspace_slug.toString(), project_slug.toString(), peekId.toString());
+ }
+ }
+ }, [workspace_slug, project_slug, issueDetailStore, issueDetails, peekId, issueStore.issues]);
+
+ const handleClose = () => {
+ issueDetailStore.setPeekId(null);
+ router.replace(
+ {
+ pathname: `/${workspace_slug?.toString()}/${project_slug}`,
+ query: {
+ board,
+ },
+ },
+ undefined,
+ { shallow: true }
+ );
+ };
+
+ useEffect(() => {
+ if (peekId) {
+ if (issueDetailStore.peekMode === "side") {
+ setIsSidePeekOpen(true);
+ setIsModalPeekOpen(false);
+ } else {
+ setIsModalPeekOpen(true);
+ setIsSidePeekOpen(false);
+ }
+ } else {
+ setIsSidePeekOpen(false);
+ setIsModalPeekOpen(false);
+ }
+ }, [peekId, issueDetailStore.peekMode]);
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {issueDetailStore.peekMode === "modal" && (
+
+ )}
+ {issueDetailStore.peekMode === "full" && (
+
+ )}
+
+
+
+
+
+ >
+ );
+});
diff --git a/space/components/issues/peek-overview/side-peek-view.tsx b/space/components/issues/peek-overview/side-peek-view.tsx
new file mode 100644
index 000000000..bacf83420
--- /dev/null
+++ b/space/components/issues/peek-overview/side-peek-view.tsx
@@ -0,0 +1,55 @@
+import { observer } from "mobx-react-lite";
+// components
+import {
+ PeekOverviewHeader,
+ PeekOverviewIssueActivity,
+ PeekOverviewIssueDetails,
+ PeekOverviewIssueProperties,
+} from "components/issues/peek-overview";
+
+import { Loader } from "components/ui/loader";
+import { IIssue } from "types/issue";
+
+type Props = {
+ handleClose: () => void;
+ issueDetails: IIssue | undefined;
+};
+
+export const SidePeekView: React.FC = observer((props) => {
+ const { handleClose, issueDetails } = props;
+
+ return (
+
+
+ {issueDetails ? (
+
+ {/* issue title and description */}
+
+ {/* issue properties */}
+
+ {/* divider */}
+
+ {/* issue activity/comments */}
+
+
+ ) : (
+
+
+
+
+
+
+
+
+ )}
+
+ );
+});
diff --git a/apps/app/components/tiptap/bubble-menu/index.tsx b/space/components/tiptap/bubble-menu/index.tsx
similarity index 91%
rename from apps/app/components/tiptap/bubble-menu/index.tsx
rename to space/components/tiptap/bubble-menu/index.tsx
index e68900782..217317ea1 100644
--- a/apps/app/components/tiptap/bubble-menu/index.tsx
+++ b/space/components/tiptap/bubble-menu/index.tsx
@@ -77,14 +77,16 @@ export const EditorBubbleMenu: FC = (props: any) => {
{...bubbleMenuProps}
className="flex w-fit divide-x divide-custom-border-300 rounded border border-custom-border-300 bg-custom-background-100 shadow-xl"
>
- {
- setIsNodeSelectorOpen(!isNodeSelectorOpen);
- setIsLinkSelectorOpen(false);
- }}
- />
+ {!props.editor.isActive("table") && (
+ {
+ setIsNodeSelectorOpen(!isNodeSelectorOpen);
+ setIsLinkSelectorOpen(false);
+ }}
+ />
+ )}
>;
}
-
export const LinkSelector: FC = ({ editor, isOpen, setIsOpen }) => {
const inputRef = useRef(null);
@@ -52,7 +51,8 @@ export const LinkSelector: FC = ({ editor, isOpen, setIsOpen
className="fixed top-full z-[99999] mt-1 flex w-60 overflow-hidden rounded border border-custom-border-300 bg-custom-background-100 dow-xl animate-in fade-in slide-in-from-top-1"
onKeyDown={(e) => {
if (e.key === "Enter") {
- e.preventDefault(); onLinkSubmit();
+ e.preventDefault();
+ onLinkSubmit();
}
}}
>
@@ -75,7 +75,9 @@ export const LinkSelector: FC = ({ editor, isOpen, setIsOpen
) : (
- {
onLinkSubmit();
}}
diff --git a/apps/app/components/tiptap/bubble-menu/node-selector.tsx b/space/components/tiptap/bubble-menu/node-selector.tsx
similarity index 98%
rename from apps/app/components/tiptap/bubble-menu/node-selector.tsx
rename to space/components/tiptap/bubble-menu/node-selector.tsx
index f6f1f18dc..34d40ec06 100644
--- a/apps/app/components/tiptap/bubble-menu/node-selector.tsx
+++ b/space/components/tiptap/bubble-menu/node-selector.tsx
@@ -13,7 +13,7 @@ import {
} from "lucide-react";
import { Dispatch, FC, SetStateAction } from "react";
-import { BubbleMenuItem } from "../bubble-menu";
+import { BubbleMenuItem } from ".";
import { cn } from "../utils";
interface NodeSelectorProps {
diff --git a/apps/app/components/tiptap/bubble-menu/utils/link-validator.tsx b/space/components/tiptap/bubble-menu/utils/link-validator.tsx
similarity index 99%
rename from apps/app/components/tiptap/bubble-menu/utils/link-validator.tsx
rename to space/components/tiptap/bubble-menu/utils/link-validator.tsx
index 5b05811d6..9af366c02 100644
--- a/apps/app/components/tiptap/bubble-menu/utils/link-validator.tsx
+++ b/space/components/tiptap/bubble-menu/utils/link-validator.tsx
@@ -9,4 +9,3 @@ export default function isValidHttpUrl(string: string): boolean {
return url.protocol === "http:" || url.protocol === "https:";
}
-
diff --git a/apps/app/components/tiptap/extensions/image-resize.tsx b/space/components/tiptap/extensions/image-resize.tsx
similarity index 78%
rename from apps/app/components/tiptap/extensions/image-resize.tsx
rename to space/components/tiptap/extensions/image-resize.tsx
index 7b2d1a2d3..448b8811c 100644
--- a/apps/app/components/tiptap/extensions/image-resize.tsx
+++ b/space/components/tiptap/extensions/image-resize.tsx
@@ -3,9 +3,7 @@ import Moveable from "react-moveable";
export const ImageResizer = ({ editor }: { editor: Editor }) => {
const updateMediaSize = () => {
- const imageInfo = document.querySelector(
- ".ProseMirror-selectednode",
- ) as HTMLImageElement;
+ const imageInfo = document.querySelector(".ProseMirror-selectednode") as HTMLImageElement;
if (imageInfo) {
const selection = editor.state.selection;
editor.commands.setImage({
@@ -28,13 +26,7 @@ export const ImageResizer = ({ editor }: { editor: Editor }) => {
keepRatio={true}
resizable={true}
throttleResize={0}
- onResize={({
- target,
- width,
- height,
- delta,
- }:
- any) => {
+ onResize={({ target, width, height, delta }: any) => {
delta[0] && (target!.style.width = `${width}px`);
delta[1] && (target!.style.height = `${height}px`);
}}
@@ -43,15 +35,10 @@ export const ImageResizer = ({ editor }: { editor: Editor }) => {
}}
scalable={true}
renderDirections={["w", "e"]}
- onScale={({
- target,
- transform,
- }:
- any) => {
+ onScale={({ target, transform }: any) => {
target!.style.transform = transform;
}}
/>
>
);
};
-
diff --git a/space/components/tiptap/extensions/index.tsx b/space/components/tiptap/extensions/index.tsx
new file mode 100644
index 000000000..f5dc11384
--- /dev/null
+++ b/space/components/tiptap/extensions/index.tsx
@@ -0,0 +1,153 @@
+import StarterKit from "@tiptap/starter-kit";
+import HorizontalRule from "@tiptap/extension-horizontal-rule";
+import TiptapLink from "@tiptap/extension-link";
+import Placeholder from "@tiptap/extension-placeholder";
+import TiptapUnderline from "@tiptap/extension-underline";
+import TextStyle from "@tiptap/extension-text-style";
+import { Color } from "@tiptap/extension-color";
+import TaskItem from "@tiptap/extension-task-item";
+import TaskList from "@tiptap/extension-task-list";
+import { Markdown } from "tiptap-markdown";
+import Highlight from "@tiptap/extension-highlight";
+import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
+import { lowlight } from "lowlight/lib/core";
+import SlashCommand from "../slash-command";
+import { InputRule } from "@tiptap/core";
+import Gapcursor from "@tiptap/extension-gapcursor";
+
+import ts from "highlight.js/lib/languages/typescript";
+
+import "highlight.js/styles/github-dark.css";
+import UniqueID from "@tiptap-pro/extension-unique-id";
+import UpdatedImage from "./updated-image";
+import isValidHttpUrl from "../bubble-menu/utils/link-validator";
+import { CustomTableCell } from "./table/table-cell";
+import { Table } from "./table/table";
+import { TableHeader } from "./table/table-header";
+import { TableRow } from "@tiptap/extension-table-row";
+
+lowlight.registerLanguage("ts", ts);
+
+export const TiptapExtensions = (
+ workspaceSlug: string,
+ setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
+) => [
+ StarterKit.configure({
+ bulletList: {
+ HTMLAttributes: {
+ class: "list-disc list-outside leading-3 -mt-2",
+ },
+ },
+ orderedList: {
+ HTMLAttributes: {
+ class: "list-decimal list-outside leading-3 -mt-2",
+ },
+ },
+ listItem: {
+ HTMLAttributes: {
+ class: "leading-normal -mb-2",
+ },
+ },
+ blockquote: {
+ HTMLAttributes: {
+ class: "border-l-4 border-custom-border-300",
+ },
+ },
+ code: {
+ HTMLAttributes: {
+ class:
+ "rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000",
+ spellcheck: "false",
+ },
+ },
+ codeBlock: false,
+ horizontalRule: false,
+ dropcursor: {
+ color: "rgba(var(--color-text-100))",
+ width: 2,
+ },
+ gapcursor: false,
+ }),
+ CodeBlockLowlight.configure({
+ lowlight,
+ }),
+ HorizontalRule.extend({
+ addInputRules() {
+ return [
+ new InputRule({
+ find: /^(?:---|—-|___\s|\*\*\*\s)$/,
+ handler: ({ state, range, commands }) => {
+ commands.splitBlock();
+
+ const attributes = {};
+ const { tr } = state;
+ const start = range.from;
+ const end = range.to;
+ // @ts-ignore
+ tr.replaceWith(start - 1, end, this.type.create(attributes));
+ },
+ }),
+ ];
+ },
+ }).configure({
+ HTMLAttributes: {
+ class: "mb-6 border-t border-custom-border-300",
+ },
+ }),
+ Gapcursor,
+ TiptapLink.configure({
+ protocols: ["http", "https"],
+ validate: (url) => isValidHttpUrl(url),
+ HTMLAttributes: {
+ class:
+ "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer",
+ },
+ }),
+ UpdatedImage.configure({
+ HTMLAttributes: {
+ class: "rounded-lg border border-custom-border-300",
+ },
+ }),
+ Placeholder.configure({
+ placeholder: ({ node }) => {
+ if (node.type.name === "heading") {
+ return `Heading ${node.attrs.level}`;
+ }
+ if (node.type.name === "image" || node.type.name === "table") {
+ return "";
+ }
+
+ return "Press '/' for commands...";
+ },
+ includeChildren: true,
+ }),
+ UniqueID.configure({
+ types: ["image"],
+ }),
+ SlashCommand(workspaceSlug, setIsSubmitting),
+ TiptapUnderline,
+ TextStyle,
+ Color,
+ Highlight.configure({
+ multicolor: true,
+ }),
+ TaskList.configure({
+ HTMLAttributes: {
+ class: "not-prose pl-2",
+ },
+ }),
+ TaskItem.configure({
+ HTMLAttributes: {
+ class: "flex items-start my-4",
+ },
+ nested: true,
+ }),
+ Markdown.configure({
+ html: true,
+ transformCopiedText: true,
+ }),
+ Table,
+ TableHeader,
+ CustomTableCell,
+ TableRow,
+ ];
diff --git a/space/components/tiptap/extensions/table/table-cell.ts b/space/components/tiptap/extensions/table/table-cell.ts
new file mode 100644
index 000000000..643cb8c64
--- /dev/null
+++ b/space/components/tiptap/extensions/table/table-cell.ts
@@ -0,0 +1,32 @@
+import { TableCell } from "@tiptap/extension-table-cell";
+
+export const CustomTableCell = TableCell.extend({
+ addAttributes() {
+ return {
+ ...this.parent?.(),
+ isHeader: {
+ default: false,
+ parseHTML: (element) => {
+ isHeader: element.tagName === "TD";
+ },
+ renderHTML: (attributes) => {
+ tag: attributes.isHeader ? "th" : "td";
+ },
+ },
+ };
+ },
+ renderHTML({ HTMLAttributes }) {
+ if (HTMLAttributes.isHeader) {
+ return [
+ "th",
+ {
+ ...HTMLAttributes,
+ class: `relative ${HTMLAttributes.class}`,
+ },
+ ["span", { class: "absolute top-0 right-0" }],
+ 0,
+ ];
+ }
+ return ["td", HTMLAttributes, 0];
+ },
+});
diff --git a/space/components/tiptap/extensions/table/table-header.ts b/space/components/tiptap/extensions/table/table-header.ts
new file mode 100644
index 000000000..f23aa93ef
--- /dev/null
+++ b/space/components/tiptap/extensions/table/table-header.ts
@@ -0,0 +1,7 @@
+import { TableHeader as BaseTableHeader } from "@tiptap/extension-table-header";
+
+const TableHeader = BaseTableHeader.extend({
+ content: "paragraph",
+});
+
+export { TableHeader };
diff --git a/space/components/tiptap/extensions/table/table.ts b/space/components/tiptap/extensions/table/table.ts
new file mode 100644
index 000000000..9b727bb51
--- /dev/null
+++ b/space/components/tiptap/extensions/table/table.ts
@@ -0,0 +1,9 @@
+import { Table as BaseTable } from "@tiptap/extension-table";
+
+const Table = BaseTable.configure({
+ resizable: true,
+ cellMinWidth: 100,
+ allowTableNodeSelection: true,
+});
+
+export { Table };
diff --git a/apps/app/components/tiptap/extensions/updated-image.tsx b/space/components/tiptap/extensions/updated-image.tsx
similarity index 95%
rename from apps/app/components/tiptap/extensions/updated-image.tsx
rename to space/components/tiptap/extensions/updated-image.tsx
index 01648dcd7..b62050953 100644
--- a/apps/app/components/tiptap/extensions/updated-image.tsx
+++ b/space/components/tiptap/extensions/updated-image.tsx
@@ -10,7 +10,7 @@ const UpdatedImage = Image.extend({
return {
...this.parent?.(),
width: {
- default: '35%',
+ default: "35%",
},
height: {
default: null,
diff --git a/apps/app/components/tiptap/index.tsx b/space/components/tiptap/index.tsx
similarity index 91%
rename from apps/app/components/tiptap/index.tsx
rename to space/components/tiptap/index.tsx
index f0315cad4..84f691c35 100644
--- a/apps/app/components/tiptap/index.tsx
+++ b/space/components/tiptap/index.tsx
@@ -1,10 +1,12 @@
+import { useImperativeHandle, useRef, forwardRef, useEffect } from "react";
import { useEditor, EditorContent, Editor } from "@tiptap/react";
import { useDebouncedCallback } from "use-debounce";
+// components
import { EditorBubbleMenu } from "./bubble-menu";
import { TiptapExtensions } from "./extensions";
import { TiptapEditorProps } from "./props";
-import { useImperativeHandle, useRef, forwardRef } from "react";
import { ImageResizer } from "./extensions/image-resize";
+import { TableMenu } from "./table-menu";
export interface ITipTapRichTextEditor {
value: string;
@@ -75,8 +77,8 @@ const Tiptap = (props: ITipTapRichTextEditor) => {
const editorClassNames = `relative w-full max-w-full sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md
${noBorder ? "" : "border border-custom-border-200"} ${
- borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0"
- } ${customClassName}`;
+ borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0"
+ } ${customClassName}`;
if (!editor) return null;
editorRef.current = editor;
@@ -92,6 +94,7 @@ const Tiptap = (props: ITipTapRichTextEditor) => {
{editor && }
+
{editor?.isActive("image") &&
}
diff --git a/space/components/tiptap/plugins/delete-image.tsx b/space/components/tiptap/plugins/delete-image.tsx
new file mode 100644
index 000000000..fdf515ccc
--- /dev/null
+++ b/space/components/tiptap/plugins/delete-image.tsx
@@ -0,0 +1,68 @@
+import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state";
+import { Node as ProseMirrorNode } from "@tiptap/pm/model";
+import fileService from "services/file.service";
+
+const deleteKey = new PluginKey("delete-image");
+const IMAGE_NODE_TYPE = "image";
+
+interface ImageNode extends ProseMirrorNode {
+ attrs: {
+ src: string;
+ id: string;
+ };
+}
+
+const TrackImageDeletionPlugin = (): Plugin =>
+ new Plugin({
+ key: deleteKey,
+ appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => {
+ const newImageSources = new Set();
+ newState.doc.descendants((node) => {
+ if (node.type.name === IMAGE_NODE_TYPE) {
+ newImageSources.add(node.attrs.src);
+ }
+ });
+
+ transactions.forEach((transaction) => {
+ if (!transaction.docChanged) return;
+
+ const removedImages: ImageNode[] = [];
+
+ oldState.doc.descendants((oldNode, oldPos) => {
+ if (oldNode.type.name !== IMAGE_NODE_TYPE) return;
+ if (oldPos < 0 || oldPos > newState.doc.content.size) return;
+ if (!newState.doc.resolve(oldPos).parent) return;
+
+ const newNode = newState.doc.nodeAt(oldPos);
+
+ // Check if the node has been deleted or replaced
+ if (!newNode || newNode.type.name !== IMAGE_NODE_TYPE) {
+ if (!newImageSources.has(oldNode.attrs.src)) {
+ removedImages.push(oldNode as ImageNode);
+ }
+ }
+ });
+
+ removedImages.forEach(async (node) => {
+ const src = node.attrs.src;
+ await onNodeDeleted(src);
+ });
+ });
+
+ return null;
+ },
+ });
+
+export default TrackImageDeletionPlugin;
+
+async function onNodeDeleted(src: string): Promise
{
+ try {
+ const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
+ const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId);
+ if (resStatus === 204) {
+ console.log("Image deleted successfully");
+ }
+ } catch (error) {
+ console.error("Error deleting image: ", error);
+ }
+}
diff --git a/apps/app/components/tiptap/plugins/upload-image.tsx b/space/components/tiptap/plugins/upload-image.tsx
similarity index 88%
rename from apps/app/components/tiptap/plugins/upload-image.tsx
rename to space/components/tiptap/plugins/upload-image.tsx
index 0657bc82b..bc0acdc54 100644
--- a/apps/app/components/tiptap/plugins/upload-image.tsx
+++ b/space/components/tiptap/plugins/upload-image.tsx
@@ -1,4 +1,3 @@
-// @ts-nocheck
import { EditorState, Plugin, PluginKey } from "@tiptap/pm/state";
import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view";
import fileService from "services/file.service";
@@ -22,10 +21,7 @@ const UploadImagesPlugin = () =>
const placeholder = document.createElement("div");
placeholder.setAttribute("class", "img-placeholder");
const image = document.createElement("img");
- image.setAttribute(
- "class",
- "opacity-10 rounded-lg border border-custom-border-300",
- );
+ image.setAttribute("class", "opacity-10 rounded-lg border border-custom-border-300");
image.src = src;
placeholder.appendChild(image);
const deco = Decoration.widget(pos + 1, placeholder, {
@@ -57,11 +53,15 @@ function findPlaceholder(state: EditorState, id: {}) {
return found.length ? found[0].from : null;
}
-export async function startImageUpload(file: File, view: EditorView, pos: number, workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) {
+export async function startImageUpload(
+ file: File,
+ view: EditorView,
+ pos: number,
+ workspaceSlug: string,
+ setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
+) {
if (!file.type.includes("image/")) {
return;
- } else if (file.size / 1024 / 1024 > 20) {
- return;
}
const id = {};
@@ -85,7 +85,7 @@ export async function startImageUpload(file: File, view: EditorView, pos: number
if (!workspaceSlug) {
return;
}
- setIsSubmitting?.("submitting")
+ setIsSubmitting?.("submitting");
const src = await UploadImageHandler(file, workspaceSlug);
const { schema } = view.state;
pos = findPlaceholder(view.state, id);
diff --git a/apps/app/components/tiptap/props.tsx b/space/components/tiptap/props.tsx
similarity index 59%
rename from apps/app/components/tiptap/props.tsx
rename to space/components/tiptap/props.tsx
index d50fc29b0..8233e3ab4 100644
--- a/apps/app/components/tiptap/props.tsx
+++ b/space/components/tiptap/props.tsx
@@ -1,7 +1,11 @@
import { EditorProps } from "@tiptap/pm/view";
import { startImageUpload } from "./plugins/upload-image";
+import { findTableAncestor } from "./table-menu";
-export function TiptapEditorProps(workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void): EditorProps {
+export function TiptapEditorProps(
+ workspaceSlug: string,
+ setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
+): EditorProps {
return {
attributes: {
class: `prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none`,
@@ -18,11 +22,16 @@ export function TiptapEditorProps(workspaceSlug: string, setIsSubmitting?: (isSu
},
},
handlePaste: (view, event) => {
- if (
- event.clipboardData &&
- event.clipboardData.files &&
- event.clipboardData.files[0]
- ) {
+ if (typeof window !== "undefined") {
+ const selection: any = window?.getSelection();
+ if (selection.rangeCount !== 0) {
+ const range = selection.getRangeAt(0);
+ if (findTableAncestor(range.startContainer)) {
+ return;
+ }
+ }
+ }
+ if (event.clipboardData && event.clipboardData.files && event.clipboardData.files[0]) {
event.preventDefault();
const file = event.clipboardData.files[0];
const pos = view.state.selection.from;
@@ -32,12 +41,16 @@ export function TiptapEditorProps(workspaceSlug: string, setIsSubmitting?: (isSu
return false;
},
handleDrop: (view, event, _slice, moved) => {
- if (
- !moved &&
- event.dataTransfer &&
- event.dataTransfer.files &&
- event.dataTransfer.files[0]
- ) {
+ if (typeof window !== "undefined") {
+ const selection: any = window?.getSelection();
+ if (selection.rangeCount !== 0) {
+ const range = selection.getRangeAt(0);
+ if (findTableAncestor(range.startContainer)) {
+ return;
+ }
+ }
+ }
+ if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) {
event.preventDefault();
const file = event.dataTransfer.files[0];
const coordinates = view.posAtCoords({
diff --git a/apps/app/components/tiptap/slash-command/index.tsx b/space/components/tiptap/slash-command/index.tsx
similarity index 51%
rename from apps/app/components/tiptap/slash-command/index.tsx
rename to space/components/tiptap/slash-command/index.tsx
index 38f5c9c0a..46bf5ea5a 100644
--- a/apps/app/components/tiptap/slash-command/index.tsx
+++ b/space/components/tiptap/slash-command/index.tsx
@@ -15,6 +15,7 @@ import {
MinusSquare,
CheckSquare,
ImageIcon,
+ Table,
} from "lucide-react";
import { startImageUpload } from "../plugins/upload-image";
import { cn } from "../utils";
@@ -46,140 +47,162 @@ const Command = Extension.create({
return [
Suggestion({
editor: this.editor,
+ allow({ editor }) {
+ return !editor.isActive("table");
+ },
...this.options.suggestion,
}),
];
},
});
-const getSuggestionItems = (workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) => ({ query }: { query: string }) =>
- [
- {
- title: "Text",
- description: "Just start typing with plain text.",
- searchTerms: ["p", "paragraph"],
- icon: ,
- command: ({ editor, range }: CommandProps) => {
- editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run();
+const getSuggestionItems =
+ (
+ workspaceSlug: string,
+ setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
+ ) =>
+ ({ query }: { query: string }) =>
+ [
+ {
+ title: "Text",
+ description: "Just start typing with plain text.",
+ searchTerms: ["p", "paragraph"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run();
+ },
},
- },
- {
- title: "Heading 1",
- description: "Big section heading.",
- searchTerms: ["title", "big", "large"],
- icon: ,
- command: ({ editor, range }: CommandProps) => {
- editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run();
+ {
+ title: "Heading 1",
+ description: "Big section heading.",
+ searchTerms: ["title", "big", "large"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run();
+ },
},
- },
- {
- title: "Heading 2",
- description: "Medium section heading.",
- searchTerms: ["subtitle", "medium"],
- icon: ,
- command: ({ editor, range }: CommandProps) => {
- editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run();
+ {
+ title: "Heading 2",
+ description: "Medium section heading.",
+ searchTerms: ["subtitle", "medium"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run();
+ },
},
- },
- {
- title: "Heading 3",
- description: "Small section heading.",
- searchTerms: ["subtitle", "small"],
- icon: ,
- command: ({ editor, range }: CommandProps) => {
- editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run();
+ {
+ title: "Heading 3",
+ description: "Small section heading.",
+ searchTerms: ["subtitle", "small"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run();
+ },
},
- },
- {
- title: "To-do List",
- description: "Track tasks with a to-do list.",
- searchTerms: ["todo", "task", "list", "check", "checkbox"],
- icon: ,
- command: ({ editor, range }: CommandProps) => {
- editor.chain().focus().deleteRange(range).toggleTaskList().run();
+ {
+ title: "To-do List",
+ description: "Track tasks with a to-do list.",
+ searchTerms: ["todo", "task", "list", "check", "checkbox"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ editor.chain().focus().deleteRange(range).toggleTaskList().run();
+ },
},
- },
- {
- title: "Bullet List",
- description: "Create a simple bullet list.",
- searchTerms: ["unordered", "point"],
- icon:
,
- command: ({ editor, range }: CommandProps) => {
- editor.chain().focus().deleteRange(range).toggleBulletList().run();
+ {
+ title: "Bullet List",
+ description: "Create a simple bullet list.",
+ searchTerms: ["unordered", "point"],
+ icon:
,
+ command: ({ editor, range }: CommandProps) => {
+ editor.chain().focus().deleteRange(range).toggleBulletList().run();
+ },
},
- },
- {
- title: "Divider",
- description: "Visually divide blocks",
- searchTerms: ["line", "divider", "horizontal", "rule", "separate"],
- icon: ,
- command: ({ editor, range }: CommandProps) => {
- editor.chain().focus().deleteRange(range).setHorizontalRule().run();
+ {
+ title: "Divider",
+ description: "Visually divide blocks",
+ searchTerms: ["line", "divider", "horizontal", "rule", "separate"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ editor.chain().focus().deleteRange(range).setHorizontalRule().run();
+ },
},
- },
- {
- title: "Numbered List",
- description: "Create a list with numbering.",
- searchTerms: ["ordered"],
- icon: ,
- command: ({ editor, range }: CommandProps) => {
- editor.chain().focus().deleteRange(range).toggleOrderedList().run();
+ {
+ title: "Table",
+ description: "Create a Table",
+ searchTerms: ["table", "cell", "db", "data", "tabular"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ editor
+ .chain()
+ .focus()
+ .deleteRange(range)
+ .insertTable({ rows: 3, cols: 3, withHeaderRow: true })
+ .run();
+ },
},
- },
- {
- title: "Quote",
- description: "Capture a quote.",
- searchTerms: ["blockquote"],
- icon: ,
- command: ({ editor, range }: CommandProps) =>
- editor
- .chain()
- .focus()
- .deleteRange(range)
- .toggleNode("paragraph", "paragraph")
- .toggleBlockquote()
- .run(),
- },
- {
- title: "Code",
- description: "Capture a code snippet.",
- searchTerms: ["codeblock"],
- icon:
,
- command: ({ editor, range }: CommandProps) =>
- editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
- },
- {
- title: "Image",
- description: "Upload an image from your computer.",
- searchTerms: ["photo", "picture", "media"],
- icon: ,
- command: ({ editor, range }: CommandProps) => {
- editor.chain().focus().deleteRange(range).run();
- // upload image
- const input = document.createElement("input");
- input.type = "file";
- input.accept = "image/*";
- input.onchange = async () => {
- if (input.files?.length) {
- const file = input.files[0];
- const pos = editor.view.state.selection.from;
- startImageUpload(file, editor.view, pos, workspaceSlug, setIsSubmitting);
- }
- };
- input.click();
+ {
+ title: "Numbered List",
+ description: "Create a list with numbering.",
+ searchTerms: ["ordered"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ editor.chain().focus().deleteRange(range).toggleOrderedList().run();
+ },
},
- },
- ].filter((item) => {
- if (typeof query === "string" && query.length > 0) {
- const search = query.toLowerCase();
- return (
- item.title.toLowerCase().includes(search) ||
- item.description.toLowerCase().includes(search) ||
- (item.searchTerms && item.searchTerms.some((term: string) => term.includes(search)))
- );
- }
- return true;
- });
+ {
+ title: "Quote",
+ description: "Capture a quote.",
+ searchTerms: ["blockquote"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) =>
+ editor
+ .chain()
+ .focus()
+ .deleteRange(range)
+ .toggleNode("paragraph", "paragraph")
+ .toggleBlockquote()
+ .run(),
+ },
+ {
+ title: "Code",
+ description: "Capture a code snippet.",
+ searchTerms: ["codeblock"],
+ icon:
,
+ command: ({ editor, range }: CommandProps) =>
+ editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
+ },
+ {
+ title: "Image",
+ description: "Upload an image from your computer.",
+ searchTerms: ["photo", "picture", "media"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ editor.chain().focus().deleteRange(range).run();
+ // upload image
+ const input = document.createElement("input");
+ input.type = "file";
+ input.accept = "image/*";
+ input.onchange = async () => {
+ if (input.files?.length) {
+ const file = input.files[0];
+ const pos = editor.view.state.selection.from;
+ startImageUpload(file, editor.view, pos, workspaceSlug, setIsSubmitting);
+ }
+ };
+ input.click();
+ },
+ },
+ ].filter((item) => {
+ if (typeof query === "string" && query.length > 0) {
+ const search = query.toLowerCase();
+ return (
+ item.title.toLowerCase().includes(search) ||
+ item.description.toLowerCase().includes(search) ||
+ (item.searchTerms && item.searchTerms.some((term: string) => term.includes(search)))
+ );
+ }
+ return true;
+ });
export const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
const containerHeight = container.offsetHeight;
@@ -328,7 +351,10 @@ const renderItems = () => {
};
};
-export const SlashCommand = (workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) =>
+export const SlashCommand = (
+ workspaceSlug: string,
+ setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
+) =>
Command.configure({
suggestion: {
items: getSuggestionItems(workspaceSlug, setIsSubmitting),
diff --git a/space/components/tiptap/table-menu/InsertBottomTableIcon.tsx b/space/components/tiptap/table-menu/InsertBottomTableIcon.tsx
new file mode 100644
index 000000000..0e42ba648
--- /dev/null
+++ b/space/components/tiptap/table-menu/InsertBottomTableIcon.tsx
@@ -0,0 +1,16 @@
+const InsertBottomTableIcon = (props: any) => (
+
+
+
+);
+
+export default InsertBottomTableIcon;
diff --git a/space/components/tiptap/table-menu/InsertLeftTableIcon.tsx b/space/components/tiptap/table-menu/InsertLeftTableIcon.tsx
new file mode 100644
index 000000000..1fd75fe87
--- /dev/null
+++ b/space/components/tiptap/table-menu/InsertLeftTableIcon.tsx
@@ -0,0 +1,15 @@
+const InsertLeftTableIcon = (props: any) => (
+
+
+
+);
+export default InsertLeftTableIcon;
diff --git a/space/components/tiptap/table-menu/InsertRightTableIcon.tsx b/space/components/tiptap/table-menu/InsertRightTableIcon.tsx
new file mode 100644
index 000000000..1a6570969
--- /dev/null
+++ b/space/components/tiptap/table-menu/InsertRightTableIcon.tsx
@@ -0,0 +1,16 @@
+const InsertRightTableIcon = (props: any) => (
+
+
+
+);
+
+export default InsertRightTableIcon;
diff --git a/space/components/tiptap/table-menu/InsertTopTableIcon.tsx b/space/components/tiptap/table-menu/InsertTopTableIcon.tsx
new file mode 100644
index 000000000..8f04f4f61
--- /dev/null
+++ b/space/components/tiptap/table-menu/InsertTopTableIcon.tsx
@@ -0,0 +1,15 @@
+const InsertTopTableIcon = (props: any) => (
+
+
+
+);
+export default InsertTopTableIcon;
diff --git a/space/components/tiptap/table-menu/index.tsx b/space/components/tiptap/table-menu/index.tsx
new file mode 100644
index 000000000..94f9c0f8d
--- /dev/null
+++ b/space/components/tiptap/table-menu/index.tsx
@@ -0,0 +1,143 @@
+import { useState, useEffect } from "react";
+import { Rows, Columns, ToggleRight } from "lucide-react";
+import { cn } from "../utils";
+import { Tooltip } from "components/ui";
+import InsertLeftTableIcon from "./InsertLeftTableIcon";
+import InsertRightTableIcon from "./InsertRightTableIcon";
+import InsertTopTableIcon from "./InsertTopTableIcon";
+import InsertBottomTableIcon from "./InsertBottomTableIcon";
+
+interface TableMenuItem {
+ command: () => void;
+ icon: any;
+ key: string;
+ name: string;
+}
+
+export const findTableAncestor = (node: Node | null): HTMLTableElement | null => {
+ while (node !== null && node.nodeName !== "TABLE") {
+ node = node.parentNode;
+ }
+ return node as HTMLTableElement;
+};
+
+export const TableMenu = ({ editor }: { editor: any }) => {
+ const [tableLocation, setTableLocation] = useState({ bottom: 0, left: 0 });
+ const isOpen = editor?.isActive("table");
+
+ const items: TableMenuItem[] = [
+ {
+ command: () => editor.chain().focus().addColumnBefore().run(),
+ icon: InsertLeftTableIcon,
+ key: "insert-column-left",
+ name: "Insert 1 column left",
+ },
+ {
+ command: () => editor.chain().focus().addColumnAfter().run(),
+ icon: InsertRightTableIcon,
+ key: "insert-column-right",
+ name: "Insert 1 column right",
+ },
+ {
+ command: () => editor.chain().focus().addRowBefore().run(),
+ icon: InsertTopTableIcon,
+ key: "insert-row-above",
+ name: "Insert 1 row above",
+ },
+ {
+ command: () => editor.chain().focus().addRowAfter().run(),
+ icon: InsertBottomTableIcon,
+ key: "insert-row-below",
+ name: "Insert 1 row below",
+ },
+ {
+ command: () => editor.chain().focus().deleteColumn().run(),
+ icon: Columns,
+ key: "delete-column",
+ name: "Delete column",
+ },
+ {
+ command: () => editor.chain().focus().deleteRow().run(),
+ icon: Rows,
+ key: "delete-row",
+ name: "Delete row",
+ },
+ {
+ command: () => editor.chain().focus().toggleHeaderRow().run(),
+ icon: ToggleRight,
+ key: "toggle-header-row",
+ name: "Toggle header row",
+ },
+ ];
+
+ useEffect(() => {
+ if (!window) return;
+
+ const handleWindowClick = () => {
+ const selection: any = window?.getSelection();
+
+ if (selection.rangeCount !== 0) {
+ const range = selection.getRangeAt(0);
+ const tableNode = findTableAncestor(range.startContainer);
+
+ let parent = tableNode?.parentElement;
+
+ if (tableNode) {
+ const tableRect = tableNode.getBoundingClientRect();
+ const tableCenter = tableRect.left + tableRect.width / 2;
+ const menuWidth = 45;
+ const menuLeft = tableCenter - menuWidth / 2;
+ const tableBottom = tableRect.bottom;
+
+ setTableLocation({ bottom: tableBottom, left: menuLeft });
+
+ while (parent) {
+ if (!parent.classList.contains("disable-scroll"))
+ parent.classList.add("disable-scroll");
+ parent = parent.parentElement;
+ }
+ } else {
+ const scrollDisabledContainers = document.querySelectorAll(".disable-scroll");
+
+ scrollDisabledContainers.forEach((container) => {
+ container.classList.remove("disable-scroll");
+ });
+ }
+ }
+ };
+
+ window.addEventListener("click", handleWindowClick);
+
+ return () => {
+ window.removeEventListener("click", handleWindowClick);
+ };
+ }, [tableLocation, editor]);
+
+ return (
+
+ {items.map((item, index) => (
+
+
+
+
+
+ ))}
+
+ );
+};
diff --git a/apps/app/components/tiptap/utils.ts b/space/components/tiptap/utils.ts
similarity index 100%
rename from apps/app/components/tiptap/utils.ts
rename to space/components/tiptap/utils.ts
diff --git a/space/components/ui/dropdown.tsx b/space/components/ui/dropdown.tsx
new file mode 100644
index 000000000..d1791de00
--- /dev/null
+++ b/space/components/ui/dropdown.tsx
@@ -0,0 +1,149 @@
+"use client";
+
+import { Fragment, useState, useRef } from "react";
+
+// next
+import Link from "next/link";
+
+// headless
+import { Popover, Transition } from "@headlessui/react";
+import { ChevronLeftIcon, CheckIcon } from "@heroicons/react/20/solid";
+
+// hooks
+import useOutSideClick from "hooks/use-outside-click";
+
+type ItemOptionType = {
+ display: React.ReactNode;
+ as?: "button" | "link" | "div";
+ href?: string;
+ isSelected?: boolean;
+ onClick?: () => void;
+ children?: ItemOptionType[] | null;
+};
+
+type DropdownItemProps = {
+ item: ItemOptionType;
+};
+
+type DropDownListProps = {
+ open: boolean;
+ handleClose?: () => void;
+ items: ItemOptionType[];
+};
+
+type DropdownProps = {
+ button: React.ReactNode | (() => React.ReactNode);
+ items: ItemOptionType[];
+};
+
+const DropdownList: React.FC = (props) => {
+ const { open, items, handleClose } = props;
+
+ const ref = useRef(null);
+
+ useOutSideClick(ref, () => {
+ if (handleClose) handleClose();
+ });
+
+ return (
+
+
+
+
+ {items.map((item, index) => (
+
+ ))}
+
+
+
+
+ );
+};
+
+const DropdownItem: React.FC = (props) => {
+ const { item } = props;
+ const { display, children, as: as_, href, onClick, isSelected } = item;
+
+ const [open, setOpen] = useState(false);
+
+ return (
+
+ {(!as_ || as_ === "button" || as_ === "div") && (
+ {
+ if (!children) {
+ if (onClick) onClick();
+ return;
+ }
+ setOpen((prev) => !prev);
+ }}
+ className={`w-full flex items-center gap-1 rounded px-1 py-1.5 text-custom-text-200 hover:bg-custom-background-80 ${
+ isSelected ? "bg-custom-background-80" : ""
+ }`}
+ >
+ {children && }
+ {!children && }
+ {display}
+
+
+ )}
+
+ {as_ === "link" && {display}}
+
+ {children && setOpen(false)} items={children} />}
+
+ );
+};
+
+const Dropdown: React.FC = (props) => {
+ const { button, items } = props;
+
+ return (
+
+ {({ open }) => (
+ <>
+
+ {typeof button === "function" ? button() : button}
+
+
+
+
+
+ {items.map((item, index) => (
+
+ ))}
+
+
+
+ >
+ )}
+
+ );
+};
+
+export { Dropdown };
diff --git a/space/components/ui/icon.tsx b/space/components/ui/icon.tsx
new file mode 100644
index 000000000..418186291
--- /dev/null
+++ b/space/components/ui/icon.tsx
@@ -0,0 +1,10 @@
+import React from "react";
+
+type Props = {
+ iconName: string;
+ className?: string;
+};
+
+export const Icon: React.FC = ({ iconName, className = "" }) => (
+ {iconName}
+);
diff --git a/space/components/ui/index.ts b/space/components/ui/index.ts
new file mode 100644
index 000000000..e44096909
--- /dev/null
+++ b/space/components/ui/index.ts
@@ -0,0 +1,8 @@
+export * from "./dropdown";
+export * from "./input";
+export * from "./loader";
+export * from "./primary-button";
+export * from "./secondary-button";
+export * from "./icon";
+export * from "./reaction-selector";
+export * from "./tooltip";
diff --git a/space/components/ui/input.tsx b/space/components/ui/input.tsx
new file mode 100644
index 000000000..b6be82ae5
--- /dev/null
+++ b/space/components/ui/input.tsx
@@ -0,0 +1,37 @@
+import React, { forwardRef, Ref } from "react";
+
+// types
+interface Props extends React.InputHTMLAttributes {
+ mode?: "primary" | "transparent" | "trueTransparent";
+ error?: boolean;
+ inputSize?: "rg" | "lg";
+ fullWidth?: boolean;
+}
+
+export const Input = forwardRef((props: Props, ref: Ref) => {
+ const { mode = "primary", error, className = "", type, fullWidth = true, id, inputSize = "rg", ...rest } = props;
+
+ return (
+
+ );
+});
+
+Input.displayName = "Input";
+
+export default Input;
diff --git a/apps/app/components/ui/loader.tsx b/space/components/ui/loader.tsx
similarity index 100%
rename from apps/app/components/ui/loader.tsx
rename to space/components/ui/loader.tsx
diff --git a/space/components/ui/primary-button.tsx b/space/components/ui/primary-button.tsx
new file mode 100644
index 000000000..b3e1b82ee
--- /dev/null
+++ b/space/components/ui/primary-button.tsx
@@ -0,0 +1,35 @@
+interface ButtonProps extends React.ButtonHTMLAttributes {
+ size?: "sm" | "md" | "lg";
+ outline?: boolean;
+ loading?: boolean;
+}
+
+export const PrimaryButton: React.FC = ({
+ children,
+ className = "",
+ onClick,
+ type = "button",
+ disabled = false,
+ loading = false,
+ size = "sm",
+ outline = false,
+}) => (
+
+ {children}
+
+);
diff --git a/space/components/ui/reaction-selector.tsx b/space/components/ui/reaction-selector.tsx
new file mode 100644
index 000000000..a7b67afa6
--- /dev/null
+++ b/space/components/ui/reaction-selector.tsx
@@ -0,0 +1,80 @@
+import { Fragment } from "react";
+
+// headless ui
+import { Popover, Transition } from "@headlessui/react";
+
+// helper
+import { renderEmoji } from "helpers/emoji.helper";
+
+// icons
+import { Icon } from "components/ui";
+
+const reactionEmojis = ["128077", "128078", "128516", "128165", "128533", "129505", "9992", "128064"];
+
+interface Props {
+ onSelect: (emoji: string) => void;
+ position?: "top" | "bottom";
+ selected?: string[];
+ size?: "sm" | "md" | "lg";
+}
+
+export const ReactionSelector: React.FC = (props) => {
+ const { onSelect, position, selected = [], size } = props;
+
+ return (
+
+ {({ open, close: closePopover }) => (
+ <>
+
+
+
+
+
+
+
+
+
+ {reactionEmojis.map((emoji) => (
+ {
+ onSelect(emoji);
+ closePopover();
+ }}
+ className={`grid place-items-center select-none rounded-md text-sm p-1 ${
+ selected.includes(emoji) ? "bg-custom-primary-100/10" : "hover:bg-custom-sidebar-background-80"
+ }`}
+ >
+ {renderEmoji(emoji)}
+
+ ))}
+
+
+
+
+ >
+ )}
+
+ );
+};
diff --git a/space/components/ui/secondary-button.tsx b/space/components/ui/secondary-button.tsx
new file mode 100644
index 000000000..2a9b3d528
--- /dev/null
+++ b/space/components/ui/secondary-button.tsx
@@ -0,0 +1,35 @@
+interface ButtonProps extends React.ButtonHTMLAttributes {
+ size?: "sm" | "md" | "lg";
+ outline?: boolean;
+ loading?: boolean;
+}
+
+export const SecondaryButton: React.FC = ({
+ children,
+ className = "",
+ onClick,
+ type = "button",
+ disabled = false,
+ loading = false,
+ size = "sm",
+ outline = false,
+}) => (
+
+ {children}
+
+);
diff --git a/apps/app/components/toast-alert/index.tsx b/space/components/ui/toast-alert.tsx
similarity index 100%
rename from apps/app/components/toast-alert/index.tsx
rename to space/components/ui/toast-alert.tsx
diff --git a/space/components/ui/tooltip.tsx b/space/components/ui/tooltip.tsx
new file mode 100644
index 000000000..994c0f32a
--- /dev/null
+++ b/space/components/ui/tooltip.tsx
@@ -0,0 +1,71 @@
+import React from "react";
+
+// next-themes
+import { useTheme } from "next-themes";
+// tooltip2
+import { Tooltip2 } from "@blueprintjs/popover2";
+
+type Props = {
+ tooltipHeading?: string;
+ tooltipContent: string | React.ReactNode;
+ position?:
+ | "top"
+ | "right"
+ | "bottom"
+ | "left"
+ | "auto"
+ | "auto-end"
+ | "auto-start"
+ | "bottom-left"
+ | "bottom-right"
+ | "left-bottom"
+ | "left-top"
+ | "right-bottom"
+ | "right-top"
+ | "top-left"
+ | "top-right";
+ children: JSX.Element;
+ disabled?: boolean;
+ className?: string;
+ openDelay?: number;
+ closeDelay?: number;
+};
+
+export const Tooltip: React.FC = ({
+ tooltipHeading,
+ tooltipContent,
+ position = "top",
+ children,
+ disabled = false,
+ className = "",
+ openDelay = 200,
+ closeDelay,
+}) => {
+ const { theme } = useTheme();
+
+ return (
+
+ {tooltipHeading && (
+
+ {tooltipHeading}
+
+ )}
+ {tooltipContent}
+
+ }
+ position={position}
+ renderTarget={({ isOpen: isTooltipOpen, ref: eleReference, ...tooltipProps }) =>
+ React.cloneElement(children, { ref: eleReference, ...tooltipProps, ...children.props })
+ }
+ />
+ );
+};
diff --git a/space/components/views/home.tsx b/space/components/views/home.tsx
new file mode 100644
index 000000000..999fce073
--- /dev/null
+++ b/space/components/views/home.tsx
@@ -0,0 +1,13 @@
+// mobx
+import { observer } from "mobx-react-lite";
+import { useMobxStore } from "lib/mobx/store-provider";
+// components
+import { SignInView, UserLoggedIn } from "components/accounts";
+
+export const HomeView = observer(() => {
+ const { user: userStore } = useMobxStore();
+
+ if (!userStore.currentUser) return
;
+
+ return
;
+});
diff --git a/space/components/views/index.ts b/space/components/views/index.ts
new file mode 100644
index 000000000..84d36cd29
--- /dev/null
+++ b/space/components/views/index.ts
@@ -0,0 +1 @@
+export * from "./home";
diff --git a/space/components/views/project-details.tsx b/space/components/views/project-details.tsx
new file mode 100644
index 000000000..1c9c6ddc9
--- /dev/null
+++ b/space/components/views/project-details.tsx
@@ -0,0 +1,98 @@
+import { useEffect } from "react";
+
+import Image from "next/image";
+import { useRouter } from "next/router";
+
+// mobx
+import { observer } from "mobx-react-lite";
+// components
+import { IssueListView } from "components/issues/board-views/list";
+import { IssueKanbanView } from "components/issues/board-views/kanban";
+import { IssueCalendarView } from "components/issues/board-views/calendar";
+import { IssueSpreadsheetView } from "components/issues/board-views/spreadsheet";
+import { IssueGanttView } from "components/issues/board-views/gantt";
+import { IssuePeekOverview } from "components/issues/peek-overview";
+// mobx store
+import { RootStore } from "store/root";
+import { useMobxStore } from "lib/mobx/store-provider";
+// assets
+import SomethingWentWrongImage from "public/something-went-wrong.svg";
+
+export const ProjectDetailsView = observer(() => {
+ const router = useRouter();
+ const { workspace_slug, project_slug, states, labels, priorities, board, peekId } = router.query;
+
+ const {
+ issue: issueStore,
+ project: projectStore,
+ issueDetails: issueDetailStore,
+ user: userStore,
+ }: RootStore = useMobxStore();
+
+ useEffect(() => {
+ if (!userStore.currentUser) {
+ userStore.fetchCurrentUser();
+ }
+ }, [userStore]);
+
+ useEffect(() => {
+ if (workspace_slug && project_slug) {
+ const params = {
+ state: states || null,
+ labels: labels || null,
+ priority: priorities || null,
+ };
+ issueStore.fetchPublicIssues(workspace_slug?.toString(), project_slug.toString(), params);
+ }
+ }, [workspace_slug, project_slug, issueStore, states, labels, priorities]);
+
+ useEffect(() => {
+ if (peekId && workspace_slug && project_slug) {
+ issueDetailStore.setPeekId(peekId.toString());
+ }
+ }, [peekId, issueDetailStore, project_slug, workspace_slug]);
+
+ return (
+
+ {workspace_slug &&
}
+
+ {issueStore?.loader && !issueStore.issues ? (
+
Loading...
+ ) : (
+ <>
+ {issueStore?.error ? (
+
+
+
+
Oops! Something went wrong.
+
The public board does not exist. Please check the URL.
+
+
+ ) : (
+ projectStore?.activeBoard && (
+ <>
+ {projectStore?.activeBoard === "list" && (
+
+
+
+ )}
+ {projectStore?.activeBoard === "kanban" && (
+
+
+
+ )}
+ {projectStore?.activeBoard === "calendar" &&
}
+ {projectStore?.activeBoard === "spreadsheet" &&
}
+ {projectStore?.activeBoard === "gantt" &&
}
+ >
+ )
+ )}
+ >
+ )}
+
+ );
+});
diff --git a/apps/space/constants/data.ts b/space/constants/data.ts
similarity index 67%
rename from apps/space/constants/data.ts
rename to space/constants/data.ts
index 81ccae116..29d411342 100644
--- a/apps/space/constants/data.ts
+++ b/space/constants/data.ts
@@ -7,7 +7,7 @@ import {
TIssueGroupKey,
IIssuePriorityFilters,
IIssueGroup,
-} from "store/types/issue";
+} from "types/issue";
// icons
import {
BacklogStateIcon,
@@ -18,69 +18,49 @@ import {
} from "components/icons";
// all issue views
-export const issueViews: IIssueBoardViews[] = [
- {
- key: "list",
+export const issueViews: any = {
+ list: {
title: "List View",
icon: "format_list_bulleted",
className: "",
},
- {
- key: "kanban",
+ kanban: {
title: "Board View",
icon: "grid_view",
className: "",
},
- // {
- // key: "calendar",
- // title: "Calendar View",
- // icon: "calendar_month",
- // className: "",
- // },
- // {
- // key: "spreadsheet",
- // title: "Spreadsheet View",
- // icon: "table_chart",
- // className: "",
- // },
- // {
- // key: "gantt",
- // title: "Gantt Chart View",
- // icon: "waterfall_chart",
- // className: "rotate-90",
- // },
-];
+};
// issue priority filters
export const issuePriorityFilters: IIssuePriorityFilters[] = [
{
key: "urgent",
title: "Urgent",
- className: "border border-red-500/50 bg-red-500/20 text-red-500",
+ className: "bg-red-500 border-red-500 text-white",
icon: "error",
},
{
key: "high",
title: "High",
- className: "border border-orange-500/50 bg-orange-500/20 text-orange-500",
+ className: "text-orange-500 border-custom-border-300",
icon: "signal_cellular_alt",
},
{
key: "medium",
title: "Medium",
- className: "border border-yellow-500/50 bg-yellow-500/20 text-yellow-500",
+ className: "text-yellow-500 border-custom-border-300",
icon: "signal_cellular_alt_2_bar",
},
{
key: "low",
title: "Low",
- className: "border border-green-500/50 bg-green-500/20 text-green-500",
+ className: "text-green-500 border-custom-border-300",
icon: "signal_cellular_alt_1_bar",
},
{
key: "none",
title: "None",
- className: "border border-gray-500/50 bg-gray-500/20 text-gray-500",
+ className: "text-gray-500 border-custom-border-300",
icon: "block",
},
];
@@ -111,35 +91,35 @@ export const issueGroups: IIssueGroup[] = [
key: "backlog",
title: "Backlog",
color: "#d9d9d9",
- className: `border-[#d9d9d9]/50 text-[#d9d9d9] bg-[#d9d9d9]/10`,
+ className: `text-[#d9d9d9] bg-[#d9d9d9]/10`,
icon: BacklogStateIcon,
},
{
key: "unstarted",
title: "Unstarted",
color: "#3f76ff",
- className: `border-[#3f76ff]/50 text-[#3f76ff] bg-[#3f76ff]/10`,
+ className: `text-[#3f76ff] bg-[#3f76ff]/10`,
icon: UnstartedStateIcon,
},
{
key: "started",
title: "Started",
color: "#f59e0b",
- className: `border-[#f59e0b]/50 text-[#f59e0b] bg-[#f59e0b]/10`,
+ className: `text-[#f59e0b] bg-[#f59e0b]/10`,
icon: StartedStateIcon,
},
{
key: "completed",
title: "Completed",
color: "#16a34a",
- className: `border-[#16a34a]/50 text-[#16a34a] bg-[#16a34a]/10`,
+ className: `text-[#16a34a] bg-[#16a34a]/10`,
icon: CompletedStateIcon,
},
{
key: "cancelled",
title: "Cancelled",
color: "#dc2626",
- className: `border-[#dc2626]/50 text-[#dc2626] bg-[#dc2626]/10`,
+ className: `text-[#dc2626] bg-[#dc2626]/10`,
icon: CancelledStateIcon,
},
];
diff --git a/space/constants/seo.ts b/space/constants/seo.ts
new file mode 100644
index 000000000..b2baca612
--- /dev/null
+++ b/space/constants/seo.ts
@@ -0,0 +1,7 @@
+export const SITE_NAME = "Plane Deploy | Make your Plane boards and roadmaps pubic with just one-click. ";
+export const SITE_TITLE = "Plane Deploy | Make your Plane boards public with one-click";
+export const SITE_DESCRIPTION = "Plane Deploy is a customer feedback management tool built on top of plane.so";
+export const SITE_KEYWORDS =
+ "software development, customer feedback, software, accelerate, code management, release management, project management, issue tracking, agile, scrum, kanban, collaboration";
+export const SITE_URL = "https://app.plane.so/";
+export const TWITTER_USER_NAME = "planepowers";
diff --git a/space/constants/workspace.ts b/space/constants/workspace.ts
new file mode 100644
index 000000000..5ae5a7cf4
--- /dev/null
+++ b/space/constants/workspace.ts
@@ -0,0 +1,12 @@
+export const USER_ROLES = [
+ { value: "Product / Project Manager", label: "Product / Project Manager" },
+ { value: "Development / Engineering", label: "Development / Engineering" },
+ { value: "Founder / Executive", label: "Founder / Executive" },
+ { value: "Freelancer / Consultant", label: "Freelancer / Consultant" },
+ { value: "Marketing / Growth", label: "Marketing / Growth" },
+ { value: "Sales / Business Development", label: "Sales / Business Development" },
+ { value: "Support / Operations", label: "Support / Operations" },
+ { value: "Student / Professor", label: "Student / Professor" },
+ { value: "Human Resources", label: "Human Resources" },
+ { value: "Other", label: "Other" },
+];
diff --git a/space/contexts/toast.context.tsx b/space/contexts/toast.context.tsx
new file mode 100644
index 000000000..a382b4fd2
--- /dev/null
+++ b/space/contexts/toast.context.tsx
@@ -0,0 +1,97 @@
+import React, { createContext, useCallback, useReducer } from "react";
+// uuid
+import { v4 as uuid } from "uuid";
+// components
+import ToastAlert from "components/ui/toast-alert";
+
+export const toastContext = createContext
({} as ContextType);
+
+// types
+type ToastAlert = {
+ id: string;
+ title: string;
+ message?: string;
+ type: "success" | "error" | "warning" | "info";
+};
+
+type ReducerActionType = {
+ type: "SET_TOAST_ALERT" | "REMOVE_TOAST_ALERT";
+ payload: ToastAlert;
+};
+
+type ContextType = {
+ alerts?: ToastAlert[];
+ removeAlert: (id: string) => void;
+ setToastAlert: (data: {
+ title: string;
+ type?: "success" | "error" | "warning" | "info" | undefined;
+ message?: string | undefined;
+ }) => void;
+};
+
+type StateType = {
+ toastAlerts?: ToastAlert[];
+};
+
+type ReducerFunctionType = (state: StateType, action: ReducerActionType) => StateType;
+
+export const initialState: StateType = {
+ toastAlerts: [],
+};
+
+export const reducer: ReducerFunctionType = (state, action) => {
+ const { type, payload } = action;
+
+ switch (type) {
+ case "SET_TOAST_ALERT":
+ return {
+ ...state,
+ toastAlerts: [...(state.toastAlerts ?? []), payload],
+ };
+
+ case "REMOVE_TOAST_ALERT":
+ return {
+ ...state,
+ toastAlerts: state.toastAlerts?.filter((toastAlert) => toastAlert.id !== payload.id),
+ };
+
+ default: {
+ return state;
+ }
+ }
+};
+
+export const ToastContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ const [state, dispatch] = useReducer(reducer, initialState);
+
+ const removeAlert = useCallback((id: string) => {
+ dispatch({
+ type: "REMOVE_TOAST_ALERT",
+ payload: { id, title: "", message: "", type: "success" },
+ });
+ }, []);
+
+ const setToastAlert = useCallback(
+ (data: { title: string; type?: "success" | "error" | "warning" | "info"; message?: string }) => {
+ const id = uuid();
+ const { title, type, message } = data;
+ dispatch({
+ type: "SET_TOAST_ALERT",
+ payload: { id, title, message, type: type ?? "success" },
+ });
+
+ const timer = setTimeout(() => {
+ removeAlert(id);
+ clearTimeout(timer);
+ }, 3000);
+ },
+ [removeAlert]
+ );
+
+ return (
+
+
+ {children}
+
+ );
+};
diff --git a/space/helpers/common.helper.ts b/space/helpers/common.helper.ts
new file mode 100644
index 000000000..758d7c370
--- /dev/null
+++ b/space/helpers/common.helper.ts
@@ -0,0 +1 @@
+export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ? process.env.NEXT_PUBLIC_API_BASE_URL : "";
diff --git a/space/helpers/date-time.helper.ts b/space/helpers/date-time.helper.ts
new file mode 100644
index 000000000..f19a5358b
--- /dev/null
+++ b/space/helpers/date-time.helper.ts
@@ -0,0 +1,37 @@
+export const timeAgo = (time: any) => {
+ switch (typeof time) {
+ case "number":
+ break;
+ case "string":
+ time = +new Date(time);
+ break;
+ case "object":
+ if (time.constructor === Date) time = time.getTime();
+ break;
+ default:
+ time = +new Date();
+ }
+};
+
+/**
+ * @description Returns date and month, if date is of the current year
+ * @description Returns date, month adn year, if date is of a different year than current
+ * @param {string} date
+ * @example renderFullDate("2023-01-01") // 1 Jan
+ * @example renderFullDate("2021-01-01") // 1 Jan, 2021
+ */
+
+export const renderFullDate = (date: string): string => {
+ if (!date) return "";
+
+ const months: string[] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
+
+ const currentDate: Date = new Date();
+ const [year, month, day]: number[] = date.split("-").map(Number);
+
+ const formattedMonth: string = months[month - 1];
+ const formattedDay: string = day < 10 ? `0${day}` : day.toString();
+
+ if (currentDate.getFullYear() === year) return `${formattedDay} ${formattedMonth}`;
+ else return `${formattedDay} ${formattedMonth}, ${year}`;
+};
diff --git a/apps/app/helpers/emoji.helper.tsx b/space/helpers/emoji.helper.tsx
similarity index 77%
rename from apps/app/helpers/emoji.helper.tsx
rename to space/helpers/emoji.helper.tsx
index 06bf8ba15..7c9f3cfcb 100644
--- a/apps/app/helpers/emoji.helper.tsx
+++ b/space/helpers/emoji.helper.tsx
@@ -41,13 +41,16 @@ export const groupReactions: (reactions: any[], key: string) => { [key: string]:
reactions: any,
key: string
) => {
- const groupedReactions = reactions.reduce((acc: any, reaction: any) => {
- if (!acc[reaction[key]]) {
- acc[reaction[key]] = [];
- }
- acc[reaction[key]].push(reaction);
- return acc;
- }, {} as { [key: string]: any[] });
+ const groupedReactions = reactions.reduce(
+ (acc: any, reaction: any) => {
+ if (!acc[reaction[key]]) {
+ acc[reaction[key]] = [];
+ }
+ acc[reaction[key]].push(reaction);
+ return acc;
+ },
+ {} as { [key: string]: any[] }
+ );
return groupedReactions;
};
diff --git a/space/helpers/string.helper.ts b/space/helpers/string.helper.ts
new file mode 100644
index 000000000..1b676ca57
--- /dev/null
+++ b/space/helpers/string.helper.ts
@@ -0,0 +1,31 @@
+export const addSpaceIfCamelCase = (str: string) => str.replace(/([a-z])([A-Z])/g, "$1 $2");
+
+const fallbackCopyTextToClipboard = (text: string) => {
+ var textArea = document.createElement("textarea");
+ textArea.value = text;
+
+ // Avoid scrolling to bottom
+ textArea.style.top = "0";
+ textArea.style.left = "0";
+ textArea.style.position = "fixed";
+
+ document.body.appendChild(textArea);
+ textArea.focus();
+ textArea.select();
+
+ try {
+ // FIXME: Even though we are using this as a fallback, execCommand is deprecated 👎. We should find a better way to do this.
+ // https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand
+ var successful = document.execCommand("copy");
+ } catch (err) {}
+
+ document.body.removeChild(textArea);
+};
+
+export const copyTextToClipboard = async (text: string) => {
+ if (!navigator.clipboard) {
+ fallbackCopyTextToClipboard(text);
+ return;
+ }
+ await navigator.clipboard.writeText(text);
+};
diff --git a/space/hooks/use-outside-click.tsx b/space/hooks/use-outside-click.tsx
new file mode 100644
index 000000000..f2bed415f
--- /dev/null
+++ b/space/hooks/use-outside-click.tsx
@@ -0,0 +1,21 @@
+"use client";
+
+import { useEffect } from "react";
+
+const useOutSideClick = (ref: any, callback: any) => {
+ const handleClick = (e: any) => {
+ if (ref.current && !ref.current.contains(e.target)) {
+ callback();
+ }
+ };
+
+ useEffect(() => {
+ document.addEventListener("click", handleClick);
+
+ return () => {
+ document.removeEventListener("click", handleClick);
+ };
+ });
+};
+
+export default useOutSideClick;
diff --git a/apps/app/hooks/use-timer.tsx b/space/hooks/use-timer.tsx
similarity index 100%
rename from apps/app/hooks/use-timer.tsx
rename to space/hooks/use-timer.tsx
diff --git a/apps/app/hooks/use-toast.tsx b/space/hooks/use-toast.tsx
similarity index 100%
rename from apps/app/hooks/use-toast.tsx
rename to space/hooks/use-toast.tsx
diff --git a/space/layouts/project-layout.tsx b/space/layouts/project-layout.tsx
new file mode 100644
index 000000000..1a0b7899e
--- /dev/null
+++ b/space/layouts/project-layout.tsx
@@ -0,0 +1,33 @@
+import Link from "next/link";
+import Image from "next/image";
+
+// mobx
+import { observer } from "mobx-react-lite";
+import planeLogo from "public/plane-logo.svg";
+// components
+import IssueNavbar from "components/issues/navbar";
+
+const ProjectLayout = ({ children }: { children: React.ReactNode }) => (
+
+);
+
+export default observer(ProjectLayout);
diff --git a/apps/space/lib/index.ts b/space/lib/index.ts
similarity index 100%
rename from apps/space/lib/index.ts
rename to space/lib/index.ts
diff --git a/space/lib/mobx/store-init.tsx b/space/lib/mobx/store-init.tsx
new file mode 100644
index 000000000..6e38d9c6d
--- /dev/null
+++ b/space/lib/mobx/store-init.tsx
@@ -0,0 +1,25 @@
+"use client";
+
+import { useEffect } from "react";
+// next imports
+import { useRouter } from "next/router";
+// mobx store
+import { useMobxStore } from "lib/mobx/store-provider";
+import { RootStore } from "store/root";
+
+const MobxStoreInit = () => {
+ const store: RootStore = useMobxStore();
+
+ const router = useRouter();
+ const { states, labels, priorities } = router.query as { states: string[]; labels: string[]; priorities: string[] };
+
+ // useEffect(() => {
+ // store.issue.userSelectedLabels = labels || [];
+ // store.issue.userSelectedPriorities = priorities || [];
+ // store.issue.userSelectedStates = states || [];
+ // }, [store.issue]);
+
+ return <>>;
+};
+
+export default MobxStoreInit;
diff --git a/apps/app/lib/mobx/store-provider.tsx b/space/lib/mobx/store-provider.tsx
similarity index 100%
rename from apps/app/lib/mobx/store-provider.tsx
rename to space/lib/mobx/store-provider.tsx
diff --git a/space/next.config.js b/space/next.config.js
new file mode 100644
index 000000000..bd3749f10
--- /dev/null
+++ b/space/next.config.js
@@ -0,0 +1,22 @@
+/** @type {import('next').NextConfig} */
+const path = require("path");
+const withImages = require("next-images");
+
+const nextConfig = {
+ reactStrictMode: false,
+ swcMinify: true,
+ experimental: {
+ outputFileTracingRoot: path.join(__dirname, "../"),
+ },
+ output: "standalone",
+};
+
+if (parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0")) {
+ const nextConfigWithNginx = withImages({
+ basePath: "/spaces",
+ ...nextConfig,
+ });
+ module.exports = nextConfigWithNginx;
+} else {
+ module.exports = nextConfig;
+}
diff --git a/space/package.json b/space/package.json
new file mode 100644
index 000000000..2cf52bbf4
--- /dev/null
+++ b/space/package.json
@@ -0,0 +1,77 @@
+{
+ "name": "space",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "next dev -p 4000",
+ "build": "next build",
+ "start": "next start -p 4000",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "@blueprintjs/core": "^4.16.3",
+ "@blueprintjs/popover2": "^1.13.3",
+ "@emotion/react": "^11.11.1",
+ "@emotion/styled": "^11.11.0",
+ "@headlessui/react": "^1.7.13",
+ "@heroicons/react": "^2.0.12",
+ "@mui/icons-material": "^5.14.1",
+ "@mui/material": "^5.14.1",
+ "@tiptap-pro/extension-unique-id": "^2.1.0",
+ "@tiptap/extension-code-block-lowlight": "^2.0.4",
+ "@tiptap/extension-color": "^2.0.4",
+ "@tiptap/extension-gapcursor": "^2.1.7",
+ "@tiptap/extension-highlight": "^2.0.4",
+ "@tiptap/extension-horizontal-rule": "^2.0.4",
+ "@tiptap/extension-image": "^2.0.4",
+ "@tiptap/extension-link": "^2.0.4",
+ "@tiptap/extension-placeholder": "^2.0.4",
+ "@tiptap/extension-table": "^2.1.6",
+ "@tiptap/extension-table-cell": "^2.1.6",
+ "@tiptap/extension-table-header": "^2.1.6",
+ "@tiptap/extension-table-row": "^2.1.6",
+ "@tiptap/extension-task-item": "^2.0.4",
+ "@tiptap/extension-task-list": "^2.0.4",
+ "@tiptap/extension-text-style": "^2.0.4",
+ "@tiptap/extension-underline": "^2.0.4",
+ "@tiptap/pm": "^2.0.4",
+ "@tiptap/react": "^2.0.4",
+ "@tiptap/starter-kit": "^2.0.4",
+ "@tiptap/suggestion": "^2.0.4",
+ "axios": "^1.3.4",
+ "clsx": "^2.0.0",
+ "js-cookie": "^3.0.1",
+ "lowlight": "^2.9.0",
+ "lucide-react": "^0.263.1",
+ "mobx": "^6.10.0",
+ "mobx-react-lite": "^4.0.3",
+ "next": "12.3.2",
+ "next-images": "^1.8.5",
+ "next-themes": "^0.2.1",
+ "nprogress": "^0.2.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-hook-form": "^7.38.0",
+ "react-moveable": "^0.54.1",
+ "swr": "^2.2.2",
+ "tailwind-merge": "^1.14.0",
+ "tiptap-markdown": "^0.8.2",
+ "typescript": "4.9.5",
+ "use-debounce": "^9.0.4",
+ "uuid": "^9.0.0"
+ },
+ "devDependencies": {
+ "@types/js-cookie": "^3.0.3",
+ "@types/node": "18.14.1",
+ "@types/nprogress": "^0.2.0",
+ "@types/react": "18.0.28",
+ "@types/react-dom": "18.0.11",
+ "@types/uuid": "^9.0.1",
+ "@typescript-eslint/eslint-plugin": "^5.48.2",
+ "eslint": "8.34.0",
+ "eslint-config-custom": "*",
+ "eslint-config-next": "13.2.1",
+ "tsconfig": "*",
+ "tailwind-config-custom": "*"
+ }
+}
diff --git a/apps/space/pages/404.tsx b/space/pages/404.tsx
similarity index 90%
rename from apps/space/pages/404.tsx
rename to space/pages/404.tsx
index d10d95fb9..be40bd501 100644
--- a/apps/space/pages/404.tsx
+++ b/space/pages/404.tsx
@@ -1,12 +1,13 @@
// next imports
import Image from "next/image";
+import notFoundImage from "public/404.svg";
const Custom404Error = () => (
-
+
Oops! Something went wrong.
diff --git a/space/pages/[workspace_slug]/[project_slug]/index.tsx b/space/pages/[workspace_slug]/[project_slug]/index.tsx
new file mode 100644
index 000000000..e50c01c18
--- /dev/null
+++ b/space/pages/[workspace_slug]/[project_slug]/index.tsx
@@ -0,0 +1,43 @@
+import Head from "next/head";
+import { useRouter } from "next/router";
+
+import useSWR from "swr";
+
+/// layouts
+import ProjectLayout from "layouts/project-layout";
+// components
+import { ProjectDetailsView } from "components/views/project-details";
+// lib
+import { useMobxStore } from "lib/mobx/store-provider";
+
+const WorkspaceProjectPage = (props: any) => {
+ const SITE_TITLE = props?.project_settings?.project_details?.name || "Plane | Deploy";
+
+ const router = useRouter();
+ const { workspace_slug, project_slug, states, labels, priorities } = router.query;
+
+ const { project: projectStore, issue: issueStore } = useMobxStore();
+
+ useSWR("REVALIDATE_ALL", () => {
+ if (workspace_slug && project_slug) {
+ projectStore.fetchProjectSettings(workspace_slug.toString(), project_slug.toString());
+ const params = {
+ state: states || null,
+ labels: labels || null,
+ priority: priorities || null,
+ };
+ issueStore.fetchPublicIssues(workspace_slug.toString(), project_slug.toString(), params);
+ }
+ });
+
+ return (
+
+
+ {SITE_TITLE}
+
+
+
+ );
+};
+
+export default WorkspaceProjectPage;
diff --git a/apps/space/app/[workspace_slug]/page.tsx b/space/pages/[workspace_slug]/index.tsx
similarity index 92%
rename from apps/space/app/[workspace_slug]/page.tsx
rename to space/pages/[workspace_slug]/index.tsx
index c35662f5a..a844c0763 100644
--- a/apps/space/app/[workspace_slug]/page.tsx
+++ b/space/pages/[workspace_slug]/index.tsx
@@ -1,5 +1,3 @@
-"use client";
-
const WorkspaceProjectPage = () => (
Plane Workspace Space
);
diff --git a/space/pages/_app.tsx b/space/pages/_app.tsx
new file mode 100644
index 000000000..33c137d41
--- /dev/null
+++ b/space/pages/_app.tsx
@@ -0,0 +1,45 @@
+import Head from "next/head";
+import type { AppProps } from "next/app";
+import { ThemeProvider } from "next-themes";
+// styles
+import "styles/globals.css";
+import "styles/editor.css";
+// contexts
+import { ToastContextProvider } from "contexts/toast.context";
+// mobx store provider
+import { MobxStoreProvider } from "lib/mobx/store-provider";
+import MobxStoreInit from "lib/mobx/store-init";
+// constants
+import { SITE_NAME, SITE_DESCRIPTION, SITE_URL, TWITTER_USER_NAME, SITE_KEYWORDS, SITE_TITLE } from "constants/seo";
+
+const prefix = parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0") === 0 ? "/" : "/spaces/";
+
+function MyApp({ Component, pageProps }: AppProps) {
+ return (
+
+
+
+ {SITE_TITLE}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default MyApp;
diff --git a/apps/space/pages/_document.tsx b/space/pages/_document.tsx
similarity index 80%
rename from apps/space/pages/_document.tsx
rename to space/pages/_document.tsx
index 8b41d05fc..ca023f4a4 100644
--- a/apps/space/pages/_document.tsx
+++ b/space/pages/_document.tsx
@@ -5,7 +5,7 @@ class MyDocument extends Document {
return (
-
+
diff --git a/space/pages/index.tsx b/space/pages/index.tsx
new file mode 100644
index 000000000..fe0b7d33a
--- /dev/null
+++ b/space/pages/index.tsx
@@ -0,0 +1,8 @@
+import React from "react";
+
+// components
+import { HomeView } from "components/views";
+
+const HomePage = () =>
;
+
+export default HomePage;
diff --git a/space/pages/onboarding/index.tsx b/space/pages/onboarding/index.tsx
new file mode 100644
index 000000000..12b09641b
--- /dev/null
+++ b/space/pages/onboarding/index.tsx
@@ -0,0 +1,45 @@
+import React, { useEffect } from "react";
+// mobx
+import { observer } from "mobx-react-lite";
+import { useMobxStore } from "lib/mobx/store-provider";
+// components
+import { OnBoardingForm } from "components/accounts/onboarding-form";
+
+const imagePrefix = Boolean(parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0")) ? "/spaces" : "";
+
+const OnBoardingPage = () => {
+ const { user: userStore } = useMobxStore();
+
+ const user = userStore?.currentUser;
+
+ useEffect(() => {
+ const user = userStore?.currentUser;
+
+ if (!user) {
+ userStore.fetchCurrentUser();
+ }
+ }, [userStore]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {user?.email}
+
+
+
+
+
+
+
+ );
+};
+
+export default observer(OnBoardingPage);
diff --git a/apps/space/app/project-not-published/page.tsx b/space/pages/project-not-published/index.tsx
similarity index 87%
rename from apps/space/app/project-not-published/page.tsx
rename to space/pages/project-not-published/index.tsx
index 82a2ff5da..985b7bc41 100644
--- a/apps/space/app/project-not-published/page.tsx
+++ b/space/pages/project-not-published/index.tsx
@@ -1,12 +1,13 @@
// next imports
import Image from "next/image";
+import projectNotPublishedImage from "public/project-not-published.svg";
const CustomProjectNotPublishedError = () => (
-
+
Oops! The page you{`'`}re looking for isn{`'`}t live at the moment.
diff --git a/space/postcss.config.js b/space/postcss.config.js
new file mode 100644
index 000000000..129aa7f59
--- /dev/null
+++ b/space/postcss.config.js
@@ -0,0 +1 @@
+module.exports = require("tailwind-config-custom/postcss.config");
diff --git a/apps/app/public/404.svg b/space/public/404.svg
similarity index 100%
rename from apps/app/public/404.svg
rename to space/public/404.svg
diff --git a/apps/app/public/favicon/android-chrome-192x192.png b/space/public/favicon/android-chrome-192x192.png
similarity index 100%
rename from apps/app/public/favicon/android-chrome-192x192.png
rename to space/public/favicon/android-chrome-192x192.png
diff --git a/apps/app/public/favicon/android-chrome-512x512.png b/space/public/favicon/android-chrome-512x512.png
similarity index 100%
rename from apps/app/public/favicon/android-chrome-512x512.png
rename to space/public/favicon/android-chrome-512x512.png
diff --git a/apps/app/public/favicon/apple-touch-icon.png b/space/public/favicon/apple-touch-icon.png
similarity index 100%
rename from apps/app/public/favicon/apple-touch-icon.png
rename to space/public/favicon/apple-touch-icon.png
diff --git a/apps/app/public/favicon/favicon-16x16.png b/space/public/favicon/favicon-16x16.png
similarity index 100%
rename from apps/app/public/favicon/favicon-16x16.png
rename to space/public/favicon/favicon-16x16.png
diff --git a/apps/app/public/favicon/favicon-32x32.png b/space/public/favicon/favicon-32x32.png
similarity index 100%
rename from apps/app/public/favicon/favicon-32x32.png
rename to space/public/favicon/favicon-32x32.png
diff --git a/apps/app/public/favicon/favicon.ico b/space/public/favicon/favicon.ico
similarity index 100%
rename from apps/app/public/favicon/favicon.ico
rename to space/public/favicon/favicon.ico
diff --git a/apps/app/public/favicon/site.webmanifest b/space/public/favicon/site.webmanifest
similarity index 100%
rename from apps/app/public/favicon/site.webmanifest
rename to space/public/favicon/site.webmanifest
diff --git a/space/public/logos/github-black.svg b/space/public/logos/github-black.svg
new file mode 100644
index 000000000..ad04a798e
--- /dev/null
+++ b/space/public/logos/github-black.svg
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
diff --git a/space/public/logos/github-square.svg b/space/public/logos/github-square.svg
new file mode 100644
index 000000000..a7836db8f
--- /dev/null
+++ b/space/public/logos/github-square.svg
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
diff --git a/space/public/logos/github-white.svg b/space/public/logos/github-white.svg
new file mode 100644
index 000000000..90fe34d8b
--- /dev/null
+++ b/space/public/logos/github-white.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
diff --git a/space/public/plane-logo.svg b/space/public/plane-logo.svg
new file mode 100644
index 000000000..11179417c
--- /dev/null
+++ b/space/public/plane-logo.svg
@@ -0,0 +1,94 @@
+
+
+
+
diff --git a/apps/app/public/plane-logos/black-horizontal-with-blue-logo.svg b/space/public/plane-logos/black-horizontal-with-blue-logo.svg
similarity index 100%
rename from apps/app/public/plane-logos/black-horizontal-with-blue-logo.svg
rename to space/public/plane-logos/black-horizontal-with-blue-logo.svg
diff --git a/apps/app/public/plane-logos/blue-without-text.png b/space/public/plane-logos/blue-without-text.png
similarity index 100%
rename from apps/app/public/plane-logos/blue-without-text.png
rename to space/public/plane-logos/blue-without-text.png
diff --git a/apps/app/public/plane-logos/white-horizontal-with-blue-logo.svg b/space/public/plane-logos/white-horizontal-with-blue-logo.svg
similarity index 100%
rename from apps/app/public/plane-logos/white-horizontal-with-blue-logo.svg
rename to space/public/plane-logos/white-horizontal-with-blue-logo.svg
diff --git a/apps/app/public/plane-logos/white-horizontal.svg b/space/public/plane-logos/white-horizontal.svg
similarity index 100%
rename from apps/app/public/plane-logos/white-horizontal.svg
rename to space/public/plane-logos/white-horizontal.svg
diff --git a/apps/space/public/project-not-published.svg b/space/public/project-not-published.svg
similarity index 100%
rename from apps/space/public/project-not-published.svg
rename to space/public/project-not-published.svg
diff --git a/space/public/site.webmanifest.json b/space/public/site.webmanifest.json
new file mode 100644
index 000000000..4c32ec6e3
--- /dev/null
+++ b/space/public/site.webmanifest.json
@@ -0,0 +1,13 @@
+{
+ "name": "Plane Space",
+ "short_name": "Plane Space",
+ "description": "Plane helps you plan your issues, cycles, and product modules.",
+ "start_url": ".",
+ "display": "standalone",
+ "background_color": "#f9fafb",
+ "theme_color": "#3f76ff",
+ "icons": [
+ { "src": "/favicon/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
+ { "src": "/favicon/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
+ ]
+}
diff --git a/space/public/something-went-wrong.svg b/space/public/something-went-wrong.svg
new file mode 100644
index 000000000..bd51f7f49
--- /dev/null
+++ b/space/public/something-went-wrong.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/space/public/user-logged-in.svg b/space/public/user-logged-in.svg
new file mode 100644
index 000000000..e20b49e82
--- /dev/null
+++ b/space/public/user-logged-in.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/apps/space/services/api.service.ts b/space/services/api.service.ts
similarity index 87%
rename from apps/space/services/api.service.ts
rename to space/services/api.service.ts
index 4cf26d9ee..d3ad3949b 100644
--- a/apps/space/services/api.service.ts
+++ b/space/services/api.service.ts
@@ -90,6 +90,16 @@ abstract class APIService {
});
}
+ mediaUpload(url: string, data = {}, config = {}): Promise
{
+ return axios({
+ method: "post",
+ url: this.baseURL + url,
+ data,
+ headers: this.getAccessToken() ? { ...this.getHeaders(), "Content-Type": "multipart/form-data" } : {},
+ ...config,
+ });
+ }
+
request(config = {}) {
return axios(config);
}
diff --git a/space/services/authentication.service.ts b/space/services/authentication.service.ts
new file mode 100644
index 000000000..4d861994f
--- /dev/null
+++ b/space/services/authentication.service.ts
@@ -0,0 +1,93 @@
+// services
+import APIService from "services/api.service";
+import { API_BASE_URL } from "helpers/common.helper";
+
+class AuthService extends APIService {
+ constructor() {
+ super(API_BASE_URL);
+ }
+
+ async emailLogin(data: any) {
+ return this.post("/api/sign-in/", data, { headers: {} })
+ .then((response) => {
+ this.setAccessToken(response?.data?.access_token);
+ this.setRefreshToken(response?.data?.refresh_token);
+ return response?.data;
+ })
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+
+ async emailSignUp(data: { email: string; password: string }) {
+ return this.post("/api/sign-up/", data, { headers: {} })
+ .then((response) => {
+ this.setAccessToken(response?.data?.access_token);
+ this.setRefreshToken(response?.data?.refresh_token);
+ return response?.data;
+ })
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+
+ async socialAuth(data: any): Promise<{
+ access_token: string;
+ refresh_toke: string;
+ user: any;
+ }> {
+ return this.post("/api/social-auth/", data, { headers: {} })
+ .then((response) => {
+ this.setAccessToken(response?.data?.access_token);
+ this.setRefreshToken(response?.data?.refresh_token);
+ return response?.data;
+ })
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+
+ async emailCode(data: any) {
+ return this.post("/api/magic-generate/", data, { headers: {} })
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+
+ async forgotPassword(data: { email: string }): Promise {
+ return this.post(`/api/forgot-password/`, data)
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response;
+ });
+ }
+
+ async magicSignIn(data: any) {
+ const response = await this.post("/api/magic-sign-in/", data, { headers: {} });
+ if (response?.status === 200) {
+ this.setAccessToken(response?.data?.access_token);
+ this.setRefreshToken(response?.data?.refresh_token);
+ return response?.data;
+ }
+ throw response.response.data;
+ }
+
+ async signOut() {
+ return this.post("/api/sign-out/", { refresh_token: this.getRefreshToken() })
+ .then((response) => {
+ this.purgeAccessToken();
+ this.purgeRefreshToken();
+ return response?.data;
+ })
+ .catch((error) => {
+ this.purgeAccessToken();
+ this.purgeRefreshToken();
+ throw error?.response?.data;
+ });
+ }
+}
+
+const authService = new AuthService();
+
+export default authService;
diff --git a/space/services/file.service.ts b/space/services/file.service.ts
new file mode 100644
index 000000000..d9783d29c
--- /dev/null
+++ b/space/services/file.service.ts
@@ -0,0 +1,99 @@
+import APIService from "services/api.service";
+import { API_BASE_URL } from "helpers/common.helper";
+
+interface UnSplashImage {
+ id: string;
+ created_at: Date;
+ updated_at: Date;
+ promoted_at: Date;
+ width: number;
+ height: number;
+ color: string;
+ blur_hash: string;
+ description: null;
+ alt_description: string;
+ urls: UnSplashImageUrls;
+ [key: string]: any;
+}
+
+interface UnSplashImageUrls {
+ raw: string;
+ full: string;
+ regular: string;
+ small: string;
+ thumb: string;
+ small_s3: string;
+}
+
+class FileServices extends APIService {
+ constructor() {
+ super(API_BASE_URL);
+ }
+
+ async uploadFile(workspaceSlug: string, file: FormData): Promise {
+ return this.mediaUpload(`/api/workspaces/${workspaceSlug}/file-assets/`, file)
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+
+ async deleteImage(assetUrlWithWorkspaceId: string): Promise {
+ return this.delete(`/api/workspaces/file-assets/${assetUrlWithWorkspaceId}/`)
+ .then((response) => response?.status)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+
+ async deleteFile(workspaceId: string, assetUrl: string): Promise {
+ const lastIndex = assetUrl.lastIndexOf("/");
+ const assetId = assetUrl.substring(lastIndex + 1);
+
+ return this.delete(`/api/workspaces/file-assets/${workspaceId}/${assetId}/`)
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+ async uploadUserFile(file: FormData): Promise {
+ return this.mediaUpload(`/api/users/file-assets/`, file)
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+
+ async deleteUserFile(assetUrl: string): Promise {
+ const lastIndex = assetUrl.lastIndexOf("/");
+ const assetId = assetUrl.substring(lastIndex + 1);
+
+ return this.delete(`/api/users/file-assets/${assetId}`)
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+
+ async getUnsplashImages(page: number = 1, query?: string): Promise {
+ const url = "/api/unsplash";
+
+ return this.request({
+ method: "get",
+ url,
+ params: {
+ page,
+ per_page: 20,
+ query,
+ },
+ })
+ .then((response) => response?.data?.results ?? response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+}
+
+const fileServices = new FileServices();
+
+export default fileServices;
diff --git a/space/services/issue.service.ts b/space/services/issue.service.ts
new file mode 100644
index 000000000..5feb1b00b
--- /dev/null
+++ b/space/services/issue.service.ts
@@ -0,0 +1,169 @@
+// services
+import APIService from "services/api.service";
+import { API_BASE_URL } from "helpers/common.helper";
+
+class IssueService extends APIService {
+ constructor() {
+ super(API_BASE_URL);
+ }
+
+ async getPublicIssues(workspace_slug: string, project_slug: string, params: any): Promise {
+ return this.get(`/api/public/workspaces/${workspace_slug}/project-boards/${project_slug}/issues/`, {
+ params,
+ })
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response;
+ });
+ }
+
+ async getIssueById(workspaceSlug: string, projectId: string, issueId: string): Promise {
+ return this.get(`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/`)
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response;
+ });
+ }
+
+ async getIssueVotes(workspaceSlug: string, projectId: string, issueId: string): Promise {
+ return this.get(`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/votes/`)
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response;
+ });
+ }
+
+ async createIssueVote(workspaceSlug: string, projectId: string, issueId: string, data: any): Promise {
+ return this.post(
+ `/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/votes/`,
+ data
+ )
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response;
+ });
+ }
+
+ async deleteIssueVote(workspaceSlug: string, projectId: string, issueId: string): Promise {
+ return this.delete(`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/votes/`)
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response;
+ });
+ }
+
+ async getIssueReactions(workspaceSlug: string, projectId: string, issueId: string): Promise {
+ return this.get(`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/reactions/`)
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response;
+ });
+ }
+
+ async createIssueReaction(workspaceSlug: string, projectId: string, issueId: string, data: any): Promise {
+ return this.post(
+ `/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/reactions/`,
+ data
+ )
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response;
+ });
+ }
+
+ async deleteIssueReaction(
+ workspaceSlug: string,
+ projectId: string,
+ issueId: string,
+ reactionId: string
+ ): Promise {
+ return this.delete(
+ `/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/reactions/${reactionId}/`
+ )
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response;
+ });
+ }
+
+ async getIssueComments(workspaceSlug: string, projectId: string, issueId: string): Promise {
+ return this.get(`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/comments/`)
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response;
+ });
+ }
+
+ async createIssueComment(workspaceSlug: string, projectId: string, issueId: string, data: any): Promise {
+ return this.post(
+ `/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/comments/`,
+ data
+ )
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response;
+ });
+ }
+
+ async updateIssueComment(
+ workspaceSlug: string,
+ projectId: string,
+ issueId: string,
+ commentId: string,
+ data: any
+ ): Promise {
+ return this.patch(
+ `/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/comments/${commentId}/`,
+ data
+ )
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response;
+ });
+ }
+
+ async deleteIssueComment(workspaceSlug: string, projectId: string, issueId: string, commentId: string): Promise {
+ return this.delete(
+ `/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/comments/${commentId}/`
+ )
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response;
+ });
+ }
+
+ async createCommentReaction(
+ workspaceSlug: string,
+ projectId: string,
+ commentId: string,
+ data: {
+ reaction: string;
+ }
+ ): Promise {
+ return this.post(
+ `/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/comments/${commentId}/reactions/`,
+ data
+ )
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response;
+ });
+ }
+
+ async deleteCommentReaction(
+ workspaceSlug: string,
+ projectId: string,
+ commentId: string,
+ reactionHex: string
+ ): Promise {
+ return this.delete(
+ `/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/comments/${commentId}/reactions/${reactionHex}/`
+ )
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response;
+ });
+ }
+}
+
+export default IssueService;
diff --git a/apps/space/services/project.service.ts b/space/services/project.service.ts
similarity index 68%
rename from apps/space/services/project.service.ts
rename to space/services/project.service.ts
index 6f0275877..0d6eca951 100644
--- a/apps/space/services/project.service.ts
+++ b/space/services/project.service.ts
@@ -1,12 +1,13 @@
// services
import APIService from "services/api.service";
+import { API_BASE_URL } from "helpers/common.helper";
class ProjectService extends APIService {
constructor() {
- super(process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
+ super(API_BASE_URL);
}
- async getProjectSettingsAsync(workspace_slug: string, project_slug: string): Promise {
+ async getProjectSettings(workspace_slug: string, project_slug: string): Promise {
return this.get(`/api/public/workspaces/${workspace_slug}/project-boards/${project_slug}/settings/`)
.then((response) => response?.data)
.catch((error) => {
diff --git a/apps/space/services/user.service.ts b/space/services/user.service.ts
similarity index 54%
rename from apps/space/services/user.service.ts
rename to space/services/user.service.ts
index a84358a96..21e9f941e 100644
--- a/apps/space/services/user.service.ts
+++ b/space/services/user.service.ts
@@ -1,9 +1,10 @@
// services
import APIService from "services/api.service";
+import { API_BASE_URL } from "helpers/common.helper";
class UserService extends APIService {
constructor() {
- super(process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
+ super(API_BASE_URL);
}
async currentUser(): Promise {
@@ -13,6 +14,14 @@ class UserService extends APIService {
throw error?.response;
});
}
+
+ async updateMe(data: any): Promise {
+ return this.patch("/api/users/me/", data)
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
}
export default UserService;
diff --git a/space/store/issue.ts b/space/store/issue.ts
new file mode 100644
index 000000000..d47336984
--- /dev/null
+++ b/space/store/issue.ts
@@ -0,0 +1,103 @@
+import { observable, action, computed, makeObservable, runInAction, reaction } from "mobx";
+// services
+import IssueService from "services/issue.service";
+// store
+import { RootStore } from "./root";
+// types
+// import { IssueDetailType, TIssueBoardKeys } from "types/issue";
+import { IIssue, IIssueState, IIssueLabel } from "types/issue";
+
+export interface IIssueStore {
+ loader: boolean;
+ error: any;
+ // issue options
+ issues: IIssue[] | null;
+ states: IIssueState[] | null;
+ labels: IIssueLabel[] | null;
+ // filtering
+ filteredStates: string[];
+ filteredLabels: string[];
+ filteredPriorities: string[];
+ // service
+ issueService: any;
+ // actions
+ fetchPublicIssues: (workspace_slug: string, project_slug: string, params: any) => void;
+ getCountOfIssuesByState: (state: string) => number;
+ getFilteredIssuesByState: (state: string) => IIssue[];
+}
+
+class IssueStore implements IIssueStore {
+ loader: boolean = false;
+ error: any | null = null;
+
+ states: IIssueState[] | null = [];
+ labels: IIssueLabel[] | null = [];
+
+ filteredStates: string[] = [];
+ filteredLabels: string[] = [];
+ filteredPriorities: string[] = [];
+
+ issues: IIssue[] | null = [];
+ issue_detail: any = {};
+
+ rootStore: RootStore;
+ issueService: any;
+
+ constructor(_rootStore: any) {
+ makeObservable(this, {
+ // observable
+ loader: observable,
+ error: observable,
+ // issue options
+ states: observable.ref,
+ labels: observable.ref,
+ // filtering
+ filteredStates: observable.ref,
+ filteredLabels: observable.ref,
+ filteredPriorities: observable.ref,
+ // issues
+ issues: observable.ref,
+ issue_detail: observable.ref,
+ // actions
+ fetchPublicIssues: action,
+ getFilteredIssuesByState: action,
+ });
+
+ this.rootStore = _rootStore;
+ this.issueService = new IssueService();
+ }
+
+ fetchPublicIssues = async (workspaceSlug: string, projectId: string, params: any) => {
+ try {
+ this.loader = true;
+ this.error = null;
+
+ const response = await this.issueService.getPublicIssues(workspaceSlug, projectId, params);
+
+ if (response) {
+ const _states: IIssueState[] = [...response?.states];
+ const _labels: IIssueLabel[] = [...response?.labels];
+ const _issues: IIssue[] = [...response?.issues];
+ runInAction(() => {
+ this.states = _states;
+ this.labels = _labels;
+ this.issues = _issues;
+ this.loader = false;
+ });
+ }
+ } catch (error) {
+ this.loader = false;
+ this.error = error;
+ }
+ };
+
+ // computed
+ getCountOfIssuesByState(state_id: string): number {
+ return this.issues?.filter((issue) => issue.state == state_id).length || 0;
+ }
+
+ getFilteredIssuesByState = (state_id: string): IIssue[] | [] =>
+ this.issues?.filter((issue) => issue.state == state_id) || [];
+}
+
+export default IssueStore;
diff --git a/space/store/issue_details.ts b/space/store/issue_details.ts
new file mode 100644
index 000000000..346206e94
--- /dev/null
+++ b/space/store/issue_details.ts
@@ -0,0 +1,434 @@
+import { makeObservable, observable, action, runInAction } from "mobx";
+import { v4 as uuidv4 } from "uuid";
+// store
+import { RootStore } from "./root";
+// services
+import IssueService from "services/issue.service";
+import { IIssue, IVote } from "types/issue";
+
+export type IPeekMode = "side" | "modal" | "full";
+
+export interface IIssueDetailStore {
+ loader: boolean;
+ error: any;
+ // peek info
+ peekId: string | null;
+ peekMode: IPeekMode;
+ details: {
+ [key: string]: IIssue;
+ };
+ // peek actions
+ setPeekId: (issueId: string | null) => void;
+ setPeekMode: (mode: IPeekMode) => void;
+ // issue details
+ fetchIssueDetails: (workspaceId: string, projectId: string, issueId: string) => void;
+ // issue comments
+ addIssueComment: (workspaceId: string, projectId: string, issueId: string, data: any) => Promise;
+ updateIssueComment: (
+ workspaceId: string,
+ projectId: string,
+ issueId: string,
+ comment_id: string,
+ data: any
+ ) => Promise;
+ deleteIssueComment: (workspaceId: string, projectId: string, issueId: string, comment_id: string) => void;
+ addCommentReaction: (
+ workspaceId: string,
+ projectId: string,
+ issueId: string,
+ commentId: string,
+ reactionHex: string
+ ) => void;
+ removeCommentReaction: (
+ workspaceId: string,
+ projectId: string,
+ issueId: string,
+ commentId: string,
+ reactionHex: string
+ ) => void;
+ // issue reactions
+ addIssueReaction: (workspaceId: string, projectId: string, issueId: string, reactionHex: string) => void;
+ removeIssueReaction: (workspaceId: string, projectId: string, issueId: string, reactionHex: string) => void;
+ // issue votes
+ addIssueVote: (workspaceId: string, projectId: string, issueId: string, data: { vote: 1 | -1 }) => Promise;
+ removeIssueVote: (workspaceId: string, projectId: string, issueId: string) => Promise;
+}
+
+class IssueDetailStore implements IssueDetailStore {
+ loader: boolean = false;
+ error: any = null;
+ peekId: string | null = null;
+ peekMode: IPeekMode = "side";
+ details: {
+ [key: string]: IIssue;
+ } = {};
+ issueService;
+ rootStore: RootStore;
+
+ constructor(_rootStore: RootStore) {
+ makeObservable(this, {
+ loader: observable.ref,
+ error: observable.ref,
+ // peek
+ peekId: observable.ref,
+ peekMode: observable.ref,
+ details: observable.ref,
+ // actions
+ setPeekId: action,
+ setPeekMode: action,
+ fetchIssueDetails: action,
+ addIssueComment: action,
+ updateIssueComment: action,
+ deleteIssueComment: action,
+ addCommentReaction: action,
+ removeCommentReaction: action,
+ addIssueReaction: action,
+ removeIssueReaction: action,
+ addIssueVote: action,
+ removeIssueVote: action,
+ });
+ this.issueService = new IssueService();
+ this.rootStore = _rootStore;
+ }
+
+ setPeekId = (issueId: string | null) => {
+ this.peekId = issueId;
+ };
+
+ setPeekMode = (mode: IPeekMode) => {
+ this.peekMode = mode;
+ };
+
+ fetchIssueDetails = async (workspaceSlug: string, projectId: string, issueId: string) => {
+ try {
+ this.loader = true;
+ this.error = null;
+
+ const issueDetails = this.rootStore.issue.issues?.find((i) => i.id === issueId);
+ const commentsResponse = await this.issueService.getIssueComments(workspaceSlug, projectId, issueId);
+
+ if (issueDetails) {
+ runInAction(() => {
+ this.details = {
+ ...this.details,
+ [issueId]: {
+ ...(this.details[issueId] ?? issueDetails),
+ comments: commentsResponse,
+ },
+ };
+ });
+ }
+ } catch (error) {
+ this.loader = false;
+ this.error = error;
+ }
+ };
+
+ addIssueComment = async (workspaceSlug: string, projectId: string, issueId: string, data: any) => {
+ try {
+ const issueDetails = this.rootStore.issue.issues?.find((i) => i.id === issueId);
+ const issueCommentResponse = await this.issueService.createIssueComment(workspaceSlug, projectId, issueId, data);
+ if (issueDetails) {
+ runInAction(() => {
+ this.details = {
+ ...this.details,
+ [issueId]: {
+ ...issueDetails,
+ comments: [...this.details[issueId].comments, issueCommentResponse],
+ },
+ };
+ });
+ }
+ return issueCommentResponse;
+ } catch (error) {
+ console.log("Failed to add issue comment");
+ throw error;
+ }
+ };
+
+ updateIssueComment = async (
+ workspaceSlug: string,
+ projectId: string,
+ issueId: string,
+ commentId: string,
+ data: any
+ ) => {
+ try {
+ runInAction(() => {
+ this.details = {
+ ...this.details,
+ [issueId]: {
+ ...this.details[issueId],
+ comments: this.details[issueId].comments.map((c) => ({
+ ...c,
+ ...(c.id === commentId ? data : {}),
+ })),
+ },
+ };
+ });
+
+ await this.issueService.updateIssueComment(workspaceSlug, projectId, issueId, commentId, data);
+ } catch (error) {
+ const issueComments = await this.issueService.getIssueComments(workspaceSlug, projectId, issueId);
+
+ runInAction(() => {
+ this.details = {
+ ...this.details,
+ [issueId]: {
+ ...this.details[issueId],
+ comments: issueComments,
+ },
+ };
+ });
+ }
+ };
+
+ deleteIssueComment = async (workspaceSlug: string, projectId: string, issueId: string, comment_id: string) => {
+ try {
+ await this.issueService.deleteIssueComment(workspaceSlug, projectId, issueId, comment_id);
+ const remainingComments = this.details[issueId].comments.filter((c) => c.id != comment_id);
+ runInAction(() => {
+ this.details = {
+ ...this.details,
+ [issueId]: {
+ ...this.details[issueId],
+ comments: remainingComments,
+ },
+ };
+ });
+ } catch (error) {
+ console.log("Failed to add issue vote");
+ }
+ };
+
+ addCommentReaction = async (
+ workspaceSlug: string,
+ projectId: string,
+ issueId: string,
+ commentId: string,
+ reactionHex: string
+ ) => {
+ const newReaction = {
+ id: uuidv4(),
+ comment: commentId,
+ reaction: reactionHex,
+ actor_detail: this.rootStore.user.currentActor,
+ };
+ const newComments = this.details[issueId].comments.map((comment) => ({
+ ...comment,
+ comment_reactions:
+ comment.id === commentId ? [...comment.comment_reactions, newReaction] : comment.comment_reactions,
+ }));
+
+ try {
+ runInAction(() => {
+ this.details = {
+ ...this.details,
+ [issueId]: {
+ ...this.details[issueId],
+ comments: [...newComments],
+ },
+ };
+ });
+
+ await this.issueService.createCommentReaction(workspaceSlug, projectId, commentId, {
+ reaction: reactionHex,
+ });
+ } catch (error) {
+ const issueComments = await this.issueService.getIssueComments(workspaceSlug, projectId, issueId);
+
+ runInAction(() => {
+ this.details = {
+ ...this.details,
+ [issueId]: {
+ ...this.details[issueId],
+ comments: issueComments,
+ },
+ };
+ });
+ }
+ };
+
+ removeCommentReaction = async (
+ workspaceSlug: string,
+ projectId: string,
+ issueId: string,
+ commentId: string,
+ reactionHex: string
+ ) => {
+ try {
+ const comment = this.details[issueId].comments.find((c) => c.id === commentId);
+ const newCommentReactions = comment?.comment_reactions.filter((r) => r.reaction !== reactionHex) ?? [];
+
+ runInAction(() => {
+ this.details = {
+ ...this.details,
+ [issueId]: {
+ ...this.details[issueId],
+ comments: this.details[issueId].comments.map((c) => ({
+ ...c,
+ comment_reactions: c.id === commentId ? newCommentReactions : c.comment_reactions,
+ })),
+ },
+ };
+ });
+
+ await this.issueService.deleteCommentReaction(workspaceSlug, projectId, commentId, reactionHex);
+ } catch (error) {
+ const issueComments = await this.issueService.getIssueComments(workspaceSlug, projectId, issueId);
+
+ runInAction(() => {
+ this.details = {
+ ...this.details,
+ [issueId]: {
+ ...this.details[issueId],
+ comments: issueComments,
+ },
+ };
+ });
+ }
+ };
+
+ addIssueReaction = async (workspaceSlug: string, projectId: string, issueId: string, reactionHex: string) => {
+ try {
+ runInAction(() => {
+ this.details = {
+ ...this.details,
+ [issueId]: {
+ ...this.details[issueId],
+ reactions: [
+ ...this.details[issueId].reactions,
+ {
+ id: uuidv4(),
+ issue: issueId,
+ reaction: reactionHex,
+ actor_detail: this.rootStore.user.currentActor,
+ },
+ ],
+ },
+ };
+ });
+
+ await this.issueService.createIssueReaction(workspaceSlug, projectId, issueId, {
+ reaction: reactionHex,
+ });
+ } catch (error) {
+ console.log("Failed to add issue vote");
+ const issueReactions = await this.issueService.getIssueReactions(workspaceSlug, projectId, issueId);
+ runInAction(() => {
+ this.details = {
+ ...this.details,
+ [issueId]: {
+ ...this.details[issueId],
+ reactions: issueReactions,
+ },
+ };
+ });
+ }
+ };
+
+ removeIssueReaction = async (workspaceSlug: string, projectId: string, issueId: string, reactionHex: string) => {
+ try {
+ const newReactions = this.details[issueId].reactions.filter(
+ (_r) => !(_r.reaction === reactionHex && _r.actor_detail.id === this.rootStore.user.currentUser?.id)
+ );
+
+ runInAction(() => {
+ this.details = {
+ ...this.details,
+ [issueId]: {
+ ...this.details[issueId],
+ reactions: newReactions,
+ },
+ };
+ });
+
+ await this.issueService.deleteIssueReaction(workspaceSlug, projectId, issueId, reactionHex);
+ } catch (error) {
+ console.log("Failed to remove issue reaction");
+ const reactions = await this.issueService.getIssueReactions(workspaceSlug, projectId, issueId);
+ runInAction(() => {
+ this.details = {
+ ...this.details,
+ [issueId]: {
+ ...this.details[issueId],
+ reactions: reactions,
+ },
+ };
+ });
+ }
+ };
+
+ addIssueVote = async (workspaceSlug: string, projectId: string, issueId: string, data: { vote: 1 | -1 }) => {
+ const newVote: IVote = {
+ actor: this.rootStore.user.currentUser?.id ?? "",
+ actor_detail: this.rootStore.user.currentActor,
+ issue: issueId,
+ project: projectId,
+ workspace: workspaceSlug,
+ vote: data.vote,
+ };
+
+ const filteredVotes = this.details[issueId].votes.filter((v) => v.actor !== this.rootStore.user.currentUser?.id);
+
+ try {
+ runInAction(() => {
+ this.details = {
+ ...this.details,
+ [issueId]: {
+ ...this.details[issueId],
+ votes: [...filteredVotes, newVote],
+ },
+ };
+ });
+
+ await this.issueService.createIssueVote(workspaceSlug, projectId, issueId, data);
+ } catch (error) {
+ console.log("Failed to add issue vote");
+ const issueVotes = await this.issueService.getIssueVotes(workspaceSlug, projectId, issueId);
+
+ runInAction(() => {
+ this.details = {
+ ...this.details,
+ [issueId]: {
+ ...this.details[issueId],
+ votes: issueVotes,
+ },
+ };
+ });
+ }
+ };
+
+ removeIssueVote = async (workspaceSlug: string, projectId: string, issueId: string) => {
+ const newVotes = this.details[issueId].votes.filter((v) => v.actor !== this.rootStore.user.currentUser?.id);
+
+ try {
+ runInAction(() => {
+ this.details = {
+ ...this.details,
+ [issueId]: {
+ ...this.details[issueId],
+ votes: newVotes,
+ },
+ };
+ });
+
+ await this.issueService.deleteIssueVote(workspaceSlug, projectId, issueId);
+ } catch (error) {
+ console.log("Failed to remove issue vote");
+ const issueVotes = await this.issueService.getIssueVotes(workspaceSlug, projectId, issueId);
+
+ runInAction(() => {
+ this.details = {
+ ...this.details,
+ [issueId]: {
+ ...this.details[issueId],
+ votes: issueVotes,
+ },
+ };
+ });
+ }
+ };
+}
+
+export default IssueDetailStore;
diff --git a/apps/space/store/project.ts b/space/store/project.ts
similarity index 52%
rename from apps/space/store/project.ts
rename to space/store/project.ts
index e5ac58261..ddd589f9a 100644
--- a/apps/space/store/project.ts
+++ b/space/store/project.ts
@@ -3,15 +3,29 @@ import { observable, action, makeObservable, runInAction } from "mobx";
// service
import ProjectService from "services/project.service";
// types
-import { IProjectStore, IWorkspace, IProject, IProjectSettings } from "./types";
+import { IWorkspace, IProject, IProjectSettings } from "types/project";
+
+export interface IProjectStore {
+ loader: boolean;
+ error: any | null;
+ workspace: IWorkspace | null;
+ project: IProject | null;
+ deploySettings: IProjectSettings | null;
+ viewOptions: any;
+ activeBoard: string | null;
+ fetchProjectSettings: (workspace_slug: string, project_slug: string) => Promise;
+ setActiveBoard: (value: string) => void;
+}
class ProjectStore implements IProjectStore {
loader: boolean = false;
error: any | null = null;
-
+ // data
workspace: IWorkspace | null = null;
project: IProject | null = null;
- workspaceProjectSettings: IProjectSettings | null = null;
+ deploySettings: IProjectSettings | null = null;
+ viewOptions: any = null;
+ activeBoard: string | null = null;
// root store
rootStore;
// service
@@ -19,14 +33,18 @@ class ProjectStore implements IProjectStore {
constructor(_rootStore: any | null = null) {
makeObservable(this, {
+ // loaders and error observables
+ loader: observable,
+ error: observable.ref,
// observable
workspace: observable.ref,
project: observable.ref,
- workspaceProjectSettings: observable.ref,
- loader: observable,
- error: observable.ref,
- // action
- getProjectSettingsAsync: action,
+ deploySettings: observable.ref,
+ viewOptions: observable.ref,
+ activeBoard: observable.ref,
+ // actions
+ fetchProjectSettings: action,
+ setActiveBoard: action,
// computed
});
@@ -34,26 +52,23 @@ class ProjectStore implements IProjectStore {
this.projectService = new ProjectService();
}
- getProjectSettingsAsync = async (workspace_slug: string, project_slug: string) => {
+ fetchProjectSettings = async (workspace_slug: string, project_slug: string) => {
try {
this.loader = true;
this.error = null;
- const response = await this.projectService.getProjectSettingsAsync(workspace_slug, project_slug);
+ const response = await this.projectService.getProjectSettings(workspace_slug, project_slug);
if (response) {
const _project: IProject = { ...response?.project_details };
const _workspace: IWorkspace = { ...response?.workspace_detail };
- const _workspaceProjectSettings: IProjectSettings = {
- comments: response?.comments,
- reactions: response?.reactions,
- votes: response?.votes,
- views: { ...response?.views },
- };
+ const _viewOptions = { ...response?.views };
+ const _deploySettings = { ...response };
runInAction(() => {
this.project = _project;
this.workspace = _workspace;
- this.workspaceProjectSettings = _workspaceProjectSettings;
+ this.viewOptions = _viewOptions;
+ this.deploySettings = _deploySettings;
this.loader = false;
});
}
@@ -64,6 +79,10 @@ class ProjectStore implements IProjectStore {
return error;
}
};
+
+ setActiveBoard = (boardValue: string) => {
+ this.activeBoard = boardValue;
+ };
}
export default ProjectStore;
diff --git a/apps/space/store/root.ts b/space/store/root.ts
similarity index 57%
rename from apps/space/store/root.ts
rename to space/store/root.ts
index dd6d620c0..6b87020ef 100644
--- a/apps/space/store/root.ts
+++ b/space/store/root.ts
@@ -2,24 +2,22 @@
import { enableStaticRendering } from "mobx-react-lite";
// store imports
import UserStore from "./user";
-import ThemeStore from "./theme";
-import IssueStore from "./issue";
-import ProjectStore from "./project";
-// types
-import { IIssueStore, IProjectStore, IThemeStore, IUserStore } from "./types";
+import IssueStore, { IIssueStore } from "./issue";
+import ProjectStore, { IProjectStore } from "./project";
+import IssueDetailStore, { IIssueDetailStore } from "./issue_details";
enableStaticRendering(typeof window === "undefined");
export class RootStore {
- user: IUserStore;
- theme: IThemeStore;
+ user: UserStore;
issue: IIssueStore;
+ issueDetails: IIssueDetailStore;
project: IProjectStore;
constructor() {
this.user = new UserStore(this);
- this.theme = new ThemeStore(this);
this.issue = new IssueStore(this);
this.project = new ProjectStore(this);
+ this.issueDetails = new IssueDetailStore(this);
}
}
diff --git a/space/store/user.ts b/space/store/user.ts
new file mode 100644
index 000000000..3a76c2111
--- /dev/null
+++ b/space/store/user.ts
@@ -0,0 +1,88 @@
+// mobx
+import { observable, action, computed, makeObservable, runInAction } from "mobx";
+// service
+import UserService from "services/user.service";
+import { ActorDetail } from "types/issue";
+// types
+import { IUser } from "types/user";
+
+export interface IUserStore {
+ currentUser: any | null;
+ fetchCurrentUser: () => void;
+ currentActor: () => any;
+}
+
+class UserStore implements IUserStore {
+ currentUser: IUser | null = null;
+ // root store
+ rootStore;
+ // service
+ userService;
+
+ constructor(_rootStore: any) {
+ makeObservable(this, {
+ // observable
+ currentUser: observable.ref,
+ // actions
+ setCurrentUser: action,
+ // computed
+ currentActor: computed,
+ });
+ this.rootStore = _rootStore;
+ this.userService = new UserService();
+ }
+
+ setCurrentUser = (user: any) => {
+ runInAction(() => {
+ this.currentUser = { ...user };
+ });
+ };
+
+ get currentActor(): any {
+ return {
+ avatar: this.currentUser?.avatar,
+ display_name: this.currentUser?.display_name,
+ first_name: this.currentUser?.first_name,
+ id: this.currentUser?.id,
+ is_bot: false,
+ last_name: this.currentUser?.last_name,
+ };
+ }
+
+ /**
+ *
+ * @param callback
+ * @description A wrapper function to check user authentication; it redirects to the login page if not authenticated, otherwise, it executes a callback.
+ * @example this.requiredLogin(() => { // do something });
+ */
+
+ requiredLogin = (callback: () => void) => {
+ if (this.currentUser) {
+ callback();
+ return;
+ }
+
+ const currentPath = window.location.pathname + window.location.search;
+ this.fetchCurrentUser()
+ .then(() => {
+ if (!this.currentUser) window.location.href = `/?next_path=${currentPath}`;
+ else callback();
+ })
+ .catch(() => (window.location.href = `/?next_path=${currentPath}`));
+ };
+
+ fetchCurrentUser = async () => {
+ try {
+ const response = await this.userService.currentUser();
+ if (response) {
+ runInAction(() => {
+ this.currentUser = response;
+ });
+ }
+ } catch (error) {
+ console.error("Failed to fetch current user", error);
+ }
+ };
+}
+
+export default UserStore;
diff --git a/apps/app/styles/editor.css b/space/styles/editor.css
similarity index 66%
rename from apps/app/styles/editor.css
rename to space/styles/editor.css
index 57c23c911..9da250dd1 100644
--- a/apps/app/styles/editor.css
+++ b/space/styles/editor.css
@@ -30,6 +30,10 @@
}
}
+.ProseMirror-gapcursor:after {
+ border-top: 1px solid rgb(var(--color-text-100)) !important;
+}
+
/* Custom TODO list checkboxes – shoutout to this awesome tutorial: https://moderncss.dev/pure-css-custom-checkbox-style/ */
ul[data-type="taskList"] li > label {
@@ -140,7 +144,7 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
height: 20px;
border-radius: 50%;
border: 3px solid rgba(var(--color-text-200));
- border-top-color: rgba(var(--color-text-800));
+ border-top-color: rgba(var(--color-text-800));
animation: spinning 0.6s linear infinite;
}
}
@@ -150,3 +154,78 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
transform: rotate(360deg);
}
}
+
+#tiptap-container {
+ table {
+ border-collapse: collapse;
+ table-layout: fixed;
+ margin: 0;
+ border: 1px solid rgb(var(--color-border-200));
+ width: 100%;
+
+ td,
+ th {
+ min-width: 1em;
+ border: 1px solid rgb(var(--color-border-200));
+ padding: 10px 15px;
+ vertical-align: top;
+ box-sizing: border-box;
+ position: relative;
+ transition: background-color 0.3s ease;
+
+ > * {
+ margin-bottom: 0;
+ }
+ }
+
+ th {
+ font-weight: bold;
+ text-align: left;
+ background-color: rgb(var(--color-primary-100));
+ }
+
+ td:hover {
+ background-color: rgba(var(--color-primary-300), 0.1);
+ }
+
+ .selectedCell:after {
+ z-index: 2;
+ position: absolute;
+ content: "";
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ background-color: rgba(var(--color-primary-300), 0.1);
+ pointer-events: none;
+ }
+
+ .column-resize-handle {
+ position: absolute;
+ right: -2px;
+ top: 0;
+ bottom: -2px;
+ width: 2px;
+ background-color: rgb(var(--color-primary-400));
+ pointer-events: none;
+ }
+ }
+}
+
+.tableWrapper {
+ overflow-x: auto;
+}
+
+.resize-cursor {
+ cursor: ew-resize;
+ cursor: col-resize;
+}
+
+.ProseMirror table * p {
+ padding: 0px 1px;
+ margin: 6px 2px;
+}
+
+.ProseMirror table * .is-empty::before {
+ opacity: 0;
+}
diff --git a/space/styles/globals.css b/space/styles/globals.css
new file mode 100644
index 000000000..1782b9b81
--- /dev/null
+++ b/space/styles/globals.css
@@ -0,0 +1,234 @@
+@import url("https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800&display=swap");
+@import url("https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@48,400,0,0&display=swap");
+
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ html {
+ font-family: "Inter", sans-serif;
+ }
+
+ :root {
+ color-scheme: light !important;
+
+ --color-primary-10: 236, 241, 255;
+ --color-primary-20: 217, 228, 255;
+ --color-primary-30: 197, 214, 255;
+ --color-primary-40: 178, 200, 255;
+ --color-primary-50: 159, 187, 255;
+ --color-primary-60: 140, 173, 255;
+ --color-primary-70: 121, 159, 255;
+ --color-primary-80: 101, 145, 255;
+ --color-primary-90: 82, 132, 255;
+ --color-primary-100: 63, 118, 255;
+ --color-primary-200: 57, 106, 230;
+ --color-primary-300: 50, 94, 204;
+ --color-primary-400: 44, 83, 179;
+ --color-primary-500: 38, 71, 153;
+ --color-primary-600: 32, 59, 128;
+ --color-primary-700: 25, 47, 102;
+ --color-primary-800: 19, 35, 76;
+ --color-primary-900: 13, 24, 51;
+
+ --color-background-100: 255, 255, 255; /* primary bg */
+ --color-background-90: 250, 250, 250; /* secondary bg */
+ --color-background-80: 245, 245, 245; /* tertiary bg */
+
+ --color-text-100: 23, 23, 23; /* primary text */
+ --color-text-200: 58, 58, 58; /* secondary text */
+ --color-text-300: 82, 82, 82; /* tertiary text */
+ --color-text-400: 163, 163, 163; /* placeholder text */
+
+ --color-border-100: 245, 245, 245; /* subtle border= 1 */
+ --color-border-200: 229, 229, 229; /* subtle border- 2 */
+ --color-border-300: 212, 212, 212; /* strong border- 1 */
+ --color-border-400: 185, 185, 185; /* strong border- 2 */
+
+ --color-shadow-2xs: 0px 0px 1px 0px rgba(23, 23, 23, 0.06), 0px 1px 2px 0px rgba(23, 23, 23, 0.06),
+ 0px 1px 2px 0px rgba(23, 23, 23, 0.14);
+ --color-shadow-xs: 0px 1px 2px 0px rgba(0, 0, 0, 0.16), 0px 2px 4px 0px rgba(16, 24, 40, 0.12),
+ 0px 1px 8px -1px rgba(16, 24, 40, 0.1);
+ --color-shadow-sm: 0px 1px 4px 0px rgba(0, 0, 0, 0.01), 0px 4px 8px 0px rgba(0, 0, 0, 0.02),
+ 0px 1px 12px 0px rgba(0, 0, 0, 0.12);
+ --color-shadow-rg: 0px 3px 6px 0px rgba(0, 0, 0, 0.1), 0px 4px 4px 0px rgba(16, 24, 40, 0.08),
+ 0px 1px 12px 0px rgba(16, 24, 40, 0.04);
+ --color-shadow-md: 0px 4px 8px 0px rgba(0, 0, 0, 0.12), 0px 6px 12px 0px rgba(16, 24, 40, 0.12),
+ 0px 1px 16px 0px rgba(16, 24, 40, 0.12);
+ --color-shadow-lg: 0px 6px 12px 0px rgba(0, 0, 0, 0.12), 0px 8px 16px 0px rgba(0, 0, 0, 0.12),
+ 0px 1px 24px 0px rgba(16, 24, 40, 0.12);
+ --color-shadow-xl: 0px 0px 18px 0px rgba(0, 0, 0, 0.16), 0px 0px 24px 0px rgba(16, 24, 40, 0.16),
+ 0px 0px 52px 0px rgba(16, 24, 40, 0.16);
+ --color-shadow-2xl: 0px 8px 16px 0px rgba(0, 0, 0, 0.12), 0px 12px 24px 0px rgba(16, 24, 40, 0.12),
+ 0px 1px 32px 0px rgba(16, 24, 40, 0.12);
+ --color-shadow-3xl: 0px 12px 24px 0px rgba(0, 0, 0, 0.12), 0px 16px 32px 0px rgba(0, 0, 0, 0.12),
+ 0px 1px 48px 0px rgba(16, 24, 40, 0.12);
+
+ --color-sidebar-background-100: var(--color-background-100); /* primary sidebar bg */
+ --color-sidebar-background-90: var(--color-background-90); /* secondary sidebar bg */
+ --color-sidebar-background-80: var(--color-background-80); /* tertiary sidebar bg */
+
+ --color-sidebar-text-100: var(--color-text-100); /* primary sidebar text */
+ --color-sidebar-text-200: var(--color-text-200); /* secondary sidebar text */
+ --color-sidebar-text-300: var(--color-text-300); /* tertiary sidebar text */
+ --color-sidebar-text-400: var(--color-text-400); /* sidebar placeholder text */
+
+ --color-sidebar-border-100: var(--color-border-100); /* subtle sidebar border= 1 */
+ --color-sidebar-border-200: var(--color-border-100); /* subtle sidebar border- 2 */
+ --color-sidebar-border-300: var(--color-border-100); /* strong sidebar border- 1 */
+ --color-sidebar-border-400: var(--color-border-100); /* strong sidebar border- 2 */
+
+ --color-sidebar-shadow-2xs: var(--color-shadow-2xs);
+ --color-sidebar-shadow-xs: var(--color-shadow-xs);
+ --color-sidebar-shadow-sm: var(--color-shadow-sm);
+ --color-sidebar-shadow-rg: var(--color-shadow-rg);
+ --color-sidebar-shadow-md: var(--color-shadow-md);
+ --color-sidebar-shadow-lg: var(--color-shadow-lg);
+ --color-sidebar-shadow-xl: var(--color-shadow-xl);
+ --color-sidebar-shadow-2xl: var(--color-shadow-2xl);
+ --color-sidebar-shadow-3xl: var(--color-shadow-3xl);
+ }
+
+ [data-theme="light"],
+ [data-theme="light-contrast"] {
+ color-scheme: light !important;
+
+ --color-background-100: 255, 255, 255; /* primary bg */
+ --color-background-90: 250, 250, 250; /* secondary bg */
+ --color-background-80: 245, 245, 245; /* tertiary bg */
+ }
+
+ [data-theme="light"] {
+ --color-text-100: 23, 23, 23; /* primary text */
+ --color-text-200: 58, 58, 58; /* secondary text */
+ --color-text-300: 82, 82, 82; /* tertiary text */
+ --color-text-400: 163, 163, 163; /* placeholder text */
+
+ --color-border-100: 245, 245, 245; /* subtle border= 1 */
+ --color-border-200: 229, 229, 229; /* subtle border- 2 */
+ --color-border-300: 212, 212, 212; /* strong border- 1 */
+ --color-border-400: 185, 185, 185; /* strong border- 2 */
+ }
+
+ [data-theme="light-contrast"] {
+ --color-text-100: 11, 11, 11; /* primary text */
+ --color-text-200: 38, 38, 38; /* secondary text */
+ --color-text-300: 58, 58, 58; /* tertiary text */
+ --color-text-400: 115, 115, 115; /* placeholder text */
+
+ --color-border-100: 34, 34, 34; /* subtle border= 1 */
+ --color-border-200: 38, 38, 38; /* subtle border- 2 */
+ --color-border-300: 46, 46, 46; /* strong border- 1 */
+ --color-border-400: 58, 58, 58; /* strong border- 2 */
+ }
+
+ [data-theme="dark"],
+ [data-theme="dark-contrast"] {
+ color-scheme: dark !important;
+
+ --color-background-100: 7, 7, 7; /* primary bg */
+ --color-background-90: 11, 11, 11; /* secondary bg */
+ --color-background-80: 23, 23, 23; /* tertiary bg */
+
+ --color-shadow-2xs: 0px 0px 1px 0px rgba(0, 0, 0, 0.15), 0px 1px 3px 0px rgba(0, 0, 0, 0.5);
+ --color-shadow-xs: 0px 0px 2px 0px rgba(0, 0, 0, 0.2), 0px 2px 4px 0px rgba(0, 0, 0, 0.5);
+ --color-shadow-sm: 0px 0px 4px 0px rgba(0, 0, 0, 0.2), 0px 2px 6px 0px rgba(0, 0, 0, 0.5);
+ --color-shadow-rg: 0px 0px 6px 0px rgba(0, 0, 0, 0.2), 0px 4px 6px 0px rgba(0, 0, 0, 0.5);
+ --color-shadow-md: 0px 2px 8px 0px rgba(0, 0, 0, 0.2), 0px 4px 8px 0px rgba(0, 0, 0, 0.5);
+ --color-shadow-lg: 0px 4px 12px 0px rgba(0, 0, 0, 0.25), 0px 4px 10px 0px rgba(0, 0, 0, 0.55);
+ --color-shadow-xl: 0px 0px 14px 0px rgba(0, 0, 0, 0.25), 0px 6px 10px 0px rgba(0, 0, 0, 0.55);
+ --color-shadow-2xl: 0px 0px 18px 0px rgba(0, 0, 0, 0.25), 0px 8px 12px 0px rgba(0, 0, 0, 0.6);
+ --color-shadow-3xl: 0px 4px 24px 0px rgba(0, 0, 0, 0.3), 0px 12px 40px 0px rgba(0, 0, 0, 0.65);
+ }
+
+ [data-theme="dark"] {
+ --color-text-100: 229, 229, 229; /* primary text */
+ --color-text-200: 163, 163, 163; /* secondary text */
+ --color-text-300: 115, 115, 115; /* tertiary text */
+ --color-text-400: 82, 82, 82; /* placeholder text */
+
+ --color-border-100: 34, 34, 34; /* subtle border= 1 */
+ --color-border-200: 38, 38, 38; /* subtle border- 2 */
+ --color-border-300: 46, 46, 46; /* strong border- 1 */
+ --color-border-400: 58, 58, 58; /* strong border- 2 */
+ }
+
+ [data-theme="dark-contrast"] {
+ --color-text-100: 250, 250, 250; /* primary text */
+ --color-text-200: 241, 241, 241; /* secondary text */
+ --color-text-300: 212, 212, 212; /* tertiary text */
+ --color-text-400: 115, 115, 115; /* placeholder text */
+
+ --color-border-100: 245, 245, 245; /* subtle border= 1 */
+ --color-border-200: 229, 229, 229; /* subtle border- 2 */
+ --color-border-300: 212, 212, 212; /* strong border- 1 */
+ --color-border-400: 185, 185, 185; /* strong border- 2 */
+ }
+
+ [data-theme="light"],
+ [data-theme="dark"],
+ [data-theme="light-contrast"],
+ [data-theme="dark-contrast"] {
+ --color-primary-10: 236, 241, 255;
+ --color-primary-20: 217, 228, 255;
+ --color-primary-30: 197, 214, 255;
+ --color-primary-40: 178, 200, 255;
+ --color-primary-50: 159, 187, 255;
+ --color-primary-60: 140, 173, 255;
+ --color-primary-70: 121, 159, 255;
+ --color-primary-80: 101, 145, 255;
+ --color-primary-90: 82, 132, 255;
+ --color-primary-100: 63, 118, 255;
+ --color-primary-200: 57, 106, 230;
+ --color-primary-300: 50, 94, 204;
+ --color-primary-400: 44, 83, 179;
+ --color-primary-500: 38, 71, 153;
+ --color-primary-600: 32, 59, 128;
+ --color-primary-700: 25, 47, 102;
+ --color-primary-800: 19, 35, 76;
+ --color-primary-900: 13, 24, 51;
+
+ --color-sidebar-background-100: var(--color-background-100); /* primary sidebar bg */
+ --color-sidebar-background-90: var(--color-background-90); /* secondary sidebar bg */
+ --color-sidebar-background-80: var(--color-background-80); /* tertiary sidebar bg */
+
+ --color-sidebar-text-100: var(--color-text-100); /* primary sidebar text */
+ --color-sidebar-text-200: var(--color-text-200); /* secondary sidebar text */
+ --color-sidebar-text-300: var(--color-text-300); /* tertiary sidebar text */
+ --color-sidebar-text-400: var(--color-text-400); /* sidebar placeholder text */
+
+ --color-sidebar-border-100: var(--color-border-100); /* subtle sidebar border= 1 */
+ --color-sidebar-border-200: var(--color-border-100); /* subtle sidebar border- 2 */
+ --color-sidebar-border-300: var(--color-border-100); /* strong sidebar border- 1 */
+ --color-sidebar-border-400: var(--color-border-100); /* strong sidebar border- 2 */
+ }
+}
+
+::-webkit-scrollbar {
+ width: 5px;
+ height: 5px;
+ border-radius: 5px;
+}
+
+::-webkit-scrollbar-track {
+ background-color: rgba(var(--color-background-100));
+}
+
+::-webkit-scrollbar-thumb {
+ border-radius: 5px;
+ background-color: rgba(var(--color-background-80));
+}
+
+.hide-vertical-scrollbar::-webkit-scrollbar {
+ width: 0 !important;
+}
+
+.hide-horizontal-scrollbar::-webkit-scrollbar {
+ height: 0 !important;
+}
+
+.hide-both-scrollbars::-webkit-scrollbar {
+ height: 0 !important;
+ width: 0 !important;
+}
diff --git a/space/tailwind.config.js b/space/tailwind.config.js
new file mode 100644
index 000000000..1e1e59826
--- /dev/null
+++ b/space/tailwind.config.js
@@ -0,0 +1 @@
+module.exports = require("tailwind-config-custom/tailwind.config");
diff --git a/space/tsconfig.json b/space/tsconfig.json
new file mode 100644
index 000000000..3047edb7c
--- /dev/null
+++ b/space/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "tsconfig/nextjs.json",
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "additional.d.ts"],
+ "exclude": ["node_modules"],
+ "compilerOptions": {
+ "baseUrl": ".",
+ "jsx": "preserve"
+ }
+}
diff --git a/space/types/issue.ts b/space/types/issue.ts
new file mode 100644
index 000000000..206327fcd
--- /dev/null
+++ b/space/types/issue.ts
@@ -0,0 +1,168 @@
+export type TIssueBoardKeys = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt";
+
+export interface IIssueBoardViews {
+ key: TIssueBoardKeys;
+ title: string;
+ icon: string;
+ className: string;
+}
+
+export type TIssuePriorityKey = "urgent" | "high" | "medium" | "low" | "none";
+export type TIssuePriorityTitle = "Urgent" | "High" | "Medium" | "Low" | "None";
+export interface IIssuePriorityFilters {
+ key: TIssuePriorityKey;
+ title: TIssuePriorityTitle;
+ className: string;
+ icon: string;
+}
+
+export type TIssueGroupKey = "backlog" | "unstarted" | "started" | "completed" | "cancelled";
+export type TIssueGroupTitle = "Backlog" | "Unstarted" | "Started" | "Completed" | "Cancelled";
+
+export interface IIssueGroup {
+ key: TIssueGroupKey;
+ title: TIssueGroupTitle;
+ color: string;
+ className: string;
+ icon: React.FC;
+}
+
+export interface IIssue {
+ id: string;
+ comments: Comment[];
+ description_html: string;
+ label_details: any;
+ name: string;
+ priority: TIssuePriorityKey | null;
+ project: string;
+ project_detail: any;
+ reactions: IIssueReaction[];
+ sequence_id: number;
+ start_date: any;
+ state: string;
+ state_detail: any;
+ target_date: any;
+ votes: IVote[];
+}
+
+export interface IIssueState {
+ id: string;
+ name: string;
+ group: TIssueGroupKey;
+ color: string;
+}
+
+export interface IIssueLabel {
+ id: string;
+ name: string;
+ color: string;
+}
+
+export interface IVote {
+ issue: string;
+ vote: -1 | 1;
+ workspace: string;
+ project: string;
+ actor: string;
+ actor_detail: ActorDetail;
+}
+
+export interface Comment {
+ actor_detail: ActorDetail;
+ access: string;
+ actor: string;
+ attachments: any[];
+ comment_html: string;
+ comment_reactions: {
+ actor_detail: ActorDetail;
+ comment: string;
+ id: string;
+ reaction: string;
+ }[];
+ comment_stripped: string;
+ created_at: Date;
+ created_by: string;
+ id: string;
+ is_member: boolean;
+ issue: string;
+ issue_detail: IssueDetail;
+ project: string;
+ project_detail: ProjectDetail;
+ updated_at: Date;
+ updated_by: string;
+ workspace: string;
+ workspace_detail: WorkspaceDetail;
+}
+
+export interface IIssueReaction {
+ actor_detail: ActorDetail;
+ id: string;
+ issue: string;
+ reaction: string;
+}
+
+export interface ActorDetail {
+ avatar?: string;
+ display_name?: string;
+ first_name?: string;
+ id?: string;
+ is_bot?: boolean;
+ last_name?: string;
+}
+
+export interface IssueDetail {
+ id: string;
+ name: string;
+ description: Description;
+ description_html: string;
+ priority: string;
+ start_date: null;
+ target_date: null;
+ sequence_id: number;
+ sort_order: number;
+}
+
+export interface Description {
+ type: string;
+ content: DescriptionContent[];
+}
+
+export interface DescriptionContent {
+ type: string;
+ attrs?: Attrs;
+ content: ContentContent[];
+}
+
+export interface Attrs {
+ level: number;
+}
+
+export interface ContentContent {
+ text: string;
+ type: string;
+}
+
+export interface ProjectDetail {
+ id: string;
+ identifier: string;
+ name: string;
+ cover_image: string;
+ icon_prop: null;
+ emoji: string;
+ description: string;
+}
+
+export interface WorkspaceDetail {
+ name: string;
+ slug: string;
+ id: string;
+}
+
+export interface IssueDetailType {
+ [issueId: string]: {
+ issue: IIssue;
+ comments: Comment[];
+ reactions: any[];
+ votes: any[];
+ };
+}
diff --git a/apps/space/store/types/project.ts b/space/types/project.ts
similarity index 64%
rename from apps/space/store/types/project.ts
rename to space/types/project.ts
index 41f4c2f44..e0e1bba9e 100644
--- a/apps/space/store/types/project.ts
+++ b/space/types/project.ts
@@ -27,14 +27,3 @@ export interface IProjectSettings {
spreadsheet: boolean;
};
}
-
-export interface IProjectStore {
- loader: boolean;
- error: any | null;
-
- workspace: IWorkspace | null;
- project: IProject | null;
- workspaceProjectSettings: IProjectSettings | null;
-
- getProjectSettingsAsync: (workspace_slug: string, project_slug: string) => Promise;
-}
diff --git a/apps/space/store/types/theme.ts b/space/types/theme.ts
similarity index 100%
rename from apps/space/store/types/theme.ts
rename to space/types/theme.ts
diff --git a/space/types/user.ts b/space/types/user.ts
new file mode 100644
index 000000000..8c6d5f681
--- /dev/null
+++ b/space/types/user.ts
@@ -0,0 +1,23 @@
+export interface IUser {
+ avatar: string;
+ cover_image: string | null;
+ created_at: Date;
+ created_location: string;
+ date_joined: Date;
+ email: string;
+ display_name: string;
+ first_name: string;
+ id: string;
+ is_email_verified: boolean;
+ is_onboarded: boolean;
+ is_tour_completed: boolean;
+ last_location: string;
+ last_login: Date;
+ last_name: string;
+ mobile_number: string;
+ role: string;
+ token: string;
+ updated_at: Date;
+ username: string;
+ user_timezone: string;
+}
diff --git a/start.sh b/start.sh
index dcb97db6d..2685c3826 100644
--- a/start.sh
+++ b/start.sh
@@ -1,9 +1,5 @@
#!/bin/sh
set -x
-# Replace the statically built BUILT_NEXT_PUBLIC_API_BASE_URL with run-time NEXT_PUBLIC_API_BASE_URL
-# NOTE: if these values are the same, this will be skipped.
-/usr/local/bin/replace-env-vars.sh "$BUILT_NEXT_PUBLIC_API_BASE_URL" "$NEXT_PUBLIC_API_BASE_URL" $2
-
echo "Starting Plane Frontend.."
node $1
diff --git a/turbo.json b/turbo.json
index 69da3c552..59bbe741f 100644
--- a/turbo.json
+++ b/turbo.json
@@ -15,16 +15,20 @@
"NEXT_PUBLIC_UNSPLASH_ACCESS",
"NEXT_PUBLIC_UNSPLASH_ENABLED",
"NEXT_PUBLIC_TRACK_EVENTS",
- "TRACKER_ACCESS_KEY",
+ "NEXT_PUBLIC_PLAUSIBLE_DOMAIN",
"NEXT_PUBLIC_CRISP_ID",
"NEXT_PUBLIC_ENABLE_SESSION_RECORDER",
"NEXT_PUBLIC_SESSION_RECORDER_KEY",
"NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS",
- "NEXT_PUBLIC_SLACK_CLIENT_ID",
- "NEXT_PUBLIC_SLACK_CLIENT_SECRET",
- "NEXT_PUBLIC_SUPABASE_URL",
- "NEXT_PUBLIC_SUPABASE_ANON_KEY",
- "NEXT_PUBLIC_PLAUSIBLE_DOMAIN"
+ "NEXT_PUBLIC_DEPLOY_WITH_NGINX",
+ "NEXT_PUBLIC_POSTHOG_KEY",
+ "NEXT_PUBLIC_POSTHOG_HOST",
+ "SLACK_OAUTH_URL",
+ "SLACK_CLIENT_ID",
+ "SLACK_CLIENT_SECRET",
+ "JITSU_TRACKER_ACCESS_KEY",
+ "JITSU_TRACKER_HOST",
+ "UNSPLASH_ACCESS_KEY"
],
"pipeline": {
"build": {
diff --git a/web/.env.example b/web/.env.example
new file mode 100644
index 000000000..88a2064c5
--- /dev/null
+++ b/web/.env.example
@@ -0,0 +1,24 @@
+# Extra image domains that need to be added for Next Image
+NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS=
+# Google Client ID for Google OAuth
+NEXT_PUBLIC_GOOGLE_CLIENTID=""
+# GitHub App ID for GitHub OAuth
+NEXT_PUBLIC_GITHUB_ID=""
+# GitHub App Name for GitHub Integration
+NEXT_PUBLIC_GITHUB_APP_NAME=""
+# Sentry DSN for error monitoring
+NEXT_PUBLIC_SENTRY_DSN=""
+# Enable/Disable OAUTH - default 0 for selfhosted instance
+NEXT_PUBLIC_ENABLE_OAUTH=0
+# Enable/Disable Sentry
+NEXT_PUBLIC_ENABLE_SENTRY=0
+# Enable/Disable session recording
+NEXT_PUBLIC_ENABLE_SESSION_RECORDER=0
+# Enable/Disable event tracking
+NEXT_PUBLIC_TRACK_EVENTS=0
+# Slack Client ID for Slack Integration
+NEXT_PUBLIC_SLACK_CLIENT_ID=""
+# For Telemetry, set it to "app.plane.so"
+NEXT_PUBLIC_PLAUSIBLE_DOMAIN=""
+# Public boards deploy URL
+NEXT_PUBLIC_DEPLOY_URL="http://localhost:3000/spaces"
\ No newline at end of file
diff --git a/web/.eslintrc.js b/web/.eslintrc.js
new file mode 100644
index 000000000..c8df60750
--- /dev/null
+++ b/web/.eslintrc.js
@@ -0,0 +1,4 @@
+module.exports = {
+ root: true,
+ extends: ["custom"],
+};
diff --git a/apps/app/.prettierrc b/web/.prettierrc
similarity index 100%
rename from apps/app/.prettierrc
rename to web/.prettierrc
diff --git a/apps/app/Dockerfile.dev b/web/Dockerfile.dev
similarity index 100%
rename from apps/app/Dockerfile.dev
rename to web/Dockerfile.dev
diff --git a/web/Dockerfile.web b/web/Dockerfile.web
new file mode 100644
index 000000000..d9260e61d
--- /dev/null
+++ b/web/Dockerfile.web
@@ -0,0 +1,63 @@
+FROM node:18-alpine AS builder
+RUN apk add --no-cache libc6-compat
+# Set working directory
+WORKDIR /app
+
+RUN yarn global add turbo
+COPY . .
+
+RUN turbo prune --scope=web --docker
+
+# Add lockfile and package.json's of isolated subworkspace
+FROM node:18-alpine AS installer
+
+RUN apk add --no-cache libc6-compat
+WORKDIR /app
+ARG NEXT_PUBLIC_API_BASE_URL=""
+ARG NEXT_PUBLIC_DEPLOY_URL=""
+
+# First install the dependencies (as they change less often)
+COPY .gitignore .gitignore
+COPY --from=builder /app/out/json/ .
+COPY --from=builder /app/out/yarn.lock ./yarn.lock
+RUN yarn install --network-timeout 500000
+
+# Build the project
+COPY --from=builder /app/out/full/ .
+COPY turbo.json turbo.json
+USER root
+ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
+ENV NEXT_PUBLIC_DEPLOY_URL=$NEXT_PUBLIC_DEPLOY_URL
+
+RUN yarn turbo run build --filter=web
+
+FROM node:18-alpine AS runner
+WORKDIR /app
+
+# Don't run production as root
+RUN addgroup --system --gid 1001 plane
+RUN adduser --system --uid 1001 captain
+USER captain
+
+COPY --from=installer /app/web/next.config.js .
+COPY --from=installer /app/web/package.json .
+
+# Automatically leverage output traces to reduce image size
+# https://nextjs.org/docs/advanced-features/output-file-tracing
+COPY --from=installer --chown=captain:plane /app/web/.next/standalone ./
+COPY --from=installer --chown=captain:plane /app/web/.next ./web/.next
+
+ARG NEXT_PUBLIC_API_BASE_URL=""
+ARG NEXT_PUBLIC_DEPLOY_URL=""
+ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
+ENV NEXT_PUBLIC_DEPLOY_URL=$NEXT_PUBLIC_DEPLOY_URL
+
+USER root
+COPY start.sh /usr/local/bin/
+RUN chmod +x /usr/local/bin/start.sh
+
+USER captain
+
+ENV NEXT_TELEMETRY_DISABLED 1
+
+EXPOSE 3000
diff --git a/apps/app/components/account/email-code-form.tsx b/web/components/account/email-code-form.tsx
similarity index 100%
rename from apps/app/components/account/email-code-form.tsx
rename to web/components/account/email-code-form.tsx
diff --git a/apps/app/components/account/email-password-form.tsx b/web/components/account/email-password-form.tsx
similarity index 100%
rename from apps/app/components/account/email-password-form.tsx
rename to web/components/account/email-password-form.tsx
diff --git a/apps/app/components/account/email-reset-password-form.tsx b/web/components/account/email-reset-password-form.tsx
similarity index 100%
rename from apps/app/components/account/email-reset-password-form.tsx
rename to web/components/account/email-reset-password-form.tsx
diff --git a/apps/app/components/account/github-login-button.tsx b/web/components/account/github-login-button.tsx
similarity index 100%
rename from apps/app/components/account/github-login-button.tsx
rename to web/components/account/github-login-button.tsx
diff --git a/apps/app/components/account/google-login.tsx b/web/components/account/google-login.tsx
similarity index 100%
rename from apps/app/components/account/google-login.tsx
rename to web/components/account/google-login.tsx
diff --git a/apps/app/components/account/index.ts b/web/components/account/index.ts
similarity index 100%
rename from apps/app/components/account/index.ts
rename to web/components/account/index.ts
diff --git a/apps/app/components/analytics/custom-analytics/create-update-analytics-modal.tsx b/web/components/analytics/custom-analytics/create-update-analytics-modal.tsx
similarity index 100%
rename from apps/app/components/analytics/custom-analytics/create-update-analytics-modal.tsx
rename to web/components/analytics/custom-analytics/create-update-analytics-modal.tsx
diff --git a/apps/app/components/analytics/custom-analytics/custom-analytics.tsx b/web/components/analytics/custom-analytics/custom-analytics.tsx
similarity index 100%
rename from apps/app/components/analytics/custom-analytics/custom-analytics.tsx
rename to web/components/analytics/custom-analytics/custom-analytics.tsx
diff --git a/apps/app/components/analytics/custom-analytics/graph/custom-tooltip.tsx b/web/components/analytics/custom-analytics/graph/custom-tooltip.tsx
similarity index 100%
rename from apps/app/components/analytics/custom-analytics/graph/custom-tooltip.tsx
rename to web/components/analytics/custom-analytics/graph/custom-tooltip.tsx
diff --git a/apps/app/components/analytics/custom-analytics/graph/index.tsx b/web/components/analytics/custom-analytics/graph/index.tsx
similarity index 100%
rename from apps/app/components/analytics/custom-analytics/graph/index.tsx
rename to web/components/analytics/custom-analytics/graph/index.tsx
diff --git a/apps/app/components/analytics/custom-analytics/index.ts b/web/components/analytics/custom-analytics/index.ts
similarity index 100%
rename from apps/app/components/analytics/custom-analytics/index.ts
rename to web/components/analytics/custom-analytics/index.ts
diff --git a/apps/app/components/analytics/custom-analytics/select-bar.tsx b/web/components/analytics/custom-analytics/select-bar.tsx
similarity index 100%
rename from apps/app/components/analytics/custom-analytics/select-bar.tsx
rename to web/components/analytics/custom-analytics/select-bar.tsx
diff --git a/apps/app/components/analytics/custom-analytics/sidebar.tsx b/web/components/analytics/custom-analytics/sidebar.tsx
similarity index 100%
rename from apps/app/components/analytics/custom-analytics/sidebar.tsx
rename to web/components/analytics/custom-analytics/sidebar.tsx
diff --git a/apps/app/components/analytics/custom-analytics/table.tsx b/web/components/analytics/custom-analytics/table.tsx
similarity index 94%
rename from apps/app/components/analytics/custom-analytics/table.tsx
rename to web/components/analytics/custom-analytics/table.tsx
index 75d1d7d40..5993bb63c 100644
--- a/apps/app/components/analytics/custom-analytics/table.tsx
+++ b/web/components/analytics/custom-analytics/table.tsx
@@ -1,13 +1,13 @@
// nivo
import { BarDatum } from "@nivo/bar";
// icons
-import { getPriorityIcon } from "components/icons";
+import { PriorityIcon } from "components/icons";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
// helpers
import { generateBarColor, renderMonthAndYear } from "helpers/analytics.helper";
// types
-import { IAnalyticsParams, IAnalyticsResponse } from "types";
+import { IAnalyticsParams, IAnalyticsResponse, TIssuePriorities } from "types";
// constants
import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES, DATE_KEYS } from "constants/analytics";
@@ -53,7 +53,7 @@ export const AnalyticsTable: React.FC = ({ analytics, barGraphData, param
>
{params.segment === "priority" ? (
- getPriorityIcon(key)
+
) : (
= ({ analytics, barGraphData, param
}`}
>
{params.x_axis === "priority" ? (
- getPriorityIcon(`${item.name}`)
+
) : (
= ({ defaultAnalytics }) => (
{group.state_group}
@@ -42,7 +42,7 @@ export const AnalyticsDemand: React.FC = ({ defaultAnalytics }) => (
className="absolute top-0 left-0 h-1 rounded duration-300"
style={{
width: `${percentage}%`,
- backgroundColor: STATE_GROUP_COLORS[group.state_group],
+ backgroundColor: STATE_GROUP_COLORS[group.state_group as TStateGroups],
}}
/>
diff --git a/apps/app/components/analytics/scope-and-demand/index.ts b/web/components/analytics/scope-and-demand/index.ts
similarity index 100%
rename from apps/app/components/analytics/scope-and-demand/index.ts
rename to web/components/analytics/scope-and-demand/index.ts
diff --git a/apps/app/components/analytics/scope-and-demand/leaderboard.tsx b/web/components/analytics/scope-and-demand/leaderboard.tsx
similarity index 100%
rename from apps/app/components/analytics/scope-and-demand/leaderboard.tsx
rename to web/components/analytics/scope-and-demand/leaderboard.tsx
diff --git a/apps/app/components/analytics/scope-and-demand/scope-and-demand.tsx b/web/components/analytics/scope-and-demand/scope-and-demand.tsx
similarity index 100%
rename from apps/app/components/analytics/scope-and-demand/scope-and-demand.tsx
rename to web/components/analytics/scope-and-demand/scope-and-demand.tsx
diff --git a/apps/app/components/analytics/scope-and-demand/scope.tsx b/web/components/analytics/scope-and-demand/scope.tsx
similarity index 100%
rename from apps/app/components/analytics/scope-and-demand/scope.tsx
rename to web/components/analytics/scope-and-demand/scope.tsx
diff --git a/apps/app/components/analytics/scope-and-demand/year-wise-issues.tsx b/web/components/analytics/scope-and-demand/year-wise-issues.tsx
similarity index 100%
rename from apps/app/components/analytics/scope-and-demand/year-wise-issues.tsx
rename to web/components/analytics/scope-and-demand/year-wise-issues.tsx
diff --git a/apps/app/components/analytics/select/index.ts b/web/components/analytics/select/index.ts
similarity index 100%
rename from apps/app/components/analytics/select/index.ts
rename to web/components/analytics/select/index.ts
diff --git a/apps/app/components/analytics/select/project.tsx b/web/components/analytics/select/project.tsx
similarity index 100%
rename from apps/app/components/analytics/select/project.tsx
rename to web/components/analytics/select/project.tsx
diff --git a/apps/app/components/analytics/select/segment.tsx b/web/components/analytics/select/segment.tsx
similarity index 100%
rename from apps/app/components/analytics/select/segment.tsx
rename to web/components/analytics/select/segment.tsx
diff --git a/apps/app/components/analytics/select/x-axis.tsx b/web/components/analytics/select/x-axis.tsx
similarity index 100%
rename from apps/app/components/analytics/select/x-axis.tsx
rename to web/components/analytics/select/x-axis.tsx
diff --git a/apps/app/components/analytics/select/y-axis.tsx b/web/components/analytics/select/y-axis.tsx
similarity index 100%
rename from apps/app/components/analytics/select/y-axis.tsx
rename to web/components/analytics/select/y-axis.tsx
diff --git a/apps/app/components/auth-screens/index.ts b/web/components/auth-screens/index.ts
similarity index 100%
rename from apps/app/components/auth-screens/index.ts
rename to web/components/auth-screens/index.ts
diff --git a/apps/app/components/auth-screens/not-authorized-view.tsx b/web/components/auth-screens/not-authorized-view.tsx
similarity index 100%
rename from apps/app/components/auth-screens/not-authorized-view.tsx
rename to web/components/auth-screens/not-authorized-view.tsx
diff --git a/apps/app/components/auth-screens/project/index.ts b/web/components/auth-screens/project/index.ts
similarity index 100%
rename from apps/app/components/auth-screens/project/index.ts
rename to web/components/auth-screens/project/index.ts
diff --git a/apps/app/components/auth-screens/project/join-project.tsx b/web/components/auth-screens/project/join-project.tsx
similarity index 100%
rename from apps/app/components/auth-screens/project/join-project.tsx
rename to web/components/auth-screens/project/join-project.tsx
diff --git a/apps/app/components/auth-screens/workspace/index.ts b/web/components/auth-screens/workspace/index.ts
similarity index 100%
rename from apps/app/components/auth-screens/workspace/index.ts
rename to web/components/auth-screens/workspace/index.ts
diff --git a/apps/app/components/auth-screens/workspace/not-a-member.tsx b/web/components/auth-screens/workspace/not-a-member.tsx
similarity index 100%
rename from apps/app/components/auth-screens/workspace/not-a-member.tsx
rename to web/components/auth-screens/workspace/not-a-member.tsx
diff --git a/web/components/automation/auto-archive-automation.tsx b/web/components/automation/auto-archive-automation.tsx
new file mode 100644
index 000000000..bb4e72e0c
--- /dev/null
+++ b/web/components/automation/auto-archive-automation.tsx
@@ -0,0 +1,97 @@
+import React, { useState } from "react";
+
+// component
+import { CustomSelect, ToggleSwitch } from "components/ui";
+import { SelectMonthModal } from "components/automation";
+// icon
+import { ArchiveRestore } from "lucide-react";
+// constants
+import { PROJECT_AUTOMATION_MONTHS } from "constants/project";
+// types
+import { IProject } from "types";
+
+type Props = {
+ projectDetails: IProject | undefined;
+ handleChange: (formData: Partial) => Promise;
+};
+
+export const AutoArchiveAutomation: React.FC = ({ projectDetails, handleChange }) => {
+ const [monthModal, setmonthModal] = useState(false);
+
+ const initialValues: Partial = { archive_in: 1 };
+ return (
+ <>
+ setmonthModal(false)}
+ handleChange={handleChange}
+ />
+
+
+
+
+
+
Auto-archive closed issues
+
+ Plane will auto archive issues that have been completed or canceled.
+
+
+
+
+ projectDetails?.archive_in === 0
+ ? handleChange({ archive_in: 1 })
+ : handleChange({ archive_in: 0 })
+ }
+ size="sm"
+ />
+
+
+ {projectDetails?.archive_in !== 0 && (
+
+
+
+ Auto-archive issues that are closed for
+
+
+ {
+ handleChange({ archive_in: val });
+ }}
+ input
+ verticalPosition="bottom"
+ width="w-full"
+ >
+ <>
+ {PROJECT_AUTOMATION_MONTHS.map((month) => (
+
+ {month.label}
+
+ ))}
+
+ setmonthModal(true)}
+ >
+ Customise Time Range
+
+ >
+
+
+
+
+ )}
+
+ >
+ );
+};
diff --git a/web/components/automation/auto-close-automation.tsx b/web/components/automation/auto-close-automation.tsx
new file mode 100644
index 000000000..8235c8063
--- /dev/null
+++ b/web/components/automation/auto-close-automation.tsx
@@ -0,0 +1,190 @@
+import React, { useState } from "react";
+
+import useSWR from "swr";
+
+import { useRouter } from "next/router";
+
+// component
+import { CustomSearchSelect, CustomSelect, Icon, ToggleSwitch } from "components/ui";
+import { SelectMonthModal } from "components/automation";
+// icons
+import { Squares2X2Icon } from "@heroicons/react/24/outline";
+import { StateGroupIcon } from "components/icons";
+import { ArchiveX } from "lucide-react";
+// services
+import stateService from "services/state.service";
+// constants
+import { PROJECT_AUTOMATION_MONTHS } from "constants/project";
+import { STATES_LIST } from "constants/fetch-keys";
+// types
+import { IProject } from "types";
+// helper
+import { getStatesList } from "helpers/state.helper";
+
+type Props = {
+ projectDetails: IProject | undefined;
+ handleChange: (formData: Partial) => Promise;
+};
+
+export const AutoCloseAutomation: React.FC = ({ projectDetails, handleChange }) => {
+ const [monthModal, setmonthModal] = useState(false);
+
+ const router = useRouter();
+ const { workspaceSlug, projectId } = router.query;
+
+ const { data: stateGroups } = useSWR(
+ workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
+ workspaceSlug && projectId
+ ? () => stateService.getStates(workspaceSlug as string, projectId as string)
+ : null
+ );
+ const states = getStatesList(stateGroups);
+
+ const options = states
+ ?.filter((state) => state.group === "cancelled")
+ .map((state) => ({
+ value: state.id,
+ query: state.name,
+ content: (
+
+
+ {state.name}
+
+ ),
+ }));
+
+ const multipleOptions = (options ?? []).length > 1;
+
+ const defaultState = stateGroups && stateGroups.cancelled ? stateGroups.cancelled[0].id : null;
+
+ const selectedOption = states?.find(
+ (s) => s.id === projectDetails?.default_state ?? defaultState
+ );
+ const currentDefaultState = states?.find((s) => s.id === defaultState);
+
+ const initialValues: Partial = {
+ close_in: 1,
+ default_state: defaultState,
+ };
+
+ return (
+ <>
+ setmonthModal(false)}
+ handleChange={handleChange}
+ />
+
+
+
+
+
+
+
Auto-close issues
+
+ Plane will automatically close issue that haven’t been completed or canceled.
+
+
+
+
+ projectDetails?.close_in === 0
+ ? handleChange({ close_in: 1, default_state: defaultState })
+ : handleChange({ close_in: 0, default_state: null })
+ }
+ size="sm"
+ />
+
+
+ {projectDetails?.close_in !== 0 && (
+
+
+
+
+ Auto-close issues that are inactive for
+
+
+ {
+ handleChange({ close_in: val });
+ }}
+ input
+ width="w-full"
+ >
+ <>
+ {PROJECT_AUTOMATION_MONTHS.map((month) => (
+
+ {month.label}
+
+ ))}
+ setmonthModal(true)}
+ >
+ Customise Time Range
+
+ >
+
+
+
+
+
+
Auto-close Status
+
+
+ {selectedOption ? (
+
+ ) : currentDefaultState ? (
+
+ ) : (
+
+ )}
+ {selectedOption?.name
+ ? selectedOption.name
+ : currentDefaultState?.name ?? (
+ State
+ )}
+
+ }
+ onChange={(val: string) => {
+ handleChange({ default_state: val });
+ }}
+ options={options}
+ disabled={!multipleOptions}
+ width="w-full"
+ input
+ />
+
+
+
+
+ )}
+
+ >
+ );
+};
diff --git a/apps/app/components/automation/index.ts b/web/components/automation/index.ts
similarity index 100%
rename from apps/app/components/automation/index.ts
rename to web/components/automation/index.ts
diff --git a/apps/app/components/automation/select-month-modal.tsx b/web/components/automation/select-month-modal.tsx
similarity index 99%
rename from apps/app/components/automation/select-month-modal.tsx
rename to web/components/automation/select-month-modal.tsx
index b91c03391..18239d62b 100644
--- a/apps/app/components/automation/select-month-modal.tsx
+++ b/web/components/automation/select-month-modal.tsx
@@ -104,7 +104,7 @@ export const SelectMonthModal: React.FC
= ({
as="h3"
className="text-lg font-medium leading-6 text-custom-text-100"
>
- Customize Time Range
+ Customise Time Range
diff --git a/apps/app/components/breadcrumbs/index.tsx b/web/components/breadcrumbs/index.tsx
similarity index 100%
rename from apps/app/components/breadcrumbs/index.tsx
rename to web/components/breadcrumbs/index.tsx
diff --git a/apps/app/components/command-palette/change-interface-theme.tsx b/web/components/command-palette/change-interface-theme.tsx
similarity index 100%
rename from apps/app/components/command-palette/change-interface-theme.tsx
rename to web/components/command-palette/change-interface-theme.tsx
diff --git a/apps/app/components/command-palette/command-k.tsx b/web/components/command-palette/command-k.tsx
similarity index 99%
rename from apps/app/components/command-palette/command-k.tsx
rename to web/components/command-palette/command-k.tsx
index a1525a348..d20a44290 100644
--- a/apps/app/components/command-palette/command-k.tsx
+++ b/web/components/command-palette/command-k.tsx
@@ -665,7 +665,7 @@ export const CommandK: React.FC
= ({ deleteIssue, isPaletteOpen, setIsPal
className="focus:outline-none"
>
-
+
Join our Discord
diff --git a/apps/app/components/command-palette/command-pallette.tsx b/web/components/command-palette/command-pallette.tsx
similarity index 80%
rename from apps/app/components/command-palette/command-pallette.tsx
rename to web/components/command-palette/command-pallette.tsx
index 4dc29afec..507d8a49c 100644
--- a/apps/app/components/command-palette/command-pallette.tsx
+++ b/web/components/command-palette/command-pallette.tsx
@@ -51,7 +51,7 @@ export const CommandPalette: React.FC = observer(() => {
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null,
workspaceSlug && projectId && issueId
? () =>
- issuesService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
+ issuesService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
: null
);
@@ -89,37 +89,37 @@ export const CommandPalette: React.FC = observer(() => {
)
return;
- if (cmdClicked) {
- if (keyPressed === "k") {
- e.preventDefault();
- setIsPaletteOpen(true);
- } else if (keyPressed === "c" && altKey) {
- e.preventDefault();
- copyIssueUrlToClipboard();
- } else if (keyPressed === "b") {
- e.preventDefault();
- store.theme.setSidebarCollapsed(!store?.theme?.sidebarCollapsed);
- }
- } else {
- if (keyPressed === "c") {
- setIsIssueModalOpen(true);
- } else if (keyPressed === "p") {
- setIsProjectModalOpen(true);
- } else if (keyPressed === "v") {
- setIsCreateViewModalOpen(true);
- } else if (keyPressed === "d") {
- setIsCreateUpdatePageModalOpen(true);
- } else if (keyPressed === "h") {
- setIsShortcutsModalOpen(true);
- } else if (keyPressed === "q") {
- setIsCreateCycleModalOpen(true);
- } else if (keyPressed === "m") {
- setIsCreateModuleModalOpen(true);
- } else if (keyPressed === "backspace" || keyPressed === "delete") {
- e.preventDefault();
- setIsBulkDeleteIssuesModalOpen(true);
- }
+ if (cmdClicked) {
+ if (keyPressed === "k") {
+ e.preventDefault();
+ setIsPaletteOpen(true);
+ } else if (keyPressed === "c" && altKey) {
+ e.preventDefault();
+ copyIssueUrlToClipboard();
+ } else if (keyPressed === "b") {
+ e.preventDefault();
+ store.theme.setSidebarCollapsed(!store?.theme?.sidebarCollapsed);
}
+ } else {
+ if (keyPressed === "c") {
+ setIsIssueModalOpen(true);
+ } else if (keyPressed === "p") {
+ setIsProjectModalOpen(true);
+ } else if (keyPressed === "v") {
+ setIsCreateViewModalOpen(true);
+ } else if (keyPressed === "d") {
+ setIsCreateUpdatePageModalOpen(true);
+ } else if (keyPressed === "h") {
+ setIsShortcutsModalOpen(true);
+ } else if (keyPressed === "q") {
+ setIsCreateCycleModalOpen(true);
+ } else if (keyPressed === "m") {
+ setIsCreateModuleModalOpen(true);
+ } else if (keyPressed === "backspace" || keyPressed === "delete") {
+ e.preventDefault();
+ setIsBulkDeleteIssuesModalOpen(true);
+ }
+ }
},
[copyIssueUrlToClipboard]
);
@@ -196,4 +196,4 @@ export const CommandPalette: React.FC = observer(() => {
/>
>
);
-})
\ No newline at end of file
+});
diff --git a/apps/app/components/command-palette/helpers.tsx b/web/components/command-palette/helpers.tsx
similarity index 100%
rename from apps/app/components/command-palette/helpers.tsx
rename to web/components/command-palette/helpers.tsx
diff --git a/apps/app/components/command-palette/index.ts b/web/components/command-palette/index.ts
similarity index 100%
rename from apps/app/components/command-palette/index.ts
rename to web/components/command-palette/index.ts
diff --git a/apps/app/components/command-palette/issue/change-issue-assignee.tsx b/web/components/command-palette/issue/change-issue-assignee.tsx
similarity index 100%
rename from apps/app/components/command-palette/issue/change-issue-assignee.tsx
rename to web/components/command-palette/issue/change-issue-assignee.tsx
diff --git a/apps/app/components/command-palette/issue/change-issue-priority.tsx b/web/components/command-palette/issue/change-issue-priority.tsx
similarity index 89%
rename from apps/app/components/command-palette/issue/change-issue-priority.tsx
rename to web/components/command-palette/issue/change-issue-priority.tsx
index 07ba210a6..e1c0d52af 100644
--- a/apps/app/components/command-palette/issue/change-issue-priority.tsx
+++ b/web/components/command-palette/issue/change-issue-priority.tsx
@@ -1,5 +1,7 @@
-import { useRouter } from "next/router";
import React, { Dispatch, SetStateAction, useCallback } from "react";
+
+import { useRouter } from "next/router";
+
import { mutate } from "swr";
// cmdk
@@ -7,12 +9,12 @@ import { Command } from "cmdk";
// services
import issuesService from "services/issues.service";
// types
-import { ICurrentUserResponse, IIssue } from "types";
+import { ICurrentUserResponse, IIssue, TIssuePriorities } from "types";
// constants
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
import { PRIORITIES } from "constants/project";
// icons
-import { CheckIcon, getPriorityIcon } from "components/icons";
+import { CheckIcon, PriorityIcon } from "components/icons";
type Props = {
setIsPaletteOpen: Dispatch>;
@@ -54,7 +56,7 @@ export const ChangeIssuePriority: React.FC = ({ setIsPaletteOpen, issue,
[workspaceSlug, issueId, projectId, user]
);
- const handleIssueState = (priority: string | null) => {
+ const handleIssueState = (priority: TIssuePriorities) => {
submitChanges({ priority });
setIsPaletteOpen(false);
};
@@ -68,7 +70,7 @@ export const ChangeIssuePriority: React.FC = ({ setIsPaletteOpen, issue,
className="focus:outline-none"
>
- {getPriorityIcon(priority)}
+
{priority ?? "None"}
{priority === issue.priority && }
diff --git a/apps/app/components/command-palette/issue/change-issue-state.tsx b/web/components/command-palette/issue/change-issue-state.tsx
similarity index 91%
rename from apps/app/components/command-palette/issue/change-issue-state.tsx
rename to web/components/command-palette/issue/change-issue-state.tsx
index 30e2cdb77..6bfc2874c 100644
--- a/apps/app/components/command-palette/issue/change-issue-state.tsx
+++ b/web/components/command-palette/issue/change-issue-state.tsx
@@ -1,22 +1,24 @@
-import { useRouter } from "next/router";
import React, { Dispatch, SetStateAction, useCallback } from "react";
+
+import { useRouter } from "next/router";
+
import useSWR, { mutate } from "swr";
// cmdk
import { Command } from "cmdk";
-// ui
-import { Spinner } from "components/ui";
-// helpers
-import { getStatesList } from "helpers/state.helper";
// services
import issuesService from "services/issues.service";
import stateService from "services/state.service";
+// ui
+import { Spinner } from "components/ui";
+// icons
+import { CheckIcon, StateGroupIcon } from "components/icons";
+// helpers
+import { getStatesList } from "helpers/state.helper";
// types
import { ICurrentUserResponse, IIssue } from "types";
// fetch keys
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, STATES_LIST } from "constants/fetch-keys";
-// icons
-import { CheckIcon, getStateGroupIcon } from "components/icons";
type Props = {
setIsPaletteOpen: Dispatch>;
@@ -82,7 +84,12 @@ export const ChangeIssueState: React.FC = ({ setIsPaletteOpen, issue, use
className="focus:outline-none"
>
- {getStateGroupIcon(state.group, "16", "16", state.color)}
+
{state.name}
{state.id === issue.state && }
diff --git a/apps/app/components/command-palette/issue/index.ts b/web/components/command-palette/issue/index.ts
similarity index 100%
rename from apps/app/components/command-palette/issue/index.ts
rename to web/components/command-palette/issue/index.ts
diff --git a/apps/app/components/command-palette/shortcuts-modal.tsx b/web/components/command-palette/shortcuts-modal.tsx
similarity index 99%
rename from apps/app/components/command-palette/shortcuts-modal.tsx
rename to web/components/command-palette/shortcuts-modal.tsx
index 86b4aeb46..82f9b398c 100644
--- a/apps/app/components/command-palette/shortcuts-modal.tsx
+++ b/web/components/command-palette/shortcuts-modal.tsx
@@ -22,8 +22,6 @@ const shortcuts = [
{ keys: "↓", description: "Move down" },
{ keys: "←", description: "Move left" },
{ keys: "→", description: "Move right" },
- { keys: "Enter", description: "Select" },
- { keys: "Esc", description: "Close" },
],
},
{
diff --git a/apps/app/components/core/activity.tsx b/web/components/core/activity.tsx
similarity index 96%
rename from apps/app/components/core/activity.tsx
rename to web/components/core/activity.tsx
index 1b9de8d25..c8f6377b7 100644
--- a/apps/app/components/core/activity.tsx
+++ b/web/components/core/activity.tsx
@@ -353,6 +353,28 @@ const activityDetails: {
.
>
);
+ else if (activity.verb === "updated")
+ return (
+ <>
+ updated the{" "}
+
+ link
+
+
+ {showIssue && (
+ <>
+ {" "}
+ from
+ >
+ )}
+ .
+ >
+ );
else
return (
<>
diff --git a/apps/app/components/core/filters/date-filter-modal.tsx b/web/components/core/filters/date-filter-modal.tsx
similarity index 89%
rename from apps/app/components/core/filters/date-filter-modal.tsx
rename to web/components/core/filters/date-filter-modal.tsx
index abc2cc7c4..eb285fd6f 100644
--- a/apps/app/components/core/filters/date-filter-modal.tsx
+++ b/web/components/core/filters/date-filter-modal.tsx
@@ -1,15 +1,11 @@
import { Fragment } from "react";
-import { useRouter } from "next/router";
-
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// react-datepicker
import DatePicker from "react-datepicker";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
-// hooks
-import useIssuesView from "hooks/use-issues-view";
// components
import { DateFilterSelect } from "./date-filter-select";
// ui
@@ -23,8 +19,10 @@ import { IIssueFilterOptions } from "types";
type Props = {
title: string;
field: keyof IIssueFilterOptions;
- isOpen: boolean;
+ filters: IIssueFilterOptions;
handleClose: () => void;
+ isOpen: boolean;
+ onSelect: (option: any) => void;
};
type TFormValues = {
@@ -39,12 +37,14 @@ const defaultValues: TFormValues = {
date2: new Date(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()),
};
-export const DateFilterModal: React.FC = ({ title, field, isOpen, handleClose }) => {
- const { filters, setFilters } = useIssuesView();
-
- const router = useRouter();
- const { viewId } = router.query;
-
+export const DateFilterModal: React.FC = ({
+ title,
+ field,
+ filters,
+ handleClose,
+ isOpen,
+ onSelect,
+}) => {
const { handleSubmit, watch, control } = useForm({
defaultValues,
});
@@ -53,10 +53,10 @@ export const DateFilterModal: React.FC = ({ title, field, isOpen, handleC
const { filterType, date1, date2 } = formData;
if (filterType === "range") {
- setFilters(
- { [field]: [`${renderDateFormat(date1)};after`, `${renderDateFormat(date2)};before`] },
- !Boolean(viewId)
- );
+ onSelect({
+ key: field,
+ value: [`${renderDateFormat(date1)};after`, `${renderDateFormat(date2)};before`],
+ });
} else {
const filteredArray = (filters?.[field] as string[])?.filter((item) => {
if (item?.includes(filterType)) return false;
@@ -66,17 +66,12 @@ export const DateFilterModal: React.FC = ({ title, field, isOpen, handleC
const filterOne = filteredArray && filteredArray?.length > 0 ? filteredArray[0] : null;
if (filterOne)
- setFilters(
- { [field]: [filterOne, `${renderDateFormat(date1)};${filterType}`] },
- !Boolean(viewId)
- );
+ onSelect({ key: field, value: [filterOne, `${renderDateFormat(date1)};${filterType}`] });
else
- setFilters(
- {
- [field]: [`${renderDateFormat(date1)};${filterType}`],
- },
- !Boolean(viewId)
- );
+ onSelect({
+ key: field,
+ value: [`${renderDateFormat(date1)};${filterType}`],
+ });
}
handleClose();
};
diff --git a/apps/app/components/core/filters/date-filter-select.tsx b/web/components/core/filters/date-filter-select.tsx
similarity index 100%
rename from apps/app/components/core/filters/date-filter-select.tsx
rename to web/components/core/filters/date-filter-select.tsx
diff --git a/apps/app/components/core/filters/filters-list.tsx b/web/components/core/filters/filters-list.tsx
similarity index 96%
rename from apps/app/components/core/filters/filters-list.tsx
rename to web/components/core/filters/filters-list.tsx
index 8192bdf7d..81a12bd86 100644
--- a/apps/app/components/core/filters/filters-list.tsx
+++ b/web/components/core/filters/filters-list.tsx
@@ -2,7 +2,7 @@ import React from "react";
// icons
import { XMarkIcon } from "@heroicons/react/24/outline";
-import { getPriorityIcon, getStateGroupIcon } from "components/icons";
+import { PriorityIcon, StateGroupIcon } from "components/icons";
// ui
import { Avatar } from "components/ui";
// helpers
@@ -71,12 +71,10 @@ export const FiltersList: React.FC = ({
}}
>
- {getStateGroupIcon(
- state?.group ?? "backlog",
- "12",
- "12",
- state?.color
- )}
+
{state?.name ?? ""}
= ({
backgroundColor: `${STATE_GROUP_COLORS[group]}20`,
}}
>
- {getStateGroupIcon(group, "16", "16")}
+
+
+
{group}
= ({
: "bg-custom-background-90 text-custom-text-200"
}`}
>
- {getPriorityIcon(priority)}
+
+
+
{priority === "null" ? "None" : priority}
{
const isArchivedIssues = router.pathname.includes("archived-issues");
const {
- issueView,
- setIssueView,
- groupByProperty,
- setGroupByProperty,
- orderBy,
- setOrderBy,
- showEmptyGroups,
- showSubIssues,
- setShowSubIssues,
- setShowEmptyGroups,
+ displayFilters,
+ setDisplayFilters,
filters,
setFilters,
resetFilterToDefault,
@@ -96,11 +88,11 @@ export const IssuesFilterView: React.FC = () => {
setIssueView(option.type)}
+ onClick={() => setDisplayFilters({ layout: option.type })}
>
{
: "text-custom-sidebar-text-200"
}`}
>
- View
+ Display
@@ -174,28 +166,30 @@ export const IssuesFilterView: React.FC = () => {
- {issueView !== "calendar" &&
- issueView !== "spreadsheet" &&
- issueView !== "gantt_chart" && (
+ {displayFilters.layout !== "calendar" &&
+ displayFilters.layout !== "spreadsheet" &&
+ displayFilters.layout !== "gantt_chart" && (
Group by
option.key === groupByProperty)
- ?.name ?? "Select"
+ GROUP_BY_OPTIONS.find(
+ (option) => option.key === displayFilters.group_by
+ )?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
>
{GROUP_BY_OPTIONS.map((option) => {
- if (issueView === "kanban" && option.key === null) return null;
+ if (displayFilters.layout === "kanban" && option.key === null)
+ return null;
if (option.key === "project") return null;
return (
setGroupByProperty(option.key)}
+ onClick={() => setDisplayFilters({ group_by: option.key })}
>
{option.name}
@@ -205,41 +199,45 @@ export const IssuesFilterView: React.FC = () => {
)}
- {issueView !== "calendar" && issueView !== "spreadsheet" && (
-
-
Order by
-
-
option.key === orderBy)?.name ??
- "Select"
- }
- className="!w-full"
- buttonClassName="w-full"
- >
- {ORDER_BY_OPTIONS.map((option) =>
- groupByProperty === "priority" && option.key === "priority" ? null : (
- {
- setOrderBy(option.key);
- }}
- >
- {option.name}
-
- )
- )}
-
+ {displayFilters.layout !== "calendar" &&
+ displayFilters.layout !== "spreadsheet" && (
+
+
Order by
+
+ option.key === displayFilters.order_by
+ )?.name ?? "Select"
+ }
+ className="!w-full"
+ buttonClassName="w-full"
+ >
+ {ORDER_BY_OPTIONS.map((option) =>
+ displayFilters.group_by === "priority" &&
+ option.key === "priority" ? null : (
+ {
+ setDisplayFilters({ order_by: option.key });
+ }}
+ >
+ {option.name}
+
+ )
+ )}
+
+
-
- )}
+ )}
Issue type
option.key === filters.type)
- ?.name ?? "Select"
+ FILTER_ISSUE_OPTIONS.find(
+ (option) => option.key === displayFilters.type
+ )?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
@@ -248,7 +246,7 @@ export const IssuesFilterView: React.FC = () => {
- setFilters({
+ setDisplayFilters({
type: option.key,
})
}
@@ -260,33 +258,40 @@ export const IssuesFilterView: React.FC = () => {
- {issueView !== "calendar" && issueView !== "spreadsheet" && (
-
-
Show sub-issues
-
- setShowSubIssues(!showSubIssues)}
- />
-
-
- )}
- {issueView !== "calendar" &&
- issueView !== "spreadsheet" &&
- issueView !== "gantt_chart" && (
+ {displayFilters.layout !== "calendar" &&
+ displayFilters.layout !== "spreadsheet" && (
-
Show empty states
+
Show sub-issues
setShowEmptyGroups(!showEmptyGroups)}
+ value={displayFilters.sub_issue ?? true}
+ onChange={() =>
+ setDisplayFilters({ sub_issue: !displayFilters.sub_issue })
+ }
/>
)}
- {issueView !== "calendar" &&
- issueView !== "spreadsheet" &&
- issueView !== "gantt_chart" && (
+ {displayFilters.layout !== "calendar" &&
+ displayFilters.layout !== "spreadsheet" &&
+ displayFilters.layout !== "gantt_chart" && (
+
+
Show empty states
+
+
+ setDisplayFilters({
+ show_empty_groups: !displayFilters.show_empty_groups,
+ })
+ }
+ />
+
+
+ )}
+ {displayFilters.layout !== "calendar" &&
+ displayFilters.layout !== "spreadsheet" &&
+ displayFilters.layout !== "gantt_chart" && (
resetFilterToDefault()}>
Reset to default
@@ -302,7 +307,7 @@ export const IssuesFilterView: React.FC = () => {
)}
- {issueView !== "gantt_chart" && (
+ {displayFilters.layout !== "gantt_chart" && (
Display Properties
@@ -310,7 +315,7 @@ export const IssuesFilterView: React.FC = () => {
if (key === "estimate" && !isEstimateActive) return null;
if (
- issueView === "spreadsheet" &&
+ displayFilters.layout === "spreadsheet" &&
(key === "attachment_count" ||
key === "link" ||
key === "sub_issue_count")
@@ -318,7 +323,7 @@ export const IssuesFilterView: React.FC = () => {
return null;
if (
- issueView !== "spreadsheet" &&
+ displayFilters.layout !== "spreadsheet" &&
(key === "created_on" || key === "updated_on")
)
return null;
diff --git a/apps/app/components/core/image-picker-popover.tsx b/web/components/core/image-picker-popover.tsx
similarity index 93%
rename from apps/app/components/core/image-picker-popover.tsx
rename to web/components/core/image-picker-popover.tsx
index 402cba022..957f1131c 100644
--- a/apps/app/components/core/image-picker-popover.tsx
+++ b/web/components/core/image-picker-popover.tsx
@@ -20,6 +20,7 @@ import fileService from "services/file.service";
import { Input, Spinner, PrimaryButton, SecondaryButton } from "components/ui";
// hooks
import useWorkspaceDetails from "hooks/use-workspace-details";
+import useOutsideClickDetector from "hooks/use-outside-click-detector";
const unsplashEnabled =
process.env.NEXT_PUBLIC_UNSPLASH_ENABLED === "true" ||
@@ -40,9 +41,15 @@ type Props = {
label: string | React.ReactNode;
value: string | null;
onChange: (data: string) => void;
+ disabled?: boolean;
};
-export const ImagePickerPopover: React.FC
= ({ label, value, onChange }) => {
+export const ImagePickerPopover: React.FC = ({
+ label,
+ value,
+ onChange,
+ disabled = false,
+}) => {
const ref = useRef(null);
const router = useRouter();
@@ -61,6 +68,8 @@ export const ImagePickerPopover: React.FC = ({ label, value, onChange })
fileService.getUnsplashImages(1, searchParams)
);
+ const imagePickerRef = useRef(null);
+
const { workspaceDetails } = useWorkspaceDetails();
const onDrop = useCallback((acceptedFiles: File[]) => {
@@ -110,13 +119,16 @@ export const ImagePickerPopover: React.FC = ({ label, value, onChange })
onChange(images[0].urls.regular);
}, [value, onChange, images]);
+ useOutsideClickDetector(imagePickerRef, () => setIsOpen(false));
+
if (!unsplashEnabled) return null;
return (
setIsOpen((prev) => !prev)}
+ disabled={disabled}
>
{label}
@@ -130,7 +142,10 @@ export const ImagePickerPopover: React.FC = ({ label, value, onChange })
leaveTo="transform opacity-0 scale-95"
>
-
+
diff --git a/apps/app/components/core/index.ts b/web/components/core/index.ts
similarity index 100%
rename from apps/app/components/core/index.ts
rename to web/components/core/index.ts
diff --git a/apps/app/components/core/modals/bulk-delete-issues-modal.tsx b/web/components/core/modals/bulk-delete-issues-modal.tsx
similarity index 97%
rename from apps/app/components/core/modals/bulk-delete-issues-modal.tsx
rename to web/components/core/modals/bulk-delete-issues-modal.tsx
index 59e7c8a84..375e155a4 100644
--- a/apps/app/components/core/modals/bulk-delete-issues-modal.tsx
+++ b/web/components/core/modals/bulk-delete-issues-modal.tsx
@@ -58,7 +58,7 @@ export const BulkDeleteIssuesModal: React.FC = ({ isOpen, setIsOpen, user
);
const { setToastAlert } = useToast();
- const { issueView, params } = useIssuesView();
+ const { displayFilters, params } = useIssuesView();
const { params: calendarParams } = useCalendarIssuesView();
const { order_by, group_by, ...viewGanttParams } = params;
@@ -126,8 +126,8 @@ export const BulkDeleteIssuesModal: React.FC = ({ isOpen, setIsOpen, user
message: "Issues deleted successfully!",
});
- if (issueView === "calendar") mutate(calendarFetchKey);
- else if (issueView === "gantt_chart") mutate(ganttFetchKey);
+ if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
+ else if (displayFilters.layout === "gantt_chart") mutate(ganttFetchKey);
else {
if (cycleId) {
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params));
diff --git a/apps/app/components/core/modals/existing-issues-list-modal.tsx b/web/components/core/modals/existing-issues-list-modal.tsx
similarity index 99%
rename from apps/app/components/core/modals/existing-issues-list-modal.tsx
rename to web/components/core/modals/existing-issues-list-modal.tsx
index 5a9f68d8b..ed3902a4a 100644
--- a/apps/app/components/core/modals/existing-issues-list-modal.tsx
+++ b/web/components/core/modals/existing-issues-list-modal.tsx
@@ -212,7 +212,7 @@ export const ExistingIssuesListModal: React.FC = ({
onClick={() => setIsWorkspaceLevel((prevData) => !prevData)}
className="flex-shrink-0"
>
- workspace level
+ Workspace Level
diff --git a/apps/app/components/core/modals/gpt-assistant-modal.tsx b/web/components/core/modals/gpt-assistant-modal.tsx
similarity index 100%
rename from apps/app/components/core/modals/gpt-assistant-modal.tsx
rename to web/components/core/modals/gpt-assistant-modal.tsx
diff --git a/apps/app/components/core/modals/image-upload-modal.tsx b/web/components/core/modals/image-upload-modal.tsx
similarity index 100%
rename from apps/app/components/core/modals/image-upload-modal.tsx
rename to web/components/core/modals/image-upload-modal.tsx
diff --git a/apps/app/components/core/modals/index.ts b/web/components/core/modals/index.ts
similarity index 100%
rename from apps/app/components/core/modals/index.ts
rename to web/components/core/modals/index.ts
diff --git a/apps/app/components/core/modals/link-modal.tsx b/web/components/core/modals/link-modal.tsx
similarity index 100%
rename from apps/app/components/core/modals/link-modal.tsx
rename to web/components/core/modals/link-modal.tsx
diff --git a/apps/app/components/core/reaction-selector.tsx b/web/components/core/reaction-selector.tsx
similarity index 97%
rename from apps/app/components/core/reaction-selector.tsx
rename to web/components/core/reaction-selector.tsx
index 06b410785..43d77de9e 100644
--- a/apps/app/components/core/reaction-selector.tsx
+++ b/web/components/core/reaction-selector.tsx
@@ -61,7 +61,7 @@ export const ReactionSelector: React.FC = (props) => {
position === "top" ? "-top-12" : "-bottom-12"
}`}
>
-
+
{reactionEmojis.map((emoji) => (
= ({
{group}
diff --git a/apps/app/components/core/sidebar/single-progress-stats.tsx b/web/components/core/sidebar/single-progress-stats.tsx
similarity index 100%
rename from apps/app/components/core/sidebar/single-progress-stats.tsx
rename to web/components/core/sidebar/single-progress-stats.tsx
diff --git a/apps/app/components/core/theme/color-picker-input.tsx b/web/components/core/theme/color-picker-input.tsx
similarity index 100%
rename from apps/app/components/core/theme/color-picker-input.tsx
rename to web/components/core/theme/color-picker-input.tsx
diff --git a/apps/app/components/core/theme/custom-theme-selector.tsx b/web/components/core/theme/custom-theme-selector.tsx
similarity index 100%
rename from apps/app/components/core/theme/custom-theme-selector.tsx
rename to web/components/core/theme/custom-theme-selector.tsx
diff --git a/apps/app/components/core/theme/index.ts b/web/components/core/theme/index.ts
similarity index 100%
rename from apps/app/components/core/theme/index.ts
rename to web/components/core/theme/index.ts
diff --git a/apps/app/components/core/theme/theme-switch.tsx b/web/components/core/theme/theme-switch.tsx
similarity index 100%
rename from apps/app/components/core/theme/theme-switch.tsx
rename to web/components/core/theme/theme-switch.tsx
diff --git a/apps/app/components/core/views/all-views.tsx b/web/components/core/views/all-views.tsx
similarity index 84%
rename from apps/app/components/core/views/all-views.tsx
rename to web/components/core/views/all-views.tsx
index 79d5d6b11..750c1a552 100644
--- a/apps/app/components/core/views/all-views.tsx
+++ b/web/components/core/views/all-views.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback } from "react";
+import React, { useCallback, useState } from "react";
import { useRouter } from "next/router";
@@ -53,6 +53,7 @@ type Props = {
handleOnDragEnd: (result: DropResult) => Promise;
openIssuesListModal: (() => void) | null;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
+ disableAddIssueOption?: boolean;
trashBox: boolean;
setTrashBox: React.Dispatch>;
viewProps: IIssueViewProps;
@@ -68,6 +69,7 @@ export const AllViews: React.FC = ({
handleOnDragEnd,
openIssuesListModal,
removeIssue,
+ disableAddIssueOption = false,
trashBox,
setTrashBox,
viewProps,
@@ -75,10 +77,12 @@ export const AllViews: React.FC = ({
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
+ const [myIssueProjectId, setMyIssueProjectId] = useState(null);
+
const { user } = useUser();
const { memberRole } = useProjectMyMembership();
- const { groupedIssues, isEmpty, issueView } = viewProps;
+ const { groupedIssues, isEmpty, displayFilters } = viewProps;
const { data: stateGroups } = useSWR(
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
@@ -88,6 +92,10 @@ export const AllViews: React.FC = ({
);
const states = getStatesList(stateGroups);
+ const handleMyIssueOpen = (issue: IIssue) => {
+ setMyIssueProjectId(issue.project);
+ };
+
const handleTrashBox = useCallback(
(isDragging: boolean) => {
if (isDragging && !trashBox) setTrashBox(true);
@@ -115,37 +123,43 @@ export const AllViews: React.FC = ({
{groupedIssues ? (
!isEmpty ||
- issueView === "kanban" ||
- issueView === "calendar" ||
- issueView === "gantt_chart" ? (
+ displayFilters?.layout === "kanban" ||
+ displayFilters?.layout === "calendar" ||
+ displayFilters?.layout === "gantt_chart" ? (
<>
- {issueView === "list" ? (
+ {displayFilters?.layout === "list" ? (
- ) : issueView === "kanban" ? (
+ ) : displayFilters?.layout === "kanban" ? (
- ) : issueView === "calendar" ? (
+ ) : displayFilters?.layout === "calendar" ? (
= ({
user={user}
userAuth={memberRole}
/>
- ) : issueView === "spreadsheet" ? (
+ ) : displayFilters?.layout === "spreadsheet" ? (
= ({
userAuth={memberRole}
/>
) : (
- issueView === "gantt_chart" &&
+ displayFilters?.layout === "gantt_chart" && (
+
+ )
)}
>
) : router.pathname.includes("archived-issues") ? (
diff --git a/apps/app/components/core/views/board-view/all-boards.tsx b/web/components/core/views/board-view/all-boards.tsx
similarity index 55%
rename from apps/app/components/core/views/board-view/all-boards.tsx
rename to web/components/core/views/board-view/all-boards.tsx
index ee0fc668b..ca0dd59a2 100644
--- a/apps/app/components/core/views/board-view/all-boards.tsx
+++ b/web/components/core/views/board-view/all-boards.tsx
@@ -1,7 +1,14 @@
+import { useRouter } from "next/router";
+
+//hook
+import useMyIssues from "hooks/my-issues/use-my-issues";
+import useIssuesView from "hooks/use-issues-view";
+import useProfileIssues from "hooks/use-profile-issues";
// components
import { SingleBoard } from "components/core/views/board-view/single-board";
+import { IssuePeekOverview } from "components/issues";
// icons
-import { getStateGroupIcon } from "components/icons";
+import { StateGroupIcon } from "components/icons";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
@@ -10,11 +17,14 @@ import { ICurrentUserResponse, IIssue, IIssueViewProps, IState, UserAuth } from
type Props = {
addIssueToGroup: (groupTitle: string) => void;
disableUserActions: boolean;
+ disableAddIssueOption?: boolean;
dragDisabled: boolean;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleTrashBox: (isDragging: boolean) => void;
openIssuesListModal?: (() => void) | null;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
+ myIssueProjectId?: string | null;
+ handleMyIssueOpen?: (issue: IIssue) => void;
states: IState[] | undefined;
user: ICurrentUserResponse | undefined;
userAuth: UserAuth;
@@ -24,27 +34,55 @@ type Props = {
export const AllBoards: React.FC = ({
addIssueToGroup,
disableUserActions,
+ disableAddIssueOption = false,
dragDisabled,
handleIssueAction,
handleTrashBox,
openIssuesListModal,
+ myIssueProjectId,
+ handleMyIssueOpen,
removeIssue,
states,
user,
userAuth,
viewProps,
}) => {
- const { groupByProperty: selectedGroup, groupedIssues, showEmptyGroups } = viewProps;
+ const router = useRouter();
+ const { workspaceSlug, projectId, userId } = router.query;
+
+ const isProfileIssue =
+ router.pathname.includes("assigned") ||
+ router.pathname.includes("created") ||
+ router.pathname.includes("subscribed");
+
+ const isMyIssue = router.pathname.includes("my-issues");
+
+ const { mutateIssues } = useIssuesView();
+ const { mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
+ const { mutateProfileIssues } = useProfileIssues(workspaceSlug?.toString(), userId?.toString());
+
+ const { displayFilters, groupedIssues } = viewProps;
return (
<>
+
+ isMyIssue ? mutateMyIssues() : isProfileIssue ? mutateProfileIssues() : mutateIssues()
+ }
+ projectId={myIssueProjectId ? myIssueProjectId : projectId?.toString() ?? ""}
+ workspaceSlug={workspaceSlug?.toString() ?? ""}
+ readOnly={disableUserActions}
+ />
{groupedIssues ? (
{Object.keys(groupedIssues).map((singleGroup, index) => {
const currentState =
- selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
+ displayFilters?.group_by === "state"
+ ? states?.find((s) => s.id === singleGroup)
+ : null;
- if (!showEmptyGroups && groupedIssues[singleGroup].length === 0) return null;
+ if (!displayFilters?.show_empty_groups && groupedIssues[singleGroup].length === 0)
+ return null;
return (
= ({
addIssueToGroup={() => addIssueToGroup(singleGroup)}
currentState={currentState}
disableUserActions={disableUserActions}
+ disableAddIssueOption={disableAddIssueOption}
dragDisabled={dragDisabled}
groupTitle={singleGroup}
handleIssueAction={handleIssueAction}
handleTrashBox={handleTrashBox}
openIssuesListModal={openIssuesListModal ?? null}
+ handleMyIssueOpen={handleMyIssueOpen}
removeIssue={removeIssue}
user={user}
userAuth={userAuth}
@@ -64,13 +104,15 @@ export const AllBoards: React.FC = ({
/>
);
})}
- {!showEmptyGroups && (
+ {!displayFilters?.show_empty_groups && (
Hidden groups
{Object.keys(groupedIssues).map((singleGroup, index) => {
const currentState =
- selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
+ displayFilters?.group_by === "state"
+ ? states?.find((s) => s.id === singleGroup)
+ : null;
if (groupedIssues[singleGroup].length === 0)
return (
@@ -79,10 +121,16 @@ export const AllBoards: React.FC
= ({
className="flex items-center justify-between gap-2 rounded bg-custom-background-90 p-2 shadow"
>
- {currentState &&
- getStateGroupIcon(currentState.group, "16", "16", currentState.color)}
+ {currentState && (
+
+ )}
- {selectedGroup === "state"
+ {displayFilters?.group_by === "state"
? addSpaceIfCamelCase(currentState?.name ?? "")
: addSpaceIfCamelCase(singleGroup)}
diff --git a/apps/app/components/core/views/board-view/board-header.tsx b/web/components/core/views/board-view/board-header.tsx
similarity index 77%
rename from apps/app/components/core/views/board-view/board-header.tsx
rename to web/components/core/views/board-view/board-header.tsx
index d5b11f5f0..1cbfdc81a 100644
--- a/apps/app/components/core/views/board-view/board-header.tsx
+++ b/web/components/core/views/board-view/board-header.tsx
@@ -13,14 +13,16 @@ import useProjects from "hooks/use-projects";
import { Avatar, Icon } from "components/ui";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
-import { getPriorityIcon, getStateGroupIcon } from "components/icons";
+import { PriorityIcon, StateGroupIcon } from "components/icons";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
import { renderEmoji } from "helpers/emoji.helper";
// types
-import { IIssueViewProps, IState } from "types";
+import { IIssueViewProps, IState, TIssuePriorities, TStateGroups } from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
+// constants
+import { STATE_GROUP_COLORS } from "constants/state";
type Props = {
currentState?: IState | null;
@@ -29,6 +31,7 @@ type Props = {
isCollapsed: boolean;
setIsCollapsed: React.Dispatch
>;
disableUserActions: boolean;
+ disableAddIssue: boolean;
viewProps: IIssueViewProps;
};
@@ -39,27 +42,34 @@ export const BoardHeader: React.FC = ({
isCollapsed,
setIsCollapsed,
disableUserActions,
+ disableAddIssue,
viewProps,
}) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
- const { groupedIssues, groupByProperty: selectedGroup } = viewProps;
+ const { displayFilters, groupedIssues } = viewProps;
+
+ console.log("dF", displayFilters);
const { data: issueLabels } = useSWR(
- workspaceSlug && projectId && selectedGroup === "labels"
+ workspaceSlug && projectId && displayFilters?.group_by === "labels"
? PROJECT_ISSUE_LABELS(projectId.toString())
: null,
- workspaceSlug && projectId && selectedGroup === "labels"
+ workspaceSlug && projectId && displayFilters?.group_by === "labels"
? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString())
: null
);
const { data: members } = useSWR(
- workspaceSlug && projectId && (selectedGroup === "created_by" || selectedGroup === "assignees")
+ workspaceSlug &&
+ projectId &&
+ (displayFilters?.group_by === "created_by" || displayFilters?.group_by === "assignees")
? PROJECT_MEMBERS(projectId.toString())
: null,
- workspaceSlug && projectId && (selectedGroup === "created_by" || selectedGroup === "assignees")
+ workspaceSlug &&
+ projectId &&
+ (displayFilters?.group_by === "created_by" || displayFilters?.group_by === "assignees")
? () => projectService.projectMembers(workspaceSlug.toString(), projectId.toString())
: null
);
@@ -69,7 +79,7 @@ export const BoardHeader: React.FC = ({
const getGroupTitle = () => {
let title = addSpaceIfCamelCase(groupTitle);
- switch (selectedGroup) {
+ switch (displayFilters?.group_by) {
case "state":
title = addSpaceIfCamelCase(currentState?.name ?? "");
break;
@@ -93,16 +103,29 @@ export const BoardHeader: React.FC = ({
const getGroupIcon = () => {
let icon;
- switch (selectedGroup) {
+ switch (displayFilters?.group_by) {
case "state":
- icon =
- currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color);
+ icon = currentState && (
+
+ );
break;
case "state_detail.group":
- icon = getStateGroupIcon(groupTitle as any, "16", "16");
+ icon = (
+
+ );
break;
case "priority":
- icon = getPriorityIcon(groupTitle, "text-lg");
+ icon = ;
break;
case "project":
const project = projects?.find((p) => p.id === groupTitle);
@@ -150,7 +173,7 @@ export const BoardHeader: React.FC = ({
{getGroupIcon()}
= ({
)}
- {!disableUserActions && selectedGroup !== "created_by" && (
+ {!disableAddIssue && !disableUserActions && displayFilters?.group_by !== "created_by" && (
void;
currentState?: IState | null;
disableUserActions: boolean;
+ disableAddIssueOption?: boolean;
dragDisabled: boolean;
groupTitle: string;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleTrashBox: (isDragging: boolean) => void;
openIssuesListModal?: (() => void) | null;
+ handleMyIssueOpen?: (issue: IIssue) => void;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
user: ICurrentUserResponse | undefined;
userAuth: UserAuth;
@@ -36,10 +38,12 @@ export const SingleBoard: React.FC = ({
currentState,
groupTitle,
disableUserActions,
+ disableAddIssueOption = false,
dragDisabled,
handleIssueAction,
handleTrashBox,
openIssuesListModal,
+ handleMyIssueOpen,
removeIssue,
user,
userAuth,
@@ -48,7 +52,7 @@ export const SingleBoard: React.FC = ({
// collapse/expand
const [isCollapsed, setIsCollapsed] = useState(true);
- const { groupedIssues, groupByProperty: selectedGroup, orderBy, properties } = viewProps;
+ const { displayFilters, groupedIssues } = viewProps;
const router = useRouter();
const { cycleId, moduleId } = router.query;
@@ -70,6 +74,7 @@ export const SingleBoard: React.FC = ({
isCollapsed={isCollapsed}
setIsCollapsed={setIsCollapsed}
disableUserActions={disableUserActions}
+ disableAddIssue={disableAddIssueOption}
viewProps={viewProps}
/>
{isCollapsed && (
@@ -77,14 +82,14 @@ export const SingleBoard: React.FC = ({
{(provided, snapshot) => (
- {orderBy !== "sort_order" && (
+ {displayFilters?.order_by !== "sort_order" && (
<>
= ({
>
This board is ordered by{" "}
{replaceUnderscoreIfSnakeCase(
- orderBy ? (orderBy[0] === "-" ? orderBy.slice(1) : orderBy) : "created_at"
+ displayFilters?.order_by
+ ? displayFilters?.order_by[0] === "-"
+ ? displayFilters?.order_by.slice(1)
+ : displayFilters?.order_by
+ : "created_at"
)}
>
@@ -128,6 +137,7 @@ export const SingleBoard: React.FC
= ({
makeIssueCopy={() => handleIssueAction(issue, "copy")}
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
handleTrashBox={handleTrashBox}
+ handleMyIssueOpen={handleMyIssueOpen}
removeIssue={() => {
if (removeIssue && issue.bridge_id)
removeIssue(issue.bridge_id, issue.id);
@@ -142,49 +152,49 @@ export const SingleBoard: React.FC = ({
))}
{provided.placeholder}
- {selectedGroup !== "created_by" && (
+ {displayFilters?.group_by !== "created_by" && (
- {type === "issue" ? (
-
-
- Add Issue
-
- ) : (
- !disableUserActions && (
-
-
- Add Issue
-
- }
- position="left"
- noBorder
- >
-
- Create new
-
- {openIssuesListModal && (
-
- Add an existing issue
+ {type === "issue"
+ ? !disableAddIssueOption && (
+
+
+ Add Issue
+
+ )
+ : !disableUserActions && (
+
+
+ Add Issue
+
+ }
+ position="left"
+ noBorder
+ >
+
+ Create new
- )}
-
- )
- )}
+ {openIssuesListModal && (
+
+ Add an existing issue
+
+ )}
+
+ )}
)}
diff --git a/apps/app/components/core/views/board-view/single-issue.tsx b/web/components/core/views/board-view/single-issue.tsx
similarity index 91%
rename from apps/app/components/core/views/board-view/single-issue.tsx
rename to web/components/core/views/board-view/single-issue.tsx
index b676e809c..ffd4747d9 100644
--- a/apps/app/components/core/views/board-view/single-issue.tsx
+++ b/web/components/core/views/board-view/single-issue.tsx
@@ -1,6 +1,5 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
-import Link from "next/link";
import { useRouter } from "next/router";
import { mutate } from "swr";
@@ -58,6 +57,7 @@ type Props = {
index: number;
editIssue: () => void;
makeIssueCopy: () => void;
+ handleMyIssueOpen?: (issue: IIssue) => void;
removeIssue?: (() => void) | null;
handleDeleteIssue: (issue: IIssue) => void;
handleTrashBox: (isDragging: boolean) => void;
@@ -75,6 +75,7 @@ export const SingleBoardIssue: React.FC = ({
index,
editIssue,
makeIssueCopy,
+ handleMyIssueOpen,
removeIssue,
groupTitle,
handleDeleteIssue,
@@ -93,7 +94,7 @@ export const SingleBoardIssue: React.FC = ({
const actionSectionRef = useRef(null);
- const { groupByProperty: selectedGroup, orderBy, properties, mutateIssues } = viewProps;
+ const { displayFilters, properties, mutateIssues } = viewProps;
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
@@ -131,9 +132,9 @@ export const SingleBoardIssue: React.FC = ({
handleIssuesMutation(
formData,
groupTitle ?? "",
- selectedGroup,
+ displayFilters?.group_by ?? null,
index,
- orderBy,
+ displayFilters?.order_by ?? "-created_at",
prevData
),
false
@@ -149,24 +150,14 @@ export const SingleBoardIssue: React.FC = ({
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
});
},
- [
- workspaceSlug,
- cycleId,
- moduleId,
- groupTitle,
- index,
- selectedGroup,
- mutateIssues,
- orderBy,
- user,
- ]
+ [displayFilters, workspaceSlug, cycleId, moduleId, groupTitle, index, mutateIssues, user]
);
const getStyle = (
style: DraggingStyle | NotDraggingStyle | undefined,
snapshot: DraggableStateSnapshot
) => {
- if (orderBy === "sort_order") return style;
+ if (displayFilters?.order_by === "sort_order") return style;
if (!snapshot.isDragging) return {};
if (!snapshot.isDropAnimating) return style;
@@ -197,6 +188,17 @@ export const SingleBoardIssue: React.FC = ({
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
+ const openPeekOverview = () => {
+ const { query } = router;
+
+ if (handleMyIssueOpen) handleMyIssueOpen(issue);
+
+ router.push({
+ pathname: router.pathname,
+ query: { ...query, peekIssue: issue.id },
+ });
+ };
+
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
return (
@@ -296,16 +298,22 @@ export const SingleBoardIssue: React.FC = ({
)}
)}
-
-
- {properties.key && (
-
- {issue.project_detail.identifier}-{issue.sequence_id}
-
- )}
- {issue.name}
-
-
+
+
+ {properties.key && (
+
+ {issue.project_detail.identifier}-{issue.sequence_id}
+
+ )}
+
+ {issue.name}
+
+
+
= ({
/>
)}
{properties.labels && issue.labels.length > 0 && (
-
+
)}
{properties.assignee && (
= ({
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
- const { calendarIssues, params, setCalendarDateRange } = useCalendarIssuesView();
+ const { calendarIssues, mutateIssues, params, displayFilters, setDisplayFilters } =
+ useCalendarIssuesView();
const totalDate = eachDayOfInterval({
start: calendarDates.startDate,
@@ -152,90 +154,103 @@ export const CalendarView: React.FC = ({
endDate,
});
- setCalendarDateRange(
- `${renderDateFormat(startDate)};after,${renderDateFormat(endDate)};before`
- );
+ setDisplayFilters({
+ calendar_date_range: `${renderDateFormat(startDate)};after,${renderDateFormat(
+ endDate
+ )};before`,
+ });
};
useEffect(() => {
- setCalendarDateRange(
- `${renderDateFormat(startOfWeek(currentDate))};after,${renderDateFormat(
- lastDayOfWeek(currentDate)
- )};before`
- );
- }, [currentDate]);
+ if (!displayFilters || displayFilters.calendar_date_range === "")
+ setDisplayFilters({
+ calendar_date_range: `${renderDateFormat(
+ startOfWeek(currentDate)
+ )};after,${renderDateFormat(lastDayOfWeek(currentDate))};before`,
+ });
+ }, [currentDate, displayFilters, setDisplayFilters]);
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
- return calendarIssues ? (
-
-
-
-
+ return (
+ <>
+
mutateIssues()}
+ projectId={projectId?.toString() ?? ""}
+ workspaceSlug={workspaceSlug?.toString() ?? ""}
+ readOnly={disableUserActions}
+ />
+ {calendarIssues ? (
+
+
+
+
-
- {weeks.map((date, index) => (
-
- {isMonthlyView
- ? formatDate(date, "eee").substring(0, 3)
- : formatDate(date, "eee")}
-
- {!isMonthlyView &&
{formatDate(date, "d")} }
+ {weeks.map((date, index) => (
+
+
+ {isMonthlyView
+ ? formatDate(date, "eee").substring(0, 3)
+ : formatDate(date, "eee")}
+
+ {!isMonthlyView && {formatDate(date, "d")} }
+
+ ))}
- ))}
-
-
- {currentViewDaysData.map((date, index) => (
-
- ))}
-
+
+ {currentViewDaysData.map((date, index) => (
+
+ ))}
+
+
+
-
-
- ) : (
-
-
-
+ ) : (
+
+
+
+ )}
+ >
);
};
diff --git a/apps/app/components/core/views/calendar-view/index.ts b/web/components/core/views/calendar-view/index.ts
similarity index 100%
rename from apps/app/components/core/views/calendar-view/index.ts
rename to web/components/core/views/calendar-view/index.ts
diff --git a/apps/app/components/core/views/calendar-view/single-date.tsx b/web/components/core/views/calendar-view/single-date.tsx
similarity index 100%
rename from apps/app/components/core/views/calendar-view/single-date.tsx
rename to web/components/core/views/calendar-view/single-date.tsx
diff --git a/apps/app/components/core/views/calendar-view/single-issue.tsx b/web/components/core/views/calendar-view/single-issue.tsx
similarity index 91%
rename from apps/app/components/core/views/calendar-view/single-issue.tsx
rename to web/components/core/views/calendar-view/single-issue.tsx
index f6c1cc2f7..3db571c99 100644
--- a/apps/app/components/core/views/calendar-view/single-issue.tsx
+++ b/web/components/core/views/calendar-view/single-issue.tsx
@@ -1,6 +1,5 @@
import React, { useCallback } from "react";
-import Link from "next/link";
import { useRouter } from "next/router";
import { mutate } from "swr";
@@ -158,6 +157,15 @@ export const SingleCalendarIssue: React.FC = ({
? Object.values(properties).some((value) => value === true)
: false;
+ const openPeekOverview = () => {
+ const { query } = router;
+
+ router.push({
+ pathname: router.pathname,
+ query: { ...query, peekIssue: issue.id },
+ });
+ };
+
return (
= ({
)}
-
-
- {properties.key && (
-
-
- {issue.project_detail?.identifier}-{issue.sequence_id}
-
-
- )}
-
- {truncateText(issue.name, 25)}
+
+
+ {properties.key && (
+
+
+ {issue.project_detail?.identifier}-{issue.sequence_id}
+
-
-
+ )}
+
+ {truncateText(issue.name, 25)}
+
+
+
{displayProperties && (
{properties.priority && (
diff --git a/apps/app/components/core/views/gantt-chart-view/index.tsx b/web/components/core/views/gantt-chart-view/index.tsx
similarity index 53%
rename from apps/app/components/core/views/gantt-chart-view/index.tsx
rename to web/components/core/views/gantt-chart-view/index.tsx
index a881cb7aa..2cd10f95f 100644
--- a/apps/app/components/core/views/gantt-chart-view/index.tsx
+++ b/web/components/core/views/gantt-chart-view/index.tsx
@@ -6,20 +6,24 @@ import { IssueGanttChartView } from "components/issues";
import { ModuleIssuesGanttChartView } from "components/modules";
import { ViewIssuesGanttChartView } from "components/views";
-export const GanttChartView = () => {
+type Props = {
+ disableUserActions: boolean;
+};
+
+export const GanttChartView: React.FC
= ({ disableUserActions }) => {
const router = useRouter();
const { cycleId, moduleId, viewId } = router.query;
return (
<>
{cycleId ? (
-
+
) : moduleId ? (
-
+
) : viewId ? (
-
+
) : (
-
+
)}
>
);
diff --git a/apps/app/components/core/views/index.ts b/web/components/core/views/index.ts
similarity index 100%
rename from apps/app/components/core/views/index.ts
rename to web/components/core/views/index.ts
diff --git a/apps/app/components/core/views/issues-view.tsx b/web/components/core/views/issues-view.tsx
similarity index 90%
rename from apps/app/components/core/views/issues-view.tsx
rename to web/components/core/views/issues-view.tsx
index 755b37aa1..e0e7e8c94 100644
--- a/apps/app/components/core/views/issues-view.tsx
+++ b/web/components/core/views/issues-view.tsx
@@ -19,7 +19,7 @@ import useIssuesProperties from "hooks/use-issue-properties";
import useProjectMembers from "hooks/use-project-members";
// components
import { FiltersList, AllViews } from "components/core";
-import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
+import { CreateUpdateIssueModal, DeleteIssueModal, IssuePeekOverview } from "components/issues";
import { CreateUpdateViewModal } from "components/views";
// ui
import { PrimaryButton, SecondaryButton } from "components/ui";
@@ -29,7 +29,7 @@ import { PlusIcon } from "@heroicons/react/24/outline";
import { getStatesList } from "helpers/state.helper";
import { orderArrayBy } from "helpers/array.helper";
// types
-import { IIssue, IIssueFilterOptions, IState } from "types";
+import { IIssue, IIssueFilterOptions, IState, TIssuePriorities } from "types";
// fetch-keys
import {
CYCLE_DETAILS,
@@ -77,18 +77,8 @@ export const IssuesView: React.FC = ({
const { setToastAlert } = useToast();
- const {
- groupedByIssues,
- mutateIssues,
- issueView,
- groupByProperty: selectedGroup,
- orderBy,
- filters,
- isEmpty,
- setFilters,
- params,
- showEmptyGroups,
- } = useIssuesView();
+ const { groupedByIssues, mutateIssues, displayFilters, filters, isEmpty, setFilters, params } =
+ useIssuesView();
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
const { data: stateGroups } = useSWR(
@@ -129,7 +119,7 @@ export const IssuesView: React.FC = ({
if (destination.droppableId === "trashBox") {
handleDeleteIssue(draggedItem);
} else {
- if (orderBy === "sort_order") {
+ if (displayFilters.order_by === "sort_order") {
let newSortOrder = draggedItem.sort_order;
const destinationGroupArray = groupedByIssues[destination.droppableId];
@@ -177,15 +167,19 @@ export const IssuesView: React.FC = ({
const destinationGroup = destination.droppableId; // destination group id
- if (orderBy === "sort_order" || source.droppableId !== destination.droppableId) {
+ if (
+ displayFilters.order_by === "sort_order" ||
+ source.droppableId !== destination.droppableId
+ ) {
// different group/column;
// source.droppableId !== destination.droppableId -> even if order by is not sort_order,
// if the issue is moved to a different group, then we will change the group of the
// dragged item(or issue)
- if (selectedGroup === "priority") draggedItem.priority = destinationGroup;
- else if (selectedGroup === "state") {
+ if (displayFilters.group_by === "priority")
+ draggedItem.priority = destinationGroup as TIssuePriorities;
+ else if (displayFilters.group_by === "state") {
draggedItem.state = destinationGroup;
draggedItem.state_detail = states?.find((s) => s.id === destinationGroup) as IState;
}
@@ -212,8 +206,14 @@ export const IssuesView: React.FC = ({
return {
...prevData,
- [sourceGroup]: orderArrayBy(sourceGroupArray, orderBy),
- [destinationGroup]: orderArrayBy(destinationGroupArray, orderBy),
+ [sourceGroup]: orderArrayBy(
+ sourceGroupArray,
+ displayFilters.order_by ?? "-created_at"
+ ),
+ [destinationGroup]: orderArrayBy(
+ destinationGroupArray,
+ displayFilters.order_by ?? "-created_at"
+ ),
};
},
false
@@ -266,13 +266,13 @@ export const IssuesView: React.FC = ({
}
},
[
+ displayFilters.group_by,
+ displayFilters.order_by,
workspaceSlug,
cycleId,
moduleId,
groupedByIssues,
projectId,
- selectedGroup,
- orderBy,
handleDeleteIssue,
params,
states,
@@ -286,19 +286,19 @@ export const IssuesView: React.FC = ({
let preloadedValue: string | string[] = groupTitle;
- if (selectedGroup === "labels") {
+ if (displayFilters.group_by === "labels") {
if (groupTitle === "None") preloadedValue = [];
else preloadedValue = [groupTitle];
}
- if (selectedGroup)
+ if (displayFilters.group_by)
setPreloadedData({
- [selectedGroup]: preloadedValue,
+ [displayFilters.group_by]: preloadedValue,
actionType: "createIssue",
});
else setPreloadedData({ actionType: "createIssue" });
},
- [setCreateIssueModal, setPreloadedData, selectedGroup]
+ [displayFilters.group_by, setCreateIssueModal, setPreloadedData]
);
const addIssueToDate = useCallback(
@@ -351,7 +351,7 @@ export const IssuesView: React.FC = ({
CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params),
(prevData: any) => {
if (!prevData) return prevData;
- if (selectedGroup) {
+ if (displayFilters.group_by) {
const filteredData: any = {};
for (const key in prevData) {
filteredData[key] = prevData[key].filter((item: any) => item.id !== issueId);
@@ -383,7 +383,7 @@ export const IssuesView: React.FC = ({
console.log(e);
});
},
- [workspaceSlug, projectId, cycleId, params, selectedGroup, setToastAlert]
+ [displayFilters.group_by, workspaceSlug, projectId, cycleId, params, setToastAlert]
);
const removeIssueFromModule = useCallback(
@@ -394,7 +394,7 @@ export const IssuesView: React.FC = ({
MODULE_ISSUES_WITH_PARAMS(moduleId as string, params),
(prevData: any) => {
if (!prevData) return prevData;
- if (selectedGroup) {
+ if (displayFilters.group_by) {
const filteredData: any = {};
for (const key in prevData) {
filteredData[key] = prevData[key].filter((item: any) => item.id !== issueId);
@@ -426,7 +426,7 @@ export const IssuesView: React.FC = ({
console.log(e);
});
},
- [workspaceSlug, projectId, moduleId, params, selectedGroup, setToastAlert]
+ [displayFilters.group_by, workspaceSlug, projectId, moduleId, params, setToastAlert]
);
const nullFilters = Object.keys(filters).filter(
@@ -462,6 +462,7 @@ export const IssuesView: React.FC = ({
data={issueToDelete}
user={user}
/>
+
{areFiltersApplied && (
<>
@@ -480,7 +481,6 @@ export const IssuesView: React.FC
= ({
state: null,
start_date: null,
target_date: null,
- type: null,
})
}
/>
@@ -512,10 +512,10 @@ export const IssuesView: React.FC = ({
addIssueToGroup={addIssueToGroup}
disableUserActions={disableUserActions}
dragDisabled={
- selectedGroup === "created_by" ||
- selectedGroup === "labels" ||
- selectedGroup === "state_detail.group" ||
- selectedGroup === "assignees"
+ displayFilters.group_by === "created_by" ||
+ displayFilters.group_by === "labels" ||
+ displayFilters.group_by === "state_detail.group" ||
+ displayFilters.group_by === "assignees"
}
emptyState={{
title: cycleId
@@ -553,15 +553,12 @@ export const IssuesView: React.FC = ({
trashBox={trashBox}
setTrashBox={setTrashBox}
viewProps={{
- groupByProperty: selectedGroup,
groupedIssues: groupedByIssues,
+ displayFilters,
isEmpty,
- issueView,
mutateIssues,
- orderBy,
params,
properties,
- showEmptyGroups,
}}
/>
>
diff --git a/web/components/core/views/list-view/all-lists.tsx b/web/components/core/views/list-view/all-lists.tsx
new file mode 100644
index 000000000..bb0a7c0fb
--- /dev/null
+++ b/web/components/core/views/list-view/all-lists.tsx
@@ -0,0 +1,101 @@
+import { useRouter } from "next/router";
+
+// hooks
+import useMyIssues from "hooks/my-issues/use-my-issues";
+import useIssuesView from "hooks/use-issues-view";
+import useProfileIssues from "hooks/use-profile-issues";
+// components
+import { SingleList } from "components/core/views/list-view/single-list";
+import { IssuePeekOverview } from "components/issues";
+// types
+import { ICurrentUserResponse, IIssue, IIssueViewProps, IState, UserAuth } from "types";
+
+// types
+type Props = {
+ states: IState[] | undefined;
+ addIssueToGroup: (groupTitle: string) => void;
+ handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
+ openIssuesListModal?: (() => void) | null;
+ myIssueProjectId?: string | null;
+ handleMyIssueOpen?: (issue: IIssue) => void;
+ removeIssue: ((bridgeId: string, issueId: string) => void) | null;
+ disableUserActions: boolean;
+ disableAddIssueOption?: boolean;
+ user: ICurrentUserResponse | undefined;
+ userAuth: UserAuth;
+ viewProps: IIssueViewProps;
+};
+
+export const AllLists: React.FC = ({
+ addIssueToGroup,
+ handleIssueAction,
+ disableUserActions,
+ disableAddIssueOption = false,
+ openIssuesListModal,
+ handleMyIssueOpen,
+ myIssueProjectId,
+ removeIssue,
+ states,
+ user,
+ userAuth,
+ viewProps,
+}) => {
+ const router = useRouter();
+ const { workspaceSlug, projectId, userId } = router.query;
+
+ const isProfileIssue =
+ router.pathname.includes("assigned") ||
+ router.pathname.includes("created") ||
+ router.pathname.includes("subscribed");
+
+ const isMyIssue = router.pathname.includes("my-issues");
+ const { mutateIssues } = useIssuesView();
+ const { mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
+ const { mutateProfileIssues } = useProfileIssues(workspaceSlug?.toString(), userId?.toString());
+
+ const { displayFilters, groupedIssues } = viewProps;
+
+ return (
+ <>
+
+ isMyIssue ? mutateMyIssues() : isProfileIssue ? mutateProfileIssues() : mutateIssues()
+ }
+ projectId={myIssueProjectId ? myIssueProjectId : projectId?.toString() ?? ""}
+ workspaceSlug={workspaceSlug?.toString() ?? ""}
+ readOnly={disableUserActions}
+ />
+ {groupedIssues && (
+
+ {Object.keys(groupedIssues).map((singleGroup) => {
+ const currentState =
+ displayFilters?.group_by === "state"
+ ? states?.find((s) => s.id === singleGroup)
+ : null;
+
+ if (!displayFilters?.show_empty_groups && groupedIssues[singleGroup].length === 0)
+ return null;
+
+ return (
+ addIssueToGroup(singleGroup)}
+ handleIssueAction={handleIssueAction}
+ handleMyIssueOpen={handleMyIssueOpen}
+ openIssuesListModal={openIssuesListModal}
+ removeIssue={removeIssue}
+ disableUserActions={disableUserActions}
+ disableAddIssueOption={disableAddIssueOption}
+ user={user}
+ userAuth={userAuth}
+ viewProps={viewProps}
+ />
+ );
+ })}
+
+ )}
+ >
+ );
+};
diff --git a/apps/app/components/core/views/list-view/index.ts b/web/components/core/views/list-view/index.ts
similarity index 100%
rename from apps/app/components/core/views/list-view/index.ts
rename to web/components/core/views/list-view/index.ts
diff --git a/apps/app/components/core/views/list-view/single-issue.tsx b/web/components/core/views/list-view/single-issue.tsx
similarity index 83%
rename from apps/app/components/core/views/list-view/single-issue.tsx
rename to web/components/core/views/list-view/single-issue.tsx
index eafe74612..ab5c080ca 100644
--- a/apps/app/components/core/views/list-view/single-issue.tsx
+++ b/web/components/core/views/list-view/single-issue.tsx
@@ -36,9 +36,21 @@ import { LayerDiagonalIcon } from "components/icons";
import { copyTextToClipboard } from "helpers/string.helper";
import { handleIssuesMutation } from "constants/issue";
// types
-import { ICurrentUserResponse, IIssue, IIssueViewProps, ISubIssueResponse, UserAuth } from "types";
+import {
+ ICurrentUserResponse,
+ IIssue,
+ IIssueViewProps,
+ ISubIssueResponse,
+ IUserProfileProjectSegregation,
+ UserAuth,
+} from "types";
// fetch-keys
-import { CYCLE_DETAILS, MODULE_DETAILS, SUB_ISSUES } from "constants/fetch-keys";
+import {
+ CYCLE_DETAILS,
+ MODULE_DETAILS,
+ SUB_ISSUES,
+ USER_PROFILE_PROJECT_SEGREGATION,
+} from "constants/fetch-keys";
type Props = {
type?: string;
@@ -49,6 +61,7 @@ type Props = {
makeIssueCopy: () => void;
removeIssue?: (() => void) | null;
handleDeleteIssue: (issue: IIssue) => void;
+ handleMyIssueOpen?: (issue: IIssue) => void;
disableUserActions: boolean;
user: ICurrentUserResponse | undefined;
userAuth: UserAuth;
@@ -64,6 +77,7 @@ export const SingleListIssue: React.FC = ({
removeIssue,
groupTitle,
handleDeleteIssue,
+ handleMyIssueOpen,
disableUserActions,
user,
userAuth,
@@ -74,12 +88,12 @@ export const SingleListIssue: React.FC = ({
const [contextMenuPosition, setContextMenuPosition] = useState(null);
const router = useRouter();
- const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
+ const { workspaceSlug, projectId, cycleId, moduleId, userId } = router.query;
const isArchivedIssues = router.pathname.includes("archived-issues");
const { setToastAlert } = useToast();
- const { groupByProperty: selectedGroup, orderBy, properties, mutateIssues } = viewProps;
+ const { displayFilters, properties, mutateIssues } = viewProps;
const partialUpdateIssue = useCallback(
(formData: Partial, issue: IIssue) => {
@@ -112,9 +126,9 @@ export const SingleListIssue: React.FC = ({
handleIssuesMutation(
formData,
groupTitle ?? "",
- selectedGroup,
+ displayFilters?.group_by ?? null,
index,
- orderBy,
+ displayFilters?.order_by ?? "-created_at",
prevData
),
false
@@ -126,19 +140,24 @@ export const SingleListIssue: React.FC = ({
.then(() => {
mutateIssues();
+ if (userId)
+ mutate(
+ USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug.toString(), userId.toString())
+ );
+
if (cycleId) mutate(CYCLE_DETAILS(cycleId as string));
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
});
},
[
+ displayFilters,
workspaceSlug,
cycleId,
moduleId,
+ userId,
groupTitle,
index,
- selectedGroup,
mutateIssues,
- orderBy,
user,
]
);
@@ -161,6 +180,16 @@ export const SingleListIssue: React.FC = ({
? `/${workspaceSlug}/projects/${issue.project}/archived-issues/${issue.id}`
: `/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`;
+ const openPeekOverview = (issue: IIssue) => {
+ const { query } = router;
+
+ if (handleMyIssueOpen) handleMyIssueOpen(issue);
+ router.push({
+ pathname: router.pathname,
+ query: { ...query, peekIssue: issue.id },
+ });
+ };
+
const isNotAllowed =
userAuth.isGuest || userAuth.isViewer || disableUserActions || isArchivedIssues;
@@ -203,23 +232,27 @@ export const SingleListIssue: React.FC = ({
}}
>
= ({
isNotAllowed={isNotAllowed}
/>
)}
- {properties.labels &&
}
+ {properties.labels &&
}
{properties.assignee && (
void;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
openIssuesListModal?: (() => void) | null;
+ handleMyIssueOpen?: (issue: IIssue) => void;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
disableUserActions: boolean;
+ disableAddIssueOption?: boolean;
user: ICurrentUserResponse | undefined;
userAuth: UserAuth;
viewProps: IIssueViewProps;
@@ -50,8 +56,10 @@ export const SingleList: React.FC = ({
addIssueToGroup,
handleIssueAction,
openIssuesListModal,
+ handleMyIssueOpen,
removeIssue,
disableUserActions,
+ disableAddIssueOption = false,
user,
userAuth,
viewProps,
@@ -63,7 +71,7 @@ export const SingleList: React.FC = ({
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
- const { groupByProperty: selectedGroup, groupedIssues } = viewProps;
+ const { displayFilters, groupedIssues } = viewProps;
const { data: issueLabels } = useSWR(
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
@@ -84,7 +92,7 @@ export const SingleList: React.FC = ({
const getGroupTitle = () => {
let title = addSpaceIfCamelCase(groupTitle);
- switch (selectedGroup) {
+ switch (displayFilters?.group_by) {
case "state":
title = addSpaceIfCamelCase(currentState?.name ?? "");
break;
@@ -107,16 +115,29 @@ export const SingleList: React.FC = ({
const getGroupIcon = () => {
let icon;
- switch (selectedGroup) {
+ switch (displayFilters?.group_by) {
case "state":
- icon =
- currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color);
+ icon = currentState && (
+
+ );
break;
case "state_detail.group":
- icon = getStateGroupIcon(groupTitle as any, "16", "16");
+ icon = (
+
+ );
break;
case "priority":
- icon = getPriorityIcon(groupTitle, "text-lg");
+ icon = ;
break;
case "project":
const project = projects?.find((p) => p.id === groupTitle);
@@ -158,13 +179,13 @@ export const SingleList: React.FC = ({
- {selectedGroup !== null && (
+ {displayFilters?.group_by !== null && (
{getGroupIcon()}
)}
- {selectedGroup !== null ? (
+ {displayFilters?.group_by !== null ? (
{getGroupTitle()}
@@ -180,13 +201,15 @@ export const SingleList: React.FC = ({
{isArchivedIssues ? (
""
) : type === "issue" ? (
-
-
-
+ !disableAddIssueOption && (
+
+
+
+ )
) : disableUserActions ? (
""
) : (
@@ -230,6 +253,7 @@ export const SingleList: React.FC = ({
editIssue={() => handleIssueAction(issue, "edit")}
makeIssueCopy={() => handleIssueAction(issue, "copy")}
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
+ handleMyIssueOpen={handleMyIssueOpen}
removeIssue={() => {
if (removeIssue !== null && issue.bridge_id)
removeIssue(issue.bridge_id, issue.id);
diff --git a/apps/app/components/core/views/spreadsheet-view/index.ts b/web/components/core/views/spreadsheet-view/index.ts
similarity index 100%
rename from apps/app/components/core/views/spreadsheet-view/index.ts
rename to web/components/core/views/spreadsheet-view/index.ts
diff --git a/apps/app/components/core/views/spreadsheet-view/single-issue.tsx b/web/components/core/views/spreadsheet-view/single-issue.tsx
similarity index 95%
rename from apps/app/components/core/views/spreadsheet-view/single-issue.tsx
rename to web/components/core/views/spreadsheet-view/single-issue.tsx
index 11a8c42c5..731d7f921 100644
--- a/apps/app/components/core/views/spreadsheet-view/single-issue.tsx
+++ b/web/components/core/views/spreadsheet-view/single-issue.tsx
@@ -6,7 +6,6 @@ import { mutate } from "swr";
// components
import {
- IssuePeekOverview,
ViewAssigneeSelect,
ViewDueDateSelect,
ViewEstimateSelect,
@@ -76,9 +75,6 @@ export const SingleSpreadsheetIssue: React.FC = ({
}) => {
const [isOpen, setIsOpen] = useState(false);
- // issue peek overview
- const [issuePeekOverview, setIssuePeekOverview] = useState(false);
-
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
@@ -161,6 +157,15 @@ export const SingleSpreadsheetIssue: React.FC = ({
[workspaceSlug, projectId, cycleId, moduleId, viewId, params, user]
);
+ const openPeekOverview = () => {
+ const { query } = router;
+
+ router.push({
+ pathname: router.pathname,
+ query: { ...query, peekIssue: issue.id },
+ });
+ };
+
const handleCopyText = () => {
const originURL =
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
@@ -183,15 +188,6 @@ export const SingleSpreadsheetIssue: React.FC = ({
return (
<>
- handleDeleteIssue(issue)}
- handleUpdateIssue={async (formData) => partialUpdateIssue(formData, issue)}
- issue={issue}
- isOpen={issuePeekOverview}
- onClose={() => setIssuePeekOverview(false)}
- workspaceSlug={workspaceSlug?.toString() ?? ""}
- readOnly={isNotAllowed}
- />
= ({
setIssuePeekOverview(true)}
+ onClick={openPeekOverview}
>
{issue.name}
@@ -327,7 +323,7 @@ export const SingleSpreadsheetIssue: React.FC
= ({
)}
{properties.labels && (
-
+
)}
diff --git a/apps/app/components/core/views/spreadsheet-view/spreadsheet-columns.tsx b/web/components/core/views/spreadsheet-view/spreadsheet-columns.tsx
similarity index 98%
rename from apps/app/components/core/views/spreadsheet-view/spreadsheet-columns.tsx
rename to web/components/core/views/spreadsheet-view/spreadsheet-columns.tsx
index 181ea93a7..f52f1ab38 100644
--- a/apps/app/components/core/views/spreadsheet-view/spreadsheet-columns.tsx
+++ b/web/components/core/views/spreadsheet-view/spreadsheet-columns.tsx
@@ -22,10 +22,10 @@ export const SpreadsheetColumns: React.FC = ({ columnData, gridTemplateCo
const { storedValue: activeSortingProperty, setValue: setActiveSortingProperty } =
useLocalStorage("spreadsheetViewActiveSortingProperty", "");
- const { orderBy, setOrderBy } = useSpreadsheetIssuesView();
+ const { displayFilters, setDisplayFilters } = useSpreadsheetIssuesView();
const handleOrderBy = (order: TIssueOrderByOptions, itemKey: string) => {
- setOrderBy(order);
+ setDisplayFilters({ order_by: order });
setSelectedMenuItem(`${order}_${itemKey}`);
setActiveSortingProperty(order === "-created_at" ? "" : itemKey);
};
@@ -239,7 +239,7 @@ export const SpreadsheetColumns: React.FC = ({ columnData, gridTemplateCo
{selectedMenuItem &&
selectedMenuItem !== "" &&
- orderBy !== "-created_at" &&
+ displayFilters?.order_by !== "-created_at" &&
selectedMenuItem.includes(col.propertyName) && (
void;
+ openIssuesListModal?: (() => void) | null;
+ disableUserActions: boolean;
+ user: ICurrentUserResponse | undefined;
+ userAuth: UserAuth;
+};
+
+export const SpreadsheetView: React.FC = ({
+ handleIssueAction,
+ openIssuesListModal,
+ disableUserActions,
+ user,
+ userAuth,
+}) => {
+ const [expandedIssues, setExpandedIssues] = useState