Skip to content

Commit

Permalink
Scene: Add tags to filter scenes (#1900)
Browse files Browse the repository at this point in the history
  • Loading branch information
callemand authored Nov 20, 2023
1 parent 30ce850 commit 7c2854c
Show file tree
Hide file tree
Showing 41 changed files with 928 additions and 230 deletions.
172 changes: 103 additions & 69 deletions front/cypress/e2e/routes/scene/Scene.cy.js
Original file line number Diff line number Diff line change
@@ -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')
Expand All @@ -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
Expand All @@ -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')
Expand All @@ -81,54 +57,108 @@ 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!!
cy.get('.btn-success').then(buttons => {
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')
.click();

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!!
cy.get('.btn-success').then(buttons => {
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')
Expand All @@ -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!!
Expand All @@ -156,6 +188,7 @@ describe('Scene view', () => {
cy.wrap(selects[2]).select('minute');
});
});

it('Should disable scene', () => {
cy.visit('/dashboard/scene');

Expand Down Expand Up @@ -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');
Expand Down
3 changes: 2 additions & 1 deletion front/src/actions/createScene.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ function createActions(store) {
newScene: {
name: '',
icon: null,
actions: [[]]
actions: [[]],
tags: []
},
newSceneErrors: null,
createSceneStatus: null
Expand Down
9 changes: 2 additions & 7 deletions front/src/components/layout/CardFilter.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,12 @@ const CardFilter = ({ changeOrderDir, orderValue = 'asc', search, searchValue, s
<Text id="global.orderDirDesc" />
</option>
</select>

<div class="input-icon ml-2">
<span class="input-icon-addon">
<i class="fe fe-search" />
</span>
<input
type="text"
class="form-control w-10"
placeholder={searchPlaceHolder}
onInput={search}
value={searchValue}
/>
<input type="text" class="form-control" placeholder={searchPlaceHolder} onInput={search} value={searchValue} />
</div>
</Fragment>
);
Expand Down
9 changes: 8 additions & 1 deletion front/src/config/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -2091,7 +2097,8 @@
"newButton": "New",
"editButton": "Edit",
"startButton": "Start",
"searchPlaceholder": "Search scenes"
"searchPlaceholder": "Search scenes",
"filterTagsName": "Filter by tags"
},
"gateway": {
"instanceConfiguredTitle": "Gladys Plus",
Expand Down
9 changes: 8 additions & 1 deletion front/src/config/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
13 changes: 12 additions & 1 deletion front/src/routes/scene/SceneCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -34,7 +35,7 @@ class SceneCard extends Component {
<div class="loader" />
<div class="dimmer-content h-100">
<div class="card h-100 d-flex flex-column justify-content-between">
<div class="card-body pb-0 pt-3 pl-3 pr-3 text-center h-100">
<div class="card-body p-3 text-center h-100 d-flex flex-column">
<div class={style.scene_icon}>
<i class={`fe fe-${props.scene.icon}`} />
</div>
Expand All @@ -53,6 +54,16 @@ class SceneCard extends Component {
</div>
<h4>{props.scene.name}</h4>
<div class={`text-muted ${style.descriptionSceneEllipsis}`}>{props.scene.description}</div>
<div>
{props.scene.tags &&
props.scene.tags.map(tag => (
<span class="badge badge-secondary mr-1">
{tag.name.length > MAX_LENGTH_TAG
? `${tag.name.substring(0, MAX_LENGTH_TAG - 3)}...`
: tag.name}
</span>
))}
</div>
</div>
<div class="mt-auto">
<div class="card-footer">
Expand Down
9 changes: 7 additions & 2 deletions front/src/routes/scene/ScenePage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import CardFilter from '../../components/layout/CardFilter';
import SceneCards from './SceneCards';
import EmptyState from './EmptyState';
import style from './style.css';
import SceneTagFilter from './SceneTagFilter';

const ScenePage = ({ children, ...props }) => (
<div class="page">
Expand All @@ -26,8 +27,12 @@ const ScenePage = ({ children, ...props }) => (
searchPlaceHolder={<Text id="scene.searchPlaceholder" />}
/>
</Localizer>
<Link href="/dashboard/scene/new" class="btn btn-outline-primary ml-2">
<Text id="scene.newButton" /> <i class="fe fe-plus" />
<SceneTagFilter tags={props.tags} searchTags={props.searchTags} sceneTagSearch={props.sceneTagSearch} />
<Link href="/dashboard/scene/new" class={cx('btn', 'btn-outline-primary', 'ml-2', style.newButton)}>
<span class="d-none d-lg-inline-block mr-2">
<Text id="scene.newButton" />
</span>
<i class="fe fe-plus" />
</Link>
</div>
</div>
Expand Down
Loading

0 comments on commit 7c2854c

Please sign in to comment.