mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
feat(webapp): badges UI (#8426)
- New badge UI, including editor. - Adds config to enable/disable badges. --------- Co-authored-by: Sebastian Stein <sebastian@codepassion.de> Co-authored-by: Maximilian Harz <maxharz@gmail.com>
This commit is contained in:
parent
0873fc748c
commit
2fd138697f
76
webapp/components/BadgeSelection.spec.js
Normal file
76
webapp/components/BadgeSelection.spec.js
Normal file
@ -0,0 +1,76 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/vue'
|
||||
import BadgeSelection from './BadgeSelection.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('Badges.vue', () => {
|
||||
const Wrapper = (propsData) => {
|
||||
return render(BadgeSelection, {
|
||||
propsData,
|
||||
localVue,
|
||||
})
|
||||
}
|
||||
|
||||
describe('without badges', () => {
|
||||
it('renders', () => {
|
||||
const wrapper = Wrapper({ badges: [] })
|
||||
expect(wrapper.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe('with badges', () => {
|
||||
const badges = [
|
||||
{
|
||||
id: '1',
|
||||
icon: '/path/to/some/icon',
|
||||
isDefault: false,
|
||||
description: 'Some description',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
icon: '/path/to/another/icon',
|
||||
isDefault: true,
|
||||
description: 'Another description',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
icon: '/path/to/third/icon',
|
||||
isDefault: false,
|
||||
description: 'Third description',
|
||||
},
|
||||
]
|
||||
|
||||
let wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper({ badges })
|
||||
})
|
||||
|
||||
it('renders', () => {
|
||||
expect(wrapper.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
describe('clicking on a badge', () => {
|
||||
beforeEach(async () => {
|
||||
const badge = screen.getByText(badges[1].description)
|
||||
await fireEvent.click(badge)
|
||||
})
|
||||
|
||||
it('emits badge-selected with badge', async () => {
|
||||
expect(wrapper.emitted()['badge-selected']).toEqual([[badges[1]]])
|
||||
})
|
||||
})
|
||||
|
||||
describe('clicking twice on a badge', () => {
|
||||
beforeEach(async () => {
|
||||
const badge = screen.getByText(badges[1].description)
|
||||
await fireEvent.click(badge)
|
||||
await fireEvent.click(badge)
|
||||
})
|
||||
|
||||
it('emits badge-selected with null', async () => {
|
||||
expect(wrapper.emitted()['badge-selected']).toEqual([[badges[1]], [null]])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
102
webapp/components/BadgeSelection.vue
Normal file
102
webapp/components/BadgeSelection.vue
Normal file
@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="badge-selection">
|
||||
<button
|
||||
v-for="(badge, index) in badges"
|
||||
:key="badge.id"
|
||||
class="badge-selection-item"
|
||||
@click="handleBadgeClick(badge, index)"
|
||||
>
|
||||
<div class="badge-icon">
|
||||
<img :src="badge.icon | proxyApiUrl" :alt="badge.id" />
|
||||
</div>
|
||||
<div class="badge-info">
|
||||
<div class="badge-description">{{ badge.description }}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'BadgeSelection',
|
||||
props: {
|
||||
badges: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedIndex: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleBadgeClick(badge, index) {
|
||||
if (this.selectedIndex === index) {
|
||||
this.selectedIndex = null
|
||||
this.$emit('badge-selected', null)
|
||||
return
|
||||
}
|
||||
|
||||
this.selectedIndex = index
|
||||
this.$emit('badge-selected', badge)
|
||||
},
|
||||
resetSelection() {
|
||||
this.selectedIndex = null
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.badge-selection {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
|
||||
.badge-selection-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 8px;
|
||||
background-color: #f5f5f5;
|
||||
transition: all 0.2s ease;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.badge-icon {
|
||||
flex-shrink: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-right: 16px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
flex-grow: 1;
|
||||
|
||||
.badge-title {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.badge-description {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,29 +1,114 @@
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import { render, screen, fireEvent } from '@testing-library/vue'
|
||||
import Badges from './Badges.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('Badges.vue', () => {
|
||||
let propsData
|
||||
const Wrapper = (propsData) => {
|
||||
return render(Badges, {
|
||||
propsData,
|
||||
localVue,
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
propsData = {}
|
||||
})
|
||||
|
||||
describe('shallowMount', () => {
|
||||
const Wrapper = () => {
|
||||
return shallowMount(Badges, { propsData })
|
||||
}
|
||||
|
||||
it('has class "hc-badges"', () => {
|
||||
expect(Wrapper().find('.hc-badges').exists()).toBe(true)
|
||||
describe('without badges', () => {
|
||||
it('renders in presentation mode', () => {
|
||||
const wrapper = Wrapper({ badges: [], selectionMode: false })
|
||||
expect(wrapper.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
describe('given a badge', () => {
|
||||
it('renders in selection mode', () => {
|
||||
const wrapper = Wrapper({ badges: [], selectionMode: true })
|
||||
expect(wrapper.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe('with badges', () => {
|
||||
const badges = [
|
||||
{
|
||||
id: '1',
|
||||
icon: '/path/to/some/icon',
|
||||
isDefault: false,
|
||||
description: 'Some description',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
icon: '/path/to/another/icon',
|
||||
isDefault: true,
|
||||
description: 'Another description',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
icon: '/path/to/third/icon',
|
||||
isDefault: false,
|
||||
description: 'Third description',
|
||||
},
|
||||
]
|
||||
|
||||
describe('in presentation mode', () => {
|
||||
let wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
propsData.badges = [{ id: '1', icon: '/path/to/some/icon' }]
|
||||
wrapper = Wrapper({ badges, scale: 1.2, selectionMode: false })
|
||||
})
|
||||
|
||||
it('proxies badge icon, which is just a URL without metadata', () => {
|
||||
expect(Wrapper().find('img[src="/api/path/to/some/icon"]').exists()).toBe(true)
|
||||
it('renders', () => {
|
||||
expect(wrapper.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('clicking on second badge does nothing', async () => {
|
||||
const badge = screen.getByTitle(badges[1].description)
|
||||
await fireEvent.click(badge)
|
||||
expect(wrapper.emitted()).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('in selection mode', () => {
|
||||
let wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper({ badges, scale: 1.2, selectionMode: true })
|
||||
})
|
||||
|
||||
it('renders', () => {
|
||||
expect(wrapper.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('clicking on first badge does nothing', async () => {
|
||||
const badge = screen.getByTitle(badges[0].description)
|
||||
await fireEvent.click(badge)
|
||||
expect(wrapper.emitted()).toEqual({})
|
||||
})
|
||||
|
||||
describe('clicking on second badge', () => {
|
||||
beforeEach(async () => {
|
||||
const badge = screen.getByTitle(badges[1].description)
|
||||
await fireEvent.click(badge)
|
||||
})
|
||||
|
||||
it('selects badge', () => {
|
||||
expect(wrapper.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('emits badge-selected with index', async () => {
|
||||
expect(wrapper.emitted()['badge-selected']).toEqual([[1]])
|
||||
})
|
||||
})
|
||||
|
||||
describe('clicking twice on second badge', () => {
|
||||
beforeEach(async () => {
|
||||
const badge = screen.getByTitle(badges[1].description)
|
||||
await fireEvent.click(badge)
|
||||
await fireEvent.click(badge)
|
||||
})
|
||||
|
||||
it('deselects badge', () => {
|
||||
expect(wrapper.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('emits badge-selected with null', async () => {
|
||||
expect(wrapper.emitted()['badge-selected']).toEqual([[1], [null]])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,69 +1,171 @@
|
||||
<template>
|
||||
<div :class="[badges.length === 2 && 'hc-badges-dual']" class="hc-badges">
|
||||
<div v-for="badge in badges" :key="badge.id" class="hc-badge-container">
|
||||
<img :title="badge.key" :src="badge.icon | proxyApiUrl" class="hc-badge" />
|
||||
</div>
|
||||
<component
|
||||
:is="selectionMode ? 'button' : 'div'"
|
||||
class="hc-badge-container"
|
||||
v-for="(badge, index) in badges"
|
||||
:key="index"
|
||||
:class="{ selectable: selectionMode && index > 0, selected: selectedIndex === index }"
|
||||
@click="handleBadgeClick(index)"
|
||||
>
|
||||
<img :title="badge.description" :src="badge.icon | proxyApiUrl" class="hc-badge" />
|
||||
</component>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Badges',
|
||||
props: {
|
||||
badges: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
selectionMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedIndex: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleBadgeClick(index) {
|
||||
if (!this.selectionMode || index === 0) {
|
||||
return
|
||||
}
|
||||
if (this.selectedIndex === index) {
|
||||
this.selectedIndex = null
|
||||
this.$emit('badge-selected', null)
|
||||
return
|
||||
}
|
||||
|
||||
this.selectedIndex = index
|
||||
this.$emit('badge-selected', index)
|
||||
},
|
||||
resetSelection() {
|
||||
this.selectedIndex = null
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use 'sass:math';
|
||||
.hc-badges {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
|
||||
transform: scale(var(--badges-scale, 1));
|
||||
|
||||
$badge-size-x: 30px;
|
||||
$badge-size-y: 26px;
|
||||
$main-badge-size-x: 60px;
|
||||
$main-badge-size-y: 52px;
|
||||
$gap-x: -6px;
|
||||
$gap-y: 1px;
|
||||
$slot-x: $badge-size-x + $gap-x;
|
||||
$slot-y: $badge-size-y + $gap-y;
|
||||
$offset-y: calc($badge-size-y / 2) - 2 * $gap-x;
|
||||
|
||||
width: $main-badge-size-x + 4 * $badge-size-x + 4 * $gap-x;
|
||||
height: $offset-y + 3 * $badge-size-y + 4 * $gap-y;
|
||||
margin: auto;
|
||||
|
||||
.hc-badge-container {
|
||||
display: inline-block;
|
||||
position: unset;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
position: absolute;
|
||||
width: $badge-size-x;
|
||||
height: $badge-size-y;
|
||||
|
||||
clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%);
|
||||
|
||||
&.selectable {
|
||||
cursor: pointer;
|
||||
|
||||
transition: transform 0.1s ease-in;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
filter: drop-shadow(0 0 0 $color-primary);
|
||||
img {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hc-badge {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
$size: 30px;
|
||||
$offset: $size * -0.2;
|
||||
|
||||
&.hc-badges-dual {
|
||||
padding-top: math.div($size, 2) - 2;
|
||||
.hc-badge-container:nth-child(1) {
|
||||
width: $main-badge-size-x;
|
||||
height: $main-badge-size-y;
|
||||
top: $offset-y + calc($gap-y / 2) - 1px;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.hc-badge-container {
|
||||
width: $size;
|
||||
height: 26px;
|
||||
margin-left: -1px;
|
||||
.hc-badge-container:nth-child(1)::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background-image: url('/img/badges/stars.svg');
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
&:nth-child(3n - 1) {
|
||||
margin-top: -$size - $offset - 3;
|
||||
margin-left: -$size * 0.33 - $offset - 2;
|
||||
}
|
||||
&:nth-child(3n + 0) {
|
||||
margin-top: $size + $offset + 3;
|
||||
margin-left: -$size;
|
||||
}
|
||||
&:nth-child(3n + 1) {
|
||||
margin-left: -6px;
|
||||
}
|
||||
&:first-child {
|
||||
margin-left: math.div(-$size, 3);
|
||||
}
|
||||
&:last-child {
|
||||
margin-right: math.div(-$size, 3);
|
||||
}
|
||||
.hc-badge-container:nth-child(2) {
|
||||
top: $offset-y + calc(-1 * $gap-y / 2) - 1px;
|
||||
left: $main-badge-size-x + $gap-x;
|
||||
}
|
||||
|
||||
.hc-badge-container:nth-child(3) {
|
||||
top: $offset-y + $slot-y + calc($gap-y / 2) - 1px;
|
||||
left: $main-badge-size-x + $gap-x;
|
||||
}
|
||||
|
||||
.hc-badge-container:nth-child(4) {
|
||||
top: $offset-y + calc(-1 * $badge-size-y / 2) - (2 * $gap-y) - 0.5px;
|
||||
left: $main-badge-size-x + $gap-x + $slot-x;
|
||||
}
|
||||
|
||||
.hc-badge-container:nth-child(5) {
|
||||
top: $offset-y + calc($badge-size-y / 2) - 0.5px;
|
||||
left: $main-badge-size-x + $gap-x + $slot-x;
|
||||
}
|
||||
|
||||
.hc-badge-container:nth-child(6) {
|
||||
top: $offset-y + (1.5 * $badge-size-y) + (2 * $gap-y) - 0.5px;
|
||||
left: $main-badge-size-x + $gap-x + $slot-x;
|
||||
}
|
||||
|
||||
.hc-badge-container:nth-child(7) {
|
||||
top: $offset-y + calc(-1 * $gap-y / 2) - 1px;
|
||||
left: $main-badge-size-x + $gap-x + (2 * $slot-x);
|
||||
}
|
||||
|
||||
.hc-badge-container:nth-child(8) {
|
||||
top: $offset-y + $slot-y + calc($gap-y / 2) - 1px;
|
||||
left: $main-badge-size-x + $gap-x + (2 * $slot-x);
|
||||
}
|
||||
|
||||
.hc-badge-container:nth-child(9) {
|
||||
top: $offset-y + $slot-y - calc($badge-size-y / 2) - $gap-y - 0.5px;
|
||||
left: $main-badge-size-x + $gap-x + (3 * $slot-x);
|
||||
}
|
||||
|
||||
.hc-badge-container:nth-child(10) {
|
||||
top: $offset-y + ($badge-size-y * 1.5) + (2 * $gap-y) - 0.5px;
|
||||
left: $main-badge-size-x + $gap-x + (3 * $slot-x);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
84
webapp/components/__snapshots__/BadgeSelection.spec.js.snap
Normal file
84
webapp/components/__snapshots__/BadgeSelection.spec.js.snap
Normal file
@ -0,0 +1,84 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Badges.vue with badges renders 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="badge-selection"
|
||||
>
|
||||
<button
|
||||
class="badge-selection-item"
|
||||
>
|
||||
<div
|
||||
class="badge-icon"
|
||||
>
|
||||
<img
|
||||
alt="1"
|
||||
src="/api/path/to/some/icon"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="badge-info"
|
||||
>
|
||||
<div
|
||||
class="badge-description"
|
||||
>
|
||||
Some description
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="badge-selection-item"
|
||||
>
|
||||
<div
|
||||
class="badge-icon"
|
||||
>
|
||||
<img
|
||||
alt="2"
|
||||
src="/api/path/to/another/icon"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="badge-info"
|
||||
>
|
||||
<div
|
||||
class="badge-description"
|
||||
>
|
||||
Another description
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="badge-selection-item"
|
||||
>
|
||||
<div
|
||||
class="badge-icon"
|
||||
>
|
||||
<img
|
||||
alt="3"
|
||||
src="/api/path/to/third/icon"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="badge-info"
|
||||
>
|
||||
<div
|
||||
class="badge-description"
|
||||
>
|
||||
Third description
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Badges.vue without badges renders 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="badge-selection"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
165
webapp/components/__snapshots__/Badges.spec.js.snap
Normal file
165
webapp/components/__snapshots__/Badges.spec.js.snap
Normal file
@ -0,0 +1,165 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Badges.vue with badges in presentation mode renders 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="hc-badges"
|
||||
scale="1.2"
|
||||
>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/path/to/some/icon"
|
||||
title="Some description"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/path/to/another/icon"
|
||||
title="Another description"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/path/to/third/icon"
|
||||
title="Third description"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Badges.vue with badges in selection mode clicking on second badge selects badge 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="hc-badges"
|
||||
scale="1.2"
|
||||
>
|
||||
<button
|
||||
class="hc-badge-container"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/path/to/some/icon"
|
||||
title="Some description"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="hc-badge-container selectable selected"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/path/to/another/icon"
|
||||
title="Another description"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="hc-badge-container selectable"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/path/to/third/icon"
|
||||
title="Third description"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Badges.vue with badges in selection mode clicking twice on second badge deselects badge 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="hc-badges"
|
||||
scale="1.2"
|
||||
>
|
||||
<button
|
||||
class="hc-badge-container"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/path/to/some/icon"
|
||||
title="Some description"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="hc-badge-container selectable"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/path/to/another/icon"
|
||||
title="Another description"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="hc-badge-container selectable"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/path/to/third/icon"
|
||||
title="Third description"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Badges.vue with badges in selection mode renders 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="hc-badges"
|
||||
scale="1.2"
|
||||
>
|
||||
<button
|
||||
class="hc-badge-container"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/path/to/some/icon"
|
||||
title="Some description"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="hc-badge-container selectable"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/path/to/another/icon"
|
||||
title="Another description"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="hc-badge-container selectable"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/path/to/third/icon"
|
||||
title="Third description"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Badges.vue without badges renders in presentation mode 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="hc-badges"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Badges.vue without badges renders in selection mode 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="hc-badges"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@ -35,6 +35,7 @@ const options = {
|
||||
COOKIE_EXPIRE_TIME: process.env.COOKIE_EXPIRE_TIME || 730, // Two years by default
|
||||
COOKIE_HTTPS_ONLY: process.env.COOKIE_HTTPS_ONLY || process.env.NODE_ENV === 'production', // ensure true in production if not set explicitly
|
||||
CATEGORIES_ACTIVE: process.env.CATEGORIES_ACTIVE === 'true' || false,
|
||||
BADGES_ENABLED: process.env.BADGES_ENABLED === 'true' || false,
|
||||
}
|
||||
|
||||
const CONFIG = {
|
||||
|
||||
@ -26,9 +26,15 @@ export const locationFragment = (lang) => gql`
|
||||
|
||||
export const badgesFragment = gql`
|
||||
fragment badges on User {
|
||||
badgeTrophies {
|
||||
badgeTrophiesSelected {
|
||||
id
|
||||
icon
|
||||
description
|
||||
}
|
||||
badgeVerification {
|
||||
id
|
||||
icon
|
||||
description
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -405,6 +405,22 @@ export const currentUserQuery = gql`
|
||||
query {
|
||||
currentUser {
|
||||
...user
|
||||
badgeTrophiesSelected {
|
||||
id
|
||||
icon
|
||||
description
|
||||
isDefault
|
||||
}
|
||||
badgeTrophiesUnused {
|
||||
id
|
||||
icon
|
||||
description
|
||||
}
|
||||
badgeVerification {
|
||||
id
|
||||
icon
|
||||
description
|
||||
}
|
||||
email
|
||||
role
|
||||
about
|
||||
@ -466,3 +482,43 @@ export const userDataQuery = (i18n) => {
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const setTrophyBadgeSelected = gql`
|
||||
mutation ($slot: Int!, $badgeId: ID) {
|
||||
setTrophyBadgeSelected(slot: $slot, badgeId: $badgeId) {
|
||||
badgeTrophiesCount
|
||||
badgeTrophiesSelected {
|
||||
id
|
||||
icon
|
||||
description
|
||||
isDefault
|
||||
}
|
||||
badgeTrophiesUnused {
|
||||
id
|
||||
icon
|
||||
description
|
||||
}
|
||||
badgeTrophiesUnusedCount
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const resetTrophyBadgesSelected = gql`
|
||||
mutation {
|
||||
resetTrophyBadgesSelected {
|
||||
badgeTrophiesCount
|
||||
badgeTrophiesSelected {
|
||||
id
|
||||
icon
|
||||
description
|
||||
isDefault
|
||||
}
|
||||
badgeTrophiesUnused {
|
||||
id
|
||||
icon
|
||||
description
|
||||
}
|
||||
badgeTrophiesUnusedCount
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -957,6 +957,16 @@
|
||||
"title": "Suchergebnisse"
|
||||
},
|
||||
"settings": {
|
||||
"badges": {
|
||||
"click-to-select": "Klicke auf einen freien Platz, um eine Badge hinzufügen.",
|
||||
"click-to-use": "Klicke auf eine Badge, um sie zu platzieren.",
|
||||
"description": "Hier hast du die Möglichkeit zu entscheiden, wie deine bereits erworbenen Badges in deinem Profil gezeigt werden sollen.",
|
||||
"name": "Badges",
|
||||
"no-badges-available": "Im Moment stehen dir keine Badges zur Verfügung, die du hinzufügen könntest.",
|
||||
"remove": "Badge entfernen",
|
||||
"success-update": "Deine Badges wurden erfolgreich gespeichert.",
|
||||
"verification": "Dies ist deine Verifikations-Badge und kann nicht geändert werden."
|
||||
},
|
||||
"blocked-users": {
|
||||
"block": "Nutzer blockieren",
|
||||
"columns": {
|
||||
|
||||
@ -957,6 +957,16 @@
|
||||
"title": "Search Results"
|
||||
},
|
||||
"settings": {
|
||||
"badges": {
|
||||
"click-to-select": "Click on an empty space to add a badge.",
|
||||
"click-to-use": "Click on a badge to use it in the selected slot.",
|
||||
"description": "Here you can choose how to display your earned badges on your profile.",
|
||||
"name": "Badges",
|
||||
"no-badges-available": "You currently don't have any badges available to add.",
|
||||
"remove": "Remove Badge",
|
||||
"success-update": "Your badges have been updated successfully.",
|
||||
"verification": "This is your verification badge and cannot be changed."
|
||||
},
|
||||
"blocked-users": {
|
||||
"block": "Block user",
|
||||
"columns": {
|
||||
|
||||
@ -957,6 +957,16 @@
|
||||
"title": null
|
||||
},
|
||||
"settings": {
|
||||
"badges": {
|
||||
"click-to-select": null,
|
||||
"click-to-use": null,
|
||||
"description": null,
|
||||
"name": null,
|
||||
"no-badges-available": null,
|
||||
"remove": null,
|
||||
"success-update": null,
|
||||
"verification": null
|
||||
},
|
||||
"blocked-users": {
|
||||
"block": "Bloquear usuario",
|
||||
"columns": {
|
||||
|
||||
@ -957,6 +957,16 @@
|
||||
"title": null
|
||||
},
|
||||
"settings": {
|
||||
"badges": {
|
||||
"click-to-select": null,
|
||||
"click-to-use": null,
|
||||
"description": null,
|
||||
"name": null,
|
||||
"no-badges-available": null,
|
||||
"remove": null,
|
||||
"success-update": null,
|
||||
"verification": null
|
||||
},
|
||||
"blocked-users": {
|
||||
"block": "Bloquer l'utilisateur",
|
||||
"columns": {
|
||||
|
||||
@ -957,6 +957,16 @@
|
||||
"title": null
|
||||
},
|
||||
"settings": {
|
||||
"badges": {
|
||||
"click-to-select": null,
|
||||
"click-to-use": null,
|
||||
"description": null,
|
||||
"name": null,
|
||||
"no-badges-available": null,
|
||||
"remove": null,
|
||||
"success-update": null,
|
||||
"verification": null
|
||||
},
|
||||
"blocked-users": {
|
||||
"block": null,
|
||||
"columns": {
|
||||
|
||||
@ -957,6 +957,16 @@
|
||||
"title": null
|
||||
},
|
||||
"settings": {
|
||||
"badges": {
|
||||
"click-to-select": null,
|
||||
"click-to-use": null,
|
||||
"description": null,
|
||||
"name": null,
|
||||
"no-badges-available": null,
|
||||
"remove": null,
|
||||
"success-update": null,
|
||||
"verification": null
|
||||
},
|
||||
"blocked-users": {
|
||||
"block": null,
|
||||
"columns": {
|
||||
|
||||
@ -957,6 +957,16 @@
|
||||
"title": null
|
||||
},
|
||||
"settings": {
|
||||
"badges": {
|
||||
"click-to-select": null,
|
||||
"click-to-use": null,
|
||||
"description": null,
|
||||
"name": null,
|
||||
"no-badges-available": null,
|
||||
"remove": null,
|
||||
"success-update": null,
|
||||
"verification": null
|
||||
},
|
||||
"blocked-users": {
|
||||
"block": null,
|
||||
"columns": {
|
||||
|
||||
@ -957,6 +957,16 @@
|
||||
"title": null
|
||||
},
|
||||
"settings": {
|
||||
"badges": {
|
||||
"click-to-select": null,
|
||||
"click-to-use": null,
|
||||
"description": null,
|
||||
"name": null,
|
||||
"no-badges-available": null,
|
||||
"remove": null,
|
||||
"success-update": null,
|
||||
"verification": null
|
||||
},
|
||||
"blocked-users": {
|
||||
"block": "Bloquear usuário",
|
||||
"columns": {
|
||||
|
||||
@ -957,6 +957,16 @@
|
||||
"title": null
|
||||
},
|
||||
"settings": {
|
||||
"badges": {
|
||||
"click-to-select": null,
|
||||
"click-to-use": null,
|
||||
"description": null,
|
||||
"name": null,
|
||||
"no-badges-available": null,
|
||||
"remove": null,
|
||||
"success-update": null,
|
||||
"verification": null
|
||||
},
|
||||
"blocked-users": {
|
||||
"block": "Блокировать",
|
||||
"columns": {
|
||||
|
||||
@ -79,6 +79,7 @@
|
||||
"@storybook/addon-actions": "^5.3.21",
|
||||
"@storybook/addon-notes": "^5.3.18",
|
||||
"@storybook/vue": "~7.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/vue": "5",
|
||||
"@vue/cli-shared-utils": "~4.3.1",
|
||||
"@vue/eslint-config-prettier": "~6.0.0",
|
||||
|
||||
427
webapp/pages/__snapshots__/settings.spec.js.snap
Normal file
427
webapp/pages/__snapshots__/settings.spec.js.snap
Normal file
@ -0,0 +1,427 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`settings.vue given badges are disabled renders 1`] = `
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
class="ds-space"
|
||||
style="margin-top: 16px; margin-bottom: 16px;"
|
||||
>
|
||||
<h1
|
||||
class="ds-heading ds-heading-h1"
|
||||
>
|
||||
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="ds-space"
|
||||
style="margin-top: 32px; margin-bottom: 32px;"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="ds-flex"
|
||||
style="margin-left: -8px; margin-right: -8px;"
|
||||
>
|
||||
<div
|
||||
class="menu-container"
|
||||
>
|
||||
<nav
|
||||
class="ds-menu"
|
||||
>
|
||||
<ul
|
||||
class="ds-menu-list"
|
||||
>
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
exact="true"
|
||||
href="/settings"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
exact="true"
|
||||
href="/settings/my-email-address"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
exact="true"
|
||||
href="/settings/security"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
exact="true"
|
||||
href="/settings/privacy"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
exact="true"
|
||||
href="/settings/my-social-media"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
exact="true"
|
||||
href="/settings/muted-users"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
exact="true"
|
||||
href="/settings/blocked-users"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
exact="true"
|
||||
href="/settings/embeds"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
exact="true"
|
||||
href="/settings/notifications"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
exact="true"
|
||||
href="/settings/data-download"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
exact="true"
|
||||
href="/settings/delete-account"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="settings-content"
|
||||
id="settings-content"
|
||||
>
|
||||
<transition-stub
|
||||
appear="true"
|
||||
name="slide-up"
|
||||
>
|
||||
<nuxt-child-stub />
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`settings.vue given badges are enabled renders 1`] = `
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
class="ds-space"
|
||||
style="margin-top: 16px; margin-bottom: 16px;"
|
||||
>
|
||||
<h1
|
||||
class="ds-heading ds-heading-h1"
|
||||
>
|
||||
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="ds-space"
|
||||
style="margin-top: 32px; margin-bottom: 32px;"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="ds-flex"
|
||||
style="margin-left: -8px; margin-right: -8px;"
|
||||
>
|
||||
<div
|
||||
class="menu-container"
|
||||
>
|
||||
<nav
|
||||
class="ds-menu"
|
||||
>
|
||||
<ul
|
||||
class="ds-menu-list"
|
||||
>
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
exact="true"
|
||||
href="/settings"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
exact="true"
|
||||
href="/settings/my-email-address"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
exact="true"
|
||||
href="/settings/badges"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
exact="true"
|
||||
href="/settings/security"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
exact="true"
|
||||
href="/settings/privacy"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
exact="true"
|
||||
href="/settings/my-social-media"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
exact="true"
|
||||
href="/settings/muted-users"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
exact="true"
|
||||
href="/settings/blocked-users"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
exact="true"
|
||||
href="/settings/embeds"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
exact="true"
|
||||
href="/settings/notifications"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
exact="true"
|
||||
href="/settings/data-download"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
exact="true"
|
||||
href="/settings/delete-account"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="settings-content"
|
||||
id="settings-content"
|
||||
>
|
||||
<transition-stub
|
||||
appear="true"
|
||||
name="slide-up"
|
||||
>
|
||||
<nuxt-child-stub />
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
847
webapp/pages/admin/users/__snapshots__/index.spec.js.snap
Normal file
847
webapp/pages/admin/users/__snapshots__/index.spec.js.snap
Normal file
@ -0,0 +1,847 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Users given badges are disabled renders 1`] = `
|
||||
<div
|
||||
class="admin-users"
|
||||
>
|
||||
<article
|
||||
class="base-card"
|
||||
>
|
||||
<h2
|
||||
class="title"
|
||||
>
|
||||
admin.users.name
|
||||
</h2>
|
||||
|
||||
<form
|
||||
autocomplete="off"
|
||||
class="ds-form"
|
||||
novalidate="novalidate"
|
||||
>
|
||||
<div
|
||||
class="ds-flex"
|
||||
style="margin-left: -8px; margin-right: -8px;"
|
||||
>
|
||||
<div
|
||||
class="ds-flex-item"
|
||||
style="flex-basis: 90%; width: 90%; padding-left: 8px; padding-right: 8px; margin-bottom: 16px;"
|
||||
>
|
||||
<div
|
||||
class="ds-form-item ds-input-size-base"
|
||||
>
|
||||
<label
|
||||
class="ds-input-label"
|
||||
style="display: none;"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</label>
|
||||
<div
|
||||
class="ds-input-wrap"
|
||||
>
|
||||
<div
|
||||
class="ds-input-icon"
|
||||
>
|
||||
<span
|
||||
aria-label="icon"
|
||||
class="ds-icon"
|
||||
>
|
||||
<svg
|
||||
class="ds-icon-svg"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 3c5.511 0 10 4.489 10 10s-4.489 10-10 10a9.923 9.923 0 01-6.313-2.25l-7.969 7.969-1.438-1.438 7.969-7.969a9.919 9.919 0 01-2.25-6.313c0-5.511 4.489-10 10-10zm0 2c-4.43 0-8 3.57-8 8s3.57 8 8 8 8-3.57 8-8-3.57-8-8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
class="ds-input ds-input-has-icon"
|
||||
name="query"
|
||||
placeholder="admin.users.form.placeholder"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
/>
|
||||
<!---->
|
||||
</div>
|
||||
<transition-stub
|
||||
name="ds-input-error"
|
||||
>
|
||||
<div
|
||||
class="ds-input-error"
|
||||
style="display: none;"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="ds-flex-item"
|
||||
style="flex-basis: 30px; width: 30px; padding-left: 8px; padding-right: 8px; margin-bottom: 16px;"
|
||||
>
|
||||
<button
|
||||
class="base-button --icon-only --circle --filled"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!---->
|
||||
</article>
|
||||
|
||||
<article
|
||||
class="base-card"
|
||||
>
|
||||
<div
|
||||
class="ds-table-wrap"
|
||||
>
|
||||
<table
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
class="ds-table ds-table-condensed ds-table-bordered"
|
||||
>
|
||||
<colgroup>
|
||||
<col
|
||||
width=""
|
||||
/>
|
||||
<col
|
||||
width=""
|
||||
/>
|
||||
<col
|
||||
width=""
|
||||
/>
|
||||
<col
|
||||
width=""
|
||||
/>
|
||||
<col
|
||||
width=""
|
||||
/>
|
||||
<col
|
||||
width=""
|
||||
/>
|
||||
<col
|
||||
width=""
|
||||
/>
|
||||
<col
|
||||
width=""
|
||||
/>
|
||||
<col
|
||||
width=""
|
||||
/>
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
class="ds-table-head-col"
|
||||
>
|
||||
|
||||
admin.users.table.columns.number
|
||||
|
||||
</th>
|
||||
<th
|
||||
class="ds-table-head-col"
|
||||
>
|
||||
|
||||
admin.users.table.columns.name
|
||||
|
||||
</th>
|
||||
<th
|
||||
class="ds-table-head-col"
|
||||
>
|
||||
|
||||
admin.users.table.columns.email
|
||||
|
||||
</th>
|
||||
<th
|
||||
class="ds-table-head-col"
|
||||
>
|
||||
|
||||
admin.users.table.columns.slug
|
||||
|
||||
</th>
|
||||
<th
|
||||
class="ds-table-head-col"
|
||||
>
|
||||
|
||||
admin.users.table.columns.createdAt
|
||||
|
||||
</th>
|
||||
<th
|
||||
class="ds-table-head-col ds-table-head-col-right"
|
||||
>
|
||||
|
||||
🖉
|
||||
|
||||
</th>
|
||||
<th
|
||||
class="ds-table-head-col ds-table-head-col-right"
|
||||
>
|
||||
|
||||
🗨
|
||||
|
||||
</th>
|
||||
<th
|
||||
class="ds-table-head-col ds-table-head-col-right"
|
||||
>
|
||||
|
||||
❤
|
||||
|
||||
</th>
|
||||
<th
|
||||
class="ds-table-head-col ds-table-head-col-right"
|
||||
>
|
||||
|
||||
admin.users.table.columns.role
|
||||
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
class="ds-table-col"
|
||||
>
|
||||
NaN.
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col"
|
||||
>
|
||||
<nuxt-link-stub
|
||||
to="[object Object]"
|
||||
>
|
||||
<b>
|
||||
User
|
||||
</b>
|
||||
</nuxt-link-stub>
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col"
|
||||
>
|
||||
<a
|
||||
href="mailto:user@example.org"
|
||||
>
|
||||
<b>
|
||||
user@example.org
|
||||
</b>
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col"
|
||||
>
|
||||
<nuxt-link-stub
|
||||
to="[object Object]"
|
||||
>
|
||||
<b>
|
||||
user
|
||||
</b>
|
||||
</nuxt-link-stub>
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col ds-table-col-right"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col ds-table-col-right"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col ds-table-col-right"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col ds-table-col-right"
|
||||
>
|
||||
<select />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
class="ds-table-col"
|
||||
>
|
||||
NaN.
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col"
|
||||
>
|
||||
<nuxt-link-stub
|
||||
to="[object Object]"
|
||||
>
|
||||
<b>
|
||||
User
|
||||
</b>
|
||||
</nuxt-link-stub>
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col"
|
||||
>
|
||||
<a
|
||||
href="mailto:user2@example.org"
|
||||
>
|
||||
<b>
|
||||
user2@example.org
|
||||
</b>
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col"
|
||||
>
|
||||
<nuxt-link-stub
|
||||
to="[object Object]"
|
||||
>
|
||||
<b>
|
||||
user
|
||||
</b>
|
||||
</nuxt-link-stub>
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col ds-table-col-right"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col ds-table-col-right"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col ds-table-col-right"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col ds-table-col-right"
|
||||
>
|
||||
<select />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="pagination-buttons"
|
||||
>
|
||||
<button
|
||||
class="previous-button base-button --icon-only --circle"
|
||||
data-test="previous-button"
|
||||
disabled="disabled"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
|
||||
<!---->
|
||||
|
||||
<button
|
||||
class="next-button base-button --icon-only --circle"
|
||||
data-test="next-button"
|
||||
disabled="disabled"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!---->
|
||||
</article>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Users given badges are enabled renders 1`] = `
|
||||
<div
|
||||
class="admin-users"
|
||||
>
|
||||
<article
|
||||
class="base-card"
|
||||
>
|
||||
<h2
|
||||
class="title"
|
||||
>
|
||||
admin.users.name
|
||||
</h2>
|
||||
|
||||
<form
|
||||
autocomplete="off"
|
||||
class="ds-form"
|
||||
novalidate="novalidate"
|
||||
>
|
||||
<div
|
||||
class="ds-flex"
|
||||
style="margin-left: -8px; margin-right: -8px;"
|
||||
>
|
||||
<div
|
||||
class="ds-flex-item"
|
||||
style="flex-basis: 90%; width: 90%; padding-left: 8px; padding-right: 8px; margin-bottom: 16px;"
|
||||
>
|
||||
<div
|
||||
class="ds-form-item ds-input-size-base"
|
||||
>
|
||||
<label
|
||||
class="ds-input-label"
|
||||
style="display: none;"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</label>
|
||||
<div
|
||||
class="ds-input-wrap"
|
||||
>
|
||||
<div
|
||||
class="ds-input-icon"
|
||||
>
|
||||
<span
|
||||
aria-label="icon"
|
||||
class="ds-icon"
|
||||
>
|
||||
<svg
|
||||
class="ds-icon-svg"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 3c5.511 0 10 4.489 10 10s-4.489 10-10 10a9.923 9.923 0 01-6.313-2.25l-7.969 7.969-1.438-1.438 7.969-7.969a9.919 9.919 0 01-2.25-6.313c0-5.511 4.489-10 10-10zm0 2c-4.43 0-8 3.57-8 8s3.57 8 8 8 8-3.57 8-8-3.57-8-8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
class="ds-input ds-input-has-icon"
|
||||
name="query"
|
||||
placeholder="admin.users.form.placeholder"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
/>
|
||||
<!---->
|
||||
</div>
|
||||
<transition-stub
|
||||
name="ds-input-error"
|
||||
>
|
||||
<div
|
||||
class="ds-input-error"
|
||||
style="display: none;"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</transition-stub>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="ds-flex-item"
|
||||
style="flex-basis: 30px; width: 30px; padding-left: 8px; padding-right: 8px; margin-bottom: 16px;"
|
||||
>
|
||||
<button
|
||||
class="base-button --icon-only --circle --filled"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!---->
|
||||
</article>
|
||||
|
||||
<article
|
||||
class="base-card"
|
||||
>
|
||||
<div
|
||||
class="ds-table-wrap"
|
||||
>
|
||||
<table
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
class="ds-table ds-table-condensed ds-table-bordered"
|
||||
>
|
||||
<colgroup>
|
||||
<col
|
||||
width=""
|
||||
/>
|
||||
<col
|
||||
width=""
|
||||
/>
|
||||
<col
|
||||
width=""
|
||||
/>
|
||||
<col
|
||||
width=""
|
||||
/>
|
||||
<col
|
||||
width=""
|
||||
/>
|
||||
<col
|
||||
width=""
|
||||
/>
|
||||
<col
|
||||
width=""
|
||||
/>
|
||||
<col
|
||||
width=""
|
||||
/>
|
||||
<col
|
||||
width=""
|
||||
/>
|
||||
<col
|
||||
width=""
|
||||
/>
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
class="ds-table-head-col"
|
||||
>
|
||||
|
||||
admin.users.table.columns.number
|
||||
|
||||
</th>
|
||||
<th
|
||||
class="ds-table-head-col"
|
||||
>
|
||||
|
||||
admin.users.table.columns.name
|
||||
|
||||
</th>
|
||||
<th
|
||||
class="ds-table-head-col"
|
||||
>
|
||||
|
||||
admin.users.table.columns.email
|
||||
|
||||
</th>
|
||||
<th
|
||||
class="ds-table-head-col"
|
||||
>
|
||||
|
||||
admin.users.table.columns.slug
|
||||
|
||||
</th>
|
||||
<th
|
||||
class="ds-table-head-col"
|
||||
>
|
||||
|
||||
admin.users.table.columns.createdAt
|
||||
|
||||
</th>
|
||||
<th
|
||||
class="ds-table-head-col ds-table-head-col-right"
|
||||
>
|
||||
|
||||
🖉
|
||||
|
||||
</th>
|
||||
<th
|
||||
class="ds-table-head-col ds-table-head-col-right"
|
||||
>
|
||||
|
||||
🗨
|
||||
|
||||
</th>
|
||||
<th
|
||||
class="ds-table-head-col ds-table-head-col-right"
|
||||
>
|
||||
|
||||
❤
|
||||
|
||||
</th>
|
||||
<th
|
||||
class="ds-table-head-col ds-table-head-col-right"
|
||||
>
|
||||
|
||||
admin.users.table.columns.role
|
||||
|
||||
</th>
|
||||
<th
|
||||
class="ds-table-head-col ds-table-head-col-right"
|
||||
>
|
||||
|
||||
admin.users.table.columns.badges
|
||||
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
class="ds-table-col"
|
||||
>
|
||||
NaN.
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col"
|
||||
>
|
||||
<nuxt-link-stub
|
||||
to="[object Object]"
|
||||
>
|
||||
<b>
|
||||
User
|
||||
</b>
|
||||
</nuxt-link-stub>
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col"
|
||||
>
|
||||
<a
|
||||
href="mailto:user@example.org"
|
||||
>
|
||||
<b>
|
||||
user@example.org
|
||||
</b>
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col"
|
||||
>
|
||||
<nuxt-link-stub
|
||||
to="[object Object]"
|
||||
>
|
||||
<b>
|
||||
user
|
||||
</b>
|
||||
</nuxt-link-stub>
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col ds-table-col-right"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col ds-table-col-right"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col ds-table-col-right"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col ds-table-col-right"
|
||||
>
|
||||
<select />
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col ds-table-col-right"
|
||||
>
|
||||
<nuxt-link-stub
|
||||
to="[object Object]"
|
||||
>
|
||||
|
||||
admin.users.table.edit
|
||||
|
||||
</nuxt-link-stub>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
class="ds-table-col"
|
||||
>
|
||||
NaN.
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col"
|
||||
>
|
||||
<nuxt-link-stub
|
||||
to="[object Object]"
|
||||
>
|
||||
<b>
|
||||
User
|
||||
</b>
|
||||
</nuxt-link-stub>
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col"
|
||||
>
|
||||
<a
|
||||
href="mailto:user2@example.org"
|
||||
>
|
||||
<b>
|
||||
user2@example.org
|
||||
</b>
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col"
|
||||
>
|
||||
<nuxt-link-stub
|
||||
to="[object Object]"
|
||||
>
|
||||
<b>
|
||||
user
|
||||
</b>
|
||||
</nuxt-link-stub>
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col ds-table-col-right"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col ds-table-col-right"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col ds-table-col-right"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col ds-table-col-right"
|
||||
>
|
||||
<select />
|
||||
</td>
|
||||
<td
|
||||
class="ds-table-col ds-table-col-right"
|
||||
>
|
||||
<nuxt-link-stub
|
||||
to="[object Object]"
|
||||
>
|
||||
|
||||
admin.users.table.edit
|
||||
|
||||
</nuxt-link-stub>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="pagination-buttons"
|
||||
>
|
||||
<button
|
||||
class="previous-button base-button --icon-only --circle"
|
||||
data-test="previous-button"
|
||||
disabled="disabled"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
|
||||
<!---->
|
||||
|
||||
<button
|
||||
class="next-button base-button --icon-only --circle"
|
||||
data-test="next-button"
|
||||
disabled="disabled"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!---->
|
||||
</article>
|
||||
</div>
|
||||
`;
|
||||
@ -10,11 +10,9 @@ const stubs = {
|
||||
|
||||
describe('Users', () => {
|
||||
let wrapper
|
||||
let Wrapper
|
||||
let getters
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn(),
|
||||
$t: jest.fn((t) => t),
|
||||
$apollo: {
|
||||
loading: false,
|
||||
mutate: jest
|
||||
@ -38,116 +36,154 @@ describe('Users', () => {
|
||||
},
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
getters = {
|
||||
'auth/isAdmin': () => true,
|
||||
'auth/user': () => {
|
||||
return { id: 'admin' }
|
||||
},
|
||||
}
|
||||
const getters = {
|
||||
'auth/isAdmin': () => true,
|
||||
'auth/user': () => {
|
||||
return { id: 'admin' }
|
||||
},
|
||||
}
|
||||
|
||||
Wrapper = () => {
|
||||
const store = new Vuex.Store({ getters })
|
||||
return mount(Users, {
|
||||
mocks,
|
||||
localVue,
|
||||
store,
|
||||
stubs,
|
||||
})
|
||||
}
|
||||
const Wrapper = () => {
|
||||
const store = new Vuex.Store({ getters })
|
||||
return mount(Users, {
|
||||
mocks,
|
||||
localVue,
|
||||
store,
|
||||
stubs,
|
||||
data: () => ({
|
||||
User: [
|
||||
{
|
||||
id: 'user',
|
||||
email: 'user@example.org',
|
||||
name: 'User',
|
||||
role: 'moderator',
|
||||
slug: 'user',
|
||||
},
|
||||
{
|
||||
id: 'user2',
|
||||
email: 'user2@example.org',
|
||||
name: 'User',
|
||||
role: 'moderator',
|
||||
slug: 'user',
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
describe('given badges are enabled', () => {
|
||||
beforeEach(() => {
|
||||
mocks.$env = {
|
||||
BADGES_ENABLED: true,
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('renders', () => {
|
||||
expect(wrapper.element).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe('given badges are disabled', () => {
|
||||
beforeEach(() => {
|
||||
mocks.$env = {
|
||||
BADGES_ENABLED: false,
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.element.tagName).toBe('DIV')
|
||||
})
|
||||
|
||||
describe('search', () => {
|
||||
let searchAction
|
||||
beforeEach(() => {
|
||||
searchAction = (wrapper, { query }) => {
|
||||
wrapper.find('input').setValue(query)
|
||||
wrapper.find('form').trigger('submit')
|
||||
return wrapper
|
||||
}
|
||||
it('renders', () => {
|
||||
expect(wrapper.element).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe('search', () => {
|
||||
let searchAction
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
searchAction = (wrapper, { query }) => {
|
||||
wrapper.find('input').setValue(query)
|
||||
wrapper.find('form').trigger('submit')
|
||||
return wrapper
|
||||
}
|
||||
})
|
||||
|
||||
describe('query looks like an email address', () => {
|
||||
it('searches users for exact email address', async () => {
|
||||
const wrapper = await searchAction(Wrapper(), { query: 'email@example.org' })
|
||||
expect(wrapper.vm.email).toEqual('email@example.org')
|
||||
expect(wrapper.vm.filter).toBe(null)
|
||||
})
|
||||
|
||||
describe('query looks like an email address', () => {
|
||||
it('searches users for exact email address', async () => {
|
||||
const wrapper = await searchAction(Wrapper(), { query: 'email@example.org' })
|
||||
expect(wrapper.vm.email).toEqual('email@example.org')
|
||||
expect(wrapper.vm.filter).toBe(null)
|
||||
})
|
||||
|
||||
it('email address is case-insensitive', async () => {
|
||||
const wrapper = await searchAction(Wrapper(), { query: 'eMaiL@example.org' })
|
||||
expect(wrapper.vm.email).toEqual('email@example.org')
|
||||
expect(wrapper.vm.filter).toBe(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('query is just text', () => {
|
||||
it('tries to find matching users by `name`, `slug` or `about`', async () => {
|
||||
const wrapper = await searchAction(await Wrapper(), { query: 'Find me' })
|
||||
const expected = {
|
||||
OR: [
|
||||
{ name_contains: 'Find me' },
|
||||
{ slug_contains: 'Find me' },
|
||||
{ about_contains: 'Find me' },
|
||||
],
|
||||
}
|
||||
expect(wrapper.vm.email).toBe(null)
|
||||
expect(wrapper.vm.filter).toEqual(expected)
|
||||
})
|
||||
it('email address is case-insensitive', async () => {
|
||||
const wrapper = await searchAction(Wrapper(), { query: 'eMaiL@example.org' })
|
||||
expect(wrapper.vm.email).toEqual('email@example.org')
|
||||
expect(wrapper.vm.filter).toBe(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('change roles', () => {
|
||||
beforeAll(() => {
|
||||
wrapper = Wrapper()
|
||||
wrapper.setData({
|
||||
User: [
|
||||
{
|
||||
id: 'admin',
|
||||
email: 'admin@example.org',
|
||||
name: 'Admin',
|
||||
role: 'admin',
|
||||
slug: 'admin',
|
||||
},
|
||||
{
|
||||
id: 'user',
|
||||
email: 'user@example.org',
|
||||
name: 'User',
|
||||
role: 'user',
|
||||
slug: 'user',
|
||||
},
|
||||
describe('query is just text', () => {
|
||||
it('tries to find matching users by `name`, `slug` or `about`', async () => {
|
||||
const wrapper = await searchAction(await Wrapper(), { query: 'Find me' })
|
||||
const expected = {
|
||||
OR: [
|
||||
{ name_contains: 'Find me' },
|
||||
{ slug_contains: 'Find me' },
|
||||
{ about_contains: 'Find me' },
|
||||
],
|
||||
userRoles: ['user', 'moderator', 'admin'],
|
||||
})
|
||||
})
|
||||
|
||||
it('cannot change own role', () => {
|
||||
const adminRow = wrapper.findAll('tr').at(1)
|
||||
expect(adminRow.find('select').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('changes the role of another user', () => {
|
||||
const userRow = wrapper.findAll('tr').at(2)
|
||||
userRow.findAll('option').at(1).setSelected()
|
||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
id: 'user',
|
||||
role: 'moderator',
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('toasts a success message after role has changed', () => {
|
||||
const userRow = wrapper.findAll('tr').at(2)
|
||||
userRow.findAll('option').at(1).setSelected()
|
||||
expect(mocks.$toast.success).toHaveBeenCalled()
|
||||
}
|
||||
expect(wrapper.vm.email).toBe(null)
|
||||
expect(wrapper.vm.filter).toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('change roles', () => {
|
||||
beforeAll(() => {
|
||||
wrapper = Wrapper()
|
||||
wrapper.setData({
|
||||
User: [
|
||||
{
|
||||
id: 'admin',
|
||||
email: 'admin@example.org',
|
||||
name: 'Admin',
|
||||
role: 'admin',
|
||||
slug: 'admin',
|
||||
},
|
||||
{
|
||||
id: 'user',
|
||||
email: 'user@example.org',
|
||||
name: 'User',
|
||||
role: 'user',
|
||||
slug: 'user',
|
||||
},
|
||||
],
|
||||
userRoles: ['user', 'moderator', 'admin'],
|
||||
})
|
||||
})
|
||||
|
||||
it('cannot change own role', () => {
|
||||
const adminRow = wrapper.findAll('tr').at(1)
|
||||
expect(adminRow.find('select').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('changes the role of another user', () => {
|
||||
const userRow = wrapper.findAll('tr').at(2)
|
||||
userRow.findAll('option').at(1).setSelected()
|
||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
id: 'user',
|
||||
role: 'moderator',
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('toasts a success message after role has changed', () => {
|
||||
const userRow = wrapper.findAll('tr').at(2)
|
||||
userRow.findAll('option').at(1).setSelected()
|
||||
expect(mocks.$toast.success).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -120,7 +120,7 @@ export default {
|
||||
currentUser: 'auth/user',
|
||||
}),
|
||||
fields() {
|
||||
return {
|
||||
const fields = {
|
||||
index: this.$t('admin.users.table.columns.number'),
|
||||
name: this.$t('admin.users.table.columns.name'),
|
||||
email: this.$t('admin.users.table.columns.email'),
|
||||
@ -142,11 +142,16 @@ export default {
|
||||
label: this.$t('admin.users.table.columns.role'),
|
||||
align: 'right',
|
||||
},
|
||||
badges: {
|
||||
}
|
||||
|
||||
if (this.$env.BADGES_ENABLED) {
|
||||
fields.badges = {
|
||||
label: this.$t('admin.users.table.columns.badges'),
|
||||
align: 'right',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return fields
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
|
||||
2442
webapp/pages/profile/_id/__snapshots__/_slug.spec.js.snap
Normal file
2442
webapp/pages/profile/_id/__snapshots__/_slug.spec.js.snap
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,22 +1,25 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { render } from '@testing-library/vue'
|
||||
import ProfileSlug from './_slug.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
localVue.filter('date', (d) => d)
|
||||
|
||||
// Mock Math.random, used in Dropdown
|
||||
Object.assign(Math, {
|
||||
random: () => 0,
|
||||
})
|
||||
|
||||
const stubs = {
|
||||
'client-only': true,
|
||||
'v-popover': true,
|
||||
'nuxt-link': true,
|
||||
'infinite-loading': true,
|
||||
'follow-list': true,
|
||||
'router-link': true,
|
||||
}
|
||||
|
||||
describe('ProfileSlug', () => {
|
||||
let wrapper
|
||||
let Wrapper
|
||||
let mocks
|
||||
|
||||
beforeEach(() => {
|
||||
@ -25,7 +28,7 @@ describe('ProfileSlug', () => {
|
||||
id: 'p23',
|
||||
name: 'It is a post',
|
||||
},
|
||||
$t: jest.fn(),
|
||||
$t: jest.fn((t) => t),
|
||||
// If you're mocking router, then don't use VueRouter with localVue: https://vue-test-utils.vuejs.org/guides/using-with-vue-router.html
|
||||
$route: {
|
||||
params: {
|
||||
@ -49,49 +52,144 @@ describe('ProfileSlug', () => {
|
||||
}
|
||||
})
|
||||
|
||||
describe('mount', () => {
|
||||
Wrapper = () => {
|
||||
return mount(ProfileSlug, {
|
||||
mocks,
|
||||
localVue,
|
||||
stubs,
|
||||
})
|
||||
}
|
||||
const Wrapper = (badgesEnabled, data) => {
|
||||
return render(ProfileSlug, {
|
||||
localVue,
|
||||
stubs,
|
||||
data: () => data,
|
||||
mocks: {
|
||||
...mocks,
|
||||
$env: {
|
||||
BADGES_ENABLED: badgesEnabled,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('given an authenticated user', () => {
|
||||
beforeEach(() => {
|
||||
mocks.$filters = {
|
||||
removeLinks: (c) => c,
|
||||
truncate: (a) => a,
|
||||
}
|
||||
mocks.$store = {
|
||||
getters: {
|
||||
'auth/isModerator': () => false,
|
||||
'auth/user': {
|
||||
id: 'u23',
|
||||
},
|
||||
describe('given an authenticated user', () => {
|
||||
beforeEach(() => {
|
||||
mocks.$filters = {
|
||||
removeLinks: (c) => c,
|
||||
truncate: (a) => a,
|
||||
}
|
||||
mocks.$store = {
|
||||
getters: {
|
||||
'auth/isModerator': () => false,
|
||||
'auth/user': {
|
||||
id: 'u23',
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe('given a user for the profile', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
wrapper.setData({
|
||||
User: [
|
||||
describe('given another profile user', () => {
|
||||
const user = {
|
||||
User: [
|
||||
{
|
||||
id: 'u3',
|
||||
name: 'Bob the builder',
|
||||
contributionsCount: 6,
|
||||
shoutedCount: 7,
|
||||
commentedCount: 8,
|
||||
badgeVerification: {
|
||||
id: 'bv1',
|
||||
icon: '/path/to/icon-bv1',
|
||||
description: 'verified',
|
||||
isDefault: false,
|
||||
},
|
||||
badgeTrophiesSelected: [
|
||||
{
|
||||
id: 'u3',
|
||||
name: 'Bob the builder',
|
||||
contributionsCount: 6,
|
||||
shoutedCount: 7,
|
||||
commentedCount: 8,
|
||||
id: 'bt1',
|
||||
icon: '/path/to/icon-bt1',
|
||||
description: 'a trophy',
|
||||
isDefault: false,
|
||||
},
|
||||
{
|
||||
id: 'bt2',
|
||||
icon: '/path/to/icon-bt2',
|
||||
description: 'no trophy',
|
||||
isDefault: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
describe('and badges are enabled', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper(true, user)
|
||||
})
|
||||
|
||||
it('displays name of the user', () => {
|
||||
expect(wrapper.text()).toContain('Bob the builder')
|
||||
it('renders', () => {
|
||||
expect(wrapper.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe('and badges are disabled', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper(false, user)
|
||||
})
|
||||
|
||||
it('renders', () => {
|
||||
expect(wrapper.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('given the logged in user as profile user', () => {
|
||||
beforeEach(() => {
|
||||
mocks.$route.params.id = 'u23'
|
||||
})
|
||||
|
||||
const user = {
|
||||
User: [
|
||||
{
|
||||
id: 'u23',
|
||||
name: 'Bob the builder',
|
||||
contributionsCount: 6,
|
||||
shoutedCount: 7,
|
||||
commentedCount: 8,
|
||||
badgeVerification: {
|
||||
id: 'bv1',
|
||||
icon: '/path/to/icon-bv1',
|
||||
description: 'verified',
|
||||
isDefault: false,
|
||||
},
|
||||
badgeTrophiesSelected: [
|
||||
{
|
||||
id: 'bt1',
|
||||
icon: '/path/to/icon-bt1',
|
||||
description: 'a trophy',
|
||||
isDefault: false,
|
||||
},
|
||||
{
|
||||
id: 'bt2',
|
||||
icon: '/path/to/icon-bt2',
|
||||
description: 'no trophy',
|
||||
isDefault: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
describe('and badges are enabled', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper(true, user)
|
||||
})
|
||||
|
||||
it('renders', () => {
|
||||
expect(wrapper.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe('and badges are disabled', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper(false, user)
|
||||
})
|
||||
|
||||
it('renders', () => {
|
||||
expect(wrapper.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -42,8 +42,11 @@
|
||||
{{ $t('profile.memberSince') }} {{ user.createdAt | date('MMMM yyyy') }}
|
||||
</ds-text>
|
||||
</ds-space>
|
||||
<ds-space v-if="user.badgeTrophies && user.badgeTrophies.length" margin="x-small">
|
||||
<hc-badges :badges="user.badgeTrophies" />
|
||||
<ds-space v-if="userBadges && userBadges.length" margin="x-small">
|
||||
<a v-if="myProfile" href="/settings/badges" class="badge-edit-link">
|
||||
<hc-badges :badges="userBadges" />
|
||||
</a>
|
||||
<hc-badges v-if="!myProfile" :badges="userBadges" />
|
||||
</ds-space>
|
||||
<ds-flex>
|
||||
<ds-flex-item>
|
||||
@ -266,6 +269,10 @@ export default {
|
||||
user() {
|
||||
return this.User ? this.User[0] : {}
|
||||
},
|
||||
userBadges() {
|
||||
if (!this.$env.BADGES_ENABLED) return null
|
||||
return [this.user.badgeVerification, ...(this.user.badgeTrophiesSelected || [])]
|
||||
},
|
||||
userName() {
|
||||
const { name } = this.user || {}
|
||||
return name || this.$t('profile.userAnonym')
|
||||
@ -456,6 +463,12 @@ export default {
|
||||
margin: auto;
|
||||
margin-top: -60px;
|
||||
}
|
||||
.badge-edit-link {
|
||||
transition: all 0.2s ease-out;
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
.page-name-profile-id-slug {
|
||||
.ds-flex-item:first-child .content-menu {
|
||||
position: absolute;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { render } from '@testing-library/vue'
|
||||
import settings from './settings.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
@ -17,21 +17,37 @@ describe('settings.vue', () => {
|
||||
}
|
||||
})
|
||||
|
||||
describe('mount', () => {
|
||||
const Wrapper = () => {
|
||||
return mount(settings, {
|
||||
mocks,
|
||||
localVue,
|
||||
stubs,
|
||||
})
|
||||
}
|
||||
const Wrapper = () => {
|
||||
return render(settings, {
|
||||
mocks,
|
||||
localVue,
|
||||
stubs,
|
||||
})
|
||||
}
|
||||
|
||||
describe('given badges are enabled', () => {
|
||||
beforeEach(() => {
|
||||
mocks.$env = {
|
||||
BADGES_ENABLED: true,
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('renders', () => {
|
||||
expect(wrapper.element.tagName).toBe('DIV')
|
||||
expect(wrapper.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe('given badges are disabled', () => {
|
||||
beforeEach(() => {
|
||||
mocks.$env = {
|
||||
BADGES_ENABLED: false,
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('renders', () => {
|
||||
expect(wrapper.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
export default {
|
||||
computed: {
|
||||
routes() {
|
||||
return [
|
||||
const routes = [
|
||||
{
|
||||
name: this.$t('settings.data.name'),
|
||||
path: `/settings`,
|
||||
@ -83,6 +83,15 @@ export default {
|
||||
},
|
||||
} */
|
||||
]
|
||||
|
||||
if (this.$env.BADGES_ENABLED) {
|
||||
routes.splice(2, 0, {
|
||||
name: this.$t('settings.badges.name'),
|
||||
path: `/settings/badges`,
|
||||
})
|
||||
}
|
||||
|
||||
return routes
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
429
webapp/pages/settings/__snapshots__/badges.spec.js.snap
Normal file
429
webapp/pages/settings/__snapshots__/badges.spec.js.snap
Normal file
@ -0,0 +1,429 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`badge settings with badges more badges available selecting an empty slot shows list with available badges 1`] = `
|
||||
<div>
|
||||
<article
|
||||
class="base-card"
|
||||
>
|
||||
<h2
|
||||
class="title"
|
||||
>
|
||||
settings.badges.name
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
settings.badges.description
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="ds-space ds-space-centered"
|
||||
style="margin-top: 24px; margin-bottom: 16px;"
|
||||
>
|
||||
<div
|
||||
class="presenterContainer"
|
||||
>
|
||||
<div
|
||||
class="hc-badges"
|
||||
>
|
||||
<button
|
||||
class="hc-badge-container"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/verification/icon"
|
||||
title="Verification description"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="hc-badge-container selectable"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/path/to/some/icon"
|
||||
title="Some description"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="hc-badge-container selectable selected"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/path/to/empty/icon"
|
||||
title="Empty"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="hc-badge-container selectable"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/path/to/third/icon"
|
||||
title="Third description"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!---->
|
||||
|
||||
<div>
|
||||
<strong>
|
||||
|
||||
settings.badges.click-to-use
|
||||
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
<!---->
|
||||
|
||||
<div
|
||||
class="selection-info"
|
||||
>
|
||||
<div
|
||||
class="badge-selection"
|
||||
>
|
||||
<button
|
||||
class="badge-selection-item"
|
||||
>
|
||||
<div
|
||||
class="badge-icon"
|
||||
>
|
||||
<img
|
||||
alt="4"
|
||||
src="/api/path/to/fourth/icon"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="badge-info"
|
||||
>
|
||||
<div
|
||||
class="badge-description"
|
||||
>
|
||||
Fourth description
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="badge-selection-item"
|
||||
>
|
||||
<div
|
||||
class="badge-icon"
|
||||
>
|
||||
<img
|
||||
alt="5"
|
||||
src="/api/path/to/fifth/icon"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="badge-info"
|
||||
>
|
||||
<div
|
||||
class="badge-description"
|
||||
>
|
||||
Fifth description
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!---->
|
||||
</article>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`badge settings with badges no more badges available selecting an empty slot shows no more badges available message 1`] = `
|
||||
<div>
|
||||
<article
|
||||
class="base-card"
|
||||
>
|
||||
<h2
|
||||
class="title"
|
||||
>
|
||||
settings.badges.name
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
settings.badges.description
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="ds-space ds-space-centered"
|
||||
style="margin-top: 24px; margin-bottom: 16px;"
|
||||
>
|
||||
<div
|
||||
class="presenterContainer"
|
||||
>
|
||||
<div
|
||||
class="hc-badges"
|
||||
>
|
||||
<button
|
||||
class="hc-badge-container"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/verification/icon"
|
||||
title="Verification description"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="hc-badge-container selectable"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/path/to/some/icon"
|
||||
title="Some description"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="hc-badge-container selectable selected"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/path/to/empty/icon"
|
||||
title="Empty"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="hc-badge-container selectable"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/path/to/third/icon"
|
||||
title="Third description"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
|
||||
settings.badges.no-badges-available
|
||||
|
||||
</p>
|
||||
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
</div>
|
||||
|
||||
<!---->
|
||||
</article>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`badge settings with badges renders 1`] = `
|
||||
<div>
|
||||
<article
|
||||
class="base-card"
|
||||
>
|
||||
<h2
|
||||
class="title"
|
||||
>
|
||||
settings.badges.name
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
settings.badges.description
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="ds-space ds-space-centered"
|
||||
style="margin-top: 24px; margin-bottom: 16px;"
|
||||
>
|
||||
<div
|
||||
class="presenterContainer"
|
||||
>
|
||||
<div
|
||||
class="hc-badges"
|
||||
>
|
||||
<button
|
||||
class="hc-badge-container"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/verification/icon"
|
||||
title="Verification description"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="hc-badge-container selectable"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/path/to/some/icon"
|
||||
title="Some description"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="hc-badge-container selectable"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/path/to/empty/icon"
|
||||
title="Empty"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="hc-badge-container selectable"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/path/to/third/icon"
|
||||
title="Third description"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!---->
|
||||
|
||||
<div>
|
||||
<strong>
|
||||
|
||||
settings.badges.click-to-select
|
||||
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
</div>
|
||||
|
||||
<!---->
|
||||
</article>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`badge settings with badges selecting a used badge clicking remove badge button with successful server request removes the badge 1`] = `
|
||||
<div>
|
||||
<article
|
||||
class="base-card"
|
||||
>
|
||||
<h2
|
||||
class="title"
|
||||
>
|
||||
settings.badges.name
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
settings.badges.description
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="ds-space"
|
||||
>
|
||||
<div
|
||||
class="presenterContainer"
|
||||
>
|
||||
<div
|
||||
class="hc-badges"
|
||||
style="transform: scale(2);"
|
||||
>
|
||||
<button
|
||||
class="hc-badge-container"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/verification/icon"
|
||||
title="Verification description"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="hc-badge-container selectable"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/path/to/some/icon"
|
||||
title="Some description"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="hc-badge-container selectable"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/path/to/empty/icon"
|
||||
title="Empty"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="hc-badge-container selectable"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/path/to/third/icon"
|
||||
title="Third description"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
</div>
|
||||
|
||||
<!---->
|
||||
</article>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`badge settings without badges renders 1`] = `
|
||||
<div>
|
||||
<article
|
||||
class="base-card"
|
||||
>
|
||||
<h2
|
||||
class="title"
|
||||
>
|
||||
settings.badges.name
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
settings.badges.description
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="ds-space ds-space-centered"
|
||||
style="margin-top: 24px; margin-bottom: 16px;"
|
||||
>
|
||||
<div
|
||||
class="presenterContainer"
|
||||
>
|
||||
<div
|
||||
class="hc-badges"
|
||||
>
|
||||
<button
|
||||
class="hc-badge-container"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/verification/icon"
|
||||
title="Verification description"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
</div>
|
||||
|
||||
<!---->
|
||||
</article>
|
||||
</div>
|
||||
`;
|
||||
302
webapp/pages/settings/badges.spec.js
Normal file
302
webapp/pages/settings/badges.spec.js
Normal file
@ -0,0 +1,302 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/vue'
|
||||
import '@testing-library/jest-dom'
|
||||
import badges from './badges.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('badge settings', () => {
|
||||
let mocks
|
||||
|
||||
const apolloMutateMock = jest.fn()
|
||||
|
||||
const Wrapper = () => {
|
||||
return render(badges, {
|
||||
localVue,
|
||||
mocks,
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$toast: {
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
$apollo: {
|
||||
mutate: apolloMutateMock,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe('without badges', () => {
|
||||
beforeEach(() => {
|
||||
mocks.$store = {
|
||||
getters: {
|
||||
'auth/isModerator': () => false,
|
||||
'auth/user': {
|
||||
id: 'u23',
|
||||
badgeVerification: {
|
||||
id: 'bv1',
|
||||
icon: '/verification/icon',
|
||||
description: 'Verification description',
|
||||
isDefault: true,
|
||||
},
|
||||
badgeTrophiesSelected: [],
|
||||
badgeTrophiesUnused: [],
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
it('renders', () => {
|
||||
const wrapper = Wrapper()
|
||||
expect(wrapper.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe('with badges', () => {
|
||||
const badgeTrophiesSelected = [
|
||||
{
|
||||
id: '1',
|
||||
icon: '/path/to/some/icon',
|
||||
isDefault: false,
|
||||
description: 'Some description',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
icon: '/path/to/empty/icon',
|
||||
isDefault: true,
|
||||
description: 'Empty',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
icon: '/path/to/third/icon',
|
||||
isDefault: false,
|
||||
description: 'Third description',
|
||||
},
|
||||
]
|
||||
|
||||
const badgeTrophiesUnused = [
|
||||
{
|
||||
id: '4',
|
||||
icon: '/path/to/fourth/icon',
|
||||
description: 'Fourth description',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
icon: '/path/to/fifth/icon',
|
||||
description: 'Fifth description',
|
||||
},
|
||||
]
|
||||
|
||||
let wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.$store = {
|
||||
getters: {
|
||||
'auth/isModerator': () => false,
|
||||
'auth/user': {
|
||||
id: 'u23',
|
||||
badgeVerification: {
|
||||
id: 'bv1',
|
||||
icon: '/verification/icon',
|
||||
description: 'Verification description',
|
||||
isDefault: false,
|
||||
},
|
||||
badgeTrophiesSelected,
|
||||
badgeTrophiesUnused,
|
||||
},
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('renders', () => {
|
||||
expect(wrapper.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
describe('selecting a used badge', () => {
|
||||
beforeEach(async () => {
|
||||
const badge = screen.getByTitle(badgeTrophiesSelected[0].description)
|
||||
await fireEvent.click(badge)
|
||||
})
|
||||
|
||||
it('shows remove badge button', () => {
|
||||
expect(screen.getByText('settings.badges.remove')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
describe('clicking remove badge button', () => {
|
||||
const clickButton = async () => {
|
||||
const removeButton = screen.getByText('settings.badges.remove')
|
||||
await fireEvent.click(removeButton)
|
||||
}
|
||||
|
||||
describe('with successful server request', () => {
|
||||
beforeEach(() => {
|
||||
apolloMutateMock.mockResolvedValue({
|
||||
data: {
|
||||
setTrophyBadgeSelected: {
|
||||
id: 'u23',
|
||||
badgeTrophiesSelected: [
|
||||
{
|
||||
id: '2',
|
||||
icon: '/path/to/empty/icon',
|
||||
isDefault: true,
|
||||
description: 'Empty',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
icon: '/path/to/empty/icon',
|
||||
isDefault: true,
|
||||
description: 'Empty',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
icon: '/path/to/third/icon',
|
||||
isDefault: false,
|
||||
description: 'Third description',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
clickButton()
|
||||
})
|
||||
|
||||
it('calls the server', () => {
|
||||
expect(apolloMutateMock).toHaveBeenCalledWith({
|
||||
mutation: expect.anything(),
|
||||
update: expect.anything(),
|
||||
variables: {
|
||||
badgeId: null,
|
||||
slot: 0,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
/* To test this, we would need a better apollo mock */
|
||||
it.skip('removes the badge', async () => {
|
||||
expect(wrapper.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('shows a success message', () => {
|
||||
expect(mocks.$toast.success).toHaveBeenCalledWith('settings.badges.success-update')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with failed server request', () => {
|
||||
beforeEach(() => {
|
||||
apolloMutateMock.mockRejectedValue({ message: 'Ouch!' })
|
||||
clickButton()
|
||||
})
|
||||
|
||||
it('shows an error message', () => {
|
||||
expect(mocks.$toast.error).toHaveBeenCalledWith('settings.badges.error-update')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('no more badges available', () => {
|
||||
beforeEach(async () => {
|
||||
mocks.$store.getters['auth/user'].badgeTrophiesUnused = []
|
||||
})
|
||||
|
||||
describe('selecting an empty slot', () => {
|
||||
beforeEach(async () => {
|
||||
const emptySlot = screen.getAllByTitle('Empty')[0]
|
||||
await fireEvent.click(emptySlot)
|
||||
})
|
||||
|
||||
it('shows no more badges available message', () => {
|
||||
expect(wrapper.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('more badges available', () => {
|
||||
describe('selecting an empty slot', () => {
|
||||
beforeEach(async () => {
|
||||
const emptySlot = screen.getAllByTitle('Empty')[0]
|
||||
await fireEvent.click(emptySlot)
|
||||
})
|
||||
|
||||
it('shows list with available badges', () => {
|
||||
expect(wrapper.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
describe('clicking on an available badge', () => {
|
||||
const clickBadge = async () => {
|
||||
const badge = screen.getByText(badgeTrophiesUnused[0].description)
|
||||
await fireEvent.click(badge)
|
||||
}
|
||||
|
||||
describe('with successful server request', () => {
|
||||
beforeEach(() => {
|
||||
apolloMutateMock.mockResolvedValue({
|
||||
data: {
|
||||
setTrophyBadgeSelected: {
|
||||
id: 'u23',
|
||||
badgeTrophiesSelected: [
|
||||
{
|
||||
id: '4',
|
||||
icon: '/path/to/fourth/icon',
|
||||
description: 'Fourth description',
|
||||
isDefault: false,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
icon: '/path/to/empty/icon',
|
||||
isDefault: true,
|
||||
description: 'Empty',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
icon: '/path/to/third/icon',
|
||||
isDefault: false,
|
||||
description: 'Third description',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
clickBadge()
|
||||
})
|
||||
|
||||
it('calls the server', () => {
|
||||
expect(apolloMutateMock).toHaveBeenCalledWith({
|
||||
mutation: expect.anything(),
|
||||
update: expect.anything(),
|
||||
variables: {
|
||||
badgeId: '4',
|
||||
slot: 1,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
/* To test this, we would need a better apollo mock */
|
||||
it.skip('adds the badge', async () => {
|
||||
expect(wrapper.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('shows a success message', () => {
|
||||
expect(mocks.$toast.success).toHaveBeenCalledWith('settings.badges.success-update')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with failed server request', () => {
|
||||
beforeEach(() => {
|
||||
apolloMutateMock.mockRejectedValue({ message: 'Ouch!' })
|
||||
clickBadge()
|
||||
})
|
||||
|
||||
it('shows an error message', () => {
|
||||
expect(mocks.$toast.error).toHaveBeenCalledWith('settings.badges.error-update')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
176
webapp/pages/settings/badges.vue
Normal file
176
webapp/pages/settings/badges.vue
Normal file
@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<base-card>
|
||||
<h2 class="title">{{ $t('settings.badges.name') }}</h2>
|
||||
<p>{{ $t('settings.badges.description') }}</p>
|
||||
<ds-space centered margin-bottom="small" margin-top="base">
|
||||
<div class="presenterContainer">
|
||||
<badges
|
||||
:badges="[currentUser.badgeVerification, ...selectedBadges]"
|
||||
:selection-mode="true"
|
||||
@badge-selected="handleBadgeSlotSelection"
|
||||
ref="badgesComponent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p v-if="!availableBadges.length && isEmptySlotSelected">
|
||||
{{ $t('settings.badges.no-badges-available') }}
|
||||
</p>
|
||||
|
||||
<div v-if="availableBadges.length > 0">
|
||||
<strong>
|
||||
{{
|
||||
selectedBadgeIndex === null
|
||||
? this.$t('settings.badges.click-to-select')
|
||||
: isEmptySlotSelected
|
||||
? this.$t('settings.badges.click-to-use')
|
||||
: ''
|
||||
}}
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedBadgeIndex !== null && !isEmptySlotSelected" class="badge-actions">
|
||||
<base-button @click="removeBadgeFromSlot" class="remove-button">
|
||||
{{ $t('settings.badges.remove') }}
|
||||
</base-button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="availableBadges.length && selectedBadgeIndex !== null && isEmptySlotSelected"
|
||||
class="selection-info"
|
||||
>
|
||||
<badge-selection
|
||||
:badges="availableBadges"
|
||||
@badge-selected="assignBadgeToSlot"
|
||||
ref="badgeSelection"
|
||||
/>
|
||||
</div>
|
||||
</ds-space>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import { setTrophyBadgeSelected } from '~/graphql/User'
|
||||
import scrollToContent from './scroll-to-content.js'
|
||||
import Badges from '../../components/Badges.vue'
|
||||
import BadgeSelection from '../../components/BadgeSelection.vue'
|
||||
|
||||
export default {
|
||||
components: { BadgeSelection, Badges },
|
||||
mixins: [scrollToContent],
|
||||
data() {
|
||||
return {
|
||||
selectedBadgeIndex: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'auth/user',
|
||||
}),
|
||||
selectedBadges() {
|
||||
return this.currentUser.badgeTrophiesSelected
|
||||
},
|
||||
availableBadges() {
|
||||
return this.currentUser.badgeTrophiesUnused
|
||||
},
|
||||
isEmptySlotSelected() {
|
||||
return this.selectedBadges[this.selectedBadgeIndex]?.isDefault ?? false
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.userBadges = [...(this.currentUser.badgeTrophiesSelected || [])]
|
||||
},
|
||||
methods: {
|
||||
...mapMutations({
|
||||
setCurrentUser: 'auth/SET_USER',
|
||||
}),
|
||||
handleBadgeSlotSelection(index) {
|
||||
if (index === 0) {
|
||||
this.$toast.info(this.$t('settings.badges.verification'))
|
||||
this.$refs.badgesComponent.resetSelection()
|
||||
return
|
||||
}
|
||||
|
||||
this.selectedBadgeIndex = index === null ? null : index - 1 // The first badge in badges component is the verification badge
|
||||
},
|
||||
async setSlot(badge, slot) {
|
||||
await this.$apollo.mutate({
|
||||
mutation: setTrophyBadgeSelected,
|
||||
variables: {
|
||||
badgeId: badge?.id ?? null,
|
||||
slot,
|
||||
},
|
||||
update: (_, { data: { setTrophyBadgeSelected } }) => {
|
||||
const { badgeTrophiesSelected, badgeTrophiesUnused } = setTrophyBadgeSelected
|
||||
this.setCurrentUser({
|
||||
...this.currentUser,
|
||||
badgeTrophiesSelected,
|
||||
badgeTrophiesUnused,
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
async assignBadgeToSlot(badge) {
|
||||
if (!badge || this.selectedBadgeIndex === null) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await this.setSlot(badge, this.selectedBadgeIndex)
|
||||
this.$toast.success(this.$t('settings.badges.success-update'))
|
||||
} catch (error) {
|
||||
this.$toast.error(this.$t('settings.badges.error-update'))
|
||||
}
|
||||
|
||||
if (this.$refs.badgeSelection && this.$refs.badgeSelection.resetSelection) {
|
||||
this.$refs.badgeSelection.resetSelection()
|
||||
}
|
||||
this.$refs.badgesComponent.resetSelection()
|
||||
this.selectedBadgeIndex = null
|
||||
},
|
||||
async removeBadgeFromSlot() {
|
||||
if (this.selectedBadgeIndex === null) return
|
||||
|
||||
try {
|
||||
await this.setSlot(null, this.selectedBadgeIndex)
|
||||
this.$toast.success(this.$t('settings.badges.success-update'))
|
||||
} catch (error) {
|
||||
this.$toast.error(this.$t('settings.badges.error-update'))
|
||||
}
|
||||
|
||||
this.$refs.badgesComponent.resetSelection()
|
||||
this.selectedBadgeIndex = null
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.presenterContainer {
|
||||
margin-top: 20px;
|
||||
padding-top: 50px;
|
||||
min-height: 220px;
|
||||
--badges-scale: 2;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 400px) {
|
||||
.presenterContainer {
|
||||
--badges-scale: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.badge-actions {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.remove-button {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.selection-info {
|
||||
margin-top: 20px;
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
11
webapp/static/img/badges/stars.svg
Normal file
11
webapp/static/img/badges/stars.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg width="170mm" height="65mm" version="1.1" viewBox="0 0 170 65" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(-40.481 -87.048)" fill="#f2bd27" stroke-width="1.5118">
|
||||
<path transform="matrix(.30472 .0030284 -.0030284 .30472 .96713 112.56)" d="m461.32 72.567-52.541-26.955-51.991 28.001 9.3998-58.299-42.696-40.794 58.35-9.0756 25.603-53.213 26.663 52.69 58.52 7.9067-41.872 41.64z"/>
|
||||
<path transform="matrix(.21871 .0021736 -.0021736 .21871 75.918 124.75)" d="m461.32 72.567-52.541-26.955-51.991 28.001 9.3998-58.299-42.696-40.794 58.35-9.0756 25.603-53.213 26.663 52.69 58.52 7.9067-41.872 41.64z"/>
|
||||
<path transform="matrix(.21871 .0021736 -.0021736 .21871 -3.5617 124.75)" d="m461.32 72.567-52.541-26.955-51.991 28.001 9.3998-58.299-42.696-40.794 58.35-9.0756 25.603-53.213 26.663 52.69 58.52 7.9067-41.872 41.64z"/>
|
||||
<path transform="matrix(.16908 .0016804 -.0016804 .16908 -13.934 138.16)" d="m461.32 72.567-52.541-26.955-51.991 28.001 9.3998-58.299-42.696-40.794 58.35-9.0756 25.603-53.213 26.663 52.69 58.52 7.9067-41.872 41.64z"/>
|
||||
<path transform="matrix(.16908 .0016804 -.0016804 .16908 126.82 138.16)" d="m461.32 72.567-52.541-26.955-51.991 28.001 9.3998-58.299-42.696-40.794 58.35-9.0756 25.603-53.213 26.663 52.69 58.52 7.9067-41.872 41.64z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@ -7,6 +7,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf"
|
||||
integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==
|
||||
|
||||
"@adobe/css-tools@^4.4.0":
|
||||
version "4.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.2.tgz#c836b1bd81e6d62cd6cdf3ee4948bcdce8ea79c8"
|
||||
integrity sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==
|
||||
|
||||
"@ampproject/remapping@^2.2.0":
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630"
|
||||
@ -4253,6 +4258,19 @@
|
||||
lz-string "^1.5.0"
|
||||
pretty-format "^27.0.2"
|
||||
|
||||
"@testing-library/jest-dom@^6.6.3":
|
||||
version "6.6.3"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz#26ba906cf928c0f8172e182c6fe214eb4f9f2bd2"
|
||||
integrity sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==
|
||||
dependencies:
|
||||
"@adobe/css-tools" "^4.4.0"
|
||||
aria-query "^5.0.0"
|
||||
chalk "^3.0.0"
|
||||
css.escape "^1.5.1"
|
||||
dom-accessibility-api "^0.6.3"
|
||||
lodash "^4.17.21"
|
||||
redent "^3.0.0"
|
||||
|
||||
"@testing-library/vue@5":
|
||||
version "5.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/vue/-/vue-5.9.0.tgz#d33c52ae89e076808abe622f70dcbccb1b5d080c"
|
||||
@ -6058,6 +6076,11 @@ aria-query@5.1.3:
|
||||
dependencies:
|
||||
deep-equal "^2.0.5"
|
||||
|
||||
aria-query@^5.0.0:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59"
|
||||
integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==
|
||||
|
||||
arr-diff@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
|
||||
@ -8230,6 +8253,11 @@ css-what@2.1, css-what@^2.1.2:
|
||||
resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
|
||||
integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
|
||||
|
||||
css.escape@^1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb"
|
||||
integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==
|
||||
|
||||
csscolorparser@~1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/csscolorparser/-/csscolorparser-1.0.3.tgz#b34f391eea4da8f3e98231e2ccd8df9c041f171b"
|
||||
@ -8793,6 +8821,11 @@ dom-accessibility-api@^0.5.9:
|
||||
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453"
|
||||
integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==
|
||||
|
||||
dom-accessibility-api@^0.6.3:
|
||||
version "0.6.3"
|
||||
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8"
|
||||
integrity sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==
|
||||
|
||||
dom-converter@^0.2:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user