diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index bae7a1173d..01b1dfa2c4 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1266,14 +1266,19 @@ admin/views/users.vue: user-not-found: "ユーザーが見つかりません" lookup: "照会" reset-password: "パスワードをリセット" + reset-password-confirm: "パスワードをリセットしますか?" password-updated: "パスワードは現在「{password}」です" suspend: "凍結" + suspend-confirm: "凍結しますか?" suspended: "凍結しました" unsuspend: "凍結の解除" + unsuspend-confirm: "凍結を解除しますか?" unsuspended: "凍結を解除しました" verify: "公式アカウントにする" + verify-confirm: "公式アカウントにしますか?" verified: "公式アカウントにしました" unverify: "公式アカウントを解除する" + unverify-confirm: "公式アカウントを解除しますか?" unverified: "公式アカウントを解除しました" users: title: "ユーザー" diff --git a/src/client/app/admin/views/users.user.vue b/src/client/app/admin/views/users.user.vue new file mode 100644 index 0000000000..afece18e82 --- /dev/null +++ b/src/client/app/admin/views/users.user.vue @@ -0,0 +1,82 @@ +<template> +<div class="kofvwchc"> + <div> + <a :href="user | userPage(null, true)"> + <mk-avatar class="avatar" :user="user" :disable-link="true"/> + </a> + </div> + <div> + <header> + <b><mk-user-name :user="user"/></b> + <span class="username">@{{ user | acct }}</span> + <span class="is-admin" v-if="user.isAdmin">admin</span> + <span class="is-moderator" v-if="user.isModerator">moderator</span> + <span class="is-verified" v-if="user.isVerified" :title="$t('@.verified-user')"><fa icon="star"/></span> + <span class="is-suspended" v-if="user.isSuspended" :title="$t('@.suspended-user')"><fa :icon="faSnowflake"/></span> + </header> + <div> + <span>{{ $t('users.updatedAt') }}: <mk-time :time="user.updatedAt" mode="detail"/></span> + </div> + <div> + <span>{{ $t('users.createdAt') }}: <mk-time :time="user.createdAt" mode="detail"/></span> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../../i18n'; +import { faSnowflake } from '@fortawesome/free-regular-svg-icons'; + +export default Vue.extend({ + i18n: i18n('admin/views/users.vue'), + props: ['user'], + data() { + return { + faSnowflake + }; + }, +}); +</script> + +<style lang="stylus" scoped> +.kofvwchc + display flex + padding 16px 0 + border-top solid 1px var(--faceDivider) + + > div:first-child + > a + > .avatar + width 64px + height 64px + + > div:last-child + flex 1 + padding-left 16px + + @media (max-width 500px) + font-size 14px + + > header + > .username + margin-left 8px + opacity 0.7 + + > .is-admin + > .is-moderator + flex-shrink 0 + align-self center + margin 0 0 0 .5em + padding 1px 6px + font-size 80% + border-radius 3px + background var(--noteHeaderAdminBg) + color var(--noteHeaderAdminFg) + + > .is-verified + > .is-suspended + margin 0 0 0 .5em + color #4dabf7 +</style> diff --git a/src/client/app/admin/views/users.vue b/src/client/app/admin/views/users.vue index 6f0f1629f1..6b829a2f8d 100644 --- a/src/client/app/admin/views/users.vue +++ b/src/client/app/admin/views/users.vue @@ -3,20 +3,26 @@ <ui-card> <div slot="title"><fa :icon="faTerminal"/> {{ $t('operation') }}</div> <section class="fit-top"> - <ui-input v-model="target" type="text"> + <ui-input class="target" v-model="target" type="text"> <span>{{ $t('username-or-userid') }}</span> </ui-input> - <ui-button @click="resetPassword"><fa :icon="faKey"/> {{ $t('reset-password') }}</ui-button> - <ui-horizon-group> - <ui-button @click="verifyUser" :disabled="verifying"><fa :icon="faCertificate"/> {{ $t('verify') }}</ui-button> - <ui-button @click="unverifyUser" :disabled="unverifying">{{ $t('unverify') }}</ui-button> - </ui-horizon-group> - <ui-horizon-group> - <ui-button @click="suspendUser" :disabled="suspending"><fa :icon="faSnowflake"/> {{ $t('suspend') }}</ui-button> - <ui-button @click="unsuspendUser" :disabled="unsuspending">{{ $t('unsuspend') }}</ui-button> - </ui-horizon-group> <ui-button @click="showUser"><fa :icon="faSearch"/> {{ $t('lookup') }}</ui-button> - <ui-textarea v-if="user" :value="user | json5" readonly tall style="margin-top:16px;"></ui-textarea> + + <div class="user" v-if="user"> + <x-user :user='user'/> + <div class="actions"> + <ui-button @click="resetPassword"><fa :icon="faKey"/> {{ $t('reset-password') }}</ui-button> + <ui-horizon-group> + <ui-button @click="verifyUser" :disabled="verifying"><fa :icon="faCertificate"/> {{ $t('verify') }}</ui-button> + <ui-button @click="unverifyUser" :disabled="unverifying">{{ $t('unverify') }}</ui-button> + </ui-horizon-group> + <ui-horizon-group> + <ui-button @click="suspendUser" :disabled="suspending"><fa :icon="faSnowflake"/> {{ $t('suspend') }}</ui-button> + <ui-button @click="unsuspendUser" :disabled="unsuspending">{{ $t('unsuspend') }}</ui-button> + </ui-horizon-group> + <ui-textarea v-if="user" :value="user | json5" readonly tall style="margin-top:16px;"></ui-textarea> + </div> + </div> </section> </ui-card> @@ -47,29 +53,7 @@ </ui-select> </ui-horizon-group> <sequential-entrance animation="entranceFromTop" delay="25"> - <div class="kofvwchc" v-for="user in users" :key="user.id"> - <div> - <a :href="user | userPage(null, true)"> - <mk-avatar class="avatar" :user="user" :disable-link="true"/> - </a> - </div> - <div> - <header> - <b><mk-user-name :user="user"/></b> - <span class="username">@{{ user | acct }}</span> - <span class="is-admin" v-if="user.isAdmin">admin</span> - <span class="is-moderator" v-if="user.isModerator">moderator</span> - <span class="is-verified" v-if="user.isVerified" :title="$t('@.verified-user')"><fa icon="star"/></span> - <span class="is-suspended" v-if="user.isSuspended" :title="$t('@.suspended-user')"><fa :icon="faSnowflake"/></span> - </header> - <div> - <span>{{ $t('users.updatedAt') }}: <mk-time :time="user.updatedAt" mode="detail"/></span> - </div> - <div> - <span>{{ $t('users.createdAt') }}: <mk-time :time="user.createdAt" mode="detail"/></span> - </div> - </div> - </div> + <x-user v-for="user in users" :user='user' :key="user.id"/> </sequential-entrance> <ui-button v-if="existMore" @click="fetchUsers">{{ $t('@.load-more') }}</ui-button> </section> @@ -83,10 +67,13 @@ import i18n from '../../i18n'; import parseAcct from "../../../../misc/acct/parse"; import { faCertificate, faUsers, faTerminal, faSearch, faKey } from '@fortawesome/free-solid-svg-icons'; import { faSnowflake } from '@fortawesome/free-regular-svg-icons'; +import XUser from './users.user.vue'; export default Vue.extend({ i18n: i18n('admin/views/users.vue'), - + components: { + XUser + }, data() { return { user: null, @@ -131,6 +118,7 @@ export default Vue.extend({ }, methods: { + /** テキストエリアのユーザーを解決する */ async fetchUser() { try { return await this.$root.api('users/show', this.target.startsWith('@') ? parseAcct(this.target) : { userId: this.target }); @@ -149,16 +137,27 @@ export default Vue.extend({ } }, + /** テキストエリアから処理対象ユーザーを設定する */ async showUser() { + this.user = null; const user = await this.fetchUser(); this.$root.api('admin/show-user', { userId: user.id }).then(info => { this.user = info; }); + this.target = ''; + }, + + /** 処理対象ユーザーの情報を更新する */ + async refreshUser() { + this.$root.api('admin/show-user', { userId: this.user._id }).then(info => { + this.user = info; + }); }, async resetPassword() { - const user = await this.fetchUser(); - this.$root.api('admin/reset-password', { userId: user.id }).then(res => { + if (!await this.getConfirmed(this.$t('reset-password-confirm'))) return; + + this.$root.api('admin/reset-password', { userId: this.user._id }).then(res => { this.$root.dialog({ type: 'success', text: this.$t('password-updated', { password: res.password }) @@ -167,11 +166,12 @@ export default Vue.extend({ }, async verifyUser() { + if (!await this.getConfirmed(this.$t('verify-confirm'))) return; + this.verifying = true; const process = async () => { - const user = await this.fetchUser(); - await this.$root.api('admin/verify-user', { userId: user.id }); + await this.$root.api('admin/verify-user', { userId: this.user._id }); this.$root.dialog({ type: 'success', text: this.$t('verified') @@ -186,14 +186,17 @@ export default Vue.extend({ }); this.verifying = false; + + this.refreshUser(); }, async unverifyUser() { + if (!await this.getConfirmed(this.$t('unverify-confirm'))) return; + this.unverifying = true; const process = async () => { - const user = await this.fetchUser(); - await this.$root.api('admin/unverify-user', { userId: user.id }); + await this.$root.api('admin/unverify-user', { userId: this.user._id }); this.$root.dialog({ type: 'success', text: this.$t('unverified') @@ -208,14 +211,17 @@ export default Vue.extend({ }); this.unverifying = false; + + this.refreshUser(); }, async suspendUser() { + if (!await this.getConfirmed(this.$t('suspend-confirm'))) return; + this.suspending = true; const process = async () => { - const user = await this.fetchUser(); - await this.$root.api('admin/suspend-user', { userId: user.id }); + await this.$root.api('admin/suspend-user', { userId: this.user._id }); this.$root.dialog({ type: 'success', text: this.$t('suspended') @@ -230,14 +236,17 @@ export default Vue.extend({ }); this.suspending = false; + + this.refreshUser(); }, async unsuspendUser() { + if (!await this.getConfirmed(this.$t('unsuspend-confirm'))) return; + this.unsuspending = true; const process = async () => { - const user = await this.fetchUser(); - await this.$root.api('admin/unsuspend-user', { userId: user.id }); + await this.$root.api('admin/unsuspend-user', { userId: this.user._id }); this.$root.dialog({ type: 'success', text: this.$t('unsuspended') @@ -252,8 +261,21 @@ export default Vue.extend({ }); this.unsuspending = false; + + this.refreshUser(); }, + async getConfirmed(text: string): Promise<Boolean> { + const confirm = await this.$root.dialog({ + type: 'warning', + showCancelButton: true, + title: 'confirm', + text, + }); + + return !confirm.canceled; + } + fetchUsers() { this.$root.api('admin/show-users', { state: this.state, @@ -277,42 +299,12 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.kofvwchc - display flex - padding 16px 0 - border-top solid 1px var(--faceDivider) +.target + margin-bottom 16px !important - > div:first-child - > a - > .avatar - width 64px - height 64px +.user + margin-top 32px - > div:last-child - flex 1 - padding-left 16px - - @media (max-width 500px) - font-size 14px - - > header - > .username - margin-left 8px - opacity 0.7 - - > .is-admin - > .is-moderator - flex-shrink 0 - align-self center - margin 0 0 0 .5em - padding 1px 6px - font-size 80% - border-radius 3px - background var(--noteHeaderAdminBg) - color var(--noteHeaderAdminFg) - - > .is-verified - > .is-suspended - margin 0 0 0 .5em - color #4dabf7 + > .actions + margin-left 80px </style>