diff --git a/front/cypress/e2e/routes/scene/Scene.cy.js b/front/cypress/e2e/routes/scene/Scene.cy.js
index cfe135bd5e..2e735ec28a 100644
--- a/front/cypress/e2e/routes/scene/Scene.cy.js
+++ b/front/cypress/e2e/routes/scene/Scene.cy.js
@@ -1,41 +1,7 @@
describe('Scene view', () => {
- before(() => {
- cy.login();
- const serverUrl = Cypress.env('serverUrl');
- cy.request({
- method: 'GET',
- url: `${serverUrl}/api/v1/room`
- }).then(res => {
- const device = {
- name: 'One device',
- external_id: 'one-device',
- selector: 'one-device',
- room_id: res.body[0].id,
- features: [
- {
- name: 'Multilevel',
- category: 'light',
- type: 'temperature',
- external_id: 'light-temperature',
- selector: 'light-temperature',
- read_only: false,
- keep_history: true,
- has_feedback: true,
- min: 0,
- max: 1
- }
- ]
- };
- cy.createDevice(device, 'mqtt');
- });
- });
beforeEach(() => {
cy.login();
});
- after(() => {
- // Delete all Bluetooth devices
- cy.deleteDevices('mqtt');
- });
it('Should create new scene', () => {
cy.visit('/dashboard/scene');
cy.contains('scene.newButton')
@@ -57,14 +23,23 @@ describe('Scene view', () => {
cy.url().should('eq', `${Cypress.config().baseUrl}/dashboard/scene/my-scene`);
});
- it('Should edit the scene description', () => {
+ it('Should edit the scene settings', () => {
cy.visit('/dashboard/scene/my-scene');
- cy.contains('editScene.editDescriptionPlaceholder').click();
+ cy.get('div[class*="card-header"]')
+ .contains('editScene.settings')
+ .should('have.class', 'card-title')
+ .click();
- cy.get('input:visible').then(inputs => {
- // Zone name
- cy.wrap(inputs[0]).type('My scene description');
+ cy.get('div[class*="form-group"]').then(inputs => {
+ cy.wrap(inputs[0])
+ .find('input')
+ .clear()
+ .type('My scene name');
+ cy.wrap(inputs[1])
+ .find('input')
+ .type('My scene description');
+ cy.wrap(inputs[2]).type('My tag 1{enter}{enter}');
});
// I don't know why, but I'm unable to get this button with
@@ -73,6 +48,7 @@ describe('Scene view', () => {
cy.wrap(buttons[0]).click();
});
});
+
it('Should add new condition house empty', () => {
cy.visit('/dashboard/scene/my-scene');
cy.contains('editScene.addActionButton')
@@ -81,12 +57,14 @@ describe('Scene view', () => {
const i18n = Cypress.env('i18n');
- cy.get('div[class*="-control"]')
- .click(0, 0, { force: true })
- .get('[class*="-menu"]')
- .find('[class*="-option"]')
- .filter(`:contains("${i18n.editScene.actions.house['is-empty']}")`)
- .click(0, 0, { force: true });
+ cy.get('div[class*="-control"]').then(inputs => {
+ cy.wrap(inputs[1])
+ .click(0, 0, { force: true })
+ .get('[class*="-menu"]')
+ .find('[class*="-option"]')
+ .filter(`:contains("${i18n.editScene.actions.house['is-empty']}")`)
+ .click(0, 0, { force: true });
+ });
// I don't know why, but I'm unable to get this button with
// the text. Using the class but it's not recommended otherwise!!
@@ -94,14 +72,56 @@ describe('Scene view', () => {
cy.wrap(buttons[1]).click();
});
- cy.get('div[class*="-control"]')
- .click(0, 0, { force: true })
- .get('[class*="-menu"]')
- .find('[class*="-option"]')
- .filter(`:contains("My House")`)
- .click(0, 0, { force: true });
+ cy.get('div[class*="-control"]').then(inputs => {
+ cy.wrap(inputs[1])
+ .click(0, 0, { force: true })
+ .get('[class*="-menu"]')
+ .find('[class*="-option"]')
+ .filter(`:contains("My House")`)
+ .click(0, 0, { force: true });
+ });
});
+
it('Should add new condition device set value', () => {
+ const serverUrl = Cypress.env('serverUrl');
+ cy.intercept(
+ {
+ method: 'GET',
+ url: `${serverUrl}/api/v1/room?expand=devices`
+ },
+ [
+ {
+ id: 'd63ce677-f5f8-47e1-816d-7aa227c863e4',
+ house_id: '6c1c78f0-1c26-4944-9149-77188e25d00d',
+ name: 'Living Room',
+ selector: 'living-room',
+ created_at: '2023-10-03T12:21:39.551Z',
+ updated_at: '2023-10-03T12:21:39.551Z',
+ devices: [
+ {
+ name: 'One device',
+ selector: 'one-device',
+ features: [
+ {
+ name: 'Multilevel',
+ selector: 'light-temperature',
+ category: 'light',
+ type: 'temperature',
+ read_only: false,
+ unit: null,
+ min: 0,
+ max: 1,
+ last_value: null,
+ last_value_changed: null
+ }
+ ],
+ service: { id: '123d4d56-6cbd-4020-991f-2a0a8e0ac3e0', name: 'mqtt' }
+ }
+ ]
+ }
+ ]
+ ).as('loadDevices');
+
cy.visit('/dashboard/scene/my-scene');
cy.contains('editScene.addActionButton')
.should('have.class', 'btn-outline-primary')
@@ -109,12 +129,14 @@ describe('Scene view', () => {
const i18n = Cypress.env('i18n');
- cy.get('div[class*="-control"]')
- .click(0, 0, { force: true })
- .get('[class*="-menu"]')
- .find('[class*="-option"]')
- .filter(`:contains("${i18n.editScene.actions.device['set-value']}")`)
- .click(0, 0, { force: true });
+ cy.get('div[class*="-control"]').then(inputs => {
+ cy.wrap(inputs[1])
+ .click(0, 0, { force: true })
+ .get('[class*="-menu"]')
+ .find('[class*="-option"]')
+ .filter(`:contains("${i18n.editScene.actions.device['set-value']}")`)
+ .click(0, 0, { force: true });
+ });
// I don't know why, but I'm unable to get this button with
// the text. Using the class but it's not recommended otherwise!!
@@ -122,13 +144,21 @@ describe('Scene view', () => {
cy.wrap(buttons[1]).click();
});
- cy.get('div[class*="-control"]')
- .click(0, 0, { force: true })
- .get('[class*="-menu"]')
- .find('[class*="-option"]')
- .filter(`:contains("Multilevel")`)
- .click(0, 0, { force: true });
+ cy.wait('@loadDevices');
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
+ cy.wait(100);
+
+ cy.get('div[class*="-control"]').then(inputs => {
+ cy.wrap(inputs[1])
+ .click(0, 0, { force: true })
+ .get('[class*="-menu"]')
+ .find('[class*="-option"]')
+ .filter(`:contains("Multilevel")`)
+ .click(0, 0, { force: true });
+ cy.log('4');
+ });
});
+
it('Should add new calendar event trigger', () => {
cy.visit('/dashboard/scene/my-scene');
cy.contains('editScene.addNewTriggerButton')
@@ -137,12 +167,14 @@ describe('Scene view', () => {
const i18n = Cypress.env('i18n');
- cy.get('div[class*="-control"]')
- .click(0, 0, { force: true })
- .get('[class*="-menu"]')
- .find('[class*="-option"]')
- .filter(`:contains("${i18n.editScene.triggers.calendar['event-is-coming']}")`)
- .click(0, 0, { force: true });
+ cy.get('div[class*="-control"]').then(inputs => {
+ cy.wrap(inputs[1])
+ .click(0, 0, { force: true })
+ .get('[class*="-menu"]')
+ .find('[class*="-option"]')
+ .filter(`:contains("${i18n.editScene.triggers.calendar['event-is-coming']}")`)
+ .click(0, 0, { force: true });
+ });
// I don't know why, but I'm unable to get this button with
// the text. Using the class but it's not recommended otherwise!!
@@ -156,6 +188,7 @@ describe('Scene view', () => {
cy.wrap(selects[2]).select('minute');
});
});
+
it('Should disable scene', () => {
cy.visit('/dashboard/scene');
@@ -202,6 +235,7 @@ describe('Scene view', () => {
cy.url().should('eq', `${Cypress.config().baseUrl}/dashboard/scene/my-duplicated-scene`);
});
+
it('Should delete existing scene', () => {
cy.login();
cy.visit('/dashboard/scene/my-scene');
diff --git a/front/src/actions/createScene.js b/front/src/actions/createScene.js
index bb68f40a00..db969a197f 100644
--- a/front/src/actions/createScene.js
+++ b/front/src/actions/createScene.js
@@ -54,7 +54,8 @@ function createActions(store) {
newScene: {
name: '',
icon: null,
- actions: [[]]
+ actions: [[]],
+ tags: []
},
newSceneErrors: null,
createSceneStatus: null
diff --git a/front/src/components/layout/CardFilter.jsx b/front/src/components/layout/CardFilter.jsx
index be8c67d289..47c9cccb22 100644
--- a/front/src/components/layout/CardFilter.jsx
+++ b/front/src/components/layout/CardFilter.jsx
@@ -11,17 +11,12 @@ const CardFilter = ({ changeOrderDir, orderValue = 'asc', search, searchValue, s
+
-
+
);
diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json
index ea119531f1..96c9bf8274 100644
--- a/front/src/config/i18n/en.json
+++ b/front/src/config/i18n/en.json
@@ -1422,9 +1422,15 @@
}
},
"editScene": {
+ "settings": "Settings",
+ "nameTitle": "Scene name",
"editNamePlaceholder": "Enter a scene name",
"descriptionTitle": "Description",
+ "iconLabel": "Icon",
"editDescriptionPlaceholder": "Enter a scene description",
+ "tagsTitle": "Tags",
+ "editTagsPlaceholder": "Enter a scene tags",
+ "createTag": "Create tag: '{{tagName}}'",
"startButton": "Start",
"saveButton": "Save",
"deleteButton": "Delete",
@@ -2091,7 +2097,8 @@
"newButton": "New",
"editButton": "Edit",
"startButton": "Start",
- "searchPlaceholder": "Search scenes"
+ "searchPlaceholder": "Search scenes",
+ "filterTagsName": "Filter by tags"
},
"gateway": {
"instanceConfiguredTitle": "Gladys Plus",
diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json
index 567b38fb1f..1e888344e5 100644
--- a/front/src/config/i18n/fr.json
+++ b/front/src/config/i18n/fr.json
@@ -1423,9 +1423,15 @@
}
},
"editScene": {
+ "settings": "Configuration",
+ "nameTitle": "Nom de scène",
"editNamePlaceholder": "Entrez un nom de scène",
"descriptionTitle": "Description",
+ "iconLabel": "Icône",
"editDescriptionPlaceholder": "Entrez une description pour la scène",
+ "createTag": "Créer le tag : '{{tagName}}'",
+ "tagsTitle": "Tags",
+ "editTagsPlaceholder": "Entrez un tag pour la scène",
"startButton": "Démarrer",
"saveButton": "Sauvegarder",
"deleteButton": "Supprimer",
@@ -2092,7 +2098,8 @@
"newButton": "Nouveau",
"editButton": "Editer",
"startButton": "Démarrer",
- "searchPlaceholder": "Chercher une scène"
+ "searchPlaceholder": "Chercher une scène",
+ "filterTagsName": "Filtrer par tags"
},
"gateway": {
"instanceConfiguredTitle": "Gladys Plus",
diff --git a/front/src/routes/scene/SceneCard.jsx b/front/src/routes/scene/SceneCard.jsx
index 6e20e17d04..b4fd0fb058 100644
--- a/front/src/routes/scene/SceneCard.jsx
+++ b/front/src/routes/scene/SceneCard.jsx
@@ -3,6 +3,7 @@ import { Component } from 'preact';
import { Link } from 'preact-router/match';
import cx from 'classnames';
import style from './style.css';
+import { MAX_LENGTH_TAG } from './constant';
class SceneCard extends Component {
startScene = async () => {
@@ -34,7 +35,7 @@ class SceneCard extends Component {
-
+
@@ -53,6 +54,16 @@ class SceneCard extends Component {
{props.scene.name}
{props.scene.description}
+
+ {props.scene.tags &&
+ props.scene.tags.map(tag => (
+
+ {tag.name.length > MAX_LENGTH_TAG
+ ? `${tag.name.substring(0, MAX_LENGTH_TAG - 3)}...`
+ : tag.name}
+
+ ))}
+
diff --git a/front/src/routes/scene/SceneTagFilter.jsx b/front/src/routes/scene/SceneTagFilter.jsx
new file mode 100644
index 0000000000..e7bc39bfc2
--- /dev/null
+++ b/front/src/routes/scene/SceneTagFilter.jsx
@@ -0,0 +1,113 @@
+import { Component } from 'preact';
+import cx from 'classnames';
+import { Text } from 'preact-i18n';
+import { MAX_LENGTH_TAG } from './constant';
+
+class SceneTagFilter extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ tagFilterDropdownOpened: false
+ };
+ }
+
+ setDropdownRef = dropdownRef => {
+ this.dropdownRef = dropdownRef;
+ };
+
+ componentWillReceiveProps(nextProps) {
+ let tagsStatus = {};
+ if (nextProps.tags) {
+ tagsStatus = nextProps.tags.reduce(
+ (tags, tag) => ({
+ ...tags,
+ [tag.name]: false
+ }),
+ {}
+ );
+ }
+ if (nextProps.sceneTagSearch) {
+ nextProps.sceneTagSearch.forEach(tagName => {
+ tagsStatus[tagName] = true;
+ });
+ }
+ this.setState({
+ tagsStatus
+ });
+ }
+
+ toggleTagFilterDropdown = () => {
+ this.setState({
+ tagFilterDropdownOpened: !this.state.tagFilterDropdownOpened
+ });
+ };
+
+ closeTagFilterDropdown = e => {
+ if (this.dropdownRef && this.dropdownRef.contains(e.target)) {
+ return;
+ }
+ this.setState({ tagFilterDropdownOpened: false });
+ };
+
+ selectedTags = async tagName => {
+ await this.setState({
+ tagsStatus: {
+ ...this.state.tagsStatus,
+ [tagName]: !this.state.tagsStatus[tagName]
+ }
+ });
+ const selectedTags = Object.keys(this.state.tagsStatus).filter(tagName => this.state.tagsStatus[tagName]);
+ this.props.searchTags(selectedTags);
+ };
+
+ componentDidMount() {
+ document.addEventListener('click', this.closeTagFilterDropdown, true);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('click', this.closeTagFilterDropdown, true);
+ }
+
+ render(props, { tagFilterDropdownOpened, tagsStatus }) {
+ return (
+
+
+
+
+ );
+ }
+}
+
+export default SceneTagFilter;
diff --git a/front/src/routes/scene/constant.js b/front/src/routes/scene/constant.js
new file mode 100644
index 0000000000..8416093cf8
--- /dev/null
+++ b/front/src/routes/scene/constant.js
@@ -0,0 +1,3 @@
+const MAX_LENGTH_TAG = 30;
+
+module.exports.MAX_LENGTH_TAG = MAX_LENGTH_TAG;
diff --git a/front/src/routes/scene/edit-scene/EditScenePage.jsx b/front/src/routes/scene/edit-scene/EditScenePage.jsx
index faa20abfca..9526285b5f 100644
--- a/front/src/routes/scene/edit-scene/EditScenePage.jsx
+++ b/front/src/routes/scene/edit-scene/EditScenePage.jsx
@@ -1,4 +1,3 @@
-import { Text, Localizer } from 'preact-i18n';
import update from 'immutability-helper';
import cx from 'classnames';
@@ -7,6 +6,8 @@ import ActionGroup from './ActionGroup';
import SceneActionsDropdown from './SceneActionsDropdown';
import TriggerGroup from './TriggerGroup';
import style from './style.css';
+import Settings from './Settings';
+import { Text } from 'preact-i18n';
const ACTION_CARD_TYPE = 'ACTION_CARD_TYPE';
const ACTION_GROUP_TYPE = 'ACTION_GROUP_TYPE';
@@ -21,87 +22,31 @@ const EditScenePage = ({ children, ...props }) => (
+
-
)}
+
+
+
+
(
+
{props.scene.actions.map((parallelActions, index) => (
diff --git a/front/src/routes/scene/edit-scene/Settings.jsx b/front/src/routes/scene/edit-scene/Settings.jsx
new file mode 100644
index 0000000000..ff47c36437
--- /dev/null
+++ b/front/src/routes/scene/edit-scene/Settings.jsx
@@ -0,0 +1,138 @@
+import cx from 'classnames';
+import { Localizer, Text } from 'preact-i18n';
+import CreatableSelect from 'react-select/creatable';
+import { Component } from 'preact';
+import styles from './style.css';
+import iconList from '../../../../../server/config/icons.json';
+
+class Settings extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ cardOpened: false
+ };
+ }
+
+ openCloseCard = () => {
+ this.setState({
+ cardOpened: !this.state.cardOpened
+ });
+ };
+
+ render(props, { cardOpened }) {
+ return (
+
+ );
+ }
+}
+
+export default Settings;
diff --git a/front/src/routes/scene/edit-scene/index.js b/front/src/routes/scene/edit-scene/index.js
index af441bf5ec..406bcafa1a 100644
--- a/front/src/routes/scene/edit-scene/index.js
+++ b/front/src/routes/scene/edit-scene/index.js
@@ -92,7 +92,6 @@ class EditScene extends Component {
this.setState({ saving: true, error: false });
try {
await this.props.httpClient.patch(`/api/v1/scene/${this.props.scene_selector}`, this.state.scene);
- this.setState({ isNameEditable: false, isDescriptionEditable: false });
} catch (e) {
console.error(e);
this.setState({ error: true });
@@ -220,6 +219,7 @@ class EditScene extends Component {
});
}, 500);
};
+
deleteScene = async () => {
this.setState({ saving: true });
try {
@@ -309,35 +309,6 @@ class EditScene extends Component {
});
};
- toggleIsNameEditable = async () => {
- await this.setState(prevState => ({ isNameEditable: !prevState.isNameEditable, isDescriptionEditable: false }));
- if (this.state.isNameEditable) {
- this.nameInput.focus();
- }
- };
-
- setNameInputRef = nameInput => {
- this.nameInput = nameInput;
- };
-
- toggleIsDescriptionEditable = async () => {
- await this.setState(prevState => ({
- isDescriptionEditable: !prevState.isDescriptionEditable,
- isNameEditable: false
- }));
- if (this.state.isDescriptionEditable) {
- this.descriptionInput.focus();
- }
- };
-
- closeEdition = () => {
- this.setState({ isNameEditable: false, isDescriptionEditable: false });
- };
-
- setDescriptionInputRef = descriptionInput => {
- this.descriptionInput = descriptionInput;
- };
-
updateSceneName = e => {
this.setState(prevState => {
const newState = update(prevState, {
@@ -364,9 +335,24 @@ class EditScene extends Component {
});
};
+ updateSceneIcon = e => {
+ console.log('updateSceneIcon', e.target.value);
+ this.setState(prevState => {
+ const newState = update(prevState, {
+ scene: {
+ icon: {
+ $set: e.target.value
+ }
+ }
+ });
+ return newState;
+ });
+ };
+
duplicateScene = () => {
route(`/dashboard/scene/${this.props.scene_selector}/duplicate`);
};
+
moveCard = async (originalX, originalY, destX, destY) => {
// incorrect coordinates
if (destX < 0 || destY < 0) {
@@ -445,20 +431,43 @@ class EditScene extends Component {
await this.setState(newState);
};
+ setTags = tags => {
+ this.setState(prevState => {
+ const newState = update(prevState, {
+ scene: {
+ tags: {
+ $set: tags.map(tag => ({ name: tag }))
+ }
+ }
+ });
+ return newState;
+ });
+ };
+
+ getTags = async () => {
+ try {
+ const tags = await this.props.httpClient.get(`/api/v1/tag_scene`);
+ this.setState({
+ tags
+ });
+ } catch (e) {
+ console.error(e);
+ }
+ };
+
constructor(props) {
super(props);
this.isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
this.state = {
scene: null,
variables: {},
- triggersVariables: [],
- isNameEditable: false
+ triggersVariables: []
};
}
componentDidMount() {
- document.addEventListener('click', this.closeEdition, true);
this.getSceneBySelector();
+ this.getTags();
this.props.session.dispatcher.addListener('scene.executing-action', payload =>
this.highlighCurrentlyExecutedAction(payload)
);
@@ -471,43 +480,42 @@ class EditScene extends Component {
document.removeEventListener('click', this.closeEdition, true);
}
- render(props, { saving, error, variables, scene, isNameEditable, isDescriptionEditable, triggersVariables }) {
+ render(props, { saving, error, variables, scene, triggersVariables, tags }) {
return (
scene && (
-
-
-
+
+
+
+
+
)
);
}
diff --git a/front/src/routes/scene/edit-scene/style.css b/front/src/routes/scene/edit-scene/style.css
index 6843b1df63..956e51c66e 100644
--- a/front/src/routes/scene/edit-scene/style.css
+++ b/front/src/routes/scene/edit-scene/style.css
@@ -31,3 +31,47 @@
left: auto;
}
}
+
+
+.settings {
+ opacity: 0;
+ max-height: 0;
+ visibility: hidden;
+ transition: opacity 0.3s ease, max-height 0.3s ease;
+ padding: 0rem
+}
+
+.settingsOpen {
+ visibility: visible;
+ opacity: 1;
+ max-height: 1000px;
+ padding: 1.5rem 1.5rem;
+}
+
+.iconContainer {
+ margin-top: 1rem;
+ height: 10rem;
+ overflow: scroll;
+}
+
+.iconDiv {
+ padding: 5px;
+ width: 35px;
+ margin-bottom: 8px;
+}
+
+.iconDivChecked {
+ background-color: #f5f7fb;
+ border-radius: 4px;
+}
+
+.iconLabel {
+ cursor: pointer;
+ margin-bottom: 0;
+}
+
+.iconInput {
+ position: absolute;
+ z-index: -1;
+ opacity: 0;
+}
diff --git a/front/src/routes/scene/index.js b/front/src/routes/scene/index.js
index ee6b54fff3..b695a7b59d 100644
--- a/front/src/routes/scene/index.js
+++ b/front/src/routes/scene/index.js
@@ -18,6 +18,9 @@ class Scene extends Component {
if (this.state.sceneSearch && this.state.sceneSearch.length) {
params.search = this.state.sceneSearch;
}
+ if (this.state.sceneTagSearch && this.state.sceneTagSearch.length) {
+ params.searchTags = this.state.sceneTagSearch.join(',');
+ }
const scenes = await this.props.httpClient.get('/api/v1/scene', params);
this.setState({
scenes,
@@ -31,12 +34,29 @@ class Scene extends Component {
});
}
};
+ getTags = async () => {
+ try {
+ const tags = await this.props.httpClient.get(`/api/v1/tag_scene`);
+ this.setState({
+ tags
+ });
+ } catch (e) {
+ console.error(e);
+ }
+ };
search = async e => {
await this.setState({
sceneSearch: e.target.value
});
await this.getScenes();
};
+ searchTags = async tags => {
+ await this.setState({
+ sceneTagSearch: tags
+ });
+ await this.getScenes();
+ };
+
changeOrderDir = async e => {
await this.setState({
getScenesOrderDir: e.target.value
@@ -88,6 +108,7 @@ class Scene extends Component {
getScenesOrderDir: 'asc',
scenes: [],
sceneSearch: null,
+ sceneTagSearch: null,
loading: true
};
this.debouncedSearch = debounce(this.search.bind(this), 200);
@@ -95,9 +116,10 @@ class Scene extends Component {
componentWillMount() {
this.getScenes();
+ this.getTags();
}
- render(props, { scenes, loading, getError }) {
+ render(props, { scenes, loading, getError, tags, sceneTagSearch }) {
return (
);
}
diff --git a/front/src/routes/scene/new-scene/NewScenePage.jsx b/front/src/routes/scene/new-scene/NewScenePage.jsx
index e8fde77d66..14290b7bf2 100644
--- a/front/src/routes/scene/new-scene/NewScenePage.jsx
+++ b/front/src/routes/scene/new-scene/NewScenePage.jsx
@@ -42,6 +42,7 @@ const NewScenePage = ({ children, ...props }) => (
+