frm/handlers/handlers.go

662 lines
18 KiB
Go
Raw Normal View History

2024-11-05 16:18:04 +00:00
package handlers
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"regexp"
"strings"
2024-11-05 16:18:04 +00:00
"github.com/acaloiaro/frm"
"github.com/acaloiaro/frm/internal"
"github.com/acaloiaro/frm/static"
"github.com/acaloiaro/frm/types"
"github.com/acaloiaro/frm/ui"
"github.com/google/uuid"
2024-11-05 16:18:04 +00:00
)
type contextKey string
var FormIDContextKey contextKey = "frm_form_id"
var FieldIDContextKey contextKey = "frm_field_id"
var ErrNoInstanceAvailable = errors.New("no frm instance is available on the context")
var ErrFormIDNotFound = errors.New("a form ID was not found in the request context")
var ErrFieldIDNotFound = errors.New("a field ID was not found in the request context")
// StaticAssetHandler handles requests for assets embedded in frm's static file system
func StaticAssetHandler(w http.ResponseWriter, r *http.Request) {
mp := r.Context().Value(internal.MountPointContextKey).(string)
// Remove the mount point from the path so the static filesystem paths are resolved relative to its root
r.URL.Path = strings.ReplaceAll(r.URL.Path, mp, "")
http.FileServer(http.FS(static.Assets)).ServeHTTP(w, r)
}
// FormEditor displays the form editor and previewer
func FormEditor(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
f, err := instance(ctx)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
id, err := formID(ctx)
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
2024-12-19 21:18:15 +00:00
draft, err := internal.Q(ctx, f.PostgresURL).GetDraft(r.Context(), internal.GetDraftParams{
WorkspaceID: f.WorkspaceID,
ID: *id,
})
if err != nil {
slog.Error("unable to get forms", slog.Any("error", err))
w.WriteHeader(http.StatusInternalServerError)
return
}
2024-12-19 21:18:15 +00:00
err = ui.Builder((frm.Form)(draft)).Render(r.Context(), w)
if err != nil {
slog.Error("unable to render builder", slog.Any("error", err))
w.WriteHeader(http.StatusInternalServerError)
}
}
// UpdateFieldOrder handles requests updating form field order
func UpdateFieldOrder(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
f, err := instance(ctx)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
formID, err := formID(ctx)
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
2024-12-19 21:18:15 +00:00
draft, err := internal.Q(ctx, f.PostgresURL).GetDraft(ctx, internal.GetDraftParams{
WorkspaceID: f.WorkspaceID,
ID: *formID,
})
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
err = r.ParseForm()
if err != nil {
slog.Error("unable to parse form", slog.Any("error", err))
w.WriteHeader(http.StatusInternalServerError)
return
}
updatedFields := types.FormFields{}
for key, field := range r.Form {
switch key {
case "order":
for order, fieldID := range field {
2024-12-19 21:18:15 +00:00
oldField := draft.Fields[fieldID]
oldField.Order = order
updatedFields[fieldID] = oldField
}
default:
}
}
2024-12-19 21:18:15 +00:00
draft.Fields = updatedFields
draft, err = internal.Q(ctx, f.PostgresURL).SaveDraft(ctx, internal.SaveDraftParams{
ID: draft.ID,
Name: draft.Name,
Fields: updatedFields,
})
if err != nil {
2024-12-19 21:18:15 +00:00
slog.Error("unable to save draft", slog.Any("error", err))
w.WriteHeader(http.StatusInternalServerError)
}
// Re-render the form fields form UI
2024-12-19 21:18:15 +00:00
err = ui.FormFieldsForm((frm.Form)(draft)).Render(ctx, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
2024-12-19 21:18:15 +00:00
err = ui.FormView((frm.Form)(draft), true).Render(ctx, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
w.WriteHeader(http.StatusOK)
}
// LogicConfigurationStep3 handles changes to form field configuration logic, rendering the correct input type for the
// chosen form field
func LogicConfiguratorStep3(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
f, err := instance(ctx)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
formID, err := formID(ctx)
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
fieldID, err := fieldID(ctx)
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
err = r.ParseForm()
if err != nil {
slog.Error("unable to parse form", slog.Any("error", err))
w.WriteHeader(http.StatusInternalServerError)
return
}
// the URL param 'id' represents the _target_ field id
targetFieldID, err := uuid.Parse(r.Form.Get("id"))
if err != nil {
slog.Error("unable to parse chosen field id", slog.Any("error", err))
w.WriteHeader(http.StatusNotFound)
return
}
2024-12-19 21:18:15 +00:00
draft, err := internal.Q(ctx, f.PostgresURL).GetDraft(ctx, internal.GetDraftParams{
WorkspaceID: f.WorkspaceID,
ID: *formID,
})
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
2024-12-19 21:18:15 +00:00
targetField, ok := draft.Fields[targetFieldID.String()]
if !ok {
w.WriteHeader(http.StatusNotFound)
return
}
2024-12-19 21:18:15 +00:00
err = ui.LogicConfiguratorStepThree((frm.Form)(draft), draft.Fields[fieldID.String()], targetField).Render(ctx, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
w.WriteHeader(200)
}
// UpdateSettings handles updates to form settings
func UpdateSettings(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
f, err := instance(ctx)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
2024-12-19 21:18:15 +00:00
draftID, err := formID(ctx)
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
2024-12-19 21:18:15 +00:00
form, err := internal.Q(ctx, f.PostgresURL).GetDraft(ctx, internal.GetDraftParams{
WorkspaceID: f.WorkspaceID,
2024-12-19 21:18:15 +00:00
ID: *draftID,
})
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
err = r.ParseForm()
if err != nil {
slog.Error("unable to parse form", slog.Any("error", err))
w.WriteHeader(http.StatusInternalServerError)
return
}
formName := r.Form.Get("name")
if len(formName) == 0 {
formName = "New form"
}
2024-12-19 21:18:15 +00:00
form, err = internal.Q(ctx, f.PostgresURL).SaveDraft(ctx, internal.SaveDraftParams{
ID: draftID,
Name: formName,
Fields: form.Fields,
})
if err != nil {
slog.Error("unable to save new form field")
w.WriteHeader(http.StatusInternalServerError)
return
}
// Re-render the form fields form UI
err = ui.FormFieldsForm((frm.Form)(form)).Render(ctx, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
// Re-render the form preview
err = ui.FormView((frm.Form)(form), true).Render(ctx, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
// Re-render the configurator form
err = ui.FormFieldConfigurator((frm.Form)(form)).Render(ctx, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
// Re-render the configurator form
err = ui.FormSettings((frm.Form)(form)).Render(ctx, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
// Re-render nav, so the title of the form updates
err = ui.FormBuilderNavTitle((frm.Form)(form)).Render(ctx, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
w.WriteHeader(200)
}
// NewField creates new form fields
func NewField(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
f, err := instance(ctx)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
formID, err := formID(ctx)
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
2024-12-19 21:18:15 +00:00
draft, err := internal.Q(ctx, f.PostgresURL).GetDraft(ctx, internal.GetDraftParams{
WorkspaceID: f.WorkspaceID,
ID: *formID,
})
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
err = r.ParseForm()
if err != nil {
slog.Error("unable to parse form", slog.Any("error", err))
w.WriteHeader(http.StatusInternalServerError)
return
}
ft := r.Form.Get("field_type")
fieldType, err := types.FormFieldTypeString(ft)
if err != nil {
slog.Error("unknown form field type", "type", ft)
w.WriteHeader(http.StatusInternalServerError)
return
}
2024-12-19 21:18:15 +00:00
fields := draft.Fields
order := len(fields) + 1 // place the new field at the end of the field list
fieldID := uuid.New()
newField := &types.FormField{ID: fieldID, Type: fieldType, Order: order}
switch fieldType {
case types.FormFieldTypeTextSingle:
newField.Label = "New text field"
newField.Placeholder = "Write some text"
case types.FormFieldTypeTextMultiple:
newField.Label = "New multi-line text field"
newField.Placeholder = "Write some text"
case types.FormFieldTypeSingleSelect:
newField.Label = "New select field"
newField.Placeholder = "Choose an item"
case types.FormFieldTypeMultiSelect:
newField.Label = "New multi select field"
newField.Placeholder = "Choose items item"
}
fields[fieldID.String()] = *newField
2024-12-19 21:18:15 +00:00
_, err = internal.Q(ctx, f.PostgresURL).SaveDraft(ctx, internal.SaveDraftParams{
ID: formID,
2024-12-19 21:18:15 +00:00
Name: draft.Name,
Fields: fields,
})
if err != nil {
slog.Error("unable to save new form field")
w.WriteHeader(http.StatusInternalServerError)
return
}
// Re-render the form fields form UI
2024-12-19 21:18:15 +00:00
err = ui.FormFieldsForm((frm.Form)(draft)).Render(ctx, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
// Re-render the form preview
2024-12-19 21:18:15 +00:00
err = ui.FormView((frm.Form)(draft), true).Render(ctx, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
// Re-render the configurator form
2024-12-19 21:18:15 +00:00
err = ui.FormFieldConfigurator((frm.Form)(draft)).Render(ctx, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
w.WriteHeader(200)
}
// UpdateFields handles updates to a form's fields
func UpdateFields(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
f, err := instance(ctx)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
formID, err := formID(ctx)
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
2024-12-19 21:18:15 +00:00
draft, err := internal.Q(ctx, f.PostgresURL).GetDraft(ctx, internal.GetDraftParams{
WorkspaceID: f.WorkspaceID,
ID: *formID,
})
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
err = r.ParseForm()
if err != nil {
slog.Error("unable to parse form", slog.Any("error", err))
w.WriteHeader(http.StatusInternalServerError)
return
}
2024-12-19 21:18:15 +00:00
updatedFields := draft.Fields
// clear out the logic fields, ensuring logic only gets configured when the UI reflects fully configured logic
for fid, f := range updatedFields {
f.Logic = nil
updatedFields[fid] = f
}
for formFieldName, formFieldValues := range r.Form {
matches := formFieldIDExtractor.FindStringSubmatch(formFieldName)
if len(matches) < 4 {
continue
}
fieldID := matches[1]
fieldGroup := matches[3]
fieldName := matches[4]
fieldValues := formFieldValues
isset := len(fieldValues) > 0
if !isset {
continue
}
switch {
case fieldName == "required":
val := (len(fieldValues) > 1 && fieldValues[1] == "on") || (len(fieldValues) > 0 && fieldValues[0] == "on")
2024-12-19 21:18:15 +00:00
oldField := draft.Fields[fieldID]
oldField.Required = val
updatedFields[fieldID] = oldField
case fieldName == "hidden":
val := (len(fieldValues) > 1 && fieldValues[1] == "on") || (len(fieldValues) > 0 && fieldValues[0] == "on")
2024-12-19 21:18:15 +00:00
oldField := draft.Fields[fieldID]
oldField.Hidden = val
updatedFields[fieldID] = oldField
case fieldName == "label":
2024-12-19 21:18:15 +00:00
oldField := draft.Fields[fieldID]
oldField.Label = fieldValues[0]
updatedFields[fieldID] = oldField
case fieldName == "placeholder":
2024-12-19 21:18:15 +00:00
oldField := draft.Fields[fieldID]
oldField.Placeholder = fieldValues[0]
updatedFields[fieldID] = oldField
case fieldName == "options":
2024-12-19 21:18:15 +00:00
oldField := draft.Fields[fieldID]
oldField.Options = toFormFieldOption(oldField, fieldValues)
updatedFields[fieldID] = oldField
case fieldGroup == ui.FieldGroupLogic && fieldName == ui.FieldLogicTargetFieldID:
targetFieldID, err := uuid.Parse(fieldValues[0])
if err != nil {
continue
}
2024-12-19 21:18:15 +00:00
oldField := draft.Fields[fieldID]
if oldField.Logic == nil {
oldField.Logic = &types.FieldLogic{}
}
oldField.Logic.TargetFieldID = targetFieldID
updatedFields[fieldID] = oldField
case fieldGroup == ui.FieldGroupLogic && fieldName == ui.FieldLogicTargetFieldValue:
2024-12-19 21:18:15 +00:00
oldField := draft.Fields[fieldID]
if oldField.Logic == nil {
oldField.Logic = &types.FieldLogic{}
}
oldField.Logic.TriggerValues = fieldValues
updatedFields[fieldID] = oldField
case fieldGroup == ui.FieldGroupLogic && fieldName == ui.FieldLogicComparator:
2024-12-19 21:18:15 +00:00
oldField := draft.Fields[fieldID]
if oldField.Logic == nil {
oldField.Logic = &types.FieldLogic{}
}
oldField.Logic.TriggerComparator, _ = types.FieldLogicComparatorString(fieldValues[0])
updatedFields[fieldID] = oldField
case fieldGroup == ui.FieldGroupLogic && fieldName == types.FieldLogicTriggerShow.String():
2024-12-19 21:18:15 +00:00
oldField := draft.Fields[fieldID]
if oldField.Logic == nil {
oldField.Logic = &types.FieldLogic{}
}
if len(fieldValues) > 0 && fieldValues[0] == "on" {
oldField.Logic.TriggerActions = []types.FieldLogicTriggerAction{types.FieldLogicTriggerShow}
}
updatedFields[fieldID] = oldField
}
}
2024-12-19 21:18:15 +00:00
draft.Fields = updatedFields
draft, err = internal.Q(ctx, f.PostgresURL).SaveDraft(ctx, internal.SaveDraftParams{
ID: draft.ID,
Name: draft.Name,
Fields: updatedFields,
})
if err != nil {
slog.Error("unable to save form", slog.Any("error", err))
w.WriteHeader(http.StatusInternalServerError)
}
// Re-render the form fields form UI
2024-12-19 21:18:15 +00:00
err = ui.FormFieldsForm((frm.Form)(draft)).Render(ctx, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
// Re-render the form preview
2024-12-19 21:18:15 +00:00
err = ui.FormView((frm.Form)(draft), true).Render(ctx, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
w.WriteHeader(http.StatusOK)
}
// DeleteField deletes fields
func DeleteField(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
f, err := instance(ctx)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
formID, err := formID(ctx)
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
fieldID, err := fieldID(ctx)
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
2024-12-19 21:18:15 +00:00
draft, err := internal.Q(ctx, f.PostgresURL).GetDraft(ctx, internal.GetDraftParams{
WorkspaceID: f.WorkspaceID,
ID: *formID,
})
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
2024-12-19 21:18:15 +00:00
updatedFields := draft.Fields
delete(updatedFields, fmt.Sprint(fieldID))
2024-12-19 21:18:15 +00:00
draft.Fields = updatedFields
draft, err = internal.Q(ctx, f.PostgresURL).SaveDraft(ctx, internal.SaveDraftParams{
ID: draft.ID,
Name: draft.Name,
Fields: updatedFields,
})
if err != nil {
slog.Error("unable to delete form field", slog.Any("error", err))
w.WriteHeader(http.StatusInternalServerError)
}
// Re-render the form fields form UI
2024-12-19 21:18:15 +00:00
err = ui.FormFieldsForm((frm.Form)(draft)).Render(ctx, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
// Re-render the form preview
2024-12-19 21:18:15 +00:00
err = ui.FormView((frm.Form)(draft), true).Render(ctx, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
// Re-render the configurator form
2024-12-19 21:18:15 +00:00
err = ui.FormFieldConfigurator((frm.Form)(draft)).Render(ctx, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
w.WriteHeader(http.StatusOK)
}
2024-12-19 16:50:49 +00:00
// NewDraft creates a new draft an existing form
func NewDraft(w http.ResponseWriter, r *http.Request) {
}
2024-12-19 21:18:15 +00:00
// PublishDraft copies drafts to published forms
func PublishDraft(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
f, err := instance(ctx)
if err != nil {
slog.Error("unable to publish draft", "error", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
formID, err := formID(ctx)
if err != nil {
slog.Error("unable to publish draft", "error", err)
w.WriteHeader(http.StatusNotFound)
return
}
draft, err := internal.Q(ctx, f.PostgresURL).GetDraft(ctx, internal.GetDraftParams{
WorkspaceID: f.WorkspaceID,
ID: *formID,
})
if err != nil {
slog.Error("unable to publish draft", "error", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
_, err = internal.Q(ctx, f.PostgresURL).PublishDraft(ctx, draft.ID)
if err != nil {
slog.Error("unable to publish draft", "error", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
2024-12-19 16:50:49 +00:00
// instance returns the frm instance from the request context
func instance(ctx context.Context) (i *frm.Frm, err error) {
var ok bool
i, ok = ctx.Value(internal.FrmContextKey).(*frm.Frm)
if !ok {
return nil, ErrNoInstanceAvailable
}
return
}
// formID gets the form ID from the request context
func formID(ctx context.Context) (formID *int64, err error) {
var ok bool
formID, ok = ctx.Value(FormIDContextKey).(*int64)
if !ok {
return nil, ErrFormIDNotFound
}
return
}
// fieldID gets the field id from the request context
func fieldID(ctx context.Context) (fieldID *uuid.UUID, err error) {
var ok bool
fieldID, ok = ctx.Value(FieldIDContextKey).(*uuid.UUID)
if !ok {
return nil, ErrFieldIDNotFound
}
return
}
// This regex parses form field names of the following form
//
// [FIELD_UUID]FIELD_NAME
// Example: [2ad1591d-c852-47b5-a16d-0b90892421c8]label
//
// [FIELD_UUID][SUBGROUP_NAME]FIELD_NAME
// Example: [2ad1591d-c852-47b5-a16d-0b90892421c8][logic]target_field_id
var formFieldIDExtractor = regexp.MustCompile(`^\[([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12})\](\[(.+)\]){0,1}(.+){1}?$`)
// toFormFieldOption takes a list of options sa strings and determines whether the string options represent new options
// being created, in which case an ID/value must be generated for the option, or if the option is amongst the existing
// options for the field being updated.
func toFormFieldOption(field types.FormField, options []string) types.FieldOptions {
fieldOptions := types.FieldOptions{}
for _, option := range options {
var id uuid.UUID
optionID, err := uuid.Parse(option)
if err != nil {
id = uuid.New()
fieldOptions = append(fieldOptions, types.Option{
ID: id,
Value: id.String(),
Label: option,
})
} else {
for _, opt := range field.Options {
if opt.ID == optionID {
fieldOptions = append(fieldOptions, opt)
}
}
}
}
return fieldOptions
}