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

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 (153) hide show
  1. package/dist/api/control-plane.d.ts +1 -1
  2. package/dist/api/control-plane.js +1 -1
  3. package/dist/cli.d.ts +30 -0
  4. package/dist/cli.js +33 -33
  5. package/dist/commands/deploy.d.ts +2 -0
  6. package/dist/commands/deploy.js +22 -12
  7. package/dist/db-content-api/index.d.ts +1 -1
  8. package/dist/db-content-api/index.js +70 -40
  9. package/dist/db-content-api/utilities/schema/getDocumentSchema.d.ts +3 -0
  10. package/dist/db-content-api/utilities/schema/getDocumentSchema.js +24 -0
  11. package/dist/exports/client.d.ts +2 -0
  12. package/dist/exports/client.js +2 -0
  13. package/dist/plugin/build-config.js +39 -4
  14. package/dist/plugin/cloud-limits/CloudLimitModal/index.d.ts +9 -0
  15. package/dist/plugin/cloud-limits/CloudLimitModal/index.js +78 -0
  16. package/dist/plugin/cloud-limits/CloudLimitModal/index.scss +51 -0
  17. package/dist/plugin/cloud-limits/CloudLimitProvider/index.d.ts +7 -0
  18. package/dist/plugin/cloud-limits/CloudLimitProvider/index.js +169 -0
  19. package/dist/plugin/cloud-limits/SlugRegistryContext/index.d.ts +2 -0
  20. package/dist/plugin/cloud-limits/SlugRegistryContext/index.js +3 -0
  21. package/dist/plugin/cloud-limits/SlugTracker/index.d.ts +9 -0
  22. package/dist/plugin/cloud-limits/SlugTracker/index.js +23 -0
  23. package/dist/plugin/cloud-limits/icons/ExternalLinkIcon.d.ts +2 -0
  24. package/dist/plugin/cloud-limits/icons/ExternalLinkIcon.js +30 -0
  25. package/dist/plugin/cloud-limits/shared.d.ts +10 -0
  26. package/dist/plugin/cloud-limits/shared.js +27 -0
  27. package/dist/plugin/sandbox-upload-fetch.d.ts +32 -0
  28. package/dist/plugin/sandbox-upload-fetch.js +53 -0
  29. package/dist/translations/ar.d.ts +12 -0
  30. package/dist/translations/ar.js +12 -0
  31. package/dist/translations/az.d.ts +12 -0
  32. package/dist/translations/az.js +12 -0
  33. package/dist/translations/bg.d.ts +12 -0
  34. package/dist/translations/bg.js +12 -0
  35. package/dist/translations/bnBd.d.ts +12 -0
  36. package/dist/translations/bnBd.js +12 -0
  37. package/dist/translations/bnIn.d.ts +12 -0
  38. package/dist/translations/bnIn.js +12 -0
  39. package/dist/translations/ca.d.ts +12 -0
  40. package/dist/translations/ca.js +12 -0
  41. package/dist/translations/cs.d.ts +12 -0
  42. package/dist/translations/cs.js +12 -0
  43. package/dist/translations/da.d.ts +12 -0
  44. package/dist/translations/da.js +12 -0
  45. package/dist/translations/de.d.ts +12 -0
  46. package/dist/translations/de.js +12 -0
  47. package/dist/translations/en.d.ts +12 -0
  48. package/dist/translations/en.js +12 -0
  49. package/dist/translations/es.d.ts +12 -0
  50. package/dist/translations/es.js +12 -0
  51. package/dist/translations/et.d.ts +12 -0
  52. package/dist/translations/et.js +12 -0
  53. package/dist/translations/fa.d.ts +12 -0
  54. package/dist/translations/fa.js +12 -0
  55. package/dist/translations/fr.d.ts +12 -0
  56. package/dist/translations/fr.js +12 -0
  57. package/dist/translations/he.d.ts +12 -0
  58. package/dist/translations/he.js +12 -0
  59. package/dist/translations/hr.d.ts +12 -0
  60. package/dist/translations/hr.js +12 -0
  61. package/dist/translations/hu.d.ts +12 -0
  62. package/dist/translations/hu.js +12 -0
  63. package/dist/translations/hy.d.ts +12 -0
  64. package/dist/translations/hy.js +12 -0
  65. package/dist/translations/id.d.ts +12 -0
  66. package/dist/translations/id.js +12 -0
  67. package/dist/translations/index.d.ts +533 -0
  68. package/dist/translations/index.js +90 -0
  69. package/dist/translations/is.d.ts +12 -0
  70. package/dist/translations/is.js +12 -0
  71. package/dist/translations/it.d.ts +12 -0
  72. package/dist/translations/it.js +12 -0
  73. package/dist/translations/ja.d.ts +12 -0
  74. package/dist/translations/ja.js +12 -0
  75. package/dist/translations/ko.d.ts +12 -0
  76. package/dist/translations/ko.js +12 -0
  77. package/dist/translations/lt.d.ts +12 -0
  78. package/dist/translations/lt.js +12 -0
  79. package/dist/translations/lv.d.ts +12 -0
  80. package/dist/translations/lv.js +12 -0
  81. package/dist/translations/my.d.ts +12 -0
  82. package/dist/translations/my.js +12 -0
  83. package/dist/translations/nb.d.ts +12 -0
  84. package/dist/translations/nb.js +12 -0
  85. package/dist/translations/nl.d.ts +12 -0
  86. package/dist/translations/nl.js +12 -0
  87. package/dist/translations/pl.d.ts +12 -0
  88. package/dist/translations/pl.js +12 -0
  89. package/dist/translations/pt.d.ts +12 -0
  90. package/dist/translations/pt.js +12 -0
  91. package/dist/translations/ro.d.ts +12 -0
  92. package/dist/translations/ro.js +12 -0
  93. package/dist/translations/rs.d.ts +12 -0
  94. package/dist/translations/rs.js +12 -0
  95. package/dist/translations/rsLatin.d.ts +12 -0
  96. package/dist/translations/rsLatin.js +12 -0
  97. package/dist/translations/ru.d.ts +12 -0
  98. package/dist/translations/ru.js +12 -0
  99. package/dist/translations/sk.d.ts +12 -0
  100. package/dist/translations/sk.js +12 -0
  101. package/dist/translations/sl.d.ts +12 -0
  102. package/dist/translations/sl.js +12 -0
  103. package/dist/translations/sv.d.ts +12 -0
  104. package/dist/translations/sv.js +12 -0
  105. package/dist/translations/ta.d.ts +12 -0
  106. package/dist/translations/ta.js +12 -0
  107. package/dist/translations/th.d.ts +12 -0
  108. package/dist/translations/th.js +12 -0
  109. package/dist/translations/tr.d.ts +12 -0
  110. package/dist/translations/tr.js +12 -0
  111. package/dist/translations/uk.d.ts +12 -0
  112. package/dist/translations/uk.js +12 -0
  113. package/dist/translations/vi.d.ts +12 -0
  114. package/dist/translations/vi.js +12 -0
  115. package/dist/translations/zh.d.ts +12 -0
  116. package/dist/translations/zh.js +12 -0
  117. package/dist/translations/zhTw.d.ts +12 -0
  118. package/dist/translations/zhTw.js +12 -0
  119. package/dist/types.d.ts +2 -0
  120. package/dist/utils/adapters/nextjs.d.ts +3 -3
  121. package/dist/utils/adapters/nextjs.js +9 -7
  122. package/dist/utils/adapters/nitro.d.ts +3 -3
  123. package/dist/utils/adapters/nitro.js +73 -77
  124. package/dist/utils/adapters/vite.d.ts +1 -1
  125. package/dist/utils/adapters/vite.js +8 -7
  126. package/dist/utils/assert-never.d.ts +1 -0
  127. package/dist/utils/assert-never.js +3 -0
  128. package/dist/utils/asset-collection.d.ts +2 -2
  129. package/dist/utils/asset-collection.js +10 -15
  130. package/dist/utils/build-detection.d.ts +6 -1
  131. package/dist/utils/build-detection.js +68 -89
  132. package/dist/utils/build-lambda-zip.d.ts +5 -6
  133. package/dist/utils/build-lambda-zip.js +31 -48
  134. package/dist/utils/deploy-adapter.d.ts +4 -4
  135. package/dist/utils/deploy-adapter.js +12 -8
  136. package/dist/utils/deploy-output/inspectNextjsOutput/getNextRelativeAppDir.d.ts +1 -0
  137. package/dist/utils/deploy-output/inspectNextjsOutput/getNextRelativeAppDir.js +15 -0
  138. package/dist/utils/deploy-output/inspectNextjsOutput.d.ts +12 -0
  139. package/dist/utils/deploy-output/inspectNextjsOutput.js +28 -0
  140. package/dist/utils/deploy-output/inspectNitroOutput.d.ts +9 -0
  141. package/dist/utils/deploy-output/inspectNitroOutput.js +13 -0
  142. package/dist/utils/deploy-output/isFile.d.ts +1 -0
  143. package/dist/utils/deploy-output/isFile.js +4 -0
  144. package/dist/utils/deploy-output/readJson.d.ts +1 -0
  145. package/dist/utils/deploy-output/readJson.js +9 -0
  146. package/dist/utils/deploy-output/resolveNitroOutputLayout.d.ts +8 -0
  147. package/dist/utils/deploy-output/resolveNitroOutputLayout.js +16 -0
  148. package/dist/utils/deploy-output/resolveOutputPath.d.ts +2 -0
  149. package/dist/utils/deploy-output/resolveOutputPath.js +9 -0
  150. package/dist/utils/deploy-output/resolveWithin.d.ts +1 -0
  151. package/dist/utils/deploy-output/resolveWithin.js +12 -0
  152. package/dist/utils/messages.js +2 -0
  153. package/package.json +6 -5
