Skip to content

Conditional Logic - #3123

Closed
mickenordin wants to merge 30 commits into
mainfrom
kano-conditional
Closed

mickenordin wants to merge 30 commits into
mainfrom
kano-conditional

Conversation

@mickenordin

@mickenordin mickenordin commented Jan 18, 2026 •

Copy link
Copy Markdown

Closed in favor of #3833


This is an implementation of Conditional Logic. It is not well tested, and I would appreciate review and testing by others to see if it works as intended. I have created a few forms and clicked around to the best of my abilities, but I have implemented the fix for the benefit of others that need it and have no real use case myself to test with.

🖼️ Screenshoots

image image image

@mickenordin
mickenordin force-pushed the kano-conditional branch 2 times, most recently from c7ff7a9 to 7887848 Compare January 18, 2026 18:57
@codecov

codecov Bot commented Jan 18, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 64.76190% with 185 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
lib/Controller/ApiController.php 64.28% 70 Missing ⚠️
lib/Service/SubmissionService.php 67.25% 56 Missing ⚠️
lib/Db/QuestionMapper.php 0.00% 30 Missing ⚠️
lib/Migration/Version050300Date20260118000000.php 0.00% 19 Missing ⚠️
lib/Service/FormsService.php 90.47% 10 Missing ⚠️

📢 Thoughts on this report? Let us know!

@mickenordin
mickenordin force-pushed the kano-conditional branch 7 times, most recently from 2d5f073 to 50a1acd Compare January 18, 2026 20:17
@mickenordin

Copy link
Copy Markdown
Author

Codecov Report

❌ Patch coverage is 56.88073% with 141 lines in your changes missing coverage. Please review.
Files with missing lines Patch % Lines
lib/Service/SubmissionService.php 48.87% 68 Missing ⚠️
lib/Db/QuestionMapper.php 0.00% 39 Missing ⚠️
lib/Migration/Version050300Date20260118000000.php 0.00% 19 Missing ⚠️
lib/Controller/ApiController.php 86.04% 12 Missing ⚠️
lib/Service/FormsService.php 93.47% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

Updated with more tests so threshold is well exceeded

@mickenordin mickenordin changed the title WIP: Conditional Logic Conditional Logic Jan 19, 2026
@mickenordin
mickenordin marked this pull request as ready for review January 19, 2026 11:21
@github-actions

github-actions Bot commented Feb 2, 2026

Copy link
Copy Markdown
Contributor

Hello there,
Thank you so much for taking the time and effort to create a pull request to our Nextcloud project.

We hope that the review process is going smooth and is helpful for you. We want to ensure your pull request is reviewed to your satisfaction. If you have a moment, our community management team would very much appreciate your feedback on your experience with this PR review process.

Your feedback is valuable to us as we continuously strive to improve our community developer experience. Please take a moment to complete our short survey by clicking on the following link: https://cloud.nextcloud.com/apps/forms/s/i9Ago4EQRZ7TWxjfmeEpPkf6

Thank you for contributing to Nextcloud and we hope to hear from you soon!

(If you believe you should not receive this message, you can add yourself to the blocklist.)

@mickenordin
mickenordin force-pushed the kano-conditional branch 3 times, most recently from 5c7d036 to d02bdbc Compare February 15, 2026 12:05
@timedin-de

Copy link
Copy Markdown

Thank you! Will test further. It seems a bit sluggish sometimes it throws an
{"ocs":{"meta":{"status":"failure","statuscode":400,"message":"Invalid extraSettings, will not update."},"data":[]}} but seems to work otherwise.

This is the missing feature i need to convince my non-profit to stop using google forms 🙏

@timedin-de

timedin-de commented May 10, 2026 •

Copy link
Copy Markdown

Hi @mickenordin , as this was stale for 3 Months i started fixing some bugs by myself. I cant push on this branch, should i just post my .patch diffs here?

Problems noticed/fixed:

  • Error Messages while editing
  • design fixes (hiding header and the broken trigger question header)
  • cloning subquestions
  • hiding unused "Clone" on triggerquestion
  • fixing options (three dot menu) on questions
  • fix options and extraSettings on subquestions not safed
  • result view (currently only Detail View)
  • result view Overview
  • export of subquestions
  • reordering/moving/dragging of subquestions
  • subquestion fileupload not shown in results
  • clear not working
  • technical name(?)
  • complex conditions (like number between....) not working
  • show all matching branches (not only first)
  • aria labeled by wrong

