Merge pull request #1322 from nextcloud/split-sidebar-content

Split sidebar content
This commit is contained in:
René Gieling 2021-01-04 23:57:59 +01:00 коммит произвёл GitHub
Родитель f7ef1a927a 02f106efa2
Коммит 6a60a1ec78
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
18 изменённых файлов: 767 добавлений и 308 удалений

Просмотреть файл

@ -0,0 +1,123 @@
<!--
- @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>
<Multiselect id="ajax"
:options="users"
:multiple="false"
:user-select="true"
:tag-width="80"
:clear-on-select="false"
:preserve-search="true"
:options-limit="30"
:loading="isLoading"
:internal-search="false"
:searchable="true"
:preselect-first="true"
:placeholder="placeholder"
label="displayName"
track-by="userId"
@select="addShare"
@search-change="loadUsersAsync">
<template slot="selection" slot-scope="{ values, isOpen }">
<span v-if="values.length &amp;&amp; !isOpen" class="multiselect__single">
{{ values.length }} users selected
</span>
</template>
</Multiselect>
</template>
<script>
import debounce from 'lodash/debounce'
import axios from '@nextcloud/axios'
import { showError } from '@nextcloud/dialogs'
import { generateUrl } from '@nextcloud/router'
import { Multiselect } from '@nextcloud/vue'
export default {
name: 'UserSearch',
components: {
Multiselect,
},
data() {
return {
searchToken: null,
users: [],
isLoading: false,
placeholder: t('polls', 'Enter a name to start the search'),
}
},
computed: {
},
methods: {
loadUsersAsync: debounce(function(query) {
if (!query) {
this.users = []
return
}
this.isLoading = true
if (this.searchToken) {
this.searchToken.cancel()
}
this.searchToken = axios.CancelToken.source()
axios.get(generateUrl('apps/polls/search/users/' + query), { cancelToken: this.searchToken.token })
.then((response) => {
this.users = response.data.siteusers
this.isLoading = false
})
.catch((error) => {
if (axios.isCancel(error)) {
// request was cancelled
} else {
console.error(error.response)
this.isLoading = false
}
})
}, 250),
addShare(payload) {
this.$store
.dispatch('poll/shares/add', {
share: payload,
type: payload.type,
id: payload.id,
emailAddress: payload.emailAddress,
})
.catch(error => {
console.error('Error while adding share - Error: ', error)
showError(t('polls', 'Error while adding share'))
})
},
},
}
</script>
<style lang="scss">
.multiselect {
width: 100% !important;
max-width: 100% !important;
}
</style>

Просмотреть файл

