diff --git a/backend/database/mongo_seed/main.go b/backend/database/mongo_seed/main.go index 4f984549..b31a666b 100644 --- a/backend/database/mongo_seed/main.go +++ b/backend/database/mongo_seed/main.go @@ -49,6 +49,31 @@ func main() { IsTerminalStep: true, }, }, + Form: models.Form{Fields: []models.FormField{ + { + Name: "field1", + Title: "Field 1", + Type: models.InputField, + Required: true, + Placeholder: "Enter text...", + MinLength: 1, + }, + { + Name: "field2", + Title: "Field 2", + Type: models.SelectField, + Required: true, + Placeholder: "Select an option", + Options: []string{"Option 1", "Option 2", "Option 3"}, + Default: "Option 1", + }, + { + Name: "field3", + Title: "Field 3", + Type: models.CheckboxField, + Options: []string{"Option 1", "Option 2", "Option 3"}, + }, + }}, } res, err := database.NewPipeline(c).Create(&pipeline) diff --git a/backend/src/database/models/pipeline.go b/backend/src/database/models/pipeline.go index a193d0ad..28803916 100644 --- a/backend/src/database/models/pipeline.go +++ b/backend/src/database/models/pipeline.go @@ -9,19 +9,21 @@ import ( type FormFieldType string const ( - TextField FormFieldType = "TEXT" - DropdownField FormFieldType = "DROPDOWN" - OptionField FormFieldType = "OPTION" - CheckboxField FormFieldType = "CHECKBOX" + InputField FormFieldType = "input" + SelectField FormFieldType = "select" + CheckboxField FormFieldType = "checkboxes" ) type FormField struct { Name string `bson:"name" json:"name"` + Title string `bson:"title" json:"title"` + Description string `bson:"description" json:"description"` Type FormFieldType `bson:"type" json:"type"` - IsRequired bool `bson:"is_required" json:"is_required"` + Required bool `bson:"required" json:"required"` Placeholder string `bson:"placeholder" json:"placeholder"` - Description string `bson:"description" json:"description"` - Values []string `bson:"values" json:"values"` + MinLength int `bson:"min_length" json:"min_length"` + Options []string `bson:"options" json:"options"` + Default string `bson:"default" json:"default"` } type Form struct { diff --git a/backend/src/server/handlers.go b/backend/src/server/handlers.go index f991e9ca..82cede66 100644 --- a/backend/src/server/handlers.go +++ b/backend/src/server/handlers.go @@ -17,6 +17,7 @@ import ( "github.com/joshtyf/flowforge/src/events" "github.com/joshtyf/flowforge/src/logger" "github.com/joshtyf/flowforge/src/util" + "github.com/joshtyf/flowforge/src/validation" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" ) @@ -360,6 +361,20 @@ func handleCreatePipeline(logger logger.ServerLogger, client *mongo.Client) http encode(w, r, http.StatusBadRequest, newHandlerError(ErrJsonParseError, http.StatusBadRequest)) return } + if pipeline.Form.Fields == nil { + logger.Error("missing form field") + encode(w, r, http.StatusUnprocessableEntity, newHandlerError(ErrJsonParseError, http.StatusUnprocessableEntity)) + return + } + + for _, element := range pipeline.Form.Fields { + err = validation.ValidateFormField(element) + if err != nil { + logger.Error(fmt.Sprintf("invalid form field: %s", err)) + encode(w, r, http.StatusBadRequest, newHandlerError(err, http.StatusUnprocessableEntity)) + return + } + } pipeline.CreatedOn = time.Now() pipeline.Version = 1 diff --git a/backend/src/validation/errors.go b/backend/src/validation/errors.go index 671078fa..cc81347c 100644 --- a/backend/src/validation/errors.go +++ b/backend/src/validation/errors.go @@ -31,6 +31,20 @@ func NewMissingRequiredFieldError(fieldName string) *MissingRequiredFieldError { } } +type InvalidPropertyError struct { + fieldName string +} + +func (e *InvalidPropertyError) Error() string { + return fmt.Sprintf("invalid value found for field: '%s'", e.fieldName) +} + +func NewInvalidPropertyValue(fieldName string) *InvalidPropertyError { + return &InvalidPropertyError{ + fieldName: fieldName, + } +} + func (e *MissingRequiredFieldError) Error() string { return fmt.Sprintf("missing required field: '%s'", e.fieldName) } diff --git a/backend/src/validation/validator.go b/backend/src/validation/validator.go index 0e6a2f19..532b45da 100644 --- a/backend/src/validation/validator.go +++ b/backend/src/validation/validator.go @@ -68,12 +68,20 @@ func ValidateFormField(f models.FormField) error { if f.Name == "" { return NewMissingRequiredFieldError("name") } + if f.Title == "" { + return NewMissingRequiredFieldError("title") + } if f.Type == "" { return NewMissingRequiredFieldError("type") } - if f.Type == models.DropdownField || f.Type == models.OptionField || f.Type == models.CheckboxField { - if f.Values == nil || len(f.Values) == 0 { - return NewMissingRequiredFieldError("values") + if f.Type == models.SelectField || f.Type == models.CheckboxField { + if f.Options == nil || len(f.Options) == 0 { + return NewMissingRequiredFieldError("options") + } + for _, option := range f.Options { + if option == "" { + return NewInvalidPropertyValue("options") + } } } return nil @@ -85,8 +93,8 @@ func (f FormFieldDataValidator) validate(field models.FormField, data any) error return f(field, data) } -// Default text field validator -func defaultTextFieldDataValidator(field models.FormField, data any) error { +// Default input field validator +func defaultInputFieldDataValidator(field models.FormField, data any) error { if _, ok := data.(string); !ok { return NewInvalidFormDataTypeError(field.Name, "string") } @@ -94,27 +102,14 @@ func defaultTextFieldDataValidator(field models.FormField, data any) error { return nil } -// Default dropdown field validator -func defaultDropdownFieldDataValidator(field models.FormField, data any) error { - dataStr, ok := data.(string) - if !ok { - return NewInvalidFormDataTypeError(field.Name, "string") - } - if !helper.StringInSlice(dataStr, field.Values) { - return NewInvalidSelectedFormDataError(field.Values, dataStr) - } - - return nil -} - -// Default option field validator -func defaultOptionFieldDataValidator(field models.FormField, data any) error { +// Default select field validator +func defaultSelectFieldDataValidator(field models.FormField, data any) error { dataStr, ok := data.(string) if !ok { return NewInvalidFormDataTypeError(field.Name, "string") } - if !helper.StringInSlice(dataStr, field.Values) { - return NewInvalidSelectedFormDataError(field.Values, dataStr) + if !helper.StringInSlice(dataStr, field.Options) { + return NewInvalidSelectedFormDataError(field.Options, dataStr) } return nil @@ -127,8 +122,8 @@ func defaultCheckboxFieldDataValidator(field models.FormField, data any) error { return NewInvalidFormDataTypeError(field.Name, "[]string") } for _, s := range dataStrings { - if !helper.StringInSlice(s, field.Values) { - return NewInvalidSelectedFormDataError(field.Values, s) + if !helper.StringInSlice(s, field.Options) { + return NewInvalidSelectedFormDataError(field.Options, s) } } return nil @@ -142,9 +137,8 @@ func NewFormDataValidator(customValidators *map[models.FormFieldType]FormFieldDa validator := &FormDataValidator{ // Initialize with default field validators fieldDataValidators: map[models.FormFieldType]FormFieldDataValidator{ - models.TextField: FormFieldDataValidator(defaultTextFieldDataValidator), - models.DropdownField: FormFieldDataValidator(defaultDropdownFieldDataValidator), - models.OptionField: FormFieldDataValidator(defaultOptionFieldDataValidator), + models.InputField: FormFieldDataValidator(defaultInputFieldDataValidator), + models.SelectField: FormFieldDataValidator(defaultSelectFieldDataValidator), models.CheckboxField: FormFieldDataValidator(defaultCheckboxFieldDataValidator), }, } @@ -160,7 +154,7 @@ func (v *FormDataValidator) Validate(formData *models.FormData, form *models.For for _, field := range form.Fields { fieldData, ok := (*formData)[field.Name] if !ok { - if field.IsRequired { + if field.Required { return NewMissingRequiredFieldError(field.Name) } else { continue diff --git a/backend/src/validation/validator_test.go b/backend/src/validation/validator_test.go index 0cbdab5c..32b809d2 100644 --- a/backend/src/validation/validator_test.go +++ b/backend/src/validation/validator_test.go @@ -237,88 +237,92 @@ func TestValidateFormField(t *testing.T) { expected error }{ { - "Valid text field", + "Valid input field", models.FormField{ - Name: "text", Type: models.TextField, + Name: "input", Title: "form", Type: models.InputField, }, nil, }, { - "Valid dropdown field", + "Valid select field", models.FormField{ - Name: "dropdown", Type: models.DropdownField, Values: []string{"test1", "test2"}, + Name: "select", Title: "Select", Type: models.SelectField, Options: []string{"test1", "test2"}, }, nil, }, { "Valid checkbox field", models.FormField{ - Name: "checkbox", Type: models.CheckboxField, Values: []string{"test1", "test2"}, + Name: "checkbox", Title: "Checkbox", Type: models.CheckboxField, Options: []string{"test1", "test2"}, }, nil, }, + { - "Valid option field", + "Form field missing name", models.FormField{ - Name: "option", Type: models.OptionField, Values: []string{"test1", "test2"}, + Title: "form", + Type: models.InputField, }, - nil, + NewMissingRequiredFieldError("name"), }, { - "Form field missing name", + "Form field missing title", models.FormField{ - Type: models.TextField, + Name: "test", + Type: models.InputField, }, - NewMissingRequiredFieldError("name"), + NewMissingRequiredFieldError("title"), }, { "Form field missing type", models.FormField{ - Name: "test", + Name: "test", + Title: "Test", }, NewMissingRequiredFieldError("type"), }, { - "Dropdown field nil values", + "Select field nil options", models.FormField{ - Name: "dropdown", Type: models.DropdownField, + Name: "select", Title: "Select", Type: models.SelectField, }, - NewMissingRequiredFieldError("values"), + NewMissingRequiredFieldError("options"), }, { - "Checkbox field nil values", + "Checkbox field nil options", models.FormField{ - Name: "checkbox", Type: models.CheckboxField, + Name: "checkbox", Title: "Checkbox", Type: models.CheckboxField, }, - NewMissingRequiredFieldError("values"), + NewMissingRequiredFieldError("options"), }, { - "Option field nil values", + "Select field 0 options", models.FormField{ - Name: "option", Type: models.OptionField, + Name: "select", Title: "Checkbox", Type: models.SelectField, Options: []string{}, }, - NewMissingRequiredFieldError("values"), + NewMissingRequiredFieldError("options"), }, { - "Dropdown field 0 values", + "Checkbox field 0 options", models.FormField{ - Name: "dropdown", Type: models.DropdownField, Values: []string{}, + Name: "checkbox", Title: "Checkbox", Type: models.CheckboxField, Options: []string{}, }, - NewMissingRequiredFieldError("values"), + NewMissingRequiredFieldError("options"), }, { - "Checkbox field 0 values", + "Select field options contains empty string", models.FormField{ - Name: "checkbox", Type: models.CheckboxField, Values: []string{}, + Name: "select", Title: "Checkbox", Type: models.SelectField, Options: []string{"Option 1", "", "Option 3"}, }, - NewMissingRequiredFieldError("values"), + NewInvalidPropertyValue("options"), }, { - "Option field 0 values", + "Checkbox field options contains empty string", models.FormField{ - Name: "Option", Type: models.OptionField, Values: []string{}, + Name: "checkbox", Title: "Checkbox", Type: models.CheckboxField, Options: []string{"Option 1", "", "Option 3"}, }, - NewMissingRequiredFieldError("values"), + NewInvalidPropertyValue("options"), }, } for _, tc := range testcases { @@ -340,10 +344,11 @@ func TestValidateFormField(t *testing.T) { }) } } -func TestTextFieldDataValidator(t *testing.T) { +func TestInputFieldDataValidator(t *testing.T) { formField := models.FormField{ - Name: "test", - Type: models.TextField, + Name: "test", + Title: "Test", + Type: models.InputField, } testCases := []struct { testDescription string @@ -352,43 +357,43 @@ func TestTextFieldDataValidator(t *testing.T) { expected error }{ { - "Valid text field", + "Valid input field", formField, "test", nil, }, { - "Empty text field", + "Empty input field", formField, "", nil, }, { - "Data of type int for text field", + "Data of type int for input field", formField, 1, NewInvalidFormDataTypeError("test", "string"), }, { - "Data of type float for text field", + "Data of type float for input field", formField, 1.0, NewInvalidFormDataTypeError("test", "string"), }, { - "Data of type bool for text field", + "Data of type bool for input field", formField, true, NewInvalidFormDataTypeError("test", "string"), }, { - "Data of type []string for text field", + "Data of type []string for input field", formField, []string{"test"}, NewInvalidFormDataTypeError("test", "string"), }, { - "Data of type map[string]string for text field", + "Data of type map[string]string for input field", formField, map[string]string{"test": "test"}, NewInvalidFormDataTypeError("test", "string"), @@ -396,7 +401,7 @@ func TestTextFieldDataValidator(t *testing.T) { } for _, tc := range testCases { t.Run(tc.testDescription, func(t *testing.T) { - validator := FormFieldDataValidator(defaultTextFieldDataValidator) + validator := FormFieldDataValidator(defaultInputFieldDataValidator) err := validator.validate(tc.field, tc.value) if err == nil { if tc.expected != nil { @@ -415,11 +420,12 @@ func TestTextFieldDataValidator(t *testing.T) { } } -func TestDropdownFieldDataValidator(t *testing.T) { +func TestSelectFieldDataValidator(t *testing.T) { formField := models.FormField{ - Name: "test", - Type: models.DropdownField, - Values: []string{"test1", "test2", "test3"}, + Name: "test", + Title: "Test", + Type: models.SelectField, + Options: []string{"test1", "test2", "test3"}, } testcases := []struct { testDescription string @@ -428,55 +434,55 @@ func TestDropdownFieldDataValidator(t *testing.T) { expected error }{ { - "Valid dropdown value (1)", + "Valid select value (1)", formField, "test1", nil, }, { - "Valid dropdown value (2)", + "Valid select value (2)", formField, "test2", nil, }, { - "Valid dropdown value (3)", + "Valid select value (3)", formField, "test3", nil, }, { - "Invalid dropdown value", + "Invalid select value", formField, "test4", - NewInvalidSelectedFormDataError(formField.Values, "test4"), + NewInvalidSelectedFormDataError(formField.Options, "test4"), }, { - "Data of type int for dropdown field", + "Data of type int for select field", formField, 1, NewInvalidFormDataTypeError("test", "string"), }, { - "Data of type float for dropdown field", + "Data of type float for select field", formField, 1.0, NewInvalidFormDataTypeError("test", "string"), }, { - "Data of type bool for dropdown field", + "Data of type bool for select field", formField, true, NewInvalidFormDataTypeError("test", "string"), }, { - "Data of type []string for dropdown field", + "Data of type []string for select field", formField, []string{"test"}, NewInvalidFormDataTypeError("test", "string"), }, { - "Data of type map[string]string for dropdown field", + "Data of type map[string]string for select field", formField, map[string]string{"test": "test"}, NewInvalidFormDataTypeError("test", "string"), @@ -485,96 +491,7 @@ func TestDropdownFieldDataValidator(t *testing.T) { for _, tc := range testcases { t.Run(tc.testDescription, func(t *testing.T) { - validator := FormFieldDataValidator(defaultDropdownFieldDataValidator) - err := validator.validate(tc.field, tc.value) - if err == nil { - if tc.expected != nil { - t.Errorf("Expected error %v, got nil", tc.expected) - } - return - } - if tc.expected == nil { - t.Errorf("Expected no error, got %v", err) - return - } - if err.Error() != tc.expected.Error() { - t.Errorf("Expected %v, got %v", tc.expected, err) - } - }) - } -} - -func TestOptionFieldDataValidator(t *testing.T) { - formField := models.FormField{ - Name: "test", - Type: models.OptionField, - Values: []string{"test1", "test2", "test3"}, - } - testcases := []struct { - testDescription string - field models.FormField - value any - expected error - }{ - { - "Valid option value (1)", - formField, - "test1", - nil, - }, - { - "Valid option value (2)", - formField, - "test2", - nil, - }, - { - "Valid option value (3)", - formField, - "test3", - nil, - }, - { - "Invalid option value", - formField, - "test4", - NewInvalidSelectedFormDataError(formField.Values, "test4"), - }, - { - "Data of type int for option field", - formField, - 1, - NewInvalidFormDataTypeError("test", "string"), - }, - { - "Data of type float for option field", - formField, - 1.0, - NewInvalidFormDataTypeError("test", "string"), - }, - { - "Data of type bool for option field", - formField, - true, - NewInvalidFormDataTypeError("test", "string"), - }, - { - "Data of type []string for option field", - formField, - []string{"test"}, - NewInvalidFormDataTypeError("test", "string"), - }, - { - "Data of type map[string]string for option field", - formField, - map[string]string{"test": "test"}, - NewInvalidFormDataTypeError("test", "string"), - }, - } - - for _, tc := range testcases { - t.Run(tc.testDescription, func(t *testing.T) { - validator := FormFieldDataValidator(defaultOptionFieldDataValidator) + validator := FormFieldDataValidator(defaultSelectFieldDataValidator) err := validator.validate(tc.field, tc.value) if err == nil { if tc.expected != nil { @@ -595,9 +512,10 @@ func TestOptionFieldDataValidator(t *testing.T) { func TestCheckboxFieldDataValidator(t *testing.T) { formField := models.FormField{ - Name: "test", - Type: models.CheckboxField, - Values: []string{"test1", "test2", "test3"}, + Name: "test", + Title: "Test", + Type: models.CheckboxField, + Options: []string{"test1", "test2", "test3"}, } testcases := []struct { testDescription string @@ -612,7 +530,7 @@ func TestCheckboxFieldDataValidator(t *testing.T) { nil, }, { - "Valid multiple checkbox values", + "Valid multiple checkbox options", formField, []string{"test1", "test2"}, nil, @@ -621,13 +539,13 @@ func TestCheckboxFieldDataValidator(t *testing.T) { "Invalid checkbox value", formField, []string{"test4"}, - NewInvalidSelectedFormDataError(formField.Values, "test4"), + NewInvalidSelectedFormDataError(formField.Options, "test4"), }, { - "Invalid multiple checkbox values", + "Invalid multiple checkbox options", formField, []string{"test1", "test4"}, - NewInvalidSelectedFormDataError(formField.Values, "test4"), + NewInvalidSelectedFormDataError(formField.Options, "test4"), }, { "Data of type string for checkbox field", @@ -694,8 +612,8 @@ func TestValidateFormData_RequiredFieldsValidation(t *testing.T) { "Form with zero required fields. Form data has all fields", models.Form{ Fields: []models.FormField{ - {Name: "test", Type: models.TextField, IsRequired: false}, - {Name: "test2", Type: models.TextField, IsRequired: false}, + {Name: "test", Type: models.InputField, Required: false}, + {Name: "test2", Type: models.InputField, Required: false}, }, }, models.FormData{ @@ -709,8 +627,8 @@ func TestValidateFormData_RequiredFieldsValidation(t *testing.T) { "Form with zero required fields. Form data has no fields", models.Form{ Fields: []models.FormField{ - {Name: "test", Type: models.TextField, IsRequired: false}, - {Name: "test2", Type: models.TextField, IsRequired: false}, + {Name: "test", Type: models.InputField, Required: false}, + {Name: "test2", Type: models.InputField, Required: false}, }, }, models.FormData{}, @@ -721,8 +639,8 @@ func TestValidateFormData_RequiredFieldsValidation(t *testing.T) { "Form with zero required fields. Form data has some fields", models.Form{ Fields: []models.FormField{ - {Name: "test", Type: models.TextField, IsRequired: false}, - {Name: "test2", Type: models.TextField, IsRequired: false}, + {Name: "test", Type: models.InputField, Required: false}, + {Name: "test2", Type: models.InputField, Required: false}, }, }, models.FormData{ @@ -735,8 +653,8 @@ func TestValidateFormData_RequiredFieldsValidation(t *testing.T) { "Form with one required field. Form data has all fields", models.Form{ Fields: []models.FormField{ - {Name: "test", Type: models.TextField, IsRequired: true}, - {Name: "test2", Type: models.TextField, IsRequired: false}, + {Name: "test", Type: models.InputField, Required: true}, + {Name: "test2", Type: models.InputField, Required: false}, }, }, models.FormData{ @@ -750,8 +668,8 @@ func TestValidateFormData_RequiredFieldsValidation(t *testing.T) { "Form with one required field. Form data has no fields", models.Form{ Fields: []models.FormField{ - {Name: "test", Type: models.TextField, IsRequired: true}, - {Name: "test2", Type: models.TextField, IsRequired: false}, + {Name: "test", Type: models.InputField, Required: true}, + {Name: "test2", Type: models.InputField, Required: false}, }, }, models.FormData{}, @@ -762,8 +680,8 @@ func TestValidateFormData_RequiredFieldsValidation(t *testing.T) { "Form with one required field. Form data has required field", models.Form{ Fields: []models.FormField{ - {Name: "test", Type: models.TextField, IsRequired: true}, - {Name: "test2", Type: models.TextField, IsRequired: false}, + {Name: "test", Type: models.InputField, Required: true}, + {Name: "test2", Type: models.InputField, Required: false}, }, }, models.FormData{ @@ -776,8 +694,8 @@ func TestValidateFormData_RequiredFieldsValidation(t *testing.T) { "Form with one required field. Form data does not have field", models.Form{ Fields: []models.FormField{ - {Name: "test", Type: models.TextField, IsRequired: true}, - {Name: "test2", Type: models.TextField, IsRequired: false}, + {Name: "test", Type: models.InputField, Required: true}, + {Name: "test2", Type: models.InputField, Required: false}, }, }, models.FormData{ @@ -790,8 +708,8 @@ func TestValidateFormData_RequiredFieldsValidation(t *testing.T) { "Form with multiple required fields. Form data has all fields", models.Form{ Fields: []models.FormField{ - {Name: "test", Type: models.TextField, IsRequired: true}, - {Name: "test2", Type: models.TextField, IsRequired: true}, + {Name: "test", Type: models.InputField, Required: true}, + {Name: "test2", Type: models.InputField, Required: true}, }, }, models.FormData{ @@ -805,8 +723,8 @@ func TestValidateFormData_RequiredFieldsValidation(t *testing.T) { "Form with multiple required fields. Form data has no fields", models.Form{ Fields: []models.FormField{ - {Name: "test", Type: models.TextField, IsRequired: true}, - {Name: "test2", Type: models.TextField, IsRequired: true}, + {Name: "test", Type: models.InputField, Required: true}, + {Name: "test2", Type: models.InputField, Required: true}, }, }, models.FormData{}, @@ -817,8 +735,8 @@ func TestValidateFormData_RequiredFieldsValidation(t *testing.T) { "Form with multiple required fields. Form data has some fields", models.Form{ Fields: []models.FormField{ - {Name: "test", Type: models.TextField, IsRequired: true}, - {Name: "test2", Type: models.TextField, IsRequired: true}, + {Name: "test", Type: models.InputField, Required: true}, + {Name: "test2", Type: models.InputField, Required: true}, }, }, models.FormData{ @@ -831,9 +749,9 @@ func TestValidateFormData_RequiredFieldsValidation(t *testing.T) { "Form only has 3 fields. Form data has more", models.Form{ Fields: []models.FormField{ - {Name: "test", Type: models.TextField, IsRequired: true}, - {Name: "test2", Type: models.TextField, IsRequired: true}, - {Name: "test3", Type: models.TextField, IsRequired: true}, + {Name: "test", Type: models.InputField, Required: true}, + {Name: "test2", Type: models.InputField, Required: true}, + {Name: "test3", Type: models.InputField, Required: true}, }, }, models.FormData{ @@ -888,11 +806,12 @@ func TestValidateFormData_ValidatorsCalledOnce(t *testing.T) { formData models.FormData }{ { - "Test text field validator called", - models.TextField, + "Test input field validator called", + models.InputField, models.Form{ Fields: []models.FormField{ - {Name: "test", Type: models.TextField}, + {Name: "test", Title: "Test", + Type: models.InputField}, }, }, models.FormData{ @@ -904,19 +823,8 @@ func TestValidateFormData_ValidatorsCalledOnce(t *testing.T) { models.CheckboxField, models.Form{ Fields: []models.FormField{ - {Name: "test", Type: models.CheckboxField, Values: []string{"test1"}}, - }, - }, - models.FormData{ - "test": "test1", - }, - }, - { - "Test dropdown field validator called", - models.DropdownField, - models.Form{ - Fields: []models.FormField{ - {Name: "test", Type: models.DropdownField, Values: []string{"test1"}}, + {Name: "test", Title: "Test", + Type: models.CheckboxField, Options: []string{"test1"}}, }, }, models.FormData{ @@ -924,11 +832,12 @@ func TestValidateFormData_ValidatorsCalledOnce(t *testing.T) { }, }, { - "Test option field validator called", - models.OptionField, + "Test select field validator called", + models.SelectField, models.Form{ Fields: []models.FormField{ - {Name: "test", Type: models.OptionField, Values: []string{"test1"}}, + {Name: "test", Title: "Test", + Type: models.SelectField, Options: []string{"test1"}}, }, }, models.FormData{ diff --git a/flowforge_api_bruno/pipeline/all pipelines.bru b/flowforge_api_bruno/pipeline/all pipelines.bru new file mode 100644 index 00000000..524cb3f8 --- /dev/null +++ b/flowforge_api_bruno/pipeline/all pipelines.bru @@ -0,0 +1,11 @@ +meta { + name: all pipelines + type: http + seq: 1 +} + +get { + url: {{HOST}}/pipeline + body: none + auth: none +} diff --git a/frontend/src/app/(authenticated)/(main)/approved-service-requests/_hooks/use-approved-service-requests.ts b/frontend/src/app/(authenticated)/(main)/approved-service-requests/_hooks/use-approved-service-requests.ts index ae3e8f88..229f9caf 100644 --- a/frontend/src/app/(authenticated)/(main)/approved-service-requests/_hooks/use-approved-service-requests.ts +++ b/frontend/src/app/(authenticated)/(main)/approved-service-requests/_hooks/use-approved-service-requests.ts @@ -1,5 +1,37 @@ +import { FormFieldType, JsonFormComponents } from "@/types/json-form-components" import { ServiceRequest, ServiceRequestStatus } from "@/types/service-request" +const DUMMY_PIPELINE_FORM: JsonFormComponents = { + fields: [ + { + name: "input", + title: "Input", + description: "", + type: FormFieldType.INPUT, + required: true, + placeholder: "Enter text...", + min_length: 1, + }, + { + name: "select", + title: "Select", + description: "", + type: FormFieldType.SELECT, + required: true, + placeholder: "Select an option", + options: ["Option 1", "Option 2", "Option 3"], + default: "Option 1", + }, + { + name: "checkbox", + title: "Checkbox", + description: "", + type: FormFieldType.CHECKBOXES, + options: ["Option 1", "Option 2", "Option 3"], + }, + ], +} + const DUMMY_SERVICE_REQUESTS: ServiceRequest[] = [ { id: "1", @@ -11,7 +43,7 @@ const DUMMY_SERVICE_REQUESTS: ServiceRequest[] = [ created_by: "User 1", last_updated: "2024-02-21T19:50:01", remarks: "", - form: {}, + form: DUMMY_PIPELINE_FORM, form_data: {}, steps: [ { @@ -34,7 +66,7 @@ const DUMMY_SERVICE_REQUESTS: ServiceRequest[] = [ created_by: "User 2", last_updated: "2024-02-21T18:50:01", remarks: "", - form: {}, + form: DUMMY_PIPELINE_FORM, form_data: {}, steps: [ { @@ -57,7 +89,7 @@ const DUMMY_SERVICE_REQUESTS: ServiceRequest[] = [ created_by: "User 3", last_updated: "2024-02-21T17:00:00", remarks: "", - form: {}, + form: DUMMY_PIPELINE_FORM, form_data: {}, steps: [ { @@ -80,7 +112,7 @@ const DUMMY_SERVICE_REQUESTS: ServiceRequest[] = [ created_by: "User 4", last_updated: "2024-02-21T00:00:00", remarks: "", - form: {}, + form: DUMMY_PIPELINE_FORM, form_data: {}, steps: [ { diff --git a/frontend/src/app/(authenticated)/(main)/pending-service-requests/_hooks/use-pending-service-requests.ts b/frontend/src/app/(authenticated)/(main)/pending-service-requests/_hooks/use-pending-service-requests.ts index 235733d1..801c83ac 100644 --- a/frontend/src/app/(authenticated)/(main)/pending-service-requests/_hooks/use-pending-service-requests.ts +++ b/frontend/src/app/(authenticated)/(main)/pending-service-requests/_hooks/use-pending-service-requests.ts @@ -1,5 +1,37 @@ +import { FormFieldType, JsonFormComponents } from "@/types/json-form-components" import { ServiceRequest, ServiceRequestStatus } from "@/types/service-request" +const DUMMY_PIPELINE_FORM: JsonFormComponents = { + fields: [ + { + name: "input", + title: "Input", + description: "", + type: FormFieldType.INPUT, + required: true, + placeholder: "Enter text...", + min_length: 1, + }, + { + name: "select", + title: "Select", + description: "", + type: FormFieldType.SELECT, + required: true, + placeholder: "Select an option", + options: ["Option 1", "Option 2", "Option 3"], + default: "Option 1", + }, + { + name: "checkbox", + title: "Checkbox", + description: "", + type: FormFieldType.CHECKBOXES, + options: ["Option 1", "Option 2", "Option 3"], + }, + ], +} + const DUMMY_SERVICE_REQUESTS: ServiceRequest[] = [ { id: "1", @@ -11,7 +43,7 @@ const DUMMY_SERVICE_REQUESTS: ServiceRequest[] = [ created_by: "User 1", last_updated: "2024-02-21T19:50:01", remarks: "", - form: {}, + form: DUMMY_PIPELINE_FORM, form_data: {}, steps: [ { @@ -34,7 +66,7 @@ const DUMMY_SERVICE_REQUESTS: ServiceRequest[] = [ created_by: "User 2", last_updated: "2024-02-21T18:50:01", remarks: "", - form: {}, + form: DUMMY_PIPELINE_FORM, form_data: {}, steps: [ { @@ -57,7 +89,7 @@ const DUMMY_SERVICE_REQUESTS: ServiceRequest[] = [ created_by: "User 3", last_updated: "2024-02-21T17:00:00", remarks: "", - form: {}, + form: DUMMY_PIPELINE_FORM, form_data: {}, steps: [ { @@ -80,7 +112,7 @@ const DUMMY_SERVICE_REQUESTS: ServiceRequest[] = [ created_by: "User 4", last_updated: "2024-02-21T00:00:00", remarks: "", - form: {}, + form: DUMMY_PIPELINE_FORM, form_data: {}, steps: [ { @@ -103,7 +135,7 @@ const DUMMY_SERVICE_REQUESTS: ServiceRequest[] = [ created_by: "User 1", last_updated: "2024-02-20T00:00:00", remarks: "", - form: {}, + form: DUMMY_PIPELINE_FORM, form_data: {}, steps: [ { @@ -126,7 +158,7 @@ const DUMMY_SERVICE_REQUESTS: ServiceRequest[] = [ created_by: "User 2", last_updated: "2024-02-10T00:00:00", remarks: "", - form: {}, + form: DUMMY_PIPELINE_FORM, form_data: {}, steps: [ { diff --git a/frontend/src/app/(authenticated)/(main)/rejected-service-requests/_hooks/use-rejected-service-requests.ts b/frontend/src/app/(authenticated)/(main)/rejected-service-requests/_hooks/use-rejected-service-requests.ts index 367a39a2..2b9e29a3 100644 --- a/frontend/src/app/(authenticated)/(main)/rejected-service-requests/_hooks/use-rejected-service-requests.ts +++ b/frontend/src/app/(authenticated)/(main)/rejected-service-requests/_hooks/use-rejected-service-requests.ts @@ -1,5 +1,37 @@ +import { FormFieldType, JsonFormComponents } from "@/types/json-form-components" import { ServiceRequest, ServiceRequestStatus } from "@/types/service-request" +const DUMMY_PIPELINE_FORM: JsonFormComponents = { + fields: [ + { + name: "input", + title: "Input", + description: "", + type: FormFieldType.INPUT, + required: true, + placeholder: "Enter text...", + min_length: 1, + }, + { + name: "select", + title: "Select", + description: "", + type: FormFieldType.SELECT, + required: true, + placeholder: "Select an option", + options: ["Option 1", "Option 2", "Option 3"], + default: "Option 1", + }, + { + name: "checkbox", + title: "Checkbox", + description: "", + type: FormFieldType.CHECKBOXES, + options: ["Option 1", "Option 2", "Option 3"], + }, + ], +} + const DUMMY_SERVICE_REQUESTS: ServiceRequest[] = [ { id: "1", @@ -11,7 +43,7 @@ const DUMMY_SERVICE_REQUESTS: ServiceRequest[] = [ created_by: "User 1", last_updated: "2024-02-21T19:50:01", remarks: "", - form: {}, + form: DUMMY_PIPELINE_FORM, form_data: {}, steps: [ { @@ -34,7 +66,7 @@ const DUMMY_SERVICE_REQUESTS: ServiceRequest[] = [ created_by: "User 2", last_updated: "2024-02-21T18:50:01", remarks: "", - form: {}, + form: DUMMY_PIPELINE_FORM, form_data: {}, steps: [ { @@ -57,7 +89,7 @@ const DUMMY_SERVICE_REQUESTS: ServiceRequest[] = [ created_by: "User 3", last_updated: "2024-02-21T17:00:00", remarks: "", - form: {}, + form: DUMMY_PIPELINE_FORM, form_data: {}, steps: [ { @@ -80,7 +112,7 @@ const DUMMY_SERVICE_REQUESTS: ServiceRequest[] = [ created_by: "User 4", last_updated: "2024-02-21T00:00:00", remarks: "", - form: {}, + form: DUMMY_PIPELINE_FORM, form_data: {}, steps: [ { diff --git a/frontend/src/app/(authenticated)/(main)/service-catalog/[pipelineId]/_hooks/use-service-request-form.ts b/frontend/src/app/(authenticated)/(main)/service-catalog/[pipelineId]/_hooks/use-service-request-form.ts index e6a00f22..c8fd133c 100644 --- a/frontend/src/app/(authenticated)/(main)/service-catalog/[pipelineId]/_hooks/use-service-request-form.ts +++ b/frontend/src/app/(authenticated)/(main)/service-catalog/[pipelineId]/_hooks/use-service-request-form.ts @@ -2,11 +2,9 @@ import { toast } from "@/components/ui/use-toast" import useOrganizationId from "@/hooks/use-organization-id" import usePipeline from "@/hooks/use-pipeline" import { createServiceRequest, getPipeline } from "@/lib/service" -import { - convertServiceRequestFormToRJSFSchema, - generateUiSchema, -} from "@/lib/utils" -import { JsonFormComponents } from "@/types/json-form-components" +import { generateUiSchema } from "@/lib/rjsf-utils" +import { convertServiceRequestFormToRJSFSchema } from "@/lib/rjsf-utils" +import { FormFieldType, JsonFormComponents } from "@/types/json-form-components" import { Pipeline } from "@/types/pipeline" import { IChangeEvent } from "@rjsf/core" import { RJSFSchema } from "@rjsf/utils" @@ -16,30 +14,35 @@ interface UseServiceRequestFormOptions { pipelineId: string } -const DUMMY_SERVICE_REQUEST_FORM: JsonFormComponents = { - input: { - title: "Input", - type: "input", - description: "Input Description with minimum length 1", - minLength: 1, - required: true, - placeholder: "Input placeholder...", - }, - select: { - title: "Select Option", - type: "select", - placeholder: "Select placeholder", - description: "Dropdown selection with default value as Item 1", - options: ["Item 1", "Item 2", "Item 3"], - required: true, - }, - checkboxes: { - title: "Checkboxes", - type: "checkboxes", - description: "You can select more than 1 item", - options: ["Item 1", "Item 2", "Item 3"], - required: false, - }, +const DUMMY_PIPELINE_FORM: JsonFormComponents = { + fields: [ + { + name: "input", + title: "Input", + description: "", + type: FormFieldType.INPUT, + required: true, + placeholder: "Enter text...", + min_length: 1, + }, + { + name: "select", + title: "Select", + description: "", + type: FormFieldType.SELECT, + required: true, + placeholder: "Select an option", + options: ["Option 1", "Option 2", "Option 3"], + default: "Option 1", + }, + { + name: "checkbox", + title: "Checkbox", + description: "", + type: FormFieldType.CHECKBOXES, + options: ["Option 1", "Option 2", "Option 3"], + }, + ], } const useServiceRequestForm = ({ diff --git a/frontend/src/app/(authenticated)/(main)/service-catalog/[pipelineId]/_hooks/use-service-request.ts b/frontend/src/app/(authenticated)/(main)/service-catalog/[pipelineId]/_hooks/use-service-request.ts index e6a00f22..2a38c150 100644 --- a/frontend/src/app/(authenticated)/(main)/service-catalog/[pipelineId]/_hooks/use-service-request.ts +++ b/frontend/src/app/(authenticated)/(main)/service-catalog/[pipelineId]/_hooks/use-service-request.ts @@ -1,45 +1,47 @@ import { toast } from "@/components/ui/use-toast" import useOrganizationId from "@/hooks/use-organization-id" import usePipeline from "@/hooks/use-pipeline" -import { createServiceRequest, getPipeline } from "@/lib/service" -import { - convertServiceRequestFormToRJSFSchema, - generateUiSchema, -} from "@/lib/utils" -import { JsonFormComponents } from "@/types/json-form-components" -import { Pipeline } from "@/types/pipeline" +import { createServiceRequest } from "@/lib/service" +import { generateUiSchema } from "@/lib/rjsf-utils" +import { convertServiceRequestFormToRJSFSchema } from "@/lib/rjsf-utils" +import { FormFieldType, JsonFormComponents } from "@/types/json-form-components" import { IChangeEvent } from "@rjsf/core" import { RJSFSchema } from "@rjsf/utils" -import { useEffect, useMemo, useState } from "react" +import { useMemo, useState } from "react" interface UseServiceRequestFormOptions { pipelineId: string } const DUMMY_SERVICE_REQUEST_FORM: JsonFormComponents = { - input: { - title: "Input", - type: "input", - description: "Input Description with minimum length 1", - minLength: 1, - required: true, - placeholder: "Input placeholder...", - }, - select: { - title: "Select Option", - type: "select", - placeholder: "Select placeholder", - description: "Dropdown selection with default value as Item 1", - options: ["Item 1", "Item 2", "Item 3"], - required: true, - }, - checkboxes: { - title: "Checkboxes", - type: "checkboxes", - description: "You can select more than 1 item", - options: ["Item 1", "Item 2", "Item 3"], - required: false, - }, + fields: [ + { + name: "input", + title: "Input", + description: "", + type: FormFieldType.INPUT, + required: true, + placeholder: "Enter text...", + min_length: 1, + }, + { + name: "select", + title: "Select", + description: "", + type: FormFieldType.SELECT, + required: true, + placeholder: "Select an option", + options: ["Option 1", "Option 2", "Option 3"], + default: "Option 1", + }, + { + name: "checkbox", + title: "Checkbox", + description: "", + type: FormFieldType.CHECKBOXES, + options: ["Option 1", "Option 2", "Option 3"], + }, + ], } const useServiceRequestForm = ({ diff --git a/frontend/src/app/(authenticated)/(main)/service-catalog/create-service/_hooks/use-create-service.ts b/frontend/src/app/(authenticated)/(main)/service-catalog/create-service/_hooks/use-create-service.ts index bd3bd558..28a50608 100644 --- a/frontend/src/app/(authenticated)/(main)/service-catalog/create-service/_hooks/use-create-service.ts +++ b/frontend/src/app/(authenticated)/(main)/service-catalog/create-service/_hooks/use-create-service.ts @@ -6,26 +6,46 @@ import { KeyboardEvent, useState } from "react" import { validateFormSchema } from "../_utils/validation" import { createPipeline } from "@/lib/service" import { Pipeline } from "@/types/pipeline" -import { JsonFormComponents } from "@/types/json-form-components" +import { + FormCheckboxes, + FormComponent, + FormFieldType, + FormInput, + FormSelect, + JsonFormComponents, +} from "@/types/json-form-components" import { toast } from "@/components/ui/use-toast" import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime" -const DEFAULT_FORM = { - input: { title: "", description: "", type: "input", required: true }, - select: { - title: "", - description: "", - type: "select", - required: true, - options: ["Option 1", "Option 2", "Option 3"], - }, - checkBoxes: { - title: "", - description: "", - type: "checkboxes", - required: false, - options: ["Option 1", "Option 2", "Option 3"], - }, +const DEFAULT_FORM: JsonFormComponents = { + fields: [ + { + name: "", + title: "", + description: "", + type: FormFieldType.INPUT, + required: true, + min_length: 1, + placeholder: "Enter text...", + } as FormInput, + { + name: "", + title: "", + description: "", + type: FormFieldType.SELECT, + required: true, + options: ["Option 1", "Option 2", "Option 3"], + default: "Option 1", + placeholder: "Select an option", + } as FormSelect, + { + name: "", + title: "", + description: "", + type: FormFieldType.CHECKBOXES, + options: ["Option 1", "Option 2", "Option 3"], + } as FormCheckboxes, + ], } const DEFAULT_PIPELINE = { @@ -116,6 +136,7 @@ const useCreateService = ({ router }: UseCreateServiceProps) => { const { description, form, name, pipeline } = values const formJson: JsonFormComponents = JSON.parse(form) + const pipelineJson: Pipeline = { pipeline_name: name, pipeline_description: description, diff --git a/frontend/src/app/(authenticated)/(main)/service-catalog/create-service/_utils/validation.ts b/frontend/src/app/(authenticated)/(main)/service-catalog/create-service/_utils/validation.ts index 115ac505..7b06b70c 100644 --- a/frontend/src/app/(authenticated)/(main)/service-catalog/create-service/_utils/validation.ts +++ b/frontend/src/app/(authenticated)/(main)/service-catalog/create-service/_utils/validation.ts @@ -1,27 +1,52 @@ -import { isJson } from "@/lib/utils" +import { isArrayValuesString, isArrayValuesUnique, isJson } from "@/lib/utils" import { FormCheckboxes, FormComponent, - FormComponentWithOptions, + FormFieldType, FormInput, FormSelect, + JsonFormComponents, + Options, } from "@/types/json-form-components" +const isNameUnique = (field: FormComponent[]) => { + return new Set(field.map((f) => f.name)).size === field.length +} + +const checkForName = (formItems: FormComponent[], errorMessages: string[]) => { + for (let i = 0; i < formItems.length; i++) { + const formItem = formItems[i] + const { name } = formItem + if (!name) { + errorMessages.push(`Please define a name for form item ${i + 1}`) + } + } + + if (!isNameUnique(formItems)) { + errorMessages.push("Please ensure that name is unique for the form items.") + } +} + const isFormAttribute = ( formItemAttribute: string ): formItemAttribute is keyof FormComponent => { return ( + formItemAttribute === "name" || formItemAttribute === "title" || formItemAttribute === "description" || - formItemAttribute === "type" || - formItemAttribute === "required" + formItemAttribute === "type" ) } const isInputFormAttribute = ( formItemAttribute: string ): formItemAttribute is keyof FormInput => { - return isFormAttribute(formItemAttribute) || formItemAttribute === "minLength" + return ( + isFormAttribute(formItemAttribute) || + formItemAttribute === "min_length" || + formItemAttribute === "required" || + formItemAttribute === "placeholder" + ) } const isSelectFormAttribute = ( @@ -29,8 +54,8 @@ const isSelectFormAttribute = ( ): formItemAttribute is keyof FormSelect => { return ( isFormAttribute(formItemAttribute) || + formItemAttribute === "required" || formItemAttribute === "options" || - formItemAttribute === "disabled" || formItemAttribute === "default" || formItemAttribute === "placeholder" ) @@ -39,11 +64,7 @@ const isSelectFormAttribute = ( const isCheckboxesFormAttribute = ( formItemAttribute: string ): formItemAttribute is keyof FormCheckboxes => { - return ( - isFormAttribute(formItemAttribute) || - formItemAttribute === "options" || - formItemAttribute === "disabled" - ) + return isFormAttribute(formItemAttribute) || formItemAttribute === "options" } function checkForUnexpectedFormAttributes( @@ -54,21 +75,21 @@ function checkForUnexpectedFormAttributes( const formItemObject = formItem as FormComponent for (const formItemAttribute in formItemObject) { if ( - formItemObject.type === "input" && + formItemObject.type === FormFieldType.INPUT && !isInputFormAttribute(formItemAttribute) ) { errorMessages.push( `Not allowed to add '${formItemAttribute}' attribute to input form item '${formItemName}'` ) } else if ( - formItemObject.type === "select" && + formItemObject.type === FormFieldType.SELECT && !isSelectFormAttribute(formItemAttribute) ) { errorMessages.push( `Not allowed to add '${formItemAttribute}' attribute to select form item '${formItemName}'` ) } else if ( - formItemObject.type === "checkboxes" && + formItemObject.type === FormFieldType.CHECKBOXES && !isCheckboxesFormAttribute(formItemAttribute) ) { errorMessages.push( @@ -84,17 +105,19 @@ function checkForMissingFormAttributes( errorMessages: string[] ) { const { title, description, type } = formItem as FormComponent - if (title === undefined) { + + if (!title) { errorMessages.push(`title is missing from form item '${formItemName}'`) } + // Check for undefined equality directly as empty string is allowed if (description === undefined) { errorMessages.push( `description is missing from form item '${formItemName}'` ) } - if (type === undefined) { + if (!type) { errorMessages.push(`type is missing from form item '${formItemName}'`) } } @@ -104,43 +127,55 @@ function checkForFormAttributeTypes( formItem: object, errorMessages: string[] ) { - const { title, description, type, required } = formItem as FormComponent + const { title, description, type } = formItem as FormComponent if (typeof title !== "string" || typeof description !== "string") { errorMessages.push( `title and description of form item '${formItemName}' can only be string.` ) } - if (type !== "input" && type !== "select" && type !== "checkboxes") { - errorMessages.push( - `type of form item '${formItemName}' can only be 'input', 'select' or 'checkboxes'.` - ) - } + switch (type) { + case FormFieldType.INPUT: { + const inputItem = formItem as FormInput + if ( + inputItem.min_length !== undefined && + typeof inputItem.min_length !== "number" + ) { + errorMessages.push( + `min_length of form item '${formItemName}' can only be number.` + ) + } - if (required !== undefined && typeof required !== "boolean") { - errorMessages.push( - `required of form item '${formItemName}' can only be boolean.` - ) - } + if ( + inputItem.required !== undefined && + typeof inputItem.required !== "boolean" + ) { + errorMessages.push( + `required of form item '${formItemName}' can only be boolean.` + ) + } + break + } + case FormFieldType.SELECT: { + const selectItem = formItem as FormSelect + if ( + selectItem.required !== undefined && + typeof selectItem.required !== "boolean" + ) { + errorMessages.push( + `required of form item '${formItemName}' can only be boolean.` + ) + } + break + } + case FormFieldType.CHECKBOXES: { + const checkboxesItem = formItem as FormCheckboxes - if (type === "input") { - const inputItem = formItem as FormInput - if ( - inputItem.minLength !== undefined && - typeof inputItem.minLength !== "number" - ) { - errorMessages.push( - `minLength of form item '${formItemName}' can only be number.` - ) + break } - } else if (type === "select" || type === "checkboxes") { - const selectItem = formItem as FormComponentWithOptions - if ( - selectItem.disabled !== undefined && - typeof selectItem.disabled !== "boolean" - ) { + default: { errorMessages.push( - `disabled of form item '${formItemName}' can only be boolean.` + `type of form item '${formItemName}' can only be 'input', 'select' or 'checkboxes'.` ) } } @@ -153,10 +188,10 @@ function checkForEmptyAttributes( ) { const { title, type } = formItem as FormComponent if (!title) { - errorMessages.push(`Form item name for '${formItemName}' cannot be empty`) + errorMessages.push(`Form item title for '${formItemName}' cannot be empty`) } - if (type === "select" || type === "checkboxes") { - const formWithOptions = formItem as FormComponentWithOptions + if (type === FormFieldType.SELECT || type === FormFieldType.CHECKBOXES) { + const formWithOptions = formItem as Options const { options } = formWithOptions if (!options) { errorMessages.push( @@ -170,30 +205,69 @@ function checkForEmptyAttributes( } } +function checkForFormAttributesValues( + formItemName: string, + formItem: object, + errorMessages: string[] +) { + const { type } = formItem as FormComponent + + switch (type) { + case FormFieldType.INPUT: { + break + } + case FormFieldType.SELECT: + case FormFieldType.CHECKBOXES: { + const { options } = formItem as Options + if (!isArrayValuesUnique(options)) { + errorMessages.push(`Option values for '${formItemName}' must be unique`) + } + + if (!isArrayValuesString(options)) { + errorMessages.push( + `Option values for '${formItemName}' can only be string` + ) + } + break + } + default: + break + } +} + export function validateFormSchema(jsonString: string) { if (!isJson(jsonString)) { return [] } - const formJson = JSON.parse(jsonString) + const formJson: JsonFormComponents = JSON.parse( + jsonString + ) as JsonFormComponents const errorList: string[] = [] - for (const formItemName in formJson) { - const formItem = formJson[formItemName] + if (!formJson.fields) { + errorList.push("Please define 'fields' in the form") + return errorList + } + + checkForName(formJson.fields, errorList) + for (const formItem of formJson.fields) { + const formItemName = formItem.name if ( - formItem.type !== "input" && - formItem.type !== "select" && - formItem.type !== "checkboxes" + formItem.type !== FormFieldType.INPUT && + formItem.type !== FormFieldType.SELECT && + formItem.type !== FormFieldType.CHECKBOXES ) { errorList.push( `Please define a type for form item '${formItemName}' (Only 'input', 'select' and 'checkboxes' type are supported)` ) } - checkForUnexpectedFormAttributes(formItemName, formItem, errorList) - checkForMissingFormAttributes(formItemName, formItem, errorList) - checkForFormAttributeTypes(formItemName, formItem, errorList) - checkForEmptyAttributes(formItemName, formItem, errorList) + checkForUnexpectedFormAttributes(formItem.name, formItem, errorList) + checkForMissingFormAttributes(formItem.name, formItem, errorList) + checkForFormAttributeTypes(formItem.name, formItem, errorList) + checkForEmptyAttributes(formItem.name, formItem, errorList) + checkForFormAttributesValues(formItem.name, formItem, errorList) } return errorList diff --git a/frontend/src/app/(authenticated)/(main)/service-request-dashboard/_hooks/use-service-requests.ts b/frontend/src/app/(authenticated)/(main)/service-request-dashboard/_hooks/use-service-requests.ts index 742d9043..aecd2c0f 100644 --- a/frontend/src/app/(authenticated)/(main)/service-request-dashboard/_hooks/use-service-requests.ts +++ b/frontend/src/app/(authenticated)/(main)/service-request-dashboard/_hooks/use-service-requests.ts @@ -1,23 +1,36 @@ -import { toast } from "@/components/ui/use-toast" -import useOrganizationId from "@/hooks/use-organization-id" -import { getAllServiceRequest } from "@/lib/service" -import { - ServiceRequest, - ServiceRequestStatus, - ServiceRequestStep, -} from "@/types/service-request" -import { useQuery } from "@tanstack/react-query" +import { FormFieldType, JsonFormComponents } from "@/types/json-form-components" +import { ServiceRequest, ServiceRequestStatus } from "@/types/service-request" -const PIPELINE_1_DUMMY_STEPS: ServiceRequestStep[] = [ - { - name: "Approval", - status: ServiceRequestStatus.NOT_STARTED, - }, - { - name: "Create EC2", - status: ServiceRequestStatus.NOT_STARTED, - }, -] +const DUMMY_PIPELINE_FORM: JsonFormComponents = { + fields: [ + { + name: "input", + title: "Input", + description: "", + type: FormFieldType.INPUT, + required: true, + placeholder: "Enter text...", + min_length: 1, + }, + { + name: "select", + title: "Select", + description: "", + type: FormFieldType.SELECT, + required: true, + placeholder: "Select an option", + options: ["Option 1", "Option 2", "Option 3"], + default: "Option 1", + }, + { + name: "checkbox", + title: "Checkbox", + description: "", + type: FormFieldType.CHECKBOXES, + options: ["Option 1", "Option 2", "Option 3"], + }, + ], +} const DUMMY_SERVICE_REQUESTS: ServiceRequest[] = [ { @@ -30,7 +43,7 @@ const DUMMY_SERVICE_REQUESTS: ServiceRequest[] = [ created_on: "2024-02-21T19:50:01", last_updated: "2024-02-21T19:50:01", remarks: "Remarks", - form: {}, + form: DUMMY_PIPELINE_FORM, form_data: {}, steps: [ { @@ -69,7 +82,7 @@ const DUMMY_SERVICE_REQUESTS: ServiceRequest[] = [ created_on: "2024-02-21T18:50:01", last_updated: "2024-02-21T18:50:01", remarks: "Remarks", - form: {}, + form: DUMMY_PIPELINE_FORM, form_data: {}, steps: [ { @@ -93,7 +106,7 @@ const DUMMY_SERVICE_REQUESTS: ServiceRequest[] = [ created_on: "2024-02-21T17:00:00", last_updated: "2024-02-21T17:00:00", remarks: "Remarks", - form: {}, + form: DUMMY_PIPELINE_FORM, form_data: {}, steps: [ { @@ -116,7 +129,7 @@ const DUMMY_SERVICE_REQUESTS: ServiceRequest[] = [ created_on: "2024-02-21T00:00:00", last_updated: "2024-02-21T00:00:00", remarks: "", - form: {}, + form: DUMMY_PIPELINE_FORM, form_data: {}, steps: [ { @@ -139,7 +152,7 @@ const DUMMY_SERVICE_REQUESTS: ServiceRequest[] = [ created_on: "2024-02-21T00:00:00", last_updated: "2024-02-21T00:00:00", remarks: "", - form: {}, + form: DUMMY_PIPELINE_FORM, form_data: {}, steps: [ { @@ -162,7 +175,7 @@ const DUMMY_SERVICE_REQUESTS: ServiceRequest[] = [ created_on: "2024-02-20T00:00:00", last_updated: "2024-02-20T00:00:00", remarks: "", - form: {}, + form: DUMMY_PIPELINE_FORM, form_data: {}, steps: [ { @@ -184,7 +197,7 @@ const DUMMY_SERVICE_REQUESTS: ServiceRequest[] = [ created_on: "2024-02-10T00:00:00", last_updated: "2024-02-10T00:00:00", remarks: "", - form: {}, + form: DUMMY_PIPELINE_FORM, form_data: {}, steps: [ { diff --git a/frontend/src/app/(authenticated)/(main)/service-request-info/[serviceRequestId]/_hooks/use-service-request-info.tsx b/frontend/src/app/(authenticated)/(main)/service-request-info/[serviceRequestId]/_hooks/use-service-request-info.tsx index 8cab350e..ded0ac1c 100644 --- a/frontend/src/app/(authenticated)/(main)/service-request-info/[serviceRequestId]/_hooks/use-service-request-info.tsx +++ b/frontend/src/app/(authenticated)/(main)/service-request-info/[serviceRequestId]/_hooks/use-service-request-info.tsx @@ -1,36 +1,39 @@ import useServiceRequest from "@/hooks/use-service-request" -import { - convertServiceRequestFormToRJSFSchema, - generateUiSchema, -} from "@/lib/utils" -import { JsonFormComponents } from "@/types/json-form-components" +import { generateUiSchema } from "@/lib/rjsf-utils" +import { convertServiceRequestFormToRJSFSchema } from "@/lib/rjsf-utils" +import { FormFieldType, JsonFormComponents } from "@/types/json-form-components" import { ServiceRequest, ServiceRequestStatus } from "@/types/service-request" import { useMemo } from "react" const DUMMY_PIPELINE_FORM: JsonFormComponents = { - input: { - title: "Input", - type: "input", - description: "Input Description with minimum length 1", - minLength: 1, - required: true, - placeholder: "Input placeholder...", - }, - select: { - title: "Select Option", - type: "select", - placeholder: "Select placeholder", - description: "Dropdown selection with default value as Item 1", - options: ["Item 1", "Item 2", "Item 3"], - required: true, - }, - checkboxes: { - title: "Checkboxes", - type: "checkboxes", - description: "You can select more than 1 item", - options: ["Item 1", "Item 2", "Item 3"], - required: false, - }, + fields: [ + { + name: "input", + title: "Input", + description: "", + type: FormFieldType.INPUT, + required: true, + placeholder: "Enter text...", + min_length: 1, + }, + { + name: "select", + title: "Select", + description: "", + type: FormFieldType.SELECT, + required: true, + placeholder: "Select an option", + options: ["Option 1", "Option 2", "Option 3"], + default: "Option 1", + }, + { + name: "checkbox", + title: "Checkbox", + description: "", + type: FormFieldType.CHECKBOXES, + options: ["Option 1", "Option 2", "Option 3"], + }, + ], } const DUMMY_SR_FORM_DATA = { diff --git a/frontend/src/app/(authenticated)/layout.tsx b/frontend/src/app/(authenticated)/layout.tsx index 39cf8274..f34d1ef9 100644 --- a/frontend/src/app/(authenticated)/layout.tsx +++ b/frontend/src/app/(authenticated)/layout.tsx @@ -21,6 +21,7 @@ export default function AuthenticatedLayout({ apiClient.defaults.headers.Authorization = `Bearer ${getCookie("access_token") as string}` setRender(true) } + apiClient.defaults.headers.Authorization = `Bearer ${getCookie("access_token") as string}` }, [router]) return render && children } diff --git a/frontend/src/lib/rjsf-utils.ts b/frontend/src/lib/rjsf-utils.ts new file mode 100644 index 00000000..f952355d --- /dev/null +++ b/frontend/src/lib/rjsf-utils.ts @@ -0,0 +1,89 @@ +import { FormFieldType, JsonFormComponents } from "@/types/json-form-components" +import { RJSFSchema, UiSchema } from "@rjsf/utils" + +export const convertServiceRequestFormToRJSFSchema = ( + jsonFormComponents?: JsonFormComponents +) => { + const schema: RJSFSchema = { + type: "object", + } + + const properties: { [key: string]: object } = {} + const required: string[] = [] + + for (const component of jsonFormComponents?.fields ?? []) { + const hasRequiredField = + component.type === FormFieldType.INPUT || + component.type === FormFieldType.SELECT + // To create required array + if (hasRequiredField && component.required) { + required.push(component.name) + } + + switch (component.type) { + case FormFieldType.INPUT: + properties[component.name] = { + type: "string", + title: component.title, + description: component.description, + minLength: component.min_length, + } + break + case FormFieldType.SELECT: + properties[component.name] = { + type: "string", + title: component.title, + description: component.description, + enum: component.options, + } + break + case FormFieldType.CHECKBOXES: + properties[component.name] = { + type: "array", + title: component.title, + description: component.description, + items: { + enum: component.options, + }, + uniqueItems: true, + } + break + default: + break + } + } + + schema.required = required + schema.properties = properties + + return schema +} + +export const generateUiSchema = (jsonFormComponents?: JsonFormComponents) => { + const uiSchema: UiSchema = {} + + for (const itemOptions of jsonFormComponents?.fields ?? []) { + switch (itemOptions.type) { + case FormFieldType.INPUT: + uiSchema[itemOptions.name] = { + "ui:placeholder": itemOptions.placeholder, + } + break + case FormFieldType.SELECT: + uiSchema[itemOptions.name] = { + "ui:placeholder": itemOptions.placeholder ?? "Select an item...", + } + break + case FormFieldType.CHECKBOXES: + uiSchema[itemOptions.name] = { + "ui:widget": "checkboxes", + } + break + + default: + break + } + } + + return uiSchema +} diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index f713256b..e649f4e3 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -1,5 +1,3 @@ -import { JsonFormComponents } from "@/types/json-form-components" -import { RJSFSchema, UiSchema } from "@rjsf/utils" import { type ClassValue, clsx } from "clsx" import { twMerge } from "tailwind-merge" @@ -7,95 +5,6 @@ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } -export const convertServiceRequestFormToRJSFSchema = ( - jsonFormComponents?: JsonFormComponents -) => { - const schema: RJSFSchema = { - type: "object", - } - - const properties: { [key: string]: object } = {} - const required: string[] = [] - - for (const item in jsonFormComponents) { - const component = jsonFormComponents[item] - // To create required array - if (component.required) { - required.push(item) - } - - switch (component.type) { - case "input": - properties[item] = { - type: "string", - title: component.title, - description: component.description, - minLength: component.minLength, - } - break - case "select": - properties[item] = { - type: "string", - title: component.title, - description: component.description, - enum: component.options, - } - break - case "checkboxes": - properties[item] = { - type: "array", - title: component.title, - description: component.description, - items: { - enum: component.options, - }, - uniqueItems: true, - } - break - default: - break - } - } - - schema.required = required - schema.properties = properties - - return schema -} - -export const generateUiSchema = (jsonFormComponents?: JsonFormComponents) => { - const uiSchema: UiSchema = {} - - for (const item in jsonFormComponents) { - const itemOptions = jsonFormComponents[item] - if (!itemOptions) { - throw new Error("Form item not found for item: " + item) - } - switch (itemOptions.type) { - case "input": - uiSchema[item] = { - "ui:placeholder": itemOptions.placeholder, - } - break - case "select": - uiSchema[item] = { - "ui:placeholder": itemOptions.placeholder ?? "Select an item...", - } - break - case "checkboxes": - uiSchema[item] = { - "ui:widget": "checkboxes", - } - break - - default: - break - } - } - - return uiSchema -} - export function isJson(item: string) { let value = typeof item !== "string" ? JSON.stringify(item) : item try { @@ -143,3 +52,11 @@ export function formatTimeDifference(date: Date) { return `${years} year${years > 1 ? "s" : ""} ago` } } + +export function isArrayValuesUnique(arr: T[]): boolean { + return new Set(arr).size === arr.length +} + +export function isArrayValuesString(arr: unknown[]): arr is string[] { + return arr.every((item): item is string => typeof item === "string") +} diff --git a/frontend/src/types/json-form-components.ts b/frontend/src/types/json-form-components.ts index ef6394ef..3e251b35 100644 --- a/frontend/src/types/json-form-components.ts +++ b/frontend/src/types/json-form-components.ts @@ -1,40 +1,48 @@ +export enum FormFieldType { + INPUT = "input", + SELECT = "select", + CHECKBOXES = "checkboxes", +} + type FormComponent = { + name: string title: string description: string - type: "input" | "select" | "checkboxes" - required?: boolean + type: FormFieldType } type FormInput = FormComponent & { - minLength?: number - type: "input" + type: FormFieldType.INPUT + required?: boolean + min_length?: number placeholder?: string } -type FormComponentWithOptions = FormComponent & { +type Options = { options: string[] - disabled?: string[] } -type FormSelect = FormComponentWithOptions & { - type: "select" - default?: string - placeholder?: string -} +type FormSelect = FormComponent & + Options & { + type: FormFieldType.SELECT + required?: boolean + default?: string + placeholder?: string + } -type FormCheckboxes = FormComponentWithOptions & { - type: "checkboxes" - required?: false -} +type FormCheckboxes = FormComponent & + Options & { + type: FormFieldType.CHECKBOXES + } type JsonFormComponents = { - [key: string]: FormInput | FormSelect | FormCheckboxes + fields: (FormInput | FormSelect | FormCheckboxes)[] } export type { FormComponent, - FormComponentWithOptions, FormInput, + Options, FormSelect, FormCheckboxes, JsonFormComponents,