@@ -4,8 +4,8 @@ import find from 'lodash/find';
44import startsWith from 'lodash/startsWith' ;
55import isEmpty from 'lodash/isEmpty' ;
66import { Button , Stack , TextField , Typography } from '@mui/material' ;
7+ import type { JSONSchema7 as JSONSchema } from 'json-schema' ;
78import { Callout } from '../Callout' ;
8- import type { TFunction } from '../../hooks/useTranslations' ;
99import { useTranslation } from '../../hooks/useTranslations' ;
1010import {
1111 stopKeyDownEvent ,
@@ -21,21 +21,41 @@ const RESERVED_NAMESPACES = ['io.resin.', 'io.balena.'];
2121
2222const newTagValidationRules = < T extends object > (
2323 t : ReturnType < typeof useTranslation > [ 't' ] ,
24- key : string ,
24+ schema : JSONSchema | undefined ,
2525 existingTags : Array < ResourceTagInfo < T > > ,
26- ) => {
26+ key : string ,
27+ value : string ,
28+ ) : Array < {
29+ test : ( ) => boolean ;
30+ field : 'tag_key' | 'value' ;
31+ message : string ;
32+ } > => {
33+ const tagKeySchema = schema ?. properties ?. tag_key ;
34+ const tagValueSchema = schema ?. properties ?. value ;
35+ const tagKeyMaxLength =
36+ tagKeySchema != null && typeof tagKeySchema === 'object'
37+ ? tagKeySchema . maxLength
38+ : null ;
39+ const tagValueMaxLength =
40+ tagValueSchema != null && typeof tagValueSchema === 'object'
41+ ? tagValueSchema . maxLength
42+ : null ;
43+
2744 return [
2845 {
2946 test : ( ) => ! key || isEmpty ( key ) ,
47+ field : 'tag_key' ,
3048 message : t ( 'fields_errors.tag_name_cannot_be_empty' ) ,
3149 } ,
3250 {
3351 test : ( ) => / \s / . test ( key ) ,
52+ field : 'tag_key' ,
3453 message : t ( 'fields_errors.tag_names_cannot_contain_whitespace' ) ,
3554 } ,
3655 {
3756 test : ( ) =>
3857 RESERVED_NAMESPACES . some ( ( reserved ) => startsWith ( key , reserved ) ) ,
58+ field : 'tag_key' ,
3959 message : t ( `fields_errors.some_tag_keys_are_reserved` , {
4060 namespace : RESERVED_NAMESPACES . join ( ', ' ) ,
4161 } ) ,
@@ -45,30 +65,61 @@ const newTagValidationRules = <T extends object>(
4565 existingTags . some (
4666 ( tag ) => tag . state !== 'deleted' && tag . tag_key === key ,
4767 ) ,
68+ field : 'tag_key' ,
4869 message : t ( 'fields_errors.tag_with_same_name_exists' ) ,
4970 } ,
71+ ...( tagKeyMaxLength != null
72+ ? ( [
73+ {
74+ test : ( ) => key . length > tagKeyMaxLength ,
75+ field : 'tag_key' ,
76+ message : t (
77+ 'fields_errors.tag_name_cannot_longer_than_maximum_characters' ,
78+ { maximum : tagKeyMaxLength } ,
79+ ) ,
80+ } ,
81+ ] satisfies ReturnType < typeof newTagValidationRules > )
82+ : [ ] ) ,
83+ ...( tagValueMaxLength != null
84+ ? ( [
85+ {
86+ test : ( ) => value . length > tagValueMaxLength ,
87+ field : 'value' ,
88+ message : t (
89+ 'fields_errors.tag_value_cannot_longer_than_maximum_characters' ,
90+ { maximum : tagValueMaxLength } ,
91+ ) ,
92+ } ,
93+ ] satisfies ReturnType < typeof newTagValidationRules > )
94+ : [ ] ) ,
5095 ] ;
5196} ;
5297
5398interface AddTagFormProps < T > {
54- t : TFunction ;
5599 itemType : string ;
100+ /**
101+ * This is atm only used for constraint validation,
102+ * but in the future it would be great if this becomes mandatory
103+ * and we use an autogenerated form.
104+ */
105+ schema ?: JSONSchema ;
56106 existingTags : Array < ResourceTagInfo < T > > ;
57107 overridableTags ?: Array < ResourceTagInfo < T > > ;
58108 addTag : ( tag : ResourceTagInfo < T > ) => void ;
59109}
60110
61111export const AddTagForm = < T extends object > ( {
62112 itemType,
113+ schema,
63114 existingTags,
64115 overridableTags = [ ] ,
65116 addTag,
66117} : AddTagFormProps < T > ) => {
67118 const { t } = useTranslation ( ) ;
68119 const [ tagKey , setTagKey ] = React . useState ( '' ) ;
69120 const [ value , setValue ] = React . useState ( '' ) ;
70- const [ tagKeyIsInvalid , setTagKeyIsInvalid ] = React . useState ( false ) ;
71- const [ error , setError ] = React . useState < { message : string } > ( ) ;
121+ const [ error , setError ] =
122+ React . useState < ReturnType < typeof newTagValidationRules > [ number ] > ( ) ;
72123 const [ canSubmit , setCanSubmit ] = React . useState ( false ) ;
73124 const [ confirmationDialogOptions , setConfirmationDialogOptions ] =
74125 React . useState < SimpleConfirmationDialogProps > ( ) ;
@@ -79,13 +130,16 @@ export const AddTagForm = <T extends object>({
79130 const formUuid = `add-tag-form-${ formId } ` ;
80131
81132 const checkNewTagValidity = ( key : string ) => {
82- const failedRule = newTagValidationRules < T > ( t , key , existingTags ) . find (
83- ( rule ) => rule . test ( ) ,
84- ) ;
133+ const failedRule = newTagValidationRules < T > (
134+ t ,
135+ schema ,
136+ existingTags ,
137+ key ,
138+ value ,
139+ ) . find ( ( rule ) => rule . test ( ) ) ;
85140
86141 const hasErrors = ! ! failedRule ;
87142
88- setTagKeyIsInvalid ( hasErrors ) ;
89143 setError ( failedRule ) ;
90144 setCanSubmit ( ! hasErrors ) ;
91145 return hasErrors ;
@@ -143,7 +197,6 @@ export const AddTagForm = <T extends object>({
143197
144198 setTagKey ( '' ) ;
145199 setValue ( '' ) ;
146- setTagKeyIsInvalid ( false ) ;
147200 setError ( undefined ) ;
148201 setCanSubmit ( false ) ;
149202
@@ -180,7 +233,7 @@ export const AddTagForm = <T extends object>({
180233 checkNewTagValidity ( e . target . value ) ;
181234 } }
182235 value = { tagKey }
183- error = { tagKeyIsInvalid }
236+ error = { error ?. field === 'tag_key' }
184237 placeholder = { t ( 'labels.tag_name' ) }
185238 />
186239
@@ -196,6 +249,7 @@ export const AddTagForm = <T extends object>({
196249 setValue ( e . target . value ) ;
197250 } }
198251 value = { value }
252+ error = { error ?. field === 'value' }
199253 placeholder = { t ( 'labels.value' ) }
200254 />
201255
0 commit comments