Further ideas for this (should be new PR):

  • nested conditionals
  • and/or logic (multiple trigger questions)
  • default case (when nothing matches)

@mickenordin

mickenordin commented May 10, 2026 •

Copy link
Copy Markdown
Author

Yes

Hi @mickenordin , as this was stale for 3 Months i started fixing some bugs by myself. I cant push on this branch, should i just post my .patch diffs here?

Problems noticed/fixed:

* [x]  Error Messages while editing

* [x]   design fixes (hiding header and the broken trigger question header)

* [x]  cloning subquestions

* [x]  hiding unused "Clone" on triggerquestion

* [x]  fixing options (three dot menu) on questions

* [x]  fix options and extraSettings on subquestions not safed

* [x]  result view (currently only Detail View)

* [ ]  result view Overview

* [ ]  export of subquestions

* [ ]  reordering/moving/dragging of subquestions

* [ ]  subquestion fileupload not shown in results

* [ ]  clear not working

* [ ]  complex conditions (like number between....) not working

Further ideas for this (should be new PR):

* [ ]  nested conditionals

Yes, please do. I don't know of a better way since I don't have access to any access settings in the Nextcloud org, even though I was given the ability to branch in their repos myself.

I'll make sure to credit you on the diffs of course.

@Chartman123 Chartman123 added this to the 5.4 milestone May 13, 2026
@timedin-de

Copy link
Copy Markdown

Hey @mickenordin
as I rebased the branch (including changing your commit, to support the new question types etc.) i guess it makes more sense to reset to my branch, if you want.

git remote add timedin git@github.com:timedin-de/forms.git
git fetch timedin
git reset timedin/kano-conditional --hard
git push --force-with-lease --force-if-includes

@mickenordin

Copy link
Copy Markdown
Author

Hey @mickenordin as I rebased the branch (including changing your commit, to support the new question types etc.) i guess it makes more sense to reset to my branch, if you want.

git remote add timedin git@github.com:timedin-de/forms.git
git fetch timedin
git reset timedin/kano-conditional --hard
git push --force-with-lease --force-if-includes

I did, please make sure to sign-off your commits, and perhaps squash them to fewer logical units. You can take a look at the failing tests and see how to fix them.

@AndyScherzinger

AndyScherzinger commented Nov 4, 2026 •

Copy link
Copy Markdown
Member

I rebased this branch on main and added the review fixes as separate commits on top; the original commits are unchanged. Each review thread has a reply naming the commit that fixes it.

Split into a stack

Following @Chartman123's suggestion (#3123 (comment)), the same code is now split into six stacked PRs, each based on the one before - I didn't rework this PR on purpose to keep the/an original copy :

  1. Conditional questions: go to a question based on the answer #3833 radio and dropdown triggers: data model, API, copy, export, results, validation, accessibility, e2e test
  2. Conditional questions: go to a question after checkbox questions #3834 checkboxes as trigger
  3. Conditional questions: go to a question after date and time questions #3835 date and time as trigger
  4. Conditional questions: branches on file upload #3836 file upload as trigger
  5. Conditional questions: go to a question based on the value of a linear scale #3837 linear scale and color as trigger
  6. Conditional questions: branches on short and long text #3838 short and long text as trigger

#3833 contains all commits of this PR, unchanged, plus the review fixes. Its last commit takes out the trigger types that #3834–#3838 add back. Together, the six PRs have the scope of this PR.

The base PR #3833 also:

  • renames the migration to Version050500Date20261104000000, since the app is 5.5 now;
  • drops the indentation of subquestions on phones, where the number (1.1, 1.2) already shows the nesting;
  • corrects docs/DataStructure.md, which listed condition types the server doesn't have.

#3834 also fixes the label of checkbox branches.

@timedin-de's list (#3123 (comment))

