Skip to content

Commit

Permalink
Merge pull request #2273 from t-h-e/feature/bulk-task-creation
Browse files Browse the repository at this point in the history
Allow bulk task creation on paste
  • Loading branch information
raimund-schluessler authored Aug 29, 2023
2 parents dfde3c8 + 55438da commit 55741e6
Show file tree
Hide file tree
Showing 5 changed files with 571 additions and 19 deletions.
205 changes: 205 additions & 0 deletions src/components/CreateMultipleTasksDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
<!--
Nextcloud - Tasks
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
License as published by the Free Software Foundation; either
version 3 of the License, or any later version.
This library 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 library. If not, see <http://www.gnu.org/licenses/>.
-->

<template>
<NcModal class="task-selector" size="small" @close="() => {!created ? cancel() : close()}">
<div v-if="!creating && !created" id="modal-inner">
<h3>{{ t('tasks', 'Create new tasks') }}</h3>

<p>{{ t('tasks', 'Create {numberOfTasks} tasks from pasted text', { numberOfTasks: tasksToCreate.numberOfTasks }) }}</p>

<div class="modal-buttons">
<NcButton @click="cancel">
{{ t('tasks', 'Cancel') }}
</NcButton>
<NcButton ref="createButton"
type="primary"
@click="addTasks">
{{ t('tasks', 'Create tasks') }}
</NcButton>
</div>
</div>
<div v-else id="modal-inner">
<NcEmptyContent v-if="creating" key="creating" :description="t('tasks', 'Creating new tasks…')">
<template #icon>
<NcLoadingIcon />
</template>
</NcEmptyContent>
<NcEmptyContent v-else-if="created" key="created" :description="createdMessage">
<template #icon>
<Check />
</template>
<template #action>
<NcButton @click="close">
{{ t('tasks', 'Close') }}
</NcButton>
</template>
</NcEmptyContent>
</div>
</NcModal>
</template>

<script>
import { translate as t } from '@nextcloud/l10n'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
import NcModal from '@nextcloud/vue/dist/Components/NcModal.js'
import Check from 'vue-material-design-icons/Check.vue'
import { mapActions } from 'vuex'
export default {
name: 'CreateMultipleTasksDialog',
components: {
Check,
NcButton,
NcEmptyContent,
NcLoadingIcon,
NcModal,
},
props: {
calendar: {
type: Object,
required: true,
},
tasksToCreate: {
type: Object,
required: true,
},
tasksAdditionalProperties: {
type: Object,
default() {
return {}
},
},
rootTask: {
type: Object,
default: undefined,
},
},
emits: ['cancel', 'close'],
data() {
return {
creating: false,
created: false,
}
},
computed: {
createdMessage() {
return t('tasks', '{numberOfTasks} tasks have been added to "{calendar}"', { numberOfTasks: this.tasksToCreate.numberOfTasks, calendar: this.calendar.displayName }, undefined, { sanitize: false, escape: false })
},
},
mounted() {
this.$nextTick(() => this.$refs.createButton?.$el?.focus())
},
methods: {
...mapActions([
'createTask',
]),
t,
cancel() {
this.$emit('cancel')
this.$root.$emit('close')
},
close() {
this.$emit('close')
this.$root.$emit('close')
},
async addTasks() {
this.creating = true
await Promise.all(this.tasksToCreate.tasks.map(task => this.addTaskWithParent(task, this.rootTask?.uid)))
this.creating = false
this.created = true
},
async addTaskWithParent(task, parentUid) {
const newParent = await this.createTask({
summary: task.summary,
calendar: this.calendar,
related: parentUid,
...this.tasksAdditionalProperties,
})
await Promise.all(task.children.map(child => this.addTaskWithParent(child, newParent?.uid)))
},
},
}
</script>
<style lang="scss" scoped>
#modal-inner {
display: flex;
flex-direction: column;
padding: 20px;
.loading-overlay {
position: absolute;
top: calc(50% - 20px);
left: calc(50% - 20px);
z-index: 1000;
}
.empty-content {
margin: 10vh 0;
:deep(.empty-content__action) {
display: flex;
}
}
}
.property__item {
border-bottom: none;
margin-bottom: 3px;
:deep(.multiselect .multiselect__tags) {
border: 2px solid var(--color-border-dark);
}
}
.property {
position: relative;
.material-design-icon {
position: absolute;
top: 14px;
left: 14px;
}
}
.modal-buttons {
display: flex;
justify-content: flex-end;
}
:deep(.calendar-picker-option__label),
:deep(.property__item .multiselect__tags) input.multiselect__input {
font-weight: normal;
}
</style>
74 changes: 64 additions & 10 deletions src/components/HeaderBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
<div class="header">
<div v-if="$route.params.collectionId !== 'completed' && calendar && !calendar.readOnly"
class="header__input">
<NcTextField :value.sync="newTaskName"
<NcTextField ref="input"
:value.sync="newTaskName"
:label="placeholder"
autocomplete="off"
class="reactive"
Expand All @@ -32,11 +33,18 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
:trailing-button-label="placeholder"
@trailing-button-click="addTask"
@keyup.esc="clearNewTask($event)"
@keyup.enter="addTask">
@keyup.enter="addTask"
@paste.stop="addMultipleTasks">
<Plus :size="20" />
</NcTextField>
</div>
<SortorderDropdown />
<CreateMultipleTasksDialog v-if="showCreateMultipleTasksModal"
:calendar="calendar"
:tasks-to-create="multipleTasks"
:tasks-additional-properties="additionalTaskProperties"
@cancel="createMultipleTasksCancelled"
@close="createMultipleTasksSuccessful" />
</div>
</template>