@ -21,53 +21,40 @@
-->
<template>
<div class="comments">
<CommentAdd v-if="acl.allowComment" />
<transition-group v-if="!showEmptyContent" name="fade" class="comments"
tag="ul">
<li v-for="(comment) in sortedList" :key="comment.id">
<div class="comment-item">
<UserItem v-bind="comment" />
<Actions v-if="comment.userId === acl.userId">
<ActionButton icon="icon-delete" @click="deleteComment(comment)">
{{ t('polls', 'Delete comment') }}
</ActionButton>
</Actions>
<div class="date">
{{ dateCommentedRelative(comment.dt) }}
</div>
<transition-group name="fade" class="comments"
tag="ul">
<li v-for="(comment) in sortedList" :key="comment.id">
<div class="comment-item">
<UserItem v-bind="comment" />
<Actions v-if="comment.userId === acl.userId">
<ActionButton icon="icon-delete" @click="deleteComment(comment)">
{{ t('polls', 'Delete comment') }}
</ActionButton>
</Actions>
<div class="date">
{{ dateCommentedRelative(comment.dt) }}
</div>
</div>
<div class="message wordwrap comment-content">
{{ comment.comment }}
</div>
</li>
</transition-group>
<EmptyContent v-else icon="icon-comment">
{{ t('polls', 'No comments') }}
<template #desc>
{{ t('polls', 'Be the first.') }}
</template>
</EmptyContent>
</div>
<div class="message wordwrap comment-content">
{{ comment.comment }}
</div>
</li>
</transition-group>
</template>
<script>
import CommentAdd from './CommentAdd'
import sortBy from 'lodash/sortBy'
import moment from '@nextcloud/moment'
import { showSuccess, showError } from '@nextcloud/dialogs'
import { Actions, ActionButton, EmptyContent } from '@nextcloud/vue'
import { mapState, mapGetters } from 'vuex'
import { Actions, ActionButton } from '@nextcloud/vue'
import { mapState } from 'vuex'
export default {
name: 'Comments',
components: {
Actions,
ActionButton,
CommentAdd,
EmptyContent,
},
data() {
return {
@ -82,14 +69,6 @@ export default {
acl: state => state.poll.acl,
}),
...mapGetters({
countComments: 'poll/comments/count',
}),
showEmptyContent() {
return this.countComments === 0
},
sortedList() {
if (this.reverse) {
return sortBy(this.comments, this.sort).reverse()

Просмотреть файл

@ -22,10 +22,7 @@
<template>
<div>
<OptionAddDate v-if="!closed" />
<OptionShiftDates v-if="options.length && !closed" />
<ConfigBox v-if="!showEmptyContent" :title="t('polls', 'Available Options')" icon-class="icon-calendar-000">
<ConfigBox v-if="options.length" :title="t('polls', 'Available Options')" icon-class="icon-calendar-000">
<transition-group is="ul">
<OptionItem v-for="(option) in sortedOptions"
:key="option.id"
@ -41,10 +38,10 @@
</Actions>
<Actions v-if="acl.allowEdit" class="action">
<ActionButton v-if="!closed" icon="icon-polls-clone" @click="cloneOptionModal(option)">
<ActionButton v-if="!pollIsClosed" icon="icon-polls-clone" @click="cloneOptionModal(option)">
{{ t('polls', 'Clone option') }}
</ActionButton>
<ActionButton v-if="closed" :icon="option.confirmed ? 'icon-polls-confirmed' : 'icon-polls-unconfirmed'"
<ActionButton v-if="pollIsClosed" :icon="option.confirmed ? 'icon-polls-confirmed' : 'icon-polls-unconfirmed'"
@click="confirmOption(option)">
{{ option.confirmed ? t('polls', 'Unconfirm option') : t('polls', 'Confirm option') }}
</ActionButton>
@ -69,27 +66,23 @@
<script>
import { mapGetters, mapState } from 'vuex'
import OptionAddDate from '../Base/OptionAddDate'
import OptionCloneDate from '../Base/OptionCloneDate'
import OptionShiftDates from '../Base/OptionShiftDates'
import OptionCloneDate from './OptionCloneDate'
import ConfigBox from '../Base/ConfigBox'
import OptionItem from '../Base/OptionItem'
import OptionItem from './OptionItem'
import moment from '@nextcloud/moment'
import { Actions, ActionButton, Modal, EmptyContent } from '@nextcloud/vue'
import { confirmOption, removeOption } from '../../mixins/optionMixins'
import { dateUnits } from '../../mixins/dateMixins'
export default {
name: 'SideBarTabOptionsDate',
name: 'OptionsDate',
components: {
Actions,
ActionButton,
ConfigBox,
EmptyContent,
OptionAddDate,
OptionCloneDate,
OptionShiftDates,
Modal,
OptionItem,
},
@ -121,13 +114,9 @@ export default {
...mapGetters({
sortedOptions: 'poll/options/sorted',
closed: 'poll/closed',
pollIsClosed: 'poll/closed',
}),
showEmptyContent() {
return this.sortedOptions.length === 0
},
dateBaseOptionString() {
return moment.unix(this.sequence.baseOption.timestamp).format('LLLL')
},

Просмотреть файл

@ -37,7 +37,7 @@ import moment from '@nextcloud/moment'
import { DatetimePicker } from '@nextcloud/vue'
export default {
name: 'OptionAddDate',
name: 'OptionsDateAdd',
components: {
ConfigBox,

Просмотреть файл

@ -56,7 +56,7 @@ import { Actions, ActionButton, Multiselect } from '@nextcloud/vue'
import { dateUnits } from '../../mixins/dateMixins'
export default {
name: 'OptionAddDate',
name: 'OptionsDateShift',
components: {
Actions,

Просмотреть файл

@ -22,12 +22,7 @@
<template>
<div>
<ConfigBox v-if="!closed" :title="t('polls', 'Add a new text option')" icon-class="icon-add">
<InputDiv v-model="newPollText" :placeholder="t('polls', 'Enter option text')"
@input="addOption()" />
</ConfigBox>
<ConfigBox v-if="!showEmptyContent" :title="t('polls', 'Available Options')" icon-class="icon-toggle-filelist">
<ConfigBox v-if="options.length" :title="t('polls', 'Available Options')" icon-class="icon-toggle-filelist">
<draggable v-model="sortOptions">
<transition-group>
<OptionItem v-for="(option) in sortOptions"
@ -41,7 +36,7 @@
</ActionButton>
</Actions>
<Actions v-if="acl.allowEdit" class="action">
<ActionButton v-if="closed" :icon="option.confirmed ? 'icon-polls-yes' : 'icon-checkmark'"
<ActionButton v-if="PollIsClosed" :icon="option.confirmed ? 'icon-polls-yes' : 'icon-checkmark'"
@click="confirmOption(option)">
{{ option.confirmed ? t('polls', 'Unconfirm option') : t('polls', 'Confirm option') }}
</ActionButton>
@ -66,8 +61,7 @@ import { mapGetters, mapState } from 'vuex'
import { Actions, ActionButton, EmptyContent } from '@nextcloud/vue'
import ConfigBox from '../Base/ConfigBox'
import draggable from 'vuedraggable'
import OptionItem from '../Base/OptionItem'
import InputDiv from '../Base/InputDiv'
import OptionItem from './OptionItem'
import { confirmOption, removeOption } from '../../mixins/optionMixins'
export default {
@ -79,7 +73,6 @@ export default {
ConfigBox,
draggable,
EmptyContent,
InputDiv,
OptionItem,
},
@ -102,13 +95,9 @@ export default {
...mapGetters({
sortedOptions: 'poll/options/sorted',
closed: 'poll/closed',
PollIsClosed: 'poll/closed',
}),
showEmptyContent() {
return this.sortedOptions.length === 0
},
sortOptions: {
get() {
return this.sortedOptions

Просмотреть файл

@ -0,0 +1,92 @@
<!--
- @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>
<ConfigBox v-if="!closed" :title="t('polls', 'Add a new text option')" icon-class="icon-add">
<InputDiv v-model="newPollText" :placeholder="t('polls', 'Enter option text')"
@input="addOption()" />
</ConfigBox>
</template>
<script>
import { mapGetters } from 'vuex'
import ConfigBox from '../Base/ConfigBox'
import InputDiv from '../Base/InputDiv'
export default {
name: 'SideBarTabOptionsText',
components: {
ConfigBox,
InputDiv,
},
data() {
return {
newPollText: '',
}
},
computed: {
...mapGetters({
closed: 'poll/closed',
}),
},
methods: {
addOption() {
if (this.newPollText) {
this.$store.dispatch('poll/options/add', {
pollOptionText: this.newPollText,
})
.then(() => {
this.newPollText = ''
})
}
},
},
}
</script>
<style lang="scss" scoped>
.optionAdd {
display: flex;
}
.newOption {
margin-left: 40px;
flex: 1;
&:empty:before {
color: grey;
}
}
.submit-option {
width: 30px;
background-color: transparent;
border: none;
opacity: 0.3;
cursor: pointer;
}
</style>

Просмотреть файл

@ -0,0 +1,143 @@
<!--
- @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>
<ConfigBox :title="t('polls', 'Effective shares')" icon-class="icon-share">
<TransitionGroup :css="false" tag="div" class="shared-list">
<UserItem v-for="(share) in invitationShares"
:key="share.id" v-bind="share"
:icon="true">
<Actions>
<ActionButton
v-if="share.emailAddress || share.type === 'group'"
icon="icon-confirm"
@click="sendInvitation(share)">
{{ share.invitationSent ? t('polls', 'Resend invitation mail') : t('polls', 'Send invitation mail') }}
</ActionButton>
<ActionButton icon="icon-clippy" @click="copyLink( { url: shareUrl(share) })">
{{ t('polls', 'Copy link to clipboard') }}
</ActionButton>
</Actions>
<Actions>
<ActionButton icon="icon-delete" @click="removeShare(share)">
{{ t('polls', 'Remove share') }}
</ActionButton>
</Actions>
</UserItem>
</TransitionGroup>
</ConfigBox>
</template>
<script>
import { mapGetters } from 'vuex'
import { showSuccess, showError } from '@nextcloud/dialogs'
import { generateUrl } from '@nextcloud/router'
import { Actions, ActionButton } from '@nextcloud/vue'
import ConfigBox from '../Base/ConfigBox'
export default {
name: 'SharesEffective',
components: {
Actions,
ActionButton,
ConfigBox,
},
data() {
return {
users: [],
isLoading: false,
placeholder: t('polls', 'Enter a name to start the search'),
}
},
computed: {
...mapGetters({
invitationShares: 'poll/shares/invitation',
}),
},
methods: {
sendInvitation(share) {
this.$store.dispatch('poll/shares/sendInvitation', { share: share })
.then((response) => {
if ('sentResult.sentMails' in response.data) {
response.data.sentResult.sentMails.forEach((item) => {
showSuccess(t('polls', 'Invitation sent to {name}', { name: item.displayName }))
})
}
if ('sentResult.abortedMails' in response.data) {
response.data.sentResult.abortedMails.forEach((item) => {
console.error('Mail could not be sent!', { recipient: item })
showError(t('polls', 'Error sending invitation to {name}', { name: item.dispalyName }))
})
}
})
},
copyLink(payload) {
this
.$copyText(window.location.origin + payload.url)
.then(() => {
showSuccess(t('polls', 'Link copied to clipboard'))
})
.catch(() => {
showError(t('polls', 'Error while copying link to clipboard'))
})
},
shareUrl(share) {
return generateUrl('apps/polls/s/') + share.token
},
removeShare(share) {
this.$store.dispatch('poll/shares/delete', { share: share })
},
},
}
</script>
<style lang="scss">
.shared-list {
display: flex;
flex-wrap: wrap;
flex-direction: column;
justify-content: flex-start;
padding-top: 8px;
> li {
display: flex;
align-items: stretch;
margin: 4px 0;
}
}
.share-item {
display: flex;
flex: 1;
align-items: center;
max-width: 100%;
}
</style>

Просмотреть файл

@ -0,0 +1,130 @@
<!--
- @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>
<ConfigBox :title="t('polls', 'Public shares')" icon-class="icon-public">
<TransitionGroup :css="false" tag="div" class="shared-list">
<PublicShareItem v-for="(share) in publicShares"
:key="share.id"
v-bind="share">
<Actions>
<ActionButton icon="icon-clippy" @click="copyLink( { url: shareUrl(share) })">
{{ t('polls', 'Copy link to clipboard') }}
</ActionButton>
</Actions>
<Actions>
<ActionButton icon="icon-delete" @click="removeShare(share)">
{{ t('polls', 'Remove share') }}
</ActionButton>
</Actions>
</PublicShareItem>
</TransitionGroup>
<ButtonDiv :title="t('polls', 'Add a public link')" icon="icon-add" @click="addShare({type: 'public', userId: '', emailAddress: ''})" />
</ConfigBox>
</template>
<script>
import { mapGetters } from 'vuex'
import { showSuccess, showError } from '@nextcloud/dialogs'
import { generateUrl } from '@nextcloud/router'
import { Actions, ActionButton } from '@nextcloud/vue'
import ButtonDiv from '../Base/ButtonDiv'
import ConfigBox from '../Base/ConfigBox'
import PublicShareItem from './PublicShareItem'
export default {
name: 'SharesPublic',
components: {
Actions,
ActionButton,
ButtonDiv,
ConfigBox,
PublicShareItem,
},
computed: {
...mapGetters({
publicShares: 'poll/shares/public',
}),
},
methods: {
copyLink(payload) {
this
.$copyText(window.location.origin + payload.url)
.then(() => {
showSuccess(t('polls', 'Link copied to clipboard'))
})
.catch(() => {
showError(t('polls', 'Error while copying link to clipboard'))
})
},
shareUrl(share) {
return generateUrl('apps/polls/s/') + share.token
},
removeShare(share) {
this.$store.dispatch('poll/shares/delete', { share: share })
},
addShare(payload) {
this.$store
.dispatch('poll/shares/add', {
share: payload,
type: payload.type,
id: payload.id,
emailAddress: payload.emailAddress,
})
.catch(error => {
console.error('Error while adding share - Error: ', error)
showError(t('polls', 'Error while adding share'))
})
},
},
}
</script>
<style lang="scss">
.shared-list {
display: flex;
flex-wrap: wrap;
flex-direction: column;
justify-content: flex-start;
padding-top: 8px;
> li {
display: flex;
align-items: stretch;
margin: 4px 0;
}
}
.share-item {
display: flex;
flex: 1;
align-items: center;
max-width: 100%;
}
</style>

Просмотреть файл

@ -0,0 +1,136 @@
<!--
- @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>
<ConfigBox v-if="unsentInvitations.length" :title="t('polls', 'Unsent invitations')" icon-class="icon-polls-mail">
<TransitionGroup :css="false" tag="div" class="shared-list">
<UserItem v-for="(share) in unsentInvitations"
:key="share.id"
v-bind="share"
:icon="true">
<Actions>
<ActionButton
v-if="share.emailAddress || share.type === 'group'"
icon="icon-confirm"
@click="sendInvitation(share)">
{{ t('polls', 'Send invitation mail') }}
</ActionButton>
<ActionButton
v-if="share.type === 'contactGroup' || share.type === 'circle'"
icon="icon-toggle-filelist"
@click="resolveGroup(share)">
{{ t('polls', 'Resolve into individual invitations') }}
</ActionButton>
</Actions>
<Actions>
<ActionButton icon="icon-delete" @click="removeShare(share)">
{{ t('polls', 'Remove invitation') }}
</ActionButton>
</Actions>
</UserItem>
</TransitionGroup>
</ConfigBox>
</template>
<script>
import { mapGetters } from 'vuex'
import { showSuccess, showError } from '@nextcloud/dialogs'
import { Actions, ActionButton } from '@nextcloud/vue'
import ConfigBox from '../Base/ConfigBox'
export default {
name: 'SideBarTabShare',
components: {
Actions,
ActionButton,
ConfigBox,
},
computed: {
...mapGetters({
unsentInvitations: 'poll/shares/unsentInvitations',
}),
},
methods: {
resolveGroup(share) {
this.$store.dispatch('poll/shares/resolveGroup', { share: share })
.catch((error) => {
if (error.response.status === 409 && error.response.data === 'Circles is not enabled for this user') {
showError(t('polls', 'Resolving of {name} is not possible. The circles app is not enabled.', { name: share.displayName }))
} else if (error.response.status === 409 && error.response.data === 'Contacts is not enabled') {
showError(t('polls', 'Resolving of {name} is not possible. The contacts app is not enabled.', { name: share.displayName }))
} else {
showError(t('polls', 'Error resolving {name}.', { name: share.displayName }))
}
})
},
sendInvitation(share) {
this.$store.dispatch('poll/shares/sendInvitation', { share: share })
.then((response) => {
if ('sentResult.sentMails' in response.data) {
response.data.sentResult.sentMails.forEach((item) => {
showSuccess(t('polls', 'Invitation sent to {name}', { name: item.displayName }))
})
}
if ('sentResult.abortedMails' in response.data) {
response.data.sentResult.abortedMails.forEach((item) => {
console.error('Mail could not be sent!', { recipient: item })
showError(t('polls', 'Error sending invitation to {name}', { name: item.dispalyName }))
})
}
})
},
removeShare(share) {
this.$store.dispatch('poll/shares/delete', { share: share })
},
},
}
</script>
<style lang="scss">
.shared-list {
display: flex;
flex-wrap: wrap;
flex-direction: column;
justify-content: flex-start;
padding-top: 8px;
> li {
display: flex;
align-items: stretch;
margin: 4px 0;
}
}
.share-item {
display: flex;
flex: 1;
align-items: center;
max-width: 100%;
}
</style>

Просмотреть файл

@ -53,7 +53,7 @@
:order="4"
:name="t('polls', 'Comments')"
icon="icon-comment">
<Comments />
<SideBarTabComments />
</AppSidebarTab>
</AppSidebar>
</template>
@ -63,7 +63,7 @@ import { AppSidebar, AppSidebarTab } from '@nextcloud/vue'
import SideBarTabConfiguration from './SideBarTabConfiguration'
import SideBarTabOptions from './SideBarTabOptions'
import Comments from '../Comments/Comments'
import SideBarTabComments from './SideBarTabComments'
import SideBarTabShare from './SideBarTabShare'
import { mapState } from 'vuex'
import { emit } from '@nextcloud/event-bus'
@ -73,7 +73,7 @@ export default {
components: {
SideBarTabConfiguration,
Comments,
SideBarTabComments,
SideBarTabOptions,
SideBarTabShare,
AppSidebar,

Просмотреть файл

@ -0,0 +1,66 @@
<!--
- @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="comments">
<CommentAdd v-if="acl.allowComment" />
<Comments v-if="!showEmptyContent" />
<EmptyContent v-else icon="icon-comment">
{{ t('polls', 'No comments') }}
<template #desc>
{{ t('polls', 'Be the first.') }}
</template>
</EmptyContent>
</div>
</template>
<script>
import CommentAdd from '../Comments/CommentAdd'
import Comments from '../Comments/Comments'
import { EmptyContent } from '@nextcloud/vue'
import { mapGetters, mapState } from 'vuex'
export default {
name: 'SideBarTabComments',
components: {
CommentAdd,
Comments,
EmptyContent,
},
computed: {
...mapState({
acl: state => state.poll.acl,
}),
...mapGetters({
countComments: 'poll/comments/count',
}),
showEmptyContent() {
return this.countComments === 0
},
},
}
</script>

Просмотреть файл

@ -22,31 +22,45 @@
<template>
<div>
<ConfigBox v-if="!acl.isOwner" :title="t('polls', 'As an admin you may edit this poll')" icon-class="icon-checkmark" />
<SideBarTabOptionsDate v-if="poll.type === 'datePoll'" />
<SideBarTabOptionsText v-if="poll.type === 'textPoll'" />
<ConfigBox v-if="!isOwner" :title="t('polls', 'As an admin you may edit this poll')" icon-class="icon-checkmark" />
<OptionsDateAdd v-if="pollType === 'datePoll' && !pollIsClosed" />
<OptionsDateShift v-if="optionsExist && !pollIsClosed" />
<OptionsDate v-if="pollType === 'datePoll'" />
<OptionsTextAdd v-if="pollType === 'textPoll' && !pollIsClosed" />
<OptionsText v-if="pollType === 'textPoll'" />
</div>
</template>
<script>
import { mapState } from 'vuex'
import { mapGetters, mapState } from 'vuex'
import ConfigBox from '../Base/ConfigBox'
import SideBarTabOptionsDate from './SideBarTabOptionsDate'
import SideBarTabOptionsText from './SideBarTabOptionsText'
import OptionsDate from '../Options/OptionsDate'
import OptionsDateAdd from '../Options/OptionsDateAdd'
import OptionsDateShift from '../Options/OptionsDateShift'
import OptionsText from '../Options/OptionsText'
import OptionsTextAdd from '../Options/OptionsTextAdd'
export default {
name: 'SideBarTabOptions',
components: {
ConfigBox,
SideBarTabOptionsDate,
SideBarTabOptionsText,
OptionsDate,
OptionsDateAdd,
OptionsDateShift,
OptionsText,
OptionsTextAdd,
},
computed: {
...mapGetters({
pollIsClosed: 'poll/closed',
}),
...mapState({
poll: state => state.poll,
acl: state => state.poll.acl,
pollType: state => state.poll.type,
isOwner: state => state.poll.acl.isOwner,
optionsExist: state => state.poll.options.length,
}),
},

Просмотреть файл

@ -22,242 +22,45 @@
<template>
<div>
<ConfigBox v-if="!acl.isOwner" :title="t('polls', 'As an admin you may edit this poll')" icon-class="icon-checkmark" />
<ConfigBox :title="t('polls', 'Effective shares')" icon-class="icon-share">
<TransitionGroup :css="false" tag="div" class="shared-list">
<UserItem v-for="(share) in invitationShares"
:key="share.id" v-bind="share"
:icon="true">
<Actions>
<ActionButton
v-if="share.emailAddress || share.type === 'group'"
icon="icon-confirm"
@click="sendInvitation(share)">
{{ share.invitationSent ? t('polls', 'Resend invitation mail') : t('polls', 'Send invitation mail') }}
</ActionButton>
<ActionButton icon="icon-clippy" @click="copyLink( { url: shareUrl(share) })">
{{ t('polls', 'Copy link to clipboard') }}
</ActionButton>
</Actions>
<Actions>
<ActionButton icon="icon-delete" @click="removeShare(share)">
{{ t('polls', 'Remove share') }}
</ActionButton>
</Actions>
</UserItem>
</TransitionGroup>
<Multiselect id="ajax"
:options="users"
:multiple="false"
:user-select="true"
:tag-width="80"
:clear-on-select="false"
:preserve-search="true"
:options-limit="30"
:loading="isLoading"
:internal-search="false"
:searchable="true"
:preselect-first="true"
:placeholder="placeholder"
label="displayName"
track-by="userId"
@select="addShare"
@search-change="loadUsersAsync">
<template slot="selection" slot-scope="{ values, isOpen }">
<span v-if="values.length &amp;&amp; !isOpen" class="multiselect__single">
{{ values.length }} users selected
</span>
</template>
</Multiselect>
</ConfigBox>
<ConfigBox :title="t('polls', 'Public shares')" icon-class="icon-public">
<TransitionGroup :css="false" tag="div" class="shared-list">
<PublicShareItem v-for="(share) in publicShares"
:key="share.id"
v-bind="share">
<Actions>
<ActionButton icon="icon-clippy" @click="copyLink( { url: shareUrl(share) })">
{{ t('polls', 'Copy link to clipboard') }}
</ActionButton>
</Actions>
<Actions>
<ActionButton icon="icon-delete" @click="removeShare(share)">
{{ t('polls', 'Remove share') }}
</ActionButton>
</Actions>
</PublicShareItem>
</TransitionGroup>
<ButtonDiv :title="t('polls', 'Add a public link')" icon="icon-add" @click="addShare({type: 'public', userId: '', emailAddress: ''})" />
</ConfigBox>
<ConfigBox v-if="unsentInvitations.length" :title="t('polls', 'Unsent invitations')" icon-class="icon-polls-mail">
<TransitionGroup :css="false" tag="div" class="shared-list">
<UserItem v-for="(share) in unsentInvitations"
:key="share.id"
v-bind="share"
:icon="true">
<Actions>
<ActionButton
v-if="share.emailAddress || share.type === 'group'"
icon="icon-confirm"
@click="sendInvitation(share)">
{{ t('polls', 'Send invitation mail') }}
</ActionButton>
<ActionButton
v-if="share.type === 'contactGroup' || share.type === 'circle'"
icon="icon-toggle-filelist"
@click="resolveGroup(share)">
{{ t('polls', 'Resolve into individual invitations') }}
</ActionButton>
</Actions>
<Actions>
<ActionButton icon="icon-delete" @click="removeShare(share)">
{{ t('polls', 'Remove invitation') }}
</ActionButton>
</Actions>
</UserItem>
</TransitionGroup>
<ConfigBox v-if="!isOwner" :title="t('polls', 'As an admin you may edit this poll')" icon-class="icon-checkmark" />
<SharesEffective />
<ConfigBox :title="t('polls', 'Add Shares')" icon-class="icon-add">
<UserSearch />
</ConfigBox>
<SharesPublic />
<SharesUnsent />
</div>
</template>
<script>
import debounce from 'lodash/debounce'
import { mapState, mapGetters } from 'vuex'
import axios from '@nextcloud/axios'
import { showSuccess, showError } from '@nextcloud/dialogs'
import { generateUrl } from '@nextcloud/router'
import { Actions, ActionButton, Multiselect } from '@nextcloud/vue'
import ButtonDiv from '../Base/ButtonDiv'
import { mapState } from 'vuex'
import ConfigBox from '../Base/ConfigBox'
import PublicShareItem from '../Base/PublicShareItem'
import UserSearch from '../Base/UserSearch'
import SharesEffective from '../Shares/SharesEffective'
import SharesPublic from '../Shares/SharesPublic'
import SharesUnsent from '../Shares/SharesUnsent'
export default {
name: 'SideBarTabShare',
components: {
Actions,
ActionButton,
ButtonDiv,
ConfigBox,
Multiselect,
PublicShareItem,
},
data() {
return {
searchToken: null,
users: [],
isLoading: false,
placeholder: t('polls', 'Enter a name to start the search'),
}
UserSearch,
SharesPublic,
SharesEffective,
SharesUnsent,
},
computed: {
...mapState({
acl: state => state.poll.acl,
}),
...mapGetters({
invitationShares: 'poll/shares/invitation',
unsentInvitations: 'poll/shares/unsentInvitations',
publicShares: 'poll/shares/public',
isOwner: state => state.poll.acl.isOwner,
}),
},
methods: {
resolveGroup(share) {
this.$store.dispatch('poll/shares/resolveGroup', { share: share })
.catch((error) => {
if (error.response.status === 409 && error.response.data === 'Circles is not enabled for this user') {
showError(t('polls', 'Resolving of {name} is not possible. The circles app is not enabled.', { name: share.displayName }))
} else if (error.response.status === 409 && error.response.data === 'Contacts is not enabled') {
showError(t('polls', 'Resolving of {name} is not possible. The contacts app is not enabled.', { name: share.displayName }))
} else {
showError(t('polls', 'Error resolving {name}.', { name: share.displayName }))
}
})
},
sendInvitation(share) {
this.$store.dispatch('poll/shares/sendInvitation', { share: share })
.then((response) => {
if ('sentResult.sentMails' in response.data) {
response.data.sentResult.sentMails.forEach((item) => {
showSuccess(t('polls', 'Invitation sent to {name}', { name: item.displayName }))
})
}
if ('sentResult.abortedMails' in response.data) {
response.data.sentResult.abortedMails.forEach((item) => {
console.error('Mail could not be sent!', { recipient: item })
showError(t('polls', 'Error sending invitation to {name}', { name: item.dispalyName }))
})
}
})
},
loadUsersAsync: debounce(function(query) {
if (!query) {
this.users = []
return
}
this.isLoading = true
if (this.searchToken) {
this.searchToken.cancel()
}
this.searchToken = axios.CancelToken.source()
axios.get(generateUrl('apps/polls/search/users/' + query), { cancelToken: this.searchToken.token })
.then((response) => {
this.users = response.data.siteusers
this.isLoading = false
})
.catch((error) => {
if (axios.isCancel(error)) {
// request was cancelled
} else {
console.error(error.response)
this.isLoading = false
}
})
}, 250),
copyLink(payload) {
this
.$copyText(window.location.origin + payload.url)
.then(() => {
showSuccess(t('polls', 'Link copied to clipboard'))
})
.catch(() => {
showError(t('polls', 'Error while copying link to clipboard'))
})
},
shareUrl(share) {
return generateUrl('apps/polls/s/') + share.token
},
removeShare(share) {
this.$store.dispatch('poll/shares/delete', { share: share })
},
addShare(payload) {
this.$store
.dispatch('poll/shares/add', {
share: payload,
type: payload.type,
id: payload.id,
emailAddress: payload.emailAddress,
})
.catch(error => {
console.error('Error while adding share - Error: ', error)
showError(t('polls', 'Error while adding share'))
})
},
},
}
</script>
@ -293,9 +96,4 @@ export default {
overflow: hidden;
text-overflow: ellipsis;
}
.multiselect {
width: 100% !important;
max-width: 100% !important;
}
</style>

Просмотреть файл

@ -34,7 +34,7 @@
<script>
import { mapState, mapGetters } from 'vuex'
import OptionItem from '../Base/OptionItem'
import OptionItem from '../Options/OptionItem'
import Counter from '../Base/Counter'
import Confirmation from '../Base/Confirmation'