Skip to content

Feature/ranking question type vue3#3262

Open
datapumpernickel wants to merge 5 commits intonextcloud:mainfrom
datapumpernickel:feature/ranking-question-type-vue3
Open

Feature/ranking question type vue3#3262
datapumpernickel wants to merge 5 commits intonextcloud:mainfrom
datapumpernickel:feature/ranking-question-type-vue3

Conversation

@datapumpernickel
Copy link
Copy Markdown

@datapumpernickel datapumpernickel commented Apr 1, 2026

As proposed in #1278: Add Ranking question type

Refactored to Vue3, after closing: #3247

Rebased to main after closing: #3248

This adds a new question type that lets respondents rank options by dragging them into order.

Results are scored using Borda count — first place gets the most points, last place gets the least. The summary shows each option's total score and average rank with a visual bar, along with a short explanation of how scoring works.

For export, ranking questions get one column per option (like grid questions do), with the rank number as the value. Answers are stored as a JSON array of option IDs, which keeps things consistent with how grid answers are stored.

There's also an optional shuffle setting.

Again disclaimer: heavy use of genAI, I did try to check the code to the best of my abilities and ran it on nextcloud-docker-dev to actually interact with it and export data.

Ok, it is now implemented as a tap-and-drag interface, which allows to set a required-flag or leave it blank. 🎊
Sorry, for the forth-and-back.

See attached video for demonstration: https://github.com/user-attachments/assets/9745e93c-87db-49ae-990f-ed305827deee

Adds a new 'ranking' question type that allows respondents to
drag-and-drop predefined options into their preferred order.
Based on refactor/vue3 branch, using vue-draggable-plus.

Signed-off-by: paul bochtler <65470117+datapumpernickel@users.noreply.github.com>
Signed-off-by: paul bochtler <65470117+datapumpernickel@users.noreply.github.com>
Signed-off-by: paul bochtler <65470117+datapumpernickel@users.noreply.github.com>
…ttest for blank answer

Signed-off-by: paul bochtler <65470117+datapumpernickel@users.noreply.github.com>
…e keyboard menu

Signed-off-by: paul bochtler <65470117+datapumpernickel@users.noreply.github.com>
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 2, 2026

Codecov Report

❌ Patch coverage is 69.23077% with 12 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
lib/Controller/ApiController.php 10.00% 9 Missing ⚠️
lib/Service/FormsService.php 0.00% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Collaborator

@Chartman123 Chartman123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did a first review of your code and added some comments/suggestions.

Regarding the design:

  • I think the current represantation of the unranked/ranked options is too massive.
  • Please try to use CSS variable defined in core for pixel values
  • I don't like that the page "jumps" as soon as all unranked options are selected (same when one of them is removed again from the ranked list
  • Now that I see it, I'm no longer sure if we should really move the drag handle to the end.

For a more profound design review, cc @nextcloud/designers

Comment on lines +1739 to +1754
if ($question['type'] === Constants::ANSWER_TYPE_RANKING) {
if (!$answerArray) {
return;
}

$answerEntity = new Answer();
$answerEntity->setSubmissionId($submissionId);
$answerEntity->setQuestionId($question['id']);

$answerText = json_encode($answerArray);
$answerEntity->setText($answerText);
$this->answerMapper->insert($answerEntity);

return;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This if-branch is exactly the same as for grid questions. Please remove it and adjust the condition above.

import IconGrid from 'vue-material-design-icons/Grid.vue'
import IconNumeric from 'vue-material-design-icons/Numeric.vue'
import IconRadioboxMarked from 'vue-material-design-icons/RadioboxMarked.vue'
import IconReorderHorizontal from 'vue-material-design-icons/ReorderHorizontal.vue'
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use this icon, as Microsoft Forms also uses this.

Suggested change
import IconReorderHorizontal from 'vue-material-design-icons/ReorderHorizontal.vue'
import IconSwapVertical from 'vue-material-design-icons/SwapVertical.vue'


ranking: {
component: markRaw(QuestionRanking),
icon: markRaw(IconReorderHorizontal),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
icon: markRaw(IconReorderHorizontal),
icon: markRaw(IconSwapVertical),

@@ -0,0 +1,454 @@
<!--
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors

</p>

<!-- Ranked list header (to separate from pool) -->
<p v-if="rankedOptions.length > 0" class="ranking-ranked__label">
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<p v-if="rankedOptions.length > 0" class="ranking-ranked__label">
<p v-else class="ranking-ranked__label">

Comment on lines +588 to +592
$sortedRanked = $rankedIds;
$sortedOptions = $optionIds;
sort($sortedRanked);
sort($sortedOptions);
if ($sortedRanked !== $sortedOptions) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$sortedRanked = $rankedIds;
$sortedOptions = $optionIds;
sort($sortedRanked);
sort($sortedOptions);
if ($sortedRanked !== $sortedOptions) {
if (sort($rankedIds) !== sort($optionIds)) {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants