, loadPage: number) {
22 | const { pagination } = model;
23 |
24 | model.clearIndexes();
25 |
26 | if (model.getPageItems(loadPage).length) {
27 | pagination.setPage(loadPage);
28 |
29 | return;
30 | }
31 |
32 | model.setIsLoading(true);
33 |
34 | const wrap = this.externalService.getItems(loadPage);
35 |
36 | if (!wrap) return;
37 |
38 | const promise = await wrap.promise;
39 |
40 | model.tableRequestId = undefined;
41 | pagination.setPage(loadPage);
42 | model.setPageItems(promise, loadPage);
43 | model.setIsLoading(false);
44 | }
45 | }
46 |
47 | export default ListService;
48 |
--------------------------------------------------------------------------------
/src/components/Loader/styled.ts:
--------------------------------------------------------------------------------
1 | import styled, { keyframes } from 'styled-components';
2 |
3 | const breatheAnimation = keyframes`
4 | 0% { transform: rotate(360deg); }
5 | 100%: { transform: rotate(0deg); }
6 | `;
7 |
8 | export const LoaderElement = styled.div`
9 | border-radius: 50%;
10 | color: #ffdd2d;
11 | font-size: 11px;
12 | text-indent: -99999em;
13 | margin: 55px auto;
14 | position: relative;
15 | width: 10em;
16 | height: 10em;
17 | box-shadow: inset 0 0 0 1em;
18 | transform: translateZ(0);
19 |
20 | &:before {
21 | position: absolute;
22 | content: '';
23 | width: 5.2em;
24 | height: 10.2em;
25 | background: #fff;
26 | border-radius: 10.2em 0 0 10.2em;
27 | top: -0.1em;
28 | left: -0.1em;
29 | transform-origin: 5.1em 5.1em;
30 | animation-name: ${breatheAnimation};
31 | animation-delay: 1.5s;
32 | animation-duration: 2s;
33 | animation-iteration-count: infinite;
34 | }
35 |
36 | &:after {
37 | position: absolute;
38 | content: '';
39 | width: 5.2em;
40 | height: 10.2em;
41 | background: #fff;
42 | border-radius: 0 10.2em 10.2em 0;
43 | top: -0.1em;
44 | left: 4.9em;
45 | transform-origin: 0.1em 5.1em;
46 | animation-name: ${breatheAnimation};
47 | animation-duration: 2s;
48 | animation-iteration-count: infinite;
49 | }
50 | `;
51 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "exclude": [
3 | "node_modules"
4 | ],
5 | "compileOnSave": true,
6 | "compilerOptions": {
7 | "baseUrl": ".",
8 | "paths": {
9 | "common-components/*": [
10 | "./src/components/*"
11 | ],
12 | "services/*": [
13 | "./src/services/*"
14 | ],
15 | "stores/*": [
16 | "./src/stores/*"
17 | ],
18 | "global-styles/*": [
19 | "./src/styles/*"
20 | ],
21 | "global-types/*": [
22 | "./src/types/*"
23 | ],
24 | "main-scene/*": [
25 | "./src/scenes/Main/*"
26 | ],
27 | "utils/*": [
28 | "./src/utils/*"
29 | ]
30 | },
31 | "target": "ESNext",
32 | "module": "esnext",
33 | "sourceMap": true,
34 | "jsx": "preserve",
35 | "removeComments": true,
36 | "noEmit": true,
37 | "strict": true,
38 | "noImplicitAny": true,
39 | "strictNullChecks": true,
40 | "strictFunctionTypes": true,
41 | "strictPropertyInitialization": true,
42 | "noImplicitThis": true,
43 | "noUnusedLocals": true,
44 | "noUnusedParameters": true,
45 | "noImplicitReturns": true,
46 | "esModuleInterop": true,
47 | "experimentalDecorators": true,
48 | "isolatedModules": true,
49 | "resolveJsonModule": true,
50 | "lib": [
51 | "dom",
52 | "dom.iterable",
53 | "esnext"
54 | ],
55 | "allowJs": true,
56 | "skipLibCheck": true,
57 | "forceConsistentCasingInFileNames": true,
58 | "moduleResolution": "node"
59 | },
60 | "include": [
61 | "next-env.d.ts",
62 | "**/*.ts",
63 | "**/*.tsx",
64 | "**/*.svg",
65 | ]
66 | }
67 |
--------------------------------------------------------------------------------
/src/components/HOC/Provider.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, JSXElementConstructor, Component, ComponentClass } from 'react';
2 | import { interfaces } from 'inversify';
3 |
4 | import { getDisplayName } from 'utils/index';
5 |
6 | import Context from './Context';
7 |
8 | type Props = {
9 | container: interfaces.Container;
10 | children: ReactNode;
11 | };
12 |
13 | function DiProvider({ container, children }: Props) {
14 | return {children};
15 | }
16 |
17 | function withProvider(
18 | component: JSXElementConstructor
& C,
19 | container: interfaces.Container
20 | ) {
21 | type Props = JSX.LibraryManagedAttributes;
22 |
23 | class ProviderWrap extends Component {
24 | // eslint-disable-next-line react/static-property-placement
25 | public static contextType = Context;
26 |
27 | // eslint-disable-next-line react/static-property-placement
28 | public static displayName = `diProvider(${getDisplayName(component)})`;
29 |
30 | public constructor(props: Props, context?: interfaces.Container) {
31 | super(props);
32 |
33 | this.context = context;
34 |
35 | if (this.context) {
36 | container.parent = this.context;
37 | }
38 | }
39 |
40 | public render() {
41 | const WrappedComponent = component;
42 |
43 | return (
44 |
45 |
46 |
47 | );
48 | }
49 | }
50 |
51 | return ProviderWrap as ComponentClass;
52 | }
53 |
54 | export default withProvider;
55 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend-live-2020",
3 | "version": "1.0.0",
4 | "description": "React DI example",
5 | "main": "index.ts",
6 | "scripts": {
7 | "dev": "next",
8 | "build": "next build",
9 | "start": "next start",
10 | "typescript": "tsc -b",
11 | "typescript:watch": "tsc --watch",
12 | "lint": "eslint --ext .ts,.tsx src/ --fix"
13 | },
14 | "author": "TQM Team",
15 | "license": "ISC",
16 | "dependencies": {
17 | "babel-plugin-parameter-decorator": "^1.0.16",
18 | "inversify": "^5.0.1",
19 | "mobx": "^5.15.6",
20 | "mobx-react": "^6.3.0",
21 | "next": "^9.5.3",
22 | "react": "^16.13.1",
23 | "react-dom": "^16.13.1",
24 | "reflect-metadata": "^0.1.13",
25 | "styled-components": "^5.2.0"
26 | },
27 | "devDependencies": {
28 | "@babel/plugin-proposal-class-properties": "^7.10.1",
29 | "@babel/plugin-proposal-decorators": "^7.10.5",
30 | "@svgr/webpack": "^5.4.0",
31 | "@types/node": "^14.6.4",
32 | "@types/react": "^16.9.49",
33 | "@types/react-dom": "^16.9.8",
34 | "@types/react-router": "^5.1.8",
35 | "@types/react-router-dom": "^5.1.5",
36 | "@types/styled-components": "^5.1.3",
37 | "@typescript-eslint/eslint-plugin": "^3.10.1",
38 | "@typescript-eslint/parser": "^3.10.1",
39 | "babel-eslint": "^10.1.0",
40 | "babel-plugin-styled-components": "^1.11.1",
41 | "eslint": "^7.8.1",
42 | "eslint-config-airbnb": "^18.2.0",
43 | "eslint-config-prettier": "^6.11.0",
44 | "eslint-import-resolver-alias": "^1.1.2",
45 | "eslint-plugin-import": "^2.22.0",
46 | "eslint-plugin-jsx-a11y": "^6.3.1",
47 | "eslint-plugin-prettier": "^3.1.4",
48 | "eslint-plugin-react": "^7.20.6",
49 | "prettier": "^2.1.1",
50 | "typescript": "^3.9.7"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/scenes/Main/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 |
3 | import { diInject, Dependence } from 'common-components/HOC';
4 | import List from 'common-components/List';
5 | import ListModel from 'common-components/List/model/ListModel';
6 | import ListService from 'common-components/List/service/ListService';
7 | import Loader from 'common-components/Loader';
8 |
9 | import { IBook } from 'global-types/api';
10 |
11 | import Book from './components/Book';
12 | import { ListWrapper, Header } from './components/styled';
13 |
14 | import constants from './constants';
15 |
16 | import MainPageService from './service/MainPage';
17 |
18 | type Props = {
19 | booksListModel: ListModel;
20 | listService: ListService;
21 | service: MainPageService;
22 | };
23 |
24 | function Main({ listService, booksListModel }: Props) {
25 | const handleClick = useCallback(() => {
26 | listService.loadPage(booksListModel, booksListModel.pagination.page + 1);
27 | }, [listService, booksListModel]);
28 | console.log(booksListModel.selectedItem);
29 | if (booksListModel.isLoading) return ;
30 |
31 | return (
32 |
33 |
34 |
35 |
41 |
42 |
43 |
44 | );
45 | }
46 |
47 | const injected = diInject(Main, {
48 | booksListModel: new Dependence(constants.booksListModelName),
49 | listService: new Dependence(ListService),
50 | service: new Dependence(MainPageService),
51 | });
52 |
53 | export default injected;
54 |
--------------------------------------------------------------------------------
/src/components/List/model/Pagination.ts:
--------------------------------------------------------------------------------
1 | import { observable, action, computed } from 'mobx';
2 |
3 | class Pagination {
4 | @observable
5 | public totalCount?: number;
6 |
7 | @observable
8 | public page = 0;
9 |
10 | @computed
11 | public get visible() {
12 | return !!this.normalizedCount && this.normalizedCount > this.itemsPerPage;
13 | }
14 |
15 | @computed
16 | public get pagesCount() {
17 | if (this.normalizedCount) {
18 | const pages = this.normalizedCount / this.itemsPerPage;
19 | return pages < 1 ? 1 : Math.ceil(pages);
20 | }
21 | return 1;
22 | }
23 |
24 | @computed
25 | public get normalizedCount() {
26 | if (this.totalCount) {
27 | const itemsLength = this.itemsLengthGetter();
28 |
29 | if (itemsLength < this.itemsPerPage) {
30 | return itemsLength;
31 | }
32 |
33 | return this.totalCount;
34 | }
35 |
36 | return this.totalCount;
37 | }
38 |
39 | public padding = 1;
40 |
41 | public itemsPerPage = 3;
42 |
43 | public tableCountRequestId?: number;
44 |
45 | public itemsLengthGetter: () => number;
46 |
47 | @observable
48 | public isExactCount = false;
49 |
50 | public constructor(itemsLengthGetter: () => number) {
51 | this.itemsLengthGetter = itemsLengthGetter;
52 | }
53 |
54 | @action
55 | public changeTotalCount(value: number) {
56 | this.totalCount = value;
57 | }
58 |
59 | @action
60 | public setPage(value: number) {
61 | this.page = value;
62 | }
63 |
64 | @action.bound
65 | public clear() {
66 | this.page = 1;
67 | this.totalCount = undefined;
68 | this.tableCountRequestId = undefined;
69 | this.isExactCount = false;
70 | }
71 | }
72 |
73 | export default Pagination;
74 |
--------------------------------------------------------------------------------
/src/components/List/model/ListModel.ts:
--------------------------------------------------------------------------------
1 | import { injectable } from 'inversify';
2 | import { observable, action, computed } from 'mobx';
3 |
4 | import Pagination from './Pagination';
5 |
6 | @injectable()
7 | class ListModel {
8 | public static diKey = Symbol.for('ListModelKey');
9 |
10 | @observable
11 | public items: {
12 | [index: number]: T[];
13 | } = {};
14 |
15 | @observable
16 | public hoveredIndex = -1;
17 |
18 | @observable
19 | public highlightIndexes: number[] = [];
20 |
21 | @observable
22 | public selectedIndex: number | null = null;
23 |
24 | @observable
25 | public isLoading = false;
26 |
27 | public tableRequestId?: number;
28 |
29 | public pagination: Pagination;
30 |
31 | @computed
32 | public get pageItems() {
33 | return this.getPageItems(this.pagination.page);
34 | }
35 |
36 | @computed
37 | public get selectedItem() {
38 | const items = this.pageItems;
39 |
40 | return this.selectedIndex != null ? items[this.selectedIndex] : null;
41 | }
42 |
43 | @computed
44 | public get itemsLength() {
45 | let length = 0;
46 |
47 | Object.values(this.items).forEach((k) => {
48 | length += k.length;
49 | });
50 |
51 | return length;
52 | }
53 |
54 | @computed
55 | public get notFound() {
56 | return !this.pageItems.length && !this.isLoading;
57 | }
58 |
59 | public constructor() {
60 | this.pagination = new Pagination(() => this.itemsLength);
61 | }
62 |
63 | @action
64 | public setIsLoading(value: boolean) {
65 | this.isLoading = value;
66 | }
67 |
68 | public getPageItems(page: number) {
69 | return this.items[page] || [];
70 | }
71 |
72 | @action
73 | public setPageItems(items: T[], pageIndex: number) {
74 | this.items[pageIndex] = items;
75 | }
76 |
77 | @action
78 | public addHighlightIndex(index: number) {
79 | if (this.highlightIndexes.some((x) => x === index)) return;
80 |
81 | this.highlightIndexes.push(index);
82 | }
83 |
84 | @action
85 | public removeHighlightIndex(index: number) {
86 | const arrayIndex = this.highlightIndexes.findIndex((x) => x === index);
87 |
88 | if (arrayIndex > -1) {
89 | this.highlightIndexes.splice(arrayIndex, 1);
90 | }
91 | }
92 |
93 | @action
94 | public setSelectedIndex(index: number | null) {
95 | this.selectedIndex = index;
96 | }
97 |
98 | @action
99 | public setHoveredIndex(index: number) {
100 | this.hoveredIndex = index;
101 | }
102 |
103 | @action
104 | public clearIndexes() {
105 | this.highlightIndexes = [];
106 | this.selectedIndex = null;
107 | this.hoveredIndex = -1;
108 | }
109 |
110 | @action
111 | public clear() {
112 | this.items = {};
113 | this.hoveredIndex = -1;
114 | this.highlightIndexes = [];
115 | this.selectedIndex = null;
116 | this.isLoading = false;
117 |
118 | this.pagination.clear();
119 | }
120 | }
121 |
122 | export default ListModel;
123 |
--------------------------------------------------------------------------------
/src/components/HOC/Injector.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-classes-per-file */
2 | import React, { Component, JSXElementConstructor, ComponentClass } from 'react';
3 | import { observer } from 'mobx-react';
4 |
5 | import { getDisplayName } from 'utils/index';
6 |
7 | import Context from './Context';
8 |
9 | type DiKey = string | symbol;
10 |
11 | type Class = { new (...args: any[]): T; diKey: DiKey };
12 |
13 | type Options = {
14 | name?: symbol;
15 | tagKey?: symbol;
16 | tagValue?: symbol;
17 | all?: boolean;
18 | transformation?: (clazz: T) => any;
19 | };
20 |
21 | type InjectParams = {
22 | diKey: DiKey;
23 | options?: {
24 | name?: symbol;
25 | tagKey?: symbol;
26 | tagValue?: symbol;
27 | all?: boolean;
28 | };
29 | };
30 |
31 | export class Dependence {
32 | private clazzOrKey: Class | DiKey;
33 |
34 | private options?: Options;
35 |
36 | public constructor(clazzOrKey: Class | DiKey, options?: Options) {
37 | this.clazzOrKey = clazzOrKey;
38 | this.options = options;
39 | }
40 |
41 | public build() {
42 | return {
43 | clazzOrKey: this.clazzOrKey,
44 | options: this.options,
45 | };
46 | }
47 | }
48 |
49 | export function diInject(
50 | component: JSXElementConstructor
& C,
51 | dependencies: Record>
52 | ) {
53 | type Props = JSX.LibraryManagedAttributes>;
54 |
55 | const displayName = getDisplayName(component);
56 | const WrappedComponent = observer(component);
57 |
58 | class DiInjectClass extends Component {
59 | // eslint-disable-next-line react/static-property-placement
60 | public static contextType = Context;
61 |
62 | public static wrappedComponent = component;
63 |
64 | // eslint-disable-next-line react/static-property-placement
65 | public static displayName = `diInject(${displayName})`;
66 |
67 | private resolve = (inject: InjectParams) => {
68 | const opt = inject;
69 | const { context } = this;
70 |
71 | if (!opt.diKey) {
72 | throw new Error('There is no static diKey in model class');
73 | }
74 |
75 | if (!opt.options) {
76 | return context.get(opt.diKey);
77 | }
78 |
79 | if (
80 | (opt.options.tagKey && !opt.options.tagValue) ||
81 | (!opt.options.tagKey && opt.options.tagValue)
82 | ) {
83 | throw new Error(`tagKey or tagValue empty for ${displayName} `);
84 | }
85 |
86 | if (!opt.options.tagKey && !opt.options.name) {
87 | if (!opt.options.all) {
88 | return context.get(opt.diKey);
89 | }
90 | return context.getAll(opt.diKey);
91 | }
92 | if (opt.options.name) {
93 | if (!opt.options.all) {
94 | return context.getNamed(opt.diKey, opt.options.name);
95 | }
96 | return context.getAllNamed(opt.diKey, opt.options.name);
97 | }
98 | if (opt.options.tagKey && opt.options.tagValue) {
99 | if (!opt.options.all) {
100 | return context.getTagged(opt.diKey, opt.options.tagKey, opt.options.tagValue);
101 | }
102 | return context.getAllTagged(opt.diKey, opt.options.tagKey, opt.options.tagValue);
103 | }
104 |
105 | return context.get(opt.diKey);
106 | };
107 |
108 | private inject = () => {
109 | if (!this.context) {
110 | throw new Error(`di container not found for ${displayName}`);
111 | }
112 |
113 | const result: Record = {} as Record;
114 |
115 | (Object.keys(dependencies) as (keyof I)[]).forEach((key) => {
116 | const obj = dependencies[key];
117 |
118 | const deps = obj.build();
119 |
120 | const injectedParams: InjectParams = {
121 | diKey:
122 | typeof deps.clazzOrKey === 'symbol' || typeof deps.clazzOrKey === 'string'
123 | ? deps.clazzOrKey
124 | : deps.clazzOrKey.diKey,
125 | options: {
126 | name: deps.options?.name,
127 | tagKey: deps.options?.tagKey,
128 | tagValue: deps.options?.tagValue,
129 | all: deps.options?.all,
130 | },
131 | };
132 |
133 | const instance = this.resolve(injectedParams);
134 |
135 | result[key] =
136 | deps.options && deps.options.transformation
137 | ? deps.options.transformation(instance)
138 | : instance;
139 | });
140 |
141 | return result;
142 | };
143 |
144 | public render() {
145 | const injections = this.inject();
146 |
147 | return ;
148 | }
149 | }
150 |
151 | return observer(DiInjectClass) as ComponentClass & {
152 | wrappedComponent: JSXElementConstructor & C;
153 | };
154 | }
155 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [
3 | 'eslint:recommended',
4 | 'airbnb',
5 | 'prettier',
6 | 'prettier/react',
7 | 'prettier/standard',
8 | 'prettier/@typescript-eslint',
9 | ],
10 | parserOptions: {
11 | ecmaVersion: 2018,
12 | sourceType: 'module',
13 | ecmaFeatures: {
14 | impliedStrict: true,
15 | },
16 | },
17 | overrides: [
18 | {
19 | files: ['**/*.ts', '**/*.tsx'],
20 | parser: '@typescript-eslint/parser',
21 | parserOptions: {
22 | sourceType: 'module',
23 | project: './tsconfig.json',
24 | },
25 | plugins: ['prettier', 'react', 'jsx-a11y', 'import', '@typescript-eslint'],
26 | rules: {
27 | 'import/extensions': [0, 'never', { jsx: 'never', js: 'never' }],
28 | 'react/jsx-filename-extension': [2, { extensions: ['.tsx', '.ts'] }],
29 | 'react/jsx-wrap-multilines': [
30 | 2,
31 | {
32 | declaration: true,
33 | assignment: true,
34 | return: true,
35 | },
36 | ],
37 | 'import/order': [
38 | 'error',
39 | {
40 | groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
41 | 'newlines-between': 'always-and-inside-groups',
42 | },
43 | ],
44 | quotes: ['error', 'single', 'avoid-escape'],
45 | 'prettier/prettier': [2],
46 | 'import/no-cycle': [0],
47 | 'no-param-reassign': [0],
48 | 'react/prefer-stateless-function': [0],
49 | 'react/forbid-prop-types': [0],
50 | 'no-confusing-arrow': [0],
51 | 'no-mixed-operators': [0],
52 | 'consistent-return': [0],
53 | 'jsx-a11y/anchor-has-content': [0],
54 | 'class-methods-use-this': [0],
55 | 'no-console': [0],
56 | 'no-bitwise': [0],
57 | 'jsx-a11y/no-static-element-interactions': [0],
58 | 'jsx-a11y/no-autofocus': [0],
59 | 'linebreak-style': [0],
60 | 'jsx-a11y/img-has-alt': [0],
61 | 'jsx-a11y/anchor-is-valid': [0],
62 | 'jsx-a11y/no-noninteractive-element-interactions': [0],
63 | 'eol-last': [0],
64 | 'react/prop-types': [0],
65 | 'jsx-a11y/label-has-for': [0],
66 | 'jsx-a11y/click-events-have-key-events': [0],
67 | 'react/default-props-match-prop-types': [0],
68 | 'react/require-default-props': [0],
69 | 'react/no-unused-prop-types': [0],
70 | 'no-unused-vars': [0],
71 | 'no-undef': [0],
72 | 'import/no-extraneous-dependencies': [0],
73 | 'jsx-a11y/no-noninteractive-tabindex': [0],
74 | 'react/button-has-type': [0],
75 | 'getter-return': 'off',
76 | 'no-dupe-args': 'off',
77 | 'no-dupe-keys': 'off',
78 | 'no-unreachable': 'off',
79 | 'valid-typeof': 'off',
80 | 'no-const-assign': 'off',
81 | 'no-new-symbol': 'off',
82 | 'no-this-before-super': 'off',
83 | 'no-dupe-class-members': 'off',
84 | 'no-redeclare': 'off',
85 | 'import/prefer-default-export': 'off',
86 | '@typescript-eslint/adjacent-overload-signatures': 'error',
87 | '@typescript-eslint/array-type': 'error',
88 | '@typescript-eslint/ban-types': 'error',
89 | '@typescript-eslint/camelcase': 'off',
90 | '@typescript-eslint/class-name-casing': 'off',
91 | '@typescript-eslint/explicit-member-accessibility': 'error',
92 | '@typescript-eslint/interface-name-prefix': 'off',
93 | '@typescript-eslint/member-delimiter-style': 'error',
94 | '@typescript-eslint/no-angle-bracket-type-assertion': 'off',
95 | '@typescript-eslint/no-array-constructor': 'error',
96 | '@typescript-eslint/no-empty-interface': 'error',
97 | '@typescript-eslint/no-inferrable-types': 'error',
98 | '@typescript-eslint/no-parameter-properties': 'error',
99 | '@typescript-eslint/no-triple-slash-reference': 'off',
100 | '@typescript-eslint/no-unused-vars': 'warn',
101 | '@typescript-eslint/no-use-before-define': 'error',
102 | '@typescript-eslint/no-var-requires': 'error',
103 | 'react/jsx-props-no-spreading': 'off',
104 | },
105 | settings: {
106 | 'import/resolver': {
107 | node: {
108 | extensions: ['.js', '.jsx', '.ts', '.tsx'],
109 | },
110 | alias: {
111 | map: [
112 | ['common-components', './src/components'],
113 | ['pages', './pages'],
114 | ['services', './src/services'],
115 | ['main-scene', './src/scenes/Main'],
116 | ['stores', './src/stores'],
117 | ['global-styles', './src/styles'],
118 | ['global-types', './src/types'],
119 | ['utils', './src/utils'],
120 | ],
121 | extensions: ['.ts', '.js', '.tsx', '.json']
122 | }
123 | },
124 | },
125 | },
126 | ],
127 | };
--------------------------------------------------------------------------------