Merge pull request #4668 from nextcloud/enh/native-datepicker

This commit is contained in:
Julius Härtl
2023-10-18 17:24:50 +02:00
committed by GitHub
4 changed files with 151 additions and 89 deletions

View File

@@ -1,5 +1,6 @@
import { randUser } from '../utils/index.js' import { randUser } from '../utils/index.js'
import { sampleBoard } from '../utils/sampleBoard' import { sampleBoard } from '../utils/sampleBoard'
import moment from '@nextcloud/moment'
const user = randUser() const user = randUser()
const boardData = sampleBoard() const boardData = sampleBoard()
@@ -177,6 +178,46 @@ describe('Card', function() {
cy.get('#app-sidebar-vue') cy.get('#app-sidebar-vue')
.find('.ProseMirror h1').contains('Hello world writing more text').should('be.visible') .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')
})
}) })
}) })

View File

@@ -35,7 +35,7 @@
@select="assignUserToCard" @select="assignUserToCard"
@remove="removeUserFromCard" /> @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"> <div v-if="projectsEnabled" class="section-wrapper">
<CollectionList v-if="card.id" <CollectionList v-if="card.id"
@@ -85,7 +85,6 @@ export default {
}, },
data() { data() {
return { return {
saving: false,
addedLabelToCard: null, addedLabelToCard: null,
copiedCard: null, copiedCard: null,
locale: getLocale(), locale: getLocale(),
@@ -105,7 +104,6 @@ export default {
this.$store.dispatch('setConfig', { cardDetailsInModal: newValue }) this.$store.dispatch('setConfig', { cardDetailsInModal: newValue })
}, },
}, },
labelsSorted() { labelsSorted() {
return [...this.currentBoard.labels].sort((a, b) => (a.title < b.title) ? -1 : 1) 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) 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) { assignUserToCard(user) {
this.$store.dispatch('assignCardToUser', { this.$store.dispatch('assignCardToUser', {
card: this.copiedCard, 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) { addLabelToCard(newLabel) {
this.copiedCard.labels.push(newLabel) this.copiedCard.labels.push(newLabel)
const data = { const data = {
@@ -205,15 +201,6 @@ export default {
} }
</script> </script>
<style lang="scss" scoped> <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 { .section-wrapper {
display: flex; display: flex;
max-width: 100%; max-width: 100%;
@@ -266,9 +253,3 @@ export default {
z-index: 0; z-index: 0;
} }
</style> </style>
<style>
.mx-datepicker-main.mx-datepicker-popup {
/* above the modal */
z-index: 9999 !important;
}
</style>

View File

@@ -4,19 +4,42 @@
<Calendar :size="20" /> <Calendar :size="20" />
</div> </div>
<div class="duedate-selector"> <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')" :placeholder="t('deck', 'Set a due date')"
type="datetime" :hide-label="true"
:minute-step="5" type="datetime-local" />
:show-second="false" <NcActions v-if="canEdit"
:lang="lang" :menu-title="!duedate ? t('deck', 'Add due date') : null"
:formatter="format" type="tertiary"
:disabled="!canEdit" data-cy-due-date-actions>
:shortcuts="shortcuts" <template v-if="!duedate" #icon>
:append-to-body="true" <Plus :size="20" />
confirm /> </template>
<NcActions v-if="canEdit"> <NcActionButton v-for="shortcut in reminderOptions"
<NcActionButton v-if="duedate" icon="icon-delete" @click="removeDue()"> :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') }} {{ t('deck', 'Remove due date') }}
</NcActionButton> </NcActionButton>
</NcActions> </NcActions>
@@ -26,17 +49,21 @@
<script> <script>
import { defineComponent } from 'vue' 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 { 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 Calendar from 'vue-material-design-icons/Calendar.vue'
import moment from '@nextcloud/moment'
export default defineComponent({ export default defineComponent({
name: 'DueDateSelector', name: 'DueDateSelector',
components: { components: {
Plus,
Calendar, Calendar,
NcActions, NcActions,
NcActionButton, NcActionButton,
NcDatetimePicker, NcActionSeparator,
NcDateTimePickerNative,
}, },
props: { props: {
card: { card: {
@@ -64,64 +91,81 @@ export default defineComponent({
stringify: this.stringify, stringify: this.stringify,
parse: this.parse, 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: { computed: {
duedate: { duedate: {
get() { get() {
return this.card.duedate ? new Date(this.card.duedate) : null return this.card?.duedate ? new Date(this.card.duedate) : null
}, },
set(val) { 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: { 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() { removeDue() {
this.duedate = null this.duedate = null
}, },
selectShortcut(shortcut) {
this.duedate = shortcut.timestamp
},
getTimestamp(momentObject) {
return momentObject?.minute(0).second(0).millisecond(0).toDate() || null
},
}, },
}) })
</script> </script>

View File

@@ -90,10 +90,6 @@ new Vue({
this.$store.commit('setSearchQuery', '') 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.interval = setInterval(() => {
this.time = Date.now() this.time = Date.now()
}, 1000) }, 1000)