Checked on a dev instance:

  • The ticked items hold, including options and extra settings of subquestions set in the editor.
  • "Result view Overview": subquestions were missing from the summary. Fixed in a33f5e9.
  • "Technical name": the trigger's menu offered Technical name, Required and Delete, which changed the conditional question itself. Fixed in a33f5e9; the trigger's menu now only has the options of its own type.
  • Nested conditionals, and/or logic and a default branch are left for follow-up PRs, as suggested there.

#3123 (comment)

Regression from #3123 (comment)

Confirmed fixed. File answers, in subquestions too, are checked against the upload token of the same form. An upload with a wrong token is rejected with 400. testValidateSubmission_rejectsForeignUploadToken covers it.

Found on the way, not caused by this PR

@AndyScherzinger

Copy link
Copy Markdown
Member

To give a bit of an explanation about the PR move and split. Given the discussion around what conditions/types to support. I've split it into the baseline (that seems generally agreed on in terms of supported types - like Google offers) and PRs per extra set, so we can discuss them after have the foundation in and see if there are use cases that justify adding them in addition.

AndyScherzinger added a commit that referenced this pull request Nov 4, 2026
A text trigger activates a branch when the answer equals (short text
only) or contains a text, or matches a regular expression. Regular
expressions use the same /pattern/modifiers format as validationRegex,
are checked when saving, and an invalid one is not saved by the
editor. Matching is limited in subject length and backtracking.

The code comes from the original pull request #3123 by Micke Nordin
and TimedIn, with the review fixes applied.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
AndyScherzinger added a commit that referenced this pull request Nov 4, 2026
A text trigger activates a branch when the answer equals (short text
only) or contains a text, or matches a regular expression. Regular
expressions use the same /pattern/modifiers format as validationRegex,
are checked when saving, and an invalid one is not saved by the
editor. Matching is limited in subject length and backtracking.

The code comes from the original pull request #3123 by Micke Nordin
and TimedIn, with the review fixes applied.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
AndyScherzinger added a commit that referenced this pull request Nov 4, 2026
A text trigger activates a branch when the answer equals (short text
only) or contains a text, or matches a regular expression. Regular
expressions use the same /pattern/modifiers format as validationRegex,
are checked when saving, and an invalid one is not saved by the
editor. Matching is limited in subject length and backtracking.

The code comes from the original pull request #3123 by Micke Nordin
and TimedIn, with the review fixes applied.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
AndyScherzinger added a commit that referenced this pull request Nov 5, 2026
A text trigger activates a branch when the answer equals (short text
only) or contains a text, or matches a regular expression. Regular
expressions use the same /pattern/modifiers format as validationRegex,
are checked when saving, and an invalid one is not saved by the
editor. Matching is limited in subject length and backtracking.

The code comes from the original pull request #3123 by Micke Nordin
and TimedIn, with the review fixes applied.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
@AndyScherzinger

Copy link
Copy Markdown
Member

cc @Chartman123 as a heads-up before the stacked PRs change again.

Following Christian's review question on #3833 ("would it be easier to not have a new question type conditional but to make this part of the props of a 'normal' question?"), I compared the approach with how Google Forms does branching. Result: I'll rework the stack #3833 → #3838 so that conditional logic becomes a property of normal questions instead of a separate question type.

What changes

  • No conditional question type anymore. Radio, dropdown, checkboxes, short/long text, linear scale, color, date, time and file questions can carry branches themselves (extraSettings.branches). triggerType goes away; the question's own type is the trigger.
  • Follow-up questions stay as they are: real question rows with parentQuestionId and branchId, shown inline on the same page when a branch matches. parentQuestionId now points at the trigger question itself.
  • Flat answers: subquestion answers are submitted like any other answer, keyed by their own question id. The {trigger, subQuestions} wrapper goes away. The server derives which questions are visible from the active branches, rejects answers to hidden ones and enforces "required" only for visible ones.
  • The split stays: Conditional questions: go to a question based on the answer #3833 brings the base with radio and dropdown, the following PRs each allow branches on more question types.

What stays

  • All condition types (option combinations, text/regex, value and date ranges, file uploaded), the server-side branch evaluation, and the accessibility work.

Why

