Skip to content

Commit f7d5f60

Browse files
committed
feat: Create invitation links
Signed-off-by: Kostiantyn Miakshyn <molodchick@gmail.com>
1 parent bf2fa16 commit f7d5f60

File tree

14 files changed

+472
-115
lines changed

14 files changed

+472
-115
lines changed

appinfo/routes.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
['name' => 'contacts#direct', 'url' => '/direct/contact/{contact}', 'verb' => 'GET'],
1111
['name' => 'contacts#directcircle', 'url' => '/direct/circle/{singleId}', 'verb' => 'GET'],
1212
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
13+
['name' => 'page#joinInvitation', 'url' => '/join/{invitationCode}', 'verb' => 'GET'],
1314
['name' => 'page#index', 'url' => '/{group}', 'verb' => 'GET', 'postfix' => 'group'],
1415
['name' => 'page#index', 'url' => '/{group}/{contact}', 'verb' => 'GET', 'postfix' => 'group.contact'],
1516
['name' => 'social_api#update_contact', 'url' => '/api/v1/social/avatar/{network}/{addressbookId}/{contactId}', 'verb' => 'PUT'],

lib/Controller/PageController.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,17 @@ public function index(): TemplateResponse {
8383

8484
return new TemplateResponse(Application::APP_ID, 'main');
8585
}
86+
87+
/**
88+
* @NoAdminRequired
89+
* @NoCSRFRequired
90+
*/
91+
public function joinInvitation(string $invitationCode): TemplateResponse {
92+
$this->initialStateService->provideInitialState(Application::APP_ID, 'invitationCode', $invitationCode);
93+
94+
Util::addStyle(Application::APP_ID, 'contacts-join-invitation');
95+
Util::addScript(Application::APP_ID, 'contacts-join-invitation');
96+
97+
return new TemplateResponse(Application::APP_ID, 'join-invitation');
98+
}
8699
}

src/components/CircleDetails/CircleConfigs.vue

Lines changed: 2 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -5,40 +5,25 @@
55

66
<template>
77
<ul>
8-
<li v-for="(configs, title) in PUBLIC_CIRCLE_CONFIG" :key="title" class="circle-config">
8+
<li v-for="(config, title) in PUBLIC_CIRCLE_CONFIG" :key="title" class="circle-config">
99
<ContentHeading class="circle-config__title">
1010
{{ title }}
1111
</ContentHeading>
1212

13-
<ul class="circle-config__list">
14-
<CheckboxRadioSwitch
15-
v-for="(label, config) in configs"
16-
:key="'circle-config' + config"
17-
:model-value="isChecked(config)"
18-
:loading="loading === config"
19-
:disabled="loading !== false"
20-
wrapper-element="li"
21-
@update:model-value="onChange(config, $event)">
22-
{{ label }}
23-
</CheckboxRadioSwitch>
24-
</ul>
13+
<component :is="config.component" v-bind="config.props" :circle="circle" />
2514
</li>
2615
</ul>
2716
</template>
2817

