rewrite texture detail page with React
This commit is contained in:
parent
2c313ebde2
commit
b6c58ef23f
|
|
@ -150,10 +150,7 @@ class SkinlibController extends Controller
|
|||
'widgets' => [
|
||||
[
|
||||
['shared.previewer'],
|
||||
[
|
||||
'skinlib.widgets.show.info',
|
||||
'skinlib.widgets.show.operations',
|
||||
],
|
||||
['skinlib.widgets.show.side'],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,103 +0,0 @@
|
|||
<template>
|
||||
<modal
|
||||
id="modal-use-as"
|
||||
ref="modal"
|
||||
:title="$t('user.closet.use-as.title')"
|
||||
:ok-button-text="$t('general.submit')"
|
||||
flex-footer
|
||||
>
|
||||
<template v-if="players.length !== 0">
|
||||
<div class="form-group">
|
||||
<input
|
||||
v-model="search"
|
||||
type="text"
|
||||
class="form-control"
|
||||
:placeholder="$t('user.typeToSearch')"
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
v-for="player in filteredPlayers"
|
||||
:key="player.pid"
|
||||
class="btn btn-block btn-outline-info text-left"
|
||||
@click="submit(player.pid)"
|
||||
>
|
||||
<img :src="avatarUrl(player)" width="45" height="45">
|
||||
<span>{{ player.name }}</span>
|
||||
</button>
|
||||
</template>
|
||||
<p v-else v-t="'user.closet.use-as.empty'" />
|
||||
<template #footer>
|
||||
<a
|
||||
v-if="allowAdd"
|
||||
v-t="'user.closet.use-as.add'"
|
||||
data-toggle="modal"
|
||||
data-target="#modal-add-player"
|
||||
class="btn btn-default"
|
||||
href="#"
|
||||
/>
|
||||
<button class="btn btn-default" data-dismiss="modal">
|
||||
{{ $t('general.cancel') }}
|
||||
</button>
|
||||
</template>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import $ from 'jquery'
|
||||
import Modal from './Modal.vue'
|
||||
import { toast } from '../scripts/notify'
|
||||
|
||||
export default {
|
||||
name: 'ApplyToPlayerDialog',
|
||||
components: {
|
||||
Modal,
|
||||
},
|
||||
props: {
|
||||
skin: Number,
|
||||
cape: Number,
|
||||
allowAdd: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
players: [],
|
||||
search: '',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredPlayers() {
|
||||
return this.players.filter(player => player.name.includes(this.search))
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async fetchList() {
|
||||
this.players = (await this.$http.get('/user/player/list')).data
|
||||
},
|
||||
async submit(selected) {
|
||||
if (!this.skin && !this.cape) {
|
||||
toast.info(this.$t('user.emptySelectedTexture'))
|
||||
return
|
||||
}
|
||||
|
||||
const { code, message } = await this.$http.post(
|
||||
`/user/player/set/${selected}`,
|
||||
{
|
||||
skin: this.skin || undefined,
|
||||
cape: this.cape || undefined,
|
||||
},
|
||||
)
|
||||
if (code === 0) {
|
||||
toast.success(message)
|
||||
$('#modal-use-as').modal('hide')
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
},
|
||||
avatarUrl(player) {
|
||||
return `${blessing.base_url}/avatar/${player.tid_skin}?3d&size=45`
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
@ -21,6 +21,7 @@ const ModalInput: React.FC<Props> = props => (
|
|||
name="modal-radios"
|
||||
id={`modal-radio-${choice.value}`}
|
||||
value={choice.value}
|
||||
checked={choice.value === props.value}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<label htmlFor={`modal-radio-${choice.value}`} className="ml-1">
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
import Vue from 'vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'Portal',
|
||||
props: {
|
||||
selector: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
tag: {
|
||||
type: String,
|
||||
default: 'div',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const container = document.querySelector(this.selector)
|
||||
if (container) {
|
||||
if (container.firstChild) {
|
||||
container.replaceChild(this.$el, container.firstChild)
|
||||
} else {
|
||||
container.appendChild(this.$el)
|
||||
}
|
||||
}
|
||||
},
|
||||
render(h) {
|
||||
return h(this.tag, [this.$slots.default])
|
||||
},
|
||||
})
|
||||
|
|
@ -1,178 +0,0 @@
|
|||
<template>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between">
|
||||
<h3 class="card-title">
|
||||
<span v-html="$t(title)" /> <!-- eslint-disable-line vue/no-v-html -->
|
||||
<span class="badge bg-olive">{{ indicator }}</span>
|
||||
</h3>
|
||||
<div class="operations">
|
||||
<i
|
||||
data-toggle="tooltip"
|
||||
data-placement="bottom"
|
||||
:title="$t('general.walk') + ' / ' + $t('general.run')"
|
||||
class="fas fa-forward"
|
||||
@click="toggleRun"
|
||||
/>
|
||||
<i
|
||||
data-toggle="tooltip"
|
||||
data-placement="bottom"
|
||||
:title="$t('general.rotation')"
|
||||
class="fas fa-redo-alt"
|
||||
@click="toggleRotate"
|
||||
/>
|
||||
<i
|
||||
data-toggle="tooltip"
|
||||
data-placement="bottom"
|
||||
:title="$t('general.pause')"
|
||||
class="fas"
|
||||
:class="{ 'fa-pause': !paused, 'fa-play': paused }"
|
||||
@click="togglePause"
|
||||
/>
|
||||
<i
|
||||
data-toggle="tooltip"
|
||||
data-placement="bottom"
|
||||
:title="$t('general.reset')"
|
||||
class="fas fa-stop"
|
||||
@click="reset"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div ref="previewer" class="previewer-3d">
|
||||
<!-- Container for 3D Preview -->
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="$slots.footer" class="card-footer">
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as skinview3d from 'skinview3d'
|
||||
import { emit } from '../scripts/event'
|
||||
import SkinSteve from '../../../misc/textures/steve.png'
|
||||
|
||||
export default {
|
||||
name: 'Previewer',
|
||||
props: {
|
||||
skin: String,
|
||||
cape: String,
|
||||
model: {
|
||||
type: String,
|
||||
default: 'steve',
|
||||
},
|
||||
closetMode: Boolean,
|
||||
title: {
|
||||
type: String,
|
||||
default: 'general.texturePreview',
|
||||
},
|
||||
initPositionZ: {
|
||||
type: Number,
|
||||
default: 70,
|
||||
},
|
||||
},
|
||||
data: () => ({
|
||||
paused: false,
|
||||
}),
|
||||
computed: {
|
||||
indicator() {
|
||||
if (!this.closetMode) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (this.skin && this.cape) {
|
||||
return `${this.$t('general.skin')} & ${this.$t('general.cape')}`
|
||||
} else if (this.skin) {
|
||||
return this.$t('general.skin')
|
||||
} else if (this.cape) {
|
||||
return this.$t('general.cape')
|
||||
}
|
||||
return ''
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
skin(url) {
|
||||
this.viewer.skinUrl = url || SkinSteve
|
||||
},
|
||||
cape(url) {
|
||||
if (!url) {
|
||||
this.viewer.playerObject.cape.visible = false
|
||||
return
|
||||
}
|
||||
this.viewer.capeUrl = url
|
||||
},
|
||||
model(value) {
|
||||
this.viewer.playerObject.skin.slim = value === 'alex'
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.initPreviewer()
|
||||
emit('skinViewerMounted', this.$refs.previewer)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.viewer.dispose()
|
||||
},
|
||||
methods: {
|
||||
initPreviewer() {
|
||||
this.viewer = new skinview3d.SkinViewer({
|
||||
domElement: this.$refs.previewer,
|
||||
width: this.$refs.previewer.clientWidth,
|
||||
height: this.$refs.previewer.clientHeight,
|
||||
detectModel: false,
|
||||
skinUrl: this.skin || SkinSteve,
|
||||
capeUrl: this.cape,
|
||||
})
|
||||
this.viewer.camera.position.z = this.initPositionZ
|
||||
this.viewer.animation = new skinview3d.CompositeAnimation()
|
||||
|
||||
this.handles = {
|
||||
walk: this.viewer.animation.add(skinview3d.WalkingAnimation),
|
||||
run: this.viewer.animation.add(skinview3d.RunningAnimation),
|
||||
rotate: this.viewer.animation.add(skinview3d.RotatingAnimation),
|
||||
}
|
||||
this.handles.run.paused = true
|
||||
this.control = skinview3d.createOrbitControls(this.viewer)
|
||||
},
|
||||
togglePause() {
|
||||
this.paused = !this.paused
|
||||
this.viewer.animationPaused = !this.viewer.animationPaused
|
||||
},
|
||||
toggleRun() {
|
||||
this.handles.run.paused = !this.handles.run.paused
|
||||
this.handles.walk.paused = false
|
||||
},
|
||||
toggleRotate() {
|
||||
this.handles.rotate.paused = !this.handles.rotate.paused
|
||||
},
|
||||
reset() {
|
||||
this.viewer.dispose()
|
||||
this.initPreviewer()
|
||||
this.handles.walk.paused = true
|
||||
this.handles.run.paused = true
|
||||
this.handles.rotate.paused = true
|
||||
this.viewer.camera.position.z = 70
|
||||
this.viewer.playerObject.skin.slim = this.model === 'alex'
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@media (min-width: 992px)
|
||||
.previewer-3d
|
||||
min-height 500px
|
||||
|
||||
.previewer-3d canvas
|
||||
cursor move
|
||||
|
||||
.operations
|
||||
i
|
||||
padding .5em .5em
|
||||
display inline
|
||||
&:hover
|
||||
color #555
|
||||
cursor pointer
|
||||
</style>
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
import Vue from 'vue'
|
||||
import { showModal, toast } from '../../scripts/notify'
|
||||
|
||||
export default Vue.extend<{
|
||||
tid: number
|
||||
}, { setAsAvatar(): Promise<void> }, {}>({
|
||||
methods: {
|
||||
async setAsAvatar() {
|
||||
try {
|
||||
await showModal({
|
||||
title: this.$t('user.setAvatar'),
|
||||
text: this.$t('user.setAvatarNotice'),
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const { code, message } = await this.$http.post(
|
||||
'/user/profile/avatar',
|
||||
{ tid: this.tid },
|
||||
)
|
||||
if (code === 0) {
|
||||
toast.success(message!)
|
||||
|
||||
Array
|
||||
.from(
|
||||
document.querySelectorAll<HTMLImageElement>('[alt="User Image"]'),
|
||||
)
|
||||
.forEach(el => (el.src += `?${new Date().getTime()}`))
|
||||
} else {
|
||||
toast.error(message!)
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
|
||||
export default function useBlessingExtra<T>(key: string): T {
|
||||
const [value, setValue] = useState<T>({} as T)
|
||||
export default function useBlessingExtra<T>(key: string, defaultValue?: T): T {
|
||||
const [value, setValue] = useState<T>(defaultValue!)
|
||||
|
||||
useEffect(() => {
|
||||
setValue(blessing.extra[key] as T)
|
||||
|
|
|
|||
16
resources/assets/src/scripts/hooks/useMount.ts
Normal file
16
resources/assets/src/scripts/hooks/useMount.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { useEffect, useMemo } from 'react'
|
||||
|
||||
export default function useMount(selector: string): HTMLElement {
|
||||
const container = useMemo(() => document.createElement('div'), [])
|
||||
|
||||
useEffect(() => {
|
||||
const mount = document.querySelector(selector)!
|
||||
mount.appendChild(container)
|
||||
|
||||
return () => {
|
||||
mount.removeChild(container)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return container
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
import React from 'react'
|
||||
|
||||
const virtual = document.createElement('div')
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '/',
|
||||
|
|
@ -118,8 +116,8 @@ export default [
|
|||
},
|
||||
{
|
||||
path: 'skinlib/show/(\\d+)',
|
||||
component: () => import('../views/skinlib/Show.vue'),
|
||||
el: virtual,
|
||||
react: () => import('../views/skinlib/Show'),
|
||||
el: '#side',
|
||||
},
|
||||
{
|
||||
path: 'skinlib/upload',
|
||||
|
|
|
|||
|
|
@ -1,442 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<portal selector="#previewer" :disabled="disablePortal">
|
||||
<previewer
|
||||
:skin="type !== 'cape' && textureUrl"
|
||||
:cape="type === 'cape' ? textureUrl : ''"
|
||||
:model="type"
|
||||
:init-position-z="60"
|
||||
>
|
||||
<template #footer>
|
||||
<button
|
||||
v-if="anonymous"
|
||||
class="btn btn-outline-secondary"
|
||||
disabled
|
||||
:title="$t('skinlib.show.anonymous')"
|
||||
>
|
||||
{{ $t('skinlib.addToCloset') }}
|
||||
</button>
|
||||
<div v-else class="d-flex justify-content-between">
|
||||
<div>
|
||||
<button
|
||||
v-if="liked"
|
||||
class="btn btn-outline-success mr-2"
|
||||
data-toggle="modal"
|
||||
data-target="#modal-use-as"
|
||||
@click="fetchPlayersList"
|
||||
>
|
||||
{{ $t('skinlib.apply') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="liked"
|
||||
class="btn btn-outline-primary mr-2"
|
||||
data-test="removeFromCloset"
|
||||
@click="removeFromCloset"
|
||||
>
|
||||
{{ $t('skinlib.removeFromCloset') }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="btn btn-outline-primary mr-2"
|
||||
data-test="addToCloset"
|
||||
@click="addToCloset"
|
||||
>
|
||||
{{ $t('skinlib.addToCloset') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="type !== 'cape'"
|
||||
class="btn btn-outline-info mr-2"
|
||||
data-test="setAsAvatar"
|
||||
@click="setAsAvatar"
|
||||
>
|
||||
{{ $t('user.setAsAvatar') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="canBeDownloaded"
|
||||
class="btn btn-outline-info mr-2"
|
||||
data-test="download"
|
||||
@click="download"
|
||||
>
|
||||
{{ $t('skinlib.show.download') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-outline-info mr-2"
|
||||
data-test="report"
|
||||
@click="report"
|
||||
>
|
||||
{{ $t('skinlib.report.title') }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="pt-2 likes"
|
||||
:class="[liked ? 'text-red' : 'text-gray']"
|
||||
:title="$t('skinlib.show.likes')"
|
||||
>
|
||||
<i class="fas fa-heart" />
|
||||
<span>{{ likes }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</previewer>
|
||||
</portal>
|
||||
|
||||
<portal selector="#texture-info" :disabled="disablePortal">
|
||||
<div class="card card-primary">
|
||||
<div class="card-header">
|
||||
<h3 v-t="'skinlib.show.detail'" class="card-title" />
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td v-t="'skinlib.show.name'" />
|
||||
<td>
|
||||
{{ name|truncate }}
|
||||
<small v-if="hasEditPermission">
|
||||
<a v-t="'skinlib.show.edit'" href="#" @click="changeTextureName" />
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td v-t="'skinlib.show.model'" />
|
||||
<td>
|
||||
<span v-if="type === 'cape'">{{ $t('general.cape') }}</span>
|
||||
<span v-else>{{ type }}</span>
|
||||
<small v-if="hasEditPermission">
|
||||
<a
|
||||
href="#"
|
||||
data-toggle="modal"
|
||||
data-target="#modal-type"
|
||||
@click="editingType = type"
|
||||
>
|
||||
{{ $t('skinlib.show.edit') }}
|
||||
</a>
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Hash</td>
|
||||
<td>
|
||||
<span :title="hash">{{ hash.slice(0, 15) }}...</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td v-t="'skinlib.show.size'" />
|
||||
<td>{{ size }} KB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td v-t="'skinlib.show.uploader'" />
|
||||
<td v-if="uploaderNickName !== null">
|
||||
<a
|
||||
:href="`${baseUrl}/skinlib?filter=${type === 'cape' ? 'cape' : 'skin'}&uploader=${uploader}`"
|
||||
>{{ uploaderNickName }}</a>
|
||||
<br>
|
||||
<span
|
||||
v-for="(badge, i) in badges"
|
||||
:key="i"
|
||||
class="badge mr-2"
|
||||
:class="`bg-${badge.color}`"
|
||||
>{{ badge.text }}</span>
|
||||
</td>
|
||||
<td v-else><span v-t="'general.unexistent-user'" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td v-t="'skinlib.show.upload-at'" />
|
||||
<td>{{ uploadAt }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</portal>
|
||||
|
||||
<portal selector="#operations" :disabled="disablePortal">
|
||||
<div v-if="hasEditPermission" class="card card-warning">
|
||||
<div class="card-header">
|
||||
<h3 v-t="'admin.operationsTitle'" class="card-title" />
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p v-t="'skinlib.show.manage-notice'" />
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<div class="container d-flex justify-content-between">
|
||||
<button class="btn btn-warning" @click="togglePrivacy">
|
||||
{{ $t(togglePrivacyText) }}
|
||||
</button>
|
||||
<button class="btn btn-danger" @click="deleteTexture">
|
||||
{{ $t('skinlib.show.delete-texture') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</portal>
|
||||
|
||||
<portal selector="#modals" :disabled="disablePortal">
|
||||
<apply-to-player-dialog
|
||||
ref="useAs"
|
||||
:allow-add="false"
|
||||
:skin="type !== 'cape' ? tid : 0"
|
||||
:cape="type === 'cape' ? tid : 0"
|
||||
/>
|
||||
|
||||
<modal
|
||||
id="modal-type"
|
||||
:title="$t(this.$t('skinlib.setNewTextureModel'))"
|
||||
center
|
||||
@confirm="changeModel"
|
||||
>
|
||||
<label class="mr-3">
|
||||
<input
|
||||
v-model="editingType"
|
||||
type="radio"
|
||||
name="type"
|
||||
value="steve"
|
||||
>
|
||||
Steve
|
||||
</label>
|
||||
<label class="mr-3">
|
||||
<input
|
||||
v-model="editingType"
|
||||
type="radio"
|
||||
name="type"
|
||||
value="alex"
|
||||
>
|
||||
Alex
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
v-model="editingType"
|
||||
type="radio"
|
||||
name="type"
|
||||
value="cape"
|
||||
>
|
||||
{{ $t('general.cape') }}
|
||||
</label>
|
||||
</modal>
|
||||
</portal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Modal from '../../components/Modal.vue'
|
||||
import Portal from '../../components/Portal'
|
||||
import setAsAvatar from '../../components/mixins/setAsAvatar'
|
||||
import addClosetItem from '../../components/mixins/addClosetItem'
|
||||
import removeClosetItem from '../../components/mixins/removeClosetItem'
|
||||
import emitMounted from '../../components/mixins/emitMounted'
|
||||
import truncateText from '../../components/mixins/truncateText'
|
||||
import ApplyToPlayerDialog from '../../components/ApplyToPlayerDialog.vue'
|
||||
import { showModal, toast } from '../../scripts/notify'
|
||||
import { truthy } from '../../scripts/validators'
|
||||
|
||||
export default {
|
||||
name: 'Show',
|
||||
components: {
|
||||
ApplyToPlayerDialog,
|
||||
Modal,
|
||||
Portal,
|
||||
Previewer: () => import('../../components/Previewer.vue'),
|
||||
},
|
||||
mixins: [
|
||||
emitMounted,
|
||||
addClosetItem,
|
||||
removeClosetItem,
|
||||
setAsAvatar,
|
||||
truncateText,
|
||||
],
|
||||
props: {
|
||||
baseUrl: {
|
||||
type: String,
|
||||
default: blessing.base_url,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tid: +this.$route[1],
|
||||
name: '',
|
||||
type: 'steve',
|
||||
likes: 0,
|
||||
hash: '',
|
||||
uploader: 0,
|
||||
size: 0,
|
||||
uploadAt: '',
|
||||
public: true,
|
||||
editingType: 'steve',
|
||||
liked: blessing.extra.inCloset,
|
||||
canBeDownloaded: blessing.extra.download,
|
||||
currentUid: blessing.extra.currentUid,
|
||||
admin: blessing.extra.admin,
|
||||
uploaderNickName: blessing.extra.nickname,
|
||||
reportScore: blessing.extra.report,
|
||||
badges: blessing.extra.badges,
|
||||
disablePortal: process.env.NODE_ENV === 'test',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
anonymous() {
|
||||
return !this.currentUid
|
||||
},
|
||||
hasEditPermission() {
|
||||
return this.uploader === this.currentUid || this.admin
|
||||
},
|
||||
togglePrivacyText() {
|
||||
return this.public ? 'skinlib.setAsPrivate' : 'skinlib.setAsPublic'
|
||||
},
|
||||
textureUrl() {
|
||||
return `${this.baseUrl}/textures/${this.hash}`
|
||||
},
|
||||
},
|
||||
beforeMount() {
|
||||
this.fetchData()
|
||||
},
|
||||
methods: {
|
||||
async fetchData() {
|
||||
const { data = {} } = await this.$http.get(`/skinlib/info/${this.tid}`)
|
||||
Object.assign(this.$data, data)
|
||||
this.uploadAt = data.upload_at
|
||||
},
|
||||
async addToCloset() {
|
||||
this.$once('like-toggled', () => {
|
||||
this.liked = true
|
||||
this.likes += 1
|
||||
})
|
||||
await this.addClosetItem()
|
||||
},
|
||||
async removeFromCloset() {
|
||||
this.$once('item-removed', () => {
|
||||
this.liked = false
|
||||
this.likes -= 1
|
||||
})
|
||||
await this.removeClosetItem()
|
||||
},
|
||||
download() {
|
||||
const a = document.createElement('a')
|
||||
a.href = `${this.baseUrl}/raw/${this.tid}`
|
||||
a.download = `${this.name}.png`
|
||||
const event = new MouseEvent('click')
|
||||
a.dispatchEvent(event)
|
||||
},
|
||||
async changeTextureName() {
|
||||
let value
|
||||
try {
|
||||
({ value } = await showModal({
|
||||
mode: 'prompt',
|
||||
text: this.$t('skinlib.setNewTextureName'),
|
||||
input: this.name,
|
||||
validator: truthy(this.$t('skinlib.emptyNewTextureName')),
|
||||
}))
|
||||
} catch (_) {
|
||||
return
|
||||
}
|
||||
|
||||
const { code, message } = await this.$http.post(
|
||||
'/skinlib/rename',
|
||||
{ tid: this.tid, new_name: value },
|
||||
)
|
||||
if (code === 0) {
|
||||
this.name = value
|
||||
toast.success(message)
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
},
|
||||
async changeModel() {
|
||||
const { code, message } = await this.$http.post(
|
||||
'/skinlib/model',
|
||||
{ tid: this.tid, model: this.editingType },
|
||||
)
|
||||
if (code === 0) {
|
||||
this.type = this.editingType
|
||||
toast.success(message)
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
},
|
||||
async togglePrivacy() {
|
||||
try {
|
||||
await showModal({
|
||||
text: this.public
|
||||
? this.$t('skinlib.setPrivateNotice')
|
||||
: this.$t('skinlib.setPublicNotice'),
|
||||
})
|
||||
} catch (_) {
|
||||
return
|
||||
}
|
||||
|
||||
const { code, message } = await this.$http.post(
|
||||
'/skinlib/privacy',
|
||||
{ tid: this.tid },
|
||||
)
|
||||
if (code === 0) {
|
||||
toast.success(message)
|
||||
this.public = !this.public
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
},
|
||||
async deleteTexture() {
|
||||
try {
|
||||
await showModal({
|
||||
text: this.$t('skinlib.deleteNotice'),
|
||||
okButtonType: 'danger',
|
||||
})
|
||||
} catch (_) {
|
||||
return
|
||||
}
|
||||
|
||||
const { code, message } = await this.$http.post(
|
||||
'/skinlib/delete',
|
||||
{ tid: this.tid },
|
||||
)
|
||||
if (code === 0) {
|
||||
toast.success(message)
|
||||
setTimeout(() => (window.location = `${this.baseUrl}/skinlib`), 1000)
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
},
|
||||
async report() {
|
||||
const prompt = (() => {
|
||||
if (this.reportScore > 0) {
|
||||
return this.$t('skinlib.report.positive', { score: this.reportScore })
|
||||
} else if (this.reportScore < 0) {
|
||||
return this.$t('skinlib.report.negative', { score: -this.reportScore })
|
||||
}
|
||||
return ''
|
||||
})()
|
||||
let reason
|
||||
try {
|
||||
({ value: reason } = await showModal({
|
||||
mode: 'prompt',
|
||||
title: this.$t('skinlib.report.title'),
|
||||
text: prompt,
|
||||
placeholder: this.$t('skinlib.report.reason'),
|
||||
}))
|
||||
} catch (_) {
|
||||
return
|
||||
}
|
||||
|
||||
const { code, message } = await this.$http.post(
|
||||
'/skinlib/report',
|
||||
{ tid: this.tid, reason },
|
||||
)
|
||||
if (code === 0) {
|
||||
toast.success(message)
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
},
|
||||
fetchPlayersList() {
|
||||
this.$refs.useAs.fetchList()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.table > tbody > tr > td
|
||||
border-top 0
|
||||
&:first-child
|
||||
min-width 30%
|
||||
</style>
|
||||
38
resources/assets/src/views/skinlib/Show/addClosetItem.ts
Normal file
38
resources/assets/src/views/skinlib/Show/addClosetItem.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { t } from '@/scripts/i18n'
|
||||
import * as fetch from '@/scripts/net'
|
||||
import { showModal, toast } from '@/scripts/notify'
|
||||
import { Texture } from '@/scripts/types'
|
||||
|
||||
export default async function addClosetItem(
|
||||
texture: Texture,
|
||||
): Promise<boolean> {
|
||||
let name: string
|
||||
try {
|
||||
const { value } = await showModal({
|
||||
mode: 'prompt',
|
||||
title: t('skinlib.setItemName'),
|
||||
text: t('skinlib.applyNotice'),
|
||||
input: texture.name,
|
||||
validator: (value: string) => {
|
||||
if (!value) {
|
||||
return t('skinlib.emptyItemName')
|
||||
}
|
||||
},
|
||||
})
|
||||
name = value
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
|
||||
const { code, message } = await fetch.post<fetch.ResponseBody>(
|
||||
'/user/closet/add',
|
||||
{ tid: texture.tid, name },
|
||||
)
|
||||
if (code === 0) {
|
||||
toast.success(message)
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
|
||||
return code === 0
|
||||
}
|
||||
434
resources/assets/src/views/skinlib/Show/index.tsx
Normal file
434
resources/assets/src/views/skinlib/Show/index.tsx
Normal file
|
|
@ -0,0 +1,434 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { hot } from 'react-hot-loader/root'
|
||||
import useBlessingExtra from '@/scripts/hooks/useBlessingExtra'
|
||||
import useMount from '@/scripts/hooks/useMount'
|
||||
import { t } from '@/scripts/i18n'
|
||||
import * as fetch from '@/scripts/net'
|
||||
import { showModal, toast } from '@/scripts/notify'
|
||||
import { Texture, TextureType } from '@/scripts/types'
|
||||
import ButtonEdit from '@/components/ButtonEdit'
|
||||
import ViewerSkeleton from '@/components/ViewerSkeleton'
|
||||
import ModalApply from '@/views/user/Closet/ModalApply'
|
||||
import removeClosetItem from '@/views/user/Closet/removeClosetItem'
|
||||
import setAsAvatar from '@/views/user/Closet/setAsAvatar'
|
||||
import addClosetItem from './addClosetItem'
|
||||
import styles from './styles.module.scss'
|
||||
|
||||
export type Badge = {
|
||||
color: string
|
||||
text: string
|
||||
}
|
||||
|
||||
const Previewer = React.lazy(() => import('@/components/Viewer'))
|
||||
|
||||
const Show: React.FC = () => {
|
||||
const [texture, setTexture] = useState<Texture>({} as Texture)
|
||||
const [showModalApply, setShowModalApply] = useState(false)
|
||||
const [liked, setLiked] = useState(false)
|
||||
const nickname = useBlessingExtra<string | null>('nickname')
|
||||
const currentUid = useBlessingExtra<number>('currentUid')
|
||||
const isAdmin = useBlessingExtra<boolean>('admin')
|
||||
const badges = useBlessingExtra<Badge[]>('badges', [])
|
||||
const canBeDownloaded = useBlessingExtra<boolean>('download')
|
||||
const reportScore = useBlessingExtra<number>('report')
|
||||
const container = useMount('#previewer')
|
||||
|
||||
useEffect(() => {
|
||||
const fetchInfo = async () => {
|
||||
const url = location.href
|
||||
.replace(blessing.base_url, '')
|
||||
.replace('show', 'info')
|
||||
|
||||
const { data } = await fetch.get<fetch.ResponseBody<Texture>>(url)
|
||||
setTexture(data)
|
||||
}
|
||||
fetchInfo()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setLiked(blessing.extra.inCloset as boolean)
|
||||
}, [])
|
||||
|
||||
const handleEditName = async () => {
|
||||
let name: string
|
||||
try {
|
||||
const { value } = await showModal({
|
||||
mode: 'prompt',
|
||||
text: t('skinlib.setNewTextureName'),
|
||||
input: texture.name,
|
||||
validator: (value: string) => {
|
||||
if (!value) {
|
||||
return t('skinlib.emptyNewTextureName')
|
||||
}
|
||||
},
|
||||
})
|
||||
name = value
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const { code, message } = await fetch.post<fetch.ResponseBody>(
|
||||
'/skinlib/rename',
|
||||
{
|
||||
tid: texture.tid,
|
||||
new_name: name,
|
||||
},
|
||||
)
|
||||
if (code === 0) {
|
||||
toast.success(message)
|
||||
setTexture(texture => ({ ...texture, name }))
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSwitchType = async () => {
|
||||
let type: TextureType
|
||||
try {
|
||||
const { value } = await showModal({
|
||||
mode: 'prompt',
|
||||
text: t('skinlib.setNewTextureModel'),
|
||||
input: texture.type,
|
||||
inputType: 'radios',
|
||||
choices: [
|
||||
{ text: 'Steve', value: 'steve' },
|
||||
{ text: 'Alex', value: 'alex' },
|
||||
{ text: t('general.cape'), value: 'cape' },
|
||||
],
|
||||
})
|
||||
type = value as TextureType
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const { code, message } = await fetch.post<fetch.ResponseBody>(
|
||||
'/skinlib/model',
|
||||
{
|
||||
tid: texture.tid,
|
||||
model: type,
|
||||
},
|
||||
)
|
||||
if (code === 0) {
|
||||
toast.success(message)
|
||||
setTexture(texture => ({ ...texture, type }))
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddItemClick = async () => {
|
||||
const ok = await addClosetItem(texture)
|
||||
if (ok) {
|
||||
setTexture(texture => ({ ...texture, likes: texture.likes + 1 }))
|
||||
setLiked(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveItemClick = async () => {
|
||||
const ok = await removeClosetItem(texture.tid)
|
||||
if (ok) {
|
||||
setTexture(texture => ({ ...texture, likes: texture.likes - 1 }))
|
||||
setLiked(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSetAsAvatar = () => setAsAvatar(texture.tid)
|
||||
|
||||
const handleDownloadClick = () => {
|
||||
const a = document.createElement('a')
|
||||
a.href = `${blessing.base_url}/raw/${texture.tid}`
|
||||
a.download = `${texture.name}.png`
|
||||
a.click()
|
||||
}
|
||||
|
||||
const handleReport = async () => {
|
||||
const prompt = (() => {
|
||||
if (reportScore > 0) {
|
||||
return t('skinlib.report.positive', { score: reportScore })
|
||||
} else if (reportScore < 0) {
|
||||
return t('skinlib.report.negative', { score: -reportScore })
|
||||
}
|
||||
return ''
|
||||
})()
|
||||
|
||||
let reason: string
|
||||
try {
|
||||
const { value } = await showModal({
|
||||
mode: 'prompt',
|
||||
title: t('skinlib.report.title'),
|
||||
text: prompt,
|
||||
placeholder: t('skinlib.report.reason'),
|
||||
})
|
||||
reason = value
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const { code, message } = await fetch.post<fetch.ResponseBody>(
|
||||
'/skinlib/report',
|
||||
{
|
||||
tid: texture.tid,
|
||||
reason,
|
||||
},
|
||||
)
|
||||
if (code === 0) {
|
||||
toast.success(message)
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePrivacyClick = async () => {
|
||||
try {
|
||||
await showModal({
|
||||
text: texture.public
|
||||
? t('skinlib.setPrivateNotice')
|
||||
: t('skinlib.setPublicNotice'),
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const { code, message } = await fetch.post<fetch.ResponseBody>(
|
||||
'/skinlib/privacy',
|
||||
{ tid: texture.tid },
|
||||
)
|
||||
if (code === 0) {
|
||||
toast.success(message)
|
||||
setTexture(texture => ({ ...texture, public: !texture.public }))
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteTextureClick = async () => {
|
||||
try {
|
||||
await showModal({
|
||||
text: t('skinlib.deleteNotice'),
|
||||
okButtonType: 'danger',
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const { code, message } = await fetch.post<fetch.ResponseBody>(
|
||||
'/skinlib/delete',
|
||||
{ tid: texture.tid },
|
||||
)
|
||||
if (code === 0) {
|
||||
toast.success(message)
|
||||
setTimeout(() => {
|
||||
window.location.href = `${blessing.base_url}/skinlib`
|
||||
}, 2000)
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
}
|
||||
|
||||
const handleOpenModalApply = () => setShowModalApply(true)
|
||||
const handleCloseModalApply = () => setShowModalApply(false)
|
||||
|
||||
const linkToUploader = (() => {
|
||||
const search = new URLSearchParams()
|
||||
search.append('filter', texture.type === 'cape' ? 'cape' : 'skin')
|
||||
search.append('uploader', texture.uploader?.toString())
|
||||
|
||||
return `${blessing.base_url}/skinlib?${search}`
|
||||
})()
|
||||
|
||||
const canEdit = currentUid === texture.uploader || isAdmin
|
||||
const textureUrl = `${blessing.base_url}/textures/${texture.hash}`
|
||||
|
||||
return (
|
||||
<>
|
||||
{createPortal(
|
||||
<React.Suspense fallback={<ViewerSkeleton />}>
|
||||
<Previewer
|
||||
{...{ [texture.type === 'cape' ? 'cape' : 'skin']: textureUrl }}
|
||||
isAlex={texture.type === 'alex'}
|
||||
initPositionZ={60}
|
||||
>
|
||||
{currentUid === 0 ? (
|
||||
<button
|
||||
className="btn btn-outline-secondary"
|
||||
title={t('skinlib.show.anonymous')}
|
||||
disabled
|
||||
>
|
||||
{t('skinlib.addToCloset')}
|
||||
</button>
|
||||
) : (
|
||||
<div className="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
{liked && (
|
||||
<button
|
||||
className="btn btn-outline-success mr-2"
|
||||
onClick={handleOpenModalApply}
|
||||
>
|
||||
{t('skinlib.apply')}
|
||||
</button>
|
||||
)}
|
||||
{liked ? (
|
||||
<button
|
||||
className="btn btn-outline-primary mr-2"
|
||||
onClick={handleRemoveItemClick}
|
||||
>
|
||||
{t('skinlib.removeFromCloset')}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="btn btn-outline-primary mr-2"
|
||||
onClick={handleAddItemClick}
|
||||
>
|
||||
{t('skinlib.addToCloset')}
|
||||
</button>
|
||||
)}
|
||||
{texture.type !== 'cape' && (
|
||||
<button
|
||||
className="btn btn-outline-info mr-2"
|
||||
onClick={handleSetAsAvatar}
|
||||
>
|
||||
{t('user.setAsAvatar')}
|
||||
</button>
|
||||
)}
|
||||
{canBeDownloaded && (
|
||||
<button
|
||||
className="btn btn-outline-info mr-2"
|
||||
onClick={handleDownloadClick}
|
||||
>
|
||||
{t('skinlib.show.download')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-outline-info mr-2"
|
||||
onClick={handleReport}
|
||||
>
|
||||
{t('skinlib.report.title')}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className={liked ? 'text-red' : 'text-gray'}
|
||||
title={t('skinlib.show.likes')}
|
||||
>
|
||||
<i className="fas fa-heart mr-1" />
|
||||
<span>{texture.likes}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Previewer>
|
||||
</React.Suspense>,
|
||||
container,
|
||||
)}
|
||||
<div className="card card-primary">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">{t('skinlib.show.detail')}</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="container">
|
||||
<div className="row mt-2 mb-4">
|
||||
<div className="col-4">{t('skinlib.show.name')}</div>
|
||||
<div className={`col-7 ${styles.truncate}`} title={texture.name}>
|
||||
{texture.name}
|
||||
</div>
|
||||
{canEdit && (
|
||||
<div className="col-1">
|
||||
<ButtonEdit
|
||||
title={t('skinlib.show.edit')}
|
||||
onClick={handleEditName}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="row my-4">
|
||||
<div className="col-4">{t('skinlib.show.model')}</div>
|
||||
<div className="col-7">
|
||||
{texture.type === 'cape' ? t('general.cape') : texture.type}
|
||||
</div>
|
||||
{canEdit && (
|
||||
<div className="col-1">
|
||||
<ButtonEdit
|
||||
title={t('skinlib.show.edit')}
|
||||
onClick={handleSwitchType}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="row my-4">
|
||||
<div className="col-4">Hash</div>
|
||||
<div className={`col-8 ${styles.truncate}`} title={texture.hash}>
|
||||
{texture.hash}
|
||||
</div>
|
||||
</div>
|
||||
<div className="row my-4">
|
||||
<div className="col-4">{t('skinlib.show.size')}</div>
|
||||
<div className="col-8">{texture.size} KB</div>
|
||||
</div>
|
||||
<div className="row my-4">
|
||||
<div className="col-4">{t('skinlib.show.uploader')}</div>
|
||||
<div className={`col-8 ${styles.truncate}`}>
|
||||
{nickname !== null ? (
|
||||
<>
|
||||
<div>
|
||||
<a href={linkToUploader} target="_blank">
|
||||
{nickname}
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
{badges.map(badge => (
|
||||
<span
|
||||
className={`badge bg-${badge.color} mr-2`}
|
||||
key={badge.text}
|
||||
>
|
||||
{badge.text}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
t('general.unexistent-user')
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="row mt-4 mb-2">
|
||||
<div className="col-4">{t('skinlib.show.upload-at')}</div>
|
||||
<div className="col-8">{texture.upload_at}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{canEdit && (
|
||||
<div className="card card-warning">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">{t('admin.operationsTitle')}</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<p>{t('skinlib.show.manage-notice')}</p>
|
||||
</div>
|
||||
<div className="card-footer">
|
||||
<div className="container d-flex justify-content-between">
|
||||
<button className="btn btn-warning" onClick={handlePrivacyClick}>
|
||||
{texture.public
|
||||
? t('skinlib.setAsPrivate')
|
||||
: t('skinlib.setAsPublic')}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onClick={handleDeleteTextureClick}
|
||||
>
|
||||
{t('skinlib.show.delete-texture')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<ModalApply
|
||||
show={showModalApply}
|
||||
canAdd={false}
|
||||
{...{ [texture.type === 'cape' ? 'cape' : 'skin']: texture.tid }}
|
||||
onClose={handleCloseModalApply}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default hot(Show)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
@use '../../../styles/utils';
|
||||
|
||||
.truncate {
|
||||
@include utils.truncate-text;
|
||||
}
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
import React from 'react'
|
||||
import { t } from '@/scripts/i18n'
|
||||
import * as fetch from '@/scripts/net'
|
||||
import { showModal, toast } from '@/scripts/notify'
|
||||
import { ClosetItem } from '@/scripts/types'
|
||||
import setAsAvatar from './setAsAvatar'
|
||||
import styles from './ClosetItem.module.scss'
|
||||
|
||||
interface Props {
|
||||
|
|
@ -20,29 +19,7 @@ const ClosetItem: React.FC<Props> = props => {
|
|||
props.onClick(item)
|
||||
}
|
||||
|
||||
const setAsAvatar = async () => {
|
||||
try {
|
||||
await showModal({
|
||||
title: t('user.setAvatar'),
|
||||
text: t('user.setAvatarNotice'),
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const { code, message } = await fetch.post<fetch.ResponseBody>(
|
||||
'/user/profile/avatar',
|
||||
{ tid: item.tid },
|
||||
)
|
||||
if (code === 0) {
|
||||
toast.success(message)
|
||||
document
|
||||
.querySelectorAll<HTMLImageElement>('[alt="User Image"]')
|
||||
.forEach(el => (el.src += `?${new Date().getTime()}`))
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
}
|
||||
const handleSetAsAvatar = () => setAsAvatar(item.tid)
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -84,7 +61,7 @@ const ClosetItem: React.FC<Props> = props => {
|
|||
>
|
||||
{t('user.viewInSkinlib')}
|
||||
</a>
|
||||
<a href="#" className="dropdown-item" onClick={setAsAvatar}>
|
||||
<a href="#" className="dropdown-item" onClick={handleSetAsAvatar}>
|
||||
{t('user.setAsAvatar')}
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import Pagination from '@/components/Pagination'
|
|||
import ClosetItem from './ClosetItem'
|
||||
import Previewer from './Previewer'
|
||||
import ModalApply from './ModalApply'
|
||||
import removeClosetItem from './removeClosetItem'
|
||||
|
||||
type Category = 'skin' | 'cape'
|
||||
|
||||
|
|
@ -125,24 +126,10 @@ const Closet: React.FC = () => {
|
|||
}
|
||||
|
||||
const removeItem = async (item: Item) => {
|
||||
try {
|
||||
await showModal({
|
||||
text: t('user.removeFromClosetNotice'),
|
||||
okButtonType: 'danger',
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const { code, message } = await fetch.post<fetch.ResponseBody>(
|
||||
`/user/closet/remove/${item.tid}`,
|
||||
)
|
||||
if (code === 0) {
|
||||
toast.success(message)
|
||||
const { tid } = item
|
||||
const { tid } = item
|
||||
const ok = await removeClosetItem(tid)
|
||||
if (ok) {
|
||||
setItems(items => items.filter(item => item.tid !== tid))
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
25
resources/assets/src/views/user/Closet/removeClosetItem.ts
Normal file
25
resources/assets/src/views/user/Closet/removeClosetItem.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { t } from '@/scripts/i18n'
|
||||
import * as fetch from '@/scripts/net'
|
||||
import { showModal, toast } from '@/scripts/notify'
|
||||
|
||||
export default async function removeClosetItem(tid: number): Promise<boolean> {
|
||||
try {
|
||||
await showModal({
|
||||
text: t('user.removeFromClosetNotice'),
|
||||
okButtonType: 'danger',
|
||||
})
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
|
||||
const { code, message } = await fetch.post<fetch.ResponseBody>(
|
||||
`/user/closet/remove/${tid}`,
|
||||
)
|
||||
if (code === 0) {
|
||||
toast.success(message)
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
|
||||
return code === 0
|
||||
}
|
||||
27
resources/assets/src/views/user/Closet/setAsAvatar.ts
Normal file
27
resources/assets/src/views/user/Closet/setAsAvatar.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { t } from '@/scripts/i18n'
|
||||
import * as fetch from '@/scripts/net'
|
||||
import { showModal, toast } from '@/scripts/notify'
|
||||
|
||||
export default async function setAsAvatar(tid: number) {
|
||||
try {
|
||||
await showModal({
|
||||
title: t('user.setAvatar'),
|
||||
text: t('user.setAvatarNotice'),
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const { code, message } = await fetch.post<fetch.ResponseBody>(
|
||||
'/user/profile/avatar',
|
||||
{ tid },
|
||||
)
|
||||
if (code === 0) {
|
||||
toast.success(message)
|
||||
document
|
||||
.querySelectorAll<HTMLImageElement>('[alt="User Image"]')
|
||||
.forEach(el => (el.src += `?${new Date().getTime()}`))
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
import 'bootstrap'
|
||||
import Vue from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { flushPromises } from '../utils'
|
||||
import { trans } from '@/scripts/i18n'
|
||||
import { toast } from '@/scripts/notify'
|
||||
import ApplyToPlayerDialog from '@/components/ApplyToPlayerDialog.vue'
|
||||
|
||||
jest.mock('@/scripts/notify')
|
||||
|
||||
test('submit applying texture', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({ data: [{ pid: 1, name: 'a' }] })
|
||||
Vue.prototype.$http.post.mockResolvedValueOnce({ code: 1 })
|
||||
.mockResolvedValue({ code: 0, message: 'ok' })
|
||||
const wrapper = mount<Vue & { fetchList(): Promise<void> }>(ApplyToPlayerDialog)
|
||||
await wrapper.vm.fetchList()
|
||||
const button = wrapper.find('.btn-outline-info')
|
||||
|
||||
button.trigger('click')
|
||||
expect(toast.info).toBeCalledWith(trans('user.emptySelectedTexture'))
|
||||
|
||||
wrapper.setProps({ skin: 1 })
|
||||
button.trigger('click')
|
||||
expect(Vue.prototype.$http.post).toBeCalledWith(
|
||||
'/user/player/set/1',
|
||||
{
|
||||
skin: 1,
|
||||
cape: undefined,
|
||||
},
|
||||
)
|
||||
wrapper.setProps({ skin: 0, cape: 1 })
|
||||
button.trigger('click')
|
||||
expect(Vue.prototype.$http.post).toBeCalledWith(
|
||||
'/user/player/set/1',
|
||||
{
|
||||
skin: undefined,
|
||||
cape: 1,
|
||||
},
|
||||
)
|
||||
await flushPromises()
|
||||
expect(toast.success).toBeCalledWith('ok')
|
||||
})
|
||||
|
||||
test('compute avatar URL', () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({ data: {} })
|
||||
// eslint-disable-next-line camelcase
|
||||
const wrapper = mount<Vue & { avatarUrl(player: { tid_skin: number }): string }>(ApplyToPlayerDialog)
|
||||
const { avatarUrl } = wrapper.vm
|
||||
expect(avatarUrl({ tid_skin: 1 })).toBe('/avatar/1?3d&size=45')
|
||||
})
|
||||
|
||||
test('search players', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({ data: [{ pid: 1, name: 'abc' }] })
|
||||
const wrapper = mount<Vue & { fetchList(): Promise<void> }>(ApplyToPlayerDialog)
|
||||
await wrapper.vm.fetchList()
|
||||
|
||||
wrapper.find('input').setValue('e')
|
||||
expect(wrapper.find('.btn-outline-info').exists()).toBeFalse()
|
||||
|
||||
wrapper.find('input').setValue('b')
|
||||
expect(wrapper.find('.btn-outline-info').exists()).toBeTrue()
|
||||
})
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
import { mount } from '@vue/test-utils'
|
||||
import Portal from '@/components/Portal'
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
|
||||
test('render default slot', () => {
|
||||
mount(Portal, {
|
||||
propsData: {
|
||||
selector: 'body',
|
||||
},
|
||||
slots: {
|
||||
default: 'children',
|
||||
},
|
||||
})
|
||||
expect(document.querySelectorAll('div')).toHaveLength(1)
|
||||
expect(document.querySelector('div')!.textContent).toBe('children')
|
||||
})
|
||||
|
||||
test('custom wrapper tag', () => {
|
||||
mount(Portal, {
|
||||
propsData: {
|
||||
selector: 'body',
|
||||
tag: 'span',
|
||||
},
|
||||
})
|
||||
expect(document.querySelectorAll('span')).toHaveLength(1)
|
||||
})
|
||||
|
||||
test('disable portal', () => {
|
||||
document.body.innerHTML = '<div id="c"></div>'
|
||||
mount(Portal, {
|
||||
propsData: {
|
||||
selector: '#c',
|
||||
disabled: true,
|
||||
},
|
||||
slots: {
|
||||
default: 'children',
|
||||
},
|
||||
})
|
||||
expect(document.querySelector('#c')!.innerHTML).toBe('')
|
||||
})
|
||||
|
||||
test('should pass if container does not exist', () => {
|
||||
mount(Portal, {
|
||||
propsData: {
|
||||
selector: '#nope',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('replace container content', () => {
|
||||
document.body.innerHTML = '<div>before</div>'
|
||||
mount(Portal, {
|
||||
propsData: {
|
||||
selector: 'body',
|
||||
},
|
||||
slots: {
|
||||
default: 'after',
|
||||
},
|
||||
})
|
||||
expect(document.querySelector('div')!.textContent).toBe('after')
|
||||
})
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
import Vue from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Previewer from '@/components/Previewer.vue'
|
||||
import * as emitter from '@/scripts/event'
|
||||
import * as mockedSkinview3d from '../__mocks__/skinview3d'
|
||||
|
||||
type Viewer = Vue & { viewer: mockedSkinview3d.SkinViewer }
|
||||
|
||||
interface Handles {
|
||||
handles: {
|
||||
run: { paused: boolean }
|
||||
walk: { paused: boolean }
|
||||
rotate: { paused: boolean }
|
||||
}
|
||||
}
|
||||
|
||||
test('initialize skinview3d', () => {
|
||||
const stub = jest.fn()
|
||||
emitter.on('skinViewerMounted', stub)
|
||||
|
||||
const wrapper = mount<Viewer>(Previewer)
|
||||
expect(wrapper.vm.viewer).toBeInstanceOf(mockedSkinview3d.SkinViewer)
|
||||
expect(wrapper.vm.viewer.camera.position.z).toBe(70)
|
||||
expect(stub).toBeCalledWith(expect.any(HTMLElement))
|
||||
})
|
||||
|
||||
test('dispose viewer before destroy', () => {
|
||||
const wrapper = mount<Viewer>(Previewer)
|
||||
wrapper.destroy()
|
||||
expect(wrapper.vm.viewer.disposed).toBeTrue()
|
||||
})
|
||||
|
||||
test('skin URL should be updated', () => {
|
||||
const wrapper = mount<Viewer>(Previewer)
|
||||
wrapper.setProps({ skin: 'abc' })
|
||||
expect(wrapper.vm.viewer.skinUrl).toBe('abc')
|
||||
})
|
||||
|
||||
test('cape URL should be updated', () => {
|
||||
const wrapper = mount<Viewer>(Previewer)
|
||||
wrapper.setProps({ cape: 'abc' })
|
||||
expect(wrapper.vm.viewer.capeUrl).toBe('abc')
|
||||
})
|
||||
|
||||
test('`footer` slot', () => {
|
||||
const wrapper = mount(Previewer, {
|
||||
slots: {
|
||||
footer: '<div id="footer" />',
|
||||
},
|
||||
})
|
||||
expect(wrapper.find('#footer').exists()).toBeTrue()
|
||||
})
|
||||
|
||||
test('disable closet mode', () => {
|
||||
const wrapper = mount(Previewer)
|
||||
expect(wrapper.find('.badge').text()).toBe('')
|
||||
})
|
||||
|
||||
test('enable closet mode', () => {
|
||||
const wrapper = mount(Previewer, {
|
||||
propsData: {
|
||||
closetMode: true,
|
||||
},
|
||||
})
|
||||
expect(wrapper.find('.badge').text()).toBe('')
|
||||
|
||||
wrapper.setProps({ skin: 'abc' })
|
||||
expect(wrapper.find('.badge').text()).toBe('general.skin')
|
||||
|
||||
wrapper.setProps({ cape: 'abc', skin: '' })
|
||||
expect(wrapper.find('.badge').text()).toBe('general.cape')
|
||||
|
||||
wrapper.setProps({ skin: 'abc', cape: 'abc' })
|
||||
expect(wrapper.find('.badge').text()).toBe('general.skin & general.cape')
|
||||
})
|
||||
|
||||
test('toggle pause', () => {
|
||||
const wrapper = mount(Previewer)
|
||||
const pauseButton = wrapper.find('.fa-pause')
|
||||
expect(pauseButton.exists()).toBeTrue()
|
||||
pauseButton.trigger('click')
|
||||
expect(wrapper.find('.fa-play').exists()).toBeTrue()
|
||||
expect(wrapper.find('.fa-pause').exists()).toBeFalse()
|
||||
})
|
||||
|
||||
test('toggle run', () => {
|
||||
const wrapper = mount<Vue & Handles>(Previewer)
|
||||
wrapper.find('.fa-forward').trigger('click')
|
||||
expect(wrapper.vm.handles.run.paused).toBeFalse()
|
||||
expect(wrapper.vm.handles.walk.paused).toBeFalse()
|
||||
})
|
||||
|
||||
test('toggle rotate', () => {
|
||||
const wrapper = mount<Vue & Handles>(Previewer)
|
||||
wrapper.find('.fa-redo-alt').trigger('click')
|
||||
expect(wrapper.vm.handles.rotate.paused).toBeTrue()
|
||||
})
|
||||
|
||||
test('reset', () => {
|
||||
mockedSkinview3d.SkinViewer.prototype.dispose = jest.fn(
|
||||
function (this: mockedSkinview3d.SkinViewer) {
|
||||
this.disposed = true
|
||||
}.bind(new mockedSkinview3d.SkinViewer()),
|
||||
)
|
||||
const wrapper = mount<Viewer>(Previewer, {
|
||||
propsData: {
|
||||
model: 'alex',
|
||||
},
|
||||
})
|
||||
wrapper.find('.fa-stop').trigger('click')
|
||||
expect(mockedSkinview3d.SkinViewer.prototype.dispose).toBeCalled()
|
||||
expect(wrapper.vm.viewer.playerObject.skin.slim).toBeTrue()
|
||||
})
|
||||
|
||||
test('custom title', () => {
|
||||
const wrapper = mount(Previewer, { propsData: { title: 'custom-title' } })
|
||||
expect(wrapper.text()).toContain('custom-title')
|
||||
})
|
||||
2
resources/assets/tests/types.d.ts
vendored
2
resources/assets/tests/types.d.ts
vendored
|
|
@ -12,7 +12,7 @@ interface Window {
|
|||
site_name: string
|
||||
version: string
|
||||
i18n: object
|
||||
extra: object
|
||||
extra: any
|
||||
}
|
||||
|
||||
fetch: jest.Mock
|
||||
|
|
|
|||
|
|
@ -1,489 +0,0 @@
|
|||
import Vue from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { flushPromises } from '../../utils'
|
||||
import { trans } from '@/scripts/i18n'
|
||||
import { showModal, toast } from '@/scripts/notify'
|
||||
import Show from '@/views/skinlib/Show.vue'
|
||||
|
||||
jest.mock('@/scripts/notify')
|
||||
|
||||
type Component = Vue & {
|
||||
liked: boolean
|
||||
likes: number
|
||||
public: boolean
|
||||
name: string
|
||||
type: 'steve' | 'alex' | 'cape'
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
window.blessing.extra = {
|
||||
download: true,
|
||||
currentUid: 0,
|
||||
admin: false,
|
||||
nickname: 'author',
|
||||
inCloset: false,
|
||||
badges: [],
|
||||
}
|
||||
})
|
||||
|
||||
const previewer = Vue.extend({
|
||||
render(h) {
|
||||
return h('div', this.$slots.footer)
|
||||
},
|
||||
})
|
||||
|
||||
test('button for adding to closet should be disabled if not auth', () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({ data: {} })
|
||||
const wrapper = mount(Show, {
|
||||
mocks: {
|
||||
$route: ['/skinlib/show/1', '1'],
|
||||
},
|
||||
stubs: { previewer },
|
||||
})
|
||||
expect(wrapper.find('.btn').attributes('disabled')).toBe('disabled')
|
||||
})
|
||||
|
||||
test('button for adding to closet should be enabled if auth', () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({ data: {} })
|
||||
Object.assign(window.blessing.extra, { inCloset: true, currentUid: 1 })
|
||||
const wrapper = mount(Show, {
|
||||
mocks: {
|
||||
$route: ['/skinlib/show/1', '1'],
|
||||
},
|
||||
stubs: { previewer },
|
||||
})
|
||||
expect(wrapper.find('[data-test="removeFromCloset"]').exists()).toBeTrue()
|
||||
})
|
||||
|
||||
test('likes count indicator', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({ data: { likes: 2 } })
|
||||
Object.assign(window.blessing.extra, { inCloset: true, currentUid: 1 })
|
||||
const wrapper = mount(Show, {
|
||||
mocks: {
|
||||
$route: ['/skinlib/show/1', '1'],
|
||||
},
|
||||
stubs: { previewer },
|
||||
})
|
||||
await flushPromises()
|
||||
expect(wrapper.find('.likes').classes()).toContain('text-red')
|
||||
expect(wrapper.find('.likes').text()).toContain('2')
|
||||
})
|
||||
|
||||
test('render basic information', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({
|
||||
data: {
|
||||
name: 'my-texture',
|
||||
type: 'alex',
|
||||
hash: '123',
|
||||
size: 2,
|
||||
upload_at: '2018',
|
||||
},
|
||||
})
|
||||
const wrapper = mount(Show, {
|
||||
mocks: {
|
||||
$route: ['/skinlib/show/1', '1'],
|
||||
},
|
||||
})
|
||||
await flushPromises()
|
||||
const text = wrapper.find('.card-primary').text()
|
||||
expect(text).toContain('my-texture')
|
||||
expect(text).toContain('alex')
|
||||
expect(text).toContain('123...')
|
||||
expect(text).toContain('2 KB')
|
||||
expect(text).toContain('2018')
|
||||
expect(text).toContain('author')
|
||||
})
|
||||
|
||||
test('render action text of editing texture name', async () => {
|
||||
Object.assign(window.blessing.extra, { admin: true })
|
||||
Vue.prototype.$http.get.mockResolvedValue({ data: { uploader: 1, name: 'name' } })
|
||||
|
||||
let wrapper = mount(Show, {
|
||||
mocks: {
|
||||
$route: ['/skinlib/show/1', '1'],
|
||||
},
|
||||
})
|
||||
await flushPromises()
|
||||
expect(wrapper.contains('small')).toBeTrue()
|
||||
|
||||
Object.assign(window.blessing.extra, { currentUid: 2, admin: false })
|
||||
wrapper = mount(Show, {
|
||||
mocks: {
|
||||
$route: ['/skinlib/show/1', '1'],
|
||||
},
|
||||
})
|
||||
await flushPromises()
|
||||
expect(wrapper.contains('small')).toBeFalse()
|
||||
})
|
||||
|
||||
test('render nickname of uploader', () => {
|
||||
Object.assign(window.blessing.extra, { nickname: null })
|
||||
Vue.prototype.$http.get.mockResolvedValue({ data: {} })
|
||||
const wrapper = mount(Show, {
|
||||
mocks: {
|
||||
$route: ['/skinlib/show/1', '1'],
|
||||
},
|
||||
})
|
||||
expect(wrapper.text()).toContain('general.unexistent-user')
|
||||
})
|
||||
|
||||
test('operation panel should not be rendered if user is anonymous', async () => {
|
||||
Object.assign(window.blessing.extra, { currentUid: 0 })
|
||||
Vue.prototype.$http.get.mockResolvedValue({ data: { uploader: 1 } })
|
||||
const wrapper = mount(Show, {
|
||||
mocks: {
|
||||
$route: ['/skinlib/show/1', '1'],
|
||||
},
|
||||
})
|
||||
await flushPromises()
|
||||
expect(wrapper.find('.card-warning').exists()).toBeFalse()
|
||||
})
|
||||
|
||||
test('operation panel should not be rendered if not privileged', async () => {
|
||||
Object.assign(window.blessing.extra, { currentUid: 2 })
|
||||
Vue.prototype.$http.get.mockResolvedValue({ data: { uploader: 1 } })
|
||||
const wrapper = mount(Show, {
|
||||
mocks: {
|
||||
$route: ['/skinlib/show/1', '1'],
|
||||
},
|
||||
})
|
||||
await flushPromises()
|
||||
expect(wrapper.find('.card-warning').exists()).toBeFalse()
|
||||
})
|
||||
|
||||
test('operation panel should be rendered if privileged', async () => {
|
||||
Object.assign(window.blessing.extra, { currentUid: 1 })
|
||||
Vue.prototype.$http.get.mockResolvedValue({ data: { uploader: 1 } })
|
||||
const wrapper = mount(Show, {
|
||||
mocks: {
|
||||
$route: ['/skinlib/show/1', '1'],
|
||||
},
|
||||
})
|
||||
await flushPromises()
|
||||
expect(wrapper.find('.card-warning').exists()).toBeTrue()
|
||||
})
|
||||
|
||||
test('download texture', async () => {
|
||||
Object.assign(window.blessing.extra, { currentUid: 1 })
|
||||
Vue.prototype.$http.get.mockResolvedValue({ data: { tid: 1, name: 'abc' } })
|
||||
const wrapper = mount(Show, {
|
||||
mocks: {
|
||||
$route: ['/skinlib/show/1', '1'],
|
||||
},
|
||||
})
|
||||
await flushPromises()
|
||||
wrapper.find('[data-test="download"]').trigger('click')
|
||||
})
|
||||
|
||||
test('link to downloading texture', async () => {
|
||||
Object.assign(window.blessing.extra, { download: false })
|
||||
Vue.prototype.$http.get.mockResolvedValue({ data: { name: '', hash: '123' } })
|
||||
const wrapper = mount(Show, {
|
||||
mocks: {
|
||||
$route: ['/skinlib/show/1', '1'],
|
||||
},
|
||||
})
|
||||
await flushPromises()
|
||||
expect(wrapper.find('span[title="123"]').exists()).toBeTrue()
|
||||
})
|
||||
|
||||
test('set as avatar', async () => {
|
||||
Object.assign(window.blessing.extra, { currentUid: 1, inCloset: true })
|
||||
Vue.prototype.$http.get.mockResolvedValueOnce({ data: { type: 'steve' } })
|
||||
const wrapper = mount(Show, {
|
||||
mocks: {
|
||||
$route: ['/skinlib/show/1', '1'],
|
||||
},
|
||||
stubs: { previewer },
|
||||
})
|
||||
await flushPromises()
|
||||
wrapper.find('[data-test="setAsAvatar"]').trigger('click')
|
||||
expect(showModal).toBeCalled()
|
||||
})
|
||||
|
||||
test('hide "set avatar" button when texture is cape', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValueOnce({ data: { type: 'cape' } })
|
||||
const wrapper = mount(Show, {
|
||||
mocks: {
|
||||
$route: ['/skinlib/show/1', '1'],
|
||||
},
|
||||
stubs: { previewer },
|
||||
})
|
||||
await flushPromises()
|
||||
expect(wrapper.find('[data-test="setAsAvatar"]').exists()).toBeFalse()
|
||||
})
|
||||
|
||||
test('add to closet', async () => {
|
||||
Object.assign(window.blessing.extra, { currentUid: 1, inCloset: false })
|
||||
Vue.prototype.$http.get.mockResolvedValue({ data: { name: 'wow', likes: 2 } })
|
||||
Vue.prototype.$http.post.mockResolvedValue({ code: 0, message: '' })
|
||||
showModal.mockResolvedValue({ value: 'a' })
|
||||
const wrapper = mount<Component>(Show, {
|
||||
mocks: {
|
||||
$route: ['/skinlib/show/1', '1'],
|
||||
},
|
||||
stubs: { previewer },
|
||||
})
|
||||
wrapper.find('[data-test="addToCloset"]').trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.vm.likes).toBe(3)
|
||||
expect(wrapper.vm.liked).toBeTrue()
|
||||
})
|
||||
|
||||
test('remove from closet', async () => {
|
||||
Object.assign(window.blessing.extra, { currentUid: 1, inCloset: true })
|
||||
Vue.prototype.$http.get.mockResolvedValue({ data: { likes: 2 } })
|
||||
Vue.prototype.$http.post.mockResolvedValue({ code: 0 })
|
||||
const wrapper = mount<Component>(Show, {
|
||||
mocks: {
|
||||
$route: ['/skinlib/show/1', '1'],
|
||||
},
|
||||
stubs: { previewer },
|
||||
})
|
||||
wrapper.find('[data-test="removeFromCloset"]').trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.vm.likes).toBe(1)
|
||||
expect(wrapper.vm.liked).toBeFalse()
|
||||
})
|
||||
|
||||
test('change texture name', async () => {
|
||||
Object.assign(window.blessing.extra, { admin: true })
|
||||
Vue.prototype.$http.get.mockResolvedValue({ data: { name: 'old-name' } })
|
||||
Vue.prototype.$http.post
|
||||
.mockResolvedValueOnce({ code: 1, message: '1' })
|
||||
.mockResolvedValue({ code: 0, message: '0' })
|
||||
showModal
|
||||
.mockRejectedValueOnce(null)
|
||||
.mockResolvedValue({ value: 'new-name' })
|
||||
const wrapper = mount<Component>(Show, {
|
||||
mocks: {
|
||||
$route: ['/skinlib/show/1', '1'],
|
||||
},
|
||||
stubs: { previewer },
|
||||
})
|
||||
const button = wrapper.find('small > a')
|
||||
|
||||
button.trigger('click')
|
||||
expect(Vue.prototype.$http.post).not.toBeCalled()
|
||||
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(Vue.prototype.$http.post).toBeCalledWith(
|
||||
'/skinlib/rename',
|
||||
{ tid: 1, new_name: 'new-name' },
|
||||
)
|
||||
expect(toast.error).toBeCalledWith('1')
|
||||
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.vm.name).toBe('new-name')
|
||||
})
|
||||
|
||||
test('change texture model', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({ data: { type: 'steve' } })
|
||||
Vue.prototype.$http.post
|
||||
.mockResolvedValueOnce({ code: 1, message: '1' })
|
||||
.mockResolvedValue({ code: 0, message: '0' })
|
||||
const wrapper = mount<Component>(Show, {
|
||||
mocks: {
|
||||
$route: ['/skinlib/show/1', '1'],
|
||||
},
|
||||
stubs: { previewer },
|
||||
})
|
||||
const modal = wrapper.find('#modal-type')
|
||||
const button = wrapper
|
||||
.findAll('small')
|
||||
.at(1)
|
||||
.find('a')
|
||||
|
||||
button.trigger('click')
|
||||
wrapper
|
||||
.findAll('[type=radio]')
|
||||
.at(1)
|
||||
.setChecked()
|
||||
modal.vm.$emit('confirm')
|
||||
await flushPromises()
|
||||
expect(Vue.prototype.$http.post).toBeCalledWith(
|
||||
'/skinlib/model',
|
||||
{ tid: 1, model: 'alex' },
|
||||
)
|
||||
expect(toast.error).toBeCalledWith('1')
|
||||
|
||||
button.trigger('click')
|
||||
wrapper
|
||||
.findAll('[type=radio]')
|
||||
.at(1)
|
||||
.setChecked()
|
||||
modal.vm.$emit('confirm')
|
||||
await flushPromises()
|
||||
expect(wrapper.vm.type).toBe('alex')
|
||||
})
|
||||
|
||||
test('toggle privacy', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({ data: { public: true } })
|
||||
Vue.prototype.$http.post
|
||||
.mockResolvedValueOnce({ code: 1, message: '1' })
|
||||
.mockResolvedValue({ code: 0, message: '0' })
|
||||
showModal
|
||||
.mockRejectedValueOnce(null)
|
||||
.mockResolvedValue({ value: '' })
|
||||
const wrapper = mount<Component>(Show, {
|
||||
mocks: {
|
||||
$route: ['/skinlib/show/1', '1'],
|
||||
},
|
||||
stubs: { previewer },
|
||||
})
|
||||
const button = wrapper.find('.btn-warning')
|
||||
|
||||
button.trigger('click')
|
||||
expect(Vue.prototype.$http.post).not.toBeCalled()
|
||||
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(Vue.prototype.$http.post).toBeCalledWith(
|
||||
'/skinlib/privacy',
|
||||
{ tid: 1 },
|
||||
)
|
||||
expect(toast.error).toBeCalledWith('1')
|
||||
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.vm.public).toBeFalse()
|
||||
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.vm.public).toBeTrue()
|
||||
})
|
||||
|
||||
test('delete texture', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({ data: {} })
|
||||
Vue.prototype.$http.post
|
||||
.mockResolvedValueOnce({ code: 1, message: '1' })
|
||||
.mockResolvedValue({ code: 0, message: '0' })
|
||||
showModal
|
||||
.mockRejectedValueOnce(null)
|
||||
.mockResolvedValue({ value: '' })
|
||||
const wrapper = mount(Show, {
|
||||
mocks: {
|
||||
$route: ['/skinlib/show/1', '1'],
|
||||
},
|
||||
stubs: { previewer },
|
||||
})
|
||||
const button = wrapper.find('.btn-danger')
|
||||
|
||||
button.trigger('click')
|
||||
expect(Vue.prototype.$http.post).not.toBeCalled()
|
||||
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(Vue.prototype.$http.post).toBeCalledWith(
|
||||
'/skinlib/delete',
|
||||
{ tid: 1 },
|
||||
)
|
||||
expect(toast.error).toBeCalledWith('1')
|
||||
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
jest.runAllTimers()
|
||||
expect(toast.success).toBeCalledWith('0')
|
||||
})
|
||||
|
||||
test('report texture', async () => {
|
||||
Object.assign(window.blessing.extra, { currentUid: 1 })
|
||||
Vue.prototype.$http.get.mockResolvedValue({ data: { report: 0 } })
|
||||
Vue.prototype.$http.post
|
||||
.mockResolvedValueOnce({ code: 1, message: 'duplicated' })
|
||||
.mockResolvedValue({ code: 0, message: 'success' })
|
||||
showModal
|
||||
.mockRejectedValueOnce(null)
|
||||
.mockRejectedValueOnce(null)
|
||||
.mockResolvedValue({ value: 'reason' })
|
||||
const wrapper = mount(Show, {
|
||||
mocks: {
|
||||
$route: ['/skinlib/show/1', '1'],
|
||||
},
|
||||
stubs: { previewer },
|
||||
})
|
||||
|
||||
const button = wrapper.find('[data-test=report]')
|
||||
button.trigger('click')
|
||||
expect(showModal).toBeCalledWith({
|
||||
mode: 'prompt',
|
||||
title: trans('skinlib.report.title'),
|
||||
text: '',
|
||||
placeholder: trans('skinlib.report.reason'),
|
||||
})
|
||||
expect(Vue.prototype.$http.post).not.toBeCalled()
|
||||
|
||||
wrapper.setData({ reportScore: -5 })
|
||||
button.trigger('click')
|
||||
expect(showModal).toBeCalledWith({
|
||||
mode: 'prompt',
|
||||
title: trans('skinlib.report.title'),
|
||||
text: trans('skinlib.report.negative', { score: 5 }),
|
||||
placeholder: trans('skinlib.report.reason'),
|
||||
})
|
||||
|
||||
wrapper.setData({ reportScore: 5 })
|
||||
button.trigger('click')
|
||||
expect(showModal).toBeCalledWith({
|
||||
mode: 'prompt',
|
||||
title: trans('skinlib.report.title'),
|
||||
text: trans('skinlib.report.positive', { score: 5 }),
|
||||
placeholder: trans('skinlib.report.reason'),
|
||||
})
|
||||
await flushPromises()
|
||||
expect(Vue.prototype.$http.post).toBeCalledWith(
|
||||
'/skinlib/report',
|
||||
{ tid: 1, reason: 'reason' },
|
||||
)
|
||||
expect(toast.error).toBeCalledWith('duplicated')
|
||||
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(toast.success).toBeCalledWith('success')
|
||||
})
|
||||
|
||||
test('apply texture to player', () => {
|
||||
Object.assign(window.blessing.extra, { currentUid: 1, inCloset: true })
|
||||
Vue.prototype.$http.get
|
||||
.mockResolvedValue({ data: {} })
|
||||
.mockResolvedValue([])
|
||||
const wrapper = mount(Show, {
|
||||
mocks: {
|
||||
$route: ['/skinlib/show/1', '1'],
|
||||
},
|
||||
stubs: { previewer },
|
||||
})
|
||||
wrapper.find('[data-target="#modal-use-as"]').trigger('click')
|
||||
expect(wrapper.find('[data-target="#modal-add-player"]').exists()).toBeFalse()
|
||||
})
|
||||
|
||||
test('truncate too long texture name', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({
|
||||
data: {
|
||||
name: 'very-very-long-texture-name',
|
||||
},
|
||||
})
|
||||
const wrapper = mount(Show, {
|
||||
mocks: {
|
||||
$route: ['/skinlib/show/1', '1'],
|
||||
},
|
||||
})
|
||||
await flushPromises()
|
||||
expect(wrapper.find('.card-primary').text()).toContain('very-very-long-...')
|
||||
})
|
||||
|
||||
test('render badges', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({ data: {} })
|
||||
Object.assign(window.blessing.extra, {
|
||||
badges: [{ text: 'reina', color: 'purple' }],
|
||||
})
|
||||
const wrapper = mount(Show, {
|
||||
mocks: {
|
||||
$route: ['/skinlib/show/1', '1'],
|
||||
},
|
||||
})
|
||||
await flushPromises()
|
||||
expect(wrapper.find('.badge.bg-purple').text()).toBe('reina')
|
||||
})
|
||||
649
resources/assets/tests/views/skinlib/Show.test.tsx
Normal file
649
resources/assets/tests/views/skinlib/Show.test.tsx
Normal file
|
|
@ -0,0 +1,649 @@
|
|||
import React from 'react'
|
||||
import { render, fireEvent, wait } from '@testing-library/react'
|
||||
import { t } from '@/scripts/i18n'
|
||||
import * as fetch from '@/scripts/net'
|
||||
import { Texture } from '@/scripts/types'
|
||||
import Show, { Badge } from '@/views/skinlib/Show'
|
||||
|
||||
jest.mock('@/scripts/net')
|
||||
|
||||
const fixtureSkin: Readonly<Texture> = Object.freeze<Texture>({
|
||||
tid: 1,
|
||||
name: 'skin',
|
||||
type: 'steve',
|
||||
hash: 'abc',
|
||||
size: 2,
|
||||
uploader: 1,
|
||||
public: true,
|
||||
upload_at: new Date().toString(),
|
||||
likes: 1,
|
||||
})
|
||||
|
||||
const fixtureCape: Readonly<Texture> = Object.freeze<Texture>({
|
||||
tid: 2,
|
||||
name: 'cape',
|
||||
type: 'cape',
|
||||
hash: 'def',
|
||||
size: 2,
|
||||
uploader: 1,
|
||||
public: true,
|
||||
upload_at: new Date().toString(),
|
||||
likes: 1,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
const container = document.createElement('div')
|
||||
container.id = 'previewer'
|
||||
document.body.appendChild(container)
|
||||
|
||||
window.blessing.extra = {
|
||||
download: true,
|
||||
currentUid: 0,
|
||||
admin: false,
|
||||
nickname: 'author',
|
||||
inCloset: false,
|
||||
report: 0,
|
||||
badges: [],
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
document.querySelector('#previewer')!.remove()
|
||||
})
|
||||
|
||||
test('without authenticated', async () => {
|
||||
fetch.get.mockResolvedValue({ data: fixtureSkin })
|
||||
|
||||
const { queryByText, queryByTitle } = render(<Show />)
|
||||
await wait()
|
||||
|
||||
expect(queryByText(fixtureSkin.name)).toBeInTheDocument()
|
||||
expect(queryByText('steve')).toBeInTheDocument()
|
||||
expect(queryByText(`${fixtureSkin.size} KB`)).toBeInTheDocument()
|
||||
expect(queryByText(fixtureSkin.hash)).toBeInTheDocument()
|
||||
expect(queryByText(window.blessing.extra.nickname)).toHaveAttribute(
|
||||
'href',
|
||||
`/skinlib?filter=skin&uploader=${fixtureSkin.uploader}`,
|
||||
)
|
||||
expect(queryByTitle(t('skinlib.show.edit'))).not.toBeInTheDocument()
|
||||
expect(queryByText(t('skinlib.addToCloset'))).toBeDisabled()
|
||||
})
|
||||
|
||||
test('authenticated but not uploader', async () => {
|
||||
fetch.get.mockResolvedValue({ data: fixtureCape })
|
||||
|
||||
const { queryByText, queryByTitle } = render(<Show />)
|
||||
await wait()
|
||||
|
||||
expect(queryByText(fixtureCape.name)).toBeInTheDocument()
|
||||
expect(queryByText(t('general.cape'))).toBeInTheDocument()
|
||||
expect(queryByText(`${fixtureCape.size} KB`)).toBeInTheDocument()
|
||||
expect(queryByText(fixtureCape.hash)).toBeInTheDocument()
|
||||
expect(queryByText(window.blessing.extra.nickname)).toHaveAttribute(
|
||||
'href',
|
||||
`/skinlib?filter=cape&uploader=${fixtureCape.uploader}`,
|
||||
)
|
||||
expect(queryByTitle(t('skinlib.show.edit'))).not.toBeInTheDocument()
|
||||
expect(queryByText(t('user.setAsAvatar'))).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('uploader is not existed', async () => {
|
||||
window.blessing.extra.nickname = null
|
||||
fetch.get.mockResolvedValue({ data: fixtureSkin })
|
||||
|
||||
const { queryByText } = render(<Show />)
|
||||
await wait()
|
||||
|
||||
expect(queryByText(t('general.unexistent-user'))).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('badges', async () => {
|
||||
window.blessing.extra.badges = [
|
||||
{ text: 'STAFF', color: 'primary' },
|
||||
] as Badge[]
|
||||
fetch.get.mockResolvedValue({ data: fixtureSkin })
|
||||
|
||||
const { queryByText } = render(<Show />)
|
||||
await wait()
|
||||
|
||||
expect(queryByText('STAFF')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('apply to player', async () => {
|
||||
window.blessing.extra.currentUid = 2
|
||||
window.blessing.extra.inCloset = true
|
||||
fetch.get
|
||||
.mockResolvedValueOnce({ data: fixtureSkin })
|
||||
.mockResolvedValueOnce({ data: [] })
|
||||
|
||||
const { getByText, getByLabelText } = render(<Show />)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getByText(t('skinlib.apply')))
|
||||
fireEvent.click(getByLabelText('Close'))
|
||||
|
||||
expect(fetch.get).toBeCalledTimes(2)
|
||||
})
|
||||
|
||||
test('set as avatar', async () => {
|
||||
window.blessing.extra.currentUid = fixtureSkin.uploader + 1
|
||||
fetch.get.mockResolvedValue({ data: fixtureSkin })
|
||||
fetch.post.mockResolvedValue({ code: 0, message: 'ok' })
|
||||
|
||||
const { getByText, getByRole, queryByText } = render(<Show />)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getByText(t('user.setAsAvatar')))
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
await wait()
|
||||
|
||||
expect(queryByText('ok')).toBeInTheDocument()
|
||||
expect(getByRole('status')).toHaveClass('alert-success')
|
||||
})
|
||||
|
||||
describe('download texture', () => {
|
||||
beforeEach(() => {
|
||||
window.blessing.extra.currentUid = fixtureSkin.uploader + 1
|
||||
fetch.get.mockResolvedValue({ data: fixtureSkin })
|
||||
})
|
||||
|
||||
it('allowed', async () => {
|
||||
const { getByText } = render(<Show />)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getByText(t('skinlib.show.download')))
|
||||
})
|
||||
|
||||
it('not allowed', async () => {
|
||||
window.blessing.extra.download = false
|
||||
const { queryByText } = render(<Show />)
|
||||
await wait()
|
||||
|
||||
expect(queryByText(t('skinlib.show.download'))).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('operation panel', () => {
|
||||
beforeEach(() => {
|
||||
fetch.get.mockResolvedValue({ data: fixtureSkin })
|
||||
})
|
||||
|
||||
it('uploader', async () => {
|
||||
window.blessing.extra.currentUid = fixtureSkin.uploader
|
||||
|
||||
const { queryByText } = render(<Show />)
|
||||
await wait()
|
||||
|
||||
expect(queryByText(t('skinlib.show.manage-notice'))).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('administrator', async () => {
|
||||
window.blessing.extra.currentUid = fixtureSkin.uploader + 1
|
||||
window.blessing.extra.admin = true
|
||||
|
||||
const { queryByText } = render(<Show />)
|
||||
await wait()
|
||||
|
||||
expect(queryByText(t('skinlib.show.manage-notice'))).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('edit texture name', () => {
|
||||
beforeEach(() => {
|
||||
window.blessing.extra.currentUid = fixtureSkin.uploader
|
||||
fetch.get.mockResolvedValue({ data: fixtureSkin })
|
||||
})
|
||||
|
||||
it('cancelled', async () => {
|
||||
const { getByText, getAllByTitle, getByDisplayValue, queryByText } = render(
|
||||
<Show />,
|
||||
)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getAllByTitle(t('skinlib.show.edit'))[0])
|
||||
fireEvent.input(getByDisplayValue(fixtureSkin.name), {
|
||||
target: { value: '' },
|
||||
})
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
expect(queryByText(t('skinlib.emptyNewTextureName'))).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(getByText(t('general.cancel')))
|
||||
await wait()
|
||||
|
||||
expect(fetch.post).not.toBeCalled()
|
||||
expect(queryByText(fixtureSkin.name)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('succeeded', async () => {
|
||||
fetch.post.mockResolvedValue({ code: 0, message: 'ok' })
|
||||
|
||||
const {
|
||||
getByText,
|
||||
getAllByTitle,
|
||||
getByDisplayValue,
|
||||
getByRole,
|
||||
queryByText,
|
||||
} = render(<Show />)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getAllByTitle(t('skinlib.show.edit'))[0])
|
||||
fireEvent.input(getByDisplayValue(fixtureSkin.name), {
|
||||
target: { value: 't' },
|
||||
})
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
await wait()
|
||||
|
||||
expect(fetch.post).toBeCalledWith('/skinlib/rename', {
|
||||
tid: fixtureSkin.tid,
|
||||
new_name: 't',
|
||||
})
|
||||
expect(queryByText('ok')).toBeInTheDocument()
|
||||
expect(getByRole('status')).toHaveClass('alert-success')
|
||||
expect(queryByText('t')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('failed', async () => {
|
||||
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
|
||||
|
||||
const {
|
||||
getByText,
|
||||
getAllByTitle,
|
||||
getByDisplayValue,
|
||||
getByRole,
|
||||
queryByText,
|
||||
} = render(<Show />)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getAllByTitle(t('skinlib.show.edit'))[0])
|
||||
fireEvent.input(getByDisplayValue(fixtureSkin.name), {
|
||||
target: { value: 't' },
|
||||
})
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
await wait()
|
||||
|
||||
expect(fetch.post).toBeCalledWith('/skinlib/rename', {
|
||||
tid: fixtureSkin.tid,
|
||||
new_name: 't',
|
||||
})
|
||||
expect(queryByText('failed')).toBeInTheDocument()
|
||||
expect(getByRole('alert')).toHaveClass('alert-danger')
|
||||
expect(queryByText(fixtureSkin.name)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('edit texture type', () => {
|
||||
beforeEach(() => {
|
||||
Object.assign(window.blessing.extra, { currentUid: fixtureSkin.uploader })
|
||||
fetch.get.mockResolvedValue({ data: fixtureSkin })
|
||||
})
|
||||
|
||||
it('cancelled', async () => {
|
||||
const { getByText, getAllByTitle, getByLabelText, queryByText } = render(
|
||||
<Show />,
|
||||
)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getAllByTitle(t('skinlib.show.edit'))[1])
|
||||
fireEvent.click(getByLabelText('Alex'))
|
||||
fireEvent.click(getByText(t('general.cancel')))
|
||||
await wait()
|
||||
|
||||
expect(fetch.post).not.toBeCalled()
|
||||
expect(queryByText('steve')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('succeeded', async () => {
|
||||
fetch.post.mockResolvedValue({ code: 0, message: 'ok' })
|
||||
|
||||
const {
|
||||
getByText,
|
||||
getAllByTitle,
|
||||
getByLabelText,
|
||||
getByRole,
|
||||
queryByText,
|
||||
} = render(<Show />)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getAllByTitle(t('skinlib.show.edit'))[1])
|
||||
fireEvent.click(getByLabelText('Alex'))
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
await wait()
|
||||
|
||||
expect(fetch.post).toBeCalledWith('/skinlib/model', {
|
||||
tid: fixtureSkin.tid,
|
||||
model: 'alex',
|
||||
})
|
||||
expect(queryByText('ok')).toBeInTheDocument()
|
||||
expect(getByRole('status')).toHaveClass('alert-success')
|
||||
expect(queryByText('alex')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('failed', async () => {
|
||||
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
|
||||
|
||||
const {
|
||||
getByText,
|
||||
getAllByTitle,
|
||||
getByLabelText,
|
||||
getByRole,
|
||||
queryByText,
|
||||
} = render(<Show />)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getAllByTitle(t('skinlib.show.edit'))[1])
|
||||
fireEvent.click(getByLabelText('Alex'))
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
await wait()
|
||||
|
||||
expect(fetch.post).toBeCalledWith('/skinlib/model', {
|
||||
tid: fixtureSkin.tid,
|
||||
model: 'alex',
|
||||
})
|
||||
expect(queryByText('failed')).toBeInTheDocument()
|
||||
expect(getByRole('alert')).toHaveClass('alert-danger')
|
||||
expect(queryByText('steve')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('add to closet', () => {
|
||||
beforeEach(() => {
|
||||
window.blessing.extra.currentUid = fixtureSkin.uploader + 1
|
||||
fetch.get.mockResolvedValue({ data: fixtureSkin })
|
||||
})
|
||||
|
||||
it('cancelled', async () => {
|
||||
const { getByText, getByDisplayValue, queryByText } = render(<Show />)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getByText(t('skinlib.addToCloset')))
|
||||
fireEvent.input(getByDisplayValue(fixtureSkin.name), {
|
||||
target: { value: '' },
|
||||
})
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
expect(queryByText(t('skinlib.emptyItemName'))).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(getByText(t('general.cancel')))
|
||||
await wait()
|
||||
|
||||
expect(fetch.post).not.toBeCalled()
|
||||
})
|
||||
|
||||
it('succeeded', async () => {
|
||||
fetch.post.mockResolvedValue({ code: 0, message: 'ok' })
|
||||
|
||||
const { getByText, getByDisplayValue, getByRole, queryByText } = render(
|
||||
<Show />,
|
||||
)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getByText(t('skinlib.addToCloset')))
|
||||
fireEvent.input(getByDisplayValue(fixtureSkin.name), {
|
||||
target: { value: 't' },
|
||||
})
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
await wait()
|
||||
|
||||
expect(fetch.post).toBeCalledWith('/user/closet/add', {
|
||||
tid: fixtureSkin.tid,
|
||||
name: 't',
|
||||
})
|
||||
expect(queryByText('ok')).toBeInTheDocument()
|
||||
expect(getByRole('status')).toHaveClass('alert-success')
|
||||
expect(queryByText('2')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('failed', async () => {
|
||||
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
|
||||
|
||||
const { getByText, getByDisplayValue, getByRole, queryByText } = render(
|
||||
<Show />,
|
||||
)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getByText(t('skinlib.addToCloset')))
|
||||
fireEvent.input(getByDisplayValue(fixtureSkin.name), {
|
||||
target: { value: 't' },
|
||||
})
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
await wait()
|
||||
|
||||
expect(fetch.post).toBeCalledWith('/user/closet/add', {
|
||||
tid: fixtureSkin.tid,
|
||||
name: 't',
|
||||
})
|
||||
expect(queryByText('failed')).toBeInTheDocument()
|
||||
expect(getByRole('alert')).toHaveClass('alert-danger')
|
||||
expect(queryByText('1')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('remove from closet', () => {
|
||||
beforeEach(() => {
|
||||
window.blessing.extra.currentUid = fixtureSkin.uploader + 1
|
||||
window.blessing.extra.inCloset = true
|
||||
fetch.get.mockResolvedValue({ data: fixtureSkin })
|
||||
})
|
||||
|
||||
it('succeeded', async () => {
|
||||
fetch.post.mockResolvedValue({ code: 0, message: 'ok' })
|
||||
|
||||
const { getByText, getByRole, queryByText } = render(<Show />)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getByText(t('skinlib.removeFromCloset')))
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
await wait()
|
||||
|
||||
expect(fetch.post).toBeCalledWith(`/user/closet/remove/${fixtureSkin.tid}`)
|
||||
expect(queryByText('ok')).toBeInTheDocument()
|
||||
expect(getByRole('status')).toHaveClass('alert-success')
|
||||
expect(queryByText('0')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('failed', async () => {
|
||||
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
|
||||
|
||||
const { getByText, getByRole, queryByText } = render(<Show />)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getByText(t('skinlib.removeFromCloset')))
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
await wait()
|
||||
|
||||
expect(fetch.post).toBeCalledWith(`/user/closet/remove/${fixtureSkin.tid}`)
|
||||
expect(queryByText('failed')).toBeInTheDocument()
|
||||
expect(getByRole('alert')).toHaveClass('alert-danger')
|
||||
expect(queryByText('1')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('report texture', () => {
|
||||
beforeEach(() => {
|
||||
window.blessing.extra.currentUid = fixtureSkin.uploader + 1
|
||||
fetch.get.mockResolvedValue({ data: fixtureSkin })
|
||||
})
|
||||
|
||||
it('positive score', async () => {
|
||||
window.blessing.extra.report = 5
|
||||
|
||||
const { getByText, queryByText } = render(<Show />)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getByText(t('skinlib.report.title')))
|
||||
expect(queryByText(t('skinlib.report.positive', { score: 5 })))
|
||||
|
||||
fireEvent.click(getByText(t('general.cancel')))
|
||||
expect(fetch.post).not.toBeCalled()
|
||||
})
|
||||
|
||||
it('negative score', async () => {
|
||||
window.blessing.extra.report = -5
|
||||
|
||||
const { getByText, queryByText } = render(<Show />)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getByText(t('skinlib.report.title')))
|
||||
expect(queryByText(t('skinlib.report.negative', { score: 5 })))
|
||||
|
||||
fireEvent.click(getByText(t('general.cancel')))
|
||||
expect(fetch.post).not.toBeCalled()
|
||||
})
|
||||
|
||||
it('succeeded', async () => {
|
||||
fetch.post.mockResolvedValue({ code: 0, message: 'ok' })
|
||||
|
||||
const { getByText, getByPlaceholderText, getByRole, queryByText } = render(
|
||||
<Show />,
|
||||
)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getByText(t('skinlib.report.title')))
|
||||
fireEvent.input(getByPlaceholderText(t('skinlib.report.reason')), {
|
||||
target: { value: 'illegal' },
|
||||
})
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
await wait()
|
||||
|
||||
expect(fetch.post).toBeCalledWith('/skinlib/report', {
|
||||
tid: fixtureSkin.tid,
|
||||
reason: 'illegal',
|
||||
})
|
||||
expect(queryByText('ok')).toBeInTheDocument()
|
||||
expect(getByRole('status')).toHaveClass('alert-success')
|
||||
})
|
||||
|
||||
it('failed', async () => {
|
||||
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
|
||||
|
||||
const { getByText, getByPlaceholderText, getByRole, queryByText } = render(
|
||||
<Show />,
|
||||
)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getByText(t('skinlib.report.title')))
|
||||
fireEvent.input(getByPlaceholderText(t('skinlib.report.reason')), {
|
||||
target: { value: 'illegal' },
|
||||
})
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
await wait()
|
||||
|
||||
expect(fetch.post).toBeCalledWith('/skinlib/report', {
|
||||
tid: fixtureSkin.tid,
|
||||
reason: 'illegal',
|
||||
})
|
||||
expect(queryByText('failed')).toBeInTheDocument()
|
||||
expect(getByRole('alert')).toHaveClass('alert-danger')
|
||||
})
|
||||
})
|
||||
|
||||
describe('change privacy', () => {
|
||||
beforeEach(() => {
|
||||
window.blessing.extra.currentUid = fixtureSkin.uploader
|
||||
})
|
||||
|
||||
it('cancelled', async () => {
|
||||
fetch.get.mockResolvedValue({ data: fixtureSkin })
|
||||
|
||||
const { getByText, queryByText } = render(<Show />)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getByText(t('skinlib.setAsPrivate')))
|
||||
fireEvent.click(getByText(t('general.cancel')))
|
||||
await wait()
|
||||
|
||||
expect(fetch.post).not.toBeCalled()
|
||||
expect(queryByText(t('skinlib.setAsPrivate'))).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('succeeded', async () => {
|
||||
fetch.get.mockResolvedValue({ data: fixtureSkin })
|
||||
fetch.post.mockResolvedValue({ code: 0, message: 'ok' })
|
||||
|
||||
const { getByText, getByRole, queryByText } = render(<Show />)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getByText(t('skinlib.setAsPrivate')))
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
await wait()
|
||||
|
||||
expect(fetch.post).toBeCalledWith('/skinlib/privacy', {
|
||||
tid: fixtureSkin.tid,
|
||||
})
|
||||
expect(queryByText('ok')).toBeInTheDocument()
|
||||
expect(getByRole('status')).toHaveClass('alert-success')
|
||||
expect(queryByText(t('skinlib.setAsPublic'))).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('failed', async () => {
|
||||
fetch.get.mockResolvedValue({ data: { ...fixtureSkin, public: false } })
|
||||
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
|
||||
|
||||
const { getByText, getByRole, queryByText } = render(<Show />)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getByText(t('skinlib.setAsPublic')))
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
await wait()
|
||||
|
||||
expect(fetch.post).toBeCalledWith('/skinlib/privacy', {
|
||||
tid: fixtureSkin.tid,
|
||||
})
|
||||
expect(queryByText('failed')).toBeInTheDocument()
|
||||
expect(getByRole('alert')).toHaveClass('alert-danger')
|
||||
expect(queryByText(t('skinlib.setAsPublic'))).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete texture', () => {
|
||||
beforeEach(() => {
|
||||
window.blessing.extra.currentUid = fixtureSkin.uploader
|
||||
fetch.get.mockResolvedValue({ data: fixtureSkin })
|
||||
})
|
||||
|
||||
it('cancelled', async () => {
|
||||
const { getByText } = render(<Show />)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getByText(t('skinlib.show.delete-texture')))
|
||||
fireEvent.click(getByText(t('general.cancel')))
|
||||
await wait()
|
||||
|
||||
expect(fetch.post).not.toBeCalled()
|
||||
})
|
||||
|
||||
it('succeeded', async () => {
|
||||
fetch.post.mockResolvedValue({ code: 0, message: 'ok' })
|
||||
|
||||
const { getByText, getByRole, queryByText } = render(<Show />)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getByText(t('skinlib.show.delete-texture')))
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
await wait()
|
||||
|
||||
expect(fetch.post).toBeCalledWith('/skinlib/delete', {
|
||||
tid: fixtureSkin.tid,
|
||||
})
|
||||
expect(queryByText('ok')).toBeInTheDocument()
|
||||
expect(getByRole('status')).toHaveClass('alert-success')
|
||||
|
||||
jest.runAllTimers()
|
||||
})
|
||||
|
||||
it('failed', async () => {
|
||||
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
|
||||
|
||||
const { getByText, getByRole, queryByText } = render(<Show />)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getByText(t('skinlib.show.delete-texture')))
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
await wait()
|
||||
|
||||
expect(fetch.post).toBeCalledWith('/skinlib/delete', {
|
||||
tid: fixtureSkin.tid,
|
||||
})
|
||||
expect(queryByText('failed')).toBeInTheDocument()
|
||||
expect(getByRole('alert')).toHaveClass('alert-danger')
|
||||
})
|
||||
})
|
||||
|
|
@ -1 +0,0 @@
|
|||
<div id="texture-info"></div>
|
||||
|
|
@ -1 +0,0 @@
|
|||
<div id="operations"></div>
|
||||
1
resources/views/skinlib/widgets/show/side.twig
Normal file
1
resources/views/skinlib/widgets/show/side.twig
Normal file
|
|
@ -0,0 +1 @@
|
|||
<div id="side"></div>
|
||||
Loading…
Reference in New Issue
Block a user