├── .nvmrc ├── .gitignore ├── test ├── src │ ├── app │ │ ├── (payload) │ │ │ ├── custom.scss │ │ │ ├── api │ │ │ │ ├── graphql │ │ │ │ │ └── route.ts │ │ │ │ ├── graphql-playground │ │ │ │ │ └── route.ts │ │ │ │ └── [...slug] │ │ │ │ │ └── route.ts │ │ │ ├── admin │ │ │ │ └── [[...segments]] │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── not-found.tsx │ │ │ └── layout.tsx │ │ ├── my-route │ │ │ └── route.ts │ │ └── (app) │ │ │ ├── layout.tsx │ │ │ ├── globals.scss │ │ │ └── page.tsx │ ├── components │ │ └── Example.tsx │ └── scripts │ │ └── standalone-script.ts ├── .vscode │ ├── extensions.json │ ├── launch.json │ └── settings.json ├── public │ ├── payload │ │ ├── og-image.png │ │ ├── SuisseIntl.woff │ │ ├── SuisseIntl.woff2 │ │ ├── SuisseIntl-Bold.woff │ │ ├── SuisseIntl-Bold.woff2 │ │ ├── SuisseIntl-Medium.woff │ │ ├── SuisseIntl-Medium.woff2 │ │ ├── SuisseIntl-SemiBold.woff │ │ ├── SuisseIntl-SemiBold.woff2 │ │ ├── merriweather-v30-latin-900.woff │ │ ├── merriweather-v30-latin-900.woff2 │ │ ├── merriweather-v30-latin-italic.woff │ │ ├── merriweather-v30-latin-italic.woff2 │ │ ├── merriweather-v30-latin-regular.woff │ │ ├── merriweather-v30-latin-regular.woff2 │ │ ├── merriweather-v30-latin-900italic.woff │ │ ├── merriweather-v30-latin-900italic.woff2 │ │ └── favicon.svg │ ├── vercel.svg │ └── next.svg ├── .env.example ├── next.config.mjs ├── .gitignore ├── seed.ts ├── start-database.sh ├── tsconfig.json ├── CHANGELOG.md ├── package.json ├── payload.config.ts ├── payload-types.ts └── README.md ├── plugin ├── src │ ├── components │ │ ├── Dialog │ │ │ ├── index.ts │ │ │ └── Dialog.tsx │ │ └── CollectionDocsOrder │ │ │ ├── index.ts │ │ │ └── CollectionDocsOrderButton.tsx │ ├── index.ts │ ├── handlers │ │ ├── types.ts │ │ ├── saveChanges.client.ts │ │ └── saveChanges.ts │ ├── defaultAccess.ts │ ├── types.ts │ ├── hooks │ │ └── incrementOrder.ts │ ├── translations │ │ └── index.ts │ ├── plugin.ts │ ├── extendCollectionsConfig.ts │ └── styles.scss ├── .swcrc ├── .gitignore ├── tsconfig.json └── package.json ├── pnpm-workspace.yaml ├── .prettierrc.mjs ├── .vscode └── settings.json ├── .github └── workflows │ └── publish.yaml ├── package.json ├── .eslintrc.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.12.2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /test/src/app/(payload)/custom.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugin/src/components/Dialog/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Dialog'; 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'plugin' 3 | - 'test' 4 | -------------------------------------------------------------------------------- /plugin/src/components/CollectionDocsOrder/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CollectionDocsOrderButton'; 2 | -------------------------------------------------------------------------------- /test/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /test/public/payload/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r1tsuu/payload-plugin-collections-docs-order/HEAD/test/public/payload/og-image.png -------------------------------------------------------------------------------- /test/public/payload/SuisseIntl.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r1tsuu/payload-plugin-collections-docs-order/HEAD/test/public/payload/SuisseIntl.woff -------------------------------------------------------------------------------- /test/public/payload/SuisseIntl.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r1tsuu/payload-plugin-collections-docs-order/HEAD/test/public/payload/SuisseIntl.woff2 -------------------------------------------------------------------------------- /test/.env.example: -------------------------------------------------------------------------------- 1 | PAYLOAD_SECRET=jawliejfilwajefSEANlawefawfewag349jwgo3gj4w 2 | MONGODB_URI=mongodb://127.0.0.1:27017/translator 3 | PAYLOAD_DROP_DATABASE=true -------------------------------------------------------------------------------- /test/public/payload/SuisseIntl-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r1tsuu/payload-plugin-collections-docs-order/HEAD/test/public/payload/SuisseIntl-Bold.woff -------------------------------------------------------------------------------- /test/public/payload/SuisseIntl-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r1tsuu/payload-plugin-collections-docs-order/HEAD/test/public/payload/SuisseIntl-Bold.woff2 -------------------------------------------------------------------------------- /test/public/payload/SuisseIntl-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r1tsuu/payload-plugin-collections-docs-order/HEAD/test/public/payload/SuisseIntl-Medium.woff -------------------------------------------------------------------------------- /test/public/payload/SuisseIntl-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r1tsuu/payload-plugin-collections-docs-order/HEAD/test/public/payload/SuisseIntl-Medium.woff2 -------------------------------------------------------------------------------- /test/public/payload/SuisseIntl-SemiBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r1tsuu/payload-plugin-collections-docs-order/HEAD/test/public/payload/SuisseIntl-SemiBold.woff -------------------------------------------------------------------------------- /test/public/payload/SuisseIntl-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r1tsuu/payload-plugin-collections-docs-order/HEAD/test/public/payload/SuisseIntl-SemiBold.woff2 -------------------------------------------------------------------------------- /test/public/payload/merriweather-v30-latin-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r1tsuu/payload-plugin-collections-docs-order/HEAD/test/public/payload/merriweather-v30-latin-900.woff -------------------------------------------------------------------------------- /test/public/payload/merriweather-v30-latin-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r1tsuu/payload-plugin-collections-docs-order/HEAD/test/public/payload/merriweather-v30-latin-900.woff2 -------------------------------------------------------------------------------- /test/public/payload/merriweather-v30-latin-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r1tsuu/payload-plugin-collections-docs-order/HEAD/test/public/payload/merriweather-v30-latin-italic.woff -------------------------------------------------------------------------------- /test/public/payload/merriweather-v30-latin-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r1tsuu/payload-plugin-collections-docs-order/HEAD/test/public/payload/merriweather-v30-latin-italic.woff2 -------------------------------------------------------------------------------- /test/public/payload/merriweather-v30-latin-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r1tsuu/payload-plugin-collections-docs-order/HEAD/test/public/payload/merriweather-v30-latin-regular.woff -------------------------------------------------------------------------------- /test/public/payload/merriweather-v30-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r1tsuu/payload-plugin-collections-docs-order/HEAD/test/public/payload/merriweather-v30-latin-regular.woff2 -------------------------------------------------------------------------------- /test/public/payload/merriweather-v30-latin-900italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r1tsuu/payload-plugin-collections-docs-order/HEAD/test/public/payload/merriweather-v30-latin-900italic.woff -------------------------------------------------------------------------------- /test/public/payload/merriweather-v30-latin-900italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r1tsuu/payload-plugin-collections-docs-order/HEAD/test/public/payload/merriweather-v30-latin-900italic.woff2 -------------------------------------------------------------------------------- /plugin/src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/padding-line-between-statements */ 2 | export { collectionsDocsOrderPlugin } from './plugin'; 3 | export type { PluginOptions } from './types'; 4 | -------------------------------------------------------------------------------- /.prettierrc.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | printWidth: 100, 3 | semi: true, 4 | singleQuote: true, 5 | trailingComma: "all", 6 | jsxSingleQuote: true, 7 | plugins: ["prettier-plugin-css-order"], 8 | }; 9 | -------------------------------------------------------------------------------- /plugin/src/handlers/types.ts: -------------------------------------------------------------------------------- 1 | import type { z } from 'zod'; 2 | 3 | import type { schema } from './saveChanges'; 4 | 5 | export type SaveChangesArgs = z.infer; 6 | 7 | export type SaveChangesResult = { 8 | success: boolean; 9 | }; 10 | -------------------------------------------------------------------------------- /test/next.config.mjs: -------------------------------------------------------------------------------- 1 | import { withPayload } from '@payloadcms/next/withPayload'; 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = { 5 | // Your Next.js config here 6 | 7 | }; 8 | 9 | export default withPayload(nextConfig); 10 | -------------------------------------------------------------------------------- /plugin/src/defaultAccess.ts: -------------------------------------------------------------------------------- 1 | import type { PayloadRequest } from 'payload/types'; 2 | 3 | import type { SaveChangesArgs } from './handlers/types'; 4 | 5 | export const defaultAccess = ({ req }: { data: SaveChangesArgs; req: PayloadRequest }) => { 6 | return !!req.user; 7 | }; 8 | -------------------------------------------------------------------------------- /test/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "command": "pnpm run dev", 5 | "cwd": "${workspaceFolder}", 6 | "name": "Run Dev (pnpm)", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ], 11 | "version": "0.2.0" 12 | } 13 | -------------------------------------------------------------------------------- /test/src/app/(payload)/api/graphql/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY it because it could be re-written at any time. */ 3 | import config from '@payload-config'; 4 | import { GRAPHQL_POST } from '@payloadcms/next/routes'; 5 | 6 | export const POST = GRAPHQL_POST(config); 7 | -------------------------------------------------------------------------------- /plugin/.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/swcrc", 3 | "sourceMaps": true, 4 | "jsc": { 5 | "target": "esnext", 6 | "parser": { 7 | "syntax": "typescript", 8 | "tsx": true, 9 | "dts": true 10 | } 11 | }, 12 | "module": { 13 | "type": "es6" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/src/app/(payload)/api/graphql-playground/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY it because it could be re-written at any time. */ 3 | import config from '@payload-config'; 4 | import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'; 5 | 6 | export const GET = GRAPHQL_PLAYGROUND_GET(config); 7 | -------------------------------------------------------------------------------- /test/src/app/my-route/route.ts: -------------------------------------------------------------------------------- 1 | import configPromise from '@payload-config'; 2 | import { getPayload } from 'payload'; 3 | 4 | export const GET = async () => { 5 | const payload = await getPayload({ 6 | config: configPromise, 7 | }); 8 | 9 | const data = await payload.find({ 10 | collection: 'users', 11 | }); 12 | 13 | return Response.json(data); 14 | }; 15 | -------------------------------------------------------------------------------- /plugin/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /test/src/components/Example.tsx: -------------------------------------------------------------------------------- 1 | import configPromise from '@payload-config'; 2 | import { getPayload } from 'payload'; 3 | import React from 'react'; 4 | 5 | const Example: React.FC = async () => { 6 | const payload = await getPayload({ config: configPromise }); 7 | 8 | const url = payload.getAdminURL(); 9 | 10 | return
The admin panel is running at: {url}
; 11 | }; 12 | 13 | export default Example; 14 | -------------------------------------------------------------------------------- /test/src/app/(app)/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.scss'; 2 | 3 | import React from 'react'; 4 | 5 | /* Our app sits here to not cause any conflicts with payload's root layout */ 6 | const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => { 7 | return ( 8 | 9 | 10 |
{children}
11 | 12 | 13 | ); 14 | }; 15 | 16 | export default Layout; 17 | -------------------------------------------------------------------------------- /test/src/app/(payload)/api/[...slug]/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY it because it could be re-written at any time. */ 3 | import config from '@payload-config'; 4 | import { REST_DELETE, REST_GET, REST_PATCH, REST_POST } from '@payloadcms/next/routes'; 5 | 6 | export const GET = REST_GET(config); 7 | 8 | export const POST = REST_POST(config); 9 | 10 | export const DELETE = REST_DELETE(config); 11 | 12 | export const PATCH = REST_PATCH(config); 13 | -------------------------------------------------------------------------------- /plugin/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { PayloadRequest } from 'payload/types'; 2 | 3 | import type { SaveChangesArgs } from './handlers/types'; 4 | 5 | export type PluginCollectionConfig = { 6 | slug: string; 7 | }; 8 | 9 | export type PluginOptions = { 10 | access?: (args: { data: SaveChangesArgs; req: PayloadRequest }) => Promise | boolean; 11 | collections: PluginCollectionConfig[]; 12 | /** 13 | * Enable or disable the plugin 14 | * @default false 15 | */ 16 | enabled?: boolean; 17 | }; 18 | -------------------------------------------------------------------------------- /test/src/scripts/standalone-script.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | 3 | import { getPayload } from 'payload'; 4 | import { importConfig } from 'payload/node'; 5 | 6 | async function run() { 7 | const awaitedConfig = await importConfig('../../payload.config.ts'); 8 | 9 | const payload = await getPayload({ config: awaitedConfig }); 10 | 11 | const pages = await payload.find({ 12 | collection: 'pages', 13 | }); 14 | 15 | console.log(pages); 16 | process.exit(0); 17 | } 18 | 19 | run().catch(console.error); 20 | -------------------------------------------------------------------------------- /test/public/payload/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "npm.packageManager": "pnpm", 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": "explicit" 5 | }, 6 | "eslint.validate": ["javascript", "javascriptreact", "html", "typescriptreact", "typescript"], 7 | "editor.defaultFormatter": "esbenp.prettier-vscode", 8 | "editor.formatOnSave": true, 9 | "editor.formatOnPaste": false, 10 | "prettier.useEditorConfig": false, 11 | "prettier.useTabs": false, 12 | "prettier.configPath": ".prettierrc.mjs", 13 | "typescript.tsdk": "node_modules/typescript/lib" 14 | } -------------------------------------------------------------------------------- /test/src/app/(payload)/admin/[[...segments]]/page.tsx: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | import config from '@payload-config'; 3 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 4 | import { RootPage } from '@payloadcms/next/views'; 5 | 6 | type Args = { 7 | params: { 8 | segments: string[]; 9 | }; 10 | searchParams: { 11 | [key: string]: string | string[]; 12 | }; 13 | }; 14 | 15 | const Page = ({ params, searchParams }: Args) => RootPage({ config, params, searchParams }); 16 | 17 | export default Page; 18 | -------------------------------------------------------------------------------- /test/src/app/(payload)/layout.tsx: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import '@payloadcms/next/css'; 4 | import './custom.scss'; 5 | 6 | import configPromise from '@payload-config'; 7 | import { RootLayout } from '@payloadcms/next/layouts'; 8 | import React from 'react'; 9 | 10 | type Args = { 11 | children: React.ReactNode; 12 | }; 13 | 14 | const Layout = ({ children }: Args) => {children}; 15 | 16 | export default Layout; 17 | -------------------------------------------------------------------------------- /plugin/src/handlers/saveChanges.client.ts: -------------------------------------------------------------------------------- 1 | import type { SaveChangesArgs, SaveChangesResult } from './types'; 2 | 3 | export const saveChanges = async ({ 4 | api, 5 | args, 6 | }: { 7 | api: string; 8 | args: SaveChangesArgs; 9 | }): Promise => { 10 | const response = await fetch(`${api}/collection-docs-order/save`, { 11 | body: JSON.stringify(args), 12 | credentials: 'include', 13 | headers: { 14 | 'Content-Type': 'application/json', 15 | }, 16 | method: 'post', 17 | }); 18 | 19 | if (!response.ok) return { success: false }; 20 | 21 | return response.json(); 22 | }; 23 | -------------------------------------------------------------------------------- /test/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | /.idea/* 10 | !/.idea/runConfigurations 11 | 12 | # testing 13 | /coverage 14 | 15 | # next.js 16 | /.next/ 17 | /out/ 18 | 19 | # production 20 | /build 21 | 22 | # misc 23 | .DS_Store 24 | *.pem 25 | 26 | # debug 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | next-env.d.ts 40 | 41 | .env 42 | 43 | /media 44 | -------------------------------------------------------------------------------- /plugin/src/hooks/incrementOrder.ts: -------------------------------------------------------------------------------- 1 | import type { CollectionBeforeValidateHook } from 'payload/types'; 2 | 3 | export const incrementOrder: CollectionBeforeValidateHook = async ({ 4 | collection, 5 | data, 6 | operation, 7 | req, 8 | }) => { 9 | if (operation === 'update') return; 10 | 11 | const { 12 | docs: [lastByOrder], 13 | } = await req.payload.find({ 14 | collection: collection.slug, 15 | req, 16 | sort: '-docOrder', 17 | }); 18 | 19 | return { 20 | ...data, 21 | docOrder: 22 | lastByOrder?.docOrder && typeof lastByOrder.docOrder === 'number' 23 | ? lastByOrder.docOrder + 1 24 | : 1, 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /test/src/app/(payload)/admin/[[...segments]]/not-found.tsx: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | import config from '@payload-config'; 3 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 4 | import { generatePageMetadata,NotFoundPage } from '@payloadcms/next/views'; 5 | import type { Metadata } from 'next'; 6 | 7 | type Args = { 8 | params: { 9 | segments: string[]; 10 | }; 11 | searchParams: { 12 | [key: string]: string | string[]; 13 | }; 14 | }; 15 | 16 | export const generateMetadata = ({ params, searchParams }: Args): Promise => 17 | generatePageMetadata({ config, params, searchParams }); 18 | 19 | const NotFound = ({ params, searchParams }: Args) => NotFoundPage({ config, params, searchParams }); 20 | 21 | export default NotFound; 22 | -------------------------------------------------------------------------------- /test/src/app/(app)/globals.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | font-family: 'Roboto', 'Inter', sans-serif; 4 | } 5 | 6 | .container { 7 | margin-right: auto; 8 | margin-left: auto; 9 | padding: 0 2rem; 10 | max-width: 37.5rem; 11 | } 12 | 13 | h1 { 14 | font-size: 4rem; 15 | } 16 | 17 | .rainbow { 18 | animation: rainbow_animation 6s ease-in-out infinite; 19 | background: linear-gradient(to right, #6666ff, #0099ff, #00ff00, #ff3399, #6666ff); 20 | background-size: 400% 100%; 21 | -webkit-background-clip: text; 22 | background-clip: text; 23 | color: transparent; 24 | font-family: monospace; 25 | letter-spacing: 5px; 26 | } 27 | 28 | @keyframes rainbow_animation { 29 | 0%, 30 | 100% { 31 | background-position: 0 0; 32 | } 33 | 34 | 50% { 35 | background-position: 100% 0; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/seed.ts: -------------------------------------------------------------------------------- 1 | import type { Payload } from 'payload'; 2 | 3 | // thanks chatgprt 4 | const randomWords = [ 5 | 'Elephant', 6 | 'Sunshine', 7 | 'Galaxy', 8 | 'Bicycle', 9 | 'Dragon', 10 | 'Adventure', 11 | 'Symphony', 12 | 'Pineapple', 13 | 'Wanderlust', 14 | 'Moonlight', 15 | 'Chocolate', 16 | 'Serendipity', 17 | 'Thunderstorm', 18 | 'Velvet', 19 | 'Kaleidoscope', 20 | 'Bubblegum', 21 | 'Enigma', 22 | 'Waterfall', 23 | 'Firefly', 24 | 'Rainbow', 25 | 'Cobweb', 26 | 'Whirlwind', 27 | 'Marshmallow', 28 | 'Stardust', 29 | 'Tornado', 30 | 'Meadow', 31 | 'Mirage', 32 | 'Saffron', 33 | 'Zephyr', 34 | 'Blizzard', 35 | ]; 36 | 37 | export const seed = async (payload: Payload) => { 38 | payload.logger.info('Seeding examples...'); 39 | for (const title of randomWords) { 40 | await payload.create({ collection: 'examples', data: { title } }); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /plugin/src/translations/index.ts: -------------------------------------------------------------------------------- 1 | export const translations = { 2 | en: { 3 | pluginCollectionsDocsOrder: { 4 | asc: 'Asceding', 5 | desc: 'Descending', 6 | error: 'Unknown server error', 7 | loadMore: 'Load more', 8 | loaded: 'Loaded', 9 | loading: 'Loading...', 10 | save: 'Save', 11 | sortItems: 'Sort items order', 12 | sortOrder: 'Sort order', 13 | success: 'Successfully modified', 14 | }, 15 | }, 16 | uk: { 17 | pluginCollectionsDocsOrder: { 18 | asc: 'Зростання', 19 | desc: 'Спадання', 20 | error: 'Невідома помилка серверу', 21 | loadMore: 'Завантажити більше', 22 | loaded: 'Завантажено', 23 | loading: 'Завантаження...', 24 | save: 'Зберегти', 25 | sortItems: 'Налаштування порядку виведення', 26 | sortOrder: 'Сортування', 27 | success: 'Успішно відреаговано', 28 | }, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Release package 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | 14 | - uses: pnpm/action-setup@v2 15 | name: Install pnpm 16 | with: 17 | version: 8 18 | run_install: false 19 | 20 | - name: Install Node.js 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version-file: '.nvmrc' 24 | cache: pnpm 25 | registry-url: https://registry.npmjs.org 26 | 27 | - name: Install dependencies 28 | run: pnpm install --frozen-lockfile 29 | 30 | - name: Publish 🚀 31 | shell: bash 32 | run: | 33 | cd plugin 34 | pnpm publish --no-git-checks 35 | env: 36 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 37 | -------------------------------------------------------------------------------- /test/start-database.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | DB_CONTAINER_NAME="next-payload-3" 4 | 5 | if ! [ -x "$(command -v docker)" ]; then 6 | echo "Docker is not installed. Please install docker and try again.\nDocker install guide: https://docs.docker.com/engine/install/" 7 | exit 1 8 | fi 9 | 10 | if [ "$(docker ps -q -f name=$DB_CONTAINER_NAME)" ]; then 11 | docker start $DB_CONTAINER_NAME 12 | echo "Database container started" 13 | exit 0 14 | fi 15 | 16 | set -a 17 | source .env 18 | 19 | DB_PASSWORD=$(echo $DATABASE_URL | awk -F':' '{print $3}' | awk -F'@' '{print $1}') 20 | 21 | if [ "$DB_PASSWORD" = "password" ]; then 22 | echo "You are using the default database password" 23 | fi 24 | 25 | docker run --name $DB_CONTAINER_NAME -e POSTGRES_PASSWORD=$DB_PASSWORD -e POSTGRES_HOST_AUTH_METHOD=trust -e POSTGRES_DB=next-payload-3 -d -p 5432:5432 docker.io/postgres 26 | 27 | echo "Database container was successfully created" 28 | 29 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"], 23 | "@payload-config": ["./payload.config.ts"], 24 | "@plugin/*": ["../plugin/src/*"], 25 | "@plugin": ["../plugin/src/main.ts"] 26 | } 27 | }, 28 | "include": [ 29 | "next-env.d.ts", 30 | "**/*.ts", 31 | "**/*.tsx", 32 | ".next/types/**/*.ts", 33 | "../plugin/src/**/*.ts", 34 | "../plugin/src/**/*.tsx" 35 | ], 36 | "exclude": ["node_modules"] 37 | } 38 | -------------------------------------------------------------------------------- /plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, // Make sure typescript knows that this module depends on their references 4 | "noEmit": false /* Do not emit outputs. */, 5 | "emitDeclarationOnly": true, 6 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 7 | "rootDir": "./src" /* Specify the root folder within your source files. */, 8 | "declaration": true, 9 | "declarationMap": true, 10 | "allowJs": true, 11 | "checkJs": false, 12 | "esModuleInterop": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "jsx": "preserve", 15 | "lib": ["dom", "dom.iterable", "esnext"], 16 | "skipLibCheck": true, 17 | "moduleResolution": "Bundler", 18 | "module": "ES6", 19 | "sourceMap": true, 20 | "strict": false, 21 | "incremental": true, 22 | "isolatedModules": true 23 | }, 24 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"], 25 | "exclude": ["dist", "build", "node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /test/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # alpha.42 2 | 3 | **We're now pre-compiling CSS for faster startup times** 4 | 5 | We are beginning to optimize the compilation and dev startup performance of Payload 3.0, and the first thing we've done is pre-compiled all SCSS that is required by the Payload admin UI. 6 | 7 | If you started before `alpha.42` and update to this version, you'll see that no styles are loaded. To fix this, take a look at the `/src/app/(payload)/layout.tsx` file within this repo, and note that this file has a new CSS import within it to load all CSS required by the Payload admin panel. 8 | 9 | Simply add this import to your `(payload)/layout.tsx` file, and you'll be back in business. 10 | 11 | **We now load all required Payload static files from your Next.js `public` folder** 12 | 13 | In addition to pre-compiling SCSS, we are also shipping static files through the Next.js `/public` folder which will cut down on compilation time as well. Make sure you copy the `public/payload` folder into your repo as well as add the CSS import above. 14 | -------------------------------------------------------------------------------- /plugin/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import './styles.scss'; 2 | 3 | import type { Plugin } from 'payload/config'; 4 | import { deepMerge } from 'payload/utilities'; 5 | 6 | import { defaultAccess } from './defaultAccess'; 7 | import { extendCollectionsConfig } from './extendCollectionsConfig'; 8 | import { saveChanges } from './handlers/saveChanges'; 9 | import { translations } from './translations'; 10 | import type { PluginOptions } from './types'; 11 | 12 | export const collectionsDocsOrderPlugin = 13 | (pluginOptions: PluginOptions): Plugin => 14 | (config) => { 15 | if (pluginOptions.enabled === false) { 16 | return config; 17 | } 18 | 19 | if (config.collections) { 20 | config.collections = extendCollectionsConfig(config.collections, pluginOptions); 21 | } 22 | 23 | config.endpoints = [ 24 | ...(config.endpoints || []), 25 | { 26 | handler: saveChanges({ access: pluginOptions.access ?? defaultAccess }), 27 | method: 'post', 28 | path: '/collection-docs-order/save', 29 | }, 30 | ]; 31 | 32 | config.i18n = { 33 | ...config.i18n, 34 | translations: { 35 | ...deepMerge(translations, config.i18n?.translations), 36 | }, 37 | }; 38 | 39 | return config; 40 | }; 41 | -------------------------------------------------------------------------------- /plugin/src/handlers/saveChanges.ts: -------------------------------------------------------------------------------- 1 | import type { PayloadHandler } from 'payload/config'; 2 | import { Forbidden } from 'payload/errors'; 3 | import { z } from 'zod'; 4 | 5 | import type { PluginOptions } from '../types'; 6 | 7 | export const schema = z.object({ 8 | collection: z.string(), 9 | docs: z.array( 10 | z.object({ 11 | id: z.number().or(z.string()), 12 | modifiedTo: z.number(), 13 | }), 14 | ), 15 | }); 16 | 17 | export const saveChanges = 18 | ({ access }: { access: NonNullable }): PayloadHandler => 19 | async (req) => { 20 | const result = schema.safeParse(await req.json()); 21 | 22 | if (!result.success) return Response.json({ errors: result.error.errors }, { status: 400 }); 23 | 24 | const allowed = await access({ data: result.data, req }); 25 | 26 | if (!allowed) throw new Forbidden(); 27 | 28 | const { 29 | data: { collection, docs }, 30 | } = result; 31 | 32 | await Promise.all( 33 | docs.map((doc) => { 34 | req.payload.update({ 35 | collection, 36 | data: { 37 | docOrder: doc.modifiedTo, 38 | }, 39 | id: doc.id, 40 | req, 41 | }); 42 | }), 43 | ); 44 | 45 | return Response.json({ success: true }); 46 | }; 47 | -------------------------------------------------------------------------------- /test/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "npm.packageManager": "pnpm", 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "[typescript]": { 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "editor.formatOnSave": true, 7 | "editor.codeActionsOnSave": { 8 | "source.fixAll.eslint": "explicit" 9 | } 10 | }, 11 | "[typescriptreact]": { 12 | "editor.defaultFormatter": "esbenp.prettier-vscode", 13 | "editor.formatOnSave": true, 14 | "editor.codeActionsOnSave": { 15 | "source.fixAll.eslint": "explicit" 16 | } 17 | }, 18 | "[javascript]": { 19 | "editor.defaultFormatter": "esbenp.prettier-vscode", 20 | "editor.formatOnSave": true, 21 | "editor.codeActionsOnSave": { 22 | "source.fixAll.eslint": "explicit" 23 | } 24 | }, 25 | "[json]": { 26 | "editor.defaultFormatter": "esbenp.prettier-vscode", 27 | "editor.formatOnSave": true 28 | }, 29 | "[jsonc]": { 30 | "editor.defaultFormatter": "esbenp.prettier-vscode", 31 | "editor.formatOnSave": true 32 | }, 33 | "editor.formatOnSaveMode": "file", 34 | "eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }], 35 | "typescript.tsdk": "node_modules/typescript/lib", 36 | "[javascript][typescript][typescriptreact]": { 37 | "editor.codeActionsOnSave": { 38 | "source.fixAll.eslint": "explicit" 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorepo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "pnpm --filter test dev", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "devDependencies": { 11 | "@next/eslint-plugin-next": "^14.1.4", 12 | "@swc/cli": "^0.1.62", 13 | "@swc/core": "^1.4.16", 14 | "@typescript-eslint/eslint-plugin": "^6.7.5", 15 | "@typescript-eslint/parser": "^6.7.5", 16 | "copyfiles": "^2.4.1", 17 | "eslint": "^8.57.0", 18 | "eslint-config-next": "14.1.0", 19 | "eslint-plugin-import": "^2.28.1", 20 | "eslint-plugin-perfectionist": "^2.1.0", 21 | "eslint-plugin-simple-import-sort": "^10.0.0", 22 | "prettier": "^3.2.5", 23 | "prettier-plugin-css-order": "^2.0.1", 24 | "rimraf": "^5.0.5", 25 | "typescript": "^5.4.2", 26 | "@payloadcms/db-mongodb": "3.0.0-beta.18", 27 | "@payloadcms/db-postgres": "3.0.0-beta.18", 28 | "@payloadcms/next": "3.0.0-beta.18", 29 | "@payloadcms/richtext-lexical": "3.0.0-beta.18", 30 | "@payloadcms/richtext-slate": "3.0.0-beta.18", 31 | "@payloadcms/ui": "3.0.0-beta.18", 32 | "@payloadcms/translations": "3.0.0-beta.18", 33 | "payload": "3.0.0-beta.18", 34 | "react": "^18.2.0", 35 | "react-dom": "^18.2.0" 36 | }, 37 | "keywords": [], 38 | "author": "", 39 | "license": "ISC" 40 | } 41 | -------------------------------------------------------------------------------- /plugin/src/extendCollectionsConfig.ts: -------------------------------------------------------------------------------- 1 | import type { CollectionConfig } from 'payload/types'; 2 | 3 | import { CollectionDocsOrderButton } from './components/CollectionDocsOrder'; 4 | import { incrementOrder } from './hooks/incrementOrder'; 5 | import type { PluginOptions } from './types'; 6 | 7 | const externdCollectionConfig = (collection: CollectionConfig) => { 8 | return { 9 | ...collection, 10 | admin: { 11 | ...(collection.admin ?? {}), 12 | components: { 13 | ...(collection.admin?.components ?? {}), 14 | BeforeList: [ 15 | ...(collection.admin?.components?.BeforeList ?? []), 16 | CollectionDocsOrderButton, 17 | ], 18 | }, 19 | }, 20 | fields: [ 21 | ...collection.fields, 22 | { 23 | access: { 24 | create: () => false, 25 | read: () => true, 26 | update: () => false, 27 | }, 28 | index: true, 29 | name: 'docOrder', 30 | type: 'number', 31 | }, 32 | ], 33 | hooks: { 34 | ...(collection.hooks ?? {}), 35 | beforeValidate: [...(collection.hooks?.beforeValidate ?? []), incrementOrder], 36 | }, 37 | } as CollectionConfig; 38 | }; 39 | 40 | export const extendCollectionsConfig = ( 41 | incomingCollections: CollectionConfig[], 42 | { collections }: PluginOptions, 43 | ) => { 44 | return incomingCollections.map((collection) => { 45 | const foundInConfig = collections.some(({ slug }) => slug === collection.slug); 46 | 47 | if (!foundInConfig) return collection; 48 | 49 | return externdCollectionConfig(collection); 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /plugin/src/components/Dialog/Dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as RadixDialog from '@radix-ui/react-dialog'; 2 | import clsx from 'clsx'; 3 | import type { ReactNode } from 'react'; 4 | import React from 'react'; 5 | 6 | type Props = { 7 | className?: string; 8 | trigger?: ReactNode; 9 | } & RadixDialog.DialogProps; 10 | 11 | export const Dialog = ({ children, className, trigger, ...dialogProps }: Props) => { 12 | return ( 13 | 14 | {trigger && {trigger}} 15 | 16 | 17 |
18 | 19 |
20 | 21 | 29 | 30 | 31 | 32 | {children} 33 |
34 |
35 |
36 |
37 |
38 |
39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "cross-env NODE_OPTIONS=--no-deprecation next dev", 8 | "devsafe": "rm -rf .next && cross-env NODE_OPTIONS=--no-deprecation next dev", 9 | "build": "cross-env NODE_OPTIONS=--no-deprecation next build", 10 | "payload": "cross-env NODE_OPTIONS=--no-deprecation payload", 11 | "start": "cross-env NODE_OPTIONS=--no-deprecation next start", 12 | "lint": "cross-env NODE_OPTIONS=--no-deprecation next lint", 13 | "generate:types": "payload generate:types", 14 | "standalone-script": "tsx ./src/scripts/standalone-script.ts" 15 | }, 16 | "engines": { 17 | "node": ">=18.19.0" 18 | }, 19 | "dependencies": { 20 | "cross-env": "^7.0.3", 21 | "next": "14.2.0-canary.23", 22 | "sharp": "0.32.6" 23 | }, 24 | "devDependencies": { 25 | "payload": "../node_modules/payload", 26 | "@payloadcms/db-mongodb": "../node_modules/@payloadcms/db-mongodb", 27 | "@payloadcms/db-postgres": "../node_modules/@payloadcms/db-postgres", 28 | "@payloadcms/next": "../node_modules/@payloadcms/next", 29 | "@payloadcms/richtext-lexical": "../node_modules/@payloadcms/richtext-lexical", 30 | "@payloadcms/richtext-slate": "../node_modules/@payloadcms/richtext-slate", 31 | "@payloadcms/ui": "../node_modules/@payloadcms/ui", 32 | "@payloadcms/translations": "../node_modules/@payloadcms/translations", 33 | "@types/node": "^20.11.25", 34 | "@types/react": "^18.2.64", 35 | "@types/react-dom": "^18.2.21", 36 | "dotenv": "^16.4.5", 37 | "tsx": "^4.7.1", 38 | "typescript": "^5.4.2" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/src/app/(app)/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import React from 'react'; 3 | 4 | import Example from '@/components/Example'; 5 | 6 | const Page = () => { 7 | return ( 8 |
9 |

10 | Payload 3.0 BETA! 11 |

12 |

13 | This BETA is rapidly evolving, you can report any bugs against{' '} 14 | 15 | the repo 16 | {' '} 17 | or in the{' '} 18 | 22 | dedicated channel in Discord 23 | 24 | . 25 |

26 | 27 |

28 | 29 | Payload is running at /admin 30 | 31 |

32 | 33 |

34 | 35 | /my-route 36 | {' '} 37 | contains an example of a custom route running the Local API. 38 |

39 | 40 | 41 | 42 |

You can use the Local API in your server components like this:

43 |
44 |         
45 |           {`import { getPayload } from 'payload'
46 | import configPromise from "@payload-config";
47 | const payload = await getPayload({ config: configPromise })
48 | 
49 | const data = await payload.find({
50 |   collection: 'posts',
51 | })`}
52 |         
53 |       
54 |
55 | ); 56 | }; 57 | 58 | export default Page; 59 | -------------------------------------------------------------------------------- /plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@r1tsu/payload-plugin-collections-docs-order", 3 | "private": false, 4 | "version": "1.0.0-beta.6", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "pnpm copyfiles && pnpm build:swc && pnpm build:types", 9 | "build:swc": "swc ./src -d ./dist --config-file .swcrc", 10 | "build:types": "tsc --emitDeclarationOnly --outDir dist", 11 | "clean": "rimraf dist", 12 | "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/", 13 | "prepublishOnly": "pnpm clean && pnpm build" 14 | }, 15 | "peerDependencies": { 16 | "@payloadcms/ui": "^3.0.0-beta.18", 17 | "payload": "^3.0.0-beta.18" 18 | }, 19 | "devDependencies": { 20 | "@payloadcms/ui": "../node_modules/@payloadcms/ui", 21 | "@types/react": "^18.2.64", 22 | "payload": "../node_modules/payload", 23 | "react": "../node_modules/react", 24 | "typescript": "^5.4.2" 25 | }, 26 | "types": "./src/index.ts", 27 | "main": "./src/index.ts", 28 | "files": [ 29 | "dist" 30 | ], 31 | "exports": { 32 | ".": { 33 | "import": "./src/index.ts", 34 | "require": "./src/index.ts", 35 | "types": "./src/index.ts" 36 | } 37 | }, 38 | "publishConfig": { 39 | "exports": { 40 | ".": { 41 | "import": "./dist/index.js", 42 | "require": "./dist/index.js", 43 | "types": "./dist/index.d.ts" 44 | } 45 | }, 46 | "main": "./dist/index.js", 47 | "registry": "https://registry.npmjs.org/", 48 | "types": "./dist/index.d.ts" 49 | }, 50 | "dependencies": { 51 | "@radix-ui/react-dialog": "^1.0.5", 52 | "clsx": "^2.1.0", 53 | "zod": "^3.23.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /plugin/src/styles.scss: -------------------------------------------------------------------------------- 1 | .collection-docs-order { 2 | display: flex; 3 | justify-content: flex-end; 4 | padding-bottom: 10px; 5 | button { 6 | margin: 0; 7 | } 8 | } 9 | 10 | .collection-docs-order-content { 11 | display: flex; 12 | flex-direction: column; 13 | gap: 25px; 14 | overflow-x: visible; 15 | } 16 | 17 | .order-list { 18 | display: flex; 19 | flex-direction: column; 20 | gap: 10px; 21 | padding-right: 25px; 22 | max-height: 600px; 23 | overflow-y: auto; 24 | } 25 | 26 | .order-item { 27 | display: flex; 28 | background: var(--theme-elevation-50); 29 | padding: var(--toggle-pad-v) var(--toggle-pad-h); 30 | --toggle-pad-h: 1.4423076923rem; 31 | --toggle-pad-v: 0.9615384615rem; 32 | gap: 10px; 33 | border-radius: 5px; 34 | } 35 | 36 | .order-buttons { 37 | display: flex; 38 | flex-direction: column; 39 | gap: 10px; 40 | } 41 | 42 | .order-buttons button { 43 | margin: 0; 44 | width: fit-content; 45 | } 46 | 47 | .dialog-overlay { 48 | display: flex; 49 | position: fixed; 50 | top: 0; 51 | right: 0; 52 | bottom: 0; 53 | left: 0; 54 | justify-content: center; 55 | z-index: 30; 56 | background-size: contain; 57 | background-repeat: no-repeat; 58 | background-color: rgba(0, 0, 0, 0.7); 59 | padding: 5rem 3rem; 60 | min-width: 100%; 61 | overflow: auto; 62 | } 63 | 64 | .dialog-content { 65 | position: relative; 66 | background-color: var(--theme-bg); 67 | padding: 2rem; 68 | width: 100%; 69 | overflow: auto; 70 | } 71 | 72 | .dialog-content .close { 73 | display: flex; 74 | position: absolute; 75 | right: 2rem; 76 | justify-content: center; 77 | align-items: center; 78 | width: 30px; 79 | height: 30px; 80 | 81 | svg { 82 | width: 30px; 83 | height: 30px; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:@typescript-eslint/recommended", 4 | "eslint-config-next", 5 | "plugin:perfectionist/recommended-natural" 6 | ], 7 | "parser": "@typescript-eslint/parser", 8 | "plugins": ["@typescript-eslint", "simple-import-sort", "import"], 9 | "rules": { 10 | "@typescript-eslint/consistent-type-definitions": ["error", "type"], 11 | "@typescript-eslint/consistent-type-imports": [ 12 | "error", 13 | { 14 | "fixStyle": "separate-type-imports" 15 | } 16 | ], 17 | "@typescript-eslint/no-explicit-any": "off", 18 | "@typescript-eslint/no-unused-vars": "error", 19 | "@typescript-eslint/padding-line-between-statements": [ 20 | "warn", 21 | { 22 | "blankLine": "always", 23 | "next": "*", 24 | "prev": ["const", "let", "var"] 25 | }, 26 | { 27 | "blankLine": "always", 28 | "next": ["return", "function", "class"], 29 | "prev": "*" 30 | }, 31 | { 32 | "blankLine": "always", 33 | "next": "*", 34 | "prev": ["function", "class"] 35 | }, 36 | { 37 | "blankLine": "always", 38 | "next": "export", 39 | "prev": "*" 40 | }, 41 | { 42 | "blankLine": "always", 43 | "next": "*", 44 | "prev": "multiline-const" 45 | } 46 | ], 47 | "import/first": "error", 48 | "import/newline-after-import": "error", 49 | "import/no-duplicates": "error", 50 | "no-unused-vars": "off", 51 | "padding-line-between-statements": "off", 52 | "perfectionist/sort-imports": "off", 53 | "prefer-const": "error", 54 | "require-await": "error", 55 | "simple-import-sort/exports": "warn", 56 | "simple-import-sort/imports": "warn", 57 | "typescript-eslint/explicit-function-return-type": "off", 58 | "perfectionist/sort-named-imports": "off", 59 | "@typescript-eslint/ban-ts-comment": "error" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/payload.config.ts: -------------------------------------------------------------------------------- 1 | import '@plugin/styles.scss'; 2 | 3 | import { mongooseAdapter } from '@payloadcms/db-mongodb'; 4 | import { lexicalEditor } from '@payloadcms/richtext-lexical'; 5 | import { collectionsDocsOrderPlugin } from '@plugin/index'; 6 | import path from 'path'; 7 | import { buildConfig } from 'payload/config'; 8 | import { en } from 'payload/i18n/en'; 9 | import { fileURLToPath } from 'url'; 10 | 11 | import { seed } from './seed'; 12 | 13 | const filename = fileURLToPath(import.meta.url); 14 | 15 | const dirname = path.dirname(filename); 16 | 17 | export default buildConfig({ 18 | admin: { 19 | autoLogin: { 20 | email: 'dev@payloadcms.com', 21 | password: 'test', 22 | }, 23 | }, 24 | collections: [ 25 | { 26 | admin: { 27 | useAsTitle: 'title', 28 | }, 29 | fields: [ 30 | { 31 | name: 'title', 32 | type: 'text', 33 | }, 34 | ], 35 | slug: 'examples', 36 | }, 37 | ], 38 | db: mongooseAdapter({ 39 | url: process.env.MONGODB_URI || '', 40 | }), 41 | editor: lexicalEditor({}), 42 | i18n: { 43 | supportedLanguages: { en }, 44 | }, 45 | localization: { 46 | defaultLocale: 'en', 47 | fallback: false, 48 | locales: ['en', 'de', 'pl'], 49 | }, 50 | async onInit(payload) { 51 | const existingUsers = await payload.find({ 52 | collection: 'users', 53 | limit: 1, 54 | }); 55 | 56 | if (existingUsers.docs.length === 0) { 57 | await payload.create({ 58 | collection: 'users', 59 | data: { 60 | email: 'dev@payloadcms.com', 61 | password: 'test', 62 | }, 63 | }); 64 | } 65 | 66 | const existingExamples = await payload.find({ collection: 'examples', limit: 1 }); 67 | 68 | if (existingExamples.docs.length === 0) await seed(payload); 69 | }, 70 | plugins: [ 71 | collectionsDocsOrderPlugin({ 72 | collections: [{ slug: 'examples' }], 73 | }), 74 | ], 75 | secret: process.env.PAYLOAD_SECRET || '', 76 | typescript: { 77 | outputFile: path.resolve(dirname, 'payload-types.ts'), 78 | }, 79 | }); 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Note! 2 | This repository for 3.0 is deprecated and has been moved to https://github.com/r1tsuu/payload-enchants in order to keep all of my payload-related packages in 1 place. 3 | [2.0 branch](https://github.com/r1tsuu/payload-plugin-collections-docs-order/tree/2.0) 4 | 5 | # Payload Plugin Collections Docs Order for Payload 3.0 6 | 7 | ## About 8 | 9 | Adds an option to re-order collection documents with drag n drop (almost like array/blocks items). Then on your front end you can query documents with applied sort by `docOrder` field. 10 | 11 | ## Video 12 | 13 | https://github.com/r1tsuu/payload-plugin-collections-docs-order/assets/64744993/2c13cdd9-f809-4c40-82c6-0b6f78997f74 14 | 15 | ## Install 16 | 17 | `pnpm add @r1tsu/payload-plugin-collections-docs-order@1.0.0-beta.6` 18 | In your payload.config.ts: 19 | 20 | ```ts 21 | /// .... 22 | import { collectionsDocsOrderPlugin } from '@r1tsu/payload-plugin-collections-docs-order'; 23 | 24 | export default buildConfig({ 25 | // ... 26 | plugins: [ 27 | collectionsDocsOrderPlugin({ 28 | collections: [{ slug: 'pages' }], // The feature will be enabled only for collections that are in this array., 29 | access: ({ req, data }) => { 30 | // Optional, configure access for `saveChanges` endpoint, default: Boolean(req.user) 31 | return req.user?.collection === 'admins'; 32 | }, 33 | }), 34 | ], 35 | }); 36 | ``` 37 | 38 | ## Querying with applied plugin's sort. 39 | 40 | REST: 41 | 42 | ```ts 43 | fetch('http://localhost:3000/api/examples?sort=docOrder').then((res) => res.json()); 44 | ``` 45 | 46 | Local API: 47 | 48 | ```ts 49 | payload.find({ collection: 'examples', sort: 'docOrder' }); 50 | ``` 51 | 52 | GraphQL: 53 | 54 | ```graphql 55 | query { 56 | Examples(sort: "docOrder") { 57 | docs { 58 | title 59 | } 60 | } 61 | 62 | ``` 63 | 64 | ## Script to setup for collections that had documents before installing the plugin 65 | 66 | 1. Create folder named cli in your project's root 67 | 2. Copy this file to the created folder and update `collections` array with your needs. https://gist.github.com/r1tsuu/047008be9800dfcbe371247d10ee6794 68 | 3. Run the file like that: `yarn ts-node --project ./tsconfig.server.json ./cli/pluginCollectionsDocsSetup.ts` (It will run for a database that in your .env, also be sure to backup if this on production) 69 | -------------------------------------------------------------------------------- /test/payload-types.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * This file was automatically generated by Payload. 5 | * DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config, 6 | * and re-run `payload generate:types` to regenerate this file. 7 | */ 8 | 9 | export interface Config { 10 | collections: { 11 | pages: Page; 12 | media: Media; 13 | users: User; 14 | 'payload-preferences': PayloadPreference; 15 | 'payload-migrations': PayloadMigration; 16 | examples: Examples; 17 | }; 18 | globals: {}; 19 | } 20 | /** 21 | * This interface was referenced by `Config`'s JSON-Schema 22 | * via the `definition` "pages". 23 | */ 24 | export interface Page { 25 | id: string; 26 | title?: string | null; 27 | content?: { 28 | root: { 29 | type: string; 30 | children: { 31 | type: string; 32 | version: number; 33 | [k: string]: unknown; 34 | }[]; 35 | direction: ('ltr' | 'rtl') | null; 36 | format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | ''; 37 | indent: number; 38 | version: number; 39 | }; 40 | [k: string]: unknown; 41 | } | null; 42 | updatedAt: string; 43 | createdAt: string; 44 | } 45 | /** 46 | * This interface was referenced by `Config`'s JSON-Schema 47 | * via the `definition` "media". 48 | */ 49 | export interface Media { 50 | id: string; 51 | text?: string | null; 52 | updatedAt: string; 53 | createdAt: string; 54 | url?: string | null; 55 | thumbnailURL?: string | null; 56 | filename?: string | null; 57 | mimeType?: string | null; 58 | filesize?: number | null; 59 | width?: number | null; 60 | height?: number | null; 61 | } 62 | 63 | export interface Examples { 64 | id: string; 65 | title?: string | null; 66 | updatedAt: string; 67 | createdAt: string; 68 | } 69 | /** 70 | * This interface was referenced by `Config`'s JSON-Schema 71 | * via the `definition` "users". 72 | */ 73 | export interface User { 74 | id: string; 75 | updatedAt: string; 76 | createdAt: string; 77 | email: string; 78 | resetPasswordToken?: string | null; 79 | resetPasswordExpiration?: string | null; 80 | salt?: string | null; 81 | hash?: string | null; 82 | loginAttempts?: number | null; 83 | lockUntil?: string | null; 84 | password?: string | null; 85 | } 86 | /** 87 | * This interface was referenced by `Config`'s JSON-Schema 88 | * via the `definition` "payload-preferences". 89 | */ 90 | export interface PayloadPreference { 91 | id: string; 92 | user: { 93 | relationTo: 'users'; 94 | value: string | User; 95 | }; 96 | key?: string | null; 97 | value?: 98 | | { 99 | [k: string]: unknown; 100 | } 101 | | unknown[] 102 | | string 103 | | number 104 | | boolean 105 | | null; 106 | updatedAt: string; 107 | createdAt: string; 108 | } 109 | /** 110 | * This interface was referenced by `Config`'s JSON-Schema 111 | * via the `definition` "payload-migrations". 112 | */ 113 | export interface PayloadMigration { 114 | id: string; 115 | name?: string | null; 116 | batch?: number | null; 117 | updatedAt: string; 118 | createdAt: string; 119 | } 120 | 121 | declare module 'payload' { 122 | export interface GeneratedTypes extends Config {} 123 | } 124 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Payload 3.0 Beta Demo 2 | 3 | This repo showcases a demo of the Payload 3.0 Beta running completely within Next.js. 4 | 5 | > [!IMPORTANT] 6 | > It's extremely important to note that as of now, this demo contains BETA software and you are 100% guaranteed to run into bugs / weird stuff. 7 | > 8 | > We're actively working toward a stable release as fast as we possibly can. 9 | 10 | ### Highlights 11 | 12 | 1. Payload is now Next.js-native 13 | 1. Turbopack works out of the box (this will get faster over time, expect more here) 14 | 1. The Payload admin UI is built with React Server Components and automatically eliminates server-side code from your admin bundle, completely alleviating the need to use Webpack aliases to remove hooks, access control, etc. 15 | 1. Payload is now fully-ESM across the board 16 | 1. GraphQL is now initialized only when you hit the GraphQL endpoint, and does not affect overhead of REST API routes 17 | 1. All UI components have been abstracted into a separate `@payloadcms/ui` package, which will be fully documented and exposed for your re-use once we hit stable 3.0 or before 18 | 1. You can run your own Next.js site alongside of Payload in the same app 19 | 1. You can now deploy Payload to Vercel, and there will be official support for Vercel Blob Storage coming soon (so no S3 needed for files) 20 | 1. Server-side HMR works out of the box, with no need for `nodemon` or similar. When the Payload config changes, your app will automatically re-initialize Payload seamlessly in the background 21 | 1. All custom React components can be server components by default, and you can decide if you want them to be server components or client components 22 | 1. Sharp has been abstracted to be an optional dependency 23 | 1. Payload now relies on the Web Request / Response APIs rather than the Node Request / Response 24 | 1. Express can still be used with Next.js' Custom Server functionality 25 | 1. Payload itself has slimmed down significantly and can now be fully portable, run anywhere. You can leverage the Payload Local API completely outside of Next.js if you want. 26 | 1. The data layer, including the shape of the database Payload used and the API responses in 2.0, has not been affected whatsoever 27 | 28 | ### Work to come 29 | 30 | We are making this available to our community so that we can gather your feedback and test the new approach that Payload is taking. Don't expect it to be fully functional yet. There are some things that we are aware of that are not yet completed, but we're going to keep blazing through the remaining items as fast as we can to reach stable 3.0 as quickly and efficiently as possible. Here are a few of the items that we are still working on (not a full list): 31 | 32 | 1. Documentation 33 | 1. Vercel Blob Storage adapter 34 | 1. Lots of bugs for sure 35 | 1. 100% of tests passing 36 | 1. Compiler speed improvements (turbo is beta still, it is slower than it should be. it will get faster) 37 | 1. Overall speed improvements 38 | 1. An install script to be able to install Payload easily into any existing Next.js app 39 | 1. A full list of breaking changes for 2.0 -> 3.0, including an in-depth migration guide 40 | 41 | ### Existing Nextjs project 42 | 43 | You can install Payload into your existing Nextjs project using this command: 44 | ``` 45 | npx create-payload-app@beta 46 | ``` 47 | Contents from `src/app` will have to be moved into a new directory `src/app/(app)` so that Payload's root layout and routes can remain isolated from the rest of your app. 48 | 49 | ### Using this repo 50 | 51 | To try out this repo yourself, follow the steps below: 52 | 53 | 1. Clone the repo to your computer (`git clone git@github.com:payloadcms/payload-3.0-demo.git`) 54 | 2. `cd` into the new folder by running `cd ./payload-3.0-demo` 55 | 3. Copy the `.env.example` by running `cp .env.example .env` in the repo, then fill out the values including the connection string to your DB 56 | 4. Install dependencies with whatever package manager you use (`yarn`, `npm install`, `pnpm i`, etc.) 57 | 5. Start your database. For local postgresql use `.\start-database.sh` to start it in docker container. 58 | 6. Fire it up (`yarn dev`, `npm run dev`, `pnpm dev`, etc.) 59 | 7. Visit https://localhost:3000 and log in with the user created within the config's `onInit` method 60 | 61 | ### Follow along with breaking changes 62 | 63 | There is a possibility that we will make breaking changes before releasing the full stable version of Payload 3.0. 64 | 65 | **To follow along with breaking changes in advance of the full, stable release,** you can keep an eye on the [CHANGELOG.md](https://github.com/payloadcms/payload-3.0-demo/blob/main/CHANGELOG.md). 66 | 67 | ### Technical details 68 | 69 | **The app folder** 70 | 71 | You'll see that Payload requires a few files to be present in your `/app` folder. There are files for the admin UI as well as files for all route handlers. We've consolidated all admin views into a single `page.tsx` and consolidated most of the REST endpoints into a single `route.ts` file for simplicity, but also for development performance. With this pattern, you only have to compile the admin UI / REST API / GraphQL API a single time - and from there, it will be lightning-fast. 72 | 73 | **The `next.config.js` `withPayload` function** 74 | 75 | You'll see in the Next.js config that we have a `withPayload` function installed. This function is required for Payload to operate, and it ensures compatibility with packages that Payload needs such as `drizzle-kit`, `sharp`, `pino`, and `mongodb`. 76 | 77 | **Using a TypeScript alias to point to your Payload config** 78 | 79 | In the `tsconfig.json` within this repo, you'll see that we have `paths` set up to point `@payload-config` to the Payload config, which is located in the root. You can put your config wherever you want. By default, the `page.tsx` files and `route.ts` files within the `/app` folder use this alias. In the future, we might make it optional to use `paths` - and by default, we might just hard-code relative path imports to the config. We would like to hear your feedback on this part. What do you prefer? Use `paths` or just use relative imports? 80 | 81 | --- 82 | 83 | ### Find a bug? 84 | 85 | Open an issue on this repo at `https://github.com/payloadcms/payload-3.0-demo` with as much detail as you can provide and we will tackle them as fast as we can. Let's get stable! 86 | -------------------------------------------------------------------------------- /plugin/src/components/CollectionDocsOrder/CollectionDocsOrderButton.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Button, toast } from '@payloadcms/ui/elements'; 4 | import { DraggableSortable } from '@payloadcms/ui/elements/DraggableSortable'; 5 | import { DraggableSortableItem } from '@payloadcms/ui/elements/DraggableSortable/DraggableSortableItem'; 6 | import { Radio } from '@payloadcms/ui/fields/RadioGroup/Radio'; 7 | import { DragHandle } from '@payloadcms/ui/icons/DragHandle'; 8 | import { useConfig } from '@payloadcms/ui/providers/Config'; 9 | import { useListInfo } from '@payloadcms/ui/providers/ListInfo'; 10 | import { useLocale } from '@payloadcms/ui/providers/Locale'; 11 | import { useTranslation } from '@payloadcms/ui/providers/Translation'; 12 | import type { PaginatedDocs } from 'payload/database'; 13 | import React, { useCallback, useEffect, useState } from 'react'; 14 | 15 | import { saveChanges } from '../../handlers/saveChanges.client'; 16 | import { Dialog } from '../Dialog'; 17 | 18 | type Doc = { 19 | docOrder: number; 20 | id: number | string; 21 | modifiedFrom?: number; 22 | modifiedTo?: number; 23 | } & Record; 24 | 25 | const CollectionDocsOrderContent = () => { 26 | const { collections, routes } = useConfig(); 27 | 28 | const { t } = useTranslation(); 29 | 30 | const { code: locale } = useLocale(); 31 | 32 | const { collectionSlug } = useListInfo(); 33 | 34 | const limit = 25; 35 | 36 | const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc'); 37 | 38 | const [data, setData] = useState<{ 39 | docs: Doc[]; 40 | hasNextPage: boolean; 41 | isLoading: boolean; 42 | loadedPages: number; 43 | totalDocs: number; 44 | }>({ 45 | docs: [], 46 | hasNextPage: false, 47 | isLoading: true, 48 | loadedPages: 0, 49 | totalDocs: 0, 50 | }); 51 | 52 | const hasSave = data.docs.some( 53 | (doc) => typeof doc.modifiedTo === 'number' && doc.modifiedTo !== doc.docOrder, 54 | ); 55 | 56 | const sort = `sort=${sortOrder === 'desc' ? '-' : ''}docOrder`; 57 | 58 | const getInitalData = useCallback(async () => { 59 | const res = await fetch( 60 | `${routes.api}/${collectionSlug}?${sort}&limit=${limit}&locale=${locale}&depth=0`, 61 | ); 62 | 63 | const { docs, hasNextPage, totalDocs } = await res.json(); 64 | 65 | return setData({ 66 | docs, 67 | hasNextPage, 68 | isLoading: false, 69 | loadedPages: 1, 70 | totalDocs, 71 | }); 72 | }, [collectionSlug, locale, routes, sort]); 73 | 74 | useEffect(() => { 75 | if (collectionSlug) getInitalData(); 76 | }, [getInitalData, collectionSlug]); 77 | 78 | const collectionConfig = collections.find((collection) => collection.slug === collectionSlug); 79 | 80 | if (!collectionConfig) return null; 81 | 82 | const useAsTitle = collectionConfig.admin.useAsTitle; 83 | 84 | const moveRow = (moveFromIndex: number, moveToIndex: number) => { 85 | setData((prev) => { 86 | const prevDocs = [...prev.docs]; 87 | 88 | const newDocs = [...prev.docs]; 89 | 90 | const [movedItem] = newDocs.splice(moveFromIndex, 1); 91 | 92 | newDocs.splice(moveToIndex, 0, movedItem); 93 | 94 | return { 95 | ...prev, 96 | docs: newDocs.map((doc, index) => { 97 | if (prevDocs[index].id !== doc.id) { 98 | return { 99 | ...doc, 100 | modifiedTo: prevDocs[index].modifiedTo ?? prevDocs[index].docOrder, 101 | }; 102 | } 103 | 104 | return doc; 105 | }), 106 | }; 107 | }); 108 | }; 109 | 110 | const save = async () => { 111 | const modifiedDocsData = data.docs 112 | .filter((doc) => typeof doc.modifiedTo === 'number' && doc.modifiedTo !== doc.docOrder) 113 | .map((doc) => ({ 114 | id: doc.id, 115 | modifiedTo: doc.modifiedTo, 116 | })); 117 | 118 | if (!collectionSlug || !modifiedDocsData) return; 119 | 120 | const { success } = await saveChanges({ 121 | api: routes.api, 122 | args: { 123 | collection: collectionSlug, 124 | docs: modifiedDocsData as { id: number | string; modifiedTo: number }[], 125 | }, 126 | }); 127 | 128 | if (success) { 129 | setData((prev) => ({ ...prev, isLoading: true })); 130 | await getInitalData(); 131 | toast.success(t('pluginCollectionsDocsOrder:success'), { 132 | position: 'bottom-center', 133 | }); 134 | } else { 135 | toast.success(t('pluginCollectionsDocsOrder:error'), { 136 | position: 'bottom-center', 137 | }); 138 | } 139 | // toast.error(t("pluginCollectionsDocsOrder:error"), { 140 | // position: "bottom-center", 141 | }; 142 | 143 | const loadMore = () => { 144 | setData((prev) => ({ ...prev, isLoading: true })); 145 | 146 | return fetch( 147 | `${routes.api}/${collectionSlug}?${sort}&limit=${limit}&page=${data.loadedPages + 1}&depth=0&locale=${locale}`, 148 | ) 149 | .then((res) => res.json()) 150 | .then(({ docs, hasNextPage }: PaginatedDocs) => 151 | setData((prev) => ({ 152 | docs: [...prev.docs, ...docs], 153 | hasNextPage, 154 | isLoading: false, 155 | loadedPages: prev.loadedPages + 1, 156 | totalDocs: prev.totalDocs, 157 | })), 158 | ); 159 | }; 160 | 161 | const handleSortOrderChange = (order: 'asc' | 'desc') => { 162 | setSortOrder(order); 163 | setData((prev) => ({ ...prev, isLoading: true })); 164 | }; 165 | 166 | return ( 167 |
168 |
169 | handleSortOrderChange('asc')} 173 | option={{ 174 | label: t('pluginCollectionsDocsOrder:asc'), 175 | value: 'asc', 176 | }} 177 | path='asc' 178 | /> 179 | handleSortOrderChange('desc')} 183 | option={{ 184 | label: t('pluginCollectionsDocsOrder:desc'), 185 | value: 'desc', 186 | }} 187 | path='desc' 188 | /> 189 |
190 | String(doc.id))} 193 | onDragEnd={({ moveFromIndex, moveToIndex }) => moveRow(moveFromIndex, moveToIndex)} 194 | > 195 | {data.docs.map((doc) => ( 196 | 197 | {(props) => { 198 | return ( 199 | 222 | ); 223 | }} 224 | 225 | ))} 226 | 227 |
228 | {data.isLoading 229 | ? 'Loading' 230 | : `${t('pluginCollectionsDocsOrder:loaded')} ${data.docs.length}/${data.totalDocs}`} 231 | {hasSave && } 232 | {data.hasNextPage && ( 233 | 234 | )} 235 |
236 |
237 | ); 238 | }; 239 | 240 | export const CollectionDocsOrderButton = () => { 241 | const { t } = useTranslation(); 242 | 243 | // const params = useParams(); 244 | 245 | // console.log(params); 246 | 247 | return ( 248 |
249 | {t('pluginCollectionsDocsOrder:sortItems')}} 251 | > 252 | 253 | {/* */} 254 | 255 |
256 | ); 257 | }; 258 | --------------------------------------------------------------------------------