From fc4d06fe0cc642fc9419f4a098045872c78e075f Mon Sep 17 00:00:00 2001 From: kunalv17 <116634168+kunalv17@users.noreply.github.com> Date: Tue, 28 Mar 2023 14:17:24 +0530 Subject: [PATCH 1/3] style: changed the workspace cards height (#558) --- apps/app/components/workspace/issues-list.tsx | 2 +- apps/app/components/workspace/issues-pie-chart.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/app/components/workspace/issues-list.tsx b/apps/app/components/workspace/issues-list.tsx index 8a42d8b34..8308baede 100644 --- a/apps/app/components/workspace/issues-list.tsx +++ b/apps/app/components/workspace/issues-list.tsx @@ -36,7 +36,7 @@ export const IssuesList: React.FC = ({ issues, type }) => {

{type} Issues

{issues ? ( -
+
= ({ groupedIssues }) => {

Issues by States

- + Date: Tue, 28 Mar 2023 14:49:27 +0530 Subject: [PATCH 2/3] feat: add global search through cmd k (#560) * feat: cmdk integration * feat: create view, cycle, project, module and workspace from command k * feat: user can logout directly from command menu * feat: user can visit sub page like various settings * feat: change state of issue from command menu * chore: add current issue state and minor UX improvements * refactor: moved change issue state to new file * feat: change issue priority from command k * feat: delete issue from command k * feat: copy issue url to clipboard * fix: change placeholder when settings page is selected * chore: remove logout option from cmd k * feat: add help options to cmd k * feat: assign issue to member from cmd k * feat: now assign issue to yourself from cmd k * chore: implement new cmd k design with icons * feat: implemented global search feature in the cmd k * feat: add keyboard acessibility to cmd k list items * chore: remove console logs * fix: pages icon in cmd list --------- Co-authored-by: Aaryan Khandelwal --- .../command-palette/change-issue-assignee.tsx | 109 +++ .../command-palette/change-issue-priority.tsx | 75 ++ .../command-palette/change-issue-state.tsx | 96 ++ .../command-palette/command-pallette.tsx | 874 +++++++++++++----- apps/app/components/command-palette/index.ts | 3 + apps/app/package.json | 1 + apps/app/pages/_app.tsx | 1 + apps/app/services/workspace.service.ts | 15 + apps/app/styles/command-pallette.css | 35 + apps/app/types/workspace.d.ts | 32 + yarn.lock | 224 ++++- 11 files changed, 1256 insertions(+), 209 deletions(-) create mode 100644 apps/app/components/command-palette/change-issue-assignee.tsx create mode 100644 apps/app/components/command-palette/change-issue-priority.tsx create mode 100644 apps/app/components/command-palette/change-issue-state.tsx create mode 100644 apps/app/styles/command-pallette.css diff --git a/apps/app/components/command-palette/change-issue-assignee.tsx b/apps/app/components/command-palette/change-issue-assignee.tsx new file mode 100644 index 000000000..54e9a4f21 --- /dev/null +++ b/apps/app/components/command-palette/change-issue-assignee.tsx @@ -0,0 +1,109 @@ +import { useRouter } from "next/router"; +import React, { Dispatch, SetStateAction, useCallback } from "react"; +import useSWR, { mutate } from "swr"; + +// cmdk +import { Command } from "cmdk"; +// services +import issuesService from "services/issues.service"; +// types +import { IIssue } from "types"; +// constants +import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, PROJECT_MEMBERS } from "constants/fetch-keys"; +// icons +import { CheckIcon } from "components/icons"; +import projectService from "services/project.service"; +import { Avatar } from "components/ui"; + +type Props = { + setIsPaletteOpen: Dispatch>; + issue: IIssue; +}; + +export const ChangeIssueAssignee: React.FC = ({ setIsPaletteOpen, issue }) => { + const router = useRouter(); + const { workspaceSlug, projectId, issueId } = router.query; + + const { data: members } = useSWR( + projectId ? PROJECT_MEMBERS(projectId as string) : null, + workspaceSlug && projectId + ? () => projectService.projectMembers(workspaceSlug as string, projectId as string) + : null + ); + + const options = + members?.map(({ member }) => ({ + value: member.id, + query: + (member.first_name && member.first_name !== "" ? member.first_name : member.email) + + " " + + member.last_name ?? "", + content: ( + <> +
+ + {member.first_name && member.first_name !== "" ? member.first_name : member.email} +
+ {issue.assignees.includes(member.id) && ( +
+ +
+ )} + + ), + })) ?? []; + + const updateIssue = useCallback( + async (formData: Partial) => { + if (!workspaceSlug || !projectId || !issueId) return; + + mutate( + ISSUE_DETAILS(issueId as string), + (prevData: IIssue) => ({ + ...prevData, + ...formData, + }), + false + ); + + const payload = { ...formData }; + await issuesService + .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_list: updatedAssignees }); + setIsPaletteOpen(false); + }; + + return ( + <> + {options.map((option) => ( + handleIssueAssignees(option.value)} + className="focus:bg-slate-200 focus:outline-none" + tabIndex={0} + > + {option.content} + + ))} + + ); +}; diff --git a/apps/app/components/command-palette/change-issue-priority.tsx b/apps/app/components/command-palette/change-issue-priority.tsx new file mode 100644 index 000000000..4c8661131 --- /dev/null +++ b/apps/app/components/command-palette/change-issue-priority.tsx @@ -0,0 +1,75 @@ +import { useRouter } from "next/router"; +import React, { Dispatch, SetStateAction, useCallback } from "react"; +import { mutate } from "swr"; + +// cmdk +import { Command } from "cmdk"; +// services +import issuesService from "services/issues.service"; +// types +import { IIssue } from "types"; +// constants +import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys"; +import { PRIORITIES } from "constants/project"; +// icons +import { CheckIcon, getPriorityIcon } from "components/icons"; + +type Props = { + setIsPaletteOpen: Dispatch>; + issue: IIssue; +}; + +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), + (prevData: IIssue) => ({ + ...prevData, + ...formData, + }), + false + ); + + const payload = { ...formData }; + await issuesService + .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: string | null) => { + submitChanges({ priority }); + setIsPaletteOpen(false); + }; + + return ( + <> + {PRIORITIES.map((priority) => ( + handleIssueState(priority)} + className="focus:bg-slate-200 focus:outline-none" + tabIndex={0} + > +
+ {getPriorityIcon(priority)} + {priority ?? "None"} +
+
{priority === issue.priority && }
+
+ ))} + + ); +}; diff --git a/apps/app/components/command-palette/change-issue-state.tsx b/apps/app/components/command-palette/change-issue-state.tsx new file mode 100644 index 000000000..a2d06050d --- /dev/null +++ b/apps/app/components/command-palette/change-issue-state.tsx @@ -0,0 +1,96 @@ +import { useRouter } from "next/router"; +import React, { Dispatch, SetStateAction, useCallback } from "react"; +import useSWR, { mutate } from "swr"; + +// cmdk +import { Command } from "cmdk"; +// ui +import { Spinner } from "components/ui"; +// helpers +import { getStatesList } from "helpers/state.helper"; +// services +import issuesService from "services/issues.service"; +import stateService from "services/state.service"; +// types +import { IIssue } from "types"; +// fetch keys +import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, STATE_LIST } from "constants/fetch-keys"; +// icons +import { CheckIcon, getStateGroupIcon } from "components/icons"; + +type Props = { + setIsPaletteOpen: Dispatch>; + issue: IIssue; +}; + +export const ChangeIssueState: React.FC = ({ setIsPaletteOpen, issue }) => { + const router = useRouter(); + const { workspaceSlug, projectId, issueId } = router.query; + + const { data: stateGroups, mutate: mutateIssueDetails } = useSWR( + workspaceSlug && projectId ? STATE_LIST(projectId as string) : null, + workspaceSlug && projectId + ? () => stateService.getStates(workspaceSlug as string, projectId as string) + : null + ); + const states = getStatesList(stateGroups ?? {}); + + const submitChanges = useCallback( + async (formData: Partial) => { + if (!workspaceSlug || !projectId || !issueId) return; + + mutate( + ISSUE_DETAILS(issueId as string), + (prevData: IIssue) => ({ + ...prevData, + ...formData, + }), + false + ); + + const payload = { ...formData }; + await issuesService + .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload) + .then(() => { + mutateIssueDetails(); + mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); + }) + .catch((e) => { + console.error(e); + }); + }, + [workspaceSlug, issueId, projectId, mutateIssueDetails] + ); + + const handleIssueState = (stateId: string) => { + submitChanges({ state: stateId }); + setIsPaletteOpen(false); + }; + + return ( + <> + {states ? ( + states.length > 0 ? ( + states.map((state) => ( + handleIssueState(state.id)} + className="focus:bg-slate-200 focus:outline-none" + tabIndex={0} + > +
+ {getStateGroupIcon(state.group, "16", "16", state.color)} +

