Feature previews (#4079)

* Feature preview

* Feature preview

* Feature preview

* Feature preview

* Feature preview

* Add visible flag

* Typo

* Typo

* Fix re-login

* Fix re-login

* Fix enable

* Fix enable

* content

* Fix types, overflow content

* Fix user menu
This commit is contained in:
Patryk Andrzejewski 2023-08-17 12:36:11 +02:00 committed by GitHub
parent e356674c76
commit aaf646399e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 2748 additions and 433 deletions

View file

@ -0,0 +1,19 @@
// @ts-nocheck
import K35081 from "./images/filters.png"
const product_filters = () => (<><p><img src={K35081} alt="new filters"/></p>
<p>Experience the new look and enhanced abilities of new fitering mechanism.
Easily combine any criteria you want, and quickly browse their values.</p>
</>)
export const AVAILABLE_FLAGS = [{
name: "product_filters",
displayName: "Products filtering",
component: product_filters,
visible: true,
content: {
enabled: false,
payload: "default",
}
}] as const;

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View file

@ -0,0 +1,12 @@
---
name: product_filters
displayName: Products filtering
enabled: false
payload: "default"
visible: true
---
![new filters](./images/filters.png)
Experience the new look and enhanced abilities of new fitering mechanism.
Easily combine any criteria you want, and quickly browse their values.

1
.gitignore vendored
View file

@ -53,3 +53,4 @@ django-queries-results.html
!.env.template !.env.template
!.dependency-cruiser.js !.dependency-cruiser.js
!.featureFlags

View file

@ -27,6 +27,7 @@ export const MockedUserProvider: React.FC<{
__typename: "User", __typename: "User",
accessibleChannels: [], accessibleChannels: [],
restrictedAccessToChannels: false, restrictedAccessToChannels: false,
metadata: []
}, },
errors: [], errors: [],
}} }}

View file

@ -758,6 +758,10 @@
"context": "code ending with label", "context": "code ending with label",
"string": "Code ending with {last4CodeChars}" "string": "Code ending with {last4CodeChars}"
}, },
"38dc43": {
"context": "Features preview",
"string": "Features preview"
},
"39yi8w": { "39yi8w": {
"context": "selected currency", "context": "selected currency",
"string": "Selected currency" "string": "Selected currency"
@ -6844,6 +6848,9 @@
"context": "Require email confirmation link", "context": "Require email confirmation link",
"string": "Require email confirmation link" "string": "Require email confirmation link"
}, },
"krer6Z": {
"string": "Features preview"
},
"kuo4fW": { "kuo4fW": {
"context": "dialog title", "context": "dialog title",
"string": "Capture manual transaction" "string": "Capture manual transaction"
@ -7408,6 +7415,9 @@
"context": "gift card history message", "context": "gift card history message",
"string": "Gift card was activated" "string": "Gift card was activated"
}, },
"pEf/m+": {
"string": "There are no previews at the moment. Stay tuned!"
},
"pFVX6g": { "pFVX6g": {
"string": "Variant with these attributes already exists" "string": "Variant with these attributes already exists"
}, },

2082
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -34,7 +34,7 @@
"@material-ui/lab": "^4.0.0-alpha.61", "@material-ui/lab": "^4.0.0-alpha.61",
"@material-ui/styles": "^4.11.4", "@material-ui/styles": "^4.11.4",
"@reach/auto-id": "^0.16.0", "@reach/auto-id": "^0.16.0",
"@saleor/macaw-ui": "0.8.0-pre.113", "@saleor/macaw-ui": "0.8.0-pre.119",
"@saleor/sdk": "0.6.0", "@saleor/sdk": "0.6.0",
"@sentry/react": "^6.0.0", "@sentry/react": "^6.0.0",
"@types/faker": "^5.1.6", "@types/faker": "^5.1.6",
@ -54,6 +54,7 @@
"faker": "^5.1.0", "faker": "^5.1.0",
"fast-array-diff": "^0.2.0", "fast-array-diff": "^0.2.0",
"find-test-names": "^1.17.1", "find-test-names": "^1.17.1",
"front-matter": "^4.0.2",
"fuse.js": "^6.6.2", "fuse.js": "^6.6.2",
"fuzzaldrin": "^2.1.0", "fuzzaldrin": "^2.1.0",
"graphiql": "^2.2.0", "graphiql": "^2.2.0",
@ -89,6 +90,7 @@
"react-router-dom": "^5.0.1", "react-router-dom": "^5.0.1",
"react-sortable-hoc": "^1.10.1", "react-sortable-hoc": "^1.10.1",
"react-sortable-tree": "^2.6.2", "react-sortable-tree": "^2.6.2",
"remark-gfm": "^3.0.1",
"slugify": "^1.4.6", "slugify": "^1.4.6",
"tslib": "^2.4.1", "tslib": "^2.4.1",
"url-join": "^4.0.1", "url-join": "^4.0.1",
@ -172,6 +174,7 @@
"eslint-plugin-react-refresh": "^0.3.1", "eslint-plugin-react-refresh": "^0.3.1",
"eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-simple-import-sort": "^10.0.0",
"graphql-request": "^3.7.0", "graphql-request": "^3.7.0",
"html-to-react": "^1.6.0",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"is-ci": "^3.0.1", "is-ci": "^3.0.1",
"jscodeshift": "^0.13.0", "jscodeshift": "^0.13.0",
@ -276,7 +279,7 @@
"^@material-ui/core$": "<rootDir>/node_modules/@material-ui/core", "^@material-ui/core$": "<rootDir>/node_modules/@material-ui/core",
"^@material-ui/icons$": "<rootDir>/node_modules/@material-ui/icons", "^@material-ui/icons$": "<rootDir>/node_modules/@material-ui/icons",
"^@material-ui/styles$": "<rootDir>/node_modules/@material-ui/styles", "^@material-ui/styles$": "<rootDir>/node_modules/@material-ui/styles",
"^@saleor/macaw-ui/next$": "<rootDir>/node_modules/@saleor/macaw-ui/dist/macaw-ui.js", "^@saleor/macaw-ui/next$": "<rootDir>/node_modules/@saleor/macaw-ui/dist/index.js",
"^react$": "<rootDir>/node_modules/react", "^react$": "<rootDir>/node_modules/react",
"^react-dom$": "<rootDir>/node_modules/react-dom" "^react-dom$": "<rootDir>/node_modules/react-dom"
} }
@ -296,6 +299,7 @@
"preview": "vite preview", "preview": "vite preview",
"build-storybook": "cross-env NODE_OPTIONS=--max-old-space-size=6144 storybook build -o build/storybook", "build-storybook": "cross-env NODE_OPTIONS=--max-old-space-size=6144 storybook build -o build/storybook",
"build-types": "node scripts/build-types.js", "build-types": "node scripts/build-types.js",
"build-flags": "node scripts/build-flags.js",
"prebuild": "npm run build-types", "prebuild": "npm run build-types",
"check-strict-null-errors": "tsc --noEmit --strictNullChecks | node scripts/count-strict-null-check-errors.js", "check-strict-null-errors": "tsc --noEmit --strictNullChecks | node scripts/count-strict-null-check-errors.js",
"check-types": "tsc --noEmit && tsc-strict", "check-types": "tsc --noEmit && tsc-strict",

