cleaned up, refactored and implemented new <ModerationTools>

This commit is contained in:
Henry Jameson 2026-06-10 01:58:49 +03:00
commit e56ea2dbeb
23 changed files with 1124 additions and 975 deletions

View file

@ -4,6 +4,7 @@ export default {
validInnerComponents: ['Text', 'Icon', 'Link', 'Border', 'ButtonUnstyled'],
variants: {
normal: '.neutral',
info: '.info',
error: '.error',
warning: '.warning',
success: '.success',
@ -47,5 +48,11 @@ export default {
background: '--cGreen',
},
},
{
variant: 'info',
directives: {
background: '--cBlue',
},
},
],
}

View file

@ -31,9 +31,7 @@ const Announcement = {
canEditAnnouncement() {
return (
this.currentUser &&
this.currentUser.privileges.includes(
'announcements_manage_announcements',
)
this.currentUser.privileges.has('announcements_manage_announcements')
)
},
content() {

View file

@ -17,7 +17,7 @@ const Interactions = {
allowFollowingMove:
this.$store.state.users.currentUser.allow_following_move,
filterMode: tabModeDict.mentions,
canSeeReports: this.$store.state.users.currentUser.privileges.includes(
canSeeReports: this.$store.state.users.currentUser.has.has(
'reports_manage_reports',
),
}

View file

@ -14,7 +14,7 @@ const List = {
},
getKey: {
type: Function,
default: (item) => item,
default: (item) => item.id,
},
getClass: {
type: Function,
@ -37,7 +37,7 @@ const List = {
default: null,
},
},
emits: ['fetchRequested'],
emits: ['fetchRequested', 'select'],
components: {
Checkbox,
},
@ -56,11 +56,14 @@ const List = {
allKeys() {
return new Set(this.finalItems.map(this.getKey))
},
filteredSelected() {
return [...this.allKeys.values().filter((key) => this.selected.has(key))]
selectedItems() {
return this.items.filter((item) => this.selected.has(this.getKey(item)))
},
allSelected() {
return this.selected.size === this.finalItems.length
return (
this.selected.size !== 0 &&
this.selected.size === this.finalItems.length
)
},
noneSelected() {
return this.selected.size === 0
@ -101,6 +104,7 @@ const List = {
.catch((error) => {
this.loading = false
this.error = error
console.error('Error loading list data:', error)
})
},
reset() {
@ -124,19 +128,17 @@ const List = {
}
},
isSelected(item) {
return this.filteredSelected.indexOf(this.getKey(item)) !== -1
return this.selected.has(this.getKey(item))
},
toggle(checked, item) {
const key = this.getKey(item)
const oldChecked = this.isSelected(key)
if (checked !== oldChecked) {
if (checked) {
this.selected.add(key)
} else {
this.selected.delete(key)
}
if (checked) {
this.selected.add(key)
} else {
this.selected.delete(key)
}
this.$emit('selected', this.selected)
this.$emit('select', this.selected)
},
toggleAll(value) {
if (value) {
@ -144,7 +146,7 @@ const List = {
} else {
this.selected = new Set([])
}
this.$emit('selected', this.selected)
this.$emit('select', this.selected)
},
},
}

View file

@ -19,7 +19,7 @@
<div class="actions">
<slot
name="header"
:selected="filteredSelected"
:selected="selectedItems"
/>
</div>
</div>

View file

@ -1,6 +1,9 @@
import DialogModal from 'src/components/dialog_modal/dialog_modal.vue'
import { last } from 'lodash'
import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue'
import Popover from 'src/components/popover/popover.vue'
import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
import { useInstanceStore } from 'src/stores/instance.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
@ -16,41 +19,372 @@ const DISABLE_REMOTE_SUBSCRIPTION = 'mrf_tag:disable-remote-subscription'
const DISABLE_ANY_SUBSCRIPTION = 'mrf_tag:disable-any-subscription'
const SANDBOX = 'mrf_tag:sandbox'
const QUARANTINE = 'mrf_tag:quarantine'
const TAGS = new Set([
FORCE_NSFW,
STRIP_MEDIA,
FORCE_UNLISTED,
DISABLE_REMOTE_SUBSCRIPTION,
DISABLE_ANY_SUBSCRIPTION,
SANDBOX,
QUARANTINE,
])
const ENTRIES = [
{
check: '!state:activated',
label: 'user_card.admin_menu.activate_account',
},
{
check: 'state:activated',
label: 'user_card.admin_menu.deactivate_account',
},
{
separator: true,
},
{
check: '!state:confirmed',
label: 'user_card.admin_menu.confirm_account',
},
{
check: 'action:resend_confirmation',
conditions: ['!state:confirmed'],
label: 'user_card.admin_menu.resend_confirmation',
},
// No API for revocation
// {
// check: 'state:confirmed',
// label: 'user_card.admin_menu.unconfirm_account',
// },
{
check: '!state:approved',
conditions: ['property:local'],
label: 'user_card.admin_menu.approve_account',
},
// No API for revocation
// {
// check: 'state:approved',
// label: 'user_card.admin_menu.unapprove_account',
// },
{
check: '!state:suggested',
// conditions: ['property:local'], // TODO Should we allow non-local users in suggested?
label: 'user_card.admin_menu.suggest_account',
},
{
check: 'state:suggested',
label: 'user_card.admin_menu.remove_suggested_account',
},
{
separator: true,
},
{
check: '!state:disable_mfa',
label: 'user_card.admin_menu.disable_mfa',
},
{
check: '!state:require_password_change',
label: 'user_card.admin_menu.require_password_change',
},
{
separator: true,
},
{
check: '!rights:moderator',
label: 'user_card.admin_menu.grant_moderator',
conditions: ['property:local', 'state:activated'],
},
{
check: 'rights:moderator',
label: 'user_card.admin_menu.revoke_moderator',
conditions: ['property:local', 'state:activated'],
},
{
check: '!rights:admin',
label: 'user_card.admin_menu.grant_admin',
conditions: ['property:local', 'state:activated'],
},
{
check: 'rights:admin',
label: 'user_card.admin_menu.revoke_admin',
conditions: ['property:local', 'state:activated'],
},
{
separator: true,
},
{
check: FORCE_NSFW,
label: 'user_card.admin_menu.force_nsfw',
},
{
check: STRIP_MEDIA,
label: 'user_card.admin_menu.strip_media',
},
{
check: FORCE_UNLISTED,
label: 'user_card.admin_menu.force_unlisted',
},
{
check: SANDBOX,
label: 'user_card.admin_menu.sandbox',
},
{
check: DISABLE_ANY_SUBSCRIPTION,
conditions: ['property:local'],
label: 'user_card.admin_menu.disable_any_subscription',
},
{
check: DISABLE_REMOTE_SUBSCRIPTION,
conditions: ['property:local'],
label: 'user_card.admin_menu.disable_remote_subscription',
},
{
check: QUARANTINE,
conditions: ['property:local'],
label: 'user_card.admin_menu.quarantine',
},
{
separator: true,
},
{
check: 'action:delete',
label: 'user_card.admin_menu.delete_account',
},
]
const ModerationTools = {
props: ['user'],
props: {
users: {
type: Array,
required: true,
},
},
created() {
if (this.users.length !== 1) return
useAdminSettingsStore().getUserData({ user: this.users[0] })
},
data() {
return {
tags: {
FORCE_NSFW,
STRIP_MEDIA,
FORCE_UNLISTED,
DISABLE_REMOTE_SUBSCRIPTION,
DISABLE_ANY_SUBSCRIPTION,
SANDBOX,
QUARANTINE,
},
showDeleteUserDialog: false,
toggled: false,
open: false,
confirmDialogShow: false,
confirmDialogTitle: null,
confirmDialogContent: null,
confirmDialogConfirm: null,
confirmDialogAction: null,
confirmDialogGroup: null,
confirmDialogName: null,
}
},
components: {
DialogModal,
ConfirmModal,
Popover,
},
computed: {
ready() {
return this.users.every((u) => u.adminData)
},
entries() {
return ENTRIES.map(({ check, label, separator, conditions }) => {
if (separator) return 'separator'
const [, negateToken, group, name] = /^([!~]?)([a-z-_]+):([a-z-_]+)$/.exec(
check,
)
const hasTag = this.tagsSet.has(`${group}:${name}`)
const noTag = this.tagsSet.has(`!${group}:${name}`)
const maybeTag = this.tagsSet.has(`~${group}:${name}`)
// We are checking for condition to show element, i.e. only show "activate" if user is "deactivated"
const checkNegated = (negateToken === '!' || negateToken === '~')
// Naturally, new value should also be the same
const value = checkNegated
const action = (() => {
switch (group) {
case 'rights':
return () => this.setRight(name, value)
case 'state':
return () => this.setStatus(name, value)
case 'mrf_tag':
return () => this.setTag(`${group}:${name}`, noTag)
case 'action': {
switch (name) {
case 'delete': {
return () => this.deleteUsers()
}
case 'resend_confirmation': {
return () => this.resendConfirmationEmail()
}
case 'disable_mfa': {
return () => this.disableMFA()
}
case 'require_password_change': {
return () => this.requirePasswordChange()
}
default:
throw new Error(`Unknown action group: ${name}`)
}
}
default:
throw new Error(`Unknown moderation group: ${group}`)
}
})()
let checkboxClass = ''
if (maybeTag) {
checkboxClass = 'menu-checkbox-indeterminate'
} else if (hasTag) {
checkboxClass = 'menu-checkbox-checked'
}
return {
check,
negateToken,
checkbox: group === 'mrf_tag',
checkboxClass,
conditions,
group,
name,
action,
label,
value,
}
})
.filter((entry) => {
if (entry === 'separator') return true
const { group, name, value, conditions } = entry
if (conditions) {
// Checking that all items match positive criteria
const positive = conditions.every((condition) =>
this.totalSet.has(condition),
)
// Checking that there are no items that don't match criteria
const negative = conditions.some((condition) =>
this.totalSet.has('!' + condition),
)
if (!(positive && !negative)) return false
}
switch (group) {
case 'action': {
return true
}
case 'rights': {
return this.canGrantRole(name, value)
}
case 'state': {
return this.canChangeState(name, value)
}
case 'mrf_tag': {
return this.canUseTagPolicy
}
default: {
throw new Error(`Unknown moderation group: ${group}`)
}
}
})
.reduce((acc, entry, index) => {
if (entry === 'separator') {
if (
acc.length === 0 ||
last(acc) === 'separator' ||
index === ENTRIES.length - 1
) {
return acc
}
}
return [...acc, entry]
}, [])
},
rightsSet() {
return this.users.reduce((acc, user) => {
if (user.rights.admin) {
acc.add('rights:admin')
} else {
acc.add('!rights:admin')
}
if (user.rights.moderator) {
acc.add('rights:moderator')
} else {
acc.add('!rights:moderator')
}
return acc
}, new Set())
},
stateSet() {
return this.users.reduce((acc, user) => {
if (!user.deactivated) {
acc.add('state:activated')
} else {
acc.add('!state:activated')
}
if (user.adminData?.is_confirmed) {
acc.add('state:confirmed')
} else {
acc.add('!state:confirmed')
}
if (user.adminData?.is_approved) {
acc.add('state:approved')
} else {
acc.add('!state:approved')
}
if (user.adminData?.is_suggested) {
acc.add('state:suggested')
} else {
acc.add('!state:suggested')
}
return acc
}, new Set())
},
tagsSet() {
return new Set(this.user.tags)
const present = new Set()
const missing = new Set()
this.users.forEach((user) => {
TAGS.forEach((tag) => {
if (user.tags.has(tag)) {
present.add(tag)
} else {
missing.add(tag)
}
})
})
const result = new Set()
TAGS.forEach((tag) => {
if (present.has(tag) && missing.has(tag)) {
result.add(`~${tag}`)
} else if (missing.has(tag)) {
result.add(`!${tag}`)
} else {
result.add(tag)
}
})
return result
},
canGrantRole() {
return (
this.user.is_local &&
!this.user.deactivated &&
this.$store.state.users.currentUser.role === 'admin'
)
propertySet() {
return this.users.reduce((acc, user) => {
if (user.is_local) {
acc.add('property:local')
} else {
acc.add('!property:local')
}
return acc
}, new Set())
},
canChangeActivationState() {
return this.privileged('users_manage_activation_state')
disabled() {
return !this.ready || this.users.length === 0
},
totalSet() {
return new Set([
...this.rightsSet,
...this.stateSet,
...this.tagsSet,
...this.propertySet,
])
},
canDeleteAccount() {
return this.privileged('users_delete')
@ -63,87 +397,221 @@ const ModerationTools = {
},
},
methods: {
hasTag(tagName) {
return this.tagsSet.has(tagName)
canGrantRole(name, value) {
const setEntry = `${value ? '!' : ''}rights:${name}`
return (
this.$store.state.users.currentUser.role === 'admin' &&
this.totalSet.has(setEntry)
)
},
canChangeState(name, value) {
let privilege
switch (name) {
// TODO detailed privileges
default: {
privilege = 'users_manage_activation_state'
}
}
const setEntry = `${value ? '!' : ''}state:${name}`
return this.privileged(privilege) && this.totalSet.has(setEntry)
},
doConfirmDialogAction() {
if (typeof this.confirmDialogAction !== 'function') {
console.error('Confirm Dialog action is not a function!!')
}
this.confirmDialogAction()
this.clearConfirmDialog()
},
clearConfirmDialog() {
this.confirmDialogShow = false
this.confirmDialogTitle = null
this.confirmDialogContent = null
this.confirmDialogContent2 = null
this.confirmDialogDanger = false
this.confirmDialogConfirm = null
this.confirmDialogAction = null
this.confirmDialogGroup = null
this.confirmDialogName = null
},
privileged(privilege) {
return this.$store.state.users.currentUser.privileges.includes(privilege)
return this.$store.state.users.currentUser.privileges.has(privilege)
},
toggleTag(tag) {
const store = this.$store
if (this.tagsSet.has(tag)) {
store.state.api.backendInteractor
.untagUser({ user: this.user, tag })
.then((response) => {
if (!response.ok) {
return
}
store.commit('untagUser', { user: this.user, tag })
})
} else {
store.state.api.backendInteractor
.tagUser({ user: this.user, tag })
.then((response) => {
if (!response.ok) {
return
}
store.commit('tagUser', { user: this.user, tag })
})
}
setTag(tag, value) {
useAdminSettingsStore().setUsersTags({ users: this.users, value, tags: [tag] })
},
toggleRight(right) {
const store = this.$store
if (this.user.rights[right]) {
store.state.api.backendInteractor
.deleteRight({ user: this.user, right })
.then((response) => {
if (!response.ok) {
return
}
store.commit('updateRight', {
user: this.user,
right,
value: false,
})
})
} else {
store.state.api.backendInteractor
.addRight({ user: this.user, right })
.then((response) => {
if (!response.ok) {
return
}
store.commit('updateRight', { user: this.user, right, value: true })
})
}
setRight(right, value) {
useAdminSettingsStore().setUsersRight({ users: this.users, value, right })
},
toggleActivationStatus() {
this.$store.dispatch('toggleActivationStatus', { user: this.user })
},
deleteUserDialog(show) {
this.showDeleteUserDialog = show
},
deleteUser() {
const store = this.$store
const user = this.user
const { id, name } = user
store.state.api.backendInteractor.deleteUser({ user }).then(() => {
this.$store.dispatch(
'markStatusesAsDeleted',
(status) => user.id === status.user.id,
)
const isProfile =
this.$route.name === 'external-user-profile' ||
this.$route.name === 'user-profile'
const isTargetUser =
this.$route.params.name === name || this.$route.params.id === id
if (isProfile && isTargetUser) {
window.history.back()
setStatus(name, value) {
const noun = (() => {
switch (name) {
case 'activated':
return 'Activation'
case 'confirmed':
return 'Confirmation'
case 'approved':
return 'Approval'
case 'suggested':
return 'Suggestion'
}
})()
useAdminSettingsStore()[`setUsers${noun}Status`]({
users: this.users,
value,
})
},
setToggled(value) {
this.toggled = value
resendConfirmationEmail() {
useAdminSettingsStore().resendConfirmationEmail({ users: this.users })
},
requirePasswordChange() {
useAdminSettingsStore().requirePasswordChange({ users: this.users })
},
disableMFA() {
this.users.forEach((user) => {
useAdminSettingsStore().disableMFA({ user })
})
},
deleteUsers() {
const { id, name } = this.users[0]
useAdminSettingsStore()
.deleteUsers({ users: this.users })
.then((userIds) => {
if (userIds.length > 1) return
const isProfile =
this.$route.name === 'external-user-profile' ||
this.$route.name === 'user-profile'
const isTargetUser =
this.$route.params.name === name || this.$route.params.id === id
if (isProfile && isTargetUser) {
window.history.back()
}
})
},
setOpen(value) {
this.open = value
},
maybeShowConfirm(close, { group, name, action, value }) {
close()
this.confirmDialogName = name
this.confirmDialogGroup = group
this.confirmDialogAction = () => action()
switch (group) {
case 'action': {
if (name === 'delete') {
this.confirmDialogShow = true
this.confirmDialogTitle = this.$t(
'user_card.admin_menu.confirm_modal.delete_title',
)
this.confirmDialogDanger = true
this.confirmDialogContent =
'user_card.admin_menu.confirm_modal.delete_content'
this.confirmDialogContent2 =
'user_card.admin_menu.confirm_modal.delete_content_2'
this.confirmDialogConfirm = this.$t(
'user_card.admin_menu.confirm_modal.delete',
)
}
break
}
case 'state': {
switch (name) {
case 'activated': {
this.confirmDialogShow = true
this.confirmDialogTitle = this.$t(
'user_card.admin_menu.confirm_modal.activate_title',
)
this.confirmDialogContent = value
? 'user_card.admin_menu.confirm_modal.activate_content'
: 'user_card.admin_menu.confirm_modal.deactivate_content'
this.confirmDialogConfirm = value
? this.$t('user_card.admin_menu.confirm_modal.activate')
: this.$t('user_card.admin_menu.confirm_modal.deactivate')
break
}
// Confirmation and Approval statuses cannot be revokedn(no API)
case 'confirmed': {
this.confirmDialogTitle = this.$t(
'user_card.admin_menu.confirm_modal.confirm_title',
)
this.confirmDialogContent = //value
/*?*/ 'user_card.admin_menu.confirm_modal.confirm_content'
//: 'user_card.admin_menu.confirm_modal.confirm_revoke_content'
this.confirmDialogConfirm = value
/*?*/ this.$t('user_card.admin_menu.confirm_modal.confirm')
//: this.$t('user_card.admin_menu.confirm_modal.revoke')
break
}
case 'approved': {
this.confirmDialogTitle = this.$t(
'user_card.admin_menu.confirm_modal.approval_title',
)
this.confirmDialogContent = //value
/*?*/ 'user_card.admin_menu.confirm_modal.approval_content'
//: 'user_card.admin_menu.confirm_modal.approval_revoke_content'
this.confirmDialogConfirm = value
/*?*/ this.$t('user_card.admin_menu.confirm_modal.approve')
//: this.$t('user_card.admin_menu.confirm_modal.revoke')
break
}
case 'suggest': {
this.confirmDialogTitle = this.$t(
'user_card.admin_menu.confirm_modal.suggest_title',
)
this.confirmDialogContent = value
? 'user_card.admin_menu.confirm_modal.add_suggest_content'
: 'user_card.admin_menu.confirm_modal.remove_suggest_content'
this.confirmDialogConfirm = value
? this.$t('user_card.admin_menu.confirm_modal.add')
: this.$t('user_card.admin_menu.confirm_modal.remove')
break
}
}
break
}
case 'rights': {
this.confirmDialogTitle = this.$t(
'user_card.admin_menu.confirm_modal.user_rights_title',
)
this.confirmDialogContent = value
? 'user_card.admin_menu.confirm_modal.grant_role_content'
: 'user_card.admin_menu.confirm_modal.revoke_role_content'
this.confirmDialogConfirm = value
? this.$t('user_card.admin_menu.confirm_modal.grant')
: this.$t('user_card.admin_menu.confirm_modal.revoke')
break
}
case 'mrf_tag': {
this.confirmDialogTitle = this.$t(
'user_card.admin_menu.user_tag_title',
)
this.confirmDialogContent = value
? 'user_card.admin_menu.confirm_modal.assign_tag_content'
: 'user_card.admin_menu.confirm_modal.unassign_tag_content'
this.confirmDialogConfirm = value
? this.$t('user_card.admin_menu.confirm_modal.assign')
: this.$t('user_card.admin_menu.confirm_modal.unassign')
break
}
}
if (this.users.length > 1) {
this.confirmDialogShow = true
}
if (!this.confirmDialogShow) {
this.doConfirmDialogAction()
}
},
},
}

View file

@ -3,154 +3,37 @@
<Popover
trigger="click"
class="moderation-tools-popover"
:disabled="disabled"
placement="bottom"
:offset="{ y: 5 }"
@show="setToggled(true)"
@close="setToggled(false)"
@show="setOpen(true)"
@close="setOpen(false)"
>
<template #content>
<template #content="{close}">
<div class="dropdown-menu">
<template v-if="canGrantRole">
<div class="menu-item dropdown-item -icon-space">
<button
class="main-button"
@click="toggleRight(&quot;admin&quot;)"
>
{{ $t(!!user.rights.admin ? 'user_card.admin_menu.revoke_admin' : 'user_card.admin_menu.grant_admin') }}
</button>
</div>
<div class="menu-item dropdown-item -icon-space">
<button
class="main-button"
@click="toggleRight(&quot;moderator&quot;)"
>
{{ $t(!!user.rights.moderator ? 'user_card.admin_menu.revoke_moderator' : 'user_card.admin_menu.grant_moderator') }}
</button>
</div>
<template v-for="(entry, index) in entries">
<div
v-if="canChangeActivationState || canDeleteAccount"
v-if="entry === 'separator'"
:key="index"
role="separator"
class="dropdown-divider"
/>
</template>
<div
v-if="canChangeActivationState"
class="menu-item dropdown-item -icon-space"
>
<button
class="main-button"
@click="toggleActivationStatus()"
>
{{ $t(!!user.deactivated ? 'user_card.admin_menu.activate_account' : 'user_card.admin_menu.deactivate_account') }}
</button>
</div>
<div
v-if="canDeleteAccount"
class="menu-item dropdown-item -icon-space"
>
<button
class="main-button"
@click="deleteUserDialog(true)"
>
{{ $t('user_card.admin_menu.delete_account') }}
</button>
</div>
<template v-if="canUseTagPolicy">
<div
role="separator"
class="dropdown-divider"
/>
<div class="menu-item dropdown-item -icon">
<button
class="main-button"
@click="toggleTag(tags.FORCE_NSFW)"
>
<span
class="input menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_NSFW) }"
/>
{{ $t('user_card.admin_menu.force_nsfw') }}
</button>
</div>
<div class="menu-item dropdown-item -icon">
<button
class="main-button"
@click="toggleTag(tags.STRIP_MEDIA)"
>
<span
class="input menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.STRIP_MEDIA) }"
/>
{{ $t('user_card.admin_menu.strip_media') }}
</button>
</div>
<div class="menu-item dropdown-item -icon">
<button
class="main-button"
@click="toggleTag(tags.FORCE_UNLISTED)"
>
<span
class="input menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_UNLISTED) }"
/>
{{ $t('user_card.admin_menu.force_unlisted') }}
</button>
</div>
<div class="menu-item dropdown-item -icon">
<button
class="main-button"
@click="toggleTag(tags.SANDBOX)"
>
<span
class="input menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.SANDBOX) }"
/>
{{ $t('user_card.admin_menu.sandbox') }}
</button>
</div>
<div
v-if="user.is_local"
class="menu-item dropdown-item -icon"
v-else
:key="entry.label"
class="menu-item dropdown-item"
:class="entry.checkbox ? '-icon' : '-icon-space'"
>
<button
class="main-button"
@click="toggleTag(tags.DISABLE_REMOTE_SUBSCRIPTION)"
@click="() => maybeShowConfirm(close, entry)"
>
<span
v-if="entry.checkbox"
class="input menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_REMOTE_SUBSCRIPTION) }"
:class="entry.checkboxClass"
/>
{{ $t('user_card.admin_menu.disable_remote_subscription') }}
</button>
</div>
<div
v-if="user.is_local"
class="menu-item dropdown-item -icon"
>
<button
class="main-button"
@click="toggleTag(tags.DISABLE_ANY_SUBSCRIPTION)"
>
<span
class="input menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_ANY_SUBSCRIPTION) }"
/>
{{ $t('user_card.admin_menu.disable_any_subscription') }}
</button>
</div>
<div
v-if="user.is_local"
class="menu-item dropdown-item -icon"
>
<button
class="main-button"
@click="toggleTag(tags.QUARANTINE)"
>
<span
class="input menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.QUARANTINE) }"
/>
{{ $t('user_card.admin_menu.quarantine') }}
{{ $t(entry.label) }}
</button>
</div>
</template>
@ -159,37 +42,60 @@
<template #trigger>
<button
class="btn button-default btn-block moderation-tools-button"
:class="{ toggled }"
:class="{ toggled: open, disabled }"
:disabled="disabled"
>
{{ $t('user_card.admin_menu.moderation') }}
<FAIcon icon="chevron-down" />
<FAIcon v-if="ready" icon="chevron-down" />
<span v-else class="loading-spinner">
<FAIcon
class="fa-old-padding"
spin
icon="circle-notch"
/>
</span>
</button>
</template>
</Popover>
<teleport to="#modal">
<DialogModal
v-if="showDeleteUserDialog"
:on-cancel="deleteUserDialog.bind(this, false)"
<ConfirmModal
v-if="confirmDialogShow"
:title="$t(confirmDialogTitle)"
:confirm-text="confirmDialogConfirm"
:confirm-danger="confirmDialogDanger"
:cancel-text="$t('general.cancel')"
@accepted="doConfirmDialogAction"
@cancelled="clearConfirmDialog"
>
<template #header>
{{ $t('user_card.admin_menu.delete_user') }}
</template>
<p>{{ $t('user_card.admin_menu.delete_user_confirmation') }}</p>
<template #footer>
<button
class="btn button-default"
@click="deleteUserDialog(false)"
>
{{ $t('general.cancel') }}
</button>
<button
class="btn button-default danger"
@click="deleteUser()"
>
{{ $t('user_card.admin_menu.delete_user') }}
</button>
</template>
</DialogModal>
<i18n-t
:plural="users.length"
scope="global"
:keypath="confirmDialogContent"
tag="p"
>
<template #user>
<span
v-text="users[0].screen_name_ui"
/>
</template>
<template #count>
{{ users.length }}
</template>
<template #name>
<code>
{{ confirmDialogName }}
</code>
</template>
</i18n-t>
<p v-if="confirmDialogContent2">
{{ $t(confirmDialogContent2) }}
</p>
<ul v-if="users.length > 1">
<li v-for="user in users">
{{ user.screen_name }}
</li>
</ul>
</ConfirmModal>
</teleport>
</div>
</template>

View file

@ -96,6 +96,11 @@
content: "";
}
&.menu-checkbox-indeterminate::after {
font-size: 1.25em;
content: "";
}
&.-radio {
border-radius: 9999px;
@ -103,6 +108,11 @@
font-size: 2em;
content: "";
}
&.menu-checkbox-indeterminate::after {
font-size: 2em;
content: "";
}
}
}
}

View file

@ -1,8 +1,7 @@
import { defineAsyncComponent } from 'vue'
import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
import { useAdminUsersStore } from 'src/stores/adminUsers.js'
import ModerationTools from 'src/components/moderation_tools/moderation_tools.vue'
const AdminCard = {
props: {
@ -12,23 +11,15 @@ const AdminCard = {
},
components: {
BasicUserCard,
ModerationTools: defineAsyncComponent(
() => import('src/components/moderation_tools/moderation_tools.vue'),
),
ModerationTools,
},
computed: {
user() {
return this.$store.getters.findUser(this.userId)
},
userAdminData() {
return useAdminUsersStore().getUser(this.userId)
},
relationship() {
return this.$store.getters.relationship(this.userId)
},
isLocal() {
return this.user.is_local
},
isAdmin() {
return this.user.rights.admin
},
@ -38,12 +29,6 @@ const AdminCard = {
isActivated() {
return !this.user.deactivated
},
isApproved() {
return this.userAdminData.is_approved
},
isConfirmed() {
return this.userAdminData.is_confirmed
},
},
}

View file

@ -26,7 +26,7 @@
</label>
<label
v-if="isActivated"
class="alert info user-role"
class="alert success user-role"
>
{{ $t('admin_dash.users.indicator.active') }}
</label>
@ -37,14 +37,32 @@
{{ $t('admin_dash.users.indicator.deactivated') }}
</label>
<label
v-if="isConfirmed"
class="alert neutral user-role"
v-if="user.adminData.is_confirmed"
class="alert success user-role"
>
{{ $t('admin_dash.users.indicator.confirmed') }}
</label>
<label
v-if="!user.adminData.is_confirmed"
class="alert warning user-role"
>
{{ $t('admin_dash.users.indicator.unconfirmed') }}
</label>
<label
v-if="user.adminData.is_approved"
class="alert success user-role"
>
{{ $t('admin_dash.users.indicator.approved') }}
</label>
<label
v-if="!user.adminData.is_approved"
class="alert warning user-role"
>
{{ $t('admin_dash.users.indicator.unapproved') }}
</label>
<ModerationTools
class="moderation-menu"
:user="user"
:users="[user]"
/>
</div>
</BasicUserCard>

View file

@ -38,7 +38,7 @@ const FrontendsTab = {
},
created() {
if (this.user.rights.admin) {
this.$store.dispatch('loadFrontendsStuff')
useAdminSettingsStore().loadFrontendsStuff()
}
},
computed: {
@ -77,7 +77,7 @@ const FrontendsTab = {
this.working = false
})
.then(async (response) => {
this.$store.dispatch('loadFrontendsStuff')
useAdminSettingsStore().loadFrontendsStuff()
if (response.error) {
const reason = await response.error.json()
useInterfaceStore().pushGlobalNotice({

View file

@ -4,15 +4,26 @@ import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import GenericConfirm from 'src/components/confirm_modal/generic_confirm.vue'
import List from 'src/components/list/list.vue'
import Popover from 'src/components/popover/popover.vue'
import ModerationTools from 'src/components/moderation_tools/moderation_tools.vue'
import ProgressButton from 'src/components/progress_button/progress_button.vue'
import Select from 'src/components/select/select.vue'
import AdminCard from 'src/components/settings_modal/admin_tabs/admin_card.vue'
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import { useAdminUsersStore } from 'src/stores/adminUsers.js'
import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
const UsersTab = {
components: {
Checkbox,
Select,
BasicUserCard,
List,
ProgressButton,
AdminCard,
TabSwitcher,
ModerationTools,
GenericConfirm,
},
provide() {
return {
defaultDraftMode: true,
@ -90,7 +101,7 @@ const UsersTab = {
local: this.filtersLocal,
external: this.filtersExternal,
needApproval: this.filtersNeedApproval,
unconfirmed: this.filtersUnconfirmeUnconfirmed,
unconfirmed: this.filtersUnconfirmed,
}
return {
@ -102,51 +113,15 @@ const UsersTab = {
}
},
},
components: {
Checkbox,
Select,
BasicUserCard,
List,
ProgressButton,
AdminCard,
TabSwitcher,
Popover,
GenericConfirm,
},
methods: {
fetchUsers(page) {
return useAdminUsersStore()
return useAdminSettingsStore()
.fetchAdminUsers({
...this.fetchOptions,
page,
})
.then(({ count, users }) => ({ count, items: users }))
},
/**
* show the confirmation box for bulk actions.
* @param {string} box ref name specified for the confirm component
*/
confirmSelection(box) {
this.$refs[box].show()
this.$refs.dropdown.hidePopover()
},
/**
* called when a bulk action was confirmed
* @param {string} action
*/
selectionConfirmed(action) {
const restricted = []
const s = this.$refs.userList.getSelected()
s.forEach((u) => {
if (
restricted.includes(action) !== false ||
u.id !== this.$store.state.users.currentUser.id
) {
const uf = this.$store.getters.findUser(u.id)
this.$store.dispatch(action, this.$store.getters.findUser(u.id))
}
})
},
},
watch: {
fetchOptions() {

View file

@ -115,7 +115,7 @@
</Checkbox>
</div>
<div class="filter">
<Checkbox v-model="filtersUncomfirmed">
<Checkbox v-model="filtersUnconfirmed">
{{ $t('admin_dash.users.options.only_unconfirmed') }}
</Checkbox>
</div>
@ -123,130 +123,15 @@
<List
ref="usersList"
:fetch-function="fetchUsers"
@select="onSelect"
selectable
scrollable
>
<template #header>
<Popover
ref="dropdown"
trigger="click"
placement="bottom"
>
<template #trigger>
<button
class="button button-default btn"
>
{{ $t('admin_dash.users.actions.title') }}
</button>
</template>
<template #content>
<div class="dropdown-menu">
<div class="menu-item dropdown-item">
<button
class="main-button"
@click="confirmSelection('confirmActivate')"
>
{{ $t('admin_dash.users.actions.activate') }}
</button>
</div>
<div class="menu-item dropdown-item">
<button
class="main-button"
@click="confirmSelection('confirmDeactivate')"
>
{{ $t('admin_dash.users.actions.deactivate') }}
</button>
</div>
<div class="menu-item dropdown-item">
<button
class="main-button"
@click="confirmSelection('confirmDelete')"
>
{{ $t('admin_dash.users.actions.delete_user') }}
</button>
</div>
<div class="menu-item dropdown-item">
<button
class="main-button"
@click="confirmSelection('confirmGrantAdmin')"
>
{{ $t('admin_dash.users.actions.grant_admin') }}
</button>
</div>
<div class="menu-item dropdown-item">
<button
class="main-button"
@click="confirmSelection('confirmRevokeAdmin')"
>
{{ $t('admin_dash.users.actions.revoke_admin') }}
</button>
</div>
<div class="menu-item dropdown-item">
<button
class="main-button"
@click="confirmSelection('confirmGrantModerator')"
>
{{ $t('admin_dash.users.actions.grant_moderator') }}
</button>
</div>
<div class="menu-item dropdown-item">
<button
class="main-button"
@click="confirmSelection('confirmRevokeModerator')"
>
{{ $t('admin_dash.users.actions.revoke_moderator') }}
</button>
</div>
<div class="menu-item dropdown-item">
<button
class="main-button"
@click="confirmSelection('confirmApprove')"
>
{{ $t('admin_dash.users.actions.approve') }}
</button>
</div>
<div class="menu-item dropdown-item">
<button
class="main-button"
@click="confirmSelection('confirmConfirm')"
>
{{ $t('admin_dash.users.actions.confirm') }}
</button>
</div>
<div class="menu-item dropdown-item">
<button
class="main-button"
@click="confirmSelection('confirmResendEmail')"
>
{{ $t('admin_dash.users.actions.resend_confirmation_email') }}
</button>
</div>
<div class="menu-item dropdown-item">
<button
class="main-button"
@click="confirmSelection('confirmRequirePasswordChange')"
>
{{ $t('admin_dash.users.actions.require_password_change') }}
</button>
</div>
<div class="menu-item dropdown-item">
<button
class="main-button"
@click="confirmSelection('confirmDisableMFA')"
>
{{ $t('admin_dash.users.actions.disable_mfa') }}
</button>
</div>
</div>
</template>
</Popover>
<template #header="{selected}">
<ModerationTools :users="selected" />
</template>
<template #item="{item}">
<AdminCard
:user-id="item.id"
:confirmed="item.is_confirmed"
:suggested="item.is_suggested"
/>
<AdminCard :user-id="item.id" />
</template>
<template #load>
<span> loading </span>
@ -255,102 +140,6 @@
<span> no users </span>
</template>
</List>
<GenericConfirm
ref="confirmActivate"
:title="$t('admin_dash.users.actions.confirm_multi.title')"
:message="$t('admin_dash.users.actions.confirm_multi.activate')"
:cancel-text="$t('admin_dash.users.actions.no')"
:confirm-text="$t('admin_dash.users.actions.yes')"
@action="selectionConfirmed('adminActivateUser')"
/>
<GenericConfirm
ref="confirmDeactivate"
:title="$t('admin_dash.users.actions.confirm_multi.title')"
:message="$t('admin_dash.users.actions.confirm_multi.deactivate')"
:cancel-text="$t('admin_dash.users.actions.no')"
:confirm-text="$t('admin_dash.users.actions.yes')"
@action="selectionConfirmed('adminDeactivateUser')"
/>
<GenericConfirm
ref="confirmDelete"
:title="$t('admin_dash.users.actions.confirm_multi.title')"
:message="$t('admin_dash.users.actions.confirm_multi.delete_user')"
:cancel-text="$t('admin_dash.users.actions.no')"
:confirm-text="$t('admin_dash.users.actions.yes')"
@action="selectionConfirmed('adminDeleteUser')"
/>
<GenericConfirm
ref="confirmGrantAdmin"
:title="$t('admin_dash.users.actions.confirm_multi.title')"
:message="$t('admin_dash.users.actions.confirm_multi.grant_admin')"
:cancel-text="$t('admin_dash.users.actions.no')"
:confirm-text="$t('admin_dash.users.actions.yes')"
@action="selectionConfirmed('adminAddUserToAdminGroup')"
/>
<GenericConfirm
ref="confirmRevokeAdmin"
:title="$t('admin_dash.users.actions.confirm_multi.title')"
:message="$t('admin_dash.users.actions.confirm_multi.revoke_admin')"
:cancel-text="$t('admin_dash.users.actions.no')"
:confirm-text="$t('admin_dash.users.actions.yes')"
@action="selectionConfirmed('adminRemoveUserFromAdminGroup')"
/>
<GenericConfirm
ref="confirmGrantModerator"
:title="$t('admin_dash.users.actions.confirm_multi.title')"
:message="$t('admin_dash.users.actions.confirm_multi.grant_moderator')"
:cancel-text="$t('admin_dash.users.actions.no')"
:confirm-text="$t('admin_dash.users.actions.yes')"
@action="selectionConfirmed('adminAddUserToModeratorGroup')"
/>
<GenericConfirm
ref="confirmRevokeModerator"
:title="$t('admin_dash.users.actions.confirm_multi.title')"
:message="$t('admin_dash.users.actions.confirm_multi.revoke_moderator')"
:cancel-text="$t('admin_dash.users.actions.no')"
:confirm-text="$t('admin_dash.users.actions.yes')"
@action="selectionConfirmed('adminRemoveUserFromModeratorGroup')"
/>
<GenericConfirm
ref="confirmApprove"
:title="$t('admin_dash.users.actions.confirm_multi.title')"
:message="$t('admin_dash.users.actions.confirm_multi.approve')"
:cancel-text="$t('admin_dash.users.actions.no')"
:confirm-text="$t('admin_dash.users.actions.yes')"
@action="selectionConfirmed('adminApproveUser')"
/>
<GenericConfirm
ref="confirmConfirm"
:title="$t('admin_dash.users.actions.confirm_multi.title')"
:message="$t('admin_dash.users.actions.confirm_multi.confirm')"
:cancel-text="$t('admin_dash.users.actions.no')"
:confirm-text="$t('admin_dash.users.actions.yes')"
@action="selectionConfirmed('adminConfirmUser')"
/>
<GenericConfirm
ref="confirmResendEmail"
:title="$t('admin_dash.users.actions.confirm_multi.title')"
:message="$t('admin_dash.users.actions.confirm_multi.resend_confirmation_email')"
:cancel-text="$t('admin_dash.users.actions.no')"
:confirm-text="$t('admin_dash.users.actions.yes')"
@action="selectionConfirmed('adminResendConfirmationEmail')"
/>
<GenericConfirm
ref="confirmRequirePasswordChange"
:title="$t('admin_dash.users.actions.confirm_multi.title')"
:message="$t('admin_dash.users.actions.confirm_multi.require_password_change')"
:cancel-text="$t('admin_dash.users.actions.no')"
:confirm-text="$t('admin_dash.users.actions.yes')"
@action="selectionConfirmed('adminRequirePasswordChange')"
/>
<GenericConfirm
ref="confirmDisableMFA"
:title="$t('admin_dash.users.actions.confirm_multi.title')"
:message="$t('admin_dash.users.actions.confirm_multi.disable_mfa')"
:cancel-text="$t('admin_dash.users.actions.no')"
:confirm-text="$t('admin_dash.users.actions.yes')"
@action="selectionConfirmed('adminDisableMFA')"
/>
</div>
</template>
<script src="./users_tab.js"></script>

View file

@ -1,6 +1,7 @@
import { mapState as mapPiniaState } from 'pinia'
import { mapState } from 'vuex'
import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
const SharedComputedObject = () => ({
@ -8,9 +9,11 @@ const SharedComputedObject = () => ({
...mapPiniaState(useMergedConfigStore, {
expertLevel: (store) => store.mergedConfig.expertLevel,
}),
...mapPiniaState(useAdminSettingsStore, {
adminConfig: (store) => store.config,
adminDraft: (store) => store.draft,
}),
...mapState({
adminConfig: (state) => state.adminSettings.config,
adminDraft: (state) => state.adminSettings.draft,
user: (state) => state.users.currentUser,
}),
})

View file

@ -229,7 +229,7 @@ export const BUTTONS = [
return (
loggedIn &&
(status.user.id === currentUser.id ||
currentUser.privileges.includes('messages_delete'))
currentUser.privileges.has('messages_delete'))
)
},
confirm: ({ getters }) => useMergedConfigStore().mergedConfig.modalOnDelete,

View file

@ -280,7 +280,7 @@ export default {
},
},
visibleRole() {
if (!this.newShowRole) {
if (!this.user.show_role && !this.user.adminData) {
return
}
const rights = this.user.rights

View file

@ -291,8 +291,7 @@
</button>
<ModerationTools
v-if="showModerationMenu"
class="moderation-menu"
:user="user"
:users="[user]"
/>
</div>
<div