Feat(settings): add renderer for settings configuration #142

Merged
jakob.scheid merged 88 commits from feature/settings-renderer into main 2026-07-28 20:14:50 +02:00
Owner

Settings renderer

This pull request adds a renderer that renders any settings configuration.

Summary

  • Pinia integration
  • Fix of settings link in footer
  • settings components and tests
  • settings view and tests
  • settings page component and tests
  • settings page composable and tests
  • settings store (persistent)
  • utility functions to get settings (recursively) and tests
  • utility functions for settings pages and tests
  • test utility to mount components

Implementation

The settings renderer always shows the current section, which is recursively determined from the route subpath (the path after /settings, for an example, /settings/section1/section2). There are components for each setting type which get information about their setting. The components do the actual work: They display the settings in the store and save the settings configured by the user in the store.

Description

Pinia integration

This PR adds Pinia, Pinia test utils, and pinia-plugin-persistedstate to the dependencies. The app now uses Pinia and Pinia uses pinia-plugin-persistedstate for persistence.

Settings components

There are component for each setting type. Each component has been thoroughly tested (see the test files).

The settings components have props for their setting and their path. Each of them shows the value from the store or the default value as a fallback. If there is no default value, a value specific to the setting type is shown as a fallback for the fallback.

Switch

The component for boolean settings. It displays the translation of the setting on the left side and a box with a circle on the right. When the user clicks the component, the setting is toggled. The position of the circle indicates the setting value (left: false, right: true).

There are tests for the switch.

Input

The component for both number and string settings. These two setting types were combined as the difference is very small; only the type of the input element is either number or text. This type is set by the component, based on the setting type.

The settings input component shows the translation on the left side and the input on the right side.

When the value of the input is different from the stored value, a done button is shown. There is an animation when it is shown and hidden. This done button is the button that submits the form the input is wrapped in. On form submit (either done button click or pressing the enter key), the entered value is stored.

When the stored value changes, the input value also updates if it there are no unsaved changes in the input.

There are tests for the input.

Selection

The component for selection settings. It shows the translation of the setting and a list of the options below that. It supports both multiple choice and single choice inputs.

If the setting allows multiple values, each option contains a radio input. If it does not, each option contains a checkbox input. A check mark icon indicated whether an option is selected or not.

If the user selects options, the value is immediately stored.

When the component is mounted, the value is retrieved from the settings store and the corresponding option(s) is/are selected.

There are tests for the selection.

Settings page link

The settings page link is used for sections. It is a button that shows the setting translation on the left and a chevron facing right on the right side.

When it is clicked, the settings page path is set to the link target (its settings section) using the useSettingsPage composable.

There are tests for the settings page link.

Settings page composable

As mentioned above, there is a composable for the settings page: useSettingsPage. It provides the function goToSettingsPage, which is used to go to a settings page. It pushes the route for the specified settings page path to the history and settings view automatically updates the section shown as it watches the router.

There are tests for this composable.

Settings store

The store for the settings is a Pinia store with persistence (it is stored locally using local storage). It provides functions to set, get, and remove values and to clear the store. The settings themselves are an object wrapped by Vue's reactive.

Settings getting utility

There are two utility functions to get settings: a one to get a setting of one level and one to get a setting recursively, based on its path.

Both are tested.

Settings pages utility

This utility provides a function to get the segments of a setting path.

Of course, there are tests for this utility.

Component mounting utility

A utility function to mount components in their context.

Related issues

Closes: #79

