@payloadcms/figma 0.1.0-alpha.3 → 0.1.0-alpha.5

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 (116) hide show
  1. package/dist/api/check-project-subdomain-availability.d.ts +12 -0
  2. package/dist/api/check-project-subdomain-availability.js +32 -0
  3. package/dist/api/control-plane-error.d.ts +6 -0
  4. package/dist/api/control-plane-error.js +8 -0
  5. package/dist/api/control-plane-fetch.d.ts +7 -0
  6. package/dist/api/control-plane-fetch.js +37 -0
  7. package/dist/api/control-plane.d.ts +0 -8
  8. package/dist/api/control-plane.js +1 -47
  9. package/dist/api/create-project.d.ts +18 -0
  10. package/dist/api/create-project.js +37 -0
  11. package/dist/api/list-project-workspaces.d.ts +8 -0
  12. package/dist/api/list-project-workspaces.js +28 -0
  13. package/dist/api/project-resource-type.d.ts +1 -0
  14. package/dist/api/project-resource-type.js +1 -0
  15. package/dist/auth/headless-oauth-flow.d.ts +23 -0
  16. package/dist/auth/headless-oauth-flow.js +69 -0
  17. package/dist/auth/oauth-flow.d.ts +2 -0
  18. package/dist/auth/oauth-flow.js +13 -8
  19. package/dist/auth/token-store.d.ts +4 -1
  20. package/dist/auth/token-store.js +14 -0
  21. package/dist/auth/types.d.ts +12 -0
  22. package/dist/cli.d.ts +10 -1
  23. package/dist/cli.js +39 -4
  24. package/dist/commands/bootstrap.js +2 -1
  25. package/dist/commands/deploy.d.ts +10 -0
  26. package/dist/commands/deploy.js +39 -11
  27. package/dist/commands/detect-project-resource-type.d.ts +3 -0
  28. package/dist/commands/detect-project-resource-type.js +28 -0
  29. package/dist/commands/init.d.ts +2 -2
  30. package/dist/commands/init.js +163 -78
  31. package/dist/commands/login.d.ts +11 -1
  32. package/dist/commands/login.js +66 -4
  33. package/dist/commands/prompt-to-create-project/format-project-subdomain.d.ts +1 -0
  34. package/dist/commands/prompt-to-create-project/format-project-subdomain.js +3 -0
  35. package/dist/commands/prompt-to-create-project/format-unavailable-subdomain-reason.d.ts +1 -0
  36. package/dist/commands/prompt-to-create-project/format-unavailable-subdomain-reason.js +7 -0
  37. package/dist/commands/prompt-to-create-project/get-default-project-name.d.ts +1 -0
  38. package/dist/commands/prompt-to-create-project/get-default-project-name.js +13 -0
  39. package/dist/commands/prompt-to-create-project/prompt-for-available-subdomain.d.ts +7 -0
  40. package/dist/commands/prompt-to-create-project/prompt-for-available-subdomain.js +32 -0
  41. package/dist/commands/prompt-to-create-project/resolve-project-region.d.ts +8 -0
  42. package/dist/commands/prompt-to-create-project/resolve-project-region.js +27 -0
  43. package/dist/commands/prompt-to-create-project/resolve-project-subdomain.d.ts +9 -0
  44. package/dist/commands/prompt-to-create-project/resolve-project-subdomain.js +54 -0
  45. package/dist/commands/prompt-to-create-project/resolve-text-input.d.ts +9 -0
  46. package/dist/commands/prompt-to-create-project/resolve-text-input.js +23 -0
  47. package/dist/commands/prompt-to-create-project/resolve-workspace-plan-key.d.ts +12 -0
  48. package/dist/commands/prompt-to-create-project/resolve-workspace-plan-key.js +36 -0
  49. package/dist/commands/prompt-to-create-project/validate-subdomain.d.ts +1 -0
  50. package/dist/commands/prompt-to-create-project/validate-subdomain.js +9 -0
  51. package/dist/commands/prompt-to-create-project.d.ts +24 -0
  52. package/dist/commands/prompt-to-create-project.js +69 -0
  53. package/dist/commands/setup-agent.d.ts +12 -0
  54. package/dist/commands/setup-agent.js +123 -0
  55. package/dist/commands/write-project-configuration.d.ts +12 -0
  56. package/dist/commands/write-project-configuration.js +16 -0
  57. package/dist/db-content-api/index.js +52 -33
  58. package/dist/db-content-api/utilities/data/injectGlobalType.d.ts +6 -0
  59. package/dist/db-content-api/utilities/data/injectGlobalType.js +11 -0
  60. package/dist/db-content-api/utilities/isRecord.d.ts +9 -0
  61. package/dist/db-content-api/utilities/isRecord.js +19 -0
  62. package/dist/db-content-api/utilities/joins.js +5 -1
  63. package/dist/db-content-api/utilities/meta/resolvesToRelationshipValue.d.ts +10 -0
  64. package/dist/db-content-api/utilities/meta/resolvesToRelationshipValue.js +95 -0
  65. package/dist/db-content-api/utilities/meta/stringifyIdsInWhere.d.ts +9 -0
  66. package/dist/db-content-api/utilities/meta/stringifyIdsInWhere.js +112 -0
  67. package/dist/db-content-api/utilities/where.d.ts +2 -6
  68. package/dist/db-content-api/utilities/where.js +20 -9
  69. package/dist/exports/views.d.ts +1 -0
  70. package/dist/exports/views.js +1 -0
  71. package/dist/imageSizeOptions.d.ts +23 -0
  72. package/dist/imageSizeOptions.js +26 -0
  73. package/dist/index.d.ts +1 -0
  74. package/dist/lib/write-agent-config.d.ts +12 -0
  75. package/dist/lib/write-agent-config.js +52 -0
  76. package/dist/oauth/defaults.d.ts +1 -0
  77. package/dist/oauth/defaults.js +1 -0
  78. package/dist/oauth/endpoints/getLoginEndpoint.js +7 -2
  79. package/dist/oauth/index.js +24 -9
  80. package/dist/oauth/utilities/getAdminCollectionSlug.d.ts +1 -1
  81. package/dist/oauth/utilities/getAuthorizeURL.d.ts +3 -0
  82. package/dist/oauth/utilities/getAuthorizeURL.js +8 -2
  83. package/dist/plugin/build-config.js +22 -9
  84. package/dist/plugin/cloud-limits/CloudLimitModal/index.d.ts +2 -3
  85. package/dist/plugin/cloud-limits/CloudLimitModal/index.js +4 -16
  86. package/dist/plugin/cloud-limits/CloudLimitModal/index.scss +5 -0
  87. package/dist/plugin/cloud-limits/CloudLimitProvider/index.d.ts +0 -3
  88. package/dist/plugin/cloud-limits/CloudLimitProvider/index.js +65 -124
  89. package/dist/plugin/cloud-limits/LimitReachedView/index.css +14 -0
  90. package/dist/plugin/cloud-limits/LimitReachedView/index.d.ts +4 -0
  91. package/dist/plugin/cloud-limits/LimitReachedView/index.js +52 -0
  92. package/dist/plugin/cloud-limits/SlugRegistryContext/index.d.ts +7 -2
  93. package/dist/plugin/cloud-limits/SlugTracker/index.d.ts +0 -8
  94. package/dist/plugin/cloud-limits/SlugTracker/index.js +14 -10
  95. package/dist/plugin/cloud-limits/routes.d.ts +2 -0
  96. package/dist/plugin/cloud-limits/routes.js +4 -0
  97. package/dist/plugin/cloud-limits/server/redirectInitialAutosaveCreate.d.ts +9 -0
  98. package/dist/plugin/cloud-limits/server/redirectInitialAutosaveCreate.js +28 -0
  99. package/dist/plugin/cloud-limits/shared.d.ts +1 -1
  100. package/dist/plugin/cloud-limits/shared.js +25 -0
  101. package/dist/plugin/cloud-limits/utilities/cloudLimitInterceptor.d.ts +2 -0
  102. package/dist/plugin/cloud-limits/utilities/cloudLimitInterceptor.js +134 -0
  103. package/dist/plugin/cloud-limits/utilities/toasts.d.ts +4 -0
  104. package/dist/plugin/cloud-limits/utilities/toasts.js +19 -0
  105. package/dist/skills/deploy-to-figma/SKILL.md +234 -0
  106. package/dist/skills/deploy-to-figma/references/cli-reference.md +146 -0
  107. package/dist/types.d.ts +9 -0
  108. package/dist/utils/lambda-config.js +4 -14
  109. package/dist/utils/messages.js +22 -3
  110. package/dist/utils/payload-generate.d.ts +10 -0
  111. package/dist/utils/payload-generate.js +59 -0
  112. package/dist/utils/pnpm-builds.d.ts +14 -0
  113. package/dist/utils/pnpm-builds.js +125 -0
  114. package/package.json +9 -1
  115. package/dist/lib/download-skill.d.ts +0 -12
  116. package/dist/lib/download-skill.js +0 -77
