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 (4)
Showing
with 838 additions and 238 deletions
......@@ -4,24 +4,35 @@ 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
### Added
- Emoji pack configuration
## [1.1.0] - 2019-09-15
### Added
- add ability to configure new settings (UploadS3 bucket namespace, Rate limit for Activity pub routes, Email notifications settings, MRF Vocabulary, user bio and name length and others)
- add ability to disable certain features (settings/reports)
- add sign in via PleromaFE
- adds ability to configure new settings (UploadS3 bucket namespace, Rate limit for Activity pub routes, Email notifications settings, MRF Vocabulary, user bio and name length and others)
- adds ability to disable certain features (settings/reports/invites)
- adds sign in via PleromaFE
- adds ability to generate invite tokens and list them on a separate tab
- adds ability to invite users via email
- adds ability to reset users passwords
- adds tests for invites and resetting password
### Changed
- removes "Dashboard" from dropdown menu
- makes all single selects clearable and allow to enter custom values in all multiple selects
- remove legacy activitypub accept_blocks setting
- removes legacy activitypub accept_blocks setting
### Fixed
- converts maps and structs to JS objects, not array of tuples when wrapping config
- changes type of IP value from string to number
- updates error handling for users and invites modules
## [1.0.1] - 2019-08-15
......
......@@ -18,7 +18,8 @@ To compile everything for production run `yarn build:prod`.
#### Disabling features
You can disable certain AdminFE features, like reports or settings by modifying `config/prod.env.js` env variable `DISABLED_FEATURES`, e.g. if you want to compile AdminFE without "Settings" you'll need to set it to: `DISABLED_FEATURES: '["settings"]'`.
You can disable certain AdminFE features, like reports or settings by modifying `config/prod.env.js` env variable `DISABLED_FEATURES`, e.g. if you want to compile AdminFE without "Settings" you'll need to set it to: `DISABLED_FEATURES: '["settings"]'`,
to disable emoji pack settings add `"emoji-packs"` to the list.
## Changelog
......
......@@ -48,7 +48,7 @@ const devWebpackConfig = merge(baseWebpackConfig, {
poll: config.dev.poll
},
headers: {
'content-security-policy': "base-uri 'self'; frame-ancestors 'none'; img-src 'self' data: https:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; font-src 'self'; manifest-src 'self'; script-src 'self';"
'content-security-policy': "base-uri 'self'; frame-ancestors 'none'; img-src 'self' data: https: http:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; font-src 'self'; manifest-src 'self'; script-src 'self';"
}
},
plugins: [
......
let inviteTokens = [
{ expires_at: '01-01-2020', id: 1, invite_type: 'one_time', max_use: 3, token: 'DCN8XyTsVEuz9_KuxPlkbH1RgMsMHepwmZE2gyX07Jw=', used: false, uses: 1 },
{ expires_at: '02-02-2020', id: 2, invite_type: 'one_time', max_use: 1, token: 'KnJTHNedj2Mh14ckx06t-VfOuFL8oNA0nVAK1HLeLf4=', used: true, uses: 1 },
{ expires_at: '03-03-2020', id: 3, invite_type: 'one_time', max_use: 5, token: 'P6F5ayP-rAMbxtmtGJwFJcd7Yk_D2g6UZRfh8EskRUc=', used: false, uses: 0 }
]
export async function generateInviteToken(max_use, expires_at, authHost, token) {
const newToken = {
expires_at: '2019-04-10',
id: 4,
invite_type: 'one_time',
max_use: 3,
token: 'JYl0SjXW8t-t-pLSZBnZLf6PwjCW-qy6Dq70jfUOuqk=',
used: false,
uses: 0
}
inviteTokens = [...inviteTokens, newToken]
return Promise.resolve({ data: newToken })
}
export async function inviteViaEmail(email, name, authHost, token) {
return Promise.resolve()
}
export async function listInviteTokens(authHost, token) {
return Promise.resolve({ data: {
invites: inviteTokens
}})
}
export async function revokeToken(tokenToRevoke, authHost, token) {
inviteTokens.splice(3, 1, { ...inviteTokens[3], used: true })
return Promise.resolve()
}
......@@ -29,6 +29,10 @@ export async function fetchUsers(filters, authHost, token, page = 1) {
}})
}
export async function getPasswordResetToken(nickname, authHost, token) {
return Promise.resolve({ data: { token: 'g05lxnBJQnL', link: 'http://url/api/pleroma/password_reset/g05lxnBJQnL' }})
}
export async function toggleUserActivation(nickname, authHost, token) {
const response = users.find(user => user.nickname === nickname)
return Promise.resolve({ data: { ...response, deactivated: !response.deactivated }})
......
import request from '@/utils/request'
import { getToken } from '@/utils/auth'
import { baseName } from './utils'
import _ from 'lodash'
export async function deletePack(host, token, name) {
return await request({
baseURL: baseName(host),
url: `/api/pleroma/emoji/packs/${name}`,
method: 'delete',
headers: authHeaders(token)
})
}
export async function reloadEmoji(host, token) {
return await request({
baseURL: baseName(host),
url: '/api/pleroma/admin/reload_emoji',
method: 'post',
headers: authHeaders(token)
})
}
export async function importFromFS(host, token) {
return await request({
baseURL: baseName(host),
url: '/api/pleroma/emoji/packs/import_from_fs',
method: 'post',
headers: authHeaders(token)
})
}
export async function createPack(host, token, name) {
return await request({
baseURL: baseName(host),
url: `/api/pleroma/emoji/packs/${name}`,
method: 'put',
headers: authHeaders(token)
})
}
export async function listPacks(host) {
return await request({
baseURL: baseName(host),
url: `/api/pleroma/emoji/packs/`,
method: 'get'
})
}
export async function downloadFrom(host, instance_address, pack_name, as, token) {
if (as.trim() === '') {
as = null
}
return await request({
baseURL: baseName(host),
url: '/api/pleroma/emoji/packs/download_from',
method: 'post',
headers: authHeaders(token),
data: { instance_address, pack_name, as },
timeout: 0
})
}
export async function savePackMetadata(host, token, name, new_data) {
return await request({
baseURL: baseName(host),
url: `/api/pleroma/emoji/packs/${name}/update_metadata`,
method: 'post',
headers: authHeaders(token),
data: { name, new_data },
timeout: 0 // This might take a long time
})
}
function fileUpdateFormData(d) {
const data = new FormData()
_.each(d, (v, k) => {
data.set(k, v)
})
return data
}
export async function updatePackFile(host, token, args) {
let data = null
switch (args.action) {
case 'add': {
const { shortcode, file, fileName } = args
data = fileUpdateFormData({
action: 'add',
shortcode: shortcode,
file: file
})
if (fileName.trim() !== '') {
data.set('filename', fileName)
}
break
}
case 'update': {
const { oldName, newName, newFilename } = args
data = fileUpdateFormData({
action: 'update',
shortcode: oldName,
new_shortcode: newName,
new_filename: newFilename
})
break
}
case 'remove': {
const { name } = args
data = fileUpdateFormData({
action: 'remove',
shortcode: name
})
break
}
}
const { packName } = args
return await request({
baseURL: baseName(host),
url: `/api/pleroma/emoji/packs/${packName}/update_file`,
method: 'post',
headers: authHeaders(token),
data: data,
timeout: 0
})
}
export function addressOfEmojiInPack(host, packName, name) {
// This needs http because hackney on the BE does not understand URLs with just "//"
return `http://${baseName(host)}/emoji/${packName}/${name}`
}
const authHeaders = (token) => token ? { 'Authorization': `Bearer ${getToken()}` } : {}
import request from '@/utils/request'
import { getToken } from '@/utils/auth'
import { baseName } from './utils'
export async function generateInviteToken(max_use, expires_at, authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/users/invite_token`,
method: 'post',
headers: authHeaders(token),
data: expires_at && expires_at.length > 0 ? { max_use, expires_at } : { max_use }
})
}
export async function inviteViaEmail(email, name, authHost, token) {
const url = name.length > 0
? `/api/pleroma/admin/users/email_invite?email=${email}&name=${name}`
: `/api/pleroma/admin/users/email_invite?email=${email}`
return await request({
baseURL: baseName(authHost),
url,
method: 'post',
headers: authHeaders(token)
})
}
export async function listInviteTokens(authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/users/invites`,
method: 'get',
headers: authHeaders(token)
})
}
export async function revokeToken(tokenToRevoke, authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/users/revoke_invite`,
method: 'post',
headers: authHeaders(token),
data: { token: tokenToRevoke }
})
}
const authHeaders = (token) => token ? { 'Authorization': `Bearer ${getToken()}` } : {}
......@@ -17,7 +17,7 @@ export async function createNewAccount(nickname, email, password, authHost, toke
url: '/api/pleroma/admin/users',
method: 'post',
headers: authHeaders(token),
data: { nickname, email, password }
data: { users: [{ nickname, email, password }] }
})
}
......@@ -57,6 +57,15 @@ export async function fetchUsers(filters, authHost, token, page = 1) {
})
}
export async function getPasswordResetToken(nickname, authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/users/${nickname}/password_reset`,
method: 'get',
headers: authHeaders(token)
})
}
export async function searchUsers(query, filters, authHost, token, page = 1) {
return await request({
baseURL: baseName(authHost),
......
<template>
<el-dropdown trigger="click" class="international" @command="handleSetLanguage">
<div>
<svg-icon class-name="international-icon" icon-class="language" />
</div>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item :disabled="language==='zh'" command="zh">中文</el-dropdown-item>
<el-dropdown-item :disabled="language==='en'" command="en">English</el-dropdown-item>
<el-dropdown-item :disabled="language==='es'" command="es">Español</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
<script>
export default {
computed: {
language() {
return this.$store.getters.language
}
},
methods: {
handleSetLanguage(lang) {
this.$i18n.locale = lang
this.$store.dispatch('setLanguage', lang)
this.$message({
message: 'Switch Language Success',
type: 'success'
})
}
}
}
</script>
<template>
<div>
<svg-icon :icon-class="isFullscreen?'exit-fullscreen':'fullscreen'" @click="click" />
</div>
</template>
<script>
import screenfull from 'screenfull'
export default {
name: 'Screenfull',
data: function() {
return {
isFullscreen: false
}
},
mounted() {
this.init()
},
methods: {
click() {
if (!screenfull.enabled) {
this.$message({
message: 'you browser can not work',
type: 'warning'
})
return false
}
screenfull.toggle()
},
init() {
if (screenfull.enabled) {
screenfull.on('change', () => {
this.isFullscreen = screenfull.isFullscreen
})
}
}
}
}
</script>
<style scoped>
.screenfull-svg {
display: inline-block;
cursor: pointer;
fill: #5a5e66;;
width: 20px;
height: 20px;
vertical-align: 10px;
}
</style>
<template>
<div>
<input ref="excel-upload-input" class="excel-upload-input" type="file" accept=".xlsx, .xls" @change="handleClick">
<div class="drop" @drop="handleDrop" @dragover="handleDragover" @dragenter="handleDragover">
Drop excel file here or
<el-button :loading="loading" style="margin-left:16px;" size="mini" type="primary" @click="handleUpload">Browse</el-button>
</div>
</div>
</template>
<script>
import XLSX from 'xlsx'
export default {
props: {
beforeUpload: Function, // eslint-disable-line
onSuccess: Function// eslint-disable-line
},
data: function() {
return {
loading: false,
excelData: {
header: null,
results: null
}
}
},
methods: {
generateData({ header, results }) {
this.excelData.header = header
this.excelData.results = results
this.onSuccess && this.onSuccess(this.excelData)
},
handleDrop(e) {
e.stopPropagation()
e.preventDefault()
if (this.loading) return
const files = e.dataTransfer.files
if (files.length !== 1) {
this.$message.error('Only support uploading one file!')
return
}
const rawFile = files[0] // only use files[0]
if (!this.isExcel(rawFile)) {
this.$message.error('Only supports upload .xlsx, .xls, .csv suffix files')
return false
}
this.upload(rawFile)
e.stopPropagation()
e.preventDefault()
},
handleDragover(e) {
e.stopPropagation()
e.preventDefault()
e.dataTransfer.dropEffect = 'copy'
},
handleUpload() {
this.$refs['excel-upload-input'].click()
},
handleClick(e) {
const files = e.target.files
const rawFile = files[0] // only use files[0]
if (!rawFile) return
this.upload(rawFile)
},
upload(rawFile) {
this.$refs['excel-upload-input'].value = null // fix can't select the same excel
if (!this.beforeUpload) {
this.readerData(rawFile)
return
}
const before = this.beforeUpload(rawFile)
if (before) {
this.readerData(rawFile)
}
},
readerData(rawFile) {
this.loading = true
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = e => {
const data = e.target.result
const workbook = XLSX.read(data, { type: 'array' })
const firstSheetName = workbook.SheetNames[0]
const worksheet = workbook.Sheets[firstSheetName]
const header = this.getHeaderRow(worksheet)
const results = XLSX.utils.sheet_to_json(worksheet)
this.generateData({ header, results })
this.loading = false
resolve()
}
reader.readAsArrayBuffer(rawFile)
})
},
getHeaderRow(sheet) {
const headers = []
const range = XLSX.utils.decode_range(sheet['!ref'])
let C
const R = range.s.r
/* start in the first row */
for (C = range.s.c; C <= range.e.c; ++C) { /* walk every column in the range */
const cell = sheet[XLSX.utils.encode_cell({ c: C, r: R })]
/* find the cell in the first row */
let hdr = 'UNKNOWN ' + C // <-- replace with your desired default
if (cell && cell.t) hdr = XLSX.utils.format_cell(cell)
headers.push(hdr)
}
return headers
},
isExcel(file) {
return /\.(xlsx|xls|csv)$/.test(file.name)
}
}
}
</script>
<style scoped>
.excel-upload-input{
display: none;
z-index: -9999;
}
.drop{
border: 2px dashed #bbb;
width: 600px;
height: 160px;
line-height: 160px;
margin: 0 auto;
font-size: 24px;
border-radius: 5px;
text-align: center;
color: #bbb;
position: relative;
}
</style>
......@@ -66,7 +66,8 @@ export default {
externalLink: 'External Link',
users: 'Users',
reports: 'Reports',
settings: 'Settings'
settings: 'Settings',
'emoji-packs': 'Emoji packs'
},
navbar: {
logOut: 'Log Out',
......@@ -201,7 +202,7 @@ export default {
disableAnySubscriptionForMultiple: 'Disallow following users at all',
selectUsers: 'Select users to apply actions to multiple users',
moderateUsers: 'Moderate multiple users',
createAccount: 'Create new user account',
createAccount: 'Create new account',
apply: 'apply',
remove: 'remove',
grantRightConfirmation: 'Are you sure you want to grant {right} rights to all selected users?',
......@@ -219,12 +220,15 @@ export default {
email: 'E-mail',
password: 'Password',
create: 'Create',
submitFormError: 'There are errors on the form. Please fix them before continuing.',
submitFormError: 'There are invalid values in the form. Please fix them before continuing.',
emptyEmailError: 'Please input the e-mail',
invalidEmailError: 'Please input valid e-mail',
emptyPasswordError: 'Please input the password',
emptyNicknameError: 'Please input the username',
invalidNicknameError: 'Username can include "a-z", "A-Z" and "0-9" characters'
invalidNicknameError: 'Username can include "a-z", "A-Z" and "0-9" characters',
getPasswordResetToken: 'Get password reset token',
passwordResetTokenCreated: 'Password reset token was created',
accountCreated: 'New account was created!'
},
userProfile: {
tags: 'Tags',
......@@ -302,5 +306,31 @@ export default {
database: 'Database',
other: 'Other',
success: 'Settings changed successfully!'
},
invites: {
inviteTokens: 'Invite tokens',
createInviteToken: 'Generate invite token',
pickDate: 'Pick a date',
maxUse: 'Max use',
expiresAt: 'Expires at',
tokenCreated: 'Invite token was created',
token: 'Token',
uses: 'Uses',
used: 'Used',
cancel: 'Cancel',
create: 'Create',
revoke: 'Revoke',
id: 'ID',
actions: 'Actions',
active: 'Active',
inviteUserViaEmail: 'Invite user via email',
sendRegistration: 'Send registration invite via email',
email: 'Email',
name: 'Name',
emptyEmailError: 'Please input the e-mail',
invalidEmailError: 'Please input valid e-mail',
emailSent: 'Invite was sent',
submitFormError: 'There are invalid values in the form. Please fix them before continuing.',
inviteViaEmailAlert: 'To send invite via email make sure to enable `invites_enabled` and disable `registrations_open`'
}
}
......@@ -16,7 +16,7 @@ const settings = {
path: 'index',
component: () => import('@/views/settings/index'),
name: 'Settings',
meta: { title: 'settings', icon: 'settings', noCache: true }
meta: { title: 'Settings', icon: 'settings', noCache: true }
}
]
}
......@@ -30,7 +30,35 @@ const reports = {
path: 'index',
component: () => import('@/views/reports/index'),
name: 'Reports',
meta: { title: 'reports', icon: 'documentation', noCache: true }
meta: { title: 'Reports', icon: 'documentation', noCache: true }
}
]
}
const invitesDisabled = disabledFeatures.includes('invites')
const invites = {
path: '/invites',
component: Layout,
children: [
{
path: 'index',
component: () => import('@/views/invites/index'),
name: 'Invites',
meta: { title: 'Invites', icon: 'guide', noCache: true }
}
]
}
const emojiPacksDisabled = disabledFeatures.includes('emoji-packs')
const emojiPacks = {
path: '/emoji-packs',
component: Layout,
children: [
{
path: 'index',
component: () => import('@/views/emoji-packs/index'),
name: 'Emoji packs',
meta: { title: 'emoji-packs', icon: 'settings', noCache: true }
}
]
}
......@@ -94,12 +122,14 @@ export const asyncRouterMap = [
path: 'index',
component: () => import('@/views/users/index'),
name: 'Users',
meta: { title: 'users', icon: 'peoples', noCache: true }
meta: { title: 'Users', icon: 'peoples', noCache: true }
}
]
},
...(settingsDisabled ? [] : [settings]),
...(reportsDisabled ? [] : [reports]),
...(invitesDisabled ? [] : [invites]),
...(emojiPacksDisabled ? [] : [emojiPacks]),
{
path: '/users/:id',
component: Layout,
......
......@@ -2,6 +2,7 @@ import Vue from 'vue'
import Vuex from 'vuex'
import app from './modules/app'
import errorLog from './modules/errorLog'
import invites from './modules/invites'
import permission from './modules/permission'
import reports from './modules/reports'
import settings from './modules/settings'
......@@ -10,6 +11,7 @@ import user from './modules/user'
import userProfile from './modules/userProfile'
import users from './modules/users'
import getters from './getters'
import emoji_packs from './modules/emoji_packs.js'
Vue.use(Vuex)
......@@ -17,13 +19,15 @@ const store = new Vuex.Store({
modules: {
app,
errorLog,
invites,
permission,
reports,
settings,
tagsView,
user,
userProfile,
users
users,
emoji_packs
},
getters
})
......
import { listPacks,
downloadFrom,
reloadEmoji,
createPack,
deletePack,
savePackMetadata,
importFromFS,
updatePackFile } from '@/api/emoji_packs'
import { Message } from 'element-ui'
import Vue from 'vue'
const packs = {
state: {
localPacks: {},
remotePacks: {}
},
mutations: {
SET_LOCAL_PACKS: (state, packs) => {
state.localPacks = packs
},
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(
state.localPacks[name],
'files',
files
)
}
},
actions: {
async SetLocalEmojiPacks({ commit, getters, state }) {
const { data } = await listPacks(getters.authHost)
commit('SET_LOCAL_PACKS', data)
},
async SetRemoteEmojiPacks({ commit, getters, state }, { remoteInstance }) {
const { data } = await listPacks(remoteInstance)
commit('SET_REMOTE_PACKS', data)
},
async DownloadFrom({ commit, getters, state }, { instanceAddress, packName, as }) {
const result = await downloadFrom(getters.authHost, instanceAddress, packName, as, getters.token)
if (result.data === 'ok') {
Message({
message: `Successfully downloaded ${packName}`,
type: 'success',
duration: 5 * 1000
})
}
},
async ReloadEmoji({ commit, getters, state }) {
await reloadEmoji(getters.authHost, getters.token)
},
async ImportFromFS({ commit, getters, state }) {
const result = await importFromFS(getters.authHost, getters.token)
if (result.status === 200) {
const message = result.data.length > 0 ? `Successfully imported ${result.data}` : 'No new packs to import'
Message({
message,
type: 'success',
duration: 5 * 1000
})
}
},
async DeletePack({ commit, getters, state }, { name }) {
await deletePack(getters.authHost, getters.token, name)
},
async CreatePack({ commit, getters, state }, { name }) {
await createPack(getters.authHost, getters.token, name)
},
async UpdateLocalPackVal({ commit, getters, state }, args) {
commit('UPDATE_LOCAL_PACK_VAL', args)
},
async SavePackMetadata({ commit, getters, state }, { packName }) {
const result =
await savePackMetadata(
getters.authHost,
getters.token,
packName,
state.localPacks[packName]['pack']
)
if (result.status === 200) {
Message({
message: `Successfully updated ${packName} metadata`,
type: 'success',
duration: 5 * 1000
})
commit('UPDATE_LOCAL_PACK_PACK', { name: packName, pack: result.data })
}
},
async UpdateAndSavePackFile({ commit, getters, state }, args) {
const result = await updatePackFile(getters.authHost, getters.token, args)
if (result.status === 200) {
const { packName } = args
Message({
message: `Successfully updated ${packName} files`,
type: 'success',
duration: 5 * 1000
})
commit('UPDATE_LOCAL_PACK_FILES', { name: packName, files: result.data })
}
}
}
}
export default packs
import { generateInviteToken, inviteViaEmail, listInviteTokens, revokeToken } from '@/api/invites'
const invites = {
state: {
inviteTokens: [],
loading: false,
newToken: {}
},
mutations: {
SET_LOADING: (state, status) => {
state.loading = status
},
SET_NEW_TOKEN: (state, token) => {
state.newToken = token
},
SET_TOKENS: (state, tokens) => {
state.inviteTokens = tokens
}
},
actions: {
async FetchInviteTokens({ commit, getters }) {
commit('SET_LOADING', true)
const response = await listInviteTokens(getters.authHost, getters.token)
commit('SET_TOKENS', response.data.invites.reverse())
commit('SET_LOADING', false)
},
async GenerateInviteToken({ commit, dispatch, getters }, { maxUse, expiresAt }) {
const { data } = await generateInviteToken(maxUse, expiresAt, getters.authHost, getters.token)
commit('SET_NEW_TOKEN', { token: data.token, maxUse: data.max_use, expiresAt: data.expires_at })
dispatch('FetchInviteTokens')
},
async InviteUserViaEmail({ commit, dispatch, getters }, { email, name }) {
await inviteViaEmail(email, name, getters.authHost, getters.token)
},
RemoveNewToken({ commit }) {
commit('SET_NEW_TOKEN', {})
},
async RevokeToken({ commit, dispatch, getters }, token) {
await revokeToken(token, getters.authHost, getters.token)
dispatch('FetchInviteTokens')
}
}
}
export default invites
import { addRight, createNewAccount, fetchUsers, deleteRight, deleteUser, searchUsers, tagUser, toggleUserActivation, untagUser } from '@/api/users'
import { addRight, createNewAccount, deleteRight, deleteUser, fetchUsers, getPasswordResetToken, searchUsers, tagUser, toggleUserActivation, untagUser } from '@/api/users'
const users = {
state: {
......@@ -12,6 +12,10 @@ const users = {
external: false,
active: false,
deactivated: false
},
passwordResetToken: {
token: '',
link: ''
}
},
mutations: {
......@@ -23,7 +27,9 @@ const users = {
},
SWAP_USER: (state, updatedUser) => {
const updated = state.fetchedUsers.map(user => user.id === updatedUser.id ? updatedUser : user)
state.fetchedUsers = updated.sort((a, b) => a.nickname.localeCompare(b.nickname))
state.fetchedUsers = updated
.map(user => user.nickname ? user : { ...user, nickname: '' })
.sort((a, b) => a.nickname.localeCompare(b.nickname))
},
SWAP_USERS: (state, users) => {
const usersWithoutSwapped = users.reduce((acc, user) => {
......@@ -43,6 +49,10 @@ const users = {
SET_PAGE_SIZE: (state, pageSize) => {
state.pageSize = pageSize
},
SET_PASSWORD_RESET_TOKEN: (state, { token, link }) => {
state.passwordResetToken.token = token
state.passwordResetToken.link = link
},
SET_SEARCH_QUERY: (state, query) => {
state.searchQuery = query
},
......@@ -79,6 +89,13 @@ const users = {
const response = await fetchUsers(filters, getters.authHost, getters.token, page)
loadUsers(commit, page, response.data)
},
async GetPasswordResetToken({ commit, state, getters }, nickname) {
const { data } = await getPasswordResetToken(nickname, getters.authHost, getters.token)
commit('SET_PASSWORD_RESET_TOKEN', data)
},
RemovePasswordToken({ commit }) {
commit('SET_PASSWORD_RESET_TOKEN', { link: '', token: '' })
},
async RemoveTag({ commit, getters }, { users, tag }) {
const nicknames = users.map(user => user.nickname)
await untagUser(nicknames, [tag], getters.authHost, getters.token)
......
......@@ -10,9 +10,9 @@ const service = axios.create({
service.interceptors.response.use(
response => response,
error => {
console.log('err' + error)
console.log('Error ' + error)
Message({
message: error.message,
message: `${error.message} - ${error.response.data}`,
type: 'error',
duration: 5 * 1000
})
......
<template>
<div>
<h2>{{ name }}</h2>
<prop-editing-row name="Share pack">
<el-switch v-model="share" :disabled="!isLocal" />
</prop-editing-row>
<prop-editing-row name="Homepage">
<el-input v-if="isLocal" v-model="homepage" />
<el-input v-else :value="homepage" />
</prop-editing-row>
<prop-editing-row name="Description">
<el-input v-if="isLocal" :rows="2" v-model="description" type="textarea" />
<el-input v-else :rows="2" :value="description" type="textarea" />
</prop-editing-row>
<prop-editing-row name="License">
<el-input v-if="isLocal" v-model="license" />
<el-input v-else :value="license" />
</prop-editing-row>
<prop-editing-row name="Fallback source">
<el-input v-if="isLocal" v-model="fallbackSrc" />
<el-input v-else :value="fallbackSrc" />
</prop-editing-row>
<prop-editing-row v-if="fallbackSrc && fallbackSrc.trim() !== ''" name="Fallback source SHA">
{{ pack.pack["fallback-src-sha256"] }}
</prop-editing-row>
<el-button v-if="isLocal" type="success" @click="savePackMetadata">Save pack metadata</el-button>
<el-collapse v-model="shownPackEmoji" class="contents-collapse">
<el-collapse-item :name="name" title="Show pack contents">
<new-emoji-uploader v-if="isLocal" :pack-name="name" class="new-emoji-uploader" />
<h4>Manage existing emoji</h4>
<single-emoji-editor
v-for="(file, ename) in pack.files"
:key="ename"
:host="host"
:pack-name="name"
:name="ename"
:file="file"
:is-local="isLocal" />
</el-collapse-item>
</el-collapse>
<div v-if="!isLocal" class="shared-pack-dl-box">
<div>
This will download the "{{ name }}" pack to the current instance under the name
"{{ downloadSharedAs.trim() === '' ? name : downloadSharedAs }}" (can be changed below).
It will then be usable and shareable from the current instance.
</div>
<el-button type="primary" @click="downloadFromInstance">
Download shared pack to current instance
</el-button>
<el-input v-model="downloadSharedAs" class="dl-as-input" placeholder="Download as (optional)" />
</div>
<el-link
v-if="pack.pack['can-download']"
:href="`//${host}/api/pleroma/emoji/packs/${name}/download_shared`"
type="primary"
target="_blank">
Download pack archive
</el-link>
<div v-if="isLocal" class="pack-actions">
<el-button type="danger" @click="deletePack">
Delete the local pack
</el-button>
</div>
</div>
</template>
<style>
.shared-pack-dl-box {
margin: 1em;
}
.dl-as-input {
margin: 1em;
max-width: 30%;
}
.contents-collapse {
margin: 1em;
}
.pack-actions {
margin-top: 1em;
}
.new-emoji-uploader {
margin-bottom: 3em;
}
</style>
<script>
import PropEditingRow from './PropertyEditingRow.vue'
import SingleEmojiEditor from './SingleEmojiEditor.vue'
import NewEmojiUploader from './NewEmojiUploader.vue'
export default {
components: { PropEditingRow, SingleEmojiEditor, NewEmojiUploader },
props: {
name: {
type: String,
required: true
},
pack: {
type: Object,
required: true
},
host: {
type: String,
required: true
},
isLocal: {
type: Boolean,
required: true
}
},
data() {
return {
shownPackEmoji: [],
downloadSharedAs: ''
}
},
computed: {
share: {
get() { return this.pack.pack['share-files'] },
set(value) {
this.$store.dispatch(
'UpdateLocalPackVal',
{ name: this.name, key: 'share-files', value }
)
}
},
homepage: {
get() { return this.pack.pack['homepage'] },
set(value) {
this.$store.dispatch(
'UpdateLocalPackVal',
{ name: this.name, key: 'homepage', value }
)
}
},
description: {
get() { return this.pack.pack['description'] },
set(value) {
this.$store.dispatch(
'UpdateLocalPackVal',
{ name: this.name, key: 'description', value }
)
}
},
license: {
get() { return this.pack.pack['license'] },
set(value) {
this.$store.dispatch(
'UpdateLocalPackVal',
{ name: this.name, key: 'license', value }
)
}
},
fallbackSrc: {
get() { return this.pack.pack['fallback-src'] },
set(value) {
if (value.trim() !== '') {
this.$store.dispatch(
'UpdateLocalPackVal',
{ name: this.name, key: 'fallback-src', value }
)
} else {
this.$store.dispatch(
'UpdateLocalPackVal',
{ name: this.name, key: 'fallback-src', value: null }
)
this.$store.dispatch(
'UpdateLocalPackVal',
{ name: this.name, key: 'fallback-src-sha256', value: null }
)
}
}
}
},
methods: {
downloadFromInstance() {
this.$store.dispatch(
'DownloadFrom',
{ instanceAddress: this.host, packName: this.name, as: this.downloadSharedAs }
).then(() => this.$store.dispatch('ReloadEmoji'))
.then(() => this.$store.dispatch('SetLocalEmojiPacks'))
},
deletePack() {
this.$confirm('This will delete the pack, are you sure?', 'Warning', {
confirmButtonText: 'Yes, delete the pack',
cancelButtonText: 'No, leave it be',
type: 'warning'
}).then(() => {
this.$store.dispatch('DeletePack', { name: this.name })
.then(() => this.$store.dispatch('ReloadEmoji'))
.then(() => this.$store.dispatch('SetLocalEmojiPacks'))
}).catch(() => {})
},
savePackMetadata() {
this.$store.dispatch('SavePackMetadata', { packName: this.name })
}
}
}
</script>
<template>
<div>
<h4>Add new emoji to the pack</h4>
<el-row :gutter="20">
<el-col :span="4" class="new-emoji-col">
<el-input v-model="shortcode" placeholder="Shortcode" />
</el-col>
<el-col :span="8">
<div>
<h5>Upload a file</h5>
</div>
File name
<el-input v-model="customFileName" size="mini" placeholder="Custom file name (optional)"/>
<input ref="fileUpload" type="file" accept="image/*" >
<div class="or">
or
</div>
<div>
<h5>Enter a URL</h5>
</div>
<el-input v-model="imageUploadURL" placeholder="Image URL" />
<small>
(If both are filled, the file is used)
</small>
</el-col>
<el-col :span="4" class="new-emoji-col">
<el-button :disabled="shortcode.trim() == ''" @click="upload">Upload</el-button>
</el-col>
</el-row>
</div>
</template>
<style>
.new-emoji-col {
margin-top: 8em;
}
.or {
margin: 1em;
}
</style>
<script>
export default {
props: {
packName: {
type: String,
required: true
}
},
data() {
return {
shortcode: '',
imageUploadURL: '',
customFileName: ''
}
},
methods: {
upload() {
let file = null
if (this.$refs.fileUpload.files.length > 0) {
file = this.$refs.fileUpload.files[0]
} else if (this.imageUploadURL.trim() !== '') {
file = this.imageUploadURL
}
if (file !== null) {
this.$store.dispatch('UpdateAndSavePackFile', {
action: 'add',
packName: this.packName,
shortcode: this.shortcode,
file: file,
fileName: this.customFileName
}).then(() => {
this.shortcode = ''
this.imageUploadURL = ''
this.$store.dispatch('ReloadEmoji')
})
}
}
}
}
</script>