diff --git a/front/cypress/e2e/routes/dashboard/Dashboard.cy.js b/front/cypress/e2e/routes/dashboard/Dashboard.cy.js index bf9b464e0d..3a3e4139e1 100644 --- a/front/cypress/e2e/routes/dashboard/Dashboard.cy.js +++ b/front/cypress/e2e/routes/dashboard/Dashboard.cy.js @@ -35,10 +35,10 @@ describe('Dashboard', () => { cy.contains('dashboard.editDashboardButton') .should('have.class', 'btn-outline-primary') .click(); - cy.contains('dashboard.editDashboardDeleteButton') + cy.contains('button', 'dashboard.editDashboardDeleteButton') .should('have.class', 'btn-outline-danger') .click(); - cy.contains('dashboard.editDashboardDeleteButton') + cy.contains('button', 'dashboard.editDashboardDeleteButton') .should('have.class', 'btn-outline-danger') .click(); cy.url().should('eq', `${Cypress.config().baseUrl}/dashboard`); diff --git a/front/src/components/app.jsx b/front/src/components/app.jsx index a137bc1776..41e4219bfc 100644 --- a/front/src/components/app.jsx +++ b/front/src/components/app.jsx @@ -46,6 +46,7 @@ import SignupSuccess from '../routes/signup/5-success'; import Dashboard from '../routes/dashboard'; import NewDashboard from '../routes/dashboard/new-dashboard'; import EditDashboard from '../routes/dashboard/edit-dashboard'; +import DuplicateDashboard from '../routes/dashboard/duplicate-dashboard'; import IntegrationPage from '../routes/integration'; import DevicesListPage from '../routes/devices'; @@ -266,6 +267,7 @@ const AppRouter = connect( + diff --git a/front/src/config/i18n/de.json b/front/src/config/i18n/de.json index 4703487ea2..d373e6cf9e 100644 --- a/front/src/config/i18n/de.json +++ b/front/src/config/i18n/de.json @@ -291,6 +291,7 @@ "disableFullScreen": "Vollbild beenden", "editDashboardTitle": "Dashboard bearbeiten", "editDashboardDeleteButton": "Löschen", + "editDashboardDuplicateButton": "Duplizieren", "editDashboardCancelButton": "Abbrechen", "editDashboardDeleteText": "Bist du sicher, dass du dieses Dashboard löschen möchtest?", "toggleDefineTabletMode": "Tablet-Modus", @@ -4007,6 +4008,17 @@ "invalidIcon": "Symbol ist erforderlich", "sceneAlreadyExist": "Eine Szene mit diesem Namen existiert bereits." }, + "duplicateDashboard": { + "cardTitle": "Dashboard \"{{name}}\" duplizieren", + "nameLabel": "Name", + "namePlaceholder": "Gib einen neuen Namen ein", + "duplicateDashboardButton": "Duplizieren", + "invalidName": "Name ist erforderlich", + "dashboardAlreadyExist": "Ein Dashboard mit diesem Namen existiert bereits.", + "unknownError": "Ein unbekannter Fehler ist aufgetreten. Bitte versuche es erneut!", + "nameAfterCopy": "Kopie von {{name}}", + "savedVersionInfo": "Die Kopie wird von der zuletzt gespeicherten Version dieses Dashboards erstellt. Nicht gespeicherte Änderungen werden nicht kopiert." + }, "duplicateScene": { "cardTitle": "Szene \"{{name}}\" duplizieren", "nameLabel": "Name", diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json index 3080417573..427de90fea 100644 --- a/front/src/config/i18n/en.json +++ b/front/src/config/i18n/en.json @@ -291,6 +291,7 @@ "disableFullScreen": "Exit full screen", "editDashboardTitle": "Edit dashboard", "editDashboardDeleteButton": "Delete", + "editDashboardDuplicateButton": "Duplicate", "editDashboardCancelButton": "Cancel", "editDashboardDeleteText": "Are you sure you want to delete this dashboard?", "toggleDefineTabletMode": "Tablet Mode", @@ -4007,6 +4008,17 @@ "invalidIcon": "Icon is required", "sceneAlreadyExist": "A scene with that name already exists." }, + "duplicateDashboard": { + "cardTitle": "Duplicate dashboard \"{{name}}\"", + "nameLabel": "Name", + "namePlaceholder": "Enter a new name", + "duplicateDashboardButton": "Duplicate", + "invalidName": "Name is required", + "dashboardAlreadyExist": "A dashboard with this name already exists.", + "unknownError": "An unknown error occurred. Please retry!", + "nameAfterCopy": "Copy of {{name}}", + "savedVersionInfo": "The copy is made from the last saved version of this dashboard. Any change not saved yet will not be copied." + }, "duplicateScene": { "cardTitle": "Duplicate scene \"{{name}}\"", "nameLabel": "Name", diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json index 152327809a..b4a3bbee4c 100644 --- a/front/src/config/i18n/fr.json +++ b/front/src/config/i18n/fr.json @@ -289,6 +289,7 @@ "newDashboardButton": "Nouveau", "editDashboardCancelButton": "Annuler", "editDashboardDeleteButton": "Supprimer", + "editDashboardDuplicateButton": "Dupliquer", "editDashboardSaveButton": "Sauvegarder", "editDashboardDeleteText": "Êtes-vous sûr de vouloir supprimer ce tableau de bord ?", "toggleDefineTabletMode": "Mode tablette", @@ -4007,6 +4008,17 @@ "invalidIcon": "L'icône est requise", "sceneAlreadyExist": "Une scène avec le même nom existe déjà." }, + "duplicateDashboard": { + "cardTitle": "Dupliquer le tableau de bord \"{{name}}\"", + "nameLabel": "Nom", + "namePlaceholder": "Entrez un nouveau nom", + "duplicateDashboardButton": "Dupliquer", + "invalidName": "Le nom est requis", + "dashboardAlreadyExist": "Un tableau de bord avec le même nom existe déjà.", + "unknownError": "Une erreur inconnue est survenue. Veuillez réessayer !", + "nameAfterCopy": "Copie de {{name}}", + "savedVersionInfo": "La copie est réalisée à partir de la dernière version enregistrée de ce tableau de bord. Les modifications non enregistrées ne seront pas copiées." + }, "duplicateScene": { "cardTitle": "Dupliquer la scène \"{{name}}\"", "nameLabel": "Nom", diff --git a/front/src/routes/dashboard/duplicate-dashboard/DuplicateDashboardPage.jsx b/front/src/routes/dashboard/duplicate-dashboard/DuplicateDashboardPage.jsx new file mode 100644 index 0000000000..41418e144c --- /dev/null +++ b/front/src/routes/dashboard/duplicate-dashboard/DuplicateDashboardPage.jsx @@ -0,0 +1,75 @@ +import { Text, Localizer } from 'preact-i18n'; +import cx from 'classnames'; +import get from 'get-value'; + +import { RequestStatus } from '../../../utils/consts'; +import style from './style.css'; + +const DuplicateDashboardPage = ({ children, ...props }) => ( +
+ + +
+ +
+); + +export default DuplicateDashboardPage; diff --git a/front/src/routes/dashboard/duplicate-dashboard/index.js b/front/src/routes/dashboard/duplicate-dashboard/index.js new file mode 100644 index 0000000000..c90607d280 --- /dev/null +++ b/front/src/routes/dashboard/duplicate-dashboard/index.js @@ -0,0 +1,130 @@ +import { Component } from 'preact'; +import { connect } from 'unistore/preact'; +import { route } from 'preact-router'; +import get from 'get-value'; + +import DuplicateDashboardPage from './DuplicateDashboardPage'; +import { RequestStatus } from '../../../utils/consts'; +import withIntlAsProp from '../../../utils/withIntlAsProp'; + +class DuplicateDashboard extends Component { + goBack = () => { + route(`/dashboard/${this.props.dashboardSelector}/edit`); + }; + + getSourceDashboard = async () => { + try { + const sourceDashboard = await this.props.httpClient.get(`/api/v1/dashboard/${this.props.dashboardSelector}`); + this.setState({ + sourceDashboard, + loading: false, + dashboard: { + name: get(this.props.intl.dictionary, 'duplicateDashboard.nameAfterCopy').replace( + '{{name}}', + sourceDashboard.name + ) + } + }); + } catch (e) { + console.error(e); + this.setState({ + loading: false, + duplicateDashboardStatus: RequestStatus.Error + }); + } + }; + + checkErrors = (name = this.state.dashboard.name) => { + const duplicateDashboardErrors = {}; + if (!name) { + duplicateDashboardErrors.name = true; + } + this.setState({ + duplicateDashboardErrors + }); + return Object.keys(duplicateDashboardErrors).length > 0; + }; + + updateDuplicateDashboardName = e => { + const { value } = e.target; + this.setState({ + dashboard: { + name: value + } + }); + if (this.state.duplicateDashboardErrors) { + // setState is asynchronous, so we validate the new value directly + this.checkErrors(value); + } + }; + + duplicateDashboard = async e => { + e.preventDefault(); + // if errored, we don't continue + if (this.checkErrors()) { + return; + } + this.setState({ + duplicateDashboardStatus: RequestStatus.Getting + }); + try { + const duplicatedDashboard = await this.props.httpClient.post( + `/api/v1/dashboard/${this.props.dashboardSelector}/duplicate`, + this.state.dashboard + ); + this.setState({ + duplicateDashboardStatus: RequestStatus.Success + }); + route(`/dashboard/${duplicatedDashboard.selector}/edit`); + } catch (e) { + console.error(e); + const status = get(e, 'response.status'); + if (status === 409) { + this.setState({ + duplicateDashboardStatus: RequestStatus.ConflictError + }); + } else { + this.setState({ + duplicateDashboardStatus: RequestStatus.Error + }); + } + } + }; + + constructor(props) { + super(props); + this.state = { + dashboard: { + name: '' + }, + sourceDashboard: { + name: '' + }, + loading: true, + duplicateDashboardErrors: null, + duplicateDashboardStatus: null + }; + } + + componentDidMount() { + this.getSourceDashboard(); + } + + render(props, { dashboard, sourceDashboard, loading, duplicateDashboardErrors, duplicateDashboardStatus }) { + return ( + + ); + } +} + +export default withIntlAsProp(connect('httpClient', {})(DuplicateDashboard)); diff --git a/front/src/routes/dashboard/duplicate-dashboard/style.css b/front/src/routes/dashboard/duplicate-dashboard/style.css new file mode 100644 index 0000000000..f5d0c084cf --- /dev/null +++ b/front/src/routes/dashboard/duplicate-dashboard/style.css @@ -0,0 +1,3 @@ +.containerWithMargin { + margin-top: 3rem; +} diff --git a/front/src/routes/dashboard/edit-dashboard/EditActions.jsx b/front/src/routes/dashboard/edit-dashboard/EditActions.jsx index 0d298a7929..f1b15b9eb3 100644 --- a/front/src/routes/dashboard/edit-dashboard/EditActions.jsx +++ b/front/src/routes/dashboard/edit-dashboard/EditActions.jsx @@ -1,20 +1,59 @@ -import { Text } from 'preact-i18n'; +import { Text, Localizer } from 'preact-i18n'; const EditActions = props => (