Skip to content

Commit

Permalink
Add duplicate scene feature (#1670)
Browse files Browse the repository at this point in the history
  • Loading branch information
callemand committed Dec 18, 2022
1 parent a4eb84f commit 5154c19
Show file tree
Hide file tree
Showing 17 changed files with 461 additions and 2 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@ stats.json
size-plugin.json
# VSCode
.vscode
#Webstorm
.idea/
.tmp
.DS_Store
.DS_Store
32 changes: 32 additions & 0 deletions front/cypress/e2e/routes/scene/Scene.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,30 @@ describe('Scene view', () => {

cy.get('[type="checkbox"]').should('be.checked');
});
it('Should duplicate existing scene', () => {
cy.login();
cy.visit('/dashboard/scene/my-scene');

cy.contains('editScene.duplicateButton')
.should('have.class', 'btn-warning')
.click();

cy.url().should('eq', `${Cypress.config().baseUrl}/dashboard/scene/my-scene/duplicate`);

cy.get('input:visible').then(inputs => {
// Zone name
cy.wrap(inputs[0]).type('My Duplicated scene');
});

cy.get('.fe-activity').click();

cy.get('.form-footer')
.contains('duplicateScene.duplicateSceneButton')
.should('have.class', 'btn-primary')
.click();

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');
Expand All @@ -171,5 +195,13 @@ describe('Scene view', () => {
.click();

cy.url().should('eq', `${Cypress.config().baseUrl}/dashboard/scene`);

cy.visit('/dashboard/scene/my-duplicated-scene');

cy.contains('editScene.deleteButton')
.should('have.class', 'btn-danger')
.click();

cy.url().should('eq', `${Cypress.config().baseUrl}/dashboard/scene`);
});
});
2 changes: 2 additions & 0 deletions front/src/components/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import MapNewAreaPage from '../routes/map/NewArea';
import CalendarPage from '../routes/calendar';
import ScenePage from '../routes/scene';
import NewScenePage from '../routes/scene/new-scene';
import DuplicateScenePage from '../routes/scene/duplicate-scene';
import EditScenePage from '../routes/scene/edit-scene';
import ProfilePage from '../routes/profile';
import SettingsSessionPage from '../routes/settings/settings-session';
Expand Down Expand Up @@ -254,6 +255,7 @@ const AppRouter = connect(
<CalendarPage path="/dashboard/calendar" />
<ScenePage path="/dashboard/scene" />
<NewScenePage path="/dashboard/scene/new" />
<DuplicateScenePage path="/dashboard/scene/:scene_selector/duplicate" />
<EditScenePage path="/dashboard/scene/:scene_selector" />
<ProfilePage path="/dashboard/profile" />
<SettingsSessionPage path="/dashboard/settings/session" />
Expand Down
11 changes: 11 additions & 0 deletions front/src/config/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1042,6 +1042,7 @@
"startButton": "Start",
"saveButton": "Save",
"deleteButton": "Delete",
"duplicateButton": "Duplicate",
"triggersTitle": "Triggers",
"newTrigger": "New trigger",
"addTriggerButton": "Add trigger",
Expand Down Expand Up @@ -1609,6 +1610,16 @@
"invalidIcon": "Icon is required",
"sceneAlreadyExist": "A scene with that name already exist."
},
"duplicateScene": {
"cardTitle": "Duplicate scene \"{{name}}\"",
"nameLabel": "Name",
"namePlaceholder": "Enter a new name",
"iconLabel": "Select an icon for you scene",
"duplicateSceneButton": "Duplicate",
"invalidName": "Name is required",
"invalidIcon": "Icon is required",
"sceneAlreadyExist": "A scene with that name already exist."
},
"scene": {
"title": "Scenes",
"emptySceneSentenceTop": "Can't find any scenes.",
Expand Down
11 changes: 11 additions & 0 deletions front/src/config/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -1042,6 +1042,7 @@
"startButton": "Démarrer",
"saveButton": "Sauvegarder",
"deleteButton": "Supprimer",
"duplicateButton": "Dupliquer",
"triggersTitle": "Déclencheurs",
"newTrigger": "Nouveau déclencheur",
"addTriggerButton": "Ajouter déclencheur",
Expand Down Expand Up @@ -1609,6 +1610,16 @@
"invalidIcon": "L'icône est requise",
"sceneAlreadyExist": "Une scène avec le même nom existe déjà."
},
"duplicateScene": {
"cardTitle": "Dupliquer la scène \"{{name}}\"",
"nameLabel": "Nom",
"namePlaceholder": "Entrez un nouveau nom",
"iconLabel": "Sélectionnez une icône pour votre scène",
"duplicateSceneButton": "Dupliquer",
"invalidName": "Le nom est requis",
"invalidIcon": "L'icône est requise",
"sceneAlreadyExist": "Une scène avec le même nom existe déjà."
},
"scene": {
"title": "Scènes",
"emptySceneSentenceTop": "Impossible de trouver des scènes.",
Expand Down
95 changes: 95 additions & 0 deletions front/src/routes/scene/duplicate-scene/DuplicateScenePage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Text, Localizer } from 'preact-i18n';
import { RequestStatus } from '../../../utils/consts';
import cx from 'classnames';
import get from 'get-value';
import style from './style.css';
import iconList from '../../../../../server/config/icons.json';

const DuplicateScenePage = ({ children, ...props }) => (
<div class={cx('container', style.containerWithMargin)}>
<button onClick={props.goBack} className="btn btn-secondary btn-sm">
<Text id="global.backButton" />
</button>

<div class="row">
<div class="col col-login mx-auto">
<form onSubmit={props.duplicateScene} class="card">
<div class="card-body p-6">
<div class="card-title">
<Text id="duplicateScene.cardTitle" fields={{ name: props.sourceScene.name }} />
</div>
{props.duplicateSceneStatus === RequestStatus.ConflictError && (
<div class="alert alert-danger">
<Text id="duplicateScene.sceneAlreadyExist" />
</div>
)}
<div class="form-group">
<label class="form-label">
<Text id="duplicateScene.nameLabel" />
</label>
<Localizer>
<input
type="text"
class={cx('form-control', {
'is-invalid': get(props, 'duplicateSceneErrors.name')
})}
placeholder={<Text id="duplicateScene.namePlaceholder" />}
value={get(props, 'scene.name')}
onInput={props.updateDuplicateSceneName}
/>
</Localizer>
<div class="invalid-feedback">
<Text id="duplicateScene.invalidName" />
</div>
</div>

<div className="form-group">
<label className="form-label">
<Text id="duplicateScene.iconLabel" />
</label>
{get(props, 'duplicateSceneErrors.icon') && (
<div className="alert alert-danger">
<Text id="duplicateScene.invalidIcon" />
</div>
)}
<div className={cx('row', style.iconContainer)}>
{iconList.map(icon => (
<div className="col-2">
<div
className={cx('text-center', style.iconDiv, {
[style.iconDivChecked]: get(props, 'scene.icon') === icon
})}
>
<label className={style.iconLabel}>
<input
name="icon"
type="radio"
onChange={props.updateDuplicateSceneIcon}
checked={get(props, 'scene.icon') === icon}
value={icon}
className={style.iconInput}
/>
<i className={`fe fe-${icon}`} />
</label>
</div>
</div>
))}
</div>
</div>
<div class="form-footer">
<button
onClick={props.duplicateScene}
class="btn btn-primary btn-block"
disabled={props.duplicateSceneStatus === RequestStatus.Getting}
>
<Text id="duplicateScene.duplicateSceneButton" />
</button>
</div>
</div>
</form>
</div>
</div>
</div>
);

export default DuplicateScenePage;
129 changes: 129 additions & 0 deletions front/src/routes/scene/duplicate-scene/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { Component } from 'preact';
import { connect } from 'unistore/preact';
import DuplicateScenePage from './DuplicateScenePage';
import { route } from 'preact-router';
import { RequestStatus } from '../../../utils/consts';
import get from 'get-value';

@connect('httpClient', {})
class DuplicateScene extends Component {
goBack = async () => {
route(`/dashboard/scene/${this.props.scene_selector}`);
};

getSourceScene = async () => {
const scene = await this.props.httpClient.get(`/api/v1/scene/${this.props.scene_selector}`);
this.setState({
sourceScene: scene,
scene: {
name: '',
icon: scene.icon
}
});
};

duplicateScene = async e => {
e.preventDefault();
// if errored, we don't continue
if (this.checkErrors()) {
return;
}
this.setState({
duplicateSceneStatus: RequestStatus.Getting
});
try {
const duplicatedScene = await this.props.httpClient.post(
`/api/v1/scene/${this.props.scene_selector}/duplicate`,
this.state.scene
);
this.setState({
duplicateSceneStatus: RequestStatus.Success
});
route(`/dashboard/scene/${duplicatedScene.selector}`);
} catch (e) {
const status = get(e, 'response.status');
if (status === 409) {
this.setState({
duplicateSceneStatus: RequestStatus.ConflictError
});
} else {
this.setState({
duplicateSceneStatus: RequestStatus.Error
});
}
}
};

checkErrors = () => {
let duplicateSceneErrors = {};
if (!this.state.scene.name) {
duplicateSceneErrors.name = true;
}
if (!this.state.scene.icon) {
duplicateSceneErrors.icon = true;
}
this.setState({
duplicateSceneErrors
});
return Object.keys(duplicateSceneErrors).length > 0;
};

updateDuplicateSceneName = e => {
this.setState({
scene: {
name: e.target.value,
icon: this.state.scene.icon
}
});
if (this.state.duplicateSceneErrors) {
this.checkErrors();
}
};

updateDuplicateSceneIcon = e => {
this.setState({
scene: {
name: this.state.scene.name,
icon: e.target.value
}
});
if (this.state.duplicateSceneErrors) {
this.checkErrors();
}
};

constructor(props) {
super(props);
this.getSourceScene();
this.state = {
scene: {
name: '',
icon: ''
},
sourceScene: {
name: '',
icon: ''
},
duplicateSceneErrors: null,
duplicateSceneStatus: null
};
}

render(props, { duplicateSceneErrors, scene, duplicateSceneStatus, sourceScene }) {
return (
<DuplicateScenePage
{...props}
goBack={this.goBack}
scene={scene}
sourceScene={sourceScene}
updateDuplicateSceneName={this.updateDuplicateSceneName}
updateDuplicateSceneIcon={this.updateDuplicateSceneIcon}
duplicateScene={this.duplicateScene}
duplicateSceneErrors={duplicateSceneErrors}
duplicateSceneStatus={duplicateSceneStatus}
/>
);
}
}

export default DuplicateScene;
32 changes: 32 additions & 0 deletions front/src/routes/scene/duplicate-scene/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
.containerWithMargin {
margin-top: 3rem;
}


.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;
}
3 changes: 3 additions & 0 deletions front/src/routes/scene/edit-scene/EditScenePage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ const EditScenePage = ({ children, ...props }) => (
<button onClick={props.startScene} class="btn btn-sm btn-primary ml-2">
<Text id="editScene.startButton" /> <i class="fe fe-play" />
</button>
<button onClick={props.duplicateScene} disabled={props.saving} className="btn btn-sm btn-warning ml-2">
<Text id="editScene.duplicateButton" /> <i className="fe fe-copy" />
</button>
<button onClick={props.saveScene} disabled={props.saving} class="btn btn-sm btn-success ml-2">
<Text id="editScene.saveButton" /> <i class="fe fe-save" />
</button>
Expand Down

0 comments on commit 5154c19

Please sign in to comment.