Skip to content

Commit 0067caf

Browse files
authored
Merge pull request #22 from NolanKingdon/MYG-13064-GroupFilter
MYG-13064 Group Filter Feature
2 parents 3f86583 + a9b8358 commit 0067caf

File tree

14 files changed

+827
-14
lines changed

14 files changed

+827
-14
lines changed

generators/app/index.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,9 +385,27 @@ module.exports = class extends yeoman {
385385
this.destinationPath('src/.dev/state.js')
386386
);
387387

388-
// Languages
389388
if(!this.props.isButton && !this.props.isDriveAddin){
389+
// Group Filter
390+
this.fs.copy(
391+
this.templatePath('src/.dev/groups/_GroupHelper.js'),
392+
this.destinationPath('src/.dev/groups/_GroupHelper.js')
393+
);
394+
395+
this.fs.copy(
396+
this.templatePath('src/.dev/groups/GroupListeners.js'),
397+
this.destinationPath('src/.dev/groups/GroupListeners.js')
398+
);
399+
400+
this.fs.copyTpl(
401+
this.templatePath('src/.dev/groups/Groups.js'),
402+
this.destinationPath('src/.dev/groups/Groups.js'),
403+
{
404+
root: this.props.camelName
405+
}
406+
);
390407

408+
// Languages
391409
this.fs.copy(
392410
this.templatePath('src/.dev/lang/Translator.js'),
393411
this.destinationPath('src/.dev/lang/Translator.js'),
@@ -489,6 +507,11 @@ module.exports = class extends yeoman {
489507
this.destinationPath('src/.dev/images/Font_Awesome_5_solid_chevron-left.svg')
490508
);
491509

510+
this.fs.copy(
511+
this.templatePath('src/.dev/images/close-round.svg'),
512+
this.destinationPath('src/.dev/images/close-round.svg')
513+
);
514+
492515
this.fs.copy(
493516
this.templatePath('src/.dev/styles/styleGuide.css'),
494517
this.destinationPath('src/.dev/styles/styleGuide.css')
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
const Groups = require('./Groups.js');
2+
let xIconSvg = require('../images/close-round.svg').default;
3+
let chevron = require('../images/Font_Awesome_5_solid_chevron-left.svg').default;
4+
const regeneratorRuntime = require('regenerator-runtime');
5+
6+
class GroupListeners {
7+
8+
constructor(api, state, target){
9+
this.groupBox = new Groups(api, state, target);
10+
window.groupsFilter = this.groupBox;
11+
this.displayBox = document.getElementById(target);
12+
this.inputBox = document.getElementById('group-input');
13+
this.groupToggle = document.getElementById('group-toggle-button');
14+
this.deleteAllBtn = document.getElementById('group-remove-all');
15+
this.firstOpen = true;
16+
this.open = false;
17+
this.closeListener;
18+
this.changeSearchTimeout;
19+
}
20+
21+
assignEventListeners(){
22+
// Hide dropdown when clicking outside of list.
23+
document.addEventListener('click', (event) => this._hideGroupsOnOffClickListener(event));
24+
25+
// Group dropdown box toggle.
26+
this.groupToggle.addEventListener('click', () => this._groupToggleListener(this.displayBox));
27+
28+
// Open dropdown on input box click.
29+
this.inputBox.addEventListener('click', () => {
30+
if(!this.open){
31+
this._groupToggleListener(this.displayBox);
32+
}
33+
});
34+
35+
// Inputbox listener for change - ie. enter presses.
36+
this.inputBox.addEventListener('change', (event) => this._groupSearchListener(event));
37+
38+
// Inputbox listener for input timeout - ie. typing then stopping.
39+
this.inputBox.addEventListener('input', (event) => this._inputTimeoutGroupSearchListener(event));
40+
41+
// Listener to reset group filters.
42+
this.deleteAllBtn.addEventListener('click', () => this._groupResetAllFilters());
43+
}
44+
45+
_inputTimeoutGroupSearchListener(event){
46+
47+
// Cancelling any previous search timeouts.
48+
clearTimeout(this.changeSearchTimeout);
49+
50+
// Making a new one.
51+
this.changeSearchTimeout = setTimeout( async () => {
52+
await this._groupSearchListener(event);
53+
}, 500);
54+
}
55+
56+
_hideGroupsOnOffClickListener(event){
57+
if(!event.target.closest('#group-wrapper, #group-dropdown-ul')){
58+
if(this.open){
59+
this._groupToggleListener(this.displayBox);
60+
}
61+
}
62+
}
63+
64+
_groupResetAllFilters(){
65+
this.groupBox.resetActiveGroups();
66+
this.inputBox.value = '';
67+
this.groupBox.previousSearchTerm = '';
68+
}
69+
70+
async _groupSearchListener(event){
71+
let keyword = event.target.value;
72+
73+
if(!this.open){
74+
await this._groupToggleListener(this.displayBox);
75+
}
76+
77+
/**
78+
* Because this listener can be called on input and on change, there's a chance for it to be
79+
* called twice. This is because the change event fires when focus on the input box is lost.
80+
* i.e. - you type, rely on the input listener, then go to click the item, the event re-fires
81+
* and nothing happens in the UI until you click again.
82+
*/
83+
if(keyword !== '' && keyword !== this.groupBox.previousSearchTerm){
84+
this.groupBox.groupSearch(keyword);
85+
} else if(keyword === '' && keyword !== this.groupBox.previousSearchTerm) {
86+
this.groupBox.generateRootHtml();
87+
}
88+
89+
this.groupBox.previousSearchTerm = keyword;
90+
}
91+
92+
async _groupToggleListener(display){
93+
if(!this.open){
94+
display.style.display = 'block';
95+
96+
if(this.firstOpen){
97+
await this.groupBox.getAllGroupsInDatabase();
98+
this.firstOpen = false;
99+
} else {
100+
this.groupBox.generateRootHtml();
101+
}
102+
103+
this.open = true;
104+
} else {
105+
this.inputBox.value = '';
106+
this.groupBox.previousSearchTerm = '';
107+
display.style.display = 'none';
108+
this.open = false;
109+
}
110+
111+
this._rotateToggleButton();
112+
}
113+
114+
_rotateToggleButton(){
115+
if(this.open){
116+
this.groupToggle.children[0].style['mask-image'] = `url(${xIconSvg})`;
117+
this.groupToggle.children[0].style['-webkit-mask-image'] = `url(${xIconSvg})`;
118+
this.groupToggle.children[0].style['transform'] = 'none';
119+
} else {
120+
this.groupToggle.children[0].style['mask-image'] = `url(${chevron})`;
121+
this.groupToggle.children[0].style['-webkit-mask-image'] = `url(${chevron})`;
122+
this.groupToggle.children[0].style['transform'] = 'rotate(-90deg)';
123+
}
124+
}
125+
126+
_groupHiddenListener(){
127+
// Hide the div.
128+
this.displayBox.style.display = none;
129+
}
130+
}
131+
132+
module.exports = GroupListeners;
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
const _GroupHelper = require('./_GroupHelper.js');
2+
3+
/**
4+
* Handles most of the HTML modification and Api calls for tasks relating to the
5+
* group filter functionality. Will make first call to the api to gather groups when
6+
* the user first clicks on the groups dropdown to save on requests to the database
7+
* when making small changes to the addins.
8+
*
9+
* Relies on the _GroupHelper class to do most of the html generation/sorting.
10+
*/
11+
class Groups {
12+
13+
constructor(api, state, target){
14+
this.api = api;
15+
this.state = state;
16+
this.baseNode;
17+
this.groupsDictionary;
18+
this.root = document.getElementById(target);
19+
this.activeLabel = document.getElementById('active-group');
20+
this.deleteAllBtn = document.getElementById('group-remove-all');
21+
this.previousGroupStack = [];
22+
this.activeGroups = [];
23+
this.previousSearchTerm;
24+
}
25+
26+
/**
27+
* Once the initial call for groups has been made, we want to re-reference the already
28+
* collected groups and use this method to just re-generate the base html.
29+
*/
30+
generateRootHtml(){
31+
let html = _GroupHelper.generateNodeHtml(groupsFilter.groupsDictionary, this.baseNode);
32+
groupsFilter.root.innerHTML = html;
33+
}
34+
35+
/**
36+
* Creates a request to the database the user is currently logged in to, and grabs all
37+
* the registered groups from there.
38+
*
39+
* @param {int} resultsLimit how many results to limit the response to.
40+
* @returns {Promise} Once the call completes, the call resolves.
41+
*/
42+
getAllGroupsInDatabase(resultsLimit = 2000){
43+
let callPromise = new Promise( (resolve, reject) => {
44+
this.api.call('Get', {
45+
'typeName': 'Group',
46+
'resultsLimit': resultsLimit
47+
},
48+
(result) => this._groupSuccessCallback(result, resolve),
49+
(error) => this._groupErrorCallback(error, reject));
50+
});
51+
52+
return callPromise;
53+
}
54+
55+
/**
56+
* Removes all group names from the 'Active Groups' header.
57+
* Removes all groups from the state.
58+
* Unselects any previously selected groups in the groupsDictionary.
59+
* Resets the HTML to remove any checked attributes.
60+
*/
61+
resetActiveGroups(){
62+
this.state._activeGroups.forEach( group => {
63+
this.groupsDictionary[group.id].selected = false;
64+
});
65+
66+
this.state._activeGroups = [];
67+
68+
this.writeActiveGroups();
69+
70+
let html = _GroupHelper.generateNodeHtml(this.groupsDictionary, this.baseNode);
71+
this.root.innerHTML = html;
72+
73+
geotab.addin.<%= root%>.focus(this.api, this.state);
74+
}
75+
76+
/**
77+
* Toggles the status of the selected node to be 'active'. Updates this on the groupsDictionary
78+
* to maintain the highlighting style when the HTML is reloaded (folder change, search, etc.)
79+
*
80+
* @param {string} id id of the node that is to be added to the 'Active Groups' filter
81+
*/
82+
toggleGroupFilter(id){
83+
let idIndex;
84+
85+
for (let i = 0; i < this.state._activeGroups.length; i++) {
86+
if(this.state._activeGroups[i].id === id){
87+
idIndex = i;
88+
}
89+
}
90+
91+
if(idIndex !== undefined){
92+
this.state._activeGroups.splice(idIndex, 1);
93+
this.groupsDictionary[id].selected = false;
94+
} else {
95+
this.groupsDictionary[id].selected = true;
96+
this.state._activeGroups.push({id});
97+
}
98+
99+
this.writeActiveGroups();
100+
101+
geotab.addin.<%= root%>.focus(this.api, this.state);
102+
}
103+
104+
/**
105+
* Writes the currently selected group's to the top bar, and injects 'OR' if there are multiple
106+
* filters selected.
107+
*/
108+
writeActiveGroups(){
109+
let text = `Active Groups:`;
110+
let stateLength = this.state._activeGroups.length;
111+
112+
if(stateLength > 0){
113+
text += _GroupHelper.generateActiveHeaderText(this.state, stateLength, this.groupsDictionary);
114+
this.deleteAllBtn.style.display = 'inline';
115+
} else {
116+
text += ` All`;
117+
this.deleteAllBtn.style.display = 'none';
118+
}
119+
120+
this.activeLabel.innerHTML = text;
121+
}
122+
123+
/**
124+
* Creates regex out of a provided search term and re-generates the group list html based on
125+
* the inclusion of the keyword. Should be run whenever the query input changes.
126+
*
127+
* @param {string} query keyword for searching the groups
128+
*/
129+
groupSearch(query){
130+
let regex = this._createRegex(query.toLowerCase());
131+
let html = _GroupHelper.generateSearchHtml(this.groupsDictionary, regex);
132+
this.root.innerHTML = html;
133+
}
134+
135+
_createRegex(input){
136+
let regex = new RegExp(`.*${input}.*`);
137+
return regex;
138+
}
139+
140+
/**
141+
* Handles navigation into a subfolder. Stores information about where the user is going
142+
* and where the user is coming from to allow us to go 'one level up' from the new group list.
143+
*
144+
* @param {string} previous the id of the folder/level that the user is leaving.
145+
* @param {string} current the id of the folder/level the user is navigating into.
146+
*/
147+
changeFocus(previous, current){
148+
this.previousGroupStack.push(previous);
149+
let html = _GroupHelper.generateNodeHtml(this.groupsDictionary, current, this.baseNode);
150+
this.root.innerHTML = html;
151+
}
152+
153+
/**
154+
* Returns to the previous folder that was in view for the user when selecting 'Go up one level'
155+
* option.
156+
*/
157+
goToPreviousFolder(){
158+
let previousFolder = this.previousGroupStack.pop();
159+
let html = _GroupHelper.generateNodeHtml(this.groupsDictionary, previousFolder, this.baseNode);
160+
this.root.innerHTML = html;
161+
}
162+
163+
/**
164+
* Callback used for collecting the group list from the authenticated database.
165+
*
166+
* @param {object} result Response from the server - should contain a list of all the groups on the DB.
167+
*/
168+
_groupSuccessCallback(result, resolve){
169+
let groupInput = document.getElementById('group-input');
170+
this.baseNode = result[0].id;
171+
this.groupsDictionary = _GroupHelper.convertGroupsListToDictionary(result);
172+
let html = _GroupHelper.generateNodeHtml(this.groupsDictionary, this.baseNode);
173+
this.root.innerHTML = html;
174+
175+
// If we had any errors, we want to reset the placeholder text.
176+
groupInput.placeholder = 'Search for Groups';
177+
resolve();
178+
}
179+
180+
/**
181+
* Notifies the user that there has been an error in creating the group list. Will retry
182+
* every 60 seconds.
183+
*
184+
* @param {string} error Error message from failed api call/Group object instantiation.
185+
*/
186+
_groupErrorCallback(error){
187+
let groupInput = document.getElementById('group-input');
188+
groupInput.placeholder = "Unable to retrieve Groups";
189+
console.log(error);
190+
191+
setTimeout(() => {
192+
groupInput.placeholder = "Retrying...";
193+
groupsFilter.getAllGroupsInDatabase();
194+
}, 60000);
195+
}
196+
}
197+
198+
module.exports = Groups;

0 commit comments

Comments
 (0)