78
scripts/build-flags.js Normal file
View file

@ -0,0 +1,78 @@
const marked = require("marked")
const fs = require("fs")
const path = require("path")
const fm = require("front-matter")
const HtmlToReactParser = require('html-to-react').Parser;
const htmlToReactParser = new HtmlToReactParser();
const ReactDOMServer = require('react-dom/server');
const imageImports = []
const generateImageComponentName = () => {
const letter = String.fromCharCode(Math.floor(Math.random() * 26 + 97)).toUpperCase()
const name = String(Math.random()).substring(2, 7)
return `${letter}${name}`
}
const renderer = {
image(href, _, text) {
const componentName = generateImageComponentName()
imageImports.push({ componentName, href })
return `<img src=${componentName} alt="${text}"/>`;
}
};
marked.use({ renderer });
const mdParsed = fs
.readdirSync(path.resolve(__dirname, "./../.featureFlags"))
.filter(f => f.endsWith(".md"))
.map(file => fs.readFileSync(path.resolve(__dirname, "./../.featureFlags/", file)))
.map(binary => binary.toString())
.map(fm)
.map(({ body, attributes }) => ({
component: htmlToReactParser.parse(marked.parse(body)),
attributes
}))
.map(({ component, attributes }) => ({
component: ReactDOMServer.renderToStaticMarkup(component),
...attributes
}))
const availableFilters = mdParsed
.reduce((prev, content) => prev.concat(content), [])
.map(el =>
`{
name: "${el.name}",
displayName: "${el.displayName}",
component: ${el.name},
visible: ${el.visible},
content: {
enabled: ${el.enabled},
payload: "${el.payload}",
}
}`
)
const componentsAsTemplate = mdParsed.reduce(
(prev, { name, component }) => `${prev}\nconst ${name} = () => (<>${component}</>)`,
"")
const components = imageImports.reduce((p, c) => {
return p.replace(`"${c.componentName}"`, `{${c.componentName}}`)
}, componentsAsTemplate)
const imports = imageImports.reduce((p, c) => `${p}\nimport ${c.componentName} from "${c.href}"`, "")
const template = `// @ts-nocheck
${imports}
${components}
export const AVAILABLE_FLAGS = [${availableFilters}] as const;
`
fs.writeFileSync(path.resolve(__dirname, "./../.featureFlags/generated.tsx"), template)

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
import { useUser } from "@dashboard/auth"; import { useUser } from "@dashboard/auth";
import { staffMemberDetailsUrl } from "@dashboard/staff/urls"; import { staffMemberDetailsUrl } from "@dashboard/staff/urls";
import { useTheme } from "@dashboard/theme"; import { useTheme } from "@dashboard/theme";
@ -16,6 +15,7 @@ import React, { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { FeatureFlagsModal } from "./FeatureFlagsModal";
import { ThemeSwitcher } from "./ThemeSwitcher"; import { ThemeSwitcher } from "./ThemeSwitcher";
export const useLegacyThemeHandler = () => { export const useLegacyThemeHandler = () => {
@ -28,11 +28,11 @@ export const useLegacyThemeHandler = () => {
}; };
const handleStorage = (event: StorageEvent) => { const handleStorage = (event: StorageEvent) => {
if (!["macaw-ui-theme", "activeMacawUITheme"].includes(event.key)) { if (!["macaw-ui-theme", "activeMacawUITheme"].includes(event.key || "")) {
return; return;
} }
const isDark = event.newValue.toLowerCase().includes("dark"); const isDark = event.newValue?.toLowerCase().includes("dark");
setLegacyTheme(isDark ? "dark" : "light"); setLegacyTheme(isDark ? "dark" : "light");
setTheme(isDark ? "defaultDark" : "defaultLight"); setTheme(isDark ? "defaultDark" : "defaultLight");
}; };
@ -50,89 +50,107 @@ export const UserControls = () => {
const { user, logout } = useUser(); const { user, logout } = useUser();
const { changeTheme, theme } = useLegacyThemeHandler(); const { changeTheme, theme } = useLegacyThemeHandler();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [flagsModalOpen, setFlagsModalOpen] = useState(false);
return ( return (
<Dropdown <>
open={open} <Dropdown
onOpenChange={value => { open={open}
setOpen(value); onOpenChange={value => {
}} setOpen(value);
> }}
<Dropdown.Trigger> >
<Button <Dropdown.Trigger>
variant="tertiary" <Button
icon={<MoreOptionsIcon />} variant="tertiary"
data-test-id="userMenu" icon={<MoreOptionsIcon />}
size="medium" data-test-id="userMenu"
onClick={() => setOpen(true)} size="medium"
/> onClick={() => setOpen(true)}
</Dropdown.Trigger> />
<Dropdown.Content align="end"> </Dropdown.Trigger>
<Box __minWidth={192}> <Dropdown.Content align="end">
<List <Box __minWidth={192}>
padding={2} <List
borderRadius={4} padding={2}
boxShadow="overlay" borderRadius={4}
backgroundColor="surfaceNeutralPlain" boxShadow="overlay"
> backgroundColor="surfaceNeutralPlain"
<Dropdown.Item> >
<List.Item <Dropdown.Item>
borderRadius={4} <List.Item
data-test-id="account-settings-button" borderRadius={4}
onClick={() => setOpen(false)} data-test-id="account-settings-button"
> onClick={() => setOpen(false)}
<Link >
to={staffMemberDetailsUrl(user?.id)} <Link
className={sprinkles({ to={staffMemberDetailsUrl(user?.id || "")}
display: "block", className={sprinkles({
width: "100%", display: "block",
...listItemStyles, width: "100%",
})} ...listItemStyles,
})}
>
<Text>
<FormattedMessage
id="NQgbYA"
defaultMessage="Account Settings"
/>
</Text>
</Link>
</List.Item>
</Dropdown.Item>
<Dropdown.Item>
<List.Item
{...listItemStyles}
onClick={() => setFlagsModalOpen(true)}
> >
<Text> <Text>
<FormattedMessage <FormattedMessage
id="NQgbYA" id="38dc43"
defaultMessage="Account Settings" defaultMessage="Features preview"
description="Features preview"
/> />
</Text> </Text>
</Link> </List.Item>
</List.Item> </Dropdown.Item>
</Dropdown.Item> <Dropdown.Item>
<Dropdown.Item> <List.Item
<List.Item onClick={logout}
onClick={logout} {...listItemStyles}
{...listItemStyles} data-test-id="log-out-button"
data-test-id="log-out-button" >
> <Text>
<Text> <FormattedMessage
<FormattedMessage id="qLbse5"
id="qLbse5" defaultMessage="Log out"
defaultMessage="Log out" description="button"
description="button" />
/> </Text>
</Text> </List.Item>
</List.Item> </Dropdown.Item>
</Dropdown.Item> <Dropdown.Item>
<Dropdown.Item> <List.Item
<List.Item display="flex"
display="flex" alignItems="center"
alignItems="center" gap={2}
gap={2} marginTop={1}
marginTop={1} onClick={() => {
onClick={() => { changeTheme();
changeTheme(); setOpen(false);
setOpen(false); }}
}} {...listItemStyles}
{...listItemStyles} data-test-id="theme-switch"
data-test-id="theme-switch" >
> <ThemeSwitcher theme={theme} />
<ThemeSwitcher theme={theme} /> </List.Item>
</List.Item> </Dropdown.Item>
</Dropdown.Item> </List>
</List> </Box>
</Box> </Dropdown.Content>
</Dropdown.Content> </Dropdown>
</Dropdown> <FeatureFlagsModal open={flagsModalOpen} onChange={setFlagsModalOpen} />
</>
); );
}; };

View file

@ -0,0 +1,42 @@
import { Name } from "@dashboard/featureFlags/availableFlags";
import { Box, Text } from "@saleor/macaw-ui/next";
import React from "react";
import { ToggleButton } from "./ToggleButton";
interface ContentProps {
flagName: string;
flagSlug: Name;
component: () => JSX.Element;
isEnabled: boolean;
}
export const Content = ({
flagName,
flagSlug,
component,
isEnabled,
}: ContentProps) => {
const TabComponent = component;
return (
<Box
borderWidth={1}
borderStyle="solid"
borderLeftWidth={0}
backgroundColor="surfaceNeutralPlain"
borderColor="neutralHighlight"
width="100%"
overflowY="scroll"
padding={3}
>
<Box display="flex" justifyContent="space-between">
<Text variant="heading">{flagName}</Text>
<ToggleButton isEnabled={isEnabled} flagSlug={flagSlug} />
</Box>
<Box fontSize="bodyMedium" paddingBottom={12}>
<TabComponent />
</Box>
</Box>
);
};

View file

@ -0,0 +1,47 @@
import { Name } from "@dashboard/featureFlags/availableFlags";
import { useFlagsInfo } from "@dashboard/featureFlags/useFlagsInfo";
import { List, Text } from "@saleor/macaw-ui/next";
import React from "react";
interface FlagListProps {
selectedName: string;
onItemClick: (name: Name) => void;
}
export const FlagList = ({ selectedName, onItemClick }: FlagListProps) => {
const flags = useFlagsInfo();
return (
<List
__width="40%"
borderWidth={1}
borderStyle="solid"
borderBottomWidth={0}
borderColor="neutralHighlight"
overflowY="scroll"
>
{flags
.filter(flag => flag.visible)
.map(flag => (
<List.Item
key={flag.name}
paddingX={3}
paddingY={4}
backgroundColor={{
default:
selectedName === flag.name
? "surfaceNeutralHighlight"
: "surfaceNeutralSubdued",
hover: "surfaceNeutralHighlight",
}}
borderColor="neutralHighlight"
borderBottomWidth={1}
borderBottomStyle="solid"
onClick={() => onItemClick(flag.name)}
>
<Text variant="body">{flag.displayName}</Text>
</List.Item>
))}
</List>
);
};

View file

@ -0,0 +1,16 @@
import { Box, Text } from "@saleor/macaw-ui/next";
import React, { ReactNode } from "react";
import { FormattedMessage } from "react-intl";
interface HeaderProps {
children: ReactNode;
}
export const Header = ({ children }: HeaderProps) => (
<Box display="flex" justifyContent="space-between" paddingX={3} paddingY={4}>
<Text variant="heading">
<FormattedMessage defaultMessage="Features preview" id="krer6Z" />
</Text>
{children}
</Box>
);

View file

@ -0,0 +1,23 @@
import { Box, Text } from "@saleor/macaw-ui/next";
import React from "react";
import { FormattedMessage } from "react-intl";
export const NoFlags = () => (
<Box
width="100%"
display="flex"
justifyContent="center"
borderWidth={1}
borderStyle="solid"
borderTopWidth={1}
borderColor="neutralHighlight"
paddingTop={52}
>
<Text color="textNeutralDisabled" size="large">
<FormattedMessage
id="pEf/m+"
defaultMessage="There are no previews at the moment. Stay tuned!"
/>
</Text>
</Box>
);

View file

@ -0,0 +1,36 @@
import { Name } from "@dashboard/featureFlags/availableFlags";
import { Button, Spinner } from "@saleor/macaw-ui/next";
import React, { useState } from "react";
import { usePersistence } from "./usePersistence";
interface ToggleButtonProps {
isEnabled: boolean;
flagSlug: Name;
}
export const ToggleButton = ({ isEnabled, flagSlug }: ToggleButtonProps) => {
const { toggleFlag } = usePersistence();
const [loading, setLoading] = useState(false);
const handleToggleClick = async () => {
setLoading(true);
await toggleFlag(flagSlug);
window.location.reload();
};
if (loading) {
return (
<Button variant="secondary" size="small" disabled>
<Spinner /> {isEnabled ? "Disabling" : "Enabling"}
</Button>
);
}
return (
<Button variant="secondary" size="small" onClick={handleToggleClick}>
{isEnabled ? "Disable" : "Enable"}
</Button>
);
};

View file

@ -0,0 +1,72 @@
import { Name } from "@dashboard/featureFlags/availableFlags";
import { Box, Button, CloseIcon, Modal } from "@saleor/macaw-ui/next";
import React from "react";
import { Content } from "./Content";
import { FlagList } from "./FlagList";
import { Header } from "./Header";
import { NoFlags } from "./NoFlags";
import { useFlagsState } from "./useFlagsState";
interface FeatureFlagsModalProps {
open: boolean;
onChange: (open: boolean) => void;
}
export const FeatureFlagsModal = ({
open,
onChange,
}: FeatureFlagsModalProps) => {
const { selectedFlag, hasNoFlags, changeTab } = useFlagsState();
const handleTabClick = (tabName: Name) => {
changeTab(tabName);
};
return (
<Modal open={open} onChange={onChange}>
<Modal.Content>
<Box
backgroundColor="surfaceNeutralPlain"
borderRadius={3}
position="fixed"
boxShadow="modal"
overflow="hidden"
__left="50%"
__top="50%"
__width="960px"
__height="600px"
__transform="translate(-50%, -50%)"
>
<Header>
<Modal.Close>
<Button variant="tertiary" icon={<CloseIcon />} size="small" />
</Modal.Close>
</Header>
<Box
display="flex"
backgroundColor="surfaceNeutralPlain"
height="100%"
>
{hasNoFlags ? (
<NoFlags />
) : (
<>
<FlagList
selectedName={selectedFlag.name}
onItemClick={handleTabClick}
/>
<Content
flagName={selectedFlag.displayName}
flagSlug={selectedFlag.name as Name}
component={selectedFlag.component}
isEnabled={selectedFlag.content.enabled}
/>
</>
)}
</Box>
</Box>
</Modal.Content>
</Modal>
);
};

View file

@ -0,0 +1,31 @@
import { FlagDefinition, Name } from "@dashboard/featureFlags/availableFlags";
import { useFlagsInfo } from "@dashboard/featureFlags/useFlagsInfo";
import { useState } from "react";
type changeTab = (name: Name) => void;
type FlagsState =
| { hasNoFlags: false; selectedFlag: FlagDefinition; changeTab: changeTab }
| { hasNoFlags: true; selectedFlag: undefined; changeTab: changeTab };
export const useFlagsState = (): FlagsState => {
const flagsInfo = useFlagsInfo();
const flags = flagsInfo.filter(f => f.visible);
const firstFlag = flags.length > 0 ? flags[0].name : null;
const [tab, changeTab] = useState(firstFlag);
const selectedFlag = flags.find(f => f.name === tab);
if (!selectedFlag) {
return {
hasNoFlags: true,
selectedFlag: undefined,
changeTab,
};
}
return {
hasNoFlags: false,
selectedFlag,
changeTab,
};
};

View file

@ -0,0 +1,26 @@
import {
flagInfoToFlagList,
Name,
} from "@dashboard/featureFlags/availableFlags";
import { flagListToMetadata } from "@dashboard/featureFlags/strategies/MetadataStrategy";
import { useFlagsInfo } from "@dashboard/featureFlags/useFlagsInfo";
import { useUserAccountUpdateMutation } from "@dashboard/graphql";
export const usePersistence = () => {
const flags = useFlagsInfo();
const [updateAccount] = useUserAccountUpdateMutation();
const toggleFlag = async (flagName: Name) => {
const flagList = flagInfoToFlagList(flags);
flagList[flagName].enabled = !flagList[flagName].enabled;
const metadata = flagListToMetadata(flagList);
await updateAccount({
variables: { input: { metadata } },
});
};
return {
toggleFlag,
};
};

View file

@ -26,6 +26,7 @@ const user = {
userPermissions: staffMember.userPermissions, userPermissions: staffMember.userPermissions,
restrictedAccessToChannels: false, restrictedAccessToChannels: false,
accessibleChannels: [], accessibleChannels: [],
metadata: [],
}; };
const versions = { const versions = {

View file

@ -1,35 +1,65 @@
import { useUser } from "@dashboard/auth";
import LoginLoading from "@dashboard/auth/components/LoginLoading/LoginLoading"; import LoginLoading from "@dashboard/auth/components/LoginLoading/LoginLoading";
import React, { ReactNode, useEffect, useState } from "react"; import React, { ReactNode, useEffect, useState } from "react";
import { FlagList } from "./availableFlags"; import { FlagList } from "./availableFlags";
import { Provider } from "./context"; import { Provider } from "./context";
import { useFlagsResolver } from "./FlagsResolver"; import { FlagsResolver } from "./FlagsResolver";
import { AvailableStrategies } from "./strategies"; import {
AvailableStrategies,
EnvVarsStrategy,
LocalStorageStrategy,
} from "./strategies";
import { MetadataStrategy } from "./strategies/MetadataStrategy";
interface FeatureFlagsProviderProps { interface FeatureFlagsProviderProps {
children: ReactNode; children: ReactNode;
strategies: AvailableStrategies[]; strategies: AvailableStrategies[];
deps?: unknown[];
} }
export const FeatureFlagsProvider = ({ export const FeatureFlagsProvider = ({
children, children,
strategies, strategies,
}: FeatureFlagsProviderProps) => { }: FeatureFlagsProviderProps) => {
const resolver = useFlagsResolver(strategies);
const [flags, setFlags] = useState<FlagList | undefined>(undefined); const [flags, setFlags] = useState<FlagList | undefined>(undefined);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const disableLoading = () => setLoading(false); const disableLoading = () => setLoading(false);
useEffect(() => { useEffect(() => {
const resolver = new FlagsResolver(strategies);
resolver resolver
.fetchAll() .fetchAll()
.combineWithPriorities() .combineWithPriorities()
.then(setFlags) .then(setFlags)
.finally(disableLoading); .finally(disableLoading);
}, []); }, [strategies]);
return ( return (
<Provider value={flags}>{loading ? <LoginLoading /> : children}</Provider> <Provider value={flags}>{loading ? <LoginLoading /> : children}</Provider>
); );
}; };
interface FeatureFlagsProviderWithUserProps {
children: ReactNode;
}
export const FeatureFlagsProviderWithUser = ({
children,
}: FeatureFlagsProviderWithUserProps) => {
const user = useUser();
return (
<FeatureFlagsProvider
strategies={[
new LocalStorageStrategy(),
new EnvVarsStrategy(),
new MetadataStrategy(user.user?.metadata || []),
]}
>
{children}
</FeatureFlagsProvider>
);
};

View file

@ -1,7 +1,4 @@
import { useRef } from "react";
import * as AvailableFlags from "./../availableFlags"; import * as AvailableFlags from "./../availableFlags";
import { AvailableStrategies } from "./../strategies";
import { DefaultsStrategy } from "./../strategies/DefaultsStrategy"; import { DefaultsStrategy } from "./../strategies/DefaultsStrategy";
import { Strategy } from "./../Strategy"; import { Strategy } from "./../Strategy";
import { reduceFlagListArray } from "./reduceFlagListArray"; import { reduceFlagListArray } from "./reduceFlagListArray";
@ -35,9 +32,3 @@ export class FlagsResolver {
return this.results; return this.results;
} }
} }
export const useFlagsResolver = (strategies: AvailableStrategies[]) => {
const resolver = useRef<FlagsResolver>(new FlagsResolver(strategies));
return resolver.current;
};

