Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
5e814ab
feat(boolean): add boolean field type enum
vitormattos Mar 20, 2026
a65f6d1
chore(boolean): extend response type union
vitormattos Mar 20, 2026
8ad8b6f
chore(boolean): widen import service type docs
vitormattos Mar 20, 2026
88a9f80
feat(boolean): accept boolean field definitions
vitormattos Mar 20, 2026
dfaebc4
feat(boolean): normalize boolean field values
vitormattos Mar 20, 2026
8c651af
fix(boolean): allow boolean definitions in imports
vitormattos Mar 20, 2026
75b64bc
feat(boolean): support boolean workflow checks
vitormattos Mar 20, 2026
9bae436
chore(openapi): regenerate admin schema for boolean
vitormattos Mar 20, 2026
3daeec8
chore(openapi): regenerate full schema for boolean
vitormattos Mar 20, 2026
4a68b68
chore(openapi): regenerate schema for boolean
vitormattos Mar 20, 2026
4b9c4a1
feat(boolean): add boolean input to admin dialog
vitormattos Mar 20, 2026
04cbe02
test(boolean): cover admin settings boolean type
vitormattos Mar 20, 2026
cd1603c
test(boolean): cover admin dialog boolean input
vitormattos Mar 20, 2026
0418d48
test(boolean): cover personal settings boolean input
vitormattos Mar 20, 2026
19746f0
test(boolean): cover workflow boolean operators
vitormattos Mar 20, 2026
fbefc1e
feat(boolean): expose boolean field in frontend types
vitormattos Mar 20, 2026
db5477c
chore(openapi): regenerate admin frontend types for boolean
vitormattos Mar 20, 2026
015d0f4
chore(openapi): regenerate full frontend types for boolean
vitormattos Mar 20, 2026
e1989be
chore(openapi): regenerate frontend types for boolean
vitormattos Mar 20, 2026
8fffcce
feat(boolean): map boolean workflow operators
vitormattos Mar 20, 2026
cd9939c
feat(boolean): offer boolean type in admin settings
vitormattos Mar 20, 2026
499d552
feat(boolean): render boolean input in personal settings
vitormattos Mar 20, 2026
e76c001
test(boolean): cover boolean definition validation
vitormattos Mar 20, 2026
c1ffe43
test(boolean): cover boolean value normalization
vitormattos Mar 20, 2026
5622fa3
test(boolean): cover workflow boolean checks
vitormattos Mar 20, 2026
f59ec03
docs(screenshots): refresh admin catalog thumb
vitormattos Mar 20, 2026
7a8b2af
docs(screenshots): refresh personal settings thumb
vitormattos Mar 20, 2026
07f61c6
docs(screenshots): refresh personal settings screenshot
vitormattos Mar 20, 2026
9ec4c3c
docs(screenshots): refresh user dialog thumb
vitormattos Mar 20, 2026
dcae115
docs(screenshots): refresh workflow thumb
vitormattos Mar 20, 2026
440bae4
docs(screenshots): add boolean field admin proof
vitormattos Mar 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified img/screenshots/admin-catalog-thumb.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/screenshots/boolean-field-admin-proof.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/screenshots/personal-settings-thumb.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/screenshots/personal-settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/screenshots/user-management-dialog-thumb.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/screenshots/workflow-notify-admins-thumb.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions lib/Enum/FieldType.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
enum FieldType: string {
case TEXT = 'text';
case NUMBER = 'number';
case BOOLEAN = 'boolean';
case DATE = 'date';
case SELECT = 'select';
case MULTISELECT = 'multiselect';
Expand All @@ -23,6 +24,7 @@ public static function values(): array {
return [
self::TEXT->value,
self::NUMBER->value,
self::BOOLEAN->value,
self::DATE->value,
self::SELECT->value,
self::MULTISELECT->value,
Expand Down
2 changes: 1 addition & 1 deletion lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
namespace OCA\ProfileFields;

/**
* @psalm-type ProfileFieldsType = 'text'|'number'|'date'|'select'|'multiselect'
* @psalm-type ProfileFieldsType = 'text'|'number'|'boolean'|'date'|'select'|'multiselect'
* @psalm-type ProfileFieldsVisibility = 'private'|'users'|'public'
* @psalm-type ProfileFieldsEditPolicy = 'admins'|'users'
* @psalm-type ProfileFieldsExposurePolicy = 'hidden'|'private'|'users'|'public'
Expand Down
6 changes: 3 additions & 3 deletions lib/Service/DataImportService.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public function import(array $payload, bool $dryRun = false): array {
* @param list<array{
* field_key: non-empty-string,
* label: non-empty-string,
* type: 'text'|'number'|'date'|'select'|'multiselect',
* type: 'text'|'number'|'boolean'|'date'|'select'|'multiselect',
* edit_policy: 'admins'|'users',
* exposure_policy: 'hidden'|'private'|'users'|'public',
* sort_order: int,
Expand Down Expand Up @@ -148,7 +148,7 @@ private function collectValueSummary(array $values, array &$summary): void {
* @param list<array{
* field_key: non-empty-string,
* label: non-empty-string,
* type: 'text'|'number'|'date'|'select'|'multiselect',
* type: 'text'|'number'|'boolean'|'date'|'select'|'multiselect',
* edit_policy: 'admins'|'users',
* exposure_policy: 'hidden'|'private'|'users'|'public',
* sort_order: int,
Expand Down Expand Up @@ -248,7 +248,7 @@ private function persistValues(array $values, array $definitionsByFieldKey, arra
* @param array{
* field_key: non-empty-string,
* label: non-empty-string,
* type: 'text'|'number'|'date'|'select'|'multiselect',
* type: 'text'|'number'|'boolean'|'date'|'select'|'multiselect',
* edit_policy: 'admins'|'users',
* exposure_policy: 'hidden'|'private'|'users'|'public',
* sort_order: int,
Expand Down
2 changes: 1 addition & 1 deletion lib/Service/FieldDefinitionValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class FieldDefinitionValidator {
* @return array{
* field_key: non-empty-string,
* label: non-empty-string,
* type: 'text'|'number'|'date'|'select'|'multiselect',
* type: 'text'|'number'|'boolean'|'date'|'select'|'multiselect',
* edit_policy: 'admins'|'users',
* exposure_policy: 'hidden'|'private'|'users'|'public',
* sort_order: int,
Expand Down
32 changes: 32 additions & 0 deletions lib/Service/FieldValueService.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ public function normalizeValue(FieldDefinition $definition, array|string|int|flo
return match ($type) {
FieldType::TEXT => $this->normalizeTextValue($rawValue),
FieldType::NUMBER => $this->normalizeNumberValue($rawValue),
FieldType::BOOLEAN => $this->normalizeBooleanValue($rawValue),
FieldType::DATE => $this->normalizeDateValue($rawValue),
FieldType::SELECT => $this->normalizeSelectValue($rawValue, $definition),
FieldType::MULTISELECT => $this->normalizeMultiSelectValue($rawValue, $definition),
Expand Down Expand Up @@ -299,6 +300,37 @@ private function normalizeNumberValue(array|string|int|float|bool $rawValue): ar
return ['value' => str_contains((string)$rawValue, '.') ? (float)$rawValue : (int)$rawValue];
}

/**
* @param array<string, mixed>|scalar $rawValue
* @return array{value: bool}
*/
private function normalizeBooleanValue(array|string|int|float|bool $rawValue): array {
if (is_bool($rawValue)) {
return ['value' => $rawValue];
}

if (is_string($rawValue)) {
$value = strtolower(trim($rawValue));
if ($value === 'true' || $value === '1') {
return ['value' => true];
}
if ($value === 'false' || $value === '0') {
return ['value' => false];
}
}

if (is_int($rawValue) || is_float($rawValue)) {
if ((float)$rawValue === 1.0) {
return ['value' => true];
}
if ((float)$rawValue === 0.0) {
return ['value' => false];
}
}

throw new InvalidArgumentException($this->l10n->t('Boolean fields require true or false values.'));
}

/**
* @param array<string, mixed>|scalar $rawValue
* @return array{value: string}
Expand Down
8 changes: 4 additions & 4 deletions lib/Service/ImportPayloadValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public function __construct(
* definitions: list<array{
* field_key: non-empty-string,
* label: non-empty-string,
* type: 'text'|'number'|'date'|'select'|'multiselect',
* type: 'text'|'number'|'boolean'|'date'|'select'|'multiselect',
* edit_policy: 'admins'|'users',
* exposure_policy: 'hidden'|'private'|'users'|'public',
* sort_order: int,
Expand Down Expand Up @@ -71,7 +71,7 @@ public function validate(array $payload): array {
* @return array<non-empty-string, array{
* field_key: non-empty-string,
* label: non-empty-string,
* type: 'text'|'number'|'date'|'select'|'multiselect',
* type: 'text'|'number'|'boolean'|'date'|'select'|'multiselect',
* edit_policy: 'admins'|'users',
* exposure_policy: 'hidden'|'private'|'users'|'public',
* sort_order: int,
Expand Down Expand Up @@ -119,7 +119,7 @@ private function validateDefinitions(array $definitions): array {
* @param array<non-empty-string, array{
* field_key: non-empty-string,
* label: non-empty-string,
* type: 'text'|'number'|'date'|'select'|'multiselect',
* type: 'text'|'number'|'boolean'|'date'|'select'|'multiselect',
* edit_policy: 'admins'|'users',
* exposure_policy: 'hidden'|'private'|'users'|'public',
* sort_order: int,
Expand Down Expand Up @@ -255,7 +255,7 @@ private function normalizeOptionalDate(array $payload, string $key, string $mess
* @param array{
* field_key: non-empty-string,
* label: non-empty-string,
* type: 'text'|'number'|'date'|'select'|'multiselect',
* type: 'text'|'number'|'boolean'|'date'|'select'|'multiselect',
* edit_policy: 'admins'|'users',
* exposure_policy: 'hidden'|'private'|'users'|'public',
* sort_order: int,
Expand Down
47 changes: 47 additions & 0 deletions lib/Workflow/UserProfileFieldCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ class UserProfileFieldCheck implements ICheck {
'greater',
'!less',
];
private const BOOLEAN_OPERATORS = [
self::OPERATOR_IS_SET,
self::OPERATOR_IS_NOT_SET,
'is',
'!is',
];
private const SELECT_OPERATORS = [
self::OPERATOR_IS_SET,
self::OPERATOR_IS_NOT_SET,
Expand Down Expand Up @@ -176,6 +182,7 @@ private function isOperatorSupported(FieldDefinition $definition, string $operat
$operators = match (FieldType::from($definition->getType())) {
FieldType::TEXT => self::TEXT_OPERATORS,
FieldType::NUMBER => self::NUMBER_OPERATORS,
FieldType::BOOLEAN => self::BOOLEAN_OPERATORS,
FieldType::DATE => self::DATE_OPERATORS,
FieldType::SELECT => self::SELECT_OPERATORS,
FieldType::MULTISELECT => self::SELECT_OPERATORS,
Expand Down Expand Up @@ -248,6 +255,11 @@ private function evaluate(FieldDefinition $definition, string $operator, string|
return match ($fieldType) {
FieldType::TEXT,
FieldType::SELECT => $this->evaluateTextOperator($operator, (string)$expectedValue, (string)$actualValue),
FieldType::BOOLEAN => $this->evaluateBooleanOperator(
$operator,
$this->normalizeBooleanComparisonOperand($expectedValue),
$this->normalizeBooleanComparisonOperand($actualValue),
),
FieldType::NUMBER => $this->evaluateNumberOperator(
$operator,
$this->normalizeNumericComparisonOperand($expectedValue),
Expand Down Expand Up @@ -297,6 +309,33 @@ private function normalizeNumericComparisonOperand(string|int|float|bool|null $v
return str_contains((string)$value, '.') ? (float)$value : (int)$value;
}

private function normalizeBooleanComparisonOperand(string|int|float|bool|null $value): bool {
if (is_bool($value)) {
return $value;
}

if (is_string($value)) {
$normalized = strtolower(trim($value));
if ($normalized === 'true' || $normalized === '1') {
return true;
}
if ($normalized === 'false' || $normalized === '0') {
return false;
}
}

if (is_int($value) || is_float($value)) {
if ((float)$value === 1.0) {
return true;
}
if ((float)$value === 0.0) {
return false;
}
}

throw new InvalidArgumentException('boolean comparison value must be true or false');
}

private function normalizeDateComparisonOperand(string|int|float|bool|null $value): int {
if (!is_string($value)) {
throw new InvalidArgumentException('date comparison value must be a valid YYYY-MM-DD string');
Expand Down Expand Up @@ -332,6 +371,14 @@ private function evaluateNumberOperator(string $operator, int|float $expectedVal
};
}

private function evaluateBooleanOperator(string $operator, bool $expectedValue, bool $actualValue): bool {
return match ($operator) {
'is' => $actualValue === $expectedValue,
'!is' => $actualValue !== $expectedValue,
default => false,
};
}

private function evaluateDateOperator(string $operator, int $expectedValue, int $actualValue): bool {
return match ($operator) {
'is' => $actualValue === $expectedValue,
Expand Down
1 change: 1 addition & 0 deletions openapi-administration.json
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@
"enum": [
"text",
"number",
"boolean",
"date",
"select",
"multiselect"
Expand Down
1 change: 1 addition & 0 deletions openapi-full.json
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@
"enum": [
"text",
"number",
"boolean",
"date",
"select",
"multiselect"
Expand Down
1 change: 1 addition & 0 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@
"enum": [
"text",
"number",
"boolean",
"date",
"select",
"multiselect"
Expand Down
60 changes: 58 additions & 2 deletions src/components/AdminUserFieldsDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,19 @@ SPDX-License-Identifier: AGPL-3.0-or-later
:placeholder="placeholderForField(field.definition.type)"
@update:model-value="updateMultiSelectValue(field.definition.id, $event)"
/>
<NcSelect
v-else-if="field.definition.type === 'boolean'"
class="profile-fields-user-dialog__input"
:input-id="`profile-fields-user-dialog-value-${field.definition.id}`"
:model-value="booleanOptionFor(field.definition.id)"
:aria-label="field.definition.label"
:clearable="true"
:searchable="false"
:options="booleanOptions"
label="label"
:placeholder="placeholderForField(field.definition.type)"
@update:model-value="updateBooleanValue(field.definition.id, $event)"
/>
<NcInputField
v-else
class="profile-fields-user-dialog__input"
Expand Down Expand Up @@ -166,7 +179,7 @@ export default defineComponent({
const savingIds = ref<number[]>([])

const userValueErrors = reactive<Record<number, string>>({})
const userDraftValues = reactive<Record<number, string | string[]>>({})
const userDraftValues = reactive<Record<number, string | string[] | boolean>>({})
const userDraftVisibilities = reactive<Record<number, FieldVisibility>>({})

const headerUserName = computed(() => props.userDisplayName.trim() !== '' ? props.userDisplayName : props.userUid)
Expand Down Expand Up @@ -196,6 +209,7 @@ export default defineComponent({
const descriptionForType = (type: FieldType): string => ({
text: t('profile_fields', 'Free text stored as a scalar value.'),
number: t('profile_fields', 'Only numeric values are accepted.'),
boolean: t('profile_fields', 'Choose either true or false.'),
date: t('profile_fields', 'Use a valid date in YYYY-MM-DD format.'),
select: t('profile_fields', 'Choose one of the predefined options.'),
multiselect: t('profile_fields', 'Choose one or more predefined options.'),
Expand All @@ -204,6 +218,7 @@ export default defineComponent({
const placeholderForField = (type: FieldType): string => ({
text: t('profile_fields', 'Enter a value'),
number: t('profile_fields', 'Enter a number'),
boolean: t('profile_fields', 'Select true or false'),
date: t('profile_fields', 'Select a date'),
select: t('profile_fields', 'Select an option'),
multiselect: t('profile_fields', 'Select one or more options'),
Expand All @@ -215,6 +230,7 @@ export default defineComponent({
const inputModeForField = (type: FieldType): string => ({
text: 'text',
number: 'decimal',
boolean: 'text',
date: 'numeric',
select: 'text',
multiselect: 'text',
Expand All @@ -223,11 +239,31 @@ export default defineComponent({
const componentInputTypeForField = (type: FieldType): string => ({
text: 'text',
number: 'text',
boolean: 'text',
date: 'date',
select: 'text',
multiselect: 'text',
} as Record<FieldType, string>)[type]

const booleanOptions = [
{ value: true, label: t('profile_fields', 'True') },
{ value: false, label: t('profile_fields', 'False') },
]

const booleanOptionFor = (fieldId: number) => {
const current = userDraftValues[fieldId]
if (typeof current !== 'boolean') {
return null
}

return booleanOptions.find((option) => option.value === current) ?? null
}

const updateBooleanValue = (fieldId: number, option: { value: boolean, label: string } | null) => {
userDraftValues[fieldId] = option?.value ?? ''
clearFieldError(fieldId)
}

const selectOptionsFor = (definition: FieldDefinition) =>
(definition.options ?? []).map((opt: string) => ({ value: opt, label: opt }))

Expand Down Expand Up @@ -281,6 +317,13 @@ export default defineComponent({
}
}

if (field.definition.type === 'boolean') {
const current = userDraftValues[field.definition.id]
if (current !== '' && typeof current !== 'boolean') {
return t('profile_fields', '{fieldLabel} must be either true or false.', { fieldLabel: field.definition.label })
}
}

if (field.definition.type === 'select') {
const options = field.definition.options ?? []
if (!options.includes(rawValue)) {
Expand Down Expand Up @@ -312,7 +355,7 @@ export default defineComponent({
const hasInvalidFields = computed(() => invalidFields.value.length > 0)

const helperTextForField = (field: AdminEditableField) => {
return field.definition.type === 'number' || field.definition.type === 'date'
return field.definition.type === 'number' || field.definition.type === 'date' || field.definition.type === 'boolean'
? descriptionForType(field.definition.type)
: ''
}
Expand All @@ -323,6 +366,8 @@ export default defineComponent({
const currentValue = field.value?.value
if (Array.isArray(currentValue?.value)) {
userDraftValues[field.definition.id] = [...currentValue.value]
} else if (typeof currentValue?.value === 'boolean') {
userDraftValues[field.definition.id] = currentValue.value
} else {
userDraftValues[field.definition.id] = currentValue?.value?.toString() ?? ''
}
Expand Down Expand Up @@ -383,6 +428,7 @@ export default defineComponent({
return ({
'text fields expect a scalar value': t('profile_fields', '{fieldLabel} must be plain text.', { fieldLabel: field.definition.label }),
'number fields expect a numeric value': t('profile_fields', '{fieldLabel} must be a numeric value.', { fieldLabel: field.definition.label }),
'Boolean fields require true or false values.': t('profile_fields', '{fieldLabel} must be either true or false.', { fieldLabel: field.definition.label }),
'Date fields require a valid ISO-8601 date in YYYY-MM-DD format.': t('profile_fields', '{fieldLabel} must be a valid date in YYYY-MM-DD format.', { fieldLabel: field.definition.label }),
'current_visibility is not supported': t('profile_fields', 'The selected visibility is not supported.'),
}[message] ?? (message.includes('is not a valid option')
Expand Down Expand Up @@ -445,6 +491,13 @@ export default defineComponent({
}
}

if (field.definition.type === 'boolean') {
return {
value: typeof current === 'boolean' ? current : null,
visibility: userDraftVisibilities[field.definition.id],
}
}

if (field.definition.type === 'multiselect') {
const selectedValues = userDraftValues[field.definition.id]
if (!Array.isArray(selectedValues)) {
Expand Down Expand Up @@ -582,6 +635,9 @@ export default defineComponent({
componentInputTypeForField,
inputModeForField,
placeholderForField,
booleanOptions,
booleanOptionFor,
updateBooleanValue,
saveAllFields,
successMessage,
updateOpen,
Expand Down
Loading
Loading