Merge pull request #4668 from nextcloud/enh/native-datepicker
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { randUser } from '../utils/index.js'
|
||||
import { sampleBoard } from '../utils/sampleBoard'
|
||||
import moment from '@nextcloud/moment'
|
||||
|
||||
const user = randUser()
|
||||
const boardData = sampleBoard()
|
||||
@@ -177,6 +178,46 @@ describe('Card', function() {
|
||||
cy.get('#app-sidebar-vue')
|
||||
.find('.ProseMirror h1').contains('Hello world writing more text').should('be.visible')
|
||||
})
|
||||
|
||||
it('Set a due date', function() {
|
||||
const newCardTitle = 'Card with a due date'
|
||||
|
||||
cy.get('.button-vue[aria-label*="Add card"]')
|
||||
.first().click()
|
||||
cy.get('.stack__card-add form input#new-stack-input-main')
|
||||
.type(newCardTitle)
|
||||
cy.get('.stack__card-add form input[type=submit]')
|
||||
.first().click()
|
||||
cy.get(`.card:contains("${newCardTitle}")`).should('be.visible')
|
||||
|
||||
cy.get('.card:contains("Card with a due date")').should('be.visible').click()
|
||||
|
||||
cy.get('#app-sidebar-vue [data-cy-due-date-actions]').should('be.visible').click()
|
||||
|
||||
// Set a due date through shortcut
|
||||
cy.get('[data-cy-due-date-shortcut="tomorrow"] button').should('be.visible').click()
|
||||
|
||||
const tomorrow = moment().add(1, 'days').hour(8).minutes(0).seconds(0)
|
||||
cy.get('#card-duedate-picker').should('have.value', tomorrow.format('YYYY-MM-DDTHH:mm'))
|
||||
|
||||
const now = moment().hour(11).minutes(0).seconds(0).toDate()
|
||||
cy.clock(now)
|
||||
cy.log(now)
|
||||
cy.tick(60_000)
|
||||
|
||||
cy.get(`.card:contains("${newCardTitle}")`).find('[data-due-state="Now"]').should('be.visible').should('contain', '21 hours')
|
||||
|
||||
|
||||
// Remove the due date again
|
||||
cy.get('#app-sidebar-vue [data-cy-due-date-actions]').should('be.visible').click()
|
||||
// tick needed to show the popover menu
|
||||
cy.tick(1_000)
|
||||
cy.get('[data-cy-due-date-remove] button').should('be.visible').click()
|
||||
|
||||
cy.get(`.card:contains("${newCardTitle}")`).find('[data-due-state]').should('not.be.visible')
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
@select="assignUserToCard"
|
||||
@remove="removeUserFromCard" />
|
||||
|
||||
<DueDateSelector :card="card" :can-edit="canEdit && !saving" @change="updateCardDue" />
|
||||
<DueDateSelector :card="card" :can-edit="canEdit" @change="updateCardDue" />
|
||||
|
||||
<div v-if="projectsEnabled" class="section-wrapper">
|
||||
<CollectionList v-if="card.id"
|
||||
@@ -85,7 +85,6 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
saving: false,
|
||||
addedLabelToCard: null,
|
||||
copiedCard: null,
|
||||
locale: getLocale(),
|
||||
@@ -105,7 +104,6 @@ export default {
|
||||
this.$store.dispatch('setConfig', { cardDetailsInModal: newValue })
|
||||
},
|
||||
},
|
||||
|
||||
labelsSorted() {
|
||||
return [...this.currentBoard.labels].sort((a, b) => (a.title < b.title) ? -1 : 1)
|
||||
},
|
||||
@@ -133,15 +131,6 @@ export default {
|
||||
localStorage.setItem('deck.selectedStackId', this.card.stackId)
|
||||
},
|
||||
|
||||
async updateCardDue(val) {
|
||||
this.saving = true
|
||||
await this.$store.dispatch('updateCardDue', {
|
||||
...this.copiedCard,
|
||||
duedate: val ? (new Date(val)).toISOString() : null,
|
||||
})
|
||||
this.saving = false
|
||||
},
|
||||
|
||||
assignUserToCard(user) {
|
||||
this.$store.dispatch('assignCardToUser', {
|
||||
card: this.copiedCard,
|
||||
@@ -162,6 +151,13 @@ export default {
|
||||
})
|
||||
},
|
||||
|
||||
updateCardDue(val) {
|
||||
this.$store.dispatch('updateCardDue', {
|
||||
...this.copiedCard,
|
||||
duedate: val ? (new Date(val)).toISOString() : null,
|
||||
})
|
||||
},
|
||||
|
||||
addLabelToCard(newLabel) {
|
||||
this.copiedCard.labels.push(newLabel)
|
||||
const data = {
|
||||
@@ -205,15 +201,6 @@ export default {
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.section-wrapper:deep(.mx-datepicker-main.mx-datepicker-popup) {
|
||||
left: 0 !important;
|
||||
}
|
||||
|
||||
.section-wrapper:deep(.mx-datepicker-main.mx-datepicker-popup.mx-datepicker-sidebar) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.section-wrapper {
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
@@ -266,9 +253,3 @@ export default {
|
||||
z-index: 0;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.mx-datepicker-main.mx-datepicker-popup {
|
||||
/* above the modal */
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,19 +4,42 @@
|
||||
<Calendar :size="20" />
|
||||
</div>
|
||||
<div class="duedate-selector">
|
||||
<NcDatetimePicker v-model="duedate"
|
||||
<NcDateTimePickerNative v-if="duedate"
|
||||
id="card-duedate-picker"
|
||||
v-model="duedate"
|
||||
:placeholder="t('deck', 'Set a due date')"
|
||||
type="datetime"
|
||||
:minute-step="5"
|
||||
:show-second="false"
|
||||
:lang="lang"
|
||||
:formatter="format"
|
||||
:disabled="!canEdit"
|
||||
:shortcuts="shortcuts"
|
||||
:append-to-body="true"
|
||||
confirm />
|
||||
<NcActions v-if="canEdit">
|
||||
<NcActionButton v-if="duedate" icon="icon-delete" @click="removeDue()">
|
||||
:hide-label="true"
|
||||
type="datetime-local" />
|
||||
<NcActions v-if="canEdit"
|
||||
:menu-title="!duedate ? t('deck', 'Add due date') : null"
|
||||
type="tertiary"
|
||||
data-cy-due-date-actions>
|
||||
<template v-if="!duedate" #icon>
|
||||
<Plus :size="20" />
|
||||
</template>
|
||||
<NcActionButton v-for="shortcut in reminderOptions"
|
||||
:key="shortcut.key"
|
||||
close-after-click
|
||||
:data-cy-due-date-shortcut="shortcut.key"
|
||||
@click="() => selectShortcut(shortcut)">
|
||||
{{ shortcut.label }}
|
||||
</NcActionButton>
|
||||
<NcActionSeparator />
|
||||
|
||||
<NcActionButton v-if="!duedate"
|
||||
close-after-click
|
||||
data-cy-due-date-pick
|
||||
@click="initDate">
|
||||
<template #icon>
|
||||
<Plus :size="20" />
|
||||
</template>
|
||||
{{ t('deck', 'Choose a date') }}
|
||||
</NcActionButton>
|
||||
<NcActionButton v-else
|
||||
icon="icon-delete"
|
||||
close-after-click
|
||||
data-cy-due-date-remove
|
||||
@click="removeDue">
|
||||
{{ t('deck', 'Remove due date') }}
|
||||
</NcActionButton>
|
||||
</NcActions>
|
||||
@@ -26,17 +49,21 @@
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
import { NcActionButton, NcActions, NcDatetimePicker } from '@nextcloud/vue'
|
||||
import { NcActionButton, NcActions, NcActionSeparator, NcDateTimePickerNative } from '@nextcloud/vue'
|
||||
import { getDayNamesMin, getFirstDay, getMonthNamesShort } from '@nextcloud/l10n'
|
||||
import Plus from 'vue-material-design-icons/Plus.vue'
|
||||
import Calendar from 'vue-material-design-icons/Calendar.vue'
|
||||
import moment from '@nextcloud/moment'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DueDateSelector',
|
||||
components: {
|
||||
Plus,
|
||||
Calendar,
|
||||
NcActions,
|
||||
NcActionButton,
|
||||
NcDatetimePicker,
|
||||
NcActionSeparator,
|
||||
NcDateTimePickerNative,
|
||||
},
|
||||
props: {
|
||||
card: {
|
||||
@@ -64,64 +91,81 @@ export default defineComponent({
|
||||
stringify: this.stringify,
|
||||
parse: this.parse,
|
||||
},
|
||||
shortcuts: [
|
||||
{
|
||||
text: t('deck', 'Today'),
|
||||
onClick() {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate())
|
||||
date.setHours(23)
|
||||
date.setMinutes(59)
|
||||
return date
|
||||
},
|
||||
},
|
||||
{
|
||||
text: t('deck', 'Tomorrow'),
|
||||
onClick() {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() + 1)
|
||||
date.setHours(23)
|
||||
date.setMinutes(59)
|
||||
return date
|
||||
},
|
||||
},
|
||||
{
|
||||
text: t('deck', 'Next week'),
|
||||
onClick() {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() + 7)
|
||||
date.setHours(23)
|
||||
date.setMinutes(59)
|
||||
return date
|
||||
},
|
||||
},
|
||||
{
|
||||
text: t('deck', 'Next month'),
|
||||
onClick() {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() + 30)
|
||||
date.setHours(23)
|
||||
date.setMinutes(59)
|
||||
return date
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
duedate: {
|
||||
get() {
|
||||
return this.card.duedate ? new Date(this.card.duedate) : null
|
||||
return this.card?.duedate ? new Date(this.card.duedate) : null
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('change', val)
|
||||
this.$emit('change', val ? new Date(val) : null)
|
||||
},
|
||||
},
|
||||
|
||||
reminderOptions() {
|
||||
const currentDateTime = moment()
|
||||
// Same day 18:00 PM (or hidden)
|
||||
const laterTodayTime = (currentDateTime.hour() < 18)
|
||||
? moment().hour(18)
|
||||
: null
|
||||
// Tomorrow 08:00 AM
|
||||
const tomorrowTime = moment().add(1, 'days').hour(8)
|
||||
// Saturday 08:00 AM (or hidden)
|
||||
const thisWeekendTime = (currentDateTime.day() !== 6 && currentDateTime.day() !== 0)
|
||||
? moment().day(6).hour(8)
|
||||
: null
|
||||
// Next Monday 08:00 AM
|
||||
const nextWeekTime = moment().add(1, 'weeks').day(1).hour(8)
|
||||
return [
|
||||
{
|
||||
key: 'laterToday',
|
||||
timestamp: this.getTimestamp(laterTodayTime),
|
||||
label: t('deck', 'Later today – {timeLocale}', { timeLocale: laterTodayTime?.format('LT') }),
|
||||
ariaLabel: t('deck', 'Set due date for later today'),
|
||||
},
|
||||
{
|
||||
key: 'tomorrow',
|
||||
timestamp: this.getTimestamp(tomorrowTime),
|
||||
label: t('deck', 'Tomorrow – {timeLocale}', { timeLocale: tomorrowTime?.format('ddd LT') }),
|
||||
ariaLabel: t('deck', 'Set due date for tomorrow'),
|
||||
},
|
||||
{
|
||||
key: 'thisWeekend',
|
||||
timestamp: this.getTimestamp(thisWeekendTime),
|
||||
label: t('deck', 'This weekend – {timeLocale}', { timeLocale: thisWeekendTime?.format('ddd LT') }),
|
||||
ariaLabel: t('deck', 'Set due date for this weekend'),
|
||||
},
|
||||
{
|
||||
key: 'nextWeek',
|
||||
timestamp: this.getTimestamp(nextWeekTime),
|
||||
label: t('deck', 'Next week – {timeLocale}', { timeLocale: nextWeekTime?.format('ddd LT') }),
|
||||
ariaLabel: t('deck', 'Set due date for next week'),
|
||||
},
|
||||
].filter(option => option.timestamp !== null)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
initDate() {
|
||||
if (this.duedate === null) {
|
||||
// We initialize empty dates with a time once clicked to make picking a day easier
|
||||
const now = new Date()
|
||||
now.setDate(now.getDate() + 1)
|
||||
now.setHours(8)
|
||||
now.setMinutes(0)
|
||||
now.setMilliseconds(0)
|
||||
this.duedate = now
|
||||
}
|
||||
},
|
||||
removeDue() {
|
||||
this.duedate = null
|
||||
},
|
||||
selectShortcut(shortcut) {
|
||||
this.duedate = shortcut.timestamp
|
||||
},
|
||||
getTimestamp(momentObject) {
|
||||
return momentObject?.minute(0).second(0).millisecond(0).toDate() || null
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -90,10 +90,6 @@ new Vue({
|
||||
this.$store.commit('setSearchQuery', '')
|
||||
})
|
||||
|
||||
// FIXME remove this once Nextcloud 20 is minimum required version
|
||||
// eslint-disable-next-line
|
||||
new OCA.Search(this.filter, this.cleanSearch)
|
||||
|
||||
this.interval = setInterval(() => {
|
||||
this.time = Date.now()
|
||||
}, 1000)
|
||||
|
||||
Reference in New Issue
Block a user