feat: admin functionality modal
This commit is contained in:
parent
6b554cf90b
commit
1c1049fbf4
@ -294,11 +294,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-button {
|
.sidebar-button {
|
||||||
@apply flex h-8 w-8 items-center justify-center rounded-lg border border-[var(--light-accent)] bg-transparent p-0 text-[var(--light-text)] hover:bg-[var(--light-background)]/80;
|
@apply flex h-8 w-8 items-center justify-center rounded-lg border border-[var(--light-accent)] bg-transparent p-0 text-[var(--light-text)] enabled:hover:bg-[var(--light-background)]/80 disabled:cursor-not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .sidebar-button {
|
.dark .sidebar-button {
|
||||||
@apply border-[var(--dark-accent)]/50 text-[var(--dark-text)] hover:bg-[var(--dark-background)]/80;
|
@apply border-[var(--dark-accent)]/50 text-[var(--dark-text)] enabled:hover:bg-[var(--dark-background)]/80;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
@ -383,11 +383,11 @@
|
|||||||
@apply text-[var(--dark-text)]/60;
|
@apply text-[var(--dark-text)]/60;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-list {
|
.sidebar-list-item {
|
||||||
@apply flex cursor-pointer flex-col rounded-lg border border-[var(--light-text)]/10 bg-transparent px-3 py-2 text-sm text-[var(--light-text)] hover:bg-[var(--light-background)]/50;
|
@apply flex cursor-pointer flex-col rounded-lg border border-[var(--light-text)]/10 bg-transparent px-3 py-2 text-sm text-[var(--light-text)] hover:bg-[var(--light-background)]/50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .sidebar-list {
|
.dark .sidebar-list-item {
|
||||||
@apply border-[var(--dark-text)]/10 bg-transparent text-[var(--dark-text)] hover:bg-[var(--dark-background)]/80;
|
@apply border-[var(--dark-text)]/10 bg-transparent text-[var(--dark-text)] hover:bg-[var(--dark-background)]/80;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -408,11 +408,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-list-item-delete-button {
|
.sidebar-list-item-delete-button {
|
||||||
@apply ml-1 h-5.5 w-5.5 flex-shrink-0 border-[var(--light-accent)]/40 hover:bg-[var(--light-accent)]/20;
|
@apply ml-1 h-5.5 w-5.5 flex-shrink-0 border-[var(--light-accent)]/40 enabled:hover:bg-[var(--light-accent)]/20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .sidebar-list-item-delete-button {
|
.dark .sidebar-list-item-delete-button {
|
||||||
@apply border-[var(--dark-accent)]/40 hover:bg-[var(--dark-accent)]/20;
|
@apply border-[var(--dark-accent)]/40 enabled:hover:bg-[var(--dark-accent)]/20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-list-item-metadata {
|
.sidebar-list-item-metadata {
|
||||||
@ -943,14 +943,30 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-table-row-item {
|
.modal-table-row-item {
|
||||||
@apply overflow-x-auto px-4 py-3 text-wrap;
|
@apply overflow-x-auto px-4 py-3 font-light text-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-table-head {
|
.modal-table-head {
|
||||||
@apply text-left font-semibold text-[var(--light-text)]/80;
|
@apply text-left font-medium text-[var(--light-text)]/80;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .modal-table-head {
|
.dark .modal-table-head {
|
||||||
@apply text-[var(--dark-text)]/80;
|
@apply text-[var(--dark-text)]/80;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-list-item {
|
||||||
|
@apply flex w-[90%] flex-col rounded-lg border border-[var(--light-text)]/10 bg-transparent px-3 py-2 text-sm text-[var(--light-text)] hover:bg-[var(--light-foreground)]/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .modal-list-item {
|
||||||
|
@apply border-[var(--dark-text)]/10 bg-transparent text-[var(--dark-text)] hover:bg-[var(--dark-foreground)]/80;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-list-item-metadata {
|
||||||
|
@apply mt-1 truncate text-xs text-[var(--light-text)]/40;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .modal-list-item-metadata {
|
||||||
|
@apply text-[var(--dark-text)]/40;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
142
web/src/lib/components/AdminModal.svelte
Normal file
142
web/src/lib/components/AdminModal.svelte
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { currentUser } from "$lib/logic/client"
|
||||||
|
import Close from "$lib/icons/Close.svelte"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import type { User } from "$lib/logic/model"
|
||||||
|
import Delete from "$lib/icons/sidebar/Delete.svelte"
|
||||||
|
import { formatDateShort } from "$lib/util/contentVisual"
|
||||||
|
|
||||||
|
// props
|
||||||
|
export let onClose: () => void
|
||||||
|
export let adminDeleteUser: (userID: string) => Promise<void>
|
||||||
|
export let users: User[] | null = null
|
||||||
|
|
||||||
|
let modalContent: HTMLDivElement | null = null
|
||||||
|
let previouslyFocusedElement: HTMLElement | null = null
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
// store the element that had focus before opening the modal
|
||||||
|
previouslyFocusedElement = document.activeElement as HTMLElement
|
||||||
|
|
||||||
|
// focus the first focusable element in the modal
|
||||||
|
if (modalContent !== null) {
|
||||||
|
const focusableElements = (modalContent as HTMLDivElement).querySelectorAll(
|
||||||
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||||
|
)
|
||||||
|
if (focusableElements.length > 0) {
|
||||||
|
;(focusableElements[0] as HTMLElement).focus()
|
||||||
|
} else {
|
||||||
|
;(modalContent as HTMLDivElement).focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// when component unmounts, restore focus
|
||||||
|
return () => {
|
||||||
|
if (previouslyFocusedElement) {
|
||||||
|
previouslyFocusedElement.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleDeleteUser = (event: MouseEvent, userID: string, username: string) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
if (confirm(`Are you sure you want to delete user '${username}'`)) {
|
||||||
|
adminDeleteUser(userID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
// close the modal if user clicks outside of it
|
||||||
|
const target = event.target as HTMLElement
|
||||||
|
if (target.classList.contains("modal-backdrop")) {
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleModalContentKeydown = (_event: KeyboardEvent) => {
|
||||||
|
// accessibility compliance handler, actual handling is done by the global keydown handler
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeydown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:keydown={handleKeydown} />
|
||||||
|
|
||||||
|
<!-- TODO: user result pagination (if needed) -->
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="modal-backdrop"
|
||||||
|
on:click={handleClickOutside}
|
||||||
|
on:keydown={handleKeydown}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||||
|
<div
|
||||||
|
bind:this={modalContent}
|
||||||
|
class="modal-content max-w-xl"
|
||||||
|
on:click|stopPropagation
|
||||||
|
on:keydown={handleModalContentKeydown}
|
||||||
|
role="document"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<!-- Header (title + close button) -->
|
||||||
|
<div class="modal-section flex items-center justify-between" role="heading" aria-level="1">
|
||||||
|
<h2 class="text-xl font-bold">Administration</h2>
|
||||||
|
<button on:click={onClose} class="sidebar-button" aria-label="Close settings">
|
||||||
|
<Close />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Account information -->
|
||||||
|
<div class="modal-section border-b-0">
|
||||||
|
<h3 class="modal-section-title">Active users</h3>
|
||||||
|
<div class="flex flex-col justify-center">
|
||||||
|
{#if users !== null && users.length > 0}
|
||||||
|
<ul class="flex flex-col items-center space-y-1" role="listbox">
|
||||||
|
{#each users as user}
|
||||||
|
<li class="modal-list-item">
|
||||||
|
<div class="flex w-full items-center justify-between">
|
||||||
|
<h3 class="modal-list-item-title">{user.username}</h3>
|
||||||
|
|
||||||
|
<!-- Delete button -->
|
||||||
|
{#if $currentUser !== null && user.id === $currentUser.id}
|
||||||
|
<button
|
||||||
|
disabled
|
||||||
|
class="sidebar-button sidebar-list-item-delete-button"
|
||||||
|
aria-label="Delete note"
|
||||||
|
>
|
||||||
|
<Delete classString="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
on:click={(e) => handleDeleteUser(e, user.id, user.username)}
|
||||||
|
class="sidebar-button sidebar-list-item-delete-button"
|
||||||
|
aria-label="Delete note"
|
||||||
|
>
|
||||||
|
<Delete classString="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="modal-list-item-metadata">{user.id}</p>
|
||||||
|
|
||||||
|
<p class="modal-list-item-metadata">
|
||||||
|
Created: {formatDateShort(user.createdAt)}, Updated: {formatDateShort(
|
||||||
|
user.updatedAt
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -191,7 +191,7 @@
|
|||||||
</h1>
|
</h1>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- TODO: handle versions pagination support later if it becomes an issue (`util/itemPagination.ts`) -->
|
<!-- TODO: note version metadata pagination (if needed) -->
|
||||||
|
|
||||||
<!-- Note action buttons -->
|
<!-- Note action buttons -->
|
||||||
<div class="note-action-container">
|
<div class="note-action-container">
|
||||||
|
@ -11,11 +11,13 @@
|
|||||||
availableNotes,
|
availableNotes,
|
||||||
versionHistory,
|
versionHistory,
|
||||||
cError,
|
cError,
|
||||||
cSuccess
|
cSuccess,
|
||||||
|
availableUsers
|
||||||
} from "$lib/logic/client"
|
} from "$lib/logic/client"
|
||||||
import Close from "$lib/icons/Close.svelte"
|
import Close from "$lib/icons/Close.svelte"
|
||||||
import { generateGreeting } from "$lib/util/greetMessage"
|
import { generateGreeting } from "$lib/util/greetMessage"
|
||||||
import { hashContent } from "$lib/util/contentValidation"
|
import { hashContent } from "$lib/util/contentValidation"
|
||||||
|
import AdminModal from "./AdminModal.svelte"
|
||||||
|
|
||||||
// error/success notification display durations
|
// error/success notification display durations
|
||||||
const ERR_NOTIFICATION_DUR = 8 * 1000 // 8 s.
|
const ERR_NOTIFICATION_DUR = 8 * 1000 // 8 s.
|
||||||
@ -23,7 +25,8 @@
|
|||||||
|
|
||||||
// local state
|
// local state
|
||||||
let isComponentReady = false
|
let isComponentReady = false
|
||||||
let showSettings = false
|
let isSettingsVisible = false
|
||||||
|
let isAdminViewVisible = false
|
||||||
let isSidebarOpen = window.innerWidth > 768
|
let isSidebarOpen = window.innerWidth > 768
|
||||||
let isSidebarUserMenuOpen = false
|
let isSidebarUserMenuOpen = false
|
||||||
let isVersionDropdownOpen = false
|
let isVersionDropdownOpen = false
|
||||||
@ -46,7 +49,9 @@
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await adminLoadUsers() // will do nothing if the user isn't an admin
|
||||||
await loadNotes()
|
await loadNotes()
|
||||||
|
|
||||||
username = $currentUser.username
|
username = $currentUser.username
|
||||||
greetMessage = generateGreeting(
|
greetMessage = generateGreeting(
|
||||||
// capitalization for the message
|
// capitalization for the message
|
||||||
@ -104,6 +109,16 @@
|
|||||||
keysPressed.clear()
|
keysPressed.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminLoadUsers = async () => {
|
||||||
|
if ($currentUser !== null && $currentUser.isAdmin) {
|
||||||
|
const users = await apiClient.adminListAll()
|
||||||
|
|
||||||
|
if (users) {
|
||||||
|
availableUsers.set(users)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const loadNotes = async () => {
|
const loadNotes = async () => {
|
||||||
const notes = await apiClient.listNotes()
|
const notes = await apiClient.listNotes()
|
||||||
|
|
||||||
@ -114,12 +129,12 @@
|
|||||||
|
|
||||||
const toggleSettingsModal = () => {
|
const toggleSettingsModal = () => {
|
||||||
isSidebarUserMenuOpen = false
|
isSidebarUserMenuOpen = false
|
||||||
showSettings = !showSettings
|
isSettingsVisible = !isSettingsVisible
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleAdminModal = () => {
|
const toggleAdminModal = () => {
|
||||||
isSidebarUserMenuOpen = false
|
isSidebarUserMenuOpen = false
|
||||||
console.log("[DBG] Admin view is not implemented yet")
|
isAdminViewVisible = !isAdminViewVisible
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleWebhookModal = () => {
|
const toggleWebhookModal = () => {
|
||||||
@ -127,11 +142,6 @@
|
|||||||
console.log("[DBG] Webhooks aren't implemented yet")
|
console.log("[DBG] Webhooks aren't implemented yet")
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleTagModal = () => {
|
|
||||||
isSidebarUserMenuOpen = false
|
|
||||||
console.log("[DBG] Tags aren't implemented yet")
|
|
||||||
}
|
|
||||||
|
|
||||||
const createNewNote = async () => {
|
const createNewNote = async () => {
|
||||||
const newNote = await apiClient.createNote()
|
const newNote = await apiClient.createNote()
|
||||||
|
|
||||||
@ -221,6 +231,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminDeleteUser = async (userID: string) => {
|
||||||
|
await apiClient.adminDeleteUser(userID)
|
||||||
|
|
||||||
|
// refresh the list due to updates being pushed to server
|
||||||
|
await adminLoadUsers()
|
||||||
|
|
||||||
|
$cSuccess = "User deleted successfully."
|
||||||
|
}
|
||||||
|
|
||||||
const deleteNote = async (noteID: string) => {
|
const deleteNote = async (noteID: string) => {
|
||||||
await apiClient.deleteNote(noteID)
|
await apiClient.deleteNote(noteID)
|
||||||
|
|
||||||
@ -321,7 +340,6 @@
|
|||||||
{toggleSettingsModal}
|
{toggleSettingsModal}
|
||||||
{toggleAdminModal}
|
{toggleAdminModal}
|
||||||
{toggleWebhookModal}
|
{toggleWebhookModal}
|
||||||
{toggleTagModal}
|
|
||||||
{logout}
|
{logout}
|
||||||
{createNewNote}
|
{createNewNote}
|
||||||
{selectNote}
|
{selectNote}
|
||||||
@ -358,8 +376,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Settings Modal -->
|
<!-- Settings Modal -->
|
||||||
{#if showSettings}
|
{#if isSettingsVisible}
|
||||||
<SettingsModal onClose={toggleSettingsModal} />
|
<SettingsModal onClose={toggleSettingsModal} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Admin view -->
|
||||||
|
{#if isAdminViewVisible}
|
||||||
|
<AdminModal users={$availableUsers} {adminDeleteUser} onClose={toggleAdminModal} />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -8,8 +8,6 @@
|
|||||||
// props
|
// props
|
||||||
export let onClose: () => void
|
export let onClose: () => void
|
||||||
|
|
||||||
const userData = $currentUser
|
|
||||||
|
|
||||||
let currentPassword = ""
|
let currentPassword = ""
|
||||||
let newPassword = ""
|
let newPassword = ""
|
||||||
let confirmPassword = ""
|
let confirmPassword = ""
|
||||||
@ -113,7 +111,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleModalContentKeydown = (event: KeyboardEvent) => {
|
const handleModalContentKeydown = (_event: KeyboardEvent) => {
|
||||||
// accessibility compliance handler, actual handling is done by the global keydown handler
|
// accessibility compliance handler, actual handling is done by the global keydown handler
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,7 +135,7 @@
|
|||||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||||
<div
|
<div
|
||||||
bind:this={modalContent}
|
bind:this={modalContent}
|
||||||
class="modal-content max-w-md"
|
class="modal-content max-w-xl"
|
||||||
on:click|stopPropagation
|
on:click|stopPropagation
|
||||||
on:keydown={handleModalContentKeydown}
|
on:keydown={handleModalContentKeydown}
|
||||||
role="document"
|
role="document"
|
||||||
@ -158,24 +156,28 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr class="modal-table-row">
|
<tr class="modal-table-row">
|
||||||
<th class="modal-table-row-item modal-table-head">Username</th>
|
<th class="modal-table-row-item modal-table-head">Username</th>
|
||||||
<td class="modal-table-row-item">{userData?.username}</td>
|
<td class="modal-table-row-item">{$currentUser?.username}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="modal-table-row">
|
<tr class="modal-table-row">
|
||||||
<th class="modal-table-row-item modal-table-head">Account created</th>
|
<th class="modal-table-row-item modal-table-head">Account created</th>
|
||||||
<td class="modal-table-row-item"
|
<td class="modal-table-row-item"
|
||||||
>{userData?.createdAt !== undefined ? formatDateLong(userData?.createdAt) : ""}</td
|
>{$currentUser?.createdAt !== undefined
|
||||||
|
? formatDateLong($currentUser?.createdAt)
|
||||||
|
: ""}</td
|
||||||
>
|
>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="modal-table-row">
|
<tr class="modal-table-row">
|
||||||
<th class="modal-table-row-item modal-table-head">Password updated</th>
|
<th class="modal-table-row-item modal-table-head">Password updated</th>
|
||||||
<td class="modal-table-row-item"
|
<td class="modal-table-row-item"
|
||||||
>{userData?.createdAt !== undefined ? formatDateLong(userData?.updatedAt) : ""}</td
|
>{$currentUser?.createdAt !== undefined
|
||||||
|
? formatDateLong($currentUser?.updatedAt)
|
||||||
|
: ""}</td
|
||||||
>
|
>
|
||||||
</tr>
|
</tr>
|
||||||
{#if userData?.isAdmin}
|
{#if $currentUser?.isAdmin}
|
||||||
<tr class="modal-table-row">
|
<tr class="modal-table-row">
|
||||||
<th class="modal-table-row-item modal-table-head">Admin permissions</th>
|
<th class="modal-table-row-item modal-table-head">Admin permissions</th>
|
||||||
<td class="modal-table-row-item">{userData?.isAdmin}</td>
|
<td class="modal-table-row-item">{$currentUser?.isAdmin}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/if}
|
{/if}
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -233,7 +235,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- "Danger zone" -->
|
<!-- "Danger zone" -->
|
||||||
<div class="modal-section">
|
<div class="modal-section border-b-0">
|
||||||
<h3 class="modal-section-title text-red-500">Danger Zone</h3>
|
<h3 class="modal-section-title text-red-500">Danger Zone</h3>
|
||||||
|
|
||||||
{#if deleteUserModalError}
|
{#if deleteUserModalError}
|
||||||
|
@ -6,7 +6,6 @@
|
|||||||
import User from "$lib/icons/sidebar/User.svelte"
|
import User from "$lib/icons/sidebar/User.svelte"
|
||||||
import ChevronDown from "$lib/icons/ChevronDown.svelte"
|
import ChevronDown from "$lib/icons/ChevronDown.svelte"
|
||||||
import ChevronUp from "$lib/icons/ChevronUp.svelte"
|
import ChevronUp from "$lib/icons/ChevronUp.svelte"
|
||||||
import Tag from "$lib/icons/sidebar/Tag.svelte"
|
|
||||||
import WebhookTruck from "$lib/icons/sidebar/WebhookTruck.svelte"
|
import WebhookTruck from "$lib/icons/sidebar/WebhookTruck.svelte"
|
||||||
import Search from "$lib/icons/sidebar/Search.svelte"
|
import Search from "$lib/icons/sidebar/Search.svelte"
|
||||||
import SettingsGear from "$lib/icons/sidebar/SettingsGear.svelte"
|
import SettingsGear from "$lib/icons/sidebar/SettingsGear.svelte"
|
||||||
@ -25,7 +24,6 @@
|
|||||||
export let toggleSettingsModal: () => void
|
export let toggleSettingsModal: () => void
|
||||||
export let toggleAdminModal: () => void
|
export let toggleAdminModal: () => void
|
||||||
export let toggleWebhookModal: () => void
|
export let toggleWebhookModal: () => void
|
||||||
export let toggleTagModal: () => void
|
|
||||||
export let logout: () => Promise<void>
|
export let logout: () => Promise<void>
|
||||||
export let createNewNote: () => Promise<void>
|
export let createNewNote: () => Promise<void>
|
||||||
export let selectNote: (
|
export let selectNote: (
|
||||||
@ -66,7 +64,7 @@
|
|||||||
: notes
|
: notes
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- TODO: handle pagination support later if it becomes an issue (`util/itemPagination.ts`) -->
|
<!-- TODO: note metadata pagination (if needed) -->
|
||||||
|
|
||||||
<!-- Outmost sidebar container with collapsible functionality -->
|
<!-- Outmost sidebar container with collapsible functionality -->
|
||||||
<div
|
<div
|
||||||
@ -98,14 +96,6 @@
|
|||||||
<button on:click={toggleWebhookModal} class="sidebar-button" aria-label="View webhooks">
|
<button on:click={toggleWebhookModal} class="sidebar-button" aria-label="View webhooks">
|
||||||
<WebhookTruck classString="h-5 w-5" />
|
<WebhookTruck classString="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
|
||||||
on:click={toggleTagModal}
|
|
||||||
class="sidebar-button"
|
|
||||||
aria-label="View tags & expiration"
|
|
||||||
>
|
|
||||||
<Tag classString="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@ -129,7 +119,14 @@
|
|||||||
<!-- Header with app logo and display name -->
|
<!-- Header with app logo and display name -->
|
||||||
<div class="sidebar-header-container">
|
<div class="sidebar-header-container">
|
||||||
<!-- svelte-ignore a11y_invalid_attribute -->
|
<!-- svelte-ignore a11y_invalid_attribute -->
|
||||||
<a href="#" class="sidebar-header-link" on:click={() => selectNote("", false, true)}>
|
<a
|
||||||
|
href="#"
|
||||||
|
class="sidebar-header-link"
|
||||||
|
on:click={() => {
|
||||||
|
selectNote("", false, true)
|
||||||
|
toggleSidebar()
|
||||||
|
}}
|
||||||
|
>
|
||||||
<img src="favicon.svg" alt="Logo" class="sidebar-header-logo" />
|
<img src="favicon.svg" alt="Logo" class="sidebar-header-logo" />
|
||||||
<span class="sidebar-header-text">QueNote</span>
|
<span class="sidebar-header-text">QueNote</span>
|
||||||
</a>
|
</a>
|
||||||
@ -148,11 +145,6 @@
|
|||||||
<WebhookTruck classString="mr-3 h-5 w-5" />
|
<WebhookTruck classString="mr-3 h-5 w-5" />
|
||||||
Webhooks
|
Webhooks
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button on:click={toggleTagModal} class="sidebar-action-button">
|
|
||||||
<Tag classString="mr-3 h-5 w-5" />
|
|
||||||
Note tags
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative flex flex-col px-2 pb-3">
|
<div class="relative flex flex-col px-2 pb-3">
|
||||||
@ -174,7 +166,7 @@
|
|||||||
<ul class="space-y-1" role="listbox">
|
<ul class="space-y-1" role="listbox">
|
||||||
{#each filteredNotes as note}
|
{#each filteredNotes as note}
|
||||||
<li
|
<li
|
||||||
class="sidebar-list"
|
class="sidebar-list-item"
|
||||||
class:sidebar-list-active={currentNote && note.id === currentNote.id}
|
class:sidebar-list-active={currentNote && note.id === currentNote.id}
|
||||||
on:click={() => selectNote(note.id, false, false)}
|
on:click={() => selectNote(note.id, false, false)}
|
||||||
on:keydown={(e) => handleNoteKeydown(e, note.id)}
|
on:keydown={(e) => handleNoteKeydown(e, note.id)}
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
export let classString = "h-6 w-6"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
class={classString}
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M7.0498 7.0498H7.0598M10.5118 3H7.8C6.11984 3 5.27976 3 4.63803 3.32698C4.07354 3.6146 3.6146 4.07354 3.32698 4.63803C3 5.27976 3 6.11984 3 7.8V10.5118C3 11.2455 3 11.6124 3.08289 11.9577C3.15638 12.2638 3.27759 12.5564 3.44208 12.8249C3.6276 13.1276 3.88703 13.387 4.40589 13.9059L9.10589 18.6059C10.2939 19.7939 10.888 20.388 11.5729 20.6105C12.1755 20.8063 12.8245 20.8063 13.4271 20.6105C14.112 20.388 14.7061 19.7939 15.8941 18.6059L18.6059 15.8941C19.7939 14.7061 20.388 14.112 20.6105 13.4271C20.8063 12.8245 20.8063 12.1755 20.6105 11.5729C20.388 10.888 19.7939 10.2939 18.6059 9.10589L13.9059 4.40589C13.387 3.88703 13.1276 3.6276 12.8249 3.44208C12.5564 3.27759 12.2638 3.15638 11.9577 3.08289C11.6124 3 11.2455 3 10.5118 3ZM7.5498 7.0498C7.5498 7.32595 7.32595 7.5498 7.0498 7.5498C6.77366 7.5498 6.5498 7.32595 6.5498 7.0498C6.5498 6.77366 6.77366 6.5498 7.0498 6.5498C7.32595 6.5498 7.5498 6.77366 7.5498 7.0498Z"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
@ -32,6 +32,7 @@ const COOKIE_SECURE = import.meta.env.PROD ? true : false
|
|||||||
export const currentUser: Writable<User | null> = writable(null)
|
export const currentUser: Writable<User | null> = writable(null)
|
||||||
export const currentFullNote: Writable<FullNote | null> = writable(null)
|
export const currentFullNote: Writable<FullNote | null> = writable(null)
|
||||||
export const availableNotes: Writable<NoteMetadata[] | null> = writable(null)
|
export const availableNotes: Writable<NoteMetadata[] | null> = writable(null)
|
||||||
|
export const availableUsers: Writable<User[] | null> = writable(null)
|
||||||
export const versionHistory: Writable<VersionMetadata[] | null> = writable(null)
|
export const versionHistory: Writable<VersionMetadata[] | null> = writable(null)
|
||||||
export const accessToken: Writable<string | null> = writable(null)
|
export const accessToken: Writable<string | null> = writable(null)
|
||||||
export const csrfToken: Writable<string | null> = writable(null)
|
export const csrfToken: Writable<string | null> = writable(null)
|
||||||
@ -441,7 +442,13 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const users = await this.handleResponse<User[]>(response, { useBearerAuth: true })
|
let users: User[] = []
|
||||||
|
const data = await this.handleResponse<ApiUserResponse[]>(response, { useBearerAuth: true })
|
||||||
|
|
||||||
|
if (users) {
|
||||||
|
users = User.fromApiResponseArray(data)
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`[ADMIN] Got ${users.length} user results`)
|
console.log(`[ADMIN] Got ${users.length} user results`)
|
||||||
|
|
||||||
return users
|
return users
|
||||||
@ -493,7 +500,7 @@ class ApiClient {
|
|||||||
})
|
})
|
||||||
|
|
||||||
let notes: NoteMetadata[] = []
|
let notes: NoteMetadata[] = []
|
||||||
let data = await this.handleResponse<ApiNoteMetadataResponse[]>(response, {
|
const data = await this.handleResponse<ApiNoteMetadataResponse[]>(response, {
|
||||||
useBearerAuth: true
|
useBearerAuth: true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -20,6 +20,10 @@ export class User {
|
|||||||
this.createdAt = new Date(apiResponse.created_at)
|
this.createdAt = new Date(apiResponse.created_at)
|
||||||
this.updatedAt = new Date(apiResponse.updated_at)
|
this.updatedAt = new Date(apiResponse.updated_at)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static fromApiResponseArray(apiResponses: ApiUserResponse[]): User[] {
|
||||||
|
return apiResponses.map((res) => new User(res))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiFullNoteResponse {
|
export interface ApiFullNoteResponse {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user