Add service list

This commit is contained in:
dominik-zeglen 2019-09-25 16:11:45 +02:00
parent 1be6d8aad4
commit b5c3a7eb3e
17 changed files with 536 additions and 0 deletions

View file

@ -6,6 +6,7 @@ import { WindowTitle } from "@saleor/components/WindowTitle";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useUser from "@saleor/hooks/useUser"; import useUser from "@saleor/hooks/useUser";
import Attributes from "@saleor/icons/Attributes"; import Attributes from "@saleor/icons/Attributes";
import Bot from "@saleor/icons/Bot";
import Navigation from "@saleor/icons/Navigation"; import Navigation from "@saleor/icons/Navigation";
import Pages from "@saleor/icons/Pages"; import Pages from "@saleor/icons/Pages";
import Plugins from "@saleor/icons/Plugins"; import Plugins from "@saleor/icons/Plugins";
@ -20,6 +21,7 @@ import { menuListUrl } from "@saleor/navigation/urls";
import { pageListUrl } from "@saleor/pages/urls"; import { pageListUrl } from "@saleor/pages/urls";
import { pluginsListUrl } from "@saleor/plugins/urls"; import { pluginsListUrl } from "@saleor/plugins/urls";
import { productTypeListUrl } from "@saleor/productTypes/urls"; import { productTypeListUrl } from "@saleor/productTypes/urls";
import { serviceListUrl } from "@saleor/services/urls";
import { shippingZonesListUrl } from "@saleor/shipping/urls"; import { shippingZonesListUrl } from "@saleor/shipping/urls";
import { siteSettingsUrl } from "@saleor/siteSettings/urls"; import { siteSettingsUrl } from "@saleor/siteSettings/urls";
import { staffListUrl } from "@saleor/staff/urls"; import { staffListUrl } from "@saleor/staff/urls";
@ -150,6 +152,15 @@ export function createConfigurationMenu(intl: IntlShape): MenuSection[] {
permission: PermissionEnum.MANAGE_SETTINGS, permission: PermissionEnum.MANAGE_SETTINGS,
title: intl.formatMessage(sectionNames.plugins), title: intl.formatMessage(sectionNames.plugins),
url: pluginsListUrl() url: pluginsListUrl()
},
{
description: intl.formatMessage({
defaultMessage: "Manage external integrations accounts"
}),
icon: <Bot fontSize="inherit" viewBox="0 0 44 44" />,
permission: PermissionEnum.MANAGE_SERVICE_ACCOUNTS,
title: intl.formatMessage(sectionNames.serviceAccounts),
url: serviceListUrl()
} }
] ]
} }

13
src/icons/Bot.tsx Normal file
View file

@ -0,0 +1,13 @@
import createSvgIcon from "@material-ui/icons/utils/createSvgIcon";
import React from "react";
export const Bot = createSvgIcon(
<path
fillRule="evenodd"
clipRule="evenodd"
d="M22.1012 3.96887C22.1012 5.12931 21.1604 6.07004 20 6.07004C18.8396 6.07004 17.8988 5.12931 17.8988 3.96887C17.8988 2.80843 18.8396 1.8677 20 1.8677C21.1604 1.8677 22.1012 2.80843 22.1012 3.96887ZM20.856 7.84519C22.6365 7.45378 23.9689 5.86692 23.9689 3.96887C23.9689 1.77692 22.1919 0 20 0C17.8081 0 16.0311 1.77692 16.0311 3.96887C16.0311 5.81119 17.2864 7.36033 18.9883 7.80765V10.1167H5.29183V16.3424H0V28.7938H5.29183V35.0195H34.7082V28.7938H40V16.3424H34.7082V10.1167H20.856V7.84519ZM34.7082 26.9261V18.2101H38.1323V26.9261H34.7082ZM5.29183 18.2101H1.8677V26.9261H5.29183V18.2101ZM12.2986 26.3603H27.7916V33.1517H32.8405V11.9844H7.15953V33.1517H12.2986V26.3603ZM14.1663 33.1517V28.228H25.9239V33.1517H14.1663ZM16.6537 19.6887C16.6537 20.8151 15.7125 21.7899 14.4747 21.7899C13.2369 21.7899 12.2957 20.8151 12.2957 19.6887C12.2957 18.5623 13.2369 17.5875 14.4747 17.5875C15.7125 17.5875 16.6537 18.5623 16.6537 19.6887ZM18.5214 19.6887C18.5214 21.8807 16.7096 23.6576 14.4747 23.6576C12.2398 23.6576 10.428 21.8807 10.428 19.6887C10.428 17.4968 12.2398 15.7198 14.4747 15.7198C16.7096 15.7198 18.5214 17.4968 18.5214 19.6887ZM25.4475 21.7899C26.6079 21.7899 27.5486 20.8492 27.5486 19.6887C27.5486 18.5283 26.6079 17.5875 25.4475 17.5875C24.287 17.5875 23.3463 18.5283 23.3463 19.6887C23.3463 20.8492 24.287 21.7899 25.4475 21.7899ZM25.4475 23.6576C27.6394 23.6576 29.4163 21.8807 29.4163 19.6887C29.4163 17.4968 27.6394 15.7198 25.4475 15.7198C23.2555 15.7198 21.4786 17.4968 21.4786 19.6887C21.4786 21.8807 23.2555 23.6576 25.4475 23.6576Z"
fill="#06847B"
/>
);
Bot.displayName = "Bot";
export default Bot;

