Attach permission variables to all queries (#1000)

* [SALEOR-2190] Attach permission variables to all queries

* Fix TS linter issues

* Update package-lock
This commit is contained in:
Jakub Majorek 2021-03-09 09:44:09 +01:00 committed by GitHub
parent 2cd4ea9529
commit a7736e2bf9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 28496 additions and 968 deletions

View file

@ -21,8 +21,15 @@
"default": "array-simple"
}
],
"@typescript-eslint/ban-types": "error",
"@typescript-eslint/class-name-casing": "error",
"@typescript-eslint/ban-types": [
"error",
{
"extendDefaults": true,
"types": {
"{}": false
}
}
],
"@typescript-eslint/consistent-type-assertions": "error",
"@typescript-eslint/consistent-type-definitions": "error",
"@typescript-eslint/explicit-member-accessibility": "off",
@ -128,7 +135,7 @@
"no-multiple-empty-lines": "off",
"no-new-func": "error",
"no-new-wrappers": "error",
"no-redeclare": "error",
"no-redeclare": "off",
"no-return-await": "error",
"no-sequences": "error",
"no-shadow": [
@ -155,7 +162,7 @@
"radix": "error",
"simple-import-sort/sort": ["error"],
"sort-imports": "off", // imports are handled by simple-import-sort/sort
"sort-keys": "warn",
"sort-keys": "off",
"space-before-function-paren": "off",
"spaced-comment": "error",
"use-isnan": "error",

View file

@ -1879,10 +1879,6 @@
"src_dot_components_dot_ErrorPage_dot_3182212440": {
"string": "We've encountered a problem..."
},
"src_dot_components_dot_FileUpload_dot_3050254265": {
"context": "upload file, button",
"string": "Upload"
},
"src_dot_components_dot_FilterBar_dot_2173195312": {
"context": "button",
"string": "Delete Search"

29222
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -26,8 +26,8 @@
"@material-ui/icons": "^4.5.1",
"@material-ui/styles": "^4.5.2",
"@saleor/macaw-ui": "^0.1.1-9",
"@types/faker": "^5.1.6",
"@sentry/react": "^6.0.0",
"@types/faker": "^5.1.6",
"apollo": "^2.21.2",
"apollo-cache-inmemory": "^1.6.5",
"apollo-client": "^2.6.8",
@ -45,7 +45,6 @@
"editorjs-undo": "^0.1.4",
"faker": "^5.1.0",
"fast-array-diff": "^0.2.0",
"fsevents": "^1.2.9",
"fuzzaldrin": "^2.1.0",
"graphql": "^14.4.2",
"graphql-tag": "^2.11.0",
@ -76,7 +75,7 @@
"react-sortable-tree": "^2.6.2",
"semver-compare": "^1.0.0",
"slugify": "^1.4.6",
"typescript": "^3.9.7",
"typescript": "^4.2.3",
"url-join": "^4.0.1",
"use-react-router": "^1.0.7"
},
@ -91,7 +90,7 @@
"@babel/plugin-proposal-optional-chaining": "^7.8.3",
"@babel/preset-env": "^7.5.4",
"@babel/preset-react": "^7.7.4",
"@babel/preset-typescript": "^7.7.4",
"@babel/preset-typescript": "^7.13.0",
"@babel/runtime": "^7.7.6",
"@pollyjs/adapter-node-http": "^5.0.0",
"@pollyjs/core": "^5.0.0",
@ -123,8 +122,8 @@
"@types/storybook__react": "^4.0.2",
"@types/url-join": "^4.0.0",
"@types/webappsec-credential-management": "^0.5.1",
"@typescript-eslint/eslint-plugin": "^2.12.0",
"@typescript-eslint/parser": "^2.9.0",
"@typescript-eslint/eslint-plugin": "^4.16.1",
"@typescript-eslint/parser": "^4.16.1",
"babel-core": "^7.0.0-bridge.0",
"babel-jest": "^23.6.0",
"babel-loader": "^8.0.6",

2
react-intl.d.ts vendored
View file

@ -13,7 +13,7 @@ declare module "react-intl" {
MessageDescriptor
>;
type PrimitiveType = string | number | boolean | null | undefined | Date;
type FormatXMLElementFn = (...args: any[]) => string | object;
type FormatXMLElementFn = (...args: any[]) => string | {};
export interface IntlFormatters
extends Omit<ReactIntl.IntlFormatters, "formatMessage"> {
formatMessage(

View file

@ -1,61 +0,0 @@
import Button from "@material-ui/core/Button";
import { makeStyles } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField";
import React from "react";
import { FormattedMessage } from "react-intl";
const useStyles = makeStyles(
{
fileUploadField: {
display: "none"
},
root: {
display: "flex"
},
textField: {
flex: 1
}
},
{ name: "FileUpload" }
);
interface FileUploadProps {
disabled?: boolean;
name?: string;
value?: any;
onChange?(event: React.ChangeEvent<any>);
}
const FileUpload: React.FC<FileUploadProps> = props => {
const { disabled, name, value, onChange } = props;
const classes = useStyles(props);
return (
<div className={classes.root}>
<input
disabled={disabled}
name={name}
onChange={onChange}
ref={ref => (this.upload = ref)}
className={classes.fileUploadField}
type="file"
value={value}
/>
<TextField
className={classes.textField}
disabled={disabled}
onChange={undefined}
value={value}
/>
<Button disabled={disabled} onClick={() => this.upload.click()}>
<FormattedMessage
defaultMessage="Upload"
description="upload file, button"
/>
</Button>
</div>
);
};
FileUpload.displayName = "FileUpload";
export default FileUpload;

View file

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

View file

@ -303,7 +303,7 @@ export const searchPageProps: SearchPageProps = {
onSearchChange: () => undefined
};
export const filterPageProps: FilterPageProps<string, object> = {
export const filterPageProps: FilterPageProps<string, {}> = {
...searchPageProps,
...tabPageProps,
filterOpts: {},

View file

@ -4,20 +4,28 @@ import gql from "graphql-tag";
import { Home, HomeVariables } from "./types/Home";
const home = gql`
query Home($channel: String!) {
salesToday: ordersTotal(period: TODAY, channel: $channel) {
query Home(
$channel: String!
$PERMISSION_MANAGE_PRODUCTS: Boolean!
$PERMISSION_MANAGE_ORDERS: Boolean!
) {
salesToday: ordersTotal(period: TODAY, channel: $channel)
@include(if: $PERMISSION_MANAGE_ORDERS) {
gross {
amount
currency
}
}
ordersToday: orders(created: TODAY, channel: $channel) {
ordersToday: orders(created: TODAY, channel: $channel)
@include(if: $PERMISSION_MANAGE_ORDERS) {
totalCount
}
ordersToFulfill: orders(status: READY_TO_FULFILL, channel: $channel) {
ordersToFulfill: orders(status: READY_TO_FULFILL, channel: $channel)
@include(if: $PERMISSION_MANAGE_ORDERS) {
totalCount
}
ordersToCapture: orders(status: READY_TO_CAPTURE, channel: $channel) {
ordersToCapture: orders(status: READY_TO_CAPTURE, channel: $channel)
@include(if: $PERMISSION_MANAGE_ORDERS) {
totalCount
}
productsOutOfStock: products(
@ -30,7 +38,7 @@ const home = gql`
period: TODAY
first: 5
channel: $channel
) {
) @include(if: $PERMISSION_MANAGE_PRODUCTS) {
edges {
node {
id
@ -57,7 +65,8 @@ const home = gql`
}
}
}
activities: homepageEvents(last: 10) {
activities: homepageEvents(last: 10)
@include(if: $PERMISSION_MANAGE_ORDERS) {
edges {
node {
amount

View file

@ -136,4 +136,6 @@ export interface Home {
export interface HomeVariables {
channel: string;
PERMISSION_MANAGE_PRODUCTS: boolean;
PERMISSION_MANAGE_ORDERS: boolean;
}

View file

@ -26,10 +26,10 @@ const HomeSection = () => {
return (
<HomePage
activities={data?.activities.edges.map(edge => edge.node).reverse()}
orders={data?.ordersToday.totalCount}
sales={data?.salesToday.gross}
topProducts={data?.productTopToday.edges.map(edge => edge.node)}
activities={data?.activities?.edges.map(edge => edge.node).reverse()}
orders={data?.ordersToday?.totalCount}
sales={data?.salesToday?.gross}
topProducts={data?.productTopToday?.edges.map(edge => edge.node)}
onProductClick={(productId, variantId) =>
navigate(productVariantEditUrl(productId, variantId))
}
@ -57,8 +57,8 @@ const HomeSection = () => {
})
)
}
ordersToCapture={data?.ordersToCapture.totalCount}
ordersToFulfill={data?.ordersToFulfill.totalCount}
ordersToCapture={data?.ordersToCapture?.totalCount}
ordersToFulfill={data?.ordersToFulfill?.totalCount}
productsOutOfStock={data?.productsOutOfStock.totalCount}
userName={getUserName(user, true)}
userPermissions={user?.userPermissions}

View file

@ -6,10 +6,33 @@ import { useEffect } from "react";
import { QueryResult, useQuery as useBaseQuery } from "react-apollo";
import { useIntl } from "react-intl";
import { User_userPermissions } from "../fragments/types/User";
import { PrefixedPermissions } from "../types/extendedTypes";
import { PermissionEnum } from "../types/globalTypes";
import useAppState from "./useAppState";
import useNotifier from "./useNotifier";
import useUser from "./useUser";
const getPermissionKey = (permission: string) =>
`PERMISSION_${permission}` as PrefixedPermissions;
const allPermissions = Object.keys(PermissionEnum).reduce(
(prev, code) => ({
...prev,
[getPermissionKey(code)]: false
}),
{} as Record<PrefixedPermissions, boolean>
);
const getUserPermissions = (userPermissions: User_userPermissions[]) =>
userPermissions.reduce(
(prev, permission) => ({
...prev,
[getPermissionKey(permission.code)]: true
}),
{} as Record<PrefixedPermissions, boolean>
);
export interface LoadMore<TData, TVariables> {
loadMore: (
mergeFunc: (prev: TData, next: TData) => TData,
@ -25,7 +48,7 @@ type UseQueryOpts<TVariables> = Partial<{
variables: TVariables;
}>;
type UseQueryHook<TData, TVariables> = (
opts: UseQueryOpts<TVariables>
opts: UseQueryOpts<Omit<TVariables, PrefixedPermissions>>
) => UseQueryResult<TData, TVariables>;
function makeQuery<TData, TVariables>(
@ -40,6 +63,15 @@ function makeQuery<TData, TVariables>(
const intl = useIntl();
const [, dispatchAppState] = useAppState();
const user = useUser();
const userPermissions = getUserPermissions(
user.user?.userPermissions || []
);
const variablesWithPermissions = {
...variables,
...allPermissions,
...userPermissions
};
const queryData = useBaseQuery(query, {
context: {
@ -56,7 +88,7 @@ function makeQuery<TData, TVariables>(
intl
),
skip,
variables
variables: variablesWithPermissions
});
useEffect(() => {
@ -82,7 +114,7 @@ function makeQuery<TData, TVariables>(
}
return mergeFunc(previousResults, fetchMoreResult);
},
variables: { ...variables, ...extraVariables }
variables: { ...variablesWithPermissions, ...extraVariables }
});
return {

View file

@ -3,16 +3,16 @@ import { removeAtIndex } from "@saleor/utils/lists";
import useStateFromProps from "./useStateFromProps";
export type FormsetChange<TValue = any> = (id: string, value: TValue) => void;
export interface FormsetAtomicData<TData = object, TValue = any> {
export interface FormsetAtomicData<TData = {}, TValue = any> {
data: TData;
id: string;
label: string;
value: TValue;
}
export type FormsetData<TData = object, TValue = any> = Array<
export type FormsetData<TData = {}, TValue = any> = Array<
FormsetAtomicData<TData, TValue>
>;
export interface UseFormsetOutput<TData = object, TValue = any> {
export interface UseFormsetOutput<TData = {}, TValue = any> {
add: (data: FormsetAtomicData<TData, TValue>) => void;
change: FormsetChange<TValue>;
data: FormsetData<TData, TValue>;
@ -21,7 +21,7 @@ export interface UseFormsetOutput<TData = object, TValue = any> {
set: (data: FormsetData<TData, TValue>) => void;
remove: (id: string) => void;
}
function useFormset<TData = object, TValue = any>(
function useFormset<TData = {}, TValue = any>(
initial: FormsetData<TData, TValue>
): UseFormsetOutput<TData, TValue> {
const [data, setData] = useStateFromProps<FormsetData<TData, TValue>>(

View file

@ -245,7 +245,7 @@ export function only<T>(obj: T, key: keyof T): boolean {
);
}
export function empty(obj: object): boolean {
export function empty(obj: {}): boolean {
return Object.keys(obj).every(key => obj[key] === undefined);
}
@ -376,10 +376,7 @@ export function generateCode(charNum: number) {
return result;
}
export function findInEnum<TEnum extends object>(
needle: string,
haystack: TEnum
) {
export function findInEnum<TEnum extends {}>(needle: string, haystack: TEnum) {
const match = Object.keys(haystack).find(key => key === needle);
if (!!match) {
return haystack[needle as keyof TEnum];
@ -388,7 +385,7 @@ export function findInEnum<TEnum extends object>(
throw new Error(`Key ${needle} not found in enum`);
}
export function findValueInEnum<TEnum extends object>(
export function findValueInEnum<TEnum extends {}>(
needle: string,
haystack: TEnum
): TEnum[keyof TEnum] {

View file

@ -9,7 +9,6 @@ import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import { CSSProperties } from "@material-ui/styles";
import CardTitle from "@saleor/components/CardTitle";
import Money from "@saleor/components/Money";
import Skeleton from "@saleor/components/Skeleton";
@ -25,7 +24,7 @@ import { OrderRefundFormData } from "../OrderRefundPage/form";
const useStyles = makeStyles(
theme => {
const inputPadding: CSSProperties = {
const inputPadding = {
paddingBottom: theme.spacing(2),
paddingTop: theme.spacing(2)
};
@ -84,7 +83,7 @@ const OrderRefundFulfilledProducts: React.FC<OrderRefundFulfilledProductsProps>
onRefundedProductQuantityChange,
onSetMaximalQuantities
} = props;
const classes = useStyles(props);
const classes = useStyles({});
const intl = useIntl();
return (

View file

@ -9,7 +9,6 @@ import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import { CSSProperties } from "@material-ui/styles";
import CardTitle from "@saleor/components/CardTitle";
import Money from "@saleor/components/Money";
import Skeleton from "@saleor/components/Skeleton";
@ -24,7 +23,7 @@ import { OrderRefundFormData } from "../OrderRefundPage/form";
const useStyles = makeStyles(
theme => {
const inputPadding: CSSProperties = {
const inputPadding = {
paddingBottom: theme.spacing(2),
paddingTop: theme.spacing(2)
};
@ -77,7 +76,7 @@ const OrderRefundUnfulfilledProducts: React.FC<OrderRefundUnfulfilledProductsPro
onRefundedProductQuantityChange,
onSetMaximalQuantities
} = props;
const classes = useStyles(props);
const classes = useStyles({});
const intl = useIntl();
return (

View file

@ -1,10 +0,0 @@
import FileUpload from "@saleor/components/FileUpload";
import { storiesOf } from "@storybook/react";
import React from "react";
import Decorator from "../../Decorator";
storiesOf("Components / FileUpload", module)
.addDecorator(Decorator)
.add("default", () => <FileUpload />)
.add("other", () => <FileUpload />);

View file

@ -96,7 +96,7 @@ export interface SearchProps {
export interface SearchPageProps extends SearchProps {
initialSearch: string;
}
export interface FilterPageProps<TKeys extends string, TOpts extends object>
export interface FilterPageProps<TKeys extends string, TOpts extends {}>
extends FilterProps<TKeys>,
SearchPageProps,
TabPageProps {

View file

@ -0,0 +1,3 @@
import { PermissionEnum } from "./globalTypes";
export type PrefixedPermissions = `PERMISSION_${PermissionEnum}`;

View file

@ -3,9 +3,9 @@ import { findValueInEnum } from "@saleor/misc";
import isArray from "lodash-es/isArray";
function createFilterUtils<
TQueryParams extends object,
TFilters extends object
>(filters: object) {
TQueryParams extends {},
TFilters extends {}
>(filters: {}) {
function getActiveFilters(params: TQueryParams): TFilters {
return Object.keys(params)
.filter(key => Object.keys(filters).includes(key))
@ -35,11 +35,11 @@ export function dedupeFilter<T>(array: T[]): T[] {
export type GetFilterQueryParam<
TFilterKeys extends string,
TFilters extends object
> = (filter: IFilterElement<TFilterKeys>, params?: object) => TFilters;
TFilters extends {}
> = (filter: IFilterElement<TFilterKeys>, params?: {}) => TFilters;
export function getFilterQueryParams<
TFilterKeys extends string,
TUrlFilters extends object
TUrlFilters extends {}
>(
filter: IFilter<TFilterKeys>,
getFilterQueryParam: GetFilterQueryParam<TFilterKeys, TUrlFilters>
@ -86,7 +86,7 @@ export function getSingleValueQueryParam<
export function getSingleEnumValueQueryParam<
TKey extends string,
TUrlKey extends string,
TEnum extends object
TEnum extends {}
>(param: IFilterElement<TKey>, key: TUrlKey, haystack: TEnum) {
const { active, value } = param;
@ -104,7 +104,7 @@ export function getSingleEnumValueQueryParam<
export function getMultipleEnumValueQueryParam<
TKey extends string,
TUrlKey extends string,
TEnum extends object
TEnum extends {}
>(param: IFilterElement<TKey>, key: TUrlKey, haystack: TEnum) {
const { active, value } = param;

View file

@ -14,7 +14,7 @@ type CreateFilterHandlers<TFilterKeys extends string> = [
function createFilterHandlers<
TFilterKeys extends string,
TFilters extends object
TFilters extends {}
>(opts: {
getFilterQueryParam: GetFilterQueryParam<TFilterKeys, TFilters>;
navigate: UseNavigatorResult;

View file

@ -16,7 +16,7 @@ describe("Multiple file upload handler", () => {
onStart: jest.fn()
};
const handle = createMultiFileUploadHandler(() => {
const promise = new Promise(resolve => {
const promise = new Promise<void>(resolve => {
expect(cbs.onBeforeUpload).toBeCalledTimes(
cbs.onAfterUpload.mock.calls.length + 1
);
@ -46,7 +46,7 @@ describe("Multiple file upload handler", () => {
onStart: jest.fn()
};
const handle = createMultiFileUploadHandler((_, fileIndex) => {
const promise = new Promise((resolve, reject) => {
const promise = new Promise<void>((resolve, reject) => {
if (fileIndex === 2) {
reject();
} else {

View file

@ -1,7 +1,7 @@
import isArray from "lodash-es/isArray";
import { stringify } from "qs";
export function stringifyQs(params: object): string {
export function stringifyQs(params: {}): string {
return stringify(params, {
indices: false
});

View file

@ -1,7 +1,7 @@
import { IFilter } from "@saleor/components/Filter";
import clone from "lodash-es/clone";
export function getExistingKeys(o: object): string[] {
export function getExistingKeys(o: {}): string[] {
return Object.keys(o).filter(key => o[key] !== undefined && o[key] !== null);
}