diff --git a/directus-extension/.env.example b/directus-extension/.env.example new file mode 100644 index 00000000..65ab850f --- /dev/null +++ b/directus-extension/.env.example @@ -0,0 +1,15 @@ +# These are the keys that have been overridden to make this work with Remix +# Add them to the top of the .env file generated by the directus init command. + +# Where to redirect to when navigating to /. Accepts a relative path, absolute URL, or false to disable ["./admin"] +ROOT_REDIRECT="false" +# Ensures Directus can't override whether we are in development or production. Set to `production` in production environments. +REMIX_ENV=development +# Only necessary in development environments. +REMIX_DEV_ORIGIN=http://0.0.0.0:3001/ +# Required for the Remix app to work +CONTENT_SECURITY_POLICY_DIRECTIVES__SCRIPT_SRC="array:'self','unsafe-inline','unsafe-eval'" +# Required for live reload to work. Not strictly necessary in production. +CONTENT_SECURITY_POLICY_DIRECTIVES__CONNECT_SRC="array:'self',https:,http:,wss:,ws:" +# Tells Directus to reload extensions when the source files change. Not required in production. +EXTENSIONS_AUTO_RELOAD=true diff --git a/directus-extension/.eslintrc.cjs b/directus-extension/.eslintrc.cjs new file mode 100644 index 00000000..2061cd22 --- /dev/null +++ b/directus-extension/.eslintrc.cjs @@ -0,0 +1,4 @@ +/** @type {import('eslint').Linter.Config} */ +module.exports = { + extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"], +}; diff --git a/directus-extension/.gitignore b/directus-extension/.gitignore new file mode 100644 index 00000000..dc698e79 --- /dev/null +++ b/directus-extension/.gitignore @@ -0,0 +1,8 @@ +node_modules + +/.cache +/build +/public/build +.env +data.db +extensions \ No newline at end of file diff --git a/directus-extension/README.md b/directus-extension/README.md new file mode 100644 index 00000000..579a287c --- /dev/null +++ b/directus-extension/README.md @@ -0,0 +1,87 @@ +# Remix Directus Extension + +This example demonstrates how a Remix app can be embedded in a [Directus](https://directus.io) app using an endpoint extension. The app is comprised of three parts: + +- The Remix app +- The self-hosted Directus instance +- A Directus endpoint extension, called `remix-frontend` + +## What is Directus? + +Directus is a self-hosted CMS written in JavaScript and Vue. It allows you to connect a relational database, define data models, view and edit the data, upload and manage files, set up automated workflows and webhooks, view data panels, manage users, roles, and auth... it provides a lot. + +It's also highly extensible. Many parts of the UI and backend can be modified and changed using the Directus extension API. + +## How it Works + +Directus allows you to add API endpoints to a running Directus instance using a special extension. This extension exposes an Express router which we can use to pass requests to our Remix app. With a bit of clever configuration, we can make our app available at the root (eg. `/`) of our Directus instance and serve all requests (except those to the Directus admin at `/admin/*`) from our Remix app. + +We can also take advantage of Remix load context and pass Directus utilities to our Remix app. Things like the Directus services, which provide a convenient API for accessing Directus resources, accountability information about the currently logged in user, and direct access to the database. + +In this example, we use the `ItemsService`, which is accessed through load context, to pull our list of blog posts in our loader to render to the page. + +### Patching Vue Types + +This example installs Directus directly, which includes `vue` as a dependency. Unfortunately, Vue uses interface overloading to alter the way TypeScript types JSX, causing errors in idiomatic React code. + +To solve this, this example uses `patch-package` to change the types in the Vue package so they don't interfere with React's JSX typings. + +This is handled automatically with a `postinstall` package.json script. + +## Development + +This example includes an example environment variables file. + +Before doing anything else, you'll need to set up your Directus instance. From your terminal run: + +```sh +npx directus@latest init +``` + + +Follow the prompts to set up the Directus app how you want. If you're just trying it out, use the `sqlite` database driver. + +Once that's done you'll need to add the necessary environment variables at top of the `.env.example` file to your `.env` file. These are required to enable Remix to run from within Directus. + +You can apply the example snapshot by running: + +```sh +npx directus schema apply ./snapshot.yml +``` + +Make sure the extension has been built before the Directus instance starts for the first time. + +From your terminal: + +```sh +npm run build +``` + +then + +```sh +npm run dev +``` + +This will start the Remix dev server, the extension compiler in watch mode, and the Directus data studio in +development mode. + +You can access your app at `http://localhost:8055` and access the Directus data studio at `http://localhost:8055/admin`. + +Sign into the data studio, add a post, visit the site, and you'll see the post appear. + +## Deployment + +First, build your app for production: + +```sh +npm run build +``` + +Then run the app in production mode: + +```sh +npm start +``` + +Now you'll need to pick a host to deploy it to. It should work out-of-the-box with any Nixpack-compatible host, like Railway or Flightcontrol. It should also work with a simple Node-based Dockerfile on hosts that support that. Just use `npm run build` as the build command and `npm run start` as the start command, and make sure you've set up the necessary environment variables. diff --git a/directus-extension/app/entry.client.tsx b/directus-extension/app/entry.client.tsx new file mode 100644 index 00000000..94d5dc0d --- /dev/null +++ b/directus-extension/app/entry.client.tsx @@ -0,0 +1,18 @@ +/** + * By default, Remix will handle hydrating your app on the client for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/file-conventions/entry.client + */ + +import { RemixBrowser } from "@remix-run/react"; +import { startTransition, StrictMode } from "react"; +import { hydrateRoot } from "react-dom/client"; + +startTransition(() => { + hydrateRoot( + document, + + + + ); +}); diff --git a/directus-extension/app/entry.server.tsx b/directus-extension/app/entry.server.tsx new file mode 100644 index 00000000..0c7712b0 --- /dev/null +++ b/directus-extension/app/entry.server.tsx @@ -0,0 +1,137 @@ +/** + * By default, Remix will handle generating the HTTP Response for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/file-conventions/entry.server + */ + +import { PassThrough } from "node:stream"; + +import type { AppLoadContext, EntryContext } from "@remix-run/node"; +import { createReadableStreamFromReadable } from "@remix-run/node"; +import { RemixServer } from "@remix-run/react"; +import isbot from "isbot"; +import { renderToPipeableStream } from "react-dom/server"; + +const ABORT_DELAY = 5_000; + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, + loadContext: AppLoadContext +) { + return isbot(request.headers.get("user-agent")) + ? handleBotRequest( + request, + responseStatusCode, + responseHeaders, + remixContext + ) + : handleBrowserRequest( + request, + responseStatusCode, + responseHeaders, + remixContext + ); +} + +function handleBotRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + return new Promise((resolve, reject) => { + let shellRendered = false; + const { pipe, abort } = renderToPipeableStream( + , + { + onAllReady() { + shellRendered = true; + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + responseStatusCode = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + } + ); + + setTimeout(abort, ABORT_DELAY); + }); +} + +function handleBrowserRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + return new Promise((resolve, reject) => { + let shellRendered = false; + const { pipe, abort } = renderToPipeableStream( + , + { + onShellReady() { + shellRendered = true; + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + responseStatusCode = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + } + ); + + setTimeout(abort, ABORT_DELAY); + }); +} diff --git a/directus-extension/app/root.tsx b/directus-extension/app/root.tsx new file mode 100644 index 00000000..54bc2cdf --- /dev/null +++ b/directus-extension/app/root.tsx @@ -0,0 +1,36 @@ +import { cssBundleHref } from "@remix-run/css-bundle"; +import type { LinksFunction } from "@remix-run/node"; +import { + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from "@remix-run/react"; +import type { EndpointExtensionContext } from "@directus/types"; + +export interface AppLoadContext extends EndpointExtensionContext {} + +export const links: LinksFunction = () => [ + ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []), +]; + +export default function App() { + return ( + + + + + + + + + + + + + + + ); +} diff --git a/directus-extension/app/routes/_index.tsx b/directus-extension/app/routes/_index.tsx new file mode 100644 index 00000000..e97841d1 --- /dev/null +++ b/directus-extension/app/routes/_index.tsx @@ -0,0 +1,48 @@ +import { + json, + type LoaderFunctionArgs, + type MetaFunction, +} from "@remix-run/node"; +import type { ItemsService as TItemsService } from "@directus/api/services/items"; +import { Link, useLoaderData } from "@remix-run/react"; +import type { Posts } from "../types"; +export const meta: MetaFunction = () => { + return [ + { title: "New Remix App" }, + { name: "description", content: "Welcome to Remix!" }, + ]; +}; + +export async function loader({ context }: LoaderFunctionArgs) { + const ItemsService = (context.services as any) + .ItemsService as typeof TItemsService; + const itemsService = new ItemsService("posts", { + schema: context.schema as any, + accountability: { admin: true, role: "" }, + }); + + const posts = await itemsService.readByQuery({ + fields: ["id", "slug", "title"], + filter: { status: { _eq: "published" } }, + sort: ["-date_published"], + limit: -1, + }); + return json({ posts }); +} + +export default function Index() { + const { posts } = useLoaderData(); + + return ( +
+

