Send moved-card notices as Nextcloud notifications too
Build package / xml-lint (push) Successful in 33s
Build package / php-lint (8.5) (push) Successful in 1m48s
Build package / php-lint (8.4) (push) Successful in 1m50s
Build package / unit-tests (push) Successful in 1m52s
Build package / package (push) Successful in 1m38s

Each workflow now has a notify channel (mail, notification or both) next
to its recipient mode. Notifications go out per card through
OCP\Notification\IManager, so they show up in the bell and as push on the
mobile and desktop clients. Without the notifications app nothing is sent.
The one-off "workflow disabled" notice is sent as a notification as well.

Existing workflows keep mail (migration Version1004). Bumps to 35.3.0 so
the migration runs.
This commit is contained in:
Patrick Niebeling committed 2026-09-30 12:31:01 +02:00
1 parent 35903772eb
commit 7612344726
19 files changed
+585 -70

No files matched your search

+55 -11
View File
@@ -18,7 +18,7 @@
<th>Titel</th>
<th>Von Stapel</th>
<th>Nach Stapel</th>
<th>E-Mail</th>
<th>Benachrichtigung</th>
<th>Aktiv</th>
<th />
</tr>
@@ -34,7 +34,7 @@
</td>
<td>{{ stackLabel(workflow.boardId, workflow.sourceStackId) }}</td>
<td>{{ stackLabel(workflow.boardId, workflow.targetStackId) }}</td>
<td>{{ notifyLabel(workflow.notifyMode) }}</td>
<td>{{ notifyLabel(workflow) }}</td>
<td>{{ yesNo(workflow.enabled) }}</td>
<td class="wfda-actions">
<NcButton variant="tertiary" @click="editWorkflow(workflow)">
@@ -89,7 +89,7 @@
placeholder="Ziel-Stapel wählen" />
<fieldset class="wfda-notify">
<legend>E-Mail bei verschobenen Karten</legend>
<legend>Benachrichtigung bei verschobenen Karten</legend>
<NcCheckboxRadioSwitch
v-for="option in notifyOptions"
:key="option.value"
@@ -100,13 +100,34 @@
{{ option.label }}
</NcCheckboxRadioSwitch>
<p v-if="form.notifyMode === 'board'" class="wfda-hint">
Jedes Board-Mitglied bekommt die E-Mail entsprechend seiner eigenen
Deck-Einstellung „Benachrichtigungen“ für dieses Board: bei „Alle Karten“ immer,
Jedes Board-Mitglied wird entsprechend seiner eigenen
Deck-Einstellung „Benachrichtigungen“ für dieses Board informiert: bei „Alle Karten“ immer,
bei „Zugewiesene Karten“ nur für Karten, denen es zugewiesen ist, bei „Keine“ nie.
Mehrere Karten eines Laufs werden zu einer E-Mail zusammengefasst.
</p>
</fieldset>
<fieldset v-if="form.notifyMode !== 'off'" class="wfda-notify">
<legend>Benachrichtigen per</legend>
<NcCheckboxRadioSwitch
v-for="option in channelOptions"
:key="option.value"
v-model="form.notifyChannel"
type="radio"
name="wfda-notify-channel"
:value="option.value">
{{ option.label }}
</NcCheckboxRadioSwitch>
<p class="wfda-hint">
Mehrere Karten eines Laufs werden zu einer E-Mail zusammengefasst.
Nextcloud-Benachrichtigungen erscheinen pro Karte in der Glocke und als Push-Nachricht
in den Nextcloud-Apps auf Handy und Desktop.
</p>
<NcNoteCard v-if="!props.notificationsAvailable && form.notifyChannel !== 'mail'" type="warning">
Die App „Benachrichtigungen“ ist auf dieser Nextcloud nicht aktiviert.
Nextcloud-Benachrichtigungen werden deshalb nicht verschickt.
</NcNoteCard>
</fieldset>
<NcCheckboxRadioSwitch v-model="form.enabled">
Workflow aktiv
</NcCheckboxRadioSwitch>
@@ -146,6 +167,15 @@ import {
updateWorkflow,
} from './api.js'
const props = defineProps({
// Whether the notifications app is enabled for the current user, from
// the data attribute set in templates/settings/personal.php.
notificationsAvailable: {
type: Boolean,
default: true,
},
})
const loading = ref(true)
const loadError = ref('')
const workflows = ref([])
@@ -173,7 +203,7 @@ const boardOptions = computed(() => boards.value
/**
* Alphabetical by the columns as displayed, left to right: board, title,
* source stack, target stack, e-mail, active.
* source stack, target stack, notification, active.
*/
const sortedWorkflows = computed(() => {
const sortKeys = (workflow) => [
@@ -181,7 +211,7 @@ const sortedWorkflows = computed(() => {
workflow.title,
stackLabel(workflow.boardId, workflow.sourceStackId),
stackLabel(workflow.boardId, workflow.targetStackId),
notifyLabel(workflow.notifyMode),
notifyLabel(workflow),
yesNo(workflow.enabled),
]
return workflows.value
@@ -245,19 +275,32 @@ function emptyForm() {
sourceStackId: null,
targetStackId: null,
notifyMode: 'off',
notifyChannel: 'mail',
enabled: true,
}
}
// Values match NotificationPolicy::MODES on the server.
const notifyOptions = [
{ value: 'off', label: 'Keine E-Mail', short: 'Keine' },
{ value: 'off', label: 'Keine Benachrichtigung', short: 'Keine' },
{ value: 'owner', label: 'Nur an mich', short: 'Nur ich' },
{ value: 'board', label: 'An Board-Mitglieder laut ihrer Deck-Einstellung', short: 'Board-Mitglieder' },
]
function notifyLabel(mode) {
return notifyOptions.find((option) => option.value === mode)?.short ?? mode
// Values match NotificationPolicy::CHANNELS on the server.
const channelOptions = [
{ value: 'mail', label: 'E-Mail', short: 'E-Mail' },
{ value: 'notification', label: 'Nextcloud-Benachrichtigung (Push)', short: 'Push' },
{ value: 'both', label: 'E-Mail und Nextcloud-Benachrichtigung', short: 'E-Mail + Push' },
]
function notifyLabel(workflow) {
const mode = notifyOptions.find((option) => option.value === workflow.notifyMode)?.short ?? workflow.notifyMode
if (workflow.notifyMode === 'off') {
return mode
}
const channel = channelOptions.find((option) => option.value === workflow.notifyChannel)?.short ?? workflow.notifyChannel
return `${mode} (${channel})`
}
function boardLabel(boardId) {
@@ -356,6 +399,7 @@ async function editWorkflow(workflow) {
sourceStackId: workflow.sourceStackId,
targetStackId: workflow.targetStackId,
notifyMode: workflow.notifyMode,
notifyChannel: workflow.notifyChannel || 'mail',
enabled: workflow.enabled,
})
editingId.value = workflow.id
+3 -1
View File
@@ -3,5 +3,7 @@ import PersonalSettings from './PersonalSettings.vue'
const mountPoint = document.getElementById('workflow-deck-automation-personal-settings')
if (mountPoint) {
createApp(PersonalSettings).mount(mountPoint)
createApp(PersonalSettings, {
notificationsAvailable: mountPoint.dataset.notificationsAvailable === '1',
}).mount(mountPoint)
}