forked from AkkomaGang/admin-fe
Generate invite tokens from admin-fe
This commit is contained in:
parent
7e159ac147
commit
81510916b5
49 changed files with 1030 additions and 780 deletions
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -14,20 +14,25 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
### Added
|
### 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)
|
- 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)
|
||||||
- add ability to disable certain features (settings/reports)
|
- adds ability to disable certain features (settings/reports/invites)
|
||||||
- add sign in via PleromaFE
|
- 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
|
### Changed
|
||||||
|
|
||||||
- removes "Dashboard" from dropdown menu
|
- removes "Dashboard" from dropdown menu
|
||||||
- makes all single selects clearable and allow to enter custom values in all multiple selects
|
- 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
|
### Fixed
|
||||||
|
|
||||||
- converts maps and structs to JS objects, not array of tuples when wrapping config
|
- converts maps and structs to JS objects, not array of tuples when wrapping config
|
||||||
- changes type of IP value from string to number
|
- changes type of IP value from string to number
|
||||||
|
- updates error handling for users and invites modules
|
||||||
|
|
||||||
## [1.0.1] - 2019-08-15
|
## [1.0.1] - 2019-08-15
|
||||||
|
|
||||||
|
|
35
src/api/__mocks__/invites.js
Normal file
35
src/api/__mocks__/invites.js
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
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) {
|
export async function toggleUserActivation(nickname, authHost, token) {
|
||||||
const response = users.find(user => user.nickname === nickname)
|
const response = users.find(user => user.nickname === nickname)
|
||||||
return Promise.resolve({ data: { ...response, deactivated: !response.deactivated }})
|
return Promise.resolve({ data: { ...response, deactivated: !response.deactivated }})
|
||||||
|
|
46
src/api/invites.js
Normal file
46
src/api/invites.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
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',
|
url: '/api/pleroma/admin/users',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
headers: authHeaders(token),
|
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) {
|
export async function searchUsers(query, filters, authHost, token, page = 1) {
|
||||||
return await request({
|
return await request({
|
||||||
baseURL: baseName(authHost),
|
baseURL: baseName(authHost),
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,51 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,136 +0,0 @@
|
||||||
<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>
|
|
|
@ -202,7 +202,7 @@ export default {
|
||||||
disableAnySubscriptionForMultiple: 'Disallow following users at all',
|
disableAnySubscriptionForMultiple: 'Disallow following users at all',
|
||||||
selectUsers: 'Select users to apply actions to multiple users',
|
selectUsers: 'Select users to apply actions to multiple users',
|
||||||
moderateUsers: 'Moderate multiple users',
|
moderateUsers: 'Moderate multiple users',
|
||||||
createAccount: 'Create new user account',
|
createAccount: 'Create new account',
|
||||||
apply: 'apply',
|
apply: 'apply',
|
||||||
remove: 'remove',
|
remove: 'remove',
|
||||||
grantRightConfirmation: 'Are you sure you want to grant {right} rights to all selected users?',
|
grantRightConfirmation: 'Are you sure you want to grant {right} rights to all selected users?',
|
||||||
|
@ -220,12 +220,15 @@ export default {
|
||||||
email: 'E-mail',
|
email: 'E-mail',
|
||||||
password: 'Password',
|
password: 'Password',
|
||||||
create: 'Create',
|
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',
|
emptyEmailError: 'Please input the e-mail',
|
||||||
invalidEmailError: 'Please input valid e-mail',
|
invalidEmailError: 'Please input valid e-mail',
|
||||||
emptyPasswordError: 'Please input the password',
|
emptyPasswordError: 'Please input the password',
|
||||||
emptyNicknameError: 'Please input the username',
|
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: {
|
userProfile: {
|
||||||
tags: 'Tags',
|
tags: 'Tags',
|
||||||
|
@ -303,5 +306,31 @@ export default {
|
||||||
database: 'Database',
|
database: 'Database',
|
||||||
other: 'Other',
|
other: 'Other',
|
||||||
success: 'Settings changed successfully!'
|
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',
|
path: 'index',
|
||||||
component: () => import('@/views/settings/index'),
|
component: () => import('@/views/settings/index'),
|
||||||
name: 'Settings',
|
name: 'Settings',
|
||||||
meta: { title: 'settings', icon: 'settings', noCache: true }
|
meta: { title: 'Settings', icon: 'settings', noCache: true }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,21 @@ const reports = {
|
||||||
path: 'index',
|
path: 'index',
|
||||||
component: () => import('@/views/reports/index'),
|
component: () => import('@/views/reports/index'),
|
||||||
name: 'Reports',
|
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 }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -108,12 +122,13 @@ export const asyncRouterMap = [
|
||||||
path: 'index',
|
path: 'index',
|
||||||
component: () => import('@/views/users/index'),
|
component: () => import('@/views/users/index'),
|
||||||
name: 'Users',
|
name: 'Users',
|
||||||
meta: { title: 'users', icon: 'peoples', noCache: true }
|
meta: { title: 'Users', icon: 'peoples', noCache: true }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
...(settingsDisabled ? [] : [settings]),
|
...(settingsDisabled ? [] : [settings]),
|
||||||
...(reportsDisabled ? [] : [reports]),
|
...(reportsDisabled ? [] : [reports]),
|
||||||
|
...(invitesDisabled ? [] : [invites]),
|
||||||
...(emojiPacksDisabled ? [] : [emojiPacks]),
|
...(emojiPacksDisabled ? [] : [emojiPacks]),
|
||||||
{
|
{
|
||||||
path: '/users/:id',
|
path: '/users/:id',
|
||||||
|
|
|
@ -2,6 +2,7 @@ import Vue from 'vue'
|
||||||
import Vuex from 'vuex'
|
import Vuex from 'vuex'
|
||||||
import app from './modules/app'
|
import app from './modules/app'
|
||||||
import errorLog from './modules/errorLog'
|
import errorLog from './modules/errorLog'
|
||||||
|
import invites from './modules/invites'
|
||||||
import permission from './modules/permission'
|
import permission from './modules/permission'
|
||||||
import reports from './modules/reports'
|
import reports from './modules/reports'
|
||||||
import settings from './modules/settings'
|
import settings from './modules/settings'
|
||||||
|
@ -18,6 +19,7 @@ const store = new Vuex.Store({
|
||||||
modules: {
|
modules: {
|
||||||
app,
|
app,
|
||||||
errorLog,
|
errorLog,
|
||||||
|
invites,
|
||||||
permission,
|
permission,
|
||||||
reports,
|
reports,
|
||||||
settings,
|
settings,
|
||||||
|
|
45
src/store/modules/invites.js
Normal file
45
src/store/modules/invites.js
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
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
|
|
@ -1,4 +1,4 @@
|
||||||
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 = {
|
const users = {
|
||||||
state: {
|
state: {
|
||||||
|
@ -12,6 +12,10 @@ const users = {
|
||||||
external: false,
|
external: false,
|
||||||
active: false,
|
active: false,
|
||||||
deactivated: false
|
deactivated: false
|
||||||
|
},
|
||||||
|
passwordResetToken: {
|
||||||
|
token: '',
|
||||||
|
link: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
|
@ -23,7 +27,9 @@ const users = {
|
||||||
},
|
},
|
||||||
SWAP_USER: (state, updatedUser) => {
|
SWAP_USER: (state, updatedUser) => {
|
||||||
const updated = state.fetchedUsers.map(user => user.id === updatedUser.id ? updatedUser : user)
|
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) => {
|
SWAP_USERS: (state, users) => {
|
||||||
const usersWithoutSwapped = users.reduce((acc, user) => {
|
const usersWithoutSwapped = users.reduce((acc, user) => {
|
||||||
|
@ -43,6 +49,10 @@ const users = {
|
||||||
SET_PAGE_SIZE: (state, pageSize) => {
|
SET_PAGE_SIZE: (state, pageSize) => {
|
||||||
state.pageSize = pageSize
|
state.pageSize = pageSize
|
||||||
},
|
},
|
||||||
|
SET_PASSWORD_RESET_TOKEN: (state, { token, link }) => {
|
||||||
|
state.passwordResetToken.token = token
|
||||||
|
state.passwordResetToken.link = link
|
||||||
|
},
|
||||||
SET_SEARCH_QUERY: (state, query) => {
|
SET_SEARCH_QUERY: (state, query) => {
|
||||||
state.searchQuery = query
|
state.searchQuery = query
|
||||||
},
|
},
|
||||||
|
@ -79,6 +89,13 @@ const users = {
|
||||||
const response = await fetchUsers(filters, getters.authHost, getters.token, page)
|
const response = await fetchUsers(filters, getters.authHost, getters.token, page)
|
||||||
loadUsers(commit, page, response.data)
|
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 }) {
|
async RemoveTag({ commit, getters }, { users, tag }) {
|
||||||
const nicknames = users.map(user => user.nickname)
|
const nicknames = users.map(user => user.nickname)
|
||||||
await untagUser(nicknames, [tag], getters.authHost, getters.token)
|
await untagUser(nicknames, [tag], getters.authHost, getters.token)
|
||||||
|
|
|
@ -15,14 +15,6 @@ const steps = [
|
||||||
position: 'bottom'
|
position: 'bottom'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
element: '.screenfull',
|
|
||||||
popover: {
|
|
||||||
title: 'Screenfull',
|
|
||||||
description: 'Bring the page into fullscreen',
|
|
||||||
position: 'left'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
element: '.international-icon',
|
element: '.international-icon',
|
||||||
popover: {
|
popover: {
|
||||||
|
|
328
src/views/invites/index.vue
Normal file
328
src/views/invites/index.vue
Normal file
|
@ -0,0 +1,328 @@
|
||||||
|
<template>
|
||||||
|
<div class="invites-container">
|
||||||
|
<h1>{{ $t('invites.inviteTokens') }}</h1>
|
||||||
|
<div class="actions-container">
|
||||||
|
<el-button class="create-invite-token" @click="createTokenDialogVisible = true">
|
||||||
|
<span>
|
||||||
|
<i class="icon el-icon-plus"/>
|
||||||
|
{{ $t('invites.createInviteToken') }}
|
||||||
|
</span>
|
||||||
|
</el-button>
|
||||||
|
<el-button class="invite-via-email" @click="inviteUserDialogVisible = true">
|
||||||
|
<span>
|
||||||
|
<i class="icon el-icon-message"/>
|
||||||
|
{{ $t('invites.inviteUserViaEmail') }}
|
||||||
|
</span>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<el-dialog
|
||||||
|
:visible.sync="createTokenDialogVisible"
|
||||||
|
:show-close="false"
|
||||||
|
:title="$t('invites.createInviteToken')"
|
||||||
|
custom-class="create-new-token-dialog">
|
||||||
|
<el-form ref="newTokenForm" :model="newTokenForm" :label-width="getLabelWidth" status-icon>
|
||||||
|
<el-form-item :label="$t('invites.maxUse')">
|
||||||
|
<el-input-number
|
||||||
|
v-model="newTokenForm.maxUse"
|
||||||
|
:min="0"
|
||||||
|
:size="isDesktop ? 'medium' : 'small'"
|
||||||
|
name="maxUse"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('invites.expiresAt')">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="newTokenForm.expiresAt"
|
||||||
|
:placeholder="$t('invites.pickDate')"
|
||||||
|
class="pick-date"
|
||||||
|
type="date"
|
||||||
|
name="date"
|
||||||
|
value-format="yyyy-MM-dd"/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<span slot="footer">
|
||||||
|
<el-button @click="closeDialogWindow">{{ $t('invites.cancel') }}</el-button>
|
||||||
|
<el-button type="primary" @click="createToken">{{ $t('invites.create') }}</el-button>
|
||||||
|
</span>
|
||||||
|
<el-card v-if="'token' in newToken">
|
||||||
|
<div slot="header" class="clearfix">
|
||||||
|
<span>{{ $t('invites.tokenCreated') }}</span>
|
||||||
|
</div>
|
||||||
|
<p>{{ this.$t('invites.token') }}: {{ newToken.token }}</p>
|
||||||
|
<p>{{ this.$t('invites.maxUse') }}: {{ newToken.maxUse }}</p>
|
||||||
|
<p>{{ this.$t('invites.expiresAt') }}: {{ newToken.expiresAt }}</p>
|
||||||
|
</el-card>
|
||||||
|
</el-dialog>
|
||||||
|
<el-dialog
|
||||||
|
:visible.sync="inviteUserDialogVisible"
|
||||||
|
:show-close="false"
|
||||||
|
:title="$t('invites.sendRegistration')"
|
||||||
|
custom-class="invite-via-email-dialog">
|
||||||
|
<div>
|
||||||
|
<p class="info">{{ $t('invites.inviteViaEmailAlert') }}</p>
|
||||||
|
<el-form ref="inviteUserForm" :model="inviteUserForm" :rules="rules" :label-width="getLabelWidth" status-icon>
|
||||||
|
<el-form-item :label="$t('invites.email')" prop="email">
|
||||||
|
<el-input v-model="inviteUserForm.email" name="email" type="email" autofocus/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('invites.name')" prop="name">
|
||||||
|
<el-input v-model="inviteUserForm.name" name="name"/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
<span slot="footer">
|
||||||
|
<el-button @click="closeDialogWindow">{{ $t('invites.cancel') }}</el-button>
|
||||||
|
<el-button type="primary" @click="inviteUserViaEmail">{{ $t('invites.create') }}</el-button>
|
||||||
|
</span>
|
||||||
|
</el-dialog>
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="tokens"
|
||||||
|
:default-sort = "{prop: 'used', order: 'ascending'}"
|
||||||
|
class="invite-token-table">
|
||||||
|
<el-table-column
|
||||||
|
v-if="isDesktop"
|
||||||
|
:label="$t('invites.id')"
|
||||||
|
min-width="60"
|
||||||
|
prop="id"
|
||||||
|
sortable/>
|
||||||
|
<el-table-column
|
||||||
|
:label="$t('invites.token')"
|
||||||
|
:min-width="isDesktop ? 350 : 125"
|
||||||
|
prop="token"/>
|
||||||
|
<el-table-column
|
||||||
|
v-if="isDesktop"
|
||||||
|
:label="$t('invites.expiresAt')"
|
||||||
|
align="center"
|
||||||
|
header-align="center"
|
||||||
|
min-width="110"
|
||||||
|
prop="expires_at"
|
||||||
|
sortable/>
|
||||||
|
<el-table-column
|
||||||
|
:label="$t('invites.maxUse')"
|
||||||
|
align="center"
|
||||||
|
header-align="center"
|
||||||
|
min-width="60"
|
||||||
|
prop="max_use"
|
||||||
|
sortable/>
|
||||||
|
<el-table-column
|
||||||
|
v-if="isDesktop"
|
||||||
|
:label="$t('invites.uses')"
|
||||||
|
align="center"
|
||||||
|
header-align="center"
|
||||||
|
min-width="60"
|
||||||
|
prop="uses"/>
|
||||||
|
<el-table-column
|
||||||
|
:label="$t('invites.used')"
|
||||||
|
:min-width="isDesktop ? 60 : 50"
|
||||||
|
align="center"
|
||||||
|
header-align="center"
|
||||||
|
prop="used"
|
||||||
|
sortable>
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
:label="$t('invites.actions')"
|
||||||
|
:min-width="isDesktop ? 100 : 50"
|
||||||
|
align="center"
|
||||||
|
header-align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button type="text" size="small" @click.native="revokeInviteToken(scope.row.token)">
|
||||||
|
{{ $t('invites.revoke') }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
rules: {
|
||||||
|
email: [
|
||||||
|
{ validator: this.validateEmail, trigger: 'blur' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
newTokenForm: {
|
||||||
|
maxUse: 1,
|
||||||
|
expiresAt: ''
|
||||||
|
},
|
||||||
|
inviteUserForm: {
|
||||||
|
email: '',
|
||||||
|
name: ''
|
||||||
|
},
|
||||||
|
createTokenDialogVisible: false,
|
||||||
|
inviteUserDialogVisible: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
getLabelWidth() {
|
||||||
|
return this.isDesktop ? '100px' : '80px'
|
||||||
|
},
|
||||||
|
isDesktop() {
|
||||||
|
return this.$store.state.app.device === 'desktop'
|
||||||
|
},
|
||||||
|
loading() {
|
||||||
|
return this.$store.state.invites.loading
|
||||||
|
},
|
||||||
|
newToken() {
|
||||||
|
return this.$store.state.invites.newToken
|
||||||
|
},
|
||||||
|
tokens() {
|
||||||
|
return this.$store.state.invites.inviteTokens
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$store.dispatch('FetchInviteTokens')
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
closeDialogWindow() {
|
||||||
|
this.inviteUserDialogVisible = false
|
||||||
|
this.createTokenDialogVisible = false
|
||||||
|
this.$store.dispatch('RemoveNewToken')
|
||||||
|
this.$data.inviteUserForm.email = ''
|
||||||
|
this.$data.inviteUserForm.name = ''
|
||||||
|
},
|
||||||
|
createToken() {
|
||||||
|
this.$store.dispatch('GenerateInviteToken', this.$data.newTokenForm)
|
||||||
|
},
|
||||||
|
async inviteUserViaEmail() {
|
||||||
|
this.$refs['inviteUserForm'].validate(async(valid) => {
|
||||||
|
if (valid) {
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('InviteUserViaEmail', this.$data.inviteUserForm)
|
||||||
|
} catch (_e) {
|
||||||
|
return
|
||||||
|
} finally {
|
||||||
|
this.closeDialogWindow()
|
||||||
|
}
|
||||||
|
this.$message({
|
||||||
|
type: 'success',
|
||||||
|
message: this.$t('invites.emailSent')
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$message({
|
||||||
|
type: 'error',
|
||||||
|
message: this.$t('invites.submitFormError')
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
revokeInviteToken(token) {
|
||||||
|
this.$store.dispatch('RevokeToken', token)
|
||||||
|
},
|
||||||
|
validateEmail(rule, value, callback) {
|
||||||
|
if (value === '') {
|
||||||
|
return callback(new Error(this.$t('invites.emptyEmailError')))
|
||||||
|
} else if (!this.validEmail(value)) {
|
||||||
|
return callback(new Error(this.$t('invites.invalidEmailError')))
|
||||||
|
} else {
|
||||||
|
return callback()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
validEmail(email) {
|
||||||
|
const re = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
|
||||||
|
return re.test(email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style rel='stylesheet/scss' lang='scss'>
|
||||||
|
.invites-container {
|
||||||
|
.actions-container {
|
||||||
|
display: flex;
|
||||||
|
height: 36px;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin: 20px 15px 15px 15px;
|
||||||
|
}
|
||||||
|
.create-invite-token {
|
||||||
|
text-align: left;
|
||||||
|
width: 350px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.create-new-token-dialog {
|
||||||
|
width: 40%
|
||||||
|
}
|
||||||
|
.el-dialog__body {
|
||||||
|
padding: 5px 20px 0 20px
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin: 22px 0 0 15px;
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
.invite-token-table {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 15px;
|
||||||
|
}
|
||||||
|
.invite-via-email {
|
||||||
|
text-align: left;
|
||||||
|
width: 350px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.invite-via-email-dialog {
|
||||||
|
width: 50%
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
color: #666666;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 22px;
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media
|
||||||
|
only screen and (max-width: 760px),
|
||||||
|
(min-device-width: 768px) and (max-device-width: 1024px) {
|
||||||
|
.invites-container {
|
||||||
|
.actions-container {
|
||||||
|
display: flex;
|
||||||
|
height: 82px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin: 15px 10px 7px 10px;
|
||||||
|
}
|
||||||
|
.create-invite-token {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.create-new-token-dialog {
|
||||||
|
width: 85%
|
||||||
|
}
|
||||||
|
.el-date-editor {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
.el-dialog__body {
|
||||||
|
padding: 5px 15px 0 15px
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin: 7px 10px 15px 10px;
|
||||||
|
}
|
||||||
|
.invite-token-table {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.invite-via-email {
|
||||||
|
width: 100%;
|
||||||
|
margin: 10px 0 0 0;
|
||||||
|
}
|
||||||
|
.invite-via-email-dialog {
|
||||||
|
width: 85%
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
margin: 0 0 10px 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.create-invite-token {
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
.invite-via-email {
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -57,8 +57,12 @@ export default {
|
||||||
updateSetting(value, tab, input) {
|
updateSetting(value, tab, input) {
|
||||||
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
||||||
},
|
},
|
||||||
onSubmit() {
|
async onSubmit() {
|
||||||
this.$store.dispatch('SubmitChanges')
|
try {
|
||||||
|
await this.$store.dispatch('SubmitChanges')
|
||||||
|
} catch (e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: i18n.t('settings.success')
|
message: i18n.t('settings.success')
|
||||||
|
|
|
@ -259,8 +259,12 @@ export default {
|
||||||
updateSetting(value, tab, input) {
|
updateSetting(value, tab, input) {
|
||||||
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
||||||
},
|
},
|
||||||
onSubmit() {
|
async onSubmit() {
|
||||||
this.$store.dispatch('SubmitChanges')
|
try {
|
||||||
|
await this.$store.dispatch('SubmitChanges')
|
||||||
|
} catch (e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: i18n.t('settings.success')
|
message: i18n.t('settings.success')
|
||||||
|
|
|
@ -107,8 +107,12 @@ export default {
|
||||||
updateSetting(value, tab, input) {
|
updateSetting(value, tab, input) {
|
||||||
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
||||||
},
|
},
|
||||||
onSubmit() {
|
async onSubmit() {
|
||||||
this.$store.dispatch('SubmitChanges')
|
try {
|
||||||
|
await this.$store.dispatch('SubmitChanges')
|
||||||
|
} catch (e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: i18n.t('settings.success')
|
message: i18n.t('settings.success')
|
||||||
|
|
|
@ -53,8 +53,12 @@ export default {
|
||||||
updateSetting(value, tab, input) {
|
updateSetting(value, tab, input) {
|
||||||
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
||||||
},
|
},
|
||||||
onSubmit() {
|
async onSubmit() {
|
||||||
this.$store.dispatch('SubmitChanges')
|
try {
|
||||||
|
await this.$store.dispatch('SubmitChanges')
|
||||||
|
} catch (e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: i18n.t('settings.success')
|
message: i18n.t('settings.success')
|
||||||
|
|
|
@ -157,8 +157,12 @@ export default {
|
||||||
updateSetting(value, tab, input) {
|
updateSetting(value, tab, input) {
|
||||||
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
||||||
},
|
},
|
||||||
onSubmit() {
|
async onSubmit() {
|
||||||
this.$store.dispatch('SubmitChanges')
|
try {
|
||||||
|
await this.$store.dispatch('SubmitChanges')
|
||||||
|
} catch (e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: i18n.t('settings.success')
|
message: i18n.t('settings.success')
|
||||||
|
|
|
@ -246,8 +246,12 @@ export default {
|
||||||
updateSetting(value, tab, input) {
|
updateSetting(value, tab, input) {
|
||||||
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
||||||
},
|
},
|
||||||
onSubmit() {
|
async onSubmit() {
|
||||||
this.$store.dispatch('SubmitChanges')
|
try {
|
||||||
|
await this.$store.dispatch('SubmitChanges')
|
||||||
|
} catch (e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: i18n.t('settings.success')
|
message: i18n.t('settings.success')
|
||||||
|
|
|
@ -82,8 +82,12 @@ export default {
|
||||||
updateSetting(value, tab, input) {
|
updateSetting(value, tab, input) {
|
||||||
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
||||||
},
|
},
|
||||||
onSubmit() {
|
async onSubmit() {
|
||||||
this.$store.dispatch('SubmitChanges')
|
try {
|
||||||
|
await this.$store.dispatch('SubmitChanges')
|
||||||
|
} catch (e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: i18n.t('settings.success')
|
message: i18n.t('settings.success')
|
||||||
|
|
|
@ -419,8 +419,12 @@ export default {
|
||||||
updateSetting(value, tab, input) {
|
updateSetting(value, tab, input) {
|
||||||
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
||||||
},
|
},
|
||||||
onSubmit() {
|
async onSubmit() {
|
||||||
this.$store.dispatch('SubmitChanges')
|
try {
|
||||||
|
await this.$store.dispatch('SubmitChanges')
|
||||||
|
} catch (e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: i18n.t('settings.success')
|
message: i18n.t('settings.success')
|
||||||
|
|
|
@ -43,8 +43,12 @@ export default {
|
||||||
updateSetting(value, tab, input) {
|
updateSetting(value, tab, input) {
|
||||||
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
||||||
},
|
},
|
||||||
onSubmit() {
|
async onSubmit() {
|
||||||
this.$store.dispatch('SubmitChanges')
|
try {
|
||||||
|
await this.$store.dispatch('SubmitChanges')
|
||||||
|
} catch (e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: i18n.t('settings.success')
|
message: i18n.t('settings.success')
|
||||||
|
|
|
@ -161,8 +161,12 @@ export default {
|
||||||
updateSetting(value, tab, input) {
|
updateSetting(value, tab, input) {
|
||||||
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
||||||
},
|
},
|
||||||
onSubmit() {
|
async onSubmit() {
|
||||||
this.$store.dispatch('SubmitChanges')
|
try {
|
||||||
|
await this.$store.dispatch('SubmitChanges')
|
||||||
|
} catch (e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: i18n.t('settings.success')
|
message: i18n.t('settings.success')
|
||||||
|
|
|
@ -396,8 +396,12 @@ export default {
|
||||||
updateSetting(value, tab, input) {
|
updateSetting(value, tab, input) {
|
||||||
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
||||||
},
|
},
|
||||||
onSubmit() {
|
async onSubmit() {
|
||||||
this.$store.dispatch('SubmitChanges')
|
try {
|
||||||
|
await this.$store.dispatch('SubmitChanges')
|
||||||
|
} catch (e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: i18n.t('settings.success')
|
message: i18n.t('settings.success')
|
||||||
|
|
|
@ -72,8 +72,12 @@ export default {
|
||||||
updateSetting(value, tab, input) {
|
updateSetting(value, tab, input) {
|
||||||
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
||||||
},
|
},
|
||||||
onSubmit() {
|
async onSubmit() {
|
||||||
this.$store.dispatch('SubmitChanges')
|
try {
|
||||||
|
await this.$store.dispatch('SubmitChanges')
|
||||||
|
} catch (e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: i18n.t('settings.success')
|
message: i18n.t('settings.success')
|
||||||
|
|
|
@ -212,8 +212,12 @@ export default {
|
||||||
updateSetting(value, tab, input) {
|
updateSetting(value, tab, input) {
|
||||||
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
||||||
},
|
},
|
||||||
onSubmit() {
|
async onSubmit() {
|
||||||
this.$store.dispatch('SubmitChanges')
|
try {
|
||||||
|
await this.$store.dispatch('SubmitChanges')
|
||||||
|
} catch (e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: i18n.t('settings.success')
|
message: i18n.t('settings.success')
|
||||||
|
|
|
@ -257,8 +257,12 @@ export default {
|
||||||
updateSetting(value, tab, input) {
|
updateSetting(value, tab, input) {
|
||||||
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
||||||
},
|
},
|
||||||
onSubmit() {
|
async onSubmit() {
|
||||||
this.$store.dispatch('SubmitChanges')
|
try {
|
||||||
|
await this.$store.dispatch('SubmitChanges')
|
||||||
|
} catch (e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: i18n.t('settings.success')
|
message: i18n.t('settings.success')
|
||||||
|
|
|
@ -249,8 +249,12 @@ export default {
|
||||||
updateSetting(value, tab, input) {
|
updateSetting(value, tab, input) {
|
||||||
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
||||||
},
|
},
|
||||||
onSubmit() {
|
async onSubmit() {
|
||||||
this.$store.dispatch('SubmitChanges')
|
try {
|
||||||
|
await this.$store.dispatch('SubmitChanges')
|
||||||
|
} catch (e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: i18n.t('settings.success')
|
message: i18n.t('settings.success')
|
||||||
|
|
|
@ -126,8 +126,12 @@ export default {
|
||||||
updateSetting(value, tab, input) {
|
updateSetting(value, tab, input) {
|
||||||
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
||||||
},
|
},
|
||||||
onSubmit() {
|
async onSubmit() {
|
||||||
this.$store.dispatch('SubmitChanges')
|
try {
|
||||||
|
await this.$store.dispatch('SubmitChanges')
|
||||||
|
} catch (e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: i18n.t('settings.success')
|
message: i18n.t('settings.success')
|
||||||
|
|
|
@ -72,8 +72,12 @@ export default {
|
||||||
updateSetting(value, tab, input) {
|
updateSetting(value, tab, input) {
|
||||||
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
||||||
},
|
},
|
||||||
onSubmit() {
|
async onSubmit() {
|
||||||
this.$store.dispatch('SubmitChanges')
|
try {
|
||||||
|
await this.$store.dispatch('SubmitChanges')
|
||||||
|
} catch (e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: i18n.t('settings.success')
|
message: i18n.t('settings.success')
|
||||||
|
|
|
@ -65,8 +65,12 @@ export default {
|
||||||
}, {})
|
}, {})
|
||||||
this.updateSetting(updatedValue, 'types', 'value')
|
this.updateSetting(updatedValue, 'types', 'value')
|
||||||
},
|
},
|
||||||
onSubmit() {
|
async onSubmit() {
|
||||||
this.$store.dispatch('SubmitChanges')
|
try {
|
||||||
|
await this.$store.dispatch('SubmitChanges')
|
||||||
|
} catch (e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: i18n.t('settings.success')
|
message: i18n.t('settings.success')
|
||||||
|
|
|
@ -379,8 +379,12 @@ export default {
|
||||||
updateSetting(value, tab, input) {
|
updateSetting(value, tab, input) {
|
||||||
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
||||||
},
|
},
|
||||||
onSubmit() {
|
async onSubmit() {
|
||||||
this.$store.dispatch('SubmitChanges')
|
try {
|
||||||
|
await this.$store.dispatch('SubmitChanges')
|
||||||
|
} catch (e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: i18n.t('settings.success')
|
message: i18n.t('settings.success')
|
||||||
|
|
|
@ -207,8 +207,12 @@ export default {
|
||||||
updateSetting(value, tab, input) {
|
updateSetting(value, tab, input) {
|
||||||
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
||||||
},
|
},
|
||||||
onSubmit() {
|
async onSubmit() {
|
||||||
this.$store.dispatch('SubmitChanges')
|
try {
|
||||||
|
await this.$store.dispatch('SubmitChanges')
|
||||||
|
} catch (e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: i18n.t('settings.success')
|
message: i18n.t('settings.success')
|
||||||
|
|
|
@ -41,8 +41,12 @@ export default {
|
||||||
updateSetting(value, tab, input) {
|
updateSetting(value, tab, input) {
|
||||||
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
|
||||||
},
|
},
|
||||||
onSubmit() {
|
async onSubmit() {
|
||||||
this.$store.dispatch('SubmitChanges')
|
try {
|
||||||
|
await this.$store.dispatch('SubmitChanges')
|
||||||
|
} catch (e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: i18n.t('settings.success')
|
message: i18n.t('settings.success')
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="app-container">
|
|
||||||
|
|
||||||
<div class="filter-container">
|
|
||||||
<el-checkbox-group v-model="checkboxVal">
|
|
||||||
<el-checkbox label="apple">apple</el-checkbox>
|
|
||||||
<el-checkbox label="banana">banana</el-checkbox>
|
|
||||||
<el-checkbox label="orange">orange</el-checkbox>
|
|
||||||
</el-checkbox-group>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-table :data="tableData" :key="key" border fit highlight-current-row style="width: 100%">
|
|
||||||
<el-table-column prop="name" label="fruitName" width="180"/>
|
|
||||||
<el-table-column v-for="fruit in formThead" :key="fruit" :label="fruit">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
{{ scope.row[fruit] }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const defaultFormThead = ['apple', 'banana']
|
|
||||||
|
|
||||||
export default {
|
|
||||||
data: function() {
|
|
||||||
return {
|
|
||||||
tableData: [
|
|
||||||
{
|
|
||||||
name: 'fruit-1',
|
|
||||||
apple: 'apple-10',
|
|
||||||
banana: 'banana-10',
|
|
||||||
orange: 'orange-10'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fruit-2',
|
|
||||||
apple: 'apple-20',
|
|
||||||
banana: 'banana-20',
|
|
||||||
orange: 'orange-20'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
key: 1, // table key
|
|
||||||
formTheadOptions: ['apple', 'banana', 'orange'],
|
|
||||||
checkboxVal: defaultFormThead, // checkboxVal
|
|
||||||
formThead: defaultFormThead // 默认表头 Default header
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
checkboxVal(valArr) {
|
|
||||||
this.formThead = this.formTheadOptions.filter(i => valArr.indexOf(i) >= 0)
|
|
||||||
this.key = this.key + 1// 为了保证table 每次都会重渲 In order to ensure the table will be re-rendered each time
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="app-container">
|
|
||||||
<div style="margin:0 0 5px 20px">{{ $t('table.dynamicTips1') }}</div>
|
|
||||||
<fixed-thead/>
|
|
||||||
|
|
||||||
<div style="margin:30px 0 5px 20px">{{ $t('table.dynamicTips2') }}</div>
|
|
||||||
<unfixed-thead/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import fixedThead from './fixedThead'
|
|
||||||
import unfixedThead from './unfixedThead'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'DynamicTable',
|
|
||||||
components: { fixedThead, unfixedThead }
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="app-container">
|
|
||||||
|
|
||||||
<div class="filter-container">
|
|
||||||
<el-checkbox-group v-model="formThead">
|
|
||||||
<el-checkbox label="apple">apple</el-checkbox>
|
|
||||||
<el-checkbox label="banana">banana</el-checkbox>
|
|
||||||
<el-checkbox label="orange">orange</el-checkbox>
|
|
||||||
</el-checkbox-group>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-table :data="tableData" border fit highlight-current-row style="width: 100%">
|
|
||||||
<el-table-column prop="name" label="fruitName" width="180"/>
|
|
||||||
<el-table-column v-for="fruit in formThead" :key="fruit" :label="fruit">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
{{ scope.row[fruit] }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
data: function() {
|
|
||||||
return {
|
|
||||||
tableData: [
|
|
||||||
{
|
|
||||||
name: 'fruit-1',
|
|
||||||
apple: 'apple-10',
|
|
||||||
banana: 'banana-10',
|
|
||||||
orange: 'orange-10'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fruit-2',
|
|
||||||
apple: 'apple-20',
|
|
||||||
banana: 'banana-20',
|
|
||||||
orange: 'orange-20'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
formThead: ['apple', 'banana']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,48 +0,0 @@
|
||||||
/**
|
|
||||||
* @Author: jianglei
|
|
||||||
* @Date: 2017-10-12 12:06:49
|
|
||||||
*/
|
|
||||||
'use strict'
|
|
||||||
import Vue from 'vue'
|
|
||||||
export default function treeToArray(data, expandAll, parent, level, item) {
|
|
||||||
const marLTemp = []
|
|
||||||
let tmp = []
|
|
||||||
Array.from(data).forEach(function(record) {
|
|
||||||
if (record._expanded === undefined) {
|
|
||||||
Vue.set(record, '_expanded', expandAll)
|
|
||||||
}
|
|
||||||
let _level = 1
|
|
||||||
if (level !== undefined && level !== null) {
|
|
||||||
_level = level + 1
|
|
||||||
}
|
|
||||||
Vue.set(record, '_level', _level)
|
|
||||||
// 如果有父元素
|
|
||||||
if (parent) {
|
|
||||||
Vue.set(record, 'parent', parent)
|
|
||||||
// 如果父元素有偏移量,需要计算在this的偏移量中
|
|
||||||
// 偏移量还与前面同级元素有关,需要加上前面所有元素的长度和
|
|
||||||
if (!marLTemp[_level]) {
|
|
||||||
marLTemp[_level] = 0
|
|
||||||
}
|
|
||||||
Vue.set(record, '_marginLeft', marLTemp[_level] + parent._marginLeft)
|
|
||||||
Vue.set(record, '_width', record[item] / parent[item] * parent._width)
|
|
||||||
// 在本次计算过偏移量后加上自己长度,以供下一个元素使用
|
|
||||||
marLTemp[_level] += record._width
|
|
||||||
} else {
|
|
||||||
// 如果为根
|
|
||||||
// 初始化偏移量存储map
|
|
||||||
marLTemp[record.id] = []
|
|
||||||
// map中是一个数组,存储的是每级的长度和
|
|
||||||
// 初始情况下为0
|
|
||||||
marLTemp[record.id][_level] = 0
|
|
||||||
Vue.set(record, '_marginLeft', 0)
|
|
||||||
Vue.set(record, '_width', 1)
|
|
||||||
}
|
|
||||||
tmp.push(record)
|
|
||||||
if (record.children && record.children.length > 0) {
|
|
||||||
const children = treeToArray(record.children, expandAll, record, _level, item)
|
|
||||||
tmp = tmp.concat(children)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return tmp
|
|
||||||
}
|
|
|
@ -1,138 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="app-container">
|
|
||||||
|
|
||||||
<el-tag style="margin-bottom:20px;">
|
|
||||||
<a href="https://github.com/PanJiaChen/vue-element-admin/tree/master/src/components/TreeTable" target="_blank">Documentation</a>
|
|
||||||
</el-tag>
|
|
||||||
|
|
||||||
<tree-table :data="data" :eval-func="func" :eval-args="args" :expand-all="expandAll" border>
|
|
||||||
<el-table-column label="事件">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<span style="color:sandybrown">{{ scope.row.event }}</span>
|
|
||||||
<el-tag>{{ scope.row.timeLine+'ms' }}</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="时间线">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<el-tooltip :content="scope.row.timeLine+'ms'" effect="dark" placement="left">
|
|
||||||
<div class="processContainer">
|
|
||||||
<div
|
|
||||||
:style="{ width:scope.row._width * 500+'px',
|
|
||||||
background:scope.row._width>0.5?'rgba(233,0,0,.5)':'rgba(0,0,233,0.5)',
|
|
||||||
marginLeft:scope.row._marginLeft * 500+'px' }"
|
|
||||||
class="process">
|
|
||||||
<span style="display:inline-block"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-tooltip>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="200">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<el-button type="text" @click="message(scope.row)">点击</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</tree-table>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
/**
|
|
||||||
Auth: Lei.j1ang
|
|
||||||
Created: 2018/1/19-14:54
|
|
||||||
*/
|
|
||||||
import treeTable from '@/components/TreeTable'
|
|
||||||
import treeToArray from './customEval'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'CustomTreeTableDemo',
|
|
||||||
components: { treeTable },
|
|
||||||
data: function() {
|
|
||||||
return {
|
|
||||||
func: treeToArray,
|
|
||||||
expandAll: false,
|
|
||||||
data:
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
event: '事件1',
|
|
||||||
timeLine: 100,
|
|
||||||
comment: '无',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
event: '事件2',
|
|
||||||
timeLine: 10,
|
|
||||||
comment: '无'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
event: '事件3',
|
|
||||||
timeLine: 90,
|
|
||||||
comment: '无',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
event: '事件4',
|
|
||||||
timeLine: 5,
|
|
||||||
comment: '无'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
event: '事件5',
|
|
||||||
timeLine: 10,
|
|
||||||
comment: '无'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
event: '事件6',
|
|
||||||
timeLine: 75,
|
|
||||||
comment: '无',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
event: '事件7',
|
|
||||||
timeLine: 50,
|
|
||||||
comment: '无',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: 71,
|
|
||||||
event: '事件71',
|
|
||||||
timeLine: 25,
|
|
||||||
comment: 'xx'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 72,
|
|
||||||
event: '事件72',
|
|
||||||
timeLine: 5,
|
|
||||||
comment: 'xx'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 73,
|
|
||||||
event: '事件73',
|
|
||||||
timeLine: 20,
|
|
||||||
comment: 'xx'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
event: '事件8',
|
|
||||||
timeLine: 25,
|
|
||||||
comment: '无'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
args: [null, null, 'timeLine']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
message(row) {
|
|
||||||
this.$message.info(row.event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,129 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="app-container">
|
|
||||||
|
|
||||||
<el-tag style="margin-bottom:20px;">
|
|
||||||
<a href="https://github.com/PanJiaChen/vue-element-admin/tree/master/src/components/TreeTable" target="_blank">Documentation</a>
|
|
||||||
</el-tag>
|
|
||||||
|
|
||||||
<tree-table :data="data" :columns="columns" border/>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
/**
|
|
||||||
Auth: Lei.j1ang
|
|
||||||
Created: 2018/1/19-14:54
|
|
||||||
*/
|
|
||||||
import treeTable from '@/components/TreeTable'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'TreeTableDemo',
|
|
||||||
components: { treeTable },
|
|
||||||
data: function() {
|
|
||||||
return {
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
text: '事件',
|
|
||||||
value: 'event',
|
|
||||||
width: 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'ID',
|
|
||||||
value: 'id'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: '时间线',
|
|
||||||
value: 'timeLine'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: '备注',
|
|
||||||
value: 'comment'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
event: '事件1',
|
|
||||||
timeLine: 50,
|
|
||||||
comment: '无'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
event: '事件1',
|
|
||||||
timeLine: 100,
|
|
||||||
comment: '无',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
event: '事件2',
|
|
||||||
timeLine: 10,
|
|
||||||
comment: '无'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
event: '事件3',
|
|
||||||
timeLine: 90,
|
|
||||||
comment: '无',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
event: '事件4',
|
|
||||||
timeLine: 5,
|
|
||||||
comment: '无'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
event: '事件5',
|
|
||||||
timeLine: 10,
|
|
||||||
comment: '无'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
event: '事件6',
|
|
||||||
timeLine: 75,
|
|
||||||
comment: '无',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
event: '事件7',
|
|
||||||
timeLine: 50,
|
|
||||||
comment: '无',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: 71,
|
|
||||||
event: '事件71',
|
|
||||||
timeLine: 25,
|
|
||||||
comment: 'xx'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 72,
|
|
||||||
event: '事件72',
|
|
||||||
timeLine: 5,
|
|
||||||
comment: 'xx'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 73,
|
|
||||||
event: '事件73',
|
|
||||||
timeLine: 20,
|
|
||||||
comment: 'xx'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
event: '事件8',
|
|
||||||
timeLine: 25,
|
|
||||||
comment: '无'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -146,35 +146,87 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
mappers() {
|
mappers() {
|
||||||
|
const applyActionToAllUsers = (filteredUsers, fn) => Promise.all(filteredUsers.map(fn))
|
||||||
|
.then(() => {
|
||||||
|
this.$message({
|
||||||
|
type: 'success',
|
||||||
|
message: this.$t('users.completed')
|
||||||
|
})
|
||||||
|
this.$emit('apply-action')
|
||||||
|
}).catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
return
|
||||||
|
})
|
||||||
return {
|
return {
|
||||||
grantRight: (right) => () => this.selectedUsers
|
grantRight: (right) => () => {
|
||||||
.filter(user => user.local && !user.roles[right] && this.$store.state.user.id !== user.id)
|
const filterUsersFn = user => user.local && !user.roles[right] && this.$store.state.user.id !== user.id
|
||||||
.map(user => this.$store.dispatch('ToggleRight', { user, right })),
|
const toggleRightFn = async(user) => await this.$store.dispatch('ToggleRight', { user, right })
|
||||||
revokeRight: (right) => () => this.selectedUsers
|
const filtered = this.selectedUsers.filter(filterUsersFn)
|
||||||
.filter(user => user.local && user.roles[right] && this.$store.state.user.id !== user.id)
|
|
||||||
.map(user => this.$store.dispatch('ToggleRight', { user, right })),
|
applyActionToAllUsers(filtered, toggleRightFn)
|
||||||
activate: () => this.selectedUsers
|
|
||||||
.filter(user => user.deactivated && this.$store.state.user.id !== user.id)
|
|
||||||
.map(user => this.$store.dispatch('ToggleUserActivation', user.nickname)),
|
|
||||||
deactivate: () => this.selectedUsers
|
|
||||||
.filter(user => !user.deactivated && this.$store.state.user.id !== user.id)
|
|
||||||
.map(user => this.$store.dispatch('ToggleUserActivation', user.nickname)),
|
|
||||||
remove: () => this.selectedUsers
|
|
||||||
.filter(user => this.$store.state.user.id !== user.id)
|
|
||||||
.map(user => this.$store.dispatch('DeleteUser', user)),
|
|
||||||
addTag: (tag) => () => {
|
|
||||||
const users = this.selectedUsers
|
|
||||||
.filter(user => tag === 'disable_remote_subscription' || tag === 'disable_any_subscription'
|
|
||||||
? user.local && !user.tags.includes(tag)
|
|
||||||
: !user.tags.includes(tag))
|
|
||||||
this.$store.dispatch('AddTag', { users, tag })
|
|
||||||
},
|
},
|
||||||
removeTag: (tag) => () => {
|
revokeRight: (right) => () => {
|
||||||
const users = this.selectedUsers
|
const filterUsersFn = user => user.local && user.roles[right] && this.$store.state.user.id !== user.id
|
||||||
.filter(user => tag === 'disable_remote_subscription' || tag === 'disable_any_subscription'
|
const toggleRightFn = async(user) => await this.$store.dispatch('ToggleRight', { user, right })
|
||||||
? user.local && user.tags.includes(tag)
|
const filtered = this.selectedUsers.filter(filterUsersFn)
|
||||||
: user.tags.includes(tag))
|
|
||||||
this.$store.dispatch('RemoveTag', { users, tag })
|
applyActionToAllUsers(filtered, toggleRightFn)
|
||||||
|
},
|
||||||
|
activate: () => {
|
||||||
|
const filtered = this.selectedUsers.filter(user => user.deactivated && this.$store.state.user.id !== user.id)
|
||||||
|
const toggleActivationFn = async(user) => await this.$store.dispatch('ToggleUserActivation', user.nickname)
|
||||||
|
|
||||||
|
applyActionToAllUsers(filtered, toggleActivationFn)
|
||||||
|
},
|
||||||
|
deactivate: () => {
|
||||||
|
const filtered = this.selectedUsers.filter(user => !user.deactivated && this.$store.state.user.id !== user.id)
|
||||||
|
const toggleActivationFn = async(user) => await this.$store.dispatch('ToggleUserActivation', user.nickname)
|
||||||
|
|
||||||
|
applyActionToAllUsers(filtered, toggleActivationFn)
|
||||||
|
},
|
||||||
|
remove: () => {
|
||||||
|
const filtered = this.selectedUsers.filter(user => this.$store.state.user.id !== user.id)
|
||||||
|
const deleteAccountFn = async(user) => await this.$store.dispatch('DeleteUser', user)
|
||||||
|
|
||||||
|
applyActionToAllUsers(filtered, deleteAccountFn)
|
||||||
|
},
|
||||||
|
addTag: (tag) => async() => {
|
||||||
|
const filterUsersFn = user => tag === 'disable_remote_subscription' || tag === 'disable_any_subscription'
|
||||||
|
? user.local && !user.tags.includes(tag)
|
||||||
|
: !user.tags.includes(tag)
|
||||||
|
const users = this.selectedUsers.filter(filterUsersFn)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('AddTag', { users, tag })
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$message({
|
||||||
|
type: 'success',
|
||||||
|
message: this.$t('users.completed')
|
||||||
|
})
|
||||||
|
this.$emit('apply-action')
|
||||||
|
},
|
||||||
|
removeTag: (tag) => async() => {
|
||||||
|
const filterUsersFn = user => tag === 'disable_remote_subscription' || tag === 'disable_any_subscription'
|
||||||
|
? user.local && user.tags.includes(tag)
|
||||||
|
: user.tags.includes(tag)
|
||||||
|
const users = this.selectedUsers.filter(filterUsersFn)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('RemoveTag', { users, tag })
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$message({
|
||||||
|
type: 'success',
|
||||||
|
message: this.$t('users.completed')
|
||||||
|
})
|
||||||
|
this.$emit('apply-action')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -234,11 +286,6 @@ export default {
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
applyAction()
|
applyAction()
|
||||||
this.$emit('apply-action')
|
|
||||||
this.$message({
|
|
||||||
type: 'success',
|
|
||||||
message: this.$t('users.completed')
|
|
||||||
})
|
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
|
|
|
@ -5,20 +5,20 @@
|
||||||
:title="$t('users.createAccount')"
|
:title="$t('users.createAccount')"
|
||||||
custom-class="create-user-dialog"
|
custom-class="create-user-dialog"
|
||||||
@open="resetForm">
|
@open="resetForm">
|
||||||
<el-form ref="form" :model="form" :rules="rules" :label-width="getLabelWidth" status-icon>
|
<el-form ref="newUserForm" :model="newUserForm" :rules="rules" :label-width="getLabelWidth" status-icon>
|
||||||
<el-form-item :label="$t('users.username')" prop="nickname" class="create-account-form-item">
|
<el-form-item :label="$t('users.username')" prop="nickname" class="create-account-form-item">
|
||||||
<el-input v-model="form.nickname" name="nickname" autofocus/>
|
<el-input v-model="newUserForm.nickname" name="nickname" autofocus/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('users.email')" prop="email" class="create-account-form-item">
|
<el-form-item :label="$t('users.email')" prop="email" class="create-account-form-item">
|
||||||
<el-input v-model="form.email" name="email" type="email"/>
|
<el-input v-model="newUserForm.email" name="email" type="email"/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('users.password')" prop="password" class="create-account-form-item">
|
<el-form-item :label="$t('users.password')" prop="password" class="create-account-form-item-without-margin">
|
||||||
<el-input v-model="form.password" type="password" name="password" autocomplete="off"/>
|
<el-input v-model="newUserForm.password" type="password" name="password" autocomplete="off"/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<span slot="footer">
|
<span slot="footer">
|
||||||
<el-button @click="closeDialogWindow">{{ $t('users.cancel') }}</el-button>
|
<el-button @click="closeDialogWindow">{{ $t('users.cancel') }}</el-button>
|
||||||
<el-button type="primary" @click="submitForm('form')">{{ $t('users.create') }}</el-button>
|
<el-button type="primary" @click="submitForm('newUserForm')">{{ $t('users.create') }}</el-button>
|
||||||
</span>
|
</span>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
@ -36,7 +36,7 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
form: {
|
newUserForm: {
|
||||||
nickname: '',
|
nickname: '',
|
||||||
email: '',
|
email: '',
|
||||||
password: ''
|
password: ''
|
||||||
|
@ -67,7 +67,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getLabelWidth() {
|
getLabelWidth() {
|
||||||
return this.isDesktop ? '120px' : '80px'
|
return this.isDesktop ? '120px' : '85px'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -76,18 +76,13 @@ export default {
|
||||||
},
|
},
|
||||||
resetForm() {
|
resetForm() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs['form'].resetFields()
|
this.$refs['newUserForm'].resetFields()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
submitForm(formName) {
|
submitForm(formName) {
|
||||||
this.$refs[formName].validate((valid) => {
|
this.$refs[formName].validate((valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
this.$emit('createNewAccount', this.$data.form)
|
this.$emit('createNewAccount', this.$data.newUserForm)
|
||||||
this.closeDialogWindow()
|
|
||||||
this.$message({
|
|
||||||
type: 'success',
|
|
||||||
message: this.$t('users.completed')
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
|
@ -135,17 +130,26 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style rel='stylesheet/scss' lang='scss'>
|
<style rel='stylesheet/scss' lang='scss'>
|
||||||
|
.el-dialog__body {
|
||||||
|
padding: 20px 20px 20px 20px
|
||||||
|
}
|
||||||
|
.create-account-form-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.create-account-form-item-without-margin {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
@media
|
@media
|
||||||
only screen and (max-width: 760px),
|
only screen and (max-width: 760px),
|
||||||
(min-device-width: 768px) and (max-device-width: 1024px) {
|
(min-device-width: 768px) and (max-device-width: 1024px) {
|
||||||
.create-user-dialog {
|
.create-user-dialog {
|
||||||
width: 80%
|
width: 85%
|
||||||
}
|
}
|
||||||
.create-account-form-item {
|
.create-account-form-item {
|
||||||
margin-bottom: 30px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
.el-dialog__body {
|
.el-dialog__body {
|
||||||
padding: 20px 20px 0 20px
|
padding: 20px 20px 20px 20px
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -9,9 +9,9 @@
|
||||||
<el-input :placeholder="$t('users.search')" v-model="search" class="search" @input="handleDebounceSearchInput"/>
|
<el-input :placeholder="$t('users.search')" v-model="search" class="search" @input="handleDebounceSearchInput"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions-container">
|
<div class="actions-container">
|
||||||
<el-button class="actions-button create-account" @click="dialogFormVisible = true">
|
<el-button class="actions-button create-account" @click="createAccountDialogOpen = true">
|
||||||
<span>
|
<span>
|
||||||
<i class="el-icon-plus" />
|
<i class="el-icon-plus"/>
|
||||||
{{ $t('users.createAccount') }}
|
{{ $t('users.createAccount') }}
|
||||||
</span>
|
</span>
|
||||||
</el-button>
|
</el-button>
|
||||||
|
@ -20,9 +20,9 @@
|
||||||
@apply-action="clearSelection"/>
|
@apply-action="clearSelection"/>
|
||||||
</div>
|
</div>
|
||||||
<new-account-dialog
|
<new-account-dialog
|
||||||
:dialog-form-visible="dialogFormVisible"
|
:dialog-form-visible="createAccountDialogOpen"
|
||||||
@createNewAccount="createNewAccount"
|
@createNewAccount="createNewAccount"
|
||||||
@closeWindow="dialogFormVisible = false"/>
|
@closeWindow="createAccountDialogOpen = false"/>
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
ref="usersTable"
|
ref="usersTable"
|
||||||
|
@ -127,11 +127,30 @@
|
||||||
{{ $t('users.disableAnySubscription') }}
|
{{ $t('users.disableAnySubscription') }}
|
||||||
<i v-if="scope.row.tags.includes('disable_any_subscription')" class="el-icon-check"/>
|
<i v-if="scope.row.tags.includes('disable_any_subscription')" class="el-icon-check"/>
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item
|
||||||
|
v-if="scope.row.local"
|
||||||
|
divided
|
||||||
|
@click.native="getPasswordResetToken(scope.row.nickname)">
|
||||||
|
{{ $t('users.getPasswordResetToken') }}
|
||||||
|
</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
<el-dialog
|
||||||
|
v-loading="loading"
|
||||||
|
:visible.sync="resetPasswordDialogOpen"
|
||||||
|
:title="$t('users.passwordResetTokenCreated')"
|
||||||
|
custom-class="password-reset-token-dialog"
|
||||||
|
@close="closeResetPasswordDialog">
|
||||||
|
<div>
|
||||||
|
<p class="password-reset-token">Password reset token was generated: {{ passwordResetToken }}</p>
|
||||||
|
<p>You can also use this link to reset password:
|
||||||
|
<a :href="passwordResetLink" target="_blank" class="reset-password-link">{{ passwordResetLink }}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
<div v-if="users.length === 0" class="no-users-message">
|
<div v-if="users.length === 0" class="no-users-message">
|
||||||
<p>There are no users to display</p>
|
<p>There are no users to display</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -166,7 +185,8 @@ export default {
|
||||||
return {
|
return {
|
||||||
search: '',
|
search: '',
|
||||||
selectedUsers: [],
|
selectedUsers: [],
|
||||||
dialogFormVisible: false
|
createAccountDialogOpen: false,
|
||||||
|
resetPasswordDialogOpen: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -185,6 +205,12 @@ export default {
|
||||||
pageSize() {
|
pageSize() {
|
||||||
return this.$store.state.users.pageSize
|
return this.$store.state.users.pageSize
|
||||||
},
|
},
|
||||||
|
passwordResetLink() {
|
||||||
|
return this.$store.state.users.passwordResetToken.link
|
||||||
|
},
|
||||||
|
passwordResetToken() {
|
||||||
|
return this.$store.state.users.passwordResetToken.token
|
||||||
|
},
|
||||||
currentPage() {
|
currentPage() {
|
||||||
return this.$store.state.users.currentPage
|
return this.$store.state.users.currentPage
|
||||||
},
|
},
|
||||||
|
@ -213,12 +239,26 @@ export default {
|
||||||
clearSelection() {
|
clearSelection() {
|
||||||
this.$refs.usersTable.clearSelection()
|
this.$refs.usersTable.clearSelection()
|
||||||
},
|
},
|
||||||
createNewAccount(accountData) {
|
async createNewAccount(accountData) {
|
||||||
this.$store.dispatch('CreateNewAccount', accountData)
|
try {
|
||||||
|
await this.$store.dispatch('CreateNewAccount', accountData)
|
||||||
|
} catch (_e) {
|
||||||
|
return
|
||||||
|
} finally {
|
||||||
|
this.createAccountDialogOpen = false
|
||||||
|
}
|
||||||
|
this.$message({
|
||||||
|
type: 'success',
|
||||||
|
message: this.$t('users.accountCreated')
|
||||||
|
})
|
||||||
},
|
},
|
||||||
getFirstLetter(str) {
|
getFirstLetter(str) {
|
||||||
return str.charAt(0).toUpperCase()
|
return str.charAt(0).toUpperCase()
|
||||||
},
|
},
|
||||||
|
getPasswordResetToken(nickname) {
|
||||||
|
this.resetPasswordDialogOpen = true
|
||||||
|
this.$store.dispatch('GetPasswordResetToken', nickname)
|
||||||
|
},
|
||||||
handleDeactivation({ nickname }) {
|
handleDeactivation({ nickname }) {
|
||||||
this.$store.dispatch('ToggleUserActivation', nickname)
|
this.$store.dispatch('ToggleUserActivation', nickname)
|
||||||
},
|
},
|
||||||
|
@ -236,6 +276,10 @@ export default {
|
||||||
handleSelectionChange(value) {
|
handleSelectionChange(value) {
|
||||||
this.$data.selectedUsers = value
|
this.$data.selectedUsers = value
|
||||||
},
|
},
|
||||||
|
closeResetPasswordDialog() {
|
||||||
|
this.resetPasswordDialogOpen = false
|
||||||
|
this.$store.dispatch('RemovePasswordToken')
|
||||||
|
},
|
||||||
showAdminAction({ local, id }) {
|
showAdminAction({ local, id }) {
|
||||||
return local && this.showDeactivatedButton(id)
|
return local && this.showDeactivatedButton(id)
|
||||||
},
|
},
|
||||||
|
@ -254,7 +298,7 @@ export default {
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style rel='stylesheet/scss' lang='scss' scoped>
|
<style rel='stylesheet/scss' lang='scss'>
|
||||||
.actions-button {
|
.actions-button {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
width: 350px;
|
width: 350px;
|
||||||
|
@ -283,6 +327,15 @@ export default {
|
||||||
.el-icon-plus {
|
.el-icon-plus {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
.password-reset-token {
|
||||||
|
margin: 0 0 14px 0;
|
||||||
|
}
|
||||||
|
.password-reset-token-dialog {
|
||||||
|
width: 50%
|
||||||
|
}
|
||||||
|
.reset-password-link {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
.users-container {
|
.users-container {
|
||||||
h1 {
|
h1 {
|
||||||
margin: 22px 0 0 15px;
|
margin: 22px 0 0 15px;
|
||||||
|
@ -312,6 +365,9 @@ export default {
|
||||||
@media
|
@media
|
||||||
only screen and (max-width: 760px),
|
only screen and (max-width: 760px),
|
||||||
(min-device-width: 768px) and (max-device-width: 1024px) {
|
(min-device-width: 768px) and (max-device-width: 1024px) {
|
||||||
|
.password-reset-token-dialog {
|
||||||
|
width: 85%
|
||||||
|
}
|
||||||
.users-container {
|
.users-container {
|
||||||
h1 {
|
h1 {
|
||||||
margin: 7px 10px 15px 10px;
|
margin: 7px 10px 15px 10px;
|
||||||
|
|
151
test/views/invites/index.test.js
Normal file
151
test/views/invites/index.test.js
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
import Vuex from 'vuex'
|
||||||
|
import { mount, createLocalVue, config } from '@vue/test-utils'
|
||||||
|
import flushPromises from 'flush-promises'
|
||||||
|
import Element from 'element-ui'
|
||||||
|
import Invites from '@/views/invites/index'
|
||||||
|
import storeConfig from './store.conf'
|
||||||
|
import { cloneDeep } from 'lodash'
|
||||||
|
|
||||||
|
config.mocks["$t"] = () => {}
|
||||||
|
|
||||||
|
const localVue = createLocalVue()
|
||||||
|
localVue.use(Vuex)
|
||||||
|
localVue.use(Element)
|
||||||
|
|
||||||
|
jest.mock('@/api/invites')
|
||||||
|
|
||||||
|
describe('Invite tokens', () => {
|
||||||
|
let store
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
store = new Vuex.Store(cloneDeep(storeConfig))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fetches initial list of invtie tokens', async (done) => {
|
||||||
|
mount(Invites, {
|
||||||
|
store,
|
||||||
|
localVue,
|
||||||
|
sync: false,
|
||||||
|
stubs: ['router-link']
|
||||||
|
})
|
||||||
|
|
||||||
|
await flushPromises()
|
||||||
|
const inviteTokens = store.state.invites.inviteTokens
|
||||||
|
expect(inviteTokens.length).toEqual(3)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('opens and closes dialog window', async (done) => {
|
||||||
|
const wrapper = mount(Invites, {
|
||||||
|
store,
|
||||||
|
localVue,
|
||||||
|
sync: false,
|
||||||
|
stubs: ['router-link']
|
||||||
|
})
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
const dialog = wrapper.find('div.el-dialog__wrapper .create-new-token-dialog')
|
||||||
|
expect(dialog.isVisible()).toBe(false)
|
||||||
|
|
||||||
|
const openDialogButton = wrapper.find('button.create-invite-token')
|
||||||
|
const closeDialogButton = wrapper.find('div.el-dialog__footer button')
|
||||||
|
|
||||||
|
openDialogButton.trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
expect(dialog.isVisible()).toBe(true)
|
||||||
|
|
||||||
|
closeDialogButton.trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
expect(dialog.isVisible()).toBe(false)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('generates new invtie token', async (done) => {
|
||||||
|
const wrapper = mount(Invites, {
|
||||||
|
store,
|
||||||
|
localVue,
|
||||||
|
sync: false,
|
||||||
|
stubs: ['router-link']
|
||||||
|
})
|
||||||
|
await flushPromises()
|
||||||
|
expect(store.state.invites.inviteTokens.length).toEqual(3)
|
||||||
|
expect(Object.keys(store.state.invites.newToken).length).toEqual(0)
|
||||||
|
|
||||||
|
const openDialogButton = wrapper.find('button.create-invite-token')
|
||||||
|
openDialogButton.trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
const maxUseInput = wrapper.find('input[name="maxUse"]')
|
||||||
|
maxUseInput.element.value = 3
|
||||||
|
maxUseInput.trigger('input')
|
||||||
|
|
||||||
|
const expireDate = wrapper.find('input[name="date"]')
|
||||||
|
expireDate.element.value = '2019-04-10'
|
||||||
|
expireDate.trigger('input')
|
||||||
|
|
||||||
|
const createButton = wrapper.find('.create-new-token-dialog button.el-button--primary')
|
||||||
|
createButton.trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(store.state.invites.inviteTokens.length).toEqual(4)
|
||||||
|
expect(Object.keys(store.state.invites.newToken).length).toEqual(3)
|
||||||
|
expect(store.state.invites.newToken.token).toEqual('JYl0SjXW8t-t-pLSZBnZLf6PwjCW-qy6Dq70jfUOuqk=')
|
||||||
|
expect(store.state.invites.newToken.expiresAt).toEqual('2019-04-10')
|
||||||
|
expect(store.state.invites.newToken.maxUse).toEqual(3)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('revokes invite token', async (done) => {
|
||||||
|
const wrapper = mount(Invites, {
|
||||||
|
store,
|
||||||
|
localVue,
|
||||||
|
sync: false,
|
||||||
|
stubs: ['router-link']
|
||||||
|
})
|
||||||
|
|
||||||
|
await flushPromises()
|
||||||
|
expect(store.state.invites.inviteTokens[3].used).toBe(false)
|
||||||
|
|
||||||
|
const revokeButton = wrapper.find('table tr button')
|
||||||
|
revokeButton.trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(store.state.invites.inviteTokens[0].used).toBe(true)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('invites user via email', async (done) => {
|
||||||
|
const wrapper = mount(Invites, {
|
||||||
|
store,
|
||||||
|
localVue,
|
||||||
|
sync: false,
|
||||||
|
stubs: ['router-link']
|
||||||
|
})
|
||||||
|
|
||||||
|
const dialog = wrapper.find('div.el-dialog__wrapper .invite-via-email-dialog')
|
||||||
|
expect(dialog.isVisible()).toBe(false)
|
||||||
|
|
||||||
|
const inviteUserViaEmailStub = jest.fn()
|
||||||
|
wrapper.setMethods({ inviteUserViaEmail: inviteUserViaEmailStub })
|
||||||
|
|
||||||
|
const openDialogButton = wrapper.find('button.invite-via-email')
|
||||||
|
openDialogButton.trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
expect(dialog.isVisible()).toBe(true)
|
||||||
|
|
||||||
|
const email = wrapper.find('input[name="email"]')
|
||||||
|
email.element.value = 'bob@gmail.com'
|
||||||
|
email.trigger('input')
|
||||||
|
|
||||||
|
const name = wrapper.find('input[name="name"]')
|
||||||
|
name.element.value = 'Bob'
|
||||||
|
name.trigger('input')
|
||||||
|
|
||||||
|
const createButton = wrapper.find('.invite-via-email-dialog button.el-button--primary')
|
||||||
|
createButton.trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(wrapper.vm.inviteUserViaEmail).toHaveBeenCalled()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
13
test/views/invites/store.conf.js
Normal file
13
test/views/invites/store.conf.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import app from '@/store/modules/app'
|
||||||
|
import user from '@/store/modules/user'
|
||||||
|
import invites from '@/store/modules/invites'
|
||||||
|
import getters from '@/store/getters'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
modules: {
|
||||||
|
app,
|
||||||
|
invites,
|
||||||
|
user
|
||||||
|
},
|
||||||
|
getters
|
||||||
|
}
|
|
@ -3,7 +3,6 @@ import errorLog from '@/store/modules/errorLog'
|
||||||
import permission from '@/store/modules/permission'
|
import permission from '@/store/modules/permission'
|
||||||
import tagsView from '@/store/modules/tagsView'
|
import tagsView from '@/store/modules/tagsView'
|
||||||
import user from '@/store/modules/user'
|
import user from '@/store/modules/user'
|
||||||
import users from '@/store/modules/users'
|
|
||||||
import getters from '@/store/getters'
|
import getters from '@/store/getters'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -238,6 +238,33 @@ describe('Users actions', () => {
|
||||||
expect(secondUserNicknameAfterToggle).toEqual('bob')
|
expect(secondUserNicknameAfterToggle).toEqual('bob')
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('creates password revoke token', async (done) => {
|
||||||
|
const wrapper = mount(Users, {
|
||||||
|
store,
|
||||||
|
localVue,
|
||||||
|
sync: false,
|
||||||
|
stubs: ['router-link']
|
||||||
|
})
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
const dialog = wrapper.find('.password-reset-token-dialog')
|
||||||
|
const closeDialogButton = wrapper.find('.password-reset-token-dialog button')
|
||||||
|
expect(dialog.isVisible()).toBe(false)
|
||||||
|
expect(store.state.users.passwordResetToken.token).toBe('')
|
||||||
|
|
||||||
|
wrapper.find(htmlElement(1, 11)).trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(dialog.isVisible()).toBe(true)
|
||||||
|
expect(store.state.users.passwordResetToken.token).toBe('g05lxnBJQnL')
|
||||||
|
expect(store.state.users.passwordResetToken.link).toBe('http://url/api/pleroma/password_reset/g05lxnBJQnL')
|
||||||
|
|
||||||
|
closeDialogButton.trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
expect(dialog.isVisible()).toBe(false)
|
||||||
|
done()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Creates new account', () => {
|
describe('Creates new account', () => {
|
||||||
|
|
Loading…
Reference in a new issue