Skip to content

feat: rating question type - #3637

Open
global-prog wants to merge 2 commits into
nextcloud:mainfrom
global-prog:contrib/rating-question
Open

global-prog wants to merge 2 commits into
nextcloud:mainfrom
global-prog:contrib/rating-question

Conversation

@global-prog

@global-prog global-prog commented Sep 9, 2026

Copy link
Copy Markdown

Closes #2608.

Summary

A compact rating question, with stars, hearts or thumbs up, configurable from 2 to 10 icons (default 5).

A linear scale already covers 1..N, but renders as a row of radio buttons. A rating is the control people expect for "how would you rate this", takes far less width, and reads at a glance in the results.

Accessibility

Built from real radio inputs rather than clickable icons, so:

  • it stays keyboard navigable, and each option is announced with the value it selects (3 of 5) rather than as an unnamed radio
  • the hit area is a full --default-clickable-area square while the icon itself stays small — an icon-sized target is awkward to hit on a phone
  • the hover animation is dropped under prefers-reduced-motion
  • the icons wrap, since ten of them plus the clear button do not fit one line on a narrow screen

Storage and validation

The answer is stored as the plain number, so results and CSV export need no special handling.

It is validated server-side as a whole number within the configured maximum rather than trusting the client. A rating carries no options, so it is checked on its own rather than as a predefined-option type.

Scope

  • no schema change; maxRating and ratingIcon live in the existing extraSettings
  • FormsQuestionType is deliberately left alone, matching how linearscale, ranking and color are already handled, so openapi.json is unaffected

Testing

  • npm run lint, npm run stylelint and php -l clean
  • built against current main with no errors
  • checked: default 5 stars, switching to hearts and thumbs, changing the count, clearing an answer, a required rating left empty, and a submitted value appearing correctly in the results and CSV export

Happy to adjust the default icon count, the icon set, or drop the hearts/thumbs options if you would rather keep it to stars.

🤖 AI (if applicable)

  • The content of this PR was partly or fully generated using AI

Implements nextcloud#356: a compact star rating, with hearts and thumbs as alternatives.

A linear scale already covers 1..N, but as a row of radio buttons. A rating is the
control people expect for "how would you rate this", takes far less width, and reads
at a glance in the results.

Built from real radio inputs rather than clickable icons, so it stays keyboard
navigable and every option is announced with the value it selects. The hit area is a
full clickable-area square while the icon itself stays small, since an icon-sized
target is awkward to hit on a phone. The hover animation is dropped under
prefers-reduced-motion.

Configurable from 2 to 10 icons, defaulting to 5. The answer is stored as the plain
number, so results and CSV export need no special handling.

Validated server-side as a whole number within the configured maximum rather than
trusting the client. Rating carries no options, so it is checked on its own rather
than as a predefined-option type.

No schema change. FormsQuestionType is left alone, matching how linearscale, ranking
and color are already handled, so openapi.json is unaffected.

Signed-off-by: global-prog <raqeeb@uosamarra.edu.iq>
@codecov

codecov Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@Chartman123

Copy link
Copy Markdown
Collaborator

@global-prog thanks for the PR :) please add the AI section to the description if you used AI to generate this PR 👍🏻

some comments on the code itself: as this uses mostly the same logic as linear scale questions, we should re-use the existing extraSettings (especially maxOption instead of maxRating) and also the validation logic as far as possible. Rest is looking good from a first glance at it.

Perhaps you could also add a screenshot/recording of how it works to the description so that the @nextcloud/designers could have a look at it without having to build it 👍🏻

A rating is a linear scale that always starts at 1 and is drawn as icons, so it should
not carry settings and checks of its own:

  * its top end is stored as optionsHighest rather than a separate maxRating key, and
    optionsLowest is refused, since a rating's lowest end is always 1;
  * the linear scale's bounds on optionsHighest (2 to 10) now apply to it as well. The
    editor already offered exactly that range, but the server never enforced it;
  * the range check that validated linear scale answers is moved into one helper that
    both types call, keeping its message and defaults, so linear scale behaviour is
    unchanged.

Adds tests for the rating's accepted settings and for its answer validation, which the
first commit did not have.

Signed-off-by: global-prog <raqeeb@uosamarra.edu.iq>
@global-prog

Copy link
Copy Markdown
Author

Thanks! Done in the latest commit:

  • the rating now stores its top end as optionsHighest instead of maxRating, and gets the linear scale's bounds on it (2 to 10). optionsLowest is refused, since a rating always starts at 1.
  • the linear scale's range check is moved into one helper that both types call, with the same message and defaults, so linear scale behaviour is unchanged.
  • added tests for the rating's settings and its answer validation.

