Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Duplicate scene #1670

Merged
merged 10 commits into from
Dec 18, 2022
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
130 changes: 130 additions & 0 deletions front/src/routes/scene/duplicate-scene/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
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 = () => {
this.props.httpClient.get(`/api/v1/scene/${this.props.scene_selector}`).then(scene => {
callemand marked this conversation as resolved.
Show resolved Hide resolved
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