Merge branch '5059-epic-groups' into search-groups

This commit is contained in:
Moriz Wahl 2022-10-21 16:59:13 +02:00
commit 37babb5c7c
38 changed files with 896 additions and 436 deletions

View File

@ -1,3 +1,5 @@
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
<!--
Please take a look at the issue templates at https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/new/choose
before submitting a new issue. Following one of the issue templates will ensure maintainers can route your request efficiently.

View File

@ -1,11 +1,10 @@
---
name: "\U0001F41B Bug Report"
about: Create a report to help us to improve.
title: "\U0001F41B [Bug] XXX"
name: 🐛 Bug report
about: Create a report to help us improve
labels: bug
assignees: ''
title: 🐛 [Bug]
---
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
## :bug: Bug Report
## 🐛 Bugreport
<!-- Describe your issue in detail. Include screenshots if needed. Give us as much information as possible. Use a clear and concise description of what the bug is.-->

View File

@ -1,11 +1,10 @@
---
name: "\U0001F4A5 DevOps Ticket"
about: Help us manage our deployed app.
title: "\U0001F4A5 [DevOps] XXX"
name: 💥 DevOps ticket
about: Help us manage our deployed Software.
labels: devops
assignees: ''
title: 💥 [DevOps]
---
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
## 💥 DevOps Ticket
## 💥 DevOps ticket
<!-- Describe your issue in detail. Include screenshots if needed. Give us as much information as possible. Use a clear and concise description of what the problem is.-->

View File

@ -1,15 +1,13 @@
---
name: "\U0001F31F Epic"
about: Define a big development step.
title: "\U0001F31F [EPIC] XXX"
name: 🌟 Epic
about: Define a big development Step
labels: epic
assignees: ''
title: 🌟 [EPIC]
---
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
<!-- THIS ISSUE-TYPE IS NOT FOR YOU! -->
<!-- If you need an answer right away, visit the ocelot.social Discord:
https://discord.gg/AJSX9DCSUA -->
<!-- Proceed only if you know what you are doing - have a chat with Project's Team first -->
## 🌟 EPIC
<!-- Describe your Epic in detail. Include screenshots and drawings -->

View File

@ -1,11 +1,10 @@
---
name: "\U0001F680 Feature Request"
about: Suggest an idea for this project.
title: "\U0001F680 [Feature] XXX"
name: 🚀 Feature request
about: Suggest an idea for this project
labels: feature
assignees: ''
title: 🚀 [Feature]
---
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
## :rocket: Feature Request
## 🚀 Feature
<!-- Give a short summary of the Feature. Use Screenshots if you want. -->

View File

@ -1,15 +1,13 @@
---
name: "\U0001F4AC Question"
about: If you need help understanding ocelot.social.
title: "\U0001F4AC [Question] XXX"
name: 💬 Question
about: If you need help understanding our Software.
labels: question
assignees: ''
title: 💬 [Question]
---
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
<!-- Chat with ocelot.social team -->
<!-- If you need an answer right away, visit the ocelot.social Discord:
https://discord.gg/AJSX9DCSUA -->
<!-- Question the project's team -->
<!-- If you need an answer right away, consider to take other means of communication with the project's team -->
## 💬 Question
<!-- Describe your Question in detail. Include screenshots and drawings if needed. -->

View File

@ -1,11 +1,10 @@
---
name: "\U0001F527 Refactor"
name: 🔧 Refactor ticket
about: Help us improve our code by refactoring it.
title: "\U0001F527 [Refactor] XXX"
labels: refactor
assignees: ''
title: 🔧 [Refactor]
---
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
## 🔧 Refactor
## 🔧 Refactor ticket
<!-- Describe your issue in detail. Include screenshots if needed. Give us as much information as possible. Use a clear and concise description of what the problem is.-->

13
.github/ISSUE_TEMPLATE/release.md vendored Normal file
View File

@ -0,0 +1,13 @@
---
name: 🎂 Release
about: Define a Release
labels: release
title: 🎂 [RELEASE]
---
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
<!-- THIS ISSUE-TYPE IS NOT FOR YOU! -->
<!-- Proceed only if you know what you are doing - have a chat with Project's Team first -->
## 🎂 RELEASE
<!-- Describe your Release in detail. Include screenshots and drawings -->

View File

@ -1,15 +1,15 @@
## 🍰 Pull Request
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
## 🍰 Pullrequest
<!-- Describe the Pullrequest. Use Screenshots if possible. -->
XXX
### Issues
<!-- Which Issues does this fix, which are related? -->
<!-- Which Issues does this fix, which are related?
- fixes #XXX
- relates #XXX
-->
- None
### Todo
<!-- In case some parts are still missing, list them here. -->
- [ ] XXX list here …
- [X] None

View File

@ -267,7 +267,7 @@ jobs:
report_name: Coverage Webapp
type: lcov
result_path: ./coverage/lcov.info
min_coverage: 65
min_coverage: 64
token: ${{ github.token }}
##############################################################################

View File

