diff --git a/src/components/InteractiveEditor/EditWidget.vue b/src/components/InteractiveEditor/EditWidget.vue index 2b9a62a7e7..3d747850e3 100644 --- a/src/components/InteractiveEditor/EditWidget.vue +++ b/src/components/InteractiveEditor/EditWidget.vue @@ -45,9 +45,36 @@ :options="boolRadioOptions" :initialOption="form.ignoreErrors" /> -

- {{ $t('interactive-editor.edit-widget.options-heading') }} -

+ +
+

Calendar options

+
+ + +
+
+ +

Additional options

({ timeout: '', useProxy: '', ignoreErrors: '', + options: {}, }); /* Coerce a string from a free-form options input back to its likely native type. */ @@ -103,6 +131,55 @@ const coerceValue = (raw) => { return raw; }; +const widgetOptionDefinitions = { + 'nextcloud-calendar': [ + { + name: 'icsUrl', + label: 'Calendar URL', + description: 'Public ICS export URL or shared calendar export URL.', + type: 'text', + }, + { + name: 'calendarId', + label: 'Calendar ID', + description: 'Nextcloud calendar ID for authenticated calendars.', + type: 'text', + }, + { + name: 'username', + label: 'Username', + description: 'Optional. Only required for private Nextcloud calendars that need authentication.', + type: 'text', + }, + { + name: 'password', + label: 'Password', + description: 'Optional. Use only with authenticated Nextcloud calendar access (app password or account password).', + type: 'password', + }, + { + name: 'pastDays', + label: 'Include recent past days', + description: 'Number of past days to show. Set 0 to show only future events.', + type: 'number', + }, + { + name: 'limit', + label: 'Event count', + description: 'Maximum upcoming events to display.', + type: 'number', + }, + { + name: 'taskLimit', + label: 'Task count', + description: 'Maximum tasks to display.', + type: 'number', + }, + ], +}; + +const getWidgetOptionDefinitions = (widgetType) => widgetOptionDefinitions[(widgetType || '').toLowerCase()] || []; + export default { name: 'EditWidget', components: { @@ -127,6 +204,9 @@ export default { }, computed: { allowViewConfig() { return this.$store.getters.permissions.allowViewConfig; }, + currentOptionDefinitions() { + return getWidgetOptionDefinitions(this.form.type); + }, }, /* Populate form before children render so Radio's `initialOption` is set on its first creation. */ created() { @@ -140,11 +220,20 @@ export default { timeout: widget.timeout ?? '', useProxy: widget.useProxy === undefined ? '' : String(widget.useProxy), ignoreErrors: widget.ignoreErrors === undefined ? '' : String(widget.ignoreErrors), + options: Object.fromEntries( + Object.entries(widget.options || {}).map(([key, value]) => [ + key, + typeof value === 'object' ? JSON.stringify(value) : String(value), + ]), + ), }; - this.optionRows = Object.entries(widget.options || {}).map(([key, value]) => ({ - key, - value: typeof value === 'object' ? JSON.stringify(value) : String(value), - })); + const reservedKeys = getWidgetOptionDefinitions(widget.type || this.form.type).map((field) => field.name); + this.optionRows = Object.entries(widget.options || {}) + .filter(([key]) => !reservedKeys.includes(key)) + .map(([key, value]) => ({ + key, + value: typeof value === 'object' ? JSON.stringify(value) : String(value), + })); }, mounted() { this.$modal.show(this.modalName); @@ -171,6 +260,11 @@ export default { if (this.form.ignoreErrors === 'true') widget.ignoreErrors = true; else if (this.form.ignoreErrors === 'false') widget.ignoreErrors = false; const options = {}; + Object.entries(this.form.options || {}).forEach(([key, value]) => { + if (value !== '' && value !== null && value !== undefined) { + options[key] = coerceValue(value); + } + }); this.optionRows.forEach(({ key, value }) => { const k = (key || '').trim(); if (k) options[k] = coerceValue(value); @@ -202,6 +296,11 @@ export default { this.$toast.error('Error saving widget. See Logs.'); } }, + boolToStr(value) { + if (value === true || value === 'true') return 'true'; + if (value === false || value === 'false') return 'false'; + return ''; + }, closeModal() { this.$modal.hide(this.modalName); }, diff --git a/src/components/Widgets/NextcloudCalendar.vue b/src/components/Widgets/NextcloudCalendar.vue new file mode 100644 index 0000000000..dbc8d60d34 --- /dev/null +++ b/src/components/Widgets/NextcloudCalendar.vue @@ -0,0 +1,449 @@ + + + + + diff --git a/src/components/Widgets/WidgetBase.vue b/src/components/Widgets/WidgetBase.vue index 34144e68b8..0c1fcb186e 100644 --- a/src/components/Widgets/WidgetBase.vue +++ b/src/components/Widgets/WidgetBase.vue @@ -113,6 +113,7 @@ const COMPAT = { 'nd-ram-history': 'NdRamHistory', 'news-headlines': 'NewsHeadlines', 'nextcloud-notifications': 'NextcloudNotifications', + 'nextcloud-calendar': 'NextcloudCalendar', 'nextcloud-php-opcache': 'NextcloudPhpOpcache', 'nextcloud-stats': 'NextcloudStats', 'nextcloud-system': 'NextcloudSystem', diff --git a/user-data/conf.yml b/user-data/conf.yml index 01cf86ddd4..b9f12ea550 100644 --- a/user-data/conf.yml +++ b/user-data/conf.yml @@ -56,3 +56,16 @@ sections: options: rssUrl: https://github.com/lissy93/dashy/releases.atom limit: 5 + + - name: Nextcloud + icon: fas fa-calendar-alt + widgets: + - type: nextcloud-calendar + label: Nextcloud Calendar (public) + options: + icsUrl: https://example.com/your-calendar.ics + useProxy: true + pastDays: 0 + showTasks: true + limit: 5 + taskLimit: 20