# Settings renderer This pull request adds a renderer that renders any settings configuration. ## Summary - Pinia integration - Fix of settings link in footer - settings components and tests - settings view and tests - settings page component and tests - settings page composable and tests - settings store (persistent) - utility functions to get settings (recursively) and tests - utility functions for settings pages and tests - test utility to mount components ## Implementation The settings renderer always shows the current section, which is recursively determined from the route subpath (the path after `/settings`, for an example, `/settings/section1/section2`). There are components for each setting type which get information about their setting. The components do the actual work: They display the settings in the store and save the settings configured by the user in the store. ## Description ### Pinia integration This PR adds Pinia, Pinia test utils, and `pinia-plugin-persistedstate` to the dependencies. The app now uses Pinia and Pinia uses `pinia-plugin-persistedstate` for persistence. ### Settings components There are component for each setting type. Each component has been thoroughly tested (see the test files). The settings components have props for their setting and their path. Each of them shows the value from the store or the default value as a fallback. If there is no default value, a value specific to the setting type is shown as a fallback for the fallback. #### Switch The component for boolean settings. It displays the translation of the setting on the left side and a box with a circle on the right. When the user clicks the component, the setting is toggled. The position of the circle indicates the setting value (left: false, right: true). There are tests for the switch. #### Input The component for both number and string settings. These two setting types were combined as the difference is very small; only the type of the input element is either `number` or `text`. This type is set by the component, based on the setting type. The settings input component shows the translation on the left side and the input on the right side. When the value of the input is different from the stored value, a done button is shown. There is an animation when it is shown and hidden. This done button is the button that submits the form the input is wrapped in. On form submit (either done button click or pressing the enter key), the entered value is stored. When the stored value changes, the input value also updates if it there are no unsaved changes in the input. There are tests for the input. #### Selection The component for selection settings. It shows the translation of the setting and a list of the options below that. It supports both multiple choice and single choice inputs. If the setting allows multiple values, each option contains a radio input. If it does not, each option contains a checkbox input. A check mark icon indicated whether an option is selected or not. If the user selects options, the value is **immediately** stored. When the component is mounted, the value is retrieved from the settings store and the corresponding option(s) is/are selected. There are tests for the selection. #### Settings page link The settings page link is used for sections. It is a button that shows the setting translation on the left and a chevron facing right on the right side. When it is clicked, the settings page path is set to the link target (its settings section) using the `useSettingsPage` composable. There are tests for the settings page link. ### Settings page composable As mentioned above, there is a composable for the settings page: `useSettingsPage`. It provides the function `goToSettingsPage`, which is used to go to a settings page. It pushes the route for the specified settings page path to the history and settings view automatically updates the section shown as it watches the router. There are tests for this composable. ### Settings store The store for the settings is a Pinia store with persistence (it is stored locally using local storage). It provides functions to set, get, and remove values and to clear the store. The settings themselves are an object wrapped by Vue's `reactive`. ### Settings getting utility There are two utility functions to get settings: a one to get a setting of one level and one to get a setting recursively, based on its path. Both are tested. ### Settings pages utility This utility provides a function to get the segments of a setting path. Of course, there are tests for this utility. ### Component mounting utility A utility function to mount components in their context. ## Related issues Closes: #79
jakob.scheid added this to the 0.1.0 milestone 2026-07-28 12:16:24 +02:00
jakob.scheid added the Area/SettingsKind/Feature
Priority
Medium
3
labels 2026-07-28 12:16:24 +02:00
jakob.scheid added this to the Project Board project 2026-07-28 12:16:24 +02:00
jakob.gregory approved these changes 2026-07-28 12:17:47 +02:00
jakob.scheid marked the pull request as work in progress 2026-07-28 12:19:58 +02:00
jakob.scheid moved this to Review in Project Board on 2026-07-28 12:30:51 +02:00
jakob.scheid marked the pull request as ready for review 2026-07-28 12:31:04 +02:00
jakob.scheid requested review from jakob.gregory 2026-07-28 12:31:12 +02:00
jakob.scheid reviewed 2026-07-28 12:33:17 +02:00
@@ -0,0 +48,4 @@
},
path: {
required: true
},
Author
Owner

Please remove this unnecessary comma.

Please remove this unnecessary comma.
jakob.scheid marked this conversation as resolved
jakob.scheid reviewed 2026-07-28 12:34:57 +02:00
@@ -0,0 +30,4 @@
},
path: {
required: true
},
Author
Owner

Please remove this unnecessary comma.

