Files
nextcloud-workflow-deck-aut…/src/PersonalSettings.vue
T
Patrick Niebeling d5ce1b3017
Lint info.xml / xml-lint (push) Successful in 22s
Lint PHP / php-lint (8.2) (push) Successful in 52s
Lint PHP / php-lint (8.4) (push) Successful in 1m1s
PHPUnit / unit-tests (push) Successful in 42s
Build release artifact / package (push) Failing after 1m46s
Lint PHP / php-lint (8.3) (push) Successful in 45s
Fix @nextcloud/vue component imports for v9 exports map
v9 restricts subpath imports via package.json "exports" to
@nextcloud/vue/components/Xxx; the old dist/Components/Xxx.js deep
import path used throughout PersonalSettings.vue is no longer
resolvable under Node's ESM export map and failed the build.
2026-08-13 11:39:47 +02:00

370 lines
9.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<NcSettingsSection
name="Deck Workflow-Automatisierung"
description="Verschiebt überfällige Karten aus einem Deck-Stapel automatisch in einen anderen Stapel. Ein Hintergrundjob prüft die Regeln alle paar Minuten.">
<NcNoteCard v-if="loadError" type="error">
{{ loadError }}
</NcNoteCard>
<div v-if="loading" class="wfda-loading">
<NcLoadingIcon :size="32" />
</div>
<template v-else>
<table v-if="workflows.length" class="wfda-table">
<thead>
<tr>
<th>Titel</th>
<th>Von Stapel</th>
<th>Nach Stapel</th>
<th>Filter</th>
<th>E-Mail</th>
<th>Aktiv</th>
<th />
</tr>
</thead>
<tbody>
<tr v-for="workflow in workflows" :key="workflow.id">
<td>{{ workflow.title }}</td>
<td>{{ stackLabel(workflow.boardId, workflow.sourceStackId) }}</td>
<td>{{ stackLabel(workflow.boardId, workflow.targetStackId) }}</td>
<td>
<span v-if="!workflow.filterUserIds.length && !workflow.filterLabelIds.length"></span>
<span v-else>
<span v-if="workflow.filterUserIds.length">{{ workflow.filterUserIds.length }} Benutzer</span>
<span v-if="workflow.filterUserIds.length && workflow.filterLabelIds.length">, </span>
<span v-if="workflow.filterLabelIds.length">{{ workflow.filterLabelIds.length }} Label</span>
</span>
</td>
<td>{{ workflow.notifyEmail ? 'Ja' : 'Nein' }}</td>
<td>{{ workflow.enabled ? 'Ja' : 'Nein' }}</td>
<td class="wfda-actions">
<NcButton type="tertiary" @click="editWorkflow(workflow)">
Bearbeiten
</NcButton>
<NcButton type="tertiary" @click="removeWorkflow(workflow)">
Löschen
</NcButton>
</td>
</tr>
</tbody>
</table>
<p v-else class="wfda-empty">
Noch keine Workflows angelegt.
</p>
<NcButton v-if="!showForm" type="primary" class="wfda-add-button" @click="startCreate">
Workflow hinzufügen
</NcButton>
<form v-if="showForm" class="wfda-form" @submit.prevent="save">
<NcTextField
:value.sync="form.title"
label="Titel"
required />
<NcSelect
v-model="form.boardId"
label="Board"
:options="boardOptions"
:reduce="option => option.value"
placeholder="Board wählen"
@update:model-value="onBoardChange" />
<NcSelect
v-model="form.sourceStackId"
label="Quell-Stapel"
:options="stackOptions"
:reduce="option => option.value"
:disabled="!form.boardId"
placeholder="Quell-Stapel wählen" />
<NcSelect
v-model="form.targetStackId"
label="Ziel-Stapel"
:options="stackOptions"
:reduce="option => option.value"
:disabled="!form.boardId"
placeholder="Ziel-Stapel wählen" />
<NcSelect
v-model="form.filterUserIds"
label="Nur für zugewiesene Benutzer (optional)"
:options="participantOptions"
:reduce="option => option.value"
:disabled="!form.boardId"
multiple
placeholder="Alle Benutzer" />
<NcSelect
v-model="form.filterLabelIds"
label="Nur mit Label (optional)"
:options="labelOptions"
:reduce="option => option.value"
:disabled="!form.boardId"
multiple
placeholder="Alle Labels" />
<NcCheckboxRadioSwitch :checked.sync="form.notifyEmail">
Benachrichtigungs-E-Mail an mich senden
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :checked.sync="form.enabled">
Workflow aktiv
</NcCheckboxRadioSwitch>
<NcNoteCard v-if="formError" type="error">
{{ formError }}
</NcNoteCard>
<div class="wfda-form-actions">
<NcButton type="primary" native-type="submit" :disabled="saving">
Speichern
</NcButton>
<NcButton type="tertiary" :disabled="saving" @click="cancelForm">
Abbrechen
</NcButton>
</div>
</form>
</template>
</NcSettingsSection>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcTextField from '@nextcloud/vue/components/NcTextField'
import NcSelect from '@nextcloud/vue/components/NcSelect'
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import {
createWorkflow,
deleteWorkflow,
fetchBoards,
fetchLabels,
fetchParticipants,
fetchStacks,
fetchWorkflows,
updateWorkflow,
} from './api.js'
const loading = ref(true)
const loadError = ref('')
const workflows = ref([])
const boards = ref([])
const stacksByBoard = reactive({})
const showForm = ref(false)
const saving = ref(false)
const formError = ref('')
const editingId = ref(null)
const stackOptions = ref([])
const labelOptions = ref([])
const participantOptions = ref([])
const boardOptions = computed(() => boards.value.map((board) => ({ value: board.id, label: board.title })))
const form = reactive(emptyForm())
function emptyForm() {
return {
title: '',
boardId: null,
sourceStackId: null,
targetStackId: null,
filterUserIds: [],
filterLabelIds: [],
notifyEmail: false,
enabled: true,
}
}
function stackLabel(boardId, stackId) {
const stacks = stacksByBoard[boardId]
if (!stacks) {
return `#${stackId}`
}
const stack = stacks.find((entry) => entry.id === stackId)
return stack ? stack.title : `#${stackId}`
}
async function loadStacksFor(boardId) {
if (!boardId || stacksByBoard[boardId]) {
return
}
try {
stacksByBoard[boardId] = await fetchStacks(boardId)
} catch (e) {
stacksByBoard[boardId] = []
}
}
async function loadAll() {
loading.value = true
loadError.value = ''
try {
const [loadedWorkflows, loadedBoards] = await Promise.all([fetchWorkflows(), fetchBoards()])
workflows.value = loadedWorkflows
boards.value = loadedBoards
await Promise.all(loadedWorkflows.map((workflow) => loadStacksFor(workflow.boardId)))
} catch (e) {
loadError.value = 'Konnte Workflows oder Deck-Boards nicht laden. Ist die Deck-App aktiviert?'
} finally {
loading.value = false
}
}
async function onBoardChange(boardId) {
form.sourceStackId = null
form.targetStackId = null
form.filterUserIds = []
form.filterLabelIds = []
stackOptions.value = []
labelOptions.value = []
participantOptions.value = []
if (!boardId) {
return
}
const [stacks, labels, participants] = await Promise.all([
fetchStacks(boardId),
fetchLabels(boardId),
fetchParticipants(boardId),
])
stacksByBoard[boardId] = stacks
stackOptions.value = stacks.map((stack) => ({ value: stack.id, label: stack.title }))
labelOptions.value = labels.map((label) => ({ value: label.id, label: label.title }))
participantOptions.value = participants.map((participant) => ({ value: participant.uid, label: participant.displayName }))
}
function startCreate() {
Object.assign(form, emptyForm())
editingId.value = null
formError.value = ''
stackOptions.value = []
labelOptions.value = []
participantOptions.value = []
showForm.value = true
}
async function editWorkflow(workflow) {
Object.assign(form, {
title: workflow.title,
boardId: workflow.boardId,
sourceStackId: workflow.sourceStackId,
targetStackId: workflow.targetStackId,
filterUserIds: [...workflow.filterUserIds],
filterLabelIds: [...workflow.filterLabelIds],
notifyEmail: workflow.notifyEmail,
enabled: workflow.enabled,
})
editingId.value = workflow.id
formError.value = ''
showForm.value = true
await onBoardChange(workflow.boardId)
}
function cancelForm() {
showForm.value = false
formError.value = ''
}
async function save() {
formError.value = ''
if (!form.title.trim()) {
formError.value = 'Bitte einen Titel angeben.'
return
}
if (!form.boardId || !form.sourceStackId || !form.targetStackId) {
formError.value = 'Bitte Board, Quell- und Ziel-Stapel wählen.'
return
}
if (form.sourceStackId === form.targetStackId) {
formError.value = 'Quell- und Ziel-Stapel müssen sich unterscheiden.'
return
}
saving.value = true
try {
if (editingId.value) {
await updateWorkflow(editingId.value, form)
} else {
await createWorkflow(form)
}
showForm.value = false
await loadAll()
} catch (e) {
formError.value = e?.response?.data?.ocs?.meta?.message || 'Speichern fehlgeschlagen.'
} finally {
saving.value = false
}
}
async function removeWorkflow(workflow) {
// eslint-disable-next-line no-alert
if (!window.confirm(`Workflow "${workflow.title}" wirklich löschen?`)) {
return
}
await deleteWorkflow(workflow.id)
await loadAll()
}
onMounted(loadAll)
</script>
<style scoped>
.wfda-loading {
display: flex;
justify-content: center;
padding: 20px;
}
.wfda-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 16px;
}
.wfda-table th,
.wfda-table td {
text-align: left;
padding: 8px 12px;
border-bottom: 1px solid var(--color-border);
}
.wfda-actions {
display: flex;
gap: 4px;
justify-content: flex-end;
}
.wfda-empty {
color: var(--color-text-maxcontrast);
margin-bottom: 16px;
}
.wfda-add-button {
margin-bottom: 16px;
}
.wfda-form {
display: flex;
flex-direction: column;
gap: 12px;
max-width: 480px;
padding: 16px;
border: 1px solid var(--color-border);
border-radius: var(--border-radius-large);
}
.wfda-form-actions {
display: flex;
gap: 8px;
}
</style>