From 31d5404a7ac6cb2b8e44c8c422cdfafc680e5b72 Mon Sep 17 00:00:00 2001 From: anupamme Date: Fri, 25 Sep 2026 12:33:41 +0000 Subject: [PATCH 1/2] fix: multi_agent.cwe-384 security vulnerability Automated security fix generated by OrbisAI Security --- Parse-Dashboard/Authentication.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Parse-Dashboard/Authentication.js b/Parse-Dashboard/Authentication.js index d161704439..62aea443f1 100644 --- a/Parse-Dashboard/Authentication.js +++ b/Parse-Dashboard/Authentication.js @@ -101,10 +101,18 @@ function initialize(app, options) { redirect = originalRedirect.charAt(0) === '/' ? originalRedirect.substring(1) : originalRedirect; } } - return passport.authenticate('local', { - successRedirect: `${self.mountPath}${redirect}`, - failureRedirect: `${self.mountPath}login${originalRedirect ? `?redirect=${originalRedirect}` : ''}`, - failureFlash : true + const failureRedirect = `${self.mountPath}login${originalRedirect ? `?redirect=${originalRedirect}` : ''}`; + return passport.authenticate('local', { failureRedirect, failureFlash: true }, (err, user) => { + if (err) { return next(err); } + if (!user) { return res.redirect(failureRedirect); } + // Regenerate the session to prevent session fixation on successful login + req.session.regenerate((err) => { + if (err) { return next(err); } + req.logIn(user, (err) => { + if (err) { return next(err); } + res.redirect(`${self.mountPath}${redirect}`); + }); + }); })(req, res, next) }, ); From 2de2dcb9efa1d00b43644d44fbddf6b0770c477a Mon Sep 17 00:00:00 2001 From: anupamme Date: Sat, 26 Sep 2026 09:38:29 +0000 Subject: [PATCH 2/2] Address review feedback (2 comments) --- .../AggregationPanel/AggregationPanel.js | 31 +- .../Autocomplete/Autocomplete.react.js | 8 +- .../BrowserCell/BrowserCell.react.js | 26 +- .../BrowserFilter/BrowserFilter.react.js | 133 +- .../BrowserFilter/FilterRow.react.js | 15 +- .../BrowserMenu/BrowserMenu.react.js | 79 +- src/components/BrowserMenu/MenuItem.react.js | 18 +- src/components/Calendar/Calendar.react.js | 4 +- .../CategoryList/CategoryList.react.js | 2 +- src/components/CodeEditor/CodeEditor.react.js | 2 +- .../CodeSnippet/CodeSnippet.react.js | 4 +- .../ColumnConfigurationItem.react.js | 8 +- .../ContextMenu/ContextMenu.react.js | 31 +- .../DataBrowserHeader.react.js | 10 +- .../DateTimePicker/DateTimePicker.react.js | 52 +- src/components/Dropdown/Dropdown.react.js | 16 +- src/components/EmptyState/EmptyState.react.js | 7 +- src/components/Field/Field.react.js | 5 +- src/components/Filter/Filter.react.js | 6 +- src/components/FormModal/FormModal.react.js | 3 +- src/components/GraphPanel/GraphPanel.react.js | 62 +- src/components/JsonEditor/JsonEditor.react.js | 13 +- src/components/Modal/Modal.react.js | 13 +- .../MultiSelect/MultiSelect.react.js | 40 +- .../NonPrintableHighlighter.react.js | 113 +- .../PermissionsDialog.react.js | 16 +- .../PushAudienceDialog.react.js | 2 +- .../PushAudiencesSelector.react.js | 12 +- src/components/Sidebar/Sidebar.react.js | 8 +- .../Sidebar/SidebarSection.react.js | 4 +- .../Sidebar/SidebarSubItem.react.js | 20 +- .../StringEditor/StringEditor.react.js | 3 +- src/components/Toggle/Toggle.react.js | 6 +- src/components/Toolbar/Toolbar.react.js | 67 +- src/components/Tooltip/Tooltip.example.js | 2 +- .../Analytics/Retention/Retention.react.js | 4 +- .../SlowQueries/SlowQueries.react.js | 2 +- src/dashboard/Apps/AppsIndex.react.js | 4 +- src/dashboard/Dashboard.js | 3 +- src/dashboard/Data/Agent/Agent.react.js | 123 +- .../Data/Browser/AddColumnDialog.react.js | 64 +- src/dashboard/Data/Browser/Browser.react.js | 202 +-- .../Data/Browser/BrowserFooter.react.js | 13 +- .../Data/Browser/BrowserTable.react.js | 2 +- .../Data/Browser/BrowserToolbar.react.js | 16 +- .../Data/Browser/DataBrowser.react.js | 598 +++++---- .../Data/Browser/EditRowDialog.react.js | 6 +- src/dashboard/Data/Browser/Editor.react.js | 42 +- .../Data/Browser/ExportDialog.react.js | 2 +- .../Data/Browser/FooterStats.react.js | 6 +- .../Data/Browser/GraphDialog.react.js | 1144 ++++++++++++----- .../Data/Browser/ImportDataDialog.react.js | 83 +- .../Data/Browser/ObjectPickerDialog.react.js | 23 +- .../Data/Browser/ScriptResponseModal.react.js | 71 +- .../Data/Browser/SecureFieldsDialog.react.js | 6 +- .../Data/Browser/SecurityDialog.react.js | 6 +- .../Data/Config/AddArrayEntryDialog.react.js | 7 +- src/dashboard/Data/Config/Config.react.js | 34 +- .../Data/Config/ConfigConflictDiff.react.js | 27 +- .../Data/Config/ConfigDialog.react.js | 224 +++- .../Config/RemoveArrayEntryDialog.react.js | 8 +- .../CustomDashboard/CanvasElement.react.js | 10 +- .../CustomDashboard/CustomDashboard.react.js | 456 ++++--- .../CustomDashboard/LoadCanvasDialog.react.js | 53 +- .../CustomDashboard/SaveCanvasDialog.react.js | 22 +- .../elements/DataTableConfigDialog.react.js | 47 +- .../elements/DataTableElement.react.js | 22 +- .../elements/ExpandModal.react.js | 25 +- .../elements/GraphConfigDialog.react.js | 48 +- .../elements/GraphElement.react.js | 17 +- .../elements/StaticTextConfigDialog.react.js | 11 +- .../elements/StaticTextElement.react.js | 6 +- .../elements/ViewConfigDialog.react.js | 48 +- .../elements/ViewElement.react.js | 54 +- src/dashboard/Data/Jobs/JobEdit.react.js | 6 +- .../Data/Jobs/JobScheduleReminder.react.js | 2 +- src/dashboard/Data/Jobs/RunJobDialog.react.js | 7 +- src/dashboard/Data/Jobs/RunNowButton.react.js | 8 +- .../Data/Migration/MigrationStep.react.js | 8 +- .../Data/Playground/Playground.react.js | 735 ++++++----- .../Views/CloudFunctionInputDialog.react.js | 9 +- .../Data/Views/CreateViewDialog.react.js | 61 +- .../Data/Views/DeleteViewDialog.react.js | 4 +- .../Data/Views/EditViewDialog.react.js | 48 +- .../Data/Views/ViewValueDialog.react.js | 10 +- src/dashboard/Data/Views/Views.react.js | 143 ++- src/dashboard/Data/Webhooks/Webhooks.react.js | 41 +- src/dashboard/Push/PushDetails.react.js | 12 +- src/dashboard/Push/PushNew.react.js | 13 +- .../Settings/CloudConfigSettings.react.js | 118 +- .../DashboardSettings.react.js | 238 ++-- .../Settings/DataBrowserSettings.react.js | 31 +- .../Settings/GeneralSettings.react.js | 5 +- .../KeyboardShortcutsSettings.react.js | 89 +- .../Settings/Security/Security.react.js | 4 +- src/lib/AgentService.js | 9 +- src/lib/AppsManager.js | 2 +- src/lib/CanvasPreferencesManager.js | 28 +- src/lib/Email.js | 6 +- src/lib/FilterPreferencesManager.js | 44 +- src/lib/Filters.js | 11 +- src/lib/FormulaEvaluator.js | 14 +- src/lib/GraphDataUtils.js | 177 ++- src/lib/GraphPreferencesManager.js | 12 +- src/lib/KeyboardShortcutsPreferences.js | 25 +- src/lib/ParseApp.js | 5 +- src/lib/RelatedRecordsUtils.js | 8 +- src/lib/ScriptManager.js | 44 +- src/lib/ScriptUtils.js | 25 +- src/lib/StoragePreferences.js | 2 +- src/lib/StringEscaping.js | 2 +- src/lib/ViewPreferencesManager.js | 43 +- src/lib/generatePath.js | 5 +- src/lib/importData.js | 82 +- src/lib/passwordStrength.js | 2 +- src/lib/tests/AgentAuth.test.js | 25 +- src/lib/tests/Browser.saveFilters.test.js | 60 +- src/lib/tests/BrowserCell.test.js | 25 +- .../tests/BrowserFilter.legacySupport.test.js | 32 +- src/lib/tests/BrowserRow.test.js | 32 +- src/lib/tests/Button.test.js | 12 +- src/lib/tests/ConfigConflictDiff.test.js | 30 +- src/lib/tests/CssModules.test.js | 14 +- src/lib/tests/FormulaEvaluator.test.js | 8 +- src/lib/tests/GraphDataUtils.test.js | 204 ++- src/lib/tests/RemoteAccess.test.js | 13 +- src/lib/tests/ScriptResponseModal.test.js | 99 +- src/lib/tests/SessionStore.test.js | 8 +- src/lib/tests/extractTime.test.js | 6 +- src/lib/tests/importData.test.js | 85 +- src/registerServiceWorker.js | 1 - 131 files changed, 4222 insertions(+), 2950 deletions(-) diff --git a/src/components/AggregationPanel/AggregationPanel.js b/src/components/AggregationPanel/AggregationPanel.js index a05fa44587..ca5997aec5 100644 --- a/src/components/AggregationPanel/AggregationPanel.js +++ b/src/components/AggregationPanel/AggregationPanel.js @@ -132,7 +132,9 @@ const AggregationPanel = ({ const renderSegmentContent = (segment, index) => (
-

{segment.title}

+

+ {segment.title} +

{segment.items.map((item, idx) => { switch (item.type) { @@ -149,7 +151,14 @@ const AggregationPanel = ({ /> ); case 'table': - return ; + return ( + + ); case 'image': return ; case 'video': @@ -219,10 +228,7 @@ const AggregationPanel = ({ {showReloadButton && (
{elapsedSeconds}s -
@@ -240,7 +246,7 @@ const AggregationPanel = ({ ); } - const handleKeyDown = (e) => { + const handleKeyDown = e => { if ((e.ctrlKey || e.metaKey) && e.key === 'c') { const selection = window.getSelection(); if (selection && selection.toString().length > 0) { @@ -253,21 +259,14 @@ const AggregationPanel = ({ }; return ( -
+
{isLoadingInfoPanel ? (
{showReloadButton && onReload && (
{elapsedSeconds}s -
diff --git a/src/components/Autocomplete/Autocomplete.react.js b/src/components/Autocomplete/Autocomplete.react.js index 705d78dab4..b864d0d257 100644 --- a/src/components/Autocomplete/Autocomplete.react.js +++ b/src/components/Autocomplete/Autocomplete.react.js @@ -47,11 +47,11 @@ export default class Autocomplete extends Component { }; this.state = { - valueFromSuggestion: props.strict ? props.value ?? props.suggestions[0] : '', + valueFromSuggestion: props.strict ? (props.value ?? props.suggestions[0]) : '', activeSuggestion: 0, filteredSuggestions: [], showSuggestions: false, - userInput: props.strict ? props.value ?? props.suggestions[0] : '', + userInput: props.strict ? (props.value ?? props.suggestions[0]) : '', label: props.label, position: null, }; @@ -95,8 +95,8 @@ export default class Autocomplete extends Component { const filteredSuggestions = buildSuggestions ? buildSuggestions(userInput) : suggestions.filter( - suggestion => suggestion.toLowerCase().indexOf(userInput.toLowerCase()) > -1 - ); + suggestion => suggestion.toLowerCase().indexOf(userInput.toLowerCase()) > -1 + ); return filteredSuggestions; } diff --git a/src/components/BrowserCell/BrowserCell.react.js b/src/components/BrowserCell/BrowserCell.react.js index 74cbe7ca04..4538f90ee9 100644 --- a/src/components/BrowserCell/BrowserCell.react.js +++ b/src/components/BrowserCell/BrowserCell.react.js @@ -322,10 +322,15 @@ export default class BrowserCell extends Component { relatedObjectsContextMenuOption && contextMenuOptions.push(relatedObjectsContextMenuOption); const relatedTextFieldsContextMenuOption = this.getRelatedTextFieldsContextMenuOption(); - !relatedObjectsContextMenuOption && relatedTextFieldsContextMenuOption && contextMenuOptions.push(relatedTextFieldsContextMenuOption); + !relatedObjectsContextMenuOption && + relatedTextFieldsContextMenuOption && + contextMenuOptions.push(relatedTextFieldsContextMenuOption); const relatedNumberFieldsContextMenuOption = this.getRelatedNumberFieldsContextMenuOption(); - !relatedObjectsContextMenuOption && !relatedTextFieldsContextMenuOption && relatedNumberFieldsContextMenuOption && contextMenuOptions.push(relatedNumberFieldsContextMenuOption); + !relatedObjectsContextMenuOption && + !relatedTextFieldsContextMenuOption && + relatedNumberFieldsContextMenuOption && + contextMenuOptions.push(relatedNumberFieldsContextMenuOption); // Group 2: Filter const addFilterContextMenuOption = this.getAddFilterContextMenuOption(constraints); @@ -424,8 +429,8 @@ export default class BrowserCell extends Component { copyableValue.length < 30 ? copyableValue : `${copyableValue.substr(0, 20)}...${copyableValue.substr( - copyableValue.length - 7 - )}`; + copyableValue.length - 7 + )}`; const text = `${this.props.field} ${definition.name}${ definition.comparable ? ' ' + value : '' }`; @@ -602,7 +607,12 @@ export default class BrowserCell extends Component { const cellValue = this.copyableValue !== undefined ? String(this.copyableValue) : ''; // Don't show for empty or special values - if (!cellValue || cellValue === '(undefined)' || cellValue === '(null)' || cellValue === '(hidden)') { + if ( + !cellValue || + cellValue === '(undefined)' || + cellValue === '(null)' || + cellValue === '(hidden)' + ) { return; } @@ -631,9 +641,9 @@ export default class BrowserCell extends Component { compareTo = value.__type ? value : { - __type: 'Date', - iso: value, - }; + __type: 'Date', + iso: value, + }; break; default: diff --git a/src/components/BrowserFilter/BrowserFilter.react.js b/src/components/BrowserFilter/BrowserFilter.react.js index 3e33b7d21c..b78ac7cc68 100644 --- a/src/components/BrowserFilter/BrowserFilter.react.js +++ b/src/components/BrowserFilter/BrowserFilter.react.js @@ -199,8 +199,8 @@ export default class BrowserFilter extends React.Component { let hasRelativeDates = false; try { const filterData = JSON.parse(savedFilter.filter); - hasRelativeDates = filterData.some(filter => - filter.compareTo && filter.compareTo.__type === 'RelativeDate' + hasRelativeDates = filterData.some( + filter => filter.compareTo && filter.compareTo.__type === 'RelativeDate' ); } catch (error) { // Log parsing errors for debugging @@ -212,7 +212,7 @@ export default class BrowserFilter extends React.Component { id: savedFilter.id, name: savedFilter.name, isApplied: true, - hasRelativeDates: hasRelativeDates + hasRelativeDates: hasRelativeDates, }; } } @@ -230,7 +230,7 @@ export default class BrowserFilter extends React.Component { name: '', isApplied: false, hasRelativeDates: false, - isLegacy: false + isLegacy: false, }; } @@ -267,8 +267,8 @@ export default class BrowserFilter extends React.Component { let hasRelativeDates = false; try { const filterData = JSON.parse(matchingFilter.filter); - hasRelativeDates = filterData.some(filter => - filter.compareTo && filter.compareTo.__type === 'RelativeDate' + hasRelativeDates = filterData.some( + filter => filter.compareTo && filter.compareTo.__type === 'RelativeDate' ); } catch (error) { console.warn('Failed to parse saved filter:', error); @@ -280,7 +280,7 @@ export default class BrowserFilter extends React.Component { name: matchingFilter.name, isApplied: true, hasRelativeDates: hasRelativeDates, - isLegacy: !matchingFilter.id // Mark as legacy if no id + isLegacy: !matchingFilter.id, // Mark as legacy if no id }; } } @@ -290,7 +290,7 @@ export default class BrowserFilter extends React.Component { name: '', isApplied: false, hasRelativeDates: false, - isLegacy: false + isLegacy: false, }; } @@ -310,10 +310,12 @@ export default class BrowserFilter extends React.Component { if (savedFilter) { try { const filterData = JSON.parse(savedFilter.filter); - return new List(filterData.map(filter => { - const processedFilter = { ...filter, class: filter.class || urlClassName }; - return new ImmutableMap(processedFilter); - })); + return new List( + filterData.map(filter => { + const processedFilter = { ...filter, class: filter.class || urlClassName }; + return new ImmutableMap(processedFilter); + }) + ); } catch (error) { console.warn('Failed to parse saved filter:', error); } @@ -324,10 +326,12 @@ export default class BrowserFilter extends React.Component { if (filtersParam) { try { const queryFilters = JSON.parse(filtersParam); - return new List(queryFilters.map(filter => { - const processedFilter = { ...filter, class: filter.class || urlClassName }; - return new ImmutableMap(processedFilter); - })); + return new List( + queryFilters.map(filter => { + const processedFilter = { ...filter, class: filter.class || urlClassName }; + return new ImmutableMap(processedFilter); + }) + ); } catch (error) { console.warn('Failed to parse URL filters:', error); } @@ -458,10 +462,12 @@ export default class BrowserFilter extends React.Component { const originalCompareTo = originalFilter.get('compareTo'); // Check all properties for equality - if (currentClass !== originalClass || - currentField !== originalField || - currentConstraint !== originalConstraint || - currentCompareTo !== originalCompareTo) { + if ( + currentClass !== originalClass || + currentField !== originalField || + currentConstraint !== originalConstraint || + currentCompareTo !== originalCompareTo + ) { return true; } } @@ -504,7 +510,7 @@ export default class BrowserFilter extends React.Component { return filter; }); return result; - } // Helper method to convert RelativeDate objects to Parse Date format for saving + } // Helper method to convert RelativeDate objects to Parse Date format for saving convertRelativeDatesToParseFormat(filters) { return filters.map(filter => { const compareTo = filter.get('compareTo'); @@ -597,7 +603,7 @@ export default class BrowserFilter extends React.Component { // Clear the filter name so user can enter a new name this.setState({ name: '', - originalFilterName: '' + originalFilterName: '', }); } @@ -702,7 +708,7 @@ export default class BrowserFilter extends React.Component { const timeDiff = compareTo.getTime() - now.getTime(); const relativeDate = { __type: 'RelativeDate', - value: Math.round(timeDiff / 1000) // Convert milliseconds to seconds + value: Math.round(timeDiff / 1000), // Convert milliseconds to seconds }; return filter.set('compareTo', relativeDate); } else if (compareTo && compareTo.__type === 'Date') { @@ -712,7 +718,7 @@ export default class BrowserFilter extends React.Component { const timeDiff = parseDateObj.getTime() - now.getTime(); const relativeDate = { __type: 'RelativeDate', - value: Math.round(timeDiff / 1000) // Convert milliseconds to seconds + value: Math.round(timeDiff / 1000), // Convert milliseconds to seconds }; return filter.set('compareTo', relativeDate); } @@ -730,7 +736,12 @@ export default class BrowserFilter extends React.Component { filterId = `legacy:${currentFilterInfo.name}`; } - const savedFilterId = await this.props.onSaveFilter(formatted, this.state.name, this.state.relativeDates, filterId); + const savedFilterId = await this.props.onSaveFilter( + formatted, + this.state.name, + this.state.relativeDates, + filterId + ); // Only close the dialog if we're not in edit mode (showMore) if (!this.state.showMore) { @@ -782,7 +793,12 @@ export default class BrowserFilter extends React.Component { const hasDateState = this.state.filters.some(filter => { const compareTo = filter.get('compareTo'); - return compareTo && (compareTo instanceof Date || compareTo.__type === 'Date' || compareTo.__type === 'RelativeDate'); + return ( + compareTo && + (compareTo instanceof Date || + compareTo.__type === 'Date' || + compareTo.__type === 'RelativeDate') + ); }); popover = ( @@ -877,16 +893,8 @@ export default class BrowserFilter extends React.Component {
{this.state.showMore && (
- this.toggleMore()} - > - + this.toggleMore()}> +
{ - if (this.state.name && (this.state.name !== this.state.originalFilterName || this.hasFilterContentChanged()) && !this.isFilterNameExists(this.state.name)) { + if ( + this.state.name && + (this.state.name !== this.state.originalFilterName || + this.hasFilterContentChanged()) && + !this.isFilterNameExists(this.state.name) + ) { this.save(); } }} @@ -909,7 +929,14 @@ export default class BrowserFilter extends React.Component { name="check" width={20} height={20} - fill={this.state.name && (this.state.name !== this.state.originalFilterName || this.hasFilterContentChanged()) && !this.isFilterNameExists(this.state.name) ? '#00db7c' : 'white'} + fill={ + this.state.name && + (this.state.name !== this.state.originalFilterName || + this.hasFilterContentChanged()) && + !this.isFilterNameExists(this.state.name) + ? '#00db7c' + : 'white' + } /> {this.isCurrentFilterSaved() && ( @@ -918,23 +945,13 @@ export default class BrowserFilter extends React.Component { className={styles.iconButton} onClick={() => this.copyCurrentFilter()} > - + this.setState({ confirmDelete: true })} > - + )} @@ -943,16 +960,8 @@ export default class BrowserFilter extends React.Component {
{!this.state.showMore && ( <> - this.toggleMore()} - > - + this.toggleMore()}> +

- Graph is a preview feature. Future versions may introduce breaking changes without announcement. + Graph is a preview feature. Future versions may introduce breaking changes without + announcement.

)} @@ -583,7 +588,7 @@ const GraphPanel = ({ const hasActiveGraph = !!graphConfig; const showDropdown = hasActiveGraph && availableGraphs && availableGraphs.length > 0; - const handleGraphSelect = (graph) => { + const handleGraphSelect = graph => { if (onGraphSelect) { onGraphSelect(graph); } @@ -615,25 +620,18 @@ const GraphPanel = ({ {showGraphDropdown && (
- {availableGraphs.map((graph) => ( + {availableGraphs.map(graph => ( ))}
- @@ -685,9 +683,7 @@ const GraphPanel = ({

Loading graph data...

) : ( -
- {renderChart()} -
+
{renderChart()}
)}
@@ -695,9 +691,9 @@ const GraphPanel = ({
Data points: {data?.length || 0} - {graphConfig.maxDataPoints && data?.length > graphConfig.maxDataPoints && - ` (showing first ${graphConfig.maxDataPoints})` - } + {graphConfig.maxDataPoints && + data?.length > graphConfig.maxDataPoints && + ` (showing first ${graphConfig.maxDataPoints})`}
)} diff --git a/src/components/JsonEditor/JsonEditor.react.js b/src/components/JsonEditor/JsonEditor.react.js index dbd5141c11..d3b0230bb3 100644 --- a/src/components/JsonEditor/JsonEditor.react.js +++ b/src/components/JsonEditor/JsonEditor.react.js @@ -62,14 +62,14 @@ export default class JsonEditor extends React.Component { this.syncScroll(); }; - handleChange = (e) => { + handleChange = e => { const { onChange } = this.props; if (onChange) { onChange(e.target.value); } }; - handleKeyDown = (e) => { + handleKeyDown = e => { // Enter key - auto-indent to match current line // Skip if Cmd/Ctrl is pressed (let it bubble up for modal confirm) if (e.key === 'Enter' && !e.metaKey && !e.ctrlKey) { @@ -105,10 +105,7 @@ export default class JsonEditor extends React.Component { return Prism.highlight(value, Prism.languages.json, 'json'); } catch { // If highlighting fails, return escaped HTML - return value - .replace(/&/g, '&') - .replace(//g, '>'); + return value.replace(/&/g, '&').replace(//g, '>'); } } @@ -130,7 +127,7 @@ export default class JsonEditor extends React.Component { }; // Validate hex color: only accept #RGB or #RRGGBB (case-insensitive) - const isValidHexColor = (color) => { + const isValidHexColor = color => { if (typeof color !== 'string') { return false; } @@ -138,7 +135,7 @@ export default class JsonEditor extends React.Component { }; // Get sanitized color for a token, falling back to default if invalid - const getSafeColor = (token) => { + const getSafeColor = token => { const color = syntaxColors[token]; return isValidHexColor(color) ? color : defaults[token]; }; diff --git a/src/components/Modal/Modal.react.js b/src/components/Modal/Modal.react.js index aacb6c15d8..ac2fb6bddb 100644 --- a/src/components/Modal/Modal.react.js +++ b/src/components/Modal/Modal.react.js @@ -90,7 +90,7 @@ const Modal = ({ return; } - const handleKeyDown = (e) => { + const handleKeyDown = e => { // Handle Escape to close modal if (e.key === 'Escape') { e.stopImmediatePropagation(); @@ -133,8 +133,15 @@ const Modal = ({ }; // Handle Command+Enter to trigger primary button (runs in bubble phase after component handlers) - const handleEnterKey = (e) => { - if (e.key === 'Enter' && e.metaKey && !e.defaultPrevented && !disabled && !progress && onConfirm) { + const handleEnterKey = e => { + if ( + e.key === 'Enter' && + e.metaKey && + !e.defaultPrevented && + !disabled && + !progress && + onConfirm + ) { e.preventDefault(); onConfirm(); } diff --git a/src/components/MultiSelect/MultiSelect.react.js b/src/components/MultiSelect/MultiSelect.react.js index f742c6c6d2..13a01f2e32 100644 --- a/src/components/MultiSelect/MultiSelect.react.js +++ b/src/components/MultiSelect/MultiSelect.react.js @@ -136,24 +136,24 @@ export default class MultiSelect extends React.Component { } else { content = this.props.chips ? selection.map((child, index) => { - let item; - if (Array.isArray(this.props.value)) { - item = this.props.value[index]; - } - return ( - { - if (removed) { - this.select(removed); - } - }} - > - {child} - - ); - }) + let item; + if (Array.isArray(this.props.value)) { + item = this.props.value[index]; + } + return ( + { + if (removed) { + this.select(removed); + } + }} + > + {child} + + ); + }) : this.props.formatSelection ? this.props.formatSelection(selection) : stringList(selection, this.props.endDelineator); @@ -186,6 +186,8 @@ MultiSelect.propTypes = { endDelineator: PropTypes.string.describe('End delineator to separate last selected option.'), dense: PropTypes.bool.describe('Mini variant - less height'), chips: PropTypes.bool.describe('Display chip for every selected item'), - formatSelection: PropTypes.func.describe('Custom function to format the display text. Receives array of selected labels.'), + formatSelection: PropTypes.func.describe( + 'Custom function to format the display text. Receives array of selected labels.' + ), disabled: PropTypes.bool.describe('When true, prevents the dropdown from opening.'), }; diff --git a/src/components/NonPrintableHighlighter/NonPrintableHighlighter.react.js b/src/components/NonPrintableHighlighter/NonPrintableHighlighter.react.js index abf9c9a999..d5f045bb23 100644 --- a/src/components/NonPrintableHighlighter/NonPrintableHighlighter.react.js +++ b/src/components/NonPrintableHighlighter/NonPrintableHighlighter.react.js @@ -130,7 +130,6 @@ const NON_PRINTABLE_REGEX = new RegExp( // Regex for non-alphanumeric characters (anything not a-z, A-Z, 0-9) const NON_ALPHANUMERIC_REGEX = /[^a-zA-Z0-9]/g; - /** * Check if a string contains non-printable characters */ @@ -151,7 +150,10 @@ function stripQuotes(str) { return str; } // Check for matching single or double quotes (minimum length 2 for valid quoted string) - if (str.length >= 2 && ((str.startsWith('"') && str.endsWith('"')) || (str.startsWith('\'') && str.endsWith('\'')))) { + if ( + str.length >= 2 && + ((str.startsWith('"') && str.endsWith('"')) || (str.startsWith("'") && str.endsWith("'"))) + ) { return str.slice(1, -1); } return str; @@ -337,7 +339,9 @@ export function getNonPrintableChars(str) { const chars = []; for (const [char, positions] of positionMap) { - const label = NON_PRINTABLE_CHARS[char] || `U+${char.charCodeAt(0).toString(16).toUpperCase().padStart(4, '0')}`; + const label = + NON_PRINTABLE_CHARS[char] || + `U+${char.charCodeAt(0).toString(16).toUpperCase().padStart(4, '0')}`; chars.push({ char, label, @@ -535,38 +539,41 @@ export default class NonPrintableHighlighter extends React.Component { } render() { - const { value, children, isJson, detectNonPrintable, detectNonAlphanumeric, detectRegex } = this.props; + const { value, children, isJson, detectNonPrintable, detectNonAlphanumeric, detectRegex } = + this.props; const { totalCount, chars } = detectNonPrintable - ? (isJson ? getNonPrintableCharsFromJson(value) : getNonPrintableChars(value)) + ? isJson + ? getNonPrintableCharsFromJson(value) + : getNonPrintableChars(value) : { totalCount: 0, chars: [] }; const hasNonPrintable = totalCount > 0; // Get non-alphanumeric characters if detection is enabled const nonAlphanumericResult = detectNonAlphanumeric - ? (isJson ? getNonAlphanumericCharsFromJson(value) : getNonAlphanumericChars(value)) + ? isJson + ? getNonAlphanumericCharsFromJson(value) + : getNonAlphanumericChars(value) : { totalCount: 0, chars: [] }; const hasNonAlphanumeric = nonAlphanumericResult.totalCount > 0; // Get regex validation if detection is enabled const regexResult = detectRegex - ? (isJson + ? isJson ? getRegexValidationFromJson(value) - : (value && typeof value === 'string' + : value && typeof value === 'string' ? { results: [{ path: null, value, ...getRegexValidation(value) }] } - : { results: [] })) + : { results: [] } : { results: [] }; const hasRegexResults = regexResult.results.length > 0; const requiredFlags = hasRegexResults - ? [...new Set(regexResult.results - .filter(r => r.isValid && r.requiredFlag) - .map(r => r.requiredFlag))] + ? [ + ...new Set( + regexResult.results.filter(r => r.isValid && r.requiredFlag).map(r => r.requiredFlag) + ), + ] : []; - const invalidCount = hasRegexResults - ? regexResult.results.filter(r => !r.isValid).length - : 0; - const regexNotes = [ - ...(requiredFlags.length > 0 ? [`${requiredFlags.join(', ')}`] : []), - ]; + const invalidCount = hasRegexResults ? regexResult.results.filter(r => !r.isValid).length : 0; + const regexNotes = [...(requiredFlags.length > 0 ? [`${requiredFlags.join(', ')}`] : [])]; return (
@@ -593,12 +600,18 @@ export default class NonPrintableHighlighter extends React.Component { {count > 1 && ×{count}} {positions && ( - @ {positions.length <= 5 ? positions.join(', ') : `${positions.slice(0, 5).join(', ')}...`} + @{' '} + {positions.length <= 5 + ? positions.join(', ') + : `${positions.slice(0, 5).join(', ')}...`} )} {locations && ( - in {locations.length <= 3 ? locations.join(', ') : `${locations.slice(0, 3).join(', ')}...`} + in{' '} + {locations.length <= 3 + ? locations.join(', ') + : `${locations.slice(0, 3).join(', ')}...`} )}
@@ -612,34 +625,47 @@ export default class NonPrintableHighlighter extends React.Component {
this.setState({ showNonAlphanumericDetails: !this.state.showNonAlphanumericDetails })} + onClick={() => + this.setState({ + showNonAlphanumericDetails: !this.state.showNonAlphanumericDetails, + }) + } title="Click for details" > ℹ - {nonAlphanumericResult.totalCount} non-alphanumeric character{nonAlphanumericResult.totalCount > 1 ? 's' : ''} + {nonAlphanumericResult.totalCount} non-alphanumeric character + {nonAlphanumericResult.totalCount > 1 ? 's' : ''}
{this.state.showNonAlphanumericDetails && (
- {nonAlphanumericResult.chars.map(({ label, code, count, positions, locations }, i) => ( -
- {label} - {code} - {count > 1 && ×{count}} - {positions && ( - - @ {positions.length <= 5 ? positions.join(', ') : `${positions.slice(0, 5).join(', ')}...`} - - )} - {locations && ( - - in {locations.length <= 3 ? locations.join(', ') : `${locations.slice(0, 3).join(', ')}...`} - - )} -
- ))} + {nonAlphanumericResult.chars.map( + ({ label, code, count, positions, locations }, i) => ( +
+ {label} + {code} + {count > 1 && ×{count}} + {positions && ( + + @{' '} + {positions.length <= 5 + ? positions.join(', ') + : `${positions.slice(0, 5).join(', ')}...`} + + )} + {locations && ( + + in{' '} + {locations.length <= 3 + ? locations.join(', ') + : `${locations.slice(0, 3).join(', ')}...`} + + )} +
+ ) + )}
)} @@ -657,8 +683,9 @@ export default class NonPrintableHighlighter extends React.Component { 0 ? styles.regexSummaryInvalid : undefined}> {isJson ? `${regexResult.results.filter(r => r.isValid).length}/${regexResult.results.length} valid regex pattern${regexResult.results.length > 1 ? 's' : ''}` - : (regexResult.results[0].isValid ? 'Valid regex pattern' : 'Invalid regex pattern') - } + : regexResult.results[0].isValid + ? 'Valid regex pattern' + : 'Invalid regex pattern'} {regexNotes.length > 0 && ` (${regexNotes.join(', ')})`} @@ -675,9 +702,7 @@ export default class NonPrintableHighlighter extends React.Component { {isValid && requiredFlag && ( {requiredFlag} )} - {!isValid && error && ( - {error} - )} + {!isValid && error && {error}}
))}
diff --git a/src/components/PermissionsDialog/PermissionsDialog.react.js b/src/components/PermissionsDialog/PermissionsDialog.react.js index 5187116b4a..b6ab60fccc 100644 --- a/src/components/PermissionsDialog/PermissionsDialog.react.js +++ b/src/components/PermissionsDialog/PermissionsDialog.react.js @@ -565,17 +565,17 @@ export default class PermissionsDialog extends React.Component { // The double check is necessary because the permissions object seems to be empty when accessing the CLP section // if the class was recently created. - (pointerPermsSubset.get = (permissions.get && permissions.get.pointerFields) || []), - (pointerPermsSubset.find = (permissions.find && permissions.find.pointerFields) || []), - (pointerPermsSubset.count = (permissions.count && permissions.count.pointerFields) || []), - (pointerPermsSubset.create = + ((pointerPermsSubset.get = (permissions.get && permissions.get.pointerFields) || []), + (pointerPermsSubset.find = (permissions.find && permissions.find.pointerFields) || []), + (pointerPermsSubset.count = (permissions.count && permissions.count.pointerFields) || []), + (pointerPermsSubset.create = (permissions.create && permissions.create.pointerFields) || []), - (pointerPermsSubset.update = + (pointerPermsSubset.update = (permissions.update && permissions.update.pointerFields) || []), - (pointerPermsSubset.delete = + (pointerPermsSubset.delete = (permissions.delete && permissions.delete.pointerFields) || []), - (pointerPermsSubset.addField = - (permissions.addField && permissions.addField.pointerFields) || []); + (pointerPermsSubset.addField = + (permissions.addField && permissions.addField.pointerFields) || [])); } const pointerPerms = {}; diff --git a/src/components/PushAudienceDialog/PushAudienceDialog.react.js b/src/components/PushAudienceDialog/PushAudienceDialog.react.js index bd13bc2ce5..07f8a31097 100644 --- a/src/components/PushAudienceDialog/PushAudienceDialog.react.js +++ b/src/components/PushAudienceDialog/PushAudienceDialog.react.js @@ -307,7 +307,7 @@ export default class PushAudienceDialog extends React.Component { 0) || - (this.state.errorMessage && this.state.errorMessage.length > 0) + (this.state.errorMessage && this.state.errorMessage.length > 0) )} color="red" > diff --git a/src/components/PushAudiencesSelector/PushAudiencesSelector.react.js b/src/components/PushAudiencesSelector/PushAudiencesSelector.react.js index 7eb2761dff..5c657b6a70 100644 --- a/src/components/PushAudiencesSelector/PushAudiencesSelector.react.js +++ b/src/components/PushAudiencesSelector/PushAudiencesSelector.react.js @@ -21,12 +21,12 @@ const PushAudiencesOptions = ({ current, onChange, onEditAudience, schema, audie const queryOrFilters = objectId === PushConstants.NEW_SEGMENT_ID ? filters.push( - fromJS({ - field: 'deviceType', - constraint: 'containedIn', - compareTo: query.deviceType['$in'], - }) - ) + fromJS({ + field: 'deviceType', + constraint: 'containedIn', + compareTo: query.deviceType['$in'], + }) + ) : query; return ( { - if (!isInsidePopover(e.relatedTarget)) { - setAppsMenuOpen(false); - setCollapsed(true); + if (!isInsidePopover(e.relatedTarget)) { + setAppsMenuOpen(false); + setCollapsed(true); + } } - } : undefined } > diff --git a/src/components/Sidebar/SidebarSection.react.js b/src/components/Sidebar/SidebarSection.react.js index 781eef835c..bde73725f7 100644 --- a/src/components/Sidebar/SidebarSection.react.js +++ b/src/components/Sidebar/SidebarSection.react.js @@ -34,7 +34,7 @@ const SidebarSection = ({ return (
{iconContent} @@ -46,7 +46,7 @@ const SidebarSection = ({
{active ? (
{iconContent} diff --git a/src/components/Sidebar/SidebarSubItem.react.js b/src/components/Sidebar/SidebarSubItem.react.js index 6de20919d5..360428b79b 100644 --- a/src/components/Sidebar/SidebarSubItem.react.js +++ b/src/components/Sidebar/SidebarSubItem.react.js @@ -16,7 +16,15 @@ const SidebarSubItem = ({ active, name, action, link, children, icon }) => {
{name} - {icon && } + {icon && ( + + )} {action ? action.renderButton() : null}
{children}
@@ -28,7 +36,15 @@ const SidebarSubItem = ({ active, name, action, link, children, icon }) => {
{name} - {icon && } + {icon && ( + + )}
); diff --git a/src/components/StringEditor/StringEditor.react.js b/src/components/StringEditor/StringEditor.react.js index 1258919851..403691d1b0 100644 --- a/src/components/StringEditor/StringEditor.react.js +++ b/src/components/StringEditor/StringEditor.react.js @@ -64,7 +64,8 @@ export default class StringEditor extends React.Component { } handleContextMenu(e) { - const { setContextMenu, arrayConfigParams, onAddToArrayConfig, getRelatedRecordsMenuItem } = this.props; + const { setContextMenu, arrayConfigParams, onAddToArrayConfig, getRelatedRecordsMenuItem } = + this.props; // Only show custom context menu when Alt key is held if (!e.altKey) { diff --git a/src/components/Toggle/Toggle.react.js b/src/components/Toggle/Toggle.react.js index f1bacdd3ea..25f646a70c 100644 --- a/src/components/Toggle/Toggle.react.js +++ b/src/components/Toggle/Toggle.react.js @@ -131,7 +131,11 @@ export default class Toggle extends React.Component { {labelLeft} )} - + {labelRight && ( {labelRight} diff --git a/src/components/Toolbar/Toolbar.react.js b/src/components/Toolbar/Toolbar.react.js index eb0b8053cd..01a8f89c9d 100644 --- a/src/components/Toolbar/Toolbar.react.js +++ b/src/components/Toolbar/Toolbar.react.js @@ -38,42 +38,45 @@ const Toolbar = props => { {props.classwiseCloudFunctions && props.classwiseCloudFunctions[`${props.appId}${props.appName}`] && props.classwiseCloudFunctions[`${props.appId}${props.appName}`][props.className] && ( -
- {props.isAutoScrolling && ( - - )} - {props.isPanelVisible && ( - <> - {props.panelCount > 1 && ( - - )} - - - )} - + )} + )} - -
- )} + +
+ )}
); }; diff --git a/src/components/Tooltip/Tooltip.example.js b/src/components/Tooltip/Tooltip.example.js index fef66a4264..446173aff2 100644 --- a/src/components/Tooltip/Tooltip.example.js +++ b/src/components/Tooltip/Tooltip.example.js @@ -28,7 +28,7 @@ export const demos = [ ), }, { - name: 'Tooltip doesn\'t break layout', + name: "Tooltip doesn't break layout", render: () => (
(window.location = 'https://parse.com/apps/quickstart')} diff --git a/src/dashboard/Analytics/SlowQueries/SlowQueries.react.js b/src/dashboard/Analytics/SlowQueries/SlowQueries.react.js index c7c16931a3..bc917984d4 100644 --- a/src/dashboard/Analytics/SlowQueries/SlowQueries.react.js +++ b/src/dashboard/Analytics/SlowQueries/SlowQueries.react.js @@ -234,7 +234,7 @@ class SlowQueries extends TableView { return ( (window.location = 'http://docs.parseplatform.org/rest/guide/#queries')} diff --git a/src/dashboard/Apps/AppsIndex.react.js b/src/dashboard/Apps/AppsIndex.react.js index 23bfba9250..2b4983a70c 100644 --- a/src/dashboard/Apps/AppsIndex.react.js +++ b/src/dashboard/Apps/AppsIndex.react.js @@ -104,9 +104,7 @@ const AppCard = ({ app, icon }) => {
{!app.serverInfo.error && app.serverInfo.versionWarning && ( -
- ⚠️ {app.serverInfo.versionWarning} -
+
⚠️ {app.serverInfo.versionWarning}
)} ); diff --git a/src/dashboard/Dashboard.js b/src/dashboard/Dashboard.js index 22fea032ba..0eab1ad003 100644 --- a/src/dashboard/Dashboard.js +++ b/src/dashboard/Dashboard.js @@ -153,8 +153,7 @@ export default class Dashboard extends React.Component { if (serverVersion && serverVersion !== 'unknown' && supportedVersion) { const cleanedVersion = semver.valid(semver.coerce(serverVersion)); if (cleanedVersion && !semver.satisfies(cleanedVersion, supportedVersion)) { - app.serverInfo.versionWarning = - `Parse Server ${serverVersion} is not officially supported by this version of Parse Dashboard. You may encounter issues or reduced functionality. Supported Parse Server versions are ${supportedVersion}. Either upgrade Parse Server, or downgrade Parse Dashboard to a compatible version.`; + app.serverInfo.versionWarning = `Parse Server ${serverVersion} is not officially supported by this version of Parse Dashboard. You may encounter issues or reduced functionality. Supported Parse Server versions are ${supportedVersion}. Either upgrade Parse Server, or downgrade Parse Dashboard to a compatible version.`; } } diff --git a/src/dashboard/Data/Agent/Agent.react.js b/src/dashboard/Data/Agent/Agent.react.js index 7f5473f403..bbd8b2b6bb 100644 --- a/src/dashboard/Data/Agent/Agent.react.js +++ b/src/dashboard/Data/Agent/Agent.react.js @@ -53,13 +53,15 @@ class Agent extends DashboardView { getStoredPermissions() { try { const stored = localStorage.getItem('agentPermissions'); - return stored ? JSON.parse(stored) : { - deleteObject: false, - deleteClass: false, - updateObject: false, - createObject: false, - createClass: false, - }; + return stored + ? JSON.parse(stored) + : { + deleteObject: false, + deleteClass: false, + updateObject: false, + createObject: false, + createClass: false, + }; } catch (error) { console.warn('Failed to parse stored permissions, using defaults:', error); return { @@ -76,7 +78,7 @@ class Agent extends DashboardView { this.setState(prevState => { const newPermissions = { ...prevState.permissions, - [operation]: enabled + [operation]: enabled, }; // Save to localStorage immediately @@ -84,28 +86,36 @@ class Agent extends DashboardView { return { permissions: newPermissions, - permissionsKey: prevState.permissionsKey + 1 + permissionsKey: prevState.permissionsKey + 1, }; }); - } + }; getStoredChatState() { try { const appSlug = this.context ? this.context.slug : null; - if (!appSlug) {return null;} + if (!appSlug) { + return null; + } const stored = localStorage.getItem(`agentChat_${appSlug}`); - if (!stored) {return null;} + if (!stored) { + return null; + } const parsedState = JSON.parse(stored); // Validate the structure - if (!parsedState || typeof parsedState !== 'object') {return null;} - if (!Array.isArray(parsedState.messages)) {return null;} + if (!parsedState || typeof parsedState !== 'object') { + return null; + } + if (!Array.isArray(parsedState.messages)) { + return null; + } // Check if the data is too old (optional: 24 hours expiry) const ONE_DAY = 24 * 60 * 60 * 1000; - if (parsedState.timestamp && (Date.now() - parsedState.timestamp > ONE_DAY)) { + if (parsedState.timestamp && Date.now() - parsedState.timestamp > ONE_DAY) { localStorage.removeItem(`agentChat_${appSlug}`); return null; } @@ -120,12 +130,14 @@ class Agent extends DashboardView { saveChatState() { try { const appSlug = this.context ? this.context.slug : null; - if (!appSlug) {return;} + if (!appSlug) { + return; + } const chatState = { messages: this.state.messages, conversationId: this.state.conversationId, - timestamp: Date.now() + timestamp: Date.now(), }; localStorage.setItem(`agentChat_${appSlug}`, JSON.stringify(chatState)); } catch (error) { @@ -151,7 +163,7 @@ class Agent extends DashboardView { // Convert timestamp strings back to Date objects const messagesWithDateTimestamps = savedChatState.messages.map(message => ({ ...message, - timestamp: new Date(message.timestamp) + timestamp: new Date(message.timestamp), })); this.setState({ @@ -173,14 +185,18 @@ class Agent extends DashboardView { } // Save chat state when messages change - if (prevState.messages.length !== this.state.messages.length || - prevState.conversationId !== this.state.conversationId) { + if ( + prevState.messages.length !== this.state.messages.length || + prevState.conversationId !== this.state.conversationId + ) { this.saveChatState(); } // Auto-scroll to bottom when new messages are added or loading state changes - if (prevState.messages.length !== this.state.messages.length || - prevState.isLoading !== this.state.isLoading) { + if ( + prevState.messages.length !== this.state.messages.length || + prevState.isLoading !== this.state.isLoading + ) { // Use requestAnimationFrame and setTimeout to ensure DOM has updated requestAnimationFrame(() => { setTimeout(() => this.scrollToBottom(), 50); @@ -212,7 +228,7 @@ class Agent extends DashboardView { // Force smooth scrolling behavior element.scrollTo({ top: element.scrollHeight, - behavior: 'smooth' + behavior: 'smooth', }); } } @@ -239,19 +255,19 @@ class Agent extends DashboardView { } } - handleInputChange = (event) => { + handleInputChange = event => { this.setState({ inputValue: event.target.value }); - } + }; - handleExampleClick = (exampleText) => { + handleExampleClick = exampleText => { this.setState({ inputValue: exampleText }, () => { // Auto-submit the example query const event = { preventDefault: () => {} }; this.handleSubmit(event); }); - } + }; - handleSubmit = async (event) => { + handleSubmit = async event => { event.preventDefault(); const { inputValue, selectedModel, messages } = this.state; const { agentConfig } = this.props; @@ -288,7 +304,8 @@ class Agent extends DashboardView { const warningMessage = { id: Date.now() - 1, type: 'warning', - content: 'The AI agent has full access to your database using the master key. It can read, modify, and delete any data. This feature is highly recommended for development environments only. Always back up important data before using the AI agent. Use the permissions menu to restrict operations.', + content: + 'The AI agent has full access to your database using the master key. It can read, modify, and delete any data. This feature is highly recommended for development environments only. Always back up important data before using the AI agent. Use the permissions menu to restrict operations.', timestamp: new Date(), }; messagesToAdd.push(warningMessage); @@ -340,7 +357,6 @@ class Agent extends DashboardView { isLoading: false, conversationId: result.conversationId, // Update conversation ID })); - } catch (error) { console.error('Agent API error:', error); @@ -373,7 +389,7 @@ class Agent extends DashboardView { this.chatInputRef.current.focus(); } }, 100); - } + }; renderToolbar() { const { agentConfig } = this.props; @@ -391,11 +407,7 @@ class Agent extends DashboardView { return ( {models.length > 0 && ( - {}} - > + {}}> {models.map((model, index) => ( {}} > - {permissionOperations.map((operation) => ( + {permissionOperations.map(operation => ( - {messages.map((message) => ( + {messages.map(message => (
{message.type === 'warning' ? ( <> - -
- {message.content} -
+ +
{message.content}
) : ( <>
- {message.type === 'agent' ? this.formatMessageContent(message.content) : message.content} + {message.type === 'agent' + ? this.formatMessageContent(message.content) + : message.content}
- {message.timestamp instanceof Date ? - message.timestamp.toLocaleTimeString() : - new Date(message.timestamp).toLocaleTimeString() - } + {message.timestamp instanceof Date + ? message.timestamp.toLocaleTimeString() + : new Date(message.timestamp).toLocaleTimeString()}
)} @@ -570,8 +587,8 @@ class Agent extends DashboardView { title="AI Agent" description={ hasNoAgentConfig - ? 'No AI agent configuration found. Please add an \'agent\' section to your dashboard configuration file.' - : 'No AI models configured. Please add models to the \'agent.models\' array in your dashboard configuration file.' + ? "No AI agent configuration found. Please add an 'agent' section to your dashboard configuration file." + : "No AI models configured. Please add models to the 'agent.models' array in your dashboard configuration file." } /> ) : ( @@ -592,13 +609,17 @@ class Agent extends DashboardView { diff --git a/src/dashboard/Data/Browser/AddColumnDialog.react.js b/src/dashboard/Data/Browser/AddColumnDialog.react.js index 56b48e6924..75c32e81aa 100644 --- a/src/dashboard/Data/Browser/AddColumnDialog.react.js +++ b/src/dashboard/Data/Browser/AddColumnDialog.react.js @@ -296,38 +296,38 @@ export default class AddColumnDialog extends React.Component { semver.valid(this.props.parseServerVersion) && semver.gte(this.props.parseServerVersion, '3.7.0') && this.state.type !== 'Relation' ? ( - <> - - } - input={this.renderDefaultValueInput()} - className={styles.addColumnToggleWrapper} - /> - - } - input={ - this.setState({ required })} - additionalStyles={{ margin: '0px' }} - /> - } - className={styles.addColumnToggleWrapper} - /> - - ) : null + <> + + } + input={this.renderDefaultValueInput()} + className={styles.addColumnToggleWrapper} + /> + + } + input={ + this.setState({ required })} + additionalStyles={{ margin: '0px' }} + /> + } + className={styles.addColumnToggleWrapper} + /> + + ) : null } ); diff --git a/src/dashboard/Data/Browser/Browser.react.js b/src/dashboard/Data/Browser/Browser.react.js index aac6b4efba..38a1d53290 100644 --- a/src/dashboard/Data/Browser/Browser.react.js +++ b/src/dashboard/Data/Browser/Browser.react.js @@ -196,7 +196,8 @@ class Browser extends DashboardView { processedScripts: 0, - reloadDataTableAfterScript: window.localStorage?.getItem('reloadDataTableAfterScript') === 'true', + reloadDataTableAfterScript: + window.localStorage?.getItem('reloadDataTableAfterScript') === 'true', rowCheckboxDragging: false, draggedRowSelection: false, @@ -386,7 +387,7 @@ class Browser extends DashboardView { if (classList) { const classNames = Object.keys(classList.toObject()); await Promise.all( - classNames.map(async (className) => { + classNames.map(async className => { try { const filters = await this.filterPreferencesManager.getFilters( this.context.applicationId, @@ -505,36 +506,42 @@ class Browser extends DashboardView { const promise = Parse.Cloud.run(cloudCodeFunction, params, options); promise.cancel = () => requestTask?.abort(); this.infoPanelQueries[objectId] = promise; - promise.then( - result => { - if (this.infoPanelQueries[objectId] !== promise) { - return; - } - if (result && result.panel && result.panel && result.panel.segments) { - this.setState({ AggregationPanelData: result, isLoadingInfoPanel: false, lastFetchedObjectId: objectId }); - } else { + promise + .then( + result => { + if (this.infoPanelQueries[objectId] !== promise) { + return; + } + if (result && result.panel && result.panel && result.panel.segments) { + this.setState({ + AggregationPanelData: result, + isLoadingInfoPanel: false, + lastFetchedObjectId: objectId, + }); + } else { + this.setState({ + isLoadingInfoPanel: false, + errorAggregatedData: 'Improper JSON format', + }); + this.showNote(this.state.errorAggregatedData, true); + } + }, + error => { + if (this.infoPanelQueries[objectId] !== promise) { + return; + } this.setState({ isLoadingInfoPanel: false, - errorAggregatedData: 'Improper JSON format', + errorAggregatedData: error.message, }); this.showNote(this.state.errorAggregatedData, true); } - }, - error => { - if (this.infoPanelQueries[objectId] !== promise) { - return; + ) + .finally(() => { + if (this.infoPanelQueries[objectId] === promise) { + delete this.infoPanelQueries[objectId]; } - this.setState({ - isLoadingInfoPanel: false, - errorAggregatedData: error.message, - }); - this.showNote(this.state.errorAggregatedData, true); - } - ).finally(() => { - if (this.infoPanelQueries[objectId] === promise) { - delete this.infoPanelQueries[objectId]; - } - }); + }); } setAggregationPanelData(data) { @@ -672,34 +679,34 @@ class Browser extends DashboardView { const query = new URLSearchParams(props.location.search); if (query.has('filters')) { const queryFilters = JSON.parse(query.get('filters')); - queryFilters.forEach( - filter => { - // Convert date strings to Parse Date objects for proper Parse query functionality - const processedFilter = { ...filter, class: filter.class || props.params.className }; - - // Check the schema to see if this field is a Date type - const classes = props.schema?.data?.get('classes'); - const className = processedFilter.class || props.params.className; - const fieldName = processedFilter.field; - - if (classes && className && fieldName) { - const classSchema = classes.get(className); - const fieldType = classSchema?.get(fieldName)?.type; - - // If field type is Date and compareTo is not already a Date object, convert it - if (fieldType === 'Date' && - processedFilter.compareTo && - typeof processedFilter.compareTo !== 'object') { - processedFilter.compareTo = { - __type: 'Date', - iso: new Date(processedFilter.compareTo).toISOString() - }; - } - } + queryFilters.forEach(filter => { + // Convert date strings to Parse Date objects for proper Parse query functionality + const processedFilter = { ...filter, class: filter.class || props.params.className }; - filters = filters.push(Map(processedFilter)); + // Check the schema to see if this field is a Date type + const classes = props.schema?.data?.get('classes'); + const className = processedFilter.class || props.params.className; + const fieldName = processedFilter.field; + + if (classes && className && fieldName) { + const classSchema = classes.get(className); + const fieldType = classSchema?.get(fieldName)?.type; + + // If field type is Date and compareTo is not already a Date object, convert it + if ( + fieldType === 'Date' && + processedFilter.compareTo && + typeof processedFilter.compareTo !== 'object' + ) { + processedFilter.compareTo = { + __type: 'Date', + iso: new Date(processedFilter.compareTo).toISOString(), + }; + } } - ); + + filters = filters.push(Map(processedFilter)); + }); } return filters; } @@ -753,7 +760,8 @@ class Browser extends DashboardView { if (skip > 0) { queryParams.set('skip', skip.toString()); } - if (limit !== 100) { // Only include limit if it's not the default + if (limit !== 100) { + // Only include limit if it's not the default queryParams.set('limit', limit.toString()); } @@ -1206,8 +1214,8 @@ class Browser extends DashboardView { this.setState(prevState => ({ counts: { ...prevState.counts, - [className]: count - } + [className]: count, + }, })); }); promises.push(promise); @@ -1480,16 +1488,19 @@ class Browser extends DashboardView { this.excludeFields(query, source); const { useMasterKey } = this.state; - query.find({ useMasterKey }).then(nextPage => { - if (className === this.props.params.className) { - this.setState(state => ({ - data: state.data.concat(nextPage), - })); - } - }).catch(error => { - const msg = typeof error === 'string' ? error : error.message; - this.showNote(msg, true); - }); + query + .find({ useMasterKey }) + .then(nextPage => { + if (className === this.props.params.className) { + this.setState(state => ({ + data: state.data.concat(nextPage), + })); + } + }) + .catch(error => { + const msg = typeof error === 'string' ? error : error.message; + this.showNote(msg, true); + }); this.setState({ lastMax: this.state.lastMax + this.state.limit }); } @@ -1562,10 +1573,7 @@ class Browser extends DashboardView { // Use server-loaded filters from state if available, otherwise fallback to local storage const existingFilters = this.state.classFilters[className] || []; - const preferences = ClassPreferences.getPreferences( - this.context.applicationId, - className - ); + const preferences = ClassPreferences.getPreferences(this.context.applicationId, className); // Initialize preferences.filters from state if needed if (!preferences.filters) { @@ -1583,8 +1591,8 @@ class Browser extends DashboardView { if (filterId && filterId.startsWith('legacy:')) { // Handle legacy filter update by converting to modern filter const legacyFilterName = filterId.substring(7); // Remove 'legacy:' prefix - const existingLegacyFilterIndex = preferences.filters.findIndex(filter => - !filter.id && filter.name === legacyFilterName + const existingLegacyFilterIndex = preferences.filters.findIndex( + filter => !filter.id && filter.name === legacyFilterName ); if (existingLegacyFilterIndex !== -1) { @@ -1625,8 +1633,8 @@ class Browser extends DashboardView { } else { // Check if this is updating an existing filter by name and content match // (legacy filters get auto-assigned UUIDs when read, so we match by content) - const existingFilterIndex = preferences.filters.findIndex(filter => - filter.name === name && filter.filter === _filters + const existingFilterIndex = preferences.filters.findIndex( + filter => filter.name === name && filter.filter === _filters ); if (existingFilterIndex !== -1) { @@ -1693,8 +1701,8 @@ class Browser extends DashboardView { this.setState(prevState => ({ classFilters: { ...prevState.classFilters, - [className]: filters || [] - } + [className]: filters || [], + }, })); } catch (error) { console.error(`Failed to reload filters for class ${className}:`, error); @@ -2244,7 +2252,9 @@ class Browser extends DashboardView { formData, this.showNote, this.state.reloadDataTableAfterScript ? this.refresh : null, - this.state.reloadDataTableAfterScript ? null : (ids) => this.dataBrowserRef.current?.handleRefreshObjects(ids) + this.state.reloadDataTableAfterScript + ? null + : ids => this.dataBrowserRef.current?.handleRefreshObjects(ids) ); } @@ -2293,14 +2303,12 @@ class Browser extends DashboardView { if (remainingObjects.length === 0) { const objectIds = objects.map(obj => obj.id); if (this.state.reloadDataTableAfterScript) { - this.setState( - { selection: {}, showExecuteScriptRowsDialog: false }, - () => this.refresh() + this.setState({ selection: {}, showExecuteScriptRowsDialog: false }, () => + this.refresh() ); } else { - this.setState( - { selection: {}, showExecuteScriptRowsDialog: false }, - () => this.dataBrowserRef.current?.handleRefreshObjects(objectIds) + this.setState({ selection: {}, showExecuteScriptRowsDialog: false }, () => + this.dataBrowserRef.current?.handleRefreshObjects(objectIds) ); } return; @@ -2368,14 +2376,10 @@ class Browser extends DashboardView { } const objectIds = objects.map(obj => obj.id); if (this.state.reloadDataTableAfterScript) { - this.setState( - { selection: {}, showExecuteScriptRowsDialog: false }, - () => this.refresh() - ); + this.setState({ selection: {}, showExecuteScriptRowsDialog: false }, () => this.refresh()); } else { - this.setState( - { selection: {}, showExecuteScriptRowsDialog: false }, - () => this.dataBrowserRef.current?.handleRefreshObjects(objectIds) + this.setState({ selection: {}, showExecuteScriptRowsDialog: false }, () => + this.dataBrowserRef.current?.handleRefreshObjects(objectIds) ); } } catch (e) { @@ -2527,9 +2531,7 @@ class Browser extends DashboardView { // Check for duplicates if needed if (importOptions.preserveObjectIds && importOptions.duplicateHandling !== 'overwrite') { - const objectIds = parseResult.rows - .map(r => r.objectId) - .filter(Boolean); + const objectIds = parseResult.rows.map(r => r.objectId).filter(Boolean); if (objectIds.length > 0) { const existing = await checkDuplicates(objectIds, className); @@ -2582,7 +2584,7 @@ class Browser extends DashboardView { masterKey: this.context.masterKey, maintenanceKey: importOptions.preserveTimestamps ? this.context.maintenanceKey : undefined, continueOnError: importOptions.continueOnError, - onProgress: (progress) => { + onProgress: progress => { if (this.importDialogRef) { this.importDialogRef.setProgress(progress); } @@ -2823,9 +2825,11 @@ class Browser extends DashboardView { let filters = this.state.classFilters[row.name]; // Fallback to local storage ONLY if not using server storage and filters not loaded yet - if (filters === undefined && - (!this.filterPreferencesManager?.isServerConfigEnabled() || - !prefersServerStorage(this.context.applicationId))) { + if ( + filters === undefined && + (!this.filterPreferencesManager?.isServerConfigEnabled() || + !prefersServerStorage(this.context.applicationId)) + ) { const prefs = ClassPreferences.getPreferences(this.context.applicationId, row.name); filters = prefs?.filters || []; } else if (filters === undefined) { @@ -2836,7 +2840,7 @@ class Browser extends DashboardView { const sortedFilters = filters.sort((a, b) => a.name.localeCompare(b.name)); allCategories.push({ ...row, - filters: sortedFilters + filters: sortedFilters, }); } @@ -3444,7 +3448,9 @@ class Browser extends DashboardView { } else if (this.state.showImportDialog) { extras = ( { this.importDialogRef = ref; }} + ref={ref => { + this.importDialogRef = ref; + }} className={className} maintenanceKey={this.context.maintenanceKey} onCancel={this.cancelImportDialog} diff --git a/src/dashboard/Data/Browser/BrowserFooter.react.js b/src/dashboard/Data/Browser/BrowserFooter.react.js index 165bc9eb8d..c1d8ef6373 100644 --- a/src/dashboard/Data/Browser/BrowserFooter.react.js +++ b/src/dashboard/Data/Browser/BrowserFooter.react.js @@ -74,7 +74,9 @@ class BrowserFooter extends React.Component { return (
- {count?.toLocaleString() || 0} objects + + {count?.toLocaleString() || 0} objects + | per page | - Objects {(skip + 1).toLocaleString()} to {Math.min(count ?? limit, skip + limit).toLocaleString()} + + Objects {(skip + 1).toLocaleString()} to{' '} + {Math.min(count ?? limit, skip + limit).toLocaleString()} + {selectedCellsCount > 0 && ( <> | - {selectedCellsCount.toLocaleString()} cells selected + + {selectedCellsCount.toLocaleString()} cells selected + )} {selectedData?.length > 0 && ( diff --git a/src/dashboard/Data/Browser/BrowserTable.react.js b/src/dashboard/Data/Browser/BrowserTable.react.js index 80ec614b7d..575311d326 100644 --- a/src/dashboard/Data/Browser/BrowserTable.react.js +++ b/src/dashboard/Data/Browser/BrowserTable.react.js @@ -135,7 +135,7 @@ export default class BrowserTable extends React.Component { }); } let editor = null; - const isRowHighlighted = (rowIndex) => + const isRowHighlighted = rowIndex => (this.props.current && this.props.current.row === rowIndex) || (this.props.selectedCells && this.props.selectedCells.rowStart >= 0 && diff --git a/src/dashboard/Data/Browser/BrowserToolbar.react.js b/src/dashboard/Data/Browser/BrowserToolbar.react.js index 7b842e6fb2..4c61a743ed 100644 --- a/src/dashboard/Data/Browser/BrowserToolbar.react.js +++ b/src/dashboard/Data/Browser/BrowserToolbar.react.js @@ -554,13 +554,7 @@ const BrowserToolbar = ({ text={ {isGraphPanelVisible && ( - + )} Show Graph Panel @@ -649,13 +643,7 @@ const BrowserToolbar = ({ text={ {reloadDataTableAfterScript && ( - + )} Reload all rows after run diff --git a/src/dashboard/Data/Browser/DataBrowser.react.js b/src/dashboard/Data/Browser/DataBrowser.react.js index f0b5be7deb..7600df09a2 100644 --- a/src/dashboard/Data/Browser/DataBrowser.react.js +++ b/src/dashboard/Data/Browser/DataBrowser.react.js @@ -101,12 +101,9 @@ export default class DataBrowser extends React.Component { props.className, columnPreferences[props.className] ); - const storedRowNumber = - window.localStorage?.getItem(BROWSER_SHOW_ROW_NUMBER) === 'true'; - const storedPanelVisible = - window.localStorage?.getItem(AGGREGATION_PANEL_VISIBLE) === 'true'; - const storedScrollToTop = - window.localStorage?.getItem(BROWSER_SCROLL_TO_TOP) !== 'false'; + const storedRowNumber = window.localStorage?.getItem(BROWSER_SHOW_ROW_NUMBER) === 'true'; + const storedPanelVisible = window.localStorage?.getItem(AGGREGATION_PANEL_VISIBLE) === 'true'; + const storedScrollToTop = window.localStorage?.getItem(BROWSER_SCROLL_TO_TOP) !== 'false'; const storedAutoLoadFirstRow = window.localStorage?.getItem(AGGREGATION_PANEL_AUTO_LOAD_FIRST_ROW) === 'true'; const storedSyncPanelScroll = @@ -120,15 +117,13 @@ export default class DataBrowser extends React.Component { const storedPanelCount = window.localStorage?.getItem(AGGREGATION_PANEL_COUNT); const parsedPanelCount = storedPanelCount ? parseInt(storedPanelCount, 10) : 1; const hasAggregation = - props.classwiseCloudFunctions?.[ - `${props.app.applicationId}${props.appName}` - ]?.[props.className]; - const storedAutoScroll = - window.localStorage?.getItem(AGGREGATION_PANEL_AUTO_SCROLL) === 'true'; + props.classwiseCloudFunctions?.[`${props.app.applicationId}${props.appName}`]?.[ + props.className + ]; + const storedAutoScroll = window.localStorage?.getItem(AGGREGATION_PANEL_AUTO_SCROLL) === 'true'; const storedAutoScrollRequireHover = window.localStorage?.getItem(AGGREGATION_PANEL_AUTO_SCROLL_REQUIRE_HOVER) !== 'false'; - const storedGraphPanelVisible = - window.localStorage?.getItem(GRAPH_PANEL_VISIBLE) === 'true'; + const storedGraphPanelVisible = window.localStorage?.getItem(GRAPH_PANEL_VISIBLE) === 'true'; const storedGraphPanelWidth = window.localStorage?.getItem(GRAPH_PANEL_WIDTH); const parsedWidth = storedGraphPanelWidth ? parseInt(storedGraphPanelWidth, 10) : 400; const parsedGraphPanelWidth = !isNaN(parsedWidth) && parsedWidth > 0 ? parsedWidth : 400; @@ -238,7 +233,8 @@ export default class DataBrowser extends React.Component { this.removePanel = this.removePanel.bind(this); this.handlePanelScroll = this.handlePanelScroll.bind(this); this.handlePanelHeaderContextMenu = this.handlePanelHeaderContextMenu.bind(this); - this.handleAggregationPanelTextContextMenu = this.handleAggregationPanelTextContextMenu.bind(this); + this.handleAggregationPanelTextContextMenu = + this.handleAggregationPanelTextContextMenu.bind(this); this.handleWrapperWheel = this.handleWrapperWheel.bind(this); this.onMouseDownPanelCheckBox = this.onMouseDownPanelCheckBox.bind(this); this.onMouseUpPanelCheckBox = this.onMouseUpPanelCheckBox.bind(this); @@ -335,12 +331,11 @@ export default class DataBrowser extends React.Component { this.setState({ order, frozenColumnIndex: -1 }); } if (props && props.className) { - const storedPanelVisible = - window.localStorage?.getItem(AGGREGATION_PANEL_VISIBLE) === 'true'; + const storedPanelVisible = window.localStorage?.getItem(AGGREGATION_PANEL_VISIBLE) === 'true'; const hasAggregation = - props.classwiseCloudFunctions?.[ - `${props.app.applicationId}${props.appName}` - ]?.[props.className]; + props.classwiseCloudFunctions?.[`${props.app.applicationId}${props.appName}`]?.[ + props.className + ]; if (!hasAggregation) { this.setState({ isPanelVisible: false }); this.setState({ selectedObjectId: undefined }); @@ -388,7 +383,12 @@ export default class DataBrowser extends React.Component { 'browser.panels.settings', this.props.app.applicationId ); - if (panelSettings !== null && typeof panelSettings === 'object' && typeof panelSettings.reverseAutoScrollSpeedFactor === 'number' && panelSettings.reverseAutoScrollSpeedFactor > 0) { + if ( + panelSettings !== null && + typeof panelSettings === 'object' && + typeof panelSettings.reverseAutoScrollSpeedFactor === 'number' && + panelSettings.reverseAutoScrollSpeedFactor > 0 + ) { this.setState({ reverseAutoScrollSpeedFactor: panelSettings.reverseAutoScrollSpeedFactor }); } } catch (error) { @@ -405,7 +405,7 @@ export default class DataBrowser extends React.Component { const graphConfig = graphs && graphs.length > 0 ? graphs[0] : null; this.setState({ availableGraphs: graphs || [], - graphConfig: graphConfig + graphConfig: graphConfig, }); } catch (error) { console.error('Failed to load graphs on mount:', error); @@ -459,7 +459,7 @@ export default class DataBrowser extends React.Component { const graphConfig = graphs && graphs.length > 0 ? graphs[0] : null; this.setState({ graphConfig, - availableGraphs: graphs || [] + availableGraphs: graphs || [], }); } catch (error) { console.error('Failed to load graphs on className change:', error); @@ -467,20 +467,22 @@ export default class DataBrowser extends React.Component { // Just clear the state on error this.setState({ graphConfig: null, - availableGraphs: [] + availableGraphs: [], }); } } // Clear panels when className changes, data becomes null, or data reloads - const shouldClearPanels = this.state.isPanelVisible && ( + const shouldClearPanels = + this.state.isPanelVisible && // Class changed - this.props.className !== prevProps.className || - // Data became null (filter change, loading state) - (this.props.data === null && prevProps.data !== null) || - // Data reloaded (script execution, refresh) - (this.props.data !== null && prevProps.data !== null && this.props.data !== prevProps.data) - ); + (this.props.className !== prevProps.className || + // Data became null (filter change, loading state) + (this.props.data === null && prevProps.data !== null) || + // Data reloaded (script execution, refresh) + (this.props.data !== null && + prevProps.data !== null && + this.props.data !== prevProps.data)); if (shouldClearPanels) { if (this._skipPanelClear) { @@ -515,17 +517,17 @@ export default class DataBrowser extends React.Component { this.props.data && this.props.data.length > 0 && !this.state.selectedObjectId && - ((!prevProps.data || prevProps.data.length === 0) || - prevProps.className !== this.props.className || - prevState.isPanelVisible !== this.state.isPanelVisible) + (!prevProps.data || + prevProps.data.length === 0 || + prevProps.className !== this.props.className || + prevState.isPanelVisible !== this.state.isPanelVisible) ) { const firstRowObjectId = this.props.data[0].id; this.setShowAggregatedData(true); this.setSelectedObjectId(firstRowObjectId); // Also set the current cell to the first cell of the first row let col = - this.state.lastSelectedCol !== undefined && - prevProps.className === this.props.className + this.state.lastSelectedCol !== undefined && prevProps.className === this.props.className ? this.state.lastSelectedCol : 0; if (col >= this.state.order.length) { @@ -548,7 +550,10 @@ export default class DataBrowser extends React.Component { if (this.state.scrollToTop) { this.aggregationPanelRef.current.scrollTop = 0; // If auto-scrolling and scrollToTop is enabled, restart animation from top - if (this.state.isAutoScrolling && this.state.selectedObjectId !== prevState.selectedObjectId) { + if ( + this.state.isAutoScrolling && + this.state.selectedObjectId !== prevState.selectedObjectId + ) { // Cancel current animation and restart from top if (this.autoScrollAnimationId) { cancelAnimationFrame(this.autoScrollAnimationId); @@ -560,7 +565,7 @@ export default class DataBrowser extends React.Component { } // Also reset multi-panel scroll positions if (this.state.panelCount > 1 && this.state.syncPanelScroll) { - this.panelColumnRefs.forEach((ref) => { + this.panelColumnRefs.forEach(ref => { if (ref && ref.current) { ref.current.scrollTop = 0; } @@ -583,8 +588,8 @@ export default class DataBrowser extends React.Component { this.setState(prev => ({ multiPanelData: { ...prev.multiPanelData, - [this.props.lastFetchedObjectId]: this.props.AggregationPanelData - } + [this.props.lastFetchedObjectId]: this.props.AggregationPanelData, + }, })); } @@ -595,7 +600,9 @@ export default class DataBrowser extends React.Component { if (prevNeedsListener !== nowNeedsListener && this.multiPanelWrapperElement) { if (nowNeedsListener) { // Add listener - this.multiPanelWrapperElement.addEventListener('wheel', this.handleWrapperWheel, { passive: false }); + this.multiPanelWrapperElement.addEventListener('wheel', this.handleWrapperWheel, { + passive: false, + }); } else { // Remove listener this.multiPanelWrapperElement.removeEventListener('wheel', this.handleWrapperWheel); @@ -764,8 +771,7 @@ export default class DataBrowser extends React.Component { const firstRowObjectId = this.props.data[0].id; this.setShowAggregatedData(true); this.setSelectedObjectId(firstRowObjectId); - let col = - this.state.lastSelectedCol !== undefined ? this.state.lastSelectedCol : 0; + let col = this.state.lastSelectedCol !== undefined ? this.state.lastSelectedCol : 0; if (col >= this.state.order.length) { col = 0; } @@ -819,8 +825,7 @@ export default class DataBrowser extends React.Component { checkClassNameChange(prevClassName, className) { if (prevClassName !== className) { - const storedPanelVisible = - window.localStorage?.getItem(AGGREGATION_PANEL_VISIBLE) === 'true'; + const storedPanelVisible = window.localStorage?.getItem(AGGREGATION_PANEL_VISIBLE) === 'true'; const hasAggregation = this.props.classwiseCloudFunctions?.[ `${this.props.app.applicationId}${this.props.appName}` @@ -886,11 +891,11 @@ export default class DataBrowser extends React.Component { // Check if the event target is an input, textarea, or select element // Allow checkboxes since they don't accept text input const target = e.target; - const isTextInputElement = target && ( - target.tagName === 'TEXTAREA' || - target.tagName === 'SELECT' || - (target.tagName === 'INPUT' && target.type !== 'checkbox') - ); + const isTextInputElement = + target && + (target.tagName === 'TEXTAREA' || + target.tagName === 'SELECT' || + (target.tagName === 'INPUT' && target.type !== 'checkbox')); // Ignore most keyboard events when focus is on text input elements // This allows normal text editing behavior in filter inputs and dropdown navigation @@ -909,7 +914,7 @@ export default class DataBrowser extends React.Component { this.aggregationPanelRef.current.scrollTop = 0; } // Scroll each individual panel column - this.panelColumnRefs.forEach((ref) => { + this.panelColumnRefs.forEach(ref => { if (ref?.current) { ref.current.scrollTop = 0; } @@ -950,7 +955,8 @@ export default class DataBrowser extends React.Component { // If there's text selected, check if we're in the aggregation panel if (selectedText.length > 0) { const target = e.target; - const isWithinPanel = this.aggregationPanelRef?.current && this.aggregationPanelRef.current.contains(target); + const isWithinPanel = + this.aggregationPanelRef?.current && this.aggregationPanelRef.current.contains(target); if (isWithinPanel) { // Let the browser handle the copy operation for selected text @@ -1064,10 +1070,10 @@ export default class DataBrowser extends React.Component { e.ctrlKey || e.metaKey ? firstVisibleColumnIndex : this.getNextVisibleColumnIndex( - -1, - firstVisibleColumnIndex, - lastVisibleColumnIndex - ), + -1, + firstVisibleColumnIndex, + lastVisibleColumnIndex + ), }, }); e.preventDefault(); @@ -1078,7 +1084,10 @@ export default class DataBrowser extends React.Component { // or with ctrl/meta (excel style - move to the first row) const prevObjectID = this.state.selectedObjectId; // Calculate step size based on batch navigation mode - const stepSize = this.state.panelCount > 1 && this.state.batchNavigate && this.state.isPanelVisible ? this.state.panelCount : 1; + const stepSize = + this.state.panelCount > 1 && this.state.batchNavigate && this.state.isPanelVisible + ? this.state.panelCount + : 1; const newRow = e.ctrlKey || e.metaKey ? 0 : Math.max(this.state.current.row - stepSize, 0); this.setState({ current: { @@ -1109,10 +1118,10 @@ export default class DataBrowser extends React.Component { e.ctrlKey || e.metaKey ? lastVisibleColumnIndex : this.getNextVisibleColumnIndex( - 1, - firstVisibleColumnIndex, - lastVisibleColumnIndex - ), + 1, + firstVisibleColumnIndex, + lastVisibleColumnIndex + ), }, }); e.preventDefault(); @@ -1123,7 +1132,10 @@ export default class DataBrowser extends React.Component { // or with ctrl/meta (excel style - move to the last row) const prevObjectID = this.state.selectedObjectId; // Calculate step size based on batch navigation mode - const stepSizeDown = this.state.panelCount > 1 && this.state.batchNavigate && this.state.isPanelVisible ? this.state.panelCount : 1; + const stepSizeDown = + this.state.panelCount > 1 && this.state.batchNavigate && this.state.isPanelVisible + ? this.state.panelCount + : 1; const newRow = e.ctrlKey || e.metaKey ? this.props.data.length - 1 @@ -1149,7 +1161,8 @@ export default class DataBrowser extends React.Component { e.preventDefault(); break; } - case 67: { // C + case 67: { + // C if ((e.ctrlKey || e.metaKey) && this.state.copyableValue !== undefined) { copy(this.state.copyableValue); // Copies current cell value to clipboard if (this.props.showNote) { @@ -1159,7 +1172,8 @@ export default class DataBrowser extends React.Component { } break; } - case 32: { // Space + case 32: { + // Space // Only handle space if not editing and there's a current row selected if (!this.state.editing && this.state.current?.row >= 0) { const rowId = this.props.data[this.state.current.row].id; @@ -1169,7 +1183,8 @@ export default class DataBrowser extends React.Component { } break; } - case 13: { // Enter (enable editing) + case 13: { + // Enter (enable editing) if (!this.state.editing && this.state.current) { this.setEditing(true); e.preventDefault(); @@ -1268,7 +1283,8 @@ export default class DataBrowser extends React.Component { if (prevState.panelCount > 1 && selectedObjectId) { // When batch-navigate is enabled, always rebuild the batch starting from the selected row // to ensure prefetched data is properly utilized - const shouldRebuildBatch = !newDisplayedObjectIds.includes(selectedObjectId) || prevState.batchNavigate; + const shouldRebuildBatch = + !newDisplayedObjectIds.includes(selectedObjectId) || prevState.batchNavigate; if (shouldRebuildBatch) { const currentIndex = this.props.data?.findIndex(obj => obj.id === selectedObjectId); @@ -1282,14 +1298,21 @@ export default class DataBrowser extends React.Component { // Build the new batch of displayed objects newDisplayedObjectIds = []; - for (let i = 0; i < prevState.panelCount && startIndex + i < this.props.data.length; i++) { + for ( + let i = 0; + i < prevState.panelCount && startIndex + i < this.props.data.length; + i++ + ) { const objectId = this.props.data[startIndex + i].id; newDisplayedObjectIds.push(objectId); // Check if data is already available if (!newMultiPanelData[objectId]) { const cached = prefetchCache[objectId]; - if (cached && (!prefetchStale || (Date.now() - cached.timestamp) / 1000 < prefetchStale)) { + if ( + cached && + (!prefetchStale || (Date.now() - cached.timestamp) / 1000 < prefetchStale) + ) { // Use cached data immediately newMultiPanelData[objectId] = cached.data; } else { @@ -1307,7 +1330,7 @@ export default class DataBrowser extends React.Component { selectionHistory: history, displayedObjectIds: newDisplayedObjectIds, multiPanelData: newMultiPanelData, - _objectsToFetch: objectsToFetch // Temporary field to handle after setState + _objectsToFetch: objectsToFetch, // Temporary field to handle after setState }; }, () => { @@ -1362,7 +1385,7 @@ export default class DataBrowser extends React.Component { if (script.showConfirmationDialog) { this.setState({ showScriptConfirmationDialog: true, - selectedScript + selectedScript, }); } else { executeScript( @@ -1603,14 +1626,21 @@ export default class DataBrowser extends React.Component { toggleAutoScrollRequireHover() { this.setState(prevState => { const newRequireHover = !prevState.autoScrollRequireHover; - window.localStorage?.setItem(AGGREGATION_PANEL_AUTO_SCROLL_REQUIRE_HOVER, String(newRequireHover)); + window.localStorage?.setItem( + AGGREGATION_PANEL_AUTO_SCROLL_REQUIRE_HOVER, + String(newRequireHover) + ); return { autoScrollRequireHover: newRequireHover }; }); } handleAutoScrollKeyDown(e) { // Command/Meta key = keyCode 91 (left) or 93 (right) - if ((e.keyCode === 91 || e.keyCode === 93) && this.state.autoScrollEnabled && this.state.isPanelVisible) { + if ( + (e.keyCode === 91 || e.keyCode === 93) && + this.state.autoScrollEnabled && + this.state.isPanelVisible + ) { if (this.state.optionKeyPressed && this.state.isAutoScrolling) { // Option already held + Cmd pressed = activate reverse auto-scroll // Clear optionKeyPressed to unblock auto-scroll @@ -1641,14 +1671,17 @@ export default class DataBrowser extends React.Component { const scrollEndTime = recordingScrollEnd || Date.now(); const delay = Math.max(200, Date.now() - scrollEndTime); // Minimum 200ms delay - this.setState({ - commandKeyPressed: false, - isRecordingAutoScroll: false, - autoScrollAmount: recordedScrollDelta, - autoScrollDelay: delay, - }, () => { - this.startAutoScroll(); - }); + this.setState( + { + commandKeyPressed: false, + isRecordingAutoScroll: false, + autoScrollAmount: recordedScrollDelta, + autoScrollDelay: delay, + }, + () => { + this.startAutoScroll(); + } + ); } else { // No scroll was recorded, just reset this.setState({ @@ -1814,7 +1847,11 @@ export default class DataBrowser extends React.Component { handleWindowBlur() { // Reset all modifier key tracking state when the window loses focus, // since keyup events won't fire while the window is not focused - if (this.state.commandKeyPressed || this.state.optionKeyPressed || this.state.reverseAutoScrollActive) { + if ( + this.state.commandKeyPressed || + this.state.optionKeyPressed || + this.state.reverseAutoScrollActive + ) { this.setState({ commandKeyPressed: false, optionKeyPressed: false, @@ -1959,7 +1996,7 @@ export default class DataBrowser extends React.Component { const panelStartPositions = []; let maxPanelStartScrollTop = 0; if (this.state.panelCount > 1 && this.state.syncPanelScroll) { - this.panelColumnRefs.forEach((ref) => { + this.panelColumnRefs.forEach(ref => { if (ref && ref.current) { panelStartPositions.push(ref.current.scrollTop); if (ref.current.scrollTop > maxPanelStartScrollTop) { @@ -1971,7 +2008,7 @@ export default class DataBrowser extends React.Component { }); } - const animateScroll = (currentTime) => { + const animateScroll = currentTime => { if (!this.state.isAutoScrolling || this.isAutoScrollBlocked()) { this.autoScrollIsBlocked = true; // If stopped or blocked during animation, schedule next check @@ -1995,7 +2032,7 @@ export default class DataBrowser extends React.Component { const easeOut = 1 - Math.pow(1 - progress, 3); // Apply scroll to main container - const newScrollTop = startScrollTop + (scrollAmount * easeOut); + const newScrollTop = startScrollTop + scrollAmount * easeOut; container.scrollTop = newScrollTop; // Sync scroll to other panels @@ -2004,8 +2041,8 @@ export default class DataBrowser extends React.Component { // During reverse auto-scroll, use the max scrollTop as the base for all panels // so that shorter panels stay put until the longest panel catches up, // matching the behavior of manual wheel scrolling (handleWrapperWheel) - const newPanelScrollTop = maxPanelStartScrollTop + (scrollAmount * easeOut); - this.panelColumnRefs.forEach((ref) => { + const newPanelScrollTop = maxPanelStartScrollTop + scrollAmount * easeOut; + this.panelColumnRefs.forEach(ref => { if (ref && ref.current) { ref.current.scrollTop = newPanelScrollTop; } @@ -2013,7 +2050,7 @@ export default class DataBrowser extends React.Component { } else { this.panelColumnRefs.forEach((ref, index) => { if (ref && ref.current && panelStartPositions[index] !== null) { - ref.current.scrollTop = panelStartPositions[index] + (scrollAmount * easeOut); + ref.current.scrollTop = panelStartPositions[index] + scrollAmount * easeOut; } }); } @@ -2059,14 +2096,14 @@ export default class DataBrowser extends React.Component { showGraphDialog(isNewGraph = false) { this.setState({ showGraphDialog: true, - isCreatingNewGraph: isNewGraph + isCreatingNewGraph: isNewGraph, }); } showNewGraphDialog() { this.setState({ showGraphDialog: true, - isCreatingNewGraph: true + isCreatingNewGraph: true, }); } @@ -2078,7 +2115,7 @@ export default class DataBrowser extends React.Component { // Ensure config has an ID for server storage const configWithId = { ...config, - id: config.id || this.graphPreferencesManager.generateGraphId() + id: config.id || this.graphPreferencesManager.generateGraphId(), }; // Store previous state for potential rollback @@ -2123,7 +2160,7 @@ export default class DataBrowser extends React.Component { // Revert optimistic update on error this.setState({ graphConfig: previousGraphConfig, - availableGraphs: previousAvailableGraphs + availableGraphs: previousAvailableGraphs, }); // Show error notification to user if (this.props.showNote) { @@ -2173,7 +2210,7 @@ export default class DataBrowser extends React.Component { this.setState({ graphConfig: previousGraphConfig, availableGraphs: previousAvailableGraphs, - isGraphPanelVisible: previousIsGraphPanelVisible + isGraphPanelVisible: previousIsGraphPanelVisible, }); // Show error notification to user if (this.props.showNote) { @@ -2251,7 +2288,7 @@ export default class DataBrowser extends React.Component { // Find the maximum scrollTop among all panels to use as the base let maxScrollTop = 0; - this.panelColumnRefs.forEach((ref) => { + this.panelColumnRefs.forEach(ref => { if (ref && ref.current && ref.current.scrollTop > maxScrollTop) { maxScrollTop = ref.current.scrollTop; } @@ -2261,7 +2298,7 @@ export default class DataBrowser extends React.Component { const delta = event.deltaY; const newScrollTop = maxScrollTop + delta; - this.panelColumnRefs.forEach((ref) => { + this.panelColumnRefs.forEach(ref => { if (ref && ref.current) { ref.current.scrollTop = newScrollTop; } @@ -2280,56 +2317,56 @@ export default class DataBrowser extends React.Component { this.setState(prev => ({ multiPanelData: { ...prev.multiPanelData, - [objectId]: cached.data - } + [objectId]: cached.data, + }, })); } else { // Fetch fresh data const cloudCodeFunction = - this.props.classwiseCloudFunctions?.[ - `${app.applicationId}${this.props.appName}` - ]?.[className]?.[0]?.cloudCodeFunction; + this.props.classwiseCloudFunctions?.[`${app.applicationId}${this.props.appName}`]?.[ + className + ]?.[0]?.cloudCodeFunction; if (!cloudCodeFunction) { return; } const params = { - object: Parse.Object.extend(className) - .createWithoutData(objectId) - .toPointer(), + object: Parse.Object.extend(className).createWithoutData(objectId).toPointer(), }; const options = { useMasterKey: true }; this.setState(prev => ({ - loadingObjectIds: new Set(prev.loadingObjectIds).add(objectId) + loadingObjectIds: new Set(prev.loadingObjectIds).add(objectId), })); - Parse.Cloud.run(cloudCodeFunction, params, options).then(result => { - // Store in both prefetchCache and multiPanelData - this.setState(prev => { - const newLoading = new Set(prev.loadingObjectIds); - newLoading.delete(objectId); - return { - loadingObjectIds: newLoading, - prefetchCache: { - ...prev.prefetchCache, - [objectId]: { data: result, timestamp: Date.now() } - }, - multiPanelData: { - ...prev.multiPanelData, - [objectId]: result - } - }; - }); - }).catch(error => { - console.error(`Failed to fetch panel data for ${objectId}:`, error); - this.setState(prev => { - const newLoading = new Set(prev.loadingObjectIds); - newLoading.delete(objectId); - return { loadingObjectIds: newLoading }; + Parse.Cloud.run(cloudCodeFunction, params, options) + .then(result => { + // Store in both prefetchCache and multiPanelData + this.setState(prev => { + const newLoading = new Set(prev.loadingObjectIds); + newLoading.delete(objectId); + return { + loadingObjectIds: newLoading, + prefetchCache: { + ...prev.prefetchCache, + [objectId]: { data: result, timestamp: Date.now() }, + }, + multiPanelData: { + ...prev.multiPanelData, + [objectId]: result, + }, + }; + }); + }) + .catch(error => { + console.error(`Failed to fetch panel data for ${objectId}:`, error); + this.setState(prev => { + const newLoading = new Set(prev.loadingObjectIds); + newLoading.delete(objectId); + return { loadingObjectIds: newLoading }; + }); }); - }); } } @@ -2341,8 +2378,11 @@ export default class DataBrowser extends React.Component { if (currentIndex !== -1 && currentIndex !== undefined) { // First, ensure current object data is in multiPanelData const currentObjectData = { ...this.state.multiPanelData }; - if (this.state.selectedObjectId && !currentObjectData[this.state.selectedObjectId] && - Object.keys(this.props.AggregationPanelData).length > 0) { + if ( + this.state.selectedObjectId && + !currentObjectData[this.state.selectedObjectId] && + Object.keys(this.props.AggregationPanelData).length > 0 + ) { currentObjectData[this.state.selectedObjectId] = this.props.AggregationPanelData; } @@ -2357,7 +2397,10 @@ export default class DataBrowser extends React.Component { // Check if data is already available if (!currentObjectData[objectId]) { const cached = prefetchCache[objectId]; - if (cached && (!prefetchStale || (Date.now() - cached.timestamp) / 1000 < prefetchStale)) { + if ( + cached && + (!prefetchStale || (Date.now() - cached.timestamp) / 1000 < prefetchStale) + ) { // Use cached data immediately currentObjectData[objectId] = cached.data; } else { @@ -2447,7 +2490,7 @@ export default class DataBrowser extends React.Component { } // Helper function to check if an object needs prefetching (missing or stale) - const needsPrefetch = (objectId) => { + const needsPrefetch = objectId => { if (!Object.prototype.hasOwnProperty.call(cache, objectId)) { return true; } @@ -2473,20 +2516,16 @@ export default class DataBrowser extends React.Component { const panelCount = this.state.panelCount; // When in multi-panel mode, prefetch all objects in the upcoming batches - for ( - let i = 1; - i <= prefetchObjects && c + (i * stepSize) < this.props.data.length; - i++ - ) { + for (let i = 1; i <= prefetchObjects && c + i * stepSize < this.props.data.length; i++) { // For each step ahead, prefetch the main object - const mainObjId = this.props.data[c + (i * stepSize)].id; + const mainObjId = this.props.data[c + i * stepSize].id; if (needsPrefetch(mainObjId)) { this.prefetchObject(mainObjId); } // If in multi-panel mode, also prefetch the other objects that would be displayed in the batch if (panelCount > 1) { - const batchStartIndex = c + (i * stepSize); + const batchStartIndex = c + i * stepSize; for (let j = 1; j < panelCount && batchStartIndex + j < this.props.data.length; j++) { const batchObjId = this.props.data[batchStartIndex + j].id; if (needsPrefetch(batchObjId)) { @@ -2561,60 +2600,57 @@ export default class DataBrowser extends React.Component { prefetchObject(objectId) { const { className, app } = this.props; const cloudCodeFunction = - this.props.classwiseCloudFunctions?.[ - `${app.applicationId}${this.props.appName}` - ]?.[className]?.[0]?.cloudCodeFunction; + this.props.classwiseCloudFunctions?.[`${app.applicationId}${this.props.appName}`]?.[ + className + ]?.[0]?.cloudCodeFunction; if (!cloudCodeFunction) { return; } const params = { - object: Parse.Object.extend(className) - .createWithoutData(objectId) - .toPointer(), + object: Parse.Object.extend(className).createWithoutData(objectId).toPointer(), }; const options = { useMasterKey: true }; - Parse.Cloud.run(cloudCodeFunction, params, options).then(result => { - this.setState(prev => ({ - prefetchCache: { - ...prev.prefetchCache, - [objectId]: { data: result, timestamp: Date.now() }, - }, - })); + Parse.Cloud.run(cloudCodeFunction, params, options) + .then(result => { + this.setState(prev => ({ + prefetchCache: { + ...prev.prefetchCache, + [objectId]: { data: result, timestamp: Date.now() }, + }, + })); - // Prefetch media if enabled - const { prefetchImage, prefetchVideo, prefetchAudio } = this.getPrefetchSettings(); - const mediaUrls = this.extractMediaUrls(result); + // Prefetch media if enabled + const { prefetchImage, prefetchVideo, prefetchAudio } = this.getPrefetchSettings(); + const mediaUrls = this.extractMediaUrls(result); - if (prefetchImage && mediaUrls.images.size > 0) { - this.prefetchMedia(mediaUrls.images, 'image'); - } - if (prefetchVideo && mediaUrls.videos.size > 0) { - this.prefetchMedia(mediaUrls.videos, 'video'); - } - if (prefetchAudio && mediaUrls.audios.size > 0) { - this.prefetchMedia(mediaUrls.audios, 'audio'); - } - }).catch(error => { - console.error(`Failed to prefetch object ${objectId}:`, error); - }); + if (prefetchImage && mediaUrls.images.size > 0) { + this.prefetchMedia(mediaUrls.images, 'image'); + } + if (prefetchVideo && mediaUrls.videos.size > 0) { + this.prefetchMedia(mediaUrls.videos, 'video'); + } + if (prefetchAudio && mediaUrls.audios.size > 0) { + this.prefetchMedia(mediaUrls.audios, 'audio'); + } + }) + .catch(error => { + console.error(`Failed to prefetch object ${objectId}:`, error); + }); } handleCallCloudFunction(objectId, className, appId) { const { prefetchCache } = this.state; const { prefetchStale } = this.getPrefetchSettings(); const cached = prefetchCache[objectId]; - if ( - cached && - (!prefetchStale || (Date.now() - cached.timestamp) / 1000 < prefetchStale) - ) { + if (cached && (!prefetchStale || (Date.now() - cached.timestamp) / 1000 < prefetchStale)) { this.props.setAggregationPanelData(cached.data); this.props.setLoadingInfoPanel(false); // Also store in multiPanelData for multi-panel display this.setState(prev => ({ multiPanelData: { ...prev.multiPanelData, - [objectId]: cached.data - } + [objectId]: cached.data, + }, })); } else { if (cached) { @@ -2646,11 +2682,7 @@ export default class DataBrowser extends React.Component { this.state.isPanelVisible && ((event.shiftKey && !firstSelectedCell) || !event.shiftKey) ) { - this.handleCallCloudFunction( - objectId, - this.props.className, - this.props.app.applicationId - ); + this.handleCallCloudFunction(objectId, this.props.className, this.props.app.applicationId); } } @@ -2693,32 +2725,38 @@ export default class DataBrowser extends React.Component { if (newSelection.size > 1) { this.setCurrent(null); this.props.setLoadingInfoPanel(false); - this.setState({ - selectedCells: { - list: newSelection, - rowStart, - rowEnd, - colStart, - colEnd, + this.setState( + { + selectedCells: { + list: newSelection, + rowStart, + rowEnd, + colStart, + colEnd, + }, + selectedObjectId: undefined, + selectedData, }, - selectedObjectId: undefined, - selectedData, - }, () => { - this.props.onCellSelectionChange?.(newSelection.size, selectedData); - }); + () => { + this.props.onCellSelectionChange?.(newSelection.size, selectedData); + } + ); } else { this.setCurrent({ row, col }); this.props.onCellSelectionChange?.(0, []); } } else { - this.setState({ - selectedCells: { list: new Set(), rowStart: -1, rowEnd: -1, colStart: -1, colEnd: -1 }, - selectedData: [], - current: { row, col }, - firstSelectedCell: clickedCellKey, - }, () => { - this.props.onCellSelectionChange?.(0, []); - }); + this.setState( + { + selectedCells: { list: new Set(), rowStart: -1, rowEnd: -1, colStart: -1, colEnd: -1 }, + selectedData: [], + current: { row, col }, + firstSelectedCell: clickedCellKey, + }, + () => { + this.props.onCellSelectionChange?.(0, []); + } + ); } } @@ -2746,9 +2784,10 @@ export default class DataBrowser extends React.Component { // Calculate max width for aggregation panel, accounting for graph panel's minimum width when visible const graphPanelMinWidth = 300; - const aggregationMaxWidth = this.state.isGraphPanelVisible && this.state.graphConfig - ? this.state.maxWidth - graphPanelMinWidth - : this.state.maxWidth; + const aggregationMaxWidth = + this.state.isGraphPanelVisible && this.state.graphConfig + ? this.state.maxWidth - graphPanelMinWidth + : this.state.maxWidth; return (
@@ -2770,11 +2809,13 @@ export default class DataBrowser extends React.Component { setSelectedObjectId={this.setSelectedObjectId} callCloudFunction={this.handleCallCloudFunction} setContextMenu={this.setContextMenu} - getRelatedRecordsMenuItem={(textValue) => buildRelatedTextFieldsMenuItem( - this.props.schema, - textValue, - this.props.onPointerCmdClick - )} + getRelatedRecordsMenuItem={textValue => + buildRelatedTextFieldsMenuItem( + this.props.schema, + textValue, + this.props.onPointerCmdClick + ) + } freezeIndex={this.state.frozenColumnIndex} freezeColumns={this.freezeColumns} unfreezeColumns={this.unfreezeColumns} @@ -2816,25 +2857,26 @@ export default class DataBrowser extends React.Component { onMouseLeave={this.handlePanelMouseLeave} > {this.state.panelCount > 1 ? ( -
+
{(() => { // If no objects are displayed, show a single panel if (this.state.displayedObjectIds.length === 0) { // If there's a selected object, show its data const panelData = this.state.selectedObjectId - ? (this.state.multiPanelData[this.state.selectedObjectId] || this.props.AggregationPanelData) + ? this.state.multiPanelData[this.state.selectedObjectId] || + this.props.AggregationPanelData : {}; - const isLoading = this.state.selectedObjectId && this.props.isLoadingCloudFunction; + const isLoading = + this.state.selectedObjectId && this.props.isLoadingCloudFunction; return ( this.props.callCloudFunction(this.state.selectedObjectId, this.props.className, this.props.app.applicationId)} + onReload={() => + this.props.callCloudFunction( + this.state.selectedObjectId, + this.props.className, + this.props.app.applicationId + ) + } /> ); } // Initialize refs array if needed if (this.panelColumnRefs.length !== this.state.displayedObjectIds.length) { - this.panelColumnRefs = this.state.displayedObjectIds.map(() => React.createRef()); + this.panelColumnRefs = this.state.displayedObjectIds.map(() => + React.createRef() + ); } return this.state.displayedObjectIds.map((objectId, index) => { const panelData = this.state.multiPanelData[objectId] || {}; - const isLoading = (objectId === this.state.selectedObjectId && this.props.isLoadingCloudFunction) || this.state.loadingObjectIds.has(objectId); + const isLoading = + (objectId === this.state.selectedObjectId && + this.props.isLoadingCloudFunction) || + this.state.loadingObjectIds.has(objectId); const isRowSelected = this.props.selection[objectId]; return ( @@ -2863,12 +2916,12 @@ export default class DataBrowser extends React.Component { onMouseEnter={() => (this.activePanelIndex = index)} onTouchStart={() => (this.activePanelIndex = index)} onFocus={() => (this.activePanelIndex = index)} - onScroll={(e) => this.handlePanelScroll(e, index)} + onScroll={e => this.handlePanelScroll(e, index)} > {this.state.showPanelCheckbox && (
{ + onMouseDown={e => { // Ignore right-click (button 2) and middle-click (button 1) if (e.button !== 0) { return; @@ -2882,23 +2935,23 @@ export default class DataBrowser extends React.Component { this.handlePanelHeaderMouseEnter(); }} onMouseLeave={this.handlePanelHeaderMouseLeave} - onContextMenu={(e) => { + onContextMenu={e => { e.preventDefault(); this.handlePanelHeaderContextMenu(e, objectId); }} > - +
)} this.props.callCloudFunction(objectId, this.props.className, this.props.app.applicationId)} + onReload={() => + this.props.callCloudFunction( + objectId, + this.props.className, + this.props.app.applicationId + ) + } />
{index < this.state.displayedObjectIds.length - 1 && ( @@ -2930,49 +2989,56 @@ export default class DataBrowser extends React.Component { appName={this.props.appName} className={this.props.className} onContextMenu={this.handleAggregationPanelTextContextMenu} - onReload={() => this.props.callCloudFunction(this.state.selectedObjectId, this.props.className, this.props.app.applicationId)} + onReload={() => + this.props.callCloudFunction( + this.state.selectedObjectId, + this.props.className, + this.props.app.applicationId + ) + } /> )}
)} - {this.state.isGraphPanelVisible && (() => { - // Calculate max width for graph panel, accounting for aggregation panel when visible - const aggregationPanelWidth = this.state.isPanelVisible ? effectivePanelWidth : 0; - const graphMaxWidth = Math.max(300, this.state.maxWidth - aggregationPanelWidth); - // Clamp the graph panel width to the available space - const graphPanelWidth = Math.min(this.state.graphPanelWidth, graphMaxWidth); - - return ( - -
- -
-
- ); - })()} + {this.state.isGraphPanelVisible && + (() => { + // Calculate max width for graph panel, accounting for aggregation panel when visible + const aggregationPanelWidth = this.state.isPanelVisible ? effectivePanelWidth : 0; + const graphMaxWidth = Math.max(300, this.state.maxWidth - aggregationPanelWidth); + // Clamp the graph panel width to the available space + const graphPanelWidth = Math.min(this.state.graphPanelWidth, graphMaxWidth); + + return ( + +
+ +
+
+ ); + })()}
this.setState({ contextMenuX: null, contextMenuY: null, contextMenuItems: null })} + onHide={() => + this.setState({ contextMenuX: null, contextMenuY: null, contextMenuItems: null }) + } /> )} {this.state.showScriptConfirmationDialog && ( this.setState({ showScriptConfirmationDialog: false, selectedScript: null })} + onCancel={() => + this.setState({ showScriptConfirmationDialog: false, selectedScript: null }) + } onConfirm={() => { executeScript( this.state.selectedScript, diff --git a/src/dashboard/Data/Browser/EditRowDialog.react.js b/src/dashboard/Data/Browser/EditRowDialog.react.js index 119e94191a..71edc4c21d 100644 --- a/src/dashboard/Data/Browser/EditRowDialog.react.js +++ b/src/dashboard/Data/Browser/EditRowDialog.react.js @@ -170,9 +170,9 @@ export default class EditRowDialog extends React.Component { const pointerId = newValue[0]; newValue = pointerId ? Parse.Object.fromJSON({ - className: targetClass, - objectId: pointerId, - }) + className: targetClass, + objectId: pointerId, + }) : undefined; this.toggleObjectPicker(name, false); } diff --git a/src/dashboard/Data/Browser/Editor.react.js b/src/dashboard/Data/Browser/Editor.react.js index 0a5ea3716d..f766cf647d 100644 --- a/src/dashboard/Data/Browser/Editor.react.js +++ b/src/dashboard/Data/Browser/Editor.react.js @@ -16,7 +16,21 @@ import decode from 'parse/lib/browser/decode'; import React from 'react'; import StringEditor from 'components/StringEditor/StringEditor.react'; -const Editor = ({ top, left, type, targetClass, value, readonly, width, onCommit, onCancel, setContextMenu, arrayConfigParams, onAddToArrayConfig, getRelatedRecordsMenuItem }) => { +const Editor = ({ + top, + left, + type, + targetClass, + value, + readonly, + width, + onCommit, + onCancel, + setContextMenu, + arrayConfigParams, + onAddToArrayConfig, + getRelatedRecordsMenuItem, +}) => { let content = null; if (type === 'String') { content = ( @@ -116,14 +130,23 @@ const Editor = ({ top, left, type, targetClass, value, readonly, width, onCommit /> ); } else { - content = ; + content = ( + + ); } } else if (type === 'Boolean') { content = ; } else if (type === 'Number') { content = ; } else if (type === 'GeoPoint') { - content = ; + content = ( + + ); } else if (type === 'File') { content = ; } else if (type === 'ACL') { @@ -141,7 +164,18 @@ const Editor = ({ top, left, type, targetClass, value, readonly, width, onCommit ); } }; - content = ; + content = ( + + ); } return
{content}
; diff --git a/src/dashboard/Data/Browser/ExportDialog.react.js b/src/dashboard/Data/Browser/ExportDialog.react.js index 814fd7fb9b..9eb66c74ad 100644 --- a/src/dashboard/Data/Browser/ExportDialog.react.js +++ b/src/dashboard/Data/Browser/ExportDialog.react.js @@ -47,7 +47,7 @@ export default class ExportDialog extends React.Component { icon="down-outline" iconSize={40} title="Export this class?" - subtitle={'We\'ll send you an email when your data is ready.'} + subtitle={"We'll send you an email when your data is ready."} confirmText="Export" cancelText="Cancel" disabled={this.state.progress === undefined || inProgress} diff --git a/src/dashboard/Data/Browser/FooterStats.react.js b/src/dashboard/Data/Browser/FooterStats.react.js index 16532d9c24..d1b571b94d 100644 --- a/src/dashboard/Data/Browser/FooterStats.react.js +++ b/src/dashboard/Data/Browser/FooterStats.react.js @@ -148,11 +148,7 @@ const FooterStats = ({ data }) => { return ( <> - {open ? renderPopover() : null} diff --git a/src/dashboard/Data/Browser/GraphDialog.react.js b/src/dashboard/Data/Browser/GraphDialog.react.js index 4aaa78dee4..3e341d5846 100644 --- a/src/dashboard/Data/Browser/GraphDialog.react.js +++ b/src/dashboard/Data/Browser/GraphDialog.react.js @@ -109,7 +109,7 @@ const PREDEFINED_COLORS = [ ]; // Validate hex color format: #RRGGBB only -const isValidHexColor = (color) => { +const isValidHexColor = color => { if (!color) { return true; } @@ -124,7 +124,9 @@ export default class GraphDialog extends React.Component { // Ensure groupByColumn is always an array const groupByColumn = initialConfig.groupByColumn - ? (Array.isArray(initialConfig.groupByColumn) ? initialConfig.groupByColumn : [initialConfig.groupByColumn]) + ? Array.isArray(initialConfig.groupByColumn) + ? initialConfig.groupByColumn + : [initialConfig.groupByColumn] : []; // Series configuration @@ -150,7 +152,8 @@ export default class GraphDialog extends React.Component { yAxisTitleSecondary: initialConfig.yAxisTitleSecondary || '', showLegend: initialConfig.showLegend !== undefined ? initialConfig.showLegend : true, showGrid: initialConfig.showGrid !== undefined ? initialConfig.showGrid : true, - showAxisLabels: initialConfig.showAxisLabels !== undefined ? initialConfig.showAxisLabels : true, + showAxisLabels: + initialConfig.showAxisLabels !== undefined ? initialConfig.showAxisLabels : true, isStacked: initialConfig.isStacked || false, maxDataPoints: initialConfig.maxDataPoints || 1000, maxDataPointsInput: null, @@ -161,7 +164,10 @@ export default class GraphDialog extends React.Component { valid() { const { chartType, xColumn, yColumn, series, calculatedValues } = this.state; - const hasSeries = Array.isArray(series) && series.length > 0 && series.some(s => s.fields && s.fields.length > 0); + const hasSeries = + Array.isArray(series) && + series.length > 0 && + series.some(s => s.fields && s.fields.length > 0); const hasCalculatedValues = Array.isArray(calculatedValues) && calculatedValues.length > 0; const hasValuesToDisplay = hasSeries || hasCalculatedValues; @@ -216,7 +222,8 @@ export default class GraphDialog extends React.Component { yColumn: this.state.yColumn || null, series: this.state.series.length > 0 ? this.state.series : null, groupByColumn: this.state.groupByColumn.length > 0 ? this.state.groupByColumn : null, - calculatedValues: this.state.calculatedValues.length > 0 ? this.state.calculatedValues : null, + calculatedValues: + this.state.calculatedValues.length > 0 ? this.state.calculatedValues : null, }); } }; @@ -266,7 +273,9 @@ export default class GraphDialog extends React.Component { } isObjectColumn(col) { - return this.props.columns && this.props.columns[col] && this.props.columns[col].type === 'Object'; + return ( + this.props.columns && this.props.columns[col] && this.props.columns[col].type === 'Object' + ); } isObjectDotPath(fieldValue) { @@ -318,12 +327,21 @@ export default class GraphDialog extends React.Component { this.setState({ series: [ ...this.state.series, - { title: '', fields: [], aggregationType: 'count', chartType: '', color: '', lineStyle: '', barStyle: '', expanded: true } - ] + { + title: '', + fields: [], + aggregationType: 'count', + chartType: '', + color: '', + lineStyle: '', + barStyle: '', + expanded: true, + }, + ], }); }; - removeSeries = (index) => { + removeSeries = index => { const newSeries = [...this.state.series]; newSeries.splice(index, 1); this.setState({ series: newSeries }); @@ -333,16 +351,16 @@ export default class GraphDialog extends React.Component { const newSeries = [...this.state.series]; newSeries[index] = { ...newSeries[index], - [key]: value + [key]: value, }; this.setState({ series: newSeries }); }; - toggleSeries = (index) => { + toggleSeries = index => { const newSeries = [...this.state.series]; newSeries[index] = { ...newSeries[index], - expanded: !newSeries[index].expanded + expanded: !newSeries[index].expanded, }; this.setState({ series: newSeries }); }; @@ -395,7 +413,9 @@ export default class GraphDialog extends React.Component { .filter(c => c.name && c.name.trim() !== '') .map(c => c.name); - const availableVariables = [...new Set([...numericColumns, ...seriesFields, ...previousCalcNames])]; + const availableVariables = [ + ...new Set([...numericColumns, ...seriesFields, ...previousCalcNames]), + ]; const validation = validateFormula(calc.formula, availableVariables); return validation.isValid ? null : validation.error; @@ -438,12 +458,12 @@ export default class GraphDialog extends React.Component { this.setState({ calculatedValues: [ ...this.state.calculatedValues, - { fields: [], operator: 'sum', name: '', chartType: '', expanded: true } - ] + { fields: [], operator: 'sum', name: '', chartType: '', expanded: true }, + ], }); }; - removeCalculatedValue = (index) => { + removeCalculatedValue = index => { const newCalculatedValues = [...this.state.calculatedValues]; newCalculatedValues.splice(index, 1); this.setState({ calculatedValues: newCalculatedValues }); @@ -453,34 +473,37 @@ export default class GraphDialog extends React.Component { const newCalculatedValues = [...this.state.calculatedValues]; newCalculatedValues[index] = { ...newCalculatedValues[index], - [field]: value + [field]: value, }; this.setState({ calculatedValues: newCalculatedValues }); }; - toggleCalculatedValue = (index) => { + toggleCalculatedValue = index => { const newCalculatedValues = [...this.state.calculatedValues]; newCalculatedValues[index] = { ...newCalculatedValues[index], - expanded: !newCalculatedValues[index].expanded + expanded: !newCalculatedValues[index].expanded, }; this.setState({ calculatedValues: newCalculatedValues }); }; renderChartTypeSection() { return ( - } input={ - this.setState({ chartType })} - > - {CHART_TYPES.map(type => ( - - ))} - - } /> + } + input={ + this.setState({ chartType })} + > + {CHART_TYPES.map(type => ( + + ))} + + } + /> ); } @@ -488,23 +511,63 @@ export default class GraphDialog extends React.Component { const { chartType } = this.state; const isExpanded = s.expanded !== false; // Display name: title if set, otherwise first field, otherwise "Series N" - const displayName = s.title || (s.fields && s.fields.length > 0 ? (s.fields.length === 1 ? s.fields[0] : `${s.fields.length} fields`) : `Series ${index + 1}`); + const displayName = + s.title || + (s.fields && s.fields.length > 0 + ? s.fields.length === 1 + ? s.fields[0] + : `${s.fields.length} fields` + : `Series ${index + 1}`); const numericAndPointerColumns = this.getNumericAndPointerColumns(); // Use series-specific chart type, or fall back to global chart type const effectiveType = s.chartType || chartType; return ( -
-
this.toggleSeries(index)}> +
+
this.toggleSeries(index)} + >
{isExpanded ? '▼' : '▶'}
-
{isExpanded && (
-
+
-
+
@@ -527,13 +598,25 @@ export default class GraphDialog extends React.Component { onChange={fields => { const added = fields.find(f => !(s.fields || []).includes(f)); if (added && this.isObjectColumn(added)) { - this.setState({ objectPathInput: { field: added, onConfirm: fullPath => this.updateSeries(index, 'fields', [...(s.fields || []), fullPath].sort(stringCompare)) } }); + this.setState({ + objectPathInput: { + field: added, + onConfirm: fullPath => + this.updateSeries( + index, + 'fields', + [...(s.fields || []), fullPath].sort(stringCompare) + ), + }, + }); return; } this.updateSeries(index, 'fields', fields); }} placeHolder="Select field(s)" - formatSelection={selection => selection.length === 1 ? selection[0] : `${selection.length} fields`} + formatSelection={selection => + selection.length === 1 ? selection[0] : `${selection.length} fields` + } > {numericAndPointerColumns.map(col => ( @@ -548,14 +631,22 @@ export default class GraphDialog extends React.Component {
-
+
this.updateSeries(index, 'aggregationType', aggregationType)} + onChange={aggregationType => + this.updateSeries(index, 'aggregationType', aggregationType) + } > {AGGREGATION_TYPES.map(type => (
{(chartType === 'bar' || chartType === 'line') && ( -
+
this.updateSeries(index, 'chartType', seriesChartType)} + onChange={seriesChartType => + this.updateSeries(index, 'chartType', seriesChartType) + } placeHolder={`Default (${chartType === 'bar' ? 'Bar' : 'Line'})`} > @@ -587,20 +686,42 @@ export default class GraphDialog extends React.Component {
)} {(chartType === 'bar' || chartType === 'line') && ( -
+
-
+
this.updateSeries(index, 'useSecondaryYAxis', useSecondaryYAxis)} + onChange={useSecondaryYAxis => + this.updateSeries(index, 'useSecondaryYAxis', useSecondaryYAxis) + } />
)} -
+
@@ -621,31 +742,83 @@ export default class GraphDialog extends React.Component {
c.value === s.color) ? s.color : (s.color ? (isValidHexColor(s.color) ? 'custom' : 'invalid') : '')} - onChange={color => { if (color !== 'custom' && color !== 'invalid') { this.updateSeries(index, 'color', color); } }} + value={ + PREDEFINED_COLORS.find(c => c.value === s.color) + ? s.color + : s.color + ? isValidHexColor(s.color) + ? 'custom' + : 'invalid' + : '' + } + onChange={color => { + if (color !== 'custom' && color !== 'invalid') { + this.updateSeries(index, 'color', color); + } + }} placeHolder="Preset" > - - {s.color && !PREDEFINED_COLORS.find(c => c.value === s.color) && !isValidHexColor(s.color) && ( - - )} - {s.color && !PREDEFINED_COLORS.find(c => c.value === s.color) && isValidHexColor(s.color) && ( - - )} + + {s.color && + !PREDEFINED_COLORS.find(c => c.value === s.color) && + !isValidHexColor(s.color) && ( + + )} + {s.color && + !PREDEFINED_COLORS.find(c => c.value === s.color) && + isValidHexColor(s.color) && ( + + )} {PREDEFINED_COLORS.map(c => ( @@ -656,7 +829,13 @@ export default class GraphDialog extends React.Component {
{effectiveType === 'line' && ( <> -
+
@@ -673,14 +852,22 @@ export default class GraphDialog extends React.Component {
-
+
this.updateSeries(index, 'strokeWidth', strokeWidth)} + onChange={strokeWidth => + this.updateSeries(index, 'strokeWidth', strokeWidth) + } > {STROKE_WIDTHS.map(sw => (