mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-29 12:30:11 +00:00
Compare commits
5 Commits
a73dfddd3f
...
a0db685af2
Author | SHA1 | Date | |
---|---|---|---|
a0db685af2 | |||
4fa0630aef | |||
3cad30a8e5 | |||
44172074b9 | |||
1032e4e747 |
@ -89,7 +89,7 @@ func activitySourceToString(v ActivitySource) string {
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param GetActivitiesDTO body GetActivitiesDTO true "search parameters"
|
// @Param GetActivitiesDTO body GetActivitiesDTO true "search parameters"
|
||||||
// @Success 200 {object} GetActivitiesRespDTO
|
// @Success 200 {object} GetActivitiesRespDTO
|
||||||
// @Router /activity [get]
|
// @Router /activity [post]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
// @tags Activity
|
// @tags Activity
|
||||||
func (app *appContext) GetActivities(gc *gin.Context) {
|
func (app *appContext) GetActivities(gc *gin.Context) {
|
||||||
@ -138,7 +138,32 @@ func (app *appContext) GetActivities(gc *gin.Context) {
|
|||||||
Value: act.Value,
|
Value: act.Value,
|
||||||
Time: act.Time.Unix(),
|
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)
|
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)
|
||||||
|
}
|
||||||
|
@ -90,6 +90,7 @@ func (app *appContext) checkInvites() {
|
|||||||
Type: ActivityDeleteInvite,
|
Type: ActivityDeleteInvite,
|
||||||
SourceType: ActivityDaemon,
|
SourceType: ActivityDaemon,
|
||||||
InviteCode: data.Code,
|
InviteCode: data.Code,
|
||||||
|
Value: data.Label,
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -141,6 +142,7 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
|
|||||||
Type: ActivityDeleteInvite,
|
Type: ActivityDeleteInvite,
|
||||||
SourceType: ActivityDaemon,
|
SourceType: ActivityDaemon,
|
||||||
InviteCode: code,
|
InviteCode: code,
|
||||||
|
Value: inv.Label,
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
})
|
})
|
||||||
} else if used {
|
} else if used {
|
||||||
@ -153,6 +155,7 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
|
|||||||
Type: ActivityDeleteInvite,
|
Type: ActivityDeleteInvite,
|
||||||
SourceType: ActivityDaemon,
|
SourceType: ActivityDaemon,
|
||||||
InviteCode: code,
|
InviteCode: code,
|
||||||
|
Value: inv.Label,
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
})
|
})
|
||||||
} else if newInv.RemainingUses != 0 {
|
} else if newInv.RemainingUses != 0 {
|
||||||
@ -460,8 +463,7 @@ func (app *appContext) DeleteInvite(gc *gin.Context) {
|
|||||||
var req deleteInviteDTO
|
var req deleteInviteDTO
|
||||||
gc.BindJSON(&req)
|
gc.BindJSON(&req)
|
||||||
app.debug.Printf("%s: Deletion requested", req.Code)
|
app.debug.Printf("%s: Deletion requested", req.Code)
|
||||||
var ok bool
|
inv, ok := app.storage.GetInvitesKey(req.Code)
|
||||||
_, ok = app.storage.GetInvitesKey(req.Code)
|
|
||||||
if ok {
|
if ok {
|
||||||
app.storage.DeleteInvitesKey(req.Code)
|
app.storage.DeleteInvitesKey(req.Code)
|
||||||
|
|
||||||
@ -470,6 +472,8 @@ func (app *appContext) DeleteInvite(gc *gin.Context) {
|
|||||||
Type: ActivityDeleteInvite,
|
Type: ActivityDeleteInvite,
|
||||||
SourceType: ActivityAdmin,
|
SourceType: ActivityAdmin,
|
||||||
Source: gc.GetString("jfId"),
|
Source: gc.GetString("jfId"),
|
||||||
|
InviteCode: req.Code,
|
||||||
|
Value: inv.Label,
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
15
api-users.go
15
api-users.go
@ -53,6 +53,7 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
|
|||||||
UserID: id,
|
UserID: id,
|
||||||
SourceType: ActivityAdmin,
|
SourceType: ActivityAdmin,
|
||||||
Source: gc.GetString("jfId"),
|
Source: gc.GetString("jfId"),
|
||||||
|
Value: user.Name,
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -328,6 +329,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
|||||||
SourceType: sourceType,
|
SourceType: sourceType,
|
||||||
Source: source,
|
Source: source,
|
||||||
InviteCode: invite.Code,
|
InviteCode: invite.Code,
|
||||||
|
Value: user.Name,
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -648,6 +650,12 @@ func (app *appContext) DeleteUsers(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
username := ""
|
||||||
|
if user, status, err := app.jf.UserByID(userID, false); status == 200 && err == nil {
|
||||||
|
username = user.Name
|
||||||
|
}
|
||||||
|
|
||||||
status, err := app.jf.DeleteUser(userID)
|
status, err := app.jf.DeleteUser(userID)
|
||||||
if !(status == 200 || status == 204) || err != nil {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
msg := fmt.Sprintf("%d: %v", status, err)
|
msg := fmt.Sprintf("%d: %v", status, err)
|
||||||
@ -664,6 +672,7 @@ func (app *appContext) DeleteUsers(gc *gin.Context) {
|
|||||||
UserID: userID,
|
UserID: userID,
|
||||||
SourceType: ActivityAdmin,
|
SourceType: ActivityAdmin,
|
||||||
Source: gc.GetString("jfId"),
|
Source: gc.GetString("jfId"),
|
||||||
|
Value: username,
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1151,8 +1160,12 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
|
|||||||
emailStore.Addr = address
|
emailStore.Addr = address
|
||||||
app.storage.SetEmailsKey(id, emailStore)
|
app.storage.SetEmailsKey(id, emailStore)
|
||||||
|
|
||||||
|
activityType := ActivityContactLinked
|
||||||
|
if address == "" {
|
||||||
|
activityType = ActivityContactUnlinked
|
||||||
|
}
|
||||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||||
Type: ActivityContactLinked,
|
Type: activityType,
|
||||||
UserID: id,
|
UserID: id,
|
||||||
SourceType: ActivityAdmin,
|
SourceType: ActivityAdmin,
|
||||||
Source: gc.GetString("jfId"),
|
Source: gc.GetString("jfId"),
|
||||||
|
@ -725,48 +725,20 @@
|
|||||||
<div class="flex-expand align-middle">
|
<div class="flex-expand align-middle">
|
||||||
<span class="text-3xl font-bold mr-4">{{ .strings.activity }}</span>
|
<span class="text-3xl font-bold mr-4">{{ .strings.activity }}</span>
|
||||||
<div id="activity-filter-dropdown" class="dropdown z-10" tabindex="0">
|
<div id="activity-filter-dropdown" class="dropdown z-10" tabindex="0">
|
||||||
<span class="h-100 button ~neutral @low center" id="accounts-filter-button">{{ .strings.filters }}</span>
|
<span class="h-100 button ~neutral @low center" id="activity-filter-button">{{ .strings.filters }}</span>
|
||||||
<div class="dropdown-display">
|
<div class="dropdown-display">
|
||||||
<div class="card ~neutral @low mt-2" id="accounts-filter-list">
|
<div class="card ~neutral @low mt-2" id="activity-filter-list">
|
||||||
<p class="supra pb-2">{{ .strings.filters }}</p>
|
<p class="supra pb-2">{{ .strings.filters }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input type="search" class="field ~neutral @low input search ml-2 mr-2" id="accounts-search" placeholder="{{ .strings.search }}">
|
<input type="search" class="field ~neutral @low input search ml-2 mr-2" id="activity-search" placeholder="{{ .strings.search }}">
|
||||||
<span class="button ~neutral @low center ml-[-2.64rem] rounded-s-none accounts-search-clear" aria-label="{{ .strings.clearSearch }}" text="{{ .strings.clearSearch }}"><i class="ri-close-line"></i></span>
|
<span class="button ~neutral @low center ml-[-2.64rem] rounded-s-none activity-search-clear" aria-label="{{ .strings.clearSearch }}" text="{{ .strings.clearSearch }}"><i class="ri-close-line"></i></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="supra py-1 sm hidden" id="accounts-search-options-header">{{ .strings.searchOptions }}</div>
|
<div class="supra py-1 sm hidden" id="activity-search-options-header">{{ .strings.searchOptions }}</div>
|
||||||
<div class="row -mx-2 mb-2">
|
<div class="row -mx-2 mb-2">
|
||||||
<button type="button" class="button ~neutral @low center mx-2 hidden"><span id="accounts-sort-by-field"></span> <i class="ri-close-line ml-2 text-2xl"></i></button>
|
<button type="button" class="button ~neutral @low center mx-2 hidden"><span id="activity-sort-by-field"></span> <i class="ri-close-line ml-2 text-2xl"></i></button>
|
||||||
<span id="accounts-filter-area"></span>
|
<span id="activity-filter-area"></span>
|
||||||
</div>
|
|
||||||
<div class="supra py-1 sm">{{ .strings.actions }}</div>
|
|
||||||
<div class="row -mx-2">
|
|
||||||
<span class="col button ~neutral @low center max-w-[20%]" id="accounts-add-user">{{ .quantityStrings.addUser.Singular }}</span>
|
|
||||||
<div id="accounts-announce-dropdown" class="col dropdown pb-0i max-w-[20%]" tabindex="0">
|
|
||||||
<span class="w-100 button ~info @low center" id="accounts-announce">{{ .strings.announce }}</span>
|
|
||||||
<div class="dropdown-display">
|
|
||||||
<div class="card ~neutral @low">
|
|
||||||
<span class="supra sm">{{ .strings.templates }}</span>
|
|
||||||
<div id="accounts-announce-templates"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="col button ~urge @low center max-w-[20%]" id="accounts-modify-user">{{ .strings.modifySettings }}</span>
|
|
||||||
{{ if .referralsEnabled }}
|
|
||||||
<span class="col button ~urge @low center max-w-[20%]" id="accounts-enable-referrals">{{ .strings.enableReferrals }}</span>
|
|
||||||
{{ end }}
|
|
||||||
<span class="col button ~warning @low center max-w-[20%]" id="accounts-extend-expiry">{{ .strings.extendExpiry }}</span>
|
|
||||||
<div id="accounts-disable-enable-dropdown" class="col dropdown manual pb-0i max-w-[20%]" tabindex="0">
|
|
||||||
<span class="w-100 button ~positive @low center" id="accounts-disable-enable">{{ .strings.disable }}</span>
|
|
||||||
<div class="dropdown-display">
|
|
||||||
<div class="card ~neutral @low">
|
|
||||||
<span class="button ~urge full-width accounts-announce-template-button" id="accounts-enable-expiry">{{ .strings.setExpiry }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="col button ~info @low center unfocused max-w-[20%]" id="accounts-send-pwr">{{ .strings.sendPWR }}</span>
|
|
||||||
<span class="col button ~critical @low center max-w-[20%]" id="accounts-delete-user">{{ .quantityStrings.deleteUser.Singular }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col md:flex-row gap-3">
|
<div class="flex flex-col md:flex-row gap-3">
|
||||||
<div class="card @low dark:~d_neutral col max-w-[20%]">
|
<div class="card @low dark:~d_neutral col max-w-[20%]">
|
||||||
@ -778,19 +750,14 @@
|
|||||||
<span class="button ~neutral @low settings-section-button justify-between mb-2" id="setting-about"><span class="flex">{{ .strings.aboutProgram }} <i class="ri-information-line ml-2"></i></span></span>
|
<span class="button ~neutral @low settings-section-button justify-between mb-2" id="setting-about"><span class="flex">{{ .strings.aboutProgram }} <i class="ri-information-line ml-2"></i></span></span>
|
||||||
<span class="button ~neutral @low settings-section-button justify-between mb-2" id="setting-profiles"><span class="flex">{{ .strings.userProfiles }} <i class="ri-user-line ml-2"></i></span></span>
|
<span class="button ~neutral @low settings-section-button justify-between mb-2" id="setting-profiles"><span class="flex">{{ .strings.userProfiles }} <i class="ri-user-line ml-2"></i></span></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card ~neutral @low col overflow" id="activity-card-list">
|
<div class="card ~neutral @low col">
|
||||||
<div class="card ~urge @low">
|
<div id="activity-card-list"></div>
|
||||||
<div class="flex justify-between mb-2">
|
<div class="unfocused h-[100%] my-3" id="activity-not-found">
|
||||||
<span class="heading text-2xl activity-title">Account Created: <a href="/fixme" class="activity-url">"hrfee"</a></span>
|
<div class="flex flex-col h-[100%] justify-center items-center">
|
||||||
<span class="text-sm font-medium activity-time">26/10/23 14:32</span>
|
<span class="text-2xl font-medium italic mb-3">{{ .strings.noResultsFound }}</span>
|
||||||
</div>
|
<button class="button ~neutral @low activity-search-clear">
|
||||||
<div class="flex justify-between">
|
<span class="mr-2">{{ .strings.clearSearch }}</span><i class="ri-close-line"></i>
|
||||||
<div>
|
</button>
|
||||||
<span class="content supra mr-2 activity-source-type">From Invite</span><a href="fixme" class="activity-source">BdBmpGDzuJhHSsboAsYgrE</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="content supra mr-2">Referrer</span><a href="fixme" class="activity-referrer">username</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -134,8 +134,8 @@
|
|||||||
"builtBy": "Built By",
|
"builtBy": "Built By",
|
||||||
"loginNotAdmin": "Not an Admin?",
|
"loginNotAdmin": "Not an Admin?",
|
||||||
"referrer": "Referrer",
|
"referrer": "Referrer",
|
||||||
"accountLinked": "{user}: {contactMethod} linked",
|
"accountLinked": "{contactMethod} linked: {user}",
|
||||||
"accountUnlinked": "{user}: {contactMethod} removed",
|
"accountUnlinked": "{contactMethod} removed: {user}",
|
||||||
"accountResetPassword": "{user} reset their password",
|
"accountResetPassword": "{user} reset their password",
|
||||||
"accountChangedPassword": "{user} changed their password",
|
"accountChangedPassword": "{user} changed their password",
|
||||||
"accountCreated": "Account created: {user}",
|
"accountCreated": "Account created: {user}",
|
||||||
@ -146,7 +146,27 @@
|
|||||||
"userDeleted": "User was deleted.",
|
"userDeleted": "User was deleted.",
|
||||||
"userDisabled": "User was disabled",
|
"userDisabled": "User was disabled",
|
||||||
"inviteCreated": "Invite created: {invite}",
|
"inviteCreated": "Invite created: {invite}",
|
||||||
"inviteDeleted": "Invite deleted: {invite}"
|
"inviteDeleted": "Invite deleted: {invite}",
|
||||||
|
"inviteExpired": "Invite expired: {invite}",
|
||||||
|
"fromInvite": "From Invite",
|
||||||
|
"byAdmin": "By Admin",
|
||||||
|
"byUser": "By User",
|
||||||
|
"byJfaGo": "By jfa-go",
|
||||||
|
"activityID": "Activity ID",
|
||||||
|
"title": "Title",
|
||||||
|
"usersMentioned": "User mentioned",
|
||||||
|
"actor": "Actor",
|
||||||
|
"actorDescription": "The thing that caused this action. <hr class=\"sep\"> \"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": {
|
"notifications": {
|
||||||
"changedEmailAddress": "Changed email address of {n}.",
|
"changedEmailAddress": "Changed email address of {n}.",
|
||||||
@ -162,6 +182,7 @@
|
|||||||
"telegramVerified": "Telegram account verified.",
|
"telegramVerified": "Telegram account verified.",
|
||||||
"accountConnected": "Account connected.",
|
"accountConnected": "Account connected.",
|
||||||
"referralsEnabled": "Referrals enabled.",
|
"referralsEnabled": "Referrals enabled.",
|
||||||
|
"activityDeleted": "Activity Deleted.",
|
||||||
"errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.",
|
"errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.",
|
||||||
"errorHomescreenAppliedNoSettings": "Homescreen layout was applied, but applying settings may have failed.",
|
"errorHomescreenAppliedNoSettings": "Homescreen layout was applied, but applying settings may have failed.",
|
||||||
"errorSettingsFailed": "Application failed.",
|
"errorSettingsFailed": "Application failed.",
|
||||||
|
18
models.go
18
models.go
@ -432,14 +432,16 @@ type EnableDisableReferralDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ActivityDTO struct {
|
type ActivityDTO struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
SourceType string `json:"source_type"`
|
Username string `json:"username"`
|
||||||
Source string `json:"source"`
|
SourceType string `json:"source_type"`
|
||||||
InviteCode string `json:"invite_code"`
|
Source string `json:"source"`
|
||||||
Value string `json:"value"`
|
SourceUsername string `json:"source_username"`
|
||||||
Time int64 `json:"time"`
|
InviteCode string `json:"invite_code"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Time int64 `json:"time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetActivitiesDTO struct {
|
type GetActivitiesDTO struct {
|
||||||
|
@ -232,7 +232,8 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
|||||||
api.DELETE(p+"/profiles/referral/:profile", app.DisableReferralForProfile)
|
api.DELETE(p+"/profiles/referral/:profile", app.DisableReferralForProfile)
|
||||||
}
|
}
|
||||||
|
|
||||||
api.GET(p+"/activity", app.GetActivities)
|
api.POST(p+"/activity", app.GetActivities)
|
||||||
|
api.DELETE(p+"/activity/:id", app.DeleteActivity)
|
||||||
|
|
||||||
if userPageEnabled {
|
if userPageEnabled {
|
||||||
user.GET("/details", app.MyDetails)
|
user.GET("/details", app.MyDetails)
|
||||||
|
@ -52,8 +52,8 @@ type Activity struct {
|
|||||||
UserID string // ID of target user. For account creation, this will be the newly created account
|
UserID string // ID of target user. For account creation, this will be the newly created account
|
||||||
SourceType ActivitySource
|
SourceType ActivitySource
|
||||||
Source string
|
Source string
|
||||||
InviteCode string // Only set for ActivityCreation
|
InviteCode string // Set for ActivityCreation, create/deleteInvite
|
||||||
Value string // Used for ActivityContactLinked, "email/discord/telegram/matrix", and Create/DeleteInvite, where it's the label.
|
Value string // Used for ActivityContactLinked where it's "email/discord/telegram/matrix", Create/DeleteInvite, where it's the label, and Creation/Deletion, where it's the Username.
|
||||||
Time time.Time
|
Time time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import { templateEmail } from "../modules/settings.js";
|
|||||||
import { Marked } from "@ts-stack/markdown";
|
import { Marked } from "@ts-stack/markdown";
|
||||||
import { stripMarkdown } from "../modules/stripmd.js";
|
import { stripMarkdown } from "../modules/stripmd.js";
|
||||||
import { DiscordUser, newDiscordSearch } from "../modules/discord.js";
|
import { DiscordUser, newDiscordSearch } from "../modules/discord.js";
|
||||||
|
import { Search, SearchConfiguration, QueryType, SearchableItem } from "../modules/search.js";
|
||||||
const dateParser = require("any-date-parser");
|
const dateParser = require("any-date-parser");
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
@ -39,7 +40,7 @@ interface announcementTemplate {
|
|||||||
|
|
||||||
var addDiscord: (passData: string) => void;
|
var addDiscord: (passData: string) => void;
|
||||||
|
|
||||||
class user implements User {
|
class user implements User, SearchableItem {
|
||||||
private _id = "";
|
private _id = "";
|
||||||
private _row: HTMLTableRowElement;
|
private _row: HTMLTableRowElement;
|
||||||
private _check: HTMLInputElement;
|
private _check: HTMLInputElement;
|
||||||
@ -269,7 +270,7 @@ class user implements User {
|
|||||||
<span class="chip btn @low"><i class="ri-link" alt="${window.lang.strings("add")}"></i></span>
|
<span class="chip btn @low"><i class="ri-link" alt="${window.lang.strings("add")}"></i></span>
|
||||||
<input type="text" class="input ~neutral @low stealth-input unfocused" placeholder="@user:riot.im">
|
<input type="text" class="input ~neutral @low stealth-input unfocused" placeholder="@user:riot.im">
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
(this._matrix.querySelector("span") as HTMLSpanElement).onclick = this._addMatrix;
|
(this._matrix.querySelector("span") as HTMLSpanElement).onclick = this._addMatrix;
|
||||||
} else {
|
} else {
|
||||||
this._notifyDropdown.querySelector(".accounts-area-matrix").classList.remove("unfocused");
|
this._notifyDropdown.querySelector(".accounts-area-matrix").classList.remove("unfocused");
|
||||||
@ -780,13 +781,13 @@ export class accountsList {
|
|||||||
private _userSelect = document.getElementById("modify-user-users") as HTMLSelectElement;
|
private _userSelect = document.getElementById("modify-user-users") as HTMLSelectElement;
|
||||||
private _referralsProfileSelect = document.getElementById("enable-referrals-user-profiles") as HTMLSelectElement;
|
private _referralsProfileSelect = document.getElementById("enable-referrals-user-profiles") as HTMLSelectElement;
|
||||||
private _referralsInviteSelect = document.getElementById("enable-referrals-user-invites") as HTMLSelectElement;
|
private _referralsInviteSelect = document.getElementById("enable-referrals-user-invites") as HTMLSelectElement;
|
||||||
private _search = document.getElementById("accounts-search") as HTMLInputElement;
|
private _searchBox = document.getElementById("accounts-search") as HTMLInputElement;
|
||||||
|
private _search: Search;
|
||||||
|
|
||||||
private _selectAll = document.getElementById("accounts-select-all") as HTMLInputElement;
|
private _selectAll = document.getElementById("accounts-select-all") as HTMLInputElement;
|
||||||
private _users: { [id: string]: user };
|
private _users: { [id: string]: user };
|
||||||
private _ordering: string[] = [];
|
private _ordering: string[] = [];
|
||||||
private _checkCount: number = 0;
|
private _checkCount: number = 0;
|
||||||
private _inSearch = false;
|
|
||||||
// Whether the enable/disable button should enable or not.
|
// Whether the enable/disable button should enable or not.
|
||||||
private _shouldEnable = false;
|
private _shouldEnable = false;
|
||||||
|
|
||||||
@ -836,7 +837,7 @@ export class accountsList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _queries: { [field: string]: { name: string, getter: string, bool: boolean, string: boolean, date: boolean, dependsOnTableHeader?: string, show?: boolean }} = {
|
private _queries: { [field: string]: QueryType } = {
|
||||||
"id": {
|
"id": {
|
||||||
// We don't use a translation here to circumvent the name substitution feature.
|
// We don't use a translation here to circumvent the name substitution feature.
|
||||||
name: "Jellyfin/Emby ID",
|
name: "Jellyfin/Emby ID",
|
||||||
@ -887,7 +888,7 @@ export class accountsList {
|
|||||||
bool: true,
|
bool: true,
|
||||||
string: false,
|
string: false,
|
||||||
date: false,
|
date: false,
|
||||||
dependsOnTableHeader: "accounts-header-access-jfa"
|
dependsOnElement: ".accounts-header-access-jfa"
|
||||||
},
|
},
|
||||||
"email": {
|
"email": {
|
||||||
name: window.lang.strings("emailAddress"),
|
name: window.lang.strings("emailAddress"),
|
||||||
@ -895,7 +896,7 @@ export class accountsList {
|
|||||||
bool: true,
|
bool: true,
|
||||||
string: true,
|
string: true,
|
||||||
date: false,
|
date: false,
|
||||||
dependsOnTableHeader: "accounts-header-email"
|
dependsOnElement: ".accounts-header-email"
|
||||||
},
|
},
|
||||||
"telegram": {
|
"telegram": {
|
||||||
name: "Telegram",
|
name: "Telegram",
|
||||||
@ -903,7 +904,7 @@ export class accountsList {
|
|||||||
bool: true,
|
bool: true,
|
||||||
string: true,
|
string: true,
|
||||||
date: false,
|
date: false,
|
||||||
dependsOnTableHeader: "accounts-header-telegram"
|
dependsOnElement: ".accounts-header-telegram"
|
||||||
},
|
},
|
||||||
"matrix": {
|
"matrix": {
|
||||||
name: "Matrix",
|
name: "Matrix",
|
||||||
@ -911,7 +912,7 @@ export class accountsList {
|
|||||||
bool: true,
|
bool: true,
|
||||||
string: true,
|
string: true,
|
||||||
date: false,
|
date: false,
|
||||||
dependsOnTableHeader: "accounts-header-matrix"
|
dependsOnElement: ".accounts-header-matrix"
|
||||||
},
|
},
|
||||||
"discord": {
|
"discord": {
|
||||||
name: "Discord",
|
name: "Discord",
|
||||||
@ -919,7 +920,7 @@ export class accountsList {
|
|||||||
bool: true,
|
bool: true,
|
||||||
string: true,
|
string: true,
|
||||||
date: false,
|
date: false,
|
||||||
dependsOnTableHeader: "accounts-header-discord"
|
dependsOnElement: ".accounts-header-discord"
|
||||||
},
|
},
|
||||||
"expiry": {
|
"expiry": {
|
||||||
name: window.lang.strings("expiry"),
|
name: window.lang.strings("expiry"),
|
||||||
@ -927,7 +928,7 @@ export class accountsList {
|
|||||||
bool: true,
|
bool: true,
|
||||||
string: false,
|
string: false,
|
||||||
date: true,
|
date: true,
|
||||||
dependsOnTableHeader: "accounts-header-expiry"
|
dependsOnElement: ".accounts-header-expiry"
|
||||||
},
|
},
|
||||||
"last-active": {
|
"last-active": {
|
||||||
name: window.lang.strings("lastActiveTime"),
|
name: window.lang.strings("lastActiveTime"),
|
||||||
@ -942,229 +943,12 @@ export class accountsList {
|
|||||||
bool: true,
|
bool: true,
|
||||||
string: false,
|
string: false,
|
||||||
date: false,
|
date: false,
|
||||||
dependsOnTableHeader: "accounts-header-referrals"
|
dependsOnElement: ".accounts-header-referrals"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _notFoundPanel: HTMLElement = document.getElementById("accounts-not-found");
|
private _notFoundPanel: HTMLElement = document.getElementById("accounts-not-found");
|
||||||
|
|
||||||
search = (query: String): string[] => {
|
|
||||||
console.log(this._queries);
|
|
||||||
this._filterArea.textContent = "";
|
|
||||||
|
|
||||||
query = query.toLowerCase();
|
|
||||||
let result: string[] = [...this._ordering];
|
|
||||||
// console.log("initial:", result);
|
|
||||||
|
|
||||||
// const words = query.split(" ");
|
|
||||||
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._users[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._queries)) continue;
|
|
||||||
|
|
||||||
const queryFormat = this._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._search.value = this._search.value.replace(split[0] + ":" + quote + split[1] + quote, "");
|
|
||||||
}
|
|
||||||
this._search.oninput((null as Event));
|
|
||||||
})
|
|
||||||
|
|
||||||
this._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._users[id];
|
|
||||||
const value = Object.getOwnPropertyDescriptor(user.prototype, 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._search.value = this._search.value.replace(regex, "");
|
|
||||||
}
|
|
||||||
this._search.oninput((null as Event));
|
|
||||||
})
|
|
||||||
|
|
||||||
this._filterArea.appendChild(filterCard);
|
|
||||||
|
|
||||||
let cachedResult = [...result];
|
|
||||||
for (let id of cachedResult) {
|
|
||||||
const u = this._users[id];
|
|
||||||
const value = Object.getOwnPropertyDescriptor(user.prototype, queryFormat.getter).get.call(u);
|
|
||||||
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._search.value = this._search.value.replace(regex, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
this._search.oninput((null as Event));
|
|
||||||
})
|
|
||||||
|
|
||||||
this._filterArea.appendChild(filterCard);
|
|
||||||
|
|
||||||
let cachedResult = [...result];
|
|
||||||
for (let id of cachedResult) {
|
|
||||||
const u = this._users[id];
|
|
||||||
const unixValue = Object.getOwnPropertyDescriptor(user.prototype, 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;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
get selectAll(): boolean { return this._selectAll.checked; }
|
get selectAll(): boolean { return this._selectAll.checked; }
|
||||||
set selectAll(state: boolean) {
|
set selectAll(state: boolean) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
@ -2014,34 +1798,23 @@ export class accountsList {
|
|||||||
this._deleteNotify.checked = false;
|
this._deleteNotify.checked = false;
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
const onchange = () => {
|
let conf: SearchConfiguration = {
|
||||||
const query = this._search.value;
|
filterArea: this._filterArea,
|
||||||
if (!query) {
|
sortingByButton: this._sortingByButton,
|
||||||
// this.setVisibility(this._ordering, true);
|
searchOptionsHeader: this._searchOptionsHeader,
|
||||||
this._inSearch = false;
|
notFoundPanel: this._notFoundPanel,
|
||||||
} else {
|
filterList: document.getElementById("accounts-filter-list"),
|
||||||
this._inSearch = true;
|
search: this._searchBox,
|
||||||
// this.setVisibility(this.search(query), true);
|
queries: this._queries,
|
||||||
}
|
setVisibility: this.setVisibility,
|
||||||
const results = this.search(query);
|
clearSearchButtonSelector: ".accounts-search-clear",
|
||||||
this.setVisibility(results, true);
|
onSearchCallback: () => {
|
||||||
this._checkCheckCount();
|
this._checkCheckCount();
|
||||||
this.showHideSearchOptionsHeader();
|
|
||||||
if (results.length == 0) {
|
|
||||||
this._notFoundPanel.classList.remove("unfocused");
|
|
||||||
} else {
|
|
||||||
this._notFoundPanel.classList.add("unfocused");
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this._search.oninput = onchange;
|
this._search = new Search(conf);
|
||||||
|
this._search.items = this._users;
|
||||||
|
|
||||||
const clearSearchButtons = Array.from(document.getElementsByClassName("accounts-search-clear")) as Array<HTMLSpanElement>;
|
|
||||||
for (let b of clearSearchButtons) {
|
|
||||||
b.addEventListener("click", () => {
|
|
||||||
this._search.value = "";
|
|
||||||
onchange();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this._announceTextarea.onkeyup = this.loadPreview;
|
this._announceTextarea.onkeyup = this.loadPreview;
|
||||||
addDiscord = newDiscordSearch(window.lang.strings("linkDiscord"), window.lang.strings("searchDiscordUser"), window.lang.strings("add"), (user: DiscordUser, id: string) => {
|
addDiscord = newDiscordSearch(window.lang.strings("linkDiscord"), window.lang.strings("searchDiscordUser"), window.lang.strings("add"), (user: DiscordUser, id: string) => {
|
||||||
@ -2088,15 +1861,16 @@ export class accountsList {
|
|||||||
|
|
||||||
document.addEventListener("header-click", (event: CustomEvent) => {
|
document.addEventListener("header-click", (event: CustomEvent) => {
|
||||||
this._ordering = this._columns[event.detail].sort(this._users);
|
this._ordering = this._columns[event.detail].sort(this._users);
|
||||||
|
this._search.ordering = this._ordering;
|
||||||
this._activeSortColumn = event.detail;
|
this._activeSortColumn = event.detail;
|
||||||
this._sortingByButton.innerHTML = this._columns[event.detail].buttonContent;
|
this._sortingByButton.innerHTML = this._columns[event.detail].buttonContent;
|
||||||
this._sortingByButton.parentElement.classList.remove("hidden");
|
this._sortingByButton.parentElement.classList.remove("hidden");
|
||||||
// console.log("ordering by", event.detail, ": ", this._ordering);
|
// console.log("ordering by", event.detail, ": ", this._ordering);
|
||||||
if (!(this._inSearch)) {
|
if (!(this._search.inSearch)) {
|
||||||
this.setVisibility(this._ordering, true);
|
this.setVisibility(this._ordering, true);
|
||||||
this._notFoundPanel.classList.add("unfocused");
|
this._notFoundPanel.classList.add("unfocused");
|
||||||
} else {
|
} else {
|
||||||
const results = this.search(this._search.value);
|
const results = this._search.search(this._searchBox.value);
|
||||||
this.setVisibility(results, true);
|
this.setVisibility(results, true);
|
||||||
if (results.length == 0) {
|
if (results.length == 0) {
|
||||||
this._notFoundPanel.classList.remove("unfocused");
|
this._notFoundPanel.classList.remove("unfocused");
|
||||||
@ -2110,84 +1884,7 @@ export class accountsList {
|
|||||||
defaultSort();
|
defaultSort();
|
||||||
this.showHideSearchOptionsHeader();
|
this.showHideSearchOptionsHeader();
|
||||||
|
|
||||||
const filterList = document.getElementById("accounts-filter-list");
|
this._search.generateFilterList();
|
||||||
|
|
||||||
const fillInFilter = (name: string, value: string, offset?: number) => {
|
|
||||||
this._search.value = name + ":" + value + " " + this._search.value;
|
|
||||||
this._search.focus();
|
|
||||||
let newPos = name.length + 1 + value.length;
|
|
||||||
if (typeof offset !== 'undefined')
|
|
||||||
newPos += offset;
|
|
||||||
this._search.setSelectionRange(newPos, newPos);
|
|
||||||
this._search.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 ("dependsOnTableHeader" in query && query.dependsOnTableHeader) {
|
|
||||||
const el = document.querySelector("."+query.dependsOnTableHeader);
|
|
||||||
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 = `<span class="mr-2">${query.name}</span>`;
|
|
||||||
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", () => 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", () => 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", () => 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", () => 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", () => 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", () => fillInFilter(queryName, `">"`, -1));
|
|
||||||
|
|
||||||
container.appendChild(onDate);
|
|
||||||
container.appendChild(beforeDate);
|
|
||||||
container.appendChild(afterDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
filterList.appendChild(container);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reload = () => {
|
reload = () => {
|
||||||
@ -2210,11 +1907,12 @@ export class accountsList {
|
|||||||
}
|
}
|
||||||
// console.log("reload, so sorting by", this._activeSortColumn);
|
// console.log("reload, so sorting by", this._activeSortColumn);
|
||||||
this._ordering = this._columns[this._activeSortColumn].sort(this._users);
|
this._ordering = this._columns[this._activeSortColumn].sort(this._users);
|
||||||
if (!(this._inSearch)) {
|
this._search.ordering = this._ordering;
|
||||||
|
if (!(this._search.inSearch)) {
|
||||||
this.setVisibility(this._ordering, true);
|
this.setVisibility(this._ordering, true);
|
||||||
this._notFoundPanel.classList.add("unfocused");
|
this._notFoundPanel.classList.add("unfocused");
|
||||||
} else {
|
} else {
|
||||||
const results = this.search(this._search.value);
|
const results = this._search.search(this._searchBox.value);
|
||||||
if (results.length == 0) {
|
if (results.length == 0) {
|
||||||
this._notFoundPanel.classList.remove("unfocused");
|
this._notFoundPanel.classList.remove("unfocused");
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { _get, toDateString } from "../modules/common.js";
|
import { _post, _delete, toDateString } from "../modules/common.js";
|
||||||
|
import { Search, SearchConfiguration, QueryType, SearchableItem } from "../modules/search.js";
|
||||||
|
|
||||||
export interface activity {
|
export interface activity {
|
||||||
id: string;
|
id: string;
|
||||||
@ -9,6 +10,8 @@ export interface activity {
|
|||||||
invite_code: string;
|
invite_code: string;
|
||||||
value: string;
|
value: string;
|
||||||
time: number;
|
time: number;
|
||||||
|
username: string;
|
||||||
|
source_username: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
var activityTypeMoods = {
|
var activityTypeMoods = {
|
||||||
@ -24,9 +27,11 @@ var activityTypeMoods = {
|
|||||||
"deleteInvite": -1
|
"deleteInvite": -1
|
||||||
};
|
};
|
||||||
|
|
||||||
var moodColours = ["~warning", "~neutral", "~urge"];
|
// var moodColours = ["~warning", "~neutral", "~urge"];
|
||||||
|
|
||||||
export class Activity { // FIXME: Add "implements"
|
export var activityReload = new CustomEvent("activity-reload");
|
||||||
|
|
||||||
|
export class Activity implements activity, SearchableItem { // FIXME: Add "implements"
|
||||||
private _card: HTMLElement;
|
private _card: HTMLElement;
|
||||||
private _title: HTMLElement;
|
private _title: HTMLElement;
|
||||||
private _time: HTMLElement;
|
private _time: HTMLElement;
|
||||||
@ -35,27 +40,90 @@ export class Activity { // FIXME: Add "implements"
|
|||||||
private _source: HTMLElement;
|
private _source: HTMLElement;
|
||||||
private _referrer: HTMLElement;
|
private _referrer: HTMLElement;
|
||||||
private _expiryTypeBadge: HTMLElement;
|
private _expiryTypeBadge: HTMLElement;
|
||||||
|
private _delete: HTMLElement;
|
||||||
private _act: activity;
|
private _act: activity;
|
||||||
|
|
||||||
|
_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 `<a class="hover:underline" href="/accounts/user/${this._act.user_id}">${this._genUserText()}</a>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
_genSrcUserLink = (): string => {
|
||||||
|
return `<a class="hover:underline" href="/accounts/user/${this._act.source}">${this._genSrcUserText()}</a>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderInvText = (): string => { return `<span class="font-medium font-mono">${this.value || this.invite_code || "???"}</span>`; }
|
||||||
|
|
||||||
|
private _genInvLink = (): string => {
|
||||||
|
return `<a class="hover:underline" href="/accounts/invites/${this.invite_code}">${this._renderInvText()}</a>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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; }
|
get type(): string { return this._act.type; }
|
||||||
set type(v: string) {
|
set type(v: string) {
|
||||||
this._act.type = v;
|
this._act.type = v;
|
||||||
|
|
||||||
let mood = activityTypeMoods[v]; // 1 = positive, 0 = neutral, -1 = negative
|
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");
|
||||||
|
|
||||||
for (let i = 0; i < moodColours.length; i++) {
|
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]);
|
if (i-1 == mood) this._card.classList.add(moodColours[i]);
|
||||||
else this._card.classList.remove(moodColours[i]);
|
else this._card.classList.remove(moodColours[i]);
|
||||||
}
|
} */
|
||||||
|
|
||||||
if (this.type == "changePassword" || this.type == "resetPassword") {
|
if (this.type == "changePassword" || this.type == "resetPassword") {
|
||||||
let innerHTML = ``;
|
let innerHTML = ``;
|
||||||
if (this.type == "changePassword") innerHTML = window.lang.strings("accountChangedPassword");
|
if (this.type == "changePassword") innerHTML = window.lang.strings("accountChangedPassword");
|
||||||
else innerHTML = window.lang.strings("accountResetPassword");
|
else innerHTML = window.lang.strings("accountResetPassword");
|
||||||
innerHTML = innerHTML.replace("{user}", `<a href="/accounts/user/${this._act.user_id}">FIXME</a>`);
|
innerHTML = innerHTML.replace("{user}", this._genUserLink());
|
||||||
this._title.innerHTML = innerHTML;
|
this._title.innerHTML = innerHTML;
|
||||||
} else if (this.type == "contactLinked" || this.type == "contactUnlinked") {
|
} else if (this.type == "contactLinked" || this.type == "contactUnlinked") {
|
||||||
let platform = this._act.type;
|
let platform = this.value;
|
||||||
if (platform == "email") {
|
if (platform == "email") {
|
||||||
platform = window.lang.strings("emailAddress");
|
platform = window.lang.strings("emailAddress");
|
||||||
} else {
|
} else {
|
||||||
@ -64,40 +132,46 @@ export class Activity { // FIXME: Add "implements"
|
|||||||
let innerHTML = ``;
|
let innerHTML = ``;
|
||||||
if (this.type == "contactLinked") innerHTML = window.lang.strings("accountLinked");
|
if (this.type == "contactLinked") innerHTML = window.lang.strings("accountLinked");
|
||||||
else innerHTML = window.lang.strings("accountUnlinked");
|
else innerHTML = window.lang.strings("accountUnlinked");
|
||||||
innerHTML = innerHTML.replace("{user}", `<a href="/accounts/user/${this._act.user_id}">FIXME</a>`).replace("{contactMethod}", platform);
|
innerHTML = innerHTML.replace("{user}", this._genUserLink()).replace("{contactMethod}", platform);
|
||||||
this._title.innerHTML = innerHTML;
|
this._title.innerHTML = innerHTML;
|
||||||
} else if (this.type == "creation") {
|
} else if (this.type == "creation") {
|
||||||
this._title.innerHTML = window.lang.strings("accountCreated").replace("{user}", `<a href="/accounts/user/${this._act.user_id}">FIXME</a>`);
|
this._title.innerHTML = window.lang.strings("accountCreated").replace("{user}", this._genUserLink());
|
||||||
if (this.source_type == "user") {
|
if (this.source_type == "user") {
|
||||||
this._referrer.innerHTML = `<span class="supra mr-2">${window.lang.strings("referrer")}</span><a href="/accounts/${this._source}">FIXME</a>`;
|
this._referrer.innerHTML = `<span class="supra mr-2">${window.lang.strings("referrer")}</span>${this._genSrcUserLink()}`;
|
||||||
} else {
|
} else {
|
||||||
this._referrer.textContent = ``;
|
this._referrer.textContent = ``;
|
||||||
}
|
}
|
||||||
} else if (this.type == "deletion") {
|
} else if (this.type == "deletion") {
|
||||||
if (this.source_type == "daemon") {
|
if (this.source_type == "daemon") {
|
||||||
this._title.innerHTML = window.lang.strings("accountExpired").replace("{user}", `<a href="/accounts/user/${this._act.user_id}">FIXME</a>`);
|
this._title.innerHTML = window.lang.strings("accountExpired").replace("{user}", this._genUserText());
|
||||||
this._expiryTypeBadge.classList.add("~critical");
|
this._expiryTypeBadge.classList.add("~critical");
|
||||||
this._expiryTypeBadge.classList.remove("~warning");
|
this._expiryTypeBadge.classList.remove("~info");
|
||||||
this._expiryTypeBadge.textContent = window.lang.strings("deleted");
|
this._expiryTypeBadge.textContent = window.lang.strings("deleted");
|
||||||
} else {
|
} else {
|
||||||
this._title.innerHTML = window.lang.strings("accountDeleted").replace("{user}", `<a href="/accounts/user/${this._act.user_id}">FIXME</a>`);
|
this._title.innerHTML = window.lang.strings("accountDeleted").replace("{user}", this._genUserText());
|
||||||
}
|
}
|
||||||
} else if (this.type == "enabled") {
|
} else if (this.type == "enabled") {
|
||||||
this._title.innerHTML = window.lang.strings("accountReEnabled").replace("{user}", `<a href="/accounts/user/${this._act.user_id}">FIXME</a>`);
|
this._title.innerHTML = window.lang.strings("accountReEnabled").replace("{user}", this._genUserLink());
|
||||||
} else if (this.type == "disabled") {
|
} else if (this.type == "disabled") {
|
||||||
if (this.source_type == "daemon") {
|
if (this.source_type == "daemon") {
|
||||||
this._title.innerHTML = window.lang.strings("accountExpired").replace("{user}", `<a href="/accounts/user/${this._act.user_id}">FIXME</a>`);
|
this._title.innerHTML = window.lang.strings("accountExpired").replace("{user}", this._genUserLink());
|
||||||
this._expiryTypeBadge.classList.add("~warning");
|
this._expiryTypeBadge.classList.add("~info");
|
||||||
this._expiryTypeBadge.classList.remove("~critical");
|
this._expiryTypeBadge.classList.remove("~critical");
|
||||||
this._expiryTypeBadge.textContent = window.lang.strings("disabled");
|
this._expiryTypeBadge.textContent = window.lang.strings("disabled");
|
||||||
} else {
|
} else {
|
||||||
this._title.innerHTML = window.lang.strings("accountDisabled").replace("{user}", `<a href="/accounts/user/${this._act.user_id}">FIXME</a>`);
|
this._title.innerHTML = window.lang.strings("accountDisabled").replace("{user}", this._genUserLink());
|
||||||
}
|
}
|
||||||
} else if (this.type == "createInvite") {
|
} else if (this.type == "createInvite") {
|
||||||
this._title.innerHTML = window.lang.strings("inviteCreated").replace("{invite}", `<a href="/accounts/user/${this.invite_code}">${this.value || this.invite_code}</a>`);
|
this._title.innerHTML = window.lang.strings("inviteCreated").replace("{invite}", this._genInvLink());
|
||||||
} else if (this.type == "deleteInvite") {
|
} 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 = window.lang.strings("inviteDeleted").replace("{invite}", this.value || this.invite_code);
|
this._title.innerHTML = innerHTML.replace("{invite}", this._renderInvText());
|
||||||
}
|
}
|
||||||
|
|
||||||
/*} else if (this.source_type == "admin") {
|
/*} else if (this.source_type == "admin") {
|
||||||
@ -118,6 +192,15 @@ export class Activity { // FIXME: Add "implements"
|
|||||||
get source_type(): string { return this._act.source_type; }
|
get source_type(): string { return this._act.source_type; }
|
||||||
set source_type(v: string) {
|
set source_type(v: string) {
|
||||||
this._act.source_type = v;
|
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; }
|
get invite_code(): string { return this._act.invite_code; }
|
||||||
@ -130,22 +213,61 @@ export class Activity { // FIXME: Add "implements"
|
|||||||
this._act.value = v;
|
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) {
|
constructor(act: activity) {
|
||||||
this._card = document.createElement("div");
|
this._card = document.createElement("div");
|
||||||
|
|
||||||
this._card.classList.add("card", "@low");
|
this._card.classList.add("card", "@low", "my-2");
|
||||||
this._card.innerHTML = `
|
this._card.innerHTML = `
|
||||||
<div class="flex justify-between mb-2">
|
<div class="flex flex-col md:flex-row justify-between mb-2">
|
||||||
<span class="heading text-2xl activity-title"></span><span class="activity-expiry-type badge"></span>
|
<span class="heading truncate flex-initial md:text-2xl text-xl activity-title"></span>
|
||||||
<span class="text-sm font-medium activity-time" aria-label="${window.lang.strings("date")}"></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>
|
||||||
<div class="flex justify-between">
|
<div class="flex flex-col md:flex-row justify-between">
|
||||||
<div>
|
<div>
|
||||||
<span class="content supra mr-2 activity-source-type"></span><span class="activity-source"></span>
|
<span class="content supra mr-2 activity-source-type"></span><span class="activity-source"></span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="content activity-referrer"></span>
|
<span class="content activity-referrer"></span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -155,6 +277,13 @@ export class Activity { // FIXME: Add "implements"
|
|||||||
this._source = this._card.querySelector(".activity-source");
|
this._source = this._card.querySelector(".activity-source");
|
||||||
this._referrer = this._card.querySelector(".activity-referrer");
|
this._referrer = this._card.querySelector(".activity-referrer");
|
||||||
this._expiryTypeBadge = this._card.querySelector(".activity-expiry-type");
|
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);
|
this.update(act);
|
||||||
}
|
}
|
||||||
@ -164,10 +293,20 @@ export class Activity { // FIXME: Add "implements"
|
|||||||
this._act = act;
|
this._act = act;
|
||||||
this.source_type = act.source_type;
|
this.source_type = act.source_type;
|
||||||
this.invite_code = act.invite_code;
|
this.invite_code = act.invite_code;
|
||||||
|
this.time = act.time;
|
||||||
|
this.source = act.source;
|
||||||
this.value = act.value;
|
this.value = act.value;
|
||||||
this.type = act.type;
|
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; };
|
asElement = () => { return this._card; };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,9 +316,35 @@ interface ActivitiesDTO {
|
|||||||
|
|
||||||
export class activityList {
|
export class activityList {
|
||||||
private _activityList: HTMLElement;
|
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 = () => {
|
reload = () => {
|
||||||
_get("/activity", null, (req: XMLHttpRequest) => {
|
let send = {
|
||||||
|
"type": [],
|
||||||
|
"limit": 60,
|
||||||
|
"page": 0,
|
||||||
|
"ascending": false
|
||||||
|
}
|
||||||
|
_post("/activity", send, (req: XMLHttpRequest) => {
|
||||||
if (req.readyState != 4) return;
|
if (req.readyState != 4) return;
|
||||||
if (req.status != 200) {
|
if (req.status != 200) {
|
||||||
window.notifications.customError("loadActivitiesError", window.lang.notif("errorLoadActivities"));
|
window.notifications.customError("loadActivitiesError", window.lang.notif("errorLoadActivities"));
|
||||||
@ -187,16 +352,165 @@ export class activityList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let resp = req.response as ActivitiesDTO;
|
let resp = req.response as ActivitiesDTO;
|
||||||
this._activityList.textContent = ``;
|
// FIXME: Don't destroy everything each reload!
|
||||||
|
this._activities = {};
|
||||||
for (let act of resp.activities) {
|
for (let act of resp.activities) {
|
||||||
const activity = new Activity(act);
|
this._activities[act.id] = new Activity(act);
|
||||||
this._activityList.appendChild(activity.asElement());
|
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() {
|
constructor() {
|
||||||
this._activityList = document.getElementById("activity-card-list");
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
385
ts/modules/search.ts
Normal file
385
ts/modules/search.ts
Normal file
@ -0,0 +1,385 @@
|
|||||||
|
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;
|
||||||
|
filterList: HTMLElement;
|
||||||
|
clearSearchButtonSelector: string;
|
||||||
|
search: HTMLInputElement;
|
||||||
|
queries: { [field: string]: QueryType };
|
||||||
|
setVisibility: (items: string[], visible: boolean) => void;
|
||||||
|
onSearchCallback: () => 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 = () => {
|
||||||
|
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();
|
||||||
|
this.showHideSearchOptionsHeader();
|
||||||
|
if (results.length == 0) {
|
||||||
|
this._c.notFoundPanel.classList.remove("unfocused");
|
||||||
|
} else {
|
||||||
|
this._c.notFoundPanel.classList.add("unfocused");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -61,10 +61,10 @@ func (app *appContext) checkUsers() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
mode := "disable"
|
mode := "disable"
|
||||||
termPlural := "Disabling"
|
term := "Disabling"
|
||||||
if app.config.Section("user_expiry").Key("behaviour").MustString("disable_user") == "delete_user" {
|
if app.config.Section("user_expiry").Key("behaviour").MustString("disable_user") == "delete_user" {
|
||||||
mode = "delete"
|
mode = "delete"
|
||||||
termPlural = "Deleting"
|
term = "Deleting"
|
||||||
}
|
}
|
||||||
contact := false
|
contact := false
|
||||||
if messagesEnabled && app.config.Section("user_expiry").Key("send_email").MustBool(true) {
|
if messagesEnabled && app.config.Section("user_expiry").Key("send_email").MustBool(true) {
|
||||||
@ -95,7 +95,7 @@ func (app *appContext) checkUsers() {
|
|||||||
app.storage.DeleteUserExpiryKey(expiry.JellyfinID)
|
app.storage.DeleteUserExpiryKey(expiry.JellyfinID)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
app.info.Printf("%s expired user \"%s\"", termPlural, user.Name)
|
app.info.Printf("%s expired user \"%s\"", term, user.Name)
|
||||||
|
|
||||||
// Record activity
|
// Record activity
|
||||||
activity := Activity{
|
activity := Activity{
|
||||||
@ -107,6 +107,7 @@ func (app *appContext) checkUsers() {
|
|||||||
if mode == "delete" {
|
if mode == "delete" {
|
||||||
status, err = app.jf.DeleteUser(id)
|
status, err = app.jf.DeleteUser(id)
|
||||||
activity.Type = ActivityDeletion
|
activity.Type = ActivityDeletion
|
||||||
|
activity.Value = user.Name
|
||||||
} else if mode == "disable" {
|
} else if mode == "disable" {
|
||||||
user.Policy.IsDisabled = true
|
user.Policy.IsDisabled = true
|
||||||
// Admins can't be disabled
|
// Admins can't be disabled
|
||||||
|
Loading…
Reference in New Issue
Block a user