@@ -1,6 +1,7 @@
1
1
  import { cloudStoragePlugin } from '@payloadcms/plugin-cloud-storage';
2
2
  import { lexicalEditor } from '@payloadcms/richtext-lexical';
3
3
  import { buildConfig as payloadBuildConfig } from 'payload';
4
+ import { deepMergeSimple } from 'payload/shared';
4
5
  import { getBootstrapInfo } from '../api/control-plane.js';
5
6
  import { getValidCredential } from '../auth/oauth-flow.js';
6
7
  import { getTokenStore } from '../auth/token-store.js';
@@ -12,12 +13,14 @@ import { schema } from '../endpoints/schema.js';
12
13
  import { oAuth2Plugin } from '../oauth/index.js';
13
14
  import { createStorageClient } from '../storage-content-api/client.js';
14
15
  import { contentApiStorageAdapter } from '../storage-content-api/index.js';
16
+ import { translations as figmaTranslations } from '../translations/index.js';
15
17
  import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
16
18
  import { getEnvVarSync } from '../utils/env-management.js';
17
19
  import * as log from '../utils/log.js';
18
20
  import { logMissingCliAuth } from './auth-preflight.js';
19
21
  import { logMissingContentSystemId } from './bootstrap-preflight.js';
20
22
  import { getDevCookieNames } from './dev-cookie-names.js';
