@payloadcms/figma 0.1.0-alpha.4 → 0.1.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/db-content-api/index.js +31 -17
  2. package/dist/db-content-api/utilities/joins.js +5 -1
  3. package/dist/db-content-api/utilities/meta/resolvesToRelationshipValue.d.ts +10 -0
  4. package/dist/db-content-api/utilities/meta/resolvesToRelationshipValue.js +95 -0
  5. package/dist/db-content-api/utilities/meta/stringifyIdsInWhere.d.ts +9 -0
  6. package/dist/db-content-api/utilities/meta/stringifyIdsInWhere.js +112 -0
  7. package/dist/db-content-api/utilities/where.d.ts +2 -6
  8. package/dist/db-content-api/utilities/where.js +20 -11
  9. package/dist/exports/views.d.ts +1 -0
  10. package/dist/exports/views.js +1 -0
  11. package/dist/imageSizeOptions.d.ts +23 -0
  12. package/dist/imageSizeOptions.js +26 -0
  13. package/dist/index.d.ts +1 -0
  14. package/dist/oauth/endpoints/getLoginEndpoint.js +5 -2
  15. package/dist/oauth/index.js +8 -7
  16. package/dist/oauth/utilities/getAdminCollectionSlug.d.ts +1 -1
  17. package/dist/oauth/utilities/getAuthorizeURL.d.ts +1 -0
  18. package/dist/oauth/utilities/getAuthorizeURL.js +5 -1
  19. package/dist/plugin/build-config.js +22 -9
  20. package/dist/plugin/cloud-limits/CloudLimitModal/index.d.ts +2 -3
  21. package/dist/plugin/cloud-limits/CloudLimitModal/index.js +4 -16
  22. package/dist/plugin/cloud-limits/CloudLimitModal/index.scss +5 -0
  23. package/dist/plugin/cloud-limits/CloudLimitProvider/index.d.ts +0 -3
  24. package/dist/plugin/cloud-limits/CloudLimitProvider/index.js +65 -124
  25. package/dist/plugin/cloud-limits/LimitReachedView/index.css +14 -0
  26. package/dist/plugin/cloud-limits/LimitReachedView/index.d.ts +4 -0
  27. package/dist/plugin/cloud-limits/LimitReachedView/index.js +52 -0
  28. package/dist/plugin/cloud-limits/SlugRegistryContext/index.d.ts +7 -2
  29. package/dist/plugin/cloud-limits/SlugTracker/index.d.ts +0 -8
  30. package/dist/plugin/cloud-limits/SlugTracker/index.js +14 -10
  31. package/dist/plugin/cloud-limits/routes.d.ts +2 -0
  32. package/dist/plugin/cloud-limits/routes.js +4 -0
  33. package/dist/plugin/cloud-limits/server/redirectInitialAutosaveCreate.d.ts +9 -0
  34. package/dist/plugin/cloud-limits/server/redirectInitialAutosaveCreate.js +28 -0
  35. package/dist/plugin/cloud-limits/shared.d.ts +1 -1
  36. package/dist/plugin/cloud-limits/shared.js +25 -0
  37. package/dist/plugin/cloud-limits/utilities/cloudLimitInterceptor.d.ts +2 -0
  38. package/dist/plugin/cloud-limits/utilities/cloudLimitInterceptor.js +134 -0
  39. package/dist/plugin/cloud-limits/utilities/toasts.d.ts +4 -0
  40. package/dist/plugin/cloud-limits/utilities/toasts.js +19 -0
  41. package/dist/storage-content-api/buildBaseMediaResponseHeaders.d.ts +1 -0
  42. package/dist/storage-content-api/buildBaseMediaResponseHeaders.js +6 -0
  43. package/dist/storage-content-api/buildClientUploadFetchBackHeaders.d.ts +10 -0
  44. package/dist/storage-content-api/buildClientUploadFetchBackHeaders.js +21 -0
  45. package/dist/storage-content-api/buildMediaResponseHeaders.d.ts +4 -0
  46. package/dist/storage-content-api/buildMediaResponseHeaders.js +45 -0
  47. package/dist/storage-content-api/getInlineContentType.d.ts +1 -0
  48. package/dist/storage-content-api/getInlineContentType.js +33 -0
  49. package/dist/storage-content-api/hasSupportedContentEncoding.d.ts +1 -0
  50. package/dist/storage-content-api/hasSupportedContentEncoding.js +4 -0
  51. package/dist/storage-content-api/mediaErrorResponse.d.ts +1 -0
  52. package/dist/storage-content-api/mediaErrorResponse.js +12 -0
  53. package/dist/storage-content-api/staticHandler.js +82 -21
  54. package/dist/utils/adapters/nextjs.js +8 -7
  55. package/dist/utils/asset-collection.d.ts +9 -1
  56. package/dist/utils/asset-collection.js +64 -2
  57. package/dist/utils/s3-upload.js +4 -1
  58. package/package.json +14 -2
  59. package/dist/db-content-api/utilities/data/stringifyNumericIds.d.ts +0 -1
  60. package/dist/db-content-api/utilities/data/stringifyNumericIds.js +0 -19
