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:
parent
e356674c76
commit
aaf646399e
30 changed files with 2748 additions and 433 deletions
19
.featureFlags/generated.tsx
Normal file
19
.featureFlags/generated.tsx
Normal 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;
|
BIN
.featureFlags/images/filters.png
Normal file
BIN
.featureFlags/images/filters.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 71 KiB |
12
.featureFlags/product-filters.md
Normal file
12
.featureFlags/product-filters.md
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
name: product_filters
|
||||||
|
displayName: Products filtering
|
||||||
|
enabled: false
|
||||||
|
payload: "default"
|
||||||
|
visible: true
|
||||||
|
---
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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
1
.gitignore
vendored
|
@ -53,3 +53,4 @@ django-queries-results.html
|
||||||
|
|
||||||
!.env.template
|
!.env.template
|
||||||
!.dependency-cruiser.js
|
!.dependency-cruiser.js
|
||||||
|
!.featureFlags
|
||||||
|
|
|
@ -27,6 +27,7 @@ export const MockedUserProvider: React.FC<{
|
||||||
__typename: "User",
|
__typename: "User",
|
||||||
accessibleChannels: [],
|
accessibleChannels: [],
|
||||||
restrictedAccessToChannels: false,
|
restrictedAccessToChannels: false,
|
||||||
|
metadata: []
|
||||||
},
|
},
|
||||||
errors: [],
|
errors: [],
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
2076
package-lock.json
generated
2076
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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
78
scripts/build-flags.js
Normal 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)
|
|
@ -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,8 +50,10 @@ 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
|
<Dropdown
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={value => {
|
onOpenChange={value => {
|
||||||
|
@ -82,7 +84,7 @@ export const UserControls = () => {
|
||||||
onClick={() => setOpen(false)}
|
onClick={() => setOpen(false)}
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
to={staffMemberDetailsUrl(user?.id)}
|
to={staffMemberDetailsUrl(user?.id || "")}
|
||||||
className={sprinkles({
|
className={sprinkles({
|
||||||
display: "block",
|
display: "block",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
@ -98,6 +100,20 @@ export const UserControls = () => {
|
||||||
</Link>
|
</Link>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
|
<Dropdown.Item>
|
||||||
|
<List.Item
|
||||||
|
{...listItemStyles}
|
||||||
|
onClick={() => setFlagsModalOpen(true)}
|
||||||
|
>
|
||||||
|
<Text>
|
||||||
|
<FormattedMessage
|
||||||
|
id="38dc43"
|
||||||
|
defaultMessage="Features preview"
|
||||||
|
description="Features preview"
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
</List.Item>
|
||||||
|
</Dropdown.Item>
|
||||||
<Dropdown.Item>
|
<Dropdown.Item>
|
||||||
<List.Item
|
<List.Item
|
||||||
onClick={logout}
|
onClick={logout}
|
||||||
|
@ -133,6 +149,8 @@ export const UserControls = () => {
|
||||||
</Box>
|
</Box>
|
||||||
</Dropdown.Content>
|
</Dropdown.Content>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
<FeatureFlagsModal open={flagsModalOpen} onChange={setFlagsModalOpen} />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
42
src/components/Sidebar/user/FeatureFlagsModal/Content.tsx
Normal file
42
src/components/Sidebar/user/FeatureFlagsModal/Content.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
47
src/components/Sidebar/user/FeatureFlagsModal/FlagList.tsx
Normal file
47
src/components/Sidebar/user/FeatureFlagsModal/FlagList.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
16
src/components/Sidebar/user/FeatureFlagsModal/Header.tsx
Normal file
16
src/components/Sidebar/user/FeatureFlagsModal/Header.tsx
Normal 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>
|
||||||
|
);
|
23
src/components/Sidebar/user/FeatureFlagsModal/NoFlags.tsx
Normal file
23
src/components/Sidebar/user/FeatureFlagsModal/NoFlags.tsx
Normal 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>
|
||||||
|
);
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
72
src/components/Sidebar/user/FeatureFlagsModal/index.tsx
Normal file
72
src/components/Sidebar/user/FeatureFlagsModal/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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,
|
||||||
|
};
|
||||||
|
};
|
|
@ -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,
|
||||||
|
};
|
||||||
|
};
|
|
@ -26,6 +26,7 @@ const user = {
|
||||||
userPermissions: staffMember.userPermissions,
|
userPermissions: staffMember.userPermissions,
|
||||||
restrictedAccessToChannels: false,
|
restrictedAccessToChannels: false,
|
||||||
accessibleChannels: [],
|
accessibleChannels: [],
|
||||||
|
metadata: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const versions = {
|
const versions = {
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -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;
|
|
||||||
};
|
|
||||||
|
|
|
@ -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);
|
||||||
|
};
|
||||||
|
|
41
src/featureFlags/strategies/MetadataStrategy.ts
Normal file
41
src/featureFlags/strategies/MetadataStrategy.ts
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -23,6 +23,10 @@ export const fragmentUser = gql`
|
||||||
firstName
|
firstName
|
||||||
lastName
|
lastName
|
||||||
isStaff
|
isStaff
|
||||||
|
metadata {
|
||||||
|
key
|
||||||
|
value
|
||||||
|
}
|
||||||
userPermissions {
|
userPermissions {
|
||||||
...UserPermission
|
...UserPermission
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,10 @@ export const staffMemberDetailsFragment = gql`
|
||||||
avatar(size: 512) {
|
avatar(size: 512) {
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
|
metadata {
|
||||||
|
key
|
||||||
|
value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
@ -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`
|
||||||
|
|
|
@ -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'];
|
||||||
|
|
|
@ -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,12 +117,6 @@ const App: React.FC = () => (
|
||||||
<ServiceWorker />
|
<ServiceWorker />
|
||||||
<BackgroundTasksProvider>
|
<BackgroundTasksProvider>
|
||||||
<AppStateProvider>
|
<AppStateProvider>
|
||||||
<FeatureFlagsProvider
|
|
||||||
strategies={[
|
|
||||||
new LocalStorageStrategy(),
|
|
||||||
new EnvVarsStrategy(),
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<ShopProvider>
|
<ShopProvider>
|
||||||
<AppChannelProvider>
|
<AppChannelProvider>
|
||||||
|
@ -140,7 +130,6 @@ const App: React.FC = () => (
|
||||||
</AppChannelProvider>
|
</AppChannelProvider>
|
||||||
</ShopProvider>
|
</ShopProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</FeatureFlagsProvider>
|
|
||||||
</AppStateProvider>
|
</AppStateProvider>
|
||||||
</BackgroundTasksProvider>
|
</BackgroundTasksProvider>
|
||||||
</MessageManagerProvider>
|
</MessageManagerProvider>
|
||||||
|
@ -173,6 +162,7 @@ const Routes: React.FC = () => {
|
||||||
<WindowTitle title={intl.formatMessage(commonMessages.dashboard)} />
|
<WindowTitle title={intl.formatMessage(commonMessages.dashboard)} />
|
||||||
{DEMO_MODE && <DemoBanner />}
|
{DEMO_MODE && <DemoBanner />}
|
||||||
{homePageLoaded ? (
|
{homePageLoaded ? (
|
||||||
|
<FeatureFlagsProviderWithUser>
|
||||||
<AppLayout fullSize={isAppPath}>
|
<AppLayout fullSize={isAppPath}>
|
||||||
<ErrorBoundary
|
<ErrorBoundary
|
||||||
onError={e => {
|
onError={e => {
|
||||||
|
@ -322,6 +312,7 @@ const Routes: React.FC = () => {
|
||||||
</Switch>
|
</Switch>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
|
</FeatureFlagsProviderWithUser>
|
||||||
) : homePageLoading ? (
|
) : homePageLoading ? (
|
||||||
<LoginLoading />
|
<LoginLoading />
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -155,6 +155,7 @@ export const staffMember: StaffMemberDetailsFragment = {
|
||||||
...p,
|
...p,
|
||||||
__typename: "UserPermission",
|
__typename: "UserPermission",
|
||||||
})),
|
})),
|
||||||
|
metadata: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const staffMemberAvatar: StaffMemberAvatarFragment = {
|
export const staffMemberAvatar: StaffMemberAvatarFragment = {
|
||||||
|
|
Loading…
Reference in a new issue