diff --git a/ui/autocomplete-compnent.ts b/ui/autocomplete-compnent.ts
index c32d99b..644c63d 100644
--- a/ui/autocomplete-compnent.ts
+++ b/ui/autocomplete-compnent.ts
@@ -51,11 +51,8 @@ export default class Autocomplete {
el.addEventListener("focus", () => {
this.element = el;
- const domRect = el.getBoundingClientRect();
- this.container.style.left = `${domRect.left + window.pageXOffset}px`;
- this.container.style.width = `${domRect.width}px`;
- this.container.style.top = `${domRect.bottom + window.pageYOffset}px`;
this.update();
+ this.reposition();
});
el.addEventListener("blur", () => {
@@ -71,7 +68,8 @@ export default class Autocomplete {
} else if (e.key === "Enter") {
if (this.default != null) {
el.value = this.default;
- e.preventDefault();
+ e.stopImmediatePropagation();
+ el.dispatchEvent(new InputEvent("input"));
this.update();
}
} else if (e.key === "ArrowDown") {
@@ -93,6 +91,19 @@ export default class Autocomplete {
});
}
+ public reposition(): void {
+ if (!this.element) return;
+ const domRect = this.element.getBoundingClientRect();
+ if (!domRect.width) {
+ // Element has been removed
+ if (this.visible) this.hide();
+ return;
+ }
+ this.container.style.left = `${domRect.left + window.pageXOffset}px`;
+ this.container.style.width = `${domRect.width}px`;
+ this.container.style.top = `${domRect.bottom + window.pageYOffset}px`;
+ }
+
private hide(): void {
this.container.style.opacity = "0";
this.visible = false;
@@ -159,6 +170,7 @@ export default class Autocomplete {
e.addEventListener("mousedown", (ev) => {
ev.preventDefault();
el.value = suggestion.value;
+ el.dispatchEvent(new InputEvent("input"));
if (this.element === el) this.update();
});
this.container.appendChild(e);
diff --git a/ui/devices-page.ts b/ui/devices-page.ts
index bf3d61a..bb1d2fa 100644
--- a/ui/devices-page.ts
+++ b/ui/devices-page.ts
@@ -328,10 +328,11 @@ export const component: ClosureComponent = (): Component => {
if (window.authorizer.hasAccess("devices", 3))
attrs["actionsCallback"] = renderActions;
- const filterAttrs = {};
- filterAttrs["resource"] = "devices";
- filterAttrs["filter"] = vnode.attrs["filter"];
- filterAttrs["onChange"] = onFilterChanged;
+ const filterAttrs = {
+ resource: "devices",
+ filter: vnode.attrs["filter"],
+ onChange: onFilterChanged,
+ };
return [
m("h1", "Listing devices"),
diff --git a/ui/faults-page.ts b/ui/faults-page.ts
index 9cae35f..63cd97c 100644
--- a/ui/faults-page.ts
+++ b/ui/faults-page.ts
@@ -200,10 +200,11 @@ export const component: ClosureComponent = (): Component => {
};
}
- const filterAttrs = {};
- filterAttrs["resource"] = "faults";
- filterAttrs["filter"] = vnode.attrs["filter"];
- filterAttrs["onChange"] = onFilterChanged;
+ const filterAttrs = {
+ resource: "faults",
+ filter: vnode.attrs["filter"],
+ onChange: onFilterChanged,
+ };
return [
m("h1", "Listing faults"),
diff --git a/ui/files-page.ts b/ui/files-page.ts
index 37ae82b..0531357 100644
--- a/ui/files-page.ts
+++ b/ui/files-page.ts
@@ -316,10 +316,11 @@ export const component: ClosureComponent = (): Component => {
};
}
- const filterAttrs = {};
- filterAttrs["resource"] = "files";
- filterAttrs["filter"] = vnode.attrs["filter"];
- filterAttrs["onChange"] = onFilterChanged;
+ const filterAttrs = {
+ resource: "files",
+ filter: vnode.attrs["filter"],
+ onChange: onFilterChanged,
+ };
return [
m("h1", "Listing files"),
diff --git a/ui/filter-component.ts b/ui/filter-component.ts
index d375bd6..f3b2385 100644
--- a/ui/filter-component.ts
+++ b/ui/filter-component.ts
@@ -17,7 +17,7 @@
* along with GenieACS. If not, see .
*/
-import m, { ClosureComponent, Component } from "mithril";
+import m, { ClosureComponent } from "mithril";
import { parse, stringify, map } from "../lib/common/expression-parser";
import memoize from "../lib/common/memoize";
import Autocomplete from "./autocomplete-compnent";
@@ -82,9 +82,9 @@ function stringifyFilter(f: Expression): string {
return stringify(f);
}
-const splitFilter = memoize((filter) => {
+function splitFilter(filter: string): string[] {
if (!filter) return [""];
- const list = [];
+ const list: string[] = [];
const f = parse(filter);
if (Array.isArray(f) && f[0] === "AND")
for (const ff of f.slice(1)) list.push(stringifyFilter(ff));
@@ -92,69 +92,82 @@ const splitFilter = memoize((filter) => {
list.push("");
return list;
-});
+}
+
+interface Attrs {
+ resource: string;
+ filter: string;
+ onChange: (filter: string) => void;
+}
+
+const component: ClosureComponent = (initialVnode) => {
+ let filterList = splitFilter(initialVnode.attrs.filter);
+ let filterInvalid = 0;
+ let filterTouched = false;
+ let attrs: Attrs = initialVnode.attrs;
+
+ function onChange(): void {
+ filterTouched = false;
+ filterInvalid = 0;
+ filterList = filterList.filter((f) => f);
+ const list = filterList.map((f, idx) => {
+ try {
+ return parseFilter(attrs.resource, f);
+ } catch (err) {
+ filterInvalid |= 1 << idx;
+ }
+ return null;
+ });
+ filterList.push("");
+
+ if (filterInvalid) {
+ m.redraw();
+ return;
+ }
+ if (list.length === 0) attrs.onChange("");
+ else if (list.length > 1) attrs.onChange(stringify(["AND", ...list]));
+ else attrs.onChange(stringify(list[0]));
+ }
-const component: ClosureComponent = (): Component => {
return {
+ onupdate: (vnode) => {
+ getAutocomplete(vnode.attrs.resource).reposition();
+ },
view: (vnode) => {
- if (
- !vnode.state["filterList"] ||
- vnode.attrs["filter"] !== vnode.state["filter"]
- ) {
- vnode.state["filterInvalid"] = 0;
- vnode.state["filter"] = vnode.attrs["filter"];
- vnode.state["filterList"] = splitFilter(vnode.attrs["filter"]);
+ if (attrs.filter !== vnode.attrs.filter) {
+ filterInvalid = 0;
+ filterList = splitFilter(vnode.attrs.filter);
}
- function onChange(): void {
- vnode.state["filterInvalid"] = 0;
- vnode.state["filterList"] = vnode.state["filterList"].filter((f) => f);
- let filter = vnode.state["filterList"].map((f, idx) => {
- try {
- return parseFilter(vnode.attrs["resource"], f);
- } catch (err) {
- vnode.state["filterInvalid"] |= 1 << idx;
- }
- return null;
- });
- vnode.state["filterList"].push("");
+ attrs = vnode.attrs;
- if (!vnode.state["filterInvalid"]) {
- delete vnode.state["filter"];
- if (filter.length === 0) {
- vnode.attrs["onChange"]("");
- } else {
- if (filter.length > 1) filter = ["AND"].concat(filter);
- else filter = filter[0];
- vnode.attrs["onChange"](stringify(filter));
- }
- }
- }
+ return m("div.filter", [
+ m("b", "Filter"),
+ ...filterList.map((fltr, idx) => {
+ return m("input", {
+ type: "text",
+ class: `${(filterInvalid >> idx) & 1 ? "error" : ""}`,
+ value: fltr,
+ oninput: (e) => {
+ e.redraw = false;
+ filterList[idx] = e.target.value;
+ filterTouched = true;
+ },
+ oncreate: (vn) => {
+ const el = vn.dom as HTMLInputElement;
+ getAutocomplete(vnode.attrs.resource).attach(el);
- return m(
- "div.filter",
- [m("b", "Filter")].concat(
- vnode.state["filterList"].map((fltr, idx) => {
- return m("input", {
- type: "text",
- class: `${
- (vnode.state["filterInvalid"] >> idx) & 1 ? "error" : ""
- }`,
- value: fltr,
- onchange: (e) => {
- vnode.state["filterList"] = vnode.state["filterList"].slice();
- vnode.state["filterList"][idx] = e.target.value.trim();
- onChange();
- },
- oncreate: (vn) => {
- getAutocomplete(vnode.attrs["resource"]).attach(
- vn.dom as HTMLInputElement
- );
- },
- });
- })
- )
- );
+ el.addEventListener("blur", () => {
+ if (filterTouched) onChange();
+ });
+
+ el.addEventListener("keydown", (e) => {
+ if (e.key === "Enter" && filterTouched) onChange();
+ });
+ },
+ });
+ }),
+ ]);
},
};
};
diff --git a/ui/permissions-page.ts b/ui/permissions-page.ts
index a791bdc..25e2f50 100644
--- a/ui/permissions-page.ts
+++ b/ui/permissions-page.ts
@@ -365,10 +365,11 @@ export const component: ClosureComponent = (): Component => {
};
}
- const filterAttrs = {};
- filterAttrs["resource"] = "permissions";
- filterAttrs["filter"] = vnode.attrs["filter"];
- filterAttrs["onChange"] = onFilterChanged;
+ const filterAttrs = {
+ resource: "permissions",
+ filter: vnode.attrs["filter"],
+ onChange: onFilterChanged,
+ };
return [
m("h1", "Listing permissions"),
diff --git a/ui/presets-page.ts b/ui/presets-page.ts
index 9f7c07f..f150167 100644
--- a/ui/presets-page.ts
+++ b/ui/presets-page.ts
@@ -427,10 +427,11 @@ export const component: ClosureComponent = (): Component => {
};
}
- const filterAttrs = {};
- filterAttrs["resource"] = "presets";
- filterAttrs["filter"] = vnode.attrs["filter"];
- filterAttrs["onChange"] = onFilterChanged;
+ const filterAttrs = {
+ resource: "presets",
+ filter: vnode.attrs["filter"],
+ onChange: onFilterChanged,
+ };
return [
m("h1", "Listing presets"),
diff --git a/ui/provisions-page.ts b/ui/provisions-page.ts
index 687283f..176a3bc 100644
--- a/ui/provisions-page.ts
+++ b/ui/provisions-page.ts
@@ -346,10 +346,11 @@ export const component: ClosureComponent = (): Component => {
};
}
- const filterAttrs = {};
- filterAttrs["resource"] = "provisions";
- filterAttrs["filter"] = vnode.attrs["filter"];
- filterAttrs["onChange"] = onFilterChanged;
+ const filterAttrs = {
+ resource: "provisions",
+ filter: vnode.attrs["filter"],
+ onChange: onFilterChanged,
+ };
return [
m("h1", "Listing provisions"),
diff --git a/ui/users-page.ts b/ui/users-page.ts
index 49c99c3..b74de30 100644
--- a/ui/users-page.ts
+++ b/ui/users-page.ts
@@ -401,10 +401,11 @@ export const component: ClosureComponent = (): Component => {
};
}
- const filterAttrs = {};
- filterAttrs["resource"] = "users";
- filterAttrs["filter"] = vnode.attrs["filter"];
- filterAttrs["onChange"] = onFilterChanged;
+ const filterAttrs = {
+ resource: "users",
+ filter: vnode.attrs["filter"],
+ onChange: onFilterChanged,
+ };
return [
m("h1", "Listing users"),
diff --git a/ui/virtual-parameters-page.ts b/ui/virtual-parameters-page.ts
index 44532b3..7192a41 100644
--- a/ui/virtual-parameters-page.ts
+++ b/ui/virtual-parameters-page.ts
@@ -346,10 +346,11 @@ export const component: ClosureComponent = (): Component => {
};
}
- const filterAttrs = {};
- filterAttrs["resource"] = "virtualParameters";
- filterAttrs["filter"] = vnode.attrs["filter"];
- filterAttrs["onChange"] = onFilterChanged;
+ const filterAttrs = {
+ resource: "virtualParameters",
+ filter: vnode.attrs["filter"],
+ onChange: onFilterChanged,
+ };
return [
m("h1", "Listing virtual parameters"),