diff options
author | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2021-05-06 16:43:56 +0200 |
---|---|---|
committer | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2021-05-30 10:28:57 +0200 |
commit | e8a26bcf0a84dc3880ede70998c75e4d85b72d60 (patch) | |
tree | 9a646f238d5a9e0c4a25346828843472d054d7b9 /src | |
parent | 5fec07abf282c01ede1a5fcbf2eff89367b39c62 (diff) |
Fix copylink, and member picker
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
Diffstat (limited to 'src')
-rw-r--r-- | src/components/AppNavigation/CircleNavigationItem.vue | 2 | ||||
-rw-r--r-- | src/components/AppNavigation/Settings/SettingsAddressbook.vue | 2 | ||||
-rw-r--r-- | src/components/CircleDetails.vue | 23 | ||||
-rw-r--r-- | src/components/EntityPicker/ContactsPicker.vue | 4 | ||||
-rw-r--r-- | src/components/EntityPicker/EntityPicker.vue | 36 | ||||
-rw-r--r-- | src/components/EntityPicker/EntitySearchResult.vue | 9 | ||||
-rw-r--r-- | src/components/EntityPicker/MembersPicker.vue | 185 | ||||
-rw-r--r-- | src/components/MemberList.vue | 13 | ||||
-rw-r--r-- | src/mixins/CopyToClipboardMixin.js | 12 | ||||
-rw-r--r-- | src/models/circle.d.ts | 4 | ||||
-rw-r--r-- | src/models/circle.ts | 7 | ||||
-rw-r--r-- | src/models/constants.d.ts | 5 | ||||
-rw-r--r-- | src/models/constants.ts | 69 | ||||
-rw-r--r-- | src/services/collaborationAutocompletion.js | 19 | ||||
-rw-r--r-- | src/store/circles.js | 2 |
15 files changed, 130 insertions, 262 deletions
diff --git a/src/components/AppNavigation/CircleNavigationItem.vue b/src/components/AppNavigation/CircleNavigationItem.vue index d6787a6e..4af059af 100644 --- a/src/components/AppNavigation/CircleNavigationItem.vue +++ b/src/components/AppNavigation/CircleNavigationItem.vue @@ -42,7 +42,7 @@ <!-- copy circle link --> <ActionLink :href="circleUrl" - :icon="copyLoading ? 'icon-loading-small' : 'icon-public'" + :icon="copyLinkIcon" @click.stop.prevent="copyToClipboard(circleUrl)"> {{ copyButtonText }} </ActionLink> diff --git a/src/components/AppNavigation/Settings/SettingsAddressbook.vue b/src/components/AppNavigation/Settings/SettingsAddressbook.vue index c4dac9fd..16625bfe 100644 --- a/src/components/AppNavigation/Settings/SettingsAddressbook.vue +++ b/src/components/AppNavigation/Settings/SettingsAddressbook.vue @@ -41,7 +41,7 @@ <!-- copy addressbook link --> <ActionLink :href="addressbook.url" - :icon="copyLoading ? 'icon-loading-small' : 'icon-public'" + :icon="copyLinkIcon" @click.stop.prevent="copyToClipboard(addressbookUrl)"> {{ copyButtonText }} </ActionLink> diff --git a/src/components/CircleDetails.vue b/src/components/CircleDetails.vue index 71a4e5dd..79cac2b2 100644 --- a/src/components/CircleDetails.vue +++ b/src/components/CircleDetails.vue @@ -54,6 +54,16 @@ <!-- actions --> <template #actions> <Actions> + <!-- copy circle link --> + <ActionLink + :href="circleUrl" + :icon="copyLinkIcon" + @click.stop.prevent="copyToClipboard(circleUrl)"> + {{ copyButtonText }} + </ActionLink> + </Actions> + + <Actions> <!-- leave circle --> <ActionButton v-if="circle.canLeave" @@ -74,15 +84,6 @@ decorative /> </ActionButton> </Actions> - <Actions> - <!-- copy circle link --> - <ActionLink - :href="circleUrl" - :icon="copyLoading ? 'icon-loading-small' : 'icon-public'" - @click.stop.prevent="copyToClipboard(circleUrl)"> - {{ copyButtonText }} - </ActionLink> - </Actions> </template> <!-- menu actions --> @@ -204,8 +205,8 @@ export default { } }, - onDisplayNameChangeDebounce: debounce(function() { - this.onDisplayNameChange(...arguments) + onDisplayNameChangeDebounce: debounce(function(event) { + this.onDisplayNameChange(event.target.value) }, 500), async onDisplayNameChange(description) { this.loadingDescription = true diff --git a/src/components/EntityPicker/ContactsPicker.vue b/src/components/EntityPicker/ContactsPicker.vue index f388298c..35b842f1 100644 --- a/src/components/EntityPicker/ContactsPicker.vue +++ b/src/components/EntityPicker/ContactsPicker.vue @@ -181,7 +181,3 @@ export default { }, } </script> - -<style lang="scss" scoped> - -</style> diff --git a/src/components/EntityPicker/EntityPicker.vue b/src/components/EntityPicker/EntityPicker.vue index 878d9fb7..0ad48a76 100644 --- a/src/components/EntityPicker/EntityPicker.vue +++ b/src/components/EntityPicker/EntityPicker.vue @@ -24,8 +24,7 @@ size="full" @close="onCancel"> <!-- Wrapper for content & navigation --> - <div - class="entity-picker"> + <div class="entity-picker"> <!-- Search --> <div class="entity-picker__search"> <div class="entity-picker__search-icon icon-search" /> @@ -68,7 +67,7 @@ :data-sources="availableEntities" :data-component="EntitySearchResult" :estimate-size="44" - :extra-props="{selection: selectionSet, onClick: onPick}" /> + :extra-props="{ selection: selectionSet, onClick }" /> <EmptyContent v-else-if="searchQuery" icon="icon-search"> {{ t('contacts', 'No results') }} @@ -166,6 +165,15 @@ export default { }, /** + * The input will also filter the dataSet based on the label. + * If you are using the search event to inject a different dataSet, you can disable this + */ + internalSearch: { + type: Boolean, + default: true, + }, + + /** * Override the local management of selection * You MUST use a sync modifier or the selection will be locked */ @@ -236,7 +244,8 @@ export default { * @returns {Object[]} */ searchSet() { - if (this.searchQuery && this.searchQuery.trim !== '') { + // If internal search is enabled and we have a search query, filter data set + if (this.internalSearch && this.searchQuery && this.searchQuery.trim !== '') { return this.dataSet.filter(entity => { return entity.label.indexOf(this.searchQuery) > -1 }) @@ -316,10 +325,16 @@ export default { }, /** - * Add entity from selection + * Add/remove entity from selection * @param {Object} entity the entity to add */ - onPick(entity) { + onClick(entity) { + if (entity.id in this.selectionSet) { + this.$delete(this.selectionSet, entity.id) + console.debug('Removed entity to selection', entity) + return + } + this.$set(this.selectionSet, entity.id, entity) console.debug('Added entity to selection', entity) }, @@ -400,7 +415,7 @@ $icon-margin: ($clickable-area - $icon-size) / 2; display: flex; overflow-y: auto; align-content: flex-start; - justify-content: space-between; + justify-content: flex-start; flex: 1 0 auto; flex-wrap: wrap; // half a line height to know there is more lines @@ -411,8 +426,9 @@ $icon-margin: ($clickable-area - $icon-size) / 2; // Allows 2 per line .entity-picker__bubble { - flex: 0 1 50%; max-width: calc(50% - #{$entity-spacing}); + margin-right: $entity-spacing; + margin-bottom: $entity-spacing; } } @@ -435,10 +451,6 @@ $icon-margin: ($clickable-area - $icon-size) / 2; margin-left: auto; } } - - &::v-deep &__bubble { - margin-bottom: $entity-spacing; - } } // Properly center Entity Picker empty content diff --git a/src/components/EntityPicker/EntitySearchResult.vue b/src/components/EntityPicker/EntitySearchResult.vue index 1def9bb4..8e94ee4e 100644 --- a/src/components/EntityPicker/EntitySearchResult.vue +++ b/src/components/EntityPicker/EntitySearchResult.vue @@ -118,12 +118,15 @@ $icon-margin: ($clickable-area - $icon-size) / 2; opacity: 0; } + // Show checkmark on selected + &--selected .entity-picker__bubble-checkmark { + opacity: 1; + } + + // Show primary bg on hovering entities &--selected, &:hover, &:focus { - .entity-picker__bubble-checkmark { - opacity: 1; - } ::v-deep .user-bubble__content { // better visual with light default tint background-color: var(--color-primary-light); diff --git a/src/components/EntityPicker/MembersPicker.vue b/src/components/EntityPicker/MembersPicker.vue deleted file mode 100644 index 882a7946..00000000 --- a/src/components/EntityPicker/MembersPicker.vue +++ /dev/null @@ -1,185 +0,0 @@ -<template> - <!-- Bulk contacts edit modal --> - <Modal v-if="isProcessing || isProcessDone" - :clear-view-delay="-1" - :can-close="isProcessDone" - @close="closeProcess"> - <AddToGroupView v-bind="processStatus" @close="closeProcess" /> - </Modal> - - <!-- contacts picker --> - <EntityPicker v-else-if="showPicker" - :confirm-label="t('contacts', 'Add to circle {circle}', { circle: pickerforCircle.name})" - :data-types="pickerTypes" - :data-set="pickerData" - @close="onContactPickerClose" - @submit="onContactPickerPick" /> -</template> - -<script> -import { subscribe } from '@nextcloud/event-bus' -import pLimit from 'p-limit' - -import Modal from '@nextcloud/vue/dist/Components/Modal' - -import AddToGroupView from '../../views/Processing/AddToGroupView' -import appendContactToGroup from '../../services/appendContactToGroup' -import EntityPicker from './EntityPicker' - -export default { - name: 'MembersPicker', - - components: { - AddToGroupView, - EntityPicker, - Modal, - - }, - - data() { - return { - // Entity picker - showPicker: false, - pickerforCircle: null, - pickerData: [], - pickerTypes: [{ - id: 'contact', - label: t('contacts', 'Contacts'), - }], - - // Bulk processing - isProcessing: false, - isProcessDone: false, - processStatus: { - failed: 0, - progress: 0, - success: 0, - total: 0, - name: '', - }, - } - }, - computed: { - contacts() { - return this.$store.getters.getContacts - }, - groups() { - return this.$store.getters.getGroups - }, - sortedContacts() { - return this.$store.getters.getSortedContacts - }, - }, - - mounted() { - // Watch for a add-to-group event - subscribe('contacts:circles:append', this.addMemberToCircle) - }, - - methods: { - // Bulk contacts group management handlers - addMemberToCircle(circleId) { - const circle = this.$store.getters.getCircle(circleId) - console.debug('Member picker opened for circle', circleId) - - // Get the full group if we provided the group name only - if (circle?.id !== circleId) { - console.error('Cannot add member to an undefined circle', circle, circleId) - return - } - - // Init data set - this.pickerData = this.sortedContacts - .map(({ key }) => { - const contact = this.contacts[key] - return { - id: contact.key, - label: contact.displayName, - type: 'contact', - readOnly: contact.addressbook.readOnly, - groups: contact.groups, - } - }) - // No read only contacts - .filter(contact => !contact.readOnly) - // No contacts already present in group - .filter(contact => contact.groups.indexOf(circle.name) === -1) - - this.showPicker = true - this.pickerforCircle = circle - }, - - onContactPickerClose() { - this.pickerData = [] - this.showPicker = false - }, - - onContactPickerPick(selection) { - console.debug('Adding', selection, 'to circle', this.pickerforCircle) - const groupName = this.pickerforCircle.name - - this.isProcessing = true - this.showPicker = false - - this.processStatus.total = selection.length - this.processStatus.name = this.pickerforCircle.name - this.processStatus.progress = 0 - this.processStatus.failed = 0 - - // max simultaneous requests - const limit = pLimit(3) - const requests = [] - - // create the array of requests to send - selection.map(async entity => { - try { - // Get contact - const contact = this.contacts[entity.id] - - // push contact to server and use limit - requests.push(limit(() => appendContactToGroup(contact, groupName) - .then((response) => { - this.$store.dispatch('addContactToGroup', { contact, groupName }) - this.processStatus.progress++ - this.processStatus.success++ - }) - .catch((error) => { - this.processStatus.progress++ - this.processStatus.error++ - console.error(error) - }) - )) - } catch (e) { - console.error(e) - } - }) - - Promise.all(requests).then(() => { - this.isProcessDone = true - this.showPicker = false - - // Auto close after 3 seconds if no errors - if (this.processStatus.failed === 0) { - setTimeout(this.closeProcess, 3000) - } - }) - }, - - closeProcess() { - this.pickerforCircle = null - this.isProcessing = false - this.isProcessDone = false - - // Reset - this.processStatus.failed = 0 - this.processStatus.progress = 0 - this.processStatus.success = 0 - this.processStatus.total = 0 - }, - }, -} -</script> - -<style lang="scss" scoped> - -</style> diff --git a/src/components/MemberList.vue b/src/components/MemberList.vue index 7b924712..2aa21ce0 100644 --- a/src/components/MemberList.vue +++ b/src/components/MemberList.vue @@ -39,6 +39,7 @@ :confirm-label="t('contacts', 'Add to {circle}', { circle: circle.displayName})" :data-types="pickerTypes" :data-set="pickerData" + :internal-search="false" :loading="pickerLoading" :selection.sync="pickerSelection" @close="resetPicker" @@ -82,6 +83,7 @@ export default { MembersListItem, pickerLoading: false, showPicker: false, + showPickerIntro: true, recommendations: [], pickerCircle: null, @@ -113,9 +115,11 @@ export default { return r }, Object.create(null)) - return CIRCLES_MEMBER_GROUPING - // Filter unpopulated types - .filter(group => groupedList[group.type]) + return Object.keys(groupedList) + // Object.keys returns string + .map(type => parseInt(type, 10)) + // Map populated types to the group entry + .map(type => CIRCLES_MEMBER_GROUPING.find(group => group.type === type)) // Injecting headings .map(group => [{ heading: true, @@ -200,7 +204,7 @@ export default { if (members.length !== selection.length) { showWarning(t('contacts', 'Some members could not be added')) // TODO filter successful members and edit selection - this.selection = [] + this.pickerSelection = [] return } @@ -220,6 +224,7 @@ export default { this.showPicker = false this.pickerCircle = null this.pickerData = [] + this.pickerSelection = [] }, }, } diff --git a/src/mixins/CopyToClipboardMixin.js b/src/mixins/CopyToClipboardMixin.js index 54e76536..3093d7dd 100644 --- a/src/mixins/CopyToClipboardMixin.js +++ b/src/mixins/CopyToClipboardMixin.js @@ -35,6 +35,18 @@ export default { } }, + computed: { + copyLinkIcon() { + if (this.copySuccess) { + return 'icon-checkmark' + } + if (this.copyLoading) { + return 'icon-loading-small' + } + return 'icon-public' + }, + }, + methods: { async copyToClipboard(url) { // change to loading status diff --git a/src/models/circle.d.ts b/src/models/circle.d.ts index 2e98fb0c..a51a56a8 100644 --- a/src/models/circle.d.ts +++ b/src/models/circle.d.ts @@ -43,6 +43,10 @@ export default class Circle { */ get displayName(): string; /** + * Set the display name + */ + set displayName(text: string); + /** * Circle creation date */ get creation(): number; diff --git a/src/models/circle.ts b/src/models/circle.ts index 8c0012a3..3a5880e3 100644 --- a/src/models/circle.ts +++ b/src/models/circle.ts @@ -78,6 +78,13 @@ export default class Circle { } /** + * Set the display name + */ + set displayName(text: string) { + this._data.displayName = text + } + + /** * Circle creation date */ get creation(): number { diff --git a/src/models/constants.d.ts b/src/models/constants.d.ts index e4a258fb..219de065 100644 --- a/src/models/constants.d.ts +++ b/src/models/constants.d.ts @@ -42,11 +42,10 @@ export declare const PUBLIC_CIRCLE_CONFIG: { export declare const CIRCLES_MEMBER_GROUPING: { id: string; label: string; + share: any; type: number; }[]; -export declare const SHARES_TYPES_MEMBER_MAP: { - [x: number]: number; -}; +export declare const SHARES_TYPES_MEMBER_MAP: {}; export declare enum MemberLevels { NONE, MEMBER, diff --git a/src/models/constants.ts b/src/models/constants.ts index 520e4b63..20a990de 100644 --- a/src/models/constants.ts +++ b/src/models/constants.ts @@ -116,31 +116,50 @@ export const PUBLIC_CIRCLE_CONFIG = { // Represents the picker options but also the // sorting of the members list -export const CIRCLES_MEMBER_GROUPING = [{ - id: `picker-${OC.Share.SHARE_TYPE_USER}`, - label: t('contacts', 'Users'), - type: MEMBER_TYPE_USER -}, { - id: `picker-${OC.Share.SHARE_TYPE_EMAIL}`, - label: t('contacts', 'Emails'), - type: MEMBER_TYPE_MAIL -}, { - id: `picker-${OC.Share.SHARE_TYPE_GROUP}`, - label: t('contacts', 'Groups'), - type: MEMBER_TYPE_GROUP -}, { - id: `picker-${OC.Share.SHARE_TYPE_CIRCLE}`, - label: t('contacts', 'Circles'), - type: MEMBER_TYPE_CIRCLE -}] - -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 const CIRCLES_MEMBER_GROUPING = [ + { + id: `picker-${OC.Share.SHARE_TYPE_USER}`, + label: t('contacts', 'Users'), + share: OC.Share.SHARE_TYPE_USER, + type: MEMBER_TYPE_USER + }, + { + id: `picker-${OC.Share.SHARE_TYPE_GROUP}`, + label: t('contacts', 'Groups'), + share: OC.Share.SHARE_TYPE_GROUP, + type: MEMBER_TYPE_GROUP + }, + { + id: `picker-${OC.Share.SHARE_TYPE_REMOTE}`, + label: t('contacts', 'Federated users'), + share: OC.Share.SHARE_TYPE_REMOTE, + type: MEMBER_TYPE_USER + }, + { + id: `picker-${OC.Share.SHARE_TYPE_REMOTE_GROUP}`, + label: t('contacts', 'Federated groups'), + share: OC.Share.SHARE_TYPE_REMOTE_GROUP, + type: MEMBER_TYPE_GROUP + }, + { + id: `picker-${OC.Share.SHARE_TYPE_CIRCLE}`, + label: t('contacts', 'Circles'), + share: OC.Share.SHARE_TYPE_CIRCLE, + type: MEMBER_TYPE_CIRCLE + }, + { + id: `picker-${OC.Share.SHARE_TYPE_EMAIL}`, + label: t('contacts', 'Emails'), + share: OC.Share.SHARE_TYPE_EMAIL, + type: MEMBER_TYPE_MAIL + }, +] + +// Generating a map between share types and circle member types +export const SHARES_TYPES_MEMBER_MAP = CIRCLES_MEMBER_GROUPING.reduce((list, entry) => { + list[entry.share] = entry.type + return list +}, {}) export enum MemberLevels { NONE = MEMBER_LEVEL_NONE, diff --git a/src/services/collaborationAutocompletion.js b/src/services/collaborationAutocompletion.js index f7200387..331e4129 100644 --- a/src/services/collaborationAutocompletion.js +++ b/src/services/collaborationAutocompletion.js @@ -24,19 +24,11 @@ import axios from '@nextcloud/axios' import { generateOcsUrl } from '@nextcloud/router' -const maxAutocompleteResults = parseInt(OC.config['sharing.maxAutocompleteResults'], 10) || 25 +import { SHARES_TYPES_MEMBER_MAP } from '../models/constants.ts' -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, -] +// generate allowed shareType from SHARES_TYPES_MEMBER_MAP +const shareType = Object.keys(SHARES_TYPES_MEMBER_MAP) +const maxAutocompleteResults = parseInt(OC.config['sharing.maxAutocompleteResults'], 10) || 25 /** * Get suggestions @@ -51,6 +43,7 @@ export const getSuggestions = async function(search) { search, perPage: maxAutocompleteResults, shareType, + lookup: false, }, }) @@ -132,7 +125,7 @@ const formatResults = function(result) { 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 + user: [OC.Share.SHARE_TYPE_USER, OC.Share.SHARE_TYPE_REMOTE].indexOf(result.value.shareType) > -1 ? result.value.shareWith : null, type, diff --git a/src/store/circles.js b/src/store/circles.js index 72d8cb60..ea056f36 100644 --- a/src/store/circles.js +++ b/src/store/circles.js @@ -192,8 +192,10 @@ const actions = { * @param {Circle} circleId the circle to delete */ async deleteCircle(context, circleId) { + const circle = context.getters.getCircle(circleId) try { await deleteCircle(circleId) + context.commit('deleteCircle', circle) console.debug('Deleted circle', circleId) } catch (error) { console.error(error) |