View file

@ -1,35 +1,18 @@
import { AVAILABLE_FLAGS as GENERATED_FLAGS } from "./../../.featureFlags/generated";
import { FlagValue } from "./FlagContent"; import { FlagValue } from "./FlagContent";
interface FlagDefinition { export interface FlagDefinition {
name: string; name: string;
displayName: string; displayName: string;
description: string; component: () => JSX.Element;
visible: boolean;
content: { content: {
enabled: boolean; enabled: boolean;
payload?: string; payload?: string;
}; };
} }
const AVAILABLE_FLAGS = [ export const AVAILABLE_FLAGS = GENERATED_FLAGS;
/*
Before use any flag pleease an entry within this array,
so the TS will infer the types, example:
{
name: "flag1",
displayName: "Flag 1",
description: "some description",
content: { enabled: false, value: "default" },
} as const,
*/
{
name: "product_filters",
displayName: "Product filters",
description: "New filters on product listing page",
content: { enabled: false, payload: "" },
} as const,
] satisfies FlagDefinition[];
type TypedEntry = (typeof AVAILABLE_FLAGS)[number]; type TypedEntry = (typeof AVAILABLE_FLAGS)[number];
type GeneralEntry = TypedEntry extends never ? FlagDefinition : TypedEntry; type GeneralEntry = TypedEntry extends never ? FlagDefinition : TypedEntry;
@ -58,3 +41,14 @@ export const asFlagInfoArray = (list: GeneralFlagList) =>
...el, ...el,
content: list[el.name], content: list[el.name],
})); }));
export const flagInfoToFlagList = (flagInfos: FlagDefinition[]): FlagList => {
return flagInfos.reduce((prev, curr) => {
prev[curr.name as Name] = new FlagValue(
curr.content.enabled,
curr.content.payload,
);
return prev;
}, {} as FlagList);
};

