Merge branch 'Make-Header-Menu-To-Component' of github.com:Ocelot-Social-Community/Ocelot-Social into 5545-Footer-And-Header-Links-Configurable

This commit is contained in:
Wolfgang Huß 2022-10-28 09:14:08 +02:00
commit 6c45b1f3a1
17 changed files with 277 additions and 81 deletions

View File

@ -65,11 +65,18 @@ Change into the new folder.
$ cd Ocelot-Social
```
## Live Demo And Developer Logins
**Try out our deployed [development environment](https://stage.ocelot.social).**
Visit our staging networks:
* central staging network: [stage.ocelot.social](https://stage.ocelot.social)
<!-- - rebranded staging network: [rebrand.ocelot.social](https://stage.ocelot.social). -->
### Login
<!-- Try out our deployed [development environment](https://develop.human-connection.org/). -->
Logins in the browser after the following installations:
Logins for the live demos and developers (local developers after the following installations) in the browser:
| email | password | role |
| :--- | :--- | :--- |

View File

@ -8,7 +8,8 @@ export default {
return resolve(root, args, context, info)
},
UpdateGroup: async (resolve, root, args, context, info) => {
args.descriptionExcerpt = trunc(args.description, DESCRIPTION_EXCERPT_HTML_LENGTH).html
if (args.description)
args.descriptionExcerpt = trunc(args.description, DESCRIPTION_EXCERPT_HTML_LENGTH).html
return resolve(root, args, context, info)
},
CreatePost: async (resolve, root, args, context, info) => {

View File

@ -249,6 +249,40 @@ const isMemberOfGroup = rule({
}
})
const canCommentPost = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
if (!(user && user.id)) return false
const { postId } = args
const userId = user.id
const session = driver.session()
const readTxPromise = session.readTransaction(async (transaction) => {
const transactionResponse = await transaction.run(
`
MATCH (post:Post { id: $postId })
OPTIONAL MATCH (post)-[:IN]->(group:Group)
OPTIONAL MATCH (user:User { id: $userId })-[membership:MEMBER_OF]->(group)
RETURN group AS group, membership AS membership
`,
{ postId, userId },
)
return {
group: transactionResponse.records.map((record) => record.get('group'))[0],
membership: transactionResponse.records.map((record) => record.get('membership'))[0],
}
})
try {
const { group, membership } = await readTxPromise
return (
!group || (membership && ['usual', 'admin', 'owner'].includes(membership.properties.role))
)
} catch (error) {
throw new Error(error)
} finally {
session.close()
}
})
const isAuthor = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
@ -361,7 +395,7 @@ export default shield(
unshout: isAuthenticated,
changePassword: isAuthenticated,
review: isModerator,
CreateComment: isAuthenticated,
CreateComment: and(isAuthenticated, canCommentPost),
UpdateComment: isAuthor,
DeleteComment: isAuthor,
DeleteUser: or(isDeletingOwnAccount, isAdmin),

View File

@ -30,18 +30,12 @@ export default {
args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Group')))
return resolve(root, args, context, info)
},
UpdateGroup: async (resolve, root, args, context, info) => {
if (args.name) {
args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Group')))
}
return resolve(root, args, context, info)
},
CreatePost: async (resolve, root, args, context, info) => {
args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post')))
return resolve(root, args, context, info)
},
UpdatePost: async (resolve, root, args, context, info) => {
// TODO: is this absolutely correct, see condition in 'UpdateGroup' above? may it works accidentally, because args.slug is always send?
// TODO: is this absolutely correct? what happens if "args.title" is not defined? may it works accidentally, because "args.title" or "args.slug" is always send?
args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post')))
return resolve(root, args, context, info)
},

View File

@ -213,33 +213,6 @@ describe('slugifyMiddleware', () => {
describe('if group exists', () => {
describe('if new slug not(!) exists', () => {
describe('setting slug by group name', () => {
it('has the new slug', async () => {
await expect(
mutate({
mutation: updateGroupMutation(),
variables: {
id: createGroupResult.data.CreateGroup.id,
name: 'My Best Group',
},
}),
).resolves.toMatchObject({
data: {
UpdateGroup: {
name: 'My Best Group',
slug: 'my-best-group',
about: 'Some about',
description: 'Some description' + descriptionAdditional100,
groupType: 'closed',
actionRadius: 'national',
myRole: 'owner',
},
},
errors: undefined,
})
})
})
describe('setting slug explicitly', () => {
it('has the new slug', async () => {
await expect(
@ -284,33 +257,6 @@ describe('slugifyMiddleware', () => {
})
})
describe('setting slug by group name', () => {
it('has unique slug "*-1"', async () => {
await expect(
mutate({
mutation: updateGroupMutation(),
variables: {
id: createGroupResult.data.CreateGroup.id,
name: 'Pre-Existing Group',
},
}),
).resolves.toMatchObject({
data: {
UpdateGroup: {
name: 'Pre-Existing Group',
slug: 'pre-existing-group-1',
about: 'Some about',
description: 'Some description' + descriptionAdditional100,
groupType: 'closed',
actionRadius: 'national',
myRole: 'owner',
},
},
errors: undefined,
})
})
})
describe('setting slug explicitly', () => {
it('rejects UpdateGroup', async (done) => {
try {

View File

@ -2714,7 +2714,7 @@ describe('in mode', () => {
UpdateGroup: {
id: 'my-group',
name: 'The New Group For Our Country',
slug: 'the-new-group-for-our-country', // changing the slug is tested in the slugifyMiddleware
slug: 'the-best-group', // changing the slug is tested in the slugifyMiddleware
about: 'We will change the land!',
description: 'Some country relevant description' + descriptionAdditional100,
descriptionExcerpt:

View File

@ -14,6 +14,7 @@ import {
profilePagePosts,
searchPosts,
} from '../../db/graphql/posts'
import { createCommentMutation } from '../../db/graphql/comments'
// eslint-disable-next-line no-unused-vars
import { DESCRIPTION_WITHOUT_HTML_LENGTH_MIN } from '../../constants/groups'
import CONFIG from '../../config'
@ -378,6 +379,170 @@ describe('Posts in Groups', () => {
})
})
describe('commenting posts in groups', () => {
describe('without membership of group', () => {
beforeAll(async () => {
authenticatedUser = await anyUser.toJson()
})
it('throws an error for public groups', async () => {
await expect(
mutate({
mutation: createCommentMutation,
variables: {
postId: 'post-to-public-group',
content:
'I am commenting a post in a public group without being a member of the group',
},
}),
).resolves.toMatchObject({
errors: expect.arrayContaining([expect.objectContaining({ message: 'Not Authorized!' })]),
})
})
it('throws an error for closed groups', async () => {
await expect(
mutate({
mutation: createCommentMutation,
variables: {
postId: 'post-to-closed-group',
content:
'I am commenting a post in a closed group without being a member of the group',
},
}),
).resolves.toMatchObject({
errors: expect.arrayContaining([expect.objectContaining({ message: 'Not Authorized!' })]),
})
})
it('throws an error for hidden groups', async () => {
await expect(
mutate({
mutation: createCommentMutation,
variables: {
postId: 'post-to-hidden-group',
content:
'I am commenting a post in a hidden group without being a member of the group',
},
}),
).resolves.toMatchObject({
errors: expect.arrayContaining([expect.objectContaining({ message: 'Not Authorized!' })]),
})
})
})
describe('as a pending member of group', () => {
beforeAll(async () => {
authenticatedUser = await pendingUser.toJson()
})
it('throws an error for public groups', async () => {
await expect(
mutate({
mutation: createCommentMutation,
variables: {
postId: 'post-to-public-group',
content: 'I am commenting a post in a public group as a pending member of the group',
},
}),
).resolves.toMatchObject({
errors: expect.arrayContaining([expect.objectContaining({ message: 'Not Authorized!' })]),
})
})
it('throws an error for closed groups', async () => {
await expect(
mutate({
mutation: createCommentMutation,
variables: {
postId: 'post-to-closed-group',
content: 'I am commenting a post in a closed group as a pending member of the group',
},
}),
).resolves.toMatchObject({
errors: expect.arrayContaining([expect.objectContaining({ message: 'Not Authorized!' })]),
})
})
it('throws an error for hidden groups', async () => {
await expect(
mutate({
mutation: createCommentMutation,
variables: {
postId: 'post-to-hidden-group',
content: 'I am commenting a post in a hidden group as a pending member of the group',
},
}),
).resolves.toMatchObject({
errors: expect.arrayContaining([expect.objectContaining({ message: 'Not Authorized!' })]),
})
})
})
describe('as a member of group', () => {
beforeAll(async () => {
authenticatedUser = await allGroupsUser.toJson()
})
it('comments a post in a public group', async () => {
await expect(
mutate({
mutation: createCommentMutation,
variables: {
postId: 'post-to-public-group',
content: 'I am commenting a post in a public group as a member of the group',
},
}),
).resolves.toMatchObject({
data: {
CreateComment: {
id: expect.any(String),
},
},
errors: undefined,
})
})
it('comments a post in a closed group', async () => {
await expect(
mutate({
mutation: createCommentMutation,
variables: {
postId: 'post-to-closed-group',
content: 'I am commenting a post in a closed group as a member of the group',
},
}),
).resolves.toMatchObject({
data: {
CreateComment: {
id: expect.any(String),
},
},
errors: undefined,
})
})
it('comments a post in a hidden group', async () => {
await expect(
mutate({
mutation: createCommentMutation,
variables: {
postId: 'post-to-hidden-group',
content: 'I am commenting a post in a hidden group as a member of the group',
},
}),
).resolves.toMatchObject({
data: {
CreateComment: {
id: expect.any(String),
},
},
errors: undefined,
})
})
})
})
describe('visibility of posts', () => {
describe('query post by ID', () => {
describe('without authentication', () => {

View File

@ -333,10 +333,10 @@ export default {
this.$refs.groupForm.update('description', value)
},
submit() {
const { name, about, description, groupType, actionRadius, /* locationName, */ categoryIds } =
this.formData
const { name, slug, about, description, groupType, actionRadius, categoryIds } = this.formData
const variables = {
name,
slug,
about,
description,
groupType,

View File

@ -7,7 +7,13 @@
<ds-flex-item :width="{ base: LOGOS.LOGO_HEADER_WIDTH }" style="margin-right: 20px">
<a
v-if="LOGOS.LOGO_HEADER_CLICK.externalLink"
:href="LOGOS.LOGO_HEADER_CLICK.externalLink"
:href="LOGOS.LOGO_HEADER_CLICK.externalLink.url"
:target="
LOGOS.LOGO_HEADER_CLICK.externalLink.target ||
LOGOS.LOGO_HEADER_CLICK.externalLink.target === ''
? LOGOS.LOGO_HEADER_CLICK.externalLink.target
: '_blank'
"
>
<logo logoType="header" />
</a>
@ -91,7 +97,23 @@
<!-- logo, hamburger-->
<ds-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.url"
:target="
LOGOS.LOGO_HEADER_CLICK.externalLink.target ||
LOGOS.LOGO_HEADER_CLICK.externalLink.target === ''
? LOGOS.LOGO_HEADER_CLICK.externalLink.target
: '_blank'
"
>
<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>

View File

@ -2,7 +2,10 @@
<div class="progress-bar-component">
<div class="progress-bar">
<div class="progress-bar__goal"></div>
<div class="progress-bar__progress" :style="progressBarWidth"></div>
<div
:class="['progress-bar__progress', progressBarColorClass]"
:style="progressBarWidth"
></div>
<div class="progress-bar__border" style="width: 100%">
<span v-if="label" class="progress-bar__label">{{ label }}</span>
</div>
@ -14,6 +17,8 @@
</template>
<script>
import { PROGRESS_BAR_COLOR_TYPE } from '~/constants/donation.js'
export default {
props: {
goal: {
@ -32,6 +37,11 @@ export default {
progressBarWidth() {
return `width: ${(this.progress / this.goal) * 100}%;`
},
progressBarColorClass() {
return PROGRESS_BAR_COLOR_TYPE === 'gradient'
? 'color-repeating-linear-gradient'
: 'color-uni'
},
},
}
</script>
@ -66,15 +76,22 @@ export default {
left: 0px;
height: 26px; // styleguide-button-size
max-width: 100%;
background: repeating-linear-gradient(
120deg,
$color-primary 0px,
$color-primary 30px,
$color-primary-light 50px,
$color-primary-light 75px,
$color-primary 95px
);
border-radius: $border-radius-base;
&.color-uni {
background: $color-primary-light;
}
&.color-repeating-linear-gradient {
background: repeating-linear-gradient(
120deg,
$color-primary 0px,
$color-primary 30px,
$color-primary-light 50px,
$color-primary-light 75px,
$color-primary 95px
);
}
}
.progress-bar__border {

View File

@ -6,6 +6,7 @@
<ds-text align="center" bold color="success">
{{ $t('components.registration.create-user-account.success') }}
</ds-text>
<ds-space margin="xxx-small" />
</div>
<div v-else-if="response === 'error'">
<transition name="ds-transition-fade">
@ -21,6 +22,7 @@
<ds-space centered>
<nuxt-link to="/login">{{ $t('site.back-to-login') }}</nuxt-link>
</ds-space>
<ds-space margin="xxx-small" />
</div>
<div v-else class="create-account-card">
<ds-form
@ -112,6 +114,7 @@
</label>
</ds-text>
</template>
<ds-space margin="xxx-small" />
</ds-form>
</div>
</template>

View File

@ -24,6 +24,7 @@
{{ $t('components.registration.email.form.sendEmailAgain') }}
</label>
</ds-text>
<ds-space margin="xxx-small" />
</ds-form>
</template>

View File

@ -17,6 +17,7 @@
{{ $t('components.registration.invite-code.form.description') }}
</ds-text>
<slot></slot>
<ds-space margin="xxx-small" />
</ds-form>
</template>

View File

@ -2,6 +2,7 @@
<ds-space centered>
<hc-empty icon="events" :message="$t('components.registration.signup.unavailable')" />
<slot></slot>
<ds-space margin="xxx-small" />
</ds-space>
</template>

View File

@ -19,6 +19,7 @@
{{ $t('components.registration.email-nonce.form.description') }}
</ds-text>
<slot></slot>
<ds-space margin="xxx-small" />
</ds-form>
</template>

View File

@ -0,0 +1 @@
export const PROGRESS_BAR_COLOR_TYPE = 'gradient' // 'uni' is the other option

View File

@ -93,7 +93,9 @@ export const updateGroupMutation = () => {
name
icon
}
# avatar # test this as result
avatar {
url
}
locationName
myRole
}