From 7aaf840fb11064b912fd044f5a3358e5b4dae91e Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 21 Nov 2023 15:46:41 +0530 Subject: [PATCH 01/14] refactor: command k modal (#2803) * refactor: command palette file structure * fix: identifier search --- .../command-palette/actions/help-actions.tsx | 83 +++ .../command-palette/actions/index.ts | 6 + .../actions/issue-actions/actions-list.tsx | 166 ++++++ .../actions/issue-actions/change-assignee.tsx | 79 +++ .../actions/issue-actions/change-priority.tsx | 56 ++ .../actions/issue-actions/change-state.tsx | 65 +++ .../actions/issue-actions/index.ts | 4 + .../actions/project-actions.tsx | 83 +++ .../actions/search-results.tsx | 49 ++ .../theme-actions.tsx} | 21 +- .../actions/workspace-settings-actions.tsx | 61 +++ .../command-palette/command-modal.tsx | 499 +++--------------- .../command-palette/command-pallette.tsx | 14 +- web/components/command-palette/helpers.tsx | 21 +- web/components/command-palette/index.ts | 3 +- .../issue/change-issue-assignee.tsx | 111 ---- .../issue/change-issue-priority.tsx | 78 --- .../issue/change-issue-state.tsx | 93 ---- web/components/command-palette/issue/index.ts | 3 - 19 files changed, 741 insertions(+), 754 deletions(-) create mode 100644 web/components/command-palette/actions/help-actions.tsx create mode 100644 web/components/command-palette/actions/index.ts create mode 100644 web/components/command-palette/actions/issue-actions/actions-list.tsx create mode 100644 web/components/command-palette/actions/issue-actions/change-assignee.tsx create mode 100644 web/components/command-palette/actions/issue-actions/change-priority.tsx create mode 100644 web/components/command-palette/actions/issue-actions/change-state.tsx create mode 100644 web/components/command-palette/actions/issue-actions/index.ts create mode 100644 web/components/command-palette/actions/project-actions.tsx create mode 100644 web/components/command-palette/actions/search-results.tsx rename web/components/command-palette/{change-interface-theme.tsx => actions/theme-actions.tsx} (74%) create mode 100644 web/components/command-palette/actions/workspace-settings-actions.tsx delete mode 100644 web/components/command-palette/issue/change-issue-assignee.tsx delete mode 100644 web/components/command-palette/issue/change-issue-priority.tsx delete mode 100644 web/components/command-palette/issue/change-issue-state.tsx delete mode 100644 web/components/command-palette/issue/index.ts diff --git a/web/components/command-palette/actions/help-actions.tsx b/web/components/command-palette/actions/help-actions.tsx new file mode 100644 index 000000000..859a6d23a --- /dev/null +++ b/web/components/command-palette/actions/help-actions.tsx @@ -0,0 +1,83 @@ +import { Command } from "cmdk"; +import { FileText, GithubIcon, MessageSquare, Rocket } from "lucide-react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// ui +import { DiscordIcon } from "@plane/ui"; + +type Props = { + closePalette: () => void; +}; + +export const CommandPaletteHelpActions: React.FC = (props) => { + const { closePalette } = props; + + const { + commandPalette: { toggleShortcutModal }, + } = useMobxStore(); + + return ( + + { + closePalette(); + toggleShortcutModal(true); + }} + className="focus:outline-none" + > +
+ + Open keyboard shortcuts +
+
+ { + closePalette(); + window.open("https://docs.plane.so/", "_blank"); + }} + className="focus:outline-none" + > +
+ + Open Plane documentation +
+
+ { + closePalette(); + window.open("https://discord.com/invite/A92xrEGCge", "_blank"); + }} + className="focus:outline-none" + > +
+ + Join our Discord +
+
+ { + closePalette(); + window.open("https://github.com/makeplane/plane/issues/new/choose", "_blank"); + }} + className="focus:outline-none" + > +
+ + Report a bug +
+
+ { + closePalette(); + (window as any)?.$crisp.push(["do", "chat:open"]); + }} + className="focus:outline-none" + > +
+ + Chat with us +
+
+
+ ); +}; diff --git a/web/components/command-palette/actions/index.ts b/web/components/command-palette/actions/index.ts new file mode 100644 index 000000000..7c3af470e --- /dev/null +++ b/web/components/command-palette/actions/index.ts @@ -0,0 +1,6 @@ +export * from "./issue-actions"; +export * from "./help-actions"; +export * from "./project-actions"; +export * from "./search-results"; +export * from "./theme-actions"; +export * from "./workspace-settings-actions"; diff --git a/web/components/command-palette/actions/issue-actions/actions-list.tsx b/web/components/command-palette/actions/issue-actions/actions-list.tsx new file mode 100644 index 000000000..73a021cf8 --- /dev/null +++ b/web/components/command-palette/actions/issue-actions/actions-list.tsx @@ -0,0 +1,166 @@ +import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; +import { Command } from "cmdk"; +import { LinkIcon, Signal, Trash2, UserMinus2, UserPlus2 } from "lucide-react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import useToast from "hooks/use-toast"; +// ui +import { DoubleCircleIcon, UserGroupIcon } from "@plane/ui"; +// helpers +import { copyTextToClipboard } from "helpers/string.helper"; +// types +import { IIssue } from "types"; + +type Props = { + closePalette: () => void; + issueDetails: IIssue | undefined; + pages: string[]; + setPages: (pages: string[]) => void; + setPlaceholder: (placeholder: string) => void; + setSearchTerm: (searchTerm: string) => void; +}; + +export const CommandPaletteIssueActions: React.FC = observer((props) => { + const { closePalette, issueDetails, pages, setPages, setPlaceholder, setSearchTerm } = props; + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { + commandPalette: { toggleCommandPaletteModal, toggleDeleteIssueModal }, + issueDetail: { updateIssue }, + user: { currentUser }, + } = useMobxStore(); + + const { setToastAlert } = useToast(); + + const handleUpdateIssue = async (formData: Partial) => { + if (!workspaceSlug || !projectId || !issueDetails) return; + + const payload = { ...formData }; + await updateIssue(workspaceSlug.toString(), projectId.toString(), issueDetails.id, payload).catch((e) => { + console.error(e); + }); + }; + + const handleIssueAssignees = (assignee: string) => { + if (!issueDetails || !assignee) return; + + closePalette(); + const updatedAssignees = issueDetails.assignees ?? []; + + if (updatedAssignees.includes(assignee)) updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1); + else updatedAssignees.push(assignee); + + handleUpdateIssue({ assignees: updatedAssignees }); + }; + + const deleteIssue = () => { + toggleCommandPaletteModal(false); + toggleDeleteIssueModal(true); + }; + + const copyIssueUrlToClipboard = () => { + if (!router.query.issueId) return; + + const url = new URL(window.location.href); + copyTextToClipboard(url.href) + .then(() => { + setToastAlert({ + type: "success", + title: "Copied to clipboard", + }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Some error occurred", + }); + }); + }; + + return ( + + { + setPlaceholder("Change state..."); + setSearchTerm(""); + setPages([...pages, "change-issue-state"]); + }} + className="focus:outline-none" + > +
+ + Change state... +
+
+ { + setPlaceholder("Change priority..."); + setSearchTerm(""); + setPages([...pages, "change-issue-priority"]); + }} + className="focus:outline-none" + > +
+ + Change priority... +
+
+ { + setPlaceholder("Assign to..."); + setSearchTerm(""); + setPages([...pages, "change-issue-assignee"]); + }} + className="focus:outline-none" + > +
+ + Assign to... +
+
+ { + handleIssueAssignees(currentUser?.id ?? ""); + setSearchTerm(""); + }} + className="focus:outline-none" + > +
+ {issueDetails?.assignees.includes(currentUser?.id ?? "") ? ( + <> + + Un-assign from me + + ) : ( + <> + + Assign to me + + )} +
+
+ +
+ + Delete issue +
+
+ { + closePalette(); + copyIssueUrlToClipboard(); + }} + className="focus:outline-none" + > +
+ + Copy issue URL +
+
+
+ ); +}); diff --git a/web/components/command-palette/actions/issue-actions/change-assignee.tsx b/web/components/command-palette/actions/issue-actions/change-assignee.tsx new file mode 100644 index 000000000..4b7ee6b99 --- /dev/null +++ b/web/components/command-palette/actions/issue-actions/change-assignee.tsx @@ -0,0 +1,79 @@ +import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; +import { Command } from "cmdk"; +import { Check } from "lucide-react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// ui +import { Avatar } from "@plane/ui"; +// types +import { IIssue } from "types"; + +type Props = { + closePalette: () => void; + issue: IIssue; +}; + +export const ChangeIssueAssignee: React.FC = observer((props) => { + const { closePalette, issue } = props; + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + // store + const { + issueDetail: { updateIssue }, + projectMember: { projectMembers }, + } = useMobxStore(); + + const options = + projectMembers?.map(({ member }) => ({ + value: member.id, + query: member.display_name, + content: ( + <> +
+ + {member.display_name} +
+ {issue.assignees.includes(member.id) && ( +
+ +
+ )} + + ), + })) ?? []; + + const handleUpdateIssue = async (formData: Partial) => { + if (!workspaceSlug || !projectId || !issue) return; + + const payload = { ...formData }; + await updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, payload).catch((e) => { + console.error(e); + }); + }; + + const handleIssueAssignees = (assignee: string) => { + const updatedAssignees = issue.assignees ?? []; + + if (updatedAssignees.includes(assignee)) updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1); + else updatedAssignees.push(assignee); + + handleUpdateIssue({ assignees: updatedAssignees }); + closePalette(); + }; + + return ( + <> + {options.map((option: any) => ( + handleIssueAssignees(option.value)} + className="focus:outline-none" + > + {option.content} + + ))} + + ); +}); diff --git a/web/components/command-palette/actions/issue-actions/change-priority.tsx b/web/components/command-palette/actions/issue-actions/change-priority.tsx new file mode 100644 index 000000000..bda152c33 --- /dev/null +++ b/web/components/command-palette/actions/issue-actions/change-priority.tsx @@ -0,0 +1,56 @@ +import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; +import { Command } from "cmdk"; +import { Check } from "lucide-react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// ui +import { PriorityIcon } from "@plane/ui"; +// types +import { IIssue, TIssuePriorities } from "types"; +// constants +import { PRIORITIES } from "constants/project"; + +type Props = { + closePalette: () => void; + issue: IIssue; +}; + +export const ChangeIssuePriority: React.FC = observer((props) => { + const { closePalette, issue } = props; + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { + issueDetail: { updateIssue }, + } = useMobxStore(); + + const submitChanges = async (formData: Partial) => { + if (!workspaceSlug || !projectId || !issue) return; + + const payload = { ...formData }; + await updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, payload).catch((e) => { + console.error(e); + }); + }; + + const handleIssueState = (priority: TIssuePriorities) => { + submitChanges({ priority }); + closePalette(); + }; + + return ( + <> + {PRIORITIES.map((priority) => ( + handleIssueState(priority)} className="focus:outline-none"> +
+ + {priority ?? "None"} +
+
{priority === issue.priority && }
+
+ ))} + + ); +}); diff --git a/web/components/command-palette/actions/issue-actions/change-state.tsx b/web/components/command-palette/actions/issue-actions/change-state.tsx new file mode 100644 index 000000000..5b147e499 --- /dev/null +++ b/web/components/command-palette/actions/issue-actions/change-state.tsx @@ -0,0 +1,65 @@ +import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// cmdk +import { Command } from "cmdk"; +// ui +import { Spinner, StateGroupIcon } from "@plane/ui"; +// icons +import { Check } from "lucide-react"; +// types +import { IIssue } from "types"; + +type Props = { + closePalette: () => void; + issue: IIssue; +}; + +export const ChangeIssueState: React.FC = observer((props) => { + const { closePalette, issue } = props; + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { + projectState: { projectStates }, + issueDetail: { updateIssue }, + } = useMobxStore(); + + const submitChanges = async (formData: Partial) => { + if (!workspaceSlug || !projectId || !issue) return; + + const payload = { ...formData }; + await updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, payload).catch((e) => { + console.error(e); + }); + }; + + const handleIssueState = (stateId: string) => { + submitChanges({ state: stateId }); + closePalette(); + }; + + return ( + <> + {projectStates ? ( + projectStates.length > 0 ? ( + projectStates.map((state) => ( + handleIssueState(state.id)} className="focus:outline-none"> +
+ +

{state.name}

+
+
{state.id === issue.state && }
+
+ )) + ) : ( +
No states found
+ ) + ) : ( + + )} + + ); +}); diff --git a/web/components/command-palette/actions/issue-actions/index.ts b/web/components/command-palette/actions/issue-actions/index.ts new file mode 100644 index 000000000..305107d60 --- /dev/null +++ b/web/components/command-palette/actions/issue-actions/index.ts @@ -0,0 +1,4 @@ +export * from "./actions-list"; +export * from "./change-state"; +export * from "./change-priority"; +export * from "./change-assignee"; diff --git a/web/components/command-palette/actions/project-actions.tsx b/web/components/command-palette/actions/project-actions.tsx new file mode 100644 index 000000000..5db9b2c4e --- /dev/null +++ b/web/components/command-palette/actions/project-actions.tsx @@ -0,0 +1,83 @@ +import { Command } from "cmdk"; +import { ContrastIcon, FileText } from "lucide-react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// ui +import { DiceIcon, PhotoFilterIcon } from "@plane/ui"; + +type Props = { + closePalette: () => void; +}; + +export const CommandPaletteProjectActions: React.FC = (props) => { + const { closePalette } = props; + + const { + commandPalette: { toggleCreateCycleModal, toggleCreateModuleModal, toggleCreatePageModal, toggleCreateViewModal }, + } = useMobxStore(); + + return ( + <> + + { + closePalette(); + toggleCreateCycleModal(true); + }} + className="focus:outline-none" + > +
+ + Create new cycle +
+ Q +
+
+ + { + closePalette(); + toggleCreateModuleModal(true); + }} + className="focus:outline-none" + > +
+ + Create new module +
+ M +
+
+ + { + closePalette(); + toggleCreateViewModal(true); + }} + className="focus:outline-none" + > +
+ + Create new view +
+ V +
+
+ + { + closePalette(); + toggleCreatePageModal(true); + }} + className="focus:outline-none" + > +
+ + Create new page +
+ D +
+
+ + ); +}; diff --git a/web/components/command-palette/actions/search-results.tsx b/web/components/command-palette/actions/search-results.tsx new file mode 100644 index 000000000..791c62656 --- /dev/null +++ b/web/components/command-palette/actions/search-results.tsx @@ -0,0 +1,49 @@ +import { useRouter } from "next/router"; +import { Command } from "cmdk"; +// helpers +import { commandGroups } from "components/command-palette"; +// types +import { IWorkspaceSearchResults } from "types"; + +type Props = { + closePalette: () => void; + results: IWorkspaceSearchResults; +}; + +export const CommandPaletteSearchResults: React.FC = (props) => { + const { closePalette, results } = props; + + const router = useRouter(); + + return ( + <> + {Object.keys(results.results).map((key) => { + const section = (results.results as any)[key]; + const currentSection = commandGroups[key]; + + if (section.length > 0) { + return ( + + {section.map((item: any) => ( + { + closePalette(); + router.push(currentSection.path(item)); + }} + value={`${key}-${item?.id}-${item.name}-${item.project__identifier ?? ""}-${item.sequence_id ?? ""}`} + className="focus:outline-none" + > +
+ {currentSection.icon} +

