From b97dc17e35c23df0994bf764ef421bc44ec2cab0 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:59:28 -0300 Subject: [PATCH 01/26] test(url): add FieldDefinitionValidator test for URL type Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../php/Unit/Service/FieldDefinitionValidatorTest.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/php/Unit/Service/FieldDefinitionValidatorTest.php b/tests/php/Unit/Service/FieldDefinitionValidatorTest.php index 95dfe66..867e4d6 100644 --- a/tests/php/Unit/Service/FieldDefinitionValidatorTest.php +++ b/tests/php/Unit/Service/FieldDefinitionValidatorTest.php @@ -130,6 +130,17 @@ public function testValidateBooleanFieldDefinition(): void { $this->assertNull($validated['options']); } + public function testValidateUrlFieldDefinition(): void { + $validated = $this->validator->validate([ + 'field_key' => 'website', + 'label' => 'Website', + 'type' => FieldType::URL->value, + ]); + + $this->assertSame(FieldType::URL->value, $validated['type']); + $this->assertNull($validated['options']); + } + public function testRejectMultiSelectWithNoOptions(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('multiselect fields require at least one option'); From 5a3b0e286ee59edc335ecf2ec6f75909420aee8d Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:59:33 -0300 Subject: [PATCH 02/26] test(url): add FieldValueService tests for URL normalization Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../Unit/Service/FieldValueServiceTest.php | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/php/Unit/Service/FieldValueServiceTest.php b/tests/php/Unit/Service/FieldValueServiceTest.php index 98348ab..481c53b 100644 --- a/tests/php/Unit/Service/FieldValueServiceTest.php +++ b/tests/php/Unit/Service/FieldValueServiceTest.php @@ -81,6 +81,40 @@ public function testNormalizeBooleanValueRejectsInvalidValue(): void { $this->service->normalizeValue($definition, 'yes'); } + public function testNormalizeUrlValueAcceptsValidUrl(): void { + $definition = $this->buildDefinition(FieldType::URL->value); + + $normalized = $this->service->normalizeValue($definition, 'https://example.com'); + + $this->assertSame(['value' => 'https://example.com'], $normalized); + } + + public function testNormalizeUrlValueTrimsWhitespace(): void { + $definition = $this->buildDefinition(FieldType::URL->value); + + $normalized = $this->service->normalizeValue($definition, ' https://example.com '); + + $this->assertSame(['value' => 'https://example.com'], $normalized); + } + + public function testNormalizeUrlValueRejectsNonUrl(): void { + $definition = $this->buildDefinition(FieldType::URL->value); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('URL fields require a valid URL.'); + + $this->service->normalizeValue($definition, 'not-a-url'); + } + + public function testNormalizeUrlValueRejectsArray(): void { + $definition = $this->buildDefinition(FieldType::URL->value); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('URL fields require a valid URL.'); + + $this->service->normalizeValue($definition, ['https://example.com']); + } + public function testNormalizeMissingValueAsNull(): void { $definition = $this->buildDefinition(FieldType::TEXT->value); From 1c2e086dfa969979b352e17a5c04b6448c570400 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:59:38 -0300 Subject: [PATCH 03/26] test(url): add UserProfileFieldCheck tests for URL operators Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../Workflow/UserProfileFieldCheckTest.php | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/php/Unit/Workflow/UserProfileFieldCheckTest.php b/tests/php/Unit/Workflow/UserProfileFieldCheckTest.php index 37aaf2d..e1867f1 100644 --- a/tests/php/Unit/Workflow/UserProfileFieldCheckTest.php +++ b/tests/php/Unit/Workflow/UserProfileFieldCheckTest.php @@ -193,6 +193,35 @@ public function testValidateCheckRejectsContainsForBooleanField(): void { $this->check->validateCheck('contains', $this->encodeConfig('is_manager', true)); } + public function testExecuteCheckMatchesUrlContains(): void { + $definition = $this->buildDefinition(8, 'website', FieldType::URL->value); + $value = $this->buildStoredValue(8, 'alice', '{"value":"https://example.com/page"}'); + + $this->fieldDefinitionService->expects($this->once()) + ->method('findByFieldKey') + ->with('website') + ->willReturn($definition); + $this->fieldValueMapper->expects($this->once()) + ->method('findByFieldDefinitionIdAndUserUid') + ->with(8, 'alice') + ->willReturn($value); + + $this->userSession->method('getUser')->willReturn($this->buildUser('alice')); + + $this->assertTrue($this->check->executeCheck('contains', $this->encodeConfig('website', 'example.com'))); + } + + public function testValidateCheckAcceptsContainsForUrlField(): void { + $this->fieldDefinitionService->expects($this->once()) + ->method('findByFieldKey') + ->with('website') + ->willReturn($this->buildDefinition(8, 'website', FieldType::URL->value)); + + // Should not throw + $this->check->validateCheck('contains', $this->encodeConfig('website', 'example.com')); + $this->addToAssertionCount(1); + } + public function testExecuteCheckTreatsMissingValueAsNotSet(): void { $definition = $this->buildDefinition(7, 'region', FieldType::TEXT->value); From 0b0bff5b388acc4be0defac4942be5c39cd48dbb Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:59:42 -0300 Subject: [PATCH 04/26] feat(url): add URL enum case to FieldType Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Enum/FieldType.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/Enum/FieldType.php b/lib/Enum/FieldType.php index 2713a0e..9dd2a8b 100644 --- a/lib/Enum/FieldType.php +++ b/lib/Enum/FieldType.php @@ -14,6 +14,7 @@ enum FieldType: string { case NUMBER = 'number'; case BOOLEAN = 'boolean'; case DATE = 'date'; + case URL = 'url'; case SELECT = 'select'; case MULTISELECT = 'multiselect'; @@ -26,6 +27,7 @@ public static function values(): array { self::NUMBER->value, self::BOOLEAN->value, self::DATE->value, + self::URL->value, self::SELECT->value, self::MULTISELECT->value, ]; From 427de2db0311d64eef2e225b44493d1d5a255f37 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:59:47 -0300 Subject: [PATCH 05/26] feat(url): add url to ProfileFieldsType psalm-type Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/ResponseDefinitions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index 832a05a..89699bc 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -10,7 +10,7 @@ namespace OCA\ProfileFields; /** - * @psalm-type ProfileFieldsType = 'text'|'number'|'boolean'|'date'|'select'|'multiselect' + * @psalm-type ProfileFieldsType = 'text'|'number'|'boolean'|'date'|'url'|'select'|'multiselect' * @psalm-type ProfileFieldsVisibility = 'private'|'users'|'public' * @psalm-type ProfileFieldsEditPolicy = 'admins'|'users' * @psalm-type ProfileFieldsExposurePolicy = 'hidden'|'private'|'users'|'public' From 616e611c7c057e294ac55594bdfabe91c06e8975 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:59:52 -0300 Subject: [PATCH 06/26] feat(url): support url in FieldDefinitionValidator type annotation Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/FieldDefinitionValidator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/FieldDefinitionValidator.php b/lib/Service/FieldDefinitionValidator.php index ea3dbf5..3257946 100644 --- a/lib/Service/FieldDefinitionValidator.php +++ b/lib/Service/FieldDefinitionValidator.php @@ -29,7 +29,7 @@ class FieldDefinitionValidator { * @return array{ * field_key: non-empty-string, * label: non-empty-string, - * type: 'text'|'number'|'boolean'|'date'|'select'|'multiselect', + * type: 'text'|'number'|'boolean'|'date'|'url'|'select'|'multiselect', * edit_policy: 'admins'|'users', * exposure_policy: 'hidden'|'private'|'users'|'public', * sort_order: int, From baa52a334f1ce435934374bfbf1567d569480555 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:59:57 -0300 Subject: [PATCH 07/26] feat(url): normalize and validate URL field values Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/FieldValueService.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/Service/FieldValueService.php b/lib/Service/FieldValueService.php index 462ac37..5428dbf 100644 --- a/lib/Service/FieldValueService.php +++ b/lib/Service/FieldValueService.php @@ -108,6 +108,7 @@ public function normalizeValue(FieldDefinition $definition, array|string|int|flo FieldType::NUMBER => $this->normalizeNumberValue($rawValue), FieldType::BOOLEAN => $this->normalizeBooleanValue($rawValue), FieldType::DATE => $this->normalizeDateValue($rawValue), + FieldType::URL => $this->normalizeUrlValue($rawValue), FieldType::SELECT => $this->normalizeSelectValue($rawValue, $definition), FieldType::MULTISELECT => $this->normalizeMultiSelectValue($rawValue, $definition), }; @@ -349,6 +350,23 @@ private function normalizeDateValue(array|string|int|float|bool $rawValue): arra return ['value' => $value]; } + /** + * @param array|scalar $rawValue + * @return array{value: string} + */ + private function normalizeUrlValue(array|string|int|float|bool $rawValue): array { + if (!is_string($rawValue)) { + throw new InvalidArgumentException($this->l10n->t('URL fields require a valid URL.')); + } + + $value = trim($rawValue); + if (filter_var($value, FILTER_VALIDATE_URL) === false) { + throw new InvalidArgumentException($this->l10n->t('URL fields require a valid URL.')); + } + + return ['value' => $value]; + } + /** * @param array $value */ From 62e81fb2b697fc59eb1ce005ab4d9e5eb763c5bb Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:00:02 -0300 Subject: [PATCH 08/26] feat(url): add url to type union in ImportPayloadValidator Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/ImportPayloadValidator.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/Service/ImportPayloadValidator.php b/lib/Service/ImportPayloadValidator.php index 3f2837b..0186433 100644 --- a/lib/Service/ImportPayloadValidator.php +++ b/lib/Service/ImportPayloadValidator.php @@ -32,7 +32,7 @@ public function __construct( * definitions: list Date: Fri, 20 Mar 2026 20:00:07 -0300 Subject: [PATCH 09/26] feat(url): add url to type annotations in DataImportService Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/DataImportService.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Service/DataImportService.php b/lib/Service/DataImportService.php index b915d7b..e7cd8a2 100644 --- a/lib/Service/DataImportService.php +++ b/lib/Service/DataImportService.php @@ -69,7 +69,7 @@ public function import(array $payload, bool $dryRun = false): array { * @param list Date: Fri, 20 Mar 2026 20:00:12 -0300 Subject: [PATCH 10/26] feat(url): add URL operators and contains bypass in workflow check Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Workflow/UserProfileFieldCheck.php | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/Workflow/UserProfileFieldCheck.php b/lib/Workflow/UserProfileFieldCheck.php index a51ced2..8d7e09e 100644 --- a/lib/Workflow/UserProfileFieldCheck.php +++ b/lib/Workflow/UserProfileFieldCheck.php @@ -59,6 +59,14 @@ class UserProfileFieldCheck implements ICheck { 'is', '!is', ]; + private const URL_OPERATORS = [ + self::OPERATOR_IS_SET, + self::OPERATOR_IS_NOT_SET, + 'is', + '!is', + 'contains', + '!contains', + ]; private const SELECT_OPERATORS = [ self::OPERATOR_IS_SET, self::OPERATOR_IS_NOT_SET, @@ -122,8 +130,11 @@ public function validateCheck($operator, $value) { if ($this->operatorRequiresValue((string)$operator)) { try { - if (FieldType::from($definition->getType()) === FieldType::MULTISELECT) { + $fieldType = FieldType::from($definition->getType()); + if ($fieldType === FieldType::MULTISELECT) { $this->normalizeExpectedMultiSelectOperand($definition, $config['value']); + } elseif ($fieldType === FieldType::URL && ((string)$operator === 'contains' || (string)$operator === '!contains')) { + // URL contains search terms are plain substrings — no URL validation needed. } else { $this->fieldValueService->normalizeValue($definition, $config['value']); } @@ -184,6 +195,7 @@ private function isOperatorSupported(FieldDefinition $definition, string $operat FieldType::NUMBER => self::NUMBER_OPERATORS, FieldType::BOOLEAN => self::BOOLEAN_OPERATORS, FieldType::DATE => self::DATE_OPERATORS, + FieldType::URL => self::URL_OPERATORS, FieldType::SELECT => self::SELECT_OPERATORS, FieldType::MULTISELECT => self::SELECT_OPERATORS, }; @@ -249,11 +261,16 @@ private function evaluate(FieldDefinition $definition, string $operator, string| return $this->evaluateMultiSelectOperator($operator, $expectedValue, $actualValue); } + if ($fieldType === FieldType::URL && ($operator === 'contains' || $operator === '!contains')) { + return $this->evaluateTextOperator($operator, trim((string)$expectedRawValue), (string)$actualValue); + } + $normalizedExpected = $this->fieldValueService->normalizeValue($definition, $expectedRawValue); $expectedValue = $normalizedExpected['value'] ?? null; return match ($fieldType) { FieldType::TEXT, + FieldType::URL, FieldType::SELECT => $this->evaluateTextOperator($operator, (string)$expectedValue, (string)$actualValue), FieldType::BOOLEAN => $this->evaluateBooleanOperator( $operator, From 524231e383fa63a343385ed38dbc967c03eccc03 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:00:16 -0300 Subject: [PATCH 11/26] feat(url): add url to FieldType union type Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/types/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/index.ts b/src/types/index.ts index b1c460c..a7ce545 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -19,7 +19,7 @@ type ApiOperationRequestBody = TOperation extends { type ApiRequestJsonBody = ApiJsonBody> -export type FieldType = ApiComponents['schemas']['Type'] | 'multiselect' | 'date' | 'boolean' +export type FieldType = ApiComponents['schemas']['Type'] | 'multiselect' | 'date' | 'boolean' | 'url' export type FieldVisibility = ApiComponents['schemas']['Visibility'] export type FieldEditPolicy = 'admins' | 'users' export type FieldExposurePolicy = 'hidden' | FieldVisibility From 031cff36216af93e5b93be03b78552f1b4163ce1 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:00:21 -0300 Subject: [PATCH 12/26] feat(url): return text operators for URL type in workflow check Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/utils/workflowProfileFieldCheck.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/utils/workflowProfileFieldCheck.ts b/src/utils/workflowProfileFieldCheck.ts index 6937721..9e6c0ed 100644 --- a/src/utils/workflowProfileFieldCheck.ts +++ b/src/utils/workflowProfileFieldCheck.ts @@ -68,6 +68,8 @@ export const getWorkflowOperatorKeys = (rawValue: string | null | undefined, def ? [...numberOperatorKeys] : definition.type === 'boolean' ? [...booleanOperatorKeys] + : definition.type === 'url' + ? [...textOperatorKeys] : [...textOperatorKeys] } From 575f76a682ed31ad7b60ce7b8df5af2b9adc5fa0 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:00:25 -0300 Subject: [PATCH 13/26] feat(url): add URL option to field type selector in admin settings Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/views/AdminSettings.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/src/views/AdminSettings.vue b/src/views/AdminSettings.vue index e714132..78193a2 100644 --- a/src/views/AdminSettings.vue +++ b/src/views/AdminSettings.vue @@ -289,6 +289,7 @@ const fieldTypeOptions: Array<{ value: FieldType, label: string }> = [ { value: 'number', label: t('profile_fields', 'Number') }, { value: 'boolean', label: t('profile_fields', 'Boolean') }, { value: 'date', label: t('profile_fields', 'Date') }, + { value: 'url', label: t('profile_fields', 'URL') }, { value: 'select', label: t('profile_fields', 'Select') }, { value: 'multiselect', label: t('profile_fields', 'Multiselect') }, ] From 26494d1c1dc0645b80a3b02fe4b288dfa2f9bbe5 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:00:30 -0300 Subject: [PATCH 14/26] feat(url): support url input type in personal settings Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/views/PersonalSettings.vue | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/views/PersonalSettings.vue b/src/views/PersonalSettings.vue index 9e45480..9194c12 100644 --- a/src/views/PersonalSettings.vue +++ b/src/views/PersonalSettings.vue @@ -268,16 +268,17 @@ const managedByAdminAriaLabel = (fieldLabel: string) => t('profile_fields', '{fi const fieldInputId = (fieldId: number) => `profile-fields-personal-value-${fieldId}` -const componentInputTypesByType: Record = { +const componentInputTypesByType: Record = { text: 'text', number: 'number', boolean: 'text', date: 'date', + url: 'url', select: 'text', multiselect: 'text', } -const componentInputTypeForType = (type: FieldType): 'text' | 'number' | 'date' => { +const componentInputTypeForType = (type: FieldType): 'text' | 'number' | 'date' | 'url' => { return componentInputTypesByType[type] } @@ -453,7 +454,7 @@ const canAutosaveField = (field: EditableField) => { return field.definition.type === 'multiselect' } - if (field.definition.type === 'text' || field.definition.type === 'select' || field.definition.type === 'date' || field.definition.type === 'boolean') { + if (field.definition.type === 'text' || field.definition.type === 'select' || field.definition.type === 'date' || field.definition.type === 'boolean' || field.definition.type === 'url') { return true } From 1d9d516515ad7a0e158fb2bac939fb698e01b233 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:00:35 -0300 Subject: [PATCH 15/26] feat(url): add URL field type support in AdminUserFieldsDialog Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/components/AdminUserFieldsDialog.vue | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/components/AdminUserFieldsDialog.vue b/src/components/AdminUserFieldsDialog.vue index 4e7ec25..e4c9be6 100644 --- a/src/components/AdminUserFieldsDialog.vue +++ b/src/components/AdminUserFieldsDialog.vue @@ -211,6 +211,7 @@ export default defineComponent({ 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.'), + url: t('profile_fields', 'Enter a valid URL (e.g. https://example.com).'), select: t('profile_fields', 'Choose one of the predefined options.'), multiselect: t('profile_fields', 'Choose one or more predefined options.'), } as Record)[type] @@ -220,6 +221,7 @@ export default defineComponent({ number: t('profile_fields', 'Enter a number'), boolean: t('profile_fields', 'Select true or false'), date: t('profile_fields', 'Select a date'), + url: t('profile_fields', 'Enter a URL'), select: t('profile_fields', 'Select an option'), multiselect: t('profile_fields', 'Select one or more options'), } as Record)[type] @@ -232,6 +234,7 @@ export default defineComponent({ number: 'decimal', boolean: 'text', date: 'numeric', + url: 'url', select: 'text', multiselect: 'text', } as Record)[type] @@ -241,6 +244,7 @@ export default defineComponent({ number: 'text', boolean: 'text', date: 'date', + url: 'url', select: 'text', multiselect: 'text', } as Record)[type] @@ -324,6 +328,15 @@ export default defineComponent({ } } + if (field.definition.type === 'url') { + try { + // eslint-disable-next-line no-new + new URL(rawValue) + } catch { + return t('profile_fields', '{fieldLabel} must be a valid URL.', { fieldLabel: field.definition.label }) + } + } + if (field.definition.type === 'select') { const options = field.definition.options ?? [] if (!options.includes(rawValue)) { @@ -355,7 +368,7 @@ export default defineComponent({ const hasInvalidFields = computed(() => invalidFields.value.length > 0) const helperTextForField = (field: AdminEditableField) => { - return field.definition.type === 'number' || field.definition.type === 'date' || field.definition.type === 'boolean' + return field.definition.type === 'number' || field.definition.type === 'date' || field.definition.type === 'boolean' || field.definition.type === 'url' ? descriptionForType(field.definition.type) : '' } @@ -430,6 +443,7 @@ export default defineComponent({ '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 }), + 'URL fields require a valid URL.': t('profile_fields', '{fieldLabel} must be a valid URL.', { 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') ? t('profile_fields', '{fieldLabel}: invalid option selected.', { fieldLabel: field.definition.label }) From fb33b385e1df1a9336271aa8e1efcbda563a4a00 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:00:41 -0300 Subject: [PATCH 16/26] test(url): add workflow check tests for URL type operators Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../utils/workflowProfileFieldCheck.spec.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/tests/utils/workflowProfileFieldCheck.spec.ts b/src/tests/utils/workflowProfileFieldCheck.spec.ts index 22fd719..691b460 100644 --- a/src/tests/utils/workflowProfileFieldCheck.spec.ts +++ b/src/tests/utils/workflowProfileFieldCheck.spec.ts @@ -15,6 +15,7 @@ const definitions = [ { field_key: 'score', label: 'Score', type: 'number', active: true }, { field_key: 'start_date', label: 'Start date', type: 'date', active: true }, { field_key: 'is_manager', label: 'Is manager', type: 'boolean', active: true }, + { field_key: 'website', label: 'Website', type: 'url', active: true }, ] as const describe('workflowProfileFieldCheck', () => { @@ -74,4 +75,19 @@ describe('workflowProfileFieldCheck', () => { expect(isWorkflowOperatorSupported('contains', serializeWorkflowCheckValue({ field_key: 'score', value: '9' }), definitions)).toBe(false) expect(isWorkflowOperatorSupported('greater', serializeWorkflowCheckValue({ field_key: 'score', value: '9' }), definitions)).toBe(true) }) + + it('returns text-style operators for url definitions', () => { + expect(getWorkflowOperatorKeys(serializeWorkflowCheckValue({ field_key: 'website', value: 'https://example.com' }), definitions)).toEqual([ + 'is-set', + '!is-set', + 'is', + '!is', + 'contains', + '!contains', + ]) + }) + + it('accepts contains operator for url field', () => { + expect(isWorkflowOperatorSupported('contains', serializeWorkflowCheckValue({ field_key: 'website', value: 'example.com' }), definitions)).toBe(true) + }) }) From 31dab839fe3f475db30fa7cdf80bccb19cbdfbe3 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:00:46 -0300 Subject: [PATCH 17/26] test(url): add URL type option test in AdminSettings spec Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/tests/components/AdminSettings.spec.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/tests/components/AdminSettings.spec.ts b/src/tests/components/AdminSettings.spec.ts index 4a2c873..5a5c1b9 100644 --- a/src/tests/components/AdminSettings.spec.ts +++ b/src/tests/components/AdminSettings.spec.ts @@ -105,4 +105,19 @@ describe('AdminSettings', () => { expect(wrapper.text()).toContain('tr:Boolean') }) + + it('offers the URL field type in the editor', async() => { + const wrapper = mount(AdminSettings, { + global: { + stubs: { + Draggable: defineComponent({ template: '
' }), + }, + }, + }) + + await flushPromises() + await wrapper.get('button').trigger('click') + + expect(wrapper.text()).toContain('tr:URL') + }) }) \ No newline at end of file From e5e7a35b5efa96956d13f62a8872896e4a332dfa Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:00:51 -0300 Subject: [PATCH 18/26] test(url): add URL field type render and helper-text tests Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../components/AdminUserFieldsDialog.spec.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/tests/components/AdminUserFieldsDialog.spec.ts b/src/tests/components/AdminUserFieldsDialog.spec.ts index 95682ae..384ef0a 100644 --- a/src/tests/components/AdminUserFieldsDialog.spec.ts +++ b/src/tests/components/AdminUserFieldsDialog.spec.ts @@ -48,6 +48,17 @@ vi.mock('../../api', () => ({ active: true, options: null, }, + { + id: 5, + field_key: 'website', + label: 'Website', + type: 'url', + edit_policy: 'users', + exposure_policy: 'private', + sort_order: 2, + active: true, + options: null, + }, ]), listAdminUserValues: vi.fn().mockResolvedValue([ { @@ -131,4 +142,40 @@ describe('AdminUserFieldsDialog', () => { expect(wrapper.text()).toContain('tr:True') expect(wrapper.text()).toContain('tr:False') }) + + it('renders url fields with type=url input', async() => { + const wrapper = mount(AdminUserFieldsDialog, { + props: { + open: true, + userUid: 'alice', + userDisplayName: 'Alice', + }, + }) + + await flushPromises() + + const urlInput = wrapper.find('#profile-fields-user-dialog-value-5') + expect(urlInput.exists()).toBe(true) + expect(urlInput.attributes('type')).toBe('url') + }) + + it('shows url helper text for url fields', async() => { + const wrapper = mount(AdminUserFieldsDialog, { + props: { + open: true, + userUid: 'alice', + userDisplayName: 'Alice', + }, + }) + + await flushPromises() + + // The helper text is passed via the :helper-text prop on NcInputField for the url field. + // Verify the URL field renders its description via a data-testid selector approach: + // the NcInputField mock renders with all bound attrs so helper-text appears as a DOM attribute. + const urlInput = wrapper.find('#profile-fields-user-dialog-value-5') + expect(urlInput.exists()).toBe(true) + // helper-text is bound as an attribute through v-bind="$attrs" + expect(urlInput.attributes('helper-text')).toBeTruthy() + }) }) \ No newline at end of file From d89c86631d562f649aaabd7537ec9d4d73194b18 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:00:55 -0300 Subject: [PATCH 19/26] test(url): add URL input type test in PersonalSettings spec Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/tests/components/PersonalSettings.spec.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/tests/components/PersonalSettings.spec.ts b/src/tests/components/PersonalSettings.spec.ts index 3bebbba..364c6d3 100644 --- a/src/tests/components/PersonalSettings.spec.ts +++ b/src/tests/components/PersonalSettings.spec.ts @@ -64,6 +64,29 @@ vi.mock('../../api', () => ({ }, can_edit: true, }, + { + definition: { + id: 5, + field_key: 'website', + label: 'Website', + type: 'url', + edit_policy: 'users', + exposure_policy: 'private', + sort_order: 2, + active: true, + options: null, + }, + value: { + id: 12, + field_definition_id: 5, + user_uid: 'alice', + value: { value: 'https://example.com' }, + current_visibility: 'private', + updated_by_uid: 'alice', + updated_at: '2026-03-20T12:00:00+00:00', + }, + can_edit: true, + }, ]), upsertOwnValue: vi.fn(), })) @@ -134,4 +157,20 @@ describe('PersonalSettings', () => { expect(wrapper.text()).toContain('tr:True') expect(wrapper.text()).toContain('tr:False') }) + + it('renders url fields with type=url input', async() => { + const wrapper = mount(PersonalSettings, { + global: { + stubs: { + Teleport: true, + }, + }, + }) + + await flushPromises() + + const input = wrapper.find('[data-testid="profile-fields-personal-input-website"]') + expect(input.exists()).toBe(true) + expect(input.attributes('type')).toBe('url') + }) }) \ No newline at end of file From 98584cedef55ec47ee0ea661db875e06737e8c00 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:01:01 -0300 Subject: [PATCH 20/26] chore(url): regenerate openapi.json with url type Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- openapi.json | 1 + 1 file changed, 1 insertion(+) diff --git a/openapi.json b/openapi.json index 415c600..6686e13 100644 --- a/openapi.json +++ b/openapi.json @@ -146,6 +146,7 @@ "number", "boolean", "date", + "url", "select", "multiselect" ] From 70f246a415d48f0f95d2912f17025f2d9ca6a315 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:01:06 -0300 Subject: [PATCH 21/26] chore(url): regenerate openapi-full.json with url type Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- openapi-full.json | 1 + 1 file changed, 1 insertion(+) diff --git a/openapi-full.json b/openapi-full.json index 312f374..d7246b7 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -275,6 +275,7 @@ "number", "boolean", "date", + "url", "select", "multiselect" ] From 24b67983e82a5a3ce0b1945d67c042fb8e89940b Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:01:11 -0300 Subject: [PATCH 22/26] chore(url): regenerate openapi-administration.json with url type Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- openapi-administration.json | 1 + 1 file changed, 1 insertion(+) diff --git a/openapi-administration.json b/openapi-administration.json index f0c947d..39c6287 100644 --- a/openapi-administration.json +++ b/openapi-administration.json @@ -200,6 +200,7 @@ "number", "boolean", "date", + "url", "select", "multiselect" ] From 922464251e967123111fbfd6e9fc8f94c775dabf Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:01:16 -0300 Subject: [PATCH 23/26] chore(url): regenerate openapi.ts types with url type Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/types/openapi/openapi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index c87e0bb..10e19d1 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -100,7 +100,7 @@ export type components = { itemsperpage?: string; }; /** @enum {string} */ - Type: "text" | "number" | "boolean" | "date" | "select" | "multiselect"; + Type: "text" | "number" | "boolean" | "date" | "url" | "select" | "multiselect"; ValuePayload: { value: Record; }; From 323ae095824cde4ce1661f259aaac16a98a90a74 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:01:21 -0300 Subject: [PATCH 24/26] chore(url): regenerate openapi-full.ts types with url type Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/types/openapi/openapi-full.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/openapi/openapi-full.ts b/src/types/openapi/openapi-full.ts index 74a1eb3..41baa38 100644 --- a/src/types/openapi/openapi-full.ts +++ b/src/types/openapi/openapi-full.ts @@ -276,7 +276,7 @@ export type components = { }; }; /** @enum {string} */ - Type: "text" | "number" | "boolean" | "date" | "select" | "multiselect"; + Type: "text" | "number" | "boolean" | "date" | "url" | "select" | "multiselect"; ValuePayload: { value: Record; }; From 1c1d7a610b99eba3efa8826877a055cfe0e5ff76 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:01:26 -0300 Subject: [PATCH 25/26] chore(url): regenerate openapi-administration.ts types with url type Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/types/openapi/openapi-administration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/openapi/openapi-administration.ts b/src/types/openapi/openapi-administration.ts index ad1fcaa..3093aa6 100644 --- a/src/types/openapi/openapi-administration.ts +++ b/src/types/openapi/openapi-administration.ts @@ -193,7 +193,7 @@ export type components = { }; }; /** @enum {string} */ - Type: "text" | "number" | "boolean" | "date" | "select" | "multiselect"; + Type: "text" | "number" | "boolean" | "date" | "url" | "select" | "multiselect"; ValuePayload: { value: Record; }; From 6d142533e10f658c91f2a3b9ad09bec289dea70e Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:01:31 -0300 Subject: [PATCH 26/26] docs(url): add admin proof screenshot for URL field type Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- img/screenshots/url-field-admin-proof.png | Bin 0 -> 29696 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 img/screenshots/url-field-admin-proof.png diff --git a/img/screenshots/url-field-admin-proof.png b/img/screenshots/url-field-admin-proof.png new file mode 100644 index 0000000000000000000000000000000000000000..47e2c3d96a0511a050567e98a3547315cc018a47 GIT binary patch literal 29696 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 literal 0 HcmV?d00001