Remove the assigned-user and label filters
Build package / php-lint (8.4) (push) Successful in 1m33s
Build package / php-lint (8.5) (push) Successful in 49s
Build package / xml-lint (push) Successful in 13s
Build package / unit-tests (push) Successful in 38s
Build package / package (push) Successful in 1m44s

Workflows now move every overdue card of their source stack. A new
migration drops filter_user_ids and filter_label_ids; workflows that
had a filter keep running unfiltered. The version goes to 35.1.0 so
the migration runs on update.

Removed along with the filters: the labels and participants endpoints
and their Deck lookups, the dead-filter warnings and markers, and
CardService::enrichCards() in the card read. enrichCards() only added
labels, assignees and comment counts, and ran a permission check
against Deck's frozen uid.
This commit is contained in:
Patrick Niebeling committed 2026-09-30 09:55:54 +02:00
1 parent e84123654e
commit 0904292c73
14 files changed
+122 -651

No files matched your search

+11 -121
View File
@@ -18,7 +18,6 @@
<th>Titel</th>
<th>Von Stapel</th>
<th>Nach Stapel</th>
<th>Filter</th>
<th>E-Mail</th>
<th>Aktiv</th>
<th />
@@ -35,7 +34,6 @@
</td>
<td>{{ stackLabel(workflow.boardId, workflow.sourceStackId) }}</td>
<td>{{ stackLabel(workflow.boardId, workflow.targetStackId) }}</td>
<td>{{ filterLabel(workflow) }}</td>
<td>{{ yesNo(workflow.notifyEmail) }}</td>
<td>{{ yesNo(workflow.enabled) }}</td>
<td class="wfda-actions">
@@ -90,24 +88,6 @@
:disabled="!form.boardId"
placeholder="Ziel-Stapel wählen" />
<NcSelect
v-model="form.filterUserIds"
input-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"
input-label="Nur mit Label (optional)"
:options="labelOptions"
:reduce="option => option.value"
:disabled="!form.boardId"
multiple
placeholder="Alle Labels" />
<NcCheckboxRadioSwitch v-model="form.notifyEmail">
Benachrichtigungs-E-Mail an mich senden
</NcCheckboxRadioSwitch>
@@ -146,8 +126,6 @@ import {
createWorkflow,
deleteWorkflow,
fetchBoards,
fetchLabels,
fetchParticipants,
fetchStacks,
fetchWorkflows,
updateWorkflow,
@@ -159,8 +137,6 @@ const workflows = ref([])
const boards = ref([])
// Per board, and `null` whenever the request failed — see loadStacksFor().
const stacksByBoard = reactive({})
const labelsByBoard = reactive({})
const participantsByBoard = reactive({})
const showForm = ref(false)
const saving = ref(false)
@@ -168,9 +144,7 @@ const formError = ref('')
const editingId = ref(null)
const stackOptions = ref([])
const labelOptions = ref([])
const participantOptions = ref([])
// Which board the three lists above currently belong to.
// Which board the list above currently belongs to.
const loadedBoardId = ref(null)
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })
@@ -184,7 +158,7 @@ const boardOptions = computed(() => boards.value
/**
* Alphabetical by the columns as displayed, left to right: board, title,
* source stack, target stack, filter, e-mail, active.
* source stack, target stack, e-mail, active.
*/
const sortedWorkflows = computed(() => {
const sortKeys = (workflow) => [
@@ -192,7 +166,6 @@ const sortedWorkflows = computed(() => {
workflow.title,
stackLabel(workflow.boardId, workflow.sourceStackId),
stackLabel(workflow.boardId, workflow.targetStackId),
filterLabel(workflow),
yesNo(workflow.notifyEmail),
yesNo(workflow.enabled),
]
@@ -217,15 +190,9 @@ const sourceStackOptions = computed(() => stackOptions.value.filter((option) =>
const targetStackOptions = computed(() => stackOptions.value.filter((option) => option.value !== form.sourceStackId))
/**
* Workflows that stopped working, keyed by id. Two different kinds:
*
* - Board or stacks deleted in Deck. The background job switches these off
* by itself, but a row that just stopped working needs to say so instead
* of looking healthy.
* - Every entry of a filter deleted from the board. Nothing switches those
* off — deliberately, since one deleted label out of three is harmless —
* yet a filter that cannot match a single card any more is otherwise
* completely invisible: the workflow keeps running and moves nothing.
* Workflows whose board or stacks were deleted in Deck, keyed by id. The
* background job switches these off by itself, but a row that just stopped
* working needs to say so instead of looking healthy.
*/
const workflowIssues = computed(() => {
const issues = {}
@@ -247,39 +214,13 @@ const workflowIssues = computed(() => {
}
if (missing.length) {
issues[workflow.id] = `${missing.join(' und ')} nicht mehr vorhanden`
continue
}
}
const dead = []
if (isDeadFilter(workflow.filterLabelIds, labelsByBoard[workflow.boardId], 'id')) {
dead.push('Label-Filter')
}
if (isDeadFilter(workflow.filterUserIds, participantsByBoard[workflow.boardId], 'uid')) {
dead.push('Benutzer-Filter')
}
if (dead.length > 1) {
issues[workflow.id] = `${dead.join(' und ')} treffen keine Karte mehr`
} else if (dead.length) {
issues[workflow.id] = `${dead[0]} trifft keine Karte mehr`
}
}
return issues
})
/**
* True only on positive knowledge: the filter has entries, the board's list
* was actually loaded, and not one entry is still on it. A failed request
* (null) says nothing and must not turn a healthy row red.
*/
function isDeadFilter(selected, available, key) {
if (!selected.length || !available) {
return false
}
return !selected.some((value) => available.some((entry) => entry[key] === value))
}
const form = reactive(emptyForm())
function emptyForm() {
@@ -288,8 +229,6 @@ function emptyForm() {
boardId: null,
sourceStackId: null,
targetStackId: null,
filterUserIds: [],
filterLabelIds: [],
notifyEmail: false,
enabled: true,
}
@@ -300,17 +239,6 @@ function boardLabel(boardId) {
return board ? board.title : `#${boardId}`
}
function filterLabel(workflow) {
const parts = []
if (workflow.filterUserIds.length) {
parts.push(`${workflow.filterUserIds.length} Benutzer`)
}
if (workflow.filterLabelIds.length) {
parts.push(`${workflow.filterLabelIds.length} Label`)
}
return parts.length ? parts.join(', ') : '–'
}
function yesNo(value) {
return value ? 'Ja' : 'Nein'
}
@@ -334,23 +262,6 @@ async function loadStacksFor(boardId) {
stacksByBoard[boardId] = await fetchStacks(boardId).catch(() => null)
}
/**
* Labels and participants of a board — needed only to judge whether a stored
* filter can still match anything, so this is fetched just for the boards of
* workflows that actually use filters.
*/
async function loadFiltersFor(boardId) {
if (!boardId || labelsByBoard[boardId]) {
return
}
const [labels, participants] = await Promise.all([
fetchLabels(boardId).catch(() => null),
fetchParticipants(boardId).catch(() => null),
])
labelsByBoard[boardId] = labels
participantsByBoard[boardId] = participants
}
async function loadAll() {
loading.value = true
loadError.value = ''
@@ -358,13 +269,7 @@ async function loadAll() {
const [loadedWorkflows, loadedBoards] = await Promise.all([fetchWorkflows(), fetchBoards()])
workflows.value = loadedWorkflows
boards.value = loadedBoards
await Promise.all(loadedWorkflows.flatMap((workflow) => {
const pending = [loadStacksFor(workflow.boardId)]
if (workflow.filterLabelIds.length || workflow.filterUserIds.length) {
pending.push(loadFiltersFor(workflow.boardId))
}
return pending
}))
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 {
@@ -373,36 +278,25 @@ async function loadAll() {
}
/**
* Fills the stack/label/user dropdowns for a board without touching the
* current selection — that separation matters: editing a workflow has to
* load the same options but must keep the stored stacks and filters.
* Fills the stack dropdowns for a board without touching the current
* selection — that separation matters: editing a workflow has to load the
* same options but must keep the stored stacks.
*/
async function loadBoardOptions(boardId) {
stackOptions.value = []
labelOptions.value = []
participantOptions.value = []
loadedBoardId.value = boardId ?? null
if (!boardId) {
return
}
// One failing endpoint must not take the other two dropdowns down with it.
const [stacks, labels, participants] = await Promise.all([
fetchStacks(boardId).catch(() => null),
fetchLabels(boardId).catch(() => null),
fetchParticipants(boardId).catch(() => null),
])
const stacks = await fetchStacks(boardId).catch(() => null)
// Feed the overview's caches too, so its red markers agree with what the
// Feed the overview's cache too, so its red markers agree with what the
// form just loaded. `null` propagates on purpose: it means "unknown".
stacksByBoard[boardId] = stacks
labelsByBoard[boardId] = labels
participantsByBoard[boardId] = participants
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 }))
}
/**
@@ -417,8 +311,6 @@ async function onBoardChange(boardId) {
form.sourceStackId = null
form.targetStackId = null
form.filterUserIds = []
form.filterLabelIds = []
await loadBoardOptions(boardId)
}
@@ -437,8 +329,6 @@ async function editWorkflow(workflow) {
boardId: workflow.boardId,
sourceStackId: workflow.sourceStackId,
targetStackId: workflow.targetStackId,
filterUserIds: [...workflow.filterUserIds],
filterLabelIds: [...workflow.filterLabelIds],
notifyEmail: workflow.notifyEmail,
enabled: workflow.enabled,
})
-10
View File
@@ -33,13 +33,3 @@ export async function fetchStacks(boardId) {
const { data } = await axios.get(ocsUrl(`/boards/${boardId}/stacks`))
return data.ocs.data
}
export async function fetchLabels(boardId) {
const { data } = await axios.get(ocsUrl(`/boards/${boardId}/labels`))
return data.ocs.data
}
export async function fetchParticipants(boardId) {
const { data } = await axios.get(ocsUrl(`/boards/${boardId}/participants`))
return data.ocs.data
}