{currentSection.itemName(item)}

+
+
+ ))} +
+ ); + } + })} + + ); +}; diff --git a/web/components/command-palette/change-interface-theme.tsx b/web/components/command-palette/actions/theme-actions.tsx similarity index 74% rename from web/components/command-palette/change-interface-theme.tsx rename to web/components/command-palette/actions/theme-actions.tsx index 0b899f811..f7266a48a 100644 --- a/web/components/command-palette/change-interface-theme.tsx +++ b/web/components/command-palette/actions/theme-actions.tsx @@ -1,4 +1,4 @@ -import React, { FC, Dispatch, SetStateAction, useEffect, useState } from "react"; +import React, { FC, useEffect, useState } from "react"; import { Command } from "cmdk"; import { useTheme } from "next-themes"; import { Settings } from "lucide-react"; @@ -10,22 +10,25 @@ import { useMobxStore } from "lib/mobx/store-provider"; import { THEME_OPTIONS } from "constants/themes"; type Props = { - setIsPaletteOpen: Dispatch>; + closePalette: () => void; }; -export const ChangeInterfaceTheme: FC = observer((props) => { - const { setIsPaletteOpen } = props; - // store - const { user: userStore } = useMobxStore(); +export const CommandPaletteThemeActions: FC = observer((props) => { + const { closePalette } = props; // states const [mounted, setMounted] = useState(false); + // store + const { + user: { updateCurrentUserTheme }, + } = useMobxStore(); // hooks const { setTheme } = useTheme(); const { setToastAlert } = useToast(); - const updateUserTheme = (newTheme: string) => { + const updateUserTheme = async (newTheme: string) => { setTheme(newTheme); - return userStore.updateCurrentUserTheme(newTheme).catch(() => { + + return updateCurrentUserTheme(newTheme).catch(() => { setToastAlert({ title: "Failed to save user theme settings!", type: "error", @@ -47,7 +50,7 @@ export const ChangeInterfaceTheme: FC = observer((props) => { key={theme.value} onSelect={() => { updateUserTheme(theme.value); - setIsPaletteOpen(false); + closePalette(); }} className="focus:outline-none" > diff --git a/web/components/command-palette/actions/workspace-settings-actions.tsx b/web/components/command-palette/actions/workspace-settings-actions.tsx new file mode 100644 index 000000000..84e62593a --- /dev/null +++ b/web/components/command-palette/actions/workspace-settings-actions.tsx @@ -0,0 +1,61 @@ +import { useRouter } from "next/router"; +import { Command } from "cmdk"; +// icons +import { SettingIcon } from "components/icons"; + +type Props = { + closePalette: () => void; +}; + +export const CommandPaletteWorkspaceSettingsActions: React.FC = (props) => { + const { closePalette } = props; + + const router = useRouter(); + const { workspaceSlug } = router.query; + + const redirect = (path: string) => { + closePalette(); + router.push(path); + }; + + return ( + <> + redirect(`/${workspaceSlug}/settings`)} className="focus:outline-none"> +
+ + General +
+
+ redirect(`/${workspaceSlug}/settings/members`)} className="focus:outline-none"> +
+ + Members +
+
+ redirect(`/${workspaceSlug}/settings/billing`)} className="focus:outline-none"> +
+ + Billing and Plans +
+
+ redirect(`/${workspaceSlug}/settings/integrations`)} className="focus:outline-none"> +
+ + Integrations +
+
+ redirect(`/${workspaceSlug}/settings/imports`)} className="focus:outline-none"> +
+ + Import +
+
+ redirect(`/${workspaceSlug}/settings/exports`)} className="focus:outline-none"> +
+ + Export +
+
+ + ); +}; diff --git a/web/components/command-palette/command-modal.tsx b/web/components/command-palette/command-modal.tsx index bd066326d..f43032cf2 100644 --- a/web/components/command-palette/command-modal.tsx +++ b/web/components/command-palette/command-modal.tsx @@ -1,22 +1,10 @@ -import React, { useCallback, useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; -import useSWR, { mutate } from "swr"; +import useSWR from "swr"; import { Command } from "cmdk"; import { Dialog, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; -import { - FileText, - FolderPlus, - LinkIcon, - MessageSquare, - Rocket, - Search, - Settings, - Signal, - Trash2, - UserMinus2, - UserPlus2, -} from "lucide-react"; +import { FolderPlus, Search, Settings } from "lucide-react"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // services @@ -24,47 +12,29 @@ import { WorkspaceService } from "services/workspace.service"; import { IssueService } from "services/issue"; // hooks import useDebounce from "hooks/use-debounce"; -import useToast from "hooks/use-toast"; // components import { - ChangeInterfaceTheme, + CommandPaletteThemeActions, ChangeIssueAssignee, ChangeIssuePriority, ChangeIssueState, - commandGroups, + CommandPaletteHelpActions, + CommandPaletteIssueActions, + CommandPaletteProjectActions, + CommandPaletteWorkspaceSettingsActions, + CommandPaletteSearchResults, } from "components/command-palette"; -import { - ContrastIcon, - DiceIcon, - DoubleCircleIcon, - LayersIcon, - Loader, - PhotoFilterIcon, - ToggleSwitch, - Tooltip, - UserGroupIcon, -} from "@plane/ui"; -// icons -import { DiscordIcon, GithubIcon, SettingIcon } from "components/icons"; -// helpers -import { copyTextToClipboard } from "helpers/string.helper"; +import { LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui"; // types -import { IIssue, IWorkspaceSearchResults } from "types"; +import { IWorkspaceSearchResults } from "types"; // fetch-keys -import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys"; - -type Props = { - deleteIssue: () => void; - isPaletteOpen: boolean; - closePalette: () => void; -}; +import { ISSUE_DETAILS } from "constants/fetch-keys"; // services const workspaceService = new WorkspaceService(); const issueService = new IssueService(); -export const CommandModal: React.FC = observer((props) => { - const { deleteIssue, isPaletteOpen, closePalette } = props; +export const CommandModal: React.FC = observer(() => { // states const [placeholder, setPlaceholder] = useState("Type a command or search..."); const [resultsCount, setResultsCount] = useState(0); @@ -85,8 +55,14 @@ export const CommandModal: React.FC = observer((props) => { const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false); const [pages, setPages] = useState([]); - const { user: userStore, commandPalette: commandPaletteStore } = useMobxStore(); - const user = userStore.currentUser ?? undefined; + const { + commandPalette: { + isCommandPaletteOpen, + toggleCommandPaletteModal, + toggleCreateIssueModal, + toggleCreateProjectModal, + }, + } = useMobxStore(); // router const router = useRouter(); @@ -96,64 +72,16 @@ export const CommandModal: React.FC = observer((props) => { const debouncedSearchTerm = useDebounce(searchTerm, 500); - const { setToastAlert } = useToast(); - + // TODO: update this to mobx store const { data: issueDetails } = useSWR( - workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null, + workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId.toString()) : null, workspaceSlug && projectId && issueId - ? () => issueService.retrieve(workspaceSlug as string, projectId as string, issueId as string) + ? () => issueService.retrieve(workspaceSlug.toString(), projectId.toString(), issueId.toString()) : null ); - const updateIssue = useCallback( - async (formData: Partial) => { - if (!workspaceSlug || !projectId || !issueId) return; - - mutate( - ISSUE_DETAILS(issueId as string), - - (prevData) => { - if (!prevData) return prevData; - - return { - ...prevData, - ...formData, - }; - }, - false - ); - - const payload = { ...formData }; - await issueService - .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload) - .then(() => { - mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); - mutate(ISSUE_DETAILS(issueId as string)); - }) - .catch((e) => { - console.error(e); - }); - }, - [workspaceSlug, issueId, projectId] - ); - - const handleIssueAssignees = (assignee: string) => { - if (!issueDetails) return; - - closePalette(); - const updatedAssignees = issueDetails.assignees ?? []; - - if (updatedAssignees.includes(assignee)) { - updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1); - } else { - updatedAssignees.push(assignee); - } - updateIssue({ assignees: updatedAssignees }); - }; - - const redirect = (path: string) => { - closePalette(); - router.push(path); + const closePalette = () => { + toggleCommandPaletteModal(false); }; const createNewWorkspace = () => { @@ -161,25 +89,6 @@ export const CommandModal: React.FC = observer((props) => { router.push("/create-workspace"); }; - const copyIssueUrlToClipboard = useCallback(() => { - if (!router.query.issueId) return; - - const url = new URL(window.location.href); - copyTextToClipboard(url.href) - .then(() => { - setToastAlert({ - type: "success", - title: "Copied to clipboard", - }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Some error occurred", - }); - }); - }, [router, setToastAlert]); - useEffect( () => { if (!workspaceSlug) return; @@ -189,7 +98,7 @@ export const CommandModal: React.FC = observer((props) => { if (debouncedSearchTerm) { setIsSearching(true); workspaceService - .searchWorkspace(workspaceSlug as string, { + .searchWorkspace(workspaceSlug.toString(), { ...(projectId ? { project_id: projectId.toString() } : {}), search: debouncedSearchTerm, workspace_search: !projectId ? true : isWorkspaceLevel, @@ -225,16 +134,8 @@ export const CommandModal: React.FC = observer((props) => { [debouncedSearchTerm, isWorkspaceLevel, projectId, workspaceSlug] // Only call effect if debounced search term changes ); - if (!user) return null; - return ( - { - setSearchTerm(""); - }} - as={React.Fragment} - > + setSearchTerm("")} as={React.Fragment}> closePalette()}> = observer((props) => { onKeyDown={(e) => { // when search is empty and page is undefined // when user tries to close the modal with esc - if (e.key === "Escape" && !page && !searchTerm) { - closePalette(); - } + if (e.key === "Escape" && !page && !searchTerm) closePalette(); + // Escape goes to previous page // Backspace goes to previous page when search is empty if (e.key === "Escape" || (e.key === "Backspace" && !searchTerm)) { @@ -318,9 +218,7 @@ export const CommandModal: React.FC = observer((props) => { className="w-full border-0 border-b border-custom-border-200 bg-transparent p-4 pl-11 text-custom-text-100 placeholder:text-custom-text-400 outline-none focus:ring-0 text-sm" placeholder={placeholder} value={searchTerm} - onValueChange={(e) => { - setSearchTerm(e); - }} + onValueChange={(e) => setSearchTerm(e)} autoFocus tabIndex={1} /> @@ -340,7 +238,7 @@ export const CommandModal: React.FC = observer((props) => { )} {!isLoading && resultsCount === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && ( -
No results found.
+
No results found.
)} {(isLoading || isSearching) && ( @@ -354,125 +252,28 @@ export const CommandModal: React.FC = observer((props) => { )} - {debouncedSearchTerm !== "" && - Object.keys(results.results).map((key) => { - const section = (results.results as any)[key]; - const currentSection = commandGroups[key]; - - if (section.length > 0) { - return ( - - {section.map((item: any) => ( - { - closePalette(); - router.push(currentSection.path(item)); - }} - value={`${key}-${item?.name}`} - className="focus:outline-none" - > -
- {currentSection.icon} -

{currentSection.itemName(item)}

-
-
- ))} -
- ); - } - })} + {debouncedSearchTerm !== "" && ( + + )} {!page && ( <> + {/* issue actions */} {issueId && ( - - { - closePalette(); - setPlaceholder("Change state..."); - setSearchTerm(""); - setPages([...pages, "change-issue-state"]); - }} - className="focus:outline-none" - > -
- - Change state... -
-
- { - setPlaceholder("Change priority..."); - setSearchTerm(""); - setPages([...pages, "change-issue-priority"]); - }} - className="focus:outline-none" - > -
- - Change priority... -
-
- { - setPlaceholder("Assign to..."); - setSearchTerm(""); - setPages([...pages, "change-issue-assignee"]); - }} - className="focus:outline-none" - > -
- - Assign to... -
-
- { - handleIssueAssignees(user.id); - setSearchTerm(""); - }} - className="focus:outline-none" - > -
- {issueDetails?.assignees.includes(user.id) ? ( - <> - - Un-assign from me - - ) : ( - <> - - Assign to me - - )} -
-
- -
- - Delete issue -
-
- { - closePalette(); - copyIssueUrlToClipboard(); - }} - className="focus:outline-none" - > -
- - Copy issue URL -
-
-
+ setPages(newPages)} + setPlaceholder={(newPlaceholder) => setPlaceholder(newPlaceholder)} + setSearchTerm={(newSearchTerm) => setSearchTerm(newSearchTerm)} + /> )} { closePalette(); - commandPaletteStore.toggleCreateIssueModal(true); + toggleCreateIssueModal(true); }} className="focus:bg-custom-background-80" > @@ -489,7 +290,7 @@ export const CommandModal: React.FC = observer((props) => { { closePalette(); - commandPaletteStore.toggleCreateProjectModal(true); + toggleCreateProjectModal(true); }} className="focus:outline-none" > @@ -502,70 +303,8 @@ export const CommandModal: React.FC = observer((props) => { )} - {projectId && ( - <> - - { - closePalette(); - commandPaletteStore.toggleCreateCycleModal(true); - }} - className="focus:outline-none" - > -
- - Create new cycle -
- Q -
-
- - { - closePalette(); - commandPaletteStore.toggleCreateModuleModal(true); - }} - className="focus:outline-none" - > -
- - Create new module -
- M -
-
- - { - closePalette(); - commandPaletteStore.toggleCreateViewModal(true); - }} - className="focus:outline-none" - > -
- - Create new view -
- V -
-
- - { - closePalette(); - commandPaletteStore.toggleCreatePageModal(true); - }} - className="focus:outline-none" - > -
- - Create new page -
- D -
-
- - )} + {/* project actions */} + {projectId && } = observer((props) => { - - { - closePalette(); - commandPaletteStore.toggleShortcutModal(true); - }} - className="focus:outline-none" - > -
- - Open keyboard shortcuts -
-
- { - closePalette(); - window.open("https://docs.plane.so/", "_blank"); - }} - className="focus:outline-none" - > -
- - Open Plane documentation -
-
- { - closePalette(); - window.open("https://discord.com/invite/A92xrEGCge", "_blank"); - }} - className="focus:outline-none" - > -
- - Join our Discord -
-
- { - closePalette(); - window.open("https://github.com/makeplane/plane/issues/new/choose", "_blank"); - }} - className="focus:outline-none" - > -
- - Report a bug -
-
- { - closePalette(); - (window as any).$crisp.push(["do", "chat:open"]); - }} - className="focus:outline-none" - > -
- - Chat with us -
-
-
+ + {/* help options */} + )} + {/* workspace settings actions */} {page === "settings" && workspaceSlug && ( - <> - redirect(`/${workspaceSlug}/settings`)} - className="focus:outline-none" - > -
- - General -
-
- redirect(`/${workspaceSlug}/settings/members`)} - className="focus:outline-none" - > -
- - Members -
-
- redirect(`/${workspaceSlug}/settings/billing`)} - className="focus:outline-none" - > -
- - Billing and Plans -
-
- redirect(`/${workspaceSlug}/settings/integrations`)} - className="focus:outline-none" - > -
- - Integrations -
-
- redirect(`/${workspaceSlug}/settings/imports`)} - className="focus:outline-none" - > -
- - Import -
-
- redirect(`/${workspaceSlug}/settings/exports`)} - className="focus:outline-none" - > -
- - Export -
-
- + )} + + {/* issue details page actions */} {page === "change-issue-state" && issueDetails && ( - + )} {page === "change-issue-priority" && issueDetails && ( - + )} {page === "change-issue-assignee" && issueDetails && ( - + + )} + + {/* theme actions */} + {page === "change-interface-theme" && ( + { + closePalette(); + setPages((pages) => pages.slice(0, -1)); + }} + /> )} - {page === "change-interface-theme" && } diff --git a/web/components/command-palette/command-pallette.tsx b/web/components/command-palette/command-pallette.tsx index 0ca293475..7708ff926 100644 --- a/web/components/command-palette/command-pallette.tsx +++ b/web/components/command-palette/command-pallette.tsx @@ -32,7 +32,6 @@ export const CommandPalette: FC = observer(() => { // store const { commandPalette, theme: themeStore } = useMobxStore(); const { - isCommandPaletteOpen, toggleCommandPaletteModal, isCreateIssueModalOpen, toggleCreateIssueModal, @@ -156,11 +155,6 @@ export const CommandPalette: FC = observer(() => { if (!user) return null; - const deleteIssue = () => { - toggleCommandPaletteModal(false); - toggleDeleteIssueModal(true); - }; - return ( <> { }} user={user} /> - { - toggleCommandPaletteModal(false); - }} - /> + ); }); diff --git a/web/components/command-palette/helpers.tsx b/web/components/command-palette/helpers.tsx index 90511f907..cfa21cede 100644 --- a/web/components/command-palette/helpers.tsx +++ b/web/components/command-palette/helpers.tsx @@ -20,9 +20,7 @@ export const commandGroups: { icon: , itemName: (cycle: IWorkspaceDefaultSearchResult) => (
- {cycle.project__identifier} - {"- "} - {cycle.name} + {cycle.project__identifier} {cycle.name}
), path: (cycle: IWorkspaceDefaultSearchResult) => @@ -33,8 +31,9 @@ export const commandGroups: { icon: , itemName: (issue: IWorkspaceIssueSearchResult) => (
- {issue.project__identifier} - {"- "} + + {issue.project__identifier}-{issue.sequence_id} + {" "} {issue.name}
), @@ -46,9 +45,7 @@ export const commandGroups: { icon: , itemName: (view: IWorkspaceDefaultSearchResult) => (
- {view.project__identifier} - {"- "} - {view.name} + {view.project__identifier} {view.name}
), path: (view: IWorkspaceDefaultSearchResult) => @@ -59,9 +56,7 @@ export const commandGroups: { icon: , itemName: (module: IWorkspaceDefaultSearchResult) => (
- {module.project__identifier} - {"- "} - {module.name} + {module.project__identifier} {module.name}
), path: (module: IWorkspaceDefaultSearchResult) => @@ -72,9 +67,7 @@ export const commandGroups: { icon: , itemName: (page: IWorkspaceDefaultSearchResult) => (
- {page.project__identifier} - {"- "} - {page.name} + {page.project__identifier} {page.name}
), path: (page: IWorkspaceDefaultSearchResult) => diff --git a/web/components/command-palette/index.ts b/web/components/command-palette/index.ts index a1230ca32..1fac3f134 100644 --- a/web/components/command-palette/index.ts +++ b/web/components/command-palette/index.ts @@ -1,5 +1,4 @@ -export * from "./issue"; -export * from "./change-interface-theme"; +export * from "./actions"; export * from "./command-modal"; export * from "./command-pallette"; export * from "./helpers"; diff --git a/web/components/command-palette/issue/change-issue-assignee.tsx b/web/components/command-palette/issue/change-issue-assignee.tsx deleted file mode 100644 index 1693a6b94..000000000 --- a/web/components/command-palette/issue/change-issue-assignee.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { Dispatch, SetStateAction, useCallback, FC } from "react"; -import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; -import { mutate } from "swr"; -import { Command } from "cmdk"; -import { Check } from "lucide-react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; -// services -import { IssueService } from "services/issue"; -// ui -import { Avatar } from "@plane/ui"; -// types -import { IUser, IIssue } from "types"; -// constants -import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys"; - -type Props = { - setIsPaletteOpen: Dispatch>; - issue: IIssue; - user: IUser | undefined; -}; - -// services -const issueService = new IssueService(); - -export const ChangeIssueAssignee: FC = observer((props) => { - const { setIsPaletteOpen, issue } = props; - // router - const router = useRouter(); - const { workspaceSlug, projectId, issueId } = router.query; - // store - const { - projectMember: { projectMembers }, - } = useMobxStore(); - - const options = - projectMembers?.map(({ member }) => ({ - value: member.id, - query: member.display_name, - content: ( - <> -
- - {member.display_name} -
- {issue.assignees.includes(member.id) && ( -
- -
- )} - - ), - })) ?? []; - - const updateIssue = useCallback( - async (formData: Partial) => { - if (!workspaceSlug || !projectId || !issueId) return; - - mutate( - ISSUE_DETAILS(issueId as string), - async (prevData) => { - if (!prevData) return prevData; - return { - ...prevData, - ...formData, - }; - }, - false - ); - - const payload = { ...formData }; - await issueService - .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload) - .then(() => { - mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); - }) - .catch((e) => { - console.error(e); - }); - }, - [workspaceSlug, issueId, projectId] - ); - - const handleIssueAssignees = (assignee: string) => { - const updatedAssignees = issue.assignees ?? []; - - if (updatedAssignees.includes(assignee)) { - updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1); - } else { - updatedAssignees.push(assignee); - } - - updateIssue({ assignees: updatedAssignees }); - setIsPaletteOpen(false); - }; - - return ( - <> - {options.map((option: any) => ( - handleIssueAssignees(option.value)} - className="focus:outline-none" - > - {option.content} - - ))} - - ); -}); diff --git a/web/components/command-palette/issue/change-issue-priority.tsx b/web/components/command-palette/issue/change-issue-priority.tsx deleted file mode 100644 index c5e4bfa2a..000000000 --- a/web/components/command-palette/issue/change-issue-priority.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React, { Dispatch, SetStateAction, useCallback } from "react"; -import { useRouter } from "next/router"; -import { mutate } from "swr"; -// cmdk -import { Command } from "cmdk"; -// services -import { IssueService } from "services/issue"; -// types -import { IIssue, IUser, TIssuePriorities } from "types"; -// constants -import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys"; -import { PRIORITIES } from "constants/project"; -// icons -import { PriorityIcon } from "@plane/ui"; -import { Check } from "lucide-react"; - -type Props = { - setIsPaletteOpen: Dispatch>; - issue: IIssue; - user: IUser; -}; - -// services -const issueService = new IssueService(); - -export const ChangeIssuePriority: React.FC = ({ setIsPaletteOpen, issue }) => { - const router = useRouter(); - const { workspaceSlug, projectId, issueId } = router.query; - - const submitChanges = useCallback( - async (formData: Partial) => { - if (!workspaceSlug || !projectId || !issueId) return; - - mutate( - ISSUE_DETAILS(issueId as string), - async (prevData) => { - if (!prevData) return prevData; - - return { - ...prevData, - ...formData, - }; - }, - false - ); - - const payload = { ...formData }; - await issueService - .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload) - .then(() => { - mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); - }) - .catch((e) => { - console.error(e); - }); - }, - [workspaceSlug, issueId, projectId] - ); - - const handleIssueState = (priority: TIssuePriorities) => { - submitChanges({ priority }); - setIsPaletteOpen(false); - }; - - return ( - <> - {PRIORITIES.map((priority) => ( - handleIssueState(priority)} className="focus:outline-none"> -
- - {priority ?? "None"} -
-
{priority === issue.priority && }
-
- ))} - - ); -}; diff --git a/web/components/command-palette/issue/change-issue-state.tsx b/web/components/command-palette/issue/change-issue-state.tsx deleted file mode 100644 index cbddfb688..000000000 --- a/web/components/command-palette/issue/change-issue-state.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import React, { Dispatch, SetStateAction, useCallback } from "react"; -import { useRouter } from "next/router"; -import useSWR, { mutate } from "swr"; -// cmdk -import { Command } from "cmdk"; -// services -import { IssueService } from "services/issue"; -import { ProjectStateService } from "services/project"; -// ui -import { Spinner, StateGroupIcon } from "@plane/ui"; -// icons -import { Check } from "lucide-react"; -// types -import { IUser, IIssue } from "types"; -// fetch keys -import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, STATES_LIST } from "constants/fetch-keys"; - -type Props = { - setIsPaletteOpen: Dispatch>; - issue: IIssue; - user: IUser | undefined; -}; - -// services -const issueService = new IssueService(); -const stateService = new ProjectStateService(); - -export const ChangeIssueState: React.FC = ({ setIsPaletteOpen, issue }) => { - const router = useRouter(); - const { workspaceSlug, projectId, issueId } = router.query; - - const { data: states, mutate: mutateStates } = useSWR( - workspaceSlug && projectId ? STATES_LIST(projectId as string) : null, - workspaceSlug && projectId ? () => stateService.getStates(workspaceSlug as string, projectId as string) : null - ); - - const submitChanges = useCallback( - async (formData: Partial) => { - if (!workspaceSlug || !projectId || !issueId) return; - - mutate( - ISSUE_DETAILS(issueId as string), - async (prevData) => { - if (!prevData) return prevData; - return { - ...prevData, - ...formData, - }; - }, - false - ); - - const payload = { ...formData }; - await issueService - .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload) - .then(() => { - mutateStates(); - mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); - }) - .catch((e) => { - console.error(e); - }); - }, - [workspaceSlug, issueId, projectId, mutateStates] - ); - - const handleIssueState = (stateId: string) => { - submitChanges({ state: stateId }); - setIsPaletteOpen(false); - }; - - return ( - <> - {states ? ( - states.length > 0 ? ( - states.map((state) => ( - handleIssueState(state.id)} className="focus:outline-none"> -
- -

