@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.
- package/dist/api/check-project-subdomain-availability.d.ts +12 -0
- package/dist/api/check-project-subdomain-availability.js +32 -0
- package/dist/api/control-plane-error.d.ts +6 -0
- package/dist/api/control-plane-error.js +8 -0
- package/dist/api/control-plane-fetch.d.ts +7 -0
- package/dist/api/control-plane-fetch.js +37 -0
- package/dist/api/control-plane.d.ts +0 -8
- package/dist/api/control-plane.js +1 -47
- package/dist/api/create-project.d.ts +18 -0
- package/dist/api/create-project.js +37 -0
- package/dist/api/list-project-workspaces.d.ts +8 -0
- package/dist/api/list-project-workspaces.js +28 -0
- package/dist/api/project-resource-type.d.ts +1 -0
- package/dist/api/project-resource-type.js +1 -0
- package/dist/auth/headless-oauth-flow.d.ts +23 -0
- package/dist/auth/headless-oauth-flow.js +69 -0
- package/dist/auth/oauth-flow.d.ts +2 -0
- package/dist/auth/oauth-flow.js +13 -8
- package/dist/auth/token-store.d.ts +4 -1
- package/dist/auth/token-store.js +14 -0
- package/dist/auth/types.d.ts +12 -0
- package/dist/cli.d.ts +10 -1
- package/dist/cli.js +39 -4
- package/dist/commands/bootstrap.js +2 -1
- package/dist/commands/deploy.d.ts +10 -0
- package/dist/commands/deploy.js +39 -11
- package/dist/commands/detect-project-resource-type.d.ts +3 -0
- package/dist/commands/detect-project-resource-type.js +28 -0
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.js +163 -78
- package/dist/commands/login.d.ts +11 -1
- package/dist/commands/login.js +66 -4
- package/dist/commands/prompt-to-create-project/format-project-subdomain.d.ts +1 -0
- package/dist/commands/prompt-to-create-project/format-project-subdomain.js +3 -0
- package/dist/commands/prompt-to-create-project/format-unavailable-subdomain-reason.d.ts +1 -0
- package/dist/commands/prompt-to-create-project/format-unavailable-subdomain-reason.js +7 -0
- package/dist/commands/prompt-to-create-project/get-default-project-name.d.ts +1 -0
- package/dist/commands/prompt-to-create-project/get-default-project-name.js +13 -0
- package/dist/commands/prompt-to-create-project/prompt-for-available-subdomain.d.ts +7 -0
- package/dist/commands/prompt-to-create-project/prompt-for-available-subdomain.js +32 -0
- package/dist/commands/prompt-to-create-project/resolve-project-region.d.ts +8 -0
- package/dist/commands/prompt-to-create-project/resolve-project-region.js +27 -0
- package/dist/commands/prompt-to-create-project/resolve-project-subdomain.d.ts +9 -0
- package/dist/commands/prompt-to-create-project/resolve-project-subdomain.js +54 -0
- package/dist/commands/prompt-to-create-project/resolve-text-input.d.ts +9 -0
- package/dist/commands/prompt-to-create-project/resolve-text-input.js +23 -0
- package/dist/commands/prompt-to-create-project/resolve-workspace-plan-key.d.ts +12 -0
- package/dist/commands/prompt-to-create-project/resolve-workspace-plan-key.js +36 -0
- package/dist/commands/prompt-to-create-project/validate-subdomain.d.ts +1 -0
- package/dist/commands/prompt-to-create-project/validate-subdomain.js +9 -0
- package/dist/commands/prompt-to-create-project.d.ts +24 -0
- package/dist/commands/prompt-to-create-project.js +69 -0
- package/dist/commands/setup-agent.d.ts +12 -0
- package/dist/commands/setup-agent.js +123 -0
- package/dist/commands/write-project-configuration.d.ts +12 -0
- package/dist/commands/write-project-configuration.js +16 -0
- package/dist/db-content-api/index.js +52 -33
- package/dist/db-content-api/utilities/data/injectGlobalType.d.ts +6 -0
- package/dist/db-content-api/utilities/data/injectGlobalType.js +11 -0
- package/dist/db-content-api/utilities/isRecord.d.ts +9 -0
- package/dist/db-content-api/utilities/isRecord.js +19 -0
- package/dist/db-content-api/utilities/joins.js +5 -1
- package/dist/db-content-api/utilities/meta/resolvesToRelationshipValue.d.ts +10 -0
- package/dist/db-content-api/utilities/meta/resolvesToRelationshipValue.js +95 -0
- package/dist/db-content-api/utilities/meta/stringifyIdsInWhere.d.ts +9 -0
- package/dist/db-content-api/utilities/meta/stringifyIdsInWhere.js +112 -0
- package/dist/db-content-api/utilities/where.d.ts +2 -6
- package/dist/db-content-api/utilities/where.js +20 -9
- package/dist/exports/views.d.ts +1 -0
- package/dist/exports/views.js +1 -0
- package/dist/imageSizeOptions.d.ts +23 -0
- package/dist/imageSizeOptions.js +26 -0
- package/dist/index.d.ts +1 -0
- package/dist/lib/write-agent-config.d.ts +12 -0
- package/dist/lib/write-agent-config.js +52 -0
- package/dist/oauth/defaults.d.ts +1 -0
- package/dist/oauth/defaults.js +1 -0
- package/dist/oauth/endpoints/getLoginEndpoint.js +7 -2
- package/dist/oauth/index.js +24 -9
- package/dist/oauth/utilities/getAdminCollectionSlug.d.ts +1 -1
- package/dist/oauth/utilities/getAuthorizeURL.d.ts +3 -0
- package/dist/oauth/utilities/getAuthorizeURL.js +8 -2
- package/dist/plugin/build-config.js +22 -9
- package/dist/plugin/cloud-limits/CloudLimitModal/index.d.ts +2 -3
- package/dist/plugin/cloud-limits/CloudLimitModal/index.js +4 -16
- package/dist/plugin/cloud-limits/CloudLimitModal/index.scss +5 -0
- package/dist/plugin/cloud-limits/CloudLimitProvider/index.d.ts +0 -3
- package/dist/plugin/cloud-limits/CloudLimitProvider/index.js +65 -124
- package/dist/plugin/cloud-limits/LimitReachedView/index.css +14 -0
- package/dist/plugin/cloud-limits/LimitReachedView/index.d.ts +4 -0
- package/dist/plugin/cloud-limits/LimitReachedView/index.js +52 -0
- package/dist/plugin/cloud-limits/SlugRegistryContext/index.d.ts +7 -2
- package/dist/plugin/cloud-limits/SlugTracker/index.d.ts +0 -8
- package/dist/plugin/cloud-limits/SlugTracker/index.js +14 -10
- package/dist/plugin/cloud-limits/routes.d.ts +2 -0
- package/dist/plugin/cloud-limits/routes.js +4 -0
- package/dist/plugin/cloud-limits/server/redirectInitialAutosaveCreate.d.ts +9 -0
- package/dist/plugin/cloud-limits/server/redirectInitialAutosaveCreate.js +28 -0
- package/dist/plugin/cloud-limits/shared.d.ts +1 -1
- package/dist/plugin/cloud-limits/shared.js +25 -0
- package/dist/plugin/cloud-limits/utilities/cloudLimitInterceptor.d.ts +2 -0
- package/dist/plugin/cloud-limits/utilities/cloudLimitInterceptor.js +134 -0
- package/dist/plugin/cloud-limits/utilities/toasts.d.ts +4 -0
- package/dist/plugin/cloud-limits/utilities/toasts.js +19 -0
- package/dist/skills/deploy-to-figma/SKILL.md +234 -0
- package/dist/skills/deploy-to-figma/references/cli-reference.md +146 -0
- package/dist/types.d.ts +9 -0
- package/dist/utils/lambda-config.js +4 -14
- package/dist/utils/messages.js +22 -3
- package/dist/utils/payload-generate.d.ts +10 -0
- package/dist/utils/payload-generate.js +59 -0
- package/dist/utils/pnpm-builds.d.ts +14 -0
- package/dist/utils/pnpm-builds.js +125 -0
- package/package.json +9 -1
- package/dist/lib/download-skill.d.ts +0 -12
- 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,
|
|
5
|
-
import {
|
|
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,
|
|
9
|
+
import { cloudLimitDocumentCountModalSlug, cloudLimitErrorCodes, getCloudLimitErrorCode } from '../shared.js';
|
|
9
10
|
import { FigmaCloudLimitSlugRegistryContext } from '../SlugRegistryContext/index.js';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
function
|
|
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
|
|
23
|
-
const
|
|
24
|
-
const [
|
|
25
|
-
|
|
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
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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:
|
|
94
|
+
value: register,
|
|
156
95
|
children: /*#__PURE__*/ _jsxs(FormErrorHandlerContext, {
|
|
157
96
|
value: handleNonFieldError,
|
|
158
97
|
children: [
|
|
159
98
|
children,
|
|
160
|
-
/*#__PURE__*/ _jsx(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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,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
|
|
2
|
-
|
|
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(
|
|
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,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:
|
|
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,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
|
+
}
|