├── .eslintrc.json
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── dependabot.yml
└── pull_request_template.md
├── .gitignore
├── .prettierrc.json
├── .travis.yml
├── CODE_OF_CONDUCT.md
├── Dockerfile
├── LICENSE
├── README.md
├── assets
├── README-root
│ ├── filepath-after.png
│ ├── filepath-before.png
│ ├── test-output.png
│ ├── ultratrimmedDemo.gif
│ ├── zustand-test-filepath-1.png
│ ├── zustand-test-filepath-2.png
│ └── zustand-test-snapshot-2.png
└── logo
│ └── Chromogen.png
├── demo-todo
├── .babelrc
├── LICENSE
├── README.md
├── __tests__
│ └── initialTestTest.js
├── package-lock.json
├── package.json
├── src
│ ├── components
│ │ ├── App.jsx
│ │ ├── Quotes.jsx
│ │ ├── ReadOnlyTodoItem.jsx
│ │ ├── SearchBar.jsx
│ │ ├── TodoItem.jsx
│ │ ├── TodoItemCreator.jsx
│ │ ├── TodoList.jsx
│ │ ├── TodoListFilters.jsx
│ │ └── TodoQuickCheck.jsx
│ ├── favicon.ico
│ ├── index.html
│ ├── index.js
│ ├── store
│ │ ├── atoms.js
│ │ └── store.js
│ └── styles
│ │ └── styles.css
└── webpack.config.js
├── demo-zustand-todo
├── .babelrc
├── LICENSE
├── __tests__
│ └── sampleTest.js
├── chromogen-4.0.4.tgz
├── package.json
├── src
│ ├── components
│ │ ├── App.jsx
│ │ ├── Quotes.jsx
│ │ ├── ReadOnlyTodoItem.jsx
│ │ ├── SearchBar.jsx
│ │ ├── TodoItem.jsx
│ │ ├── TodoItemCreator.jsx
│ │ ├── TodoList.jsx
│ │ ├── TodoListFilters.jsx
│ │ └── TodoQuickCheck.jsx
│ ├── favicon.ico
│ ├── index.html
│ ├── index.js
│ ├── store
│ │ └── store.js
│ └── styles
│ │ └── styles.css
└── webpack.config.js
├── jenkins
├── Jenkinsfile
└── scripts
│ ├── deliver.sh
│ ├── kill.sh
│ └── test.sh
├── package-lock.json
├── package.json
└── package
├── LICENSE
├── README.md
├── babel.config.js
├── index.ts
├── package-lock.json
├── package.json
├── recoil_generator
├── __tests__
│ ├── api.test.js
│ ├── component-utils.test.js
│ ├── component.test.js
│ ├── core-utils.test.jx
│ ├── output-utils.test.js
│ ├── output.test.js
│ └── utils.test.js
└── src
│ ├── api
│ ├── api.ts
│ ├── core-utils.ts
│ └── family-utils.ts
│ ├── component
│ ├── ChromogenObserver.tsx
│ └── component-utils.ts
│ ├── output
│ ├── output-utils.ts
│ └── output.ts
│ ├── types.ts
│ └── utils
│ ├── ledger.ts
│ ├── store.ts
│ └── utils.ts
├── tsconfig.json
└── zustand_generator
├── __tests__
├── api.test.js
└── output-utils.test.js
└── src
├── GlobalStyle.ts
├── api
└── api.ts
├── component
├── Buttons
│ ├── RecordingButton.tsx
│ ├── RecordingVariations
│ │ ├── Record.tsx
│ │ └── Start.tsx
│ └── SecondaryButton.tsx
├── ChromogenZustandObserver.tsx
├── Editor.tsx
├── EditorTab.tsx
├── Header.tsx
├── Icons.tsx
├── Numbers.tsx
├── Resizing
│ └── Resizer.tsx
├── component-utils.ts
└── panel.tsx
├── output
├── output-utils.ts
└── output.ts
├── types.ts
└── utils
├── ledger.ts
├── store.ts
└── utils.ts
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "env": {
5 | "browser": true,
6 | "es2020": true,
7 | "jest": true
8 | },
9 | "globals": {
10 | "chrome": "readonly"
11 | },
12 | "extends": [
13 | "airbnb",
14 | // added below for npm install -D eslint-config-airbnb-typescript --legacy-peer-deps
15 | "airbnb-typescript",
16 | "prettier",
17 | "prettier/react"
18 | ],
19 | "plugins": ["prettier"],
20 | "parserOptions": {
21 | "ecmaVersion": 2022,
22 | "sourceType": "module",
23 | "ecmaFeatures": {
24 | "jsx": true
25 | },
26 | "project": "./package/tsconfig.json",
27 | "tsconfigRootDir": "__dirname"
28 | },
29 | "settings": {
30 | "import/extensions": [
31 | ".js",
32 | ".jsx",
33 | ".ts",
34 | ".tsx"
35 | ],
36 | "import/parsers": {
37 | "@typescript-eslint/parser": [
38 | ".ts",
39 | ".tsx"
40 | ]
41 | },
42 | "import/resolver": {
43 | "typescript": {
44 | "directory": "./package/tsconfig.json"
45 | },
46 | "node": {
47 | "extensions": [
48 | ".js",
49 | ".jsx",
50 | ".ts",
51 | ".tsx"
52 | ]
53 | }
54 | },
55 | "rules": {
56 | "prettier/prettier": ["warn"],
57 | "arrow-body-style": ["error", "as-needed"],
58 | "default-case-last": "error",
59 | "default-param-last": ["error"],
60 | "func-style": ["off", "expression"],
61 | "no-constant-condition": "error",
62 | "no-useless-call": "error",
63 | "prefer-exponentiation-operator": "error",
64 | "prefer-regex-literals": "error",
65 | "quotes": [
66 | "error",
67 | "single",
68 | {
69 | "avoidEscape": true,
70 | "allowTemplateLiterals": false
71 | }
72 | ],
73 | "import/prefer-default-export": "off",
74 | "import/extensions": [
75 | "error",
76 | "ignorePackages",
77 | {
78 | "js": "never",
79 | "jsx": "never",
80 | "ts": "never",
81 | "tsx": "never"
82 | }
83 | ],
84 | "react/jsx-filename-extension": ["off"],
85 | "react/function-component-definition": [
86 | "error",
87 | {
88 | "namedComponents": "arrow-function",
89 | "unnamedComponents": "arrow-function"
90 | }
91 | ],
92 | "react/jsx-handler-names": [
93 | "error",
94 | {
95 | "eventHandlerPrefix": "handle",
96 | "eventHandlerPropPrefix": "on"
97 | }
98 | ],
99 | "react/jsx-key": "error",
100 | "react/jsx-no-useless-fragment": "error",
101 | "react/jsx-sort-props": [
102 | "error",
103 | {
104 | "callbacksLast": true,
105 | "shorthandFirst": true,
106 | "shorthandLast": false,
107 | "ignoreCase": true,
108 | "noSortAlphabetically": false,
109 | "reservedFirst": true
110 | }
111 | ],
112 | "react/no-adjacent-inline-elements": "error",
113 | "react/no-direct-mutation-state": "error",
114 | "react/no-multi-comp": "error",
115 | "react/prop-types": [
116 | "error",
117 | { "skipUndeclared": true }
118 | ]
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "npm"
4 | directory: "/package"
5 | schedule:
6 | interval: "weekly"
7 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## Types of changes
2 |
3 | - [ ] Bugfix (change which fixes an issue)
4 | - [ ] New feature (change which adds functionality)
5 | - [ ] Refactor (change which changes the codebase without affecting its external behavior)
6 | - [ ] Non-breaking change (fix or feature that would causes existing functionality to work as expected)
7 | - [ ] Breaking change (fix or feature that would cause existing functionality to __not__ work as expected)
8 | ## Purpose
9 |
10 | ## Approach
11 |
12 | ## Resources
13 |
14 | ## Screenshot(s)
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # npm
2 | node_modules
3 |
4 | # System files
5 | .DS_Store
6 | .vscode
7 |
8 | # Tests
9 | coverage
10 |
11 | # Package
12 | package/build
13 |
14 | # Demo App
15 | chromogen.test.js
16 | package-lock.json
17 | TODO.md
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": true,
6 | "singleQuote": true,
7 | "quoteProps": "as-needed",
8 | "jsxSingleQuote": false,
9 | "trailingComma": "all",
10 | "bracketSpacing": true,
11 | "jsxBracketSameLine": false,
12 | "arrowParens": "always",
13 | "endOfLine": "lf"
14 | }
15 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 14
4 | env:
5 | - TEST_DIR=package
6 | before_install:
7 | - cd $TEST_DIR
8 | install:
9 | - npm install
10 | script:
11 | - npm run test
12 | - npm run coveralls
13 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at chromogen.app@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM jenkins/jenkins:2.375.3
2 | USER root
3 | RUN apt-get update && apt-get install -y lsb-release
4 | RUN curl -fsSLo /usr/share/keyrings/docker-archive-keyring.asc \
5 | https://download.docker.com/linux/debian/gpg
6 | RUN echo "deb [arch=$(dpkg --print-architecture) \
7 | signed-by=/usr/share/keyrings/docker-archive-keyring.asc] \
8 | https://download.docker.com/linux/debian \
9 | $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list
10 | RUN apt-get update && apt-get install -y docker-ce-cli
11 | USER jenkins
12 | RUN jenkins-plugin-cli --plugins "blueocean docker-workflow"
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 OSLabs Beta
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/assets/README-root/filepath-after.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/open-source-labs/Chromogen/6b2085e43c65c20a90916b22255b2f1606411682/assets/README-root/filepath-after.png
--------------------------------------------------------------------------------
/assets/README-root/filepath-before.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/open-source-labs/Chromogen/6b2085e43c65c20a90916b22255b2f1606411682/assets/README-root/filepath-before.png
--------------------------------------------------------------------------------
/assets/README-root/test-output.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/open-source-labs/Chromogen/6b2085e43c65c20a90916b22255b2f1606411682/assets/README-root/test-output.png
--------------------------------------------------------------------------------
/assets/README-root/ultratrimmedDemo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/open-source-labs/Chromogen/6b2085e43c65c20a90916b22255b2f1606411682/assets/README-root/ultratrimmedDemo.gif
--------------------------------------------------------------------------------
/assets/README-root/zustand-test-filepath-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/open-source-labs/Chromogen/6b2085e43c65c20a90916b22255b2f1606411682/assets/README-root/zustand-test-filepath-1.png
--------------------------------------------------------------------------------
/assets/README-root/zustand-test-filepath-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/open-source-labs/Chromogen/6b2085e43c65c20a90916b22255b2f1606411682/assets/README-root/zustand-test-filepath-2.png
--------------------------------------------------------------------------------
/assets/README-root/zustand-test-snapshot-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/open-source-labs/Chromogen/6b2085e43c65c20a90916b22255b2f1606411682/assets/README-root/zustand-test-snapshot-2.png
--------------------------------------------------------------------------------
/assets/logo/Chromogen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/open-source-labs/Chromogen/6b2085e43c65c20a90916b22255b2f1606411682/assets/logo/Chromogen.png
--------------------------------------------------------------------------------
/demo-todo/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["@babel/preset-env"],
4 | "@babel/preset-react"
5 | ],
6 | "plugins": [
7 | //"react-hot-loader/babel"
8 | ]
9 | }
--------------------------------------------------------------------------------
/demo-todo/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Michelle Holland
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/demo-todo/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # The official demo app for [Chromogen](https://github.com/oslabs-beta/Chromogen).
4 |
5 | 
6 |
7 |
8 |
9 | ## Selector Implementations
10 | - Readonly:
11 | 1. displayed todo list items, based on filter selection (sort & active/complete)
12 | 2. stats (priority count and active/complete counts)
13 | 3. displayed todo list empty / non-empty
14 | - Writeable:
15 | 1. "all complete" checkbox toggle
16 | 1. reset filter states
17 | - Promise:
18 | 1. quote text
19 | - Async / Await:
20 | 1. xkcd comic
21 | - selectorFamily (_in progress_):
22 | 1. search bar
--------------------------------------------------------------------------------
/demo-todo/__tests__/initialTestTest.js:
--------------------------------------------------------------------------------
1 | import { renderRecoilHook, act } from 'react-recoil-hooks-testing-library';
2 | import { useRecoilValue, useRecoilState } from 'recoil';
3 | import {
4 | filteredTodoListState,
5 | sortedTodoListState,
6 | todoListSortedStats,
7 | todoListStatsState,
8 | filteredListContentState,
9 | allCompleteState,
10 | refreshFilterState,
11 | searchBarSelectorFam,
12 |
13 | } from '../src/store/store';
14 | import {
15 | todoListState,
16 | todoListFilterState,
17 | todoListSortState,
18 | quoteNumberState,
19 | searchResultState,
20 |
21 | } from '../src/store/atoms';
22 |
23 | // Suppress 'Batcher' warnings from React / Recoil conflict
24 | console.error = jest.fn();
25 |
26 | // Hook to return atom/selector values and/or modifiers for react-recoil-hooks-testing-library
27 | const useStoreHook = () => {
28 | // atoms
29 | const [todoListStateValue, settodoListState] = useRecoilState(todoListState);
30 | const [todoListFilterStateValue, settodoListFilterState] = useRecoilState(todoListFilterState);
31 | const [todoListSortStateValue, settodoListSortState] = useRecoilState(todoListSortState);
32 | const [quoteNumberStateValue, setquoteNumberState] = useRecoilState(quoteNumberState);
33 | const [searchResultStateValue, setsearchResultState] = useRecoilState(searchResultState);
34 |
35 | // writeable selectors
36 | const [allCompleteStateValue, setallCompleteState] = useRecoilState(allCompleteState);
37 | const [refreshFilterStateValue, setrefreshFilterState] = useRecoilState(refreshFilterState);
38 |
39 | // read-only selectors
40 | const filteredTodoListStateValue = useRecoilValue(filteredTodoListState);
41 | const sortedTodoListStateValue = useRecoilValue(sortedTodoListState);
42 | const todoListSortedStatsValue = useRecoilValue(todoListSortedStats);
43 | const todoListStatsStateValue = useRecoilValue(todoListStatsState);
44 | const filteredListContentStateValue = useRecoilValue(filteredListContentState);
45 |
46 | // atom families
47 |
48 | // writeable selector families
49 |
50 | // read-only selector families
51 |
52 |
53 |
54 |
55 | return {
56 | todoListStateValue,
57 | settodoListState,
58 | todoListFilterStateValue,
59 | settodoListFilterState,
60 | todoListSortStateValue,
61 | settodoListSortState,
62 | quoteNumberStateValue,
63 | setquoteNumberState,
64 | searchResultStateValue,
65 | setsearchResultState,
66 | allCompleteStateValue,
67 | setallCompleteState,
68 | refreshFilterStateValue,
69 | setrefreshFilterState,
70 | filteredTodoListStateValue,
71 | sortedTodoListStateValue,
72 | todoListSortedStatsValue,
73 | todoListStatsStateValue,
74 | filteredListContentStateValue,
75 | };
76 | };
77 |
78 | describe('INITIAL RENDER', () => {
79 | const { result } = renderRecoilHook(useStoreHook);
80 |
81 | it('filteredTodoListState should initialize correctly', () => {
82 | expect(result.current.filteredTodoListStateValue).toStrictEqual([]);
83 | });
84 |
85 | it('sortedTodoListState should initialize correctly', () => {
86 | expect(result.current.sortedTodoListStateValue).toStrictEqual([]);
87 | });
88 |
89 | it('allCompleteState should initialize correctly', () => {
90 | expect(result.current.allCompleteStateValue).toStrictEqual(true);
91 | });
92 |
93 | it('filteredListContentState should initialize correctly', () => {
94 | expect(result.current.filteredListContentStateValue).toStrictEqual(false);
95 | });
96 |
97 | it('todoListSortedStats should initialize correctly', () => {
98 | expect(result.current.todoListSortedStatsValue).toStrictEqual({});
99 | });
100 |
101 | it('todoListStatsState should initialize correctly', () => {
102 | expect(result.current.todoListStatsStateValue).toStrictEqual({ "totalNum": 0, "totalCompletedNum": 0, "totalUncompletedNum": 0, "percentCompleted": 0 });
103 | });
104 |
105 |
106 | });
107 |
108 | describe('SELECTORS', () => {
109 | it('todoListSortedStats should properly derive state when todoListState updates', () => {
110 | const { result } = renderRecoilHook(useStoreHook);
111 |
112 | act(() => {
113 | result.current.settodoListState([{ "id": 1, "text": "tennis", "priority": "low", "isComplete": false }]);
114 |
115 | result.current.settodoListFilterState("Show All");
116 |
117 | result.current.settodoListSortState(false);
118 |
119 | result.current.setquoteNumberState(23);
120 |
121 | result.current.setsearchResultState({ "all": { "searchTerm": "", "results": [] }, "high": { "searchTerm": "", "results": [] }, "medium": { "searchTerm": "", "results": [] }, "low": { "searchTerm": "", "results": [] } });
122 |
123 |
124 |
125 | });
126 | expect(result.current.todoListSortedStatsValue).toStrictEqual({ "low": 1 });
127 |
128 | });
129 |
130 | it('todoListSortedStats should properly derive state when todoListState updates', () => {
131 | const { result } = renderRecoilHook(useStoreHook);
132 |
133 | act(() => {
134 | result.current.settodoListState([{ "id": 1, "text": "tennis", "priority": "low", "isComplete": false }, { "id": 2, "text": "chinese chicken", "priority": "low", "isComplete": false }]);
135 |
136 | result.current.settodoListFilterState("Show All");
137 |
138 | result.current.settodoListSortState(false);
139 |
140 | result.current.setquoteNumberState(23);
141 |
142 | result.current.setsearchResultState({ "all": { "searchTerm": "", "results": [] }, "high": { "searchTerm": "", "results": [] }, "medium": { "searchTerm": "", "results": [] }, "low": { "searchTerm": "", "results": [] } });
143 |
144 |
145 |
146 | });
147 | expect(result.current.todoListSortedStatsValue).toStrictEqual({ "low": 2 });
148 |
149 | });
150 |
151 | });
152 |
153 | describe('SETTERS', () => {
154 | });
--------------------------------------------------------------------------------
/demo-todo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chromogen-todo",
3 | "version": "1.0.0",
4 | "description": "demo todo app for Chromogen using React + Recoil",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "webpack-dev-server --open",
8 | "test": "NODE_OPTIONS=--experimental-vm-modules npx jest --verbose"
9 | },
10 | "keywords": [
11 | "react",
12 | "recoil",
13 | "chromogen",
14 | "demo",
15 | "example",
16 | "todo"
17 | ],
18 | "repository": {
19 | "type": "git",
20 | "url": "https://github.com/open-source-labs/Chromogen.git"
21 | },
22 | "contributors": [
23 | {
24 | "name": "Michelle Holland",
25 | "url": "https://github.com/michellebholland/"
26 | },
27 | {
28 | "name": "Jim Chen",
29 | "url": "https://github.com/chenchingk"
30 | },
31 | {
32 | "name": "Andy Wang",
33 | "url": "https://github.com/andywang23"
34 | },
35 | {
36 | "name": "Connor Rose Delisle",
37 | "url": "https://github.com/connorrose"
38 | }
39 | ],
40 | "license": "MIT",
41 | "devDependencies": {
42 | "@babel/core": "^7.11.1",
43 | "@babel/plugin-transform-runtime": "^7.11.0",
44 | "@babel/preset-env": "^7.19.1",
45 | "@babel/preset-react": "^7.10.4",
46 | "@testing-library/react": "^13.1.1",
47 | "babel-loader": "^8.1.0",
48 | "chromogen": "^4.0.4",
49 | "css-loader": "^4.2.1",
50 | "identity-obj-proxy": "^3.0.0",
51 | "jest": "^26.4.2",
52 | "style-loader": "^1.2.1",
53 | "webpack": "^5.74.0",
54 | "webpack-cli": "^3.3.12",
55 | "webpack-dev-server": "^4.11.1"
56 | },
57 | "peerDependencies": {
58 | "typescript": "^4.0.3"
59 | },
60 | "dependencies": {
61 | "@babel/runtime": "^7.11.2",
62 | "@emotion/react": "^11.10.4",
63 | "@emotion/styled": "^11.10.4",
64 | "@mui/icons-material": "^5.10.6",
65 | "@mui/material": "^5.10.6",
66 | "babel-jest": "^26.3.0",
67 | "react": "^18.0.0",
68 | "react-dom": "^18.0.0",
69 | "react-recoil-hooks-testing-library": "^0.1.0",
70 | "react-test-renderer": "^18.1.0",
71 | "recoil": "0.7.2",
72 | "typescript": "^4.0.3"
73 | },
74 | "jest": {
75 | "moduleNameMapper": {
76 | "\\.(css|less)$": "identity-obj-proxy"
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/demo-todo/src/components/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { RecoilRoot } from 'recoil';
3 | import { ChromogenObserver } from 'chromogen';
4 | import TodoList from './TodoList';
5 | import * as selectors from '../store/store';
6 | import * as atoms from '../store/atoms';
7 |
8 | const App = () => (
9 |
10 |
11 |
12 |
13 | );
14 |
15 | export default App;
16 |
--------------------------------------------------------------------------------
/demo-todo/src/components/Quotes.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useRecoilValue, useSetRecoilState } from 'recoil';
3 | import { quoteTextState, xkcdState } from '../store/store';
4 | import { quoteNumberState } from '../store/atoms';
5 |
6 | const Quotes = () => {
7 | const setQuoteNumber = useSetRecoilState(quoteNumberState);
8 | const quoteText = useRecoilValue(quoteTextState);
9 |
10 | return (
11 | <>
12 |
13 |
{quoteText}
14 |
setQuoteNumber(Math.floor(Math.random() * 1643))}>
15 | New Quote
16 |
17 |
18 | >
19 | );
20 | };
21 |
22 | export default Quotes;
23 |
--------------------------------------------------------------------------------
/demo-todo/src/components/ReadOnlyTodoItem.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Checkbox from '@mui/material/Checkbox';
3 | import '../styles/styles.css';
4 | import { todoListState } from '../store/atoms';
5 | import { useRecoilValue } from 'recoil';
6 |
7 | const ReadOnlyTodoItem = ({ item }) => {
8 | const checkBoxClasses = {
9 | low: 'lowPriority',
10 | medium: 'mediumPriority',
11 | high: 'highPriority',
12 | };
13 |
14 | const todoList = useRecoilValue(todoListState);
15 |
16 | return todoList.find((todo) => todo.id === item.id) ? (
17 |
18 |
19 | todo.id === item.id).isComplete}
22 | color="default"
23 | inputProps={{ 'aria-label': 'primary checkbox' }}
24 | style={{ cursor: 'default' }}
25 | />
26 |
27 | ) : null;
28 | };
29 | export default ReadOnlyTodoItem;
30 |
--------------------------------------------------------------------------------
/demo-todo/src/components/SearchBar.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useRecoilState } from 'recoil';
3 | import { searchBarSelectorFam } from '../store/store';
4 |
5 | import ReadOnlyTodoItem from './ReadOnlyTodoItem';
6 |
7 | const SearchBar = () => {
8 | const [searchFilter, setSearchFilter] = useState('all');
9 | const [searchText, setSearchText] = useState('');
10 | const [searchState, setSearchState] = useRecoilState(searchBarSelectorFam(searchFilter));
11 |
12 | const onSearchTextChange = (e) => {
13 | setSearchText(e.target.value);
14 | setSearchState(e.target.value);
15 | };
16 | const onSelectChange = (e) => {
17 | setSearchText('');
18 | setSearchFilter(e.target.value);
19 | };
20 |
21 | return (
22 |
23 |
31 |
32 | All Priorities
33 | High Priority
34 | Medium Priority
35 | Low Priority
36 |
37 |
38 | {searchState.results.map((result, idx) => (
39 |
40 | ))}
41 |
42 |
43 | );
44 | };
45 |
46 | export default SearchBar;
47 |
--------------------------------------------------------------------------------
/demo-todo/src/components/TodoItem.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useRecoilState } from 'recoil';
3 | import Checkbox from '@mui/material/Checkbox';
4 | import { todoListState } from '../store/atoms';
5 | import '../styles/styles.css';
6 |
7 | function replaceItemAtIndex(arr, index, newValue) {
8 | return [...arr.slice(0, index), newValue, ...arr.slice(index + 1)];
9 | }
10 |
11 | function removeItemAtIndex(arr, index) {
12 | return [...arr.slice(0, index), ...arr.slice(index + 1)];
13 | }
14 |
15 | const TodoItem = ({ item }) => {
16 | const [todoList, setTodoList] = useRecoilState(todoListState);
17 | const index = todoList.findIndex((listItem) => listItem === item);
18 |
19 | const editItemText = ({ target: { value } }) => {
20 | const newList = replaceItemAtIndex(todoList, index, {
21 | ...item,
22 | text: value,
23 | });
24 | setTodoList(newList);
25 | };
26 | const toggleItemCompletion = () => {
27 | const newList = replaceItemAtIndex(todoList, index, {
28 | ...item,
29 | isComplete: !item.isComplete,
30 | });
31 | setTodoList(newList);
32 | };
33 | const deleteItem = () => {
34 | const newList = removeItemAtIndex(todoList, index);
35 | setTodoList(newList);
36 | };
37 |
38 | const checkBoxClasses = {
39 | low: 'lowPriority',
40 | medium: 'mediumPriority',
41 | high: 'highPriority',
42 | };
43 |
44 | return (
45 |
46 |
47 |
54 |
55 | X
56 |
57 |
58 | );
59 | };
60 | export default TodoItem;
61 |
--------------------------------------------------------------------------------
/demo-todo/src/components/TodoItemCreator.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/jsx-props-no-spreading */
2 | import React, { useState } from 'react';
3 | import RadioGroup from '@mui/material/RadioGroup';
4 | import FormControlLabel from '@mui/material/FormControlLabel';
5 | import FormControl from '@mui/material/FormControl';
6 | import FormLabel from '@mui/material/FormLabel';
7 | import Radio from '@mui/material/Radio';
8 | import { useSetRecoilState } from 'recoil';
9 | import { todoListState } from '../store/atoms';
10 |
11 | // utility for creating unique Id
12 | let id = 0;
13 | const getId = () => {
14 | id += 1;
15 | return id;
16 | };
17 |
18 | const TodoItemCreator = () => {
19 | const [inputValue, setInputValue] = useState('');
20 | const [priorityValue, setPriorityValue] = useState('low');
21 | const setTodoList = useSetRecoilState(todoListState);
22 |
23 | const addItem = () => {
24 | setTodoList((oldTodoList) => [
25 | ...oldTodoList,
26 | {
27 | id: getId(),
28 | text: inputValue,
29 | priority: priorityValue,
30 | isComplete: false,
31 | },
32 | ]);
33 | setInputValue('');
34 | setPriorityValue('low');
35 | };
36 |
37 | const onChange = ({ target: { value } }) => {
38 | setInputValue(value);
39 | };
40 |
41 | const handleChange = (event) => {
42 | setPriorityValue(event.target.value);
43 | };
44 |
45 | /* MUI Radio Button styles */
46 | const GreenRadio = (props) => ;
47 |
48 | const YellowRadio = (props) => ;
49 |
50 | const RedRadio = (props) => ;
51 |
52 | return (
53 |
54 |
61 |
62 |
63 |
64 |
71 | } value="high" />
72 | } value="medium" />
73 | } value="low" />
74 |
75 |
76 |
77 |
78 |
79 | Add
80 |
81 |
82 | );
83 | };
84 |
85 | export default TodoItemCreator;
86 |
--------------------------------------------------------------------------------
/demo-todo/src/components/TodoList.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useRecoilValue } from 'recoil';
3 | import { sortedTodoListState } from '../store/store';
4 | import TodoItem from './TodoItem';
5 | import TodoItemCreator from './TodoItemCreator';
6 | import TodoListFilters from './TodoListFilters';
7 | import TodoQuickCheck from './TodoQuickCheck';
8 | import Quotes from './Quotes';
9 | import SearchBar from './SearchBar';
10 | import '../styles/styles.css';
11 |
12 | const TodoList = () => {
13 | const todoList = useRecoilValue(sortedTodoListState);
14 |
15 | return (
16 |
17 |
18 | Loading...}>
19 |
20 |
21 |
22 |
23 |
24 |
Totally Todos!
25 |
26 |
27 |
28 |
29 | {todoList.map((todoItem) => (
30 |
31 | ))}
32 |
33 |
34 |
35 |
36 |
37 |
38 | );
39 | };
40 |
41 | export default TodoList;
42 |
--------------------------------------------------------------------------------
/demo-todo/src/components/TodoListFilters.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import SortIcon from '@mui/icons-material/Sort';
3 | import EqualizerIcon from '@mui/icons-material/Equalizer';
4 | import RefreshIcon from '@mui/icons-material/Refresh';
5 | import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil';
6 | import { todoListStatsState, todoListSortedStats, refreshFilterState } from '../store/store';
7 | import { todoListFilterState, todoListSortState } from '../store/atoms';
8 |
9 | const TodoListFilters = () => {
10 | const [filter, setFilter] = useRecoilState(todoListFilterState);
11 | // selector - grabs totals for each category
12 | const { high, medium, low } = useRecoilValue(todoListSortedStats);
13 | // selector *writeable - resets sort and filter
14 | const resetFilters = useResetRecoilState(refreshFilterState);
15 | // selector - toggles sort on and off
16 | const [sort, setSort] = useRecoilState(todoListSortState);
17 | // toggle priority stats display
18 | const [displayStats, setDisplayStats] = useState(false);
19 | // selector - totals for each filter
20 | const { totalNum, totalCompletedNum, totalUncompletedNum } = useRecoilValue(todoListStatsState);
21 | const updateFilter = ({ target: { value } }) => setFilter(value);
22 |
23 | const toggleSort = () => setSort(!sort);
24 | const toggleDisplayStats = () => setDisplayStats(!displayStats);
25 | const reset = () => {
26 | setDisplayStats(false); // displayStats is local state
27 | resetFilters();
28 | };
29 |
30 | const sortIconColor = {
31 | true: 'sortedWhite',
32 | false: 'aqua',
33 | };
34 |
35 | return (
36 |
37 |
45 | All {totalNum || ''}
46 |
47 |
55 | Active {totalUncompletedNum || ''}
56 |
57 |
64 | Complete {totalCompletedNum || ''}
65 |
66 |
67 |
68 |
69 |
70 |
71 | {displayStats && totalNum ? (
72 |
73 | {high || 0}
74 | {medium || 0}
75 | {low || 0}
76 |
77 | ) : (
78 |
79 | )}
80 |
81 |
82 |
83 |
84 |
85 | );
86 | };
87 |
88 | export default TodoListFilters;
89 |
--------------------------------------------------------------------------------
/demo-todo/src/components/TodoQuickCheck.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useRecoilState, useRecoilValue } from 'recoil';
3 | import Checkbox from '@mui/material/Checkbox';
4 | import { allCompleteState, filteredListContentState } from '../store/store';
5 |
6 | const TodoQuickCheck = () => {
7 | const [allComplete, setAllComplete] = useRecoilState(allCompleteState);
8 | const display = useRecoilValue(filteredListContentState);
9 |
10 | return (
11 | display && (
12 |
13 | setAllComplete(!allComplete)}
19 | />
20 | all
21 |
22 | )
23 | );
24 | };
25 |
26 | export default TodoQuickCheck;
27 |
--------------------------------------------------------------------------------
/demo-todo/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/open-source-labs/Chromogen/6b2085e43c65c20a90916b22255b2f1606411682/demo-todo/src/favicon.ico
--------------------------------------------------------------------------------
/demo-todo/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 | Chromogen To-Do Demo
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/demo-todo/src/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/jsx-filename-extension */
2 | import React from 'react';
3 | //import { render } from 'react-dom';
4 | import App from './components/App';
5 | import { createRoot } from'react-dom/client';
6 |
7 | const root = createRoot(document.getElementById('app'));
8 |
9 | root.render(
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/demo-todo/src/store/atoms.js:
--------------------------------------------------------------------------------
1 | import { atom } from 'chromogen';
2 |
3 | /* ----- ATOMS ----- */
4 |
5 | // unsorted, unfiltered todo list
6 | const todoListState = atom({
7 | key: 'mismatchTodoList',
8 | default: [], // array of objects - each object has id, text, isComplete, and priority props
9 | });
10 |
11 | // filter select
12 | const todoListFilterState = atom({
13 | key: 'todoListFilterState',
14 | default: 'Show All',
15 | });
16 |
17 | // toggle sort
18 | const todoListSortState = atom({
19 | key: 'todoListSortState',
20 | default: false,
21 | });
22 |
23 | // random number for fetching quote & comic
24 | const quoteNumberState = atom({
25 | key: 'quoteNumberState',
26 | default: Math.floor(Math.random() * 1643),
27 | });
28 |
29 | const searchResultState = atom({
30 | key: 'searchResultState',
31 | default: {
32 | all: {
33 | searchTerm: '',
34 | results: [],
35 | },
36 | high: {
37 | searchTerm: '',
38 | results: [],
39 | },
40 | medium: {
41 | searchTerm: '',
42 | results: [],
43 | },
44 | low: {
45 | searchTerm: '',
46 | results: [],
47 | },
48 | },
49 | });
50 |
51 | export {
52 | todoListState,
53 | todoListFilterState,
54 | todoListSortState,
55 | quoteNumberState,
56 | searchResultState,
57 | };
58 |
--------------------------------------------------------------------------------
/demo-todo/src/store/store.js:
--------------------------------------------------------------------------------
1 | import { selector, selectorFamily } from 'chromogen';
2 | import {
3 | todoListState,
4 | todoListFilterState,
5 | todoListSortState,
6 | quoteNumberState,
7 | searchResultState,
8 | } from './atoms';
9 |
10 | /* ----- SELECTORS ---- */
11 |
12 | // filtered todo list
13 | const filteredTodoListState = selector({
14 | key: 'filteredTodoListState',
15 | get: ({ get }) => {
16 | const filter = get(todoListFilterState);
17 | const list = get(todoListState);
18 |
19 | switch (filter) {
20 | case 'Show Completed':
21 | return list.filter((item) => item.isComplete);
22 | case 'Show Uncompleted':
23 | return list.filter((item) => !item.isComplete);
24 | default:
25 | return list;
26 | }
27 | },
28 | });
29 |
30 | // sorted todo list
31 | const sortedTodoListState = selector({
32 | key: 'mismatchSortedTodoList',
33 | get: ({ get }) => {
34 | const sort = get(todoListSortState);
35 | const list = get(filteredTodoListState);
36 | const high = list.filter((item) => item.priority === 'high');
37 | const medium = list.filter((item) => item.priority === 'medium');
38 | const low = list.filter((item) => item.priority === 'low');
39 | return sort === false ? list : [...high, ...medium, ...low];
40 | },
41 | });
42 |
43 | // priority stats
44 | const todoListSortedStats = selector({
45 | key: 'todoListSortedStats',
46 | get: ({ get }) => {
47 | const list = get(sortedTodoListState);
48 | return list.reduce((acc, cv) => {
49 | acc[cv.priority] = cv.priority in acc ? acc[cv.priority] + 1 : 1;
50 | return acc;
51 | }, {});
52 | },
53 | });
54 |
55 | // completion (filter) stats
56 | const todoListStatsState = selector({
57 | key: 'todoListStatsState',
58 | get: ({ get }) => {
59 | const list = get(todoListState);
60 | const totalNum = list.length;
61 | const totalCompletedNum = list.filter((todo) => todo.isComplete).length;
62 | const totalUncompletedNum = totalNum - totalCompletedNum;
63 | const percentCompleted = totalNum === 0 ? 0 : totalCompletedNum / totalNum;
64 | return {
65 | totalNum,
66 | totalCompletedNum,
67 | totalUncompletedNum,
68 | percentCompleted,
69 | };
70 | },
71 | });
72 |
73 | // is filtered list non-empty? (determines whether check-all displays)
74 | const filteredListContentState = selector({
75 | key: 'filteredListContentState',
76 | get: ({ get }) => !!get(filteredTodoListState).length,
77 | });
78 |
79 | // WRITEABLE GET/SET SELECTOR - (un)check all filtered items
80 | const allCompleteState = selector({
81 | key: 'mismatchAllComplete',
82 | // if any item in filteredList is not complete, allComplete is false
83 | get: ({ get }) => !get(filteredTodoListState).some(({ isComplete }) => !isComplete),
84 | set: ({ get, set }, newValue) => {
85 | // update ONLY items in filtered list
86 | const lookupTable = {};
87 | get(todoListState).forEach((item) => {
88 | lookupTable[item.id] = item;
89 | });
90 | get(filteredTodoListState).forEach((item) => {
91 | lookupTable[item.id] = {
92 | ...item,
93 | isComplete: newValue,
94 | };
95 | });
96 | set(todoListState, Object.values(lookupTable));
97 | },
98 | });
99 |
100 | // WRITEABLE RESET SELECTOR - undo sort + filter
101 | const refreshFilterState = selector({
102 | key: 'refreshFilterState',
103 | get: () => null,
104 | set: ({ reset }) => {
105 | reset(todoListSortState);
106 | reset(todoListFilterState);
107 | },
108 | });
109 |
110 | // PROMISE-BASED SELECTOR - fetch quote text
111 | const quoteTextState = selector({
112 | key: 'quoteTextState',
113 | get: ({ get }) => {
114 | const quoteNumber = get(quoteNumberState);
115 | return fetch('https://type.fit/api/quotes')
116 | .then((response) => response.json())
117 | .then((data) => {
118 | const quote = data[quoteNumber];
119 | return `"${quote.text}"\n\t- ${quote.author || 'unknown'}`;
120 | })
121 | .catch((err) => {
122 | console.error(err);
123 | return 'No quote available';
124 | });
125 | },
126 | });
127 |
128 | // ASYNC SELECTOR - fetch comic img
129 | // const xkcdState = selector({
130 | // key: 'xkcdState',
131 | // get: async ({ get }) => {
132 | // const quoteNumber = get(quoteNumberState);
133 | // try {
134 | // // Fetch much be proxied through cors-anywhere to test on localhost
135 | // const response = await fetch(
136 | // `https://cors-anywhere.herokuapp.com/http://xkcd.com/${quoteNumber}/info.0.json`,
137 | // );
138 | // const { img } = await response.json();
139 | // return img;
140 | // } catch (err) {
141 | // // Fallback comic
142 | // return 'https://imgs.xkcd.com/comics/api.png';
143 | // }
144 | // },
145 | // });
146 |
147 | const searchBarSelectorFam = selectorFamily({
148 | key: 'searchBarSelectorFam',
149 | get:
150 | (searchFilter) =>
151 | ({ get }) =>
152 | get(searchResultState)[searchFilter],
153 | set:
154 | (searchFilter) =>
155 | ({ get, set }, searchTerm) => {
156 | set(searchResultState, (prevState) => {
157 | const newResults = get(todoListState).filter((todo) => {
158 | if (searchTerm !== '' && todo.text.includes(searchTerm))
159 | return searchFilter === 'all' ? true : todo.priority === searchFilter;
160 | return false;
161 | });
162 | return { ...prevState, [searchFilter]: { searchTerm, results: newResults } };
163 | });
164 | },
165 | });
166 |
167 | export {
168 | filteredTodoListState,
169 | filteredListContentState,
170 | todoListStatsState,
171 | allCompleteState,
172 | sortedTodoListState,
173 | todoListSortedStats,
174 | refreshFilterState,
175 | quoteTextState,
176 | //xkcdState,
177 | searchBarSelectorFam,
178 | };
179 |
--------------------------------------------------------------------------------
/demo-todo/src/styles/styles.css:
--------------------------------------------------------------------------------
1 | /* -------------General Styles---------------- */
2 | html {
3 | margin: 0;
4 | background-color: rgb(48, 48, 48);
5 | color: whitesmoke;
6 | font-family: 'Palanquin', sans-serif;
7 | overflow: hidden;
8 | }
9 |
10 | h1 {
11 | text-align: center;
12 | font-size: 2.3rem;
13 | color: #af6358;
14 | text-shadow: 1px 2px 2px rgba(250, 250, 250, 0.267);
15 | letter-spacing: 3px;
16 | font-style: italic;
17 | }
18 | input,
19 | button,
20 | select {
21 | background-color: rgb(63, 63, 63);
22 | border: 1px solid lightgray;
23 | border: none;
24 | padding: 20px;
25 | border-radius: 4px;
26 | color: whitesmoke;
27 | font-size: 16px;
28 | letter-spacing: 1px;
29 | }
30 |
31 | /* remove browser defaults */
32 | button:focus {
33 | outline: none;
34 | }
35 | input:focus {
36 | outline: none;
37 | }
38 | /* ------------TodoList------------- */
39 |
40 | /* topmost container */
41 | .mainContainer {
42 | display: grid;
43 | height: 100vh;
44 | width: 100vw;
45 | grid-template-rows: 15fr 33fr 33fr;
46 | }
47 |
48 | /* overall row container for todo list display */
49 | .todosDisplayRow {
50 | margin: 0 auto;
51 | width: 700px;
52 | }
53 |
54 | /* container for entire list display */
55 | .todosContainer {
56 | background-color: rgb(63, 63, 63);
57 | box-shadow: 0px 0px 35px 20px rgba(10, 10, 10, 0.096);
58 | border-radius: 5px;
59 | padding: 5px 12px 0 12px;
60 | }
61 |
62 | /* -------------TodoItemCreator---------------- */
63 |
64 | .itemCreator button {
65 | padding: 0px;
66 | }
67 |
68 | input::placeholder {
69 | font-style: italic;
70 | letter-spacing: 1.5px;
71 | }
72 |
73 | .itemCreator input {
74 | padding: 0 0 0 5%;
75 | width: 70%;
76 | height: 60px;
77 | }
78 |
79 | #radioContainer svg {
80 | margin-top: 11px;
81 | opacity: 0.7;
82 | }
83 |
84 | /* -------------TodoItem---------------- */
85 |
86 | .itemContainer,
87 | .lowPriority,
88 | .mediumPriority,
89 | .highPriority {
90 | display: grid;
91 | grid-template-columns: 79fr 14fr 7fr;
92 | border-bottom: 1px solid rgba(245, 245, 245, 0.336);
93 | }
94 |
95 | /* dynamic checkbox color */
96 | .highPriority svg {
97 | color: #ef5350;
98 | opacity: 0.7;
99 | margin-right: 28px;
100 | }
101 | .mediumPriority svg {
102 | color: #ffee58;
103 | opacity: 0.7;
104 | margin-right: 28px;
105 | }
106 | .lowPriority svg {
107 | color: #66bb6a;
108 | opacity: 0.7;
109 | margin-right: 28px;
110 | }
111 |
112 | #todoItem button {
113 | margin-left: 7px;
114 | }
115 |
116 | /* -------------TodoListFilter---------------- */
117 |
118 | ul {
119 | display: grid;
120 | grid-template-columns: 27fr 27fr 27fr 6fr 6fr 6fr;
121 | margin: 0;
122 | padding: 0px;
123 | width: 100%;
124 | }
125 |
126 | .filter-button {
127 | margin-top: 10px;
128 | margin-bottom: 10px;
129 | border-right: 1px solid rgba(143, 143, 143, 0.26);
130 | padding: 10px 20px;
131 | border-radius: 0px 0px 4px 4px;
132 | }
133 |
134 | /* dynamic sort icon color */
135 | #sortedWhite svg {
136 | color: whitesmoke;
137 | }
138 | #unsortedGray svg {
139 | color: rgba(245, 245, 245, 0.336);
140 | }
141 | #unsortedGray {
142 | width: 64px;
143 | }
144 |
145 | #statsSpan {
146 | display: grid;
147 | grid-template-columns: 30fr 30fr 30fr;
148 | align-items: center;
149 | }
150 | #highSpan {
151 | color: #ef5350;
152 | opacity: 0.7;
153 | margin-right: 4px;
154 | }
155 | #mediumSpan {
156 | color: #ffee58;
157 | opacity: 0.7;
158 | margin-right: 4px;
159 | }
160 | #lowSpan {
161 | color: #66bb6a;
162 | opacity: 0.7;
163 | }
164 |
165 | /* filter stats (number) */
166 | button span {
167 | opacity: 0.6;
168 | }
169 |
170 | /* ----------QuoteBox---------- */
171 |
172 | #quoteContainer {
173 | display: flex;
174 | flex-direction: column;
175 | justify-content: space-between;
176 | }
177 |
178 | #quoteContainer p {
179 | white-space: pre-wrap;
180 | }
181 |
182 | .quoteBox {
183 | display: flex;
184 | flex-direction: row;
185 | justify-content: space-between;
186 | background-color: rgb(63, 63, 63);
187 | box-shadow: 0px 0px 35px 20px rgba(10, 10, 10, 0.096);
188 | border-radius: 5px;
189 | margin: auto;
190 | padding: 12px;
191 | }
192 |
193 | .quoteBox img {
194 | margin: 20px;
195 | height: 150px;
196 | width: 150px;
197 | }
198 |
199 | .quoteBox button {
200 | width: 150px;
201 | }
202 |
203 | .quoteBox button:hover {
204 | border: 1px solid whitesmoke;
205 | cursor: pointer;
206 | }
207 |
208 | /* ---------- TodoQuickCheck ---------- */
209 |
210 | #quickCheck {
211 | font-family: Arial, Helvetica, sans-serif;
212 | }
213 |
214 | #quickCheck svg {
215 | color: #af6358;
216 | }
217 |
218 | /* ---------- Search ---------- */
219 | .searchContainer {
220 | margin-top: 100px;
221 | background-color: rgb(63, 63, 63);
222 | box-shadow: 0px 0px 35px 20px rgba(10, 10, 10, 0.096);
223 | border-radius: 5px;
224 | padding: 5px 12px 12px 12px;
225 | display: grid;
226 | grid-template-columns: 70% 30%;
227 | }
228 |
229 | .searchField {
230 | border-bottom: solid 1px whitesmoke;
231 | border-radius: 0;
232 | }
233 |
234 | .prioritySelect {
235 | grid-column-start: 2;
236 | border-bottom: solid 1px whitesmoke;
237 | border-radius: 0;
238 | }
239 |
240 | .searchResults {
241 | grid-column-start: span 3;
242 | }
243 |
--------------------------------------------------------------------------------
/demo-todo/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | entry: path.resolve(__dirname, './src/index.js'),
5 | output: {
6 | filename: 'bundle.js',
7 | },
8 | devServer: {
9 | contentBase: path.resolve(__dirname, './src'),
10 | historyApiFallback: true,
11 | },
12 | mode: process.env.NODE_ENV,
13 | module: {
14 | rules: [
15 | {
16 | test: /\.(js|jsx)$/,
17 | exclude: /node_modules/,
18 | loader: 'babel-loader',
19 | options: {
20 | presets: ['@babel/preset-env', '@babel/preset-react'],
21 | plugins: ['@babel/transform-runtime'],
22 | },
23 | },
24 | {
25 | test: /\.css$/,
26 | use: [
27 | {
28 | loader: 'style-loader',
29 | },
30 | {
31 | loader: 'css-loader',
32 | },
33 | ],
34 | },
35 | ],
36 | },
37 | resolve: {
38 | extensions: ['.js', '.jsx'],
39 | },
40 | };
41 |
--------------------------------------------------------------------------------
/demo-zustand-todo/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["@babel/preset-env"],
4 | "@babel/preset-react"
5 | ],
6 | "plugins": [
7 | //"react-hot-loader/babel"
8 | ]
9 | }
--------------------------------------------------------------------------------
/demo-zustand-todo/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Michelle Holland
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/demo-zustand-todo/__tests__/sampleTest.js:
--------------------------------------------------------------------------------
1 | import { renderHook, act } from '@testing-library/react';
2 | import useStore from '../src/store/store';
3 |
4 | describe('INITIAL RENDER', () => {
5 | const { result } = renderHook(useStore);
6 |
7 | it('todoListState should initialize correctly', () => {
8 | expect(result.current.todoListState).toStrictEqual([]);
9 | });
10 |
11 | it('todoListFilterState should initialize correctly', () => {
12 | expect(result.current.todoListFilterState).toStrictEqual('Show All');
13 | });
14 |
15 | it('todoListSortState should initialize correctly', () => {
16 | expect(result.current.todoListSortState).toStrictEqual(false);
17 | });
18 |
19 | it('quoteText should initialize correctly', () => {
20 | expect(result.current.quoteText).toStrictEqual('');
21 | });
22 |
23 | it('quoteNumber should initialize correctly', () => {
24 | expect(result.current.quoteNumber).toStrictEqual(0);
25 | });
26 |
27 | it('checkBox should initialize correctly', () => {
28 | expect(result.current.checkBox).toStrictEqual(false);
29 | });
30 |
31 | it('searchResultState should initialize correctly', () => {
32 | expect(result.current.searchResultState).toStrictEqual({
33 | all: { searchTerm: '', results: [] },
34 | high: { searchTerm: '', results: [] },
35 | medium: { searchTerm: '', results: [] },
36 | low: { searchTerm: '', results: [] },
37 | });
38 | });
39 | });
40 |
41 | describe('STATE CHANGES', () => {
42 | const { result } = renderHook(useStore);
43 |
44 | it('checkBox & quoteText & todoListState should update correctly', () => {
45 | const { result } = renderHook(useStore);
46 |
47 | act(() => {
48 | result.current.setCheckBox();
49 | result.current.setCheckBox();
50 | result.current.changeQuoteText(
51 | '"Your ability to learn faster than your competition is your only sustainable competitive advantage."\n\t- Arie de Gues',
52 | );
53 | result.current.addTodoListItem({ id: 2, text: 'tennis', priority: 'low', isComplete: false });
54 | });
55 |
56 | expect(result.current.checkBox).toStrictEqual(true);
57 | expect(result.current.quoteText).toStrictEqual(
58 | '"Your ability to learn faster than your competition is your only sustainable competitive advantage."\n\t- Arie de Gues',
59 | );
60 | expect(result.current.todoListState).toStrictEqual([
61 | { id: 2, text: 'tennis', priority: 'low', isComplete: false },
62 | ]);
63 | });
64 | it('checkBox & todoListState should update correctly', () => {
65 | const { result } = renderHook(useStore);
66 |
67 | act(() => {
68 | result.current.setCheckBox();
69 | result.current.setCheckBox();
70 | result.current.addTodoListItem({ id: 3, text: 'hockey', priority: 'low', isComplete: false });
71 | result.current.setCheckBox();
72 | });
73 |
74 | expect(result.current.checkBox).toStrictEqual(false);
75 | expect(result.current.todoListState).toStrictEqual([
76 | { id: 2, text: 'tennis', priority: 'low', isComplete: false },
77 | { id: 3, text: 'hockey', priority: 'low', isComplete: false },
78 | ]);
79 | });
80 | it('todoListState should update correctly', () => {
81 | const { result } = renderHook(useStore);
82 |
83 | act(() => {
84 | result.current.addTodoListItem({ id: 4, text: 'hocka', priority: 'low', isComplete: false });
85 | result.current.setCheckBox();
86 | });
87 |
88 | expect(result.current.todoListState).toStrictEqual([
89 | { id: 2, text: 'tennis', priority: 'low', isComplete: false },
90 | { id: 3, text: 'hockey', priority: 'low', isComplete: false },
91 | { id: 4, text: 'hocka', priority: 'low', isComplete: false },
92 | ]);
93 | });
94 | it('todoListState & searchResultState should update correctly', () => {
95 | const { result } = renderHook(useStore);
96 |
97 | act(() => {
98 | result.current.addTodoListItem({ id: 5, text: 'canoe', priority: 'low', isComplete: false });
99 | result.current.setCheckBox();
100 | result.current.setSearchState('c', 'all');
101 | });
102 |
103 | expect(result.current.todoListState).toStrictEqual([
104 | { id: 2, text: 'tennis', priority: 'low', isComplete: false },
105 | { id: 3, text: 'hockey', priority: 'low', isComplete: false },
106 | { id: 4, text: 'hocka', priority: 'low', isComplete: false },
107 | { id: 5, text: 'canoe', priority: 'low', isComplete: false },
108 | ]);
109 | expect(result.current.searchResultState).toStrictEqual({
110 | all: {
111 | searchTerm: 'c',
112 | results: [
113 | { id: 3, text: 'hockey', priority: 'low', isComplete: false },
114 | { id: 4, text: 'hocka', priority: 'low', isComplete: false },
115 | { id: 5, text: 'canoe', priority: 'low', isComplete: false },
116 | ],
117 | },
118 | high: { searchTerm: '', results: [] },
119 | medium: { searchTerm: '', results: [] },
120 | low: { searchTerm: '', results: [] },
121 | });
122 | });
123 | it('searchResultState should update correctly', () => {
124 | const { result } = renderHook(useStore);
125 |
126 | act(() => {
127 | result.current.setSearchState('ca', 'all');
128 | });
129 |
130 | expect(result.current.searchResultState).toStrictEqual({
131 | all: {
132 | searchTerm: 'ca',
133 | results: [{ id: 5, text: 'canoe', priority: 'low', isComplete: false }],
134 | },
135 | high: { searchTerm: '', results: [] },
136 | medium: { searchTerm: '', results: [] },
137 | low: { searchTerm: '', results: [] },
138 | });
139 | });
140 | it('searchResultState should update correctly', () => {
141 | const { result } = renderHook(useStore);
142 |
143 | act(() => {
144 | result.current.setSearchState('can', 'all');
145 | });
146 |
147 | expect(result.current.searchResultState).toStrictEqual({
148 | all: {
149 | searchTerm: 'can',
150 | results: [{ id: 5, text: 'canoe', priority: 'low', isComplete: false }],
151 | },
152 | high: { searchTerm: '', results: [] },
153 | medium: { searchTerm: '', results: [] },
154 | low: { searchTerm: '', results: [] },
155 | });
156 | });
157 | it('searchResultState should update correctly', () => {
158 | const { result } = renderHook(useStore);
159 |
160 | act(() => {
161 | result.current.setSearchState('cano', 'all');
162 | });
163 |
164 | expect(result.current.searchResultState).toStrictEqual({
165 | all: {
166 | searchTerm: 'cano',
167 | results: [{ id: 5, text: 'canoe', priority: 'low', isComplete: false }],
168 | },
169 | high: { searchTerm: '', results: [] },
170 | medium: { searchTerm: '', results: [] },
171 | low: { searchTerm: '', results: [] },
172 | });
173 | });
174 | });
175 |
--------------------------------------------------------------------------------
/demo-zustand-todo/chromogen-4.0.4.tgz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/open-source-labs/Chromogen/6b2085e43c65c20a90916b22255b2f1606411682/demo-zustand-todo/chromogen-4.0.4.tgz
--------------------------------------------------------------------------------
/demo-zustand-todo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chromogen-todo",
3 | "version": "1.0.2",
4 | "description": "demo todo app for Chromogen using React + Recoil",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "webpack-dev-server --open",
8 | "test": "jest --verbose",
9 | "update": " npm run uninstall && npm run install && npm run start",
10 | "uninstall": "npm uninstall chromogen",
11 | "install": "npm install ../package",
12 | "buildPackage": "tsc",
13 | "tarballUpdate": "npm --prefix ../package run build && npm pack ../package && npm uninstall chromogen && npm install ./chromogen-5.0.1.tgz && npm start"
14 | },
15 | "keywords": [
16 | "react",
17 | "recoil",
18 | "chromogen",
19 | "demo",
20 | "example",
21 | "todo"
22 | ],
23 | "repository": {
24 | "type": "git",
25 | "url": "https://github.com/open-source-labs/Chromogen.git"
26 | },
27 | "contributors": [
28 | {
29 | "name": "Brach Burdick",
30 | "url": "https://github.com/sirbrachthepale/"
31 | },
32 | {
33 | "name": "Francois Denavaut",
34 | "url": "https://github.com/dnvt/"
35 | },
36 | {
37 | "name": "Maggie Kwan",
38 | "url": "https://github.com/maggiekwan/"
39 | },
40 | {
41 | "name": "Lawrence Liang",
42 | "url": "https://github.com/Lawliang/"
43 | },
44 | {
45 | "name": "Michelle Holland",
46 | "url": "https://github.com/michellebholland/"
47 | },
48 | {
49 | "name": "Jim Chen",
50 | "url": "https://github.com/chenchingk"
51 | },
52 | {
53 | "name": "Andy Wang",
54 | "url": "https://github.com/andywang23"
55 | },
56 | {
57 | "name": "Connor Rose Delisle",
58 | "url": "https://github.com/connorrose"
59 | }
60 | ],
61 | "license": "MIT",
62 | "devDependencies": {
63 | "@babel/core": "^7.11.1",
64 | "@babel/plugin-transform-runtime": "^7.11.0",
65 | "@babel/preset-env": "^7.11.0",
66 | "@babel/preset-react": "^7.10.4",
67 | "@testing-library/react": "^13.1.1",
68 | "babel-loader": "^8.1.0",
69 | "css-loader": "^4.2.1",
70 | "identity-obj-proxy": "^3.0.0",
71 | "jest": "^26.4.2",
72 | "prettier": "^2.7.1",
73 | "style-loader": "^1.2.1",
74 | "webpack": "^4.44.1",
75 | "webpack-cli": "^3.3.12",
76 | "webpack-dev-server": "^3.11.0"
77 | },
78 | "peerDependencies": {
79 | "typescript": "^4.0.3"
80 | },
81 | "dependencies": {
82 | "@babel/runtime": "^7.11.2",
83 | "@emotion/react": "^11.10.5",
84 | "@emotion/styled": "^11.10.4",
85 | "@mui/icons-material": "^5.10.6",
86 | "@mui/material": "^5.10.6",
87 | "babel-jest": "^26.3.0",
88 | "chromogen": "file:chromogen-4.0.4.tgz",
89 | "file-loader": "^6.2.0",
90 | "react": "^18.0.0",
91 | "react-dom": "^18.0.0",
92 | "react-test-renderer": "^18.1.0",
93 | "recoil": "0.7.5",
94 | "typescript": "^4.0.3",
95 | "url-loader": "^4.1.1",
96 | "zustand": "^4.1.1"
97 | },
98 | "jest": {
99 | "moduleNameMapper": {
100 | "\\.(css|less)$": "identity-obj-proxy"
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/demo-zustand-todo/src/components/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ChromogenZustandObserver } from 'chromogen';
3 | import TodoList from './TodoList';
4 | import '../styles/styles.css';
5 |
6 | const App = () => (
7 | <>
8 |
9 |
10 |
11 | >
12 | );
13 |
14 | export default App;
15 |
--------------------------------------------------------------------------------
/demo-zustand-todo/src/components/Quotes.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import shallow from 'zustand/shallow';
3 | import useToDoStore from '../store/store';
4 | import { useEffect } from 'react';
5 |
6 | const selector = (state) => ({
7 | changeQuoteText: state.changeQuoteText,
8 | quoteText: state.quoteText,
9 | });
10 |
11 | const Quotes = () => {
12 | const { changeQuoteText, quoteText } = useToDoStore(selector, shallow);
13 |
14 | const fetchMe = () => {
15 | let randomNum = Math.floor(Math.random() * 1643);
16 |
17 | fetch('https://type.fit/api/quotes')
18 | .then((response) => response.json())
19 | .then((data) => {
20 | const quote = data[randomNum];
21 | changeQuoteText(`"${quote.text}"\n\t- ${quote.author || 'unknown'}`);
22 | })
23 | .catch((err) => {
24 | console.error(err);
25 | return 'No quote available';
26 | });
27 | };
28 |
29 | useEffect(() => fetchMe(), []);
30 |
31 | return (
32 | <>
33 |
37 | >
38 | );
39 | };
40 |
41 | export default Quotes;
42 |
--------------------------------------------------------------------------------
/demo-zustand-todo/src/components/ReadOnlyTodoItem.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Checkbox from '@mui/material/Checkbox';
3 | import '../styles/styles.css';
4 | import useToDoStore from '../store/store';
5 |
6 | const ReadOnlyTodoItem = ({ item }) => {
7 | const checkBoxClasses = {
8 | low: 'lowPriority',
9 | medium: 'mediumPriority',
10 | high: 'highPriority',
11 | };
12 |
13 | const todoList = useToDoStore((state) => state.todoListState);
14 |
15 | return todoList.find((todo) => todo.id === item.id) ? (
16 |
17 |
18 | todo.id === item.id).isComplete}
21 | color="default"
22 | inputProps={{ 'aria-label': 'primary checkbox' }}
23 | style={{ cursor: 'default' }}
24 | />
25 |
26 | ) : null;
27 | };
28 | export default ReadOnlyTodoItem;
29 |
--------------------------------------------------------------------------------
/demo-zustand-todo/src/components/SearchBar.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import useToDoStore from '../store/store';
3 | import shallow from 'zustand/shallow';
4 |
5 | import ReadOnlyTodoItem from './ReadOnlyTodoItem';
6 |
7 | const selector = (state) => ({
8 | searchResultState: state.searchResultState,
9 | setSearchState: state.setSearchState,
10 | });
11 |
12 | const SearchBar = () => {
13 | const [searchFilter, setSearchFilter] = useState('all');
14 | const [searchText, setSearchText] = useState('');
15 | const { searchResultState, setSearchState } = useToDoStore(selector, shallow);
16 | const searchResults = searchResultState[searchFilter];
17 |
18 | const onSearchTextChange = (e) => {
19 | setSearchText(e.target.value);
20 | setSearchState(e.target.value, searchFilter);
21 | };
22 | const onSelectChange = (e) => {
23 | setSearchText('');
24 | setSearchFilter(e.target.value);
25 | };
26 |
27 | return (
28 |
29 |
37 |
42 | All Priorities
43 | High Priority
44 | Medium Priority
45 | Low Priority
46 |
47 |
48 | {searchResults.results.map((result, idx) => (
49 |
50 | ))}
51 |
52 |
53 | );
54 | };
55 |
56 | export default SearchBar;
57 |
--------------------------------------------------------------------------------
/demo-zustand-todo/src/components/TodoItem.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Checkbox from '@mui/material/Checkbox';
3 | import '../styles/styles.css';
4 | import shallow from 'zustand/shallow';
5 | import useToDoStore from '../store/store';
6 |
7 | const selector = (state) => ({
8 | todoListState: state.todoListState,
9 | deleteTodoListItem: state.deleteTodoListItem,
10 | editItemText: state.editItemText,
11 | toggleItemCompletion: state.toggleItemCompletion,
12 | });
13 |
14 | const TodoItem = ({ item }) => {
15 | const { deleteTodoListItem, editItemText, toggleItemCompletion } = useToDoStore(
16 | selector,
17 | shallow,
18 | );
19 |
20 | const checkBoxClasses = {
21 | low: 'lowPriority',
22 | medium: 'mediumPriority',
23 | high: 'highPriority',
24 | };
25 |
26 | return (
27 |
28 | editItemText(e.target.value, item.id)}
32 | />
33 | toggleItemCompletion(item.id)}
39 | />
40 | deleteTodoListItem(item.id)}>
41 | X
42 |
43 |
44 | );
45 | };
46 | export default TodoItem;
47 |
--------------------------------------------------------------------------------
/demo-zustand-todo/src/components/TodoItemCreator.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/jsx-props-no-spreading */
2 | import React, { useState } from 'react';
3 | import RadioGroup from '@mui/material/RadioGroup';
4 | import FormControlLabel from '@mui/material/FormControlLabel';
5 | import FormControl from '@mui/material/FormControl';
6 | import FormLabel from '@mui/material/FormLabel';
7 | import Radio from '@mui/material/Radio';
8 | import useToDoStore from '../store/store';
9 |
10 | const selector = (state) => state.addTodoListItem;
11 |
12 | // utility for creating unique Id
13 | let id = 1;
14 | const getId = () => {
15 | id += 1;
16 | return id;
17 | };
18 |
19 | const TodoItemCreator = () => {
20 | const [inputValue, setInputValue] = useState('');
21 | const [priorityValue, setPriorityValue] = useState('low');
22 | const addTodoListItem = useToDoStore(selector);
23 |
24 | const addItem = () => {
25 | addTodoListItem({
26 | id: getId(),
27 | text: inputValue,
28 | priority: priorityValue,
29 | isComplete: false,
30 | });
31 | setInputValue('');
32 | setPriorityValue('low');
33 | };
34 |
35 | const onChange = ({ target: { value } }) => {
36 | setInputValue(value);
37 | };
38 |
39 | const handleChange = (event) => {
40 | setPriorityValue(event.target.value);
41 | };
42 |
43 | /* MUI Radio Button styles */
44 | const GreenRadio = (props) => ;
45 |
46 | const YellowRadio = (props) => ;
47 |
48 | const RedRadio = (props) => ;
49 |
50 | return (
51 |
52 |
59 |
60 |
61 |
62 |
69 | } value="high" />
70 | } value="medium" />
71 | } value="low" />
72 |
73 |
74 |
75 |
76 |
77 | Add
78 |
79 |
80 | );
81 | };
82 |
83 | export default TodoItemCreator;
84 |
--------------------------------------------------------------------------------
/demo-zustand-todo/src/components/TodoList.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TodoItem from './TodoItem';
3 | import TodoItemCreator from './TodoItemCreator';
4 | import TodoListFilters from './TodoListFilters';
5 | import TodoQuickCheck from './TodoQuickCheck';
6 | import Quotes from './Quotes';
7 | import SearchBar from './SearchBar';
8 | import '../styles/styles.css';
9 | import shallow from 'zustand/shallow';
10 | import useToDoStore from '../store/store';
11 |
12 | const selector = (state) => ({
13 | todoListState: state.todoListState,
14 | todoListFilterState: state.todoListFilterState,
15 | todoListSortState: state.todoListSortState,
16 | });
17 |
18 | const filterList = (list, filter) => {
19 | switch (filter) {
20 | case 'Show Completed':
21 | return list.filter((item) => item.isComplete);
22 | case 'Show Uncompleted':
23 | return list.filter((item) => !item.isComplete);
24 | default:
25 | return list;
26 | }
27 | };
28 |
29 | const sortList = (list, sortingMethod) => {
30 | if (!sortingMethod) return list;
31 | const high = list.filter((item) => item.priority === 'high');
32 | const medium = list.filter((item) => item.priority === 'medium');
33 | const low = list.filter((item) => item.priority === 'low');
34 | return [...high, ...medium, ...low];
35 | };
36 |
37 | const TodoList = () => {
38 | const { todoListState, todoListFilterState, todoListSortState } = useToDoStore(selector, shallow);
39 | const todoList = sortList(filterList(todoListState, todoListFilterState), todoListSortState);
40 |
41 | return (
42 |
43 |
44 |
45 |
50 |
51 |
52 | Loading...}>
53 |
54 |
55 |
56 |
57 |
To-Do List
58 |
59 |
60 |
61 | {todoList.map((todoItem) => (
62 |
63 | ))}
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | );
72 | };
73 |
74 | export default TodoList;
75 |
--------------------------------------------------------------------------------
/demo-zustand-todo/src/components/TodoListFilters.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import SortIcon from '@mui/icons-material/Sort';
3 | import EqualizerIcon from '@mui/icons-material/Equalizer';
4 | import RefreshIcon from '@mui/icons-material/Refresh';
5 | import useToDoStore from '../store/store';
6 | import shallow from 'zustand/shallow';
7 |
8 | const selector = (state) => ({
9 | todoListFilterState: state.todoListFilterState,
10 | todoListState: state.todoListState,
11 | resetFiltersAndSorted: state.resetFiltersAndSorted,
12 | todoListSortState: state.todoListSortState,
13 | toggleSort: state.toggleSort,
14 | setFilter: state.setFilter,
15 | });
16 |
17 | const TodoListFilters = () => {
18 | const {
19 | todoListFilterState,
20 | todoListState,
21 | resetFiltersAndSorted,
22 | todoListSortState,
23 | toggleSort,
24 | setFilter,
25 | } = useToDoStore(selector, shallow);
26 |
27 | // // selector - grabs totals for each category
28 | const { high, medium, low } = todoListState.reduce((acc, cur) => {
29 | acc[cur.priority] = (acc[cur.priority] ?? 0) + 1;
30 | return acc;
31 | }, {});
32 |
33 | // // toggle priority stats display
34 | const [displayStats, setDisplayStats] = useState(false);
35 |
36 | // // selector - totals for each filter
37 | const totalNum = todoListState.length;
38 | const totalCompletedNum = todoListState.filter((todo) => todo.isComplete).length;
39 | const totalUncompletedNum = todoListState.filter((todo) => !todo.isComplete).length;
40 |
41 | const updateFilter = ({ target: { value } }) => setFilter(value);
42 |
43 | const toggleDisplayStats = () => setDisplayStats(!displayStats);
44 | const reset = () => {
45 | setDisplayStats(false); // displayStats is local state
46 | resetFiltersAndSorted();
47 | };
48 |
49 | const sortIconColor = {
50 | true: 'sortedWhite',
51 | false: 'unsortedGray',
52 | };
53 |
54 | return (
55 |
56 |
64 | All {totalNum || ''}
65 |
66 |
74 | Active {totalUncompletedNum || ''}
75 |
76 |
83 | Complete {totalCompletedNum || ''}
84 |
85 |
86 |
87 |
88 |
89 |
90 | {displayStats && totalNum ? (
91 |
92 | {high || 0}
93 | {medium || 0}
94 | {low || 0}
95 |
96 | ) : (
97 |
98 | )}
99 |
100 |
101 |
102 |
103 |
104 | );
105 | };
106 |
107 | export default TodoListFilters;
108 |
--------------------------------------------------------------------------------
/demo-zustand-todo/src/components/TodoQuickCheck.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Checkbox from '@mui/material/Checkbox';
3 | import shallow from 'zustand/shallow';
4 | import useToDoStore from '../store/store';
5 | import { useEffect } from 'react';
6 |
7 | const selector = (state) => ({
8 | setAllComplete: state.setAllComplete,
9 | checkBox: state.checkBox,
10 | setCheckBox: state.setCheckBox,
11 | });
12 |
13 | const TodoQuickCheck = () => {
14 | const { setAllComplete, checkBox, setCheckBox } = useToDoStore(selector, shallow);
15 |
16 | useEffect(() => setCheckBox());
17 |
18 | return (
19 |
20 | setAllComplete()}
26 | />
27 | All
28 |
29 | );
30 | };
31 |
32 | export default TodoQuickCheck;
33 |
--------------------------------------------------------------------------------
/demo-zustand-todo/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/open-source-labs/Chromogen/6b2085e43c65c20a90916b22255b2f1606411682/demo-zustand-todo/src/favicon.ico
--------------------------------------------------------------------------------
/demo-zustand-todo/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 | Chromogen Zustand Demo To-Do
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/demo-zustand-todo/src/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/jsx-filename-extension */
2 | import React from 'react';
3 | import App from './components/App';
4 | import { createRoot } from 'react-dom/client';
5 |
6 | const root = createRoot(document.getElementById('app'));
7 |
8 | root.render( );
9 |
--------------------------------------------------------------------------------
/demo-zustand-todo/src/store/store.js:
--------------------------------------------------------------------------------
1 | import { chromogenZustandMiddleware } from 'chromogen';
2 | import { create } from 'zustand';
3 |
4 | const useToDoStore = create(
5 | chromogenZustandMiddleware((set) => ({
6 | todoListState: [],
7 |
8 | todoListFilterState: 'Show All',
9 |
10 | todoListSortState: false,
11 |
12 | resetFiltersAndSorted: () =>
13 | set(
14 | () => ({ todoListFilterState: 'Show All', todoListSortState: false }),
15 | false,
16 | 'resetFiltersAndSorted',
17 | ),
18 |
19 | toggleSort: () =>
20 | set((state) => ({ todoListSortState: !state.todoListSortState }), false, 'toggleSort'),
21 |
22 | setFilter: (filter) => set(() => ({ todoListFilterState: filter }), false, 'setFilter'),
23 |
24 | quoteText: '',
25 |
26 | changeQuoteText: (text) => set(() => ({ quoteText: text }), false, 'changeQuoteText', text),
27 |
28 | quoteNumber: 0,
29 |
30 | changeQuoteNumber: () =>
31 | set(() => ({ quoteNumber: Math.floor(Math.random() * 1643) }), false, 'changeQuoteNumber'),
32 |
33 | setAllComplete: () =>
34 | set(
35 | (state) => ({
36 | todoListState: state.todoListState.some((todo) => todo.isComplete === false)
37 | ? state.todoListState.map((todo) => {
38 | return { ...todo, isComplete: true };
39 | })
40 | : state.todoListState.map((todo) => {
41 | return { ...todo, isComplete: false };
42 | }),
43 | }),
44 | false,
45 | 'setAllComplete',
46 | ),
47 |
48 | checkBox: false,
49 |
50 | setCheckBox: () =>
51 | set(
52 | (state) => ({
53 | checkBox: state.todoListState.some((todo) => todo.isComplete === false) ? false : true,
54 | }),
55 | false,
56 | 'setCheckBox',
57 | ),
58 |
59 | addTodoListItem: (todo) =>
60 | set(
61 | (state) => ({ todoListState: [...state.todoListState, todo] }),
62 | false,
63 | 'addTodoListItem',
64 | todo,
65 | ),
66 |
67 | deleteTodoListItem: (id) =>
68 | set(
69 | (state) => ({ todoListState: state.todoListState.filter((todo) => todo.id !== id) }),
70 | false,
71 | 'deleteTodoListItem',
72 | id,
73 | ),
74 |
75 | editItemText: (text, id) =>
76 | set(
77 | (state) => ({
78 | todoListState: state.todoListState.map((todo) => {
79 | if (todo.id === id) {
80 | return { ...todo, text: text };
81 | } else {
82 | return todo;
83 | }
84 | }),
85 | }),
86 | false,
87 | 'editItemText',
88 | text,
89 | id,
90 | ),
91 |
92 | toggleItemCompletion: (id) =>
93 | set(
94 | (state) => ({
95 | todoListState: state.todoListState.map((todo) => {
96 | if (todo.id === id) {
97 | return { ...todo, isComplete: !todo.isComplete };
98 | } else {
99 | return todo;
100 | }
101 | }),
102 | }),
103 | false,
104 | 'toggleItemCompletion',
105 | id,
106 | ),
107 |
108 | searchResultState: {
109 | all: {
110 | searchTerm: '',
111 | results: [],
112 | },
113 | high: {
114 | searchTerm: '',
115 | results: [],
116 | },
117 | medium: {
118 | searchTerm: '',
119 | results: [],
120 | },
121 | low: {
122 | searchTerm: '',
123 | results: [],
124 | },
125 | },
126 |
127 | setSearchState: (searchTerm, priority) =>
128 | set(
129 | (state) => {
130 | if (searchTerm === '')
131 | return {
132 | searchResultState: {
133 | ...state.searchResultState,
134 | [priority]: { searchTerm, results: [] },
135 | },
136 | };
137 | let results = [...state.todoListState].filter((todo) => todo.text.includes(searchTerm));
138 | if (priority !== 'all') results = results.filter((todo) => todo.priority === priority);
139 | return {
140 | searchResultState: { ...state.searchResultState, [priority]: { searchTerm, results } },
141 | };
142 | },
143 | false,
144 | 'setSearchState',
145 | searchTerm,
146 | priority,
147 | ),
148 | })),
149 | );
150 |
151 | export default useToDoStore;
152 |
--------------------------------------------------------------------------------
/demo-zustand-todo/src/styles/styles.css:
--------------------------------------------------------------------------------
1 | *,
2 | *::before,
3 | *::after {
4 | box-sizing: border-box;
5 | }
6 | * {
7 | margin: 0;
8 | }
9 | html,
10 | body,
11 | #root, /* for create-react-app */
12 | #__next /* for Next.js */ {
13 | height: 100%;
14 | }
15 | body {
16 | font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, Arial, sans-serif;
17 | -webkit-font-smoothing: antialiased;
18 | }
19 | img,
20 | picture,
21 | video,
22 | canvas,
23 | svg {
24 | display: block;
25 | max-width: 100%;
26 | }
27 | input,
28 | button,
29 | textarea,
30 | select {
31 | font: inherit;
32 | }
33 | p,
34 | h1,
35 | h2,
36 | h3,
37 | h4,
38 | h5,
39 | h6 {
40 | overflow-wrap: break-word;
41 | }
42 | #root,
43 | #__next {
44 | isolation: isolate;
45 | }
46 |
47 | /* -------------General Styles---------------- */
48 | html {
49 | margin: 0;
50 | background-color: #2e3237;
51 | color: #9ee6f7;
52 | overflow-x: hidden;
53 | }
54 |
55 | h1 {
56 | text-align: center;
57 | font-size: 2.3rem;
58 | color: #9ee6f7;
59 | text-shadow: 1px 2px 2px rgba(250, 250, 250, 0.267);
60 | letter-spacing: 3px;
61 | }
62 | input,
63 | button,
64 | select {
65 | background-color: transparent;
66 | /* border: 1px solid lightgray; */
67 | border: none;
68 | padding: 20px 0;
69 | border-radius: 4px;
70 | color: #9ee6f7;
71 | font-size: 18px;
72 | letter-spacing: 1px;
73 | }
74 |
75 | /* remove browser defaults */
76 | button:focus {
77 | outline: none;
78 | }
79 | input:focus {
80 | outline: none;
81 | }
82 | /* ------------TodoList------------- */
83 |
84 | /* topmost container */
85 | .mainContainer {
86 | display: flex;
87 | flex-direction: column;
88 | height: 100vh;
89 | width: 100vw;
90 | overflow-y: scroll;
91 | }
92 |
93 | .wrapper {
94 | display: flex;
95 | margin: 0 auto;
96 | padding-inline: 32px;
97 | width: 100%;
98 | max-width: 800px;
99 | height: 100%;
100 | flex-direction: column;
101 | padding-bottom: 24px;
102 | }
103 |
104 | /* overall row container for todo list display */
105 | .todosDisplayRow {
106 | margin: 0 auto;
107 | width: 100%;
108 | height: 100%;
109 | color: #9ee6f7;
110 | }
111 |
112 | a {
113 | color: #9ee6f7;
114 | text-decoration: none;
115 | }
116 |
117 | a:hover {
118 | text-decoration: underline;
119 | }
120 |
121 | .todosDisplayRow h1 {
122 | padding-block: 40px;
123 | }
124 |
125 | /* container for entire list display */
126 | .todosContainer {
127 | background-color: rgb(255, 255, 255, 0.1);
128 | box-shadow: 0px 0px 35px 20px rgba(10, 10, 10, 0.096);
129 | border-radius: 5px;
130 | padding: 5px 12px 0 12px;
131 | }
132 |
133 | /* -------------TodoItemCreator---------------- */
134 |
135 | .itemCreator {
136 | display: flex;
137 | border-bottom: 1px solid rgba(245, 245, 245, 0.336);
138 | }
139 |
140 | .itemCreator button {
141 | padding: 0 24px;
142 | }
143 |
144 | input::placeholder {
145 | letter-spacing: 1.5px;
146 | }
147 |
148 | .itemCreator input {
149 | padding: 0 0 0 12px;
150 | flex-grow: 1;
151 | height: 60px;
152 | }
153 |
154 | #radioContainer svg {
155 | margin-top: 11px;
156 | opacity: 0.7;
157 | }
158 |
159 | label.MuiFormControlLabel-root {
160 | margin-left: 0;
161 | margin-right: 0;
162 | }
163 |
164 | /* -------------TodoItem---------------- */
165 |
166 | .itemContainer,
167 | .lowPriority,
168 | .mediumPriority,
169 | .highPriority {
170 | display: grid;
171 | grid-template-columns: 79fr 14fr 7fr;
172 | border-bottom: 1px solid rgba(245, 245, 245, 0.336);
173 | }
174 |
175 | /* dynamic checkbox color */
176 | .highPriority svg {
177 | color: #f10101;
178 | opacity: 0.7;
179 | margin-right: 28px;
180 | }
181 | .mediumPriority svg {
182 | color: #ffe600;
183 | opacity: 0.7;
184 | margin-right: 28px;
185 | }
186 | .lowPriority svg {
187 | color: #05fb11;
188 | opacity: 0.7;
189 | margin-right: 28px;
190 | }
191 |
192 | #todoItem button {
193 | margin-left: 7px;
194 | }
195 |
196 | /* -------------TodoListFilter---------------- */
197 |
198 | ul {
199 | display: grid;
200 | grid-template-columns: 27fr 27fr 27fr 6fr 6fr 6fr;
201 | margin: 0;
202 | padding: 0px;
203 | width: 100%;
204 | }
205 |
206 | .filter-button {
207 | margin-top: 10px;
208 | margin-bottom: 10px;
209 | border-right: 1px solid rgba(234, 230, 230, 0.26);
210 | padding: 10px 20px;
211 | border-radius: 0px 0px 4px 4px;
212 | }
213 |
214 | /* dynamic sort icon color */
215 | #sortedWhite svg {
216 | color: whitesmoke;
217 | }
218 | #unsortedGray svg {
219 | color: rgba(245, 245, 245, 0.336);
220 | }
221 | #unsortedGray {
222 | width: 64px;
223 | }
224 |
225 | #statsSpan {
226 | display: grid;
227 | grid-template-columns: 30fr 30fr 30fr;
228 | align-items: center;
229 | }
230 | #highSpan {
231 | color: #ef5350;
232 | opacity: 0.7;
233 | margin-right: 4px;
234 | }
235 | #mediumSpan {
236 | color: #ffee58;
237 | opacity: 0.7;
238 | margin-right: 4px;
239 | }
240 | #lowSpan {
241 | color: #66bb6a;
242 | opacity: 0.7;
243 | }
244 |
245 | /* filter stats (number) */
246 | button span {
247 | opacity: 0.6;
248 | }
249 |
250 | /* ----------QuoteBox---------- */
251 |
252 | #quoteContainer {
253 | display: flex;
254 | flex-direction: column;
255 | width: 100%;
256 | /* justify-content: space-between; */
257 | font-size: 18px;
258 | }
259 |
260 | #quoteContainer p {
261 | white-space: pre-wrap;
262 | padding-bottom: 24px;
263 | }
264 |
265 | .quoteBox {
266 | display: flex;
267 | flex-direction: row;
268 | justify-content: space-between;
269 | border: 1px solid rgba(234, 230, 230, 0.1);
270 | /* background-color: rgb(63, 63, 63); */
271 | /* box-shadow: 0px 0px 35px 20px rgba(10, 10, 10, 0.096); */
272 | border-radius: 5px;
273 | padding: 16px 24px;
274 | }
275 |
276 | .quoteBox img {
277 | margin: 20px;
278 | height: 150px;
279 | width: 150px;
280 | }
281 |
282 | .quoteBox button {
283 | width: 150px;
284 | padding: 24 0;
285 | text-align: left;
286 | }
287 |
288 | .quoteBox button:hover {
289 | cursor: pointer;
290 | /* background-color: rgba(255, 255, 255, 0.1); */
291 | }
292 |
293 | /* .quoteBox button:active {
294 | background-color: #75acb990;
295 | } */
296 |
297 | /* ---------- TodoQuickCheck ---------- */
298 |
299 | /* #quickCheck {
300 | } */
301 |
302 | #quickCheck svg {
303 | color: #9ee6f7;
304 | }
305 |
306 | button {
307 | cursor: pointer;
308 | }
309 |
310 | /* ---------- Search ---------- */
311 | .searchContainer {
312 | display: sticky;
313 | margin-top: 100px;
314 | background-color: rgb(255, 255, 255, 0.1);
315 | box-shadow: 0px 0px 35px 20px rgba(10, 10, 10, 0.096);
316 | border-radius: 5px;
317 | padding: 5px 12px 12px 12px;
318 | display: grid;
319 | grid-template-columns: 70% 30%;
320 | }
321 |
322 | .searchField {
323 | border-bottom: solid 1px whitesmoke;
324 | border-radius: 0;
325 | padding-left: 12px;
326 | }
327 |
328 | .prioritySelect {
329 | grid-column-start: 2;
330 | border-bottom: solid 1px whitesmoke;
331 | border-radius: 0;
332 | -webkit-appearance-select: none;
333 | }
334 |
335 | .searchResults {
336 | grid-column-start: span 3;
337 | }
338 |
339 | #newChromogenLogo {
340 | display: flex;
341 | justify-content: center;
342 | width: 400px;
343 | height: 300px;
344 | }
345 |
346 | select {
347 | -webkit-appearance-select: none;
348 | /* background: url("data:image/svg+xml;utf8, ")
349 | no-repeat; */
350 | }
351 |
352 | /* */
353 |
354 | .w-tc-editor[data-color-mode*='dark'],
355 | [data-color-mode*='dark'] .w-tc-editor,
356 | [data-color-mode*='dark'] .w-tc-editor-var,
357 | body[data-color-mode*='dark'] {
358 | --color-fg-default: #ddd;
359 | --color-canvas-subtle: #161b22;
360 | --color-prettylights-syntax-comment: #818c97;
361 | --color-prettylights-syntax-entity-tag: #ed81b0;
362 | --color-prettylights-syntax-entity: #d2a8ff;
363 | --color-prettylights-syntax-sublimelinter-gutter-mark: #ddd;
364 | --color-prettylights-syntax-constant: #ed8876;
365 | --color-prettylights-syntax-string: #68afc8;
366 | --color-prettylights-syntax-keyword: #ed81b0;
367 | --color-prettylights-syntax-markup-bold: #c9d1d9;
368 | }
369 |
--------------------------------------------------------------------------------
/demo-zustand-todo/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | entry: path.resolve(__dirname, './src/index.js'),
5 | output: {
6 | filename: 'bundle.js',
7 | },
8 | devServer: {
9 | contentBase: path.resolve(__dirname, './src'),
10 | historyApiFallback: true,
11 | },
12 | mode: process.env.NODE_ENV,
13 | module: {
14 | rules: [
15 | {
16 | test: /\.(js|jsx)$/,
17 | exclude: /node_modules/,
18 | loader: 'babel-loader',
19 | options: {
20 | presets: ['@babel/preset-env', '@babel/preset-react'],
21 | plugins: ['@babel/transform-runtime'],
22 | },
23 | },
24 | {
25 | test: /\.css$/,
26 | use: [
27 | {
28 | loader: 'style-loader',
29 | },
30 | {
31 | loader: 'css-loader',
32 | },
33 | ],
34 | },
35 | {
36 | test: /\.(png|jpg)$/,
37 | use: ['file-loader', 'url-loader?limit=8192'],
38 | },
39 | ],
40 | },
41 | resolve: {
42 | extensions: ['.js', '.jsx'],
43 | },
44 | };
45 |
--------------------------------------------------------------------------------
/jenkins/Jenkinsfile:
--------------------------------------------------------------------------------
1 | pipeline {
2 | agent {
3 | docker {
4 | image 'node:lts-buster-slim'
5 | args '-p 3003:3003'
6 | }
7 | }
8 | environment {
9 | CI = 'true'
10 | }
11 | stages {
12 | stage('Build') {
13 | steps {
14 | sh 'npm --prefix ./package install'
15 | }
16 | }
17 | stage('Test') {
18 | steps {
19 | sh './jenkins/scripts/test.sh'
20 | }
21 | }
22 | stage('Deliver') {
23 | steps {
24 | sh './jenkins/scripts/deliver.sh'
25 | input message: 'Finished using the web site? (Click "Proceed" to continue)'
26 | sh './jenkins/scripts/kill.sh'
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/jenkins/scripts/deliver.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | echo 'The following "npm" command builds your Node.js/React application for'
4 | echo 'production in the local "build" directory (i.e. within the'
5 | echo '"/var/jenkins_home/workspace/simple-node-js-react-app" directory),'
6 | echo 'correctly bundles React in production mode and optimizes the build for'
7 | echo 'the best performance.'
8 | set -x
9 | npm run build
10 | set +x
11 |
12 | echo 'The following "npm" command runs your Node.js/React application in'
13 | echo 'development mode and makes the application available for web browsing.'
14 | echo 'The "npm start" command has a trailing ampersand so that the command runs'
15 | echo 'as a background process (i.e. asynchronously). Otherwise, this command'
16 | echo 'can pause running builds of CI/CD applications indefinitely. "npm start"'
17 | echo 'is followed by another command that retrieves the process ID (PID) value'
18 | echo 'of the previously run process (i.e. "npm start") and writes this value to'
19 | echo 'the file ".pidfile".'
20 | set -x
21 | npm --prefix ./package run symlink &
22 | sleep 1
23 | echo $! > .pidfile
24 | set +x
25 |
26 | echo 'Now...'
27 | echo 'Visit http://localhost:3003 to see your Node.js/React application in action.'
28 | echo '(This is why you specified the "args ''-p 3003:3003''" parameter when you'
29 | echo 'created your initial Pipeline as a Jenkinsfile..)'
30 |
--------------------------------------------------------------------------------
/jenkins/scripts/kill.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | echo 'The following command terminates the "npm start" process using its PID'
4 | echo '(written to ".pidfile"), all of which were conducted when "deliver.sh"'
5 | echo 'was executed.'
6 | set -x
7 | kill $(cat .pidfile)
8 |
--------------------------------------------------------------------------------
/jenkins/scripts/test.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | echo 'The following "npm" command (if executed) installs the "cross-env"'
4 | echo 'dependency into the local "node_modules" directory, which will ultimately'
5 | echo 'be stored in the Jenkins home directory. As described in'
6 | echo 'https://docs.npmjs.com/cli/install, the "--save-dev" flag causes the'
7 | echo '"cross-env" dependency to be installed as "devDependencies". For the'
8 | echo 'purposes of this tutorial, this flag is not important. However, when'
9 | echo 'installing this dependency, it would typically be done so using this'
10 | echo 'flag. For a comprehensive explanation about "devDependencies", see'
11 | echo 'https://stackoverflow.com/questions/18875674/whats-the-difference-between-dependencies-devdependencies-and-peerdependencies.'
12 | set -x
13 | npm install --save-dev cross-env
14 | set +x
15 |
16 | echo 'The following "npm" command tests that your simple Node.js/React'
17 | echo 'application renders satisfactorily. This command actually invokes the test'
18 | echo 'runner Jest (https://facebook.github.io/jest/).'
19 | set -x
20 | npm --prefix ./package run test
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chromogen-root",
3 | "version": "5.0.1",
4 | "description": "simple, interaction-driven test generator for Recoil and Zustand apps",
5 | "scripts": {
6 | "ci-all": "(npm ci); (cd ./package && npm ci); (cd ./dev-tool && npm ci); (cd ./demo-todo && npm ci);",
7 | "build": "echo \"ERROR! Nothing to build in root directory.\n\""
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/oslabs-beta/Chromogen.git"
12 | },
13 | "contributors": [
14 | {
15 | "name": "Brach Burdick",
16 | "url": "https://github.com/sirbrachthepale/"
17 | },
18 | {
19 | "name": "Francois Denavaut",
20 | "url": "https://github.com/dnvt/"
21 | },
22 | {
23 | "name": "Maggie Kwan",
24 | "url": "https://github.com/maggiekwan/"
25 | },
26 | {
27 | "name": "Lawrence Liang",
28 | "url": "https://github.com/Lawliang/"
29 | },
30 | {
31 | "name": "Michelle Holland",
32 | "url": "https://github.com/michellebholland/"
33 | },
34 | {
35 | "name": "Jim Chen",
36 | "url": "https://github.com/chenchingk"
37 | },
38 | {
39 | "name": "Andy Wang",
40 | "url": "https://github.com/andywang23"
41 | },
42 | {
43 | "name": "Connor Rose Delisle",
44 | "url": "https://github.com/connorrose"
45 | },
46 | {
47 | "name": "Amy Yee",
48 | "url": "https://github.com/amyy98"
49 | },
50 | {
51 | "name": "Cameron Greer",
52 | "url": "https://github.com/cgreer011"
53 | },
54 | {
55 | "name": "Jinseon Shin",
56 | "url": "https://github.com/wlstjs"
57 | },
58 | {
59 | "name": "Nicholas Shay",
60 | "url": "https://github.com/nicholasjs"
61 | },
62 | {
63 | "name": "Ryan Tumel",
64 | "url": "https://github.com/rtumel123"
65 | },
66 | {
67 | "name": "Marcellies Pettiford",
68 | "url": "https://github.com/mp-04"
69 | },
70 | {
71 | "name": "Sung Kim",
72 | "url": "https://github.com/smk53664"
73 | },
74 | {
75 | "name": "Lina Lee",
76 | "url": "https://github.com/lina4lee"
77 | },
78 | {
79 | "name": "Erica Oh",
80 | "url": "https://github.com/ericaysoh"
81 | },
82 | {
83 | "name": "Dani Almaraz",
84 | "url": "https://github.com/dtalmaraz"
85 | },
86 | {
87 | "name": "Craig Boswell",
88 | "url": "https://github.com/crgb0s"
89 | },
90 | {
91 | "name": "Hussein Ahmed",
92 | "url": "https://github.com/Hali3030"
93 | },
94 | {
95 | "name": "Ian Kila",
96 | "url": "https://github.com/iannkila"
97 | },
98 | {
99 | "name": "Yuehao Wong",
100 | "url": "https://github.com/yuehaowong"
101 | }
102 | ],
103 | "license": "MIT",
104 | "bugs": {
105 | "url": "https://github.com/oslabs-beta/Chromogen/issues"
106 | },
107 | "homepage": "https://github.com/oslabs-beta/Chromogen#readme",
108 | "dependencies": {
109 | "@uiw/react-textarea-code-editor": "^2.1.1",
110 | "dependency-cruiser": "^12.9.0",
111 | "react": "^18.0.0",
112 | "react-dom": "^18.0.0",
113 | "recoil": "^0.7.2",
114 | "redux": "^4.0.5",
115 | "styled-components": "^5.3.6",
116 | "zustand": "^4.1.1"
117 | },
118 | "devDependencies": {
119 | "@babel/core": "^7.11.6",
120 | "@babel/preset-env": "^7.11.5",
121 | "@babel/preset-react": "^7.10.4",
122 | "@babel/preset-typescript": "^7.10.4",
123 | "@testing-library/react": "^13.1.1",
124 | "@types/node": "^14.11.2",
125 | "@types/react": "^18.0.6",
126 | "@types/react-dom": "^18.0.2",
127 | "@types/styled-components": "^5.1.26",
128 | "babel-jest": "^26.3.0",
129 | "coveralls": "^3.1.0",
130 | "css-loader": "^6.7.3",
131 | "eslint-config-airbnb-typescript": "^17.0.0",
132 | "jest": "^26.4.2",
133 | "react-test-renderer": "^18.0.0",
134 | "typescript": "^4.0.3"
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/package/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 OSLabs Beta
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package/README.md:
--------------------------------------------------------------------------------
1 |
2 |
Chromogen
3 |
4 |
10 |
11 |
12 |
A UI-driven Jest test-generation package for Recoil.js selectors and Zustand store hooks.
13 |
14 |
15 |
16 | [](https://www.npmjs.com/package/chromogen)
17 | [](https://github.com/open-source-labs/Chromogen/blob/master/LICENSE)
18 |
19 |
20 |
21 |
22 |
23 | **Now Compatible with React V18**
24 |
25 | Chromogen (Now on Version 4.0) is a Jest unit-test generation tool for Zustand Stores and Recoil selectors. It captures state changes during user interaction and auto-generates corresponding test suites. Simply launch your application after following the installation instructions below, interact as a user normally would, and with one click you can download a ready-to-run Jest test file. Alternatively, you can copy the generated tests straight to your clipboard.
26 |
27 |
28 | ## Installation for Zustand Apps
29 |
30 | Before using Chromogen, you'll need to make two changes to your application:
31 |
32 | 1. Import the ` ` component and render it alongside any other components in ` `
33 | 2. Import `chromogenZustandMiddleware` function from Chromogen. This will be used as middleware when setting up your store.
34 |
35 | ### Import the ChromogenZustandObserver component
36 |
37 | Import `ChromogenZustandObserver`. ChromogenZustandObserver can be rendered alongside any other components in ` `.
38 |
39 | ```jsx
40 | import React from 'react';
41 | import { ChromogenZustandObserver } from 'chromogen';
42 | import TodoList from './TodoList';
43 |
44 | const App = () => (
45 | <>
46 |
47 |
48 | >
49 | );
50 |
51 | export default App;
52 | ```
53 |
54 | Import `chromogenZustandMiddleware`. When you call create, wrap your store function with chromogenZustandMiddleware. **Note**, when using chromogenZustandMiddleware, you'll need to provide some additional arguments into the set function.
55 |
56 | 1. _Overwrite State_ (boolean) - Without middleware, this defaults to `false`, but you'll need to explicitly provide a value when using Chromogen.
57 | 2. _Action Name_ - Used for test generation
58 | 3. _Action Parameters_ - If the action requires input parameters, pass these in after the Action Name.
59 |
60 | ```jsx
61 | import { chromogenZustandMiddleware } from 'chromogen';
62 | import create from 'zustand';
63 |
64 | const useStore = create(
65 | chromogenZustandMiddleware((set) => ({
66 | counter: 0,
67 | color: 'black',
68 | prioritizeTask: ['walking', 5],
69 | addCounter: () => set(() => ({ counter: (counter += 1) }), false, 'addCounter'),
70 | changeColor: (newColor) => set(() => ({ color: newColor }), false, 'changeColor', newColor),
71 | setTaskPriority: (task, priority) =>
72 | set(() => ({ prioritizeTask: [task, priority] }), false, 'setTaskPriority', task, priority),
73 | })),
74 | );
75 |
76 | export default useStore;
77 | ```
78 |
79 |
80 |
81 | ## Installation for Recoil Apps
82 |
83 | Before running Chromogen, you'll need to make two changes to your application:
84 |
85 | 1. Import the ` ` component as a child of ` `
86 | 1. Import the `atom` and `selector` functions from Chromogen instead of Recoil
87 |
88 | Note: These changes do have a small performance cost, so they should be reverted before deploying to production.
89 |
90 |
91 |
92 | ### Import the ChromogenObserver component
93 |
94 | ChromogenObserver should be included as a direct child of RecoilRoot. It does not need to wrap any other components, and it takes no mandatory props. It utilizes Recoil's TransactionObserver Hook to record snapshots on state change.
95 |
96 | ```jsx
97 | import React from 'react';
98 | import { RecoilRoot } from 'recoil';
99 | import { ChromogenObserver } from 'chromogen';
100 | import MyComponent from './components/MyComponent.jsx';
101 |
102 | const App = (props) => (
103 |
104 |
105 |
106 |
107 | );
108 |
109 | export default App;
110 | ```
111 |
112 | If you are using pseudo-random key names, such as with _UUID_, you'll need to pass all of your store exports to the ChromogenObserver component as a `store` prop. This will allow Chromogen to use source code variable names in the output file, instead of relying on keys. When all atoms and selectors are exported from a single file, you can pass the imported module directly:
113 |
114 | ```jsx
115 | import * as store from './store';
116 | // ...
117 | ;
118 | ```
119 |
120 | If your store utilizes seprate files for various pieces of state, you can pass all of the imports in an array:
121 |
122 | ```jsx
123 | import * as atoms from './store/atoms';
124 | import * as selectors from './store/selectors';
125 | import * as misc from './store/arbitraryRecoilState';
126 | // ...
127 | ;
128 | ```
129 |
130 |
131 |
132 | ### Import atom & selector functions from Chromogen
133 |
134 | Wherever you import `atom` and/or `selector` functions from Recoil (typically in your `store` file), import them from Chromogen instead. The arguments passed in do **not** need to change in any away, and the return value will still be a normal RecoilAtom or RecoilSelector. Chromogen wraps the native Recoil functions to track which pieces of state have been created, as well as when various selectors are called and what values they return.
135 |
136 | ```js
137 | import { atom, selector } from 'chromogen';
138 |
139 | export const fooState = atom({
140 | key: 'fooState',
141 | default: {},
142 | });
143 |
144 | export const barState = selector({
145 | key: 'barState',
146 | get: ({ get }) => {
147 | const derivedState = get(fooState);
148 | return derivedState.baz || 'value does not exist';
149 | },
150 | });
151 | ```
152 |
153 |
154 |
155 | ## Usage for All Apps
156 |
157 | After following the installation steps above, launch your application as normal. You should see two buttons in the bottom left corner.
158 |
159 |
160 |
161 | 
162 |
163 |
164 |
165 | The pause button on the left is the **pause recording** button. Clicking it will pause recording, so that no tests are generated during subsequent state changes. Pausing is useful for setting up a complex initial state with repetitive actions, where you don't want to test every step of the process.
166 |
167 | The button in the middle is the **download** button. Clicking it will download a new test file that includes _all_ tests generated since the app was last launched or refreshed.
168 |
169 | The button on the right is the **copy-to-clipboard** button. Clicking it will copy your tests, including _all_ tests generated since the app was last launched or refreshed.
170 |
171 | Once you've recorded all the interactions you want to test, click the pause button and then the download button to generate the test file or press copy to copy to your clipboard. You can now drag-and-drop the downloaded file into your app's test directory or paste the code in your new file. **Don't forget to add the source path in your test file**
172 |
173 | You're now ready to run your tests! After running your normal Jest test command, you should see a test suite for `chromogen.test.js`.
174 |
175 | The current tests check whether state has changed after an interaction and checks whether the resulting state change variables have been updated as expected.
176 |
177 |
178 |
179 | Please visit our [main repo](https://github.com/open-source-labs/Chromogen) for more detailed instructions, as well as any bug reports, support issues, or feature requests.
180 |
--------------------------------------------------------------------------------
/package/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | ['@babel/preset-env', { targets: { node: 'current' } }],
4 | '@babel/preset-react',
5 | '@babel/preset-typescript',
6 | ],
7 | };
8 |
--------------------------------------------------------------------------------
/package/index.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import { atom, selector, atomFamily, selectorFamily } from './recoil_generator/src/api/api';
3 | import { ChromogenZustandObserver } from './zustand_generator/src/component/ChromogenZustandObserver';
4 | import { ChromogenObserver } from './recoil_generator/src/component/ChromogenObserver';
5 | import { chromogenZustandMiddleware } from './zustand_generator/src/api/api';
6 | import Editor from './zustand_generator/src/component/Editor';
7 |
8 | // CHROMGOEN FAMILY APIs ARE CURRENTLY UNSTABLE
9 | export {
10 | atom,
11 | selector,
12 | atomFamily,
13 | selectorFamily,
14 | ChromogenObserver,
15 | chromogenZustandMiddleware,
16 | ChromogenZustandObserver,
17 | Editor,
18 | };
19 |
--------------------------------------------------------------------------------
/package/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chromogen",
3 | "version": "5.0.1",
4 | "description": "simple, interaction-driven Jest test generator for Recoil and React Hooks apps",
5 | "main": "build/index.js",
6 | "keywords": [
7 | "react",
8 | "recoil",
9 | "jest",
10 | "testing"
11 | ],
12 | "files": [
13 | "build"
14 | ],
15 | "scripts": {
16 | "prepublishOnly": "npm run build",
17 | "build": "tsc",
18 | "test": "jest --verbose --coverage",
19 | "localUpdate": "tsc && npm --prefix ../demo-zustand-todo run update",
20 | "tarballUpdate": "npm --prefix ../demo-zustand-todo run symlink",
21 | "coveralls": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js"
22 | },
23 | "repository": {
24 | "type": "git",
25 | "url": "https://github.com/open-source-labs/Chromogen.git"
26 | },
27 | "contributors": [
28 | {
29 | "name": "Brach Burdick",
30 | "url": "https://github.com/sirbrachthepale/"
31 | },
32 | {
33 | "name": "Francois Denavaut",
34 | "url": "https://github.com/dnvt/"
35 | },
36 | {
37 | "name": "Maggie Kwan",
38 | "url": "https://github.com/maggiekwan/"
39 | },
40 | {
41 | "name": "Lawrence Liang",
42 | "url": "https://github.com/Lawliang/"
43 | },
44 | {
45 | "name": "Michelle Holland",
46 | "url": "https://github.com/michellebholland/"
47 | },
48 | {
49 | "name": "Jim Chen",
50 | "url": "https://github.com/chenchingk"
51 | },
52 | {
53 | "name": "Andy Wang",
54 | "url": "https://github.com/andywang23"
55 | },
56 | {
57 | "name": "Connor Rose Delisle",
58 | "url": "https://github.com/connorrose"
59 | },
60 | {
61 | "name": "Amy Yee",
62 | "url": "https://github.com/amyy98"
63 | },
64 | {
65 | "name": "Cameron Greer",
66 | "url": "https://github.com/cgreer011"
67 | },
68 | {
69 | "name": "Jinseon Shin",
70 | "url": "https://github.com/wlstjs"
71 | },
72 | {
73 | "name": "Nicholas Shay",
74 | "url": "https://github.com/nicholasjs"
75 | },
76 | {
77 | "name": "Ryan Tumel",
78 | "url": "https://github.com/rtumel123"
79 | },
80 | {
81 | "name": "Marcellies Pettiford",
82 | "url": "https://github.com/mp-04"
83 | },
84 | {
85 | "name": "Sung Kim",
86 | "url": "https://github.com/smk53664"
87 | },
88 | {
89 | "name": "Lina Lee",
90 | "url": "https://github.com/lina4lee"
91 | },
92 | {
93 | "name": "Erica Oh",
94 | "url": "https://github.com/ericaysoh"
95 | },
96 | {
97 | "name": "Dani Almaraz",
98 | "url": "https://github.com/dtalmaraz"
99 | },
100 | {
101 | "name": "Craig Boswell",
102 | "url": "https://github.com/crgb0s"
103 | },
104 | {
105 | "name": "Hussein Ahmed",
106 | "url": "https://github.com/Hali3030"
107 | },
108 | {
109 | "name": "Ian Kila",
110 | "url": "https://github.com/iannkila"
111 | },
112 | {
113 | "name": "Yuehao Wong",
114 | "url": "https://github.com/yuehaowong"
115 | }
116 | ],
117 | "license": "MIT",
118 | "bugs": {
119 | "url": "https://github.com/open-source-labs/Chromogen/issues"
120 | },
121 | "homepage": "https://github.com/open-source-labs/Chromogen#readme",
122 | "peerDependencies": {
123 | "jest": ">=24.0.0",
124 | "typescript": ">=3.8.0"
125 | },
126 | "dependencies": {
127 | "@uiw/react-textarea-code-editor": "^2.1.1",
128 | "dependency-cruiser": "^12.9.0",
129 | "react": "^18.0.0",
130 | "react-dom": "^18.0.0",
131 | "recoil": "^0.7.2",
132 | "redux": "^4.0.5",
133 | "styled-components": "^5.3.6",
134 | "zustand": "^4.1.1"
135 | },
136 | "devDependencies": {
137 | "@babel/core": "^7.11.6",
138 | "@babel/preset-env": "^7.11.5",
139 | "@babel/preset-react": "^7.10.4",
140 | "@babel/preset-typescript": "^7.10.4",
141 | "@testing-library/react": "^13.1.1",
142 | "@types/node": "^14.11.2",
143 | "@types/react": "^18.0.6",
144 | "@types/react-dom": "^18.0.2",
145 | "@types/styled-components": "^5.1.26",
146 | "babel-jest": "^26.3.0",
147 | "coveralls": "^3.1.0",
148 | "css-loader": "^6.7.3",
149 | "eslint-config-airbnb-typescript": "^17.0.0",
150 | "jest": "^26.6.3",
151 | "react-test-renderer": "^18.0.0",
152 | "typescript": "^4.0.3"
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/package/recoil_generator/__tests__/api.test.js:
--------------------------------------------------------------------------------
1 | import { ledger } from '../src/utils/ledger.ts';
2 | import { atom, selector, selectorFamily, atomFamily } from '../src/api/api.ts';
3 |
4 | // testing the atom
5 | describe('atom', () => {
6 | // destructuring atoms from ledger interface in utils folder
7 | const { atoms } = ledger;
8 | it('is a function', () => {
9 | expect(typeof atom).toBe('function');
10 | });
11 |
12 | it('should update ledger upon invocation', () => {
13 | // creating a mock atom
14 | atom({
15 | key: 'exampleAtom',
16 | default: false,
17 | });
18 | // verifying atoms property (array) on ledger has been updated with input atom
19 | expect(atoms).toHaveLength(1);
20 | });
21 |
22 | it('should create Recoil atom with correct key name', () => {
23 | // verifying that input atom key matches 'exampleAtom'
24 | expect(atoms[0]).toHaveProperty('key', 'exampleAtom');
25 | });
26 | });
27 |
28 | describe('selector', () => {
29 | // destructuring selectors from ledger object in utils folder
30 | const { selectors } = ledger;
31 | const test = true;
32 |
33 | it('is a function', () => {
34 | // verify selector is a function
35 | expect(typeof selector).toBe('function');
36 | });
37 |
38 | it('should update ledger upon invocation', () => {
39 | // creating a mock selector with key, get, set
40 | selector({
41 | key: 'exampleSelector',
42 | get: () => 'getMethod',
43 | set: () => 'setMethod',
44 | });
45 | // verify selectors property in ledger has been updated with mock selector
46 | expect(selectors).toHaveLength(1);
47 | });
48 | // verifying that input selector key matches 'exampleSelector'
49 | it('should capture correct key name', () => {
50 | expect(selectors[0]).toEqual('exampleSelector');
51 | });
52 |
53 | xit('should return an object if an input condition evaluates to true', () => {
54 | // verify that selector (recoilSelector in this context) invocation returns an object
55 | expect(typeof selector(test)).toBe('object');
56 | });
57 | });
58 |
59 | describe('atomFamily', () => {
60 | it('should return a function', () => {
61 | // create a mock atomFamily
62 | const familyFactory = atomFamily({
63 | key: 'familyKey',
64 | default: (param) => param.toString(),
65 | });
66 | // verify that familyFactory is a function
67 | expect(typeof familyFactory).toEqual('function');
68 | });
69 | });
70 |
71 | describe('selectorFamily', () => {
72 | // truthy parameter
73 | const test = true;
74 | it('should return a function', () => {
75 | // create a mock selectorFamily
76 | const familyFactory = selectorFamily({
77 | key: 'familyKey',
78 | get: () => () => 'some value',
79 | set: () => () => undefined,
80 | default: (param) => param.toString(),
81 | });
82 | // verify that familyFactory is a function
83 | expect(typeof familyFactory).toEqual('function');
84 | });
85 | it('should return an object if an input condition evaluates to true', () => {
86 | // verify that selectorFamily (recoilSelectorFamily in this context) invocation returns an object
87 | expect(typeof selectorFamily(test)).toBe('function');
88 | });
89 | });
90 |
--------------------------------------------------------------------------------
/package/recoil_generator/__tests__/component-utils.test.js:
--------------------------------------------------------------------------------
1 | import { ledger } from '../src/utils/ledger.ts';
2 | import {generateFile} from '../src/component/component-utils';
3 |
4 | // Testing generateFile
5 | xdescribe('generateFile', () => {
6 | const setFile = 0;
7 | const array = [[], [], []];
8 | let storeMap = new Map(array);
9 |
10 | const {
11 | atoms,
12 | selectors,
13 | setters,
14 | atomFamilies,
15 | selectorFamilies,
16 | initialRender,
17 | initialRenderFamilies,
18 | transactions,
19 | setTransactions,
20 | } = ledger;
21 |
22 | // We expect our generate file to not be the falsy return statement, which is the entirety of the ledger, with atoms being the new user input
23 | generateFile(setFile, storeMap);
24 | });
25 |
--------------------------------------------------------------------------------
/package/recoil_generator/__tests__/component.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { RecoilRoot, useRecoilState } from 'recoil';
3 | import { render } from '@testing-library/react';
4 | import { ChromogenObserver } from '../src/component/ChromogenObserver.tsx';
5 | import { ledger } from '../src/utils/ledger.ts';
6 | import { atom } from '../src/api/api.ts';
7 |
8 | // import {shallow} from 'enzyme';
9 | // import {mount} from 'enzyme';
10 |
11 | describe('chromogenObserver', () => {
12 | global.URL = {
13 | createObjectURL: () => 'http://mockURL.com',
14 | };
15 |
16 | beforeEach(() => {
17 | console.error = jest.fn();
18 | // creating a mockAtom
19 | const mockAtom = atom({ key: 'mockAtom', default: true });
20 | // create a functional mockComponent
21 | const MockComponent = () => {
22 | // declaring a React Hook using mockAtom as recoilState
23 | const [mock, setMock] = useRecoilState(mockAtom);
24 | // render a mock-button that toggles mock recoilState onclick
25 | return setMock(!mock)} />;
26 | };
27 |
28 | render(
29 |
30 |
31 |
32 | ,
33 | );
34 | });
35 |
36 | // ChromogenObserver lines 25-36
37 | it('should relay messages to DevTool', () => {
38 |
39 | // expect(window.addEventListener('message', 'connectChromogen')).toBeTruthy()
40 |
41 | // expect(window.addEventListener('message', 'downloadFile')).toBeTruthy()
42 |
43 | // expect(window.addEventListener('message', 'toggleRecord')).toBeTruthy()
44 | });
45 |
46 | // ChromogenObserver lines 56-80
47 | // Store contains atoms and selectors
48 | it('should update storeMap with all items from passed in store', () => {
49 |
50 | })
51 |
52 | // ChromogenObserver lines 104-117
53 | it('should update atomFamilyState', () => {
54 |
55 | })
56 |
57 | // ChromogenObserver lines 142-145
58 | it('should ', () => {
59 |
60 | })
61 |
62 | // ChromogenObserver lines 154-155
63 | it('should change button color on mouse enter/leave', () => {
64 |
65 | })
66 |
67 | it('should render a download link', () => {
68 | // verify that download Chromogen tests link exists and is being rendered
69 | expect(document.getElementById('chromogen-download')).toBeTruthy();
70 | });
71 |
72 | it('should render two buttons by default', () => {
73 | // verify that generate-file and record buttons are being rendered
74 | expect(document.getElementById('chromogen-generate-file')).toBeTruthy();
75 | expect(document.getElementById('chromogen-toggle-record')).toBeTruthy();
76 | });
77 |
78 | xit('should create a file URL on button click', () => {
79 | // invoking a click on generate-file button
80 | document.getElementById('chromogen-generate-file').click();
81 | // declaring a const downloadLink referencing hidden download link
82 | const downloadLink = document.getElementById('chromogen-download');
83 | // verify that download link has a URL that is being referenced
84 | expect(downloadLink.getAttribute('href')).toBeTruthy();
85 | });
86 |
87 | it('should create transactions when state updates', () => {
88 | // invoking a click to check that state in mock-button is being toggled (in the state mock)
89 | document.getElementById('mock-button').click();
90 |
91 | // Using Promise to get around async nature of Recoil transactions
92 | expect(
93 | // verifying that resolve (mockData) has been added to ledger.transactions
94 | new Promise((resolve) => setTimeout(() => resolve(ledger.transactions), 100)),
95 | // verify that resolve property on promise object has been updated
96 | ).resolves.toHaveLength(1);
97 | });
98 | });
99 |
--------------------------------------------------------------------------------
/package/recoil_generator/__tests__/core-utils.test.jx:
--------------------------------------------------------------------------------
1 | import {
2 | debouncedAddToTransactions,
3 | wrapGetter,
4 | wrapSetter,
5 | } from '../src/api/core-utils';
6 |
7 | import { debounce } from '../src/utils/utils';
8 |
9 | xdescribe('debouncedAddToTransaction', () => {
10 |
11 | });
12 |
13 | xdescribe('wrapGetter', () => {
14 |
15 | });
16 |
17 | xdescribe('wrapSetter', () => {
18 |
19 | });
--------------------------------------------------------------------------------
/package/recoil_generator/__tests__/output-utils.test.js:
--------------------------------------------------------------------------------
1 | import {
2 | initializeAtoms,
3 | assertState,
4 | testSelectors,
5 | testSetters,
6 | importRecoilFamily,
7 | atomFamilyHook,
8 | //writeableHook,
9 | readableHook,
10 | } from '../src/output/output-utils.ts';
11 |
12 | // testing ternary operator in initializeAtoms helper function
13 | describe('initializeAtoms', () => {
14 | // create mock atomUpdate object, follows AtomUpdate interface
15 | const atomUpdate = {
16 | key: 'testAtom',
17 | value: 2,
18 | previous: 1,
19 | updated: true,
20 | };
21 |
22 | it('should set correct atom value if current is true', () => {
23 | // create variable to hold evaluated result of invoking initializeAtoms on the mock array with a parameter of true
24 | const returnString = initializeAtoms([atomUpdate], true);
25 | // verify that returnString contains key property of a string and contains a value property on the atomUpdate object (since true was passed into 'initializeAtoms')
26 | expect(returnString).toEqual(
27 | expect.stringContaining(`result.current.set${atomUpdate.key}(${atomUpdate.value})`),
28 | );
29 | });
30 |
31 | it('should set correct atom value if current is false', () => {
32 | // create variable to hold evaluated result of invoking initializeAtoms on the mock array with a parameter of false
33 | const returnString = initializeAtoms([atomUpdate], false);
34 | // verify that returnString contains key property of a string and contains a value property on the atomUpdate object (since false was passed into 'initializeAtoms')
35 | expect(returnString).toEqual(
36 | expect.stringContaining(`result.current.set${atomUpdate.key}(${atomUpdate.previous})`),
37 | );
38 | });
39 | });
40 |
41 | describe('assertState', () => {
42 | // create mock selectors array, follows SelectorUpdate interface
43 | const selectorUpdates = [
44 | {
45 | key: 'testSelector1',
46 | value: true,
47 | },
48 | {
49 | key: 'testSelector2',
50 | value: 100,
51 | },
52 | ];
53 |
54 | it('should assert on each selector value', () => {
55 | // create variable to hold evaluated result of invoking assertState on mock array
56 | const returnString = assertState(selectorUpdates);
57 | // verify that output test contains a string checking that the object's key equals a stringified version of its value on the same object
58 | expect(returnString).toEqual(
59 | expect.stringContaining(
60 | `expect(result.current.${selectorUpdates[0].key}Value).toStrictEqual(${JSON.stringify(
61 | selectorUpdates[0].value,
62 | )});`,
63 | ),
64 | );
65 |
66 | expect(returnString).toEqual(
67 | expect.stringContaining(
68 | `expect(result.current.${selectorUpdates[1].key}Value).toStrictEqual(${JSON.stringify(
69 | selectorUpdates[1].value,
70 | )});`,
71 | ),
72 | );
73 | });
74 | });
75 |
76 | describe('importRecoilFamily', () => {
77 | const familyObj = {
78 | familyName: 'string',
79 | atomName: 'test',
80 | };
81 | it('should return a string with an object as its parameter', () => {
82 | expect(typeof importRecoilFamily(familyObj)).toBe('string');
83 | });
84 | });
85 |
86 | describe('readableHook', () => {
87 | const keyArray = ['one', 'two', 'chromogen'];
88 | it('should return a string', () => {
89 | expect(typeof readableHook(keyArray)).toBe('string');
90 | });
91 | });
92 |
93 | // describe('writeableHook', () => {
94 | // const keyArray = ['chromo', 'gen', 'chromogen'];
95 | // it('should return a string', () => {
96 | // expect(typeof writeableHook(keyArray)).toBe('string');
97 | // });
98 | // });
99 |
100 | describe('testSelectors', () => {
101 | it('should scrub special characters from key names', () => {
102 | // create instance of invoking testSelectors on mock array that follows the Transaction interface
103 | const returnString = testSelectors([
104 | {
105 | state: [
106 | {
107 | key: 'atom1',
108 | value: 1,
109 | previous: 2,
110 | updated: true,
111 | },
112 | ],
113 | updates: [
114 | {
115 | key: 'selector1',
116 | value: 3,
117 | },
118 | ],
119 | atomFamilyState: [
120 | {
121 | family: 'familyName1',
122 | key: 'spec!alCh@r',
123 | value: 4,
124 | updated: true,
125 | },
126 | ],
127 | familyUpdates: [
128 | {
129 | key: 'familyUpdate1',
130 | value: 5,
131 | params: 'params',
132 | },
133 | ],
134 | },
135 | ]);
136 | // verify that if key property's value is a string with special characters they will be removed
137 | expect(returnString).toEqual(expect.not.stringContaining('spec!alCh@r'));
138 | });
139 | });
140 |
141 | // covers branch test percentage in testSetters
142 | describe('testSetters', () => {
143 | // create mock array with setter object
144 | const setTransactionsArrayWithSetter = [
145 | {
146 | state: [
147 | {
148 | key: 'atom1',
149 | value: 1,
150 | previous: 0,
151 | updated: true,
152 | },
153 | ],
154 | setter: {
155 | key: 'selector1',
156 | value: 2,
157 | params: 'spec!alCh@r',
158 | },
159 | },
160 | ];
161 | // create mock array without setter object
162 | const setTransactionsArrayWithoutSetter = [
163 | {
164 | state: [
165 | {
166 | key: 'atom1',
167 | value: 1,
168 | previous: 0,
169 | updated: true,
170 | },
171 | ],
172 | },
173 | ];
174 | const truthyReturnString = testSetters(setTransactionsArrayWithSetter);
175 | const falsyReturnString = testSetters(setTransactionsArrayWithoutSetter);
176 |
177 | it('should scrub special characters from params', () => {
178 | // verify that if params property's value is a string with special characters, they will be removed
179 | expect(truthyReturnString).toEqual(expect.not.stringContaining('spec!alCh@r'));
180 | });
181 | it('should return a string if an array is passed in', () => {
182 | // verify that a string is returned if provided an array with out a setter object
183 | expect(typeof falsyReturnString).toBe('string');
184 | });
185 | });
186 |
187 | // TEST FOR ATOMFAMILYHOOK lines 70-81
188 | //create mock transactionArray
189 | xdescribe('atomFamilyHook', () => {
190 | const transactionArray = [
191 | {
192 | atomFamilyState: [
193 | {
194 | key: 'spec!alCh@rspec!alCh@r',
195 | family: 'familyName',
196 | value: 10,
197 | updated: true,
198 | },
199 | ],
200 | familyUpdates: [
201 | {
202 | key: 'familyUpdate1',
203 | value: 5,
204 | params: 'params',
205 | },
206 | ],
207 | },
208 | ]; // truthy
209 |
210 | const transactionArray2 = []; // falsy
211 |
212 | const truthyReturnStr = atomFamilyHook(transactionArray);
213 | const falsyReturnStr = atomFamilyHook(transactionArray2);
214 |
215 | it('should scrub special characters', () => {
216 | expect(truthyReturnStr).toEqual(expect.not.stringContaining('spec!alCh@r'));
217 | });
218 |
219 | it('should return empty string when transactionsArr length is falsy', () => {
220 | expect(falsyReturnStr).toBe('');
221 | });
222 | });
223 |
--------------------------------------------------------------------------------
/package/recoil_generator/__tests__/output.test.js:
--------------------------------------------------------------------------------
1 | import { setFilter, output } from '../src/output/output.ts';
2 |
3 | // testing setFilter function
4 | describe('setFilter', () => {
5 | it('should remove setter keys from array of selector keys', () => {
6 | // create mock selectors array
7 | const selectors = ['one', 'two', 'three'];
8 | // create mock setters array
9 | const setters = ['one'];
10 | // store evaluated result of invoking setFilter on the mock data in an array
11 | const filtered = setFilter(selectors, setters);
12 | // verify that the info from setters caused a matching value to be removed from the selectors array ('one')
13 | expect(filtered).not.toContain('one');
14 | });
15 | });
16 |
17 | describe('output', () => {
18 | it('should return a string', () => {
19 | // create mock ledger object
20 | const mockLedger = {
21 | atoms: [],
22 | selectors: [],
23 | setters: [],
24 | atomFamilies: [],
25 | selectorFamilies: [],
26 | initialRender: [],
27 | initialRenderFamilies: [],
28 | transactions: [],
29 | setTransactions: [],
30 | };
31 | // verify that type of mockLedger is a string after output function is invoked on it
32 | expect(typeof output(mockLedger)).toEqual('string');
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/package/recoil_generator/__tests__/utils.test.js:
--------------------------------------------------------------------------------
1 | import { debounce, convertFamilyTrackerKeys } from '../src/utils/utils.ts';
2 |
3 | jest.useFakeTimers();
4 |
5 | describe('debounce', () => {
6 | it('should return a new function', () => {
7 | // declare mock function that returns a string
8 | const inputFunction = () => 'example';
9 | // declare a function that stores the evaluated result of invoking debounce on inputFunction with a wait time of 0s
10 | const outputFunction = debounce(inputFunction, 0);
11 | // verify that outputFunction is a function
12 | expect(typeof outputFunction).toBe('function');
13 | // verify that that the 'debounced function' (outputFunction) is different than the parameter function (inputFunction)
14 | expect(outputFunction).not.toBe(inputFunction);
15 | });
16 |
17 | it('should limit consecutive calls', () => {
18 | // increment count to 1 after 100ms
19 | let count = 0;
20 | const increment = debounce(() => {
21 | count += 1;
22 | }, 100);
23 | // invoke increment twice
24 | increment();
25 | increment();
26 | // advance timer to 101ms using a mock jest function
27 | jest.advanceTimersByTime(101);
28 | // verify that count only incremented once because it was debounced
29 | expect(count).toEqual(1);
30 | });
31 | });
32 |
33 | // testing convertFamilyTrackerKeys
34 | describe('convertFamilyTrackerKeys', () => {
35 | it('should update key names if in map', () => {
36 | // create mock tracker (object)
37 | const newTracker = convertFamilyTrackerKeys(
38 | // first parameter is an object with a property whose value is a string
39 | { keyOne: 'some value' },
40 | // second parameter is a new Map
41 | new Map([['keyOne', 'keyUpdated']]),
42 | );
43 | // verify that newTracker object includes property from Map (keyUpdated)
44 | expect(newTracker).toHaveProperty('keyUpdated');
45 | // verify that newTracker did not update key name with first parameter
46 | expect(newTracker).not.toHaveProperty('keyOne');
47 | });
48 |
49 | it('should preserve key names if not in map', () => {
50 | // create mock tracker (object)
51 | const newTracker = convertFamilyTrackerKeys(
52 | { keyOne: 'some value' },
53 | new Map([['keyTwo', 'keyNotUpdated']]),
54 | );
55 | // verify that newTracker's first parameter has been preserved
56 | expect(newTracker).toHaveProperty('keyOne');
57 | // verify that newTracker does have keyTwo in first parameter
58 | expect(newTracker).not.toHaveProperty('keyTwo');
59 | });
60 | });
61 |
--------------------------------------------------------------------------------
/package/recoil_generator/src/api/api.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import type {
3 | RecoilState,
4 | RecoilValueReadOnly,
5 | AtomOptions,
6 | ReadWriteSelectorOptions,
7 | ReadOnlySelectorOptions,
8 | SerializableParam,
9 | AtomFamilyOptions,
10 | ReadWriteSelectorFamilyOptions,
11 | ReadOnlySelectorFamilyOptions,
12 | } from 'recoil';
13 | import type { SelectorConfig, SelectorFamilyConfig } from '../types';
14 |
15 | import {
16 | selector as recoilSelector,
17 | atom as recoilAtom,
18 | atomFamily as recoilAtomFamily,
19 | selectorFamily as recoilSelectorFamily,
20 | } from 'recoil';
21 | import { wrapGetter, wrapSetter } from './core-utils';
22 | import { dummyParam } from '../utils/utils';
23 | import { ledger } from '../utils/ledger';
24 | import { wrapFamilyGetter, wrapFamilySetter } from './family-utils';
25 | /* eslint-enable */
26 |
27 | /**
28 | * If transactions.length is greater than 1, the selector is being created after the initial render
29 | * (i.e. a dynamically generated selector) and will not be tracked. Doing so would break the imports
30 | * and assertions within the output test file. Same logic is applied to new atoms.
31 | *
32 | * If get is undefined, native Async, or Babel-transpiled generator-based async (id'd via RegEx),
33 | * we don't do any injecting or tracking. Selector just gets created & returned back out.
34 | *
35 | * Otherwise, we attempt to wrap get & set methods with custom functions that log the return
36 | * value on each transaction to the corresponding ledger array.
37 | *
38 | * If get returns a promise on page load, we delete selector from the selectors array
39 | * and do not track it on subsequent calls (using "returnedPromise" flag, since we can't "un-inject").
40 | */
41 |
42 | /* ----- SELECTOR ----- */
43 | export function selector(options: ReadWriteSelectorOptions): RecoilState;
44 | export function selector(options: ReadOnlySelectorOptions): RecoilValueReadOnly;
45 | // Overload function signature
46 | export function selector(config: ReadWriteSelectorOptions | ReadOnlySelectorOptions) {
47 | const { key, get } = config;
48 | const { transactions, selectors, setters } = ledger;
49 | if (
50 | transactions.length > 0
51 | || !get
52 | || get.constructor.name === 'AsyncFunction'
53 | || get.toString().match(/^\s*return\s*_.*\.apply\(this, arguments\);$/m)
54 | ) {
55 | return recoilSelector(config);
56 | }
57 |
58 | // Wrap get method with tracking logic & update config
59 | const getter = wrapGetter(key, get);
60 | const newConfig: SelectorConfig = { key, get: getter };
61 |
62 | // Add setter to newConfig only if set method is defined
63 | if ('set' in config) {
64 | const setter = wrapSetter(key, config.set);
65 | newConfig.set = setter;
66 | setters.push(key);
67 | }
68 |
69 | // Create selector & add to ledger
70 | const trackedSelector = recoilSelector(newConfig);
71 | selectors.push(trackedSelector.key);
72 | return trackedSelector;
73 | }
74 |
75 | /* ----- ATOM ----- */
76 | export function atom(config: AtomOptions): RecoilState {
77 | const { transactions, atoms } = ledger;
78 | const newAtom = recoilAtom(config);
79 |
80 | // Can't use key-only b/c atoms must be passed to getLoadable during transaction iteration
81 | if (transactions.length === 0) atoms.push(newAtom);
82 |
83 | return newAtom;
84 | }
85 |
86 | /* ----- ATOM FAMILY ----- */
87 | export function atomFamily(
88 | config: AtomFamilyOptions,
89 | ): (params: P) => RecoilState {
90 | const { atomFamilies } = ledger;
91 | const { key } = config;
92 |
93 | // Initialize new family in atomFamilies tracker
94 | atomFamilies[key] = {};
95 |
96 | return (params: P): RecoilState => {
97 | const strParams = JSON.stringify(params);
98 | // If the atom has already been created, return from cache, otherwise we'll be creating a new
99 | // instance of an atom every time we invoke this func (which can lead to infinite re-render loop)
100 | const cachedAtom = atomFamilies[key][strParams];
101 | if (cachedAtom !== undefined) return cachedAtom;
102 |
103 | const newAtomFamilyMember = recoilAtomFamily(config)(params);
104 | // Storing every atom created except for dummy atom created by ChromogenObserver's onload useEffect hook
105 | if (strParams !== dummyParam) atomFamilies[key][strParams] = newAtomFamilyMember;
106 | return newAtomFamilyMember;
107 | };
108 | }
109 |
110 | /* ----- SELECTOR FAMILY ----- */
111 | export function selectorFamily(
112 | options: ReadWriteSelectorFamilyOptions,
113 | ): (param: P) => RecoilState;
114 | export function selectorFamily(
115 | options: ReadOnlySelectorFamilyOptions,
116 | ): (param: P) => RecoilValueReadOnly;
117 | // Overload function signature
118 | export function selectorFamily(
119 | config:
120 | | ReadWriteSelectorFamilyOptions
121 | | ReadOnlySelectorFamilyOptions,
122 | ) {
123 | const { key, get } = config;
124 | const { transactions, selectorFamilies } = ledger;
125 |
126 | // Testing whether returned function from configGet is async
127 | if (
128 | !get
129 | || transactions.length > 0
130 | || get(dummyParam).constructor.name === 'AsyncFunction'
131 | || get(dummyParam)
132 | .toString()
133 | .match(/^\s*return\s*_.*\.apply\(this, arguments\);$/m)
134 | ) {
135 | return recoilSelectorFamily(config);
136 | }
137 |
138 | const getter = wrapFamilyGetter(key, get);
139 |
140 | const newConfig: SelectorFamilyConfig = { key, get: getter };
141 |
142 | let isSettable = false;
143 |
144 | if ('set' in config) {
145 | isSettable = true;
146 | const setter = wrapFamilySetter(key, config.set);
147 | newConfig.set = setter;
148 | }
149 |
150 | // Create selector generator & add to selectorFamily for test setup
151 | const trackedSelectorFamily = recoilSelectorFamily(newConfig);
152 | selectorFamilies[key] = { trackedSelectorFamily, prevParams: new Set(), isSettable };
153 | return trackedSelectorFamily;
154 | }
155 |
--------------------------------------------------------------------------------
/package/recoil_generator/src/api/core-utils.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import { debounce } from '../utils/utils';
3 | import { ledger } from '../utils/ledger';
4 | import { recordingState } from '../utils/store';
5 | /* eslint-enable */
6 |
7 | const { transactions, initialRender, selectors, setTransactions } = ledger;
8 |
9 | const DEBOUNCE_MS = 250;
10 |
11 | // Set timeout for selector get calls
12 | const debouncedAddToTransactions = debounce(
13 | (key, value, params) =>
14 | params !== undefined
15 | ? transactions[transactions.length - 1].familyUpdates.push({ key, value, params })
16 | : transactions[transactions.length - 1].updates.push({ key, value }),
17 | DEBOUNCE_MS,
18 | );
19 |
20 | // the logic for recording selectors only when they fire
21 | // whenever get method is fired, chromogen records
22 | const wrapGetter = (key: string, get: Function) => {
23 | let returnedPromise: boolean = false;
24 |
25 | return (utils: any) => {
26 | //will return what normal recoil selector will return aka regular selector method
27 | const value = get(utils);
28 |
29 | //Checking whether value is async
30 | // Only capture selector data if currently recording (if record button has been hit)
31 | if (utils.get(recordingState)) {
32 | //making sure no transactions have been fired
33 | if (transactions.length === 0) {
34 | // Promise-validation is expensive, so we only do it once, on initial load
35 | if (typeof value === 'object' && value !== null && value.constructor.name === 'Promise') {
36 | ledger.selectors = selectors.filter((current) => current !== key);
37 | returnedPromise = true;
38 | } else {
39 | initialRender.push({ key, value });
40 | }
41 | } else if (!returnedPromise) {
42 | // Debouncing (throttling) allows TransactionObserver to push to array first
43 | // Length must be computed within debounce to correctly find last transaction
44 | // only capture meaningful function calls
45 | // when called, timer starts; if x amount of time passes and function isnt called again, it fires; if called, resets timer
46 | debouncedAddToTransactions(key, value);
47 | }
48 | }
49 |
50 | return value;
51 | };
52 | };
53 |
54 | const wrapSetter = (key: string, set: Function) => (utils: any, newValue: any) => {
55 | if (utils.get(recordingState) && setTransactions.length > 0) {
56 | // allow TransactionObserver to push to array first
57 | // Length must be computed after timeout to correctly find last transaction
58 | // this is here b/c of async stuff with useRecoilTransactionObserver
59 | setTimeout(() => {
60 | setTransactions[setTransactions.length - 1].setter = { key, newValue };
61 | }, 0);
62 | }
63 | // returns what regular selector would return (?)
64 | return set(utils, newValue);
65 | };
66 |
67 | export {debouncedAddToTransactions, wrapGetter, wrapSetter};
--------------------------------------------------------------------------------
/package/recoil_generator/src/api/family-utils.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import type { SerializableParam } from 'recoil';
3 |
4 | import { ledger } from '../utils/ledger';
5 | import { dummyParam } from '../utils/utils';
6 | import { recordingState } from '../utils/store';
7 | import { debouncedAddToTransactions } from './core-utils';
8 | /* eslint-enable */
9 |
10 | const { transactions, selectorFamilies, initialRenderFamilies, setTransactions } = ledger;
11 |
12 | export const wrapFamilyGetter = (key: string, configGet: Function) => {
13 | let returnedPromise = false;
14 |
15 | return (params: SerializableParam) => (utils: any) => {
16 | const { get } = utils;
17 | const value = configGet(params)(utils);
18 | // Only capture selector data if currently recording
19 |
20 | if (get(recordingState)) {
21 | if (transactions.length === 0) {
22 | // Promise-validation is expensive, so we only do it once, on initial load
23 | if (
24 | typeof value === 'object'
25 | && value !== null
26 | && Object.prototype.toString.call(value) === '[object Promise]'
27 | ) {
28 | delete selectorFamilies[key];
29 | returnedPromise = true;
30 | } else {
31 | initialRenderFamilies.push({ key, params, value });
32 | }
33 | } else if (!returnedPromise) {
34 | // Track every new params
35 | if (!selectorFamilies[key].prevParams.has(params)) {
36 | selectorFamilies[key].prevParams.add(params);
37 | }
38 | // Debouncing allows TransactionObserver to push to array first
39 | // Length must be computed within debounce to correctly find last transaction
40 | // Excluding dummy selector created by ChromogenObserver's onload useEffect hook
41 | if (params !== dummyParam) debouncedAddToTransactions(key, value, params);
42 | }
43 | }
44 |
45 | // Return value from original get method
46 | return value;
47 | };
48 | };
49 |
50 | export const wrapFamilySetter = (key: string, set: Function) => (params: SerializableParam) => (
51 | utils: any,
52 | newValue: any,
53 | ) => {
54 | if (utils.get(recordingState) && setTransactions.length > 0) {
55 | // allow TransactionObserver to push to array first
56 | // Length must be computed after timeout to correctly find last transaction
57 | setTimeout(() => {
58 | setTransactions[setTransactions.length - 1].setter = { key, params, newValue };
59 | }, 0);
60 | }
61 | return set(params)(utils, newValue);
62 | };
63 |
--------------------------------------------------------------------------------
/package/recoil_generator/src/component/component-utils.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import type { CSSProperties } from 'react';
3 | import type { SerializableParam } from 'recoil';
4 | import type { Ledger } from '../types';
5 |
6 | import { ledger } from '../utils/ledger';
7 | import { convertFamilyTrackerKeys } from '../utils/utils';
8 | import { output } from '../output/output';
9 | /* eslint-enable */
10 |
11 | const buttonStyle: CSSProperties = {
12 | display: 'inline-block',
13 | margin: '8px',
14 | marginLeft: '13px',
15 | padding: '0px',
16 | height: '25px',
17 | width: '65px',
18 | borderRadius: '4px',
19 | justifyContent: 'space-evenly',
20 | border: '1px',
21 | cursor: 'pointer',
22 | color: '#90d1f0',
23 | fontSize: '10px',
24 | };
25 |
26 | const divStyle: CSSProperties = {
27 | display: 'flex',
28 | position: 'absolute',
29 | bottom: '100px',
30 | left: '100px',
31 | backgroundColor: '#aaa',
32 | borderRadius: '4px',
33 | margin: 0,
34 | padding: 0,
35 | zIndex: 999999,
36 | };
37 |
38 | const playStyle: CSSProperties = {
39 | boxSizing: 'border-box',
40 | marginLeft: '25px',
41 | borderStyle: 'solid',
42 | borderWidth: '7px 0px 7px 14px',
43 | };
44 |
45 | const pauseStyle: CSSProperties = {
46 | width: '14px',
47 | height: '14px',
48 | borderWidth: '0px 0px 0px 10px',
49 | borderStyle: 'double',
50 | marginLeft: '27px',
51 | };
52 |
53 | export const styles = { buttonStyle, divStyle, playStyle, pauseStyle };
54 |
55 | /**
56 | * onclick function that generates test file & sets download URL
57 | *
58 | * Key-to-Variable name mapping is applied if storeMap has any contents
59 | * (meaning atom / selector nodes were passed as props)
60 | * Applying only at point-of-download keeps performance cost low for users who
61 | * don't need to pass nodes while creating a moderate performance hit for others
62 | * only while downloading, never while interacting with their app.
63 | */
64 | export const generateTests = (storeMap: Map): string[] => {
65 | const {
66 | atoms,
67 | selectors,
68 | setters,
69 | atomFamilies,
70 | selectorFamilies,
71 | initialRender,
72 | initialRenderFamilies,
73 | transactions,
74 | setTransactions,
75 | } = ledger;
76 |
77 | const finalLedger: Ledger =
78 | storeMap.size > 0
79 | ? {
80 | atoms: atoms.map(({ key }) => storeMap.get(key) || key),
81 | selectors: selectors.map((key) => storeMap.get(key) || key),
82 | atomFamilies: convertFamilyTrackerKeys(atomFamilies, storeMap),
83 | selectorFamilies: convertFamilyTrackerKeys(selectorFamilies, storeMap),
84 | setters: setters.map((key) => storeMap.get(key) || key),
85 | initialRender: initialRender.map(({ key, value }) => {
86 | const newKey = storeMap.get(key) || key;
87 | return { key: newKey, value };
88 | }),
89 | initialRenderFamilies: initialRenderFamilies.map(({ key, value, params }) => {
90 | const newKey = storeMap.get(key) || key;
91 | return { key: newKey, value, params };
92 | }),
93 | transactions: transactions.map(({ state, updates, atomFamilyState, familyUpdates }) => {
94 | const newState = state.map((eachAtom) => {
95 | const key = storeMap.get(eachAtom.key) || eachAtom.key;
96 | return { ...eachAtom, key };
97 | });
98 | const newUpdates = updates.map((eachSelector) => {
99 | const key = storeMap.get(eachSelector.key) || eachSelector.key;
100 | const { value } = eachSelector;
101 | return { key, value };
102 | });
103 | const newAtomFamilyState = atomFamilyState.map((eachFamAtom) => {
104 | const family = storeMap.get(eachFamAtom.family) || eachFamAtom.family;
105 | const oldKey = eachFamAtom.key;
106 | const keySuffix = oldKey.substring(eachFamAtom.family.length);
107 | const key = family + keySuffix;
108 | return { ...eachFamAtom, family, key };
109 | });
110 | const newFamilyUpdates = familyUpdates.map((eachFamSelector) => {
111 | const key = storeMap.get(eachFamSelector.key) || eachFamSelector.key;
112 | return { ...eachFamSelector, key };
113 | });
114 | return {
115 | state: newState,
116 | updates: newUpdates,
117 | atomFamilyState: newAtomFamilyState,
118 | familyUpdates: newFamilyUpdates,
119 | };
120 | }),
121 | setTransactions: setTransactions.map(({ state, setter }) => {
122 | const newState = state.map((eachAtom) => {
123 | const key = storeMap.get(eachAtom.key) || eachAtom.key;
124 | return { ...eachAtom, key };
125 | });
126 | const newSetter = setter;
127 | if (newSetter) {
128 | const { key } = newSetter;
129 | newSetter.key = storeMap.get(key) || key;
130 | }
131 | return { state: newState, setter: newSetter };
132 | }),
133 | }
134 | : { ...ledger, atoms: atoms.map(({ key }) => key) };
135 |
136 | //return setFile(URL.createObjectURL(new Blob([output(finalLedger)])));
137 | return [output(finalLedger)];
138 | };
139 |
140 | export const generateFile = (setFile: Function, storeMap: Map): string[] => {
141 | const tests = generateTests(storeMap);
142 | const blob = new Blob(tests);
143 | setFile(URL.createObjectURL(blob));
144 | return tests;
145 | };
146 |
--------------------------------------------------------------------------------
/package/recoil_generator/src/output/output.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import type { Ledger } from '../types';
3 | import type { SerializableParam } from 'recoil';
4 | import {
5 | importRecoilState,
6 | writeableHook,
7 | readableHook,
8 | returnWriteable,
9 | returnReadable,
10 | testSelectors,
11 | testSetters,
12 | importRecoilFamily,
13 | atomFamilyHook,
14 | selectorFamilyHook,
15 | returnSelectorFamily,
16 | initializeSelectors,
17 | returnAtomFamily,
18 | } from './output-utils';
19 |
20 | /* eslint-enable */
21 |
22 | /* ----- HELPERS ----- */
23 | export const setFilter = (selectors: string[], setters: string[]): string[] =>
24 | selectors.filter((key) => !setters.includes(key));
25 |
26 | /* ----- MAIN ----- */
27 | export const output = ({
28 | atoms,
29 | selectors,
30 | setters,
31 | atomFamilies,
32 | selectorFamilies,
33 | initialRender,
34 | transactions,
35 | setTransactions,
36 | }: Ledger): string =>
37 | `import { renderRecoilHook, act } from 'react-recoil-hooks-testing-library';
38 | import { useRecoilValue, useRecoilState } from 'recoil';
39 | import {
40 | ${
41 | importRecoilState(selectors)
42 | + importRecoilFamily(selectorFamilies)
43 | }
44 | } from '';
45 | import {
46 | ${
47 | importRecoilState(atoms)
48 | + importRecoilFamily(atomFamilies)
49 | }
50 | } from '';
51 |
52 | // Suppress 'Batcher' warnings from React / Recoil conflict
53 | console.error = jest.fn();
54 |
55 | // Hook to return atom/selector values and/or modifiers for react-recoil-hooks-testing-library
56 | const useStoreHook = () => {
57 | // atoms
58 | ${writeableHook(atoms)}
59 | // writeable selectors
60 | ${writeableHook(setters)}
61 | // read-only selectors
62 | ${readableHook(setFilter(selectors, setters))}
63 | // atom families
64 | ${atomFamilyHook(transactions)}
65 | // writeable selector families
66 | ${selectorFamilyHook(selectorFamilies, true)}
67 | // read-only selector families
68 | ${selectorFamilyHook(selectorFamilies, false)}
69 |
70 |
71 |
72 | return {
73 | ${
74 | returnWriteable(atoms)
75 | + returnWriteable(setters)
76 | + returnReadable(setFilter(selectors, setters))
77 | + returnAtomFamily(transactions)
78 | + returnSelectorFamily(selectorFamilies, true)
79 | + returnSelectorFamily(selectorFamilies, false)
80 | }\t};
81 | };
82 |
83 | describe('INITIAL RENDER', () => {
84 | const { result } = renderRecoilHook(useStoreHook);
85 |
86 | ${initializeSelectors(initialRender)}
87 | });
88 |
89 | describe('SELECTORS', () => {
90 | ${testSelectors(transactions)}});
91 |
92 | describe('SETTERS', () => {
93 | ${testSetters(setTransactions)}});`;
94 |
--------------------------------------------------------------------------------
/package/recoil_generator/src/types.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import type {
3 | RecoilState,
4 | RecoilValue,
5 | DefaultValue,
6 | SerializableParam,
7 | RecoilValueReadOnly,
8 | } from 'recoil';
9 | /* eslint-enable */
10 |
11 | // ----- INITIALIZING NON-IMPORTABLE RECOIL TYPES -----
12 | type ResetRecoilState = (recoilVal: RecoilState) => void;
13 |
14 | type GetRecoilValue = (recoilVal: RecoilValue) => T;
15 |
16 | type SetRecoilState = (
17 | recoilVal: RecoilState,
18 | newVal: T | DefaultValue | ((prevValue: T) => T | DefaultValue),
19 | ) => void;
20 |
21 | // ----- EXPORTING TYPES TO BE USED IN SRC/.TSX FILES -----
22 | export interface SetterUpdate {
23 | key: string;
24 | newValue: any;
25 | params?: SerializableParam;
26 | }
27 |
28 | export interface SelectorUpdate {
29 | key: string;
30 | value: any;
31 | }
32 |
33 | export interface SelectorFamilyUpdate extends SelectorUpdate {
34 | params: SerializableParam;
35 | }
36 |
37 | export interface AtomUpdate extends SelectorUpdate {
38 | previous: any;
39 | updated: boolean;
40 | }
41 |
42 | export interface AtomFamilyState {
43 | family: string;
44 | key: string;
45 | value: any;
46 | updated: boolean;
47 | }
48 |
49 | export interface Transaction {
50 | state: AtomUpdate[];
51 | updates: SelectorUpdate[];
52 | atomFamilyState: AtomFamilyState[];
53 | familyUpdates: SelectorFamilyUpdate[];
54 | }
55 |
56 | export interface SetTransaction {
57 | state: AtomUpdate[];
58 | setter: null | SetterUpdate;
59 | }
60 |
61 | export interface AtomFamilyMembers {
62 | [atomName: string]: RecoilState;
63 | }
64 | export interface AtomFamilies {
65 | [familyName: string]: AtomFamilyMembers;
66 | }
67 |
68 | export interface SelectorFamilyConfig {
69 | key: string;
70 | get: (param: P) => (opts: { get: GetRecoilValue }) => Promise | RecoilValue | T;
71 | set?: (
72 | param: P,
73 | ) => (
74 | opts: { set: SetRecoilState; get: GetRecoilValue; reset: ResetRecoilState },
75 | newValue: T | DefaultValue,
76 | ) => void;
77 | dangerouslyAllowMutability?: boolean;
78 | }
79 | export interface SelectorFamilyMembers {
80 | trackedSelectorFamily: (param: P) => RecoilState | RecoilValueReadOnly;
81 | isSettable: boolean;
82 | prevParams: Set;
83 | }
84 | export interface SelectorFamilies {
85 | [familyName: string]: SelectorFamilyMembers;
86 | }
87 |
88 | // atoms should take RecoilState[] | string[]
89 | export interface Ledger {
90 | atoms: T[];
91 | selectors: string[];
92 | atomFamilies: AtomFamilies;
93 | selectorFamilies: SelectorFamilies;
94 | setters: string[];
95 | initialRender: SelectorUpdate[];
96 | initialRenderFamilies: SelectorFamilyUpdate[];
97 | transactions: Transaction[];
98 | setTransactions: SetTransaction[];
99 | }
100 |
101 | export interface SelectorConfig {
102 | key: string;
103 | get: (opts: { get: GetRecoilValue }) => T | Promise | RecoilValue;
104 | set?: (
105 | opts: { get: GetRecoilValue; set: SetRecoilState; reset: ResetRecoilState },
106 | newValue: T | DefaultValue,
107 | ) => void;
108 | dangerouslyAllowMutability?: boolean;
109 | }
110 |
--------------------------------------------------------------------------------
/package/recoil_generator/src/utils/ledger.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import type { Ledger } from '../types';
3 | import { RecoilState, SerializableParam } from 'recoil';
4 | /* eslint-enable */
5 |
6 | export const ledger: Ledger, any, SerializableParam> = {
7 | atoms: [],
8 | selectors: [], //get
9 | atomFamilies: {},
10 | selectorFamilies: {},
11 | setters: [], //set
12 | initialRender: [],
13 | initialRenderFamilies: [],
14 | transactions: [],//get
15 | setTransactions: [],//set
16 | };
17 |
--------------------------------------------------------------------------------
/package/recoil_generator/src/utils/store.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import type { RecoilState } from 'recoil';
3 | import { atom } from 'recoil';
4 | /* eslint-enable */
5 |
6 | // Recording toggle
7 | export const recordingState: RecoilState = atom({
8 | key: 'recordingState',
9 | default: true,
10 | });
11 |
--------------------------------------------------------------------------------
/package/recoil_generator/src/utils/utils.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import type { SerializableParam } from 'recoil';
3 | import type { AtomFamilies, SelectorFamilies } from '../types';
4 | /* eslint-enable */
5 |
6 | // Debouncing for selector transaction updates
7 | export const debounce = (func: (...args: any[]) => any, wait: number) => {
8 | let timeout: any;
9 |
10 | return (...args: any[]) => {
11 | const timeoutCallback = () => {
12 | timeout = null;
13 | func(...args);
14 | };
15 |
16 | clearTimeout(timeout);
17 | timeout = setTimeout(timeoutCallback, wait);
18 | };
19 | };
20 |
21 | // Used in key-to-variable name mapping in generateFile
22 | export function convertFamilyTrackerKeys(
23 | familyTracker: AtomFamilies,
24 | storeMap: Map,
25 | ): AtomFamilies;
26 | export function convertFamilyTrackerKeys(
27 | familyTracker: SelectorFamilies,
28 | storeMap: Map,
29 | ): SelectorFamilies;
30 |
31 | export function convertFamilyTrackerKeys(
32 | familyTracker: AtomFamilies | SelectorFamilies,
33 | storeMap: Map,
34 | ) {
35 | const refactoredTracker: AtomFamilies | SelectorFamilies = {};
36 |
37 | Object.keys(familyTracker).forEach((key) => {
38 | const newKey: string = storeMap.get(key) || key;
39 | refactoredTracker[newKey] = familyTracker[key];
40 | });
41 |
42 | return refactoredTracker;
43 | }
44 |
45 | // Dummy param for use in various checks (most notably the key-to-variable name mapping)
46 | export const dummyParam = 'chromogenDummyParam';
47 |
--------------------------------------------------------------------------------
/package/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
4 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
5 | "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
6 | "declaration": true /* Generates corresponding '.d.ts' file. */,
7 | "outDir": "./build" /* Redirect output structure to the directory. */,
8 | "rootDir": "." /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
9 | "removeComments": true /* Do not emit comments to output. */,
10 | "strict": true /* Enable all strict type-checking options. */,
11 | "noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */,
12 | "strictNullChecks": true /* Enable strict null checks. */,
13 | "strictFunctionTypes": true /* Enable strict checking of function types. */,
14 | "strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */,
15 | "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */,
16 | "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */,
17 | "noUnusedLocals": true /* Report errors on unused locals. */,
18 | "noUnusedParameters": true /* Report errors on unused parameters. */,
19 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
20 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
21 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
22 | "baseUrl": "./" /* Base directory to resolve non-absolute module names. */,
23 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
24 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
25 | },
26 | // "include": ["package/**/*"]
27 | }
--------------------------------------------------------------------------------
/package/zustand_generator/__tests__/api.test.js:
--------------------------------------------------------------------------------
1 | import { ledger } from '../src/utils/ledger';
2 | import { chromogenZustandMiddleware } from '../src/api/api';
3 | import { renderHook, act } from '@testing-library/react';
4 | import create from 'zustand';
5 |
6 | // testing chromogenZustandMiddleware
7 | describe('chromogenZustandMiddleware', () => {
8 | // destructuring atoms from ledger interface in utils folder
9 | it('is a function', () => {
10 | expect(typeof chromogenZustandMiddleware).toBe('function');
11 | });
12 |
13 | it('should update ledger upon invocation', () => {
14 | // creating a mock store
15 | const useStore = create(
16 | chromogenZustandMiddleware((set) => ({
17 | count: 0,
18 | increment: () => {
19 | set((state) => ({ count: count + 1 }), false, 'increment');
20 | },
21 | })),
22 | );
23 | //rendering the useStore Hook
24 | const { result } = renderHook(useStore);
25 |
26 | // verifying atoms property (array) on ledger has been updated with input atom
27 | expect(result.current.count).toStrictEqual(0);
28 | expect(ledger.initialRender).toStrictEqual({ count: 0 });
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/package/zustand_generator/__tests__/output-utils.test.js:
--------------------------------------------------------------------------------
1 | import {
2 | testInitialState,
3 | testStateChangesExpect,
4 | testStateChangesAct,
5 | generateActLine
6 | } from '../src/output/output-utils';
7 |
8 | const initialRender = {
9 | todoListState: [],
10 | todoListFilterState: 'Show All',
11 | todoListSortState: false,
12 | quoteText: '',
13 | quoteNumber: 0,
14 | checkBox: false,
15 | }
16 |
17 | describe('INITIAL RENDER', () => {
18 | //create a variable to hold our expected output
19 | const expectedOutput = ''
20 | + `\tit('todoListState should initialize correctly', () => {\n\t\texpect(result.current.todoListState).toStrictEqual([]);\n\t});\n\n`
21 | + `\tit('todoListFilterState should initialize correctly', () => {\n\t\texpect(result.current.todoListFilterState).toStrictEqual("Show All");\n\t});\n\n`
22 | + `\tit('todoListSortState should initialize correctly', () => {\n\t\texpect(result.current.todoListSortState).toStrictEqual(false);\n\t});\n\n`
23 | + `\tit('quoteText should initialize correctly', () => {\n\t\texpect(result.current.quoteText).toStrictEqual("");\n\t});\n\n`
24 | + `\tit('quoteNumber should initialize correctly', () => {\n\t\texpect(result.current.quoteNumber).toStrictEqual(0);\n\t});\n\n`
25 | + `\tit('checkBox should initialize correctly', () => {\n\t\texpect(result.current.checkBox).toStrictEqual(false);\n\t});\n\n`
26 | //create a variable and assign it to the evaluated result of the calling the testInitialState on our input
27 | const evaluatedResult = testInitialState(initialRender);
28 | //expect(realOutput).toStrictEqual(expectedOutput);
29 | it('expectedOutput should equal evaulatedResult', () => {
30 | expect(evaluatedResult).toStrictEqual(expectedOutput);
31 | });
32 |
33 |
34 |
35 | })
36 |
37 | describe('TEST STATE CHANGES ACT', () => {
38 |
39 | const transaction = [
40 | {
41 | action: 'setFilter',
42 | arguments: ['Show Uncompleted'],
43 | changedValues: { 'todoListFilterState': 'Show Uncompleted' }
44 | }
45 | ];
46 | const testStateChangesActOutput = testStateChangesAct(transaction);
47 |
48 | const expectedOutput =
49 |
50 | `\n\tit('todoListFilterState should update correctly', () => {
51 | const { result } = renderHook(useStore);
52 |
53 | act(() => {\n result.current.setFilter("Show Uncompleted");\n});
54 |
55 | \nexpect(result.current.todoListFilterState).toStrictEqual("Show Uncompleted");
56 | });`;
57 |
58 | let expectedNoWhitespace = expectedOutput.replace(/\s/g, '');
59 |
60 |
61 | it('expect output to equal expected output', () => {
62 | expect(testStateChangesActOutput.replace(/\s/g, '')).toStrictEqual(expectedNoWhitespace)
63 | })
64 | })
65 |
66 |
67 |
68 | describe('TEST STATE CHANGES EXPECT', () => {
69 | //1. Create function inputs manually
70 | const input = ["todoListFilterState", "Show Completed"];
71 | //2. Manually write out expected output of function
72 | const expectedOutput = `\nexpect(result.current.todoListFilterState).toStrictEqual("Show Completed");`
73 | //3. Run function to get actual output
74 | //4. Compare exptected ouptut to actual output
75 | const testStateChanges = testStateChangesExpect(input)
76 |
77 | it('it should be true when when input is passed into testStateChanges function ', () => {
78 | expect(testStateChanges).toStrictEqual(expectedOutput);
79 | })
80 | })
81 |
82 |
83 |
84 |
85 | describe('GENERATE ACT LINE', () => {
86 | // Create a tranaction inputs manually
87 | const transaction = {
88 | action: 'setFilter',
89 | arguments: ['Show Completed'],
90 | changedValues: { todoListFilterState: "Show Completed" }
91 | };
92 | const action = transaction.action;
93 | const args = transaction.arguments;
94 |
95 | const expectedOutput = `\tresult.current.${action}(${args?.map(arg => JSON.stringify(arg)).join(', ')});\n`
96 |
97 | const testActGeneration = generateActLine(transaction)
98 | // Manually writing out expected output of function
99 | //const evaluateActGeneration = `\tresult.current.${action}(${args?.map(arg => JSON.stringify(arg)).join(', ')});\n`
100 | // Run func to get actual ouput
101 | //Compare step2 to step 3
102 | it('testActGeneration should generate an action line', () => {
103 | expect(expectedOutput).toStrictEqual(testActGeneration)
104 | })
105 | })
--------------------------------------------------------------------------------
/package/zustand_generator/src/GlobalStyle.ts:
--------------------------------------------------------------------------------
1 | /* Reset CSS section */
2 | import { createGlobalStyle } from 'styled-components';
3 |
4 | const GlobalStyle = createGlobalStyle`
5 | *,
6 | *::before,
7 | *::after {
8 | box-sizing: border-box;
9 | }
10 |
11 | * {
12 | margin: 0;
13 | }
14 |
15 | html,
16 | body,
17 | #root // for create-react-app
18 | {
19 | height: 100%;
20 | }
21 |
22 | img,
23 | picture,
24 | video,
25 | canvas,
26 | svg {
27 | display: block;
28 | max-width: 100%;
29 | }
30 |
31 | input,
32 | button,
33 | textarea,
34 | select {
35 | font: inherit;
36 | }
37 |
38 | p,
39 | h1,
40 | h2,
41 | h3,
42 | h4,
43 | h5,
44 | h6 {
45 | overflow-wrap: break-word;
46 | }
47 |
48 | h2 {
49 | font-size: 36px;
50 | }
51 |
52 | #root,
53 | #__next {
54 | isolation: isolate;
55 | }
56 |
57 | @media (prefers-color-scheme: dark) {
58 | :root {
59 | --background-default: #222;
60 | --background-body: #161616;
61 | --background-button: #dbdbdb;
62 | --background-button-hover: #fafafa;
63 | --card-background: #323232;
64 | --card-border: #000;
65 | --input-background: #333;
66 | --input-border: #222;
67 | --input-border-hover: #222;
68 | --font-default: #ffffff;
69 | --font-secondary: #8e8e8e;
70 | --font-tertiary: #666;
71 | --font-button: #161616;
72 | --font-link: rgb(0, 149, 246);
73 | --font-error: rgba(220, 70, 70, .6);
74 | --background-error: rgba(220, 70, 70, .3);
75 | }
76 | }
77 |
78 | @media (prefers-color-scheme: light) {
79 | :root {
80 | --background-default: #fff;
81 | --background-body: #fafafa;
82 | --background-button: #323232;
83 | --background-button-hover: #161616;
84 | --card-background: #fff;
85 | --card-border: #dbdbdb;
86 | --input-background: #eee;
87 | --input-border: #dbdbdb;
88 | --input-border-hover: #191919c2;
89 | --font-default: #191919;
90 | --font-secondary: #8e8e8e;
91 | --font-tertiary: #8e8e8e;
92 | --font-button: #fafafa;
93 | --font-link: rgb(0, 149, 246);
94 | --font-error: rgba(220, 70, 70, .6);
95 | --background-error: rgba(220, 70, 70, .3);
96 | }
97 | }
98 |
99 | body {
100 | margin: 0;
101 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
102 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
103 | sans-serif;
104 | box-sizing: border-box;
105 | }
106 |
107 | #app {
108 | height: 100%;
109 | width: 100%;
110 | }
111 | `;
112 |
113 | export default GlobalStyle;
114 |
--------------------------------------------------------------------------------
/package/zustand_generator/src/api/api.ts:
--------------------------------------------------------------------------------
1 | import { ledger } from '../utils/ledger';
2 | import { Transaction, InitialRender } from '../types';
3 | import { StoreApi, StateCreator, StoreMutatorIdentifier } from 'zustand';
4 |
5 | // Referenced Zustand First Party Middleware for Type implementation
6 | // See here: https://github.com/pmndrs/zustand/tree/main/src/middleware
7 |
8 | type Chromogen = <
9 | T extends unknown,
10 | Mps extends [StoreMutatorIdentifier, unknown][] = [],
11 | Mcs extends [StoreMutatorIdentifier, unknown][] = [],
12 | >(
13 | creatorFunction: StateCreator,
14 | ) => StateCreator;
15 |
16 | type ChromogenImpl = (
17 | creatorFunction: PopArgument>,
18 | ) => PopArgument>;
19 |
20 | type PopArgument unknown> = T extends (
21 | ...a: [...infer A, infer _]
22 | ) => infer R
23 | ? (...a: A) => R
24 | : never;
25 |
26 | type TakeTwo = T extends []
27 | ? [undefined, undefined]
28 | : T extends [unknown]
29 | ? [...a0: T, a1: undefined]
30 | : T extends [unknown?]
31 | ? [...a0: T, a1: undefined]
32 | : T extends [unknown, unknown]
33 | ? T
34 | : T extends [unknown, unknown?]
35 | ? T
36 | : T extends [unknown?, unknown?]
37 | ? T
38 | : T extends [infer A0, infer A1, ...unknown[]]
39 | ? [A0, A1]
40 | : T extends [infer A0, (infer A1)?, ...unknown[]]
41 | ? [A0, A1?]
42 | : T extends [(infer A0)?, (infer A1)?, ...unknown[]]
43 | ? [A0?, A1?]
44 | : never;
45 |
46 | type StoreDevtools = S extends {
47 | setState: (...a: infer Sa) => infer Sr;
48 | }
49 | ? {
50 | setState(...a: [...a: TakeTwo, action?: A]): Sr;
51 | }
52 | : never;
53 |
54 | type Write = Omit & U;
55 |
56 | type WithDevtools = Write>;
57 |
58 | type NamedSet = WithDevtools>['setState'];
59 | /*
60 | Chromogen Middleware business logic. Performs 2 main functions:
61 | 1. Captures initial state for all store properties and writes to ledger as initialRender (For generating initial state tests)
62 | 2. Wraps set function to capture any subsequent state changes (along with funciton name, arguments, and before/after for changes store slices)
63 | */
64 | const chromogenImpl: ChromogenImpl = (creatorFunction) => (set, get, api) => {
65 | //get initial render and save it to ledger
66 | const initialStateEntries = creatorFunction(api.setState, get, api);
67 | const initialRender: InitialRender = filterOutFuncs(initialStateEntries);
68 | ledger.initialRender = initialRender;
69 |
70 | type S = ReturnType;
71 | (api.setState as NamedSet) = (partial, replace, action, ...args) => {
72 | const oldStore = filterOutFuncs(get());
73 | const r = set(partial, replace);
74 | const newStore = filterOutFuncs(get());
75 | const changedValues = diffStateObjects(oldStore, newStore);
76 |
77 | //create Transaction obj and write it to ledger for generating tests
78 | const newAction: Transaction = {
79 | action: typeof action === 'string' ? action : 'UnknownAction',
80 | changedValues,
81 | arguments: args,
82 | };
83 |
84 | ledger.transactions.push(newAction);
85 | return r;
86 | };
87 | return creatorFunction(api.setState, get, api);
88 | };
89 |
90 | export const chromogenZustandMiddleware = chromogenImpl as unknown as Chromogen;
91 |
92 | /* Goes through the store object and returns a new object containing state without any actions*/
93 | const filterOutFuncs = (store) => {
94 | const result = {};
95 | for (const [k, v] of Object.entries(store)) {
96 | if (typeof v !== 'function') result[k] = v;
97 | }
98 | return result;
99 | };
100 | /* Identifies the difference between initial Store and newStore containing newly invoked actions */
101 | const diffStateObjects = (oldStore, newStore) => {
102 | const changedValues = {};
103 | for (const [k, v] of Object.entries(newStore)) {
104 | if (JSON.stringify(oldStore[k]) !== JSON.stringify(v)) changedValues[k] = v;
105 | }
106 | return changedValues;
107 | };
108 |
--------------------------------------------------------------------------------
/package/zustand_generator/src/component/Buttons/RecordingButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import RecordButton from './RecordingVariations/Record';
3 | import StartButton from './RecordingVariations/Start';
4 |
5 | const RecordingButton = () => {
6 | const [isRecording, setIsRecording] = useState(true);
7 | const handleClick = () => setIsRecording(!isRecording);
8 |
9 | return (
10 |
19 | {isRecording ? (
20 |
21 | ) : (
22 |
23 | )}
24 |
25 | );
26 | };
27 |
28 | export default RecordingButton;
29 |
--------------------------------------------------------------------------------
/package/zustand_generator/src/component/Buttons/RecordingVariations/Record.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | const Record = (props): JSX.Element => {
4 | //hover
5 | const [isHover, setIsHover] = useState(false);
6 | const handleMouseEnter = () => {
7 | setIsHover(true);
8 | };
9 | const handleMouseLeave = () => {
10 | setIsHover(false);
11 | };
12 |
13 | //recording
14 | const recordButtonShape: React.CSSProperties = {
15 | display: 'flex',
16 | flexDirection: 'row',
17 | alignItems: 'center',
18 | position: 'absolute',
19 | width: '252px',
20 | height: '48px',
21 | // left: '1482px',
22 | // top: '1081px',
23 | borderRadius: '42px',
24 | justifyContent: 'center',
25 | padding: '14px 24px',
26 | columnGap: '16px',
27 | background: '#181818',
28 | border: '1px solid rgba(243, 246, 248, 0.1)',
29 | boxShadow:
30 | '0px 18px 24px rgba(0, 0, 0, 0.16), 0px 12px 16px rgba(6, 9, 11, 0.1), 0px 6px 12px rgba(0, 0, 0, 0.18), 0px 1px 20px rgba(0, 0, 0, 0.12)',
31 | cursor: 'pointer',
32 | bottom: '20px',
33 | };
34 |
35 | const recordIcon: React.CSSProperties = {
36 | width: '20px',
37 | height: '20px',
38 | background: '#D75959',
39 | opacity: '0.8',
40 | boxShadow:
41 | '0px 2px 6px rgba(215, 89, 89, 0.24), 0px 6px 10px rgba(215, 89, 89, 0.2), 0px 1px 16px rgba(215, 89, 89, 0.06)',
42 | borderRadius: '30px',
43 | flex: 'none',
44 | order: '0',
45 | flexGrow: '0',
46 | animation: !isHover ? 'glowing 1500ms infinite' : 'none',
47 | };
48 |
49 | const glowingAnimation = `
50 | @keyframes glowing {
51 | 0% { background-color: #D75959; box-shadow: 0 0 3px #D75959; }
52 | 50% { background-color: #ce4949; box-shadow: 0 0 30px #D75959; }
53 | 100% { background-color: #D75959; box-shadow: 0 0 3px #D75959; }
54 | }
55 |
56 | .glowing {
57 | animation: glowing 1500ms infinite;
58 | }
59 | `;
60 |
61 | const recordText: React.CSSProperties = {
62 | fontSize: '14px',
63 | lineHeight: '16px',
64 | color: '#CB4F4F',
65 | opacity: '0.8',
66 | flex: 'none',
67 | order: '1',
68 | flexGrow: '0',
69 | };
70 |
71 | //stop
72 | const stopButtonShape: React.CSSProperties = {
73 | display: 'flex',
74 | fontSize: '14px',
75 | flexDirection: 'row',
76 | alignItems: 'center',
77 | position: 'absolute',
78 | justifyContent: 'center',
79 | width: '252px',
80 | height: '48px',
81 | // left: '1482px',
82 | // top: '1081px',
83 | borderRadius: '42px',
84 | padding: '14px 24px',
85 | columnGap: '16px',
86 | background: '#212121',
87 | border: '1px solid rgba(243, 246, 248, 0.1)',
88 | boxShadow:
89 | '0px 18px 24px rgba(0, 0, 0, 0.16), 0px 12px 16px rgba(6, 9, 11, 0.1), 0px 6px 12px rgba(0, 0, 0, 0.18), 0px 1px 20px rgba(0, 0, 0, 0.12)',
90 | cursor: 'pointer',
91 | bottom: '20px',
92 | };
93 |
94 | const stopIcon: React.CSSProperties = {
95 | width: '14px',
96 | height: '16px',
97 | background: 'rgba(243, 246, 248, 0.9)',
98 | // opacity: '0.8',
99 | // boxShadow: '0px 2px 6px rgba(215, 89, 89, 0.24), 0px 6px 10px rgba(215, 89, 89, 0.2), 0px 1px 16px rgba(215, 89, 89, 0.06)',
100 | borderRadius: '1px',
101 | flex: 'none',
102 | order: '0',
103 | flexGrow: '0',
104 | };
105 |
106 | const stopText: React.CSSProperties = {
107 | height: '16px',
108 | fontSize: '14px',
109 | lineHeight: '16px',
110 | color: '#F3F6F8',
111 | opacity: '0.8',
112 | flex: 'none',
113 | order: '1',
114 | flexGrow: '0',
115 | };
116 |
117 | return (
118 | <>
119 |
120 |
126 |
127 |
128 |
129 | {isHover ? 'Stop recording' : 'Recording in progress'}
130 |
131 |
132 | >
133 | );
134 | };
135 |
136 | export default Record;
137 |
--------------------------------------------------------------------------------
/package/zustand_generator/src/component/Buttons/RecordingVariations/Start.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | const Start = (props): JSX.Element => {
4 | //hover
5 | const [isHover, setIsHover] = useState(false);
6 | const handleMouseEnter = () => {
7 | setIsHover(true);
8 | };
9 | const handleMouseLeave = () => {
10 | setIsHover(false);
11 | };
12 |
13 | const startButtonShape: React.CSSProperties = {
14 | display: 'flex',
15 | width: '252px',
16 | flexDirection: 'row',
17 | alignItems: 'center',
18 | position: 'absolute',
19 | justifyContent: 'center',
20 | height: '48px',
21 | // left: '1482px',
22 | // top: '1081px',
23 | borderRadius: '42px',
24 | padding: '14px 24px',
25 | columnGap: '16px',
26 | background: '#212121',
27 | border: '1px solid rgba(243, 246, 248, 0.1)',
28 | cursor: 'pointer',
29 | bottom: '20px',
30 | };
31 |
32 | const startButtonHover: React.CSSProperties = {
33 | display: 'flex',
34 | flexDirection: 'row',
35 | alignItems: 'center',
36 | position: 'absolute',
37 | width: '252px',
38 | height: '48px',
39 | borderRadius: '42px',
40 | padding: '14px 24px',
41 | columnGap: '16px',
42 | justifyContent: 'center',
43 | background: '#1C1C1C',
44 | border: '1px solid rgba(243, 246, 248, 0.1)',
45 | cursor: 'pointer',
46 | bottom: '20px',
47 | };
48 |
49 | const startIcon: React.CSSProperties = {
50 | width: '0',
51 | height: '0',
52 | borderTop: '8px solid transparent',
53 | borderBottom: '8px solid transparent',
54 | borderLeft: '16px solid rgba(243, 246, 248, 0.8)',
55 | flex: 'none',
56 | order: '0',
57 | flexGrow: '0',
58 | borderRadius: '2px',
59 | };
60 |
61 | const startText: React.CSSProperties = {
62 | fontSize: '14px',
63 | lineHeight: '16px',
64 | color: '#F3F6F8',
65 | opacity: '0.8',
66 | flex: 'none',
67 | order: '1',
68 | flexGrow: '0',
69 | };
70 |
71 | return (
72 |
78 |
79 | Start recording
80 |
81 | );
82 | };
83 |
84 | export default Start;
85 |
--------------------------------------------------------------------------------
/package/zustand_generator/src/component/Buttons/SecondaryButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import {
3 | icon_arrow,
4 | icon_copy,
5 | icon_download,
6 | icon_retract,
7 | icon_expand,
8 | icon_check,
9 | } from '../Icons';
10 |
11 | const downloadButton: React.CSSProperties = {
12 | font: `-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen", "Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue", sans-serif`,
13 | color: 'rgba(243, 246, 248, 0.7)',
14 | backgroundColor: 'rgb(243, 246, 248, 0.03)',
15 | height: '32px',
16 | border: '1px solid rgba(243, 246, 248, 0.05)',
17 | borderRadius: '6px',
18 | display: 'flex',
19 | flexDirection: 'row',
20 | alignItems: 'center',
21 | padding: '6px',
22 | gap: '4px',
23 | };
24 |
25 | const downloadHover: React.CSSProperties = {
26 | font: `-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen", "Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue", sans-serif`,
27 | color: 'rgba(243, 246, 248, 0.7)',
28 | backgroundColor: 'rgb(243, 246, 248, 0.1)',
29 | height: '32px',
30 | border: '1px solid rgba(243, 246, 248, 0.05)',
31 | borderRadius: '6px',
32 | display: 'flex',
33 | flexDirection: 'row',
34 | alignItems: 'center',
35 | padding: '6px',
36 | gap: '4px',
37 | };
38 |
39 | const downloadTitleContainer: React.CSSProperties = {
40 | display: 'flex',
41 | alignItems: 'center',
42 | gap: '6px',
43 | };
44 |
45 | const downloadIconBox: React.CSSProperties = {
46 | height: '20px',
47 | width: '20px',
48 | display: 'flex',
49 | alignItems: 'center',
50 | justifyContent: 'center',
51 | };
52 |
53 | const downloadTitleText: React.CSSProperties = {
54 | fontSize: '12px',
55 | fontWeight: 500,
56 | lineHeight: '16px',
57 | color: 'rgba(243, 246, 248, 0.7)',
58 | marginRight: '12px',
59 | };
60 |
61 | const downloadLine: React.CSSProperties = {
62 | width: '1px',
63 | height: '20px',
64 | background: 'rgba(243, 246, 248, 0.05)',
65 | };
66 |
67 | export const downloadArrow: React.CSSProperties = {
68 | width: '20px',
69 | height: '20px',
70 | };
71 |
72 | type Icon = 'download' | 'expand' | 'retract' | 'copy' | 'arrow' | 'check';
73 |
74 | interface Props {
75 | icon: Icon;
76 | handleClick?: any;
77 | value?: string;
78 | arrow?: boolean;
79 | }
80 |
81 | const arrowIcons = (key: Icon): JSX.Element => {
82 | if (key == 'download') return icon_download;
83 | if (key == 'expand') return icon_expand;
84 | if (key == 'retract') return icon_retract;
85 | if (key == 'copy') return icon_copy;
86 | if (key == 'arrow') return icon_arrow;
87 | if (key == 'check') return icon_check;
88 | else return icon_arrow;
89 | };
90 |
91 | const ArrowSection: React.FC = () => {
92 | return (
93 | <>
94 |
95 | {icon_arrow}
96 | >
97 | );
98 | };
99 |
100 | const SecondaryButton: React.FC = ({ value, arrow, icon, handleClick }) => {
101 | const [isHover, setIsHover] = useState(false);
102 | const handleMouseEnter = () => {
103 | setIsHover(true);
104 | };
105 | const handleMouseLeave = () => {
106 | setIsHover(false);
107 | };
108 |
109 | return (
110 |
117 |
118 |
{arrowIcons(icon)}
119 | {value &&
{value}
}
120 |
121 | {arrow && }
122 |
123 | );
124 | };
125 |
126 | export default SecondaryButton;
127 |
--------------------------------------------------------------------------------
/package/zustand_generator/src/component/ChromogenZustandObserver.tsx:
--------------------------------------------------------------------------------
1 | import Editor from './Editor';
2 | import EditorTab from './EditorTab';
3 | import React, { useState } from 'react';
4 | import { generateTests } from './component-utils';
5 | import GlobalStyle from '../GlobalStyle';
6 |
7 | const panel: React.CSSProperties = {
8 | display: 'flex',
9 | position: 'relative',
10 | // width: '531.49px'
11 | };
12 |
13 | interface Props {
14 | children: JSX.Element;
15 | }
16 |
17 | export const ChromogenZustandObserver: React.FC = ({ children }): JSX.Element => {
18 | const [code, setCode] = React.useState('');
19 | const [storeMap] = React.useState>(new Map());
20 | const [isHidden, setIsHidden] = useState(false);
21 |
22 | const timer = setInterval(() => {
23 | console.log('Firing');
24 | setCode(String(generateTests(storeMap)));
25 | }, 1000);
26 |
27 | React.useEffect(() => {
28 | console.log(code);
29 | timer;
30 | }, [timer]);
31 |
32 | // React.useEffect(() => console.log(ledger.transactions[2].changedValues), []);
33 |
34 | return (
35 |
36 | {children}
37 | {isHidden ? (
38 |
39 | ) : (
40 |
41 | )}
42 |
43 |
44 | );
45 | };
46 |
--------------------------------------------------------------------------------
/package/zustand_generator/src/component/Editor.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import CodeEditor from '@uiw/react-textarea-code-editor';
3 | import NumberList from './Numbers';
4 | import { Header } from './Header';
5 | import RecordingButton from './Buttons/RecordingButton';
6 |
7 | const editorStyle: React.CSSProperties = {
8 | display: 'flex',
9 | flexDirection: 'column',
10 | height: '100%',
11 | overflow: 'auto',
12 | borderLeft: '1px solid rgba(243,246,248,.1)',
13 | backgroundColor: '#1C1C1C',
14 | width: '50vw',
15 | };
16 | const codePanel: React.CSSProperties = {
17 | display: 'flex',
18 | // flexGrow: 1,
19 | overflowY: 'scroll',
20 | height: 'calc(100vh - 56px)',
21 | };
22 |
23 | interface Props {
24 | code: string;
25 | setIsHidden: Function;
26 | isHidden: boolean;
27 | }
28 | const Editorfield = ({ code, isHidden, setIsHidden }: Props): JSX.Element => {
29 | const [, setInnerCode] = useState(code);
30 | let breakLine = 0;
31 |
32 | for (let curr = 0; curr < code.length; curr++) {
33 | if (code[curr] == '\n') breakLine++;
34 | }
35 |
36 | console.log(breakLine);
37 |
38 | return (
39 |
40 |
41 |
42 |
43 | setInnerCode(evn.target.value)}
49 | padding={15}
50 | style={{
51 | maxWidth: 1000,
52 | width: 'calc(100% - 60px)',
53 | maxHeight: '100vh',
54 | overflow: 'visible',
55 | fontSize: 12,
56 | backgroundColor: '#1c1c1c',
57 | fontFamily:
58 | 'ui-monospace, SFMono-Regular, SF Mono, Consolas, Liberation Mono, Menlo,monospace',
59 | }}
60 | />
61 |
62 |
63 |
64 | );
65 | };
66 |
67 | export default Editorfield;
68 |
--------------------------------------------------------------------------------
/package/zustand_generator/src/component/EditorTab.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import SecondaryButton from './Buttons/SecondaryButton';
3 |
4 | type Props = {
5 | setIsHidden: Function;
6 | isHidden: boolean;
7 | };
8 |
9 | const EditorTab = (props: Props): JSX.Element => {
10 | const { isHidden, setIsHidden } = props;
11 | return (
12 |
13 | setIsHidden(!isHidden)} />
14 |
15 | );
16 | };
17 |
18 | export default EditorTab;
19 |
--------------------------------------------------------------------------------
/package/zustand_generator/src/component/Header.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import SecondaryButton from './Buttons/SecondaryButton';
3 | import { generateFile, generateTests } from './component-utils';
4 |
5 | const toolBar: React.CSSProperties = {
6 | display: 'flex',
7 | height: '56px',
8 | width: '100%',
9 | padding: '8px 16px',
10 | alignItems: 'center',
11 | gap: '8px',
12 | borderBottom: `1px solid rgba(243,246,248,.05)`,
13 | };
14 |
15 | const toolBarLogoBox: React.CSSProperties = {
16 | height: '40px',
17 | width: '40px',
18 | display: 'flex',
19 | alignItems: 'center',
20 | justifyContent: 'center',
21 | };
22 |
23 | const toolBarTitleContainer: React.CSSProperties = {
24 | display: 'flex',
25 | flexDirection: 'column',
26 | color: '#fff',
27 | lineHeight: '16px',
28 | opacity: 0.9,
29 | marginLeft: '8px',
30 | rowGap: '4px',
31 | flexGrow: 1,
32 | };
33 |
34 | const toolBarTitle: React.CSSProperties = {
35 | fontSize: '16px',
36 | fontWeight: 500,
37 | fontFamily: 'Inter !important',
38 | };
39 |
40 | const toolBarDescription: React.CSSProperties = {
41 | fontSize: '12px',
42 | fontWeight: 400,
43 | };
44 |
45 | export const copyButton: React.CSSProperties = {
46 | height: '32px',
47 | width: '32px',
48 | backgroundColor: 'rgb(243, 246, 248, 0.03)',
49 | border: '1px solid rgba(243, 246, 248, 0.05)',
50 | borderRadius: '6px',
51 | display: 'flex',
52 | alignItems: 'center',
53 | padding: '6px',
54 | };
55 |
56 | export const minimizeButton: React.CSSProperties = {
57 | height: '24px',
58 | width: '24px',
59 | backgroundColor: 'white',
60 | marginLeft: '10px',
61 | padding: 0,
62 | cursor: 'pointer',
63 | };
64 |
65 | export const minimizeIcon: React.CSSProperties = {};
66 |
67 | export const Header = ({ isHidden, setIsHidden }) => {
68 | const [file, setFile] = useState(undefined);
69 | const [copied, setCopied] = useState(false);
70 |
71 | useEffect(() => {
72 | copyOff();
73 | }, [copied]);
74 |
75 | function copyOff() {
76 | setTimeout(() => {
77 | setCopied(false);
78 | }, 3000);
79 | }
80 |
81 | return (
82 |
83 |
84 |
85 |
92 |
93 |
94 |
95 |
96 |
101 |
102 |
103 |
104 |
105 |
106 |
Chromogen Tests
107 |
Interact with the app to generate tests
108 |
109 |
115 | generateFile(setFile, new Map())}
120 | />
121 |
122 |
{
125 | navigator.clipboard.writeText(generateTests(new Map())[0]);
126 | setCopied(true);
127 | }}
128 | />
129 | setIsHidden(!isHidden)} />
130 |
131 | );
132 | };
133 |
--------------------------------------------------------------------------------
/package/zustand_generator/src/component/Icons.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const icon_download = (
4 |
5 |
6 |
7 |
8 |
9 |
14 |
15 |
16 | );
17 |
18 | export const icon_copy = (
19 |
20 |
21 |
22 |
23 |
24 |
29 |
30 |
31 | );
32 |
33 | export const icon_retract = (
34 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
46 | );
47 |
48 | export const icon_expand = (
49 |
50 |
51 |
52 |
53 |
54 |
59 |
60 |
61 | );
62 |
63 | export const icon_arrow = (
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | );
73 |
74 | export const icon_check = (
75 |
76 |
77 |
78 |
79 |
80 |
85 |
86 |
87 | );
88 |
--------------------------------------------------------------------------------
/package/zustand_generator/src/component/Numbers.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const listStyle: React.CSSProperties = {
4 | paddingBlock: '28px',
5 | display: 'flex',
6 | flexDirection: 'column',
7 | alignContent: 'flex-end',
8 | textAlign: 'end',
9 | background: '#1c1c1c',
10 | paddingInline: '16px',
11 | border: '1px solid #1c1c1c',
12 | height: 'auto',
13 | width: '60px',
14 | };
15 |
16 | const numberStyle: React.CSSProperties = {
17 | fontSize: 12,
18 | color: '#747478',
19 | fontFamily: 'ui-monospace, SFMono-Regular, SF Mono, Consolas, Liberation Mono, Menlo,monospace',
20 | };
21 |
22 | const unique = (val: number | string) => (
23 |
24 | {val}
25 |
26 | );
27 |
28 | const numerous = (num = 1000) => {
29 | let pointer: number = 1;
30 | let allNumbers: JSX.Element[] = [];
31 |
32 | while (pointer < num) {
33 | allNumbers = [...allNumbers, unique(pointer)];
34 | pointer++;
35 | }
36 |
37 | console.log(allNumbers);
38 |
39 | return allNumbers;
40 | };
41 |
42 | const NumberList = ({ number }): JSX.Element => {numerous(number)}
;
43 |
44 | export default NumberList;
45 |
--------------------------------------------------------------------------------
/package/zustand_generator/src/component/Resizing/Resizer.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import styled from 'styled-components';
3 |
4 | const ResizerStyle = styled.div`
5 | position: absolute;
6 | cursor: ew-resize;
7 | width: 2px;
8 | height: 100%;
9 | z-index: 1;
10 | left: -1;
11 | top: 0;
12 | &:hover {
13 | background: #4848be;
14 | }
15 | `;
16 |
17 | interface ResizerProps {
18 | onResize: Function;
19 | }
20 |
21 | const Resizer: React.FC = ({ onResize }) => {
22 | const [direction, setDirection] = useState('');
23 | const [mouseDown, setMouseDown] = useState(false);
24 |
25 | useEffect(() => {
26 | const handleMouseMove = (e) => {
27 | if (!direction) return;
28 | onResize(direction, e.movementX, e.movementY);
29 | };
30 |
31 | if (mouseDown) {
32 | window.addEventListener('mousemove', handleMouseMove);
33 | }
34 |
35 | return () => window.removeEventListener('mousemove', handleMouseMove);
36 | }, [mouseDown, direction, onResize]);
37 |
38 | useEffect(() => {
39 | const handleMouseUp = () => setMouseDown(false);
40 | window.addEventListener('mouseup', handleMouseUp);
41 |
42 | return () => window.removeEventListener('mouseup', handleMouseUp);
43 | }, []);
44 |
45 | const handleMouseDown = (direction) => {
46 | setDirection(direction);
47 | setMouseDown(true);
48 | };
49 |
50 | return handleMouseDown('left')}> ;
51 | };
52 |
53 | export default Resizer;
54 |
--------------------------------------------------------------------------------
/package/zustand_generator/src/component/component-utils.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import type { CSSProperties } from 'react';
3 | // import type { SerializableParam } from 'recoil';
4 | import type { Ledger } from '../types';
5 |
6 | import { ledger } from '../utils/ledger';
7 | import { output } from '../output/output';
8 | /* eslint-enable */
9 |
10 | const buttonStyle: CSSProperties = {
11 | display: 'inline-block',
12 | margin: '8px',
13 | marginLeft: '13px',
14 | padding: '0px',
15 | height: '25px',
16 | width: '65px',
17 | borderRadius: '4px',
18 | justifyContent: 'space-evenly',
19 | border: '1px',
20 | cursor: 'pointer',
21 | color: '#90d1f0',
22 | fontSize: '10px',
23 | };
24 |
25 | const divStyle: CSSProperties = {
26 | display: 'flex',
27 | position: 'relative',
28 | height: '100%',
29 | top: '0px',
30 | right: '0px',
31 | bottom: '0px',
32 | width: '30vw',
33 | backgroundColor: '#222222',
34 | borderRadius: '4px',
35 | margin: 0,
36 | padding: 0,
37 | zIndex: 999999,
38 | };
39 |
40 | const playStyle: CSSProperties = {
41 | boxSizing: 'border-box',
42 | marginLeft: '25px',
43 | borderStyle: 'solid',
44 | borderWidth: '7px 0px 7px 14px',
45 | };
46 |
47 | const pauseStyle: CSSProperties = {
48 | width: '14px',
49 | height: '14px',
50 | borderWidth: '0px 0px 0px 10px',
51 | borderStyle: 'double',
52 | marginLeft: '27px',
53 | };
54 |
55 | export const styles = { buttonStyle, divStyle, playStyle, pauseStyle };
56 |
57 | /*
58 | generateFile generates test file & sets download URL
59 | The passed in setFile function updates _file_ state in Chromogen observer
60 | Applying only at point-of-download keeps performance cost low for users who
61 | don't need to pass nodes while creating a moderate performance hit for others
62 | only while downloading, never while interacting with their app.
63 | */
64 | export const generateFile = (setFile: Function, storeMap: Map): string[] => {
65 | const tests = generateTests(storeMap);
66 | const blob = new Blob(tests);
67 | setFile(URL.createObjectURL(blob));
68 | return tests;
69 | };
70 |
71 | /* generateTests is invoked within generateFile, returning our desired test string within an array */
72 | export const generateTests = (storeMap: Map): string[] => {
73 | const { initialRender, transactions } = ledger;
74 |
75 | const finalLedger: Ledger =
76 | storeMap.size > 0
77 | ? {
78 | initialRender,
79 | transactions: transactions,
80 | }
81 | : { ...ledger };
82 |
83 | return [output(finalLedger)];
84 | };
85 |
--------------------------------------------------------------------------------
/package/zustand_generator/src/component/panel.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import React, { useState, useEffect } from 'react';
3 | import { useStore } from '../utils/store';
4 | import { styles, generateFile, generateTests } from './component-utils';
5 | /* eslint-enable */
6 |
7 | /* using a zustand store to keep track of recording state */
8 | const selector = (state) => ({
9 | recording: state.recording,
10 | toggleRecording: state.toggleRecording,
11 | });
12 |
13 | export const Panel: React.FC = () => {
14 | // Initializing as undefined over null to match React typing for AnchorHTML attributes
15 | const [file, setFile] = useState(undefined);
16 | const [storeMap] = useState>(new Map());
17 | const { recording, toggleRecording } = useStore<{
18 | recording: boolean;
19 | toggleRecording: Function;
20 | }>(selector);
21 |
22 | // Auto-click download link when a new file is generated (via button click)
23 | useEffect(() => document.getElementById('chromogen-download')!.click(), [file]);
24 | // ! to get around strict null check in tsconfig
25 |
26 | const [pauseColor, setPauseColor] = useState('#90d1f0');
27 | const pauseBorderStyle = {
28 | borderColor: `${pauseColor}`,
29 | };
30 |
31 | const [playColor, setPlayColor] = useState('transparent transparent transparent #90d1f0');
32 | const playBorderStyle = {
33 | borderColor: `${playColor}`,
34 | };
35 |
36 | return (
37 | <>
38 | {
39 |
40 |
41 |
Testing!
42 |
{
48 | toggleRecording();
49 | // if (!recording) return true;
50 | // return false;
51 | }}
52 | onMouseEnter={() =>
53 | recording
54 | ? setPauseColor('#f6f071')
55 | : setPlayColor('transparent transparent transparent #f6f071')
56 | }
57 | onMouseLeave={() =>
58 | recording
59 | ? setPauseColor('#90d1f0')
60 | : setPlayColor('transparent transparent transparent #90d1f0')
61 | }
62 | >
63 |
64 | {recording ? (
65 |
66 | ) : (
67 |
68 | )}
69 |
70 |
71 |
generateFile(setFile, storeMap)}
82 | onMouseEnter={() =>
83 | (document.getElementById('chromogen-generate-file')!.style.color = '#f6f071')
84 | }
85 | onMouseLeave={() =>
86 | (document.getElementById('chromogen-generate-file')!.style.color = '#90d1f0')
87 | }
88 | >
89 | {'Download'}
90 |
91 |
{
102 | navigator.clipboard.writeText(generateTests(storeMap)[0]);
103 | }}
104 | onMouseEnter={() =>
105 | (document.getElementById('chromogen-copy-test')!.style.color = '#f6f071')
106 | }
107 | onMouseLeave={() =>
108 | (document.getElementById('chromogen-copy-test')!.style.color = '#90d1f0')
109 | }
110 | >
111 | {'Copy To Clipboard'}
112 |
113 |
114 |
115 | }
116 |
122 | Download Test
123 |
124 | >
125 | );
126 | };
127 |
--------------------------------------------------------------------------------
/package/zustand_generator/src/output/output-utils.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import type { Transaction, InitialRender } from '../types';
3 | // } from '../types';
4 |
5 | import { dummyParam } from '../utils/utils';
6 |
7 | /* eslint-enable */
8 |
9 | /* ----- HELPER FUNCTIONS ----- */
10 |
11 | export function importZustandStore(): string {
12 | return `import useStore from '';`;
13 | }
14 |
15 | export function testInitialState(initialRender: InitialRender): string {
16 | return Object.entries(initialRender).reduce((acc, [k, v]) => {
17 | return (
18 | acc
19 | + `\tit('${k} should initialize correctly', () => {\n\t\texpect(result.current.${k}).toStrictEqual(${JSON.stringify(
20 | v,
21 | )});\n\t});\n\n`
22 | );
23 | }, '');
24 | }
25 |
26 | const dummyTransaction = { action: dummyParam, changedValues: {} };
27 |
28 | //Takes in an array of transactions and returns a full set of tests ("it blocks") for all actions and corresponding state changes
29 | export function testStateChangesAct(transactions: Transaction[]): string {
30 | //Groups transactions together based on whether the transactions impact the same slice of state
31 | //Each "group" of transactions will affect each store parameter 0 or 1 times.
32 | let groupedTransactions: Transaction[][] = [...transactions, dummyTransaction].reduce(
33 | (
34 | acc: {
35 | groups: Transaction[][];
36 | currentGroup: Transaction[];
37 | changedValues: { [nameOfChangedValue: string]: any };
38 | },
39 | cur,
40 | ) => {
41 | if (
42 | Object.keys(cur.changedValues).some((v) => acc.changedValues[v])
43 | || cur.action === dummyParam
44 | ) {
45 | acc.groups.push(acc.currentGroup);
46 | acc.currentGroup = [cur];
47 | acc.changedValues = Object.keys(cur.changedValues).reduce((acc, k) => {
48 | acc[k] = true;
49 | return acc;
50 | }, {});
51 | } else {
52 | acc.currentGroup.push(cur);
53 | Object.keys(cur.changedValues).forEach((k) => (acc.changedValues[k] = true));
54 | }
55 | return acc;
56 | },
57 | { groups: [], currentGroup: [], changedValues: {} },
58 | ).groups;
59 |
60 | //For each group of transactions, we generate an "It block"
61 | return groupedTransactions.reduce(
62 | (acc, group) => {
63 | const { str, actBlock } = generateItBlock(group);
64 | acc.str += str;
65 | acc.actStatements = actBlock;
66 | return acc;
67 | },
68 | { str: '', actStatements: '' },
69 | ).str;
70 | }
71 | //Takes in an entry for a slice of state and generates an expect statement asserting that the state properties have correct value in the store
72 | export function testStateChangesExpect([propertyName, newValue]: [string, any]): string {
73 | return `\nexpect(result.current.${propertyName}).toStrictEqual(${JSON.stringify(newValue)});`;
74 | }
75 |
76 | //Takes in a transaction and generates an act statement using the action name and argument(s)
77 | export function generateActLine(t: Transaction): string {
78 | const { action } = t;
79 | const args = t.arguments;
80 | return `\tresult.current.${action}(${args?.map((arg) => JSON.stringify(arg)).join(', ')});\n`;
81 | }
82 |
83 | //Takes in an array of grouped Transactions and returns an It Block (unit test) with all act &
84 | // expect statements for transactions in input
85 | function generateItBlock(transactions: Transaction[]): { str: string; actBlock: string } {
86 | const valuesChanged: string[] = [];
87 | let expectBlock = '';
88 |
89 | transactions.forEach((t) =>
90 | Object.entries(t.changedValues).forEach(([changedValue, newValue]) => {
91 | valuesChanged.push(changedValue);
92 | expectBlock += testStateChangesExpect([changedValue, newValue]);
93 | }),
94 | );
95 |
96 | let newActBlock = transactions.map(generateActLine).join('');
97 |
98 | return {
99 | str: `\n\tit('${valuesChanged.join(' & ')} should update correctly', () => {
100 | const { result } = renderHook(useStore);
101 |
102 | act(() => {\n${newActBlock}\n});
103 |
104 | ${expectBlock}
105 | });`,
106 | actBlock: newActBlock,
107 | };
108 | }
109 |
--------------------------------------------------------------------------------
/package/zustand_generator/src/output/output.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import type { Ledger } from '../types';
3 | import { importZustandStore, testInitialState, testStateChangesAct } from './output-utils';
4 | /* eslint-enable */
5 |
6 | /* ----- MAIN ----- */
7 | /* Output takes in initialRender and transactions from the ledger and tests them from the functions in output-utils*/
8 | export const output = ({ initialRender, transactions }: Ledger): string =>
9 | `
10 | import { renderHook, act } from '@testing-library/react';
11 | ${importZustandStore()}
12 |
13 | describe('INITIAL RENDER', () => {
14 | const { result } = renderHook(useStore);
15 |
16 | ${testInitialState(initialRender)}
17 | });
18 |
19 |
20 | describe('STATE CHANGES', () => {
21 | const { result } = renderHook(useStore);
22 |
23 | ${testStateChangesAct(transactions)}
24 | });`;
25 |
26 | export const unitOutput = (initialRender: any, action: any): string => {
27 | console.log('within unitOutput. init, action : ', initialRender, action);
28 | let retString = '';
29 | if (initialRender) {
30 | console.log('within unitOutput initialRender');
31 | retString += `
32 | import { renderHook, act } from '@testing-library/react';
33 | ${importZustandStore()}
34 | describe('INITIAL RENDER', () => {
35 | const { result } = renderHook(useStore);
36 | ${testInitialState(initialRender)}
37 | });
38 | `;
39 | } else if (action) {
40 | console.log('within unitOutput action');
41 | retString += `
42 | describe('STATE CHANGES', () => {
43 | const { result } = renderHook(useStore);
44 | ${testStateChangesAct([action])}
45 | });`;
46 | }
47 | return retString;
48 | };
49 |
50 | //NOTE: Test output is not linted/formatted in any meaningful way. The Chromogen team recommends formatting tests in line with your personal or organizational preferences;
51 |
--------------------------------------------------------------------------------
/package/zustand_generator/src/types.ts:
--------------------------------------------------------------------------------
1 | // ----- EXPORTING TYPES TO BE USED IN SRC/.TSX FILES -----
2 |
3 | type NotAFunction = { [k: string]: unknown } & ({ bind?: never } | { call?: never });
4 |
5 | export type InitialRender = {
6 | [stateParam: string]: NotAFunction;
7 | };
8 |
9 | export interface Transaction {
10 | action: string;
11 | arguments?: T;
12 | changedValues: {
13 | [nameOfChangedValue: string]: NotAFunction;
14 | };
15 | }
16 |
17 | export interface Ledger {
18 | initialRender: InitialRender;
19 | transactions: Transaction[];
20 | }
21 |
--------------------------------------------------------------------------------
/package/zustand_generator/src/utils/ledger.ts:
--------------------------------------------------------------------------------
1 | import type { Ledger } from '../types';
2 |
3 | export const ledger: Ledger = {
4 | initialRender: {},
5 | transactions: [],
6 | };
7 |
--------------------------------------------------------------------------------
/package/zustand_generator/src/utils/store.ts:
--------------------------------------------------------------------------------
1 | import create from 'zustand';
2 |
3 | interface RecordingState {
4 | recording: boolean;
5 | toggleRecording: () => void;
6 | }
7 |
8 | /*
9 | Allows for recording to always be on during page load
10 | and the ability to pause recording
11 | */
12 | export const useStore = create((set) => ({
13 | recording: true,
14 | toggleRecording: () => {
15 | set((state) => ({ recording: !state.recording }), false);
16 | },
17 | }));
18 |
--------------------------------------------------------------------------------
/package/zustand_generator/src/utils/utils.ts:
--------------------------------------------------------------------------------
1 | export const dummyParam = 'chromogenDummyParam';
2 |
--------------------------------------------------------------------------------