{state.name}

-
-
{state.id === issue.state && }
-
- )) - ) : ( -
No states found
- ) - ) : ( - - )} - - ); -}; diff --git a/web/components/command-palette/issue/index.ts b/web/components/command-palette/issue/index.ts deleted file mode 100644 index 7d0bbd05d..000000000 --- a/web/components/command-palette/issue/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./change-issue-state"; -export * from "./change-issue-priority"; -export * from "./change-issue-assignee"; From 982eba0bd1c36a7eca0f198b9d52a59ec4eecc49 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 21 Nov 2023 15:47:34 +0530 Subject: [PATCH 02/14] fix: complete pages editor not clickable, recent pages calculation logic (#2820) * fix: whole editor not clickable * fix: recent pages calculation * chore: update older pages calculation logic in recent pages list * fix: archived pages computed function * chore: add type for older pages --- .../src/ui/components/page-renderer.tsx | 53 +++++++++------- .../pages/create-update-page-modal.tsx | 4 +- web/components/pages/page-form.tsx | 20 ++----- .../pages/pages-list/recent-pages-list.tsx | 3 +- .../projects/[projectId]/pages/[pageId].tsx | 2 +- .../projects/[projectId]/pages/index.tsx | 2 +- web/store/page.store.ts | 60 ++++++++++++++----- web/types/pages.d.ts | 1 + 8 files changed, 89 insertions(+), 56 deletions(-) diff --git a/packages/editor/document-editor/src/ui/components/page-renderer.tsx b/packages/editor/document-editor/src/ui/components/page-renderer.tsx index aca50f3ff..746d46e45 100644 --- a/packages/editor/document-editor/src/ui/components/page-renderer.tsx +++ b/packages/editor/document-editor/src/ui/components/page-renderer.tsx @@ -1,33 +1,46 @@ -import { EditorContainer, EditorContentWrapper } from "@plane/editor-core" -import { Editor } from "@tiptap/react" -import { DocumentDetails } from "../types/editor-types" +import { EditorContainer, EditorContentWrapper } from "@plane/editor-core"; +import { Editor } from "@tiptap/react"; +import { DocumentDetails } from "../types/editor-types"; interface IPageRenderer { - sidePeakVisible: boolean, - documentDetails: DocumentDetails , - editor: Editor, - editorClassNames: string, - editorContentCustomClassNames?: string + sidePeakVisible: boolean; + documentDetails: DocumentDetails; + editor: Editor; + editorClassNames: string; + editorContentCustomClassNames?: string; } -export const PageRenderer = ({ sidePeakVisible, documentDetails, editor, editorClassNames, editorContentCustomClassNames }: IPageRenderer) => { +export const PageRenderer = ({ + sidePeakVisible, + documentDetails, + editor, + editorClassNames, + editorContentCustomClassNames, +}: IPageRenderer) => { return ( -
+
-

{documentDetails.title}

+

+ {documentDetails.title} +

