зеркало из https://github.com/nextcloud/forms.git
Merge pull request #346 from nextcloud/fix/sharing-users-groups
This commit is contained in:
Коммит
78a0f7fb60
|
@ -60,7 +60,5 @@ return [
|
|||
|
||||
['name' => 'api#insertSubmission', 'url' => '/api/v1/submission/insert', 'verb' => 'POST'],
|
||||
['name' => 'api#deleteSubmission', 'url' => '/api/v1/submission/{id}', 'verb' => 'DELETE'],
|
||||
|
||||
['name' => 'system#get_site_users_and_groups', 'url' => '/get/siteusers', 'verb' => 'POST'],
|
||||
]
|
||||
];
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
|
||||
namespace OCA\Forms\Controller;
|
||||
|
||||
use Exception;
|
||||
use OCA\Forms\Db\Answer;
|
||||
use OCA\Forms\Db\AnswerMapper;
|
||||
use OCA\Forms\Db\Form;
|
||||
|
@ -127,6 +128,7 @@ class ApiController extends Controller {
|
|||
'hash' => $form->getHash(),
|
||||
'title' => $form->getTitle(),
|
||||
'expires' => $form->getExpires(),
|
||||
'partial' => true
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -210,6 +212,21 @@ class ApiController extends Controller {
|
|||
return new Http\JSONResponse([], Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
|
||||
// Make sure we only store id
|
||||
try {
|
||||
if ($keyValuePairs['access']) {
|
||||
$keyValuePairs['access']['users'] = array_map(function (array $user): string {
|
||||
return $user['shareWith'];
|
||||
}, $keyValuePairs['access']['users']);
|
||||
$keyValuePairs['access']['groups'] = array_map(function (array $group): string {
|
||||
return $group['shareWith'];
|
||||
}, $keyValuePairs['access']['groups']);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->logger->debug('Malformed access');
|
||||
return new Http\JSONResponse(['message' => 'Malformed access'], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
||||
// Create FormEntity with given Params & Id.
|
||||
$form = Form::fromParams($keyValuePairs);
|
||||
$form->setId($id);
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* @copyright Copyright (c) 2017 Vinzenz Rosenkranz <vinzenz.rosenkranz@gmail.com>
|
||||
*
|
||||
* @author affan98 <affan98@gmail.com>
|
||||
* @author John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
|
||||
* @author Roeland Jago Douma <roeland@famdouma.nl>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Forms\Controller;
|
||||
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
|
||||
use OCP\IGroupManager;
|
||||
use OCP\IUserManager;
|
||||
use OCP\IRequest;
|
||||
|
||||
class SystemController extends Controller {
|
||||
public function __construct(
|
||||
string $appName,
|
||||
IGroupManager $groupManager,
|
||||
IUserManager $userManager,
|
||||
IRequest $request
|
||||
) {
|
||||
parent::__construct($appName, $request);
|
||||
$this->groupManager = $groupManager;
|
||||
$this->userManager = $userManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of NC users and groups
|
||||
* @NoAdminRequired
|
||||
* @return DataResponse
|
||||
*/
|
||||
public function getSiteUsersAndGroups($query = '', $getGroups = true, $getUsers = true, $skipGroups = [], $skipUsers = []) {
|
||||
$list = [];
|
||||
$data = [];
|
||||
if ($getGroups) {
|
||||
$groups = $this->groupManager->search($query);
|
||||
foreach ($groups as $group) {
|
||||
if (!in_array($group->getGID(), $skipGroups)) {
|
||||
$list[] = [
|
||||
'id' => $group->getGID(),
|
||||
'user' => $group->getGID(),
|
||||
'type' => 'group',
|
||||
'desc' => 'group',
|
||||
'icon' => 'icon-group',
|
||||
'displayName' => $group->getGID(),
|
||||
'avatarURL' => ''
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($getUsers) {
|
||||
$users = $this->userManager->searchDisplayName($query);
|
||||
foreach ($users as $user) {
|
||||
if (!in_array($user->getUID(), $skipUsers)) {
|
||||
$list[] = [
|
||||
'id' => $user->getUID(),
|
||||
'user' => $user->getUID(),
|
||||
'type' => 'user',
|
||||
'desc' => 'user',
|
||||
'icon' => 'icon-user',
|
||||
'displayName' => $user->getDisplayName(),
|
||||
'avatarURL' => '',
|
||||
'lastLogin' => $user->getLastLogin(),
|
||||
'cloudId' => $user->getCloudId()
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$data['siteusers'] = $list;
|
||||
return new DataResponse($data, Http::STATUS_OK);
|
||||
}
|
||||
}
|
|
@ -29,8 +29,12 @@ use OCA\Forms\Db\QuestionMapper;
|
|||
use OCA\Forms\Db\SubmissionMapper;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Db\IMapperException;
|
||||
use OCP\IGroup;
|
||||
use OCP\IGroupManager;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserManager;
|
||||
use OCP\IUserSession;
|
||||
use OCP\Share\IShare;
|
||||
|
||||
/**
|
||||
* Trait for getting forms information in a service
|
||||
|
@ -51,6 +55,9 @@ class FormsService {
|
|||
|
||||
/** @var IGroupManager */
|
||||
private $groupManager;
|
||||
|
||||
/** @var IUserManager */
|
||||
private $userManager;
|
||||
|
||||
/** @var IUserSession */
|
||||
private $userSession;
|
||||
|
@ -60,12 +67,14 @@ class FormsService {
|
|||
OptionMapper $optionMapper,
|
||||
SubmissionMapper $submissionMapper,
|
||||
IGroupManager $groupManager,
|
||||
IUserManager $userManager,
|
||||
IUserSession $userSession) {
|
||||
$this->formMapper = $formMapper;
|
||||
$this->questionMapper = $questionMapper;
|
||||
$this->optionMapper = $optionMapper;
|
||||
$this->submissionMapper = $submissionMapper;
|
||||
$this->groupManager = $groupManager;
|
||||
$this->userManager = $userManager;
|
||||
$this->userSession = $userSession;
|
||||
}
|
||||
|
||||
|
@ -112,6 +121,15 @@ class FormsService {
|
|||
$result = $form->read();
|
||||
$result['questions'] = $this->getQuestions($id);
|
||||
|
||||
// Set proper user/groups properties
|
||||
|
||||
// Make sure we have the bare minimum
|
||||
$result['access'] = array_merge(['users' => [], 'groups' => []], $result['access']);
|
||||
|
||||
// Properly format users & groups
|
||||
$result['access']['users'] = array_map([$this, 'formatUsers'], $result['access']['users']);
|
||||
$result['access']['groups'] = array_map([$this, 'formatGroups'], $result['access']['groups']);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
@ -205,4 +223,40 @@ class FormsService {
|
|||
// None of the possible access-options matched.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format users access
|
||||
*
|
||||
* @param string $userId
|
||||
* @return array
|
||||
*/
|
||||
private function formatUsers(string $userId): array {
|
||||
$user = $this->userManager->get($userId);
|
||||
if ($user instanceof IUser) {
|
||||
return [
|
||||
'shareWith' => $userId,
|
||||
'displayName' => $user->getDisplayName(),
|
||||
'shareType' => IShare::TYPE_USER
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format groups access
|
||||
*
|
||||
* @param string $groupId
|
||||
* @return array
|
||||
*/
|
||||
private function formatGroups(string $groupId): array {
|
||||
$group = $this->groupManager->get($groupId);
|
||||
if ($group instanceof IGroup) {
|
||||
return [
|
||||
'shareWith' => $groupId,
|
||||
'displayName' => $group->getDisplayName(),
|
||||
'shareType' => IShare::TYPE_GROUP
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,7 +58,9 @@
|
|||
<!-- No errors show router content -->
|
||||
<template v-else>
|
||||
<router-view :form.sync="selectedForm" />
|
||||
<router-view :form="selectedForm" name="sidebar" />
|
||||
<router-view v-if="!selectedForm.partial"
|
||||
:form="selectedForm"
|
||||
name="sidebar" />
|
||||
</template>
|
||||
</Content>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,398 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de>
|
||||
-
|
||||
- @author René Gieling <github@dartcafe.de>
|
||||
-
|
||||
- @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/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="sharing">
|
||||
<Multiselect id="ajax"
|
||||
:clear-on-select="false"
|
||||
:hide-selected="true"
|
||||
:internal-search="false"
|
||||
:loading="loading"
|
||||
:multiple="true"
|
||||
:options="options"
|
||||
:placeholder="t('forms', 'User or group name …')"
|
||||
:preselect-first="true"
|
||||
:preserve-search="true"
|
||||
:searchable="true"
|
||||
:user-select="true"
|
||||
label="displayName"
|
||||
track-by="shareWith"
|
||||
@search-change="asyncFind"
|
||||
@select="addShare">
|
||||
<template #noOptions>
|
||||
{{ t('forms', 'No recommendations. Start typing.') }}
|
||||
</template>
|
||||
<template #noResult>
|
||||
{{ noResultText }}
|
||||
</template>
|
||||
</Multiselect>
|
||||
|
||||
<TransitionGroup :css="false" tag="ul" class="shared-list">
|
||||
<!-- TODO: Iterate two times, will be cleaner, one for users, one for groups -->
|
||||
<li v-for="(item, index) in sortedShares" :key="item.shareWith + '-' + item.shareType" :data-index="index">
|
||||
<UserDiv v-bind="item" />
|
||||
<div class="options">
|
||||
<a class="icon icon-delete svg delete-form" @click="removeShare(item)" />
|
||||
</div>
|
||||
</li>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import axios from '@nextcloud/axios'
|
||||
import debounce from 'debounce'
|
||||
import Multiselect from '@nextcloud/vue/dist/Components/Multiselect'
|
||||
import ShareTypes from '../mixins/ShareTypes'
|
||||
|
||||
// TODO: replace with same design as core sharing
|
||||
import UserDiv from './UserDiv'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Multiselect,
|
||||
UserDiv,
|
||||
},
|
||||
|
||||
mixins: [ShareTypes],
|
||||
|
||||
props: {
|
||||
groupShares: {
|
||||
type: Array,
|
||||
default: () => ([]),
|
||||
},
|
||||
userShares: {
|
||||
type: Array,
|
||||
default: () => ([]),
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
query: '',
|
||||
loading: false,
|
||||
|
||||
// TODO: have a global mixin for this, shared with server?
|
||||
minSearchStringLength: parseInt(OC.config['sharing.minSearchStringLength'], 10) || 0,
|
||||
maxAutocompleteResults: parseInt(OC.config['sharing.maxAutocompleteResults'], 10) || 200,
|
||||
|
||||
// Search data
|
||||
recommendations: [],
|
||||
suggestions: [],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
sortedShares() {
|
||||
return [...this.userShares, ...this.groupShares].slice()
|
||||
.sort(this.sortByDisplayname)
|
||||
},
|
||||
|
||||
/**
|
||||
* Is the search valid ?
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isValidQuery() {
|
||||
return this.query && this.query.trim() !== '' && this.query.length > this.minSearchStringLength
|
||||
},
|
||||
|
||||
/**
|
||||
* Multiseelct options. Recommendations by default,
|
||||
* direct search when search query is valid.
|
||||
* Filter out existing shares
|
||||
* @returns {Array}
|
||||
*/
|
||||
options() {
|
||||
const shares = [...this.userShares, ...this.groupShares]
|
||||
if (this.isValidQuery) {
|
||||
// Filter out existing shares
|
||||
return this.suggestions.filter(item => !shares.find(share => share.shareWith === item.shareWith && share.shareType === item.shareType))
|
||||
}
|
||||
// Filter out existing shares
|
||||
return this.recommendations.filter(item => !shares.find(share => share.shareWith === item.shareWith && share.shareType === item.shareType))
|
||||
},
|
||||
|
||||
noResultText() {
|
||||
if (this.loading) {
|
||||
return t('forms', 'Searching …')
|
||||
}
|
||||
return t('forms', 'No elements found.')
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.getRecommendations()
|
||||
},
|
||||
|
||||
methods: {
|
||||
removeShare(item) {
|
||||
// Filter out the removed item
|
||||
const users = this.userShares.filter(user => !(user.shareWith === item.shareWith && item.shareType === this.SHARE_TYPES.SHARE_TYPE_USER))
|
||||
const groups = this.groupShares.filter(group => !(group.shareWith === item.shareWith && item.shareType === this.SHARE_TYPES.SHARE_TYPE_GROUP))
|
||||
this.$emit('update:shares', { users, groups })
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a new share and dispatch the change to the parent
|
||||
* @param {Object} share the new share
|
||||
*/
|
||||
addShare(share) {
|
||||
const users = this.userShares.slice()
|
||||
const groups = this.groupShares.slice()
|
||||
const newShare = {
|
||||
shareWith: share.shareWith,
|
||||
displayName: share.displayName,
|
||||
shareType: share.shareType,
|
||||
}
|
||||
|
||||
// TODO: detect if already present
|
||||
|
||||
if (share.shareType === this.SHARE_TYPES.SHARE_TYPE_USER) {
|
||||
users.push(newShare)
|
||||
} else if (share.shareType === this.SHARE_TYPES.SHARE_TYPE_GROUP) {
|
||||
groups.push(newShare)
|
||||
}
|
||||
|
||||
console.debug('Adding new share', share, users, groups)
|
||||
this.$emit('update:shares', { users, groups })
|
||||
},
|
||||
|
||||
sortByDisplayname(a, b) {
|
||||
if (a.displayName.toLowerCase() < b.displayName.toLowerCase()) return -1
|
||||
if (a.displayName.toLowerCase() > b.displayName.toLowerCase()) return 1
|
||||
return 0
|
||||
},
|
||||
|
||||
async asyncFind(query) {
|
||||
// save current query to check if we display
|
||||
// recommendations or search results
|
||||
this.query = query.trim()
|
||||
if (this.isValidQuery) {
|
||||
// start loading now to have proper ux feedback
|
||||
// during the debounce
|
||||
this.loading = true
|
||||
await this.debounceGetSuggestions(query)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get suggestions
|
||||
*
|
||||
* @param {string} search the search query
|
||||
*/
|
||||
async getSuggestions(search) {
|
||||
this.loading = true
|
||||
|
||||
const shareType = [
|
||||
this.SHARE_TYPES.SHARE_TYPE_USER,
|
||||
this.SHARE_TYPES.SHARE_TYPE_GROUP,
|
||||
]
|
||||
|
||||
const request = await axios.get(generateOcsUrl('apps/files_sharing/api/v1') + 'sharees', {
|
||||
params: {
|
||||
format: 'json',
|
||||
itemType: 'file',
|
||||
search,
|
||||
perPage: this.maxAutocompleteResults,
|
||||
shareType,
|
||||
},
|
||||
})
|
||||
|
||||
if (request.data.ocs.meta.statuscode !== 100) {
|
||||
console.error('Error fetching suggestions', request)
|
||||
return
|
||||
}
|
||||
|
||||
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 = this.filterOutUnwantedShares(rawExactSuggestions)
|
||||
.map(share => this.formatForMultiselect(share))
|
||||
// sort by type so we can get user&groups first...
|
||||
.sort((a, b) => a.shareType - b.shareType)
|
||||
const suggestions = this.filterOutUnwantedShares(rawSuggestions)
|
||||
.map(share => this.formatForMultiselect(share))
|
||||
// sort by type so we can get user&groups first...
|
||||
.sort((a, b) => a.shareType - b.shareType)
|
||||
|
||||
this.suggestions = exactSuggestions.concat(suggestions)
|
||||
|
||||
this.loading = false
|
||||
console.info('suggestions', this.suggestions)
|
||||
},
|
||||
|
||||
/**
|
||||
* Debounce getSuggestions
|
||||
*
|
||||
* @param {...*} args the arguments
|
||||
*/
|
||||
debounceGetSuggestions: debounce(function(...args) {
|
||||
this.getSuggestions(...args)
|
||||
}, 300),
|
||||
|
||||
/**
|
||||
* Get the sharing recommendations
|
||||
*/
|
||||
async getRecommendations() {
|
||||
this.loading = true
|
||||
|
||||
const request = await axios.get(generateOcsUrl('apps/files_sharing/api/v1') + 'sharees_recommended', {
|
||||
params: {
|
||||
format: 'json',
|
||||
itemType: 'file',
|
||||
},
|
||||
})
|
||||
|
||||
if (request.data.ocs.meta.statuscode !== 100) {
|
||||
console.error('Error fetching recommendations', request)
|
||||
return
|
||||
}
|
||||
|
||||
const exact = request.data.ocs.data.exact
|
||||
|
||||
// flatten array of arrays
|
||||
const rawRecommendations = Object.values(exact).reduce((arr, elem) => arr.concat(elem), [])
|
||||
|
||||
// remove invalid data and format to user-select layout
|
||||
this.recommendations = this.filterOutUnwantedShares(rawRecommendations)
|
||||
.map(share => this.formatForMultiselect(share))
|
||||
|
||||
this.loading = false
|
||||
console.info('recommendations', this.recommendations)
|
||||
},
|
||||
|
||||
/**
|
||||
* Filter out unwated shares
|
||||
*
|
||||
* @param {Object[]} shares the array of shares object
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
filterOutUnwantedShares(shares) {
|
||||
return shares.reduce((arr, share) => {
|
||||
// only check proper objects
|
||||
if (typeof share !== 'object') {
|
||||
return arr
|
||||
}
|
||||
|
||||
try {
|
||||
// filter out current user
|
||||
if (share.value.shareType === this.SHARE_TYPES.SHARE_TYPE_USER
|
||||
&& share.value.shareWith === getCurrentUser().uid) {
|
||||
return arr
|
||||
}
|
||||
|
||||
// ALL GOOD
|
||||
// let's add the suggestion
|
||||
arr.push(share)
|
||||
} catch {
|
||||
return arr
|
||||
}
|
||||
return arr
|
||||
}, [])
|
||||
},
|
||||
|
||||
/**
|
||||
* Format shares for the multiselect options
|
||||
* @param {Object} result select entry item
|
||||
* @returns {Object}
|
||||
*/
|
||||
formatForMultiselect(result) {
|
||||
return {
|
||||
shareWith: result.value.shareWith,
|
||||
shareType: result.value.shareType,
|
||||
user: result.uuid || result.value.shareWith,
|
||||
isNoUser: result.value.shareType !== this.SHARE_TYPES.SHARE_TYPE_USER,
|
||||
displayName: result.name || result.label,
|
||||
icon: this.shareTypeToIcon(result.value.shareType),
|
||||
// Vue unique binding to render within Multiselect's AvatarSelectOption
|
||||
key: result.uuid || result.value.shareWith + '-' + result.value.shareType + '-' + result.name || result.label,
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the icon based on the share type
|
||||
* @param {number} type the share type
|
||||
* @returns {string} the icon class
|
||||
*/
|
||||
shareTypeToIcon(type) {
|
||||
switch (type) {
|
||||
case this.SHARE_TYPES.SHARE_TYPE_GUEST:
|
||||
// default is a user, other icons are here to differenciate
|
||||
// themselves from it, so let's not display the user icon
|
||||
// case this.SHARE_TYPES.SHARE_TYPE_REMOTE:
|
||||
// case this.SHARE_TYPES.SHARE_TYPE_USER:
|
||||
return 'icon-user'
|
||||
case this.SHARE_TYPES.SHARE_TYPE_REMOTE_GROUP:
|
||||
case this.SHARE_TYPES.SHARE_TYPE_GROUP:
|
||||
return 'icon-group'
|
||||
case this.SHARE_TYPES.SHARE_TYPE_EMAIL:
|
||||
return 'icon-mail'
|
||||
case this.SHARE_TYPES.SHARE_TYPE_CIRCLE:
|
||||
return 'icon-circle'
|
||||
case this.SHARE_TYPES.SHARE_TYPE_ROOM:
|
||||
return 'icon-room'
|
||||
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sharing {
|
||||
margin: 8px 8px 8px 36px;
|
||||
}
|
||||
|
||||
.shared-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
padding-top: 8px;
|
||||
|
||||
> li {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
position: relative;
|
||||
top: -12px;
|
||||
left: -13px;
|
||||
}
|
||||
|
||||
.multiselect {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
</style>
|
|
@ -20,79 +20,47 @@
|
|||
-
|
||||
-->
|
||||
|
||||
/* global Vue, oc_userconfig */
|
||||
<template>
|
||||
<div :class="type" class="user-row">
|
||||
<div v-if="description" class="description">
|
||||
{{ description }}
|
||||
</div>
|
||||
<Avatar :user="userId" :display-name="computedDisplayName" :is-no-user="isNoUser" />
|
||||
<div v-if="!hideNames" class="user-name">
|
||||
{{ computedDisplayName }}
|
||||
</div>
|
||||
<div class="user-row">
|
||||
<Avatar :user="shareWith" :display-name="computedDisplayName" :is-no-user="isNoUser" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Avatar from '@nextcloud/vue/dist/Components/Avatar'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import ShareTypes from '../mixins/ShareTypes'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Avatar,
|
||||
},
|
||||
mixins: [ShareTypes],
|
||||
|
||||
props: {
|
||||
hideNames: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
userId: {
|
||||
shareWith: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
required: true,
|
||||
},
|
||||
displayName: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: true,
|
||||
},
|
||||
size: {
|
||||
shareType: {
|
||||
type: Number,
|
||||
default: 32,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'user',
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: true,
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
nothidden: false,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
isNoUser() {
|
||||
return this.type !== 'user'
|
||||
return this.shareType !== this.SHARE_TYPES.SHARE_TYPE_USER
|
||||
},
|
||||
computedDisplayName() {
|
||||
let value = this.displayName
|
||||
|
||||
if (this.userId === getCurrentUser().uid) {
|
||||
value = getCurrentUser().displayName
|
||||
} else {
|
||||
if (!this.displayName) {
|
||||
value = this.userId
|
||||
}
|
||||
if (this.shareType === this.SHARE_TYPES.SHARE_TYPE_GROUP) {
|
||||
return `${this.displayName} (${t('forms', 'Group')})`
|
||||
}
|
||||
if (this.type === 'group') {
|
||||
value = value + ' (' + t('forms', 'Group') + ')'
|
||||
}
|
||||
return value
|
||||
return this.displayName
|
||||
},
|
||||
|
||||
},
|
|
@ -1,181 +0,0 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de>
|
||||
-
|
||||
- @author René Gieling <github@dartcafe.de>
|
||||
-
|
||||
- @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/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="sharing">
|
||||
<Multiselect id="ajax"
|
||||
v-model="shares"
|
||||
:options="users"
|
||||
:multiple="true"
|
||||
:user-select="true"
|
||||
:tag-width="80"
|
||||
:clear-on-select="false"
|
||||
:preserve-search="true"
|
||||
:options-limit="20"
|
||||
:loading="isLoading"
|
||||
:internal-search="false"
|
||||
:searchable="true"
|
||||
:preselect-first="true"
|
||||
:placeholder="placeholder"
|
||||
label="displayName"
|
||||
track-by="user"
|
||||
@search-change="loadUsersAsync"
|
||||
@close="updateShares">
|
||||
<template slot="selection" slot-scope="{ values, search, isOpen }">
|
||||
<span v-if="values.length && !isOpen" class="multiselect__single">
|
||||
{{ values.length }} users selected
|
||||
</span>
|
||||
</template>
|
||||
</Multiselect>
|
||||
|
||||
<TransitionGroup :css="false" tag="ul" class="shared-list">
|
||||
<li v-for="(item, index) in sortedShares" :key="item.displayName" :data-index="index">
|
||||
<UserDiv :user-id="item.user"
|
||||
:display-name="item.displayName"
|
||||
:type="item.type"
|
||||
:hide-names="hideNames" />
|
||||
<div class="options">
|
||||
<a class="icon icon-delete svg delete-form" @click="removeShare(index, item)" />
|
||||
</div>
|
||||
</li>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import axios from '@nextcloud/axios'
|
||||
import Multiselect from '@nextcloud/vue/dist/Components/Multiselect'
|
||||
|
||||
// TODO: replace with same design as core sharing
|
||||
import UserDiv from './_base-UserDiv'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Multiselect,
|
||||
UserDiv,
|
||||
},
|
||||
|
||||
props: {
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
activeShares: {
|
||||
type: Array,
|
||||
default: function() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
|
||||
hideNames: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
shares: [],
|
||||
users: [],
|
||||
isLoading: false,
|
||||
siteUsersListOptions: {
|
||||
getUsers: true,
|
||||
getGroups: true,
|
||||
query: '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
sortedShares() {
|
||||
return this.shares.slice(0).sort(this.sortByDisplayname)
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
activeShares(value) {
|
||||
this.shares = value.slice(0)
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
removeShare(index, item) {
|
||||
this.$emit('remove-share', item)
|
||||
},
|
||||
|
||||
updateShares() {
|
||||
this.$emit('update-shares', this.shares)
|
||||
},
|
||||
|
||||
loadUsersAsync(query) {
|
||||
this.isLoading = false
|
||||
this.siteUsersListOptions.query = query
|
||||
axios.post(generateUrl('apps/forms/get/siteusers'), this.siteUsersListOptions)
|
||||
.then((response) => {
|
||||
this.users = response.data.siteusers
|
||||
this.isLoading = false
|
||||
}, (error) => {
|
||||
/* eslint-disable-next-line no-console */
|
||||
console.log(error.response)
|
||||
})
|
||||
},
|
||||
|
||||
sortByDisplayname(a, b) {
|
||||
if (a.displayName.toLowerCase() < b.displayName.toLowerCase()) return -1
|
||||
if (a.displayName.toLowerCase() > b.displayName.toLowerCase()) return 1
|
||||
return 0
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sharing {
|
||||
margin: 8px 8px 8px 36px;
|
||||
}
|
||||
|
||||
.shared-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
padding-top: 8px;
|
||||
|
||||
> li {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
position: relative;
|
||||
top: -12px;
|
||||
left: -13px;
|
||||
}
|
||||
|
||||
.multiselect {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2019 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 default {
|
||||
data() {
|
||||
return {
|
||||
SHARE_TYPES: {
|
||||
SHARE_TYPE_USER: OC.Share.SHARE_TYPE_USER,
|
||||
SHARE_TYPE_GROUP: OC.Share.SHARE_TYPE_GROUP,
|
||||
SHARE_TYPE_LINK: OC.Share.SHARE_TYPE_LINK,
|
||||
SHARE_TYPE_EMAIL: OC.Share.SHARE_TYPE_EMAIL,
|
||||
SHARE_TYPE_REMOTE: OC.Share.SHARE_TYPE_REMOTE,
|
||||
SHARE_TYPE_CIRCLE: OC.Share.SHARE_TYPE_CIRCLE,
|
||||
SHARE_TYPE_GUEST: OC.Share.SHARE_TYPE_GUEST,
|
||||
SHARE_TYPE_REMOTE_GROUP: OC.Share.SHARE_TYPE_REMOTE_GROUP,
|
||||
SHARE_TYPE_ROOM: OC.Share.SHARE_TYPE_ROOM,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
|
@ -21,7 +21,7 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<AppSidebar v-if="form"
|
||||
<AppSidebar
|
||||
v-show="opened"
|
||||
:title="form.title"
|
||||
@close="onClose">
|
||||
|
@ -44,7 +44,7 @@
|
|||
:disabled="isPublic || form.isAnonymous"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
@change="onSubmOnceChange">
|
||||
@change="onSubmitOnceChange">
|
||||
<label for="submitOnce">
|
||||
{{ t('forms', 'Only allow one response per user') }}
|
||||
</label>
|
||||
|
@ -113,11 +113,9 @@
|
|||
</span>
|
||||
</label>
|
||||
<ShareDiv v-show="form.access.type === 'selected'"
|
||||
:active-shares="form.shares"
|
||||
:placeholder="t('forms', 'Name of user or group')"
|
||||
:hide-names="true"
|
||||
@update-shares="updateShares"
|
||||
@remove-share="removeShare" />
|
||||
:user-shares="userShares"
|
||||
:group-shares="groupShares"
|
||||
@update:shares="onSharingChange" />
|
||||
</li>
|
||||
</ul>
|
||||
</AppSidebar>
|
||||
|
@ -130,7 +128,7 @@ import moment from '@nextcloud/moment'
|
|||
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
|
||||
import { getLocale, getDayNamesShort, getMonthNamesShort } from '@nextcloud/l10n'
|
||||
|
||||
import ShareDiv from '../components/shareDiv'
|
||||
import ShareDiv from '../components/ShareDiv'
|
||||
import ViewsMixin from '../mixins/ViewsMixin'
|
||||
|
||||
export default {
|
||||
|
@ -182,10 +180,16 @@ export default {
|
|||
expirationDate() {
|
||||
return moment(this.form.expires, 'X').toDate()
|
||||
},
|
||||
|
||||
isExpired() {
|
||||
return this.form.expires && moment().unix() > this.form.expires
|
||||
},
|
||||
|
||||
userShares() {
|
||||
return [...this.form?.access?.users || []]
|
||||
},
|
||||
groupShares() {
|
||||
return [...this.form?.access?.groups || []]
|
||||
},
|
||||
},
|
||||
|
||||
async created() {
|
||||
|
@ -224,18 +228,6 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
addShare(item) {
|
||||
this.form.shares.push(item)
|
||||
},
|
||||
|
||||
updateShares(share) {
|
||||
this.form.shares = share.slice(0)
|
||||
},
|
||||
|
||||
removeShare(item) {
|
||||
this.form.shares.splice(this.form.shares.indexOf(item), 1)
|
||||
},
|
||||
|
||||
/**
|
||||
* Sidebar state methods
|
||||
*/
|
||||
|
@ -252,12 +244,17 @@ export default {
|
|||
onAnonChange() {
|
||||
this.saveFormProperty('isAnonymous')
|
||||
},
|
||||
onSubmOnceChange() {
|
||||
onSubmitOnceChange() {
|
||||
this.saveFormProperty('submitOnce')
|
||||
},
|
||||
onAccessChange() {
|
||||
this.saveFormProperty('access')
|
||||
},
|
||||
onSharingChange({ groups, users }) {
|
||||
this.$set(this.form.access, 'groups', groups)
|
||||
this.$set(this.form.access, 'users', users)
|
||||
this.onAccessChange()
|
||||
},
|
||||
|
||||
/**
|
||||
* On date picker change
|
||||
|
|
Загрузка…
Ссылка в новой задаче