Merge branch 'master' into 6014-feature-webapp-join-leave-button-on-group-page-is-misleading

This commit is contained in:
Wolfgang Huß 2023-03-15 12:48:17 +01:00 committed by GitHub
commit 3a2e18d794
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 448 additions and 135 deletions

View File

@ -329,19 +329,16 @@ jobs:
- name: backend | docker-compose - name: backend | docker-compose
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp neo4j backend run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp neo4j backend
- name: cypress | Fullstack tests - name: cypress | Fullstack tests
id: e2e-tests
run: | run: |
yarn install yarn install
yarn run cypress:run --spec $(cypress/parallel-features.sh ${{ matrix.job }} ${{ env.jobs }} ) yarn run cypress:run --spec $(cypress/parallel-features.sh ${{ matrix.job }} ${{ env.jobs }} )
########################################################################## ##########################################################################
# UPLOAD SCREENSHOTS & VIDEO ############################################# # UPLOAD SCREENSHOTS - IF TESTS FAIL #####################################
########################################################################## ##########################################################################
- name: Upload Artifact - name: Full stack tests | if any test failed, upload screenshots
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: cypress-screenshots name: cypress-screenshots
path: cypress/screenshots/ path: cypress/screenshots/
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: cypress-videos
path: cypress/videos/

View File

@ -197,7 +197,8 @@ Prepare database once before you start by running the following command in a sec
```bash ```bash
# in main folder while docker-compose is up # in main folder while docker-compose is up
$ docker-compose exec backend yarn run db:migrate init $ docker compose exec backend yarn run db:migrate init
$ docker compose exec backend yarn run db:migrate up
``` ```
Then clear and seed database by running the following command as well in the second terminal: Then clear and seed database by running the following command as well in the second terminal:

View File

@ -81,8 +81,7 @@ More details about our GraphQL playground and how to use it with ocelot.social c
### Database Indexes and Constraints ### Database Indexes and Constraints
Database indexes and constraints need to be created when the database and the Database indexes and constraints need to be created and upgraded when the database and the backend are running:
backend is running:
{% tabs %} {% tabs %}
{% tab title="Docker" %} {% tab title="Docker" %}
@ -98,6 +97,11 @@ $ docker compose exec backend yarn prod:migrate init
$ docker compose exec backend /bin/sh -c "yarn prod:migrate init" $ docker compose exec backend /bin/sh -c "yarn prod:migrate init"
``` ```
```bash
# in main folder with docker compose running
$ docker exec backend yarn run db:migrate up
```
{% endtab %} {% endtab %}
{% tab title="Without Docker" %} {% tab title="Without Docker" %}
@ -107,6 +111,11 @@ $ docker compose exec backend /bin/sh -c "yarn prod:migrate init"
yarn run db:migrate init yarn run db:migrate init
``` ```
```bash
# in backend/ with database running (In docker or local)
yarn run db:migrate up
```
{% endtab %} {% endtab %}
{% endtabs %} {% endtabs %}
@ -134,6 +143,8 @@ $ docker exec backend yarn run db:reset
$ docker-compose down -v $ docker-compose down -v
# if container is not running, run this command to set up your database indexes and constraints # if container is not running, run this command to set up your database indexes and constraints
$ docker exec backend yarn run db:migrate init $ docker exec backend yarn run db:migrate init
# And then upgrade the indexes and const
$ docker exec backend yarn run db:migrate up
``` ```
{% endtab %} {% endtab %}

View File

