Skip to content

Commit 3b2a306

Browse files
authored
Merge pull request #56694 from nextcloud/refactor/files_reminders-vue3
2 parents 8eff653 + 7cafbb0 commit 3b2a306

File tree

106 files changed

+411
-1058
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

106 files changed

+411
-1058
lines changed

apps/files_reminders/lib/Listener/LoadAdditionalScriptsListener.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public function handle(Event $event): void {
3636
return;
3737
}
3838

39+
Util::addStyle(Application::APP_ID, 'init');
3940
Util::addInitScript(Application::APP_ID, 'init');
4041
}
4142
}

apps/files_reminders/src/components/SetCustomReminderModal.vue

Lines changed: 111 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,118 @@
33
- SPDX-License-Identifier: AGPL-3.0-or-later
44
-->
55

6+
<script setup lang="ts">
7+
import type { INode } from '@nextcloud/files'
8+
9+
import { showError, showSuccess } from '@nextcloud/dialogs'
10+
import { emit as emitEventBus } from '@nextcloud/event-bus'
11+
import { t } from '@nextcloud/l10n'
12+
import { onBeforeMount, onMounted, ref } from 'vue'
13+
import NcButton from '@nextcloud/vue/components/NcButton'
14+
import NcDateTime from '@nextcloud/vue/components/NcDateTime'
15+
import NcDateTimePickerNative from '@nextcloud/vue/components/NcDateTimePickerNative'
16+
import NcDialog from '@nextcloud/vue/components/NcDialog'
17+
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
18+
import { clearReminder, setReminder } from '../services/reminderService.ts'
19+
import { logger } from '../shared/logger.ts'
20+
import { getInitialCustomDueDate } from '../shared/utils.ts'
21+
22+
const props = defineProps<{
23+
node: INode
24+
}>()
25+
26+
const emit = defineEmits<{
27+
close: [void]
28+
}>()
29+
30+
const hasDueDate = ref(false)
31+
const opened = ref(false)
32+
const isValid = ref(true)
33+
const customDueDate = ref<Date>()
34+
const nowDate = ref(new Date())
35+
36+
onBeforeMount(() => {
37+
const dueDate = props.node.attributes['reminder-due-date']
38+
? new Date(props.node.attributes['reminder-due-date'])
39+
: undefined
40+
41+
hasDueDate.value = Boolean(dueDate)
42+
isValid.value = true
43+
opened.value = true
44+
customDueDate.value = dueDate ?? getInitialCustomDueDate()
45+
nowDate.value = new Date()
46+
})
47+
48+
onMounted(() => {
49+
const input = document.getElementById('set-custom-reminder') as HTMLInputElement
50+
input.focus()
51+
if (!hasDueDate.value) {
52+
input.showPicker()
53+
}
54+
})
55+
56+
/**
57+
* Set the custom reminder
58+
*/
59+
async function setCustom(): Promise<void> {
60+
// Handle input cleared or invalid date
61+
if (!(customDueDate.value instanceof Date) || isNaN(customDueDate.value.getTime())) {
62+
showError(t('files_reminders', 'Please choose a valid date & time'))
63+
return
64+
}
65+
66+
try {
67+
await setReminder(props.node.fileid!, customDueDate.value)
68+
const node = props.node.clone()
69+
node.attributes['reminder-due-date'] = customDueDate.value.toISOString()
70+
emitEventBus('files:node:updated', node)
71+
showSuccess(t('files_reminders', 'Reminder set for "{fileName}"', { fileName: props.node.displayname }))
72+
onClose()
73+
} catch (error) {
74+
logger.error('Failed to set reminder', { error })
75+
showError(t('files_reminders', 'Failed to set reminder'))
76+
}
77+
}
78+
79+
/**
80+
* Clear the reminder
81+
*/
82+
async function clear(): Promise<void> {
83+
try {
84+
await clearReminder(props.node.fileid!)
85+
const node = props.node.clone()
86+
node.attributes['reminder-due-date'] = ''
87+
emitEventBus('files:node:updated', node)
88+
showSuccess(t('files_reminders', 'Reminder cleared for "{fileName}"', { fileName: props.node.displayname }))
89+
onClose()
90+
} catch (error) {
91+
logger.error('Failed to clear reminder', { error })
92+
showError(t('files_reminders', 'Failed to clear reminder'))
93+
}
94+
}
95+
96+
/**
97+
* Close the modal
98+
*/
99+
function onClose(): void {
100+
opened.value = false
101+
emit('close')
102+
}
103+
104+
/**
105+
* Validate the input on change
106+
*/
107+
function onInput(): void {
108+
const input = document.getElementById('set-custom-reminder') as HTMLInputElement
109+
isValid.value = input.checkValidity()
110+
}
111+
</script>
112+
6113
<template>
7114
<NcDialog
8115
v-if="opened"
9-
:name="name"
10-
:out-transition="true"
116+
:name="t('files_reminders', `Set reminder for '{fileName}'`, { fileName: node.displayname })"
117+
out-transition
11118
size="small"
12119
close-on-click-outside
13120
@closing="onClose">
@@ -18,13 +125,13 @@
18125
<NcDateTimePickerNative
19126
id="set-custom-reminder"
20127
v-model="customDueDate"
21-
:label="label"
128+
:label="t('files_reminders', 'Reminder at custom date & time')"
22129
:min="nowDate"
23130
:required="true"
24131
type="datetime-local"
25132
@input="onInput" />
26133

27-
<NcNoteCard v-if="isValid" type="info">
134+
<NcNoteCard v-if="isValid && customDueDate" type="info">
28135
{{ t('files_reminders', 'We will remind you of this file') }}
29136
<NcDateTime :timestamp="customDueDate" />
30137
</NcNoteCard>
@@ -56,142 +163,6 @@
56163
</NcDialog>
57164
</template>
58165

59-
<script lang="ts">
60-
import type { Node } from '@nextcloud/files'
61-
62-
import { showError, showSuccess } from '@nextcloud/dialogs'
63-
import { emit } from '@nextcloud/event-bus'
64-
import { translate as t } from '@nextcloud/l10n'
65-
import Vue from 'vue'
66-
import NcButton from '@nextcloud/vue/components/NcButton'
67-
import NcDateTime from '@nextcloud/vue/components/NcDateTime'
68-
import NcDateTimePickerNative from '@nextcloud/vue/components/NcDateTimePickerNative'
69-
import NcDialog from '@nextcloud/vue/components/NcDialog'
70-
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
71-
import { clearReminder, setReminder } from '../services/reminderService.ts'
72-
import { logger } from '../shared/logger.ts'
73-
import { getDateString, getInitialCustomDueDate } from '../shared/utils.ts'
74-
75-
export default Vue.extend({
76-
name: 'SetCustomReminderModal',
77-
78-
components: {
79-
NcButton,
80-
NcDateTime,
81-
NcDateTimePickerNative,
82-
NcDialog,
83-
NcNoteCard,
84-
},
85-
86-
data() {
87-
return {
88-
node: undefined as Node | undefined,
89-
hasDueDate: false,
90-
opened: false,
91-
isValid: true,
92-
93-
customDueDate: null as null | Date,
94-
nowDate: new Date(),
95-
}
96-
},
97-
98-
computed: {
99-
fileId(): number | undefined {
100-
return this.node?.fileid
101-
},
102-
103-
fileName(): string | undefined {
104-
return this.node?.basename
105-
},
106-
107-
name() {
108-
return this.fileName ? t('files_reminders', 'Set reminder for "{fileName}"', { fileName: this.fileName }) : ''
109-
},
110-
111-
label(): string {
112-
return t('files_reminders', 'Reminder at custom date & time')
113-
},
114-
115-
clearAriaLabel(): string {
116-
return t('files_reminders', 'Clear reminder')
117-
},
118-
},
119-
120-
methods: {
121-
t,
122-
getDateString,
123-
124-
/**
125-
* Open the modal to set a custom reminder
126-
* and reset the state.
127-
*
128-
* @param node The node to set a reminder for
129-
*/
130-
open(node: Node): void {
131-
const dueDate = node.attributes['reminder-due-date'] ? new Date(node.attributes['reminder-due-date']) : null
132-
133-
this.node = node
134-
this.hasDueDate = Boolean(dueDate)
135-
this.isValid = true
136-
this.opened = true
137-
this.customDueDate = dueDate ?? getInitialCustomDueDate()
138-
this.nowDate = new Date()
139-
140-
// Focus the input and show the picker after the animation
141-
setTimeout(() => {
142-
const input = document.getElementById('set-custom-reminder') as HTMLInputElement
143-
input.focus()
144-
if (!this.hasDueDate) {
145-
input.showPicker()
146-
}
147-
}, 300)
148-
},
149-
150-
async setCustom(): Promise<void> {
151-
// Handle input cleared or invalid date
152-
if (!(this.customDueDate instanceof Date) || isNaN(this.customDueDate)) {
153-
showError(t('files_reminders', 'Please choose a valid date & time'))
154-
return
155-
}
156-
157-
try {
158-
await setReminder(this.fileId, this.customDueDate)
159-
Vue.set(this.node.attributes, 'reminder-due-date', this.customDueDate.toISOString())
160-
emit('files:node:updated', this.node)
161-
showSuccess(t('files_reminders', 'Reminder set for "{fileName}"', { fileName: this.fileName }))
162-
this.onClose()
163-
} catch (error) {
164-
logger.error('Failed to set reminder', { error })
165-
showError(t('files_reminders', 'Failed to set reminder'))
166-
}
167-
},
168-
169-
async clear(): Promise<void> {
170-
try {
171-
await clearReminder(this.fileId)
172-
Vue.set(this.node.attributes, 'reminder-due-date', '')
173-
emit('files:node:updated', this.node)
174-
showSuccess(t('files_reminders', 'Reminder cleared for "{fileName}"', { fileName: this.fileName }))
175-
this.onClose()
176-
} catch (error) {
177-
logger.error('Failed to clear reminder', { error })
178-
showError(t('files_reminders', 'Failed to clear reminder'))
179-
}
180-
},
181-
182-
onClose(): void {
183-
this.opened = false
184-
this.$emit('close')
185-
},
186-
187-
onInput(): void {
188-
const input = document.getElementById('set-custom-reminder') as HTMLInputElement
189-
this.isValid = input.checkValidity()
190-
},
191-
},
192-
})
193-
</script>
194-
195166
<style lang="scss" scoped>
196167
.custom-reminder-modal {
197168
margin: 0 12px;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { registerFileAction } from '@nextcloud/files'
7+
import { registerDavProperty } from '@nextcloud/files/dav'
8+
import { action as clearAction } from './files_actions/clearReminderAction.ts'
9+
import { action as statusAction } from './files_actions/reminderStatusAction.ts'
10+
import { action as customAction } from './files_actions/setReminderCustomAction.ts'
11+
import { action as menuAction } from './files_actions/setReminderMenuAction.ts'
12+
import { actions as suggestionActions } from './files_actions/setReminderSuggestionActions.ts'
13+
14+
registerDavProperty('nc:reminder-due-date', { nc: 'http://nextcloud.org/ns' })
15+
16+
registerFileAction(statusAction)
17+
registerFileAction(clearAction)
18+
registerFileAction(menuAction)
19+
registerFileAction(customAction)
20+
suggestionActions.forEach((action) => registerFileAction(action))
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import type { View } from '@nextcloud/files'
7+
8+
import { Folder } from '@nextcloud/files'
9+
import { beforeEach, describe, expect, it, vi } from 'vitest'
10+
import { action } from './clearReminderAction.ts'
11+
12+
describe('clearReminderAction', () => {
13+
const folder = new Folder({
14+
owner: 'user',
15+
source: 'https://example.com/remote.php/dav/files/user/folder',
16+
attributes: {
17+
'reminder-due-date': '2024-12-25T10:00:00Z',
18+
},
19+
})
20+
21+
beforeEach(() => vi.resetAllMocks())
22+
23+
it('should be enabled for one node with due date', () => {
24+
expect(action.enabled!([folder], {} as unknown as View)).toBe(true)
25+
})
26+
27+
it('should be disabled with more than one node', () => {
28+
expect(action.enabled!([folder, folder], {} as unknown as View)).toBe(false)
29+
})
30+
31+
it('should be disabled if no due date', () => {
32+
const node = folder.clone()
33+
delete node.attributes['reminder-due-date']
34+
expect(action.enabled!([node], {} as unknown as View)).toBe(false)
35+
})
36+
37+
it('should have title based on due date', () => {
38+
expect(action.title!([folder], {} as unknown as View)).toMatchInlineSnapshot('"Clear reminder – Wednesday, December 25, 2024 at 10:00 AM"')
39+
})
40+
})

0 commit comments

Comments
 (0)