{state.name}

+
+
{state.id === issue.state && }
+
+ )) + ) : ( +
No states found
+ ) + ) : ( + + )} + + ); +}; diff --git a/apps/app/components/command-palette/command-pallette.tsx b/apps/app/components/command-palette/command-pallette.tsx index 714e39164..aa834c51b 100644 --- a/apps/app/components/command-palette/command-pallette.tsx +++ b/apps/app/components/command-palette/command-pallette.tsx @@ -1,100 +1,174 @@ -import React, { useState, useCallback, useEffect } from "react"; - import { useRouter } from "next/router"; +import React, { useCallback, useEffect, useState } from "react"; +import useSWR, { mutate } from "swr"; -import useSWR from "swr"; - +// icons +import { + ArrowRightIcon, + ChartBarIcon, + ClipboardIcon, + FolderPlusIcon, + InboxIcon, + MagnifyingGlassIcon, + Squares2X2Icon, + TrashIcon, + UserMinusIcon, + UserPlusIcon, + UsersIcon, +} from "@heroicons/react/24/outline"; +import { + AssignmentClipboardIcon, + BoltIcon, + ContrastIcon, + DiscordIcon, + DocumentIcon, + GithubIcon, + LayerDiagonalIcon, + PeopleGroupIcon, + SettingIcon, + ViewListIcon, + PencilScribbleIcon +} from "components/icons"; // headless ui -import { Combobox, Dialog, Transition } from "@headlessui/react"; -// services -import userService from "services/user.service"; +import { Dialog, Transition } from "@headlessui/react"; +// cmdk +import { Command } from "cmdk"; // hooks import useTheme from "hooks/use-theme"; import useToast from "hooks/use-toast"; import useUser from "hooks/use-user"; // components -import { ShortcutsModal } from "components/command-palette"; -import { BulkDeleteIssuesModal } from "components/core"; -import { CreateProjectModal } from "components/project"; -import { CreateUpdateIssueModal } from "components/issues"; -import { CreateUpdateCycleModal } from "components/cycles"; -import { CreateUpdateModuleModal } from "components/modules"; -// ui -import { SecondaryButton } from "components/ui"; -// icons import { - FolderIcon, - RectangleStackIcon, - ClipboardDocumentListIcon, - MagnifyingGlassIcon, -} from "@heroicons/react/24/outline"; + ShortcutsModal, + ChangeIssueState, + ChangeIssuePriority, + ChangeIssueAssignee, +} from "components/command-palette"; +import { BulkDeleteIssuesModal } from "components/core"; +import { CreateUpdateCycleModal } from "components/cycles"; +import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; +import { CreateUpdateModuleModal } from "components/modules"; +import { CreateProjectModal } from "components/project"; +import { CreateUpdateViewModal } from "components/views"; // helpers -import { copyTextToClipboard } from "helpers/string.helper"; +import { + capitalizeFirstLetter, + copyTextToClipboard, + replaceUnderscoreIfSnakeCase, +} from "helpers/string.helper"; +// services +import issuesService from "services/issues.service"; // types -import { IIssue } from "types"; -// fetch-keys -import { USER_ISSUE } from "constants/fetch-keys"; +import { IIssue, IWorkspaceSearchResults } from "types"; +// fetch keys +import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys"; +import useDebounce from "hooks/use-debounce"; +import workspaceService from "services/workspace.service"; export const CommandPalette: React.FC = () => { - const [query, setQuery] = useState(""); - const [isPaletteOpen, setIsPaletteOpen] = useState(false); const [isIssueModalOpen, setIsIssueModalOpen] = useState(false); const [isProjectModalOpen, setIsProjectModalOpen] = useState(false); const [isShortcutsModalOpen, setIsShortcutsModalOpen] = useState(false); const [isCreateCycleModalOpen, setIsCreateCycleModalOpen] = useState(false); + const [isCreateViewModalOpen, setIsCreateViewModalOpen] = useState(false); const [isCreateModuleModalOpen, setIsCreateModuleModalOpen] = useState(false); const [isBulkDeleteIssuesModalOpen, setIsBulkDeleteIssuesModalOpen] = useState(false); + const [deleteIssueModal, setDeleteIssueModal] = useState(false); + + const [searchTerm, setSearchTerm] = React.useState(""); + const [results, setResults] = useState({ + results: { + workspace: [], + project: [], + issue: [], + cycle: [], + module: [], + issue_view: [], + page: [], + }, + }); + const [isPendingAPIRequest, setIsPendingAPIRequest] = useState(false); + const debouncedSearchTerm = useDebounce(searchTerm, 500); + const [placeholder, setPlaceholder] = React.useState("Type a command or search..."); + const [pages, setPages] = React.useState([]); + const page = pages[pages.length - 1]; const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug, projectId, issueId } = router.query; const { user } = useUser(); const { setToastAlert } = useToast(); const { toggleCollapsed } = useTheme(); - const { data: myIssues } = useSWR( - workspaceSlug ? USER_ISSUE(workspaceSlug as string) : null, - workspaceSlug ? () => userService.userIssues(workspaceSlug as string) : null + const { data: issueDetails } = useSWR( + workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null, + workspaceSlug && projectId && issueId + ? () => + issuesService.retrieve(workspaceSlug as string, projectId as string, issueId as string) + : null ); - const filteredIssues: IIssue[] = - query === "" - ? myIssues ?? [] - : myIssues?.filter( - (issue) => - issue.name.toLowerCase().includes(query.toLowerCase()) || - `${issue.project_detail.identifier}-${issue.sequence_id}` - .toLowerCase() - .includes(query.toLowerCase()) - ) ?? []; + const updateIssue = useCallback( + async (formData: Partial) => { + if (!workspaceSlug || !projectId || !issueId) return; - const quickActions = [ - { - name: "Add new issue...", - icon: RectangleStackIcon, - hide: !projectId, - shortcut: "C", - onClick: () => { - setIsIssueModalOpen(true); - }, - }, - { - name: "Add new project...", - icon: ClipboardDocumentListIcon, - hide: !workspaceSlug, - shortcut: "P", - onClick: () => { - setIsProjectModalOpen(true); - }, - }, - ]; + mutate( + ISSUE_DETAILS(issueId as string), + (prevData: IIssue) => ({ + ...prevData, + ...formData, + }), + false + ); + + const payload = { ...formData }; + await issuesService + .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; - const handleCommandPaletteClose = () => { setIsPaletteOpen(false); - setQuery(""); + const updatedAssignees = issueDetails.assignees ?? []; + + if (updatedAssignees.includes(assignee)) { + updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1); + } else { + updatedAssignees.push(assignee); + } + updateIssue({ assignees_list: updatedAssignees }); }; + 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]); + const handleKeyDown = useCallback( (e: KeyboardEvent) => { if ( @@ -108,22 +182,7 @@ export const CommandPalette: React.FC = () => { } else if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "c") { if (e.altKey) { e.preventDefault(); - 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", - }); - }); + copyIssueUrlToClipboard(); } } else if (e.key.toLowerCase() === "c") { e.preventDefault(); @@ -149,7 +208,7 @@ export const CommandPalette: React.FC = () => { } } }, - [toggleCollapsed, setToastAlert, router] + [toggleCollapsed, copyIssueUrlToClipboard] ); useEffect(() => { @@ -157,8 +216,69 @@ export const CommandPalette: React.FC = () => { return () => document.removeEventListener("keydown", handleKeyDown); }, [handleKeyDown]); + useEffect( + () => { + if (!workspaceSlug || !projectId) return; + + // this is done prevent api request when user is clearing input + // or searchTerm has not been updated within last 500ms. + if (debouncedSearchTerm) { + setIsPendingAPIRequest(true); + workspaceService + .searchWorkspace(workspaceSlug as string, projectId as string, debouncedSearchTerm) + .then((results) => { + setIsPendingAPIRequest(false); + setResults(results); + }); + } else { + setIsPendingAPIRequest(false); + } + }, + [debouncedSearchTerm, workspaceSlug, projectId] // Only call effect if debounced search term changes + ); + if (!user) return null; + const createNewWorkspace = () => { + setIsPaletteOpen(false); + router.push("/create-workspace"); + }; + + const createNewProject = () => { + setIsPaletteOpen(false); + setIsProjectModalOpen(true); + }; + + const createNewIssue = () => { + setIsPaletteOpen(false); + setIsIssueModalOpen(true); + }; + + const createNewCycle = () => { + setIsPaletteOpen(false); + setIsCreateCycleModalOpen(true); + }; + + const createNewView = () => { + setIsPaletteOpen(false); + setIsCreateViewModalOpen(true); + }; + + const createNewModule = () => { + setIsPaletteOpen(false); + setIsCreateModuleModalOpen(true); + }; + + const deleteIssue = () => { + setIsPaletteOpen(false); + setDeleteIssueModal(true); + }; + + const goToSettings = (path: string = "") => { + setIsPaletteOpen(false); + router.push(`/${workspaceSlug}/settings/${path}`); + }; + return ( <> @@ -175,8 +295,20 @@ export const CommandPalette: React.FC = () => { isOpen={isCreateModuleModalOpen} setIsOpen={setIsCreateModuleModalOpen} /> + setIsCreateViewModalOpen(false)} + isOpen={isCreateViewModalOpen} + /> )} + {issueId && issueDetails && ( + setDeleteIssueModal(false)} + isOpen={deleteIssueModal} + data={issueDetails} + /> + )} + setIsIssueModalOpen(false)} @@ -187,11 +319,12 @@ export const CommandPalette: React.FC = () => { /> { + setSearchTerm(""); + }} as={React.Fragment} - afterLeave={() => setQuery("")} - appear > - + setIsPaletteOpen(false)}> {
-
+
- { - if (value?.url) router.push(value.url); - else if (value?.onClick) value.onClick(); - handleCommandPaletteClose(); + { + if (value.toLowerCase().includes(search.toLowerCase())) return 1; + return 0; + }} + onKeyDown={(e) => { + // when seach is empty and page is undefined + // when user tries to close the modal with esc + if (e.key === "Escape" && !page && !searchTerm) { + setIsPaletteOpen(false); + } + // Escape goes to previous page + // Backspace goes to previous page when search is empty + if (e.key === "Escape" || (e.key === "Backspace" && !searchTerm)) { + e.preventDefault(); + setPages((pages) => pages.slice(0, -1)); + setPlaceholder("Type a command or search..."); + } }} > -
+ {issueId && issueDetails && ( +
+ + {issueDetails.project_detail?.identifier}-{issueDetails.sequence_id}{" "} + {issueDetails?.name} + +
+ )} +
+ + + No results found. + - - {filteredIssues.length > 0 && ( + {debouncedSearchTerm !== "" && ( <> -
  • - {query === "" && ( -

    - Select issues -

    - )} -
      - {filteredIssues.map((issue) => ( - - `flex cursor-pointer select-none items-center justify-between rounded-md px-3 py-2 ${ - active ? "bg-gray-500 bg-opacity-5 text-gray-900" : "" - }` - } + {Object.keys(results.results).map((key) => { + const section = (results.results as any)[key]; + if (section.length > 0) { + return ( + - {({ active }) => ( - <> -
      - - - {issue.project_detail?.identifier}-{issue.sequence_id} - - {issue.name} -
      - - - )} -
      - ))} -
    -
  • +
    + + {item.name} +
    + + ); + })} + + ); + } + })} )} - {query === "" && ( -
  • -

    Quick actions

    -
      - {quickActions.map( - (action) => - !action.hide && ( - - `flex cursor-default select-none items-center rounded-md px-3 py-2 ${ - active ? "bg-gray-500 bg-opacity-5 text-gray-900" : "" - }` - } - > - {({ active }) => ( - <> - - ) - )} -
    -
  • + + {!page && ( + <> + {issueId && ( + <> + { + setPlaceholder("Change state..."); + setSearchTerm(""); + setPages([...pages, "change-issue-state"]); + }} + className="focus:bg-slate-200 focus:outline-none" + tabIndex={0} + > +
    + + Change state... +
    +
    + { + setPlaceholder("Change priority..."); + setSearchTerm(""); + setPages([...pages, "change-issue-priority"]); + }} + className="focus:bg-slate-200 focus:outline-none" + tabIndex={0} + > +
    + + Change priority... +
    +
    + { + setPlaceholder("Assign to..."); + setSearchTerm(""); + setPages([...pages, "change-issue-assignee"]); + }} + className="focus:bg-slate-200 focus:outline-none" + tabIndex={0} + > +
    + + Assign to... +
    +
    + { + handleIssueAssignees(user.id); + setSearchTerm(""); + }} + className="focus:bg-slate-200 focus:outline-none" + tabIndex={0} + > +
    + {issueDetails?.assignees.includes(user.id) ? ( + <> + + Un-assign from me + + ) : ( + <> + + Assign to me + + )} +
    +
    + + +
    + + Delete issue +
    +
    + { + setIsPaletteOpen(false); + copyIssueUrlToClipboard(); + }} + className="focus:bg-slate-200 focus:outline-none" + tabIndex={0} + > +
    + + Copy issue URL to clipboard +
    +
    + + )} + + +
    + + Create new issue +
    + C +
    +
    + + {workspaceSlug && ( + + +
    + + Create new project +
    + P +
    +
    + )} + + {projectId && ( + <> + + +
    + + Create new cycle +
    + Q +
    +
    + + + +
    + + Create new module +
    + M +
    +
    + + + +
    + + Create new view +
    + Q +
    +
    + + )} + + + { + setPlaceholder("Search workspace settings..."); + setSearchTerm(""); + setPages([...pages, "settings"]); + }} + className="focus:bg-slate-200 focus:outline-none" + tabIndex={0} + > +
    + + Search settings... +
    +
    +
    + + +
    + + Create new workspace +
    +
    +
    + + { + setIsPaletteOpen(false); + const e = new KeyboardEvent("keydown", { + key: "h", + }); + document.dispatchEvent(e); + }} + className="focus:bg-slate-200 focus:outline-none" + tabIndex={0} + > +
    + + Open keyboard shortcuts +
    +
    + { + setIsPaletteOpen(false); + window.open("https://docs.plane.so/", "_blank"); + }} + className="focus:bg-slate-200 focus:outline-none" + tabIndex={0} + > +
    + + Open Plane documentation +
    +
    + { + setIsPaletteOpen(false); + window.open("https://discord.com/invite/A92xrEGCge", "_blank"); + }} + className="focus:bg-slate-200 focus:outline-none" + tabIndex={0} + > +
    + + Join our discord +
    +
    + { + setIsPaletteOpen(false); + window.open( + "https://github.com/makeplane/plane/issues/new/choose", + "_blank" + ); + }} + className="focus:bg-slate-200 focus:outline-none" + tabIndex={0} + > +
    + + Report a bug +
    +
    + { + setIsPaletteOpen(false); + window.open("mailto:hello@plane.so", "_blank"); + }} + className="focus:bg-slate-200 focus:outline-none" + tabIndex={0} + > +
    + + Email us +
    +
    +
    + )} -
    - {query !== "" && filteredIssues.length === 0 && ( -
    -
    - )} - - -
    - Close -
    + )} + {page === "change-issue-assignee" && issueDetails && ( + + )} +
    +
    diff --git a/apps/app/components/command-palette/index.ts b/apps/app/components/command-palette/index.ts index 542d69214..c1d79a2c4 100644 --- a/apps/app/components/command-palette/index.ts +++ b/apps/app/components/command-palette/index.ts @@ -1,2 +1,5 @@ export * from "./command-pallette"; export * from "./shortcuts-modal"; +export * from "./change-issue-state"; +export * from "./change-issue-priority"; +export * from "./change-issue-assignee"; diff --git a/apps/app/package.json b/apps/app/package.json index bbaf59ae8..6940377dc 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -21,6 +21,7 @@ "@types/lodash.debounce": "^4.0.7", "@types/react-datepicker": "^4.8.0", "axios": "^1.1.3", + "cmdk": "^0.2.0", "js-cookie": "^3.0.1", "lodash.debounce": "^4.0.8", "next": "12.3.2", diff --git a/apps/app/pages/_app.tsx b/apps/app/pages/_app.tsx index 617913e98..0d3bf3105 100644 --- a/apps/app/pages/_app.tsx +++ b/apps/app/pages/_app.tsx @@ -3,6 +3,7 @@ import dynamic from "next/dynamic"; // styles import "styles/globals.css"; import "styles/editor.css"; +import "styles/command-pallette.css"; import "styles/nprogress.css"; // router diff --git a/apps/app/services/workspace.service.ts b/apps/app/services/workspace.service.ts index 658f713a4..e7d10d2f8 100644 --- a/apps/app/services/workspace.service.ts +++ b/apps/app/services/workspace.service.ts @@ -11,6 +11,7 @@ import { ILastActiveWorkspaceDetails, IAppIntegrations, IWorkspaceIntegrations, + IWorkspaceSearchResults, } from "types"; class WorkspaceService extends APIService { @@ -197,6 +198,20 @@ class WorkspaceService extends APIService { throw error?.response?.data; }); } + + async searchWorkspace( + workspaceSlug: string, + projectId: string, + query: string + ): Promise { + return this.get( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/search/?search=${query}` + ) + .then((res) => res?.data) + .catch((error) => { + throw error?.response?.data; + }); + } } export default new WorkspaceService(); diff --git a/apps/app/styles/command-pallette.css b/apps/app/styles/command-pallette.css new file mode 100644 index 000000000..00f4e6483 --- /dev/null +++ b/apps/app/styles/command-pallette.css @@ -0,0 +1,35 @@ +[cmdk-group]:not(:first-child) { + margin-top: 0.5rem; +} + +[cmdk-group-heading] { + color: rgb(107 114 128); + font-size: 0.75rem; + margin: 0 0 0.25rem 0.25rem; +} + +[cmdk-item] { + display: flex; + align-items: center; + justify-content: space-between; + border-radius: 0.375rem; + padding: 0.5rem; + font-size: 0.825rem; + line-height: 1.25rem; + cursor: pointer; +} + +[cmdk-item] kbd { + height: 1.25rem; + width: 1.25rem; + display: grid; + place-items: center; + font-size: 0.75rem; + line-height: 1rem; + border-radius: 0.25rem; + background-color: rgb(229 231 235); +} + +[cmdk-item]:hover { + background-color: rgb(243 244 246); +} diff --git a/apps/app/types/workspace.d.ts b/apps/app/types/workspace.d.ts index 2871f31df..6cfd12317 100644 --- a/apps/app/types/workspace.d.ts +++ b/apps/app/types/workspace.d.ts @@ -44,3 +44,35 @@ export interface ILastActiveWorkspaceDetails { workspace_details: IWorkspace; project_details?: IProjectMember[]; } + +export interface IWorkspaceDefaultSearchResult { + name: string; + id: string; + project_id: string; + workspace__slug: string; +} +export interface IWorkspaceSearchResult { + name: string; + id: string; + slug: string; +} + +export interface IWorkspaceIssueSearchResult { + name: string; + id: string; + sequence_id: number; + project__identifier: string; + project_id: string; + workspace__slug: string; +} +export interface IWorkspaceSearchResults { + results: { + workspace: IWorkspaceSearchResult[]; + project: IWorkspaceDefaultSearchResult[]; + issue: IWorkspaceIssueSearchResult[]; + cycle: IWorkspaceDefaultSearchResult[]; + module: IWorkspaceDefaultSearchResult[]; + issue_view: IWorkspaceDefaultSearchResult[]; + page: IWorkspaceDefaultSearchResult[]; + }; +} diff --git a/yarn.lock b/yarn.lock index 171bd6e39..89feb625f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1766,6 +1766,148 @@ resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz" integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw== +"@radix-ui/primitive@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.0.0.tgz#e1d8ef30b10ea10e69c76e896f608d9276352253" + integrity sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-compose-refs@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz#37595b1f16ec7f228d698590e78eeed18ff218ae" + integrity sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-context@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.0.tgz#f38e30c5859a9fb5e9aa9a9da452ee3ed9e0aee0" + integrity sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-dialog@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.0.0.tgz#997e97cb183bc90bd888b26b8e23a355ac9fe5f0" + integrity sha512-Yn9YU+QlHYLWwV1XfKiqnGVpWYWk6MeBVM6x/bcoyPvxgjQGoeT35482viLPctTMWoMw0PoHgqfSox7Ig+957Q== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.0" + "@radix-ui/react-compose-refs" "1.0.0" + "@radix-ui/react-context" "1.0.0" + "@radix-ui/react-dismissable-layer" "1.0.0" + "@radix-ui/react-focus-guards" "1.0.0" + "@radix-ui/react-focus-scope" "1.0.0" + "@radix-ui/react-id" "1.0.0" + "@radix-ui/react-portal" "1.0.0" + "@radix-ui/react-presence" "1.0.0" + "@radix-ui/react-primitive" "1.0.0" + "@radix-ui/react-slot" "1.0.0" + "@radix-ui/react-use-controllable-state" "1.0.0" + aria-hidden "^1.1.1" + react-remove-scroll "2.5.4" + +"@radix-ui/react-dismissable-layer@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.0.tgz#35b7826fa262fd84370faef310e627161dffa76b" + integrity sha512-n7kDRfx+LB1zLueRDvZ1Pd0bxdJWDUZNQ/GWoxDn2prnuJKRdxsjulejX/ePkOsLi2tTm6P24mDqlMSgQpsT6g== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.0" + "@radix-ui/react-compose-refs" "1.0.0" + "@radix-ui/react-primitive" "1.0.0" + "@radix-ui/react-use-callback-ref" "1.0.0" + "@radix-ui/react-use-escape-keydown" "1.0.0" + +"@radix-ui/react-focus-guards@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.0.tgz#339c1c69c41628c1a5e655f15f7020bf11aa01fa" + integrity sha512-UagjDk4ijOAnGu4WMUPj9ahi7/zJJqNZ9ZAiGPp7waUWJO0O1aWXi/udPphI0IUjvrhBsZJGSN66dR2dsueLWQ== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-focus-scope@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.0.tgz#95a0c1188276dc8933b1eac5f1cdb6471e01ade5" + integrity sha512-C4SWtsULLGf/2L4oGeIHlvWQx7Rf+7cX/vKOAD2dXW0A1b5QXwi3wWeaEgW+wn+SEVrraMUk05vLU9fZZz5HbQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "1.0.0" + "@radix-ui/react-primitive" "1.0.0" + "@radix-ui/react-use-callback-ref" "1.0.0" + +"@radix-ui/react-id@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.0.0.tgz#8d43224910741870a45a8c9d092f25887bb6d11e" + integrity sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-layout-effect" "1.0.0" + +"@radix-ui/react-portal@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.0.tgz#7220b66743394fabb50c55cb32381395cc4a276b" + integrity sha512-a8qyFO/Xb99d8wQdu4o7qnigNjTPG123uADNecz0eX4usnQEj7o+cG4ZX4zkqq98NYekT7UoEQIjxBNWIFuqTA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-primitive" "1.0.0" + +"@radix-ui/react-presence@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.0.tgz#814fe46df11f9a468808a6010e3f3ca7e0b2e84a" + integrity sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "1.0.0" + "@radix-ui/react-use-layout-effect" "1.0.0" + +"@radix-ui/react-primitive@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.0.tgz#376cd72b0fcd5e0e04d252ed33eb1b1f025af2b0" + integrity sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-slot" "1.0.0" + +"@radix-ui/react-slot@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.0.tgz#7fa805b99891dea1e862d8f8fbe07f4d6d0fd698" + integrity sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "1.0.0" + +"@radix-ui/react-use-callback-ref@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz#9e7b8b6b4946fe3cbe8f748c82a2cce54e7b6a90" + integrity sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-use-controllable-state@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.0.tgz#a64deaafbbc52d5d407afaa22d493d687c538b7f" + integrity sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-callback-ref" "1.0.0" + +"@radix-ui/react-use-escape-keydown@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.0.tgz#aef375db4736b9de38a5a679f6f49b45a060e5d1" + integrity sha512-JwfBCUIfhXRxKExgIqGa4CQsiMemo1Xt0W/B4ei3fpzpvPENKpMKQ8mZSB6Acj3ebrAEgi2xiQvcI1PAAodvyg== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-callback-ref" "1.0.0" + +"@radix-ui/react-use-layout-effect@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz#2fc19e97223a81de64cd3ba1dc42ceffd82374dc" + integrity sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@reach/auto-id@^0.16.0": version "0.16.0" resolved "https://registry.npmjs.org/@reach/auto-id/-/auto-id-0.16.0.tgz" @@ -3556,6 +3698,13 @@ argparse@^2.0.1: resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +aria-hidden@^1.1.1: + version "1.2.3" + resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.3.tgz#14aeb7fb692bbb72d69bebfa47279c1fd725e954" + integrity sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ== + dependencies: + tslib "^2.0.0" + aria-query@^5.1.3: version "5.1.3" resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz" @@ -3965,6 +4114,14 @@ clsx@^1.2.0, clsx@^1.2.1: resolved "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz" integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== +cmdk@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/cmdk/-/cmdk-0.2.0.tgz#53c52d56d8776c8bb8ced1055b5054100c388f7c" + integrity sha512-JQpKvEOb86SnvMZbYaFKYhvzFntWBeSZdyii0rZPhKJj9uwJBxu4DaVYDrRN7r3mPop56oPhRw+JYWTKs66TYw== + dependencies: + "@radix-ui/react-dialog" "1.0.0" + command-score "0.1.2" + code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz" @@ -4021,6 +4178,11 @@ comma-separated-tokens@^2.0.0: resolved "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz" integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== +command-score@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/command-score/-/command-score-0.1.2.tgz#b986ad7e8c0beba17552a56636c44ae38363d381" + integrity sha512-VtDvQpIJBvBatnONUsPzXYFVKQQAhuf3XTNOAsdBxCNO/QCtUUd8LSgjn0GVarBkCad6aJCZfXgrjYbl/KRr7w== + commander@^2.20.0: version "2.20.3" resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" @@ -4356,6 +4518,11 @@ dequal@^2.0.0: resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== +detect-node-es@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" + integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ== + detective@^5.2.1: version "5.2.1" resolved "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz" @@ -5341,6 +5508,11 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3: has "^1.0.3" has-symbols "^1.0.3" +get-nonce@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" + integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q== + get-own-enumerable-property-symbols@^3.0.0: version "3.0.2" resolved "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz" @@ -5680,6 +5852,13 @@ internal-slot@^1.0.3, internal-slot@^1.0.4: resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== +invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + is-alphabetical@^1.0.0: version "1.0.4" resolved "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz" @@ -7831,6 +8010,25 @@ react-redux@^7.2.0: prop-types "^15.7.2" react-is "^17.0.2" +react-remove-scroll-bar@^2.3.3: + version "2.3.4" + resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz#53e272d7a5cb8242990c7f144c44d8bd8ab5afd9" + integrity sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A== + dependencies: + react-style-singleton "^2.2.1" + tslib "^2.0.0" + +react-remove-scroll@2.5.4: + version "2.5.4" + resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.4.tgz#afe6491acabde26f628f844b67647645488d2ea0" + integrity sha512-xGVKJJr0SJGQVirVFAUZ2k1QLyO6m+2fy0l8Qawbp5Jgrv3DeLalrfMNBFSlmz5kriGGzsVBtGVnf4pTKIhhWA== + dependencies: + react-remove-scroll-bar "^2.3.3" + react-style-singleton "^2.2.1" + tslib "^2.1.0" + use-callback-ref "^1.3.0" + use-sidecar "^1.1.2" + react-resize-detector@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-7.1.2.tgz#8ef975dd8c3d56f9a5160ac382ef7136dcd2d86c" @@ -7846,6 +8044,15 @@ react-smooth@^2.0.1: fast-equals "^4.0.3" react-transition-group "2.9.0" +react-style-singleton@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" + integrity sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g== + dependencies: + get-nonce "^1.0.0" + invariant "^2.2.4" + tslib "^2.0.0" + react-transition-group@2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d" @@ -8875,7 +9082,7 @@ tslib@^1.8.1, tslib@^1.9.3: resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0: +tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0: version "2.5.0" resolved "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz" integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== @@ -9145,6 +9352,13 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +use-callback-ref@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.0.tgz#772199899b9c9a50526fedc4993fc7fa1f7e32d5" + integrity sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w== + dependencies: + tslib "^2.0.0" + use-isomorphic-layout-effect@^1.1.0, use-isomorphic-layout-effect@^1.1.1: version "1.1.2" resolved "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz" @@ -9162,6 +9376,14 @@ use-previous@^1.1.0: dependencies: use-isomorphic-layout-effect "^1.1.0" +use-sidecar@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2" + integrity sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw== + dependencies: + detect-node-es "^1.1.0" + tslib "^2.0.0" + use-sync-external-store@1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz" From b7b8d3914ac460679fd93f0a14f385de847439bf Mon Sep 17 00:00:00 2001 From: kunalv17 <116634168+kunalv17@users.noreply.github.com> Date: Tue, 28 Mar 2023 16:48:46 +0530 Subject: [PATCH 3/3] style: views list page, chore: views favotire (#562) --- .../components/modules/single-module-card.tsx | 61 ++++---- apps/app/components/views/index.ts | 1 + .../app/components/views/single-view-item.tsx | 132 ++++++++++++++++++ .../projects/[projectId]/views/index.tsx | 38 ++--- apps/app/services/modules.service.ts | 2 +- apps/app/services/views.service.ts | 32 +++++ apps/app/types/views.d.ts | 1 + 7 files changed, 201 insertions(+), 66 deletions(-) create mode 100644 apps/app/components/views/single-view-item.tsx diff --git a/apps/app/components/modules/single-module-card.tsx b/apps/app/components/modules/single-module-card.tsx index 1e0df412a..82e59cd41 100644 --- a/apps/app/components/modules/single-module-card.tsx +++ b/apps/app/components/modules/single-module-card.tsx @@ -55,29 +55,22 @@ export const SingleModuleCard: React.FC = ({ module, handleEditModule }) }; const handleAddToFavorites = () => { - if (!workspaceSlug && !projectId && !module) return; + if (!workspaceSlug || !projectId || !module) return; + + mutate( + MODULE_LIST(projectId as string), + (prevData) => + (prevData ?? []).map((m) => ({ + ...m, + is_favorite: m.id === module.id ? true : m.is_favorite, + })), + false + ); modulesService .addModuleToFavorites(workspaceSlug as string, projectId as string, { module: module.id, }) - .then(() => { - mutate( - MODULE_LIST(projectId as string), - (prevData) => - (prevData ?? []).map((m) => ({ - ...m, - is_favorite: m.id === module.id ? true : m.is_favorite, - })), - false - ); - - setToastAlert({ - type: "success", - title: "Success!", - message: "Successfully added the module to favorites.", - }); - }) .catch(() => { setToastAlert({ type: "error", @@ -88,26 +81,20 @@ export const SingleModuleCard: React.FC = ({ module, handleEditModule }) }; const handleRemoveFromFavorites = () => { - if (!workspaceSlug || !module) return; + if (!workspaceSlug || !projectId || !module) return; + + mutate( + MODULE_LIST(projectId as string), + (prevData) => + (prevData ?? []).map((m) => ({ + ...m, + is_favorite: m.id === module.id ? false : m.is_favorite, + })), + false + ); modulesService .removeModuleFromFavorites(workspaceSlug as string, projectId as string, module.id) - .then(() => { - mutate( - MODULE_LIST(projectId as string), - (prevData) => - (prevData ?? []).map((m) => ({ - ...m, - is_favorite: m.id === module.id ? false : m.is_favorite, - })), - false - ); - setToastAlert({ - type: "success", - title: "Success!", - message: "Successfully removed the module from favorites.", - }); - }) .catch(() => { setToastAlert({ type: "error", @@ -161,11 +148,11 @@ export const SingleModuleCard: React.FC = ({ module, handleEditModule }) {module?.status?.replace("-", " ")}
    {module.is_favorite ? ( - ) : ( - )} diff --git a/apps/app/components/views/index.ts b/apps/app/components/views/index.ts index cad0d63d8..7a6307e56 100644 --- a/apps/app/components/views/index.ts +++ b/apps/app/components/views/index.ts @@ -2,3 +2,4 @@ export * from "./delete-view-modal"; export * from "./form"; export * from "./modal"; export * from "./select-filters"; +export * from "./single-view-item" diff --git a/apps/app/components/views/single-view-item.tsx b/apps/app/components/views/single-view-item.tsx new file mode 100644 index 000000000..8b47d65c7 --- /dev/null +++ b/apps/app/components/views/single-view-item.tsx @@ -0,0 +1,132 @@ +import React, { useState } from "react"; + +import Link from "next/link"; +import { useRouter } from "next/router"; +import { IView } from "types"; + +// icons +import { TrashIcon, StarIcon } from "@heroicons/react/24/outline"; +import { StackedLayersIcon } from "components/icons"; + +//components +import { CustomMenu } from "components/ui"; + +import viewsService from "services/views.service"; + +import { mutate } from "swr"; + +import { VIEWS_LIST } from "constants/fetch-keys"; + +import useToast from "hooks/use-toast"; + +type Props = { + view: IView, + setSelectedView: React.Dispatch>, +}; + + +export const SingleViewItem: React.FC = ({ + view, + setSelectedView, +}) => { + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { setToastAlert } = useToast(); + + const handleAddToFavorites = () => { + if (!workspaceSlug || !projectId || !view) return; + + mutate( + VIEWS_LIST(projectId as string), + (prevData) => + (prevData ?? []).map((v) => ({ + ...v, + is_favorite: v.id === view.id ? true : v.is_favorite, + })), + false + ); + + viewsService + .addViewToFavorites(workspaceSlug as string, projectId as string, { + view: view.id, + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Couldn't add the view to favorites. Please try again.", + }); + }); + }; + + const handleRemoveFromFavorites = () => { + if (!workspaceSlug || !view) return; + + mutate( + VIEWS_LIST(projectId as string), + (prevData) => + (prevData ?? []).map((v) => ({ + ...v, + is_favorite: v.id === view.id ? false : v.is_favorite, + })), + false + ); + + viewsService + .removeViewFromFavorites(workspaceSlug as string, projectId as string, view.id) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Couldn't remove the view from favorites. Please try again.", + }); + }); + }; + + return ( +
    +
    +
    +
    + + + {view.name} + +
    +
    + { + view.is_favorite ? ( + + ) : ( + + ) + } + + { + setSelectedView(view); + }} + > + + + Delete + + + +
    +
    + {view?.description &&

    + {view.description} +

    } +
    +
    + ) +} diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx index f89dcf857..848073251 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx @@ -1,6 +1,5 @@ import React, { useState } from "react"; -import Link from "next/link"; import { useRouter } from "next/router"; import useSWR from "swr"; @@ -16,19 +15,18 @@ import projectService from "services/project.service"; import AppLayout from "layouts/app-layout"; // ui import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; -// icons -import { TrashIcon } from "@heroicons/react/24/outline"; + // image import emptyView from "public/empty-state/empty-view.svg"; // fetching keys import { PROJECT_DETAILS, VIEWS_LIST } from "constants/fetch-keys"; // components -import { CustomMenu, PrimaryButton, Loader, EmptyState } from "components/ui"; -import { DeleteViewModal, CreateUpdateViewModal } from "components/views"; +import { PrimaryButton, Loader, EmptyState } from "components/ui"; +import { DeleteViewModal, CreateUpdateViewModal, SingleViewItem } from "components/views"; + // types import { IView, UserAuth } from "types"; import type { NextPage, GetServerSidePropsContext } from "next"; -import { StackedLayersIcon } from "components/icons"; const ProjectViews: NextPage = (props) => { const [isCreateViewModalOpen, setIsCreateViewModalOpen] = useState(false); @@ -52,6 +50,8 @@ const ProjectViews: NextPage = (props) => { : null ); + console.log(views) + return ( = (props) => {

    Views

    {views.map((view) => ( -
    -
    - - - {view.name} - -
    - - { - setSelectedView(view); - }} - > - - - Delete - - - -
    + view={view} + setSelectedView={setSelectedView} + /> ))}
    diff --git a/apps/app/services/modules.service.ts b/apps/app/services/modules.service.ts index e1cde9cc9..29a1f02be 100644 --- a/apps/app/services/modules.service.ts +++ b/apps/app/services/modules.service.ts @@ -1,7 +1,7 @@ // services import APIService from "services/api.service"; // types -import type { IIssueViewOptions, IModule, ModuleIssueResponse, IIssue } from "types"; +import type { IIssueViewOptions, IModule, IIssue } from "types"; const { NEXT_PUBLIC_API_BASE_URL } = process.env; diff --git a/apps/app/services/views.service.ts b/apps/app/services/views.service.ts index a3bdf550c..80699d994 100644 --- a/apps/app/services/views.service.ts +++ b/apps/app/services/views.service.ts @@ -80,6 +80,38 @@ class ViewServices extends APIService { throw error?.response?.data; }); } + + async addViewToFavorites( + workspaceSlug: string, + projectId: string, + data: { + view: string; + } + ): Promise { + return this.post( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-views/`, + data + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async removeViewFromFavorites( + workspaceSlug: string, + projectId: string, + viewId: string + ): Promise { + return this.delete( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-views/${viewId}/` + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + } export default new ViewServices(); diff --git a/apps/app/types/views.d.ts b/apps/app/types/views.d.ts index f5cef0126..f599d2f6d 100644 --- a/apps/app/types/views.d.ts +++ b/apps/app/types/views.d.ts @@ -3,6 +3,7 @@ export interface IView { access: string; created_at: Date; updated_at: Date; + is_favorite: boolean; created_by: string; updated_by: string; name: string;