@@ -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;
@@ -0,0 +1,134 @@
1
+ 'use client';
2
+ import { toast } from '@payloadcms/ui';
3
+ import { formatAdminURL } from 'payload/shared';
4
+ import { getCloudLimitErrorCode } from '../shared.js';
5
+ import { showCloudLimitToast } from './toasts.js';
6
+ const ERROR_SUPPRESSION_MS = 1_000;
7
+ const SUMMARY_SUPPRESSION_MS = 60_000;
8
+ function restoreBulkErrorCodes(response, json) {
9
+ if (!json || typeof json !== 'object' || !Array.isArray(json.docs) || !Array.isArray(json.errors)) {
10
+ return undefined;
11
+ }
12
+ let hasLimitError = false;
13
+ const errors = json.errors.map((error)=>{
14
+ const code = getCloudLimitErrorCode({
15
+ errors: [
16
+ error
17
+ ]
18
+ });
19
+ if (!code || !error || typeof error !== 'object') {
20
+ return error;
21
+ }
22
+ hasLimitError = true;
23
+ const data = error.data;
24
+ return {
25
+ ...error,
26
+ data: {
27
+ ...typeof data === 'object' && data ? data : {},
28
+ code
29
+ }
30
+ };
31
+ });
32
+ if (!hasLimitError) {
33
+ return undefined;
34
+ }
35
+ const { message: _aggregateMessage, ...result } = json;
36
+ const headers = new Headers(response.headers);
37
+ headers.delete('content-encoding');
38
+ headers.delete('content-length');
39
+ return new Response(JSON.stringify({
40
+ ...result,
41
+ errors
42
+ }), {
43
+ headers,
44
+ status: response.status,
45
+ statusText: response.statusText
46
+ });
47
+ }
48
+ export function cloudLimitInterceptor(apiRoute, translate) {
49
+ const originalFetch = window.fetch;
50
+ const originalToastError = toast.error;
51
+ const suppressions = new Map();
52
+ let suppressSummary = false;
53
+ let summaryTimeout;
54
+ const patchedToastError = (...args)=>{
55
+ const message = typeof args[0] === 'string' ? args[0] : args[0]?.props?.errorMessage ?? '';
56
+ const match = [
57
+ ...suppressions
58
+ ].find(([rawMessage])=>message.includes(rawMessage));
59
+ if (match) {
60
+ const [, suppression] = match;
61
+ // Keep the suppression for its full lifetime so overlapping requests with the same raw
62
+ // error cannot leak duplicate Payload error toasts. The stable info-toast ID deduplicates
63
+ // the replacement notification.
64
+ showCloudLimitToast(suppression.code, translate);
65
+ if (suppression.isBulkUpload) {
66
+ suppressSummary = true;
67
+ window.clearTimeout(summaryTimeout);
68
+ summaryTimeout = window.setTimeout(()=>suppressSummary = false, SUMMARY_SUPPRESSION_MS);
69
+ }
70
+ return '';
71
+ }
72
+ if (suppressSummary && /^Failed to save \d+ files$/.test(message)) {
73
+ suppressSummary = false;
74
+ window.clearTimeout(summaryTimeout);
75
+ return '';
76
+ }
77
+ return originalToastError(...args);
78
+ };
79
+ const patchedFetch = async (...args)=>{
80
+ const response = await originalFetch(...args);
81
+ if (response.status < 400) {
82
+ return response;
83
+ }
84
+ try {
85
+ const json = await response.clone().json();
86
+ const bulkResponse = response.status === 400 ? restoreBulkErrorCodes(response, json) : undefined;
87
+ if (bulkResponse) {
88
+ return bulkResponse;
89
+ }
90
+ const input = args[0];
91
+ const pathname = new URL(input instanceof Request ? input.url : String(input), window.location.origin).pathname;
92
+ const method = (input instanceof Request ? input.method : args[1]?.method ?? 'GET').toUpperCase();
93
+ const apiPath = formatAdminURL({
94
+ apiRoute,
95
+ path: ''
96
+ }).replace(/\/$/, '');
97
+ const endpoint = pathname.startsWith(`${apiPath}/`) ? pathname.slice(apiPath.length + 1) : '';
98
+ const isUploadInstructions = endpoint === 'upload-instructions';
99
+ const isCollectionCreate = method === 'POST' && endpoint !== '' && !endpoint.includes('/');
100
+ if (!isUploadInstructions && !isCollectionCreate) {
101
+ return response;
102
+ }
103
+ const code = getCloudLimitErrorCode(json);
104
+ const rawMessage = json?.errors?.[0]?.message;
105
+ if (code && rawMessage) {
106
+ const previous = suppressions.get(rawMessage);
107
+ window.clearTimeout(previous?.timeout);
108
+ const suppression = {
109
+ code,
110
+ isBulkUpload: isCollectionCreate,
111
+ timeout: 0
112
+ };
113
+ suppression.timeout = window.setTimeout(()=>suppressions.delete(rawMessage), ERROR_SUPPRESSION_MS);
114
+ suppressions.set(rawMessage, suppression);
115
+ }
116
+ } catch {
117
+ // Leave Payload's default handling intact for malformed error responses.
118
+ }
119
+ return response;
120
+ };
121
+ window.fetch = patchedFetch;
122
+ toast.error = patchedToastError;
123
+ return ()=>{
124
+ suppressions.forEach(({ timeout })=>window.clearTimeout(timeout));
125
+ window.clearTimeout(summaryTimeout);
126
+ if (window.fetch === patchedFetch) {
127
+ window.fetch = originalFetch;
128
+ }
129
+ if (toast.error === patchedToastError) {
130
+ ;
131
+ toast.error = originalToastError;
132
+ }
133
+ };
134
+ }
@@ -0,0 +1,4 @@
1
+ import type { CloudLimitErrorCode } from '../shared.js';
2
+ export type Translate = (key: string) => string;
3
+ export declare function dismissSubmittingToast(label: string): void;
4
+ export declare function showCloudLimitToast(code: CloudLimitErrorCode, translate: Translate): void;
@@ -0,0 +1,19 @@
1
+ 'use client';
2
+ import { toast } from '@payloadcms/ui';
3
+ import { cloudLimitToastKeyByCode } from '../shared.js';
4
+ const TOAST_ID_PREFIX = 'figma-cloud-limit';
5
+ export function dismissSubmittingToast(label) {
6
+ const activeToasts = toast.getToasts();
7
+ for(let index = activeToasts.length - 1; index >= 0; index--){
8
+ const candidate = activeToasts[index];
9
+ if (candidate && !('dismiss' in candidate) && candidate.promise !== undefined && candidate.title === label && candidate.type === 'loading') {
10
+ toast.dismiss(candidate.id);
11
+ return;
12
+ }
13
+ }
14
+ }
15
+ export function showCloudLimitToast(code, translate) {
16
+ toast.info(translate(cloudLimitToastKeyByCode[code]), {
17
+ id: `${TOAST_ID_PREFIX}-${code}`
18
+ });
19
+ }