@ -23,6 +23,7 @@ export const cleanDatabase = async (options = {}) => {
return transaction.run( return transaction.run(
` `
MATCH (everything) MATCH (everything)
WHERE NOT 'Migration' IN labels(everything)
DETACH DELETE everything DETACH DELETE everything
`, `,
) )

View File

@ -16,6 +16,7 @@ export default {
Group: async (_object, params, context, _resolveInfo) => { Group: async (_object, params, context, _resolveInfo) => {
const { isMember, id, slug, first, offset } = params const { isMember, id, slug, first, offset } = params
let pagination = '' let pagination = ''
const orderBy = 'ORDER BY group.createdAt DESC'
if (first !== undefined && offset !== undefined) pagination = `SKIP ${offset} LIMIT ${first}` if (first !== undefined && offset !== undefined) pagination = `SKIP ${offset} LIMIT ${first}`
const matchParams = { id, slug } const matchParams = { id, slug }
removeUndefinedNullValuesFromObject(matchParams) removeUndefinedNullValuesFromObject(matchParams)
@ -29,6 +30,7 @@ export default {
WITH group, membership WITH group, membership
WHERE (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner']) WHERE (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner'])
RETURN group {.*, myRole: membership.role} RETURN group {.*, myRole: membership.role}
${orderBy}
${pagination} ${pagination}
` `
} else { } else {
@ -39,6 +41,7 @@ export default {
WITH group WITH group
WHERE group.groupType IN ['public', 'closed'] WHERE group.groupType IN ['public', 'closed']
RETURN group {.*, myRole: NULL} RETURN group {.*, myRole: NULL}
${orderBy}
${pagination} ${pagination}
` `
} else { } else {
@ -48,6 +51,7 @@ export default {
WITH group, membership WITH group, membership
WHERE (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner']) WHERE (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner'])
RETURN group {.*, myRole: membership.role} RETURN group {.*, myRole: membership.role}
${orderBy}
${pagination} ${pagination}
` `
} }

View File

@ -4,6 +4,7 @@
"ignoreTestFiles": "*.js", "ignoreTestFiles": "*.js",
"chromeWebSecurity": false, "chromeWebSecurity": false,
"baseUrl": "http://localhost:3000", "baseUrl": "http://localhost:3000",
"video":false,
"retries": { "retries": {
"runMode": 2, "runMode": 2,
"openMode": 0 "openMode": 0

View File

@ -0,0 +1,33 @@
# Docker
## Apple M1 Platform
***Attention:** For using Docker commands in Apple M1 environments!*
```bash
# set env variable for your shell
$ export DOCKER_DEFAULT_PLATFORM=linux/amd64
```
For even more informations, see [Docker More Closely](#docker-more-closely)
### Docker Compose Override File For Apple M1 Platform
For Docker compose `up` or `build` commands, you can use our Apple M1 override file that specifies the M1 platform:
```bash
# in main folder
# for production
$ docker compose -f docker-compose.yml -f docker-compose.apple-m1.override.yml up
# for production testing Docker images from DockerHub
$ docker compose -f docker-compose.ocelotsocial-branded.yml -f docker-compose.apple-m1.override.yml up
# only once: init admin user and create indexes and constraints in Neo4j database
$ docker compose exec backend /bin/sh -c "yarn prod:migrate init"
```
## Docker More Closely In Main Code
To get more informations about the Apple M1 platform and to analyze the Docker builds etc. you find our documentation in our main code, [here](https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/master/DOCKER_MORE_CLOSELY.md).

View File

@ -0,0 +1,12 @@
#!/bin/bash
# base setup
SCRIPT_PATH=$(realpath $0)
SCRIPT_DIR=$(dirname $SCRIPT_PATH)
# configuration
CONFIGURATION=${CONFIGURATION:-"example"}
KUBECONFIG=${KUBECONFIG:-${SCRIPT_DIR}/../configurations/${CONFIGURATION}/kubeconfig.yaml}
# clean & seed
kubectl --kubeconfig=${KUBECONFIG} -n default exec -it $(kubectl --kubeconfig=${KUBECONFIG} -n default get pods | grep ocelot-backend | awk '{ print $1 }') -- /bin/sh -c "node --experimental-repl-await dist/db/clean.js && node --experimental-repl-await dist/db/seed.js"

View File

@ -0,0 +1,14 @@
#!/bin/bash
# generate a secret and store it in the SECRET file.
# Note that this overwrites the existing file
# base setup
SCRIPT_PATH=$(realpath $0)
SCRIPT_DIR=$(dirname $SCRIPT_PATH)
# configuration
CONFIGURATION=${CONFIGURATION:-"example"}
SECRET_FILE=${SCRIPT_DIR}/../configurations/${CONFIGURATION}/SECRET
openssl rand -base64 32 > ${SECRET_FILE}

View File

@ -0,0 +1,44 @@
#!/bin/bash
# decrypt secrets in the selected configuration
# Note that existing decrypted files will be overwritten
# base setup
SCRIPT_PATH=$(realpath $0)
SCRIPT_DIR=$(dirname $SCRIPT_PATH)
# configuration
CONFIGURATION=${CONFIGURATION:-"example"}
SECRET=${SECRET}
SECRET_FILE=${SCRIPT_DIR}/../configurations/${CONFIGURATION}/SECRET
FILES=(\
"${SCRIPT_DIR}/../configurations/${CONFIGURATION}/.env" \
"${SCRIPT_DIR}/../configurations/${CONFIGURATION}/kubeconfig.yaml" \
"${SCRIPT_DIR}/../configurations/${CONFIGURATION}/kubernetes/values.yaml" \
"${SCRIPT_DIR}/../configurations/${CONFIGURATION}/kubernetes/dns.values.yaml" \
)
# Load SECRET from file if it is not set explicitly
if [ -z ${SECRET} ] && [ -f "${SECRET_FILE}" ]; then
SECRET=$(<${SECRET_FILE})
fi
# exit when there is no SECRET set
if [ -z ${SECRET} ]; then
echo "No SECRET provided and no SECRET-File found."
exit 1
fi
# decrypt
for file in "${FILES[@]}"
do
if [ -f "${file}.enc" ]; then
#gpg --symmetric --batch --passphrase="${SECRET}" --cipher-algo AES256 --output ${file}.enc ${file}
gpg --quiet --batch --yes --decrypt --passphrase="${SECRET}" --output ${file} ${file}.enc
echo "Decrypted ${file}"
fi
done
echo "DONE"
# gpg --quiet --batch --yes --decrypt --passphrase="${SECRET}" \
# --output $HOME/secrets/my_secret.json my_secret.json.gpg

View File

@ -0,0 +1,41 @@
#!/bin/bash
# encrypt secrets in the selected configuration
# Note that existing encrypted files will be overwritten
# base setup
SCRIPT_PATH=$(realpath $0)
SCRIPT_DIR=$(dirname $SCRIPT_PATH)
# configuration
CONFIGURATION=${CONFIGURATION:-"example"}
SECRET=${SECRET}
SECRET_FILE=${SCRIPT_DIR}/../configurations/${CONFIGURATION}/SECRET
FILES=(\
"${SCRIPT_DIR}/../configurations/${CONFIGURATION}/.env" \
"${SCRIPT_DIR}/../configurations/${CONFIGURATION}/kubeconfig.yaml" \
"${SCRIPT_DIR}/../configurations/${CONFIGURATION}/kubernetes/values.yaml" \
"${SCRIPT_DIR}/../configurations/${CONFIGURATION}/kubernetes/dns.values.yaml" \
)
# Load SECRET from file if it is not set explicitly
if [ -z ${SECRET} ] && [ -f "${SECRET_FILE}" ]; then
SECRET=$(<${SECRET_FILE})
fi
# exit when there is no SECRET set
if [ -z ${SECRET} ]; then
echo "No SECRET provided and no SECRET-File found."
exit 1
fi
# encrypt
for file in "${FILES[@]}"
do
if [ -f "${file}" ]; then
gpg --symmetric --batch --yes --passphrase="${SECRET}" --cipher-algo AES256 --output ${file}.enc ${file}
echo "Encrypted ${file}"
fi
done
echo "DONE"

View File

@ -1,3 +0,0 @@
/dns.values.yaml
/nginx.values.yaml
/values.yaml

View File

@ -44,7 +44,7 @@ for development, spin up a
[hosted Neo4j Sandbox instance](https://neo4j.com/download/), run Neo4j in one [hosted Neo4j Sandbox instance](https://neo4j.com/download/), run Neo4j in one
of the [many cloud options](https://neo4j.com/developer/guide-cloud-deployment/), of the [many cloud options](https://neo4j.com/developer/guide-cloud-deployment/),
[spin up Neo4j in a Docker container](https://neo4j.com/developer/docker/), [spin up Neo4j in a Docker container](https://neo4j.com/developer/docker/),
on Archlinux you can install [neo4j-community from AUR](https://aur.archlinux.org/packages/neo4j-community/) on Arch linux you can install [neo4j-community from AUR](https://aur.archlinux.org/packages/neo4j-community/)
or on Debian-based systems install [Neo4j from the Debian Repository](http://debian.neo4j.org/). or on Debian-based systems install [Neo4j from the Debian Repository](http://debian.neo4j.org/).
Just be sure to update the Neo4j connection string and credentials accordingly Just be sure to update the Neo4j connection string and credentials accordingly
in `backend/.env`. in `backend/.env`.

View File

@ -30,6 +30,7 @@ export default {
} }
} }
.filterActive { .filterActive {
background-color: $color-success-active; color: $color-primary-inverse;
background-color: $color-primary-active;
} }
</style> </style>

View File

@ -0,0 +1,57 @@
<template>
<span>
<base-button
class="my-filter-button my-filter-button-selected"
right
@click="clickButton"
filled
>
{{ title }}
</base-button>
<base-button
class="filter-remove"
@click="clickRemove"
icon="close"
:title="titleRemove"
size="small"
circle
filled
/>
</span>
</template>
<script>
export default {
name: 'HeaderButton',
props: {
title: {
type: String,
required: true,
},
clickButton: {
type: Function,
required: true,
},
titleRemove: {
type: String,
required: true,
},
clickRemove: {
type: Function,
required: true,
},
},
}
</script>
<style lang="scss">
.my-filter-button-selected {
padding-right: 30px;
margin-top: 4px;
}
.base-button.filter-remove {
position: relative;
margin-left: -31px;
top: -5px;
margin-right: 8px;
}
</style>

View File

@ -30,7 +30,7 @@ export default {
/* dirty fix to override broken styleguide inline-styles */ /* dirty fix to override broken styleguide inline-styles */
.ds-grid { .ds-grid {
grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)) !important; grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)) !important;
gap: 16px !important; gap: 32px 16px !important;
grid-auto-rows: 20px; grid-auto-rows: 20px;
} }

View File

@ -59,8 +59,8 @@ describe('NotificationsTable.vue', () => {
wrapper = Wrapper() wrapper = Wrapper()
}) })
it('renders a table', () => { it('renders a grid table', () => {
expect(wrapper.find('.ds-table').exists()).toBe(true) expect(wrapper.find('.notification-grid').exists()).toBe(true)
}) })
describe('renders 4 columns', () => { describe('renders 4 columns', () => {
@ -84,7 +84,7 @@ describe('NotificationsTable.vue', () => {
describe('Post', () => { describe('Post', () => {
let firstRowNotification let firstRowNotification
beforeEach(() => { beforeEach(() => {
firstRowNotification = wrapper.findAll('tbody tr').at(0) firstRowNotification = wrapper.findAll('.notification-grid-row').at(0)
}) })
it('renders the author', () => { it('renders the author', () => {
@ -117,7 +117,7 @@ describe('NotificationsTable.vue', () => {
describe('Comment', () => { describe('Comment', () => {
let secondRowNotification let secondRowNotification
beforeEach(() => { beforeEach(() => {
secondRowNotification = wrapper.findAll('tbody tr').at(1) secondRowNotification = wrapper.findAll('.notification-grid-row').at(1)
}) })
it('renders the author', () => { it('renders the author', () => {

View File

@ -1,62 +1,108 @@
<template> <template>
<ds-table v-if="notifications && notifications.length" :data="notifications" :fields="fields"> <div class="notification-grid" v-if="notifications && notifications.length">
<template #icon="scope"> <ds-grid>
<base-icon <ds-grid-item v-if="!isMobile" column-span="fullWidth">
v-if="scope.row.from.post" <ds-grid class="header-grid">
name="comment" <ds-grid-item v-for="field in fields" :key="field.label" class="ds-table-head-col">
v-tooltip="{ content: $t('notifications.comment'), placement: 'right' }" {{ field.label }}
/> </ds-grid-item>
<base-icon </ds-grid>
v-else </ds-grid-item>
name="bookmark" <ds-grid-item
v-tooltip="{ content: $t('notifications.post'), placement: 'right' }" v-for="notification in notifications"
/> :key="notification.id"
</template> column-span="fullWidth"
<template #user="scope"> class="notification-grid-row"
<ds-space margin-bottom="base">
<client-only>
<user-teaser
:user="scope.row.from.author"
:date-time="scope.row.from.createdAt"
:class="{ 'notification-status': scope.row.read }"
/>
</client-only>
</ds-space>
<ds-text :class="{ 'notification-status': scope.row.read, reason: true }">
{{ $t(`notifications.reason.${scope.row.reason}`) }}
</ds-text>
</template>
<template #post="scope">
<nuxt-link
class="notification-mention-post"
:class="{ 'notification-status': scope.row.read }"
:to="{
name: 'post-id-slug',
params: params(scope.row.from),
hash: hashParam(scope.row.from),
}"
@click.native="markNotificationAsRead(scope.row.from.id)"
> >
<b>{{ scope.row.from.title || scope.row.from.post.title | truncate(50) }}</b> <ds-grid>
</nuxt-link> <ds-grid-item>
</template> <ds-flex class="user-section">
<template #content="scope"> <ds-flex-item :width="{ base: '20%' }">
<b :class="{ 'notification-status': scope.row.read }"> <div>
{{ scope.row.from.contentExcerpt | removeHtml }} <base-card :wide-content="true">
</b> <base-icon
</template> v-if="notification.from.post"
</ds-table> name="comment"
v-tooltip="{ content: $t('notifications.comment'), placement: 'right' }"
/>
<base-icon
v-else
name="bookmark"
v-tooltip="{ content: $t('notifications.post'), placement: 'right' }"
/>
</base-card>
</div>
</ds-flex-item>
<ds-flex-item>
<div>
<base-card :wide-content="true">
<ds-space margin-bottom="base">
<client-only>
<user-teaser
:user="notification.from.author"
:date-time="notification.from.createdAt"
:class="{ 'notification-status': notification.read }"
/>
</client-only>
</ds-space>
<ds-text :class="{ 'notification-status': notification.read, reason: true }">
{{ $t(`notifications.reason.${notification.reason}`) }}
</ds-text>
</base-card>
</div>
</ds-flex-item>
</ds-flex>
</ds-grid-item>
<ds-grid-item>
<ds-flex class="content-section" :direction="{ base: 'column', xs: 'row' }">
<ds-flex-item>
<base-card :wide-content="true">
<nuxt-link
class="notification-mention-post"
:class="{ 'notification-status': notification.read }"
:to="{
name: 'post-id-slug',
params: params(notification.from),
hash: hashParam(notification.from),
}"
@click.native="markNotificationAsRead(notification.from.id)"
>
<b>
{{ notification.from.title || notification.from.post.title | truncate(50) }}
</b>
</nuxt-link>
</base-card>
</ds-flex-item>
<ds-flex-item>
<base-card :wide-content="true">
<b :class="{ 'notification-status': notification.read }">
{{ notification.from.contentExcerpt | removeHtml }}
</b>
</base-card>
</ds-flex-item>
</ds-flex>
</ds-grid-item>
</ds-grid>
</ds-grid-item>
</ds-grid>
</div>
<hc-empty v-else icon="alert" :message="$t('notifications.empty')" /> <hc-empty v-else icon="alert" :message="$t('notifications.empty')" />
</template> </template>
<script> <script>
import UserTeaser from '~/components/UserTeaser/UserTeaser' import UserTeaser from '~/components/UserTeaser/UserTeaser'
import HcEmpty from '~/components/Empty/Empty' import HcEmpty from '~/components/Empty/Empty'
import BaseCard from '../_new/generic/BaseCard/BaseCard.vue'
import mobile from '~/mixins/mobile'
const maxMobileWidth = 768 // at this point the table breaks down
export default { export default {
components: { components: {
UserTeaser, UserTeaser,
HcEmpty, HcEmpty,
BaseCard,
}, },
mixins: [mobile(maxMobileWidth)],
props: { props: {
notifications: { type: Array, default: () => [] }, notifications: { type: Array, default: () => [] },
}, },
@ -106,4 +152,39 @@ export default {
.notification-status { .notification-status {
opacity: $opacity-soft; opacity: $opacity-soft;
} }
/* fix to override flex-wrap style of ds flex component */
.notification-grid .content-section {
flex-wrap: nowrap;
}
.notification-grid .ds-grid.header-grid {
grid-template-columns: 1fr 4fr 3fr 3fr !important;
}
.notification-grid-row {
border-top: 1px dotted #e5e3e8;
}
.notification-grid .base-card {
border-radius: 0;
box-shadow: none;
padding: 16px 4px;
}
/* dirty fix to override broken styleguide inline-styles */
.notification-grid .ds-grid {
grid-template-columns: 5fr 6fr !important;
grid-auto-rows: auto !important;
grid-template-rows: 1fr;
gap: 0px !important;
}
@media screen and (max-width: 768px) {
.notification-grid .ds-grid {
grid-template-columns: 1fr !important;
}
.notification-grid .content-section {
border-top: 1px dotted #e5e3e8;
}
.notification-grid-row {
box-shadow: 0px 12px 26px -4px rgb(0 0 0 / 10%);
margin-top: 5px;
border-top: none;
}
}
</style> </style>

View File

@ -15,7 +15,13 @@
<img :src="post.image | proxyApiUrl" class="image" /> <img :src="post.image | proxyApiUrl" class="image" />
</template> </template>
<client-only> <client-only>
<user-teaser :user="post.author" :group="post.group" :date-time="post.createdAt" /> <div class="post-user-row">
<user-teaser :user="post.author" :group="post.group" :date-time="post.createdAt" />
<hc-ribbon
:class="[isPinned ? '--pinned' : '', post.image ? 'post-ribbon-w-img' : 'post-ribbon']"
:text="isPinned ? $t('post.pinned') : $t('post.name')"
/>
</div>
</client-only> </client-only>
<h2 class="title hyphenate-text">{{ post.title }}</h2> <h2 class="title hyphenate-text">{{ post.title }}</h2>
<!-- TODO: replace editor content with tiptap render view --> <!-- TODO: replace editor content with tiptap render view -->
@ -26,7 +32,7 @@
v-observe-visibility="(isVisible, entry) => visibilityChanged(isVisible, entry, post.id)" v-observe-visibility="(isVisible, entry) => visibilityChanged(isVisible, entry, post.id)"
> >
<div class="categories" v-if="categoriesActive"> <div class="categories" v-if="categoriesActive">
<hc-category <category
v-for="category in post.categories" v-for="category in post.categories"
:key="category.id" :key="category.id"
v-tooltip="{ v-tooltip="{
@ -73,19 +79,15 @@
</client-only> </client-only>
</footer> </footer>
</base-card> </base-card>
<hc-ribbon
:class="{ '--pinned': isPinned }"
:text="isPinned ? $t('post.pinned') : $t('post.name')"
/>
</nuxt-link> </nuxt-link>
</template> </template>
<script> <script>
import UserTeaser from '~/components/UserTeaser/UserTeaser' import Category from '~/components/Category'
import ContentMenu from '~/components/ContentMenu/ContentMenu' import ContentMenu from '~/components/ContentMenu/ContentMenu'
import HcRibbon from '~/components/Ribbon'
import HcCategory from '~/components/Category'
import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon' import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon'
import HcRibbon from '~/components/Ribbon'
import UserTeaser from '~/components/UserTeaser/UserTeaser'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import PostMutations from '~/graphql/PostMutations' import PostMutations from '~/graphql/PostMutations'
import { postMenuModalsData, deletePostMutation } from '~/components/utils/PostHelpers' import { postMenuModalsData, deletePostMutation } from '~/components/utils/PostHelpers'
@ -93,11 +95,11 @@ import { postMenuModalsData, deletePostMutation } from '~/components/utils/PostH
export default { export default {
name: 'PostTeaser', name: 'PostTeaser',
components: { components: {
UserTeaser, Category,
HcCategory,
HcRibbon,
ContentMenu, ContentMenu,
CounterIcon, CounterIcon,
HcRibbon,
UserTeaser,
}, },
props: { props: {
post: { post: {
@ -192,19 +194,38 @@ export default {
display: block; display: block;
height: 100%; height: 100%;
color: $text-color-base; color: $text-color-base;
}
> .ribbon { .post-user-row {
position: relative;
> .post-ribbon-w-img {
position: absolute; position: absolute;
top: 50%; // 14px (~height of ribbon element) + 24px(=margin of hero image)
right: -7px; top: -38px;
// 7px+24px(=padding of parent)
right: -31px;
}
> .post-ribbon {
position: absolute;
// 14px (~height of ribbon element) + 24px(=margin of hero image)
top: -24px;
// 7px(=offset)+24px(=margin of parent)
right: -31px;
} }
} }
.post-teaser > .base-card { .post-teaser > .base-card {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: visible;
height: 100%; height: 100%;
> .hero-image {
border-top-left-radius: 5px;
border-top-right-radius: 5px;
}
&.--blur-image > .hero-image > .image { &.--blur-image > .hero-image > .image {
filter: blur($blur-radius); filter: blur($blur-radius);
} }

View File

@ -74,7 +74,9 @@ describe('FollowList.vue', () => {
expect(wrapper.vm.allConnectionsCount).toBe(user.followingCount) expect(wrapper.vm.allConnectionsCount).toBe(user.followingCount)
expect(wrapper.findAll('.user-teaser')).toHaveLength(user.following.length) expect(wrapper.findAll('.user-teaser')).toHaveLength(user.following.length)
expect(wrapper.emitted('fetchAllConnections')).toEqual([['following']]) expect(wrapper.emitted('fetchAllConnections')).toEqual([
['following', user.followingCount],
])
}) })
}) })
@ -85,7 +87,9 @@ describe('FollowList.vue', () => {
expect(wrapper.vm.allConnectionsCount).toBe(user.followedByCount) expect(wrapper.vm.allConnectionsCount).toBe(user.followedByCount)
expect(wrapper.findAll('.user-teaser')).toHaveLength(user.followedBy.length) expect(wrapper.findAll('.user-teaser')).toHaveLength(user.followedBy.length)
expect(wrapper.emitted('fetchAllConnections')).toEqual([['followedBy']]) expect(wrapper.emitted('fetchAllConnections')).toEqual([
['followedBy', user.followedByCount],
])
}) })
}) })
}) })

View File

@ -6,7 +6,7 @@
:allProfilesCount="allConnectionsCount" :allProfilesCount="allConnectionsCount"
:profiles="connections" :profiles="connections"
:loading="loading" :loading="loading"
@fetchAllProfiles="$emit('fetchAllConnections', type)" @fetchAllProfiles="$emit('fetchAllConnections', type, allConnectionsCount)"
/> />
</template> </template>

View File

@ -15,7 +15,7 @@ export default {
}, },
{ {
name: 'viewport', name: 'viewport',
content: 'width=device-width, initial-scale=1', content: 'initial-scale=1',
}, },
{ {
hid: 'description', hid: 'description',

View File

@ -62,7 +62,7 @@ export default {
}, },
{ {
name: 'viewport', name: 'viewport',
content: 'width=device-width, initial-scale=1', content: 'initial-scale=1',
}, },
{ {
hid: 'description', hid: 'description',

View File

@ -134,15 +134,15 @@ describe('PostIndex', () => {
}) })
describe('donation-info', () => { describe('donation-info', () => {
it('shows donation-info on default', () => { it('hides donation-info on default', () => {
wrapper = Wrapper() wrapper = Wrapper()
expect(wrapper.find('.top-info-bar').exists()).toBe(true) expect(wrapper.find('.top-info-bar').exists()).toBe(false)
}) })
it('hides donation-info if not "showDonations"', async () => { it('shows donation-info if "showDonations"', async () => {
wrapper = Wrapper() wrapper = Wrapper()
await wrapper.setData({ showDonations: false }) await wrapper.setData({ showDonations: true })
expect(wrapper.find('.top-info-bar').exists()).toBe(false) expect(wrapper.find('.top-info-bar').exists()).toBe(true)
}) })
}) })
}) })

View File

@ -43,47 +43,30 @@
&nbsp; &nbsp;
<base-icon class="my-filter-button" :name="filterButtonIcon"></base-icon> <base-icon class="my-filter-button" :name="filterButtonIcon"></base-icon>
</base-button> </base-button>
<span v-if="postsFilter['categories_some']">
<base-button class="my-filter-button" right @click="showFilter = !showFilter" filled>
{{ $t('contribution.filterMasonryGrid.myTopics') }}
</base-button>
<base-button
class="filter-remove"
@click="resetCategories"
icon="close"
:title="$t('filter-menu.deleteFilter')"
style="margin-left: -8px"
filled
/>
</span>
<span v-if="postsFilter['author']">
<base-button class="my-filter-button" right @click="showFilter = !showFilter" filled>
{{ $t('contribution.filterMasonryGrid.myFriends') }}
</base-button>
<base-button
class="filter-remove"
@click="resetByFollowed"
icon="close"
:title="$t('filter-menu.deleteFilter')"
style="margin-left: -8px"
filled
/>
</span>
<span v-if="postsFilter['postsInMyGroups']"> <header-button
<base-button class="my-filter-button" right @click="showFilter = !showFilter" filled> v-if="postsFilter['categories_some']"
{{ $t('contribution.filterMasonryGrid.myGroups') }} :title="$t('contribution.filterMasonryGrid.myTopics')"
</base-button> :clickButton="openFilterMenu"
<base-button :titleRemove="$t('filter-menu.deleteFilter')"
class="filter-remove" :clickRemove="resetCategories"
@click="resetByGroups" />
icon="close"
:title="$t('filter-menu.deleteFilter')"
style="margin-left: -8px"
filled
/>
</span>
<header-button
v-if="postsFilter['author']"
:title="$t('contribution.filterMasonryGrid.myFriends')"
:clickButton="openFilterMenu"
:titleRemove="$t('filter-menu.deleteFilter')"
:clickRemove="resetByFollowed"
/>
<header-button
v-if="postsFilter['postsInMyGroups']"
:title="$t('contribution.filterMasonryGrid.myGroups')"
:clickButton="openFilterMenu"
:titleRemove="$t('filter-menu.deleteFilter')"
:clickRemove="resetByGroups"
/>
<div id="my-filter" v-if="showFilter"> <div id="my-filter" v-if="showFilter">
<div @mouseleave="showFilter = false"> <div @mouseleave="showFilter = false">
<filter-menu-component @showFilterMenu="showFilterMenu" /> <filter-menu-component @showFilterMenu="showFilterMenu" />
@ -142,6 +125,7 @@ import HcEmpty from '~/components/Empty/Empty'
import PostTeaser from '~/components/PostTeaser/PostTeaser.vue' import PostTeaser from '~/components/PostTeaser/PostTeaser.vue'
import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue' import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue'
import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue' import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
import HeaderButton from '~/components/FilterMenu/HeaderButton'
import { mapGetters, mapMutations } from 'vuex' import { mapGetters, mapMutations } from 'vuex'
import { DonationsQuery } from '~/graphql/Donations' import { DonationsQuery } from '~/graphql/Donations'
import { filterPosts } from '~/graphql/PostQuery.js' import { filterPosts } from '~/graphql/PostQuery.js'
@ -159,6 +143,7 @@ export default {
MasonryGrid, MasonryGrid,
MasonryGridItem, MasonryGridItem,
FilterMenuComponent, FilterMenuComponent,
HeaderButton,
}, },
mixins: [postListActions], mixins: [postListActions],
data() { data() {
@ -167,7 +152,7 @@ export default {
hideByScroll: false, hideByScroll: false,
revScrollpos: 0, revScrollpos: 0,
showFilter: false, showFilter: false,
showDonations: true, showDonations: false,
goal: 15000, goal: 15000,
progress: 7000, progress: 7000,
posts: [], posts: [],
@ -225,6 +210,9 @@ export default {
resetCategories: 'posts/RESET_CATEGORIES', resetCategories: 'posts/RESET_CATEGORIES',
toggleCategory: 'posts/TOGGLE_CATEGORY', toggleCategory: 'posts/TOGGLE_CATEGORY',
}), }),
openFilterMenu() {
this.showFilter = !this.showFilter
},
showFilterMenu(e) { showFilterMenu(e) {
if (!e || (!e.target.closest('#my-filter') && !e.target.closest('.my-filter-button'))) { if (!e || (!e.target.closest('#my-filter') && !e.target.closest('.my-filter-button'))) {
if (!this.showFilter) return if (!this.showFilter) return
@ -354,13 +342,18 @@ export default {
align-items: center; align-items: center;
} }
.filterButtonMenu { .filterButtonMenu {
width: 95%;
position: fixed; position: fixed;
z-index: 6; z-index: 6;
margin-top: -35px; margin-top: -35px;
padding: 20px 10px 5px 10px; padding: 20px 10px 5px 10px;
border-radius: 7px;
background-color: #f5f4f6; background-color: #f5f4f6;
} }
@media screen and (max-width: 656px) {
.filterButtonMenu {
margin-top: -50px;
}
}
#my-filter { #my-filter {
background-color: white; background-color: white;
box-shadow: rgb(189 189 189) 1px 9px 15px 1px; box-shadow: rgb(189 189 189) 1px 9px 15px 1px;

View File

@ -384,9 +384,9 @@ export default {
this.user.followedByCurrentUser = followedByCurrentUser this.user.followedByCurrentUser = followedByCurrentUser
this.user.followedBy = followedBy this.user.followedBy = followedBy
}, },
fetchAllConnections(type) { fetchAllConnections(type, count) {
if (type === 'following') this.followingCount = Infinity if (type === 'following') this.followingCount = count
if (type === 'followedBy') this.followedByCount = Infinity if (type === 'followedBy') this.followedByCount = count
}, },
}, },
apollo: { apollo: {