mirror of https://github.com/hrfee/jfa-go
commit
1be20d471d
@ -0,0 +1,186 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/timshannon/badgerhold/v4"
|
||||
)
|
||||
|
||||
func stringToActivityType(v string) ActivityType {
|
||||
switch v {
|
||||
case "creation":
|
||||
return ActivityCreation
|
||||
case "deletion":
|
||||
return ActivityDeletion
|
||||
case "disabled":
|
||||
return ActivityDisabled
|
||||
case "enabled":
|
||||
return ActivityEnabled
|
||||
case "contactLinked":
|
||||
return ActivityContactLinked
|
||||
case "contactUnlinked":
|
||||
return ActivityContactUnlinked
|
||||
case "changePassword":
|
||||
return ActivityChangePassword
|
||||
case "resetPassword":
|
||||
return ActivityResetPassword
|
||||
case "createInvite":
|
||||
return ActivityCreateInvite
|
||||
case "deleteInvite":
|
||||
return ActivityDeleteInvite
|
||||
}
|
||||
return ActivityUnknown
|
||||
}
|
||||
|
||||
func activityTypeToString(v ActivityType) string {
|
||||
switch v {
|
||||
case ActivityCreation:
|
||||
return "creation"
|
||||
case ActivityDeletion:
|
||||
return "deletion"
|
||||
case ActivityDisabled:
|
||||
return "disabled"
|
||||
case ActivityEnabled:
|
||||
return "enabled"
|
||||
case ActivityContactLinked:
|
||||
return "contactLinked"
|
||||
case ActivityContactUnlinked:
|
||||
return "contactUnlinked"
|
||||
case ActivityChangePassword:
|
||||
return "changePassword"
|
||||
case ActivityResetPassword:
|
||||
return "resetPassword"
|
||||
case ActivityCreateInvite:
|
||||
return "createInvite"
|
||||
case ActivityDeleteInvite:
|
||||
return "deleteInvite"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func stringToActivitySource(v string) ActivitySource {
|
||||
switch v {
|
||||
case "user":
|
||||
return ActivityUser
|
||||
case "admin":
|
||||
return ActivityAdmin
|
||||
case "anon":
|
||||
return ActivityAnon
|
||||
case "daemon":
|
||||
return ActivityDaemon
|
||||
}
|
||||
return ActivityAnon
|
||||
}
|
||||
|
||||
func activitySourceToString(v ActivitySource) string {
|
||||
switch v {
|
||||
case ActivityUser:
|
||||
return "user"
|
||||
case ActivityAdmin:
|
||||
return "admin"
|
||||
case ActivityAnon:
|
||||
return "anon"
|
||||
case ActivityDaemon:
|
||||
return "daemon"
|
||||
}
|
||||
return "anon"
|
||||
}
|
||||
|
||||
// @Summary Get the requested set of activities, Paginated, filtered and sorted.
|
||||
// @Produce json
|
||||
// @Param GetActivitiesDTO body GetActivitiesDTO true "search parameters"
|
||||
// @Success 200 {object} GetActivitiesRespDTO
|
||||
// @Router /activity [post]
|
||||
// @Security Bearer
|
||||
// @tags Activity
|
||||
func (app *appContext) GetActivities(gc *gin.Context) {
|
||||
req := GetActivitiesDTO{}
|
||||
gc.BindJSON(&req)
|
||||
query := &badgerhold.Query{}
|
||||
activityTypes := make([]interface{}, len(req.Type))
|
||||
for i, v := range req.Type {
|
||||
activityTypes[i] = stringToActivityType(v)
|
||||
}
|
||||
if len(activityTypes) != 0 {
|
||||
query = badgerhold.Where("Type").In(activityTypes...)
|
||||
}
|
||||
|
||||
if !req.Ascending {
|
||||
query = query.Reverse()
|
||||
}
|
||||
|
||||
query = query.SortBy("Time")
|
||||
|
||||
if req.Limit == 0 {
|
||||
req.Limit = 10
|
||||
}
|
||||
|
||||
query = query.Skip(req.Page * req.Limit).Limit(req.Limit)
|
||||
|
||||
var results []Activity
|
||||
err := app.storage.db.Find(&results, query)
|
||||
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to read activities from DB: %v\n", err)
|
||||
}
|
||||
|
||||
resp := GetActivitiesRespDTO{
|
||||
Activities: make([]ActivityDTO, len(results)),
|
||||
LastPage: len(results) != req.Limit,
|
||||
}
|
||||
|
||||
for i, act := range results {
|
||||
resp.Activities[i] = ActivityDTO{
|
||||
ID: act.ID,
|
||||
Type: activityTypeToString(act.Type),
|
||||
UserID: act.UserID,
|
||||
SourceType: activitySourceToString(act.SourceType),
|
||||
Source: act.Source,
|
||||
InviteCode: act.InviteCode,
|
||||
Value: act.Value,
|
||||
Time: act.Time.Unix(),
|
||||
}
|
||||
if act.Type == ActivityDeletion || act.Type == ActivityCreation {
|
||||
resp.Activities[i].Username = act.Value
|
||||
resp.Activities[i].Value = ""
|
||||
} else if user, status, err := app.jf.UserByID(act.UserID, false); status == 200 && err == nil {
|
||||
resp.Activities[i].Username = user.Name
|
||||
}
|
||||
|
||||
if (act.SourceType == ActivityUser || act.SourceType == ActivityAdmin) && act.Source != "" {
|
||||
user, status, err := app.jf.UserByID(act.Source, false)
|
||||
if status == 200 && err == nil {
|
||||
resp.Activities[i].SourceUsername = user.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gc.JSON(200, resp)
|
||||
}
|
||||
|
||||
// @Summary Delete the activity with the given ID. No-op if non-existent, always succeeds.
|
||||
// @Produce json
|
||||
// @Param id path string true "ID of activity to delete"
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Router /activity/{id} [delete]
|
||||
// @Security Bearer
|
||||
// @tags Activity
|
||||
func (app *appContext) DeleteActivity(gc *gin.Context) {
|
||||
app.storage.DeleteActivityKey(gc.Param("id"))
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Returns the total number of activities stored in the database.
|
||||
// @Produce json
|
||||
// @Success 200 {object} GetActivityCountDTO
|
||||
// @Router /activity/count [get]
|
||||
// @Security Bearer
|
||||
// @tags Activity
|
||||
func (app *appContext) GetActivityCount(gc *gin.Context) {
|
||||
resp := GetActivityCountDTO{}
|
||||
var err error
|
||||
resp.Count, err = app.storage.db.Count(&Activity{}, &badgerhold.Query{})
|
||||
if err != nil {
|
||||
resp.Count = 0
|
||||
}
|
||||
gc.JSON(200, resp)
|
||||
}
|
@ -0,0 +1,736 @@
|
||||
import { _get, _post, _delete, toDateString, addLoader, removeLoader } from "../modules/common.js";
|
||||
import { Search, SearchConfiguration, QueryType, SearchableItem } from "../modules/search.js";
|
||||
import { accountURLEvent } from "../modules/accounts.js";
|
||||
import { inviteURLEvent } from "../modules/invites.js";
|
||||
|
||||
export interface activity {
|
||||
id: string;
|
||||
type: string;
|
||||
user_id: string;
|
||||
source_type: string;
|
||||
source: string;
|
||||
invite_code: string;
|
||||
value: string;
|
||||
time: number;
|
||||
username: string;
|
||||
source_username: string;
|
||||
}
|
||||
|
||||
var activityTypeMoods = {
|
||||
"creation": 1,
|
||||
"deletion": -1,
|
||||
"disabled": -1,
|
||||
"enabled": 1,
|
||||
"contactLinked": 1,
|
||||
"contactUnlinked": -1,
|
||||
"changePassword": 0,
|
||||
"resetPassword": 0,
|
||||
"createInvite": 1,
|
||||
"deleteInvite": -1
|
||||
};
|
||||
|
||||
// var moodColours = ["~warning", "~neutral", "~urge"];
|
||||
|
||||
export var activityReload = new CustomEvent("activity-reload");
|
||||
|
||||
export class Activity implements activity, SearchableItem {
|
||||
private _card: HTMLElement;
|
||||
private _title: HTMLElement;
|
||||
private _time: HTMLElement;
|
||||
private _timeUnix: number;
|
||||
private _sourceType: HTMLElement;
|
||||
private _source: HTMLElement;
|
||||
private _referrer: HTMLElement;
|
||||
private _expiryTypeBadge: HTMLElement;
|
||||
private _delete: HTMLElement;
|
||||
private _act: activity;
|
||||
private _urlBase: string = ((): string => {
|
||||
let link = window.location.href;
|
||||
for (let split of ["#", "?", "/activity"]) {
|
||||
link = link.split(split)[0];
|
||||
}
|
||||
if (link.slice(-1) != "/") { link += "/"; }
|
||||
return link;
|
||||
})();
|
||||
|
||||
_genUserText = (): string => {
|
||||
return `<span class="font-medium">${this._act.username || this._act.user_id.substring(0, 5)}</span>`;
|
||||
}
|
||||
|
||||
_genSrcUserText = (): string => {
|
||||
return `<span class="font-medium">${this._act.source_username || this._act.source.substring(0, 5)}</span>`;
|
||||
}
|
||||
|
||||
_genUserLink = (): string => {
|
||||
return `<span role="link" tabindex="0" class="hover:underline cursor-pointer activity-pseudo-link-user" data-id="${this._act.user_id}" data-href="${this._urlBase}accounts/user/${this._act.user_id}">${this._genUserText()}</span>`;
|
||||
}
|
||||
|
||||
_genSrcUserLink = (): string => {
|
||||
return `<span role="link" tabindex="0" class="hover:underline cursor-pointer activity-pseudo-link-user" data-id="${this._act.user_id}" data-href="${this._urlBase}accounts/user/${this._act.source}">${this._genSrcUserText()}</span>`;
|
||||
}
|
||||
|
||||
private _renderInvText = (): string => { return `<span class="font-medium font-mono">${this.value || this.invite_code || "???"}</span>`; }
|
||||
|
||||
private _genInvLink = (): string => {
|
||||
return `<span role="link" tabindex="0" class="hover:underline cursor-pointer activity-pseudo-link-invite" data-id="${this.invite_code}" data-href="${this._urlBase}invites/${this.invite_code}">${this._renderInvText()}</span>`;
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
|
||||
let mood = activityTypeMoods[v]; // 1 = positive, 0 = neutral, -1 = negative
|
||||
for (let el of [this._card, this._delete]) {
|
||||
el.classList.remove("~warning");
|
||||
el.classList.remove("~neutral");
|
||||
el.classList.remove("~urge");
|
||||
|
||||
if (mood == -1) {
|
||||
el.classList.add("~warning");
|
||||
} else if (mood == 0) {
|
||||
el.classList.add("~neutral");
|
||||
} else if (mood == 1) {
|
||||
el.classList.add("~urge");
|
||||
}
|
||||
}
|
||||
|
||||
/* for (let i = 0; i < moodColours.length; i++) {
|
||||
if (i-1 == mood) this._card.classList.add(moodColours[i]);
|
||||
else this._card.classList.remove(moodColours[i]);
|
||||
} */
|
||||
|
||||
if (this.type == "changePassword" || this.type == "resetPassword") {
|
||||
let innerHTML = ``;
|
||||
if (this.type == "changePassword") innerHTML = window.lang.strings("accountChangedPassword");
|
||||
else innerHTML = window.lang.strings("accountResetPassword");
|
||||
innerHTML = innerHTML.replace("{user}", this._genUserLink());
|
||||
this._title.innerHTML = innerHTML;
|
||||
} else if (this.type == "contactLinked" || this.type == "contactUnlinked") {
|
||||
let platform = this.value;
|
||||
if (platform == "email") {
|
||||
platform = window.lang.strings("emailAddress");
|
||||
} else {
|
||||
platform = platform.charAt(0).toUpperCase() + platform.slice(1);
|
||||
}
|
||||
let innerHTML = ``;
|
||||
if (this.type == "contactLinked") innerHTML = window.lang.strings("accountLinked");
|
||||
else innerHTML = window.lang.strings("accountUnlinked");
|
||||
innerHTML = innerHTML.replace("{user}", this._genUserLink()).replace("{contactMethod}", platform);
|
||||
this._title.innerHTML = innerHTML;
|
||||
} else if (this.type == "creation") {
|
||||
this._title.innerHTML = window.lang.strings("accountCreated").replace("{user}", this._genUserLink());
|
||||
if (this.source_type == "user") {
|
||||
this._referrer.innerHTML = `<span class="supra mr-2">${window.lang.strings("referrer")}</span>${this._genSrcUserLink()}`;
|
||||
} else {
|
||||
this._referrer.textContent = ``;
|
||||
}
|
||||
} else if (this.type == "deletion") {
|
||||
if (this.source_type == "daemon") {
|
||||
this._title.innerHTML = window.lang.strings("accountExpired").replace("{user}", this._genUserText());
|
||||
this._expiryTypeBadge.classList.add("~critical");
|
||||
this._expiryTypeBadge.classList.remove("~info");
|
||||
this._expiryTypeBadge.textContent = window.lang.strings("deleted");
|
||||
} else {
|
||||
this._title.innerHTML = window.lang.strings("accountDeleted").replace("{user}", this._genUserText());
|
||||
}
|
||||
} else if (this.type == "enabled") {
|
||||
this._title.innerHTML = window.lang.strings("accountReEnabled").replace("{user}", this._genUserLink());
|
||||
} else if (this.type == "disabled") {
|
||||
if (this.source_type == "daemon") {
|
||||
this._title.innerHTML = window.lang.strings("accountExpired").replace("{user}", this._genUserLink());
|
||||
this._expiryTypeBadge.classList.add("~info");
|
||||
this._expiryTypeBadge.classList.remove("~critical");
|
||||
this._expiryTypeBadge.textContent = window.lang.strings("disabled");
|
||||
} else {
|
||||
this._title.innerHTML = window.lang.strings("accountDisabled").replace("{user}", this._genUserLink());
|
||||
}
|
||||
} else if (this.type == "createInvite") {
|
||||
this._title.innerHTML = window.lang.strings("inviteCreated").replace("{invite}", this._genInvLink());
|
||||
} else if (this.type == "deleteInvite") {
|
||||
let innerHTML = ``;
|
||||
if (this.source_type == "daemon") {
|
||||
innerHTML = window.lang.strings("inviteExpired");
|
||||
} else {
|
||||
innerHTML = window.lang.strings("inviteDeleted");
|
||||
}
|
||||
|
||||
this._title.innerHTML = innerHTML.replace("{invite}", this._renderInvText());
|
||||
}
|
||||
}
|
||||
|
||||
get time(): number { return this._timeUnix; }
|
||||
set time(v: number) {
|
||||
this._timeUnix = v;
|
||||
this._time.textContent = toDateString(new Date(v*1000));
|
||||
}
|
||||
|
||||
get source_type(): string { return this._act.source_type; }
|
||||
set source_type(v: string) {
|
||||
this._act.source_type = v;
|
||||
if ((this.source_type == "anon" || this.source_type == "user") && this.type == "creation") {
|
||||
this._sourceType.textContent = window.lang.strings("fromInvite");
|
||||
} else if (this.source_type == "admin") {
|
||||
this._sourceType.textContent = window.lang.strings("byAdmin");
|
||||
} else if (this.source_type == "user" && this.type != "creation") {
|
||||
this._sourceType.textContent = window.lang.strings("byUser");
|
||||
} else if (this.source_type == "daemon") {
|
||||
this._sourceType.textContent = window.lang.strings("byJfaGo");
|
||||
}
|
||||
}
|
||||
|
||||
get invite_code(): string { return this._act.invite_code; }
|
||||
set invite_code(v: string) {
|
||||
this._act.invite_code = v;
|
||||
}
|
||||
|
||||
get value(): string { return this._act.value; }
|
||||
set value(v: string) {
|
||||
this._act.value = v;
|
||||
}
|
||||
|
||||
get source(): string { return this._act.source; }
|
||||
set source(v: string) {
|
||||
this._act.source = v;
|
||||
if ((this.source_type == "anon" || this.source_type == "user") && this.type == "creation") {
|
||||
this._source.innerHTML = this._genInvLink();
|
||||
} else if ((this.source_type == "admin" || this.source_type == "user") && this._act.source != "" && this._act.source_username != "") {
|
||||
this._source.innerHTML = this._genSrcUserLink();
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
this._card.classList.add("card", "@low", "my-2");
|
||||
this._card.innerHTML = `
|
||||
<div class="flex flex-col md:flex-row justify-between mb-2">
|
||||
<span class="heading truncate flex-initial md:text-2xl text-xl activity-title"></span>
|
||||
<div class="flex flex-col flex-none ml-0 md:ml-2">
|
||||
<span class="font-medium md:text-sm text-xs activity-time" aria-label="${window.lang.strings("date")}"></span>
|
||||
<span class="activity-expiry-type badge self-start md:self-end mt-1"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row justify-between">
|
||||
<div>
|
||||
<span class="content supra mr-2 activity-source-type"></span><span class="activity-source"></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="content activity-referrer"></span>
|
||||
</div>
|
||||
<div>
|
||||
<button class="button @low hover:~critical rounded-full px-1 py-px activity-delete" aria-label="${window.lang.strings("delete")}"><i class="ri-close-line"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this._title = this._card.querySelector(".activity-title");
|
||||
this._time = this._card.querySelector(".activity-time");
|
||||
this._sourceType = this._card.querySelector(".activity-source-type");
|
||||
this._source = this._card.querySelector(".activity-source");
|
||||
this._referrer = this._card.querySelector(".activity-referrer");
|
||||
this._expiryTypeBadge = this._card.querySelector(".activity-expiry-type");
|
||||
this._delete = this._card.querySelector(".activity-delete");
|
||||
|
||||
document.addEventListener("timefmt-change", () => {
|
||||
this.time = this.time;
|
||||
});
|
||||
|
||||
this._delete.addEventListener("click", this.delete);
|
||||
|
||||
this.update(act);
|
||||
|
||||
const pseudoUsers = this._card.getElementsByClassName("activity-pseudo-link-user") as HTMLCollectionOf<HTMLAnchorElement>;
|
||||
const pseudoInvites = this._card.getElementsByClassName("activity-pseudo-link-invite") as HTMLCollectionOf<HTMLAnchorElement>;
|
||||
|
||||
for (let i = 0; i < pseudoUsers.length; i++) {
|
||||
const navigate = (event: Event) => {
|
||||
event.preventDefault()
|
||||
window.tabs.switch("accounts");
|
||||
document.dispatchEvent(accountURLEvent(pseudoUsers[i].getAttribute("data-id")));
|
||||
window.history.pushState(null, document.title, pseudoUsers[i].getAttribute("data-href"));
|
||||
}
|
||||
pseudoUsers[i].onclick = navigate;
|
||||
pseudoUsers[i].onkeydown = navigate;
|
||||
}
|
||||
for (let i = 0; i < pseudoInvites.length; i++) {
|
||||
const navigate = (event: Event) => {
|
||||
event.preventDefault();
|
||||
window.invites.reload(() => {
|
||||
window.tabs.switch("invites");
|
||||
document.dispatchEvent(inviteURLEvent(pseudoInvites[i].getAttribute("data-id")));
|
||||
window.history.pushState(null, document.title, pseudoInvites[i].getAttribute("data-href"));
|
||||
});
|
||||
}
|
||||
pseudoInvites[i].onclick = navigate;
|
||||
pseudoInvites[i].onkeydown = navigate;
|
||||
}
|
||||
}
|
||||
|
||||
update = (act: activity) => {
|
||||
this._act = act;
|
||||
this.source_type = act.source_type;
|
||||
this.invite_code = act.invite_code;
|
||||
this.time = act.time;
|
||||
this.source = act.source;
|
||||
this.value = act.value;
|
||||
this.type = act.type;
|
||||
}
|
||||
|
||||
delete = () => _delete("/activity/" + this._act.id, null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState != 4) return;
|
||||
if (req.status == 200) {
|
||||
window.notifications.customSuccess("activityDeleted", window.lang.notif("activityDeleted"));
|
||||
}
|
||||
document.dispatchEvent(activityReload);
|
||||
});
|
||||
|
||||
asElement = () => { return this._card; };
|
||||
}
|
||||
|
||||
interface ActivitiesDTO {
|
||||
activities: activity[];
|
||||
last_page: boolean;
|
||||
}
|
||||
|
||||
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 _sortDirection = document.getElementById("activity-sort-direction") as HTMLButtonElement;
|
||||
private _loader = document.getElementById("activity-loader");
|
||||
private _loadMoreButton = document.getElementById("activity-load-more") as HTMLButtonElement;
|
||||
private _loadAllButton = document.getElementById("activity-load-all") as HTMLButtonElement;
|
||||
private _refreshButton = document.getElementById("activity-refresh") as HTMLButtonElement;
|
||||
private _keepSearchingDescription = document.getElementById("activity-keep-searching-description");
|
||||
private _keepSearchingButton = document.getElementById("activity-keep-searching");
|
||||
|
||||
private _totalRecords = document.getElementById("activity-total-records");
|
||||
private _loadedRecords = document.getElementById("activity-loaded-records");
|
||||
private _shownRecords = document.getElementById("activity-shown-records");
|
||||
|
||||
private _total: number;
|
||||
private _loaded: number;
|
||||
private _shown: number;
|
||||
|
||||
get total(): number { return this._total; }
|
||||
set total(v: number) {
|
||||
this._total = v;
|
||||
this._totalRecords.textContent = window.lang.var("strings", "totalRecords", `${v}`);
|
||||
}
|
||||
|
||||
get loaded(): number { return this._loaded; }
|
||||
set loaded(v: number) {
|
||||
this._loaded = v;
|
||||
this._loadedRecords.textContent = window.lang.var("strings", "loadedRecords", `${v}`);
|
||||
}
|
||||
|
||||
get shown(): number { return this._shown; }
|
||||
set shown(v: number) {
|
||||
this._shown = v;
|
||||
this._shownRecords.textContent = window.lang.var("strings", "shownRecords", `${v}`);
|
||||
}
|
||||
|
||||
private _search: Search;
|
||||
private _ascending: boolean;
|
||||
private _hasLoaded: boolean;
|
||||
private _lastLoad: number;
|
||||
private _page: number = 0;
|
||||
private _lastPage: boolean;
|
||||
|
||||
|
||||
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 = () => {
|
||||
this._lastLoad = Date.now();
|
||||
this._lastPage = false;
|
||||
this._loadMoreButton.textContent = window.lang.strings("loadMore");
|
||||
this._loadMoreButton.disabled = false;
|
||||
this._loadAllButton.classList.remove("unfocused");
|
||||
this._loadAllButton.disabled = false;
|
||||
|
||||
this.total = 0;
|
||||
this.loaded = 0;
|
||||
this.shown = 0;
|
||||
|
||||
// this._page = 0;
|
||||
let limit = 10;
|
||||
if (this._page != 0) {
|
||||
limit *= this._page+1;
|
||||
};
|
||||
|
||||
let send = {
|
||||
"type": [],
|
||||
"limit": limit,
|
||||
"page": 0,
|
||||
"ascending": this.ascending
|
||||
}
|
||||
|
||||
_get("/activity/count", null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState != 4 || req.status != 200) return;
|
||||
this.total = req.response["count"] as number;
|
||||
});
|
||||
|
||||
_post("/activity", send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState != 4) return;
|
||||
if (req.status != 200) {
|
||||
window.notifications.customError("loadActivitiesError", window.lang.notif("errorLoadActivities"));
|
||||
return;
|
||||
}
|
||||
|
||||
this._hasLoaded = true;
|
||||
// Allow refreshes every 15s
|
||||
this._refreshButton.disabled = true;
|
||||
setTimeout(() => this._refreshButton.disabled = false, 15000);
|
||||
|
||||
let resp = req.response as ActivitiesDTO;
|
||||
// FIXME: Don't destroy everything each reload!
|
||||
this._activities = {};
|
||||
this._ordering = [];
|
||||
|
||||
for (let act of resp.activities) {
|
||||
this._activities[act.id] = new Activity(act);
|
||||
this._ordering.push(act.id);
|
||||
}
|
||||
this._search.items = this._activities;
|
||||
this._search.ordering = this._ordering;
|
||||
|
||||
this.loaded = this._ordering.length;
|
||||
|
||||
if (this._search.inSearch) {
|
||||
this._search.onSearchBoxChange(true);
|
||||
this._loadAllButton.classList.remove("unfocused");
|
||||
} else {
|
||||
this.shown = this.loaded;
|
||||
this.setVisibility(this._ordering, true);
|
||||
this._loadAllButton.classList.add("unfocused");
|
||||
this._notFoundPanel.classList.add("unfocused");
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
loadMore = (callback?: () => void, loadAll: boolean = false) => {
|
||||
this._lastLoad = Date.now();
|
||||
this._loadMoreButton.disabled = true;
|
||||
// this._loadAllButton.disabled = true;
|
||||
const timeout = setTimeout(() => {
|
||||
this._loadMoreButton.disabled = false;
|
||||
// this._loadAllButton.disabled = false;
|
||||
}, 1000);
|
||||
this._page += 1;
|
||||
|
||||
let send = {
|
||||
"type": [],
|
||||
"limit": 10,
|
||||
"page": this._page,
|
||||
"ascending": this._ascending
|
||||
};
|
||||
|
||||
// this._activityList.classList.add("unfocused");
|
||||
// addLoader(this._loader, false, true);
|
||||
|
||||
_post("/activity", send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState != 4) return;
|
||||
if (req.status != 200) {
|
||||
window.notifications.customError("loadActivitiesError", window.lang.notif("errorLoadActivities"));
|
||||
return;
|
||||
}
|
||||
|
||||
let resp = req.response as ActivitiesDTO;
|
||||
|
||||
this._lastPage = resp.last_page;
|
||||
if (this._lastPage) {
|
||||
clearTimeout(timeout);
|
||||
this._loadMoreButton.disabled = true;
|
||||
removeLoader(this._loadAllButton);
|
||||
this._loadAllButton.classList.add("unfocused");
|
||||
this._loadMoreButton.textContent = window.lang.strings("noMoreResults");
|
||||
}
|
||||
|
||||
for (let act of resp.activities) {
|
||||
this._activities[act.id] = new Activity(act);
|
||||
this._ordering.push(act.id);
|
||||
}
|
||||
// this._search.items = this._activities;
|
||||
// this._search.ordering = this._ordering;
|
||||
|
||||
this.loaded = this._ordering.length;
|
||||
|
||||
if (this._search.inSearch || loadAll) {
|
||||
if (this._lastPage) {
|
||||
loadAll = false;
|
||||
}
|
||||
this._search.onSearchBoxChange(true, loadAll);
|
||||
} else {
|
||||
this.setVisibility(this._ordering, true);
|
||||
this._notFoundPanel.classList.add("unfocused");
|
||||
}
|
||||
|
||||
if (callback) callback();
|
||||
// removeLoader(this._loader);
|
||||
// this._activityList.classList.remove("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
|
||||
}
|
||||
};
|
||||
|
||||
get ascending(): boolean { return this._ascending; }
|
||||
set ascending(v: boolean) {
|
||||
this._ascending = v;
|
||||
this._sortDirection.innerHTML = `${window.lang.strings("sortDirection")} <i class="ri-arrow-${v ? "up" : "down"}-s-line ml-2"></i>`;
|
||||
if (this._hasLoaded) {
|
||||
this.reload();
|
||||
}
|
||||
}
|
||||
|
||||
detectScroll = () => {
|
||||
if (!this._hasLoaded) return;
|
||||
// console.log(window.innerHeight + document.documentElement.scrollTop, document.scrollingElement.scrollHeight);
|
||||
if (Math.abs(window.innerHeight + document.documentElement.scrollTop - document.scrollingElement.scrollHeight) < 50) {
|
||||
// window.notifications.customSuccess("scroll", "Reached bottom.");
|
||||
// Wait .5s between loads
|
||||
if (this._lastLoad + 500 > Date.now()) return;
|
||||
this.loadMore();
|
||||
}
|
||||
}
|
||||
|
||||
private _prevResultCount = 0;
|
||||
|
||||
private _notFoundCallback = (notFound: boolean) => {
|
||||
if (notFound) this._loadMoreButton.classList.add("unfocused");
|
||||
else this._loadMoreButton.classList.remove("unfocused");
|
||||
|
||||
if (notFound && !this._lastPage) {
|
||||
this._keepSearchingButton.classList.remove("unfocused");
|
||||
this._keepSearchingDescription.classList.remove("unfocused");
|
||||
} else {
|
||||
this._keepSearchingButton.classList.add("unfocused");
|
||||
this._keepSearchingDescription.classList.add("unfocused");
|
||||
}
|
||||
};
|
||||
|
||||
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"),
|
||||
// notFoundCallback: this._notFoundCallback,
|
||||
onSearchCallback: (visibleCount: number, newItems: boolean, loadAll: boolean) => {
|
||||
this.shown = visibleCount;
|
||||
|
||||
if (this._search.inSearch && !this._lastPage) this._loadAllButton.classList.remove("unfocused");
|
||||
else this._loadAllButton.classList.add("unfocused");
|
||||
|
||||
if (visibleCount < 10 || loadAll) {
|
||||
if (!newItems || this._prevResultCount != visibleCount || (visibleCount == 0 && !this._lastPage) || loadAll) this.loadMore(() => {}, loadAll);
|
||||
}
|
||||
this._prevResultCount = visibleCount;
|
||||
}
|
||||
}
|
||||
this._search = new Search(conf);
|
||||
this._search.generateFilterList();
|
||||
|
||||
this._hasLoaded = false;
|
||||
this.ascending = false;
|
||||
this._sortDirection.addEventListener("click", () => this.ascending = !this.ascending);
|
||||
|
||||
this._loadMoreButton.onclick = () => this.loadMore();
|
||||
this._loadAllButton.onclick = () => {
|
||||
addLoader(this._loadAllButton, true);
|
||||
this.loadMore(() => {}, true);
|
||||
};
|
||||
/* this._keepSearchingButton.onclick = () => {
|
||||
addLoader(this._keepSearchingButton, true);
|
||||
this.loadMore(() => removeLoader(this._keepSearchingButton, true));
|
||||
}; */
|
||||
this._refreshButton.onclick = this.reload;
|
||||
|
||||
window.onscroll = this.detectScroll;
|
||||
}
|
||||
}
|
@ -0,0 +1,390 @@
|
||||
const dateParser = require("any-date-parser");
|
||||
|
||||
export interface QueryType {
|
||||
name: string;
|
||||
description?: string;
|
||||
getter: string;
|
||||
bool: boolean;
|
||||
string: boolean;
|
||||
date: boolean;
|
||||
dependsOnElement?: string; // Format for querySelector
|
||||
show?: boolean;
|
||||
}
|
||||
|
||||
export interface SearchConfiguration {
|
||||
filterArea: HTMLElement;
|
||||
sortingByButton: HTMLButtonElement;
|
||||
searchOptionsHeader: HTMLElement;
|
||||
notFoundPanel: HTMLElement;
|
||||
notFoundCallback?: (notFound: boolean) => void;
|
||||
filterList: HTMLElement;
|
||||
clearSearchButtonSelector: string;
|
||||
search: HTMLInputElement;
|
||||
queries: { [field: string]: QueryType };
|
||||
setVisibility: (items: string[], visible: boolean) => void;
|
||||
onSearchCallback: (visibleCount: number, newItems: boolean, loadAll: boolean) => void;
|
||||
loadMore?: () => void;
|
||||
}
|
||||
|
||||
export interface SearchableItem {
|
||||
matchesSearch: (query: string) => boolean;
|
||||
}
|
||||
|
||||
export class Search {
|
||||
private _c: SearchConfiguration;
|
||||
private _ordering: string[] = [];
|
||||
private _items: { [id: string]: SearchableItem };
|
||||
inSearch: boolean;
|
||||
|
||||
search = (query: String): string[] => {
|
||||
this._c.filterArea.textContent = "";
|
||||
|
||||
query = query.toLowerCase();
|
||||
|
||||
let result: string[] = [...this._ordering];
|
||||
let words: string[] = [];
|
||||
|
||||
let quoteSymbol = ``;
|
||||
let queryStart = -1;
|
||||
let lastQuote = -1;
|
||||
for (let i = 0; i < query.length; i++) {
|
||||
if (queryStart == -1 && query[i] != " " && query[i] != `"` && query[i] != `'`) {
|
||||
queryStart = i;
|
||||
}
|
||||
if ((query[i] == `"` || query[i] == `'`) && (quoteSymbol == `` || query[i] == quoteSymbol)) {
|
||||
if (lastQuote != -1) {
|
||||
lastQuote = -1;
|
||||
quoteSymbol = ``;
|
||||
} else {
|
||||
lastQuote = i;
|
||||
quoteSymbol = query[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (query[i] == " " || i == query.length-1) {
|
||||
if (lastQuote != -1) {
|
||||
continue;
|
||||
} else {
|
||||
let end = i+1;
|
||||
if (query[i] == " ") {
|
||||
end = i;
|
||||
while (i+1 < query.length && query[i+1] == " ") {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
words.push(query.substring(queryStart, end).replace(/['"]/g, ""));
|
||||
console.log("pushed", words);
|
||||
queryStart = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query = "";
|
||||
for (let word of words) {
|
||||
if (!word.includes(":")) {
|
||||
let cachedResult = [...result];
|
||||
for (let id of cachedResult) {
|
||||
const u = this._items[id];
|
||||
if (!u.matchesSearch(word)) {
|
||||
result.splice(result.indexOf(id), 1);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const split = [word.substring(0, word.indexOf(":")), word.substring(word.indexOf(":")+1)];
|
||||
|
||||
if (!(split[0] in this._c.queries)) continue;
|
||||
|
||||
const queryFormat = this._c.queries[split[0]];
|
||||
|
||||
if (queryFormat.bool) {
|
||||
let isBool = false;
|
||||
let boolState = false;
|
||||
if (split[1] == "true" || split[1] == "yes" || split[1] == "t" || split[1] == "y") {
|
||||
isBool = true;
|
||||
boolState = true;
|
||||
} else if (split[1] == "false" || split[1] == "no" || split[1] == "f" || split[1] == "n") {
|
||||
isBool = true;
|
||||
boolState = false;
|
||||
}
|
||||
if (isBool) {
|
||||
const filterCard = document.createElement("span");
|
||||
filterCard.ariaLabel = window.lang.strings("clickToRemoveFilter");
|
||||
filterCard.classList.add("button", "~" + (boolState ? "positive" : "critical"), "@high", "center", "mx-2", "h-full");
|
||||
filterCard.innerHTML = `
|
||||
<span class="font-bold mr-2">${queryFormat.name}</span>
|
||||
<i class="text-2xl ri-${boolState? "checkbox" : "close"}-circle-fill"></i>
|
||||
`;
|
||||
|
||||
filterCard.addEventListener("click", () => {
|
||||
for (let quote of [`"`, `'`, ``]) {
|
||||
this._c.search.value = this._c.search.value.replace(split[0] + ":" + quote + split[1] + quote, "");
|
||||
}
|
||||
this._c.search.oninput((null as Event));
|
||||
})
|
||||
|
||||
this._c.filterArea.appendChild(filterCard);
|
||||
|
||||
// console.log("is bool, state", boolState);
|
||||
// So removing elements doesn't affect us
|
||||
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);
|
||||
// console.log("got", queryFormat.getter + ":", value);
|
||||
// Remove from result if not matching query
|
||||
if (!((value && boolState) || (!value && !boolState))) {
|
||||
// console.log("not matching, result is", result);
|
||||
result.splice(result.indexOf(id), 1);
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (queryFormat.string) {
|
||||
const filterCard = document.createElement("span");
|
||||
filterCard.ariaLabel = window.lang.strings("clickToRemoveFilter");
|
||||
filterCard.classList.add("button", "~neutral", "@low", "center", "mx-2", "h-full");
|
||||
filterCard.innerHTML = `
|
||||
<span class="font-bold mr-2">${queryFormat.name}:</span> "${split[1]}"
|
||||
`;
|
||||
|
||||
filterCard.addEventListener("click", () => {
|
||||
for (let quote of [`"`, `'`, ``]) {
|
||||
let regex = new RegExp(split[0] + ":" + quote + split[1] + quote, "ig");
|
||||
this._c.search.value = this._c.search.value.replace(regex, "");
|
||||
}
|
||||
this._c.search.oninput((null as Event));
|
||||
})
|
||||
|
||||
this._c.filterArea.appendChild(filterCard);
|
||||
|
||||
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).toLowerCase();
|
||||
if (!(value.includes(split[1]))) {
|
||||
result.splice(result.indexOf(id), 1);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (queryFormat.date) {
|
||||
// -1 = Before, 0 = On, 1 = After, 2 = No symbol, assume 0
|
||||
let compareType = (split[1][0] == ">") ? 1 : ((split[1][0] == "<") ? -1 : ((split[1][0] == "=") ? 0 : 2));
|
||||
let unmodifiedValue = split[1];
|
||||
if (compareType != 2) {
|
||||
split[1] = split[1].substring(1);
|
||||
}
|
||||
if (compareType == 2) compareType = 0;
|
||||
|
||||
let attempt: { year?: number, month?: number, day?: number, hour?: number, minute?: number } = dateParser.attempt(split[1]);
|
||||
// Month in Date objects is 0-based, so make our parsed date that way too
|
||||
if ("month" in attempt) attempt.month -= 1;
|
||||
|
||||
let date: Date = (Date as any).fromString(split[1]) as Date;
|
||||
console.log("Read", attempt, "and", date);
|
||||
if ("invalid" in (date as any)) continue;
|
||||
|
||||
const filterCard = document.createElement("span");
|
||||
filterCard.ariaLabel = window.lang.strings("clickToRemoveFilter");
|
||||
filterCard.classList.add("button", "~neutral", "@low", "center", "m-2", "h-full");
|
||||
filterCard.innerHTML = `
|
||||
<span class="font-bold mr-2">${queryFormat.name}:</span> ${(compareType == 1) ? window.lang.strings("after")+" " : ((compareType == -1) ? window.lang.strings("before")+" " : "")}${split[1]}
|
||||
`;
|
||||
|
||||
filterCard.addEventListener("click", () => {
|
||||
for (let quote of [`"`, `'`, ``]) {
|
||||
let regex = new RegExp(split[0] + ":" + quote + unmodifiedValue + quote, "ig");
|
||||
this._c.search.value = this._c.search.value.replace(regex, "");
|
||||
}
|
||||
|
||||
this._c.search.oninput((null as Event));
|
||||
})
|
||||
|
||||
this._c.filterArea.appendChild(filterCard);
|
||||
|
||||
let cachedResult = [...result];
|
||||
for (let id of cachedResult) {
|
||||
const u = this._items[id];
|
||||
const unixValue = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(u), queryFormat.getter).get.call(u);
|
||||
if (unixValue == 0) {
|
||||
result.splice(result.indexOf(id), 1);
|
||||
continue;
|
||||
}
|
||||
let value = new Date(unixValue*1000);
|
||||
|
||||
const getterPairs: [string, () => number][] = [["year", Date.prototype.getFullYear], ["month", Date.prototype.getMonth], ["day", Date.prototype.getDate], ["hour", Date.prototype.getHours], ["minute", Date.prototype.getMinutes]];
|
||||
|
||||
// When doing > or < <time> with no date, we need to ignore the rest of the Date object
|
||||
if (compareType != 0 && Object.keys(attempt).length == 2 && "hour" in attempt && "minute" in attempt) {
|
||||
const temp = new Date(date.valueOf());
|
||||
temp.setHours(value.getHours(), value.getMinutes());
|
||||
value = temp;
|
||||
console.log("just hours/minutes workaround, value set to", value);
|
||||
}
|
||||
|
||||
|
||||
let match = true;
|
||||
if (compareType == 0) {
|
||||
for (let pair of getterPairs) {
|
||||
if (pair[0] in attempt) {
|
||||
if (compareType == 0 && attempt[pair[0]] != pair[1].call(value)) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (compareType == -1) {
|
||||
match = (value < date);
|
||||
} else if (compareType == 1) {
|
||||
match = (value > date);
|
||||
}
|
||||
if (!match) {
|
||||
result.splice(result.indexOf(id), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
showHideSearchOptionsHeader = () => {
|
||||
const sortingBy = !(this._c.sortingByButton.parentElement.classList.contains("hidden"));
|
||||
const hasFilters = this._c.filterArea.textContent != "";
|
||||
console.log("sortingBy", sortingBy, "hasFilters", hasFilters);
|
||||
if (sortingBy || hasFilters) {
|
||||
this._c.searchOptionsHeader.classList.remove("hidden");
|
||||
} else {
|
||||
this._c.searchOptionsHeader.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
get items(): { [id: string]: SearchableItem } { return this._items; }
|
||||
set items(v: { [id: string]: SearchableItem }) {
|
||||
this._items = v;
|
||||
}
|
||||
|
||||
get ordering(): string[] { return this._ordering; }
|
||||
set ordering(v: string[]) { this._ordering = v; }
|
||||
|
||||
onSearchBoxChange = (newItems: boolean = false, loadAll: boolean = false) => {
|
||||
const query = this._c.search.value;
|
||||
if (!query) {
|
||||
this.inSearch = false;
|
||||
} else {
|
||||
this.inSearch = true;
|
||||
}
|
||||
const results = this.search(query);
|
||||
this._c.setVisibility(results, true);
|
||||
this._c.onSearchCallback(results.length, newItems, loadAll);
|
||||
this.showHideSearchOptionsHeader();
|
||||
if (results.length == 0) {
|
||||
this._c.notFoundPanel.classList.remove("unfocused");
|
||||
} else {
|
||||
this._c.notFoundPanel.classList.add("unfocused");
|
||||
}
|
||||
if (this._c.notFoundCallback) this._c.notFoundCallback(results.length == 0);
|
||||
}
|
||||
|
||||
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 = `
|
||||
<div class="flex flex-col mr-2">
|
||||
<span>${query.name}</span>
|
||||
<span class="support">${query.description || ""}</span>
|
||||
</div>
|
||||
`;
|
||||
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 = `<i class="ri-checkbox-circle-fill"></i>`;
|
||||
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 = `<i class="ri-close-circle-fill"></i>`;
|
||||
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 = `<i class="ri-equal-line mr-2"></i>${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 = `<i class="ri-calendar-check-line mr-2"></i>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 = `<i class="ri-calendar-check-line mr-2"></i>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 = `<i class="ri-calendar-check-line mr-2"></i>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;
|
||||
|
||||
this._c.search.oninput = () => this.onSearchBoxChange();
|
||||
|
||||
const clearSearchButtons = Array.from(document.querySelectorAll(this._c.clearSearchButtonSelector)) as Array<HTMLSpanElement>;
|
||||
for (let b of clearSearchButtons) {
|
||||
b.addEventListener("click", () => {
|
||||
this._c.search.value = "";
|
||||
this.onSearchBoxChange();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in new issue