Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • pleroma/admin-fe
  • linafilippova/admin-fe
  • Exilat_a_Tolosa/admin-fe
  • mkljczk/admin-fe
  • maxf/admin-fe
  • kphrx/admin-fe
  • vaartis/admin-fe
  • ELR/admin-fe
  • eugenijm/admin-fe
  • jp/admin-fe
  • mkfain/admin-fe
  • lorenzoancora/admin-fe
  • alexgleason/admin-fe
  • seanking/admin-fe
  • ilja/admin-fe
15 results
Show changes
Commits on Source (38)
Showing
with 437 additions and 375 deletions
......@@ -4,32 +4,31 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased
### Changed
- **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
## [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
- Mobile UI for Settings tab
- 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 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
......@@ -38,6 +37,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
......
......@@ -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: '' })
}
......
......@@ -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),
......
......@@ -40,4 +40,13 @@ export async function removeSettings(configs, authHost, token) {
})
}
export async function restartApp(authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/restart`,
method: 'get',
headers: authHeaders(token)
})
}
const authHeaders = (token) => token ? { 'Authorization': `Bearer ${getToken()}` } : {}
......@@ -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%;
}
......
......@@ -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',
......@@ -254,7 +254,7 @@ export default {
external: 'external',
localUppercase: 'Local',
nickname: 'Nickname',
recentStatuses: 'Recent Statues',
recentStatuses: 'Recent Statuses',
showPrivateStatuses: 'Show private statuses',
roles: 'Roles',
activeUppercase: 'Active',
......@@ -273,7 +273,6 @@ export default {
},
reports: {
reports: 'Reports',
groupedReports: 'Grouped reports',
reply: 'Reply',
from: 'From',
showNotes: 'Show notes',
......@@ -363,7 +362,10 @@ export default {
assets: 'Assets',
emoji: 'Emoji',
markup: 'Markup settings',
corsPlug: 'CORS plug config'
corsPlug: 'CORS plug config',
instanceReboot: 'Instance Reboot',
restartApp: 'You must restart the instance to apply settings',
restartSuccess: 'Instance rebooted successfully!'
},
invites: {
inviteTokens: 'Invite tokens',
......@@ -413,7 +415,7 @@ export default {
shortcode: 'Shortcode',
fallbackSrc: 'Fallback source',
fallbackSrcSha: 'Fallback source SHA',
savePackMetadata: 'Save pack metadata',
saveMetadata: 'Save metadata',
deletePack: 'Delete pack',
downloadPack: 'Download pack',
downloadPackArchive: 'Download pack archive',
......@@ -425,6 +427,7 @@ export default {
willBeUsable: 'It will then be usable and shareable from the current instance',
downloadAsOptional: 'Download as (optional)',
downloadSharedPack: 'Download shared pack to current instance',
downloadSharedPackMobile: 'Download pack to instance',
optional: 'optional',
uploadFile: 'Upload a file',
url: 'URL',
......
......@@ -16,24 +16,25 @@ import Vue from 'vue'
const packs = {
state: {
localPacks: {},
remoteInstance: '',
remotePacks: {}
},
mutations: {
SET_LOCAL_PACKS: (state, packs) => {
state.localPacks = packs
},
SET_REMOTE_INSTANCE: (state, name) => {
state.remoteInstance = name
},
SET_REMOTE_PACKS: (state, packs) => {
state.remotePacks = packs
},
UPDATE_LOCAL_PACK_VAL: (state, { name, key, value }) => {
Vue.set(state.localPacks[name]['pack'], key, value)
},
UPDATE_LOCAL_PACK_PACK: (state, { name, pack }) => {
state.localPacks[name]['pack'] = pack
},
UPDATE_LOCAL_PACK_FILES: (state, { name, files }) => {
// Use vue.set in case "files" was null
Vue.set(
......@@ -105,6 +106,7 @@ const packs = {
async SetRemoteEmojiPacks({ commit, getters }, { remoteInstance }) {
const { data } = await listRemotePacks(getters.authHost, getters.token, remoteInstance)
commit('SET_REMOTE_INSTANCE', remoteInstance)
commit('SET_REMOTE_PACKS', data)
},
async UpdateAndSavePackFile({ commit, getters }, args) {
......
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)
......
import { fetchDescription, fetchSettings, removeSettings, updateSettings } from '@/api/settings'
import { fetchDescription, fetchSettings, removeSettings, restartApp, updateSettings } from '@/api/settings'
import { checkPartialUpdate, parseNonTuples, parseTuples, valueHasTuples, wrapUpdatedSettings } from './normalizers'
import _ from 'lodash'
......@@ -6,11 +6,12 @@ const settings = {
state: {
activeTab: 'instance',
configDisabled: true,
db: {},
description: [],
loading: true,
needReboot: false,
settings: {},
updatedSettings: {},
db: {},
loading: true
updatedSettings: {}
},
mutations: {
CLEAR_UPDATED_SETTINGS: (state) => {
......@@ -50,6 +51,9 @@ const settings = {
state.settings = newSettings
state.db = newDbSettings
},
TOGGLE_REBOOT: (state, needReboot) => {
state.needReboot = needReboot || false
},
TOGGLE_TABS: (state, status) => {
state.configDisabled = status
},
......@@ -74,6 +78,7 @@ const settings = {
const description = await fetchDescription(getters.authHost, getters.token)
commit('SET_DESCRIPTION', description.data)
commit('SET_SETTINGS', response.data.configs)
commit('TOGGLE_REBOOT', response.data.need_reboot)
} catch (_e) {
commit('TOGGLE_TABS', true)
commit('SET_ACTIVE_TAB', 'relays')
......@@ -88,8 +93,13 @@ const settings = {
const response = await fetchSettings(getters.authHost, getters.token)
const { group, key, subkeys } = configs[0]
commit('SET_SETTINGS', response.data.configs)
commit('TOGGLE_REBOOT', response.data.need_reboot)
commit('REMOVE_SETTING_FROM_UPDATED', { group, key, subkeys: subkeys || [] })
},
async RestartApplication({ commit, getters }) {
await restartApp(getters.authHost, getters.token)
commit('TOGGLE_REBOOT', false)
},
SetActiveTab({ commit }, tab) {
commit('SET_ACTIVE_TAB', tab)
},
......@@ -102,6 +112,7 @@ const settings = {
await updateSettings(configs, getters.authHost, getters.token)
const response = await fetchSettings(getters.authHost, getters.token)
commit('SET_SETTINGS', response.data.configs)
commit('TOGGLE_REBOOT', response.data.need_reboot)
commit('CLEAR_UPDATED_SETTINGS')
},
UpdateSettings({ commit }, { group, key, input, value, type }) {
......
......@@ -36,8 +36,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,8 +46,6 @@ 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 }) {
......
<template>
<el-collapse-item :title="name" :name="name" class="has-background">
<el-form v-if="isLocal" label-width="120px" label-position="left" size="small" class="emoji-pack-metadata">
<el-form v-if="isLocal" :label-width="labelWidth" label-position="left" size="small" class="emoji-pack-metadata">
<el-form-item :label=" $t('emoji.sharePack')">
<el-switch v-model="share" />
</el-form-item>
......@@ -21,11 +21,13 @@
:label=" $t('emoji.fallbackSrcSha')">
{{ pack.pack["fallback-src-sha256"] }}
</el-form-item>
<el-form-item class="save-pack-button">
<el-button type="primary" @click="savePackMetadata">{{ $t('emoji.savePackMetadata') }}</el-button>
<el-button @click="deletePack">{{ $t('emoji.deletePack') }}</el-button>
</el-form-item>
<el-form-item>
</el-form>
<div v-if="isLocal" class="pack-button-container">
<div class="save-pack-button-container">
<el-button type="primary" class="save-pack-button" @click="savePackMetadata">{{ $t('emoji.saveMetadata') }}</el-button>
<el-button class="delete-pack-button" @click="deletePack">{{ $t('emoji.deletePack') }}</el-button>
</div>
<div class="download-pack-button-container">
<el-link
v-if="pack.pack['can-download']"
:href="`//${host}/api/pleroma/emoji/packs/${name}/download_shared`"
......@@ -34,9 +36,9 @@
target="_blank">
<el-button class="download-archive">{{ $t('emoji.downloadPackArchive') }}</el-button>
</el-link>
</el-form-item>
</el-form>
<el-form v-if="!isLocal" label-width="120px" label-position="left" size="small" class="emoji-pack-metadata">
</div>
</div>
<el-form v-if="!isLocal" :label-width="labelWidth" label-position="left" size="small" class="emoji-pack-metadata remote-pack-metadata">
<el-form-item :label=" $t('emoji.sharePack')">
<el-switch v-model="share" disabled />
</el-form-item>
......@@ -91,7 +93,7 @@
<div class="download-shared-pack">
<el-input v-model="downloadSharedAs" :placeholder=" $t('emoji.downloadAsOptional')"/>
<el-button type="primary" class="download-shared-pack-button" @click="downloadFromInstance">
{{ $t('emoji.downloadSharedPack') }}
{{ isDesktop ? $t('emoji.downloadSharedPack') : $t('emoji.downloadSharedPackMobile') }}
</el-button>
</div>
</el-collapse-item>
......@@ -104,7 +106,6 @@ import SingleEmojiEditor from './SingleEmojiEditor.vue'
import NewEmojiUploader from './NewEmojiUploader.vue'
export default {
components: { SingleEmojiEditor, NewEmojiUploader },
props: {
name: {
......@@ -132,6 +133,24 @@ export default {
}
},
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 '90px'
} else if (this.isTablet) {
return '120px'
} else {
return '120px'
}
},
share: {
get() { return this.pack.pack['share-files'] },
set(value) {
......@@ -221,6 +240,18 @@ export default {
.download-archive {
width: 250px
}
.download-pack-button-container {
width: 265px;
.el-link {
width: inherit;
span {
width: inherit;
.download-archive {
width: inherit;
}
}
}
}
.download-shared-pack {
display: flex;
margin-bottom: 10px;
......@@ -251,7 +282,54 @@ export default {
.no-background .el-collapse-item__header {
background: white;
}
.save-pack-button {
margin-bottom: 5px
.pack-button-container {
margin: 0 0 18px 120px;
}
.save-pack-button-container {
margin-bottom: 8px;
width: 265px;
display: flex;
justify-content: space-between;
}
@media only screen and (max-width:480px) {
.delete-pack-button {
width: 45%;
}
.download-pack-button-container {
width: 100%;
}
.download-shared-pack {
flex-direction: column;
}
.download-shared-pack-button {
margin-left: 0;
margin-top: 10px;
padding: 10px;
}
.pack-button-container {
width: 100%;
margin: 0 0 22px 0;
}
.remote-pack-metadata {
.el-form-item__content {
line-height: 24px;
margin-top: 4px;
}
}
.save-pack-button {
width: 54%;
}
.save-pack-button-container {
margin-bottom: 8px;
width: 100%;
display: flex;
justify-content: space-between;
button {
padding: 10px 5px;
}
.el-button+.el-button {
margin-left: 3px;
}
}
}
</style>
<template>
<el-form label-width="130px" label-position="left" size="small">
<el-form :label-position="isMobile ? 'top' : 'left'" label-width="130px" size="small" class="new-emoji-uploader-form">
<el-form-item :label="$t('emoji.shortcode')">
<el-input v-model="shortcode" :placeholder="$t('emoji.required')"/>
</el-form-item>
......@@ -25,30 +25,6 @@
</el-form>
</template>
<style>
.add-new-emoji {
height: 36px;
font-size: 14px;
font-weight: 700;
color: #606266;
}
.text {
line-height: 20px;
margin-right: 15px
}
.upload-container {
display: flex;
align-items: baseline;
}
.upload-button {
margin-left: 10px;
}
.upload-file-url {
display: flex;
justify-content: space-between
}
</style>
<script>
export default {
props: {
......@@ -65,6 +41,12 @@ export default {
}
},
computed: {
isDesktop() {
return this.$store.state.app.device === 'desktop'
},
isMobile() {
return this.$store.state.app.device === 'mobile'
},
shortcodePresent() {
return this.shortcode.trim() === ''
}
......@@ -88,3 +70,34 @@ export default {
}
}
</script>
<style rel='stylesheet/scss' lang='scss'>
.add-new-emoji {
height: 36px;
font-size: 14px;
font-weight: 700;
color: #606266;
}
.text {
line-height: 20px;
margin-right: 15px;
}
.upload-container {
display: flex;
align-items: baseline;
}
.upload-button {
margin-left: 10px;
}
.upload-file-url {
display: flex;
justify-content: space-between;
}
@media only screen and (max-width:480px) {
.new-emoji-uploader-form {
label.el-form-item__label {
padding: 0;
}
}
}
</style>
<template>
<div>
<div v-if="isLocal" class="emoji-container">
<div v-if="isLocal" :class="isMobile ? 'emoji-container-flex' : 'emoji-container-grid'">
<img
:src="addressOfEmojiInPack(host, packName, file)"
class="emoji-preview-img">
......@@ -8,19 +8,19 @@
<el-input v-model="emojiFile" :placeholder="$t('emoji.file')" class="emoji-info"/>
<div class="emoji-buttons">
<el-button type="primary" @click="update">{{ $t('emoji.update') }}</el-button>
<el-button @click="remove">{{ $t('emoji.remove') }}</el-button>
<el-button class="remove-emoji-button" @click="remove">{{ $t('emoji.remove') }}</el-button>
</div>
</div>
<div v-if="!isLocal" class="emoji-container">
<div v-if="!isLocal" :class="isMobile ? 'emoji-container-flex' : 'remote-emoji-container-grid'">
<img
:src="addressOfEmojiInPack(host, packName, file)"
:src="addressOfEmojiInPack(remoteInstance, packName, file)"
class="emoji-preview-img">
<el-input :value="emojiName" :placeholder="$t('emoji.shortcode')" class="emoji-info"/>
<el-input :value="emojiFile" :placeholder="$t('emoji.file')" class="emoji-info"/>
<el-popover v-model="copyPopoverVisible" placement="left-start" popper-class="copy-popover">
<el-popover v-model="copyPopoverVisible" placement="left-start" popper-class="copy-popover" class="copy-pack-container">
<p>{{ $t('emoji.selectLocalPack') }}</p>
<el-select v-model="copyToLocalPackName" :placeholder="$t('emoji.localPack')">
<el-select v-model="copyToLocalPackName" :placeholder="$t('emoji.localPack')" class="copy-pack-select">
<el-option
v-for="(_pack, name) in localPacks"
:key="name"
......@@ -34,7 +34,6 @@
<el-button
:disabled="!copyToLocalPackName"
type="primary"
class="copy-to-local-button"
@click="copyToLocal">{{ $t('emoji.copy') }}</el-button>
<el-button slot="reference" type="primary" class="emoji-button">{{ $t('emoji.copyToLocalPack') }}</el-button>
</el-popover>
......@@ -93,8 +92,17 @@ export default {
},
set(val) { this.newFile = val }
},
isDesktop() {
return this.$store.state.app.device === 'desktop'
},
isMobile() {
return this.$store.state.app.device === 'mobile'
},
localPacks() {
return this.$store.state.emojiPacks.localPacks
},
remoteInstance() {
return this.$store.state.emojiPacks.remoteInstance
}
},
methods: {
......@@ -151,7 +159,7 @@ export default {
}
</script>
<style>
<style rel='stylesheet/scss' lang='scss'>
.copy-popover {
width: 330px
}
......@@ -159,9 +167,9 @@ export default {
place-self: center;
min-width: 200px
}
.emoji-container {
.emoji-container-grid {
display: grid;
grid-template-columns: 75px auto auto 195px;
grid-template-columns: 75px auto auto 200px;
grid-column-gap: 15px;
margin-bottom: 10px;
}
......@@ -172,8 +180,56 @@ export default {
.emoji-info {
place-self: center;
}
.copy-to-local-button {
margin-top: 12px;
float: right;
.copy-pack-container {
place-self: center stretch;
}
.copy-pack-select {
width: 100%;
}
.remote-emoji-container-grid {
display: grid;
grid-template-columns: 75px auto auto 160px;
grid-column-gap: 15px;
margin-bottom: 10px;
}
@media only screen and (max-width:480px) {
.emoji-container-flex {
display: flex;
flex-direction: column;
border: 1px solid #dcdfe6;
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
border-radius: 4px;
padding: 15px;
margin: 0 15px 15px 0;
}
.emoji-preview-img {
margin-bottom: 10px;
}
.emoji-info {
margin-bottom: 10px;
}
.emoji-buttons {
display: flex;
justify-content: space-between;
width: 100%;
button {
padding: 10px 5px;
width: 47%;
}
}
}
@media only screen and (max-width:801px) and (min-width: 481px) {
.emoji-container-grid {
grid-column-gap: 10px;
}
.emoji-buttons {
.el-button+.el-button {
margin-left: 5px;
}
}
.remote-emoji-container-grid {
grid-column-gap: 10px;
}
}
</style>
<template>
<div class="emoji-packs">
<h1 class="emoji-packs-header">{{ $t('emoji.emojiPacks') }}</h1>
<div class="button-container">
<el-button type="primary" @click="reloadEmoji">{{ $t('emoji.reloadEmoji') }}</el-button>
<el-tooltip :content="$t('emoji.importEmojiTooltip')" effects="dark" placement="bottom">
<div class="emoji-packs-header-button-container">
<el-button type="primary" class="reload-emoji-button" @click="reloadEmoji">{{ $t('emoji.reloadEmoji') }}</el-button>
<el-tooltip :content="$t('emoji.importEmojiTooltip')" effects="dark" placement="bottom" class="import-pack-button">
<el-button type="primary" @click="importFromFS">
{{ $t('emoji.importPacks') }}
</el-button>
</el-tooltip>
</div>
<el-divider class="divider"/>
<el-form label-width="180px" class="emoji-packs-form">
<el-form :label-width="labelWidth" class="emoji-packs-form">
<el-form-item :label="$t('emoji.localPacks')">
<el-button type="primary" @click="refreshLocalPacks">{{ $t('emoji.refreshLocalPacks') }}</el-button>
</el-form-item>
......@@ -71,8 +71,17 @@ export default {
isMobile() {
return this.$store.state.app.device === 'mobile'
},
isTablet() {
return this.$store.state.app.device === 'tablet'
},
labelWidth() {
return this.isMobile ? '120px' : '240px'
if (this.isMobile) {
return '105px'
} else if (this.isTablet) {
return '180px'
} else {
return '240px'
}
},
localPacks() {
return this.$store.state.emojiPacks.localPacks
......@@ -131,7 +140,8 @@ export default {
</script>
<style rel='stylesheet/scss' lang='scss'>
.button-container {
.emoji-packs-header-button-container {
display: flex;
margin: 0 0 22px 15px;
}
.create-pack {
......@@ -147,6 +157,9 @@ export default {
.emoji-packs-header {
margin: 22px 0 20px 15px;
}
.import-pack-button {
margin-left: 10px;
}
.line {
width: 100%;
height: 0;
......@@ -160,4 +173,46 @@ export default {
margin: auto;
}
}
@media only screen and (max-width:480px) {
.create-pack {
height: 82px;
flex-direction: column;
}
.create-pack-button {
margin-left: 0;
}
.divider {
margin: 15px 0;
}
.el-message {
min-width: 80%;
}
.el-message-box {
width: 80%;
}
.emoji-packs-form {
margin: 0 7px;
label {
padding-right: 8px;
}
.el-form-item {
margin-bottom: 15px;
}
}
.emoji-packs-header {
margin: 15px;
}
.emoji-packs-header-button-container {
height: 82px;
flex-direction: column;
.el-button+.el-button {
margin: 7px 0 0 0;
width: fit-content;
}
}
.reload-emoji-button {
width: fit-content;
}
}
</style>
......@@ -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%
......
import store from '@/store'
const { body } = document
const WIDTH = 1024
const RATIO = 3
const mobileWidth = 480
const tabletWidth = 801
const ratio = 3
export default {
watch: {
......@@ -17,23 +18,31 @@ export default {
},
mounted() {
const isMobile = this.isMobile()
if (isMobile) {
store.dispatch('toggleDevice', 'mobile')
const isTablet = this.isTablet()
if (isMobile || isTablet) {
store.dispatch('toggleDevice', isMobile ? 'mobile' : 'tablet')
store.dispatch('closeSideBar', { withoutAnimation: true })
}
},
methods: {
isMobile() {
const rect = body.getBoundingClientRect()
return rect.width - RATIO < WIDTH
return rect.width - ratio < mobileWidth
},
isTablet() {
const rect = body.getBoundingClientRect()
return rect.width - ratio < tabletWidth && rect.width - ratio > mobileWidth
},
resizeHandler() {
if (!document.hidden) {
const isMobile = this.isMobile()
store.dispatch('toggleDevice', isMobile ? 'mobile' : 'desktop')
const isTablet = this.isTablet()
if (isMobile) {
if (isMobile || isTablet) {
store.dispatch('toggleDevice', isMobile ? 'mobile' : 'tablet')
store.dispatch('closeSideBar', { withoutAnimation: true })
} else {
store.dispatch('toggleDevice', 'desktop')
}
}
}
......
<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"
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"
......@@ -55,6 +49,7 @@
:hide-on-single-page="true"
:page-size="50"
:total="total"
:small="isMobile"
layout="prev, pager, next"
@current-change="fetchLogWithFilters" />
</div>
......@@ -76,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
......@@ -138,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;
}
......@@ -148,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>
<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>
......@@ -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;
}
......
......@@ -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>