View file

@ -0,0 +1,41 @@
import { MetadataItemFragment } from "@dashboard/graphql";
import { asFlagValue, FlagList } from "../availableFlags";
import * as AvailableFlags from "../availableFlags";
import { Strategy } from "../Strategy";
const METADATA_KEY = "feature_flags";
export const flagListToMetadata = (
list: FlagList,
): Array<Omit<MetadataItemFragment, "__typename">> => {
return [{ key: METADATA_KEY, value: JSON.stringify(list) }];
};
export const flagListFromMetadata = (
metadata: Array<Omit<MetadataItemFragment, "__typename">>,
): FlagList => {
const defaultsList = asFlagValue();
const rawFlags = metadata.find(x => x.key === METADATA_KEY);
const flagList = rawFlags
? JSON.parse(rawFlags.value)
: (defaultsList as FlagList);
return Object.keys(defaultsList).reduce((list: FlagList, key: string) => {
list[key as AvailableFlags.Name] = defaultsList[key as AvailableFlags.Name];
if (flagList[key]) {
list[key as AvailableFlags.Name] = flagList[key];
}
return list;
}, {} as FlagList);
};
export class MetadataStrategy implements Strategy {
constructor(public metadata: MetadataItemFragment[]) {}
fetchAll(): Promise<AvailableFlags.FlagList> {
return Promise.resolve(flagListFromMetadata(this.metadata));
}
}

