Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public function handle(Event $event): void {
return;
}

Util::addStyle(Application::APP_ID, 'init');
Util::addInitScript(Application::APP_ID, 'init');
}
}
251 changes: 111 additions & 140 deletions apps/files_reminders/src/components/SetCustomReminderModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,118 @@
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

<script setup lang="ts">
import type { INode } from '@nextcloud/files'

import { showError, showSuccess } from '@nextcloud/dialogs'
import { emit as emitEventBus } from '@nextcloud/event-bus'
import { t } from '@nextcloud/l10n'
import { onBeforeMount, onMounted, ref } from 'vue'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcDateTime from '@nextcloud/vue/components/NcDateTime'
import NcDateTimePickerNative from '@nextcloud/vue/components/NcDateTimePickerNative'
import NcDialog from '@nextcloud/vue/components/NcDialog'
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import { clearReminder, setReminder } from '../services/reminderService.ts'
import { logger } from '../shared/logger.ts'
import { getInitialCustomDueDate } from '../shared/utils.ts'

const props = defineProps<{
node: INode
}>()

const emit = defineEmits<{
close: [void]
}>()

const hasDueDate = ref(false)
const opened = ref(false)
const isValid = ref(true)
const customDueDate = ref<Date>()
const nowDate = ref(new Date())

onBeforeMount(() => {
const dueDate = props.node.attributes['reminder-due-date']
? new Date(props.node.attributes['reminder-due-date'])
: undefined

hasDueDate.value = Boolean(dueDate)
isValid.value = true
opened.value = true
customDueDate.value = dueDate ?? getInitialCustomDueDate()
nowDate.value = new Date()
})

onMounted(() => {
const input = document.getElementById('set-custom-reminder') as HTMLInputElement
input.focus()
if (!hasDueDate.value) {
input.showPicker()
}
})

/**
* Set the custom reminder
*/
async function setCustom(): Promise<void> {
// Handle input cleared or invalid date
if (!(customDueDate.value instanceof Date) || isNaN(customDueDate.value.getTime())) {
showError(t('files_reminders', 'Please choose a valid date & time'))
return
}

try {
await setReminder(props.node.fileid!, customDueDate.value)
const node = props.node.clone()
node.attributes['reminder-due-date'] = customDueDate.value.toISOString()
emitEventBus('files:node:updated', node)
showSuccess(t('files_reminders', 'Reminder set for "{fileName}"', { fileName: props.node.displayname }))
onClose()
} catch (error) {
logger.error('Failed to set reminder', { error })
showError(t('files_reminders', 'Failed to set reminder'))
}
}

/**
* Clear the reminder
*/
async function clear(): Promise<void> {
try {
await clearReminder(props.node.fileid!)
const node = props.node.clone()
node.attributes['reminder-due-date'] = ''
emitEventBus('files:node:updated', node)
showSuccess(t('files_reminders', 'Reminder cleared for "{fileName}"', { fileName: props.node.displayname }))
onClose()
} catch (error) {
logger.error('Failed to clear reminder', { error })
showError(t('files_reminders', 'Failed to clear reminder'))
}
}

/**
* Close the modal
*/
function onClose(): void {
opened.value = false
emit('close')
}

/**
* Validate the input on change
*/
function onInput(): void {
const input = document.getElementById('set-custom-reminder') as HTMLInputElement
isValid.value = input.checkValidity()
}
</script>

<template>
<NcDialog
v-if="opened"
:name="name"
:out-transition="true"
:name="t('files_reminders', `Set reminder for '{fileName}'`, { fileName: node.displayname })"
out-transition
size="small"
close-on-click-outside
@closing="onClose">
Expand All @@ -18,13 +125,13 @@
<NcDateTimePickerNative
id="set-custom-reminder"
v-model="customDueDate"
:label="label"
:label="t('files_reminders', 'Reminder at custom date & time')"
:min="nowDate"
:required="true"
type="datetime-local"
@input="onInput" />

<NcNoteCard v-if="isValid" type="info">
<NcNoteCard v-if="isValid && customDueDate" type="info">
{{ t('files_reminders', 'We will remind you of this file') }}
<NcDateTime :timestamp="customDueDate" />
</NcNoteCard>
Expand Down Expand Up @@ -56,142 +163,6 @@
</NcDialog>
</template>

<script lang="ts">
import type { Node } from '@nextcloud/files'

import { showError, showSuccess } from '@nextcloud/dialogs'
import { emit } from '@nextcloud/event-bus'
import { translate as t } from '@nextcloud/l10n'
import Vue from 'vue'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcDateTime from '@nextcloud/vue/components/NcDateTime'
import NcDateTimePickerNative from '@nextcloud/vue/components/NcDateTimePickerNative'
import NcDialog from '@nextcloud/vue/components/NcDialog'
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import { clearReminder, setReminder } from '../services/reminderService.ts'
import { logger } from '../shared/logger.ts'
import { getDateString, getInitialCustomDueDate } from '../shared/utils.ts'

