├── .eslintrc
├── .github
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
└── workflows
│ ├── codeql-analysis.yml
│ └── test.yml
├── .gitignore
├── .prettierrc.js
├── .yarnrc.yml
├── CHANGELOG.md
├── LICENSE.md
├── Makefile
├── README.md
├── assets
└── video.jpg
├── babel.config.js
├── cypress.config.ts
├── cypress
├── e2e
│ ├── adminGuesser.cy.ts
│ ├── lists.cy.ts
│ └── login.ts
├── fixtures
│ └── example.json
├── support
│ ├── commands.ts
│ └── e2e.ts
└── tsconfig.json
├── jest.config.js
├── lerna.json
├── package.json
├── packages
├── demo
│ ├── .env.local-example
│ ├── .gitignore
│ ├── README.md
│ ├── db-seed.ts
│ ├── index.html
│ ├── package.json
│ ├── public
│ │ ├── auth-callback.html
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── logo192.png
│ │ ├── logo512.png
│ │ ├── logos
│ │ │ ├── 0.png
│ │ │ ├── 1.png
│ │ │ ├── 10.png
│ │ │ ├── 11.png
│ │ │ ├── 12.png
│ │ │ ├── 13.png
│ │ │ ├── 14.png
│ │ │ ├── 15.png
│ │ │ ├── 16.png
│ │ │ ├── 17.png
│ │ │ ├── 18.png
│ │ │ ├── 19.png
│ │ │ ├── 2.png
│ │ │ ├── 20.png
│ │ │ ├── 21.png
│ │ │ ├── 22.png
│ │ │ ├── 23.png
│ │ │ ├── 24.png
│ │ │ ├── 25.png
│ │ │ ├── 26.png
│ │ │ ├── 27.png
│ │ │ ├── 28.png
│ │ │ ├── 29.png
│ │ │ ├── 3.png
│ │ │ ├── 30.png
│ │ │ ├── 31.png
│ │ │ ├── 32.png
│ │ │ ├── 33.png
│ │ │ ├── 34.png
│ │ │ ├── 35.png
│ │ │ ├── 36.png
│ │ │ ├── 37.png
│ │ │ ├── 38.png
│ │ │ ├── 39.png
│ │ │ ├── 4.png
│ │ │ ├── 40.png
│ │ │ ├── 41.png
│ │ │ ├── 42.png
│ │ │ ├── 43.png
│ │ │ ├── 44.png
│ │ │ ├── 45.png
│ │ │ ├── 46.png
│ │ │ ├── 47.png
│ │ │ ├── 48.png
│ │ │ ├── 49.png
│ │ │ ├── 5.png
│ │ │ ├── 50.png
│ │ │ ├── 51.png
│ │ │ ├── 52.png
│ │ │ ├── 53.png
│ │ │ ├── 54.png
│ │ │ ├── 55.png
│ │ │ ├── 6.png
│ │ │ ├── 7.png
│ │ │ ├── 8.png
│ │ │ ├── 9.png
│ │ │ └── Readme.md
│ │ ├── manifest.json
│ │ └── robots.txt
│ ├── src
│ │ ├── App.tsx
│ │ ├── dataGenerator
│ │ │ ├── companies.ts
│ │ │ ├── contactNotes.ts
│ │ │ ├── contacts.ts
│ │ │ ├── dealNotes.ts
│ │ │ ├── deals.ts
│ │ │ ├── finalize.ts
│ │ │ ├── index.ts
│ │ │ ├── sales.ts
│ │ │ ├── tags.ts
│ │ │ ├── tasks.ts
│ │ │ ├── types.ts
│ │ │ └── utils.ts
│ │ ├── env.d.ts
│ │ ├── index.tsx
│ │ ├── logo.svg
│ │ ├── react-app-env.d.ts
│ │ ├── reportWebVitals.js
│ │ ├── setupTests.js
│ │ └── types.ts
│ ├── tsconfig.json
│ └── vite.config.ts
├── ra-supabase-core
│ ├── README.md
│ ├── babel.config.js
│ ├── package.json
│ ├── src
│ │ ├── authProvider.ts
│ │ ├── dataProvider.ts
│ │ ├── getSearchString.ts
│ │ ├── index.ts
│ │ ├── useAPISchema.ts
│ │ ├── useRedirectIfAuthenticated.test.tsx
│ │ ├── useRedirectIfAuthenticated.ts
│ │ ├── useResetPassword.ts
│ │ ├── useSetPassword.test.tsx
│ │ ├── useSetPassword.ts
│ │ ├── useSupabaseAccessToken.test.tsx
│ │ └── useSupabaseAccessToken.ts
│ └── tsconfig.json
├── ra-supabase-language-english
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ └── index.ts
│ └── tsconfig.json
├── ra-supabase-language-french
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ └── index.ts
│ └── tsconfig.json
├── ra-supabase-ui-materialui
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── AuthLayout.tsx
│ │ ├── ForgotPasswordForm.tsx
│ │ ├── ForgotPasswordPage.tsx
│ │ ├── LoginForm.tsx
│ │ ├── LoginPage.tsx
│ │ ├── SetPasswordForm.tsx
│ │ ├── SetPasswordPage.tsx
│ │ ├── SocialAuthButton.tsx
│ │ ├── guessers
│ │ │ ├── CreateGuesser.tsx
│ │ │ ├── EditGuesser.tsx
│ │ │ ├── InferredElement.ts
│ │ │ ├── ListGuesser.tsx
│ │ │ ├── ShowGuesser.tsx
│ │ │ ├── editFieldTypes.tsx
│ │ │ ├── exampleSchema.json
│ │ │ ├── index.ts
│ │ │ ├── inferElementFromType.ts
│ │ │ ├── useCrudGuesser.spec.tsx
│ │ │ └── useCrudGuesser.tsx
│ │ ├── icons.tsx
│ │ ├── index.ts
│ │ └── mui.d.ts
│ └── tsconfig.json
└── ra-supabase
│ ├── README.md
│ ├── assets
│ └── demo.png
│ ├── package.json
│ ├── src
│ ├── AdminGuesser.tsx
│ ├── defaultI18nProvider.ts
│ └── index.ts
│ └── tsconfig.json
├── supabase
├── .gitignore
├── config.toml
├── migrations
│ ├── 20230126140204_init.sql
│ ├── 20230220132931_fts.sql
│ ├── 20240606161030_allow_nullable.sql
│ └── 20250325160944_contact_tags.sql
├── seed.sql
└── templates
│ ├── invite.html
│ └── recovery.html
├── test-global-setup.js
├── test-setup.js
├── tsconfig.json
└── yarn.lock
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "extends": [
4 | "react-app",
5 | "plugin:prettier/recommended"
6 | ],
7 | "plugins": [
8 | "jsx-a11y",
9 | "prettier",
10 | "react",
11 | "react-hooks"
12 | ],
13 | "rules": {
14 | "no-use-before-define": "off",
15 | "prettier/prettier": "error",
16 | "no-restricted-imports": [
17 | "error",
18 | {
19 | "paths": [
20 | {
21 | "name": "@material-ui/core",
22 | "importNames": ["makeStyles", "createMuiTheme"],
23 | "message": "Please import from @material-ui/core/styles instead. See https://material-ui.com/guides/minimizing-bundle-size/#option-2 for more information"
24 | }
25 | ]
26 | }
27 | ],
28 | "no-redeclare": "off",
29 | "@typescript-eslint/no-redeclare": "error"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/.github/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 contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at info@marmelab.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: https://contributor-covenant.org
46 | [version]: https://contributor-covenant.org/version/1/4/
47 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | So you want to contribute to react-admin? Awesome! Thank you in advance for your contribution. Here are a few guidelines that will help you along the way.
4 |
5 | ## Asking Questions
6 |
7 | For how-to questions and other non-issues, please use [StackOverflow](https://stackoverflow.com/questions/tagged/react-admin) instead of Github issues. There is a StackOverflow tag called "react-admin" that you can use to tag your questions.
8 |
9 | ## Opening an Issue
10 |
11 | If you think you have found a bug, or have a new feature idea, please start by making sure it hasn't already been [reported or fixed](https://github.com/marmelab/react-admin/issues?q=is%3Aissue+is%3Aclosed). You can search through existing issues and PRs to see if someone has reported one similar to yours.
12 |
13 | Next, create a new issue that briefly explains the problem, and provides a bit of background as to the circumstances that triggered it, and steps to reproduce it.
14 |
15 | For code issues please include:
16 | * React-admin version
17 | * React version
18 | * Browser version
19 | * A code example or link to a repo, gist or running site. (hint: fork [this sandbox](https://codesandbox.io/s/github/marmelab/react-admin/tree/master/examples/simple) to create a reproducible version of your bug)
20 |
21 | For visual or layout problems, images or animated gifs can help explain your issue.
22 | It's even better with a live reproduction test case.
23 |
24 | ### Issue Guidelines
25 |
26 | Please use a succinct description. "doesn't work" doesn't help others find similar issues.
27 |
28 | Please don't group multiple topics into one issue, but instead each should be its own issue.
29 |
30 | And please don't just '+1' an issue. It spams the maintainers and doesn't help move the issue forward.
31 |
32 | ## Submitting a Pull Request
33 |
34 | React-admin is a community project, so pull requests are always welcome, but before working on a large change, it is best to open an issue first to discuss it with the maintainers. In that case, prefix it with "[RFC]" (Request for Comments)
35 |
36 | When in doubt, keep your pull requests small. To give a PR the best chance of getting accepted, don't bundle more than one feature or bug fix per pull request. It's always best to create two smaller PRs than one big one.
37 |
38 | The core team prefix their PRs width "[WIP]" (Work in Progress) or "[RFR]" (ready for Review), don't hesitate to do the same to explain how far you are from completion.
39 |
40 | When adding new features or modifying existing, please attempt to include tests to confirm the new behaviour.
41 |
42 | ### Patches or hotfix: PR on `master`
43 |
44 | Bug fixes that don't break existing apps can be PRed against `master`. Hotfix versions are released usually within days.
45 |
46 | ### New Features or BC breaks: PR on `next`
47 |
48 | At any given time, `next` represents the latest development version of the library. It is merged to master and released on a monthly basis.
49 |
50 | ### Coding style
51 |
52 | You must follow the coding style of the existing files. React-admin uses eslint and [prettier](https://github.com/prettier/prettier). You can reformat all the project files automatically by calling
53 |
54 | ```sh
55 | make prettier
56 | ```
57 |
58 | **Tip**: If possible, enable linting in your editor to get realtime feedback and/or fixes.
59 |
60 | ## License
61 |
62 | By contributing your code to the marmelab/react-admin GitHub repository, you agree to license your contribution under the MIT license.
63 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | name: 'Code scanning - action'
2 |
3 | on:
4 | push:
5 | pull_request:
6 | schedule:
7 | - cron: '0 10 * * 3'
8 |
9 | jobs:
10 | CodeQL-Build:
11 | # CodeQL runs on ubuntu-latest and windows-latest
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - name: Cancel Previous Runs
16 | uses: styfle/cancel-workflow-action@0.11.0
17 | - name: Checkout repository
18 | uses: actions/checkout@v2
19 | with:
20 | # We must fetch at least the immediate parents so that if this is
21 | # a pull request then we can checkout the head.
22 | fetch-depth: 2
23 |
24 | # If this run was triggered by a pull request event, then checkout
25 | # the head of the pull request instead of the merge commit.
26 | - run: git checkout HEAD^2
27 | if: ${{ github.event_name == 'pull_request' }}
28 |
29 | # Initializes the CodeQL tools for scanning.
30 | - name: Initialize CodeQL
31 | uses: github/codeql-action/init@v1
32 | # Override language selection by uncommenting this and choosing your languages
33 | # with:
34 | # languages: go, javascript, csharp, python, cpp, java
35 |
36 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
37 | # If this step fails, then you should remove it and run the build manually (see below)
38 | - name: Autobuild
39 | uses: github/codeql-action/autobuild@v1
40 |
41 | # ℹ️ Command-line programs to run using the OS shell.
42 | # 📚 https://git.io/JvXDl
43 |
44 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
45 | # and modify them (or add more) to build your code if your project
46 | # uses a compiled language
47 |
48 | #- run: |
49 | # make bootstrap
50 | # make release
51 |
52 | - name: Perform CodeQL Analysis
53 | uses: github/codeql-action/analyze@v1
54 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: 'Test - action'
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | - next
8 | pull_request:
9 |
10 | jobs:
11 | unit-test:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Cancel Previous Runs
15 | uses: styfle/cancel-workflow-action@0.11.0
16 | - name: Checkout
17 | uses: actions/checkout@v3
18 | - name: Use Node.js LTS
19 | uses: actions/setup-node@v3
20 | with:
21 | node-version: '22.x'
22 | - name: Enable corepack
23 | run: corepack enable
24 | - name: Yarn install
25 | run: yarn install --immutable
26 | - name: Build
27 | run: make build
28 | - name: Lint
29 | run: make lint
30 | - name: Unit Tests
31 | run: make test-unit
32 | env:
33 | CI: true
34 |
35 | cypress-run:
36 | runs-on: ubuntu-latest
37 | steps:
38 | - name: Cancel Previous Runs
39 | uses: styfle/cancel-workflow-action@0.11.0
40 | - name: Checkout
41 | uses: actions/checkout@v3
42 | - name: Use Node.js LTS
43 | uses: actions/setup-node@v3
44 | with:
45 | node-version: '22.x'
46 | - name: Enable corepack
47 | run: corepack enable
48 | - name: Yarn install
49 | run: yarn install --immutable
50 | - name: Prepare env
51 | run: cp -n ./packages/demo/.env.local-example ./packages/demo/.env
52 | - name: Build
53 | run: make build build-demo
54 | - uses: supabase/setup-cli@v1.4.0
55 | with:
56 | version: 2.12.1
57 | - name: Start Supabase local development setup
58 | run: supabase start
59 | - name: Setup database
60 | run: make db-migrate db-seed
61 | - name: Cypress run
62 | uses: cypress-io/github-action@v5.0.7
63 | with:
64 | start: make run-prod
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 |
6 | # testing
7 | coverage
8 |
9 | # production
10 | build
11 | lib
12 | esm
13 | es6
14 | dist
15 |
16 | # misc
17 | .DS_Store
18 | .env.local
19 | .env.development.local
20 | .env.test.local
21 | .env.production.local
22 | .eslintcache
23 |
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | cypress/videos
29 | cypress/screenshots
30 | cypress/downloads
31 |
32 | .pnp.*
33 | .yarn/*
34 | !.yarn/patches
35 | !.yarn/plugins
36 | !.yarn/releases
37 | !.yarn/sdks
38 | !.yarn/versions
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | arrowParens: 'avoid',
3 | bracketSpacing: true,
4 | jsxBracketSameLine: false,
5 | jsxSingleQuote: false,
6 | printWidth: 80,
7 | quoteProps: 'as-needed',
8 | rangeStart: 0,
9 | rangeEnd: Infinity,
10 | semi: true,
11 | singleQuote: true,
12 | tabWidth: 4,
13 | trailingComma: 'es5',
14 | useTabs: false
15 | }
16 |
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
2 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016-present, Francois Zaninotto, Marmelab
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 NON INFRINGEMENT. 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 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: build help
2 |
3 | help:
4 | @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
5 |
6 | setup-env: ## setup the environment
7 | cp -n ./packages/demo/.env.local-example ./packages/demo/.env 2>/dev/null || :
8 |
9 | install: setup-env package.json ## install dependencies
10 | @if [ "$(CI)" != "true" ]; then \
11 | echo "Full install..."; \
12 | yarn; \
13 | fi
14 | @if [ "$(CI)" = "true" ]; then \
15 | echo "Frozen install..."; \
16 | yarn --frozen-lockfile; \
17 | fi
18 |
19 | run: ## run the demo
20 | @yarn run-demo
21 |
22 | run-prod: ## run the demo in prod
23 | @yarn run-demo-prod
24 |
25 | build-demo: ## compile the demo to static js
26 | @yarn build-demo
27 |
28 | build-ra-supabase-core:
29 | @echo "Transpiling ra-supabase-core files...";
30 | @cd ./packages/ra-supabase-core && yarn build
31 |
32 | build-ra-supabase-language-english:
33 | @echo "Transpiling ra-supabase-language-english files...";
34 | @cd ./packages/ra-supabase-language-english && yarn build
35 |
36 | build-ra-supabase-language-french:
37 | @echo "Transpiling ra-supabase-language-french files...";
38 | @cd ./packages/ra-supabase-language-french && yarn build
39 |
40 | build-ra-supabase-ui-materialui:
41 | @echo "Transpiling ra-supabase-ui-materialui files...";
42 | @cd ./packages/ra-supabase-ui-materialui && yarn build
43 |
44 | build-ra-supabase:
45 | @echo "Transpiling ra-supabase files...";
46 | @cd ./packages/ra-supabase && yarn build
47 |
48 | build: build-ra-supabase-core build-ra-supabase-ui-materialui build-ra-supabase-language-english build-ra-supabase-language-french build-ra-supabase ## compile ES6 files to JS
49 |
50 | lint: ## lint the code and check coding conventions
51 | @echo "Running linter..."
52 | @yarn lint
53 |
54 | prettier: ## prettify the source code using prettier
55 | @echo "Running prettier..."
56 | @yarn prettier
57 |
58 | test: build test-unit lint ## launch all tests
59 |
60 | test-unit: ## launch unit tests
61 | @echo "Running unit tests...";
62 | @yarn test-unit;
63 |
64 | test-unit-watch: ## launch unit tests and watch for changes
65 | @echo "Running unit tests...";
66 | @yarn test-unit --watch;
67 |
68 | test-e2e: ## launch e2e tests
69 | @echo "Running e2e tests...";
70 | @yarn test-e2e;
71 |
72 | test-e2e-local: ## launch e2e tests
73 | @echo "Running e2e tests...";
74 | @yarn test-e2e-local;
75 |
76 | supabase-start: ## start the supabase server
77 | @echo "Starting supabase server..."
78 | @yarn supabase start
79 |
80 | supabase-stop: ## stop the supabase server
81 | @echo "Stopping supabase server..."
82 | @yarn supabase stop
83 |
84 | db-migrate: ## migrate the database
85 | @echo "Apply migrations on the database..."
86 | @yarn db-migrate
87 |
88 | db-seed: ## seed the database
89 | @echo "Seeding the database..."
90 | @yarn db-seed
91 |
92 | db-setup: db-migrate db-seed ## setup the database
93 |
--------------------------------------------------------------------------------
/assets/video.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/assets/video.jpg
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/cypress.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'cypress';
2 |
3 | export default defineConfig({
4 | e2e: {
5 | baseUrl: 'http://localhost:8000',
6 | },
7 | });
8 |
--------------------------------------------------------------------------------
/cypress/e2e/adminGuesser.cy.ts:
--------------------------------------------------------------------------------
1 | import { login } from './login';
2 |
3 | describe('AdminGuesser', () => {
4 | it('should render one menu item per resource', () => {
5 | cy.visit('/');
6 | login();
7 | cy.findAllByText('Contacts');
8 | cy.findByText('Companies').should('exist');
9 | cy.findByText('Deals').should('exist');
10 | cy.findByText('Tags').should('exist');
11 | cy.findByText('Tasks').should('exist');
12 | cy.findByText('Dealnotes').should('exist');
13 | cy.findByText('Contactnotes').should('exist');
14 | cy.findByText('Sales').should('exist');
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/cypress/e2e/lists.cy.ts:
--------------------------------------------------------------------------------
1 | import { login } from './login';
2 |
3 | const getPaginationText = () =>
4 | cy.findByText(/\d+-\d+ of \d+/, { timeout: 10000 });
5 |
6 | describe('Lists', () => {
7 | it('should render a list', () => {
8 | cy.visit('/');
9 | login();
10 | getPaginationText();
11 | });
12 |
13 | it('should allow to move through pages', () => {
14 | cy.visit('/');
15 | login();
16 | getPaginationText().then(el => {
17 | const page = parseInt(el.text().split('-')[0].trim());
18 | cy.findByLabelText('Go to page 4').click();
19 |
20 | let page4 = 0;
21 | // Use should here to allow built-in retry as it may take a few ms for the list to update
22 | getPaginationText().should(el => {
23 | page4 = parseInt(el.text().split('-')[0].trim());
24 | expect(page4).to.be.greaterThan(page);
25 | });
26 |
27 | cy.findByLabelText('Go to page 2').click();
28 |
29 | // Use should here to allow built-in retry as it may take a few ms for the list to update
30 | getPaginationText().should(el => {
31 | const page2 = parseInt(el.text().split('-')[0].trim());
32 | expect(page2).to.be.greaterThan(page);
33 | expect(page2).to.be.lessThan(page4);
34 | });
35 | });
36 | });
37 |
38 | it('should allow to sort data', () => {
39 | cy.visit('/');
40 | login();
41 | cy.findByLabelText('Sort by gender ascending').click();
42 | cy.findAllByText('female', { timeout: 10000 }).should(
43 | 'have.length',
44 | 10
45 | );
46 | cy.findByText('male').should('not.exist');
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/cypress/e2e/login.ts:
--------------------------------------------------------------------------------
1 | export const login = (
2 | email: string = 'janedoe@atomic.dev',
3 | password: string = 'password'
4 | ) => {
5 | cy.wait(1000);
6 | cy.findByLabelText('Email *').type(email);
7 | cy.findByLabelText('Password *').type(password);
8 | cy.findByText('Sign in').click();
9 | };
10 |
--------------------------------------------------------------------------------
/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
6 |
--------------------------------------------------------------------------------
/cypress/support/commands.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import '@testing-library/cypress/add-commands';
3 |
--------------------------------------------------------------------------------
/cypress/support/e2e.ts:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/e2e.ts is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands';
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/cypress/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "lib",
5 | "rootDir": ".",
6 | "declaration": true,
7 | "declarationMap": true,
8 | "allowJs": false,
9 | "types": ["cypress", "@testing-library/cypress"]
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | globalSetup: './test-global-setup.js',
3 | setupFilesAfterEnv: ['./test-setup.js'],
4 | testEnvironment: 'jsdom',
5 | testPathIgnorePatterns: [
6 | '/node_modules/',
7 | '/lib/',
8 | '/esm/',
9 | '/packages/demo',
10 | ],
11 | transformIgnorePatterns: [
12 | '[/\\\\]node_modules[/\\\\](?!(@hookform)/).+\\.(js|jsx|mjs|ts|tsx)$',
13 | ],
14 | testEnvironmentOptions: {
15 | customExportConditions: [], // don't load "browser" field
16 | },
17 | transform: {
18 | // '^.+\\.[tj]sx?$' to process js/ts with `ts-jest`
19 | '^.+\\.[tj]sx?$': [
20 | 'ts-jest',
21 | {
22 | isolatedModules: true,
23 | useESM: true,
24 | },
25 | ],
26 | },
27 | };
28 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "packages": ["packages/*"],
3 | "version": "3.5.0",
4 | "useWorkspaces": true,
5 | "npmClient": "yarn"
6 | }
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ra-supabase-monorepo",
3 | "private": true,
4 | "version": "1.0.0",
5 | "repository": "git@github.com:marmelab/ra-supabase.git",
6 | "author": "Gildas Garcia <1122076+djhi@users.noreply.github.com>",
7 | "license": "MIT",
8 | "files": [
9 | "*.md",
10 | "lib",
11 | "esm",
12 | "src"
13 | ],
14 | "main": "lib/index",
15 | "module": "esm/index.js",
16 | "types": "esm/index.d.ts",
17 | "sideEffects": false,
18 | "devDependencies": {
19 | "@testing-library/cypress": "^9.0.0",
20 | "@types/jest": "^29.5.11",
21 | "@typescript-eslint/eslint-plugin": "^5.62.0",
22 | "@typescript-eslint/parser": "^5.62.0",
23 | "babel-jest": "^29.7.0",
24 | "cypress": "^12.17.4",
25 | "eslint": "^8.56.0",
26 | "eslint-config-prettier": "^8.10.0",
27 | "eslint-config-react-app": "^7.0.1",
28 | "eslint-plugin-cypress": "^2.15.1",
29 | "eslint-plugin-flowtype": "^8.0.3",
30 | "eslint-plugin-import": "^2.29.1",
31 | "eslint-plugin-jsx-a11y": "^6.8.0",
32 | "eslint-plugin-prettier": "^4.2.1",
33 | "eslint-plugin-react": "^7.33.2",
34 | "eslint-plugin-react-hooks": "^4.6.0",
35 | "husky": "^8.0.3",
36 | "jest": "^29.7.0",
37 | "jest-environment-jsdom": "^29.7.0",
38 | "lerna": "^6.6.2",
39 | "prettier": "~2.8.8",
40 | "raf": "^3.4.1",
41 | "supabase": "^2.12.1",
42 | "ts-jest": "^29.1.0",
43 | "typescript": "^5.7.3",
44 | "whatwg-fetch": "^3.0.0"
45 | },
46 | "resolutions": {
47 | "esbuild": "^0.24.2"
48 | },
49 | "scripts": {
50 | "build": "lerna run build",
51 | "build-demo": "cd packages/demo && yarn build",
52 | "run-demo": "cd packages/demo && yarn start",
53 | "run-demo-prod": "cd packages/demo && yarn serve",
54 | "test-unit": "jest",
55 | "test-e2e": "cypress run",
56 | "test-e2e-local": "cypress open",
57 | "watch": "lerna run --parallel watch",
58 | "lint": "eslint --fix --ext .js,.ts,.tsx \"./packages/**/src/**/*.{js,ts,tsx}\"",
59 | "prettier": "prettier --write \"./packages/**/src/**/*.{js,ts,tsx}\"",
60 | "db-seed": "cd packages/demo && yarn db-seed",
61 | "db-migrate": "cd packages/demo && yarn supabase db reset",
62 | "cypress:open": "cypress open"
63 | },
64 | "workspaces": [
65 | "packages/*"
66 | ],
67 | "packageManager": "yarn@4.6.0"
68 | }
69 |
--------------------------------------------------------------------------------
/packages/demo/.env.local-example:
--------------------------------------------------------------------------------
1 | VITE_SUPABASE_URL=http://localhost:54321
2 | VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
3 | SUPABASE_SERVICE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU
4 |
--------------------------------------------------------------------------------
/packages/demo/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env
15 | .env.local
16 | .env.development.local
17 | .env.test.local
18 | .env.production.local
19 | .eslintcache
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/packages/demo/README.md:
--------------------------------------------------------------------------------
1 | # React-admin CRM
2 |
3 | This is a demo of the [react-admin](https://github.com/marmelab/react-admin) library for React.js. It's a CRM for a fake Web agency with a few sales. You can test it online at https://marmelab.com/react-admin-crm.
4 |
5 | https://user-images.githubusercontent.com/99944/116970434-4a926480-acb8-11eb-8ce2-0602c680e45e.mp4
6 |
7 | React-admin usually requires a REST/GraphQL server to provide data. In this demo however, the API is simulated by the browser (using [FakeRest](https://github.com/marmelab/FakeRest)). The source data is generated at runtime by a package called [data-generator](https://github.com/marmelab/react-admin/tree/master/examples/data-generator).
8 |
9 | To explore the source code, start with [src/App.tsx](https://github.com/marmelab/react-admin/blob/master/examples/crm/src/App.tsx).
10 |
11 | **Note**: This project was bootstrapped with [Vite](https://vitejs.dev/).
12 |
13 | ## How to run
14 |
15 | After having cloned the react-admin repository, run the following commands at the react-admin root:
16 |
17 | ```sh
18 | make install
19 |
20 | make build
21 |
22 | make run-crm
23 | ```
24 |
25 | ## Available Scripts
26 |
27 | In the project directory, you can run:
28 |
29 | ### `npm start`
30 |
31 | Runs the app in the development mode.
32 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
33 |
34 | The page will reload if you make edits.
35 | You will also see any lint errors in the console.
36 |
37 | ### `npm test`
38 |
39 | Launches the test runner in the interactive watch mode.
40 | See the section about [running tests](#running-tests) for more information.
41 |
42 | ### `npm run build`
43 |
44 | Builds the app for production to the `build` folder.
45 | It correctly bundles React in production mode and optimizes the build for the best performance.
46 |
47 | The build is minified and the filenames include the hashes.
48 | Your app is ready to be deployed!
49 |
50 | ### `npm run deploy`
51 |
52 | Deploy the build to GitHub gh-pages.
53 |
--------------------------------------------------------------------------------
/packages/demo/db-seed.ts:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 | import { createClient } from '@supabase/supabase-js';
3 | import { generateSales } from './src/dataGenerator/sales';
4 | import { generateTags } from './src/dataGenerator/tags';
5 | import { generateCompanies } from './src/dataGenerator/companies';
6 | import { generateContacts } from './src/dataGenerator/contacts';
7 | import { generateContactNotes } from './src/dataGenerator/contactNotes';
8 | import { generateDeals } from './src/dataGenerator/deals';
9 | import { generateDealNotes } from './src/dataGenerator/dealNotes';
10 | import { generateTasks } from './src/dataGenerator/tasks';
11 |
12 | dotenv.config();
13 |
14 | async function main() {
15 | const supabase = createClient(
16 | process.env.VITE_SUPABASE_URL as string,
17 | process.env.SUPABASE_SERVICE_KEY as string
18 | );
19 |
20 | const sales = generateSales();
21 |
22 | console.log('Adding sales...');
23 | const { data: persistedSales, error: errorSales } = await supabase
24 | .from('sales')
25 | .insert(sales.map(({ id, ...sale }) => sale))
26 | .select();
27 |
28 | if (errorSales) {
29 | throw errorSales;
30 | }
31 |
32 | console.log('Adding tags...');
33 | const tags = generateTags();
34 | const { data: persistedTags, error: errorTags } = await supabase
35 | .from('tags')
36 | .insert(tags.map(({ id, ...tag }) => tag))
37 | .select();
38 |
39 | if (errorTags) {
40 | throw errorTags;
41 | }
42 |
43 | console.log('Adding companies...');
44 | const companies = generateCompanies({ sales: persistedSales });
45 | const { data: persistedCompanies, error: errorCompanies } = await supabase
46 | .from('companies')
47 | .insert(
48 | companies.map(
49 | ({ nb_contacts, nb_deals, id, ...company }) => company
50 | )
51 | )
52 | .select();
53 |
54 | if (errorCompanies) {
55 | throw errorCompanies;
56 | }
57 |
58 | console.log('Adding contacts...');
59 | const contacts = generateContacts({
60 | tags: persistedTags,
61 | companies: persistedCompanies,
62 | });
63 | const { data: persistedContacts, error: errorContacts } = await supabase
64 | .from('contacts')
65 | .insert(contacts.map(({ id, ...contact }) => contact))
66 | .select();
67 |
68 | if (errorContacts) {
69 | throw errorContacts;
70 | }
71 |
72 | console.log('Adding contact notes...');
73 | const contactNotes = generateContactNotes({
74 | contacts: persistedContacts,
75 | });
76 | const { data: persistedContactNotes, error: errorContactNotes } =
77 | await supabase
78 | .from('contactNotes')
79 | .insert(contactNotes.map(({ id, ...note }) => note))
80 | .select();
81 |
82 | if (errorContactNotes) {
83 | throw errorContactNotes;
84 | }
85 |
86 | console.log('Updating contacts status...');
87 | await Promise.all(
88 | persistedContactNotes
89 | .sort(
90 | (a, b) =>
91 | new Date(a.date).valueOf() - new Date(b.date).valueOf()
92 | )
93 | .map(note => {
94 | const relatedContact = persistedContacts.find(
95 | contact => contact.id === note.contact_id
96 | );
97 | return supabase.from('contacts').update({
98 | ...relatedContact,
99 | status: note.status,
100 | });
101 | })
102 | );
103 |
104 | console.log('Adding deals...');
105 | const deals = generateDeals({
106 | companies: persistedCompanies,
107 | contacts: persistedContacts,
108 | });
109 | const { data: persistedDeals, error: errorDeals } = await supabase
110 | .from('deals')
111 | .insert(deals.map(({ id, ...deal }) => deal))
112 | .select();
113 |
114 | if (errorDeals) {
115 | throw errorDeals;
116 | }
117 |
118 | console.log('Adding deal notes...');
119 | const dealNotes = generateDealNotes({
120 | companies: persistedCompanies,
121 | deals: persistedDeals,
122 | });
123 | const { error: errorDealNotes } = await supabase
124 | .from('dealNotes')
125 | .insert(dealNotes.map(({ id, ...note }) => note));
126 |
127 | if (errorDealNotes) {
128 | throw errorDealNotes;
129 | }
130 |
131 | console.log('Adding tasks...');
132 | const tasks = generateTasks({
133 | contacts: persistedContacts,
134 | });
135 | const { error: errorTasks } = await supabase
136 | .from('tasks')
137 | .insert(tasks.map(({ id, ...task }) => task));
138 |
139 | if (errorTasks) {
140 | throw errorTasks;
141 | }
142 |
143 | console.log('Adding users...');
144 | for (const sale of sales) {
145 | const { error } = await supabase.auth.admin.createUser({
146 | email: sale.email,
147 | password: 'password',
148 | email_confirm: true,
149 | });
150 |
151 | if (error) {
152 | throw error;
153 | }
154 | }
155 |
156 | console.log('Data seeded successfully');
157 | }
158 |
159 | main().catch(error => {
160 | console.error(error);
161 |
162 | process.exit(1);
163 | });
164 |
--------------------------------------------------------------------------------
/packages/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
11 |
12 | CRM Demo
13 |
109 |
110 |
114 |
115 |
116 |
117 |
118 |
123 |
124 |
125 |
126 |
--------------------------------------------------------------------------------
/packages/demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-admin-crm",
3 | "version": "3.5.0",
4 | "private": true,
5 | "dependencies": {
6 | "date-fns": "^3.6.0",
7 | "faker": "~5.4.0",
8 | "ra-supabase": "^3.5.0",
9 | "react": "^18.2.0",
10 | "react-admin": "^5.8.0",
11 | "react-dom": "^18.2.0"
12 | },
13 | "devDependencies": {
14 | "@types/faker": "~5.1.7",
15 | "@types/react": "^18.0.0",
16 | "@types/react-dom": "^18.0.0",
17 | "@vitejs/plugin-react": "^4.3.4",
18 | "source-map-explorer": "^2.0.0",
19 | "supabase": "^2.12.1",
20 | "tsx": "^4.19.2",
21 | "typescript": "^5.7.3",
22 | "vite": "^6.1.0",
23 | "web-vitals": "^1.0.1"
24 | },
25 | "scripts": {
26 | "start": "vite --port 8000",
27 | "serve": "vite preview --port 8000",
28 | "build": "vite build",
29 | "test": "vite test",
30 | "lint": "eslint --fix ./src",
31 | "db-seed": "tsx db-seed.ts"
32 | },
33 | "homepage": ".",
34 | "browserslist": {
35 | "production": [
36 | ">0.2%",
37 | "not dead",
38 | "not op_mini all"
39 | ],
40 | "development": [
41 | "last 1 chrome version",
42 | "last 1 firefox version",
43 | "last 1 safari version"
44 | ]
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/packages/demo/public/auth-callback.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
11 |
12 | Atomic CRM
13 |
109 |
110 |
111 |
112 |
113 |
118 |
119 |
140 |
141 |
--------------------------------------------------------------------------------
/packages/demo/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/favicon.ico
--------------------------------------------------------------------------------
/packages/demo/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
14 |
15 |
24 | Atomic CRM
25 |
26 |
30 |
31 |
32 |
33 |
34 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/packages/demo/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logo192.png
--------------------------------------------------------------------------------
/packages/demo/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logo512.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/0.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/1.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/10.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/11.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/12.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/13.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/13.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/14.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/14.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/15.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/15.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/16.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/17.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/17.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/18.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/18.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/19.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/2.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/20.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/21.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/21.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/22.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/22.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/23.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/23.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/24.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/25.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/25.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/26.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/26.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/27.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/27.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/28.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/28.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/29.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/3.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/30.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/30.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/31.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/31.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/32.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/33.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/33.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/34.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/34.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/35.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/35.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/36.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/36.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/37.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/37.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/38.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/38.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/39.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/39.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/4.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/40.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/41.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/41.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/42.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/42.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/43.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/43.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/44.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/44.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/45.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/45.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/46.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/46.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/47.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/47.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/48.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/49.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/49.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/5.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/50.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/50.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/51.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/51.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/52.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/52.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/53.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/53.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/54.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/54.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/55.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/55.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/6.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/7.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/8.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/demo/public/logos/9.png
--------------------------------------------------------------------------------
/packages/demo/public/logos/Readme.md:
--------------------------------------------------------------------------------
1 | Logos from https://uilogos.co/
--------------------------------------------------------------------------------
/packages/demo/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/packages/demo/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/packages/demo/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { AdminGuesser } from 'ra-supabase';
2 |
3 | const App = () => (
4 |
8 | );
9 |
10 | export default App;
11 |
--------------------------------------------------------------------------------
/packages/demo/src/dataGenerator/companies.ts:
--------------------------------------------------------------------------------
1 | import { company, internet, address, phone, random } from 'faker/locale/en_US';
2 | import { randomDate } from './utils';
3 |
4 | import { Db } from './types';
5 | import { Company } from '../types';
6 |
7 | const sectors = [
8 | 'Communication Services',
9 | 'Consumer Discretionary',
10 | 'Consumer Staples',
11 | 'Energy',
12 | 'Financials',
13 | 'Health Care',
14 | 'Industrials',
15 | 'Information Technology',
16 | 'Materials',
17 | 'Real Estate',
18 | 'Utilities',
19 | ];
20 |
21 | const sizes = [1, 10, 50, 250, 500];
22 |
23 | const regex = /\W+/;
24 |
25 | export const generateCompanies = (db: Pick): Company[] => {
26 | return Array.from(Array(55).keys()).map(id => {
27 | const name = company.companyName();
28 | return {
29 | id,
30 | name: name,
31 | logo: `/logos/${id}.png`,
32 | sector: random.arrayElement(sectors),
33 | size: random.arrayElement(sizes) as 1 | 10 | 50 | 250 | 500,
34 | linkedIn: `https://www.linkedin.com/company/${name
35 | .toLowerCase()
36 | .replace(regex, '_')}`,
37 | website: internet.url(),
38 | phone_number: phone.phoneNumber(),
39 | address: address.streetAddress(),
40 | zipcode: address.zipCode(),
41 | city: address.city(),
42 | stateAbbr: address.stateAbbr(),
43 | nb_contacts: 0,
44 | nb_deals: 0,
45 | // at least 1/3rd of companies for Jane Doe
46 | sales_id:
47 | random.number({ min: 0, max: 2 }) === 0
48 | ? 1
49 | : random.arrayElement(db.sales).id,
50 | created_at: randomDate().toISOString(),
51 | };
52 | });
53 | };
54 |
--------------------------------------------------------------------------------
/packages/demo/src/dataGenerator/contactNotes.ts:
--------------------------------------------------------------------------------
1 | import { random, lorem } from 'faker/locale/en_US';
2 |
3 | import { Db } from './types';
4 | import { ContactNote } from '../types';
5 | import { randomDate } from './utils';
6 |
7 | const status = ['cold', 'cold', 'cold', 'warm', 'warm', 'hot', 'in-contract'];
8 |
9 | export const generateContactNotes = (
10 | db: Pick
11 | ): ContactNote[] => {
12 | return Array.from(Array(1200).keys()).map(id => {
13 | const contact = random.arrayElement(db.contacts);
14 | const date = randomDate(new Date(contact.first_seen)).toISOString();
15 | contact.last_seen = date > contact.last_seen ? date : contact.last_seen;
16 | return {
17 | id,
18 | contact_id: contact.id,
19 | text: lorem.paragraphs(random.number({ min: 1, max: 4 })),
20 | date,
21 | sales_id: contact.sales_id,
22 | status: random.arrayElement(status),
23 | };
24 | });
25 | };
26 |
--------------------------------------------------------------------------------
/packages/demo/src/dataGenerator/contacts.ts:
--------------------------------------------------------------------------------
1 | import {
2 | name,
3 | internet,
4 | random,
5 | company as fakerCompany,
6 | phone,
7 | lorem,
8 | } from 'faker/locale/en_US';
9 |
10 | import { randomDate, weightedBoolean } from './utils';
11 | import { Db } from './types';
12 | import { Contact } from '../types';
13 |
14 | const genders = ['male', 'female'];
15 | const status = ['cold', 'cold', 'cold', 'warm', 'warm', 'hot', 'in-contract'];
16 | const maxContacts = {
17 | 1: 1,
18 | 10: 4,
19 | 50: 12,
20 | 250: 25,
21 | 500: 50,
22 | };
23 |
24 | export const generateContacts = (
25 | db: Pick
26 | ): Contact[] => {
27 | const nbAvailblePictures = 223;
28 | let numberOfContacts = 0;
29 |
30 | return Array.from(Array(500).keys()).map(id => {
31 | const has_avatar =
32 | weightedBoolean(25) && numberOfContacts < nbAvailblePictures;
33 | const gender = random.arrayElement(genders);
34 | const first_name = name.firstName(gender as any);
35 | const last_name = name.lastName();
36 | const email = internet.email(first_name, last_name);
37 | const avatar = has_avatar
38 | ? 'https://marmelab.com/posters/avatar-' +
39 | (223 - numberOfContacts) +
40 | '.jpeg'
41 | : undefined;
42 | const title = fakerCompany.bsAdjective();
43 |
44 | if (has_avatar) {
45 | numberOfContacts++;
46 | }
47 |
48 | // choose company with people left to know
49 | let company;
50 | do {
51 | company = random.arrayElement(db.companies);
52 | } while (company.nb_contacts >= maxContacts[company.size]);
53 | company.nb_contacts++;
54 |
55 | const first_seen = randomDate(
56 | new Date(company.created_at)
57 | ).toISOString();
58 | const last_seen = first_seen;
59 |
60 | return {
61 | id,
62 | first_name,
63 | last_name,
64 | gender,
65 | title: title.charAt(0).toUpperCase() + title.substr(1),
66 | company_id: company.id,
67 | email,
68 | phone_number1: phone.phoneNumber(),
69 | phone_number2: phone.phoneNumber(),
70 | background: lorem.sentence(),
71 | acquisition: random.arrayElement(['inbound', 'outbound']),
72 | avatar,
73 | first_seen: first_seen,
74 | last_seen: last_seen,
75 | has_newsletter: weightedBoolean(30),
76 | status: random.arrayElement(status),
77 | tag_ids: random
78 | .arrayElements(db.tags, random.arrayElement([0, 0, 0, 1, 1, 2]))
79 | .map(tag => tag.id), // finalize
80 | sales_id: company.sales_id,
81 | };
82 | });
83 | };
84 |
--------------------------------------------------------------------------------
/packages/demo/src/dataGenerator/dealNotes.ts:
--------------------------------------------------------------------------------
1 | import { random, lorem } from 'faker/locale/en_US';
2 |
3 | import { Db } from './types';
4 | import { randomDate } from './utils';
5 |
6 | const type = ['Email', 'Call', 'Call', 'Call', 'Call', 'Meeting', 'Reminder'];
7 |
8 | export const generateDealNotes = (db: Pick) => {
9 | return Array.from(Array(300).keys()).map(id => {
10 | const deal = random.arrayElement(db.deals);
11 | const company = db.companies[deal.company_id as number];
12 | const date = company
13 | ? randomDate(new Date(company.created_at)).toISOString()
14 | : randomDate().toISOString();
15 | return {
16 | id,
17 | deal_id: deal.id,
18 | type: random.arrayElement(type),
19 | text: lorem.paragraphs(random.number({ min: 1, max: 4 })),
20 | date,
21 | sales_id: deal.sales_id,
22 | };
23 | });
24 | };
25 |
--------------------------------------------------------------------------------
/packages/demo/src/dataGenerator/deals.ts:
--------------------------------------------------------------------------------
1 | import { random, lorem } from 'faker/locale/en_US';
2 | import { add } from 'date-fns';
3 |
4 | import { Db } from './types';
5 | import { Deal } from '../types';
6 | import { randomDate } from './utils';
7 |
8 | const type = [
9 | 'Other',
10 | 'Copywriting',
11 | 'Print project',
12 | 'UI Design',
13 | 'Website design',
14 | ];
15 | const stages = [
16 | 'opportunity',
17 | 'proposal-sent',
18 | 'in-negociation',
19 | 'won',
20 | 'lost',
21 | 'delayed',
22 | ];
23 | //const tags = ["new deal", "upsell", "SAV"];
24 |
25 | export const generateDeals = (
26 | db: Pick
27 | ): Deal[] => {
28 | const deals = Array.from(Array(50).keys()).map(id => {
29 | const company = random.arrayElement(db.companies);
30 | company.nb_deals++;
31 | const contacts = random.arrayElements(
32 | db.contacts.filter(contact => contact.company_id === company.id),
33 | random.number({ min: 1, max: 3 })
34 | );
35 | const lowercaseName = lorem.words();
36 | const created_at = randomDate(
37 | new Date(company.created_at)
38 | ).toISOString();
39 | return {
40 | id,
41 | name: lowercaseName[0].toUpperCase() + lowercaseName.slice(1),
42 | company_id: company.id,
43 | contact_ids: contacts.map(contact => contact.id),
44 | type: random.arrayElement(type),
45 | stage: random.arrayElement(stages),
46 | description: lorem.paragraphs(random.number({ min: 1, max: 4 })),
47 | amount: random.number(1000) * 100,
48 | created_at: created_at,
49 | updated_at: randomDate(new Date(created_at)).toISOString(),
50 | start_at: randomDate(
51 | new Date(),
52 | add(new Date(), { months: 6 })
53 | ).toISOString(),
54 | sales_id: company.sales_id,
55 | index: 0,
56 | };
57 | });
58 | // compute index based on stage
59 | stages.forEach(stage => {
60 | deals
61 | .filter(deal => deal.stage === stage)
62 | .forEach((deal, index) => {
63 | deals[deal.id].index = index;
64 | });
65 | });
66 | return deals;
67 | };
68 |
--------------------------------------------------------------------------------
/packages/demo/src/dataGenerator/finalize.ts:
--------------------------------------------------------------------------------
1 | import { Db } from './types';
2 |
3 | export const finalize = (db: Db) => {
4 | // set contact status according to the latest note
5 | db.contactNotes
6 | .sort((a, b) => new Date(a.date).valueOf() - new Date(b.date).valueOf())
7 | .forEach(note => {
8 | db.contacts[note.contact_id as number].status = note.status;
9 | });
10 | };
11 |
--------------------------------------------------------------------------------
/packages/demo/src/dataGenerator/index.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-anonymous-default-export */
2 | import { generateSales } from './sales';
3 | import { generateTags } from './tags';
4 | import { generateCompanies } from './companies';
5 | import { generateContacts } from './contacts';
6 | import { generateContactNotes } from './contactNotes';
7 | import { generateTasks } from './tasks';
8 | import { generateDeals } from './deals';
9 | import { generateDealNotes } from './dealNotes';
10 | import { finalize } from './finalize';
11 | import { Db } from './types';
12 |
13 | export default (): Db => {
14 | const db = {} as Db;
15 | db.sales = generateSales(db);
16 | db.tags = generateTags(db);
17 | db.companies = generateCompanies(db);
18 | db.contacts = generateContacts(db);
19 | db.contactNotes = generateContactNotes(db);
20 | db.deals = generateDeals(db);
21 | db.dealNotes = generateDealNotes(db);
22 | db.tasks = generateTasks(db);
23 | finalize(db);
24 |
25 | return db;
26 | };
27 |
--------------------------------------------------------------------------------
/packages/demo/src/dataGenerator/sales.ts:
--------------------------------------------------------------------------------
1 | import { name, internet } from 'faker/locale/en_US';
2 |
3 | export const generateSales = () => {
4 | const randomSales = Array.from(Array(10).keys()).map(id => {
5 | const first_name = name.firstName();
6 | const last_name = name.lastName();
7 | const email = internet.email(first_name, last_name);
8 |
9 | return {
10 | id: id + 1,
11 | first_name,
12 | last_name,
13 | email,
14 | };
15 | });
16 | return [
17 | {
18 | id: 0,
19 | first_name: 'Jane',
20 | last_name: 'Doe',
21 | email: 'janedoe@atomic.dev',
22 | },
23 | ...randomSales,
24 | ];
25 | };
26 |
--------------------------------------------------------------------------------
/packages/demo/src/dataGenerator/tags.ts:
--------------------------------------------------------------------------------
1 | // --champagne-pink: #eddcd2ff;
2 | // --linen: #fff1e6ff;
3 | // --pale-pink: #fde2e4ff;
4 | // --mimi-pink: #fad2e1ff;
5 | // --powder-blue: #c5deddff;
6 | // --mint-cream: #dbe7e4ff;
7 | // --isabelline: #f0efebff;
8 | // --alice-blue: #d6e2e9ff;
9 | // --beau-blue: #bcd4e6ff;
10 | // --pale-cerulean: #99c1deff;
11 |
12 | const tags = [
13 | { id: 0, name: 'football-fan', color: '#eddcd2' },
14 | { id: 1, name: 'holiday-card', color: '#fff1e6' },
15 | { id: 2, name: 'influencer', color: '#fde2e4' },
16 | { id: 3, name: 'manager', color: '#fad2e1' },
17 | { id: 4, name: 'musician', color: '#c5dedd' },
18 | { id: 5, name: 'vip', color: '#dbe7e4' },
19 | ];
20 |
21 | export const generateTags = () => {
22 | return [...tags];
23 | };
24 |
--------------------------------------------------------------------------------
/packages/demo/src/dataGenerator/tasks.ts:
--------------------------------------------------------------------------------
1 | import { random, lorem } from 'faker/locale/en_US';
2 |
3 | import { Db } from './types';
4 | import { randomDate } from './utils';
5 |
6 | const type = [
7 | 'Email',
8 | 'Email',
9 | 'Email',
10 | 'Email',
11 | 'Email',
12 | 'Email',
13 | 'Call',
14 | 'Call',
15 | 'Call',
16 | 'Call',
17 | 'Call',
18 | 'Call',
19 | 'Call',
20 | 'Call',
21 | 'Call',
22 | 'Call',
23 | 'Call',
24 | 'Demo',
25 | 'Lunch',
26 | 'Meeting',
27 | 'Follow-up',
28 | 'Follow-up',
29 | 'Thank you',
30 | 'Ship',
31 | 'None',
32 | ];
33 |
34 | export const generateTasks = (db: Pick) => {
35 | return Array.from(Array(400).keys()).map(id => {
36 | const contact = random.arrayElement(db.contacts);
37 | return {
38 | id,
39 | contact_id: contact.id,
40 | type: random.arrayElement(type),
41 | text: lorem.sentence(),
42 | due_date: randomDate(new Date(contact.first_seen)),
43 | sales_id: contact.sales_id,
44 | };
45 | });
46 | };
47 |
--------------------------------------------------------------------------------
/packages/demo/src/dataGenerator/types.ts:
--------------------------------------------------------------------------------
1 | import { RaRecord } from 'react-admin';
2 | import { Company, Contact, ContactNote, Deal, Sale, Tag } from '../types';
3 |
4 | export interface Db {
5 | companies: Company[];
6 | contacts: Contact[];
7 | contactNotes: ContactNote[];
8 | deals: Deal[];
9 | dealNotes: RaRecord[];
10 | sales: Sale[];
11 | tags: Tag[];
12 | tasks: RaRecord[];
13 | }
14 |
--------------------------------------------------------------------------------
/packages/demo/src/dataGenerator/utils.ts:
--------------------------------------------------------------------------------
1 | import faker from 'faker/locale/en';
2 |
3 | export const weightedArrayElement = (values: any[], weights: any) =>
4 | faker.random.arrayElement(
5 | values.reduce(
6 | (acc, value, index) =>
7 | acc.concat(new Array(weights[index]).fill(value)),
8 | []
9 | )
10 | );
11 |
12 | export const weightedBoolean = (likelyhood: number) =>
13 | faker.random.number(99) < likelyhood;
14 |
15 | export const randomDate = (minDate?: Date, maxDate?: Date) => {
16 | const minTs =
17 | minDate instanceof Date
18 | ? minDate.getTime()
19 | : Date.now() - 5 * 365 * 24 * 60 * 60 * 1000; // 5 years
20 | const maxTs = maxDate instanceof Date ? maxDate.getTime() : Date.now();
21 | const range = maxTs - minTs;
22 | const randomRange = faker.random.number({ max: range });
23 | // move it more towards today to account for traffic increase
24 | const ts = Math.sqrt(randomRange / range) * range;
25 | return new Date(minTs + ts);
26 | };
27 |
28 | export const randomFloat = (min: number, max: number) =>
29 | parseFloat(faker.random.number({ min, max, precision: 0.01 }).toFixed(2));
30 |
--------------------------------------------------------------------------------
/packages/demo/src/env.d.ts:
--------------------------------------------------------------------------------
1 | interface ImportMetaEnv {
2 | VITE_SUPABASE_URL: string;
3 | VITE_SUPABASE_ANON_KEY: string;
4 | }
5 |
6 | interface ImportMeta {
7 | readonly env: ImportMetaEnv;
8 | }
9 |
--------------------------------------------------------------------------------
/packages/demo/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import App from './App';
4 | import reportWebVitals from './reportWebVitals';
5 |
6 | const root = createRoot(document.getElementById('root') as HTMLElement);
7 | root.render(
8 |
9 |
10 |
11 | );
12 |
13 | // If you want to start measuring performance in your app, pass a function
14 | // to log results (for example: reportWebVitals(console.log))
15 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
16 | reportWebVitals();
17 |
--------------------------------------------------------------------------------
/packages/demo/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/demo/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/packages/demo/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(
4 | ({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
5 | getCLS(onPerfEntry);
6 | getFID(onPerfEntry);
7 | getFCP(onPerfEntry);
8 | getLCP(onPerfEntry);
9 | getTTFB(onPerfEntry);
10 | }
11 | );
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/packages/demo/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/packages/demo/src/types.ts:
--------------------------------------------------------------------------------
1 | import { Identifier, RaRecord } from 'react-admin';
2 |
3 | export interface Sale extends RaRecord {
4 | first_name: string;
5 | last_name: string;
6 | email: string;
7 | }
8 |
9 | export interface Company extends RaRecord {
10 | name: string;
11 | logo: string;
12 | sector: string;
13 | size: 1 | 10 | 50 | 250 | 500;
14 | linkedIn: string;
15 | website: string;
16 | phone_number: string;
17 | address: string;
18 | zipcode: string;
19 | city: string;
20 | stateAbbr: string;
21 | nb_contacts: number;
22 | nb_deals: number;
23 | sales_id: Identifier;
24 | created_at: string;
25 | }
26 |
27 | export interface Contact extends RaRecord {
28 | first_name: string;
29 | last_name: string;
30 | title: string;
31 | company_id: Identifier;
32 | email: string;
33 | avatar?: string;
34 | first_seen: string;
35 | last_seen: string;
36 | has_newsletter: Boolean;
37 | tag_ids: Identifier[];
38 | gender: string;
39 | sales_id: Identifier;
40 | status: string;
41 | background: string;
42 | }
43 |
44 | export interface ContactNote extends RaRecord {
45 | contact_id: Identifier;
46 | text: string;
47 | date: string;
48 | sales_id: Identifier;
49 | status: string;
50 | }
51 |
52 | export interface Deal extends RaRecord {
53 | name: string;
54 | company_id: Identifier;
55 | contact_ids: Identifier[];
56 | type: string;
57 | stage: string;
58 | description: string;
59 | amount: number;
60 | created_at: string;
61 | updated_at: string;
62 | start_at: string;
63 | sales_id: Identifier;
64 | index: number;
65 | }
66 |
67 | export interface Tag extends RaRecord {
68 | name: string;
69 | color: string;
70 | }
71 |
--------------------------------------------------------------------------------
/packages/demo/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/packages/demo/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import path from 'path';
3 | import fs from 'fs';
4 | import react from '@vitejs/plugin-react';
5 |
6 | const packages = fs.readdirSync(path.resolve(__dirname, '../../packages'));
7 | const aliases = packages.reduce((acc, dirName) => {
8 | if (dirName === 'demo') return acc;
9 | const packageJson = require(path.resolve(
10 | __dirname,
11 | '../../packages',
12 | dirName,
13 | 'package.json'
14 | ));
15 | acc[packageJson.name] = path.resolve(
16 | __dirname,
17 | `${path.resolve('../..')}/packages/${packageJson.name}/src`
18 | );
19 | return acc;
20 | }, {});
21 |
22 | // https://vitejs.dev/config/
23 | export default defineConfig({
24 | plugins: [react()],
25 | define: {
26 | 'process.env': process.env,
27 | },
28 | server: {
29 | port: 8000,
30 | open: true,
31 | },
32 | base: './',
33 | esbuild: {
34 | keepNames: true,
35 | },
36 | build: {
37 | sourcemap: true,
38 | },
39 | resolve: {
40 | preserveSymlinks: true,
41 | alias: [
42 | {
43 | find: 'ra-core',
44 | replacement: path.resolve(
45 | __dirname,
46 | '../../node_modules/ra-core/src'
47 | ),
48 | },
49 | {
50 | find: 'ra-i18n-polyglot',
51 | replacement: path.resolve(
52 | __dirname,
53 | '../../node_modules/ra-i18n-polyglot/src'
54 | ),
55 | },
56 | {
57 | find: 'ra-language-english',
58 | replacement: path.resolve(
59 | __dirname,
60 | '../../node_modules/ra-language-english/src'
61 | ),
62 | },
63 | {
64 | find: 'ra-ui-materialui',
65 | replacement: path.resolve(
66 | __dirname,
67 | '../../node_modules/ra-ui-materialui/src'
68 | ),
69 | },
70 | {
71 | find: 'react-admin',
72 | replacement: path.resolve(
73 | __dirname,
74 | '../../node_modules/react-admin/src'
75 | ),
76 | },
77 | // we need to manually follow the symlinks for local packages to allow deep HMR
78 | ...Object.keys(aliases).map(packageName => ({
79 | find: packageName,
80 | replacement: aliases[packageName],
81 | })),
82 | ],
83 | },
84 | });
85 |
--------------------------------------------------------------------------------
/packages/ra-supabase-core/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 |
--------------------------------------------------------------------------------
/packages/ra-supabase-core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ra-supabase-core",
3 | "version": "3.5.0",
4 | "repository": "git@github.com:marmelab/ra-supabase.git",
5 | "author": "Gildas Garcia <1122076+djhi@users.noreply.github.com>",
6 | "license": "MIT",
7 | "files": [
8 | "*.md",
9 | "lib",
10 | "esm",
11 | "src"
12 | ],
13 | "main": "lib/index",
14 | "module": "esm/index.js",
15 | "types": "esm/index.d.ts",
16 | "sideEffects": false,
17 | "peerDependencies": {
18 | "ra-core": "^5.8.0"
19 | },
20 | "dependencies": {
21 | "@raphiniert/ra-data-postgrest": "^2.4.1",
22 | "@supabase/supabase-js": "^2.48.1",
23 | "openapi-types": "^12.1.3"
24 | },
25 | "devDependencies": {
26 | "@testing-library/jest-dom": "^6.4.5",
27 | "@testing-library/react": "^15.0.7",
28 | "@testing-library/user-event": "^14.5.2",
29 | "ra-core": "^5.8.0",
30 | "react": "^18.3.1",
31 | "react-dom": "^18.3.1",
32 | "react-router": "^6.28.1",
33 | "react-router-dom": "^6.28.1",
34 | "rimraf": "^6.0.1",
35 | "typescript": "^5.7.3"
36 | },
37 | "scripts": {
38 | "build": "yarn run build-cjs && yarn run build-esm",
39 | "build-cjs": "rimraf ./lib && tsc",
40 | "build-esm": "rimraf ./esm && tsc --outDir esm --module es2015",
41 | "watch": "tsc --outDir esm --module es2015 --watch",
42 | "lint": "eslint --fix ./src",
43 | "test-unit": "jest ./src"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/packages/ra-supabase-core/src/dataProvider.ts:
--------------------------------------------------------------------------------
1 | import { DataProvider, fetchUtils } from 'ra-core';
2 | import postgrestRestProvider, {
3 | IDataProviderConfig,
4 | defaultPrimaryKeys,
5 | defaultSchema,
6 | } from '@raphiniert/ra-data-postgrest';
7 | import { createClient } from '@supabase/supabase-js';
8 | import type { SupabaseClient } from '@supabase/supabase-js';
9 | import type { OpenAPIV2 } from 'openapi-types';
10 |
11 | /**
12 | * A function that returns a dataProvider for Supabase.
13 | * @param instanceUrl The URL of the Supabase instance
14 | * @param apiKey The API key of the Supabase instance. Prefer the anonymous key.
15 | * @param supabaseClient The Supabase client
16 | * @param httpClient Optional - The httpClient to use. Defaults to a httpClient that handles the authentication.
17 | * @param defaultListOp Optional - The default list filter operator. Defaults to 'eq'.
18 | * @param primaryKeys Optional - The primary keys of the tables. Defaults to 'id'.
19 | * @param schema Optional - The custom schema to use. Defaults to none.
20 | * @returns A dataProvider for Supabase
21 | */
22 | export const supabaseDataProvider = ({
23 | instanceUrl,
24 | apiKey,
25 | supabaseClient = createClient(instanceUrl, apiKey),
26 | httpClient = supabaseHttpClient({ apiKey, supabaseClient }),
27 | defaultListOp = 'eq',
28 | primaryKeys = defaultPrimaryKeys,
29 | schema = defaultSchema,
30 | ...rest
31 | }: {
32 | instanceUrl: string;
33 | apiKey: string;
34 | supabaseClient?: SupabaseClient;
35 | } & Partial>): DataProvider => {
36 | const config: IDataProviderConfig = {
37 | apiUrl: `${instanceUrl}/rest/v1`,
38 | httpClient,
39 | defaultListOp,
40 | primaryKeys,
41 | schema,
42 | ...rest,
43 | };
44 | return {
45 | supabaseClient: (url: string, options?: any) =>
46 | httpClient(`${config.apiUrl}/${url}`, options),
47 | getSchema: async (): Promise => {
48 | const { json } = await httpClient(`${config.apiUrl}/`, {});
49 | if (!json || !json.swagger) {
50 | throw new Error('The Open API schema is not readable');
51 | }
52 | return json;
53 | },
54 | ...postgrestRestProvider(config),
55 | };
56 | };
57 |
58 | /**
59 | * A function that returns a httpClient for Supabase. It handles the authentication.
60 | * @param apiKey The API key of the Supabase instance. Prefer the anonymous key.
61 | * @param supabaseClient The Supabase client
62 | * @returns A httpClient for Supabase
63 | */
64 | export const supabaseHttpClient =
65 | ({
66 | apiKey,
67 | supabaseClient,
68 | }: {
69 | apiKey: string;
70 | supabaseClient: SupabaseClient;
71 | }) =>
72 | async (url: string, options: any = {}) => {
73 | const { data } = await supabaseClient.auth.getSession();
74 | if (!options.headers) options.headers = new Headers({});
75 |
76 | if (supabaseClient['headers']) {
77 | Object.entries(supabaseClient['headers']).forEach(([name, value]) =>
78 | options.headers.set(name, value)
79 | );
80 | }
81 | if (data.session) {
82 | options.user = {
83 | authenticated: true,
84 | // This ensures that users are identified correctly and that RLS can be applied
85 | token: `Bearer ${data.session.access_token}`,
86 | };
87 | }
88 | // Always send the apiKey even if there isn't a session
89 | options.headers.set('apiKey', apiKey);
90 |
91 | return fetchUtils.fetchJson(url, options);
92 | };
93 |
--------------------------------------------------------------------------------
/packages/ra-supabase-core/src/getSearchString.ts:
--------------------------------------------------------------------------------
1 | export function getSearchString() {
2 | const search = window.location.search;
3 | const hash = window.location.hash.substring(1);
4 |
5 | return search && search !== ''
6 | ? search
7 | : hash.includes('?')
8 | ? hash.split('?')[1]
9 | : hash;
10 | }
11 |
--------------------------------------------------------------------------------
/packages/ra-supabase-core/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './authProvider';
2 | export * from './dataProvider';
3 | export * from './useAPISchema';
4 | export * from './useRedirectIfAuthenticated';
5 | export * from './useResetPassword';
6 | export * from './useSetPassword';
7 | export * from './useSupabaseAccessToken';
8 |
--------------------------------------------------------------------------------
/packages/ra-supabase-core/src/useAPISchema.ts:
--------------------------------------------------------------------------------
1 | import { useDataProvider } from 'react-admin';
2 | import { useQuery } from '@tanstack/react-query';
3 | import type { UseQueryOptions } from '@tanstack/react-query';
4 | import type { OpenAPIV2 } from 'openapi-types';
5 |
6 | export const useAPISchema = ({
7 | options,
8 | }: {
9 | options?: Partial<
10 | Omit, 'queryKey' | 'queryFn'>
11 | >;
12 | } = {}) => {
13 | const dataProvider = useDataProvider();
14 | if (!dataProvider.getSchema) {
15 | throw new Error(
16 | "The data provider doesn't have access to the database schema"
17 | );
18 | }
19 | return useQuery({
20 | queryKey: ['getSchema'],
21 | queryFn: () => dataProvider.getSchema() as Promise,
22 | staleTime: 1000 * 60, // 1 minute
23 | ...options,
24 | });
25 | };
26 |
--------------------------------------------------------------------------------
/packages/ra-supabase-core/src/useRedirectIfAuthenticated.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { CoreAdminContext, TestMemoryRouter } from 'ra-core';
3 | import { render, waitFor } from '@testing-library/react';
4 | import {
5 | useRedirectIfAuthenticated,
6 | UseRedirectIfAuthenticatedOptions,
7 | } from './useRedirectIfAuthenticated';
8 |
9 | describe('useRedirectIfAuthenticated', () => {
10 | const UseRedirectIfAuthenticated = ({
11 | redirectTo,
12 | }: {
13 | redirectTo?: UseRedirectIfAuthenticatedOptions;
14 | }) => {
15 | useRedirectIfAuthenticated(redirectTo);
16 |
17 | return null;
18 | };
19 |
20 | test('should not redirect users if they are not authenticated', async () => {
21 | const authProvider = {
22 | login: jest.fn(),
23 | logout: jest.fn(),
24 | checkAuth: jest
25 | .fn()
26 | .mockRejectedValue(new Error('Not authenticated')),
27 | checkError: jest.fn(),
28 | getPermissions: jest.fn(),
29 | setPassword: jest.fn(),
30 | };
31 |
32 | let location;
33 | render(
34 | {
37 | location = l;
38 | }}
39 | >
40 |
41 |
42 |
43 |
44 | );
45 |
46 | expect(authProvider.checkAuth).toHaveBeenCalled();
47 | expect(location).toEqual(
48 | expect.objectContaining({
49 | hash: '',
50 | pathname: '/login',
51 | search: '',
52 | })
53 | );
54 | });
55 |
56 | test('should redirect users if they are authenticated', async () => {
57 | const authProvider = {
58 | login: jest.fn(),
59 | logout: jest.fn(),
60 | checkAuth: jest.fn().mockResolvedValue(undefined),
61 | checkError: jest.fn(),
62 | getPermissions: jest.fn(),
63 | setPassword: jest.fn(),
64 | };
65 |
66 | let location;
67 | render(
68 | {
71 | location = l;
72 | }}
73 | >
74 |
75 |
76 |
77 |
78 | );
79 |
80 | expect(authProvider.checkAuth).toHaveBeenCalled();
81 | await waitFor(() => {
82 | expect(location).toEqual(
83 | expect.objectContaining({
84 | hash: '',
85 | pathname: '/',
86 | search: '',
87 | })
88 | );
89 | });
90 | });
91 |
92 | test('should redirect users to the provided path if they are authenticated', async () => {
93 | const authProvider = {
94 | login: jest.fn(),
95 | logout: jest.fn(),
96 | checkAuth: jest.fn().mockResolvedValue(undefined),
97 | checkError: jest.fn(),
98 | getPermissions: jest.fn(),
99 | setPassword: jest.fn(),
100 | };
101 |
102 | let location;
103 | render(
104 | {
107 | location = l;
108 | }}
109 | >
110 |
111 |
112 |
113 |
114 | );
115 |
116 | expect(authProvider.checkAuth).toHaveBeenCalled();
117 | await waitFor(() => {
118 | expect(location).toEqual(
119 | expect.objectContaining({
120 | hash: '',
121 | pathname: '/dashboard',
122 | search: '',
123 | })
124 | );
125 | });
126 | });
127 | });
128 |
--------------------------------------------------------------------------------
/packages/ra-supabase-core/src/useRedirectIfAuthenticated.ts:
--------------------------------------------------------------------------------
1 | import { useCheckAuth } from 'ra-core';
2 | import { useEffect } from 'react';
3 | import { To, useNavigate } from 'react-router';
4 |
5 | /**
6 | * This hook redirect the user to the provided path (/ by default) if they are authenticated.
7 | *
8 | * @example
9 | * import { useRedirectIfAuthenticated } from 'react-admin';
10 | * const MyLoginPage = () => {
11 | * useRedirectIfAuthenticated();
12 | * // UI and logic for authentication
13 | * }
14 | **/
15 | export const useRedirectIfAuthenticated = (
16 | redirectTo: UseRedirectIfAuthenticatedOptions = '/'
17 | ) => {
18 | const navigate = useNavigate();
19 | const checkAuth = useCheckAuth();
20 |
21 | useEffect(() => {
22 | checkAuth({}, false, undefined)
23 | .then(() => {
24 | // already authenticated, redirect to the home page
25 | navigate(redirectTo);
26 | })
27 | .catch(() => {
28 | // not authenticated, stay on the login page
29 | });
30 | }, [checkAuth, navigate, redirectTo]);
31 | };
32 |
33 | export type UseRedirectIfAuthenticatedOptions = To;
34 |
--------------------------------------------------------------------------------
/packages/ra-supabase-core/src/useResetPassword.ts:
--------------------------------------------------------------------------------
1 | import {
2 | OnError,
3 | OnSuccess,
4 | useAuthProvider,
5 | useNotify,
6 | useRedirect,
7 | } from 'ra-core';
8 | import { useMutation, UseMutationResult } from '@tanstack/react-query';
9 | import { ResetPasswordParams, SupabaseAuthProvider } from './authProvider';
10 |
11 | /**
12 | * This hook returns a function to call in order to reset a user password on Supabase.
13 | *
14 | * @example
15 | * import { useSupabaseAccessToken } from 'ra-supabase-core';
16 | *
17 | * const ResetPasswordPage = () => {
18 | * const [resetPassword] = useResetPassword();
19 | *
20 | * const handleSubmit = event => {
21 | * resetPassword({
22 | * email: event.currentTarget.elements.email.value,
23 | * });
24 | * };
25 | *
26 | * return (
27 | *
32 | * );
33 | * };
34 | **/
35 | export const useResetPassword = (
36 | options?: UseResetPasswordOptions
37 | ): [
38 | UseMutationResult['mutate'],
39 | UseMutationResult
40 | ] => {
41 | const notify = useNotify();
42 | const redirect = useRedirect();
43 | const authProvider = useAuthProvider();
44 |
45 | if (authProvider == null) {
46 | throw new Error(
47 | 'No authProvider found. Did you forget to set up an AuthProvider on the component?'
48 | );
49 | }
50 |
51 | if (authProvider.resetPassword == null) {
52 | throw new Error(
53 | 'The setPassword() method is missing from the AuthProvider although it is required. You may consider adding it'
54 | );
55 | }
56 |
57 | const {
58 | onSuccess = () => {
59 | redirect('/login');
60 | notify('ra-supabase.auth.password_reset', { type: 'info' });
61 | },
62 | onError = error => notify(error.message, { type: 'error' }),
63 | } = options || {};
64 |
65 | const mutation = useMutation({
66 | mutationFn: params => {
67 | return authProvider.resetPassword(params);
68 | },
69 | onSuccess,
70 | onError,
71 | retry: false,
72 | });
73 |
74 | return [mutation.mutate, mutation];
75 | };
76 |
77 | export type UseResetPasswordOptions = {
78 | onSuccess?: OnSuccess;
79 | onError?: OnError;
80 | };
81 |
--------------------------------------------------------------------------------
/packages/ra-supabase-core/src/useSetPassword.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { useEffect } from 'react';
3 | import { CoreAdminContext } from 'ra-core';
4 | import { render, waitFor } from '@testing-library/react';
5 | import { useSetPassword, UseSetPasswordOptions } from './useSetPassword';
6 |
7 | describe('useSetPassword', () => {
8 | const UseSetPassword = ({ onSuccess, onError }: UseSetPasswordOptions) => {
9 | const [setPassword] = useSetPassword({
10 | onSuccess,
11 | onError,
12 | });
13 |
14 | useEffect(() => {
15 | setPassword({
16 | access_token: 'token',
17 | refresh_token: 'refresh',
18 | password: 'bazinga',
19 | });
20 | }, [setPassword]);
21 |
22 | return null;
23 | };
24 |
25 | test('should accept a custom onSuccess function', async () => {
26 | const authProvider = {
27 | login: jest.fn(),
28 | logout: jest.fn(),
29 | checkAuth: jest.fn(),
30 | checkError: jest.fn(),
31 | getPermissions: jest.fn(),
32 | setPassword: jest.fn().mockResolvedValue(undefined),
33 | };
34 | const myOnSuccess = jest.fn();
35 |
36 | render(
37 |
38 |
39 |
40 | );
41 |
42 | await waitFor(() => {
43 | expect(authProvider.setPassword).toHaveBeenCalledWith({
44 | access_token: 'token',
45 | refresh_token: 'refresh',
46 | password: 'bazinga',
47 | });
48 | });
49 |
50 | await waitFor(() => {
51 | expect(myOnSuccess).toHaveBeenCalledTimes(1);
52 | });
53 | });
54 |
55 | test('should accept a custom onError function', async () => {
56 | const error = new Error('boo');
57 | const authProvider = {
58 | login: jest.fn(),
59 | logout: jest.fn(),
60 | checkAuth: jest.fn(),
61 | checkError: jest.fn(),
62 | getPermissions: jest.fn(),
63 | setPassword: jest.fn().mockRejectedValue(error),
64 | };
65 | const myOnError = jest.fn();
66 |
67 | render(
68 |
69 |
70 |
71 | );
72 |
73 | await waitFor(() => {
74 | expect(authProvider.setPassword).toHaveBeenCalledWith({
75 | access_token: 'token',
76 | refresh_token: 'refresh',
77 | password: 'bazinga',
78 | });
79 | });
80 | await waitFor(() => {
81 | expect(myOnError).toHaveBeenCalledWith(
82 | error,
83 | {
84 | access_token: 'token',
85 | refresh_token: 'refresh',
86 | password: 'bazinga',
87 | },
88 | undefined
89 | );
90 | });
91 | });
92 | });
93 |
--------------------------------------------------------------------------------
/packages/ra-supabase-core/src/useSetPassword.ts:
--------------------------------------------------------------------------------
1 | import {
2 | OnError,
3 | OnSuccess,
4 | useAuthProvider,
5 | useNotify,
6 | useRedirect,
7 | } from 'ra-core';
8 | import { useMutation, UseMutationResult } from '@tanstack/react-query';
9 | import { SetPasswordParams, SupabaseAuthProvider } from './authProvider';
10 |
11 | /**
12 | * This hook returns a function to call in order to set a user password on Supabase.
13 | *
14 | * @example
15 | * import { useSupabaseAccessToken } from 'ra-supabase-core';
16 | *
17 | * const SetPasswordPage = () => {
18 | * const access_token = useSupabaseAccessToken();
19 | * const setPassword = useSetPassword();
20 | *
21 | * const handleSubmit = event => {
22 | * setPassword({
23 | * access_token,
24 | * password: event.currentTarget.elements.password.value,
25 | * });
26 | * };
27 | *
28 | * return (
29 | *
34 | * );
35 | * };
36 | **/
37 | export const useSetPassword = (
38 | options?: UseSetPasswordOptions
39 | ): [
40 | UseMutationResult['mutate'],
41 | UseMutationResult
42 | ] => {
43 | const notify = useNotify();
44 | const redirect = useRedirect();
45 | const authProvider = useAuthProvider();
46 |
47 | if (authProvider == null) {
48 | throw new Error(
49 | 'No authProvider found. Did you forget to set up an AuthProvider on the component?'
50 | );
51 | }
52 |
53 | if (authProvider.setPassword == null) {
54 | throw new Error(
55 | 'The setPassword() method is missing from the AuthProvider although it is required. You may consider adding it'
56 | );
57 | }
58 |
59 | const {
60 | onSuccess = () => redirect('/'),
61 | onError = error => notify(error.message, { type: 'error' }),
62 | } = options || {};
63 |
64 | const mutation = useMutation({
65 | mutationFn: params => {
66 | return authProvider.setPassword(params);
67 | },
68 | onSuccess,
69 | onError,
70 | retry: false,
71 | });
72 |
73 | return [mutation.mutate, mutation];
74 | };
75 |
76 | export type UseSetPasswordOptions = {
77 | onSuccess?: OnSuccess;
78 | onError?: OnError;
79 | };
80 |
--------------------------------------------------------------------------------
/packages/ra-supabase-core/src/useSupabaseAccessToken.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render, waitFor } from '@testing-library/react';
3 | import { CoreAdminContext, TestMemoryRouter } from 'ra-core';
4 | import {
5 | useSupabaseAccessToken,
6 | UseSupabaseAccessTokenOptions,
7 | } from './useSupabaseAccessToken';
8 |
9 | // TODO: fix those tests
10 | describe('useSupabaseAccessToken', () => {
11 | const UseSupabaseAccessToken = (props?: UseSupabaseAccessTokenOptions) => {
12 | const token = useSupabaseAccessToken(props);
13 |
14 | return {token};
15 | };
16 |
17 | test('should return the access token if present in the URL', async () => {
18 | window.history.pushState(
19 | {},
20 | 'React Admin',
21 | '/set-password?access_token=bazinga'
22 | );
23 |
24 | const { queryByText } = render(
25 |
26 |
27 |
28 |
29 |
30 | );
31 |
32 | await waitFor(() => {
33 | expect(queryByText('bazinga')).not.toBeNull();
34 | });
35 | });
36 |
37 | test('should return the access token if present in the hash route', async () => {
38 | window.history.pushState(
39 | {},
40 | 'React Admin',
41 | '/set-password#access_token=bazinga'
42 | );
43 |
44 | const { queryByText } = render(
45 |
46 |
47 |
48 |
49 |
50 | );
51 |
52 | await waitFor(() => {
53 | expect(queryByText('bazinga')).not.toBeNull();
54 | });
55 | });
56 |
57 | test('should return the access token from the provided key if present in the URL', async () => {
58 | window.history.pushState(
59 | {},
60 | 'React Admin',
61 | '/set-password?my_token=bazinga'
62 | );
63 |
64 | const { queryByText } = render(
65 |
66 |
67 |
68 |
69 |
70 | );
71 |
72 | await waitFor(() => {
73 | expect(queryByText('bazinga')).not.toBeNull();
74 | });
75 | });
76 |
77 | test.skip('should redirect users if the access token is not present in the URL', async () => {
78 | window.history.pushState({}, 'React Admin', '/set-password');
79 |
80 | render(
81 |
82 |
83 |
84 |
85 |
86 | );
87 |
88 | await waitFor(() => {
89 | expect(window.location.pathname).toEqual('/');
90 | });
91 | });
92 |
93 | test.skip('should redirect users to the provided path if the access token is not present in the URL', async () => {
94 | window.history.pushState({}, 'React Admin', '/set-password');
95 |
96 | render(
97 |
98 |
99 |
100 |
101 |
102 | );
103 |
104 | await waitFor(() => {
105 | expect(window.location.pathname).toEqual('/login');
106 | });
107 | });
108 |
109 | test.skip('should not redirect users if the access token is not present in the URL and redirectTo is false', async () => {
110 | window.history.pushState({}, 'React Admin', '/set-password');
111 |
112 | render(
113 |
114 |
115 |
116 |
117 |
118 | );
119 |
120 | await waitFor(() => {
121 | expect(window.location.pathname).toEqual('/set-password');
122 | });
123 | });
124 | });
125 |
--------------------------------------------------------------------------------
/packages/ra-supabase-core/src/useSupabaseAccessToken.ts:
--------------------------------------------------------------------------------
1 | import { useRedirect } from 'ra-core';
2 | import { useEffect } from 'react';
3 | import { getSearchString } from './getSearchString';
4 |
5 | /**
6 | * This hook gets the access_token from supabase in the current browser URL and redirects to the specified page (/ by default) if there is none.
7 | * To be used in pages such as those which set the password after a reset or an invitation.
8 | *
9 | * @example
10 | * import { useSupabaseAccessToken } from 'ra-supabase-core';
11 | *
12 | * const SetPasswordPage = () => {
13 | * const access_token = useSupabaseAccessToken();
14 | * const setPassword = useSetPassword();
15 | *
16 | * const handleSubmit = event => {
17 | * setPassword({
18 | * access_token,
19 | * password: event.currentTarget.elements.password.value,
20 | * });
21 | * };
22 | *
23 | * return (
24 | *
29 | * );
30 | * };
31 | **/
32 | export const useSupabaseAccessToken = ({
33 | redirectTo = '/',
34 | parameterName = 'access_token',
35 | }: UseSupabaseAccessTokenOptions = {}) => {
36 | const redirect = useRedirect();
37 |
38 | const searchStr = getSearchString();
39 | const urlSearchParams = new URLSearchParams(searchStr);
40 | const access_token = urlSearchParams.get(parameterName);
41 | useEffect(() => {
42 | if (access_token == null) {
43 | if (redirectTo !== false) {
44 | redirect(redirectTo);
45 | }
46 | }
47 | });
48 |
49 | return access_token;
50 | };
51 |
52 | export type UseSupabaseAccessTokenOptions = {
53 | redirectTo?: string | false;
54 | parameterName?: string;
55 | };
56 |
--------------------------------------------------------------------------------
/packages/ra-supabase-core/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "lib",
5 | "rootDir": "src",
6 | "declaration": true,
7 | "declarationMap": true,
8 | "allowJs": false,
9 | "strictNullChecks": true
10 | },
11 | "exclude": ["**/*.spec.ts", "**/*.spec.tsx", "**/*.spec.js"],
12 | "include": ["src"],
13 | }
14 |
--------------------------------------------------------------------------------
/packages/ra-supabase-language-english/README.md:
--------------------------------------------------------------------------------
1 | # ra-supabase-language-english
2 |
3 | English translations for [ra-supabase](https://www.npmjs.com/package/ra-supabase).
4 |
5 | ## Installation
6 |
7 | ```sh
8 | yarn add ra-supabase-language-english
9 | # or
10 | npm install ra-supabase-language-english
11 | ```
12 |
13 | ## Usage
14 |
15 | ```js
16 | // in i18nProvider.js
17 | import { mergeTranslations } from 'ra-core';
18 | import polyglotI18nProvider from 'ra-i18n-polyglot';
19 | import englishMessages from 'ra-language-english';
20 | import frenchMessages from 'ra-language-french';
21 | import { raSupabaseEnglishMessages } from 'ra-supabase-language-english';
22 | import { raSupabaseFrenchMessages } from 'ra-supabase-language-french';
23 |
24 | const allEnglishMessages = mergeTranslations(
25 | englishMessages,
26 | raSupabaseEnglishMessages
27 | );
28 | const allFrenchMessages = mergeTranslations(
29 | frenchMessages,
30 | raSupabaseFrenchMessages
31 | );
32 |
33 | export const i18nProvider = polyglotI18nProvider(
34 | locale => (locale === 'fr' ? allFrenchMessages : allEnglishMessages),
35 | 'en'
36 | );
37 |
38 | // in App.js
39 | import { Admin, Resource, ListGuesser } from 'react-admin';
40 | import { authRoutes } from 'ra-supabase';
41 | import { dataProvider } from './dataProvider';
42 | import { authProvider } from './authProvider';
43 | import { i18nProvider } from './i18nProvider';
44 |
45 | export const MyAdmin = () => (
46 |
52 |
53 |
54 |
55 | );
56 | ```
57 |
--------------------------------------------------------------------------------
/packages/ra-supabase-language-english/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ra-supabase-language-english",
3 | "version": "3.5.0",
4 | "repository": "git@github.com:marmelab/ra-supabase.git",
5 | "author": "Gildas Garcia <1122076+djhi@users.noreply.github.com>",
6 | "license": "MIT",
7 | "files": [
8 | "*.md",
9 | "lib",
10 | "esm",
11 | "src"
12 | ],
13 | "main": "lib/index",
14 | "module": "esm/index.js",
15 | "types": "esm/index.d.ts",
16 | "sideEffects": false,
17 | "scripts": {
18 | "build": "yarn run build-cjs && yarn run build-esm",
19 | "build-cjs": "rimraf ./lib && tsc",
20 | "build-esm": "rimraf ./esm && tsc --outDir esm --module es2015",
21 | "watch": "tsc --outDir esm --module es2015 --watch",
22 | "lint": "eslint --fix ./src"
23 | },
24 | "devDependencies": {
25 | "rimraf": "^6.0.1",
26 | "typescript": "^5.7.3"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/packages/ra-supabase-language-english/src/index.ts:
--------------------------------------------------------------------------------
1 | export const raSupabaseEnglishMessages = {
2 | 'ra-supabase': {
3 | auth: {
4 | email: 'Email',
5 | confirm_password: 'Confirm password',
6 | sign_in_with: 'Sign in with %{provider}',
7 | forgot_password: 'Forgot password?',
8 | reset_password: 'Reset password',
9 | password_reset:
10 | 'Your password has been reset. You will receive an email containing a link to log in.',
11 | missing_tokens: 'Access and refresh tokens are missing',
12 | back_to_login: 'Back to login',
13 | },
14 | reset_password: {
15 | forgot_password: 'Forgot password?',
16 | forgot_password_details: 'Enter your email for instructions.',
17 | },
18 | set_password: {
19 | new_password: 'Choose your password',
20 | },
21 | validation: {
22 | password_mismatch: 'Passwords do not match',
23 | },
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/packages/ra-supabase-language-english/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "lib",
5 | "rootDir": "src",
6 | "declaration": true,
7 | "declarationMap": true,
8 | "allowJs": false,
9 | "strictNullChecks": true
10 | },
11 | "exclude": ["**/*.spec.ts", "**/*.spec.tsx", "**/*.spec.js"],
12 | "include": ["src"]
13 | }
14 |
--------------------------------------------------------------------------------
/packages/ra-supabase-language-french/README.md:
--------------------------------------------------------------------------------
1 | # ra-supabase-language-french
2 |
3 | French translations for [ra-supabase](https://www.npmjs.com/package/ra-supabase).
4 |
5 | ## Installation
6 |
7 | ```sh
8 | yarn add ra-supabase-language-french
9 | # or
10 | npm install ra-supabase-language-french
11 | ```
12 |
13 | ## Usage
14 |
15 | ```js
16 | // in i18nProvider.js
17 | import { mergeTranslations } from 'ra-core';
18 | import polyglotI18nProvider from 'ra-i18n-polyglot';
19 | import englishMessages from 'ra-language-english';
20 | import frenchMessages from 'ra-language-french';
21 | import { raSupabaseEnglishMessages } from 'ra-supabase-language-english';
22 | import { raSupabaseFrenchMessages } from 'ra-supabase-language-french';
23 |
24 | const allEnglishMessages = mergeTranslations(
25 | englishMessages,
26 | raSupabaseEnglishMessages
27 | );
28 | const allFrenchMessages = mergeTranslations(
29 | frenchMessages,
30 | raSupabaseFrenchMessages
31 | );
32 |
33 | export const i18nProvider = polyglotI18nProvider(
34 | locale => (locale === 'fr' ? allFrenchMessages : allEnglishMessages),
35 | 'en'
36 | );
37 |
38 | // in App.js
39 | import { Admin, Resource, ListGuesser } from 'react-admin';
40 | import { authRoutes } from 'ra-supabase';
41 | import { dataProvider } from './dataProvider';
42 | import { authProvider } from './authProvider';
43 | import { i18nProvider } from './i18nProvider';
44 |
45 | export const MyAdmin = () => (
46 |
52 |
53 |
54 |
55 | );
56 | ```
57 |
--------------------------------------------------------------------------------
/packages/ra-supabase-language-french/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ra-supabase-language-french",
3 | "version": "3.5.0",
4 | "repository": "git@github.com:marmelab/ra-supabase.git",
5 | "author": "Gildas Garcia <1122076+djhi@users.noreply.github.com>",
6 | "license": "MIT",
7 | "files": [
8 | "*.md",
9 | "lib",
10 | "esm",
11 | "src"
12 | ],
13 | "main": "lib/index",
14 | "module": "esm/index.js",
15 | "types": "esm/index.d.ts",
16 | "sideEffects": false,
17 | "scripts": {
18 | "build": "yarn run build-cjs && yarn run build-esm",
19 | "build-cjs": "rimraf ./lib && tsc",
20 | "build-esm": "rimraf ./esm && tsc --outDir esm --module es2015",
21 | "watch": "tsc --outDir esm --module es2015 --watch",
22 | "lint": "eslint --fix ./src"
23 | },
24 | "devDependencies": {
25 | "rimraf": "^6.0.1",
26 | "typescript": "^5.7.3"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/packages/ra-supabase-language-french/src/index.ts:
--------------------------------------------------------------------------------
1 | export const raSupabaseFrenchMessages = {
2 | 'ra-supabase': {
3 | auth: {
4 | email: 'Email',
5 | confirm_password: 'Confirmation du mot de passe',
6 | sign_in_with: 'Se connecter avec %{provider}',
7 | forgot_password: 'Mot de passe oublié ?',
8 | reset_password: 'Réinitialiser le mot de passe',
9 | password_reset:
10 | 'Votre mot de passe a été réinitialisé. Vous recevrez un email contenant un lien pour vous connecter.',
11 | missing_tokens:
12 | "Les jetons d'accès et de rafraîchissement sont manquants",
13 | back_to_login: 'Retour à la page de connexion',
14 | },
15 | reset_password: {
16 | forgot_password: 'Mot de passe oublié ?',
17 | forgot_password_details: 'Obtenez les instructions par email.',
18 | },
19 | set_password: {
20 | new_password: 'Nouveau mot de passe',
21 | },
22 | validation: {
23 | password_mismatch: 'Les mots de passe ne correspondent pas',
24 | },
25 | },
26 | };
27 |
--------------------------------------------------------------------------------
/packages/ra-supabase-language-french/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "lib",
5 | "rootDir": "src",
6 | "declaration": true,
7 | "declarationMap": true,
8 | "allowJs": false,
9 | "strictNullChecks": true
10 | },
11 | "exclude": ["**/*.spec.ts", "**/*.spec.tsx", "**/*.spec.js"],
12 | "include": ["src"]
13 | }
14 |
--------------------------------------------------------------------------------
/packages/ra-supabase-ui-materialui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ra-supabase-ui-materialui",
3 | "version": "3.5.0",
4 | "repository": "git@github.com:marmelab/ra-supabase.git",
5 | "author": "Gildas Garcia <1122076+djhi@users.noreply.github.com>",
6 | "license": "MIT",
7 | "files": [
8 | "*.md",
9 | "lib",
10 | "esm",
11 | "src"
12 | ],
13 | "main": "lib/index",
14 | "module": "esm/index.js",
15 | "types": "esm/index.d.ts",
16 | "sideEffects": false,
17 | "dependencies": {
18 | "ra-supabase-core": "^3.5.0"
19 | },
20 | "devDependencies": {
21 | "@mui/icons-material": "^5.16.12",
22 | "@mui/material": "^5.16.12",
23 | "@mui/system": "^5.16.12",
24 | "@supabase/supabase-js": "^2.48.1",
25 | "ra-core": "^5.8.0",
26 | "ra-ui-materialui": "^5.8.0",
27 | "react": "^18.3.1",
28 | "react-dom": "^18.3.1",
29 | "react-router": "^6.23.1",
30 | "rimraf": "^6.0.1",
31 | "typescript": "^5.7.3"
32 | },
33 | "peerDependencies": {
34 | "@mui/icons-material": "^5.16.12 || ^6.0.0 || ^7.0.0",
35 | "@mui/material": "^5.16.12 || ^6.0.0 || ^7.0.0",
36 | "@supabase/supabase-js": "^2.48.1",
37 | "ra-core": "^5.8.0",
38 | "ra-ui-materialui": "^5.8.0",
39 | "react": "^18.0.0 || ^19.0.0",
40 | "react-dom": "^18.0.0 || ^19.0.0",
41 | "react-router": "^6.28.1 || ^7.1.1"
42 | },
43 | "scripts": {
44 | "build": "yarn run build-cjs && yarn run build-esm",
45 | "build-cjs": "rimraf ./lib && tsc",
46 | "build-esm": "rimraf ./esm && tsc --outDir esm --module es2015",
47 | "watch": "tsc --outDir esm --module es2015 --watch",
48 | "lint": "eslint --fix ./src"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/packages/ra-supabase-ui-materialui/src/AuthLayout.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | HtmlHTMLAttributes,
3 | ComponentType,
4 | createElement,
5 | ReactNode,
6 | useRef,
7 | useEffect,
8 | } from 'react';
9 | import { Card, Avatar, styled } from '@mui/material';
10 | import LockIcon from '@mui/icons-material/Lock';
11 | import { TitleComponent } from 'ra-core';
12 | import { Notification } from 'ra-ui-materialui';
13 |
14 | /**
15 | * A standalone login page, to serve as authentication gate to the admin
16 | *
17 | * Expects the user to enter a login and a password, which will be checked
18 | * by the `authProvider.login()` method. Redirects to the root page (/)
19 | * upon success, otherwise displays an authentication error message.
20 | *
21 | * Copy and adapt this component to implement your own login logic
22 | * (e.g. to authenticate via email or facebook or anything else).
23 | *
24 | * @example
25 | * import MyLoginPage from './MyLoginPage';
26 | * const App = () => (
27 | *
28 | * ...
29 | *
30 | * );
31 | */
32 | export const AuthLayout: React.FunctionComponent = props => {
33 | const {
34 | title,
35 | classes: classesOverride,
36 | className,
37 | children,
38 | notification = Notification,
39 | backgroundImage,
40 | ...rest
41 | } = props;
42 | const containerRef = useRef(null);
43 | let backgroundImageLoaded = false;
44 |
45 | const updateBackgroundImage = () => {
46 | if (!backgroundImageLoaded && containerRef.current) {
47 | containerRef.current.style.backgroundImage = `url(${backgroundImage})`;
48 | backgroundImageLoaded = true;
49 | }
50 | };
51 |
52 | // Load background image asynchronously to speed up time to interactive
53 | const lazyLoadBackgroundImage = () => {
54 | if (backgroundImage) {
55 | const img = new Image();
56 | img.onload = updateBackgroundImage;
57 | img.src = backgroundImage;
58 | }
59 | };
60 |
61 | useEffect(() => {
62 | if (!backgroundImageLoaded) {
63 | lazyLoadBackgroundImage();
64 | }
65 | });
66 |
67 | return (
68 |
69 |
70 |
75 | {children}
76 |
77 | {notification ? createElement(notification) : null}
78 |
79 | );
80 | };
81 |
82 | export interface LoginProps
83 | extends Omit, 'title'> {
84 | backgroundImage?: string;
85 | children?: ReactNode;
86 | classes?: object;
87 | className?: string;
88 | notification?: ComponentType;
89 | title?: TitleComponent;
90 | }
91 |
92 | const PREFIX = 'RaAuthLayout';
93 |
94 | export const AuthLayoutClasses = {
95 | card: `${PREFIX}-card`,
96 | avatar: `${PREFIX}-avatar`,
97 | icon: `${PREFIX}-icon`,
98 | };
99 |
100 | const Root = styled('div', {
101 | name: PREFIX,
102 | overridesResolver: (props, styles) => styles.root,
103 | })(({ theme }) => ({
104 | display: 'flex',
105 | flexDirection: 'column',
106 | minHeight: '100vh',
107 | height: '1px',
108 | alignItems: 'center',
109 | justifyContent: 'flex-start',
110 | backgroundRepeat: 'no-repeat',
111 | backgroundSize: 'cover',
112 | backgroundImage:
113 | 'radial-gradient(circle at 50% 14em, #313264 0%, #00023b 60%, #00023b 100%)',
114 | [`& .${AuthLayoutClasses.card}`]: {
115 | minWidth: 300,
116 | marginTop: '6em',
117 | },
118 | [`& .${AuthLayoutClasses.avatar}`]: {
119 | margin: '1em',
120 | display: 'flex',
121 | justifyContent: 'center',
122 | },
123 | [`& .${AuthLayoutClasses.icon}`]: {
124 | backgroundColor: (theme.vars || theme).palette.grey[500],
125 | },
126 | }));
127 |
--------------------------------------------------------------------------------
/packages/ra-supabase-ui-materialui/src/ForgotPasswordForm.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { CardActions, Stack, styled, Typography } from '@mui/material';
3 | import { useResetPassword } from 'ra-supabase-core';
4 | import { Form, required, useNotify, useTranslate } from 'ra-core';
5 | import { Link, SaveButton, TextInput } from 'ra-ui-materialui';
6 |
7 | /**
8 | * A component that renders a form for resetting the user password.
9 | */
10 | export const ForgotPasswordForm = () => {
11 | const notify = useNotify();
12 | const translate = useTranslate();
13 | const [, { mutateAsync: resetPassword }] = useResetPassword();
14 |
15 | const submit = async (values: FormData) => {
16 | try {
17 | await resetPassword({
18 | email: values.email,
19 | });
20 | } catch (error) {
21 | notify(
22 | typeof error === 'string'
23 | ? error
24 | : typeof error === 'undefined' || !error.message
25 | ? 'ra.auth.sign_in_error'
26 | : error.message,
27 | {
28 | type: 'warning',
29 | messageArgs: {
30 | _:
31 | typeof error === 'string'
32 | ? error
33 | : error && error.message
34 | ? error.message
35 | : undefined,
36 | },
37 | }
38 | );
39 | }
40 | };
41 |
42 | return (
43 |
44 |
45 |
46 |
52 | {translate(
53 | 'ra-supabase.reset_password.forgot_password',
54 | { _: 'Forgot password?' }
55 | )}
56 |
57 |
58 |
65 | {translate(
66 | 'ra-supabase.reset_password.forgot_password_details',
67 | {
68 | _: 'Enter your email to receive a reset password link.',
69 | }
70 | )}
71 |
72 |
73 |
74 |
75 |
84 |
85 |
86 |
87 | >}
95 | />
96 |
97 | {translate('ra-supabase.auth.back_to_login', {
98 | _: 'Back to login page',
99 | })}
100 |
101 |
102 |
103 | );
104 | };
105 |
106 | interface FormData {
107 | email: string;
108 | }
109 |
110 | const PREFIX = 'RaSupabaseForgotPasswordForm';
111 |
112 | const SupabaseLoginFormClasses = {
113 | container: `${PREFIX}-container`,
114 | input: `${PREFIX}-input`,
115 | button: `${PREFIX}-button`,
116 | };
117 |
118 | const Root = styled(Form, {
119 | name: PREFIX,
120 | overridesResolver: (props, styles) => styles.root,
121 | })(({ theme }) => ({
122 | [`& .${SupabaseLoginFormClasses.container}`]: {
123 | padding: '0 1em 0 1em',
124 | },
125 | [`& .${SupabaseLoginFormClasses.input}`]: {
126 | marginTop: '1em',
127 | },
128 | [`& .${SupabaseLoginFormClasses.button}`]: {
129 | width: '100%',
130 | },
131 | }));
132 |
--------------------------------------------------------------------------------
/packages/ra-supabase-ui-materialui/src/ForgotPasswordPage.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import type { ReactNode } from 'react';
3 |
4 | import { AuthLayout } from './AuthLayout';
5 | import { ForgotPasswordForm } from './ForgotPasswordForm';
6 |
7 | /**
8 | * A component that renders a page for resetting the current user password through Supabase.
9 | * @param props
10 | * @param props.children The content of the page. If not set, it will render a ForgotPasswordForm.
11 | *
12 | * @example
13 | * import { ForgotPasswordPage } from 'ra-supabase-ui-materialui';
14 | * import { Admin, CustomRoutes } from 'react-admin';
15 | *
16 | * const App = () => (
17 | *
18 | *
19 | * } />
20 | *
21 | * ...
22 | *
23 | * );
24 | */
25 | export const ForgotPasswordPage = (props: ForgotPasswordPageProps) => {
26 | const { children = } = props;
27 |
28 | return {children};
29 | };
30 |
31 | ForgotPasswordPage.path = '/forgot-password';
32 |
33 | export type ForgotPasswordPageProps = {
34 | children?: ReactNode;
35 | };
36 |
--------------------------------------------------------------------------------
/packages/ra-supabase-ui-materialui/src/LoginForm.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import type { ComponentProps } from 'react';
3 | import { Box, styled } from '@mui/material';
4 | import { required, useTranslate } from 'ra-core';
5 | import {
6 | Link,
7 | LoginForm as RaLoginForm,
8 | PasswordInput,
9 | TextInput,
10 | } from 'ra-ui-materialui';
11 |
12 | import { ForgotPasswordPage } from './ForgotPasswordPage';
13 |
14 | /**
15 | * A component that renders a form to login to the application with an email and password.
16 | */
17 | export const LoginForm = ({
18 | disableForgotPassword,
19 | ...props
20 | }: LoginFormProps) => {
21 | const translate = useTranslate();
22 |
23 | return (
24 |
25 |
26 |
36 |
44 |
45 | {!disableForgotPassword ? (
46 |
52 |
53 | {translate('ra-supabase.auth.forgot_password', {
54 | _: 'Forgot password?',
55 | })}
56 |
57 |
58 | ) : null}
59 |
60 | );
61 | };
62 |
63 | export interface LoginFormProps
64 | extends Omit, 'onSubmit' | 'children'> {
65 | disableForgotPassword?: boolean;
66 | }
67 |
68 | const PREFIX = 'RaSupabaseLoginForm';
69 |
70 | const Root = styled('div', {
71 | name: PREFIX,
72 | overridesResolver: (props, styles) => styles.root,
73 | })(() => ({}));
74 |
--------------------------------------------------------------------------------
/packages/ra-supabase-ui-materialui/src/LoginPage.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import type { ReactNode } from 'react';
3 | import { Login } from 'ra-ui-materialui';
4 | import { Provider } from '@supabase/supabase-js';
5 | import { Divider, Stack } from '@mui/material';
6 |
7 | import {
8 | AppleButton,
9 | AzureButton,
10 | BitbucketButton,
11 | DiscordButton,
12 | FacebookButton,
13 | GithubButton,
14 | GitlabButton,
15 | GoogleButton,
16 | KeycloakButton,
17 | LinkedInButton,
18 | NotionButton,
19 | SlackButton,
20 | SpotifyButton,
21 | TwitchButton,
22 | TwitterButton,
23 | WorkosButton,
24 | } from './SocialAuthButton';
25 | import { LoginForm } from './LoginForm';
26 |
27 | /**
28 | * A component that renders a login page to login to the application through Supabase. It renders a LoginForm by default. It support social login providers.
29 | * @param props
30 | * @param props.children The content of the login page. If not set, it will render a LoginForm.
31 | * @param props.disableEmailPassword If true, the email/password login form will not be rendered.
32 | * @param props.providers The list of social login providers to render. Defaults to no providers.
33 | * @example
34 | * import { LoginPage } from 'ra-supabase-ui-materialui';
35 | * import { Admin } from 'react-admin';
36 | *
37 | * const App = () => (
38 | *
39 | * ...
40 | *
41 | * );
42 | *
43 | * @example With social login providers
44 | * import { LoginPage } from 'ra-supabase-ui-materialui';
45 | * import { Admin } from 'react-admin';
46 | *
47 | * const App = () => (
48 | * }>
49 | * ...
50 | *
51 | * );
52 | *
53 | * @example With social login providers
54 | * import { LoginPage } from 'ra-supabase-ui-materialui';
55 | * import { Admin } from 'react-admin';
56 | *
57 | * const App = () => (
58 | * }>
59 | * ...
60 | *
61 | * );
62 | *
63 | * @example With social login providers and no email/password login form
64 | * import { LoginPage } from 'ra-supabase-ui-materialui';
65 | * import { Admin } from 'react-admin';
66 | *
67 | * const App = () => (
68 | * }>
69 | * ...
70 | *
71 | * );
72 | */
73 | export const LoginPage = (props: LoginPageProps) => {
74 | const {
75 | children,
76 | disableEmailPassword = false,
77 | disableForgotPassword = false,
78 | providers = [],
79 | } = props;
80 |
81 | return (
82 |
83 | {children ?? (
84 | <>
85 | {disableEmailPassword ? null : (
86 |
89 | )}
90 | {disableEmailPassword || providers.length === 0 ? null : (
91 |
92 | )}
93 | {providers && providers.length > 0 ? (
94 | <>
95 |
101 | {providers.includes('apple') ? (
102 |
103 | ) : null}
104 | {providers.includes('azure') ? (
105 |
106 | ) : null}
107 | {providers.includes('bitbucket') ? (
108 |
109 | ) : null}
110 | {providers.includes('discord') ? (
111 |
112 | ) : null}
113 | {providers.includes('facebook') ? (
114 |
115 | ) : null}
116 | {providers.includes('gitlab') ? (
117 |
118 | ) : null}
119 | {providers.includes('github') ? (
120 |
121 | ) : null}
122 | {providers.includes('google') ? (
123 |
124 | ) : null}
125 | {providers.includes('keycloak') ? (
126 |
127 | ) : null}
128 | {providers.includes('linkedin') ? (
129 |
130 | ) : null}
131 | {providers.includes('notion') ? (
132 |
133 | ) : null}
134 | {providers.includes('slack') ? (
135 |
136 | ) : null}
137 | {providers.includes('spotify') ? (
138 |
139 | ) : null}
140 | {providers.includes('twitch') ? (
141 |
142 | ) : null}
143 | {providers.includes('twitter') ? (
144 |
145 | ) : null}
146 | {providers.includes('workos') ? (
147 |
148 | ) : null}
149 |
150 | >
151 | ) : null}
152 | >
153 | )}
154 |
155 | );
156 | };
157 |
158 | export type LoginPageProps = {
159 | children?: ReactNode;
160 | disableEmailPassword?: boolean;
161 | disableForgotPassword?: boolean;
162 | providers?: Provider[];
163 | };
164 |
--------------------------------------------------------------------------------
/packages/ra-supabase-ui-materialui/src/SetPasswordForm.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { CardActions, styled, Typography } from '@mui/material';
3 | import { Form, required, useNotify, useTranslate } from 'ra-core';
4 | import { PasswordInput, SaveButton } from 'ra-ui-materialui';
5 | import { useSetPassword, useSupabaseAccessToken } from 'ra-supabase-core';
6 |
7 | /**
8 | * A component that renders a form for setting the current user password through Supabase.
9 | * Can be used for the first login after a user has been invited or to reset the password.
10 | */
11 | export const SetPasswordForm = () => {
12 | const access_token = useSupabaseAccessToken();
13 | const refresh_token = useSupabaseAccessToken({
14 | parameterName: 'refresh_token',
15 | });
16 |
17 | const notify = useNotify();
18 | const translate = useTranslate();
19 | const [, { mutateAsync: setPassword }] = useSetPassword();
20 |
21 | const validate = (values: FormData) => {
22 | if (values.password !== values.confirmPassword) {
23 | return {
24 | password: 'ra-supabase.validation.password_mismatch',
25 | confirmPassword: 'ra-supabase.validation.password_mismatch',
26 | };
27 | }
28 | return {};
29 | };
30 |
31 | if (!access_token || !refresh_token) {
32 | if (process.env.NODE_ENV === 'development') {
33 | console.error(
34 | 'Missing access_token or refresh_token for set password'
35 | );
36 | }
37 | return (
38 |
39 |
{translate('ra-supabase.auth.missing_tokens')}
40 |
41 | );
42 | }
43 |
44 | const submit = async (values: FormData) => {
45 | try {
46 | await setPassword({
47 | access_token,
48 | refresh_token,
49 | password: values.password,
50 | });
51 | } catch (error) {
52 | notify(
53 | typeof error === 'string'
54 | ? error
55 | : typeof error === 'undefined' || !error.message
56 | ? 'ra.auth.sign_in_error'
57 | : error.message,
58 | {
59 | type: 'warning',
60 | messageArgs: {
61 | _:
62 | typeof error === 'string'
63 | ? error
64 | : error && error.message
65 | ? error.message
66 | : undefined,
67 | },
68 | }
69 | );
70 | }
71 | };
72 |
73 | return (
74 |
75 |
76 |
83 | {translate('ra-supabase.set_password.new_password', {
84 | _: 'Choose your password',
85 | })}
86 |
87 |
88 |
99 |
109 |
110 |
111 |
117 |
118 |
119 | );
120 | };
121 |
122 | interface FormData {
123 | password: string;
124 | confirmPassword: string;
125 | }
126 |
127 | const PREFIX = 'RaSupabaseSetPasswordForm';
128 |
129 | const SupabaseLoginFormClasses = {
130 | container: `${PREFIX}-container`,
131 | input: `${PREFIX}-input`,
132 | button: `${PREFIX}-button`,
133 | };
134 |
135 | const Root = styled(Form, {
136 | name: PREFIX,
137 | overridesResolver: (props, styles) => styles.root,
138 | })(({ theme }) => ({
139 | [`& .${SupabaseLoginFormClasses.container}`]: {
140 | padding: '0 1em 0 1em',
141 | },
142 | [`& .${SupabaseLoginFormClasses.input}`]: {
143 | marginTop: '1em',
144 | },
145 | [`& .${SupabaseLoginFormClasses.button}`]: {
146 | width: '100%',
147 | },
148 | }));
149 |
--------------------------------------------------------------------------------
/packages/ra-supabase-ui-materialui/src/SetPasswordPage.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import type { ReactNode } from 'react';
3 |
4 | import { AuthLayout } from './AuthLayout';
5 | import { SetPasswordForm } from './SetPasswordForm';
6 |
7 | /**
8 | * A component that renders a page for setting the current user password through Supabase.
9 | * Can be used for the first login after a user has been invited or to reset the password.
10 | * @param props
11 | * @param props.children The content of the page. If not set, it will render a SetPasswordForm.
12 | *
13 | * @example
14 | * import { SetPasswordPage } from 'ra-supabase-ui-materialui';
15 | * import { Admin, CustomRoutes } from 'react-admin';
16 | *
17 | * const App = () => (
18 | *
19 | *
20 | * } />
21 | *
22 | * ...
23 | *
24 | * );
25 | */
26 | export const SetPasswordPage = (props: SetPasswordPageProps) => {
27 | const { children = } = props;
28 |
29 | return {children};
30 | };
31 |
32 | SetPasswordPage.path = '/set-password';
33 |
34 | export type SetPasswordPageProps = {
35 | children?: ReactNode;
36 | };
37 |
--------------------------------------------------------------------------------
/packages/ra-supabase-ui-materialui/src/guessers/CreateGuesser.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import type { ReactNode } from 'react';
3 | import { useAPISchema } from 'ra-supabase-core';
4 | import { CreateBase, useResourceContext } from 'ra-core';
5 | import { CreateView, Loading } from 'ra-ui-materialui';
6 | import type { CreateProps, CreateViewProps } from 'ra-ui-materialui';
7 | import { capitalize, singularize } from 'inflection';
8 |
9 | import { inferElementFromType } from './inferElementFromType';
10 | import { InferredElement } from './InferredElement';
11 | import { editFieldTypes } from './editFieldTypes';
12 |
13 | export const CreateGuesser = (props: CreateProps & { enableLog?: boolean }) => {
14 | const {
15 | mutationOptions,
16 | resource,
17 | record,
18 | transform,
19 | redirect,
20 | disableAuthentication,
21 | ...rest
22 | } = props;
23 | return (
24 |
32 |
33 |
34 | );
35 | };
36 |
37 | export const CreateGuesserView = (
38 | props: CreateViewProps & {
39 | enableLog?: boolean;
40 | }
41 | ) => {
42 | const { data: schema, error, isPending } = useAPISchema();
43 | const resource = useResourceContext();
44 | const [child, setChild] = React.useState(null);
45 | if (!resource) {
46 | throw new Error('CreateGuesser must be used withing a ResourceContext');
47 | }
48 | const { enableLog = process.env.NODE_ENV === 'development', ...rest } =
49 | props;
50 |
51 | React.useEffect(() => {
52 | if (isPending || error) {
53 | return;
54 | }
55 | const resourceDefinition = schema.definitions?.[resource];
56 | const requiredFields = resourceDefinition?.required || [];
57 | if (!resourceDefinition || !resourceDefinition.properties) {
58 | throw new Error(
59 | `The resource ${resource} is not defined in the API schema`
60 | );
61 | }
62 | const inferredInputs = Object.keys(resourceDefinition.properties)
63 | .filter((source: string) => source !== 'id')
64 | .filter(
65 | source =>
66 | resourceDefinition.properties![source].format !== 'tsvector'
67 | )
68 | .map((source: string) =>
69 | inferElementFromType({
70 | name: source,
71 | types: editFieldTypes,
72 | description:
73 | resourceDefinition.properties![source].description,
74 | format: resourceDefinition.properties![source].format,
75 | type: (resourceDefinition.properties &&
76 | resourceDefinition.properties[source] &&
77 | typeof resourceDefinition.properties[source].type ===
78 | 'string'
79 | ? resourceDefinition.properties![source].type
80 | : 'string') as string,
81 | requiredFields,
82 | schema,
83 | })
84 | );
85 | const inferredForm = new InferredElement(
86 | editFieldTypes.form,
87 | null,
88 | inferredInputs
89 | );
90 | setChild(inferredForm.getElement());
91 | if (!enableLog) return;
92 |
93 | const representation = inferredForm.getRepresentation();
94 |
95 | const components = ['Create']
96 | .concat(
97 | Array.from(
98 | new Set(
99 | Array.from(representation.matchAll(/<([^/\s>]+)/g))
100 | .map(match => match[1])
101 | .filter(component => component !== 'span')
102 | )
103 | )
104 | )
105 | .sort();
106 |
107 | const warnings = inferredInputs
108 | .map(inferredInput => inferredInput.getWarning())
109 | .filter(warning => warning != null);
110 |
111 | // eslint-disable-next-line no-console
112 | console.log(
113 | `Guessed Create:
114 |
115 | import { ${components.join(', ')} } from 'react-admin';
116 |
117 | export const ${capitalize(singularize(resource))}Create = () => (
118 |
119 | ${representation}
120 |
121 | );${
122 | warnings.length > 0
123 | ? warnings.map(warning => `\n\n${warning}`).join('')
124 | : ''
125 | }`
126 | );
127 | }, [resource, isPending, error, schema, enableLog]);
128 |
129 | if (isPending) return ;
130 | if (error) return Error: {error.message}
;
131 |
132 | return {child};
133 | };
134 |
--------------------------------------------------------------------------------
/packages/ra-supabase-ui-materialui/src/guessers/EditGuesser.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import type { ReactNode } from 'react';
3 | import { useAPISchema } from 'ra-supabase-core';
4 | import { EditBase, InferredElement, useResourceContext } from 'ra-core';
5 | import {
6 | type EditProps,
7 | EditView,
8 | type EditViewProps,
9 | Loading,
10 | } from 'ra-ui-materialui';
11 | import { capitalize, singularize } from 'inflection';
12 |
13 | import { inferElementFromType } from './inferElementFromType';
14 | import { editFieldTypes } from './editFieldTypes';
15 |
16 | export const EditGuesser = (props: EditProps & { enableLogs?: boolean }) => {
17 | const {
18 | resource,
19 | id,
20 | mutationMode,
21 | mutationOptions,
22 | queryOptions,
23 | redirect,
24 | transform,
25 | disableAuthentication,
26 | ...rest
27 | } = props;
28 | return (
29 |
39 |
40 |
41 | );
42 | };
43 |
44 | export const EditGuesserView = (
45 | props: EditViewProps & {
46 | enableLog?: boolean;
47 | }
48 | ) => {
49 | const { data: schema, error, isPending } = useAPISchema();
50 | const resource = useResourceContext();
51 | const [child, setChild] = React.useState(null);
52 | if (!resource) {
53 | throw new Error('EditGuesser must be used withing a ResourceContext');
54 | }
55 | const { enableLog = process.env.NODE_ENV === 'development', ...rest } =
56 | props;
57 |
58 | React.useEffect(() => {
59 | if (isPending || error) {
60 | return;
61 | }
62 | const resourceDefinition = schema.definitions?.[resource];
63 | const requiredFields = resourceDefinition?.required || [];
64 | if (!resourceDefinition || !resourceDefinition.properties) {
65 | throw new Error(
66 | `The resource ${resource} is not defined in the API schema`
67 | );
68 | }
69 | const inferredInputs = Object.keys(resourceDefinition.properties)
70 | .filter((source: string) => source !== 'id')
71 | .filter(
72 | source =>
73 | resourceDefinition.properties![source].format !== 'tsvector'
74 | )
75 | .map((source: string) =>
76 | inferElementFromType({
77 | name: source,
78 | types: editFieldTypes,
79 | description:
80 | resourceDefinition.properties![source].description,
81 | format: resourceDefinition.properties![source].format,
82 | type: (resourceDefinition.properties &&
83 | resourceDefinition.properties[source] &&
84 | typeof resourceDefinition.properties[source].type ===
85 | 'string'
86 | ? resourceDefinition.properties![source].type
87 | : 'string') as string,
88 | requiredFields,
89 | schema,
90 | })
91 | );
92 | const inferredForm = new InferredElement(
93 | editFieldTypes.form,
94 | null,
95 | inferredInputs
96 | );
97 | setChild(inferredForm.getElement());
98 | if (!enableLog) return;
99 |
100 | const representation = inferredForm.getRepresentation();
101 |
102 | const components = ['Edit']
103 | .concat(
104 | Array.from(
105 | new Set(
106 | Array.from(representation.matchAll(/<([^/\s>]+)/g))
107 | .map(match => match[1])
108 | .filter(component => component !== 'span')
109 | )
110 | )
111 | )
112 | .sort();
113 |
114 | const warnings = inferredInputs
115 | .map(inferredInput => inferredInput.getWarning())
116 | .filter(warning => warning != null);
117 |
118 | // eslint-disable-next-line no-console
119 | console.log(
120 | `Guessed Edit:
121 |
122 | import { ${components.join(', ')} } from 'react-admin';
123 |
124 | export const ${capitalize(singularize(resource))}Edit = () => (
125 |
126 | ${representation}
127 |
128 | );${
129 | warnings.length > 0
130 | ? warnings.map(warning => `\n\n${warning}`).join('')
131 | : ''
132 | }`
133 | );
134 | }, [resource, isPending, error, schema, enableLog]);
135 |
136 | if (isPending) return ;
137 | if (error) return Error: {error.message}
;
138 |
139 | return {child};
140 | };
141 |
--------------------------------------------------------------------------------
/packages/ra-supabase-ui-materialui/src/guessers/InferredElement.ts:
--------------------------------------------------------------------------------
1 | import { InferredElement as CoreInferredElement, InferredType } from 'ra-core';
2 |
3 | export class InferredElement extends CoreInferredElement {
4 | constructor(
5 | type?: InferredType,
6 | props?: any,
7 | children?: any,
8 | private warning?: string
9 | ) {
10 | super(type, props, children);
11 | }
12 |
13 | getWarning() {
14 | return this.warning;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/ra-supabase-ui-materialui/src/guessers/ShowGuesser.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import type { ReactNode } from 'react';
3 | import { useAPISchema } from 'ra-supabase-core';
4 | import { ShowBase, useResourceContext } from 'ra-core';
5 | import {
6 | Loading,
7 | showFieldTypes,
8 | type ShowProps,
9 | ShowView,
10 | type ShowViewProps,
11 | } from 'ra-ui-materialui';
12 | import { capitalize, singularize } from 'inflection';
13 |
14 | import { inferElementFromType } from './inferElementFromType';
15 | import { InferredElement } from './InferredElement';
16 |
17 | export const ShowGuesser = (props: ShowProps & { enableLog?: boolean }) => {
18 | const { id, disableAuthentication, queryOptions, resource, ...rest } =
19 | props;
20 | return (
21 |
27 |
28 |
29 | );
30 | };
31 |
32 | export const ShowGuesserView = (
33 | props: ShowViewProps & {
34 | enableLog?: boolean;
35 | }
36 | ) => {
37 | const { data: schema, error, isPending } = useAPISchema();
38 | const resource = useResourceContext();
39 | const [child, setChild] = React.useState(null);
40 | if (!resource) {
41 | throw new Error('ShowGuesser must be used withing a ResourceContext');
42 | }
43 | const { enableLog = process.env.NODE_ENV === 'development', ...rest } =
44 | props;
45 |
46 | React.useEffect(() => {
47 | if (isPending || error) {
48 | return;
49 | }
50 | const resourceDefinition = schema.definitions?.[resource];
51 | if (!resourceDefinition || !resourceDefinition.properties) {
52 | throw new Error(
53 | `The resource ${resource} is not defined in the API schema`
54 | );
55 | }
56 | const inferredFields = Object.keys(resourceDefinition.properties)
57 | .filter(
58 | source =>
59 | resourceDefinition.properties![source].format !== 'tsvector'
60 | )
61 | .map((source: string) =>
62 | inferElementFromType({
63 | name: source,
64 | types: showFieldTypes,
65 | description:
66 | resourceDefinition.properties![source].description,
67 | format: resourceDefinition.properties![source].format,
68 | type: (resourceDefinition.properties &&
69 | resourceDefinition.properties[source] &&
70 | typeof resourceDefinition.properties[source].type ===
71 | 'string'
72 | ? resourceDefinition.properties![source].type
73 | : 'string') as string,
74 | schema,
75 | })
76 | );
77 | const inferredLayout = new InferredElement(
78 | showFieldTypes.show,
79 | null,
80 | inferredFields
81 | );
82 | setChild(inferredLayout.getElement());
83 | if (!enableLog) return;
84 |
85 | const representation = inferredLayout.getRepresentation();
86 |
87 | const components = ['Show']
88 | .concat(
89 | Array.from(
90 | new Set(
91 | Array.from(representation.matchAll(/<([^/\s>]+)/g))
92 | .map(match => match[1])
93 | .filter(component => component !== 'span')
94 | )
95 | )
96 | )
97 | .sort();
98 |
99 | const warnings = inferredFields
100 | .map(inferredInput => inferredInput.getWarning())
101 | .filter(warning => warning != null);
102 |
103 | // eslint-disable-next-line no-console
104 | console.log(
105 | `Guessed Show:
106 |
107 | import { ${components.join(', ')} } from 'react-admin';
108 |
109 | export const ${capitalize(singularize(resource))}Show = () => (
110 |
111 | ${representation}
112 |
113 | );${
114 | warnings.length > 0
115 | ? warnings.map(warning => `\n\n${warning}`).join('')
116 | : ''
117 | }`
118 | );
119 | }, [resource, isPending, error, schema, enableLog]);
120 |
121 | if (isPending) return ;
122 | if (error) return Error: {error.message}
;
123 |
124 | return {child};
125 | };
126 |
--------------------------------------------------------------------------------
/packages/ra-supabase-ui-materialui/src/guessers/editFieldTypes.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { InferredElement, InferredTypeMap } from 'ra-core';
3 | import {
4 | AutocompleteArrayInput,
5 | AutocompleteArrayInputProps,
6 | AutocompleteInput,
7 | AutocompleteInputProps,
8 | editFieldTypes as defaultEditFieldTypes,
9 | ReferenceArrayInput,
10 | ReferenceArrayInputProps,
11 | ReferenceInput,
12 | ReferenceInputProps,
13 | } from 'ra-ui-materialui';
14 |
15 | export const editFieldTypes: InferredTypeMap = {
16 | ...defaultEditFieldTypes,
17 | reference: {
18 | component: ReferenceInput,
19 | representation: (
20 | props: ReferenceInputProps,
21 | children?: InferredElement[]
22 | ) =>
23 | children
24 | ? `\n${children
27 | .map(
28 | child =>
29 | ` ${child.getRepresentation()}`
30 | )
31 | .join('\n')}\n `
32 | : ``,
33 | },
34 | autocompleteInput: {
35 | component: (props: AutocompleteInputProps) =>
36 | props.optionText ? (
37 | ({
39 | [`${props.optionText}@ilike`]: `%${searchText}%`,
40 | })}
41 | {...props}
42 | />
43 | ) : (
44 |
45 | ),
46 | representation: (props: AutocompleteInputProps) =>
47 | ` ({ '${props.optionText}@ilike': \`%\${searchText}%\` })}`
52 | : ''
53 | } />`,
54 | },
55 | referenceArray: {
56 | component: ReferenceArrayInput,
57 | representation: (
58 | props: ReferenceArrayInputProps,
59 | children?: InferredElement[]
60 | ) =>
61 | children
62 | ? `\n${children
65 | .map(child => `\t\t${child.getRepresentation()}`)
66 | .join('\n')}\n\t`
67 | : ``,
68 | },
69 | autocompleteArrayInput: {
70 | component: (props: AutocompleteArrayInputProps) =>
71 | props.optionText ? (
72 | ({
74 | [`${props.optionText}@ilike`]: `%${searchText}%`,
75 | })}
76 | {...props}
77 | />
78 | ) : (
79 |
80 | ),
81 | representation: (props: AutocompleteInputProps) =>
82 | ` ({ '${props.optionText}@ilike': \`%\${searchText}%\` })}`
87 | : ''
88 | } />`,
89 | },
90 | };
91 |
--------------------------------------------------------------------------------
/packages/ra-supabase-ui-materialui/src/guessers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './CreateGuesser';
2 | export * from './EditGuesser';
3 | export * from './ListGuesser';
4 | export * from './useCrudGuesser';
5 | export * from './ShowGuesser';
6 |
--------------------------------------------------------------------------------
/packages/ra-supabase-ui-materialui/src/guessers/inferElementFromType.ts:
--------------------------------------------------------------------------------
1 | import { required, type InferredTypeMap } from 'ra-core';
2 | import { pluralize } from 'inflection';
3 | import type { OpenAPIV2 } from 'openapi-types';
4 | import { InferredElement } from './InferredElement';
5 |
6 | const hasType = (type, types) => typeof types[type] !== 'undefined';
7 |
8 | export const inferElementFromType = ({
9 | name,
10 | description,
11 | format,
12 | type,
13 | requiredFields,
14 | types,
15 | props,
16 | schema,
17 | }: {
18 | name: string;
19 | types: InferredTypeMap;
20 | description?: string;
21 | format?: string;
22 | type?: string;
23 | requiredFields?: string[];
24 | props?: any;
25 | schema: OpenAPIV2.Document;
26 | }) => {
27 | if (name === 'id' && hasType('id', types)) {
28 | return new InferredElement(types.id, { source: 'id' });
29 | }
30 | const validate = requiredFields?.includes(name) ? [required()] : undefined;
31 | if (
32 | description?.startsWith('Note:\nThis is a Foreign Key to') &&
33 | hasType('reference', types)
34 | ) {
35 | const reference = description.split('`')[1].split('.')[0];
36 | const referenceResourceDefinition = schema.definitions?.[reference];
37 | if (!referenceResourceDefinition) {
38 | throw new Error(
39 | `The referenced resource ${reference} is not defined in the API schema`
40 | );
41 | }
42 | const referenceRecordRepresentationField =
43 | inferRecordRepresentationField(referenceResourceDefinition);
44 |
45 | return new InferredElement(
46 | types.reference,
47 | {
48 | source: name,
49 | reference,
50 | ...props,
51 | },
52 | hasType('autocompleteInput', types) &&
53 | referenceRecordRepresentationField
54 | ? [
55 | new InferredElement(types.autocompleteInput, {
56 | optionText: referenceRecordRepresentationField,
57 | }),
58 | ]
59 | : undefined,
60 | hasType('autocompleteInput', types) &&
61 | !referenceRecordRepresentationField
62 | ? `Could not infer the field to use to filter referenced ${reference} records. Please provide the \`filterToQuery\` prop to the component. See https://github.com/marmelab/ra-supabase/blob/main/packages/ra-supabase/README.md#autocompleteinput-with-references`
63 | : undefined
64 | );
65 | }
66 | if (
67 | name.substring(name.length - 4) === '_ids' &&
68 | hasType('referenceArray', types)
69 | ) {
70 | const reference = pluralize(name.substr(0, name.length - 4));
71 | const referenceResourceDefinition = schema.definitions?.[reference];
72 | if (!referenceResourceDefinition) {
73 | throw new Error(
74 | `The referenced resource ${reference} is not defined in the API schema`
75 | );
76 | }
77 | const referenceRecordRepresentationField =
78 | inferRecordRepresentationField(referenceResourceDefinition);
79 |
80 | return new InferredElement(
81 | types.referenceArray,
82 | {
83 | source: name,
84 | reference,
85 | ...props,
86 | },
87 | hasType('autocompleteArrayInput', types) &&
88 | referenceRecordRepresentationField
89 | ? [
90 | new InferredElement(types.autocompleteArrayInput, {
91 | optionText: referenceRecordRepresentationField,
92 | }),
93 | ]
94 | : undefined,
95 | hasType('autocompleteArrayInput', types) &&
96 | !referenceRecordRepresentationField
97 | ? `Could not infer the field to use to filter referenced ${reference} records. Please provide the \`filterToQuery\` prop to the component. See https://github.com/marmelab/ra-supabase/blob/main/packages/ra-supabase/README.md#autocompleteinput-with-references`
98 | : undefined
99 | );
100 | }
101 | if (type === 'array') {
102 | // FIXME instrospect further
103 | return new InferredElement(types.string, {
104 | source: name,
105 | validate,
106 | });
107 | }
108 | if (type === 'string') {
109 | if (name === 'email' && hasType('email', types)) {
110 | return new InferredElement(types.email, {
111 | source: name,
112 | validate,
113 | ...props,
114 | });
115 | }
116 | if (['url', 'website'].includes(name) && hasType('url', types)) {
117 | return new InferredElement(types.url, {
118 | source: name,
119 | validate,
120 | ...props,
121 | });
122 | }
123 | if (
124 | format &&
125 | [
126 | 'timestamp with time zone',
127 | 'timestamp without time zone',
128 | ].includes(format) &&
129 | hasType('date', types)
130 | ) {
131 | return new InferredElement(types.date, {
132 | source: name,
133 | validate,
134 | ...props,
135 | });
136 | }
137 | }
138 | if (type === 'integer' && hasType('number', types)) {
139 | return new InferredElement(types.number, {
140 | source: name,
141 | validate,
142 | ...props,
143 | });
144 | }
145 | if (type && hasType(type, types)) {
146 | return new InferredElement(types[type], {
147 | source: name,
148 | validate,
149 | ...props,
150 | });
151 | }
152 | return new InferredElement(types.string, {
153 | source: name,
154 | validate,
155 | ...props,
156 | });
157 | };
158 |
159 | const inferRecordRepresentationField = (
160 | referenceResourceDefinition: OpenAPIV2.SchemaObject
161 | ) => {
162 | if (referenceResourceDefinition.properties?.name != null) {
163 | return 'name';
164 | }
165 | if (referenceResourceDefinition.properties?.title != null) {
166 | return 'title';
167 | }
168 | if (referenceResourceDefinition.properties?.label != null) {
169 | return 'label';
170 | }
171 | if (referenceResourceDefinition.properties?.reference != null) {
172 | return 'reference';
173 | }
174 | };
175 |
--------------------------------------------------------------------------------
/packages/ra-supabase-ui-materialui/src/guessers/useCrudGuesser.spec.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { DataProviderContext, TestMemoryRouter } from 'ra-core';
3 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4 | import { render, screen, within } from '@testing-library/react';
5 |
6 | import { useCrudGuesser } from './useCrudGuesser';
7 | import exampleSchema from './exampleSchema.json';
8 |
9 | const dataProvider = {
10 | getSchema: async () => exampleSchema,
11 | } as any;
12 |
13 | const wrapper = ({ children }) => (
14 |
15 |
16 |
17 | {children}
18 |
19 |
20 |
21 | );
22 |
23 | describe('useAPISchema', () => {
24 | it('should create one resource per path in the schema', async () => {
25 | const Component = () => {
26 | const resources = useCrudGuesser();
27 | return (
28 |
29 | {resources.map(resource => (
30 | - {resource.name}
31 | ))}
32 |
33 | );
34 | };
35 | render(, { wrapper });
36 | const litItems = await screen.findAllByRole('listitem');
37 | expect(litItems).toHaveLength(8);
38 | });
39 |
40 | it('should set the CRUD pages according to the available paths', async () => {
41 | const Component = () => {
42 | const resources = useCrudGuesser();
43 | return (
44 |
45 | {resources.map(resource => (
46 | -
47 | {resource.name}
48 |
49 | {resource.list && - list
}
50 | {resource.show && - show
}
51 | {resource.edit && - edit
}
52 | {resource.create && - create
}
53 |
54 |
55 | ))}
56 |
57 | );
58 | };
59 | render(, { wrapper });
60 | const companies = await screen.findByText('companies');
61 | expect(within(companies).queryByText('list')).not.toBeNull();
62 | expect(within(companies).queryByText('show')).not.toBeNull();
63 | expect(within(companies).queryByText('edit')).not.toBeNull();
64 | expect(within(companies).queryByText('create')).not.toBeNull();
65 |
66 | const sales = await screen.findByText('sales');
67 | expect(within(sales).queryByText('list')).not.toBeNull();
68 | expect(within(sales).queryByText('show')).not.toBeNull();
69 | expect(within(sales).queryByText('edit')).toBeNull();
70 | expect(within(sales).queryByText('create')).toBeNull();
71 | });
72 | });
73 |
--------------------------------------------------------------------------------
/packages/ra-supabase-ui-materialui/src/guessers/useCrudGuesser.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import { useAPISchema } from 'ra-supabase-core';
3 | import type { ResourceProps } from 'ra-core';
4 |
5 | import { ListGuesser } from './ListGuesser';
6 | import { CreateGuesser } from './CreateGuesser';
7 | import { EditGuesser } from './EditGuesser';
8 | import { ShowGuesser } from './ShowGuesser';
9 |
10 | export const useCrudGuesser = () => {
11 | const { data: schema, error, isPending } = useAPISchema();
12 | return useMemo(() => {
13 | if (isPending || error) {
14 | return [];
15 | }
16 | const resourceNames = Object.keys(schema.definitions!);
17 | return resourceNames.map(name => {
18 | const resourcePaths = schema.paths[`/${name}`] ?? {};
19 | return {
20 | name,
21 | list: resourcePaths.get ? ListGuesser : undefined,
22 | show: resourcePaths.get ? ShowGuesser : undefined,
23 | edit: resourcePaths.patch ? EditGuesser : undefined,
24 | create: resourcePaths.post ? CreateGuesser : undefined,
25 | };
26 | });
27 | }, [schema, isPending, error]);
28 | };
29 |
--------------------------------------------------------------------------------
/packages/ra-supabase-ui-materialui/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './AuthLayout';
2 | export * from './guessers';
3 | export * from './LoginForm';
4 | export * from './LoginPage';
5 | export * from './ForgotPasswordForm';
6 | export * from './ForgotPasswordPage';
7 | export * from './SetPasswordForm';
8 | export * from './SetPasswordPage';
9 | export * from './SocialAuthButton';
10 | export * from './icons';
11 |
--------------------------------------------------------------------------------
/packages/ra-supabase-ui-materialui/src/mui.d.ts:
--------------------------------------------------------------------------------
1 | import type {} from '@mui/material/themeCssVarsAugmentation';
2 |
--------------------------------------------------------------------------------
/packages/ra-supabase-ui-materialui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "lib",
5 | "rootDir": "src",
6 | "declaration": true,
7 | "declarationMap": true,
8 | "allowJs": false,
9 | "strictNullChecks": true
10 | },
11 | "exclude": ["**/*.spec.ts", "**/*.spec.tsx", "**/*.spec.js"],
12 | "include": ["src"]
13 | }
14 |
--------------------------------------------------------------------------------
/packages/ra-supabase/assets/demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/packages/ra-supabase/assets/demo.png
--------------------------------------------------------------------------------
/packages/ra-supabase/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ra-supabase",
3 | "version": "3.5.0",
4 | "repository": "git@github.com:marmelab/ra-supabase.git",
5 | "author": "Gildas Garcia <1122076+djhi@users.noreply.github.com>",
6 | "license": "MIT",
7 | "files": [
8 | "*.md",
9 | "lib",
10 | "esm",
11 | "src"
12 | ],
13 | "main": "lib/index",
14 | "module": "esm/index.js",
15 | "types": "esm/index.d.ts",
16 | "sideEffects": false,
17 | "dependencies": {
18 | "ra-supabase-core": "^3.5.0",
19 | "ra-supabase-language-english": "^3.5.0",
20 | "ra-supabase-ui-materialui": "^3.5.0"
21 | },
22 | "devDependencies": {
23 | "rimraf": "^6.0.1",
24 | "typescript": "^5.7.3"
25 | },
26 | "scripts": {
27 | "build": "yarn run build-cjs && yarn run build-esm",
28 | "build-cjs": "rimraf ./lib && tsc",
29 | "build-esm": "rimraf ./esm && tsc --outDir esm --module es2015",
30 | "watch": "tsc --outDir esm --module es2015 --watch",
31 | "lint": "eslint --fix ./src"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/ra-supabase/src/AdminGuesser.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {
3 | AdminContext,
4 | AdminUI,
5 | Resource,
6 | Loading,
7 | CustomRoutes,
8 | } from 'react-admin';
9 | import type { AdminProps, AdminUIProps } from 'react-admin';
10 | import { Route, BrowserRouter } from 'react-router-dom';
11 |
12 | import { supabaseDataProvider, supabaseAuthProvider } from 'ra-supabase-core';
13 | import {
14 | useCrudGuesser,
15 | LoginPage,
16 | SetPasswordPage,
17 | ForgotPasswordPage,
18 | } from 'ra-supabase-ui-materialui';
19 | import { createClient } from '@supabase/supabase-js';
20 | import { defaultI18nProvider } from './defaultI18nProvider';
21 |
22 | export const AdminGuesser = (
23 | props: AdminProps & { instanceUrl: string; apiKey: string }
24 | ) => {
25 | const {
26 | instanceUrl,
27 | apiKey,
28 | dataProvider,
29 | authProvider,
30 | basename,
31 | darkTheme,
32 | defaultTheme,
33 | i18nProvider = defaultI18nProvider,
34 | lightTheme,
35 | queryClient,
36 | store,
37 | theme,
38 | ...rest
39 | } = props;
40 |
41 | const defaultSupabaseClient =
42 | instanceUrl && apiKey ? createClient(instanceUrl, apiKey) : null;
43 | const defaultDataProvider =
44 | instanceUrl && apiKey && defaultSupabaseClient
45 | ? supabaseDataProvider({
46 | instanceUrl,
47 | apiKey,
48 | supabaseClient: defaultSupabaseClient,
49 | })
50 | : undefined;
51 | const defaultAuthProvider =
52 | instanceUrl && apiKey && defaultSupabaseClient
53 | ? supabaseAuthProvider(defaultSupabaseClient, {})
54 | : undefined;
55 |
56 | return (
57 |
58 |
70 |
71 |
72 |
73 | );
74 | };
75 |
76 | const AdminUIGuesser = (props: AdminUIProps) => {
77 | const resourceDefinitions = useCrudGuesser();
78 | const { children, ...rest } = props;
79 | // while we're guessing, we don't want to show the not found page
80 | const [CatchAll, setCatchAll] = React.useState<
81 | React.ComponentType | undefined
82 | >(() => Loading);
83 | React.useEffect(() => {
84 | if (!children && resourceDefinitions.length > 0) {
85 | console.log(
86 | `Guessed Admin:
87 |
88 | import { Admin, Resource, CustomRoutes } from 'react-admin';
89 | import { BrowserRouter, Route } from 'react-router-dom';
90 | import { createClient } from '@supabase/supabase-js';
91 | import {
92 | CreateGuesser,
93 | EditGuesser,
94 | ForgotPasswordPage,
95 | ListGuesser,
96 | LoginPage,
97 | SetPasswordPage,
98 | ShowGuesser,
99 | defaultI18nProvider,
100 | supabaseDataProvider,
101 | supabaseAuthProvider
102 | } from 'ra-supabase';
103 |
104 | const instanceUrl = YOUR_SUPABASE_URL;
105 | const apiKey = YOUR_SUPABASE_API_KEY;
106 | const supabaseClient = createClient(instanceUrl, apiKey);
107 | const dataProvider = supabaseDataProvider({ instanceUrl, apiKey, supabaseClient });
108 | const authProvider = supabaseAuthProvider(supabaseClient, {});
109 |
110 | export const App = () => (
111 |
112 | ${resourceDefinitions
118 | .map(
119 | def => `
120 | `
125 | )
126 | .join('')}
127 |
128 | } />
129 | } />
130 |
131 |
132 |
133 | );`
134 | );
135 | }
136 | }, [resourceDefinitions, children]);
137 |
138 | React.useEffect(() => {
139 | // once we have guessed all the resources, we can show the not found page for unknown paths
140 | if (!children && resourceDefinitions.length > 0) {
141 | setCatchAll(undefined);
142 | }
143 | }, [resourceDefinitions, children]);
144 |
145 | const resourceElements = resourceDefinitions.map(resourceDefinition => (
146 |
147 | )) as any;
148 |
149 | return (
150 |
156 | {children ?? resourceElements}
157 |
158 | }
161 | />
162 | }
165 | />
166 |
167 |
168 | );
169 | };
170 |
--------------------------------------------------------------------------------
/packages/ra-supabase/src/defaultI18nProvider.ts:
--------------------------------------------------------------------------------
1 | import { mergeTranslations } from 'react-admin';
2 | import polyglotI18nProvider from 'ra-i18n-polyglot';
3 | import englishMessages from 'ra-language-english';
4 | import { raSupabaseEnglishMessages } from 'ra-supabase-language-english';
5 |
6 | export const defaultI18nProvider = polyglotI18nProvider(() => {
7 | return mergeTranslations(englishMessages, raSupabaseEnglishMessages);
8 | }, 'en');
9 |
--------------------------------------------------------------------------------
/packages/ra-supabase/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from 'ra-supabase-core';
2 | export * from 'ra-supabase-ui-materialui';
3 | export * from 'ra-supabase-language-english';
4 |
5 | export * from './defaultI18nProvider';
6 | export * from './AdminGuesser';
7 |
--------------------------------------------------------------------------------
/packages/ra-supabase/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "lib",
5 | "rootDir": "src",
6 | "declaration": true,
7 | "declarationMap": true,
8 | "allowJs": false,
9 | "strictNullChecks": true
10 | },
11 | "exclude": ["**/*.spec.ts", "**/*.spec.tsx", "**/*.spec.js"],
12 | "include": ["src"]
13 | }
14 |
--------------------------------------------------------------------------------
/supabase/.gitignore:
--------------------------------------------------------------------------------
1 | # Supabase
2 | .branches
3 | .temp
4 |
--------------------------------------------------------------------------------
/supabase/config.toml:
--------------------------------------------------------------------------------
1 | # A string used to distinguish different Supabase projects on the same host. Defaults to the working
2 | # directory name when running `supabase init`.
3 | project_id = "ra-supabase"
4 |
5 | [api]
6 | # Port to use for the API URL.
7 | port = 54321
8 | # Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
9 | # endpoints. public and storage are always included.
10 | schemas = ["public", "storage", "graphql_public"]
11 | # Extra schemas to add to the search_path of every request. public is always included.
12 | extra_search_path = ["public", "extensions"]
13 | # The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
14 | # for accidental or malicious requests.
15 | max_rows = 1000
16 |
17 | [db]
18 | # Port to use for the local database URL.
19 | port = 54322
20 | # The database major version to use. This has to be the same as your remote database's. Run `SHOW
21 | # server_version;` on the remote database to check.
22 | major_version = 15
23 |
24 | [studio]
25 | # Port to use for Supabase Studio.
26 | port = 54323
27 |
28 | # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
29 | # are monitored, and you can view the emails that would have been sent from the web interface.
30 | [inbucket]
31 | # Port to use for the email testing server web interface.
32 | port = 54324
33 | smtp_port = 54325
34 | pop3_port = 54326
35 |
36 | [storage]
37 | # The maximum file size allowed (e.g. "5MB", "500KB").
38 | file_size_limit = "50MiB"
39 |
40 | [auth]
41 | # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
42 | # in emails.
43 | site_url = "http://localhost:8000"
44 | # A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
45 | additional_redirect_urls = ["http://localhost:8000/auth-callback"]
46 | # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one
47 | # week).
48 | jwt_expiry = 3600
49 | # Allow/disallow new user signups to your project.
50 | enable_signup = true
51 |
52 | [auth.email]
53 | # Allow/disallow new user signups via email to your project.
54 | enable_signup = true
55 | # If enabled, a user will be required to confirm any email change on both the old, and new email
56 | # addresses. If disabled, only the new email is required to confirm.
57 | double_confirm_changes = true
58 | # If enabled, users need to confirm their email address before signing in.
59 | enable_confirmations = false
60 |
61 | [auth.email.template.invite]
62 | subject = "You have been invited"
63 | content_path = "./supabase/templates/invite.html"
64 |
65 | [auth.email.template.recovery]
66 | subject = "Reset Password"
67 | content_path = "./supabase/templates/recovery.html"
68 |
69 | # Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
70 | # `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin`, `notion`, `twitch`,
71 | # `twitter`, `slack`, `spotify`, `workos`, `zoom`.
72 | [auth.external.apple]
73 | enabled = false
74 | client_id = ""
75 | secret = ""
76 | # Overrides the default auth redirectUrl.
77 | redirect_uri = ""
78 | # Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
79 | # or any other third-party OIDC providers.
80 | url = ""
81 |
--------------------------------------------------------------------------------
/supabase/migrations/20230126140204_init.sql:
--------------------------------------------------------------------------------
1 | create table "public"."companies" (
2 | "id" bigint generated by default as identity not null,
3 | "name" character varying not null,
4 | "logo" character varying not null,
5 | "sector" character varying not null,
6 | "size" smallint not null,
7 | "linkedIn" character varying not null,
8 | "website" character varying not null,
9 | "phone_number" character varying not null,
10 | "address" character varying not null,
11 | "zipcode" character varying not null,
12 | "city" character varying not null,
13 | "stateAbbr" character varying not null,
14 | "sales_id" bigint not null,
15 | "created_at" timestamp without time zone not null
16 | );
17 |
18 |
19 | create table "public"."contactNotes" (
20 | "id" bigint generated by default as identity not null,
21 | "date" timestamp with time zone not null default now(),
22 | "text" character varying not null,
23 | "sales_id" bigint not null,
24 | "status" character varying not null,
25 | "contact_id" bigint
26 | );
27 |
28 |
29 | create table "public"."contacts" (
30 | "id" bigint generated by default as identity not null,
31 | "first_name" character varying not null,
32 | "last_name" character varying not null,
33 | "gender" character varying not null,
34 | "title" character varying not null,
35 | "company_id" bigint not null,
36 | "email" character varying not null,
37 | "phone_number1" character varying not null,
38 | "phone_number2" character varying not null,
39 | "background" character varying not null,
40 | "acquisition" character varying not null,
41 | "avatar" character varying null,
42 | "first_seen" timestamp without time zone not null,
43 | "last_seen" timestamp without time zone not null,
44 | "has_newsletter" boolean not null,
45 | "status" character varying not null,
46 | "tags" bigint[] not null,
47 | "sales_id" bigint not null
48 | );
49 |
50 |
51 | create table "public"."dealNotes" (
52 | "id" bigint generated by default as identity not null,
53 | "date" timestamp with time zone not null default now(),
54 | "deal_id" bigint not null,
55 | "sales_id" bigint not null,
56 | "type" character varying not null,
57 | "text" character varying not null
58 | );
59 |
60 |
61 | create table "public"."deals" (
62 | "id" bigint generated by default as identity not null,
63 | "created_at" timestamp without time zone not null,
64 | "name" character varying not null,
65 | "company_id" bigint not null,
66 | "contact_ids" bigint[] not null,
67 | "type" character varying not null,
68 | "stage" character varying not null,
69 | "description" character varying not null,
70 | "amount" bigint not null,
71 | "updated_at" timestamp without time zone not null,
72 | "start_at" timestamp without time zone not null,
73 | "sales_id" bigint not null,
74 | "index" bigint not null
75 | );
76 |
77 |
78 | create table "public"."sales" (
79 | "id" bigint generated by default as identity not null,
80 | "first_name" character varying not null,
81 | "last_name" character varying not null,
82 | "email" character varying not null
83 | );
84 |
85 |
86 | create table "public"."tags" (
87 | "id" bigint generated by default as identity not null,
88 | "name" character varying not null,
89 | "color" character varying not null
90 | );
91 |
92 |
93 | create table "public"."tasks" (
94 | "id" bigint generated by default as identity not null,
95 | "due_date" timestamp with time zone,
96 | "contact_id" bigint,
97 | "sales_id" bigint,
98 | "text" character varying,
99 | "type" character varying,
100 | "done_date" timestamp with time zone
101 | );
102 |
103 |
104 | CREATE UNIQUE INDEX companies_pkey ON public.companies USING btree (id);
105 |
106 | CREATE UNIQUE INDEX "contactNotes_pkey" ON public."contactNotes" USING btree (id);
107 |
108 | CREATE UNIQUE INDEX contacts_pkey ON public.contacts USING btree (id);
109 |
110 | CREATE UNIQUE INDEX "dealNotes_pkey" ON public."dealNotes" USING btree (id);
111 |
112 | CREATE UNIQUE INDEX deals_pkey ON public.deals USING btree (id);
113 |
114 | CREATE UNIQUE INDEX sales_pkey ON public.sales USING btree (id);
115 |
116 | CREATE UNIQUE INDEX tags_pkey ON public.tags USING btree (id);
117 |
118 | CREATE UNIQUE INDEX tasks_pkey ON public.tasks USING btree (id);
119 |
120 | alter table "public"."companies" add constraint "companies_pkey" PRIMARY KEY using index "companies_pkey";
121 |
122 | alter table "public"."contactNotes" add constraint "contactNotes_pkey" PRIMARY KEY using index "contactNotes_pkey";
123 |
124 | alter table "public"."contacts" add constraint "contacts_pkey" PRIMARY KEY using index "contacts_pkey";
125 |
126 | alter table "public"."dealNotes" add constraint "dealNotes_pkey" PRIMARY KEY using index "dealNotes_pkey";
127 |
128 | alter table "public"."deals" add constraint "deals_pkey" PRIMARY KEY using index "deals_pkey";
129 |
130 | alter table "public"."sales" add constraint "sales_pkey" PRIMARY KEY using index "sales_pkey";
131 |
132 | alter table "public"."tags" add constraint "tags_pkey" PRIMARY KEY using index "tags_pkey";
133 |
134 | alter table "public"."tasks" add constraint "tasks_pkey" PRIMARY KEY using index "tasks_pkey";
135 |
136 | alter table "public"."companies" add constraint "companies_sales_id_fkey" FOREIGN KEY (sales_id) REFERENCES sales(id) not valid;
137 |
138 | alter table "public"."companies" validate constraint "companies_sales_id_fkey";
139 |
140 | alter table "public"."contactNotes" add constraint "contactNotes_contact_id_fkey" FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE not valid;
141 |
142 | alter table "public"."contactNotes" validate constraint "contactNotes_contact_id_fkey";
143 |
144 | alter table "public"."contactNotes" add constraint "contactNotes_sales_id_fkey" FOREIGN KEY (sales_id) REFERENCES sales(id) ON DELETE CASCADE not valid;
145 |
146 | alter table "public"."contactNotes" validate constraint "contactNotes_sales_id_fkey";
147 |
148 | alter table "public"."contacts" add constraint "contacts_company_id_fkey" FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE not valid;
149 |
150 | alter table "public"."contacts" validate constraint "contacts_company_id_fkey";
151 |
152 | alter table "public"."contacts" add constraint "contacts_sales_id_fkey" FOREIGN KEY (sales_id) REFERENCES sales(id) not valid;
153 |
154 | alter table "public"."contacts" validate constraint "contacts_sales_id_fkey";
155 |
156 | alter table "public"."dealNotes" add constraint "dealNotes_deal_id_fkey" FOREIGN KEY (deal_id) REFERENCES deals(id) ON DELETE CASCADE not valid;
157 |
158 | alter table "public"."dealNotes" validate constraint "dealNotes_deal_id_fkey";
159 |
160 | alter table "public"."dealNotes" add constraint "dealNotes_sales_id_fkey" FOREIGN KEY (sales_id) REFERENCES sales(id) not valid;
161 |
162 | alter table "public"."dealNotes" validate constraint "dealNotes_sales_id_fkey";
163 |
164 | alter table "public"."deals" add constraint "deals_company_id_fkey" FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE not valid;
165 |
166 | alter table "public"."deals" validate constraint "deals_company_id_fkey";
167 |
168 | alter table "public"."deals" add constraint "deals_sales_id_fkey" FOREIGN KEY (sales_id) REFERENCES sales(id) not valid;
169 |
170 | alter table "public"."deals" validate constraint "deals_sales_id_fkey";
171 |
172 | alter table "public"."tasks" add constraint "tasks_contact_id_fkey" FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE not valid;
173 |
174 | alter table "public"."tasks" validate constraint "tasks_contact_id_fkey";
175 |
176 | alter table "public"."tasks" add constraint "tasks_sales_id_fkey" FOREIGN KEY (sales_id) REFERENCES sales(id) not valid;
177 |
178 | alter table "public"."tasks" validate constraint "tasks_sales_id_fkey";
179 |
180 |
181 |
--------------------------------------------------------------------------------
/supabase/migrations/20230220132931_fts.sql:
--------------------------------------------------------------------------------
1 | alter table "public"."companies" add column "fts" tsvector generated always as (to_tsvector('english'::regconfig, (((name)::text || ' '::text) || (website)::text))) stored;
2 |
3 | alter table "public"."contacts" add column "fts" tsvector generated always as (to_tsvector('english'::regconfig, (((((first_name)::text || ' '::text) || (last_name)::text) || ' '::text) || (email)::text))) stored;
4 |
5 | alter table "public"."deals" add column "fts" tsvector generated always as (to_tsvector('english'::regconfig, (((name)::text || ' '::text) || (description)::text))) stored;
6 |
7 | CREATE INDEX companies_fts ON public.companies USING gin (fts);
8 |
9 | CREATE INDEX contacts_fts ON public.contacts USING gin (fts);
10 |
11 | CREATE INDEX deals_fts ON public.deals USING gin (fts);
12 |
13 |
14 |
--------------------------------------------------------------------------------
/supabase/migrations/20240606161030_allow_nullable.sql:
--------------------------------------------------------------------------------
1 | alter table "public"."companies" alter column "address" drop not null;
2 | alter table "public"."companies" alter column "city" drop not null;
3 | alter table "public"."companies" alter column "created_at" drop not null;
4 | alter table "public"."companies" alter column "linkedIn" drop not null;
5 | alter table "public"."companies" alter column "logo" drop not null;
6 | alter table "public"."companies" alter column "phone_number" drop not null;
7 | alter table "public"."companies" alter column "sales_id" drop not null;
8 | alter table "public"."companies" alter column "sector" drop not null;
9 | alter table "public"."companies" alter column "size" drop not null;
10 | alter table "public"."companies" alter column "stateAbbr" drop not null;
11 | alter table "public"."companies" alter column "website" drop not null;
12 | alter table "public"."companies" alter column "zipcode" drop not null;
13 | alter table "public"."contacts" alter column "acquisition" drop not null;
14 | alter table "public"."contacts" alter column "background" drop not null;
15 | alter table "public"."contacts" alter column "email" drop not null;
16 | alter table "public"."contacts" alter column "first_seen" drop not null;
17 | alter table "public"."contacts" alter column "gender" drop not null;
18 | alter table "public"."contacts" alter column "last_seen" drop not null;
19 | alter table "public"."contacts" alter column "phone_number1" drop not null;
20 | alter table "public"."contacts" alter column "phone_number2" drop not null;
21 | alter table "public"."contacts" alter column "sales_id" drop not null;
22 | alter table "public"."contacts" alter column "status" drop not null;
23 | alter table "public"."contacts" alter column "title" drop not null;
24 | alter table "public"."deals" alter column "amount" drop not null;
25 | alter table "public"."deals" alter column "contact_ids" drop not null;
26 | alter table "public"."deals" alter column "created_at" drop not null;
27 | alter table "public"."deals" alter column "description" drop not null;
28 | alter table "public"."deals" alter column "sales_id" drop not null;
29 | alter table "public"."deals" alter column "start_at" drop not null;
30 | alter table "public"."deals" alter column "type" drop not null;
31 | alter table "public"."deals" alter column "updated_at" drop not null;
--------------------------------------------------------------------------------
/supabase/migrations/20250325160944_contact_tags.sql:
--------------------------------------------------------------------------------
1 | alter table "public"."contacts" rename column "tags" TO "tag_ids";
2 |
3 |
4 |
--------------------------------------------------------------------------------
/supabase/seed.sql:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marmelab/ra-supabase/3e06c12c7b916e067a0af5e4a270e0a8a257dd89/supabase/seed.sql
--------------------------------------------------------------------------------
/supabase/templates/invite.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | You have been invited
4 | You have been invited to create a user on {{ .SiteURL }}. Follow this link to accept the invite:
5 | Accept the invite
6 |
7 |
--------------------------------------------------------------------------------
/supabase/templates/recovery.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Reset Password
4 | Reset your password
5 |
6 |
--------------------------------------------------------------------------------
/test-global-setup.js:
--------------------------------------------------------------------------------
1 | module.exports = async () => {
2 | process.env.TZ = 'Europe/Paris';
3 | };
4 |
--------------------------------------------------------------------------------
/test-setup.js:
--------------------------------------------------------------------------------
1 | // Ignore warnings about act()
2 | // See https://github.com/testing-library/react-testing-library/issues/281,
3 | // https://github.com/facebook/react/issues/14769
4 | import { TextEncoder, TextDecoder } from 'util';
5 |
6 | const originalError = console.error;
7 | jest.spyOn(console, 'error').mockImplementation((...args) => {
8 | if (/Warning.*not wrapped in act/.test(args[0])) {
9 | return;
10 | }
11 | originalError.call(console, ...args);
12 | });
13 |
14 | /**
15 | * Mock fetch objects Response, Request and Headers
16 | */
17 | const { Response, Headers, Request } = require('whatwg-fetch');
18 |
19 | global.Response = Response;
20 | global.Headers = Headers;
21 | global.Request = Request;
22 |
23 | Object.assign(global, { TextDecoder, TextEncoder });
24 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
4 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
5 | "lib": [
6 | "es2017",
7 | "dom"
8 | ] /* Specify library files to be included in the compilation. */,
9 | "declaration": true,
10 | "declarationMap": true,
11 | "allowJs": true,
12 | "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
13 | "noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */,
14 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
15 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
16 | "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
17 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
18 | "skipLibCheck": true,
19 | "resolveJsonModule": true
20 | }
21 | }
22 |
--------------------------------------------------------------------------------