diff --git a/.changeset/add-isnullable-multiselect.md b/.changeset/add-isnullable-multiselect.md deleted file mode 100644 index edf3088e6da..00000000000 --- a/.changeset/add-isnullable-multiselect.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@keystone-6/core': minor ---- - -Add `db.isNullable` support for multiselect field type, defaults to false diff --git a/.changeset/add-relationship-search.md b/.changeset/add-relationship-search.md new file mode 100644 index 00000000000..7d037994509 --- /dev/null +++ b/.changeset/add-relationship-search.md @@ -0,0 +1,5 @@ +--- +"@keystone-6/core": minor +--- + +Add support for searching relationship fields in the list view diff --git a/.changeset/config.json b/.changeset/config.json index a96ace8c24c..24654475a0b 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -5,6 +5,9 @@ "baseBranch": "main", "linked": [], "access": "public", + "privatePackages": { + "version": false + }, "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { "onlyUpdatePeerDependentsWhenOutOfRange": true } diff --git a/.changeset/contributors.json b/.changeset/contributors.json index 895f18cd532..06c011b28fd 100644 --- a/.changeset/contributors.json +++ b/.changeset/contributors.json @@ -4,6 +4,7 @@ "AliceRossa", "CarlQLange", "ChrisLaneAU", + "DavidMulder0", "DiesIrae", "DustinWoods", "Greenheart", @@ -30,6 +31,7 @@ "allcontributors[bot]", "austin047", "bartduisters", + "benderham", "bketelsen", "bladey", "borisno2", @@ -38,6 +40,7 @@ "dcousens", "dependabot", "dependabot[bot]", + "direisc", "dominikwilkowski", "duidae", "emmatown", @@ -60,6 +63,7 @@ "jossmac", "kennedybaird", "keystonejs-release-bot", + "kidneyweakx", "kporten", "lahirurane-rau", "leopoldkristjansson", diff --git a/.changeset/fix-bigint-validation.md b/.changeset/fix-bigint-validation.md deleted file mode 100644 index 2c49d9e94d8..00000000000 --- a/.changeset/fix-bigint-validation.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@keystone-6/core': patch ---- - -Fix bigInt field type to throw if `defaultValue: { kind: 'autoincrement' }` and `validation.isRequired` is set diff --git a/.changeset/fix-hide-ui.md b/.changeset/fix-hide-ui.md deleted file mode 100644 index a0fa2c69c6f..00000000000 --- a/.changeset/fix-hide-ui.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@keystone-6/core': patch ---- - -Fix `list.ui.hide*` defaulting to false when GraphQL is omitted diff --git a/.changeset/fix-keystone-prisma.md b/.changeset/fix-keystone-prisma.md deleted file mode 100644 index 4b71b10fec5..00000000000 --- a/.changeset/fix-keystone-prisma.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@keystone-6/core': patch ---- - -Fix `keystone prisma ...` not returning the same error code as the Prisma engine diff --git a/.changeset/fix-search.md b/.changeset/fix-search.md new file mode 100644 index 00000000000..dfdfd535551 --- /dev/null +++ b/.changeset/fix-search.md @@ -0,0 +1,5 @@ +--- +"@keystone-6/core": patch +--- + +Fix list view ignoring `.ui.listView.searchFields` diff --git a/.changeset/fix-uuid-rel.md b/.changeset/fix-uuid-rel.md deleted file mode 100644 index dc50719a017..00000000000 --- a/.changeset/fix-uuid-rel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@keystone-6/core': patch ---- - -Fix malformed uuid's from breaking relationship filters when using POSTGRESQL diff --git a/.changeset/light-lamps-eat.md b/.changeset/light-lamps-eat.md deleted file mode 100644 index 208bfb90424..00000000000 --- a/.changeset/light-lamps-eat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@keystone-6/core': minor ---- - -Add exports for internal AdminUI pagination components `Pagination`, `PaginationLabel` and `usePaginationParams` for use in custom pages diff --git a/.changeset/split-document-field.md b/.changeset/split-document-field.md deleted file mode 100644 index 19265a633ed..00000000000 --- a/.changeset/split-document-field.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@keystone-6/fields-document': minor ---- - -Fix `@keystone-6/fields-document` package breaking when compiling in SSR environments (#8717) diff --git a/.changeset/update-field-hooks.md b/.changeset/update-field-hooks.md deleted file mode 100644 index b0ea3bd7d5f..00000000000 --- a/.changeset/update-field-hooks.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@keystone-6/core": patch ---- - -Update built-in fields to use newer validate hook syntax diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index aab37766eae..2b501e76da4 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -1,5 +1,5 @@ { "packages": ["packages/*", "design-system/packages/*"], "sandboxes": ["/tests/sandbox"], - "node": "20" + "node": "22" } diff --git a/.github/actions/ci-setup-examples/action.yml b/.github/actions/ci-setup-examples/action.yml index 3bb0f9953af..27cf91d97ad 100644 --- a/.github/actions/ci-setup-examples/action.yml +++ b/.github/actions/ci-setup-examples/action.yml @@ -8,7 +8,7 @@ runs: - uses: actions/setup-node@main with: # preferably lts/*, but we hit API limits when querying that - node-version: 20 + node-version: 22 registry-url: 'https://registry.npmjs.org' cache: pnpm diff --git a/.github/actions/ci-setup/action.yml b/.github/actions/ci-setup/action.yml index 27babea7f8c..c642554b8fe 100644 --- a/.github/actions/ci-setup/action.yml +++ b/.github/actions/ci-setup/action.yml @@ -8,7 +8,7 @@ runs: - uses: actions/setup-node@main with: # preferably lts/*, but we hit API limits when querying that - node-version: 20 + node-version: 22 registry-url: 'https://registry.npmjs.org' cache: pnpm diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9ebb22e21df..e927c2dff09 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -7,6 +7,7 @@ jobs: publish: name: Publish runs-on: ubuntu-latest + timeout-minutes: 10 environment: Release steps: - uses: actions/checkout@main diff --git a/.github/workflows/publish_snapshot.yml b/.github/workflows/publish_snapshot.yml index cfbe618997c..5f392e45da2 100644 --- a/.github/workflows/publish_snapshot.yml +++ b/.github/workflows/publish_snapshot.yml @@ -11,6 +11,7 @@ jobs: publish_snapshot: name: Publish (Snapshot) runs-on: ubuntu-latest + timeout-minutes: 10 environment: Release steps: - uses: actions/checkout@main diff --git a/.github/workflows/tests_ci.yml b/.github/workflows/tests_ci.yml index 9a3d129ab0a..bcdaf875c8c 100644 --- a/.github/workflows/tests_ci.yml +++ b/.github/workflows/tests_ci.yml @@ -18,6 +18,7 @@ jobs: linting: name: Linting runs-on: ubuntu-latest + timeout-minutes: 10 steps: - uses: actions/checkout@main - uses: ./.github/actions/ci-setup @@ -31,9 +32,21 @@ jobs: - name: Prisma Filters run: pnpm test:filters + linting-examples: + name: Linting + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@main + - uses: ./.github/actions/ci-setup-examples + + - name: TypeScript + run: pnpm test:types + unit_tests: name: Package Unit Tests runs-on: ubuntu-latest + timeout-minutes: 10 steps: - uses: actions/checkout@main - uses: ./.github/actions/ci-setup @@ -44,9 +57,10 @@ jobs: graphql_api_tests_postgresql: name: API Tests PostgreSQL runs-on: ubuntu-latest + timeout-minutes: 10 services: postgres: - image: postgres:16 + image: postgres:17 env: POSTGRES_USER: testuser POSTGRES_PASSWORD: testpass @@ -69,6 +83,7 @@ jobs: graphql_api_tests_sqlite: name: API Tests SQLite runs-on: ubuntu-latest + timeout-minutes: 10 strategy: fail-fast: false matrix: @@ -85,9 +100,10 @@ jobs: graphql_api_tests_mysql: name: API Tests MySQL runs-on: ubuntu-latest + timeout-minutes: 10 services: mysql: - image: mariadb:11.4 + image: mariadb:11.6 env: MYSQL_ROOT_PASSWORD: testpass ports: @@ -108,9 +124,10 @@ jobs: field_crud_tests_postgresql: name: Field CRUD Tests PostgreSQL runs-on: ubuntu-latest + timeout-minutes: 10 services: postgres: - image: postgres:16 + image: postgres:17 env: POSTGRES_USER: testuser POSTGRES_PASSWORD: testpass @@ -139,6 +156,7 @@ jobs: field_crud_tests_sqlite: name: Field CRUD Tests SQLite runs-on: ubuntu-latest + timeout-minutes: 10 steps: - uses: actions/checkout@main - uses: ./.github/actions/ci-setup @@ -161,9 +179,10 @@ jobs: field_crud_tests_mysql: name: Field CRUD Tests MySQL runs-on: ubuntu-latest + timeout-minutes: 10 services: mysql: - image: mariadb:11.4 + image: mariadb:11.6 env: MYSQL_ROOT_PASSWORD: testpass ports: @@ -190,6 +209,7 @@ jobs: examples_tests: name: Testing example project runs-on: ubuntu-latest + timeout-minutes: 10 steps: - uses: actions/checkout@main - uses: ./.github/actions/ci-setup-examples @@ -200,6 +220,7 @@ jobs: examples_next_app_build: name: Ensure Next in App directory builds runs-on: ubuntu-latest + timeout-minutes: 10 steps: - uses: actions/checkout@main - uses: ./.github/actions/ci-setup-examples @@ -210,6 +231,7 @@ jobs: examples_smoke_tests: name: Smoke Tests For Examples runs-on: ubuntu-latest + timeout-minutes: 10 env: DATABASE_URL: file:./test.db strategy: @@ -251,6 +273,7 @@ jobs: admin_ui_integration_tests: name: Integration tests for Admin UI runs-on: ubuntu-latest + timeout-minutes: 10 env: DATABASE_URL: file:./test.db strategy: diff --git a/.github/workflows/tests_ci_windows.yml b/.github/workflows/tests_ci_windows.yml index e5375a1bb60..656c9c42ab6 100644 --- a/.github/workflows/tests_ci_windows.yml +++ b/.github/workflows/tests_ci_windows.yml @@ -11,6 +11,7 @@ jobs: unit_tests: name: Package Unit Tests runs-on: windows-latest + timeout-minutes: 10 steps: - uses: actions/checkout@main - uses: ./.github/actions/ci-setup diff --git a/.github/workflows/website_preview.yml b/.github/workflows/website_preview.yml index 09207067651..edcd99d50f2 100644 --- a/.github/workflows/website_preview.yml +++ b/.github/workflows/website_preview.yml @@ -12,6 +12,7 @@ jobs: VERCEL_ORG_ID: ${{ secrets.VERCEL_TEAM_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} runs-on: ubuntu-latest + timeout-minutes: 10 steps: - uses: actions/checkout@main - uses: ./.github/actions/ci-setup diff --git a/.github/workflows/website_production.yml b/.github/workflows/website_production.yml index 3c78c6a4e6a..c6e0b1b6c1e 100644 --- a/.github/workflows/website_production.yml +++ b/.github/workflows/website_production.yml @@ -15,6 +15,7 @@ jobs: VERCEL_ORG_ID: ${{ secrets.VERCEL_TEAM_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} runs-on: ubuntu-latest + timeout-minutes: 10 steps: - uses: actions/checkout@main - uses: ./.github/actions/ci-setup diff --git a/.gitignore b/.gitignore index 9a1df743cce..f5cea221f7b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ dist/ node_modules/ *.db +.env # ts-gql __generated__ @@ -10,3 +11,5 @@ __generated__ # system .DS_Store *.vscode + +.pnpm-store diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000000..6c2b9be4c4f --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +link-workspace-packages=true +prefer-workspace-packages=true diff --git a/LICENSE b/LICENSE index 2762e648976..8f0e5cd3917 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Thinkmill Labs Pty Ltd +Copyright (c) 2024 Thinkmill Labs Pty Ltd Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 8e401a59fdd..54b63a1a1e9 100644 --- a/README.md +++ b/README.md @@ -87,4 +87,4 @@ For vulnerability reporting, please refer to our [security policy](/SECURITY.md) -Copyright (c) 2023 [Thinkmill Labs](https://www.thinkmill.com.au/labs?utm_campaign=keystone-github) Pty Ltd. Licensed under the MIT License. +Copyright (c) 2024 [Thinkmill Labs](https://www.thinkmill.com.au/labs?utm_campaign=keystone-github) Pty Ltd. Licensed under the MIT License. diff --git a/design-system/packages/button/package.json b/design-system/packages/button/package.json index 4566143100b..2522f425f13 100644 --- a/design-system/packages/button/package.json +++ b/design-system/packages/button/package.json @@ -12,14 +12,14 @@ "./package.json": "./package.json" }, "devDependencies": { - "@types/react": "catalog:" + "@types/react": "^18.3.3" }, "dependencies": { "@babel/runtime": "^7.24.7", "@keystone-ui/core": "workspace:^", "@keystone-ui/icons": "workspace:^", "@keystone-ui/loading": "workspace:^", - "react": "catalog:" + "react": "^18.3.1" }, "repository": "https://github.com/keystonejs/keystone/tree/main/design-system/packages/button" } diff --git a/design-system/packages/core/package.json b/design-system/packages/core/package.json index e4e0d5d09eb..5fb189129e1 100644 --- a/design-system/packages/core/package.json +++ b/design-system/packages/core/package.json @@ -12,13 +12,13 @@ "./package.json": "./package.json" }, "devDependencies": { - "@types/react": "catalog:", - "react": "catalog:", - "react-dom": "catalog:" + "@types/react": "^18.3.3", + "react": "^18.3.1", + "react-dom": "^18.3.1" }, "peerDependencies": { - "react": "catalog:", - "react-dom": "catalog:" + "react": "^18.3.1", + "react-dom": "^18.3.1" }, "dependencies": { "@babel/runtime": "^7.24.7", diff --git a/design-system/packages/core/src/components/Box.tsx b/design-system/packages/core/src/components/Box.tsx index 68abdc3c192..998cc699523 100644 --- a/design-system/packages/core/src/components/Box.tsx +++ b/design-system/packages/core/src/components/Box.tsx @@ -181,10 +181,10 @@ function useRadii ( { rounding, roundingTop, roundingRight, roundingBottom, roundingLeft }: RadiiProps, { radii }: Theme ) { - let borderBottomLeftRadius = roundingBottom || roundingLeft || rounding - let borderBottomRightRadius = roundingBottom || roundingRight || rounding - let borderTopLeftRadius = roundingTop || roundingLeft || rounding - let borderTopRightRadius = roundingTop || roundingRight || rounding + const borderBottomLeftRadius = roundingBottom || roundingLeft || rounding + const borderBottomRightRadius = roundingBottom || roundingRight || rounding + const borderTopLeftRadius = roundingTop || roundingLeft || rounding + const borderTopRightRadius = roundingTop || roundingRight || rounding return { borderBottomLeftRadius: @@ -208,10 +208,10 @@ function usePadding ( }: PaddingProps, { spacing }: Theme ) { - let pb = paddingBottom || paddingY || padding - let pt = paddingTop || paddingY || padding - let pl = paddingLeft || paddingX || padding - let pr = paddingRight || paddingX || padding + const pb = paddingBottom || paddingY || padding + const pt = paddingTop || paddingY || padding + const pl = paddingLeft || paddingX || padding + const pr = paddingRight || paddingX || padding return { paddingBottom: pb && mapResponsiveProp(pb, spacing), @@ -225,10 +225,10 @@ function useMargin ( { margin, marginTop, marginRight, marginBottom, marginLeft, marginY, marginX }: MarginProps, { spacing }: Theme ) { - let mb = marginBottom || marginY || margin - let mt = marginTop || marginY || margin - let ml = marginLeft || marginX || margin - let mr = marginRight || marginX || margin + const mb = marginBottom || marginY || margin + const mt = marginTop || marginY || margin + const ml = marginLeft || marginX || margin + const mr = marginRight || marginX || margin return { marginBottom: mb && mapResponsiveProp(mb, spacing), diff --git a/design-system/packages/core/src/components/Link.tsx b/design-system/packages/core/src/components/Link.tsx index df3321ffb89..d23cc6b032c 100644 --- a/design-system/packages/core/src/components/Link.tsx +++ b/design-system/packages/core/src/components/Link.tsx @@ -5,7 +5,7 @@ import { jsx } from '../emotion' import { useTheme } from '../theme' import { forwardRefWithAs } from '../utils' -export const Link = forwardRefWithAs<'a', {}>(({ as: Tag = 'a', ...props }, ref) => { +export const Link = forwardRefWithAs<'a', unknown>(({ as: Tag = 'a', ...props }, ref) => { const { typography, colors } = useTheme() const styles = { diff --git a/design-system/packages/fields/package.json b/design-system/packages/fields/package.json index 5fe7a755d5f..625081f0026 100644 --- a/design-system/packages/fields/package.json +++ b/design-system/packages/fields/package.json @@ -12,17 +12,17 @@ "./package.json": "./package.json" }, "devDependencies": { - "@types/react": "catalog:" + "@types/react": "^18.3.3" }, "dependencies": { "@babel/runtime": "^7.24.7", "@keystone-ui/core": "workspace:^", "@keystone-ui/icons": "workspace:^", "@keystone-ui/popover": "workspace:^", - "date-fns": "^3.0.0", - "react": "catalog:", - "react-day-picker": "^8.0.4", - "react-dom": "catalog:", + "date-fns": "^4.0.0", + "react": "^18.3.1", + "react-day-picker": "^9.0.0", + "react-dom": "^18.3.1", "react-focus-lock": "^2.7.1", "react-select": "^5.2.1" }, diff --git a/design-system/packages/fields/src/FieldContainer.tsx b/design-system/packages/fields/src/FieldContainer.tsx index a14e34b4061..6bef2fcf039 100644 --- a/design-system/packages/fields/src/FieldContainer.tsx +++ b/design-system/packages/fields/src/FieldContainer.tsx @@ -2,6 +2,6 @@ /** @jsx jsx */ import { jsx, forwardRefWithAs } from '@keystone-ui/core' -export const FieldContainer = forwardRefWithAs<'div', {}>(({ as: Tag = 'div', ...props }, ref) => { +export const FieldContainer = forwardRefWithAs<'div', unknown>(({ as: Tag = 'div', ...props }, ref) => { return }) diff --git a/design-system/packages/fields/src/Select.tsx b/design-system/packages/fields/src/Select.tsx index c643bfd7583..406ba3038bf 100644 --- a/design-system/packages/fields/src/Select.tsx +++ b/design-system/packages/fields/src/Select.tsx @@ -167,6 +167,7 @@ export function MultiSelect ({ inputId={id} styles={composedStyles} value={value} + filterOption={null} onChange={value => { if (!value) { onChange([]) diff --git a/design-system/packages/fields/src/Switch.tsx b/design-system/packages/fields/src/Switch.tsx index 290c331c8d9..ec790764768 100644 --- a/design-system/packages/fields/src/Switch.tsx +++ b/design-system/packages/fields/src/Switch.tsx @@ -47,7 +47,7 @@ type SwitchControlProps = { export const SwitchControl = forwardRef( ({ a11yLabels = { on: 'On', off: 'Off' }, checked = false, onChange, ...props }, ref) => { - let onClick = () => { + const onClick = () => { if (onChange) { onChange(!checked) } @@ -64,10 +64,10 @@ export const SwitchControl = forwardRef( const Button = forwardRef((props, ref) => { const { animation, fields, sizing } = useTheme() - let gutter = 3 - let trackHeight = sizing.xsmall + gutter - let trackWidth = trackHeight * 2 - 2 * gutter - let handleSize = trackHeight - gutter * 2 + const gutter = 3 + const trackHeight = sizing.xsmall + gutter + const trackWidth = trackHeight * 2 - 2 * gutter + const handleSize = trackHeight - gutter * 2 return ( { - let Icon = forwardRef( + const Icon = forwardRef( ({ size = 'medium', color, ...props }: IconProps, ref: any) => { const resolvedSize = typeof size === 'number' ? size : mapResponsiveProp(size, sizeMap) diff --git a/design-system/packages/loading/package.json b/design-system/packages/loading/package.json index 9cd72e5df7a..161bef8e560 100644 --- a/design-system/packages/loading/package.json +++ b/design-system/packages/loading/package.json @@ -12,12 +12,12 @@ "./package.json": "./package.json" }, "devDependencies": { - "@types/react": "catalog:" + "@types/react": "^18.3.3" }, "dependencies": { "@babel/runtime": "^7.24.7", "@keystone-ui/core": "workspace:^", - "react": "catalog:" + "react": "^18.3.1" }, "repository": "https://github.com/keystonejs/keystone/tree/main/design-system/packages/loading" } diff --git a/design-system/packages/modals/package.json b/design-system/packages/modals/package.json index 7ad7a70e4f4..0ec62b0f393 100644 --- a/design-system/packages/modals/package.json +++ b/design-system/packages/modals/package.json @@ -12,15 +12,15 @@ "./package.json": "./package.json" }, "devDependencies": { - "@types/react": "catalog:", - "@types/react-transition-group": "4.4.10" + "@types/react": "^18.3.3", + "@types/react-transition-group": "4.4.11" }, "dependencies": { "@babel/runtime": "^7.24.7", "@keystone-ui/button": "workspace:^", "@keystone-ui/core": "workspace:^", - "react": "catalog:", - "react-dom": "catalog:", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-focus-lock": "^2.7.1", "react-remove-scroll": "^2.4.3", "react-transition-group": "^4.4.2" diff --git a/design-system/packages/modals/src/DrawerBase.tsx b/design-system/packages/modals/src/DrawerBase.tsx index 3bcfa181c72..64faa073b3b 100644 --- a/design-system/packages/modals/src/DrawerBase.tsx +++ b/design-system/packages/modals/src/DrawerBase.tsx @@ -51,7 +51,7 @@ export const DrawerBase = ({ const uniqueKey = makeId('drawer', id) // sync drawer state - let drawerDepth = useDrawerManager(uniqueKey) + const drawerDepth = useDrawerManager(uniqueKey) const onKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape' && !event.defaultPrevented) { @@ -71,7 +71,7 @@ export const DrawerBase = ({ let Tag: 'div' | 'form' = 'div' if (onSubmit) { Tag = 'form' - let oldOnSubmit = onSubmit + const oldOnSubmit = onSubmit // @ts-expect-error onSubmit = (event: any) => { if (!event.defaultPrevented) { @@ -134,8 +134,8 @@ export const DrawerBase = ({ // ------------------------------ function getDialogTransition (depth: number) { - let scaleInc = 0.05 - let transformValue = `scale(${1 - scaleInc * depth}) translateX(-${depth * 40}px)` + const scaleInc = 0.05 + const transformValue = `scale(${1 - scaleInc * depth}) translateX(-${depth * 40}px)` return { entering: { transform: 'translateX(100%)' }, diff --git a/design-system/packages/modals/src/DrawerController.tsx b/design-system/packages/modals/src/DrawerController.tsx index 2f8be1a53be..f5958a57c15 100644 --- a/design-system/packages/modals/src/DrawerController.tsx +++ b/design-system/packages/modals/src/DrawerController.tsx @@ -13,7 +13,7 @@ const DrawerControllerContext = React.createContext(null export const DrawerControllerContextProvider = DrawerControllerContext.Provider export const useDrawerControllerContext = () => { - let context = useContext(DrawerControllerContext) + const context = useContext(DrawerControllerContext) if (!context) { throw new Error( 'Drawers must be wrapped in a . You should generally do this outside of the component that renders the or .' diff --git a/design-system/packages/modals/src/drawer-context.tsx b/design-system/packages/modals/src/drawer-context.tsx index 243a5126f41..c751860ac40 100644 --- a/design-system/packages/modals/src/drawer-context.tsx +++ b/design-system/packages/modals/src/drawer-context.tsx @@ -9,14 +9,14 @@ export type ModalState = { const ModalContext = React.createContext(null) export const DrawerProvider = ({ children }: { children: ReactNode }) => { - let [drawerStack, setDrawerStack] = useState([]) + const [drawerStack, setDrawerStack] = useState([]) const pushToDrawerStack = useCallback((key: string) => { setDrawerStack(stack => [...stack, key]) }, []) const popFromDrawerStack = useCallback(() => { setDrawerStack(stack => { - let less = stack.slice(0, -1) + const less = stack.slice(0, -1) return less }) }, []) @@ -50,7 +50,7 @@ export const useDrawerManager = (uniqueKey: string) => { }, []) // the last key in the array is the "top" modal visually, so the depth is the inverse index // be careful not to mutate the stack - let depth = modalState.drawerStack.slice().reverse().indexOf(uniqueKey) + const depth = modalState.drawerStack.slice().reverse().indexOf(uniqueKey) // if it's not in the stack already, // we know that it should be the last drawer in the stack but the effect hasn't happened yet // so we need to make the depth 0 so the depth is correct even though the effect hasn't happened yet diff --git a/design-system/packages/notice/package.json b/design-system/packages/notice/package.json index a222ed7e3c8..bb87940b905 100644 --- a/design-system/packages/notice/package.json +++ b/design-system/packages/notice/package.json @@ -12,14 +12,14 @@ "./package.json": "./package.json" }, "devDependencies": { - "@types/react": "catalog:" + "@types/react": "^18.3.3" }, "dependencies": { "@babel/runtime": "^7.24.7", "@keystone-ui/button": "workspace:^", "@keystone-ui/core": "workspace:^", "@keystone-ui/icons": "workspace:^", - "react": "catalog:" + "react": "^18.3.1" }, "repository": "https://github.com/keystonejs/keystone/tree/main/design-system/packages/notice" } diff --git a/design-system/packages/options/package.json b/design-system/packages/options/package.json index 38022dd136d..38e4ec075b4 100644 --- a/design-system/packages/options/package.json +++ b/design-system/packages/options/package.json @@ -12,12 +12,12 @@ "./package.json": "./package.json" }, "peerDependencies": { - "react": "catalog:", - "react-dom": "catalog:" + "react": "^18.3.1", + "react-dom": "^18.3.1" }, "devDependencies": { - "react": "catalog:", - "react-dom": "catalog:" + "react": "^18.3.1", + "react-dom": "^18.3.1" }, "dependencies": { "@babel/runtime": "^7.24.7", diff --git a/design-system/packages/pill/package.json b/design-system/packages/pill/package.json index a6a41a74e62..4cfa2e947f7 100644 --- a/design-system/packages/pill/package.json +++ b/design-system/packages/pill/package.json @@ -12,10 +12,10 @@ "./package.json": "./package.json" }, "peerDependencies": { - "react": "catalog:" + "react": "^18.3.1" }, "devDependencies": { - "react": "catalog:" + "react": "^18.3.1" }, "dependencies": { "@babel/runtime": "^7.24.7", diff --git a/design-system/packages/popover/package.json b/design-system/packages/popover/package.json index 2dcf0d766b6..a7164d3d0b6 100644 --- a/design-system/packages/popover/package.json +++ b/design-system/packages/popover/package.json @@ -12,12 +12,12 @@ "./package.json": "./package.json" }, "peerDependencies": { - "react": "catalog:", - "react-dom": "catalog:" + "react": "^18.3.1", + "react-dom": "^18.3.1" }, "devDependencies": { - "react": "catalog:", - "react-dom": "catalog:" + "react": "^18.3.1", + "react-dom": "^18.3.1" }, "dependencies": { "@babel/runtime": "^7.24.7", diff --git a/design-system/packages/popover/src/Popover.tsx b/design-system/packages/popover/src/Popover.tsx index 4f6a235c8cd..78477004491 100644 --- a/design-system/packages/popover/src/Popover.tsx +++ b/design-system/packages/popover/src/Popover.tsx @@ -262,7 +262,7 @@ type UseClickOutsideProps = { const useClickOutside = ({ handler, elements, listenWhen }: UseClickOutsideProps) => { useEffect(() => { if (listenWhen) { - let handleMouseDown = (event: MouseEvent) => { + const handleMouseDown = (event: MouseEvent) => { // bail on mouse down "inside" any of the provided elements if (elements.some(el => el && el.contains(event.target as Node))) { return @@ -300,8 +300,8 @@ const useKeyPress = ({ // add event listeners useEffect(() => { - let target = targetElement || document.body - let onDown = (event: KeyboardEvent) => { + const target = targetElement || document.body + const onDown = (event: KeyboardEvent) => { if (event.key === targetKey) { setKeyPressed(true) @@ -310,7 +310,7 @@ const useKeyPress = ({ } } } - let onUp = (event: KeyboardEvent) => { + const onUp = (event: KeyboardEvent) => { if (event.key === targetKey) { setKeyPressed(false) diff --git a/design-system/packages/segmented-control/package.json b/design-system/packages/segmented-control/package.json index 40c2182a763..12fdfb60c44 100644 --- a/design-system/packages/segmented-control/package.json +++ b/design-system/packages/segmented-control/package.json @@ -12,15 +12,15 @@ "./package.json": "./package.json" }, "devDependencies": { - "@types/react": "catalog:", - "react": "catalog:" + "@types/react": "^18.3.3", + "react": "^18.3.1" }, "dependencies": { "@babel/runtime": "^7.24.7", "@keystone-ui/core": "workspace:^" }, "peerDependencies": { - "react": "catalog:" + "react": "^18.3.1" }, "repository": "https://github.com/keystonejs/keystone/tree/main/design-system/packages/segmented-control" } diff --git a/design-system/packages/segmented-control/src/SegmentedControl.tsx b/design-system/packages/segmented-control/src/SegmentedControl.tsx index 0c78b4c1189..1a6b22ce812 100644 --- a/design-system/packages/segmented-control/src/SegmentedControl.tsx +++ b/design-system/packages/segmented-control/src/SegmentedControl.tsx @@ -82,8 +82,8 @@ export const SegmentedControl = ({ // Animate the selected segment indicator useEffect(() => { if (animate && rootRef.current instanceof HTMLElement) { - let nodes = Array.from(rootRef.current.children) - let selected = selectedIndex !== undefined && nodes[selectedIndex] + const nodes = Array.from(rootRef.current.children) + const selected = selectedIndex !== undefined && nodes[selectedIndex] let rootRect let nodeRect = { height: 0, width: 0, left: 0, top: 0 } let offsetLeft diff --git a/design-system/packages/toast/package.json b/design-system/packages/toast/package.json index 6c76869eca8..76f139fceb0 100644 --- a/design-system/packages/toast/package.json +++ b/design-system/packages/toast/package.json @@ -12,8 +12,8 @@ "./package.json": "./package.json" }, "devDependencies": { - "@types/react": "catalog:", - "react": "catalog:" + "@types/react": "^18.3.3", + "react": "^18.3.1" }, "dependencies": { "@babel/runtime": "^7.24.7", @@ -21,7 +21,7 @@ "@keystone-ui/icons": "workspace:^" }, "peerDependencies": { - "react": "catalog:" + "react": "^18.3.1" }, "repository": "https://github.com/keystonejs/keystone/tree/main/design-system/packages/toast" } diff --git a/design-system/packages/toast/src/Toast.tsx b/design-system/packages/toast/src/Toast.tsx index 6357cfc4cb4..5311ee39260 100644 --- a/design-system/packages/toast/src/Toast.tsx +++ b/design-system/packages/toast/src/Toast.tsx @@ -29,7 +29,7 @@ export function ToastProvider ({ children }: { children: ReactNode }) { } // populate defaults and update state - let toast = populateDefaults(options) + const toast = populateDefaults(options) return [...currentStack, toast] }) }, @@ -68,7 +68,7 @@ export function ToastProvider ({ children }: { children: ReactNode }) { // ------------------------------ let idCount = -1 -let genId = () => ++idCount +const genId = () => ++idCount function populateDefaults (props: ToastProps): ToastPropsExact { return { title: props.title, diff --git a/design-system/packages/tooltip/package.json b/design-system/packages/tooltip/package.json index e29f3351f94..e4bc853c3c5 100644 --- a/design-system/packages/tooltip/package.json +++ b/design-system/packages/tooltip/package.json @@ -12,12 +12,12 @@ "./package.json": "./package.json" }, "peerDependencies": { - "react": "catalog:", - "react-dom": "catalog:" + "react": "^18.3.1", + "react-dom": "^18.3.1" }, "devDependencies": { - "react": "catalog:", - "react-dom": "catalog:" + "react": "^18.3.1", + "react-dom": "^18.3.1" }, "dependencies": { "@babel/runtime": "^7.24.7", diff --git a/design-system/website/components/Navigation.tsx b/design-system/website/components/Navigation.tsx index f93a7e9ea14..c8e52158799 100644 --- a/design-system/website/components/Navigation.tsx +++ b/design-system/website/components/Navigation.tsx @@ -4,7 +4,7 @@ import { Fragment, type ReactNode } from 'react' import { jsx, useTheme } from '@keystone-ui/core' import Link from 'next/link' -import { useRouter } from 'next/router' +import { usePathname } from 'next/navigation' const Brand = () => { const { palette } = useTheme() @@ -37,8 +37,8 @@ const Section = ({ label, children }: SectionProps) => { type NavItemProps = { href: string, children: ReactNode } const NavItem = ({ href, children }: NavItemProps) => { const { palette, radii, spacing } = useTheme() - const router = useRouter() - const isSelected = router.pathname === href + const pathname = usePathname() + const isSelected = pathname === href return ( ) = } const BasicDatePicker = () => { - let [value, setValue] = useState('') + const [value, setValue] = useState('') return ( {value || 'no value'} diff --git a/design-system/website/pages/components/modals.tsx b/design-system/website/pages/components/modals.tsx index 52f1469474d..a7b72fdb112 100644 --- a/design-system/website/pages/components/modals.tsx +++ b/design-system/website/pages/components/modals.tsx @@ -8,9 +8,9 @@ import { Drawer, DrawerController, AlertDialog } from '@keystone-ui/modals' import { Page } from '../../components/Page' export default function ModalsPage () { - let [isNarrowOpen, setIsNarrowOpen] = useState(false) - let [isWideOpen, setIsWideOpen] = useState(false) - let [isAlertDialogOpen, setIsAlertDialogOpen] = useState(false) + const [isNarrowOpen, setIsNarrowOpen] = useState(false) + const [isWideOpen, setIsWideOpen] = useState(false) + const [isAlertDialogOpen, setIsAlertDialogOpen] = useState(false) return ( diff --git a/design-system/website/pages/components/options.tsx b/design-system/website/pages/components/options.tsx index 74e89b927c9..42fd94d88f0 100644 --- a/design-system/website/pages/components/options.tsx +++ b/design-system/website/pages/components/options.tsx @@ -17,7 +17,7 @@ const props: ComponentProps[] = [ ] export default function OptionsPage () { - let [value, setValue] = useState() + const [value, setValue] = useState() return ( Options diff --git a/docs/.env.example b/docs/.env.example new file mode 100644 index 00000000000..493eb515408 --- /dev/null +++ b/docs/.env.example @@ -0,0 +1 @@ +BUTTONDOWN_API_KEY= \ No newline at end of file diff --git a/docs/app/(site)/blog/[post]/page-client.tsx b/docs/app/(site)/blog/[post]/page-client.tsx new file mode 100644 index 00000000000..af2440272bd --- /dev/null +++ b/docs/app/(site)/blog/[post]/page-client.tsx @@ -0,0 +1,59 @@ +/** @jsxImportSource @emotion/react */ + +'use client' + +import Link from 'next/link' +import { useParams } from 'next/navigation' +import { parse, format } from 'date-fns' + +import { Markdoc } from '../../../../components/Markdoc' +import { BlogPage } from '../../../../components/Page' +import { Heading } from '../../../../components/docs/Heading' +import { Type } from '../../../../components/primitives/Type' +import { type BlogPost } from './page' +import { extractHeadings } from '../../../../markdoc/headings' + +export default function Page ({ post }: { post: BlogPost }) { + const params = useParams() + const headings = [{ id: 'title', depth: 1, label: post.title }, ...extractHeadings(post.content)] + + const publishedDate = post.publishDate + const parsedDate = parse(publishedDate, 'yyyy-M-d', new Date()) + const formattedDateStr = format(parsedDate, 'MMMM do, yyyy') + + return ( + + + {post.title} + + + + Published on {formattedDateStr} + {post.authorHandle ? ( + + {' '} + by{' '} + + {post.authorName} + + + ) : ( + by {post.authorName} + )} + + + {post.content.children.map((child, i) => ( + + ))} + + ) +} diff --git a/docs/app/(site)/blog/[post]/page.tsx b/docs/app/(site)/blog/[post]/page.tsx new file mode 100644 index 00000000000..ac22aa6516f --- /dev/null +++ b/docs/app/(site)/blog/[post]/page.tsx @@ -0,0 +1,68 @@ +import { type Tag, transform } from '@markdoc/markdoc' +import { notFound } from 'next/navigation' + +import { getOgAbsoluteUrl } from '../../../../lib/og-util' +import { reader } from '../../../../keystatic/reader' +import { baseMarkdocConfig } from '../../../../markdoc/config' +import { type EntryWithResolvedLinkedFiles } from '@keystatic/core/reader' +import type keystaticConfig from '../../../../keystatic.config' +import PageClient from './page-client' +import { type Metadata } from 'next' + +export type BlogPost = NonNullable< + Omit< + EntryWithResolvedLinkedFiles<(typeof keystaticConfig)['collections']['posts']>, + 'content' + > & { + content: Tag + } +> + +export default async function Page ({ params }) { + const post = await reader.collections.posts.read(params!.post, { + resolveLinkedFiles: true, + }) + + if (!post) return notFound() + + return ( + + ) +} + +// Dynamic SEO page metadata +export async function generateMetadata ({ params }): Promise { + const post = await reader.collections.posts.read(params!.post) + + const title = post?.title ? `${post.title} - Keystone 6 Blog` : 'Keystone 6 Blog' + + let ogImageUrl = post?.metaImageUrl + if (!ogImageUrl) { + ogImageUrl = getOgAbsoluteUrl({ + title, + type: 'Blog', + }) + } + + return { + title, + description: post?.description, + openGraph: { + images: ogImageUrl, + }, + } +} + +// Static HTML page generation for each document page +export async function generateStaticParams () { + const posts = await reader.collections.posts.list() + return posts.map((post) => ({ post })) +} diff --git a/docs/pages/blog/index.tsx b/docs/app/(site)/blog/page-client.tsx similarity index 52% rename from docs/pages/blog/index.tsx rename to docs/app/(site)/blog/page-client.tsx index c825d433b7e..503a2261ed9 100644 --- a/docs/pages/blog/index.tsx +++ b/docs/app/(site)/blog/page-client.tsx @@ -1,50 +1,20 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { type InferGetStaticPropsType, type GetStaticPropsResult } from 'next' +/** @jsxImportSource @emotion/react */ + +'use client' + import Link from 'next/link' -import { parse, format } from 'date-fns' -import { jsx } from '@emotion/react' -import { MWrapper } from '../../components/content/MWrapper' -import { Page } from '../../components/Page' -import { Type } from '../../components/primitives/Type' -import { Highlight } from '../../components/primitives/Highlight' -import { useMediaQuery } from '../../lib/media' -import { siteBaseUrl } from '../../lib/og-util' -import { reader } from '../../lib/keystatic-reader' -import { type Entry } from '@keystatic/core/reader' -import type keystaticConfig from '../../keystatic.config' +import { MWrapper } from '../../../components/content/MWrapper' +import { Page } from '../../../components/Page' +import { Type } from '../../../components/primitives/Type' +import { Highlight } from '../../../components/primitives/Highlight' +import { useMediaQuery } from '../../../lib/media' -const today = new Date() -export default function Docs (props: InferGetStaticPropsType) { +export default function Docs ({ posts }) { const mq = useMediaQuery() - // reverse chronologically sorted - const posts = props.posts - .map(p => { - const publishedDate = p.frontmatter.publishDate - const parsedDate = parse(publishedDate, 'yyyy-M-d', today) - const formattedDateStr = format(parsedDate, 'MMMM do, yyyy') - return { - ...p, - frontmatter: { ...p.frontmatter }, - parsedDate: parsedDate, - formattedDateStr: formattedDateStr, - } - }) - .sort((a, b) => { - if (a.frontmatter.publishDate === b.frontmatter.publishDate) { - return a.frontmatter.title.localeCompare(b.frontmatter.title) - } - return b.frontmatter.publishDate.localeCompare(a.frontmatter.publishDate) - }) - return ( - + - {posts.map(post => { + {posts.map((post) => { return ( - - - - {post.frontmatter.title} - - + + + {post.frontmatter.title} + {post.frontmatter.description} @@ -157,20 +127,3 @@ export default function Docs (props: InferGetStaticPropsType ) } - -export async function getStaticProps (): Promise< - GetStaticPropsResult<{ - posts: { - slug: string - frontmatter: Omit, 'content'> - }[] - }> -> { - const keystaticPosts = await reader.collections.posts.all() - - const postMeta = keystaticPosts.map(post => ({ - slug: post.slug, - frontmatter: { ...post.entry, content: null }, - })) - return { props: { posts: postMeta } } -} diff --git a/docs/app/(site)/blog/page.tsx b/docs/app/(site)/blog/page.tsx new file mode 100644 index 00000000000..9ed12ec9a1f --- /dev/null +++ b/docs/app/(site)/blog/page.tsx @@ -0,0 +1,46 @@ +import { parse, format } from 'date-fns' + +import { reader } from '../../../keystatic/reader' +import ClientPage from './page-client' +import { type Metadata } from 'next' + +const today = new Date() + +export const metadata: Metadata = { + title: 'Keystone Blog', + description: 'Blog posts from the team maintaining Keystone.', + openGraph: { + images: '/assets/blog/the-keystone-blog-cover.png' + } +} + +export default async function Docs () { + const keystaticPosts = await reader.collections.posts.all() + + const transformedPosts = keystaticPosts.map((post) => ({ + slug: post.slug, + frontmatter: { ...post.entry, content: null }, + })) + + // Reverse chronologically sorted + const sortedPosts = transformedPosts + .map((p) => { + const publishedDate = p.frontmatter.publishDate + const parsedDate = parse(publishedDate, 'yyyy-M-d', today) + const formattedDateStr = format(parsedDate, 'MMMM do, yyyy') + return { + ...p, + frontmatter: { ...p.frontmatter }, + parsedDate: parsedDate, + formattedDateStr: formattedDateStr, + } + }) + .sort((a, b) => { + if (a.frontmatter.publishDate === b.frontmatter.publishDate) { + return a.frontmatter.title.localeCompare(b.frontmatter.title) + } + return b.frontmatter.publishDate.localeCompare(a.frontmatter.publishDate) + }) + + return +} diff --git a/docs/pages/branding.tsx b/docs/app/(site)/branding/page-client.tsx similarity index 85% rename from docs/pages/branding.tsx rename to docs/app/(site)/branding/page-client.tsx index cd2b446ace7..8ee68425f55 100644 --- a/docs/pages/branding.tsx +++ b/docs/app/(site)/branding/page-client.tsx @@ -1,27 +1,27 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' +/** @jsxImportSource @emotion/react */ -import { IntroWrapper, IntroHeading, IntroLead } from '../components/content/Intro' -import { Highlight } from '../components/primitives/Highlight' -import { MWrapper } from '../components/content/MWrapper' -import { Download } from '../components/icons/Download' -import { Keystone } from '../components/icons/Keystone' -import { Type } from '../components/primitives/Type' -import { Pill } from '../components/content/Pill' -import { Nope } from '../components/icons/Nope' -import { Tick } from '../components/icons/Tick' -import { useMediaQuery } from '../lib/media' -import { Page } from '../components/Page' -import { Alert } from '../components/primitives/Alert' -import { Button } from '../components/primitives/Button' -import { ArrowR } from '../components/icons/ArrowR' +'use client' + +import { IntroWrapper, IntroHeading, IntroLead } from '../../../components/content/Intro' +import { Highlight } from '../../../components/primitives/Highlight' +import { MWrapper } from '../../../components/content/MWrapper' +import { Download } from '../../../components/icons/Download' +import { Keystone } from '../../../components/icons/Keystone' +import { Type } from '../../../components/primitives/Type' +import { Pill } from '../../../components/content/Pill' +import { Nope } from '../../../components/icons/Nope' +import { Tick } from '../../../components/icons/Tick' +import { useMediaQuery } from '../../../lib/media' +import { Page } from '../../../components/Page' +import { Alert } from '../../../components/primitives/Alert' +import { Button } from '../../../components/primitives/Button' +import { ArrowR } from '../../../components/icons/ArrowR' export default function Brand () { const mq = useMediaQuery() return ( - + Branding diff --git a/docs/app/(site)/branding/page.tsx b/docs/app/(site)/branding/page.tsx new file mode 100644 index 00000000000..32e07479bb4 --- /dev/null +++ b/docs/app/(site)/branding/page.tsx @@ -0,0 +1,10 @@ +import PageClient from './page-client' + +export const metadata = { + title: 'KeytoneJS Brand', + description: 'Keystones brand assets and guidelines', +} + +export default function Brand () { + return +} diff --git a/docs/app/(site)/docs/[...rest]/page-client.tsx b/docs/app/(site)/docs/[...rest]/page-client.tsx new file mode 100644 index 00000000000..c5531b2b94c --- /dev/null +++ b/docs/app/(site)/docs/[...rest]/page-client.tsx @@ -0,0 +1,21 @@ +'use client' + +import { type Document } from './page' + +import { Markdoc } from '../../../../components/Markdoc' + +import { Heading } from '../../../../components/docs/Heading' + +export default function PageClient ({ document }: { document: Document }) { + return ( + <> + + {document.title} + + + {document.content.children.map((child, i) => ( + + ))} + > + ) +} diff --git a/docs/app/(site)/docs/[...rest]/page.tsx b/docs/app/(site)/docs/[...rest]/page.tsx new file mode 100644 index 00000000000..e8a29b42bb8 --- /dev/null +++ b/docs/app/(site)/docs/[...rest]/page.tsx @@ -0,0 +1,57 @@ +import { notFound } from 'next/navigation' +import { type Tag, transform } from '@markdoc/markdoc' + +import { reader } from '../../../../keystatic/reader' +import { baseMarkdocConfig } from '../../../../markdoc/config' +import { extractHeadings } from '../../../../markdoc/headings' +import PageClient from './page-client' +import { type EntryWithResolvedLinkedFiles } from '@keystatic/core/reader' +import type keystaticConfig from '../../../../keystatic.config' +import { DocsLayout } from '../../../../components/docs/DocsLayout' + +export type Document = NonNullable< + Pick< + EntryWithResolvedLinkedFiles<(typeof keystaticConfig)['collections']['docs']>, + 'title' | 'description' + > & { + content: Tag + } +> + +export default async function DocPage ({ params }) { + const doc = await reader.collections.docs.read(params!.rest.join('/'), { + resolveLinkedFiles: true, + }) + if (!doc) return notFound() + + const transformedDoc: Document = { + ...doc, + content: transform(doc.content.node, baseMarkdocConfig) as Tag, + } + + const headings = [ + { id: 'title', depth: 1, label: transformedDoc.title }, + ...extractHeadings(transformedDoc.content), + ] + + return ( + + + + ) +} + +// Dynamic SEO page metadata +export async function generateMetadata ({ params }) { + const doc = await reader.collections.docs.read(params!.rest.join('/')) + return { + title: doc?.title ? `${doc.title} - Keystone 6 Documentation` : 'Keystone 6 Documentation', + description: doc?.description, + } +} + +// Static HTML page generation for each document page +export async function generateStaticParams () { + const pages = await reader.collections.docs.list() + return pages.map((page) => ({ rest: page.split('/') })) +} diff --git a/docs/pages/docs/config/overview.tsx b/docs/app/(site)/docs/config/overview/page-client.tsx similarity index 76% rename from docs/pages/docs/config/overview.tsx rename to docs/app/(site)/docs/config/overview/page-client.tsx index 78606e7bee3..293146cdd57 100644 --- a/docs/pages/docs/config/overview.tsx +++ b/docs/app/(site)/docs/config/overview/page-client.tsx @@ -1,25 +1,17 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' +/** @jsxImportSource @emotion/react */ -import { CommunitySlackCTA } from '../../../components/docs/CommunitySlackCTA' -import { Type } from '../../../components/primitives/Type' -import { Well } from '../../../components/primitives/Well' -import { DocsPage } from '../../../components/Page' -import { useMediaQuery } from '../../../lib/media' +'use client' + +import { CommunitySlackCTA } from '../../../../../components/docs/CommunitySlackCTA' +import { Type } from '../../../../../components/primitives/Type' +import { Well } from '../../../../../components/primitives/Well' +import { useMediaQuery } from '../../../../../lib/media' export default function Docs () { const mq = useMediaQuery() return ( - + <> Configuration Overview @@ -62,6 +54,6 @@ export default function Docs () { and one-time authentication tokens. - + > ) } diff --git a/docs/app/(site)/docs/config/overview/page.tsx b/docs/app/(site)/docs/config/overview/page.tsx new file mode 100644 index 00000000000..45367d9b252 --- /dev/null +++ b/docs/app/(site)/docs/config/overview/page.tsx @@ -0,0 +1,15 @@ +import { DocsLayout } from '../../../../../components/docs/DocsLayout' +import PageClient from './page-client' + +export const metadata = { + title: 'APIs', + description: 'Index for Keystone’s API reference docs.', +} + +export default function Docs () { + return ( + + + + ) +} diff --git a/docs/app/(site)/docs/examples/page-client.tsx b/docs/app/(site)/docs/examples/page-client.tsx new file mode 100644 index 00000000000..6c01dccecca --- /dev/null +++ b/docs/app/(site)/docs/examples/page-client.tsx @@ -0,0 +1,97 @@ +/** @jsxImportSource @emotion/react */ + +'use client' + +import { type Tag } from '@markdoc/markdoc' + +import { type GroupedExamples } from './page' +import { GitHubExamplesCTA } from '../../../../components/docs/GitHubExamplesCTA' +import { Type } from '../../../../components/primitives/Type' +import { FeaturedCard, SplitCardContainer } from '../../../../components/docs/FeaturedCard' +import { useMediaQuery } from '../../../../lib/media' + +export default function Docs ({ + standaloneExamples, + endToEndExamples, + deploymentExamples, +}: GroupedExamples) { + return ( + <> + + Examples + + + + A growing collection of projects you can run locally to learn more about Keystone’s + capabilities. Each example includes documentation explaining the how and why. Use them as a + reference for best practice, and a jumping off point when adding features to your own + Keystone project. + + + + + + Standalone Examples + + + + Standalone examples demonstrate how a particular feature works or how to solve a problem + with Keystone. + + + + {standaloneExamples.map((example) => ( + + ))} + + + + End-to-End Examples + + + + End to end examples demonstrate how a feature works or how to solve a problem with an + independent frontend application and a Keystone server. + + + + {endToEndExamples.map((example) => ( + + ))} + + + + Deployment Examples + + + + Examples with all the code and documentation you need to get a Keystone project hosted on + the web. You can find them in the{' '} + Keystone Github Organisation. + + + + {deploymentExamples.map((example) => ( + + ))} + + > + ) +} diff --git a/docs/app/(site)/docs/examples/page.tsx b/docs/app/(site)/docs/examples/page.tsx new file mode 100644 index 00000000000..714e9dac62a --- /dev/null +++ b/docs/app/(site)/docs/examples/page.tsx @@ -0,0 +1,44 @@ +import { transform } from '@markdoc/markdoc' +import { DocsLayout } from '../../../../components/docs/DocsLayout' +import { reader } from '../../../../keystatic/reader' +import PageClient from './page-client' +import { baseMarkdocConfig } from '../../../../markdoc/config' + +export const metadata = { + title: 'Examples', + description: 'A growing collection of projects you can run locally to learn more about Keystone’s many features. Use them as a reference for best practice, and springboard when adding features to your own project.', +} + +export type GroupedExamples = Awaited> + +async function getGroupedExamples () { + const examples = await reader.collections.examples.all({resolveLinkedFiles: true}) + const transformedExamples = await Promise.all(examples.map(async example => { + return { + ...example, + entry : { + ...example.entry, + description: transform(example.entry.description.node, baseMarkdocConfig) + } + } + })) + + const standaloneExamples = transformedExamples.filter(example => example.entry.kind === 'standalone') + const endToEndExamples = transformedExamples.filter(example => example.entry.kind === 'end-to-end') + const deploymentExamples = transformedExamples.filter(example => example.entry.kind === 'deployment') + + return { + standaloneExamples, + endToEndExamples, + deploymentExamples + } +} + +export default async function Docs () { + const pageData = await getGroupedExamples() + return ( + + + + ) +} diff --git a/docs/pages/docs/guides/document-field-demo.tsx b/docs/app/(site)/docs/guides/document-field-demo/page-client.tsx similarity index 70% rename from docs/pages/docs/guides/document-field-demo.tsx rename to docs/app/(site)/docs/guides/document-field-demo/page-client.tsx index 71d1692b35d..a4069d16453 100644 --- a/docs/pages/docs/guides/document-field-demo.tsx +++ b/docs/app/(site)/docs/guides/document-field-demo/page-client.tsx @@ -1,42 +1,22 @@ + +'use client' + import React from 'react' -import { H1, H2 } from '../../../components/docs/Heading' -import { DocsPage } from '../../../components/Page' + +import { H1, H2 } from '../../../../../components/docs/Heading' + import { DocumentEditorDemo, DocumentFeaturesProvider, -} from '../../../components/docs/DocumentEditorDemo' -import { Well } from '../../../components/primitives/Well' -import { RelatedContent } from '../../../components/RelatedContent' -import { InlineCode } from '../../../components/primitives/Code' +} from '../../../../../components/docs/DocumentEditorDemo' +import { Well } from '../../../../../components/primitives/Well' +import { RelatedContent } from '../../../../../components/RelatedContent' +import { InlineCode } from '../../../../../components/primitives/Code' export default function DocumentFieldDemo () { const title = 'Document Fields Demo' return ( - + <> {title} @@ -85,6 +65,6 @@ export default function DocumentFieldDemo () { - + > ) } diff --git a/docs/app/(site)/docs/guides/document-field-demo/page.tsx b/docs/app/(site)/docs/guides/document-field-demo/page.tsx new file mode 100644 index 00000000000..953bb958c8d --- /dev/null +++ b/docs/app/(site)/docs/guides/document-field-demo/page.tsx @@ -0,0 +1,36 @@ +import { DocsLayout } from '../../../../../components/docs/DocsLayout' +import PageClient from './page-client' + +export const metadata = { + title: 'Document Fields Demo', + description: + 'Try Keystone’s powerful new Document field. Experiment with its config settings in real time to shape custom editing experiences.', +} + +export default function DocumentFieldDemo () { + return ( + + + + ) +} diff --git a/docs/pages/docs/guides/overview.tsx b/docs/app/(site)/docs/guides/overview/page-client.tsx similarity index 86% rename from docs/pages/docs/guides/overview.tsx rename to docs/app/(site)/docs/guides/overview/page-client.tsx index 0433d4906e0..fff1a218e37 100644 --- a/docs/pages/docs/guides/overview.tsx +++ b/docs/app/(site)/docs/guides/overview/page-client.tsx @@ -1,27 +1,17 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' +/** @jsxImportSource @emotion/react */ -import { CommunitySlackCTA } from '../../../components/docs/CommunitySlackCTA' -import { Type } from '../../../components/primitives/Type' -import { Well } from '../../../components/primitives/Well' -import { DocsPage } from '../../../components/Page' -import { useMediaQuery } from '../../../lib/media' +'use client' + +import { CommunitySlackCTA } from '../../../../../components/docs/CommunitySlackCTA' +import { Type } from '../../../../../components/primitives/Type' +import { Well } from '../../../../../components/primitives/Well' +import { useMediaQuery } from '../../../../../lib/media' export default function Docs () { const mq = useMediaQuery() return ( - + <> Keystone Guides @@ -113,6 +103,6 @@ export default function Docs () { Learn how to add your own custom pages to Keystone’s Admin UI. - + > ) } diff --git a/docs/app/(site)/docs/guides/overview/page.tsx b/docs/app/(site)/docs/guides/overview/page.tsx new file mode 100644 index 00000000000..a8c53f83910 --- /dev/null +++ b/docs/app/(site)/docs/guides/overview/page.tsx @@ -0,0 +1,16 @@ +import { DocsLayout } from '../../../../../components/docs/DocsLayout' +import PageClient from './page-client' + +export const metadata = { + title: 'Guides', + description: + 'Practical explanations of Keystone’s fundamental building blocks. Learn how to think about, and get the most out of Keystone’s many features.', +} + +export default function Docs () { + return ( + + + + ) +} diff --git a/docs/app/(site)/docs/page-client.tsx b/docs/app/(site)/docs/page-client.tsx new file mode 100644 index 00000000000..48ffe26f1a2 --- /dev/null +++ b/docs/app/(site)/docs/page-client.tsx @@ -0,0 +1,43 @@ +/** @jsxImportSource @emotion/react */ + +'use client' + +import { CommunitySlackCTA } from '../../../components/docs/CommunitySlackCTA' +import { Keystone5DocsCTA } from '../../../components/docs/Keystone5DocsCTA' +import { Type } from '../../../components/primitives/Type' +import { CommunityCta } from '../../../components/content/CommunityCta' +import { Alert } from '../../../components/primitives/Alert' +import { Button } from '../../../components/primitives/Button' +import { ArrowR } from '../../../components/icons' +import { KeystoneExperience } from '../../../components/docs/KeystoneExperience' + +export default function DocsPageClient ({ featuredExamples, featuredDocs }) { + + return ( + <> + + Developer Docs + + + + + + + Looking for enterprise-grade consulting & support? + + + Learn more + + + + {featuredDocs} + {featuredExamples} + + > + ) +} diff --git a/docs/app/(site)/docs/page.tsx b/docs/app/(site)/docs/page.tsx new file mode 100644 index 00000000000..a81481a282a --- /dev/null +++ b/docs/app/(site)/docs/page.tsx @@ -0,0 +1,21 @@ +import PageClient from './page-client' +import { FeaturedExamples } from '../../../components/docs/featured-examples' +import { FeaturedDocs } from '../../../components/docs/featured-docs' + +import { DocsLayout } from '../../../components/docs/DocsLayout' + +export const metadata = { + title: 'Keystone Docs Home', + description: 'Developer docs for KeystoneJS: The superpowered headless CMS for developers.', +} + +export default function Docs () { + return ( + + } + featuredDocs={} + /> + + ) +} diff --git a/docs/app/(site)/docs/walkthroughs/page-client.tsx b/docs/app/(site)/docs/walkthroughs/page-client.tsx new file mode 100644 index 00000000000..170af891063 --- /dev/null +++ b/docs/app/(site)/docs/walkthroughs/page-client.tsx @@ -0,0 +1,67 @@ +/** @jsxImportSource @emotion/react */ + +'use client' + +import { Type } from '../../../../components/primitives/Type' +import { type FeaturedDocsMap } from '../../../../keystatic/get-featured-docs-map' +import { + FeaturedCard, + FullWidthCardContainer, + SplitCardContainer, +} from '../../../../components/docs/FeaturedCard' + +export default function Docs ({ + quickstart, + walkthroughs, +}: { + quickstart: NonNullable[number]['items'][number] + walkthroughs: NonNullable[number]['items'] +}) { + return ( + <> + + Walkthroughs + + + + Step-by-step tutorials for building with Keystone. + + + + Getting started + + + + If you’re new to Keystone begin here. These walkthroughs introduce the system, key concepts, + and show you how to get up and running with schema-driven development the Keystone way. + + + + + + + + Learn Keystone + + + + Learn how to build a functioning blog backend with relationships, auth, and session data + from an empty folder, and gain insights into Keystone’s core concepts along the way. + + + + {walkthroughs.map((item) => ( + + ))} + + > + ) +} diff --git a/docs/app/(site)/docs/walkthroughs/page.tsx b/docs/app/(site)/docs/walkthroughs/page.tsx new file mode 100644 index 00000000000..081484041e1 --- /dev/null +++ b/docs/app/(site)/docs/walkthroughs/page.tsx @@ -0,0 +1,26 @@ +import { DocsLayout } from '../../../../components/docs/DocsLayout' +import { getFeaturedDocsMap } from '../../../../keystatic/get-featured-docs-map' +import { reader } from '../../../../keystatic/reader' +import PageClient from './page-client' + +export const metadata = { + title: 'Walkthroughs', + description: + 'Explore tutorials with step-by-step instruction on building solutions with Keystone.', +} + +export default async function Docs () { + const docs = await getFeaturedDocsMap() + if (!docs) throw new Error('No `featuredDocs` found') + const featuredDocs = docs[0] + const [quickstart, ...walkthroughs] = featuredDocs.items + + return ( + + + + ) +} diff --git a/docs/pages/ds.tsx b/docs/app/(site)/ds/page-client.tsx similarity index 94% rename from docs/pages/ds.tsx rename to docs/app/(site)/ds/page-client.tsx index e631e7c4e7e..41ddd4e74a6 100644 --- a/docs/pages/ds.tsx +++ b/docs/app/(site)/ds/page-client.tsx @@ -1,24 +1,25 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ +/** @jsxImportSource @emotion/react */ + +'use client' + import { Fragment, useState } from 'react' -import { jsx } from '@emotion/react' -import { CodeWindow, WindowWrapper, WindowL, WindowR } from '../components/content/CodeWindow' -import { GitHubButton } from '../components/primitives/GitHubButton' -import { COLORS, TYPESCALE, TYPE, SPACE } from '../lib/TOKENS' -import { Highlight } from '../components/primitives/Highlight' -import { styleMap, Type } from '../components/primitives/Type' -import { InlineCode } from '../components/primitives/Code' -import { Status } from '../components/primitives/Status' -import { Button } from '../components/primitives/Button' -import { Alert } from '../components/primitives/Alert' -import { Badge } from '../components/primitives/Badge' -import { Emoji } from '../components/primitives/Emoji' -import { Stack } from '../components/primitives/Stack' -import { Field } from '../components/primitives/Field' -import { Well } from '../components/primitives/Well' -import * as allIcons from '../components/icons' -import { Page } from '../components/Page' +import { CodeWindow, WindowWrapper, WindowL, WindowR } from '../../../components/content/CodeWindow' +import { GitHubButton } from '../../../components/primitives/GitHubButton' +import { COLORS, TYPESCALE, TYPE, SPACE } from '../../../lib/TOKENS' +import { Highlight } from '../../../components/primitives/Highlight' +import { styleMap, Type } from '../../../components/primitives/Type' +import { InlineCode } from '../../../components/primitives/Code' +import { Status } from '../../../components/primitives/Status' +import { Button } from '../../../components/primitives/Button' +import { Alert } from '../../../components/primitives/Alert' +import { Badge } from '../../../components/primitives/Badge' +import { Emoji } from '../../../components/primitives/Emoji' +import { Stack } from '../../../components/primitives/Stack' +import { Field } from '../../../components/primitives/Field' +import { Well } from '../../../components/primitives/Well' +import * as allIcons from '../../../components/icons' +import { Page } from '../../../components/Page' const EXCEPT_ICONS = ['FrontEndLogos', 'ClientLogos'] @@ -96,10 +97,7 @@ export default function DS () { let firstGrad: string return ( - + Design System @@ -264,7 +262,7 @@ export default function DS () { Type - {(Object.keys(styleMap) as Array).map(style => ( + {(Object.keys(styleMap) as Array).map((style) => ( Type {style} diff --git a/docs/app/(site)/ds/page.tsx b/docs/app/(site)/ds/page.tsx new file mode 100644 index 00000000000..5991a0df853 --- /dev/null +++ b/docs/app/(site)/ds/page.tsx @@ -0,0 +1,10 @@ +import PageClient from './page-client' + +export const metadata = { + title: 'Design System Components and Tokens', + description: 'Design System Components & Tokens for the KeystoneJS website', +} + +export default function DS () { + return +} diff --git a/docs/pages/enterprise.tsx b/docs/app/(site)/enterprise/page-client.tsx similarity index 76% rename from docs/pages/enterprise.tsx rename to docs/app/(site)/enterprise/page-client.tsx index 9b29ae31bb3..5e851cf3547 100644 --- a/docs/pages/enterprise.tsx +++ b/docs/app/(site)/enterprise/page-client.tsx @@ -1,24 +1,25 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' -import { useMediaQuery } from '../lib/media' -import { Highlight } from '../components/primitives/Highlight' -import { MWrapper } from '../components/content/MWrapper' -import { Quote } from '../components/content/Quote' -import { Type } from '../components/primitives/Type' -import { Pill } from '../components/content/Pill' -import { Page } from '../components/Page' -import { ContactForm } from '../components/ContactForm' -import { CustomerCard } from '../components/content/CustomerCard' -import { VocalLogo } from '../components/icons/VocalLogo' -import { PJohnsonLogo } from '../components/icons/PJohnsonLogo' -import { DFATLogo } from '../components/icons/DFATLogo' -import { EnliticLogo } from '../components/icons/EnliticLogo' -import { RugbyAuLogo } from '../components/icons/RugbyAuLogo' -import { WestpacLogo } from '../components/icons/WestpacLogo' -import { PrintBarLogo } from '../components/icons/PrintBarLogo' -import { IntroHeading, IntroLead, IntroWrapper } from '../components/content/Intro' -import { Stack } from '../components/primitives/Stack' +/** @jsxImportSource @emotion/react */ + +'use client' + +import { useMediaQuery } from '../../../lib/media' +import { Highlight } from '../../../components/primitives/Highlight' +import { MWrapper } from '../../../components/content/MWrapper' +import { Quote } from '../../../components/content/Quote' +import { Type } from '../../../components/primitives/Type' +import { Pill } from '../../../components/content/Pill' +import { Page } from '../../../components/Page' +import { ContactForm } from '../../../components/ContactForm' +import { CustomerCard } from '../../../components/content/CustomerCard' +import { VocalLogo } from '../../../components/icons/VocalLogo' +import { PJohnsonLogo } from '../../../components/icons/PJohnsonLogo' +import { DFATLogo } from '../../../components/icons/DFATLogo' +import { EnliticLogo } from '../../../components/icons/EnliticLogo' +import { RugbyAuLogo } from '../../../components/icons/RugbyAuLogo' +import { WestpacLogo } from '../../../components/icons/WestpacLogo' +import { PrintBarLogo } from '../../../components/icons/PrintBarLogo' +import { IntroHeading, IntroLead, IntroWrapper } from '../../../components/content/Intro' +import { Stack } from '../../../components/primitives/Stack' const customers = [ { @@ -64,14 +65,8 @@ const customers = [ export default function ForOrganisations () { const mq = useMediaQuery() - return ( - + Keystone for Enterprise @@ -134,15 +129,15 @@ export default function ForOrganisations () { {learnMoreHref ? ( - + {copy} - + Learn more . - - + + ) : ( copy )} diff --git a/docs/app/(site)/enterprise/page.tsx b/docs/app/(site)/enterprise/page.tsx new file mode 100644 index 00000000000..81a361d2813 --- /dev/null +++ b/docs/app/(site)/enterprise/page.tsx @@ -0,0 +1,11 @@ +import PageClient from './page-client' + +export const metadata = { + title: 'Keystone for Enterprise', + description: + 'Discover how Keystone’s Admin UI is a natural extension of how you work in code, and has the flexibility you need to enable content creatives.', +} + +export default function ForOrganisations () { + return +} diff --git a/docs/pages/for-content-management.tsx b/docs/app/(site)/for-content-management/page-client.tsx similarity index 90% rename from docs/pages/for-content-management.tsx rename to docs/app/(site)/for-content-management/page-client.tsx index a1168e6c668..ed2041f4fdb 100644 --- a/docs/pages/for-content-management.tsx +++ b/docs/app/(site)/for-content-management/page-client.tsx @@ -1,39 +1,35 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' +/** @jsxImportSource @emotion/react */ + +'use client' + import Image from 'next/image' import Link from 'next/link' -import { useMediaQuery } from '../lib/media' -import { IntroWrapper, IntroHeading, IntroLead } from '../components/content/Intro' -import { Highlight } from '../components/primitives/Highlight' -import { MWrapper } from '../components/content/MWrapper' -import { Section, SideBySideSection } from '../components/content/Section' -import { Button } from '../components/primitives/Button' -import { Quote } from '../components/content/Quote' -import { Type } from '../components/primitives/Type' -import { ArrowR } from '../components/icons/ArrowR' -import { Pill } from '../components/content/Pill' -import { Tick } from '../components/icons/Tick' -import { Page } from '../components/Page' +import { useMediaQuery } from '../../../lib/media' +import { IntroWrapper, IntroHeading, IntroLead } from '../../../components/content/Intro' +import { Highlight } from '../../../components/primitives/Highlight' +import { MWrapper } from '../../../components/content/MWrapper' +import { Section, SideBySideSection } from '../../../components/content/Section' +import { Button } from '../../../components/primitives/Button' +import { Quote } from '../../../components/content/Quote' +import { Type } from '../../../components/primitives/Type' +import { ArrowR } from '../../../components/icons/ArrowR' +import { Pill } from '../../../components/content/Pill' +import { Tick } from '../../../components/icons/Tick' +import { Page } from '../../../components/Page' -import dsGeneration from '../public/assets/ds-generation.png' -import contentManagement1 from '../public/assets/content-management-1.png' -import contentManagement2 from '../public/assets/content-management-2.png' -import contentManagement3 from '../public/assets/content-management-3.png' -import contentManagement4 from '../public/assets/content-management-4.png' -import { EndCta } from '../components/content/EndCta' +import dsGeneration from '../../../public/assets/ds-generation.png' +import contentManagement1 from '../../../public/assets/content-management-1.png' +import contentManagement2 from '../../../public/assets/content-management-2.png' +import contentManagement3 from '../../../public/assets/content-management-3.png' +import contentManagement4 from '../../../public/assets/content-management-4.png' +import { EndCta } from '../../../components/content/EndCta' export default function ForOrganisations () { const mq = useMediaQuery() return ( - + Keystone for content management diff --git a/docs/app/(site)/for-content-management/page.tsx b/docs/app/(site)/for-content-management/page.tsx new file mode 100644 index 00000000000..29999fd0b78 --- /dev/null +++ b/docs/app/(site)/for-content-management/page.tsx @@ -0,0 +1,11 @@ +import PageClient from './page-client' + +export const metadata = { + title: 'KeystoneJS for Content Management', + description: + 'Discover how Keystone’s Admin UI is a natural extension of how you work in code, and has the flexibility you need to enable content creatives.', +} + +export default function ForOrganisations () { + return +} diff --git a/docs/pages/for-developers.tsx b/docs/app/(site)/for-developers/page-client.tsx similarity index 90% rename from docs/pages/for-developers.tsx rename to docs/app/(site)/for-developers/page-client.tsx index 8e4c1724451..944e63ce025 100644 --- a/docs/pages/for-developers.tsx +++ b/docs/app/(site)/for-developers/page-client.tsx @@ -1,48 +1,44 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' +/** @jsxImportSource @emotion/react */ + +'use client' + import Image from 'next/image' import Link from 'next/link' -import { useMediaQuery } from '../lib/media' -import { IntroWrapper, IntroHeading, IntroLead } from '../components/content/Intro' -import { CommunityCta } from '../components/content/CommunityCta' -import { FrontEndLogos } from '../components/icons/FrontEndLogos' -import { Highlight } from '../components/primitives/Highlight' -import { Relational } from '../components/icons/Relational' -import { TweetBox } from '../components/content/TweetBox' -import { MWrapper } from '../components/content/MWrapper' -import { Typescript } from '../components/icons/Typescript' -import { Section, SideBySideSection } from '../components/content/Section' -import { CodeBox } from '../components/content/CodeBox' -import { Button } from '../components/primitives/Button' -import { EndCta } from '../components/content/EndCta' -import { Postgres } from '../components/icons/Postgres' -import { Emoji } from '../components/primitives/Emoji' -import { GraphQl } from '../components/icons/GraphQl' -import { Type } from '../components/primitives/Type' -import { ArrowR } from '../components/icons/ArrowR' -import { Nextjs } from '../components/icons/Nextjs' -import { Shield } from '../components/icons/Shield' -import { Prisma } from '../components/icons/Prisma' -import { Pill } from '../components/content/Pill' -import { Tick } from '../components/icons/Tick' -import { Cli } from '../components/icons/Cli' -import { Page } from '../components/Page' +import { useMediaQuery } from '../../../lib/media' +import { IntroWrapper, IntroHeading, IntroLead } from '../../../components/content/Intro' +import { CommunityCta } from '../../../components/content/CommunityCta' +import { FrontEndLogos } from '../../../components/icons/FrontEndLogos' +import { Highlight } from '../../../components/primitives/Highlight' +import { Relational } from '../../../components/icons/Relational' +import { TweetBox } from '../../../components/content/TweetBox' +import { MWrapper } from '../../../components/content/MWrapper' +import { Typescript } from '../../../components/icons/Typescript' +import { Section, SideBySideSection } from '../../../components/content/Section' +import { CodeBox } from '../../../components/content/CodeBox' +import { Button } from '../../../components/primitives/Button' +import { EndCta } from '../../../components/content/EndCta' +import { Postgres } from '../../../components/icons/Postgres' +import { Emoji } from '../../../components/primitives/Emoji' +import { GraphQl } from '../../../components/icons/GraphQl' +import { Type } from '../../../components/primitives/Type' +import { ArrowR } from '../../../components/icons/ArrowR' +import { Nextjs } from '../../../components/icons/Nextjs' +import { Shield } from '../../../components/icons/Shield' +import { Prisma } from '../../../components/icons/Prisma' +import { Pill } from '../../../components/content/Pill' +import { Tick } from '../../../components/icons/Tick' +import { Cli } from '../../../components/icons/Cli' +import { Page } from '../../../components/Page' -import editorStorytelling from '../public/assets/editor-storytelling.png' -import richTextEditor from '../public/assets/rich-text-editor.png' +import editorStorytelling from '../../../public/assets/editor-storytelling.png' +import richTextEditor from '../../../public/assets/rich-text-editor.png' export default function ForDevelopers () { const mq = useMediaQuery() return ( - + Keystone for developers @@ -65,7 +61,7 @@ export default function ForDevelopers () { marginTop: '2.5rem', })} > - + +} diff --git a/docs/pages/for-organisations.tsx b/docs/app/(site)/for-organisations/page-client.tsx similarity index 90% rename from docs/pages/for-organisations.tsx rename to docs/app/(site)/for-organisations/page-client.tsx index 198bfa580af..b332d8c8782 100644 --- a/docs/pages/for-organisations.tsx +++ b/docs/app/(site)/for-organisations/page-client.tsx @@ -1,37 +1,33 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' +/** @jsxImportSource @emotion/react */ + +'use client' + import Image from 'next/image' import Link from 'next/link' -import { useMediaQuery } from '../lib/media' -import { IntroWrapper, IntroHeading, IntroLead } from '../components/content/Intro' -import { Highlight } from '../components/primitives/Highlight' -import { ClientLogos } from '../components/icons/ClientLogos' -import { TweetBox } from '../components/content/TweetBox' -import { MWrapper } from '../components/content/MWrapper' -import { Section } from '../components/content/Section' -import { Thinkmill } from '../components/icons/Thinkmill' -import { Quote } from '../components/content/Quote' -import { Type } from '../components/primitives/Type' -import { Pill } from '../components/content/Pill' -import { Tick } from '../components/icons/Tick' -import { Page } from '../components/Page' +import { useMediaQuery } from '../../../lib/media' +import { IntroWrapper, IntroHeading, IntroLead } from '../../../components/content/Intro' +import { Highlight } from '../../../components/primitives/Highlight' +import { ClientLogos } from '../../../components/icons/ClientLogos' +import { TweetBox } from '../../../components/content/TweetBox' +import { MWrapper } from '../../../components/content/MWrapper' +import { Section } from '../../../components/content/Section' +import { Thinkmill } from '../../../components/icons/Thinkmill' +import { Quote } from '../../../components/content/Quote' +import { Type } from '../../../components/primitives/Type' +import { Pill } from '../../../components/content/Pill' +import { Tick } from '../../../components/icons/Tick' +import { Page } from '../../../components/Page' -import editor from '../public/assets/editor.png' -import dataIntegrity from '../public/assets/data-integrity.png' -import { EndCta } from '../components/content/EndCta' +import editor from '../../../public/assets/editor.png' +import dataIntegrity from '../../../public/assets/data-integrity.png' +import { EndCta } from '../../../components/content/EndCta' export default function ForOrganisations () { const mq = useMediaQuery() return ( - + Keystone for organisations diff --git a/docs/app/(site)/for-organisations/page.tsx b/docs/app/(site)/for-organisations/page.tsx new file mode 100644 index 00000000000..1a34b44c0e2 --- /dev/null +++ b/docs/app/(site)/for-organisations/page.tsx @@ -0,0 +1,11 @@ +import PageClient from './page-client' + +export const metadata = { + title: 'KeystoneJS for Organisations', + description: + 'Discover how Keystone’s flexibility lets organisations scale fast and sustainably with a backend that can be shaped to any business logic.', +} + +export default function ForOrganisations () { + return +} diff --git a/docs/app/(site)/layout-client.tsx b/docs/app/(site)/layout-client.tsx new file mode 100644 index 00000000000..39e4c8a0d64 --- /dev/null +++ b/docs/app/(site)/layout-client.tsx @@ -0,0 +1,211 @@ +/** @jsxImportSource @emotion/react */ + +'use client' + +import { CacheProvider } from '@emotion/react' +import { useServerInsertedHTML } from 'next/navigation' +import { useContext, useEffect, useMemo, useState } from 'react' +import createCache from '@emotion/cache' +import Script from 'next/script' +import { Global, css } from '@emotion/react' + +import { proseStyles } from '../../lib/prose-lite' +import { Theme } from '../../components/Theme' +import { NavContextProvider } from '../../components/docs/Navigation' +import { SkipLinks } from '../../components/SkipLinks' +import { createContext } from 'react' + +function getTheme () { + const isSystemColorSchemeDark = window.matchMedia('(prefers-color-scheme: dark)').matches + const localStorageTheme = localStorage.theme + if ((!localStorageTheme && isSystemColorSchemeDark) || localStorageTheme === 'dark') { + return 'dark' + } + return 'light' +} + +const ThemeContext = createContext({ + theme: 'light' as 'dark' | 'light', + setTheme: (theme: 'dark' | 'light') => {}, +}) + +export function useThemeContext () { + return useContext(ThemeContext) +} + +export function Html (props: { children: React.ReactNode}) { + const [theme, setTheme] = useState<'light' | 'dark'>(() => { + if (typeof window === 'undefined') return 'light' + return getTheme() + }) + useEffect(() => { + const listener = () => { + setTheme(getTheme()) + } + const match = window.matchMedia('(prefers-color-scheme: dark)') + match.addEventListener('change', listener) + return () => { + match.removeEventListener('change', listener) + } + }, []) + const context = useMemo(() => ({ + theme, + setTheme (theme:'dark' | 'light') { + setTheme((theme) => (theme === 'dark' ? 'light' : 'dark')) + localStorage.setItem('theme', theme) + } + }), [theme, setTheme]) + return {props.children} +} + +export default function RootLayoutClient ({ children }: { children: React.ReactNode }) { + const [{ cache, flush }] = useState(() => { + const cache = createCache({ key: 'my' }) + cache.compat = true + const prevInsert = cache.insert + let inserted: string[] = [] + cache.insert = (...args) => { + const serialized = args[1] + if (cache.inserted[serialized.name] === undefined) { + inserted.push(serialized.name) + } + return prevInsert(...args) + } + const flush = () => { + const prevInserted = inserted + inserted = [] + return prevInserted + } + return { cache, flush } + }) + + useServerInsertedHTML(() => { + const names = flush() + if (names.length === 0) return null + let styles = '' + for (const name of names) { + styles += cache.inserted[name] + } + return ( + + ) + }) + + return ( + + + + + + + + {children} + + + + + + + + ) +} diff --git a/docs/app/(site)/layout.tsx b/docs/app/(site)/layout.tsx new file mode 100644 index 00000000000..f2cb4e4fd83 --- /dev/null +++ b/docs/app/(site)/layout.tsx @@ -0,0 +1,107 @@ +import type { Metadata } from 'next' +import RootLayoutClient, { Html } from './layout-client' +import { siteBaseUrl } from '../../lib/og-util' + +const defaultTitle = 'KeystoneJS: The superpowered Node.js Headless CMS for developers' +const defaultDescription = + 'Build faster and scale further with the programmable open source GraphQL API back-end for structured content projects.' + +export const metadata: Metadata = { + metadataBase: new URL(siteBaseUrl), + title: defaultTitle, + description: defaultDescription, + icons: { + apple: '/apple-touch-icon.png', + icon: [ + { url: '/favicon-32x32.png', sizes: '32x32', type: 'image/png' }, + { url: '/favicon-16x16.png', sizes: '16x16', type: 'image/png' }, + ], + shortcut: '/favicon.ico', + }, + openGraph: { + title: defaultTitle, + siteName: defaultTitle, + description: defaultDescription, + type: 'website', + locale: 'en', + images: [ + { + url: '/og-image-landscape.png', + width: 761, + height: 410, + alt: defaultDescription, + }, + ], + }, + twitter: { + title: defaultTitle, + description: defaultDescription, + card: 'summary_large_image', + images: [ + { + url: '/og-image-landscape.png', + width: 761, + height: 410, + alt: defaultTitle, + }, + ], + }, + other: { + 'msapplication-TileColor': '#2684FF', + 'msapplication-config': '/browserconfig.xml', + }, +} + +export default function RootLayout ({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + + + + + + + + + {children} + + ) +} diff --git a/docs/pages/404.tsx b/docs/app/(site)/not-found.tsx similarity index 70% rename from docs/pages/404.tsx rename to docs/app/(site)/not-found.tsx index 151825598b7..addcf907b6e 100644 --- a/docs/pages/404.tsx +++ b/docs/app/(site)/not-found.tsx @@ -1,11 +1,12 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' -import { useRouter } from 'next/router' +/** @jsxImportSource @emotion/react */ -import { Highlight } from '../components/primitives/Highlight' -import { Type } from '../components/primitives/Type' -import { Page } from '../components/Page' +'use client' + +import { usePathname } from 'next/navigation' + +import { Highlight } from '../../components/primitives/Highlight' +import { Type } from '../../components/primitives/Type' +import { Page } from '../../components/Page' function ConstructionIllustration () { return ( @@ -34,11 +35,16 @@ function ConstructionIllustration () { // see https://github.com/keystonejs/keystone/pull/6411#issuecomment-906085389 const v5PathList = ['/tutorials', '/guides', '/keystonejs', '/api', '/discussions'] +export const metadata = { + title: 'Page Not Found (404)', + description: 'The page you are looking for could not be found.', +} + export default function NotFoundPage () { - const { asPath } = useRouter() - const tryV5Link = v5PathList.some(x => asPath.startsWith(x)) + const pathname = usePathname() + const tryV5Link = v5PathList.some((x) => pathname.startsWith(x)) return ( - + If you were looking for a page in the Keystone 5 docs, try{' '} - https://v5.keystonejs.com{asPath}. + https://v5.keystonejs.com{pathname} + . ) : null} diff --git a/docs/pages/index.tsx b/docs/app/(site)/page-client.tsx similarity index 91% rename from docs/pages/index.tsx rename to docs/app/(site)/page-client.tsx index e8b725978d7..1775309ca48 100644 --- a/docs/pages/index.tsx +++ b/docs/app/(site)/page-client.tsx @@ -1,55 +1,51 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' +/** @jsxImportSource @emotion/react */ + +'use client' + import Image from 'next/image' import Link from 'next/link' -import { useMediaQuery } from '../lib/media' -import { CodeWindow, WindowWrapper, WindowL, WindowR } from '../components/content/CodeWindow' -import { IntroWrapper, IntroHeading, IntroLead } from '../components/content/Intro' -import { CommunityCta } from '../components/content/CommunityCta' -import { FrontEndLogos } from '../components/icons/FrontEndLogos' -import { Highlight } from '../components/primitives/Highlight' -import { WhyKeystone } from '../components/icons/WhyKeystone' -import { MWrapper } from '../components/content/MWrapper' -import { Relational } from '../components/icons/Relational' -import { TweetBox } from '../components/content/TweetBox' -import { Typescript } from '../components/icons/Typescript' -import { Code as SourceCode, InlineCode } from '../components/primitives/Code' -import { Automated } from '../components/icons/Automated' -import { Section } from '../components/content/Section' -import { Thinkmill } from '../components/icons/Thinkmill' -import { CodeBox } from '../components/content/CodeBox' -import { PillCta } from '../components/content/PillCta' -import { Migration } from '../components/icons/Migration' -import { Button } from '../components/primitives/Button' -import { EndCta } from '../components/content/EndCta' -import { Emoji } from '../components/primitives/Emoji' -import { Updates } from '../components/icons/Updates' -import { Type } from '../components/primitives/Type' -import { ArrowR } from '../components/icons/ArrowR' -import { Custom } from '../components/icons/Custom' -import { Filter } from '../components/icons/Filter' -import { Shield } from '../components/icons/Shield' -import { Watch } from '../components/icons/Watch' -import { Tick } from '../components/icons/Tick' -import { Page } from '../components/Page' -import { Organization } from '../components/icons/Organization' -import { Content } from '../components/icons/Content' -import { Code } from '../components/icons/Code' -import contentEditorMockui from '../public/assets/content-editor-mockui.png' -import docEditorHome from '../public/assets/doc-editor-home.png' -import deployTargets from '../public/assets/deploy-targets.png' +import { useMediaQuery } from '../../lib/media' +import { CodeWindow, WindowWrapper, WindowL, WindowR } from '../../components/content/CodeWindow' +import { IntroWrapper, IntroHeading, IntroLead } from '../../components/content/Intro' +import { CommunityCta } from '../../components/content/CommunityCta' +import { FrontEndLogos } from '../../components/icons/FrontEndLogos' +import { Highlight } from '../../components/primitives/Highlight' +import { WhyKeystone } from '../../components/icons/WhyKeystone' +import { MWrapper } from '../../components/content/MWrapper' +import { Relational } from '../../components/icons/Relational' +import { TweetBox } from '../../components/content/TweetBox' +import { Typescript } from '../../components/icons/Typescript' +import { Code as SourceCode, InlineCode } from '../../components/primitives/Code' +import { Automated } from '../../components/icons/Automated' +import { Section } from '../../components/content/Section' +import { Thinkmill } from '../../components/icons/Thinkmill' +import { CodeBox } from '../../components/content/CodeBox' +import { PillCta } from '../../components/content/PillCta' +import { Migration } from '../../components/icons/Migration' +import { Button } from '../../components/primitives/Button' +import { EndCta } from '../../components/content/EndCta' +import { Emoji } from '../../components/primitives/Emoji' +import { Updates } from '../../components/icons/Updates' +import { Type } from '../../components/primitives/Type' +import { ArrowR } from '../../components/icons/ArrowR' +import { Custom } from '../../components/icons/Custom' +import { Filter } from '../../components/icons/Filter' +import { Shield } from '../../components/icons/Shield' +import { Watch } from '../../components/icons/Watch' +import { Tick } from '../../components/icons/Tick' +import { Page } from '../../components/Page' +import { Organization } from '../../components/icons/Organization' +import { Content } from '../../components/icons/Content' +import { Code } from '../../components/icons/Code' +import contentEditorMockui from '../../public/assets/content-editor-mockui.png' +import docEditorHome from '../../public/assets/doc-editor-home.png' +import deployTargets from '../../public/assets/deploy-targets.png' -export default function IndexPage () { +export default function PageClient () { const mq = useMediaQuery() return ( - + @@ -57,12 +53,12 @@ export default function IndexPage () { Keystone helps you build faster and scale further than any other CMS or App Framework. - Just describe your schema, and get a powerful GraphQL API & beautiful Management UI for + Describe your schema, and you get a powerful GraphQL API & beautiful Management UI for content and data. - No boilerplate or bootstrapping – just elegant APIs to help you ship the code that - matters without sacrificing the flexibility or power of a bespoke back-end. + No boilerplate or bootstrapping – only elegant APIs to help you ship the code that + matters, without sacrificing the flexibility or power of a bespoke back-end. @@ -75,7 +71,7 @@ export default function IndexPage () { marginTop: '2.5rem', })} > - + - 160+ + 250+ Contributors diff --git a/docs/app/(site)/page.tsx b/docs/app/(site)/page.tsx new file mode 100644 index 00000000000..4e3f0aa8305 --- /dev/null +++ b/docs/app/(site)/page.tsx @@ -0,0 +1,11 @@ +import PageClient from './page-client' + +export const metadata = { + title: 'KeystoneJS: The superpowered Node.js Headless CMS for developers', + description: + 'Build faster and scale further with the programmable open source GraphQL API back-end for structured content projects.', +} + +export default function Page () { + return +} diff --git a/docs/app/(site)/roadmap/page-client.tsx b/docs/app/(site)/roadmap/page-client.tsx new file mode 100644 index 00000000000..ee0d9ba20ea --- /dev/null +++ b/docs/app/(site)/roadmap/page-client.tsx @@ -0,0 +1,528 @@ +/** @jsxImportSource @emotion/react */ + +'use client' + +import Link from 'next/link' + +import { useMediaQuery } from '../../../lib/media' +import { IntroWrapper, IntroHeading, IntroLead } from '../../../components/content/Intro' +import { Highlight } from '../../../components/primitives/Highlight' +import { MWrapper } from '../../../components/content/MWrapper' +import { Type } from '../../../components/primitives/Type' +import { Page } from '../../../components/Page' +import { EndCta } from '../../../components/content/EndCta' +import { Alert } from '../../../components/primitives/Alert' +import { Emoji } from '../../../components/primitives/Emoji' +import { Fragment, type ComponentProps, type ReactNode } from 'react' +import { Gradient } from '../../../components/primitives/Gradient' +import { InlineCode } from '../../../components/primitives/Code' +import { Tick } from '../../../components/icons' + +function TimelineItem ({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} + +function TimelineMarker ({ look }: Pick, 'look'>) { + return ( + + + + + ) +} + +function TimelineWeAreHere () { + const arrowSize = '0.4rem' + const mq = useMediaQuery() + return ( + + + We are here! + + ) +} + +type TimelineContentProps = { + title: ReactNode + children: ReactNode +} & Pick, 'look'> + +function TimelineContent ({ title, look, children }: TimelineContentProps) { + return ( + + + {title} + + + {children} + + + ) +} + +type RoadmapListProps = { + children: ReactNode +} + +function RoadmapList ({ children }: RoadmapListProps) { + const mq = useMediaQuery() + return ( + + {children} + + ) +} + +const roadmapItemSectionStyles = { + margin: '0rem 0 .75rem', + borderRadius: '.4rem', + display: 'inline-block', + padding: '0.1825rem 0.5rem', + fontSize: '0.9rem', + fontWeight: 700, +} + +const roadmapItemSection = { + docs: () => ( + Docs + ), + 'fields and schema': () => ( + + Fields & Schema + + ), + core: () => ( + Core + ), + 'admin ui': () => ( + + Admin UI + + ), +} + +type RoadmapItemProps = { + title: ReactNode + section?: keyof typeof roadmapItemSection + isReleased?: boolean + children: ReactNode +} + +function RoadmapItem ({ title, section, isReleased = false, children }: RoadmapItemProps) { + const Section = section ? roadmapItemSection[section] : null + return ( + + {isReleased && ( + + )} + {Section && } + + {title} + + + {children} + + + ) +} + +function Divider () { + return ( + + ) +} + +export default function Roadmap () { + const mq = useMediaQuery() + + return ( + + + + + The Keystone Roadmap + + + Keystone 6 is actively maintained and steadily improving. We've graduated to the{' '} + @keystone-6 namespace on npm and have a stable set of APIs that + you can confidently build on 🚀 + + + + + To see what we've recently shipped, checkout our release notes + + + + + + + + Surveyed the landscape. Formed concepts around what a next-gen experience would look + like. Started laying the foundations. + + + + + + Published the New Interfaces as{' '} + @keystone-next + on npm. Commenced iteration based on community & internal feedback. + + + + + + + Stabilised the new architecture & APIs. Docs & example projects. Published as{' '} + @keystone-6 on npm. + + + + + + A better dev-configured editing experience. Maturity & features in Keystone core. More + pathways to grow with Keystone. + + + + + What's up next + + + We're using the foundations we shipped in 2021 as a springboard to bring{' '} + developers,{' '} + project owners, and{' '} + editors into more productive ways of working + together. Our key areas of focus are: + + + + + Schema-driven auth that streamlines how users authenticate with + Keystone applications + + + A next-gen Admin UI that unifies developer and editor collaborations + in new and exciting ways + + + Maturing the DX with better Types and capabilities for self-hosting + and media management + + + + + Schema-driven authentication + + + Keystone operates on the schema-driven principle. Right now, authentication in Keystone is + left up to the developers to implement. This leads to every Keystone project having auth + handled in a slightly different way. + + + We want to change that and offer a predictable, schema-driven first-party authentication + solution for Keystone. + + + Next-gen Admin UI + + + Our design and labs teams have re-imagined a state-of-the-art Admin UI. One that is + responsive, accessible and functional. The fruit of this work led to{' '} + Keystar UI, + an internal design system we've been battle-testing with{' '} + Keystatic. + + + + This body of work will elevate the experience of authoring content in Keystone to the same + high standards we have for authoring with Keystone's core APIs. + + + Maturing the Developer Experience + + + At{' '} + + Thinkmill + + , we have a longstanding commitment to open source that includes the likes of{' '} + + react select + + ,{' '} + + classnames + + ,{' '} + + Keystatic + + , and of course Keystone. + + + We'll continue to iterate on this knowledge and learning loop to make Keystone the easiest + way to design and standup a GraphQL API on the web. Going all-in on Typescript has made + the DX so sweet, but we want to take it further so that Keystone's the best + Typescript > GraphQL developer experience in the ecosystem.{' '} + + + + We'll also focus on making better pathways for you to integrate Keystone with the + deployment services you use most, so you can get the most out of where the modern web is + going. Image and file management is an area we're actively iterating on, and we'll have + more to share soon. + + + + Feature roadmap + + + + Here's what we've been working on, and what's coming next. + + + + Recently released + + + + + A way to define a single object in schema that's editable in Admin UI and accessible + in the GraphQL API. Handy for storing website & social settings, API keys, and{' '} + more. + + + + + It's often easier to work with content when the form is grouped into different + sections of related fields. Learn more + + + + + Access your GraphQL APIs from Node.js for greater flexibility when writing apps and + hybrid use-cases. + + + + + + + Current focus + + + + + Dynamically showing fields based on the value of other fields is a great way to + improve editing flow and content integrity. + + + + + Make the Admin UI responsive, accessible and i18n compliant with the battle-tested{' '} + + Keystar UI + {' '} + design system. + + + + + + Next up + + + + + Sometimes you need to manage data in structures that are nested and/or repeating. + We're working on a way to define these in schema and have them stored as JSON field in + the database. + + + + + Certain list types come with a need to order the items they contain. We're looking in + to an approach that's easy to implement in schema. + + + + + We're broadening our list of streamlined scenarios & looking into options for + serverless environments. + + + + + + Further afield + + + + + Built-in tooling for you to give editors a sense of how their content will be consumed + by end users. + + + + + Design your own journey from draft to{' '} + published to meet the unique needs of your content team. + + + + + More powerful interfaces for managing different scales of related data, from small to + really really large + + + + + Long lasting server operations that can change their result over time. Handy for + updating your front-end in real time when important data changes in your backend. + + + + + A built-in schema-driven approach to supporting multilingual content projects. + + + + + If you want to update an item but aren't sure if it exists, this will update the item + if it's there or create a new item with the data you've provided. + + + + + The future of deployment is serverless, and we're tracking the state of the ecosystem + to make sure Keystone is ready for it. + + + + + A way for you and editors to easily search for strings across your entire dataset. + Handy for when you need something specific but don't know where it lives. + + + + + + Got a feature you're after, or want to know more about the future? + + + Join the Keystone conversation on{' '} + + Slack + + ,{' '} + + GitHub + + , and{' '} + + Twitter + + . + + + + + + ) +} diff --git a/docs/app/(site)/roadmap/page.tsx b/docs/app/(site)/roadmap/page.tsx new file mode 100644 index 00000000000..94a54e89c71 --- /dev/null +++ b/docs/app/(site)/roadmap/page.tsx @@ -0,0 +1,10 @@ +import PageClient from './page-client' + +export const metadata = { + title: 'Roadmap', + description: "Discover where KeystoneJS is headed, and why we're going there.", +} + +export default function ForOrganisations () { + return +} diff --git a/docs/pages/why-keystone.tsx b/docs/app/(site)/why-keystone/page-client.tsx similarity index 91% rename from docs/pages/why-keystone.tsx rename to docs/app/(site)/why-keystone/page-client.tsx index 11eee71e283..9ec0383d22d 100644 --- a/docs/pages/why-keystone.tsx +++ b/docs/app/(site)/why-keystone/page-client.tsx @@ -1,52 +1,48 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' +/** @jsxImportSource @emotion/react */ + +'use client' + import Image from 'next/image' import Link from 'next/link' -import { useMediaQuery } from '../lib/media' -import { Highlight } from '../components/primitives/Highlight' -import { Relationship } from '../components/icons/Relationship' -import { WhyKeystone } from '../components/icons/WhyKeystone' -import { MWrapper } from '../components/content/MWrapper' -import { TweetBox } from '../components/content/TweetBox' -import { Typescript } from '../components/icons/Typescript' -import { Automated } from '../components/icons/Automated' -import { Migration } from '../components/icons/Migration' -import { Section } from '../components/content/Section' -import { Button } from '../components/primitives/Button' -import { AdvancedReactCta } from '../components/content/AdvancedReactCta' -import { EndCta } from '../components/content/EndCta' -import { Emoji } from '../components/primitives/Emoji' -import { Updates } from '../components/icons/Updates' -import { Quote } from '../components/content/Quote' -import { Type } from '../components/primitives/Type' -import { ArrowR } from '../components/icons/ArrowR' -import { Code } from '../components/icons/Code' -import { Content } from '../components/icons/Content' -import { Organization } from '../components/icons/Organization' -import { Custom } from '../components/icons/Custom' -import { Editor } from '../components/icons/Editor' -import { Filter } from '../components/icons/Filter' -import { Pill } from '../components/content/Pill' -import { Shield } from '../components/icons/Shield' -import { Watch } from '../components/icons/Watch' -import { Cli } from '../components/icons/Cli' -import { Page } from '../components/Page' -import { IntroHeading } from '../components/content/Intro' +import { useMediaQuery } from '../../../lib/media' +import { Highlight } from '../../../components/primitives/Highlight' +import { Relationship } from '../../../components/icons/Relationship' +import { WhyKeystone } from '../../../components/icons/WhyKeystone' +import { MWrapper } from '../../../components/content/MWrapper' +import { TweetBox } from '../../../components/content/TweetBox' +import { Typescript } from '../../../components/icons/Typescript' +import { Automated } from '../../../components/icons/Automated' +import { Migration } from '../../../components/icons/Migration' +import { Section } from '../../../components/content/Section' +import { Button } from '../../../components/primitives/Button' +import { AdvancedReactCta } from '../../../components/content/AdvancedReactCta' +import { EndCta } from '../../../components/content/EndCta' +import { Emoji } from '../../../components/primitives/Emoji' +import { Updates } from '../../../components/icons/Updates' +import { Quote } from '../../../components/content/Quote' +import { Type } from '../../../components/primitives/Type' +import { ArrowR } from '../../../components/icons/ArrowR' +import { Code } from '../../../components/icons/Code' +import { Content } from '../../../components/icons/Content' +import { Organization } from '../../../components/icons/Organization' +import { Custom } from '../../../components/icons/Custom' +import { Editor } from '../../../components/icons/Editor' +import { Filter } from '../../../components/icons/Filter' +import { Pill } from '../../../components/content/Pill' +import { Shield } from '../../../components/icons/Shield' +import { Watch } from '../../../components/icons/Watch' +import { Cli } from '../../../components/icons/Cli' +import { Page } from '../../../components/Page' +import { IntroHeading } from '../../../components/content/Intro' -import adminUi from '../public/assets/admin-ui.png' +import adminUi from '../../../public/assets/admin-ui.png' export default function WhyKeystonePage () { const mq = useMediaQuery() return ( - + Why Keystone diff --git a/docs/app/(site)/why-keystone/page.tsx b/docs/app/(site)/why-keystone/page.tsx new file mode 100644 index 00000000000..ff1723d09cc --- /dev/null +++ b/docs/app/(site)/why-keystone/page.tsx @@ -0,0 +1,11 @@ +import PageClient from './page-client' + +export const metadata = { + title: 'Why KeystoneJS', + description: + 'More than a backend framework, and more than a Headless CMS, discover why Keystone is the platform for next-gen development workflows and evolution.', +} + +export default function WhyKeystonePage () { + return +} diff --git a/docs/app/actions.ts b/docs/app/actions.ts new file mode 100644 index 00000000000..b55d26a9e10 --- /dev/null +++ b/docs/app/actions.ts @@ -0,0 +1,43 @@ +'use server' + +// ------------------------------ +// Buttondown subscription +// ------------------------------ +export async function subscribeToButtondown (pathname: string, formData: FormData) { + try { + const data = { + email: formData.get('email'), + tags: [ + ...formData.getAll('tags'), + `source:keystonejs.com${pathname}`.substring(0, 80), + ], + } + + const buttondownResponse = await fetch('https://api.buttondown.email/v1/subscribers', { + method: 'POST', + headers: { + Authorization: `Token ${process.env.BUTTONDOWN_API_KEY}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email_address: data.email, + tags: data.tags, + }), + }) + + if (!buttondownResponse.ok) { + const error = await buttondownResponse.json() + return { + // 409 status Conflict has no detail message + error: error?.detail || 'Sorry, an error has occurred — please try again later.', + } + } + + return { success: true } + } catch (error) { + console.error('An error occurred: ', error) + return { + error: 'Sorry, an error has occurred — please try again later.', + } + } +} diff --git a/docs/pages/api/hero-image.tsx b/docs/app/api/hero-image/route.tsx similarity index 84% rename from docs/pages/api/hero-image.tsx rename to docs/app/api/hero-image/route.tsx index 50a923675c2..81faba252ee 100644 --- a/docs/pages/api/hero-image.tsx +++ b/docs/app/api/hero-image/route.tsx @@ -1,15 +1,10 @@ -import React from 'react' import { ImageResponse } from '@vercel/og' -import type { NextRequest } from 'next/server' -import { siteBaseUrl } from '../../lib/og-util' - -export const config = { - runtime: 'experimental-edge', -} +import { type NextRequest } from 'next/server' +import { siteBaseUrl } from '../../../lib/og-util' const bgImgUrl = `url(${siteBaseUrl}/assets/blog/blog-cover-bg.png)` -export const HeroImage = ({ title, type }: { title: string, type?: string }) => { +const HeroImage = ({ title, type }: { title: string, type?: string }) => { const clippedTitle = title.length > 100 ? title.substring(0, 100) + '...' : title let titleFontSize = 96 if (clippedTitle.length > 35) { @@ -26,7 +21,6 @@ export const HeroImage = ({ title, type }: { title: string, type?: string }) => style={{ display: 'flex', backgroundColor: 'white', - // backgroundImage: 'linear-gradient(115.92deg, #1476FF 22.53%, #00ABDA 88.23%)', backgroundImage: bgImgUrl, height: '100%', width: '100%', @@ -107,14 +101,13 @@ export const HeroImage = ({ title, type }: { title: string, type?: string }) => } const interSemiBold = fetch( - new URL('../../public/assets/blog/font/Inter-SemiBold.ttf', import.meta.url) -).then(res => res.arrayBuffer()) + new URL('../../../public/assets/blog/font/Inter-SemiBold.ttf', import.meta.url) +).then((res) => res.arrayBuffer()) const interExtraBold = fetch( - new URL('../../public/assets/blog/font/Inter-ExtraBold.ttf', import.meta.url) -).then(res => res.arrayBuffer()) + new URL('../../../public/assets/blog/font/Inter-ExtraBold.ttf', import.meta.url) +).then((res) => res.arrayBuffer()) -// vercel API route that generates the OG image -export default async function handler (req: NextRequest) { +export async function GET (req: NextRequest) { const interSemiBoldData = await interSemiBold const interExtraBoldData = await interExtraBold @@ -158,3 +151,5 @@ export default async function handler (req: NextRequest) { }) } } + +export const runtime = 'edge' diff --git a/docs/components/Announce.tsx b/docs/components/Announce.tsx index 9a094aa3802..7e6c59326a3 100644 --- a/docs/components/Announce.tsx +++ b/docs/components/Announce.tsx @@ -1,6 +1,6 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' + +/** @jsxImportSource @emotion/react */ + import { type HTMLAttributes, type ReactNode } from 'react' import { Wrapper } from './primitives/Wrapper' diff --git a/docs/components/Breadcrumbs.tsx b/docs/components/Breadcrumbs.tsx index b55d64aa8a5..929611c12ce 100644 --- a/docs/components/Breadcrumbs.tsx +++ b/docs/components/Breadcrumbs.tsx @@ -1,7 +1,8 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { useRouter } from 'next/router' -import { jsx } from '@emotion/react' +/** @jsxImportSource @emotion/react */ + +'use client' + +import { usePathname, useRouter } from 'next/navigation' import Link from 'next/link' import { Type } from './primitives/Type' @@ -10,9 +11,10 @@ type Path = { title: string, href: string } export function Breadcrumbs () { const router = useRouter() + const pathname = usePathname() // remove anchor and split path - const linkPath = new URL(router.asPath, 'https://keystonejs.com').pathname.split('/') + const linkPath = new URL(pathname || '', 'https://keystonejs.com').pathname.split('/') linkPath.shift() const breadcrumbs = linkPath.map((path, i): Path => { diff --git a/docs/components/ContactForm.tsx b/docs/components/ContactForm.tsx index e7f940568e9..5f08596283b 100644 --- a/docs/components/ContactForm.tsx +++ b/docs/components/ContactForm.tsx @@ -1,7 +1,6 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ +/** @jsxImportSource @emotion/react */ + import { Fragment, useState, type ReactNode, type SyntheticEvent, type HTMLAttributes } from 'react' -import { jsx } from '@emotion/react' import { useMediaQuery } from '../lib/media' import { Button } from './primitives/Button' @@ -9,10 +8,9 @@ import { Field } from './primitives/Field' import { Stack } from './primitives/Stack' import { Type } from './primitives/Type' -const validEmail = (email: string) => - /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test( - email - ) +function validEmail (email: string) { + return /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(email) +} const enquiryUrl = 'https://endpoints.thinkmill.com.au/enquiry' diff --git a/docs/components/Footer.tsx b/docs/components/Footer.tsx index 516e821f3e8..031f174c3b9 100644 --- a/docs/components/Footer.tsx +++ b/docs/components/Footer.tsx @@ -1,6 +1,5 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' +/** @jsxImportSource @emotion/react */ + import Link from 'next/link' import { type HTMLAttributes } from 'react' @@ -75,7 +74,7 @@ export function Footer () { For Content management - For Enterprise + For Enterprise ↗ @@ -142,10 +141,10 @@ export function Footer () { - Latest News + Blog - Roadmap + Roadmap Release Notes diff --git a/docs/components/Header.tsx b/docs/components/Header.tsx index 46ae5ca9f45..3036c1e7b9c 100644 --- a/docs/components/Header.tsx +++ b/docs/components/Header.tsx @@ -1,5 +1,7 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ +/** @jsxImportSource @emotion/react */ + +'use client' + import { createContext, useContext, @@ -10,8 +12,7 @@ import { type ReactNode, type RefObject, } from 'react' -import { useRouter } from 'next/router' -import { jsx } from '@emotion/react' +import { useRouter, usePathname } from 'next/navigation' import Link from 'next/link' import debounce from 'lodash.debounce' @@ -75,15 +76,25 @@ function Logo () { } function useCurrentSection () { - const { pathname } = useRouter() - const check = (candidate: string) => pathname.startsWith(candidate) + const pathname = usePathname() + const check = (candidate: string) => pathname?.startsWith(candidate) if (['/updates', '/releases'].some(check)) return '/updates' if (['/why-keystone', '/for-'].some(check)) return '/why-keystone' if (['/docs'].some(check)) return '/docs' if (['/blog'].some(check)) return '/blog' } -function LinkItem ({ children, href }: { children: ReactNode, href: string }) { +function LinkItem ({ + children, + href, + target, + rel, +}: { + children: ReactNode + href: string + target?: string + rel?: string +}) { const mq = useMediaQuery() const currentSection = useCurrentSection() const isActive = href === currentSection @@ -94,6 +105,8 @@ function LinkItem ({ children, href }: { children: ReactNode, href: string }) { isActive={isActive} alwaysVisible href={href} + target={target} + rel={rel} css={{ padding: '0 !important', }} @@ -133,7 +146,7 @@ function FlatMenu ({ const [showContent, setShowContent] = useState(false) const onClickHandler = useCallback(() => { - setShowContent(b => !b) + setShowContent((b) => !b) }, [setShowContent]) const closeMenu = useCallback(() => { @@ -196,7 +209,7 @@ function FlatMenu ({ border: '1px solid var(--border)', borderRadius: '0.25rem', background: 'var(--app-bg)', - gap: '1.25rem', + gap: '.25rem', }} > {items.map(({ href, label }) => { @@ -208,7 +221,7 @@ function FlatMenu ({ alwaysVisible href={href} css={{ - padding: '0 !important', + padding: '0.5rem 0 !important', }} > {label} @@ -224,6 +237,7 @@ function FlatMenu ({ export function Header () { const mq = useMediaQuery() const router = useRouter() + const pathname = usePathname() const menuRef = useRef(null) const headerRef = useRef(null) @@ -258,7 +272,7 @@ export function Header () { useEffect(() => { document.body.style.overflow = 'auto' // search - init field - let searchAttempt = 0 + const searchAttempt = 0 // @ts-expect-error document.getElementById('search-field').disabled = true const loadSearch = (searchAttempt: number) => { @@ -294,8 +308,8 @@ export function Header () { // yoo hooo loadSearch(searchAttempt) // search - keyboard shortcut - let keysPressed: { [key: KeyboardEvent['key']]: boolean } = {} - document.body.addEventListener('keydown', event => { + const keysPressed: { [key: KeyboardEvent['key']]: boolean } = {} + document.body.addEventListener('keydown', (event) => { // If we're typing in an input, don't ever focus the search input if ( document.activeElement && @@ -310,7 +324,7 @@ export function Header () { document.getElementById('search-field')?.focus() } }) - document.body.addEventListener('keyup', event => { + document.body.addEventListener('keyup', (event) => { delete keysPressed[event.key] }) }, []) @@ -328,11 +342,8 @@ export function Header () { }, []) useEffect(() => { - router.events.on('routeChangeComplete', handleClose) - return () => { - router.events.off('routeChangeComplete', handleClose) - } - }, [router.events, handleClose]) + handleClose() + }, [pathname]) return ( @@ -386,9 +397,9 @@ export function Header () { { label: 'For Developers', href: '/for-developers' }, { label: 'For Organisations', href: '/for-organisations' }, { label: 'For Content Management', href: '/for-content-management' }, - { label: 'Our Roadmap', href: '/updates/roadmap' }, + { label: 'Our Roadmap', href: '/roadmap' }, { label: 'GitHub Releases', href: 'https://github.com/keystonejs/keystone/releases' }, - { label: 'Enterprise', href: '/enterprise' }, + { label: 'Enterprise', href: 'https://www.thinkmill.com.au/services/keystone' }, ]} /> @@ -430,7 +441,13 @@ export function Header () { display: ['none', null, 'inline-block'], })} > - Enterprise + + Enterprise ↗ + = { code: InlineCode, CodeBlock (props: { content: string, language: string }) { - return ( + return ( {props.content} @@ -82,37 +84,4 @@ export function Markdoc (props: { content: RenderableTreeNodes }) { } return render(props.content) as JSX.Element -} - -export type HeadingType = { - id: string - depth: number - label: string -} - -export function extractHeadings (content: Tag): HeadingType[] { - const headings: HeadingType[] = [] - for (const child of content.children) { - if (isTag(child) && child.name === 'Heading') { - headings.push({ - id: child.attributes.id, - depth: child.attributes.level, - label: stringifyDocContent(child), - }) - } - } - return headings -} - -function stringifyDocContent (node: RenderableTreeNode): string { - if (typeof node === 'string') { - return node - } - if (Array.isArray(node)) { - return node.map(stringifyDocContent).join('') - } - if (!isTag(node)) { - return '' - } - return node.children.map(stringifyDocContent).join('') -} +} \ No newline at end of file diff --git a/docs/components/MobileMenu.tsx b/docs/components/MobileMenu.tsx index be2ac9fb1bf..521e1bcb15d 100644 --- a/docs/components/MobileMenu.tsx +++ b/docs/components/MobileMenu.tsx @@ -1,6 +1,5 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' +/** @jsxImportSource @emotion/react */ + import Link from 'next/link' // import { useRouter } from 'next/router'; import { Fragment, useEffect, type ReactNode, type MouseEvent } from 'react' @@ -49,6 +48,7 @@ export function MobileMenu ({ handleClose }: MobileMenuProps) { For Developers For Organisations For Content Management + Roadmap For Enterprise - {/* Updates */} Blog - Roadmap GitHub Releases diff --git a/docs/components/Page.tsx b/docs/components/Page.tsx index 8ebd3f0db36..ec2863db525 100644 --- a/docs/components/Page.tsx +++ b/docs/components/Page.tsx @@ -1,258 +1,106 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ +/** @jsxImportSource @emotion/react */ + +'use client' + import { useRef, Fragment, type ReactNode } from 'react' -import { useRouter } from 'next/router' -import { jsx } from '@emotion/react' -import Head from 'next/head' +import { usePathname } from 'next/navigation' import { useMediaQuery } from '../lib/media' import { TableOfContents } from './docs/TableOfContents' import { Wrapper } from './primitives/Wrapper' import { EditButton } from './primitives/EditButton' import { Breadcrumbs } from './Breadcrumbs' -import { Sidebar } from './docs/Sidebar' import { Stack } from './primitives/Stack' import { Header } from './Header' -import { Footer, DocsFooter } from './Footer' +import { Footer } from './Footer' import { type HeadingType } from './Markdoc' -function OpenGraph ({ - title, - description, - ogImage, -}: { - title: string - description: string - ogImage?: string -}) { - const siteUrl = process.env.siteUrl - if (!ogImage) { - ogImage = `${siteUrl}/og-image-landscape.png` - } - return ( - - {title} - - - - - - - - - - - - ) -} - -const pagesWithUpdatesSidebar = ['/updates'] -export function DocsPage ({ - children, - headings = [], - noProse, - noRightNav, - title, - description, - ogImage, - isIndexPage, - editPath, -}: { - children: ReactNode - headings?: HeadingType[] - noProse?: boolean - noRightNav?: boolean - title: string - description: string - ogImage?: string - isIndexPage?: boolean - editPath?: string -}) { - const contentRef = useRef(null) - const mq = useMediaQuery() - const { pathname } = useRouter() - const isUpdatesPage = pagesWithUpdatesSidebar.some(p => pathname.startsWith(p)) - - const metaTitle = title ? `${title} - Keystone 6 Documentation` : `Keystone 6 Documentation` - - return ( - - - - - - - - - - - - - - - {children} - - - {!!headings.length && !noRightNav && ( - - )} - - - - - - ) -} - export function BlogPage ({ children, headings = [], noRightNav, - title, - description, - ogImage, isIndexPage, editPath, }: { children: ReactNode headings?: HeadingType[] noRightNav?: boolean - title: string - description: string - ogImage?: string isIndexPage?: boolean editPath?: string }) { const contentRef = useRef(null) const mq = useMediaQuery() - const { pathname } = useRouter() - - const metaTitle = title ? `${title} | Keystone Blog` : `Keystone Blog` + const pathname = usePathname() return ( - - - + + - - - - - - + - - - - {children} - - - {!!headings.length && !noRightNav && ( - - )} - - - - - + + + + {children} + + + {!!headings.length && !noRightNav && ( + + )} + + + + ) } -export function Page ({ - children, - title, - description, - ogImage, -}: { - children: ReactNode - title: string - description: string - ogImage?: string -}) { - const metaTitle = title ? `${title} - Keystone 6` : `Keystone 6` +export function Page ({ children }: { children: ReactNode }) { return ( - - /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test( - email - ) - -const signupURL = 'https://signup.keystonejs.cloud/api/newsletter-signup' +import { usePathname } from 'next/navigation' type SubscriptFormProps = { autoFocus?: boolean @@ -22,89 +17,112 @@ type SubscriptFormProps = { } & HTMLAttributes export function SubscribeForm ({ autoFocus, stacked, children, ...props }: SubscriptFormProps) { - const [email, setEmail] = useState('') - const [loading, setLoading] = useState(false) + const pathname = usePathname() + const mq = useMediaQuery() + const [isPending, startTransition] = useTransition() + const [error, setError] = useState(null) const [formSubmitted, setFormSubmitted] = useState(false) - const mq = useMediaQuery() - const onSubmit = (event: SyntheticEvent) => { - event.preventDefault() - setError(null) - // Check if user wants to subscribe. - // and there's a valid email address. - // Basic validation check on the email? - setLoading(true) - if (validEmail(email)) { - // if good add email to mailing list - // and redirect to dashboard. - return fetch(signupURL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - email, - source: '@keystone-6/website', - }), - }) - .then(res => { - if (res.status !== 200) { - // We explicitly set the status in our endpoint - // any status that isn't 200 we assume is a failure - // which we want to surface to the user - res.json().then(({ error }) => { - setError(error) - setLoading(false) - }) - } else { - setFormSubmitted(true) - } - }) - .catch(err => { - // network errors or failed parse - setError(err.toString()) - setLoading(false) - }) - } else { - setLoading(false) - // if email fails validation set error message - setError('Please enter a valid email') - return - } + // Augment the server action with the pathname + const subscribeToButtondownWithPathname = subscribeToButtondown.bind(null, pathname) + + async function submitAction (formData: FormData) { + startTransition(async () => { + const response = await subscribeToButtondownWithPathname(formData) + if (response.error) return setError(response.error) + if (response.success) return setFormSubmitted(true) + }) } return !formSubmitted ? ( {children} - + + + Email address + setEmail(e.target.value)} css={mq({ maxWidth: '25rem', margin: ['0 auto', 0], })} + name="email" + id="email" + required /> - + + + + + + Keystone news + + + + + + Thinkmill news ( + + example + + ) + + + + + {error ? 'Try again' : 'Subscribe'} - {error ? {error} : null} + {error ? ( + {error} + ) : null} ) : ( - ❤️ Thank you for subscribing! + ❤️ Thank you! Please check your email to confirm your subscription. ) } diff --git a/docs/components/Theme.tsx b/docs/components/Theme.tsx index 3498f0c6171..f8daf02264b 100644 --- a/docs/components/Theme.tsx +++ b/docs/components/Theme.tsx @@ -1,6 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx, Global } from '@emotion/react' +import { Global } from '@emotion/react' import { COLORS, SPACE, TYPE, TYPESCALE } from '../lib/TOKENS' diff --git a/docs/components/ThemeToggle.tsx b/docs/components/ThemeToggle.tsx index a4ddb1c1f3a..769d7064c81 100644 --- a/docs/components/ThemeToggle.tsx +++ b/docs/components/ThemeToggle.tsx @@ -1,19 +1,11 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ +/** @jsxImportSource @emotion/react */ + import { Fragment, useState, useEffect, type HTMLAttributes } from 'react' -import { jsx } from '@emotion/react' import { type COLORS } from '../lib/TOKENS' import { LightMode } from './icons/LightMode' import { DarkMode } from './icons/DarkMode' - -function ModeIcon ({ theme }: { theme: 'light' | 'dark' }) { - if (theme === 'dark') { - return - } - - return -} +import { useThemeContext } from '../app/(site)/layout-client' export function ThemeToggle (props: HTMLAttributes) { /* @@ -22,29 +14,14 @@ export function ThemeToggle (props: HTMLAttributes) { even if the theme is dark mode based on system preference. So we render the toggle only on the client */ - const [theme, setTheme] = useState(null) - - useEffect(() => { - const currentTheme = document.documentElement.getAttribute('data-theme') as 'light' | 'dark' - setTheme(currentTheme) - }, [setTheme]) - - const handleThemeChange = () => { - const newTheme = theme === 'dark' ? 'light' : 'dark' - if (newTheme === 'dark') { - document.documentElement.setAttribute('data-theme', 'dark') - } else { - document.documentElement.setAttribute('data-theme', 'light') - } - setTheme(newTheme) - localStorage.setItem('theme', newTheme) - } + const theme = useThemeContext() return ( { + theme.setTheme(theme.theme === 'dark' ? 'light' : 'dark') + }} css={{ display: 'inline-flex', appearance: 'none', @@ -64,7 +41,18 @@ export function ThemeToggle (props: HTMLAttributes) { }} {...props} > - {theme === null ? : } + + ) diff --git a/docs/components/content/AdvancedReactCta.tsx b/docs/components/content/AdvancedReactCta.tsx index 8574136c0e9..dc1def6a2ec 100644 --- a/docs/components/content/AdvancedReactCta.tsx +++ b/docs/components/content/AdvancedReactCta.tsx @@ -1,7 +1,6 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ +/** @jsxImportSource @emotion/react */ + import type { HTMLAttributes } from 'react' -import { jsx } from '@emotion/react' import Image from 'next/image' import wesBosCta from '../../public/assets/wesbos-cta.jpg' diff --git a/docs/components/content/CodeBox.tsx b/docs/components/content/CodeBox.tsx index bd10b71cb8b..9ef2fc63685 100644 --- a/docs/components/content/CodeBox.tsx +++ b/docs/components/content/CodeBox.tsx @@ -1,7 +1,6 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ +/** @jsxImportSource @emotion/react */ + import { type HTMLAttributes, useEffect, useState } from 'react' -import { jsx } from '@emotion/react' import copy from 'clipboard-copy' import { CheckIcon } from '@keystone-ui/icons/icons/CheckIcon' diff --git a/docs/components/content/CodeWindow.tsx b/docs/components/content/CodeWindow.tsx index a764806cc46..9795bf8ef2a 100644 --- a/docs/components/content/CodeWindow.tsx +++ b/docs/components/content/CodeWindow.tsx @@ -1,7 +1,6 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ +/** @jsxImportSource @emotion/react */ + import type { HTMLAttributes } from 'react' -import { jsx } from '@emotion/react' import { useMediaQuery } from '../../lib/media' diff --git a/docs/components/content/CommunityCta.tsx b/docs/components/content/CommunityCta.tsx index 5134bf8e3ea..b35eabe5770 100644 --- a/docs/components/content/CommunityCta.tsx +++ b/docs/components/content/CommunityCta.tsx @@ -1,7 +1,6 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ +/** @jsxImportSource @emotion/react */ + import type { HTMLAttributes } from 'react' -import { jsx } from '@emotion/react' import Image from 'next/image' import communityMap from '../../public/assets/community-map.png' diff --git a/docs/components/content/CustomerCard.tsx b/docs/components/content/CustomerCard.tsx index a094e15a6f5..784f1f38c7e 100644 --- a/docs/components/content/CustomerCard.tsx +++ b/docs/components/content/CustomerCard.tsx @@ -1,7 +1,6 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ +/** @jsxImportSource @emotion/react */ + import type { HTMLAttributes } from 'react' -import { jsx } from '@emotion/react' import { Type } from '../primitives/Type' import { type IconProps } from '../icons/util' diff --git a/docs/components/content/EndCta.tsx b/docs/components/content/EndCta.tsx index 978d31758f6..4d671881a93 100644 --- a/docs/components/content/EndCta.tsx +++ b/docs/components/content/EndCta.tsx @@ -1,6 +1,6 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' + +/** @jsxImportSource @emotion/react */ + import { type HTMLAttributes } from 'react' import { Highlight } from '../primitives/Highlight' @@ -73,7 +73,7 @@ export function EndCta ({ grad = 'grad1', ...props }: EndCtaProps) { - + Get started diff --git a/docs/components/content/Intro.tsx b/docs/components/content/Intro.tsx index 8eb7077ef8b..66aca6560fe 100644 --- a/docs/components/content/Intro.tsx +++ b/docs/components/content/Intro.tsx @@ -1,7 +1,6 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ +/** @jsxImportSource @emotion/react */ + import { type HTMLAttributes } from 'react' -import { jsx } from '@emotion/react' import { Type } from '../primitives/Type' diff --git a/docs/components/content/MWrapper.tsx b/docs/components/content/MWrapper.tsx index 16fbb3c07d6..1ec02e0f6ac 100644 --- a/docs/components/content/MWrapper.tsx +++ b/docs/components/content/MWrapper.tsx @@ -1,7 +1,6 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ +/** @jsxImportSource @emotion/react */ + import type { ElementType, HTMLAttributes } from 'react' -import { jsx } from '@emotion/react' import { useMediaQuery } from '../../lib/media' diff --git a/docs/components/content/Pill.tsx b/docs/components/content/Pill.tsx index 9a9105a812d..86b9ab8db23 100644 --- a/docs/components/content/Pill.tsx +++ b/docs/components/content/Pill.tsx @@ -1,6 +1,5 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' +/** @jsxImportSource @emotion/react */ + import { type HTMLAttributes } from 'react' type PillProps = { diff --git a/docs/components/content/PillCta.tsx b/docs/components/content/PillCta.tsx index 36518b8fcec..cbc91730e62 100644 --- a/docs/components/content/PillCta.tsx +++ b/docs/components/content/PillCta.tsx @@ -1,6 +1,6 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' + +/** @jsxImportSource @emotion/react */ + import { type HTMLAttributes } from 'react' import { useMediaQuery } from '../../lib/media' diff --git a/docs/components/content/Quote.tsx b/docs/components/content/Quote.tsx index 764dd26ec10..632ca40026a 100644 --- a/docs/components/content/Quote.tsx +++ b/docs/components/content/Quote.tsx @@ -1,6 +1,5 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' +/** @jsxImportSource @emotion/react */ + import { type HTMLAttributes } from 'react' import { useMediaQuery } from '../../lib/media' diff --git a/docs/components/content/Section.tsx b/docs/components/content/Section.tsx index 669ab8a838e..189b9215806 100644 --- a/docs/components/content/Section.tsx +++ b/docs/components/content/Section.tsx @@ -1,7 +1,6 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ +/** @jsxImportSource @emotion/react */ + import type { HTMLAttributes } from 'react' -import { jsx } from '@emotion/react' import { useMediaQuery } from '../../lib/media' diff --git a/docs/components/content/TweetBox.tsx b/docs/components/content/TweetBox.tsx index 0205cb6cca0..8f62b23e444 100644 --- a/docs/components/content/TweetBox.tsx +++ b/docs/components/content/TweetBox.tsx @@ -1,7 +1,6 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ +/** @jsxImportSource @emotion/react */ + import type { HTMLAttributes } from 'react' -import { jsx } from '@emotion/react' import { Type } from '../primitives/Type' import { Quote } from '../icons/Quote' diff --git a/docs/components/docs/ComingSoon.tsx b/docs/components/docs/ComingSoon.tsx index 04e71539d17..cf341be0b30 100644 --- a/docs/components/docs/ComingSoon.tsx +++ b/docs/components/docs/ComingSoon.tsx @@ -1,6 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' +/** @jsxImportSource @emotion/react */ import { Alert } from '../primitives/Alert' diff --git a/docs/components/docs/CommunitySlackCTA.tsx b/docs/components/docs/CommunitySlackCTA.tsx index 8534baf3674..9ee4115c2ee 100644 --- a/docs/components/docs/CommunitySlackCTA.tsx +++ b/docs/components/docs/CommunitySlackCTA.tsx @@ -1,6 +1,5 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' + +/** @jsxImportSource @emotion/react */ import { Alert } from '../primitives/Alert' import { Button } from '../primitives/Button' diff --git a/docs/components/docs/CopyToClipboard.tsx b/docs/components/docs/CopyToClipboard.tsx index ccd49113b63..bb9782a8524 100644 --- a/docs/components/docs/CopyToClipboard.tsx +++ b/docs/components/docs/CopyToClipboard.tsx @@ -1,6 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' +/** @jsxImportSource @emotion/react */ import { Link } from '../icons/Link' diff --git a/docs/components/docs/DocsFooter.tsx b/docs/components/docs/DocsFooter.tsx deleted file mode 100644 index 219f598b4cd..00000000000 --- a/docs/components/docs/DocsFooter.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' - -import { Wrapper } from '../primitives/Wrapper' -import { Emoji } from '../primitives/Emoji' - -export function DocsFooter () { - return ( - - ) -} diff --git a/docs/components/docs/DocsLayout.tsx b/docs/components/docs/DocsLayout.tsx new file mode 100644 index 00000000000..127d57a6720 --- /dev/null +++ b/docs/components/docs/DocsLayout.tsx @@ -0,0 +1,6 @@ +import { DocsLayoutClient } from './DocsLayoutClient' +import { DocsNavigation } from './docs-navigation' + +export async function DocsLayout (props) { + return } /> +} diff --git a/docs/components/docs/DocsLayoutClient.tsx b/docs/components/docs/DocsLayoutClient.tsx new file mode 100644 index 00000000000..e05c4a5d7ea --- /dev/null +++ b/docs/components/docs/DocsLayoutClient.tsx @@ -0,0 +1,104 @@ +/** @jsxImportSource @emotion/react */ + +'use client' + +import { type ReactNode, useRef } from 'react' +import { usePathname } from 'next/navigation' +import { Header } from '../Header' +import { Wrapper } from '../primitives/Wrapper' +import { Sidebar } from './Sidebar' +import { Stack } from '../primitives/Stack' +import { Breadcrumbs } from '../Breadcrumbs' +import { EditButton } from '../primitives/EditButton' +import { TableOfContents } from './TableOfContents' + +import { useMediaQuery } from '../../lib/media' +import { DocsFooter } from '../Footer' +import { type HeadingType } from '../../markdoc/headings' + +export function DocsLayoutClient ({ + children, + headings = [], + noProse, + noRightNav, + isIndexPage, + editPath, + docsNavigation +}: { + children: ReactNode + headings?: HeadingType[] + noProse?: boolean + noRightNav?: boolean + isIndexPage?: boolean + editPath?: string + docsNavigation: ReactNode +}) { + const contentRef = useRef(null) + const mq = useMediaQuery() + const pathname = usePathname() + + return ( + + + + + + + + + + + + + {children} + + + {!!headings.length && !noRightNav && ( + + )} + + + + + ) +} \ No newline at end of file diff --git a/docs/components/docs/DocumentEditorDemo.tsx b/docs/components/docs/DocumentEditorDemo.tsx index 35dacf5375b..c72651873d4 100644 --- a/docs/components/docs/DocumentEditorDemo.tsx +++ b/docs/components/docs/DocumentEditorDemo.tsx @@ -1,5 +1,7 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ +/** @jsxImportSource @emotion/react */ + +'use client' + import React, { type ReactNode, useContext, useEffect, useMemo, useState } from 'react' import { type DocumentFeatures } from '@keystone-6/fields-document/views' import { @@ -175,7 +177,7 @@ function objToShorthand< Obj extends Record> > (obj: Obj): Obj | true | undefined { const values = Object.values(obj) - let state: (typeof values)[number] = values[0]! + const state: (typeof values)[number] = values[0]! for (const val of values) { if (val !== state || (val !== undefined && val !== true)) { return obj diff --git a/docs/components/docs/ExamplesList.tsx b/docs/components/docs/ExamplesList.tsx deleted file mode 100644 index f63c6818dcf..00000000000 --- a/docs/components/docs/ExamplesList.tsx +++ /dev/null @@ -1,160 +0,0 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' - -import { Well } from '../primitives/Well' -import { useMediaQuery } from '../../lib/media' -import { InlineCode } from '../../components/primitives/Code' - -export function Examples () { - const mq = useMediaQuery() - return ( - - - A basic Blog schema with Posts and Authors. Use this as a starting place for learning how to - use Keystone. It’s also a starter for other feature projects. - - - A basic Task Management app, with Tasks and People who can be assigned to tasks. Great for - learning how to use Keystone. It’s also a starter for other feature projects. - - - Shows you how to extend the Keystone GraphQL API with custom queries and mutations. Builds - upon the Blog starter project. - - - Demonstrates how to use default values for fields. Builds upon the Task Manager starter - project. - - - Implements virtual fields in a Keystone list. Builds on the Blog starter project. - - - Illustrates how to configure document fields in your Keystone - system and render their data in a frontend application. Builds on the Blog starter project. - - - Shows you how to write tests against the GraphQL API to your Keystone system. Builds on the - Authentication example project. - - - Adds password-based authentication to the Task Manager starter project. - - - Illustrates how to use the json field type. - - - Adds a custom Admin UI view to a json field which provides a - customised editing experience for users. - - - Adds a custom field type based on the integer field type which lets - users rate items on a 5-star scale. - - - Adds a custom page in the Admin UI. - - - Adds a custom logo component in the Admin UI. - - - Adds a custom Navigation component to the Admin UI. - - - Example to demonstrate customisation of Keystone's document field and document renderer. - - - ) -} diff --git a/docs/components/docs/FeaturedCard.tsx b/docs/components/docs/FeaturedCard.tsx new file mode 100644 index 00000000000..e2ea1f60842 --- /dev/null +++ b/docs/components/docs/FeaturedCard.tsx @@ -0,0 +1,62 @@ +/** @jsxImportSource @emotion/react */ + +'use client' + +import { useId } from 'react' +import { Well } from '../primitives/Well' +import { Markdoc } from '../Markdoc' +import { useMediaQuery } from '../../lib/media' +import { type Gradient } from '../../keystatic/gradient-selector' +import { RenderableTreeNode, type Tag } from '@markdoc/markdoc' + +export function FeaturedCard ({ + label, + description, + href, + gradient = 'grad1', +}: { + label: string + description: Tag | null + href: string + gradient?: Gradient +}) { + const id = useId() + return ( + + {description?.children.map((child, i) => ( + + ))} + + ) +} + +export function FullWidthCardContainer ({ children }: { children: React.ReactNode }) { + const mq = useMediaQuery() + return ( + + {children} + + ) +} + +export function SplitCardContainer ({ children }: { children: React.ReactNode }) { + const mq = useMediaQuery() + return ( + + {children} + + ) +} diff --git a/docs/components/docs/GitHubExamplesCTA.tsx b/docs/components/docs/GitHubExamplesCTA.tsx index e4753ef1f86..6902eb01e7a 100644 --- a/docs/components/docs/GitHubExamplesCTA.tsx +++ b/docs/components/docs/GitHubExamplesCTA.tsx @@ -1,6 +1,5 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' + +/** @jsxImportSource @emotion/react */ import { Alert } from '../primitives/Alert' import { Button } from '../primitives/Button' diff --git a/docs/components/docs/Heading.tsx b/docs/components/docs/Heading.tsx index 5ae725e74a7..28437f957b3 100644 --- a/docs/components/docs/Heading.tsx +++ b/docs/components/docs/Heading.tsx @@ -1,7 +1,8 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ +/** @jsxImportSource @emotion/react */ + +'use client' + import slugify from '@sindresorhus/slugify' -import { jsx } from '@emotion/react' import { type ReactNode } from 'react' import { HeadingIdLink } from './CopyToClipboard' diff --git a/docs/components/docs/Keystone5DocsCTA.tsx b/docs/components/docs/Keystone5DocsCTA.tsx index d600e9365c4..c873c55f5a7 100644 --- a/docs/components/docs/Keystone5DocsCTA.tsx +++ b/docs/components/docs/Keystone5DocsCTA.tsx @@ -1,6 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' +/** @jsxImportSource @emotion/react */ import { Alert } from '../primitives/Alert' diff --git a/docs/components/docs/KeystoneExperience.tsx b/docs/components/docs/KeystoneExperience.tsx new file mode 100644 index 00000000000..3d49c8dc214 --- /dev/null +++ b/docs/components/docs/KeystoneExperience.tsx @@ -0,0 +1,217 @@ +/** @jsxImportSource @emotion/react */ + +'use client' + +import Link from 'next/link' + +import { Bulb } from '../icons/Bulb' +import { Content } from '../icons/Content' +import { Code } from '../icons/Code' +import { Organization } from '../icons/Organization' +import { Video } from '../icons/Video' + +import { useMediaQuery } from '../../lib/media' +import { Type } from '../primitives/Type' + +export function KeystoneExperience () { + const mq = useMediaQuery() + return ( + <> + + The Keystone Experience + + + + Discover the vision behind Keystone and what it's like to work with. If you’ve just heard + of Keystone, start here first: + + a': { + borderRadius: '1rem', + boxShadow: '0 0 5px var(--shadow)', + padding: '1.5rem', + color: 'var(--app-bg)', + transition: 'box-shadow 0.2s ease, transform 0.2s ease, padding 0.2s ease', + textDecoration: 'none !important', + '&:hover, &:focus': { + boxShadow: '0 7px 21px var(--shadow)', + transform: 'translateY(-4px)', + }, + '& svg': { + height: '2rem', + }, + }, + })} + > + + + + Video Intro → + + + Learn how Keystone’s leading a new generation of content management tools. + + + + + + Why Keystone → + + + The makers. The vision. What’s in the box, and what you can build with it. + + + + + a': { + borderRadius: '1rem', + boxShadow: '0 0 5px var(--shadow)', + padding: '1.5rem', + color: 'var(--app-bg)', + transition: 'box-shadow 0.2s ease, transform 0.2s ease, padding 0.2s ease', + textDecoration: 'none !important', + '&:hover, &:focus': { + boxShadow: '0 7px 21px var(--shadow)', + transform: 'translateY(-4px)', + }, + '& svg': { + height: '2rem', + }, + }, + })} + > + + + + For Developers → + + + Built the way you’d want it made. Keystone fits with the tools you know and love. + + + + + + For Editors → + + + The configurable editing environment you need to do your best work. + + + + + + For Organisations → + + + Own your data. Start fast. Find your audience anywhere. Scale on your terms. + + + + > + ) +} diff --git a/docs/components/docs/Navigation.tsx b/docs/components/docs/Navigation.tsx index 1deed57b8c9..31dcd68fb26 100644 --- a/docs/components/docs/Navigation.tsx +++ b/docs/components/docs/Navigation.tsx @@ -1,5 +1,7 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ +/** @jsxImportSource @emotion/react */ + +'use client' + import { type AnchorHTMLAttributes, type ReactNode, @@ -8,8 +10,7 @@ import { useContext, useMemo, } from 'react' -import { useRouter } from 'next/router' -import { jsx } from '@emotion/react' +import { usePathname } from 'next/navigation' import Link from 'next/link' import { useMediaQuery } from '../../lib/media' @@ -39,11 +40,11 @@ export const NavContextProvider = ({ children }: { children: ReactNode }) => { const expandSection = (title: string) => { const isSectionAlreadyExpanded = !collapsedSections.includes(title) if (!isSectionAlreadyExpanded) { - setCollapsedSections(collapsedSections.filter(cs => cs !== title)) + setCollapsedSections(collapsedSections.filter((cs) => cs !== title)) } } const isSectionCollapsed = (title: string) => { - return collapsedSections.some(cs => cs === title) + return collapsedSections.some((cs) => cs === title) } return { isSectionCollapsed, collapseSection, expandSection } @@ -66,7 +67,7 @@ type NavSectionProps = { children: ReactNode } -function NavSection ({ title, children }: NavSectionProps) { +export function NavSection ({ title, children }: NavSectionProps) { const { isSectionCollapsed, collapseSection, expandSection } = useNavContext() const isCollapsed = isSectionCollapsed(title) return ( @@ -129,9 +130,9 @@ export function NavItem ({ alwaysVisible, ...props }: NavItemProps) { - const { asPath } = useRouter() + const pathname = usePathname() const mq = useMediaQuery() - const isActive = typeof _isActive !== 'undefined' ? _isActive : asPath === href + const isActive = typeof _isActive !== 'undefined' ? _isActive : pathname === href const ctx = useHeaderContext() const isMobileNavOpen = ctx ? ctx.mobileNavIsOpen : true const desktopOpenState = ctx ? ctx.desktopOpenState : -1 @@ -162,8 +163,8 @@ type PrimaryNavItemProps = { } & AnchorHTMLAttributes export function PrimaryNavItem ({ href, children }: PrimaryNavItemProps) { - const { asPath } = useRouter() - const isActive = asPath === href + const pathname = usePathname() + const isActive = pathname === href const ctx = useHeaderContext() const isMobileNavOpen = ctx ? ctx.mobileNavIsOpen : true const desktopOpenState = ctx ? ctx.desktopOpenState : -1 @@ -189,124 +190,10 @@ export function PrimaryNavItem ({ href, children }: PrimaryNavItemProps) { ) } -export function DocsNavigation () { - return ( - // - - Docs Home - Getting Started - Walkthroughs - Examples - - Overview - Command Line - Relationships - Choosing a Database - - Database Migration New - - - Query Filters Updated - - - Hooks Updated - - - Auth & Access Control New - - - Images & Files New - - - GraphQL Schema ExtensionNew - - Testing - Document Fields - Document Field Demo - Virtual Fields - Custom Fields - {/* Disable placeholder for now */} - {/* - Custom Field Views - */} - Custom Admin UI Logo - Custom Admin UI Pages - Custom Admin UI Navigation - - - Overview - Config - Lists - Authentication - - Access Control Updated - - - Hooks Updated - - Session - - - - Overview - BigInt - Calendar Day - Checkbox - Cloudinary Image - Decimal - Document - File - Float - Image - Integer - JSON - Multiselect - Password - Relationship - Select - Text - Timestamp - Virtual - - - - Overview - getContext - Query - DB - - - - - Overview Updated - - - Query Filters Updated - - - - Telemetry - - - // - ) -} - -export function UpdatesNavigation () { +export function DocsNavigation ({ docsNavigation }: { docsNavigation?: React.ReactNode }) { return ( - - Latest News - Roadmap - + {docsNavigation} ) -} +} \ No newline at end of file diff --git a/docs/components/docs/Sidebar.tsx b/docs/components/docs/Sidebar.tsx index 3e27bec94e9..f5df3473295 100644 --- a/docs/components/docs/Sidebar.tsx +++ b/docs/components/docs/Sidebar.tsx @@ -1,17 +1,14 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' +/** @jsxImportSource @emotion/react */ import { useMediaQuery } from '../../lib/media' -import { DocsNavigation, UpdatesNavigation } from './Navigation' +import { DocsNavigation } from './Navigation' type SidebarProps = { - isUpdatesPage?: boolean + docsNavigation?: React.ReactNode } -export function Sidebar ({ isUpdatesPage }: SidebarProps) { +export function Sidebar ({ docsNavigation }: SidebarProps) { const mq = useMediaQuery() - const Navigation = isUpdatesPage ? UpdatesNavigation : DocsNavigation return ( diff --git a/docs/components/docs/TableOfContents.tsx b/docs/components/docs/TableOfContents.tsx index 9a980797cbc..67f04a98b0c 100644 --- a/docs/components/docs/TableOfContents.tsx +++ b/docs/components/docs/TableOfContents.tsx @@ -1,7 +1,8 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ +/** @jsxImportSource @emotion/react */ + +'use client' + import { useState, useEffect } from 'react' -import { jsx } from '@emotion/react' import { useMediaQuery } from '../../lib/media' import { Type } from '../primitives/Type' @@ -31,8 +32,8 @@ export function TableOfContents ({ container: React.RefObject headings: Heading[] }) { - let [visibleIds, setVisibleIds] = useState>([]) - let [lastVisibleId, setLastVisbleId] = useState(null) + const [visibleIds, setVisibleIds] = useState>([]) + const [lastVisibleId, setLastVisbleId] = useState(null) const mq = useMediaQuery() @@ -40,7 +41,7 @@ export function TableOfContents ({ useEffect(() => { if (container.current) { - let allIds = headings.map(h => h.id) + const allIds = headings.map(h => h.id) const observer = new IntersectionObserver(entries => { entries.forEach(entry => { const targetId: string | null = entry.target.getAttribute('id') @@ -61,7 +62,7 @@ export function TableOfContents ({ }, [container, headings]) // catch if we're in a long gap between headings and resolve to the last available. - let activeId = visibleIds[0] || lastVisibleId + const activeId = visibleIds[0] || lastVisibleId return ( @@ -86,7 +87,7 @@ export function TableOfContents ({ {headings.map((h: Heading, i: number) => { - let isActive = activeId === h.id + const isActive = activeId === h.id const slug = `#${h.id}` return ( - - - Take a tour of Keystone in minutes with our CLI starter project - - - - - Get Keystone up and running with your first content type - - - Connect two content types and learn how to configure the appearance of field inputs - - - Support publishing needs with Keystone's select and{' '} - timestamp fields - - - Add sessions, password protection, and a sign-in screen to your Keystone app - - - Add a powerful document field to your app and learn how to - configure it to meet your needs - - - - ) -} diff --git a/docs/components/docs/docs-navigation/client.tsx b/docs/components/docs/docs-navigation/client.tsx new file mode 100644 index 00000000000..b9139fef6b4 --- /dev/null +++ b/docs/components/docs/docs-navigation/client.tsx @@ -0,0 +1,48 @@ +/** @jsxImportSource @emotion/react */ + +'use client' + +import { type NavigationMap } from '.' +import { Badge } from '../../primitives/Badge' +import { NavItem, NavSection } from '../Navigation' + +function KeystaticNavItem ({ item }: { item: NonNullable[number]['items'][number] }) { + return ( + + {item.label} + {/* Status badges */} + {item.status !== 'default' && ' '} + {(item.status === 'new' || item.status === 'updated') && ( + {item.status} + )} + + ) +} + +export function DocsNavigationClient ({ navigationMap }: { navigationMap: NavigationMap }) { + if (!navigationMap) return null + + return ( + + {navigationMap && + navigationMap.map((group, i) => ( + + {/* No collapsible section for the first group */} + {i === 0 ? ( + group.items.map((item, j) => ) + ) : ( + + {group.items.map((item, j) => ( + + ))} + + )} + + ))} + + ) +} diff --git a/docs/components/docs/docs-navigation/index.tsx b/docs/components/docs/docs-navigation/index.tsx new file mode 100644 index 00000000000..0536d325215 --- /dev/null +++ b/docs/components/docs/docs-navigation/index.tsx @@ -0,0 +1,33 @@ +import { reader } from '../../../keystatic/reader' +import { DocsNavigationClient } from './client' + +export type NavigationMap = Awaited> + +export async function getNavigationMap () { + const navigation = await reader.singletons.navigation.read() + const pages = await reader.collections.docs.all() + + const pagesBySlug = Object.fromEntries(pages.map((page) => [page.slug, page])) + + const navigationMap = navigation?.navGroups.map(({ groupName, items }) => ({ + groupName, + items: items.map(({ label, link, status }) => { + const { discriminant, value } = link + const page = discriminant === 'page' && value ? pagesBySlug[value] : null + const url = discriminant === 'url' ? value : `/docs/${page?.slug}` + + return { + label: label || page?.entry.title || '', + href: url || '', + status, + } + }), + })) + + return navigationMap +} + +export async function DocsNavigation () { + const navigationMap = await getNavigationMap() + return +} \ No newline at end of file diff --git a/docs/components/docs/featured-docs/client.tsx b/docs/components/docs/featured-docs/client.tsx new file mode 100644 index 00000000000..74a03c48507 --- /dev/null +++ b/docs/components/docs/featured-docs/client.tsx @@ -0,0 +1,79 @@ +'use client' + +import { Well } from '../../primitives/Well' +import { Type } from '../../primitives/Type' + +import { Markdoc } from '../../Markdoc' +import { FeaturedCard, FullWidthCardContainer, SplitCardContainer } from '../FeaturedCard' +import { type FeaturedDocsMap } from '../../../keystatic/get-featured-docs-map' + +export function FeaturedDocsClient ({ featuredDocsMap }: { featuredDocsMap: FeaturedDocsMap }) { + if (!featuredDocsMap) return null + + // Separating the first group/item for featured UI treatment + const [firstGroup, ...restGroups] = featuredDocsMap + const [featuredItem, ...restItems] = firstGroup.items + + return ( + <> + {/* First Group */} + + {firstGroup.groupName} + + + {firstGroup.groupDescription.children.map((child, i) => ( + + ))} + + + {/* Featured Item */} + {!!featuredItem.description && ( + + + {featuredItem.description.children.map((child, i) => ( + + ))} + + + )} + {/* Remaining items of the first group */} + + {restItems.map((item, i) => ( + + ))} + + + {/* Remaining groups */} + {restGroups.map((group, i) => ( + + + {group.groupName} + + + {group.groupDescription.children.map((child, j) => ( + + ))} + + + + {group.items.map((item, j) => ( + + ))} + + + ))} + > + ) +} diff --git a/docs/components/docs/featured-docs/index.tsx b/docs/components/docs/featured-docs/index.tsx new file mode 100644 index 00000000000..27b7a76e067 --- /dev/null +++ b/docs/components/docs/featured-docs/index.tsx @@ -0,0 +1,8 @@ +import { FeaturedDocsClient } from './client' + +import { getFeaturedDocsMap } from '../../../keystatic/get-featured-docs-map' + +export async function FeaturedDocs () { + const featuredDocsMap = await getFeaturedDocsMap() + return +} diff --git a/docs/components/docs/featured-examples/client.tsx b/docs/components/docs/featured-examples/client.tsx new file mode 100644 index 00000000000..d6ca864b9fa --- /dev/null +++ b/docs/components/docs/featured-examples/client.tsx @@ -0,0 +1,55 @@ +/** @jsxImportSource @emotion/react */ + +'use client' + +import { Well } from '../../primitives/Well' +import { useMediaQuery } from '../../../lib/media' +import { Markdoc } from '../../Markdoc' +import { type FeaturedExamples } from '.' +import { Type } from '../../primitives/Type' + +export default function ExamplesList ({ featuredExamples }: { featuredExamples: FeaturedExamples }) { + if (!featuredExamples) return null + const mq = useMediaQuery() + return ( + <> + + {featuredExamples.label} + + + {!!featuredExamples.description && ( + + {featuredExamples.description.children.map((child, i) => ( + + ))} + + )} + + + {featuredExamples.items.map( + (item, i) => + !!item && ( + + {item.description.children.map((child, i) => ( + + ))} + + ) + )} + + > + ) +} diff --git a/docs/components/docs/featured-examples/index.tsx b/docs/components/docs/featured-examples/index.tsx new file mode 100644 index 00000000000..91827782a14 --- /dev/null +++ b/docs/components/docs/featured-examples/index.tsx @@ -0,0 +1,41 @@ +import ClientComponent from './client' + +import { type Tag, transform } from '@markdoc/markdoc' +import { reader } from '../../../keystatic/reader' +import { baseMarkdocConfig } from '../../../markdoc/config' + + +export type FeaturedExamples = Awaited> + +async function getFeaturedExamples () { + const featuredExamples = await reader.singletons.featuredExamples.read({ + resolveLinkedFiles: true, + }) + + if (!featuredExamples) return null + + // Get the rich text fields Markdoc-ready + const transformedFeaturedExamples = { + ...featuredExamples, + description: transform(featuredExamples.description.node, baseMarkdocConfig) as Tag, + items: await Promise.all( + featuredExamples.items.map(async (itemSlug) => { + const item = await reader.collections.examples.read(itemSlug, { + resolveLinkedFiles: true, + }) + if (!item) return null + return { + ...item, + description: transform(item.description.node, baseMarkdocConfig) as Tag, + } + }) + ), + } + + return transformedFeaturedExamples +} + +export async function FeaturedExamples () { + const featuredExamples = await getFeaturedExamples() + return +} diff --git a/docs/components/icons/ArrowR.tsx b/docs/components/icons/ArrowR.tsx index bdd04951cdc..58c54013615 100644 --- a/docs/components/icons/ArrowR.tsx +++ b/docs/components/icons/ArrowR.tsx @@ -1,7 +1,3 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' - import { Gradients, type IconProps } from './util' export function ArrowR ({ grad, ...props }: IconProps) { diff --git a/docs/components/icons/Automated.tsx b/docs/components/icons/Automated.tsx index e71212282d3..b5f2a0ff05f 100644 --- a/docs/components/icons/Automated.tsx +++ b/docs/components/icons/Automated.tsx @@ -1,7 +1,3 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' - import { Gradients, type IconProps } from './util' export function Automated ({ grad, ...props }: IconProps) { diff --git a/docs/components/icons/Bulb.tsx b/docs/components/icons/Bulb.tsx index 492dc94a249..a44fe5e24d6 100644 --- a/docs/components/icons/Bulb.tsx +++ b/docs/components/icons/Bulb.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/Cli.tsx b/docs/components/icons/Cli.tsx index 32f52e7b826..e4dc5cba34f 100644 --- a/docs/components/icons/Cli.tsx +++ b/docs/components/icons/Cli.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/ClientLogos.tsx b/docs/components/icons/ClientLogos.tsx index f9a8269ca83..15b26d51a10 100644 --- a/docs/components/icons/ClientLogos.tsx +++ b/docs/components/icons/ClientLogos.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/Close.tsx b/docs/components/icons/Close.tsx index 5bb8ed274ea..07b82994788 100644 --- a/docs/components/icons/Close.tsx +++ b/docs/components/icons/Close.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/Code.tsx b/docs/components/icons/Code.tsx index 9a19d45b3a0..f229c42a0c5 100644 --- a/docs/components/icons/Code.tsx +++ b/docs/components/icons/Code.tsx @@ -1,7 +1,3 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' - import { Gradients, type IconProps } from './util' export function Code ({ grad, ...props }: IconProps) { diff --git a/docs/components/icons/Content.tsx b/docs/components/icons/Content.tsx index aee10580861..75d8d1774f6 100644 --- a/docs/components/icons/Content.tsx +++ b/docs/components/icons/Content.tsx @@ -1,7 +1,3 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' - import { Gradients, type IconProps } from './util' export function Content ({ grad, ...props }: IconProps) { diff --git a/docs/components/icons/Copy.tsx b/docs/components/icons/Copy.tsx index 395479a2269..659bbafd137 100644 --- a/docs/components/icons/Copy.tsx +++ b/docs/components/icons/Copy.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/Custom.tsx b/docs/components/icons/Custom.tsx index 09d4a632a9c..ff401c1fe41 100644 --- a/docs/components/icons/Custom.tsx +++ b/docs/components/icons/Custom.tsx @@ -1,7 +1,3 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' - import { Gradients, type IconProps } from './util' export function Custom ({ grad, ...props }: IconProps) { diff --git a/docs/components/icons/DFATLogo.tsx b/docs/components/icons/DFATLogo.tsx index b9c019076c4..73286d7170c 100644 --- a/docs/components/icons/DFATLogo.tsx +++ b/docs/components/icons/DFATLogo.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { type IconProps } from './util' diff --git a/docs/components/icons/DarkMode.tsx b/docs/components/icons/DarkMode.tsx index d596067618c..fe4f4a6123c 100644 --- a/docs/components/icons/DarkMode.tsx +++ b/docs/components/icons/DarkMode.tsx @@ -1,7 +1,3 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' - import { Gradients, type IconProps } from './util' export function DarkMode ({ grad, ...props }: IconProps) { diff --git a/docs/components/icons/Docs.tsx b/docs/components/icons/Docs.tsx index 507be8f2d54..d3e5e3f5b6f 100644 --- a/docs/components/icons/Docs.tsx +++ b/docs/components/icons/Docs.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/Download.tsx b/docs/components/icons/Download.tsx index 4d412d827ba..2c6d7af0c3e 100644 --- a/docs/components/icons/Download.tsx +++ b/docs/components/icons/Download.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/Edit.tsx b/docs/components/icons/Edit.tsx index 352b7742353..887cafeb91c 100644 --- a/docs/components/icons/Edit.tsx +++ b/docs/components/icons/Edit.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/Editor.tsx b/docs/components/icons/Editor.tsx index d051ab3bcbc..c0ab1bc1f14 100644 --- a/docs/components/icons/Editor.tsx +++ b/docs/components/icons/Editor.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/EnliticLogo.tsx b/docs/components/icons/EnliticLogo.tsx index 20e8a884afb..358c610337a 100644 --- a/docs/components/icons/EnliticLogo.tsx +++ b/docs/components/icons/EnliticLogo.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { type IconProps } from './util' diff --git a/docs/components/icons/Filter.tsx b/docs/components/icons/Filter.tsx index 03d01fa8040..be0557a9e2d 100644 --- a/docs/components/icons/Filter.tsx +++ b/docs/components/icons/Filter.tsx @@ -1,7 +1,3 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' - import { Gradients, type IconProps } from './util' export function Filter ({ grad, ...props }: IconProps) { diff --git a/docs/components/icons/FrontEndLogos.tsx b/docs/components/icons/FrontEndLogos.tsx index 77b261aafb1..b8017322f93 100644 --- a/docs/components/icons/FrontEndLogos.tsx +++ b/docs/components/icons/FrontEndLogos.tsx @@ -1,6 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' + import { type SVGAttributes } from 'react' export function FrontEndLogos (props: SVGAttributes) { diff --git a/docs/components/icons/GitHub.tsx b/docs/components/icons/GitHub.tsx index bbc707308ec..a2b6343b456 100644 --- a/docs/components/icons/GitHub.tsx +++ b/docs/components/icons/GitHub.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/GraphQl.tsx b/docs/components/icons/GraphQl.tsx index eaa8514cf77..c5ccc76363b 100644 --- a/docs/components/icons/GraphQl.tsx +++ b/docs/components/icons/GraphQl.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/Hamburger.tsx b/docs/components/icons/Hamburger.tsx index 9c872016c34..0aea9e51e70 100644 --- a/docs/components/icons/Hamburger.tsx +++ b/docs/components/icons/Hamburger.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/Keystone.tsx b/docs/components/icons/Keystone.tsx index 7bc6102bfa7..757ffa20436 100644 --- a/docs/components/icons/Keystone.tsx +++ b/docs/components/icons/Keystone.tsx @@ -1,7 +1,3 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' - import { Gradients, type IconProps } from './util' export function Keystone ({ grad, ...props }: IconProps) { diff --git a/docs/components/icons/Lab.tsx b/docs/components/icons/Lab.tsx index 07bb90b898e..66f13792bb8 100644 --- a/docs/components/icons/Lab.tsx +++ b/docs/components/icons/Lab.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/LightMode.tsx b/docs/components/icons/LightMode.tsx index 75dedfd459d..17bcdb1aad6 100644 --- a/docs/components/icons/LightMode.tsx +++ b/docs/components/icons/LightMode.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/Link.tsx b/docs/components/icons/Link.tsx index a1201710e21..d273c4f3cbe 100644 --- a/docs/components/icons/Link.tsx +++ b/docs/components/icons/Link.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/Migration.tsx b/docs/components/icons/Migration.tsx index 680c728879c..53a414b7810 100644 --- a/docs/components/icons/Migration.tsx +++ b/docs/components/icons/Migration.tsx @@ -1,7 +1,3 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' - import { Gradients, type IconProps } from './util' export function Migration ({ grad, ...props }: IconProps) { diff --git a/docs/components/icons/Nextjs.tsx b/docs/components/icons/Nextjs.tsx index 26ad5ad3c21..832d03664fe 100644 --- a/docs/components/icons/Nextjs.tsx +++ b/docs/components/icons/Nextjs.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/Nope.tsx b/docs/components/icons/Nope.tsx index 352cc2e58ae..5883de401e4 100644 --- a/docs/components/icons/Nope.tsx +++ b/docs/components/icons/Nope.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/Organization.tsx b/docs/components/icons/Organization.tsx index e25dcd759af..73cadf67ff2 100644 --- a/docs/components/icons/Organization.tsx +++ b/docs/components/icons/Organization.tsx @@ -1,7 +1,3 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' - import { Gradients, type IconProps } from './util' export function Organization ({ grad, ...props }: IconProps) { diff --git a/docs/components/icons/PJohnsonLogo.tsx b/docs/components/icons/PJohnsonLogo.tsx index 6b5820246ba..4fefab15211 100644 --- a/docs/components/icons/PJohnsonLogo.tsx +++ b/docs/components/icons/PJohnsonLogo.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { type IconProps } from './util' diff --git a/docs/components/icons/Postgres.tsx b/docs/components/icons/Postgres.tsx index 76cc647b924..b7b0f002695 100644 --- a/docs/components/icons/Postgres.tsx +++ b/docs/components/icons/Postgres.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/PrintBarLogo.tsx b/docs/components/icons/PrintBarLogo.tsx index 2698927aa02..109d1e826d5 100644 --- a/docs/components/icons/PrintBarLogo.tsx +++ b/docs/components/icons/PrintBarLogo.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { type IconProps } from './util' diff --git a/docs/components/icons/Prisma.tsx b/docs/components/icons/Prisma.tsx index 5ed97697ad4..4798e04e444 100644 --- a/docs/components/icons/Prisma.tsx +++ b/docs/components/icons/Prisma.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/Profile.tsx b/docs/components/icons/Profile.tsx index 887f4336833..b489fc99c1c 100644 --- a/docs/components/icons/Profile.tsx +++ b/docs/components/icons/Profile.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/Quote.tsx b/docs/components/icons/Quote.tsx index bf553389c59..6009c6c1e72 100644 --- a/docs/components/icons/Quote.tsx +++ b/docs/components/icons/Quote.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/Relational.tsx b/docs/components/icons/Relational.tsx index 780f44d418a..0212ac43a47 100644 --- a/docs/components/icons/Relational.tsx +++ b/docs/components/icons/Relational.tsx @@ -1,7 +1,3 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' - import { Gradients, type IconProps } from './util' export function Relational ({ grad, ...props }: IconProps) { diff --git a/docs/components/icons/Relationship.tsx b/docs/components/icons/Relationship.tsx index afd2e8317f4..7a5d524a772 100644 --- a/docs/components/icons/Relationship.tsx +++ b/docs/components/icons/Relationship.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/Roadmap.tsx b/docs/components/icons/Roadmap.tsx index f07c6b24e4a..cf5e343307b 100644 --- a/docs/components/icons/Roadmap.tsx +++ b/docs/components/icons/Roadmap.tsx @@ -1,7 +1,3 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' - import { Gradients, type IconProps } from './util' export function Roadmap ({ grad, ...props }: IconProps) { diff --git a/docs/components/icons/RugbyAuLogo.tsx b/docs/components/icons/RugbyAuLogo.tsx index a3c6c2b3680..ebf209c9957 100644 --- a/docs/components/icons/RugbyAuLogo.tsx +++ b/docs/components/icons/RugbyAuLogo.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { type IconProps } from './util' diff --git a/docs/components/icons/Search.tsx b/docs/components/icons/Search.tsx index 03ca23036e5..95bded21e11 100644 --- a/docs/components/icons/Search.tsx +++ b/docs/components/icons/Search.tsx @@ -1,6 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' +/** @jsxImportSource @emotion/react */ import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/SearchKeys.tsx b/docs/components/icons/SearchKeys.tsx index d8d41d5059f..4c1e1937a74 100644 --- a/docs/components/icons/SearchKeys.tsx +++ b/docs/components/icons/SearchKeys.tsx @@ -1,6 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' +/** @jsxImportSource @emotion/react */ import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/Shield.tsx b/docs/components/icons/Shield.tsx index b4c3d2547ec..c5739afcfb2 100644 --- a/docs/components/icons/Shield.tsx +++ b/docs/components/icons/Shield.tsx @@ -1,7 +1,3 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' - import { Gradients, type IconProps } from './util' export function Shield ({ grad, ...props }: IconProps) { diff --git a/docs/components/icons/Slack.tsx b/docs/components/icons/Slack.tsx index fa0a21f1fde..3755aafe45a 100644 --- a/docs/components/icons/Slack.tsx +++ b/docs/components/icons/Slack.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/Thinkmill.tsx b/docs/components/icons/Thinkmill.tsx index 421441c93aa..b66006d006e 100644 --- a/docs/components/icons/Thinkmill.tsx +++ b/docs/components/icons/Thinkmill.tsx @@ -1,7 +1,3 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' - import { Gradients, type IconProps } from './util' export function Thinkmill ({ grad, ...props }: IconProps) { diff --git a/docs/components/icons/Tick.tsx b/docs/components/icons/Tick.tsx index 2eab84ed34e..0d7c52587ce 100644 --- a/docs/components/icons/Tick.tsx +++ b/docs/components/icons/Tick.tsx @@ -1,7 +1,3 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' - import { Gradients, type IconProps } from './util' export function Tick ({ grad, ...props }: IconProps) { diff --git a/docs/components/icons/Twitter.tsx b/docs/components/icons/Twitter.tsx index 9a529750165..9bef79a4f07 100644 --- a/docs/components/icons/Twitter.tsx +++ b/docs/components/icons/Twitter.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/Typescript.tsx b/docs/components/icons/Typescript.tsx index ad4d5494b0e..a962b36d41d 100644 --- a/docs/components/icons/Typescript.tsx +++ b/docs/components/icons/Typescript.tsx @@ -1,7 +1,3 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' - import { Gradients, type IconProps } from './util' export function Typescript ({ grad, ...props }: IconProps) { diff --git a/docs/components/icons/Updates.tsx b/docs/components/icons/Updates.tsx index 986da021050..e966bbfd075 100644 --- a/docs/components/icons/Updates.tsx +++ b/docs/components/icons/Updates.tsx @@ -1,7 +1,3 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' - import { Gradients, type IconProps } from './util' export function Updates ({ grad, ...props }: IconProps) { diff --git a/docs/components/icons/Video.tsx b/docs/components/icons/Video.tsx index 0af6dcfaa71..1ca61d2e923 100644 --- a/docs/components/icons/Video.tsx +++ b/docs/components/icons/Video.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/VocalLogo.tsx b/docs/components/icons/VocalLogo.tsx index 36bdd7b15f2..19696915365 100644 --- a/docs/components/icons/VocalLogo.tsx +++ b/docs/components/icons/VocalLogo.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { type IconProps } from './util' diff --git a/docs/components/icons/Watch.tsx b/docs/components/icons/Watch.tsx index 2c9b91b095f..8410eff04e2 100644 --- a/docs/components/icons/Watch.tsx +++ b/docs/components/icons/Watch.tsx @@ -1,6 +1,3 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/Welcome.tsx b/docs/components/icons/Welcome.tsx index 51111ace2f1..2911aa729d9 100644 --- a/docs/components/icons/Welcome.tsx +++ b/docs/components/icons/Welcome.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/WestpacLogo.tsx b/docs/components/icons/WestpacLogo.tsx index 77ff209634a..e56819ddb0f 100644 --- a/docs/components/icons/WestpacLogo.tsx +++ b/docs/components/icons/WestpacLogo.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { type IconProps } from './util' diff --git a/docs/components/icons/WhyKeystone.tsx b/docs/components/icons/WhyKeystone.tsx index 29bc205559d..eacfa8aebfb 100644 --- a/docs/components/icons/WhyKeystone.tsx +++ b/docs/components/icons/WhyKeystone.tsx @@ -1,6 +1,3 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/YouTube.tsx b/docs/components/icons/YouTube.tsx index 21ae20f20f2..601feef4fc7 100644 --- a/docs/components/icons/YouTube.tsx +++ b/docs/components/icons/YouTube.tsx @@ -1,5 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@emotion/react' import { Gradients, type IconProps } from './util' diff --git a/docs/components/icons/util.tsx b/docs/components/icons/util.tsx index d54907ad37a..499278727e3 100644 --- a/docs/components/icons/util.tsx +++ b/docs/components/icons/util.tsx @@ -1,6 +1,3 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' import { type SVGAttributes } from 'react' export type IconGradient = 'grad1' | 'grad2' | 'grad3' | 'grad4' | 'grad5' | 'grad6' | 'logo' diff --git a/docs/components/primitives/Alert.tsx b/docs/components/primitives/Alert.tsx index 875126e597f..18870682a67 100644 --- a/docs/components/primitives/Alert.tsx +++ b/docs/components/primitives/Alert.tsx @@ -1,6 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' + import classnames from 'classnames' import { type HTMLAttributes } from 'react' @@ -10,5 +8,5 @@ type AlertProps = { export function Alert ({ look = 'neutral', className, ...props }: AlertProps) { const classes = classnames('hint', look, className) // styles for this component can be found in the _app.js file - return + return } diff --git a/docs/components/primitives/Badge.tsx b/docs/components/primitives/Badge.tsx index 47aa03232e2..1f57305a4f1 100644 --- a/docs/components/primitives/Badge.tsx +++ b/docs/components/primitives/Badge.tsx @@ -1,6 +1,5 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' +/** @jsxImportSource @emotion/react */ + import { type HTMLAttributes } from 'react' const styleMap = { diff --git a/docs/components/primitives/Button.tsx b/docs/components/primitives/Button.tsx index 13e0e140dbe..e113f6aa71b 100644 --- a/docs/components/primitives/Button.tsx +++ b/docs/components/primitives/Button.tsx @@ -1,5 +1,5 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ +/** @jsxImportSource @emotion/react */ + import { type ReactNode } from 'react' import { type CSSObject, jsx } from '@emotion/react' import Link from 'next/link' diff --git a/docs/components/primitives/Code.tsx b/docs/components/primitives/Code.tsx index af46180cf64..138f04a7c03 100644 --- a/docs/components/primitives/Code.tsx +++ b/docs/components/primitives/Code.tsx @@ -1,7 +1,8 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { Highlight, Prism } from 'prism-react-renderer' -import { jsx } from '@emotion/react' +/** @jsxImportSource @emotion/react */ + +'use client' + +import { Highlight, Prism, type LineOutputProps, type TokenOutputProps } from 'prism-react-renderer' import { type ReactNode, useEffect, useMemo, useState } from 'react' import theme from '../../lib/prism-theme' @@ -10,14 +11,14 @@ type Range = { start: number, end: number } type CollapseRange = Range & { isCollapsed: boolean } const getRanges = (lines: string): Range[] => { - let ranges: Range[] = [] + const ranges: Range[] = [] - lines.split(',').forEach(lineRange => { + lines.split(',').forEach((lineRange) => { if (lineRange.length) { const [range1, range2] = lineRange.split('-') let parsedRange1 = parseInt(range1) - let parsedRange2 = parseInt(range2) + const parsedRange2 = parseInt(range2) if (isNaN(parsedRange1)) { throw new Error(`When trying to do highlighting, error in {${lines}}`) @@ -40,24 +41,24 @@ const getRanges = (lines: string): Range[] => { const parseClassName = ( className?: string ): { highlightRanges: Range[], collapseRanges: CollapseRange[], language: string } => { - let trimmedLanguage = (className || '').replace(/language-/, '') + const trimmedLanguage = (className || '').replace(/language-/, '') let language, highlights, collapses if ( !trimmedLanguage.includes('[') || trimmedLanguage.indexOf('{') < trimmedLanguage.indexOf('[') ) { - let [scopedLanguage, modifiers = ''] = trimmedLanguage.split('{') + const [scopedLanguage, modifiers = ''] = trimmedLanguage.split('{') - let [scopedHighlights, scopedCollapses] = modifiers.split('[') + const [scopedHighlights, scopedCollapses] = modifiers.split('[') language = scopedLanguage highlights = scopedHighlights collapses = scopedCollapses } else { - let [scopedLanguage, modifiers = ''] = trimmedLanguage.split('[') + const [scopedLanguage, modifiers = ''] = trimmedLanguage.split('[') - let [scopedCollapses, scopedHighlights] = modifiers.split('{') + const [scopedCollapses, scopedHighlights] = modifiers.split('{') language = scopedLanguage highlights = scopedHighlights @@ -67,7 +68,7 @@ const parseClassName = ( return { language: (language as any) || 'typescript', highlightRanges: getRanges(highlights?.replace('}', '') || ''), - collapseRanges: getRanges(collapses?.replace(']', '') || '').map(range => ({ + collapseRanges: getRanges(collapses?.replace(']', '') || '').map((range) => ({ ...range, isCollapsed: true, })), @@ -91,7 +92,7 @@ export function CodeBlock (props: { children: string, className?: string }) { } export function Code ({ children, className }: { children: string, className?: string }) { - let { language, highlightRanges, collapseRanges } = useMemo( + const { language, highlightRanges, collapseRanges } = useMemo( () => parseClassName(className), [className] ) @@ -119,7 +120,7 @@ export function Code ({ children, className }: { children: string, className?: s { - let updated = collapseState.map(item => + const updated = collapseState.map((item) => item.start === i ? { ...item, isCollapsed: false } : item ) @@ -142,10 +143,13 @@ export function Code ({ children, className }: { children: string, className?: s return undefined } + // Need to extract the key from lineProps, React not happy about spreading a key on an element + const lineProps = getLineProps({ line, key: i }) + const { key, ...restLineProps } = lineProps return ( + + // Need to extract the key from lineProps, React not happy about spreading a key on an element + const tokenProps = getTokenProps({ token, key }) + const { key: k, ...restTokenProps } = tokenProps + return })} ) diff --git a/docs/components/primitives/EditButton.tsx b/docs/components/primitives/EditButton.tsx index 85812ff9199..42fde7ba3db 100644 --- a/docs/components/primitives/EditButton.tsx +++ b/docs/components/primitives/EditButton.tsx @@ -1,6 +1,4 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' +/** @jsxImportSource @emotion/react */ import { Edit } from '../../components/icons/Edit' import { Button } from './Button' @@ -14,12 +12,12 @@ export function EditButton ({ isIndexPage?: boolean editPath?: string }) { - let fileUrl = `https://github.com/keystonejs/keystone/edit/main/docs/pages` + let fileUrl = `https://github.com/keystonejs/keystone/edit/main/docs` if (editPath) { - fileUrl += `/${editPath}` + fileUrl += `/content/${editPath}` } else if (isIndexPage) { - fileUrl += `${pathName}/index.tsx` + fileUrl += `/app/(site)${pathName}/page-client.tsx` } else { fileUrl += `${pathName}.md` } diff --git a/docs/components/primitives/Emoji.tsx b/docs/components/primitives/Emoji.tsx index 9024d642641..4a5f6cc39a8 100644 --- a/docs/components/primitives/Emoji.tsx +++ b/docs/components/primitives/Emoji.tsx @@ -1,6 +1,8 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx, keyframes } from '@emotion/react' +/** @jsxImportSource @emotion/react */ + +'use client' + +import { keyframes } from '@emotion/react' import { useRef, useState, useEffect, type HTMLAttributes, type ReactNode } from 'react' const fadeInTop = keyframes` diff --git a/docs/components/primitives/Field.tsx b/docs/components/primitives/Field.tsx index bfbf932b411..154f4aa366d 100644 --- a/docs/components/primitives/Field.tsx +++ b/docs/components/primitives/Field.tsx @@ -1,6 +1,5 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' +/** @jsxImportSource @emotion/react */ + import { type InputHTMLAttributes } from 'react' import { Stack } from './Stack' import { Type } from './Type' diff --git a/docs/components/primitives/GitHubButton.tsx b/docs/components/primitives/GitHubButton.tsx index c8e720bb28b..402a4c62d3a 100644 --- a/docs/components/primitives/GitHubButton.tsx +++ b/docs/components/primitives/GitHubButton.tsx @@ -1,6 +1,7 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' +/** @jsxImportSource @emotion/react */ + +'use client' + import { useState, useEffect, type HTMLAttributes } from 'react' import { Loading } from './Loading' diff --git a/docs/components/primitives/Gradient.tsx b/docs/components/primitives/Gradient.tsx index d9f6be164c1..b797761ad84 100644 --- a/docs/components/primitives/Gradient.tsx +++ b/docs/components/primitives/Gradient.tsx @@ -1,6 +1,5 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' +/** @jsxImportSource @emotion/react */ + import { type HTMLAttributes, type ElementType } from 'react' const styleMap = { diff --git a/docs/components/primitives/Highlight.tsx b/docs/components/primitives/Highlight.tsx index d6f99678a95..1e31261c13c 100644 --- a/docs/components/primitives/Highlight.tsx +++ b/docs/components/primitives/Highlight.tsx @@ -1,6 +1,5 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' +/** @jsxImportSource @emotion/react */ + import { type HTMLAttributes, type ElementType } from 'react' const styleMap = { diff --git a/docs/components/primitives/Loading.tsx b/docs/components/primitives/Loading.tsx index f9c57ee0a8f..f5791f57c64 100644 --- a/docs/components/primitives/Loading.tsx +++ b/docs/components/primitives/Loading.tsx @@ -1,6 +1,8 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx, keyframes } from '@emotion/react' +/** @jsxImportSource @emotion/react */ + +'use client' + +import { keyframes } from '@emotion/react' import { type HTMLAttributes } from 'react' const loading = keyframes({ diff --git a/docs/components/primitives/SearchField.tsx b/docs/components/primitives/SearchField.tsx index ad907d89cdf..bd82e1ca5f5 100644 --- a/docs/components/primitives/SearchField.tsx +++ b/docs/components/primitives/SearchField.tsx @@ -1,6 +1,6 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx, Global, css } from '@emotion/react' +/** @jsxImportSource @emotion/react */ + +import { Global, css } from '@emotion/react' import { Fragment, type HTMLAttributes } from 'react' import { algoliaStyles } from '../../lib/algoliaStyles' diff --git a/docs/components/primitives/Stack.tsx b/docs/components/primitives/Stack.tsx index 9590c0ed373..05c5ea7b253 100644 --- a/docs/components/primitives/Stack.tsx +++ b/docs/components/primitives/Stack.tsx @@ -1,6 +1,5 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' +/** @jsxImportSource @emotion/react */ + import { type HTMLAttributes } from 'react' import { SPACE } from '../../lib/TOKENS' diff --git a/docs/components/primitives/Status.tsx b/docs/components/primitives/Status.tsx index 42443c2967d..75dbb3cc0a7 100644 --- a/docs/components/primitives/Status.tsx +++ b/docs/components/primitives/Status.tsx @@ -1,6 +1,5 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' + +/** @jsxImportSource @emotion/react */ const statusMap = { notStarted: { diff --git a/docs/components/primitives/Type.tsx b/docs/components/primitives/Type.tsx index 6603d6abb68..61055844f5a 100644 --- a/docs/components/primitives/Type.tsx +++ b/docs/components/primitives/Type.tsx @@ -1,6 +1,5 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' + +/** @jsxImportSource @emotion/react */ import { forwardRefWithAs } from '../../lib/forwardRefWithAs' import { useMediaQuery } from '../../lib/media' diff --git a/docs/components/primitives/Well.tsx b/docs/components/primitives/Well.tsx index f12314020ad..693274d527f 100644 --- a/docs/components/primitives/Well.tsx +++ b/docs/components/primitives/Well.tsx @@ -1,6 +1,5 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' +/** @jsxImportSource @emotion/react */ + import Link from 'next/link' import { type AnchorHTMLAttributes, type ReactNode } from 'react' diff --git a/docs/components/primitives/Wrapper.tsx b/docs/components/primitives/Wrapper.tsx index 6b79ff8baec..bcf118f0153 100644 --- a/docs/components/primitives/Wrapper.tsx +++ b/docs/components/primitives/Wrapper.tsx @@ -1,6 +1,5 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' +/** @jsxImportSource @emotion/react */ + import type { ElementType, HTMLAttributes } from 'react' import { useMediaQuery } from '../../lib/media' diff --git a/docs/components/primitives/YouTubeEmbed.tsx b/docs/components/primitives/YouTubeEmbed.tsx index 884646d5f3f..bba8079798a 100644 --- a/docs/components/primitives/YouTubeEmbed.tsx +++ b/docs/components/primitives/YouTubeEmbed.tsx @@ -1,7 +1,3 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' - type YouTubeEmbedProps = { url: string label: string diff --git a/docs/content/blog/a-year-of-releases-in-review.md b/docs/content/blog/a-year-of-releases-in-review.md new file mode 100644 index 00000000000..f46116ddd1a --- /dev/null +++ b/docs/content/blog/a-year-of-releases-in-review.md @@ -0,0 +1,108 @@ +--- +title: A year of releases in review +description: >- + Over the past year, Keystone has received numerous updates, new features, and + essential bug fixes. Here’s a snapshot of the key improvements over the past + 12 months. +publishDate: 2024-08-07 +authorName: The Keystone Team +authorHandle: https://twitter.com/keystonejs +--- +Over the past year, Keystone has received numerous updates, new features, and essential bug fixes. Here’s a snapshot of the key improvements over the past 12 months. + +## Major Enhancements + +### Enhanced Database Integration + +One of the standout improvements this year is the update to Prisma `v5.13.0`. This update brings better performance and more robust features for database interactions. Alongside this, Keystone now supports extending the `PrismaClient`, providing more flexibility and control over database operations. + +### Interactive Transactions + +Keystone introduced `context.transaction`, allowing for interactive transactions within the `context`. This feature simplifies managing transactions, ensuring data integrity and consistency across operations. + +### Improved Migration Commands + +New migration commands like `keystone migrate create` and `keystone migrate apply` were added, making database migrations more straightforward and user-friendly. + +### Validation and Hooks + +A significant update was the overhaul of validation hooks for fields and lists. + +Hooks like`{field|list}.hooks.validate.[create|update|delete]` provide developers with more granular control over data validation, enhancing data integrity and application reliability. + +## Breaking Changes and Cleanups + +### Identifier Overhaul + +Keystone made a substantial change by switching file and image identifiers to random 128-bit `base64url` by default, replacing the older UUID system. This change improves security and performance. + +### Type and Parameter Cleanup + +Several deprecated types and parameters were removed, streamlining the codebase and improving overall efficiency. Notable removals include the old `AdminUIConfig`, `DatabaseConfig`, `GraphQLConfig`, and `ServerConfig` types, among others. + +### Prisma and GraphQL Schema Handling + +The handling of Prisma and GraphQL schemas saw improvements, with configurations now more centralised and consistent. This includes moving schema-related configurations to more intuitive locations within the setup. + +## New Features + +### Prisma Error Logging + +Server-side Prisma errors are now logged with `console.error`, making debugging easier and ensuring that critical issues are more visible during development. + +### Async HTTP Server Extension + +Support for asynchronous operations during HTTP server startup was added, allowing for more complex and dynamic server initialization processes. + +### GraphQL Error Messages + +Improved error messaging for GraphQL errors in the Admin UI list view helps developers quickly identify and resolve issues. + +### Admin UI Enhancements + +The Admin UI saw multiple enhancements, including a responsive menu for smaller devices, better handling of field omissions, and consistent sub-field ordering for image fields. + +## Bug Fixes + +Keystone JS addressed numerous bugs throughout the year, improving stability and performance. Some key fixes include: + +- Resolving issues with session strategies in the auth package. +- Fixing default values and ordering for various field types. +- Addressing Prisma migration errors in non-interactive environments. +- Correcting access and overflow issues in the AdminUI. + +## Keystatic-powered docs & blog + +A growing portion of the Keystone documentation (as well as this blog!) is now content-editable via [Keystatic](https://keystatic.com), another [Thinkmill](https://thinkmill.com.au) product. + +Keystatic shares common DNA with Keystone, but operates without a database. Instead, it stores content in Markdown/YAML/JSON/Markdoc/MDX on the local file system, and syncs it with the GitHub API. + +The content can be edited via a world-class Admin UI (powered by [Keystar UI](https://github.com/Thinkmill/keystatic/tree/main/design-system) and coming to Keystone soon!), but also directly from the [codebase's content files](https://github.com/keystonejs/keystone/tree/main/docs/content). + +## Community Contributions + +Keystone JS has a vibrant community that continually contributes to its growth ❤️ + +This year, we saw first-time contributions from several developers, helping to enhance the platform with new features and fixes. + +We’d like to extend our heartfelt thanks to all the contributors who have dedicated their time and effort to make Keystone JS better. + +### Acknowledgements + +A special acknowledgment goes to some of our key community contributors who have made significant impacts: + +- [@renovate](https://github.com/renovate) for consistent contributions and improvements across various packages. +- [@borisno2](https://github.com/borisno2) for significant enhancements and bug fixes. +- [@iamandrewluca](https://github.com/iamandrewluca) for introducing new capabilities and extending functionality. + +Additionally, we’d like to highlight a few first-time contributors who have joined the community and made valuable contributions: + +{% hint kind="tip" %} +[@ggpwnkthx](https://github.com/ggpwnkthx), [@dagrinchi](https://github.com/dagrinchi), [@Grumaks](https://github.com/Grumaks), [@PaulAroo](https://github.com/PaulAroo), [@leopoldkristjansson](https://github.com/leopoldkristjansson), [@ScottAgirs](https://github.com/ScottAgirs), [@lahirurane-rau](https://github.com/lahirurane-rau), [@MarcelMalik](https://github.com/MarcelMalik), [@TweededBadger](https://github.com/TweededBadger) +{% /hint %} + +## Looking Ahead + +The Keystone JS team remains committed to delivering high-quality updates and features. We are excited about the future and look forward to continuing to enhance the platform, making it even more powerful and user-friendly. + +Thank you to all the community members and contributors for your ongoing support and contributions! Here’s to another year of innovation and improvement with Keystone! diff --git a/docs/content/blog/embedded-mode-with-sqlite-nextjs.md b/docs/content/blog/embedded-mode-with-sqlite-nextjs.md index f334151bca7..422564617c2 100644 --- a/docs/content/blog/embedded-mode-with-sqlite-nextjs.md +++ b/docs/content/blog/embedded-mode-with-sqlite-nextjs.md @@ -50,7 +50,7 @@ Here's what we're going to do: Create a basic Next.js project with the `--typescript` option in an empty directory. ```bash -yarn create next-app --typescript my-project +npm create next-app --typescript my-project cd my-project ``` @@ -71,7 +71,7 @@ It is recommended that you use the same major version of `next` as used internal ### Start your local server -Run `yarn dev` at the root of your project. +Run `npm run dev` at the root of your project. Next.js will start a local server for you at @@ -86,7 +86,7 @@ Now that we have the Next.js starter with static files, let‘s embed Keystone i Add the following Keystone dependency to your project: ```bash -yarn add @keystone-6/core +npm install @keystone-6/core ``` ### Update .gitignore @@ -167,7 +167,7 @@ Finally, make a small change to the `scripts` object in `package.json` to includ ## Start all the things -Running `yarn dev` again will do the following: +Running `npm run dev` again will do the following: - Provision a GraphQL schema based on the configuration of `keystone.ts` - Build a [Prisma.io](https://www.prisma.io/) schema (which Keystone uses to manage the database) @@ -288,7 +288,7 @@ export async function getStaticProps({ params }: GetStaticPropsContext) { } ``` -Run `yarn dev` again. +Run `npm run dev` again. **Congratulations!** 🙌 You now have: diff --git a/docs/content/blog/general-availability.md b/docs/content/blog/general-availability.md index d99f704fd14..22da1c73e32 100644 --- a/docs/content/blog/general-availability.md +++ b/docs/content/blog/general-availability.md @@ -53,7 +53,7 @@ Here's a few of the other cool things we shipped in Keystone this year: - [JSON field](/docs/fields/json) - [17 example projects](/docs/examples) to explore Keystone's many features and get you up and running on the web -This release completes a body of work that make **Keystone 6 our best developer experience yet**. If you've been waiting to tryout Keystone 6 **there's never been a better time**. Just `yarn create keystone-app` or read our [getting started guide](/docs/getting-started) to take your first steps. +This release completes a body of work that make **Keystone 6 our best developer experience yet**. If you've been waiting to tryout Keystone 6 **there's never been a better time**. Just `npm create keystone-app@latest` or read our [getting started guide](/docs/getting-started) to take your first steps. ## What's Next diff --git a/docs/content/blog/introducing-keystone-blog.md b/docs/content/blog/introducing-keystone-blog.md index 66eaa0885be..74b8c23aa31 100644 --- a/docs/content/blog/introducing-keystone-blog.md +++ b/docs/content/blog/introducing-keystone-blog.md @@ -7,7 +7,7 @@ authorHandle: "https://twitter.com/flexdinesh" metaImageUrl: "" --- -We're happy to introduce the **Keystone Blog**, a buzzing corner for all the latest news and announcements about Keystone, brought to you by the Keystone team. +We're happy to introduce the **Keystone Blog**, brought to you by the Keystone team. Our _Updates_ page is going away and we are not one to let good content slip away. So some of the useful and important content in _Updates_ have been repurposed into blog posts. Woot! diff --git a/docs/content/docs/config/config.md b/docs/content/docs/config/config.md index d21ca962754..86a98c712a3 100644 --- a/docs/content/docs/config/config.md +++ b/docs/content/docs/config/config.md @@ -166,8 +166,6 @@ export default config({ { mode: 'write', src: ` - /** @jsxRuntime classic */ -/** @jsx jsx */ import { jsx } from '@keystone-ui/core'; export default function Welcome() { return (Welcome to my Keystone system); diff --git a/docs/content/docs/config/hooks.md b/docs/content/docs/config/hooks.md index 0068ad744c6..27d0259105e 100644 --- a/docs/content/docs/config/hooks.md +++ b/docs/content/docs/config/hooks.md @@ -9,7 +9,7 @@ The differences will be explicitly called out below. For each hook, the fields hooks are applied to **all fields first** in parallel, followed by the list hooks. -All hook functions are async and, with the exception of `resolveInput`, do not return a value. +Hook functions support `async` and, with the exception of `resolveInput`, do not need a return value. When operating on multiple values the hooks are called individually for each item being updated, created or deleted. @@ -27,19 +27,44 @@ export default config({ create: async args => { /* ... */ }, update: async args => { /* ... */ }, }, - validateInput: async args => { /* ... */ }, - validateDelete: async args => { /* ... */ }, - beforeOperation: async args => { /* ... */ }, - afterOperation: async args => { /* ... */ }, + validate: { + create: async args => { /* ... */ }, + update: async args => { /* ... */ }, + delete: async args => { /* ... */ }, + }, + beforeOperation: { + create: async args => { /* ... */ }, + update: async args => { /* ... */ }, + delete: async args => { /* ... */ }, + }, + afterOperation: { + create: async args => { /* ... */ }, + update: async args => { /* ... */ }, + delete: async args => { /* ... */ }, + } }, fields: { someFieldName: text({ hooks: { - resolveInput: async args => { /* ... */ }, - validateInput: async args => { /* ... */ }, - validateDelete: async args => { /* ... */ }, - beforeOperation: async args => { /* ... */ }, - afterOperation: async args => { /* ... */ }, + resolveInput: { + create: async args => { /* ... */ }, + update: async args => { /* ... */ }, + }, + validate: { + create: async args => { /* ... */ }, + update: async args => { /* ... */ }, + delete: async args => { /* ... */ }, + }, + beforeOperation: { + create: async args => { /* ... */ }, + update: async args => { /* ... */ }, + delete: async args => { /* ... */ }, + }, + afterOperation: { + create: async args => { /* ... */ }, + update: async args => { /* ... */ }, + delete: async args => { /* ... */ }, + } }, }), }, @@ -50,13 +75,13 @@ export default config({ ### resolveInput -The `resolveInput` function is used to modify or augment the `data` values passed in to a `create` or `update` operation. +The `resolveInput` hook is a transform for mutating the input `data` value prior to calling any other successive hooks, as part of the operation. This hook is the final stage in the [data resolving process](#resolved-data-stages), and is invoked after access control has been applied. For field hooks, the return value should be an updated value for that specific field. For list hooks, the return value should be a [`resolved data`](#resolved-data-stages) object. -The result of `resolveInput` will be passed as `resolvedData` into the next stages of the operation. +The result of `resolveInput` hooks is accessible as the argument `resolvedData` in the hooks that follow, for the remainder of the operation. | Argument | Description | | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | @@ -65,7 +90,7 @@ The result of `resolveInput` will be passed as `resolvedData` into the next stag | `operation` | The operation being performed (`'create'` or `'update'`). | | `inputData` | The value of `data` passed into the mutation. | | `item` | The currently stored item (`undefined` for `create` operations). This object is an internal database item. [DB API](../context/db-items) for more details on internal database items. | -| `resolvedData` | A [`resolved data`](#resolved-data-stages) object. The resolved data value after default values, relationship resolvers, and field resolvers have been applied. | +| `resolvedData` | A [`resolved data`](#resolved-data-stages) object. The resolved data value after default values, relationship resolvers, field resolvers, and `resolveInput` hooks have been applied. | | `context` | The [`KeystoneContext`](../context/overview) object of the originating GraphQL operation. | ```typescript @@ -76,32 +101,59 @@ export default config({ lists: { SomeListName: list({ hooks: { - resolveInput: async ({ - listKey, - operation, - inputData, - item, - resolvedData, - context, - }) => { - /* ... */ - return resolvedData; + resolveInput: { + create: async ({ + listKey, + operation, // always 'create' + inputData, + item, + resolvedData, + context, + }) => { + /* ... */ + return resolvedData; + }, + update: async ({ + listKey, + operation, // always 'update' + inputData, + item, + resolvedData, + context, + }) => { + /* ... */ + return resolvedData; + }, }, }, fields: { someFieldName: text({ hooks: { - resolveInput: async ({ - listKey, - fieldKey, - operation, - inputData, - item, - resolvedData, - context, - }) => { - /* ... */ - return resolvedData[fieldKey]; + resolveInput: { + create: async ({ + listKey, + fieldKey, + operation, + inputData, + item, + resolvedData, + context, + }) => { + /* ... */ + return resolvedData[fieldKey]; + }, + update: async ({ + listKey, + fieldKey, + operation, + inputData, + item, + resolvedData, + context, + }) => { + /* ... */ + return resolvedData[fieldKey]; + }, }, }, }), @@ -111,23 +163,26 @@ export default config({ }); ``` -### validateInput +### validate + +The `validate` hooks can be used to validate your [`resolvedData`](#resolved-data-stages) before a `create` or `update` operation completes, ensuring your expectations are met. +This hook can additionally be used to check your expectations as part of a `delete` operation. -The `validateInput` function is used to validate the [`resolvedData`](#resolved-data-stages) that will be saved during a `create` or `update` operation. +For `create` and `update` operations, this hook is invoked after the respective `resolveInput` hooks has been run. -It is invoked after the `resolveInput` hooks have been run. +This hook should report any validation errors using the `addValidationError(message)` function, which is provided as a parameter. +This is preferred to throwing to easily support more than one error message, if required. -If the `resolvedData` is invalid then the function should report validation errors with `addValidationError(msg)`. These error messages will be returned as a `ValidationFailureError` from the GraphQL API, and the operation will not be completed. | Argument | Description | | :------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `listKey` | The key of the list being operated on. | | `fieldKey` | The key of the field being operated on (field hooks only). | -| `operation` | The operation being performed (`'create'` or `'update'`). | -| `inputData` | The value of `data` passed into the mutation. | +| `operation` | The operation being performed (`'create'`, `'update'` or `'delete'`). | +| `inputData` | The value of `data` passed into the mutation (`undefined` for `delete` operations). | | `item` | The current value of the item being updated (`undefined` for `create` operations). This object is an internal database item. [DB API](../context/db-items) for more details on internal database items. | -| `resolvedData` | A [`resolved data`](#resolved-data-stages) object. The resolved data value after all data resolver stages have been completed. | +| `resolvedData` | A [`resolved data`](#resolved-data-stages) object (`undefined` for `delete` operations). The resolved data value after all data resolver stages have been completed. | | `context` | The [`KeystoneContext`](../context/overview) object of the originating GraphQL operation. | | `addValidationError(msg)` | Used to set a validation error. | @@ -139,82 +194,65 @@ export default config({ lists: { SomeListName: list({ hooks: { - validateInput: async ({ - listKey, - operation, - inputData, - item, - resolvedData, - context, - addValidationError, - }) => { /* ... */ }, - }, - fields: { - someFieldName: text({ - hooks: { - validateInput: async ({ - listKey, - fieldKey, - operation, - inputData, - item, - resolvedData, - context, - addValidationError, - }) => { /* ... */ }, - }, - }), - }, - }), - }, -}); -``` - -### validateDelete - -The `validateDelete` function is used during a `delete` operation to validate that deleting the selected item will not cause an issue in your system. - -It is invoked after access control has been applied. - -If the delete operation is invalid then the function should report validation errors with `addValidationError(msg)`. -These error messages will be returned as a `ValidationFailureError` from the GraphQL API. - -| Argument | Description | -| :------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `listKey` | The key of the list being operated on. | -| `fieldKey` | The key of the field being operated on (field hooks only). | -| `operation` | The operation being performed (`'delete'`). | -| `item` | The value of the item to be deleted. This object is an internal database item. [DB API](../context/db-items) for more details on internal database items. | -| `context` | The [`KeystoneContext`](../context/overview) object of the originating GraphQL operation. | -| `addValidationError(msg)` | Used to set a validation error. | - -```typescript -import { config, list } from '@keystone-6/core'; -import { text } from '@keystone-6/core/fields'; - -export default config({ - lists: { - SomeListName: list({ - hooks: { - validateDelete: async ({ - listKey, - operation, - item, - context, - addValidationError, - }) => { /* ... */ }, + validate: { + create: async ({ + listKey, + operation, + inputData, + resolvedData, + context, + addValidationError, + }) => { /* ... */ }, + update: async ({ + listKey, + operation, + inputData, + item, + resolvedData, + context, + addValidationError, + }) => { /* ... */ }, + delete: async ({ + listKey, + operation, + item, + context, + addValidationError, + }) => { /* ... */ }, + }, }, fields: { someFieldName: text({ hooks: { - validateDelete: async ({ - listKey, - fieldKey, - operation, - item, - context, - addValidationError, - }) => { /* ... */ }, + validate: { + create: async ({ + listKey, + fieldKey, + operation, + inputData, + resolvedData, + context, + addValidationError, + }) => { /* ... */ }, + update: async ({ + listKey, + fieldKey, + operation, + inputData, + item, + resolvedData, + context, + addValidationError, + }) => { /* ... */ }, + delete: async ({ + listKey, + fieldKey, + operation, + item, + context, + addValidationError, + }) => { /* ... */ }, + }, }, }), }, @@ -225,9 +263,9 @@ export default config({ ### beforeOperation -The `beforeOperation` function is used to perform side effects just before the data is saved to the database (for a `create` or `update` operation), or deleted from the database (for `delete` operations). +The `beforeOperation` hook is used to perform side effects just before the data is saved to the database (for a `create` or `update` operation), or deleted from the database (for `delete` operations). -It is invoked after all `validateInput`/`validateDelete` hooks have been run, but before the database is updated. +It is invoked after the `resolveInput` and `validate` hooks, but before the database is updated by Prisma. | Argument | Description | | :------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -247,27 +285,59 @@ export default config({ lists: { SomeListName: list({ hooks: { - beforeOperation: async ({ - listKey, - operation, - inputData, - item, - resolvedData, - context, - }) => { /* ... */ }, + beforeOperation: { + create: async ({ + listKey, + operation, + inputData, + resolvedData, + context, + }) => { /* ... */ }, + update: async ({ + listKey, + operation, + inputData, + item, + resolvedData, + context, + }) => { /* ... */ }, + delete: async ({ + listKey, + operation, + item, + context, + }) => { /* ... */ }, + }, }, fields: { someFieldName: text({ hooks: { - beforeOperation: async ({ - listKey, - fieldKey, - operation, - inputData, - item, - resolvedData, - context, - }) => { /* ... */ }, + beforeOperation: { + create: async ({ + listKey, + fieldKey, + operation, + inputData, + resolvedData, + context, + }) => { /* ... */ }, + update: async ({ + listKey, + fieldKey, + operation, + inputData, + item, + resolvedData, + context, + }) => { /* ... */ }, + delete: async ({ + listKey, + fieldKey, + operation, + item, + context, + }) => { /* ... */ }, + }, }, }), }, @@ -278,7 +348,7 @@ export default config({ ### afterOperation -The `afterOperation` function is used to perform side effects after the data has been saved to the database (for a `create` or `update` operation), or deleted from the database (for `delete` operations). +The `afterOperation` hook is used to perform side effects after the data has been saved to the database (for a `create` or `update` operation), or deleted from the database (for `delete` operations). | Argument | Description | | :------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -299,29 +369,63 @@ export default config({ lists: { SomeListName: list({ hooks: { - afterOperation: async ({ - listKey, - operation, - inputData, - originalItem, - item, - resolvedData, - context, - }) => { /* ... */ }, + afterOperation: { + create: async ({ + listKey, + operation, + inputData, + item, + resolvedData, + context, + }) => { /* ... */ }, + update: async ({ + listKey, + operation, + inputData, + originalItem, + item, + resolvedData, + context, + }) => { /* ... */ }, + delete: async ({ + listKey, + operation, + originalItem, + context, + }) => { /* ... */ }, + }, }, fields: { someFieldName: text({ hooks: { - afterOperation: async ({ - listKey, - fieldKey, - operation, - inputData, - originalItem, - item, - resolvedData, - context, - }) => { /* ... */ }, + afterOperation: { + create: async ({ + listKey, + fieldKey, + operation, + inputData, + item, + resolvedData, + context, + }) => { /* ... */ }, + update: async ({ + listKey, + fieldKey, + operation, + inputData, + originalItem, + item, + resolvedData, + context, + }) => { /* ... */ }, + delete: async ({ + listKey, + fieldKey, + operation, + originalItem, + context, + }) => { /* ... */ }, + }, }, }), }, diff --git a/docs/content/docs/config/lists.md b/docs/content/docs/config/lists.md index 9140322e5f0..eebfed74b33 100644 --- a/docs/content/docs/config/lists.md +++ b/docs/content/docs/config/lists.md @@ -77,7 +77,7 @@ Options: - `labelField`: Selects the field which will be used as the label column in the Admin UI. By default looks for a field called `'label'`, then falls back to `'name'`, then `'title'`, and finally `'id'`, which is guaranteed to exist. -- `searchFields`: The fields used by the Admin UI when searching this list on the list view and in relationship fields. +- `searchFields`: The fields used by the Admin UI when searching this list on the list view and in relationship fields. Nominated fields need to support the `contains` filter. It is always possible to search by an id and `'id'` should not be specified in this option. By default, the `labelField` is used if it has a string `contains` filter, otherwise none. - `description` (default: `undefined`): Sets the list description displayed in the Admin UI. @@ -89,17 +89,17 @@ Options: Can be either a boolean value or an async function with an argument `{ session, context }` that returns a boolean value. - `createView`: Controls the create view page of the Admin UI. - `defaultFieldMode` (default: `'edit'`): - Can be overridden by per-field values in the `field.ui.createView.fieldMode` config. + Can be overridden by per-field values in the `ui.createView.fieldMode` config. See the [Fields API](../fields/overview#common-configuration) for details. Can be one of `['edit', 'hidden']`, or an async function with an argument `{ session, context }` that returns one of `['edit', 'hidden']`. - `itemView`: Controls the item view page of the Admin UI. - `defaultFieldMode` (default: `'edit'`): - Can be overridden by per-field values in the `field.ui.itemView.fieldMode` config. + Can be overridden by per-field values in the `ui.itemView.fieldMode` config. See the [Fields API](../fields/overview#common-configuration) for details. Can be one of `['edit', 'read', 'hidden']`, or an async function with an argument `{ session, context, item }` that returns one of `['edit', 'read', 'hidden']`. - `listView`: Controls the list view page of the Admin UI. - `defaultFieldMode` (default: `'read'`): Controls the default mode of fields in the list view. - Can be overridden by per-field values in the `field.ui.listView.fieldMode` config. + Can be overridden by per-field values in the `ui.listView.fieldMode` config. See the [Fields API](../fields/overview#common-configuration) for details. Can be one of `['read', 'hidden']`, or an async function with an argument `{ session, context }` that returns one of `['read', 'hidden']`. - `initialColumns` (default: The first three fields defined in the list). A list of field names to display in columns in the list view. By default only the label column, as determined by `labelField`, is shown. diff --git a/docs/content/docs/fields/relationship.md b/docs/content/docs/fields/relationship.md index fd85a4a58b9..16416f3a9fe 100644 --- a/docs/content/docs/fields/relationship.md +++ b/docs/content/docs/fields/relationship.md @@ -14,10 +14,10 @@ Read our [relationships guide](../guides/relationships) for details on Keystone - `ui` (default: `{ hideCreate: false, displayMode: 'select' }`): Configures the display mode of the field in the Admin UI. - `hideCreate` (default: `false`). If `true`, the "Create related item" button is not shown in the item view. - `displayMode` (default: `'select'`): Controls the mode used to display the field in the item view. The mode `'select'` displays related items in a select component, while `'cards'` displays the related items in a card layout. Each display mode supports further configuration. -- `ui.displayMode === 'select'` options: +- `displayMode === 'select'` options: - `labelField`: The field path from the related list to use for item labels in the select. Defaults to the `labelField` configured on the related list. -- `searchFields`: The fields used by the UI to search for this item, in context of this relationship field. Defaults to `searchFields` configured on the related list. -- `ui.displayMode === 'cards'` options: +- `searchFields`: The fields used by the Admin UI when searching by this relationship on the list view and in relationship fields. Nominated fields need to support the `contains` filter. +- `displayMode === 'cards'` options: - `cardFields`: A list of field paths from the related list to render in the card component. Defaults to `'id'` and the `labelField` configured on the related list. - `linkToItem` (default `false`): If `true`, the default card component will render as a link to navigate to the related item. - `removeMode` (default: `'disconnect'`): Controls whether the `Remove` button is present in the card. If `'disconnect'`, the button will be present. If `'none'`, the button will not be present. @@ -27,7 +27,7 @@ Read our [relationships guide](../guides/relationships) for details on Keystone Alternatively this can be an object with the properties: - `labelField`: The field path from the related list to use for item labels in select. Defaults to the `labelField` configured on the related list. - `searchFields`: The fields used by the UI to search for this item, in context of this relationship field. Defaults to `searchFields` configured on the related list. -- `ui.displayMode === 'count'` only supports `many` relationships +- `displayMode === 'count'` only supports `many` relationships ```typescript import { config, list } from '@keystone-6/core'; diff --git a/docs/content/docs/getting-started.md b/docs/content/docs/getting-started.md index 32b803d1ba5..d07fe2f0fe2 100644 --- a/docs/content/docs/getting-started.md +++ b/docs/content/docs/getting-started.md @@ -11,9 +11,9 @@ It generates some files for you and installs all the dependencies you need to ru ## Quick Start ```sh -yarn create keystone-app +npm create keystone-app@latest cd my-app -yarn dev +npm run dev ``` ## Installing a Keystone instance @@ -21,41 +21,19 @@ yarn dev Open your preferred shell and make sure you’re in the folder you want to create your new project in. `create-keystone-app` will generate a new folder with your new Keystone files in it. -### Yarn - -Use [`yarn create`](https://classic.yarnpkg.com/en/docs/cli/create/): - -```sh -cd your/path/ -yarn create keystone-app -``` - ### npm -Use [`npm init`](https://docs.npmjs.com/cli/v7/commands/npm-init): - ```sh -cd your/path/ -npm init keystone-app@latest +npm create keystone-app@latest ``` - -{% hint kind="warn" %} -`npm init ""` is available in npm 6+ -{% /hint %} - ### npx Use npm's [`npx`](https://docs.npmjs.com/cli/v7/commands/npx): ```sh -cd your/path/ npx create-keystone-app@latest ``` -{% hint kind="warn" %} -`npx` comes with npm 5.2+ -{% /hint %} - ## Naming your app The CLI will ask you to name your app. Once named, it will create a new SQLite database. @@ -70,7 +48,7 @@ You can now `cd` into the folder that was created for you and start Keystone: ```sh cd my-app -yarn dev +npm run dev ``` This will generate the Admin UI pages via [Next.js](https://nextjs.org/) on . When you visit the Admin UI for the first time you will be presented with a handy screen that asks you to create a user: @@ -88,16 +66,16 @@ Keytone creates the following files in your newly generated folder. The most imp ```sh . -├── auth.ts # Authentication configuration for Keystone -├── keystone.ts # The main entry file for configuring Keystone -├── node_modules # Your dependencies -├── package.json # Your package.json with four scripts prepared for you -├── README.md # Additional info to help you get started -├── schema.graphql # GraphQL schema (automatically generated by Keystone) -├── schema.prisma # Prisma configuration (automatically generated by Keystone) -├── schema.ts # Where you design your data schema -├── tsconfig.json # Your typescript config -└── yarn.lock # Your yarn lock file +├── auth.ts # Authentication configuration for Keystone +├── keystone.ts # The main entry file for configuring Keystone +├── node_modules # Your dependencies +├── package.json # Your package.json with four scripts prepared for you +├── package-lock.json # Your npm lock file +├── README.md # Additional info to help you get started +├── schema.graphql # GraphQL schema (automatically generated by Keystone) +├── schema.prisma # Prisma configuration (automatically generated by Keystone) +├── schema.ts # Where you design your data schema +└── tsconfig.json # Your typescript config ``` ## Scripts diff --git a/docs/content/docs/graphql/filters.md b/docs/content/docs/graphql/filters.md index 90c726a1d2f..723a5a857ec 100644 --- a/docs/content/docs/graphql/filters.md +++ b/docs/content/docs/graphql/filters.md @@ -1,8 +1,10 @@ --- -title: "GraphQL Query Filters" -description: "A reference list of every filters available for every Keystone field type. Keystone filters are typically named after the field they are filtering." ---- +title: GraphQL Query Filters +description: >- + A reference list of every filters available for every Keystone field type. + Keystone filters are typically named after the field they are filtering. +--- Each field type provides its own set of filters which can be used with [queries](./overview#users). This page lists all the filters available for each field type. For more details on how to use filters in queries please consult to the [GraphQL Queries - Filters](../guides/filters) guide. @@ -11,36 +13,99 @@ For more details on how to use filters in queries please consult to the [GraphQL ### checkbox -| **Filter name** | **Type** | **Description** | -| --------------- | ----------------------- | ------------------------------- | -| `equals` | `Boolean` | Equals | -| `not` | `BooleanNullableFilter` | Does not match the inner filter | +{% table %} +- **Filter name** +- **Type** +- **Description** +--- +- `equals` +- `Boolean` +- Equals +--- +- `not` +- `BooleanNullableFilter` +- Does not match the inner filter +{% /table %} ### integer -| **Filter name** | **Type** | **Description** | -| --------------- | ------------------- | ------------------------------- | -| `equals` | `Int` | Equals | -| `lt` | `Int` | Less than | -| `lte` | `Int` | Less than or equal | -| `gt` | `Int` | Greater than | -| `gte` | `Int` | Greater than or equal | -| `in` | `[Int!]` | Is in the array | -| `notIn` | `[Int!]` | Is not in the array | -| `not` | `IntNullableFilter` | Does not match the inner filter | +{% table %} +- **Filter name** +- **Type** +- **Description** +--- +- `equals` +- `Int` +- Equals +--- +- `lt` +- `Int` +- Less than +--- +- `lte` +- `Int` +- Less than or equal +--- +- `gt` +- `Int` +- Greater than +--- +- `gte` +- `Int` +- Greater than or equal +--- +- `in` +- `[Int!]` +- Is in the array +--- +- `notIn` +- `[Int!]` +- Is not in the array +--- +- `not` +- `IntNullableFilter` +- Does not match the inner filter +{% /table %} ### bigInt -| **Filter name** | **Type** | **Description** | -| --------------- | ---------------------- | ------------------------------- | -| `equals` | `BigInt` | Equals | -| `lt` | `BigInt` | Less than | -| `lte` | `BigInt` | Less than or equal | -| `gt` | `BigInt` | Greater than | -| `gte` | `BigInt` | Greater than or equal | -| `in` | `[BigInt!]` | Is in the array | -| `notIn` | `[BigInt!]` | Is not in the array | -| `not` | `BigIntNullableFilter` | Does not match the inner filter | +{% table %} +- **Filter name** +- **Type** +- **Description** +--- +- `equals` +- `BigInt` +- Equals +--- +- `lt` +- `BigInt` +- Less than +--- +- `lte` +- `BigInt` +- Less than or equal +--- +- `gt` +- `BigInt` +- Greater than +--- +- `gte` +- `BigInt` +- Greater than or equal +--- +- `in` +- `[BigInt!]` +- Is in the array +--- +- `notIn` +- `[BigInt!]` +- Is not in the array +--- +- `not` +- `BigIntNullableFilter` +- Does not match the inner filter +{% /table %} ### json @@ -48,51 +113,136 @@ The `json` field type does not support filters. ### float -| **Filter name** | **Type** | **Description** | -| --------------- | --------------------- | ------------------------------- | -| `equals` | `Float` | Equals | -| `lt` | `Float` | Less than | -| `lte` | `Float` | Less than or equal | -| `gt` | `Float` | Greater than | -| `gte` | `Float` | Greater than or equal | -| `in` | `[Float!]` | Is in the array | -| `notIn` | `[Float!]` | Is not in the array | -| `not` | `FloatNullableFilter` | Does not match the inner filter | +{% table %} +- **Filter name** +- **Type** +- **Description** +--- +- `equals` +- `Float` +- Equals +--- +- `lt` +- `Float` +- Less than +--- +- `lte` +- `Float` +- Less than or equal +--- +- `gt` +- `Float` +- Greater than +--- +- `gte` +- `Float` +- Greater than or equal +--- +- `in` +- `[Float!]` +- Is in the array +--- +- `notIn` +- `[Float!]` +- Is not in the array +--- +- `not` +- `FloatNullableFilter` +- Does not match the inner filter +{% /table %} ### password -| **Filter name** | **Type** | **Description** | -| --------------- | --------- | --------------- | -| `isSet` | `Boolean` | A value is set | +{% table %} +- **Filter name** +- **Type** +- **Description** +--- +- `isSet` +- `Boolean` +- A value is set +{% /table %} ### select - If the `type` is `string`(the default), the same filters as `text` will be available. - If the `type` is `integer`, the same filters as `integer` will be available. - If the `type` is `enum`, the following filters will be available: - \| **Filter name** \| **Type** \| **Description** \| - \| --------------- \| ---------- \| ------------------- \| - \| `equals` \| `ListKeyFieldKeyType` | Equals | - \| `in` \| `[ListKeyFieldKeyType!]` | Is in the array | - \| `notIn` \| `[ListKeyFieldKeyType!]` | Is not in the array | - \| `not` \| `ListKeyFieldKeyTypeNullableFilter` | Does not match the inner filter | + | **Filter name** | **Type** | **Description** | + | --------------- | ---------- | ------------------- | + | `equals` | `ListKeyFieldKeyType` | Equals | + | `in` | `[ListKeyFieldKeyType!]` | Is in the array | + | `notIn` | `[ListKeyFieldKeyType!]` | Is not in the array | + | `not` | `ListKeyFieldKeyTypeNullableFilter` | Does not match the inner filter | ### text -| **Filter name** | **Type** | **Description** | **Notes** | -| --------------- | ---------------------------------------- | ----------------------------------------------------- | --------- | -| `equals` | `String` | Equals | | -| `lt` | `String` | Less than | | -| `lte` | `String` | Less than or equal | | -| `gt` | `String` | Greater than | | -| `gte` | `String` | Greater than or equal | | -| `contains` | `String` | Contains | [1] | -| `startsWith` | `String` | Starts with | [1] | -| `endsWith` | `String` | Ends with | [1] | -| `in` | `[String!]` | Is in the array | | -| `notIn` | `[String!]` | Is not in the array | | -| `mode` | `QueryMode` (`default` or `insensitive`) | Whether the filters should be case insensitive or not | [2] | -| `not` | `NestedStringNullableFilter` | Does not match the inner filter | | +{% table %} +- **Filter name** +- **Type** +- **Description** +- **Notes** +--- +- `equals` +- `String` +- Equals +- +--- +- `lt` +- `String` +- Less than +- +--- +- `lte` +- `String` +- Less than or equal +- +--- +- `gt` +- `String` +- Greater than +- +--- +- `gte` +- `String` +- Greater than or equal +- +--- +- `contains` +- `String` +- Contains +- [1] +--- +- `startsWith` +- `String` +- Starts with +- [1] +--- +- `endsWith` +- `String` +- Ends with +- [1] +--- +- `in` +- `[String!]` +- Is in the array +- +--- +- `notIn` +- `[String!]` +- Is not in the array +- +--- +- `mode` +- `QueryMode` (`default` or `insensitive`) +- Whether the filters should be case insensitive or not +- [2] +--- +- `not` +- `NestedStringNullableFilter` +- Does not match the inner filter +- +{% /table %} #### Notes @@ -101,16 +251,43 @@ The `json` field type does not support filters. ### timestamp -| **Filter name** | **Type** | **Description** | -| --------------- | ------------------------ | ------------------------------- | -| `equals` | `String` | Equals | -| `lt` | `String` | Less than | -| `lte` | `String` | Less than or equal | -| `gt` | `String` | Greater than | -| `gte` | `String` | Greater than or equal | -| `in` | `[String!]` | Is in the array | -| `notIn` | `[String!]` | Is not in the array | -| `not` | `DateTimeNullableFilter` | Does not match the inner filter | +{% table %} +- **Filter name** +- **Type** +- **Description** +--- +- `equals` +- `String` +- Equals +--- +- `lt` +- `String` +- Less than +--- +- `lte` +- `String` +- Less than or equal +--- +- `gt` +- `String` +- Greater than +--- +- `gte` +- `String` +- Greater than or equal +--- +- `in` +- `[String!]` +- Is in the array +--- +- `notIn` +- `[String!]` +- Is not in the array +--- +- `not` +- `DateTimeNullableFilter` +- Does not match the inner filter +{% /table %} ## Relationship type @@ -118,17 +295,35 @@ The `json` field type does not support filters. #### many: true -| **Filter name** | **Type** | **Description** | -| --------------- | --------------- | ------------------------------------------ | -| `every` | `FooWhereInput` | All related items match the nested filter | -| `some` | `FooWhereInput` | Some related items match the nested filter | -| `none` | `FooWhereInput` | No related items match the nested filter | +{% table %} +- **Filter name** +- **Type** +- **Description** +--- +- `every` +- `FooWhereInput` +- All related items match the nested filter +--- +- `some` +- `FooWhereInput` +- Some related items match the nested filter +--- +- `none` +- `FooWhereInput` +- No related items match the nested filter +{% /table %} #### many: false -| **Filter name** | **Type** | **Description** | -| --------------- | --------------- | ------------------------- | -| `foo` | `FooWhereInput` | Matches the nested filter | +{% table %} +- **Filter name** +- **Type** +- **Description** +--- +- `foo` +- `FooWhereInput` +- Matches the nested filter +{% /table %} ## Virtual type @@ -150,8 +345,10 @@ The `image` field type does not support filters. {% related-content %} {% well -heading="Query Filters Guide" -href="/docs/guides/filters" %} + heading="Query Filters Guide" + grad="grad1" + href="/docs/guides/filters" + target="" %} Query filters are an integral part of Keystone’s powerful GraphQL APIs. This guide will show you how to use filters to get the data you need from your system. {% /well %} {% /related-content %} diff --git a/docs/content/docs/guides/auth-and-access-control.md b/docs/content/docs/guides/auth-and-access-control.md index c9711e99c92..b1bab5a3696 100644 --- a/docs/content/docs/guides/auth-and-access-control.md +++ b/docs/content/docs/guides/auth-and-access-control.md @@ -205,7 +205,7 @@ type Session = { We can now set up **operation** access control to restrict the **create**, **update** and **delete** operations to authenticated users with the `isAdmin` checkbox set: ```ts -const isAdmin = ({ session }: { session: Session }) => Boolean(session?.data.isAdmin); +const isAdmin = ({ session }: { session?: Session }) => Boolean(session?.data.isAdmin); const Post = list({ access: { @@ -225,11 +225,11 @@ const Post = list({ We can also use **filter** access control to make sure that unauthenticated users can only see published posts: ```ts -const filterPosts = ({ session }: { session: Session }) => { +function filterPosts ({ session }: { session?: Session }) { // if the user is an Admin, they can access all the records if (session?.data.isAdmin) return true; // otherwise, filter for published posts - return { isPublished: { equals: true } }; + return { isPublished: { equals: true } } } const Post = list({ @@ -435,7 +435,7 @@ When you need it, you can call `context.sudo()` to create a new context with ele For example, we probably want to block all public access to querying users in our system: ```ts -const isAdmin = ({ session }: { session: Session }) => Boolean(session?.data.isAdmin); +const isAdmin = ({ session }: { session?: Session }) => Boolean(session?.data.isAdmin); const Person = list({ access: { @@ -520,19 +520,19 @@ type PersonData = { }; // Validate there is a user with a valid session -const isUser = ({ session }: { session: Session }) => +const isUser = ({ session }: { session?: Session }) => !!session?.data.id; // Validate the current user is an Admin -const isAdmin = ({ session }: { session: Session }) => +const isAdmin = ({ session }: { session?: Session }) => Boolean(session?.data.isAdmin); // Validate the current user is updating themselves -const isPerson = ({ session, item }: { session: Session, item: PersonData }) => +const isPerson = ({ session, item }: { session?: Session, item: PersonData }) => session?.data.id === item.id; // Validate the current user is an Admin, or updating themselves -const isAdminOrPerson = ({ session, item }: { session: Session, item: PersonData }) => +const isAdminOrPerson = ({ session, item }: { session?: Session, item: PersonData }) => isAdmin({ session }) || isPerson({ session, item }); const Person = list({ diff --git a/docs/content/docs/guides/cli.md b/docs/content/docs/guides/cli.md index b4bc02ecf8f..9458e3bae6c 100644 --- a/docs/content/docs/guides/cli.md +++ b/docs/content/docs/guides/cli.md @@ -1,8 +1,9 @@ --- -title: "Command Line" -description: "Learn how to use Keystone's command line interface (CLI) to develop, build, and deploy your Keystone projects." +title: Command Line +description: >- + Learn how to use Keystone's command line interface (CLI) to develop, build, + and deploy your Keystone projects. --- - Keystone's command line interface (CLI) has been designed to help you develop, build, and deploy your Keystone project. Using the `keystone` command you can start the dev process, build and run your app in production, and control how you migrate the structure of your database as your schema changes. @@ -125,6 +126,7 @@ This is typically useful early in a project's development lifecycle, when you wa ```bash $ keystone postinstall ``` + {% hint kind="tip" %} Note: `postinstall` is an alias for `keystone build --no-ui --frozen` we recommend switching to this `build` command {% /hint %} @@ -142,6 +144,7 @@ While the recommended way to fix this problem is to start your app using `keysto ```bash $ keystone postinstall --fix ``` + {% hint kind="tip" %} Note: `postinstall --fix` is an alias for `keystone build --no-ui` we recommend switching to this `build` command {% /hint %} @@ -155,7 +158,9 @@ $ keystone build This command generates the files needed for Keystone to start in **production** mode. You should run it during the build phase of your production deployment. It will also validate that the generated files you should have committed to source control are in sync with your Keystone Schema. + ### build flags + - `--frozen` - Don't update the graphql or prisma schemas, only validate them, exits with error if the schemas don't match what keystone would generate. - `--no-prisma` - Don't build or validate the prisma schema - `--no-ui` - Don't build the AdminUI @@ -169,7 +174,9 @@ $ keystone start This command starts Keystone in **production** mode. It requires a build to have been generated (see `build` above). It will not generate or apply any database migrations - these should be run during the **build** or **release** phase of your production deployment. + ### start flags + - `--with-migrations` - Trigger prisma to run migrations as part of startup - `--no-ui` - Don't serve the AdminUI @@ -220,7 +227,7 @@ Most application hosting platforms allow you to specify a `build` script (for ne Install the project dependencies, generate the build and deploy migrations: ```bash -yarn && yarn keystone build && yarn keystone prisma migrate deploy +npm i && npx keystone build && npx keystone prisma migrate deploy ``` {% hint kind="error" %} @@ -232,8 +239,9 @@ Note: it is only safe to run migrations in the build step if deploys are built s Start Keystone in production mode: ```bash -yarn keystone start +npx keystone start ``` + {% hint kind="tip" %} Note: To run migrations before you start Keystone use `keystone start --with-migrations` {% /hint %} @@ -249,8 +257,10 @@ Note: To run migrations before you start Keystone use `keystone start --with-mig {% related-content %} {% well -heading="Getting Started with create-keystone-app" -href="/docs/getting-started" %} + heading="Getting Started with create-keystone-app" + grad="grad1" + href="/docs/getting-started" + target="" %} How to use Keystone's CLI app to standup a new local project with an Admin UI & the GraphQL Playground. {% /well %} {% /related-content %} diff --git a/docs/content/docs/guides/custom-admin-ui-logo.md b/docs/content/docs/guides/custom-admin-ui-logo.md index 50e76c2c006..a034017659a 100644 --- a/docs/content/docs/guides/custom-admin-ui-logo.md +++ b/docs/content/docs/guides/custom-admin-ui-logo.md @@ -27,8 +27,7 @@ If you have styling constraints, we recommend using the jsx export from the `@ke ```tsx // admin/config.tsx -/** @jsxRuntime classic */ -/** @jsx jsx */ + import { jsx } from '@keystone-ui/core'; function CustomLogo () { diff --git a/docs/content/docs/guides/custom-admin-ui-pages.md b/docs/content/docs/guides/custom-admin-ui-pages.md index add8bc37a4f..1ae24fd08dd 100644 --- a/docs/content/docs/guides/custom-admin-ui-pages.md +++ b/docs/content/docs/guides/custom-admin-ui-pages.md @@ -154,8 +154,6 @@ The snippet below uses the emotion `jsx` runtime exported from `@keystone-ui/cor ```tsx // admin/pages/custom-page.tsx /** @jsxRuntime classic */ -/** @jsxRuntime classic */ -/** @jsx jsx */ import Link from 'next/link'; import { jsx } from '@keystone-ui/core'; diff --git a/docs/content/docs/guides/relationships.md b/docs/content/docs/guides/relationships.md index d4d1a37ef45..52a09381c9f 100644 --- a/docs/content/docs/guides/relationships.md +++ b/docs/content/docs/guides/relationships.md @@ -1,8 +1,10 @@ --- -title: "Understanding Relationships" -description: "Learn how to reason about and configure relationships in Keystone so you can bring value to your project through structured content." ---- +title: Understanding Relationships +description: >- + Learn how to reason about and configure relationships in Keystone so you can + bring value to your project through structured content. +--- Relationships are the connections you make between different lists of content in Keystone. What you build depends a great deal on what you need. This guide will show you how to reason about, and configure relationships in Keystone so you can bring value to your project through structured content. --- @@ -15,7 +17,7 @@ Keystone provides a lot of flexibility when it comes to relationships. To get wh Your needs here will define whether your relationship needs to be [one, or two-sided](#one-sided-and-two-sided-relationships). -2. **How many connections do I need on either side of my relationship?** +1. **How many connections do I need on either side of my relationship?** Understanding this will determine the kind of [cardinality](#establishing-cardinality) you need to configure. @@ -44,12 +46,12 @@ export default config({ ``` {% hint kind="tip" %} -The `many` config option relates to _cardinality_ which we explore later +The `many` config option relates to *cardinality* which we explore later {% /hint %} ## One-sided & two-sided relationships -In Keystone it’s possible to define relationships from one, or both sides of the two lists you’re connecting. We refer to these as _one-sided_, and _two-sided_ relationships: +In Keystone it’s possible to define relationships from one, or both sides of the two lists you’re connecting. We refer to these as *one-sided*, and *two-sided* relationships: ### One-sided @@ -126,7 +128,7 @@ Two-sided relationships are declared in two places, but **there is only one rela ### Self-referencing relationships -Keystone also lets you define one, and two-sided **relationships that refer to the same list**. To make a _one-sided_ Twitter style following relationship we do the following: +Keystone also lets you define one, and two-sided **relationships that refer to the same list**. To make a *one-sided* Twitter style following relationship we do the following: ```typescript{9}[1-3] import { config, list } from '@keystone-6/core'; @@ -144,7 +146,7 @@ export default config({ }); ``` -Or change this into a _two-sided_ relationship to also access the followers of every user: +Or change this into a *two-sided* relationship to also access the followers of every user: ```typescript{9-10}[1-3] import { config, list } from '@keystone-6/core'; @@ -169,17 +171,28 @@ The only relationship configuration not currently supported is having a field re ## Establishing cardinality -_Cardinality_ is a term used to describe how many items can exist on either side of a relationship. Each side can have either `one` or `many` related items. Since each relationship can have one and two sides, we have the following options available: +*Cardinality* is a term used to describe how many items can exist on either side of a relationship. Each side can have either `one` or `many` related items. Since each relationship can have one and two sides, we have the following options available: -| Relationship type | One to one | One to many | Many to many | -| ----------------- | -------------------------------------------- | ---------------------------------------- | ---------------------------------------- | -| One-sided | {% emoji symbol="❌" alt="Not supported" /%} | {% emoji symbol="✅" alt="Supported" /%} | {% emoji symbol="✅" alt="Supported" /%} | -| Two-sided | {% emoji symbol="✅" alt="Supported" /%} | {% emoji symbol="✅" alt="Supported" /%} | {% emoji symbol="✅" alt="Supported" /%} | +{% table %} +- Relationship type +- One to one +- One to many +- Many to many +--- +- One-sided +- {% emoji symbol="❌" alt="Not supported" /%} +- {% emoji symbol="✅" alt="Supported" /%} +- {% emoji symbol="✅" alt="Supported" /%} +--- +- Two-sided +- {% emoji symbol="✅" alt="Supported" /%} +- {% emoji symbol="✅" alt="Supported" /%} +- {% emoji symbol="✅" alt="Supported" /%} +{% /table %} Cardinality is defined through the `relationship` field’s `many` configuration option. It’s a boolean where: - `false` = one - - `true` = many {% hint kind="tip" %} @@ -350,8 +363,10 @@ Keystone relationships are managed using the [relationship](../fields/relationsh {% related-content %} {% well -heading="Relationship Field API Reference" -href="/docs/fields/relationship" %} + heading="Relationship Field API Reference" + grad="grad1" + href="/docs/fields/relationship" + target="" %} Defines the names, types, and configuration of Keystone fields. See all the fields and the configuration options they accept. {% /well %} {% /related-content %} diff --git a/docs/content/docs/guides/schema-extension.md b/docs/content/docs/guides/schema-extension.md index 7e96205dbfc..27280bb413d 100644 --- a/docs/content/docs/guides/schema-extension.md +++ b/docs/content/docs/guides/schema-extension.md @@ -65,7 +65,7 @@ As `extendGraphqlSchema` expects a function that returns a valid GraphQL schema Start by installing `@graphql-tools/schema` ```bash -yarn add @graphql-tools/schema +npm install @graphql-tools/schema ``` Then import into your Keystone configuration diff --git a/docs/content/docs/reference/telemetry.md b/docs/content/docs/reference/telemetry.md index 7a401fe1636..c779ea1e6f2 100644 --- a/docs/content/docs/reference/telemetry.md +++ b/docs/content/docs/reference/telemetry.md @@ -63,6 +63,7 @@ Keystone collects telemetry information in the form of two different types of da We refer to these two different reports, as “device telemetry” and “project telemetry” respectively. These reports are forwarded to [https://telemetry.keystonejs.com/](https://telemetry.keystonejs.com/), and are reported separately to minimize any correlation between them insofar as the timing and grouping of that data, that an otherwise combined report may have. We are collecting these two reports for different reasons, and thus have no need to associate them. +We differentiate and record the type and version of reports from the URL used by Keystone. We additionally record a timestamp of the time that the report is received by the server at [https://telemetry.keystonejs.com](https://telemetry.keystonejs.com/). @@ -78,9 +79,9 @@ A device telemetry report is formatted as JSON and currently looks like: ```json { - "previous": "2022-11-23", + "lastSentDate": "2024-11-23", "os": "darwin", - "node": "18" + "node": "20" } ``` @@ -89,7 +90,8 @@ A device telemetry report is formatted as JSON and currently looks like: The type of information contained within a project telemetry report is currently: - The last date you used `keystone dev` for this project, and -- The resolved versions of any `@keystone-6` packages used by this project, and +- The resolved package versions of any `@keystone-6` packages used by this project, and +- The database type used by the project, - The number of lists for this project, and - The name and number of field types that you are using @@ -97,19 +99,20 @@ A project telemetry report is formatted as JSON and currently looks like: ```json { - "previous": "2022-11-23", - "versions": { - "@keystone-6/auth": "5.0.1", - "@keystone-6/core": "3.1.2", + "lastSentDate": "2024-11-23", + "packages": { + "@keystone-6/auth": "8.0.1", + "@keystone-6/core": "6.1.0", "@keystone-6/document-renderer": "1.1.2", "@keystone-6/fields-document": "5.0.2" }, + "database": "postgresql", "lists": 3, "fields": { "unknown": 1, "@keystone-6/text": 5, - "@keystone-6/image": 1, - "@keystone-6/file": 1 + "@keystone-6/timestamp": 2, + "@keystone-6/checkbox": 1 } } ``` @@ -185,8 +188,8 @@ If you wish to see how telemetry is currently configured for your device or proj ## What if I have a complaint or question -If you have any questions or concerns about the information that is gathered please contact us by logging a GitHub Issue [https://github.com/keystonejs/keystone](https://github.com/keystonejs/keystone). +If you have any questions or concerns about the information that is gathered please contact us by logging a GitHub Issue [https://github.com/keystonejs/keystone](https://github.com/keystonejs/keystone). -Alternatively please contact our Privacy Officer by email to [privacy@keystonejs.com](mailto:privacy@keystonejs.com), or by mail to Level 10, 191 Clarence Street, Sydney NSW 2000. +Alternatively please contact our Privacy Officer by email to [privacy@keystonejs.com](mailto:privacy@keystonejs.com), or by mail to Level 10, 191 Clarence Street, Sydney NSW 2000. For further information about Keystone’s security policy please see [https://github.com/keystonejs/keystone/security/policy](https://github.com/keystonejs/keystone/security/policy) diff --git a/docs/content/docs/walkthroughs/lesson-1.md b/docs/content/docs/walkthroughs/lesson-1.md index 8debe44867c..d1af920df81 100644 --- a/docs/content/docs/walkthroughs/lesson-1.md +++ b/docs/content/docs/walkthroughs/lesson-1.md @@ -1,8 +1,7 @@ --- -title: "Lesson 1: Installing Keystone" -description: "Learn Keystone: Lesson 1" +title: 'Lesson 1: Installing Keystone' +description: 'Learn Keystone: Lesson 1' --- - Learn how to install Keystone, create your first content type, and get an app up and running with an intuitive editing environment. ## Introduction @@ -29,17 +28,17 @@ Let’s start by setting up a workspace for a new Keystone project. Make a new f ```bash mkdir keystone-learning cd keystone-learning -yarn init +npm init ``` {% hint kind="tip" %} -We’ll be using `yarn` for installing packages, but you can use `npm` or any other package manager you prefer. +We’ll be using `npm` for installing packages, but you can use any other package manager you prefer. {% /hint %} Now add the Keystone package: ```bash -yarn add @keystone-6/core +npm install @keystone-6/core ``` ## Configure Keystone @@ -54,7 +53,7 @@ export default {}; And add TypeScript as a dependency: ```bash -yarn add typescript +npm install typescript ``` {% hint kind="tip" %} @@ -65,10 +64,10 @@ Your folder structure should now look like this: ```sh . -├── node_modules # Dependencies -├── keystone.ts # Keystone config -├── package.json # With Keystone and TypeScript as dependencies -└── yarn.lock # Your yarn lock file +├── node_modules # Dependencies +├── keystone.ts # Keystone config +├── package.json # With Keystone and TypeScript as dependencies +└── package-lock.json # Your npm lock file ``` We now need to configure `keystone.ts` with two parts to get our project running: @@ -143,14 +142,14 @@ are required, and declared that emails must be unique, so there can only be one We now have everything we need to start Keystone, so let’s do just that: ```bash -yarn keystone dev +npx keystone dev ``` -In a few seconds your terminal will provide you with you a link to the Keystone Admin UI at +In a few seconds your terminal will provide you with you a link to the Keystone Admin UI at [http://localhost:3000](http://localhost:3000) ![Terminal dialog showing successful Keystone startup](https://keystonejs.s3.amazonaws.com/framework-assets/assets/walkthroughs/lesson-1/keystone-startup.png) -Head on over to where you can create your first user with a `name` and `email`: +Head on over to [http://localhost:3000/users](http://localhost:3000/users) where you can create your first user with a `name` and `email`: ![Adding a user record in Keystone Admin UI](https://keystonejs.s3.amazonaws.com/framework-assets/assets/walkthroughs/lesson-1/first-user-creation.gif) @@ -161,7 +160,11 @@ Next up, we’ll level-up our blog starter with a `post` list and connect it to ## Next lesson {% related-content %} -{% well heading="Lesson 2: Relating things" href="/docs/walkthroughs/lesson-2" %} +{% well + heading="Lesson 2: Relating things" + grad="grad1" + href="/docs/walkthroughs/lesson-2" + target="" %} Connect two content types and learn how to configure the appearance of field inputs {% /well %} {% /related-content %} diff --git a/docs/content/docs/walkthroughs/lesson-2.md b/docs/content/docs/walkthroughs/lesson-2.md index b9eb96ece18..fd8183e53c2 100644 --- a/docs/content/docs/walkthroughs/lesson-2.md +++ b/docs/content/docs/walkthroughs/lesson-2.md @@ -1,8 +1,7 @@ --- -title: "Lesson 2: Creating relationships" -description: "Learn Keystone: Lesson 2" +title: 'Lesson 2: Creating relationships' +description: 'Learn Keystone: Lesson 2' --- - Learn how to connect two content types to each other and configure how you make those connections in Admin UI. ## Where we left off @@ -267,8 +266,10 @@ export default config({ {% related-content %} {% well -heading="Lesson 3: Publishing workflows" -href="/docs/walkthroughs/lesson-3" %} + heading="Lesson 3: Publishing workflows" + grad="grad1" + href="/docs/walkthroughs/lesson-3" + target="" %} Support publishing needs with Keystone's `select` and `timestamp` fields {% /well %} diff --git a/docs/content/docs/walkthroughs/lesson-3.md b/docs/content/docs/walkthroughs/lesson-3.md index 372c26b4d99..829785cfba6 100644 --- a/docs/content/docs/walkthroughs/lesson-3.md +++ b/docs/content/docs/walkthroughs/lesson-3.md @@ -1,8 +1,8 @@ --- -title: "Lesson 3: Publishing workflows" -description: "Learn Keystone: Lesson 3" ---- +title: 'Lesson 3: Publishing workflows' +description: 'Learn Keystone: Lesson 3' +--- Learn how to create a publishing workflow to your app using Keystons’s `select` and `timestamp` fields. ## Where we left off @@ -294,7 +294,11 @@ export default config({ ## Next lesson {% related-content %} -{% well heading="Lesson 4: Auth & Sessions" href="/docs/walkthroughs/lesson-4" %} +{% well + heading="Lesson 4: Auth & Sessions" + grad="grad1" + href="/docs/walkthroughs/lesson-4" + target="" %} Add sessions, password protection, and a sign-in screen to your Keystone app {% /well %} {% /related-content %} diff --git a/docs/content/docs/walkthroughs/lesson-4.md b/docs/content/docs/walkthroughs/lesson-4.md index 33db767623e..981d8441c5e 100644 --- a/docs/content/docs/walkthroughs/lesson-4.md +++ b/docs/content/docs/walkthroughs/lesson-4.md @@ -1,8 +1,8 @@ --- -title: "Lesson 4: Auth & Sessions" -description: "Learn Keystone: Lesson 4" ---- +title: 'Lesson 4: Auth & Sessions' +description: 'Learn Keystone: Lesson 4' +--- Learn how to add passwords, session data and authentication to your Keystone app. ## Where we left off @@ -105,7 +105,7 @@ That's all we need to store secure passwords in our database! Authentication isn't built directly in to Keystone - it's an enhancement you can add on top. To use it in our app we need to add Keystone’s [auth package](https://github.com/keystonejs/keystone/tree/main/packages/auth): ```sh -yarn add @keystone-6/auth +npm install @keystone-6/auth ``` Now that we have the package, let’s create a new file in the root of our project to write our auth config in: @@ -332,7 +332,11 @@ export default config( ## Next lesson {% related-content %} -{% well heading="Lesson 5: Rich Text" href="/docs/walkthroughs/lesson-5" %} +{% well + heading="Lesson 5: Rich Text" + grad="grad1" + href="/docs/walkthroughs/lesson-5" + target="" %} Add a powerful `document` field to your app and learn how to configure it to meet your needs {% /well %} diff --git a/docs/content/docs/walkthroughs/lesson-5.md b/docs/content/docs/walkthroughs/lesson-5.md index 1c4f0f13d0a..5a58bb133da 100644 --- a/docs/content/docs/walkthroughs/lesson-5.md +++ b/docs/content/docs/walkthroughs/lesson-5.md @@ -1,8 +1,7 @@ --- -title: "Lesson 5: Document field" -description: "Learn Keystone: Lesson 5" +title: 'Lesson 5: Document field' +description: 'Learn Keystone: Lesson 5' --- - Learn how to implement a powerful and customisable Rich Text editing experience with Keystone’s `document` field. ## Where we left off @@ -65,7 +64,7 @@ Keystone’s [document](https://keystonejs.com/docs/fields/document) field is a To implement the document field we start by adding the package to our project: ```sh -yarn add @keystone-6/fields-document +npm install @keystone-6/fields-document ``` Next, we add the document field to our `post` list: @@ -261,10 +260,15 @@ Congratulations! You've now built a Keystone app from an empty folder and have t This lesson marks the end of this learning series. To dive deeper into Keystone's capabilities take a look at the following: {% related-content %} -{% well heading="Examples" href="/docs/examples" %} +{% well heading="Examples" grad="grad1" href="/docs/examples" target="" %} A growing collection of projects you can run locally to learn more about Keystone’s capabilities {% /well %} -{% well heading="Guides" href="/docs/guides/overview" %} + +{% well + heading="Guides" + grad="grad1" + href="/docs/guides/overview" + target="" %} Practical explanations of Keystone's fundamental building blocks {% /well %} {% /related-content %} diff --git a/docs/content/examples/auth.yaml b/docs/content/examples/auth.yaml new file mode 100644 index 00000000000..d5983d8190b --- /dev/null +++ b/docs/content/examples/auth.yaml @@ -0,0 +1,5 @@ +url: https://github.com/keystonejs/keystone/tree/main/examples/auth +title: Authentication +kind: standalone +description: > + Adds password-based authentication to the Task Manager starter project. diff --git a/docs/content/examples/azure.yaml b/docs/content/examples/azure.yaml new file mode 100644 index 00000000000..e5edf787e2e --- /dev/null +++ b/docs/content/examples/azure.yaml @@ -0,0 +1,6 @@ +title: Microsoft Azure +kind: deployment +url: https://github.com/aaronpowell/keystone-6-azure-example +description: > + Deploys a Keystone app backend to Microsoft Azure. Based on the `with-auth` + project. **One-click deployment** included. diff --git a/docs/content/examples/blog.yaml b/docs/content/examples/blog.yaml new file mode 100644 index 00000000000..fa9d654f263 --- /dev/null +++ b/docs/content/examples/blog.yaml @@ -0,0 +1,6 @@ +title: Blog +kind: standalone +url: https://github.com/keystonejs/keystone/tree/main/examples/usecase-blog +description: > + A basic Blog schema with Posts and Authors. Use this as a starting place for + learning how to use Keystone. It’s also a starter for other feature projects. diff --git a/docs/content/examples/custom-admin-ui-logo.yaml b/docs/content/examples/custom-admin-ui-logo.yaml new file mode 100644 index 00000000000..d5bf1250891 --- /dev/null +++ b/docs/content/examples/custom-admin-ui-logo.yaml @@ -0,0 +1,5 @@ +title: Custom Admin UI Logo +url: https://github.com/keystonejs/keystone/blob/main/examples/custom-admin-ui-logo +kind: standalone +description: > + Adds a custom logo component in the Admin UI. diff --git a/docs/content/examples/custom-admin-ui-navigation.yaml b/docs/content/examples/custom-admin-ui-navigation.yaml new file mode 100644 index 00000000000..f1439f3e10d --- /dev/null +++ b/docs/content/examples/custom-admin-ui-navigation.yaml @@ -0,0 +1,5 @@ +title: Custom Admin UI Navigation +url: https://github.com/keystonejs/keystone/tree/main/examples/custom-admin-ui-navigation +kind: standalone +description: > + Adds a custom Navigation component to the Admin UI. diff --git a/docs/content/examples/custom-admin-ui-pages.yaml b/docs/content/examples/custom-admin-ui-pages.yaml new file mode 100644 index 00000000000..0235df6f2b7 --- /dev/null +++ b/docs/content/examples/custom-admin-ui-pages.yaml @@ -0,0 +1,5 @@ +url: https://github.com/keystonejs/keystone/tree/main/examples/custom-admin-ui-pages +title: Custom Admin UI Pages +kind: standalone +description: > + Adds a custom page in the Admin UI. diff --git a/docs/content/examples/custom-field-type.yaml b/docs/content/examples/custom-field-type.yaml new file mode 100644 index 00000000000..7b05f00dbc4 --- /dev/null +++ b/docs/content/examples/custom-field-type.yaml @@ -0,0 +1,5 @@ +url: https://github.com/keystonejs/keystone/tree/main/examples/custom-field +title: Custom Field Type +kind: standalone +description: > + Adds a custom field type based on the `integer` field type which lets users rate items on a 5-star scale. diff --git a/docs/content/examples/custom-field-view.yaml b/docs/content/examples/custom-field-view.yaml new file mode 100644 index 00000000000..35b1c3dc32d --- /dev/null +++ b/docs/content/examples/custom-field-view.yaml @@ -0,0 +1,5 @@ +url: https://github.com/keystonejs/keystone/tree/main/examples/custom-field-view +title: Custom Field View +kind: standalone +description: > + Adds a custom Admin UI view to a `json` field which provides a customised editing experience for users. diff --git a/docs/content/examples/default-values.yaml b/docs/content/examples/default-values.yaml new file mode 100644 index 00000000000..f9e0832f81f --- /dev/null +++ b/docs/content/examples/default-values.yaml @@ -0,0 +1,8 @@ +title: Default Values +kind: standalone +url: https://github.com/keystonejs/keystone/tree/main/examples/default-values +description: > + Demonstrates how to use default values for fields. Builds upon the Task + Manager starter + + project. diff --git a/docs/content/examples/document-field-customisation.yaml b/docs/content/examples/document-field-customisation.yaml new file mode 100644 index 00000000000..2c1f2f0f5d0 --- /dev/null +++ b/docs/content/examples/document-field-customisation.yaml @@ -0,0 +1,5 @@ +url: https://github.com/keystonejs/keystone/tree/main/examples/usecase-blog +title: Document Field Customisation +kind: end-to-end +description: > + Example to demonstrate customisation of Keystone's document field and document renderer. diff --git a/docs/content/examples/document-field.yaml b/docs/content/examples/document-field.yaml new file mode 100644 index 00000000000..0ae9d24f6ce --- /dev/null +++ b/docs/content/examples/document-field.yaml @@ -0,0 +1,6 @@ +url: https://github.com/keystonejs/keystone/tree/main/examples/document-field +title: Document field +kind: standalone +description: > + Illustrates how to configure `document` fields in your + Keystone system and render their data in a frontend application. diff --git a/docs/content/examples/extend-express-app.yaml b/docs/content/examples/extend-express-app.yaml new file mode 100644 index 00000000000..c549b2565dd --- /dev/null +++ b/docs/content/examples/extend-express-app.yaml @@ -0,0 +1,6 @@ +url: https://github.com/keystonejs/keystone/tree/main/examples/extend-express-app +title: REST API endpoint +kind: standalone +description: > + Demonstrates how to create REST endpoints by extending Keystone's express app and using the + Query API to execute queries against the schema. diff --git a/docs/content/examples/extend-graphql-schema-nexus.yaml b/docs/content/examples/extend-graphql-schema-nexus.yaml new file mode 100644 index 00000000000..cf157b1d2cd --- /dev/null +++ b/docs/content/examples/extend-graphql-schema-nexus.yaml @@ -0,0 +1,6 @@ +url: https://github.com/keystonejs/keystone/tree/main/examples/extend-graphql-schema-nexus +title: Extend GraphQL API with Nexus +kind: standalone +description: > + Shows you how to use `Nexus` to extend the GraphQL API provided by Keystone + with custom queries and mutations. diff --git a/docs/content/examples/extend-graphql-schema-ts.yaml b/docs/content/examples/extend-graphql-schema-ts.yaml new file mode 100644 index 00000000000..8bbcd4db886 --- /dev/null +++ b/docs/content/examples/extend-graphql-schema-ts.yaml @@ -0,0 +1,6 @@ +url: https://github.com/keystonejs/keystone/tree/main/examples/extend-graphql-schema-graphql-ts +title: Extend GraphQL Schema with GraphQL TS +kind: standalone +description: > + Demonstrates how to extend the GraphQL API provided by Keystone with custom queries and + mutations using graphql-ts. diff --git a/docs/content/examples/heroku.yaml b/docs/content/examples/heroku.yaml new file mode 100644 index 00000000000..0585f3802d9 --- /dev/null +++ b/docs/content/examples/heroku.yaml @@ -0,0 +1,6 @@ +title: Heroku +kind: deployment +url: https://github.com/keystonejs/keystone-6-heroku-example +description: > + Based on the `with-auth` project, this example deploys a simple app backend to + Heroku. Includes a **one-click deployment** for Heroku account holders. diff --git a/docs/content/examples/next-js.yaml b/docs/content/examples/next-js.yaml new file mode 100644 index 00000000000..6aa9cea7fcd --- /dev/null +++ b/docs/content/examples/next-js.yaml @@ -0,0 +1,5 @@ +url: https://github.com/keystonejs/keystone/tree/main/examples/framework-nextjs-app-directory +title: Next.js + Keystone +kind: end-to-end +description: > + Shows you how to use Keystone as a data engine within Next.js applications. diff --git a/docs/content/examples/railway.yaml b/docs/content/examples/railway.yaml new file mode 100644 index 00000000000..5d24141dbfc --- /dev/null +++ b/docs/content/examples/railway.yaml @@ -0,0 +1,6 @@ +title: Railway +kind: deployment +url: https://github.com/keystonejs/keystone-6-railway-example +description: > + Deploys a Keystone app backend to Railway. Based on the `with-auth` project. + **One-click deployment** included. diff --git a/docs/content/examples/singleton.yaml b/docs/content/examples/singleton.yaml new file mode 100644 index 00000000000..824427b184a --- /dev/null +++ b/docs/content/examples/singleton.yaml @@ -0,0 +1,5 @@ +url: https://github.com/keystonejs/keystone/tree/main/examples/singleton +title: Singleton +kind: standalone +description: > + Demonstrates how to use singleton lists with Keystone. diff --git a/docs/content/examples/task-manager.yaml b/docs/content/examples/task-manager.yaml new file mode 100644 index 00000000000..2d72c0a1bb5 --- /dev/null +++ b/docs/content/examples/task-manager.yaml @@ -0,0 +1,7 @@ +title: Task Manager +kind: standalone +url: https://github.com/keystonejs/keystone/tree/main/examples/usecase-todo +description: > + A basic Task Management app, with Tasks and People who can be assigned to + tasks. Great for learning how to use Keystone. It’s also a starter for other + feature projects. diff --git a/docs/content/examples/testing.yaml b/docs/content/examples/testing.yaml new file mode 100644 index 00000000000..fd7ac3a82d3 --- /dev/null +++ b/docs/content/examples/testing.yaml @@ -0,0 +1,5 @@ +url: https://github.com/keystonejs/keystone/tree/main/examples/testing +title: Testing +kind: standalone +description: > + Shows you how to write tests against the GraphQL API to your Keystone system. diff --git a/docs/content/examples/virtual-field.yaml b/docs/content/examples/virtual-field.yaml new file mode 100644 index 00000000000..2ceeffce844 --- /dev/null +++ b/docs/content/examples/virtual-field.yaml @@ -0,0 +1,5 @@ +url: https://github.com/keystonejs/keystone/tree/main/examples/virtual-field +title: Virtual fields +kind: standalone +description: > + Implements virtual fields in a Keystone list. diff --git a/docs/content/featured-docs.yaml b/docs/content/featured-docs.yaml new file mode 100644 index 00000000000..533b6539c7b --- /dev/null +++ b/docs/content/featured-docs.yaml @@ -0,0 +1,145 @@ +groups: + - groupName: Walkthroughs + groupDescription: | + Step-by-step instructions for getting things done with Keystone. + gradient: grad2 + items: + - label: Keystone Quick Start + link: + discriminant: docs + value: + docPage: getting-started + description: | + Take a tour of Keystone in minutes with our CLI starter project + - label: 'Lesson 1: Installing Keystone' + link: + discriminant: docs + value: + docPage: walkthroughs/lesson-1 + description: | + Get Keystone up and running with your first content type + - label: 'Lesson 2: Relating things' + link: + discriminant: docs + value: + docPage: walkthroughs/lesson-2 + description: | + Connect two content types and learn how to configure the appearance of field inputs + + - label: 'Lesson 3: Publishing Workflows' + link: + discriminant: docs + value: + docPage: walkthroughs/lesson-3 + description: | + Support publishing needs with Keystone's `select` and `timestamp` fields + + - label: 'Lesson 4: Auth & Sessions' + link: + discriminant: docs + value: + docPage: walkthroughs/lesson-4 + description: | + Add sessions, password protection, and a sign-in screen to your Keystone app + - label: 'Lesson 5: Rich Text' + link: + discriminant: docs + value: + docPage: walkthroughs/lesson-5 + description: | + Add a powerful `document` field to your app and learn how to configure it to meet your needs + - groupName: Guides + groupDescription: > + Practical explanations of Keystone’s fundamental building blocks. When + you’re trying to get something done, Keystone guides show you how to think + about, and get the most out of each feature. + gradient: grad2 + items: + - label: Command Line Foundations + link: + discriminant: docs + value: + docPage: guides/cli + description: | + Keystone’s CLI helps you develop, build, and deploy projects. This guide explains all you need to standup a new backend in the terminal. + + - label: Understanding Relationships + link: + discriminant: docs + value: + docPage: guides/relationships + description: | + Learn how to reason about and configure relationships in Keystone, so you can bring value to your project through structured content. + + - label: GraphQL Queries - Filters + link: + discriminant: docs + value: + docPage: guides/filters + description: | + Query filters are an integral part of Keystone’s powerful GraphQL APIs. This guide will show you how to use filters to get the data you need from your system. + + - label: Understanding Hooks + link: + discriminant: docs + value: + docPage: guides/hooks + description: | + Learn how to use Hooks within your schema to extend Keystone’s powerful CRUD GraphQL APIs with your own business logic. + + - label: How To Use Document Fields + link: + discriminant: docs + value: + docPage: guides/document-fields + description: | + Keystone’s document field is a highly customisable rich text editor that stores content as structured JSON. Learn how to configure it and incorporate your own custom React components. + + - label: Document Field Demo + link: + discriminant: url + value: + url: /docs/guides/document-field-demo + description: | + Test drive the many features of Keystone’s Document field on this website. + + - label: Custom Fields + link: + discriminant: docs + value: + docPage: guides/custom-fields + description: | + Learn how to define your own custom field types in Keystone, with customisable backend data structure, and Admin UI appearance. + + - label: Testing Guide + link: + discriminant: docs + value: + docPage: guides/testing + description: | + Learn how to test the behaviour of your Keystone system to ensure it does what you expect. + + - label: Virtual Fields + link: + discriminant: docs + value: + docPage: guides/virtual-fields + description: | + Virtual fields offer a powerful way to extend your GraphQL API. This guide introduces the syntax and shows you how start simply and end up with a complex result. + + - label: Choosing a Database + link: + discriminant: docs + value: + docPage: guides/choosing-a-database + description: | + Keystone supports Postgres, MySQL and SQLite. This guide explains how to choose the best for your project. + + - label: Images and Files + link: + discriminant: docs + value: + docPage: guides/images-and-files + description: | + Learn how to store and manage Images and Files in Keystone. + diff --git a/docs/content/featured-examples.yaml b/docs/content/featured-examples.yaml new file mode 100644 index 00000000000..b980a9de66e --- /dev/null +++ b/docs/content/featured-examples.yaml @@ -0,0 +1,23 @@ +label: Example Projects +description: > + A growing collection of projects you can run locally to learn more about + Keystone features. Use these as a reference for best practice, and a jumping + off point when adding features to your own Keystone project. [View on Github + →](https://github.com/keystonejs/keystone/tree/main/examples) +gradient: grad3 +items: + - blog + - task-manager + - extend-graphql-schema + - default-values + - virtual-field + - document-field + - testing + - auth + - json-field + - custom-field-view + - custom-field-type + - custom-admin-ui-pages + - custom-admin-ui-logo + - custom-admin-ui-navigation + - document-field-customisation diff --git a/docs/content/navigation.yaml b/docs/content/navigation.yaml new file mode 100644 index 00000000000..3d18e22fce1 --- /dev/null +++ b/docs/content/navigation.yaml @@ -0,0 +1,285 @@ +navGroups: + - groupName: Start + items: + - label: Docs Home + link: + discriminant: url + value: /docs + status: default + - label: Walkthroughs + link: + discriminant: url + value: /docs/walkthroughs + status: default + - label: Examples + link: + discriminant: url + value: /docs/examples + status: default + - groupName: Guides + items: + - label: Overview + link: + discriminant: url + value: /docs/guides/overview + status: default + - label: Command Line + link: + discriminant: page + value: guides/cli + status: default + - label: Relationships + link: + discriminant: page + value: guides/relationships + status: default + - label: Choosing a Database + link: + discriminant: page + value: guides/choosing-a-database + status: default + - label: Database Migration + link: + discriminant: page + value: guides/database-migration + status: new + - label: Query Filters + link: + discriminant: page + value: guides/filters + status: updated + - label: Hooks + link: + discriminant: page + value: guides/hooks + status: updated + - label: Auth & Access Control + link: + discriminant: page + value: guides/auth-and-access-control + status: new + - label: Images & Files + link: + discriminant: page + value: guides/images-and-files + status: new + - label: GraphQL Schema Extension + link: + discriminant: page + value: guides/schema-extension + status: new + - label: Testing + link: + discriminant: page + value: guides/testing + status: default + - label: Document Fields + link: + discriminant: page + value: guides/document-fields + status: default + - label: Document Fields Demo + link: + discriminant: url + value: /docs/guides/document-field-demo + status: default + - label: Virtual Fields + link: + discriminant: page + value: guides/virtual-fields + status: default + - label: Custom Fields + link: + discriminant: page + value: guides/custom-fields + status: default + - label: Custom Admin UI Logo + link: + discriminant: page + value: guides/custom-admin-ui-logo + status: default + - label: Custom Admin UI Pages + link: + discriminant: page + value: guides/custom-admin-ui-pages + status: default + - label: Custom Admin UI Navigation + link: + discriminant: page + value: guides/custom-admin-ui-navigation + status: default + - groupName: Configuration + items: + - label: Overview + link: + discriminant: url + value: /docs/config/overview + status: default + - label: Config + link: + discriminant: url + value: /docs/config/config + status: default + - label: Lists + link: + discriminant: page + value: config/lists + status: default + - label: Authentication + link: + discriminant: page + value: config/auth + status: default + - label: Access Control + link: + discriminant: page + value: config/access-control + status: updated + - label: Hooks + link: + discriminant: page + value: config/hooks + status: updated + - label: Session + link: + discriminant: page + value: config/session + status: default + - groupName: Fields + items: + - label: Overview + link: + discriminant: page + value: fields/overview + status: default + - label: BigInt + link: + discriminant: page + value: fields/bigint + status: default + - label: Calendar Day + link: + discriminant: page + value: fields/calendarday + status: default + - label: Checkbox + link: + discriminant: page + value: fields/checkbox + status: default + - label: Cloudinary Image + link: + discriminant: page + value: fields/cloudinaryimage + status: default + - label: Decimal + link: + discriminant: page + value: fields/decimal + status: default + - label: Document + link: + discriminant: page + value: fields/document + status: default + - label: File + link: + discriminant: page + value: fields/file + status: default + - label: Float + link: + discriminant: page + value: fields/float + status: default + - label: Image + link: + discriminant: page + value: fields/image + status: default + - label: Integer + link: + discriminant: page + value: fields/integer + status: default + - label: JSON + link: + discriminant: page + value: fields/json + status: default + - label: Multiselect + link: + discriminant: page + value: fields/multiselect + status: default + - label: Password + link: + discriminant: page + value: fields/password + status: default + - label: Relationship + link: + discriminant: page + value: fields/relationship + status: default + - label: Select + link: + discriminant: page + value: fields/select + status: default + - label: Text + link: + discriminant: page + value: fields/text + status: default + - label: Timestamp + link: + discriminant: page + value: fields/timestamp + status: default + - label: Virtual + link: + discriminant: page + value: fields/virtual + status: default + - groupName: Context + items: + - label: Overview + link: + discriminant: page + value: context/overview + status: default + - label: getContext + link: + discriminant: page + value: context/get-context + status: default + - label: Query + link: + discriminant: page + value: context/query + status: default + - label: DB + link: + discriminant: page + value: context/db-items + status: default + - groupName: GraphQL + items: + - label: Overview + link: + discriminant: page + value: graphql/overview + status: updated + - label: Query Filters + link: + discriminant: page + value: graphql/filters + status: updated + - groupName: Reference + items: + - label: Telemetry + link: + discriminant: page + value: reference/telemetry + status: default \ No newline at end of file diff --git a/docs/keystatic.config.tsx b/docs/keystatic.config.tsx index 2881a47530f..7da18b30091 100644 --- a/docs/keystatic.config.tsx +++ b/docs/keystatic.config.tsx @@ -1,9 +1,11 @@ -// keystatic.config.ts -import { config, fields, collection } from '@keystatic/core' +import { config, fields, collection, singleton } from '@keystatic/core' import { superscriptIcon } from '@keystar/ui/icon/icons/superscriptIcon' +import { mark, wrapper } from '@keystatic/core/content-components' +import { cableIcon } from '@keystar/ui/icon/icons/cableIcon' +import { creditCardIcon } from '@keystar/ui/icon/icons/creditCardIcon' -import { inline, mark, wrapper } from '@keystatic/core/content-components' -// import { WellPreview } from './keystatic/admin-previews' +import { emoji, hint } from './keystatic/content-components' +import { gradientSelector } from './keystatic/gradient-selector' export default config({ storage: { @@ -13,17 +15,25 @@ export default config({ brand: { name: 'Keystone Website', }, + navigation: { + Pages: ['docs', 'posts', 'examples'], + Config: ['navigation', 'featuredDocs', 'featuredExamples'], + }, }, collections: { + // ------------------------------ + // Docs + // ------------------------------ docs: collection({ - label: 'Docs', + label: 'Docs Pages', path: 'content/docs/**', slugField: 'title', columns: ['title'], format: { contentField: 'content' }, + entryLayout: 'content', schema: { - title: fields.slug({ name: { label: 'Title' } }), - description: fields.text({ label: 'Description' }), + title: fields.slug({ name: { label: 'Title', validation: { isRequired: true } } }), + description: fields.text({ label: 'Description', validation: { isRequired: true } }), content: fields.markdoc({ label: 'Content', extension: 'md', @@ -39,6 +49,7 @@ export default config({ heading: wrapper({ label: 'Heading', schema: { id: fields.text({ label: 'ID' }) } }), well: wrapper({ label: 'Well', + icon: creditCardIcon, schema: { heading: fields.text({ label: 'Heading' }), grad: fields.select({ @@ -70,38 +81,21 @@ export default config({ // ContentView: (data) => , }), - hint: wrapper({ - label: 'Hint', - schema: { - kind: fields.select({ - label: 'Kind', - options: [ - { label: 'Tip', value: 'tip' }, - { label: 'Warning', value: 'warn' }, - { label: 'Error', value: 'error' }, - ], - defaultValue: 'tip', - }), - }, - }), - 'related-content': wrapper({ label: 'Related Content', schema: {} }), + hint: hint(), + 'related-content': wrapper({ label: 'Related Content', icon: cableIcon, schema: {} }), sup: mark({ label: 'Superscript', schema: {}, icon: superscriptIcon, tag: 'sup' }), - emoji: inline({ - label: 'Emoji', - schema: { - symbol: fields.text({ label: 'Symbol' }), - alt: fields.text({ label: 'Alt' }), - }, - }), + emoji: emoji(), }, }), }, }), + // ------------------------------ // Blog + // ------------------------------ posts: collection({ + label: 'Blog Posts', path: 'content/blog/*', - label: 'Blog', slugField: 'title', columns: ['title'], format: { contentField: 'content' }, @@ -112,7 +106,172 @@ export default config({ authorName: fields.text({ label: 'Author Name', validation: { isRequired: true } }), authorHandle: fields.url({ label: 'Author Handle' }), metaImageUrl: fields.url({ label: 'Meta Image URL' }), - content: fields.markdoc({ label: 'Content', extension: 'md' }), + content: fields.markdoc({ + label: 'Content', + extension: 'md', + components: { + hint: hint('Callout'), + emoji: emoji(), + }, + }), + }, + }), + + // ------------------------------ + // Examples + // ------------------------------ + examples: collection({ + label: 'GitHub Examples', + path: 'content/examples/*', + slugField: 'title', + schema: { + title: fields.slug({ name: { label: 'Title' } }), + kind: fields.select({ + label: 'Example Kind', + options: [ + { label: 'Standalone', value: 'standalone' }, + { label: 'End-to-End', value: 'end-to-end' }, + { label: 'Deployment', value: 'deployment' }, + ], + defaultValue: 'standalone', + }), + url: fields.text({ + label: 'URL', + validation: { isRequired: true }, + }), + description: fields.markdoc.inline({ label: 'Description' }), + }, + }), + }, + singletons: { + // ------------------------------ + // Navigation + // ------------------------------ + navigation: singleton({ + label: 'Docs Sidebar Navigation', + path: 'content/navigation', + schema: { + navGroups: fields.array( + fields.object({ + groupName: fields.text({ label: 'Group name' }), + items: fields.array( + fields.object({ + label: fields.text({ + label: 'Label', + description: "Required when using a URL, or overriding the page's title", + }), + link: fields.conditional( + fields.select({ + label: 'Link type', + options: [ + { label: 'Page', value: 'page' }, + { label: 'URL', value: 'url' }, + ], + defaultValue: 'page', + }), + { + page: fields.relationship({ + label: 'Docs Page', + collection: 'docs', + }), + url: fields.text({ label: 'URL' }), + } + ), + status: fields.select({ + label: 'Status', + options: [ + { label: 'Default', value: 'default' }, + { label: 'New', value: 'new' }, + { label: 'Updated', value: 'updated' }, + ], + defaultValue: 'default', + }), + }), + { + label: 'Navigation items', + itemLabel: (props) => props.fields.label.value, + } + ), + }), + { + label: 'Navigation groups', + itemLabel: (props) => props.fields.groupName.value, + } + ), + }, + }), + + // ------------------------------ + // Featured Docs + // ------------------------------ + featuredDocs: singleton({ + label: 'Featured Docs', + path: 'content/featured-docs', + schema: { + groups: fields.array( + fields.object({ + groupName: fields.text({ label: 'Group name' }), + groupDescription: fields.markdoc.inline({ label: 'Group description' }), + gradient: gradientSelector({ defaultValue: 'grad1' }), + items: fields.array( + fields.object({ + label: fields.text({ + label: 'Label', + description: "Required when using a URL, or overriding the page's title", + validation: { isRequired: true }, + }), + link: fields.conditional( + fields.select({ + label: 'Link type', + options: [ + { label: 'Docs Page', value: 'docs' }, + { label: 'URL', value: 'url' }, + ], + defaultValue: 'docs', + }), + { + docs: fields.object( + { + docPage: fields.relationship({ + label: 'Docs Page', + collection: 'docs', + }), + description: fields.markdoc.inline({ label: 'Description' }), + }, + { + label: 'Docs Page', + } + ), + url: fields.object({ + url: fields.text({ label: 'URL' }), + description: fields.markdoc.inline({ label: 'Description' }), + }), + } + ), + }), + { + label: 'Featured Items', + itemLabel: (props) => + `${props.fields.label.value} — [${props.fields.link.discriminant}]`, + } + ), + }), + { + label: 'Featured Groups', + itemLabel: (props) => props.fields.groupName.value, + } + ), + }, + }), + + featuredExamples: singleton({ + label: 'Featured Examples', + path: 'content/featured-examples', + schema: { + label: fields.text({ label: 'Label' }), + description: fields.markdoc.inline({ label: 'Group description' }), + gradient: gradientSelector({ defaultValue: 'grad1' }), + items: fields.multiRelationship({ label: 'Examples List', collection: 'examples' }), }, }), }, diff --git a/docs/keystatic/admin-previews.tsx b/docs/keystatic/admin-previews.tsx deleted file mode 100644 index 87547995376..00000000000 --- a/docs/keystatic/admin-previews.tsx +++ /dev/null @@ -1,7 +0,0 @@ -'use client' - -import { Well } from '../components/primitives/Well' - -export function WellPreview (data) { - return {data.children} -} diff --git a/docs/keystatic/content-components.tsx b/docs/keystatic/content-components.tsx new file mode 100644 index 00000000000..43c0da277fc --- /dev/null +++ b/docs/keystatic/content-components.tsx @@ -0,0 +1,35 @@ +import { fields } from '@keystatic/core' +import { inline, wrapper } from '@keystatic/core/content-components' +import { megaphoneIcon } from '@keystar/ui/icon/icons/megaphoneIcon' +import { smileIcon } from '@keystar/ui/icon/icons/smileIcon' + +type Label = string + +export function hint (label: Label = 'Hint') { + return wrapper({ + label: label, + icon: megaphoneIcon, + schema: { + kind: fields.select({ + label: 'Kind', + options: [ + { label: 'Tip', value: 'tip' }, + { label: 'Warning', value: 'warn' }, + { label: 'Error', value: 'error' }, + ], + defaultValue: 'tip', + }), + }, + }) +} + +export function emoji (label: Label = 'Emoji') { + return inline({ + label: label, + icon: smileIcon, + schema: { + symbol: fields.text({ label: 'Symbol' }), + alt: fields.text({ label: 'Alt' }), + }, + }) +} diff --git a/docs/keystatic/get-featured-docs-map.ts b/docs/keystatic/get-featured-docs-map.ts new file mode 100644 index 00000000000..6af662e1fc5 --- /dev/null +++ b/docs/keystatic/get-featured-docs-map.ts @@ -0,0 +1,56 @@ +import { type Tag, transform } from '@markdoc/markdoc' +import { reader } from './reader' +import { baseMarkdocConfig } from '../markdoc/config' + +export type FeaturedDocsMap = Awaited> + +export async function getFeaturedDocsMap () { + const featuredDocs = await reader.singletons.featuredDocs.read({ resolveLinkedFiles: true }) + if (!featuredDocs) return null + + // We need the docs data as well... + const docs = await reader.collections.docs.all() + // Individual doc accessor + const docsBySlug = new Map(docs.map((doc) => [doc.slug, doc])) + + // Each `item` will need to be processed differently based on the `link` discriminant + async function processItem ({ label, link, wide, gradient }) { + const { discriminant, value } = link + let description: Tag | null = null + let href: string = '#' + + switch (discriminant) { + case 'url': { + description = transform(value.description.node, baseMarkdocConfig) as Tag + href = value.url + break + } + case 'docs': { + const docPage = value.docPage ? docsBySlug.get(value.docPage) : null + if (!docPage) throw new Error(`No doc page found for slug: ${value.docPage}`) + description = transform(value.description.node, baseMarkdocConfig) as Tag + href = docPage.slug ? `/docs/${docPage.slug}` : '#' + break + } + } + + return { label, description, href, wide, gradient } + } + + // Processing all groups... + const processedGroups = await Promise.all( + featuredDocs.groups.map(async ({ groupName, groupDescription, gradient, items }) => { + const transformedGroupDescription = transform(groupDescription.node, baseMarkdocConfig) as Tag + const processedItems = await Promise.all(items.map(processItem)) + + return { + groupName, + groupDescription: transformedGroupDescription, + gradient, + items: processedItems, + } + }) + ) + + return processedGroups +} \ No newline at end of file diff --git a/docs/keystatic/gradient-selector.ts b/docs/keystatic/gradient-selector.ts new file mode 100644 index 00000000000..75da45021f8 --- /dev/null +++ b/docs/keystatic/gradient-selector.ts @@ -0,0 +1,17 @@ +import { fields } from '@keystatic/core' + +export type Gradient = 'grad1' | 'grad2' | 'grad3' | 'grad4' + +export function gradientSelector ({ defaultValue = 'grad1' }: { defaultValue: Gradient }) { + return fields.select({ + label: 'Gradient', + description: 'The gradient to use for the group', + options: [ + { label: '1', value: 'grad1' }, + { label: '2', value: 'grad2' }, + { label: '3', value: 'grad3' }, + { label: '4', value: 'grad4' }, + ], + defaultValue: defaultValue, + }) +} diff --git a/docs/lib/keystatic-reader.ts b/docs/keystatic/reader.ts similarity index 100% rename from docs/lib/keystatic-reader.ts rename to docs/keystatic/reader.ts diff --git a/docs/markdoc/headings.ts b/docs/markdoc/headings.ts new file mode 100644 index 00000000000..273bfc2bdd6 --- /dev/null +++ b/docs/markdoc/headings.ts @@ -0,0 +1,38 @@ + + + +import type { RenderableTreeNode, Tag } from '@markdoc/markdoc' +import { isTag } from './isTag' + +export type HeadingType = { + id: string + depth: number + label: string +} + +export function extractHeadings (content: Tag): HeadingType[] { + const headings: HeadingType[] = [] + for (const child of content.children) { + if (isTag(child) && child.name === 'Heading') { + headings.push({ + id: child.attributes.id, + depth: child.attributes.level, + label: stringifyDocContent(child), + }) + } + } + return headings +} + +function stringifyDocContent (node: RenderableTreeNode): string { + if (typeof node === 'string') { + return node + } + if (Array.isArray(node)) { + return node.map(stringifyDocContent).join('') + } + if (!isTag(node)) { + return '' + } + return node.children.map(stringifyDocContent).join('') +} diff --git a/docs/markdoc/index.ts b/docs/markdoc/index.ts index 476c2efb2d2..feb995b79e5 100644 --- a/docs/markdoc/index.ts +++ b/docs/markdoc/index.ts @@ -42,13 +42,13 @@ export type BlogContent = BlogFrontmatter & { } export async function readBlogContent (filepath: string): Promise { - let content = await fs.readFile(filepath, 'utf8') + const content = await fs.readFile(filepath, 'utf8') const frontmatter = extractBlogFrontmatter(content) return { content: transformContent(`docs/${filepath}`, content), ...frontmatter } } export async function readDocsContent (filepath: string): Promise { - let content = await fs.readFile(filepath, 'utf8') + const content = await fs.readFile(filepath, 'utf8') const frontmatter = extractDocsFrontmatter(content) return { content: transformContent(`docs/${filepath}`, content), ...frontmatter } } @@ -96,7 +96,7 @@ export function extractDocsFrontmatter (content: string): { `Expected frontmatter yaml to be an object but found:\n${JSON.stringify(parsed)}` ) } - let obj = parsed as Record + const obj = parsed as Record if (typeof obj.title !== 'string') { throw new Error(`Expected frontmatter to contain a title`) } @@ -138,7 +138,7 @@ export function extractBlogFrontmatter (content: string): BlogFrontmatter { `Expected frontmatter yaml to be an object but found:\n${JSON.stringify(parsed)}` ) } - let obj = parsed as Record + const obj = parsed as Record if (typeof obj.title !== 'string') { throw new Error(`Expected frontmatter to contain title`) } diff --git a/docs/next-env.d.ts b/docs/next-env.d.ts index fd36f9494e2..40c3d68096c 100644 --- a/docs/next-env.d.ts +++ b/docs/next-env.d.ts @@ -1,6 +1,5 @@ /// /// -/// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/docs/package.json b/docs/package.json index d726501b9de..465e7aee0a4 100644 --- a/docs/package.json +++ b/docs/package.json @@ -16,11 +16,11 @@ "dependencies": { "@babel/runtime": "^7.24.7", "@codemod/core": "^2.0.1", - "@emotion/cache": "11.11.0", + "@emotion/cache": "11.14.0", "@emotion/css": "^11.7.1", "@emotion/react": "^11.7.1", "@emotion/server": "11.11.0", - "@emotion/weak-memoize": "^0.3.0", + "@emotion/weak-memoize": "^0.4.0", "@keystar/ui": "^0.7.6", "@keystatic/core": "^0.5.24", "@keystatic/next": "^5.0.1", @@ -33,32 +33,32 @@ "@vercel/og": "^0.6.0", "classnames": "^2.3.1", "clipboard-copy": "^4.0.1", - "date-fns": "^3.0.0", + "date-fns": "^4.0.0", "dedent": "^1.0.0", "facepaint": "^1.2.1", "globby": "^14.0.0", "js-yaml": "^4.1.0", "lodash.debounce": "^4.0.8", - "next": "catalog:", + "next": "^15.0.0", "next-compose-plugins": "^2.2.1", "prism-react-renderer": "^2.0.0", - "react": "catalog:", - "react-dom": "catalog:", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-focus-lock": "^2.7.1", "rss": "^1.2.2", - "tsx": "catalog:" + "tsx": "^4.0.0" }, "devDependencies": { "@types/babel__core": "7.20.5", "@types/facepaint": "^1.2.2", "@types/gtag.js": "^0.0.20", "@types/js-yaml": "^4.0.5", - "@types/react": "catalog:", - "@types/react-dom": "catalog:", "@types/lodash.debounce": "^4.0.6", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", "@types/rss": "^0.0.32", "next-sitemap": "^4.0.0", - "typescript": "catalog:" + "typescript": "^5.5.0" }, "repository": "https://github.com/keystonejs/keystone/tree/main/docs" } diff --git a/docs/pages/_app.tsx b/docs/pages/_app.tsx deleted file mode 100644 index 306643d3c92..00000000000 --- a/docs/pages/_app.tsx +++ /dev/null @@ -1,108 +0,0 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx, Global, css, CacheProvider } from '@emotion/react' -import type { AppProps } from 'next/app' -import { cache } from '@emotion/css' - -import Head from 'next/head' -import { proseStyles } from '../lib/prose-lite' -import { Theme } from '../components/Theme' -import { NavContextProvider } from '../components/docs/Navigation' -import { SkipLinks } from '../components/SkipLinks' - -export default function App ({ Component, pageProps }: AppProps) { - return ( - - - - - - - - - - - - - ) -} diff --git a/docs/pages/_document.tsx b/docs/pages/_document.tsx deleted file mode 100644 index c2117349b24..00000000000 --- a/docs/pages/_document.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import Document, { Html, Head, Main, NextScript, type DocumentContext } from 'next/document' -import React from 'react' -import createEmotionServer from '@emotion/server/create-instance' -import { cache } from '@emotion/css' - -const { extractCriticalToChunks } = createEmotionServer(cache) - -class MyDocument extends Document { - static async getInitialProps (ctx: DocumentContext) { - const initialProps = await Document.getInitialProps(ctx) - const data = extractCriticalToChunks(initialProps.html) - - return { - ...initialProps, - styles: [ - - {initialProps.styles} - {data!.styles.map((data, i) => ( - - ))} - , - ], - } - } - - render () { - return ( - - - - - - - - - - - - - - - - - - - - - {/* - The page is server rendered and hydrated on the browser client. - While server rendering we do not know what the user's preferred/saved theme is and default to light theme. - So we run this script in the browser before the page is rendered (not the react render but the browser render) - and set the theme class in html element so our styles would know which theme to paint on first paint. - All this to avoid a flash of default light theme when user either prefers dark theme or has previously - saved dark theme to their local storage. ¯\_(ツ)_/¯ React is hard sometimes. - */} - - - - - - - - ) - } -} - -export default MyDocument diff --git a/docs/pages/blog/[post].tsx b/docs/pages/blog/[post].tsx deleted file mode 100644 index e5001be227a..00000000000 --- a/docs/pages/blog/[post].tsx +++ /dev/null @@ -1,110 +0,0 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' -import { transform } from '@markdoc/markdoc' -import { - type GetStaticPathsResult, - type GetStaticPropsContext, - type GetStaticPropsResult, - type InferGetStaticPropsType, -} from 'next' -import Link from 'next/link' -import { useRouter } from 'next/router' -import { parse, format } from 'date-fns' -import { type BlogContent } from '../../markdoc' -import { extractHeadings, Markdoc } from '../../components/Markdoc' -import { BlogPage } from '../../components/Page' -import { Heading } from '../../components/docs/Heading' -import { Type } from '../../components/primitives/Type' -import { getOgAbsoluteUrl } from '../../lib/og-util' -import { reader } from '../../lib/keystatic-reader' -import { baseMarkdocConfig } from '../../markdoc/config' - -export default function Page (props: InferGetStaticPropsType) { - const router = useRouter() - const headings = [ - { id: 'title', depth: 1, label: props.title }, - ...extractHeadings(props.content), - ] - - const publishedDate = props.publishDate - const parsedDate = parse(publishedDate, 'yyyy-M-d', new Date()) - const formattedDateStr = format(parsedDate, 'MMMM do, yyyy') - - let ogImageUrl = props.metaImageUrl - if (!ogImageUrl) { - ogImageUrl = getOgAbsoluteUrl({ - title: props.title, - type: 'Blog', - }) - } - - return ( - - - {props.title} - - - - Published on {formattedDateStr} - {props.authorHandle ? ( - - {' '} - by{' '} - - {props.authorName} - - - ) : ( - by {props.authorName} - )} - - - {props.content.children.map((child, i) => ( - - ))} - - ) -} - -export async function getStaticPaths (): Promise { - const posts = await reader.collections.posts.list() - return { - paths: posts.map(post => ({ params: { post } })), - fallback: false, - } -} - -type KeystaticPostsContent = Omit & { - authorHandle: string | null - metaImageUrl: string | null -} - -export async function getStaticProps ( - args: GetStaticPropsContext<{ post: string }> -): Promise> { - const keystaticPost = await reader.collections.posts.read(args.params!.post, { - resolveLinkedFiles: true, - }) - - if (!keystaticPost) throw new Error(`Post not found: ${args.params!.post}`) - - const transformedContent = transform(keystaticPost.content.node, baseMarkdocConfig) - - return { props: { ...keystaticPost, content: JSON.parse(JSON.stringify(transformedContent)) } } -} diff --git a/docs/pages/docs/[...rest].tsx b/docs/pages/docs/[...rest].tsx deleted file mode 100644 index 63733032634..00000000000 --- a/docs/pages/docs/[...rest].tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react' -import { - type GetStaticPathsResult, - type GetStaticPropsContext, - type GetStaticPropsResult, - type InferGetStaticPropsType, -} from 'next' -import { useRouter } from 'next/router' -import { type DocsContent } from '../../markdoc' -import { extractHeadings, Markdoc } from '../../components/Markdoc' -import { DocsPage } from '../../components/Page' -import { Heading } from '../../components/docs/Heading' -import { reader } from '../../lib/keystatic-reader' -import { transform } from '@markdoc/markdoc' -import { baseMarkdocConfig } from '../../markdoc/config' - -export default function DocPage (props: InferGetStaticPropsType) { - const router = useRouter() - const headings = [ - { id: 'title', depth: 1, label: props.title }, - ...extractHeadings(props.content), - ] - return ( - - - {props.title} - - {props.content.children.map((child, i) => ( - - ))} - - ) -} - -export async function getStaticPaths (): Promise { - const pages = await reader.collections.docs.list() - return { - paths: pages.map(page => ({ params: { rest: page.split('/') } })), - fallback: false, - } -} - -export async function getStaticProps ( - args: GetStaticPropsContext<{ rest: string[] }> -): Promise> { - const doc = await reader.collections.docs.read(args.params!.rest.join('/'), { - resolveLinkedFiles: true, - }) - - if (!doc) throw new Error(`Doc page not found: ${args.params!.rest.join('/')}`) - - const transformedContent = transform(doc.content.node, baseMarkdocConfig) - - return { props: { ...doc, content: JSON.parse(JSON.stringify(transformedContent)) } } -} diff --git a/docs/pages/docs/examples.tsx b/docs/pages/docs/examples.tsx deleted file mode 100644 index e4b551c3f79..00000000000 --- a/docs/pages/docs/examples.tsx +++ /dev/null @@ -1,316 +0,0 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' - -import { GitHubExamplesCTA } from '../../components/docs/GitHubExamplesCTA' -import { Type } from '../../components/primitives/Type' -import { DocsPage } from '../../components/Page' -import { Well } from '../../components/primitives/Well' -import { useMediaQuery } from '../../lib/media' -import { InlineCode } from '../../components/primitives/Code' - -export default function Docs () { - const mq = useMediaQuery() - - return ( - - - Examples - - - - A growing collection of projects you can run locally to learn more about Keystone’s - capabilities. Each example includes documentation explaining the how and why. Use them as a - reference for best practice, and a jumping off point when adding features to your own - Keystone project. - - - - - - Standalone Examples - - - - Standalone examples demonstrate how a particular feature works or how to solve a problem - with Keystone. - - - - - A basic Blog schema with Posts and Authors. Use this as a starting place for learning how - to use Keystone. - - - A basic Task Management app, with Tasks and People who can be assigned to tasks. Great for - learning how to use Keystone. - - - - Adds password-based authentication to the Task Manager starter project. - - - Adds a custom logo component in the Admin UI. - - - Adds a custom Navigation component to the Admin UI. - - - Adds a custom page in the Admin UI. - - - Adds a custom field type based on the integer field type which - lets users rate items on a 5-star scale. - - - Adds a custom Admin UI view to a json field which provides a - customised editing experience for users. - - - Demonstrates how to use default values for fields. - - - Illustrates how to configure document fields in your Keystone - system and render their data in a frontend application. - - - Shows you how to extend the Keystone GraphQL API with custom queries and mutations. - - - Demonstrates how to extend the GraphQL API provided by Keystone with custom queries and - mutations using graphql-ts. - - - Shows you how to use Nexus to extend the GraphQL API provided by Keystone - with custom queries and mutations. - - - - Illustrates how to use the json field type. Builds on the Task - Manager starter project. - - - - Demonstrates how to create REST endpoints by extending Keystone's express app and using - the Query API to execute queries against the schema. - - - Shows you how to write tests against the GraphQL API to your Keystone system. - - - Implements virtual fields in a Keystone list. - - - Demonstrates how to use singleton lists with Keystone. - - - - - End-to-End Examples - - - - End to end examples demonstrate how a feature works or how to solve a problem with an - independent frontend application and a Keystone server. - - - - - Example to demonstrate customisation of Keystone's document field and document renderer. - - - - Shows you how to use Keystone as a data engine within Next.js applications. - - - - - Deployment Examples - - - - Examples with all the code and documentation you need to get a Keystone project hosted on - the web. You can find them in the{' '} - Keystone Github Organisation. - - - - - Based on the with-auth project, this example deploys a simple app - backend to Heroku. Includes a one-click deployment for Heroku account - holders. - - - Deploys a Keystone app backend to Microsoft Azure. Based on the{' '} - with-auth project. One-click deployment{' '} - included. - - - Deploys a Keystone app backend to Railway. Based on the with-auth{' '} - project. One-click deployment included. - - - - ) -} diff --git a/docs/pages/docs/index.tsx b/docs/pages/docs/index.tsx deleted file mode 100644 index 880a7280f0e..00000000000 --- a/docs/pages/docs/index.tsx +++ /dev/null @@ -1,344 +0,0 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' -import Link from 'next/link' - -import { CommunitySlackCTA } from '../../components/docs/CommunitySlackCTA' -import { Keystone5DocsCTA } from '../../components/docs/Keystone5DocsCTA' -import { Examples } from '../../components/docs/ExamplesList' -import { Walkthroughs } from '../../components/docs/WalkthroughsList' -import { Type } from '../../components/primitives/Type' -import { Well } from '../../components/primitives/Well' -import { DocsPage } from '../../components/Page' -import { useMediaQuery } from '../../lib/media' -import { CommunityCta } from '../../components/content/CommunityCta' -import { Content } from '../../components/icons/Content' -import { Code } from '../../components/icons/Code' -import { Bulb } from '../../components/icons/Bulb' -import { Video } from '../../components/icons/Video' -import { Organization } from '../../components/icons/Organization' -import { Alert } from '../../components/primitives/Alert' -import { Button } from '../../components/primitives/Button' -import { ArrowR } from '../../components/icons' - -export default function Docs () { - const mq = useMediaQuery() - - return ( - - - Developer Docs - - - - - - - Looking for enterprise-grade consulting & support? - - - Learn more - - - - - The Keystone Experience - - - - Discover the vision behind Keystone and what it's like to work with. If you’ve just heard - of Keystone, start here first: - - a': { - borderRadius: '1rem', - boxShadow: '0 0 5px var(--shadow)', - padding: '1.5rem', - color: 'var(--app-bg)', - transition: 'box-shadow 0.2s ease, transform 0.2s ease, padding 0.2s ease', - textDecoration: 'none !important', - '&:hover, &:focus': { - boxShadow: '0 7px 21px var(--shadow)', - transform: 'translateY(-4px)', - }, - '& svg': { - height: '2rem', - }, - }, - })} - > - - - - Video Intro → - - - Learn how Keystone’s leading a new generation of content management tools. - - - - - - Why Keystone → - - - The makers. The vision. What’s in the box, and what you can build with it. - - - - - a': { - borderRadius: '1rem', - boxShadow: '0 0 5px var(--shadow)', - padding: '1.5rem', - color: 'var(--app-bg)', - transition: 'box-shadow 0.2s ease, transform 0.2s ease, padding 0.2s ease', - textDecoration: 'none !important', - '&:hover, &:focus': { - boxShadow: '0 7px 21px var(--shadow)', - transform: 'translateY(-4px)', - }, - '& svg': { - height: '2rem', - }, - }, - })} - > - - - - For Developers → - - - Built the way you’d want it made. Keystone fits with the tools you know and love. - - - - - - For Editors → - - - The configurable editing environment you need to do your best work. - - - - - - For Organisations → - - - Own your data. Start fast. Find your audience anywhere. Scale on your terms. - - - - - - Walkthroughs - - - - Step-by-step instructions for getting things done with Keystone. - - - - - - Guides - - - - Practical explanations of Keystone’s fundamental building blocks. When you’re trying to get - something done, Keystone guides show you how to think about, and get the most out of each - feature. - - - - - Keystone’s CLI helps you develop, build, and deploy projects. This guide explains all you - need to standup a new backend in the terminal. - - - Learn how to reason about and configure relationships in Keystone, so you can bring value - to your project through structured content. - - - Query filters are an integral part of Keystone’s powerful GraphQL APIs. This guide will - show you how to use filters to get the data you need from your system. - - - Learn how to use Hooks within your schema to extend Keystone’s powerful CRUD GraphQL APIs - with your own business logic. - - - Keystone’s document field is a highly customisable rich text editor that stores content as - structured JSON. Learn how to configure it and incorporate your own custom React - components. - - - Test drive the many features of Keystone’s Document field on this website. - - - Learn how to define your own custom field types in Keystone, with customisable backend - data structure, and Admin UI appearance. - - - Learn how to test the behaviour of your Keystone system to ensure it does what you expect. - - - Virtual fields offer a powerful way to extend your GraphQL API. This guide introduces the - syntax and shows you how start simply and end up with a complex result. - - - Keystone supports Postgres, MySQL and SQLite. This guide explains how to choose the best - for your project. - - - Learn how to store and manage Images and Files in Keystone. - - - - - Example projects - - - - A growing collection of projects you can run locally to learn more about Keystone features. - Use these as a reference for best practice, and a jumping off point when adding features to - your own Keystone project.{' '} - - View on Github → - - - - - - - - ) -} diff --git a/docs/pages/docs/walkthroughs/index.tsx b/docs/pages/docs/walkthroughs/index.tsx deleted file mode 100644 index 2312bb6efc8..00000000000 --- a/docs/pages/docs/walkthroughs/index.tsx +++ /dev/null @@ -1,115 +0,0 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { jsx } from '@emotion/react' - -import { Type } from '../../../components/primitives/Type' -import { DocsPage } from '../../../components/Page' -import { Well } from '../../../components/primitives/Well' -import { useMediaQuery } from '../../../lib/media' -import { InlineCode } from '../../../components/primitives/Code' - -export function QuickStart () { - const mq = useMediaQuery() - return ( - - - - Take a tour of Keystone in minutes with our CLI starter project - - - - ) -} - -export function Foundations () { - const mq = useMediaQuery() - return ( - - - - Get Keystone up and running with your first content type - - - Connect two content types and learn how to configure the appearance of field inputs - - - Support publishing needs with Keystone's select and{' '} - timestamp fields - - - Add sessions, password protection, and a sign-in screen to your Keystone app - - - Add a powerful document field to your app and learn how to - configure it to meet your needs - - - - ) -} - -export default function Docs () { - return ( - - - Walkthroughs - - - - Step-by-step tutorials for building with Keystone. - - - - Getting started - - - - If you’re new to Keystone begin here. These walkthroughs introduce the system, key concepts, - and show you how to get up and running with schema-driven development the Keystone way. - - - - - - Learn Keystone - - - - Learn how to build a functioning blog backend with relationships, auth, and session data - from an empty folder, and gain insights into Keystone’s core concepts along the way. - - - - - ) -} diff --git a/docs/pages/updates/index.tsx b/docs/pages/updates/index.tsx deleted file mode 100644 index 50a3ff8e55d..00000000000 --- a/docs/pages/updates/index.tsx +++ /dev/null @@ -1,918 +0,0 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { type HTMLAttributes, type ReactNode } from 'react' -import { jsx } from '@emotion/react' -import Link from 'next/link' - -import { InlineCode } from '../../components/primitives/Code' -import { Button } from '../../components/primitives/Button' -import { Alert } from '../../components/primitives/Alert' -import { Type } from '../../components/primitives/Type' -import { DocsPage } from '../../components/Page' -import { ArrowR } from '../../components/icons/ArrowR' -import { Emoji } from '../../components/primitives/Emoji' -import { useMediaQuery } from '../../lib/media' - -type TimelineProps = { - date: string - isLatest?: boolean - isFirst?: boolean -} & HTMLAttributes - -function Timeline ({ date, isLatest, isFirst, ...props }: TimelineProps) { - return ( - - - {isLatest && ( - - )} - - - - {date} - - - ) -} - -type BoxProps = { - link?: string - heading?: ReactNode - children: ReactNode -} & HTMLAttributes - -function Box ({ link, heading, children, ...props }: BoxProps) { - return ( - - {heading && ( - - {heading} - - )} - - {children} - - {link && ( - - Read more - - )} - - ) -} - -export default function WhatsNew () { - const mq = useMediaQuery() - - return ( - - - Latest News - - - - A snapshot of Keystone improvements and community happenings. - - For developer changelogs see our{' '} - GitHub Releases. - - - - - What are we working on next?{' '} - - See our{' '} - - Roadmap - - - - - - - - - Now you can provide additional context to the reader of Admin UI form fields. Use them to - provide a better editing experience and improve the quality of what goes in to your - system. - - - - - We’ve added support for MySQL to Keystone's list of DB providers, bringing the total - number of supported DB types to three. - - - - It’s now much easier to get Images and Files working with Keystone’s new{' '} - storage configuration object.{' '} - - - - Document field components now accept re-orderable arrays. Drag and drop your way to - carousels, galleries, recipe steps and more. Look for the Carousel component in our{' '} - Document Field demo. - - - - You can now experiment with Keystone{' '} - example projects{' '} - in a browser using the free codesandbox.io service. - Try the{' '} - - blog example - {' '} - today. Thanks @murznn for making it happen!{' '} - - - - - Aimed at Keystone beginners, our{' '} - new learning series shows you how to - turn an empty folder into a functioning blog backend with relationships, auth, and session - data. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Keystone 6 is now in General Availability! Today's Keystone is faster and more flexible - than it's ever been, and is ready for you to build amazing things with{' '} - {' '} - Read the full story here. - - - - Keystone now uses Prisma’s Node-API Query Engine. Query times are now much faster, - especially for large data sets. - - - - We now have a brand portal full of logos, monograms and - assets. Perfect for your blog posts, plugins and more. - - - - - This example - {' '} - uses{' '} - - Nexus - {' '} - — a declarative, code-first and strongly typed GraphQL schema construction for TypeScript - & JavaScript —to extend the GraphQL API provided by Keystone with custom queries and - mutations. - - - - The image and files configuration options have been removed from Keystone's configuration, - and a new storage configuration object introduced. timestamp,{' '} - float and decimal fields with{' '} - isIndexed: 'unique' now have unique filters via - ListWhereUniqueInput. - - - - You can now use different table and column names for your list and field keys. Powered by - Prisma's @map and @@map attributes: this - is useful if you don’t want to modify your existing database (such as a read-only - database) and use it with Keystone. - - - - With keystone-next dev you can now update your GraphQL schema, - change hooks and access control, log errors to see how your data returns, then immediately - use the playground to test it and iterate. This is on top of to the current support for - live reloading changes to custom views in the Admin UI. - - - - - This example - {' '} - shows you how to deploy Keystone to Azure PaaS using AppService, Storage and Postgres. - Thanks to community member Aaron Powell for the awesome contribution{' '} - - - - - - This example - {' '} - shows you how to create REST endpoints by extending Keystone’s express app so you can use - the Query API to execute queries against the schema. - - {/* - - - - - - - - - - - - - - - - - - Taking the chance to introduce the core team and address some community questions in - person, the team held an online Q&A event and launched a YouTube channel at the same time.{' '} - - Watch it online here - - . - */} - - - Our CLI app now uses SQLite under the hood so you don’t have to - spend time on DB config when trying out new ideas. We also updated the{' '} - getting started walkthrough to reflect this - improvement. - - - - Learn how to get your Keystone project on the web with our new one-click starters for{' '} - - Heroku - {' '} - and{' '} - - Railway - - . - - - - Jed spoke at Prisma Meetup Korea, covering V6 general availability, user-facing - management, UI authentication, access control, business logic integrations and more.{' '} - - Watch it online here - - . - - - - Major release #2 of #3 planned ahead of Keystone 6 General Availability includes: - - - A better Access Control API - - Customisable Express + GraphQL API paths - Apollo Server introspection - Omit GraphQL operations - Faster startups in local dev - Keystone has been updated to Next.js v11 - - - - - Access Control is now easier to program, and makes it harder to introduce security gaps in - your system. - - - The static approach to access control has been replaced. Now access - control never effects the operations in your GraphQL API. - - - Keystone used to return an access denied error from a{' '} - query if an item couldn't be found, or explicitly had access denied. - The improved API never returns that error type on a query. - - - Access rules are now more explicit, and support fewer variations so - you're less likely to introduce security gaps. - - - To securely upgrade your system, follow the instructions in our{' '} - Access Control upgrade guide. - - - - A long awaited feature, the Express App that Keystone creates is now{' '} - customisable with the new{' '} - extendExpressApp option: - - Add your own custom server routes - Host two apps on separate ports - And more... - - - - - The GraphQL endpoint accessible by default at `/api/graphql` can now be customised with - the new option config.graphql.path. You can find this and all - other options in our GraphQL API docs. - - - - A major milestone in the path to a General Availability status - for Keystone 6, we've just released a new and improved GraphQL API.{' '} - - - - We’ve made the experience of working with Keystone’s GraphQL API easier to program and - reason about. Be sure to check our{' '} - GraphQL API docs. - - - - We're opening Admin UI up to support a more personal content experience. Now you can: - - - embed your own custom logo, - - - add custom pages with Admin UI - routes, and - - - link to them (and elsewhere) with{' '} - custom navigation. - - - To deliver a more productive editor experience that's aligned with the needs and brand of - your organisation. - - - - We've added an optional /_healthcheck endpoint to Keystone's - express server. Use it to ensure your Keystone instance is up and running with website - monitoring solutions. - - - - - - - - Follow along in with the repo - {' '} - as Jed builds a front and back-end for a Blog app with Prisma, KeystoneJS, GraphQL, - Next.js and Tailwind, that gives you: - - - Public auth and signup - - - Role-based access control - - - Custom components in the{' '} - document field - - - Editors can embed audience Polls in post content, and authenticated visitors can make - their vote count in the frontend. - - - - Learn how to create a{' '} - - custom field view - {' '} - for a JSON field that lets users users add, edit and remove - navigation items from a list. - - - - Jed's talk at Prisma Day 2021 is a great overview into what makes Keystone special. Watch - below, or read the full transcript. - - - - - - - We've launched our new website for Keystone 6! There’s a new home page, - and background on why Keystone is built for projects - that need to scale on their own terms. Navigating the docs is easier with breadcrumbs, - index pages for Walkthroughs,{' '} - Guides, and APIs - , and a better mobile experience. We hope you like it - - - - In our contuing efforts to improve the developer documentation for Keystone 6, we’ve - written the following guides: - - - Virtual fields - - - Relationships - - - Hooks - - - Query Filters - - - Testing - - - - - - After months of work deep in the codebase, Keystone 6 now has a new core. This unblocks a - bunch of roadmap features like custom field types, GraphQL Schema extensions, and more. - The new core does bring some minor behavioural changes to Keystone’s APIs. See the release - notes for more information. - - - - We’ve made accessibility updates to DatePicker labels,{' '} - relationship fields, as well as visual improvements to segment - control (when no value is selected), and more. - - - - A long awaited feature: you can now find an item by unique fields in your schema. It works - for{' '} - - text - {' '} - and{' '} - - integer - {' '} - fields that have isUnique: true set. - - - - You can now use JSON blobs in your backend, and provide your own React UI components to - edit them. Try it out in this{' '} - - example project - - . It accepts any valid JSON node including: - - - string - - - number - - - array - - - object - - - - - - We now have a{' '} - - collection of example projects - {' '} - you can run locally to learn more about a particular feature of Keystone. Each project - comes with explainers on the how and why. Use them as a reference for best practice, and - as a jumping off point when adding features to your own Keystone project. - - - - We’ve pruned a lot of code to make way for a more efficient and productive core in - Keystone 6. Changes include: - - - Removed DB adapters and many redundant methods and arguments (now that Keystone 6 uses - Prisma under the hood). - - - Exchanged deploy, reset and{' '} - generate commands for{' '} - keystone-next prisma commands. - - - - - - Keystone 5 is now in maintenance mode while we focus all our efforts on building Keystone - 6. If you’re wondering which version to start your next project with, this guide is for - you. - - - - - Need answers to Keystone questions? Get help in our - - - Community Slack - - - - ) -} diff --git a/docs/pages/updates/roadmap.tsx b/docs/pages/updates/roadmap.tsx deleted file mode 100644 index 12afb2ffeb5..00000000000 --- a/docs/pages/updates/roadmap.tsx +++ /dev/null @@ -1,490 +0,0 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -import { type ComponentProps, Fragment, type ReactNode } from 'react' -import { jsx } from '@emotion/react' -import Link from 'next/link' - -import { InlineCode } from '../../components/primitives/Code' -import { Highlight } from '../../components/primitives/Highlight' -import { Gradient } from '../../components/primitives/Gradient' -import { Alert } from '../../components/primitives/Alert' -import { Emoji } from '../../components/primitives/Emoji' -import { Type } from '../../components/primitives/Type' -import { DocsPage } from '../../components/Page' -import { useMediaQuery } from '../../lib/media' - -function TimelineItem ({ children }: { children: ReactNode }) { - return ( - - {children} - - ) -} - -function TimelineMarker ({ look }: Pick, 'look'>) { - return ( - - - - - ) -} - -function TimelineWeAreHere () { - const arrowSize = '0.4rem' - const mq = useMediaQuery() - return ( - - - We are here! - - ) -} - -type TimelineContentProps = { - title: ReactNode - children: ReactNode -} & Pick, 'look'> -function TimelineContent ({ title, look, children }: TimelineContentProps) { - return ( - - - {title} - - - {children} - - - ) -} - -type RoadmapListProps = { - children: ReactNode -} - -function RoadmapList ({ children }: RoadmapListProps) { - const mq = useMediaQuery() - return ( - - {children} - - ) -} - -const roadmapItemSectionStyles = { - margin: '0rem 0 .75rem', - borderRadius: '.4rem', - display: 'inline-block', - padding: '0.1825rem 0.5rem', - fontSize: '0.9rem', - fontWeight: 700, -} -const roadmapItemSection = { - docs: () => ( - Docs - ), - 'fields and schema': () => ( - - Fields & Schema - - ), - core: () => ( - Core - ), - 'admin ui': () => ( - - Admin UI - - ), -} - -type RoadmapItemProps = { - title: ReactNode - section?: keyof typeof roadmapItemSection - children: ReactNode -} -function RoadmapItem ({ title, section, children }: RoadmapItemProps) { - const Section = section ? roadmapItemSection[section] : null - return ( - - {Section && } - - {title} - - - {children} - - - ) -} - -function Divider () { - return ( - - ) -} - -export default function Roadmap () { - const mq = useMediaQuery() - - return ( - - - Roadmap - - - After a year of intensive development Keystone 6 has achieved a{' '} - General Availability release! We've graduated - to the - @keystone-6 namespace on npm and have a stable set of APIs that you - can confidently build on - - - - - - Surveyed the landscape. Formed concepts around what a next-gen experience would look - like. Started laying the foundations. - - - - - - Published the New Interfaces as @keystone-next - on npm. Commenced iteration based on community & internal feedback. - - - - - - - Stabilised the new architecture & APIs. Docs & example projects. Published as{' '} - @keystone-6 on npm. - - - - - - A better dev-configured editing experience. Maturity & features in Keystone core. More - pathways to grow with Keystone. - - - - - What's Next - - - We're using the foundations we shipped in 2021 as a springboard to bring{' '} - developers,{' '} - project owners, and{' '} - editors into more productive ways of working - together. Our key areas of focus for 2022 are: - - - - - A next-gen Admin UI that unifies developer and editor collaborations in - new and exciting ways - - - Maturing the DX with better Types and capabilities for self-hosting and - media management - - - Enabling community with more pathways for you to learn and grow with - Keystone - - - - - Next-gen Admin UI - - - Our design team spent much of the second half of 2021 defining a new vision for Admin UI - that gives you more capabilities to support content editors in ways that matter most to - them. We've already shipped quick wins for customisable{' '} - logos,{' '} - pages, and{' '} - navigation, but the really - transformative features (that rely on more extensive customisation) are still in the works. - This body of work will elevate the experience of authoring content in Keystone to the same - high standards we have for authoring with Keystone's core APIs. - - - Maturing the Developer Experience - - - We'll continue to iterate on making Keystone the easiest way to design and standup a GraphQL - API on the web. Going all-in on Typescript has made the DX so sweet, but we want to take it{' '} - further so that Keystone's the best Typescript > GraphQL developer experience in - the ecosystem. - - - We'll also focus on making better pathways for you to integrate Keystone with the deployment - services you use most, so you can get the most out of where the modern web is going. Image - and file management is an area we're actively iterating on, and we'll have more to share - soon. - - - Enabling the community - - - At{' '} - - Thinkmill - - , we have a longstanding commitment to open source that includes the likes of{' '} - - react select - - ,{' '} - - classnames - - , and of course Keystone . Now that Keystone 6 is stable - enough to develop features and integrations around, we'll put better processes in place for - you to collaborate around the Keystone project and showcase your awesome work with others in - the community. - - - Feature Roadmap - - - - To see what we've recently shipped, checkout our updates and{' '} - release notes - - - - Current focus - - - - - A way to define a single object in schema that's editable in Admin UI and accessible in - the GraphQL API. Handy for storing website & social settings, API keys, and more. - - - - - Dynamically showing fields based on the value of other fields is a great way to improve - editing flow and content integrity. - - - - - It's often easier to work with content when the form is grouped into different sections - of related fields. - - - - - - Next up - - - - - Sometimes you need to manage data in structures that are nested and/or repeating. We're - working on a way to define these in schema and have them stored as JSON field in the - database. - - - - - Access your GraphQL APIs from Node.js for greater flexibility when writing apps and - hybrid use-cases. - - - - - Certain list types come with a need to order the items they contain. We're looking in to - an approach that's easy to implement in schema. - - - - - We're broadening our list of streamlined scenarios & looking into options for serverless - environments. - - - - - - Further afield - - - - - When an English-language UI doesn't work for your team there'll be a way for you to add - translations to all the strings in Admin UI. - - - - - An editing interface that's available for you to use no matter what device you're on. - - - - - Solving accessibility in a customisable editing interface is a hard problem. We're up - for the challenge. - - - - - Built-in tooling for you to give editors a sense of how their content will be consumed - by end users. - - - - - Design your own journey from draft to{' '} - published to meet the unique needs of your content team. - - - - - More powerful interfaces for managing different scales of related data, from small to - really really large - - - - - Long lasting server operations that can change their result over time. Handy for - updating your front-end in real time when important data changes in your backend. - - - - - A built-in schema-driven approach to supporting multilingual content projects. - - - - - If you want to update an item but aren't sure if it exists, this will update the item if - it's there or create a new item with the data you've provided. - - - - - The future of deployment is serverless, and we're tracking the state of the ecosystem to - make sure Keystone is ready for it. - - - - - A way for you and editors to easily search for strings across your entire dataset. Handy - for when you need something specific but don't know where it lives. - - - - - - Got a feature you're after, or want to know more about the future? - - - Join the Keystone conversation on{' '} - - Slack - - ,{' '} - - GitHub - - , and{' '} - - Twitter - - . - - - - ) -} diff --git a/docs/public/assets/getting-started/cover.svg b/docs/public/assets/getting-started/cover.svg index 7117e72e990..70e48a67f00 100644 --- a/docs/public/assets/getting-started/cover.svg +++ b/docs/public/assets/getting-started/cover.svg @@ -13,8 +13,8 @@ λ - yarn create keystone-app - yarn create v1.22.10 + npm create keystone-app@latest + [1/4] 🔍 @@ -46,7 +46,7 @@ To launch your app, run: - cd my-app - - yarn dev + - npm run dev Next steps: diff --git a/docs/public/sitemap-0.xml b/docs/public/sitemap-0.xml index d775b6be305..318dc2a6eaf 100644 --- a/docs/public/sitemap-0.xml +++ b/docs/public/sitemap-0.xml @@ -1,83 +1,82 @@ -https://keystonejs.com2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/blog2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/branding2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/config/overview2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/examples2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/guides/document-field-demo2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/guides/overview2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/walkthroughs2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/enterprise2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/for-content-management2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/for-developers2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/for-organisations2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/updates2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/updates/roadmap2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/why-keystone2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/config/access-control2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/config/auth2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/config/config2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/config/hooks2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/config/lists2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/config/session2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/context/db-items2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/context/get-context2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/context/overview2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/context/query2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/fields/bigint2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/fields/calendarday2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/fields/checkbox2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/fields/cloudinaryimage2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/fields/decimal2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/fields/document2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/fields/file2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/fields/float2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/fields/image2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/fields/integer2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/fields/json2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/fields/multiselect2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/fields/overview2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/fields/password2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/fields/relationship2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/fields/select2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/fields/text2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/fields/timestamp2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/fields/virtual2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/getting-started2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/graphql/filters2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/graphql/overview2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/guides/auth-and-access-control2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/guides/choosing-a-database2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/guides/cli2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/guides/custom-admin-ui-logo2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/guides/custom-admin-ui-navigation2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/guides/custom-admin-ui-pages2024-07-08T03:24:41.483Zdaily0.7 -https://keystonejs.com/docs/guides/custom-field-views2024-07-08T03:24:41.484Zdaily0.7 -https://keystonejs.com/docs/guides/custom-fields2024-07-08T03:24:41.484Zdaily0.7 -https://keystonejs.com/docs/guides/database-migration2024-07-08T03:24:41.484Zdaily0.7 -https://keystonejs.com/docs/guides/document-fields2024-07-08T03:24:41.484Zdaily0.7 -https://keystonejs.com/docs/guides/filters2024-07-08T03:24:41.484Zdaily0.7 -https://keystonejs.com/docs/guides/hooks2024-07-08T03:24:41.484Zdaily0.7 -https://keystonejs.com/docs/guides/images-and-files2024-07-08T03:24:41.484Zdaily0.7 -https://keystonejs.com/docs/guides/relationships2024-07-08T03:24:41.484Zdaily0.7 -https://keystonejs.com/docs/guides/schema-extension2024-07-08T03:24:41.484Zdaily0.7 -https://keystonejs.com/docs/guides/testing2024-07-08T03:24:41.484Zdaily0.7 -https://keystonejs.com/docs/guides/virtual-fields2024-07-08T03:24:41.484Zdaily0.7 -https://keystonejs.com/docs/reference/telemetry2024-07-08T03:24:41.484Zdaily0.7 -https://keystonejs.com/docs/walkthroughs/lesson-12024-07-08T03:24:41.484Zdaily0.7 -https://keystonejs.com/docs/walkthroughs/lesson-22024-07-08T03:24:41.484Zdaily0.7 -https://keystonejs.com/docs/walkthroughs/lesson-32024-07-08T03:24:41.484Zdaily0.7 -https://keystonejs.com/docs/walkthroughs/lesson-42024-07-08T03:24:41.484Zdaily0.7 -https://keystonejs.com/docs/walkthroughs/lesson-52024-07-08T03:24:41.484Zdaily0.7 -https://keystonejs.com/blog/customisable-admin-ui2024-07-08T03:24:41.484Zdaily0.7 -https://keystonejs.com/blog/embedded-mode-with-sqlite-nextjs2024-07-08T03:24:41.484Zdaily0.7 -https://keystonejs.com/blog/general-availability2024-07-08T03:24:41.484Zdaily0.7 -https://keystonejs.com/blog/introducing-keystone-blog2024-07-08T03:24:41.484Zdaily0.7 -https://keystonejs.com/blog/keystone-tutorials2024-07-08T03:24:41.484Zdaily0.7 -https://keystonejs.com/blog/mysql-support2024-07-08T03:24:41.484Zdaily0.7 -https://keystonejs.com/blog/nextjs-keystone2024-07-08T03:24:41.484Zdaily0.7 -https://keystonejs.com/blog/prisma-day-20212024-07-08T03:24:41.484Zdaily0.7 -https://keystonejs.com/blog/singleton2024-07-08T03:24:41.484Zdaily0.7 +https://keystonejs.com/docs2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/examples2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/enterprise2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/walkthroughs2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/for-organisations2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/roadmap2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/why-keystone2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/config/overview2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/for-content-management2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/blog/customisable-admin-ui2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/blog/embedded-mode-with-sqlite-nextjs2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/blog/general-availability2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/blog/introducing-keystone-blog2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/blog/keystone-tutorials2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/blog/mysql-support2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/blog/nextjs-keystone2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/blog/prisma-day-20212024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/blog/singleton2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/for-developers2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/guides/document-field-demo2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/blog2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/guides/overview2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/branding2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/config/access-control2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/config/auth2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/config/config2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/config/hooks2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/config/lists2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/config/session2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/context/db-items2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/context/get-context2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/context/overview2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/context/query2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/fields/bigint2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/fields/calendarday2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/fields/checkbox2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/fields/cloudinaryimage2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/fields/decimal2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/fields/document2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/fields/file2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/fields/float2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/fields/image2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/fields/integer2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/fields/json2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/fields/multiselect2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/fields/overview2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/fields/password2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/fields/relationship2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/fields/select2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/fields/text2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/fields/timestamp2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/fields/virtual2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/getting-started2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/graphql/filters2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/graphql/overview2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/guides/auth-and-access-control2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/guides/choosing-a-database2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/guides/cli2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/guides/custom-admin-ui-logo2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/guides/custom-admin-ui-navigation2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/guides/custom-admin-ui-pages2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/guides/custom-field-views2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/guides/custom-fields2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/guides/database-migration2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/guides/document-fields2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/guides/filters2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/guides/hooks2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/guides/images-and-files2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/guides/relationships2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/guides/schema-extension2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/guides/testing2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/guides/virtual-fields2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/reference/telemetry2024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/walkthroughs/lesson-12024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/walkthroughs/lesson-22024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/walkthroughs/lesson-32024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/walkthroughs/lesson-42024-08-09T00:04:47.022Zdaily0.7 +https://keystonejs.com/docs/walkthroughs/lesson-52024-08-09T00:04:47.022Zdaily0.7 \ No newline at end of file diff --git a/docs/redirects.mjs b/docs/redirects.mjs index 21fc60fcace..73477e14dd3 100644 --- a/docs/redirects.mjs +++ b/docs/redirects.mjs @@ -47,7 +47,7 @@ const KEYSTONE_5 = [ // Old roadmap URL just redirects to the new roadmap { source: '/guides/road-map', - destination: '/updates/roadmap', + destination: '/roadmap', permanent: true, }, // Linked to from google results (2021-06-28) and possibly elsewhere? @@ -93,7 +93,8 @@ const ORIGINAL_NEXT = [ destination: 'https://github.com/keystonejs/keystone/releases', permanent: true, }, - { source: '/roadmap', destination: '/updates/roadmap', permanent: true }, + { source: '/updates', destination: '/blog', permanent: true }, + { source: '/updates/roadmap', destination: '/roadmap', permanent: true }, { source: '/whats-new', destination: '/updates/whats-new-in-v6', permanent: true }, ] @@ -232,6 +233,11 @@ const CURRENT = [ destination: '/docs/graphql/overview', permanent: false, }, + { + source: '/enterprise', + destination: 'https://www.thinkmill.com.au/services/keystone', + permanent: true, + }, /* Telemetry - used to shorten the URL for CLI message */ { source: '/telemetry', diff --git a/docs/tsconfig.json b/docs/tsconfig.json index a4c1f66a19b..8ce8e6d3e05 100644 --- a/docs/tsconfig.json +++ b/docs/tsconfig.json @@ -10,8 +10,8 @@ "esModuleInterop": true, "moduleResolution": "bundler", "resolveJsonModule": true, - "isolatedModules": true, "jsx": "preserve", + "isolatedModules": true, "plugins": [ { "name": "next" diff --git a/eslint.config.js b/eslint.config.mjs similarity index 71% rename from eslint.config.js rename to eslint.config.mjs index 0f4f6d17b92..7cfea9e6796 100644 --- a/eslint.config.js +++ b/eslint.config.mjs @@ -1,42 +1,39 @@ // @ts-check -const eslint = require('@eslint/js') -const tseslint = require('typescript-eslint') +import eslint from '@eslint/js' +import tseslint from 'typescript-eslint' +import stylisticTs from '@stylistic/eslint-plugin-ts' -module.exports = tseslint.config( +export default tseslint.config( { ignores: [ '**/.keystone/', + '**/.next/', '**/dist/', + '**/__generated__/', '**/node_modules/', '**/syntax-error.js', '**/public/', - 'examples/extend-graphql-schema-nexus/nexus-types.ts' + 'examples/', ], }, eslint.configs.recommended, ...tseslint.configs.strict, -// ...tseslint.configs.stylistic, { + plugins: { + '@stylistic/ts': stylisticTs, + }, rules: { // TODO: remove 'no-empty': 'off', - 'no-empty-pattern': ['error', { allowObjectPatternsAsParameters: true }], - 'no-extra-boolean-cast': 'off', - 'no-async-promise-executor': 'off', - '@typescript-eslint/no-non-null-asserted-optional-chain': 'off', 'prefer-const': 'off', - 'no-regex-spaces': 'off', - 'no-useless-escape': 'off', - '@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/no-unused-vars': 'off', '@typescript-eslint/ban-ts-comment': 'off', - '@typescript-eslint/ban-types': 'off', '@typescript-eslint/no-dynamic-delete': 'off', '@typescript-eslint/no-invalid-void-type': 'off', '@typescript-eslint/no-namespace': 'off', - 'import/no-unresolved': 'off', + '@typescript-eslint/no-require-imports': 'off', // TODO: always // TODO: remove semi: ['error', 'never'], @@ -50,7 +47,7 @@ module.exports = tseslint.config( 'space-before-blocks': ['error', 'always'], 'space-before-function-paren': ['error', 'always'], 'space-in-parens': ['error', 'never'], - '@typescript-eslint/member-delimiter-style': [ + '@stylistic/ts/member-delimiter-style': [ 'error', { multiline: { delimiter: 'none' }, @@ -64,7 +61,7 @@ module.exports = tseslint.config( }], '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/prefer-ts-expect-error': 'error', - '@typescript-eslint/semi': ['error', 'never'], + '@stylistic/ts/semi': ['error', 'never'], } } ) diff --git a/examples/assets-local/keystone.ts b/examples/assets-local/keystone.ts index 4a976bcc226..de4cfc66bac 100644 --- a/examples/assets-local/keystone.ts +++ b/examples/assets-local/keystone.ts @@ -1,6 +1,6 @@ import { config } from '@keystone-6/core' -import { fixPrismaPath } from '../example-utils' import { lists } from './schema' +import bytes from 'bytes' export default config({ db: { @@ -8,9 +8,12 @@ export default config({ url: process.env.DATABASE_URL || 'file:./keystone-example.db', // WARNING: this is only needed for our monorepo examples, dont do this - ...fixPrismaPath, + prismaClientPath: 'node_modules/myprisma', }, lists, + server: { + maxFileSize: bytes('40Mb') + }, storage: { my_images: { kind: 'local', diff --git a/examples/assets-local/package.json b/examples/assets-local/package.json index 425d98a6be2..ccb057e35c3 100644 --- a/examples/assets-local/package.json +++ b/examples/assets-local/package.json @@ -1,6 +1,6 @@ { "name": "@keystone-6/example-assets-local", - "version": "0.0.8", + "verison": null, "private": true, "license": "MIT", "scripts": { @@ -10,11 +10,13 @@ "postinstall": "keystone postinstall" }, "dependencies": { - "@keystone-6/core": "workspace:^", - "@prisma/client": "catalog:" + "@keystone-6/core": "^6.3.1", + "@prisma/client": "5.19.0", + "bytes": "^3.1.1" }, "devDependencies": { - "prisma": "catalog:", - "typescript": "catalog:" + "@types/bytes": "^3.1.1", + "prisma": "5.19.0", + "typescript": "^5.5.0" } } diff --git a/examples/assets-local/sandbox.config.json b/examples/assets-local/sandbox.config.json index 7a34682ee45..e53d3f9dadc 100644 --- a/examples/assets-local/sandbox.config.json +++ b/examples/assets-local/sandbox.config.json @@ -2,6 +2,6 @@ "template": "node", "container": { "startScript": "keystone dev", - "node": "16" + "node": "22" } } diff --git a/examples/assets-local/schema.graphql b/examples/assets-local/schema.graphql index aa2c66202d4..04eb959be33 100644 --- a/examples/assets-local/schema.graphql +++ b/examples/assets-local/schema.graphql @@ -139,8 +139,8 @@ type Mutation { } type Query { - posts(where: PostWhereInput! = {}, orderBy: [PostOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: PostWhereUniqueInput): [Post!] post(where: PostWhereUniqueInput!): Post + posts(where: PostWhereInput! = {}, orderBy: [PostOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: PostWhereUniqueInput): [Post!] postsCount(where: PostWhereInput! = {}): Int keystone: KeystoneMeta! } @@ -156,23 +156,25 @@ type KeystoneAdminMeta { type KeystoneAdminUIListMeta { key: String! - itemQueryName: String! - listQueryName: String! - hideCreate: Boolean! - hideDelete: Boolean! path: String! label: String! singular: String! plural: String! description: String - initialColumns: [String!]! pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! groups: [KeystoneAdminUIFieldGroupMeta!]! + graphql: KeystoneAdminUIGraphQL! + initialColumns: [String!]! + initialSearchFields: [String!]! initialSort: KeystoneAdminUISort - isHidden: Boolean! isSingleton: Boolean! + hideCreate: Boolean! + hideDelete: Boolean! + isHidden: Boolean! + itemQueryName: String! + listQueryName: String! } type KeystoneAdminUIFieldMeta { @@ -242,6 +244,33 @@ type KeystoneAdminUIFieldGroupMeta { fields: [KeystoneAdminUIFieldMeta!]! } +type KeystoneAdminUIGraphQL { + names: KeystoneAdminUIGraphQLNames! +} + +type KeystoneAdminUIGraphQLNames { + outputTypeName: String! + whereInputName: String! + whereUniqueInputName: String! + createInputName: String! + createMutationName: String! + createManyMutationName: String! + relateToOneForCreateInputName: String! + relateToManyForCreateInputName: String! + itemQueryName: String! + listOrderName: String! + listQueryCountName: String! + listQueryName: String! + updateInputName: String! + updateMutationName: String! + updateManyInputName: String! + updateManyMutationName: String! + relateToOneForUpdateInputName: String! + relateToManyForUpdateInputName: String! + deleteMutationName: String! + deleteManyMutationName: String! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/assets-local/schema.prisma b/examples/assets-local/schema.prisma index e1151645890..931c8139b39 100644 --- a/examples/assets-local/schema.prisma +++ b/examples/assets-local/schema.prisma @@ -9,7 +9,7 @@ datasource sqlite { generator client { provider = "prisma-client-js" - output = "node_modules/.myprisma/client" + output = "node_modules/myprisma" } model Post { diff --git a/examples/assets-s3/keystone.ts b/examples/assets-s3/keystone.ts index fa1a84e9ae0..c317b20e4e5 100644 --- a/examples/assets-s3/keystone.ts +++ b/examples/assets-s3/keystone.ts @@ -1,7 +1,7 @@ import 'dotenv/config' import { config } from '@keystone-6/core' -import { fixPrismaPath } from '../example-utils' import { lists } from './schema' +import bytes from 'bytes' const { S3_BUCKET_NAME: bucketName = 'keystone-test', @@ -16,9 +16,12 @@ export default config({ url: process.env.DATABASE_URL || 'file:./keystone-example.db', // WARNING: this is only needed for our monorepo examples, dont do this - ...fixPrismaPath, + prismaClientPath: 'node_modules/myprisma', }, lists, + server: { + maxFileSize: bytes('8Mb') + }, storage: { my_images: { kind: 's3', diff --git a/examples/assets-s3/package.json b/examples/assets-s3/package.json index 5d5c98c2bdd..9b6d9571d09 100644 --- a/examples/assets-s3/package.json +++ b/examples/assets-s3/package.json @@ -1,6 +1,6 @@ { "name": "@keystone-6/example-assets-s3", - "version": "0.0.7", + "version": null, "private": true, "license": "MIT", "scripts": { @@ -10,12 +10,14 @@ "postinstall": "keystone postinstall" }, "dependencies": { - "@keystone-6/core": "workspace:^", - "@prisma/client": "catalog:", + "@keystone-6/core": "^6.3.1", + "@prisma/client": "5.19.0", + "bytes": "^3.1.1", "dotenv": "^16.0.0" }, "devDependencies": { - "prisma": "catalog:", - "typescript": "catalog:" + "@types/bytes": "^3.1.1", + "prisma": "5.19.0", + "typescript": "^5.5.0" } } diff --git a/examples/assets-s3/schema.graphql b/examples/assets-s3/schema.graphql index aa2c66202d4..04eb959be33 100644 --- a/examples/assets-s3/schema.graphql +++ b/examples/assets-s3/schema.graphql @@ -139,8 +139,8 @@ type Mutation { } type Query { - posts(where: PostWhereInput! = {}, orderBy: [PostOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: PostWhereUniqueInput): [Post!] post(where: PostWhereUniqueInput!): Post + posts(where: PostWhereInput! = {}, orderBy: [PostOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: PostWhereUniqueInput): [Post!] postsCount(where: PostWhereInput! = {}): Int keystone: KeystoneMeta! } @@ -156,23 +156,25 @@ type KeystoneAdminMeta { type KeystoneAdminUIListMeta { key: String! - itemQueryName: String! - listQueryName: String! - hideCreate: Boolean! - hideDelete: Boolean! path: String! label: String! singular: String! plural: String! description: String - initialColumns: [String!]! pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! groups: [KeystoneAdminUIFieldGroupMeta!]! + graphql: KeystoneAdminUIGraphQL! + initialColumns: [String!]! + initialSearchFields: [String!]! initialSort: KeystoneAdminUISort - isHidden: Boolean! isSingleton: Boolean! + hideCreate: Boolean! + hideDelete: Boolean! + isHidden: Boolean! + itemQueryName: String! + listQueryName: String! } type KeystoneAdminUIFieldMeta { @@ -242,6 +244,33 @@ type KeystoneAdminUIFieldGroupMeta { fields: [KeystoneAdminUIFieldMeta!]! } +type KeystoneAdminUIGraphQL { + names: KeystoneAdminUIGraphQLNames! +} + +type KeystoneAdminUIGraphQLNames { + outputTypeName: String! + whereInputName: String! + whereUniqueInputName: String! + createInputName: String! + createMutationName: String! + createManyMutationName: String! + relateToOneForCreateInputName: String! + relateToManyForCreateInputName: String! + itemQueryName: String! + listOrderName: String! + listQueryCountName: String! + listQueryName: String! + updateInputName: String! + updateMutationName: String! + updateManyInputName: String! + updateManyMutationName: String! + relateToOneForUpdateInputName: String! + relateToManyForUpdateInputName: String! + deleteMutationName: String! + deleteManyMutationName: String! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/assets-s3/schema.prisma b/examples/assets-s3/schema.prisma index e1151645890..931c8139b39 100644 --- a/examples/assets-s3/schema.prisma +++ b/examples/assets-s3/schema.prisma @@ -9,7 +9,7 @@ datasource sqlite { generator client { provider = "prisma-client-js" - output = "node_modules/.myprisma/client" + output = "node_modules/myprisma" } model Post { diff --git a/examples/auth/app/(admin)/.admin/index.tsx b/examples/auth/app/(admin)/.admin/index.tsx new file mode 100644 index 00000000000..cc237ef7f98 --- /dev/null +++ b/examples/auth/app/(admin)/.admin/index.tsx @@ -0,0 +1,21 @@ +/* eslint-disable */ +import * as view0 from "@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/id-field-view" +import * as view1 from "@keystone-6/core/fields/types/text/views" +import * as view2 from "@keystone-6/core/fields/types/password/views" +import * as view3 from "@keystone-6/core/fields/types/checkbox/views" +import * as view4 from "@keystone-6/core/fields/types/timestamp/views" +import * as view5 from "@keystone-6/core/fields/types/relationship/views" +import * as view6 from "@keystone-6/core/fields/types/integer/views" +import * as view7 from "@keystone-6/core/fields/types/select/views" +import * as view8 from "@keystone-6/core/fields/types/json/views" + +const adminConfig = {} + +export const config = { + lazyMetadataQuery: {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"keystone","loc":{"start":22,"end":30}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"adminMeta","loc":{"start":39,"end":48}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lists","loc":{"start":59,"end":64}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key","loc":{"start":77,"end":80}},"arguments":[],"directives":[],"loc":{"start":77,"end":80}},{"kind":"Field","name":{"kind":"Name","value":"isHidden","loc":{"start":91,"end":99}},"arguments":[],"directives":[],"loc":{"start":91,"end":99}},{"kind":"Field","name":{"kind":"Name","value":"fields","loc":{"start":110,"end":116}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"path","loc":{"start":131,"end":135}},"arguments":[],"directives":[],"loc":{"start":131,"end":135}},{"kind":"Field","name":{"kind":"Name","value":"createView","loc":{"start":148,"end":158}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fieldMode","loc":{"start":175,"end":184}},"arguments":[],"directives":[],"loc":{"start":175,"end":184}}],"loc":{"start":159,"end":198}},"loc":{"start":148,"end":198}}],"loc":{"start":117,"end":210}},"loc":{"start":110,"end":210}}],"loc":{"start":65,"end":220}},"loc":{"start":59,"end":220}}],"loc":{"start":49,"end":228}},"loc":{"start":39,"end":228}}],"loc":{"start":31,"end":234}},"loc":{"start":22,"end":234}},{"kind":"Field","name":{"kind":"Name","value":"authenticatedItem"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}, + fieldViews: [view0,view1,view2,view3,view4,view5,view6,view7,view8], + adminMetaHash: '1c8ah28', + adminConfig, + apiPath: '/api/graphql', + adminPath: '', +}; diff --git a/examples/auth/app/(admin)/[listKey]/[id]/page.tsx b/examples/auth/app/(admin)/[listKey]/[id]/page.tsx new file mode 100644 index 00000000000..c5d7ea2be62 --- /dev/null +++ b/examples/auth/app/(admin)/[listKey]/[id]/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { ItemPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage' + +export default ItemPage diff --git a/examples/auth/app/(admin)/[listKey]/create/page.tsx b/examples/auth/app/(admin)/[listKey]/create/page.tsx new file mode 100644 index 00000000000..d6042acaa96 --- /dev/null +++ b/examples/auth/app/(admin)/[listKey]/create/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { CreateItemPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/CreateItemPage' + +export default CreateItemPage diff --git a/examples/auth/app/(admin)/[listKey]/page.tsx b/examples/auth/app/(admin)/[listKey]/page.tsx new file mode 100644 index 00000000000..f6e75f8cfab --- /dev/null +++ b/examples/auth/app/(admin)/[listKey]/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { ListPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage' + +export default ListPage diff --git a/examples/auth/app/(admin)/init/page.tsx b/examples/auth/app/(admin)/init/page.tsx new file mode 100644 index 00000000000..224220389cb --- /dev/null +++ b/examples/auth/app/(admin)/init/page.tsx @@ -0,0 +1,7 @@ +'use client' +/* eslint-disable */ +import { getInitPage } from '@keystone-6/auth/pages/InitPage' + +const fieldPaths = ["name","password"] + +export default getInitPage({"listKey":"User","fieldPaths":["name","password"],"enableWelcome":true}) diff --git a/examples/auth/app/(admin)/layout.tsx b/examples/auth/app/(admin)/layout.tsx new file mode 100644 index 00000000000..abb5a0f3b2c --- /dev/null +++ b/examples/auth/app/(admin)/layout.tsx @@ -0,0 +1,16 @@ +'use client' +import { Layout } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/App' +import { config } from './.admin' + + +export default function AdminLayout ({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/examples/auth/app/(admin)/no-access/page.tsx b/examples/auth/app/(admin)/no-access/page.tsx new file mode 100644 index 00000000000..00571f2f9b3 --- /dev/null +++ b/examples/auth/app/(admin)/no-access/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { getNoAccessPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/NoAccessPage' + +export default getNoAccessPage({ sessionsEnabled: true }) diff --git a/examples/auth/app/(admin)/page.tsx b/examples/auth/app/(admin)/page.tsx new file mode 100644 index 00000000000..5c268390b0f --- /dev/null +++ b/examples/auth/app/(admin)/page.tsx @@ -0,0 +1,2 @@ +'use client' +export { HomePage as default } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/HomePage' diff --git a/examples/auth/app/(admin)/signin/page.tsx b/examples/auth/app/(admin)/signin/page.tsx new file mode 100644 index 00000000000..ef30e38dcac --- /dev/null +++ b/examples/auth/app/(admin)/signin/page.tsx @@ -0,0 +1,5 @@ +'use client' +/* eslint-disable */ +import { getSigninPage } from '@keystone-6/auth/pages/SigninPage' + +export default getSigninPage({"identityField":"name","secretField":"password","mutationName":"authenticateUserWithPassword","successTypename":"UserAuthenticationWithPasswordSuccess","failureTypename":"UserAuthenticationWithPasswordFailure"}) diff --git a/examples/auth/app/layout.tsx b/examples/auth/app/layout.tsx new file mode 100644 index 00000000000..38a4853e3a5 --- /dev/null +++ b/examples/auth/app/layout.tsx @@ -0,0 +1,11 @@ +export default function RootLayout ({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/examples/auth/keystone.ts b/examples/auth/keystone.ts index 88f7606d0a1..5d2b780a3f5 100644 --- a/examples/auth/keystone.ts +++ b/examples/auth/keystone.ts @@ -1,7 +1,6 @@ import { config } from '@keystone-6/core' import { statelessSessions } from '@keystone-6/core/session' import { createAuth } from '@keystone-6/auth' -import { fixPrismaPath } from '../example-utils' import { type Session, lists } from './schema' import { type TypeInfo } from '.keystone/types' @@ -55,7 +54,7 @@ export default withAuth>( url: process.env.DATABASE_URL || 'file:./keystone-example.db', // WARNING: this is only needed for our monorepo examples, dont do this - ...fixPrismaPath, + prismaClientPath: 'node_modules/myprisma', }, lists, ui: { @@ -71,5 +70,5 @@ export default withAuth>( // the session secret is used to encrypt cookie data secret: sessionSecret, }), - }) + }) as any ) diff --git a/examples/nextjs-keystone-two-servers/nextjs-frontend/next-env.d.ts b/examples/auth/next-env.d.ts similarity index 100% rename from examples/nextjs-keystone-two-servers/nextjs-frontend/next-env.d.ts rename to examples/auth/next-env.d.ts diff --git a/examples/auth/next.config.mjs b/examples/auth/next.config.mjs new file mode 100644 index 00000000000..8f06b183bdf --- /dev/null +++ b/examples/auth/next.config.mjs @@ -0,0 +1,10 @@ +// you don't need this if you're building something outside of the Keystone repo + +export default { + eslint: { + ignoreDuringBuilds: true, + }, + typescript: { + ignoreBuildErrors: true, + }, +} diff --git a/examples/auth/package.json b/examples/auth/package.json index 7e757dcb1dc..45ee13412f7 100644 --- a/examples/auth/package.json +++ b/examples/auth/package.json @@ -1,6 +1,6 @@ { "name": "@keystone-6/example-auth", - "version": "0.1.2", + "version": null, "private": true, "license": "MIT", "scripts": { @@ -10,12 +10,13 @@ "postinstall": "keystone postinstall" }, "dependencies": { - "@keystone-6/auth": "workspace:^", - "@keystone-6/core": "workspace:^", - "@prisma/client": "catalog:" + "@keystone-6/auth": "^8.1.0", + "@keystone-6/core": "^6.3.1", + "@prisma/client": "5.19.0", + "next": "^15.0.0" }, "devDependencies": { - "prisma": "catalog:", - "typescript": "catalog:" + "prisma": "5.19.0", + "typescript": "^5.5.0" } } diff --git a/examples/auth/sandbox.config.json b/examples/auth/sandbox.config.json index 7a34682ee45..e53d3f9dadc 100644 --- a/examples/auth/sandbox.config.json +++ b/examples/auth/sandbox.config.json @@ -2,6 +2,6 @@ "template": "node", "container": { "startScript": "keystone dev", - "node": "16" + "node": "22" } } diff --git a/examples/auth/schema.graphql b/examples/auth/schema.graphql index 3bbe083ed5c..da46536e183 100644 --- a/examples/auth/schema.graphql +++ b/examples/auth/schema.graphql @@ -67,6 +67,380 @@ input UserCreateInput { isAdmin: Boolean } +type Project { + id: ID! + slug: String + name: String + createdAt: DateTime + updatedAt: DateTime + deletedAt: DateTime + taskRuns(where: TaskRunWhereInput! = {}, orderBy: [TaskRunOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: TaskRunWhereUniqueInput): [TaskRun!] + taskRunsCount(where: TaskRunWhereInput! = {}): Int +} + +scalar DateTime @specifiedBy(url: "https://datatracker.ietf.org/doc/html/rfc3339#section-5.6") + +input ProjectWhereUniqueInput { + id: ID + slug: String +} + +input ProjectWhereInput { + AND: [ProjectWhereInput!] + OR: [ProjectWhereInput!] + NOT: [ProjectWhereInput!] + id: IDFilter + slug: StringFilter + name: StringFilter + createdAt: DateTimeNullableFilter + updatedAt: DateTimeNullableFilter + deletedAt: DateTimeNullableFilter + taskRuns: TaskRunManyRelationFilter +} + +input StringFilter { + equals: String + in: [String!] + notIn: [String!] + lt: String + lte: String + gt: String + gte: String + contains: String + startsWith: String + endsWith: String + not: NestedStringFilter +} + +input NestedStringFilter { + equals: String + in: [String!] + notIn: [String!] + lt: String + lte: String + gt: String + gte: String + contains: String + startsWith: String + endsWith: String + not: NestedStringFilter +} + +input DateTimeNullableFilter { + equals: DateTime + in: [DateTime!] + notIn: [DateTime!] + lt: DateTime + lte: DateTime + gt: DateTime + gte: DateTime + not: DateTimeNullableFilter +} + +input TaskRunManyRelationFilter { + every: TaskRunWhereInput + some: TaskRunWhereInput + none: TaskRunWhereInput +} + +input ProjectOrderByInput { + id: OrderDirection + slug: OrderDirection + name: OrderDirection + createdAt: OrderDirection + updatedAt: OrderDirection + deletedAt: OrderDirection +} + +input ProjectUpdateInput { + slug: String + name: String + createdAt: DateTime + updatedAt: DateTime + deletedAt: DateTime + taskRuns: TaskRunRelateToManyForUpdateInput +} + +input TaskRunRelateToManyForUpdateInput { + disconnect: [TaskRunWhereUniqueInput!] + set: [TaskRunWhereUniqueInput!] + create: [TaskRunCreateInput!] + connect: [TaskRunWhereUniqueInput!] +} + +input ProjectUpdateArgs { + where: ProjectWhereUniqueInput! + data: ProjectUpdateInput! +} + +input ProjectCreateInput { + slug: String + name: String + createdAt: DateTime + updatedAt: DateTime + deletedAt: DateTime + taskRuns: TaskRunRelateToManyForCreateInput +} + +input TaskRunRelateToManyForCreateInput { + create: [TaskRunCreateInput!] + connect: [TaskRunWhereUniqueInput!] +} + +type TaskRun { + id: ID! + number: Int + friendlyId: String + status: String + taskIdentifier: String + isTest: Boolean + payload: String + payloadType: String + context: JSON + traceId: String + spanId: String + project: Project + createdAt: DateTime + updatedAt: DateTime + attempts(where: TaskRunAttemptWhereInput! = {}, orderBy: [TaskRunAttemptOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: TaskRunAttemptWhereUniqueInput): [TaskRunAttempt!] + attemptsCount(where: TaskRunAttemptWhereInput! = {}): Int + startedAt: DateTime + completedAt: DateTime +} + +input TaskRunWhereUniqueInput { + id: ID +} + +input TaskRunWhereInput { + AND: [TaskRunWhereInput!] + OR: [TaskRunWhereInput!] + NOT: [TaskRunWhereInput!] + id: IDFilter + number: IntNullableFilter + friendlyId: StringFilter + status: StringNullableFilter + taskIdentifier: StringFilter + isTest: BooleanFilter + payload: StringFilter + payloadType: StringFilter + traceId: StringFilter + spanId: StringFilter + project: ProjectWhereInput + createdAt: DateTimeNullableFilter + updatedAt: DateTimeNullableFilter + attempts: TaskRunAttemptManyRelationFilter + startedAt: DateTimeNullableFilter + completedAt: DateTimeNullableFilter +} + +input IntNullableFilter { + equals: Int + in: [Int!] + notIn: [Int!] + lt: Int + lte: Int + gt: Int + gte: Int + not: IntNullableFilter +} + +input StringNullableFilter { + equals: String + in: [String!] + notIn: [String!] + lt: String + lte: String + gt: String + gte: String + contains: String + startsWith: String + endsWith: String + not: StringNullableFilter +} + +input TaskRunAttemptManyRelationFilter { + every: TaskRunAttemptWhereInput + some: TaskRunAttemptWhereInput + none: TaskRunAttemptWhereInput +} + +input TaskRunOrderByInput { + id: OrderDirection + number: OrderDirection + friendlyId: OrderDirection + status: OrderDirection + taskIdentifier: OrderDirection + isTest: OrderDirection + payload: OrderDirection + payloadType: OrderDirection + traceId: OrderDirection + spanId: OrderDirection + createdAt: OrderDirection + updatedAt: OrderDirection + startedAt: OrderDirection + completedAt: OrderDirection +} + +input TaskRunUpdateInput { + number: Int + friendlyId: String + status: String + taskIdentifier: String + isTest: Boolean + payload: String + payloadType: String + context: JSON + traceId: String + spanId: String + project: ProjectRelateToOneForUpdateInput + createdAt: DateTime + updatedAt: DateTime + attempts: TaskRunAttemptRelateToManyForUpdateInput + startedAt: DateTime + completedAt: DateTime +} + +input ProjectRelateToOneForUpdateInput { + create: ProjectCreateInput + connect: ProjectWhereUniqueInput + disconnect: Boolean +} + +input TaskRunAttemptRelateToManyForUpdateInput { + disconnect: [TaskRunAttemptWhereUniqueInput!] + set: [TaskRunAttemptWhereUniqueInput!] + create: [TaskRunAttemptCreateInput!] + connect: [TaskRunAttemptWhereUniqueInput!] +} + +input TaskRunUpdateArgs { + where: TaskRunWhereUniqueInput! + data: TaskRunUpdateInput! +} + +input TaskRunCreateInput { + number: Int + friendlyId: String + status: String + taskIdentifier: String + isTest: Boolean + payload: String + payloadType: String + context: JSON + traceId: String + spanId: String + project: ProjectRelateToOneForCreateInput + createdAt: DateTime + updatedAt: DateTime + attempts: TaskRunAttemptRelateToManyForCreateInput + startedAt: DateTime + completedAt: DateTime +} + +input ProjectRelateToOneForCreateInput { + create: ProjectCreateInput + connect: ProjectWhereUniqueInput +} + +input TaskRunAttemptRelateToManyForCreateInput { + create: [TaskRunAttemptCreateInput!] + connect: [TaskRunAttemptWhereUniqueInput!] +} + +type TaskRunAttempt { + id: ID! + number: Int + friendlyId: String + taskRun: TaskRun + status: String + createdAt: DateTime + updatedAt: DateTime + startedAt: DateTime + completedAt: DateTime + error: JSON + output: String + outputType: String +} + +input TaskRunAttemptWhereUniqueInput { + id: ID +} + +input TaskRunAttemptWhereInput { + AND: [TaskRunAttemptWhereInput!] + OR: [TaskRunAttemptWhereInput!] + NOT: [TaskRunAttemptWhereInput!] + id: IDFilter + number: IntNullableFilter + friendlyId: StringFilter + taskRun: TaskRunWhereInput + status: StringNullableFilter + createdAt: DateTimeNullableFilter + updatedAt: DateTimeNullableFilter + startedAt: DateTimeNullableFilter + completedAt: DateTimeNullableFilter + output: StringFilter + outputType: StringFilter +} + +input TaskRunAttemptOrderByInput { + id: OrderDirection + number: OrderDirection + friendlyId: OrderDirection + status: OrderDirection + createdAt: OrderDirection + updatedAt: OrderDirection + startedAt: OrderDirection + completedAt: OrderDirection + output: OrderDirection + outputType: OrderDirection +} + +input TaskRunAttemptUpdateInput { + number: Int + friendlyId: String + taskRun: TaskRunRelateToOneForUpdateInput + status: String + createdAt: DateTime + updatedAt: DateTime + startedAt: DateTime + completedAt: DateTime + error: JSON + output: String + outputType: String +} + +input TaskRunRelateToOneForUpdateInput { + create: TaskRunCreateInput + connect: TaskRunWhereUniqueInput + disconnect: Boolean +} + +input TaskRunAttemptUpdateArgs { + where: TaskRunAttemptWhereUniqueInput! + data: TaskRunAttemptUpdateInput! +} + +input TaskRunAttemptCreateInput { + number: Int + friendlyId: String + taskRun: TaskRunRelateToOneForCreateInput + status: String + createdAt: DateTime + updatedAt: DateTime + startedAt: DateTime + completedAt: DateTime + error: JSON + output: String + outputType: String +} + +input TaskRunRelateToOneForCreateInput { + create: TaskRunCreateInput + connect: TaskRunWhereUniqueInput +} + """ The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). """ @@ -79,6 +453,24 @@ type Mutation { updateUsers(data: [UserUpdateArgs!]!): [User] deleteUser(where: UserWhereUniqueInput!): User deleteUsers(where: [UserWhereUniqueInput!]!): [User] + createProject(data: ProjectCreateInput!): Project + createProjects(data: [ProjectCreateInput!]!): [Project] + updateProject(where: ProjectWhereUniqueInput!, data: ProjectUpdateInput!): Project + updateProjects(data: [ProjectUpdateArgs!]!): [Project] + deleteProject(where: ProjectWhereUniqueInput!): Project + deleteProjects(where: [ProjectWhereUniqueInput!]!): [Project] + createTaskRun(data: TaskRunCreateInput!): TaskRun + createTaskRuns(data: [TaskRunCreateInput!]!): [TaskRun] + updateTaskRun(where: TaskRunWhereUniqueInput!, data: TaskRunUpdateInput!): TaskRun + updateTaskRuns(data: [TaskRunUpdateArgs!]!): [TaskRun] + deleteTaskRun(where: TaskRunWhereUniqueInput!): TaskRun + deleteTaskRuns(where: [TaskRunWhereUniqueInput!]!): [TaskRun] + createTaskRunAttempt(data: TaskRunAttemptCreateInput!): TaskRunAttempt + createTaskRunAttempts(data: [TaskRunAttemptCreateInput!]!): [TaskRunAttempt] + updateTaskRunAttempt(where: TaskRunAttemptWhereUniqueInput!, data: TaskRunAttemptUpdateInput!): TaskRunAttempt + updateTaskRunAttempts(data: [TaskRunAttemptUpdateArgs!]!): [TaskRunAttempt] + deleteTaskRunAttempt(where: TaskRunAttemptWhereUniqueInput!): TaskRunAttempt + deleteTaskRunAttempts(where: [TaskRunAttemptWhereUniqueInput!]!): [TaskRunAttempt] endSession: Boolean! authenticateUserWithPassword(name: String!, password: String!): UserAuthenticationWithPasswordResult createInitialUser(data: CreateInitialUserInput!): UserAuthenticationWithPasswordSuccess! @@ -101,9 +493,18 @@ input CreateInitialUserInput { } type Query { - users(where: UserWhereInput! = {}, orderBy: [UserOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: UserWhereUniqueInput): [User!] user(where: UserWhereUniqueInput!): User + users(where: UserWhereInput! = {}, orderBy: [UserOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: UserWhereUniqueInput): [User!] usersCount(where: UserWhereInput! = {}): Int + projects(where: ProjectWhereInput! = {}, orderBy: [ProjectOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: ProjectWhereUniqueInput): [Project!] + project(where: ProjectWhereUniqueInput!): Project + projectsCount(where: ProjectWhereInput! = {}): Int + taskRuns(where: TaskRunWhereInput! = {}, orderBy: [TaskRunOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: TaskRunWhereUniqueInput): [TaskRun!] + taskRun(where: TaskRunWhereUniqueInput!): TaskRun + taskRunsCount(where: TaskRunWhereInput! = {}): Int + taskRunAttempts(where: TaskRunAttemptWhereInput! = {}, orderBy: [TaskRunAttemptOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: TaskRunAttemptWhereUniqueInput): [TaskRunAttempt!] + taskRunAttempt(where: TaskRunAttemptWhereUniqueInput!): TaskRunAttempt + taskRunAttemptsCount(where: TaskRunAttemptWhereInput! = {}): Int keystone: KeystoneMeta! authenticatedItem: AuthenticatedItem } @@ -121,23 +522,25 @@ type KeystoneAdminMeta { type KeystoneAdminUIListMeta { key: String! - itemQueryName: String! - listQueryName: String! - hideCreate: Boolean! - hideDelete: Boolean! path: String! label: String! singular: String! plural: String! description: String - initialColumns: [String!]! pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! groups: [KeystoneAdminUIFieldGroupMeta!]! + graphql: KeystoneAdminUIGraphQL! + initialColumns: [String!]! + initialSearchFields: [String!]! initialSort: KeystoneAdminUISort - isHidden: Boolean! isSingleton: Boolean! + hideCreate: Boolean! + hideDelete: Boolean! + isHidden: Boolean! + itemQueryName: String! + listQueryName: String! } type KeystoneAdminUIFieldMeta { @@ -207,6 +610,33 @@ type KeystoneAdminUIFieldGroupMeta { fields: [KeystoneAdminUIFieldMeta!]! } +type KeystoneAdminUIGraphQL { + names: KeystoneAdminUIGraphQLNames! +} + +type KeystoneAdminUIGraphQLNames { + outputTypeName: String! + whereInputName: String! + whereUniqueInputName: String! + createInputName: String! + createMutationName: String! + createManyMutationName: String! + relateToOneForCreateInputName: String! + relateToManyForCreateInputName: String! + itemQueryName: String! + listOrderName: String! + listQueryCountName: String! + listQueryName: String! + updateInputName: String! + updateMutationName: String! + updateManyInputName: String! + updateManyMutationName: String! + relateToOneForUpdateInputName: String! + relateToManyForUpdateInputName: String! + deleteMutationName: String! + deleteManyMutationName: String! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/auth/schema.prisma b/examples/auth/schema.prisma index 9ae26a3cb1c..a711ebf4eee 100644 --- a/examples/auth/schema.prisma +++ b/examples/auth/schema.prisma @@ -9,7 +9,7 @@ datasource sqlite { generator client { provider = "prisma-client-js" - output = "node_modules/.myprisma/client" + output = "node_modules/myprisma" } model User { @@ -18,3 +18,54 @@ model User { password String isAdmin Boolean @default(false) } + +model Project { + id String @id @default(cuid()) + slug String @unique @default("") + name String @default("") + createdAt DateTime? @default(now()) + updatedAt DateTime? @default(now()) @updatedAt + deletedAt DateTime? + taskRuns TaskRun[] @relation("TaskRun_project") +} + +model TaskRun { + id String @id @default(cuid()) + number Int? @default(0) + friendlyId String @default("") + status String? @default("pending") + taskIdentifier String @default("") + isTest Boolean @default(false) + payload String @default("") + payloadType String @default("application/json") + context String? + traceId String @default("") + spanId String @default("") + project Project? @relation("TaskRun_project", fields: [projectId], references: [id]) + projectId String? @map("project") + createdAt DateTime? @default(now()) + updatedAt DateTime? @default(now()) @updatedAt + attempts TaskRunAttempt[] @relation("TaskRunAttempt_taskRun") + startedAt DateTime? + completedAt DateTime? + + @@index([projectId]) +} + +model TaskRunAttempt { + id String @id @default(cuid()) + number Int? @default(0) + friendlyId String @default("") + taskRun TaskRun? @relation("TaskRunAttempt_taskRun", fields: [taskRunId], references: [id]) + taskRunId String? @map("taskRun") + status String? @default("pending") + createdAt DateTime? @default(now()) + updatedAt DateTime? @default(now()) @updatedAt + startedAt DateTime? + completedAt DateTime? + error String? + output String @default("") + outputType String @default("application/json") + + @@index([taskRunId]) +} diff --git a/examples/auth/schema.ts b/examples/auth/schema.ts index 2879ee9f407..a657f6863c2 100644 --- a/examples/auth/schema.ts +++ b/examples/auth/schema.ts @@ -1,6 +1,6 @@ import { list } from '@keystone-6/core' import { allowAll, denyAll } from '@keystone-6/core/access' -import { text, checkbox, password } from '@keystone-6/core/fields' +import { text, checkbox, password, timestamp, relationship, json, select, integer } from '@keystone-6/core/fields' import type { Lists } from '.keystone/types' // WARNING: this example is for demonstration purposes only @@ -153,4 +153,76 @@ export const lists = { }), }, }), + Project: list({ + access: allowAll, + + fields: { + slug: text({ isIndexed: 'unique' }), + name: text({ validation: { isRequired: true } }), + createdAt: timestamp({ defaultValue: { kind: 'now' } }), + updatedAt: timestamp({ defaultValue: { kind: 'now' }, db: { updatedAt: true } }), + deletedAt: timestamp({}), + taskRuns: relationship({ ref: 'TaskRun.project', many: true }), + } + }), + TaskRun: list({ + access: allowAll, + + fields: { + number: integer({ defaultValue: 0 }), + friendlyId: text({}), + status: select({ + options: [ + { label: 'Pending', value: 'pending' }, + { label: 'Executing', value: 'executing' }, + { label: 'Paused', value: 'paused' }, + { label: 'Canceled', value: 'canceled' }, + { label: 'Interrupted', value: 'interrupted' }, + { label: 'Completed Successfully', value: 'completed_successfully' }, + { label: 'Completed With Errors', value: 'completed_with_errors' }, + { label: 'System Failure', value: 'system_failure' }, + { label: 'Crashed', value: 'crashed' }], + defaultValue: 'pending' + }), + taskIdentifier: text({}), + isTest: checkbox({ defaultValue: false }), + payload: text({}), + payloadType: text({ defaultValue: 'application/json' }), + context: json({}), + traceId: text({}), + spanId: text({}), + project: relationship({ ref: 'Project.taskRuns' }), + createdAt: timestamp({ defaultValue: { kind: 'now' } }), + updatedAt: timestamp({ defaultValue: { kind: 'now' }, db: { updatedAt: true } }), + attempts: relationship({ ref: 'TaskRunAttempt.taskRun', many: true }), + startedAt: timestamp({}), + completedAt: timestamp({}), + } + }), + TaskRunAttempt: list({ + access: allowAll, + + fields: { + number: integer({ defaultValue: 0 }), + friendlyId: text({}), + taskRun: relationship({ ref: 'TaskRun.attempts' }), + status: select({ + options: [ + { label: 'Pending', value: 'pending' }, + { label: 'Executing', value: 'executing' }, + { label: 'Paused', value: 'paused' }, + { label: 'Failed', value: 'failed' }, + { label: 'Canceled', value: 'canceled' }, + { label: 'Completed', value: 'completed' }], + defaultValue: 'pending' + }), + createdAt: timestamp({ defaultValue: { kind: 'now' } }), + updatedAt: timestamp({ defaultValue: { kind: 'now' }, db: { updatedAt: true } }), + startedAt: timestamp({}), + completedAt: timestamp({}), + error: json({}), + output: text({}), + outputType: text({ defaultValue: 'application/json' }), + } + }) } satisfies Lists diff --git a/examples/auth/tsconfig.json b/examples/auth/tsconfig.json new file mode 100644 index 00000000000..ccb2ed95d83 --- /dev/null +++ b/examples/auth/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "noEmit": true, + "incremental": true, + "module": "esnext", + "esModuleInterop": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "plugins": [ + { + "name": "next" + } + ] + }, + "include": [ + "next-env.d.ts", + ".next/types/**/*.ts", + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/examples/better-list-search/README.md b/examples/better-list-search/README.md new file mode 100644 index 00000000000..479accb2e51 --- /dev/null +++ b/examples/better-list-search/README.md @@ -0,0 +1,32 @@ +## Base Project - Blog + +This project implements a basic **Blog**, with `Posts` and `Authors`. + +Use it as a starting place for learning how to use Keystone. + +## Instructions + +To run this project, clone the Keystone repository locally, run `pnpm install` at the root of this repository, then navigate to this directory and run: + +```shell +pnpm dev +``` + +This will start the Admin UI at [localhost:3000](http://localhost:3000). +You can use the Admin UI to create items in your database. + +You can also access a GraphQL Playground at [localhost:3000/api/graphql](http://localhost:3000/api/graphql), which allows you to directly run GraphQL queries and mutations. + +Congratulations, you're now up and running with Keystone! 🚀 + +### Optional: add sample data + +This example includes sample data. To add it to your database: + +1. Ensure you’ve initialised your project with `pnpm dev` at least once. +2. Run `pnpm seed-data`. This will populate your database with sample content. +3. Run `pnpm dev` again to startup Admin UI with sample data in place. + +## Try it out in CodeSandbox 🧪 + +You can play with this example online in a web browser using the free [codesandbox.io](https://codesandbox.io/) service. To launch this example, open the URL . You can also fork this sandbox to make your own changes. diff --git a/examples/better-list-search/keystone.ts b/examples/better-list-search/keystone.ts new file mode 100644 index 00000000000..e76d454cffe --- /dev/null +++ b/examples/better-list-search/keystone.ts @@ -0,0 +1,14 @@ +import { config } from '@keystone-6/core' +import { lists } from './schema' +import type { TypeInfo } from '.keystone/types' + +export default config({ + db: { + provider: 'sqlite', + url: process.env.DATABASE_URL || 'file:./keystone-example.db', + + // WARNING: this is only needed for our monorepo examples, dont do this + prismaClientPath: 'node_modules/myprisma', + }, + lists, +}) diff --git a/examples/better-list-search/package.json b/examples/better-list-search/package.json new file mode 100644 index 00000000000..83b3b1cad39 --- /dev/null +++ b/examples/better-list-search/package.json @@ -0,0 +1,23 @@ +{ + "name": "@keystone-6/example-better-list-search", + "version": null, + "private": true, + "license": "MIT", + "scripts": { + "dev": "keystone dev", + "start": "keystone start", + "build": "keystone build", + "postinstall": "keystone postinstall", + "seed-data": "tsx seed-data.ts" + }, + "dependencies": { + "@keystone-6/core": "^6.3.1", + "@keystone-6/fields-document": "^9.1.1", + "@prisma/client": "5.19.0" + }, + "devDependencies": { + "prisma": "5.19.0", + "tsx": "^4.0.0", + "typescript": "^5.5.0" + } +} diff --git a/examples/better-list-search/sandbox.config.json b/examples/better-list-search/sandbox.config.json new file mode 100644 index 00000000000..e53d3f9dadc --- /dev/null +++ b/examples/better-list-search/sandbox.config.json @@ -0,0 +1,7 @@ +{ + "template": "node", + "container": { + "startScript": "keystone dev", + "node": "22" + } +} diff --git a/packages/create/starter/schema.graphql b/examples/better-list-search/schema.graphql similarity index 69% rename from packages/create/starter/schema.graphql rename to examples/better-list-search/schema.graphql index 51d80f30050..b062d907379 100644 --- a/packages/create/starter/schema.graphql +++ b/examples/better-list-search/schema.graphql @@ -1,36 +1,29 @@ # This file is automatically generated by Keystone, do not modify it manually. # Modify your Keystone config when you want to change this. -type User { +type Post { id: ID! - name: String - email: String - password: PasswordState - posts(where: PostWhereInput! = {}, orderBy: [PostOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: PostWhereUniqueInput): [Post!] - postsCount(where: PostWhereInput! = {}): Int - createdAt: DateTime -} - -type PasswordState { - isSet: Boolean! + title: String + author: Author + related(where: PostWhereInput! = {}, orderBy: [PostOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: PostWhereUniqueInput): [Post!] + relatedCount(where: PostWhereInput! = {}): Int + tags(where: TagWhereInput! = {}, orderBy: [TagOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: TagWhereUniqueInput): [Tag!] + tagsCount(where: TagWhereInput! = {}): Int } -scalar DateTime @specifiedBy(url: "https://datatracker.ietf.org/doc/html/rfc3339#section-5.6") - -input UserWhereUniqueInput { +input PostWhereUniqueInput { id: ID - email: String } -input UserWhereInput { - AND: [UserWhereInput!] - OR: [UserWhereInput!] - NOT: [UserWhereInput!] +input PostWhereInput { + AND: [PostWhereInput!] + OR: [PostWhereInput!] + NOT: [PostWhereInput!] id: IDFilter - name: StringFilter - email: StringFilter - posts: PostManyRelationFilter - createdAt: DateTimeNullableFilter + title: StringFilter + author: AuthorWhereInput + related: PostManyRelationFilter + tags: TagManyRelationFilter } input IDFilter { @@ -78,22 +71,15 @@ input PostManyRelationFilter { none: PostWhereInput } -input DateTimeNullableFilter { - equals: DateTime - in: [DateTime!] - notIn: [DateTime!] - lt: DateTime - lte: DateTime - gt: DateTime - gte: DateTime - not: DateTimeNullableFilter +input TagManyRelationFilter { + every: TagWhereInput + some: TagWhereInput + none: TagWhereInput } -input UserOrderByInput { +input PostOrderByInput { id: OrderDirection - name: OrderDirection - email: OrderDirection - createdAt: OrderDirection + title: OrderDirection } enum OrderDirection { @@ -101,12 +87,17 @@ enum OrderDirection { desc } -input UserUpdateInput { - name: String - email: String - password: String - posts: PostRelateToManyForUpdateInput - createdAt: DateTime +input PostUpdateInput { + title: String + author: AuthorRelateToOneForUpdateInput + related: PostRelateToManyForUpdateInput + tags: TagRelateToManyForUpdateInput +} + +input AuthorRelateToOneForUpdateInput { + create: AuthorCreateInput + connect: AuthorWhereUniqueInput + disconnect: Boolean } input PostRelateToManyForUpdateInput { @@ -116,17 +107,28 @@ input PostRelateToManyForUpdateInput { connect: [PostWhereUniqueInput!] } -input UserUpdateArgs { - where: UserWhereUniqueInput! - data: UserUpdateInput! +input TagRelateToManyForUpdateInput { + disconnect: [TagWhereUniqueInput!] + set: [TagWhereUniqueInput!] + create: [TagCreateInput!] + connect: [TagWhereUniqueInput!] } -input UserCreateInput { - name: String - email: String - password: String - posts: PostRelateToManyForCreateInput - createdAt: DateTime +input PostUpdateArgs { + where: PostWhereUniqueInput! + data: PostUpdateInput! +} + +input PostCreateInput { + title: String + author: AuthorRelateToOneForCreateInput + related: PostRelateToManyForCreateInput + tags: TagRelateToManyForCreateInput +} + +input AuthorRelateToOneForCreateInput { + create: AuthorCreateInput + connect: AuthorWhereUniqueInput } input PostRelateToManyForCreateInput { @@ -134,84 +136,44 @@ input PostRelateToManyForCreateInput { connect: [PostWhereUniqueInput!] } -type Post { - id: ID! - title: String - content: Post_content_Document - author: User - tags(where: TagWhereInput! = {}, orderBy: [TagOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: TagWhereUniqueInput): [Tag!] - tagsCount(where: TagWhereInput! = {}): Int +input TagRelateToManyForCreateInput { + create: [TagCreateInput!] + connect: [TagWhereUniqueInput!] } -type Post_content_Document { - document(hydrateRelationships: Boolean! = false): JSON! +type Author { + id: ID! + name: String } -input PostWhereUniqueInput { +input AuthorWhereUniqueInput { id: ID } -input PostWhereInput { - AND: [PostWhereInput!] - OR: [PostWhereInput!] - NOT: [PostWhereInput!] +input AuthorWhereInput { + AND: [AuthorWhereInput!] + OR: [AuthorWhereInput!] + NOT: [AuthorWhereInput!] id: IDFilter - title: StringFilter - author: UserWhereInput - tags: TagManyRelationFilter -} - -input TagManyRelationFilter { - every: TagWhereInput - some: TagWhereInput - none: TagWhereInput + name: StringFilter } -input PostOrderByInput { +input AuthorOrderByInput { id: OrderDirection - title: OrderDirection -} - -input PostUpdateInput { - title: String - content: JSON - author: UserRelateToOneForUpdateInput - tags: TagRelateToManyForUpdateInput -} - -input UserRelateToOneForUpdateInput { - create: UserCreateInput - connect: UserWhereUniqueInput - disconnect: Boolean -} - -input TagRelateToManyForUpdateInput { - disconnect: [TagWhereUniqueInput!] - set: [TagWhereUniqueInput!] - create: [TagCreateInput!] - connect: [TagWhereUniqueInput!] -} - -input PostUpdateArgs { - where: PostWhereUniqueInput! - data: PostUpdateInput! + name: OrderDirection } -input PostCreateInput { - title: String - content: JSON - author: UserRelateToOneForCreateInput - tags: TagRelateToManyForCreateInput +input AuthorUpdateInput { + name: String } -input UserRelateToOneForCreateInput { - create: UserCreateInput - connect: UserWhereUniqueInput +input AuthorUpdateArgs { + where: AuthorWhereUniqueInput! + data: AuthorUpdateInput! } -input TagRelateToManyForCreateInput { - create: [TagCreateInput!] - connect: [TagWhereUniqueInput!] +input AuthorCreateInput { + name: String } type Tag { @@ -260,62 +222,39 @@ The `JSON` scalar type represents JSON values as specified by [ECMA-404](http:// scalar JSON @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf") type Mutation { - createUser(data: UserCreateInput!): User - createUsers(data: [UserCreateInput!]!): [User] - updateUser(where: UserWhereUniqueInput!, data: UserUpdateInput!): User - updateUsers(data: [UserUpdateArgs!]!): [User] - deleteUser(where: UserWhereUniqueInput!): User - deleteUsers(where: [UserWhereUniqueInput!]!): [User] createPost(data: PostCreateInput!): Post createPosts(data: [PostCreateInput!]!): [Post] updatePost(where: PostWhereUniqueInput!, data: PostUpdateInput!): Post updatePosts(data: [PostUpdateArgs!]!): [Post] deletePost(where: PostWhereUniqueInput!): Post deletePosts(where: [PostWhereUniqueInput!]!): [Post] + createAuthor(data: AuthorCreateInput!): Author + createAuthors(data: [AuthorCreateInput!]!): [Author] + updateAuthor(where: AuthorWhereUniqueInput!, data: AuthorUpdateInput!): Author + updateAuthors(data: [AuthorUpdateArgs!]!): [Author] + deleteAuthor(where: AuthorWhereUniqueInput!): Author + deleteAuthors(where: [AuthorWhereUniqueInput!]!): [Author] createTag(data: TagCreateInput!): Tag createTags(data: [TagCreateInput!]!): [Tag] updateTag(where: TagWhereUniqueInput!, data: TagUpdateInput!): Tag updateTags(data: [TagUpdateArgs!]!): [Tag] deleteTag(where: TagWhereUniqueInput!): Tag deleteTags(where: [TagWhereUniqueInput!]!): [Tag] - endSession: Boolean! - authenticateUserWithPassword(email: String!, password: String!): UserAuthenticationWithPasswordResult - createInitialUser(data: CreateInitialUserInput!): UserAuthenticationWithPasswordSuccess! -} - -union UserAuthenticationWithPasswordResult = UserAuthenticationWithPasswordSuccess | UserAuthenticationWithPasswordFailure - -type UserAuthenticationWithPasswordSuccess { - sessionToken: String! - item: User! -} - -type UserAuthenticationWithPasswordFailure { - message: String! -} - -input CreateInitialUserInput { - name: String - email: String - password: String } type Query { - users(where: UserWhereInput! = {}, orderBy: [UserOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: UserWhereUniqueInput): [User!] - user(where: UserWhereUniqueInput!): User - usersCount(where: UserWhereInput! = {}): Int - posts(where: PostWhereInput! = {}, orderBy: [PostOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: PostWhereUniqueInput): [Post!] post(where: PostWhereUniqueInput!): Post + posts(where: PostWhereInput! = {}, orderBy: [PostOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: PostWhereUniqueInput): [Post!] postsCount(where: PostWhereInput! = {}): Int - tags(where: TagWhereInput! = {}, orderBy: [TagOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: TagWhereUniqueInput): [Tag!] + author(where: AuthorWhereUniqueInput!): Author + authors(where: AuthorWhereInput! = {}, orderBy: [AuthorOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: AuthorWhereUniqueInput): [Author!] + authorsCount(where: AuthorWhereInput! = {}): Int tag(where: TagWhereUniqueInput!): Tag + tags(where: TagWhereInput! = {}, orderBy: [TagOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: TagWhereUniqueInput): [Tag!] tagsCount(where: TagWhereInput! = {}): Int keystone: KeystoneMeta! - authenticatedItem: AuthenticatedItem } -union AuthenticatedItem = User - type KeystoneMeta { adminMeta: KeystoneAdminMeta! } @@ -327,23 +266,25 @@ type KeystoneAdminMeta { type KeystoneAdminUIListMeta { key: String! - itemQueryName: String! - listQueryName: String! - hideCreate: Boolean! - hideDelete: Boolean! path: String! label: String! singular: String! plural: String! description: String - initialColumns: [String!]! pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! groups: [KeystoneAdminUIFieldGroupMeta!]! + graphql: KeystoneAdminUIGraphQL! + initialColumns: [String!]! + initialSearchFields: [String!]! initialSort: KeystoneAdminUISort - isHidden: Boolean! isSingleton: Boolean! + hideCreate: Boolean! + hideDelete: Boolean! + isHidden: Boolean! + itemQueryName: String! + listQueryName: String! } type KeystoneAdminUIFieldMeta { @@ -413,6 +354,33 @@ type KeystoneAdminUIFieldGroupMeta { fields: [KeystoneAdminUIFieldMeta!]! } +type KeystoneAdminUIGraphQL { + names: KeystoneAdminUIGraphQLNames! +} + +type KeystoneAdminUIGraphQLNames { + outputTypeName: String! + whereInputName: String! + whereUniqueInputName: String! + createInputName: String! + createMutationName: String! + createManyMutationName: String! + relateToOneForCreateInputName: String! + relateToManyForCreateInputName: String! + itemQueryName: String! + listOrderName: String! + listQueryCountName: String! + listQueryName: String! + updateInputName: String! + updateMutationName: String! + updateManyInputName: String! + updateManyMutationName: String! + relateToOneForUpdateInputName: String! + relateToManyForUpdateInputName: String! + deleteMutationName: String! + deleteManyMutationName: String! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/better-list-search/schema.prisma b/examples/better-list-search/schema.prisma new file mode 100644 index 00000000000..51653e5f76e --- /dev/null +++ b/examples/better-list-search/schema.prisma @@ -0,0 +1,37 @@ +// This file is automatically generated by Keystone, do not modify it manually. +// Modify your Keystone config when you want to change this. + +datasource sqlite { + url = env("DATABASE_URL") + shadowDatabaseUrl = env("SHADOW_DATABASE_URL") + provider = "sqlite" +} + +generator client { + provider = "prisma-client-js" + output = "node_modules/myprisma" +} + +model Post { + id String @id @default(cuid()) + title String @default("") + author Author? @relation("Post_author", fields: [authorId], references: [id]) + authorId String? @map("author") + related Post[] @relation("Post_related") + tags Tag[] @relation("Post_tags") + from_Post_related Post[] @relation("Post_related") + + @@index([authorId]) +} + +model Author { + id String @id @default(cuid()) + name String @default("") + from_Post_author Post[] @relation("Post_author") +} + +model Tag { + id String @id @default(cuid()) + name String @default("") + posts Post[] @relation("Post_tags") +} diff --git a/examples/better-list-search/schema.ts b/examples/better-list-search/schema.ts new file mode 100644 index 00000000000..306899db27d --- /dev/null +++ b/examples/better-list-search/schema.ts @@ -0,0 +1,66 @@ +import { list } from '@keystone-6/core' +import { allowAll } from '@keystone-6/core/access' + +import { text, relationship } from '@keystone-6/core/fields' +import type { Lists } from '.keystone/types' + +export const lists = { + Post: list({ + // WARNING - for this example, anyone can create, query, update and delete anything + access: allowAll, + + fields: { + title: text({ validation: { isRequired: true } }), + author: relationship({ + ref: 'Author', + }), + related: relationship({ + ref: 'Post', + many: true, + }), + tags: relationship({ + ref: 'Tag.posts', + many: true, + }), + }, + + ui: { + searchFields: [ + 'title', + 'author', // WARNING: results in searching by post.*.name + 'tags', // this is quite powerful, but may load your database + ] + } + }), + + Author: list({ + // WARNING - for this example, anyone can create, query, update and delete anything + access: allowAll, + + fields: { + name: text({ validation: { isRequired: true } }), + }, + + ui: { + searchFields: [ + 'name', + ] + } + }), + + Tag: list({ + // WARNING - for this example, anyone can create, query, update and delete anything + access: allowAll, + + fields: { + name: text({ validation: { isRequired: true } }), + posts: relationship({ ref: 'Post.tags', many: true }), + }, + + ui: { + searchFields: [ + 'name', + ] + } + }), +} satisfies Lists diff --git a/examples/cloudinary/keystone.ts b/examples/cloudinary/keystone.ts index b9b7b9e731a..47af4414568 100644 --- a/examples/cloudinary/keystone.ts +++ b/examples/cloudinary/keystone.ts @@ -1,6 +1,5 @@ import 'dotenv/config' import { config } from '@keystone-6/core' -import { fixPrismaPath } from '../example-utils' import { lists } from './schema' export default config({ @@ -9,7 +8,7 @@ export default config({ url: process.env.DATABASE_URL || 'file:./keystone-example.db', // WARNING: this is only needed for our monorepo examples, dont do this - ...fixPrismaPath, + prismaClientPath: 'node_modules/myprisma', }, lists, }) diff --git a/examples/cloudinary/package.json b/examples/cloudinary/package.json index 657c808cba4..7885f2bb759 100644 --- a/examples/cloudinary/package.json +++ b/examples/cloudinary/package.json @@ -1,6 +1,6 @@ { "name": "@keystone-6/example-cloudinary", - "version": "0.0.2", + "version": null, "private": true, "license": "MIT", "scripts": { @@ -10,14 +10,14 @@ "postinstall": "keystone postinstall" }, "dependencies": { - "@keystone-6/cloudinary": "workspace:^", - "@keystone-6/auth": "workspace:^", - "@keystone-6/core": "workspace:^", - "@prisma/client": "catalog:", + "@keystone-6/cloudinary": "^8.0.0", + "@keystone-6/auth": "^8.1.0", + "@keystone-6/core": "^6.3.1", + "@prisma/client": "5.19.0", "dotenv": "^16.0.0" }, "devDependencies": { - "prisma": "catalog:", - "typescript": "catalog:" + "prisma": "5.19.0", + "typescript": "^5.5.0" } } diff --git a/examples/cloudinary/schema.graphql b/examples/cloudinary/schema.graphql index 3f72e019b47..6f88003e940 100644 --- a/examples/cloudinary/schema.graphql +++ b/examples/cloudinary/schema.graphql @@ -154,8 +154,8 @@ type Mutation { } type Query { - posts(where: PostWhereInput! = {}, orderBy: [PostOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: PostWhereUniqueInput): [Post!] post(where: PostWhereUniqueInput!): Post + posts(where: PostWhereInput! = {}, orderBy: [PostOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: PostWhereUniqueInput): [Post!] postsCount(where: PostWhereInput! = {}): Int keystone: KeystoneMeta! } @@ -171,23 +171,25 @@ type KeystoneAdminMeta { type KeystoneAdminUIListMeta { key: String! - itemQueryName: String! - listQueryName: String! - hideCreate: Boolean! - hideDelete: Boolean! path: String! label: String! singular: String! plural: String! description: String - initialColumns: [String!]! pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! groups: [KeystoneAdminUIFieldGroupMeta!]! + graphql: KeystoneAdminUIGraphQL! + initialColumns: [String!]! + initialSearchFields: [String!]! initialSort: KeystoneAdminUISort - isHidden: Boolean! isSingleton: Boolean! + hideCreate: Boolean! + hideDelete: Boolean! + isHidden: Boolean! + itemQueryName: String! + listQueryName: String! } type KeystoneAdminUIFieldMeta { @@ -257,6 +259,33 @@ type KeystoneAdminUIFieldGroupMeta { fields: [KeystoneAdminUIFieldMeta!]! } +type KeystoneAdminUIGraphQL { + names: KeystoneAdminUIGraphQLNames! +} + +type KeystoneAdminUIGraphQLNames { + outputTypeName: String! + whereInputName: String! + whereUniqueInputName: String! + createInputName: String! + createMutationName: String! + createManyMutationName: String! + relateToOneForCreateInputName: String! + relateToManyForCreateInputName: String! + itemQueryName: String! + listOrderName: String! + listQueryCountName: String! + listQueryName: String! + updateInputName: String! + updateMutationName: String! + updateManyInputName: String! + updateManyMutationName: String! + relateToOneForUpdateInputName: String! + relateToManyForUpdateInputName: String! + deleteMutationName: String! + deleteManyMutationName: String! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/cloudinary/schema.prisma b/examples/cloudinary/schema.prisma index f49a2dda8b2..d48f2c78e4a 100644 --- a/examples/cloudinary/schema.prisma +++ b/examples/cloudinary/schema.prisma @@ -9,7 +9,7 @@ datasource sqlite { generator client { provider = "prisma-client-js" - output = "node_modules/.myprisma/client" + output = "node_modules/myprisma" } model Post { diff --git a/examples/custom-admin-ui-logo/app/(admin)/.admin/index.tsx b/examples/custom-admin-ui-logo/app/(admin)/.admin/index.tsx new file mode 100644 index 00000000000..5dd11446daf --- /dev/null +++ b/examples/custom-admin-ui-logo/app/(admin)/.admin/index.tsx @@ -0,0 +1,18 @@ +/* eslint-disable */ +import * as view0 from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/id-field-view' +import * as view1 from '@keystone-6/core/fields/types/text/views' +import * as view2 from '@keystone-6/core/fields/types/select/views' +import * as view3 from '@keystone-6/core/fields/types/checkbox/views' +import * as view4 from '@keystone-6/core/fields/types/relationship/views' +import * as view5 from '@keystone-6/core/fields/types/timestamp/views' + +import * as adminConfig from '../config' + +export const config = { + lazyMetadataQuery: {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"keystone","loc":{"start":22,"end":30}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"adminMeta","loc":{"start":39,"end":48}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lists","loc":{"start":59,"end":64}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key","loc":{"start":77,"end":80}},"arguments":[],"directives":[],"loc":{"start":77,"end":80}},{"kind":"Field","name":{"kind":"Name","value":"isHidden","loc":{"start":91,"end":99}},"arguments":[],"directives":[],"loc":{"start":91,"end":99}},{"kind":"Field","name":{"kind":"Name","value":"fields","loc":{"start":110,"end":116}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"path","loc":{"start":131,"end":135}},"arguments":[],"directives":[],"loc":{"start":131,"end":135}},{"kind":"Field","name":{"kind":"Name","value":"createView","loc":{"start":148,"end":158}},"arguments":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fieldMode","loc":{"start":175,"end":184}},"arguments":[],"directives":[],"loc":{"start":175,"end":184}}],"loc":{"start":159,"end":198}},"loc":{"start":148,"end":198}}],"loc":{"start":117,"end":210}},"loc":{"start":110,"end":210}}],"loc":{"start":65,"end":220}},"loc":{"start":59,"end":220}}],"loc":{"start":49,"end":228}},"loc":{"start":39,"end":228}}],"loc":{"start":31,"end":234}},"loc":{"start":22,"end":234}}]}}]}, + fieldViews: [view0,view1,view2,view3,view4,view5], + adminMetaHash: '1mrsjib', + adminConfig, + apiPath: '/api/graphql', + adminPath: '', +}; diff --git a/examples/custom-admin-ui-logo/app/(admin)/[listKey]/[id]/page.tsx b/examples/custom-admin-ui-logo/app/(admin)/[listKey]/[id]/page.tsx new file mode 100644 index 00000000000..c5d7ea2be62 --- /dev/null +++ b/examples/custom-admin-ui-logo/app/(admin)/[listKey]/[id]/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { ItemPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage' + +export default ItemPage diff --git a/examples/custom-admin-ui-logo/app/(admin)/[listKey]/create/page.tsx b/examples/custom-admin-ui-logo/app/(admin)/[listKey]/create/page.tsx new file mode 100644 index 00000000000..d6042acaa96 --- /dev/null +++ b/examples/custom-admin-ui-logo/app/(admin)/[listKey]/create/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { CreateItemPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/CreateItemPage' + +export default CreateItemPage diff --git a/examples/custom-admin-ui-logo/app/(admin)/[listKey]/page.tsx b/examples/custom-admin-ui-logo/app/(admin)/[listKey]/page.tsx new file mode 100644 index 00000000000..f6e75f8cfab --- /dev/null +++ b/examples/custom-admin-ui-logo/app/(admin)/[listKey]/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { ListPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage' + +export default ListPage diff --git a/examples/custom-admin-ui-logo/app/(admin)/config.tsx b/examples/custom-admin-ui-logo/app/(admin)/config.tsx new file mode 100644 index 00000000000..f2b986e29ca --- /dev/null +++ b/examples/custom-admin-ui-logo/app/(admin)/config.tsx @@ -0,0 +1 @@ +export { components } from '../config' \ No newline at end of file diff --git a/examples/custom-admin-ui-logo/app/(admin)/layout.tsx b/examples/custom-admin-ui-logo/app/(admin)/layout.tsx new file mode 100644 index 00000000000..abb5a0f3b2c --- /dev/null +++ b/examples/custom-admin-ui-logo/app/(admin)/layout.tsx @@ -0,0 +1,16 @@ +'use client' +import { Layout } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/App' +import { config } from './.admin' + + +export default function AdminLayout ({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/examples/custom-admin-ui-logo/app/(admin)/no-access/page.tsx b/examples/custom-admin-ui-logo/app/(admin)/no-access/page.tsx new file mode 100644 index 00000000000..70877231fee --- /dev/null +++ b/examples/custom-admin-ui-logo/app/(admin)/no-access/page.tsx @@ -0,0 +1,4 @@ +'use client' +import { getNoAccessPage } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/NoAccessPage' + +export default getNoAccessPage({ sessionsEnabled: false }) diff --git a/examples/custom-admin-ui-logo/app/(admin)/page.tsx b/examples/custom-admin-ui-logo/app/(admin)/page.tsx new file mode 100644 index 00000000000..5c268390b0f --- /dev/null +++ b/examples/custom-admin-ui-logo/app/(admin)/page.tsx @@ -0,0 +1,2 @@ +'use client' +export { HomePage as default } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/HomePage' diff --git a/examples/custom-admin-ui-logo/admin/components/CustomLogo.tsx b/examples/custom-admin-ui-logo/app/config/components/CustomLogo.tsx similarity index 94% rename from examples/custom-admin-ui-logo/admin/components/CustomLogo.tsx rename to examples/custom-admin-ui-logo/app/config/components/CustomLogo.tsx index 1a7994f2b29..f8ce3fc3b7a 100644 --- a/examples/custom-admin-ui-logo/admin/components/CustomLogo.tsx +++ b/examples/custom-admin-ui-logo/app/config/components/CustomLogo.tsx @@ -3,7 +3,7 @@ import Link from 'next/link' import { jsx, H3 } from '@keystone-ui/core' -export const CustomLogo = () => { +export function CustomLogo () { return ( + {children} +
{value || 'no value'}
{props.content}
{error}
❤️ Thank you for subscribing!
❤️ Thank you! Please check your email to confirm your subscription.