├── .devcontainer └── devcontainer.json ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── 01_bug_report.yaml │ ├── 02_feature_request.yaml │ ├── 03_question.yaml │ └── 04_codebase_improvement.yml ├── pull_request_template.md └── workflows │ ├── publish.yaml │ └── test.yaml ├── .gitignore ├── .markdownlint.json ├── .prettierignore ├── .prettierrc.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── jest.config.json ├── package-lock.json ├── package.json ├── renovate.json ├── rollup.config.js ├── src ├── DirectusProvider.tsx ├── components │ ├── DirectusAsset.spec.tsx │ ├── DirectusAsset.tsx │ ├── DirectusFile.tsx │ ├── DirectusImage.spec.tsx │ └── DirectusImage.tsx ├── hooks │ └── useDirectusAuth.tsx └── index.ts └── tsconfig.json /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "node:18", 3 | "postCreateCommand": "npm update -g npm", 4 | "mounts": [ 5 | { 6 | "source": "${localWorkspaceFolderBasename}-dist", 7 | "target": "${containerWorkspaceFolder}/dist", 8 | "type": "volume" 9 | }, 10 | { 11 | "source": "${localWorkspaceFolderBasename}-node_modules", 12 | "target": "${containerWorkspaceFolder}/node_modules", 13 | "type": "volume" 14 | } 15 | ], 16 | "customizations": { 17 | "vscode": { 18 | "settings": { 19 | "editor.codeLens": true, 20 | "editor.formatOnSave": true, 21 | "editor.linkedEditing": false, 22 | "editor.codeActionsOnSave": { 23 | "source.fixAll.eslint": true, 24 | "source.fixAll.markdownlint": true 25 | }, 26 | "eslint.enable": true, 27 | "eslint.format.enable": true, 28 | "files.exclude": { 29 | "dist/": true, 30 | "node_modules/": true 31 | }, 32 | "testExplorer.useNativeTesting": true 33 | }, 34 | "extensions": [ 35 | "DavidAnson.vscode-markdownlint", 36 | "dbaeumer.vscode-eslint", 37 | "donjayamanne.githistory", 38 | "editorconfig.editorconfig", 39 | "kavod-io.vscode-jest-test-adapter", 40 | "remcohaszing.schemastore" 41 | ] 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint", "eslint-plugin-tsdoc", "prettier"], 5 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"], 6 | "env": { 7 | "browser": true, 8 | "es6": true, 9 | "node": true 10 | }, 11 | "rules": { 12 | "prettier/prettier": "error", 13 | "consistent-return": "warn", 14 | "curly": ["warn", "all"], 15 | "eqeqeq": "warn", 16 | 17 | "new-cap": [ 18 | "warn", 19 | { 20 | "capIsNewExceptions": [] 21 | } 22 | ], 23 | 24 | "no-nested-ternary": "warn", 25 | "no-return-assign": ["warn", "always"], 26 | "no-unneeded-ternary": "warn", 27 | "no-var": "warn", 28 | "prefer-const": "warn", 29 | "sort-imports": [ 30 | "warn", 31 | { 32 | "ignoreCase": true 33 | } 34 | ], 35 | "spaced-comment": ["warn", "always"], 36 | "yoda": [ 37 | "warn", 38 | "always", 39 | { 40 | "onlyEquality": true 41 | } 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/01_bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug report 2 | description: If something isn't working 🔧 3 | title: '[Bug]: ' 4 | labels: ['bug'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | **Thanks ❤️ for taking the time to fill out this bug report!** We kindly ask that you search to see if an issue [already exists](https://github.com/gremo/react-directus/issues?q=is%3Aissue+label%3Abug+sort%3Acreated-desc+) for the bug. 10 | 11 | - type: input 12 | attributes: 13 | label: Version of react-directus package? 14 | description: Please verify the output of the command `npm list react-directus` and provide the version here. 15 | validations: 16 | required: true 17 | 18 | - type: input 19 | attributes: 20 | label: Version of Directus SDK? 21 | description: Please verify the output of the command `npm list @directus/sdk` and provide the version here. 22 | validations: 23 | required: true 24 | 25 | - type: textarea 26 | attributes: 27 | label: Bug description 28 | description: A clear and concise description of the issue you're experiencing and the steps to reproduce. 29 | validations: 30 | required: true 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/02_feature_request.yaml: -------------------------------------------------------------------------------- 1 | name: 🚀 Feature request 2 | description: If you have a feature request 💡 3 | title: '[Feature]: ' 4 | labels: ['enhancement'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | **Thanks :heart: for taking the time to fill out this feature request!** We kindly ask that you search to see if an issue [already exists](https://github.com/gremo/react-directus/issues?q=is%3Aissue+label%3Aenhancement+sort%3Acreated-desc+) for the feature. 10 | 11 | - type: textarea 12 | attributes: 13 | label: Description 14 | description: | 15 | A clear and concise description of the feature you're interested in. 16 | validations: 17 | required: true 18 | 19 | - type: textarea 20 | attributes: 21 | label: Suggested solution 22 | description: | 23 | Describe the solution you'd like. A clear and concise description of what you want to happen. 24 | 25 | - type: checkboxes 26 | attributes: 27 | label: Are you willing to contribute it? 28 | options: 29 | - label: I can work on a pull request to implement this feature 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/03_question.yaml: -------------------------------------------------------------------------------- 1 | name: ❓Question and help 2 | description: If you have any question or need help 🛟 3 | title: '[Question]: ' 4 | labels: ['documentation', 'question'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | **Thanks :heart: for taking the time to fill out this question!** We kindly ask that you search to see if an issue [already exists](https://github.com/gremo/react-directus/issues?q=is%3Aissue+label%3Adocumentation,question+sort%3Acreated-desc+) for the question. 10 | 11 | - type: textarea 12 | attributes: 13 | label: Description 14 | description: A clear and concise summary of your question. 15 | validations: 16 | required: true 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/04_codebase_improvement.yml: -------------------------------------------------------------------------------- 1 | name: 🤓 Codebase improvement 2 | description: If you want to improve codebase quality and maintainability 👨‍💻 3 | title: '[Codebase]: ' 4 | labels: ['enhancement'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | **Thanks :heart: for taking the time to fill out this codebase improvement!** We kindly ask that you search to see if an issue [already exists](https://github.com/gremo/react-directus/issues?q=is%3Aissue+label%3Aenhancement+sort%3Acreated-desc+) for the improvement. 10 | 11 | - type: textarea 12 | attributes: 13 | label: Description 14 | description: | 15 | A clear and concise description of the improvement you suggest. 16 | validations: 17 | required: true 18 | 19 | - type: checkboxes 20 | attributes: 21 | label: Are you willing to contribute it? 22 | options: 23 | - label: I can work on a pull request to implement this improvement 24 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | #### 🔧 Types of changes 2 | 3 | Please explain the changes you made here. 4 | 5 | *Put an `x` in the boxes that apply (you can also fill these out after creating the PR).* 6 | 7 | - [ ] Bugfix (non-breaking change which fixes an issue) 8 | - [ ] New feature (non-breaking change which adds functionality) 9 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 10 | - [ ] Documentation update 11 | - [ ] Codebase improvement 12 | - [ ] Other (if none of the other choices apply) 13 | 14 | #### 🚨 Checklist 15 | 16 | Your checklist for this pull request. 17 | 18 | *Put an `x` in the boxes that apply (you can also fill these out after creating the PR).* 19 | 20 | - [ ] I've read the [guidelines for contributing](https://github.com/gremo/react-directus/blob/main/CONTRIBUTING.md) 21 | - [ ] I've added necessary documentation (if appropriate) 22 | - [ ] I've ensured that my code additions do not fail linting or unit tests (if applicable) 23 | 24 | #### Description 25 | 26 | Please describe your pull request here. 27 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | 15 | - name: Setup Node.js environment 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: 18 19 | registry-url: 'https://registry.npmjs.org' 20 | 21 | - name: Install dependencies 22 | run: npm ci 23 | 24 | - name: Build 25 | run: npm run build 26 | 27 | - name: Publish 28 | run: npm publish 29 | env: 30 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 31 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | paths: 6 | - .github/workflows/test.yaml 7 | - src/** 8 | - jest.config.json 9 | - package.json 10 | - rollup.config.js 11 | - tsconfig.json 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | react-version: [17, ""] 20 | 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 26 | 27 | - name: Setup Node.js 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: 18 31 | 32 | - name: Install dependencies 33 | run: | 34 | npm config set legacy-peer-deps true 35 | npm install 36 | 37 | - name: Install react 38 | if: matrix.react-version 39 | run: npm install react@^${{ matrix.react-version }} react-dom@^${{ matrix.react-version }} 40 | 41 | # react 17.x support @testing-library/react up to version 12.x 42 | - name: Install @testing-library/react 43 | if: >- 44 | startsWith(matrix.react-version, '17') 45 | run: npm install @testing-library/react@^12 46 | 47 | - name: Test 48 | run: npm test 49 | 50 | - name: Build 51 | run: npm run build 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | /node_modules/ 3 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "first-line-heading": false, 4 | "line-length": false, 5 | "no-inline-html": false 6 | } 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | ./github 2 | *.md -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "es5", 8 | "bracketSpacing": true, 9 | "jsxSingleQuote": true, 10 | "quoteProps": "as-needed", 11 | "arrowParens": "avoid", 12 | "insertPragma": false 13 | } 14 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our community include: 12 | 13 | - Demonstrating empathy and kindness toward other people 14 | - Being respectful of differing opinions, viewpoints, and experiences 15 | - Giving and gracefully accepting constructive feedback 16 | - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 17 | - Focusing on what is best not just for us as individuals, but for the overall community 18 | 19 | Examples of unacceptable behavior include: 20 | 21 | - The use of sexualized language or imagery, and sexual attention or advances of any kind 22 | - Trolling, insulting or derogatory comments, and personal or political attacks 23 | - Public or private harassment 24 | - Publishing others' private information, such as a physical or email address, without their explicit permission 25 | - Other conduct which could reasonably be considered inappropriate in a professional setting 26 | 27 | ## Enforcement Responsibilities 28 | 29 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | Community leaders 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, and will communicate reasons for moderation decisions when appropriate. 32 | 33 | ## Scope 34 | 35 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 36 | 37 | ## Enforcement 38 | 39 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [gremo1982@gmail.com](mailto:gremo1982@gmail.com). All complaints will be reviewed and investigated promptly and fairly. 40 | 41 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 42 | 43 | ## Attribution 44 | 45 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. 46 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First off, thanks for taking the time to contribute! ❤️ 4 | 5 | New features, ideas and bug fixes are always welcome! Everyone interacting in the project's code bases or issue trackers, is expected to follow the [Code of Conduct](CODE_OF_CONDUCT.md). 6 | 7 | > And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about: 8 | > - Star the project 9 | > - Tweet about it 10 | > - Refer this project in your project's readme 11 | > - Mention the project at local meetups and tell your friends/colleagues 12 | 13 | In order to contribute to this project, follow a few easy steps: 14 | 15 | 1. [Fork](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) this repository and clone it on your machine 16 | 2. Open the local repository with [Visual Studio Code](https://code.visualstudio.com) with the remote development feature enabled (install the [Remote Development extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack)) 17 | 3. Create a branch `my-awesome-feature` and commit to it 18 | 4. Run `npm run pre-pull-request` and verify that they complete without errors. 19 | 5. Push `my-awesome-feature` branch to GitHub and open a [pull request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests) 20 | 6. Liked some of my work? Buy me a ☕ (or more likely 🍺) 21 | 22 |

23 | 24 |

25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2021, Marco Polichetti 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Directus logo 3 |

4 | 5 |

6 | react-directus 7 |

8 | 9 |

10 | NPM version 11 | NPM downloads 12 | 13 |

14 | 15 |

16 | GitHub last commit 17 | GitHub Workflow Status 18 | GitHub issues 19 | GitHub pull requests 20 |

21 | 22 |

23 | A set of React components and utilities for Directus Headless CMS. 24 |

25 | 26 | ## 🚀 Quick start 27 | 28 | Install this library along with `@directus/sdk@` (version 10 or below): 29 | 30 | > **Note**: Directus SDK version 11 and upwards are currently not supported, but active work is in progress to add support for these versions in future releases. 31 | 32 | ```bash 33 | npm install react-directus @directus/sdk@^10 34 | ``` 35 | 36 | The `` component makes the [Directus JavaScript SDK](https://docs.directus.io/reference/sdk/) available to any nested components that need to access it. The provider accepts the following props: 37 | 38 | - `apiUrl`: the URL of your Directus API 39 | - `options` (optional): an object containing the [Directus client options](https://docs.directus.io/reference/sdk/#reference) 40 | - `autoLogin` (optional): if `true`, the SDK will try to login using the `accessToken` stored in the browser's local storage 41 | - `onAutoLoginError` (optional): a callback function that is called when the auto-login fails 42 | 43 | ```jsx 44 | import { App } from './App'; 45 | import { DirectusProvider } from 'react-directus'; 46 | import { createRoot } from 'react-dom/client'; 47 | 48 | const root = createRoot(document.getElementById('root')); 49 | 50 | root.render( 51 | 52 | 53 | 54 | ); 55 | ``` 56 | 57 | With **TypeScript**, you can use the optional generic collection type for Directus, as described in the [Directus TypeScript documentation](https://docs.directus.io/reference/old-sdk.html#typescript): 58 | 59 | ```jsx 60 | import { App } from './App'; 61 | import { DirectusProvider } from 'react-directus'; 62 | import { createRoot } from 'react-dom/client'; 63 | 64 | import MyCollections from './types'; 65 | 66 | const root = createRoot(document.getElementById('root')); 67 | 68 | root.render( 69 | apiUrl="https://api.example.com" options={{}}> 70 | 71 | 72 | ); 73 | ``` 74 | 75 | ## ⚙️ Hooks 76 | 77 | ### `useDirectus` 78 | 79 | After adding the provider, you can access the configured client anywhere in the app, using the `useDirectus` hook: 80 | 81 | ```jsx 82 | import { useEffect, useState } from 'react'; 83 | import { useDirectus } from 'react-directus' 84 | 85 | export const TodoList = () => { 86 | // Get the Directus SDK object 87 | const { directus } = useDirectus(); 88 | const [todos, setTodos] = useState([]); 89 | 90 | useEffect(() => { 91 | const fetchTodos = async () => { 92 | const todos = (await directus.items('todos').readMany()).data; 93 | setTodos(todos); 94 | }; 95 | 96 | fetchTodos(); 97 | }, [directus]); 98 | 99 | return todos.map(item => ); 100 | }; 101 | ``` 102 | 103 | ### `useDirectusAuth` 104 | 105 | The `useDirectusAuth` hook provides a few methods for working with the [Directus Authentication API](https://docs.directus.io/reference/old-sdk.html#authentication): 106 | 107 | - `login` - a function that accepts an email and password and returns a promise that resolves to the user object if the login is successful or rejects with an error otherwise 108 | - `logout` - a function that logs out the current user 109 | - `user` - the current user object 110 | - `authState` - the current authentication state, one of `loading` (the initial state), `logged-in` or `logged-out` 111 | 112 | ```jsx 113 | import { useDirectusAuth } from 'react-directus'; 114 | import { FormEvent } from 'react'; 115 | 116 | const Login = () => { 117 | const { login } = useDirectusAuth(); 118 | 119 | const handleSubmit = (e: FormEvent) => { 120 | e.preventDefault(); 121 | 122 | const { email, password } = e.currentTarget.elements; 123 | login(email.value, password.value).catch(err => { 124 | console.error(err); 125 | }); 126 | }; 127 | 128 | return ( 129 |
130 | 131 | 132 | 133 |
134 | ); 135 | }; 136 | 137 | export default Login; 138 | 139 | ``` 140 | 141 | ## 🧩 Components (so far...) 142 | 143 | This package contains a component for working with Direcuts [files](https://docs.directus.io/reference/files/). It is configured for using the `apiUrl` and `accessToken` specified in the provider. Hopefully, more will come in the future 🤗. 144 | 145 | > **Note**: The components can also be used in a "standalone" way, meaning that they are not bound to the `apiUrl` specified in the provider. In that case, they both accept an `apiUrl` and an optional `accessToken` prop. 146 | 147 | ### `` 148 | 149 | Computes the URL of the given resource `asset`, rendering it using the `render` prop: 150 | 151 | - `apiUrl`: the API URL of the Directus instance(can be omitted if the provider is used) 152 | - `accessToken`: the access token to use for authentication (can be omitted if the provider is used) 153 | - `asset`: the asset representing the resource (`string` or `object` with an `id` property) 154 | - `download`: force browser to download the asset (force the `Content-Disposition` header) 155 | - `directusTransform`: an object with the Directus [transform](https://docs.directus.io/reference/files/#transform) options or a preset key 156 | - `filename`: the filename to use for the asset [SEO](https://docs.directus.io/reference/files/#accessing-a-file) 157 | - `render`: a function (which receives an object with the `url` property) that provides the component to render 158 | 159 | #### Example with custom transform 160 | 161 | ```jsx 162 | import { DirectusFile } from 'react-directus'; 163 | 164 | export const MyImage = ({ imageId }) => ( 165 | } 169 | /> 170 | ); 171 | ``` 172 | 173 | #### Example for downloading a file 174 | 175 | ```jsx 176 | import { DirectusFile } from 'react-directus'; 177 | 178 | export const MyImage = ({ imageId }) => ( 179 | Download} 184 | /> 185 | ); 186 | ``` 187 | 188 | ## 📱 React Native 189 | 190 | To make the project fully compatible with React Native you need to install the [localstorage-polyfill](https://www.npmjs.com/package/localstorage-polyfill) package: 191 | 192 | ```bash 193 | npm install localstorage-polyfill 194 | ``` 195 | 196 | Then import the module **before any other import** and force the storage mode "LocalStorage" in your Directus instance: 197 | 198 | ```jsx 199 | import 'localstorage-polyfill'; 200 | import { DirectusProvider } from 'react-directus'; 201 | import { View } from 'react-native'; 202 | 203 | export default function App({}) { 204 | return ( 205 | 209 | 210 | 211 | ) 212 | } 213 | ``` 214 | 215 | In future releases, a solution using `AsyncStorage` or an encrypted secure storage option is planned. 216 | 217 | ## ❤️ Contributing 218 | 219 | All types of contributions are encouraged and valued. See the [Contributing](CONTRIBUTING.md) guidelines, the community looks forward to your contributions! 220 | 221 | ## 📘 License 222 | 223 | This project is released under the under terms of the [ISC License](LICENSE). 224 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | **Please do not report security vulnerabilities through public GitHub issues.** 6 | 7 | To report a security issue, please email [gremo1982@gmail.com](mailto:gremo1982@gmail.com) with a description of the issue, the steps you took to create the issue, affected versions, and, if known, mitigations for the issue. You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. 8 | 9 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 10 | 11 | - Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 12 | - Full paths of source file(s) related to the manifestation of the issue 13 | - The location of the affected source code (tag/branch/commit or direct URL) 14 | - Any special configuration required to reproduce the issue 15 | - Step-by-step instructions to reproduce the issue 16 | - Proof-of-concept or exploit code (if possible) 17 | - Impact of the issue, including how an attacker might exploit the issue 18 | 19 | This information will help us triage your report more quickly. 20 | 21 | ## Preferred Languages 22 | 23 | We prefer all communications to be in English. 24 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "displayName": "react-directus", 3 | "preset": "ts-jest", 4 | "testEnvironment": "jsdom", 5 | "testMatch": ["/src/**/*(*.)@(spec|test).[tj]s?(x)"], 6 | "moduleNameMapper": { 7 | "^@/(.*)$": "/src/$1", 8 | "^@components/(.*)$": "/src/components/$1", 9 | "^@hooks/(.*)$": "/src/hooks/$1" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-directus", 3 | "version": "0.0.2", 4 | "description": "A set of React components and utilities for Directus Headless CMS", 5 | "homepage": "https://github.com/gremo/react-directus", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/gremo/react-directus.git" 9 | }, 10 | "type": "module", 11 | "files": [ 12 | "dist" 13 | ], 14 | "main": "dist/index.cjs.js", 15 | "module": "dist/index.esm.js", 16 | "types": "dist/types", 17 | "scripts": { 18 | "lint": "eslint src/* --ext .ts,.tsx", 19 | "test": "jest", 20 | "build": "rimraf dist/* && rollup -c --bundleConfigAsCjs", 21 | "prepublishOnly": "npm run build", 22 | "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json}\"", 23 | "pre-pull-request": "npm run format && npm run lint && npm run test && npm run build" 24 | }, 25 | "pre-commit": [ 26 | "format", 27 | "lint" 28 | ], 29 | "keywords": [ 30 | "react", 31 | "react-hooks", 32 | "directus", 33 | "headless", 34 | "cms" 35 | ], 36 | "author": "Marco Polichetti ", 37 | "license": "ISC", 38 | "devDependencies": { 39 | "@directus/sdk": "10.3.5", 40 | "@rollup/plugin-commonjs": "25.0.8", 41 | "@rollup/plugin-node-resolve": "15.3.1", 42 | "@rollup/plugin-typescript": "11.1.6", 43 | "@testing-library/react": "14.3.1", 44 | "@types/jest": "29.5.14", 45 | "@types/react": "18.3.18", 46 | "@types/react-dom": "18.3.5", 47 | "@typescript-eslint/eslint-plugin": "6.21.0", 48 | "@typescript-eslint/parser": "6.21.0", 49 | "eslint": "8.57.1", 50 | "eslint-config-prettier": "9.1.0", 51 | "eslint-plugin-prettier": "5.2.3", 52 | "eslint-plugin-tsdoc": "0.4.0", 53 | "jest": "29.7.0", 54 | "jest-environment-jsdom": "29.7.0", 55 | "pre-commit": "1.2.2", 56 | "prettier": "3.5.3", 57 | "react": "18.3.1", 58 | "react-dom": "18.3.1", 59 | "rimraf": "5.0.10", 60 | "rollup": "4.34.9", 61 | "rollup-plugin-typescript2": "0.36.0", 62 | "ts-jest": "29.2.6", 63 | "tslib": "2.8.1", 64 | "typescript": "5.8.2" 65 | }, 66 | "peerDependencies": { 67 | "@directus/sdk": "^9.0.0 || ^10.0.0", 68 | "react": "^17.0.0 || ^18.0.0", 69 | "react-dom": "^17.0.0 || ^18.0.0" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | "docker:disable", 5 | ":automergeMinor", 6 | ":automergePatch", 7 | ":disablePeerDependencies", 8 | ":pinDevDependencies", 9 | ":semanticCommitsDisabled" 10 | ], 11 | "packageRules": [ 12 | { 13 | "matchDepTypes": ["dependencies"], 14 | "rangeStrategy": "bump" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | import pkg from './package.json'; 3 | import resolve from '@rollup/plugin-node-resolve'; 4 | import typescript from '@rollup/plugin-typescript'; 5 | export default { 6 | input: 'src/index.ts', 7 | output: [ 8 | { 9 | file: pkg.main, 10 | format: 'cjs', 11 | }, 12 | { 13 | file: pkg.module, 14 | format: 'es', 15 | }, 16 | ], 17 | external: ['@directus/sdk', 'react'], 18 | plugins: [resolve(), commonjs(), typescript()], 19 | }; 20 | -------------------------------------------------------------------------------- /src/DirectusProvider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, Dispatch, ReactNode, SetStateAction, useContext, useEffect, useMemo, useState } from 'react'; 2 | import { Directus, DirectusOptions, IDirectus, TypeMap, UserType } from '@directus/sdk'; 3 | import { DirectusAsset, DirectusAssetProps } from '@components/DirectusAsset'; 4 | import { DirectusImage, DirectusImageProps } from '@components/DirectusImage'; 5 | import { AuthStates } from '@hooks/useDirectusAuth'; 6 | 7 | /** 8 | * Shape of the main context. 9 | * @typeParam T - The `TypeMap` of your Directus instance. 10 | */ 11 | export interface DirectusContextType { 12 | /** url to your Directus instance. */ 13 | apiUrl: string; 14 | /** 15 | * The Directus client instance configured with: 16 | * - the `TypeMap` you provided 17 | * - the `apiUrl` you provided 18 | * - the `options` you provided 19 | */ 20 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 21 | directus: IDirectus; 22 | /** 23 | * {@inheritDoc DirectusAsset} 24 | * @deprecated Please import the new `DirectusFile` component instead. 25 | DirectusAsset: typeof DirectusAsset; 26 | /** 27 | * {@inheritDoc DirectusImage} 28 | * @deprecated Please import the `DirectusFile` component instead. 29 | */ 30 | DirectusImage: typeof DirectusImage; 31 | /** 32 | * Please use the data provided by the `useDirectusAuth` hook instead. 33 | * @defaultValue 'loading' 34 | * @internal 35 | */ 36 | _authState: AuthStates; 37 | /** 38 | * Please use the functions provided by the `useDirectusAuth` hook instead. 39 | * @internal 40 | */ 41 | _setAuthState: Dispatch>; 42 | /** 43 | * Please use the data provided by the `useDirectusAuth` hook instead. 44 | * @defaultValue null 45 | * @internal 46 | */ 47 | _directusUser: UserType | null; 48 | /** 49 | * Please use the functions provided by the `useDirectusAuth` hook instead. 50 | * @internal 51 | */ 52 | _setDirectusUser: Dispatch>; 53 | } 54 | 55 | export type DirectusContextTypeGeneric = DirectusContextType | null; 56 | 57 | /** 58 | * DirectusContext is a React Context that provides an instance of the Directus SDK and the apiUrl to all child components. 59 | * @typeParam T - TypeMap of your Directus Collections 60 | * @returns DirectusContext 61 | */ 62 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 63 | export const DirectusContext = createContext>(null); 64 | 65 | export interface DirectusProviderProps { 66 | /** url to your Directus instance. */ 67 | apiUrl: string; 68 | /** A set of options to pass to the Directus client. {@link https://docs.directus.io/reference/old-sdk.html#custom-configuration | Directus Client configuration} */ 69 | options?: DirectusOptions; 70 | /** 71 | * If `true`, the provider will try to login the user automatically on mount. 72 | * @defaultValue false 73 | */ 74 | autoLogin?: boolean; 75 | /** 76 | * Callback function that will be called if the auto login fails. 77 | */ 78 | onAutoLoginError?: (error: Error) => void; 79 | children: ReactNode; 80 | } 81 | 82 | /** 83 | * DirectusProvider is a React Context Provider that provides an instance of the Directus SDK and the apiUrl to all child components. 84 | * @param apiUrl - The URL of your Directus API 85 | * @param options - Directus SDK options 86 | * @typeParam T - TypeMap of your Directus Collections 87 | * @returns DirectusProvider 88 | * @example Here is an example of how to use DirectusProvider 89 | * ```tsx 90 | * import { App } from './App'; 91 | * import { DirectusProvider } from 'react-directus'; 92 | * import { createRoot } from 'react-dom/client'; 93 | * 94 | * const root = createRoot(document.getElementById('root')); 95 | * root.render( 96 | * 97 | * 98 | * 99 | * ); 100 | * ``` 101 | */ 102 | export const DirectusProvider = ({ 103 | apiUrl, 104 | options, 105 | autoLogin, 106 | onAutoLoginError, 107 | children, 108 | }: DirectusProviderProps): JSX.Element => { 109 | const [user, setUser] = useState(null); 110 | const [authState, setAuthState] = useState(autoLogin ? AuthStates.LOADING : AuthStates.UNAUTHENTICATED); 111 | 112 | const directus = useMemo(() => new Directus(apiUrl, options), [apiUrl, options]); 113 | 114 | const value = useMemo>( 115 | () => ({ 116 | apiUrl, 117 | directus, 118 | DirectusAsset: ({ asset, render, ...props }: DirectusAssetProps) => { 119 | console.warn('Deprecated: Please import the new `DirectusFile` component instead.'); 120 | return ; 121 | }, 122 | DirectusImage: ({ asset, render, ...props }: DirectusImageProps) => { 123 | console.warn('Deprecated: Please import the new `DirectusFile` component instead.'); 124 | return ; 125 | }, 126 | _directusUser: user, 127 | _setDirectusUser: setUser, 128 | _authState: authState, 129 | _setAuthState: setAuthState, 130 | }), 131 | [apiUrl, directus, user, authState] 132 | ); 133 | 134 | useEffect(() => { 135 | const checkAuth = async () => { 136 | let newAuthState: AuthStates = AuthStates.UNAUTHENTICATED; 137 | try { 138 | await directus.auth.refresh(); 139 | const token = await directus.auth.token; 140 | 141 | if (token) { 142 | const dUser = (await directus.users.me.read({ 143 | // * is a valid field, but typescript doesn't like it 144 | // It's a wildcard, so it will return all fields 145 | // This is the only way to get all fields 146 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 147 | fields: ['*'] as any, 148 | })) as UserType; 149 | 150 | if (dUser) { 151 | newAuthState = AuthStates.AUTHENTICATED; 152 | setUser(dUser); 153 | } 154 | } 155 | } catch (error) { 156 | if (onAutoLoginError && error instanceof Error) { 157 | onAutoLoginError(error); 158 | } 159 | } finally { 160 | setAuthState(newAuthState || AuthStates.UNAUTHENTICATED); 161 | } 162 | }; 163 | if (autoLogin) { 164 | checkAuth(); 165 | } 166 | }, [directus, autoLogin]); 167 | 168 | return {children}; 169 | }; 170 | 171 | /** 172 | * useDirectus is a React Hook that provides an instance of the Directus SDK and the apiUrl 173 | * @returns DirectusContextType 174 | * @example Here is an example of how to use useDirectus 175 | * ```tsx 176 | * const { directus } = useDirectus(); 177 | * directus.auth.login({ email: '', password: '' }); 178 | * ``` 179 | */ 180 | export const useDirectus = () => { 181 | const directusContext = useContext(DirectusContext); 182 | 183 | if (!directusContext) { 184 | throw new Error('useDirectus has to be used within the DirectusProvider'); 185 | } 186 | 187 | return directusContext; 188 | }; 189 | -------------------------------------------------------------------------------- /src/components/DirectusAsset.spec.tsx: -------------------------------------------------------------------------------- 1 | import { act, render } from '@testing-library/react'; 2 | import { DirectusAsset, RenderPropsAsset } from '@components/DirectusAsset'; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 5 | const dummyRenderer = jest.fn(arg => <>); 6 | 7 | beforeEach(() => { 8 | dummyRenderer.mockClear(); 9 | }); 10 | 11 | describe('Component', () => { 12 | it.each([ 13 | [{ apiUrl: 'http://example.com', asset: '11263b49-13b9-4ed8-ba39-01fe7cbeb284', download: true }], 14 | [{ apiUrl: 'http://example.com', asset: { id: '1b86a776-34e5-4297-b8c5-58a85f15abe2' }, download: false }], 15 | ])('pass the props to the renderer', async props => { 16 | await act(async () => { 17 | render(); 18 | }); 19 | 20 | expect(dummyRenderer.mock.calls[0][0]).toMatchObject(props); 21 | }); 22 | 23 | it.each([ 24 | [ 25 | { apiUrl: 'http://example.com', asset: '8237bae3-2667-472f-91b3-642408afd69c', download: true }, 26 | 'http://example.com/assets/8237bae3-2667-472f-91b3-642408afd69c?download=', 27 | ], 28 | [ 29 | { apiUrl: 'http://example.com', asset: { id: '177326d9-e0f2-4747-b078-76de4f2d4de2' } }, 30 | 'http://example.com/assets/177326d9-e0f2-4747-b078-76de4f2d4de2?', 31 | ], 32 | ])('build and pass the url to the renderer', async (props, expectedUrl) => { 33 | await act(async () => { 34 | render(); 35 | }); 36 | 37 | expect(dummyRenderer.mock.calls[0][0].url).toBe(expectedUrl); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/components/DirectusAsset.tsx: -------------------------------------------------------------------------------- 1 | import { DirectusAssetObject, DirectusFile, RenderPropsFile } from '@components/DirectusFile'; 2 | 3 | export interface RenderPropsAsset extends Omit { 4 | url?: string; 5 | } 6 | 7 | export interface DirectusAssetProps { 8 | apiUrl: string; 9 | asset: DirectusAssetObject; 10 | download?: boolean; 11 | render: (props: RenderPropsAsset) => JSX.Element; 12 | } 13 | 14 | /** 15 | * @deprecated Please import the new `DirectusFile` component instead. 16 | */ 17 | export const DirectusAsset = ({ apiUrl, asset, download, render }: DirectusAssetProps): JSX.Element => { 18 | const renderOld = (props: RenderPropsFile): JSX.Element => { 19 | return render({ 20 | apiUrl, 21 | url: props.url ?? '', 22 | asset: props.asset, 23 | download, 24 | }); 25 | }; 26 | 27 | return ; 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/DirectusFile.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useState } from 'react'; 2 | import { DirectusContext } from '@/DirectusProvider'; 3 | 4 | /** 5 | * DirectusAssetObject is the object that is returned by the Directus API when you request an asset. 6 | * It can be either the id of the asset or an object containing the id and additional information. 7 | */ 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | export type DirectusAssetObject = string | ({ id: string } & Record); 10 | 11 | /** 12 | * The fit of the thumbnail while always preserving the aspect ratio. 13 | */ 14 | export enum Fit { 15 | /** Covers both width/height by cropping/clipping to fit */ 16 | cover = 'cover', 17 | /** Contain within both width/height using "letterboxing" as needed */ 18 | contain = 'contain', 19 | /** Resize to be as large as possible, ensuring dimensions are less than or equal to the requested width and height */ 20 | inside = 'inside', 21 | /** Resize to be as small as possible, ensuring dimensions are greater than or equal to the requested width and height */ 22 | outside = 'outside', 23 | } 24 | 25 | /** 26 | * What file format to return the image in. 27 | */ 28 | export enum Format { 29 | /** Will try to format it in ´webp´ or ´avif´ if the browser supports it, otherwise it will fallback to ´jpg´. */ 30 | auto = 'auto', 31 | jpg = 'jpg', 32 | png = 'png', 33 | webp = 'webp', 34 | tiff = 'tiff', 35 | } 36 | 37 | /** 38 | * Represents the {@link https://docs.directus.io/reference/files.html#requesting-a-thumbnail | Custom Transformations} you can apply to an image. 39 | */ 40 | export interface TransformCustomProp { 41 | /** The width of the thumbnail in pixels.*/ 42 | width: number; 43 | /** The height of the thumbnail in pixels. */ 44 | height: number; 45 | /** The quality of the thumbnail (1 to 100). */ 46 | quality: number; 47 | /** The fit of the thumbnail while always preserving the aspect ratio. */ 48 | fit: Fit; 49 | /** The file format of the thumbnail. */ 50 | format: Format; 51 | /** Disable image up-scaling. */ 52 | withoutEnlargement: boolean; 53 | /** An array of sharp operations to apply to the image. {@link https://sharp.pixelplumbing.com/api-operation | Sharp API}*/ 54 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 55 | transforms: [string, ...any[]][]; 56 | } 57 | 58 | export interface RenderPropsFile extends Omit { 59 | url: string | undefined; 60 | } 61 | 62 | export type DirectusFileRenderer = (props: RenderPropsFile) => JSX.Element; 63 | 64 | export interface DirectusFileProps { 65 | /** url to your Directus instance. */ 66 | apiUrl?: string; 67 | /** The current user's access token. */ 68 | accessToken?: string; 69 | /** The asset that should be rendered. */ 70 | asset: DirectusAssetObject; 71 | /** If the asset should be downloaded instead of rendered. */ 72 | download?: boolean; 73 | /** Either a preset key or a custom transform object. */ 74 | directusTransform?: Partial | string; 75 | /** 76 | * The filename of the image. If the filename is not provided, the image will be downloaded with the asset's id as filename. 77 | * {@link https://docs.directus.io/reference/files.html#accessing-a-file| SEO} 78 | */ 79 | filename?: string; 80 | /** A function that returns the React element to be rendered.*/ 81 | render: DirectusFileRenderer; 82 | } 83 | 84 | /** 85 | * DirectusFile is a React Component that renders an image from your Directus API. 86 | * @example Here is an example of how to use DirectusFile with a custom transform 87 | * ```tsx 88 | * import { DirectusFile } from 'react-directus'; 89 | * 90 | * export const MyImage = ({ imageId }) => ( 91 | * } 96 | * ``` 97 | * 98 | * @example Here is an example of how to use DirectusFile to download an file 99 | * ```tsx 100 | * import { DirectusFile } from 'react-directus'; 101 | * 102 | * export const MyImage = ({ imageId }) => ( 103 | * Download 108 | * />} 109 | * 110 | * ``` 111 | */ 112 | 113 | export const DirectusFile = ({ 114 | apiUrl: propsApiUrl, 115 | accessToken: propsAccessToken, 116 | asset, 117 | download, 118 | filename, 119 | directusTransform, 120 | render, 121 | }: DirectusFileProps): JSX.Element => { 122 | const directusContext = useContext(DirectusContext); 123 | const { directus, apiUrl: contextApiUrl, _authState } = directusContext || {}; 124 | const apiUrl = propsApiUrl || contextApiUrl; 125 | 126 | if (!apiUrl) { 127 | throw new Error('DirectusFile requires either a DirectusProvider or an apiUrl prop'); 128 | } 129 | 130 | const assetId = asset && 'object' === typeof asset ? asset.id : asset; 131 | 132 | if (!assetId) { 133 | throw new Error('DirectusFile requires an asset id'); 134 | } 135 | 136 | const generateImageUrl = (token: string | null = null): string => { 137 | const params = new URLSearchParams(); 138 | 139 | if (token) { 140 | params.append('access_token', token); 141 | } 142 | 143 | if (download) { 144 | params.append('download', ''); 145 | } 146 | 147 | if ('string' === typeof directusTransform) { 148 | params.append('key', directusTransform); 149 | } else if ('object' === typeof directusTransform) { 150 | // Adds all the custom transforms to the params 151 | for (const [key, value] of Object.entries(directusTransform)) { 152 | if (value) { 153 | params.append(key, value.toString()); 154 | } 155 | } 156 | } 157 | 158 | return `${apiUrl}/assets/${assetId}${filename ? '/' + filename : ''}?${params.toString()}`; 159 | }; 160 | 161 | const getInitialImageUrl = (): string | undefined => { 162 | if (propsAccessToken) { 163 | return generateImageUrl(propsAccessToken); 164 | } else if ('authenticated' !== _authState) { 165 | return generateImageUrl(); 166 | } 167 | return undefined; 168 | }; 169 | 170 | const [imageUrl, setImageUrl] = useState(getInitialImageUrl()); 171 | 172 | useEffect(() => { 173 | const gen = async () => { 174 | const token = propsAccessToken || (await directus?.auth.token); 175 | 176 | setImageUrl(generateImageUrl(token)); 177 | }; 178 | gen(); 179 | }, [directusContext, asset, propsApiUrl, propsAccessToken, download, directusTransform]); 180 | 181 | return render({ 182 | apiUrl, 183 | asset, 184 | url: imageUrl, 185 | download, 186 | directusTransform: directusTransform, 187 | }); 188 | }; 189 | -------------------------------------------------------------------------------- /src/components/DirectusImage.spec.tsx: -------------------------------------------------------------------------------- 1 | import { act, render } from '@testing-library/react'; 2 | import { DirectusImage, DirectusImageProps, RenderPropsImage } from '@components/DirectusImage'; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 5 | const dummyRenderer = jest.fn(arg => <>); 6 | 7 | beforeEach(() => { 8 | dummyRenderer.mockClear(); 9 | }); 10 | 11 | describe('Component', () => { 12 | it.each([ 13 | [{ apiUrl: 'http://example.com', asset: '11263b49-13b9-4ed8-ba39-01fe7cbeb284', width: 640, height: 480 }], 14 | [{ apiUrl: 'http://example.com', asset: { id: '1b86a776-34e5-4297-b8c5-58a85f15abe2' }, quality: 75 }], 15 | [{ apiUrl: 'http://example.com', asset: '11263b49-13b9-4ed8-ba39-01fe7cbeb284', fit: 'cover' }], 16 | ])('pass the props to the renderer', async props => { 17 | await act(async () => { 18 | render(); 19 | }); 20 | 21 | const rendererArg = dummyRenderer.mock.calls[0][0]; 22 | expect(rendererArg).toMatchObject(props); 23 | }); 24 | 25 | it('build and pass the url to the renderer', async () => { 26 | await act(async () => { 27 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 28 | render( 29 | 38 | ); 39 | }); 40 | 41 | const { url } = dummyRenderer.mock.calls[0][0]; 42 | expect(url).toContain('http://example.com/assets/8ac24997-cda5-4675-a1e0-1af72fddd220?'); 43 | expect(url).toContain('width=640'); 44 | expect(url).toContain('height=480'); 45 | expect(url).toContain('quality=75'); 46 | expect(url).toContain('fit=outside'); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/components/DirectusImage.tsx: -------------------------------------------------------------------------------- 1 | import { DirectusAssetObject, DirectusFile, Fit, Format, RenderPropsFile } from '@components/DirectusFile'; 2 | 3 | export interface RenderPropsImage extends Omit { 4 | url?: string; 5 | } 6 | 7 | export interface DirectusImageProps { 8 | apiUrl: string; 9 | asset: DirectusAssetObject; 10 | height?: number; 11 | width?: number; 12 | fit?: 'contain' | 'cover' | 'inside' | 'outside'; 13 | quality?: number; 14 | format?: 'auto' | 'jpg' | 'png' | 'webp' | 'tiff'; 15 | render: (props: RenderPropsImage) => JSX.Element; 16 | } 17 | 18 | /** 19 | * @deprecated Please import the new `DirectusFile` component instead. 20 | */ 21 | export const DirectusImage = ({ 22 | apiUrl, 23 | asset, 24 | render, 25 | height, 26 | width, 27 | fit, 28 | quality, 29 | format, 30 | }: DirectusImageProps): JSX.Element => { 31 | // Convert the the new props to the old props 32 | const renderOld = (props: RenderPropsFile): JSX.Element => { 33 | return render({ 34 | apiUrl, 35 | url: props.url, 36 | asset, 37 | width, 38 | height, 39 | fit, 40 | quality, 41 | format, 42 | }); 43 | }; 44 | 45 | return ( 46 | 58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /src/hooks/useDirectusAuth.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useContext, useMemo } from 'react'; 2 | import { DirectusContext } from '@/DirectusProvider'; 3 | import { UserType } from '@directus/sdk'; 4 | 5 | /** 6 | * Possible states of the authentication. 7 | * @defaultValue AuthStates.UNAUTHENTICATED 8 | * @defaultValue AuthStates.LOADING - When AutoLogin is enabled. 9 | */ 10 | export enum AuthStates { 11 | LOADING = 'loading', 12 | AUTHENTICATED = 'authenticated', 13 | UNAUTHENTICATED = 'unauthenticated', 14 | } 15 | 16 | /** 17 | * A set of functions and data to manage authentication. 18 | */ 19 | export interface DirectusAuthHook { 20 | /** 21 | * Login the user. If successful, the user will be stored in the context. 22 | * Else, an error will be thrown. 23 | * @param email - The user email. 24 | * @param password - The user password. 25 | * @throws {Error} - If the login fails. 26 | */ 27 | login: (email: string, password: string) => Promise; 28 | /** 29 | * Logout the user. If successful, the user will be removed from the context. 30 | * Else, an error will be thrown. 31 | * @throws {Error} - If the logout fails. 32 | */ 33 | logout: () => Promise; 34 | /** 35 | * Represents the current authentication state. 36 | * @defaultValue 'loading' 37 | */ 38 | authState: AuthStates; 39 | /** 40 | * The current authenticated user. 41 | * @defaultValue null 42 | */ 43 | user: UserType | null; 44 | } 45 | 46 | /** 47 | * A hook to access the Directus authentication state and methods. 48 | * 49 | * @example 50 | * ```tsx 51 | * import { useDirectusAuth } from 'react-directus'; 52 | * import { FormEvent } from 'react'; 53 | * 54 | * const Login = () => { 55 | * const { login } = useDirectusAuth(); 56 | * 57 | * const handleSubmit = (e: FormEvent) => { 58 | * e.preventDefault(); 59 | * 60 | * const { email, password } = e.currentTarget.elements; 61 | * login(email.value, password.value) 62 | * .catch((err) => { 63 | * console.error(err); 64 | * }); 65 | * }; 66 | * 67 | * return ( 68 | *
69 | * 70 | * 71 | * 72 | *
73 | * ); 74 | * }; 75 | * 76 | * export default Login; 77 | * ``` 78 | */ 79 | export const useDirectusAuth = (): DirectusAuthHook => { 80 | const directusContext = useContext(DirectusContext); 81 | 82 | if (!directusContext) { 83 | throw new Error('useDirectusAuth has to be used within the DirectusProvider'); 84 | } 85 | 86 | const { 87 | directus, 88 | _authState: authState, 89 | _setAuthState: setAuthState, 90 | _directusUser: directusUser, 91 | _setDirectusUser: setDirectusUser, 92 | } = directusContext; 93 | 94 | const login = useCallback( 95 | async (email: string, password: string) => { 96 | await directus.auth.login({ 97 | email, 98 | password, 99 | }); 100 | 101 | const dUser = (await directus.users.me.read({ 102 | fields: ['*'], 103 | })) as UserType; 104 | 105 | if (dUser) { 106 | setDirectusUser(dUser); 107 | setAuthState(AuthStates.AUTHENTICATED); 108 | } else { 109 | setDirectusUser(null); 110 | setAuthState(AuthStates.UNAUTHENTICATED); 111 | } 112 | }, 113 | [directus] 114 | ); 115 | 116 | const logout = useCallback(async () => { 117 | try { 118 | await directus.auth.logout(); 119 | } finally { 120 | setAuthState(AuthStates.UNAUTHENTICATED); 121 | setDirectusUser(null); 122 | } 123 | }, [directus]); 124 | 125 | const value = useMemo( 126 | () => ({ 127 | user: directusUser, 128 | authState, 129 | login, 130 | logout, 131 | }), 132 | [directus, directusUser, authState] 133 | ); 134 | 135 | return value; 136 | }; 137 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { DirectusProvider, useDirectus } from '@/DirectusProvider'; 2 | export { DirectusAsset } from '@components/DirectusAsset'; 3 | export { DirectusImage } from '@components/DirectusImage'; 4 | export { DirectusFile } from '@components/DirectusFile'; 5 | export { useDirectusAuth } from '@hooks/useDirectusAuth'; 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "target": "ES6", 5 | "module": "ESNext", 6 | "jsx": "react-jsx", 7 | "declaration": true, 8 | "declarationDir": "dist/types", 9 | "rootDir": "src", 10 | "strict": true, 11 | "moduleResolution": "node", 12 | "esModuleInterop": true, 13 | "noEmit": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "paths": { 16 | "@/*": ["*"], 17 | "@components/*": ["components/*"], 18 | "@hooks/*": ["hooks/*"] 19 | } 20 | } 21 | } 22 | --------------------------------------------------------------------------------