Skip to content

Commit be481c3

Browse files
committed
feat: flow got one-click expression and statement helpers and various minor editor improvements
1 parent 80f3869 commit be481c3

9 files changed

Lines changed: 335 additions & 7 deletions

application/flow/evt_form_created.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"go.wdy.de/nago/application/evs"
1414
"go.wdy.de/nago/pkg/xerrors"
15+
"go.wdy.de/nago/presentation/ui"
1516
)
1617

1718
type FormID string
@@ -42,6 +43,8 @@ func (evt FormCreated) Evolve(ctx context.Context, ws *Workspace) error {
4243
}
4344

4445
vstack := NewFormVStack(ViewID(evt.ID + "-root"))
46+
vstack.SetFrame(ui.Frame{}.Larger())
47+
4548
form := NewForm(evt.ID, evt.Name, vstack, evt.Package, evt.Struct)
4649

4750
ws.Forms.AddForm(form)

application/flow/ui/render_context.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ func (ctx RContext) InspectorMode() bool {
4141
return ctx.inspectorMode
4242
}
4343

44+
func (ctx RContext) InheritRef() RContext {
45+
ctx.insertMode = false
46+
ctx.inspectorMode = false
47+
return ctx
48+
}
49+
4450
func (ctx RContext) Workspace() *flow.Workspace {
4551
return ctx.ws
4652
}
@@ -98,6 +104,15 @@ func (ctx RContext) EditorAction(view flow.FormView) func() {
98104
for _, c := range ctx.selectedStates {
99105
c.Set(false)
100106
}
107+
108+
// do not toggle if near-preview mode, that feels strange
109+
if !ctx.inspectorMode {
110+
state.Set(true)
111+
ctx.parent.selected.Set(view.Identity())
112+
return
113+
}
114+
115+
// toggle mode for inspector
101116
state.Set(!wasSelected)
102117
if state.Get() {
103118
ctx.parent.selected.Set(view.Identity())

application/flow/ui/render_ctx_view.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,12 @@ func (c ViewerRenderContext) runExpr(estr flow.Expression) (any, error) {
7474
return c.state.Get().GetOr(name, jsonptr.Null{})
7575
},
7676
"has": func(name string) bool {
77-
_, ok := c.state.Get().Get(name)
77+
v, ok := c.state.Get().Get(name)
78+
var zero jsonptr.Null
79+
if ok && v == zero {
80+
return false
81+
}
82+
7883
return ok
7984
},
8085
"put": func(name string, value any) bool {

application/flow/ui/renderer_formref.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ func (r *FormRefRenderer) TeaserPreview(ctx RContext) core.View {
9595
}
9696

9797
func (r *FormRefRenderer) Preview(ctx RContext, view flow.FormView) core.View {
98+
ctx = ctx.InheritRef()
9899
ref := view.(*flow.FormRef)
99100
refForm, ok := ctx.Workspace().Forms.ByID(ref.Ref())
100101
if !ok {
@@ -107,8 +108,13 @@ func (r *FormRefRenderer) Preview(ctx RContext, view flow.FormView) core.View {
107108
ctx.RenderPreview(refForm.Root),
108109
ui.VStack(
109110
ui.Text(refForm.String()).Color(ui.ColorWhite),
110-
).BackgroundColor("#00000055").Position(ui.Position{Type: ui.PositionAbsolute, Bottom: "0rem", Left: "0rem", Right: "0rem", Top: "0rem"}),
111-
).Action(ctx.EditorAction(view)).Position(ui.Position{Type: ui.PositionRelative}).FullWidth()
111+
).BackgroundColor("#00000055").
112+
Position(ui.Position{Type: ui.PositionAbsolute, Bottom: "0rem", Left: "0rem", Right: "0rem", Top: "0rem"}).
113+
Border(ui.Border{}.Radius(ui.L8)),
114+
).Action(ctx.EditorAction(view)).
115+
Position(ui.Position{Type: ui.PositionRelative}).
116+
FullWidth().
117+
Padding(ui.Padding{}.All(ui.L8))
112118
}
113119

114120
func (r *FormRefRenderer) Bind(ctx ViewerRenderContext, view flow.FormView, state *core.State[*jsonptr.Obj]) core.View {
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright (c) 2025 worldiety GmbH
2+
//
3+
// This file is part of the NAGO Low-Code Platform.
4+
// Licensed under the terms specified in the LICENSE file.
5+
//
6+
// SPDX-License-Identifier: Custom-License
7+
8+
package uiflow
9+
10+
import (
11+
"fmt"
12+
"strings"
13+
14+
"go.wdy.de/nago/application/flow"
15+
"go.wdy.de/nago/presentation/core"
16+
"go.wdy.de/nago/presentation/ui"
17+
)
18+
19+
// magic field values
20+
const (
21+
mfVEmpty = "[empty]"
22+
mfVNotEmpty = "[not empty]"
23+
mfVTrue = "true"
24+
mfVFalse = "false"
25+
mfVNull = "[null]"
26+
mfVHas = "[has]"
27+
mfVDelete = "[delete]"
28+
)
29+
30+
func (c TFormEditor) viewHelpFieldsExpr(structType *flow.StructType, state *core.State[string]) core.View {
31+
var views []core.View
32+
views = append(views, ui.Text("Field helper templates: click a value to append it as expression"))
33+
for field := range structType.Fields.All() {
34+
headingName := field.JSONName()
35+
if headingName != string(field.Name()) {
36+
headingName += " (" + string(field.Name()) + ")"
37+
}
38+
39+
views = append(views, ui.VStack(
40+
ui.Heading(5, headingName),
41+
c.possibleFieldValuesExpr(field, state),
42+
).Alignment(ui.Leading))
43+
}
44+
45+
return ui.VStack(views...).Alignment(ui.Leading).FullWidth().Gap(ui.L8)
46+
}
47+
48+
func (c TFormEditor) possibleFieldValuesExpr(field flow.Field, state *core.State[string]) core.View {
49+
var views []core.View
50+
switch field := field.(type) {
51+
case *flow.StringField:
52+
views = append(views, pillTextExpr(state, field, mfVEmpty), pillTextExpr(state, field, mfVNotEmpty), pillTextExpr(state, field, mfVHas), pillTextExpr(state, field, `any text`))
53+
case *flow.BoolField:
54+
views = append(views, pillTextExpr(state, field, mfVTrue), pillTextExpr(state, field, mfVFalse), pillTextExpr(state, field, mfVHas))
55+
case *flow.TypeField:
56+
baseType, ok := c.ws.Packages.TypeByID(field.Type)
57+
if !ok {
58+
views = append(views, pillTextExpr(state, field, "unknown type"))
59+
} else {
60+
if str, ok := baseType.(*flow.StringType); ok {
61+
views = append(views, pillTextExpr(state, field, mfVEmpty), pillTextExpr(state, field, mfVNotEmpty), pillTextExpr(state, field, mfVHas))
62+
if str.Enumeration.Len() == 0 {
63+
views = append(views, pillTextExpr(state, field, `any text`))
64+
}
65+
66+
for literal := range str.Enumeration.All() {
67+
views = append(views, pillTextExpr(state, field, literal.Value))
68+
}
69+
}
70+
}
71+
}
72+
return ui.HStack(views...).Gap(ui.L8).Wrap(true)
73+
}
74+
75+
func pillTextExpr(state *core.State[string], field flow.Field, s string) core.View {
76+
return ui.HStack(ui.Text(s)).
77+
BackgroundColor(ui.ColorCardFooter).
78+
Action(func() {
79+
fname := field.JSONName()
80+
var expr string
81+
switch s {
82+
case mfVEmpty:
83+
expr = fmt.Sprintf(`field("%s").String() == ""`, fname)
84+
case mfVNotEmpty:
85+
expr = fmt.Sprintf(`field("%s").String() != ""`, fname)
86+
case mfVTrue:
87+
expr = fmt.Sprintf(`field("%s").Bool() == true`, fname)
88+
case mfVFalse:
89+
expr = fmt.Sprintf(`field("%s").Bool() == false`, fname)
90+
case mfVHas:
91+
expr = fmt.Sprintf(`has("%s")`, fname)
92+
default:
93+
expr = fmt.Sprintf(`field("%s").String() == "%s"`, fname, s)
94+
}
95+
96+
state.Set(strings.TrimSpace(state.Get()))
97+
if state.Get() != "" {
98+
expr = " && " + expr
99+
}
100+
101+
state.Set(state.Get() + expr)
102+
}).
103+
Padding(ui.Padding{}.All(ui.L4).Horizontal(ui.L8)).
104+
Border(ui.Border{}.Radius(ui.L8))
105+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright (c) 2025 worldiety GmbH
2+
//
3+
// This file is part of the NAGO Low-Code Platform.
4+
// Licensed under the terms specified in the LICENSE file.
5+
//
6+
// SPDX-License-Identifier: Custom-License
7+
8+
package uiflow
9+
10+
import (
11+
"fmt"
12+
"strings"
13+
14+
"go.wdy.de/nago/application/flow"
15+
"go.wdy.de/nago/presentation/core"
16+
"go.wdy.de/nago/presentation/ui"
17+
)
18+
19+
func (c TFormEditor) viewHelpFieldsStmt(structType *flow.StructType, state *core.State[string]) core.View {
20+
var views []core.View
21+
views = append(views, ui.Text("Field helper templates: click a value to append it as statement"))
22+
for field := range structType.Fields.All() {
23+
headingName := field.JSONName()
24+
if headingName != string(field.Name()) {
25+
headingName += " (" + string(field.Name()) + ")"
26+
}
27+
28+
views = append(views, ui.VStack(
29+
ui.Heading(5, headingName),
30+
c.possibleFieldValuesPut(field, state),
31+
).Alignment(ui.Leading))
32+
}
33+
34+
return ui.VStack(views...).Alignment(ui.Leading).FullWidth().Gap(ui.L8)
35+
}
36+
37+
func (c TFormEditor) possibleFieldValuesPut(field flow.Field, state *core.State[string]) core.View {
38+
var views []core.View
39+
switch field := field.(type) {
40+
case *flow.StringField:
41+
views = append(views, pillTextPutStmt(state, field, mfVEmpty), pillTextPutStmt(state, field, mfVDelete), pillTextPutStmt(state, field, `any text`))
42+
case *flow.BoolField:
43+
views = append(views, pillTextPutStmt(state, field, mfVTrue), pillTextPutStmt(state, field, mfVFalse), pillTextPutStmt(state, field, mfVDelete))
44+
case *flow.TypeField:
45+
baseType, ok := c.ws.Packages.TypeByID(field.Type)
46+
if !ok {
47+
views = append(views, pillTextPutStmt(state, field, "unknown type"))
48+
} else {
49+
if str, ok := baseType.(*flow.StringType); ok {
50+
views = append(views, pillTextPutStmt(state, field, mfVEmpty), pillTextPutStmt(state, field, mfVDelete))
51+
if str.Enumeration.Len() == 0 {
52+
views = append(views, pillTextPutStmt(state, field, `any text`))
53+
}
54+
55+
for literal := range str.Enumeration.All() {
56+
views = append(views, pillTextPutStmt(state, field, literal.Value))
57+
}
58+
}
59+
}
60+
}
61+
return ui.HStack(views...).Gap(ui.L8).Wrap(true)
62+
}
63+
64+
func pillTextPutStmt(state *core.State[string], field flow.Field, s string) core.View {
65+
return ui.HStack(ui.Text(s)).
66+
BackgroundColor(ui.ColorCardFooter).
67+
Action(func() {
68+
fname := field.JSONName()
69+
var expr string
70+
switch s {
71+
case mfVEmpty:
72+
expr = fmt.Sprintf(`put("%s","")`, fname)
73+
case mfVTrue:
74+
expr = fmt.Sprintf(`put("%s",true)`, fname)
75+
case mfVFalse:
76+
expr = fmt.Sprintf(`put("%s",false)`, fname)
77+
case mfVDelete:
78+
expr = fmt.Sprintf(`delete("%s")`, fname)
79+
default:
80+
expr = fmt.Sprintf(`put("%s","%s")`, fname, s)
81+
}
82+
83+
state.Set(strings.TrimSpace(state.Get()))
84+
if state.Get() != "" {
85+
expr = "\n" + expr
86+
}
87+
88+
state.Set(state.Get() + expr)
89+
}).
90+
Padding(ui.Padding{}.All(ui.L4).Horizontal(ui.L8)).
91+
Border(ui.Border{}.Radius(ui.L8))
92+
}

application/flow/ui/view_form_edit.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ func (c TFormEditor) renderSelectedViewEditor(ctx RContext) core.View {
286286
editBorderable(ctx, view),
287287
editFrameable(ctx, view),
288288
editPaddable(ctx, view),
289+
editCard(ctx, view),
289290

290291
ui.HLine(),
291292
ui.Text("Scripts"),
@@ -341,7 +342,7 @@ func (c TFormEditor) Render(ctx core.RenderContext) core.RenderNode {
341342
Frame(ui.Frame{Width: ui.L400, MaxWidth: ui.L400}).
342343
Padding(ui.Padding{}.All(ui.L8)).
343344
Border(ui.Border{}.Radius(ui.L16).Shadow(ui.L8)),
344-
).Render(ctx)
345+
).FullWidth().Render(ctx)
345346
}
346347

347348
rctx := c.newRenderContext(ctx.Window())
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright (c) 2025 worldiety GmbH
2+
//
3+
// This file is part of the NAGO Low-Code Platform.
4+
// Licensed under the terms specified in the LICENSE file.
5+
//
6+
// SPDX-License-Identifier: Custom-License
7+
8+
package uiflow
9+
10+
import (
11+
"go.wdy.de/nago/application/flow"
12+
"go.wdy.de/nago/presentation/core"
13+
"go.wdy.de/nago/presentation/ui"
14+
"go.wdy.de/nago/presentation/ui/accordion"
15+
"go.wdy.de/nago/presentation/ui/alert"
16+
)
17+
18+
func editCard(ctx RContext, view flow.FormView) core.View {
19+
paddable, _ := view.(flow.Paddable)
20+
if paddable == nil {
21+
return nil
22+
}
23+
borderable, _ := view.(flow.Borderable)
24+
if borderable == nil {
25+
return nil
26+
}
27+
28+
backgroundable, _ := view.(flow.Backgroundable)
29+
if backgroundable == nil {
30+
return nil
31+
}
32+
33+
state := core.AutoState[bool](ctx.Window())
34+
toggle := core.StateOf[bool](ctx.wnd, "toggle-card")
35+
apply := func(bgColor ui.Color, border ui.Border, padding ui.Padding) {
36+
if err := ctx.HandleCommand(flow.UpdateFormBorder{
37+
Workspace: ctx.Workspace().ID,
38+
Form: ctx.Form().ID,
39+
ID: view.Identity(),
40+
Border: border,
41+
}); err != nil {
42+
alert.ShowBannerError(ctx.Window(), err)
43+
return
44+
}
45+
46+
if err := ctx.HandleCommand(flow.UpdateFormPadding{
47+
Workspace: ctx.Workspace().ID,
48+
Form: ctx.Form().ID,
49+
ID: view.Identity(),
50+
Padding: padding,
51+
}); err != nil {
52+
alert.ShowBannerError(ctx.Window(), err)
53+
return
54+
}
55+
56+
if err := ctx.HandleCommand(flow.UpdateFormBackgroundColorCmd{
57+
Workspace: ctx.Workspace().ID,
58+
Form: ctx.Form().ID,
59+
ID: view.Identity(),
60+
Color: bgColor,
61+
}); err != nil {
62+
alert.ShowBannerError(ctx.Window(), err)
63+
return
64+
}
65+
66+
state.Invalidate()
67+
}
68+
69+
return accordion.Accordion(
70+
ui.Text("Card Style Presets"),
71+
ui.VStack(
72+
ui.SecondaryButton(func() {
73+
apply(
74+
ui.M4,
75+
ui.Border{}.Radius(ui.L8),
76+
ui.Padding{}.All(ui.L16),
77+
)
78+
}).Title("Default card").FullWidth(),
79+
80+
ui.SecondaryButton(func() {
81+
apply(
82+
"",
83+
ui.Border{},
84+
ui.Padding{},
85+
)
86+
}).Title("Clear").FullWidth(),
87+
).FullWidth().Alignment(ui.Leading).Gap(ui.L8),
88+
toggle,
89+
).FullWidth()
90+
91+
}

0 commit comments

Comments
 (0)