The wrapper type is special-cased in about 15 places on the server and in 5 frontend files, and most issues from the last review round came from exactly those special cases (editing responses, export/import, analytics, a duplicated answer-type registry). Doing this before the first merge means no data migration and no API deprecation later.

Not part of this

Google-style sections with "go to section" navigation are a separate feature (#357, #624) and not required for conditionals. They can follow later on their own.

The commits in #3833 → #3838 will be rewritten again; the commit hashes in my earlier replies here will no longer match.

AndyScherzinger added a commit that referenced this pull request Nov 7, 2026
A text question opens a branch when the answer equals (short text only)
or contains a text, or matches a regular expression. Regular
expressions use the same /pattern/modifiers format as validationRegex,
are checked when saving, and an invalid one is not saved by the
editor. Matching is limited in subject length and backtracking.

The code comes from the original pull request #3123 by Micke Nordin
and TimedIn, with the review fixes applied.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
AndyScherzinger added a commit that referenced this pull request Nov 7, 2026
A text question opens a branch when the answer equals (short text only)
or contains a text, or matches a regular expression. Regular
expressions use the same /pattern/modifiers format as validationRegex,
are checked when saving, and an invalid one is not saved by the
editor. Matching is limited in subject length and backtracking.

The code comes from the original pull request #3123 by Micke Nordin
and TimedIn, with the review fixes applied.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
AndyScherzinger added a commit that referenced this pull request Nov 7, 2026
A text question opens a branch when the answer equals (short text only)
or contains a text, or matches a regular expression. Regular
expressions use the same /pattern/modifiers format as validationRegex,
are checked when saving, and an invalid one is not saved by the
editor. Matching is limited in subject length and backtracking.

The code comes from the original pull request #3123 by Micke Nordin
and TimedIn, with the review fixes applied.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
AndyScherzinger added a commit that referenced this pull request Nov 8, 2026
A text question opens a branch when the answer equals (short text only)
or contains a text, or matches a regular expression. Regular
expressions use the same /pattern/modifiers format as validationRegex,
are checked when saving, and an invalid one is not saved by the
editor. Matching is limited in subject length and backtracking.

The code comes from the original pull request #3123 by Micke Nordin
and TimedIn, with the review fixes applied.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
@Chartman123 Chartman123 added Do not merge Don't merge, as it will break master (e.g. due to pending dependency) and removed 3. to review Waiting for reviews labels Nov 9, 2026
@Chartman123 Chartman123 removed this from the 5.5 milestone Nov 9, 2026
@Chartman123 Chartman123 closed this Nov 9, 2026
AndyScherzinger added a commit that referenced this pull request Nov 9, 2026
A text question opens a branch when the answer equals (short text only)
or contains a text, or matches a regular expression. Regular
expressions use the same /pattern/modifiers format as validationRegex,
are checked when saving, and an invalid one is not saved by the
editor. Matching is limited in subject length and backtracking.

The code comes from the original pull request #3123 by Micke Nordin
and TimedIn, with the review fixes applied.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
AndyScherzinger added a commit that referenced this pull request Nov 10, 2026
Each value of a linear scale gets a row with the same select as on
options, the values at the ends with their labels, e.g. "1 - Strongly
disagree". The jumps are stored in extraSettings.goTo as
{value, target}. A value is matched as a whole number, so "04" counts
as 4 as it does for the answer itself. Values outside the scale never
match, so changing the bounds needs no cleanup.

Based on the linear scale support of #3123 by TimedIn.

Co-Authored-By: TimedIn <git@timedin.net>
Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
AndyScherzinger added a commit that referenced this pull request Nov 10, 2026
Each value of a linear scale gets a row with the same select as on
options, the values at the ends with their labels, e.g. "1 - Strongly
disagree". The jumps are stored in extraSettings.goTo as
{value, target}. A value is matched as a whole number, so "04" counts
as 4 as it does for the answer itself. Values outside the scale never
match, so changing the bounds needs no cleanup.

Based on the linear scale support of #3123 by TimedIn.

Co-Authored-By: TimedIn <git@timedin.net>
Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
AndyScherzinger added a commit that referenced this pull request Nov 10, 2026
Each value of a linear scale gets a row with the same select as on
options, the values at the ends with their labels, e.g. "1 - Strongly
disagree". The jumps are stored in extraSettings.goTo as
{value, target}. A value is matched as a whole number, so "04" counts
as 4 as it does for the answer itself. Values outside the scale never
match, so changing the bounds needs no cleanup.

Based on the linear scale support of #3123 by TimedIn.

Co-Authored-By: TimedIn <git@timedin.net>
Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
AndyScherzinger added a commit that referenced this pull request Nov 10, 2026
Each value of a linear scale gets a row with the same select as on
options, the values at the ends with their labels, e.g. "1 - Strongly
disagree". The jumps are stored in extraSettings.goTo as
{value, target}. A value is matched as a whole number, so "04" counts
as 4 as it does for the answer itself. Values outside the scale never
match, so changing the bounds needs no cleanup.

Based on the linear scale support of #3123 by TimedIn.

Co-Authored-By: TimedIn <git@timedin.net>
Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
AndyScherzinger added a commit that referenced this pull request Nov 10, 2026
Each value of a linear scale gets a row with the same select as on
options, the values at the ends with their labels, e.g. "1 - Strongly
disagree". The jumps are stored in extraSettings.goTo as
{value, target}. A value is matched as a whole number, so "04" counts
as 4 as it does for the answer itself. Values outside the scale never
match, so changing the bounds needs no cleanup.

Based on the linear scale support of #3123 by TimedIn.

Co-Authored-By: TimedIn <git@timedin.net>
Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
AndyScherzinger added a commit that referenced this pull request Nov 10, 2026
Each value of a linear scale gets a row with the same select as on
options, the values at the ends with their labels, e.g. "1 - Strongly
disagree". The jumps are stored in extraSettings.goTo as
{value, target}. A value is matched as a whole number, so "04" counts
as 4 as it does for the answer itself. Values outside the scale never
match, so changing the bounds needs no cleanup.

Based on the linear scale support of #3123 by TimedIn.

Co-Authored-By: TimedIn <git@timedin.net>
Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
AndyScherzinger added a commit that referenced this pull request Nov 10, 2026
Each value of a linear scale gets a row with the same select as on
options, the values at the ends with their labels, e.g. "1 - Strongly
disagree". The jumps are stored in extraSettings.goTo as
{value, target}. A value is matched as a whole number, so "04" counts
as 4 as it does for the answer itself. Values outside the scale never
match, so changing the bounds needs no cleanup.

Based on the linear scale support of #3123 by TimedIn.

Co-Authored-By: TimedIn <git@timedin.net>
Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
AndyScherzinger added a commit that referenced this pull request Nov 10, 2026
Each value of a linear scale gets a row with the same select as on
options, the values at the ends with their labels, e.g. "1 - Strongly
disagree". The jumps are stored in extraSettings.goTo as
{value, target}. A value is matched as a whole number, so "04" counts
as 4 as it does for the answer itself. Values outside the scale never
match, so changing the bounds needs no cleanup.

Based on the linear scale support of #3123 by TimedIn.

Co-Authored-By: TimedIn <git@timedin.net>
Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
AndyScherzinger added a commit that referenced this pull request Nov 10, 2026
Each value of a linear scale gets a row with the same select as on
options, the values at the ends with their labels, e.g. "1 - Strongly
disagree". The jumps are stored in extraSettings.goTo as
{value, target}. A value is matched as a whole number, so "04" counts
as 4 as it does for the answer itself. Values outside the scale never
match, so changing the bounds needs no cleanup.

Based on the linear scale support of #3123 by TimedIn.

Co-Authored-By: TimedIn <git@timedin.net>
Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
AndyScherzinger added a commit that referenced this pull request Nov 10, 2026
Each value of a linear scale gets a row with the same select as on
options, the values at the ends with their labels, e.g. "1 - Strongly
disagree". The jumps are stored in extraSettings.goTo as
{value, target}. A value is matched as a whole number, so "04" counts
as 4 as it does for the answer itself. Values outside the scale never
match, so changing the bounds needs no cleanup.

Based on the linear scale support of #3123 by TimedIn.

Co-Authored-By: TimedIn <git@timedin.net>
Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Do not merge Don't merge, as it will break master (e.g. due to pending dependency) enhancement New feature or request feedback-requested

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants