mirror of
https://codeberg.org/yeentown/barkey.git
synced 2025-07-07 20:44:34 +00:00
merge: Cleanup admin user UI (!1012)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1012 Approved-by: dakkar <dakkar@thenautilus.net> Approved-by: Marie <github@yuugi.dev>
This commit is contained in:
commit
b05b4ec74d
8 changed files with 261 additions and 74 deletions
54
locales/index.d.ts
vendored
54
locales/index.d.ts
vendored
|
@ -2447,7 +2447,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"disablePagesScript": string;
|
||||
/**
|
||||
* リモートユーザー情報の更新
|
||||
* Refresh remote data
|
||||
*/
|
||||
"updateRemoteUser": string;
|
||||
/**
|
||||
|
@ -13069,6 +13069,58 @@ export interface Locale extends ILocale {
|
|||
* Users popular on {name}
|
||||
*/
|
||||
"popularUsersLocal": ParameterizedString<"name">;
|
||||
/**
|
||||
* Silenced
|
||||
*/
|
||||
"silenced": string;
|
||||
/**
|
||||
* Total followers
|
||||
*/
|
||||
"totalFollowers": string;
|
||||
/**
|
||||
* Total following
|
||||
*/
|
||||
"totalFollowing": string;
|
||||
/**
|
||||
* Local followers
|
||||
*/
|
||||
"localFollowers": string;
|
||||
/**
|
||||
* Local following
|
||||
*/
|
||||
"localFollowing": string;
|
||||
/**
|
||||
* Remote followers
|
||||
*/
|
||||
"remoteFollowers": string;
|
||||
/**
|
||||
* Remote following
|
||||
*/
|
||||
"remoteFollowing": string;
|
||||
/**
|
||||
* Activity Pub
|
||||
*/
|
||||
"activityPub": string;
|
||||
/**
|
||||
* IP
|
||||
*/
|
||||
"ip": string;
|
||||
/**
|
||||
* The date is when IP address was first used.
|
||||
*/
|
||||
"ipTip": string;
|
||||
/**
|
||||
* Period
|
||||
*/
|
||||
"rolePeriod": string;
|
||||
/**
|
||||
* Assigned
|
||||
*/
|
||||
"roleAssigned": string;
|
||||
/**
|
||||
* automatic
|
||||
*/
|
||||
"roleAutomatic": string;
|
||||
/**
|
||||
* Translation timeout
|
||||
*/
|
||||
|
|
|
@ -12,6 +12,7 @@ import { RoleEntityService } from '@/core/entities/RoleEntityService.js';
|
|||
import { IdService } from '@/core/IdService.js';
|
||||
import { notificationRecieveConfig } from '@/models/json-schema/user.js';
|
||||
import { isSystemAccount } from '@/misc/is-system-account.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
@ -186,6 +187,36 @@ export const meta = {
|
|||
},
|
||||
},
|
||||
},
|
||||
followStats: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
totalFollowing: {
|
||||
type: 'number',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
totalFollowers: {
|
||||
type: 'number',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
localFollowing: {
|
||||
type: 'number',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
localFollowers: {
|
||||
type: 'number',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
remoteFollowing: {
|
||||
type: 'number',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
remoteFollowers: {
|
||||
type: 'number',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
@ -213,6 +244,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private roleService: RoleService,
|
||||
private roleEntityService: RoleEntityService,
|
||||
private idService: IdService,
|
||||
private readonly cacheService: CacheService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const [user, profile] = await Promise.all([
|
||||
|
@ -237,6 +269,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
const roleAssigns = await this.roleService.getUserAssigns(user.id);
|
||||
const roles = await this.roleService.getUserRoles(user.id);
|
||||
|
||||
const followStats = await this.cacheService.getFollowStats(user.id);
|
||||
|
||||
return {
|
||||
email: profile.email,
|
||||
emailVerified: profile.emailVerified,
|
||||
|
@ -269,6 +303,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
expiresAt: a.expiresAt ? a.expiresAt.toISOString() : null,
|
||||
roleId: a.roleId,
|
||||
})),
|
||||
followStats: {
|
||||
...followStats,
|
||||
totalFollowers: Math.max(user.followersCount, followStats.localFollowers + followStats.remoteFollowers),
|
||||
totalFollowing: Math.max(user.followingCount, followStats.localFollowing + followStats.remoteFollowing),
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<button
|
||||
v-if="!link"
|
||||
ref="el" class="_button"
|
||||
:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike, [$style.iconOnly]: iconOnly, [$style.wait]: wait }]"
|
||||
:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.accent]: accent, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike, [$style.iconOnly]: iconOnly, [$style.wait]: wait }]"
|
||||
:type="type"
|
||||
:name="name"
|
||||
:value="value"
|
||||
|
@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</button>
|
||||
<MkA
|
||||
v-else class="_button"
|
||||
:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike, [$style.iconOnly]: iconOnly, [$style.wait]: wait }]"
|
||||
:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.accent]: accent, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike, [$style.iconOnly]: iconOnly, [$style.wait]: wait }]"
|
||||
:to="to ?? '#'"
|
||||
:behavior="linkBehavior"
|
||||
@mousedown="onMousedown"
|
||||
|
@ -48,6 +48,7 @@ const props = defineProps<{
|
|||
linkBehavior?: null | 'window' | 'browser';
|
||||
autofocus?: boolean;
|
||||
wait?: boolean;
|
||||
accent?: boolean;
|
||||
danger?: boolean;
|
||||
full?: boolean;
|
||||
small?: boolean;
|
||||
|
@ -234,6 +235,24 @@ function onMousedown(evt: MouseEvent): void {
|
|||
}
|
||||
}
|
||||
|
||||
&.accent {
|
||||
font-weight: bold;
|
||||
color: var(--MI_THEME-accent);
|
||||
|
||||
&.primary {
|
||||
color: #fff;
|
||||
background: var(--MI_THEME-accent);
|
||||
|
||||
&:not(:disabled):hover {
|
||||
background: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
|
||||
}
|
||||
|
||||
&:not(:disabled):active {
|
||||
background: hsl(from var(--MI_THEME-accent) h s calc(l - 10));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.danger {
|
||||
font-weight: bold;
|
||||
color: var(--MI_THEME-error);
|
||||
|
|
|
@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { reactive, watch } from 'vue';
|
||||
import number from '@/filters/number.js';
|
||||
import { prefer } from '@/preferences';
|
||||
|
||||
const props = defineProps<{
|
||||
value: number;
|
||||
|
@ -36,7 +37,11 @@ watch(() => props.value, (to, from) => {
|
|||
}
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(step);
|
||||
if (prefer.s.animation) {
|
||||
window.requestAnimationFrame(step);
|
||||
} else {
|
||||
tweened.number = to;
|
||||
}
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
|
|
|
@ -41,7 +41,7 @@ import { i18n } from '@/i18n.js';
|
|||
const props = withDefaults(defineProps<{
|
||||
role: Misskey.entities.Role;
|
||||
forModeration: boolean;
|
||||
detailed: boolean;
|
||||
detailed?: boolean;
|
||||
}>(), {
|
||||
detailed: true,
|
||||
});
|
||||
|
|
|
@ -7,38 +7,49 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs">
|
||||
<div class="_spacer" style="--MI_SPACER-w: 600px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
|
||||
<FormSuspense :p="init">
|
||||
<div v-if="tab === 'overview'" class="_gaps_m">
|
||||
<div class="aeakzknw">
|
||||
<div v-if="tab === 'overview'" class="_gaps">
|
||||
<div v-if="user" class="aeakzknw">
|
||||
<MkAvatar class="avatar" :user="user" indicator link preview/>
|
||||
<div class="body">
|
||||
<span class="name"><MkUserName class="name" :user="user"/></span>
|
||||
<span class="sub"><span class="acct _monospace">@{{ acct(user) }}</span></span>
|
||||
<span class="sub">
|
||||
<span class="acct _monospace">@{{ acct(user) }}</span>
|
||||
<button v-tooltip="i18n.ts.copy" class="_textButton" style="margin-left: 0.5em;" @click="copyToClipboard('@' + acct(user))"><i class="ti ti-copy"></i></button>
|
||||
</span>
|
||||
<span class="sub">
|
||||
<span class="_monospace">{{ user.id }}</span>
|
||||
<button v-tooltip="i18n.ts.copy" class="_textButton" style="margin-left: 0.5em;" @click="copyToClipboard(user.id)"><i class="ti ti-copy"></i></button>
|
||||
</span>
|
||||
<span class="state">
|
||||
<span v-if="!approved" class="silenced">{{ i18n.ts.notApproved }}</span>
|
||||
<span v-if="approved && !user.host" class="moderator">{{ i18n.ts.approved }}</span>
|
||||
<span v-if="suspended" class="suspended">Suspended</span>
|
||||
<span v-if="silenced" class="silenced">Silenced</span>
|
||||
<span v-if="moderator" class="moderator">Moderator</span>
|
||||
<span v-if="suspended" class="suspended">{{ i18n.ts.suspended }}</span>
|
||||
<span v-if="silenced" class="silenced">{{ i18n.ts.silenced }}</span>
|
||||
<span v-if="moderator" class="moderator">{{ i18n.ts.moderator }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MkInfo v-if="isSystem">{{ i18n.ts.isSystemAccount }}</MkInfo>
|
||||
|
||||
<FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ i18n.ts.instanceInfo }}</FormLink>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 1em;">
|
||||
<MkKeyValue :copy="user.id" oneline>
|
||||
<template #key>ID</template>
|
||||
<template #value><span class="_monospace">{{ user.id }}</span></template>
|
||||
</MkKeyValue>
|
||||
<!-- 要る?
|
||||
<MkKeyValue v-if="ips.length > 0" :copy="user.id" oneline>
|
||||
<template #key>IP (recent)</template>
|
||||
<template #value><span class="_monospace">{{ ips[0].ip }}</span></template>
|
||||
</MkKeyValue>
|
||||
-->
|
||||
<template v-if="!isSystem">
|
||||
<MkFolder v-if="!isSystem">
|
||||
<template #icon><i class="ph-list-bullets ph-bold ph-lg"></i></template>
|
||||
<template #label>{{ i18n.ts.details }}</template>
|
||||
<div style="display: flex; flex-direction: column; gap: 1em;">
|
||||
<MkKeyValue v-if="user" :copy="user.id" oneline>
|
||||
<template #key>{{ i18n.ts.id }}</template>
|
||||
<template #value><span class="_monospace">{{ user.id }}</span></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue v-if="user" :copy="'@' + acct(user)" oneline>
|
||||
<template #key>{{ i18n.ts.username }}</template>
|
||||
<template #value><span class="_monospace">@{{ acct(user) }}</span></template>
|
||||
</MkKeyValue>
|
||||
<!-- 要る?
|
||||
<MkKeyValue v-if="ips.length > 0" :copy="user.id" oneline>
|
||||
<template #key>IP (recent)</template>
|
||||
<template #value><span class="_monospace">{{ ips[0].ip }}</span></template>
|
||||
</MkKeyValue>
|
||||
-->
|
||||
<MkKeyValue oneline>
|
||||
<template #key>{{ i18n.ts.createdAt }}</template>
|
||||
<template #value><span class="_monospace"><MkTime :time="user.createdAt" :mode="'detail'"/></span></template>
|
||||
|
@ -51,16 +62,64 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #key>{{ i18n.ts.email }}</template>
|
||||
<template #value><span class="_monospace">{{ info.email }}</span></template>
|
||||
</MkKeyValue>
|
||||
</template>
|
||||
</div>
|
||||
<MkKeyValue v-if="info" oneline>
|
||||
<template #key>{{ i18n.ts.totalFollowers }}</template>
|
||||
<template #value><span class="_monospace"><MkNumber :value="info.followStats.totalFollowers"></MkNumber></span></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue v-if="info" oneline>
|
||||
<template #key>{{ i18n.ts.totalFollowing }}</template>
|
||||
<template #value><span class="_monospace"><MkNumber :value="info.followStats.totalFollowing"></MkNumber></span></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue v-if="info" oneline>
|
||||
<template #key>{{ i18n.ts.remoteFollowers }}</template>
|
||||
<template #value><span class="_monospace"><MkNumber :value="info.followStats.remoteFollowers"></MkNumber></span></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue v-if="info" oneline>
|
||||
<template #key>{{ i18n.ts.remoteFollowing }}</template>
|
||||
<template #value><span class="_monospace"><MkNumber :value="info.followStats.remoteFollowing"></MkNumber></span></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue v-if="info" oneline>
|
||||
<template #key>{{ i18n.ts.localFollowers }}</template>
|
||||
<template #value><span class="_monospace"><MkNumber :value="info.followStats.localFollowers"></MkNumber></span></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue v-if="info" oneline>
|
||||
<template #key>{{ i18n.ts.localFollowing }}</template>
|
||||
<template #value><span class="_monospace"><MkNumber :value="info.followStats.localFollowing"></MkNumber></span></template>
|
||||
</MkKeyValue>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkTextarea v-model="moderationNote" manualSave @update:modelValue="onModerationNoteChanged">
|
||||
<MkFolder v-if="info">
|
||||
<template #icon><i class="ph-scroll ph-bold ph-lg"></i></template>
|
||||
<template #label>{{ i18n.ts._role.policies }}</template>
|
||||
<div class="_gaps">
|
||||
<div v-for="policy in Object.keys(info.policies)" :key="policy">
|
||||
{{ policy }} ... {{ info.policies[policy] }}
|
||||
</div>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="iAmAdmin && ips && ips.length > 0">
|
||||
<template #icon><i class="ph-network ph-bold ph-lg"></i></template>
|
||||
<template #label>{{ i18n.ts.ip }}</template>
|
||||
<MkInfo>{{ i18n.ts.ipTip }}</MkInfo>
|
||||
<div v-for="record in ips" :key="record.ip" class="_monospace" :class="$style.ip" style="margin: 1em 0;">
|
||||
<span class="date">{{ record.createdAt }}</span>
|
||||
<span class="ip">{{ record.ip }}</span>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="iAmModerator" :defaultOpen="moderationNote.length > 0">
|
||||
<template #icon><i class="ph-stamp ph-bold ph-lg"></i></template>
|
||||
<template #label>{{ i18n.ts.moderationNote }}</template>
|
||||
<template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
|
||||
</MkTextarea>
|
||||
<MkTextarea v-model="moderationNote" manualSave @update:modelValue="onModerationNoteChanged">
|
||||
<template #label>{{ i18n.ts.moderationNote }}</template>
|
||||
<template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
|
||||
</MkTextarea>
|
||||
</MkFolder>
|
||||
|
||||
<FormSection v-if="user.host">
|
||||
<template #label>ActivityPub</template>
|
||||
<FormSection v-if="user?.host">
|
||||
<template #label>{{ i18n.ts.activityPub }}</template>
|
||||
|
||||
<div class="_gaps_m">
|
||||
<div style="display: flex; flex-direction: column; gap: 1em;">
|
||||
|
@ -73,12 +132,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #value><MkTime mode="detail" :time="user.lastFetchedAt"/></template>
|
||||
</MkKeyValue>
|
||||
</div>
|
||||
|
||||
<MkButton @click="updateRemoteUser"><i class="ti ti-refresh"></i> {{ i18n.ts.updateRemoteUser }}</MkButton>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
<FormSection v-if="!isSystem">
|
||||
<FormSection v-if="!isSystem && user && iAmModerator">
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="silenced" @update:modelValue="toggleSilence">{{ i18n.ts.silence }}</MkSwitch>
|
||||
<MkSwitch v-if="!isSystem" v-model="suspended" @update:modelValue="toggleSuspend">{{ i18n.ts.suspend }}</MkSwitch>
|
||||
|
@ -90,58 +147,40 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #caption>{{ i18n.ts.mandatoryCWDescription }}</template>
|
||||
</MkInput>
|
||||
|
||||
<div>
|
||||
<MkButton v-if="user.host == null && !isSystem" inline style="margin-right: 8px;" @click="resetPassword"><i class="ti ti-key"></i> {{ i18n.ts.resetPassword }}</MkButton>
|
||||
<div :class="$style.buttonStrip">
|
||||
<MkButton v-if="user.host != null" inline @click="updateRemoteUser"><i class="ph-cloud-arrow-down ph-bold ph-lg"></i> {{ i18n.ts.updateRemoteUser }}</MkButton>
|
||||
<MkButton v-if="user.host == null" inline accent @click="resetPassword"><i class="ph-password ph-bold ph-lg"></i> {{ i18n.ts.resetPassword }}</MkButton>
|
||||
<MkButton inline accent @click="unsetUserAvatar"><i class="ph-camera-slash ph-bold ph-lg"></i> {{ i18n.ts.unsetUserAvatar }}</MkButton>
|
||||
<MkButton inline accent @click="unsetUserBanner"><i class="ph-image-broken ph-bold ph-lg"></i> {{ i18n.ts.unsetUserBanner }}</MkButton>
|
||||
<MkButton inline danger @click="deleteAllFiles"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.deleteAllFiles }}</MkButton>
|
||||
<MkButton v-if="iAmAdmin" inline danger @click="deleteAccount"><i class="ph-skull ph-bold ph-lg"></i> {{ i18n.ts.deleteAccount }}</MkButton>
|
||||
</div>
|
||||
|
||||
<MkFolder>
|
||||
<template #icon><i class="ph-scroll ph-bold ph-lg"></i></template>
|
||||
<template #label>{{ i18n.ts._role.policies }}</template>
|
||||
<div class="_gaps">
|
||||
<div v-for="policy in Object.keys(info.policies)" :key="policy">
|
||||
{{ policy }} ... {{ info.policies[policy] }}
|
||||
</div>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-password"></i></template>
|
||||
<template #label>IP</template>
|
||||
<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo>
|
||||
<!-- TODO translate -->
|
||||
<MkInfo v-else>The date is the IP address was first acknowledged.</MkInfo>
|
||||
<template v-if="iAmAdmin && ips">
|
||||
<div v-for="record in ips" :key="record.ip" class="_monospace" :class="$style.ip" style="margin: 1em 0;">
|
||||
<span class="date">{{ record.createdAt }}</span>
|
||||
<span class="ip">{{ record.ip }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</MkFolder>
|
||||
|
||||
<div>
|
||||
<MkButton v-if="iAmModerator" inline danger style="margin-right: 8px;" @click="unsetUserAvatar"><i class="ti ti-user-circle"></i> {{ i18n.ts.unsetUserAvatar }}</MkButton>
|
||||
<MkButton v-if="iAmModerator" inline danger style="margin-right: 8px;" @click="unsetUserBanner"><i class="ti ti-photo"></i> {{ i18n.ts.unsetUserBanner }}</MkButton>
|
||||
<MkButton v-if="iAmModerator" inline danger @click="deleteAllFiles"><i class="ph-cloud ph-bold ph-lg"></i> {{ i18n.ts.deleteAllFiles }}</MkButton>
|
||||
</div>
|
||||
<MkButton v-if="$i.isAdmin && !isSystem" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tab === 'roles'" class="_gaps">
|
||||
<MkButton v-if="user.host == null" primary rounded @click="assignRole"><i class="ti ti-plus"></i> {{ i18n.ts.assign }}</MkButton>
|
||||
<MkButton primary rounded @click="assignRole"><i class="ti ti-plus"></i> {{ i18n.ts.assign }}</MkButton>
|
||||
|
||||
<div v-for="role in info.roles" :key="role.id">
|
||||
<div :class="$style.roleItemMain">
|
||||
<MkRolePreview :class="$style.role" :role="role" :forModeration="true"/>
|
||||
<button class="_button" @click="toggleRoleItem(role)"><i class="ti ti-chevron-down"></i></button>
|
||||
<button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="unassignRole(role, $event)"><i class="ti ti-x"></i></button>
|
||||
<button class="_button" @click="toggleRoleItem(role)">
|
||||
<i v-if="!expandedRoles.includes(role.id)" class="ti ti-chevron-down"></i>
|
||||
<i v-if="expandedRoles.includes(role.id)" class="ti ti-chevron-left"></i>
|
||||
</button>
|
||||
<button v-if="role.target === 'manual' || info.roleAssigns.some(a => a.roleId === role.id)" class="_button" :class="$style.roleUnassign" @click="unassignRole(role, $event)"><i class="ti ti-x"></i></button>
|
||||
<button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button>
|
||||
</div>
|
||||
<div v-if="expandedRoles.includes(role.id)" :class="$style.roleItemSub">
|
||||
<div>Assigned: <MkTime :time="info.roleAssigns.find(a => a.roleId === role.id).createdAt" mode="detail"/></div>
|
||||
<div v-if="info.roleAssigns.find(a => a.roleId === role.id).expiresAt">Period: {{ new Date(info.roleAssigns.find(a => a.roleId === role.id).expiresAt).toLocaleString() }}</div>
|
||||
<div v-else>Period: {{ i18n.ts.indefinitely }}</div>
|
||||
<template v-if="info.roleAssigns.some(a => a.roleId === role.id)">
|
||||
<div>{{ i18n.ts.roleAssigned }}: <MkTime :time="info.roleAssigns.find(a => a.roleId === role.id).createdAt" mode="detail"/></div>
|
||||
<div v-if="info.roleAssigns.find(a => a.roleId === role.id).expiresAt">{{ i18n.ts.rolePeriod }}: {{ new Date(info.roleAssigns.find(a => a.roleId === role.id).expiresAt).toLocaleString() }}</div>
|
||||
<div v-else>{{ i18n.ts.rolePeriod }}: {{ i18n.ts.indefinitely }}</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div>{{ i18n.ts.roleAssigned }}: {{ i18n.ts.roleAutomatic }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -231,6 +270,8 @@ import { iAmAdmin, $i, iAmModerator } from '@/i.js';
|
|||
import MkRolePreview from '@/components/MkRolePreview.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkNumber from '@/components/MkNumber.vue';
|
||||
import { copyToClipboard } from '@/utility/copy-to-clipboard';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
userId: string;
|
||||
|
@ -740,4 +781,12 @@ definePage(() => ({
|
|||
border-radius: var(--MI-radius-sm);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.buttonStrip {
|
||||
margin: calc(var(--MI-margin) / 2 * -1);
|
||||
|
||||
>* {
|
||||
margin: calc(var(--MI-margin) / 2);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -11222,6 +11222,14 @@ export type operations = {
|
|||
expiresAt: string | null;
|
||||
roleId: string;
|
||||
})[];
|
||||
followStats: {
|
||||
totalFollowing: number;
|
||||
totalFollowers: number;
|
||||
localFollowing: number;
|
||||
localFollowers: number;
|
||||
remoteFollowing: number;
|
||||
remoteFollowers: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
@ -572,5 +572,20 @@ bubbleTimelineMustBeEnabled: "Note: the bubble timeline is hidden by default, an
|
|||
popularUsersGlobal: "Users popular on the global network"
|
||||
popularUsersLocal: "Users popular on {name}"
|
||||
|
||||
silenced: "Silenced"
|
||||
totalFollowers: "Total followers"
|
||||
totalFollowing: "Total following"
|
||||
localFollowers: "Local followers"
|
||||
localFollowing: "Local following"
|
||||
remoteFollowers: "Remote followers"
|
||||
remoteFollowing: "Remote following"
|
||||
updateRemoteUser: "Refresh remote data"
|
||||
activityPub: "Activity Pub"
|
||||
ip: "IP"
|
||||
ipTip: "The date is when IP address was first used."
|
||||
rolePeriod: "Period"
|
||||
roleAssigned: "Assigned"
|
||||
roleAutomatic: "automatic"
|
||||
|
||||
translationTimeoutLabel: "Translation timeout"
|
||||
translationTimeoutCaption: "Timeout in milliseconds for translation API requests."
|
||||
|
|
Loading…
Add table
Reference in a new issue