Compare commits
1 Commits
feat/sorta
...
chore/grap
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7e036e1b8 |
@@ -70,7 +70,6 @@ The following options are available:
|
||||
| `fields` * | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). |
|
||||
| `graphQL` | Manage GraphQL-related properties for this collection. [More](#graphql) |
|
||||
| `hooks` | Entry point for Hooks. [More details](../hooks/overview#collection-hooks). |
|
||||
| `orderable` | If true, enables custom ordering for the collection, and documents can be reordered via drag and drop. Uses [fractional indexing](https://observablehq.com/@dgreensp/implementing-fractional-indexing) for efficient reordering. |
|
||||
| `labels` | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
|
||||
| `lockDocuments` | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). |
|
||||
| `slug` * | Unique, URL-friendly string that will act as an identifier for this Collection. |
|
||||
|
||||
@@ -27,19 +27,19 @@ Then build your Custom Provider as follows:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import React, { createContext, use } from 'react'
|
||||
import React, { createContext, useContext } from 'react'
|
||||
|
||||
const MyCustomContext = React.createContext(myCustomValue)
|
||||
|
||||
export function MyProvider({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<MyCustomContext value={myCustomValue}>
|
||||
<MyCustomContext.Provider value={myCustomValue}>
|
||||
{children}
|
||||
</MyCustomContext>
|
||||
</MyCustomContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useMyCustomContext = () => use(MyCustomContext)
|
||||
export const useMyCustomContext = () => useContext(MyCustomContext)
|
||||
```
|
||||
|
||||
_For details on how to build Custom Components, see [Building Custom Components](./overview#building-custom-components)._
|
||||
|
||||
@@ -297,22 +297,6 @@ export const CustomBlocksFieldLabelClient: BlocksFieldLabelClientComponent = ({
|
||||
}
|
||||
```
|
||||
|
||||
### Row Label
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import { useRowLabel } from '@payloadcms/ui'
|
||||
|
||||
export const BlockRowLabel = () => {
|
||||
const { data, rowNumber } = useRowLabel<{ title?: string }>()
|
||||
|
||||
const customLabel = `${data.type} ${String(rowNumber).padStart(2, '0')} `
|
||||
|
||||
return <div>Custom Label: {customLabel}</div>
|
||||
}
|
||||
```
|
||||
|
||||
## Block References
|
||||
|
||||
If you have multiple blocks used in multiple places, your Payload Config can grow in size, potentially sending more data to the client and requiring more processing on the server. However, you can optimize performance by defining each block **once** in your Payload Config and then referencing its slug wherever it's used instead of passing the entire block config.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import type { Permissions } from 'payload/auth'
|
||||
|
||||
import React, { createContext, useCallback, use, useEffect, useState } from 'react'
|
||||
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
||||
|
||||
import type { User } from '../../../../payload-types'
|
||||
import type { AuthContext, Create, ForgotPassword, Login, Logout, ResetPassword } from './types'
|
||||
@@ -163,7 +163,7 @@ export const AuthProvider: React.FC<{ api?: 'gql' | 'rest'; children: React.Reac
|
||||
)
|
||||
|
||||
return (
|
||||
<Context
|
||||
<Context.Provider
|
||||
value={{
|
||||
create,
|
||||
forgotPassword,
|
||||
@@ -177,10 +177,10 @@ export const AuthProvider: React.FC<{ api?: 'gql' | 'rest'; children: React.Reac
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Context>
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
type UseAuth<T = User> = () => AuthContext
|
||||
type UseAuth<T = User> = () => AuthContext
|
||||
|
||||
export const useAuth: UseAuth = () => use(Context)
|
||||
export const useAuth: UseAuth = () => useContext(Context)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import type { Theme } from '@/providers/Theme/types'
|
||||
|
||||
import React, { createContext, useCallback, use, useState } from 'react'
|
||||
import React, { createContext, useCallback, useContext, useState } from 'react'
|
||||
|
||||
import canUseDOM from '@/utilities/canUseDOM'
|
||||
|
||||
@@ -27,7 +27,11 @@ export const HeaderThemeProvider = ({ children }: { children: React.ReactNode })
|
||||
setThemeState(themeToSet)
|
||||
}, [])
|
||||
|
||||
return <HeaderThemeContext value={{ headerTheme, setHeaderTheme }}>{children}</HeaderThemeContext>
|
||||
return (
|
||||
<HeaderThemeContext.Provider value={{ headerTheme, setHeaderTheme }}>
|
||||
{children}
|
||||
</HeaderThemeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useHeaderTheme = (): ContextType => use(HeaderThemeContext)
|
||||
export const useHeaderTheme = (): ContextType => useContext(HeaderThemeContext)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import React, { createContext, useCallback, use, useEffect, useState } from 'react'
|
||||
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
||||
|
||||
import type { Theme, ThemeContextType } from './types'
|
||||
|
||||
@@ -51,7 +51,7 @@ export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
setThemeState(themeToSet)
|
||||
}, [])
|
||||
|
||||
return <ThemeContext value={{ setTheme, theme }}>{children}</ThemeContext>
|
||||
return <ThemeContext.Provider value={{ setTheme, theme }}>{children}</ThemeContext.Provider>
|
||||
}
|
||||
|
||||
export const useTheme = (): ThemeContextType => use(ThemeContext)
|
||||
export const useTheme = (): ThemeContextType => useContext(ThemeContext)
|
||||
|
||||
1303
examples/multi-tenant/pnpm-lock.yaml
generated
1303
examples/multi-tenant/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/admin-bar",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"description": "An admin bar for React apps using Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-sqlite",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"description": "The officially supported SQLite database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-vercel-postgres",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"description": "Vercel Postgres adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/drizzle",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"description": "A library of shared functions used by different payload database adapters",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-nodemailer",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"description": "Payload Nodemailer Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-resend",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"description": "Payload Resend Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FieldWithSubFields, Tab, TabsField } from 'payload'
|
||||
import type { FieldWithSubFields, TabsField } from 'payload'
|
||||
|
||||
import { fieldAffectsData, fieldIsPresentationalOnly } from 'payload/shared'
|
||||
|
||||
@@ -17,7 +17,7 @@ export const recursivelyBuildNestedPaths = ({ field, nestedFieldName2, parentNam
|
||||
if (field.type === 'tabs') {
|
||||
// if the tab has a name, treat it as a group
|
||||
// otherwise, treat it as a row
|
||||
return (field.tabs as Tab[]).reduce((tabSchema, tab: any) => {
|
||||
return field.tabs.reduce((tabSchema, tab: any) => {
|
||||
tabSchema.push(
|
||||
...recursivelyBuildNestedPaths({
|
||||
field: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"description": "The official React SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-vue",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"description": "The official Vue SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { fieldSchemaToJSON } from 'payload/shared'
|
||||
import type { Dispatch } from 'react'
|
||||
import type React from 'react'
|
||||
|
||||
import { createContext, use } from 'react'
|
||||
import { createContext, useContext } from 'react'
|
||||
|
||||
import type { usePopupWindow } from '../usePopupWindow.js'
|
||||
import type { SizeReducerAction } from './sizeReducer.js'
|
||||
@@ -83,4 +83,4 @@ export const LivePreviewContext = createContext<LivePreviewContextType>({
|
||||
zoom: 1,
|
||||
})
|
||||
|
||||
export const useLivePreviewContext = () => use(LivePreviewContext)
|
||||
export const useLivePreviewContext = () => useContext(LivePreviewContext)
|
||||
|
||||
@@ -166,7 +166,7 @@ export const LivePreviewProvider: React.FC<LivePreviewProviderProps> = ({
|
||||
}, [previewWindowType, isPopupOpen, handleWindowChange])
|
||||
|
||||
return (
|
||||
<LivePreviewContext
|
||||
<LivePreviewContext.Provider
|
||||
value={{
|
||||
appIsReady,
|
||||
breakpoint,
|
||||
@@ -198,6 +198,6 @@ export const LivePreviewProvider: React.FC<LivePreviewProviderProps> = ({
|
||||
<DndContext collisionDetection={customCollisionDetection} onDragEnd={handleDragEnd}>
|
||||
{listeningForMessages && children}
|
||||
</DndContext>
|
||||
</LivePreviewContext>
|
||||
</LivePreviewContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, use } from 'react'
|
||||
import React, { createContext } from 'react'
|
||||
|
||||
type SelectedLocalesContextType = {
|
||||
selectedLocales: string[]
|
||||
@@ -10,4 +10,4 @@ export const SelectedLocalesContext = createContext<SelectedLocalesContextType>(
|
||||
selectedLocales: [],
|
||||
})
|
||||
|
||||
export const useSelectedLocales = () => use(SelectedLocalesContext)
|
||||
export const useSelectedLocales = () => React.useContext(SelectedLocalesContext)
|
||||
|
||||
@@ -182,11 +182,11 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<SelectedLocalesContext
|
||||
<SelectedLocalesContext.Provider
|
||||
value={{ selectedLocales: selectedLocales.map((locale) => locale.value) }}
|
||||
>
|
||||
{doc?.version && RenderedDiff}
|
||||
</SelectedLocalesContext>
|
||||
</SelectedLocalesContext.Provider>
|
||||
</Gutter>
|
||||
</main>
|
||||
)
|
||||
|
||||
@@ -102,7 +102,7 @@ export const withPayload = (nextConfig = {}, options = {}) => {
|
||||
'pino-pretty',
|
||||
'graphql',
|
||||
// Do not bundle server-only packages during dev to improve compile speed
|
||||
...(process.env.NODE_ENV === 'development' && options.devBundleServerPackages === false
|
||||
...(process.env.npm_lifecycle_event === 'dev' && options.devBundleServerPackages === false
|
||||
? [
|
||||
'payload',
|
||||
'@payloadcms/db-mongodb',
|
||||
@@ -114,11 +114,11 @@ export const withPayload = (nextConfig = {}, options = {}) => {
|
||||
'@payloadcms/email-resend',
|
||||
'@payloadcms/graphql',
|
||||
'@payloadcms/payload-cloud',
|
||||
'@payloadcms/plugin-cloud-storage',
|
||||
'@payloadcms/plugin-redirects',
|
||||
'@payloadcms/plugin-sentry',
|
||||
'@payloadcms/plugin-stripe',
|
||||
// TODO: Add the following packages, excluding their /client subpath exports, once Next.js supports it
|
||||
//'@payloadcms/plugin-cloud-storage',
|
||||
//'@payloadcms/plugin-sentry',
|
||||
//'@payloadcms/plugin-stripe',
|
||||
// @payloadcms/richtext-lexical
|
||||
//'@payloadcms/storage-azure',
|
||||
//'@payloadcms/storage-gcs',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/payload-cloud",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"description": "The official Payload Cloud plugin",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
|
||||
"keywords": [
|
||||
"admin panel",
|
||||
|
||||
@@ -25,11 +25,8 @@ import type {
|
||||
} from '../types.js'
|
||||
|
||||
export type ClientTab =
|
||||
| ({ fields: ClientField[]; passesCondition?: boolean; readonly path?: string } & Omit<
|
||||
NamedTab,
|
||||
'fields'
|
||||
>)
|
||||
| ({ fields: ClientField[]; passesCondition?: boolean } & Omit<UnnamedTab, 'fields'>)
|
||||
| ({ fields: ClientField[]; readonly path?: string } & Omit<NamedTab, 'fields'>)
|
||||
| ({ fields: ClientField[] } & Omit<UnnamedTab, 'fields'>)
|
||||
|
||||
type TabsFieldBaseClientProps = FieldPaths
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
|
||||
import type { Config, SanitizedConfig } from '../../config/types.js'
|
||||
import type {
|
||||
CollectionConfig,
|
||||
@@ -28,7 +27,6 @@ import {
|
||||
} from './defaults.js'
|
||||
import { sanitizeAuthFields, sanitizeUploadFields } from './reservedFieldNames.js'
|
||||
import { sanitizeCompoundIndexes } from './sanitizeCompoundIndexes.js'
|
||||
import { sanitizeOrderable } from './sanitizeOrderable.js'
|
||||
import { validateUseAsTitle } from './useAsTitle.js'
|
||||
|
||||
export const sanitizeCollection = async (
|
||||
@@ -239,8 +237,6 @@ export const sanitizeCollection = async (
|
||||
|
||||
const sanitizedConfig = sanitized as SanitizedCollectionConfig
|
||||
|
||||
sanitizeOrderable(sanitizedConfig)
|
||||
|
||||
sanitizedConfig.joins = joins
|
||||
sanitizedConfig.polymorphicJoins = polymorphicJoins
|
||||
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import type { Field } from '../../fields/config/types.js'
|
||||
import type { BeforeChangeHook, SanitizedCollectionConfig } from './types.js'
|
||||
|
||||
import { generateKeyBetween } from '../../utilities/fractional-indexing.js'
|
||||
import { getReorderEndpoint } from '../endpoints/reorder.js'
|
||||
|
||||
export const ORDER_FIELD_NAME = '_order'
|
||||
|
||||
export const sanitizeOrderable = (collection: SanitizedCollectionConfig) => {
|
||||
if (!collection.orderable) {
|
||||
return
|
||||
}
|
||||
// 1. Add field
|
||||
const orderField: Field = {
|
||||
name: ORDER_FIELD_NAME,
|
||||
type: 'text',
|
||||
admin: {
|
||||
disableBulkEdit: true,
|
||||
disabled: true,
|
||||
disableListColumn: true,
|
||||
disableListFilter: true,
|
||||
hidden: true,
|
||||
readOnly: true,
|
||||
},
|
||||
index: true,
|
||||
label: ({ t }) => t('general:order'),
|
||||
required: true,
|
||||
unique: true,
|
||||
}
|
||||
|
||||
collection.fields.unshift(orderField)
|
||||
|
||||
// 2. Add hook
|
||||
const orderBeforeChangeHook: BeforeChangeHook = async ({ data, operation, req }) => {
|
||||
// Only set _order on create, not on update (unless explicitly provided)
|
||||
if (operation === 'create') {
|
||||
// Find the last document to place this one after
|
||||
const lastDoc = await req.payload.find({
|
||||
collection: collection.slug,
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
select: { [ORDER_FIELD_NAME]: true },
|
||||
sort: `-${ORDER_FIELD_NAME}`,
|
||||
})
|
||||
|
||||
const lastOrderValue: null | string = (lastDoc.docs[0]?.[ORDER_FIELD_NAME] as string) || null
|
||||
data[ORDER_FIELD_NAME] = generateKeyBetween(lastOrderValue, null)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
collection.hooks.beforeChange.push(orderBeforeChangeHook)
|
||||
|
||||
// 3. Add endpoint
|
||||
if (collection.endpoints !== false) {
|
||||
collection.endpoints.push(getReorderEndpoint(collection))
|
||||
}
|
||||
}
|
||||
@@ -495,17 +495,6 @@ export type CollectionConfig<TSlug extends CollectionSlug = any> = {
|
||||
duration: number
|
||||
}
|
||||
| false
|
||||
/**
|
||||
* If true, enables custom ordering for the collection, and documents in the listView can be reordered via drag and drop.
|
||||
* New documents are inserted at the end of the list according to this parameter.
|
||||
*
|
||||
* Under the hood, a field with {@link https://observablehq.com/@dgreensp/implementing-fractional-indexing|fractional indexing} is used to optimize inserts and reorderings.
|
||||
*
|
||||
* @default false
|
||||
*
|
||||
* @experimental There may be frequent breaking changes to this API
|
||||
*/
|
||||
orderable?: boolean
|
||||
slug: string
|
||||
/**
|
||||
* Add `createdAt` and `updatedAt` fields
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import type { Endpoint, PayloadHandler } from '../../config/types.js'
|
||||
import type { PayloadRequest, SanitizedCollectionConfig } from '../../index.js'
|
||||
|
||||
import { APIError, executeAccess } from '../../index.js'
|
||||
import { generateNKeysBetween } from '../../utilities/fractional-indexing.js'
|
||||
import { ORDER_FIELD_NAME } from '../config/sanitizeOrderable.js'
|
||||
|
||||
export const getReorderEndpoint = (
|
||||
collection: SanitizedCollectionConfig,
|
||||
): Omit<Endpoint, 'root'> => {
|
||||
const reorderHandler: PayloadHandler = async (req: PayloadRequest) => {
|
||||
if (!req.json) {
|
||||
throw new APIError('Unreachable')
|
||||
}
|
||||
const body = await req.json()
|
||||
type KeyAndID = {
|
||||
id: string
|
||||
key: string
|
||||
}
|
||||
const { docsToMove, newKeyWillBe, target } = body as {
|
||||
// array of docs IDs to be moved before or after the target
|
||||
docsToMove: string[]
|
||||
// new key relative to the target. We don't use "after" or "before" as
|
||||
// it can be misleading if the table is sorted in descending order.
|
||||
newKeyWillBe: 'greater' | 'less'
|
||||
target: KeyAndID
|
||||
}
|
||||
|
||||
if (!Array.isArray(docsToMove) || docsToMove.length === 0) {
|
||||
return new Response(JSON.stringify({ error: 'docsToMove must be a non-empty array' }), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
status: 400,
|
||||
})
|
||||
}
|
||||
if (
|
||||
typeof target !== 'object' ||
|
||||
typeof target.id !== 'string' ||
|
||||
typeof target.key !== 'string'
|
||||
) {
|
||||
return new Response(JSON.stringify({ error: 'target must be an object with id and key' }), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
status: 400,
|
||||
})
|
||||
}
|
||||
if (newKeyWillBe !== 'greater' && newKeyWillBe !== 'less') {
|
||||
return new Response(JSON.stringify({ error: 'newKeyWillBe must be "greater" or "less"' }), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
status: 400,
|
||||
})
|
||||
}
|
||||
|
||||
// Prevent reordering if user doesn't have editing permissions
|
||||
await executeAccess(
|
||||
{
|
||||
// Currently only one doc can be moved at a time. We should review this if we want to allow
|
||||
// multiple docs to be moved at once in the future.
|
||||
id: docsToMove[0],
|
||||
data: {},
|
||||
req,
|
||||
},
|
||||
collection.access.update,
|
||||
)
|
||||
|
||||
const targetId = target.id
|
||||
let targetKey: null | string = target.key
|
||||
|
||||
// If targetKey = pending, we need to find its current key.
|
||||
// This can only happen if the user reorders rows quickly with a slow connection.
|
||||
if (targetKey === 'pending') {
|
||||
const beforeDoc = await req.payload.findByID({
|
||||
id: targetId,
|
||||
collection: collection.slug,
|
||||
depth: 0,
|
||||
select: { [ORDER_FIELD_NAME]: true },
|
||||
})
|
||||
targetKey = (beforeDoc?.[ORDER_FIELD_NAME] as string) || null
|
||||
}
|
||||
|
||||
// The reason the endpoint does not receive this docId as an argument is that there
|
||||
// are situations where the user may not see or know what the next or previous one is. For
|
||||
// example, access control restrictions, if docBefore is the last one on the page, etc.
|
||||
const adjacentDoc = await req.payload.find({
|
||||
collection: collection.slug,
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
select: { [ORDER_FIELD_NAME]: true },
|
||||
sort: newKeyWillBe === 'greater' ? ORDER_FIELD_NAME : `-${ORDER_FIELD_NAME}`,
|
||||
where: {
|
||||
[ORDER_FIELD_NAME]: {
|
||||
[newKeyWillBe === 'greater' ? 'greater_than' : 'less_than']: targetKey,
|
||||
},
|
||||
},
|
||||
})
|
||||
const adjacentDocKey: null | string =
|
||||
(adjacentDoc.docs?.[0]?.[ORDER_FIELD_NAME] as string) || null
|
||||
|
||||
// Currently N (= docsToMove.length) is always 1. Maybe in the future we will
|
||||
// allow dragging and reordering multiple documents at once via the UI.
|
||||
const orderValues =
|
||||
newKeyWillBe === 'greater'
|
||||
? generateNKeysBetween(targetKey, adjacentDocKey, docsToMove.length)
|
||||
: generateNKeysBetween(adjacentDocKey, targetKey, docsToMove.length)
|
||||
|
||||
// Update each document with its new order value
|
||||
|
||||
for (const id of docsToMove) {
|
||||
await req.payload.update({
|
||||
id,
|
||||
collection: collection.slug,
|
||||
data: {
|
||||
[ORDER_FIELD_NAME]: orderValues.shift(),
|
||||
},
|
||||
depth: 0,
|
||||
req,
|
||||
select: { [ORDER_FIELD_NAME]: true },
|
||||
})
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ orderValues, success: true }), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
status: 200,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
handler: reorderHandler,
|
||||
method: 'post',
|
||||
path: '/reorder',
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
// @ts-strict-ignore
|
||||
import { deepMergeSimple } from '@payloadcms/translations/utilities'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
import type {
|
||||
CollectionConfig,
|
||||
@@ -289,16 +288,6 @@ export const sanitizeFields = async ({
|
||||
tab.label = toWords(tab.name)
|
||||
}
|
||||
|
||||
if (
|
||||
'admin' in tab &&
|
||||
tab.admin?.condition &&
|
||||
typeof tab.admin.condition === 'function' &&
|
||||
!tab.id
|
||||
) {
|
||||
// Always attach a UUID to tabs with a condition so there's no conflicts even if there are duplicate nested names
|
||||
tab.id = tabHasName(tab) ? `${tab.name}_${uuid()}` : uuid()
|
||||
}
|
||||
|
||||
tab.fields = await sanitizeFields({
|
||||
config,
|
||||
existingFieldNames: tabHasName(tab) ? new Set() : existingFieldNames,
|
||||
|
||||
@@ -788,7 +788,6 @@ type TabBase = {
|
||||
*/
|
||||
description?: LabelFunction | StaticDescription
|
||||
fields: Field[]
|
||||
id?: string
|
||||
interfaceName?: string
|
||||
saveToJWT?: boolean | string
|
||||
} & Omit<FieldBase, 'required' | 'validate'>
|
||||
@@ -820,11 +819,11 @@ export type UnnamedTab = {
|
||||
} & Omit<TabBase, 'name' | 'virtual'>
|
||||
|
||||
export type Tab = NamedTab | UnnamedTab
|
||||
|
||||
export type TabsField = {
|
||||
admin?: Omit<Admin, 'description'>
|
||||
type: 'tabs'
|
||||
} & {
|
||||
tabs: Tab[]
|
||||
type: 'tabs'
|
||||
} & Omit<FieldBase, 'admin' | 'localized' | 'name' | 'saveToJWT' | 'virtual'>
|
||||
|
||||
export type TabsFieldClient = {
|
||||
|
||||
@@ -1,318 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* THIS FILE IS COPIED FROM:
|
||||
* https://github.com/rocicorp/fractional-indexing/blob/main/src/index.js
|
||||
*
|
||||
* I AM NOT INSTALLING THAT LIBRARY BECAUSE JEST COMPLAINS ABOUT THE ESM MODULE AND THE TESTS FAIL.
|
||||
* DO NOT MODIFY IT
|
||||
*/
|
||||
|
||||
// License: CC0 (no rights reserved).
|
||||
|
||||
// This is based on https://observablehq.com/@dgreensp/implementing-fractional-indexing
|
||||
|
||||
export const BASE_62_DIGITS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
|
||||
|
||||
// `a` may be empty string, `b` is null or non-empty string.
|
||||
// `a < b` lexicographically if `b` is non-null.
|
||||
// no trailing zeros allowed.
|
||||
// digits is a string such as '0123456789' for base 10. Digits must be in
|
||||
// ascending character code order!
|
||||
/**
|
||||
* @param {string} a
|
||||
* @param {string | null | undefined} b
|
||||
* @param {string} digits
|
||||
* @returns {string}
|
||||
*/
|
||||
function midpoint(a, b, digits) {
|
||||
const zero = digits[0]
|
||||
if (b != null && a >= b) {
|
||||
throw new Error(a + ' >= ' + b)
|
||||
}
|
||||
if (a.slice(-1) === zero || (b && b.slice(-1) === zero)) {
|
||||
throw new Error('trailing zero')
|
||||
}
|
||||
if (b) {
|
||||
// remove longest common prefix. pad `a` with 0s as we
|
||||
// go. note that we don't need to pad `b`, because it can't
|
||||
// end before `a` while traversing the common prefix.
|
||||
let n = 0
|
||||
while ((a[n] || zero) === b[n]) {
|
||||
n++
|
||||
}
|
||||
if (n > 0) {
|
||||
return b.slice(0, n) + midpoint(a.slice(n), b.slice(n), digits)
|
||||
}
|
||||
}
|
||||
// first digits (or lack of digit) are different
|
||||
const digitA = a ? digits.indexOf(a[0]) : 0
|
||||
const digitB = b != null ? digits.indexOf(b[0]) : digits.length
|
||||
if (digitB - digitA > 1) {
|
||||
const midDigit = Math.round(0.5 * (digitA + digitB))
|
||||
return digits[midDigit]
|
||||
} else {
|
||||
// first digits are consecutive
|
||||
if (b && b.length > 1) {
|
||||
return b.slice(0, 1)
|
||||
} else {
|
||||
// `b` is null or has length 1 (a single digit).
|
||||
// the first digit of `a` is the previous digit to `b`,
|
||||
// or 9 if `b` is null.
|
||||
// given, for example, midpoint('49', '5'), return
|
||||
// '4' + midpoint('9', null), which will become
|
||||
// '4' + '9' + midpoint('', null), which is '495'
|
||||
return digits[digitA] + midpoint(a.slice(1), null, digits)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} int
|
||||
* @return {void}
|
||||
*/
|
||||
|
||||
function validateInteger(int) {
|
||||
if (int.length !== getIntegerLength(int[0])) {
|
||||
throw new Error('invalid integer part of order key: ' + int)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} head
|
||||
* @return {number}
|
||||
*/
|
||||
|
||||
function getIntegerLength(head) {
|
||||
if (head >= 'a' && head <= 'z') {
|
||||
return head.charCodeAt(0) - 'a'.charCodeAt(0) + 2
|
||||
} else if (head >= 'A' && head <= 'Z') {
|
||||
return 'Z'.charCodeAt(0) - head.charCodeAt(0) + 2
|
||||
} else {
|
||||
throw new Error('invalid order key head: ' + head)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @return {string}
|
||||
*/
|
||||
|
||||
function getIntegerPart(key) {
|
||||
const integerPartLength = getIntegerLength(key[0])
|
||||
if (integerPartLength > key.length) {
|
||||
throw new Error('invalid order key: ' + key)
|
||||
}
|
||||
return key.slice(0, integerPartLength)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {string} digits
|
||||
* @return {void}
|
||||
*/
|
||||
|
||||
function validateOrderKey(key, digits) {
|
||||
if (key === 'A' + digits[0].repeat(26)) {
|
||||
throw new Error('invalid order key: ' + key)
|
||||
}
|
||||
// getIntegerPart will throw if the first character is bad,
|
||||
// or the key is too short. we'd call it to check these things
|
||||
// even if we didn't need the result
|
||||
const i = getIntegerPart(key)
|
||||
const f = key.slice(i.length)
|
||||
if (f.slice(-1) === digits[0]) {
|
||||
throw new Error('invalid order key: ' + key)
|
||||
}
|
||||
}
|
||||
|
||||
// note that this may return null, as there is a largest integer
|
||||
/**
|
||||
* @param {string} x
|
||||
* @param {string} digits
|
||||
* @return {string | null}
|
||||
*/
|
||||
function incrementInteger(x, digits) {
|
||||
validateInteger(x)
|
||||
const [head, ...digs] = x.split('')
|
||||
let carry = true
|
||||
for (let i = digs.length - 1; carry && i >= 0; i--) {
|
||||
const d = digits.indexOf(digs[i]) + 1
|
||||
if (d === digits.length) {
|
||||
digs[i] = digits[0]
|
||||
} else {
|
||||
digs[i] = digits[d]
|
||||
carry = false
|
||||
}
|
||||
}
|
||||
if (carry) {
|
||||
if (head === 'Z') {
|
||||
return 'a' + digits[0]
|
||||
}
|
||||
if (head === 'z') {
|
||||
return null
|
||||
}
|
||||
const h = String.fromCharCode(head.charCodeAt(0) + 1)
|
||||
if (h > 'a') {
|
||||
digs.push(digits[0])
|
||||
} else {
|
||||
digs.pop()
|
||||
}
|
||||
return h + digs.join('')
|
||||
} else {
|
||||
return head + digs.join('')
|
||||
}
|
||||
}
|
||||
|
||||
// note that this may return null, as there is a smallest integer
|
||||
/**
|
||||
* @param {string} x
|
||||
* @param {string} digits
|
||||
* @return {string | null}
|
||||
*/
|
||||
|
||||
function decrementInteger(x, digits) {
|
||||
validateInteger(x)
|
||||
const [head, ...digs] = x.split('')
|
||||
let borrow = true
|
||||
for (let i = digs.length - 1; borrow && i >= 0; i--) {
|
||||
const d = digits.indexOf(digs[i]) - 1
|
||||
if (d === -1) {
|
||||
digs[i] = digits.slice(-1)
|
||||
} else {
|
||||
digs[i] = digits[d]
|
||||
borrow = false
|
||||
}
|
||||
}
|
||||
if (borrow) {
|
||||
if (head === 'a') {
|
||||
return 'Z' + digits.slice(-1)
|
||||
}
|
||||
if (head === 'A') {
|
||||
return null
|
||||
}
|
||||
const h = String.fromCharCode(head.charCodeAt(0) - 1)
|
||||
if (h < 'Z') {
|
||||
digs.push(digits.slice(-1))
|
||||
} else {
|
||||
digs.pop()
|
||||
}
|
||||
return h + digs.join('')
|
||||
} else {
|
||||
return head + digs.join('')
|
||||
}
|
||||
}
|
||||
|
||||
// `a` is an order key or null (START).
|
||||
// `b` is an order key or null (END).
|
||||
// `a < b` lexicographically if both are non-null.
|
||||
// digits is a string such as '0123456789' for base 10. Digits must be in
|
||||
// ascending character code order!
|
||||
/**
|
||||
* @param {string | null | undefined} a
|
||||
* @param {string | null | undefined} b
|
||||
* @param {string=} digits
|
||||
* @return {string}
|
||||
*/
|
||||
export function generateKeyBetween(a, b, digits = BASE_62_DIGITS) {
|
||||
if (a != null) {
|
||||
validateOrderKey(a, digits)
|
||||
}
|
||||
if (b != null) {
|
||||
validateOrderKey(b, digits)
|
||||
}
|
||||
if (a != null && b != null && a >= b) {
|
||||
throw new Error(a + ' >= ' + b)
|
||||
}
|
||||
if (a == null) {
|
||||
if (b == null) {
|
||||
return 'a' + digits[0]
|
||||
}
|
||||
|
||||
const ib = getIntegerPart(b)
|
||||
const fb = b.slice(ib.length)
|
||||
if (ib === 'A' + digits[0].repeat(26)) {
|
||||
return ib + midpoint('', fb, digits)
|
||||
}
|
||||
if (ib < b) {
|
||||
return ib
|
||||
}
|
||||
const res = decrementInteger(ib, digits)
|
||||
if (res == null) {
|
||||
throw new Error('cannot decrement any more')
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
if (b == null) {
|
||||
const ia = getIntegerPart(a)
|
||||
const fa = a.slice(ia.length)
|
||||
const i = incrementInteger(ia, digits)
|
||||
return i == null ? ia + midpoint(fa, null, digits) : i
|
||||
}
|
||||
|
||||
const ia = getIntegerPart(a)
|
||||
const fa = a.slice(ia.length)
|
||||
const ib = getIntegerPart(b)
|
||||
const fb = b.slice(ib.length)
|
||||
if (ia === ib) {
|
||||
return ia + midpoint(fa, fb, digits)
|
||||
}
|
||||
const i = incrementInteger(ia, digits)
|
||||
if (i == null) {
|
||||
throw new Error('cannot increment any more')
|
||||
}
|
||||
if (i < b) {
|
||||
return i
|
||||
}
|
||||
return ia + midpoint(fa, null, digits)
|
||||
}
|
||||
|
||||
/**
|
||||
* same preconditions as generateKeysBetween.
|
||||
* n >= 0.
|
||||
* Returns an array of n distinct keys in sorted order.
|
||||
* If a and b are both null, returns [a0, a1, ...]
|
||||
* If one or the other is null, returns consecutive "integer"
|
||||
* keys. Otherwise, returns relatively short keys between
|
||||
* a and b.
|
||||
* @param {string | null | undefined} a
|
||||
* @param {string | null | undefined} b
|
||||
* @param {number} n
|
||||
* @param {string} digits
|
||||
* @return {string[]}
|
||||
*/
|
||||
export function generateNKeysBetween(a, b, n, digits = BASE_62_DIGITS) {
|
||||
if (n === 0) {
|
||||
return []
|
||||
}
|
||||
if (n === 1) {
|
||||
return [generateKeyBetween(a, b, digits)]
|
||||
}
|
||||
if (b == null) {
|
||||
let c = generateKeyBetween(a, b, digits)
|
||||
const result = [c]
|
||||
for (let i = 0; i < n - 1; i++) {
|
||||
c = generateKeyBetween(c, b, digits)
|
||||
result.push(c)
|
||||
}
|
||||
return result
|
||||
}
|
||||
if (a == null) {
|
||||
let c = generateKeyBetween(a, b, digits)
|
||||
const result = [c]
|
||||
for (let i = 0; i < n - 1; i++) {
|
||||
c = generateKeyBetween(a, c, digits)
|
||||
result.push(c)
|
||||
}
|
||||
result.reverse()
|
||||
return result
|
||||
}
|
||||
const mid = Math.floor(n / 2)
|
||||
const c = generateKeyBetween(a, b, digits)
|
||||
return [
|
||||
...generateNKeysBetween(a, c, mid, digits),
|
||||
c,
|
||||
...generateNKeysBetween(c, b, n - mid - 1, digits),
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-cloud-storage",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"description": "The official cloud storage plugin for Payload CMS",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-form-builder",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"description": "Form builder plugin for Payload CMS",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-import-export",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"description": "Import-Export plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import React, { createContext, use, useCallback, useState } from 'react'
|
||||
import React, { createContext, useCallback, useContext, useState } from 'react'
|
||||
|
||||
type ImportExportContext = {
|
||||
collection: string
|
||||
@@ -16,15 +16,15 @@ export const ImportExportProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ImportExportContext
|
||||
<ImportExportContext.Provider
|
||||
value={{
|
||||
collection,
|
||||
setCollection,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ImportExportContext>
|
||||
</ImportExportContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useImportExport = (): ImportExportContext => use(ImportExportContext)
|
||||
export const useImportExport = (): ImportExportContext => useContext(ImportExportContext)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-multi-tenant",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"description": "Multi Tenant plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { RelationshipFieldClientProps } from 'payload'
|
||||
import { RelationshipField, useField } from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
|
||||
import { SELECT_ALL } from '../../constants.js'
|
||||
import { useTenantSelection } from '../../providers/TenantSelectionProvider/index.client.js'
|
||||
import './index.scss'
|
||||
|
||||
@@ -29,11 +30,14 @@ export const TenantField = (args: Props) => {
|
||||
setTenant({ id: value, refresh: unique })
|
||||
} else {
|
||||
// in the document view, the tenant field should always have a value
|
||||
const defaultValue = selectedTenantID || options[0]?.value
|
||||
const defaultValue =
|
||||
!selectedTenantID || selectedTenantID === SELECT_ALL
|
||||
? options[0]?.value
|
||||
: selectedTenantID
|
||||
setTenant({ id: defaultValue, refresh: unique })
|
||||
}
|
||||
hasSetValueRef.current = true
|
||||
} else if (!value || value !== selectedTenantID) {
|
||||
} else if ((!value || value !== selectedTenantID) && selectedTenantID !== SELECT_ALL) {
|
||||
// Update the field on the document value when the tenant is changed
|
||||
setValue(selectedTenantID)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import './index.scss'
|
||||
import { SelectInput, useTranslation } from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
|
||||
import { SELECT_ALL } from '../../constants.js'
|
||||
import { useTenantSelection } from '../../providers/TenantSelectionProvider/index.client.js'
|
||||
|
||||
export const TenantSelector = ({ label, viewType }: { label: string; viewType?: ViewTypes }) => {
|
||||
@@ -37,7 +38,13 @@ export const TenantSelector = ({ label, viewType }: { label: string; viewType?:
|
||||
onChange={handleChange}
|
||||
options={options}
|
||||
path="setTenant"
|
||||
value={selectedTenantID as string | undefined}
|
||||
value={
|
||||
selectedTenantID
|
||||
? selectedTenantID === SELECT_ALL
|
||||
? undefined
|
||||
: (selectedTenantID as string)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
2
packages/plugin-multi-tenant/src/constants.ts
Normal file
2
packages/plugin-multi-tenant/src/constants.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// The tenant cookie can be set to _ALL_ to allow users to see all results for tenants they are a member of.
|
||||
export const SELECT_ALL = '_ALL_'
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { PayloadRequest, Where } from 'payload'
|
||||
|
||||
import { SELECT_ALL } from '../constants.js'
|
||||
import { getCollectionIDType } from '../utilities/getCollectionIDType.js'
|
||||
import { getTenantFromCookie } from '../utilities/getTenantFromCookie.js'
|
||||
|
||||
@@ -19,13 +20,13 @@ export const filterDocumentsBySelectedTenant = ({
|
||||
})
|
||||
const selectedTenant = getTenantFromCookie(req.headers, idType)
|
||||
|
||||
if (selectedTenant) {
|
||||
return {
|
||||
[tenantFieldName]: {
|
||||
equals: selectedTenant,
|
||||
},
|
||||
}
|
||||
if (selectedTenant === SELECT_ALL) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return {}
|
||||
return {
|
||||
[tenantFieldName]: {
|
||||
equals: selectedTenant,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { PayloadRequest, Where } from 'payload'
|
||||
|
||||
import { SELECT_ALL } from '../constants.js'
|
||||
import { getCollectionIDType } from '../utilities/getCollectionIDType.js'
|
||||
import { getTenantFromCookie } from '../utilities/getTenantFromCookie.js'
|
||||
|
||||
@@ -17,13 +18,13 @@ export const filterTenantsBySelectedTenant = ({
|
||||
})
|
||||
const selectedTenant = getTenantFromCookie(req.headers, idType)
|
||||
|
||||
if (selectedTenant) {
|
||||
return {
|
||||
id: {
|
||||
equals: selectedTenant,
|
||||
},
|
||||
}
|
||||
if (selectedTenant === SELECT_ALL) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return {}
|
||||
return {
|
||||
id: {
|
||||
equals: selectedTenant,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { PayloadRequest, Where } from 'payload'
|
||||
|
||||
import { SELECT_ALL } from '../constants.js'
|
||||
import { getCollectionIDType } from '../utilities/getCollectionIDType.js'
|
||||
import { getTenantFromCookie } from '../utilities/getTenantFromCookie.js'
|
||||
|
||||
@@ -24,13 +25,13 @@ export const filterUsersBySelectedTenant = ({
|
||||
})
|
||||
const selectedTenant = getTenantFromCookie(req.headers, idType)
|
||||
|
||||
if (selectedTenant) {
|
||||
return {
|
||||
[`${tenantsArrayFieldName}.${tenantsArrayTenantFieldName}`]: {
|
||||
in: [selectedTenant],
|
||||
},
|
||||
}
|
||||
if (selectedTenant === SELECT_ALL) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return {}
|
||||
return {
|
||||
[`${tenantsArrayFieldName}.${tenantsArrayTenantFieldName}`]: {
|
||||
in: [selectedTenant],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import { useAuth } from '@payloadcms/ui'
|
||||
import { useRouter } from 'next/navigation.js'
|
||||
import React, { createContext } from 'react'
|
||||
|
||||
import { SELECT_ALL } from '../../constants.js'
|
||||
|
||||
type ContextType = {
|
||||
/**
|
||||
* Array of options to select from
|
||||
@@ -74,8 +76,8 @@ export const TenantSelectionProviderClient = ({
|
||||
({ id, refresh }) => {
|
||||
if (id === undefined) {
|
||||
if (tenantOptions.length > 1) {
|
||||
setSelectedTenantID(undefined)
|
||||
deleteCookie()
|
||||
setSelectedTenantID(SELECT_ALL)
|
||||
setCookie(SELECT_ALL)
|
||||
} else {
|
||||
setSelectedTenantID(tenantOptions[0]?.value)
|
||||
setCookie(String(tenantOptions[0]?.value))
|
||||
@@ -88,11 +90,15 @@ export const TenantSelectionProviderClient = ({
|
||||
router.refresh()
|
||||
}
|
||||
},
|
||||
[deleteCookie, preventRefreshOnChange, router, setCookie, setSelectedTenantID, tenantOptions],
|
||||
[setSelectedTenantID, setCookie, router, preventRefreshOnChange, tenantOptions],
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (selectedTenantID && !tenantOptions.find((option) => option.value === selectedTenantID)) {
|
||||
if (
|
||||
selectedTenantID &&
|
||||
selectedTenantID !== SELECT_ALL &&
|
||||
!tenantOptions.find((option) => option.value === selectedTenantID)
|
||||
) {
|
||||
if (tenantOptions?.[0]?.value) {
|
||||
setTenant({ id: tenantOptions[0].value, refresh: true })
|
||||
} else {
|
||||
@@ -105,13 +111,9 @@ export const TenantSelectionProviderClient = ({
|
||||
if (userID && !tenantCookie) {
|
||||
// User is logged in, but does not have a tenant cookie, set it
|
||||
setSelectedTenantID(initialValue)
|
||||
if (initialValue) {
|
||||
setCookie(String(initialValue))
|
||||
} else {
|
||||
deleteCookie()
|
||||
}
|
||||
setCookie(String(initialValue))
|
||||
}
|
||||
}, [userID, tenantCookie, initialValue, setCookie, deleteCookie, router])
|
||||
}, [userID, tenantCookie, initialValue, setCookie, router])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!userID && tenantCookie) {
|
||||
@@ -129,7 +131,7 @@ export const TenantSelectionProviderClient = ({
|
||||
data-selected-tenant-id={selectedTenantID}
|
||||
data-selected-tenant-title={selectedTenantLabel}
|
||||
>
|
||||
<Context
|
||||
<Context.Provider
|
||||
value={{
|
||||
options: tenantOptions,
|
||||
selectedTenantID,
|
||||
@@ -138,9 +140,9 @@ export const TenantSelectionProviderClient = ({
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Context>
|
||||
</Context.Provider>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export const useTenantSelection = () => React.use(Context)
|
||||
export const useTenantSelection = () => React.useContext(Context)
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { OptionObject, Payload, User } from 'payload'
|
||||
|
||||
import { cookies as getCookies } from 'next/headers.js'
|
||||
|
||||
import { SELECT_ALL } from '../../constants.js'
|
||||
import { findTenantOptions } from '../../queries/findTenantOptions.js'
|
||||
import { TenantSelectionProviderClient } from './index.client.js'
|
||||
|
||||
@@ -42,24 +43,18 @@ export const TenantSelectionProvider = async ({
|
||||
let tenantCookie = cookies.get('payload-tenant')?.value
|
||||
let initialValue = undefined
|
||||
|
||||
/**
|
||||
* Ensure the cookie is a valid tenant
|
||||
*/
|
||||
if (tenantCookie) {
|
||||
if (tenantOptions.length > 1 && tenantCookie === SELECT_ALL) {
|
||||
initialValue = SELECT_ALL
|
||||
} else {
|
||||
const matchingOption = tenantOptions.find((option) => String(option.value) === tenantCookie)
|
||||
if (matchingOption) {
|
||||
initialValue = matchingOption.value
|
||||
} else {
|
||||
tenantCookie = undefined
|
||||
initialValue = tenantOptions.length > 1 ? SELECT_ALL : tenantOptions[0]?.value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the there was no cookie or the cookie was an invalid tenantID set intialValue
|
||||
*/
|
||||
if (!initialValue) {
|
||||
tenantCookie = undefined
|
||||
initialValue = tenantOptions.length > 1 ? undefined : tenantOptions[0]?.value
|
||||
}
|
||||
|
||||
return (
|
||||
<TenantSelectionProviderClient
|
||||
initialValue={initialValue}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Payload, User, ViewTypes } from 'payload'
|
||||
|
||||
import { SELECT_ALL } from '../constants.js'
|
||||
import { findTenantOptions } from '../queries/findTenantOptions.js'
|
||||
import { getCollectionIDType } from './getCollectionIDType.js'
|
||||
import { getTenantFromCookie } from './getTenantFromCookie.js'
|
||||
@@ -33,7 +34,7 @@ export async function getGlobalViewRedirect({
|
||||
let tenant = getTenantFromCookie(headers, idType)
|
||||
let redirectRoute
|
||||
|
||||
if (!tenant) {
|
||||
if (!tenant || tenant === SELECT_ALL) {
|
||||
const tenantsQuery = await findTenantOptions({
|
||||
limit: 1,
|
||||
payload,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-nested-docs",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"description": "The official Nested Docs plugin for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-redirects",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"description": "Redirects plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-search",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"description": "Search plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-sentry",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"description": "Sentry plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-seo",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"description": "SEO plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-stripe",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"description": "Stripe plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-lexical",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"description": "The officially supported Lexical richtext adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -49,7 +49,7 @@ const BlockComponentContext = createContext<BlockComponentContextType>({
|
||||
initialState: false,
|
||||
})
|
||||
|
||||
export const useBlockComponentContext = () => React.use(BlockComponentContext)
|
||||
export const useBlockComponentContext = () => React.useContext(BlockComponentContext)
|
||||
|
||||
/**
|
||||
* The actual content of the Block. This should be INSIDE a Form component,
|
||||
@@ -99,7 +99,7 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
)
|
||||
|
||||
return CustomBlock ? (
|
||||
<BlockComponentContext
|
||||
<BlockComponentContext.Provider
|
||||
value={{
|
||||
BlockCollapsible: CollapsibleWithErrorProps,
|
||||
EditButton,
|
||||
@@ -110,7 +110,7 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
>
|
||||
{CustomBlock}
|
||||
<BlockDrawer />
|
||||
</BlockComponentContext>
|
||||
</BlockComponentContext.Provider>
|
||||
) : (
|
||||
<CollapsibleWithErrorProps>
|
||||
<RenderFields
|
||||
|
||||
@@ -61,7 +61,7 @@ const InlineBlockComponentContext = createContext<InlineBlockComponentContextTyp
|
||||
initialState: false,
|
||||
})
|
||||
|
||||
export const useInlineBlockComponentContext = () => React.use(InlineBlockComponentContext)
|
||||
export const useInlineBlockComponentContext = () => React.useContext(InlineBlockComponentContext)
|
||||
|
||||
export const InlineBlockComponent: React.FC<Props> = (props) => {
|
||||
const { cacheBuster, formData, nodeKey } = props
|
||||
@@ -426,7 +426,7 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
|
||||
</Drawer>
|
||||
</EditDepthProvider>
|
||||
{CustomBlock ? (
|
||||
<InlineBlockComponentContext
|
||||
<InlineBlockComponentContext.Provider
|
||||
value={{
|
||||
EditButton,
|
||||
initialState,
|
||||
@@ -437,7 +437,7 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
|
||||
}}
|
||||
>
|
||||
{CustomBlock}
|
||||
</InlineBlockComponentContext>
|
||||
</InlineBlockComponentContext.Provider>
|
||||
) : (
|
||||
<InlineBlockContainer>
|
||||
{initialState ? <Label /> : <ShimmerEffect height="15px" width="40px" />}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { INSERT_TABLE_COMMAND, TableCellNode, TableNode, TableRowNode } from '@l
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { formatDrawerSlug, useEditDepth } from '@payloadcms/ui'
|
||||
import { $getSelection, $isRangeSelection, COMMAND_PRIORITY_EDITOR, createCommand } from 'lexical'
|
||||
import { createContext, use, useEffect, useMemo, useState } from 'react'
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
|
||||
import * as React from 'react'
|
||||
|
||||
import type { PluginComponent } from '../../../../typesClient.js'
|
||||
@@ -64,7 +64,7 @@ export function TableContext({ children }: { children: JSX.Element }) {
|
||||
cellEditorPlugins: null,
|
||||
})
|
||||
return (
|
||||
<CellContext
|
||||
<CellContext.Provider
|
||||
value={useMemo(
|
||||
() => ({
|
||||
cellEditorConfig: contextValue.cellEditorConfig,
|
||||
@@ -77,13 +77,13 @@ export function TableContext({ children }: { children: JSX.Element }) {
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</CellContext>
|
||||
</CellContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const TablePlugin: PluginComponent = () => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const cellContext = use(CellContext)
|
||||
const cellContext = useContext(CellContext)
|
||||
const editDepth = useEditDepth()
|
||||
const {
|
||||
fieldProps: { schemaPath },
|
||||
|
||||
@@ -48,7 +48,7 @@ export function DropDownItem({
|
||||
|
||||
const ref = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const dropDownContext = React.use(DropDownContext)
|
||||
const dropDownContext = React.useContext(DropDownContext)
|
||||
|
||||
if (dropDownContext === null) {
|
||||
throw new Error('DropDownItem must be used within a DropDown')
|
||||
@@ -170,7 +170,7 @@ function DropDownItems({
|
||||
}, [items, highlightedItem])
|
||||
|
||||
return (
|
||||
<DropDownContext value={contextValue}>
|
||||
<DropDownContext.Provider value={contextValue}>
|
||||
<div
|
||||
className={(itemsContainerClassNames ?? ['toolbar-popup__dropdown-items']).join(' ')}
|
||||
onKeyDown={handleKeyDown}
|
||||
@@ -178,7 +178,7 @@ function DropDownItems({
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</DropDownContext>
|
||||
</DropDownContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { MarkRequired } from 'ts-essentials'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext.js'
|
||||
import { useEditDepth } from '@payloadcms/ui'
|
||||
import * as React from 'react'
|
||||
import { createContext, use, useMemo, useRef, useState } from 'react'
|
||||
import { createContext, useContext, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import type { InlineBlockNode } from '../../../features/blocks/client/nodes/InlineBlocksNode.js'
|
||||
import type { LexicalRichTextFieldProps } from '../../../types.js'
|
||||
@@ -140,11 +140,11 @@ export const EditorConfigProvider = ({
|
||||
],
|
||||
)
|
||||
|
||||
return <Context value={editorContext}>{children}</Context>
|
||||
return <Context.Provider value={editorContext}>{children}</Context.Provider>
|
||||
}
|
||||
|
||||
export const useEditorConfigContext = (): EditorConfigContextType => {
|
||||
const context = use(Context)
|
||||
const context = useContext(Context)
|
||||
if (context === undefined) {
|
||||
throw new Error('useEditorConfigContext must be used within an EditorConfigProvider')
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-slate",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"description": "The officially supported Slate richtext adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -25,17 +25,17 @@ export const ElementButtonProvider: React.FC<
|
||||
const { children, ...rest } = props
|
||||
|
||||
return (
|
||||
<ElementButtonContext
|
||||
<ElementButtonContext.Provider
|
||||
value={{
|
||||
...rest,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ElementButtonContext>
|
||||
</ElementButtonContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useElementButton = () => {
|
||||
const path = React.use(ElementButtonContext)
|
||||
const path = React.useContext(ElementButtonContext)
|
||||
return path
|
||||
}
|
||||
|
||||
@@ -33,17 +33,17 @@ export const ElementProvider: React.FC<
|
||||
const { childNodes, children, ...rest } = props
|
||||
|
||||
return (
|
||||
<ElementContext
|
||||
<ElementContext.Provider
|
||||
value={{
|
||||
...rest,
|
||||
children: childNodes,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ElementContext>
|
||||
</ElementContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useElement = <T,>(): ElementContextType<T> => {
|
||||
return React.use(ElementContext) as ElementContextType<T>
|
||||
return React.useContext(ElementContext) as ElementContextType<T>
|
||||
}
|
||||
|
||||
@@ -24,17 +24,17 @@ export const LeafButtonProvider: React.FC<
|
||||
const { children, ...rest } = props
|
||||
|
||||
return (
|
||||
<LeafButtonContext
|
||||
<LeafButtonContext.Provider
|
||||
value={{
|
||||
...rest,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</LeafButtonContext>
|
||||
</LeafButtonContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useLeafButton = () => {
|
||||
const path = React.use(LeafButtonContext)
|
||||
const path = React.useContext(LeafButtonContext)
|
||||
return path
|
||||
}
|
||||
|
||||
@@ -32,18 +32,18 @@ export const LeafProvider: React.FC<
|
||||
const { children, result, ...rest } = props
|
||||
|
||||
return (
|
||||
<LeafContext
|
||||
<LeafContext.Provider
|
||||
value={{
|
||||
...rest,
|
||||
children: result,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</LeafContext>
|
||||
</LeafContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useLeaf = () => {
|
||||
const path = React.use(LeafContext)
|
||||
const path = React.useContext(LeafContext)
|
||||
return path
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import React, { createContext, type ReactNode, use } from 'react'
|
||||
import React, { createContext, type ReactNode, useContext } from 'react'
|
||||
|
||||
interface SlateProps {
|
||||
schemaPath: string
|
||||
@@ -15,11 +15,11 @@ export function SlatePropsProvider({
|
||||
children: ReactNode
|
||||
schemaPath: string
|
||||
}) {
|
||||
return <SlatePropsContext value={{ schemaPath }}>{children}</SlatePropsContext>
|
||||
return <SlatePropsContext.Provider value={{ schemaPath }}>{children}</SlatePropsContext.Provider>
|
||||
}
|
||||
|
||||
export function useSlateProps() {
|
||||
const context = use(SlatePropsContext)
|
||||
const context = useContext(SlatePropsContext)
|
||||
if (!context) {
|
||||
throw new Error('useSlateProps must be used within SlatePropsProvider')
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-azure",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"description": "Payload storage adapter for Azure Blob Storage",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-gcs",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"description": "Payload storage adapter for Google Cloud Storage",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-s3",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"description": "Payload storage adapter for Amazon S3",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -52,7 +52,6 @@
|
||||
"@payloadcms/plugin-cloud-storage": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@smithy/node-http-handler": "4.0.3",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -5,7 +5,6 @@ import type {
|
||||
CollectionOptions,
|
||||
GeneratedAdapter,
|
||||
} from '@payloadcms/plugin-cloud-storage/types'
|
||||
import type { NodeHttpHandlerOptions } from '@smithy/node-http-handler'
|
||||
import type { Config, Plugin, UploadCollectionSlug } from 'payload'
|
||||
|
||||
import * as AWS from '@aws-sdk/client-s3'
|
||||
@@ -65,31 +64,16 @@ export type S3StorageOptions = {
|
||||
|
||||
type S3StoragePlugin = (storageS3Args: S3StorageOptions) => Plugin
|
||||
|
||||
let storageClient: AWS.S3 | null = null
|
||||
|
||||
const defaultRequestHandlerOpts: NodeHttpHandlerOptions = {
|
||||
httpAgent: {
|
||||
keepAlive: true,
|
||||
maxSockets: 100,
|
||||
},
|
||||
httpsAgent: {
|
||||
keepAlive: true,
|
||||
maxSockets: 100,
|
||||
},
|
||||
}
|
||||
|
||||
export const s3Storage: S3StoragePlugin =
|
||||
(s3StorageOptions: S3StorageOptions) =>
|
||||
(incomingConfig: Config): Config => {
|
||||
let storageClient: AWS.S3 | null = null
|
||||
|
||||
const getStorageClient: () => AWS.S3 = () => {
|
||||
if (storageClient) {
|
||||
return storageClient
|
||||
}
|
||||
|
||||
storageClient = new AWS.S3({
|
||||
requestHandler: defaultRequestHandlerOpts,
|
||||
...(s3StorageOptions.config ?? {}),
|
||||
})
|
||||
storageClient = new AWS.S3(s3StorageOptions.config ?? {})
|
||||
return storageClient
|
||||
}
|
||||
|
||||
|
||||
@@ -24,12 +24,6 @@ const isNodeReadableStream = (body: unknown): body is Readable => {
|
||||
)
|
||||
}
|
||||
|
||||
const destroyStream = (object: AWS.GetObjectOutput | undefined) => {
|
||||
if (object?.Body && isNodeReadableStream(object.Body)) {
|
||||
object.Body.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
// Convert a stream into a promise that resolves with a Buffer
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const streamToBuffer = async (readableStream: any) => {
|
||||
@@ -42,13 +36,12 @@ const streamToBuffer = async (readableStream: any) => {
|
||||
|
||||
export const getHandler = ({ bucket, collection, getStorageClient }: Args): StaticHandler => {
|
||||
return async (req, { params: { clientUploadContext, filename } }) => {
|
||||
let object: AWS.GetObjectOutput | undefined = undefined
|
||||
try {
|
||||
const prefix = await getFilePrefix({ clientUploadContext, collection, filename, req })
|
||||
|
||||
const key = path.posix.join(prefix, filename)
|
||||
|
||||
object = await getStorageClient().getObject({
|
||||
const object = await getStorageClient().getObject({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
})
|
||||
@@ -61,7 +54,7 @@ export const getHandler = ({ bucket, collection, getStorageClient }: Args): Stat
|
||||
const objectEtag = object.ETag
|
||||
|
||||
if (etagFromHeaders && etagFromHeaders === objectEtag) {
|
||||
return new Response(null, {
|
||||
const response = new Response(null, {
|
||||
headers: new Headers({
|
||||
'Accept-Ranges': String(object.AcceptRanges),
|
||||
'Content-Length': String(object.ContentLength),
|
||||
@@ -70,6 +63,13 @@ export const getHandler = ({ bucket, collection, getStorageClient }: Args): Stat
|
||||
}),
|
||||
status: 304,
|
||||
})
|
||||
|
||||
// Manually destroy stream before returning cached results to close socket
|
||||
if (object.Body && isNodeReadableStream(object.Body)) {
|
||||
object.Body.destroy()
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// On error, manually destroy stream to close socket
|
||||
@@ -99,8 +99,6 @@ export const getHandler = ({ bucket, collection, getStorageClient }: Args): Stat
|
||||
} catch (err) {
|
||||
req.payload.logger.error(err)
|
||||
return new Response('Internal Server Error', { status: 500 })
|
||||
} finally {
|
||||
destroyStream(object)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-uploadthing",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"description": "Payload storage adapter for uploadthing",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-vercel-blob",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"description": "Payload storage adapter for Vercel Blob Storage",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/translations",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/ui",
|
||||
"version": "3.28.1",
|
||||
"version": "3.28.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -514,7 +514,7 @@ export function FormsManagerProvider({ children }: FormsManagerProps) {
|
||||
])
|
||||
|
||||
return (
|
||||
<Context
|
||||
<Context.Provider
|
||||
value={{
|
||||
activeIndex: state.activeIndex,
|
||||
addFiles,
|
||||
@@ -545,10 +545,10 @@ export function FormsManagerProvider({ children }: FormsManagerProps) {
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</Context>
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useFormsManager() {
|
||||
return React.use(Context)
|
||||
return React.useContext(Context)
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ export function BulkUploadProvider({ children }: { readonly children: React.Reac
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Context
|
||||
<Context.Provider
|
||||
value={{
|
||||
collectionSlug: collection,
|
||||
currentActivePath,
|
||||
@@ -164,11 +164,11 @@ export function BulkUploadProvider({ children }: { readonly children: React.Reac
|
||||
{children}
|
||||
<BulkUploadDrawer />
|
||||
</React.Fragment>
|
||||
</Context>
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useBulkUpload = () => React.use(Context)
|
||||
export const useBulkUpload = () => React.useContext(Context)
|
||||
|
||||
export function useBulkUploadDrawerSlug() {
|
||||
const depth = useDrawerDepth()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import React, { createContext, use } from 'react'
|
||||
import React, { createContext, useContext } from 'react'
|
||||
|
||||
type ContextType = {
|
||||
isCollapsed: boolean
|
||||
@@ -32,7 +32,7 @@ export const CollapsibleProvider: React.FC<{
|
||||
}
|
||||
}, [isCollapsed, isWithinCollapsible, toggle, parentIsCollapsed, isVisible])
|
||||
|
||||
return <Context value={contextValue}>{children}</Context>
|
||||
return <Context.Provider value={contextValue}>{children}</Context.Provider>
|
||||
}
|
||||
|
||||
export const useCollapsible = (): ContextType => use(Context)
|
||||
export const useCollapsible = (): ContextType => useContext(Context)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ClientCollectionConfig, Data, FormState, TypeWithID } from 'payload'
|
||||
|
||||
import { createContext, use } from 'react'
|
||||
import { createContext, useContext } from 'react'
|
||||
|
||||
export type DocumentDrawerContextProps = {
|
||||
readonly clearDoc?: () => void
|
||||
@@ -31,11 +31,15 @@ export const DocumentDrawerContextProvider: React.FC<
|
||||
children: React.ReactNode
|
||||
} & DocumentDrawerContextProps
|
||||
> = ({ children, ...rest }) => {
|
||||
return <DocumentDrawerCallbacksContext value={rest}>{children}</DocumentDrawerCallbacksContext>
|
||||
return (
|
||||
<DocumentDrawerCallbacksContext.Provider value={rest}>
|
||||
{children}
|
||||
</DocumentDrawerCallbacksContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useDocumentDrawerContext = (): DocumentDrawerContextType => {
|
||||
const context = use(DocumentDrawerCallbacksContext)
|
||||
const context = useContext(DocumentDrawerCallbacksContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useDocumentDrawerContext must be used within a DocumentDrawerProvider')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import { Modal, useModal } from '@faceless-ui/modal'
|
||||
import React, { createContext, use, useCallback, useEffect, useState } from 'react'
|
||||
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
||||
|
||||
import type { Props, TogglerProps } from './types.js'
|
||||
|
||||
@@ -140,7 +140,7 @@ export const DrawerDepthProvider: React.FC<{ children: React.ReactNode }> = ({ c
|
||||
const parentDepth = useDrawerDepth()
|
||||
const depth = parentDepth + 1
|
||||
|
||||
return <DrawerDepthContext value={depth}>{children}</DrawerDepthContext>
|
||||
return <DrawerDepthContext.Provider value={depth}>{children}</DrawerDepthContext.Provider>
|
||||
}
|
||||
|
||||
export const useDrawerDepth = (): number => use(DrawerDepthContext)
|
||||
export const useDrawerDepth = (): number => useContext(DrawerDepthContext)
|
||||
|
||||
@@ -266,7 +266,7 @@ export const EditManyDrawerContent: React.FC<
|
||||
unpublishedVersionCount={0}
|
||||
versionCount={0}
|
||||
>
|
||||
<OperationContext value="update">
|
||||
<OperationContext.Provider value="update">
|
||||
<div className={`${baseClass}__main`}>
|
||||
<div className={`${baseClass}__header`}>
|
||||
<h2 className={`${baseClass}__header__title`}>
|
||||
@@ -332,7 +332,7 @@ export const EditManyDrawerContent: React.FC<
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</OperationContext>
|
||||
</OperationContext.Provider>
|
||||
</DocumentInfoProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CollectionSlug, Data, ListQuery } from 'payload'
|
||||
|
||||
import { createContext, use } from 'react'
|
||||
import { createContext, useContext } from 'react'
|
||||
|
||||
import type { useSelection } from '../../providers/Selection/index.js'
|
||||
import type { UseDocumentDrawer } from '../DocumentDrawer/types.js'
|
||||
@@ -40,14 +40,14 @@ export const ListDrawerContextProvider: React.FC<
|
||||
} & ListDrawerContextProps
|
||||
> = ({ children, ...rest }) => {
|
||||
return (
|
||||
<ListDrawerContext value={{ isInDrawer: Boolean(rest.drawerSlug), ...rest }}>
|
||||
<ListDrawerContext.Provider value={{ isInDrawer: Boolean(rest.drawerSlug), ...rest }}>
|
||||
{children}
|
||||
</ListDrawerContext>
|
||||
</ListDrawerContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useListDrawerContext = (): ListDrawerContextType => {
|
||||
const context = use(ListDrawerContext)
|
||||
const context = useContext(ListDrawerContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useListDrawerContext must be used within a ListDrawerContextProvider')
|
||||
|
||||
@@ -54,7 +54,7 @@ export const LoadingOverlayProvider: React.FC<{ children?: React.ReactNode }> =
|
||||
)
|
||||
|
||||
return (
|
||||
<Context
|
||||
<Context.Provider
|
||||
value={{
|
||||
isOnScreen: isMounted,
|
||||
toggleLoadingOverlay,
|
||||
@@ -69,12 +69,12 @@ export const LoadingOverlayProvider: React.FC<{ children?: React.ReactNode }> =
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</Context>
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useLoadingOverlay = (): LoadingOverlayContext => {
|
||||
const contextHook = React.use(Context)
|
||||
const contextHook = React.useContext(Context)
|
||||
if (contextHook === undefined) {
|
||||
throw new Error('useLoadingOverlay must be used within a LoadingOverlayProvider')
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ export const NavContext = React.createContext<NavContextType>({
|
||||
shouldAnimate: false,
|
||||
})
|
||||
|
||||
export const useNav = () => React.use(NavContext)
|
||||
export const useNav = () => React.useContext(NavContext)
|
||||
|
||||
const getNavPreference = async (getPreference): Promise<boolean> => {
|
||||
const navPrefs = await getPreference('nav')
|
||||
@@ -111,8 +111,8 @@ export const NavProvider: React.FC<{
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<NavContext value={{ hydrated, navOpen, navRef, setNavOpen, shouldAnimate }}>
|
||||
<NavContext.Provider value={{ hydrated, navOpen, navRef, setNavOpen, shouldAnimate }}>
|
||||
{children}
|
||||
</NavContext>
|
||||
</NavContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
@import '../../scss/styles.scss';
|
||||
|
||||
@layer payload-default {
|
||||
.sort-header {
|
||||
display: flex;
|
||||
gap: calc(var(--base) / 2);
|
||||
align-items: center;
|
||||
|
||||
&__buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: calc(var(--base) / 4);
|
||||
}
|
||||
|
||||
&__button {
|
||||
margin: 0;
|
||||
padding: 0 !important;
|
||||
opacity: 0.3;
|
||||
padding: calc(var(--base) / 4);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
&.sort-header--active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.btn {
|
||||
opacity: 0.4;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
&--appearance-condensed {
|
||||
gap: calc(var(--base) / 4);
|
||||
|
||||
.sort-header__buttons {
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React, { useRef } from 'react'
|
||||
|
||||
import { SortDownIcon, SortUpIcon } from '../../icons/Sort/index.js'
|
||||
import { useListQuery } from '../../providers/ListQuery/index.js'
|
||||
import './index.scss'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
|
||||
export type SortHeaderProps = {
|
||||
readonly appearance?: 'condensed' | 'default'
|
||||
readonly disable?: boolean
|
||||
}
|
||||
|
||||
const baseClass = 'sort-header'
|
||||
|
||||
function useSort() {
|
||||
const { handleSortChange, query } = useListQuery()
|
||||
const sort = useRef<'asc' | 'desc'>(query.sort === '-_order' ? 'desc' : 'asc')
|
||||
const isActive = query.sort === '-_order' || query.sort === '_order'
|
||||
|
||||
const handleSortPress = () => {
|
||||
// If it's already sorted by the "_order" field, toggle between "asc" and "desc"
|
||||
if (isActive) {
|
||||
void handleSortChange(sort.current === 'asc' ? '-_order' : '_order')
|
||||
sort.current = sort.current === 'asc' ? 'desc' : 'asc'
|
||||
return
|
||||
}
|
||||
// If NOT sorted by the "_order" field, sort by that field but do not toggle the current value of "asc" or "desc".
|
||||
void handleSortChange(sort.current === 'asc' ? '_order' : '-_order')
|
||||
}
|
||||
|
||||
return { handleSortPress, isActive, sort }
|
||||
}
|
||||
|
||||
export const SortHeader: React.FC<SortHeaderProps> = (props) => {
|
||||
const { appearance } = props
|
||||
const { handleSortPress, isActive, sort } = useSort()
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[baseClass, appearance && `${baseClass}--appearance-${appearance}`]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
<div className={`${baseClass}__buttons`}>
|
||||
{sort.current === 'desc' ? (
|
||||
<button
|
||||
aria-label={t('general:sortByLabelDirection', {
|
||||
direction: t('general:ascending'),
|
||||
label: 'Order',
|
||||
})}
|
||||
className={`${baseClass}__button ${baseClass}__${sort.current} ${isActive ? `${baseClass}--active` : ''}`}
|
||||
onClick={handleSortPress}
|
||||
type="button"
|
||||
>
|
||||
<SortDownIcon />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
aria-label={t('general:sortByLabelDirection', {
|
||||
direction: t('general:descending'),
|
||||
label: 'Order',
|
||||
})}
|
||||
className={`${baseClass}__button ${baseClass}__${sort.current} ${isActive ? `${baseClass}--active` : ''}`}
|
||||
onClick={handleSortPress}
|
||||
type="button"
|
||||
>
|
||||
<SortUpIcon />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
@import '../../scss/styles.scss';
|
||||
|
||||
@layer payload-default {
|
||||
.sort-row {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
|
||||
&.active {
|
||||
cursor: grab;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
margin-left: -2px;
|
||||
margin-top: -2px;
|
||||
display: block;
|
||||
width: min-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import { DragHandleIcon } from '../../icons/DragHandle/index.js'
|
||||
import './index.scss'
|
||||
import { useListQuery } from '../../providers/ListQuery/index.js'
|
||||
|
||||
const baseClass = 'sort-row'
|
||||
|
||||
export const SortRow = () => {
|
||||
const { query } = useListQuery()
|
||||
const isActive = query.sort === '_order' || query.sort === '-_order'
|
||||
|
||||
return (
|
||||
<div className={`${baseClass} ${isActive ? 'active' : ''}`} role="button" tabIndex={0}>
|
||||
<DragHandleIcon className={`${baseClass}__icon`} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client'
|
||||
import React, { createContext, use, useState } from 'react'
|
||||
import React, { createContext, useContext, useState } from 'react'
|
||||
|
||||
import type { ContextType } from './types.js'
|
||||
|
||||
export const useStepNav = (): ContextType => use(Context)
|
||||
export const useStepNav = (): ContextType => useContext(Context)
|
||||
|
||||
export const Context = createContext({} as ContextType)
|
||||
|
||||
@@ -11,13 +11,13 @@ export const StepNavProvider: React.FC<{ children?: React.ReactNode }> = ({ chil
|
||||
const [stepNav, setStepNav] = useState([])
|
||||
|
||||
return (
|
||||
<Context
|
||||
<Context.Provider
|
||||
value={{
|
||||
setStepNav,
|
||||
stepNav,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Context>
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { ClientCollectionConfig, Column } from 'payload'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { useListQuery } from '../../providers/ListQuery/index.js'
|
||||
import { DraggableSortableItem } from '../DraggableSortable/DraggableSortableItem/index.js'
|
||||
import { DraggableSortable } from '../DraggableSortable/index.js'
|
||||
|
||||
const baseClass = 'table'
|
||||
|
||||
export type Props = {
|
||||
readonly appearance?: 'condensed' | 'default'
|
||||
readonly collection: ClientCollectionConfig
|
||||
readonly columns?: Column[]
|
||||
readonly data: Record<string, unknown>[]
|
||||
}
|
||||
|
||||
export const OrderableTable: React.FC<Props> = ({
|
||||
appearance = 'default',
|
||||
collection,
|
||||
columns,
|
||||
data: initialData,
|
||||
}) => {
|
||||
const { data: listQueryData, handleSortChange, query } = useListQuery()
|
||||
// Use the data from ListQueryProvider if available, otherwise use the props
|
||||
const serverData = listQueryData?.docs || initialData
|
||||
|
||||
// Local state to track the current order of rows
|
||||
const [localData, setLocalData] = useState(serverData)
|
||||
|
||||
// id -> index for each column
|
||||
const [cellMap, setCellMap] = useState<Record<string, number>>({})
|
||||
|
||||
// Update local data when server data changes
|
||||
useEffect(() => {
|
||||
setLocalData(serverData)
|
||||
setCellMap(
|
||||
Object.fromEntries(serverData.map((item, index) => [String(item.id ?? item._id), index])),
|
||||
)
|
||||
}, [serverData])
|
||||
|
||||
const activeColumns = columns?.filter((col) => col?.active)
|
||||
|
||||
if (
|
||||
!activeColumns ||
|
||||
activeColumns.filter((col) => !['_dragHandle', '_select'].includes(col.accessor)).length === 0
|
||||
) {
|
||||
return <div>No columns selected</div>
|
||||
}
|
||||
|
||||
const handleDragEnd = async ({ moveFromIndex, moveToIndex }) => {
|
||||
if (query.sort !== '_order' && query.sort !== '-_order') {
|
||||
toast.warning('To reorder the rows you must first sort them by the "Order" column')
|
||||
return
|
||||
}
|
||||
|
||||
if (moveFromIndex === moveToIndex) {
|
||||
return
|
||||
}
|
||||
|
||||
const movedId = localData[moveFromIndex].id ?? localData[moveFromIndex]._id
|
||||
const newBeforeRow =
|
||||
moveToIndex > moveFromIndex ? localData[moveToIndex] : localData[moveToIndex - 1]
|
||||
const newAfterRow =
|
||||
moveToIndex > moveFromIndex ? localData[moveToIndex + 1] : localData[moveToIndex]
|
||||
|
||||
// Store the original data for rollback
|
||||
const previousData = [...localData]
|
||||
|
||||
// Optimisitc update of local state to reorder the rows
|
||||
setLocalData((currentData) => {
|
||||
const newData = [...currentData]
|
||||
// Update the rendered cell for the moved row to show "pending"
|
||||
newData[moveFromIndex]._order = `pending`
|
||||
// Move the item in the array
|
||||
newData.splice(moveToIndex, 0, newData.splice(moveFromIndex, 1)[0])
|
||||
return newData
|
||||
})
|
||||
|
||||
try {
|
||||
type KeyAndID = {
|
||||
id: string
|
||||
key: string
|
||||
}
|
||||
|
||||
const target: KeyAndID = newBeforeRow
|
||||
? {
|
||||
id: newBeforeRow.id ?? newBeforeRow._id,
|
||||
key: newBeforeRow._order,
|
||||
}
|
||||
: {
|
||||
id: newAfterRow.id ?? newAfterRow._id,
|
||||
key: newAfterRow._order,
|
||||
}
|
||||
|
||||
const newKeyWillBe =
|
||||
(newBeforeRow && query.sort === '_order') || (!newBeforeRow && query.sort === '-_order')
|
||||
? 'greater'
|
||||
: 'less'
|
||||
|
||||
const response = await fetch(`/api/${collection.slug}/reorder`, {
|
||||
body: JSON.stringify({
|
||||
docsToMove: [movedId],
|
||||
newKeyWillBe,
|
||||
target,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
if (response.status === 403) {
|
||||
throw new Error('You do not have permission to reorder these rows')
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
'Failed to reorder. This can happen if you reorder several rows too quickly. Please try again.',
|
||||
)
|
||||
}
|
||||
|
||||
// This will trigger an update of the data from the server
|
||||
handleSortChange(query.sort as string).catch((error) => {
|
||||
throw error
|
||||
})
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err.message : String(err)
|
||||
// Rollback to previous state if the request fails
|
||||
setLocalData(previousData)
|
||||
toast.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
const rowIds = localData.map((row) => row.id ?? row._id)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[baseClass, appearance && `${baseClass}--appearance-${appearance}`]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
<DraggableSortable ids={rowIds} onDragEnd={handleDragEnd}>
|
||||
<table cellPadding="0" cellSpacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
{activeColumns.map((col, i) => (
|
||||
<th id={`heading-${col.accessor}`} key={i}>
|
||||
{col.Heading}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{localData.map((row, rowIndex) => (
|
||||
<DraggableSortableItem id={rowIds[rowIndex]} key={rowIds[rowIndex]}>
|
||||
{({ attributes, listeners, setNodeRef, transform, transition }) => (
|
||||
<tr
|
||||
className={`row-${rowIndex + 1}`}
|
||||
ref={setNodeRef}
|
||||
style={{
|
||||
transform,
|
||||
transition,
|
||||
}}
|
||||
>
|
||||
{activeColumns.map((col, colIndex) => {
|
||||
const { accessor } = col
|
||||
|
||||
// Use the cellMap to find which index in the renderedCells to use
|
||||
const cell = col.renderedCells[cellMap[row.id ?? row._id]]
|
||||
|
||||
// For drag handles, wrap in div with drag attributes
|
||||
if (accessor === '_dragHandle') {
|
||||
return (
|
||||
<td className={`cell-${accessor}`} key={colIndex}>
|
||||
<div {...attributes} {...listeners}>
|
||||
{cell}
|
||||
</div>
|
||||
</td>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<td className={`cell-${accessor}`} key={colIndex}>
|
||||
{cell}
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
)}
|
||||
</DraggableSortableItem>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</DraggableSortable>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import type { TypeWithID } from 'payload'
|
||||
|
||||
import React, { createContext, use, useCallback, useEffect, useReducer, useRef } from 'react'
|
||||
import React, { createContext, useCallback, useContext, useEffect, useReducer, useRef } from 'react'
|
||||
|
||||
import { useDebounce } from '../../../hooks/useDebounce.js'
|
||||
import { useConfig } from '../../../providers/Config/index.js'
|
||||
@@ -115,7 +115,7 @@ export const RelationshipProvider: React.FC<{ readonly children?: React.ReactNod
|
||||
[],
|
||||
)
|
||||
|
||||
return <Context value={{ documents, getRelationships }}>{children}</Context>
|
||||
return <Context.Provider value={{ documents, getRelationships }}>{children}</Context.Provider>
|
||||
}
|
||||
|
||||
export const useListRelationships = (): ListRelationshipContext => use(Context)
|
||||
export const useListRelationships = (): ListRelationshipContext => useContext(Context)
|
||||
|
||||
@@ -13,7 +13,7 @@ const baseClass = 'default-cell'
|
||||
const CellPropsContext = React.createContext<DefaultCellComponentProps | null>(null)
|
||||
|
||||
export const useCellProps = (): DefaultCellComponentProps | null =>
|
||||
React.use(CellPropsContext)
|
||||
React.useContext(CellPropsContext)
|
||||
|
||||
export const RenderDefaultCell: React.FC<{
|
||||
clientProps: DefaultCellComponentProps
|
||||
@@ -44,8 +44,8 @@ export const RenderDefaultCell: React.FC<{
|
||||
}
|
||||
|
||||
return (
|
||||
<CellPropsContext value={propsToPass}>
|
||||
<CellPropsContext.Provider value={propsToPass}>
|
||||
{isLinkedColumn && LinkedCellOverride ? LinkedCellOverride : <DefaultCell {...propsToPass} />}
|
||||
</CellPropsContext>
|
||||
</CellPropsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createContext, use } from 'react'
|
||||
import { createContext, useContext } from 'react'
|
||||
|
||||
import type { ITableColumns } from './types.js'
|
||||
|
||||
export const TableColumnContext = createContext<ITableColumns>({} as ITableColumns)
|
||||
|
||||
export const useTableColumns = (): ITableColumns => use(TableColumnContext)
|
||||
export const useTableColumns = (): ITableColumns => useContext(TableColumnContext)
|
||||
|
||||
@@ -90,7 +90,7 @@ export const TableColumnsProvider: React.FC<TableColumnsProviderProps> = ({
|
||||
}, [defaultColumns, setActiveColumns])
|
||||
|
||||
return (
|
||||
<TableColumnContext
|
||||
<TableColumnContext.Provider
|
||||
value={{
|
||||
columns: columnState,
|
||||
LinkedCellOverride,
|
||||
@@ -101,6 +101,6 @@ export const TableColumnsProvider: React.FC<TableColumnsProviderProps> = ({
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TableColumnContext>
|
||||
</TableColumnContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { ClientBlock, ClientField, Labels, Row, SanitizedFieldPermissions } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import React from 'react'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import type { UseDraggableSortableReturn } from '../../elements/DraggableSortable/useDraggableSortable/types.js'
|
||||
import type { RenderFieldsProps } from '../../forms/RenderFields/types.js'
|
||||
@@ -13,7 +13,6 @@ import { Pill } from '../../elements/Pill/index.js'
|
||||
import { ShimmerEffect } from '../../elements/ShimmerEffect/index.js'
|
||||
import { useFormSubmitted } from '../../forms/Form/context.js'
|
||||
import { RenderFields } from '../../forms/RenderFields/index.js'
|
||||
import { RowLabel } from '../../forms/RowLabel/index.js'
|
||||
import { useThrottledValue } from '../../hooks/useThrottledValue.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { RowActions } from './RowActions.js'
|
||||
@@ -147,28 +146,21 @@ export const BlockRow: React.FC<BlocksFieldProps> = ({
|
||||
<ShimmerEffect height="1rem" width="8rem" />
|
||||
) : (
|
||||
<div className={`${baseClass}__block-header`}>
|
||||
<RowLabel
|
||||
CustomComponent={Label}
|
||||
label={
|
||||
<>
|
||||
<span className={`${baseClass}__block-number`}>
|
||||
{String(rowIndex + 1).padStart(2, '0')}
|
||||
</span>
|
||||
<Pill
|
||||
className={`${baseClass}__block-pill ${baseClass}__block-pill-${row.blockType}`}
|
||||
pillStyle="white"
|
||||
>
|
||||
{getTranslation(block.labels.singular, i18n)}
|
||||
</Pill>
|
||||
{showBlockName && (
|
||||
<SectionTitle path={`${path}.blockName`} readOnly={readOnly} />
|
||||
)}
|
||||
</>
|
||||
}
|
||||
path={path}
|
||||
rowNumber={rowIndex}
|
||||
/>
|
||||
{fieldHasErrors && <ErrorPill count={errorCount} i18n={i18n} withMessage />}
|
||||
{Label || (
|
||||
<Fragment>
|
||||
<span className={`${baseClass}__block-number`}>
|
||||
{String(rowIndex + 1).padStart(2, '0')}
|
||||
</span>
|
||||
<Pill
|
||||
className={`${baseClass}__block-pill ${baseClass}__block-pill-${row.blockType}`}
|
||||
pillStyle="white"
|
||||
>
|
||||
{getTranslation(block.labels.singular, i18n)}
|
||||
</Pill>
|
||||
{showBlockName && <SectionTitle path={`${path}.blockName`} readOnly={readOnly} />}
|
||||
{fieldHasErrors && <ErrorPill count={errorCount} i18n={i18n} withMessage />}
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import React, { createContext, use } from 'react'
|
||||
import React, { createContext, useContext } from 'react'
|
||||
|
||||
export const GroupContext = createContext(false)
|
||||
|
||||
@@ -7,7 +7,7 @@ export const GroupProvider: React.FC<{ children?: React.ReactNode; withinGroup?:
|
||||
children,
|
||||
withinGroup = true,
|
||||
}) => {
|
||||
return <GroupContext value={withinGroup}>{children}</GroupContext>
|
||||
return <GroupContext.Provider value={withinGroup}>{children}</GroupContext.Provider>
|
||||
}
|
||||
|
||||
export const useGroup = (): boolean => use(GroupContext)
|
||||
export const useGroup = (): boolean => useContext(GroupContext)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user