@@ -11,6 +11,7 @@ import { scheduleExtractPlugin } from '../deploy/schedule-extract-plugin.js';
11
11
  import { health } from '../endpoints/health.js';
12
12
  import { schema } from '../endpoints/schema.js';
13
13
  import { oAuth2Plugin } from '../oauth/index.js';
14
+ import { getAdminCollectionSlug } from '../oauth/utilities/getAdminCollectionSlug.js';
14
15
  import { createStorageClient } from '../storage-content-api/client.js';
15
16
  import { contentApiStorageAdapter } from '../storage-content-api/index.js';
16
17
  import { translations as figmaTranslations } from '../translations/index.js';
@@ -19,6 +20,7 @@ import { getEnvVarSync } from '../utils/env-management.js';
19
20
  import * as log from '../utils/log.js';
20
21
  import { logMissingCliAuth } from './auth-preflight.js';
21
22
  import { logMissingContentSystemId } from './bootstrap-preflight.js';
23
+ import { LIMIT_REACHED_VIEW_KEY, LIMIT_REACHED_VIEW_PATH } from './cloud-limits/routes.js';
22
24
  import { getDevCookieNames } from './dev-cookie-names.js';
23
25
  import { createSandboxUploadFetchPlugin } from './sandbox-upload-fetch.js';
24
26
  /**
@@ -30,7 +32,7 @@ import { createSandboxUploadFetchPlugin } from './sandbox-upload-fetch.js';
30
32
  * KEEP_IN_SYNC(cloudflare/wrangler/sites-worker/src/auth.ts): FIGMA_INTERNAL_FORWARDED_HAS_AUTH_BYPASS_HEADER
31
33
  */ export const FIGMA_INTERNAL_FORWARDED_HAS_AUTH_BYPASS_HEADER = 'figma-internal-forwarded-has-auth-bypass';
