diff --git a/src/dashboard/Data/Views/DeleteViewDialog.react.js b/src/dashboard/Data/Views/DeleteViewDialog.react.js new file mode 100644 index 0000000000..eae2ed039d --- /dev/null +++ b/src/dashboard/Data/Views/DeleteViewDialog.react.js @@ -0,0 +1,45 @@ +import Field from 'components/Field/Field.react'; +import Label from 'components/Label/Label.react'; +import Modal from 'components/Modal/Modal.react'; +import React from 'react'; +import TextInput from 'components/TextInput/TextInput.react'; + +export default class DeleteViewDialog extends React.Component { + constructor() { + super(); + this.state = { + confirmation: '', + }; + } + + valid() { + return this.state.confirmation === this.props.name; + } + + render() { + return ( + + } + input={ + this.setState({ confirmation })} + /> + } + /> + + ); + } +} diff --git a/src/dashboard/Data/Views/EditViewDialog.react.js b/src/dashboard/Data/Views/EditViewDialog.react.js new file mode 100644 index 0000000000..50e3bbb57f --- /dev/null +++ b/src/dashboard/Data/Views/EditViewDialog.react.js @@ -0,0 +1,112 @@ +import Dropdown from 'components/Dropdown/Dropdown.react'; +import Field from 'components/Field/Field.react'; +import Label from 'components/Label/Label.react'; +import Modal from 'components/Modal/Modal.react'; +import Option from 'components/Dropdown/Option.react'; +import React from 'react'; +import TextInput from 'components/TextInput/TextInput.react'; +import Checkbox from 'components/Checkbox/Checkbox.react'; + +function isValidJSON(value) { + try { + JSON.parse(value); + return true; + } catch { + return false; + } +} + +export default class EditViewDialog extends React.Component { + constructor(props) { + super(); + const view = props.view || {}; + this.state = { + name: view.name || '', + className: view.className || '', + query: JSON.stringify(view.query || [], null, 2), + showCounter: !!view.showCounter, + }; + } + + valid() { + return ( + this.state.name.length > 0 && + this.state.className.length > 0 && + isValidJSON(this.state.query) + ); + } + + render() { + const { classes, onConfirm, onCancel } = this.props; + return ( + + onConfirm({ + name: this.state.name, + className: this.state.className, + query: JSON.parse(this.state.query), + showCounter: this.state.showCounter, + }) + } + > + } + input={ + this.setState({ name })} + /> + } + /> + } + input={ + this.setState({ className })} + > + {classes.map(c => ( + + ))} + + } + /> + + } + input={ + this.setState({ query })} + /> + } + /> + } + input={ + this.setState({ showCounter })} + /> + } + /> + + ); + } +} diff --git a/src/dashboard/Data/Views/Views.react.js b/src/dashboard/Data/Views/Views.react.js index f8626929d6..5029ed6ca8 100644 --- a/src/dashboard/Data/Views/Views.react.js +++ b/src/dashboard/Data/Views/Views.react.js @@ -6,9 +6,14 @@ import LoaderContainer from 'components/LoaderContainer/LoaderContainer.react'; import Parse from 'parse'; import React from 'react'; import Notification from 'dashboard/Data/Browser/Notification.react'; -import Icon from 'components/Icon/Icon.react'; +import Pill from 'components/Pill/Pill.react'; import DragHandle from 'components/DragHandle/DragHandle.react'; import CreateViewDialog from './CreateViewDialog.react'; +import EditViewDialog from './EditViewDialog.react'; +import DeleteViewDialog from './DeleteViewDialog.react'; +import BrowserMenu from 'components/BrowserMenu/BrowserMenu.react'; +import MenuItem from 'components/BrowserMenu/MenuItem.react'; +import Separator from 'components/BrowserMenu/Separator.react'; import * as ViewPreferences from 'lib/ViewPreferences'; import generatePath from 'lib/generatePath'; import { withRouter } from 'lib/withRouter'; @@ -34,9 +39,13 @@ class Views extends TableView { columns: {}, tableWidth: 0, showCreate: false, + editView: null, + editIndex: null, + deleteIndex: null, lastError: null, lastNote: null, }; + this.headersRef = React.createRef(); this.noteTimeout = null; this.action = new SidebarAction('Create a view', () => this.setState({ showCreate: true }) @@ -49,6 +58,10 @@ class Views extends TableView { .then(() => this.loadViews(this.context)); } + componentWillUnmount() { + clearTimeout(this.noteTimeout); + } + componentWillReceiveProps(nextProps, nextContext) { if (this.context !== nextContext) { this.props.schema @@ -98,7 +111,22 @@ class Views extends TableView { .aggregate(view.query, { useMasterKey: true }) .then(results => { const columns = {}; - const computeWidth = str => Math.max((String(str).length + 2) * 8, 40); + const computeWidth = str => { + const text = + typeof str === 'object' && str !== null + ? JSON.stringify(str) + : String(str); + if (typeof document !== 'undefined') { + const canvas = + computeWidth._canvas || + (computeWidth._canvas = document.createElement('canvas')); + const context = canvas.getContext('2d'); + context.font = '12px "Source Code Pro", "Courier New", monospace'; + const width = context.measureText(text).width + 32; + return Math.max(width, 40); + } + return Math.max((text.length + 2) * 12, 40); + }; results.forEach(item => { Object.keys(item).forEach(key => { const val = item[key]; @@ -121,7 +149,11 @@ class Views extends TableView { } } if (!columns[key]) { - columns[key] = { type, width: computeWidth(key) }; + columns[key] = { type, width: Math.min(computeWidth(key), 200) }; + } + const width = computeWidth(val); + if (width > columns[key].width && columns[key].width < 200) { + columns[key].width = Math.min(width, 200); } }); }); @@ -154,7 +186,11 @@ class Views extends TableView { console.warn('tableData() needs to return an array of objects'); } else { if (data.length === 0) { - content =
{this.renderEmpty()}
; + content = ( +
+ {this.renderEmpty()} +
+ ); } else { content = (
@@ -174,15 +210,29 @@ class Views extends TableView { return (
-
{content}
+
+
+
+ {headers} +
+ {content} +
+
{toolbar} -
- {headers} -
{extras}
); @@ -191,21 +241,37 @@ class Views extends TableView { renderRow(row) { return ( - {this.state.order.map(({ name, width }) => { + {this.state.order.map(({ name }) => { const value = row[name]; - const type = this.state.columns[name]?.type; + let type = 'String'; + if (typeof value === 'number') { + type = 'Number'; + } else if (typeof value === 'boolean') { + type = 'Boolean'; + } else if (value && typeof value === 'object') { + if (value.__type === 'Date') { + type = 'Date'; + } else if (value.__type === 'Pointer') { + type = 'Pointer'; + } else if (value.__type === 'File') { + type = 'File'; + } else if (value.__type === 'GeoPoint') { + type = 'GeoPoint'; + } else { + type = 'Object'; + } + } let content = ''; if (type === 'Pointer' && value && value.className && value.objectId) { const id = value.objectId; const className = value.className; content = ( - this.handlePointerClick({ className, id })} - > - {id} - - + followClick + shrinkablePill + /> ); } else if (type === 'Object') { content = JSON.stringify(value); @@ -244,6 +310,7 @@ class Views extends TableView { }); } + renderHeaders() { return this.state.order.map(({ name, width }, i) => (
@@ -267,7 +334,9 @@ class Views extends TableView { return ( {}} categories={categories} /> ); @@ -275,7 +344,44 @@ class Views extends TableView { renderToolbar() { const subsection = this.props.params.name || ''; - return ; + let editMenu = null; + if (this.props.params.name) { + editMenu = ( + {}}> + { + const index = this.state.views.findIndex( + v => v.name === this.props.params.name + ); + if (index >= 0) { + this.setState({ + editView: this.state.views[index], + editIndex: index, + }); + } + }} + /> + + { + const index = this.state.views.findIndex( + v => v.name === this.props.params.name + ); + if (index >= 0) { + this.setState({ deleteIndex: index }); + } + }} + /> + + ); + } + return ( + + {editMenu} + + ); } renderExtras() { @@ -306,6 +412,64 @@ class Views extends TableView { }} /> ); + } else if (this.state.editView) { + let classNames = []; + if (this.props.schema?.data) { + const classes = this.props.schema.data.get('classes'); + if (classes) { + classNames = Object.keys(classes.toObject()); + } + } + extras = ( + this.setState({ editView: null, editIndex: null })} + onConfirm={view => { + this.setState( + state => { + const newViews = [...state.views]; + newViews[state.editIndex] = view; + return { editView: null, editIndex: null, views: newViews }; + }, + () => { + ViewPreferences.saveViews( + this.context.applicationId, + this.state.views + ); + this.loadViews(this.context); + } + ); + }} + /> + ); + } else if (this.state.deleteIndex !== null) { + const name = this.state.views[this.state.deleteIndex]?.name || ''; + extras = ( + this.setState({ deleteIndex: null })} + onConfirm={() => { + this.setState( + state => { + const newViews = state.views.filter((_, i) => i !== state.deleteIndex); + return { deleteIndex: null, views: newViews }; + }, + () => { + ViewPreferences.saveViews( + this.context.applicationId, + this.state.views + ); + if (this.props.params.name === name) { + const path = generatePath(this.context, 'views'); + this.props.navigate(path); + } + this.loadViews(this.context); + } + ); + }} + /> + ); } let notification = null; if (this.state.lastError) { diff --git a/src/dashboard/Data/Views/Views.scss b/src/dashboard/Data/Views/Views.scss index 35a2c6d438..028832be21 100644 --- a/src/dashboard/Data/Views/Views.scss +++ b/src/dashboard/Data/Views/Views.scss @@ -40,8 +40,8 @@ } .cell { - line-height: 30px; - padding: 10px 16px; + line-height: 20px; + padding: 5px 16px; border-right: 1px solid #e3e3ea; overflow: hidden; text-overflow: ellipsis; @@ -49,13 +49,3 @@ max-width: none; } -.pointerLink { - display: inline-flex; - align-items: center; - cursor: pointer; - - svg { - margin-left: 4px; - transform: rotate(316deg); - } -}