View file

@ -171,6 +171,10 @@ export const sectionNames = defineMessages({
defaultMessage: "Sales", defaultMessage: "Sales",
description: "sales section name" description: "sales section name"
}, },
serviceAccounts: {
defaultMessage: "Service Accounts",
description: "service accounts section name"
},
shipping: { shipping: {
defaultMessage: "Shipping Methods", defaultMessage: "Shipping Methods",
description: "shipping section name" description: "shipping section name"

View file

@ -0,0 +1,180 @@
import IconButton from "@material-ui/core/IconButton";
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 Typography from "@material-ui/core/Typography";
import DeleteIcon from "@material-ui/icons/Delete";
import EditIcon from "@material-ui/icons/Edit";
import React from "react";
import { FormattedMessage } from "react-intl";
import Checkbox from "@saleor/components/Checkbox";
import Skeleton from "@saleor/components/Skeleton";
import TableHead from "@saleor/components/TableHead";
import TablePagination from "@saleor/components/TablePagination";
import { maybe, renderCollection } from "@saleor/misc";
import { ListActions, ListProps } from "@saleor/types";
import { ServiceList_serviceAccounts_edges_node } from "../../types/ServiceList";
export interface ServiceListProps extends ListProps, ListActions {
services: ServiceList_serviceAccounts_edges_node[];
onRemove: (id: string) => void;
}
const styles = (theme: Theme) =>
createStyles({
[theme.breakpoints.up("lg")]: {
colName: {
"&&": {
width: "auto"
}
}
},
colAction: {
"&&": {
paddingRight: theme.spacing.unit
},
textAlign: "right",
width: 100
},
colName: {
paddingLeft: 0,
width: 250
},
table: {
tableLayout: "fixed"
}
});
const numberOfColumns = 3;
const ServiceList = withStyles(styles, {
name: "ServiceList"
})(
({
classes,
settings,
disabled,
onNextPage,
onPreviousPage,
onUpdateListSettings,
onRemove,
onRowClick,
pageInfo,
services,
isChecked,
selected,
toggle,
toggleAll,
toolbar
}: ServiceListProps & WithStyles<typeof styles>) => (
<Table className={classes.table}>
<TableHead
colSpan={numberOfColumns}
selected={selected}
disabled={disabled}
items={services}
toggleAll={toggleAll}
toolbar={toolbar}
>
<TableCell className={classes.colName}>
<FormattedMessage defaultMessage="Code" description="voucher code" />
</TableCell>
<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(
services,
service => {
const isSelected = service ? isChecked(service.id) : false;
return (
<TableRow
className={!!service ? classes.tableRow : undefined}
hover={!!service}
key={service ? service.id : "skeleton"}
selected={isSelected}
onClick={service ? onRowClick(service.id) : undefined}
>
<TableCell padding="checkbox">
<Checkbox
checked={isSelected}
disabled={disabled}
disableClickPropagation
onChange={() => toggle(service.id)}
/>
</TableCell>
<TableCell className={classes.colName}>
<span data-tc="name">
{maybe<React.ReactNode>(() => service.name, <Skeleton />)}
</span>
<Typography data-tc="isActive" variant="caption">
{maybe(() =>
service.isActive ? (
<FormattedMessage
defaultMessage="active"
description="account status"
/>
) : (
<FormattedMessage
defaultMessage="inactive"
description="account status"
/>
)
)}
</Typography>
</TableCell>
<TableCell className={classes.colAction}>
<IconButton
color="primary"
onClick={service ? onRowClick(service.id) : undefined}
>
<EditIcon />
</IconButton>
<IconButton
color="primary"
onClick={service ? () => onRemove(service.id) : undefined}
>
<DeleteIcon />
</IconButton>
</TableCell>
</TableRow>
);
},
() => (
<TableRow>
<TableCell colSpan={numberOfColumns}>
<FormattedMessage defaultMessage="No service accounts found" />
</TableCell>
</TableRow>
)
)}
</TableBody>
</Table>
)
);
ServiceList.displayName = "ServiceList";
export default ServiceList;

View file

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

View file

@ -0,0 +1,32 @@
import { storiesOf } from "@storybook/react";
import React from "react";
import {
listActionsProps,
pageListProps,
searchPageProps,
tabPageProps
} from "@saleor/fixtures";
import ServiceListPage, {
ServiceListPageProps
} from "@saleor/services/components/ServiceListPage";
import Decorator from "@saleor/storybook/Decorator";
import { serviceList } from "../../fixtures";
const props: ServiceListPageProps = {
...listActionsProps,
...pageListProps.default,
...searchPageProps,
...tabPageProps,
onBack: () => undefined,
onRemove: () => undefined,
services: serviceList
};
storiesOf("Views / Services / Service list", module)
.addDecorator(Decorator)
.add("default", () => <ServiceListPage {...props} />)
.add("loading", () => (
<ServiceListPage {...props} disabled={true} services={undefined} />
))
.add("no data", () => <ServiceListPage {...props} services={[]} />);

View file

@ -0,0 +1,82 @@
import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import AppHeader from "@saleor/components/AppHeader";
import Container from "@saleor/components/Container";
import PageHeader from "@saleor/components/PageHeader";
import SearchBar from "@saleor/components/SearchBar";
import { sectionNames } from "@saleor/intl";
import {
ListActions,
PageListProps,
SearchPageProps,
TabPageProps
} from "@saleor/types";
import { ServiceList_serviceAccounts_edges_node } from "../../types/ServiceList";
import ServiceList from "../ServiceList";
export interface ServiceListPageProps
extends PageListProps,
ListActions,
SearchPageProps,
TabPageProps {
services: ServiceList_serviceAccounts_edges_node[];
onBack: () => void;
onRemove: (id: string) => void;
}
const ServiceListPage: React.StatelessComponent<ServiceListPageProps> = ({
currentTab,
initialSearch,
onAdd,
onAll,
onBack,
onSearchChange,
onTabChange,
onTabDelete,
onTabSave,
tabs,
...listProps
}) => {
const intl = useIntl();
return (
<Container>
<AppHeader onBack={onBack}>
{intl.formatMessage(sectionNames.configuration)}
</AppHeader>
<PageHeader title={intl.formatMessage(sectionNames.serviceAccounts)}>
<Button onClick={onAdd} variant="contained" color="primary">
<FormattedMessage
defaultMessage="Create account"
description="button"
/>
</Button>
</PageHeader>
<Card>
<SearchBar
allTabLabel={intl.formatMessage({
defaultMessage: "All Service Accounts",
description: "tab name"
})}
currentTab={currentTab}
initialSearch={initialSearch}
searchPlaceholder={intl.formatMessage({
defaultMessage: "Search Service Accounts"
})}
tabs={tabs}
onAll={onAll}
onSearchChange={onSearchChange}
onTabChange={onTabChange}
onTabDelete={onTabDelete}
onTabSave={onTabSave}
/>
<ServiceList {...listProps} />
</Card>
</Container>
);
};
ServiceListPage.displayName = "ServiceListPage";
export default ServiceListPage;

View file

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

22
src/services/fixtures.ts Normal file
View file

@ -0,0 +1,22 @@
import { ServiceList_serviceAccounts_edges_node } from "./types/ServiceList";
export const serviceList: ServiceList_serviceAccounts_edges_node[] = [
{
__typename: "ServiceAccount" as "ServiceAccount",
id: "1",
isActive: true,
name: "Slack"
},
{
__typename: "ServiceAccount" as "ServiceAccount",
id: "2",
isActive: true,
name: "Facebook Market"
},
{
__typename: "ServiceAccount" as "ServiceAccount",
id: "3",
isActive: false,
name: "Magento Importer"
}
];

51
src/services/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 {
serviceListPath,
ServiceListUrlQueryParams
// servicePath
} from "./urls";
// import ServiceDetailsComponent from "./views/ServiceDetails";
import ServiceListComponent from "./views/ServiceList";
const PluginList: React.StatelessComponent<RouteComponentProps<any>> = ({
location
}) => {
const qs = parseQs(location.search.substr(1));
const params: ServiceListUrlQueryParams = qs;
return <ServiceListComponent params={params} />;
};
// const ServiceDetails: React.StatelessComponent<RouteComponentProps<any>> = ({
// match
// }) => {
// const qs = parseQs(location.search.substr(1));
// const params: ServiceListUrlQueryParams = qs;
// return (
// <ServiceDetailsComponent
// id={decodeURIComponent(match.params.id)}
// params={params}
// />
// );
// };
const Component = () => {
const intl = useIntl();
return (
<>
<WindowTitle title={intl.formatMessage(sectionNames.serviceAccounts)} />
<Switch>
<Route exact path={serviceListPath} component={PluginList} />
{/* <Route path={servicePath(":id")} component={ServiceDetails} /> */}
</Switch>
</>
);
};
export default Component;

41
src/services/queries.ts Normal file
View file

@ -0,0 +1,41 @@
import gql from "graphql-tag";
import { pageInfoFragment, TypedQuery } from "../queries";
import { ServiceList, ServiceListVariables } from "./types/ServiceList";
export const serviceFragment = gql`
fragment ServiceFragment on ServiceAccount {
id
name
isActive
}
`;
const serviceList = gql`
${pageInfoFragment}
${serviceFragment}
query ServiceList(
$first: Int
$after: String
$last: Int
$before: String
$filter: ServiceAccountFilterInput
) {
serviceAccounts(
first: $first
after: $after
before: $before
last: $last
filter: $filter
) {
edges {
node {
...ServiceFragment
}
}
}
}
`;
export const ServiceListQuery = TypedQuery<ServiceList, ServiceListVariables>(
serviceList
);

View file

@ -0,0 +1,14 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL fragment: ServiceFragment
// ====================================================
export interface ServiceFragment {
__typename: "ServiceAccount";
id: string;
name: string | null;
isActive: boolean | null;
}

View file

@ -0,0 +1,38 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { ServiceAccountFilterInput } from "./../../types/globalTypes";
// ====================================================
// GraphQL query operation: ServiceList
// ====================================================
export interface ServiceList_serviceAccounts_edges_node {
__typename: "ServiceAccount";
id: string;
name: string | null;
isActive: boolean | null;
}
export interface ServiceList_serviceAccounts_edges {
__typename: "ServiceAccountCountableEdge";
node: ServiceList_serviceAccounts_edges_node;
}
export interface ServiceList_serviceAccounts {
__typename: "ServiceAccountCountableConnection";
edges: ServiceList_serviceAccounts_edges[];
}
export interface ServiceList {
serviceAccounts: ServiceList_serviceAccounts | null;
}
export interface ServiceListVariables {
first?: number | null;
after?: string | null;
last?: number | null;
before?: string | null;
filter?: ServiceAccountFilterInput | null;
}

37
src/services/urls.ts Normal file
View file

@ -0,0 +1,37 @@
import { stringify as stringifyQs } from "qs";
import urlJoin from "url-join";
import {
ActiveTab,
BulkAction,
Dialog,
Filters,
Pagination,
SingleAction,
TabActionDialog
} from "../types";
export const serviceSection = "/services/";
export const serviceListPath = serviceSection;
export enum ServiceListUrlFiltersEnum {
query = "query"
}
export type ServiceListUrlFilters = Filters<ServiceListUrlFiltersEnum>;
export type ServiceListUrlDialog = "remove" | TabActionDialog;
export type ServiceListUrlQueryParams = ActiveTab &
BulkAction &
ServiceListUrlFilters &
Dialog<ServiceListUrlDialog> &
Pagination;
export const serviceListUrl = (params?: ServiceListUrlQueryParams) =>
serviceListPath + "?" + stringifyQs(params);
export const servicePath = (id: string) => urlJoin(serviceSection, id);
export type ServiceUrlDialog = "create-token" | "remove" | "remove-token";
export type ServiceUrlQueryParams = Dialog<ServiceUrlDialog> & SingleAction;
export const serviceUrl = (id: string, params?: ServiceUrlQueryParams) =>
servicePath(encodeURIComponent(id)) + "?" + stringifyQs(params);
export const serviceAddPath = urlJoin(serviceSection, "add");
export const serviceAddUrl = serviceAddPath;

View file

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

View file

@ -649,6 +649,11 @@ export interface SeoInput {
description?: string | null; description?: string | null;
} }
export interface ServiceAccountFilterInput {
search?: string | null;
isActive?: boolean | null;
}
export interface ShippingPriceInput { export interface ShippingPriceInput {
name?: string | null; name?: string | null;
price?: any | null; price?: any | null;