Deck's Entity getters are magic (__call), so method_exists() never saw getDeletedAt() or getArchived(): trashed cards were moved, trashed boards were neither dropped from "my boards" nor detected by findStackIds(), and every board counted as not archived. DeckIntegrationService now checks the properties with property_exists() via isTrashed()/isArchived(). Workflows on archived boards stay enabled but are skipped by the runner (isBoardArchived(), read through BoardMapper) until the board is unarchived, and the settings list marks them. Archived boards are no longer offered in the board dropdown at all; editing a workflow on one clears board and stacks and says why.
24 KiB
CLAUDE.md
Leitfaden für Claude Code (claude.ai/code) in diesem Repository.
Worum es geht
Nextcloud-App workflow_deck_automation (Namespace WorkflowDeckAutomation) für Nextcloud 35 (main, min-version="35" max-version="35"). Jeder Nutzer legt in seinen persönlichen Einstellungen „Workflows“ an, die Deck-Karten von einem Stack in einen anderen verschieben, sobald ihr Fälligkeits- oder Startdatum erreicht ist (trigger_date, pro Workflow), optional mit Mail. Es gibt bewusst keine Kartenfilter: ein Workflow verschiebt jede Karte seines Quell-Stacks, deren Datum erreicht ist. Ein Hintergrund-Job wertet alle aktiven Workflows bei jedem Cron-Lauf aus.
Harte Vorgabe: keine HTTP/OCS-Aufrufe gegen Deck
Alle Lesezugriffe (Boards, Stacks, Karten) und das Verschieben laufen über Decks interne PHP-Klassen (OCA\Deck\Service\*, OCA\Deck\Db\*), in-process per \OCP\Server::get(). Das ist eine ausdrückliche, wiederholte Anforderung des Nutzers – nicht auf IClientService/curl gegen /ocs/... „umbauen“.
Nur lib/Service/DeckIntegrationService.php referenziert OCA\Deck\*. Neue Deck-Daten → diese Klasse erweitern. Jeder Aufruf dort ist abgesichert (class_exists(), IAppManager::isEnabledForUser('deck', …), try/catch → DeckUnavailableException), weil OCA\Deck\* keine stabile, dokumentierte API ist (keine @since-Marker).
Impersonation im Hintergrundjob – Decks eingefrorene $userId
WorkflowRunner gruppiert Workflows nach Owner und impersoniert jeden nacheinander, in zwei Schritten (beide im finally via clearImpersonation() zurückgesetzt):
IUserSession::setUser()+IRootFolder::getUserFolder()– für die wenigen Dienste, die die Session lesen. Nebensächlich.DeckIntegrationService::beginUserContext()– der tragende Teil.
Deck liest für Berechtigungen nie die Session. PermissionService (hinter jeder Prüfung, auch in CardService::reorder()), CardService, BoardService und ActivityManager halten private ?string $userId aus dem userId-Service des Deck-Containers – shared, also einmal pro Prozess aufgelöst. Im Cron ist das nie der impersonierte Nutzer, sondern null (wenn vorher ein Deck-Job lief) oder der erste Owner. null lässt jede Prüfung mit NoPermissionException scheitern.
beginUserContext() überschreibt daher das private Feld per Reflection und endUserContext() stellt es wieder her. PermissionService ist Pflicht: klappt das Pinnen nicht, wirft die Methode und der Runner überspringt den Nutzer komplett. Die anderen drei sind best effort (nur Activity-Zuordnung/Unread-Zähler).
Regel: Bevor ein weiterer OCA\Deck\*-Dienst im Runner genutzt wird, prüfen, ob er eine eingefrorene $userId trägt – der Fehler ist still und nutzerübergreifend. Bevorzugt Mapper (QBMapper ohne Nutzerzustand) oder Methoden mit explizitem uid-Parameter verwenden. Deshalb wird BoardService::getUserBoards() nicht verwendet.
Der Datumsvergleich WorkflowRunner::hasPassed(), TriggerDate, LoopDetector und die Empfängerregel NotificationPolicy::shouldNotify() sind bewusst static und seiteneffektfrei, also ohne Deck unit-testbar (tests/Unit/Service/). Das sind die einzigen getesteten Teile der App. Neue reine Logik in dieser Form halten.
Workflows werden deaktiviert, wenn Board/Stack verschwinden
WorkflowRunner::findBrokenTarget() prüft vor jedem Lauf Board und Stacks; fehlt etwas, wird der Workflow abgeschaltet (enabled = false) und einmalig gemailt. Invarianten:
- Nur positives Wissen deaktiviert.
findStackIds()liefertnullnur bei „Board-Zeile weg“ (DoesNotExistException, DecksNotFoundException), „Board im Papierkorb“ (deleted_at > 0) oder „uid hat keine Leserechte“ – alles andere wirft, und der Runner überspringt den Workflow. Degradiert die Methode auf[]/null, deaktiviert ein Deck-Ausfall alle Workflows der Instanz und mailt alle Nutzer an. findStackIds()fragt nie einen Deck-Dienst, wer fragt. Sie bekommt die uid als Argument und nutzt nurBoardMapper/StackMapperplusPermissionService::getPermissions($boardId, $userId)mit expliziter uid. Nicht zuStackService::findAll($boardId)„vereinfachen“ – genau das prüfte gegen die eingefrorenenull-uid, warfNoPermissionException, wurde als „Board weg“ gelesen und verschickte Deaktivierungsmails für intakte Boards.NoPermissionExceptionbeweist nichts und steht deshalb bewusst nicht inisMissing().- Die Deaktivierungsmail ignoriert
notifyModeundnotifyChannel(die betreffen verschobene Karten) und geht immer nur an den Owner, zusätzlich als Nextcloud-Benachrichtigung, falls die Notifications-App aktiv ist. Sie bleibt einmalig, weilfindAllEnabled()deaktivierte Workflows nicht mehr lädt.
Archivierte Boards lösen das nicht aus (sie bleiben lesbar). Ihre Workflows bleiben aktiv, werden aber übersprungen: runWorkflow() fragt nach findBrokenTarget() DeckIntegrationService::isBoardArchived() (BoardMapper, false bei fehlendem Board, sonst wirft es). Im Dropdown erscheinen archivierte Boards nie, auch nicht beim Bearbeiten (dann leert das Formular Board und Stapel und zeigt einen Hinweis). In der Übersicht werden sie markiert.
Ein Workflow gehört dem Board-Owner, nicht seinem Ersteller
Wird ein Board in Deck übertragen, wandern seine Workflows mit.
- „Meine Boards“ =
listOwnedBoards($uid)→BoardMapper::findAllByOwner($uid)(keine geteilten Boards, keine eingefrorene uid). Diese Mapper-Methode filtert weder Papierkorb noch Archiv: Papierkorb-Boards werden verworfen, archivierte mitarchived: truebehalten (Workflows sichtbar, aber nicht im Dropdown, vonvalidate()abgelehnt, vom Runner übersprungen). Wirft statt[]zurückzugeben –[]würde alle Workflows des Nutzers verstecken. - Auflisten/Bearbeiten/Löschen nur über
board_id IN (listOwnedBoards)(WorkflowMapper::findAllForBoards(),WorkflowController::findOwnWorkflow()).user_idspielt keine Rolle – keineuser_id-Abfragen im Controller wieder einführen. - Anlegen/Speichern validiert gegen dieselbe Liste und setzt
user_idauf den aktuellen Nutzer. - Runner:
followBoardOwner()→findBoardOwner()(BoardMapper::find()) schreibt einen abweichenden Owner inuser_idzurück. Lauf und Mails gehen also an den aktuellen Owner. Ein verschwundenes Board behält den letzten Owner, damitfindBrokenTarget()ihn anmailen kann.
user_id heißt also nur „als wer läuft der Job / wer bekommt Mail“.
Mails über verschobene Karten folgen Decks Benachrichtigungseinstellung
notify_mode (NotificationPolicy::MODES) ersetzt seit Version1002…/Version1003… das frühere notify_email (true → owner):
off: keine Mail.owner: nur der Board-Owner, ohne Rücksicht auf seine Deck-Einstellung (altes Verhalten).board: alle Board-Berechtigten, gefiltert nach deren Deck-Nutzereinstellungdeck/board:<id>:notify-due(off/assigned/all, Defaultassigned).NotificationPolicy::shouldNotify()bildet Decks Regel ausNotificationHelper::sendCardDuedate()nach und ist reine, getestete Logik. Dazu gehört: Der Owner eines ungeteilten Boards wird außer beioffimmer benachrichtigt.DeckIntegrationService::findNotificationRecipients()nutzt nur uid-explizite Bausteine:PermissionService::findUsers($boardId, true)(Owner + ACL, löst Gruppen/Circles auf,trueumgeht den Prozess-Cache),AssignmentMapper::isUserAssigned()(auch Gruppen-/Circle-Zuweisungen) undIConfig.FederatedUserwird übersprungen. Bei jedem Fehler wirft die Methode, und der Runner verschickt dann keine Mail, statt an eine falsch aufgelöste Empfängerliste zu schicken.- Der Runner sammelt verschobene Karten in
$pendingNoticesund verschickt nach der Nutzer-Schleife (also ohne Impersonation) je nachnotify_channeldes Workflows eine Sammelmail pro Empfänger und Lauf und/oder eine Nextcloud-Benachrichtigung pro Karte.
Nextcloud-Benachrichtigungen sind ein zweiter Kanal, keine zweite Empfängerregel
notify_channel (NotificationPolicy::CHANNELS: mail/notification/both, Default und Migrationswert mail, Version1004…) sagt nur, wie die über notify_mode ermittelten Empfänger informiert werden. NotificationPolicy::sendsMail()/sendsNotification() sind reine, getestete Logik. Unbekannte Werte zählen als mail.
NotificationPusherlegt überOCP\Notification\IManagereine Benachrichtigung pro Karte an (Objektcard/Karten-ID, Subjectcard_moved) und räumt vorher permarkProcessed()eine ältere zur selben Karte weg. Gespeichert werden nur Rohwerte (Titel, IDs), keine Texte.Notification\Notifier(perregisterNotifierService()inApplication::register()) baut daraus deutsche Texte und ignoriert$languageCode. Titel sind Nutzereingaben und gehen nur als Rich-Objekte (deck-card,deck-board,highlight) in den Text, nie ins Template, sonst würde ein{…}im Titel als Platzhalter gelesen. Der Notifier fragt Deck nicht: Er läuft beim Abruf durch irgendeinen Nutzer, und Deck-Dienste hätten dort die eingefrorene uid.- Ohne Notifications-App passiert nichts:
NotificationPusher::isAvailableFor()prüftIAppManager::isEnabledForUser('notifications', $user)pro Empfänger und kehrt stumm zurück. Kein Fallback auf Mail, kein Fehler-Log. Die Einstellungsseite bekommt denselben Check alsdata-notifications-availableund zeigt dann einen Hinweis. - Push aufs Handy/Desktop ist Sache der Notifications-App (Push-Proxy bzw.
notify_push), nicht dieser App.
Nur Deutsch, keine Übersetzungen
Die App wird nur intern genutzt und spricht ausschließlich Deutsch. Oberfläche, Controller-Fehlermeldungen (erscheinen im Formular), Sektionsname, Mails und Nextcloud-Benachrichtigungen sind direkt auf Deutsch geschrieben. Es gibt kein IL10N und keinen l10n/-Ordner. Nextclouds eigene Mail-Fußzeile wird per addFooter('', 'de') ebenfalls deutsch, sonst würde sie im Cron der Instanz-Standardsprache folgen. Log-Meldungen bleiben englisch. Neue nutzersichtbare Texte auf Deutsch schreiben, mit „du“ wie im Rest der App.
Fälligkeits- oder Startdatum löst das Verschieben aus
trigger_date (TriggerDate::ALL: due/start, Default und Migrationswert due, Version1005…) wählt pro Workflow, welches Kartendatum der Runner mit „jetzt“ vergleicht. Unbekannte Werte zählen als due. Karten ohne das gewählte Datum bleiben liegen.
startdategibt es erst seit Deck 1.18.Card-Getter sind magisch (Entity::__call),method_exists()sieht sie nicht undgetStartdate()würde erst beim Aufruf werfen. DaherDeckIntegrationService::supportsStartdate()(property_exists). Der Runner liestgetStartdate()nur beistartund überspringt solche Workflows ohne Deaktivierung, wenn Deck das Feld nicht kennt. Der Controller lehntstartdann ab.- Texte:
TriggerDate::describeReason()liefert den Satzteil („ist überfällig“ / „hat ihr Startdatum erreicht“) für Mail und Notifier.NotificationPusherspeicherttriggerDatein den Subject-Parametern. Ältere Benachrichtigungen ohne den Schlüssel gelten alsdue. - Empfänger: Auch Startdatums-Workflows nutzen bei
notify_mode = boardDecksnotify-due-Einstellung. Deck hat keine eigene für das Startdatum.
Keine Schleifen, kein Quelle = Ziel
Jeder aktive Workflow ist eine Kante „Quell-Stack → Ziel-Stack“. LoopDetector (rein, getestet) findet Kreise. „Quelle = Ziel“ ist ein Kreis der Länge 1. Das Auslöse-Datum wird bewusst ignoriert: Bei S1→S2 per Start und S2→S1 per Fälligkeit pendelt jede Karte, bei der beide Daten erreicht sind. Ohne Prüfung würde der Runner sie sogar im selben Lauf hin- und herschieben (jeder Workflow liest seinen Quell-Stack neu), mit einer Benachrichtigung pro Bewegung. Ketten sind erlaubt.
- Controller:
validate()→assertNoLoop()lehnt einen aktiven Workflow ab, dessen Ziel über die anderen aktiven Workflows des Boards zu seiner Quelle zurückführt, und nennt sie. Deaktivierte Workflows zählen nicht. Beim Aktivieren läuft dieselbe Prüfung, weil auch das überupdate()geht. - Runner und Liste: Altbestand kann trotzdem Kreise bilden.
run()überspringt alle Workflows auf einem Kreis (nur Log, keine Deaktivierung, keine Mail), undindex()liefertloop: true, das die Oberfläche rot markiert. - Stack-IDs sind in Deck boardübergreifend eindeutig, deshalb darf der Runner alle Workflows der Instanz in einen Graphen werfen.
Fälligkeit wird als UTC-Zeitpunkt verglichen
Decks duedate und startdate sind DATETIME-Spalten ohne Zeitzone mit UTC-Uhrzeit (Frontend sendet toISOString()). Entity baut daraus new \DateTime($string) in PHPs Standardzone, die Nextcloud auf UTC setzt. WorkflowRunner::asStoredUtc() legt die Uhrzeit trotzdem ausdrücklich auf UTC fest, damit das Ergebnis nicht von dieser Standardzone abhängt. Decks eigene Pfade Card::getDaysUntilDue() (tagesgenau) und CardMapper::findOverdue() (vergleicht mit dem NOW() der Datenbank, also Ortszeit) sind dafür ungeeignet und werden nicht verwendet.
Kartenfilter wurden entfernt
Die früheren Filter (filter_user_ids/filter_label_ids) wurden auf Wunsch entfernt. Version1001Date20260930120000 löscht die Spalten, und gefilterte Workflows laufen seitdem ungefiltert weiter (bewusste Entscheidung). Die Spalten müssen gelöscht bleiben: Entity::fromRow() ruft pro Spalte einen Setter auf, den Workflow nicht mehr hat. Mit entfernt wurden: Labels-/Participants-Endpunkte, listLabels()/listParticipants()/findFilterOptions() und CardService::enrichCards() in getActiveCardsInStack() (lief über BoardService::find() mit eingefrorener uid). Die Karten dort sind also nicht angereichert (keine assignedUsers/Labels).
Der Job hat kein Intervall
RunWorkflowsJob erweitert OCP\BackgroundJob\Job, nicht TimedJob: ein Lauf pro Cron-Durchgang, kein Intervall, keine Konfiguration (der frühere config.php-Schlüssel workflow_deck_automation.interval ist seit v0.2.0 weg). Nicht wieder einführen.
Zeitkritikalität entscheidet, ob der Job überhaupt läuft. CronService::runCli() holt außerhalb des Wartungsfensters (maintenance_window_start … +4 h UTC) nur zeitkritische Jobs (getNext($onlyTimeSensitive = true)). Eine frühere Version war TIME_INSENSITIVE und lief auf Instanzen mit Wartungsfenster nie (last_run = 1970-01-01). Manuelle Tests (background-job:worker '<Klasse>', Ajax/Webcron) umgehen dieses Gate, deshalb wirkt dort alles normal.
Als einfacher Job ist das ausgeschlossen: JobList::add() schreibt die Spalte time_sensitive nicht (Default TIME_SENSITIVE), und nur JobList::setLastRun() senkt sie – ausschließlich für TimedJob && !isTimeSensitive(), ohne Rückweg. Alte, einmal als insensitive gestempelte Zeilen bleiben es (auch nach App-Update). Einmalig reparieren:
UPDATE oc_jobs SET time_sensitive = 0
WHERE class = 'OCA\\WorkflowDeckAutomation\\BackgroundJob\\RunWorkflowsJob';
oder occ background-job:delete <id>, dann occ app:disable workflow_deck_automation && occ app:enable workflow_deck_automation.
Debuggen: occ background-job:list --class '<Klasse>' (ID, letzter Lauf), dann occ background-job:execute <id> --force-execute. background-job:worker erwartet Job-Klassen, keine App-ID.
Registrierung deklarativ statt über Bootstrap
Hintergrundjob und persönliche Einstellungen stehen in appinfo/info.xml (<background-jobs>, <settings><personal>/<personal-section>). IRegistrationContext hat dafür keine Methoden. lib/AppInfo/Application.php ist absichtlich fast leer und registriert nur den Notifier (dafür gibt es keinen info.xml-Eintrag).
Architektur
- UI:
src/PersonalSettings.vue(Vue 3), gemountet austemplates/settings/personal.php(Sektion:lib/Settings/PersonalSection.php+Personal.php) →src/api.js→lib/Controller/WorkflowController.php(OCSController, Routen inappinfo/routes.php,/ocs/v2.php/apps/workflow_deck_automation/api/v1/...). - Speicher:
lib/Db/Workflow.php/WorkflowMapper.php, Tabellewfda_workflows(Migrationen inlib/Migration/). - Automation:
RunWorkflowsJob→WorkflowRunner→DeckIntegrationService(alle Deck-Aufrufe),NotificationMailer(IMailer, an die Account-Adressen der Empfänger, nie an eine eingegebene Adresse) undNotificationPusher(IManager, Texte auslib/Notification/Notifier.php). - Controller (Dropdowns) und Runner (Karten lesen/verschieben) nutzen denselben
DeckIntegrationService. - Doku:
README.mdrichtet sich an Anwender und Admins und enthält keine Klassennamen.DEVELOPMENT.mdist für Entwickler (Aufbau, Datenmodell, Tests, CI, Release, Fehlersuche). Neue Nutzer-Funktionen in der README beschreiben, technische Details inDEVELOPMENT.md. Beide Dateien sind perMakefilevom Paket ausgeschlossen.
Befehle und Verifikation
Lokal gibt es kein PHP und kein npm (Windows, siehe CLAUDE.local.md) – composer/php/npm nicht lokal ausführen. Verifiziert wird über Push/PR und den Gitea-Actions-Lauf (Runner gitea-runner-server03).
composer install
composer run lint # php -l über lib/ und tests/
composer run cs:check # nextcloud/coding-standard; cs:fix korrigiert
composer run test:unit # PHPUnit, nur reine Logik
vendor/bin/phpunit --filter <testName> tests/Unit/Service/WorkflowRunnerTest.php
npm install # nicht `npm ci`: Lockfiles sind absichtlich nicht committet
npm run build # bzw. npm run watch
composer.lock und package-lock.json stehen in .gitignore → CI muss composer install/npm install verwenden (npm ci hat den Build schon einmal gebrochen). Die Deck-Integration lässt sich nur auf einer echten Nextcloud+Deck-Instanz testen.
Release und CI
Versionsschema: <Nextcloud-Major>.<minor>.<patch>. Die erste Zahl entspricht min-/max-version in info.xml und ändert sich nur mit der unterstützten NC-Major (dann minor.patch = 0.0). 0.x-Versionen stammen aus der Zeit davor. Die 34er-Linie existiert nur noch als Tags (v34.*). Kein Branch.
<version>inappinfo/info.xmlundversioninpackage.jsonsynchron erhöhen.- Committen, nach
mainpushen,build-main.ymlmuss grün sein. git tag vX.Y.Z && git push origin vX.Y.Z→ Checks, Frontend-Build,make appstore, Release mitworkflow_deck_automation.tar.gz.
Ist ein Tag-Build fehlgeschlagen und nie ein Artefakt entstanden, den Tag auf den Fix verschieben statt die Version zu erhöhen (git tag -d vX.Y.Z && git tag -a vX.Y.Z -m "..." && git push origin :refs/tags/vX.Y.Z && git push origin vX.Y.Z). Der Job löscht ein vorhandenes Release vorher. Nach einem veröffentlichten Artefakt den Tag nicht mehr verschieben.
.gitea/workflows/build-main.yml ist der einzige Build-/Release-Workflow (Checks php-lint mit PHP 8.4 und 8.5, xml-lint, unit-tests + Job package), damit needs: das Release an grüne Checks koppelt. Nicht wieder aufteilen.
.gitea/workflows/appstore.yml ist nur manuell (workflow_dispatch) und soll so bleiben. Er baut nicht selbst, sondern signiert den Tarball eines vorhandenen v*-Release und meldet dessen Gitea-Download-URL beim App Store an (Secrets APPSTORE_PRIVATE_KEY, APPSTORE_TOKEN). Signiert wird genau die Datei hinter der URL. Nach dem Store-Upload darf der Tag also nicht mehr verschoben werden, sonst passt die Signatur nicht mehr. latest-main wird abgelehnt. Kein appinfo/signature.json (Code-Signing): Das braucht der Store nicht, und Nextcloud prüft es bei Nicht-Core-Apps nur, wenn die Datei existiert.
packagebaut und veröffentlicht in einem Job und braucht nur Node (make appstoreist reinesmkdir/tar/rm).- Bei
pull_requestwirdPublish releaseübersprungen (if: gitea.event_name == 'push') und der Job ist nur ein Build-Check. - Push auf
main→ Pre-Releaselatest-main, Release und Tag werden jedes Mal neu erzeugt (Download-Link zeigt immer den neuesten Build).v*-Tag → normales Release. Dabei wird nie der Tag gelöscht (FlagRECREATE_TAG). - Kein
actions/upload-artifact: Gitea gibt sich als GHES aus,@actions/artifact≥ 2 verweigert dann (GHESNotSupportedError). Veröffentlicht wird percurlgegen die Gitea-Release-API.checkout/setup-nodesind nicht betroffen. - Kein PAT nötig: Auth über das automatische
secrets.GITEA_TOKENmitpermissions: contents: write. Bei 403: prüfen, ob die Token-Berechtigung des Repos/der Org auf Restricted steht (deckeltpermissions:). Das ist eine Einstellung, kein Workflow-Bug.
Frontend-Build-Fallen (alle erst in CI aufgefallen)
vitemuss zum Peer von@nextcloud/vite-configpassen (aktuellvite@^7.3.6), sonstERESOLVE. Beim Update vonvite-configdessenpeerDependencies.viteprüfen."type": "module"inpackage.json, sonst scheitertvite.config.jsam ESM-only-Paket.tsconfig.jsonundtypescriptals devDependency sind Pflicht, obwohl es kein TypeScript gibt:@nextcloud/vite-configimportiert über sein Barrelvite-plugin-dts, dastypescriptals Peer braucht. Ohne das Paket crasht es beim Laden (Cannot read properties of undefined (reading 'useCaseSensitiveFileNames')).typescriptnahe an der Version vonvite-confighalten (aktuell^5.9.3).createAppConfigstellt die App-ID vor jeden Entry-Namen:personal-settings→js/workflow_deck_automation-personal-settings.mjs.Util::addScript()/addStyle()inlib/Settings/Personal.phpbrauchen diesen vollen Namen (Application::APP_ID . '-personal-settings'). Falsch → leere Einstellungsseite, nurCould not find resource …im Log. Umbenennungen invite.config.jsauch dort nachziehen.- Die eigentliche CSS steckt in einem gehashten
*.chunk.css. Die Entry-CSS ist ein ~100-Byte-Stub (createEmptyCSSEntryPoints). Das ist korrekt, also Code-Splitting nicht abschalten.
@nextcloud/vue 9 (Vue 3) – v8-Idiome scheitern still
- Kein
.sync:NcTextField/NcCheckboxRadioSwitch→v-model.:value.synclässt den Wert auf dem Startwert stehen. NcSelect:labelist der Anzeige-Schlüssel der Optionen (vue-select), die Beschriftung istinput-label.label="Board"rendert überallundefined.NcButton:variant= Stil,type= nativer Button-Typ,native-typegibt es nicht mehr. Ein übrig gebliebenestype="primary"wird zusubmit, und Abbrechen schickt das Formular ab.NcSelectsreduceerreicht vue-select nur per Attribut-Fallthrough (funktioniert, ist aber undokumentiert).NcNoteCardnutzt weiterhintype(success|info|warning|error).
Deck-Interna, die man kennen muss
- Relationale Entities: Auf angereicherten Entities liefern
$acl->getParticipant(),$board->getOwner()oder Assignment-Teilnehmer einRelationalObjectstatt einer uid (uid =getPrimaryKey()). Neue Teilnehmer-/Owner-Felder immer überDeckIntegrationService::unwrapUid()laufen lassen. - Die ACL eines Boards enthält nicht den Owner (privates Board = leere ACL). ACL-Einträge können Gruppen (
type === 1) oder Circles sein. StackMapper::findAll()filtertdeleted_at = 0selbst. Stacks haben keinen Archivstatus.- Entity-Getter sind magisch (
Entity::__call, z. B.getDeletedAt(),getArchived(),getStartdate()).method_exists()ist für sie immerfalse. Vorhandensein perproperty_exists()prüfen (DeckIntegrationService::isTrashed()/isArchived()/supportsStartdate()). Mitmethod_exists()wurden früher Karten aus dem Papierkorb verschoben, und Papierkorb- wie Archiv-Boards blieben unerkannt. - Der Deck-Quellcode liegt lokal unter
C:\Users\pani\Documents\git-privat\nextcloud-deckund dient zum Nachprüfen von Interna.