├── .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 |
3 |
4 |
5 |
6 | react-directus
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
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 |
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 | *
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 |
--------------------------------------------------------------------------------