{title}
-{body}
-{{ text }}
-Everything you need to start building. Provisioned in 90 seconds. Starting at $15/month.
- Get Started -{{ getPrompt() }}
-How helpful was this article?
-This article is
-{{ getRatingOption(feedback.rating)?.message }}
- - -Thanks for your feedback!
-- - -- - -
Get insights, releases, and updates delivered directly to your inbox once a month.
- -Project tutorials, tips & tricks, and best practices from the Directus team and community.
- -Published {{ getFriendlyDate(params.date_published) }}
- - -Our official guides to help you get started, integrate, and make the most of Directus.
-- This is an auto-generated document to support extension builders understand the internal packages they can - utilize. To find our written guides, tutorials, and API/SDK reference, check out our - main docs - . -
-| - Terminal 1 [Api] - | -- Terminal 2 [App] - | -
|---|---|
| - -```bash -pnpm --filter api dev -``` - - | -- -```bash -pnpm --filter app dev -``` - - | -
Name: {{ name }}
-Collection: {{ collection }}
-Content Here
-{% endblock %} -``` - -In this example, anything inside this content block will replace the content block in the base template. - -## Variables in Templates - -There are a few predefined variables available to email templates. They are: - -| Variable | Description | Default | -| -------------- | ----------- | ---------- | -| `projectName` | String | `Directus` | -| `projectColor` | Hex Color | `#546e7a` | -| `projectLogo` | Image URL | | -| `projectUrl` | URL | | - -Beyond this, you can inject whatever data you need. If you are using an extension, you can include information inside -the data section: - -```js -await mailService.send({ - to: 'name@example.com', - subject: 'This is an example email', - template: { - name: 'my-custom-email-template', - data: { - firstname: user.firstname, - }, - }, -}); -``` - -If you are using Flows, you can also inject data into emails: - - - -In your template, you can use the `firstname` variable like this: - -```liquid -{% layout "my-custom-base" %} -{% block content %} -Hi {{ firstname }},
-{% endblock %} -``` - -You may also provide a fallback if this variable is not provided. - -```liquid -{% layout "my-custom-base" %} -{% block content %} -Hi{% if firstname %}{{ firstname }}{% endif %},
-{% endblock %} -``` - -## Items and For Loops - -You can provide an array of data to a template and use a for loop to render the items. - -```liquid -{% layout "my-custom-base" %} -{% block content %} -| Subscription | -New Members | -
|---|---|
| {{ item.subscription.name | capitalize }} | -{{ item.count.customer_id }} | -
Collection: {{ collection }}
- -| Name | -- |
|---|
Collection: {{ collection }}
- -| Name | -- | |
|---|---|---|
| {{ item.name }} | -{{ item.email }} | -View Details | -
Lorem ipsum dolor sit amet
'; - break; - case 'hello-world': - page_title.value = 'Hello World'; - page_banner.value = '/assets/853B243D-A1BF-6051-B1BF-23EDA8E32A09?width=2000&height=563&fit=cover'; - page_cards.value = all_pages.value; - page_body.value = 'Lorem ipsum dolor sit amet
'; - break; - case 'contact': - page_title.value = 'Contact Us'; - page_banner.value = '/assets/91CE173D-A1AD-4104-A1EC-74FCB8F41B58?width=2000&height=563&fit=cover'; - page_cards.value = []; - page_body.value = 'Lorem ipsum dolor sit amet
'; - break; - default: - page_title.value = '404: Not Found'; -} -``` - -Or from the internal API providing you have a table with the fields `title`, `banner` (image field) and `content` -(WYSIWYG field): - -```js -api.get(`/items/pages?fields=title,banner,content&filter[uri][_eq]=${page}`).then((rsp) => { - if(rsp.data.data){ - rsp.data.data.forEach(item => { - page_title.value = item.title; - page_banner.value = `/assets/${item.banner}?width=2000&height=563&fit=cover`; - page_body.value = item.content; - }); - } else { - page_title.value = "404: Not Found"; - } -}).catch((error) => { - console.log(error); -}); -``` - -### Work With Images - -::: warning DEPRECATED - -Since [Directus version 10.10.0](/releases/breaking-changes.html#version-10-10-0) the query parameter authentication is -no longer required and considered deprecated, you can rely on -[session cookies](/reference/authentication.html#access-tokens) instead. - -::: - -To use internal images, an access token needs to be included in the request. Create a new file called -`use-directus-token.js` and copy the following code: - -```js -export default function useDirectusToken(directusApi) { - return { - addQueryToPath, - getToken, - addTokenToURL, - }; - - function addQueryToPath(path, query) { - const queryParams = []; - - for (const [key, value] of Object.entries(query)) { - queryParams.push(`${key}=${value}`); - } - - return path.includes('?') ? `${path}&${queryParams.join('&')}` : `${path}?${queryParams.join('&')}`; - } - - function getToken() { - return ( - directusApi.defaults?.headers?.['Authorization']?.split(' ')[1] || - directusApi.defaults?.headers?.common?.['Authorization']?.split(' ')[1] || - null - ); - } - - function addTokenToURL(url) { - const accessToken = getToken(); - if (!accessToken) return url; - return addQueryToPath(url, { - access_token: accessToken, - }); - } -}; -``` - -This will use the access token of the current user to render the images. Alternatively, you can enable Read permissions -on the Public role for the image ID or images with a specific folder ID to remove the need for an access token. - -Import the function into the `module.vue` file to make it available in your script: - -```js -import useDirectusToken from './use-directus-token'; -``` - -Include the function `AddTokenToURL` as a variable from the new script. - -```js -setup(props) { - const router = useRouter(); - const api = useApi(); - const { addTokenToURL } = useDirectusToken(api); - - // Existing code -} -``` - -Then wrap any internal images with this function: - -```js -page_banner.value = addTokenToURL(`/assets/${item.banner}?width=2000&height=563&fit=cover`); -``` - -:::info External Images - -If you are using images from external sources, the host must be added to the Content Security Policy (CSP) inside the -environment or config file. - -::: - -## Style the Module - -Add some SCSS at the bottom of the `module.vue` file. When dealing with multiple vue files, don’t scope the SCSS, -instead prefix each class with a unique reference to prevent changing other components in Directus. In this example, use -the following SCSS: - -```vue - -``` - -This will format the banner, cards and the container. It’s a good idea to make use of the native CSS of Directus as much -as possible so your module appears part of Directus. - -Now the page will look like this: - - - -Our files are now complete. Build the module with the latest changes: - -```shell -npm run build -``` - -## Add Module to Directus - -When Directus starts, it will look in the `extensions` directory for any subdirectory starting with -`directus-extension-`, and attempt to load them. - -To install an extension, copy the entire directory with all source code, the `package.json` file, and the `dist` -directory into the Directus `extensions` directory. Make sure the directory with your extension has a name that starts -with `directus-extension`. In this case, you may choose to use `directus-extension-module-landing-page`. - -Restart Directus to load the extension. - -:::info Required files - -Only the `package.json` and `dist` directory are required inside of your extension directory. However, adding the source -code has no negative effect. - -::: - -## Use the Module - -To use your new module in Directus, you need to enable it in the -[Project Settings](/user-guide/settings/project-settings#modules). - -## Summary - -You have created a new module from the extension SDK boilerplate template and extended it to multiple pages that make -use of the `vue-router` and utilize the left navigation panel. You can also use the internal API to fetch content and -images from within Directus to surface on the page. From here you can create content rich modules driven by the features -of the Directus platform. - -::: code-group - -```js [index.js] -import ModuleComponent from './module.vue'; - -export default { - id: 'landing-page', - name: 'Landing Page', - icon: 'rocket_launch', - routes: [ - { - name: 'home', - path: '', - props: true, - component: ModuleComponent, - }, - { - name: 'page', - path: ':page', - props: true, - component: ModuleComponent, - }, - ], -}; -``` - -```vue [module.vue] - -
-
-`options` are the fields presented in the frontend when adding this operation to the Flow. To add a comment, you will
-need to know the collection, item id and the comment itself. Replace the placeholder options with the following:
-
-```js
-options: [
- {
- field: 'collection',
- name: '$t:collection',
- type: 'string',
- meta: {
- width: 'half',
- interface: 'system-collection',
- },
- },
- {
- field: 'comment_key',
- name: 'ID',
- type: 'string',
- meta: {
- width: 'half',
- interface: 'tags',
- options: {
- iconRight: 'vpn_key',
- },
- },
- },
- {
- field: 'comment',
- name: 'Comment',
- type: 'text',
- meta: {
- width: 'full',
- interface: 'input-multiline',
- },
- },
-],
-```
-
-- `collection` - the interface system-collection which renders a searchable dropdown for all collections. This field
- will also accept a manually entered string or a variable from the Flow.
-- `comment_key` - field where the ID of the item is entered. This will accept a string, guid, int or a variable from the
- Flow. This must be an existing record in Directus.
-- `comment` - a simple input-multiline field (textarea) to match how comments are made. Variables from the Flow can be
- mixed with standard text.
-
-
-
-The `overview` section defines what is visible inside the operation’s card on the Flow canvas. An overview object
-contains 2 parameters, `label` and `text`. The label can be any string and does not need to match the field name. The
-text parameter can be a variable or just another string.
-
-It will be useful to see the collection and the comment on the card. To do this you must include the fields value from
-the options (`collection` and `comment`) as properties. Replace the placeholder objects with the following:
-
-```js
-overview: ({ collection, comment }) => [
- {
- label: '$t:collection',
- text: collection,
- },
- {
- label: 'Comment',
- text: comment,
- },
-],
-```
-
-- `collection` will use the system’s label for a collection and the text will be the chosen collection from the user.
-- `comment` uses a string for the label, and the text will be the contents of the comment textarea field from the user.
-
-
-
-## Build the API Function
-
-Open the `api.js` file and update the `id` to match the one used in the `app.js` file.
-
-The `handler` needs to include the values from the options and some key services to create a comment. Replace the
-handler definition with the following:
-
-```js
-handler: async ({ collection, comment_key, comment }, { services, database, accountability, getSchema }) => {
-```
-
-Notice the fields are added in the first object, then the services in the second.
-
-Comments are stored inside the `directus_activity` table so the `ActivityService` is required to perform the action.
-Inside the handler, set the following constants:
-
-```js
-const { ActivityService } = services;
-const schema = await getSchema({ database });
-
-const activityService = new ActivityService({
- schema: schema,
- accountability: accountability,
- knex: database,
-});
-```
-
-The id field called `comment_key` needs to be able to accept both a single ID or multiple IDs. Add the following to
-convert a single ID to an array then add them to the `keys` constant. Note the use of JSON.parse to allow JSON entry
-into the field.
-
-```js
-if (!Array.isArray(comment_key) && comment_key.includes('[') === false) {
- comment_key = [comment_key];
-}
-
-const keys = Array.isArray(comment_key) ? comment_key : JSON.parse(Array.isArray(comment_key));
-```
-
-The final part of the script is to loop through all the keys and write the comment to them. Also it will be useful to
-write the outcome back to the Flow’s logs. This is done by return as response to the function.
-
-Create some disposable variables results and activity, then write the response from `activtyService.createOne` to the
-activity variable and append it to the results array.
-
-```js
-let results = [];
-let activity = null;
-
-for await (const key of keys) {
- try {
- activity = await activityService.createOne({
- action: 'comment',
- comment: comment,
- user: accountability?.user ?? null,
- collection: collection,
- ip: accountability?.ip ?? null,
- user_agent: accountability?.userAgent ?? null,
- origin: accountability?.origin ?? null,
- item: key,
- });
- results.push(activity);
- } catch (error) {
- return error;
- }
-};
-return results;
-```
-
-The function `activtyService.createOne` requires the above parameters. For the comment to work, the `action` must be
-`'comment'`. Then update the comment, collection and item parameters with the fields from `app.js`. The exception being
-item which uses the key from the loop.
-
-`user`, `ip`, `user_agent`, and `origin` all come from the `accountability` service. This means the comment will be left
-by the user that triggers the workflow. At the end, the results array is returned and will be visible in the Flow logs.
-
-Here is an example of how the comment will appear on a record:
-
-
-
-Build the operation with the latest changes.
-
-```
-npm run build
-```
-
-## Add Operation to Directus
-
-When Directus starts, it will look in the `extensions` directory for any subdirectory starting with
-`directus-extension-`, and attempt to load them.
-
-To install an extension, copy the entire directory with all source code, the `package.json` file, and the `dist`
-directory into the Directus `extensions` directory. Make sure the directory with your extension has a name that starts
-with `directus-extension`. In this case, you may choose to use `directus-extension-operation-add-comment`.
-
-Restart Directus to load the extension.
-
-:::info Required files
-
-Only the `package.json` and `dist` directory are required inside of your extension directory. However, adding the source
-code has no negative effect.
-
-:::
-
-## Use the Operation
-
-In the Directus Data Studio, open the Flows section in Settings. Create a new flow with an event trigger such as
-`item.update`. Select the collection(s) to use.
-
-Add a new step (operation) in the flow by clicking the tick/plus on the card. This can be at any point in the workflow.
-From the list of options, choose **Add Comment**.
-
-
-
-Save the operation, save the Flow, and then trigger it by updating a record from the chosen collections. Open the
-collection records again to see the new comment. To see the response from the API function, open the flow and check out
-the logs in the right side toolbar.
-
-## Summary
-
-With this operation, a comment will be created with the pre-configured settings on all submitted keys and respond with
-an array of activity IDs for the comments. This operation requires setting up fields on the front-end and using Directus
-services on the API side to complete the transaction. Now that you know how to interact with these services, you can
-investigate other ways to extend your operations.
-
-## Complete Code
-
-`app.js`
-
-```js
-export default {
- id: 'your-extension-id',
- name: 'Add Comment',
- icon: 'chat',
- description: 'Add a comment to a record',
- overview: ({ collection, comment }) => [
- {
- label: '$t:collection',
- text: collection,
- },
- {
- label: 'Comment',
- text: comment,
- },
- ],
- options: [
- {
- field: 'collection',
- name: '$t:collection',
- type: 'string',
- meta: {
- width: 'half',
- interface: 'system-collection',
- },
- },
- {
- field: 'permissions',
- name: '$t:permissions',
- type: 'string',
- schema: {
- default_value: '$trigger',
- },
- meta: {
- width: 'half',
- interface: 'select-dropdown',
- options: {
- choices: [
- {
- text: 'From Trigger',
- value: '$trigger',
- },
- {
- text: 'Public Role',
- value: '$public',
- },
- {
- text: 'Full Access',
- value: '$full',
- },
- ],
- allowOther: true,
- },
- },
- },
- {
- field: 'comment_key',
- name: 'ID',
- type: 'string',
- meta: {
- width: 'half',
- interface: 'tags',
- options: {
- iconRight: 'vpn_key',
- },
- },
- },
- {
- field: 'comment',
- name: 'Comment',
- type: 'text',
- meta: {
- width: 'full',
- interface: 'input-multiline',
- },
- },
- ],
-};
-```
-
-`api.js`
-
-```js
-export default {
- id: 'your-extension-id',
- handler: async ({ collection, comment_key, comment }, { services, database, accountability, getSchema }) => {
- const { ActivityService } = services;
- const schema = await getSchema({ database });
-
- const activityService = new ActivityService({
- schema: schema,
- accountability: accountability,
- knex: database,
- });
-
- if (!Array.isArray(comment_key) && comment_key.includes('[') === false) {
- comment_key = [comment_key];
- }
-
- const keys = Array.isArray(comment_key) ? comment_key : JSON.parse(Array.isArray(comment_key));
-
- console.log(`Converted ${keys}`);
-
- let results = [];
- let activity = null;
-
- for await (const key of keys) {
- try {
- activity = await activityService.createOne({
- action: 'comment',
- comment: comment,
- user: accountability?.user ?? null,
- collection: collection,
- ip: accountability?.ip ?? null,
- user_agent: accountability?.userAgent ?? null,
- origin: accountability?.origin ?? null,
- item: key,
- });
-
- results.push(activity);
- } catch (error) {
- return error;
- }
- }
-
- return results;
- },
-};
-```
diff --git a/docs/guides/extensions/operations-bulk-email-sendgrid.md b/docs/guides/extensions/operations-bulk-email-sendgrid.md
deleted file mode 100644
index badd66f78c..0000000000
--- a/docs/guides/extensions/operations-bulk-email-sendgrid.md
+++ /dev/null
@@ -1,509 +0,0 @@
----
-description: Learn how to create an operation to send bulk email with SendGrid's Dynamic Templates.
-contributors: Tim Butterfield, Kevin Lewis
----
-
-# Use Custom Operations to Send Bulk Email With SendGrid
-
-Operations allow you to trigger your own code in a Flow. This guide will show you how to use the SendGrid SDK to bulk
-send emails as an operation in Flows.
-
-
-
-## Install Dependencies
-
-To follow this guide, you will need a SendGrid API Key and access to SendGrid Dynamic Templates.
-
-Open a console to your preferred working directory and initialize a new extension, which will create the boilerplate
-code for your operation.
-
-```shell
-npx create-directus-extension@latest
-```
-
-A list of options will appear (choose operation), and type a name for your extension (for example,
-`directus-operation-bulk-sendgrid`). For this guide, select JavaScript.
-
-Now the boilerplate has been created, install the `@sendgrid/mail` package, and then open the directory in your code
-editor.
-
-```shell
-cd directus-operation-bulk-sendgrid
-npm install @sendgrid/mail
-```
-
-## Build the Operation UI
-
-Operations have 2 parts - the `api.js` file that performs logic, and the `app.js` file that describes the front-end UI
-for the operation.
-
-Open `app.js` and change the `id`, `name`, `icon`, and `description`.
-
-```js
-id: 'operation-bulk-sendgrid',
-name: 'SendGrid Bulk Email',
-icon: 'mail',
-description: 'Send bulk emails using SendGrid API.',
-```
-
-Make sure the `id` is unique between all extensions including ones created by 3rd parties - a good practice is to
-include a professional prefix. You can choose an icon from the library [here](https://fonts.google.com/icons).
-
-With the information above, the operation will appear in the list like this:
-
-
-
-`options` are the fields presented in the frontend when adding this operation to the Flow. To send an email with
-SendGrid, you will need the sending address, a recipient address, a subject and the template id. Replace the placeholder
-options with the following:
-
-```js
-options: [
- {
- field: 'from',
- name: 'From Address',
- type: 'string',
- meta: {
- width: 'half',
- interface: 'input',
- },
- },
- {
- field: 'recipients',
- name: 'Recipients Object',
- type: 'json',
- meta: {
- width: 'half',
- interface: 'input',
- },
- },
- {
- field: 'email_key',
- name: 'Recipients Email Key',
- type: 'string',
- meta: {
- width: 'half',
- interface: 'input',
- },
- },
- {
- field: 'subject',
- name: 'Subject',
- type: 'string',
- meta: {
- width: 'half',
- interface: 'input',
- },
- },
- {
- field: 'template_data',
- name: 'Dynamic Template Data',
- type: 'json',
- meta: {
- width: 'full',
- interface: 'list',
- options: {
- template: '{{var}}: {{key}}',
- fields: [
- {
- field: 'var',
- name: 'Email Variable',
- type: 'string',
- meta: {
- width: 'half',
- interface: 'input',
- options: {
- font: 'monospace',
- },
- },
- },
- {
- field: 'key',
- name: 'Recipient Object Key',
- type: 'string',
- meta: {
- width: 'half',
- interface: 'input',
- options: {
- font: 'monospace',
- },
- },
- },
- ],
- },
- },
- },
- {
- field: 'template_id',
- name: 'SendGrid Template ID',
- type: 'string',
- meta: {
- width: 'full',
- interface: 'input',
- },
- },
-],
-```
-
-- `from` and `subject` interfaces use an input interface for a simple string.
-- `recipients` is set to JSON so it can receive all the recipient’s data such as email address, name, job title or
- anything else that is needed for the email.
-- `email` is required for fetching the email address from the recipient object.
-- `template_data` uses a list interface (also known as a repeater interface) where the user can specify what variables
- are within the email and what to replace them with. The var value needs to be the object key relative to a single
- recipient object. For example, `first_name` or `building.name`.
-- `template_id` field uses a standard input field for the ID from SendGrid. This can be a static ID or dynamically added
- through the workflow.
-
-
-
-The `overview` section defines what is visible inside the operation’s card on the Flow canvas. An overview object
-contains 2 parameters, `label` and `text`. The label can be any string and does not need to match the field name. The
-text parameter can be a variable or just another string.
-
-It will be useful to see the subject and the from address on the card. To do this you must include the fields value from
-the options (`subject` and `from`) as properties. Replace the placeholder objects with the following:
-
-```js
-overview: ({ subject, from }) => [
- {
- label: 'Subject',
- text: subject,
- },
- {
- label: 'From',
- text: from,
- },
-],
-```
-
-Now, the overview of the operation looks like this:
-
-
-
-## Build the API Function
-
-Open the `api.js` file, update the `id` to match the one used in the `app.js` file, and import the SendGrid package at
-the very top:
-
-```js
-import sgMail from '@sendgrid/mail'
-```
-
-The handler needs to include the fields from the `app.js` options and the environment variables from Directus. Replace
-the handler definition with the following:
-
-```js
-handler: ({ from, recipients, substitutions, subject, template_id }, { env }
-) => {
-```
-
-Set up the SendGrid API and message object with the following code:
-
-```js
-sgMail.setApiKey(env.SENDGRID_API_KEY);
-
-let msg = {
- from: { email: from },
- personalizations: [],
- template_id: template_id,
-};
-```
-
-Read more about the SendGrid Dynamic Template API request in the
-[SendGrid documentation](https://docs.sendgrid.com/ui/sending-email/how-to-send-an-email-with-dynamic-templates).
-
-The `recipients` need to be added to the personalization list. Each item needs to follow this structure:
-
-```json
-{
- "to": [{ "email": "example@sendgrid.net" }],
- "dynamic_template_data": {
- "key": "value"
- }
-}
-```
-
-_Note: do not add multiple email addresses inside the `to` object. Instead, each recipient must have their own
-personalization object where you can include dynamic template data relevant to that person._
-
-Convert both the recipients and dynamic variables to objects if they are submitted as JSON, and create a function to
-parse values from the recipients and return the final value:
-
-```js
-const rec = Array.isArray(recipients) ? recipients : JSON.parse(recipients);
-const dyn = Array.isArray(template_data) ? template_data : JSON.parse(template_data);
-
-function parseValues(recipient, key) {
- if (key.includes('.')) {
- let value = recipient;
- let fields = key.split('.');
-
- fields.forEach((f) => {
- if (value != null) value = value[f];
- });
-
- return value;
- } else {
- return recipient[key];
- }
-}
-```
-
-For each recipient, build the personalization object and add it to the `msg` variable. This will include the subject as
-a dynamic variable. To use this, make sure to change the subject in SendGrid to `{{{subject}}}` to
-receive this value.
-
-```js
-rec.forEach((recipient) => {
- let email_address = parseValues(recipient, email_key);
-
- let personalization = {
- to: [{ email: email_address }],
- dynamic_template_data: {
- subject: subject,
- },
- };
-
- dyn.forEach((s) => {
- personalization.dynamic_template_data[s.var] = parseValues(recipient, s.key);
- });
-
- msg.personalizations.push(personalization);
-});
-```
-
-The `msg` variable is now ready to be sent to SendGrid. Use the following to send the data and return the response to
-the workflow:
-
-```js
-return sgMail.send(msg);
-```
-
-Both files are now complete. Build the operation with the latest changes.
-
-```
-npm run build
-```
-
-## Add Operation to Directus
-
-When Directus starts, it will look in the `extensions` directory for any subdirectory starting with
-`directus-extension-`, and attempt to load them.
-
-To install an extension, copy the entire directory with all source code, the `package.json` file, and the `dist`
-directory into the Directus `extensions` directory. Make sure the directory with your extension has a name that starts
-with `directus-extension`. In this case, you may choose to use `directus-extension-operation-bulk-sendgrid`.
-
-Ensure the `.env` file has `SENDGRID_API_KEY` variable.
-
-Restart Directus to load the extension.
-
-:::info Required files
-
-Only the `package.json` and `dist` directory are required inside of your extension directory. However, adding the source
-code has no negative effect.
-
-:::
-
-## Use the Operation
-
-In the Directus Data Studio, open the Flows section in Settings. Create a new flow with a manual trigger. Select the
-collection(s) to include this button on.
-
-Fetch some recipients by adding a **Read Data** operation and include the required fields and filters. Add a new step
-(operation) after the Data Read operation by clicking the tick/plus on the card, then choose **SendGrid Bulk Email**
-from the list.
-
-
-
-- For the **From Address**, type `no-reply@company.com`. This must be the domain registered with SendGrid
-- For the **Recipients Object**, use a previous flow operation such as Read Data to fetch all the required recipients as
- an array of objects. Make sure to include fields that will be used in the email. In this example, you will use
- `{{$last}}`.
-- For the **Recipients Email Key**, type the field name that contains the recipient's email address and make sure it’s
- included in the Recipients Object.
-- For the **Subject**, add a catchy subject for the email. If you want to personalize the subject, this is best done on
- the template with SendGrid.
-- To add **Dynamic Template Data**, click Create New and set the **Email Variable** to the key used in the template and
- the **Recipient Object Key** to the field from the recipient object. In this example, use their first name from a
- field called `first_name` and use it to replace `{{{name}}}` in the SendGrid template.
-- The `SendGrid Template ID` must be supplied. This can be added as a static string or dynamically added from the Flow.
-
-Save the operation, save the Flow, and then trigger the flow by opening the chosen collection, then trigger the manual
-flow from the right side toolbar.
-
-## Summary
-
-This operation will send an SendGrid API request to use an email template inside SendGrid along with the supplied
-personal information to bulk send an email to the recipients. The response is captured in the logs for reference. Now
-that you know how to interact with a third party API, you can investigate other services that can be used in your
-workflows.
-
-## Complete Code
-
-`app.js`
-
-```js
-export default {
- id: 'operation-bulk-sendgrid',
- name: 'Sendgrid Bulk Email',
- icon: 'mail',
- description: 'Send bulk emails using SendGrid API.',
- overview: ({ subject, from }) => [
- {
- label: 'Subject',
- text: subject,
- },
- {
- label: 'From',
- text: from,
- },
- ],
- options: [
- {
- field: 'from',
- name: 'From Address',
- type: 'string',
- meta: {
- width: 'half',
- interface: 'input',
- },
- },
- {
- field: 'recipients',
- name: 'Recipients Object',
- type: 'json',
- meta: {
- width: 'half',
- interface: 'input',
- },
- },
- {
- field: 'email_key',
- name: 'Recipent Email Key',
- type: 'string',
- meta: {
- width: 'half',
- interface: 'input',
- },
- },
- {
- field: 'subject',
- name: 'Subject',
- type: 'string',
- meta: {
- width: 'half',
- interface: 'input',
- },
- },
- {
- field: 'template_data',
- name: 'Dynamic Template Data',
- type: 'json',
- meta: {
- width: 'full',
- interface: 'list',
- options: {
- template: '{{var}}: {{key}}',
- fields: [
- {
- field: 'var',
- name: 'Email Variable',
- type: 'string',
- meta: {
- width: 'half',
- interface: 'input',
- options: {
- font: 'monospace',
- },
- },
- },
- {
- field: 'key',
- name: 'Recipient Object Key',
- type: 'string',
- meta: {
- width: 'half',
- interface: 'input',
- options: {
- font: 'monospace',
- },
- },
- },
- ],
- },
- },
- },
- {
- field: 'template_id',
- name: 'SendGrid Template ID',
- type: 'text',
- meta: {
- width: 'full',
- interface: 'input',
- },
- },
- ],
-};
-```
-
-`api.js`
-
-```js
-import sgMail from '@sendgrid/mail';
-
-export default {
- id: 'operation-bulk-sendgrid',
- handler: ({ from, email_key, recipients, template_data, subject, template_id }, { env }) => {
- sgMail.setApiKey(env.SENDGRID_API_KEY);
-
- let msg = {
- from: {
- email: from,
- },
- personalizations: [],
- template_id: template_id,
- };
-
- const rec = Array.isArray(recipients) ? recipients : JSON.parse(recipients);
- const dyn = Array.isArray(template_data) ? template_data : JSON.parse(template_data);
-
- function parseValues(recipient, key) {
- if (key.includes('.')) {
- let value = recipient;
- let fields = key.split('.');
-
- fields.forEach((f) => {
- if (value != null) {
- value = value[f];
- }
- });
-
- return value;
- } else {
- return recipient[key];
- }
- }
-
- rec.forEach((recipient) => {
- let email_address = parseValues(recipient, email_key);
-
- let personalization = {
- to: [{ email: email_address }],
- dynamic_template_data: {
- subject: subject,
- },
- };
-
- dyn.forEach((s) => {
- personalization.dynamic_template_data[s.var] = parseValues(recipient, s.key);
- });
-
- msg.personalizations.push(personalization);
- });
-
- return sgMail.send(msg);
- },
-};
-```
diff --git a/docs/guides/extensions/operations-npm-package.md b/docs/guides/extensions/operations-npm-package.md
deleted file mode 100644
index 2452a2e395..0000000000
--- a/docs/guides/extensions/operations-npm-package.md
+++ /dev/null
@@ -1,176 +0,0 @@
----
-description: Learn how to expose any package on NPM as a custom operation.
-contributors: Nils Twelker, Kevin Lewis
----
-
-# Exposing an NPM Package as a Custom Operation
-
-This guide will show you how to expose an NPM package as a custom operation in Flows. We will use `lodash` here, but the
-process should be the same for any package.
-
-## Install Dependencies
-
-Open a console to your preferred working directory and initialize a new extension, which will create the boilerplate
-code for your operation.
-
-```shell
-npx create-directus-extension@latest
-```
-
-A list of options will appear (choose operation), and type a name for your extension (for example,
-`directus-operation-lodash`). For this guide, select JavaScript.
-
-Now the boilerplate has been created, install the lodash package, and then open the directory in your code editor.
-
-```shell
-cd directus-operation-lodash
-npm install lodash
-```
-
-## Build the Operation UI
-
-Operations have 2 parts - the `api.js` file that performs logic, and the `app.js` file that describes the front-end UI
-for the operation.
-
-Open `app.js` and change the `id`, `name`, `icon`, and `description`.
-
-```js
-id: 'operation-lodash-camelcase',
-name: 'Lodash Camel Case',
-icon: 'electric_bolt',
-description: 'Use Lodash Camel Case Function.',
-```
-
-Make sure the `id` is unique between all extensions including ones created by 3rd parties - a good practice is to
-include a professional prefix. You can choose an icon from the library [here](https://fonts.google.com/icons).
-
-With the information above, the operation will appear in the list like this:
-
-
-
-### Accepting Inputs
-
-The default `options` object in `app.js` has a single text interface called `text`:
-
-```js
-options: [
- {
- field: 'text',
- name: 'Text',
- type: 'string',
- meta: {
- width: 'full',
- interface: 'input',
- },
- },
-],
-```
-
-If your NPM package function requires multiple inputs, you can add them here. The `overview` array defines what is shown
-on the card when the operation is not selected.
-
-## Build the API Function
-
-Open the `api.js` file and update the `id` to match the one used in the `app.js` file. Import from your NPM package,
-execute your logic, and finish by return any data from the operation into the data chain.
-
-```js
-import { defineOperationApi } from '@directus/extensions-sdk';
-import { camelCase } from 'lodash'; // [!code ++]
-
-export default defineOperationApi({
- id: 'operation-lodash-camelcase',
- handler: ({ text }) => {
- console.log(text); // [!code --]
- return { // [!code ++]
- text: camelCase(text) // [!code ++]
- }; // [!code ++]
- },
-});
-```
-
-Both files are now complete. Build the operation with the latest changes.
-
-```
-npm run build
-```
-
-## Add Operation to Directus
-
-When Directus starts, it will look in the `extensions` directory for any subdirectory starting with
-`directus-extension-`, and attempt to load them.
-
-To install an extension, copy the entire directory with all source code, the `package.json` file, and the `dist`
-directory into the Directus `extensions` directory. Make sure the directory with your extension has a name that starts
-with `directus-extension`. In this case, you may choose to use `directus-extension-operation-lodash`.
-
-Restart Directus to load the extension.
-
-:::info Required files
-
-Only the `package.json` and `dist` directory are required inside of your extension directory. However, adding the source
-code has no negative effect.
-
-:::
-
-## Use the Operation
-
-In the Directus Data Studio, open the Flows section in Settings. Create a new flow with a manual trigger. Select the
-collection(s) to include this button on.
-
-Add a new step (operation) by clicking the tick/plus on the card, then choose **Lodash Camel Case** from the list. In
-the text box, you can use any of the values from the trigger, or a hardcoded string.
-
-Save the operation, save the Flow, and then trigger the flow by opening the chosen collection, then trigger the manual
-flow from the right side toolbar.
-
-## Summary
-
-This operation takes an NPM package (`lodash`) and exposes it as a custom operation extension. You can use the same
-technique for other packages to extend on the features of Directus Flows.
-
-## Complete Code
-
-`app.js`
-
-```js
-export default {
- id: 'operation-lodash-camelcase',
- name: 'Lodash Camel Case',
- icon: 'electric_bolt',
- description: 'Use Lodash Camel Case Function.',
- overview: ({ text }) => [
- {
- label: 'Text',
- text: text,
- },
- ],
- options: [
- {
- field: 'text',
- name: 'Text',
- type: 'string',
- meta: {
- width: 'full',
- interface: 'input',
- },
- },
- ],
-};
-```
-
-`api.js`
-
-```js
-import { defineOperationApi } from '@directus/extensions-sdk';
-import { camelCase } from 'lodash';
-
-export default defineOperationApi({
- id: 'operation-lodash-camelcase',
- handler: ({ text }) => {
- return {
- text: camelCase(text),
- };
- },
-});
-```
diff --git a/docs/guides/extensions/operations-send-sms-twilio.md b/docs/guides/extensions/operations-send-sms-twilio.md
deleted file mode 100644
index 945d2ce2af..0000000000
--- a/docs/guides/extensions/operations-send-sms-twilio.md
+++ /dev/null
@@ -1,282 +0,0 @@
----
-description: Learn how to create an operation to send an SMS using Twilio.
-contributors: Tim Butterfield, Kevin Lewis
----
-
-# Use Custom Operations to Send SMS Notifications With Twilio
-
-Operations allow you to trigger your own code in a Flow. This guide will show you how to use the Twilio Node.js helper
-library to send SMS messages in Flows.
-
-
-
-## Install Dependencies
-
-To follow this guide, you will need a Twilio API Key.
-
-Open a console to your preferred working directory and initialize a new extension, which will create the boilerplate
-code for your operation.
-
-```shell
-npx create-directus-extension@latest
-```
-
-A list of options will appear (choose operation), and type a name for your extension (for example,
-`directus-operation-twilio-sms`). For this guide, select JavaScript.
-
-Now the boilerplate has been created, install the Twilio library, and then open the directory in your code editor.
-
-```shell
-cd directus-operation-twilio-sms
-npm install twilio
-```
-
-## Build the Operation UI
-
-Operations have 2 parts - the `api.js` file that performs logic, and the `app.js` file that describes the front-end UI
-for the operation.
-
-Open `app.js` and change the `id`, `name`, `icon`, and `description`.
-
-```js
-id: 'operation-twilio-sms',
-name: 'Twilio SMS',
-icon: 'forum',
-description: 'Send SMS using the Twilio API.',
-```
-
-Make sure the `id` is unique between all extensions including ones created by 3rd parties - a good practice is to
-include a professional prefix. You can choose an icon from the library [here](https://fonts.google.com/icons).
-
-With the information above, the operation will appear in the list like this:
-
-
-
-`options` are the fields presented in the frontend when adding this operation to the Flow. To send an SMS, you will need
-the phone number and a message. Replace the placeholder options with the following:
-
-```js
-options: [
- {
- field: 'phone_number',
- name: 'Phone Number',
- type: 'string',
- meta: {
- width: 'half',
- interface: 'input',
- },
- },
- {
- field: 'message',
- name: 'Message',
- type: 'text',
- meta: {
- width: 'full',
- interface: 'input-multiline',
- },
- },
-],
-```
-
-- `phone_number` is a standard string input to allow for international numbers that begin with a plus (+).
-- `message` uses an input-multiline field (textarea) to allow for a long message to be sent.
-
-
-
-The `overview` section defines what is visible inside the operation’s card on the Flow canvas. An overview object
-contains 2 parameters, `label` and `text`. The label can be any string and does not need to match the field name. The
-text parameter can be a variable or just another string.
-
-It will be useful to see both fields on the card. Replace the placeholder objects with the following:
-
-```js
-overview: ({ phone_number, message }) => [
- {
- label: 'Phone Number',
- text: phone_number,
- },
- {
- label: 'Message',
- text: message,
- },
-],
-```
-
-Now, the overview of the operation looks like this:
-
-
-
-## Build the API Function
-
-Open the `api.js` file, import the Twilio library and update the `id` to match the one used in the `app.js` file:
-
-```js
-import twilio from 'twilio';
-
-export default {
- id: 'operation-twilio-sms',
- handler: () => {
- // ...
- },
-};
-```
-
-The handler needs to include the fields from the `app.js` options and the environment variables from Directus. Replace
-the handler definition with the following:
-
-```js
-handler: ({ phone_number: toNumber, message }, { env }) => {
-```
-
-Set up the Twilio API and environment variables with the following code. These environment variables will need to be
-added to the project when installing this extension.
-
-```js
-const accountSid = env.TWILIO_ACCOUNT_SID;
-const authToken = env.TWILIO_AUTH_TOKEN;
-const fromNumber = env.TWILIO_PHONE_NUMBER;
-const client = new twilio(accountSid, authToken);
-```
-
-Use the Twilio `messages` endpoint and create a new message, setting the `body`, `to`, and `from` parameters. `body`
-will use the message variable from our handler, `to` will use the `phone_number` variable from our handler, aliased as
-`toNumber` for clarity, and `from` will use the `fromNumber` constant from the environment variable
-`TWILIO_PHONE_NUMBER`.
-
-```js
-client.messages
- .create({
- body: message,
- to: toNumber,
- from: fromNumber,
- })
- .then((response) => {
- return response;
- })
- .catch((error) => {
- return error;
- });
-```
-
-Make sure the return the `response` and `error` so they can be included in the Flow’s log.
-
-Both files are now complete. Build the operation with the latest changes.
-
-```
-npm run build
-```
-
-## Add Operation to Directus
-
-When Directus starts, it will look in the `extensions` directory for any subdirectory starting with
-`directus-extension-`, and attempt to load them.
-
-To install an extension, copy the entire directory with all source code, the `package.json` file, and the `dist`
-directory into the Directus `extensions` directory. Make sure the directory with your extension has a name that starts
-with `directus-extension`. In this case, you may choose to use `directus-extension-operation-twilio-sms`.
-
-Ensure the `.env` file has `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN`, and `TWILIO_PHONE_NUMBER` variables.
-
-Restart Directus to load the extension.
-
-:::info Required files
-
-Only the `package.json` and `dist` directory are required inside of your extension directory. However, adding the source
-code has no negative effect.
-
-:::
-
-## Use the Operation
-
-In the Directus Data Studio, open the Flows section in Settings. Create a new flow with an event trigger. Select the
-collection(s) to include.
-
-If the payload does not contain the phone number, use the **Read Data** operation to fetch the phone number from the
-relevant collection. Add a new operation by clicking the tick/plus on the card, then choose **Twilio SMS** from the
-list.
-
-
-
-- For the **Phone Number**, you can use a dynamic value from a payload such as
- `{{$trigger.payload.phone_number}}` or type a static number in the field.
-- For the **Message**, type anything that you would like to send and remember to shorten your links.
-
-Save the operation, save the Flow, and then trigger the flow by creating a record in the chosen collection.
-
-## Summary
-
-This operation will create a Twilio API request to send an SMS using the supplied number and message and the response is
-captured in the logs for reference. Now that you know how to interact with a third party API, you can investigate other
-services that can be used in your workflows.
-
-## Complete Code
-
-`app.js`
-
-```js
-export default {
- id: 'operation-twilio-sms',
- name: 'Twilio SMS',
- icon: 'forum',
- description: 'Send SMS using the Twilio API.',
- overview: ({ phone_number, message }) => [
- {
- label: 'Phone Number',
- text: phone_number,
- },
- {
- label: 'Message',
- text: message,
- },
- ],
- options: [
- {
- field: 'phone_number',
- name: 'Phone Number',
- type: 'string',
- meta: {
- width: 'full',
- interface: 'input',
- },
- },
- {
- field: 'message',
- name: 'Message',
- type: 'text',
- meta: {
- width: 'full',
- interface: 'input-multiline',
- },
- },
- ],
-};
-```
-
-`api.js`
-
-```js
-import twilio from 'twilio';
-
-export default {
- id: 'operation-twilio-sms',
- handler: ({ phone_number: toNumber, message }, { env }) => {
- const accountSid = env.TWILIO_ACCOUNT_SID;
- const authToken = env.TWILIO_AUTH_TOKEN;
- const fromNumber = env.TWILIO_PHONE_NUMBER;
- const client = new twilio(accountSid, authToken);
-
- client.messages
- .create({
- body: message,
- to: toNumber,
- from: fromNumber,
- })
- .then((response) => {
- return response;
- })
- .catch((error) => {
- return error;
- });
- },
-};
-```
diff --git a/docs/guides/extensions/panels-create-items.md b/docs/guides/extensions/panels-create-items.md
deleted file mode 100644
index b98aa073d4..0000000000
--- a/docs/guides/extensions/panels-create-items.md
+++ /dev/null
@@ -1,614 +0,0 @@
----
-description: Learn how to create custom panel extensions with dynamic forms that enter items into a collection.
-contributors: Tim Butterfield, Kevin Lewis
----
-
-# Create An Interactive Panel To Create Items
-
-Panels are used in dashboards as part of the Insights module. As well as read-only data panels, they can be interactive
-with form inputs. In this guide, you will create a panel that automatically generates a form based on a collection's
-fields, and allows item creation from an Insights dashboard.
-
-
-
-## Install Dependencies
-
-Open a console to your preferred working directory and initialize a new extension, which will create the boilerplate
-code for your operation.
-
-```shell
-npx create-directus-extension@latest
-```
-
-A list of options will appear (choose panel), and type a name for your extension (for example,
-`directus-panel-internal-form`). For this guide, select JavaScript.
-
-Now the boilerplate has been created, open the directory in your code editor.
-
-## Specify Configuration
-
-Panels have two parts - the `index.js` configuration file, and the `panel.vue` view. The first part is defining what
-information you need to render the panel in the configuration.
-
-Open `index.js` and change the `id`, `name`, `icon`, and `description`.
-
-```js
-id: 'panel-internal-form',
-name: 'Internal Form',
-icon: 'view_day',
-description: 'Output a form to insert data into a collection.',
-```
-
-Make sure the `id` is unique between all extensions including ones created by 3rd parties - a good practice is to
-include a professional prefix. You can choose an icon from the library [here](https://fonts.google.com/icons).
-
-The Panel will need some configuration options so the user can choose the collection and fields from that collection to
-include on the panel.
-
-Replace the existing text field with the following fields inside the `options` array:
-
-```js
-{
- field: 'collection',
- type: 'string',
- name: '$t:collection',
- meta: {
- interface: 'system-collection',
- options: {
- includeSystem: true,
- includeSingleton: false,
- },
- width: 'half',
- },
-},
-{
- field: 'fields',
- type: 'string',
- name: 'Included Fields',
- meta: {
- interface: 'system-field',
- options: {
- collectionField: 'collection',
- multiple: true,
- },
- width: 'half',
- },
-},
-{
- field: 'responseFormat',
- name: 'Response',
- type: 'string',
- meta: {
- interface: 'system-display-template',
- options: {
- collectionField: 'collection',
- placeholder: '{{ field }}',
- },
- width: 'full',
- },
-},
-```
-
-After the `options` section, there is the ability to limit the width and height of the panel. Since this panel will hold
-a lot of data, set these to `24` for the width and `18` for the height:
-
-```js
-minWidth: 24,
-minHeight: 18,
-skipUndefinedKeys: ['responseFormat'],
-```
-
-The output of these options will look like this:
-
-
-
-## Prepare the View
-
-Open the `panel.vue` file and you will see the starter template and script. Skip to the script section and import the
-following packages:
-
-```js
-import { useApi, useCollection, useStores } from '@directus/extensions-sdk';
-import { ref, watch } from 'vue';
-```
-
-In the `props`, `showHeader` is one of the built-in properties which you can use to alter your panel if a header is
-showing. Remove the text property and add the collection and fields properties as well as the width and height which is
-useful for styling:
-
-```js
-props: {
- showHeader: {
- type: Boolean,
- default: false,
- },
- collection: {
- type: String,
- default: '',
- },
- fields: {
- type: Array,
- default: [],
- },
- responseFormat: {
- type: String,
- default: '',
- },
- width: String,
- height: String,
-},
-```
-
-After the `props`, create a `setup(props)` section and create the variables needed:
-
-```js
-setup(props) {
- const { useFieldsStore, usePermissionsStore } = useStores();
- const fieldsStore = useFieldsStore();
- const permissionsStore = usePermissionsStore();
- const hasPermission = permissionsStore.hasPermission(props.collection, 'create');
- const api = useApi();
- const { primaryKeyField } = useCollection(props.collection);
- const formData = ref({});
- const fieldData = ref([]);
-
- const formResponse = ref({});
- const formError = ref({});
- const responseDialog = ref(false);
-}
-```
-
-The `FieldsStore` fetches all of the collection’s fields, the `PermissionsStore` checks the current user’s access to the
-collection, and the `Collection` store for fetching information about the selected collection and the API for performing
-the final POST request.
-
-You will also need to capture a response to present to the user. The `responseFormat` contains a string where the user
-can create their own response with data from the API. A `v-dialog` can show an important message to the user. This
-requires a boolean value (here `responseDialog`) to control the visibility of the dialog box.
-
-Create a `getFields` function to fetch the detailed information for each selected field then call the function
-afterwards so it populates the variable when the panel loads:
-
-```js
-function getFields() {
- fieldData.value = [];
-
- props.fields.forEach((field) => {
- fieldData.value.push(fieldsStore.getField(props.collection, field));
- });
-}
-
-getFields();
-```
-
-If the fields, collection, or response format is changed, the `getFields` function will need to be called again. Use the
-following code:
-
-```js
-watch([() => props.collection, () => props.fields, () => props.responseFormat], getFields);
-```
-
-Create a `submitForm` function. This will send the contents of `formData` to the selected collection and capture the
-response, resetting the form once successful. If an error occurs, the response is captured in the `formError` variable:
-
-```js
-function submitForm() {
- api
- .post(`/items/${props.collection}`, formData.value)
- .then((response) => {
- formResponse.value = response.data.data;
- responseDialog.value = true;
- formData.value = {};
- })
- .catch((error) => {
- formError.value = error;
- responseDialog.value = true;
- });
-}
-```
-
-To show the response, the `responseDialog` variable is changed to `true`.
-
-In the successful response, it will be useful to have a link to the new record. Create the following function to build
-the URL for the newly created item:
-
-```js
-function getLinkForItem(item) {
- if (item === undefined) return;
- const primaryKey = item[primaryKeyField.value.field];
- return `/content/${props.collection}/${encodeURIComponent(primaryKey)}`;
-}
-```
-
-At the end of the script, return the required constants and functions for use in the Vue template:
-
-```js
-return {
- hasPermission,
- primaryKeyField,
- formData,
- fieldData,
- submitForm,
- formResponse,
- formError,
- responseDialog,
- getLinkForItem,
-};
-```
-
-## Build the View
-
-Back to the template section, remove all the content between the template tags, then add the following code to handle
-the permissions:
-
-```vue
-
- --- -- -
- {{ formError }} --
-
-## Summary
-
-With this panel, you can create forms to create items in your collections. You have worked with the `FieldsStore` and
-`PermissionsStore`, and can further expand on this example for other changes to your database.
-
-## Complete Code
-
-`index.js`
-
-```js
-import PanelComponent from './panel.vue';
-
-export default {
- id: 'panel-internal-form',
- name: 'Internal Form',
- icon: 'view_day',
- description: 'Output a form to insert data into a collection.',
- component: PanelComponent,
- options: [
- {
- field: 'collection',
- type: 'string',
- name: '$t:collection',
- meta: {
- interface: 'system-collection',
- options: {
- includeSystem: true,
- includeSingleton: false,
- },
- width: 'half',
- },
- },
- {
- field: 'fields',
- type: 'string',
- name: 'Included Fields',
- meta: {
- interface: 'system-field',
- options: {
- collectionField: 'collection',
- multiple: true,
- },
- width: 'half',
- },
- },
- {
- field: 'responseFormat',
- name: 'Response',
- type: 'string',
- meta: {
- interface: 'system-display-template',
- options: {
- collectionField: 'collection',
- placeholder: '{{ field }}',
- },
- width: 'full',
- },
- },
- ],
- minWidth: 12,
- minHeight: 8,
- skipUndefinedKeys: ['responseFormat'],
-};
-```
-
-`panel.vue`
-
-```vue
-
- - --- -- -
- {{ formError }} -- -
-
-Panels can only talk to internal Directus services, and can't reliably make external web requests because browser
-security protections prevent these cross-origin requests from being made. To create a panel that can interact with
-external APIs, this guide will create a bundle of an endpoint (that can make external requests) and a panel (that uses
-the endpoint).
-
-## Before You Start
-
-You will need a Directus project - check out our [quickstart guide](https://docs.directus.io/getting-started/quickstart)
-if you don't already have one. You will also need a
-[Vonage Developer API account](https://developer.vonage.com/sign-up), taking note of your API Key and Secret.
-
-## Create Bundle
-
-Open a console to your preferred working directory and initialize a new extension, which will create the boilerplate
-code for your operation.
-
-```shell
-npx create-directus-extension@latest
-```
-
-A list of options will appear (choose bundle), and type a name for your extension (for example,
-`directus-extension-bundle-vonage-activity`).
-
-Now the boilerplate bundle has been created, navigate to the directory with
-`cd directus-extension-bundle-vonage-activity` and open the directory in your code editor.
-
-## Add an Endpoint to the Bundle
-
-In your terminal, run `npm run add` to create a new extension in this bundle. A list of options will appear (choose
-endpoint), and type a name for your extension (for example, `directus-endpoint-vonage`). For this guide, select
-JavaScript.
-
-This will add an entry to the `directus:extension` metadata in your `package.json` file.
-
-## Build the Endpoint
-
-As there is a more detailed guide on
-[building an authenticated custom endpoint to proxy external APIs](/guides/extensions/endpoints-api-proxy-twilio), this
-guide will be more brief in this section.
-
-Open the `src/directus-endpoint-vonage/index.js` file and replace it with the following:
-
-```js
-import { createError } from '@directus/errors';
-
-const ForbiddenError = createError('VONAGE_FORBIDDEN', 'You need to be authenticated to access this endpoint');
-
-export default {
- id: 'vonage',
- handler: (router, { env }) => {
- const { VONAGE_API_KEY, VONAGE_API_SECRET } = env;
- const baseURL = 'https://api.nexmo.com';
- const token = Buffer.from(`${VONAGE_API_KEY}:${VONAGE_API_SECRET}`).toString('base64');
- const headers = { Authorization: `Basic ${token}` };
-
- router.get('/records', async (req, res) => {
- if (req.accountability == null) throw new ForbiddenError();
-
- try {
- const url = baseURL + `/v2/reports/records?account_id=${VONAGE_API_KEY}&${req._parsedUrl.query}`;
- const response = await fetch(url, { headers });
-
- if (response.ok) {
- res.json(await response.json());
- } else {
- res.status(response.status).send(response.statusText);
- }
- } catch (error) {
- res.status(500).send(response.statusText);
- }
- });
- },
-};
-```
-
-This extension introduces the `/vonage/records` endpoint to your application. Make sure to add the `VONAGE_API_KEY` and
-`VONAGE_API_SECRET` to your environment variables.
-
-## Add a View to the Bundle
-
-In your terminal, run `npm run add` to create a new extension in this bundle. A list of options will appear (choose
-panel), and type a name for your extension (for example, `directus-panel-vonage-activity`). For this guide, select
-JavaScript.
-
-This will add an entry to the `directus:extension` metadata in your `package.json` file.
-
-## Configure the View
-
-Panels have two parts - the `index.js` configuration file, and the `panel.vue` view. The first part is defining what
-information you need to render the panel in the configuration.
-
-Open `index.js` and change the `id`, `name`, `icon`, and `description`.
-
-```js
-id: 'panel-vonage-activity',
-name: 'Vonage Reports',
-icon: 'list_alt',
-description: 'View recent Vonage SMS activity.',
-```
-
-Make sure the `id` is unique between all extensions including ones created by 3rd parties - a good practice is to
-include a professional prefix. You can choose an icon from the library [here](https://fonts.google.com/icons).
-
-The Panel will accept configuration options. The Vonage API supports `date_start`, `date_end`, `status`, `direction`
-(incoming/outgoing), and `product` type (SMS/Messages).
-
-For the product type, add a selection field with the options `SMS` and `MESSAGES`:
-
-```js
-{
- field: 'type',
- name: 'Product Type',
- type: 'string',
- meta: {
- width: 'half',
- interface: 'select-dropdown',
- options: {
- choices: [
- { text: 'SMS', value: 'SMS' },
- { text: 'Messages', value: 'MESSAGES' }
- ],
- },
- },
-},
-```
-
-Add another selection field for the ‘direction’ of the messages, inbound and outbound.
-
-```js
-{
- field: 'direction',
- name: 'Direction',
- type: 'string',
- meta: {
- width: 'half',
- interface: 'select-dropdown',
- options: {
- choices: [
- { text: 'Outbound', value: 'outbound' },
- { text: 'Inbound', value: 'inbound' }
- ],
- }
- }
-},
-```
-
-It would be useful to control the scope of data for those who transact larger amounts of messages. Add the following
-option for the user to select a range:
-
-```js
-{
- field: 'range',
- type: 'dropdown',
- name: '$t:date_range',
- schema: { default_value: '1 day' },
- meta: {
- interface: 'select-dropdown',
- width: 'half',
- options: {
- choices: [
- { text: 'Past 5 Minutes', value: '5 minutes' },
- { text: 'Past 15 Minutes', value: '15 minutes' },
- { text: 'Past 30 Minutes', value: '30 minutes' },
- { text: 'Past 1 Hour', value: '1 hour' },
- { text: 'Past 4 Hours', value: '4 hours' },
- { text: 'Past 1 Day', value: '1 day' },
- { text: 'Past 2 Days', value: '2 days' }
- ]
- }
- }
-},
-```
-
-Vonage has the ability to include the message in the response. This will be useful to provide as a preview upon click
-but for larger datasets may impact the performance of the API. Create an option to toggle this on/off:
-
-```js
-{
- field: 'includeMessage',
- name: 'Include Message',
- type: 'boolean',
- meta: {
- interface: 'boolean',
- width: 'half',
- },
- schema: {
- default_value: false,
- }
-},
-```
-
-Lastly, add the option to limit the messages to a specific state such as delivered or failed. The default option is
-`Any`:
-
-```js
-{
- field: 'status',
- name: 'Status',
- type: 'string',
- schema: {
- default_value: 'any',
- },
- meta: {
- width: 'half',
- interface: 'select-dropdown',
- options: {
- choices: [
- { text: 'Any', value: 'any' },
- { text: 'Delivered', value: 'delivered' },
- { text: 'Expired', value: 'expired' },
- { text: 'Failed', value: 'failed' },
- { text: 'Rejected', value: 'rejected' },
- { text: 'Accepted', value: 'accepted' },
- { text: 'buffered', value: 'buffered' },
- { text: 'Unknown', value: 'unknown' },
- { text: 'Deleted', value: 'deleted' }
- ]
- }
- }
-},
-```
-
-After the `options` section, there is the ability to limit the width and height of the panel. Since this panel will hold
-a lot of data, set these to `24` for the width and `18` for the height:
-
-```js
-minWidth: 24,
-minHeight: 18,
-```
-
-The output of these options will look like this:
-
-
-
-## Prepare the View
-
-Open the `panel.vue` file and you will see the starter template and script. Skip to the script section and import the
-following packages:
-
-```js
-import { useApi } from '@directus/extensions-sdk';
-import { adjustDate } from '@directus/shared/utils';
-import { formatISO, formatDistanceToNow, parseISO } from 'date-fns';
-import { ref, watch } from 'vue';
-```
-
-In the `props`, `showHeader` is one of the built-in properties which you can use to alter your panel if a header is
-showing. Remove the text property and add all the options that were created in the previous file:
-
-```js
-props: {
- showHeader: {
- type: Boolean,
- default: false,
- },
- type: {
- type: String,
- default: '',
- },
- direction: {
- type: String,
- default: '',
- },
- range: {
- type: String,
- default: '',
- },
- includeMessage: {
- type: Boolean,
- default: false,
- },
- status: {
- type: String,
- default: '',
- },
-},
-```
-
-After the `props`, create a `setup(props)` section and create the variables needed:
-
-```js
-
-setup(props) {
- const api = useApi();
- const activityData = ref([]);
- const now = ref(new Date());
- const isLoading = ref(true);
- const errorMessage = ref();
-},
-
-```
-
-Create a `fetchData` function that will use the information provided to construct the query parameters and perform the
-API query. The response is written to the `activityData` variable.
-
-Use the `isLoading` variable to hide or show the progress spinner to indicate that the query is running:
-
-```js
-async function fetchData() {
- isLoading.value = true;
- activityData.value = [];
-
- const dateStart = adjustDate(now.value, props.range ? `-${props.range}` : '-1 day');
-
- const params = {
- product: props.type || 'SMS',
- direction: props.direction || 'outbound',
- include_message: props.includeMessage.toString(),
- date_start: dateStart ? formatISO(dateStart) : '',
- status: props.status || 'any',
- };
-
- if (props.status) params.status = props.status;
-
- const url_params = new URLSearchParams(params);
-
- try {
- const response = await api.get(`/vonage/records?${url_params.toString()}`);
- activityData.value = response.data.records;
- } catch {
- errorMessage.value = 'Internal Server Error';
- } finally {
- isLoading.value = false;
- }
-}
-
-fetchData();
-```
-
-The endpoint `/vonage/records` comes from the custom extension created in an earlier step. When `fetchData()` is called,
-the `activityData` variable is updated with the result.
-
-If any of the properties are changed, the function will need to update the activity data again. Use the following code:
-
-```js
-watch(
- [() => props.type, () => props.direction, () => props.range, () => props.includeMessage, () => props.status],
- fetchData
-);
-```
-
-At the end of the script, return the required variables and functions for use in the Vue template:
-
-```js
-return { activityData, isLoading, errorMessage, formatDistanceToNow, parseISO };
-```
-
-## Build the View
-
-Back to the template section, remove all the content between the template tags, then add a fallback notice if some
-essential information is missing. Start with this:
-
-```vue
-
-
-
-```
-
-The `v-progress-circular` is a loading spinner that is active while the `isLoading` variable is true. After that, there
-is a `danger` notice if `errorMessage` contains a value, then an `info` notice if there aren't any messages in the data.
-
-Next, build a table to present the data:
-
-```vue
-| Status | -Sent | -Received | -Message | -Recipient | -From | -Provider | -
|---|---|---|---|---|---|---|
| {{ message.status }} | -- {{ formatDistanceToNow(parseISO(message.date_finalized ? message.date_finalized : message.date_received)) }} ago - | - -{{ message.to }} | -{{ message.from }} | -{{ type == 'MESSAGES' ? message.provider : message.network_name }} | -
-
-## Summary
-
-With this panel, Messages and SMS recently sent through Vonage are listed on your dashboards. You can alter your custom
-endpoint extension to create more panels for other Vonage APIs.
-
-## Complete Code
-
-### Endpoint
-
-`index.js`
-
-```js
-import { createError } from '@directus/errors';
-
-const ForbiddenError = createError('VONAGE_FORBIDDEN', 'You need to be authenticated to access this endpoint');
-
-export default {
- id: 'vonage',
- handler: (router, { env }) => {
- const { VONAGE_API_KEY, VONAGE_API_SECRET } = env;
- const baseURL = 'https://api.nexmo.com';
- const token = Buffer.from(`${VONAGE_API_KEY}:${VONAGE_API_SECRET}`).toString('base64');
- const headers = { Authorization: `Basic ${token}` };
-
- router.get('/records', async (req, res) => {
- if (req.accountability == null) throw new ForbiddenError();
-
- try {
- const url = baseURL + `/v2/reports/records?account_id=${VONAGE_API_KEY}&${req._parsedUrl.query}`;
- const response = await fetch(url, { headers });
-
- if (response.ok) {
- res.json(await response.json());
- } else {
- res.status(response.status).send(response.statusText);
- }
- } catch (error) {
- res.status(500).send(response.statusText);
- }
- });
- },
-};
-```
-
-### Panel
-
-`index.js`
-
-```js
-import PanelComponent from './panel.vue';
-
-export default {
- id: 'panel-vonage-sms-activity',
- name: 'Vonage Reports',
- icon: 'list_alt',
- description: 'View recent SMS activity.',
- component: PanelComponent,
- options: [
- {
- field: 'type',
- name: 'Product Type',
- type: 'string',
- meta: {
- width: 'half',
- interface: 'select-dropdown',
- options: {
- choices: [
- { text: 'SMS', value: 'SMS' },
- { text: 'Messages', value: 'MESSAGES' },
- ],
- },
- },
- },
- {
- field: 'direction',
- name: 'Direction',
- type: 'string',
- meta: {
- width: 'half',
- interface: 'select-dropdown',
- options: {
- choices: [
- { text: 'Outbound', value: 'outbound' },
- { text: 'Inbound', value: 'inbound' },
- ],
- },
- },
- },
- {
- field: 'range',
- type: 'dropdown',
- name: '$t:date_range',
- schema: {
- default_value: '1 day',
- },
- meta: {
- interface: 'select-dropdown',
- width: 'half',
- options: {
- choices: [
- { text: 'Past 5 Minutes', value: '5 minutes' },
- { text: 'Past 15 Minutes', value: '15 minutes' },
- { text: 'Past 30 Minutes', value: '30 minutes' },
- { text: 'Past 1 Hour', value: '1 hour' },
- { text: 'Past 4 Hours', value: '4 hours' },
- { text: 'Past 1 Day', value: '1 day' },
- { text: 'Past 2 Days', value: '2 days' },
- ],
- },
- },
- },
- {
- field: 'includeMessage',
- name: 'Include Message',
- type: 'boolean',
- meta: {
- interface: 'boolean',
- width: 'half',
- },
- schema: {
- default_value: false,
- },
- },
- {
- field: 'status',
- name: 'Status',
- type: 'string',
- schema: {
- default_value: 'any',
- },
- meta: {
- width: 'half',
- interface: 'select-dropdown',
- options: {
- choices: [
- { text: 'Any', value: 'any' },
- { text: 'Delivered', value: 'delivered' },
- { text: 'Expired', value: 'expired' },
- { text: 'Failed', value: 'failed' },
- { text: 'Rejected', value: 'rejected' },
- { text: 'Accepted', value: 'accepted' },
- { text: 'buffered', value: 'buffered' },
- { text: 'Unknown', value: 'unknown' },
- { text: 'Deleted', value: 'deleted' },
- ],
- },
- },
- },
- ],
- minWidth: 24,
- minHeight: 18,
-};
-```
-
-`panel.vue`
-
-```vue
-
-
-
-
-
-
-
-```
diff --git a/docs/guides/extensions/panels-send-sms-twilio.md b/docs/guides/extensions/panels-send-sms-twilio.md
deleted file mode 100644
index 46d1e8dd70..0000000000
--- a/docs/guides/extensions/panels-send-sms-twilio.md
+++ /dev/null
@@ -1,1131 +0,0 @@
----
-description: A guide on how you can create a panel to send SMS messages via an external API like Twilio.
-contributors: Tim Butterfield, Kevin Lewis
----
-
-# Create An Interactive Panel To Send SMS With Twilio
-
-Panels are used in dashboards as part of the Insights module. As well as read-only data panels, they can be interactive
-with form inputs.
-
-
-
-## Install Dependencies
-
-Panels can only talk to internal Directus services, and can't reliably make external web requests because browser
-security protections prevent these cross-origin requests from being made. To create a panel that can interact with
-external APIs, you must interact with the API using an endpoint. This particular panel extension builds off of the
-[Twilio Custom Endpoint Extension guide](/guides/extensions/endpoints-api-proxy-twilio). Make sure you have access to
-these custom endpoints before starting this guide.
-
-Open a console to your preferred working directory and initialize a new extension, which will create the boilerplate
-code for your operation.
-
-```shell
-npx create-directus-extension@latest
-```
-
-A list of options will appear (choose panel), and type a name for your extension (for example,
-`directus-panel-twilio-sms`). For this guide, select JavaScript.
-
-Now the boilerplate has been created, open the directory in your code editor.
-
-## Specify Configuration
-
-Panels have two parts - the `index.js` configuration file, and the `panel.vue` view. The first part is defining what
-information you need to render the panel in the configuration.
-
-Open `index.js` and change the `id`, `name`, `icon`, and `description`.
-
-```js
-id: 'panel-twilio-sms',
-name: 'Twilio SMS',
-icon: 'forum',
-description: 'Send a SMS from a panel.',
-```
-
-Make sure the `id` is unique between all extensions including ones created by 3rd parties - a good practice is to
-include a professional prefix. You can choose an icon from the library [here](https://fonts.google.com/icons).
-
-With the information above, the panel will appear in the list like this:
-
-
-
-The Panel will need some configuration to be able to send messages such as the Twilio account, the sending number, where
-to find the contacts and some visual customization. In the `options` section, add two fields to collect the Twilio Phone
-Number and Account SID:
-
-```js
-{
- field: 'twilioPhoneNumber',
- name: 'Twilio Phone Number',
- type: 'string',
- meta: {
- interface: 'input',
- width: 'half',
- },
-},
-{
- field: 'twilioSid',
- name: 'Twilio Account SID',
- type: 'string',
- meta: {
- interface: 'input',
- width: 'half',
- },
-},
-```
-
-To fetch the contacts, add a field for selecting a collection using the system-collection interface and a field for
-selecting the phone number field using the system-field interface. These will automatically populate the dropdown with
-the values from Directus and form the basis for an API call.
-
-For occasions where the user might want to limit the scope of contacts, add a filter field using the system-filter
-interface.
-
-```js
-{
- field: 'collection',
- type: 'string',
- name: '$t:collection',
- meta: {
- interface: 'system-collection',
- options: {
- includeSystem: true,
- includeSingleton: false,
- },
- width: 'half',
- },
-},
-{
- field: 'phoneNumberField',
- type: 'string',
- name: 'Phone Number',
- meta: {
- interface: 'system-field',
- options: {
- collectionField: 'collection',
- typeAllowList: ['string','integer'],
- },
- width: 'half',
- },
-},
-{
- field: 'filter',
- type: 'json',
- name: '$t:filter',
- meta: {
- interface: 'system-filter',
- options: {
- collectionField: 'collection',
- relationalFieldSelectable: false,
- },
- },
-},
-```
-
-There are many ways to implement this panel so customization is key. Add the following options to allow a fixed 'static'
-message, a custom button label, batch recipient list and a custom display template for contacts:
-
-```js
-{
- field: 'message',
- type: 'text',
- name: 'Message',
- meta: {
- interface: 'input-multiline',
- width: 'full',
- },
-},
-{
- field: 'buttonLabel',
- name: 'Button Label',
- type: 'string',
- meta: {
- interface: 'input',
- width: 'half',
- },
-},
-{
- field: 'batchSend',
- name: 'Send to All',
- type: 'boolean',
- meta: {
- interface: 'boolean',
- width: 'half',
- },
- schema: {
- default_value: false,
- },
-},
-{
- field: 'displayTemplate',
- name: 'Name in list',
- type: 'string',
- meta: {
- interface: 'system-display-template',
- options: {
- collectionField: 'collection',
- placeholder: '{{ field }}',
- },
- width: 'full',
- },
-},
-```
-
-After the options section, there is the ability to limit the width and height of the panel. Set these to 12 for the
-width and 5 for the height.
-
-It is important to include `skipUndefinedKeys` which is a list of system-display-template fields.
-
-This completes the `index.js` file. The output of the options will look like this:
-
-
-
-## Prepare the View
-
-Open the `panel.vue` file and import the following functions at the top of the `
-
-
-```
diff --git a/docs/guides/flows/flows-for-loop.md b/docs/guides/flows/flows-for-loop.md
deleted file mode 100644
index 9cf0dc1175..0000000000
--- a/docs/guides/flows/flows-for-loop.md
+++ /dev/null
@@ -1,64 +0,0 @@
----
-description:
- When most flows begin, they pass the trigger's payload to the data chain and execute once. This recipe explains how to
- execute a flow for each element in a payload's array.
-directus_version: 9.18.1
-author: Eron Powell
----
-
-# For Loops In Flows
-
-{global.description}
-Once upon a time, there was a bunny named Benny who loved to write and share his stories with the world. Benny had been using a headless CMS to manage his content, but he realized that he was only reaching a small audience in his own country.
\nBenny decided that he wanted to internationalize his content and reach a broader audience. He furiously typed away on his keyboard, translating his stories into different languages.
\nBut as he scrolled through the list of supported languages in the headless CMS, he realized that he didn't know how to speak any of them. He had used Google Translate to translate his stories, but he wasn't sure if the translations were accurate.
\nAs he pondered his dilemma, Benny heard a knock at the door. It was a group of rabbits from different countries, all wanting to read Benny's stories in their native languages.
\nBenny was overjoyed and started to share his stories with them. But as the rabbits listened, they started to laugh uncontrollably.
\nBenny was confused and asked them what was so funny. One of the rabbits replied, \"Benny, your translations are hilarious! They don't make any sense!\"
\nBenny was embarrassed but couldn't help but laugh along with the other rabbits. He realized that he needed to find a better way to internationalize his content.
\nFrom that day on, Benny made sure to hire professional translators to translate his stories into different languages. And he lived happily ever after, sharing his stories with rabbits from all over the world.
" - } - ] - } - ] -} -``` - -::: - -## Next Steps - -Over the course of this guide, you've setup the basics for translating your content within Directus. Nice work! - -Next, you would setup translations collections for all the different content collections you'll be translating. - -### Test Your API Calls - -Your Headless CMS is just one side of the equation so don't forget to test the API responses and handle them -appropriately within your frontend. - -### Check Your Permissions - -If you notice you aren't receiving the data that you expect, check the Permissions settings for your Public or chosen -role. You'll have to enable Read access for the `languages`, `articles`, and `articles_translations` collections. diff --git a/docs/guides/headless-cms/content-versioning.md b/docs/guides/headless-cms/content-versioning.md deleted file mode 100644 index c50070ad6f..0000000000 --- a/docs/guides/headless-cms/content-versioning.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -description: This guide covers the process of enabling and utilizing Content Versioning in Directus. -contributors: Esther Agbaje ---- - -# Implementing Content Versioning - -> {{ $frontmatter.description }} - -## Introduction - -Content Versioning allows teams to create and manage different versions of their content. There are several reasons to -use Content Versioning, including drafting content without publishing it, and more ways to collaborate with others. - -## Concepts - -1. **Version:** A version is a snapshot of the differences between the original item and the created version. Each - version represents a set of future changes to be applied to the item. -2. **Main:** The main version is the original version of a piece of content that has been created and published. It is - the default version that is displayed to users. The main version is the "source of truth" for all other versions. -3. **Promote:** Promoting a version means to make it the new main version. When a new version is promoted, it becomes - the main version that is displayed to users, and it replaces the previous main version. - -## Setting Up Content Versioning - -To enable Content Versioning for a collection in Directus, follow these steps: - -1. Log in to your Directus instance. -2. Navigate to **Settings** > **Data Model**. -3. Select the collection that you want to enable Content Versioning for. -4. Scroll down to the Content Versioning section. -5. Toggle "**Enable Versions**" and save your data model. - -## Creating a New Version - -With Content Versioning set up, you can now create a new version of your content: - -1. Open an item within your desired collection. -2. At the top of the item view, you will notice a dropdown with the main Content Version displayed as "**main**". -3. From the dropdown, click "**Create Version**". -4. Provide a **key** and a **name** for the new version. For instance, you can use "draft" as the key and "My Draft" as - the name. -5. Click "**Save**" to create the new version. - -::: tip Version Source - -All new versions originate from the main version. This implies that the main version acts as the single source of truth -for other versions. - -::: - -## Making Changes to a Version - -After creating a new version, you can make changes without affecting the main version: - -1. Open the item in the newly created version e.g. "**My Draft**" version. -2. Make the desired edits to the item's content. -3. Save the changes. -4. Notice that the main version remains unaffected, while the changes are reflected only in the "**My Draft**" version. - -## Reviewing and Promoting a Version - -Once you are satisfied with the changes made in a version and want to incorporate them into the main version, you can -promote the version. Follow these steps to promote a version: - -1. Open the version you want to promote. -2. Select the "**Promote Version**" option from the dropdown. -3. In the "**Changes**" tab, you can review all the changes made in the version and decide which changes to accept or - reject. -4. Switch to the "**Preview**" tab to see a preview of the changes you are about to promote. -5. After reviewing the changes, confirm the promotion by clicking "**Save**". - -After promoting a version, you can choose to keep or delete the version. - -::: tip Programmatically Implement Content Versioning - -You have the option to integrate Content Versioning through the API. To learn how to accomplish this, please refer to -our [API reference documentation](/reference/system/versions). - -::: - -## Revisions and Content Versioning - -Under the hood, content versions are stored in the `directus_revisions` collection. In bigger projects this collection -can get large. - -Some Directus users combat this by periodically purging some or all data in this collection. Be aware that this could -unintentionally purge content versions, so purging logic may have to be updated. - -## Next Steps - -You have now set up Content Versioning for a collection and learned how to promote a version to the main version. Here -are some additional tips when using this feature: - -- Name versions descriptively so it's clear what changes each version contains. -- To prevent conflicts, ensure no modifications are made to the main version at the time a new version is created. -- Access the version history by referring the "**Revisions**" sidebar. -- Rollback to a previous main version by using the existing revert feature. -- Configure roles and permissions as needed to the `directus_versions` system collection. diff --git a/docs/guides/headless-cms/live-preview/index.md b/docs/guides/headless-cms/live-preview/index.md deleted file mode 100644 index b4ce67e956..0000000000 --- a/docs/guides/headless-cms/live-preview/index.md +++ /dev/null @@ -1,19 +0,0 @@ -# Set Up Live Preview - -{body}
-{body}
- {isEnabled &&(Draft Mode)
} // [!code ++] -Rabbits are a great source of environmental benefit. They help to keep grasslands and other ecosystems in check. Rabbits are herbivores, meaning they eat only plants, which helps to keep vegetation in balance. Additionally, rabbits are crucial to the food chain, providing sustenance for predators in their environment.
\nRabbits also help to improve the quality of soil by digging burrows and depositing their waste in them. This helps to aerate the soil, improving its quality and allowing for better plant growth. Additionally, the waste from rabbits is a rich source of nutrients for plants and other animals in the area. This helps to keep the soil healthy and support the overall ecosystem.
" - } - } - ] - } - ] -} -``` - -::: - -### Structuring Your Front End - -We have [integration guides](https://directus.io/guides/) for many popular front-end frameworks. But there are far too -many to cover in this recipe. - -Here’s some general advice on how to structure your front end to display page blocks / Many-To-Any (M2A) Relationship -data. - -**Create a single component for each individual page_builder collection.** - -- Hero -- Gallery -- Article - -**Create a dynamic route that does the following:** - -- Imports your page builder components. -- Calls your `pages` collection via the API. Add a filter rule to match the requested page’s `slug`. -- Maps all the possible `page.pages_blocks.collection` names to your page block components. -- Loops through the `page.blocks` array and passes the correct data (props) that each page_builder component needs to - render properly. - -## Final Tips - -This guide has quite a few steps and involves several different collections. Here are some helpful tips to consider. - -### Study the API Response - -To prevent frustration when building your front-end, it’s important you understand the structure of the JSON data that -Directus returns for Many To Any (M2A) relationships. - -- Complete your page builder data model inside Directus. -- Add some content. -- Test your API calls. - -### Check Your Permissions - -If you notice you aren't receiving the data that you expect, check the Permissions settings for your Public or chosen -role. You'll have to enable Read access for each collection using in the Pages > Blocks Many-To-Any field. - -### Use Typescript - -We recommend adding types for each of your different collections to your frontend framework. - -### Organize Your Data Model with Folders - -Consider using [data model folders](/app/data-model/collections#create-a-folder) to keep things nicely organized and -your collections easy to find. - - - -### Use Translations for Collection Names - -When [setting up Collections](/app/data-model/collections#collection-setup) within your data model, use the Collection -Naming Translations to create names that easier for the Data Studio users to understand. - - - -For example: - -- `block_richtext` becomes `Rich Text` -- `block_hero` becomes `Hero` or `Hero Block` -- `block_cardgroup` becomes `Card Group` diff --git a/docs/guides/headless-cms/schedule-content/dynamic-sites.md b/docs/guides/headless-cms/schedule-content/dynamic-sites.md deleted file mode 100644 index 447ee0a86f..0000000000 --- a/docs/guides/headless-cms/schedule-content/dynamic-sites.md +++ /dev/null @@ -1,139 +0,0 @@ ---- -description: - This recipe explains how to schedule content to be published for a future date depending on your front-end approach. -directus_version: 9.21.2 -author: Bryant Gillespie ---- - -# Schedule Future Content for Dynamic Sites - -