mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2026-01-20 20:01:25 +00:00
Merge branch 'master' of github.com:Ocelot-Social-Community/Ocelot-Social into brand-reformer-network-first-step
This commit is contained in:
commit
5084620a2d
49
CHANGELOG.md
49
CHANGELOG.md
@ -4,8 +4,31 @@ All notable changes to this project will be documented in this file. Dates are d
|
||||
|
||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
|
||||
#### [3.6.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.5.3...3.6.0)
|
||||
|
||||
- Show invititation dropdown until user clicks somewhere else [`#8539`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8539)
|
||||
- feat(webapp): redirect to group after registration with invite to group [`#8540`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8540)
|
||||
- fix(webapp): fix layout break and hidden group name appearance [`#8538`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8538)
|
||||
- feat(webapp): several group and personal invitation links [`#8504`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8504)
|
||||
- fix(backend): category seed [`#8536`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8536)
|
||||
- correct copy path in dockerfile [`#8519`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8519)
|
||||
- feat(webapp): group invite after login [`#8518`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8518)
|
||||
- feat(webapp): redirect on registration for invite links [`#8517`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8517)
|
||||
- fix(webapp): mobile optimization [`#8516`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8516)
|
||||
- feat(docu): update email snapshots [`#8514`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8514)
|
||||
- fix(backend): fix user profile and group links in e-mails [`#8512`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8512)
|
||||
- fix(backend): fix registration with invite code [`#8513`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8513)
|
||||
- fix locales errors (german) [`#8510`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8510)
|
||||
- fix(backend): invite codes - hotfix 1 [`#8508`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8508)
|
||||
- refactor(backend): category seed [`#8505`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8505)
|
||||
- feat(backend): group invite codes [`#8499`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8499)
|
||||
- feat(webapp): change german to `du` and `dich` [`#8507`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8507)
|
||||
|
||||
#### [3.5.3](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.5.2...3.5.3)
|
||||
|
||||
> 7 May 2025
|
||||
|
||||
- chore(release): v3.5.3 [`#8503`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8503)
|
||||
- fix(backend): correct email from [`#8501`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8501)
|
||||
- refactor(backend): types for global config [`#8485`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8485)
|
||||
- fix warning in workflow for lower case as [`#8494`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8494)
|
||||
@ -1438,31 +1461,15 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
|
||||
- updated CHANGELOG.md [`9d9075f`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9d9075f2117b2eb4b607e7d59ab18c7e655c6ea7)
|
||||
|
||||
#### [0.6.4](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/v0.6.4...0.6.4)
|
||||
#### [0.6.4](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.3...0.6.4)
|
||||
|
||||
> 8 February 2021
|
||||
|
||||
- - adjusted changelog to ocelot-social repo [`9603882`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9603882edebf8967e05abfa94e4e1ebf452d4e24)
|
||||
- - first steps towards docker image deployment & github autotagging [`5503216`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/5503216ad4a0230ac533042e4a69806590fc2a5a)
|
||||
- - deploy structure image [`a60400b`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/a60400b4fe6f59bbb80e1073db4def3ba205e1a7)
|
||||
- regenerated `CHANGELOG.md` [`ee688ec`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/ee688ece24cf592b3989e83340701ca8772e876e)
|
||||
- fetch full history [`5ecee4d`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/5ecee4d73a92d2e5c5ae971d79848ed27f65a72c)
|
||||
- don't fail if tag exists (release) [`39c82fc`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/39c82fcb37d5c8e7e78a79288e1ef6280f8d0892)
|
||||
|
||||
#### [v0.6.4](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.3...v0.6.4)
|
||||
|
||||
> 9 February 2021
|
||||
|
||||
- chore(release): 0.6.4 [`8b7570d`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/8b7570dc35d0ea431f673a711ac051f1e1320acb)
|
||||
- change user roles is working, test fails [`8c3310a`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/8c3310abaf87c0e5597fec4f93fb37d27122c9e7)
|
||||
- change user role: tests are working [`f10da4b`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/f10da4b09388fe1e2b85abd53f6ffc67c785d4c1)
|
||||
|
||||
#### [0.6.3](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/v0.6.3...0.6.3)
|
||||
|
||||
> 8 February 2021
|
||||
|
||||
- - adjusted changelog to ocelot-social repo [`9603882`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9603882edebf8967e05abfa94e4e1ebf452d4e24)
|
||||
- - fixed changelog [`cf70b12`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/cf70b12ed74011924ea788ab932fc9d7ac0e6bd9)
|
||||
- - yarn install to allow yarn auto-changelog [`fc496aa`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/fc496aa04cb7e804da4335da0cb5cda26f874ea2)
|
||||
|
||||
#### [v0.6.3](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.0...v0.6.3)
|
||||
#### [0.6.3](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.0...0.6.3)
|
||||
|
||||
> 8 February 2021
|
||||
|
||||
|
||||
@ -21,9 +21,9 @@ CMD ["/bin/sh", "-c", "yarn install && yarn run dev"]
|
||||
FROM base AS build
|
||||
COPY . .
|
||||
ONBUILD COPY ./branding/constants/ src/config/tmp
|
||||
ONBUILD RUN tools/replace-constants.sh
|
||||
# copy categories to brand them (use yarn prod:db:data:categories)
|
||||
ONBUILD COPY branding/constants/ src/constants/
|
||||
ONBUILD COPY ./branding/constants/ src/constants/
|
||||
ONBUILD RUN tools/replace-constants.sh
|
||||
ONBUILD COPY ./branding/email/ src/middleware/helpers/email/
|
||||
ONBUILD COPY ./branding/middlewares/ src/middleware/branding/
|
||||
ONBUILD COPY ./branding/data/ src/db/data
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ocelot-social-backend",
|
||||
"version": "3.5.3",
|
||||
"version": "3.6.0",
|
||||
"description": "GraphQL Backend for ocelot.social",
|
||||
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
|
||||
"author": "ocelot.social Community",
|
||||
|
||||
@ -12,10 +12,21 @@ const createCategories = async () => {
|
||||
query: 'MATCH (category:Category) RETURN category { .* }',
|
||||
})
|
||||
|
||||
const existingCategories = result.records.map((r) => r.get('category'))
|
||||
const existingCategoryIds = existingCategories.map((c) => c.id)
|
||||
const categoryIds = categories.map((c) => c.id)
|
||||
const categorySlugs = categories.map((c) => c.slug)
|
||||
await write({
|
||||
query: `MATCH (category:Category)
|
||||
WHERE NOT category.id IN $categoryIds
|
||||
DETACH DELETE category`,
|
||||
variables: {
|
||||
categoryIds,
|
||||
categorySlugs,
|
||||
},
|
||||
})
|
||||
|
||||
const newCategories = categories.filter((c) => !existingCategoryIds.includes(c.id))
|
||||
const existingCategories = result.records.map((r) => r.get('category'))
|
||||
|
||||
const newCategories = categories.filter((c) => !existingCategories.some((cat) => c.id === cat.id))
|
||||
|
||||
await write({
|
||||
query: `UNWIND $newCategories AS map
|
||||
@ -27,15 +38,6 @@ const createCategories = async () => {
|
||||
},
|
||||
})
|
||||
|
||||
const categoryIds = categories.map((c) => c.id)
|
||||
await write({
|
||||
query: `MATCH (category:Category)
|
||||
WHERE NOT category.id IN $categoryIds
|
||||
DETACH DELETE category`,
|
||||
variables: {
|
||||
categoryIds,
|
||||
},
|
||||
})
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Successfully created categories!')
|
||||
await driver.close()
|
||||
|
||||
@ -308,12 +308,11 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
await dagobert.relateTo(louie, 'blocked')
|
||||
|
||||
// categories
|
||||
let i = 0
|
||||
for (const category of categories) {
|
||||
await Factory.build('category', {
|
||||
id: `cat${i++}`,
|
||||
slug: category.name,
|
||||
naem: category.name,
|
||||
id: category.id,
|
||||
slug: category.slug,
|
||||
name: category.name,
|
||||
icon: category.icon,
|
||||
})
|
||||
}
|
||||
|
||||
@ -514,6 +514,7 @@ export default shield(
|
||||
},
|
||||
Group: {
|
||||
'*': isAuthenticated, // TODO - only those who are allowed to see the group
|
||||
slug: allow,
|
||||
avatar: allow,
|
||||
name: allow,
|
||||
about: allow,
|
||||
|
||||
@ -4,4 +4,5 @@
|
||||
[ -f src/config/tmp/emails.js ] && mv src/config/tmp/emails.js src/config/emails.ts
|
||||
[ -f src/config/tmp/logos.js ] && mv src/config/tmp/logos.js src/config/logos.ts
|
||||
[ -f src/config/tmp/metadata.js ] && mv src/config/tmp/metadata.js src/config/metadata.ts
|
||||
[ -f src/constants/categories.js ] && mv src/constants/categories.js src/constants/categories.ts
|
||||
exit 0
|
||||
|
||||
@ -21,4 +21,4 @@ version: 0.1.0
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "3.5.3"
|
||||
appVersion: "3.6.0"
|
||||
|
||||
@ -21,4 +21,4 @@ version: 0.1.0
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "3.5.3"
|
||||
appVersion: "3.6.0"
|
||||
|
||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ocelot-social-frontend",
|
||||
"version": "3.5.3",
|
||||
"version": "3.6.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ocelot-social-frontend",
|
||||
"version": "3.5.3",
|
||||
"version": "3.6.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@intlify/unplugin-vue-i18n": "^2.0.0",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ocelot-social-frontend",
|
||||
"version": "3.5.3",
|
||||
"version": "3.6.0",
|
||||
"description": "ocelot.social new Frontend (in development and not fully implemented) by IT4C Boilerplate for frontends",
|
||||
"main": "build/index.js",
|
||||
"type": "module",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ocelot-social",
|
||||
"version": "3.5.3",
|
||||
"version": "3.6.0",
|
||||
"description": "Free and open source software program code available to run social networks.",
|
||||
"author": "ocelot.social Community",
|
||||
"license": "MIT",
|
||||
|
||||
@ -6,3 +6,6 @@ MAPBOX_TOKEN="pk.eyJ1IjoiYnVzZmFrdG9yIiwiYSI6ImNraDNiM3JxcDBhaWQydG1uczhpZWtpOW4
|
||||
PUBLIC_REGISTRATION=false
|
||||
INVITE_REGISTRATION=true
|
||||
CATEGORIES_ACTIVE=false
|
||||
BADGES_ENABLED=true
|
||||
INVITE_LINK_LIMIT=7
|
||||
NETWORK_NAME="Ocelot.social"
|
||||
|
||||
@ -142,6 +142,12 @@ hr {
|
||||
}
|
||||
}
|
||||
|
||||
body.dropdown-open {
|
||||
max-height: 100vh;
|
||||
overflow: hidden;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.base-card > .ds-section {
|
||||
padding: 0;
|
||||
margin: -$space-base;
|
||||
|
||||
@ -89,6 +89,11 @@ export default {
|
||||
path: `/groups/edit/${this.group.id}`,
|
||||
icon: 'edit',
|
||||
})
|
||||
routes.push({
|
||||
label: this.$t('group.contentMenu.inviteLinks'),
|
||||
path: `/groups/edit/${this.group.id}/invites`,
|
||||
icon: 'link',
|
||||
})
|
||||
}
|
||||
|
||||
return routes
|
||||
|
||||
@ -88,6 +88,27 @@ exports[`GroupContentMenu renders as groupProfile when I am the owner 1`] = `
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
href="/groups/edit/groupid/invites"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
|
||||
group.contentMenu.inviteLinks
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@ -29,11 +29,11 @@ export default {
|
||||
placement: { type: String, default: 'bottom-end' },
|
||||
disabled: { type: Boolean, default: false },
|
||||
offset: { type: [String, Number], default: '16' },
|
||||
noMouseLeaveClosing: { type: Boolean, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isPopoverOpen: false,
|
||||
developerNoAutoClosing: false, // stops automatic closing of menu for developer purposes: default is 'false'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -94,8 +94,7 @@ export default {
|
||||
}
|
||||
},
|
||||
popoverMouseLeave() {
|
||||
if (this.developerNoAutoClosing) return
|
||||
if (this.disabled) {
|
||||
if (this.noMouseLeaveClosing || this.disabled) {
|
||||
return
|
||||
}
|
||||
this.clearTimeouts()
|
||||
|
||||
@ -1,59 +0,0 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import InviteButton from './InviteButton.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const stubs = {
|
||||
'v-popover': {
|
||||
template: '<span><slot /></span>',
|
||||
},
|
||||
}
|
||||
|
||||
describe('InviteButton.vue', () => {
|
||||
let wrapper
|
||||
let mocks
|
||||
let propsData
|
||||
|
||||
beforeEach(() => {
|
||||
mocks = {
|
||||
$t: jest.fn(),
|
||||
navigator: {
|
||||
clipboard: {
|
||||
writeText: jest.fn(),
|
||||
},
|
||||
},
|
||||
}
|
||||
propsData = {}
|
||||
})
|
||||
|
||||
describe('mount', () => {
|
||||
const Wrapper = () => {
|
||||
return mount(InviteButton, { mocks, localVue, propsData, stubs })
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('renders', () => {
|
||||
expect(wrapper.find('.invite-button').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('open popup', () => {
|
||||
wrapper.find('.base-button').trigger('click')
|
||||
expect(wrapper.find('.invite-button').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('invite codes not available', async () => {
|
||||
wrapper.find('.base-button').trigger('click') // open popup
|
||||
wrapper.find('.invite-button').trigger('click') // click copy button
|
||||
expect(mocks.$t).toHaveBeenCalledWith('invite-codes.not-available')
|
||||
})
|
||||
|
||||
it.skip('invite codes copied to clipboard', async () => {
|
||||
wrapper.find('.base-button').trigger('click') // open popup
|
||||
wrapper.find('.invite-button').trigger('click') // click copy button
|
||||
expect(mocks.$t).toHaveBeenCalledWith('invite-codes.not-available')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<dropdown class="invite-button" offset="8" :placement="placement">
|
||||
<dropdown class="invite-button" offset="8" :placement="placement" noMouseLeaveClosing>
|
||||
<template #default="{ toggleMenu }">
|
||||
<base-button
|
||||
icon="user-plus"
|
||||
@ -13,24 +13,18 @@
|
||||
/>
|
||||
</template>
|
||||
<template #popover>
|
||||
<div class="invite-button-menu-popover">
|
||||
<div v-if="inviteCode && inviteCode.code">
|
||||
<p class="description">{{ $t('invite-codes.your-code') }}</p>
|
||||
<base-card class="code-card" wideContent>
|
||||
<base-button
|
||||
v-if="canCopy"
|
||||
class="invite-code"
|
||||
icon="copy"
|
||||
ghost
|
||||
@click="copyInviteLink"
|
||||
>
|
||||
<ds-text bold>{{ $t('invite-codes.copy-code') }}</ds-text>
|
||||
</base-button>
|
||||
</base-card>
|
||||
</div>
|
||||
<div v-else>
|
||||
<ds-text>{{ $t('invite-codes.not-available') }}</ds-text>
|
||||
</div>
|
||||
<div class="invite-list">
|
||||
<h2>{{ $t('invite-codes.my-invite-links') }}</h2>
|
||||
<invitation-list
|
||||
@generate-invite-code="generatePersonalInviteCode"
|
||||
@invalidate-invite-code="invalidateInviteCode"
|
||||
:inviteCodes="user.inviteCodes"
|
||||
:copy-message="
|
||||
$t('invite-codes.invite-link-message-personal', {
|
||||
network: $env.NETWORK_NAME,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</dropdown>
|
||||
@ -38,82 +32,88 @@
|
||||
|
||||
<script>
|
||||
import Dropdown from '~/components/Dropdown'
|
||||
import gql from 'graphql-tag'
|
||||
import BaseCard from '../_new/generic/BaseCard/BaseCard.vue'
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import InvitationList from '~/components/_new/features/Invitations/InvitationList.vue'
|
||||
import { generatePersonalInviteCode, invalidateInviteCode } from '~/graphql/InviteCode'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Dropdown,
|
||||
BaseCard,
|
||||
InvitationList,
|
||||
},
|
||||
props: {
|
||||
placement: { type: String, default: 'top-end' },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
inviteCode: null,
|
||||
canCopy: false,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.canCopy = !!navigator.clipboard
|
||||
},
|
||||
|
||||
computed: {
|
||||
inviteLink() {
|
||||
return (
|
||||
'https://' +
|
||||
window.location.hostname +
|
||||
'/registration?method=invite-code&inviteCode=' +
|
||||
this.inviteCode.code
|
||||
)
|
||||
...mapGetters({
|
||||
user: 'auth/user',
|
||||
}),
|
||||
inviteCode() {
|
||||
return this.user.inviteCodes[0] || null
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async copyInviteLink() {
|
||||
await navigator.clipboard.writeText(this.inviteLink)
|
||||
this.$toast.success(this.$t('invite-codes.copy-success'))
|
||||
...mapMutations({
|
||||
setCurrentUser: 'auth/SET_USER_PARTIAL',
|
||||
}),
|
||||
async generatePersonalInviteCode(comment) {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: generatePersonalInviteCode(),
|
||||
variables: {
|
||||
comment,
|
||||
},
|
||||
update: (_, { data: { generatePersonalInviteCode } }) => {
|
||||
this.setCurrentUser({
|
||||
...this.currentUser,
|
||||
inviteCodes: [...this.user.inviteCodes, generatePersonalInviteCode],
|
||||
})
|
||||
},
|
||||
})
|
||||
this.$toast.success(this.$t('invite-codes.create-success'))
|
||||
} catch (error) {
|
||||
this.$toast.error(this.$t('invite-codes.create-error', { error: error.message }))
|
||||
}
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
inviteCode: {
|
||||
query() {
|
||||
return gql`
|
||||
query {
|
||||
getInviteCode {
|
||||
code
|
||||
}
|
||||
}
|
||||
`
|
||||
},
|
||||
variables() {},
|
||||
update({ getInviteCode }) {
|
||||
return getInviteCode
|
||||
},
|
||||
async invalidateInviteCode(code) {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: invalidateInviteCode(),
|
||||
variables: {
|
||||
code,
|
||||
},
|
||||
update: (_, { data: { _invalidateInviteCode } }) => {
|
||||
this.setCurrentUser({
|
||||
...this.currentUser,
|
||||
inviteCodes: this.user.inviteCodes.map((inviteCode) => ({
|
||||
...inviteCode,
|
||||
isValid: inviteCode.code === code ? false : inviteCode.isValid,
|
||||
})),
|
||||
})
|
||||
},
|
||||
})
|
||||
this.$toast.success(this.$t('invite-codes.invalidate-success'))
|
||||
} catch (error) {
|
||||
this.$toast.error(this.$t('invite-codes.invalidate-error', { error: error.message }))
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scope>
|
||||
<style lang="scss" scoped>
|
||||
.invite-button {
|
||||
color: $color-secondary;
|
||||
}
|
||||
|
||||
.invite-button-menu-popover {
|
||||
.invite-list {
|
||||
max-width: min(400px, 90vw);
|
||||
padding: $space-small;
|
||||
margin-top: $space-base;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.description {
|
||||
margin-top: $space-x-small;
|
||||
margin-bottom: $space-x-small;
|
||||
}
|
||||
.code-card {
|
||||
margin-bottom: $space-x-small;
|
||||
}
|
||||
}
|
||||
|
||||
.invite-code {
|
||||
margin-left: 25%;
|
||||
flex-flow: column;
|
||||
gap: $space-small;
|
||||
--invitation-column-max-width: 75%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -269,7 +269,17 @@ export default {
|
||||
setTimeout(async () => {
|
||||
await this.$store.dispatch('auth/login', { email, password })
|
||||
this.$toast.success(this.$t('login.success'))
|
||||
this.$router.push('/')
|
||||
const { validateInviteCode } = this.sliderData.sliders[0].data.response
|
||||
if (
|
||||
validateInviteCode &&
|
||||
validateInviteCode.invitedTo &&
|
||||
validateInviteCode.invitedTo.groupType === 'public'
|
||||
) {
|
||||
const { invitedTo } = validateInviteCode
|
||||
this.$router.push(`/groups/${invitedTo.slug}`)
|
||||
} else {
|
||||
this.$router.push('/')
|
||||
}
|
||||
this.sliderData.setSliderValuesCallback(null, {
|
||||
sliderSettings: { buttonLoading: false },
|
||||
})
|
||||
|
||||
@ -13,28 +13,48 @@
|
||||
id="inviteCode"
|
||||
icon="question-circle"
|
||||
/>
|
||||
<ds-text>
|
||||
<ds-text v-if="!validInput">
|
||||
{{ $t('components.registration.invite-code.form.description') }}
|
||||
</ds-text>
|
||||
<div class="invitation-info" v-if="invitedBy">
|
||||
<profile-avatar :profile="invitedBy" size="small" />
|
||||
<span v-if="invitedTo && invitedTo.groupType === 'hidden'">
|
||||
{{
|
||||
$t('components.registration.invite-code.invited-to-hidden-group', {
|
||||
invitedBy: invitedBy.name,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span v-else-if="invitedTo">
|
||||
{{
|
||||
$t('components.registration.invite-code.invited-by-and-to', {
|
||||
invitedBy: invitedBy.name,
|
||||
invitedTo: invitedTo.name,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t('components.registration.invite-code.invited-by', { invitedBy: invitedBy.name }) }}
|
||||
</span>
|
||||
</div>
|
||||
<slot></slot>
|
||||
<ds-space margin="xxx-small" />
|
||||
</ds-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import registrationConstants from '~/constants/registration'
|
||||
import { validateInviteCode } from '~/graphql/InviteCode'
|
||||
import ProfileAvatar from '~/components/_new/generic/ProfileAvatar/ProfileAvatar'
|
||||
|
||||
export const isValidInviteCodeQuery = gql`
|
||||
query ($code: ID!) {
|
||||
isValidInviteCode(code: $code)
|
||||
}
|
||||
`
|
||||
export default {
|
||||
name: 'RegistrationSlideInvite',
|
||||
props: {
|
||||
sliderData: { type: Object, required: true },
|
||||
},
|
||||
components: {
|
||||
ProfileAvatar,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formData: {
|
||||
@ -75,6 +95,16 @@ export default {
|
||||
validInput() {
|
||||
return this.formData.inviteCode.length === 6
|
||||
},
|
||||
invitedBy() {
|
||||
return this.sliderData.sliders[this.sliderIndex].data.response.validateInviteCode
|
||||
? this.sliderData.sliders[this.sliderIndex].data.response.validateInviteCode.generatedBy
|
||||
: null
|
||||
},
|
||||
invitedTo() {
|
||||
return this.sliderData.sliders[this.sliderIndex].data.response.validateInviteCode
|
||||
? this.sliderData.sliders[this.sliderIndex].data.response.validateInviteCode.invitedTo
|
||||
: null
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async sendValidation() {
|
||||
@ -84,8 +114,7 @@ export default {
|
||||
|
||||
let dbValidated = false
|
||||
if (this.validInput) {
|
||||
await this.handleSubmitVerify()
|
||||
dbValidated = this.sliderData.sliders[this.sliderIndex].data.response.isValidInviteCode
|
||||
dbValidated = await this.handleSubmitVerify()
|
||||
}
|
||||
this.sliderData.setSliderValuesCallback(dbValidated)
|
||||
},
|
||||
@ -110,7 +139,7 @@ export default {
|
||||
try {
|
||||
this.dbRequestInProgress = true
|
||||
|
||||
const response = await this.$apollo.query({ query: isValidInviteCodeQuery, variables })
|
||||
const response = await this.$apollo.query({ query: validateInviteCode(), variables })
|
||||
this.sliderData.setSliderValuesCallback(null, {
|
||||
sliderData: {
|
||||
request: { variables },
|
||||
@ -118,20 +147,22 @@ export default {
|
||||
},
|
||||
})
|
||||
|
||||
if (this.sliderData.sliders[this.sliderIndex].data.response) {
|
||||
if (this.sliderData.sliders[this.sliderIndex].data.response.isValidInviteCode) {
|
||||
this.$toast.success(
|
||||
this.$t('components.registration.invite-code.form.validations.success', {
|
||||
inviteCode,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
this.$toast.error(
|
||||
this.$t('components.registration.invite-code.form.validations.error', {
|
||||
inviteCode,
|
||||
}),
|
||||
)
|
||||
}
|
||||
const validationResult = response.data.validateInviteCode
|
||||
|
||||
if (validationResult && validationResult.isValid) {
|
||||
this.$toast.success(
|
||||
this.$t('components.registration.invite-code.form.validations.success', {
|
||||
inviteCode,
|
||||
}),
|
||||
)
|
||||
return true
|
||||
} else {
|
||||
this.$toast.error(
|
||||
this.$t('components.registration.invite-code.form.validations.error', {
|
||||
inviteCode,
|
||||
}),
|
||||
)
|
||||
return false
|
||||
}
|
||||
} catch (err) {
|
||||
this.sliderData.setSliderValuesCallback(false, {
|
||||
@ -140,6 +171,7 @@ export default {
|
||||
|
||||
const { message } = err
|
||||
this.$toast.error(message)
|
||||
return false
|
||||
} finally {
|
||||
this.dbRequestInProgress = false
|
||||
}
|
||||
@ -152,10 +184,25 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
.enter-invite {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: $space-large 0 $space-xxx-small 0;
|
||||
}
|
||||
|
||||
.invitation-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: $space-x-small;
|
||||
gap: $space-small;
|
||||
|
||||
> * {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
> span {
|
||||
flex: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
|
||||
<template v-if="registrationType !== 'no-public-registration'" #footer>
|
||||
<ds-space margin-bottom="xxx-small" margin-top="small" centered>
|
||||
<nuxt-link to="/login">{{ $t('site.back-to-login') }}</nuxt-link>
|
||||
<nuxt-link :to="loginLink">{{ $t('site.back-to-login') }}</nuxt-link>
|
||||
</ds-space>
|
||||
</template>
|
||||
</component-slider>
|
||||
@ -163,6 +163,10 @@ export default {
|
||||
}
|
||||
|
||||
return {
|
||||
loginLink: {
|
||||
name: 'login',
|
||||
query: this.$route.query,
|
||||
},
|
||||
links,
|
||||
metadata,
|
||||
sliderData: {
|
||||
|
||||
@ -0,0 +1,51 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/vue'
|
||||
|
||||
import CreateInvitation from './CreateInvitation.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('CreateInvitation.vue', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = ({ isDisabled = false }) => {
|
||||
return render(CreateInvitation, {
|
||||
localVue,
|
||||
propsData: {
|
||||
isDisabled,
|
||||
},
|
||||
mocks: {
|
||||
$t: jest.fn((v) => v),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
it('renders', () => {
|
||||
wrapper = Wrapper({})
|
||||
expect(wrapper.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders with disabled button', () => {
|
||||
wrapper = Wrapper({ isDisabled: true })
|
||||
expect(wrapper.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
describe('when the form is submitted', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper({})
|
||||
})
|
||||
|
||||
it('emits generate-invite-code with empty comment', async () => {
|
||||
const button = screen.getByRole('button')
|
||||
await fireEvent.click(button)
|
||||
expect(wrapper.emitted()['generate-invite-code']).toEqual([['']])
|
||||
})
|
||||
|
||||
it('emits generate-invite-code with comment', async () => {
|
||||
const button = screen.getByRole('button')
|
||||
const input = screen.getByPlaceholderText('invite-codes.comment-placeholder')
|
||||
await fireEvent.update(input, 'Test comment')
|
||||
await fireEvent.click(button)
|
||||
expect(wrapper.emitted()['generate-invite-code']).toEqual([['Test comment']])
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div class="create-invitation">
|
||||
<div>{{ $t('invite-codes.generate-code-explanation') }}</div>
|
||||
<form @submit.prevent="generateInviteCode" class="generate-invite-code-form">
|
||||
<ds-input
|
||||
name="comment"
|
||||
:placeholder="$t('invite-codes.comment-placeholder')"
|
||||
v-model="comment"
|
||||
:schema="{ type: 'string', max: 30 }"
|
||||
/>
|
||||
<base-button
|
||||
circle
|
||||
class="generate-invite-code"
|
||||
:aria-label="$t('invite-codes.generate-code')"
|
||||
icon="plus"
|
||||
type="submit"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CreateInvitation',
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
comment: '',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async generateInviteCode() {
|
||||
this.$emit('generate-invite-code', this.comment)
|
||||
this.comment = ''
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.create-invitation {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $space-small;
|
||||
}
|
||||
|
||||
.generate-invite-code-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $space-x-small;
|
||||
}
|
||||
|
||||
::v-deep .ds-form-item {
|
||||
margin-bottom: 0;
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
::v-deep .ds-input-error {
|
||||
margin-top: $space-xx-small;
|
||||
margin-left: $space-x-small;
|
||||
}
|
||||
</style>
|
||||
115
webapp/components/_new/features/Invitations/Invitation.spec.js
Normal file
115
webapp/components/_new/features/Invitations/Invitation.spec.js
Normal file
@ -0,0 +1,115 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/vue'
|
||||
import '@testing-library/jest-dom'
|
||||
|
||||
import Invitation from './Invitation.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
Object.assign(navigator, {
|
||||
clipboard: {
|
||||
writeText: jest.fn(),
|
||||
},
|
||||
})
|
||||
|
||||
const mutations = {
|
||||
'modal/SET_OPEN': jest.fn().mockResolvedValue(),
|
||||
}
|
||||
|
||||
describe('Invitation.vue', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = ({ wasRedeemed = false, withCopymessage = false }) => {
|
||||
const propsData = {
|
||||
inviteCode: {
|
||||
code: 'test-invite-code',
|
||||
comment: 'test-comment',
|
||||
redeemedByCount: wasRedeemed ? 1 : 0,
|
||||
},
|
||||
copyMessage: withCopymessage ? 'test-copy-message' : undefined,
|
||||
}
|
||||
return render(Invitation, {
|
||||
localVue,
|
||||
propsData,
|
||||
mocks: {
|
||||
$t: jest.fn((v) => v),
|
||||
$toast: {
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
},
|
||||
mutations,
|
||||
})
|
||||
}
|
||||
|
||||
describe('when the invite code was redeemed', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper({ wasRedeemed: true })
|
||||
})
|
||||
|
||||
it('renders', () => {
|
||||
expect(wrapper.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('says how many times the code was redeemed', () => {
|
||||
const redeemedCount = screen.getByText('invite-codes.redeemed-count')
|
||||
expect(redeemedCount).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the invite code was not redeemed', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper({ wasRedeemed: false })
|
||||
})
|
||||
|
||||
it('renders', () => {
|
||||
expect(wrapper.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('says it was not redeemed', () => {
|
||||
const redeemedCount = screen.queryByText('invite-codes.redeemed-count-0')
|
||||
expect(redeemedCount).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('without copy message', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper({ withCopymessage: false })
|
||||
})
|
||||
|
||||
it('can copy the link', async () => {
|
||||
const clipboardMock = jest.spyOn(navigator.clipboard, 'writeText').mockResolvedValue()
|
||||
const copyButton = screen.getByLabelText('invite-codes.copy-code')
|
||||
await fireEvent.click(copyButton)
|
||||
expect(clipboardMock).toHaveBeenCalledWith(
|
||||
'http://localhost/registration?method=invite-code&inviteCode=test-invite-code',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with copy message', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper({ withCopymessage: true })
|
||||
})
|
||||
|
||||
it('can copy the link with message', async () => {
|
||||
const clipboardMock = jest.spyOn(navigator.clipboard, 'writeText').mockResolvedValue()
|
||||
const copyButton = screen.getByLabelText('invite-codes.copy-code')
|
||||
await fireEvent.click(copyButton)
|
||||
expect(clipboardMock).toHaveBeenCalledWith(
|
||||
'test-copy-message http://localhost/registration?method=invite-code&inviteCode=test-invite-code',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe.skip('invalidate button', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper({ wasRedeemed: false })
|
||||
})
|
||||
|
||||
it('opens the delete modal', async () => {
|
||||
const deleteButton = screen.getByLabelText('invite-codes.invalidate')
|
||||
await fireEvent.click(deleteButton)
|
||||
expect(mutations['modal/SET_OPEN']).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
156
webapp/components/_new/features/Invitations/Invitation.vue
Normal file
156
webapp/components/_new/features/Invitations/Invitation.vue
Normal file
@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<li class="invitation">
|
||||
<div class="column1">
|
||||
<div class="code">
|
||||
{{ inviteCode.code }}
|
||||
<span v-if="inviteCode.comment" class="mdash">—</span>
|
||||
<span v-if="inviteCode.comment" class="comment">{{ inviteCode.comment }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span v-if="inviteCode.redeemedByCount === 0">
|
||||
{{ $t('invite-codes.redeemed-count-0') }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t('invite-codes.redeemed-count', { count: inviteCode.redeemedByCount }) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<base-button
|
||||
circle
|
||||
class="copy-button"
|
||||
icon="copy"
|
||||
@click="copyInviteCode(inviteCode.copy)"
|
||||
:disabled="!canCopy"
|
||||
:aria-label="$t('invite-codes.copy-code')"
|
||||
/>
|
||||
<base-button
|
||||
circle
|
||||
class="invalidate-button"
|
||||
icon="trash"
|
||||
@click="openDeleteModal"
|
||||
:aria-label="$t('invite-codes.invalidate')"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapMutations } from 'vuex'
|
||||
import BaseButton from '~/components/_new/generic/BaseButton/BaseButton.vue'
|
||||
|
||||
export default {
|
||||
name: 'Invitation',
|
||||
components: {
|
||||
BaseButton,
|
||||
},
|
||||
props: {
|
||||
inviteCode: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
copyMessage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
inviteLink() {
|
||||
return `${window.location.origin}/registration?method=invite-code&inviteCode=${this.inviteCode.code}`
|
||||
},
|
||||
inviteMessageAndLink() {
|
||||
return this.copyMessage ? `${this.copyMessage} ${this.inviteLink}` : this.inviteLink
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
canCopy: false,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.canCopy = !!navigator.clipboard
|
||||
},
|
||||
methods: {
|
||||
...mapMutations({
|
||||
commitModalData: 'modal/SET_OPEN',
|
||||
}),
|
||||
async copyInviteCode() {
|
||||
await navigator.clipboard.writeText(this.inviteMessageAndLink)
|
||||
this.$toast.success(this.$t('invite-codes.copy-success'))
|
||||
},
|
||||
openDeleteModal() {
|
||||
this.commitModalData({
|
||||
name: 'confirm',
|
||||
data: {
|
||||
type: '',
|
||||
resource: { id: '' },
|
||||
modalData: {
|
||||
titleIdent: this.$t('invite-codes.delete-modal.title'),
|
||||
messageIdent: this.$t('invite-codes.delete-modal.message'),
|
||||
buttons: {
|
||||
confirm: {
|
||||
danger: true,
|
||||
icon: 'trash',
|
||||
textIdent: 'actions.delete',
|
||||
callback: () => {
|
||||
this.$emit('invalidate-invite-code', this.inviteCode.code)
|
||||
},
|
||||
},
|
||||
cancel: {
|
||||
icon: 'close',
|
||||
textIdent: 'actions.cancel',
|
||||
callback: () => {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.invitation {
|
||||
display: flex;
|
||||
padding: calc($space-base / 2);
|
||||
border-bottom: 1px dotted #e5e3e8;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.invitation:nth-child(odd) {
|
||||
background-color: $color-neutral-90;
|
||||
}
|
||||
|
||||
.invitation:nth-child(even) {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.column1 {
|
||||
flex: auto;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: $space-xx-small;
|
||||
max-width: var(--invitation-column-max-width, 100%);
|
||||
}
|
||||
|
||||
.code {
|
||||
display: inline-flex;
|
||||
max-width: 73%;
|
||||
}
|
||||
|
||||
.comment {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: inline-flex;
|
||||
gap: $space-x-small;
|
||||
}
|
||||
|
||||
.mdash {
|
||||
margin-inline: $space-x-small;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,113 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/vue'
|
||||
import '@testing-library/jest-dom'
|
||||
|
||||
import InvitationList from './InvitationList.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
Object.assign(navigator, {
|
||||
clipboard: {
|
||||
writeText: jest.fn(),
|
||||
},
|
||||
})
|
||||
|
||||
const sampleInviteCodes = [
|
||||
{
|
||||
code: 'test-invite-code-1',
|
||||
comment: 'test-comment',
|
||||
redeemedByCount: 0,
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
code: 'test-invite-code-2',
|
||||
comment: 'test-comment-2',
|
||||
redeemedByCount: 1,
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
code: 'test-invite-code-3',
|
||||
comment: 'test-comment-3',
|
||||
redeemedByCount: 0,
|
||||
isValid: false,
|
||||
},
|
||||
]
|
||||
|
||||
describe('InvitationList.vue', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = ({ withInviteCodes, withCopymessage = false, limit = 3 }) => {
|
||||
const propsData = {
|
||||
inviteCodes: withInviteCodes ? sampleInviteCodes : [],
|
||||
copyMessage: withCopymessage ? 'test-copy-message' : undefined,
|
||||
}
|
||||
return render(InvitationList, {
|
||||
localVue,
|
||||
propsData,
|
||||
mocks: {
|
||||
$t: jest.fn((v) => v),
|
||||
$toast: {
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
$env: {
|
||||
INVITE_LINK_LIMIT: limit,
|
||||
},
|
||||
},
|
||||
stubs: {
|
||||
'client-only': true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
it('renders', () => {
|
||||
wrapper = Wrapper({ withInviteCodes: true })
|
||||
expect(wrapper.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders empty state', () => {
|
||||
wrapper = Wrapper({ withInviteCodes: false })
|
||||
expect(wrapper.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('does not render invalid invite codes', () => {
|
||||
wrapper = Wrapper({ withInviteCodes: true })
|
||||
const invalidInviteCode = screen.queryByText('invite-codes.test-invite-code-3')
|
||||
expect(invalidInviteCode).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
describe('without copy message', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper({ withCopymessage: false, withInviteCodes: true })
|
||||
})
|
||||
|
||||
it('can copy a link', async () => {
|
||||
const clipboardMock = jest.spyOn(navigator.clipboard, 'writeText').mockResolvedValue()
|
||||
const copyButton = screen.getAllByLabelText('invite-codes.copy-code')[0]
|
||||
await fireEvent.click(copyButton)
|
||||
expect(clipboardMock).toHaveBeenCalledWith(
|
||||
'http://localhost/registration?method=invite-code&inviteCode=test-invite-code-1',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with copy message', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper({ withCopymessage: true, withInviteCodes: true })
|
||||
})
|
||||
|
||||
it('can copy the link with message', async () => {
|
||||
const clipboardMock = jest.spyOn(navigator.clipboard, 'writeText').mockResolvedValue()
|
||||
const copyButton = screen.getAllByLabelText('invite-codes.copy-code')[0]
|
||||
await fireEvent.click(copyButton)
|
||||
expect(clipboardMock).toHaveBeenCalledWith(
|
||||
'test-copy-message http://localhost/registration?method=invite-code&inviteCode=test-invite-code-1',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('cannot generate more than the limit of invite codes', () => {
|
||||
wrapper = Wrapper({ withInviteCodes: true, limit: 2 })
|
||||
const generateButton = screen.getByLabelText('invite-codes.generate-code')
|
||||
expect(generateButton).toBeDisabled()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div class="invitation-list">
|
||||
<ul v-if="validInviteCodes.length">
|
||||
<client-only>
|
||||
<invitation
|
||||
v-for="inviteCode in validInviteCodes"
|
||||
:key="inviteCode.code"
|
||||
:invite-code="inviteCode"
|
||||
:copy-message="copyMessage"
|
||||
@invalidate-invite-code="invalidateInviteCode"
|
||||
/>
|
||||
</client-only>
|
||||
</ul>
|
||||
<div v-else class="no-invitation">
|
||||
{{ $t('invite-codes.no-links', { max: maxLinks }) }}
|
||||
</div>
|
||||
<create-invitation
|
||||
@generate-invite-code="generateInviteCode"
|
||||
:disabled="isLimitReached"
|
||||
class="create-invitation"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Invitation from './Invitation.vue'
|
||||
import CreateInvitation from './CreateInvitation.vue'
|
||||
|
||||
export default {
|
||||
name: 'InvitationList',
|
||||
props: {
|
||||
inviteCodes: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
copyMessage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Invitation,
|
||||
CreateInvitation,
|
||||
},
|
||||
computed: {
|
||||
validInviteCodes() {
|
||||
return this.inviteCodes.filter((inviteCode) => inviteCode.isValid)
|
||||
},
|
||||
maxLinks() {
|
||||
return Number(this.$env.INVITE_LINK_LIMIT)
|
||||
},
|
||||
isLimitReached() {
|
||||
return this.validInviteCodes.length >= this.maxLinks
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
generateInviteCode(comment) {
|
||||
this.$emit('generate-invite-code', comment)
|
||||
},
|
||||
invalidateInviteCode(code) {
|
||||
this.$emit('invalidate-invite-code', code)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.invitation-list {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: $space-base;
|
||||
padding-bottom: $space-base;
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
}
|
||||
|
||||
.create-invitation {
|
||||
margin-top: $space-base;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,140 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CreateInvitation.vue renders 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="create-invitation"
|
||||
>
|
||||
<div>
|
||||
invite-codes.generate-code-explanation
|
||||
</div>
|
||||
|
||||
<form
|
||||
class="generate-invite-code-form"
|
||||
>
|
||||
<div
|
||||
class="ds-form-item ds-input-size-base"
|
||||
>
|
||||
<label
|
||||
class="ds-input-label"
|
||||
style="display: none;"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</label>
|
||||
<div
|
||||
class="ds-input-wrap"
|
||||
>
|
||||
<!---->
|
||||
<input
|
||||
class="ds-input"
|
||||
name="comment"
|
||||
placeholder="invite-codes.comment-placeholder"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
/>
|
||||
<!---->
|
||||
</div>
|
||||
<transition-stub
|
||||
name="ds-input-error"
|
||||
>
|
||||
<div
|
||||
class="ds-input-error"
|
||||
style="display: none;"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</transition-stub>
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-label="invite-codes.generate-code"
|
||||
class="generate-invite-code base-button --icon-only --circle"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CreateInvitation.vue renders with disabled button 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="create-invitation"
|
||||
isdisabled="true"
|
||||
>
|
||||
<div>
|
||||
invite-codes.generate-code-explanation
|
||||
</div>
|
||||
|
||||
<form
|
||||
class="generate-invite-code-form"
|
||||
>
|
||||
<div
|
||||
class="ds-form-item ds-input-size-base"
|
||||
>
|
||||
<label
|
||||
class="ds-input-label"
|
||||
style="display: none;"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</label>
|
||||
<div
|
||||
class="ds-input-wrap"
|
||||
>
|
||||
<!---->
|
||||
<input
|
||||
class="ds-input"
|
||||
name="comment"
|
||||
placeholder="invite-codes.comment-placeholder"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
/>
|
||||
<!---->
|
||||
</div>
|
||||
<transition-stub
|
||||
name="ds-input-error"
|
||||
>
|
||||
<div
|
||||
class="ds-input-error"
|
||||
style="display: none;"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</transition-stub>
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-label="invite-codes.generate-code"
|
||||
class="generate-invite-code base-button --icon-only --circle"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -0,0 +1,147 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Invitation.vue when the invite code was not redeemed renders 1`] = `
|
||||
<div>
|
||||
<li
|
||||
class="invitation"
|
||||
>
|
||||
<div
|
||||
class="column1"
|
||||
>
|
||||
<div
|
||||
class="code"
|
||||
>
|
||||
|
||||
test-invite-code
|
||||
|
||||
<span
|
||||
class="mdash"
|
||||
>
|
||||
—
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="comment"
|
||||
>
|
||||
test-comment
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span>
|
||||
|
||||
invite-codes.redeemed-count-0
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
aria-label="invite-codes.copy-code"
|
||||
class="copy-button base-button --icon-only --circle"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
|
||||
<button
|
||||
aria-label="invite-codes.invalidate"
|
||||
class="invalidate-button base-button --icon-only --circle"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Invitation.vue when the invite code was redeemed renders 1`] = `
|
||||
<div>
|
||||
<li
|
||||
class="invitation"
|
||||
>
|
||||
<div
|
||||
class="column1"
|
||||
>
|
||||
<div
|
||||
class="code"
|
||||
>
|
||||
|
||||
test-invite-code
|
||||
|
||||
<span
|
||||
class="mdash"
|
||||
>
|
||||
—
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="comment"
|
||||
>
|
||||
test-comment
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span>
|
||||
|
||||
invite-codes.redeemed-count
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
aria-label="invite-codes.copy-code"
|
||||
class="copy-button base-button --icon-only --circle"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
|
||||
<button
|
||||
aria-label="invite-codes.invalidate"
|
||||
class="invalidate-button base-button --icon-only --circle"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
`;
|
||||
@ -0,0 +1,296 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`InvitationList.vue renders 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="invitation-list"
|
||||
>
|
||||
<ul>
|
||||
<client-only-stub>
|
||||
<li
|
||||
class="invitation"
|
||||
>
|
||||
<div
|
||||
class="column1"
|
||||
>
|
||||
<div
|
||||
class="code"
|
||||
>
|
||||
|
||||
test-invite-code-1
|
||||
|
||||
<span
|
||||
class="mdash"
|
||||
>
|
||||
—
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="comment"
|
||||
>
|
||||
test-comment
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span>
|
||||
|
||||
invite-codes.redeemed-count-0
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
aria-label="invite-codes.copy-code"
|
||||
class="copy-button base-button --icon-only --circle"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
|
||||
<button
|
||||
aria-label="invite-codes.invalidate"
|
||||
class="invalidate-button base-button --icon-only --circle"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class="invitation"
|
||||
>
|
||||
<div
|
||||
class="column1"
|
||||
>
|
||||
<div
|
||||
class="code"
|
||||
>
|
||||
|
||||
test-invite-code-2
|
||||
|
||||
<span
|
||||
class="mdash"
|
||||
>
|
||||
—
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="comment"
|
||||
>
|
||||
test-comment-2
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span>
|
||||
|
||||
invite-codes.redeemed-count
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
aria-label="invite-codes.copy-code"
|
||||
class="copy-button base-button --icon-only --circle"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
|
||||
<button
|
||||
aria-label="invite-codes.invalidate"
|
||||
class="invalidate-button base-button --icon-only --circle"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</client-only-stub>
|
||||
</ul>
|
||||
|
||||
<div
|
||||
class="create-invitation create-invitation"
|
||||
>
|
||||
<div>
|
||||
invite-codes.generate-code-explanation
|
||||
</div>
|
||||
|
||||
<form
|
||||
class="generate-invite-code-form"
|
||||
>
|
||||
<div
|
||||
class="ds-form-item ds-input-size-base"
|
||||
>
|
||||
<label
|
||||
class="ds-input-label"
|
||||
style="display: none;"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</label>
|
||||
<div
|
||||
class="ds-input-wrap"
|
||||
>
|
||||
<!---->
|
||||
<input
|
||||
class="ds-input"
|
||||
name="comment"
|
||||
placeholder="invite-codes.comment-placeholder"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
/>
|
||||
<!---->
|
||||
</div>
|
||||
<transition-stub
|
||||
name="ds-input-error"
|
||||
>
|
||||
<div
|
||||
class="ds-input-error"
|
||||
style="display: none;"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</transition-stub>
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-label="invite-codes.generate-code"
|
||||
class="generate-invite-code base-button --icon-only --circle"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`InvitationList.vue renders empty state 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="invitation-list"
|
||||
>
|
||||
<div
|
||||
class="no-invitation"
|
||||
>
|
||||
|
||||
invite-codes.no-links
|
||||
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="create-invitation create-invitation"
|
||||
>
|
||||
<div>
|
||||
invite-codes.generate-code-explanation
|
||||
</div>
|
||||
|
||||
<form
|
||||
class="generate-invite-code-form"
|
||||
>
|
||||
<div
|
||||
class="ds-form-item ds-input-size-base"
|
||||
>
|
||||
<label
|
||||
class="ds-input-label"
|
||||
style="display: none;"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</label>
|
||||
<div
|
||||
class="ds-input-wrap"
|
||||
>
|
||||
<!---->
|
||||
<input
|
||||
class="ds-input"
|
||||
name="comment"
|
||||
placeholder="invite-codes.comment-placeholder"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
/>
|
||||
<!---->
|
||||
</div>
|
||||
<transition-stub
|
||||
name="ds-input-error"
|
||||
>
|
||||
<div
|
||||
class="ds-input-error"
|
||||
style="display: none;"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</transition-stub>
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-label="invite-codes.generate-code"
|
||||
class="generate-invite-code base-button --icon-only --circle"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -36,6 +36,8 @@ const options = {
|
||||
COOKIE_HTTPS_ONLY: process.env.COOKIE_HTTPS_ONLY || process.env.NODE_ENV === 'production', // ensure true in production if not set explicitly
|
||||
CATEGORIES_ACTIVE: process.env.CATEGORIES_ACTIVE === 'true' || false,
|
||||
BADGES_ENABLED: process.env.BADGES_ENABLED === 'true' || false,
|
||||
INVITE_LINK_LIMIT: process.env.INVITE_LINK_LIMIT || 7,
|
||||
NETWORK_NAME: process.env.NETWORK_NAME || 'Ocelot.social',
|
||||
}
|
||||
|
||||
const CONFIG = {
|
||||
|
||||
138
webapp/graphql/InviteCode.js
Normal file
138
webapp/graphql/InviteCode.js
Normal file
@ -0,0 +1,138 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const validateInviteCode = () => gql`
|
||||
query validateInviteCode($code: String!) {
|
||||
validateInviteCode(code: $code) {
|
||||
code
|
||||
invitedTo {
|
||||
slug
|
||||
groupType
|
||||
name
|
||||
about
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
generatedBy {
|
||||
name
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
isValid
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const generatePersonalInviteCode = () => gql`
|
||||
mutation generatePersonalInviteCode($expiresAt: String, $comment: String) {
|
||||
generatePersonalInviteCode(expiresAt: $expiresAt, comment: $comment) {
|
||||
code
|
||||
createdAt
|
||||
generatedBy {
|
||||
id
|
||||
name
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
redeemedBy {
|
||||
id
|
||||
name
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
redeemedByCount
|
||||
expiresAt
|
||||
comment
|
||||
invitedTo {
|
||||
groupType
|
||||
name
|
||||
about
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
isValid
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const generateGroupInviteCode = () => gql`
|
||||
mutation generateGroupInviteCode($groupId: ID!, $expiresAt: String, $comment: String) {
|
||||
generateGroupInviteCode(groupId: $groupId, expiresAt: $expiresAt, comment: $comment) {
|
||||
code
|
||||
createdAt
|
||||
generatedBy {
|
||||
id
|
||||
name
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
redeemedBy {
|
||||
id
|
||||
name
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
redeemedByCount
|
||||
expiresAt
|
||||
comment
|
||||
invitedTo {
|
||||
id
|
||||
groupType
|
||||
name
|
||||
about
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
isValid
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const invalidateInviteCode = () => gql`
|
||||
mutation invalidateInviteCode($code: String!) {
|
||||
invalidateInviteCode(code: $code) {
|
||||
code
|
||||
createdAt
|
||||
generatedBy {
|
||||
id
|
||||
name
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
redeemedBy {
|
||||
id
|
||||
name
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
redeemedByCount
|
||||
expiresAt
|
||||
comment
|
||||
invitedTo {
|
||||
id
|
||||
groupType
|
||||
name
|
||||
about
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
isValid
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const redeemInviteCode = () => gql`
|
||||
mutation redeemInviteCode($code: String!) {
|
||||
redeemInviteCode(code: $code)
|
||||
}
|
||||
`
|
||||
@ -406,6 +406,15 @@ export const currentUserQuery = gql`
|
||||
query {
|
||||
currentUser {
|
||||
...user
|
||||
inviteCodes {
|
||||
code
|
||||
isValid
|
||||
redeemedBy {
|
||||
id
|
||||
}
|
||||
comment
|
||||
redeemedByCount
|
||||
}
|
||||
badgeTrophiesSelected {
|
||||
id
|
||||
icon
|
||||
|
||||
@ -195,6 +195,16 @@ export const groupQuery = (i18n) => {
|
||||
lat
|
||||
}
|
||||
myRole
|
||||
inviteCodes {
|
||||
createdAt
|
||||
code
|
||||
isValid
|
||||
redeemedBy {
|
||||
id
|
||||
}
|
||||
comment
|
||||
redeemedByCount
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
19
webapp/graphql/inviteCodes.js
Normal file
19
webapp/graphql/inviteCodes.js
Normal file
@ -0,0 +1,19 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const validateInviteCodeQuery = gql`
|
||||
query ($code: String!) {
|
||||
validateInviteCode(code: $code) {
|
||||
invitedTo {
|
||||
id
|
||||
slug
|
||||
groupType
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const redeemInviteCodeMutation = gql`
|
||||
mutation ($code: String!) {
|
||||
redeemInviteCode(code: $code)
|
||||
}
|
||||
`
|
||||
@ -244,7 +244,10 @@
|
||||
"length": "muss genau {inviteCodeLength} Buchstaben lang sein",
|
||||
"success": "Gültiger Einladungs-Code <b>{inviteCode}</b>!"
|
||||
}
|
||||
}
|
||||
},
|
||||
"invited-by": "Eingeladen von {invitedBy}",
|
||||
"invited-by-and-to": "Einladung von {invitedBy} zur Grupppe {invitedTo}",
|
||||
"invited-to-hidden-group": "Eingeladen von {invitedBy} zu einer versteckten Gruppe"
|
||||
},
|
||||
"no-public-registrstion": {
|
||||
"title": "Keine öffentliche Registrierung möglich"
|
||||
@ -509,6 +512,7 @@
|
||||
"categoriesTitle": "Themen der Gruppe",
|
||||
"changeMemberRole": "Die Rolle wurde auf „{role}“ geändert!",
|
||||
"contentMenu": {
|
||||
"inviteLinks": "Einladungslinks",
|
||||
"muteGroup": "Stummschalten",
|
||||
"unmuteGroup": "Nicht stummschalten",
|
||||
"visitGroupPage": "Gruppe anzeigen"
|
||||
@ -531,6 +535,7 @@
|
||||
"goal": "Ziel der Gruppe",
|
||||
"groupCreated": "Die Gruppe wurde angelegt!",
|
||||
"in": "in",
|
||||
"invite-links": "Einladungslinks",
|
||||
"joinLeaveButton": {
|
||||
"iAmMember": "Bin Mitglied",
|
||||
"join": "Beitreten",
|
||||
@ -628,10 +633,29 @@
|
||||
"button": {
|
||||
"tooltip": "Freunde einladen"
|
||||
},
|
||||
"comment-placeholder": "Kommentar (optional)",
|
||||
"copy-code": "Einladungslink kopieren",
|
||||
"copy-success": "Einladungscode erfolgreich in die Zwischenablage kopiert",
|
||||
"not-available": "Du hast keinen Einladungscode zur Verfügung!",
|
||||
"your-code": "Sende diesen Link per E-Mail oder in sozialen Medien, um deine Freunde einzuladen:"
|
||||
"create-error": "Einladungslink konnte nicht erstellt werden: {error}",
|
||||
"create-success": "Einladungslink erfolgreich erstellt!",
|
||||
"delete-modal": {
|
||||
"message": "Möchtest du diesen Einladungslink wirklich ungültig machen?",
|
||||
"title": "Einladungslink widerrufen"
|
||||
},
|
||||
"generate-code": "Neuen Einladungslink erstellen",
|
||||
"generate-code-explanation": "Erstelle einen neuen Link. Wenn du möchtest, füge einen Kommentar hinzu (nur für dich sichtbar). ",
|
||||
"group-invite-links": "Gruppen-Einladungslinks",
|
||||
"invalidate": "Widerrufen",
|
||||
"invalidate-error": "Einladungslink konnte nicht ungültig gemacht werden: {error}",
|
||||
"invalidate-success": "Einladungslink erfolgreich widerrufen",
|
||||
"invite-link-message-group": "Du wurdest eingeladen, der Gruppe {groupName} auf {network} beizutreten.",
|
||||
"invite-link-message-hidden-group": "Du wurdest eingeladen, einer versteckten Gruppe auf {network} beizutreten.",
|
||||
"invite-link-message-personal": "Du wurdest eingeladen, dem Netzwerk {network} beizutreten",
|
||||
"limit-reached": "Du hast die maximale Anzahl an Einladungslinks erreicht.",
|
||||
"my-invite-links": "Meine Einladungslinks",
|
||||
"no-links": "Keine Links vorhanden",
|
||||
"redeemed-count": "{count} mal eingelöst",
|
||||
"redeemed-count-0": "Noch von niemandem eingelöst"
|
||||
},
|
||||
"localeSwitch": {
|
||||
"tooltip": "Sprache wählen"
|
||||
|
||||
@ -244,7 +244,10 @@
|
||||
"length": "must be {inviteCodeLength} characters long",
|
||||
"success": "Valid invite code <b>{inviteCode}</b>!"
|
||||
}
|
||||
}
|
||||
},
|
||||
"invited-by": "Invited by {invitedBy}.",
|
||||
"invited-by-and-to": "Invited by {invitedBy} to group {invitedTo}.",
|
||||
"invited-to-hidden-group": "Invited by {invitedBy} to a hidden group."
|
||||
},
|
||||
"no-public-registrstion": {
|
||||
"title": "No Public Registration"
|
||||
@ -509,6 +512,7 @@
|
||||
"categoriesTitle": "Topics of the group",
|
||||
"changeMemberRole": "The role has been changed to “{role}”!",
|
||||
"contentMenu": {
|
||||
"inviteLinks": "Invite links",
|
||||
"muteGroup": "Mute group",
|
||||
"unmuteGroup": "Unmute group",
|
||||
"visitGroupPage": "Show group"
|
||||
@ -531,6 +535,7 @@
|
||||
"goal": "Goal of group",
|
||||
"groupCreated": "The group was created!",
|
||||
"in": "in",
|
||||
"invite-links": "Invite Links",
|
||||
"joinLeaveButton": {
|
||||
"iAmMember": "I'm a member",
|
||||
"join": "Join",
|
||||
@ -628,10 +633,29 @@
|
||||
"button": {
|
||||
"tooltip": "Invite friends"
|
||||
},
|
||||
"comment-placeholder": "Comment (optional)",
|
||||
"copy-code": "Copy Invite Link",
|
||||
"copy-success": "Invite code copied to clipboard",
|
||||
"not-available": "You have no valid invite code available!",
|
||||
"your-code": "Send this link per e-mail or in social media to invite your friends:"
|
||||
"create-error": "Creating a new invite link failed! Error: {error}",
|
||||
"create-success": "Invite link created successfully!",
|
||||
"delete-modal": {
|
||||
"message": "Do you really want to invalidate this invite link?",
|
||||
"title": "Invalidate link?"
|
||||
},
|
||||
"generate-code": "Create new link",
|
||||
"generate-code-explanation": "Create a new link. You can add a comment if you like (only visible to you).",
|
||||
"group-invite-links": "Group invite links",
|
||||
"invalidate": "Invalidate link",
|
||||
"invalidate-error": "Invalidating the invite link failed! Error: {error}",
|
||||
"invalidate-success": "Invite link invalidated successfully!",
|
||||
"invite-link-message-group": "You have been invited to join the group “{groupName}” on {network}.",
|
||||
"invite-link-message-hidden-group": "You have been invited to join a hidden group on {network}.",
|
||||
"invite-link-message-personal": "You have been invited to join {network}.",
|
||||
"limit-reached": "You have reached the maximum number of invite links.",
|
||||
"my-invite-links": "My invite links",
|
||||
"no-links": "No invite links created yet.",
|
||||
"redeemed-count": "This code has been used {count} times.",
|
||||
"redeemed-count-0": "No one has used this code yet."
|
||||
},
|
||||
"localeSwitch": {
|
||||
"tooltip": "Choose language"
|
||||
|
||||
@ -244,7 +244,10 @@
|
||||
"length": null,
|
||||
"success": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"invited-by": null,
|
||||
"invited-by-and-to": null,
|
||||
"invited-to-hidden-group": null
|
||||
},
|
||||
"no-public-registrstion": {
|
||||
"title": null
|
||||
@ -509,6 +512,7 @@
|
||||
"categoriesTitle": null,
|
||||
"changeMemberRole": null,
|
||||
"contentMenu": {
|
||||
"inviteLinks": null,
|
||||
"muteGroup": "Silenciar grupo",
|
||||
"unmuteGroup": "Desactivar silencio del grupo",
|
||||
"visitGroupPage": null
|
||||
@ -531,6 +535,7 @@
|
||||
"goal": null,
|
||||
"groupCreated": null,
|
||||
"in": null,
|
||||
"invite-links": null,
|
||||
"joinLeaveButton": {
|
||||
"iAmMember": null,
|
||||
"join": null,
|
||||
@ -628,10 +633,29 @@
|
||||
"button": {
|
||||
"tooltip": null
|
||||
},
|
||||
"comment-placeholder": null,
|
||||
"copy-code": null,
|
||||
"copy-success": null,
|
||||
"not-available": null,
|
||||
"your-code": null
|
||||
"create-error": null,
|
||||
"create-success": null,
|
||||
"delete-modal": {
|
||||
"message": null,
|
||||
"title": null
|
||||
},
|
||||
"generate-code": null,
|
||||
"generate-code-explanation": null,
|
||||
"group-invite-links": null,
|
||||
"invalidate": null,
|
||||
"invalidate-error": null,
|
||||
"invalidate-success": null,
|
||||
"invite-link-message-group": null,
|
||||
"invite-link-message-hidden-group": null,
|
||||
"invite-link-message-personal": null,
|
||||
"limit-reached": null,
|
||||
"my-invite-links": null,
|
||||
"no-links": null,
|
||||
"redeemed-count": null,
|
||||
"redeemed-count-0": null
|
||||
},
|
||||
"localeSwitch": {
|
||||
"tooltip": null
|
||||
|
||||
@ -244,7 +244,10 @@
|
||||
"length": null,
|
||||
"success": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"invited-by": null,
|
||||
"invited-by-and-to": null,
|
||||
"invited-to-hidden-group": null
|
||||
},
|
||||
"no-public-registrstion": {
|
||||
"title": null
|
||||
@ -509,6 +512,7 @@
|
||||
"categoriesTitle": null,
|
||||
"changeMemberRole": null,
|
||||
"contentMenu": {
|
||||
"inviteLinks": null,
|
||||
"muteGroup": null,
|
||||
"unmuteGroup": null,
|
||||
"visitGroupPage": null
|
||||
@ -531,6 +535,7 @@
|
||||
"goal": null,
|
||||
"groupCreated": null,
|
||||
"in": null,
|
||||
"invite-links": null,
|
||||
"joinLeaveButton": {
|
||||
"iAmMember": null,
|
||||
"join": null,
|
||||
@ -628,10 +633,29 @@
|
||||
"button": {
|
||||
"tooltip": null
|
||||
},
|
||||
"comment-placeholder": null,
|
||||
"copy-code": null,
|
||||
"copy-success": null,
|
||||
"not-available": null,
|
||||
"your-code": null
|
||||
"create-error": null,
|
||||
"create-success": null,
|
||||
"delete-modal": {
|
||||
"message": null,
|
||||
"title": null
|
||||
},
|
||||
"generate-code": null,
|
||||
"generate-code-explanation": null,
|
||||
"group-invite-links": null,
|
||||
"invalidate": null,
|
||||
"invalidate-error": null,
|
||||
"invalidate-success": null,
|
||||
"invite-link-message-group": null,
|
||||
"invite-link-message-hidden-group": null,
|
||||
"invite-link-message-personal": null,
|
||||
"limit-reached": null,
|
||||
"my-invite-links": null,
|
||||
"no-links": null,
|
||||
"redeemed-count": null,
|
||||
"redeemed-count-0": null
|
||||
},
|
||||
"localeSwitch": {
|
||||
"tooltip": null
|
||||
|
||||
@ -244,7 +244,10 @@
|
||||
"length": null,
|
||||
"success": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"invited-by": null,
|
||||
"invited-by-and-to": null,
|
||||
"invited-to-hidden-group": null
|
||||
},
|
||||
"no-public-registrstion": {
|
||||
"title": null
|
||||
@ -509,6 +512,7 @@
|
||||
"categoriesTitle": null,
|
||||
"changeMemberRole": null,
|
||||
"contentMenu": {
|
||||
"inviteLinks": null,
|
||||
"muteGroup": null,
|
||||
"unmuteGroup": null,
|
||||
"visitGroupPage": null
|
||||
@ -531,6 +535,7 @@
|
||||
"goal": null,
|
||||
"groupCreated": null,
|
||||
"in": null,
|
||||
"invite-links": null,
|
||||
"joinLeaveButton": {
|
||||
"iAmMember": null,
|
||||
"join": null,
|
||||
@ -628,10 +633,29 @@
|
||||
"button": {
|
||||
"tooltip": null
|
||||
},
|
||||
"comment-placeholder": null,
|
||||
"copy-code": null,
|
||||
"copy-success": null,
|
||||
"not-available": null,
|
||||
"your-code": null
|
||||
"create-error": null,
|
||||
"create-success": null,
|
||||
"delete-modal": {
|
||||
"message": null,
|
||||
"title": null
|
||||
},
|
||||
"generate-code": null,
|
||||
"generate-code-explanation": null,
|
||||
"group-invite-links": null,
|
||||
"invalidate": null,
|
||||
"invalidate-error": null,
|
||||
"invalidate-success": null,
|
||||
"invite-link-message-group": null,
|
||||
"invite-link-message-hidden-group": null,
|
||||
"invite-link-message-personal": null,
|
||||
"limit-reached": null,
|
||||
"my-invite-links": null,
|
||||
"no-links": null,
|
||||
"redeemed-count": null,
|
||||
"redeemed-count-0": null
|
||||
},
|
||||
"localeSwitch": {
|
||||
"tooltip": null
|
||||
|
||||
@ -244,7 +244,10 @@
|
||||
"length": null,
|
||||
"success": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"invited-by": null,
|
||||
"invited-by-and-to": null,
|
||||
"invited-to-hidden-group": null
|
||||
},
|
||||
"no-public-registrstion": {
|
||||
"title": null
|
||||
@ -509,6 +512,7 @@
|
||||
"categoriesTitle": null,
|
||||
"changeMemberRole": null,
|
||||
"contentMenu": {
|
||||
"inviteLinks": null,
|
||||
"muteGroup": null,
|
||||
"unmuteGroup": null,
|
||||
"visitGroupPage": null
|
||||
@ -531,6 +535,7 @@
|
||||
"goal": null,
|
||||
"groupCreated": null,
|
||||
"in": null,
|
||||
"invite-links": null,
|
||||
"joinLeaveButton": {
|
||||
"iAmMember": null,
|
||||
"join": null,
|
||||
@ -628,10 +633,29 @@
|
||||
"button": {
|
||||
"tooltip": null
|
||||
},
|
||||
"comment-placeholder": null,
|
||||
"copy-code": null,
|
||||
"copy-success": null,
|
||||
"not-available": null,
|
||||
"your-code": null
|
||||
"create-error": null,
|
||||
"create-success": null,
|
||||
"delete-modal": {
|
||||
"message": null,
|
||||
"title": null
|
||||
},
|
||||
"generate-code": null,
|
||||
"generate-code-explanation": null,
|
||||
"group-invite-links": null,
|
||||
"invalidate": null,
|
||||
"invalidate-error": null,
|
||||
"invalidate-success": null,
|
||||
"invite-link-message-group": null,
|
||||
"invite-link-message-hidden-group": null,
|
||||
"invite-link-message-personal": null,
|
||||
"limit-reached": null,
|
||||
"my-invite-links": null,
|
||||
"no-links": null,
|
||||
"redeemed-count": null,
|
||||
"redeemed-count-0": null
|
||||
},
|
||||
"localeSwitch": {
|
||||
"tooltip": null
|
||||
|
||||
@ -244,7 +244,10 @@
|
||||
"length": null,
|
||||
"success": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"invited-by": null,
|
||||
"invited-by-and-to": null,
|
||||
"invited-to-hidden-group": null
|
||||
},
|
||||
"no-public-registrstion": {
|
||||
"title": null
|
||||
@ -509,6 +512,7 @@
|
||||
"categoriesTitle": null,
|
||||
"changeMemberRole": null,
|
||||
"contentMenu": {
|
||||
"inviteLinks": null,
|
||||
"muteGroup": null,
|
||||
"unmuteGroup": null,
|
||||
"visitGroupPage": null
|
||||
@ -531,6 +535,7 @@
|
||||
"goal": null,
|
||||
"groupCreated": null,
|
||||
"in": null,
|
||||
"invite-links": null,
|
||||
"joinLeaveButton": {
|
||||
"iAmMember": null,
|
||||
"join": null,
|
||||
@ -628,10 +633,29 @@
|
||||
"button": {
|
||||
"tooltip": null
|
||||
},
|
||||
"comment-placeholder": null,
|
||||
"copy-code": null,
|
||||
"copy-success": null,
|
||||
"not-available": null,
|
||||
"your-code": null
|
||||
"create-error": null,
|
||||
"create-success": null,
|
||||
"delete-modal": {
|
||||
"message": null,
|
||||
"title": null
|
||||
},
|
||||
"generate-code": null,
|
||||
"generate-code-explanation": null,
|
||||
"group-invite-links": null,
|
||||
"invalidate": null,
|
||||
"invalidate-error": null,
|
||||
"invalidate-success": null,
|
||||
"invite-link-message-group": null,
|
||||
"invite-link-message-hidden-group": null,
|
||||
"invite-link-message-personal": null,
|
||||
"limit-reached": null,
|
||||
"my-invite-links": null,
|
||||
"no-links": null,
|
||||
"redeemed-count": null,
|
||||
"redeemed-count-0": null
|
||||
},
|
||||
"localeSwitch": {
|
||||
"tooltip": null
|
||||
|
||||
@ -244,7 +244,10 @@
|
||||
"length": null,
|
||||
"success": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"invited-by": null,
|
||||
"invited-by-and-to": null,
|
||||
"invited-to-hidden-group": null
|
||||
},
|
||||
"no-public-registrstion": {
|
||||
"title": null
|
||||
@ -509,6 +512,7 @@
|
||||
"categoriesTitle": null,
|
||||
"changeMemberRole": null,
|
||||
"contentMenu": {
|
||||
"inviteLinks": null,
|
||||
"muteGroup": null,
|
||||
"unmuteGroup": null,
|
||||
"visitGroupPage": null
|
||||
@ -531,6 +535,7 @@
|
||||
"goal": null,
|
||||
"groupCreated": null,
|
||||
"in": null,
|
||||
"invite-links": null,
|
||||
"joinLeaveButton": {
|
||||
"iAmMember": null,
|
||||
"join": null,
|
||||
@ -628,10 +633,29 @@
|
||||
"button": {
|
||||
"tooltip": null
|
||||
},
|
||||
"comment-placeholder": null,
|
||||
"copy-code": null,
|
||||
"copy-success": null,
|
||||
"not-available": null,
|
||||
"your-code": null
|
||||
"create-error": null,
|
||||
"create-success": null,
|
||||
"delete-modal": {
|
||||
"message": null,
|
||||
"title": null
|
||||
},
|
||||
"generate-code": null,
|
||||
"generate-code-explanation": null,
|
||||
"group-invite-links": null,
|
||||
"invalidate": null,
|
||||
"invalidate-error": null,
|
||||
"invalidate-success": null,
|
||||
"invite-link-message-group": null,
|
||||
"invite-link-message-hidden-group": null,
|
||||
"invite-link-message-personal": null,
|
||||
"limit-reached": null,
|
||||
"my-invite-links": null,
|
||||
"no-links": null,
|
||||
"redeemed-count": null,
|
||||
"redeemed-count-0": null
|
||||
},
|
||||
"localeSwitch": {
|
||||
"tooltip": null
|
||||
|
||||
@ -244,7 +244,10 @@
|
||||
"length": null,
|
||||
"success": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"invited-by": null,
|
||||
"invited-by-and-to": null,
|
||||
"invited-to-hidden-group": null
|
||||
},
|
||||
"no-public-registrstion": {
|
||||
"title": null
|
||||
@ -509,6 +512,7 @@
|
||||
"categoriesTitle": null,
|
||||
"changeMemberRole": null,
|
||||
"contentMenu": {
|
||||
"inviteLinks": null,
|
||||
"muteGroup": null,
|
||||
"unmuteGroup": null,
|
||||
"visitGroupPage": null
|
||||
@ -531,6 +535,7 @@
|
||||
"goal": null,
|
||||
"groupCreated": null,
|
||||
"in": null,
|
||||
"invite-links": null,
|
||||
"joinLeaveButton": {
|
||||
"iAmMember": null,
|
||||
"join": null,
|
||||
@ -628,10 +633,29 @@
|
||||
"button": {
|
||||
"tooltip": null
|
||||
},
|
||||
"comment-placeholder": null,
|
||||
"copy-code": null,
|
||||
"copy-success": null,
|
||||
"not-available": null,
|
||||
"your-code": null
|
||||
"create-error": null,
|
||||
"create-success": null,
|
||||
"delete-modal": {
|
||||
"message": null,
|
||||
"title": null
|
||||
},
|
||||
"generate-code": null,
|
||||
"generate-code-explanation": null,
|
||||
"group-invite-links": null,
|
||||
"invalidate": null,
|
||||
"invalidate-error": null,
|
||||
"invalidate-success": null,
|
||||
"invite-link-message-group": null,
|
||||
"invite-link-message-hidden-group": null,
|
||||
"invite-link-message-personal": null,
|
||||
"limit-reached": null,
|
||||
"my-invite-links": null,
|
||||
"no-links": null,
|
||||
"redeemed-count": null,
|
||||
"redeemed-count-0": null
|
||||
},
|
||||
"localeSwitch": {
|
||||
"tooltip": null
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ocelot-social/maintenance",
|
||||
"version": "3.5.3",
|
||||
"version": "3.6.0",
|
||||
"description": "Maintenance page for ocelot.social",
|
||||
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
|
||||
"author": "ocelot.social Community",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ocelot-social-webapp",
|
||||
"version": "3.5.3",
|
||||
"version": "3.6.0",
|
||||
"description": "ocelot.social Frontend",
|
||||
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
|
||||
"author": "ocelot.social Community",
|
||||
|
||||
@ -151,6 +151,27 @@ exports[`GroupProfileSlug given a puplic group – "yoga-practice" given a close
|
||||
</router-link-stub>
|
||||
<!---->
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<router-link-stub
|
||||
class="ds-menu-item-link"
|
||||
to="/groups/edit/g1/invites"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
|
||||
group.contentMenu.inviteLinks
|
||||
|
||||
</router-link-stub>
|
||||
<!---->
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
@ -3009,6 +3030,27 @@ exports[`GroupProfileSlug given a puplic group – "yoga-practice" given a curre
|
||||
</router-link-stub>
|
||||
<!---->
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<router-link-stub
|
||||
class="ds-menu-item-link"
|
||||
to="/groups/edit/g2/invites"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
|
||||
group.contentMenu.inviteLinks
|
||||
|
||||
</router-link-stub>
|
||||
<!---->
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
@ -6489,6 +6531,27 @@ exports[`GroupProfileSlug given a puplic group – "yoga-practice" given a hidde
|
||||
</router-link-stub>
|
||||
<!---->
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<router-link-stub
|
||||
class="ds-menu-item-link"
|
||||
to="/groups/edit/g0/invites"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
|
||||
group.contentMenu.inviteLinks
|
||||
|
||||
</router-link-stub>
|
||||
<!---->
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
</ds-flex-item>
|
||||
<ds-flex-item :width="{ base: '100%', md: 1 }">
|
||||
<transition name="slide-up" appear>
|
||||
<nuxt-child :group="group" />
|
||||
<nuxt-child :group="group" @update-invite-codes="updateInviteCodes" />
|
||||
</transition>
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
@ -39,9 +39,18 @@ export default {
|
||||
name: this.$t('group.members'),
|
||||
path: `/groups/edit/${this.group.id}/members`,
|
||||
},
|
||||
{
|
||||
name: this.$t('group.invite-links'),
|
||||
path: `/groups/edit/${this.group.id}/invites`,
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
group: {},
|
||||
}
|
||||
},
|
||||
async asyncData(context) {
|
||||
const {
|
||||
app,
|
||||
@ -62,5 +71,10 @@ export default {
|
||||
}
|
||||
return { group }
|
||||
},
|
||||
methods: {
|
||||
updateInviteCodes(inviteCodes) {
|
||||
this.group.inviteCodes = inviteCodes
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
167
webapp/pages/groups/edit/_id/__snapshots__/invites.spec.js.snap
Normal file
167
webapp/pages/groups/edit/_id/__snapshots__/invites.spec.js.snap
Normal file
@ -0,0 +1,167 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`invites.vue renders 1`] = `
|
||||
<div>
|
||||
<div>
|
||||
<article
|
||||
class="base-card"
|
||||
>
|
||||
<h3
|
||||
class="ds-heading ds-heading-h3"
|
||||
>
|
||||
invite-codes.group-invite-links
|
||||
</h3>
|
||||
|
||||
<div
|
||||
class="ds-space"
|
||||
style="margin-top: 32px; margin-bottom: 32px;"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="invitation-list"
|
||||
>
|
||||
<ul>
|
||||
<client-only-stub>
|
||||
<li
|
||||
class="invitation"
|
||||
>
|
||||
<div
|
||||
class="column1"
|
||||
>
|
||||
<div
|
||||
class="code"
|
||||
>
|
||||
|
||||
INVITE1
|
||||
|
||||
<span
|
||||
class="mdash"
|
||||
>
|
||||
—
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="comment"
|
||||
>
|
||||
Test invite 1
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span>
|
||||
|
||||
invite-codes.redeemed-count-0
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
aria-label="invite-codes.copy-code"
|
||||
class="copy-button base-button --icon-only --circle"
|
||||
disabled="disabled"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
|
||||
<button
|
||||
aria-label="invite-codes.invalidate"
|
||||
class="invalidate-button base-button --icon-only --circle"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</client-only-stub>
|
||||
</ul>
|
||||
|
||||
<div
|
||||
class="create-invitation create-invitation"
|
||||
>
|
||||
<div>
|
||||
invite-codes.generate-code-explanation
|
||||
</div>
|
||||
|
||||
<form
|
||||
class="generate-invite-code-form"
|
||||
>
|
||||
<div
|
||||
class="ds-form-item ds-input-size-base"
|
||||
>
|
||||
<label
|
||||
class="ds-input-label"
|
||||
style="display: none;"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</label>
|
||||
<div
|
||||
class="ds-input-wrap"
|
||||
>
|
||||
<!---->
|
||||
<input
|
||||
class="ds-input"
|
||||
name="comment"
|
||||
placeholder="invite-codes.comment-placeholder"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
/>
|
||||
<!---->
|
||||
</div>
|
||||
<transition-stub
|
||||
name="ds-input-error"
|
||||
>
|
||||
<div
|
||||
class="ds-input-error"
|
||||
style="display: none;"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</transition-stub>
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-label="invite-codes.generate-code"
|
||||
class="generate-invite-code base-button --icon-only --circle"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!---->
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
86
webapp/pages/groups/edit/_id/invites.spec.js
Normal file
86
webapp/pages/groups/edit/_id/invites.spec.js
Normal file
@ -0,0 +1,86 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/vue'
|
||||
|
||||
import invites from './invites.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('invites.vue', () => {
|
||||
let wrapper
|
||||
let mocks
|
||||
|
||||
beforeEach(() => {
|
||||
mocks = {
|
||||
$t: jest.fn((v) => v),
|
||||
$apollo: {
|
||||
mutate: jest.fn(),
|
||||
},
|
||||
$env: {
|
||||
NETWORK_NAME: 'test-network',
|
||||
INVITE_LINK_LIMIT: 5,
|
||||
},
|
||||
$toast: {
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
localVue,
|
||||
}
|
||||
})
|
||||
|
||||
const Wrapper = () => {
|
||||
return render(invites, {
|
||||
localVue,
|
||||
propsData: {
|
||||
group: {
|
||||
id: 'group1',
|
||||
name: 'Group 1',
|
||||
inviteCodes: [
|
||||
{
|
||||
code: 'INVITE1',
|
||||
comment: 'Test invite 1',
|
||||
redeemedByCount: 0,
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
code: 'INVITE2',
|
||||
comment: 'Test invite 2',
|
||||
redeemedByCount: 1,
|
||||
isValid: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
mocks,
|
||||
stubs: {
|
||||
'client-only': true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
it('renders', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
describe('when a new invite code is generated', () => {
|
||||
beforeEach(async () => {
|
||||
wrapper = Wrapper()
|
||||
const createButton = screen.getByLabelText('invite-codes.generate-code')
|
||||
await fireEvent.click(createButton)
|
||||
})
|
||||
|
||||
it('calls the mutation to generate a new invite code', () => {
|
||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith({
|
||||
mutation: expect.anything(),
|
||||
update: expect.anything(),
|
||||
variables: {
|
||||
groupId: 'group1',
|
||||
comment: '',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('shows a success message', () => {
|
||||
expect(mocks.$toast.success).toHaveBeenCalledWith('invite-codes.create-success')
|
||||
})
|
||||
})
|
||||
})
|
||||
81
webapp/pages/groups/edit/_id/invites.vue
Normal file
81
webapp/pages/groups/edit/_id/invites.vue
Normal file
@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div>
|
||||
<base-card>
|
||||
<ds-heading tag="h3">{{ $t('invite-codes.group-invite-links') }}</ds-heading>
|
||||
<ds-space margin="large" />
|
||||
<invitation-list
|
||||
@generate-invite-code="generateGroupInviteCode"
|
||||
@invalidate-invite-code="invalidateInviteCode"
|
||||
:inviteCodes="group.inviteCodes"
|
||||
:copy-message="
|
||||
group.groupType === 'hidden'
|
||||
? $t('invite-codes.invite-link-message-hidden-group', {
|
||||
network: $env.NETWORK_NAME,
|
||||
})
|
||||
: $t('invite-codes.invite-link-message-group', {
|
||||
groupName: group.name,
|
||||
network: $env.NETWORK_NAME,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</base-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import InvitationList from '~/components/_new/features/Invitations/InvitationList.vue'
|
||||
import { generateGroupInviteCode, invalidateInviteCode } from '~/graphql/InviteCode'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
InvitationList,
|
||||
},
|
||||
props: {
|
||||
group: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async generateGroupInviteCode(comment) {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: generateGroupInviteCode(),
|
||||
variables: {
|
||||
comment,
|
||||
groupId: this.group.id,
|
||||
},
|
||||
update: (_, { data: { generateGroupInviteCode } }) => {
|
||||
this.$emit('update-invite-codes', [...this.group.inviteCodes, generateGroupInviteCode])
|
||||
},
|
||||
})
|
||||
this.$toast.success(this.$t('invite-codes.create-success'))
|
||||
} catch (error) {
|
||||
this.$toast.error(this.$t('invite-codes.create-error', { error: error.message }))
|
||||
}
|
||||
},
|
||||
async invalidateInviteCode(code) {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: invalidateInviteCode(),
|
||||
variables: {
|
||||
code,
|
||||
},
|
||||
update: (_, { data: { _invalidateInviteCode } }) => {
|
||||
this.$emit(
|
||||
'update-invite-codes',
|
||||
this.group.inviteCodes.map((inviteCode) => ({
|
||||
...inviteCode,
|
||||
isValid: inviteCode.code === code ? false : inviteCode.isValid,
|
||||
})),
|
||||
)
|
||||
},
|
||||
})
|
||||
this.$toast.success(this.$t('invite-codes.invalidate-success'))
|
||||
} catch (error) {
|
||||
this.$toast.error(this.$t('invite-codes.invalidate-error', { error: error.message }))
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -1,14 +1,20 @@
|
||||
import Vuex from 'vuex'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import login from './login.vue'
|
||||
import LoginForm from '~/components/LoginForm/LoginForm.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const stubs = {
|
||||
'client-only': true,
|
||||
'nuxt-link': true,
|
||||
'router-link': true,
|
||||
}
|
||||
|
||||
const routerPushMock = jest.fn()
|
||||
const routerReplaceMock = jest.fn()
|
||||
const i18nSetMock = jest.fn()
|
||||
|
||||
describe('Login.vue', () => {
|
||||
let store
|
||||
let mocks
|
||||
@ -22,6 +28,14 @@ describe('Login.vue', () => {
|
||||
$t: jest.fn(),
|
||||
$i18n: {
|
||||
locale: () => 'en',
|
||||
set: i18nSetMock,
|
||||
},
|
||||
$route: {
|
||||
query: {},
|
||||
},
|
||||
$router: {
|
||||
replace: routerReplaceMock,
|
||||
push: routerPushMock,
|
||||
},
|
||||
}
|
||||
asyncData = false
|
||||
@ -73,5 +87,51 @@ describe('Login.vue', () => {
|
||||
wrapper = await Wrapper()
|
||||
expect(redirect).toHaveBeenCalledWith('/')
|
||||
})
|
||||
|
||||
describe('handle succcess', () => {
|
||||
beforeEach(async () => {
|
||||
asyncData = true
|
||||
tosVersion = '0.0.4'
|
||||
})
|
||||
|
||||
describe('with route query to invite code', () => {
|
||||
beforeEach(async () => {
|
||||
mocks.$route.query = {
|
||||
inviteCode: 'ABCDEF',
|
||||
}
|
||||
wrapper = await Wrapper()
|
||||
wrapper.findComponent(LoginForm).vm.$emit('success')
|
||||
})
|
||||
|
||||
it('calls i18n.set', () => {
|
||||
expect(i18nSetMock).toBeCalledWith('en')
|
||||
})
|
||||
|
||||
it('call router push to registration page', () => {
|
||||
expect(routerPushMock).toBeCalledWith({
|
||||
name: 'registration',
|
||||
query: {
|
||||
inviteCode: 'ABCDEF',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('without route query to invite code', () => {
|
||||
beforeEach(async () => {
|
||||
mocks.$route.query = {}
|
||||
wrapper = await Wrapper()
|
||||
wrapper.findComponent(LoginForm).vm.$emit('success')
|
||||
})
|
||||
|
||||
it('calls i18n.set', () => {
|
||||
expect(i18nSetMock).toBeCalledWith('en')
|
||||
})
|
||||
|
||||
it('call router push to registration page', () => {
|
||||
expect(routerReplaceMock).toBeCalledWith('/')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -32,7 +32,14 @@ export default {
|
||||
this.$i18n.set(this.user.locale || 'en')
|
||||
|
||||
try {
|
||||
await this.$router.replace(this.$route.query.path || '/')
|
||||
if (this.$route.query.inviteCode) {
|
||||
this.$router.push({
|
||||
name: 'registration',
|
||||
query: this.$route.query,
|
||||
})
|
||||
} else {
|
||||
await this.$router.replace(this.$route.query.path || '/')
|
||||
}
|
||||
} catch (err) {
|
||||
// throw new Error(`Problem handling something: ${err}.`);
|
||||
// TODO this is causing trouble - most likely due to double redirect on terms&conditions
|
||||
|
||||
@ -2,6 +2,7 @@ import Vuex from 'vuex'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Registration from './registration.vue'
|
||||
import Vue from 'vue'
|
||||
import { validateInviteCodeQuery, redeemInviteCodeMutation } from '~/graphql/inviteCodes'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
@ -12,6 +13,18 @@ const stubs = {
|
||||
'infinite-loading': true,
|
||||
}
|
||||
|
||||
const queryMock = jest.fn()
|
||||
const mutationMock = jest.fn()
|
||||
|
||||
const app = {
|
||||
apolloProvider: {
|
||||
defaultClient: {
|
||||
query: queryMock,
|
||||
mutate: mutationMock,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
describe('Registration', () => {
|
||||
let wrapper
|
||||
let Wrapper
|
||||
@ -20,6 +33,7 @@ describe('Registration', () => {
|
||||
let store
|
||||
let redirect
|
||||
let isLoggedIn
|
||||
let route
|
||||
|
||||
beforeEach(() => {
|
||||
mocks = {
|
||||
@ -39,6 +53,9 @@ describe('Registration', () => {
|
||||
asyncData = false
|
||||
isLoggedIn = false
|
||||
redirect = jest.fn()
|
||||
route = {
|
||||
query: {},
|
||||
}
|
||||
})
|
||||
|
||||
describe('mount', () => {
|
||||
@ -65,6 +82,8 @@ describe('Registration', () => {
|
||||
const aData = await Registration.asyncData({
|
||||
store,
|
||||
redirect,
|
||||
route,
|
||||
app,
|
||||
})
|
||||
Registration.data = function () {
|
||||
return { ...data, ...aData }
|
||||
@ -330,6 +349,181 @@ describe('Registration', () => {
|
||||
expect(redirect).toHaveBeenCalledWith('/')
|
||||
})
|
||||
|
||||
describe('already logged in', () => {
|
||||
beforeEach(async () => {
|
||||
asyncData = true
|
||||
isLoggedIn = true
|
||||
})
|
||||
|
||||
describe('route contains personal invite code', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
queryMock.mockResolvedValue({
|
||||
data: {
|
||||
validateInviteCode: {
|
||||
invitedTo: null,
|
||||
},
|
||||
},
|
||||
})
|
||||
route.query.inviteCode = 'ABCDEF'
|
||||
wrapper = await Wrapper()
|
||||
})
|
||||
|
||||
it('calls validate invite code', () => {
|
||||
expect(queryMock).toHaveBeenCalledWith({
|
||||
query: validateInviteCodeQuery,
|
||||
variables: {
|
||||
code: 'ABCDEF',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('redirects to index', () => {
|
||||
expect(redirect).toHaveBeenCalledWith('/')
|
||||
})
|
||||
|
||||
it('does not redeem the link', () => {
|
||||
expect(mutationMock).not.toBeCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('route contains group invite code to public group', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
queryMock.mockResolvedValue({
|
||||
data: {
|
||||
validateInviteCode: {
|
||||
invitedTo: {
|
||||
id: 'public-group',
|
||||
slug: 'public-group',
|
||||
groupType: 'public',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
mutationMock.mockResolvedValue({
|
||||
data: {
|
||||
redeemInviteCode: true,
|
||||
},
|
||||
})
|
||||
route.query.inviteCode = 'ABCDEF'
|
||||
wrapper = await Wrapper()
|
||||
})
|
||||
|
||||
it('calls validate invite code', () => {
|
||||
expect(queryMock).toHaveBeenCalledWith({
|
||||
query: validateInviteCodeQuery,
|
||||
variables: {
|
||||
code: 'ABCDEF',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('redirects to group', () => {
|
||||
expect(redirect).toHaveBeenCalledWith('/groups/public-group/public-group')
|
||||
})
|
||||
|
||||
it('redeems the code', () => {
|
||||
expect(mutationMock).toBeCalledWith({
|
||||
mutation: redeemInviteCodeMutation,
|
||||
variables: {
|
||||
code: 'ABCDEF',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('route contains group invite code to closed group', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
queryMock.mockResolvedValue({
|
||||
data: {
|
||||
validateInviteCode: {
|
||||
invitedTo: {
|
||||
id: 'closed-group',
|
||||
slug: 'closed-group',
|
||||
groupType: 'closed',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
mutationMock.mockResolvedValue({
|
||||
data: {
|
||||
redeemInviteCode: true,
|
||||
},
|
||||
})
|
||||
route.query.inviteCode = 'ABCDEF'
|
||||
wrapper = await Wrapper()
|
||||
})
|
||||
|
||||
it('calls validate invite code', () => {
|
||||
expect(queryMock).toHaveBeenCalledWith({
|
||||
query: validateInviteCodeQuery,
|
||||
variables: {
|
||||
code: 'ABCDEF',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('redirects to index', () => {
|
||||
expect(redirect).toHaveBeenCalledWith('/')
|
||||
})
|
||||
|
||||
it('redeems the code', () => {
|
||||
expect(mutationMock).toBeCalledWith({
|
||||
mutation: redeemInviteCodeMutation,
|
||||
variables: {
|
||||
code: 'ABCDEF',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('route contains group invite code to public group, but redeem throws', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
queryMock.mockResolvedValue({
|
||||
data: {
|
||||
validateInviteCode: {
|
||||
invitedTo: {
|
||||
id: 'public-group',
|
||||
slug: 'public-group',
|
||||
groupType: 'public',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
mutationMock.mockRejectedValue({
|
||||
error: 'Aua!',
|
||||
})
|
||||
route.query.inviteCode = 'ABCDEF'
|
||||
wrapper = await Wrapper()
|
||||
})
|
||||
|
||||
it('calls validate invite code', () => {
|
||||
expect(queryMock).toHaveBeenCalledWith({
|
||||
query: validateInviteCodeQuery,
|
||||
variables: {
|
||||
code: 'ABCDEF',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('redirects to index', () => {
|
||||
expect(redirect).toHaveBeenCalledWith('/')
|
||||
})
|
||||
|
||||
it('redeems the code', () => {
|
||||
expect(mutationMock).toBeCalledWith({
|
||||
mutation: redeemInviteCodeMutation,
|
||||
variables: {
|
||||
code: 'ABCDEF',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// copied from webapp/components/Registration/Signup.spec.js as testing template
|
||||
// describe('with invitation code', () => {
|
||||
// let action
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
<script>
|
||||
import registrationConstants from '~/constants/registrationBranded.js'
|
||||
import RegistrationSlider from '~/components/Registration/RegistrationSlider'
|
||||
import { validateInviteCodeQuery, redeemInviteCodeMutation } from '~/graphql/inviteCodes'
|
||||
|
||||
export default {
|
||||
layout: registrationConstants.LAYOUT,
|
||||
@ -34,8 +35,41 @@ export default {
|
||||
inviteRegistration: this.$env.INVITE_REGISTRATION === true, // for 'false' in .env INVITE_REGISTRATION is of type undefined and not(!) boolean false, because of internal handling
|
||||
}
|
||||
},
|
||||
asyncData({ store, redirect }) {
|
||||
async asyncData({ store, route, app, redirect }) {
|
||||
// http://localhost:3000/registration?method=invite-code&inviteCode=PEY8FN
|
||||
if (store.getters['auth/isLoggedIn']) {
|
||||
const {
|
||||
query: { inviteCode: code },
|
||||
} = route
|
||||
if (code) {
|
||||
const {
|
||||
apolloProvider: { defaultClient: client },
|
||||
} = app
|
||||
try {
|
||||
const result = await client.query({
|
||||
query: validateInviteCodeQuery,
|
||||
variables: { code },
|
||||
})
|
||||
const {
|
||||
data: {
|
||||
validateInviteCode: { invitedTo: group },
|
||||
},
|
||||
} = result
|
||||
if (group) {
|
||||
const mutationResult = await client.mutate({
|
||||
mutation: redeemInviteCodeMutation,
|
||||
variables: { code },
|
||||
})
|
||||
if (mutationResult.data.redeemInviteCode && group.groupType === 'public') {
|
||||
redirect(`/groups/${group.id}/${group.slug}`)
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (_err) {
|
||||
redirect('/')
|
||||
return
|
||||
}
|
||||
}
|
||||
redirect('/')
|
||||
}
|
||||
},
|
||||
|
||||
@ -18,6 +18,9 @@ export const mutations = {
|
||||
SET_USER(state, user) {
|
||||
state.user = user || null
|
||||
},
|
||||
SET_USER_PARTIAL(state, user) {
|
||||
state.user = { ...state.user, ...user }
|
||||
},
|
||||
SET_TOKEN(state, token) {
|
||||
state.token = token || null
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user