-
-
+
+
+
+
+ {{ .strings.noResultsFound }}
+
diff --git a/lang/admin/en-us.json b/lang/admin/en-us.json
index 6b9e16f5..86c48727 100644
--- a/lang/admin/en-us.json
+++ b/lang/admin/en-us.json
@@ -151,7 +151,22 @@
"fromInvite": "From Invite",
"byAdmin": "By Admin",
"byUser": "By User",
- "byJfaGo": "By jfa-go"
+ "byJfaGo": "By jfa-go",
+ "activityID": "Activity ID",
+ "title": "Title",
+ "usersMentioned": "User mentioned",
+ "actor": "Actor",
+ "actorDescription": "The thing that caused this action.
\"user\"/\"admin\"/\"daemon\" or a username.",
+ "accountCreationFilter": "Account Creation",
+ "accountDeletionFilter": "Account Deletion",
+ "accountDisabledFilter": "Account Disabled",
+ "accountEnabledFilter": "Account Enabled",
+ "contactLinkedFilter": "Contact Linked",
+ "contactUnlinkedFilter": "Contact Unlinked",
+ "passwordChangeFilter": "Password Changed",
+ "passwordResetFilter": "Password Reset",
+ "inviteCreatedFilter": "Invite Created",
+ "inviteDeletedFilter": "Invite Deleted/Expired"
},
"notifications": {
"changedEmailAddress": "Changed email address of {n}.",
diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts
index cb1c757b..df1da8df 100644
--- a/ts/modules/accounts.ts
+++ b/ts/modules/accounts.ts
@@ -1803,6 +1803,7 @@ export class accountsList {
sortingByButton: this._sortingByButton,
searchOptionsHeader: this._searchOptionsHeader,
notFoundPanel: this._notFoundPanel,
+ filterList: document.getElementById("accounts-filter-list"),
search: this._searchBox,
queries: this._queries,
setVisibility: this.setVisibility,
@@ -1883,84 +1884,7 @@ export class accountsList {
defaultSort();
this.showHideSearchOptionsHeader();
- const filterList = document.getElementById("accounts-filter-list");
-
- const fillInFilter = (name: string, value: string, offset?: number) => {
- this._searchBox.value = name + ":" + value + " " + this._searchBox.value;
- this._searchBox.focus();
- let newPos = name.length + 1 + value.length;
- if (typeof offset !== 'undefined')
- newPos += offset;
- this._searchBox.setSelectionRange(newPos, newPos);
- this._searchBox.oninput(null as any);
- };
-
- // Generate filter buttons
- for (let queryName of Object.keys(this._queries)) {
- const query = this._queries[queryName];
- if ("show" in query && !query.show) continue;
- if ("dependsOnElement" in query && query.dependsOnElement) {
- const el = document.querySelector(query.dependsOnElement);
- if (el === null) continue;
- }
-
- const container = document.createElement("span") as HTMLSpanElement;
- container.classList.add("button", "button-xl", "~neutral", "@low", "mb-1", "mr-2");
- container.innerHTML = `
${query.name}`;
- if (query.bool) {
- const pos = document.createElement("button") as HTMLButtonElement;
- pos.type = "button";
- pos.ariaLabel = `Filter by "${query.name}": True`;
- pos.classList.add("button", "~positive", "ml-2");
- pos.innerHTML = `
`;
- pos.addEventListener("click", () => fillInFilter(queryName, "true"));
- const neg = document.createElement("button") as HTMLButtonElement;
- neg.type = "button";
- neg.ariaLabel = `Filter by "${query.name}": False`;
- neg.classList.add("button", "~critical", "ml-2");
- neg.innerHTML = `
`;
- neg.addEventListener("click", () => fillInFilter(queryName, "false"));
-
- container.appendChild(pos);
- container.appendChild(neg);
- }
- if (query.string) {
- const button = document.createElement("button") as HTMLButtonElement;
- button.type = "button";
- button.classList.add("button", "~urge", "ml-2");
- button.innerHTML = `
${window.lang.strings("matchText")}`;
-
- // Position cursor between quotes
- button.addEventListener("click", () => fillInFilter(queryName, `""`, -1));
-
- container.appendChild(button);
- }
- if (query.date) {
- const onDate = document.createElement("button") as HTMLButtonElement;
- onDate.type = "button";
- onDate.classList.add("button", "~urge", "ml-2");
- onDate.innerHTML = `
On Date`;
- onDate.addEventListener("click", () => fillInFilter(queryName, `"="`, -1));
-
- const beforeDate = document.createElement("button") as HTMLButtonElement;
- beforeDate.type = "button";
- beforeDate.classList.add("button", "~urge", "ml-2");
- beforeDate.innerHTML = `
Before Date`;
- beforeDate.addEventListener("click", () => fillInFilter(queryName, `"<"`, -1));
-
- const afterDate = document.createElement("button") as HTMLButtonElement;
- afterDate.type = "button";
- afterDate.classList.add("button", "~urge", "ml-2");
- afterDate.innerHTML = `
After Date`;
- afterDate.addEventListener("click", () => fillInFilter(queryName, `">"`, -1));
-
- container.appendChild(onDate);
- container.appendChild(beforeDate);
- container.appendChild(afterDate);
- }
-
- filterList.appendChild(container);
- }
+ this._search.generateFilterList();
}
reload = () => {
diff --git a/ts/modules/activity.ts b/ts/modules/activity.ts
index 16146e54..b09d42ec 100644
--- a/ts/modules/activity.ts
+++ b/ts/modules/activity.ts
@@ -1,14 +1,15 @@
import { _post, _delete, toDateString } from "../modules/common.js";
+import { Search, SearchConfiguration, QueryType, SearchableItem } from "../modules/search.js";
export interface activity {
- id: string;
- type: string;
- user_id: string;
- source_type: string;
- source: string;
- invite_code: string;
- value: string;
- time: number;
+ id: string;
+ type: string;
+ user_id: string;
+ source_type: string;
+ source: string;
+ invite_code: string;
+ value: string;
+ time: number;
username: string;
source_username: string;
}
@@ -26,11 +27,11 @@ var activityTypeMoods = {
"deleteInvite": -1
};
-var moodColours = ["~warning", "~neutral", "~urge"];
+// var moodColours = ["~warning", "~neutral", "~urge"];
export var activityReload = new CustomEvent("activity-reload");
-export class Activity { // FIXME: Add "implements"
+export class Activity implements activity, SearchableItem { // FIXME: Add "implements"
private _card: HTMLElement;
private _title: HTMLElement;
private _time: HTMLElement;
@@ -64,6 +65,33 @@ export class Activity { // FIXME: Add "implements"
return `
${this._renderInvText()}`;
}
+
+ get accountCreation(): boolean { return this.type == "creation"; }
+ get accountDeletion(): boolean { return this.type == "deletion"; }
+ get accountDisabled(): boolean { return this.type == "disabled"; }
+ get accountEnabled(): boolean { return this.type == "enabled"; }
+ get contactLinked(): boolean { return this.type == "contactLinked"; }
+ get contactUnlinked(): boolean { return this.type == "contactUnlinked"; }
+ get passwordChange(): boolean { return this.type == "changePassword"; }
+ get passwordReset(): boolean { return this.type == "resetPassword"; }
+ get inviteCreated(): boolean { return this.type == "createInvite"; }
+ get inviteDeleted(): boolean { return this.type == "deleteInvite"; }
+
+ get mentionedUsers(): string {
+ return (this.username + " " + this.source_username).toLowerCase();
+ }
+
+ get actor(): string {
+ let out = this.source_type + " ";
+ if (this.source_type == "admin" || this.source_type == "user") out += this.source_username;
+ return out.toLowerCase();
+ }
+
+ get referrer(): string {
+ if (this.type != "creation" || this.source_type != "user") return "";
+ return this.source_username.toLowerCase();
+ }
+
get type(): string { return this._act.type; }
set type(v: string) {
this._act.type = v;
@@ -195,6 +223,29 @@ export class Activity { // FIXME: Add "implements"
}
}
+ get id(): string { return this._act.id; }
+ set id(v: string) { this._act.id = v; }
+
+ get user_id(): string { return this._act.user_id; }
+ set user_id(v: string) { this._act.user_id = v; }
+
+ get username(): string { return this._act.username; }
+ set username(v: string) { this._act.username = v; }
+
+ get source_username(): string { return this._act.source_username; }
+ set source_username(v: string) { this._act.source_username = v; }
+
+ get title(): string { return this._title.textContent; }
+
+ matchesSearch = (query: string): boolean => {
+ // console.log(this.title, "matches", query, ":", this.title.includes(query));
+ return (
+ this.title.toLowerCase().includes(query) ||
+ this.username.toLowerCase().includes(query) ||
+ this.source_username.toLowerCase().includes(query)
+ );
+ }
+
constructor(act: activity) {
this._card = document.createElement("div");
@@ -265,6 +316,26 @@ interface ActivitiesDTO {
export class activityList {
private _activityList: HTMLElement;
+ private _activities: { [id: string]: Activity } = {};
+ private _ordering: string[] = [];
+ private _filterArea = document.getElementById("activity-filter-area");
+ private _searchOptionsHeader = document.getElementById("activity-search-options-header");
+ private _sortingByButton = document.getElementById("activity-sort-by-field") as HTMLButtonElement;
+ private _notFoundPanel = document.getElementById("activity-not-found");
+ private _searchBox = document.getElementById("activity-search") as HTMLInputElement;
+ private _search: Search;
+
+
+ setVisibility = (activities: string[], visible: boolean) => {
+ this._activityList.textContent = ``;
+ for (let id of this._ordering) {
+ if (visible && activities.indexOf(id) != -1) {
+ this._activityList.appendChild(this._activities[id].asElement());
+ } else if (!visible && activities.indexOf(id) == -1) {
+ this._activityList.appendChild(this._activities[id].asElement());
+ }
+ }
+ }
reload = () => {
let send = {
@@ -281,17 +352,165 @@ export class activityList {
}
let resp = req.response as ActivitiesDTO;
- this._activityList.textContent = ``;
-
+ // FIXME: Don't destroy everything each reload!
+ this._activities = {};
for (let act of resp.activities) {
- const activity = new Activity(act);
- this._activityList.appendChild(activity.asElement());
+ this._activities[act.id] = new Activity(act);
+ this._activityList.appendChild(this._activities[act.id].asElement());
+ }
+ this._search.items = this._activities;
+ // FIXME: Actually implement sorting
+ this._ordering = Object.keys(this._activities);
+ this._search.ordering = this._ordering;
+
+ if (this._search.inSearch) {
+ const results = this._search.search(this._searchBox.value);
+ this.setVisibility(results, true);
+ if (results.length == 0) {
+ this._notFoundPanel.classList.remove("unfocused");
+ } else {
+ this._notFoundPanel.classList.add("unfocused");
+ }
+ } else {
+ this.setVisibility(this._ordering, true);
+ this._notFoundPanel.classList.add("unfocused");
}
}, true);
}
+
+ private _queries: { [field: string]: QueryType } = {
+ "id": {
+ name: window.lang.strings("activityID"),
+ getter: "id",
+ bool: false,
+ string: true,
+ date: false
+ },
+ "title": {
+ name: window.lang.strings("title"),
+ getter: "title",
+ bool: false,
+ string: true,
+ date: false
+ },
+ "user": {
+ name: window.lang.strings("usersMentioned"),
+ getter: "mentionedUsers",
+ bool: false,
+ string: true,
+ date: false
+ },
+ "actor": {
+ name: window.lang.strings("actor"),
+ description: window.lang.strings("actorDescription"),
+ getter: "actor",
+ bool: false,
+ string: true,
+ date: false
+ },
+ "referrer": {
+ name: window.lang.strings("referrer"),
+ getter: "referrer",
+ bool: true,
+ string: true,
+ date: false
+ },
+ "date": {
+ name: window.lang.strings("date"),
+ getter: "date",
+ bool: false,
+ string: false,
+ date: true
+ },
+ "account-creation": {
+ name: window.lang.strings("accountCreationFilter"),
+ getter: "accountCreation",
+ bool: true,
+ string: false,
+ date: false
+ },
+ "account-deletion": {
+ name: window.lang.strings("accountDeletionFilter"),
+ getter: "accountDeletion",
+ bool: true,
+ string: false,
+ date: false
+ },
+ "account-disabled": {
+ name: window.lang.strings("accountDisabledFilter"),
+ getter: "accountDisabled",
+ bool: true,
+ string: false,
+ date: false
+ },
+ "account-enabled": {
+ name: window.lang.strings("accountEnabledFilter"),
+ getter: "accountEnabled",
+ bool: true,
+ string: false,
+ date: false
+ },
+ "contact-linked": {
+ name: window.lang.strings("contactLinkedFilter"),
+ getter: "contactLinked",
+ bool: true,
+ string: false,
+ date: false
+ },
+ "contact-unlinked": {
+ name: window.lang.strings("contactUnlinkedFilter"),
+ getter: "contactUnlinked",
+ bool: true,
+ string: false,
+ date: false
+ },
+ "password-change": {
+ name: window.lang.strings("passwordChangeFilter"),
+ getter: "passwordChange",
+ bool: true,
+ string: false,
+ date: false
+ },
+ "password-reset": {
+ name: window.lang.strings("passwordResetFilter"),
+ getter: "passwordReset",
+ bool: true,
+ string: false,
+ date: false
+ },
+ "invite-created": {
+ name: window.lang.strings("inviteCreatedFilter"),
+ getter: "inviteCreated",
+ bool: true,
+ string: false,
+ date: false
+ },
+ "invite-deleted": {
+ name: window.lang.strings("inviteDeletedFilter"),
+ getter: "inviteDeleted",
+ bool: true,
+ string: false,
+ date: false
+ }
+ };
constructor() {
this._activityList = document.getElementById("activity-card-list");
document.addEventListener("activity-reload", this.reload);
+
+ let conf: SearchConfiguration = {
+ filterArea: this._filterArea,
+ sortingByButton: this._sortingByButton,
+ searchOptionsHeader: this._searchOptionsHeader,
+ notFoundPanel: this._notFoundPanel,
+ search: this._searchBox,
+ clearSearchButtonSelector: ".activity-search-clear",
+ queries: this._queries,
+ setVisibility: this.setVisibility,
+ filterList: document.getElementById("activity-filter-list"),
+ onSearchCallback: () => {}
+ }
+ this._search = new Search(conf);
+ this._search.generateFilterList();
}
}
diff --git a/ts/modules/search.ts b/ts/modules/search.ts
index 81b8e16b..0260f605 100644
--- a/ts/modules/search.ts
+++ b/ts/modules/search.ts
@@ -2,6 +2,7 @@ const dateParser = require("any-date-parser");
export interface QueryType {
name: string;
+ description?: string;
getter: string;
bool: boolean;
string: boolean;
@@ -15,6 +16,7 @@ export interface SearchConfiguration {
sortingByButton: HTMLButtonElement;
searchOptionsHeader: HTMLElement;
notFoundPanel: HTMLElement;
+ filterList: HTMLElement;
clearSearchButtonSelector: string;
search: HTMLInputElement;
queries: { [field: string]: QueryType };
@@ -158,7 +160,7 @@ export class Search {
let cachedResult = [...result];
for (let id of cachedResult) {
const u = this._items[id];
- const value = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(u), queryFormat.getter).get.call(u);
+ const value = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(u), queryFormat.getter).get.call(u).toLowerCase();
if (!(value.includes(split[1]))) {
result.splice(result.indexOf(id), 1);
}
@@ -283,6 +285,90 @@ export class Search {
}
}
+ fillInFilter = (name: string, value: string, offset?: number) => {
+ this._c.search.value = name + ":" + value + " " + this._c.search.value;
+ this._c.search.focus();
+ let newPos = name.length + 1 + value.length;
+ if (typeof offset !== 'undefined')
+ newPos += offset;
+ this._c.search.setSelectionRange(newPos, newPos);
+ this._c.search.oninput(null as any);
+ };
+
+ generateFilterList = () => {
+ // Generate filter buttons
+ for (let queryName of Object.keys(this._c.queries)) {
+ const query = this._c.queries[queryName];
+ if ("show" in query && !query.show) continue;
+ if ("dependsOnElement" in query && query.dependsOnElement) {
+ const el = document.querySelector(query.dependsOnElement);
+ if (el === null) continue;
+ }
+
+ const container = document.createElement("span") as HTMLSpanElement;
+ container.classList.add("button", "button-xl", "~neutral", "@low", "mb-1", "mr-2");
+ container.innerHTML = `
+
+ ${query.name}
+ ${query.description || ""}
+
+ `;
+ if (query.bool) {
+ const pos = document.createElement("button") as HTMLButtonElement;
+ pos.type = "button";
+ pos.ariaLabel = `Filter by "${query.name}": True`;
+ pos.classList.add("button", "~positive", "ml-2");
+ pos.innerHTML = `
`;
+ pos.addEventListener("click", () => this.fillInFilter(queryName, "true"));
+ const neg = document.createElement("button") as HTMLButtonElement;
+ neg.type = "button";
+ neg.ariaLabel = `Filter by "${query.name}": False`;
+ neg.classList.add("button", "~critical", "ml-2");
+ neg.innerHTML = `
`;
+ neg.addEventListener("click", () => this.fillInFilter(queryName, "false"));
+
+ container.appendChild(pos);
+ container.appendChild(neg);
+ }
+ if (query.string) {
+ const button = document.createElement("button") as HTMLButtonElement;
+ button.type = "button";
+ button.classList.add("button", "~urge", "ml-2");
+ button.innerHTML = `
${window.lang.strings("matchText")}`;
+
+ // Position cursor between quotes
+ button.addEventListener("click", () => this.fillInFilter(queryName, `""`, -1));
+
+ container.appendChild(button);
+ }
+ if (query.date) {
+ const onDate = document.createElement("button") as HTMLButtonElement;
+ onDate.type = "button";
+ onDate.classList.add("button", "~urge", "ml-2");
+ onDate.innerHTML = `
On Date`;
+ onDate.addEventListener("click", () => this.fillInFilter(queryName, `"="`, -1));
+
+ const beforeDate = document.createElement("button") as HTMLButtonElement;
+ beforeDate.type = "button";
+ beforeDate.classList.add("button", "~urge", "ml-2");
+ beforeDate.innerHTML = `
Before Date`;
+ beforeDate.addEventListener("click", () => this.fillInFilter(queryName, `"<"`, -1));
+
+ const afterDate = document.createElement("button") as HTMLButtonElement;
+ afterDate.type = "button";
+ afterDate.classList.add("button", "~urge", "ml-2");
+ afterDate.innerHTML = `
After Date`;
+ afterDate.addEventListener("click", () => this.fillInFilter(queryName, `">"`, -1));
+
+ container.appendChild(onDate);
+ container.appendChild(beforeDate);
+ container.appendChild(afterDate);
+ }
+
+ this._c.filterList.appendChild(container);
+ }
+ }
+
constructor(c: SearchConfiguration) {
this._c = c;