Merge remote-tracking branch 'upstream/main'

This commit is contained in:
Adrian Simmons 2021-09-16 13:25:25 +01:00
commit 5ac95e914b
40 changed files with 402 additions and 1010 deletions

View File

@ -35,7 +35,7 @@
"vue-shortkey": "3.1.7",
"vuedraggable": "2.24.3",
"vuex": "3.6.2",
"workbox-precaching": "6.2.4"
"workbox-precaching": "6.3.0"
},
"devDependencies": {
"@4tw/cypress-drag-drop": "2.0.0",
@ -53,7 +53,7 @@
"babel-eslint": "10.1.0",
"cypress": "8.3.1",
"cypress-file-upload": "5.0.8",
"esbuild": "0.12.25",
"esbuild": "0.12.26",
"eslint": "7.32.0",
"eslint-plugin-vue": "7.17.0",
"express": "4.17.1",
@ -61,7 +61,7 @@
"jest": "27.1.1",
"rollup-plugin-terser": "7.0.2",
"rollup-plugin-visualizer": "5.5.2",
"sass": "1.39.0",
"sass": "1.39.2",
"ts-jest": "27.0.5",
"typescript": "4.4.2",
"vite": "2.5.6",
@ -72,7 +72,7 @@
"vue-router": "3.5.2",
"vue-template-compiler": "2.6.14",
"wait-on": "6.0.0",
"workbox-cli": "6.2.4"
"workbox-cli": "6.3.0"
},
"eslintConfig": {
"root": true,

View File

@ -23,11 +23,9 @@
</template>
<script>
import {mapState} from 'vuex'
import {mapState, mapGetters} from 'vuex'
import isTouchDevice from 'is-touch-device'
import authTypes from './models/authTypes'
import Notification from './components/misc/notification'
import {KEYBOARD_SHORTCUTS_ACTIVE, ONLINE} from './store/mutation-types'
import KeyboardShortcuts from './components/misc/keyboard-shortcuts'
@ -74,11 +72,13 @@ export default {
return isTouchDevice()
},
...mapState({
authUser: state => state.auth.authenticated && (state.auth.info && state.auth.info.type === authTypes.USER),
authLinkShare: state => state.auth.authenticated && (state.auth.info && state.auth.info.type === authTypes.LINK_SHARE),
online: ONLINE,
keyboardShortcutsActive: KEYBOARD_SHORTCUTS_ACTIVE,
}),
...mapGetters('auth', [
'authUser',
'authLinkShare',
]),
},
methods: {
setupOnlineStatus() {

View File

@ -97,7 +97,7 @@
<script>
import {mapState} from 'vuex'
import {CURRENT_LIST, QUICK_ACTIONS_ACTIVE} from '@/store/mutation-types'
import Rights from '@/models/rights.json'
import Rights from '@/models/constants/rights.json'
import Update from '@/components/home/update.vue'
import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue'
import Dropdown from '@/components/misc/dropdown.vue'

View File

@ -4,7 +4,7 @@
:checked="checked"
:disabled="disabled"
:id="checkBoxId"
@change="updateData"
@change="(event) => updateData(event.target.checked)"
style="display: none;"
type="checkbox"/>
<label :for="checkBoxId" class="check">
@ -51,10 +51,10 @@ export default {
this.checkBoxId = 'fancycheckbox' + Math.random()
},
methods: {
updateData(e) {
this.checked = e.target.checked
this.$emit('input', this.checked)
this.$emit('change', e.target.checked)
updateData(checked) {
this.checked = checked
this.$emit('input', checked)
this.$emit('change', checked)
},
},
}

View File

@ -190,6 +190,35 @@ import ListService from '@/services/list'
import NamespaceService from '@/services/namespace'
import EditLabels from '@/components/tasks/partials/editLabels.vue'
// FIXME: merge with DEFAULT_PARAMS in taskList.js
const DEFAULT_PARAMS = {
sort_by: [],
order_by: [],
filter_by: [],
filter_value: [],
filter_comparator: [],
filter_include_nulls: true,
filter_concat: 'or',
s: '',
}
const DEFAULT_FILTERS = {
done: false,
dueDate: '',
requireAllFilters: false,
priority: 0,
usePriority: false,
startDate: '',
endDate: '',
percentDone: 0,
usePercentDone: false,
reminders: '',
assignees: '',
labels: '',
list_id: '',
namespace: '',
}
export default {
name: 'filters',
components: {
@ -202,32 +231,8 @@ export default {
},
data() {
return {
params: {
sort_by: [],
order_by: [],
filter_by: [],
filter_value: [],
filter_comparator: [],
filter_include_nulls: true,
filter_concat: 'or',
s: '',
},
filters: {
done: false,
dueDate: '',
requireAllFilters: false,
priority: 0,
usePriority: false,
startDate: '',
endDate: '',
percentDone: 0,
usePercentDone: false,
reminders: '',
assignees: '',
labels: '',
list_id: '',
namespace: '',
},
params: DEFAULT_PARAMS,
filters: DEFAULT_FILTERS,
usersService: UserService,
foundusers: [],

View File

@ -50,7 +50,7 @@
<script>
import NotificationService from '@/services/notification'
import User from '@/components/misc/user.vue'
import names from '@/models/notificationNames.json'
import names from '@/models/constants/notificationNames.json'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
import {mapState} from 'vuex'

View File

@ -173,7 +173,7 @@
</template>
<script>
import rights from '../../models/rights'
import rights from '../../models/constants/rights'
import LinkShareService from '../../services/linkShare'
import LinkShareModel from '../../models/linkShare'

View File

@ -145,7 +145,7 @@ import TeamListService from '../../services/teamList'
import TeamService from '../../services/team'
import TeamModel from '../../models/team'
import rights from '../../models/rights'
import rights from '../../models/constants/rights.json'
import Multiselect from '@/components/input/multiselect.vue'
import Nothing from '@/components/misc/nothing.vue'

View File

@ -72,7 +72,7 @@
import ListService from '../../services/list'
import TaskService from '../../services/task'
import TaskModel from '../../models/task'
import priorities from '../../models/priorities'
import priorities from '../../models/constants/priorities'
import EditLabels from './partials/editLabels'
import Reminders from './partials/reminders'
import ColorPicker from '../input/colorPicker'

View File

@ -11,7 +11,7 @@
</x-button>
</div>
<filter-popup
@change="loadTasks"
@change="loadTasks()"
:visible="showTaskFilter"
v-model="params"
/>
@ -24,10 +24,7 @@
class="month"
v-for="(m, mk) in days[yk]"
>
{{
new Date(new Date(`${yk}-${parseInt(mk) + 1}-01`)).toLocaleString('en-us', {month: 'long'})
}},
{{ new Date(yk).getFullYear() }}
{{ formatYear(new Date(`${yk}-${parseInt(mk) + 1}-01`)) }}
<div class="days">
<div
:class="{ today: d.toDateString() === now.toDateString() }"
@ -191,12 +188,13 @@ import EditTask from './edit-task'
import TaskService from '../../services/task'
import TaskModel from '../../models/task'
import priorities from '../../models/priorities'
import priorities from '../../models/constants/priorities'
import PriorityLabel from './partials/priorityLabel'
import TaskCollectionService from '../../services/taskCollection'
import {mapState} from 'vuex'
import Rights from '../../models/rights.json'
import Rights from '../../models/constants/rights.json'
import FilterPopup from '@/components/list/partials/filter-popup.vue'
import {format} from 'date-fns'
export default {
name: 'GanttChart',
@ -481,6 +479,9 @@ export default {
this.error(e)
})
},
formatYear(date) {
return format(date, 'MMMM, yyyy')
},
},
}
</script>

View File

@ -1,17 +1,70 @@
import TaskCollectionService from '../../../services/taskCollection'
import TaskCollectionService from '@/services/taskCollection'
import cloneDeep from 'lodash/cloneDeep'
import {calculateItemPosition} from '../../../helpers/calculateItemPosition'
// FIXME: merge with DEFAULT_PARAMS in filters.vue
const DEFAULT_PARAMS = {
sort_by: ['position', 'id'],
order_by: ['asc', 'desc'],
filter_by: ['done'],
filter_value: ['false'],
filter_comparator: ['equals'],
filter_concat: 'and',
}
function createPagination(totalPages, currentPage) {
const pages = []
for (let i = 0; i < totalPages; i++) {
// Show ellipsis instead of all pages
if (
i > 0 && // Always at least the first page
(i + 1) < totalPages && // And the last page
(
// And the current with current + 1 and current - 1
(i + 1) > currentPage + 1 ||
(i + 1) < currentPage - 1
)
) {
// Only add an ellipsis if the last page isn't already one
if (pages[i - 1] && !pages[i - 1].isEllipsis) {
pages.push({
number: 0,
isEllipsis: true,
})
}
continue
}
pages.push({
number: i + 1,
isEllipsis: false,
})
}
return pages
}
export function getRouteForPagination(page = 1, type = 'list') {
return {
name: 'list.' + type,
params: {
type: type,
},
query: {
page: page,
},
}
}
/**
* This mixin provides a base set of methods and properties to get tasks on a list.
*/
export default {
data() {
return {
taskCollectionService: TaskCollectionService,
taskCollectionService: new TaskCollectionService(),
tasks: [],
pages: [],
currentPage: 0,
loadedList: null,
@ -20,27 +73,21 @@ export default {
searchTerm: '',
showTaskFilter: false,
params: {
sort_by: ['position', 'id'],
order_by: ['asc', 'desc'],
filter_by: ['done'],
filter_value: ['false'],
filter_comparator: ['equals'],
filter_concat: 'and',
},
params: DEFAULT_PARAMS,
}
},
watch: {
'$route.query': 'loadTasksForPage', // Only listen for query path changes
// Only listen for query path changes
'$route.query': {
handler: 'loadTasksForPage',
immediate: true,
},
'$route.path': 'loadTasksOnSavedFilter',
},
beforeMount() {
// Triggering loading the tasks in beforeMount lets the component maintain the current page, therefore the page
// is not lost after navigating back from a task detail page for example.
this.loadTasksForPage(this.$route.query)
},
created() {
this.taskCollectionService = new TaskCollectionService()
computed: {
pages() {
return createPagination(this.taskCollectionService.totalPages, this.currentPage)
},
},
methods: {
loadTasks(
@ -80,48 +127,20 @@ export default {
return
}
this.$set(this, 'tasks', [])
this.tasks = []
this.taskCollectionService.getAll(list, params, page)
.then(r => {
this.$set(this, 'tasks', r)
this.$set(this, 'pages', [])
this.tasks = r
this.currentPage = page
for (let i = 0; i < this.taskCollectionService.totalPages; i++) {
// Show ellipsis instead of all pages
if (
i > 0 && // Always at least the first page
(i + 1) < this.taskCollectionService.totalPages && // And the last page
(
// And the current with current + 1 and current - 1
(i + 1) > this.currentPage + 1 ||
(i + 1) < this.currentPage - 1
)
) {
// Only add an ellipsis if the last page isn't already one
if (this.pages[i - 1] && !this.pages[i - 1].isEllipsis) {
this.pages.push({
number: 0,
isEllipsis: true,
})
}
continue
}
this.pages.push({
number: i + 1,
isEllipsis: false,
})
}
this.loadedList = cloneDeep(currentList)
})
.catch(e => {
this.error(e)
})
},
loadTasksForPage(e) {
// The page parameter can be undefined, in the case where the user loads a new list from the side bar menu
let page = Number(e.page)
@ -177,17 +196,6 @@ export default {
this.showTaskSearch = false
}, 200)
},
getRouteForPagination(page = 1, type = 'list') {
return {
name: 'list.' + type,
params: {
type: type,
},
query: {
page: page,
},
}
},
saveTaskPosition(e) {
this.drag = false
@ -205,5 +213,6 @@ export default {
this.error(e)
})
},
getRouteForPagination,
},
}

View File

@ -7,16 +7,17 @@
<h1
class="title input"
:class="{'disabled': !canWrite}"
@focusout="save()"
@keydown.enter.prevent.stop="save()"
@blur="save($event.target.textContent)"
@keydown.enter.prevent.stop="$event.target.blur()"
:contenteditable="canWrite ? 'true' : 'false'"
spellcheck="false"
ref="taskTitle">{{ task.title.trim() }}</h1>
<transition name="fade">
<span class="is-inline-flex is-align-items-center" v-if="loading && saving">
<span class="loader is-inline-block mr-2"></span>
{{ $t('misc.saving') }}
</span>
<span class="has-text-success is-inline-flex is-align-content-center" v-if="!loading && saved">
<span class="has-text-success is-inline-flex is-align-content-center" v-if="!loading && showSavedMessage">
<icon icon="check" class="mr-2"/>
{{ $t('misc.saved') }}
</span>
@ -25,22 +26,22 @@
</template>
<script>
import {LOADING} from '@/store/mutation-types'
import {mapState} from 'vuex'
export default {
name: 'heading',
data() {
return {
task: {title: '', identifier: '', index: ''},
taskTitle: '',
saved: false,
showSavedMessage: false,
saving: false, // Since loading is global state, this variable ensures we're only showing the saving icon when saving the description.
}
},
computed: mapState({
loading: LOADING,
}),
computed: {
...mapState(['loading']),
task() {
return this.value
},
},
props: {
value: {
required: true,
@ -50,50 +51,29 @@ export default {
default: false,
},
},
watch: {
value(newVal) {
this.task = newVal
this.taskTitle = this.task.title
},
},
mounted() {
this.task = this.value
this.taskTitle = this.task.title
},
methods: {
save() {
this.$refs.taskTitle.spellcheck = false
// Pull the task title from the contenteditable
let taskTitle = this.$refs.taskTitle.textContent
this.task.title = taskTitle
// We only want to save if the title was actually change.
// Because the contenteditable does not have a change event,
// we're building it ourselves and only calling saveTask()
save(title) {
// We only want to save if the title was actually changed.
// Because the contenteditable does not have a change event
// we're building it ourselves and only continue
// if the task title changed.
if (this.task.title !== this.taskTitle) {
this.$refs.taskTitle.blur()
this.saveTask()
this.taskTitle = taskTitle
}
},
saveTask() {
// When only saving with enter, the focusout event is called as well. This then leads to the saveTask
// method being called twice, overriding some task attributes in the second run.
// If we simply check if we're already in the process of saving, we can prevent that.
if (this.saving) {
if (title === this.task.title) {
return
}
this.saving = true
this.$store.dispatch('tasks/update', this.task)
.then(() => {
this.$emit('input', this.task)
this.saved = true
const newTask = {
...this.task,
title,
}
this.$store.dispatch('tasks/update', newTask)
.then((task) => {
this.$emit('input', task)
this.showSavedMessage = true
setTimeout(() => {
this.saved = false
this.showSavedMessage = false
}, 2000)
})
.catch(e => {

View File

@ -21,7 +21,7 @@
</template>
<script>
import priorites from '../../../models/priorities'
import priorites from '../../../models/constants/priorities'
export default {
name: 'priorityLabel',

View File

@ -12,7 +12,7 @@
</template>
<script>
import priorites from '../../../models/priorities'
import priorites from '../../../models/constants/priorities'
export default {
name: 'prioritySelect',

View File

@ -122,7 +122,7 @@
import TaskService from '../../../services/task'
import TaskModel from '../../../models/task'
import TaskRelationService from '../../../services/taskRelation'
import relationKinds from '../../../models/relationKinds'
import relationKinds from '../../../models/constants/relationKinds'
import TaskRelationModel from '../../../models/taskRelation'
import Multiselect from '@/components/input/multiselect.vue'

View File

@ -51,7 +51,7 @@
</template>
<script>
import repeatModes from '@/models/taskRepeatModes'
import repeatModes from '@/models/constants/taskRepeatModes'
export default {
name: 'repeatAfter',
@ -62,7 +62,7 @@ export default {
amount: 0,
type: '',
},
repeatModes: repeatModes,
repeatModes,
}
},
props: {

3
src/helpers/find.ts Normal file
View File

@ -0,0 +1,3 @@
export function findIndexById(array : [], id : string | number) {
return array.findIndex(({id: currentId}) => currentId === id)
}

View File

@ -1,5 +0,0 @@
{
"UNKNOWN": 0,
"USER": 1,
"LINK_SHARE": 2
}

View File

@ -5,7 +5,7 @@ import TaskModel from '@/models/task'
import TaskCommentModel from '@/models/taskComment'
import ListModel from '@/models/list'
import TeamModel from '@/models/team'
import names from './notificationNames.json'
import names from './constants/notificationNames.json'
export default class NotificationModel extends AbstractModel {
constructor(data) {

View File

@ -2,7 +2,7 @@ import AbstractModel from './abstractModel'
import UserModel from './user'
import LabelModel from './label'
import AttachmentModel from './attachment'
import {REPEAT_MODE_DEFAULT} from './taskRepeatModes'
import {REPEAT_MODE_DEFAULT} from './constants/taskRepeatModes'
import SubscriptionModel from '@/models/subscription'
import {parseDateOrNull} from '@/helpers/parseDateOrNull'
@ -15,6 +15,7 @@ export default class TaskModel extends AbstractModel {
super(data)
this.id = Number(this.id)
this.title = this.title?.trim()
this.listId = Number(this.listId)
// Make date objects from timestamps

View File

@ -1,7 +1,7 @@
import {parseTaskText} from './parseTaskText'
import {getDateFromText, getDateFromTextIn} from '../helpers/time/parseDate'
import {calculateDayInterval} from '../helpers/time/calculateDayInterval'
import priorities from '../models/priorities.json'
import priorities from '../models/constants/priorities.json'
describe('Parse Task Text', () => {
it('should return text with no intents as is', () => {

View File

@ -1,5 +1,5 @@
import {parseDate} from '../helpers/time/parseDate'
import _priorities from '../models/priorities.json'
import _priorities from '../models/constants/priorities.json'
const LABEL_PREFIX: string = '@'
const LIST_PREFIX: string = '#'

View File

@ -39,6 +39,8 @@ export default class TaskService extends AbstractService {
processModel(model) {
model.title = model.title?.trim()
// Ensure that listId is an int
model.listId = Number(model.listId)

View File

@ -1,5 +1,7 @@
import Vue from 'vue'
import {findIndexById} from '@/helpers/find'
export default {
namespaced: true,
state: () => ({
@ -15,13 +17,9 @@ export default {
state.attachments.push(attachment)
},
removeById(state, id) {
for (const a in state.attachments) {
if (state.attachments[a].id === id) {
state.attachments.splice(a, 1)
console.debug('Remove attachement', id)
break
}
}
const attachmentIndex = findIndexById(state.attachments, id)
state.attachments.splice(attachmentIndex, 1)
console.debug('Remove attachement', id)
},
},
}

View File

@ -3,6 +3,12 @@ import {ERROR_MESSAGE, LOADING} from '../mutation-types'
import UserModel from '../../models/user'
import {getToken, refreshToken, removeToken, saveToken} from '@/helpers/auth'
const AUTH_TYPES = {
'UNKNOWN': 0,
'USER': 1,
'LINK_SHARE': 2,
}
const defaultSettings = settings => {
if (typeof settings.weekStart === 'undefined' || settings.weekStart === '') {
settings.weekStart = 0
@ -21,6 +27,20 @@ export default {
lastUserInfoRefresh: null,
settings: {},
}),
getters: {
authUser(state) {
return state.authenticated && (
state.info &&
state.info.type === AUTH_TYPES.USER
)
},
authLinkShare(state) {
return state.authenticated && (
state.info &&
state.info.type === AUTH_TYPES.LINK_SHARE
)
},
},
mutations: {
info(state, info) {
state.info = info

View File

@ -1,4 +1,12 @@
@import "../../../node_modules/bulma/bulma";
// utilities are imported in variables.scss
@import "../../../node_modules/bulma/sass/base/_all";
@import "../../../node_modules/bulma/sass/elements/_all";
@import "../../../node_modules/bulma/sass/form/_all";
@import "../../../node_modules/bulma/sass/components/_all";
@import "../../../node_modules/bulma/sass/grid/_all";
@import "../../../node_modules/bulma/sass/helpers/_all";
@import "../../../node_modules/bulma/sass/layout/_all";
@import "fonts";

View File

@ -1,3 +1,5 @@
@import "../../../node_modules/bulma/sass/utilities/_all";
@import 'colors';
@import 'shadows';
@import 'variables';

View File

@ -1,7 +1,7 @@
/* eslint-disable no-console */
/* eslint-disable no-undef */
const workboxVersion = 'v6.2.4'
const workboxVersion = 'v6.3.0'
importScripts( `/workbox-${workboxVersion}/workbox-sw.js`)
workbox.setConfig({modulePathPrefix: `/workbox-${workboxVersion}`})

View File

@ -239,7 +239,7 @@ import BucketModel from '../../../models/bucket'
import {filterObject} from '@/helpers/filterObject'
import {mapState} from 'vuex'
import {saveListView} from '@/helpers/saveListView'
import Rights from '../../../models/rights.json'
import Rights from '../../../models/constants/rights.json'
import {LOADING, LOADING_MODULE} from '@/store/mutation-types'
import FilterPopup from '@/components/list/partials/filter-popup.vue'
import Dropdown from '@/components/misc/dropdown.vue'

View File

@ -177,7 +177,7 @@ 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 Rights from '../../../models/rights.json'
import Rights from '../../../models/constants/rights.json'
import FilterPopup from '@/components/list/partials/filter-popup.vue'
import {HAS_TASKS} from '@/store/mutation-types'
import Nothing from '@/components/misc/nothing.vue'

View File

@ -33,8 +33,7 @@
</template>
<script>
import {mapState} from 'vuex'
import authTypes from '@/models/authTypes.json'
import {mapGetters} from 'vuex'
export default {
name: 'LinkSharingAuth',
@ -54,9 +53,9 @@ export default {
mounted() {
this.setTitle(this.$t('sharing.authenticating'))
},
computed: mapState({
authLinkShare: state => state.auth.authenticated && (state.auth.info && state.auth.info.type === authTypes.LINK_SHARE),
}),
computed: mapGetters('auth', [
'authLinkShare',
]),
methods: {
auth() {
this.errorMessage = ''

View File

@ -419,10 +419,10 @@
<script>
import TaskService from '../../services/task'
import TaskModel from '../../models/task'
import relationKinds from '../../models/relationKinds.json'
import relationKinds from '../../models/constants/relationKinds.json'
import priorites from '../../models/priorities.json'
import rights from '../../models/rights.json'
import priorites from '../../models/constants/priorities.json'
import rights from '../../models/constants/rights.json'
import PrioritySelect from '../../components/tasks/partials/prioritySelect'
import PercentDoneSelect from '../../components/tasks/partials/percentDoneSelect'

View File

@ -167,7 +167,7 @@ import TeamMemberService from '../../services/teamMember'
import TeamMemberModel from '../../models/teamMember'
import UserModel from '../../models/user'
import UserService from '../../services/user'
import Rights from '../../models/rights.json'
import Rights from '../../models/constants/rights.json'
import LoadingComponent from '../../components/misc/loading'
import ErrorComponent from '../../components/misc/error'

1003
yarn.lock

File diff suppressed because it is too large Load Diff