Better reminders #308
|
@ -19,7 +19,12 @@ steps:
|
|||
- yarn --frozen-lockfile --network-timeout 100000
|
||||
- yarn run lint
|
||||
- yarn run build
|
||||
|
||||
- name: test
|
||||
image: node:13
|
||||
pull: true
|
||||
group: build-static
|
||||
commands:
|
||||
- yarn test
|
||||
---
|
||||
kind: pipeline
|
||||
name: release-latest
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build --modern",
|
||||
"lint": "vue-cli-service lint"
|
||||
"lint": "vue-cli-service lint --ignore-pattern '*.test.*'",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"bulma": "0.9.1",
|
||||
|
@ -40,6 +41,7 @@
|
|||
"babel-eslint": "10.1.0",
|
||||
"eslint": "7.14.0",
|
||||
"eslint-plugin-vue": "7.1.0",
|
||||
"jest": "^26.6.3",
|
||||
"node-sass": "5.0.0",
|
||||
"sass-loader": "10.1.0",
|
||||
"vue-flatpickr-component": "8.1.6",
|
||||
|
|
|
@ -0,0 +1,246 @@
|
|||
<template>
|
||||
<div class="datepicker" :class="{'disabled': disabled}">
|
||||
<a @click.stop="toggleDatePopup" class="show">
|
||||
<template v-if="date === null">
|
||||
{{ chooseDateLabel }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ formatDateShort(date) }}
|
||||
</template>
|
||||
</a>
|
||||
|
||||
<transition name="fade">
|
||||
<div v-if="show" class="datepicker-popup" ref="datepickerPopup">
|
||||
|
||||
<a @click.stop="() => setDate('today')" v-if="(new Date()).getHours() < 21">
|
||||
<span class="icon">
|
||||
<icon :icon="['far', 'calendar-alt']"/>
|
||||
</span>
|
||||
<span class="text">
|
||||
<span>
|
||||
Today
|
||||
</span>
|
||||
<span class="weekday">
|
||||
{{ getWeekdayFromStringInterval('today') }}
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
<a @click.stop="() => setDate('tomorrow')">
|
||||
<span class="icon">
|
||||
<icon :icon="['far', 'sun']"/>
|
||||
</span>
|
||||
<span class="text">
|
||||
<span>
|
||||
Tomorrow
|
||||
</span>
|
||||
<span class="weekday">
|
||||
{{ getWeekdayFromStringInterval('tomorrow') }}
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
<a @click.stop="() => setDate('nextMonday')">
|
||||
<span class="icon">
|
||||
<icon icon="coffee"/>
|
||||
</span>
|
||||
<span class="text">
|
||||
<span>
|
||||
Next Monday
|
||||
</span>
|
||||
<span class="weekday">
|
||||
{{ getWeekdayFromStringInterval('nextMonday') }}
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
<a @click.stop="() => setDate('thisWeekend')">
|
||||
<span class="icon">
|
||||
<icon icon="cocktail"/>
|
||||
</span>
|
||||
<span class="text">
|
||||
<span>
|
||||
This Weekend
|
||||
</span>
|
||||
<span class="weekday">
|
||||
{{ getWeekdayFromStringInterval('thisWeekend') }}
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
<a @click.stop="() => setDate('laterThisWeek')">
|
||||
<span class="icon">
|
||||
<icon icon="chess-knight"/>
|
||||
</span>
|
||||
<span class="text">
|
||||
<span>
|
||||
Later This Week
|
||||
</span>
|
||||
<span class="weekday">
|
||||
{{ getWeekdayFromStringInterval('laterThisWeek') }}
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
<a @click.stop="() => setDate('nextWeek')">
|
||||
<span class="icon">
|
||||
<icon icon="forward"/>
|
||||
</span>
|
||||
<span class="text">
|
||||
<span>
|
||||
Next Week
|
||||
</span>
|
||||
<span class="weekday">
|
||||
{{ getWeekdayFromStringInterval('nextWeek') }}
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<flat-pickr
|
||||
:config="flatPickerConfig"
|
||||
class="input"
|
||||
v-model="flatPickrDate"
|
||||
/>
|
||||
|
||||
<a
|
||||
class="button is-outlined is-primary has-no-shadow is-fullwidth"
|
||||
@click="close"
|
||||
>
|
||||
Confirm
|
||||
</a>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import flatPickr from 'vue-flatpickr-component'
|
||||
import 'flatpickr/dist/flatpickr.css'
|
||||
|
||||
import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'
|
||||
import {format} from 'date-fns'
|
||||
import {calculateNearestHours} from '@/helpers/time/calculateNearestHours'
|
||||
|
||||
export default {
|
||||
name: 'datepicker',
|
||||
data() {
|
||||
return {
|
||||
date: null,
|
||||
show: false,
|
||||
changed: false,
|
||||
|
||||
flatPickerConfig: {
|
||||
altFormat: 'j M Y H:i',
|
||||
altInput: true,
|
||||
dateFormat: 'Y-m-d H:i',
|
||||
enableTime: true,
|
||||
time_24hr: true,
|
||||
inline: true,
|
||||
},
|
||||
// Since flatpickr dates are strings, we need to convert them to native date objects.
|
||||
// To make that work, we need a separate variable since flatpickr does not have a change event.
|
||||
flatPickrDate: null,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
flatPickr,
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
validator: prop => prop instanceof Date || prop === null || typeof prop === 'string'
|
||||
},
|
||||
chooseDateLabel: {
|
||||
type: String,
|
||||
default: 'Choose a date'
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.date = this.value
|
||||
document.addEventListener('click', this.hideDatePopup)
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('click', this.hideDatePopup)
|
||||
},
|
||||
watch: {
|
||||
value(newVal) {
|
||||
if(newVal === null) {
|
||||
this.date = null
|
||||
return
|
||||
}
|
||||
this.date = new Date(newVal)
|
||||
},
|
||||
flatPickrDate(newVal) {
|
||||
this.date = new Date(newVal)
|
||||
this.updateData()
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateData() {
|
||||
this.changed = true
|
||||
this.$emit('input', this.date)
|
||||
this.$emit('change', this.date)
|
||||
},
|
||||
toggleDatePopup() {
|
||||
if(this.disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
this.show = !this.show
|
||||
},
|
||||
hideDatePopup(e) {
|
||||
if (this.show) {
|
||||
|
||||
// We walk up the tree to see if any parent of the clicked element is the datepicker element.
|
||||
// If it is not, we hide the popup. We're doing all this hassle to prevent the popup from closing when
|
||||
// clicking an element of flatpickr.
|
||||
let parent = e.target.parentElement
|
||||
while (parent !== this.$refs.datepickerPopup) {
|
||||
if (parent.parentElement === null) {
|
||||
parent = null
|
||||
break
|
||||
}
|
||||
|
||||
parent = parent.parentElement
|
||||
}
|
||||
|
||||
if (parent === this.$refs.datepickerPopup) {
|
||||
return
|
||||
}
|
||||
|
||||
this.close()
|
||||
}
|
||||
},
|
||||
close() {
|
||||
this.show = false
|
||||
this.$emit('close', this.changed)
|
||||
if(this.changed) {
|
||||
this.changed = false
|
||||
this.$emit('close-on-change', this.changed)
|
||||
}
|
||||
},
|
||||
setDate(date) {
|
||||
if (this.date === null) {
|
||||
this.date = new Date()
|
||||
}
|
||||
|
||||
const interval = calculateDayInterval(date)
|
||||
const newDate = new Date()
|
||||
newDate.setDate(newDate.getDate() + interval)
|
||||
newDate.setHours(calculateNearestHours(newDate))
|
||||
newDate.setMinutes(0)
|
||||
newDate.setSeconds(0)
|
||||
this.date = newDate
|
||||
this.flatPickrDate = newDate
|
||||
this.updateData()
|
||||
},
|
||||
getDayIntervalFromString(date) {
|
||||
return calculateDayInterval(date)
|
||||
},
|
||||
getWeekdayFromStringInterval(date) {
|
||||
const interval = calculateDayInterval(date)
|
||||
const newDate = new Date()
|
||||
newDate.setDate(newDate.getDate() + interval)
|
||||
return format(newDate, 'E')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -1,70 +1,79 @@
|
|||
<template>
|
||||
<div class="reminders">
|
||||
<div
|
||||
:class="{ 'overdue': (r < nowUnix && index !== (reminders.length - 1))}"
|
||||
v-for="(r, index) in reminders"
|
||||
:key="index"
|
||||
:class="{ 'overdue': r < new Date()}"
|
||||
class="reminder-input"
|
||||
v-for="(r, index) in reminders">
|
||||
<flat-pickr
|
||||
:config="flatPickerConfig"
|
||||
:data-index="index"
|
||||
>
|
||||
<datepicker
|
||||
v-model="reminders[index]"
|
||||
:disabled="disabled"
|
||||
:value="r"
|
||||
@close-on-change="() => addReminderDate(index)"
|
||||
/>
|
||||
<a @click="removeReminderByIndex(index)" v-if="!disabled">
|
||||
<a @click="removeReminderByIndex(index)" v-if="!disabled" class="remove">
|
||||
<icon icon="times"></icon>
|
||||
</a>
|
||||
</div>
|
||||
<div class="reminder-input" v-if="showNewReminder">
|
||||
<flat-pickr
|
||||
:config="flatPickerConfig"
|
||||
:disabled="disabled"
|
||||
:value="null"
|
||||
placeholder="Add a new reminder..."
|
||||
<div class="reminder-input" v-if="!disabled">
|
||||
<datepicker
|
||||
v-model="newReminder"
|
||||
@close-on-change="() => addReminderDate()"
|
||||
choose-date-label="Add a new reminder..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import flatPickr from 'vue-flatpickr-component'
|
||||
import 'flatpickr/dist/flatpickr.css'
|
||||
import datepicker from '@/components/input/datepicker'
|
||||
|
||||
export default {
|
||||
name: 'reminders',
|
||||
data() {
|
||||
return {
|
||||
newReminder: null,
|
||||
reminders: [],
|
||||
lastReminder: 0,
|
||||
nowUnix: new Date(),
|
||||
showNewReminder: true,
|
||||
flatPickerConfig: {
|
||||
altFormat: 'j M Y H:i',
|
||||
altInput: true,
|
||||
dateFormat: 'Y-m-d H:i',
|
||||
enableTime: true,
|
||||
onOpen: this.updateLastReminderDate,
|
||||
onClose: this.addReminderDate,
|
||||
},
|
||||
}
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
default: () => [],
|
||||
type: Array,
|
||||
validator: prop => {
|
||||
// This allows arrays of Dates and strings
|
||||
if (!(prop instanceof Array)) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (const e of prop) {
|
||||
const isDate = e instanceof Date
|
||||
const isString = typeof e === 'string'
|
||||
if (!isDate && !isString) {
|
||||
console.log('validation failed', e, e instanceof Date)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
},
|
||||
disabled: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
flatPickr,
|
||||
datepicker,
|
||||
},
|
||||
mounted() {
|
||||
this.reminders = this.value
|
||||
},
|
||||
watch: {
|
||||
value(newVal) {
|
||||
for (const i in newVal) {
|
||||
if (typeof newVal[i] === 'string') {
|
||||
newVal[i] = new Date(newVal[i])
|
||||
}
|
||||
}
|
||||
this.reminders = newVal
|
||||
},
|
||||
},
|
||||
|
@ -73,40 +82,22 @@ export default {
|
|||
this.$emit('input', this.reminders)
|
||||
this.$emit('change')
|
||||
},
|
||||
updateLastReminderDate(selectedDates) {
|
||||
this.lastReminder = +new Date(selectedDates[0])
|
||||
},
|
||||
addReminderDate(selectedDates, dateStr, instance) {
|
||||
const newDate = +new Date(selectedDates[0])
|
||||
|
||||
// Don't update if nothing changed
|
||||
if (newDate === this.lastReminder) {
|
||||
addReminderDate(index = null) {
|
||||
// New Date
|
||||
if (index === null) {
|
||||
if (this.newReminder === null) {
|
||||
return
|
||||
}
|
||||
this.reminders.push(new Date(this.newReminder))
|
||||
this.newReminder = null
|
||||
} else if(this.reminders[index] === null) {
|
||||
return
|
||||
}
|
||||
|
||||
// No date selected
|
||||
if (isNaN(newDate)) {
|
||||
return
|
||||
}
|
||||
|
||||
const index = parseInt(instance.input.dataset.index)
|
||||
if (isNaN(index)) {
|
||||
this.reminders.push(newDate)
|
||||
// This is a workaround to recreate the flatpicker instance which essentially resets it.
|
||||
// Even though flatpickr itself has a reset event, the Vue component does not expose it.
|
||||
this.showNewReminder = false
|
||||
this.$nextTick(() => this.showNewReminder = true)
|
||||
} else {
|
||||
this.reminders[index] = newDate
|
||||
}
|
||||
|
||||
this.updateData()
|
||||
},
|
||||
removeReminderByIndex(index) {
|
||||
this.reminders.splice(index, 1)
|
||||
// Reset the last to 0 to have the "add reminder" button
|
||||
this.reminders[this.reminders.length - 1] = null
|
||||
|
||||
this.updateData()
|
||||
},
|
||||
},
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
export function calculateDayInterval(date, currentDay = (new Date().getDay())) {
|
||||
switch (date) {
|
||||
case 'today':
|
||||
return 0
|
||||
case 'tomorrow':
|
||||
return 1
|
||||
case 'nextMonday':
|
||||
// Monday is 1, so we calculate the distance to the next 1
|
||||
return (currentDay + (8 - currentDay * 2)) % 7
|
||||
case 'thisWeekend':
|
||||
// Saturday is 6 so we calculate the distance to the next 6
|
||||
return (6 - currentDay) % 6
|
||||
case 'laterThisWeek':
|
||||
if (currentDay === 5 || currentDay === 6 || currentDay === 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return 2
|
||||
case 'laterNextWeek':
|
||||
return calculateDayInterval('laterThisWeek', currentDay) + 7
|
||||
case 'nextWeek':
|
||||
return 7
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
import {calculateDayInterval} from './calculateDayInterval'
|
||||
|
||||
const days = {
|
||||
monday: 1,
|
||||
tuesday: 2,
|
||||
wednesday: 3,
|
||||
thursday: 4,
|
||||
friday: 5,
|
||||
saturday: 6,
|
||||
sunday: 0,
|
||||
}
|
||||
|
||||
for (const n in days) {
|
||||
test(`today on a ${n}`, () => {
|
||||
expect(calculateDayInterval('today', days[n])).toBe(0)
|
||||
})
|
||||
}
|
||||
|
||||
for (const n in days) {
|
||||
test(`tomorrow on a ${n}`, () => {
|
||||
expect(calculateDayInterval('tomorrow', days[n])).toBe(1)
|
||||
})
|
||||
}
|
||||
|
||||
const nextMonday = {
|
||||
monday: 0,
|
||||
tuesday: 6,
|
||||
wednesday: 5,
|
||||
thursday: 4,
|
||||
friday: 3,
|
||||
saturday: 2,
|
||||
sunday: 1,
|
||||
}
|
||||
|
||||
for (const n in nextMonday) {
|
||||
test(`next monday on a ${n}`, () => {
|
||||
expect(calculateDayInterval('nextMonday', days[n])).toBe(nextMonday[n])
|
||||
})
|
||||
}
|
||||
|
||||
const thisWeekend = {
|
||||
monday: 5,
|
||||
tuesday: 4,
|
||||
wednesday: 3,
|
||||
thursday: 2,
|
||||
friday: 1,
|
||||
saturday: 0,
|
||||
sunday: 0,
|
||||
}
|
||||
|
||||
for (const n in thisWeekend) {
|
||||
test(`this weekend on a ${n}`, () => {
|
||||
expect(calculateDayInterval('thisWeekend', days[n])).toBe(thisWeekend[n])
|
||||
})
|
||||
}
|
||||
|
||||
const laterThisWeek = {
|
||||
monday: 2,
|
||||
tuesday: 2,
|
||||
wednesday: 2,
|
||||
thursday: 2,
|
||||
friday: 0,
|
||||
saturday: 0,
|
||||
sunday: 0,
|
||||
}
|
||||
|
||||
for (const n in laterThisWeek) {
|
||||
test(`later this week on a ${n}`, () => {
|
||||
expect(calculateDayInterval('laterThisWeek', days[n])).toBe(laterThisWeek[n])
|
||||
})
|
||||
}
|
||||
|
||||
const laterNextWeek = {
|
||||
monday: 7 + 2,
|
||||
tuesday: 7 + 2,
|
||||
wednesday: 7 + 2,
|
||||
thursday: 7 + 2,
|
||||
friday: 7 + 0,
|
||||
saturday: 7 + 0,
|
||||
sunday: 7 + 0,
|
||||
}
|
||||
|
||||
for (const n in laterNextWeek) {
|
||||
test(`later next week on a ${n} (this week)`, () => {
|
||||
expect(calculateDayInterval('laterNextWeek', days[n])).toBe(laterNextWeek[n])
|
||||
})
|
||||
}
|
||||
|
||||
for (const n in days) {
|
||||
test(`next week on a ${n}`, () => {
|
||||
expect(calculateDayInterval('nextWeek', days[n])).toBe(7)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
export function calculateNearestHours(currentDate = new Date()) {
|
||||
if (currentDate.getHours() <= 9 || currentDate.getHours() > 21) {
|
||||
return 9
|
||||
}
|
||||
|
||||
if (currentDate.getHours() <= 12) {
|
||||
return 12
|
||||
}
|
||||
|
||||
if (currentDate.getHours() <= 15) {
|
||||
return 15
|
||||
}
|
||||
|
||||
if (currentDate.getHours() <= 18) {
|
||||
return 18
|
||||
}
|
||||
|
||||
if (currentDate.getHours() <= 21) {
|
||||
return 21
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
import {calculateNearestHours} from './calculateNearestHours'
|
||||
|
||||
test('5:00', () => {
|
||||
const date = new Date()
|
||||
date.setHours(5)
|
||||
expect(calculateNearestHours(date)).toBe(9)
|
||||
})
|
||||
|
||||
test('7:00', () => {
|
||||
const date = new Date()
|
||||
date.setHours(7)
|
||||
expect(calculateNearestHours(date)).toBe(9)
|
||||
})
|
||||
|
||||
test('7:41', () => {
|
||||
const date = new Date()
|
||||
date.setHours(7)
|
||||
date.setMinutes(41)
|
||||
expect(calculateNearestHours(date)).toBe(9)
|
||||
})
|
||||
|
||||
test('9:00', () => {
|
||||
const date = new Date()
|
||||
date.setHours(9)
|
||||
date.setMinutes(0)
|
||||
expect(calculateNearestHours(date)).toBe(9)
|
||||
})
|
||||
|
||||
test('10:00', () => {
|
||||
const date = new Date()
|
||||
date.setHours(10)
|
||||
date.setMinutes(0)
|
||||
expect(calculateNearestHours(date)).toBe(12)
|
||||
})
|
||||
|
||||
test('12:00', () => {
|
||||
const date = new Date()
|
||||
date.setHours(12)
|
||||
date.setMinutes(0)
|
||||
expect(calculateNearestHours(date)).toBe(12)
|
||||
})
|
||||
|
||||
test('13:00', () => {
|
||||
const date = new Date()
|
||||
date.setHours(13)
|
||||
date.setMinutes(0)
|
||||
expect(calculateNearestHours(date)).toBe(15)
|
||||
})
|
||||
|
||||
test('15:00', () => {
|
||||
const date = new Date()
|
||||
date.setHours(15)
|
||||
date.setMinutes(0)
|
||||
expect(calculateNearestHours(date)).toBe(15)
|
||||
})
|
||||
|
||||
test('16:00', () => {
|
||||
const date = new Date()
|
||||
date.setHours(16)
|
||||
date.setMinutes(0)
|
||||
expect(calculateNearestHours(date)).toBe(18)
|
||||
})
|
||||
|
||||
test('18:00', () => {
|
||||
const date = new Date()
|
||||
date.setHours(18)
|
||||
date.setMinutes(0)
|
||||
expect(calculateNearestHours(date)).toBe(18)
|
||||
})
|
||||
|
||||
test('19:00', () => {
|
||||
const date = new Date()
|
||||
date.setHours(19)
|
||||
date.setMinutes(0)
|
||||
expect(calculateNearestHours(date)).toBe(21)
|
||||
})
|
||||
|
||||
test('22:00', () => {
|
||||
const date = new Date()
|
||||
date.setHours(22)
|
||||
date.setMinutes(0)
|
||||
expect(calculateNearestHours(date)).toBe(9)
|
||||
})
|
||||
|
||||
test('22:40', () => {
|
||||
const date = new Date()
|
||||
date.setHours(22)
|
||||
date.setMinutes(0)
|
||||
expect(calculateNearestHours(date)).toBe(9)
|
||||
})
|
21
src/main.js
21
src/main.js
|
@ -53,8 +53,12 @@ import {
|
|||
faTrashAlt,
|
||||
faUser,
|
||||
faUsers,
|
||||
faForward,
|
||||
faChessKnight,
|
||||
faCoffee,
|
||||
faCocktail,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import {faCalendarAlt, faClock, faComments, faSave, faStar, faTimesCircle} from '@fortawesome/free-regular-svg-icons'
|
||||
import {faCalendarAlt, faClock, faComments, faSave, faStar, faTimesCircle, faSun} from '@fortawesome/free-regular-svg-icons'
|
||||
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'
|
||||
// PWA
|
||||
import './registerServiceWorker'
|
||||
|
@ -135,6 +139,11 @@ library.add(faFillDrip)
|
|||
library.add(faKeyboard)
|
||||
library.add(faSave)
|
||||
library.add(faStarSolid)
|
||||
library.add(faForward)
|
||||
library.add(faSun)
|
||||
library.add(faChessKnight)
|
||||
library.add(faCoffee)
|
||||
library.add(faCocktail)
|
||||
|
||||
Vue.component('icon', FontAwesomeIcon)
|
||||
|
||||
|
@ -146,6 +155,13 @@ Vue.directive('focus', focus)
|
|||
import tooltip from '@/directives/tooltip'
|
||||
Vue.directive('tooltip', tooltip)
|
||||
|
||||
const formatDate = (date, f) => {
|
||||
if (typeof date === 'string') {
|
||||
date = new Date(date)
|
||||
}
|
||||
return date ? format(date, f) : ''
|
||||
}
|
||||
|
||||
Vue.mixin({
|
||||
methods: {
|
||||
formatDateSince: date => {
|
||||
|
@ -170,6 +186,9 @@ Vue.mixin({
|
|||
}
|
||||
return date ? format(date, 'PPPPpppp') : ''
|
||||
},
|
||||
formatDateShort: date => {
|
||||
return formatDate(date, 'PPpp')
|
||||
},
|
||||
error: (e, context, actions = []) => message.error(e, context, actions),
|
||||
success: (s, context, actions = []) => message.success(s, context, actions),
|
||||
colorIsDark: colorIsDark,
|
||||
|
|
|
@ -22,3 +22,4 @@
|
|||
@import 'legal';
|
||||
@import 'keyboard-shortcuts';
|
||||
@import 'api-config';
|
||||
@import 'datepicker'
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
.datepicker {
|
||||
input.input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.disabled a {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.datepicker-popup {
|
||||
position: absolute;
|
||||
z-index: 99;
|
||||
width: 320px;
|
||||
background: $white;
|
||||
border-radius: $radius;
|
||||
box-shadow: $card-shadow;
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 .5rem;
|
||||
width: 100%;
|
||||
height: 2.25rem;
|
||||
color: $text;
|
||||
transition: all $transition;
|
||||
|
||||
&:first-child {
|
||||
border-radius: $radius $radius 0 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $light;
|
||||
}
|
||||
|
||||
.text {
|
||||
width: 100%;
|
||||
font-size: .85rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-right: .25rem;
|
||||
|
||||
.weekday {
|
||||
color: $text-light;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.button {
|
||||
margin: 1rem;
|
||||
width: calc(100% - 2rem);
|
||||
}
|
||||
}
|
||||
|
||||
.flatpickr-calendar {
|
||||
margin: 0 auto 8px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: ($tablet)) {
|
||||
width: calc(100vw - 4rem);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.overdue input {
|
||||
&.overdue .datepicker a.show {
|
||||
color: $red;
|
||||
}
|
||||
|
||||
|
@ -11,19 +11,9 @@
|
|||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
a {
|
||||
a.remove {
|
||||
color: $red;
|
||||
padding-left: .5rem;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
border: none;
|
||||
|
||||
&:focus {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
a {
|
||||
a.remove {
|
||||
color: $red;
|
||||
vertical-align: middle;
|
||||
padding-left: .5em;
|
||||
|
@ -58,6 +58,27 @@
|
|||
}
|
||||
}
|
||||
|
||||
.datepicker {
|
||||
width: 100%;
|
||||
|
||||
a.show {
|
||||
color: $text;
|
||||
padding: .25rem .5rem;
|
||||
transition: background-color $transition;
|
||||
border-radius: $radius;
|
||||
display: block;
|
||||
margin: .1rem 0;
|
||||
|
||||
&:hover {
|
||||
background: $white;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled a.show:hover {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
table-layout: fixed;
|
||||
|
||||
|
|
|
@ -47,18 +47,14 @@
|
|||
Due Date
|
||||
</div>
|
||||
<div class="date-input">
|
||||
<flat-pickr
|
||||
:class="{ 'disabled': taskService.loading}"
|
||||
:config="flatPickerConfig"
|
||||
<datepicker
|
||||
v-model="task.dueDate"
|
||||
@close-on-change="() => saveTask()"
|
||||
choose-date-label="Click here to set a due date"
|
||||
:disabled="taskService.loading || !canWrite"
|
||||
@on-close="() => saveTask()"
|
||||
class="input"
|
||||
placeholder="Click here to set a due date"
|
||||
ref="dueDate"
|
||||
v-model="dueDate"
|
||||
>
|
||||
</flat-pickr>
|
||||
<a @click="() => {dueDate = task.dueDate = null;saveTask()}" v-if="dueDate && canWrite">
|
||||
/>
|
||||
<a @click="() => {task.dueDate = null;saveTask()}" v-if="task.dueDate && canWrite" class="remove">
|
||||
<span class="icon is-small">
|
||||
<icon icon="times"></icon>
|
||||
</span>
|
||||
|
@ -84,18 +80,14 @@
|
|||
Start Date
|
||||
</div>
|
||||
<div class="date-input">
|
||||
<flat-pickr
|
||||
:class="{ 'disabled': taskService.loading}"
|
||||
:config="flatPickerConfig"
|
||||
:disabled="taskService.loading || !canWrite"
|
||||
@on-close="() => saveTask()"
|
||||
class="input"
|
||||
placeholder="Click here to set a start date"
|
||||
ref="startDate"
|
||||
<datepicker
|
||||
v-model="task.startDate"
|
||||
>
|
||||
</flat-pickr>
|
||||
<a @click="() => {task.startDate = null;saveTask()}" v-if="task.startDate && canWrite">
|
||||
@close-on-change="() => saveTask()"
|
||||
choose-date-label="Click here to set a start date"
|
||||
:disabled="taskService.loading || !canWrite"
|
||||
ref="startDate"
|
||||
/>
|
||||
<a @click="() => {task.startDate = null;saveTask()}" v-if="task.startDate && canWrite" class="remove">
|
||||
<span class="icon is-small">
|
||||
<icon icon="times"></icon>
|
||||
</span>
|
||||
|
@ -109,18 +101,14 @@
|
|||
End Date
|
||||
</div>
|
||||
<div class="date-input">
|
||||
<flat-pickr
|
||||
:class="{ 'disabled': taskService.loading}"
|
||||
:config="flatPickerConfig"
|
||||
:disabled="taskService.loading || !canWrite"
|
||||
@on-close="() => saveTask()"
|
||||
class="input"
|
||||
placeholder="Click here to set an end date"
|
||||
ref="endDate"
|
||||
<datepicker
|
||||
v-model="task.endDate"
|
||||
>
|
||||
</flat-pickr>
|
||||
<a @click="() => {task.endDate = null;saveTask()}" v-if="task.endDate && canWrite">
|
||||
@close-on-change="() => saveTask()"
|
||||
choose-date-label="Click here to set an end date"
|
||||
:disabled="taskService.loading || !canWrite"
|
||||
ref="endDate"
|
||||
/>
|
||||
<a @click="() => {task.endDate = null;saveTask()}" v-if="task.endDate && canWrite" class="remove">
|
||||
<span class="icon is-small">
|
||||
<icon icon="times"></icon>
|
||||
</span>
|
||||
|
@ -356,9 +344,6 @@ import relationKinds from '../../models/relationKinds.json'
|
|||
import priorites from '../../models/priorities.json'
|
||||
import rights from '../../models/rights.json'
|
||||
|
||||
import flatPickr from 'vue-flatpickr-component'
|
||||
import 'flatpickr/dist/flatpickr.css'
|
||||
|
||||
import PrioritySelect from '../../components/tasks/partials/prioritySelect'
|
||||
import PercentDoneSelect from '../../components/tasks/partials/percentDoneSelect'
|
||||
import EditLabels from '../../components/tasks/partials/editLabels'
|
||||
|
@ -374,10 +359,12 @@ import description from '@/components/tasks/partials/description'
|
|||
import ColorPicker from '../../components/input/colorPicker'
|
||||
import attachmentUpload from '../../components/tasks/mixins/attachmentUpload'
|
||||
import heading from '@/components/tasks/partials/heading'
|
||||
import Datepicker from '@/components/input/datepicker'
|
||||
|
||||
export default {
|
||||
name: 'TaskDetailView',
|
||||
components: {
|
||||
Datepicker,
|
||||
ColorPicker,
|
||||
ListSearch,
|
||||
Reminders,
|
||||
|
@ -389,7 +376,6 @@ export default {
|
|||
PercentDoneSelect,
|
||||
PrioritySelect,
|
||||
Comments,
|
||||
flatPickr,
|
||||
description,
|
||||
heading,
|
||||
},
|
||||
|
@ -402,9 +388,6 @@ export default {
|
|||
taskService: TaskService,
|
||||
task: TaskModel,
|
||||
relationKinds: relationKinds,
|
||||
// The due date is a seperate property in the task to prevent flatpickr from modifying the task model
|
||||
// in store right after updating it from the api resulting in the wrong due date format being saved in the task.
|
||||
dueDate: null,
|
||||
// We doubled the task color property here because verte does not have a real change property, leading
|
||||
// to the color property change being triggered when the # is removed from it, leading to an update,
|
||||
// which leads in turn to a change... This creates an infinite loop in which the task is updated, changed,
|
||||
|
@ -421,13 +404,6 @@ export default {
|
|||
descriptionRecentlySaved: false,
|
||||
|
||||
priorities: priorites,
|
||||
flatPickerConfig: {
|
||||
altFormat: 'j M Y H:i',
|
||||
altInput: true,
|
||||
dateFormat: 'Y-m-d H:i',
|
||||
enableTime: true,
|
||||
time_24hr: true,
|
||||
},
|
||||
activeFields: {
|
||||
assignees: false,
|
||||
priority: false,
|
||||
|
@ -504,7 +480,6 @@ export default {
|
|||
},
|
||||
setActiveFields() {
|
||||
|
||||
this.dueDate = this.task.dueDate ? this.task.dueDate : null
|
||||
this.task.startDate = this.task.startDate ? this.task.startDate : null
|
||||
this.task.endDate = this.task.endDate ? this.task.endDate : null
|
||||
|
||||
|
@ -530,7 +505,6 @@ export default {
|
|||
// We're doing the whole update in a nextTick because sometimes race conditions can occur when
|
||||
// setting the due date on mobile which leads to no due date change being saved.
|
||||
this.$nextTick(() => {
|
||||
this.task.dueDate = this.dueDate
|
||||
this.task.hexColor = this.taskColor
|
||||
|
||||
// If no end date is being set, but a start date and due date,
|
||||
|
|
Reference in New Issue