New member button and virtual list
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
This commit is contained in:
parent
21c5e699ff
commit
77cc60e0ed
20
.eslintrc.js
20
.eslintrc.js
|
@ -1,8 +1,20 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
globals: {
|
globals: {
|
||||||
appVersion: true
|
appVersion: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: ['import'],
|
||||||
|
extends: ['@nextcloud'],
|
||||||
|
|
||||||
|
settings: {
|
||||||
|
'import/parsers': {
|
||||||
|
'@typescript-eslint/parser': ['.ts', '.tsx'],
|
||||||
|
},
|
||||||
|
'import/resolver': {
|
||||||
|
typescript: {
|
||||||
|
alwaysTryTypes: true,
|
||||||
|
paths: './tsconfig.json',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extends: [
|
|
||||||
'@nextcloud'
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,13 +36,14 @@
|
||||||
"@mattkrick/sanitize-svg": "^0.3.1",
|
"@mattkrick/sanitize-svg": "^0.3.1",
|
||||||
"@nextcloud/auth": "^1.3.0",
|
"@nextcloud/auth": "^1.3.0",
|
||||||
"@nextcloud/axios": "^1.6.0",
|
"@nextcloud/axios": "^1.6.0",
|
||||||
|
"@nextcloud/capabilities": "^1.0.4",
|
||||||
"@nextcloud/dialogs": "^3.1.2",
|
"@nextcloud/dialogs": "^3.1.2",
|
||||||
"@nextcloud/event-bus": "^1.2.0",
|
"@nextcloud/event-bus": "^1.2.0",
|
||||||
"@nextcloud/initial-state": "^1.2.0",
|
"@nextcloud/initial-state": "^1.2.0",
|
||||||
"@nextcloud/l10n": "^1.4.1",
|
"@nextcloud/l10n": "^1.4.1",
|
||||||
"@nextcloud/moment": "^1.1.1",
|
"@nextcloud/moment": "^1.1.1",
|
||||||
"@nextcloud/paths": "^1.1.2",
|
"@nextcloud/paths": "^1.1.2",
|
||||||
"@nextcloud/router": "^1.2.0",
|
"@nextcloud/router": "^2.0.0",
|
||||||
"@nextcloud/vue": "^3.9.0",
|
"@nextcloud/vue": "^3.9.0",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"b64-to-blob": "^1.2.19",
|
"b64-to-blob": "^1.2.19",
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
</EmptyContent>
|
</EmptyContent>
|
||||||
|
|
||||||
<EmptyContent v-else-if="circle.isPendingJoin" icon="icon-loading">
|
<EmptyContent v-else-if="circle.isPendingJoin" icon="icon-loading">
|
||||||
{{ t('contacts', 'Joining circle') }}
|
{{ t('contacts', 'Your request to join this circle is pending approval') }}
|
||||||
</EmptyContent>
|
</EmptyContent>
|
||||||
|
|
||||||
<EmptyContent v-else icon="icon-loading">
|
<EmptyContent v-else icon="icon-loading">
|
||||||
|
@ -76,7 +76,8 @@ import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent'
|
||||||
import CircleDetails from '../CircleDetails'
|
import CircleDetails from '../CircleDetails'
|
||||||
import MemberList from '../MemberList'
|
import MemberList from '../MemberList'
|
||||||
import RouterMixin from '../../mixins/RouterMixin'
|
import RouterMixin from '../../mixins/RouterMixin'
|
||||||
import { MEMBER_LEVEL_NONE } from '../../models/constants'
|
import { joinCircle } from '../../services/circles.ts'
|
||||||
|
import { showError } from '@nextcloud/dialogs'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CircleContent',
|
name: 'CircleContent',
|
||||||
|
@ -123,14 +124,6 @@ export default {
|
||||||
isEmptyCircle() {
|
isEmptyCircle() {
|
||||||
return this.members.length === 0
|
return this.members.length === 0
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Is the current user member of this circle?
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
isMemberOfCircle() {
|
|
||||||
return this.circle.initiator?.level > MEMBER_LEVEL_NONE
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -150,8 +143,17 @@ export default {
|
||||||
/**
|
/**
|
||||||
* Request to join this circle
|
* Request to join this circle
|
||||||
*/
|
*/
|
||||||
requestJoin() {
|
async requestJoin() {
|
||||||
this.loadingJoin = true
|
this.loadingJoin = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await joinCircle(this.circle.id)
|
||||||
|
} catch (error) {
|
||||||
|
showError(t('contacts', 'Unable to join the circle'))
|
||||||
|
} finally {
|
||||||
|
this.loadingJoin = false
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
|
|
||||||
<!-- copy circle link -->
|
<!-- copy circle link -->
|
||||||
<ActionLink
|
<ActionLink
|
||||||
:href="circle.url"
|
:href="circleUrl"
|
||||||
:icon="copyLoading ? 'icon-loading-small' : 'icon-public'"
|
:icon="copyLoading ? 'icon-loading-small' : 'icon-public'"
|
||||||
@click.stop.prevent="copyToClipboard(circleUrl)">
|
@click.stop.prevent="copyToClipboard(circleUrl)">
|
||||||
{{ copyButtonText }}
|
{{ copyButtonText }}
|
||||||
|
@ -49,7 +49,7 @@
|
||||||
|
|
||||||
<!-- leave circle -->
|
<!-- leave circle -->
|
||||||
<ActionButton
|
<ActionButton
|
||||||
v-if="circle.isMember"
|
v-if="circle.canLeave"
|
||||||
@click="leaveCircle">
|
@click="leaveCircle">
|
||||||
{{ t('contacts', 'Leave circle') }}
|
{{ t('contacts', 'Leave circle') }}
|
||||||
<ExitToApp slot="icon"
|
<ExitToApp slot="icon"
|
||||||
|
@ -59,7 +59,7 @@
|
||||||
|
|
||||||
<!-- join circle -->
|
<!-- join circle -->
|
||||||
<ActionButton
|
<ActionButton
|
||||||
v-else-if="circle.canJoin"
|
v-else-if="!circle.isMember && circle.canJoin"
|
||||||
@click="joinCircle">
|
@click="joinCircle">
|
||||||
{{ joinButtonTitle }}
|
{{ joinButtonTitle }}
|
||||||
<LocationEnter slot="icon"
|
<LocationEnter slot="icon"
|
||||||
|
@ -93,9 +93,10 @@ import AppNavigationItem from '@nextcloud/vue/dist/Components/AppNavigationItem'
|
||||||
import ExitToApp from 'vue-material-design-icons/ExitToApp'
|
import ExitToApp from 'vue-material-design-icons/ExitToApp'
|
||||||
import LocationEnter from 'vue-material-design-icons/LocationEnter'
|
import LocationEnter from 'vue-material-design-icons/LocationEnter'
|
||||||
|
|
||||||
import CopyToClipboardMixin from '../../mixins/CopyToClipboardMixin'
|
import { deleteCircle, joinCircle } from '../../services/circles.ts'
|
||||||
import { deleteCircle, joinCircle } from '../../services/circles'
|
|
||||||
import { showError } from '@nextcloud/dialogs'
|
import { showError } from '@nextcloud/dialogs'
|
||||||
|
import Circle from '../../models/circle.ts'
|
||||||
|
import CopyToClipboardMixin from '../../mixins/CopyToClipboardMixin'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CircleNavigationItem',
|
name: 'CircleNavigationItem',
|
||||||
|
@ -114,7 +115,7 @@ export default {
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
circle: {
|
circle: {
|
||||||
type: Object,
|
type: Circle,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -136,7 +137,8 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
circleUrl() {
|
circleUrl() {
|
||||||
return window.location.origin + this.circle.url
|
const route = this.$router.resolve(this.circle.router)
|
||||||
|
return window.location.origin + route.href
|
||||||
},
|
},
|
||||||
|
|
||||||
joinButtonTitle() {
|
joinButtonTitle() {
|
||||||
|
@ -147,21 +149,25 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
memberCount() {
|
memberCount() {
|
||||||
return this.circle?.members?.length || 0
|
return Object.values(this.circle?.members || []).length
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
// Trigger the entity picker view
|
// Trigger the entity picker view
|
||||||
addMemberToCircle() {
|
async addMemberToCircle() {
|
||||||
|
await this.$router.push(this.circle.router)
|
||||||
emit('contacts:circles:append', this.circle.id)
|
emit('contacts:circles:append', this.circle.id)
|
||||||
},
|
},
|
||||||
|
|
||||||
async joinCircle() {
|
async joinCircle() {
|
||||||
|
this.loading = true
|
||||||
try {
|
try {
|
||||||
await joinCircle(this.circle.id)
|
await joinCircle(this.circle.id)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(t('contacts', 'Unable to join the circle'))
|
showError(t('contacts', 'Unable to join the circle'))
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
|
@ -144,7 +144,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { GROUP_ALL_CONTACTS, GROUP_NO_GROUP_CONTACTS, GROUP_RECENTLY_CONTACTED, ELLIPSIS_COUNT } from '../../models/constants'
|
import { GROUP_ALL_CONTACTS, GROUP_NO_GROUP_CONTACTS, GROUP_RECENTLY_CONTACTED, ELLIPSIS_COUNT } from '../../models/constants.ts'
|
||||||
|
|
||||||
import ActionInput from '@nextcloud/vue/dist/Components/ActionInput'
|
import ActionInput from '@nextcloud/vue/dist/Components/ActionInput'
|
||||||
import ActionText from '@nextcloud/vue/dist/Components/ActionText'
|
import ActionText from '@nextcloud/vue/dist/Components/ActionText'
|
||||||
|
@ -348,14 +348,14 @@ export default {
|
||||||
|
|
||||||
this.createCircleError = null
|
this.createCircleError = null
|
||||||
|
|
||||||
const circleId = await this.$store.dispatch('createCircle', circleName)
|
const circle = await this.$store.dispatch('createCircle', circleName)
|
||||||
this.isNewCircleMenuOpen = false
|
this.isNewCircleMenuOpen = false
|
||||||
|
|
||||||
// Select group
|
// Select group
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
name: 'circle',
|
name: 'circle',
|
||||||
params: {
|
params: {
|
||||||
selectedCircle: circleId,
|
selectedCircle: circle.id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -368,12 +368,12 @@ export default {
|
||||||
#newcircle {
|
#newcircle {
|
||||||
margin-top: 22px;
|
margin-top: 22px;
|
||||||
|
|
||||||
/deep/ a {
|
::v-deep a {
|
||||||
color: var(--color-text-maxcontrast)
|
color: var(--color-text-maxcontrast)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-navigation__collapse /deep/ a {
|
.app-navigation__collapse ::v-deep a {
|
||||||
color: var(--color-text-maxcontrast)
|
color: var(--color-text-maxcontrast)
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -90,7 +90,7 @@ import { encodePath } from '@nextcloud/paths'
|
||||||
import { getCurrentUser } from '@nextcloud/auth'
|
import { getCurrentUser } from '@nextcloud/auth'
|
||||||
import { generateRemoteUrl } from '@nextcloud/router'
|
import { generateRemoteUrl } from '@nextcloud/router'
|
||||||
import { getFilePickerBuilder } from '@nextcloud/dialogs'
|
import { getFilePickerBuilder } from '@nextcloud/dialogs'
|
||||||
import axios from 'axios'
|
import axios from '@nextcloud/axios'
|
||||||
|
|
||||||
const CancelToken = axios.CancelToken
|
const CancelToken = axios.CancelToken
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
<!-- contacts picker -->
|
<!-- contacts picker -->
|
||||||
<EntityPicker v-else-if="showPicker"
|
<EntityPicker v-else-if="showPicker"
|
||||||
:confirm-label="t('contacts', 'Add to group {group}', { group: pickerforGroup.name})"
|
:confirm-label="t('contacts', 'Add to {group}', { group: pickerforGroup.name})"
|
||||||
:data-types="pickerTypes"
|
:data-types="pickerTypes"
|
||||||
:data-set="pickerData"
|
:data-set="pickerData"
|
||||||
@close="onContactPickerClose"
|
@close="onContactPickerClose"
|
||||||
|
|
|
@ -109,4 +109,5 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -35,17 +35,17 @@
|
||||||
:placeholder="t('contacts', 'Search {types}', {types: searchPlaceholderTypes})"
|
:placeholder="t('contacts', 'Search {types}', {types: searchPlaceholderTypes})"
|
||||||
class="entity-picker__search-input"
|
class="entity-picker__search-input"
|
||||||
type="search"
|
type="search"
|
||||||
@change="onSearch">
|
@input="onSearch">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Picked entities -->
|
<!-- Picked entities -->
|
||||||
<transition-group
|
<transition-group
|
||||||
v-if="Object.keys(selection).length > 0"
|
v-if="Object.keys(selectionSet).length > 0"
|
||||||
name="zoom"
|
name="zoom"
|
||||||
tag="ul"
|
tag="ul"
|
||||||
class="entity-picker__selection">
|
class="entity-picker__selection">
|
||||||
<EntityBubble
|
<EntityBubble
|
||||||
v-for="entity in selection"
|
v-for="entity in selectionSet"
|
||||||
:key="entity.key || `entity-${entity.type}-${entity.id}`"
|
:key="entity.key || `entity-${entity.type}-${entity.id}`"
|
||||||
v-bind="entity"
|
v-bind="entity"
|
||||||
@delete="onDelete(entity)" />
|
@delete="onDelete(entity)" />
|
||||||
|
@ -68,7 +68,7 @@
|
||||||
:data-sources="availableEntities"
|
:data-sources="availableEntities"
|
||||||
:data-component="EntitySearchResult"
|
:data-component="EntitySearchResult"
|
||||||
:estimate-size="44"
|
:estimate-size="44"
|
||||||
:extra-props="{selection, onClick: onPick}" />
|
:extra-props="{selection: selectionSet, onClick: onPick}" />
|
||||||
|
|
||||||
<EmptyContent v-else-if="searchQuery" icon="icon-search">
|
<EmptyContent v-else-if="searchQuery" icon="icon-search">
|
||||||
{{ t('contacts', 'No results') }}
|
{{ t('contacts', 'No results') }}
|
||||||
|
@ -92,9 +92,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Modal from '@nextcloud/vue/dist/Components/Modal'
|
import debounce from 'debounce'
|
||||||
import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent'
|
|
||||||
import VirtualList from 'vue-virtual-scroll-list'
|
import VirtualList from 'vue-virtual-scroll-list'
|
||||||
|
import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent'
|
||||||
|
import Modal from '@nextcloud/vue/dist/Components/Modal'
|
||||||
|
|
||||||
import EntityBubble from './EntityBubble'
|
import EntityBubble from './EntityBubble'
|
||||||
import EntitySearchResult from './EntitySearchResult'
|
import EntitySearchResult from './EntitySearchResult'
|
||||||
|
@ -138,6 +139,14 @@ export default {
|
||||||
dataSet: {
|
dataSet: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
|
validator: data => {
|
||||||
|
data.forEach(source => {
|
||||||
|
if (!source.id || !source.label) {
|
||||||
|
console.error('The following source MUST have a proper id and label key', source)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -155,17 +164,44 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
default: t('contacts', 'Add to group'),
|
default: t('contacts', 'Add to group'),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override the local management of selection
|
||||||
|
* You MUST use a sync modifier or the selection will be locked
|
||||||
|
*/
|
||||||
|
selection: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
selection: {},
|
localSelection: {},
|
||||||
EntitySearchResult,
|
EntitySearchResult,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
/**
|
||||||
|
* If the selection is set externally, let's use it
|
||||||
|
*/
|
||||||
|
selectionSet: {
|
||||||
|
get() {
|
||||||
|
if (this.selection !== null) {
|
||||||
|
return this.selection
|
||||||
|
}
|
||||||
|
return this.localSelection
|
||||||
|
},
|
||||||
|
set(selection) {
|
||||||
|
if (this.selection !== null) {
|
||||||
|
this.$emit('update:selection', selection)
|
||||||
|
}
|
||||||
|
this.localSelection = selection
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Are we handling a single entity type ?
|
* Are we handling a single entity type ?
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
|
@ -179,7 +215,7 @@ export default {
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
isEmptySelection() {
|
isEmptySelection() {
|
||||||
return Object.keys(this.selection).length === 0
|
return Object.keys(this.selectionSet).length === 0
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -219,14 +255,24 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Else group by types
|
// Else group by types
|
||||||
return this.dataTypes.map(type => [
|
return this.dataTypes.map(type => {
|
||||||
{
|
const dataSet = this.searchSet.filter(entity => entity.type === type.id)
|
||||||
id: type.id,
|
const dataList = [
|
||||||
label: type.label,
|
{
|
||||||
heading: true,
|
id: type.id,
|
||||||
},
|
label: type.label,
|
||||||
...this.searchSet.filter(entity => entity.type === type.id),
|
heading: true,
|
||||||
]).flat()
|
},
|
||||||
|
...dataSet,
|
||||||
|
]
|
||||||
|
|
||||||
|
// If no results, hide the type
|
||||||
|
if (dataSet.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataList
|
||||||
|
}).flat()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -249,23 +295,23 @@ export default {
|
||||||
* Emitted when user submit the form
|
* Emitted when user submit the form
|
||||||
* @type {Array} the selected entities
|
* @type {Array} the selected entities
|
||||||
*/
|
*/
|
||||||
this.$emit('submit', Object.values(this.selection))
|
this.$emit('submit', Object.values(this.selectionSet))
|
||||||
},
|
},
|
||||||
|
|
||||||
onSearch(event) {
|
onSearch: debounce(function() {
|
||||||
/**
|
/**
|
||||||
* Emitted when search change
|
* Emitted when search change
|
||||||
* @type {string} the search query
|
* @type {string} the search query
|
||||||
*/
|
*/
|
||||||
this.$emit('search', this.searchQuery)
|
this.$emit('search', this.searchQuery)
|
||||||
},
|
}, 200),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove entity from selection
|
* Remove entity from selection
|
||||||
* @param {Object} entity the entity to remove
|
* @param {Object} entity the entity to remove
|
||||||
*/
|
*/
|
||||||
onDelete(entity) {
|
onDelete(entity) {
|
||||||
this.$delete(this.selection, entity.id, entity)
|
this.$delete(this.selectionSet, entity.id, entity)
|
||||||
console.debug('Removing entity from selection', entity)
|
console.debug('Removing entity from selection', entity)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -274,7 +320,7 @@ export default {
|
||||||
* @param {Object} entity the entity to add
|
* @param {Object} entity the entity to add
|
||||||
*/
|
*/
|
||||||
onPick(entity) {
|
onPick(entity) {
|
||||||
this.$set(this.selection, entity.id, entity)
|
this.$set(this.selectionSet, entity.id, entity)
|
||||||
console.debug('Added entity to selection', entity)
|
console.debug('Added entity to selection', entity)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -283,7 +329,7 @@ export default {
|
||||||
* @param {Object} entity the entity to add/remove
|
* @param {Object} entity the entity to add/remove
|
||||||
*/
|
*/
|
||||||
onToggle(entity) {
|
onToggle(entity) {
|
||||||
if (entity.id in this.selection) {
|
if (entity.id in this.selectionSet) {
|
||||||
this.onDelete(entity)
|
this.onDelete(entity)
|
||||||
} else {
|
} else {
|
||||||
this.onPick(entity)
|
this.onPick(entity)
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
class="entity-picker__bubble"
|
class="entity-picker__bubble"
|
||||||
:class="{'entity-picker__bubble--selected': isSelected}"
|
:class="{'entity-picker__bubble--selected': isSelected}"
|
||||||
:display-name="source.label"
|
:display-name="source.label"
|
||||||
|
:user="source.user"
|
||||||
:margin="6"
|
:margin="6"
|
||||||
:size="44"
|
:size="44"
|
||||||
url="#"
|
url="#"
|
||||||
|
@ -145,6 +146,11 @@ $icon-margin: ($clickable-area - $icon-size) / 2;
|
||||||
&, * {
|
&, * {
|
||||||
// the whole row is clickable,let's force the proper cursor
|
// the whole row is clickable,let's force the proper cursor
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
-khtml-user-drag: none;
|
||||||
|
-moz-user-drag: none;
|
||||||
|
-o-user-drag: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,24 +21,58 @@
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VirtualList class="member-list app-content-list"
|
<AppContentList>
|
||||||
data-key="id"
|
<div class="members-list__new">
|
||||||
:data-sources="list"
|
<button class="icon-add" @click="onShowPicker(circle.id)">
|
||||||
:data-component="MemberListItem"
|
{{ t('contacts', 'Add members') }}
|
||||||
:estimate-size="68"
|
</button>
|
||||||
item-class="member-list__item" />
|
</div>
|
||||||
|
|
||||||
|
<VirtualList class="members-list"
|
||||||
|
data-key="id"
|
||||||
|
:data-sources="list"
|
||||||
|
:data-component="MembersListItem"
|
||||||
|
:estimate-size="68" />
|
||||||
|
|
||||||
|
<!-- member picker -->
|
||||||
|
<EntityPicker v-if="showPicker"
|
||||||
|
:confirm-label="t('contacts', 'Add to {circle}', { circle: circle.displayName})"
|
||||||
|
:data-types="pickerTypes"
|
||||||
|
:data-set="pickerData"
|
||||||
|
:loading="pickerLoading"
|
||||||
|
:selection.sync="pickerSelection"
|
||||||
|
@close="resetPicker"
|
||||||
|
@search="onSearch"
|
||||||
|
@submit="onPickerPick" />
|
||||||
|
</AppContentList>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import MemberListItem from './MemberList/MemberListItem'
|
import AppContentList from '@nextcloud/vue/dist/Components/AppContentList'
|
||||||
|
import Actions from '@nextcloud/vue/dist/Components/Actions'
|
||||||
|
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
|
||||||
import VirtualList from 'vue-virtual-scroll-list'
|
import VirtualList from 'vue-virtual-scroll-list'
|
||||||
|
|
||||||
|
import MembersListItem from './MembersList/MembersListItem'
|
||||||
|
import EntityPicker from './EntityPicker/EntityPicker'
|
||||||
|
import RouterMixin from '../mixins/RouterMixin'
|
||||||
|
|
||||||
|
import { getRecommendations, getSuggestions } from '../services/collaborationAutocompletion'
|
||||||
|
import { showError, showWarning } from '@nextcloud/dialogs'
|
||||||
|
import { subscribe } from '@nextcloud/event-bus'
|
||||||
|
import { SHARES_TYPES_MEMBER_MAP } from '../models/constants.ts'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MemberList',
|
name: 'MemberList',
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
|
Actions,
|
||||||
|
ActionButton,
|
||||||
|
AppContentList,
|
||||||
VirtualList,
|
VirtualList,
|
||||||
|
EntityPicker,
|
||||||
},
|
},
|
||||||
|
mixins: [RouterMixin],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
list: {
|
list: {
|
||||||
|
@ -49,20 +83,165 @@ export default {
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
MemberListItem,
|
MembersListItem,
|
||||||
|
pickerLoading: false,
|
||||||
|
showPicker: false,
|
||||||
|
|
||||||
|
recommendations: [],
|
||||||
|
pickerCircle: null,
|
||||||
|
pickerData: [],
|
||||||
|
pickerSelection: {},
|
||||||
|
pickerTypes: [{
|
||||||
|
id: `picker-${OC.Share.SHARE_TYPE_USER}`,
|
||||||
|
label: t('contacts', 'Users'),
|
||||||
|
}, {
|
||||||
|
id: `picker-${OC.Share.SHARE_TYPE_GROUP}`,
|
||||||
|
label: t('contacts', 'Groups'),
|
||||||
|
}, {
|
||||||
|
id: `picker-${OC.Share.SHARE_TYPE_CIRCLE}`,
|
||||||
|
label: t('contacts', 'Circles'),
|
||||||
|
}, {
|
||||||
|
id: `picker-${OC.Share.SHARE_TYPE_EMAIL}`,
|
||||||
|
label: t('contacts', 'Email'),
|
||||||
|
}],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
/**
|
||||||
|
* Return the current circle
|
||||||
|
* @returns {Circle}
|
||||||
|
*/
|
||||||
|
circle() {
|
||||||
|
return this.$store.getters.getCircle(this.selectedCircle)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
mounted() {
|
||||||
|
subscribe('contacts:circles:append', this.onShowPicker)
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
/**
|
||||||
|
* Show picker and fetch for recommendations
|
||||||
|
* Cache the circleId in case the url change or something
|
||||||
|
* and make sure we add them to the desired circle.
|
||||||
|
* @param {string} circleId the circle id to add members to
|
||||||
|
*/
|
||||||
|
async onShowPicker(circleId) {
|
||||||
|
this.showPicker = true
|
||||||
|
this.pickerLoading = true
|
||||||
|
this.pickerCircle = circleId
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = await getRecommendations()
|
||||||
|
// cache recommendations
|
||||||
|
this.recommendations = results
|
||||||
|
this.pickerData = results
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Unable to get the recommendations list', error)
|
||||||
|
// Do not show the error, let the user search
|
||||||
|
// showError(t('contacts', 'Unable to get the recommendations list'))
|
||||||
|
} finally {
|
||||||
|
this.pickerLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On EntityPicker search.
|
||||||
|
* Returns recommendations if empty
|
||||||
|
* @param {string} term the searched term
|
||||||
|
*/
|
||||||
|
async onSearch(term) {
|
||||||
|
if (term.trim() === '') {
|
||||||
|
this.pickerData = this.recommendations
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pickerLoading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = await getSuggestions(term)
|
||||||
|
this.pickerData = results
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Unable to get the results', error)
|
||||||
|
showError(t('contacts', 'Unable to get the results'))
|
||||||
|
} finally {
|
||||||
|
this.pickerLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On picker submit
|
||||||
|
* @param {Array} selection the selection to add to the circle
|
||||||
|
*/
|
||||||
|
async onPickerPick(selection) {
|
||||||
|
console.info('Adding selection to circle', selection, this.pickerCircle)
|
||||||
|
|
||||||
|
this.pickerLoading = true
|
||||||
|
|
||||||
|
selection = selection.map(entry => ({
|
||||||
|
id: entry.shareWith,
|
||||||
|
type: SHARES_TYPES_MEMBER_MAP[entry.shareType],
|
||||||
|
}))
|
||||||
|
|
||||||
|
try {
|
||||||
|
const members = await this.$store.dispatch('addMembersToCircle', { circleId: this.pickerCircle, selection })
|
||||||
|
|
||||||
|
if (members.length !== selection.length) {
|
||||||
|
showWarning(t('contacts', 'Some members could not be added'))
|
||||||
|
// TODO filter successful members and edit selection
|
||||||
|
this.selection = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resetPicker()
|
||||||
|
} catch (error) {
|
||||||
|
showError(t('contacts', 'There was an issue adding members to the circle'))
|
||||||
|
console.error('There was an issue adding members to the circle', this.pickerCircle, error)
|
||||||
|
} finally {
|
||||||
|
this.pickerLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset picker related variables
|
||||||
|
*/
|
||||||
|
resetPicker() {
|
||||||
|
this.showPicker = false
|
||||||
|
this.pickerCircle = null
|
||||||
|
this.pickerData = []
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.app-content-list {
|
||||||
|
flex: 1 1 300px;
|
||||||
|
// Cancel scrolling
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
|
.empty-content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.members-list {
|
||||||
|
// Make virtual scroller scrollable
|
||||||
|
max-height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
&__new {
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
height: 44px;
|
||||||
|
padding-left: 44px;
|
||||||
|
background-position: 14px center;
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -22,13 +22,14 @@
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ListItemIcon
|
<ListItemIcon
|
||||||
:id="source.id"
|
:id="source.singleId"
|
||||||
:key="source.id"
|
:key="source.singleId"
|
||||||
:avatar-size="44"
|
:avatar-size="44"
|
||||||
:is-no-user="!source.isUser"
|
:is-no-user="!source.isUser"
|
||||||
:subtitle="levelName"
|
:subtitle="levelName"
|
||||||
:title="source.displayName"
|
:title="source.displayName"
|
||||||
:user="source.userId">
|
:user="source.userId"
|
||||||
|
class="members-list__item">
|
||||||
<Actions @close="onMenuClose">
|
<Actions @close="onMenuClose">
|
||||||
<template v-if="loading">
|
<template v-if="loading">
|
||||||
<ActionText icon="icon-loading-small">
|
<ActionText icon="icon-loading-small">
|
||||||
|
@ -36,31 +37,26 @@
|
||||||
</ActionText>
|
</ActionText>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Level picker -->
|
|
||||||
<template v-else-if="showLevelMenu">
|
|
||||||
<ActionButton @click="toggleLevelMenu">
|
|
||||||
{{ t('contacts', 'Back to the menu') }}
|
|
||||||
<ArrowLeft slot="icon"
|
|
||||||
:size="16"
|
|
||||||
decorative />
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton
|
|
||||||
v-for="level in availableLevelsChange"
|
|
||||||
:key="level"
|
|
||||||
icon=""
|
|
||||||
@click="changeLevel(level)">
|
|
||||||
{{ CIRCLES_MEMBER_LEVELS[level] }}
|
|
||||||
</ActionButton>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Normal menu -->
|
<!-- Normal menu -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<ActionButton v-if="canChangeLevel" @click="toggleLevelMenu">
|
<!-- Level picker -->
|
||||||
{{ t('contacts', 'Change level') }}
|
<template v-if="canChangeLevel">
|
||||||
<ShieldCheck slot="icon"
|
<ActionText>
|
||||||
:size="16"
|
{{ t('contacts', 'Manage level') }}
|
||||||
decorative />
|
<ShieldCheck slot="icon"
|
||||||
</ActionButton>
|
:size="16"
|
||||||
|
decorative />
|
||||||
|
</ActionText>
|
||||||
|
<ActionButton
|
||||||
|
v-for="level in availableLevelsChange"
|
||||||
|
:key="level"
|
||||||
|
icon=""
|
||||||
|
@click="changeLevel(level)">
|
||||||
|
{{ levelChangeLabel(level) }}
|
||||||
|
</ActionButton>
|
||||||
|
|
||||||
|
<ActionSeparator />
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Leave or delete member from circle -->
|
<!-- Leave or delete member from circle -->
|
||||||
<ActionButton v-if="isCurrentUser && !circle.isOwner" @click="deleteMember">
|
<ActionButton v-if="isCurrentUser && !circle.isOwner" @click="deleteMember">
|
||||||
|
@ -78,30 +74,30 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { MEMBER_LEVEL_MEMBER, CIRCLES_MEMBER_LEVELS } from '../../models/constants'
|
import { CIRCLES_MEMBER_LEVELS, MemberLevels } from '../../models/constants.ts'
|
||||||
|
|
||||||
import Actions from '@nextcloud/vue/dist/Components/Actions'
|
import Actions from '@nextcloud/vue/dist/Components/Actions'
|
||||||
import ListItemIcon from '@nextcloud/vue/dist/Components/ListItemIcon'
|
import ListItemIcon from '@nextcloud/vue/dist/Components/ListItemIcon'
|
||||||
|
import ActionSeparator from '@nextcloud/vue/dist/Components/ActionSeparator'
|
||||||
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
|
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
|
||||||
import ActionText from '@nextcloud/vue/dist/Components/ActionText'
|
import ActionText from '@nextcloud/vue/dist/Components/ActionText'
|
||||||
|
|
||||||
import ArrowLeft from 'vue-material-design-icons/ArrowLeft'
|
|
||||||
import ExitToApp from 'vue-material-design-icons/ExitToApp'
|
import ExitToApp from 'vue-material-design-icons/ExitToApp'
|
||||||
import ShieldCheck from 'vue-material-design-icons/ShieldCheck'
|
import ShieldCheck from 'vue-material-design-icons/ShieldCheck'
|
||||||
|
|
||||||
import { changeMemberLevel } from '../../services/circles'
|
import { changeMemberLevel } from '../../services/circles.ts'
|
||||||
import { showError } from '@nextcloud/dialogs'
|
import { showError } from '@nextcloud/dialogs'
|
||||||
import Member from '../../models/member'
|
import Member from '../../models/member.ts'
|
||||||
import RouterMixin from '../../mixins/RouterMixin'
|
import RouterMixin from '../../mixins/RouterMixin'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MemberListItem',
|
name: 'MembersListItem',
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
Actions,
|
Actions,
|
||||||
ActionButton,
|
ActionButton,
|
||||||
|
ActionSeparator,
|
||||||
ActionText,
|
ActionText,
|
||||||
ArrowLeft,
|
|
||||||
ExitToApp,
|
ExitToApp,
|
||||||
ListItemIcon,
|
ListItemIcon,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
|
@ -120,7 +116,6 @@ export default {
|
||||||
CIRCLES_MEMBER_LEVELS,
|
CIRCLES_MEMBER_LEVELS,
|
||||||
|
|
||||||
loading: false,
|
loading: false,
|
||||||
showLevelMenu: false,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -146,7 +141,7 @@ export default {
|
||||||
*/
|
*/
|
||||||
levelName() {
|
levelName() {
|
||||||
return CIRCLES_MEMBER_LEVELS[this.source.level]
|
return CIRCLES_MEMBER_LEVELS[this.source.level]
|
||||||
|| CIRCLES_MEMBER_LEVELS[MEMBER_LEVEL_MEMBER]
|
|| CIRCLES_MEMBER_LEVELS[MemberLevels.MEMBER]
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -154,7 +149,7 @@ export default {
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
currentUserLevel() {
|
currentUserLevel() {
|
||||||
return this.circle?.initiator?.level || MEMBER_LEVEL_MEMBER
|
return this.circle?.initiator?.level || MemberLevels.MEMBER
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -162,7 +157,7 @@ export default {
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
currentUserId() {
|
currentUserId() {
|
||||||
return this.circle?.initiator?.id
|
return this.circle?.initiator?.singleId
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -170,7 +165,13 @@ export default {
|
||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
*/
|
*/
|
||||||
availableLevelsChange() {
|
availableLevelsChange() {
|
||||||
return Object.keys(CIRCLES_MEMBER_LEVELS).filter(level => level < this.currentUserLevel)
|
return Object.keys(CIRCLES_MEMBER_LEVELS)
|
||||||
|
// 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)
|
||||||
|
// we cannot set to the level this member is already
|
||||||
|
.filter(level => level !== this.source.level)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -178,7 +179,7 @@ export default {
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
isCurrentUser() {
|
isCurrentUser() {
|
||||||
return this.currentUserId === this.source.id
|
return this.currentUserId === this.source.singleId
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -188,9 +189,11 @@ export default {
|
||||||
canChangeLevel() {
|
canChangeLevel() {
|
||||||
// we can change if the member is at the same
|
// we can change if the member is at the same
|
||||||
// or lower level as the current user
|
// or lower level as the current user
|
||||||
|
// BUT not an owner as there can/must always be one
|
||||||
return this.availableLevelsChange.length > 0
|
return this.availableLevelsChange.length > 0
|
||||||
&& this.currentUserLevel >= this.source.level
|
&& this.currentUserLevel >= this.source.level
|
||||||
&& this.circle.canManageMembers
|
&& this.circle.canManageMembers
|
||||||
|
&& !(this.circle.isOwner && this.isCurrentUser)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -198,13 +201,22 @@ export default {
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
canDelete() {
|
canDelete() {
|
||||||
return this.currentUserLevel > MEMBER_LEVEL_MEMBER
|
return this.currentUserLevel > MemberLevels.MEMBER
|
||||||
&& this.source.level <= this.currentUserLevel
|
&& this.source.level <= this.currentUserLevel
|
||||||
|
&& !this.isCurrentUser
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleLevelMenu() {
|
/**
|
||||||
this.showLevelMenu = !this.showLevelMenu
|
* Return the promote/demote member action label
|
||||||
|
* @param {MemberLevel} level the member level
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
levelChangeLabel(level) {
|
||||||
|
if (this.source.level < level) {
|
||||||
|
return t('contacts', 'Promote to {level}', { level: CIRCLES_MEMBER_LEVELS[level] })
|
||||||
|
}
|
||||||
|
return t('contacts', 'Demote to {level}', { level: CIRCLES_MEMBER_LEVELS[level] })
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -219,6 +231,10 @@ export default {
|
||||||
leave: this.isCurrentUser,
|
leave: this.isCurrentUser,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error.response.status === 404) {
|
||||||
|
console.debug('Member is not in circle')
|
||||||
|
return
|
||||||
|
}
|
||||||
console.error('Could not delete the member', this.source, error)
|
console.error('Could not delete the member', this.source, error)
|
||||||
showError(t('contacts', 'Could not delete the member {displayName}', this.source))
|
showError(t('contacts', 'Could not delete the member {displayName}', this.source))
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -232,6 +248,10 @@ export default {
|
||||||
try {
|
try {
|
||||||
await changeMemberLevel(this.circle.id, this.source.id, level)
|
await changeMemberLevel(this.circle.id, this.source.id, level)
|
||||||
this.showLevelMenu = false
|
this.showLevelMenu = false
|
||||||
|
|
||||||
|
// 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
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Could not change the member level to', CIRCLES_MEMBER_LEVELS[level])
|
console.error('Could not change the member level to', CIRCLES_MEMBER_LEVELS[level])
|
||||||
showError(t('contacts', 'Could not change the member level to {level}', {
|
showError(t('contacts', 'Could not change the member level to {level}', {
|
||||||
|
@ -252,7 +272,12 @@ export default {
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.member-list__item {
|
.members-list__item {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-background-hover);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -0,0 +1,142 @@
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2018 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 Member from './member';
|
||||||
|
declare type MemberList = Record<string, Member>;
|
||||||
|
export default class Circle {
|
||||||
|
_data: any;
|
||||||
|
_members: MemberList;
|
||||||
|
_owner: Member;
|
||||||
|
_initiator: Member;
|
||||||
|
/**
|
||||||
|
* Creates an instance of Circle
|
||||||
|
*/
|
||||||
|
constructor(data: Object);
|
||||||
|
/**
|
||||||
|
* Update inner circle data, owner and initiator
|
||||||
|
*/
|
||||||
|
updateData(data: any): void;
|
||||||
|
/**
|
||||||
|
* Circle id
|
||||||
|
*/
|
||||||
|
get id(): string;
|
||||||
|
/**
|
||||||
|
* Formatted display name
|
||||||
|
*/
|
||||||
|
get displayName(): string;
|
||||||
|
/**
|
||||||
|
* Circle creation date
|
||||||
|
*/
|
||||||
|
get creation(): number;
|
||||||
|
/**
|
||||||
|
* Circle description
|
||||||
|
*/
|
||||||
|
get description(): string;
|
||||||
|
/**
|
||||||
|
* Circle description
|
||||||
|
*/
|
||||||
|
set description(text: string);
|
||||||
|
/**
|
||||||
|
* Circle ini_initiator the current
|
||||||
|
* user info for this circle
|
||||||
|
*/
|
||||||
|
get initiator(): Member;
|
||||||
|
/**
|
||||||
|
* Circle ownership
|
||||||
|
*/
|
||||||
|
get owner(): Member;
|
||||||
|
/**
|
||||||
|
* Set new circle owner
|
||||||
|
*/
|
||||||
|
set owner(owner: Member);
|
||||||
|
/**
|
||||||
|
* Circle members
|
||||||
|
*/
|
||||||
|
get members(): MemberList;
|
||||||
|
/**
|
||||||
|
* Define members circle
|
||||||
|
*/
|
||||||
|
set members(members: MemberList);
|
||||||
|
/**
|
||||||
|
* Add a member to this circle
|
||||||
|
*/
|
||||||
|
addMember(member: Member): void;
|
||||||
|
/**
|
||||||
|
* Remove a member from this circle
|
||||||
|
*/
|
||||||
|
deleteMember(member: Member): void;
|
||||||
|
get settings(): any;
|
||||||
|
/**
|
||||||
|
* Circle config
|
||||||
|
*/
|
||||||
|
get config(): any;
|
||||||
|
/**
|
||||||
|
* Circle requires invite to be confirmed by moderator or above
|
||||||
|
*/
|
||||||
|
get requireJoinAccept(): boolean;
|
||||||
|
/**
|
||||||
|
* Circle can be requested to join
|
||||||
|
*/
|
||||||
|
get canJoin(): boolean;
|
||||||
|
/**
|
||||||
|
* Circle is visible to others
|
||||||
|
*/
|
||||||
|
get isVisible(): boolean;
|
||||||
|
/**
|
||||||
|
* Circle requires invite to be accepted by the member
|
||||||
|
*/
|
||||||
|
get requireInviteAccept(): boolean;
|
||||||
|
/**
|
||||||
|
* Can the initiator add members to this circle?
|
||||||
|
*/
|
||||||
|
get isOwner(): boolean;
|
||||||
|
/**
|
||||||
|
* Is the initiator a member of this circle?
|
||||||
|
*/
|
||||||
|
get isMember(): boolean;
|
||||||
|
/**
|
||||||
|
* Can the initiator delete this circle?
|
||||||
|
*/
|
||||||
|
get canDelete(): boolean;
|
||||||
|
/**
|
||||||
|
* Can the initiator leave this circle?
|
||||||
|
*/
|
||||||
|
get canLeave(): boolean;
|
||||||
|
/**
|
||||||
|
* Can the initiator add/remove members to this circle?
|
||||||
|
*/
|
||||||
|
get canManageMembers(): boolean;
|
||||||
|
/**
|
||||||
|
* Vue router param
|
||||||
|
*/
|
||||||
|
get router(): {
|
||||||
|
name: string;
|
||||||
|
params: {
|
||||||
|
selectedCircle: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Default javascript fallback
|
||||||
|
* Used for sorting as well
|
||||||
|
*/
|
||||||
|
toString(): string;
|
||||||
|
}
|
||||||
|
export {};
|
|
@ -20,38 +20,31 @@
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** @typedef { import('./member') } Member */
|
|
||||||
|
|
||||||
import {
|
|
||||||
MEMBER_LEVEL_MODERATOR, MEMBER_LEVEL_NONE, MEMBER_LEVEL_OWNER,
|
|
||||||
CIRCLE_CONFIG_REQUEST, CIRCLE_CONFIG_INVITE, CIRCLE_CONFIG_OPEN, CIRCLE_CONFIG_VISIBLE,
|
|
||||||
} from './constants'
|
|
||||||
|
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import Member from './member'
|
import Member from './member'
|
||||||
|
|
||||||
|
import { CircleConfigs, MemberLevels } from './constants'
|
||||||
|
|
||||||
|
type MemberList = Record<string, Member>
|
||||||
|
|
||||||
export default class Circle {
|
export default class Circle {
|
||||||
|
|
||||||
_data = {}
|
_data: any = {}
|
||||||
_members = {}
|
_members: MemberList = {}
|
||||||
|
_owner: Member
|
||||||
|
_initiator: Member
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance of Circle
|
* Creates an instance of Circle
|
||||||
*
|
|
||||||
* @param {Object} data the vcard data as string with proper new lines
|
|
||||||
* @param {object} circle the addressbook which the contat belongs to
|
|
||||||
* @memberof Circle
|
|
||||||
*/
|
*/
|
||||||
constructor(data) {
|
constructor(data: Object) {
|
||||||
this.updateData(data)
|
this.updateData(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update inner circle data, owner and initiator
|
* Update inner circle data, owner and initiator
|
||||||
* @param {Object} data the vcard data as string with proper new lines
|
|
||||||
* @memberof Circle
|
|
||||||
*/
|
*/
|
||||||
updateData(data) {
|
updateData(data: any) {
|
||||||
if (typeof data !== 'object') {
|
if (typeof data !== 'object') {
|
||||||
throw new Error('Invalid circle')
|
throw new Error('Invalid circle')
|
||||||
}
|
}
|
||||||
|
@ -62,145 +55,119 @@ export default class Circle {
|
||||||
}
|
}
|
||||||
|
|
||||||
this._data = data
|
this._data = data
|
||||||
this._data.initiator = new Member(data.initiator, this)
|
this._owner = new Member(data.owner, this)
|
||||||
this._data.owner = new Member(data.owner)
|
|
||||||
|
if (data.initiator) {
|
||||||
|
this._initiator = new Member(data.initiator, this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// METADATA -----------------------------------------
|
// METADATA -----------------------------------------
|
||||||
/**
|
/**
|
||||||
* Circle id
|
* Circle id
|
||||||
* @readonly
|
|
||||||
* @memberof Circle
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
*/
|
||||||
get id() {
|
get id(): string {
|
||||||
return this._data.id
|
return this._data.id
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formatted display name
|
* Formatted display name
|
||||||
* @readonly
|
|
||||||
* @memberof Circle
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
*/
|
||||||
get displayName() {
|
get displayName(): string {
|
||||||
return this._data.displayName
|
return this._data.displayName
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Circle creation date
|
* Circle creation date
|
||||||
* @readonly
|
|
||||||
* @memberof Circle
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
*/
|
||||||
get creation() {
|
get creation(): number {
|
||||||
return this._data.creation
|
return this._data.creation
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Circle description
|
* Circle description
|
||||||
* @readonly
|
|
||||||
* @memberof Circle
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
*/
|
||||||
get description() {
|
get description(): string {
|
||||||
return this._data.description
|
return this._data.description
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Circle description
|
* Circle description
|
||||||
* @param {string} text circle description
|
|
||||||
* @memberof Circle
|
|
||||||
*/
|
*/
|
||||||
set description(text) {
|
set description(text: string) {
|
||||||
this._data.description = text
|
this._data.description = text
|
||||||
}
|
}
|
||||||
|
|
||||||
// MEMBERSHIP -----------------------------------------
|
// MEMBERSHIP -----------------------------------------
|
||||||
/**
|
/**
|
||||||
* Circle initiator. This is the current
|
* Circle ini_initiator the current
|
||||||
* user info for this circle
|
* user info for this circle
|
||||||
* @readonly
|
|
||||||
* @memberof Circle
|
|
||||||
* @returns {Member}
|
|
||||||
*/
|
*/
|
||||||
get initiator() {
|
get initiator(): Member {
|
||||||
return this._data.initiator
|
return this._initiator
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Circle ownership
|
* Circle ownership
|
||||||
* @readonly
|
|
||||||
* @memberof Circle
|
|
||||||
* @returns {Member}
|
|
||||||
*/
|
*/
|
||||||
get owner() {
|
get owner(): Member {
|
||||||
return this._data.owner
|
return this._owner
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set new circle owner
|
* Set new circle owner
|
||||||
* @param {Member} owner circle owner
|
|
||||||
* @memberof Circle
|
|
||||||
*/
|
*/
|
||||||
set owner(owner) {
|
set owner(owner: Member) {
|
||||||
if (owner.constructor.name !== Member.name) {
|
if (owner.constructor.name !== Member.name) {
|
||||||
throw new Error('Owner must be a Member type')
|
throw new Error('Owner must be a Member type')
|
||||||
}
|
}
|
||||||
this._data.owner = owner
|
this._owner = owner
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Circle members
|
* Circle members
|
||||||
* @readonly
|
|
||||||
* @memberof Circle
|
|
||||||
* @returns {Member[]}
|
|
||||||
*/
|
*/
|
||||||
get members() {
|
get members(): MemberList {
|
||||||
return this._members
|
return this._members
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define members circle
|
* Define members circle
|
||||||
* @param {Member[]} members the members list
|
|
||||||
* @memberof Circle
|
|
||||||
*/
|
*/
|
||||||
set members(members) {
|
set members(members: MemberList) {
|
||||||
this._members = members
|
this._members = members
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a member to this circle
|
* Add a member to this circle
|
||||||
* @param {Member} member the member to add
|
|
||||||
*/
|
*/
|
||||||
addMember(member) {
|
addMember(member: Member) {
|
||||||
if (member.constructor.name !== Member.name) {
|
if (member.constructor.name !== Member.name) {
|
||||||
throw new Error('Member must be a Member type')
|
throw new Error('Member must be a Member type')
|
||||||
}
|
}
|
||||||
|
|
||||||
const uid = member.id
|
const singleId = member.singleId
|
||||||
if (this._members[uid]) {
|
if (this._members[singleId]) {
|
||||||
console.warn('Duplicate member overrided', this._members[uid], member)
|
console.warn('Ignoring duplicate member', member)
|
||||||
}
|
}
|
||||||
Vue.set(this._members, uid, member)
|
Vue.set(this._members, singleId, member)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a member from this circle
|
* Remove a member from this circle
|
||||||
* @param {Member} member the member to delete
|
|
||||||
*/
|
*/
|
||||||
deleteMember(member) {
|
deleteMember(member: Member) {
|
||||||
if (member.constructor.name !== Member.name) {
|
if (member.constructor.name !== Member.name) {
|
||||||
throw new Error('Member must be a Member type')
|
throw new Error('Member must be a Member type')
|
||||||
}
|
}
|
||||||
|
|
||||||
const uid = member.id
|
const singleId = member.singleId
|
||||||
if (!this._members[uid]) {
|
if (!this._members[singleId]) {
|
||||||
console.warn('The member was not in this circle. Nothing was done.', member)
|
console.warn('The member was not in this circle. Nothing was done.', member)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete and clear memory
|
// Delete and clear memory
|
||||||
Vue.delete(this._members, uid)
|
Vue.delete(this._members, singleId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CONFIGS --------------------------------------------
|
// CONFIGS --------------------------------------------
|
||||||
|
@ -210,9 +177,6 @@ export default class Circle {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Circle config
|
* Circle config
|
||||||
* @readonly
|
|
||||||
* @memberof Circle
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
*/
|
||||||
get config() {
|
get config() {
|
||||||
return this._data.config
|
return this._data.config
|
||||||
|
@ -220,91 +184,71 @@ export default class Circle {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Circle requires invite to be confirmed by moderator or above
|
* Circle requires invite to be confirmed by moderator or above
|
||||||
* @readonly
|
|
||||||
* @memberof Circle
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
*/
|
||||||
get requireJoinAccept() {
|
get requireJoinAccept() {
|
||||||
return (this._data.config & CIRCLE_CONFIG_REQUEST) !== 0
|
return (this._data.config & CircleConfigs.VISIBLE) !== 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Circle can be requested to join
|
* Circle can be requested to join
|
||||||
* @readonly
|
|
||||||
* @memberof Circle
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
*/
|
||||||
get canJoin() {
|
get canJoin() {
|
||||||
return (this._data.config & CIRCLE_CONFIG_OPEN) !== 0
|
return (this._data.config & CircleConfigs.OPEN) !== 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Circle is visible to others
|
* Circle is visible to others
|
||||||
* @readonly
|
|
||||||
* @memberof Circle
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
*/
|
||||||
get isVisible() {
|
get isVisible() {
|
||||||
return (this._data.config & CIRCLE_CONFIG_VISIBLE) !== 0
|
return (this._data.config & CircleConfigs.VISIBLE) !== 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Circle requires invite to be accepted by the member
|
* Circle requires invite to be accepted by the member
|
||||||
* @readonly
|
|
||||||
* @memberof Circle
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
*/
|
||||||
get requireInviteAccept() {
|
get requireInviteAccept() {
|
||||||
return (this._data.config & CIRCLE_CONFIG_INVITE) !== 0
|
return (this._data.config & CircleConfigs.INVITE) !== 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// PERMISSIONS SHORTCUTS ------------------------------
|
// PERMISSIONS SHORTCUTS ------------------------------
|
||||||
/**
|
/**
|
||||||
* Can the initiator add members to this circle?
|
* Can the initiator add members to this circle?
|
||||||
* @readonly
|
|
||||||
* @memberof Circle
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
*/
|
||||||
get isOwner() {
|
get isOwner() {
|
||||||
return this.initiator.level === MEMBER_LEVEL_OWNER
|
return this.initiator?.level === MemberLevels.OWNER
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is the initiator a member of this circle?
|
* Is the initiator a member of this circle?
|
||||||
* @readonly
|
|
||||||
* @memberof Circle
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
*/
|
||||||
get isMember() {
|
get isMember() {
|
||||||
return this.initiator.level > MEMBER_LEVEL_NONE
|
return this.initiator?.level > MemberLevels.NONE
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Can the initiator delete this circle?
|
* Can the initiator delete this circle?
|
||||||
* @readonly
|
|
||||||
* @memberof Circle
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
*/
|
||||||
get canDelete() {
|
get canDelete() {
|
||||||
return this.isOwner
|
return this.isOwner
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can the initiator leave this circle?
|
||||||
|
*/
|
||||||
|
get canLeave() {
|
||||||
|
return this.isMember && !this.isOwner
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Can the initiator add/remove members to this circle?
|
* Can the initiator add/remove members to this circle?
|
||||||
* @readonly
|
|
||||||
* @memberof Circle
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
*/
|
||||||
get canManageMembers() {
|
get canManageMembers() {
|
||||||
return this.initiator.level >= MEMBER_LEVEL_MODERATOR
|
return this.initiator?.level >= MemberLevels.MODERATOR
|
||||||
}
|
}
|
||||||
|
|
||||||
// PARAMS ---------------------------------------------
|
// PARAMS ---------------------------------------------
|
||||||
/**
|
/**
|
||||||
* Vue router param
|
* Vue router param
|
||||||
* @readonly
|
|
||||||
* @memberof Circle
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
*/
|
||||||
get router() {
|
get router() {
|
||||||
return {
|
return {
|
||||||
|
@ -316,8 +260,6 @@ export default class Circle {
|
||||||
/**
|
/**
|
||||||
* Default javascript fallback
|
* Default javascript fallback
|
||||||
* Used for sorting as well
|
* Used for sorting as well
|
||||||
* @memberof Circle
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
*/
|
||||||
toString() {
|
toString() {
|
||||||
return this.displayName
|
return this.displayName
|
|
@ -0,0 +1,68 @@
|
||||||
|
/**
|
||||||
|
* @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/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export declare type CircleConfig = number;
|
||||||
|
export declare type MemberLevel = number;
|
||||||
|
export declare type MemberType = number;
|
||||||
|
export declare const LIST_SIZE = 60;
|
||||||
|
export declare const GROUP_ALL_CONTACTS: string;
|
||||||
|
export declare const GROUP_NO_GROUP_CONTACTS: string;
|
||||||
|
export declare const GROUP_RECENTLY_CONTACTED: string;
|
||||||
|
export declare const ROUTE_CIRCLE = "circle";
|
||||||
|
export declare const ELLIPSIS_COUNT = 5;
|
||||||
|
export declare const CIRCLES_MEMBER_TYPES: {
|
||||||
|
[x: number]: string;
|
||||||
|
};
|
||||||
|
export declare const CIRCLES_MEMBER_LEVELS: {
|
||||||
|
[x: number]: string;
|
||||||
|
};
|
||||||
|
export declare const SHARES_TYPES_MEMBER_MAP: {
|
||||||
|
[x: number]: number;
|
||||||
|
};
|
||||||
|
export declare enum MemberLevels {
|
||||||
|
NONE,
|
||||||
|
MEMBER,
|
||||||
|
MODERATOR,
|
||||||
|
ADMIN,
|
||||||
|
OWNER
|
||||||
|
}
|
||||||
|
export declare enum MemberTypes {
|
||||||
|
CIRCLE,
|
||||||
|
USER,
|
||||||
|
GROUP,
|
||||||
|
MAIL,
|
||||||
|
CONTACT
|
||||||
|
}
|
||||||
|
export declare enum CircleConfigs {
|
||||||
|
SYSTEM,
|
||||||
|
VISIBLE,
|
||||||
|
OPEN,
|
||||||
|
INVITE,
|
||||||
|
REQUEST,
|
||||||
|
FRIEND,
|
||||||
|
PROTECTED,
|
||||||
|
NO_OWNER,
|
||||||
|
HIDDEN,
|
||||||
|
BACKEND,
|
||||||
|
ROOT,
|
||||||
|
CIRCLE_INVITE,
|
||||||
|
FEDERATED
|
||||||
|
}
|
|
@ -1,75 +0,0 @@
|
||||||
/**
|
|
||||||
* @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/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
/* eslint-disable no-tabs */
|
|
||||||
|
|
||||||
// Dynamic groups
|
|
||||||
export const GROUP_ALL_CONTACTS = t('contacts', 'All contacts')
|
|
||||||
export const GROUP_NO_GROUP_CONTACTS = t('contacts', 'Not grouped')
|
|
||||||
export const GROUP_RECENTLY_CONTACTED = t('contactsinteraction', 'Recently contacted')
|
|
||||||
|
|
||||||
// Default max number of items to show in the navigation
|
|
||||||
export const ELLIPSIS_COUNT = 5
|
|
||||||
|
|
||||||
// Circles member levels
|
|
||||||
export const MEMBER_LEVEL_NONE = 0
|
|
||||||
export const MEMBER_LEVEL_MEMBER = 1
|
|
||||||
export const MEMBER_LEVEL_MODERATOR = 4
|
|
||||||
export const MEMBER_LEVEL_ADMIN = 8
|
|
||||||
export const MEMBER_LEVEL_OWNER = 9
|
|
||||||
|
|
||||||
// Circles member types
|
|
||||||
export const MEMBER_TYPE_CIRCLE = 16
|
|
||||||
export const MEMBER_TYPE_USER = 1
|
|
||||||
export const MEMBER_TYPE_GROUP = 2
|
|
||||||
export const MEMBER_TYPE_MAIL = 3
|
|
||||||
export const MEMBER_TYPE_CONTACT = 4
|
|
||||||
|
|
||||||
// Circles config flags
|
|
||||||
export const CIRCLE_CONFIG_SYSTEM = 4 // System Circle (not managed by the official front-end). Meaning some config are limited
|
|
||||||
export const CIRCLE_CONFIG_VISIBLE = 8 // Visible to everyone, if not visible, people have to know its name to be able to find it
|
|
||||||
export const CIRCLE_CONFIG_OPEN = 16 // Circle is open, people can join
|
|
||||||
export const CIRCLE_CONFIG_INVITE = 32 // Adding a member generate an invitation that needs to be accepted
|
|
||||||
export const CIRCLE_CONFIG_REQUEST = 64 // Request to join Circles needs to be confirmed by a moderator
|
|
||||||
export const CIRCLE_CONFIG_FRIEND = 128 // Members of the circle can invite their friends
|
|
||||||
export const CIRCLE_CONFIG_PROTECTED = 256 // Password protected to join/request
|
|
||||||
export const CIRCLE_CONFIG_NO_OWNER = 512 // no owner, only members
|
|
||||||
export const CIRCLE_CONFIG_HIDDEN = 1024 // hidden from listing, but available as a share entity
|
|
||||||
export const CIRCLE_CONFIG_BACKEND = 2048 // Fully hidden, only backend Circles
|
|
||||||
export const CIRCLE_CONFIG_ROOT = 4096 // Circle cannot be inside another Circle
|
|
||||||
export const CIRCLE_CONFIG_CIRCLE_INVITE = 8192 // Circle must confirm when invited in another circle
|
|
||||||
export const CIRCLE_CONFIG_FEDERATED = 16384 // Federated
|
|
||||||
|
|
||||||
export const CIRCLES_MEMBER_TYPES = {
|
|
||||||
[MEMBER_TYPE_CIRCLE]: t('circles', 'Circle'),
|
|
||||||
[MEMBER_TYPE_USER]: t('circles', 'User'),
|
|
||||||
[MEMBER_TYPE_GROUP]: t('circles', 'Group'),
|
|
||||||
[MEMBER_TYPE_MAIL]: t('circles', 'Mail'),
|
|
||||||
[MEMBER_TYPE_CONTACT]: t('circles', 'Contact'),
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CIRCLES_MEMBER_LEVELS = {
|
|
||||||
// [MEMBER_LEVEL_NONE]: t('circles', 'None'),
|
|
||||||
[MEMBER_LEVEL_MEMBER]: t('circles', 'Member'),
|
|
||||||
[MEMBER_LEVEL_MODERATOR]: t('circles', 'Moderator'),
|
|
||||||
[MEMBER_LEVEL_ADMIN]: t('circles', 'Admin'),
|
|
||||||
[MEMBER_LEVEL_OWNER]: t('circles', 'Owner'),
|
|
||||||
}
|
|
|
@ -0,0 +1,133 @@
|
||||||
|
/**
|
||||||
|
* @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/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
/// <reference types="@nextcloud/typings" />
|
||||||
|
|
||||||
|
import { translate as t } from '@nextcloud/l10n'
|
||||||
|
|
||||||
|
interface OC extends Nextcloud.Common.OC {
|
||||||
|
Share: any
|
||||||
|
}
|
||||||
|
declare const OC: OC
|
||||||
|
|
||||||
|
export type CircleConfig = number
|
||||||
|
export type MemberLevel = number
|
||||||
|
export type MemberType = number
|
||||||
|
|
||||||
|
// Global sizes
|
||||||
|
export const LIST_SIZE = 60
|
||||||
|
|
||||||
|
// Dynamic groups
|
||||||
|
export const GROUP_ALL_CONTACTS = t('contacts', 'All contacts')
|
||||||
|
export const GROUP_NO_GROUP_CONTACTS = t('contacts', 'Not grouped')
|
||||||
|
export const GROUP_RECENTLY_CONTACTED = t('contactsinteraction', 'Recently contacted')
|
||||||
|
|
||||||
|
// Circle route, see vue-router conf
|
||||||
|
export const ROUTE_CIRCLE = 'circle'
|
||||||
|
|
||||||
|
// Default max number of items to show in the navigation
|
||||||
|
export const ELLIPSIS_COUNT = 5
|
||||||
|
|
||||||
|
// Circles member levels
|
||||||
|
const MEMBER_LEVEL_NONE: MemberLevel = 0
|
||||||
|
const MEMBER_LEVEL_MEMBER: MemberLevel = 1
|
||||||
|
const MEMBER_LEVEL_MODERATOR: MemberLevel = 4
|
||||||
|
const MEMBER_LEVEL_ADMIN: MemberLevel = 8
|
||||||
|
const MEMBER_LEVEL_OWNER: MemberLevel = 9
|
||||||
|
|
||||||
|
// Circles member types
|
||||||
|
const MEMBER_TYPE_SINGLEID: MemberType = 0
|
||||||
|
const MEMBER_TYPE_USER: MemberType = 1
|
||||||
|
const MEMBER_TYPE_GROUP : MemberType= 2
|
||||||
|
const MEMBER_TYPE_MAIL: MemberType = 4
|
||||||
|
const MEMBER_TYPE_CONTACT: MemberType = 8
|
||||||
|
const MEMBER_TYPE_CIRCLE: MemberType = 16
|
||||||
|
|
||||||
|
// Circles config flags
|
||||||
|
const CIRCLE_CONFIG_SYSTEM: CircleConfig = 4 // System Circle (not managed by the official front-end). Meaning some config are limited
|
||||||
|
const CIRCLE_CONFIG_VISIBLE: CircleConfig = 8 // Visible to everyone, if not visible, people have to know its name to be able to find it
|
||||||
|
const CIRCLE_CONFIG_OPEN: CircleConfig = 16 // Circle is open, people can join
|
||||||
|
const CIRCLE_CONFIG_INVITE: CircleConfig = 32 // Adding a member generate an invitation that needs to be accepted
|
||||||
|
const CIRCLE_CONFIG_REQUEST: CircleConfig = 64 // Request to join Circles needs to be confirmed by a moderator
|
||||||
|
const CIRCLE_CONFIG_FRIEND: CircleConfig = 128 // Members of the circle can invite their friends
|
||||||
|
const CIRCLE_CONFIG_PROTECTED: CircleConfig = 256 // Password protected to join/request
|
||||||
|
const CIRCLE_CONFIG_NO_OWNER: CircleConfig = 512 // no owner, only members
|
||||||
|
const CIRCLE_CONFIG_HIDDEN: CircleConfig = 1024 // hidden from listing, but available as a share entity
|
||||||
|
const CIRCLE_CONFIG_BACKEND: CircleConfig = 2048 // Fully hidden, only backend Circles
|
||||||
|
const CIRCLE_CONFIG_ROOT: CircleConfig = 4096 // Circle cannot be inside another Circle
|
||||||
|
const CIRCLE_CONFIG_CIRCLE_INVITE: CircleConfig = 8192 // Circle must confirm when invited in another circle
|
||||||
|
const CIRCLE_CONFIG_FEDERATED: CircleConfig = 16384 // Federated
|
||||||
|
|
||||||
|
export const CIRCLES_MEMBER_TYPES = {
|
||||||
|
[MEMBER_TYPE_CIRCLE]: t('circles', 'Circle'),
|
||||||
|
[MEMBER_TYPE_USER]: t('circles', 'User'),
|
||||||
|
[MEMBER_TYPE_GROUP]: t('circles', 'Group'),
|
||||||
|
[MEMBER_TYPE_MAIL]: t('circles', 'Mail'),
|
||||||
|
[MEMBER_TYPE_CONTACT]: t('circles', 'Contact'),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CIRCLES_MEMBER_LEVELS = {
|
||||||
|
// [MEMBER_LEVEL_NONE]: t('circles', 'None'),
|
||||||
|
[MEMBER_LEVEL_MEMBER]: t('circles', 'Member'),
|
||||||
|
[MEMBER_LEVEL_MODERATOR]: t('circles', 'Moderator'),
|
||||||
|
[MEMBER_LEVEL_ADMIN]: t('circles', 'Admin'),
|
||||||
|
[MEMBER_LEVEL_OWNER]: t('circles', 'Owner'),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SHARES_TYPES_MEMBER_MAP = {
|
||||||
|
[OC.Share.SHARE_TYPE_CIRCLE]: MEMBER_TYPE_SINGLEID,
|
||||||
|
[OC.Share.SHARE_TYPE_USER]: MEMBER_TYPE_USER,
|
||||||
|
[OC.Share.SHARE_TYPE_GROUP]: MEMBER_TYPE_GROUP,
|
||||||
|
[OC.Share.SHARE_TYPE_EMAIL]: MEMBER_TYPE_MAIL,
|
||||||
|
// []: MEMBER_TYPE_CONTACT,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MemberLevels {
|
||||||
|
NONE = MEMBER_LEVEL_NONE,
|
||||||
|
MEMBER = MEMBER_LEVEL_MEMBER,
|
||||||
|
MODERATOR = MEMBER_LEVEL_MODERATOR,
|
||||||
|
ADMIN = MEMBER_LEVEL_ADMIN,
|
||||||
|
OWNER = MEMBER_LEVEL_OWNER,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MemberTypes {
|
||||||
|
CIRCLE = MEMBER_TYPE_CIRCLE,
|
||||||
|
USER = MEMBER_TYPE_USER,
|
||||||
|
GROUP = MEMBER_TYPE_GROUP,
|
||||||
|
MAIL = MEMBER_TYPE_MAIL,
|
||||||
|
CONTACT = MEMBER_TYPE_CONTACT,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CircleConfigs {
|
||||||
|
SYSTEM = CIRCLE_CONFIG_SYSTEM,
|
||||||
|
VISIBLE = CIRCLE_CONFIG_VISIBLE,
|
||||||
|
OPEN = CIRCLE_CONFIG_OPEN,
|
||||||
|
INVITE = CIRCLE_CONFIG_INVITE,
|
||||||
|
REQUEST = CIRCLE_CONFIG_REQUEST,
|
||||||
|
FRIEND = CIRCLE_CONFIG_FRIEND,
|
||||||
|
PROTECTED = CIRCLE_CONFIG_PROTECTED,
|
||||||
|
NO_OWNER = CIRCLE_CONFIG_NO_OWNER,
|
||||||
|
HIDDEN = CIRCLE_CONFIG_HIDDEN,
|
||||||
|
BACKEND = CIRCLE_CONFIG_BACKEND,
|
||||||
|
ROOT = CIRCLE_CONFIG_ROOT,
|
||||||
|
CIRCLE_INVITE = CIRCLE_CONFIG_CIRCLE_INVITE,
|
||||||
|
FEDERATED = CIRCLE_CONFIG_FEDERATED,
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2018 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 Circle from './circle';
|
||||||
|
import { MemberLevel } from './constants';
|
||||||
|
export default class Member {
|
||||||
|
_data: any;
|
||||||
|
_circle: Circle;
|
||||||
|
/**
|
||||||
|
* Creates an instance of Member
|
||||||
|
*/
|
||||||
|
constructor(data: any, circle: Circle);
|
||||||
|
/**
|
||||||
|
* Get the circle of this member
|
||||||
|
*/
|
||||||
|
get circle(): Circle;
|
||||||
|
/**
|
||||||
|
* Set the circle of this member
|
||||||
|
*/
|
||||||
|
set circle(circle: Circle);
|
||||||
|
/**
|
||||||
|
* Member id
|
||||||
|
*/
|
||||||
|
get id(): string;
|
||||||
|
/**
|
||||||
|
* Single uid
|
||||||
|
*/
|
||||||
|
get singleId(): string;
|
||||||
|
/**
|
||||||
|
* Formatted display name
|
||||||
|
*/
|
||||||
|
get displayName(): string;
|
||||||
|
/**
|
||||||
|
* Member userId
|
||||||
|
*/
|
||||||
|
get userId(): string;
|
||||||
|
/**
|
||||||
|
* Member level
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
get level(): MemberLevel;
|
||||||
|
/**
|
||||||
|
* Set member level
|
||||||
|
*/
|
||||||
|
set level(level: MemberLevel);
|
||||||
|
/**
|
||||||
|
* Is the current member a user?
|
||||||
|
*/
|
||||||
|
get isUser(): boolean;
|
||||||
|
/**
|
||||||
|
* Is the current member without a circle?
|
||||||
|
*/
|
||||||
|
get isOrphan(): boolean;
|
||||||
|
/**
|
||||||
|
* Delete this member and any reference from its circle
|
||||||
|
*/
|
||||||
|
delete(): void;
|
||||||
|
}
|
|
@ -20,25 +20,18 @@
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** @typedef { import('./circle') } Circle */
|
|
||||||
|
|
||||||
import { MEMBER_TYPE_USER } from './constants'
|
|
||||||
|
|
||||||
import Circle from './circle'
|
import Circle from './circle'
|
||||||
|
import { MemberLevel, MemberLevels } from './constants'
|
||||||
|
|
||||||
export default class Member {
|
export default class Member {
|
||||||
|
|
||||||
/** @typedef Circle */
|
_data: any = {}
|
||||||
_circle
|
_circle: Circle
|
||||||
_data = {}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance of Contact
|
* Creates an instance of Member
|
||||||
*
|
|
||||||
* @param {Object} data the vcard data as string with proper new lines
|
|
||||||
* @param {Circle} circle the addressbook which the contat belongs to
|
|
||||||
* @memberof Member
|
|
||||||
*/
|
*/
|
||||||
constructor(data, circle) {
|
constructor(data: any, circle: Circle) {
|
||||||
if (typeof data !== 'object') {
|
if (typeof data !== 'object') {
|
||||||
throw new Error('Invalid member')
|
throw new Error('Invalid member')
|
||||||
}
|
}
|
||||||
|
@ -55,19 +48,15 @@ export default class Member {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the circle of this member
|
* Get the circle of this member
|
||||||
* @readonly
|
|
||||||
* @memberof Member
|
|
||||||
*/
|
*/
|
||||||
get circle() {
|
get circle(): Circle {
|
||||||
return this._circle
|
return this._circle
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the circle of this member
|
* Set the circle of this member
|
||||||
* @param {Circle} circle the circle
|
|
||||||
* @memberof Member
|
|
||||||
*/
|
*/
|
||||||
set circle(circle) {
|
set circle(circle: Circle) {
|
||||||
if (circle.constructor.name !== Circle.name) {
|
if (circle.constructor.name !== Circle.name) {
|
||||||
throw new Error('circle must be a Circle type')
|
throw new Error('circle must be a Circle type')
|
||||||
}
|
}
|
||||||
|
@ -76,54 +65,59 @@ export default class Member {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Member id
|
* Member id
|
||||||
* @readonly
|
|
||||||
* @memberof Member
|
|
||||||
*/
|
*/
|
||||||
get id() {
|
get id(): string {
|
||||||
return this._data.id
|
return this._data.id
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formatted display name
|
* Single uid
|
||||||
* @readonly
|
|
||||||
* @memberof Member
|
|
||||||
*/
|
*/
|
||||||
get displayName() {
|
get singleId(): string {
|
||||||
|
return this._data.singleId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formatted display name
|
||||||
|
*/
|
||||||
|
get displayName(): string {
|
||||||
return this._data.displayName
|
return this._data.displayName
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Member userId
|
* Member userId
|
||||||
* @readonly
|
|
||||||
* @memberof Member
|
|
||||||
*/
|
*/
|
||||||
get userId() {
|
get userId(): string {
|
||||||
return this._data.userId
|
return this._data.userId
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Member level
|
* Member level
|
||||||
* @see file src/models/constants.js
|
*
|
||||||
* @readonly
|
|
||||||
* @memberof Member
|
|
||||||
*/
|
*/
|
||||||
get level() {
|
get level(): MemberLevel {
|
||||||
return this._data.level
|
return this._data.level
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set member level
|
||||||
|
*/
|
||||||
|
set level(level: MemberLevel) {
|
||||||
|
if (!(level in MemberLevels)) {
|
||||||
|
throw new Error('Invalid level')
|
||||||
|
}
|
||||||
|
this._data.level = level
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is the current member a user?
|
* Is the current member a user?
|
||||||
* @readonly
|
|
||||||
* @memberof Member
|
|
||||||
*/
|
*/
|
||||||
get isUser() {
|
get isUser() {
|
||||||
return this._data.userType === MEMBER_TYPE_USER
|
return this._data.userType === MemberLevels.MEMBER
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is the current member without a circle?
|
* Is the current member without a circle?
|
||||||
* @readonly
|
|
||||||
* @memberof Member
|
|
||||||
*/
|
*/
|
||||||
get isOrphan() {
|
get isOrphan() {
|
||||||
return this._circle?.constructor?.name !== 'Circle'
|
return this._circle?.constructor?.name !== 'Circle'
|
||||||
|
@ -137,7 +131,6 @@ export default class Member {
|
||||||
throw new Error('Cannot delete this member as it doesn\'t belong to any circle')
|
throw new Error('Cannot delete this member as it doesn\'t belong to any circle')
|
||||||
}
|
}
|
||||||
this.circle.deleteMember(this)
|
this.circle.deleteMember(this)
|
||||||
this._circle = undefined
|
|
||||||
this._data = undefined
|
this._data = undefined
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,8 @@
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import Router from 'vue-router'
|
import Router from 'vue-router'
|
||||||
import { generateUrl } from '@nextcloud/router'
|
import { generateUrl } from '@nextcloud/router'
|
||||||
|
|
||||||
|
import { ROUTE_CIRCLE } from '../models/constants.ts'
|
||||||
import Contacts from '../views/Contacts'
|
import Contacts from '../views/Contacts'
|
||||||
|
|
||||||
Vue.use(Router)
|
Vue.use(Router)
|
||||||
|
@ -51,13 +53,13 @@ export default new Router({
|
||||||
component: Contacts,
|
component: Contacts,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ':selectedGroup',
|
path: `${ROUTE_CIRCLE}/:selectedCircle`,
|
||||||
name: 'group',
|
name: 'circle',
|
||||||
component: Contacts,
|
component: Contacts,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'circle/:selectedCircle',
|
path: ':selectedGroup',
|
||||||
name: 'circle',
|
name: 'group',
|
||||||
component: Contacts,
|
component: Contacts,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
/**
|
||||||
|
* @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 { MemberLevel, MemberType } from '../models/constants';
|
||||||
|
interface MemberPairs {
|
||||||
|
id: string;
|
||||||
|
type: MemberType;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get the circles list without the members
|
||||||
|
*
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
export declare const getCircles: () => Promise<any>;
|
||||||
|
/**
|
||||||
|
* Create a new circle
|
||||||
|
*
|
||||||
|
* @param {string} name the circle name
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
export declare const createCircle: (name: string) => Promise<any>;
|
||||||
|
/**
|
||||||
|
* Delete an existing circle
|
||||||
|
*
|
||||||
|
* @param {string} circleId the circle name
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
export declare const deleteCircle: (circleId: string) => Promise<any>;
|
||||||
|
/**
|
||||||
|
* Join a circle
|
||||||
|
*
|
||||||
|
* @param {string} circleId the circle name
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
export declare const joinCircle: (circleId: string) => Promise<any>;
|
||||||
|
/**
|
||||||
|
* Leave a circle
|
||||||
|
*
|
||||||
|
* @param {string} circleId the circle name
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
export declare const leaveCircle: (circleId: string) => Promise<any>;
|
||||||
|
/**
|
||||||
|
* Get the circle members without the members
|
||||||
|
*
|
||||||
|
* @param {string} circleId the circle id
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
export declare const getCircleMembers: (circleId: string) => Promise<any>;
|
||||||
|
/**
|
||||||
|
* Search a potential circle member
|
||||||
|
*
|
||||||
|
* @param {string} term the search query
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
export declare const searchMember: (term: string) => Promise<any>;
|
||||||
|
/**
|
||||||
|
* Add a circle member
|
||||||
|
*
|
||||||
|
* @param {string} circleId the circle id
|
||||||
|
* @param {string} members the member id
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
export declare const addMembers: (circleId: string, members: Array<MemberPairs>) => Promise<any>;
|
||||||
|
/**
|
||||||
|
* Delete a circle member
|
||||||
|
*
|
||||||
|
* @param {string} circleId the circle id
|
||||||
|
* @param {string} memberId the member id
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
export declare const deleteMember: (circleId: string, memberId: string) => Promise<unknown[]>;
|
||||||
|
/**
|
||||||
|
* change a member level
|
||||||
|
* @see levels file src/models/constants.js
|
||||||
|
*
|
||||||
|
* @param {string} circleId the circle id
|
||||||
|
* @param {string} memberId the member id
|
||||||
|
* @param {number} level the new member level
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
export declare const changeMemberLevel: (circleId: string, memberId: string, level: MemberLevel) => Promise<unknown[]>;
|
||||||
|
export {};
|
|
@ -22,9 +22,12 @@
|
||||||
|
|
||||||
import axios from '@nextcloud/axios'
|
import axios from '@nextcloud/axios'
|
||||||
import { generateOcsUrl } from '@nextcloud/router'
|
import { generateOcsUrl } from '@nextcloud/router'
|
||||||
import { CIRCLES_MEMBER_LEVELS } from '../models/constants'
|
import { MemberLevel, MemberLevels, MemberType } from '../models/constants'
|
||||||
|
|
||||||
const baseApi = generateOcsUrl('apps/circles', 2)
|
interface MemberPairs {
|
||||||
|
id: string,
|
||||||
|
type: MemberType
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the circles list without the members
|
* Get the circles list without the members
|
||||||
|
@ -32,7 +35,7 @@ const baseApi = generateOcsUrl('apps/circles', 2)
|
||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
*/
|
*/
|
||||||
export const getCircles = async function() {
|
export const getCircles = async function() {
|
||||||
const response = await axios.get(baseApi + 'circles')
|
const response = await axios.get(generateOcsUrl('apps/circles/circles'))
|
||||||
return response.data.ocs.data
|
return response.data.ocs.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,8 +45,8 @@ export const getCircles = async function() {
|
||||||
* @param {string} name the circle name
|
* @param {string} name the circle name
|
||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
export const createCircle = async function(name) {
|
export const createCircle = async function(name: string) {
|
||||||
const response = await axios.post(baseApi + 'circles', {
|
const response = await axios.post(generateOcsUrl('apps/circles/circles'), {
|
||||||
name,
|
name,
|
||||||
})
|
})
|
||||||
return response.data.ocs.data
|
return response.data.ocs.data
|
||||||
|
@ -55,8 +58,8 @@ export const createCircle = async function(name) {
|
||||||
* @param {string} circleId the circle name
|
* @param {string} circleId the circle name
|
||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
export const deleteCircle = async function(circleId) {
|
export const deleteCircle = async function(circleId: string) {
|
||||||
const response = await axios.delete(baseApi + `circles/${circleId}`)
|
const response = await axios.delete(generateOcsUrl('apps/circles/circles/{circleId}', { circleId }))
|
||||||
return response.data.ocs.data
|
return response.data.ocs.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,8 +69,8 @@ export const deleteCircle = async function(circleId) {
|
||||||
* @param {string} circleId the circle name
|
* @param {string} circleId the circle name
|
||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
*/
|
*/
|
||||||
export const joinCircle = async function(circleId) {
|
export const joinCircle = async function(circleId: string) {
|
||||||
const response = await axios.put(baseApi + `circles/${circleId}/join`)
|
const response = await axios.put(generateOcsUrl('apps/circles/circles/{circleId}/join', { circleId }))
|
||||||
return response.data.ocs.data
|
return response.data.ocs.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,8 +80,8 @@ export const joinCircle = async function(circleId) {
|
||||||
* @param {string} circleId the circle name
|
* @param {string} circleId the circle name
|
||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
*/
|
*/
|
||||||
export const leaveCircle = async function(circleId) {
|
export const leaveCircle = async function(circleId: string) {
|
||||||
const response = await axios.put(baseApi + `circles/${circleId}/leave`)
|
const response = await axios.put(generateOcsUrl('apps/circles/circles/{circleId}/leave', { circleId }))
|
||||||
return response.data.ocs.data
|
return response.data.ocs.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,21 +91,32 @@ export const leaveCircle = async function(circleId) {
|
||||||
* @param {string} circleId the circle id
|
* @param {string} circleId the circle id
|
||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
*/
|
*/
|
||||||
export const getCircleMembers = async function(circleId) {
|
export const getCircleMembers = async function(circleId: string) {
|
||||||
const response = await axios.get(baseApi + `circles/${circleId}/members`)
|
const response = await axios.get(generateOcsUrl('apps/circles/circles/{circleId}/members', { circleId }))
|
||||||
return Object.values(response.data.ocs.data)
|
return response.data.ocs.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search a potential circle member
|
||||||
|
*
|
||||||
|
* @param {string} term the search query
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
export const searchMember = async function(term: string) {
|
||||||
|
const response = await axios.get(generateOcsUrl('apps/circles/search?term={term}', { term }))
|
||||||
|
return response.data.ocs.data
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a circle member
|
* Add a circle member
|
||||||
*
|
*
|
||||||
* @param {string} circleId the circle id
|
* @param {string} circleId the circle id
|
||||||
* @param {string} memberId the member id
|
* @param {string} members the member id
|
||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
*/
|
*/
|
||||||
export const addMember = async function(circleId, memberId) {
|
export const addMembers = async function(circleId: string, members: Array<MemberPairs>) {
|
||||||
const response = await axios.delete(baseApi + `circles/${circleId}/members/${memberId}`)
|
const response = await axios.post(generateOcsUrl('apps/circles/circles/{circleId}/members/multi', { circleId }), { members })
|
||||||
return Object.values(response.data.ocs.data)
|
return response.data.ocs.data
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -112,8 +126,8 @@ export const addMember = async function(circleId, memberId) {
|
||||||
* @param {string} memberId the member id
|
* @param {string} memberId the member id
|
||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
*/
|
*/
|
||||||
export const deleteMember = async function(circleId, memberId) {
|
export const deleteMember = async function(circleId: string, memberId: string) {
|
||||||
const response = await axios.delete(baseApi + `circles/${circleId}/members/${memberId}`)
|
const response = await axios.delete(generateOcsUrl('apps/circles/circles/{circleId}/members/{memberId}', { circleId, memberId }))
|
||||||
return Object.values(response.data.ocs.data)
|
return Object.values(response.data.ocs.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,12 +140,12 @@ export const deleteMember = async function(circleId, memberId) {
|
||||||
* @param {number} level the new member level
|
* @param {number} level the new member level
|
||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
*/
|
*/
|
||||||
export const changeMemberLevel = async function(circleId, memberId, level) {
|
export const changeMemberLevel = async function(circleId: string, memberId: string, level: MemberLevel) {
|
||||||
if (!(level in CIRCLES_MEMBER_LEVELS)) {
|
if (!(level in MemberLevels)) {
|
||||||
throw new Error('Invalid level. Valid levels are', CIRCLES_MEMBER_LEVELS)
|
throw new Error('Invalid level.')
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await axios.put(baseApi + `circles/${circleId}/members/${memberId}}/level`, {
|
const response = await axios.put(generateOcsUrl('apps/circles/circles/{circleId}/members/{memberId}/level', { circleId, memberId }), {
|
||||||
level,
|
level,
|
||||||
})
|
})
|
||||||
return Object.values(response.data.ocs.data)
|
return Object.values(response.data.ocs.data)
|
|
@ -0,0 +1,141 @@
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2018 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 axios from '@nextcloud/axios'
|
||||||
|
import { generateOcsUrl } from '@nextcloud/router'
|
||||||
|
|
||||||
|
const maxAutocompleteResults = parseInt(OC.config['sharing.maxAutocompleteResults'], 10) || 25
|
||||||
|
|
||||||
|
export const shareType = [
|
||||||
|
OC.Share.SHARE_TYPE_USER,
|
||||||
|
OC.Share.SHARE_TYPE_GROUP,
|
||||||
|
// OC.Share.SHARE_TYPE_REMOTE,
|
||||||
|
// OC.Share.SHARE_TYPE_REMOTE_GROUP,
|
||||||
|
OC.Share.SHARE_TYPE_CIRCLE,
|
||||||
|
// OC.Share.SHARE_TYPE_ROOM,
|
||||||
|
// OC.Share.SHARE_TYPE_GUEST,
|
||||||
|
// OC.Share.SHARE_TYPE_DECK,
|
||||||
|
OC.Share.SHARE_TYPE_EMAIL,
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get suggestions
|
||||||
|
*
|
||||||
|
* @param {string} search the search query
|
||||||
|
*/
|
||||||
|
export const getSuggestions = async function(search) {
|
||||||
|
const request = await axios.get(generateOcsUrl('apps/files_sharing/api/v1/sharees'), {
|
||||||
|
params: {
|
||||||
|
format: 'json',
|
||||||
|
itemType: 'file',
|
||||||
|
search,
|
||||||
|
perPage: maxAutocompleteResults,
|
||||||
|
shareType,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = request.data.ocs.data
|
||||||
|
const exact = request.data.ocs.data.exact
|
||||||
|
data.exact = [] // removing exact from general results
|
||||||
|
|
||||||
|
// flatten array of arrays
|
||||||
|
const rawExactSuggestions = Object.values(exact).reduce((arr, elem) => arr.concat(elem), [])
|
||||||
|
const rawSuggestions = Object.values(data).reduce((arr, elem) => arr.concat(elem), [])
|
||||||
|
|
||||||
|
// remove invalid data and format to user-select layout
|
||||||
|
const exactSuggestions = rawExactSuggestions
|
||||||
|
.filter(result => typeof result === 'object')
|
||||||
|
.map(share => formatResults(share))
|
||||||
|
// sort by type so we can get user&groups first...
|
||||||
|
.sort((a, b) => a.shareType - b.shareType)
|
||||||
|
const suggestions = rawSuggestions
|
||||||
|
.filter(result => typeof result === 'object')
|
||||||
|
.map(share => formatResults(share))
|
||||||
|
// sort by type so we can get user&groups first...
|
||||||
|
.sort((a, b) => a.shareType - b.shareType)
|
||||||
|
|
||||||
|
const allSuggestions = exactSuggestions.concat(suggestions)
|
||||||
|
|
||||||
|
// Count occurances of display names in order to provide a distinguishable description if needed
|
||||||
|
const nameCounts = allSuggestions.reduce((nameCounts, result) => {
|
||||||
|
if (!result.displayName) {
|
||||||
|
return nameCounts
|
||||||
|
}
|
||||||
|
if (!nameCounts[result.displayName]) {
|
||||||
|
nameCounts[result.displayName] = 0
|
||||||
|
}
|
||||||
|
nameCounts[result.displayName]++
|
||||||
|
return nameCounts
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const finalResults = allSuggestions.map(item => {
|
||||||
|
// Make sure that items with duplicate displayName get the shareWith applied as a description
|
||||||
|
if (nameCounts[item.displayName] > 1 && !item.desc) {
|
||||||
|
return { ...item, desc: item.shareWithDisplayNameUnique }
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
|
||||||
|
console.info('suggestions', finalResults)
|
||||||
|
|
||||||
|
return finalResults
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the sharing recommendations
|
||||||
|
*/
|
||||||
|
export const getRecommendations = async function() {
|
||||||
|
const request = await axios.get(generateOcsUrl('apps/files_sharing/api/v1/sharees_recommended'), {
|
||||||
|
params: {
|
||||||
|
format: 'json',
|
||||||
|
itemType: 'file',
|
||||||
|
shareType,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// flatten array of arrays
|
||||||
|
const exact = request.data.ocs.data.exact
|
||||||
|
const recommendations = Object.values(exact).reduce((arr, elem) => arr.concat(elem), [])
|
||||||
|
|
||||||
|
// remove invalid data and format to user-select layout
|
||||||
|
const finalResults = recommendations
|
||||||
|
.map(share => formatResults(share))
|
||||||
|
|
||||||
|
console.info('recommendations', finalResults)
|
||||||
|
|
||||||
|
return finalResults
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatResults = function(result) {
|
||||||
|
const type = `picker-${result.value.shareType}`
|
||||||
|
return {
|
||||||
|
label: result.label,
|
||||||
|
id: `${type}-${result.value.shareWith}`,
|
||||||
|
// If this is a user, set as user for avatar display by UserBubble
|
||||||
|
user: result.value.shareType === OC.Share.SHARE_TYPE_USER
|
||||||
|
? result.value.shareWith
|
||||||
|
: null,
|
||||||
|
type,
|
||||||
|
...result.value,
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,9 +23,9 @@
|
||||||
import { showError } from '@nextcloud/dialogs'
|
import { showError } from '@nextcloud/dialogs'
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
|
||||||
import { createCircle, deleteCircle, deleteMember, getCircleMembers, getCircles, leaveCircle } from '../services/circles'
|
import { createCircle, deleteCircle, deleteMember, getCircleMembers, getCircles, leaveCircle, addMembers } from '../services/circles.ts'
|
||||||
import Member from '../models/member'
|
import Member from '../models/member.ts'
|
||||||
import Circle from '../models/circle'
|
import Circle from '../models/circle.ts'
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
/** @type {Object.<string>} Circle */
|
/** @type {Object.<string>} Circle */
|
||||||
|
@ -113,10 +113,20 @@ const actions = {
|
||||||
const circles = await getCircles()
|
const circles = await getCircles()
|
||||||
console.debug(`Retrieved ${circles.length} circle(s)`, circles)
|
console.debug(`Retrieved ${circles.length} circle(s)`, circles)
|
||||||
|
|
||||||
circles.map(circle => new Circle(circle))
|
let failure = false
|
||||||
.forEach(circle => {
|
circles.forEach(circle => {
|
||||||
context.commit('addCircle', circle)
|
try {
|
||||||
})
|
const newCircle = new Circle(circle)
|
||||||
|
context.commit('addCircle', newCircle)
|
||||||
|
} catch (error) {
|
||||||
|
failure = true
|
||||||
|
console.error('This circle failed to be processed', circle, error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (failure) {
|
||||||
|
showError(t('contacts', 'Some circle(s) errored, check the console for more details'))
|
||||||
|
}
|
||||||
|
|
||||||
return circles
|
return circles
|
||||||
},
|
},
|
||||||
|
@ -140,12 +150,15 @@ const actions = {
|
||||||
*
|
*
|
||||||
* @param {Object} context the store mutations Current context
|
* @param {Object} context the store mutations Current context
|
||||||
* @param {string} circleName the circle name
|
* @param {string} circleName the circle name
|
||||||
|
* @returns {Circle} the new circle
|
||||||
*/
|
*/
|
||||||
async createCircle(context, circleName) {
|
async createCircle(context, circleName) {
|
||||||
try {
|
try {
|
||||||
const response = await createCircle(circleName)
|
const response = await createCircle(circleName)
|
||||||
const circle = new Circle(response)
|
const circle = new Circle(response)
|
||||||
|
context.commit('addCircle', circle)
|
||||||
console.debug('Created circle', circleName, circle)
|
console.debug('Created circle', circleName, circle)
|
||||||
|
return circle
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
showError(t('contacts', 'Unable to create circle {circleName}', { circleName }))
|
showError(t('contacts', 'Unable to create circle {circleName}', { circleName }))
|
||||||
|
@ -169,16 +182,23 @@ const actions = {
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a member to a circle
|
* Add members to a circle
|
||||||
*
|
*
|
||||||
* @param {Object} context the store mutations Current context
|
* @param {Object} context the store mutations Current context
|
||||||
* @param {Object} data destructuring object
|
* @param {Object} data destructuring object
|
||||||
* @param {string} data.circleId the circle to manage
|
* @param {string} data.circleId the circle to manage
|
||||||
* @param {string} data.memberId the member to add
|
* @param {Array} data.selection the members to add, see addMembers service
|
||||||
|
* @returns {Member[]}
|
||||||
*/
|
*/
|
||||||
async addMemberToCircle(context, { circleId, memberId }) {
|
async addMembersToCircle(context, { circleId, selection }) {
|
||||||
await this.addMember(circleId, memberId)
|
const circle = context.getters.getCircle(circleId)
|
||||||
console.debug('Added member', circleId, memberId)
|
const results = await addMembers(circleId, selection)
|
||||||
|
const members = results.map(member => new Member(member, circle))
|
||||||
|
|
||||||
|
console.debug('Added members to circle', circle, members)
|
||||||
|
context.commit('appendMembersToCircle', members)
|
||||||
|
|
||||||
|
return members
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -66,7 +66,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { GROUP_ALL_CONTACTS, GROUP_NO_GROUP_CONTACTS } from '../models/constants'
|
import { GROUP_ALL_CONTACTS, GROUP_NO_GROUP_CONTACTS, ROUTE_CIRCLE } from '../models/constants.ts'
|
||||||
|
|
||||||
import AppNavigationNew from '@nextcloud/vue/dist/Components/AppNavigationNew'
|
import AppNavigationNew from '@nextcloud/vue/dist/Components/AppNavigationNew'
|
||||||
import Content from '@nextcloud/vue/dist/Components/Content'
|
import Content from '@nextcloud/vue/dist/Components/Content'
|
||||||
|
@ -133,6 +133,9 @@ export default {
|
||||||
addressbooks() {
|
addressbooks() {
|
||||||
return this.$store.getters.getAddressbooks
|
return this.$store.getters.getAddressbooks
|
||||||
},
|
},
|
||||||
|
contacts() {
|
||||||
|
return this.$store.getters.getContacts
|
||||||
|
},
|
||||||
sortedContacts() {
|
sortedContacts() {
|
||||||
return this.$store.getters.getSortedContacts
|
return this.$store.getters.getSortedContacts
|
||||||
},
|
},
|
||||||
|
@ -186,6 +189,10 @@ export default {
|
||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
},
|
},
|
||||||
|
|
||||||
|
ungroupedContacts() {
|
||||||
|
return this.sortedContacts.filter(contact => this.contacts[contact.key].groups && this.contacts[contact.key].groups.length === 0)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -343,10 +350,14 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown group
|
// Unknown group
|
||||||
if (!this.groups.find(group => group.name === this.selectedGroup)
|
if (!this.selectedCircle
|
||||||
&& this.GROUP_ALL_CONTACTS !== this.selectedGroup
|
&& !this.groups.find(group => group.name === this.selectedGroup)
|
||||||
&& this.GROUP_NO_GROUP_CONTACTS !== this.selectedGroup) {
|
&& GROUP_ALL_CONTACTS !== this.selectedGroup
|
||||||
showError(t('contacts', 'Group not found'))
|
&& GROUP_NO_GROUP_CONTACTS !== this.selectedGroup
|
||||||
|
&& ROUTE_CIRCLE !== this.selectedGroup) {
|
||||||
|
showError(t('contacts', 'Group {group} not found', { group: this.selectedGroup }))
|
||||||
|
console.error('Group not found', this.selectedGroup)
|
||||||
|
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
name: 'root',
|
name: 'root',
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "ES6",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"target": "ES6",
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"declaration": true,
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./src/**/*"
|
||||||
|
]
|
||||||
|
}
|
|
@ -8,4 +8,12 @@ webpackConfig.plugins.push(...[
|
||||||
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
|
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
webpackConfig.module.rules.push({
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
use: 'ts-loader',
|
||||||
|
exclude: /node_modules/,
|
||||||
|
})
|
||||||
|
|
||||||
|
webpackConfig.resolve.extensions = ['.js', '.vue', '.ts', '.tsx']
|
||||||
|
|
||||||
module.exports = webpackConfig
|
module.exports = webpackConfig
|
||||||
|
|
Loading…
Reference in New Issue