From 5faad9ee36f20d4bcedc150e6593eebebf7b0190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chy=C5=82a?= Date: Thu, 7 Mar 2024 11:15:32 +0100 Subject: [PATCH] Add shortcuts at bottom of sidebar (#4705) * Add shortcuts at bottom of sidebar * Navigator context * Style sidebar * Remove menu back to cloud tests * Add changeset * Add condtion to back to cloud btn * Style improvents, show cmd base on os * Show playground on ctrl + ' * Test sidebar go to cloud and shortcuts * Update changeset * Improve naming * Rename Navigator to NavigatorSearch * Move context outside AppLayout * Memoize shortcuts * Update tests * Fix typo * Refactor dev panel --- .changeset/serious-beans-try.md | 6 + locale/defaultMessages.json | 8 + src/components/AppLayout/AppLayout.tsx | 49 +------ src/components/DevModePanel/DevModePanel.tsx | 19 +-- .../DevModePanel/DevModeProvider.tsx | 27 ++++ .../DevModePanel/useDevModeKeyTrigger.ts | 17 ++- src/components/Navigator/NavigatorInput.tsx | 135 ----------------- src/components/Navigator/index.ts | 2 - .../NavigatorSearch.tsx} | 39 ++--- .../NavigatorSearch/NavigatorSearchInput.tsx | 136 +++++++++++++++++ .../NavigatorSearchProvider.tsx | 22 +++ .../NavigatorSearchSection.tsx} | 10 +- src/components/NavigatorSearch/index.ts | 2 + .../modes/catalog.ts | 0 .../modes/commands/actions.ts | 0 .../modes/commands/index.ts | 0 .../modes/customers.ts | 0 .../modes/default/default.ts | 0 .../modes/default/index.ts | 0 .../modes/default/views.ts | 0 .../modes/help.ts | 0 .../modes/index.ts | 0 .../modes/messages.ts | 0 .../modes/orders.ts | 0 .../modes/types.ts | 0 .../modes/utils.ts | 0 .../queries/queries.ts | 0 .../queries/useCatalogSearch.ts | 0 .../queries/useCheckIfOrderExists.ts | 0 .../{Navigator => NavigatorSearch}/types.ts | 0 .../useNavigatorSearchContext.ts | 22 +++ .../useQuickSearch.ts | 0 src/components/Sidebar/Content.tsx | 39 +++-- src/components/Sidebar/ShotcutsItems.tsx | 27 ++++ src/components/Sidebar/Sidebar.test.tsx | 138 ++++++++++++++++++ .../Sidebar/menu/EnvironmentLink.tsx | 52 +++---- src/components/Sidebar/menu/Menu.test.tsx | 52 ------- src/components/Sidebar/menu/Menu.tsx | 18 ++- .../Sidebar/menu/hooks/useEnvLink.test.ts | 35 +++++ .../Sidebar/menu/hooks/useEnvLink.ts | 17 +++ .../menu/{ => hooks}/useMenuStructure.tsx | 4 +- .../Sidebar/shortcuts/ShortcutItem.tsx | 57 ++++++++ .../Sidebar/shortcuts/Shortcuts.tsx | 10 ++ src/components/Sidebar/shortcuts/index.ts | 1 + src/components/Sidebar/shortcuts/messages.ts | 14 ++ .../Sidebar/shortcuts/useShortcuts.tsx | 57 ++++++++ .../Sidebar/shortcuts/utils.test.ts | 33 +++++ src/components/Sidebar/shortcuts/utils.ts | 3 + src/icons/Graphql.tsx | 21 +++ src/index.tsx | 9 +- 50 files changed, 750 insertions(+), 331 deletions(-) create mode 100644 .changeset/serious-beans-try.md delete mode 100644 src/components/Navigator/NavigatorInput.tsx delete mode 100644 src/components/Navigator/index.ts rename src/components/{Navigator/Navigator.tsx => NavigatorSearch/NavigatorSearch.tsx} (88%) create mode 100644 src/components/NavigatorSearch/NavigatorSearchInput.tsx create mode 100644 src/components/NavigatorSearch/NavigatorSearchProvider.tsx rename src/components/{Navigator/NavigatorSection.tsx => NavigatorSearch/NavigatorSearchSection.tsx} (89%) create mode 100644 src/components/NavigatorSearch/index.ts rename src/components/{Navigator => NavigatorSearch}/modes/catalog.ts (100%) rename src/components/{Navigator => NavigatorSearch}/modes/commands/actions.ts (100%) rename src/components/{Navigator => NavigatorSearch}/modes/commands/index.ts (100%) rename src/components/{Navigator => NavigatorSearch}/modes/customers.ts (100%) rename src/components/{Navigator => NavigatorSearch}/modes/default/default.ts (100%) rename src/components/{Navigator => NavigatorSearch}/modes/default/index.ts (100%) rename src/components/{Navigator => NavigatorSearch}/modes/default/views.ts (100%) rename src/components/{Navigator => NavigatorSearch}/modes/help.ts (100%) rename src/components/{Navigator => NavigatorSearch}/modes/index.ts (100%) rename src/components/{Navigator => NavigatorSearch}/modes/messages.ts (100%) rename src/components/{Navigator => NavigatorSearch}/modes/orders.ts (100%) rename src/components/{Navigator => NavigatorSearch}/modes/types.ts (100%) rename src/components/{Navigator => NavigatorSearch}/modes/utils.ts (100%) rename src/components/{Navigator => NavigatorSearch}/queries/queries.ts (100%) rename src/components/{Navigator => NavigatorSearch}/queries/useCatalogSearch.ts (100%) rename src/components/{Navigator => NavigatorSearch}/queries/useCheckIfOrderExists.ts (100%) rename src/components/{Navigator => NavigatorSearch}/types.ts (100%) create mode 100644 src/components/NavigatorSearch/useNavigatorSearchContext.ts rename src/components/{Navigator => NavigatorSearch}/useQuickSearch.ts (100%) create mode 100644 src/components/Sidebar/ShotcutsItems.tsx create mode 100644 src/components/Sidebar/Sidebar.test.tsx delete mode 100644 src/components/Sidebar/menu/Menu.test.tsx create mode 100644 src/components/Sidebar/menu/hooks/useEnvLink.test.ts create mode 100644 src/components/Sidebar/menu/hooks/useEnvLink.ts rename src/components/Sidebar/menu/{ => hooks}/useMenuStructure.tsx (98%) create mode 100644 src/components/Sidebar/shortcuts/ShortcutItem.tsx create mode 100644 src/components/Sidebar/shortcuts/Shortcuts.tsx create mode 100644 src/components/Sidebar/shortcuts/index.ts create mode 100644 src/components/Sidebar/shortcuts/messages.ts create mode 100644 src/components/Sidebar/shortcuts/useShortcuts.tsx create mode 100644 src/components/Sidebar/shortcuts/utils.test.ts create mode 100644 src/components/Sidebar/shortcuts/utils.ts create mode 100644 src/icons/Graphql.tsx diff --git a/.changeset/serious-beans-try.md b/.changeset/serious-beans-try.md new file mode 100644 index 00000000000..fea128da3cf --- /dev/null +++ b/.changeset/serious-beans-try.md @@ -0,0 +1,6 @@ +--- +"saleor-dashboard": minor +--- + +Introduce menu items with sortcuts for GraphQL playground and search actions in sidebar. +Move "Go to Saleor Cloud" button at bottom of sidebar diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index f919e44ec2e..63a6fa7a4f6 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -2145,6 +2145,10 @@ "context": "window title", "string": "Grant refund" }, + "Cn6l5R": { + "context": "playground shortcut", + "string": "Playground" + }, "Co2U4u": { "string": "No plugins found" }, @@ -6568,6 +6572,10 @@ "context": "swatch attribute type", "string": "Swatch" }, + "gx6b6x": { + "context": "search shortcut", + "string": "Search" + }, "gxPjIQ": { "string": "Are you sure you want to delete {email} from staff members?" }, diff --git a/src/components/AppLayout/AppLayout.tsx b/src/components/AppLayout/AppLayout.tsx index f0d67f3919d..7b770f3dc62 100644 --- a/src/components/AppLayout/AppLayout.tsx +++ b/src/components/AppLayout/AppLayout.tsx @@ -1,19 +1,13 @@ import useAppState from "@dashboard/hooks/useAppState"; -import { DevModeQuery } from "@dashboard/orders/queries"; -import { getFilterVariables } from "@dashboard/orders/views/OrderList/filters"; import { LinearProgress } from "@material-ui/core"; import { useActionBar } from "@saleor/macaw-ui"; import { Box } from "@saleor/macaw-ui-next"; -import React, { useState } from "react"; -import { useLocation } from "react-router"; +import React from "react"; import { DevModePanel } from "../DevModePanel/DevModePanel"; -import { useDevModeContext } from "../DevModePanel/hooks"; -import { useDevModeKeyTrigger } from "../DevModePanel/useDevModeKeyTrigger"; -import Navigator from "../Navigator"; +import NavigatorSearch from "../NavigatorSearch"; import { Sidebar } from "../Sidebar"; import { useStyles } from "./styles"; -import { extractQueryParams } from "./util"; interface AppLayoutProps { children: React.ReactNode; @@ -24,45 +18,12 @@ const AppLayout: React.FC = ({ children }) => { const classes = useStyles(); const { anchor: appActionAnchor } = useActionBar(); const [appState] = useAppState(); - const [isNavigatorVisible, setNavigatorVisibility] = useState(false); - - const { - isDevModeVisible, - setDevModeVisibility, - setDevModeContent, - setVariables, - } = useDevModeContext(); - - const params = extractQueryParams(useLocation().search); - - useDevModeKeyTrigger((_err, { shift }) => { - if (shift) { - setDevModeContent(DevModeQuery); - const variables = JSON.stringify( - { - filter: getFilterVariables(params), - }, - null, - 2, - ); - setVariables(variables); - } else { - setDevModeContent(""); - setVariables(""); - } - setDevModeVisibility(!isDevModeVisible); - }); return ( <> - - + + + {appState.loading && ( diff --git a/src/components/DevModePanel/DevModePanel.tsx b/src/components/DevModePanel/DevModePanel.tsx index 12cb2312f9b..f54cc77c51e 100644 --- a/src/components/DevModePanel/DevModePanel.tsx +++ b/src/components/DevModePanel/DevModePanel.tsx @@ -13,25 +13,18 @@ import { messages } from "./messages"; const authorizedFetch = createFetch(); -interface DevModePanelProps { - isDevModeVisible: boolean; - setDevModeVisibility: (value: boolean) => void; -} +export const DevModePanel: React.FC = () => { + const intl = useIntl(); + const { rootStyle } = useDashboardTheme(); + + const { isDevModeVisible, variables, devModeContent, setDevModeVisibility } = + useDevModeContext(); -export const DevModePanel: React.FC = ({ - isDevModeVisible, - setDevModeVisibility, -}) => { const fetcher = createGraphiQLFetcher({ url: process.env.API_URI, fetch: authorizedFetch, }); - const intl = useIntl(); - const { rootStyle } = useDashboardTheme(); - - const { devModeContent, variables } = useDevModeContext(); - const overwriteCodeMirrorCSSVariables = { __html: ` .graphiql-container, .CodeMirror-info, .CodeMirror-lint-tooltip, reach-portal{ diff --git a/src/components/DevModePanel/DevModeProvider.tsx b/src/components/DevModePanel/DevModeProvider.tsx index f0cbae5638c..1daecf2da39 100644 --- a/src/components/DevModePanel/DevModeProvider.tsx +++ b/src/components/DevModePanel/DevModeProvider.tsx @@ -1,7 +1,12 @@ // @ts-strict-ignore +import { DevModeQuery } from "@dashboard/orders/queries"; +import { getFilterVariables } from "@dashboard/orders/views/OrderList/filters"; import React, { useState } from "react"; +import { useLocation } from "react-router"; +import { extractQueryParams } from "../AppLayout/util"; import { DevModeContext } from "./hooks"; +import { useDevModeKeyTrigger } from "./useDevModeKeyTrigger"; export function DevModeProvider({ children }) { // stringified variables (as key/value) passed along with the query @@ -11,6 +16,28 @@ export function DevModeProvider({ children }) { const [devModeContent, setDevModeContent] = useState(""); const [isDevModeVisible, setDevModeVisibility] = useState(false); + const params = extractQueryParams(useLocation().search); + + const triggerHandler = ({ shift }: { shift: boolean }) => { + if (shift) { + setDevModeContent(DevModeQuery); + const variables = JSON.stringify( + { + filter: getFilterVariables(params), + }, + null, + 2, + ); + setVariables(variables); + } else { + setDevModeContent(""); + setVariables(""); + } + setDevModeVisibility(!isDevModeVisible); + }; + + useDevModeKeyTrigger(triggerHandler); + return ( void; +type DevModeKeyTriggerCallback = ({ shift }: { shift: boolean }) => void; -export const useDevModeKeyTrigger = (callback: DevModeKeyTriggerCallback) => { +export const useDevModeKeyTrigger = ( + callbackHandler: DevModeKeyTriggerCallback, +) => { useEffect(() => { const handler = (event: KeyboardEvent) => { if (event.shiftKey && event.metaKey && event.code === "Quote") { - callback(null, { shift: true }); + callbackHandler({ shift: true }); } else if (event.metaKey && event.code === "Quote") { - callback(null, { shift: false }); + callbackHandler({ shift: false }); + } else if (event.ctrlKey && event.code === "Quote") { + callbackHandler({ shift: false }); } }; document.addEventListener("keydown", handler); return () => document.removeEventListener("keydown", handler); - }, [callback]); + }, [callbackHandler]); }; diff --git a/src/components/Navigator/NavigatorInput.tsx b/src/components/Navigator/NavigatorInput.tsx deleted file mode 100644 index 8a76538691c..00000000000 --- a/src/components/Navigator/NavigatorInput.tsx +++ /dev/null @@ -1,135 +0,0 @@ -// @ts-strict-ignore -import { makeStyles, SearchLargeIcon } from "@saleor/macaw-ui"; -import React from "react"; -import { useIntl } from "react-intl"; - -import { QuickSearchMode } from "./types"; - -const useStyles = makeStyles( - theme => { - const typography = { - ...theme.typography.body1, - color: theme.palette.saleor.main[1], - fontWeight: 500, - letterSpacing: "0.02rem", - }; - - return { - adornment: { - ...typography, - alignSelf: "center", - color: theme.palette.text.secondary, - marginRight: theme.spacing(1), - textAlign: "center", - width: 32, - }, - input: { - "&::placeholder": { - color: theme.palette.saleor.main[3], - }, - ...typography, - background: "transparent", - border: "none", - outline: 0, - padding: 0, - width: "100%", - }, - root: { - background: theme.palette.background.paper, - display: "flex", - padding: theme.spacing(2, 3), - height: 72, - }, - searchIcon: { - alignSelf: "center", - width: 32, - height: 32, - marginRight: theme.spacing(1), - }, - }; - }, - { - name: "NavigatorInput", - }, -); - -interface NavigatorInputProps - extends React.InputHTMLAttributes { - mode: QuickSearchMode; -} - -const NavigatorInput = React.forwardRef( - (props, ref) => { - const { mode, ...rest } = props; - const classes = useStyles(props); - const intl = useIntl(); - - return ( -
- {mode !== "default" ? ( - - {mode === "orders" - ? "#" - : mode === "customers" - ? "@" - : mode === "catalog" - ? "$" - : mode === "help" - ? "?" - : ">"} - - ) : ( - - )} - -
- ); - }, -); - -NavigatorInput.displayName = "NavigatorInput"; -export default NavigatorInput; diff --git a/src/components/Navigator/index.ts b/src/components/Navigator/index.ts deleted file mode 100644 index 8816cc0133c..00000000000 --- a/src/components/Navigator/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "./Navigator"; -export * from "./Navigator"; diff --git a/src/components/Navigator/Navigator.tsx b/src/components/NavigatorSearch/NavigatorSearch.tsx similarity index 88% rename from src/components/Navigator/Navigator.tsx rename to src/components/NavigatorSearch/NavigatorSearch.tsx index 950f60fd071..c21c8ac1043 100644 --- a/src/components/Navigator/Navigator.tsx +++ b/src/components/NavigatorSearch/NavigatorSearch.tsx @@ -18,9 +18,10 @@ import { hasCustomers, hasViews, } from "./modes/utils"; -import NavigatorInput from "./NavigatorInput"; -import NavigatorSection from "./NavigatorSection"; +import NavigatorSearchInput from "./NavigatorSearchInput"; +import NavigatorSection from "./NavigatorSearchSection"; import { QuickSearchAction } from "./types"; +import { useNavigatorSearchContext } from "./useNavigatorSearchContext"; import useQuickSearch from "./useQuickSearch"; const navigatorHotkey = "ctrl+k, command+k"; @@ -55,18 +56,18 @@ const useStyles = makeStyles( }, }), { - name: "Navigator", + name: "NavigatorSearch", }, ); -export interface NavigatorProps { - visible: boolean; - setVisibility: (state: boolean) => void; -} - -const Navigator: React.FC = ({ visible, setVisibility }) => { +const NavigatorSearch: React.FC = () => { + const { isNavigatorVisible, setNavigatorVisibility } = + useNavigatorSearchContext(); const input = React.useRef(null); - const [query, mode, change, actions] = useQuickSearch(visible, input); + const [query, mode, change, actions] = useQuickSearch( + isNavigatorVisible, + input, + ); const intl = useIntl(); const notify = useNotifier(); const [notifiedAboutNavigator, setNotifiedAboutNavigator] = useLocalStorage( @@ -79,7 +80,7 @@ const Navigator: React.FC = ({ visible, setVisibility }) => { React.useEffect(() => { hotkeys(navigatorHotkey, event => { event.preventDefault(); - setVisibility(!visible); + setNavigatorVisibility(!isNavigatorVisible); }); if (!notifiedAboutNavigator) { @@ -118,10 +119,14 @@ const Navigator: React.FC = ({ visible, setVisibility }) => { return ( setVisibility(false)} + open={isNavigatorVisible} + onClose={() => setNavigatorVisibility(false)} > - +
= ({ visible, setVisibility }) => { onSelect={(item: QuickSearchAction) => { const shouldRemainVisible = item?.onClick(); if (!shouldRemainVisible) { - setVisibility(false); + setNavigatorVisibility(false); } }} onInputValueChange={value => @@ -146,7 +151,7 @@ const Navigator: React.FC = ({ visible, setVisibility }) => { > {({ getInputProps, getItemProps, highlightedIndex }) => (
- = ({ visible, setVisibility }) => { ); }; -export default Navigator; +export default NavigatorSearch; diff --git a/src/components/NavigatorSearch/NavigatorSearchInput.tsx b/src/components/NavigatorSearch/NavigatorSearchInput.tsx new file mode 100644 index 00000000000..dda04664579 --- /dev/null +++ b/src/components/NavigatorSearch/NavigatorSearchInput.tsx @@ -0,0 +1,136 @@ +// @ts-strict-ignore +import { makeStyles, SearchLargeIcon } from "@saleor/macaw-ui"; +import React from "react"; +import { useIntl } from "react-intl"; + +import { QuickSearchMode } from "./types"; + +const useStyles = makeStyles( + theme => { + const typography = { + ...theme.typography.body1, + color: theme.palette.saleor.main[1], + fontWeight: 500, + letterSpacing: "0.02rem", + }; + + return { + adornment: { + ...typography, + alignSelf: "center", + color: theme.palette.text.secondary, + marginRight: theme.spacing(1), + textAlign: "center", + width: 32, + }, + input: { + "&::placeholder": { + color: theme.palette.saleor.main[3], + }, + ...typography, + background: "transparent", + border: "none", + outline: 0, + padding: 0, + width: "100%", + }, + root: { + background: theme.palette.background.paper, + display: "flex", + padding: theme.spacing(2, 3), + height: 72, + }, + searchIcon: { + alignSelf: "center", + width: 32, + height: 32, + marginRight: theme.spacing(1), + }, + }; + }, + { + name: "NavigatorInput", + }, +); + +interface NavigatorSearchInputProps + extends React.InputHTMLAttributes { + mode: QuickSearchMode; +} + +const NavigatorSearchInput = React.forwardRef< + HTMLInputElement, + NavigatorSearchInputProps +>((props, ref) => { + const { mode, ...rest } = props; + const classes = useStyles(props); + const intl = useIntl(); + + return ( +
+ {mode !== "default" ? ( + + {mode === "orders" + ? "#" + : mode === "customers" + ? "@" + : mode === "catalog" + ? "$" + : mode === "help" + ? "?" + : ">"} + + ) : ( + + )} + +
+ ); +}); + +NavigatorSearchInput.displayName = "NavigatorSearchInput"; +export default NavigatorSearchInput; diff --git a/src/components/NavigatorSearch/NavigatorSearchProvider.tsx b/src/components/NavigatorSearch/NavigatorSearchProvider.tsx new file mode 100644 index 00000000000..44383d00541 --- /dev/null +++ b/src/components/NavigatorSearch/NavigatorSearchProvider.tsx @@ -0,0 +1,22 @@ +import React, { ReactNode, useState } from "react"; + +import { NavigatorSearchContext } from "./useNavigatorSearchContext"; + +export const NavigatorSearchProvider = ({ + children, +}: { + children: ReactNode; +}) => { + const [isNavigatorVisible, setNavigatorVisibility] = useState(false); + + return ( + + {children} + + ); +}; diff --git a/src/components/Navigator/NavigatorSection.tsx b/src/components/NavigatorSearch/NavigatorSearchSection.tsx similarity index 89% rename from src/components/Navigator/NavigatorSection.tsx rename to src/components/NavigatorSearch/NavigatorSearchSection.tsx index 678fdb3ba02..f54dff14587 100644 --- a/src/components/Navigator/NavigatorSection.tsx +++ b/src/components/NavigatorSearch/NavigatorSearchSection.tsx @@ -5,7 +5,7 @@ import React from "react"; import { QuickSearchAction } from "./types"; -interface NavigatorSectionProps { +interface NavigatorSearchSectionProps { getItemProps: (options: GetItemPropsOptions) => any; highlightedIndex: number; label: string; @@ -46,11 +46,11 @@ const useStyles = makeStyles( }, }), { - name: "NavigatorSection", + name: "NavigatorSearchSection", }, ); -const NavigatorSection: React.FC = props => { +const NavigatorSearchSection: React.FC = props => { const { getItemProps, highlightedIndex, label, items, offset } = props; const classes = useStyles(props); @@ -96,5 +96,5 @@ const NavigatorSection: React.FC = props => { ); }; -NavigatorSection.displayName = "NavigatorSection"; -export default NavigatorSection; +NavigatorSearchSection.displayName = "NavigatorSearchSection"; +export default NavigatorSearchSection; diff --git a/src/components/NavigatorSearch/index.ts b/src/components/NavigatorSearch/index.ts new file mode 100644 index 00000000000..598d21014ed --- /dev/null +++ b/src/components/NavigatorSearch/index.ts @@ -0,0 +1,2 @@ +export { default } from "./NavigatorSearch"; +export * from "./NavigatorSearch"; diff --git a/src/components/Navigator/modes/catalog.ts b/src/components/NavigatorSearch/modes/catalog.ts similarity index 100% rename from src/components/Navigator/modes/catalog.ts rename to src/components/NavigatorSearch/modes/catalog.ts diff --git a/src/components/Navigator/modes/commands/actions.ts b/src/components/NavigatorSearch/modes/commands/actions.ts similarity index 100% rename from src/components/Navigator/modes/commands/actions.ts rename to src/components/NavigatorSearch/modes/commands/actions.ts diff --git a/src/components/Navigator/modes/commands/index.ts b/src/components/NavigatorSearch/modes/commands/index.ts similarity index 100% rename from src/components/Navigator/modes/commands/index.ts rename to src/components/NavigatorSearch/modes/commands/index.ts diff --git a/src/components/Navigator/modes/customers.ts b/src/components/NavigatorSearch/modes/customers.ts similarity index 100% rename from src/components/Navigator/modes/customers.ts rename to src/components/NavigatorSearch/modes/customers.ts diff --git a/src/components/Navigator/modes/default/default.ts b/src/components/NavigatorSearch/modes/default/default.ts similarity index 100% rename from src/components/Navigator/modes/default/default.ts rename to src/components/NavigatorSearch/modes/default/default.ts diff --git a/src/components/Navigator/modes/default/index.ts b/src/components/NavigatorSearch/modes/default/index.ts similarity index 100% rename from src/components/Navigator/modes/default/index.ts rename to src/components/NavigatorSearch/modes/default/index.ts diff --git a/src/components/Navigator/modes/default/views.ts b/src/components/NavigatorSearch/modes/default/views.ts similarity index 100% rename from src/components/Navigator/modes/default/views.ts rename to src/components/NavigatorSearch/modes/default/views.ts diff --git a/src/components/Navigator/modes/help.ts b/src/components/NavigatorSearch/modes/help.ts similarity index 100% rename from src/components/Navigator/modes/help.ts rename to src/components/NavigatorSearch/modes/help.ts diff --git a/src/components/Navigator/modes/index.ts b/src/components/NavigatorSearch/modes/index.ts similarity index 100% rename from src/components/Navigator/modes/index.ts rename to src/components/NavigatorSearch/modes/index.ts diff --git a/src/components/Navigator/modes/messages.ts b/src/components/NavigatorSearch/modes/messages.ts similarity index 100% rename from src/components/Navigator/modes/messages.ts rename to src/components/NavigatorSearch/modes/messages.ts diff --git a/src/components/Navigator/modes/orders.ts b/src/components/NavigatorSearch/modes/orders.ts similarity index 100% rename from src/components/Navigator/modes/orders.ts rename to src/components/NavigatorSearch/modes/orders.ts diff --git a/src/components/Navigator/modes/types.ts b/src/components/NavigatorSearch/modes/types.ts similarity index 100% rename from src/components/Navigator/modes/types.ts rename to src/components/NavigatorSearch/modes/types.ts diff --git a/src/components/Navigator/modes/utils.ts b/src/components/NavigatorSearch/modes/utils.ts similarity index 100% rename from src/components/Navigator/modes/utils.ts rename to src/components/NavigatorSearch/modes/utils.ts diff --git a/src/components/Navigator/queries/queries.ts b/src/components/NavigatorSearch/queries/queries.ts similarity index 100% rename from src/components/Navigator/queries/queries.ts rename to src/components/NavigatorSearch/queries/queries.ts diff --git a/src/components/Navigator/queries/useCatalogSearch.ts b/src/components/NavigatorSearch/queries/useCatalogSearch.ts similarity index 100% rename from src/components/Navigator/queries/useCatalogSearch.ts rename to src/components/NavigatorSearch/queries/useCatalogSearch.ts diff --git a/src/components/Navigator/queries/useCheckIfOrderExists.ts b/src/components/NavigatorSearch/queries/useCheckIfOrderExists.ts similarity index 100% rename from src/components/Navigator/queries/useCheckIfOrderExists.ts rename to src/components/NavigatorSearch/queries/useCheckIfOrderExists.ts diff --git a/src/components/Navigator/types.ts b/src/components/NavigatorSearch/types.ts similarity index 100% rename from src/components/Navigator/types.ts rename to src/components/NavigatorSearch/types.ts diff --git a/src/components/NavigatorSearch/useNavigatorSearchContext.ts b/src/components/NavigatorSearch/useNavigatorSearchContext.ts new file mode 100644 index 00000000000..2ad430695fc --- /dev/null +++ b/src/components/NavigatorSearch/useNavigatorSearchContext.ts @@ -0,0 +1,22 @@ +import { createContext, useContext } from "react"; + +interface NavigatorContext { + isNavigatorVisible: boolean; + setNavigatorVisibility: (visible: boolean) => void; +} + +export const NavigatorSearchContext = createContext( + null, +); + +export const useNavigatorSearchContext = () => { + const context = useContext(NavigatorSearchContext); + + if (context === null) { + throw new Error( + "You are using useNavigatorContext outisde of its provider", + ); + } + + return context; +}; diff --git a/src/components/Navigator/useQuickSearch.ts b/src/components/NavigatorSearch/useQuickSearch.ts similarity index 100% rename from src/components/Navigator/useQuickSearch.ts rename to src/components/NavigatorSearch/useQuickSearch.ts diff --git a/src/components/Sidebar/Content.tsx b/src/components/Sidebar/Content.tsx index f91ab87d13a..3bedd20b8b1 100644 --- a/src/components/Sidebar/Content.tsx +++ b/src/components/Sidebar/Content.tsx @@ -1,20 +1,33 @@ +import { useCloud } from "@dashboard/auth/hooks/useCloud"; import { Box } from "@saleor/macaw-ui-next"; import React from "react"; import { Menu } from "./menu"; +import { EnvironmentLink } from "./menu/EnvironmentLink"; import { MountingPoint } from "./MountingPoint"; import { UserInfo } from "./user"; -export const SidebarContent = () => ( - - - - - -); +export const SidebarContent = () => { + const { isAuthenticatedViaCloud } = useCloud(); + + return ( + + + + + {isAuthenticatedViaCloud && ( + + + + )} + + + + ); +}; diff --git a/src/components/Sidebar/ShotcutsItems.tsx b/src/components/Sidebar/ShotcutsItems.tsx new file mode 100644 index 00000000000..64b2d2a449f --- /dev/null +++ b/src/components/Sidebar/ShotcutsItems.tsx @@ -0,0 +1,27 @@ +import { Box } from "@saleor/macaw-ui-next"; +import React, { memo } from "react"; + +import { ShortcutItem } from "./shortcuts/ShortcutItem"; +import { Shortcut } from "./shortcuts/useShortcuts"; + +interface ShortcutsItemsProps { + items: Shortcut[]; +} + +export const ShortcutsItems = memo(({ items }: ShortcutsItemsProps) => { + return ( + + {items.map(({ icon, id, name, shortcut, action }) => ( + + {icon} + {name} + + {shortcut} + + + ))} + + ); +}); + +ShortcutsItems.displayName = "ShortcutsItems"; diff --git a/src/components/Sidebar/Sidebar.test.tsx b/src/components/Sidebar/Sidebar.test.tsx new file mode 100644 index 00000000000..15764c3612b --- /dev/null +++ b/src/components/Sidebar/Sidebar.test.tsx @@ -0,0 +1,138 @@ +import { useCloud } from "@dashboard/auth/hooks/useCloud"; +import { useDevModeContext } from "@dashboard/components/DevModePanel/hooks"; +import { useNavigatorSearchContext } from "@dashboard/components/NavigatorSearch/useNavigatorSearchContext"; +import { ThemeProvider as LegacyThemeProvider } from "@saleor/macaw-ui"; +import { ThemeProvider } from "@saleor/macaw-ui-next"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import React, { ReactNode } from "react"; + +import { Sidebar } from "./Sidebar"; + +jest.mock("react-intl", () => ({ + useIntl: jest.fn(() => ({ + formatMessage: jest.fn(x => x.defaultMessage), + })), + defineMessages: jest.fn(x => x), + FormattedMessage: ({ defaultMessage }: { defaultMessage: string }) => ( + <>{defaultMessage} + ), +})); + +jest.mock("./menu/hooks/useMenuStructure", () => ({ + useMenuStructure: jest.fn(() => []), +})); + +jest.mock("@dashboard/featureFlags/useFlagsInfo", () => ({ + useFlagsInfo: jest.fn(() => []), +})); + +jest.mock("@dashboard/auth/hooks/useCloud", () => ({ + useCloud: jest.fn(() => ({ + isAuthenticatedViaCloud: false, + })), +})); + +jest.mock("@dashboard/components/DevModePanel/hooks", () => ({ + useDevModeContext: jest.fn(() => ({ + variables: "", + setVariables: jest.fn(), + isDevModeVisible: false, + setDevModeVisibility: jest.fn(), + devModeContent: "", + setDevModeContent: jest.fn(), + })), +})); + +jest.mock( + "@dashboard/components/NavigatorSearch/useNavigatorSearchContext", + () => ({ + useNavigatorSearchContext: jest.fn(() => ({ + isNavigatorVisible: false, + setNavigatorVisibility: jest.fn(), + })), + }), +); + +const Wrapper = ({ children }: { children: ReactNode }) => { + return ( + + {children} + + ); +}; + +describe("Sidebar", () => { + it('shouldd render "Go to Saleor Cloud" link when is cloud instance', () => { + // Arrange + (useCloud as jest.Mock).mockImplementation(() => ({ + isAuthenticatedViaCloud: true, + })); + + // Act + render(, { wrapper: Wrapper }); + + // Assert + expect(screen.getByText("Go to Saleor Cloud")).toBeInTheDocument(); + }); + + it('shouldd not render "Go to Saleor Cloud" link when is not cloud instance', () => { + // Arrange + (useCloud as jest.Mock).mockImplementation(() => ({ + isAuthenticatedViaCloud: false, + })); + + // Act + render(, { wrapper: Wrapper }); + + // Assert + expect(screen.queryByText("Go to Saleor Cloud")).not.toBeInTheDocument(); + }); + + it("should render keyboard shortcuts", () => { + // Arrange & Act + render(, { wrapper: Wrapper }); + + // Assert + expect(screen.getByText("Search")).toBeInTheDocument(); + expect(screen.getByText("Playground")).toBeInTheDocument(); + }); + + it("should call callback when click on playground shortcut", async () => { + // Arrange + const actionCallback = jest.fn(); + (useDevModeContext as jest.Mock).mockImplementationOnce(() => ({ + variables: "", + setVariables: jest.fn(), + isDevModeVisible: false, + setDevModeVisibility: actionCallback, + devModeContent: "", + setDevModeContent: jest.fn(), + })); + + render(, { wrapper: Wrapper }); + + // Act + await userEvent.click(screen.getByText("Playground")); + + // Assert + expect(actionCallback).toHaveBeenCalledWith(true); + }); + + it("should call callback when click on search shortcut", async () => { + // Arrange + const actionCallback = jest.fn(); + (useNavigatorSearchContext as jest.Mock).mockImplementationOnce(() => ({ + isNavigatorVisible: false, + setNavigatorVisibility: actionCallback, + })); + + render(, { wrapper: Wrapper }); + + // Act + await userEvent.click(screen.getByText("Search")); + + // Assert + expect(actionCallback).toHaveBeenCalledWith(true); + }); +}); diff --git a/src/components/Sidebar/menu/EnvironmentLink.tsx b/src/components/Sidebar/menu/EnvironmentLink.tsx index d1a5cd57391..ae403fbd052 100644 --- a/src/components/Sidebar/menu/EnvironmentLink.tsx +++ b/src/components/Sidebar/menu/EnvironmentLink.tsx @@ -1,42 +1,32 @@ -import { ArrowLeftIcon, Box, List, Text } from "@saleor/macaw-ui-next"; +import { ArrowLeftIcon, Box, Text } from "@saleor/macaw-ui-next"; import React from "react"; import { FormattedMessage } from "react-intl"; -const UTM_PARAMS = "?utm_source=dashboard&utm_content=sidebar_button"; - -const stagingLink = (hostname: string) => - `https://staging-cloud.saleor.io/env/${hostname}${UTM_PARAMS}`; - -const prodLink = (hostname: string) => - `https://cloud.saleor.io/env/${hostname}${UTM_PARAMS}`; - -const generateEnvLink = () => { - const { hostname } = window.location; - - if (hostname.includes(".staging.")) { - return stagingLink(hostname); - } - - return prodLink(hostname); -}; +import { useEnvLink } from "./hooks/useEnvLink"; export const EnvironmentLink = () => { + const envLink = useEnvLink(); + return ( - - - - - - - - + + + + + ); }; diff --git a/src/components/Sidebar/menu/Menu.test.tsx b/src/components/Sidebar/menu/Menu.test.tsx deleted file mode 100644 index 80ef2a8ec6a..00000000000 --- a/src/components/Sidebar/menu/Menu.test.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import React from "react"; - -import { Menu } from "./Menu"; - -jest.mock("react-intl", () => ({ - FormattedMessage: () => <>Open env, - defineMessages: jest.fn(), -})); -jest.mock("./useMenuStructure", () => ({ - useMenuStructure: jest.fn(() => []), -})); - -jest.mock("@dashboard/auth/hooks/useCloud", () => ({ - useCloud: jest.fn(() => ({ isAuthenticatedViaCloud: true })), -})); - -describe("Sidebar menu", () => { - it("renders link to the cloud environment on production", async () => { - // Arrange - const stagingHref = - "https://cloud.saleor.io/env/test.com?utm_source=dashboard&utm_content=sidebar_button"; - delete (window as { location?: unknown }).location; - // @ts-expect-error - window.location = { hostname: "test.com" }; - render(); - - // Assert - await screen.findAllByTestId((content, element) => { - const isMatchTestId = content === "menu-item-label-env"; - const isMatchHref = element?.getAttribute("href") === stagingHref; - return isMatchTestId && isMatchHref; - }); - }); - - it("renders link to the cloud environment on staging", async () => { - // Arrange - const stagingHref = - "https://staging-cloud.saleor.io/env/test.staging.com?utm_source=dashboard&utm_content=sidebar_button"; - delete (window as { location?: unknown }).location; - // @ts-expect-error - window.location = { hostname: "test.staging.com" }; - render(); - - // Assert - await screen.findAllByTestId((content, element) => { - const isMatchTestId = content === "menu-item-label-env"; - const isMatchHref = element?.getAttribute("href") === stagingHref; - return isMatchTestId && isMatchHref; - }); - }); -}); diff --git a/src/components/Sidebar/menu/Menu.tsx b/src/components/Sidebar/menu/Menu.tsx index 7333fc2119f..90286c0fd77 100644 --- a/src/components/Sidebar/menu/Menu.tsx +++ b/src/components/Sidebar/menu/Menu.tsx @@ -1,23 +1,29 @@ -import { useCloud } from "@dashboard/auth/hooks/useCloud"; import { Box, List } from "@saleor/macaw-ui-next"; import React from "react"; -import { EnvironmentLink } from "./EnvironmentLink"; +import { Shortcusts } from "../shortcuts"; +import { useMenuStructure } from "./hooks/useMenuStructure"; import { MenuItem } from "./Item"; -import { useMenuStructure } from "./useMenuStructure"; export const Menu = () => { const menuStructure = useMenuStructure(); - const { isAuthenticatedViaCloud } = useCloud(); return ( - + - {isAuthenticatedViaCloud && } {menuStructure.map(menuItem => ( ))} + + ); }; diff --git a/src/components/Sidebar/menu/hooks/useEnvLink.test.ts b/src/components/Sidebar/menu/hooks/useEnvLink.test.ts new file mode 100644 index 00000000000..89937625e4e --- /dev/null +++ b/src/components/Sidebar/menu/hooks/useEnvLink.test.ts @@ -0,0 +1,35 @@ +import { renderHook } from "@testing-library/react-hooks"; + +import { useEnvLink } from "./useEnvLink"; + +describe("useEnvLink", () => { + it("should return link to the cloud environment on production.", () => { + // Arrange + const cloudHref = + "https://cloud.saleor.io/env/test.com?utm_source=dashboard&utm_content=sidebar_button"; + delete (window as { location?: unknown }).location; + // @ts-expect-error + window.location = { hostname: "test.com" }; + + // Act + const { result } = renderHook(() => useEnvLink()); + + // Assert + expect(result.current).toBe(cloudHref); + }); + + it("should return link to the cloud environment on staging", () => { + // Arrange + const stagingHref = + "https://staging-cloud.saleor.io/env/test.staging.com?utm_source=dashboard&utm_content=sidebar_button"; + delete (window as { location?: unknown }).location; + // @ts-expect-error + window.location = { hostname: "test.staging.com" }; + + // Act + const { result } = renderHook(() => useEnvLink()); + + // Assert + expect(result.current).toBe(stagingHref); + }); +}); diff --git a/src/components/Sidebar/menu/hooks/useEnvLink.ts b/src/components/Sidebar/menu/hooks/useEnvLink.ts new file mode 100644 index 00000000000..d761f2f275f --- /dev/null +++ b/src/components/Sidebar/menu/hooks/useEnvLink.ts @@ -0,0 +1,17 @@ +const UTM_PARAMS = "?utm_source=dashboard&utm_content=sidebar_button"; + +const stagingLink = (hostname: string) => + `https://staging-cloud.saleor.io/env/${hostname}${UTM_PARAMS}`; + +const prodLink = (hostname: string) => + `https://cloud.saleor.io/env/${hostname}${UTM_PARAMS}`; + +export const useEnvLink = () => { + const { hostname } = window.location; + + if (hostname.includes(".staging.")) { + return stagingLink(hostname); + } + + return prodLink(hostname); +}; diff --git a/src/components/Sidebar/menu/useMenuStructure.tsx b/src/components/Sidebar/menu/hooks/useMenuStructure.tsx similarity index 98% rename from src/components/Sidebar/menu/useMenuStructure.tsx rename to src/components/Sidebar/menu/hooks/useMenuStructure.tsx index 5918b8af57f..8e98a5b1489 100644 --- a/src/components/Sidebar/menu/useMenuStructure.tsx +++ b/src/components/Sidebar/menu/hooks/useMenuStructure.tsx @@ -33,8 +33,8 @@ import isEmpty from "lodash/isEmpty"; import React from "react"; import { useIntl } from "react-intl"; -import { SidebarMenuItem } from "./types"; -import { mapToExtensionsItems } from "./utils"; +import { SidebarMenuItem } from "../types"; +import { mapToExtensionsItems } from "../utils"; const iconSettings = { color: "default2", diff --git a/src/components/Sidebar/shortcuts/ShortcutItem.tsx b/src/components/Sidebar/shortcuts/ShortcutItem.tsx new file mode 100644 index 00000000000..3d04fff57d4 --- /dev/null +++ b/src/components/Sidebar/shortcuts/ShortcutItem.tsx @@ -0,0 +1,57 @@ +import { Box, List } from "@saleor/macaw-ui-next"; +import React, { ReactNode } from "react"; + +interface ChildrenProps { + children: ReactNode; +} + +interface ShortcutItemProps extends ChildrenProps { + onClick?: () => void; +} + +const ShortcutItemWrapper = ({ children, onClick }: ShortcutItemProps) => { + return ( + + {children} + + ); +}; + +const Icon = ({ children }: ChildrenProps) => { + return ( + + {children} + + ); +}; + +const KeyboardShortcut = ({ children }: ChildrenProps) => { + return ( + + {children} + + ); +}; + +export const ShortcutItem = Object.assign(ShortcutItemWrapper, { + Icon, + KeyboardShortcut, +}); diff --git a/src/components/Sidebar/shortcuts/Shortcuts.tsx b/src/components/Sidebar/shortcuts/Shortcuts.tsx new file mode 100644 index 00000000000..b068290512f --- /dev/null +++ b/src/components/Sidebar/shortcuts/Shortcuts.tsx @@ -0,0 +1,10 @@ +import React from "react"; + +import { ShortcutsItems } from "../ShotcutsItems"; +import { useShortcuts } from "./useShortcuts"; + +export const Shortcusts = () => { + const shortcuts = useShortcuts(); + // ShortcutsItems is memoized, to prevent rerender when context change + return ; +}; diff --git a/src/components/Sidebar/shortcuts/index.ts b/src/components/Sidebar/shortcuts/index.ts new file mode 100644 index 00000000000..162fbd92167 --- /dev/null +++ b/src/components/Sidebar/shortcuts/index.ts @@ -0,0 +1 @@ +export * from "./Shortcuts"; diff --git a/src/components/Sidebar/shortcuts/messages.ts b/src/components/Sidebar/shortcuts/messages.ts new file mode 100644 index 00000000000..a07714cc4fc --- /dev/null +++ b/src/components/Sidebar/shortcuts/messages.ts @@ -0,0 +1,14 @@ +import { defineMessages } from "react-intl"; + +export const shortcutsMessages = defineMessages({ + search: { + id: "gx6b6x", + defaultMessage: "Search", + description: "search shortcut", + }, + playground: { + id: "Cn6l5R", + defaultMessage: "Playground", + description: "playground shortcut", + }, +}); diff --git a/src/components/Sidebar/shortcuts/useShortcuts.tsx b/src/components/Sidebar/shortcuts/useShortcuts.tsx new file mode 100644 index 00000000000..90fd9aecfd8 --- /dev/null +++ b/src/components/Sidebar/shortcuts/useShortcuts.tsx @@ -0,0 +1,57 @@ +import { useDevModeContext } from "@dashboard/components/DevModePanel/hooks"; +import { useNavigatorSearchContext } from "@dashboard/components/NavigatorSearch/useNavigatorSearchContext"; +import { Graphql } from "@dashboard/icons/Graphql"; +import { SearchIcon } from "@saleor/macaw-ui-next"; +import React, { useCallback, useMemo } from "react"; +import { useIntl } from "react-intl"; + +import { shortcutsMessages } from "./messages"; +import { getShortcutLeadingKey } from "./utils"; + +export interface Shortcut { + id: string; + name: string; + icon: React.ReactNode; + shortcut: string; + action: () => void; +} + +export const useShortcuts = (): Shortcut[] => { + const intl = useIntl(); + const devContext = useDevModeContext(); + const { setNavigatorVisibility } = useNavigatorSearchContext(); + + const controlKey = getShortcutLeadingKey(); + + const handleOpenPlayground = useCallback(() => { + devContext.setDevModeContent(""); + devContext.setVariables(""); + devContext.setDevModeVisibility(true); + }, []); + + const handleOpenSearch = useCallback(() => { + setNavigatorVisibility(true); + }, []); + + const shortcuts = useMemo( + () => [ + { + id: "search", + name: intl.formatMessage(shortcutsMessages.search), + icon: , + shortcut: `${controlKey} + K`, + action: handleOpenSearch, + }, + { + id: "playground", + name: intl.formatMessage(shortcutsMessages.playground), + icon: , + shortcut: `${controlKey} + '`, + action: handleOpenPlayground, + }, + ], + [intl], + ); + + return shortcuts; +}; diff --git a/src/components/Sidebar/shortcuts/utils.test.ts b/src/components/Sidebar/shortcuts/utils.test.ts new file mode 100644 index 00000000000..8174558c6f6 --- /dev/null +++ b/src/components/Sidebar/shortcuts/utils.test.ts @@ -0,0 +1,33 @@ +import { getShortcutLeadingKey } from "./utils"; + +describe("getShortcutLeadingKey", () => { + it('should return "⌘" if navigator.appVersion includes "mac"', () => { + // Arrange + Object.defineProperty(navigator, "appVersion", { + value: + "5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", + writable: true, + }); + + // Act + const result = getShortcutLeadingKey(); + + // Assert + expect(result).toBe("⌘"); + }); + + it('should return "Ctrl" if navigator.appVersion does not include "mac"', () => { + // Arrange + Object.defineProperty(navigator, "appVersion", { + value: + "5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", + writable: true, + }); + + // Act + const result = getShortcutLeadingKey(); + + // Assert + expect(result).toBe("Ctrl"); + }); +}); diff --git a/src/components/Sidebar/shortcuts/utils.ts b/src/components/Sidebar/shortcuts/utils.ts new file mode 100644 index 00000000000..75c937cbc06 --- /dev/null +++ b/src/components/Sidebar/shortcuts/utils.ts @@ -0,0 +1,3 @@ +export const getShortcutLeadingKey = () => { + return navigator.appVersion.toLowerCase().includes("mac") ? "⌘" : "Ctrl"; +}; diff --git a/src/icons/Graphql.tsx b/src/icons/Graphql.tsx new file mode 100644 index 00000000000..28f0f715a43 --- /dev/null +++ b/src/icons/Graphql.tsx @@ -0,0 +1,21 @@ +import React from "react"; + +export const Graphql = () => { + return ( + // Mark component as candidate for macaw-ui + + + + + + + + + + ); +}; diff --git a/src/index.tsx b/src/index.tsx index 06d9590f400..0f217414392 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -39,6 +39,7 @@ import ErrorPage from "./components/ErrorPage"; import ExitFormDialogProvider from "./components/Form/ExitFormDialogProvider"; import { LocaleProvider } from "./components/Locale"; import MessageManagerProvider from "./components/messages"; +import { NavigatorSearchProvider } from "./components/NavigatorSearch/NavigatorSearchProvider"; import { ProductAnalytics } from "./components/ProductAnalytics"; import { ShopProvider } from "./components/Shop"; import { WindowTitle } from "./components/WindowTitle"; @@ -122,9 +123,11 @@ const App: React.FC = () => ( - - - + + + + +