Skip to content

Commit

Permalink
feat: tree view for scopes (projectcaluma#328)
Browse files Browse the repository at this point in the history
* feat: tree view for scopes (wip)

* feat: tree view as explicit component

* test: add tests for tree view

* fix: revert removal of settled() helpers

* chore: minor refactoring, fix tests

* chore: fix minor navigation issues, refactoring

* chore: fix tests (wip)

* feat: let datatable make use ember-resources

* fix: redirect logic, avoid duplicate request for relationship

* fix: tests

Co-authored-by: Christian Zosel <[email protected]>
  • Loading branch information
derrabauke and czosel authored Dec 10, 2021
1 parent f3fd796 commit 36e5976
Show file tree
Hide file tree
Showing 35 changed files with 528 additions and 93 deletions.
11 changes: 5 additions & 6 deletions addon/components/data-table.hbs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="uk-animation-fade uk-flex uk-flex-right uk-width-1">
<div class="uk-flex uk-flex-right uk-width-1">
<form
class="uk-search uk-search-default uk-width-1-3"
{{on "submit" this.updateSearch}}
Expand All @@ -21,10 +21,10 @@
</div>

<table
class="uk-animation-fade uk-table uk-table-divider"
{{did-insert (perform this.fetchData)}}
class="uk-table uk-table-divider"
...attributes
>

{{#if this.isLoading}}
<tbody>
<td colspan="99" class="uk-padding-large uk-animation-fade">
Expand All @@ -34,19 +34,18 @@
@vertical="middle"
class="uk-height-large"
>
<UkSpinner @ratio="3" />
<UkSpinner @ratio="3" />
</UkFlex>
</td>
</tbody>
{{else}}
{{yield
(hash
body=(component "data-table/body" models=this.models)
body=(component "data-table/body" models=this.data.value)
head=(component "data-table/head")
refresh=(perform this.fetchData)
)
}}

<tfoot>
<tr>
<td colspan="99">
Expand Down
14 changes: 11 additions & 3 deletions addon/components/data-table.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { task, lastValue } from "ember-concurrency";
import { task } from "ember-concurrency";
import { useFunction } from "ember-resources";

export default class DataTableComponent extends Component {
@service store;
Expand All @@ -13,7 +14,14 @@ export default class DataTableComponent extends Component {
@tracked numPages;
@tracked internalSearch;
@tracked internalPage = 1;
@lastValue("fetchData") models;

// While using 'useTask' we ended up in an infinite loop.
// data = useTask(this, this.fetchData, () => [this.args.filter]);
data = useFunction(this, this.fetchData.perform, () => [
this.args.filter,
this.page,
this.search,
]);

get page() {
return this.args.page || this.internalPage;
Expand Down Expand Up @@ -81,7 +89,7 @@ export default class DataTableComponent extends Component {

@action
updateSearch(submitEvent) {
// Prevent reaload because of form submit
// Prevent reload because of form submit
submitEvent.preventDefault();
this.search = submitEvent.target.elements.search.value;
this.fetchData.perform();
Expand Down
3 changes: 3 additions & 0 deletions addon/components/relationship-select.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export default class RelationshipSelectComponent extends Component {
@restartableTask
@handleModelErrors
*fetchModels(search) {
if (this.args.model) {
return this.args.model;
}
if (typeof search === "string") {
yield timeout(500);
return yield this.store.query(this.args.modelName, {
Expand Down
21 changes: 21 additions & 0 deletions addon/components/tree-node.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<li class="{{if (and (not @flat) @item.children.length 0) 'childless'}}">
{{#if (and (not @flat) @item.children)}}
<UkButton class="tree-node-icon" @color="link" {{on "click" this.toggle}}>
<UkIcon @icon={{if this.expanded "chevron-down" "chevron-right"}} />
</UkButton>
{{/if}}
<LinkTo @route={{@itemRoute}} @model={{@item}} class="uk-link-text" data-test-node-id={{@item.id}}>
{{@item.name}}{{#if @item.children}} ({{@item.children.length}}){{/if}}
</LinkTo>
{{#if this.expanded}}
<ul class="tree">
{{#each (sort-by "name" @item.children) as |child|}}
<TreeNode
@item={{child}}
@itemRoute={{@itemRoute}}
@activeItem={{@activeItem}}
@expandedItems={{@expandedItems}}/>
{{/each}}
</ul>
{{/if}}
</li>
44 changes: 44 additions & 0 deletions addon/components/tree-node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";

export default class TreeNodeComponent extends Component {
@tracked expandedByUser = null;
@service router;

@action
toggle() {
if (this.expandedByUser === null) {
this.expandedByUser = !this.expanded;
} else {
this.expandedByUser = !this.expandedByUser;
}

if (
this.args.activeItem
?.findParents()
.find((parent) => parent.id === this.args.item.id)
) {
this.router.transitionTo(this.args.itemRoute, this.args.item.id);
}
}

get expandedDefault() {
return (
this.args.item.level === 0 ||
this.args.activeItem?.id === this.args.item.id
);
}

get expanded() {
return (
!this.args.flat &&
this.args.item.children &&
(this.args.expandedItems?.find((item) => item.id === this.args.item.id) ||
(this.expandedByUser !== null
? this.expandedByUser
: this.expandedDefault))
);
}
}
24 changes: 24 additions & 0 deletions addon/components/tree.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<div ...attributes>
<div class="uk-inline uk-margin-small-bottom uk-width-1-1">
<span class="uk-form-icon" uk-icon="icon: search"></span>
<Input
class="uk-input tree-search"
aira-label="search"
@type="search"
@value={{this.filterValue}}
placeholder={{t "emeis.search.placeholder"}}
data-test-tree-search
{{on "input" this.filter}}
/>
</div>
<ul class="tree">
{{#each (sort-by "name" this.items) as |item|}}
<TreeNode
@item={{item}}
@itemRoute={{@itemRoute}}
@activeItem={{@activeItem}}
@expandedItems={{this.expandedItems}}
@flat={{this.hasFilter}}/>
{{/each}}
</ul>
</div>
55 changes: 55 additions & 0 deletions addon/components/tree.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { isArray } from "@ember/array";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";

export default class TreeComponent extends Component {
@service store;

@tracked filterValue;

@tracked filtered;

get items() {
if (!this.filterValue) return this.args.items;
return this.filtered;
}

get hasFilter() {
return !!this.filterValue;
}

get expandedItems() {
return this.args.activeItem?.findParents
? this.args.activeItem?.findParents()
: [];
}

@action
filter(event) {
const filterItems = (
items,
searchTerm = this.filterValue,
includedKeys = ["name", "description"]
) => {
if (!searchTerm || !items || !isArray(items)) {
return [];
}
const ownMatches = items.filter((item) =>
includedKeys.find((key) =>
item[key]?.toLowerCase().includes(searchTerm.toLowerCase())
)
);

const childMatches = items
.filter((item) => item.children)
.flatMap((item) =>
filterItems(item.children, searchTerm, includedKeys)
);
return [...ownMatches, ...childMatches];
};

this.filtered = filterItems(this.args.items, event.target.value);
}
}
17 changes: 17 additions & 0 deletions addon/controllers/scopes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Controller from "@ember/controller";
import { inject as service } from "@ember/service";

export default class ScopesController extends Controller {
@service router;

get activeScope() {
if (!this.router.currentRouteName.includes("ember-emeis.scopes.edit")) {
return null;
}
return this.router.currentRoute.attributes;
}

get rootScopes() {
return this.model?.filter((scope) => !scope.parent);
}
}
5 changes: 0 additions & 5 deletions addon/controllers/scopes/edit/acl.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { inject as service } from "@ember/service";

import PaginationController from "ember-emeis/-private/controllers/pagination";

export default class ScopesEditACLController extends PaginationController {
@service intl;
@service store;

get queryParamsfilter() {
return { scope: this.model.id };
}
Expand Down
6 changes: 6 additions & 0 deletions addon/controllers/scopes/edit/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,17 @@ import { inject as service } from "@ember/service";
export default class ScopesEditIndexController extends Controller {
@service emeisOptions;
@service intl;
@service router;
@service store;

get metaFields() {
return this.emeisOptions.metaFields?.scope;
}

get allScopes() {
return this.store.peekAll("scope");
}

@action
updateModel(model, formElements) {
model.name = formElements.name.value;
Expand Down
3 changes: 0 additions & 3 deletions addon/controllers/scopes/index.js

This file was deleted.

14 changes: 12 additions & 2 deletions addon/models/scope.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,17 @@ export default class ScopeModel extends LocalizedModel {
@attr level;
@attr meta;

@belongsTo("scope", { inverse: "children" }) parent;
@hasMany("scope", { inverse: "parent" }) children;
@belongsTo("scope", { inverse: "children", async: false }) parent;
@hasMany("scope", { inverse: "parent", async: false }) children;
@hasMany("acl") acls;

findParents() {
const anchestors = [];
let node = this;
while (node.parent) {
anchestors.push(node.parent);
node = node.parent;
}
return anchestors;
}
}
19 changes: 18 additions & 1 deletion addon/routes/scopes.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
import Route from "@ember/routing/route";
import { inject as service } from "@ember/service";

export default class ScopesRoute extends Route {}
export default class ScopesRoute extends Route {
@service store;
@service router;

model() {
return this.store.findAll("scope");
}

redirect(scopes, transition) {
if (
transition.targetName === "ember-emeis.scopes.index" &&
scopes.firstObject
) {
this.router.replaceWith("ember-emeis.scopes.edit", scopes.firstObject);
}
}
}
2 changes: 1 addition & 1 deletion addon/routes/scopes/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ export default class ScopesEditRoute extends Route {

@handleModelErrors({ routeFor404: "scopes.index" })
model({ scope_id: id }) {
return this.store.findRecord("scope", id);
return this.store.peekRecord("scope", id);
}
}
2 changes: 1 addition & 1 deletion addon/routes/scopes/edit/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Route from "@ember/routing/route";

export default class ScopesEditIndexRoute extends Route {
async model() {
model() {
return this.modelFor("scopes.edit");
}
}
14 changes: 13 additions & 1 deletion addon/templates/scopes.hbs
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
<SectionTitle @model="scopes" />

{{outlet}}
<div class="uk-grid">
<div class="uk-width-1-4 uk-first-column tree-wrapper">
<Tree
class="uk-margin-small-left"
@items={{this.rootScopes}}
@itemRoute="scopes.edit"
@activeItem={{this.activeScope}}
/>
</div>
<div class="uk-width-3-4">
{{outlet}}
</div>
</div>
2 changes: 1 addition & 1 deletion addon/templates/scopes/edit/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

<EditForm::Element @label={{t "emeis.scopes.headings.parent"}}>
<RelationshipSelect
@modelName="scope"
@model={{this.allScopes}}
@selected={{@model.parent}}
@placeholder="{{t "emeis.scopes.headings.parent"}}..."
@onChange={{set @model.parent}} as |scope|
Expand Down
Loading

0 comments on commit 36e5976

Please sign in to comment.