diff --git a/front/src/components/boxs/scene/EditSceneBox.jsx b/front/src/components/boxs/scene/EditSceneBox.jsx
index 94541784c6..fa500e2547 100644
--- a/front/src/components/boxs/scene/EditSceneBox.jsx
+++ b/front/src/components/boxs/scene/EditSceneBox.jsx
@@ -1,10 +1,12 @@
import { Component } from 'preact';
import { Localizer, Text } from 'preact-i18n';
+import update from 'immutability-helper';
import BaseEditBox from '../baseEditBox';
import withIntlAsProp from '../../../utils/withIntlAsProp';
import { connect } from 'unistore/preact';
import Select from 'react-select';
import { RequestStatus } from '../../../utils/consts';
+import { SceneListWithDragAndDrop } from '../../drag-and-drop/SceneListWithDragAndDrop';
class EditSceneBox extends Component {
updateScenes = selectedSceneOptions => {
@@ -22,6 +24,33 @@ class EditSceneBox extends Component {
});
};
+ updateCustomOrder = e => {
+ const customOrder = e.target.checked;
+ const newBoxConfig = {
+ scene_custom_order: customOrder
+ };
+ if (customOrder) {
+ // The currently displayed (alphabetical) order becomes
+ // the starting point of the custom order.
+ newBoxConfig.scenes = (this.state.selectedSceneOptions || []).map(option => option.value);
+ }
+ this.props.updateBoxConfig(this.props.x, this.props.y, newBoxConfig);
+ };
+
+ moveScene = (currentIndex, newIndex) => {
+ const scenes = (this.state.selectedSceneOptions || []).map(option => option.value);
+ const movedScene = scenes[currentIndex];
+ const scenesWithoutMovedScene = update(scenes, {
+ $splice: [[currentIndex, 1]]
+ });
+ const newScenes = update(scenesWithoutMovedScene, {
+ $splice: [[newIndex, 0, movedScene]]
+ });
+ this.props.updateBoxConfig(this.props.x, this.props.y, {
+ scenes: newScenes
+ });
+ };
+
getScenes = async () => {
try {
this.setState({ status: RequestStatus.Getting });
@@ -54,11 +83,22 @@ class EditSceneBox extends Component {
refreshSelectedOptions = async props => {
const selectedSceneOptions = [];
if (this.state.sceneOptions) {
- this.state.sceneOptions.forEach(sceneOption => {
- if (props.box.scenes && props.box.scenes.indexOf(sceneOption.value) !== -1) {
- selectedSceneOptions.push(sceneOption);
- }
- });
+ if (props.box.scene_custom_order && props.box.scenes) {
+ // The scenes are displayed in the order chosen by the user
+ props.box.scenes.forEach(sceneSelector => {
+ const sceneOption = this.state.sceneOptions.find(option => option.value === sceneSelector);
+ if (sceneOption) {
+ selectedSceneOptions.push(sceneOption);
+ }
+ });
+ } else {
+ // By default, the scenes are displayed in alphabetical order
+ this.state.sceneOptions.forEach(sceneOption => {
+ if (props.box.scenes && props.box.scenes.indexOf(sceneOption.value) !== -1) {
+ selectedSceneOptions.push(sceneOption);
+ }
+ });
+ }
}
await this.setState({ selectedSceneOptions });
};
@@ -68,14 +108,17 @@ class EditSceneBox extends Component {
};
componentWillReceiveProps(nextProps) {
- if (nextProps.box && nextProps.box.scenes) {
- if (!this.props.box || !this.props.box.scenes || nextProps.box.scenes !== this.props.box.scenes) {
+ const currentBox = this.props.box || {};
+ const nextBox = nextProps.box || {};
+ if (nextBox.scenes) {
+ if (nextBox.scenes !== currentBox.scenes || nextBox.scene_custom_order !== currentBox.scene_custom_order) {
this.refreshSelectedOptions(nextProps);
}
}
}
render(props, { status, selectedSceneOptions, sceneOptions }) {
const loading = status === RequestStatus.Getting && !status;
+ const displayScenesOrder = props.box.scene_custom_order && selectedSceneOptions && selectedSceneOptions.length > 0;
return (
@@ -112,6 +155,33 @@ class EditSceneBox extends Component {
/>
)}
+ {sceneOptions && (
+
+
+
+
+
+
+ )}
+ {displayScenesOrder && (
+
+
+
+
+ )}
diff --git a/front/src/components/boxs/scene/SceneBox.jsx b/front/src/components/boxs/scene/SceneBox.jsx
index 1de708fac9..3289ec39c9 100644
--- a/front/src/components/boxs/scene/SceneBox.jsx
+++ b/front/src/components/boxs/scene/SceneBox.jsx
@@ -111,9 +111,25 @@ class SceneBoxComponent extends Component {
}
}
+ // The API returns the scenes sorted alphabetically.
+ // If the user chose a custom order in the widget configuration,
+ // the scenes are displayed in the order of the box configuration.
+ getOrderedScenes = scenes => {
+ const { box } = this.props;
+ if (!scenes || !box.scene_custom_order || !box.scenes) {
+ return scenes;
+ }
+ const getScenePosition = selector => {
+ const position = box.scenes.indexOf(selector);
+ return position === -1 ? box.scenes.length : position;
+ };
+ return [...scenes].sort((a, b) => getScenePosition(a.selector) - getScenePosition(b.selector));
+ };
+
render(props, { scenes, status, runningScenes, now }) {
const boxTitle = props.box.name;
const loading = status === RequestStatus.Getting && !status;
+ const orderedScenes = this.getOrderedScenes(scenes);
return (
@@ -132,8 +148,8 @@ class SceneBoxComponent extends Component {
- {scenes &&
- scenes.map(scene => (
+ {orderedScenes &&
+ orderedScenes.map(scene => (
{
+ const ref = useRef(null);
+ const [{ isDragging }, drag, preview] = useDrag(
+ () => ({
+ type: SCENE_TYPE,
+ item: () => {
+ return { index };
+ },
+ collect: monitor => ({
+ isDragging: !!monitor.isDragging()
+ })
+ }),
+ // the rows have a stable key, so a row keeps its instance when the order changes:
+ // the drag spec must be re-created when the index of the row changes
+ [index]
+ );
+ const [{ isActive }, drop] = useDrop({
+ accept: SCENE_TYPE,
+ collect: monitor => ({
+ isActive: monitor.canDrop() && monitor.isOver()
+ }),
+ drop(item) {
+ if (!ref.current) {
+ return;
+ }
+ moveScene(item.index, index);
+ }
+ });
+ preview(drop(ref));
+
+ return (
+
+ );
+};
+
+const { backend: dragAndDropBackend, options: dragAndDropBackendOptions } = getDragAndDropBackend();
+
+const SceneListWithDragAndDrop = ({ selectedSceneOptions, moveScene }) => (
+
+ {selectedSceneOptions.map((selectedSceneOption, index) => (
+
+ ))}
+
+);
+
+export { SceneListWithDragAndDrop };
diff --git a/front/src/components/drag-and-drop/style.css b/front/src/components/drag-and-drop/style.css
index abcb9ce1a4..c3ee0245a4 100644
--- a/front/src/components/drag-and-drop/style.css
+++ b/front/src/components/drag-and-drop/style.css
@@ -46,4 +46,24 @@
.deviceListRemoveButton {
z-index: 0;
+}
+
+.sceneListDragAndDrop {
+ cursor: pointer;
+ user-select: none;
+}
+
+.sceneListDragAndDropDragging {
+ opacity: 0.5;
+}
+
+.sceneListDragAndDropActive {
+ background-color: #ecf0f1;
+}
+
+.sceneListDragAndDropLabel {
+ height: auto;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
\ No newline at end of file
diff --git a/front/src/config/i18n/de.json b/front/src/config/i18n/de.json
index d44f29a73c..eaee822006 100644
--- a/front/src/config/i18n/de.json
+++ b/front/src/config/i18n/de.json
@@ -610,7 +610,10 @@
"scene": {
"editNameLabel": "Widget-Name (optional)",
"editNamePlaceholder": "Name auf dem Dashboard angezeigt",
- "editSceneLabel": "Wähle die Szene aus, die hier angezeigt werden soll."
+ "editSceneLabel": "Wähle die Szene aus, die hier angezeigt werden soll.",
+ "customOrderLabel": "Benutzerdefinierte Reihenfolge",
+ "customOrderDescription": "Szenen werden standardmäßig in alphabetischer Reihenfolge angezeigt. Aktiviere diese Option, um sie nach deinen Wünschen zu sortieren.",
+ "orderSceneLabel": "Ziehe die Szenen per Drag & Drop, um ihre Reihenfolge zu ändern"
},
"link": {
"editTitleLabel": "Titel",
diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json
index 45877e8876..21ba148908 100644
--- a/front/src/config/i18n/en.json
+++ b/front/src/config/i18n/en.json
@@ -629,7 +629,10 @@
"scene": {
"editNameLabel": "Widget Name (optional)",
"editNamePlaceholder": "Name displayed on the dashboard",
- "editSceneLabel": "Select the scene you want to display here."
+ "editSceneLabel": "Select the scene you want to display here.",
+ "customOrderLabel": "Custom order",
+ "customOrderDescription": "By default, scenes are displayed in alphabetical order. Enable this option to sort them the way you want.",
+ "orderSceneLabel": "Drag and drop the scenes to change their order"
},
"link": {
"editTitleLabel": "Title",
diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json
index 729f9b9335..8d6a8f6420 100644
--- a/front/src/config/i18n/fr.json
+++ b/front/src/config/i18n/fr.json
@@ -595,7 +595,10 @@
"scene": {
"editNameLabel": "Nom du widget (optionnel)",
"editNamePlaceholder": "Nom affiché sur le tableau de bord",
- "editSceneLabel": "Sélectionnez la scène que vous souhaitez afficher ici."
+ "editSceneLabel": "Sélectionnez la scène que vous souhaitez afficher ici.",
+ "customOrderLabel": "Ordre personnalisé",
+ "customOrderDescription": "Par défaut, les scènes sont affichées par ordre alphabétique. Activez cette option pour les trier comme vous le souhaitez.",
+ "orderSceneLabel": "Glissez-déposez les scènes pour modifier leur ordre"
},
"link": {
"editTitleLabel": "Titre",
diff --git a/server/models/dashboard.js b/server/models/dashboard.js
index c8be178723..b825cf8c23 100644
--- a/server/models/dashboard.js
+++ b/server/models/dashboard.js
@@ -34,6 +34,9 @@ const boxesSchema = Joi.array().items(
camera_latency: Joi.string(),
camera_live_auto_start: Joi.boolean(),
scenes: Joi.array().items(Joi.string()),
+ // scene box: when true, the scenes are displayed in the order of the "scenes" array
+ // instead of the default alphabetical order
+ scene_custom_order: Joi.boolean(),
humidity_use_custom_value: Joi.boolean(),
humidity_min: Joi.number(),
humidity_max: Joi.number(),
diff --git a/server/test/lib/dashboard/dashboard.create.test.js b/server/test/lib/dashboard/dashboard.create.test.js
index 37ce0bb9bd..4b450e6a14 100644
--- a/server/test/lib/dashboard/dashboard.create.test.js
+++ b/server/test/lib/dashboard/dashboard.create.test.js
@@ -23,6 +23,28 @@ describe('dashboard.create', () => {
// selector should be the slug of the name + a dash + 4 random characters
expect(newDashboard.selector).to.match(/^my-new-dashboard-[a-z0-9]{4}$/);
});
+ it('should create a dashboard with a scene box with a custom scene order', async () => {
+ const newDashboard = await dashboard.create('0cd30aef-9c4e-4a23-88e3-3547971296e5', {
+ name: 'My dashboard with a scene box',
+ type: DASHBOARD_TYPE.MAIN,
+ position: 0,
+ visibility: DASHBOARD_VISIBILITY.PRIVATE,
+ boxes: [
+ [
+ {
+ type: DASHBOARD_BOX_TYPE.SCENE,
+ scenes: ['my-second-scene', 'my-first-scene'],
+ scene_custom_order: true,
+ },
+ ],
+ ],
+ });
+ expect(newDashboard.boxes[0][0]).to.deep.equal({
+ type: DASHBOARD_BOX_TYPE.SCENE,
+ scenes: ['my-second-scene', 'my-first-scene'],
+ scene_custom_order: true,
+ });
+ });
it('should create a dashboard with the selector given', async () => {
const newDashboard = await dashboard.create('0cd30aef-9c4e-4a23-88e3-3547971296e5', {
name: 'My dashboard with a custom selector',