Merge pull request #135 from mirumee/add/dynamic-dashboard-settings

Add dynamic dashboard settings
This commit is contained in:
Marcin Gębala 2019-08-30 15:31:32 +02:00 committed by GitHub
commit a940e45a4d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 2867 additions and 762 deletions

View file

@ -10,3 +10,4 @@ All notable, unreleased changes to this project will be documented in this file.
- Add switch to make attribute available in product list as a column - #99 by @dominik-zeglen
- Add tc tags for E2E testing - #134 by @dominik-zeglen
- Use react-intl - #105 by @dominik-zeglen
- Add dynamic dashboard settings - #135 by @benekex2

41
package-lock.json generated
View file

@ -9409,7 +9409,8 @@
},
"ansi-regex": {
"version": "2.1.1",
"bundled": true
"bundled": true,
"optional": true
},
"aproba": {
"version": "1.2.0",
@ -9427,11 +9428,13 @@
},
"balanced-match": {
"version": "1.0.0",
"bundled": true
"bundled": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -9444,15 +9447,18 @@
},
"code-point-at": {
"version": "1.1.0",
"bundled": true
"bundled": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true
"bundled": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true
"bundled": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
@ -9555,7 +9561,8 @@
},
"inherits": {
"version": "2.0.3",
"bundled": true
"bundled": true,
"optional": true
},
"ini": {
"version": "1.3.5",
@ -9565,6 +9572,7 @@
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -9577,17 +9585,20 @@
"minimatch": {
"version": "3.0.4",
"bundled": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"bundled": true
"bundled": true,
"optional": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -9604,6 +9615,7 @@
"mkdirp": {
"version": "0.5.1",
"bundled": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -9676,7 +9688,8 @@
},
"number-is-nan": {
"version": "1.0.1",
"bundled": true
"bundled": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
@ -9686,6 +9699,7 @@
"once": {
"version": "1.4.0",
"bundled": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -9761,7 +9775,8 @@
},
"safe-buffer": {
"version": "5.1.2",
"bundled": true
"bundled": true,
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
@ -9791,6 +9806,7 @@
"string-width": {
"version": "1.0.2",
"bundled": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -9808,6 +9824,7 @@
"strip-ansi": {
"version": "3.0.1",
"bundled": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -9846,11 +9863,13 @@
},
"wrappy": {
"version": "1.0.2",
"bundled": true
"bundled": true,
"optional": true
},
"yallist": {
"version": "3.0.3",
"bundled": true
"bundled": true,
"optional": true
}
}
},

View file

@ -143,8 +143,8 @@ type AssignNavigation {
type Attribute implements Node {
id: ID!
productTypes(before: String, after: String, first: Int, last: Int): ProductTypeCountableConnection
productVariantTypes(before: String, after: String, first: Int, last: Int): ProductTypeCountableConnection
productTypes(before: String, after: String, first: Int, last: Int): ProductTypeCountableConnection!
productVariantTypes(before: String, after: String, first: Int, last: Int): ProductTypeCountableConnection!
privateMeta: [MetaStore]!
meta: [MetaStore]!
inputType: AttributeInputTypeEnum
@ -228,6 +228,7 @@ input AttributeFilterInput {
filterableInDashboard: Boolean
availableInGrid: Boolean
search: String
ids: [ID]
}
input AttributeInput {
@ -518,7 +519,8 @@ type Checkout implements Node {
shippingAddress: Address
shippingMethod: ShippingMethod
note: String!
discountAmount: Money
discountAmount: Money @deprecated(reason: "Use discount instead.")
discount: Money
discountName: String
translatedDiscountName: String
voucherCode: String
@ -969,7 +971,6 @@ enum CountryCode {
LT
LU
MO
MK
MG
MW
MY
@ -1003,6 +1004,7 @@ enum CountryCode {
NU
NF
KP
MK
MP
NO
OM
@ -1867,6 +1869,7 @@ type Mutations {
paymentCapture(amount: Decimal, paymentId: ID!): PaymentCapture
paymentRefund(amount: Decimal, paymentId: ID!): PaymentRefund
paymentVoid(paymentId: ID!): PaymentVoid
paymentSecureConfirm(paymentId: ID!): PaymentSecureConfirm
pageCreate(input: PageInput!): PageCreate
pageDelete(id: ID!): PageDelete
pageBulkDelete(ids: [ID]!): PageBulkDelete
@ -1909,7 +1912,7 @@ type Mutations {
giftCardCreate(input: GiftCardCreateInput!): GiftCardCreate
giftCardDeactivate(id: ID!): GiftCardDeactivate
giftCardUpdate(id: ID!, input: GiftCardUpdateInput!): GiftCardUpdate
pluginConfigurationUpdate(id: ID!, input: PluginConfigurationUpdateInput!): PluginConfigurationUpdate
pluginUpdate(id: ID!, input: PluginUpdateInput!): PluginUpdate
saleCreate(input: SaleInput!): SaleCreate
saleDelete(id: ID!): SaleDelete
saleBulkDelete(ids: [ID]!): SaleBulkDelete
@ -1946,8 +1949,9 @@ type Mutations {
checkoutClearMetadata(id: ID!, input: MetaPath!): CheckoutClearStoredMeta
checkoutUpdatePrivateMetadata(id: ID!, input: MetaInput!): CheckoutUpdatePrivateMeta
checkoutClearPrivateMetadata(id: ID!, input: MetaPath!): CheckoutClearStoredPrivateMeta
requestPasswordReset(email: String!): RequestPasswordReset
setPassword(id: ID!, input: SetPasswordInput!): SetPassword
requestPasswordReset(email: String!, redirectUrl: String!): RequestPasswordReset
setPassword(token: String!, email: String!, password: String!): SetPassword
passwordChange(newPassword: String!, oldPassword: String!): PasswordChange
userUpdateMetadata(id: ID!, input: MetaInput!): UserUpdateMeta
userClearStoredMetadata(id: ID!, input: MetaPath!): UserClearStoredMeta
accountAddressCreate(input: AddressInput!, type: AddressTypeEnum): AccountAddressCreate
@ -1956,7 +1960,7 @@ type Mutations {
accountSetDefaultAddress(id: ID!, type: AddressTypeEnum!): AccountSetDefaultAddress
accountRegister(input: AccountRegisterInput!): AccountRegister
accountUpdate(input: AccountInput!): AccountUpdate
accountRequestDeletion: AccountRequestDeletion
accountRequestDeletion(redirectUrl: String!): AccountRequestDeletion
accountDelete(token: String!): AccountDelete
customerPasswordReset(input: CustomerPasswordResetInput!): CustomerPasswordReset
customerAddressCreate(input: AddressInput!, type: AddressTypeEnum): CustomerAddressCreate
@ -2011,12 +2015,12 @@ type Order implements Node {
billingAddress: Address
shippingAddress: Address
shippingMethod: ShippingMethod
shippingPrice: TaxedMoney
shippingMethodName: String
shippingPrice: TaxedMoney
token: String!
voucher: Voucher
giftCards: [GiftCard]
discountAmount: Money
discount: Money
discountName: String!
translatedDiscountName: String!
displayGrossPrices: Boolean!
@ -2041,6 +2045,7 @@ type Order implements Node {
totalBalance: Money!
userEmail: String
isShippingRequired: Boolean!
discountAmount: Money! @deprecated(reason: "Deprecated: use discount instead.")
}
enum OrderAction {
@ -2341,6 +2346,11 @@ type PageUpdate {
page: Page
}
type PasswordChange {
errors: [Error!]
user: User
}
type PasswordReset {
errors: [Error!]
}
@ -2404,6 +2414,11 @@ type PaymentRefund {
payment: Payment
}
type PaymentSecureConfirm {
errors: [Error!]
payment: Payment
}
type PaymentSource {
gateway: String!
creditCardInfo: CreditCard
@ -2435,7 +2450,7 @@ enum PermissionEnum {
MANAGE_TRANSLATIONS
}
type PluginConfiguration implements Node {
type Plugin implements Node {
name: String!
description: String!
active: Boolean!
@ -2443,23 +2458,23 @@ type PluginConfiguration implements Node {
id: ID!
}
type PluginConfigurationCountableConnection {
type PluginCountableConnection {
pageInfo: PageInfo!
edges: [PluginConfigurationCountableEdge!]!
edges: [PluginCountableEdge!]!
totalCount: Int
}
type PluginConfigurationCountableEdge {
node: PluginConfiguration!
type PluginCountableEdge {
node: Plugin!
cursor: String!
}
type PluginConfigurationUpdate {
type PluginUpdate {
errors: [Error!]
pluginConfiguration: PluginConfiguration
plugin: Plugin
}
input PluginConfigurationUpdateInput {
input PluginUpdateInput {
active: Boolean
configuration: [ConfigurationItemInput]
}
@ -2493,6 +2508,7 @@ type Product implements Node {
isAvailable: Boolean
basePrice: Money
price: Money @deprecated(reason: "Has been replaced by 'basePrice'")
minimalVariantPrice: Money
taxRate: TaxRateType @deprecated(reason: "taxRate is deprecated. Use taxType to obtain taxCode for given tax gateway")
taxType: TaxType
attributes: [SelectedAttribute!]!
@ -2579,6 +2595,7 @@ input ProductFilterInput {
stockAvailability: StockAvailability
productType: ID
search: String
minimalPrice: PriceRangeInput
}
type ProductImage implements Node {
@ -2656,6 +2673,7 @@ input ProductOrder {
enum ProductOrderField {
NAME
PRICE
MINIMAL_PRICE
DATE
}
@ -2748,6 +2766,7 @@ enum ProductTypeEnum {
}
input ProductTypeFilterInput {
search: String
configurable: ProductTypeConfigurable
productType: ProductTypeEnum
}
@ -2925,7 +2944,7 @@ type Query {
product(id: ID!): Product
products(filter: ProductFilterInput, attributes: [AttributeScalar], categories: [ID], collections: [ID], priceLte: Float, priceGte: Float, sortBy: ProductOrder, stockAvailability: StockAvailability, query: String, before: String, after: String, first: Int, last: Int): ProductCountableConnection
productType(id: ID!): ProductType
productTypes(filter: ProductTypeFilterInput, before: String, after: String, first: Int, last: Int): ProductTypeCountableConnection
productTypes(filter: ProductTypeFilterInput, query: String, before: String, after: String, first: Int, last: Int): ProductTypeCountableConnection
productVariant(id: ID!): ProductVariant
productVariants(ids: [ID], before: String, after: String, first: Int, last: Int): ProductVariantCountableConnection
reportProductSales(period: ReportingPeriod!, before: String, after: String, first: Int, last: Int): ProductVariantCountableConnection
@ -2946,8 +2965,8 @@ type Query {
menuItems(query: String, before: String, after: String, first: Int, last: Int): MenuItemCountableConnection
giftCard(id: ID!): GiftCard
giftCards(before: String, after: String, first: Int, last: Int): GiftCardCountableConnection
pluginConfiguration(id: ID!): PluginConfiguration
pluginConfigurations(before: String, after: String, first: Int, last: Int): PluginConfigurationCountableConnection
plugin(id: ID!): Plugin
plugins(before: String, after: String, first: Int, last: Int): PluginCountableConnection
sale(id: ID!): Sale
sales(filter: SaleFilterInput, query: String, before: String, after: String, first: Int, last: Int): SaleCountableConnection
voucher(id: ID!): Voucher
@ -2957,7 +2976,7 @@ type Query {
checkouts(before: String, after: String, first: Int, last: Int): CheckoutCountableConnection
checkoutLine(id: ID): CheckoutLine
checkoutLines(before: String, after: String, first: Int, last: Int): CheckoutLineCountableConnection
addressValidationRules(countryCode: CountryCode, countryArea: String, cityArea: String): AddressValidationData
addressValidationRules(countryCode: CountryCode!, countryArea: String, city: String, cityArea: String): AddressValidationData
customers(filter: CustomerFilterInput, query: String, before: String, after: String, first: Int, last: Int): UserCountableConnection
me: User
staffUsers(filter: StaffUserInput, query: String, before: String, after: String, first: Int, last: Int): UserCountableConnection
@ -3089,15 +3108,11 @@ input SeoInput {
}
type SetPassword {
errors: [Error!]
token: String
errors: [Error]!
user: User
}
input SetPasswordInput {
token: String!
password: String!
}
type ShippingMethod implements Node {
id: ID!
name: String!
@ -3418,6 +3433,7 @@ enum TransactionKind {
REFUND
CAPTURE
VOID
CONFIRM
}
union TranslatableItem = Product | Category | Collection | Attribute | AttributeValue | ProductVariant | Page | ShippingMethod | Sale | Voucher | MenuItem
@ -3466,7 +3482,6 @@ type User implements Node {
firstName: String!
lastName: String!
isStaff: Boolean!
token: UUID!
isActive: Boolean!
note: String
dateJoined: DateTime!
@ -3592,13 +3607,14 @@ type Voucher implements Node {
applyOncePerCustomer: Boolean!
discountValueType: DiscountValueTypeEnum!
discountValue: Float!
minAmountSpent: Money
minSpent: Money
minCheckoutItemsQuantity: Int
categories(before: String, after: String, first: Int, last: Int): CategoryCountableConnection
collections(before: String, after: String, first: Int, last: Int): CollectionCountableConnection
products(before: String, after: String, first: Int, last: Int): ProductCountableConnection
countries: [CountryDisplay]
translation(languageCode: LanguageCodeEnum!): VoucherTranslation
minAmountSpent: Money @deprecated(reason: "Use the minSpent field instead.")
}
type VoucherAddCatalogues {

View file

@ -23,10 +23,10 @@ export interface TableHeadProps extends MuiTableHeadProps {
colSpan: number;
disabled: boolean;
dragRows?: boolean;
selected: number;
selected?: number;
items: Node[];
toolbar: React.ReactNode | React.ReactNodeArray;
toggleAll: (items: Node[], selected: number) => void;
toolbar?: React.ReactNode | React.ReactNodeArray;
toggleAll?: (items: Node[], selected: number) => void;
}
const styles = (theme: Theme) =>
@ -101,25 +101,26 @@ const TableHead = withStyles(styles, {
})}
/>
)}
{(items === undefined || items.length > 0) && (
<TableCell
padding="checkbox"
className={classNames({
[classes.checkboxSelected]: selected,
[classes.dragRows]: dragRows
})}
>
<Checkbox
{(items === undefined || items.length > 0) &&
(selected && (
<TableCell
padding="checkbox"
className={classNames({
[classes.checkboxPartialSelect]:
items && items.length > selected && selected > 0
[classes.checkboxSelected]: selected,
[classes.dragRows]: dragRows
})}
checked={selected === 0 ? false : true}
disabled={disabled}
onChange={() => toggleAll(items, selected)}
/>
</TableCell>
)}
>
<Checkbox
className={classNames({
[classes.checkboxPartialSelect]:
items && items.length > selected && selected > 0
})}
checked={selected === 0 ? false : true}
disabled={disabled}
onChange={() => toggleAll(items, selected)}
/>
</TableCell>
))}
{selected ? (
<>
<TableCell
@ -138,7 +139,7 @@ const TableHead = withStyles(styles, {
</Typography>
)}
<div className={classes.spacer} />
<div className={classes.toolbar}>{toolbar}</div>
{toolbar && <div className={classes.toolbar}>{toolbar}</div>}
</div>
</TableCell>
</>

View file

@ -21,6 +21,7 @@ export interface AppListViewSettings {
[ListViews.NAVIGATION_LIST]: ListSettings;
[ListViews.ORDER_LIST]: ListSettings;
[ListViews.PAGES_LIST]: ListSettings;
[ListViews.PLUGINS_LIST]: ListSettings;
[ListViews.PRODUCT_LIST]: ListSettings<ProductListColumns>;
[ListViews.SALES_LIST]: ListSettings;
[ListViews.SHIPPING_METHODS_LIST]: ListSettings;
@ -49,6 +50,9 @@ export const defaultListSettings: AppListViewSettings = {
[ListViews.PAGES_LIST]: {
rowNumber: PAGINATE_BY
},
[ListViews.PLUGINS_LIST]: {
rowNumber: PAGINATE_BY
},
[ListViews.PRODUCT_LIST]: {
columns: ["isPublished", "price", "productType"],
rowNumber: PAGINATE_BY

View file

@ -7,6 +7,7 @@ import useNavigator from "@saleor/hooks/useNavigator";
import useUser from "@saleor/hooks/useUser";
import Navigation from "@saleor/icons/Navigation";
import Pages from "@saleor/icons/Pages";
import Plugins from "@saleor/icons/Plugins";
import ProductTypes from "@saleor/icons/ProductTypes";
import ShippingMethods from "@saleor/icons/ShippingMethods";
import SiteSettings from "@saleor/icons/SiteSettings";
@ -16,6 +17,7 @@ import { sectionNames } from "@saleor/intl";
import { maybe } from "@saleor/misc";
import { menuListUrl } from "@saleor/navigation/urls";
import { pageListUrl } from "@saleor/pages/urls";
import { pluginsListUrl } from "@saleor/plugins/urls";
import { productTypeListUrl } from "@saleor/productTypes/urls";
import { shippingZonesListUrl } from "@saleor/shipping/urls";
import { siteSettingsUrl } from "@saleor/siteSettings/urls";
@ -105,6 +107,22 @@ export function createConfigurationMenu(intl: IntlShape): MenuItem[] {
permission: PermissionEnum.MANAGE_PAGES,
title: intl.formatMessage(sectionNames.pages),
url: pageListUrl()
},
{
description: intl.formatMessage({
defaultMessage: "View and update your plugins and their settings.",
id: "configurationPluginsPages"
}),
icon: (
<Plugins
fontSize="inherit"
viewBox="-8 -5 44 44"
preserveAspectRatio="xMinYMin meet"
/>
),
permission: PermissionEnum.MANAGE_SETTINGS,
title: intl.formatMessage(sectionNames.plugins),
url: pluginsListUrl()
}
];
}

17
src/icons/Plugins.tsx Normal file
View file

@ -0,0 +1,17 @@
import createSvgIcon from "@material-ui/icons/utils/createSvgIcon";
import React from "react";
export const Plugins = createSvgIcon(
<>
<g>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M7.12891 6.10352e-05H12.9999V7.80006H18.7418V6.10352e-05H24.6128V7.80006H31.9999V14.6751H29.0967V29.7876H20.7418V33.6876H16.8708V40.0001H14.8708V33.6876H10.9999V29.7876H2.9031V14.6751H-0.00012207V7.80006H7.12891V6.10352e-05ZM9.12891 7.80006H10.9999V2.00006H9.12891V7.80006ZM4.9031 14.6751V27.7876H27.0967V14.6751H4.9031ZM12.9999 29.7876V31.6876H18.7418V29.7876H12.9999ZM22.6128 7.80006V2.00006H20.7418V7.80006H22.6128ZM1.99988 9.80006V12.6751H29.9999V9.80006H1.99988Z"
fill="#06847B"
/>
</g>
</>
);
Plugins.displayName = "Plugins";
export default Plugins;

View file

@ -39,6 +39,7 @@ import { navigationSection } from "./navigation/urls";
import { NotFound } from "./NotFound";
import OrdersSection from "./orders";
import PageSection from "./pages";
import PluginsSection from "./plugins";
import ProductSection from "./products";
import ProductTypesSection from "./productTypes";
import ShippingSection from "./shipping";
@ -171,6 +172,11 @@ const Routes: React.FC = () => {
path="/pages"
component={PageSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_PLUGINS]}
path="/plugins"
component={PluginsSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_ORDERS]}
path="/orders"

View file

@ -152,6 +152,10 @@ export const sectionNames = defineMessages({
defaultMessage: "Pages",
description: "pages section name"
},
plugins: {
defaultMessage: "Plugins",
description: "plugins section name"
},
productTypes: {
defaultMessage: "Product Types",
description: "product types section name"

View file

@ -0,0 +1,88 @@
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import Typography from "@material-ui/core/Typography";
import makeStyles from "@material-ui/styles/makeStyles";
import React from "react";
import { useIntl } from "react-intl";
import CardTitle from "@saleor/components/CardTitle";
import ControlledSwitch from "@saleor/components/ControlledSwitch";
import FormSpacer from "@saleor/components/FormSpacer";
import Hr from "@saleor/components/Hr";
import { FormData } from "../PluginsDetailsPage";
interface PluginInfoProps {
data: FormData;
description: string;
name: string;
onChange: (event: React.ChangeEvent<any>) => void;
}
const useStyles = makeStyles(() => ({
status: {
paddingTop: 20
},
title: {
fontSize: 14,
paddingTop: 10
}
}));
const PluginInfo: React.StatelessComponent<PluginInfoProps> = ({
data,
description,
name,
onChange
}) => {
const classes = useStyles({});
const intl = useIntl();
return (
<Card>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Plugin Information and Status",
description: "plugin title"
})}
/>
<CardContent>
<Typography className={classes.title} variant="h6">
{intl.formatMessage({
defaultMessage: "Plugin Name",
description: "plugin name"
})}
</Typography>
<Typography>{name}</Typography>
{description && (
<>
<Typography className={classes.title} variant="h6">
{intl.formatMessage({
defaultMessage: "Plugin Description",
description: "plugin description"
})}
</Typography>
<Typography>{description}</Typography>
</>
)}
<FormSpacer />
<Hr />
<Typography className={classes.status}>
{intl.formatMessage({
defaultMessage: "Status",
description: "plugin status"
})}
</Typography>
<ControlledSwitch
checked={data.active}
label={intl.formatMessage({
defaultMessage: "Set plugin as Active",
description: "plugin active label"
})}
name={"active" as keyof FormData}
onChange={onChange}
/>
</CardContent>
</Card>
);
};
PluginInfo.displayName = "PluginInfo";
export default PluginInfo;

View file

@ -0,0 +1,2 @@
export { default } from "./PluginInfo";
export * from "./PluginInfo";

View file

@ -0,0 +1,87 @@
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import TextField from "@material-ui/core/TextField";
import makeStyles from "@material-ui/styles/makeStyles";
import CardTitle from "@saleor/components/CardTitle";
import ControlledSwitch from "@saleor/components/ControlledSwitch";
import { FormErrors } from "@saleor/types";
import { ConfigurationTypeFieldEnum } from "@saleor/types/globalTypes";
import React from "react";
import { useIntl } from "react-intl";
import { FormData } from "../PluginsDetailsPage";
interface PluginSettingsProps {
data: FormData;
errors: FormErrors<"name" | "configuration">;
disabled: boolean;
onChange: (event: React.ChangeEvent<any>) => void;
fields: Array<{
name: string;
type: ConfigurationTypeFieldEnum | null;
value: string;
helpText: string | null;
label: string | null;
}>;
}
const useStyles = makeStyles(() => ({
item: {
paddingBottom: 10,
paddingTop: 10
}
}));
const PluginSettings: React.StatelessComponent<PluginSettingsProps> = ({
data,
disabled,
errors,
onChange,
fields
}) => {
const classes = useStyles({});
const intl = useIntl();
return (
<Card>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Plugin Settings",
description: "plugin section title"
})}
/>
<CardContent>
{data.configuration.map((configuration, index) => (
<div className={classes.item} key={index}>
{fields[index].type === ConfigurationTypeFieldEnum.STRING && (
<TextField
disabled={disabled}
error={!!errors.name}
helperText={fields[index].helpText}
label={fields[index].label}
name={configuration.name}
fullWidth
value={configuration.value}
onChange={onChange}
/>
)}
{fields[index].type === ConfigurationTypeFieldEnum.BOOLEAN && (
<ControlledSwitch
checked={
typeof configuration.value !== "boolean"
? configuration.value === "true"
: configuration.value
}
disabled={disabled}
label={fields[index].label}
name={configuration.name}
onChange={onChange}
/>
)}
</div>
))}
</CardContent>
</Card>
);
};
PluginSettings.displayName = "PluginSettings";
export default PluginSettings;

View file

@ -0,0 +1,2 @@
export { default } from "./PluginSettings";
export * from "./PluginSettings";

View file

@ -0,0 +1,143 @@
import Typography from "@material-ui/core/Typography";
import AppHeader from "@saleor/components/AppHeader";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import Container from "@saleor/components/Container";
import Form from "@saleor/components/Form";
import Grid from "@saleor/components/Grid";
import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar";
import { sectionNames } from "@saleor/intl";
import { maybe } from "@saleor/misc";
import { UserError } from "@saleor/types";
import { ConfigurationItemInput } from "@saleor/types/globalTypes";
import React from "react";
import { useIntl } from "react-intl";
import { Plugin_plugin } from "../../types/Plugin";
import PluginInfo from "../PluginInfo";
import PluginSettings from "../PluginSettings";
export interface FormData {
active: boolean;
configuration: ConfigurationItemInput[];
}
export interface PluginsDetailsPageProps {
disabled: boolean;
errors: UserError[];
plugin: Plugin_plugin;
saveButtonBarState: ConfirmButtonTransitionState;
onBack: () => void;
onSubmit: (data: FormData) => void;
}
const PluginsDetailsPage: React.StatelessComponent<PluginsDetailsPageProps> = ({
disabled,
errors,
plugin,
saveButtonBarState,
onBack,
onSubmit
}) => {
const intl = useIntl();
const initialForm: FormData = {
active: maybe(() => plugin.active, false),
configuration: maybe(() => plugin.configuration, [])
};
return (
<Form errors={errors} initial={initialForm} onSubmit={onSubmit}>
{({ data, errors, hasChanged, submit, set, triggerChange }) => {
const onChange = event => {
const newData = {
active: data.active,
configuration: data.configuration
};
const { name, value } = event.target;
name === "active"
? (newData.active = value)
: (newData.active = data.active);
if (newData.configuration) {
newData.configuration.map(item => {
if (item.name === name) {
item.value = value;
}
});
}
triggerChange();
set(newData);
};
return (
<Container>
<AppHeader onBack={onBack}>
{intl.formatMessage(sectionNames.plugins)}
</AppHeader>
<PageHeader
title={`${maybe(() => plugin.name, "")} ${intl.formatMessage({
defaultMessage: "Details",
description: "plugin page title"
})}`}
/>
<Grid variant="inverted">
<div>
<Typography variant="h6">
{intl.formatMessage({
defaultMessage: "Plugin Information and Status",
description: "plugin section title"
})}
</Typography>
<Typography>
{intl.formatMessage({
defaultMessage:
"These are general information about your store. They define what is the URL of your store and what is shown in brow sers taskbar.",
description: "plugin section description"
})}
</Typography>
</div>
<PluginInfo
data={data}
description={maybe(() => plugin.description, "")}
name={maybe(() => plugin.name, "")}
onChange={onChange}
/>
{data.configuration && (
<>
<div>
<Typography variant="h6">
{intl.formatMessage({
defaultMessage: "Plugin Settings",
description: "plugin section title"
})}
</Typography>
<Typography>
{intl.formatMessage({
defaultMessage:
"This adress will be used to generate invoices and calculate shipping rates. Email adress you provide here will be used as a contact adress for your customers.",
description: "plugin section description"
})}
</Typography>
</div>
<PluginSettings
data={data}
fields={maybe(() => plugin.configuration, [])}
errors={errors}
disabled={disabled}
onChange={onChange}
/>
</>
)}
</Grid>
<SaveButtonBar
disabled={disabled || !hasChanged}
state={saveButtonBarState}
onCancel={onBack}
onSave={submit}
/>
</Container>
);
}}
</Form>
);
};
PluginsDetailsPage.displayName = "PluginsDetailsPage";
export default PluginsDetailsPage;

View file

@ -0,0 +1,2 @@
export { default } from "./PluginsDetailsPage";
export * from "./PluginsDetailsPage";

View file

@ -0,0 +1,160 @@
import Card from "@material-ui/core/Card";
import {
createStyles,
Theme,
withStyles,
WithStyles
} from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableFooter from "@material-ui/core/TableFooter";
import TableRow from "@material-ui/core/TableRow";
import EditIcon from "@material-ui/icons/Edit";
import React from "react";
import { useIntl } from "react-intl";
import Skeleton from "@saleor/components/Skeleton";
import StatusLabel from "@saleor/components/StatusLabel";
import TableHead from "@saleor/components/TableHead";
import TablePagination from "@saleor/components/TablePagination";
import { translateBoolean } from "@saleor/intl";
import { maybe, renderCollection } from "@saleor/misc";
import { ListProps } from "@saleor/types";
import { Plugins_plugins_edges_node } from "../../types/Plugins";
export interface PluginListProps extends ListProps {
plugins: Plugins_plugins_edges_node[];
}
const styles = (theme: Theme) =>
createStyles({
[theme.breakpoints.up("lg")]: {
colAction: {
"& svg": {
color: theme.palette.primary.main
},
textAlign: "right"
},
colActive: {},
colName: {}
},
colAction: {},
colActive: {},
colName: {},
link: {
cursor: "pointer"
}
});
const numberOfColumns = 4;
const PluginList = withStyles(styles, { name: "PluginList" })(
({
classes,
settings,
plugins,
disabled,
onNextPage,
pageInfo,
onRowClick,
onUpdateListSettings,
onPreviousPage
}: PluginListProps & WithStyles<typeof styles>) => {
const intl = useIntl();
return (
<Card>
<Table>
<TableHead
colSpan={numberOfColumns}
disabled={disabled}
items={plugins}
>
<TableCell className={classes.colName} padding="dense">
{intl.formatMessage({
defaultMessage: "Name",
description: "plugin list table header"
})}
</TableCell>
<TableCell className={classes.colActive} padding="dense">
{intl.formatMessage({
defaultMessage: "Active",
description: "plugin list table header"
})}
</TableCell>
<TableCell className={classes.colAction} padding="dense">
{intl.formatMessage({
defaultMessage: "Action",
description: "plugin list table header"
})}
</TableCell>
</TableHead>
<TableFooter>
<TableRow>
<TablePagination
colSpan={numberOfColumns}
settings={settings}
hasNextPage={
pageInfo && !disabled ? pageInfo.hasNextPage : false
}
onNextPage={onNextPage}
onUpdateListSettings={onUpdateListSettings}
hasPreviousPage={
pageInfo && !disabled ? pageInfo.hasPreviousPage : false
}
onPreviousPage={onPreviousPage}
/>
</TableRow>
</TableFooter>
<TableBody>
{renderCollection(
plugins,
plugin => {
return (
<TableRow
hover={!!plugin}
className={!!plugin ? classes.link : undefined}
onClick={plugin ? onRowClick(plugin.id) : undefined}
key={plugin ? plugin.id : "skeleton"}
>
<TableCell className={classes.colName}>
{maybe<React.ReactNode>(() => plugin.name, <Skeleton />)}
</TableCell>
<TableCell className={classes.colActive}>
{maybe<React.ReactNode>(
() => (
<StatusLabel
label={translateBoolean(plugin.active, intl)}
status={plugin.active ? "success" : "error"}
/>
),
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colAction}>
<div onClick={plugin ? onRowClick(plugin.id) : undefined}>
<EditIcon />
</div>
</TableCell>
</TableRow>
);
},
() => (
<TableRow>
<TableCell colSpan={numberOfColumns}>
{intl.formatMessage({
defaultMessage: "No plugins found",
description: "plugin no found"
})}
</TableCell>
</TableRow>
)
)}
</TableBody>
</Table>
</Card>
);
}
);
PluginList.displayName = "PluginList";
export default PluginList;

View file

@ -0,0 +1,2 @@
export { default } from "./PluginsList";
export * from "./PluginsList";

View file

@ -0,0 +1,52 @@
import React from "react";
import { useIntl } from "react-intl";
import AppHeader from "@saleor/components/AppHeader";
import Container from "@saleor/components/Container";
import PageHeader from "@saleor/components/PageHeader";
import { sectionNames } from "@saleor/intl";
import { PageListProps } from "@saleor/types";
import { Plugins_plugins_edges_node } from "../../types/Plugins";
import PluginsList from "../PluginsList/PluginsList";
export interface PluginsListPageProps extends PageListProps {
plugins: Plugins_plugins_edges_node[];
onBack: () => void;
}
const PluginsListPage: React.StatelessComponent<PluginsListPageProps> = ({
disabled,
settings,
onBack,
onNextPage,
onPreviousPage,
onRowClick,
onUpdateListSettings,
pageInfo,
plugins
}) => {
const intl = useIntl();
return (
<Container>
<AppHeader onBack={onBack}>
{intl.formatMessage({
defaultMessage: "Configuration",
description: "plugin back button"
})}
</AppHeader>
<PageHeader title={intl.formatMessage(sectionNames.plugins)} />
<PluginsList
disabled={disabled}
settings={settings}
plugins={plugins}
onNextPage={onNextPage}
onPreviousPage={onPreviousPage}
onUpdateListSettings={onUpdateListSettings}
onRowClick={onRowClick}
pageInfo={pageInfo}
/>
</Container>
);
};
PluginsListPage.displayName = "PluginsListPage";
export default PluginsListPage;

View file

@ -0,0 +1,2 @@
export { default } from "./PluginsListPage";
export * from "./PluginsListPage";

57
src/plugins/fixtures.ts Normal file
View file

@ -0,0 +1,57 @@
import { ConfigurationTypeFieldEnum } from "@saleor/types/globalTypes";
import { Plugin_plugin } from "./types/Plugin";
import { Plugins_plugins_edges_node } from "./types/Plugins";
export const pluginList: Plugins_plugins_edges_node[] = [
{
__typename: "Plugin",
active: true,
description:
"Lorem ipsum dolor sit amet enim. Etiam ullamcorper. Suspendisse a pellentesque dui, non felis. Maecenas malesuada elit lectus felis, malesuada ultricies. Curabitur et ligula. Ut molestie a, ultricies porta urna. Vestibulum commodo volutpat a, convallis ac, laoreet enim. Phasellus fermentum in, dolor. Pellentesque facilisis. Nulla imperdiet sit amet magna.",
id: "Jzx123sEt==",
name: "Avalara"
},
{
__typename: "Plugin",
active: false,
description:
"Lorem ipsum dolor sit amet enim. Etiam ullamcorper. Suspendisse a pellentesque dui, non felis. Maecenas malesuada elit lectus felis, malesuada ultricies. Curabitur et ligula. Ut molestie a, ultricies porta urna. Vestibulum commodo volutpat a, convallis ac, laoreet enim. Phasellus fermentum in, dolor. Pellentesque facilisis. Nulla imperdiet sit amet magna.",
id: "Jzx123sEt==",
name: "VatLayer"
}
];
export const plugin: Plugin_plugin = {
__typename: "Plugin",
active: true,
configuration: [
{
__typename: "ConfigurationItem",
helpText: "Provide user or account details",
label: "Username or account",
name: "Username or account",
type: ConfigurationTypeFieldEnum.STRING,
value: ""
},
{
__typename: "ConfigurationItem",
helpText: "Provide password or license details",
label: "Password or license",
name: "Password or license",
type: ConfigurationTypeFieldEnum.STRING,
value: ""
},
{
__typename: "ConfigurationItem",
helpText: "Determines if Saleor should use Avatax sandbox API.",
label: "Use sandbox",
name: "Use sandbox",
type: ConfigurationTypeFieldEnum.BOOLEAN,
value: "true"
}
],
description:
"Lorem ipsum dolor sit amet enim. Etiam ullamcorper. Suspendisse a pellentesque dui, non felis. Maecenas malesuada elit lectus felis, malesuada ultricies. Curabitur et ligula. Ut molestie a, ultricies porta urna. Vestibulum commodo volutpat a, convallis ac, laoreet enim. Phasellus fermentum in, dolor. Pellentesque facilisis. Nulla imperdiet sit amet magna.",
id: "UGx1Z2luQ29uZmlndXJhdGlvbjoy",
name: "Username or account"
};

51
src/plugins/index.tsx Normal file
View file

@ -0,0 +1,51 @@
import { parse as parseQs } from "qs";
import React from "react";
import { useIntl } from "react-intl";
import { Route, RouteComponentProps, Switch } from "react-router-dom";
import { sectionNames } from "@saleor/intl";
import { WindowTitle } from "../components/WindowTitle";
import {
pluginsListPath,
PluginsListUrlQueryParams,
pluginsPath
} from "./urls";
import PluginsDetailsComponent from "./views/PluginsDetails";
import PluginsListComponent from "./views/PluginsList";
const PluginList: React.StatelessComponent<RouteComponentProps<any>> = ({
location
}) => {
const qs = parseQs(location.search.substr(1));
const params: PluginsListUrlQueryParams = qs;
return <PluginsListComponent params={params} />;
};
const PageDetails: React.StatelessComponent<RouteComponentProps<any>> = ({
match
}) => {
const qs = parseQs(location.search.substr(1));
const params: PluginsListUrlQueryParams = qs;
return (
<PluginsDetailsComponent
id={decodeURIComponent(match.params.id)}
params={params}
/>
);
};
const Component = () => {
const intl = useIntl();
return (
<>
<WindowTitle title={intl.formatMessage(sectionNames.plugins)} />
<Switch>
<Route exact path={pluginsListPath} component={PluginList} />
<Route path={pluginsPath(":id")} component={PageDetails} />
</Switch>
</>
);
};
export default Component;

24
src/plugins/mutations.ts Normal file
View file

@ -0,0 +1,24 @@
import gql from "graphql-tag";
import { TypedMutation } from "../mutations";
import { pluginsDetailsFragment } from "./queries";
import { PluginUpdate, PluginUpdateVariables } from "./types/PluginUpdate";
const pluginUpdate = gql`
${pluginsDetailsFragment}
mutation PluginUpdate($id: ID!, $input: PluginUpdateInput!) {
pluginUpdate(id: $id, input: $input) {
errors {
field
message
}
plugin {
...PluginsDetailsFragment
}
}
}
`;
export const TypedPluginUpdate = TypedMutation<
PluginUpdate,
PluginUpdateVariables
>(pluginUpdate);

62
src/plugins/queries.ts Normal file
View file

@ -0,0 +1,62 @@
import gql from "graphql-tag";
import { TypedQuery } from "../queries";
import { Plugin, PluginVariables } from "./types/Plugin";
import { Plugins, PluginsVariables } from "./types/Plugins";
export const pluginsFragment = gql`
fragment PluginFragment on Plugin {
id
name
description
active
}
`;
export const pluginsDetailsFragment = gql`
${pluginsFragment}
fragment PluginsDetailsFragment on Plugin {
...PluginFragment
configuration {
name
type
value
helpText
label
}
}
`;
const pluginsList = gql`
${pluginsFragment}
query Plugins($first: Int, $after: String, $last: Int, $before: String) {
plugins(before: $before, after: $after, first: $first, last: $last) {
edges {
node {
...PluginFragment
}
}
pageInfo {
hasPreviousPage
hasNextPage
startCursor
endCursor
}
}
}
`;
export const TypedPluginsListQuery = TypedQuery<Plugins, PluginsVariables>(
pluginsList
);
const pluginsDetails = gql`
${pluginsDetailsFragment}
query Plugin($id: ID!) {
plugin(id: $id) {
...PluginsDetailsFragment
}
}
`;
export const TypedPluginsDetailsQuery = TypedQuery<Plugin, PluginVariables>(
pluginsDetails
);

View file

@ -0,0 +1,35 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { ConfigurationTypeFieldEnum } from "./../../types/globalTypes";
// ====================================================
// GraphQL query operation: Plugin
// ====================================================
export interface Plugin_plugin_configuration {
__typename: "ConfigurationItem";
name: string;
type: ConfigurationTypeFieldEnum | null;
value: string;
helpText: string | null;
label: string | null;
}
export interface Plugin_plugin {
__typename: "Plugin";
id: string;
name: string;
description: string;
active: boolean;
configuration: (Plugin_plugin_configuration | null)[] | null;
}
export interface Plugin {
plugin: Plugin_plugin | null;
}
export interface PluginVariables {
id: string;
}

View file

@ -0,0 +1,48 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { PluginUpdateInput, ConfigurationTypeFieldEnum } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: PluginUpdate
// ====================================================
export interface PluginUpdate_pluginUpdate_errors {
__typename: "Error";
field: string | null;
message: string | null;
}
export interface PluginUpdate_pluginUpdate_plugin_configuration {
__typename: "ConfigurationItem";
name: string;
type: ConfigurationTypeFieldEnum | null;
value: string;
helpText: string | null;
label: string | null;
}
export interface PluginUpdate_pluginUpdate_plugin {
__typename: "Plugin";
id: string;
name: string;
description: string;
active: boolean;
configuration: (PluginUpdate_pluginUpdate_plugin_configuration | null)[] | null;
}
export interface PluginUpdate_pluginUpdate {
__typename: "PluginUpdate";
errors: PluginUpdate_pluginUpdate_errors[] | null;
plugin: PluginUpdate_pluginUpdate_plugin | null;
}
export interface PluginUpdate {
pluginUpdate: PluginUpdate_pluginUpdate | null;
}
export interface PluginUpdateVariables {
id: string;
input: PluginUpdateInput;
}

View file

@ -0,0 +1,45 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL query operation: Plugins
// ====================================================
export interface Plugins_plugins_edges_node {
__typename: "Plugin";
id: string;
name: string;
description: string;
active: boolean;
}
export interface Plugins_plugins_edges {
__typename: "PluginCountableEdge";
node: Plugins_plugins_edges_node;
}
export interface Plugins_plugins_pageInfo {
__typename: "PageInfo";
hasPreviousPage: boolean;
hasNextPage: boolean;
startCursor: string | null;
endCursor: string | null;
}
export interface Plugins_plugins {
__typename: "PluginCountableConnection";
edges: Plugins_plugins_edges[];
pageInfo: Plugins_plugins_pageInfo;
}
export interface Plugins {
plugins: Plugins_plugins | null;
}
export interface PluginsVariables {
first?: number | null;
after?: string | null;
last?: number | null;
before?: string | null;
}

View file

@ -0,0 +1,15 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL fragment: pluginFragment
// ====================================================
export interface pluginFragment {
__typename: "Plugin";
id: string;
name: string;
description: string;
active: boolean;
}

View file

@ -0,0 +1,27 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { ConfigurationTypeFieldEnum } from "./../../types/globalTypes";
// ====================================================
// GraphQL fragment: pluginsDetailsFragment
// ====================================================
export interface pluginsDetailsFragment_configuration {
__typename: "ConfigurationItem";
name: string;
type: ConfigurationTypeFieldEnum | null;
value: string;
helpText: string | null;
label: string | null;
}
export interface pluginsDetailsFragment {
__typename: "Plugin";
id: string;
name: string;
description: string;
active: boolean;
configuration: (pluginsDetailsFragment_configuration | null)[] | null;
}

16
src/plugins/urls.ts Normal file
View file

@ -0,0 +1,16 @@
import { stringify as stringifyQs } from "qs";
import urlJoin from "url-join";
import { Pagination, SingleAction } from "../types";
export const pluginsSection = "/plugins/";
export const pluginsListPath = pluginsSection;
export type PluginsListUrlQueryParams = Pagination & SingleAction;
export const pluginsListUrl = (params?: PluginsListUrlQueryParams) =>
pluginsListPath + "?" + stringifyQs(params);
export const pluginsPath = (id: string) => urlJoin(pluginsSection, id);
export type PluginsUrlQueryParams = SingleAction;
export const pluginsUrl = (id: string, params?: PluginsUrlQueryParams) =>
pluginsPath(encodeURIComponent(id)) + "?" + stringifyQs(params);

View file

@ -0,0 +1,100 @@
import { WindowTitle } from "@saleor/components/WindowTitle";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import React from "react";
import { useIntl } from "react-intl";
import { getMutationState, maybe } from "../../misc";
import PluginsDetailsPage from "../components/PluginsDetailsPage";
import { TypedPluginUpdate } from "../mutations";
import { TypedPluginsDetailsQuery } from "../queries";
import { pluginsListUrl, PluginsListUrlQueryParams } from "../urls";
export interface PluginsDetailsProps {
id: string;
params: PluginsListUrlQueryParams;
}
export const PluginsDetails: React.StatelessComponent<PluginsDetailsProps> = ({
id
}) => {
const navigate = useNavigator();
const notify = useNotifier();
const intl = useIntl();
return (
<TypedPluginUpdate>
{(pluginUpdate, pluginUpdateOpts) => (
<TypedPluginsDetailsQuery variables={{ id }}>
{PluginDetails => {
const formTransitionState = getMutationState(
pluginUpdateOpts.called,
pluginUpdateOpts.loading,
maybe(() => pluginUpdateOpts.data.pluginUpdate.errors)
);
const formErrors = maybe(
() => pluginUpdateOpts.data.pluginUpdate.errors,
[]
);
if (formErrors.length) {
formErrors.map(error => {
notify({
text: error.message
});
});
} else {
if (pluginUpdateOpts.data) {
notify({
text: intl.formatMessage({
defaultMessage: "Succesfully updated plugin settings",
description: "plugin success message"
})
});
}
}
return (
<>
<WindowTitle
title={maybe(() => PluginDetails.data.plugin.name)}
/>
<PluginsDetailsPage
disabled={PluginDetails.loading}
errors={formErrors}
saveButtonBarState={formTransitionState}
plugin={maybe(() => PluginDetails.data.plugin)}
onBack={() => navigate(pluginsListUrl())}
onSubmit={formData => {
const configurationInput =
formData.configuration &&
formData.configuration.map(item => {
return {
name: item.name,
value: item.value.toString()
};
});
pluginUpdate({
variables: {
id,
input: {
active: formData.active,
configuration: configurationInput
? configurationInput
: null
}
}
});
}}
/>
</>
);
}}
</TypedPluginsDetailsQuery>
)}
</TypedPluginUpdate>
);
};
PluginsDetails.displayName = "PluginsDetails";
export default PluginsDetails;

View file

@ -0,0 +1,58 @@
import { configurationMenuUrl } from "@saleor/configuration";
import useListSettings from "@saleor/hooks/useListSettings";
import useNavigator from "@saleor/hooks/useNavigator";
import usePaginator, {
createPaginationState
} from "@saleor/hooks/usePaginator";
import { maybe } from "@saleor/misc";
import { ListViews } from "@saleor/types";
import React from "react";
import PluginsListPage from "../components/PluginsListPage/PluginsListPage";
import { TypedPluginsListQuery } from "../queries";
import { PluginsListUrlQueryParams, pluginsUrl } from "../urls";
interface PluginsListProps {
params: PluginsListUrlQueryParams;
}
export const PluginsList: React.StatelessComponent<PluginsListProps> = ({
params
}) => {
const navigate = useNavigator();
const paginate = usePaginator();
const { updateListSettings, settings } = useListSettings(
ListViews.PLUGINS_LIST
);
const paginationState = createPaginationState(settings.rowNumber, params);
return (
<TypedPluginsListQuery displayLoader variables={paginationState}>
{({ data, loading }) => {
const { loadNextPage, loadPreviousPage, pageInfo } = paginate(
maybe(() => data.plugins.pageInfo),
paginationState,
params
);
return (
<>
<PluginsListPage
disabled={loading}
settings={settings}
plugins={maybe(() => data.plugins.edges.map(edge => edge.node))}
pageInfo={pageInfo}
onAdd={() => navigate(configurationMenuUrl)}
onBack={() => navigate(configurationMenuUrl)}
onNextPage={loadNextPage}
onPreviousPage={loadPreviousPage}
onUpdateListSettings={updateListSettings}
onRowClick={id => () => navigate(pluginsUrl(id))}
/>
</>
);
}}
</TypedPluginsListQuery>
);
};
export default PluginsList;

File diff suppressed because it is too large Load diff

View file

@ -101,6 +101,10 @@ function loadStories() {
require("./stories/pages/PageDetailsPage");
require("./stories/pages/PageListPage");
// Plugins
require("./stories/plugins/PluginDetailsPage");
require("./stories/plugins/PluginsListPage");
// Products
require("./stories/products/ProductCreatePage");
require("./stories/products/ProductImagePage");

View file

@ -0,0 +1,36 @@
import { storiesOf } from "@storybook/react";
import React from "react";
import PluginsDetailsPage, {
FormData,
PluginsDetailsPageProps
} from "../../../plugins/components/PluginsDetailsPage";
import { plugin } from "../../../plugins/fixtures";
import Decorator from "../../Decorator";
import { formError } from "../../misc";
const props: PluginsDetailsPageProps = {
disabled: false,
errors: [],
onBack: () => undefined,
onSubmit: () => undefined,
plugin,
saveButtonBarState: "default"
};
storiesOf("Views / Plugins / Plugin details", module)
.addDecorator(Decorator)
.add("default", () => <PluginsDetailsPage {...props} />)
.add("loading", () => (
<PluginsDetailsPage {...props} disabled={true} plugin={undefined} />
))
.add("form errors", () => (
<PluginsDetailsPage
{...props}
errors={([
"active",
"Username or account",
"Password or license"
] as Array<keyof FormData>).map(formError)}
/>
));

View file

@ -0,0 +1,23 @@
import { storiesOf } from "@storybook/react";
import React from "react";
import { pageListProps } from "../../../fixtures";
import PluginsListPage, {
PluginsListPageProps
} from "../../../plugins/components/PluginsListPage";
import { pluginList } from "../../../plugins/fixtures";
import Decorator from "../../Decorator";
const props: PluginsListPageProps = {
...pageListProps.default,
onBack: () => undefined,
plugins: pluginList
};
storiesOf("Views / Plugins / Plugin list", module)
.addDecorator(Decorator)
.add("default", () => <PluginsListPage {...props} />)
.add("loading", () => (
<PluginsListPage {...props} disabled={true} plugins={undefined} />
))
.add("no data", () => <PluginsListPage {...props} plugins={[]} />);

View file

@ -23,6 +23,7 @@ export enum ListViews {
NAVIGATION_LIST = "NAVIGATION_LIST",
ORDER_LIST = "ORDER_LIST",
PAGES_LIST = "PAGES_LIST",
PLUGINS_LIST = "PLUGIN_LIST",
PRODUCT_LIST = "PRODUCT_LIST",
SALES_LIST = "SALES_LIST",
SHIPPING_METHODS_LIST = "SHIPPING_METHODS_LIST",

View file

@ -33,6 +33,11 @@ export enum AuthorizationKeyType {
GOOGLE_OAUTH2 = "GOOGLE_OAUTH2",
}
export enum ConfigurationTypeFieldEnum {
BOOLEAN = "BOOLEAN",
STRING = "STRING",
}
export enum DiscountValueTypeEnum {
FIXED = "FIXED",
PERCENTAGE = "PERCENTAGE",
@ -340,6 +345,11 @@ export interface CollectionInput {
publicationDate?: any | null;
}
export interface ConfigurationItemInput {
name: string;
value: string;
}
export interface CustomerInput {
defaultBillingAddress?: AddressInput | null;
defaultShippingAddress?: AddressInput | null;
@ -466,6 +476,11 @@ export interface PageTranslationInput {
contentJson?: any | null;
}
export interface PluginUpdateInput {
active?: boolean | null;
configuration?: (ConfigurationItemInput | null)[] | null;
}
export interface PriceRangeInput {
gte?: number | null;
lte?: number | null;
@ -480,6 +495,7 @@ export interface ProductFilterInput {
stockAvailability?: StockAvailability | null;
productType?: string | null;
search?: string | null;
minimalPrice?: PriceRangeInput | null;
}
export interface ProductTypeInput {