feat: publish form from a draft

This commit is contained in:
Adriano Caloiaro 2024-12-19 14:18:15 -07:00
parent 15518cc1b4
commit 92378b5d1d
No known key found for this signature in database
GPG key ID: C2BC56DE73CE3F75
13 changed files with 1009 additions and 676 deletions

View file

@ -71,20 +71,21 @@ func TestCreateAndUpdateForm(t *testing.T) {
t.Error(err)
return
}
f, err := internal.Q(ctx, frms.PostgresURL).SaveForm(ctx, internal.SaveFormParams{
ID: 1,
Name: "hello world",
Fields: fields,
f, err := internal.Q(ctx, frms.PostgresURL).SaveDraft(ctx, internal.SaveDraftParams{
Name: "hello world",
Fields: fields,
WorkspaceID: frms.WorkspaceID,
})
if err != nil {
t.Error(err)
return
}
f, err = internal.Q(ctx, frms.PostgresURL).SaveForm(ctx, internal.SaveFormParams{
ID: 1,
Name: nameUpdate,
Fields: updatedFields,
f, err = internal.Q(ctx, frms.PostgresURL).SaveDraft(ctx, internal.SaveDraftParams{
ID: 1,
Name: nameUpdate,
Fields: updatedFields,
WorkspaceID: frms.WorkspaceID,
})
if err != nil {
t.Error(err)

View file

@ -1,4 +1,3 @@
DROP TABLE IF EXISTS forms;
DROP TABLE IF EXISTS draft_forms;
DROP SEQUENCE IF EXISTS forms_ids;
DROP SEQUENCE IF EXISTS draft_forms_ids;
DROP SEQUENCE IF EXISTS form_ids;
DROP TYPE IF EXISTS form_status;

View file

@ -3,30 +3,27 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- create the forms pk sequence
CREATE SEQUENCE IF NOT EXISTS form_ids START 1;
CREATE SEQUENCE IF NOT EXISTS draft_form_ids START 1;
-- create form statuses enum
CREATE TYPE form_status AS ENUM (
'published',
'draft'
);
-- the forms table contains all forms
-- workspaces represent users, tenants, groupings, etc. in external systems
CREATE TABLE IF NOT EXISTS forms (
id BIGINT PRIMARY KEY DEFAULT nextval('form_ids'),
form_id BIGINT REFERENCES forms(id),
workspace_id UUID NOT NULL,
name text not null,
fields jsonb default '{}' NOT NULL,
status form_status default 'draft' NOT NULL,
created_at timestamptz not null default timezone('utc', now()),
updated_at timestamptz not null default timezone('utc', now())
);
CREATE TABLE IF NOT EXISTS draft_forms (
id BIGINT PRIMARY KEY DEFAULT nextval('draft_form_ids'),
form_id BIGINT REFERENCES forms(id),
workspace_id UUID,
name text not null,
fields jsonb default '{}' NOT NULL,
created_at timestamptz not null default timezone('utc', now()),
updated_at timestamptz not null default timezone('utc', now())
);
COMMENT ON table forms IS 'Form contains all the data necesary to render a form';
COMMENT ON table draft_forms IS 'Draft forms allow forms to be edited in situ and act as a staging ground for Forms';
COMMENT ON column forms.workspace_id IS 'a namespace for the form';
COMMENT ON column forms.fields IS 'all form fields are serialized to JSON, see types.FormFields for structure details';

View file

@ -5,17 +5,82 @@ FROM forms
WHERE workspace_id = @workspace_id
AND id = @id;
-- name: GetDraft :one
SELECT *
FROM forms
WHERE workspace_id = @workspace_id
AND id = @id
AND status = 'draft';
-- name: ListForms :many
SELECT *
FROM forms
WHERE workspace_id = @workspace_id;
-- name: SaveForm :one
-- name: ListDrafts :many
INSERT INTO forms (id, workspace_id, name, fields)
VALUES (coalesce(nullif(@id, 0), nextval('form_ids'))::bigint, @workspace_id, @name, @fields) ON conflict(id) DO
SELECT *
FROM forms
WHERE workspace_id = @workspace_id
AND form_id = @form_id
AND status = 'draft';
-- name: SaveDraft :one
INSERT INTO forms (id, form_id, workspace_id, name, fields)
VALUES (coalesce(nullif(@id, 0), nextval('form_ids'))::bigint, @form_id, @workspace_id, @name, @fields) ON conflict(id) DO
UPDATE
SET updated_at = timezone('utc', now()),
name = @name,
fields = @fields RETURNING *;
-- name: PublishDraft :one
WITH draft AS
(SELECT CASE
WHEN form_id IS NOT NULL THEN form_id
ELSE nextval('form_ids')
END AS id,
form_id,
workspace_id,
name,
fields,
'published'
FROM forms
WHERE forms.id = @id)
INSERT INTO forms(id, form_id, workspace_id, name, fields, status)
VALUES ((SELECT id FROM draft), NULL, (SELECT workspace_id FROM draft), (SELECT name FROM draft), (SELECT fields FROM draft), 'published') ON conflict(id) DO
UPDATE
SET updated_at = timezone('utc', now()),
form_id = NULL,
workspace_id =
(SELECT workspace_id
FROM draft),
name =
(SELECT name
FROM draft),
fields =
(SELECT fields
FROM draft),
status = 'published' RETURNING *;
-- update
-- INSERT INTO forms(id, form_id, workspace_id, name, fields, status)
-- SELECT CASE
-- WHEN form_id IS NOT NULL THEN form_id
-- ELSE nextval('form_ids')
-- END AS id,
-- form_id,
-- workspace_id,
-- name,
-- fields,
-- 'published'
-- FROM forms
-- WHERE forms.id = @id ON conflict(id) DO
-- UPDATE
-- SET updated_at = timezone('utc', now()),
-- form_id = NULL,
-- name = forms.name,
-- fields = forms.fields,
-- status = 'published' RETURNING *;

View file

@ -49,7 +49,7 @@ func FormEditor(w http.ResponseWriter, r *http.Request) {
return
}
form, err := internal.Q(ctx, f.PostgresURL).GetForm(r.Context(), internal.GetFormParams{
draft, err := internal.Q(ctx, f.PostgresURL).GetDraft(r.Context(), internal.GetDraftParams{
WorkspaceID: f.WorkspaceID,
ID: *id,
})
@ -59,7 +59,7 @@ func FormEditor(w http.ResponseWriter, r *http.Request) {
return
}
err = ui.Builder((frm.Form)(form)).Render(r.Context(), w)
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)
@ -80,7 +80,7 @@ func UpdateFieldOrder(w http.ResponseWriter, r *http.Request) {
return
}
form, err := internal.Q(ctx, f.PostgresURL).GetForm(ctx, internal.GetFormParams{
draft, err := internal.Q(ctx, f.PostgresURL).GetDraft(ctx, internal.GetDraftParams{
WorkspaceID: f.WorkspaceID,
ID: *formID,
})
@ -101,7 +101,7 @@ func UpdateFieldOrder(w http.ResponseWriter, r *http.Request) {
switch key {
case "order":
for order, fieldID := range field {
oldField := form.Fields[fieldID]
oldField := draft.Fields[fieldID]
oldField.Order = order
updatedFields[fieldID] = oldField
}
@ -109,24 +109,24 @@ func UpdateFieldOrder(w http.ResponseWriter, r *http.Request) {
}
}
form.Fields = updatedFields
form, err = internal.Q(ctx, f.PostgresURL).SaveForm(ctx, internal.SaveFormParams{
ID: form.ID,
Name: form.Name,
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))
slog.Error("unable to save draft", slog.Any("error", err))
w.WriteHeader(http.StatusInternalServerError)
}
// Re-render the form fields form UI
err = ui.FormFieldsForm((frm.Form)(form)).Render(ctx, w)
err = ui.FormFieldsForm((frm.Form)(draft)).Render(ctx, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
err = ui.FormView((frm.Form)(form), true).Render(ctx, w)
err = ui.FormView((frm.Form)(draft), true).Render(ctx, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
@ -170,7 +170,7 @@ func LogicConfiguratorStep3(w http.ResponseWriter, r *http.Request) {
return
}
form, err := internal.Q(ctx, f.PostgresURL).GetForm(ctx, internal.GetFormParams{
draft, err := internal.Q(ctx, f.PostgresURL).GetDraft(ctx, internal.GetDraftParams{
WorkspaceID: f.WorkspaceID,
ID: *formID,
})
@ -179,13 +179,13 @@ func LogicConfiguratorStep3(w http.ResponseWriter, r *http.Request) {
return
}
targetField, ok := form.Fields[targetFieldID.String()]
targetField, ok := draft.Fields[targetFieldID.String()]
if !ok {
w.WriteHeader(http.StatusNotFound)
return
}
err = ui.LogicConfiguratorStepThree((frm.Form)(form), form.Fields[fieldID.String()], targetField).Render(ctx, w)
err = ui.LogicConfiguratorStepThree((frm.Form)(draft), draft.Fields[fieldID.String()], targetField).Render(ctx, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
@ -201,14 +201,14 @@ func UpdateSettings(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
return
}
formID, err := formID(ctx)
draftID, err := formID(ctx)
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
form, err := internal.Q(ctx, f.PostgresURL).GetForm(ctx, internal.GetFormParams{
form, err := internal.Q(ctx, f.PostgresURL).GetDraft(ctx, internal.GetDraftParams{
WorkspaceID: f.WorkspaceID,
ID: *formID,
ID: *draftID,
})
if err != nil {
w.WriteHeader(http.StatusNotFound)
@ -226,8 +226,8 @@ func UpdateSettings(w http.ResponseWriter, r *http.Request) {
if len(formName) == 0 {
formName = "New form"
}
form, err = internal.Q(ctx, f.PostgresURL).SaveForm(ctx, internal.SaveFormParams{
ID: formID,
form, err = internal.Q(ctx, f.PostgresURL).SaveDraft(ctx, internal.SaveDraftParams{
ID: draftID,
Name: formName,
Fields: form.Fields,
})
@ -283,7 +283,7 @@ func NewField(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
return
}
form, err := internal.Q(ctx, f.PostgresURL).GetForm(ctx, internal.GetFormParams{
draft, err := internal.Q(ctx, f.PostgresURL).GetDraft(ctx, internal.GetDraftParams{
WorkspaceID: f.WorkspaceID,
ID: *formID,
})
@ -305,7 +305,7 @@ func NewField(w http.ResponseWriter, r *http.Request) {
return
}
fields := form.Fields
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}
@ -325,9 +325,9 @@ func NewField(w http.ResponseWriter, r *http.Request) {
}
fields[fieldID.String()] = *newField
_, err = internal.Q(ctx, f.PostgresURL).SaveForm(ctx, internal.SaveFormParams{
_, err = internal.Q(ctx, f.PostgresURL).SaveDraft(ctx, internal.SaveDraftParams{
ID: formID,
Name: form.Name,
Name: draft.Name,
Fields: fields,
})
if err != nil {
@ -337,19 +337,19 @@ func NewField(w http.ResponseWriter, r *http.Request) {
}
// Re-render the form fields form UI
err = ui.FormFieldsForm((frm.Form)(form)).Render(ctx, w)
err = ui.FormFieldsForm((frm.Form)(draft)).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)
err = ui.FormView((frm.Form)(draft), 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)
err = ui.FormFieldConfigurator((frm.Form)(draft)).Render(ctx, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
@ -370,7 +370,7 @@ func UpdateFields(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
return
}
form, err := internal.Q(ctx, f.PostgresURL).GetForm(ctx, internal.GetFormParams{
draft, err := internal.Q(ctx, f.PostgresURL).GetDraft(ctx, internal.GetDraftParams{
WorkspaceID: f.WorkspaceID,
ID: *formID,
})
@ -386,7 +386,7 @@ func UpdateFields(w http.ResponseWriter, r *http.Request) {
return
}
updatedFields := form.Fields
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 {
@ -410,24 +410,24 @@ func UpdateFields(w http.ResponseWriter, r *http.Request) {
switch {
case fieldName == "required":
val := (len(fieldValues) > 1 && fieldValues[1] == "on") || (len(fieldValues) > 0 && fieldValues[0] == "on")
oldField := form.Fields[fieldID]
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")
oldField := form.Fields[fieldID]
oldField := draft.Fields[fieldID]
oldField.Hidden = val
updatedFields[fieldID] = oldField
case fieldName == "label":
oldField := form.Fields[fieldID]
oldField := draft.Fields[fieldID]
oldField.Label = fieldValues[0]
updatedFields[fieldID] = oldField
case fieldName == "placeholder":
oldField := form.Fields[fieldID]
oldField := draft.Fields[fieldID]
oldField.Placeholder = fieldValues[0]
updatedFields[fieldID] = oldField
case fieldName == "options":
oldField := form.Fields[fieldID]
oldField := draft.Fields[fieldID]
oldField.Options = toFormFieldOption(oldField, fieldValues)
updatedFields[fieldID] = oldField
case fieldGroup == ui.FieldGroupLogic && fieldName == ui.FieldLogicTargetFieldID:
@ -435,28 +435,28 @@ func UpdateFields(w http.ResponseWriter, r *http.Request) {
if err != nil {
continue
}
oldField := form.Fields[fieldID]
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:
oldField := form.Fields[fieldID]
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:
oldField := form.Fields[fieldID]
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():
oldField := form.Fields[fieldID]
oldField := draft.Fields[fieldID]
if oldField.Logic == nil {
oldField.Logic = &types.FieldLogic{}
}
@ -467,10 +467,10 @@ func UpdateFields(w http.ResponseWriter, r *http.Request) {
}
}
form.Fields = updatedFields
form, err = internal.Q(ctx, f.PostgresURL).SaveForm(ctx, internal.SaveFormParams{
ID: form.ID,
Name: form.Name,
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 {
@ -479,13 +479,13 @@ func UpdateFields(w http.ResponseWriter, r *http.Request) {
}
// Re-render the form fields form UI
err = ui.FormFieldsForm((frm.Form)(form)).Render(ctx, w)
err = ui.FormFieldsForm((frm.Form)(draft)).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)
err = ui.FormView((frm.Form)(draft), true).Render(ctx, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
@ -512,7 +512,7 @@ func DeleteField(w http.ResponseWriter, r *http.Request) {
return
}
form, err := internal.Q(ctx, f.PostgresURL).GetForm(ctx, internal.GetFormParams{
draft, err := internal.Q(ctx, f.PostgresURL).GetDraft(ctx, internal.GetDraftParams{
WorkspaceID: f.WorkspaceID,
ID: *formID,
})
@ -520,12 +520,12 @@ func DeleteField(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
return
}
updatedFields := form.Fields
updatedFields := draft.Fields
delete(updatedFields, fmt.Sprint(fieldID))
form.Fields = updatedFields
form, err = internal.Q(ctx, f.PostgresURL).SaveForm(ctx, internal.SaveFormParams{
ID: form.ID,
Name: form.Name,
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 {
@ -534,19 +534,19 @@ func DeleteField(w http.ResponseWriter, r *http.Request) {
}
// Re-render the form fields form UI
err = ui.FormFieldsForm((frm.Form)(form)).Render(ctx, w)
err = ui.FormFieldsForm((frm.Form)(draft)).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)
err = ui.FormView((frm.Form)(draft), 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)
err = ui.FormFieldConfigurator((frm.Form)(draft)).Render(ctx, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
@ -558,6 +558,42 @@ func DeleteField(w http.ResponseWriter, r *http.Request) {
func NewDraft(w http.ResponseWriter, r *http.Request) {
}
// 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)
}
// instance returns the frm instance from the request context
func instance(ctx context.Context) (i *frm.Frm, err error) {
var ok bool

View file

@ -5,31 +5,66 @@
package internal
import (
"database/sql/driver"
"fmt"
"time"
"github.com/acaloiaro/frm/types"
uuid "github.com/google/uuid"
)
// Draft forms allow forms to be edited in situ and act as a staging ground for Forms
type DraftForm struct {
ID int64 `json:"id"`
FormID *int64 `json:"form_id"`
WorkspaceID uuid.UUID `json:"workspace_id"`
Name string `json:"name"`
Fields []byte `json:"fields"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
type FormStatus string
const (
FormStatusPublished FormStatus = "published"
FormStatusDraft FormStatus = "draft"
)
func (e *FormStatus) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = FormStatus(s)
case string:
*e = FormStatus(s)
default:
return fmt.Errorf("unsupported scan type for FormStatus: %T", src)
}
return nil
}
type NullFormStatus struct {
FormStatus FormStatus `json:"form_status"`
Valid bool `json:"valid"` // Valid is true if FormStatus is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullFormStatus) Scan(value interface{}) error {
if value == nil {
ns.FormStatus, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.FormStatus.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullFormStatus) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.FormStatus), nil
}
// Form contains all the data necesary to render a form
type Form struct {
ID int64 `json:"id"`
ID int64 `json:"id"`
FormID *int64 `json:"form_id"`
// a namespace for the form
WorkspaceID uuid.UUID `json:"workspace_id"`
Name string `json:"name"`
// all form fields are serialized to JSON, see types.FormFields for structure details
Fields types.FormFields `json:"fields"`
Status FormStatus `json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

View file

@ -12,9 +12,46 @@ import (
uuid "github.com/google/uuid"
)
const getDraft = `-- name: GetDraft :one
SELECT id, form_id, workspace_id, name, fields, status, created_at, updated_at
FROM forms
WHERE workspace_id = $1
AND id = $2
AND status = 'draft'
`
type GetDraftParams struct {
WorkspaceID uuid.UUID `json:"workspace_id"`
ID int64 `json:"id"`
}
// GetDraft
//
// SELECT id, form_id, workspace_id, name, fields, status, created_at, updated_at
// FROM forms
// WHERE workspace_id = $1
// AND id = $2
// AND status = 'draft'
func (q *Queries) GetDraft(ctx context.Context, arg GetDraftParams) (Form, error) {
row := q.db.QueryRow(ctx, getDraft, arg.WorkspaceID, arg.ID)
var i Form
err := row.Scan(
&i.ID,
&i.FormID,
&i.WorkspaceID,
&i.Name,
&i.Fields,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getForm = `-- name: GetForm :one
SELECT id, workspace_id, name, fields, created_at, updated_at
SELECT id, form_id, workspace_id, name, fields, status, created_at, updated_at
FROM forms
WHERE workspace_id = $1
AND id = $2
@ -27,7 +64,7 @@ type GetFormParams struct {
// GetForm
//
// SELECT id, workspace_id, name, fields, created_at, updated_at
// SELECT id, form_id, workspace_id, name, fields, status, created_at, updated_at
// FROM forms
// WHERE workspace_id = $1
// AND id = $2
@ -36,29 +73,40 @@ func (q *Queries) GetForm(ctx context.Context, arg GetFormParams) (Form, error)
var i Form
err := row.Scan(
&i.ID,
&i.FormID,
&i.WorkspaceID,
&i.Name,
&i.Fields,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const listForms = `-- name: ListForms :many
const listDrafts = `-- name: ListDrafts :many
SELECT id, workspace_id, name, fields, created_at, updated_at
SELECT id, form_id, workspace_id, name, fields, status, created_at, updated_at
FROM forms
WHERE workspace_id = $1
AND form_id = $2
AND status = 'draft'
`
// ListForms
type ListDraftsParams struct {
WorkspaceID uuid.UUID `json:"workspace_id"`
FormID *int64 `json:"form_id"`
}
// ListDrafts
//
// SELECT id, workspace_id, name, fields, created_at, updated_at
// SELECT id, form_id, workspace_id, name, fields, status, created_at, updated_at
// FROM forms
// WHERE workspace_id = $1
func (q *Queries) ListForms(ctx context.Context, workspaceID uuid.UUID) ([]Form, error) {
rows, err := q.db.Query(ctx, listForms, workspaceID)
// AND form_id = $2
// AND status = 'draft'
func (q *Queries) ListDrafts(ctx context.Context, arg ListDraftsParams) ([]Form, error) {
rows, err := q.db.Query(ctx, listDrafts, arg.WorkspaceID, arg.FormID)
if err != nil {
return nil, err
}
@ -68,9 +116,11 @@ func (q *Queries) ListForms(ctx context.Context, workspaceID uuid.UUID) ([]Form,
var i Form
if err := rows.Scan(
&i.ID,
&i.FormID,
&i.WorkspaceID,
&i.Name,
&i.Fields,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
@ -84,34 +134,152 @@ func (q *Queries) ListForms(ctx context.Context, workspaceID uuid.UUID) ([]Form,
return items, nil
}
const saveForm = `-- name: SaveForm :one
const listForms = `-- name: ListForms :many
INSERT INTO forms (id, workspace_id, name, fields)
VALUES (coalesce(nullif($1, 0), nextval('form_ids'))::bigint, $2, $3, $4) ON conflict(id) DO
UPDATE
SET updated_at = timezone('utc', now()),
name = $3,
fields = $4 RETURNING id, workspace_id, name, fields, created_at, updated_at
SELECT id, form_id, workspace_id, name, fields, status, created_at, updated_at
FROM forms
WHERE workspace_id = $1
`
type SaveFormParams struct {
// ListForms
//
// SELECT id, form_id, workspace_id, name, fields, status, created_at, updated_at
// FROM forms
// WHERE workspace_id = $1
func (q *Queries) ListForms(ctx context.Context, workspaceID uuid.UUID) ([]Form, error) {
rows, err := q.db.Query(ctx, listForms, workspaceID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Form
for rows.Next() {
var i Form
if err := rows.Scan(
&i.ID,
&i.FormID,
&i.WorkspaceID,
&i.Name,
&i.Fields,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const publishDraft = `-- name: PublishDraft :one
WITH draft AS
(SELECT CASE
WHEN form_id IS NOT NULL THEN form_id
ELSE nextval('form_ids')
END AS id,
form_id,
workspace_id,
name,
fields,
'published'
FROM forms
WHERE forms.id = $1)
INSERT INTO forms(id, form_id, workspace_id, name, fields, status)
VALUES ((SELECT id FROM draft), NULL, (SELECT workspace_id FROM draft), (SELECT name FROM draft), (SELECT fields FROM draft), 'published') ON conflict(id) DO
UPDATE
SET updated_at = timezone('utc', now()),
form_id = NULL,
workspace_id =
(SELECT workspace_id
FROM draft),
name =
(SELECT name
FROM draft),
fields =
(SELECT fields
FROM draft),
status = 'published' RETURNING id, form_id, workspace_id, name, fields, status, created_at, updated_at
`
// PublishDraft
//
// WITH draft AS
// (SELECT CASE
// WHEN form_id IS NOT NULL THEN form_id
// ELSE nextval('form_ids')
// END AS id,
// form_id,
// workspace_id,
// name,
// fields,
// 'published'
// FROM forms
// WHERE forms.id = $1)
// INSERT INTO forms(id, form_id, workspace_id, name, fields, status)
// VALUES ((SELECT id FROM draft), NULL, (SELECT workspace_id FROM draft), (SELECT name FROM draft), (SELECT fields FROM draft), 'published') ON conflict(id) DO
// UPDATE
// SET updated_at = timezone('utc', now()),
// form_id = NULL,
// workspace_id =
// (SELECT workspace_id
// FROM draft),
// name =
// (SELECT name
// FROM draft),
// fields =
// (SELECT fields
// FROM draft),
// status = 'published' RETURNING id, form_id, workspace_id, name, fields, status, created_at, updated_at
func (q *Queries) PublishDraft(ctx context.Context, id int64) (Form, error) {
row := q.db.QueryRow(ctx, publishDraft, id)
var i Form
err := row.Scan(
&i.ID,
&i.FormID,
&i.WorkspaceID,
&i.Name,
&i.Fields,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const saveDraft = `-- name: SaveDraft :one
INSERT INTO forms (id, form_id, workspace_id, name, fields)
VALUES (coalesce(nullif($1, 0), nextval('form_ids'))::bigint, $2, $3, $4, $5) ON conflict(id) DO
UPDATE
SET updated_at = timezone('utc', now()),
name = $4,
fields = $5 RETURNING id, form_id, workspace_id, name, fields, status, created_at, updated_at
`
type SaveDraftParams struct {
ID interface{} `json:"id"`
FormID *int64 `json:"form_id"`
WorkspaceID uuid.UUID `json:"workspace_id"`
Name string `json:"name"`
Fields types.FormFields `json:"fields"`
}
// SaveForm
// SaveDraft
//
// INSERT INTO forms (id, workspace_id, name, fields)
// VALUES (coalesce(nullif($1, 0), nextval('form_ids'))::bigint, $2, $3, $4) ON conflict(id) DO
// INSERT INTO forms (id, form_id, workspace_id, name, fields)
// VALUES (coalesce(nullif($1, 0), nextval('form_ids'))::bigint, $2, $3, $4, $5) ON conflict(id) DO
// UPDATE
// SET updated_at = timezone('utc', now()),
// name = $3,
// fields = $4 RETURNING id, workspace_id, name, fields, created_at, updated_at
func (q *Queries) SaveForm(ctx context.Context, arg SaveFormParams) (Form, error) {
row := q.db.QueryRow(ctx, saveForm,
// name = $4,
// fields = $5 RETURNING id, form_id, workspace_id, name, fields, status, created_at, updated_at
func (q *Queries) SaveDraft(ctx context.Context, arg SaveDraftParams) (Form, error) {
row := q.db.QueryRow(ctx, saveDraft,
arg.ID,
arg.FormID,
arg.WorkspaceID,
arg.Name,
arg.Fields,
@ -119,9 +287,11 @@ func (q *Queries) SaveForm(ctx context.Context, arg SaveFormParams) (Form, error
var i Form
err := row.Scan(
&i.ID,
&i.FormID,
&i.WorkspaceID,
&i.Name,
&i.Fields,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
)

View file

@ -33,6 +33,7 @@ func Mount(router chi.Router, mountPoint string, f *frm.Frm) {
rc := r.With(addRequestContext)
rc.Get(fmt.Sprintf("/{%s}", urlParamFormID), handlers.FormEditor)
rc.Get(fmt.Sprintf("/{%s}/draft", urlParamFormID), handlers.NewDraft)
rc.Put(fmt.Sprintf("/{%s}/publish", urlParamFormID), handlers.PublishDraft)
rc.Put(fmt.Sprintf("/{%s}/fields/order", urlParamFormID), handlers.UpdateFieldOrder)
rc.Get(fmt.Sprintf("/{%s}/logic_configurator/{%s}/step3", urlParamFormID, urlParamFieldID), handlers.LogicConfiguratorStep3)
rc.Put(fmt.Sprintf("/{%s}/settings", urlParamFormID), handlers.UpdateSettings)

View file

@ -37,6 +37,15 @@ overrides:
import: github.com/google/uuid
package: uuid
type: UUID
- column: draft_forms.fields
go_type:
import: github.com/acaloiaro/frm/types
type: FormFields
- column: draft_forms.workspace_id
go_type:
import: github.com/google/uuid
package: uuid
type: UUID
sql:
- engine: postgresql

View file

@ -6,7 +6,7 @@ import (
"github.com/acaloiaro/frm/internal"
)
// Form is a form
// Form is a published form
type Form internal.Form
type Forms []internal.Form

View file

@ -111,7 +111,13 @@ templ FormBuilderNav(form frm.Form) {
<a href="#" class="text-sm p-2 hover:bg-gray-100 cursor-pointer rounded-lg text-gray-500 hover:text-gray-800 cursor-pointer"><span class="iconify i-heroicons:question-mark-circle w-5 h-5" aria-hidden="true"></span></a><!---->
</div>
<div class="relative inline-flex">
<button type="button" class="focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:cursor-not-allowed aria-disabled:opacity-75 flex-shrink-0 font-medium rounded-md text-sm gap-x-1.5 shadow-sm text-white dark:text-gray-900 bg-primary-500 hover:bg-primary-600 disabled:bg-primary-500 aria-disabled:bg-primary-500 dark:bg-primary-400 dark:hover:bg-primary-500 dark:disabled:bg-primary-400 dark:aria-disabled:bg-primary-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 dark:focus-visible:outline-primary-400 inline-flex items-center px-8 md:px-4 py-2">
<button
type="button"
class="focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:cursor-not-allowed aria-disabled:opacity-75 flex-shrink-0 font-medium rounded-md text-sm gap-x-1.5 shadow-sm text-white dark:text-gray-900 bg-primary-500 hover:bg-primary-600 disabled:bg-primary-500 aria-disabled:bg-primary-500 dark:bg-primary-400 dark:hover:bg-primary-500 dark:disabled:bg-primary-400 dark:aria-disabled:bg-primary-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 dark:focus-visible:outline-primary-400 inline-flex items-center px-8 md:px-4 py-2"
data-hx-put={ formUrl[string](ctx, form, "/publish") }
data-hx-trigger="click"
data-hx-swap="none"
>
<!---->
<svg class="w-4 h-4 text-white inline mr-1 -mt-1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M17 21V13H7V21M7 3V8H15M19 21H5C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3H16L21 8V19C21 19.5304 20.7893 20.0391 20.4142 20.4142C20.0391 20.7893 19.5304 21 19 21Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg> Save Form <!---->
</button><!---->

File diff suppressed because it is too large Load diff

View file

@ -12,7 +12,8 @@
</h3></div>
<div id=\"form-builder-nav\" data-hx-swap-oob=\"true\" class=\"w-full border-b p-2 flex gap-x-2 items-center bg-white\"><div id=\"form-editor-navbar-tabs\"><div role=\"tablist\" aria-orientation=\"horizontal\" class=\"tabs tabs-boxed bg-gray-50 dark:bg-gray-800 rounded-lg p-1 h-auto grid grid-cols-2 items-center gap-x-1.5 px-2\">
</div></div>
<div class=\"flex items-stretch gap-x-2\"><div class=\"inline-flex items-center relative\"><a href=\"#\" class=\"text-sm p-2 hover:bg-gray-100 cursor-pointer rounded-lg text-gray-500 hover:text-gray-800 cursor-pointer\"><span class=\"iconify i-heroicons:question-mark-circle w-5 h-5\" aria-hidden=\"true\"></span></a><!----></div><div class=\"relative inline-flex\"><button type=\"button\" class=\"focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:cursor-not-allowed aria-disabled:opacity-75 flex-shrink-0 font-medium rounded-md text-sm gap-x-1.5 shadow-sm text-white dark:text-gray-900 bg-primary-500 hover:bg-primary-600 disabled:bg-primary-500 aria-disabled:bg-primary-500 dark:bg-primary-400 dark:hover:bg-primary-500 dark:disabled:bg-primary-400 dark:aria-disabled:bg-primary-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 dark:focus-visible:outline-primary-400 inline-flex items-center px-8 md:px-4 py-2\"><!----><svg class=\"w-4 h-4 text-white inline mr-1 -mt-1\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M17 21V13H7V21M7 3V8H15M19 21H5C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3H16L21 8V19C21 19.5304 20.7893 20.0391 20.4142 20.4142C20.0391 20.7893 19.5304 21 19 21Z\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></path></svg> Save Form <!----></button><!----></div></div></div>
<div class=\"flex items-stretch gap-x-2\"><div class=\"inline-flex items-center relative\"><a href=\"#\" class=\"text-sm p-2 hover:bg-gray-100 cursor-pointer rounded-lg text-gray-500 hover:text-gray-800 cursor-pointer\"><span class=\"iconify i-heroicons:question-mark-circle w-5 h-5\" aria-hidden=\"true\"></span></a><!----></div><div class=\"relative inline-flex\"><button type=\"button\" class=\"focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:cursor-not-allowed aria-disabled:opacity-75 flex-shrink-0 font-medium rounded-md text-sm gap-x-1.5 shadow-sm text-white dark:text-gray-900 bg-primary-500 hover:bg-primary-600 disabled:bg-primary-500 aria-disabled:bg-primary-500 dark:bg-primary-400 dark:hover:bg-primary-500 dark:disabled:bg-primary-400 dark:aria-disabled:bg-primary-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 dark:focus-visible:outline-primary-400 inline-flex items-center px-8 md:px-4 py-2\" data-hx-put=\"
\" data-hx-trigger=\"click\" data-hx-swap=\"none\"><!----><svg class=\"w-4 h-4 text-white inline mr-1 -mt-1\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M17 21V13H7V21M7 3V8H15M19 21H5C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3H16L21 8V19C21 19.5304 20.7893 20.0391 20.4142 20.4142C20.0391 20.7893 19.5304 21 19 21Z\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></path></svg> Save Form <!----></button><!----></div></div></div>
<head><title>
::
</title><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB