Skip to content

Commit 1969192

Browse files
authored
Merge pull request #4773 from Ocelot-Social-Community/4771-refactor-social-media-list-with-slots
feat: 🍰 Refactor Social Media List With Slots
2 parents ea71e9b + f63165a commit 1969192

File tree

11 files changed

+507
-197
lines changed

11 files changed

+507
-197
lines changed

backend/src/middleware/notifications/notificationsMiddleware.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ const publishNotifications = async (context, promises) => {
4141
notifications.forEach((notificationAdded, index) => {
4242
pubsub.publish(NOTIFICATION_ADDED, { notificationAdded })
4343
if (notificationAdded.to.sendNotificationEmails) {
44-
// Wolle await
4544
sendMail(
4645
notificationTemplate({
4746
email: notificationsEmailAddresses[index].email,
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { When } from "cypress-cucumber-preprocessor/steps";
22

33
When('I add a social media link', () => {
4-
cy.get('input#addSocialMedia')
4+
cy.get('button')
5+
.contains('Add link')
6+
.click()
7+
.get('#editSocialMedia')
58
.type('https://freeradical.zone/peter-pan')
69
.get('button')
7-
.contains('Add link')
10+
.contains('Save')
811
.click()
912
})

cypress/integration/UserProfile.SocialMedia/I_have_added_a_social_media_link.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ import { Given } from "cypress-cucumber-preprocessor/steps";
22

33
Given('I have added a social media link', () => {
44
cy.visit('/settings/my-social-media')
5-
.get('input#addSocialMedia')
6-
.type('https://freeradical.zone/peter-pan')
75
.get('button')
86
.contains('Add link')
97
.click()
8+
.get('#editSocialMedia')
9+
.type('https://freeradical.zone/peter-pan')
10+
.get('button')
11+
.contains('Save')
12+
.click()
1013
})
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { mount } from '@vue/test-utils'
2+
import MySomethingList from './MySomethingList.vue'
3+
import Vue from 'vue'
4+
5+
const localVue = global.localVue
6+
7+
describe('MySomethingList.vue', () => {
8+
let wrapper
9+
let propsData
10+
let data
11+
let mocks
12+
13+
beforeEach(() => {
14+
propsData = {
15+
useFormData: { dummy: '' },
16+
useItems: [{ id: 'id', dummy: 'dummy' }],
17+
namePropertyKey: 'dummy',
18+
callbacks: { edit: jest.fn(), submit: jest.fn(), delete: jest.fn() },
19+
}
20+
data = () => {
21+
return {}
22+
}
23+
mocks = {
24+
$t: jest.fn(),
25+
$apollo: {
26+
mutate: jest.fn(),
27+
},
28+
$toast: {
29+
error: jest.fn(),
30+
success: jest.fn(),
31+
},
32+
}
33+
})
34+
35+
describe('mount', () => {
36+
let form, slots
37+
const Wrapper = () => {
38+
slots = {
39+
'list-item': '<div class="list-item"></div>',
40+
'edit-item': '<div class="edit-item"></div>',
41+
}
42+
return mount(MySomethingList, {
43+
propsData,
44+
data,
45+
mocks,
46+
localVue,
47+
slots,
48+
})
49+
}
50+
51+
describe('given existing item', () => {
52+
beforeEach(() => {
53+
wrapper = Wrapper()
54+
})
55+
56+
describe('for each item it', () => {
57+
it('displays the item as slot "list-item"', () => {
58+
expect(wrapper.find('.list-item').exists()).toBe(true)
59+
})
60+
61+
it('displays the edit button', () => {
62+
expect(wrapper.find('.base-button[data-test="edit-button"]').exists()).toBe(true)
63+
})
64+
65+
it('displays the delete button', () => {
66+
expect(wrapper.find('.base-button[data-test="delete-button"]').exists()).toBe(true)
67+
})
68+
})
69+
70+
describe('editing item', () => {
71+
beforeEach(async () => {
72+
const editButton = wrapper.find('.base-button[data-test="edit-button"]')
73+
editButton.trigger('click')
74+
await Vue.nextTick()
75+
})
76+
77+
it('disables adding items while editing', () => {
78+
const submitButton = wrapper.find('.base-button[data-test="add-save-button"]')
79+
expect(submitButton.text()).not.toContain('settings.social-media.submit')
80+
})
81+
82+
it('allows the user to cancel editing', async () => {
83+
expect(wrapper.find('.edit-item').exists()).toBe(true)
84+
const cancelButton = wrapper.find('button#cancel')
85+
cancelButton.trigger('click')
86+
await Vue.nextTick()
87+
expect(wrapper.find('.edit-item').exists()).toBe(false)
88+
})
89+
})
90+
91+
describe('calls callback functions', () => {
92+
it('calls edit', async () => {
93+
const editButton = wrapper.find('.base-button[data-test="edit-button"]')
94+
editButton.trigger('click')
95+
await Vue.nextTick()
96+
const expectedItem = expect.objectContaining({ id: 'id', dummy: 'dummy' })
97+
expect(propsData.callbacks.edit).toHaveBeenCalledTimes(1)
98+
expect(propsData.callbacks.edit).toHaveBeenCalledWith(expect.any(Object), expectedItem)
99+
})
100+
101+
it('calls submit', async () => {
102+
form = wrapper.find('form')
103+
form.trigger('submit')
104+
await Vue.nextTick()
105+
form.trigger('submit')
106+
await Vue.nextTick()
107+
const expectedItem = expect.objectContaining({ id: '' })
108+
expect(propsData.callbacks.submit).toHaveBeenCalledTimes(1)
109+
expect(propsData.callbacks.submit).toHaveBeenCalledWith(
110+
expect.any(Object),
111+
true,
112+
expectedItem,
113+
{ dummy: '' },
114+
)
115+
})
116+
117+
it('calls delete', async () => {
118+
const deleteButton = wrapper.find('.base-button[data-test="delete-button"]')
119+
deleteButton.trigger('click')
120+
await Vue.nextTick()
121+
const expectedItem = expect.objectContaining({ id: 'id', dummy: 'dummy' })
122+
expect(propsData.callbacks.delete).toHaveBeenCalledTimes(1)
123+
expect(propsData.callbacks.delete).toHaveBeenCalledWith(expect.any(Object), expectedItem)
124+
})
125+
})
126+
})
127+
})
128+
})
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
<template>
2+
<ds-form
3+
v-model="formData"
4+
:schema="formSchema"
5+
@input="handleInput"
6+
@input-valid="handleInputValid"
7+
@submit="handleSubmitItem"
8+
>
9+
<div v-if="isEditing">
10+
<ds-space margin="base">
11+
<ds-heading tag="h5">
12+
{{
13+
isCreation
14+
? $t('settings.social-media.addNewTitle')
15+
: $t('settings.social-media.editTitle', { name: editingItem[namePropertyKey] })
16+
}}
17+
</ds-heading>
18+
</ds-space>
19+
<ds-space v-if="items" margin-top="base">
20+
<slot name="edit-item" />
21+
</ds-space>
22+
</div>
23+
<div v-else>
24+
<ds-space v-if="items" margin-top="base">
25+
<ds-list>
26+
<ds-list-item v-for="item in items" :key="item.id" class="list-item--high">
27+
<template>
28+
<slot name="list-item" :item="item" />
29+
<span class="divider">|</span>
30+
<base-button
31+
icon="edit"
32+
circle
33+
ghost
34+
@click="handleEditItem(item)"
35+
:title="$t('actions.edit')"
36+
data-test="edit-button"
37+
/>
38+
<base-button
39+
icon="trash"
40+
circle
41+
ghost
42+
@click="handleDeleteItem(item)"
43+
:title="$t('actions.delete')"
44+
data-test="delete-button"
45+
/>
46+
</template>
47+
</ds-list-item>
48+
</ds-list>
49+
</ds-space>
50+
</div>
51+
52+
<ds-space margin-top="base">
53+
<ds-space margin-top="base">
54+
<base-button
55+
filled
56+
:disabled="loading || !(!isEditing || (isEditing && !disabled))"
57+
:loading="loading"
58+
type="submit"
59+
data-test="add-save-button"
60+
>
61+
{{ isEditing ? $t('actions.save') : $t('settings.social-media.submit') }}
62+
</base-button>
63+
<base-button v-if="isEditing" id="cancel" danger @click="handleCancel()">
64+
{{ $t('actions.cancel') }}
65+
</base-button>
66+
</ds-space>
67+
</ds-space>
68+
</ds-form>
69+
</template>
70+
71+
<script>
72+
export default {
73+
name: 'MySomethingList',
74+
props: {
75+
useFormData: {
76+
type: Object,
77+
default: () => ({}),
78+
},
79+
useFormSchema: {
80+
type: Object,
81+
default: () => ({}),
82+
},
83+
useItems: {
84+
type: Array,
85+
default: () => [],
86+
},
87+
defaultItem: {
88+
type: Object,
89+
default: () => ({}),
90+
},
91+
namePropertyKey: {
92+
type: String,
93+
required: true,
94+
},
95+
callbacks: {
96+
type: Object,
97+
default: () => ({
98+
handleInput: () => {},
99+
handleInputValid: () => {},
100+
edit: () => {},
101+
submit: () => {},
102+
delete: () => {},
103+
}),
104+
},
105+
},
106+
data() {
107+
return {
108+
formData: this.useFormData,
109+
formSchema: this.useFormSchema,
110+
items: this.useItems,
111+
disabled: true,
112+
loading: false,
113+
editingItem: null,
114+
}
115+
},
116+
computed: {
117+
isEditing() {
118+
return this.editingItem !== null
119+
},
120+
isCreation() {
121+
return this.editingItem !== null && this.editingItem.id === ''
122+
},
123+
},
124+
watch: {
125+
// can change by a parents callback and again given trough by v-bind from there
126+
useItems(newItems) {
127+
this.items = newItems
128+
},
129+
},
130+
methods: {
131+
handleInput(data) {
132+
this.callbacks.handleInput(this, data)
133+
this.disabled = true
134+
},
135+
handleInputValid(data) {
136+
this.callbacks.handleInputValid(this, data)
137+
},
138+
handleEditItem(item) {
139+
this.editingItem = item
140+
this.callbacks.edit(this, item)
141+
},
142+
async handleSubmitItem() {
143+
if (!this.isEditing) {
144+
this.handleEditItem({ ...this.defaultItem, id: '' })
145+
} else {
146+
this.loading = true
147+
if (await this.callbacks.submit(this, this.isCreation, this.editingItem, this.formData)) {
148+
this.disabled = true
149+
this.editingItem = null
150+
}
151+
this.loading = false
152+
}
153+
},
154+
handleCancel() {
155+
this.editingItem = null
156+
this.disabled = true
157+
},
158+
async handleDeleteItem(item) {
159+
await this.callbacks.delete(this, item)
160+
},
161+
},
162+
}
163+
</script>
164+
165+
<style lang="scss" scope>
166+
.divider {
167+
opacity: 0.4;
168+
padding: 0 $space-small;
169+
}
170+
171+
.icon-button {
172+
cursor: pointer;
173+
}
174+
175+
.list-item--high {
176+
.ds-list-item-prefix {
177+
align-self: center;
178+
}
179+
180+
.ds-list-item-content {
181+
display: flex;
182+
align-items: center;
183+
}
184+
}
185+
</style>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { shallowMount } from '@vue/test-utils'
2+
import SocialMediaListItem from './SocialMediaListItem.vue'
3+
4+
describe('SocialMediaListItem.vue', () => {
5+
let wrapper
6+
let propsData
7+
const socialMediaUrl = 'https://freeradical.zone/@mattwr18'
8+
const faviconUrl = 'https://freeradical.zone/favicon.ico'
9+
10+
beforeEach(() => {
11+
propsData = {}
12+
})
13+
14+
describe('shallowMount', () => {
15+
const Wrapper = () => {
16+
return shallowMount(SocialMediaListItem, { propsData })
17+
}
18+
19+
describe('given existing social media links', () => {
20+
beforeEach(() => {
21+
propsData = { item: { id: 's1', url: socialMediaUrl, favicon: faviconUrl } }
22+
wrapper = Wrapper()
23+
})
24+
25+
describe('for each link item it', () => {
26+
it('displays the favicon', () => {
27+
expect(wrapper.find(`img[src="${faviconUrl}"]`).exists()).toBe(true)
28+
})
29+
30+
it('displays the url', () => {
31+
expect(wrapper.find(`a[href="${socialMediaUrl}"]`).exists()).toBe(true)
32+
})
33+
})
34+
})
35+
})
36+
})

0 commit comments

Comments
 (0)