mirror of https://github.com/nextcloud/contacts
Refactor circle actions
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
This commit is contained in:
parent
bb5f38e923
commit
9facd8fbf3
|
@ -36,34 +36,36 @@
|
|||
</EmptyContent>
|
||||
</AppContentDetails>
|
||||
|
||||
<!-- not a member -->
|
||||
<AppContentDetails v-else-if="!circle.isMember">
|
||||
<EmptyContent v-if="!loadingJoin" icon="icon-circles">
|
||||
{{ t('contacts', 'You are not a member of this circle') }}
|
||||
|
||||
<!-- Only show the join button if the circle is accepting requests -->
|
||||
<template v-if="circle.canJoin" #desc>
|
||||
<button :disabled="loadingJoin" class="primary" @click="requestJoin">
|
||||
{{ t('contacts', 'Request to join') }}
|
||||
</button>
|
||||
</template>
|
||||
</EmptyContent>
|
||||
|
||||
<EmptyContent v-else-if="circle.isPendingJoin" icon="icon-loading">
|
||||
{{ t('contacts', 'Your request to join this circle is pending approval') }}
|
||||
</EmptyContent>
|
||||
|
||||
<EmptyContent v-else icon="icon-loading">
|
||||
{{ t('contacts', 'Joining circle') }}
|
||||
</EmptyContent>
|
||||
</AppContentDetails>
|
||||
|
||||
<template v-else>
|
||||
<!-- member list -->
|
||||
<MemberList :list="members" />
|
||||
|
||||
<!-- main contacts details -->
|
||||
<CircleDetails :circle-id="selectedCircle" />
|
||||
<CircleDetails :circle="circle">
|
||||
<!-- not a member -->
|
||||
<template v-if="!circle.isMember">
|
||||
<!-- Join request in progress -->
|
||||
<EmptyContent v-if="loadingJoin" icon="icon-loading">
|
||||
{{ t('contacts', 'Joining circle') }}
|
||||
</EmptyContent>
|
||||
|
||||
<!-- Pending request validation -->
|
||||
<EmptyContent v-else-if="circle.isPendingJoin" icon="icon-loading">
|
||||
{{ t('contacts', 'Your request to join this circle is pending approval') }}
|
||||
</EmptyContent>
|
||||
|
||||
<EmptyContent v-else icon="icon-circles">
|
||||
{{ t('contacts', 'You are not a member of this circle') }}
|
||||
|
||||
<!-- Only show the join button if the circle is accepting requests -->
|
||||
<template v-if="circle.canJoin" #desc>
|
||||
<button :disabled="loadingJoin" class="primary" @click="requestJoin">
|
||||
{{ t('contacts', 'Request to join') }}
|
||||
</button>
|
||||
</template>
|
||||
</EmptyContent>
|
||||
</template>
|
||||
</CircleDetails>
|
||||
</template>
|
||||
</div>
|
||||
</AppContent>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
- @copyright Copyright (c) 2021 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
-
|
||||
- @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
-
|
||||
|
@ -25,7 +25,7 @@
|
|||
:to="circle.router"
|
||||
:title="circle.displayName"
|
||||
:icon="circle.icon">
|
||||
<template v-if="loading" slot="actions">
|
||||
<template v-if="loadingAction" slot="actions">
|
||||
<ActionText icon="icon-loading-small">
|
||||
{{ t('contacts', 'Loading …') }}
|
||||
</ActionText>
|
||||
|
@ -50,7 +50,7 @@
|
|||
<!-- leave circle -->
|
||||
<ActionButton
|
||||
v-if="circle.canLeave"
|
||||
@click="leaveCircle">
|
||||
@click="confirmLeaveCircle">
|
||||
{{ t('contacts', 'Leave circle') }}
|
||||
<ExitToApp slot="icon"
|
||||
:size="16"
|
||||
|
@ -71,7 +71,7 @@
|
|||
<ActionButton
|
||||
v-if="circle.canDelete"
|
||||
icon="icon-delete"
|
||||
@click="deleteCircle">
|
||||
@click="confirmDeleteCircle">
|
||||
{{ t('contacts', 'Delete') }}
|
||||
</ActionButton>
|
||||
</template>
|
||||
|
@ -83,8 +83,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
|
||||
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
|
||||
import ActionLink from '@nextcloud/vue/dist/Components/ActionLink'
|
||||
import ActionText from '@nextcloud/vue/dist/Components/ActionText'
|
||||
|
@ -93,10 +91,8 @@ import AppNavigationItem from '@nextcloud/vue/dist/Components/AppNavigationItem'
|
|||
import ExitToApp from 'vue-material-design-icons/ExitToApp'
|
||||
import LocationEnter from 'vue-material-design-icons/LocationEnter'
|
||||
|
||||
import { joinCircle } from '../../services/circles.ts'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import Circle from '../../models/circle.ts'
|
||||
import CopyToClipboardMixin from '../../mixins/CopyToClipboardMixin'
|
||||
import CircleActionsMixin from '../../mixins/CircleActionsMixin'
|
||||
|
||||
export default {
|
||||
name: 'CircleNavigationItem',
|
||||
|
@ -111,7 +107,7 @@ export default {
|
|||
LocationEnter,
|
||||
},
|
||||
|
||||
mixins: [CopyToClipboardMixin],
|
||||
mixins: [CircleActionsMixin],
|
||||
|
||||
props: {
|
||||
circle: {
|
||||
|
@ -120,87 +116,10 @@ export default {
|
|||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
copyButtonText() {
|
||||
if (this.copied) {
|
||||
return this.copySuccess
|
||||
? t('contacts', 'Copied')
|
||||
: t('contacts', 'Could not copy')
|
||||
}
|
||||
return t('contacts', 'Copy link')
|
||||
},
|
||||
|
||||
circleUrl() {
|
||||
const route = this.$router.resolve(this.circle.router)
|
||||
return window.location.origin + route.href
|
||||
},
|
||||
|
||||
joinButtonTitle() {
|
||||
if (this.circle.requireJoinAccept) {
|
||||
return t('contacts', 'Request to join')
|
||||
}
|
||||
return t('contacts', 'Join circle')
|
||||
},
|
||||
|
||||
memberCount() {
|
||||
return Object.values(this.circle?.members || []).length
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
// Trigger the entity picker view
|
||||
async addMemberToCircle() {
|
||||
await this.$router.push(this.circle.router)
|
||||
emit('contacts:circles:append', this.circle.id)
|
||||
},
|
||||
|
||||
async joinCircle() {
|
||||
this.loading = true
|
||||
try {
|
||||
await joinCircle(this.circle.id)
|
||||
} catch (error) {
|
||||
showError(t('contacts', 'Unable to join the circle'))
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
async leaveCircle() {
|
||||
this.loading = true
|
||||
const member = this.circle.initiator
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('deleteMemberFromCircle', {
|
||||
member,
|
||||
leave: true,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Could not leave the circle', member, error)
|
||||
showError(t('contacts', 'Could not leave the circle {displayName}', this.circle))
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
async deleteCircle() {
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
this.$store.dispatch('deleteCircle', this.circle.id)
|
||||
} catch (error) {
|
||||
showError(t('contacts', 'Unable to delete the circle'))
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -69,7 +69,7 @@
|
|||
</AppNavigationCounter>
|
||||
</AppNavigationItem>
|
||||
|
||||
<AppNavigationItem
|
||||
<AppNavigationCaption
|
||||
id="newgroup"
|
||||
:force-menu="true"
|
||||
:menu-open.sync="isNewGroupMenuOpen"
|
||||
|
@ -85,7 +85,7 @@
|
|||
:placeholder="t('contacts','Group name')"
|
||||
@submit.prevent.stop="createNewGroup" />
|
||||
</template>
|
||||
</AppNavigationItem>
|
||||
</AppNavigationCaption>
|
||||
|
||||
<!-- Custom groups -->
|
||||
<GroupNavigationItem
|
||||
|
@ -101,7 +101,7 @@
|
|||
icon=""
|
||||
@click="onToggleGroups" />
|
||||
|
||||
<AppNavigationItem
|
||||
<AppNavigationCaption
|
||||
id="newcircle"
|
||||
:force-menu="true"
|
||||
:menu-open.sync="isNewCircleMenuOpen"
|
||||
|
@ -117,7 +117,7 @@
|
|||
:placeholder="t('contacts','Circle name')"
|
||||
@submit.prevent.stop="createNewCircle" />
|
||||
</template>
|
||||
</AppNavigationItem>
|
||||
</AppNavigationCaption>
|
||||
|
||||
<!-- Circles -->
|
||||
<CircleNavigationItem
|
||||
|
@ -152,6 +152,7 @@ import AppNavigation from '@nextcloud/vue/dist/Components/AppNavigation'
|
|||
import AppNavigationCounter from '@nextcloud/vue/dist/Components/AppNavigationCounter'
|
||||
import AppNavigationItem from '@nextcloud/vue/dist/Components/AppNavigationItem'
|
||||
import AppNavigationSettings from '@nextcloud/vue/dist/Components/AppNavigationSettings'
|
||||
import AppNavigationCaption from '@nextcloud/vue/dist/Components/AppNavigationCaption'
|
||||
|
||||
import naturalCompare from 'string-natural-compare'
|
||||
|
||||
|
@ -171,6 +172,7 @@ export default {
|
|||
AppNavigationCounter,
|
||||
AppNavigationItem,
|
||||
AppNavigationSettings,
|
||||
AppNavigationCaption,
|
||||
CircleNavigationItem,
|
||||
GroupNavigationItem,
|
||||
SettingsSection,
|
||||
|
|
|
@ -44,44 +44,101 @@
|
|||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
name="displayname"
|
||||
@input="debounceUpdateCircle">
|
||||
@input="onDisplayNameChangeDebounce">
|
||||
|
||||
<!-- org, title -->
|
||||
<template #subtitle>
|
||||
<template v-if="!circle.isOwner" #subtitle>
|
||||
{{ t('contacts', 'Circle owned by {owner}', { owner: circle.owner.displayName}) }}
|
||||
</template>
|
||||
|
||||
<!-- actions -->
|
||||
<template #actions>
|
||||
<Actions>
|
||||
<!-- leave circle -->
|
||||
<ActionButton
|
||||
v-if="circle.canLeave"
|
||||
@click="confirmLeaveCircle">
|
||||
{{ t('contacts', 'Leave circle') }}
|
||||
<ExitToApp slot="icon"
|
||||
:size="16"
|
||||
decorative />
|
||||
</ActionButton>
|
||||
|
||||
<!-- join circle -->
|
||||
<ActionButton
|
||||
v-else-if="!circle.isMember && circle.canJoin"
|
||||
@click="joinCircle">
|
||||
{{ joinButtonTitle }}
|
||||
<LocationEnter slot="icon"
|
||||
:size="16"
|
||||
decorative />
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
<Actions>
|
||||
<!-- copy circle link -->
|
||||
<ActionLink
|
||||
:href="circleUrl"
|
||||
:icon="copyLoading ? 'icon-loading-small' : 'icon-public'"
|
||||
@click.stop.prevent="copyToClipboard(circleUrl)">
|
||||
{{ copyButtonText }}
|
||||
</ActionLink>
|
||||
</Actions>
|
||||
</template>
|
||||
|
||||
<!-- menu actions -->
|
||||
<template #actions-menu>
|
||||
<!-- delete circle -->
|
||||
<ActionButton
|
||||
v-if="circle.canDelete"
|
||||
icon="icon-delete"
|
||||
@click="confirmDeleteCircle">
|
||||
{{ t('contacts', 'Delete') }}
|
||||
</ActionButton>
|
||||
</template>
|
||||
</DetailsHeader>
|
||||
|
||||
<section class="circle-details-section">
|
||||
<ContentHeading>{{ t('contacts', 'Description') }}</ContentHeading>
|
||||
<ContentHeading :loading="loadingDescription">
|
||||
{{ t('contacts', 'Description') }}
|
||||
</ContentHeading>
|
||||
|
||||
<RichContenteditable class="circle-details-section__description"
|
||||
:value="circle.description"
|
||||
<RichContenteditable
|
||||
:value.sync="circle.description"
|
||||
:auto-complete="onAutocomplete"
|
||||
:maxlength="1024"
|
||||
:multiline="true"
|
||||
:disabled="loading"
|
||||
:placeholder="t('contacts', 'Enter a description for the circle')"
|
||||
@submit="onDescriptionSubmit" />
|
||||
:contenteditable="circle.isOwner"
|
||||
:placeholder="descriptionPlaceholder"
|
||||
class="circle-details-section__description"
|
||||
@update:value="onDescriptionChangeDebounce" />
|
||||
</section>
|
||||
|
||||
<section class="circle-details-section">
|
||||
<section v-if="circle.isOwner" class="circle-details-section">
|
||||
<CircleConfigs class="circle-details-section__configs" :circle="circle" />
|
||||
</section>
|
||||
|
||||
<section v-else>
|
||||
<slot />
|
||||
</section>
|
||||
</AppContentDetails>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import debounce from 'debounce'
|
||||
|
||||
import Actions from '@nextcloud/vue/dist/Components/Actions'
|
||||
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
|
||||
import ActionLink from '@nextcloud/vue/dist/Components/ActionLink'
|
||||
import AppContentDetails from '@nextcloud/vue/dist/Components/AppContentDetails'
|
||||
import Avatar from '@nextcloud/vue/dist/Components/Avatar'
|
||||
import RichContenteditable from '@nextcloud/vue/dist/Components/RichContenteditable'
|
||||
|
||||
import ExitToApp from 'vue-material-design-icons/ExitToApp'
|
||||
import LocationEnter from 'vue-material-design-icons/LocationEnter'
|
||||
|
||||
import { CircleEdit, editCircle } from '../services/circles.ts'
|
||||
import CircleActionsMixin from '../mixins/CircleActionsMixin'
|
||||
import DetailsHeader from './DetailsHeader'
|
||||
import CircleConfigs from './CircleDetails/CircleConfigs'
|
||||
import ContentHeading from './CircleDetails/ContentHeading'
|
||||
|
@ -90,26 +147,36 @@ export default {
|
|||
name: 'CircleDetails',
|
||||
|
||||
components: {
|
||||
ActionButton,
|
||||
ActionLink,
|
||||
Actions,
|
||||
AppContentDetails,
|
||||
Avatar,
|
||||
CircleConfigs,
|
||||
ContentHeading,
|
||||
DetailsHeader,
|
||||
ExitToApp,
|
||||
LocationEnter,
|
||||
RichContenteditable,
|
||||
},
|
||||
|
||||
props: {
|
||||
circleId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
mixins: [CircleActionsMixin],
|
||||
|
||||
data() {
|
||||
return {
|
||||
loadingDescription: false,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
circle() {
|
||||
return this.$store.getters.getCircle(this.circleId)
|
||||
descriptionPlaceholder() {
|
||||
if (this.circle.description.trim() === '') {
|
||||
return t('contacts', 'There is no description for this circle')
|
||||
}
|
||||
return t('contacts', 'Enter a description for the circle')
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Autocomplete @mentions on the description
|
||||
|
@ -122,8 +189,34 @@ export default {
|
|||
callback([])
|
||||
},
|
||||
|
||||
onDescriptionSubmit() {
|
||||
console.info(...arguments)
|
||||
onDescriptionChangeDebounce: debounce(function() {
|
||||
this.onDescriptionChange(...arguments)
|
||||
}, 500),
|
||||
async onDescriptionChange(description) {
|
||||
this.loadingDescription = true
|
||||
try {
|
||||
await editCircle(this.circle.id, CircleEdit.Description, description)
|
||||
} catch (error) {
|
||||
console.error('Unable to edit circle description', description, error)
|
||||
showError(t('contacts', 'An error happened during description sync'))
|
||||
} finally {
|
||||
this.loadingDescription = false
|
||||
}
|
||||
},
|
||||
|
||||
onDisplayNameChangeDebounce: debounce(function() {
|
||||
this.onDisplayNameChange(...arguments)
|
||||
}, 500),
|
||||
async onDisplayNameChange(description) {
|
||||
this.loadingDescription = true
|
||||
try {
|
||||
await editCircle(this.circle.id, CircleEdit.Description, description)
|
||||
} catch (error) {
|
||||
console.error('Unable to edit circle description', description, error)
|
||||
showError(t('contacts', 'An error happened during description sync'))
|
||||
} finally {
|
||||
this.loadingDescription = false
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -133,17 +226,16 @@ export default {
|
|||
.app-content-details {
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
padding: 0 80px;
|
||||
}
|
||||
|
||||
.circle-details-section {
|
||||
padding: 0 80px;
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
&__description {
|
||||
max-width: 400px;
|
||||
max-width: 800px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -28,31 +28,34 @@
|
|||
</ContentHeading>
|
||||
|
||||
<ul class="circle-config__list">
|
||||
<CheckboxRadio v-for="(label, config) in configs"
|
||||
<CheckboxRadioSwitch v-for="(label, config) in configs"
|
||||
:key="'circle-config' + config"
|
||||
:checked="isChecked(config)"
|
||||
:loading="loading === config"
|
||||
:disabled="loading !== false"
|
||||
wrapper-element="li"
|
||||
@update:checked="onChange(config, $event)">
|
||||
{{ label }}
|
||||
</CheckboxRadio>
|
||||
</CheckboxRadioSwitch>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CheckboxRadio from '@nextcloud/vue/dist/Components/CheckboxRadio'
|
||||
import CheckboxRadioSwitch from '@nextcloud/vue/dist/Components/CheckboxRadioSwitch'
|
||||
import ContentHeading from './ContentHeading'
|
||||
|
||||
import { PUBLIC_CIRCLE_CONFIG } from '../../models/constants.ts'
|
||||
import Circle from '../../models/circle.ts'
|
||||
import { CircleEdit, editCircle } from '../../services/circles'
|
||||
import { CircleEdit, editCircle } from '../../services/circles.ts'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
|
||||
export default {
|
||||
name: 'CircleConfigs',
|
||||
|
||||
components: {
|
||||
CheckboxRadio,
|
||||
CheckboxRadioSwitch,
|
||||
ContentHeading,
|
||||
},
|
||||
|
||||
|
@ -66,6 +69,8 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
PUBLIC_CIRCLE_CONFIG,
|
||||
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -80,21 +85,29 @@ export default {
|
|||
* @param {boolean} checked checked or not
|
||||
*/
|
||||
async onChange(config, checked) {
|
||||
console.debug('Circle config', `'${PUBLIC_CIRCLE_CONFIG[config]}'`, 'is set to', checked)
|
||||
console.debug('Circle config', config, 'is set to', checked)
|
||||
|
||||
this.loading = config
|
||||
const prevConfig = this.circle.config
|
||||
|
||||
if (checked) {
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
this.circle.config = prevConfig | config
|
||||
config = prevConfig | config
|
||||
} else {
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
this.circle.config = prevConfig & ~config
|
||||
config = prevConfig & ~config
|
||||
}
|
||||
|
||||
const data = await editCircle(this.circle.id, CircleEdit.Config, this.circle.config)
|
||||
console.info(data)
|
||||
try {
|
||||
const circleData = await editCircle(this.circle.id, CircleEdit.Config, config)
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
this.circle.config = circleData.config
|
||||
|
||||
} catch (error) {
|
||||
console.error('Unable to edit circle config', prevConfig, config, error)
|
||||
showError(t('contacts', 'An error happened during the config change'))
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -23,17 +23,30 @@
|
|||
<template>
|
||||
<h3 class="app-content-heading">
|
||||
<slot />
|
||||
<div v-if="loading" class="app-content-heading__loader icon-loading-small" />
|
||||
</h3>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ContentHeading',
|
||||
|
||||
props: {
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-content-heading {
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
|
||||
&__loader {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -771,6 +771,7 @@ export default {
|
|||
.app-content-details {
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
padding: 0 80px;
|
||||
}
|
||||
|
||||
// List of all properties
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
<h2 class="contact-header__infos-title">
|
||||
<slot name="title" />
|
||||
</h2>
|
||||
<div class="contact-header__infos-subtitle">
|
||||
<div v-if="$slots.subtitle" class="contact-header__infos-subtitle">
|
||||
<slot name="subtitle" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -84,14 +84,12 @@ export default {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
padding: 50px 0 20px;
|
||||
font-weight: bold;
|
||||
|
||||
&__avatar {
|
||||
position: relative;
|
||||
flex: 1 1 var(--avatar-size);
|
||||
min-width: var(--avatar-size);
|
||||
max-width: 120px;
|
||||
flex: 0 0 var(--avatar-size);
|
||||
margin: 10px;
|
||||
margin-left: 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
@ -114,7 +112,7 @@ export default {
|
|||
min-width: 100px;
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
padding: 4px 5px;
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
border: none;
|
||||
|
|
|
@ -165,7 +165,7 @@ export default {
|
|||
// Object.keys returns those as string
|
||||
.map(level => parseInt(level, 10))
|
||||
// we cannot set to a level higher than the current user's level
|
||||
.filter(level => level < this.currentUserLevel)
|
||||
.filter(level => level <= this.currentUserLevel)
|
||||
// we cannot set to the level this member is already
|
||||
.filter(level => level !== this.source.level)
|
||||
},
|
||||
|
@ -209,6 +209,10 @@ export default {
|
|||
* @returns {string}
|
||||
*/
|
||||
levelChangeLabel(level) {
|
||||
if (level === MemberLevels.OWNER) {
|
||||
return t('contacts', 'Promote as sole owner')
|
||||
}
|
||||
|
||||
if (this.source.level < level) {
|
||||
return t('contacts', 'Promote to {level}', { level: CIRCLES_MEMBER_LEVELS[level] })
|
||||
}
|
||||
|
@ -245,6 +249,13 @@ export default {
|
|||
await changeMemberLevel(this.circle.id, this.source.id, level)
|
||||
this.showLevelMenu = false
|
||||
|
||||
// If we changed an owner, let's refresh the whole dataset to update all ownership & memberships
|
||||
if (level === MemberLevels.OWNER) {
|
||||
await this.$store.dispatch('getCircle', this.circle.id)
|
||||
await this.$store.dispatch('getCircleMembers', this.circle.id)
|
||||
return
|
||||
}
|
||||
|
||||
// this.source is a class. We're modifying the class setter, not the prop itself
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
this.source.level = level
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2021 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
|
||||
import { joinCircle } from '../services/circles.ts'
|
||||
import Circle from '../models/circle.ts'
|
||||
import CopyToClipboardMixin from './CopyToClipboardMixin'
|
||||
|
||||
export default {
|
||||
|
||||
props: {
|
||||
circle: {
|
||||
type: Circle,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
mixins: [CopyToClipboardMixin],
|
||||
|
||||
data() {
|
||||
return {
|
||||
loadingAction: false,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
copyButtonText() {
|
||||
if (this.copied) {
|
||||
return this.copySuccess
|
||||
? t('contacts', 'Copied')
|
||||
: t('contacts', 'Could not copy')
|
||||
}
|
||||
return t('contacts', 'Copy link')
|
||||
},
|
||||
|
||||
circleUrl() {
|
||||
const route = this.$router.resolve(this.circle.router)
|
||||
return window.location.origin + route.href
|
||||
},
|
||||
|
||||
joinButtonTitle() {
|
||||
if (this.circle.requireJoinAccept) {
|
||||
return t('contacts', 'Request to join')
|
||||
}
|
||||
return t('contacts', 'Join circle')
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
confirmLeaveCircle() {
|
||||
OC.dialogs.confirmDestructive(
|
||||
t('contacts', 'You are about to leave {circle}.\n Are you sure ?', {
|
||||
circle: this.circle.displayName,
|
||||
}),
|
||||
t('contacts', 'Please confirm circle leave'),
|
||||
OC.dialogs.YES_NO_BUTTONS,
|
||||
this.leaveCircle,
|
||||
true
|
||||
)
|
||||
},
|
||||
async leaveCircle(confirm) {
|
||||
if (!confirm) {
|
||||
console.debug('Circle leave cancelled')
|
||||
return
|
||||
}
|
||||
|
||||
this.loadingAction = true
|
||||
const member = this.circle.initiator
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('deleteMemberFromCircle', {
|
||||
member,
|
||||
leave: true,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Could not leave the circle', member, error)
|
||||
showError(t('contacts', 'Could not leave the circle {displayName}', this.circle))
|
||||
} finally {
|
||||
this.loadingAction = false
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
async joinCircle() {
|
||||
this.loadingAction = true
|
||||
try {
|
||||
await joinCircle(this.circle.id)
|
||||
} catch (error) {
|
||||
showError(t('contacts', 'Unable to join the circle'))
|
||||
} finally {
|
||||
this.loadingAction = false
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
confirmDeleteCircle() {
|
||||
OC.dialogs.confirmDestructive(
|
||||
t('contacts', 'You are about to delete {circle}.\n Are you sure ?', {
|
||||
circle: this.circle.displayName,
|
||||
}),
|
||||
t('contacts', 'Please confirm circle deletion'),
|
||||
OC.dialogs.YES_NO_BUTTONS,
|
||||
this.deleteCircle,
|
||||
true
|
||||
)
|
||||
},
|
||||
async deleteCircle(confirm) {
|
||||
if (!confirm) {
|
||||
console.debug('Circle deletion cancelled')
|
||||
return
|
||||
}
|
||||
|
||||
this.loadingAction = true
|
||||
|
||||
try {
|
||||
this.$store.dispatch('deleteCircle', this.circle.id)
|
||||
} catch (error) {
|
||||
showError(t('contacts', 'Unable to delete the circle'))
|
||||
} finally {
|
||||
this.loadingAction = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Trigger the entity picker view
|
||||
*/
|
||||
async addMemberToCircle() {
|
||||
await this.$router.push(this.circle.router)
|
||||
emit('contacts:circles:append', this.circle.id)
|
||||
},
|
||||
},
|
||||
}
|
|
@ -96,7 +96,7 @@ export const PUBLIC_CIRCLE_CONFIG = {
|
|||
[t('contacts', 'Invites')]: {
|
||||
[CIRCLE_CONFIG_OPEN]: t('contacts', 'Open, anyone can join'),
|
||||
[CIRCLE_CONFIG_INVITE]: t('contacts', 'Members need to accept invitation'),
|
||||
[CIRCLE_CONFIG_REQUEST]: t('contacts', 'Members need to be accepted by a moderator'),
|
||||
[CIRCLE_CONFIG_REQUEST]: t('contacts', 'Members need to be accepted by a moderator (requires Open)'),
|
||||
[CIRCLE_CONFIG_FRIEND]: t('contacts', 'Members can also invite'),
|
||||
// Let's manage password protection independently as we also need a password
|
||||
// [CIRCLE_CONFIG_PROTECTED]: t('contacts', 'Password protect'),
|
||||
|
@ -104,11 +104,11 @@ export const PUBLIC_CIRCLE_CONFIG = {
|
|||
|
||||
[t('contacts', 'Visibility')]: {
|
||||
[CIRCLE_CONFIG_VISIBLE]: t('contacts', 'Visible to everyone'),
|
||||
[CIRCLE_CONFIG_HIDDEN]: t('contacts', 'Hide this circle from listings'),
|
||||
},
|
||||
|
||||
[t('contacts', 'Circle membership')]: {
|
||||
[CIRCLE_CONFIG_CIRCLE_INVITE]: t('contacts', 'Circle must confirm when invited in another circle'),
|
||||
// TODO: implement backend
|
||||
// [CIRCLE_CONFIG_CIRCLE_INVITE]: t('contacts', 'Circle must confirm when invited in another circle'),
|
||||
[CIRCLE_CONFIG_ROOT]: t('contacts', 'Prevent circle from being a member of another circle'),
|
||||
},
|
||||
}
|
||||
|
|
|
@ -37,6 +37,12 @@ export declare enum CircleEdit {
|
|||
* @returns {Array}
|
||||
*/
|
||||
export declare const getCircles: () => Promise<any>;
|
||||
/**
|
||||
* Get a specific circle
|
||||
* @param {string} circleId
|
||||
* @returns {Object}
|
||||
*/
|
||||
export declare const getCircle: (circleId: string) => Promise<any>;
|
||||
/**
|
||||
* Create a new circle
|
||||
*
|
||||
|
@ -47,14 +53,14 @@ export declare const createCircle: (name: string) => Promise<any>;
|
|||
/**
|
||||
* Delete an existing circle
|
||||
*
|
||||
* @param {string} circleId the circle name
|
||||
* @param {string} circleId the circle id
|
||||
* @returns {Object}
|
||||
*/
|
||||
export declare const deleteCircle: (circleId: string) => Promise<any>;
|
||||
/**
|
||||
* Edit an existing circle
|
||||
*
|
||||
* @param {string} circleId the circle name
|
||||
* @param {string} circleId the circle id
|
||||
* @param {CircleEditType} type the edit type
|
||||
* @param {any} data the data
|
||||
* @returns {Object}
|
||||
|
@ -63,14 +69,14 @@ export declare const editCircle: (circleId: string, type: CircleEditType, value:
|
|||
/**
|
||||
* Join a circle
|
||||
*
|
||||
* @param {string} circleId the circle name
|
||||
* @param {string} circleId the circle id
|
||||
* @returns {Array}
|
||||
*/
|
||||
export declare const joinCircle: (circleId: string) => Promise<any>;
|
||||
/**
|
||||
* Leave a circle
|
||||
*
|
||||
* @param {string} circleId the circle name
|
||||
* @param {string} circleId the circle id
|
||||
* @returns {Array}
|
||||
*/
|
||||
export declare const leaveCircle: (circleId: string) => Promise<any>;
|
||||
|
|
|
@ -46,6 +46,16 @@ export const getCircles = async function() {
|
|||
return response.data.ocs.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific circle
|
||||
* @param {string} circleId
|
||||
* @returns {Object}
|
||||
*/
|
||||
export const getCircle = async function(circleId: string) {
|
||||
const response = await axios.get(generateOcsUrl('apps/circles/circles/{circleId}', { circleId }))
|
||||
return response.data.ocs.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new circle
|
||||
*
|
||||
|
@ -62,7 +72,7 @@ export const createCircle = async function(name: string) {
|
|||
/**
|
||||
* Delete an existing circle
|
||||
*
|
||||
* @param {string} circleId the circle name
|
||||
* @param {string} circleId the circle id
|
||||
* @returns {Object}
|
||||
*/
|
||||
export const deleteCircle = async function(circleId: string) {
|
||||
|
@ -73,7 +83,7 @@ export const deleteCircle = async function(circleId: string) {
|
|||
/**
|
||||
* Edit an existing circle
|
||||
*
|
||||
* @param {string} circleId the circle name
|
||||
* @param {string} circleId the circle id
|
||||
* @param {CircleEditType} type the edit type
|
||||
* @param {any} data the data
|
||||
* @returns {Object}
|
||||
|
@ -86,7 +96,7 @@ export const editCircle = async function(circleId: string, type: CircleEditType,
|
|||
/**
|
||||
* Join a circle
|
||||
*
|
||||
* @param {string} circleId the circle name
|
||||
* @param {string} circleId the circle id
|
||||
* @returns {Array}
|
||||
*/
|
||||
export const joinCircle = async function(circleId: string) {
|
||||
|
@ -97,7 +107,7 @@ export const joinCircle = async function(circleId: string) {
|
|||
/**
|
||||
* Leave a circle
|
||||
*
|
||||
* @param {string} circleId the circle name
|
||||
* @param {string} circleId the circle id
|
||||
* @returns {Array}
|
||||
*/
|
||||
export const leaveCircle = async function(circleId: string) {
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
import { showError } from '@nextcloud/dialogs'
|
||||
import Vue from 'vue'
|
||||
|
||||
import { createCircle, deleteCircle, deleteMember, getCircleMembers, getCircles, leaveCircle, addMembers } from '../services/circles.ts'
|
||||
import { createCircle, deleteCircle, deleteMember, getCircleMembers, getCircle, getCircles, leaveCircle, addMembers } from '../services/circles.ts'
|
||||
import Member from '../models/member.ts'
|
||||
import Circle from '../models/circle.ts'
|
||||
|
||||
|
@ -102,7 +102,6 @@ const getters = {
|
|||
}
|
||||
|
||||
const actions = {
|
||||
|
||||
/**
|
||||
* Retrieve and commit circles
|
||||
*
|
||||
|
@ -131,6 +130,27 @@ const actions = {
|
|||
return circles
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve and commit circles
|
||||
*
|
||||
* @param {Object} context the store mutations
|
||||
* @param {string} circleId the circle id
|
||||
* @returns {Object[]} the circles
|
||||
*/
|
||||
async getCircle(context, circleId) {
|
||||
const circle = await getCircle(circleId)
|
||||
console.debug('Retrieved 1 circle', circle)
|
||||
|
||||
try {
|
||||
const newCircle = new Circle(circle)
|
||||
context.commit('addCircle', newCircle)
|
||||
} catch (error) {
|
||||
console.error('This circle failed to be processed', circle, error)
|
||||
}
|
||||
|
||||
return circle
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve and commit circle members
|
||||
*
|
||||
|
|
Loading…
Reference in New Issue