@ -360,6 +360,33 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
}),
])
// post into group
await Promise.all([
mutate({
mutation: createPostMutation(),
variables: {
id: 'p0-g0',
groupId: 'g0',
title: `What happend in Shanghai?`,
content: 'A sack of rise dropped in Shanghai. Should we further investigate?',
categoryIds: ['cat6'],
},
}),
])
authenticatedUser = await bobDerBaumeister.toJson()
await Promise.all([
mutate({
mutation: createPostMutation(),
variables: {
id: 'p1-g0',
groupId: 'g0',
title: `The man on the moon`,
content: 'We have to further investigate about the stories of a man living on the moon.',
categoryIds: ['cat12', 'cat16'],
},
}),
])
authenticatedUser = await jennyRostock.toJson()
await Promise.all([
mutate({
@ -439,6 +466,32 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
},
}),
])
// post into group
await Promise.all([
mutate({
mutation: createPostMutation(),
variables: {
id: 'p0-g1',
groupId: 'g1',
title: `Can we use ocelot for education?`,
content: 'I like the concept of this school. Can we use our software in this?',
categoryIds: ['cat8'],
},
}),
])
authenticatedUser = await peterLustig.toJson()
await Promise.all([
mutate({
mutation: createPostMutation(),
variables: {
id: 'p1-g1',
groupId: 'g1',
title: `Can we push this idea out of France?`,
content: 'This idea is too inportant to have the scope only on France.',
categoryIds: ['cat14'],
},
}),
])
authenticatedUser = await bobDerBaumeister.toJson()
await Promise.all([
@ -527,6 +580,20 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
}),
])
authenticatedUser = await louie.toJson()
await Promise.all([
mutate({
mutation: createPostMutation(),
variables: {
id: 'p0-g2',
groupId: 'g2',
title: `I am a Noob`,
content: 'I am new to Yoga and did not join this group so far.',
categoryIds: ['cat4'],
},
}),
])
// Create Posts
const [p0, p1, p3, p4, p5, p6, p9, p10, p11, p13, p14, p15] = await Promise.all([

View File

@ -138,6 +138,7 @@ describe('ContributionForm.vue', () => {
categoryIds: [],
id: null,
image: null,
groupId: null,
},
}
postTitleInput = wrapper.find('.ds-input')
@ -260,6 +261,7 @@ describe('ContributionForm.vue', () => {
content: propsData.contribution.content,
categoryIds: [],
id: propsData.contribution.id,
groupId: null,
image: {
sensitive: false,
},

View File

@ -99,6 +99,10 @@ export default {
type: Object,
default: () => ({}),
},
groupId: {
type: String,
default: () => null,
},
},
data() {
const { title, content, image, categories } = this.contribution
@ -173,6 +177,7 @@ export default {
categoryIds,
id: this.contribution.id || null,
image,
groupId: this.groupId,
},
})
.then(({ data }) => {

View File

@ -14,6 +14,7 @@
<div class="filter-menu-options">
<h2 class="title">{{ $t('filter-menu.filter-by') }}</h2>
<following-filter />
<categories-filter v-if="categoriesActive" />
</div>
<div class="filter-menu-options">
<h2 class="title">{{ $t('filter-menu.order-by') }}</h2>
@ -28,12 +29,19 @@ import Dropdown from '~/components/Dropdown'
import { mapGetters } from 'vuex'
import FollowingFilter from './FollowingFilter'
import OrderByFilter from './OrderByFilter'
import CategoriesFilter from './CategoriesFilter'
export default {
components: {
Dropdown,
FollowingFilter,
OrderByFilter,
CategoriesFilter,
},
data() {
return {
categoriesActive: this.$env.CATEGORIES_ACTIVE,
}
},
props: {
placement: { type: String },

View File

@ -11,13 +11,13 @@
<template #default="{ errors }">
<!-- group Name -->
<ds-input
:label="$t('group.name')"
name="name"
:label="$t('group.name')"
model="name"
autofocus
:placeholder="`${$t('group.name')} …`"
/>
<ds-chip size="base" :color="errors && errors.name && 'danger'">
<ds-chip size="base" :color="errors && errors.name ? 'danger' : 'medium'">
{{ `${formData.name.length} / ${formSchema.name.min}${formSchema.name.max}` }}
<base-icon v-if="errors && errors.name" name="warning" />
</ds-chip>
@ -42,6 +42,7 @@
<select
class="select ds-input appearance--auto"
name="groupType"
model="groupType"
:value="formData.groupType"
:disabled="update"
@change="changeGroupType($event)"
@ -52,7 +53,7 @@
</select>
<ds-chip
size="base"
:color="errors && errors.groupType && formData.groupType === '' && 'danger'"
:color="errors && errors.groupType && formData.groupType === '' ? 'danger' : 'medium'"
>
{{ `${formData.groupType === '' ? 0 : 1} / 1` }}
<base-icon
@ -77,14 +78,14 @@
{{ $t('group.description') }}
</ds-text>
<editor
name="description"
model="description"
:users="null"
:value="formData.description"
:hashtags="null"
model="description"
name="description"
@input="updateEditorDescription"
/>
<ds-chip size="base" :color="errors && errors.description && 'danger'">
<ds-chip size="base" :color="errors && errors.description ? 'danger' : 'medium'">
{{ `${descriptionLength} / ${formSchema.description.min}` }}
<base-icon v-if="errors && errors.description" name="warning" />
</ds-chip>
@ -110,7 +111,9 @@
</select>
<ds-chip
size="base"
:color="errors && errors.actionRadius && formData.actionRadius === '' && 'danger'"
:color="
errors && errors.actionRadius && formData.actionRadius === '' ? 'danger' : 'medium'
"
>
{{ `${formData.actionRadius === '' ? 0 : 1} / 1` }}
<base-icon
@ -122,7 +125,7 @@
<!-- location -->
<ds-select
id="city"
:label="$t('settings.data.labelCity')"
:label="$t('settings.data.labelCity') + locationNameLabelAddOnOldName"
v-model="formData.locationName"
:options="cities"
icon="map-marker"
@ -132,11 +135,11 @@
@input.native="handleCityInput"
/>
<base-button
v-if="formData.locationName !== ''"
v-if="formLocationName !== ''"
icon="close"
ghost
size="small"
style="position: relative; display: inline-block; right: -93%; top: -45px"
style="position: relative; display: inline-block; right: -96%; top: -33px; width: 26px"
@click="formData.locationName = ''"
></base-button>
@ -152,7 +155,7 @@
<ds-chip
v-if="categoriesActive"
size="base"
:color="errors && errors.categoryIds && 'danger'"
:color="errors && errors.categoryIds ? 'danger' : 'medium'"
>
{{ formData.categoryIds.length }} / 3
<base-icon v-if="errors && errors.categoryIds" name="warning" />
@ -163,7 +166,7 @@
<nuxt-link to="/my-groups">
<ds-button>{{ $t('actions.cancel') }}</ds-button>
</nuxt-link>
<ds-button type="submit" icon="save" primary :disabled="errors" fill>
<ds-button type="submit" icon="save" primary :disabled="checkFormError(errors)" fill>
{{ update ? $t('group.update') : $t('group.save') }}
</ds-button>
</ds-space>
@ -219,6 +222,12 @@ export default {
groupType: groupType || '',
about: about || '',
description: description || '',
// from database 'locationName' comes as "string | null"
// 'formData.locationName':
// see 'created': tries to set it to a "requestGeoData" object and fills the menu if possible
// if user selects one from menu we get a "requestGeoData" object here
// "requestGeoData" object: "{ id: String, label: String, value: String }"
// otherwise it's a string: empty or none empty
locationName: locationName || '',
actionRadius: actionRadius || '',
categoryIds: categories ? categories.map((category) => category.id) : [],
@ -257,12 +266,63 @@ export default {
},
}
},
async created() {
// set to "requestGeoData" object and fill select menu if possible
this.formData.locationName =
(await this.requestGeoData(this.formLocationName)) || this.formLocationName
},
computed: {
formLocationName() {
const isNestedValue =
typeof this.formData.locationName === 'object' &&
typeof this.formData.locationName.value === 'string'
const isDirectString = typeof this.formData.locationName === 'string'
return isNestedValue
? this.formData.locationName.value
: isDirectString
? this.formData.locationName
: ''
},
locationNameLabelAddOnOldName() {
return this.formLocationName !== '' ? ' — ' + this.formLocationName : ''
},
descriptionLength() {
return this.$filters.removeHtml(this.formData.description).length
},
sameLocation() {
const dbLocationName = this.group.locationName || ''
return dbLocationName === this.formLocationName
},
sameCategories() {
if (this.group.categories.length !== this.formData.categoryIds.length) return false
const groupCategories = []
this.group.categories.forEach((categories) => {
groupCategories.push(categories.id)
const some = this.formData.categoryIds.some((item) => item === categories.id)
if (!some) return false
})
return true
},
disableButtonByUpdate() {
if (!this.update) return true
return (
this.group.name === this.formData.name &&
this.group.slug === this.formData.slug &&
this.group.about === this.formData.about &&
this.group.description === this.formData.description &&
this.group.actionRadius === this.formData.actionRadius &&
this.sameLocation &&
this.sameCategories
)
},
},
methods: {
checkFormError(error) {
if (!this.update && error && !!error && this.disableButtonByUpdate) return true
if (this.update && !error && this.disableButtonByUpdate) return true
return false
},
changeGroupType(event) {
this.formData.groupType = event.target.value
},
@ -273,7 +333,7 @@ export default {
this.$refs.groupForm.update('description', value)
},
submit() {
const { name, about, description, groupType, actionRadius, locationName, categoryIds } =
const { name, about, description, groupType, actionRadius, /* locationName, */ categoryIds } =
this.formData
const variables = {
name,
@ -281,7 +341,7 @@ export default {
description,
groupType,
actionRadius,
locationName: locationName.label ? locationName.label : '',
locationName: this.formLocationName,
categoryIds,
}
this.update
@ -291,9 +351,12 @@ export default {
})
: this.$emit('createGroup', variables)
},
handleCityInput(value) {
handleCityInput(event) {
clearTimeout(timeout)
timeout = setTimeout(() => this.requestGeoData(value), 500)
timeout = setTimeout(
() => this.requestGeoData(event.target ? event.target.value.trim() : ''),
500,
)
},
processLocationsResult(places) {
if (!places.length) {
@ -310,8 +373,7 @@ export default {
return result
},
async requestGeoData(e) {
const value = e.target ? e.target.value.trim() : ''
async requestGeoData(value) {
if (value === '') {
this.cities = []
return
@ -322,11 +384,13 @@ export default {
const lang = this.$i18n.locale()
const {
data: { queryLocations: res },
data: { queryLocations: result },
} = await this.$apollo.query({ query: queryLocations(), variables: { place, lang } })
this.cities = this.processLocationsResult(res)
this.cities = this.processLocationsResult(result)
this.loadingGeo = false
return this.cities.find((city) => city.value === value)
},
},
}

View File

@ -0,0 +1,33 @@
import { mount } from '@vue/test-utils'
import GroupList from './GroupList.vue'
const localVue = global.localVue
const propsData = {
groups: [],
}
describe('GroupList', () => {
let wrapper
let mocks
beforeEach(() => {
mocks = {
$t: jest.fn(),
}
})
describe('mount', () => {
const Wrapper = () => {
return mount(GroupList, { propsData, mocks, localVue })
}
beforeEach(() => {
wrapper = Wrapper()
})
it('renders', () => {
expect(wrapper.findAll('.group-list')).toHaveLength(1)
})
})
})

View File

@ -1,5 +1,5 @@
<template>
<div>
<div class="group-list">
<ds-space margin-bottom="small" v-for="group in groups" :key="group.id">
<group-teaser :group="group" />
</ds-space>

View File

@ -0,0 +1,34 @@
import { mount } from '@vue/test-utils'
import GroupMember from './GroupMember.vue'
const localVue = global.localVue
const propsData = {
groupId: '',
groupMembers: [],
}
describe('GroupMember', () => {
let wrapper
let mocks
beforeEach(() => {
mocks = {
$t: jest.fn(),
}
})
describe('mount', () => {
const Wrapper = () => {
return mount(GroupMember, { propsData, mocks, localVue })
}
beforeEach(() => {
wrapper = Wrapper()
})
it('renders', () => {
expect(wrapper.findAll('.group-member')).toHaveLength(1)
})
})
})

View File

@ -1,5 +1,41 @@
<template>
<div>
<div class="group-member">
<base-card>
<h2 class="title">{{ $t('group.addUser') }}</h2>
<ds-form v-model="form" @submit="submit">
<ds-flex gutter="small">
<ds-flex-item width="90%">
<ds-input
name="query"
model="query"
:placeholder="$t('group.addUserPlaceholder')"
icon="search"
/>
</ds-flex-item>
<ds-flex-item width="30px">
<!-- <base-button filled circle type="submit" icon="search" :loading="$apollo.loading" /> -->
<base-button filled circle type="submit" icon="search" />
</ds-flex-item>
</ds-flex>
</ds-form>
<div v-if="noSlug">Kein User mit diesem Slug gefunden!</div>
<div v-if="slugUser.length > 0">
<ds-space margin="base" />
<ds-flex>
<ds-flex-item>
<ds-avatar online size="small" :name="slugUser[0].name"></ds-avatar>
</ds-flex-item>
<ds-flex-item>{{ slugUser[0].name }}</ds-flex-item>
<ds-flex-item>{{ slugUser[0].slug }}</ds-flex-item>
<ds-flex-item>
<ds-button size="small" primary @click="addMemberToGroup(slugUser)">
{{ $t('group.addMemberToGroup') }}
</ds-button>
</ds-flex-item>
</ds-flex>
<ds-space margin="base" />
</div>
</base-card>
<ds-table :fields="tableFields" :data="groupMembers" condensed>
<template #avatar="scope">
<nuxt-link
@ -51,7 +87,12 @@
</ds-chip>
</template>
<template #edit="scope">
<ds-button size="small" primary :disabled="true" @click="openModal(scope.row)">
<ds-button
v-if="scope.row.myRoleInGroup !== 'owner'"
size="small"
primary
@click="deleteMember(scope.row.id)"
>
<!-- TODO: implement removal of group members -->
<!-- :disabled="scope.row.myRoleInGroup === 'owner'"
-->
@ -74,6 +115,7 @@
</div>
</template>
<script>
import { minimisedUserQuery } from '~/graphql/User'
import { changeGroupMemberRoleMutation } from '~/graphql/groups.js'
export default {
@ -92,6 +134,11 @@ export default {
return {
isOpen: false,
memberId: null,
noSlug: false,
slugUser: [],
form: {
query: '',
},
}
},
computed: {
@ -135,14 +182,53 @@ export default {
this.$toast.error(error.message)
}
},
async addMemberToGroup() {
const newRole = 'usual'
try {
await this.$apollo.mutate({
mutation: changeGroupMemberRoleMutation(),
variables: { groupId: this.groupId, userId: this.slugUser[0].id, roleInGroup: newRole },
})
// this.$apollo.queries.GroupMembers.refetch()
this.$emit('loadGroupMembers')
this.slugUser = []
this.form.query = ''
this.$toast.success(
this.$t('group.changeMemberRole', { role: this.$t(`group.roles.${newRole}`) }),
)
} catch (error) {
this.$toast.error(error.message)
}
},
async submit() {
try {
const {
data: { User },
} = await this.$apollo.query({
query: minimisedUserQuery(),
variables: {
slug: this.form.query,
},
})
if (User.length === 0) {
this.noSlug = true
} else {
this.noSlug = false
this.slugUser = User
}
} catch (error) {
this.noSlug = true
} finally {
}
},
// TODO: implement removal of group members
// openModal(row) {
// this.isOpen = true
// this.memberId = row.id
// },
// deleteMember(id) {
// alert('deleteMember: ' + id)
// },
deleteMember(id) {
alert('deleteMember: ' + id)
},
},
}
</script>

View File

@ -1,13 +1,12 @@
export default {
MENU: [
// {
// name: 'Beiträge',
// path: '/#',
// nameIdent: 'ocelotRebranding.newsFeed',
// path: '/',
// },
// {
// name: 'Über Yunite',
// path: '/#',
// url: 'https://yunite.org',
// nameIdent: 'ocelotRebranding.about',
// url: 'https://ocelot.org',
// },
],
}

View File

@ -3,6 +3,15 @@
export default {
LOGO_HEADER_PATH: '/img/custom/logo-horizontal.svg',
LOGO_HEADER_WIDTH: '130px',
LOGO_HEADER_CLICK: {
externalLink: null,
internalPath: {
to: {
name: 'index',
},
scrollTo: '.main-navigation',
},
},
LOGO_SIGNUP_PATH: '/img/custom/logo-squared.svg',
LOGO_WELCOME_PATH: '/img/custom/logo-squared.svg',
LOGO_LOGOUT_PATH: '/img/custom/logo-squared.svg',

View File

@ -3,8 +3,20 @@ import gql from 'graphql-tag'
export default () => {
return {
CreatePost: gql`
mutation ($title: String!, $content: String!, $categoryIds: [ID], $image: ImageInput) {
CreatePost(title: $title, content: $content, categoryIds: $categoryIds, image: $image) {
mutation (
$title: String!
$content: String!
$categoryIds: [ID]
$image: ImageInput
$groupId: ID
) {
CreatePost(
title: $title
content: $content
categoryIds: $categoryIds
image: $image
groupId: $groupId
) {
title
slug
content

View File

@ -39,6 +39,11 @@ export default (i18n) => {
...locationAndBadges
}
}
group {
id
name
slug
}
}
}
`

View File

@ -49,8 +49,8 @@ export default (i18n) => {
export const minimisedUserQuery = () => {
return gql`
query {
User(orderBy: slug_asc) {
query ($slug: String) {
User(slug: $slug, orderBy: slug_asc) {
id
slug
name

View File

@ -5,7 +5,17 @@
<div>
<ds-flex class="main-navigation-flex">
<ds-flex-item :width="{ base: LOGOS.LOGO_HEADER_WIDTH }" style="margin-right: 20px">
<nuxt-link :to="{ name: 'index' }" v-scroll-to="'.main-navigation'">
<a
v-if="LOGOS.LOGO_HEADER_CLICK.externalLink"
:href="LOGOS.LOGO_HEADER_CLICK.externalLink"
>
<logo logoType="header" />
</a>
<nuxt-link
v-else
:to="LOGOS.LOGO_HEADER_CLICK.internalPath.to"
v-scroll-to="LOGOS.LOGO_HEADER_CLICK.internalPath.scrollTo"
>
<logo logoType="header" />
</nuxt-link>
</ds-flex-item>
@ -19,24 +29,15 @@
>
<a v-if="item.url" :href="item.url" target="_blank">
<ds-text size="large" bold>
{{ item.name }}
{{ $t(item.nameIdent) }}
</ds-text>
</a>
<nuxt-link v-else :to="item.path">
<ds-text size="large" bold>
{{ item.name }}
{{ $t(item.nameIdent) }}
</ds-text>
</nuxt-link>
</ds-flex-item>
<ds-flex-item
v-if="categoriesActive && isLoggedIn"
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
style="flex-grow: 0; flex-basis: auto; margin-right: 20px"
>
<client-only>
<categories-menu></categories-menu>
</client-only>
</ds-flex-item>
<ds-flex-item
:width="{ base: '40%', sm: '40%', md: '40%', lg: '0%' }"
class="mobile-hamburger-menu"
@ -116,35 +117,33 @@
<script>
import { mapGetters } from 'vuex'
import Logo from '~/components/Logo/Logo'
import { SHOW_GROUP_BUTTON_IN_HEADER } from '~/constants/groups.js'
import headerMenu from '~/constants/headerMenu.js'
import LOGOS from '~/constants/logos.js'
import LOGOS from '../constants/logos.js'
import headerMenu from '../constants/headerMenu.js'
import seo from '~/mixins/seo'
import AvatarMenu from '~/components/AvatarMenu/AvatarMenu'
import FilterMenu from '~/components/FilterMenu/FilterMenu.vue'
import InviteButton from '~/components/InviteButton/InviteButton'
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
import Logo from '~/components/Logo/Logo'
import SearchField from '~/components/features/SearchField/SearchField.vue'
import Modal from '~/components/Modal'
import AvatarMenu from '~/components/AvatarMenu/AvatarMenu'
import CategoriesMenu from '~/components/FilterMenu/CategoriesMenu'
import FilterMenu from '~/components/FilterMenu/FilterMenu.vue'
import GroupButton from '~/components/Group/GroupButton'
import InviteButton from '~/components/InviteButton/InviteButton'
import NotificationMenu from '~/components/NotificationMenu/NotificationMenu'
import PageFooter from '~/components/PageFooter/PageFooter'
export default {
components: {
Logo,
LocaleSwitch,
SearchField,
Modal,
NotificationMenu,
AvatarMenu,
FilterMenu,
PageFooter,
InviteButton,
CategoriesMenu,
LocaleSwitch,
Logo,
Modal,
NotificationMenu,
PageFooter,
GroupButton,
SearchField,
},
mixins: [seo],
data() {

View File

@ -404,6 +404,9 @@
"regional": "Regionale Gruppe"
},
"actionRadius": "Aktionsradius",
"addMemberToGroup": "Zur Gruppe hinzufügen",
"addUser": "Benutzer hinzufügen",
"addUserPlaceholder": "eindeutiger Benutzername > @slug-from-user",
"categories": "Thema ::: Themen",
"changeMemberRole": "Die Rolle wurde auf „{role}“ geändert!",
"contentMenu": {
@ -605,7 +608,19 @@
"submitted": "Kommentar gesendet",
"updated": "Änderungen gespeichert"
},
"createNewPost": {
"forGroup": {
"title": "Für die Gruppe „{name}“"
},
"title": "Erstelle einen neuen Beitrag"
},
"edited": "bearbeitet",
"editPost": {
"forGroup": {
"title": "Für die Gruppe „{name}“"
},
"title": "Bearbeite deinen Beitrag"
},
"menu": {
"delete": "Beitrag löschen",
"edit": "Beitrag bearbeiten",
@ -618,6 +633,12 @@
"pinned": "Meldung",
"takeAction": {
"name": "Aktiv werden"
},
"viewPost": {
"forGroup": {
"title": "In der Gruppe „{name}“"
},
"title": "Beitrag"
}
},
"profile": {

View File

@ -404,6 +404,9 @@
"regional": "Regional Group"
},
"actionRadius": "Action radius",
"addMemberToGroup": "Add to group",
"addUser": "Add User",
"addUserPlaceholder": "unique username > @slug-from-user",
"categories": "Topic ::: Topics",
"changeMemberRole": "The role has been changed to “{role}”!",
"contentMenu": {
@ -605,7 +608,19 @@
"submitted": "Comment submitted!",
"updated": "Changes saved!"
},
"createNewPost": {
"forGroup": {
"title": "For The Group “{name}”"
},
"title": "Create A New Post"
},
"edited": "edited",
"editPost": {
"forGroup": {
"title": "For The Group “{name}”"
},
"title": "Edit Your Post"
},
"menu": {
"delete": "Delete post",
"edit": "Edit post",
@ -618,6 +633,12 @@
"pinned": "Announcement",
"takeAction": {
"name": "Take action"
},
"viewPost": {
"forGroup": {
"title": "In The Group “{name}”"
},
"title": "Post"
}
},
"profile": {

View File

@ -8,8 +8,8 @@ localVue.filter('date', (d) => d)
config.stubs['client-only'] = '<span><slot /></span>'
config.stubs['v-popover'] = '<span><slot /></span>'
config.stubs['nuxt-link'] = '<span><slot /></span>'
// config.stubs['infinite-loading'] = '<span><slot /></span>'
// config.stubs['follow-list'] = '<span><slot /></span>'
config.stubs['infinite-loading'] = '<span><slot /></span>'
config.stubs['follow-list'] = '<span><slot /></span>'
describe('GroupProfileSlug', () => {
let wrapper
@ -196,10 +196,11 @@ describe('GroupProfileSlug', () => {
})
describe('mount', () => {
Wrapper = () => {
Wrapper = (data = () => {}) => {
return mount(GroupProfileSlug, {
mocks,
localVue,
data,
})
}
@ -213,15 +214,16 @@ describe('GroupProfileSlug', () => {
'auth/isModerator': () => false,
},
}
wrapper = Wrapper()
wrapper.setData({
Group: [
{
...yogaPractice,
myRole: 'owner',
},
],
GroupMembers: [peterLustig, jennyRostock, bobDerBaumeister, huey],
wrapper = Wrapper(() => {
return {
Group: [
{
...yogaPractice,
myRole: 'owner',
},
],
GroupMembers: [peterLustig, jennyRostock, bobDerBaumeister, huey],
}
})
})
@ -365,15 +367,16 @@ describe('GroupProfileSlug', () => {
'auth/isModerator': () => false,
},
}
wrapper = Wrapper()
wrapper.setData({
Group: [
{
...yogaPractice,
myRole: 'usual',
},
],
GroupMembers: [peterLustig, jennyRostock, bobDerBaumeister, huey],
wrapper = Wrapper(() => {
return {
Group: [
{
...yogaPractice,
myRole: 'usual',
},
],
GroupMembers: [peterLustig, jennyRostock, bobDerBaumeister, huey],
}
})
})
@ -477,15 +480,16 @@ describe('GroupProfileSlug', () => {
'auth/isModerator': () => false,
},
}
wrapper = Wrapper()
wrapper.setData({
Group: [
{
...yogaPractice,
myRole: 'pending',
},
],
GroupMembers: [peterLustig, jennyRostock, bobDerBaumeister, huey],
wrapper = Wrapper(() => {
return {
Group: [
{
...yogaPractice,
myRole: 'pending',
},
],
GroupMembers: [peterLustig, jennyRostock, bobDerBaumeister, huey],
}
})
})
@ -589,15 +593,16 @@ describe('GroupProfileSlug', () => {
'auth/isModerator': () => false,
},
}
wrapper = Wrapper()
wrapper.setData({
Group: [
{
...yogaPractice,
myRole: null,
},
],
GroupMembers: [peterLustig, jennyRostock, bobDerBaumeister, huey],
wrapper = Wrapper(() => {
return {
Group: [
{
...yogaPractice,
myRole: null,
},
],
GroupMembers: [peterLustig, jennyRostock, bobDerBaumeister, huey],
}
})
})
@ -705,15 +710,16 @@ describe('GroupProfileSlug', () => {
'auth/isModerator': () => false,
},
}
wrapper = Wrapper()
wrapper.setData({
Group: [
{
...schoolForCitizens,
myRole: 'owner',
},
],
GroupMembers: [peterLustig, jennyRostock, bobDerBaumeister, huey],
wrapper = Wrapper(() => {
return {
Group: [
{
...schoolForCitizens,
myRole: 'owner',
},
],
GroupMembers: [peterLustig, jennyRostock, bobDerBaumeister, huey],
}
})
})
@ -821,15 +827,16 @@ describe('GroupProfileSlug', () => {
'auth/isModerator': () => false,
},
}
wrapper = Wrapper()
wrapper.setData({
Group: [
{
...schoolForCitizens,
myRole: 'usual',
},
],
GroupMembers: [peterLustig, jennyRostock, bobDerBaumeister, huey],
wrapper = Wrapper(() => {
return {
Group: [
{
...schoolForCitizens,
myRole: 'usual',
},
],
GroupMembers: [peterLustig, jennyRostock, bobDerBaumeister, huey],
}
})
})
@ -937,15 +944,16 @@ describe('GroupProfileSlug', () => {
'auth/isModerator': () => false,
},
}
wrapper = Wrapper()
wrapper.setData({
Group: [
{
...schoolForCitizens,
myRole: 'pending',
},
],
GroupMembers: [peterLustig, jennyRostock, bobDerBaumeister, huey],
wrapper = Wrapper(() => {
return {
Group: [
{
...schoolForCitizens,
myRole: 'pending',
},
],
GroupMembers: [peterLustig, jennyRostock, bobDerBaumeister, huey],
}
})
})
@ -1053,15 +1061,16 @@ describe('GroupProfileSlug', () => {
'auth/isModerator': () => false,
},
}
wrapper = Wrapper()
wrapper.setData({
Group: [
{
...schoolForCitizens,
myRole: null,
},
],
GroupMembers: [peterLustig, jennyRostock, bobDerBaumeister, huey],
wrapper = Wrapper(() => {
return {
Group: [
{
...schoolForCitizens,
myRole: null,
},
],
GroupMembers: [peterLustig, jennyRostock, bobDerBaumeister, huey],
}
})
})
@ -1173,15 +1182,16 @@ describe('GroupProfileSlug', () => {
'auth/isModerator': () => false,
},
}
wrapper = Wrapper()
wrapper.setData({
Group: [
{
...investigativeJournalism,
myRole: 'owner',
},
],
GroupMembers: [peterLustig, jennyRostock, bobDerBaumeister, huey],
wrapper = Wrapper(() => {
return {
Group: [
{
...investigativeJournalism,
myRole: 'owner',
},
],
GroupMembers: [peterLustig, jennyRostock, bobDerBaumeister, huey],
}
})
})
@ -1292,15 +1302,16 @@ describe('GroupProfileSlug', () => {
'auth/isModerator': () => false,
},
}
wrapper = Wrapper()
wrapper.setData({
Group: [
{
...investigativeJournalism,
myRole: 'usual',
},
],
GroupMembers: [peterLustig, jennyRostock, bobDerBaumeister, huey],
wrapper = Wrapper(() => {
return {
Group: [
{
...investigativeJournalism,
myRole: 'usual',
},
],
GroupMembers: [peterLustig, jennyRostock, bobDerBaumeister, huey],
}
})
})
@ -1411,15 +1422,16 @@ describe('GroupProfileSlug', () => {
'auth/isModerator': () => false,
},
}
wrapper = Wrapper()
wrapper.setData({
Group: [
{
...investigativeJournalism,
myRole: 'pending',
},
],
GroupMembers: [peterLustig, jennyRostock, bobDerBaumeister, huey],
wrapper = Wrapper(() => {
return {
Group: [
{
...investigativeJournalism,
myRole: 'pending',
},
],
GroupMembers: [peterLustig, jennyRostock, bobDerBaumeister, huey],
}
})
})
@ -1518,15 +1530,16 @@ describe('GroupProfileSlug', () => {
'auth/isModerator': () => false,
},
}
wrapper = Wrapper()
wrapper.setData({
Group: [
{
...investigativeJournalism,
myRole: null,
},
],
GroupMembers: [peterLustig, jennyRostock, bobDerBaumeister, huey],
wrapper = Wrapper(() => {
return {
Group: [
{
...investigativeJournalism,
myRole: null,
},
],
GroupMembers: [peterLustig, jennyRostock, bobDerBaumeister, huey],
}
})
})

View File

@ -250,10 +250,9 @@
</base-card>
</ds-space>
<ds-space v-if="isGroupMemberNonePending" centered>
<nuxt-link :to="{ name: 'post-create' }">
<nuxt-link :to="{ name: 'post-create', query: { groupId: group.id } }">
<base-button
class="profile-post-add-button"
:path="{ name: 'post-create' }"
icon="plus"
circle
filled
@ -297,9 +296,9 @@
</ds-grid-item>
</template>
</masonry-grid>
<!-- <client-only>
<client-only>
<infinite-loading v-if="hasMore" @infinite="showMoreContributions" />
</client-only> -->
</client-only>
</ds-flex-item>
</ds-flex>
</div>
@ -307,11 +306,11 @@
<script>
import uniqBy from 'lodash/uniqBy'
// import { profilePagePosts } from '~/graphql/PostQuery'
import { profilePagePosts } from '~/graphql/PostQuery'
import { updateGroupMutation, groupQuery, groupMembersQuery } from '~/graphql/groups'
// import { muteUser, unmuteUser } from '~/graphql/settings/MutedUsers'
// import { blockUser, unblockUser } from '~/graphql/settings/BlockedUsers'
// import UpdateQuery from '~/components/utils/UpdateQuery'
import UpdateQuery from '~/components/utils/UpdateQuery'
import postListActions from '~/mixins/postListActions'
import AvatarUploader from '~/components/Uploader/AvatarUploader'
import Category from '~/components/Category'
@ -369,17 +368,16 @@ export default {
},
data() {
// const filter = tabToFilterMapping({ tab: 'post', id: this.$route.params.id })
const filter = { group: { id: this.$route.params.id } }
return {
categoriesActive: this.$env.CATEGORIES_ACTIVE,
Group: [],
GroupMembers: [],
loadGroupMembers: false,
posts: [],
// hasMore: true,
// offset: 0,
// pageSize: 6,
hasMore: true,
offset: 0,
pageSize: 6,
// tabActive: 'post',
// filter,
filter,
// followedByCountStartValue: 0,
// followedByCount: 7,
// followingCount: 7,
@ -468,30 +466,30 @@ export default {
uniq(items, field = 'id') {
return uniqBy(items, field)
},
// showMoreContributions($state) {
// const { profilePagePosts: PostQuery } = this.$apollo.queries
// if (!PostQuery) return // seems this can be undefined on subpages
// this.offset += this.pageSize
showMoreContributions($state) {
const { profilePagePosts: PostQuery } = this.$apollo.queries
if (!PostQuery) return // seems this can be undefined on subpages
this.offset += this.pageSize
// PostQuery.fetchMore({
// variables: {
// offset: this.offset,
// filter: this.filter,
// first: this.pageSize,
// orderBy: 'createdAt_desc',
// },
// updateQuery: UpdateQuery(this, { $state, pageKey: 'profilePagePosts' }),
// })
// },
// resetPostList() {
// this.offset = 0
// this.posts = []
// this.hasMore = true
// },
// refetchPostList() {
// this.resetPostList()
// this.$apollo.queries.profilePagePosts.refetch()
// },
PostQuery.fetchMore({
variables: {
offset: this.offset,
filter: this.filter,
first: this.pageSize,
orderBy: 'createdAt_desc',
},
updateQuery: UpdateQuery(this, { $state, pageKey: 'profilePagePosts' }),
})
},
resetPostList() {
this.offset = 0
this.posts = []
this.hasMore = true
},
refetchPostList() {
this.resetPostList()
this.$apollo.queries.profilePagePosts.refetch()
},
// async muteUser(user) {
// try {
// await this.$apollo.mutate({ mutation: muteUser(), variables: { id: user.id } })
@ -578,23 +576,23 @@ export default {
},
},
apollo: {
// profilePagePosts: {
// query() {
// return profilePagePosts(this.$i18n)
// },
// variables() {
// return {
// filter: this.filter,
// first: this.pageSize,
// offset: 0,
// orderBy: 'createdAt_desc',
// }
// },
// update({ profilePagePosts }) {
// this.posts = profilePagePosts
// },
// fetchPolicy: 'cache-and-network',
// },
profilePagePosts: {
query() {
return profilePagePosts(this.$i18n)
},
variables() {
return {
filter: this.filter,
first: this.pageSize,
offset: 0,
orderBy: 'createdAt_desc',
}
},
update({ profilePagePosts }) {
this.posts = profilePagePosts
},
fetchPolicy: 'cache-and-network',
},
Group: {
query() {
return groupQuery(this.$i18n)

View File

@ -3,7 +3,11 @@
<base-card>
<ds-heading tag="h3">{{ $t('group.members') }}</ds-heading>
<ds-space margin="large" />
<group-member :groupId="group.id" :groupMembers="groupMembers" />
<group-member
:groupId="group.id"
:groupMembers="groupMembers"
@loadGroupMembers="loadGroupMembers"
/>
</base-card>
</div>
</template>
@ -44,5 +48,10 @@ export default {
fetchPolicy: 'cache-and-network',
},
},
methods: {
loadGroupMembers() {
this.$apollo.queries.GroupMembers.refetch()
},
},
}
</script>

View File

@ -8,7 +8,7 @@
<ds-grid-item v-if="showDonations" class="top-info-bar" :row-span="1" column-span="fullWidth">
<donation-info :goal="goal" :progress="progress" />
</ds-grid-item>
<!-- newsfeed -->
<!-- news feed -->
<template v-if="hasResults">
<masonry-grid-item
v-for="post in posts"

View File

@ -1,16 +1,7 @@
<template>
<div>
<ds-flex gutter="small">
<ds-flex-item :width="{ base: '100%', sm: 2, md: 2, lg: 1 }">
<transition name="slide-up" appear>
<nuxt-child />
</transition>
</ds-flex-item>
<ds-flex-item :width="{ base: '200px' }">
<ds-menu :routes="routes" class="post-side-navigation" />
</ds-flex-item>
</ds-flex>
</div>
<transition name="slide-up" appear>
<nuxt-child />
</transition>
</template>
<script>
@ -41,40 +32,5 @@ const persistentLinks = PersistentLinks(options)
export default {
mixins: [persistentLinks],
computed: {
routes() {
const { slug, id } = this.$route.params
return [
{
name: this.$t('common.post', null, 1),
path: `/post/${id}/${slug}`,
children: [
{
name: this.$t('common.comment', null, 2),
path: `/post/${id}/${slug}#comments`,
},
// TODO implement
/* {
name: this.$t('common.letsTalk'),
path: `/post/${id}/${slug}#lets-talk`
}, */
// TODO implement
/* {
name: this.$t('common.versus'),
path: `/post/${id}/${slug}#versus`
} */
],
},
]
},
},
}
</script>
<style lang="scss">
.post-side-navigation {
position: sticky;
top: 65px;
z-index: 2;
}
</style>

View File

@ -56,6 +56,10 @@ describe('PostSlug', () => {
},
$route: {
hash: '',
params: {
slug: 'slug',
id: 'id',
},
},
// If you are mocking the router, then don't use VueRouter with localVue: https://vue-test-utils.vuejs.org/guides/using-with-vue-router.html
$router: {

View File

@ -1,107 +1,129 @@
<template>
<transition name="fade" appear>
<base-card
v-if="post && ready"
:lang="post.language"
:class="{
'post-page': true,
'disabled-content': post.disabled,
'--blur-image': blurred,
}"
:style="heroImageStyle"
>
<template #heroImage v-if="post.image">
<img :src="post.image | proxyApiUrl" class="image" />
<aside v-show="post.image && post.image.sensitive" class="blur-toggle">
<img v-show="blurred" :src="post.image | proxyApiUrl" class="preview" />
<base-button
:icon="blurred ? 'eye' : 'eye-slash'"
filled
circle
@click="blurred = !blurred"
/>
</aside>
</template>
<section class="menu">
<user-teaser :user="post.author" :date-time="post.createdAt">
<template #dateTime>
<ds-text v-if="post.createdAt !== post.updatedAt">({{ $t('post.edited') }})</ds-text>
</template>
</user-teaser>
<client-only>
<content-menu
placement="bottom-end"
resource-type="contribution"
:resource="post"
:modalsData="menuModalsData"
:is-owner="isAuthor"
@pinPost="pinPost"
@unpinPost="unpinPost"
/>
</client-only>
</section>
<ds-space margin-bottom="small" />
<h2 class="title hyphenate-text">{{ post.title }}</h2>
<ds-space margin-bottom="small" />
<content-viewer class="content hyphenate-text" :content="post.content" />
<!-- Categories -->
<div v-if="categoriesActive" class="categories">
<ds-space margin="xx-large" />
<ds-space margin="xx-small" />
<hc-category
v-for="category in post.categories"
:key="category.id"
:icon="category.icon"
:name="$t(`contribution.category.name.${category.slug}`)"
v-tooltip="{
content: $t(`contribution.category.description.${category.slug}`),
placement: 'bottom-start',
}"
/>
</div>
<ds-space margin-bottom="small" />
<!-- Tags -->
<div v-if="post.tags && post.tags.length" class="tags">
<ds-space margin="xx-small" />
<hc-hashtag v-for="tag in sortedTags" :key="tag.id" :id="tag.id" />
</div>
<ds-space margin-top="small">
<ds-flex :gutter="{ lg: 'small' }">
<!-- Shout Button -->
<ds-flex-item
:width="{ lg: '15%', md: '22%', sm: '22%', base: '100%' }"
class="shout-button"
>
<hc-shout-button
v-if="post.author"
:disabled="isAuthor"
:count="post.shoutedCount"
:is-shouted="post.shoutedByCurrentUser"
:post-id="post.id"
/>
</ds-flex-item>
</ds-flex>
<div>
<ds-space margin="small">
<ds-heading tag="h1">{{ $t('post.viewPost.title') }}</ds-heading>
<ds-heading v-if="post && post.group" tag="h2">
{{ $t('post.viewPost.forGroup.title', { name: post.group.name }) }}
</ds-heading>
</ds-space>
<!-- Comments -->
<ds-section>
<comment-list :post="post" @toggleNewCommentForm="toggleNewCommentForm" @reply="reply" />
<ds-space margin-bottom="large" />
<comment-form
v-if="showNewCommentForm && !isBlocked"
ref="commentForm"
:post="post"
@createComment="createComment"
/>
<ds-placeholder v-if="isBlocked">
{{ $t('settings.blocked-users.explanation.commenting-disabled') }}
<br />
{{ $t('settings.blocked-users.explanation.commenting-explanation') }}
<page-params-link :pageParams="links.FAQ">
{{ $t('site.faq') }}
</page-params-link>
</ds-placeholder>
</ds-section>
</base-card>
<ds-space margin="large" />
<ds-flex gutter="small">
<ds-flex-item :width="{ base: '100%', sm: 2, md: 2, lg: 1 }">
<base-card
v-if="post && ready"
:lang="post.language"
:class="{
'post-page': true,
'disabled-content': post.disabled,
'--blur-image': blurred,
}"
:style="heroImageStyle"
>
<template #heroImage v-if="post.image">
<img :src="post.image | proxyApiUrl" class="image" />
<aside v-show="post.image && post.image.sensitive" class="blur-toggle">
<img v-show="blurred" :src="post.image | proxyApiUrl" class="preview" />
<base-button
:icon="blurred ? 'eye' : 'eye-slash'"
filled
circle
@click="blurred = !blurred"
/>
</aside>
</template>
<section class="menu">
<user-teaser :user="post.author" :date-time="post.createdAt">
<template #dateTime>
<ds-text v-if="post.createdAt !== post.updatedAt">
({{ $t('post.edited') }})
</ds-text>
</template>
</user-teaser>
<client-only>
<content-menu
placement="bottom-end"
resource-type="contribution"
:resource="post"
:modalsData="menuModalsData"
:is-owner="isAuthor"
@pinPost="pinPost"
@unpinPost="unpinPost"
/>
</client-only>
</section>
<ds-space margin-bottom="small" />
<h2 class="title hyphenate-text">{{ post.title }}</h2>
<ds-space margin-bottom="small" />
<content-viewer class="content hyphenate-text" :content="post.content" />
<!-- Categories -->
<div v-if="categoriesActive" class="categories">
<ds-space margin="xx-large" />
<ds-space margin="xx-small" />
<hc-category
v-for="category in post.categories"
:key="category.id"
:icon="category.icon"
:name="$t(`contribution.category.name.${category.slug}`)"
v-tooltip="{
content: $t(`contribution.category.description.${category.slug}`),
placement: 'bottom-start',
}"
/>
</div>
<ds-space margin-bottom="small" />
<!-- Tags -->
<div v-if="post.tags && post.tags.length" class="tags">
<ds-space margin="xx-small" />
<hc-hashtag v-for="tag in sortedTags" :key="tag.id" :id="tag.id" />
</div>
<ds-space margin-top="small">
<ds-flex :gutter="{ lg: 'small' }">
<!-- Shout Button -->
<ds-flex-item
:width="{ lg: '15%', md: '22%', sm: '22%', base: '100%' }"
class="shout-button"
>
<hc-shout-button
v-if="post.author"
:disabled="isAuthor"
:count="post.shoutedCount"
:is-shouted="post.shoutedByCurrentUser"
:post-id="post.id"
/>
</ds-flex-item>
</ds-flex>
</ds-space>
<!-- Comments -->
<ds-section>
<comment-list
:post="post"
@toggleNewCommentForm="toggleNewCommentForm"
@reply="reply"
/>
<ds-space margin-bottom="large" />
<comment-form
v-if="showNewCommentForm && !isBlocked"
ref="commentForm"
:post="post"
@createComment="createComment"
/>
<ds-placeholder v-if="isBlocked">
{{ $t('settings.blocked-users.explanation.commenting-disabled') }}
<br />
{{ $t('settings.blocked-users.explanation.commenting-explanation') }}
<page-params-link :pageParams="links.FAQ">
{{ $t('site.faq') }}
</page-params-link>
</ds-placeholder>
</ds-section>
</base-card>
</ds-flex-item>
<ds-flex-item :width="{ base: '200px' }">
<ds-menu :routes="routes" class="post-side-navigation" />
</ds-flex-item>
</ds-flex>
</div>
</transition>
</template>
@ -167,6 +189,31 @@ export default {
}, 50)
},
computed: {
routes() {
const { slug, id } = this.$route.params
return [
{
name: this.$t('common.post', null, 1),
path: `/post/${id}/${slug}`,
children: [
{
name: this.$t('common.comment', null, 2),
path: `/post/${id}/${slug}#comments`,
},
// TODO implement
/* {
name: this.$t('common.letsTalk'),
path: `/post/${id}/${slug}#lets-talk`
}, */
// TODO implement
/* {
name: this.$t('common.versus'),
path: `/post/${id}/${slug}#versus`
} */
],
},
]
},
menuModalsData() {
return postMenuModalsData(
// "this.post" may not always be defined at the beginning
@ -266,6 +313,11 @@ export default {
}
</script>
<style lang="scss">
.post-side-navigation {
position: sticky;
top: 65px;
z-index: 2;
}
.post-page {
> .hero-image {
position: relative;

View File

@ -11,6 +11,11 @@ describe('create.vue', () => {
$env: {
CATEGORIES_ACTIVE: false,
},
$route: {
query: {
groupId: null,
},
},
}
describe('mount', () => {

View File

@ -1,18 +1,60 @@
<template>
<ds-flex :width="{ base: '100%' }" gutter="base">
<ds-flex-item :width="{ base: '100%', md: 5 }">
<hc-contribution-form />
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', md: 1 }">&nbsp;</ds-flex-item>
</ds-flex>
<div>
<ds-space margin="small">
<ds-heading tag="h1">{{ $t('post.createNewPost.title') }}</ds-heading>
<ds-heading v-if="group" tag="h2">
{{ $t('post.createNewPost.forGroup.title', { name: group.name }) }}
</ds-heading>
</ds-space>
<ds-space margin="large" />
<ds-flex :width="{ base: '100%' }" gutter="base">
<ds-flex-item :width="{ base: '100%', md: 5 }">
<contribution-form :groupId="groupId" />
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', md: 1 }">&nbsp;</ds-flex-item>
</ds-flex>
</div>
</template>
<script>
import HcContributionForm from '~/components/ContributionForm/ContributionForm'
import { groupQuery } from '~/graphql/groups'
import ContributionForm from '~/components/ContributionForm/ContributionForm'
export default {
components: {
HcContributionForm,
ContributionForm,
},
data() {
const { groupId = null } = this.$route.query
return {
groupId,
}
},
computed: {
group() {
return this.Group && this.Group[0] ? this.Group[0] : null
},
},
apollo: {
Group: {
query() {
return groupQuery(this.$i18n)
},
variables() {
return {
id: this.groupId,
// followedByCount: this.followedByCount,
// followingCount: this.followingCount,
}
},
skip() {
return !this.groupId
},
error(error) {
this.$toast.error(error.message)
},
fetchPolicy: 'cache-and-network',
},
},
}
</script>

View File

@ -1,20 +1,29 @@
<template>
<ds-flex :width="{ base: '100%' }" gutter="base">
<ds-flex-item :width="{ base: '100%', md: 3 }">
<hc-contribution-form :contribution="contribution" />
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', md: 1 }">&nbsp;</ds-flex-item>
</ds-flex>
<div>
<ds-space margin="small">
<ds-heading tag="h1">{{ $t('post.editPost.title') }}</ds-heading>
<ds-heading v-if="contribution && contribution.group" tag="h2">
{{ $t('post.editPost.forGroup.title', { name: contribution.group.name }) }}
</ds-heading>
</ds-space>
<ds-space margin="large" />
<ds-flex :width="{ base: '100%' }" gutter="base">
<ds-flex-item :width="{ base: '100%', md: 3 }">
<contribution-form :contribution="contribution" />
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', md: 1 }">&nbsp;</ds-flex-item>
</ds-flex>
</div>
</template>
<script>
import HcContributionForm from '~/components/ContributionForm/ContributionForm'
import ContributionForm from '~/components/ContributionForm/ContributionForm'
import PostQuery from '~/graphql/PostQuery'
import { mapGetters } from 'vuex'
export default {
components: {
HcContributionForm,
ContributionForm,
},
computed: {
...mapGetters({