From 1721b2156349336980afd9857d663f4d5ac18cc3 Mon Sep 17 00:00:00 2001 From: sadakchap Date: Tue, 21 Apr 2026 17:18:26 +0530 Subject: [PATCH 1/5] add json editor --- .../CustomParseOptions.react.js | 490 +++++++++++++++--- 1 file changed, 425 insertions(+), 65 deletions(-) diff --git a/src/dashboard/CustomParseOptions/CustomParseOptions.react.js b/src/dashboard/CustomParseOptions/CustomParseOptions.react.js index c79c4f602a..417f931ad7 100644 --- a/src/dashboard/CustomParseOptions/CustomParseOptions.react.js +++ b/src/dashboard/CustomParseOptions/CustomParseOptions.react.js @@ -26,6 +26,8 @@ import TextInputSettings from 'components/TextInputSettings/TextInputSettings.re import Button from 'components/Button/Button.react'; import B4aTooltip from 'components/Tooltip/B4aTooltip.react'; import Icon from 'components/Icon/Icon.react'; +import B4aModal from 'components/B4aModal/B4aModal.react'; +import B4aCodeEditor from 'components/CodeEditor/B4aCodeEditor.react'; import { Link } from 'react-router-dom'; import deepmerge from 'deepmerge'; @@ -34,6 +36,140 @@ import CustomParseOptionsValidations from './CustomParseOptionsValidations'; import getError from 'dashboard/Settings/Util/getError'; import semver from 'semver'; +const CUSTOM_PAGES_KEYS = [ + 'choosePassword', + 'verifyEmailSuccess', + 'parseFrameURL', + 'passwordResetSuccess', + 'invalidLink', + 'invalidVerificationLink', + 'linkSendSuccess', + 'linkSendFail', +]; + +const getActualChanges = (changes, initial) => { + const result = {}; + for (const key of Object.keys(changes)) { + const val = changes[key]; + const ref = initial ? initial[key] : undefined; + if (val && typeof val === 'object' && !Array.isArray(val)) { + const nested = getActualChanges(val, ref || {}); + if (Object.keys(nested).length > 0) { + result[key] = nested; + } + } else if (val !== ref) { + result[key] = val; + } + } + return result; +}; + +const transformCustomOptionsForPayload = (customOptionsInput) => { + if (!customOptionsInput) { + return undefined; + } + const payload = JSON.parse(JSON.stringify(customOptionsInput)); + delete payload.databaseURI; + if (payload.maxUploadSize != null && payload.maxUploadSize !== '') { + payload.maxUploadSize = `${payload.maxUploadSize}mb`; + } + const customPages = { ...(payload.customPages || {}) }; + CUSTOM_PAGES_KEYS.forEach(key => { + if (Object.prototype.hasOwnProperty.call(payload, key)) { + customPages[key] = payload[key] === '' ? undefined : payload[key]; + delete payload[key]; + } + }); + if (Object.keys(customPages).length > 0) { + payload.customPages = customPages; + } + for (const key of Object.keys(payload)) { + if (typeof payload[key] === 'string' && payload[key].trim() === '') { + payload[key] = undefined; + } + } + return payload; +}; + +const buildSaveParseOptionsPayload = (fields, initialFields) => { + const changes = getActualChanges(fields, initialFields); + return { + customOptions: transformCustomOptionsForPayload(changes.customOptions), + clientPush: Object.prototype.hasOwnProperty.call(changes, 'clientPush') + ? changes.clientPush + : undefined, + clientClassCreation: Object.prototype.hasOwnProperty.call(changes, 'clientClassCreation') + ? changes.clientClassCreation + : undefined, + }; +}; + +// Keys that live alongside `customOptions` at the top of the saveParseOptionsAndSettings +// payload. Everything else in the flat JSON editor gets grouped under `customOptions`. +const TOP_LEVEL_PAYLOAD_KEYS = ['clientPush', 'clientClassCreation']; + +// Builds the flat shape shown in the JSON editor: all customOptions keys are +// merged into the root next to clientPush/clientClassCreation so the user +// doesn't need to know which keys belong under `customOptions`. +const buildFlatEditorPayload = (fields) => { + const transformed = transformCustomOptionsForPayload(fields.customOptions) || {}; + return { + ...transformed, + clientPush: fields.clientPush, + clientClassCreation: fields.clientClassCreation, + }; +}; + +// Inverse of `buildFlatEditorPayload` — groups every non top-level key back +// under `customOptions` so we can send the correct saveParseOptionsAndSettings +// shape. +const unflattenEditorPayload = (flatParsed) => { + const result = { + customOptions: {}, + clientPush: undefined, + clientClassCreation: undefined, + }; + if (!flatParsed || typeof flatParsed !== 'object' || Array.isArray(flatParsed)) { + return result; + } + for (const key of Object.keys(flatParsed)) { + if (TOP_LEVEL_PAYLOAD_KEYS.indexOf(key) !== -1) { + result[key] = flatParsed[key]; + } else { + result.customOptions[key] = flatParsed[key]; + } + } + return result; +}; + +// Reverse of `transformCustomOptionsForPayload` so JSON edits from the modal +// can be mapped back onto the form fields (which use raw shapes like a numeric +// maxUploadSize and flat custom-page keys). +const reverseTransformCustomOptions = (payloadCustomOptions) => { + if (!payloadCustomOptions || typeof payloadCustomOptions !== 'object') { + return {}; + } + const result = JSON.parse(JSON.stringify(payloadCustomOptions)); + + if (typeof result.maxUploadSize === 'string') { + const parsed = parseInt(result.maxUploadSize, 10); + if (!Number.isNaN(parsed)) { + result.maxUploadSize = parsed; + } + } + + if (result.customPages && typeof result.customPages === 'object') { + CUSTOM_PAGES_KEYS.forEach(key => { + if (Object.prototype.hasOwnProperty.call(result.customPages, key)) { + result[key] = result.customPages[key]; + } + }); + delete result.customPages; + } + + return result; +}; + const LabelInfoTooltip = ({ description, children }) => { const [visible, setVisible] = React.useState(false); const [placement, setPlacement] = React.useState('top'); @@ -112,6 +248,11 @@ class CustomParseOptions extends DashboardView { canChangeCustomParseOptions: false, hasOtherConfigsPermission: true, isOwner: false, + showPayloadModal: false, + copyStatus: '', + payloadJson: '', + isSavingPayload: false, + payloadSaveError: '', }; this.onRefresh = this.onRefresh.bind(this); } @@ -239,7 +380,7 @@ class CustomParseOptions extends DashboardView { ); } - renderParseOptionsForm({ fields, setField, setFieldJson, errors }) { + renderParseOptionsForm({ fields, setField, setFieldJson, resetFields, errors }) { const customOptions = fields.customOptions || {}; const clientPush = fields.clientPush === true; const clientClassCreation = fields.clientClassCreation; @@ -269,6 +410,7 @@ class CustomParseOptions extends DashboardView { const isOwner = this.state.isOwner; return ( + <>
Parse Server Options
@@ -1106,11 +1248,290 @@ class CustomParseOptions extends DashboardView { /> }
+ +
+ +
+ + } + input={ +
+
+ } + theme={Field.Theme.BLUE} + /> +
+ + {this.state.showPayloadModal && this.renderPayloadModal(fields, setField, resetFields)} + ) } + renderPayloadModal(fields, setField, resetFields) { + const payloadJson = this.state.payloadJson; + const isSaving = this.state.isSavingPayload; + const payloadSaveError = this.state.payloadSaveError; + + let parseError = ''; + try { + JSON.parse(payloadJson); + } catch (e) { + parseError = e && e.message ? e.message : 'Invalid JSON'; + } + + const closeModal = () => { + if (isSaving) { + return; + } + this.setState({ + showPayloadModal: false, + copyStatus: '', + payloadSaveError: '', + }); + }; + + const resetPayload = () => { + const flatPayload = buildFlatEditorPayload(fields); + this.setState({ + payloadJson: JSON.stringify(flatPayload, null, 2), + copyStatus: '', + payloadSaveError: '', + }); + }; + + const applyJsonToFields = (value) => { + let parsed; + try { + parsed = JSON.parse(value); + } catch (e) { + return; + } + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + return; + } + const unflat = unflattenEditorPayload(parsed); + setField('customOptions', reverseTransformCustomOptions(unflat.customOptions)); + if (Object.prototype.hasOwnProperty.call(parsed, 'clientPush')) { + setField('clientPush', unflat.clientPush); + } + if (Object.prototype.hasOwnProperty.call(parsed, 'clientClassCreation')) { + setField('clientClassCreation', unflat.clientClassCreation); + } + }; + + const handleCodeChange = (value) => { + this.setState({ payloadJson: value, copyStatus: '', payloadSaveError: '' }); + applyJsonToFields(value); + }; + + const copyToClipboard = async () => { + try { + if (navigator && navigator.clipboard && navigator.clipboard.writeText) { + await navigator.clipboard.writeText(payloadJson); + } else { + const textarea = document.createElement('textarea'); + textarea.value = payloadJson; + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand('copy'); + document.body.removeChild(textarea); + } + this.setState({ copyStatus: 'Copied!' }); + setTimeout(() => { + if (this.state.showPayloadModal) { + this.setState({ copyStatus: '' }); + } + }, 1500); + } catch (e) { + this.setState({ copyStatus: 'Failed to copy' }); + } + }; + + const handleSave = async () => { + let parsed; + try { + parsed = JSON.parse(payloadJson); + } catch (e) { + this.setState({ + payloadSaveError: `Invalid JSON: ${e && e.message ? e.message : 'parse failed'}`, + }); + return; + } + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + this.setState({ payloadSaveError: 'Payload must be a JSON object.' }); + return; + } + + const unflat = unflattenEditorPayload(parsed); + + this.setState({ isSavingPayload: true, payloadSaveError: '' }); + + try { + await this.context.saveParseOptionsAndSettings(unflat); + + const prevInitial = this.state.initialFields || {}; + this.setState({ + isSavingPayload: false, + payloadSaveError: '', + showPayloadModal: false, + copyStatus: '', + payloadJson: '', + initialFields: { + customOptions: reverseTransformCustomOptions(unflat.customOptions), + clientPush: Object.prototype.hasOwnProperty.call(parsed, 'clientPush') + ? unflat.clientPush + : prevInitial.clientPush, + clientClassCreation: Object.prototype.hasOwnProperty.call(parsed, 'clientClassCreation') + ? unflat.clientClassCreation + : prevInitial.clientClassCreation, + }, + }); + + if (typeof resetFields === 'function') { + // Clear FlowView's local changes so the dirty-footer resets to the new baseline. + resetFields(); + } + } catch (e) { + const errors = Array.isArray(e && e.errors) ? e.errors : []; + const message = + errors.join(' ') || + (e && (e.error || e.message || e.notice)) || + (typeof e === 'string' ? e : 'Failed to save parse options.'); + this.setState({ + isSavingPayload: false, + payloadSaveError: message, + }); + } + }; + + return ( + +
+
+ +
+
+ + {parseError + ? `Invalid JSON: ${parseError}` + : 'Valid edits sync to the form fields.'} + +
+ + +
+
+ {payloadSaveError && ( +
+ {payloadSaveError} +
+ )} +
+
+ ); + } + renderContent() { const toolbar = this.renderToolbar(); const loading = this.state.isLoading; @@ -1120,22 +1541,6 @@ class CustomParseOptions extends DashboardView { clientClassCreation: true, }; - const getActualChanges = (changes, initial) => { - const result = {}; - for (const key of Object.keys(changes)) { - const val = changes[key]; - const ref = initial ? initial[key] : undefined; - if (val && typeof val === 'object' && !Array.isArray(val)) { - const nested = getActualChanges(val, ref || {}); - if (Object.keys(nested).length > 0) { - result[key] = nested; - } - } else if (val !== ref) { - result[key] = val; - } - } - return result; - }; const customParseOptionsFieldsOptions = { customOptions: { friendlyName: 'custom options', type: 'json' }, clientPush: { friendlyName: 'push notification from client', showTo: true }, @@ -1175,54 +1580,9 @@ class CustomParseOptions extends DashboardView { defaultFooterMessage={You don't have permission to edit this feature.} hideButtonsOnDefaultMessage={true} onSubmit={({ changes: rawChanges }) => { - const changes = getActualChanges(rawChanges, initialFields); - const customPagesKeys = [ - 'choosePassword', - 'verifyEmailSuccess', - 'parseFrameURL', - 'passwordResetSuccess', - 'invalidLink', - 'invalidVerificationLink', - 'linkSendSuccess', - 'linkSendFail', - ]; - const payload = changes.customOptions - ? JSON.parse(JSON.stringify(changes.customOptions)) - : undefined; - - if (payload) { - delete payload.databaseURI; - if (payload.maxUploadSize != null && payload.maxUploadSize !== '') { - payload.maxUploadSize = `${payload.maxUploadSize}mb`; - } - - const customPages = { ...(payload.customPages || {}) }; - customPagesKeys.forEach(key => { - if (Object.prototype.hasOwnProperty.call(payload, key)) { - customPages[key] = payload[key] === '' ? undefined : payload[key]; - delete payload[key]; - } - }); - if (Object.keys(customPages).length > 0) { - payload.customPages = customPages; - } - - for (const key of Object.keys(payload)) { - if (typeof payload[key] === 'string' && payload[key].trim() === '') { - payload[key] = undefined; - } - } - } - - return this.context.saveParseOptionsAndSettings({ - customOptions: payload, - clientPush: Object.prototype.hasOwnProperty.call(changes, 'clientPush') - ? changes.clientPush - : undefined, - clientClassCreation: Object.prototype.hasOwnProperty.call(changes, 'clientClassCreation') - ? changes.clientClassCreation - : undefined, - }); + return this.context.saveParseOptionsAndSettings( + buildSaveParseOptionsPayload(rawChanges, initialFields) + ); }} afterSave={({ fields, resetFields }) => { this.setState({ From a9f0d1165cd58abb3bade3b7ae612e16aab7d581 Mon Sep 17 00:00:00 2001 From: sadakchap Date: Tue, 21 Apr 2026 18:41:01 +0530 Subject: [PATCH 2/5] fix requested changes --- .../CustomParseOptions.react.js | 112 +++++++++++++++++- .../CustomParseOptions.scss | 6 + 2 files changed, 112 insertions(+), 6 deletions(-) diff --git a/src/dashboard/CustomParseOptions/CustomParseOptions.react.js b/src/dashboard/CustomParseOptions/CustomParseOptions.react.js index 417f931ad7..354a0307b4 100644 --- a/src/dashboard/CustomParseOptions/CustomParseOptions.react.js +++ b/src/dashboard/CustomParseOptions/CustomParseOptions.react.js @@ -47,6 +47,59 @@ const CUSTOM_PAGES_KEYS = [ 'linkSendFail', ]; +// Whitelist of `customOptions` keys that the form actually manages. Used by +// the "Reset to current form state" action to drop any stray keys the user +// typed into the JSON editor. +const KNOWN_CUSTOM_OPTIONS_KEYS = [ + 'publicServerURL', + 'maxUploadSize', + 'preserveFileName', + 'enableSingleSchemaCache', + 'allowCustomObjectId', + 'objectIdSize', + 'passwordPolicy', + 'accountLockout', + 'enableAnonymousUsers', + 'enforcePrivateUsers', + 'sessionLength', + 'emailVerifyTokenValidityDuration', + 'expireInactiveSessions', + ...CUSTOM_PAGES_KEYS, +]; + +const filterToKnownCustomOptions = (rawCustomOptions) => { + const filtered = {}; + if (!rawCustomOptions || typeof rawCustomOptions !== 'object') { + return filtered; + } + for (const key of KNOWN_CUSTOM_OPTIONS_KEYS) { + if (Object.prototype.hasOwnProperty.call(rawCustomOptions, key)) { + filtered[key] = rawCustomOptions[key]; + } + } + return filtered; +}; + +// Recursively sort object keys so the JSON editor shows properties in a +// deterministic, alphabetical order whenever we rebuild the text ourselves +// (modal open, reset, etc.). User-driven edits bypass this, preserving caret +// position while they're typing. +const sortKeysDeep = (value) => { + if (Array.isArray(value)) { + return value.map(sortKeysDeep); + } + if (value && typeof value === 'object') { + const sorted = {}; + for (const key of Object.keys(value).sort()) { + sorted[key] = sortKeysDeep(value[key]); + } + return sorted; + } + return value; +}; + +const stringifyAlpha = (value) => JSON.stringify(sortKeysDeep(value), null, 2); + const getActualChanges = (changes, initial) => { const result = {}; for (const key of Object.keys(changes)) { @@ -111,12 +164,14 @@ const TOP_LEVEL_PAYLOAD_KEYS = ['clientPush', 'clientClassCreation']; // Builds the flat shape shown in the JSON editor: all customOptions keys are // merged into the root next to clientPush/clientClassCreation so the user // doesn't need to know which keys belong under `customOptions`. +// The `?? default` guards ensure these two always render (JSON.stringify +// drops properties whose value is `undefined`). const buildFlatEditorPayload = (fields) => { const transformed = transformCustomOptionsForPayload(fields.customOptions) || {}; return { ...transformed, - clientPush: fields.clientPush, - clientClassCreation: fields.clientClassCreation, + clientPush: fields.clientPush ?? false, + clientClassCreation: fields.clientClassCreation ?? true, }; }; @@ -253,6 +308,9 @@ class CustomParseOptions extends DashboardView { payloadJson: '', isSavingPayload: false, payloadSaveError: '', + // Snapshot of fields taken when the JSON modal opens so "Reset" can + // undo any keys the user added in the JSON editor (not just reformat). + payloadFieldsSnapshot: null, }; this.onRefresh = this.onRefresh.bind(this); } @@ -414,7 +472,18 @@ class CustomParseOptions extends DashboardView {
Parse Server Options
-
Configure advanced settings of your Parse Server instance, including server behavior, authentication, and security rules.
+
+ Configure advanced settings of your Parse Server instance, including server behavior, authentication, and security rules. + Check available option in {' '} + + Documentation + . +
Warning: Changes apply immediately and may affect your app’s availability or client connections.
{!this.state.canChangeCustomParseOptions && ( @@ -1267,11 +1336,19 @@ class CustomParseOptions extends DashboardView { primary={true} onClick={() => { const flatPayload = buildFlatEditorPayload(fields); + const snapshot = { + customOptions: fields.customOptions + ? JSON.parse(JSON.stringify(fields.customOptions)) + : {}, + clientPush: fields.clientPush, + clientClassCreation: fields.clientClassCreation, + }; this.setState({ showPayloadModal: true, copyStatus: '', payloadSaveError: '', - payloadJson: JSON.stringify(flatPayload, null, 2), + payloadJson: stringifyAlpha(flatPayload), + payloadFieldsSnapshot: snapshot, }); }} /> @@ -1308,16 +1385,38 @@ class CustomParseOptions extends DashboardView { showPayloadModal: false, copyStatus: '', payloadSaveError: '', + payloadFieldsSnapshot: null, }); }; const resetPayload = () => { - const flatPayload = buildFlatEditorPayload(fields); + // Reset to the snapshot captured when the modal opened so we drop any + // extra keys the user added inside the editor (not just reformat them). + // Also filter `customOptions` down to keys the form actually manages, + // so the JSON editor only shows form-field properties after reset. + const snapshot = this.state.payloadFieldsSnapshot || { + customOptions: fields.customOptions || {}, + clientPush: fields.clientPush, + clientClassCreation: fields.clientClassCreation, + }; + const filteredCustomOptions = filterToKnownCustomOptions(snapshot.customOptions); + const flatPayload = buildFlatEditorPayload({ + customOptions: filteredCustomOptions, + clientPush: snapshot.clientPush, + clientClassCreation: snapshot.clientClassCreation, + }); this.setState({ - payloadJson: JSON.stringify(flatPayload, null, 2), + payloadJson: stringifyAlpha(flatPayload), copyStatus: '', payloadSaveError: '', }); + // Also drop form-field changes the JSON editor pushed in since open. + setField( + 'customOptions', + JSON.parse(JSON.stringify(filteredCustomOptions)) + ); + setField('clientPush', snapshot.clientPush); + setField('clientClassCreation', snapshot.clientClassCreation); }; const applyJsonToFields = (value) => { @@ -1399,6 +1498,7 @@ class CustomParseOptions extends DashboardView { showPayloadModal: false, copyStatus: '', payloadJson: '', + payloadFieldsSnapshot: null, initialFields: { customOptions: reverseTransformCustomOptions(unflat.customOptions), clientPush: Object.prototype.hasOwnProperty.call(parsed, 'clientPush') diff --git a/src/dashboard/CustomParseOptions/CustomParseOptions.scss b/src/dashboard/CustomParseOptions/CustomParseOptions.scss index cdcac3f668..2e8539eadf 100644 --- a/src/dashboard/CustomParseOptions/CustomParseOptions.scss +++ b/src/dashboard/CustomParseOptions/CustomParseOptions.scss @@ -232,4 +232,10 @@ display: inline-flex; align-items: center; line-height: 1; +} + +.docsLink { + cursor: pointer; + text-decoration: underline; + font-weight: 600; } \ No newline at end of file From 26f69230f4e8bd423c313718c311b0e31b65d73a Mon Sep 17 00:00:00 2001 From: sadakchap Date: Tue, 21 Apr 2026 19:10:44 +0530 Subject: [PATCH 3/5] fix 2 --- .../CustomParseOptions.react.js | 58 +++++++------------ 1 file changed, 22 insertions(+), 36 deletions(-) diff --git a/src/dashboard/CustomParseOptions/CustomParseOptions.react.js b/src/dashboard/CustomParseOptions/CustomParseOptions.react.js index 354a0307b4..5ff2ce38cf 100644 --- a/src/dashboard/CustomParseOptions/CustomParseOptions.react.js +++ b/src/dashboard/CustomParseOptions/CustomParseOptions.react.js @@ -48,8 +48,8 @@ const CUSTOM_PAGES_KEYS = [ ]; // Whitelist of `customOptions` keys that the form actually manages. Used by -// the "Reset to current form state" action to drop any stray keys the user -// typed into the JSON editor. +// the "Reset to saved values" action to drop any stray keys the user typed +// into the JSON editor. const KNOWN_CUSTOM_OPTIONS_KEYS = [ 'publicServerURL', 'maxUploadSize', @@ -308,9 +308,6 @@ class CustomParseOptions extends DashboardView { payloadJson: '', isSavingPayload: false, payloadSaveError: '', - // Snapshot of fields taken when the JSON modal opens so "Reset" can - // undo any keys the user added in the JSON editor (not just reformat). - payloadFieldsSnapshot: null, }; this.onRefresh = this.onRefresh.bind(this); } @@ -473,7 +470,7 @@ class CustomParseOptions extends DashboardView {
Parse Server Options
- Configure advanced settings of your Parse Server instance, including server behavior, authentication, and security rules. + Configure advanced settings of your Parse Server instance, including server behavior, authentication, and security rules. Check available option in {' '} { + if (!isOwner || !this.state.canChangeCustomParseOptions) { + return; + } const flatPayload = buildFlatEditorPayload(fields); - const snapshot = { - customOptions: fields.customOptions - ? JSON.parse(JSON.stringify(fields.customOptions)) - : {}, - clientPush: fields.clientPush, - clientClassCreation: fields.clientClassCreation, - }; this.setState({ showPayloadModal: true, copyStatus: '', payloadSaveError: '', payloadJson: stringifyAlpha(flatPayload), - payloadFieldsSnapshot: snapshot, }); }} /> @@ -1385,38 +1378,32 @@ class CustomParseOptions extends DashboardView { showPayloadModal: false, copyStatus: '', payloadSaveError: '', - payloadFieldsSnapshot: null, }); }; const resetPayload = () => { - // Reset to the snapshot captured when the modal opened so we drop any - // extra keys the user added inside the editor (not just reformat them). - // Also filter `customOptions` down to keys the form actually manages, - // so the JSON editor only shows form-field properties after reset. - const snapshot = this.state.payloadFieldsSnapshot || { - customOptions: fields.customOptions || {}, - clientPush: fields.clientPush, - clientClassCreation: fields.clientClassCreation, - }; - const filteredCustomOptions = filterToKnownCustomOptions(snapshot.customOptions); + // Revert to the original values from the most recent API response + // (stored in `initialFields`). This drops every form-level and + // JSON-editor change the user has made in this session. We still + // filter `customOptions` to keys the form actually manages so the + // editor only shows form-field properties after reset. + const apiInitialFields = this.state.initialFields || {}; + const filteredCustomOptions = filterToKnownCustomOptions(apiInitialFields.customOptions); const flatPayload = buildFlatEditorPayload({ customOptions: filteredCustomOptions, - clientPush: snapshot.clientPush, - clientClassCreation: snapshot.clientClassCreation, + clientPush: apiInitialFields.clientPush, + clientClassCreation: apiInitialFields.clientClassCreation, }); this.setState({ payloadJson: stringifyAlpha(flatPayload), copyStatus: '', payloadSaveError: '', }); - // Also drop form-field changes the JSON editor pushed in since open. - setField( - 'customOptions', - JSON.parse(JSON.stringify(filteredCustomOptions)) - ); - setField('clientPush', snapshot.clientPush); - setField('clientClassCreation', snapshot.clientClassCreation); + // Clear FlowView's tracked changes so `fields` = `initialFields` again + // (the form fields revert alongside the JSON). + if (typeof resetFields === 'function') { + resetFields(); + } }; const applyJsonToFields = (value) => { @@ -1498,7 +1485,6 @@ class CustomParseOptions extends DashboardView { showPayloadModal: false, copyStatus: '', payloadJson: '', - payloadFieldsSnapshot: null, initialFields: { customOptions: reverseTransformCustomOptions(unflat.customOptions), clientPush: Object.prototype.hasOwnProperty.call(parsed, 'clientPush') @@ -1608,7 +1594,7 @@ class CustomParseOptions extends DashboardView { padding: 0, }} > - Reset to current form state + Reset to saved values
From 7169552ddedb1ac42405c1bf82bfefb8b0119611 Mon Sep 17 00:00:00 2001 From: sadakchap Date: Tue, 21 Apr 2026 19:32:07 +0530 Subject: [PATCH 4/5] fix bug3 --- .../CustomParseOptions.react.js | 46 ++----------------- 1 file changed, 4 insertions(+), 42 deletions(-) diff --git a/src/dashboard/CustomParseOptions/CustomParseOptions.react.js b/src/dashboard/CustomParseOptions/CustomParseOptions.react.js index 5ff2ce38cf..ad25f5be36 100644 --- a/src/dashboard/CustomParseOptions/CustomParseOptions.react.js +++ b/src/dashboard/CustomParseOptions/CustomParseOptions.react.js @@ -47,39 +47,6 @@ const CUSTOM_PAGES_KEYS = [ 'linkSendFail', ]; -// Whitelist of `customOptions` keys that the form actually manages. Used by -// the "Reset to saved values" action to drop any stray keys the user typed -// into the JSON editor. -const KNOWN_CUSTOM_OPTIONS_KEYS = [ - 'publicServerURL', - 'maxUploadSize', - 'preserveFileName', - 'enableSingleSchemaCache', - 'allowCustomObjectId', - 'objectIdSize', - 'passwordPolicy', - 'accountLockout', - 'enableAnonymousUsers', - 'enforcePrivateUsers', - 'sessionLength', - 'emailVerifyTokenValidityDuration', - 'expireInactiveSessions', - ...CUSTOM_PAGES_KEYS, -]; - -const filterToKnownCustomOptions = (rawCustomOptions) => { - const filtered = {}; - if (!rawCustomOptions || typeof rawCustomOptions !== 'object') { - return filtered; - } - for (const key of KNOWN_CUSTOM_OPTIONS_KEYS) { - if (Object.prototype.hasOwnProperty.call(rawCustomOptions, key)) { - filtered[key] = rawCustomOptions[key]; - } - } - return filtered; -}; - // Recursively sort object keys so the JSON editor shows properties in a // deterministic, alphabetical order whenever we rebuild the text ourselves // (modal open, reset, etc.). User-driven edits bypass this, preserving caret @@ -1384,16 +1351,11 @@ class CustomParseOptions extends DashboardView { const resetPayload = () => { // Revert to the original values from the most recent API response // (stored in `initialFields`). This drops every form-level and - // JSON-editor change the user has made in this session. We still - // filter `customOptions` to keys the form actually manages so the - // editor only shows form-field properties after reset. + // JSON-editor change the user has made in this session and surfaces + // every saved key — including any the form doesn't render — so the + // editor matches the GET /parseOptions response. const apiInitialFields = this.state.initialFields || {}; - const filteredCustomOptions = filterToKnownCustomOptions(apiInitialFields.customOptions); - const flatPayload = buildFlatEditorPayload({ - customOptions: filteredCustomOptions, - clientPush: apiInitialFields.clientPush, - clientClassCreation: apiInitialFields.clientClassCreation, - }); + const flatPayload = buildFlatEditorPayload(apiInitialFields); this.setState({ payloadJson: stringifyAlpha(flatPayload), copyStatus: '', From 2a968576c2267018d34ea29430ab475dc173ace6 Mon Sep 17 00:00:00 2001 From: sadakchap Date: Tue, 21 Apr 2026 19:57:07 +0530 Subject: [PATCH 5/5] last fix maybe --- .../CustomParseOptions/CustomParseOptions.react.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/dashboard/CustomParseOptions/CustomParseOptions.react.js b/src/dashboard/CustomParseOptions/CustomParseOptions.react.js index ad25f5be36..5573e06a4e 100644 --- a/src/dashboard/CustomParseOptions/CustomParseOptions.react.js +++ b/src/dashboard/CustomParseOptions/CustomParseOptions.react.js @@ -385,7 +385,7 @@ class CustomParseOptions extends DashboardView { initialFields: { customOptions, clientPush: response.clientPush ?? false, - clientClassCreation: response.clientClassCreation !== 'undefined' ? response.clientClassCreation : true, + clientClassCreation: response.clientClassCreation ?? true, }, }); } catch (error) { @@ -1288,7 +1288,7 @@ class CustomParseOptions extends DashboardView { @@ -1415,6 +1415,11 @@ class CustomParseOptions extends DashboardView { }, 1500); } catch (e) { this.setState({ copyStatus: 'Failed to copy' }); + setTimeout(() => { + if (this.state.showPayloadModal && this.state.copyStatus === 'Failed to copy') { + this.setState({ copyStatus: '' }); + } + }, 1500); } };