I've added the AI declaration to the description, and I'll add a screenshot of the editor and the filled-in form there as well.

@Chartman123 Chartman123 left a comment

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.

I've add a closer look on the code now and added a few more suggestions and comments. Could you please also rebase your branch on current main? Thanks :)

Comment thread lib/Constants.php
Comment on lines +224 to +230
/**
* A rating is a linear scale that always starts at 1 and is drawn as icons, so it
* shares the linear scale's key for its top end (and that key's bounds) rather than
* having one of its own. optionsLowest is deliberately absent: a rating's lowest end
* is always 1. ratingIcon is one of 'star' (default), 'heart' or 'thumb'.
*/
public const EXTRA_SETTINGS_RATING = [

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.

We don't need the doc block here

Suggested change
/**
* A rating is a linear scale that always starts at 1 and is drawn as icons, so it
* shares the linear scale's key for its top end (and that key's bounds) rather than
* having one of its own. optionsLowest is deliberately absent: a rating's lowest end
* is always 1. ratingIcon is one of 'star' (default), 'heart' or 'thumb'.
*/
public const EXTRA_SETTINGS_RATING = [
public const EXTRA_SETTINGS_RATING = [

Comment on lines 633 to 645
}

// Check if all answers are within the possible options
// A rating carries no options, so it cannot go through the predefined-option
// branch below, but its answer is a point on a scale exactly as a linear scale's
// is, so it is held to the same rule.
if ($question['type'] === Constants::ANSWER_TYPE_RATING) {
foreach ($answers[$questionId] as $answer) {
$this->validateScaleAnswer($question, $answer);
}
}

if (in_array($question['type'], Constants::ANSWER_TYPES_PREDEFINED) && empty($question['extraSettings']['allowOtherAnswer'])) {

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.

During the review I noticed that we actually shouldn't keep linearscale in the predefined questions array in Constants.php. As a preparation for a follow-up PR we should already move rating questions to a new type array ANSWER_TYPES_SCALE in Constants and check the question type for being a member of that new array.

Suggested change
}
// Check if all answers are within the possible options
// A rating carries no options, so it cannot go through the predefined-option
// branch below, but its answer is a point on a scale exactly as a linear scale's
// is, so it is held to the same rule.
if ($question['type'] === Constants::ANSWER_TYPE_RATING) {
foreach ($answers[$questionId] as $answer) {
$this->validateScaleAnswer($question, $answer);
}
}
if (in_array($question['type'], Constants::ANSWER_TYPES_PREDEFINED) && empty($question['extraSettings']['allowOtherAnswer'])) {
}
// Check if answers to scale questions are within the limits
if (in_array($question['type'], Constants::ANSWER_TYPES_SCALE)) {
foreach ($answers[$questionId] as $answer) {
$this->validateScaleAnswer($question, $answer);
}
}
// Check if all answers are within the possible options
if (in_array($question['type'], Constants::ANSWER_TYPES_PREDEFINED) && empty($question['extraSettings']['allowOtherAnswer'])) {

Comment thread lib/Constants.php
self::ANSWER_TYPE_RATING,
self::ANSWER_TYPE_SHORT,
self::ANSWER_TYPE_TIME,
];

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
];
];
// AnswerTypes that use a number scale
public const ANSWER_TYPES_SCALE = [
self::ANSWER_TYPE_RATING,
];

useQuestion,
} from '../../composables/useQuestion.ts'

/** Matches the linear scale default the server assumes when optionsHighest is unset. */

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.

We could skip this comment here

Comment on lines +52 to +54
n('forms', '%n of {max}', '%n of {max}', value, {
max: optionsHighest,
})

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.

why don't you use just t here?

:modelValue="optionsHighest"
type="multiselect"
:clearable="false"
:label="t('forms', 'Number of icons')"

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.

Could we find a better wording here?

Suggested change
:label="t('forms', 'Number of icons')"
:label="t('forms', 'Value of highest rating')"

Comment on lines +63 to +68
<NcButton
v-if="readOnly && currentValue"
variant="tertiary"
@click="onPick(0)">
{{ t('forms', 'Clear') }}
</NcButton>

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.

Do we really need this button? Radio/Linear Scale question can't be cleared neither. If we really want to keep it, we should use a separate method onClear and really unset the value

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.

Looks good in general, however I'm not sure about the presentation of the question. Google e.g. uses colored buttons (stars -> yellow, hearts -> red, thumbs -> blue). We could also use the whole width for displaying the icons like we do for linear scale questions. The settings could also be reworked a little and contain the icons and the texts. Google also has labels above the icons.

But I think this is something our designers could review/judge best :)

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

Labels

2. developing Work in progress enhancement New feature or request feature: ❓ question types

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[QuestionType] Rating

2 participants