From 74346405252b3314f7fe0b411e9d36d43c67a666 Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 27 Jul 2021 17:57:42 +0200 Subject: [PATCH 01/40] Use kanbanPosition or position, whatever makes sense --- src/components/tasks/mixins/createTask.js | 11 +++++++---- src/helpers/calculateTaskPosition.test.ts | 18 ++++++++++++++++++ src/helpers/calculateTaskPosition.ts | 21 +++++++++++++++++++++ src/models/task.js | 3 +++ src/store/modules/kanban.js | 4 ++-- src/views/list/views/Kanban.vue | 17 ++++------------- 6 files changed, 55 insertions(+), 19 deletions(-) create mode 100644 src/helpers/calculateTaskPosition.test.ts create mode 100644 src/helpers/calculateTaskPosition.ts diff --git a/src/components/tasks/mixins/createTask.js b/src/components/tasks/mixins/createTask.js index 2c68c5a4f..989636a6e 100644 --- a/src/components/tasks/mixins/createTask.js +++ b/src/components/tasks/mixins/createTask.js @@ -36,14 +36,17 @@ export default { const list = this.$store.getters['lists/findListByExactname'](parsedTask.list) listId = list === null ? null : list.id } - if (listId === null) { - listId = lId !== 0 ? lId : this.$route.params.listId + if (lId !== 0) { + listId = lId + } + if (typeof this.$route.params.listId !== 'undefined') { + listId = parseInt(this.$route.params.listId) } - if (typeof listId === 'undefined' || listId === 0) { + if (typeof listId === 'undefined' || listId === null) { return Promise.reject('NO_LIST') } - + // Separate closure because we need to wait for the results of the user search if users were entered in the // task create request. Because _that_ happens in a promise, we'll need something to call when it resolves. const createTask = () => { diff --git a/src/helpers/calculateTaskPosition.test.ts b/src/helpers/calculateTaskPosition.test.ts new file mode 100644 index 000000000..f41290088 --- /dev/null +++ b/src/helpers/calculateTaskPosition.test.ts @@ -0,0 +1,18 @@ +import {calculateTaskPosition} from './calculateTaskPosition' + +it('should calculate the task position', () => { + const result = calculateTaskPosition(10, 100) + expect(result).toBe(55) +}) +it('should return 0 if no position was provided', () => { + const result = calculateTaskPosition(null, null) + expect(result).toBe(0) +}) +it('should calculate the task position for the first task', () => { + const result = calculateTaskPosition(null, 100) + expect(result).toBe(50) +}) +it('should calculate the task position for the last task', () => { + const result = calculateTaskPosition(10, null) + expect(result).toBe(65546) +}) diff --git a/src/helpers/calculateTaskPosition.ts b/src/helpers/calculateTaskPosition.ts new file mode 100644 index 000000000..3a777ed09 --- /dev/null +++ b/src/helpers/calculateTaskPosition.ts @@ -0,0 +1,21 @@ +export const calculateTaskPosition = (positionBefore: number | null, positionAfter: number | null): number => { + console.log('calculate', positionBefore, positionAfter) + + if (positionBefore === null && positionAfter === null) { + return 0 + } + + // If there is no task before, our task is the first task in which case we let it have half of the position of the task after it + if (positionBefore === null && positionAfter !== null) { + return positionAfter / 2 + } + + // If there is no task after it, we just add 2^16 to the last position to have enough room in the future + if (positionBefore !== null && positionAfter === null) { + return positionBefore + Math.pow(2, 16) + } + + // If we have both a task before and after it, we acually calculate the position + // @ts-ignore - can never be null but TS does not seem to understand that + return positionBefore + (positionAfter - positionBefore) / 2 +} \ No newline at end of file diff --git a/src/models/task.js b/src/models/task.js index 7beeca23c..91f6cff69 100644 --- a/src/models/task.js +++ b/src/models/task.js @@ -104,6 +104,9 @@ export default class TaskModel extends AbstractModel { index: 0, isFavorite: false, subscription: null, + + position: 0, + kanbanPosition: 0, createdBy: UserModel, created: null, diff --git a/src/store/modules/kanban.js b/src/store/modules/kanban.js index ba1007ad7..89722f584 100644 --- a/src/store/modules/kanban.js +++ b/src/store/modules/kanban.js @@ -11,7 +11,7 @@ const tasksPerBucket = 25 const addTaskToBucketAndSort = (state, task) => { const bi = filterObject(state.buckets, b => b.id === task.bucketId) state.buckets[bi].tasks.push(task) - state.buckets[bi].tasks.sort((a, b) => a.position > b.position ? 1 : -1) + state.buckets[bi].tasks.sort((a, b) => a.kanbanPosition > b.kanbanPosition ? 1 : -1) } /** @@ -208,7 +208,7 @@ export default { const params = cloneDeep(ps) - params.sort_by = 'position' + params.sort_by = 'kanban_position' params.order_by = 'asc' let hasBucketFilter = false diff --git a/src/views/list/views/Kanban.vue b/src/views/list/views/Kanban.vue index d7afcc4f5..6bdcfd785 100644 --- a/src/views/list/views/Kanban.vue +++ b/src/views/list/views/Kanban.vue @@ -295,6 +295,7 @@ import Dropdown from '@/components/misc/dropdown.vue' import {playPop} from '@/helpers/playPop' import createTask from '@/components/tasks/mixins/createTask' import {getCollapsedBucketState, saveCollapsedBucketState} from '@/helpers/saveCollapsedBucketState' +import {calculateTaskPosition} from '../../../helpers/calculateTaskPosition' export default { name: 'Kanban', @@ -447,19 +448,9 @@ export default { this.$set(this.taskUpdating, task.id, true) this.oneTaskUpdating = true - // If there is no task before, our task is the first task in which case we let it have half of the position of the task after it - if (taskBefore === null && taskAfter !== null) { - task.position = taskAfter.position / 2 - } - // If there is no task after it, we just add 2^16 to the last position - if (taskBefore !== null && taskAfter === null) { - task.position = taskBefore.position + Math.pow(2, 16) - } - // If we have both a task before and after it, we acually calculate the position - if (taskAfter !== null && taskBefore !== null) { - task.position = taskBefore.position + (taskAfter.position - taskBefore.position) / 2 - } - + task.kanbanPosition = calculateTaskPosition(taskBefore !== null ? taskBefore.kanbanPosition : null, taskAfter !== null ? taskAfter.kanbanPosition : null) + console.log(task.kanbanPosition) + task.bucketId = bucketId this.$store.dispatch('tasks/update', task) -- 2.40.1 From 5a29982e997dc9cca5ef7836181064ace9c6089a Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 27 Jul 2021 18:04:00 +0200 Subject: [PATCH 02/40] Add reordering tasks --- package.json | 1 + src/components/tasks/add-task.vue | 8 ++- src/components/tasks/mixins/createTask.js | 3 +- src/components/tasks/mixins/taskList.js | 21 ++++++- src/views/list/views/List.vue | 70 ++++++++++++++--------- yarn.lock | 12 ++++ 6 files changed, 85 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index a1a7ba0a6..095ca49ed 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "vue-i18n": "8.25.0", "vue-shortkey": "3.1.7", "vue-smooth-dnd": "0.8.1", + "vuedraggable": "^2.24.3", "vuex": "3.6.2", "workbox-precaching": "6.1.5" }, diff --git a/src/components/tasks/add-task.vue b/src/components/tasks/add-task.vue index 64f71c91f..d92854628 100644 --- a/src/components/tasks/add-task.vue +++ b/src/components/tasks/add-task.vue @@ -66,6 +66,12 @@ export default { this.labelService = new LabelService() this.labelTaskService = new LabelTaskService() }, + props: { + defaultPosition: { + type: Number, + required: false, + }, + }, methods: { addTask() { if (this.newTaskTitle === '') { @@ -74,7 +80,7 @@ export default { } this.errorMessage = '' - this.createNewTask(this.newTaskTitle, 0, this.$store.state.auth.settings.defaultListId) + this.createNewTask(this.newTaskTitle, 0, this.$store.state.auth.settings.defaultListId, this.defaultPosition) .then(task => { this.newTaskTitle = '' this.$emit('taskAdded', task) diff --git a/src/components/tasks/mixins/createTask.js b/src/components/tasks/mixins/createTask.js index 989636a6e..968516666 100644 --- a/src/components/tasks/mixins/createTask.js +++ b/src/components/tasks/mixins/createTask.js @@ -22,7 +22,7 @@ export default { labels: state => state.labels.labels, }), methods: { - createNewTask(newTaskTitle, bucketId = 0, lId = 0) { + createNewTask(newTaskTitle, bucketId = 0, lId = 0, position = 0) { const parsedTask = parseTaskText(newTaskTitle) const assignees = [] @@ -57,6 +57,7 @@ export default { priority: parsedTask.priority, assignees: assignees, bucketId: bucketId, + position: position, }) return this.taskService.create(task) .then(task => { diff --git a/src/components/tasks/mixins/taskList.js b/src/components/tasks/mixins/taskList.js index 2a2576915..64c425b66 100644 --- a/src/components/tasks/mixins/taskList.js +++ b/src/components/tasks/mixins/taskList.js @@ -1,5 +1,6 @@ import TaskCollectionService from '../../../services/taskCollection' import cloneDeep from 'lodash/cloneDeep' +import {calculateTaskPosition} from '../../../helpers/calculateTaskPosition' /** * This mixin provides a base set of methods and properties to get tasks on a list. @@ -20,7 +21,7 @@ export default { showTaskFilter: false, params: { - sort_by: ['done', 'id'], + sort_by: ['position', 'id'], order_by: ['asc', 'desc'], filter_by: ['done'], filter_value: ['false'], @@ -187,5 +188,23 @@ export default { }, } }, + saveTaskPosition(e) { + console.log(e.oldIndex, e.newIndex) + console.log('elem', this.tasks[e.newIndex].title) + + const task = this.tasks[e.newIndex] + const taskBefore = this.tasks[e.newIndex - 1] ?? null + const taskAfter = this.tasks[e.newIndex + 1] ?? null + + task.position = calculateTaskPosition(taskBefore !== null ? taskBefore.position : null, taskAfter !== null ? taskAfter.position : null) + + this.$store.dispatch('tasks/update', task) + .then(r => { + this.$set(this.tasks, e.newIndex, r) + }) + .catch(e => { + this.error(e) + }) + }, }, } \ No newline at end of file diff --git a/src/views/list/views/List.vue b/src/views/list/views/List.vue index 722c67041..47d75b7e4 100644 --- a/src/views/list/views/List.vue +++ b/src/views/list/views/List.vue @@ -21,7 +21,7 @@ v-model="searchTerm" /> - +
@@ -63,16 +63,15 @@ - {{ $t('list.list.empty') }} {{ $t('list.list.newTaskCta') }} -
@@ -81,23 +80,25 @@ class="tasks mt-0" v-if="tasks && tasks.length > 0" > - -
+ - -
-
+
+ +
+ +
- +
@@ -163,14 +164,17 @@ import EditTask from '../../../components/tasks/edit-task' import AddTask from '../../../components/tasks/add-task' import SingleTaskInList from '../../../components/tasks/partials/singleTaskInList' import taskList from '../../../components/tasks/mixins/taskList' -import { saveListView } from '@/helpers/saveListView' +import {saveListView} from '@/helpers/saveListView' import Rights from '../../../models/rights.json' -import { mapState } from 'vuex' import FilterPopup from '@/components/list/partials/filter-popup.vue' -import { HAS_TASKS } from '@/store/mutation-types' +import {HAS_TASKS} from '@/store/mutation-types' import Nothing from '@/components/misc/nothing.vue' import createTask from '@/components/tasks/mixins/createTask' +import {mapState} from 'vuex' +import draggable from 'vuedraggable' +import {calculateTaskPosition} from '../../../helpers/calculateTaskPosition' + export default { name: 'List', data() { @@ -191,6 +195,7 @@ export default { SingleTaskInList, EditTask, AddTask, + draggable, }, created() { this.taskService = new TaskService() @@ -199,10 +204,21 @@ export default { // We use local storage and not vuex here to make it persistent across reloads. saveListView(this.$route.params.listId, this.$route.name) }, - computed: mapState({ - canWrite: state => state.currentList.maxRight > Rights.READ, - list: state => state.currentList, - }), + computed: { + firstNewPosition() { + if (this.tasks.length === 0) { + return 0 + } + + const p = calculateTaskPosition(null, this.tasks[0].position) + console.log('newp', p) + return p + }, + ...mapState({ + canWrite: state => state.currentList.maxRight > Rights.READ, + list: state => state.currentList, + }), + }, mounted() { this.$nextTick(() => (this.ctaVisible = true)) }, diff --git a/yarn.lock b/yarn.lock index e790f84e6..88f4d02cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7245,6 +7245,11 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" +sortablejs@1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.10.2.tgz#6e40364d913f98b85a14f6678f92b5c1221f5290" + integrity sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A== + source-map-js@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e" @@ -8169,6 +8174,13 @@ vue@2.6.14: resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.14.tgz#e51aa5250250d569a3fbad3a8a5a687d6036e235" integrity sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ== +vuedraggable@^2.24.3: + version "2.24.3" + resolved "https://registry.yarnpkg.com/vuedraggable/-/vuedraggable-2.24.3.tgz#43c93849b746a24ce503e123d5b259c701ba0d19" + integrity sha512-6/HDXi92GzB+Hcs9fC6PAAozK1RLt1ewPTLjK0anTYguXLAeySDmcnqE8IC0xa7shvSzRjQXq3/+dsZ7ETGF3g== + dependencies: + sortablejs "1.10.2" + vuex@3.6.2: version "3.6.2" resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.6.2.tgz#236bc086a870c3ae79946f107f16de59d5895e71" -- 2.40.1 From e1ecca3596f8977b85a4cd1a5d38a454c12c2f93 Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 27 Jul 2021 18:07:49 +0200 Subject: [PATCH 03/40] Format --- src/styles/components/tasks.scss | 520 +++++++++++++++---------------- 1 file changed, 260 insertions(+), 260 deletions(-) diff --git a/src/styles/components/tasks.scss b/src/styles/components/tasks.scss index db9ed108d..c3df77118 100644 --- a/src/styles/components/tasks.scss +++ b/src/styles/components/tasks.scss @@ -1,341 +1,341 @@ .tasks-container { - display: flex; + display: flex; - .tasks { - width: 100%; - } + .tasks { + width: 100%; + } - .taskedit { - width: 50%; - } + .taskedit { + width: 50%; + } } .tasks { - margin-top: 1rem; - padding: 0; - text-align: left; + margin-top: 1rem; + padding: 0; + text-align: left; - @media screen and (min-width: $tablet) { - &.short { - max-width: 53vw; - } - } + @media screen and (min-width: $tablet) { + &.short { + max-width: 53vw; + } + } - @media screen and (max-width: $tablet) { - max-width: 100%; - } + @media screen and (max-width: $tablet) { + max-width: 100%; + } - &.noborder { - margin: 1rem -0.5rem; - } + &.noborder { + margin: 1rem -0.5rem; + } - .task { - display: flex; - flex-wrap: wrap; - padding: 0.5rem; - transition: background-color $transition; - align-items: center; - cursor: pointer; - margin: 0 .5rem; - border-radius: $radius; + .task { + display: flex; + flex-wrap: wrap; + padding: 0.5rem; + transition: background-color $transition; + align-items: center; + cursor: pointer; + margin: 0 .5rem; + border-radius: $radius; - &:first-child { - margin-top: .5rem; - } + &:first-child { + margin-top: .5rem; + } - &:last-child { - margin-bottom: .5rem; - } + &:last-child { + margin-bottom: .5rem; + } - &:hover { - background-color: $grey-100; - } + &:hover { + background-color: $grey-100; + } - .tasktext, - &.tasktext { - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - display: inline-block; - flex: 1 0 50%; + .tasktext, + &.tasktext { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + display: inline-block; + flex: 1 0 50%; - .overdue { - color: $red; - } - } + .overdue { + color: $red; + } + } - .task-list { - width: auto; - color: $grey-400; - font-size: .9rem; - white-space: nowrap; - } + .task-list { + width: auto; + color: $grey-400; + font-size: .9rem; + white-space: nowrap; + } - .fancycheckbox span { - display: none; - } + .fancycheckbox span { + display: none; + } - .color-bubble { - height: 10px; - flex: 0 0 10px; - } + .color-bubble { + height: 10px; + flex: 0 0 10px; + } - .tag { - margin: 0 0.5rem; - } + .tag { + margin: 0 0.5rem; + } - .avatar { - border-radius: 50%; - vertical-align: bottom; - margin-left: 5px; - height: 27px; - width: 27px; - } + .avatar { + border-radius: 50%; + vertical-align: bottom; + margin-left: 5px; + height: 27px; + width: 27px; + } - .list-task-icon { - margin-left: 6px; + .list-task-icon { + margin-left: 6px; - &:not(:first-of-type) { - margin-left: 8px; - } - - } + &:not(:first-of-type) { + margin-left: 8px; + } - a { - color: $text; - transition: color ease $transition-duration; + } - &:hover { - color: $grey-900; - } - } + a { + color: $text; + transition: color ease $transition-duration; - .favorite { - opacity: 0; - text-align: center; - width: 27px; - transition: opacity $transition, color $transition; + &:hover { + color: $grey-900; + } + } - &:hover { - color: $orange; - } + .favorite { + opacity: 0; + text-align: center; + width: 27px; + transition: opacity $transition, color $transition; - &.is-favorite { - opacity: 1; - color: $orange; - } - } + &:hover { + color: $orange; + } - &:hover .favorite { - opacity: 1; - } + &.is-favorite { + opacity: 1; + color: $orange; + } + } - .fancycheckbox { - height: 18px; - padding-top: 0; - padding-right: .5rem; + &:hover .favorite { + opacity: 1; + } - span { - display: none; - } - } + .fancycheckbox { + height: 18px; + padding-top: 0; + padding-right: .5rem; - .tasktext.done { - text-decoration: line-through; - color: $grey-500; - } + span { + display: none; + } + } - span.parent-tasks { - color: $grey-500; - width: auto; - } + .tasktext.done { + text-decoration: line-through; + color: $grey-500; + } - .remove { - color: $red; - } + span.parent-tasks { + color: $grey-500; + width: auto; + } - input[type="checkbox"] { - vertical-align: middle; - } + .remove { + color: $red; + } - .settings { - float: right; - width: 24px; - cursor: pointer; - } + input[type="checkbox"] { + vertical-align: middle; + } - &.loader-container.is-loading:after { - top: calc(50% - 1rem); - left: calc(50% - 1rem); - width: 2rem; - height: 2rem; - border-left-color: $grey-300; - border-bottom-color: $grey-300; - } - } + .settings { + float: right; + width: 24px; + cursor: pointer; + } - .progress { - width: 50px; - margin: 0 0.5rem 0 0; - flex: 3 1 auto; + &.loader-container.is-loading:after { + top: calc(50% - 1rem); + left: calc(50% - 1rem); + width: 2rem; + height: 2rem; + border-left-color: $grey-300; + border-bottom-color: $grey-300; + } + } - @media screen and (max-width: $tablet) { - margin: 0.5rem 0 0 0; - order: 1; - width: 100%; - } - } + .progress { + width: 50px; + margin: 0 0.5rem 0 0; + flex: 3 1 auto; - .task:last-child { - border-bottom: none; - } + @media screen and (max-width: $tablet) { + margin: 0.5rem 0 0 0; + order: 1; + width: 100%; + } + } + + .task:last-child { + border-bottom: none; + } } .is-menu-enabled .tasks .task { - span:not(.tag), a { - .tasktext, &.tasktext { - @media screen and (max-width: $desktop) { - max-width: calc(100vw - 27px - 2rem - 1.5rem - 3rem - #{$navbar-width}); // 1.5rem is the padding of the tasks container, 3rem is the padding of .app-container - } + span:not(.tag), a { + .tasktext, &.tasktext { + @media screen and (max-width: $desktop) { + max-width: calc(100vw - 27px - 2rem - 1.5rem - 3rem - #{$navbar-width}); // 1.5rem is the padding of the tasks container, 3rem is the padding of .app-container + } - // Duplicated rule to have it work properly in at least some browsers - // This should be fine as the ui doesn't work in rare edge cases to begin with - @media screen and (max-width: calc(#{$desktop} + #{$navbar-width})) { - max-width: calc(100vw - 27px - 2rem - 1.5rem - 3rem - #{$navbar-width}); // 1.5rem is the padding of the tasks container, 3rem is the padding of .app-container - } + // Duplicated rule to have it work properly in at least some browsers + // This should be fine as the ui doesn't work in rare edge cases to begin with + @media screen and (max-width: calc(#{$desktop} + #{$navbar-width})) { + max-width: calc(100vw - 27px - 2rem - 1.5rem - 3rem - #{$navbar-width}); // 1.5rem is the padding of the tasks container, 3rem is the padding of .app-container + } + } } - } } .taskedit { - min-height: calc(100% - 1rem); - margin-top: 1rem; + min-height: calc(100% - 1rem); + margin-top: 1rem; - .priority-select { - .select, select { - width: 100%; - } - } + .priority-select { + .select, select { + width: 100%; + } + } - ul.assingees { - list-style: none; - margin: 0; + ul.assingees { + list-style: none; + margin: 0; - li { - padding: 0.5rem 0.5rem 0; + li { + padding: 0.5rem 0.5rem 0; - a { - float: right; - color: $red; - transition: all $transition; - } - } - } + a { + float: right; + color: $red; + transition: all $transition; + } + } + } - .tag { - margin-right: 0.5rem; - margin-bottom: 0.5rem; + .tag { + margin-right: 0.5rem; + margin-bottom: 0.5rem; - &:last-child { - margin-right: 0; - } - } + &:last-child { + margin-right: 0; + } + } } .show-tasks { - h3 { - text-align: left; + h3 { + text-align: left; - &.nothing { - text-align: center; - margin-top: 3rem; - } + &.nothing { + text-align: center; + margin-top: 3rem; + } - .input { - width: 190px; - vertical-align: middle; - margin: .5rem 0; - } - } + .input { + width: 190px; + vertical-align: middle; + margin: .5rem 0; + } + } - img { - margin-top: 2rem; - } + img { + margin-top: 2rem; + } - .user img{ - margin: 0; - } + .user img { + margin: 0; + } - .spinner.is-loading:after { - margin-left: calc(40% - 1rem); - } + .spinner.is-loading:after { + margin-left: calc(40% - 1rem); + } } .defer-task { - $defer-task-max-width: 350px; + $defer-task-max-width: 350px; - position: absolute; - width: 100%; - max-width: $defer-task-max-width; - border-radius: $radius; - border: 1px solid $grey-200; - padding: 1rem; - margin: 1rem; - background: $white; - color: $text; - cursor: default; - z-index: 10; - box-shadow: $shadow-lg; + position: absolute; + width: 100%; + max-width: $defer-task-max-width; + border-radius: $radius; + border: 1px solid $grey-200; + padding: 1rem; + margin: 1rem; + background: $white; + color: $text; + cursor: default; + z-index: 10; + box-shadow: $shadow-lg; - input.input { - display: none; - } + input.input { + display: none; + } - .flatpickr-calendar { - margin: 0 auto; - box-shadow: none; + .flatpickr-calendar { + margin: 0 auto; + box-shadow: none; - span { - width: auto !important; - } - } + span { + width: auto !important; + } + } - .defer-days { - justify-content: space-between; - display: flex; - margin: .5rem 0; - } + .defer-days { + justify-content: space-between; + display: flex; + margin: .5rem 0; + } - @media screen and (max-width: ($defer-task-max-width + 100px)) { // 100px is roughly the size the pane is pulled to the right - left: .5rem; - right: .5rem; - max-width: 100%; - width: calc(100vw - 1rem - 2rem); + @media screen and (max-width: ($defer-task-max-width + 100px)) { // 100px is roughly the size the pane is pulled to the right + left: .5rem; + right: .5rem; + max-width: 100%; + width: calc(100vw - 1rem - 2rem); - .flatpickr-calendar { - max-width: 100%; + .flatpickr-calendar { + max-width: 100%; - .flatpickr-innerContainer { - overflow: scroll; - } - } - } + .flatpickr-innerContainer { + overflow: scroll; + } + } + } } .is-max-width-desktop .tasks .task { - max-width: $desktop; + max-width: $desktop; } .tasktext { - :focus { - box-shadow: inset 0 0 0 2px rgba($primary, 0.5); - } + :focus { + box-shadow: inset 0 0 0 2px rgba($primary, 0.5); + } - :focus:not(:focus-visible) { - outline: 0; - } + :focus:not(:focus-visible) { + outline: 0; + } - :focus-visible, :-moz-focusring { - box-shadow: inset 0 0 0 2px rgba($primary, 0.5); - } + :focus-visible, :-moz-focusring { + box-shadow: inset 0 0 0 2px rgba($primary, 0.5); + } } -- 2.40.1 From 21a210b28509b870dfc541ef1c6e0245feb2c269 Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 27 Jul 2021 18:24:35 +0200 Subject: [PATCH 04/40] Add reordering animation --- src/styles/components/tasks.scss | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/styles/components/tasks.scss b/src/styles/components/tasks.scss index c3df77118..d5cfca69f 100644 --- a/src/styles/components/tasks.scss +++ b/src/styles/components/tasks.scss @@ -3,6 +3,24 @@ .tasks { width: 100%; + + .flip-list-move { + transition: transform $transition-duration; + } + + .no-move { + transition: transform 0; + } + + .ghost { + border-radius: $radius; + background: $grey-100; + border: 3px dashed $grey-300; + + * { + opacity: 0; + } + } } .taskedit { @@ -32,12 +50,13 @@ .task { display: flex; flex-wrap: wrap; - padding: 0.5rem; + padding: .3rem; transition: background-color $transition; align-items: center; cursor: pointer; margin: 0 .5rem; border-radius: $radius; + border: 3px solid transparent; &:first-child { margin-top: .5rem; -- 2.40.1 From d17e509844cf56fef5fd2b114a45956f88e4c9b0 Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 27 Jul 2021 18:24:38 +0200 Subject: [PATCH 05/40] Add reordering animation --- src/components/tasks/mixins/taskList.js | 3 +- src/views/list/views/List.vue | 54 +++++++++++++++---------- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/src/components/tasks/mixins/taskList.js b/src/components/tasks/mixins/taskList.js index 64c425b66..a0b5bedf1 100644 --- a/src/components/tasks/mixins/taskList.js +++ b/src/components/tasks/mixins/taskList.js @@ -189,8 +189,7 @@ export default { } }, saveTaskPosition(e) { - console.log(e.oldIndex, e.newIndex) - console.log('elem', this.tasks[e.newIndex].title) + this.drag = false const task = this.tasks[e.newIndex] const taskBefore = this.tasks[e.newIndex - 1] ?? null diff --git a/src/views/list/views/List.vue b/src/views/list/views/List.vue index 47d75b7e4..4308412d9 100644 --- a/src/views/list/views/List.vue +++ b/src/views/list/views/List.vue @@ -80,24 +80,32 @@ class="tasks mt-0" v-if="tasks && tasks.length > 0" > - - -
+ + - -
-
+
+ +
+ +
state.currentList.maxRight > Rights.READ, -- 2.40.1 From 7ab100e29ff1e5872fdeddec4341425f68470cec Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 27 Jul 2021 18:27:32 +0200 Subject: [PATCH 06/40] Tune animation duration --- src/views/list/views/List.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/list/views/List.vue b/src/views/list/views/List.vue index 4308412d9..5d6da08fb 100644 --- a/src/views/list/views/List.vue +++ b/src/views/list/views/List.vue @@ -194,7 +194,7 @@ export default { drag: false, dragOptions: { - animation: 150, + animation: 100, ghostClass: 'ghost', }, } -- 2.40.1 From 3382569d2b65b9fb787645bea681796476e8862f Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 27 Jul 2021 18:27:41 +0200 Subject: [PATCH 07/40] Fix sorting --- src/components/tasks/mixins/taskList.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/tasks/mixins/taskList.js b/src/components/tasks/mixins/taskList.js index a0b5bedf1..b711c43f9 100644 --- a/src/components/tasks/mixins/taskList.js +++ b/src/components/tasks/mixins/taskList.js @@ -149,9 +149,9 @@ export default { if (a.done > b.done) return 1 - if (a.id > b.id) + if (a.position < b.position) return -1 - if (a.id < b.id) + if (a.position > b.position) return 1 return 0 }) -- 2.40.1 From 4e18692d49e6119154360f570703f330e18b608a Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 27 Jul 2021 20:10:52 +0200 Subject: [PATCH 08/40] Add handle to drag tasks --- src/main.ts | 2 ++ src/styles/components/tasks.scss | 13 ++++++++++++- src/views/list/views/List.vue | 4 ++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index bfd40b709..2303d4284 100644 --- a/src/main.ts +++ b/src/main.ts @@ -72,6 +72,7 @@ import { faShareAlt, faImage, faBell, + faGripLines, } from '@fortawesome/free-solid-svg-icons' import { faCalendarAlt, @@ -179,6 +180,7 @@ library.add(faShareAlt) library.add(faImage) library.add(faBell) library.add(faBellSlash) +library.add(faGripLines) Vue.component('icon', FontAwesomeIcon) diff --git a/src/styles/components/tasks.scss b/src/styles/components/tasks.scss index d5cfca69f..9456b01d4 100644 --- a/src/styles/components/tasks.scss +++ b/src/styles/components/tasks.scss @@ -16,7 +16,7 @@ border-radius: $radius; background: $grey-100; border: 3px dashed $grey-300; - + * { opacity: 0; } @@ -149,6 +149,17 @@ opacity: 1; } + .handle { + opacity: 0; + transition: opacity $transition; + margin-right: .25rem; + cursor: grab; + } + + &:hover .handle { + opacity: 1; + } + .fancycheckbox { height: 18px; padding-top: 0; diff --git a/src/views/list/views/List.vue b/src/views/list/views/List.vue index 5d6da08fb..7c92c2215 100644 --- a/src/views/list/views/List.vue +++ b/src/views/list/views/List.vue @@ -86,6 +86,7 @@ @start="() => drag = true" @end="saveTaskPosition" v-bind="dragOptions" + handle=".handle" > + + +
Date: Tue, 27 Jul 2021 21:29:02 +0200 Subject: [PATCH 09/40] Format --- src/styles/theme/navigation.scss | 693 +++++++++++++++---------------- 1 file changed, 346 insertions(+), 347 deletions(-) diff --git a/src/styles/theme/navigation.scss b/src/styles/theme/navigation.scss index ff8304086..4c427524f 100644 --- a/src/styles/theme/navigation.scss +++ b/src/styles/theme/navigation.scss @@ -1,397 +1,396 @@ @use "sass:math"; .navbar { - z-index: 4 !important; + z-index: 4 !important; - .navbar-dropdown { - box-shadow: $navbar-dropdown-boxed-shadow; - top: 101%; - } + .navbar-dropdown { + box-shadow: $navbar-dropdown-boxed-shadow; + top: 101%; + } - .navbar-brand { - display: flex; - align-items: center; + .navbar-brand { + display: flex; + align-items: center; - .logo img { - width: $vikunja-nav-logo-full-width; - } - } + .logo img { + width: $vikunja-nav-logo-full-width; + } + } } .navbar.main-theme { - background: $light-background; - z-index: 5 !important; - justify-content: space-between; - align-items: center; + background: $light-background; + z-index: 5 !important; + justify-content: space-between; + align-items: center; - .title { - margin: 0; - font-size: 1.75rem; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - } + .title { + margin: 0; + font-size: 1.75rem; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } - .navbar-end { - margin-left: 0; - align-items: center; - display: flex; - } + .navbar-end { + margin-left: 0; + align-items: center; + display: flex; + } - @media screen and (max-width: $desktop) { - display: flex; - justify-content: space-between; + @media screen and (max-width: $desktop) { + display: flex; + justify-content: space-between; - @media screen and (max-width: $tablet) { - .navbar-brand { - display: none; - } + @media screen and (max-width: $tablet) { + .navbar-brand { + display: none; + } - .user { - width: $user-dropdown-width-mobile; - display: flex; - align-items: center; + .user { + width: $user-dropdown-width-mobile; + display: flex; + align-items: center; - .dropdown-trigger { - line-height: 1; + .dropdown-trigger { + line-height: 1; - .button { - padding: 0 0.25rem; - height: 1rem; + .button { + padding: 0 0.25rem; + height: 1rem; - .icon { - width: .5rem; - } - } - } + .icon { + width: .5rem; + } + } + } - .username { - display: none; - } - } - } - } + .username { + display: none; + } + } + } + } } .navbar-menu .navbar-item .icon { - margin: 0 0.5rem; + margin: 0 0.5rem; } .app-container { - .namespace-container { - background: $vikunja-nav-background; - z-index: 6; - color: $vikunja-nav-color; - padding: 0; - transition: all $transition; - position: fixed; - bottom: 0; - top: $navbar-height; - overflow-x: auto; - width: $navbar-width; + .namespace-container { + background: $vikunja-nav-background; + z-index: 6; + color: $vikunja-nav-color; + padding: 0; + transition: all $transition; + position: fixed; + bottom: 0; + top: $navbar-height; + overflow-x: auto; + width: $navbar-width; - padding: 0 0 1rem; - left: -147vw; - bottom: 0; + padding: 0 0 1rem; + left: -147vw; + bottom: 0; - @media screen and (max-width: $tablet) { - top: 0; - width: 70vw; - } + @media screen and (max-width: $tablet) { + top: 0; + width: 70vw; + } - &.is-active { - left: 0; - } + &.is-active { + left: 0; + } - .menu { - .menu-label { - font-size: 1rem; - font-weight: 700; - font-weight: bold; - font-family: $vikunja-font; - color: $vikunja-nav-color; - font-weight: 500; - min-height: 2.5rem; - padding-top: 0; - padding-left: $navbar-padding; + .menu { + .menu-label { + font-size: 1rem; + font-weight: 700; + font-weight: bold; + font-family: $vikunja-font; + color: $vikunja-nav-color; + font-weight: 500; + min-height: 2.5rem; + padding-top: 0; + padding-left: $navbar-padding; - overflow: hidden; - } + overflow: hidden; + } - .menu-label, .menu-list span.list-menu-link, .menu-list a { - display: flex; - align-items: center; - justify-content: space-between; - cursor: pointer; + .menu-label, .menu-list span.list-menu-link, .menu-list a { + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; - .list-menu-title { - overflow: hidden; - text-overflow: ellipsis; - width: 100%; - } + .list-menu-title { + overflow: hidden; + text-overflow: ellipsis; + width: 100%; + } - .color-bubble { - height: 12px; - flex: 0 0 12px; - } + .color-bubble { + height: 12px; + flex: 0 0 12px; + } - .favorite { - margin-left: .25rem; - transition: opacity $transition, color $transition; - opacity: 0; + .favorite { + margin-left: .25rem; + transition: opacity $transition, color $transition; + opacity: 0; - &:hover { - color: $orange; - } + &:hover { + color: $orange; + } - &.is-favorite { - opacity: 1; - color: $orange; - } - } + &.is-favorite { + opacity: 1; + color: $orange; + } + } - &:hover .favorite { - opacity: 1; - } - } + &:hover .favorite { + opacity: 1; + } + } - .menu-label { - .color-bubble { - width: 14px !important; - height: 14px !important; - } + .menu-label { + .color-bubble { + width: 14px !important; + height: 14px !important; + } - .is-archived { - min-width: 85px; - } - } + .is-archived { + min-width: 85px; + } + } - .namespace-title { - display: flex; - align-items: center; - justify-content: space-between; + .namespace-title { + display: flex; + align-items: center; + justify-content: space-between; - .menu-label { - margin-bottom: 0; - flex: 1 1 auto; + .menu-label { + margin-bottom: 0; + flex: 1 1 auto; - .name { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - } + .name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } - a:not(.dropdown-item) { - color: $vikunja-nav-color; - padding: 0 .25rem; - } + a:not(.dropdown-item) { + color: $vikunja-nav-color; + padding: 0 .25rem; + } - .dropdown-trigger { - padding: .5rem; - cursor: pointer; - } + .dropdown-trigger { + padding: .5rem; + cursor: pointer; + } - .toggle-lists-icon { - svg { - transition: all $transition; - transform: rotate(90deg); - opacity: 1; - } + .toggle-lists-icon { + svg { + transition: all $transition; + transform: rotate(90deg); + opacity: 1; + } - &.active svg { - transform: rotate(0deg); - opacity: 0; - } - } + &.active svg { + transform: rotate(0deg); + opacity: 0; + } + } - &:hover .toggle-lists-icon svg { - opacity: 1; - } + &:hover .toggle-lists-icon svg { + opacity: 1; + } - &:not(.has-menu) .toggle-lists-icon { - padding-right: 1rem; - } - } + &:not(.has-menu) .toggle-lists-icon { + padding-right: 1rem; + } + } - .menu-label, .nsettings, .menu-list span.list-menu-link, .menu-list a { - color: $vikunja-nav-color; - } + .menu-label, .nsettings, .menu-list span.list-menu-link, .menu-list a { + color: $vikunja-nav-color; + } - .menu-list { - li { - height: 44px; - display: flex; - align-items: center; + .menu-list { + li { + height: 44px; + display: flex; + align-items: center; - .dropdown-trigger { - opacity: 0; - padding: .5rem; - cursor: pointer; - transition: $transition; - } + .dropdown-trigger { + opacity: 0; + padding: .5rem; + cursor: pointer; + transition: $transition; + } - &:hover { - background: $white; + &:hover { + background: $white; - .dropdown-trigger { - opacity: 1; - } - } - } + .dropdown-trigger { + opacity: 1; + } + } + } - a:hover { - background: transparent; - } + a:hover { + background: transparent; + } - span.list-menu-link, li > a { - padding: 0.75rem .5rem 0.75rem $navbar-padding * 1.5; - transition: all 0.2s ease; + span.list-menu-link, li > a { + padding: 0.75rem .5rem 0.75rem $navbar-padding * 1.5; + transition: all 0.2s ease; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - width: 100%; - border-left: $vikunja-nav-selected-width solid transparent; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + width: 100%; + border-left: $vikunja-nav-selected-width solid transparent; - .icon { - height: 1rem; - vertical-align: middle; - padding-bottom: 4px; - padding-right: 0.5rem; - } + .icon { + height: 1rem; + vertical-align: middle; + padding-right: 0.5rem; + } - &.router-link-exact-active { - color: $primary; - border-left: $vikunja-nav-selected-width solid $primary; + &.router-link-exact-active { + color: $primary; + border-left: $vikunja-nav-selected-width solid $primary; - .icon { - color: $primary; - } - } + .icon { + color: $primary; + } + } - &:hover { - border-left: $vikunja-nav-selected-width solid $primary; - } - } - } + &:hover { + border-left: $vikunja-nav-selected-width solid $primary; + } + } + } - .logo { - display: none; + .logo { + display: none; - @media screen and (max-width: $tablet) { - display: block; - } - } + @media screen and (max-width: $tablet) { + display: block; + } + } - &.namespaces-lists { - padding-top: math.div($navbar-padding, 2); - } + &.namespaces-lists { + padding-top: math.div($navbar-padding, 2); + } - &.loader-container.is-loading:after { - width: 1.5rem; - height: 1.5rem; - top: calc(50% - .75rem); - left: calc(50% - .75rem); - border-width: 2px; - } + &.loader-container.is-loading:after { + width: 1.5rem; + height: 1.5rem; + top: calc(50% - .75rem); + left: calc(50% - .75rem); + border-width: 2px; + } - .icon { - color: $grey-400 !important; - } - } + .icon { + color: $grey-400 !important; + } + } - .top-menu { - margin-top: math.div($navbar-padding, 2); + .top-menu { + margin-top: math.div($navbar-padding, 2); - .menu-list { - li { - font-weight: 500; - font-family: $vikunja-font; - } + .menu-list { + li { + font-weight: 500; + font-family: $vikunja-font; + } - span.list-menu-link, li > a { - padding-left: 2rem; - display: inline-block; - } - } + span.list-menu-link, li > a { + padding-left: 2rem; + display: inline-block; + } + } - } - } + } + } } .navbar { - .trigger-button { - cursor: pointer; - color: $grey-400; - padding: .5rem; - font-size: 1.25rem; - position: relative; - } + .trigger-button { + cursor: pointer; + color: $grey-400; + padding: .5rem; + font-size: 1.25rem; + position: relative; + } - > * > .trigger-button { - width: $navbar-icon-width; - } + > * > .trigger-button { + width: $navbar-icon-width; + } - .user { - display: flex; - align-items: center; + .user { + display: flex; + align-items: center; - span { - font-family: $vikunja-font; - } + span { + font-family: $vikunja-font; + } - .avatar { - -webkit-border-radius: 100%; - -moz-border-radius: 100%; - border-radius: 100%; - vertical-align: middle; - height: 40px; - } + .avatar { + -webkit-border-radius: 100%; + -moz-border-radius: 100%; + border-radius: 100%; + vertical-align: middle; + height: 40px; + } - .logout-icon { - color: $grey-900; + .logout-icon { + color: $grey-900; - .icon { - vertical-align: middle; - } - } + .icon { + vertical-align: middle; + } + } - .dropdown-trigger .button { - background: none; + .dropdown-trigger .button { + background: none; - &:focus:not(:active), &:active { - outline: none !important; - -webkit-box-shadow: none !important; - -moz-box-shadow: none !important; - box-shadow: none !important; - } - } - } + &:focus:not(:active), &:active { + outline: none !important; + -webkit-box-shadow: none !important; + -moz-box-shadow: none !important; + box-shadow: none !important; + } + } + } } .menu-hide-button, .menu-show-button { - display: none; - z-index: 31; - font-weight: bold; - font-size: 2rem; - color: $grey-400; - line-height: 1; - transition: all $transition; + display: none; + z-index: 31; + font-weight: bold; + font-size: 2rem; + color: $grey-400; + line-height: 1; + transition: all $transition; - &:hover, &:focus { - height: 1rem; - color: $grey-600; - } + &:hover, &:focus { + height: 1rem; + color: $grey-600; + } } .menu-show-button { height: .75rem; width: 2rem; - + &:before, &:after { display: block; content: ''; @@ -399,18 +398,18 @@ border-radius: $radius; transition: all $transition; } - + &:before { margin-bottom: .5rem; } - + &:after { margin-top: .5rem; } &:hover, &:focus { color: $grey-600; - + &:before { margin-bottom: .75rem; } @@ -422,61 +421,61 @@ } .menu-hide-button { - position: fixed; + position: fixed; - &:hover, &:focus { - color: $text; - } + &:hover, &:focus { + color: $text; + } } .navbar-brand .menu-show-button { - display: block; + display: block; } .mobile-overlay { - display: none; - position: fixed; - top: 0; - bottom: 0; - left: 0; - right: 0; - background: rgba(250, 250, 250, 0.8); - z-index: 5; - opacity: 0; - transition: all $transition; + display: none; + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + background: rgba(250, 250, 250, 0.8); + z-index: 5; + opacity: 0; + transition: all $transition; } @media screen and (max-width: $tablet) { - .menu-hide-button { - display: block; - top: $hamburger-menu-icon-spacing; - right: $hamburger-menu-icon-spacing; - } + .menu-hide-button { + display: block; + top: $hamburger-menu-icon-spacing; + right: $hamburger-menu-icon-spacing; + } - .menu-show-button { - display: block; - margin-left: $hamburger-menu-icon-spacing; - } + .menu-show-button { + display: block; + margin-left: $hamburger-menu-icon-spacing; + } - .mobile-overlay { - display: block; - opacity: 1; - } + .mobile-overlay { + display: block; + opacity: 1; + } - .navbar.is-dark .navbar-brand > .navbar-item { - margin: 0 auto; - } + .navbar.is-dark .navbar-brand > .navbar-item { + margin: 0 auto; + } } .logout-icon { - margin-right: 0.85rem !important; + margin-right: 0.85rem !important; } .menu-bottom-link { - width: 100%; - color: $grey-300; - text-align: center; - display: block; - margin: 1rem 0; - font-size: .8rem; + width: 100%; + color: $grey-300; + text-align: center; + display: block; + margin: 1rem 0; + font-size: .8rem; } -- 2.40.1 From cd8f77765c3ce82aaeb6ab2946946613d2bffd33 Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 27 Jul 2021 22:15:01 +0200 Subject: [PATCH 10/40] Add updating the list position through the menu --- src/components/home/navigation.vue | 88 +++++++++++++------ src/components/tasks/mixins/taskList.js | 4 +- ...skPosition.ts => calculateItemPosition.ts} | 4 +- src/helpers/calculateTaskPosition.test.ts | 10 +-- src/styles/theme/navigation.scss | 18 +++- src/views/list/views/Kanban.vue | 4 +- src/views/list/views/List.vue | 4 +- src/views/namespaces/ListNamespaces.vue | 4 + 8 files changed, 95 insertions(+), 41 deletions(-) rename src/helpers/{calculateTaskPosition.ts => calculateItemPosition.ts} (86%) diff --git a/src/components/home/navigation.vue b/src/components/home/navigation.vue index 454eb658e..f49f1db31 100644 --- a/src/components/home/navigation.vue +++ b/src/components/home/navigation.vue @@ -49,7 +49,7 @@