export default Vue.extend({
name: 'SetCustomReminderModal',

components: {
NcButton,
NcDateTime,
NcDateTimePickerNative,
NcDialog,
NcNoteCard,
},

data() {
return {
node: undefined as Node | undefined,
hasDueDate: false,
opened: false,
isValid: true,

customDueDate: null as null | Date,
nowDate: new Date(),
}
},

computed: {
fileId(): number | undefined {
return this.node?.fileid
},

fileName(): string | undefined {
return this.node?.basename
},

name() {
return this.fileName ? t('files_reminders', 'Set reminder for "{fileName}"', { fileName: this.fileName }) : ''
},

label(): string {
return t('files_reminders', 'Reminder at custom date & time')
},

clearAriaLabel(): string {
return t('files_reminders', 'Clear reminder')
},
},

methods: {
t,
getDateString,

/**
* Open the modal to set a custom reminder
* and reset the state.
*
* @param node The node to set a reminder for
*/
open(node: Node): void {
const dueDate = node.attributes['reminder-due-date'] ? new Date(node.attributes['reminder-due-date']) : null

this.node = node
this.hasDueDate = Boolean(dueDate)
this.isValid = true
this.opened = true
this.customDueDate = dueDate ?? getInitialCustomDueDate()
this.nowDate = new Date()

// Focus the input and show the picker after the animation
setTimeout(() => {
const input = document.getElementById('set-custom-reminder') as HTMLInputElement
input.focus()
if (!this.hasDueDate) {
input.showPicker()
}
}, 300)
},

async setCustom(): Promise<void> {
// Handle input cleared or invalid date
if (!(this.customDueDate instanceof Date) || isNaN(this.customDueDate)) {
showError(t('files_reminders', 'Please choose a valid date & time'))
return
}

try {
await setReminder(this.fileId, this.customDueDate)
Vue.set(this.node.attributes, 'reminder-due-date', this.customDueDate.toISOString())
emit('files:node:updated', this.node)
showSuccess(t('files_reminders', 'Reminder set for "{fileName}"', { fileName: this.fileName }))
this.onClose()
} catch (error) {
logger.error('Failed to set reminder', { error })
showError(t('files_reminders', 'Failed to set reminder'))
}
},

async clear(): Promise<void> {
try {
await clearReminder(this.fileId)
Vue.set(this.node.attributes, 'reminder-due-date', '')
emit('files:node:updated', this.node)
showSuccess(t('files_reminders', 'Reminder cleared for "{fileName}"', { fileName: this.fileName }))
this.onClose()
} catch (error) {
logger.error('Failed to clear reminder', { error })
showError(t('files_reminders', 'Failed to clear reminder'))
}
},

onClose(): void {
this.opened = false
this.$emit('close')
},

onInput(): void {
const input = document.getElementById('set-custom-reminder') as HTMLInputElement
this.isValid = input.checkValidity()
},
},
})
</script>

<style lang="scss" scoped>
.custom-reminder-modal {
margin: 0 12px;
Expand Down
20 changes: 20 additions & 0 deletions apps/files_reminders/src/files-init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { registerFileAction } from '@nextcloud/files'
import { registerDavProperty } from '@nextcloud/files/dav'
import { action as clearAction } from './files_actions/clearReminderAction.ts'
import { action as statusAction } from './files_actions/reminderStatusAction.ts'
import { action as customAction } from './files_actions/setReminderCustomAction.ts'
import { action as menuAction } from './files_actions/setReminderMenuAction.ts'
import { actions as suggestionActions } from './files_actions/setReminderSuggestionActions.ts'

registerDavProperty('nc:reminder-due-date', { nc: 'http://nextcloud.org/ns' })

registerFileAction(statusAction)
registerFileAction(clearAction)
registerFileAction(menuAction)
registerFileAction(customAction)
suggestionActions.forEach((action) => registerFileAction(action))
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { View } from '@nextcloud/files'

import { Folder } from '@nextcloud/files'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { action } from './clearReminderAction.ts'

describe('clearReminderAction', () => {
const folder = new Folder({
owner: 'user',
source: 'https://example.com/remote.php/dav/files/user/folder',
attributes: {
'reminder-due-date': '2024-12-25T10:00:00Z',
},
})

beforeEach(() => vi.resetAllMocks())

it('should be enabled for one node with due date', () => {
expect(action.enabled!([folder], {} as unknown as View)).toBe(true)
})

it('should be disabled with more than one node', () => {
expect(action.enabled!([folder, folder], {} as unknown as View)).toBe(false)
})

it('should be disabled if no due date', () => {
const node = folder.clone()
delete node.attributes['reminder-due-date']
expect(action.enabled!([node], {} as unknown as View)).toBe(false)
})

it('should have title based on due date', () => {
expect(action.title!([folder], {} as unknown as View)).toMatchInlineSnapshot('"Clear reminder – Wednesday, December 25, 2024 at 10:00 AM"')
})
})
Loading
Loading