Expand All @@ -51,15 +59,22 @@ import Plus from 'vue-material-design-icons/Plus.vue'
import { mapGetters, mapActions } from 'vuex'
import { textToTask } from '../utils/textToTask.js'
import CreateMultipleTasksDialog from './CreateMultipleTasksDialog.vue'
export default {
components: {
CreateMultipleTasksDialog,
NcTextField,
SortorderDropdown,
Plus,
},
data() {
return {
newTaskName: '',
showCreateMultipleTasksModal: false,
multipleTasks: { numberOfTasks: 0, tasks: {} },
additionalTaskProperties: {},
}
},
computed: {
Expand Down Expand Up @@ -91,31 +106,70 @@ export default {
this.newTaskName = ''
},
addTask() {
const task = { summary: this.newTaskName }
addTaskWithName(task) {
// If the task is created in the calendar view,
// we set the current calendar
if (this.$route.params.calendarId) {
task.calendar = this.calendar
}
return this.createTask({
...task,
...this.getAdditionalTaskProperties(),
})
},
getAdditionalTaskProperties() {
const taskProperties = {}
// If the task is created in a collection view,
// set the appropriate properties.
if (this.$route.params.collectionId === 'starred') {
task.priority = 1
taskProperties.priority = 1
}
if (this.$route.params.collectionId === 'today'
|| this.$route.params.collectionId === 'week') {
task.due = moment().startOf('day').format('YYYY-MM-DDTHH:mm:ss')
task.allDay = this.$store.state.settings.settings.allDay
taskProperties.due = moment().startOf('day').format('YYYY-MM-DDTHH:mm:ss')
taskProperties.allDay = this.$store.state.settings.settings.allDay
}
if (this.$route.params.collectionId === 'current') {
task.start = moment().format('YYYY-MM-DDTHH:mm:ss')
taskProperties.start = moment().format('YYYY-MM-DDTHH:mm:ss')
}
return taskProperties
},
addMultipleTasks($event) {
const pastedText = $event.clipboardData.getData('text')
const tasksFromText = textToTask(pastedText)
if (tasksFromText.numberOfTasks <= 1) {
return
}
this.createTask(task)
this.multipleTasks = tasksFromText
this.showCreateMultipleTasksModal = true
this.additionalTaskProperties = this.getAdditionalTaskProperties()
},
addTask() {
this.addTaskWithName({ summary: this.newTaskName })
this.newTaskName = ''
},
async createMultipleTasksCancelled() {
this.showCreateMultipleTasksModal = false
this.multipleTasks = { numberOfTasks: 0, tasks: {} }
this.additionalTaskProperties = {}
await this.$nextTick()
this.$refs.input.$refs.inputField.$refs.input.focus()
},
async createMultipleTasksSuccessful() {
this.showCreateMultipleTasksModal = false
this.multipleTasks = { numberOfTasks: 0, tasks: {} }
this.additionalTaskProperties = {}
this.newTaskName = ''
await this.$nextTick()
this.$refs.input.$refs.inputField.$refs.input.focus()
},
},
}
Expand Down
Loading

0 comments on commit 55741e6

Please sign in to comment.