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
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.
370 lines
9.2 KiB
Vue
370 lines
9.2 KiB
Vue
<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>
|