23
+ import { createSandboxUploadFetchPlugin } from './sandbox-upload-fetch.js';
21
24
  /**
22
25
  * Worker-trusted marker injected by Figma's sites-worker after it validates the
23
26
  * X-Figma-Job-Queues-Auth-Bypass secret on a Payload jobs endpoint. The default
@@ -322,7 +325,14 @@ export async function buildFigmaConfig(config) {
322
325
  avatar: {
323
326
  Component: '@payloadcms/figma/client#FigmaAvatar'
324
327
  },
325
- ...config.admin
328
+ ...config.admin,
329
+ components: {
330
+ ...config.admin?.components,
331
+ providers: [
332
+ ...config.admin?.components?.providers ?? [],
333
+ '@payloadcms/figma/client#FigmaCloudLimitProvider'
334
+ ]
335
+ }
326
336
  },
327
337
  custom: {
328
338
  figma: {
@@ -338,6 +348,10 @@ export async function buildFigmaConfig(config) {
338
348
  health,
339
349
  schema
340
350
  ],
351
+ i18n: {
352
+ ...config.i18n,
353
+ translations: deepMergeSimple(figmaTranslations, config.i18n?.translations ?? {})
354
+ },
341
355
  // Gate /api/payload-jobs/run and /api/payload-jobs/handle-schedules. Honor the
342
356
  // sites-worker trust marker so Figma infra job runs never break, defer to the
343
357
  // tenant's jobs.access.run if they set one, otherwise fall back to Payload's
@@ -392,21 +406,42 @@ export async function buildFigmaConfig(config) {
392
406
  debug: !!process.env.DEBUG,
393
407
  disabled: false
394
408
  }),
395
- scheduleExtractPlugin()
409
+ scheduleExtractPlugin(),
410
+ // Not folded into createStoragePlugin, which is skipped when
411
+ // figma.storage is false and would break remote uploads for those apps.
412
+ createSandboxUploadFetchPlugin({
413
+ userCollections: config.collections
414
+ })
396
415
  ]
397
416
  };
398
417
  // Inject figma fields into users collection
399
418
  const collections = configWithFigmaDefaults.collections?.map((col)=>{
419
+ const withSlugTracker = {
420
+ ...col,
421
+ admin: {
422
+ ...col.admin,
423
+ components: {
424
+ ...col.admin?.components,
425
+ edit: {
426
+ ...col.admin?.components?.edit,
427
+ beforeDocumentControls: [
428
+ '@payloadcms/figma/client#FigmaCloudLimitSlugTracker',
429
+ ...col.admin?.components?.edit?.beforeDocumentControls ?? []
430
+ ]
431
+ }
432
+ }
433
+ }
434
+ };
400
435
  if (col.slug === 'users') {
401
436
  return {
402
- ...col,
437
+ ...withSlugTracker,
403
438
  fields: [
404
439
  ...col.fields || [],
405
440
  ...figmaUserFields
406
441
  ]
407
442
  };
408
443
  }
409
- return col;
444
+ return withSlugTracker;
410
445
  });
411
446
  if (collections) {
412
447
  configWithFigmaDefaults.collections = collections;
@@ -0,0 +1,9 @@
1
+ import './index.scss';
2
+ type Props = {
3
+ adminRoute: string;
4
+ collectionLabel?: string;
5
+ collectionSlug?: string;
6
+ onRetry: () => void;
7
+ };
8
+ export declare function FigmaCloudLimitDocumentCountModal({ adminRoute, collectionLabel, collectionSlug, onRetry, }: Props): import("react").JSX.Element;
9
+ export {};
@@ -0,0 +1,78 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
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
+ import { ExternalLinkIcon } from '../icons/ExternalLinkIcon.js';
7
+ import { cloudLimitDocumentCountModalSlug } from '../shared.js';
8
+ import './index.scss';
9
+ const baseClass = 'figma-cloud-limit-modal';
10
+ export function FigmaCloudLimitDocumentCountModal({ adminRoute, collectionLabel, collectionSlug, onRetry }) {
11
+ const { t } = useTranslation();
12
+ 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
+ return /*#__PURE__*/ _jsxs(DialogModal, {
24
+ size: "small",
25
+ slug: cloudLimitDocumentCountModalSlug,
26
+ children: [
27
+ /*#__PURE__*/ _jsx(DialogHeader, {
28
+ showClose: true,
29
+ title: t('figma:cloudLimitModalHeading')
30
+ }),
31
+ /*#__PURE__*/ _jsx(DialogBody, {
32
+ children: /*#__PURE__*/ _jsx("p", {
33
+ className: `${baseClass}__body`,
34
+ children: t('figma:cloudLimitModalBody')
35
+ })
36
+ }),
37
+ /*#__PURE__*/ _jsx(DialogFooter, {
38
+ children: /*#__PURE__*/ _jsxs("div", {
39
+ className: `${baseClass}__footer`,
40
+ children: [
41
+ collectionLabel && collectionSlug ? /*#__PURE__*/ _jsxs("button", {
42
+ className: `${baseClass}__review-btn`,
43
+ onClick: handleReviewClick,
44
+ type: "button",
45
+ children: [
46
+ /*#__PURE__*/ _jsx("span", {
47
+ className: `${baseClass}__review-btn-label`,
48
+ children: t('figma:cloudLimitModalReviewCollection', {
49
+ collection: collectionLabel
50
+ })
51
+ }),
52
+ /*#__PURE__*/ _jsx(ExternalLinkIcon, {})
53
+ ]
54
+ }) : null,
55
+ /*#__PURE__*/ _jsxs("div", {
56
+ className: `${baseClass}__actions`,
57
+ children: [
58
+ /*#__PURE__*/ _jsx(DialogCancel, {
59
+ label: t('figma:cloudLimitModalConfirm')
60
+ }),
61
+ /*#__PURE__*/ _jsx(Button, {
62
+ buttonStyle: "primary",
63
+ margin: false,
64
+ onClick: ()=>{
65
+ closeModal(cloudLimitDocumentCountModalSlug);
66
+ onRetry();
67
+ },
68
+ size: "medium",
69
+ children: t('figma:cloudLimitModalRetry')
70
+ })
71
+ ]
72
+ })
73
+ ]
74
+ })
75
+ })
76
+ ]
77
+ });
78
+ }
@@ -0,0 +1,51 @@
1
+ .figma-cloud-limit-modal {
2
+ &__body {
3
+ margin: 0;
4
+ }
5
+
6
+ &__footer {
7
+ align-items: center;
8
+ display: flex;
9
+ gap: var(--spacer-4);
10
+ width: 100%;
11
+ }
12
+
13
+ &__review-btn {
14
+ align-items: center;
15
+ background: none;
16
+ border: none;
17
+ color: var(--color-text-brand);
18
+ cursor: pointer;
19
+ display: inline-flex;
20
+ flex-shrink: 1;
21
+ font-size: var(--text-body-medium-font-size);
22
+ gap: var(--spacer-1);
23
+ min-width: 0;
24
+ padding: 0;
25
+
26
+ &:hover {
27
+ color: var(--ramp-blue-700);
28
+ }
29
+
30
+ &:active {
31
+ color: var(--ramp-blue-800);
32
+ }
33
+ }
34
+
35
+ &__review-btn-label {
36
+ overflow: hidden;
37
+ text-overflow: ellipsis;
38
+ white-space: nowrap;
39
+ }
40
+
41
+ // Prevents the icon from shrinking when the label ellipses.
42
+ &__review-btn svg {
43
+ flex-shrink: 0;
44
+ }
45
+
46
+ &__actions {
47
+ display: flex;
48
+ gap: var(--spacer-2);
49
+ margin-inline-start: auto;
50
+ }
51
+ }
@@ -0,0 +1,7 @@
1
+ import type { ReactNode } from 'react';
2
+ /**
3
+ * Surfaces Content API cloud-limit errors in the admin UI.
4
+ */
5
+ export declare function FigmaCloudLimitProvider({ children }: {
6
+ children: ReactNode;
7
+ }): import("react").JSX.Element;
@@ -0,0 +1,169 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { getTranslation } from '@payloadcms/translations';
4
+ import { FormErrorHandlerContext, toast, useConfig, useFormErrorHandler, useModal, useTranslation } from '@payloadcms/ui';
5
+ import { hasAutosaveEnabled } from 'payload/shared';
6
+ import { useCallback, useEffect, useRef, useState } from 'react';
7
+ import { FigmaCloudLimitDocumentCountModal } from '../CloudLimitModal/index.js';
8
+ import { cloudLimitDocumentCountModalSlug, cloudLimitErrorCodes, cloudLimitToastKeyByCode, getCloudLimitErrorCode } from '../shared.js';
9
+ 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 }) {
18
+ const { openModal } = useModal();
19
+ const { i18n, t } = useTranslation();
20
+ const { config, getEntityConfig } = useConfig();
21
+ 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();
26
+ 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
+ ];
44
+ 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);
61
+ 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
+ }
101
+ }
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
+ };
110
+ }, []);
111
+ const handleNonFieldError = useCallback((err, retry)=>{
112
+ const code = getCloudLimitErrorCode({
113
+ errors: [
114
+ err
115
+ ]
116
+ });
117
+ if (!code) {
118
+ return parentErrorHandler?.(err, retry) ?? false;
119
+ }
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);
140
+ return true;
141
+ }
142
+ const messageKey = cloudLimitToastKeyByCode[code];
143
+ if (messageKey) {
144
+ toast.info(t(messageKey));
145
+ return true;
146
+ }
147
+ return false;
148
+ }, [
149
+ i18n,
150
+ openModal,
151
+ parentErrorHandler,
152
+ t
153
+ ]);
154
+ return /*#__PURE__*/ _jsx(FigmaCloudLimitSlugRegistryContext, {
155
+ value: registerCollectionSlug,
156
+ children: /*#__PURE__*/ _jsxs(FormErrorHandlerContext, {
157
+ value: handleNonFieldError,
158
+ children: [
159
+ children,
160
+ /*#__PURE__*/ _jsx(FigmaCloudLimitDocumentCountModal, {
161
+ adminRoute: adminRouteRef.current,
162
+ collectionLabel: collectionLabel,
163
+ collectionSlug: collectionSlug,
164
+ onRetry: ()=>retryRef.current?.()
165
+ })
166
+ ]
167
+ })
168
+ });
169
+ }
@@ -0,0 +1,2 @@
1
+ export type RegisterCollectionSlug = (slug: string | undefined) => () => void;
2
+ export declare const FigmaCloudLimitSlugRegistryContext: import("react").Context<RegisterCollectionSlug>;
@@ -0,0 +1,3 @@
1
+ 'use client';
2
+ import { createContext } from 'react';
3
+ export const FigmaCloudLimitSlugRegistryContext = /*#__PURE__*/ createContext(()=>()=>{});
@@ -0,0 +1,9 @@
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
+ export declare function FigmaCloudLimitSlugTracker(): null;
@@ -0,0 +1,23 @@
1
+ 'use client';
2
+ import { useDocumentInfo } from '@payloadcms/ui';
3
+ import { use, useEffect } from 'react';
4
+ 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() {
13
+ const register = use(FigmaCloudLimitSlugRegistryContext);
14
+ const { collectionSlug } = useDocumentInfo();
15
+ useEffect(()=>{
16
+ const unregister = register(collectionSlug);
17
+ return unregister;
18
+ }, [
19
+ collectionSlug,
20
+ register
21
+ ]);
22
+ return null;
23
+ }
@@ -0,0 +1,2 @@
1
+ /** External link icon (box with arrow pointing out). Used in cloud-limit UI. */
2
+ export declare function ExternalLinkIcon(): import("react").JSX.Element;
@@ -0,0 +1,30 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /** External link icon (box with arrow pointing out). Used in cloud-limit UI. */ export function ExternalLinkIcon() {
4
+ return /*#__PURE__*/ _jsxs("svg", {
5
+ "aria-hidden": "true",
6
+ fill: "none",
7
+ height: "14",
8
+ stroke: "currentColor",
9
+ strokeLinecap: "round",
10
+ strokeLinejoin: "round",
11
+ strokeWidth: "2",
12
+ viewBox: "0 0 24 24",
13
+ width: "14",
14
+ xmlns: "http://www.w3.org/2000/svg",
15
+ children: [
16
+ /*#__PURE__*/ _jsx("path", {
17
+ d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"
18
+ }),
19
+ /*#__PURE__*/ _jsx("polyline", {
20
+ points: "15 3 21 3 21 9"
21
+ }),
22
+ /*#__PURE__*/ _jsx("line", {
23
+ x1: "10",
24
+ x2: "21",
25
+ y1: "14",
26
+ y2: "3"
27
+ })
28
+ ]
29
+ });
30
+ }
@@ -0,0 +1,10 @@
1
+ export declare const cloudLimitErrorCodes: {
2
+ readonly assetStorage: "cloud_limit.cms_assets_bytes_exceeded";
3
+ readonly documentCount: "cloud_limit.document_count_exceeded";
4
+ readonly documentDataSize: "cloud_limit.per_document_data_bytes_exceeded";
5
+ };
6
+ export type CloudLimitErrorCode = (typeof cloudLimitErrorCodes)[keyof typeof cloudLimitErrorCodes];
7
+ export declare function isCloudLimitErrorCode(code: unknown): code is CloudLimitErrorCode;
8
+ export declare function getCloudLimitErrorCode(json: unknown): CloudLimitErrorCode | undefined;
9
+ export declare const cloudLimitDocumentCountModalSlug = "figma-cloud-limit-document-count";
10
+ export declare const cloudLimitToastKeyByCode: Partial<Record<CloudLimitErrorCode, string>>;
@@ -0,0 +1,27 @@
1
+ export const cloudLimitErrorCodes = {
2
+ assetStorage: 'cloud_limit.cms_assets_bytes_exceeded',
3
+ documentCount: 'cloud_limit.document_count_exceeded',
4
+ documentDataSize: 'cloud_limit.per_document_data_bytes_exceeded'
5
+ };
6
+ const knownCloudLimitCodes = Object.values(cloudLimitErrorCodes);
7
+ export function isCloudLimitErrorCode(code) {
8
+ return typeof code === 'string' && knownCloudLimitCodes.includes(code);
9
+ }
10
+ export function getCloudLimitErrorCode(json) {
11
+ const errors = json?.errors;
12
+ if (!Array.isArray(errors)) {
13
+ return undefined;
14
+ }
15
+ for (const err of errors){
16
+ const code = err?.data?.code;
17
+ if (isCloudLimitErrorCode(code)) {
18
+ return code;
19
+ }
20
+ }
21
+ return undefined;
22
+ }
23
+ export const cloudLimitDocumentCountModalSlug = 'figma-cloud-limit-document-count';
24
+ export const cloudLimitToastKeyByCode = {
25
+ [cloudLimitErrorCodes.assetStorage]: 'figma:cloudLimitAssetStorageError',
26
+ [cloudLimitErrorCodes.documentDataSize]: 'figma:cloudLimitDocumentDataSizeError'
27
+ };
@@ -0,0 +1,32 @@
1
+ import type { Config } from 'payload';
2
+ /**
3
+ * True inside the Make sandbox. agentplat injects `FIGMA=1`, absent locally and
4
+ * in the deployed Lambda; `FIGMA=true` matches the app template's vite config.
5
+ *
6
+ * KEEP_IN_SYNC(services/agentplat/scriptrun/script_runner.go): EnvFigma
7
+ */
8
+ export declare function isMakeSandbox(): boolean;
9
+ type SandboxUploadFetchOptions = {
10
+ /**
11
+ * The app's collections before any plugin runs. `plugin-cloud-storage` later
12
+ * rewrites `skipSafeFetch`, erasing whether the app set `false` or nothing, so
13
+ * intent must be read here.
14
+ */
15
+ userCollections: Config['collections'];
16
+ };
17
+ /**
18
+ * Defaults upload collections to skip `safeFetch` in the Make sandbox, where DNS
19
+ * maps external hostnames to unique-local IPv6 addresses (`fc00::/7`) that
20
+ * `safeFetch` rejects as non-unicast. A no-op elsewhere.
21
+ *
22
+ * Only a default: a collection that set `skipSafeFetch` itself keeps its value.
23
+ * `externalFileHeaderFilter` is left unset on purpose, since it would replace
24
+ * the cookie logic that forwards credentials for relative URLs.
25
+ *
26
+ * SSRF risks, bounded by the routes' auth gate and sandbox network limits:
27
+ * `getExternalFile` re-validates redirects only against `pasteURL.allowList`,
28
+ * any session can GET an arbitrary URL and read the body, and request cookies
29
+ * reach the target.
30
+ */
31
+ export declare function createSandboxUploadFetchPlugin({ userCollections }: SandboxUploadFetchOptions): (config: Config) => Config;
32
+ export {};
@@ -0,0 +1,53 @@
1
+ import * as log from '../utils/log.js';
2
+ /**
3
+ * True inside the Make sandbox. agentplat injects `FIGMA=1`, absent locally and
4
+ * in the deployed Lambda; `FIGMA=true` matches the app template's vite config.
5
+ *
6
+ * KEEP_IN_SYNC(services/agentplat/scriptrun/script_runner.go): EnvFigma
7
+ */ export function isMakeSandbox() {
8
+ return process.env.FIGMA === '1' || process.env.FIGMA === 'true';
9
+ }
10
+ /**
11
+ * Defaults upload collections to skip `safeFetch` in the Make sandbox, where DNS
12
+ * maps external hostnames to unique-local IPv6 addresses (`fc00::/7`) that
13
+ * `safeFetch` rejects as non-unicast. A no-op elsewhere.
14
+ *
15
+ * Only a default: a collection that set `skipSafeFetch` itself keeps its value.
16
+ * `externalFileHeaderFilter` is left unset on purpose, since it would replace
17
+ * the cookie logic that forwards credentials for relative URLs.
18
+ *
19
+ * SSRF risks, bounded by the routes' auth gate and sandbox network limits:
20
+ * `getExternalFile` re-validates redirects only against `pasteURL.allowList`,
21
+ * any session can GET an arbitrary URL and read the body, and request cookies
22
+ * reach the target.
23
+ */ export function createSandboxUploadFetchPlugin({ userCollections }) {
24
+ const appOwnedSlugs = new Set((userCollections ?? []).filter((collection)=>typeof collection.upload === 'object' && 'skipSafeFetch' in collection.upload).map((collection)=>collection.slug));
25
+ return (config)=>{
26
+ if (!isMakeSandbox()) {
27
+ return config;
28
+ }
29
+ const modifiedSlugs = [];
30
+ const collections = (config.collections ?? []).map((collection)=>{
31
+ if (!collection.upload || appOwnedSlugs.has(collection.slug)) {
32
+ return collection;
33
+ }
34
+ modifiedSlugs.push(collection.slug);
35
+ const upload = collection.upload === true ? {} : collection.upload;
36
+ return {
37
+ ...collection,
38
+ upload: {
39
+ ...upload,
40
+ skipSafeFetch: true
41
+ }
42
+ };
43
+ });
44
+ if (modifiedSlugs.length === 0) {
45
+ return config;
46
+ }
47
+ log.debug(`Make sandbox detected: upload collections skip safeFetch (${modifiedSlugs.join(', ')})`);
48
+ return {
49
+ ...config,
50
+ collections
51
+ };
52
+ };
53
+ }
@@ -0,0 +1,12 @@
1
+ export declare const arTranslations: {
2
+ figma: {
3
+ cloudLimitAssetStorageError: string;
4
+ cloudLimitDocumentCountError: string;
5
+ cloudLimitDocumentDataSizeError: string;
6
+ cloudLimitModalBody: string;
7
+ cloudLimitModalConfirm: string;
8
+ cloudLimitModalHeading: string;
9
+ cloudLimitModalRetry: string;
10
+ cloudLimitModalReviewCollection: string;
11
+ };
12
+ };
@@ -0,0 +1,12 @@
1
+ export const arTranslations = {
2
+ figma: {
3
+ cloudLimitAssetStorageError: 'يتعذر التحميل. يرجى تحرير بعض المساحة للمتابعة.',
4
+ cloudLimitDocumentCountError: 'غير قادر على إنشاء وثيقة. تم الوصول إلى الحد الأقصى لسعة التخزين.',
5
+ cloudLimitDocumentDataSizeError: 'تعذر حفظ المستند، النص كبير جدًا.',
6
+ cloudLimitModalBody: 'لقد وصلت إلى الحد الأقصى لعدد الوثائق. يرجى حذف بعض الوثائق الحالية لمواصلة الإنشاء.',
7
+ cloudLimitModalConfirm: 'تم فهم ذلك',
8
+ cloudLimitModalHeading: 'تعذر حفظ المستند',
9
+ cloudLimitModalRetry: 'إعادة المحاولة',
10
+ cloudLimitModalReviewCollection: 'مراجعة {{collection}}'
11
+ }
12
+ };
@@ -0,0 +1,12 @@
1
+ export declare const azTranslations: {
2
+ figma: {
3
+ cloudLimitAssetStorageError: string;
4
+ cloudLimitDocumentCountError: string;
5
+ cloudLimitDocumentDataSizeError: string;
6
+ cloudLimitModalBody: string;
7
+ cloudLimitModalConfirm: string;
8
+ cloudLimitModalHeading: string;
9
+ cloudLimitModalRetry: string;
10
+ cloudLimitModalReviewCollection: string;
11
+ };
12
+ };