32
34
  /**
33
- * Fields added to users collection for Figma profile info
35
+ * Fields added to the configured Payload Admin collection for Figma profile info
34
36
  */ const figmaUserFields = [
35
37
  {
36
38
  name: 'figmaHandle',
@@ -280,7 +282,8 @@ export async function buildFigmaConfig(config) {
280
282
  }
281
283
  // Determine database adapter based on environment
282
284
  let db;
283
- if (config.figma.useContentSystem === false) {
285
+ const usesContentSystem = config.figma.useContentSystem !== false;
286
+ if (!usesContentSystem) {
284
287
  db = config.db;
285
288
  } else if (process.env.FIGMA_CONTENT_API_ACCESS_KEY) {
286
289
  db = contentAPIAdapter({
@@ -316,6 +319,7 @@ export async function buildFigmaConfig(config) {
316
319
  });
317
320
  }
318
321
  const userJobsAccessRun = config.jobs?.access?.run;
322
+ const adminCollectionSlug = getAdminCollectionSlug(config);
319
323
  // Build complete config with Figma platform defaults
320
324
  const configWithFigmaDefaults = {
321
325
  ...config,
@@ -386,7 +390,7 @@ export async function buildFigmaConfig(config) {
386
390
  oAuth2Plugin({
387
391
  collections: [
388
392
  {
389
- slug: 'users',
393
+ slug: adminCollectionSlug,
390
394
  // Use adapter because Figma app doesn't support OIDC scopes
391
395
  authorizationURLParams: {
392
396
  response_mode: 'query',
@@ -414,9 +418,10 @@ export async function buildFigmaConfig(config) {
414
418
  })
415
419
  ]
416
420
  };
417
- // Inject figma fields into users collection
421
+ // Inject Figma-managed identity fields only into the Payload Admin collection.
418
422
  const collections = configWithFigmaDefaults.collections?.map((col)=>{
419
- const withSlugTracker = {
423
+ const views = col.admin?.components?.views;
424
+ const withLimitView = usesContentSystem ? {
420
425
  ...col,
421
426
  admin: {
422
427
  ...col.admin,
@@ -428,20 +433,28 @@ export async function buildFigmaConfig(config) {
428
433
  '@payloadcms/figma/client#FigmaCloudLimitSlugTracker',
429
434
  ...col.admin?.components?.edit?.beforeDocumentControls ?? []
430
435
  ]
436
+ },
437
+ views: {
438
+ ...views,
439
+ [LIMIT_REACHED_VIEW_KEY]: {
440
+ Component: '@payloadcms/figma/views#LimitReachedView',
441
+ exact: true,
442
+ path: LIMIT_REACHED_VIEW_PATH
443
+ }
431
444
  }
432
445
  }
433
446
  }
434
- };
435
- if (col.slug === 'users') {
447
+ } : col;
448
+ if (col.slug === adminCollectionSlug) {
436
449
  return {
437
- ...withSlugTracker,
450
+ ...withLimitView,
438
451
  fields: [
439
452
  ...col.fields || [],
440
453
  ...figmaUserFields
441
454
  ]
442
455
  };
443
456
  }
444
- return withSlugTracker;
457
+ return withLimitView;
445
458
  });
446
459
  if (collections) {
447
460
  configWithFigmaDefaults.collections = collections;
@@ -1,9 +1,8 @@
1
1
  import './index.scss';
2
2
  type Props = {
3
- adminRoute: string;
4
3
  collectionLabel?: string;
5
- collectionSlug?: string;
4
+ collectionListURL?: string;
6
5
  onRetry: () => void;
7
6
  };
8
- export declare function FigmaCloudLimitDocumentCountModal({ adminRoute, collectionLabel, collectionSlug, onRetry, }: Props): import("react").JSX.Element;
7
+ export declare function FigmaCloudLimitDocumentCountModal({ collectionLabel, collectionListURL, onRetry, }: Props): import("react").JSX.Element;
9
8
  export {};
@@ -1,25 +1,13 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Button, DialogBody, DialogCancel, DialogFooter, DialogHeader, DialogModal, useModal, useTranslation } from '@payloadcms/ui';
4
- import { useRouter } from 'next/navigation.js';
5
- import { useCallback } from 'react';
6
4
  import { ExternalLinkIcon } from '../icons/ExternalLinkIcon.js';
7
5
  import { cloudLimitDocumentCountModalSlug } from '../shared.js';
8
6
  import './index.scss';
9
7
  const baseClass = 'figma-cloud-limit-modal';
10
- export function FigmaCloudLimitDocumentCountModal({ adminRoute, collectionLabel, collectionSlug, onRetry }) {
8
+ export function FigmaCloudLimitDocumentCountModal({ collectionLabel, collectionListURL, onRetry }) {
11
9
  const { t } = useTranslation();
12
10
  const { closeModal } = useModal();
13
- const router = useRouter();
14
- const handleReviewClick = useCallback(()=>{
15
- closeModal(cloudLimitDocumentCountModalSlug);
16
- router.push(`${adminRoute}/collections/${collectionSlug}`);
17
- }, [
18
- adminRoute,
19
- closeModal,
20
- collectionSlug,
21
- router
22
- ]);
23
11
  return /*#__PURE__*/ _jsxs(DialogModal, {
24
12
  size: "small",
25
13
  slug: cloudLimitDocumentCountModalSlug,
@@ -38,10 +26,10 @@ export function FigmaCloudLimitDocumentCountModal({ adminRoute, collectionLabel,
38
26
  children: /*#__PURE__*/ _jsxs("div", {
39
27
  className: `${baseClass}__footer`,
40
28
  children: [
41
- collectionLabel && collectionSlug ? /*#__PURE__*/ _jsxs("button", {
29
+ collectionLabel && collectionListURL ? /*#__PURE__*/ _jsxs("a", {
42
30
  className: `${baseClass}__review-btn`,
43
- onClick: handleReviewClick,
44
- type: "button",
31
+ href: collectionListURL,
32
+ onClick: ()=>closeModal(cloudLimitDocumentCountModalSlug),
45
33
  children: [
46
34
  /*#__PURE__*/ _jsx("span", {
47
35
  className: `${baseClass}__review-btn-label`,
@@ -23,6 +23,11 @@
23
23
  min-width: 0;
24
24
  padding: 0;
25
25
 
26
+ .btn__content,
27
+ .btn__label {
28
+ min-width: 0;
29
+ }
30
+
26
31
  &:hover {
27
32
  color: var(--ramp-blue-700);
28
33
  }
@@ -1,7 +1,4 @@
1
1
  import type { ReactNode } from 'react';
2
- /**
3
- * Surfaces Content API cloud-limit errors in the admin UI.
4
- */
5
2
  export declare function FigmaCloudLimitProvider({ children }: {
6
3
  children: ReactNode;
7
4
  }): import("react").JSX.Element;
@@ -1,113 +1,40 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { getTranslation } from '@payloadcms/translations';
4
- import { FormErrorHandlerContext, toast, useConfig, useFormErrorHandler, useModal, useTranslation } from '@payloadcms/ui';
5
- import { hasAutosaveEnabled } from 'payload/shared';
4
+ import { FormErrorHandlerContext, useConfig, useFormErrorHandler, useModal, useTranslation } from '@payloadcms/ui';
5
+ import { DrawerDepthContext } from '@payloadcms/ui/elements/Drawer';
6
+ import { formatAdminURL, hasAutosaveEnabled } from 'payload/shared';
6
7
  import { useCallback, useEffect, useRef, useState } from 'react';
7
8
  import { FigmaCloudLimitDocumentCountModal } from '../CloudLimitModal/index.js';
8
- import { cloudLimitDocumentCountModalSlug, cloudLimitErrorCodes, cloudLimitToastKeyByCode, getCloudLimitErrorCode } from '../shared.js';
9
+ import { cloudLimitDocumentCountModalSlug, cloudLimitErrorCodes, getCloudLimitErrorCode } from '../shared.js';
9
10
  import { FigmaCloudLimitSlugRegistryContext } from '../SlugRegistryContext/index.js';
10
- const AUTOSAVE_TOAST_ID = 'figma-cloud-limit-doc-count';
11
- let isUploadFetchPatched = false;
12
- function slugFromPathSuffix(suffix) {
13
- return /^\/collections\/([^/?#]+)/.exec(suffix)?.[1];
14
- }
15
- /**
16
- * Surfaces Content API cloud-limit errors in the admin UI.
17
- */ export function FigmaCloudLimitProvider({ children }) {
11
+ import { cloudLimitInterceptor } from '../utilities/cloudLimitInterceptor.js';
12
+ import { dismissSubmittingToast, showCloudLimitToast } from '../utilities/toasts.js';
13
+ export function FigmaCloudLimitProvider({ children }) {
18
14
  const { openModal } = useModal();
19
15
  const { i18n, t } = useTranslation();
20
16
  const { config, getEntityConfig } = useConfig();
21
17
  const parentErrorHandler = useFormErrorHandler();
22
- const adminRouteRef = useRef(config.routes?.admin ?? '/admin');
23
- const apiRouteRef = useRef(config.routes?.api ?? '/api');
24
- const [collectionSlug, setCollectionSlug] = useState();
25
- const [collectionLabel, setCollectionLabel] = useState();
18
+ const adminRoute = config.routes?.admin ?? '/admin';
19
+ const apiRoute = config.routes?.api ?? '/api';
20
+ const [modal, setModal] = useState({
21
+ drawerDepth: 0
22
+ });
26
23
  const retryRef = useRef(undefined);
27
- // Ref so the handler always calls the latest getEntityConfig without being recreated.
28
- const getEntityConfigRef = useRef(getEntityConfig);
29
- getEntityConfigRef.current = getEntityConfig;
30
- const tRef = useRef(t);
31
- tRef.current = t;
32
- // Stack of active collection slugs. FigmaCloudLimitSlugTracker components push/pop as
33
- // documents mount/unmount (including drawers). The last entry is the active form's collection.
34
- const slugStackRef = useRef([]);
35
- const registerCollectionSlug = useCallback((slug)=>{
36
- const id = Symbol();
37
- slugStackRef.current = [
38
- ...slugStackRef.current,
39
- {
40
- id,
41
- slug
42
- }
43
- ];
24
+ const registrationsRef = useRef([]);
25
+ const register = useCallback((registration)=>{
26
+ registrationsRef.current.push(registration);
44
27
  return ()=>{
45
- slugStackRef.current = slugStackRef.current.filter((e)=>e.id !== id);
46
- };
47
- }, []);
48
- // Detect upload related cloud-limit that bypass form state
49
- useEffect(()=>{
50
- if (isUploadFetchPatched) {
51
- return;
52
- }
53
- isUploadFetchPatched = true;
54
- const originalFetch = window.fetch.bind(window);
55
- const originalToastError = toast.error;
56
- const toastObj = toast;
57
- const pendingSuppressions = [];
58
- const removeSuppression = (suppression)=>{
59
- window.clearTimeout(suppression.timeout);
60
- const index = pendingSuppressions.indexOf(suppression);
28
+ const index = registrationsRef.current.indexOf(registration);
61
29
  if (index !== -1) {
62
- pendingSuppressions.splice(index, 1);
63
- }
64
- };
65
- toastObj.error = (...args)=>{
66
- // prevent the thrown error from sending a duplicate toast
67
- const msg = typeof args[0] === 'string' ? args[0] : args[0]?.props?.errorMessage ?? '';
68
- const suppression = pendingSuppressions.find(({ message })=>msg.includes(message));
69
- if (suppression) {
70
- removeSuppression(suppression);
71
- return '';
72
- }
73
- return originalToastError(...args);
74
- };
75
- window.fetch = async function(...args) {
76
- const response = await originalFetch(...args);
77
- if (response.status === 409) {
78
- const url = args[0] instanceof Request ? args[0].url : String(args[0]);
79
- const uploadInstructionsPath = `${apiRouteRef.current.replace(/\/$/, '')}/upload-instructions`;
80
- if (new URL(url, window.location.origin).pathname === uploadInstructionsPath) {
81
- try {
82
- const json = await response.clone().json();
83
- const code = getCloudLimitErrorCode(json);
84
- const messageKey = code !== undefined ? cloudLimitToastKeyByCode[code] : undefined;
85
- if (messageKey) {
86
- toast.info(tRef.current(messageKey));
87
- const rawMessage = json?.errors?.[0]?.message;
88
- if (rawMessage) {
89
- const suppression = {
90
- message: rawMessage,
91
- timeout: 0
92
- };
93
- suppression.timeout = window.setTimeout(()=>removeSuppression(suppression), 0);
94
- pendingSuppressions.push(suppression);
95
- }
96
- }
97
- } catch {
98
- // ignore JSON parse failures
99
- }
100
- }
30
+ registrationsRef.current.splice(index, 1);
101
31
  }
102
- return response;
103
- };
104
- return ()=>{
105
- pendingSuppressions.forEach(({ timeout })=>window.clearTimeout(timeout));
106
- window.fetch = originalFetch;
107
- toastObj.error = originalToastError;
108
- isUploadFetchPatched = false;
109
32
  };
110
33
  }, []);
34
+ useEffect(()=>cloudLimitInterceptor(apiRoute, (key)=>t(key)), [
35
+ apiRoute,
36
+ t
37
+ ]);
111
38
  const handleNonFieldError = useCallback((err, retry)=>{
112
39
  const code = getCloudLimitErrorCode({
113
40
  errors: [
@@ -117,51 +44,65 @@ function slugFromPathSuffix(suffix) {
117
44
  if (!code) {
118
45
  return parentErrorHandler?.(err, retry) ?? false;
119
46
  }
120
- if (code === cloudLimitErrorCodes.documentCount) {
121
- // Prefer the tracker-provided slug (correct even in drawers) over URL parsing.
122
- const stack = slugStackRef.current;
123
- const slug = stack.length > 0 ? stack[stack.length - 1]?.slug : slugFromPathSuffix(window.location.pathname.slice(adminRouteRef.current.length));
124
- const collectionConfig = slug ? getEntityConfigRef.current({
125
- collectionSlug: slug
126
- }) : undefined;
127
- const isAutosave = collectionConfig ? hasAutosaveEnabled(collectionConfig) : false;
128
- if (isAutosave) {
129
- // Stable ID prevents a new toast stacking on every autosave attempt.
130
- toast.info(t('figma:cloudLimitDocumentCountError'), {
131
- id: AUTOSAVE_TOAST_ID
132
- });
133
- return true;
134
- }
135
- const label = collectionConfig?.labels?.plural != null ? getTranslation(collectionConfig.labels.plural, i18n) : slug;
136
- setCollectionSlug(slug);
137
- setCollectionLabel(label);
138
- retryRef.current = retry;
139
- openModal(cloudLimitDocumentCountModalSlug);
47
+ dismissSubmittingToast(t('general:submitting'));
48
+ if (code !== cloudLimitErrorCodes.documentCount) {
49
+ showCloudLimitToast(code, t);
140
50
  return true;
141
51
  }
142
- const messageKey = cloudLimitToastKeyByCode[code];
143
- if (messageKey) {
144
- toast.info(t(messageKey));
52
+ const registrations = registrationsRef.current;
53
+ let activeRegistration;
54
+ for(let index = registrations.length - 1; index >= 0; index--){
55
+ if (registrations[index]?.isSubmitting) {
56
+ activeRegistration = registrations[index];
57
+ break;
58
+ }
59
+ }
60
+ activeRegistration ??= registrations[registrations.length - 1];
61
+ const adminPath = formatAdminURL({
62
+ adminRoute,
63
+ includeBasePath: true
64
+ });
65
+ const slug = activeRegistration?.slug ?? /^\/collections\/([^/?#]+)/.exec(window.location.pathname.slice(adminPath.length))?.[1];
66
+ const collectionConfig = slug ? getEntityConfig({
67
+ collectionSlug: slug
68
+ }) : undefined;
69
+ if (collectionConfig && hasAutosaveEnabled(collectionConfig)) {
70
+ showCloudLimitToast(code, t);
145
71
  return true;
146
72
  }
147
- return false;
73
+ setModal({
74
+ collectionLabel: collectionConfig?.labels?.plural != null ? getTranslation(collectionConfig.labels.plural, i18n) : slug,
75
+ collectionListURL: slug ? formatAdminURL({
76
+ adminRoute,
77
+ includeBasePath: true,
78
+ path: `/collections/${slug}`
79
+ }) : undefined,
80
+ drawerDepth: activeRegistration?.drawerDepth ?? 0
81
+ });
82
+ retryRef.current = retry;
83
+ openModal(cloudLimitDocumentCountModalSlug);
84
+ return true;
148
85
  }, [
86
+ adminRoute,
87
+ getEntityConfig,
149
88
  i18n,
150
89
  openModal,
151
90
  parentErrorHandler,
152
91
  t
153
92
  ]);
154
93
  return /*#__PURE__*/ _jsx(FigmaCloudLimitSlugRegistryContext, {
155
- value: registerCollectionSlug,
94
+ value: register,
156
95
  children: /*#__PURE__*/ _jsxs(FormErrorHandlerContext, {
157
96
  value: handleNonFieldError,
158
97
  children: [
159
98
  children,
160
- /*#__PURE__*/ _jsx(FigmaCloudLimitDocumentCountModal, {
161
- adminRoute: adminRouteRef.current,
162
- collectionLabel: collectionLabel,
163
- collectionSlug: collectionSlug,
164
- onRetry: ()=>retryRef.current?.()
99
+ /*#__PURE__*/ _jsx(DrawerDepthContext, {
100
+ value: modal.drawerDepth,
101
+ children: /*#__PURE__*/ _jsx(FigmaCloudLimitDocumentCountModal, {
102
+ collectionLabel: modal.collectionLabel,
103
+ collectionListURL: modal.collectionListURL,
104
+ onRetry: ()=>retryRef.current?.()
105
+ })
165
106
  })
166
107
  ]
167
108
  })
@@ -0,0 +1,14 @@
1
+ .limit-hit-on-create {
2
+ display: flex;
3
+ flex-direction: column;
4
+ align-items: center;
5
+ justify-content: center;
6
+ gap: var(--spacer-3);
7
+ min-block-size: calc(100dvh - var(--app-header-height, 0px));
8
+ }
9
+
10
+ .limit-hit-on-create__message {
11
+ margin: 0;
12
+ color: var(--color-text);
13
+ font-size: var(--text-body-medium-font-size);
14
+ }
@@ -0,0 +1,4 @@
1
+ import type { AdminViewServerProps } from 'payload';
2
+ import React from 'react';
3
+ import './index.css';
4
+ export declare const LimitReachedView: React.FC<AdminViewServerProps>;
@@ -0,0 +1,52 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { getTranslation } from '@payloadcms/translations';
3
+ import { Button, SetStepNav } from '@payloadcms/ui';
4
+ import { formatAdminURL } from 'payload/shared';
5
+ import React from 'react';
6
+ import './index.css';
7
+ const baseClass = 'limit-hit-on-create';
8
+ export const LimitReachedView = ({ collectionConfig, initPageResult })=>{
9
+ const { req } = initPageResult;
10
+ const adminRoute = req.payload.config.routes.admin;
11
+ const tFigma = req.t;
12
+ if (!collectionConfig) {
13
+ return null;
14
+ }
15
+ const collectionLabel = getTranslation(collectionConfig.labels.plural, req.i18n);
16
+ const collectionListURL = formatAdminURL({
17
+ adminRoute,
18
+ includeBasePath: true,
19
+ path: `/collections/${collectionConfig.slug}`
20
+ });
21
+ return /*#__PURE__*/ _jsxs("main", {
22
+ className: baseClass,
23
+ children: [
24
+ /*#__PURE__*/ _jsx(SetStepNav, {
25
+ nav: [
26
+ {
27
+ label: collectionLabel,
28
+ url: collectionListURL
29
+ },
30
+ {
31
+ label: req.t('general:createNew')
32
+ }
33
+ ]
34
+ }),
35
+ /*#__PURE__*/ _jsx("p", {
36
+ className: `${baseClass}__message`,
37
+ children: tFigma('figma:cloudLimitDocumentCountError')
38
+ }),
39
+ /*#__PURE__*/ _jsx(Button, {
40
+ buttonStyle: "primary",
41
+ el: "link",
42
+ margin: false,
43
+ size: "medium",
44
+ url: formatAdminURL({
45
+ adminRoute,
46
+ includeBasePath: true
47
+ }),
48
+ children: req.t('general:backToDashboard')
49
+ })
50
+ ]
51
+ });
52
+ };
@@ -1,2 +1,7 @@
1
- export type RegisterCollectionSlug = (slug: string | undefined) => () => void;
2
- export declare const FigmaCloudLimitSlugRegistryContext: import("react").Context<RegisterCollectionSlug>;
1
+ export type CollectionFormRegistration = {
2
+ drawerDepth: number;
3
+ isSubmitting: boolean;
4
+ slug: string | undefined;
5
+ };
6
+ export type RegisterCollectionForm = (registration: CollectionFormRegistration) => () => void;
7
+ export declare const FigmaCloudLimitSlugRegistryContext: import("react").Context<RegisterCollectionForm>;
@@ -1,9 +1 @@
1
- /**
2
- * Invisible component injected into every collection's
3
- * `admin.components.edit.beforeDocumentControls`. Renders inside
4
- * DocumentInfoProvider so it can read the correct collectionSlug even inside
5
- * relationship drawers. Pushes to / pops from the slug stack owned by
6
- * FigmaCloudLimitProvider so the error handler always knows which collection
7
- * the active form belongs to.
8
- */
9
1
  export declare function FigmaCloudLimitSlugTracker(): null;
@@ -1,22 +1,26 @@
1
1
  'use client';
2
- import { useDocumentInfo } from '@payloadcms/ui';
2
+ import { useDocumentInfo, useFormBackgroundProcessing, useFormProcessing } from '@payloadcms/ui';
3
+ import { useDrawerDepth } from '@payloadcms/ui/elements/Drawer';
3
4
  import { use, useEffect } from 'react';
4
5
  import { FigmaCloudLimitSlugRegistryContext } from '../SlugRegistryContext/index.js';
5
- /**
6
- * Invisible component injected into every collection's
7
- * `admin.components.edit.beforeDocumentControls`. Renders inside
8
- * DocumentInfoProvider so it can read the correct collectionSlug even inside
9
- * relationship drawers. Pushes to / pops from the slug stack owned by
10
- * FigmaCloudLimitProvider so the error handler always knows which collection
11
- * the active form belongs to.
12
- */ export function FigmaCloudLimitSlugTracker() {
6
+ export function FigmaCloudLimitSlugTracker() {
13
7
  const register = use(FigmaCloudLimitSlugRegistryContext);
14
8
  const { collectionSlug } = useDocumentInfo();
9
+ const drawerDepth = useDrawerDepth();
10
+ const isBackgroundProcessing = useFormBackgroundProcessing();
11
+ const isProcessing = useFormProcessing();
15
12
  useEffect(()=>{
16
- const unregister = register(collectionSlug);
13
+ const unregister = register({
14
+ slug: collectionSlug,
15
+ drawerDepth,
16
+ isSubmitting: isBackgroundProcessing || isProcessing
17
+ });
17
18
  return unregister;
18
19
  }, [
19
20
  collectionSlug,
21
+ drawerDepth,
22
+ isBackgroundProcessing,
23
+ isProcessing,
20
24
  register
21
25
  ]);
22
26
  return null;
@@ -0,0 +1,2 @@
1
+ export declare const LIMIT_REACHED_VIEW_KEY = "figmaLimitReached";
2
+ export declare const LIMIT_REACHED_VIEW_PATH = "/limit-reached";
@@ -0,0 +1,4 @@
1
+ export const LIMIT_REACHED_VIEW_KEY = 'figmaLimitReached';
2
+ // Reserved collection-level admin route. Applications using custom string IDs must not use
3
+ // "limit-reached"; the custom view intentionally takes precedence over a document edit route.
4
+ export const LIMIT_REACHED_VIEW_PATH = '/limit-reached';
@@ -0,0 +1,9 @@
1
+ import type { Payload, PayloadRequest } from 'payload';
2
+ type Args = {
3
+ collectionSlug: string;
4
+ errorCode: unknown;
5
+ payload: Payload;
6
+ req?: Partial<Pick<PayloadRequest, 'pathname' | 'server'>>;
7
+ };
8
+ export declare function redirectInitialAutosaveCreate({ collectionSlug, errorCode, payload, req, }: Args): void;
9
+ export {};
@@ -0,0 +1,28 @@
1
+ import { formatAdminURL, hasAutosaveEnabled } from 'payload/shared';
2
+ import { LIMIT_REACHED_VIEW_PATH } from '../routes.js';
3
+ import { cloudLimitErrorCodes } from '../shared.js';
4
+ export function redirectInitialAutosaveCreate({ collectionSlug, errorCode, payload, req }) {
5
+ // Initial autosave creates an empty document, so documentDataSize is not expected in
6
+ // production. Keep it as a defensive/test-only fallback and intentionally route it
7
+ // through the same terminal create-failure view.
8
+ const isSupportedLimit = errorCode === cloudLimitErrorCodes.documentCount || errorCode === cloudLimitErrorCodes.documentDataSize;
9
+ const collectionConfig = payload.collections[collectionSlug]?.config;
10
+ const server = req?.server;
11
+ if (!isSupportedLimit || !collectionConfig || !server || !hasAutosaveEnabled(collectionConfig)) {
12
+ return;
13
+ }
14
+ const { admin: adminRoute } = payload.config.routes;
15
+ const createPath = formatAdminURL({
16
+ adminRoute,
17
+ includeBasePath: true,
18
+ path: `/collections/${collectionSlug}/create`
19
+ });
20
+ if (req.pathname?.replace(/\/$/, '') !== createPath.replace(/\/$/, '')) {
21
+ return;
22
+ }
23
+ server.redirect(formatAdminURL({
24
+ adminRoute,
25
+ includeBasePath: true,
26
+ path: `/collections/${collectionSlug}${LIMIT_REACHED_VIEW_PATH}`
27
+ }));
28
+ }
@@ -7,4 +7,4 @@ export type CloudLimitErrorCode = (typeof cloudLimitErrorCodes)[keyof typeof clo
7
7
  export declare function isCloudLimitErrorCode(code: unknown): code is CloudLimitErrorCode;
8
8
  export declare function getCloudLimitErrorCode(json: unknown): CloudLimitErrorCode | undefined;
9
9
  export declare const cloudLimitDocumentCountModalSlug = "figma-cloud-limit-document-count";
10
- export declare const cloudLimitToastKeyByCode: Partial<Record<CloudLimitErrorCode, string>>;
10
+ export declare const cloudLimitToastKeyByCode: Record<CloudLimitErrorCode, string>;
@@ -7,6 +7,20 @@ const knownCloudLimitCodes = Object.values(cloudLimitErrorCodes);
7
7
  export function isCloudLimitErrorCode(code) {
8
8
  return typeof code === 'string' && knownCloudLimitCodes.includes(code);
9
9
  }
10
+ const cloudLimitCodeByMessagePattern = [
11
+ {
12
+ code: cloudLimitErrorCodes.documentCount,
13
+ pattern: /document count/i
14
+ },
15
+ {
16
+ code: cloudLimitErrorCodes.documentDataSize,
17
+ pattern: /document data size/i
18
+ },
19
+ {
20
+ code: cloudLimitErrorCodes.assetStorage,
21
+ pattern: /asset storage limit/i
22
+ }
23
+ ];
10
24
  export function getCloudLimitErrorCode(json) {
11
25
  const errors = json?.errors;
12
26
  if (!Array.isArray(errors)) {
@@ -17,11 +31,22 @@ export function getCloudLimitErrorCode(json) {
17
31
  if (isCloudLimitErrorCode(code)) {
18
32
  return code;
19
33
  }
34
+ const message = err?.message;
35
+ if (message?.startsWith('Content API ')) {
36
+ // Payload's bulk-upload response currently drops APIError.data.code. Match only the
37
+ // identifying phrase from each Content API message so variable values and minor wording
38
+ // changes do not break the fallback. Prefer the structured code above whenever present.
39
+ const match = cloudLimitCodeByMessagePattern.find(({ pattern })=>pattern.test(message));
40
+ if (match) {
41
+ return match.code;
42
+ }
43
+ }
20
44
  }
21
45
  return undefined;
22
46
  }
23
47
  export const cloudLimitDocumentCountModalSlug = 'figma-cloud-limit-document-count';
24
48
  export const cloudLimitToastKeyByCode = {
25
49
  [cloudLimitErrorCodes.assetStorage]: 'figma:cloudLimitAssetStorageError',
50
+ [cloudLimitErrorCodes.documentCount]: 'figma:cloudLimitDocumentCountError',
26
51
  [cloudLimitErrorCodes.documentDataSize]: 'figma:cloudLimitDocumentDataSizeError'
27
52
  };
@@ -0,0 +1,2 @@
1
+ import type { Translate } from './toasts.js';
2
+ export declare function cloudLimitInterceptor(apiRoute: string, translate: Translate): () => void;