Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6a3f316
feat(multiselect): update FieldType.php
vitormattos Mar 20, 2026
7163669
feat(multiselect): update ResponseDefinitions.php
vitormattos Mar 20, 2026
3b0fd0a
feat(multiselect): update FieldDefinitionValidator.php
vitormattos Mar 20, 2026
a7ca47a
feat(multiselect): update FieldValueService.php
vitormattos Mar 20, 2026
3c43d9a
feat(multiselect): update UserProfileFieldCheck.php
vitormattos Mar 20, 2026
d046364
feat(multiselect): update AdminUserFieldsDialog.vue
vitormattos Mar 20, 2026
f827cc3
feat(multiselect): update index.ts
vitormattos Mar 20, 2026
912497f
feat(multiselect): update AdminSettings.vue
vitormattos Mar 20, 2026
0dedced
feat(multiselect): update PersonalSettings.vue
vitormattos Mar 20, 2026
e81c77d
feat(multiselect): update FieldDefinitionValidatorTest.php
vitormattos Mar 20, 2026
0e0ee4d
feat(multiselect): update FieldValueServiceTest.php
vitormattos Mar 20, 2026
5836c15
feat(multiselect): update UserProfileFieldCheckTest.php
vitormattos Mar 20, 2026
6114c31
feat(multiselect): update admin-catalog-thumb.png
vitormattos Mar 20, 2026
35f40dc
feat(multiselect): update admin-catalog.png
vitormattos Mar 20, 2026
4df31f2
feat(multiselect): update personal-settings-thumb.png
vitormattos Mar 20, 2026
ff8ceed
feat(multiselect): update personal-settings.png
vitormattos Mar 20, 2026
c608f39
feat(multiselect): update user-management-dialog-thumb.png
vitormattos Mar 20, 2026
6255eb2
feat(multiselect): update user-management-dialog.png
vitormattos Mar 20, 2026
7da09dc
feat(multiselect): update workflow-notify-admins-thumb.png
vitormattos Mar 20, 2026
f9d712d
chore(openapi): regenerate openapi-administration.json
vitormattos Mar 20, 2026
d3d386a
chore(openapi): regenerate openapi-full.json
vitormattos Mar 20, 2026
d70d556
chore(openapi): regenerate openapi.json
vitormattos Mar 20, 2026
f6f88b7
chore(openapi): regenerate openapi-administration.ts
vitormattos Mar 20, 2026
0ee9f2f
chore(openapi): regenerate openapi-full.ts
vitormattos Mar 20, 2026
0b2b815
chore(openapi): regenerate openapi.ts
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 modified img/screenshots/admin-catalog.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/user-management-dialog.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 @@ -13,6 +13,7 @@ enum FieldType: string {
case TEXT = 'text';
case NUMBER = 'number';
case SELECT = 'select';
case MULTISELECT = 'multiselect';

/**
* @return list<string>
Expand All @@ -22,6 +23,7 @@ public static function values(): array {
self::TEXT->value,
self::NUMBER->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'|'select'
* @psalm-type ProfileFieldsType = 'text'|'number'|'select'|'multiselect'
* @psalm-type ProfileFieldsVisibility = 'private'|'users'|'public'
* @psalm-type ProfileFieldsEditPolicy = 'admins'|'users'
* @psalm-type ProfileFieldsExposurePolicy = 'hidden'|'private'|'users'|'public'
Expand Down
7 changes: 4 additions & 3 deletions 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'|'select',
* type: 'text'|'number'|'select'|'multiselect',
* edit_policy: 'admins'|'users',
* exposure_policy: 'hidden'|'private'|'users'|'public',
* sort_order: int,
Expand Down Expand Up @@ -78,12 +78,13 @@ public function validate(array $definition): array {
* @return list<string>|null
*/
private function validateOptions(string $type, mixed $options): ?array {
if ($type !== FieldType::SELECT->value) {
$isSelectLike = in_array($type, [FieldType::SELECT->value, FieldType::MULTISELECT->value], true);
if (!$isSelectLike) {
return null;
}

if (!is_array($options) || count($options) === 0) {
throw new InvalidArgumentException('select fields require at least one option');
throw new InvalidArgumentException($type . ' fields require at least one option');
}

$normalized = [];
Expand Down
31 changes: 31 additions & 0 deletions lib/Service/FieldValueService.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ public function normalizeValue(FieldDefinition $definition, array|string|int|flo
FieldType::TEXT => $this->normalizeTextValue($rawValue),
FieldType::NUMBER => $this->normalizeNumberValue($rawValue),
FieldType::SELECT => $this->normalizeSelectValue($rawValue, $definition),
FieldType::MULTISELECT => $this->normalizeMultiSelectValue($rawValue, $definition),
};
}

Expand Down Expand Up @@ -255,6 +256,36 @@ private function normalizeSelectValue(array|string|int|float|bool $rawValue, Fie
return ['value' => $value];
}

/**
* @param array<string, mixed>|scalar $rawValue
* @return array{value: list<string>}
*/
private function normalizeMultiSelectValue(array|string|int|float|bool $rawValue, FieldDefinition $definition): array {
if (!is_array($rawValue) || !array_is_list($rawValue)) {
throw new InvalidArgumentException($this->l10n->t('Multiselect fields require one or more configured option values.'));
}

$options = json_decode($definition->getOptions() ?? '[]', true);
$normalized = [];
foreach ($rawValue as $candidate) {
if (!is_string($candidate) || trim($candidate) === '') {
throw new InvalidArgumentException($this->l10n->t('Multiselect fields require one or more configured option values.'));
}

$value = trim($candidate);
if (!in_array($value, $options, true)) {
// TRANSLATORS %s is an invalid option value provided by the user.
throw new InvalidArgumentException($this->l10n->t('"%s" is not a valid option for this field', [$value]));
}

if (!in_array($value, $normalized, true)) {
$normalized[] = $value;
}
}

return ['value' => $normalized];
}

/**
* @param array<string, mixed>|scalar $rawValue
* @return array{value: int|float}
Expand Down
76 changes: 70 additions & 6 deletions lib/Workflow/UserProfileFieldCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,11 @@ public function validateCheck($operator, $value) {

if ($this->operatorRequiresValue((string)$operator)) {
try {
$this->fieldValueService->normalizeValue($definition, $config['value']);
if (FieldType::from($definition->getType()) === FieldType::MULTISELECT) {
$this->normalizeExpectedMultiSelectOperand($definition, $config['value']);
} else {
$this->fieldValueService->normalizeValue($definition, $config['value']);
}
} catch (InvalidArgumentException $exception) {
throw new \UnexpectedValueException($this->l10n->t('The configured comparison value is invalid'), 4, $exception);
}
Expand Down Expand Up @@ -163,6 +167,7 @@ private function isOperatorSupported(FieldDefinition $definition, string $operat
FieldType::TEXT => self::TEXT_OPERATORS,
FieldType::NUMBER => self::NUMBER_OPERATORS,
FieldType::SELECT => self::SELECT_OPERATORS,
FieldType::MULTISELECT => self::SELECT_OPERATORS,
};

return in_array($operator, $operators, true);
Expand All @@ -172,19 +177,38 @@ private function operatorRequiresValue(string $operator): bool {
return !in_array($operator, [self::OPERATOR_IS_SET, self::OPERATOR_IS_NOT_SET], true);
}

private function extractActualValue(?FieldValue $value): string|int|float|bool|null {
/**
* @return string|int|float|bool|list<string>|null
*/
private function extractActualValue(?FieldValue $value): string|int|float|bool|array|null {
if ($value === null) {
return null;
}

$serialized = $this->fieldValueService->serializeForResponse($value);
$payload = $serialized['value']['value'] ?? null;

return is_array($payload) || is_object($payload) ? null : $payload;
if (is_array($payload) && array_is_list($payload)) {
$normalized = [];
foreach ($payload as $candidate) {
if (is_string($candidate)) {
$normalized[] = $candidate;
}
}

return $normalized;
}

return is_object($payload) ? null : $payload;
}

private function evaluate(FieldDefinition $definition, string $operator, string|int|float|bool|null $expectedRawValue, string|int|float|bool|null $actualValue): bool {
$isSet = $actualValue !== null && $actualValue !== '';
/**
* @param string|int|float|bool|list<string>|null $actualValue
*/
private function evaluate(FieldDefinition $definition, string $operator, string|int|float|bool|null $expectedRawValue, string|int|float|bool|array|null $actualValue): bool {
$isSet = $actualValue !== null
&& $actualValue !== ''
&& (!is_array($actualValue) || count($actualValue) > 0);
if ($operator === self::OPERATOR_IS_SET) {
return $isSet;
}
Expand All @@ -195,20 +219,60 @@ private function evaluate(FieldDefinition $definition, string $operator, string|
return false;
}

$fieldType = FieldType::from($definition->getType());

if ($fieldType === FieldType::MULTISELECT) {
if (!is_array($actualValue)) {
return false;
}

$expectedValue = $this->normalizeExpectedMultiSelectOperand($definition, $expectedRawValue);

return $this->evaluateMultiSelectOperator($operator, $expectedValue, $actualValue);
}

$normalizedExpected = $this->fieldValueService->normalizeValue($definition, $expectedRawValue);
$expectedValue = $normalizedExpected['value'] ?? null;

return match (FieldType::from($definition->getType())) {
return match ($fieldType) {
FieldType::TEXT,
FieldType::SELECT => $this->evaluateTextOperator($operator, (string)$expectedValue, (string)$actualValue),
FieldType::NUMBER => $this->evaluateNumberOperator(
$operator,
$this->normalizeNumericComparisonOperand($expectedValue),
$this->normalizeNumericComparisonOperand($actualValue),
),
FieldType::MULTISELECT => false,
};
}

/**
* @param list<string> $actualValues
*/
private function evaluateMultiSelectOperator(string $operator, string $expectedValue, array $actualValues): bool {
$contains = in_array($expectedValue, $actualValues, true);

return match ($operator) {
'is' => $contains,
'!is' => !$contains,
default => false,
};
}

private function normalizeExpectedMultiSelectOperand(FieldDefinition $definition, string|int|float|bool|null $expectedRawValue): string {
if (!is_string($expectedRawValue)) {
throw new InvalidArgumentException('multiselect comparison value must be one configured option');
}

$value = trim($expectedRawValue);
$options = json_decode($definition->getOptions() ?? '[]', true);
if ($value === '' || !is_array($options) || !in_array($value, $options, true)) {
throw new InvalidArgumentException('multiselect comparison value must be one configured option');
}

return $value;
}

private function normalizeNumericComparisonOperand(string|int|float|bool|null $value): int|float {
if (is_int($value) || is_float($value)) {
return $value;
Expand Down
3 changes: 2 additions & 1 deletion openapi-administration.json
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,8 @@
"enum": [
"text",
"number",
"select"
"select",
"multiselect"
]
},
"ValuePayload": {
Expand Down
3 changes: 2 additions & 1 deletion openapi-full.json
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,8 @@
"enum": [
"text",
"number",
"select"
"select",
"multiselect"
]
},
"ValuePayload": {
Expand Down
3 changes: 2 additions & 1 deletion openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@
"enum": [
"text",
"number",
"select"
"select",
"multiselect"
]
},
"ValuePayload": {
Expand Down
Loading
Loading