My Blog

+
    + {posts.map((post) => ( +
  • + {post.title} +
  • + ))} +
+
+ ); +} diff --git a/directus-extension/app/routes/post.$slug._index.tsx b/directus-extension/app/routes/post.$slug._index.tsx new file mode 100644 index 00000000..6927bcba --- /dev/null +++ b/directus-extension/app/routes/post.$slug._index.tsx @@ -0,0 +1,47 @@ +import { type LoaderFunctionArgs } from "@remix-run/node"; +import type { Posts } from "~/types"; +import type { ItemsService as TItemsService } from "@directus/api/services/items"; +import { Link, useLoaderData } from "@remix-run/react"; + +export async function loader({ context, params }: LoaderFunctionArgs) { + const ItemsService = (context.services as any) + .ItemsService as typeof TItemsService; + const itemsService = new ItemsService("posts", { + schema: context.schema as any, + accountability: { admin: true, role: "" }, + }); + + const [post] = await itemsService.readByQuery({ + limit: 1, + filter: { slug: { _eq: params.slug }, status: { _eq: "published" } }, + fields: ["*"], + }); + + if (!post) + throw new Response(null, { + status: 404, + statusText: "Not Found", + }); + + return { post }; +} + +const formatter = new Intl.DateTimeFormat("en", { dateStyle: "medium" }); +export default function Post() { + const { post } = useLoaderData(); + return ( +
+ Back +

{post.title}

+

+ Published: {formatter.format(Number(new Date(post.date_published)))} +

+ {post.title} +
+
+ ); +} diff --git a/directus-extension/app/types.tsx b/directus-extension/app/types.tsx new file mode 100644 index 00000000..38a4dcdb --- /dev/null +++ b/directus-extension/app/types.tsx @@ -0,0 +1,10 @@ +export interface Posts { + id: number; + slug: string; + title: string; + status: "draft" | "published"; + content: string; + image: string; + date_published: string; + excerpt: string; +} diff --git a/directus-extension/package.json b/directus-extension/package.json new file mode 100644 index 00000000..0cbd69b8 --- /dev/null +++ b/directus-extension/package.json @@ -0,0 +1,52 @@ +{ + "name": "remix-directus", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "build": "run-s build:*", + "build:remix": "remix build", + "build:extension": "npm run build --workspace=remix-frontend", + "dev": "remix dev --manual -c \"run-p site-dev:*\"", + "site-dev:extension": "npm run dev --workspace=remix-frontend", + "site-dev:directus": "directus start", + "start": "directus start", + "typecheck": "tsc", + "postinstall": "patch-package" + }, + "dependencies": { + "@remix-run/css-bundle": "^2.0.1", + "@remix-run/node": "^2.0.1", + "@remix-run/react": "^2.0.1", + "@remix-run/serve": "^2.0.1", + "directus": "^10.6.3", + "isbot": "^3.6.8", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-markdown": "^9.0.0", + "react-syntax-highlighter": "^15.5.0", + "rehype-raw": "^7.0.0", + "rehype-slug": "^6.0.0", + "remark-gfm": "^4.0.0", + "sqlite3": "^5.1.6", + "unified": "^11.0.3" + }, + "devDependencies": { + "@remix-run/dev": "^2.0.1", + "@remix-run/eslint-config": "^2.0.1", + "@types/react": "^18.2.20", + "@types/react-dom": "^18.2.7", + "@types/react-syntax-highlighter": "^15.5.7", + "directus-extension-seed": "^2.0.4", + "eslint": "^8.38.0", + "npm-run-all": "^4.1.5", + "patch-package": "^8.0.0", + "typescript": "^5.1.6" + }, + "engines": { + "node": ">=18.0.0" + }, + "workspaces": [ + "remix-frontend" + ] +} diff --git a/directus-extension/patches/vue+3.3.4.patch b/directus-extension/patches/vue+3.3.4.patch new file mode 100644 index 00000000..db65f2eb --- /dev/null +++ b/directus-extension/patches/vue+3.3.4.patch @@ -0,0 +1,26 @@ +diff --git a/node_modules/vue/jsx-runtime/index.d.ts b/node_modules/vue/jsx-runtime/index.d.ts +index a44382c..93bcc3a 100644 +--- a/node_modules/vue/jsx-runtime/index.d.ts ++++ b/node_modules/vue/jsx-runtime/index.d.ts +@@ -12,7 +12,7 @@ import type { + */ + export { h as jsx, h as jsxDEV, Fragment } from '@vue/runtime-dom' + +-export namespace JSX { ++export namespace JSX_Vue { + export interface Element extends VNode {} + export interface ElementClass { + $props: {} +diff --git a/node_modules/vue/jsx.d.ts b/node_modules/vue/jsx.d.ts +index afc1039..47f0942 100644 +--- a/node_modules/vue/jsx.d.ts ++++ b/node_modules/vue/jsx.d.ts +@@ -8,7 +8,7 @@ import type { + } from '@vue/runtime-dom' + + declare global { +- namespace JSX { ++ namespace JSX_Vue { + export interface Element extends VNode {} + export interface ElementClass { + $props: {} diff --git a/directus-extension/public/favicon.ico b/directus-extension/public/favicon.ico new file mode 100644 index 00000000..8830cf68 Binary files /dev/null and b/directus-extension/public/favicon.ico differ diff --git a/directus-extension/remix-frontend/.gitignore b/directus-extension/remix-frontend/.gitignore new file mode 100644 index 00000000..2e354c27 --- /dev/null +++ b/directus-extension/remix-frontend/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +node_modules +dist +index.js \ No newline at end of file diff --git a/directus-extension/remix-frontend/package.json b/directus-extension/remix-frontend/package.json new file mode 100644 index 00000000..e6ce0758 --- /dev/null +++ b/directus-extension/remix-frontend/package.json @@ -0,0 +1,32 @@ +{ + "name": "remix-frontend", + "description": "Please enter a description for your extension", + "icon": "extension", + "version": "1.0.0", + "keywords": [ + "directus", + "directus-extension", + "directus-custom-endpoint" + ], + "type": "module", + "directus:extension": { + "type": "endpoint", + "path": "../extensions/endpoints/remix-frontend/index.js", + "source": "src/index.ts", + "host": "^10.1.11" + }, + "scripts": { + "build": "directus-extension build", + "dev": "REMIX_ENV=development directus-extension build -w --no-minify" + }, + "devDependencies": { + "@directus/extensions-sdk": "10.1.11", + "@types/node": "^20.7.0", + "typescript": "^5.2.2" + }, + "dependencies": { + "@remix-run/express": "^2.0.1", + "@remix-run/node": "^2.0.1", + "serve-static": "^1.15.0" + } +} diff --git a/directus-extension/remix-frontend/src/handler.ts b/directus-extension/remix-frontend/src/handler.ts new file mode 100644 index 00000000..ad4999a1 --- /dev/null +++ b/directus-extension/remix-frontend/src/handler.ts @@ -0,0 +1,101 @@ +import { createRequestHandler } from "@remix-run/express"; +import * as path from "node:path"; +import serveStatic from "serve-static"; +import * as fs from "node:fs"; +import * as url from "node:url"; +import { broadcastDevReady } from "@remix-run/node"; +import type { Router } from "express"; +import type { EndpointExtensionContext } from "@directus/types"; + +/** @typedef {import('@remix-run/node').ServerBuild} ServerBuild */ + +const BUILD_PATH = path.resolve("./build/index.js"); +const VERSION_PATH = path.resolve("./build/version.txt"); +let initialBuild = await reimportServer(); + +const __dirname = process.cwd(); + +const serve = serveStatic(path.resolve(__dirname, "public"), { + maxAge: "1h", +}); +const serveBuild = serveStatic(path.resolve(__dirname, "public/build"), { + maxAge: "1y", + immutable: true, +}); + +function getLoadContext(context: EndpointExtensionContext) { + return (req: any) => { + return { + ...context, + schema: req.schema, + accountability: req.accountability, + }; + }; +} + +export async function handler( + router: Router, + context: EndpointExtensionContext +) { + const requestHandler = + process.env.REMIX_ENV === "development" + ? createDevRequestHandler(initialBuild, context) + : createRequestHandler({ + build: initialBuild, + getLoadContext: getLoadContext(context), + }); + router.all("*", (req, res, next) => { + // Handling for Directus URLs + if (req.url.startsWith("/auth/login") || req.url.startsWith("/admin")) { + return next(); + } + serveBuild(req, res, () => { + serve(req, res, () => { + requestHandler(req, res, next); + }); + }); + }); +} + +/** + * @returns {Promise} + */ +export async function reimportServer() { + const stat = fs.statSync(BUILD_PATH); + + // convert build path to URL for Windows compatibility with dynamic `import` + const BUILD_URL = url.pathToFileURL(BUILD_PATH).href; + + // use a timestamp query parameter to bust the import cache + return import(BUILD_URL + "?t=" + stat.mtimeMs); +} + +/** + * @param {ServerBuild} initialBuild + */ +function createDevRequestHandler( + b: typeof initialBuild, + context: EndpointExtensionContext +) { + let build = b; + async function handleServerUpdate() { + // 1. re-import the server build + build = await reimportServer(); + // 2. tell Remix that this app server is now up-to-date and ready + broadcastDevReady(build); + } + + fs.watch(VERSION_PATH, handleServerUpdate); + // wrap request handler to make sure its recreated with the latest build for every request + return async (req: any, res: any, next: any) => { + try { + return createRequestHandler({ + build, + mode: "development", + getLoadContext: getLoadContext(context), + })(req, res, next); + } catch (error) { + next(error); + } + }; +} diff --git a/directus-extension/remix-frontend/src/index.ts b/directus-extension/remix-frontend/src/index.ts new file mode 100644 index 00000000..53118222 --- /dev/null +++ b/directus-extension/remix-frontend/src/index.ts @@ -0,0 +1,7 @@ +import { defineEndpoint } from "@directus/extensions-sdk"; +import { handler } from "./handler"; + +export default defineEndpoint({ + id: "", + handler, +}); diff --git a/directus-extension/remix-frontend/tsconfig.json b/directus-extension/remix-frontend/tsconfig.json new file mode 100644 index 00000000..da188522 --- /dev/null +++ b/directus-extension/remix-frontend/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2019", "DOM"], + "module": "ES2022", + "moduleResolution": "node", + "strict": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "noImplicitAny": true, + "noImplicitThis": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUncheckedIndexedAccess": true, + "noUnusedParameters": true, + "alwaysStrict": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "resolveJsonModule": false, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "allowSyntheticDefaultImports": true, + "isolatedModules": true, + "rootDir": "./src" + }, + "include": ["./src/**/*.ts"] +} diff --git a/directus-extension/remix.config.js b/directus-extension/remix.config.js new file mode 100644 index 00000000..7fac2d30 --- /dev/null +++ b/directus-extension/remix.config.js @@ -0,0 +1,8 @@ +/** @type {import('@remix-run/dev').AppConfig} */ +export default { + ignoredRouteFiles: ["**/.*"], + // appDirectory: "app", + // assetsBuildDirectory: "public/build", + // publicPath: "/build/", + // serverBuildPath: "build/index.js", +}; diff --git a/directus-extension/remix.env.d.ts b/directus-extension/remix.env.d.ts new file mode 100644 index 00000000..dcf8c45e --- /dev/null +++ b/directus-extension/remix.env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/directus-extension/snapshot.yml b/directus-extension/snapshot.yml new file mode 100644 index 00000000..5839a039 --- /dev/null +++ b/directus-extension/snapshot.yml @@ -0,0 +1,524 @@ +version: 1 +directus: 13.1.1 +vendor: sqlite +collections: + - collection: posts + meta: + accountability: all + archive_app_filter: true + archive_field: status + archive_value: archived + collapse: open + collection: posts + color: null + display_template: '{{title}}' + group: null + hidden: false + icon: post_add + item_duplication_fields: null + note: null + preview_url: null + singleton: false + sort: null + sort_field: null + translations: null + unarchive_value: draft + schema: + name: posts +fields: + - collection: posts + field: content + type: text + meta: + collection: posts + conditions: null + display: null + display_options: null + field: content + group: null + hidden: false + interface: input-multiline + note: null + options: + folder: null + readonly: false + required: false + sort: 10 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: content + table: posts + data_type: text + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: posts + field: date_created + type: timestamp + meta: + collection: posts + conditions: null + display: datetime + display_options: + relative: true + field: date_created + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + sort: 6 + special: + - cast-timestamp + - date-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_created + table: posts + data_type: datetime + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: posts + field: date_published + type: dateTime + meta: + collection: posts + conditions: null + display: datetime + display_options: null + field: date_published + group: null + hidden: false + interface: datetime + note: null + options: null + readonly: false + required: false + sort: 8 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_published + table: posts + data_type: datetime + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: posts + field: date_updated + type: timestamp + meta: + collection: posts + conditions: null + display: datetime + display_options: + relative: true + field: date_updated + group: null + hidden: true + interface: datetime + note: null + options: null + readonly: true + required: false + sort: 7 + special: + - cast-timestamp + - date-updated + translations: null + validation: null + validation_message: null + width: half + schema: + name: date_updated + table: posts + data_type: datetime + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: posts + field: excerpt + type: text + meta: + collection: posts + conditions: null + display: null + display_options: null + field: excerpt + group: null + hidden: false + interface: input-multiline + note: null + options: + softLength: 160 + trim: true + readonly: false + required: false + sort: 11 + special: null + translations: null + validation: null + validation_message: null + width: full + schema: + name: excerpt + table: posts + data_type: text + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: posts + field: id + type: integer + meta: + collection: posts + conditions: null + display: null + display_options: null + field: id + group: null + hidden: true + interface: input + note: null + options: null + readonly: true + required: false + sort: 4 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: id + table: posts + data_type: integer + default_value: null + max_length: null + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_primary_key: true + is_generated: false + generation_expression: null + has_auto_increment: true + foreign_key_table: null + foreign_key_column: null + - collection: posts + field: image + type: uuid + meta: + collection: posts + conditions: null + display: null + display_options: null + field: image + group: null + hidden: false + interface: file-image + note: null + options: + folder: null + readonly: false + required: false + sort: 9 + special: + - file + translations: null + validation: null + validation_message: null + width: full + schema: + name: image + table: posts + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_files + foreign_key_column: id + - collection: posts + field: slug + type: string + meta: + collection: posts + conditions: null + display: null + display_options: null + field: slug + group: null + hidden: false + interface: input + note: null + options: + slug: true + readonly: false + required: true + sort: 2 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: slug + table: posts + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: true + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: posts + field: status + type: string + meta: + collection: posts + conditions: null + display: labels + display_options: + choices: + - text: $t:published + value: published + foreground: '#FFFFFF' + background: var(--primary) + - text: $t:draft + value: draft + foreground: '#18222F' + background: '#D3DAE4' + - text: $t:archived + value: archived + foreground: '#FFFFFF' + background: var(--warning) + showAsDot: true + field: status + group: null + hidden: false + interface: select-dropdown + note: null + options: + choices: + - text: $t:published + value: published + - text: $t:draft + value: draft + - text: $t:archived + value: archived + readonly: false + required: false + sort: 3 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: status + table: posts + data_type: varchar + default_value: draft + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: false + is_unique: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: posts + field: title + type: string + meta: + collection: posts + conditions: null + display: null + display_options: null + field: title + group: null + hidden: false + interface: input + note: null + options: null + readonly: false + required: true + sort: 1 + special: null + translations: null + validation: null + validation_message: null + width: half + schema: + name: title + table: posts + data_type: varchar + default_value: null + max_length: 255 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: null + foreign_key_column: null + - collection: posts + field: user_created + type: string + meta: + collection: posts + conditions: null + display: user + display_options: null + field: user_created + group: null + hidden: true + interface: select-dropdown-m2o + note: null + options: + template: '{{avatar.$thumbnail}} {{first_name}} {{last_name}}' + readonly: true + required: false + sort: 5 + special: + - user-created + translations: null + validation: null + validation_message: null + width: half + schema: + name: user_created + table: posts + data_type: char + default_value: null + max_length: 36 + numeric_precision: null + numeric_scale: null + is_nullable: true + is_unique: false + is_primary_key: false + is_generated: false + generation_expression: null + has_auto_increment: false + foreign_key_table: directus_users + foreign_key_column: id +relations: + - collection: posts + field: image + related_collection: directus_files + meta: + junction_field: null + many_collection: posts + many_field: image + one_allowed_collections: null + one_collection: directus_files + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: posts + column: image + foreign_key_table: directus_files + foreign_key_column: id + constraint_name: null + on_update: NO ACTION + on_delete: SET NULL + - collection: posts + field: user_created + related_collection: directus_users + meta: + junction_field: null + many_collection: posts + many_field: user_created + one_allowed_collections: null + one_collection: directus_users + one_collection_field: null + one_deselect_action: nullify + one_field: null + sort_field: null + schema: + table: posts + column: user_created + foreign_key_table: directus_users + foreign_key_column: id + constraint_name: null + on_update: NO ACTION + on_delete: NO ACTION diff --git a/directus-extension/tsconfig.json b/directus-extension/tsconfig.json new file mode 100644 index 00000000..3afc0fb5 --- /dev/null +++ b/directus-extension/tsconfig.json @@ -0,0 +1,23 @@ +{ + "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "isolatedModules": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "target": "ES2022", + "module": "ES2022", + "strict": true, + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./app/*"] + }, + + // Remix takes care of building everything in `remix build`. + "noEmit": true + } +}