diff --git a/CHANGELOG.md b/CHANGELOG.md index c2ce8f4007d214382acaa4de288a63ce97210d76..0a193e2329bdfa3647d6129afd9f8a6797c18ec2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Add a dialog window with a confirmation when a remove button is clicked on the Settings page - Disable tab on the Settings page if there are no settings on this tab that can be changed in Admin FE - Settings that can't be altered in Admin FE are removed: HTTP Signatures settings, Federation publisher modules and Oban Repo +- When rendering user's profile, statuses, reports and notes check if required properties exist +- Remove ability to moderate users that don't have valid nickname ### Fixed diff --git a/src/api/__mocks__/reports.js b/src/api/__mocks__/reports.js index 5c6fe3a1265d0a1031f89fb36138a7567ffa7860..5d2e209727f2b957ad657d1cb5c0ef6a627361f3 100644 --- a/src/api/__mocks__/reports.js +++ b/src/api/__mocks__/reports.js @@ -1,14 +1,14 @@ const reports = [ - { created_at: '2019-05-21T21:35:33.000Z', account: { display_name: 'Benjamin Fame', tags: [] }, actor: {}, state: 'open', id: '2', content: 'This is a report', statuses: [] }, - { created_at: '2019-05-20T22:45:33.000Z', account: { display_name: 'Alice Pool', tags: [] }, actor: {}, state: 'resolved', id: '1', content: 'Please block this user', statuses: [] }, - { created_at: '2019-05-18T13:01:33.000Z', account: { display_name: 'Nick Keys', tags: [] }, actor: {}, state: 'closed', id: '3', content: '', statuses: [] }, - { created_at: '2019-05-21T21:35:33.000Z', account: { display_name: 'Benjamin Fame', tags: [] }, actor: {}, state: 'open', id: '5', content: 'This is a report', statuses: [] }, - { created_at: '2019-05-20T22:45:33.000Z', account: { display_name: 'Alice Pool', tags: [] }, actor: {}, state: 'resolved', id: '7', content: 'Please block this user', statuses: [ - { account: { display_name: 'Alice Pool', avatar: '' }, visibility: 'public', sensitive: false, id: '11', content: 'Hey!', url: '', created_at: '2019-05-10T21:35:33.000Z' }, - { account: { display_name: 'Alice Pool', avatar: '' }, visibility: 'unlisted', sensitive: true, id: '10', content: 'Bye!', url: '', created_at: '2019-05-10T21:00:33.000Z' } + { created_at: '2019-05-21T21:35:33.000Z', account: { nickname: 'benjamin', tags: [] }, actor: {}, state: 'open', id: '2', content: 'This is a report', statuses: [] }, + { created_at: '2019-05-20T22:45:33.000Z', account: { nickname: 'alice', tags: [] }, actor: {}, state: 'resolved', id: '1', content: 'Please block this user', statuses: [] }, + { created_at: '2019-05-18T13:01:33.000Z', account: { nickname: 'nick_keys', tags: [] }, actor: {}, state: 'closed', id: '3', content: '', statuses: [] }, + { created_at: '2019-05-21T21:35:33.000Z', account: { nickname: 'benjamin', tags: [] }, actor: {}, state: 'open', id: '5', content: 'This is a report', statuses: [] }, + { created_at: '2019-05-20T22:45:33.000Z', account: { nickname: 'alice', tags: [] }, actor: {}, state: 'resolved', id: '7', content: 'Please block this user', statuses: [ + { account: { nickname: 'alice', avatar: '' }, visibility: 'public', sensitive: false, id: '11', content: 'Hey!', url: '', created_at: '2019-05-10T21:35:33.000Z' }, + { account: { nickname: 'alice', avatar: '' }, visibility: 'unlisted', sensitive: true, id: '10', content: 'Bye!', url: '', created_at: '2019-05-10T21:00:33.000Z' } ] }, - { created_at: '2019-05-18T13:01:33.000Z', account: { display_name: 'Nick Keys', tags: [] }, actor: {}, state: 'closed', id: '6', content: '', statuses: [] }, - { created_at: '2019-05-18T13:01:33.000Z', account: { display_name: 'Nick Keys', tags: [] }, actor: {}, state: 'closed', id: '4', content: '', statuses: [] } + { created_at: '2019-05-18T13:01:33.000Z', account: { nickname: 'nick_keys', tags: [] }, actor: {}, state: 'closed', id: '6', content: '', statuses: [] }, + { created_at: '2019-05-18T13:01:33.000Z', account: { nickname: 'nick_keys', tags: [] }, actor: {}, state: 'closed', id: '4', content: '', statuses: [] } ] export async function fetchReports(filter, page, pageSize, authHost, token) { diff --git a/src/api/__mocks__/status.js b/src/api/__mocks__/status.js index 232ba69a8d441a920972e9b0ce1b8ad4277700fb..0fa2ccf5bb778518e92adf6fe117ddbd52c548a9 100644 --- a/src/api/__mocks__/status.js +++ b/src/api/__mocks__/status.js @@ -11,7 +11,7 @@ export async function fetchStatus(id, authHost, token) { account: { id: '9n1bySks25olxWrku0', avatar: 'http://localhost:4000/images/avi.png', - display_name: 'dolin', + nickname: 'dolin', tags: ['strip_media', 'sandbox', 'disable_any_subscription', 'force_nsfw'], url: 'http://localhost:4000/users/dolin' }, @@ -36,7 +36,7 @@ export async function fetchStatusesByInstance({ instance, authHost, token, pageS ? [{ 'account': { 'avatar': 'http://localhost:4000/images/avi.png', - 'display_name': 'sky', + 'nickname': 'sky', 'url': 'http://localhost:4000/users/sky' }, 'content': 'A nice young couple contacted us from Brazil to decorate their newly acquired apartment.', @@ -52,7 +52,7 @@ export async function fetchStatusesByInstance({ instance, authHost, token, pageS { 'account': { 'avatar': 'http://localhost:4000/images/avi.png', - 'display_name': 'sky', + 'nickname': 'sky', 'url': 'http://localhost:4000/users/sky' }, 'content': 'A nice young couple contacted us from Brazil to decorate their newly acquired apartment.', @@ -65,7 +65,7 @@ export async function fetchStatusesByInstance({ instance, authHost, token, pageS { 'account': { 'avatar': 'http://localhost:4000/images/avi.png', - 'display_name': 'sky', + 'nickname': 'sky', 'url': 'http://localhost:4000/users/sky' }, 'content': 'the happiest man ever', diff --git a/src/api/__mocks__/users.js b/src/api/__mocks__/users.js index bdcf404aa03e6f1f457c10ef52bf88230956255e..7d4172d63d9addd2b253242a08a3f95d444513ef 100644 --- a/src/api/__mocks__/users.js +++ b/src/api/__mocks__/users.js @@ -4,12 +4,12 @@ export let users = [ { active: false, deactivated: true, id: 'abc', nickname: 'john', local: true, external: false, roles: { admin: false, moderator: false }, tags: ['strip_media'] } ] -const userProfile = { avatar: 'avatar.jpg', display_name: 'Allis', nickname: 'allis', id: '2', tags: [], roles: { admin: true, moderator: false }, local: true, external: false } +const userProfile = { avatar: 'avatar.jpg', nickname: 'allis', id: '2', tags: [], roles: { admin: true, moderator: false }, local: true, external: false } const userStatuses = [ - { account: { id: '9n1bySks25olxWrku0', display_name: 'dolin' }, content: 'pizza makes everything better', id: '9vJOO3iFPyjNaEhJ5s', created_at: '2020-05-22T17:34:34.000Z', visibility: 'public' }, - { account: { id: '9n1bySks25olxWrku0', display_name: 'dolin' }, content: 'pizza time', id: '9vJPD5XKOdzQ0bvGLY', created_at: '2020-05-22T17:34:34.000Z', visibility: 'public' }, - { account: { id: '9n1bySks25olxWrku0', display_name: 'dolin' }, content: 'what is yout favorite pizza?', id: '9jop82OBXeFPYulVjM', created_at: '2020-05-22T17:34:34.000Z', visibility: 'public' } + { account: { id: '9n1bySks25olxWrku0', nickname: 'dolin' }, content: 'pizza makes everything better', id: '9vJOO3iFPyjNaEhJ5s', created_at: '2020-05-22T17:34:34.000Z', visibility: 'public' }, + { account: { id: '9n1bySks25olxWrku0', nickname: 'dolin' }, content: 'pizza time', id: '9vJPD5XKOdzQ0bvGLY', created_at: '2020-05-22T17:34:34.000Z', visibility: 'public' }, + { account: { id: '9n1bySks25olxWrku0', nickname: 'dolin' }, content: 'what is yout favorite pizza?', id: '9jop82OBXeFPYulVjM', created_at: '2020-05-22T17:34:34.000Z', visibility: 'public' } ] const filterUsers = (str) => { diff --git a/src/components/Status/index.vue b/src/components/Status/index.vue index ffb8f74c3514125c31a7b17c67bbd0b6578bde6a..292e8e68c596f4eec1039028f7da7631c493f3bd 100644 --- a/src/components/Status/index.vue +++ b/src/components/Status/index.vue @@ -5,16 +5,18 @@ <div class="status-account-container"> <div class="status-account"> <el-checkbox v-if="showCheckbox" class="status-checkbox" @change="handleStatusSelection(account)"/> - <router-link v-if="!account.deactivated && account.id" :to="{ name: 'UsersShow', params: { id: account.id }}" @click.native.stop> + <router-link v-if="propertyExists(account, 'id')" :to="{ name: 'UsersShow', params: { id: account.id }}" @click.native.stop> <div class="status-card-header"> - <img :src="account.avatar" class="status-avatar-img"> - <h3 class="status-account-name">{{ account.display_name }}</h3> + <img v-if="propertyExists(account, 'avatar')" :src="account.avatar" class="status-avatar-img"> + <span v-if="propertyExists(account, 'nickname')" class="status-account-name">{{ account.nickname }}</span> + <span v-else> + <span v-if="propertyExists(account, 'nickname')" class="status-account-name"> + {{ account.nickname }} + </span> + <span v-else class="status-account-name deactivated">({{ $t('users.invalidNickname') }})</span> + </span> </div> </router-link> - <span v-else> - <h3 class="status-account-name">{{ account.display_name }}</h3> - <h3 class="status-account-name deactivated"> (deactivated)</h3> - </span> </div> </div> <div class="status-actions"> @@ -228,6 +230,12 @@ export default { }, parseTimestamp(timestamp) { return moment(timestamp).format('YYYY-MM-DD HH:mm') + }, + propertyExists(account, property, _secondProperty) { + if (_secondProperty) { + return account[property] && account[_secondProperty] + } + return account[property] } } } @@ -245,6 +253,11 @@ export default { .account:hover { text-decoration: underline; } + .deactivated { + color: gray; + line-height: 28px; + vertical-align: middle; + } .image { width: 20%; img { @@ -267,7 +280,8 @@ export default { .status-account-name { display: inline-block; margin: 0; - font-size: 16px; + font-size: 15px; + font-weight: 500; } .status-body { display: flex; diff --git a/src/lang/en.js b/src/lang/en.js index 86bc614cc980d81c1ef7b8fc51df9845af4140a3..291bbde34635a595981f74f8ca5fd78642790db2 100644 --- a/src/lang/en.js +++ b/src/lang/en.js @@ -238,7 +238,11 @@ export default { unconfirmedEmail: 'User didn\'t confirm the email', confirmAccount: 'Confirm account', confirmAccounts: 'Confirm accounts', - resendConfirmation: 'Resend confirmation email' + resendConfirmation: 'Resend confirmation email', + invalidAccount: 'This account has invalid nickname and can\'t be modified', + invalidNickname: 'invalid nickname', + passwordResetTokenGenerated: 'Password reset token was generated:', + linkToResetPassword: 'You can also use this link to reset password:' }, statuses: { statuses: 'Statuses', diff --git a/src/store/modules/reports.js b/src/store/modules/reports.js index 3a836a2fe732bef9bc4e55d63cea1029a7432f1e..6bc989931363daca9b08d2a6a9b0c15a19d5f3db 100644 --- a/src/store/modules/reports.js +++ b/src/store/modules/reports.js @@ -61,9 +61,8 @@ const reports = { const optimisticNote = { user: { avatar: rootState.user.avatar, - display_name: rootState.user.name, - url: `${rootState.user.authHost}/${rootState.user.name}`, - acct: rootState.user.name + nickname: rootState.user.name, + id: rootState.user.id }, content: content, created_at: new Date().getTime() diff --git a/src/store/modules/users.js b/src/store/modules/users.js index e0645784362873a5bfb34a8998831a6689c1a582..90b37e562bd76512b9dac91c264b45ef5ee5db42 100644 --- a/src/store/modules/users.js +++ b/src/store/modules/users.js @@ -123,6 +123,10 @@ const users = { dispatch('ApplyChanges', { updatedUsers, callApiFn, userId: _userId, statusId: _statusId }) }, + ClearUsersState({ commit }) { + commit('SET_SEARCH_QUERY', '') + commit('SET_USERS_FILTERS', { local: false, external: false, active: false, deactivated: false }) + }, async ClearFilters({ commit, dispatch, state }) { commit('CLEAR_USERS_FILTERS') dispatch('SearchUsers', { query: state.searchQuery, page: 1 }) diff --git a/src/views/reports/components/NoteCard.vue b/src/views/reports/components/NoteCard.vue index 79637f1601ae5b97df1d7de2ec570e95921b911e..a2a6a427a964ad65d0698b9f77f66118c8b1164a 100644 --- a/src/views/reports/components/NoteCard.vue +++ b/src/views/reports/components/NoteCard.vue @@ -2,25 +2,14 @@ <el-card class="note-card"> <div slot="header"> <div class="note-header"> - <div class="note-actor-container"> - <div class="note-actor"> - <img :src="note.user.avatar" class="note-avatar-img"> - <h3 class="note-actor-name">{{ note.user.display_name }}</h3> - </div> - <a :href="note.user.url" target="_blank"> - @{{ note.user.display_name }} - </a> + <div class="note-actor"> + <img v-if="propertyExists(note.user, 'avatar')" :src="note.user.avatar" class="note-avatar-img"> + <span v-if="propertyExists(note.user, 'nickname')" class="note-actor-name">{{ note.user.nickname }}</span> </div> <div> - <el-popconfirm - title="Are you sure to delete this?" - confirm-button-text="Yes" - cancel-button-text="No" - @onConfirm="handleNoteDeletion(note.id, report.id)"> - <el-button slot="reference" size="mini"> - {{ $t('reports.deleteNote') }} - </el-button> - </el-popconfirm> + <el-button size="mini" @click.native="handleNoteDeletion(note.id, report.id)"> + {{ $t('reports.deleteNote') }} + </el-button> </div> </div> </div> @@ -47,11 +36,29 @@ export default { } }, methods: { + handleNoteDeletion(noteID, reportID) { + this.$confirm('Are you sure you want to delete this note?', 'Warning', { + confirmButtonText: 'OK', + cancelButtonText: 'Cancel', + type: 'warning' + }).then(() => { + this.$store.dispatch('DeleteReportNote', { noteID, reportID }) + this.$message({ + type: 'success', + message: 'Delete completed' + }) + }).catch(() => { + this.$message({ + type: 'info', + message: 'Delete canceled' + }) + }) + }, parseTimestamp(timestamp) { return moment(timestamp).format('YYYY-MM-DD HH:mm') }, - handleNoteDeletion(noteID, reportID) { - this.$store.dispatch('DeleteReportNote', { noteID, reportID }) + propertyExists(account, property) { + return account[property] } } } @@ -76,7 +83,7 @@ export default { } .note-actor-name { margin: 0; - height: 22px; + height: 28px; } .note-avatar-img { width: 15px; @@ -96,6 +103,10 @@ export default { .note-header { display: flex; justify-content: space-between; + align-items: center; + height: 28px; + font-size: 15px; + font-weight: 500; } @media only screen and (max-width:480px) { @@ -105,14 +116,15 @@ export default { .note-header { display: flex; flex-direction: column; - height: 80px; + height: 65px; } - .note-actor-container { + .note-actor { margin-bottom: 5px; } .note-header { display: flex; flex-direction: column; + align-items: flex-start; } } </style> diff --git a/src/views/reports/components/Report.vue b/src/views/reports/components/Report.vue index 565b3cb0c91007ed31918db66076ae0da868076d..cd938c9101cd0ffdb85502e6a71c48bbaae8aab6 100644 --- a/src/views/reports/components/Report.vue +++ b/src/views/reports/components/Report.vue @@ -10,9 +10,9 @@ <el-card class="report"> <div class="report-header-container"> <div class="title-container"> - <h3 v-if="accountExists(report.account, 'display_name')" class="report-title">{{ $t('reports.reportOn') }} {{ report.account.display_name }}</h3> + <h3 v-if="propertyExists(report.account, 'nickname')" class="report-title">{{ $t('reports.reportOn') }} {{ report.account.nickname }}</h3> <h3 v-else class="report-title">{{ $t('reports.report') }}</h3> - <h5 class="id">{{ $t('reports.id') }}: {{ report.id }}</h5> + <h5 v-if="propertyExists(report.account, 'id')" class="id">{{ $t('reports.id') }}: {{ report.id }}</h5> </div> <div> <el-tag :type="getStateType(report.state)" size="large" class="report-tag">{{ capitalizeFirstLetter(report.state) }}</el-tag> @@ -24,26 +24,26 @@ <el-dropdown-item v-if="report.state !== 'closed'" @click.native="changeReportState('closed', report.id)">{{ $t('reports.close') }}</el-dropdown-item> </el-dropdown-menu> </el-dropdown> - <moderate-user-dropdown :account="report.account"/> + <moderate-user-dropdown v-if="propertyExists(report.account, 'nickname')" :account="report.account"/> </div> </div> <div> <el-divider class="divider"/> <span class="report-row-key">{{ $t('reports.account') }}:</span> - <span v-if="accountExists(report.account, 'avatar') && accountExists(report.account, 'display_name')"> - <img - :src="report.account.avatar" - alt="avatar" - class="avatar-img"> - <a v-if="!report.account.deactivated" :href="report.account.url" target="_blank" class="account"> - <span>{{ report.account.display_name }}</span> - </a> - <span v-else> - {{ report.account.display_name }} - <span class="deactivated"> (deactivated)</span> + <img + v-if="propertyExists(report.account, 'avatar')" + :src="report.account.avatar" + alt="avatar" + class="avatar-img"> + <a v-if="propertyExists(report.account, 'url', 'nickname')" :href="report.account.url" target="_blank" class="account"> + <span class="report-account-name">{{ report.account.nickname }}</span> + </a> + <span v-else> + <span v-if="propertyExists(report.account, 'nickname')" class="report-account-name"> + {{ report.account.nickname }} </span> + <span v-else class="report-account-name deactivated">({{ $t('users.invalidNickname') }})</span> </span> - <span v-else class="deactivated">({{ $t('reports.notFound') }})</span> </div> <div v-if="report.content && report.content.length > 0"> <el-divider class="divider"/> @@ -54,22 +54,26 @@ <div :style="showStatuses(report.statuses) ? '' : 'margin-bottom:15px'"> <el-divider class="divider"/> <span class="report-row-key">{{ $t('reports.actor') }}:</span> - <span v-if="accountExists(report.actor, 'avatar') && accountExists(report.actor, 'display_name')"> - <img - :src="report.actor.avatar" - alt="avatar" - class="avatar-img"> - <a :href="report.actor.url" target="_blank" class="account"> - <span>{{ report.actor.display_name }}</span> - </a> + <img + v-if="propertyExists(report.actor, 'avatar')" + :src="report.actor.avatar" + alt="avatar" + class="avatar-img"> + <a v-if="propertyExists(report.actor, 'url', 'nickname')" :href="report.actor.url" target="_blank" class="account"> + <span class="report-account-name">{{ report.actor.nickname }}</span> + </a> + <span v-else> + <span v-if="propertyExists(report.actor, 'nickname')" class="report-account-name"> + {{ report.actor.nickname }} + </span> + <span v-else class="report-account-name deactivated">({{ $t('users.invalidNickname') }})</span> </span> - <span v-else class="deactivated">({{ $t('reports.notFound') }})</span> </div> <div v-if="showStatuses(report.statuses)" class="statuses"> <el-collapse> <el-collapse-item :title="getStatusesTitle(report.statuses)"> <div v-for="status in report.statuses" :key="status.id"> - <status :status="status" :account="status.account.display_name ? status.account : report.account" :show-checkbox="false" :page="currentPage"/> + <status :status="status" :account="status.account.nickname ? status.account : report.account" :show-checkbox="false" :page="currentPage"/> </div> </el-collapse-item> </el-collapse> @@ -142,9 +146,6 @@ export default { } }, methods: { - accountExists(account, key) { - return account[key] - }, changeReportState(state, id) { this.$store.dispatch('ChangeReportState', [{ state, id }]) }, @@ -177,6 +178,12 @@ export default { parseTimestamp(timestamp) { return moment(timestamp).format('L HH:mm') }, + propertyExists(account, property, _secondProperty) { + if (_secondProperty) { + return account[property] && account[_secondProperty] + } + return account[property] + }, showStatuses(statuses = []) { return statuses.length > 0 } @@ -262,6 +269,10 @@ export default { font-style: italic; color: gray; } + .report-account-name { + font-size: 15px; + font-weight: 500; + } .report-row-key { font-size: 14px; font-weight: 500; diff --git a/src/views/statuses/show.vue b/src/views/statuses/show.vue index 8ae05bcb7e13f4fa10002ad4a762ec1312927e0f..d6136129e092f839cac4ebbe63f5ee4b6c2cf391 100644 --- a/src/views/statuses/show.vue +++ b/src/views/statuses/show.vue @@ -2,13 +2,14 @@ <div v-if="!loading" class="status-show-container"> <header v-if="isDesktop || isTablet" class="user-page-header"> <div class="avatar-name-container"> - <router-link :to="{ name: 'UsersShow', params: { id: user.id }}"> + <router-link v-if="propertyExists(user, 'id')" :to="{ name: 'UsersShow', params: { id: user.id }}"> <div class="avatar-name-header"> - <el-avatar v-if="accountExists(user, 'avatar')" :src="user.avatar" size="large" /> - <h1 v-if="accountExists(user, 'display_name')">{{ user.display_name }}</h1> + <el-avatar v-if="propertyExists(user, 'avatar')" :src="user.avatar" size="large" /> + <h1 v-if="propertyExists(user, 'nickname')">{{ user.nickname }}</h1> + <h1 v-else class="invalid">({{ $t('users.invalidNickname') }})</h1> </div> </router-link> - <a v-if="accountExists(user, 'url')" :href="user.url" target="_blank" class="account"> + <a v-if="propertyExists(user, 'url')" :href="user.url" target="_blank" class="account"> <i class="el-icon-top-right" title="Open user in instance"/> </a> </div> @@ -24,8 +25,8 @@ <div v-if="isMobile" class="status-page-header-container"> <header class="user-page-header"> <div class="avatar-name-container"> - <el-avatar v-if="accountExists(user, 'avatar')" :src="user.avatar" size="large" /> - <h1 v-if="accountExists(user, 'display_name')">{{ user.display_name }}</h1> + <el-avatar v-if="propertyExists(user, 'avatar')" :src="user.avatar" size="large" /> + <h1 v-if="propertyExists(user, 'nickname')">{{ user.nickname }}</h1> </div> <reboot-button/> </header> @@ -41,7 +42,10 @@ <status :status="status" :account="user" :show-checkbox="false" :godmode="showPrivate"/> </div> <div class="recent-statuses-container-show"> - <h2 class="recent-statuses">{{ $t('userProfile.recentStatuses') }} by {{ user.display_name }}</h2> + <h2 v-if="propertyExists(user, 'nickname')" class="recent-statuses"> + {{ $t('userProfile.recentStatuses') }} by {{ user.nickname }} + </h2> + <h2 v-else class="recent-statuses">{{ $t('userProfile.recentStatuses') }}</h2> <el-checkbox v-model="showPrivate" class="show-private-statuses" @change="onTogglePrivate"> {{ $t('statuses.showPrivateStatuses') }} </el-checkbox> @@ -102,9 +106,6 @@ export default { this.$store.dispatch('FetchStatus', this.$route.params.id) }, methods: { - accountExists(account, key) { - return account[key] - }, closeResetPasswordDialog() { this.resetPasswordDialogOpen = false this.$store.dispatch('RemovePasswordToken') @@ -114,6 +115,9 @@ export default { }, openResetPasswordDialog() { this.resetPasswordDialogOpen = true + }, + propertyExists(account, property) { + return account[property] } } } @@ -134,6 +138,9 @@ export default { height: 40px; align-items: center; } +.invalid { + color: gray; +} .no-statuses { margin-left: 28px; color: #606266; @@ -176,6 +183,7 @@ export default { display: flex; justify-content: space-between; margin: 22px 15px 22px 20px; + padding: 0; align-items: center; h1 { display: inline; diff --git a/src/views/users/components/ModerationDropdown.vue b/src/views/users/components/ModerationDropdown.vue index f8cf5ee60339a1950de76875cd0d8da4225bcc27..ef5e2395ad3fc9059d812bfbeaa770d3ced5fd67 100644 --- a/src/views/users/components/ModerationDropdown.vue +++ b/src/views/users/components/ModerationDropdown.vue @@ -1,10 +1,10 @@ <template> - <el-dropdown :hide-on-click="false" size="small" trigger="click" placement="top-start"> + <el-dropdown :hide-on-click="false" size="small" trigger="click" placement="top-start" @click.native.stop> <div> - <span v-if="page === 'users'" class="el-dropdown-link"> + <el-button v-if="page === 'users'" type="text" class="el-dropdown-link"> {{ $t('users.moderation') }} <i v-if="isDesktop" class="el-icon-arrow-down el-icon--right"/> - </span> + </el-button> <el-button v-if="page === 'userPage' || page === 'statusPage'" class="moderate-user-button"> <span class="moderate-user-button-container"> <span> diff --git a/src/views/users/components/MultipleUsersMenu.vue b/src/views/users/components/MultipleUsersMenu.vue index eaf6d74271d66b390b13b6fb613ec7e313d37644..8af223fd80cf97de89c1e8745d1d30ab23936bb0 100644 --- a/src/views/users/components/MultipleUsersMenu.vue +++ b/src/views/users/components/MultipleUsersMenu.vue @@ -165,33 +165,33 @@ export default { } return { grantRight: (right) => () => { - const filterUsersFn = user => user.local && !user.roles[right] && this.$store.state.user.id !== user.id + const filterUsersFn = user => this.isLocalUser(user) && !user.roles[right] && this.$store.state.user.id !== user.id const addRightFn = async(users) => await this.$store.dispatch('AddRight', { users, right }) const filtered = this.selectedUsers.filter(filterUsersFn) applyAction(filtered, addRightFn) }, revokeRight: (right) => () => { - const filterUsersFn = user => user.local && user.roles[right] && this.$store.state.user.id !== user.id + const filterUsersFn = user => this.isLocalUser(user) && user.roles[right] && this.$store.state.user.id !== user.id const deleteRightFn = async(users) => await this.$store.dispatch('DeleteRight', { users, right }) const filtered = this.selectedUsers.filter(filterUsersFn) applyAction(filtered, deleteRightFn) }, activate: () => { - const filtered = this.selectedUsers.filter(user => user.deactivated && this.$store.state.user.id !== user.id) + const filtered = this.selectedUsers.filter(user => user.nickname && user.deactivated && this.$store.state.user.id !== user.id) const activateUsersFn = async(users) => await this.$store.dispatch('ActivateUsers', { users }) applyAction(filtered, activateUsersFn) }, deactivate: () => { - const filtered = this.selectedUsers.filter(user => !user.deactivated && this.$store.state.user.id !== user.id) + const filtered = this.selectedUsers.filter(user => user.nickname && !user.deactivated && this.$store.state.user.id !== user.id) const deactivateUsersFn = async(users) => await this.$store.dispatch('DeactivateUsers', { users }) applyAction(filtered, deactivateUsersFn) }, remove: () => { - const filtered = this.selectedUsers.filter(user => this.$store.state.user.id !== user.id) + const filtered = this.selectedUsers.filter(user => user.nickname && this.$store.state.user.id !== user.id) const deleteAccountFn = async(users) => await this.$store.dispatch('DeleteUsers', { users }) applyAction(filtered, deleteAccountFn) @@ -199,40 +199,43 @@ export default { addTag: (tag) => () => { const filtered = this.selectedUsers.filter(user => tag === 'disable_remote_subscription' || tag === 'disable_any_subscription' - ? user.local && !user.tags.includes(tag) - : !user.tags.includes(tag)) + ? this.isLocalUser(user) && !user.tags.includes(tag) + : user.nickname && !user.tags.includes(tag)) const addTagFn = async(users) => await this.$store.dispatch('AddTag', { users, tag }) applyAction(filtered, addTagFn) }, removeTag: (tag) => async() => { const filtered = this.selectedUsers.filter(user => tag === 'disable_remote_subscription' || tag === 'disable_any_subscription' - ? user.local && user.tags.includes(tag) - : user.tags.includes(tag)) + ? this.isLocalUser(user) && user.tags.includes(tag) + : user.nickname && user.tags.includes(tag)) const removeTagFn = async(users) => await this.$store.dispatch('RemoveTag', { users, tag }) applyAction(filtered, removeTagFn) }, requirePasswordReset: () => { - const filtered = this.selectedUsers.filter(user => user.local) + const filtered = this.selectedUsers.filter(user => this.isLocalUser(user)) const requirePasswordResetFn = async(users) => await this.$store.dispatch('RequirePasswordReset', users) applyAction(filtered, requirePasswordResetFn) }, confirmAccounts: () => { - const filtered = this.selectedUsers.filter(user => user.local && user.confirmation_pending) + const filtered = this.selectedUsers.filter(user => this.isLocalUser(user) && user.confirmation_pending) const confirmAccountFn = async(users) => await this.$store.dispatch('ConfirmUsersEmail', { users }) applyAction(filtered, confirmAccountFn) }, resendConfirmation: () => { - const filtered = this.selectedUsers.filter(user => user.local && user.confirmation_pending) + const filtered = this.selectedUsers.filter(user => this.isLocalUser(user) && user.confirmation_pending) const resendConfirmationFn = async(users) => await this.$store.dispatch('ResendConfirmationEmail', users) applyAction(filtered, resendConfirmationFn) } } }, + isLocalUser(user) { + return user.nickname && user.local + }, grantRightToMultipleUsers(right) { const { grantRight } = this.mappers() this.confirmMessage( diff --git a/src/views/users/components/ResetPasswordDialog.vue b/src/views/users/components/ResetPasswordDialog.vue index b70f289da75ca62106a625f22f9a931ced795c12..f47886cc6ea1dbfeda3495e16ea51efa75cbd41f 100644 --- a/src/views/users/components/ResetPasswordDialog.vue +++ b/src/views/users/components/ResetPasswordDialog.vue @@ -6,8 +6,8 @@ custom-class="password-reset-token-dialog" @close="closeResetPasswordDialog"> <div> - <p class="password-reset-token">Password reset token was generated: {{ passwordResetToken }}</p> - <p>You can also use this link to reset password: + <p class="password-reset-token">{{ $t('users.passwordResetTokenGenerated') }} {{ passwordResetToken }}</p> + <p>{{ $t('users.linkToResetPassword') }} <a :href="passwordResetLink" target="_blank" class="reset-password-link">{{ passwordResetLink }}</a> </p> </div> diff --git a/src/views/users/index.vue b/src/views/users/index.vue index 4c28f9b8fa340a94feedf1310c326fb9c75bae45..58fad28c5db480c3a564b00a9a0d50e217e4616c 100644 --- a/src/views/users/index.vue +++ b/src/views/users/index.vue @@ -37,6 +37,7 @@ :data="users" row-key="id" style="width: 100%" + @row-click="handleRowClick($event)" @selection-change="handleSelectionChange"> <el-table-column v-if="isDesktop" @@ -47,7 +48,7 @@ <el-table-column :min-width="width" :label="$t('users.id')" prop="id" /> <el-table-column :label="$t('users.name')" prop="nickname"> <template slot-scope="scope"> - <router-link :to="{ name: 'UsersShow', params: { id: scope.row.id }}">{{ scope.row.nickname }}</router-link> + {{ scope.row.nickname }} <el-tag v-if="isDesktop" type="info" size="mini"> <span>{{ scope.row.local ? $t('users.local') : $t('users.external') }}</span> </el-tag> @@ -75,9 +76,14 @@ <el-table-column :label="$t('users.actions')" fixed="right"> <template slot-scope="scope"> <moderation-dropdown + v-if="propertyExists(scope.row, 'nickname')" :user="scope.row" :page="'users'" @open-reset-token-dialog="openResetPasswordDialog"/> + <el-button v-else type="text" disabled> + {{ $t('users.moderation') }} + <i v-if="isDesktop" class="el-icon-arrow-down el-icon--right"/> + </el-button> </template> </el-table-column> </el-table> @@ -132,12 +138,6 @@ export default { normalizedUsersCount() { return numeral(this.$store.state.users.totalUsersCount).format('0a') }, - users() { - return this.$store.state.users.fetchedUsers - }, - usersCount() { - return this.$store.state.users.totalUsersCount - }, pageSize() { return this.$store.state.users.pageSize }, @@ -150,6 +150,12 @@ export default { isMobile() { return this.$store.state.app.device === 'mobile' }, + users() { + return this.$store.state.users.fetchedUsers + }, + usersCount() { + return this.$store.state.users.totalUsersCount + }, width() { return this.isMobile ? 55 : false } @@ -163,6 +169,9 @@ export default { this.$store.dispatch('NeedReboot') this.$store.dispatch('FetchUsers', { page: 1 }) }, + destroyed() { + this.$store.dispatch('ClearUsersState') + }, methods: { activationIcon(status) { return status ? 'el-icon-error' : 'el-icon-success' @@ -189,12 +198,20 @@ export default { this.$store.dispatch('SearchUsers', { query: searchQuery, page }) } }, + handleRowClick(row) { + if (row.id) { + this.$router.push({ name: 'UsersShow', params: { id: row.id }}) + } + }, handleSelectionChange(value) { this.$data.selectedUsers = value }, openResetPasswordDialog() { this.resetPasswordDialogOpen = true }, + propertyExists(account, property) { + return account[property] + }, showDeactivatedButton(id) { return this.$store.state.user.id !== id } @@ -253,6 +270,9 @@ export default { margin: 10px 0 0 15px; height: 40px; } + .el-table__row:hover { + cursor: pointer; + } .pagination { margin: 25px 0; text-align: center; diff --git a/src/views/users/show.vue b/src/views/users/show.vue index 1a9d6b871fc54a4736de44d87016eeb0c6b3cea8..424096b96727c80cf832ed3b48fa17dad9f31de3 100644 --- a/src/views/users/show.vue +++ b/src/views/users/show.vue @@ -2,11 +2,13 @@ <main v-if="!userProfileLoading"> <header v-if="isDesktop || isTablet" class="user-page-header"> <div class="avatar-name-container"> - <el-avatar :src="user.avatar" size="large" /> - <h1>{{ user.display_name }}</h1> + <el-avatar v-if="propertyExists(user, 'avatar')" :src="user.avatar" size="large" /> + <h1 v-if="propertyExists(user, 'nickname')">{{ user.nickname }}</h1> + <h1 v-else class="invalid">({{ $t('users.invalidNickname') }})</h1> </div> <div class="left-header-container"> <moderation-dropdown + v-if="propertyExists(user, 'nickname')" :user="user" :page="'userPage'" @open-reset-token-dialog="openResetPasswordDialog"/> @@ -16,12 +18,14 @@ <div v-if="isMobile" class="user-page-header-container"> <header class="user-page-header"> <div class="avatar-name-container"> - <el-avatar :src="user.avatar" size="large" /> - <h1>{{ user.display_name }}</h1> + <el-avatar v-if="propertyExists(user, 'avatar')" :src="user.avatar" size="large" /> + <h1 v-if="propertyExists(user, 'nickname')">{{ user.nickname }}</h1> + <h1 v-else class="invalid">({{ $t('users.invalidNickname') }})</h1> </div> <reboot-button/> </header> <moderation-dropdown + v-if="propertyExists(user, 'nickname')" :user="user" :page="'userPage'" @open-reset-token-dialog="openResetPasswordDialog"/> @@ -32,14 +36,11 @@ <div class="user-profile-container"> <el-card class="user-profile-card"> <div class="el-table el-table--fit el-table--enable-row-hover el-table--enable-row-transition el-table--medium"> + <el-tag v-if="!propertyExists(user, 'nickname')" type="info" class="invalid-user-tag"> + {{ $t('users.invalidAccount') }} + </el-tag> <table class="user-profile-table"> <tbody> - <tr class="el-table__row"> - <td>{{ $t('userProfile.nickname') }}</td> - <td> - {{ user.nickname }} - </td> - </tr> <tr class="el-table__row"> <td class="name-col">ID</td> <td class="value-col"> @@ -49,8 +50,8 @@ <tr class="el-table__row"> <td>{{ $t('userProfile.tags') }}</td> <td> - <el-tag v-for="tag in user.tags" :key="tag" class="user-profile-tag">{{ humanizeTag(tag) }}</el-tag> - <span v-if="user.tags.length === 0">—</span> + <span v-if="user.tags.length === 0 || !propertyExists(user, 'tags')">—</span> + <el-tag v-for="tag in user.tags" v-else :key="tag" class="user-profile-tag">{{ humanizeTag(tag) }}</el-tag> </td> </tr> <tr class="el-table__row"> @@ -62,7 +63,7 @@ <el-tag v-if="user.roles.moderator" class="user-profile-tag"> {{ $t('users.moderator') }} </el-tag> - <span v-if="!user.roles.moderator && !user.roles.admin">—</span> + <span v-if="!propertyExists(user, 'roles') || (!user.roles.moderator && !user.roles.admin)">—</span> </td> </tr> <tr class="el-table__row"> @@ -82,10 +83,11 @@ </tbody> </table> </div> - <el-button icon="el-icon-lock" class="security-setting-button" @click="securitySettingsModalVisible = true"> + <el-button v-if="propertyExists(user, 'nickname')" icon="el-icon-lock" class="security-setting-button" @click="securitySettingsModalVisible = true"> {{ $t('userProfile.securitySettings.securitySettings') }} </el-button> <SecuritySettingsModal + v-if="propertyExists(user, 'nickname')" :user="user" :visible="securitySettingsModalVisible" @close="securitySettingsModalVisible = false" /> @@ -178,6 +180,9 @@ export default { }, openResetPasswordDialog() { this.resetPasswordDialogOpen = true + }, + propertyExists(account, property) { + return account[property] } } } @@ -203,6 +208,9 @@ table { display: flex; align-items: center; } +.invalid { + color: gray; +} .el-table--border::after, .el-table--group::after, .el-table::before { background-color: transparent; } @@ -212,6 +220,14 @@ table { width: 100%; } } +.invalid-user-tag { + font-size: 14px; + width: inherit; + height: auto; + text-align: center; + word-wrap: break-word; + white-space: normal; +} .left-header-container { align-items: center; display: flex; @@ -270,7 +286,8 @@ table { .user-page-header { display: flex; justify-content: space-between; - padding: 0 15px 0 20px; + margin: 22px 15px 22px 20px; + padding: 0; align-items: center; h1 { display: inline @@ -286,6 +303,7 @@ table { } .user-profile-table { margin: 0; + width: inherit; } .user-profile-tag { margin: 0 4px 4px 0; diff --git a/test/views/statuses/index.test.js b/test/views/statuses/index.test.js index e6173883085a1c07b28572fa9542df92897c4739..62921650cb3b32d604a9895808b397dc8e8da589 100644 --- a/test/views/statuses/index.test.js +++ b/test/views/statuses/index.test.js @@ -73,7 +73,7 @@ describe('Statuses', () => { await flushPromises() expect(wrapper.vm.selectedUsers.length).toEqual(1) - expect(wrapper.vm.selectedUsers[0].display_name).toBe('sky') + expect(wrapper.vm.selectedUsers[0].nickname).toBe('sky') done() }) diff --git a/test/views/statuses/show.test.js b/test/views/statuses/show.test.js index 6bf4fdffc70c8bd7ffd737a1cc322c625f9b983e..66da07124f30ceccef2e50fc9742757fcfcfa88e 100644 --- a/test/views/statuses/show.test.js +++ b/test/views/statuses/show.test.js @@ -45,7 +45,7 @@ describe('Status show page', () => { expect(wrapper.find('.status-container').isVisible()).toBe(true) expect(store.state.status.fetchedStatus.id).toBe('9vJOO3iFPyjNaEhJ5s') - expect(store.state.status.fetchedStatus.account.display_name).toBe('dolin') + expect(store.state.status.fetchedStatus.account.nickname).toBe('dolin') expect(store.state.userProfile.statuses.length).toEqual(3) done() }) @@ -81,7 +81,7 @@ describe('Status show page', () => { await flushPromises() expect(wrapper.find('.status-card').exists()).toBe(true) - expect(wrapper.find('router-link-stub h3').text()).toBe('dolin') + expect(wrapper.find('router-link-stub span.status-account-name').text()).toBe('dolin') expect(wrapper.find('span.el-tag').text()).not.toBe('Sensitive') expect(wrapper.find('span.el-tag').text()).toBe('Public') expect(wrapper.find('button.status-actions-button').exists()).toBe(true)