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 = (
+ {}}>
+
+ );
+ }
+ 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);
- }
-}