View file

@ -1,6 +1,10 @@
import { EnvVarsStrategy } from "./EnvVarsStrategy"; import { EnvVarsStrategy } from "./EnvVarsStrategy";
import { LocalStorageStrategy } from "./LocalStorageStrategy"; import { LocalStorageStrategy } from "./LocalStorageStrategy";
import { MetadataStrategy } from "./MetadataStrategy";
export { EnvVarsStrategy, LocalStorageStrategy }; export { EnvVarsStrategy, LocalStorageStrategy, MetadataStrategy };
export type AvailableStrategies = EnvVarsStrategy | LocalStorageStrategy; export type AvailableStrategies =
| EnvVarsStrategy
| LocalStorageStrategy
| MetadataStrategy;

View file

@ -23,6 +23,10 @@ export const fragmentUser = gql`
firstName firstName
lastName lastName
isStaff isStaff
metadata {
key
value
}
userPermissions { userPermissions {
...UserPermission ...UserPermission
} }

View file

@ -24,6 +24,10 @@ export const staffMemberDetailsFragment = gql`
avatar(size: 512) { avatar(size: 512) {
url url
} }
metadata {
key
value
}
} }
`; `;

View file

@ -202,6 +202,10 @@ export const UserFragmentDoc = gql`
firstName firstName
lastName lastName
isStaff isStaff
metadata {
key
value
}
userPermissions { userPermissions {
...UserPermission ...UserPermission
} }
@ -2758,6 +2762,10 @@ export const StaffMemberDetailsFragmentDoc = gql`
avatar(size: 512) { avatar(size: 512) {
url url
} }
metadata {
key
value
}
} }
${StaffMemberFragmentDoc}`; ${StaffMemberFragmentDoc}`;
export const CountryFragmentDoc = gql` export const CountryFragmentDoc = gql`

View file