Please remove this unnecessary comma.
jakob.scheid marked this conversation as resolved
jakob.scheid reviewed 2026-07-28 12:37:59 +02:00
@@ -0,0 +20,4 @@
import { expect, describe, test } from 'vitest';
import { nextTick } from 'vue';
const getWrapper = function getWrapper (options = {}) {
Author
Owner

I think object destructuring would make more sense here.

I think object destructuring would make more sense here.
jakob.scheid marked this conversation as resolved
jakob.scheid reviewed 2026-07-28 12:42:44 +02:00
@@ -0,0 +55,4 @@
describe('Selection', () => {
test('displays options correctly', () => {
const options = [
Author
Owner

Use the exampleOptions object.

Use the `exampleOptions` object.
jakob.scheid marked this conversation as resolved
jakob.scheid reviewed 2026-07-28 12:48:20 +02:00
@@ -0,0 +22,4 @@
describe('Switch', () => {
test('shows value from store', async () => {
const wrapper = mountComponent(Switch, {
Author
Owner

Please add a function to get the wrapper and avoid repeating code.

Please add a function to get the wrapper and avoid repeating code.
jakob.scheid marked this conversation as resolved
jakob.scheid force-pushed feature/settings-renderer from 99ae2725cd to 312734233b 2026-07-28 12:53:52 +02:00 Compare
jakob.scheid reviewed 2026-07-28 12:55:26 +02:00
@@ -0,0 +21,4 @@
export const getSettingRecursively = function getSettingRecursively (sectionPath, settings) {
const oneSettingsLevel = function oneSettingsLevel (cursor, acc) {
const setting = getSetting(cursor[0], acc.content);
if (!setting) {
Author
Owner

Put this in one line.

Put this in one line.
jakob.scheid marked this conversation as resolved
jakob.scheid reviewed 2026-07-28 12:57:41 +02:00
@@ -0,0 +18,4 @@
return settings.filter((setting) => setting.name === name)[0];
};
export const getSettingRecursively = function getSettingRecursively (sectionPath, settings) {
Author
Owner

It makes more sense to pass the section path as an array.

It makes more sense to pass the section path as an array.
jakob.scheid marked this conversation as resolved
jakob.scheid reviewed 2026-07-28 13:08:09 +02:00
@@ -0,0 +68,4 @@
</div>
<ul class="options-list">
<li v-for="option in props.setting.options" :key="option.name">
<label class="settings-button">
Author
Owner

Please add a title and aria-label attributes.

Please add a `title` and `aria-label` attributes.
jakob.scheid marked this conversation as resolved
jakob.scheid reviewed 2026-07-28 13:08:38 +02:00
@@ -0,0 +63,4 @@
<template>
<div>
<div class="label">
Author
Owner

Please add a label relationship between div.label and ul.options-list.

Please add a label relationship between `div.label` and `ul.options-list`.
jakob.scheid marked this conversation as resolved
jakob.scheid reviewed 2026-07-28 13:10:07 +02:00
@@ -0,0 +56,4 @@
const optionType = computed(() => props.setting.allowMultiple ? 'checkbox' : 'radio');
const selected = ref(normalizeSelectedValue(store.get(props.path) ?? props.setting.default));
watch(selected, (newValue, oldValue) => {
Author
Owner

The oldValue parameter is not necessary.

The `oldValue` parameter is not necessary.
jakob.scheid marked this conversation as resolved
Member

This looks better.
Very good and very fast!

This looks better. **Very** good and **very** fast!
jakob.scheid added a new dependency 2026-07-28 18:48:29 +02:00
jakob.scheid removed review request for jakob.gregory 2026-07-28 20:02:55 +02:00
jakob.scheid added 87 commits 2026-07-28 20:10:50 +02:00
Renamed the variable 'inputClass' to 'inputType' because this name is
more accurate
Utility function getSetting for getting a first-level setting from a
list of settings and unit tests.
Utility function getSettingRecursively to get any setting from a list
of settings. The setting can be at any nesting level in the settings
list and unit tests.
Previously, the settings view now does not check whether the settings
content is falsy. But, if the setting does not exist, the setting itself
is undefined. So, if the specified settings path did not exist, an
error was thrown.
Now, the setting is checked for being falsy, not the setting content.
Added a simple message as a fallback that is used if no section is
selected.
Added a wrapper for the selection setting label to make it easier to
find (internal).
The .settings-main-content wrapper in the settings view is now only
used if there are settings shown. It is not used if the settings are
still loading or no section is selected.
Added a wrapper for the settings selection option label to make it
easier to identify (internal).
Added tests for the selection setting component:
  - correct display of options
  - shows values correctly
  - store of values
  - correct translation
The settings page composable function to go to a settings page now
returns the promise router.push returns.
The composable now uses replace instead of replaceAll which is not
supported by some old browsers.
Modified the useSettingsPage composable to use a route object instead
of a raw string in goToSettingsPage and moved settings route child
routes into the settings route itself using 'rest(.*)*'.
Separated unit tests and integration tests in the test for
useSettingsPage more clearly.
The composable useSettingsPage now omits empty segments in the path.
Added a type check to the normalizePagePath function. The function now
returns an empty array if the path is not a string.
Removed the utility getSectionHeadingLevel and its tests.
Moved the normalizePagePath utility from the settings page composable to
a separate utility file and restructured tests accordingly.
Added tests for the settings view (SettingsView.vue):
  - heading translation
  - sidebar top-level sections
  - correct main element
Added tests for the SettingsPage component:
  - correct heading
  - correct settings
Updated the getWrapper function to use object destructuring.
The SettingsInput component now uses the default value if there is a
default value. Tests for this were added.
Removed unnecessary trailing comma in the selection component.
Removed unnecessary commas in settings input component and settings page
component.
Updated the getWrapper function to use object destructuring.
Use exampleOptions instead of an own options array, which was equal to
exampleOptions.
Added and used getWrapper function to switch component test to mount the
component and avoid repeating code.
Use a oneline if statement in the getSetting utility.
Tests and all files the utility is used in are also updated.

Warning: This change is breaking!
Removed unused oldValue parameter in the selection component.
Added attributes to indicate that div.label is the label for
ul.options-list in the selection component.
jakob.scheid force-pushed feature/settings-renderer from c1f2d31f4e to eb43e2f49f 2026-07-28 20:10:50 +02:00 Compare
jakob.scheid added 1 commit 2026-07-28 20:13:55 +02:00
jakob.scheid merged commit 799d7ee0bc into main 2026-07-28 20:14:50 +02:00
jakob.scheid deleted branch feature/settings-renderer 2026-07-28 20:14:51 +02:00
jakob.scheid moved this to Done in Project Board on 2026-07-28 20:15:53 +02:00
Sign in to join this conversation.
2 Participants
Notifications
Due Date
No due date set.
Blocks
Reference: Seekra/frontend#142