2918
<script>
30-
import { showError } from '@nextcloud/dialogs'
31-
import { NcCheckboxRadioSwitch as CheckboxRadioSwitch } from '@nextcloud/vue'
3219
import ContentHeading from './ContentHeading.vue'
3320
import Circle from '../../models/circle.ts'
3421
import { PUBLIC_CIRCLE_CONFIG } from '../../models/constants.ts'
35-
import { CircleEdit, editCircle } from '../../services/circles.ts'
3622
3723
export default {
3824
name: 'CircleConfigs',
3925
4026
components: {
41-
CheckboxRadioSwitch,
4227
ContentHeading,
4328
},
4429
@@ -52,45 +37,8 @@ export default {
5237
data() {
5338
return {
5439
PUBLIC_CIRCLE_CONFIG,
55-
56-
loading: false,
5740
}
5841
},
59-
60-
methods: {
61-
isChecked(config) {
62-
return (this.circle.config & config) !== 0
63-
},
64-
65-
/**
66-
* On toggle, add or remove the config bitwise
67-
*
68-
* @param {CircleConfig} config the circle config to manage
69-
* @param {boolean} checked checked or not
70-
*/
71-
async onChange(config, checked) {
72-
this.logger.debug(`Circle config ${config} is set to ${checked}`)
73-
74-
this.loading = config
75-
const prevConfig = this.circle.config
76-
if (checked) {
77-
config = prevConfig | config
78-
} else {
79-
config = prevConfig & ~config
80-
}
81-
82-
try {
83-
const circleData = await editCircle(this.circle.id, CircleEdit.Config, config)
84-
// eslint-disable-next-line vue/no-mutating-props
85-
this.circle.config = circleData.config
86-
} catch (error) {
87-
console.error('Unable to edit circle config', prevConfig, config, error)
88-
showError(t('contacts', 'An error happened during the config change'))
89-
} finally {
90-
this.loading = false
91-
}
92-
},
93-
},
9442
}
9543
</script>
9644

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<template>
7+
<ul class="circle-config__list">
8+
<NcCheckboxRadioSwitch
9+
v-for="(label, config) in configs"
10+
:key="'circle-config' + config"
11+
:model-value="isChecked(config)"
12+
:loading="loading === config"
13+
:disabled="loading !== false"
14+
wrapper-element="li"
15+
@update:model-value="onChange(config, $event)">
16+
{{ label }}
17+
</NcCheckboxRadioSwitch>
18+
</ul>
19+
</template>
20+
21+
<script>
22+
import { showError } from '@nextcloud/dialogs'
23+
import { t } from '@nextcloud/l10n'
24+
import { NcCheckboxRadioSwitch } from '@nextcloud/vue'
25+
import Circle from '../../../models/circle.ts'
26+
import { CircleEdit, editCircle } from '../../../services/circles.ts'
27+
28+
export default {
29+
name: 'CircleConfigCheckboxesList',
30+
31+
components: {
32+
NcCheckboxRadioSwitch,
33+
},
34+
35+
props: {
36+
circle: {
37+
type: Circle,
38+
required: true,
39+
},
40+
41+
configs: {
42+
type: Object,
43+
required: true,
44+
},
45+
},
46+
47+
data() {
48+
return {
49+
loading: false,
50+
}
51+
},
52+
53+
methods: {
54+
isChecked(config) {
55+
return (this.circle.config & config) !== 0
56+
},
57+
58+
/**
59+
* On toggle, add or remove the config bitwise
60+
*
61+
* @param {CircleConfig} config the circle config to manage
62+
* @param {boolean} checked checked or not
63+
*/
64+
async onChange(config, checked) {
65+
this.logger.debug(`Circle config ${config} is set to ${checked}`)
66+
67+
this.loading = config
68+
const prevConfig = this.circle.config
69+
if (checked) {
70+
config = prevConfig | config
71+
} else {
72+
config = prevConfig & ~config
73+
}
74+
75+
try {
76+
const circleData = await editCircle(this.circle.id, CircleEdit.Config, config)
77+
// eslint-disable-next-line vue/no-mutating-props
78+
this.circle.config = circleData.config
79+
} catch (error) {
80+
console.error('Unable to edit circle config', prevConfig, config, error)
81+
showError(t('contacts', 'An error happened during the config change'))
82+
} finally {
83+
this.loading = false
84+
}
85+
},
86+
},
87+
}
88+
</script>
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<template>
7+
<NcActions :inline="3" force-name variant="tertiary">
8+
<NcActionButton
9+
v-if="!invitationUrl"
10+
@click="createInvitationLink()">
11+
<template #icon>
12+
<LinkPlus :size="20" />
13+
</template>
14+
{{ t('contacts', 'Create link') }}
15+
</NcActionButton>
16+
<template v-else>
17+
<NcActionLink
18+
:href="invitationUrl"
19+
:icon="copyLinkIcon"
20+
@click.stop.prevent="copyToClipboard(invitationUrl)">
21+
{{ copyButtonText }}
22+
</NcActionLink>
23+
24+
<NcActionButton
25+
@click="confirm(
26+
t('contacts', 'This action will make it impossible to join the team using the current link. Do we really want to change the link?'),
27+
() => createInvitationLink(),
28+
)">
29+
<template #icon>
30+
<Autorenew :size="20" />
31+
</template>
32+
{{ t('contacts', 'Reset link') }}
33+
</NcActionButton>
34+
35+
<NcActionButton
36+
@click="confirm(
37+
t('contacts', 'This action will make it impossible to join the team using the current link. Do we really want to delete the link?'),
38+
() => revokeInvitationLink(),
39+
)">
40+
<template #icon>
41+
<LinkOff :size="20" />
42+
</template>
43+
{{ t('contacts', 'Reject link') }}
44+
</NcActionButton>
45+
</template>
46+
</NcActions>
47+
</template>
48+
49+
<script>
50+
import { generateUrl, getBaseUrl } from '@nextcloud/router'
51+
import { NcActionButton, NcActionLink, NcActions } from '@nextcloud/vue'
52+
import Autorenew from 'vue-material-design-icons/Autorenew.vue'
53+
import LinkOff from 'vue-material-design-icons/LinkOff.vue'
54+
import LinkPlus from 'vue-material-design-icons/LinkPlus.vue'
55+
import CopyToClipboardMixin from '../../../mixins/CopyToClipboardMixin.js'
56+
import Circle from '../../../models/circle.ts'
57+
58+
export default {
59+
name: 'InvitationLink',
60+
components: {
61+
LinkOff,
62+
Autorenew,
63+
LinkPlus,
64+
NcActions,
65+
NcActionLink,
66+
NcActionButton,
67+
},
68+
69+
mixins: [CopyToClipboardMixin],
70+
props: {
71+
circle: {
72+
type: Circle,
73+
required: true,
74+
},
75+
},
76+
77+
computed: {
78+
invitationUrl() {
79+
if (!this.circle.invitationCode) {
80+
return null
81+
}
82+
83+
return getBaseUrl() + generateUrl(
84+
'apps/contacts/join/{invitationCode}',
85+
{ invitationCode: this.circle.invitationCode.match(/.{1,4}/g).join('-') },
86+
)
87+
},
88+
89+
copyButtonText() {
90+
if (this.copied) {
91+
return this.copySuccess
92+
? t('contacts', 'Copied')
93+
: t('contacts', 'Could not copy')
94+
}
95+
return t('contacts', 'Copy link')
96+
},
97+
},
98+
99+
methods: {
100+
async createInvitationLink() {
101+
const circleId = this.circle.id
102+
await this.$store.dispatch('createInvitationLink', { circleId })
103+
104+
await this.copyToClipboard(this.invitationUrl)
105+
},
106+
107+
async revokeInvitationLink() {
108+
const circleId = this.circle.id
109+
await this.$store.dispatch('revokeInvitationLink', { circleId })
110+
},
111+
112+
confirm(message, action) {
113+
if (window.confirm(message)) {
114+
action()
115+
}
116+
},
117+
},
118+
}
119+
</script>

0 commit comments

Comments
 (0)