@ -8375,7 +8375,7 @@ export type AvailableExternalAuthenticationsQuery = { __typename: 'Query', shop:
export type UserDetailsQueryVariables = Exact<{ [key: string]: never; }>; export type UserDetailsQueryVariables = Exact<{ [key: string]: never; }>;
export type UserDetailsQuery = { __typename: 'Query', me: { __typename: 'User', id: string, email: string, firstName: string, lastName: string, isStaff: boolean, restrictedAccessToChannels: boolean, userPermissions: Array<{ __typename: 'UserPermission', code: PermissionEnum, name: string }> | null, avatar: { __typename: 'Image', url: string } | null, accessibleChannels: Array<{ __typename: 'Channel', id: string, isActive: boolean, name: string, slug: string, currencyCode: string, defaultCountry: { __typename: 'CountryDisplay', code: string, country: string }, stockSettings: { __typename: 'StockSettings', allocationStrategy: AllocationStrategyEnum } }> | null } | null }; export type UserDetailsQuery = { __typename: 'Query', me: { __typename: 'User', id: string, email: string, firstName: string, lastName: string, isStaff: boolean, restrictedAccessToChannels: boolean, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, userPermissions: Array<{ __typename: 'UserPermission', code: PermissionEnum, name: string }> | null, avatar: { __typename: 'Image', url: string } | null, accessibleChannels: Array<{ __typename: 'Channel', id: string, isActive: boolean, name: string, slug: string, currencyCode: string, defaultCountry: { __typename: 'CountryDisplay', code: string, country: string }, stockSettings: { __typename: 'StockSettings', allocationStrategy: AllocationStrategyEnum } }> | null } | null };
export type CategoryDeleteMutationVariables = Exact<{ export type CategoryDeleteMutationVariables = Exact<{
id: Scalars['ID']; id: Scalars['ID'];
@ -9092,7 +9092,7 @@ export type UserPermissionFragment = { __typename: 'UserPermission', code: Permi
export type UserUserPermissionWithSourcePermissionGroupsFragment = { __typename: 'UserPermission', code: PermissionEnum, name: string, sourcePermissionGroups: Array<{ __typename: 'Group', id: string }> | null }; export type UserUserPermissionWithSourcePermissionGroupsFragment = { __typename: 'UserPermission', code: PermissionEnum, name: string, sourcePermissionGroups: Array<{ __typename: 'Group', id: string }> | null };
export type UserFragment = { __typename: 'User', id: string, email: string, firstName: string, lastName: string, isStaff: boolean, restrictedAccessToChannels: boolean, userPermissions: Array<{ __typename: 'UserPermission', code: PermissionEnum, name: string }> | null, avatar: { __typename: 'Image', url: string } | null, accessibleChannels: Array<{ __typename: 'Channel', id: string, isActive: boolean, name: string, slug: string, currencyCode: string, defaultCountry: { __typename: 'CountryDisplay', code: string, country: string }, stockSettings: { __typename: 'StockSettings', allocationStrategy: AllocationStrategyEnum } }> | null }; export type UserFragment = { __typename: 'User', id: string, email: string, firstName: string, lastName: string, isStaff: boolean, restrictedAccessToChannels: boolean, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, userPermissions: Array<{ __typename: 'UserPermission', code: PermissionEnum, name: string }> | null, avatar: { __typename: 'Image', url: string } | null, accessibleChannels: Array<{ __typename: 'Channel', id: string, isActive: boolean, name: string, slug: string, currencyCode: string, defaultCountry: { __typename: 'CountryDisplay', code: string, country: string }, stockSettings: { __typename: 'StockSettings', allocationStrategy: AllocationStrategyEnum } }> | null };
export type UserBaseFragment = { __typename: 'User', id: string, firstName: string, lastName: string }; export type UserBaseFragment = { __typename: 'User', id: string, firstName: string, lastName: string };
@ -9488,7 +9488,7 @@ export type ShopFragment = { __typename: 'Shop', customerSetPasswordUrl: string
export type StaffMemberFragment = { __typename: 'User', id: string, email: string, firstName: string, isActive: boolean, lastName: string }; export type StaffMemberFragment = { __typename: 'User', id: string, email: string, firstName: string, isActive: boolean, lastName: string };
export type StaffMemberDetailsFragment = { __typename: 'User', id: string, email: string, firstName: string, isActive: boolean, lastName: string, permissionGroups: Array<{ __typename: 'Group', id: string, name: string, userCanManage: boolean }> | null, userPermissions: Array<{ __typename: 'UserPermission', code: PermissionEnum, name: string }> | null, avatar: { __typename: 'Image', url: string } | null }; export type StaffMemberDetailsFragment = { __typename: 'User', id: string, email: string, firstName: string, isActive: boolean, lastName: string, permissionGroups: Array<{ __typename: 'Group', id: string, name: string, userCanManage: boolean }> | null, userPermissions: Array<{ __typename: 'UserPermission', code: PermissionEnum, name: string }> | null, avatar: { __typename: 'Image', url: string } | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> };
export type StaffMemberAvatarFragment = { __typename: 'User', id: string, email: string, firstName: string, isActive: boolean, lastName: string, avatar: { __typename: 'Image', url: string } | null }; export type StaffMemberAvatarFragment = { __typename: 'User', id: string, email: string, firstName: string, isActive: boolean, lastName: string, avatar: { __typename: 'Image', url: string } | null };
@ -11095,7 +11095,7 @@ export type StaffMemberAddMutationVariables = Exact<{
}>; }>;
export type StaffMemberAddMutation = { __typename: 'Mutation', staffCreate: { __typename: 'StaffCreate', errors: Array<{ __typename: 'StaffError', code: AccountErrorCode, field: string | null, message: string | null }>, user: { __typename: 'User', id: string, email: string, firstName: string, isActive: boolean, lastName: string, permissionGroups: Array<{ __typename: 'Group', id: string, name: string, userCanManage: boolean }> | null, userPermissions: Array<{ __typename: 'UserPermission', code: PermissionEnum, name: string }> | null, avatar: { __typename: 'Image', url: string } | null } | null } | null }; export type StaffMemberAddMutation = { __typename: 'Mutation', staffCreate: { __typename: 'StaffCreate', errors: Array<{ __typename: 'StaffError', code: AccountErrorCode, field: string | null, message: string | null }>, user: { __typename: 'User', id: string, email: string, firstName: string, isActive: boolean, lastName: string, permissionGroups: Array<{ __typename: 'Group', id: string, name: string, userCanManage: boolean }> | null, userPermissions: Array<{ __typename: 'UserPermission', code: PermissionEnum, name: string }> | null, avatar: { __typename: 'Image', url: string } | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null } | null };
export type StaffMemberUpdateMutationVariables = Exact<{ export type StaffMemberUpdateMutationVariables = Exact<{
id: Scalars['ID']; id: Scalars['ID'];
@ -11103,7 +11103,7 @@ export type StaffMemberUpdateMutationVariables = Exact<{
}>; }>;
export type StaffMemberUpdateMutation = { __typename: 'Mutation', staffUpdate: { __typename: 'StaffUpdate', errors: Array<{ __typename: 'StaffError', code: AccountErrorCode, field: string | null, message: string | null }>, user: { __typename: 'User', id: string, email: string, firstName: string, isActive: boolean, lastName: string, permissionGroups: Array<{ __typename: 'Group', id: string, name: string, userCanManage: boolean }> | null, userPermissions: Array<{ __typename: 'UserPermission', code: PermissionEnum, name: string }> | null, avatar: { __typename: 'Image', url: string } | null } | null } | null }; export type StaffMemberUpdateMutation = { __typename: 'Mutation', staffUpdate: { __typename: 'StaffUpdate', errors: Array<{ __typename: 'StaffError', code: AccountErrorCode, field: string | null, message: string | null }>, user: { __typename: 'User', id: string, email: string, firstName: string, isActive: boolean, lastName: string, permissionGroups: Array<{ __typename: 'Group', id: string, name: string, userCanManage: boolean }> | null, userPermissions: Array<{ __typename: 'UserPermission', code: PermissionEnum, name: string }> | null, avatar: { __typename: 'Image', url: string } | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null } | null };
export type UserPassowrdChangeMutationVariables = Exact<{ export type UserPassowrdChangeMutationVariables = Exact<{
newPassword: Scalars['String']; newPassword: Scalars['String'];
@ -11164,7 +11164,7 @@ export type StaffMemberDetailsQueryVariables = Exact<{
}>; }>;
export type StaffMemberDetailsQuery = { __typename: 'Query', user: { __typename: 'User', id: string, email: string, firstName: string, isActive: boolean, lastName: string, permissionGroups: Array<{ __typename: 'Group', id: string, name: string, userCanManage: boolean }> | null, userPermissions: Array<{ __typename: 'UserPermission', code: PermissionEnum, name: string }> | null, avatar: { __typename: 'Image', url: string } | null } | null }; export type StaffMemberDetailsQuery = { __typename: 'Query', user: { __typename: 'User', id: string, email: string, firstName: string, isActive: boolean, lastName: string, permissionGroups: Array<{ __typename: 'Group', id: string, name: string, userCanManage: boolean }> | null, userPermissions: Array<{ __typename: 'UserPermission', code: PermissionEnum, name: string }> | null, avatar: { __typename: 'Image', url: string } | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null };
export type TaxConfigurationUpdateMutationVariables = Exact<{ export type TaxConfigurationUpdateMutationVariables = Exact<{
id: Scalars['ID']; id: Scalars['ID'];

View file

@ -52,11 +52,7 @@ import CustomAppsSection from "./custom-apps";
import { CustomAppSections } from "./custom-apps/urls"; import { CustomAppSections } from "./custom-apps/urls";
import { CustomerSection } from "./customers"; import { CustomerSection } from "./customers";
import DiscountSection from "./discounts"; import DiscountSection from "./discounts";
import { import { FeatureFlagsProviderWithUser } from "./featureFlags/FeatureFlagsProvider";
EnvVarsStrategy,
FeatureFlagsProvider,
LocalStorageStrategy,
} from "./featureFlags";
import GiftCardSection from "./giftCards"; import GiftCardSection from "./giftCards";
import { giftCardsSectionUrlName } from "./giftCards/urls"; import { giftCardsSectionUrlName } from "./giftCards/urls";
import { apolloClient, saleorClient } from "./graphql/client"; import { apolloClient, saleorClient } from "./graphql/client";
@ -121,26 +117,19 @@ const App: React.FC = () => (
<ServiceWorker /> <ServiceWorker />
<BackgroundTasksProvider> <BackgroundTasksProvider>
<AppStateProvider> <AppStateProvider>
<FeatureFlagsProvider <AuthProvider>
strategies={[ <ShopProvider>
new LocalStorageStrategy(), <AppChannelProvider>
new EnvVarsStrategy(), <ExternalAppProvider>
]} <ExitFormDialogProvider>
> <DevModeProvider>
<AuthProvider> <Routes />
<ShopProvider> </DevModeProvider>
<AppChannelProvider> </ExitFormDialogProvider>
<ExternalAppProvider> </ExternalAppProvider>
<ExitFormDialogProvider> </AppChannelProvider>
<DevModeProvider> </ShopProvider>
<Routes /> </AuthProvider>
</DevModeProvider>
</ExitFormDialogProvider>
</ExternalAppProvider>
</AppChannelProvider>
</ShopProvider>
</AuthProvider>
</FeatureFlagsProvider>
</AppStateProvider> </AppStateProvider>
</BackgroundTasksProvider> </BackgroundTasksProvider>
</MessageManagerProvider> </MessageManagerProvider>
@ -173,155 +162,157 @@ const Routes: React.FC = () => {
<WindowTitle title={intl.formatMessage(commonMessages.dashboard)} /> <WindowTitle title={intl.formatMessage(commonMessages.dashboard)} />
{DEMO_MODE && <DemoBanner />} {DEMO_MODE && <DemoBanner />}
{homePageLoaded ? ( {homePageLoaded ? (
<AppLayout fullSize={isAppPath}> <FeatureFlagsProviderWithUser>
<ErrorBoundary <AppLayout fullSize={isAppPath}>
onError={e => { <ErrorBoundary
const errorId = errorTracker.captureException(e); onError={e => {
const errorId = errorTracker.captureException(e);
dispatchAppState({ dispatchAppState({
payload: { payload: {
error: "unhandled", error: "unhandled",
errorId, errorId,
}, },
type: "displayError", type: "displayError",
}); });
}} }}
fallbackRender={({ resetErrorBoundary }) => ( fallbackRender={({ resetErrorBoundary }) => (
<ErrorPage <ErrorPage
onBack={resetErrorBoundary} onBack={resetErrorBoundary}
onRefresh={() => window.location.reload()} onRefresh={() => window.location.reload()}
/> />
)} )}
> >
<Switch> <Switch>
<SectionRoute exact path="/" component={HomePage} /> <SectionRoute exact path="/" component={HomePage} />
<SectionRoute <SectionRoute
permissions={[PermissionEnum.MANAGE_PRODUCTS]} permissions={[PermissionEnum.MANAGE_PRODUCTS]}
path="/categories" path="/categories"
component={CategorySection} component={CategorySection}
/> />
<SectionRoute <SectionRoute
permissions={[PermissionEnum.MANAGE_PRODUCTS]} permissions={[PermissionEnum.MANAGE_PRODUCTS]}
path="/collections" path="/collections"
component={CollectionSection} component={CollectionSection}
/> />
<SectionRoute <SectionRoute
permissions={[PermissionEnum.MANAGE_USERS]} permissions={[PermissionEnum.MANAGE_USERS]}
path="/customers" path="/customers"
component={CustomerSection} component={CustomerSection}
/> />
<SectionRoute <SectionRoute
permissions={[PermissionEnum.MANAGE_GIFT_CARD]} permissions={[PermissionEnum.MANAGE_GIFT_CARD]}
path={giftCardsSectionUrlName} path={giftCardsSectionUrlName}
component={GiftCardSection} component={GiftCardSection}
/> />
<SectionRoute <SectionRoute
permissions={[PermissionEnum.MANAGE_DISCOUNTS]} permissions={[PermissionEnum.MANAGE_DISCOUNTS]}
path="/discounts" path="/discounts"
component={DiscountSection} component={DiscountSection}
/> />
<SectionRoute <SectionRoute
permissions={[PermissionEnum.MANAGE_PAGES]} permissions={[PermissionEnum.MANAGE_PAGES]}
path="/pages" path="/pages"
component={PageSection} component={PageSection}
/> />
<SectionRoute <SectionRoute
permissions={[ permissions={[
PermissionEnum.MANAGE_PAGES, PermissionEnum.MANAGE_PAGES,
PermissionEnum.MANAGE_PAGE_TYPES_AND_ATTRIBUTES, PermissionEnum.MANAGE_PAGE_TYPES_AND_ATTRIBUTES,
]} ]}
path="/page-types" path="/page-types"
component={PageTypesSection} component={PageTypesSection}
matchPermission="any" matchPermission="any"
/> />
<SectionRoute <SectionRoute
permissions={[PermissionEnum.MANAGE_PLUGINS]} permissions={[PermissionEnum.MANAGE_PLUGINS]}
path="/plugins" path="/plugins"
component={PluginsSection} component={PluginsSection}
/> />
<SectionRoute <SectionRoute
permissions={[PermissionEnum.MANAGE_ORDERS]} permissions={[PermissionEnum.MANAGE_ORDERS]}
path="/orders" path="/orders"
component={OrdersSection} component={OrdersSection}
/> />
<SectionRoute <SectionRoute
permissions={[PermissionEnum.MANAGE_PRODUCTS]} permissions={[PermissionEnum.MANAGE_PRODUCTS]}
path="/products" path="/products"
component={ProductSection} component={ProductSection}
/> />
<SectionRoute <SectionRoute
permissions={[ permissions={[
PermissionEnum.MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES, PermissionEnum.MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES,
]} ]}
path="/product-types" path="/product-types"
component={ProductTypesSection} component={ProductTypesSection}
/> />
<SectionRoute path="/staff" component={StaffSection} /> <SectionRoute path="/staff" component={StaffSection} />
<SectionRoute <SectionRoute
permissions={[PermissionEnum.MANAGE_STAFF]} permissions={[PermissionEnum.MANAGE_STAFF]}
path="/permission-groups" path="/permission-groups"
component={PermissionGroupSection} component={PermissionGroupSection}
/> />
<SectionRoute <SectionRoute
permissions={[PermissionEnum.MANAGE_SETTINGS]} permissions={[PermissionEnum.MANAGE_SETTINGS]}
path="/site-settings" path="/site-settings"
component={SiteSettingsSection} component={SiteSettingsSection}
/> />
<SectionRoute path="/taxes" component={TaxesSection} /> <SectionRoute path="/taxes" component={TaxesSection} />
<SectionRoute <SectionRoute
permissions={[PermissionEnum.MANAGE_SHIPPING]} permissions={[PermissionEnum.MANAGE_SHIPPING]}
path="/shipping" path="/shipping"
component={ShippingSection} component={ShippingSection}
/> />
<SectionRoute <SectionRoute
permissions={[PermissionEnum.MANAGE_TRANSLATIONS]} permissions={[PermissionEnum.MANAGE_TRANSLATIONS]}
path="/translations" path="/translations"
component={TranslationsSection} component={TranslationsSection}
/> />
<SectionRoute <SectionRoute
permissions={[PermissionEnum.MANAGE_MENUS]} permissions={[PermissionEnum.MANAGE_MENUS]}
path={navigationSection} path={navigationSection}
component={NavigationSection} component={NavigationSection}
/> />
<SectionRoute <SectionRoute
permissions={[ permissions={[
PermissionEnum.MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES, PermissionEnum.MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES,
PermissionEnum.MANAGE_PAGE_TYPES_AND_ATTRIBUTES, PermissionEnum.MANAGE_PAGE_TYPES_AND_ATTRIBUTES,
]} ]}
path={attributeSection} path={attributeSection}
component={AttributeSection} component={AttributeSection}
matchPermission="any" matchPermission="any"
/> />
<SectionRoute <SectionRoute
permissions={[PermissionEnum.MANAGE_APPS]} permissions={[PermissionEnum.MANAGE_APPS]}
path={AppSections.appsSection} path={AppSections.appsSection}
component={AppsSectionRoot} component={AppsSectionRoot}
/> />
<SectionRoute <SectionRoute
permissions={[PermissionEnum.MANAGE_PRODUCTS]} permissions={[PermissionEnum.MANAGE_PRODUCTS]}
path={warehouseSection} path={warehouseSection}
component={WarehouseSection} component={WarehouseSection}
/> />
<SectionRoute <SectionRoute
permissions={[PermissionEnum.MANAGE_CHANNELS]} permissions={[PermissionEnum.MANAGE_CHANNELS]}
path={channelsSection} path={channelsSection}
component={ChannelsSection} component={ChannelsSection}
/> />
<SectionRoute <SectionRoute
matchPermission="any" matchPermission="any"
permissions={getConfigMenuItemsPermissions(intl)} permissions={getConfigMenuItemsPermissions(intl)}
exact exact
path="/configuration" path="/configuration"
component={ConfigurationSection} component={ConfigurationSection}
/> />
<SectionRoute <SectionRoute
path={CustomAppSections.appsSection} path={CustomAppSections.appsSection}
component={CustomAppsSection} component={CustomAppsSection}
/> />
<Route component={NotFound} /> <Route component={NotFound} />
</Switch> </Switch>
</ErrorBoundary> </ErrorBoundary>
</AppLayout> </AppLayout>
</FeatureFlagsProviderWithUser>
) : homePageLoading ? ( ) : homePageLoading ? (
<LoginLoading /> <LoginLoading />
) : ( ) : (

View file

@ -155,6 +155,7 @@ export const staffMember: StaffMemberDetailsFragment = {
...p, ...p,
__typename: "UserPermission", __typename: "UserPermission",
})), })),
metadata: [],
}; };
export const staffMemberAvatar: StaffMemberAvatarFragment = { export const staffMemberAvatar: StaffMemberAvatarFragment = {