-
-
+
+
-
- +
+
- +
- ) -} + ); +}; diff --git a/web/components/pages/create-update-page-modal.tsx b/web/components/pages/create-update-page-modal.tsx index 62e3d244e..7816e8dce 100644 --- a/web/components/pages/create-update-page-modal.tsx +++ b/web/components/pages/create-update-page-modal.tsx @@ -38,7 +38,7 @@ export const CreateUpdatePageModal: FC = (props) => { const createProjectPage = async (payload: IPage) => { if (!workspaceSlug) return; - createPage(workspaceSlug.toString(), projectId, payload) + await createPage(workspaceSlug.toString(), projectId, payload) .then((res) => { router.push(`/${workspaceSlug}/projects/${projectId}/pages/${res.id}`); onClose(); @@ -67,7 +67,7 @@ export const CreateUpdatePageModal: FC = (props) => { const updateProjectPage = async (payload: IPage) => { if (!data || !workspaceSlug) return; - return updatePage(workspaceSlug.toString(), projectId, data.id, payload) + await updatePage(workspaceSlug.toString(), projectId, data.id, payload) .then((res) => { onClose(); setToastAlert({ diff --git a/web/components/pages/page-form.tsx b/web/components/pages/page-form.tsx index 18366286c..594390255 100644 --- a/web/components/pages/page-form.tsx +++ b/web/components/pages/page-form.tsx @@ -1,9 +1,9 @@ -import { useEffect } from "react"; import { Controller, useForm } from "react-hook-form"; // ui import { Button, Input, Tooltip } from "@plane/ui"; // types import { IPage } from "types"; +// constants import { PAGE_ACCESS_SPECIFIERS } from "constants/page"; type Props = { @@ -18,31 +18,21 @@ const defaultValues = { access: 0, }; -export const PageForm: React.FC = ({ handleFormSubmit, handleClose, data }) => { +export const PageForm: React.FC = (props) => { + const { handleFormSubmit, handleClose, data } = props; + const { formState: { errors, isSubmitting }, handleSubmit, control, - reset, } = useForm({ - defaultValues, + defaultValues: { ...defaultValues, ...data }, }); const handleCreateUpdatePage = async (formData: IPage) => { await handleFormSubmit(formData); - - reset({ - ...defaultValues, - }); }; - useEffect(() => { - reset({ - ...defaultValues, - ...data, - }); - }, [data, reset]); - return (
diff --git a/web/components/pages/pages-list/recent-pages-list.tsx b/web/components/pages/pages-list/recent-pages-list.tsx index 7122fa071..0f213db83 100644 --- a/web/components/pages/pages-list/recent-pages-list.tsx +++ b/web/components/pages/pages-list/recent-pages-list.tsx @@ -38,8 +38,9 @@ export const RecentPagesList: FC = observer(() => { <> {Object.keys(recentProjectPages).map((key) => { if (recentProjectPages[key].length === 0) return null; + return ( -
+

{replaceUnderscoreIfSnakeCase(key)}

diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx index 6906243ca..c725f2b89 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx @@ -235,7 +235,7 @@ const PageDetailsPage: NextPageWithLayout = () => { debouncedUpdatesEnabled={false} setIsSubmitting={setIsSubmitting} value={!value || value === "" ? "

" : value} - customClassName="tracking-tight self-center w-full max-w-full px-0" + customClassName="tracking-tight self-center px-0 h-full w-full" onChange={(_description_json: Object, description_html: string) => { onChange(description_html); setIsSubmitting("submitting"); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx index ce69f2c5e..edc2ef7d9 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx @@ -90,7 +90,7 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => { projectId={projectId.toString()} /> )} -
+

Pages

diff --git a/web/store/page.store.ts b/web/store/page.store.ts index 44feaceb4..c86cd3814 100644 --- a/web/store/page.store.ts +++ b/web/store/page.store.ts @@ -86,39 +86,67 @@ export class PageStore implements IPageStore { } get projectPages() { - if (!this.rootStore.project.projectId) return; - return this.pages?.[this.rootStore.project.projectId] || []; + const projectId = this.rootStore.project.projectId; + + if (!projectId || !this.pages[projectId]) return undefined; + + return this.pages?.[projectId] || []; } get recentProjectPages() { - if (!this.rootStore.project.projectId) return; - const data: IRecentPages = { today: [], yesterday: [], this_week: [] }; - data["today"] = this.pages[this.rootStore.project.projectId]?.filter((p) => isToday(new Date(p.created_at))) || []; - data["yesterday"] = - this.pages[this.rootStore.project.projectId]?.filter((p) => isYesterday(new Date(p.created_at))) || []; + const projectId = this.rootStore.project.projectId; + + if (!projectId) return undefined; + + if (!this.pages[projectId]) return undefined; + + const data: IRecentPages = { today: [], yesterday: [], this_week: [], older: [] }; + + data["today"] = this.pages[projectId]?.filter((p) => isToday(new Date(p.created_at))) || []; + data["yesterday"] = this.pages[projectId]?.filter((p) => isYesterday(new Date(p.created_at))) || []; data["this_week"] = - this.pages[this.rootStore.project.projectId]?.filter((p) => isThisWeek(new Date(p.created_at))) || []; + this.pages[projectId]?.filter( + (p) => + isThisWeek(new Date(p.created_at)) && !isToday(new Date(p.created_at)) && !isYesterday(new Date(p.created_at)) + ) || []; + data["older"] = + this.pages[projectId]?.filter( + (p) => !isThisWeek(new Date(p.created_at)) && !isYesterday(new Date(p.created_at)) + ) || []; + return data; } get favoriteProjectPages() { - if (!this.rootStore.project.projectId) return; - return this.pages[this.rootStore.project.projectId]?.filter((p) => p.is_favorite); + const projectId = this.rootStore.project.projectId; + + if (!projectId || !this.pages[projectId]) return undefined; + + return this.pages[projectId]?.filter((p) => p.is_favorite); } get privateProjectPages() { - if (!this.rootStore.project.projectId) return; - return this.pages[this.rootStore.project.projectId]?.filter((p) => p.access === 1); + const projectId = this.rootStore.project.projectId; + + if (!projectId || !this.pages[projectId]) return undefined; + + return this.pages[projectId]?.filter((p) => p.access === 1); } get sharedProjectPages() { - if (!this.rootStore.project.projectId) return; - return this.pages[this.rootStore.project.projectId]?.filter((p) => p.access === 0); + const projectId = this.rootStore.project.projectId; + + if (!projectId || !this.pages[projectId]) return undefined; + + return this.pages[projectId]?.filter((p) => p.access === 0); } get archivedProjectPages() { - if (!this.rootStore.project.projectId) return; - return this.archivedPages[this.rootStore.project.projectId]; + const projectId = this.rootStore.project.projectId; + + if (!projectId || !this.archivedPages[projectId]) return undefined; + + return this.archivedPages[projectId]; } addToFavorites = async (workspaceSlug: string, projectId: string, pageId: string) => { diff --git a/web/types/pages.d.ts b/web/types/pages.d.ts index c3c87f572..a1c241f6a 100644 --- a/web/types/pages.d.ts +++ b/web/types/pages.d.ts @@ -30,6 +30,7 @@ export interface IRecentPages { today: IPage[]; yesterday: IPage[]; this_week: IPage[]; + older: IPage[]; [key: string]: IPage[]; } From 561223ea71fd67d8207600b6b6e43e51d73b278b Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 21 Nov 2023 17:35:15 +0530 Subject: [PATCH 03/14] chore: update join project endpoint (#2821) --- web/components/project/join-project-modal.tsx | 16 ++++++++-------- web/services/project/project.service.ts | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/web/components/project/join-project-modal.tsx b/web/components/project/join-project-modal.tsx index 22fc2e9f5..67adc881d 100644 --- a/web/components/project/join-project-modal.tsx +++ b/web/components/project/join-project-modal.tsx @@ -21,22 +21,21 @@ export const JoinProjectModal: React.FC = (props) => { // states const [isJoiningLoading, setIsJoiningLoading] = useState(false); // store - const { project: projectStore } = useMobxStore(); + const { + project: { joinProject }, + } = useMobxStore(); // router const router = useRouter(); const handleJoin = () => { setIsJoiningLoading(true); - projectStore - .joinProject(workspaceSlug, [project.id]) + joinProject(workspaceSlug, [project.id]) .then(() => { - setIsJoiningLoading(false); - router.push(`/${workspaceSlug}/projects/${project.id}/issues`); handleClose(); }) - .catch(() => { + .finally(() => { setIsJoiningLoading(false); }); }; @@ -73,8 +72,9 @@ export const JoinProjectModal: React.FC = (props) => { Join Project?

- Are you sure you want to join the project {project?.name}? - Please click the 'Join Project' button below to continue. + Are you sure you want to join the project{" "} + {project?.name}? Please click the 'Join + Project' button below to continue.

diff --git a/web/services/project/project.service.ts b/web/services/project/project.service.ts index e4f1149f2..5fb0e4a10 100644 --- a/web/services/project/project.service.ts +++ b/web/services/project/project.service.ts @@ -69,7 +69,7 @@ export class ProjectService extends APIService { } async joinProject(workspaceSlug: string, project_ids: string[]): Promise { - return this.post(`/api/workspaces/${workspaceSlug}/projects/join/`, { project_ids }) + return this.post(`/api/users/me/workspaces/${workspaceSlug}/projects/invitations/`, { project_ids }) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; From d91b4e6fa1b22632c6d9a3438b2e9291d01503dd Mon Sep 17 00:00:00 2001 From: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com> Date: Tue, 21 Nov 2023 17:35:29 +0530 Subject: [PATCH 04/14] fix: bug fixes & UI improvements (#2819) * fix: profile setting fields border * fix: webhooks empty state UI * fix: cycle delete redirection from cycle detail * fix: integration access restriction --- web/components/api-token/empty-state.tsx | 2 +- web/components/cycles/delete-modal.tsx | 7 +++ .../integration/single-integration-card.tsx | 44 ++++++++++++++--- web/components/web-hooks/empty-webhooks.tsx | 46 +++++++++-------- .../[workspaceSlug]/me/profile/index.tsx | 5 +- .../settings/webhooks/index.tsx | 8 ++- web/public/empty-state/web-hook.svg | 49 +++++++++++++++++++ 7 files changed, 126 insertions(+), 35 deletions(-) create mode 100644 web/public/empty-state/web-hook.svg diff --git a/web/components/api-token/empty-state.tsx b/web/components/api-token/empty-state.tsx index 77618049f..fab8a9683 100644 --- a/web/components/api-token/empty-state.tsx +++ b/web/components/api-token/empty-state.tsx @@ -11,7 +11,7 @@ import emptyApiTokens from "public/empty-state/api-token.svg"; const ApiTokenEmptyState = () => { const router = useRouter(); return ( -
+
empty
No API Tokens
diff --git a/web/components/cycles/delete-modal.tsx b/web/components/cycles/delete-modal.tsx index f5024b3a2..aa28ea153 100644 --- a/web/components/cycles/delete-modal.tsx +++ b/web/components/cycles/delete-modal.tsx @@ -1,4 +1,6 @@ import { Fragment, useState } from "react"; +// next +import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; import { AlertTriangle } from "lucide-react"; @@ -27,6 +29,8 @@ export const CycleDeleteModal: React.FC = observer((props) => { const { setToastAlert } = useToast(); // states const [loader, setLoader] = useState(false); + const router = useRouter(); + const { cycleId } = router.query; const formSubmit = async () => { setLoader(true); @@ -38,6 +42,9 @@ export const CycleDeleteModal: React.FC = observer((props) => { title: "Success!", message: "Cycle deleted successfully.", }); + + if (cycleId) router.replace(`/${workspaceSlug}/projects/${projectId}/cycles`); + handleClose(); } catch (error) { setToastAlert({ diff --git a/web/components/integration/single-integration-card.tsx b/web/components/integration/single-integration-card.tsx index f5ff2b326..e0781b3f9 100644 --- a/web/components/integration/single-integration-card.tsx +++ b/web/components/integration/single-integration-card.tsx @@ -11,7 +11,7 @@ import { IntegrationService } from "services/integrations"; import useToast from "hooks/use-toast"; import useIntegrationPopup from "hooks/use-integration-popup"; // ui -import { Button, Loader } from "@plane/ui"; +import { Button, Loader, Tooltip } from "@plane/ui"; // icons import GithubLogo from "public/services/github.png"; import SlackLogo from "public/services/slack.png"; @@ -46,8 +46,11 @@ const integrationService = new IntegrationService(); export const SingleIntegrationCard: React.FC = observer(({ integration }) => { const { appConfig: { envConfig }, + user: { currentWorkspaceRole }, } = useMobxStore(); + const isUserAdmin = currentWorkspaceRole === 20; + const [deletingIntegration, setDeletingIntegration] = useState(false); const router = useRouter(); @@ -127,13 +130,40 @@ export const SingleIntegrationCard: React.FC = observer(({ integration }) {workspaceIntegrations ? ( isInstalled ? ( - + + + ) : ( - + + + ) ) : ( diff --git a/web/components/web-hooks/empty-webhooks.tsx b/web/components/web-hooks/empty-webhooks.tsx index d6ed6f2cd..d6a5d58de 100644 --- a/web/components/web-hooks/empty-webhooks.tsx +++ b/web/components/web-hooks/empty-webhooks.tsx @@ -1,28 +1,32 @@ -import { FC } from "react"; -import Link from "next/link"; -import { Button } from "@plane/ui"; +// next +import { useRouter } from "next/router"; import Image from "next/image"; -import EmptyWebhookLogo from "public/empty-state/issue.svg"; +// ui +import { Button } from "@plane/ui"; +// assets +import EmptyWebhook from "public/empty-state/web-hook.svg"; -interface IWebHookLists { - workspaceSlug: string; -} - -export const EmptyWebhooks: FC = (props) => { - const { workspaceSlug } = props; +export const EmptyWebhooks = () => { + const router = useRouter(); return ( -
-
- empty-webhook image - -
No Webhooks
-

Create webhooks to receive real-time updates and automate actions

- - - +
+
+ empty +
No Webhooks
+ { +

+ Create webhooks to receive real-time updates and automate actions +

+ } +
); diff --git a/web/pages/[workspaceSlug]/me/profile/index.tsx b/web/pages/[workspaceSlug]/me/profile/index.tsx index 33813fa52..a6ae7c784 100644 --- a/web/pages/[workspaceSlug]/me/profile/index.tsx +++ b/web/pages/[workspaceSlug]/me/profile/index.tsx @@ -303,7 +303,8 @@ const ProfilePage: NextPageWithLayout = () => { value={value} onChange={onChange} label={value ? value.toString() : "Select your role"} - buttonClassName={errors.role ? "border-red-500 bg-red-500/10" : ""} + buttonClassName={errors.role ? "border-red-500 bg-red-500/10" : "border-none"} + className="rounded-md border !border-custom-border-200" width="w-full" input > @@ -369,6 +370,8 @@ const ProfilePage: NextPageWithLayout = () => { options={timeZoneOptions} onChange={onChange} optionsClassName="w-full" + buttonClassName={"border-none"} + className="rounded-md border !border-custom-border-200" input /> )} diff --git a/web/pages/[workspaceSlug]/settings/webhooks/index.tsx b/web/pages/[workspaceSlug]/settings/webhooks/index.tsx index ce650e81c..f311741b8 100644 --- a/web/pages/[workspaceSlug]/settings/webhooks/index.tsx +++ b/web/pages/[workspaceSlug]/settings/webhooks/index.tsx @@ -31,7 +31,7 @@ const WebhooksPage: NextPage = observer(() => { return ( }> -
+
{loader ? (
@@ -41,10 +41,8 @@ const WebhooksPage: NextPage = observer(() => { {Object.keys(webhooks).length > 0 ? ( ) : ( -
-
- -
+
+
)} diff --git a/web/public/empty-state/web-hook.svg b/web/public/empty-state/web-hook.svg new file mode 100644 index 000000000..f8e32d3e5 --- /dev/null +++ b/web/public/empty-state/web-hook.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From fb1f65c2c13c3fcfd9af86e69d191d6729970549 Mon Sep 17 00:00:00 2001 From: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com> Date: Tue, 21 Nov 2023 17:37:17 +0530 Subject: [PATCH 05/14] fix: sidebar project section hover (#2818) * fix: sidebar project section hover * fix: icons alignment --- web/components/project/sidebar-list-item.tsx | 44 ++++++++++++++++---- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/web/components/project/sidebar-list-item.tsx b/web/components/project/sidebar-list-item.tsx index 480a99a92..48c9d0b9e 100644 --- a/web/components/project/sidebar-list-item.tsx +++ b/web/components/project/sidebar-list-item.tsx @@ -1,11 +1,22 @@ -import { useState } from "react"; +import { useRef, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/router"; import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd"; import { Disclosure, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; // icons -import { MoreVertical, PenSquare, LinkIcon, Star, FileText, Settings, Share2, LogOut, ChevronDown } from "lucide-react"; +import { + MoreVertical, + PenSquare, + LinkIcon, + Star, + FileText, + Settings, + Share2, + LogOut, + ChevronDown, + MoreHorizontal, +} from "lucide-react"; // hooks import useToast from "hooks/use-toast"; // helpers @@ -17,6 +28,7 @@ import { useMobxStore } from "lib/mobx/store-provider"; // components import { CustomMenu, Tooltip, ArchiveIcon, PhotoFilterIcon, DiceIcon, ContrastIcon, LayersIcon } from "@plane/ui"; import { LeaveProjectModal, PublishProjectModal } from "components/project"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; type Props = { project: IProject; @@ -72,12 +84,15 @@ export const ProjectSidebarListItem: React.FC = observer((props) => { // states const [leaveProjectModalOpen, setLeaveProjectModal] = useState(false); const [publishModalOpen, setPublishModal] = useState(false); + const [isMenuActive, setIsMenuActive] = useState(false); const isAdmin = project.member_role === 20; const isViewerOrGuest = project.member_role === 10 || project.member_role === 5; const isCollapsed = themeStore.sidebarCollapsed; + const actionSectionRef = useRef(null); + const handleAddToFavorites = () => { if (!workspaceSlug) return; @@ -110,6 +125,8 @@ export const ProjectSidebarListItem: React.FC = observer((props) => { setLeaveProjectModal(false); }; + useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false)); + return ( <> setPublishModal(false)} /> @@ -120,7 +137,7 @@ export const ProjectSidebarListItem: React.FC = observer((props) => {
{provided && ( = observer((props) => { type="button" className={`absolute top-1/2 -translate-y-1/2 -left-2.5 hidden rounded p-0.5 text-custom-sidebar-text-400 ${ isCollapsed ? "" : "group-hover:!flex" - } ${project.sort_order === null ? "opacity-60 cursor-not-allowed" : ""}`} + } ${project.sort_order === null ? "opacity-60 cursor-not-allowed" : ""} ${ + isMenuActive ? "!flex" : "" + }`} {...provided?.dragHandleProps} > @@ -169,9 +188,9 @@ export const ProjectSidebarListItem: React.FC = observer((props) => {
{!isCollapsed && (
+ } + className={`hidden group-hover:block flex-shrink-0 ${isMenuActive ? "!block" : ""}`} buttonClassName="!text-custom-sidebar-text-400 hover:text-custom-sidebar-text-400" ellipsis placement="bottom-start" From 4cf3e69e22e9bb2506b57d4b214f99910950af52 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Tue, 21 Nov 2023 17:52:19 +0530 Subject: [PATCH 06/14] chore: file asset update (#2816) * chore: endpoint to update file asset * chore: aws storage endpoint change --- apiserver/plane/app/views/asset.py | 12 +++++------- apiserver/plane/app/views/project.py | 4 ++-- apiserver/plane/bgtasks/export_task.py | 8 ++++---- apiserver/plane/bgtasks/exporter_expired_task.py | 4 ++-- .../plane/db/management/commands/create_bucket.py | 4 ++-- apiserver/plane/settings/common.py | 4 ++-- 6 files changed, 17 insertions(+), 19 deletions(-) diff --git a/apiserver/plane/app/views/asset.py b/apiserver/plane/app/views/asset.py index 31e002e15..dc2827080 100644 --- a/apiserver/plane/app/views/asset.py +++ b/apiserver/plane/app/views/asset.py @@ -1,7 +1,7 @@ # Third party imports from rest_framework import status from rest_framework.response import Response -from rest_framework.parsers import MultiPartParser, FormParser +from rest_framework.parsers import MultiPartParser, FormParser, JSONParser # Module imports from .base import BaseAPIView @@ -10,7 +10,7 @@ from plane.app.serializers import FileAssetSerializer class FileAssetEndpoint(BaseAPIView): - parser_classes = (MultiPartParser, FormParser) + parser_classes = (MultiPartParser, FormParser, JSONParser,) """ A viewset for viewing and editing task instances. @@ -25,7 +25,6 @@ class FileAssetEndpoint(BaseAPIView): else: return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK) - def post(self, request, slug): serializer = FileAssetSerializer(data=request.data) if serializer.is_valid(): @@ -34,12 +33,11 @@ class FileAssetEndpoint(BaseAPIView): serializer.save(workspace_id=workspace.id) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - - def delete(self, request, workspace_id, asset_key): + + def patch(self, request, workspace_id, asset_key): asset_key = str(workspace_id) + "/" + asset_key file_asset = FileAsset.objects.get(asset=asset_key) - file_asset.is_deleted = True + file_asset.is_deleted = request.data.get("is_deleted", file_asset.is_deleted) file_asset.save() return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/project.py b/apiserver/plane/app/views/project.py index 727aa06ba..2d616e5a6 100644 --- a/apiserver/plane/app/views/project.py +++ b/apiserver/plane/app/views/project.py @@ -975,7 +975,7 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView): aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, ) params = { - "Bucket": settings.AWS_S3_BUCKET_NAME, + "Bucket": settings.AWS_STORAGE_BUCKET_NAME, "Prefix": "static/project-cover/", } @@ -987,7 +987,7 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView): "/" ): # This line ensures we're only getting files, not "sub-folders" files.append( - f"https://{settings.AWS_S3_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}" + f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}" ) return Response(files, status=status.HTTP_200_OK) diff --git a/apiserver/plane/bgtasks/export_task.py b/apiserver/plane/bgtasks/export_task.py index a49f8bb86..e895b859d 100644 --- a/apiserver/plane/bgtasks/export_task.py +++ b/apiserver/plane/bgtasks/export_task.py @@ -81,13 +81,13 @@ def upload_to_s3(zip_file, workspace_id, token_id, slug): ) s3.upload_fileobj( zip_file, - settings.AWS_S3_BUCKET_NAME, + settings.AWS_STORAGE_BUCKET_NAME, file_name, ExtraArgs={"ACL": "public-read", "ContentType": "application/zip"}, ) presigned_url = s3.generate_presigned_url( "get_object", - Params={"Bucket": settings.AWS_S3_BUCKET_NAME, "Key": file_name}, + Params={"Bucket": settings.AWS_STORAGE_BUCKET_NAME, "Key": file_name}, ExpiresIn=expires_in, ) # Create the new url with updated domain and protocol @@ -105,14 +105,14 @@ def upload_to_s3(zip_file, workspace_id, token_id, slug): ) s3.upload_fileobj( zip_file, - settings.AWS_S3_BUCKET_NAME, + settings.AWS_STORAGE_BUCKET_NAME, file_name, ExtraArgs={"ACL": "public-read", "ContentType": "application/zip"}, ) presigned_url = s3.generate_presigned_url( "get_object", - Params={"Bucket": settings.AWS_S3_BUCKET_NAME, "Key": file_name}, + Params={"Bucket": settings.AWS_STORAGE_BUCKET_NAME, "Key": file_name}, ExpiresIn=expires_in, ) diff --git a/apiserver/plane/bgtasks/exporter_expired_task.py b/apiserver/plane/bgtasks/exporter_expired_task.py index aef4408d4..30b638c84 100644 --- a/apiserver/plane/bgtasks/exporter_expired_task.py +++ b/apiserver/plane/bgtasks/exporter_expired_task.py @@ -42,8 +42,8 @@ def delete_old_s3_link(): # Delete object from S3 if file_name: if settings.USE_MINIO: - s3.delete_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key=file_name) + s3.delete_object(Bucket=settings.AWS_STORAGE_BUCKET_NAME, Key=file_name) else: - s3.delete_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key=file_name) + s3.delete_object(Bucket=settings.AWS_STORAGE_BUCKET_NAME, Key=file_name) ExporterHistory.objects.filter(id=exporter_id).update(url=None) diff --git a/apiserver/plane/db/management/commands/create_bucket.py b/apiserver/plane/db/management/commands/create_bucket.py index fbda34f77..054523bf9 100644 --- a/apiserver/plane/db/management/commands/create_bucket.py +++ b/apiserver/plane/db/management/commands/create_bucket.py @@ -40,7 +40,7 @@ class Command(BaseCommand): ) # Create an S3 client using the session s3_client = session.client('s3', endpoint_url=settings.AWS_S3_ENDPOINT_URL) - bucket_name = settings.AWS_S3_BUCKET_NAME + bucket_name = settings.AWS_STORAGE_BUCKET_NAME self.stdout.write(self.style.NOTICE("Checking bucket...")) @@ -50,7 +50,7 @@ class Command(BaseCommand): self.set_bucket_public_policy(s3_client, bucket_name) except ClientError as e: error_code = int(e.response['Error']['Code']) - bucket_name = settings.AWS_S3_BUCKET_NAME + bucket_name = settings.AWS_STORAGE_BUCKET_NAME if error_code == 404: # Bucket does not exist, create it self.stdout.write(self.style.WARNING(f"Bucket '{bucket_name}' does not exist. Creating bucket...")) diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 4fa761f06..c76ec1340 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -224,7 +224,7 @@ STORAGES["default"] = { } AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key") AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "secret-key") -AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads") +AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads") AWS_REGION = os.environ.get("AWS_REGION", "") AWS_DEFAULT_ACL = "public-read" AWS_QUERYSTRING_AUTH = False @@ -234,7 +234,7 @@ AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", None) or os.environ. ) if AWS_S3_ENDPOINT_URL: parsed_url = urlparse(os.environ.get("WEB_URL", "http://localhost")) - AWS_S3_CUSTOM_DOMAIN = f"{parsed_url.netloc}/{AWS_S3_BUCKET_NAME}" + AWS_S3_CUSTOM_DOMAIN = f"{parsed_url.netloc}/{AWS_STORAGE_BUCKET_NAME}" AWS_S3_URL_PROTOCOL = f"{parsed_url.scheme}:" From c8c89007c03e8ea4a880161325dbc9be45b26cbc Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 22 Nov 2023 12:32:49 +0530 Subject: [PATCH 07/14] style: revamped page details UI (#2823) * style: revamp page details UI * chore: updated the info popover date format * fix: page actions mutation * style: made the page content responsive --- .../src/ui/components/alert-label.tsx | 26 +-- .../src/ui/components/content-browser.tsx | 58 +++---- .../src/ui/components/editor-header.tsx | 149 +++++++++-------- .../src/ui/components/index.ts | 9 ++ .../src/ui/components/info-popover.tsx | 79 +++++++++ .../src/ui/components/page-renderer.tsx | 49 +++--- .../src/ui/components/popover.tsx | 67 -------- .../src/ui/components/summary-popover.tsx | 57 +++++++ .../src/ui/components/summary-side-bar.tsx | 29 ++-- .../ui/components/vertical-dropdown-menu.tsx | 65 ++++---- .../editor/document-editor/src/ui/index.tsx | 140 ++++++++-------- .../src/ui/menu/fixed-menu.tsx | 117 +++++++------- .../document-editor/src/ui/readonly/index.tsx | 95 +++++------ .../src/ui/utils/menu-options.ts | 95 ++++++----- .../projects/[projectId]/pages/[pageId].tsx | 151 +++++++++--------- 15 files changed, 662 insertions(+), 524 deletions(-) create mode 100644 packages/editor/document-editor/src/ui/components/index.ts create mode 100644 packages/editor/document-editor/src/ui/components/info-popover.tsx delete mode 100644 packages/editor/document-editor/src/ui/components/popover.tsx create mode 100644 packages/editor/document-editor/src/ui/components/summary-popover.tsx diff --git a/packages/editor/document-editor/src/ui/components/alert-label.tsx b/packages/editor/document-editor/src/ui/components/alert-label.tsx index 7246647bc..0f0a238ba 100644 --- a/packages/editor/document-editor/src/ui/components/alert-label.tsx +++ b/packages/editor/document-editor/src/ui/components/alert-label.tsx @@ -1,19 +1,21 @@ -import { Icon } from "lucide-react" +import { Icon } from "lucide-react"; interface IAlertLabelProps { - Icon: Icon, - backgroundColor: string, - textColor?: string, - label: string, + Icon?: Icon; + backgroundColor: string; + textColor?: string; + label: string; } -export const AlertLabel = ({ Icon, backgroundColor,textColor, label }: IAlertLabelProps) => { +export const AlertLabel = (props: IAlertLabelProps) => { + const { Icon, backgroundColor, textColor, label } = props; return ( -
- - {label} +
+ {Icon && } + {label}
- ) - -} + ); +}; diff --git a/packages/editor/document-editor/src/ui/components/content-browser.tsx b/packages/editor/document-editor/src/ui/components/content-browser.tsx index 755d67b2d..bb0e3fb8b 100644 --- a/packages/editor/document-editor/src/ui/components/content-browser.tsx +++ b/packages/editor/document-editor/src/ui/components/content-browser.tsx @@ -8,33 +8,33 @@ interface ContentBrowserProps { markings: IMarking[]; } -export const ContentBrowser = ({ - editor, - markings, -}: ContentBrowserProps) => ( -
-

- Table of Contents -

-
- {markings.length !== 0 ? ( - markings.map((marking) => - marking.level === 1 ? ( - scrollSummary(editor, marking)} - heading={marking.text} - /> +export const ContentBrowser = (props: ContentBrowserProps) => { + const { editor, markings } = props; + + return ( +
+

Table of Contents

+
+ {markings.length !== 0 ? ( + markings.map((marking) => + marking.level === 1 ? ( + scrollSummary(editor, marking)} + heading={marking.text} + /> + ) : ( + scrollSummary(editor, marking)} + subHeading={marking.text} + /> + ), + ) ) : ( - scrollSummary(editor, marking)} - subHeading={marking.text} - /> - ) - ) - ) : ( -

- {"Headings will be displayed here for Navigation"} -

- )} -
-); +

+ Headings will be displayed here for navigation +

+ )} +
+
+ ); +}; diff --git a/packages/editor/document-editor/src/ui/components/editor-header.tsx b/packages/editor/document-editor/src/ui/components/editor-header.tsx index 32ebe43c9..e16f6768d 100644 --- a/packages/editor/document-editor/src/ui/components/editor-header.tsx +++ b/packages/editor/document-editor/src/ui/components/editor-header.tsx @@ -1,79 +1,90 @@ -import { Editor } from "@tiptap/react" -import { Lock, ArchiveIcon, MenuSquare } from "lucide-react" -import { useRef, useState } from "react" -import { usePopper } from "react-popper" -import { IMarking, UploadImage } from ".." -import { FixedMenu } from "../menu" -import { DocumentDetails } from "../types/editor-types" -import { AlertLabel } from "./alert-label" -import { ContentBrowser } from "./content-browser" -import { IVerticalDropdownItemProps, VerticalDropdownMenu } from "./vertical-dropdown-menu" +import { Editor } from "@tiptap/react"; +import { Archive, Info, Lock } from "lucide-react"; +import { IMarking, UploadImage } from ".."; +import { FixedMenu } from "../menu"; +import { DocumentDetails } from "../types/editor-types"; +import { AlertLabel } from "./alert-label"; +import { + IVerticalDropdownItemProps, + VerticalDropdownMenu, +} from "./vertical-dropdown-menu"; +import { SummaryPopover } from "./summary-popover"; +import { InfoPopover } from "./info-popover"; interface IEditorHeader { - editor: Editor, - KanbanMenuOptions: IVerticalDropdownItemProps[], - sidePeakVisible: boolean, - setSidePeakVisible: (currentState: boolean) => void, - markings: IMarking[], - isLocked: boolean, - isArchived: boolean, - archivedAt?: Date, - readonly: boolean, - uploadFile?: UploadImage, - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void, - documentDetails: DocumentDetails + editor: Editor; + KanbanMenuOptions: IVerticalDropdownItemProps[]; + sidePeekVisible: boolean; + setSidePeekVisible: (sidePeekState: boolean) => void; + markings: IMarking[]; + isLocked: boolean; + isArchived: boolean; + archivedAt?: Date; + readonly: boolean; + uploadFile?: UploadImage; + setIsSubmitting?: ( + isSubmitting: "submitting" | "submitted" | "saved", + ) => void; + documentDetails: DocumentDetails; } -export const EditorHeader = ({ documentDetails, archivedAt, editor, sidePeakVisible, readonly, setSidePeakVisible, markings, uploadFile, setIsSubmitting, KanbanMenuOptions, isArchived, isLocked }: IEditorHeader) => { - - const summaryMenuRef = useRef(null); - const summaryButtonRef = useRef(null); - const [summaryPopoverVisible, setSummaryPopoverVisible] = useState(false); - - const { styles: summaryPopoverStyles, attributes: summaryPopoverAttributes } = usePopper(summaryButtonRef.current, summaryMenuRef.current, { - placement: "bottom-start" - }) +export const EditorHeader = (props: IEditorHeader) => { + const { + documentDetails, + archivedAt, + editor, + sidePeekVisible, + readonly, + setSidePeekVisible, + markings, + uploadFile, + setIsSubmitting, + KanbanMenuOptions, + isArchived, + isLocked, + } = props; return ( +
+
+ +
-
-
-
-
setSummaryPopoverVisible(true)} - onMouseLeave={() => setSummaryPopoverVisible(false)} - > - - {summaryPopoverVisible && -
- -
- } -
- {isLocked && } - {(isArchived && archivedAt) && } -
+
+ {!readonly && uploadFile && ( + + )} +
- {(!readonly && uploadFile) && } -
- {!isArchived &&

{`Last updated at ${new Date(documentDetails.last_updated_at).toLocaleString()}`}

} - -
+
+ {isLocked && ( + + )} + {isArchived && archivedAt && ( + + )} + {!isArchived && } +
- ) - -} + ); +}; diff --git a/packages/editor/document-editor/src/ui/components/index.ts b/packages/editor/document-editor/src/ui/components/index.ts new file mode 100644 index 000000000..1496a3cf4 --- /dev/null +++ b/packages/editor/document-editor/src/ui/components/index.ts @@ -0,0 +1,9 @@ +export * from "./alert-label"; +export * from "./content-browser"; +export * from "./editor-header"; +export * from "./heading-component"; +export * from "./info-popover"; +export * from "./page-renderer"; +export * from "./summary-popover"; +export * from "./summary-side-bar"; +export * from "./vertical-dropdown-menu"; diff --git a/packages/editor/document-editor/src/ui/components/info-popover.tsx b/packages/editor/document-editor/src/ui/components/info-popover.tsx new file mode 100644 index 000000000..d42e32a8d --- /dev/null +++ b/packages/editor/document-editor/src/ui/components/info-popover.tsx @@ -0,0 +1,79 @@ +import { useState } from "react"; +import { usePopper } from "react-popper"; +import { Calendar, History, Info } from "lucide-react"; +// types +import { DocumentDetails } from "../types/editor-types"; + +type Props = { + documentDetails: DocumentDetails; +}; + +// function to render a Date in the format- 25 May 2023 at 2:53PM +const renderDate = (date: Date): string => { + const options: Intl.DateTimeFormatOptions = { + day: "numeric", + month: "long", + year: "numeric", + hour: "numeric", + minute: "numeric", + hour12: true, + }; + + const formattedDate: string = new Intl.DateTimeFormat( + "en-US", + options, + ).format(date); + + return formattedDate; +}; + +export const InfoPopover: React.FC = (props) => { + const { documentDetails } = props; + + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const [referenceElement, setReferenceElement] = + useState(null); + const [popperElement, setPopperElement] = useState( + null, + ); + + const { styles: infoPopoverStyles, attributes: infoPopoverAttributes } = + usePopper(referenceElement, popperElement, { + placement: "bottom-start", + }); + + return ( +
setIsPopoverOpen(true)} + onMouseLeave={() => setIsPopoverOpen(false)} + > + + {isPopoverOpen && ( +
+
+
Last updated on
+
+ + {renderDate(new Date(documentDetails.last_updated_at))} +
+
+
+
Created on
+
+ + {renderDate(new Date(documentDetails.created_on))} +
+
+
+ )} +
+ ); +}; diff --git a/packages/editor/document-editor/src/ui/components/page-renderer.tsx b/packages/editor/document-editor/src/ui/components/page-renderer.tsx index 746d46e45..194152dd3 100644 --- a/packages/editor/document-editor/src/ui/components/page-renderer.tsx +++ b/packages/editor/document-editor/src/ui/components/page-renderer.tsx @@ -3,43 +3,32 @@ import { Editor } from "@tiptap/react"; import { DocumentDetails } from "../types/editor-types"; interface IPageRenderer { - sidePeakVisible: boolean; documentDetails: DocumentDetails; editor: Editor; editorClassNames: string; editorContentCustomClassNames?: string; } -export const PageRenderer = ({ - sidePeakVisible, - documentDetails, - editor, - editorClassNames, - editorContentCustomClassNames, -}: IPageRenderer) => { +export const PageRenderer = (props: IPageRenderer) => { + const { + documentDetails, + editor, + editorClassNames, + editorContentCustomClassNames, + } = props; + return ( -
-
-
-

- {documentDetails.title} -

-
-
-
- -
- -
-
-
+
+

+ {documentDetails.title} +

+
+ + +
); diff --git a/packages/editor/document-editor/src/ui/components/popover.tsx b/packages/editor/document-editor/src/ui/components/popover.tsx deleted file mode 100644 index 8c587b603..000000000 --- a/packages/editor/document-editor/src/ui/components/popover.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React, { Fragment, useState } from "react"; -import { usePopper } from "react-popper"; -import { Popover, Transition } from "@headlessui/react"; -import { Placement } from "@popperjs/core"; -// ui -import { Button } from "@plane/ui"; -// icons -import { ChevronUp, MenuIcon } from "lucide-react"; - -type Props = { - children: React.ReactNode; - title?: string; - placement?: Placement; -}; - -export const SummaryPopover: React.FC = (props) => { - const { children, title = "SummaryPopover", placement } = props; - - const [referenceElement, setReferenceElement] = useState(null); - const [popperElement, setPopperElement] = useState(null); - - const { styles, attributes } = usePopper(referenceElement, popperElement, { - placement: placement ?? "auto", - }); - - return ( - - {({ open }) => { - if (open) { - } - return ( - <> - - - - - -
-
{children}
-
-
-
- - ); - }} -
- ); -}; diff --git a/packages/editor/document-editor/src/ui/components/summary-popover.tsx b/packages/editor/document-editor/src/ui/components/summary-popover.tsx new file mode 100644 index 000000000..7c85ed945 --- /dev/null +++ b/packages/editor/document-editor/src/ui/components/summary-popover.tsx @@ -0,0 +1,57 @@ +import { useState } from "react"; +import { Editor } from "@tiptap/react"; +import { usePopper } from "react-popper"; +import { List } from "lucide-react"; +// components +import { ContentBrowser } from "./content-browser"; +// types +import { IMarking } from ".."; + +type Props = { + editor: Editor; + markings: IMarking[]; + sidePeekVisible: boolean; + setSidePeekVisible: (sidePeekState: boolean) => void; +}; + +export const SummaryPopover: React.FC = (props) => { + const { editor, markings, sidePeekVisible, setSidePeekVisible } = props; + + const [referenceElement, setReferenceElement] = + useState(null); + const [popperElement, setPopperElement] = useState( + null, + ); + + const { styles: summaryPopoverStyles, attributes: summaryPopoverAttributes } = + usePopper(referenceElement, popperElement, { + placement: "bottom-start", + }); + + return ( +
+ + {!sidePeekVisible && ( +
+ +
+ )} +
+ ); +}; diff --git a/packages/editor/document-editor/src/ui/components/summary-side-bar.tsx b/packages/editor/document-editor/src/ui/components/summary-side-bar.tsx index 304c80018..dd98e0572 100644 --- a/packages/editor/document-editor/src/ui/components/summary-side-bar.tsx +++ b/packages/editor/document-editor/src/ui/components/summary-side-bar.tsx @@ -1,18 +1,25 @@ -import { Editor } from "@tiptap/react" -import { IMarking } from ".." -import { ContentBrowser } from "./content-browser" +import { Editor } from "@tiptap/react"; +import { IMarking } from ".."; +import { ContentBrowser } from "./content-browser"; interface ISummarySideBarProps { - editor: Editor, - markings: IMarking[], - sidePeakVisible: boolean + editor: Editor; + markings: IMarking[]; + sidePeekVisible: boolean; } -export const SummarySideBar = ({ editor, markings, sidePeakVisible }: ISummarySideBarProps) => { +export const SummarySideBar = ({ + editor, + markings, + sidePeekVisible, +}: ISummarySideBarProps) => { return ( - -
+
- ) -} + ); +}; diff --git a/packages/editor/document-editor/src/ui/components/vertical-dropdown-menu.tsx b/packages/editor/document-editor/src/ui/components/vertical-dropdown-menu.tsx index c28cb4d32..cf7ee7db1 100644 --- a/packages/editor/document-editor/src/ui/components/vertical-dropdown-menu.tsx +++ b/packages/editor/document-editor/src/ui/components/vertical-dropdown-menu.tsx @@ -1,41 +1,52 @@ -import { Button, CustomMenu } from "@plane/ui" -import { ChevronUp, Icon, MoreVertical } from "lucide-react" +import { Button, CustomMenu } from "@plane/ui"; +import { ChevronUp, Icon, MoreVertical } from "lucide-react"; - -type TMenuItems = "archive_page" | "unarchive_page" | "lock_page" | "unlock_page" | "copy_markdown" | "close_page" | "copy_page_link" | "duplicate_page" +type TMenuItems = + | "archive_page" + | "unarchive_page" + | "lock_page" + | "unlock_page" + | "copy_markdown" + | "close_page" + | "copy_page_link" + | "duplicate_page"; export interface IVerticalDropdownItemProps { - key: number, - type: TMenuItems, - Icon: Icon, - label: string, - action: () => Promise | void + key: number; + type: TMenuItems; + Icon: Icon; + label: string; + action: () => Promise | void; } export interface IVerticalDropdownMenuProps { - items: IVerticalDropdownItemProps[], + items: IVerticalDropdownItemProps[]; } -const VerticalDropdownItem = ({ Icon, label, action }: IVerticalDropdownItemProps) => { - +const VerticalDropdownItem = ({ + Icon, + label, + action, +}: IVerticalDropdownItemProps) => { return ( - - + + +
{label}
- ) -} + ); +}; export const VerticalDropdownMenu = ({ items }: IVerticalDropdownMenuProps) => { - return ( - - }> + } + > {items.map((item, index) => ( { /> ))} - ) -} + ); +}; diff --git a/packages/editor/document-editor/src/ui/index.tsx b/packages/editor/document-editor/src/ui/index.tsx index be75ff8fb..f46c5ca47 100644 --- a/packages/editor/document-editor/src/ui/index.tsx +++ b/packages/editor/document-editor/src/ui/index.tsx @@ -1,34 +1,40 @@ -"use client" -import React, { useState } from 'react'; -import { cn, getEditorClassNames, useEditor } from '@plane/editor-core'; -import { DocumentEditorExtensions } from './extensions'; -import { IDuplicationConfig, IPageArchiveConfig, IPageLockConfig } from './types/menu-actions'; -import { EditorHeader } from './components/editor-header'; -import { useEditorMarkings } from './hooks/use-editor-markings'; -import { SummarySideBar } from './components/summary-side-bar'; -import { DocumentDetails } from './types/editor-types'; -import { PageRenderer } from './components/page-renderer'; -import { getMenuOptions } from './utils/menu-options'; -import { useRouter } from 'next/router'; +"use client"; +import React, { useState } from "react"; +import { cn, getEditorClassNames, useEditor } from "@plane/editor-core"; +import { DocumentEditorExtensions } from "./extensions"; +import { + IDuplicationConfig, + IPageArchiveConfig, + IPageLockConfig, +} from "./types/menu-actions"; +import { EditorHeader } from "./components/editor-header"; +import { useEditorMarkings } from "./hooks/use-editor-markings"; +import { SummarySideBar } from "./components/summary-side-bar"; +import { DocumentDetails } from "./types/editor-types"; +import { PageRenderer } from "./components/page-renderer"; +import { getMenuOptions } from "./utils/menu-options"; +import { useRouter } from "next/router"; export type UploadImage = (file: File) => Promise; export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise; interface IDocumentEditor { - documentDetails: DocumentDetails, + documentDetails: DocumentDetails; value: string; uploadFile: UploadImage; deleteFile: DeleteImage; customClassName?: string; editorContentCustomClassNames?: string; onChange: (json: any, html: string) => void; - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void; + setIsSubmitting?: ( + isSubmitting: "submitting" | "submitted" | "saved", + ) => void; setShouldShowAlert?: (showAlert: boolean) => void; forwardedRef?: any; debouncedUpdatesEnabled?: boolean; - duplicationConfig?: IDuplicationConfig, - pageLockConfig?: IPageLockConfig, - pageArchiveConfig?: IPageArchiveConfig + duplicationConfig?: IDuplicationConfig; + pageLockConfig?: IPageLockConfig; + pageArchiveConfig?: IPageArchiveConfig; } interface DocumentEditorProps extends IDocumentEditor { forwardedRef?: React.Ref; @@ -40,10 +46,10 @@ interface EditorHandle { } export interface IMarking { - type: "heading", - level: number, - text: string, - sequence: number + type: "heading"; + level: number; + text: string; + sequence: number; } const DocumentEditor = ({ @@ -60,21 +66,20 @@ const DocumentEditor = ({ forwardedRef, duplicationConfig, pageLockConfig, - pageArchiveConfig + pageArchiveConfig, }: IDocumentEditor) => { - // const [alert, setAlert] = useState("") - const { markings, updateMarkings } = useEditorMarkings() - const [sidePeakVisible, setSidePeakVisible] = useState(true) - const router = useRouter() + const { markings, updateMarkings } = useEditorMarkings(); + const [sidePeekVisible, setSidePeekVisible] = useState(true); + const router = useRouter(); const editor = useEditor({ onChange(json, html) { - updateMarkings(json) - onChange(json, html) + updateMarkings(json); + onChange(json, html); }, onStart(json) { - updateMarkings(json) + updateMarkings(json); }, debouncedUpdatesEnabled, setIsSubmitting, @@ -87,65 +92,66 @@ const DocumentEditor = ({ }); if (!editor) { - return null + return null; } - const KanbanMenuOptions = getMenuOptions( - { - editor: editor, - router: router, - duplicationConfig: duplicationConfig, - pageLockConfig: pageLockConfig, - pageArchiveConfig: pageArchiveConfig, - } - ) - const editorClassNames = getEditorClassNames({ noBorder: true, borderOnFocus: false, customClassName }); + const KanbanMenuOptions = getMenuOptions({ + editor: editor, + router: router, + duplicationConfig: duplicationConfig, + pageLockConfig: pageLockConfig, + pageArchiveConfig: pageArchiveConfig, + }); + const editorClassNames = getEditorClassNames({ + noBorder: true, + borderOnFocus: false, + customClassName, + }); if (!editor) return null; return ( -
-
- -
-
-
+
+ setSidePeekVisible(val)} + markings={markings} + uploadFile={uploadFile} + setIsSubmitting={setIsSubmitting} + isLocked={!pageLockConfig ? false : pageLockConfig.is_locked} + isArchived={!pageArchiveConfig ? false : pageArchiveConfig.is_archived} + archivedAt={pageArchiveConfig && pageArchiveConfig.archived_at} + documentDetails={documentDetails} + /> +
+
+
+
- {/* Page Element */}
+
); -} +}; -const DocumentEditorWithRef = React.forwardRef((props, ref) => ( - -)); +const DocumentEditorWithRef = React.forwardRef( + (props, ref) => , +); DocumentEditorWithRef.displayName = "DocumentEditorWithRef"; -export { DocumentEditor, DocumentEditorWithRef } +export { DocumentEditor, DocumentEditorWithRef }; diff --git a/packages/editor/document-editor/src/ui/menu/fixed-menu.tsx b/packages/editor/document-editor/src/ui/menu/fixed-menu.tsx index 2cd07ec14..8080f7c63 100644 --- a/packages/editor/document-editor/src/ui/menu/fixed-menu.tsx +++ b/packages/editor/document-editor/src/ui/menu/fixed-menu.tsx @@ -1,7 +1,22 @@ import { Editor } from "@tiptap/react"; -import { BoldIcon, Heading1, Heading2, Heading3 } from "lucide-react"; +import { BoldIcon } from "lucide-react"; -import { BoldItem, BulletListItem, cn, CodeItem, ImageItem, ItalicItem, NumberedListItem, QuoteItem, StrikeThroughItem, TableItem, UnderLineItem, HeadingOneItem, HeadingTwoItem, HeadingThreeItem } from "@plane/editor-core"; +import { + BoldItem, + BulletListItem, + cn, + CodeItem, + ImageItem, + ItalicItem, + NumberedListItem, + QuoteItem, + StrikeThroughItem, + TableItem, + UnderLineItem, + HeadingOneItem, + HeadingTwoItem, + HeadingThreeItem, +} from "@plane/editor-core"; import { UploadImage } from ".."; export interface BubbleMenuItem { @@ -14,77 +29,69 @@ export interface BubbleMenuItem { type EditorBubbleMenuProps = { editor: Editor; uploadFile: UploadImage; - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void; -} + setIsSubmitting?: ( + isSubmitting: "submitting" | "submitted" | "saved", + ) => void; +}; export const FixedMenu = (props: EditorBubbleMenuProps) => { + const { editor, uploadFile, setIsSubmitting } = props; + const basicMarkItems: BubbleMenuItem[] = [ - HeadingOneItem(props.editor), - HeadingTwoItem(props.editor), - HeadingThreeItem(props.editor), - BoldItem(props.editor), - ItalicItem(props.editor), - UnderLineItem(props.editor), - StrikeThroughItem(props.editor), + HeadingOneItem(editor), + HeadingTwoItem(editor), + HeadingThreeItem(editor), + BoldItem(editor), + ItalicItem(editor), + UnderLineItem(editor), + StrikeThroughItem(editor), ]; const listItems: BubbleMenuItem[] = [ - BulletListItem(props.editor), - NumberedListItem(props.editor), + BulletListItem(editor), + NumberedListItem(editor), ]; const userActionItems: BubbleMenuItem[] = [ - QuoteItem(props.editor), - CodeItem(props.editor), + QuoteItem(editor), + CodeItem(editor), ]; const complexItems: BubbleMenuItem[] = [ - TableItem(props.editor), - ImageItem(props.editor, props.uploadFile, props.setIsSubmitting), + TableItem(editor), + ImageItem(editor, uploadFile, setIsSubmitting), ]; - // const handleAccessChange = (accessKey: string) => { - // props.commentAccessSpecifier?.onAccessChange(accessKey); - // }; - - return ( -
-
- {basicMarkItems.map((item, index) => ( +
+
+ {basicMarkItems.map((item) => ( ))}
-
- {listItems.map((item, index) => ( +
+ {listItems.map((item) => ( ))}
-
- {userActionItems.map((item, index) => ( +
+ {userActionItems.map((item) => ( ))}
-
- {complexItems.map((item, index) => ( +
+ {complexItems.map((item) => ( + )} {(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && (
diff --git a/web/components/workspace/settings/members-list-item.tsx b/web/components/workspace/settings/members-list-item.tsx index d98846e74..7c00f4f12 100644 --- a/web/components/workspace/settings/members-list-item.tsx +++ b/web/components/workspace/settings/members-list-item.tsx @@ -11,7 +11,7 @@ import { ConfirmWorkspaceMemberRemove } from "components/workspace"; // ui import { CustomSelect, Tooltip } from "@plane/ui"; // icons -import { ChevronDown, XCircle } from "lucide-react"; +import { ChevronDown, Dot, XCircle } from "lucide-react"; // constants import { ROLE } from "constants/workspace"; import { TUserWorkspaceRole } from "types"; @@ -132,7 +132,15 @@ export const WorkspaceMembersListItem: FC = (props) => { ) : (

{member.display_name || member.email}

)} -

{member.email ?? member.display_name}

+
+

{member.display_name}

+ {isAdmin && ( + <> + +

{member.email}

+ + )} +
diff --git a/web/components/workspace/settings/members-list.tsx b/web/components/workspace/settings/members-list.tsx index b4f263c49..2244c5cad 100644 --- a/web/components/workspace/settings/members-list.tsx +++ b/web/components/workspace/settings/members-list.tsx @@ -56,7 +56,7 @@ export const WorkspaceMembersList: FC<{ searchQuery: string }> = observer(({ sea ); return ( -
+
{workspaceMembersWithInvitations.length > 0 ? searchedMembers?.map((member) => ) : null} diff --git a/web/constants/issue.ts b/web/constants/issue.ts index d80430d0f..7979672fd 100644 --- a/web/constants/issue.ts +++ b/web/constants/issue.ts @@ -239,7 +239,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { filters: ["priority", "state_group", "labels", "start_date", "target_date"], display_properties: true, display_filters: { - group_by: ["state_detail.group", "priority", "project", "labels", null], + group_by: ["state_detail.group", "priority", "project", "labels"], order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"], type: [null, "active", "backlog"], }, @@ -282,7 +282,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { filters: ["priority", "state_group", "labels", "start_date", "target_date"], display_properties: true, display_filters: { - group_by: ["state_detail.group", "priority", "project", "labels", null], + group_by: ["state_detail.group", "priority", "project", "labels"], order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"], type: [null, "active", "backlog"], }, From 3df4794e77bfea4cb9c3899e033bd420e292f044 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Wed, 22 Nov 2023 13:20:59 +0530 Subject: [PATCH 09/14] fix: AI Assistance hide/unhide depending on the configuration (#2825) * fix: gpt error handlijng * fix: enabling ai assistance only when it is configured. --- .../core/modals/gpt-assistant-modal.tsx | 2 +- web/components/issues/draft-issue-form.tsx | 60 ++++++++++--------- web/components/issues/form.tsx | 53 ++++++++-------- .../pages/create-update-block-inline.tsx | 60 ++++++++++--------- .../web-hooks/webhooks-list-item.tsx | 6 +- web/types/app.d.ts | 2 + 6 files changed, 99 insertions(+), 84 deletions(-) diff --git a/web/components/core/modals/gpt-assistant-modal.tsx b/web/components/core/modals/gpt-assistant-modal.tsx index d677e7daa..82b082c29 100644 --- a/web/components/core/modals/gpt-assistant-modal.tsx +++ b/web/components/core/modals/gpt-assistant-modal.tsx @@ -92,7 +92,7 @@ export const GptAssistantModal: React.FC = (props) => { .catch((err) => { const error = err?.data?.error; - if (err.status === 429) + if (err?.status === 429) setToastAlert({ type: "error", title: "Error!", diff --git a/web/components/issues/draft-issue-form.tsx b/web/components/issues/draft-issue-form.tsx index e80c4609a..be9857dc8 100644 --- a/web/components/issues/draft-issue-form.tsx +++ b/web/components/issues/draft-issue-form.tsx @@ -31,6 +31,8 @@ import type { IUser, IIssue, ISearchIssueResponse } from "types"; // components import { RichTextEditorWithRef } from "@plane/rich-text-editor"; import useEditorSuggestions from "hooks/use-editor-suggestions"; +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; const aiService = new AIService(); const fileService = new FileService(); @@ -89,7 +91,7 @@ interface IssueFormProps { )[]; } -export const DraftIssueForm: FC = (props) => { +export const DraftIssueForm: FC = observer((props) => { const { handleFormSubmit, data, @@ -100,30 +102,30 @@ export const DraftIssueForm: FC = (props) => { createMore, setCreateMore, status, - user, fieldsToShow, handleDiscard, } = props; - + // states const [stateModal, setStateModal] = useState(false); const [labelModal, setLabelModal] = useState(false); const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false); const [selectedParentIssue, setSelectedParentIssue] = useState(null); - const [gptAssistantModal, setGptAssistantModal] = useState(false); const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false); - + // hooks const { setValue: setLocalStorageValue } = useLocalStorage("draftedIssue", {}); - + const { setToastAlert } = useToast(); + const editorSuggestions = useEditorSuggestions(); + // refs const editorRef = useRef(null); - + // router const router = useRouter(); const { workspaceSlug } = router.query; - - const { setToastAlert } = useToast(); - - const editorSuggestions = useEditorSuggestions(); - + // store + const { + appConfig: { envConfig }, + } = useMobxStore(); + // form info const { formState: { errors, isSubmitting }, handleSubmit, @@ -440,21 +442,23 @@ export const DraftIssueForm: FC = (props) => { /> )} /> - { - setGptAssistantModal(false); - // this is done so that the title do not reset after gpt popover closed - reset(getValues()); - }} - inset="top-2 left-0" - content="" - htmlContent={watch("description_html")} - onResponse={(response) => { - handleAiAssistance(response); - }} - projectId={projectId} - /> + {envConfig?.has_openai_configured && ( + { + setGptAssistantModal(false); + // this is done so that the title do not reset after gpt popover closed + reset(getValues()); + }} + inset="top-2 left-0" + content="" + htmlContent={watch("description_html")} + onResponse={(response) => { + handleAiAssistance(response); + }} + projectId={projectId} + /> + )}
)}
@@ -623,4 +627,4 @@ export const DraftIssueForm: FC = (props) => { ); -}; +}); diff --git a/web/components/issues/form.tsx b/web/components/issues/form.tsx index f33d8e6be..454647bfb 100644 --- a/web/components/issues/form.tsx +++ b/web/components/issues/form.tsx @@ -94,28 +94,29 @@ export const IssueForm: FC = observer((props) => { fieldsToShow, handleFormDirty, } = props; - + // states const [stateModal, setStateModal] = useState(false); const [labelModal, setLabelModal] = useState(false); const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false); const [selectedParentIssue, setSelectedParentIssue] = useState(null); - const [gptAssistantModal, setGptAssistantModal] = useState(false); const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false); - + // refs const editorRef = useRef(null); - + // router const router = useRouter(); const { workspaceSlug } = router.query; - - const { user: userStore } = useMobxStore(); - + // store + const { + user: userStore, + appConfig: { envConfig }, + } = useMobxStore(); const user = userStore.currentUser; - + console.log("envConfig", envConfig); + // hooks const editorSuggestion = useEditorSuggestions(); - const { setToastAlert } = useToast(); - + // form info const { formState: { errors, isSubmitting, isDirty }, handleSubmit, @@ -396,21 +397,23 @@ export const IssueForm: FC = observer((props) => { /> )} /> - { - setGptAssistantModal(false); - // this is done so that the title do not reset after gpt popover closed - reset(getValues()); - }} - inset="top-2 left-0" - content="" - htmlContent={watch("description_html")} - onResponse={(response) => { - handleAiAssistance(response); - }} - projectId={projectId} - /> + {envConfig?.has_openai_configured && ( + { + setGptAssistantModal(false); + // this is done so that the title do not reset after gpt popover closed + reset(getValues()); + }} + inset="top-2 left-0" + content="" + htmlContent={watch("description_html")} + onResponse={(response) => { + handleAiAssistance(response); + }} + projectId={projectId} + /> + )}
)}
diff --git a/web/components/pages/create-update-block-inline.tsx b/web/components/pages/create-update-block-inline.tsx index d2533a967..a18ca204c 100644 --- a/web/components/pages/create-update-block-inline.tsx +++ b/web/components/pages/create-update-block-inline.tsx @@ -19,6 +19,7 @@ import { IUser, IPageBlock } from "types"; // fetch-keys import { PAGE_BLOCKS_LIST } from "constants/fetch-keys"; import useEditorSuggestions from "hooks/use-editor-suggestions"; +import { useMobxStore } from "lib/mobx/store-provider"; type Props = { handleClose: () => void; @@ -40,19 +41,24 @@ const pagesService = new PageService(); const issueService = new IssueService(); const fileService = new FileService(); -export const CreateUpdateBlockInline: FC = ({ handleClose, data, handleAiAssistance, setIsSyncing, focus }) => { +export const CreateUpdateBlockInline: FC = (props) => { + const { handleClose, data, handleAiAssistance, setIsSyncing, focus } = props; + // states const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false); const [gptAssistantModal, setGptAssistantModal] = useState(false); - + // store + const { + appConfig: { envConfig }, + } = useMobxStore(); + // refs const editorRef = useRef(null); - + // router const router = useRouter(); const { workspaceSlug, projectId, pageId } = router.query; - + // hooks const editorSuggestion = useEditorSuggestions(); - const { setToastAlert } = useToast(); - + // form info const { handleSubmit, control, @@ -222,9 +228,7 @@ export const CreateUpdateBlockInline: FC = ({ handleClose, data, handleAi else handleSubmit(createPageBlock)(); } }; - window.addEventListener("keydown", submitForm); - return () => { window.removeEventListener("keydown", submitForm); }; @@ -345,26 +349,28 @@ export const CreateUpdateBlockInline: FC = ({ handleClose, data, handleAi
- setGptAssistantModal(false)} - inset="top-8 left-0" - content={watch("description_html")} - htmlContent={watch("description_html")} - onResponse={(response) => { - if (data && handleAiAssistance) { - handleAiAssistance(response); - editorRef.current?.setEditorValue(`${watch("description_html")}

${response}

` ?? ""); - } else { - setValue("description", {}); - setValue("description_html", `${watch("description_html")}

${response}

`); + {envConfig?.has_openai_configured && ( + setGptAssistantModal(false)} + inset="top-8 left-0" + content={watch("description_html")} + htmlContent={watch("description_html")} + onResponse={(response) => { + if (data && handleAiAssistance) { + handleAiAssistance(response); + editorRef.current?.setEditorValue(`${watch("description_html")}

${response}

` ?? ""); + } else { + setValue("description", {}); + setValue("description_html", `${watch("description_html")}

${response}

`); - editorRef.current?.setEditorValue(watch("description_html") ?? ""); - } - }} - projectId={projectId?.toString() ?? ""} - /> + editorRef.current?.setEditorValue(watch("description_html") ?? ""); + } + }} + projectId={projectId?.toString() ?? ""} + /> + )}
); }; diff --git a/web/components/web-hooks/webhooks-list-item.tsx b/web/components/web-hooks/webhooks-list-item.tsx index 4d99e1d22..549caf024 100644 --- a/web/components/web-hooks/webhooks-list-item.tsx +++ b/web/components/web-hooks/webhooks-list-item.tsx @@ -1,10 +1,10 @@ -import { FC, useState } from "react"; +import { FC } from "react"; import { ToggleSwitch } from "@plane/ui"; -import { Pencil, XCircle } from "lucide-react"; -import { IWebhook } from "types"; import Link from "next/link"; import { RootStore } from "store/root"; import { useMobxStore } from "lib/mobx/store-provider"; +// types +import { IWebhook } from "types"; interface IWebhookListItem { workspaceSlug: string; diff --git a/web/types/app.d.ts b/web/types/app.d.ts index d5a7953b1..05f0fc7e5 100644 --- a/web/types/app.d.ts +++ b/web/types/app.d.ts @@ -11,4 +11,6 @@ export interface IAppConfig { slack_client_id: string | null; posthog_api_key: string | null; posthog_host: string | null; + has_openai_configured: boolean; + has_unsplash_configured: boolean; } From 67000892e59e70abd455d62217ef9c9c14d1410b Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 22 Nov 2023 13:47:59 +0530 Subject: [PATCH 10/14] chore: dashboard redirection fix (#2826) --- web/components/workspace/issues-stats.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/components/workspace/issues-stats.tsx b/web/components/workspace/issues-stats.tsx index 22e966ac8..aef5cd108 100644 --- a/web/components/workspace/issues-stats.tsx +++ b/web/components/workspace/issues-stats.tsx @@ -23,7 +23,10 @@ export const IssuesStats: React.FC = ({ data }) => {

Issues assigned to you

{data ? ( -
router.push(`/${workspaceSlug}/me/my-issues`)}> +
router.push(`/${workspaceSlug}/workspace-views/assigned`)} + > {data.assigned_issues_count}
) : ( From 021c0675b79de723b17679f176f66a4d7419bb2e Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 22 Nov 2023 14:36:29 +0530 Subject: [PATCH 11/14] fix: module sidebar link section (#2830) --- web/components/modules/sidebar.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/web/components/modules/sidebar.tsx b/web/components/modules/sidebar.tsx index 62613bd39..df42fc0e4 100644 --- a/web/components/modules/sidebar.tsx +++ b/web/components/modules/sidebar.tsx @@ -429,10 +429,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { {({ open }) => (
- +
Links
From 6cb393983504af5fa62ef072f5e911e8273690bc Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 22 Nov 2023 14:54:52 +0530 Subject: [PATCH 12/14] style: project card improvement (#2827) --- web/components/project/card.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/project/card.tsx b/web/components/project/card.tsx index 9bfe6b7a7..25faed4b3 100644 --- a/web/components/project/card.tsx +++ b/web/components/project/card.tsx @@ -113,7 +113,7 @@ export const ProjectCard: React.FC = observer((props) => { className="absolute top-0 left-0 h-full w-full object-cover rounded-t" /> -
+
From 59dbbb29cd0a981a5c6236fde06aea2eff50abdb Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 22 Nov 2023 14:55:18 +0530 Subject: [PATCH 13/14] fix: custom analytics project dropdown fix (#2828) --- web/components/analytics/custom-analytics/select/project.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/analytics/custom-analytics/select/project.tsx b/web/components/analytics/custom-analytics/select/project.tsx index c57e56709..a4220266b 100644 --- a/web/components/analytics/custom-analytics/select/project.tsx +++ b/web/components/analytics/custom-analytics/select/project.tsx @@ -34,7 +34,7 @@ export const SelectProject: React.FC = ({ value, onChange, projects }) => .join(", ") : "All projects" } - optionsClassName="min-w-full" + optionsClassName="min-w-full max-w-[20rem]" multiple /> ); From 174bf70cce9dde8eb62bc333a4dc1dd078c2c8be Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Wed, 22 Nov 2023 15:31:29 +0530 Subject: [PATCH 14/14] chore: api fixes --- apiserver/plane/api/serializers/issue.py | 2 +- apiserver/plane/api/urls/cycle.py | 2 +- apiserver/plane/api/urls/issue.py | 12 ++++++------ apiserver/plane/api/urls/module.py | 2 +- apiserver/plane/api/urls/state.py | 5 +++++ apiserver/plane/api/views/cycle.py | 8 ++++---- apiserver/plane/api/views/issue.py | 23 +++++++++++------------ apiserver/plane/api/views/module.py | 18 +++++++++++++----- apiserver/plane/api/views/state.py | 22 ++++++++++------------ apiserver/plane/app/views/base.py | 11 +++++++---- apiserver/plane/app/views/cycle.py | 4 ++-- apiserver/plane/app/views/module.py | 4 ++-- 12 files changed, 63 insertions(+), 50 deletions(-) diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index f628c0358..2dafaa7e6 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -308,5 +308,5 @@ class IssueActivitySerializer(BaseSerializer): model = IssueActivity exclude = [ "created_by", - "udpated_by", + "updated_by", ] diff --git a/apiserver/plane/api/urls/cycle.py b/apiserver/plane/api/urls/cycle.py index b377aa4bc..f557f8af0 100644 --- a/apiserver/plane/api/urls/cycle.py +++ b/apiserver/plane/api/urls/cycle.py @@ -23,7 +23,7 @@ urlpatterns = [ name="cycle-issues", ), path( - "workspaces//projects//cycles//cycle-issues//", + "workspaces//projects//cycles//cycle-issues//", CycleIssueAPIEndpoint.as_view(), name="cycle-issues", ), diff --git a/apiserver/plane/api/urls/issue.py b/apiserver/plane/api/urls/issue.py index e6770579d..070ea8bd9 100644 --- a/apiserver/plane/api/urls/issue.py +++ b/apiserver/plane/api/urls/issue.py @@ -20,22 +20,22 @@ urlpatterns = [ name="issue", ), path( - "workspaces//projects//issue-labels/", + "workspaces//projects//labels/", LabelAPIEndpoint.as_view(), name="label", ), path( - "workspaces//projects//issue-labels//", + "workspaces//projects//labels//", LabelAPIEndpoint.as_view(), name="label", ), path( - "workspaces//projects//issues//issue-links/", + "workspaces//projects//issues//links/", IssueLinkAPIEndpoint.as_view(), name="link", ), path( - "workspaces//projects//issues//issue-links//", + "workspaces//projects//issues//links//", IssueLinkAPIEndpoint.as_view(), name="link", ), @@ -50,12 +50,12 @@ urlpatterns = [ name="comment", ), path( - "workspaces//projects//issues//activites/", + "workspaces//projects//issues//activities/", IssueActivityAPIEndpoint.as_view(), name="activity", ), path( - "workspaces//projects//issues//activites//", + "workspaces//projects//issues//activities//", IssueActivityAPIEndpoint.as_view(), name="activity", ), diff --git a/apiserver/plane/api/urls/module.py b/apiserver/plane/api/urls/module.py index 7860a0fce..7117a9e8b 100644 --- a/apiserver/plane/api/urls/module.py +++ b/apiserver/plane/api/urls/module.py @@ -19,7 +19,7 @@ urlpatterns = [ name="module-issues", ), path( - "workspaces//projects//modules//module-issues//", + "workspaces//projects//modules//module-issues//", ModuleIssueAPIEndpoint.as_view(), name="module-issues", ), diff --git a/apiserver/plane/api/urls/state.py b/apiserver/plane/api/urls/state.py index cf5eefd53..0676ac5ad 100644 --- a/apiserver/plane/api/urls/state.py +++ b/apiserver/plane/api/urls/state.py @@ -8,4 +8,9 @@ urlpatterns = [ StateAPIEndpoint.as_view(), name="states", ), + path( + "workspaces//projects//states//", + StateAPIEndpoint.as_view(), + name="states", + ), ] \ No newline at end of file diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index 52458f317..8345d7824 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -291,7 +291,7 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView): } ), actor_id=str(request.user.id), - issue_id=str(pk), + issue_id=None, project_id=str(project_id), current_instance=None, epoch=int(timezone.now().timestamp()), @@ -476,9 +476,9 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView): status=status.HTTP_200_OK, ) - def delete(self, request, slug, project_id, cycle_id, pk): + def delete(self, request, slug, project_id, cycle_id, issue_id): cycle_issue = CycleIssue.objects.get( - pk=pk, workspace__slug=slug, project_id=project_id, cycle_id=cycle_id + issue_id=issue_id, workspace__slug=slug, project_id=project_id, cycle_id=cycle_id ) issue_id = cycle_issue.issue_id cycle_issue.delete() @@ -491,7 +491,7 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView): } ), actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("pk", None)), + issue_id=str(issue_id), project_id=str(self.kwargs.get("project_id", None)), current_instance=None, epoch=int(timezone.now().timestamp()), diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index f2ffb7f0d..3b43c7ab8 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -352,11 +352,11 @@ class IssueLinkAPIEndpoint(BaseAPIView): .distinct() ) - def get(self, request, slug, project_id, pk=None): + def get(self, request, slug, project_id, issue_id, pk=None): if pk is None: - labels = self.get_queryset() + issue_links = self.get_queryset() serializer = IssueLinkSerializer( - labels, + issue_links, fields=self.fields, expand=self.expand, ) @@ -370,14 +370,13 @@ class IssueLinkAPIEndpoint(BaseAPIView): expand=self.expand, ).data, ) - else: - label = self.get_queryset().get(pk=pk) - serializer = IssueLinkSerializer( - label, - fields=self.fields, - expand=self.expand, - ) - return Response(serializer.data, status=status.HTTP_200_OK) + issue_link = self.get_queryset().get(pk=pk) + serializer = IssueLinkSerializer( + issue_link, + fields=self.fields, + expand=self.expand, + ) + return Response(serializer.data, status=status.HTTP_200_OK) def post(self, request, slug, project_id, issue_id): serializer = IssueLinkSerializer(data=request.data) @@ -590,7 +589,7 @@ class IssueActivityAPIEndpoint(BaseAPIView): serializer = IssueActivitySerializer(issue_activities) return Response(serializer.data, status=status.HTTP_200_OK) - self.paginate( + return self.paginate( request=request, queryset=(issue_activities), on_results=lambda issue_activity: IssueActivitySerializer( diff --git a/apiserver/plane/api/views/module.py b/apiserver/plane/api/views/module.py index 78f721adc..51be6be30 100644 --- a/apiserver/plane/api/views/module.py +++ b/apiserver/plane/api/views/module.py @@ -129,6 +129,14 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView): serializer = ModuleSerializer(module) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def patch(self, request, slug, project_id, pk): + module = Module.objects.get(pk=pk, project_id=project_id, workspace__slug=slug) + serializer = ModuleSerializer(module, data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def get(self, request, slug, project_id, pk=None): if pk: @@ -168,7 +176,7 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView): } ), actor_id=str(request.user.id), - issue_id=str(pk), + issue_id=None, project_id=str(project_id), current_instance=None, epoch=int(timezone.now().timestamp()), @@ -323,7 +331,7 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView): # Capture Issue Activity issue_activity.delay( type="module.activity.created", - requested_data=json.dumps({"modules_list": issues}), + requested_data=json.dumps({"modules_list": str(issues)}), actor_id=str(self.request.user.id), issue_id=None, project_id=str(self.kwargs.get("project_id", None)), @@ -343,9 +351,9 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView): status=status.HTTP_200_OK, ) - def delete(self, request, slug, project_id, module_id, pk): + def delete(self, request, slug, project_id, module_id, issue_id): module_issue = ModuleIssue.objects.get( - workspace__slug=slug, project_id=project_id, module_id=module_id, pk=pk + workspace__slug=slug, project_id=project_id, module_id=module_id, issue_id=issue_id ) module_issue.delete() issue_activity.delay( @@ -357,7 +365,7 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView): } ), actor_id=str(request.user.id), - issue_id=str(pk), + issue_id=str(issue_id), project_id=str(project_id), current_instance=None, epoch=int(timezone.now().timestamp()), diff --git a/apiserver/plane/api/views/state.py b/apiserver/plane/api/views/state.py index 8e7a73d9b..99b3a943f 100644 --- a/apiserver/plane/api/views/state.py +++ b/apiserver/plane/api/views/state.py @@ -23,10 +23,8 @@ class StateAPIEndpoint(BaseAPIView): ] def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) + return ( + State.objects.filter(workspace__slug=self.kwargs.get("slug")) .filter(project_id=self.kwargs.get("project_id")) .filter(project__project_projectmember__member=self.request.user) .filter(~Q(name="Triage")) @@ -42,9 +40,9 @@ class StateAPIEndpoint(BaseAPIView): return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def get(self, request, slug, project_id, pk=None): - if pk: - serializer = StateSerializer(self.get_queryset().get(pk=pk)) + def get(self, request, slug, project_id, state_id=None): + if state_id: + serializer = StateSerializer(self.get_queryset().get(pk=state_id)) return Response(serializer.data, status=status.HTTP_200_OK) return self.paginate( request=request, @@ -57,10 +55,10 @@ class StateAPIEndpoint(BaseAPIView): ).data, ) - def delete(self, request, slug, project_id, pk): + def delete(self, request, slug, project_id, state_id): state = State.objects.get( ~Q(name="Triage"), - pk=pk, + pk=state_id, project_id=project_id, workspace__slug=slug, ) @@ -69,7 +67,7 @@ class StateAPIEndpoint(BaseAPIView): return Response({"error": "Default state cannot be deleted"}, status=False) # Check for any issues in the state - issue_exist = Issue.issue_objects.filter(state=pk).exists() + issue_exist = Issue.issue_objects.filter(state=state_id).exists() if issue_exist: return Response( @@ -80,8 +78,8 @@ class StateAPIEndpoint(BaseAPIView): state.delete() return Response(status=status.HTTP_204_NO_CONTENT) - def patch(self, request, slug, project_id, pk=None): - state = State.objects.filter(workspace__slug=slug, project_id=project_id, pk=pk) + def patch(self, request, slug, project_id, state_id=None): + state = State.objects.filter(workspace__slug=slug, project_id=project_id, pk=state_id) serializer = StateSerializer(state, data=request.data, partial=True) if serializer.is_valid(): serializer.save() diff --git a/apiserver/plane/app/views/base.py b/apiserver/plane/app/views/base.py index de7bafd57..7e3aa82b9 100644 --- a/apiserver/plane/app/views/base.py +++ b/apiserver/plane/app/views/base.py @@ -46,19 +46,22 @@ class WebhookMixin: def finalize_response(self, request, response, *args, **kwargs): response = super().finalize_response(request, response, *args, **kwargs) - if ( self.webhook_event - and self.request.method in ["POST", "PATCH", "DELETE"] + and self.request.method in ["POST", "PATCH"] and response.status_code in [200, 201, 204] ): + # Get the id + object_id = ( + response.data.get("id") if isinstance(response.data, dict) else None + ) + send_webhook.delay( event=self.webhook_event, - event_data=json.dumps(response.data, cls=DjangoJSONEncoder), + event_id=object_id, action=self.request.method, slug=self.workspace_slug, ) - return response diff --git a/apiserver/plane/app/views/cycle.py b/apiserver/plane/app/views/cycle.py index 7228aa088..5a2cf6807 100644 --- a/apiserver/plane/app/views/cycle.py +++ b/apiserver/plane/app/views/cycle.py @@ -688,7 +688,6 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet): pk=pk, workspace__slug=slug, project_id=project_id, cycle_id=cycle_id ) issue_id = cycle_issue.issue_id - cycle_issue.delete() issue_activity.delay( type="cycle.activity.deleted", requested_data=json.dumps( @@ -698,11 +697,12 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet): } ), actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("pk", None)), + issue_id=str(cycle_issue.issue_id), project_id=str(self.kwargs.get("project_id", None)), current_instance=None, epoch=int(timezone.now().timestamp()), ) + cycle_issue.delete() return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/module.py b/apiserver/plane/app/views/module.py index 28986ea0f..4e10a0629 100644 --- a/apiserver/plane/app/views/module.py +++ b/apiserver/plane/app/views/module.py @@ -461,7 +461,6 @@ class ModuleIssueViewSet(BaseViewSet): module_issue = ModuleIssue.objects.get( workspace__slug=slug, project_id=project_id, module_id=module_id, pk=pk ) - module_issue.delete() issue_activity.delay( type="module.activity.deleted", requested_data=json.dumps( @@ -471,11 +470,12 @@ class ModuleIssueViewSet(BaseViewSet): } ), actor_id=str(request.user.id), - issue_id=str(pk), + issue_id=str(module_issue.issue_id), project_id=str(project_id), current_instance=None, epoch=int(timezone.now().timestamp()), ) + module_issue.delete() return Response(status=status.HTTP_204_NO_CONTENT)