chore(front): refactor admin settings users

This commit is contained in:
ArneBo 2025-03-12 12:00:48 +01:00
parent 1779e66d4a
commit 606903f7e7
5 changed files with 284 additions and 268 deletions

View File

@ -5,6 +5,8 @@ import { ref, computed, reactive, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import DangerousButton from '~/components/common/DangerousButton.vue'
import Layout from '~/components/ui/Layout.vue'
import Button from '~/components/ui/Button.vue'
import axios from 'axios'
@ -171,72 +173,71 @@ const launchAction = async () => {
<span v-if="needsRefresh">
{{ t('components.common.ActionTable.message.needsRefresh') }}
</span>
<button
class="ui basic icon button"
<Button
tiny
icon="bi-arrow-clockwise"
:title="labels.refresh"
:aria-label="labels.refresh"
@click="$emit('refresh')"
>
<i class="refresh icon" />
</button>
/>
</div>
<div
<Layout
v-if="actionUrl && actions.length > 0"
class="ui small left floated form"
stack
no-gap
class="ui form"
>
<div class="ui inline fields">
<div class="field">
<label for="actions-select">{{ t('components.common.ActionTable.label.actions') }}</label>
<select
id="actions-select"
v-model="currentActionName"
class="ui dropdown"
<label for="actions-select">{{ t('components.common.ActionTable.label.actions') }}</label>
<Layout flex>
<select
id="actions-select"
v-model="currentActionName"
class="ui dropdown"
>
<option
v-for="action in actions"
:key="action.name"
:value="action.name"
>
<option
v-for="action in actions"
:key="action.name"
:value="action.name"
>
{{ action.label }}
</option>
</select>
</div>
<div class="field">
<dangerous-button
v-if="selectAll || currentAction?.isDangerous"
:disabled="checked.length === 0 || undefined"
:is-loading="isLoading"
:confirm-color="currentAction?.confirmColor ?? 'success'"
:aria-label="labels.performAction"
:title="t('components.common.ActionTable.modal.performAction.header', { action: currentActionName }, affectedObjectsCount)"
@confirm="launchAction"
>
{{ t('components.common.ActionTable.button.go') }}
<template #modal-content>
<template v-if="currentAction?.confirmationMessage">
{{ currentAction?.confirmationMessage }}
</template>
<span v-else>
{{ t('components.common.ActionTable.modal.performAction.content.warning') }}
</span>
{{ action.label }}
</option>
</select>
<dangerous-button
v-if="selectAll || currentAction?.isDangerous"
:disabled="checked.length === 0 || undefined"
:is-loading="isLoading"
:confirm-color="currentAction?.confirmColor ?? 'success'"
:aria-label="labels.performAction"
:title="t('components.common.ActionTable.modal.performAction.header', { action: currentActionName }, affectedObjectsCount)"
@confirm="launchAction"
>
{{ t('components.common.ActionTable.button.go') }}
<template #modal-content>
<template v-if="currentAction?.confirmationMessage">
{{ currentAction?.confirmationMessage }}
</template>
<template #modal-confirm>
<span :aria-label="labels.performAction">
{{ t('components.common.ActionTable.button.launch') }}
</span>
</template>
</dangerous-button>
<button
v-else
:disabled="checked.length === 0"
:aria-label="labels.performAction"
:class="['ui', {disabled: checked.length === 0}, {'loading': isLoading}, 'button']"
@click="launchAction"
>
{{ t('components.common.ActionTable.button.go') }}
</button>
</div>
<span v-else>
{{ t('components.common.ActionTable.modal.performAction.content.warning') }}
</span>
</template>
<template #modal-confirm>
<span :aria-label="labels.performAction">
{{ t('components.common.ActionTable.button.launch') }}
</span>
</template>
</dangerous-button>
<Button
v-else
primary
:disabled="checked.length === 0"
:aria-label="labels.performAction"
:class="[{disabled: checked.length === 0}, {'loading': isLoading}]"
style="margin-top: 7px;"
@click="launchAction"
>
{{ t('components.common.ActionTable.button.go') }}
</Button>
<div class="count field">
<span v-if="selectAll">
{{ t('components.common.ActionTable.button.allSelected', objectsData.count) }}
@ -265,11 +266,10 @@ const launchAction = async () => {
</a>
</template>
</div>
</div>
<div
</Layout>
<Alert
v-if="errors.length > 0"
role="alert"
class="ui negative message"
red
>
<h4 class="header">
{{ t('components.common.ActionTable.header.error') }}
@ -282,10 +282,10 @@ const launchAction = async () => {
{{ error }}
</li>
</ul>
</div>
<div
</Alert>
<Alert
v-if="result"
class="ui positive message"
green
>
<p>
<span>
@ -297,8 +297,8 @@ const launchAction = async () => {
name="action-success-footer"
:result="result"
/>
</div>
</div>
</Alert>
</Layout>
</th>
</tr>
<tr>

View File

@ -8,6 +8,12 @@ import { useStore } from '~/store'
import axios from 'axios'
import Layout from '~/components/ui/Layout.vue'
import Button from '~/components/ui/Button.vue'
import Alert from '~/components/ui/Alert.vue'
import Spacer from '~/components/ui/Spacer.vue'
import Input from '~/components/ui/Input.vue'
interface Invitation {
code: string
}
@ -47,14 +53,14 @@ const getUrl = (code: string) => store.getters['instance/absoluteUrl'](router.re
<template>
<div>
<form
<Layout
form
class="ui form"
@submit.prevent="submit"
>
<div
<Alert
v-if="errors.length > 0"
role="alert"
class="ui negative message"
red
>
<h4 class="header">
{{ t('components.manage.users.InvitationForm.header.failure') }}
@ -67,31 +73,31 @@ const getUrl = (code: string) => store.getters['instance/absoluteUrl'](router.re
{{ error }}
</li>
</ul>
</div>
</Alert>
<div class="inline fields">
<div class="ui field">
<label for="invitation-code">{{ t('components.manage.users.InvitationForm.label.invite') }}</label>
<input
v-model="code"
for="invitation-code"
name="code"
type="text"
:placeholder="labels.placeholder"
>
</div>
<div class="ui field">
<button
:class="['ui', {loading: isLoading}, 'button']"
:disabled="isLoading"
type="submit"
>
{{ t('components.manage.users.InvitationForm.button.new') }}
</button>
</div>
<Input
v-model="code"
for="invitation-code"
name="code"
type="text"
:label="t('components.manage.users.InvitationForm.label.invite')"
:placeholder="labels.placeholder"
>
<template #input-right>
<Button
primary
:class="[{loading: isLoading}]"
:disabled="isLoading"
type="submit"
>
{{ t('components.manage.users.InvitationForm.button.new') }}
</Button>
</template>
</Input>
</div>
</form>
</Layout>
<Spacer :size="16" />
<div v-if="invitations.length > 0">
<div class="ui hidden divider" />
<table class="ui ui basic table">
<thead>
<tr>
@ -118,12 +124,14 @@ const getUrl = (code: string) => store.getters['instance/absoluteUrl'](router.re
</tr>
</tbody>
</table>
<button
class="ui basic button"
<Spacer :size="8"/>
<Button
destructive
icon="bi-trash"
@click="invitations.length = 0"
>
{{ t('components.manage.users.InvitationForm.button.clear') }}
</button>
</Button>
</div>
</div>
</template>

View File

@ -11,7 +11,11 @@ import moment from 'moment'
import axios from 'axios'
import ActionTable from '~/components/common/ActionTable.vue'
import Pagination from '~/components/vui/Pagination.vue'
import Pagination from '~/components/ui/Pagination.vue'
import Layout from '~/components/ui/Layout.vue'
import Input from '~/components/ui/Input.vue'
import Spacer from '~/components/ui/Spacer.vue'
import Loader from '~/components/ui/Loader.vue'
import useSharedLabels from '~/composables/locale/useSharedLabels'
import useOrdering from '~/composables/navigation/useOrdering'
@ -100,141 +104,139 @@ const labels = computed(() => ({
</script>
<template>
<div>
<div class="ui inline form">
<div class="fields">
<div class="ui field">
<label for="invitations-search">{{ t('components.manage.users.InvitationsTable.label.search') }}</label>
<input
id="invitations-search"
v-model="query"
name="search"
type="text"
:placeholder="labels.searchPlaceholder"
>
</div>
<div class="field">
<label for="invitations-ordering">{{ t('components.manage.users.InvitationsTable.ordering.label') }}</label>
<select
id="invitations-ordering"
v-model="ordering"
class="ui dropdown"
>
<option
v-for="(option, key) in orderingOptions"
:key="key"
:value="option[0]"
>
{{ sharedLabels.filters[option[1]] }}
</option>
</select>
</div>
<div class="field">
<label for="invitations-status">{{ t('components.manage.users.InvitationsTable.label.status') }}</label>
<select
id="invitations-status"
v-model="isOpen"
class="ui dropdown"
>
<option :value="null">
{{ t('components.manage.users.InvitationsTable.option.all') }}
</option>
<option :value="true">
{{ t('components.manage.users.InvitationsTable.option.open') }}
</option>
<option :value="false">
{{ t('components.manage.users.InvitationsTable.option.expired') }}
</option>
</select>
</div>
</div>
</div>
<div class="dimmable">
<div
v-if="isLoading"
class="ui active inverted dimmer"
>
<div class="ui loader" />
</div>
<action-table
v-if="result"
:objects-data="result"
:actions="actions"
:action-url="'manage/users/invitations/action/'"
:filters="actionFilters"
@action-launched="fetchData"
>
<template #header-cells>
<th>
{{ t('components.manage.users.InvitationsTable.table.invitation.header.owner') }}
</th>
<th>
{{ t('components.manage.users.InvitationsTable.table.invitation.header.user') }}
</th>
<th>
{{ t('components.manage.users.InvitationsTable.table.invitation.header.status') }}
</th>
<th>
{{ t('components.manage.users.InvitationsTable.table.invitation.header.creationDate') }}
</th>
<th>
{{ t('components.manage.users.InvitationsTable.table.invitation.header.expirationDate') }}
</th>
<th>
{{ t('components.manage.users.InvitationsTable.table.invitation.header.code') }}
</th>
</template>
<template
#row-cells="scope"
>
<td>
<router-link :to="{name: 'manage.moderation.accounts.detail', params: {id: scope.obj.id }}">
{{ scope.obj.owner.username }}
</router-link>
</td>
<td>
<span v-if="scope.obj.invited_user">
{{ scope.obj.invited_user.username }}
</span>
</td>
<td>
<span
v-if="scope.obj.users.length > 0"
class="ui success basic label"
>{{ t('components.manage.users.InvitationsTable.label.used') }}</span>
<span
v-else-if="moment().isAfter(scope.obj.expiration_date)"
class="ui danger basic label"
>{{ t('components.manage.users.InvitationsTable.label.expired') }}</span>
<span
v-else
class="ui basic label"
>{{ t('components.manage.users.InvitationsTable.label.unused') }}</span>
</td>
<td>
<human-date :date="scope.obj.creation_date" />
</td>
<td>
<human-date :date="scope.obj.expiration_date" />
</td>
<td>
{{ scope.obj.code.toUpperCase() }}
</td>
</template>
</action-table>
</div>
<div>
<pagination
v-if="result && result.count > paginateBy"
v-model:current="page"
:compact="true"
:paginate-by="paginateBy"
:total="result.count"
<Layout
form
class="ui inline form"
>
<div class="ui field">
<label for="invitations-search">{{ t('components.manage.users.InvitationsTable.label.search') }}</label>
<Input
id="invitations-search"
v-model="query"
search
name="search"
type="text"
:placeholder="labels.searchPlaceholder"
/>
<span v-if="result && result.results.length > 0">
{{ t('components.manage.users.InvitationsTable.pagination.results', { start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count }, result.results.length) }}
</span>
</div>
<Layout flex>
<Spacer grow />
<div class="field">
<label for="invitations-ordering">{{ t('components.manage.users.InvitationsTable.ordering.label') }}</label>
<select
id="invitations-ordering"
v-model="ordering"
class="ui dropdown"
>
<option
v-for="(option, key) in orderingOptions"
:key="key"
:value="option[0]"
>
{{ sharedLabels.filters[option[1]] }}
</option>
</select>
</div>
<div class="field">
<label for="invitations-status">{{ t('components.manage.users.InvitationsTable.label.status') }}</label>
<select
id="invitations-status"
v-model="isOpen"
class="ui dropdown"
>
<option :value="null">
{{ t('components.manage.users.InvitationsTable.option.all') }}
</option>
<option :value="true">
{{ t('components.manage.users.InvitationsTable.option.open') }}
</option>
<option :value="false">
{{ t('components.manage.users.InvitationsTable.option.expired') }}
</option>
</select>
</div>
</Layout>
</Layout>
<div class="dimmable">
<Loader
v-if="isLoading"
/>
<action-table
v-if="result"
:objects-data="result"
:actions="actions"
:action-url="'manage/users/invitations/action/'"
:filters="actionFilters"
@action-launched="fetchData"
>
<template #header-cells>
<th>
{{ t('components.manage.users.InvitationsTable.table.invitation.header.owner') }}
</th>
<th>
{{ t('components.manage.users.InvitationsTable.table.invitation.header.user') }}
</th>
<th>
{{ t('components.manage.users.InvitationsTable.table.invitation.header.status') }}
</th>
<th>
{{ t('components.manage.users.InvitationsTable.table.invitation.header.creationDate') }}
</th>
<th>
{{ t('components.manage.users.InvitationsTable.table.invitation.header.expirationDate') }}
</th>
<th>
{{ t('components.manage.users.InvitationsTable.table.invitation.header.code') }}
</th>
</template>
<template
#row-cells="scope"
>
<td>
<router-link :to="{name: 'manage.moderation.accounts.detail', params: {id: scope.obj.id }}">
{{ scope.obj.owner.username }}
</router-link>
</td>
<td>
<span v-if="scope.obj.invited_user">
{{ scope.obj.invited_user.username }}
</span>
</td>
<td>
<span
v-if="scope.obj.users.length > 0"
class="ui success basic label"
>{{ t('components.manage.users.InvitationsTable.label.used') }}</span>
<span
v-else-if="moment().isAfter(scope.obj.expiration_date)"
class="ui danger basic label"
>{{ t('components.manage.users.InvitationsTable.label.expired') }}</span>
<span
v-else
class="ui basic label"
>{{ t('components.manage.users.InvitationsTable.label.unused') }}</span>
</td>
<td>
<human-date :date="scope.obj.creation_date" />
</td>
<td>
<human-date :date="scope.obj.expiration_date" />
</td>
<td>
{{ scope.obj.code.toUpperCase() }}
</td>
</template>
</action-table>
</div>
<div>
<Pagination
v-if="result && result.count > paginateBy"
v-model:current="page"
:paginate-by="paginateBy"
/>
<span v-if="result && result.results.length > 0">
{{ t('components.manage.users.InvitationsTable.pagination.results', { start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count }, result.results.length) }}
</span>
</div>
</template>

View File

@ -10,7 +10,11 @@ import { useI18n } from 'vue-i18n'
import axios from 'axios'
import ActionTable from '~/components/common/ActionTable.vue'
import Pagination from '~/components/vui/Pagination.vue'
import Pagination from '~/components/ui/Pagination.vue'
import Layout from '~/components/ui/Layout.vue'
import Input from '~/components/ui/Input.vue'
import Spacer from '~/components/ui/Spacer.vue'
import useSharedLabels from '~/composables/locale/useSharedLabels'
import useErrorHandler from '~/composables/useErrorHandler'
@ -102,19 +106,25 @@ const labels = computed(() => ({
</script>
<template>
<div>
<div class="ui inline form">
<div class="fields">
<div class="ui field">
<label for="users-search">{{ t('components.manage.users.UsersTable.label.search') }}</label>
<input
id="users-search"
v-model="query"
name="search"
type="text"
:placeholder="labels.searchPlaceholder"
>
</div>
<Layout
form
class="ui form"
>
<div class="fields">
<div class="ui field">
<Input
id="users-search"
v-model="query"
search
:label="t('components.manage.users.UsersTable.label.search')"
name="search"
type="text"
:placeholder="labels.searchPlaceholder"
/>
</div>
<Spacer :size="16" />
<Layout flex>
<Spacer grow />
<div class="field">
<label for="users-ordering">{{ t('components.manage.users.UsersTable.ordering.label') }}</label>
<select
@ -146,7 +156,7 @@ const labels = computed(() => ({
</option>
</select>
</div>
</div>
</Layout>
</div>
<div class="dimmable">
<div
@ -257,17 +267,15 @@ const labels = computed(() => ({
</action-table>
</div>
<div>
<pagination
<Pagination
v-if="result && result.count > paginateBy"
v-model:current="page"
:compact="true"
:paginate-by="paginateBy"
:total="result.count"
/>
<span v-if="result && result.results.length > 0">
{{ t('components.manage.users.UsersTable.pagination.results', {start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}, result.results.length) }}
</span>
</div>
</div>
</Layout>
</template>

View File

@ -2,6 +2,10 @@
import { useI18n } from 'vue-i18n'
import { computed } from 'vue'
import Layout from '~/components/ui/Layout.vue'
import Tabs from '~/components/ui/Tabs.vue'
import Tab from '~/components/ui/Tab.vue'
const { t } = useI18n()
const labels = computed(() => ({
@ -11,28 +15,22 @@ const labels = computed(() => ({
</script>
<template>
<div
<Layout
v-title="labels.manageUsers"
class="main"
main
stack
>
<nav
class="ui secondary pointing menu"
role="navigation"
:aria-label="labels.secondaryMenu"
>
<router-link
class="ui item"
<Tabs>
<Tab
:title="t('views.admin.users.Base.link.users')"
:to="{name: 'manage.users.users.list'}"
>
{{ t('views.admin.users.Base.link.users') }}
</router-link>
<router-link
class="ui item"
/>
<Tab
:title="t('views.admin.users.Base.link.invitations')"
:to="{name: 'manage.users.invitations.list'}"
>
{{ t('views.admin.users.Base.link.invitations') }}
</router-link>
</nav>
/>
</Tabs>
<router-view :key="$route.fullPath" />
</div>
</Layout>
</template>