Merge branch 'develop' into feature/add_settings_search

This commit is contained in:
Angelina Filippova 2020-03-14 21:44:01 +03:00
commit 41e94628cb
36 changed files with 655 additions and 543 deletions

View file

@ -6,31 +6,40 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased
### Changed
### Added
- **breaking** PleromaFE login feature relies on `admin` scope presence in PleromaFE token (older versions of PleromaFE don't support it)
- Moves emoji pack configuration from the main menu to settings tab, redesigns it and fixes bugs
- `mailerEnabled` must be set to `true` in order to require password reset (password reset currently only works via email)
- Remove fetching initial data for configuring server settings
- Actions in users module (ActivateUsers, AddRight, DeactivateUsers, DeleteRight, DeleteUsers) now accept an array of users instead of one user
- Leave dropdown menu open after clicking an action
- Move current try/catch error handling from view files to module, add it where necessary
- Display checkboxes in status card and fetch statuses only when status card was rendered from Statuses by instance page
- Move statuses by instance state from local state to store state
- Pass user's ID to actions that moderate users when action is called from user's profile page
- Ability to see local statuses in Statuses by instance section
- Ability to configure Oban.Cron settings and settings for notifications streamer
### Fixed
- Fix parsing tuples in Pleroma.Upload.Filter.Mogrify and Pleroma.Emails.Mailer settings
## [2.0] - 2020-02-27
### Added
- Optimistic update for actions in users module and fetching users after api function finished its execution
- Relay management
- Ability to fetch all statuses from a given instance
- Grouped reports: now you can view reports, which are grouped by status (pagination is not implemented yet, though)
- Ability to confirm users' emails and resend confirmation emails
- Report notes
- Ability to moderate users on the statuses page
- Ability to moderate user on the user's page
- Ability to remove setting's updated value and set it back to initial value
- Ability to restart an application when settings that require instance reboot were changed
- Mobile UI for Settings tab
- Mobile and Tablet UI for all sections
### Changed
- **breaking** PleromaFE login feature relies on `admin` scope presence in PleromaFE token (older versions of PleromaFE don't support it)
- `mailerEnabled` must be set to `true` in order to require password reset (password reset currently only works via email)
- Render inputs for configuring settings based on description that comes from the BE
- Remove fetching initial data for configuring server settings
- Actions in users module (ActivateUsers, AddRight, DeactivateUsers, DeleteRight, DeleteUsers) now accept an array of users instead of one user
- Leave dropdown menu open after clicking an action
- Display checkboxes in status card and fetch statuses only when status card was rendered from Statuses by instance page
- Move statuses by instance state from local state to store state
### Fixed
@ -39,6 +48,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Remove duplicated success message
- Fix styles for Statuses by instance page
- Fix styles for Reports section
- Fix listing remote emoji
## [1.2.0] - 2019-09-27

View file

@ -11,40 +11,12 @@ const reports = [
{ created_at: '2019-05-18T13:01:33.000Z', account: { acct: 'nick', display_name: 'Nick Keys', tags: [] }, actor: { acct: 'admin' }, state: 'closed', id: '4', content: '', statuses: [] }
]
const groupedReports = [
{ account: { avatar: 'http://localhost:4000/images/avi.png', confirmation_pending: false, deactivated: false, display_name: 'leo', id: '9oG0YghgBi94EATI9I', local: true, nickname: 'leo', roles: { admin: false, moderator: false }, tags: [] },
actors: [{ acct: 'admin', avatar: 'http://localhost:4000/images/avi.png', deactivated: false, display_name: 'admin', id: '9oFz4pTauG0cnJ581w', local: true, nickname: 'admin', roles: { admin: false, moderator: false }, tags: [], url: 'http://localhost:4000/users/admin', username: 'admin' }],
date: '2019-11-23T12:56:11.969772Z',
reports: [
{ created_at: '2019-05-21T21:35:33.000Z', account: { acct: 'benj', display_name: 'Benjamin Fame', tags: [] }, actor: { acct: 'admin' }, state: 'open', id: '2', content: 'This is a report', statuses: [] },
{ created_at: '2019-05-20T22:45:33.000Z', account: { acct: 'alice', display_name: 'Alice Pool', tags: [] }, actor: { acct: 'admin2' }, 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' }
] }
],
status: {
account: { acct: 'leo' },
content: 'At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis',
created_at: '2019-11-23T12:55:20.000Z',
id: '9pFoQO69piu7cUDnJg',
url: 'http://localhost:4000/notice/9pFoQO69piu7cUDnJg',
visibility: 'unlisted',
sensitive: true
},
status_deleted: false
}
]
export async function fetchReports(filter, page, pageSize, authHost, token) {
return filter.length > 0
? Promise.resolve({ data: { reports: reports.filter(report => report.state === filter) }})
: Promise.resolve({ data: { reports }})
}
export async function fetchGroupedReports(authHost, token) {
return Promise.resolve({ data: { reports: groupedReports }})
}
export async function changeState(reportsData, authHost, token) {
return Promise.resolve({ data: '' })
}

View file

@ -24,15 +24,6 @@ export async function fetchReports(filter, page, pageSize, authHost, token) {
})
}
export async function fetchGroupedReports(authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/grouped_reports`,
method: 'get',
headers: authHeaders(token)
})
}
export async function createNote(content, reportID, authHost, token) {
return await request({
baseURL: baseName(authHost),

View file

@ -21,6 +21,15 @@ export async function deleteStatus(id, authHost, token) {
})
}
export async function fetchStatuses({ godmode, localOnly, authHost, token, pageSize, page }) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/statuses?godmode=${godmode}&local_only=${localOnly}&page=${page}&page_size=${pageSize}`,
method: 'get',
headers: authHeaders(token)
})
}
export async function fetchStatusesByInstance({ instance, authHost, token, pageSize, page }) {
return await request({
baseURL: baseName(authHost),

View file

@ -267,9 +267,8 @@ export default {
font-style: italic;
}
}
@media
only screen and (max-width: 760px),
(min-device-width: 768px) and (max-device-width: 1024px) {
@media only screen and (max-width:480px) {
.el-message {
min-width: 80%;
}

View file

@ -193,7 +193,7 @@ export default {
deleteAccount: 'Delete Account',
deleteAccounts: 'Delete Accounts',
forceNsfw: 'Force posts to be NSFW',
stripMedia: 'Force posts not to have media',
stripMedia: 'Force posts to not have media',
forceUnlisted: 'Force posts to be unlisted',
sandbox: 'Force posts to be followers-only',
disableRemoteSubscription: 'Disallow following user from remote instances',
@ -244,7 +244,9 @@ export default {
statuses: 'Statuses by instance',
instanceFilter: 'Instance filter',
loadMore: 'Load more',
noInstances: 'No other instances found'
noInstances: 'No other instances found',
onlyLocalStatuses: 'Show only local statuses',
showPrivateStatuses: 'Show private statuses'
},
userProfile: {
tags: 'Tags',
@ -254,8 +256,7 @@ export default {
external: 'external',
localUppercase: 'Local',
nickname: 'Nickname',
recentStatuses: 'Recent Statues',
showPrivateStatuses: 'Show private statuses',
recentStatuses: 'Recent Statuses',
roles: 'Roles',
activeUppercase: 'Active',
active: 'active',
@ -273,7 +274,6 @@ export default {
},
reports: {
reports: 'Reports',
groupedReports: 'Grouped reports',
reply: 'Reply',
from: 'From',
showNotes: 'Show notes',

View file

@ -17,8 +17,6 @@ const getters = {
errorLogs: state => state.errorLog.logs,
users: state => state.users.fetchedUsers,
authHost: state => state.user.authHost,
settings: state => state.settings,
instances: state => state.peers.fetchedPeers,
statuses: state => state.status.fetchedStatuses
settings: state => state.settings
}
export default getters

View file

@ -63,6 +63,9 @@ export const parseNonTuples = (key, value) => {
return updated
}
if (key === ':args') {
if (typeof value === 'string') {
return [value]
}
const index = value.findIndex(el => typeof el === 'object' && el.tuple.includes('implode'))
const updated = value.map((el, i) => i === index ? 'implode' : el)
return updated
@ -80,10 +83,15 @@ export const parseTuples = (tuples, key) => {
accum[item.tuple[0]] = item.tuple[1].reduce((acc, mascot) => {
return [...acc, { [mascot.tuple[0]]: { ...mascot.tuple[1], id: `f${(~~(Math.random() * 1e8)).toString(16)}` }}]
}, [])
} else if (item.tuple[0] === ':groups' || item.tuple[0] === ':replace' || item.tuple[0] === ':retries') {
} else if (Array.isArray(item.tuple[1]) &&
(item.tuple[0] === ':groups' || item.tuple[0] === ':replace' || item.tuple[0] === ':retries')) {
accum[item.tuple[0]] = item.tuple[1].reduce((acc, group) => {
return [...acc, { [group.tuple[0]]: { value: group.tuple[1], id: `f${(~~(Math.random() * 1e8)).toString(16)}` }}]
}, [])
} else if (item.tuple[0] === ':crontab') {
accum[item.tuple[0]] = item.tuple[1].reduce((acc, group) => {
return { ...acc, [group.tuple[1]]: group.tuple[0] }
}, {})
} else if (item.tuple[0] === ':match_actor') {
accum[item.tuple[0]] = Object.keys(item.tuple[1]).reduce((acc, regex) => {
return [...acc, { [regex]: { value: item.tuple[1][regex], id: `f${(~~(Math.random() * 1e8)).toString(16)}` }}]
@ -218,7 +226,12 @@ export const wrapUpdatedSettings = (group, settings, currentState) => {
const wrapValues = (settings, currentState) => {
return Object.keys(settings).map(setting => {
const [type, value] = settings[setting]
if (type === 'keyword' || type.includes('keyword') || setting === ':replace') {
if (
type === 'keyword' ||
type.includes('keyword') ||
type.includes('tuple') && type.includes('list') ||
setting === ':replace'
) {
return { 'tuple': [setting, wrapValues(value, currentState)] }
} else if (type === 'atom' && value.length > 0) {
return { 'tuple': [setting, `:${value}`] }
@ -226,8 +239,8 @@ const wrapValues = (settings, currentState) => {
return typeof value === 'string'
? { 'tuple': [setting, value] }
: { 'tuple': [setting, { 'tuple': value }] }
} else if (type.includes('tuple') && type.includes('list')) {
return { 'tuple': [setting, value] }
} else if (type === 'reversed_tuple') {
return { 'tuple': [value, setting] }
} else if (type === 'map') {
const mapValue = Object.keys(value).reduce((acc, key) => {
acc[key] = setting === ':match_actor' ? value[key] : value[key][1]

View file

@ -1,13 +1,11 @@
import { changeState, fetchReports, fetchGroupedReports, createNote, deleteNote } from '@/api/reports'
import { changeState, fetchReports, createNote, deleteNote } from '@/api/reports'
const reports = {
state: {
fetchedReports: [],
fetchedGroupedReports: [],
totalReportsCount: 0,
currentPage: 1,
pageSize: 50,
groupReports: false,
stateFilter: '',
loading: true
},
@ -24,17 +22,11 @@ const reports = {
SET_REPORTS: (state, reports) => {
state.fetchedReports = reports
},
SET_GROUPED_REPORTS: (state, reports) => {
state.fetchedGroupedReports = reports
},
SET_REPORTS_COUNT: (state, total) => {
state.totalReportsCount = total
},
SET_REPORTS_FILTER: (state, filter) => {
state.stateFilter = filter
},
SET_REPORTS_GROUPING: (state) => {
state.groupReports = !state.groupReports
}
},
actions: {
@ -46,14 +38,7 @@ const reports = {
return updatedReportsIds.includes(report.id) ? { ...report, state: reportsData[0].state } : report
})
const updatedGroupedReports = state.fetchedGroupedReports.map(group => {
const updatedReportsIds = reportsData.map(({ id }) => id)
const updatedReports = group.reports.map(report => updatedReportsIds.includes(report.id) ? { ...report, state: reportsData[0].state } : report)
return { ...group, reports: updatedReports }
})
commit('SET_REPORTS', updatedReports)
commit('SET_GROUPED_REPORTS', updatedGroupedReports)
},
ClearFetchedReports({ commit }) {
commit('SET_REPORTS', [])
@ -67,19 +52,9 @@ const reports = {
commit('SET_PAGE', page)
commit('SET_LOADING', false)
},
async FetchGroupedReports({ commit, getters }) {
commit('SET_LOADING', true)
const { data } = await fetchGroupedReports(getters.authHost, getters.token)
commit('SET_GROUPED_REPORTS', data.reports)
commit('SET_LOADING', false)
},
SetFilter({ commit }, filter) {
commit('SET_REPORTS_FILTER', filter)
},
ToggleReportsGrouping({ commit }) {
commit('SET_REPORTS_GROUPING')
},
CreateReportNote({ commit, getters, state, rootState }, { content, reportID }) {
createNote(content, reportID, getters.authHost, getters.token)

View file

@ -1,4 +1,4 @@
import { changeStatusScope, deleteStatus, fetchStatusesByInstance } from '@/api/status'
import { changeStatusScope, deleteStatus, fetchStatuses, fetchStatusesByInstance } from '@/api/status'
const status = {
state: {
@ -6,11 +6,21 @@ const status = {
loading: false,
statusesByInstance: {
selectedInstance: '',
showLocal: false,
showPrivate: false,
page: 1,
pageSize: 30
pageSize: 20,
buttonLoading: false,
allLoaded: false
}
},
mutations: {
CHANGE_GODMODE_CHECKBOX_VALUE: (state, value) => {
state.statusesByInstance.showPrivate = value
},
CHANGE_LOCAL_CHECKBOX_VALUE: (state, value) => {
state.statusesByInstance.showLocal = value
},
CHANGE_PAGE: (state, page) => {
state.statusesByInstance.page = page
},
@ -23,6 +33,12 @@ const status = {
PUSH_STATUSES: (state, statuses) => {
state.fetchedStatuses = [...state.fetchedStatuses, ...statuses]
},
SET_ALL_LOADED: (state, status) => {
state.statusesByInstance.allLoaded = status
},
SET_BUTTON_LOADING: (state, status) => {
state.statusesByInstance.buttonLoading = status
},
SET_LOADING: (state, status) => {
state.loading = status
}
@ -36,8 +52,6 @@ const status = {
dispatch('FetchUserStatuses', { userId, godmode })
} else if (fetchStatusesByInstance) { // called from Statuses by Instance
dispatch('FetchStatusesByInstance')
} else { // called from GroupedReports
dispatch('FetchGroupedReports')
}
},
async DeleteStatus({ dispatch, getters }, { statusId, reportCurrentPage, userId, godmode, fetchStatusesByInstance }) {
@ -48,14 +62,50 @@ const status = {
dispatch('FetchUserStatuses', { userId, godmode })
} else if (fetchStatusesByInstance) { // called from Statuses by Instance
dispatch('FetchStatusesByInstance')
} else { // called from GroupedReports
dispatch('FetchGroupedReports')
}
},
async FetchStatusesByInstance({ commit, getters, state }) {
async FetchStatusesByInstance({ commit, getters, state, rootState }) {
commit('SET_LOADING', true)
const statuses = state.statusesByInstance.selectedInstance === ''
? { data: [] }
if (state.statusesByInstance.selectedInstance === '') {
commit('SET_STATUSES_BY_INSTANCE', [])
} else {
const statuses = state.statusesByInstance.selectedInstance === rootState.user.authHost
? await fetchStatuses(
{
godmode: state.statusesByInstance.showPrivate,
localOnly: state.statusesByInstance.showLocal,
authHost: getters.authHost,
token: getters.token,
pageSize: state.statusesByInstance.pageSize,
page: state.statusesByInstance.page
})
: await fetchStatusesByInstance(
{
instance: state.statusesByInstance.selectedInstance,
authHost: getters.authHost,
token: getters.token,
pageSize: state.statusesByInstance.pageSize,
page: state.statusesByInstance.page
})
commit('SET_STATUSES_BY_INSTANCE', statuses.data)
if (statuses.data.length < state.statusesByInstance.pageSize) {
commit('SET_ALL_LOADED', true)
}
}
commit('SET_LOADING', false)
},
async FetchStatusesPageByInstance({ commit, getters, rootState, state }) {
commit('SET_BUTTON_LOADING', true)
const statuses = state.statusesByInstance.selectedInstance === rootState.user.authHost
? await fetchStatuses(
{
godmode: state.statusesByInstance.showPrivate,
localOnly: state.statusesByInstance.showLocal,
authHost: getters.authHost,
token: getters.token,
pageSize: state.statusesByInstance.pageSize,
page: state.statusesByInstance.page
})
: await fetchStatusesByInstance(
{
instance: state.statusesByInstance.selectedInstance,
@ -64,26 +114,29 @@ const status = {
pageSize: state.statusesByInstance.pageSize,
page: state.statusesByInstance.page
})
commit('SET_STATUSES_BY_INSTANCE', statuses.data)
commit('SET_LOADING', false)
},
async FetchStatusesPageByInstance({ commit, getters, state }) {
commit('SET_LOADING', true)
const statuses = await fetchStatusesByInstance(
{
instance: state.statusesByInstance.selectedInstance,
authHost: getters.authHost,
token: getters.token,
pageSize: state.statusesByInstance.pageSize,
page: state.statusesByInstance.page
})
commit('PUSH_STATUSES', statuses.data)
commit('SET_LOADING', false)
commit('SET_BUTTON_LOADING', false)
if (statuses.data.length < state.statusesByInstance.pageSize) {
commit('SET_ALL_LOADED', true)
}
},
HandleGodmodeCheckboxChange({ commit, dispatch }, value) {
dispatch('HandlePageChange', 1)
commit('SET_ALL_LOADED', false)
commit('CHANGE_GODMODE_CHECKBOX_VALUE', value)
dispatch('FetchStatusesByInstance')
},
HandleLocalCheckboxChange({ commit, dispatch }, value) {
dispatch('HandlePageChange', 1)
commit('SET_ALL_LOADED', false)
commit('CHANGE_LOCAL_CHECKBOX_VALUE', value)
dispatch('FetchStatusesByInstance')
},
HandleFilterChange({ commit }, instance) {
commit('CHANGE_SELECTED_INSTANCE', instance)
commit('SET_ALL_LOADED', false)
},
HandlePageChange({ commit }, page) {
commit('CHANGE_PAGE', page)

View file

@ -44,6 +44,9 @@ export default {
isDesktop() {
return this.$store.state.app.device === 'desktop'
},
isMobile() {
return this.$store.state.app.device === 'mobile'
},
shortcodePresent() {
return this.shortcode.trim() === ''
}

View file

@ -95,6 +95,9 @@ export default {
isDesktop() {
return this.$store.state.app.device === 'desktop'
},
isMobile() {
return this.$store.state.app.device === 'mobile'
},
localPacks() {
return this.$store.state.emojiPacks.localPacks
},

View file

@ -85,7 +85,7 @@
sortable/>
<el-table-column
:label="$t('invites.token')"
:min-width="isDesktop ? 350 : 125"
:min-width="isDesktop ? 320 : 120"
prop="token"/>
<el-table-column
v-if="isDesktop"
@ -119,7 +119,9 @@
<template slot-scope="scope">
<el-tag
:type="scope.row.used ? 'danger' : 'success'"
disable-transitions>{{ scope.row.used ? $t('invites.used') : $t('invites.active') }}</el-tag>
disable-transitions>
{{ scope.row.used ? $t('invites.used') : $t('invites.active') }}
</el-tag>
</template>
</el-table-column>
<el-table-column
@ -268,9 +270,8 @@ export default {
margin: 0 0 10px 0;
}
}
@media
only screen and (max-width: 760px),
(min-device-width: 768px) and (max-device-width: 1024px) {
@media only screen and (max-width:480px) {
.invites-container {
.actions-container {
display: flex;
@ -279,6 +280,9 @@ only screen and (max-width: 760px),
align-items: center;
margin: 15px 10px 7px 10px;
}
.cell {
padding: 0;
}
.create-invite-token {
width: 100%;
}
@ -296,7 +300,9 @@ only screen and (max-width: 760px),
}
.invite-token-table {
width: 100%;
margin: 0;
margin: 0 5px;
font-size: 12px;
font-weight: 500;
}
.invite-via-email {
width: 100%;
@ -308,6 +314,11 @@ only screen and (max-width: 760px),
.info {
margin: 0 0 10px 5px;
}
th {
.cell {
padding: 0;
}
}
}
.create-invite-token {
width: 100%

View file

@ -1,47 +1,40 @@
<template>
<div v-if="!loading" class="moderation-log-container">
<h1>{{ $t('moderationLog.moderationLog') }}</h1>
<el-row type="flex" class="row-bg" justify="space-between">
<el-col :span="9">
<el-select
v-model="user"
class="user-select"
clearable
placeholder="Filter by admin/moderator"
@change="fetchLogWithFilters">
<el-option-group
v-for="group in users"
:key="group.label"
:label="group.label">
<el-option
v-for="item in group.options"
:key="item.id"
:label="item.nickname"
:value="item.id" />
</el-option-group>
</el-select>
</el-col>
<el-col :span="6" class="search-container">
<el-input
v-model="search"
placeholder="Search logs"
prefix-icon="el-icon-search"
clearable
@input="handleDebounceSearchInput" />
</el-col>
</el-row>
<el-row type="flex" class="row-bg" justify="space-between">
<el-col :span="9" class="date-container">
<el-date-picker
:default-time="['00:00:00', '23:59:59']"
v-model="dateRange"
type="daterange"
start-placeholder="Start date"
end-placeholder="End date"
unlink-panels
@change="fetchLogWithFilters" />
</el-col>
</el-row>
<div class="moderation-log-nav-container">
<el-select
v-model="user"
class="moderation-log-user-select"
clearable
placeholder="Filter by admin/moderator"
@change="fetchLogWithFilters">
<el-option-group
v-for="group in users"
:key="group.label"
:label="group.label">
<el-option
v-for="item in group.options"
:key="item.id"
:label="item.nickname"
:value="item.id"/>
</el-option-group>
</el-select>
<el-input
v-model="search"
placeholder="Search logs"
clearable
class="moderation-log-search"
@input="handleDebounceSearchInput"/>
</div>
<el-date-picker
:default-time="['00:00:00', '23:59:59']"
v-model="dateRange"
type="daterange"
start-placeholder="Start date"
end-placeholder="End date"
unlink-panels
class="moderation-log-date-panel"
@change="fetchLogWithFilters" />
<el-timeline>
<el-timeline-item
v-for="(logEntry, index) in log"
@ -56,6 +49,7 @@
:hide-on-single-page="true"
:page-size="50"
:total="total"
:small="isMobile"
layout="prev, pager, next"
@current-change="fetchLogWithFilters" />
</div>
@ -77,6 +71,9 @@ export default {
}
},
computed: {
isMobile() {
return this.$store.state.app.device === 'mobile'
},
loading() {
return this.$store.state.moderationLog.logLoading &&
this.$store.state.moderationLog.adminsLoading
@ -139,7 +136,17 @@ h1 {
margin: 25px 45px 0 0;
padding: 0px;
}
.user-select {
.moderation-log-date-panel {
width: 350px;
}
.moderation-log-nav-container {
display: flex;
justify-content: space-between;
}
.moderation-log-search {
width: 350px;
}
.moderation-log-user-select {
margin: 0 0 20px;
width: 350px;
}
@ -149,4 +156,30 @@ h1 {
.pagination {
text-align: center;
}
@media only screen and (max-width:480px) {
.moderation-log-date-panel {
width: 100%;
}
.moderation-log-user-select {
margin: 0 0 10px;
width: 55%;
}
.moderation-log-search {
width: 40%;
}
}
@media only screen and (max-width:801px) and (min-width: 481px) {
.moderation-log-date-panel {
width: 55%;
}
.moderation-log-user-select {
margin: 0 0 10px;
width: 55%;
}
.moderation-log-search {
width: 40%;
}
}
</style>

View file

@ -1,159 +0,0 @@
<template>
<el-timeline class="reports-timeline">
<el-timeline-item
v-for="groupedReport in groupedReports"
:key="groupedReport.id"
:timestamp="parseTimestamp(groupedReport.date)"
placement="top"
class="timeline-item-container">
<el-card class="grouped-report">
<div class="header-container">
<div>
<h3 class="report-title">{{ $t('reports.reportsOn') }} {{ groupedReport.account.display_name }}</h3>
</div>
<div>
<el-dropdown trigger="click">
<el-button plain size="small" icon="el-icon-edit" class="report-actions-button">{{ $t('reports.changeAllReports') }}<i class="el-icon-arrow-down el-icon--right"/></el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="changeAllReports('resolved', groupedReport.reports)">{{ $t('reports.resolveAll') }}</el-dropdown-item>
<el-dropdown-item @click.native="changeAllReports('open', groupedReport.reports)">{{ $t('reports.reopenAll') }}</el-dropdown-item>
<el-dropdown-item @click.native="changeAllReports('closed', groupedReport.reports)">{{ $t('reports.closeAll') }}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<moderate-user-dropdown :account="groupedReport.account"/>
</div>
</div>
<div>
<el-divider class="divider"/>
<span class="report-row-key">{{ $t('reports.account') }}:</span>
<img
:src="groupedReport.account.avatar"
alt="avatar"
class="avatar-img">
<a :href="groupedReport.account.url" target="_blank">
<span>{{ groupedReport.account.nickname }}</span>
</a>
</div>
<div>
<el-divider class="divider"/>
<span class="report-row-key">{{ $t('reports.actors') }}:</span>
<span v-for="(actor, index) in groupedReport.actors" :key="actor.id">
<a :href="actor.url" target="_blank">
{{ actor.acct }}<span v-if="index < groupedReport.actors.length - 1">, </span>
</a>
</span>
</div>
<div v-if="groupedReport.status">
<el-divider class="divider"/>
<span class="report-row-key">{{ $t('reports.reportedStatus') }}:</span>
<status :status="groupedReport.status" :show-checkbox="false" class="reported-status"/>
</div>
<div v-if="groupedReport.reports">
<el-collapse>
<el-collapse-item :title="$t('reports.reports')">
<report-card :reports="groupedReport.reports"/>
</el-collapse-item>
</el-collapse>
</div>
</el-card>
</el-timeline-item>
</el-timeline>
</template>
<script>
import moment from 'moment'
import ModerateUserDropdown from './ModerateUserDropdown'
import ReportCard from './ReportCard'
import Status from '@/components/Status'
export default {
name: 'Report',
components: { ModerateUserDropdown, ReportCard, Status },
props: {
groupedReports: {
type: Array,
required: true
}
},
methods: {
changeAllReports(reportState, groupOfReports) {
const reportsData = groupOfReports.map(report => {
return { id: report.id, state: reportState }
})
this.$store.dispatch('ChangeReportState', reportsData)
},
parseTimestamp(timestamp) {
return moment(timestamp).format('L HH:mm')
}
}
}
</script>
<style rel='stylesheet/scss' lang='scss'>
a {
text-decoration: underline;
}
.avatar-img {
vertical-align: bottom;
width: 15px;
height: 15px;
margin-left: 5px;
}
.el-card__body {
padding: 17px;
}
.el-card__header {
background-color: #FAFAFA;
padding: 10px 20px;
}
.el-icon-arrow-right {
margin-right: 6px;
}
.grouped-report {
.header-container {
display: flex;
justify-content: space-between;
align-items: baseline;
height: 36px;
}
}
.line {
width: 100%;
height: 0;
border: 0.5px solid #EBEEF5;
margin: 15px 0 15px;
}
.report-title {
margin: 0;
}
.report-row-key {
font-size: 14px;
font-weight: 500;
}
.reports-timeline {
margin: 30px 45px 45px 19px;
padding: 0px;
}
.reported-status {
margin-top: 15px;
}
@media
only screen and (max-width: 760px),
(min-device-width: 768px) and (max-device-width: 1024px) {
.grouped-report {
.header-container {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
height: auto;
}
.report-actions-button {
margin: 3px 0 6px;
}
.report-title {
margin-bottom: 7px;
}
}
}
</style>

View file

@ -97,9 +97,8 @@ export default {
display: flex;
justify-content: space-between;
}
@media
only screen and (max-width: 760px),
(min-device-width: 768px) and (max-device-width: 1024px) {
@media only screen and (max-width:480px) {
.el-card__header {
padding: 10px 17px;
}

View file

@ -74,7 +74,7 @@
v-model="notes[report.id]"
:placeholder="$t('reports.leaveNote')"
type="textarea"
rows="3"/>
rows="2"/>
<div class="report-post-note">
<el-button @click="handleNewNote(report.id)">{{ $t('reports.postNote') }}</el-button>
</div>
@ -180,6 +180,9 @@ export default {
height: 15px;
margin-left: 5px;
}
.divider {
margin: 15px 0;
}
.el-card__body {
padding: 17px;
}
@ -279,9 +282,8 @@ export default {
font-style: italic;
color: gray;
}
@media
only screen and (max-width: 760px),
(min-device-width: 768px) and (max-device-width: 1024px) {
@media only screen and (max-width:480px) {
.report {
.header-container {
display: flex;
@ -303,5 +305,11 @@ export default {
margin-bottom: 7px;
}
}
.reports-timeline {
margin: 20px 10px;
.el-timeline-item__wrapper {
padding-left: 20px;
}
}
}
</style>

View file

@ -108,9 +108,8 @@ export default {
display: flex;
justify-content: space-between;
}
@media
only screen and (max-width: 760px),
(min-device-width: 768px) and (max-device-width: 1024px) {
@media only screen and (max-width:480px) {
.el-card__header {
padding: 10px 17px;
}

View file

@ -54,12 +54,17 @@ export default {
.select-field {
width: 350px;
}
@media
only screen and (max-width: 760px),
(min-device-width: 768px) and (max-device-width: 1024px) {
@media only screen and (max-width:480px) {
.select-field {
width: 100%;
margin-bottom: 5px;
}
}
@media only screen and (max-width:801px) and (min-width: 481px) {
.select-field {
width: 50%;
}
}
</style>

View file

@ -1,22 +1,14 @@
<template>
<div class="reports-container">
<h1 v-if="groupReports">
{{ $t('reports.groupedReports') }}
<span class="report-count">({{ normalizedReportsCount }})</span>
</h1>
<h1 v-else>
<h1>
{{ $t('reports.reports') }}
<span class="report-count">({{ normalizedReportsCount }})</span>
</h1>
<div class="reports-filter-container">
<reports-filter v-if="!groupReports"/>
<el-checkbox v-model="groupReports" class="group-reports-checkbox">
Group reports by statuses
</el-checkbox>
<reports-filter/>
</div>
<div class="block">
<grouped-report v-loading="loading" v-if="groupReports" :grouped-reports="groupedReports"/>
<report v-loading="loading" v-else :reports="reports"/>
<report v-loading="loading" :reports="reports"/>
<div v-if="reports.length === 0" class="no-reports-message">
<p>There are no reports to display</p>
</div>
@ -25,32 +17,18 @@
</template>
<script>
import GroupedReport from './components/GroupedReport'
import numeral from 'numeral'
import Report from './components/Report'
import ReportsFilter from './components/ReportsFilter'
export default {
components: { GroupedReport, Report, ReportsFilter },
components: { Report, ReportsFilter },
computed: {
groupedReports() {
return this.$store.state.reports.fetchedGroupedReports
},
groupReports: {
get() {
return this.$store.state.reports.groupReports
},
set() {
this.toggleReportsGrouping()
}
},
loading() {
return this.$store.state.reports.loading
},
normalizedReportsCount() {
return this.groupReports
? numeral(this.$store.state.reports.fetchedGroupedReports.length).format('0a')
: numeral(this.$store.state.reports.totalReportsCount).format('0a')
return numeral(this.$store.state.reports.totalReportsCount).format('0a')
},
reports() {
return this.$store.state.reports.fetchedReports
@ -58,12 +36,6 @@ export default {
},
mounted() {
this.$store.dispatch('FetchReports', 1)
this.$store.dispatch('FetchGroupedReports')
},
methods: {
toggleReportsGrouping() {
this.$store.dispatch('ToggleReportsGrouping')
}
}
}
</script>
@ -77,9 +49,6 @@ export default {
margin: 22px 15px 22px 15px;
padding-bottom: 0
}
.group-reports-checkbox {
margin-top: 10px;
}
h1 {
margin: 22px 0 0 15px;
}
@ -92,9 +61,8 @@ export default {
font-size: 28px;
}
}
@media
only screen and (max-width: 760px),
(min-device-width: 768px) and (max-device-width: 1024px) {
@media only screen and (max-width:480px) {
.reports-container {
h1 {
margin: 7px 10px 15px 10px;
@ -103,8 +71,5 @@ only screen and (max-width: 760px),
margin: 0 10px;
}
}
#app > div > div.main-container > section > div > div.block > ul {
margin: 45px 45px 5px 19px;
}
}
</style>

View file

@ -3,6 +3,10 @@
<el-form ref="frontendData" :model="frontendData" :label-width="labelWidth">
<setting :setting-group="frontend" :data="frontendData"/>
</el-form>
<el-form ref="staticFeData" :model="staticFeData" :label-width="labelWidth">
<setting :setting-group="staticFe" :data="staticFeData"/>
</el-form>
<el-divider class="divider thick-line"/>
<el-form ref="assetsData" :model="assetsData" :label-width="labelWidth">
<el-form-item class="grouped-settings-header">
<span class="label-font">{{ $t('settings.assets') }}</span>
@ -70,12 +74,6 @@ export default {
frontendData() {
return _.get(this.settings.settings, [':pleroma', ':frontend_configurations']) || {}
},
markup() {
return this.settings.description.find(setting => setting.key === ':markup')
},
markupData() {
return _.get(this.settings.settings, [':pleroma', ':markup']) || {}
},
isMobile() {
return this.$store.state.app.device === 'mobile'
},
@ -93,6 +91,18 @@ export default {
},
loading() {
return this.settings.loading
},
markup() {
return this.settings.description.find(setting => setting.key === ':markup')
},
markupData() {
return _.get(this.settings.settings, [':pleroma', ':markup']) || {}
},
staticFe() {
return this.settings.description.find(setting => setting.key === ':static_fe')
},
staticFeData() {
return _.get(this.settings.settings, [':pleroma', ':static_fe']) || {}
}
},
methods: {

View file

@ -3,10 +3,6 @@
<el-form ref="httpData" :model="httpData" :label-width="labelWidth">
<setting :setting-group="http" :data="httpData"/>
</el-form>
<el-form ref="teslaAdapter" :model="teslaAdapterData" :label-width="labelWidth">
<setting :setting-group="teslaAdapter" :data="teslaAdapterData"/>
</el-form>
<el-divider class="divider thick-line"/>
<el-form ref="corsPlugData" :model="corsPlugData" :label-width="labelWidth">
<el-form-item class="grouped-settings-header">
<span class="label-font">{{ $t('settings.corsPlug') }}</span>
@ -86,12 +82,6 @@ export default {
loading() {
return this.settings.loading
},
teslaAdapter() {
return this.settings.description.find(setting => setting.group === ':tesla')
},
teslaAdapterData() {
return _.get(this.settings.settings, [':tesla']) || {}
},
webCacheTtl() {
return this.settings.description.find(setting => setting.key === ':web_cache_ttl')
},

View file

@ -95,11 +95,12 @@
</el-input>
<!-- special inputs -->
<auto-linker-input v-if="settingGroup.group === ':auto_linker'" :data="data" :setting-group="settingGroup" :setting="setting"/>
<mascots-input v-if="setting.key === ':mascots'" :data="keywordData" :setting-group="settingGroup" :setting="setting"/>
<crontab-input v-if="setting.key === ':crontab'" :data="data[setting.key]" :setting-group="settingGroup" :setting="setting"/>
<editable-keyword-input v-if="editableKeyword(setting.key, setting.type)" :data="keywordData" :setting-group="settingGroup" :setting="setting"/>
<icons-input v-if="setting.key === ':icons'" :data="iconsData" :setting-group="settingGroup" :setting="setting"/>
<proxy-url-input v-if="setting.key === ':proxy_url'" :data="data[setting.key]" :setting-group="settingGroup" :setting="setting" :parents="settingParent"/>
<mascots-input v-if="setting.key === ':mascots'" :data="keywordData" :setting-group="settingGroup" :setting="setting"/>
<multiple-select v-if="setting.key === ':backends' || setting.key === ':args'" :data="data" :setting-group="settingGroup" :setting="setting"/>
<proxy-url-input v-if="setting.key === ':proxy_url'" :data="data[setting.key]" :setting-group="settingGroup" :setting="setting" :parents="settingParent"/>
<prune-input v-if="setting.key === ':prune'" :data="data[setting.key]" :setting-group="settingGroup" :setting="setting"/>
<rate-limit-input v-if="settingGroup.key === ':rate_limit'" :data="data" :setting-group="settingGroup" :setting="setting"/>
<!-------------------->
@ -117,7 +118,7 @@
<script>
import i18n from '@/lang'
import { AutoLinkerInput, EditableKeywordInput, IconsInput, MascotsInput, MultipleSelect, ProxyUrlInput, PruneInput, RateLimitInput } from './inputComponents'
import { AutoLinkerInput, CrontabInput, EditableKeywordInput, IconsInput, MascotsInput, MultipleSelect, ProxyUrlInput, PruneInput, RateLimitInput } from './inputComponents'
import { processNested } from '@/store/modules/normalizers'
import _ from 'lodash'
import marked from 'marked'
@ -126,6 +127,7 @@ export default {
name: 'Inputs',
components: {
AutoLinkerInput,
CrontabInput,
EditableKeywordInput,
IconsInput,
MascotsInput,
@ -198,7 +200,7 @@ export default {
return Array.isArray(this.data[':icons']) ? this.data[':icons'] : []
},
inputValue() {
if ([':esshd', ':cors_plug', ':quack', ':http_signatures', ':tesla'].includes(this.settingGroup.group) &&
if ([':esshd', ':cors_plug', ':quack', ':http_signatures', ':tesla', ':swoosh'].includes(this.settingGroup.group) &&
this.data[this.setting.key]) {
return this.setting.type === 'atom' && this.data[this.setting.key].value[0] === ':'
? this.data[this.setting.key].value.substr(1)
@ -249,8 +251,8 @@ export default {
methods: {
editableKeyword(key, type) {
return key === ':replace' ||
(Array.isArray(type) && type.includes('keyword') && type.includes('integer')) ||
type === 'map' ||
(Array.isArray(type) && type.includes('keyword') && type.includes('integer')) ||
(Array.isArray(type) && type.includes('keyword') && type.findIndex(el => el.includes('list') && el.includes('string')) !== -1)
},
getFormattedDescription(desc) {

View file

@ -23,6 +23,17 @@
<el-form ref="pleromaUser" :model="pleromaUserData" :label-width="labelWidth">
<setting :setting-group="pleromaUser" :data="pleromaUserData"/>
</el-form>
<el-divider class="divider thick-line"/>
<el-form ref="uriSchemes" :model="uriSchemesData" :label-width="labelWidth">
<setting :setting-group="uriSchemes" :data="uriSchemesData"/>
</el-form>
<el-divider class="divider thick-line"/>
<el-form ref="feed" :model="feedData" :label-width="labelWidth">
<setting :setting-group="feed" :data="feedData"/>
</el-form>
<el-form ref="streamer" :model="streamerData" :label-width="labelWidth">
<setting :setting-group="streamer" :data="streamerData"/>
</el-form>
<div class="submit-button-container">
<el-button class="submit-button" type="primary" @click="onSubmit">Submit</el-button>
</div>
@ -50,6 +61,12 @@ export default {
adminTokenData() {
return _.get(this.settings.settings, [':pleroma', ':admin_token']) || {}
},
feed() {
return this.settings.description.find(setting => setting.key === ':feed')
},
feedData() {
return _.get(this.settings.settings, [':pleroma', ':feed']) || {}
},
fetchInitialPosts() {
return this.settings.description.find(setting => setting.key === ':fetch_initial_posts')
},
@ -97,6 +114,18 @@ export default {
},
scheduledActivityData() {
return _.get(this.settings.settings, [':pleroma', 'Pleroma.ScheduledActivity']) || {}
},
streamer() {
return this.$store.state.settings.description.find(setting => setting.key === ':streamer')
},
streamerData() {
return _.get(this.settings.settings, [':pleroma', ':streamer']) || {}
},
uriSchemes() {
return this.settings.description.find(setting => setting.key === ':uri_schemes')
},
uriSchemesData() {
return _.get(this.settings.settings, [':pleroma', ':uri_schemes']) || {}
}
},
methods: {

View file

@ -29,6 +29,14 @@
<el-form ref="mrfVocabulary" :model="mrfVocabularyData" :label-width="labelWidth">
<setting :setting-group="mrfVocabulary" :data="mrfVocabularyData"/>
</el-form>
<el-divider class="divider thick-line"/>
<el-form ref="mrfObjectAge" :model="mrfObjectAgeData" :label-width="labelWidth">
<setting :setting-group="mrfObjectAge" :data="mrfObjectAgeData"/>
</el-form>
<el-divider class="divider thick-line"/>
<el-form ref="modules" :model="modulesData" :label-width="labelWidth">
<setting :setting-group="modules" :data="modulesData"/>
</el-form>
<div class="submit-button-container">
<el-button class="submit-button" type="primary" @click="onSubmit">Submit</el-button>
</div>
@ -66,6 +74,12 @@ export default {
loading() {
return this.settings.loading
},
modules() {
return this.settings.description.find(setting => setting.key === ':modules')
},
modulesData() {
return _.get(this.settings.settings, [':pleroma', ':modules']) || {}
},
mrfSimple() {
return this.settings.description.find(setting => setting.key === ':mrf_simple')
},
@ -90,6 +104,12 @@ export default {
mrfKeywordData() {
return _.get(this.settings.settings, [':pleroma', ':mrf_keyword']) || {}
},
mrfObjectAge() {
return this.settings.description.find(setting => setting.key === ':mrf_object_age')
},
mrfObjectAgeData() {
return _.get(this.settings.settings, [':pleroma', ':mrf_object_age']) || {}
},
mrfSubchain() {
return this.settings.description.find(setting => setting.key === ':mrf_subchain')
},

View file

@ -4,6 +4,10 @@
<setting :setting-group="mailer" :data="mailerData"/>
</el-form>
<el-divider class="divider thick-line"/>
<el-form ref="swoosh" :model="swooshData" :label-width="labelWidth">
<setting :setting-group="swoosh" :data="swooshData"/>
</el-form>
<el-divider class="divider thick-line"/>
<el-form ref="emailNotifications" :model="emailNotificationsData" :label-width="labelWidth">
<setting :setting-group="emailNotifications" :data="emailNotificationsData"/>
</el-form>
@ -61,6 +65,12 @@ export default {
mailerData() {
return _.get(this.settings.settings, [':pleroma', 'Pleroma.Emails.Mailer']) || {}
},
swoosh() {
return this.settings.description.find(setting => setting.group === ':swoosh')
},
swooshData() {
return _.get(this.settings.settings, [':swoosh']) || {}
},
userEmail() {
return this.settings.description.find(setting => setting.key === 'Pleroma.Emails.UserEmail')
},

View file

@ -0,0 +1,86 @@
<template>
<el-form :label-width="labelWidth" :label-position="isMobile ? 'top' : 'right'" class="crontab">
<el-form-item v-for="worker in workers" :key="worker" :label="worker" class="crontab-container">
<el-input
:value="data[worker]"
:placeholder="getSuggestion(worker) || null"
class="input setting-input"
@input="update($event, worker)"/>
</el-form-item>
</el-form>
</template>
<script>
export default {
name: 'CrontabInput',
props: {
data: {
type: Object,
default: function() {
return {}
}
},
setting: {
type: Object,
default: function() {
return {}
}
},
settingGroup: {
type: Object,
default: function() {
return {}
}
}
},
computed: {
isDesktop() {
return this.$store.state.app.device === 'desktop'
},
isMobile() {
return this.$store.state.app.device === 'mobile'
},
isTablet() {
return this.$store.state.app.device === 'tablet'
},
labelWidth() {
if (this.isMobile) {
return '100%'
} else {
return '380px'
}
},
workers() {
return this.setting.suggestions.map(worker => worker[1])
}
},
methods: {
getSuggestion(worker) {
return this.setting.suggestions.find(suggestion => suggestion[1] === worker)[0]
},
update(value, worker) {
const currentValue = this.$store.state.settings.settings[this.settingGroup.group][this.settingGroup.key][this.setting.key]
const updatedValue = { ...currentValue, [worker]: value }
const updatedValueWithType = Object.keys(currentValue).reduce((acc, key) => {
if (key === worker) {
return { ...acc, [key]: ['reversed_tuple', value] }
} else {
return { ...acc, [key]: ['reversed_tuple', currentValue[key]] }
}
}, {})
this.$store.dispatch('UpdateSettings',
{ group: this.settingGroup.group, key: this.settingGroup.key, input: this.setting.key, value: updatedValueWithType, type: this.setting.type }
)
this.$store.dispatch('UpdateState',
{ group: this.settingGroup.group, key: this.settingGroup.key, input: this.setting.key, value: updatedValue }
)
}
}
}
</script>
<style rel='stylesheet/scss' lang='scss'>
@import '../../styles/main';
@include settings
</style>

View file

@ -96,15 +96,18 @@ export default {
this.updateSetting(updatedValue, this.settingGroup.group, this.settingGroup.key, this.setting.key, this.setting.type)
},
updateSetting(value, group, key, input, type) {
const updatedSettings = type !== 'map'
? value.reduce((acc, element) => {
return { ...acc, [Object.keys(element)[0]]: ['list', Object.values(element)[0].value] }
}, {})
: value.reduce((acc, element) => {
return { ...acc, [Object.keys(element)[0]]: Object.values(element)[0].value }
}, {})
const updatedSettings = this.wrapUpdatedSettings(value, input, type)
this.$store.dispatch('UpdateSettings', { group, key, input, value: updatedSettings, type })
this.$store.dispatch('UpdateState', { group, key, input, value })
},
wrapUpdatedSettings(value, input, type) {
return type === 'map'
? value.reduce((acc, element) => {
return { ...acc, [Object.keys(element)[0]]: Object.values(element)[0].value }
}, {})
: value.reduce((acc, element) => {
return { ...acc, [Object.keys(element)[0]]: ['list', Object.values(element)[0].value] }
}, {})
}
}
}

View file

@ -1,5 +1,6 @@
export { default as AutoLinkerInput } from './AutoLinkerInput'
export { default as EditableKeywordInput } from './EditableKeywordInput'
export { default as CrontabInput } from './CrontabInput'
export { default as IconsInput } from './IconsInput'
export { default as MascotsInput } from './MascotsInput'
export { default as MultipleSelect } from './MultipleSelect'

View file

@ -339,6 +339,12 @@
}
@media only screen and (max-width:480px) {
.crontab {
width: 100%;
label {
width: 100%;
}
}
.delete-setting-button {
margin: 4px 0 0 5px;
height: 28px;
@ -382,6 +388,10 @@
margin: 0;
padding: 0 15px 10px 0;
}
.el-form-item.crontab-container:first-child {
margin: 0;
padding: 0 ;
}
.el-form-item:first-child .mascot-form-item {
padding: 0;
}

View file

@ -22,6 +22,15 @@
:selected-users="selectedUsers"
@apply-action="clearSelection"/>
</div>
<div v-if="currentInstance" class="checkbox-container">
<el-checkbox v-model="showLocal" class="show-private-statuses">
{{ $t('statuses.onlyLocalStatuses') }}
</el-checkbox>
<el-checkbox v-model="showPrivate" class="show-private-statuses">
{{ $t('statuses.showPrivateStatuses') }}
</el-checkbox>
</div>
<p v-if="statuses.length === 0" class="no-statuses">{{ $t('userProfile.noStatuses') }}</p>
<div v-for="status in statuses" :key="status.id" class="status-container">
<status
:status="status"
@ -30,13 +39,13 @@
@status-selection="handleStatusSelection" />
</div>
<div v-if="statuses.length > 0" class="statuses-pagination">
<el-button @click="handleLoadMore">{{ $t('statuses.loadMore') }}</el-button>
<el-button v-if="!allLoaded" :loading="buttonLoading" @click="handleLoadMore">{{ $t('statuses.loadMore') }}</el-button>
<el-button v-else icon="el-icon-check" circle/>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import MultipleUsersMenu from '@/views/users/components/MultipleUsersMenu'
import Status from '@/components/Status'
@ -52,10 +61,18 @@ export default {
}
},
computed: {
...mapGetters([
'instances',
'statuses'
]),
allLoaded() {
return this.$store.state.status.statusesByInstance.allLoaded
},
buttonLoading() {
return this.$store.state.status.statusesByInstance.buttonLoading
},
currentInstance() {
return this.selectedInstance === this.$store.state.user.authHost
},
instances() {
return [this.$store.state.user.authHost, ...this.$store.state.peers.fetchedPeers]
},
isDesktop() {
return this.$store.state.app.device === 'desktop'
},
@ -75,6 +92,25 @@ export default {
set(instance) {
this.$store.dispatch('HandleFilterChange', instance)
}
},
showLocal: {
get() {
return this.$store.state.status.statusesByInstance.showLocal
},
set(value) {
this.$store.dispatch('HandleLocalCheckboxChange', value)
}
},
showPrivate: {
get() {
return this.$store.state.status.statusesByInstance.showPrivate
},
set(value) {
this.$store.dispatch('HandleGodmodeCheckboxChange', value)
}
},
statuses() {
return this.$store.state.status.fetchedStatuses
}
},
mounted() {
@ -97,7 +133,6 @@ export default {
if (this.selectedUsers.find(selectedUser => user.id === selectedUser.id) !== undefined) {
return
}
this.selectedUsers = [...this.selectedUsers, user]
}
}
@ -111,6 +146,9 @@ export default {
margin: 0 0 10px;
}
}
.checkbox-container {
margin-bottom: 15px;
}
.filter-container {
display: flex;
height: 36px;
@ -129,19 +167,22 @@ h1 {
margin: 22px 0 0 0;
}
@media
only screen and (max-width: 760px),
(min-device-width: 768px) and (max-device-width: 1024px) {
@media only screen and (max-width:480px) {
.checkbox-container {
margin-bottom: 10px;
}
.filter-container {
display: flex;
height: 36px;
flex-direction: column;
margin: 10px 10px
margin: 10px 0;
}
.select-field {
width: 100%;
margin-bottom: 5px;
}
.select-instance {
width: 100%;
}
}
</style>

View file

@ -139,9 +139,8 @@ export default {
.create-account-form-item-without-margin {
margin-bottom: 0px;
}
@media
only screen and (max-width: 760px),
(min-device-width: 768px) and (max-device-width: 1024px) {
@media only screen and (max-width:480px) {
.create-user-dialog {
width: 85%
}

View file

@ -61,12 +61,17 @@ export default {
.select-field {
width: 350px;
}
@media
only screen and (max-width: 760px),
(min-device-width: 768px) and (max-device-width: 1024px) {
@media only screen and (max-width:480px) {
.select-field {
width: 100%;
margin-bottom: 5px;
}
}
@media only screen and (max-width:801px) and (min-width: 481px) {
.select-field {
width: 50%;
}
}
</style>

View file

@ -274,9 +274,8 @@ export default {
font-size: 28px;
}
}
@media
only screen and (max-width: 760px),
(min-device-width: 768px) and (max-device-width: 1024px) {
@media only screen and (max-width:480px) {
.password-reset-token-dialog {
width: 85%
}

View file

@ -23,81 +23,73 @@
</p>
</div>
</el-dialog>
<el-row>
<el-col :span="8">
<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">
<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">
{{ user.id }}
</td>
</tr>
<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">{{ tag }}</el-tag>
<span v-if="user.tags.length === 0"></span>
</td>
</tr>
<tr class="el-table__row">
<td>{{ $t('userProfile.roles') }}</td>
<td>
<el-tag v-if="user.roles.admin" class="user-profile-tag">
{{ $t('users.admin') }}
</el-tag>
<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>
</td>
</tr>
<tr class="el-table__row">
<td>{{ $t('userProfile.localUppercase') }}</td>
<td>
<el-tag v-if="user.local" type="info">{{ $t('userProfile.local') }}</el-tag>
<el-tag v-if="!user.local" type="info">{{ $t('userProfile.external') }}</el-tag>
</td>
</tr>
<tr class="el-table__row">
<td>{{ $t('userProfile.activeUppercase') }}</td>
<td>
<el-tag v-if="!user.deactivated" type="success">{{ $t('userProfile.active') }}</el-tag>
<el-tag v-if="user.deactivated" type="danger">{{ $t('userProfile.deactivated') }}</el-tag>
</td>
</tr>
</tbody>
</table>
</div>
</el-card>
</el-col>
<el-row type="flex" class="row-bg" justify="space-between">
<el-col :span="18">
<h2 class="recent-statuses">{{ $t('userProfile.recentStatuses') }}</h2>
</el-col>
<el-col :span="6" class="show-private">
<el-checkbox v-model="showPrivate" @change="onTogglePrivate">
{{ $t('userProfile.showPrivateStatuses') }}
</el-checkbox>
</el-col>
</el-row>
<el-col :span="16">
<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">
<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">
{{ user.id }}
</td>
</tr>
<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">{{ tag }}</el-tag>
<span v-if="user.tags.length === 0"></span>
</td>
</tr>
<tr class="el-table__row">
<td>{{ $t('userProfile.roles') }}</td>
<td>
<el-tag v-if="user.roles.admin" class="user-profile-tag">
{{ $t('users.admin') }}
</el-tag>
<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>
</td>
</tr>
<tr class="el-table__row">
<td>{{ $t('userProfile.localUppercase') }}</td>
<td>
<el-tag v-if="user.local" type="info">{{ $t('userProfile.local') }}</el-tag>
<el-tag v-if="!user.local" type="info">{{ $t('userProfile.external') }}</el-tag>
</td>
</tr>
<tr class="el-table__row">
<td>{{ $t('userProfile.activeUppercase') }}</td>
<td>
<el-tag v-if="!user.deactivated" type="success">{{ $t('userProfile.active') }}</el-tag>
<el-tag v-if="user.deactivated" type="danger">{{ $t('userProfile.deactivated') }}</el-tag>
</td>
</tr>
</tbody>
</table>
</div>
</el-card>
<div class="recent-statuses-container">
<h2 class="recent-statuses">{{ $t('userProfile.recentStatuses') }}</h2>
<el-checkbox v-model="showPrivate" class="show-private-statuses" @change="onTogglePrivate">
{{ $t('statuses.showPrivateStatuses') }}
</el-checkbox>
<el-timeline v-if="!statusesLoading" class="statuses">
<el-timeline-item v-for="status in statuses" :key="status.id">
<status :status="status" :show-checkbox="false" :user-id="user.id" :godmode="showPrivate"/>
</el-timeline-item>
<p v-if="statuses.length === 0" class="no-statuses">{{ $t('userProfile.noStatuses') }}</p>
</el-timeline>
</el-col>
</el-row>
</div>
</div>
</main>
</template>
@ -193,6 +185,11 @@ table {
margin-left: 28px;
color: #606266;
}
.recent-statuses-container {
display: flex;
flex-direction: column;
width: 67%;
}
.recent-statuses-header {
margin-top: 10px;
}
@ -205,6 +202,10 @@ table {
line-height: 67px;
margin-right: 20px;
}
.show-private-statuses {
margin-left: 28px;
margin-bottom: 20px;
}
.recent-statuses {
margin-left: 28px;
}
@ -218,6 +219,11 @@ table {
}
.user-profile-card {
margin: 0 20px;
width: 30%;
height: fit-content;
}
.user-profile-container {
display: flex;
}
.user-profile-table {
margin: 0;
@ -225,4 +231,65 @@ table {
.user-profile-tag {
margin: 0 4px 4px 0;
}
@media only screen and (max-width:480px) {
.avatar-name-container {
margin-bottom: 10px;
}
.recent-statuses {
margin: 20px 10px 15px 10px;
}
.recent-statuses-container {
width: 100%;
margin: 0 10px;
}
.show-private-statuses {
margin: 0 10px 20px 10px;
}
.user-page-header {
flex-direction: column;
align-items: flex-start;
padding: 0;
margin: 7px 0 15px 10px;
}
.user-profile-card {
margin: 0 10px;
width: 95%;
td {
width: 80px;
}
}
.user-profile-container {
flex-direction: column;
}
}
@media only screen and (max-width:801px) and (min-width: 481px) {
.avatar-name-container {
margin-bottom: 20px;
}
.recent-statuses {
margin: 20px 10px 15px 0;
}
.recent-statuses-container {
width: 97%;
margin: 0 20px;
}
.show-private-statuses {
margin: 0 10px 20px 0;
}
.user-page-header {
flex-direction: column;
align-items: flex-start;
padding: 0;
margin: 7px 0 20px 20px;
}
.user-profile-card {
margin: 0 20px;
width: fit-content;
}
.user-profile-container {
flex-direction: column;
}
}
</style>

View file

@ -1,47 +0,0 @@
import Vuex from 'vuex'
import { mount, createLocalVue, config } from '@vue/test-utils'
import Element from 'element-ui'
import GroupedReport from '@/views/reports/components/GroupedReport'
import storeConfig from './store.conf'
import { cloneDeep } from 'lodash'
import flushPromises from 'flush-promises'
config.mocks["$t"] = () => {}
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(Element)
jest.mock('@/api/reports')
describe('Grouped report', () => {
let store
beforeEach(async() => {
store = new Vuex.Store(cloneDeep(storeConfig))
store.dispatch('FetchGroupedReports')
await flushPromises()
})
it('changes state of all reports in a group', async (done) => {
const groupedReports = store.state.reports.fetchedGroupedReports
const wrapper = mount(GroupedReport, {
store,
localVue,
propsData: {
groupedReports
}
})
expect(groupedReports[0].reports[0].state).toBe('open')
expect(groupedReports[0].reports[1].state).toBe('resolved')
const button = wrapper.find(`.grouped-report .el-dropdown-menu__item:nth-child(3)`)
button.trigger('click')
await flushPromises()
expect(store.state.reports.fetchedGroupedReports[0].reports[0].state).toBe('closed')
expect(store.state.reports.fetchedGroupedReports[0].reports[1].state).toBe('closed')
done()
})
})