More avatar providers (#200)
Reload the avatar after changing it Hide cropper after upload Fix aspect ratio Add loading variable Move avatar settings to seperate component Add avatar crop Fix avatar upload Add avatar file upload Add abstract methods for file upload Add saving avatar status Add avatar setting Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/200
This commit is contained in:
parent
617bcea04e
commit
ec1b039daa
11 changed files with 295 additions and 58 deletions
|
@ -20,6 +20,7 @@
|
||||||
"v-tooltip": "2.0.3",
|
"v-tooltip": "2.0.3",
|
||||||
"verte": "0.0.12",
|
"verte": "0.0.12",
|
||||||
"vue": "2.6.11",
|
"vue": "2.6.11",
|
||||||
|
"vue-advanced-cropper": "^0.16.10",
|
||||||
"vue-drag-resize": "1.4.2",
|
"vue-drag-resize": "1.4.2",
|
||||||
"vue-easymde": "1.2.2",
|
"vue-easymde": "1.2.2",
|
||||||
"vue-shortkey": "3.1.7",
|
"vue-shortkey": "3.1.7",
|
||||||
|
|
|
@ -44,7 +44,7 @@
|
||||||
<a @click="refreshApp()" class="button is-primary noshadow">Update Now</a>
|
<a @click="refreshApp()" class="button is-primary noshadow">Update Now</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="user">
|
<div class="user">
|
||||||
<img :src="userInfo.getAvatarUrl()" class="avatar" alt=""/>
|
<img :src="userAvatar" class="avatar" alt=""/>
|
||||||
<div class="dropdown is-right is-active">
|
<div class="dropdown is-right is-active">
|
||||||
<div class="dropdown-trigger">
|
<div class="dropdown-trigger">
|
||||||
<button class="button noshadow" @click.stop="userMenuActive = !userMenuActive">
|
<button class="button noshadow" @click.stop="userMenuActive = !userMenuActive">
|
||||||
|
@ -415,6 +415,7 @@
|
||||||
},
|
},
|
||||||
computed: mapState({
|
computed: mapState({
|
||||||
userInfo: state => state.auth.info,
|
userInfo: state => state.auth.info,
|
||||||
|
userAvatar: state => state.auth.avatarUrl,
|
||||||
userAuthenticated: state => state.auth.authenticated,
|
userAuthenticated: state => state.auth.authenticated,
|
||||||
motd: state => state.config.motd,
|
motd: state => state.config.motd,
|
||||||
online: ONLINE,
|
online: ONLINE,
|
||||||
|
|
149
src/components/user/avatar-settings.vue
Normal file
149
src/components/user/avatar-settings.vue
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
<template>
|
||||||
|
<div class="card">
|
||||||
|
<header class="card-header">
|
||||||
|
<p class="card-header-title">
|
||||||
|
Avatar
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="control mb-4">
|
||||||
|
<label class="radio">
|
||||||
|
<input type="radio" name="avatarProvider" v-model="avatarProvider" value="default"/>
|
||||||
|
Default
|
||||||
|
</label>
|
||||||
|
<label class="radio">
|
||||||
|
<input type="radio" name="avatarProvider" v-model="avatarProvider" value="initials"/>
|
||||||
|
Initials
|
||||||
|
</label>
|
||||||
|
<label class="radio">
|
||||||
|
<input type="radio" name="avatarProvider" v-model="avatarProvider" value="gravatar"/>
|
||||||
|
Gravatar
|
||||||
|
</label>
|
||||||
|
<label class="radio">
|
||||||
|
<input type="radio" name="avatarProvider" v-model="avatarProvider" value="upload"/>
|
||||||
|
Upload
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="avatarProvider === 'upload'">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref="avatarUploadInput"
|
||||||
|
@change="cropAvatar"
|
||||||
|
class="is-hidden"
|
||||||
|
accept="image/*"
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
v-if="!isCropAvatar"
|
||||||
|
class="button is-primary"
|
||||||
|
@click="$refs.avatarUploadInput.click()"
|
||||||
|
:class="{ 'is-loading': avatarService.loading || loading}">
|
||||||
|
Upload Avatar
|
||||||
|
</a>
|
||||||
|
<template v-else>
|
||||||
|
<cropper
|
||||||
|
:src="avatarToCrop"
|
||||||
|
class="mb-4"
|
||||||
|
@ready="() => loading = false"
|
||||||
|
:stencil-props="{aspectRatio: 1}"
|
||||||
|
ref="cropper"/>
|
||||||
|
<a
|
||||||
|
class="button is-primary"
|
||||||
|
@click="uploadAvatar"
|
||||||
|
:class="{ 'is-loading': avatarService.loading || loading}">
|
||||||
|
Upload Avatar
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="bigbuttons" v-if="avatarProvider !== 'upload'">
|
||||||
|
<button @click="updateAvatarStatus()" class="button is-primary is-fullwidth"
|
||||||
|
:class="{ 'is-loading': avatarService.loading || loading}">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {Cropper} from 'vue-advanced-cropper'
|
||||||
|
|
||||||
|
import AvatarService from '../../services/avatar'
|
||||||
|
import AvatarModel from '../../models/avatar'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'avatar-settings',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
avatarProvider: '',
|
||||||
|
avatarService: AvatarService,
|
||||||
|
isCropAvatar: false,
|
||||||
|
avatarToCrop: null,
|
||||||
|
loading: false, // Seperate variable because some things we're doing in browser take a bit
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.avatarService = new AvatarService()
|
||||||
|
this.avatarStatus()
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Cropper,
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
avatarStatus() {
|
||||||
|
this.avatarService.get({})
|
||||||
|
.then(r => {
|
||||||
|
this.avatarProvider = r.avatarProvider
|
||||||
|
})
|
||||||
|
.catch(e => this.error(e, this))
|
||||||
|
},
|
||||||
|
updateAvatarStatus() {
|
||||||
|
const avatarStatus = new AvatarModel({avatarProvider: this.avatarProvider})
|
||||||
|
this.avatarService.update(avatarStatus)
|
||||||
|
.then(() => {
|
||||||
|
this.success({message: 'Avatar status was updated successfully!'}, this)
|
||||||
|
this.$store.commit('auth/reloadAvatar')
|
||||||
|
})
|
||||||
|
.catch(e => this.error(e, this))
|
||||||
|
},
|
||||||
|
uploadAvatar() {
|
||||||
|
this.loading = true
|
||||||
|
const {canvas} = this.$refs.cropper.getResult()
|
||||||
|
|
||||||
|
if (canvas) {
|
||||||
|
canvas.toBlob(blob => {
|
||||||
|
this.avatarService.create(blob)
|
||||||
|
.then(() => {
|
||||||
|
this.success({message: 'The avatar has been set successfully!'}, this)
|
||||||
|
this.$store.commit('auth/reloadAvatar')
|
||||||
|
})
|
||||||
|
.catch(e => this.error(e, this))
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
this.isCropAvatar = false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cropAvatar() {
|
||||||
|
const avatar = this.$refs.avatarUploadInput.files
|
||||||
|
|
||||||
|
if (avatar.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = e => {
|
||||||
|
this.avatarToCrop = e.target.result
|
||||||
|
this.isCropAvatar = true
|
||||||
|
}
|
||||||
|
reader.onloadend = () => this.loading = false
|
||||||
|
reader.readAsDataURL(avatar[0])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
9
src/models/avatar.js
Normal file
9
src/models/avatar.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import AbstractModel from './abstractModel'
|
||||||
|
|
||||||
|
export default class AvatarModel extends AbstractModel {
|
||||||
|
defaults() {
|
||||||
|
return {
|
||||||
|
avatarProvider: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import {reduce, replace} from 'lodash'
|
import {reduce, replace} from 'lodash'
|
||||||
import { objectToSnakeCase } from '../helpers/case'
|
import {objectToSnakeCase} from '../helpers/case'
|
||||||
|
|
||||||
export default class AbstractService {
|
export default class AbstractService {
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ export default class AbstractService {
|
||||||
|
|
||||||
http = null
|
http = null
|
||||||
loading = false
|
loading = false
|
||||||
|
uploadProgress = 0
|
||||||
paths = {
|
paths = {
|
||||||
create: '',
|
create: '',
|
||||||
get: '',
|
get: '',
|
||||||
|
@ -134,10 +135,10 @@ export default class AbstractService {
|
||||||
pattern = new RegExp(pattern instanceof RegExp ? pattern.source : pattern, 'g')
|
pattern = new RegExp(pattern instanceof RegExp ? pattern.source : pattern, 'g')
|
||||||
|
|
||||||
for (let parameter; (parameter = pattern.exec(route)) !== null;) {
|
for (let parameter; (parameter = pattern.exec(route)) !== null;) {
|
||||||
replace$$1[parameter[0]] = parameters[parameter[1]];
|
replace$$1[parameter[0]] = parameters[parameter[1]]
|
||||||
}
|
}
|
||||||
|
|
||||||
return replace$$1;
|
return replace$$1
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -443,4 +444,62 @@ export default class AbstractService {
|
||||||
cancel()
|
cancel()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a file to a url.
|
||||||
|
* @param url
|
||||||
|
* @param file
|
||||||
|
* @param fieldName The name of the field the file is uploaded to.
|
||||||
|
* @returns {Q.Promise<unknown>}
|
||||||
|
*/
|
||||||
|
uploadFile(url, file, fieldName) {
|
||||||
|
return this.uploadBlob(url, new Blob([file]), fieldName, file.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a blob to a url.
|
||||||
|
* @param url
|
||||||
|
* @param blob
|
||||||
|
* @param fieldName
|
||||||
|
* @param filename
|
||||||
|
* @returns {Q.Promise<unknown>}
|
||||||
|
*/
|
||||||
|
uploadBlob(url, blob, fieldName, filename) {
|
||||||
|
const data = new FormData()
|
||||||
|
data.append(fieldName, blob, filename)
|
||||||
|
return this.uploadFormData(url, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a form data object.
|
||||||
|
* @param url
|
||||||
|
* @param formData
|
||||||
|
* @returns {Q.Promise<unknown>}
|
||||||
|
*/
|
||||||
|
uploadFormData(url, formData) {
|
||||||
|
const cancel = this.setLoading()
|
||||||
|
return this.http.put(
|
||||||
|
url,
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type':
|
||||||
|
'multipart/form-data; boundary=' + formData._boundary,
|
||||||
|
},
|
||||||
|
onUploadProgress: progressEvent => {
|
||||||
|
this.uploadProgress = Math.round((progressEvent.loaded * 100) / progressEvent.total)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
return this.errorHandler(error)
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
return Promise.resolve(this.modelCreateFactory(response.data))
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.uploadProgress = 0
|
||||||
|
cancel()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -16,8 +16,6 @@ export default class AttachmentService extends AbstractService {
|
||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadProgress = 0
|
|
||||||
|
|
||||||
useCreateInterceptor() {
|
useCreateInterceptor() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -61,36 +59,15 @@ export default class AttachmentService extends AbstractService {
|
||||||
* @returns {Promise<any|never>}
|
* @returns {Promise<any|never>}
|
||||||
*/
|
*/
|
||||||
create(model, files) {
|
create(model, files) {
|
||||||
|
const data = new FormData()
|
||||||
let data = new FormData()
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
// TODO: Validation of file size
|
// TODO: Validation of file size
|
||||||
data.append('files', new Blob([files[i]]), files[i].name);
|
data.append('files', new Blob([files[i]]), files[i].name);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cancel = this.setLoading()
|
return this.uploadFormData(
|
||||||
return this.http.put(
|
|
||||||
this.getReplacedRoute(this.paths.create, model),
|
this.getReplacedRoute(this.paths.create, model),
|
||||||
data,
|
data
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Content-Type':
|
|
||||||
'multipart/form-data; boundary=' + data._boundary,
|
|
||||||
},
|
|
||||||
onUploadProgress: progressEvent => {
|
|
||||||
this.uploadProgress = Math.round( (progressEvent.loaded * 100) / progressEvent.total );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
.catch(error => {
|
|
||||||
return this.errorHandler(error)
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
return Promise.resolve(this.modelCreateFactory(response.data))
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.uploadProgress = 0
|
|
||||||
cancel()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
29
src/services/avatar.js
Normal file
29
src/services/avatar.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import AbstractService from './abstractService'
|
||||||
|
import AvatarModel from '../models/avatar'
|
||||||
|
|
||||||
|
export default class AvatarService extends AbstractService {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
get: '/user/settings/avatar',
|
||||||
|
update: '/user/settings/avatar',
|
||||||
|
create: '/user/settings/avatar/upload',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
modelFactory(data) {
|
||||||
|
return new AvatarModel(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
useCreateInterceptor() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
create(blob) {
|
||||||
|
return this.uploadBlob(
|
||||||
|
this.paths.create,
|
||||||
|
blob,
|
||||||
|
'avatar',
|
||||||
|
'avatar.jpg', // This fails without a file name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,8 +8,6 @@ export default class BackgroundUploadService extends AbstractService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadProgress = 0
|
|
||||||
|
|
||||||
useCreateInterceptor() {
|
useCreateInterceptor() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -25,33 +23,10 @@ export default class BackgroundUploadService extends AbstractService {
|
||||||
* @returns {Promise<any|never>}
|
* @returns {Promise<any|never>}
|
||||||
*/
|
*/
|
||||||
create(listId, file) {
|
create(listId, file) {
|
||||||
|
return this.uploadFile(
|
||||||
let data = new FormData()
|
|
||||||
data.append('background', new Blob([file]), file.name);
|
|
||||||
|
|
||||||
const cancel = this.setLoading()
|
|
||||||
return this.http.put(
|
|
||||||
this.getReplacedRoute(this.paths.create, {listId: listId}),
|
this.getReplacedRoute(this.paths.create, {listId: listId}),
|
||||||
data,
|
file,
|
||||||
{
|
'background'
|
||||||
headers: {
|
|
||||||
'Content-Type':
|
|
||||||
'multipart/form-data; boundary=' + data._boundary,
|
|
||||||
},
|
|
||||||
onUploadProgress: progressEvent => {
|
|
||||||
this.uploadProgress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
.catch(error => {
|
|
||||||
return this.errorHandler(error)
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
return Promise.resolve(this.modelCreateFactory(response.data))
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.uploadProgress = 0
|
|
||||||
cancel()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,12 @@ export default {
|
||||||
isLinkShareAuth: false,
|
isLinkShareAuth: false,
|
||||||
info: {},
|
info: {},
|
||||||
needsTotpPasscode: false,
|
needsTotpPasscode: false,
|
||||||
|
avatarUrl: '',
|
||||||
}),
|
}),
|
||||||
mutations: {
|
mutations: {
|
||||||
info(state, info) {
|
info(state, info) {
|
||||||
state.info = info
|
state.info = info
|
||||||
|
state.avatarUrl = info.getAvatarUrl()
|
||||||
},
|
},
|
||||||
authenticated(state, authenticated) {
|
authenticated(state, authenticated) {
|
||||||
state.authenticated = authenticated
|
state.authenticated = authenticated
|
||||||
|
@ -23,6 +25,9 @@ export default {
|
||||||
needsTotpPasscode(state, needs) {
|
needsTotpPasscode(state, needs) {
|
||||||
state.needsTotpPasscode = needs
|
state.needsTotpPasscode = needs
|
||||||
},
|
},
|
||||||
|
reloadAvatar(state) {
|
||||||
|
state.avatarUrl = `${state.info.getAvatarUrl()}&=${+new Date()}`
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
// Logs a user in with a set of credentials.
|
// Logs a user in with a set of credentials.
|
||||||
|
|
|
@ -106,6 +106,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Avatar -->
|
||||||
|
<avatar-settings/>
|
||||||
|
|
||||||
<!-- TOTP -->
|
<!-- TOTP -->
|
||||||
<div class="card" v-if="totpEnabled">
|
<div class="card" v-if="totpEnabled">
|
||||||
<header class="card-header">
|
<header class="card-header">
|
||||||
|
@ -200,6 +203,8 @@
|
||||||
|
|
||||||
import {mapState} from 'vuex'
|
import {mapState} from 'vuex'
|
||||||
|
|
||||||
|
import AvatarSettings from '../../components/user/avatar-settings'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Settings',
|
name: 'Settings',
|
||||||
data() {
|
data() {
|
||||||
|
@ -220,6 +225,9 @@
|
||||||
totpDisablePassword: '',
|
totpDisablePassword: '',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
components: {
|
||||||
|
AvatarSettings,
|
||||||
|
},
|
||||||
created() {
|
created() {
|
||||||
this.passwordUpdateService = new PasswordUpdateService()
|
this.passwordUpdateService = new PasswordUpdateService()
|
||||||
this.passwordUpdate = new PasswordUpdateModel()
|
this.passwordUpdate = new PasswordUpdateModel()
|
||||||
|
|
24
yarn.lock
24
yarn.lock
|
@ -3881,6 +3881,11 @@ class-utils@^0.3.5:
|
||||||
isobject "^3.0.0"
|
isobject "^3.0.0"
|
||||||
static-extend "^0.1.1"
|
static-extend "^0.1.1"
|
||||||
|
|
||||||
|
classnames@^2.2.6:
|
||||||
|
version "2.2.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
|
||||||
|
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
|
||||||
|
|
||||||
clean-css@4.2.x:
|
clean-css@4.2.x:
|
||||||
version "4.2.1"
|
version "4.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.1.tgz#2d411ef76b8569b6d0c84068dabe85b0aa5e5c17"
|
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.1.tgz#2d411ef76b8569b6d0c84068dabe85b0aa5e5c17"
|
||||||
|
@ -4737,6 +4742,11 @@ de-indent@^1.0.2:
|
||||||
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
|
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
|
||||||
integrity sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=
|
integrity sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=
|
||||||
|
|
||||||
|
debounce@^1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.0.tgz#44a540abc0ea9943018dc0eaa95cce87f65cd131"
|
||||||
|
integrity sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg==
|
||||||
|
|
||||||
debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
|
debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
|
||||||
version "2.6.9"
|
version "2.6.9"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||||
|
@ -5179,6 +5189,11 @@ duplexify@^3.4.2, duplexify@^3.6.0:
|
||||||
readable-stream "^2.0.0"
|
readable-stream "^2.0.0"
|
||||||
stream-shift "^1.0.0"
|
stream-shift "^1.0.0"
|
||||||
|
|
||||||
|
easy-bem@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/easy-bem/-/easy-bem-1.0.2.tgz#3f759158c045465744900aa26ba8bd7d88d7a969"
|
||||||
|
integrity sha512-tHtLDhcEHZIMKdiiZElQoR8TcZ/6rvcNp7//93Vx/mqNLah9BOFGhhzTUfWLJs7uxZiKMdP/KzGOtzq14DrrqQ==
|
||||||
|
|
||||||
easy-stack@^1.0.0:
|
easy-stack@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/easy-stack/-/easy-stack-1.0.0.tgz#12c91b3085a37f0baa336e9486eac4bf94e3e788"
|
resolved "https://registry.yarnpkg.com/easy-stack/-/easy-stack-1.0.0.tgz#12c91b3085a37f0baa336e9486eac4bf94e3e788"
|
||||||
|
@ -12454,6 +12469,15 @@ vscode-uri@^1.0.6:
|
||||||
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-1.0.8.tgz#9769aaececae4026fb6e22359cb38946580ded59"
|
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-1.0.8.tgz#9769aaececae4026fb6e22359cb38946580ded59"
|
||||||
integrity sha512-obtSWTlbJ+a+TFRYGaUumtVwb+InIUVI0Lu0VBUAPmj2cU5JutEXg3xUE0c2J5Tcy7h2DEKVJBFi+Y9ZSFzzPQ==
|
integrity sha512-obtSWTlbJ+a+TFRYGaUumtVwb+InIUVI0Lu0VBUAPmj2cU5JutEXg3xUE0c2J5Tcy7h2DEKVJBFi+Y9ZSFzzPQ==
|
||||||
|
|
||||||
|
vue-advanced-cropper@^0.16.10:
|
||||||
|
version "0.16.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/vue-advanced-cropper/-/vue-advanced-cropper-0.16.10.tgz#2792003c3cf55fb028c6822aa6f50f18019f2741"
|
||||||
|
integrity sha512-xUr7tTpbm+EyrILgLinhxj3NrmJlhUgMCtsifEldwZ8vHyHCCKCnvjRsMpiAQWPLrfhjMwECA0U77Mu93tHRow==
|
||||||
|
dependencies:
|
||||||
|
classnames "^2.2.6"
|
||||||
|
debounce "^1.2.0"
|
||||||
|
easy-bem "^1.0.2"
|
||||||
|
|
||||||
vue-cli-plugin-apollo@^0.21.3:
|
vue-cli-plugin-apollo@^0.21.3:
|
||||||
version "0.21.3"
|
version "0.21.3"
|
||||||
resolved "https://registry.yarnpkg.com/vue-cli-plugin-apollo/-/vue-cli-plugin-apollo-0.21.3.tgz#520d336db0e88b26fe854833a555e2e29fe26571"
|
resolved "https://registry.yarnpkg.com/vue-cli-plugin-apollo/-/vue-cli-plugin-apollo-0.21.3.tgz#520d336db0e88b26fe854833a555e2e29fe26571"
|
||||||
|
|
Loading…
Add table
Reference in a new issue