ballerina-core 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/main.ts +3 -0
- package/package.json +1 -1
- package/readme.md +122 -0
- package/src/forms/domains/parser/domains/merger/state.ts +9 -0
- package/src/forms/domains/parser/domains/validator/state.ts +18 -5
- package/src/forms/domains/parser/state.tsx +95 -62
- package/src/forms/domains/primitives/domains/boolean/state.ts +7 -0
- package/src/forms/domains/primitives/domains/boolean/template.tsx +18 -1
- package/src/forms/domains/singleton/domains/mapping/state.ts +113 -0
- package/src/forms/domains/singleton/domains/mapping/template.tsx +30 -0
- package/src/forms/domains/singleton/template.tsx +1 -1
package/main.ts
CHANGED
|
@@ -87,8 +87,11 @@ export * from "./src/forms/domains/primitives/domains/searchable-infinite-stream
|
|
|
87
87
|
export * from "./src/forms/domains/primitives/domains/searchable-infinite-stream/template"
|
|
88
88
|
export * from "./src/forms/domains/primitives/domains/searchable-infinite-stream-multiselect/state"
|
|
89
89
|
export * from "./src/forms/domains/primitives/domains/searchable-infinite-stream-multiselect/template"
|
|
90
|
+
export * from "./src/forms/domains/singleton/domains/mapping/state"
|
|
91
|
+
export * from "./src/forms/domains/singleton/domains/mapping/template"
|
|
90
92
|
export * from "./src/forms/domains/parser/state"
|
|
91
93
|
export * from "./src/forms/domains/parser/domains/validator/state"
|
|
94
|
+
export * from "./src/forms/domains/parser/domains/merger/state"
|
|
92
95
|
export * from "./src/forms/domains/launcher/domains/edit/state"
|
|
93
96
|
export * from "./src/forms/domains/launcher/domains/edit/template"
|
|
94
97
|
export * from "./src/forms/domains/launcher/domains/edit/coroutines/runner"
|
package/package.json
CHANGED
package/readme.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Ballerina 🩰
|
|
2
|
+
|
|
3
|
+
Welcome to _ballerina_, the effortlessly elegant functional programming framework for frontend web development, with a particular but non-exclusive preference for React.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## Quick start
|
|
7
|
+
Everything in Ballerina 🩰 is based on the separation of code into units called _domains_.
|
|
8
|
+
|
|
9
|
+
Create a Typescript/React project however you want (I like to use rspack but you can use whatever you prefer).
|
|
10
|
+
|
|
11
|
+
Head over to the sources, and create all the necessary files and directories for a new domain (this is just a best practice and it avoids merge conflicts in the long run):
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
mkdir helloWorld
|
|
15
|
+
cd helloWorld
|
|
16
|
+
touch state.ts
|
|
17
|
+
touch template.tsx
|
|
18
|
+
mkdir coroutines
|
|
19
|
+
touch coroutines/runner.ts
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Let's define the state of our domain in `state.ts`:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { simpleUpdater } from "ballerina-core"
|
|
26
|
+
|
|
27
|
+
export type HelloWorldContext = { greeting:string }
|
|
28
|
+
export type HelloWorldState = { counter:number, toggle:boolean }
|
|
29
|
+
export const HelloWorldState = {
|
|
30
|
+
Default:() : HelloWorldState => ({
|
|
31
|
+
counter:0,
|
|
32
|
+
toggle:false
|
|
33
|
+
}),
|
|
34
|
+
Updaters:{
|
|
35
|
+
...simpleUpdater<HelloWorldState>()("counter"),
|
|
36
|
+
...simpleUpdater<HelloWorldState>()("toggle")
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Let's define a simple automation in `coroutine/runner.ts`:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { CoTypedFactory, replaceWith, Unit } from "ballerina-core"
|
|
45
|
+
import { HelloWorldContext, HelloWorldState } from "../state"
|
|
46
|
+
import { Range } from "immutable"
|
|
47
|
+
|
|
48
|
+
const Co = CoTypedFactory<HelloWorldContext, HelloWorldState>()
|
|
49
|
+
export const helloWorldRunner =
|
|
50
|
+
Co.Template<Unit>(
|
|
51
|
+
Co.Repeat(
|
|
52
|
+
Co.Seq([
|
|
53
|
+
Co.SetState(
|
|
54
|
+
HelloWorldState.Updaters.counter(replaceWith(0))
|
|
55
|
+
),
|
|
56
|
+
Co.Wait(250),
|
|
57
|
+
Co.For(Range(0, 3))(
|
|
58
|
+
i => Co.Seq([
|
|
59
|
+
Co.SetState(
|
|
60
|
+
HelloWorldState.Updaters.counter(_ => _ + 1)
|
|
61
|
+
),
|
|
62
|
+
Co.Wait(250)
|
|
63
|
+
])
|
|
64
|
+
)
|
|
65
|
+
])
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Let's put something ugly on the screen in `template.tsx`:
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
import { Template, Unit } from "ballerina-core";
|
|
74
|
+
import { HelloWorldContext, HelloWorldState } from "./state";
|
|
75
|
+
import { helloWorldRunner } from "./coroutines/runner";
|
|
76
|
+
|
|
77
|
+
export const HelloWorldTemplate =
|
|
78
|
+
Template.Default<HelloWorldContext & HelloWorldState, HelloWorldState, Unit>(props =>
|
|
79
|
+
<>
|
|
80
|
+
<p>{props.context.greeting}</p>
|
|
81
|
+
<p>Counter: {props.context.counter}</p>
|
|
82
|
+
<button
|
|
83
|
+
onClick={() => props.setState(HelloWorldState.Updaters.toggle(_ => !_))}>
|
|
84
|
+
Toggle {props.context.toggle ? "off" : "on"}
|
|
85
|
+
</button>
|
|
86
|
+
</>
|
|
87
|
+
).any([
|
|
88
|
+
helloWorldRunner
|
|
89
|
+
])
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Finally, let's render this as a top-level stateful domain from the React entry point (which file depends on the template you used):
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
import { useState } from "react";
|
|
96
|
+
import "./App.css";
|
|
97
|
+
import { HelloWorldState } from "./domains/helloWorld/state";
|
|
98
|
+
import { HelloWorldTemplate } from "./domains/helloWorld/template";
|
|
99
|
+
import { unit } from "ballerina-core";
|
|
100
|
+
|
|
101
|
+
export const App = (props: {}) => {
|
|
102
|
+
const [helloWorld, setHelloWorld] = useState(HelloWorldState.Default())
|
|
103
|
+
return <>
|
|
104
|
+
<HelloWorldTemplate
|
|
105
|
+
context={{
|
|
106
|
+
greeting:"Hello!",
|
|
107
|
+
...helloWorld
|
|
108
|
+
}}
|
|
109
|
+
setState={setHelloWorld}
|
|
110
|
+
foreignMutations={unit}
|
|
111
|
+
view={unit}
|
|
112
|
+
/>
|
|
113
|
+
</>
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Head over to the page and you will see an animated `counter` value that changes on its own as well as a button you can interact with.
|
|
118
|
+
|
|
119
|
+
We have barely scratched the surface of all you can do with Ballerina 🩰 though.
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
If you want to know more, head over to [the official git repo](https://github.com/giuseppemag/ballerina) and check out the samples and the official documentation.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Set, Map, OrderedMap } from "immutable";
|
|
2
|
-
import { BoolExpr, Sum } from "../../../../../../main";
|
|
2
|
+
import { BoolExpr, FormsConfigMerger, Sum } from "../../../../../../main";
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
export type FieldName = string;
|
|
@@ -12,7 +12,7 @@ export type TypeDefinition = {
|
|
|
12
12
|
export type Type = {
|
|
13
13
|
kind: "lookup"; name: TypeName;
|
|
14
14
|
} | {
|
|
15
|
-
kind: "primitive"; value: "string" | "number" | "boolean" | "Date" | "CollectionReference";
|
|
15
|
+
kind: "primitive"; value: "string" | "number" | "maybeBoolean" | "boolean" | "Date" | "CollectionReference";
|
|
16
16
|
} | {
|
|
17
17
|
kind: "application"; value: TypeName; args: Array<TypeName>;
|
|
18
18
|
};
|
|
@@ -65,6 +65,7 @@ export type BuiltIns = {
|
|
|
65
65
|
generics: Set<string>;
|
|
66
66
|
renderers: {
|
|
67
67
|
BooleanViews: Set<string>;
|
|
68
|
+
MaybeBooleanViews: Set<string>;
|
|
68
69
|
NumberViews: Set<string>;
|
|
69
70
|
StringViews: Set<string>;
|
|
70
71
|
DateViews: Set<string>;
|
|
@@ -80,6 +81,13 @@ export const FormsConfig = {
|
|
|
80
81
|
Default: {
|
|
81
82
|
validateAndParseAPIResponse: (builtIns: BuiltIns) => (formsConfig: any): FormValidationResult => {
|
|
82
83
|
let errors: Array<FormValidationError> = [];
|
|
84
|
+
if (Array.isArray(formsConfig)) {
|
|
85
|
+
alert("formsConfig is an array!")
|
|
86
|
+
const merged = FormsConfigMerger.Default.merge(formsConfig, errors)
|
|
87
|
+
formsConfig = merged[0]
|
|
88
|
+
errors = merged[0]
|
|
89
|
+
}
|
|
90
|
+
|
|
83
91
|
let types: Map<TypeName, TypeDefinition> = Map();
|
|
84
92
|
if ("types" in formsConfig == false) {
|
|
85
93
|
errors.push("the formsConfig does not contain a 'types' field");
|
|
@@ -133,9 +141,9 @@ export const FormsConfig = {
|
|
|
133
141
|
if (fieldDef.kind == "primitive" && !builtIns.primitives.includes(fieldDef.value))
|
|
134
142
|
errors.push(`field ${fieldName} of type ${typeName} is non-existent primitive type ${fieldDef.value}`);
|
|
135
143
|
if (fieldDef.kind == "lookup" && !types.has(fieldDef.name))
|
|
136
|
-
errors.push(`field ${fieldName} of type ${typeName} is non-existent
|
|
144
|
+
errors.push(`field ${fieldName} of type ${typeName} is non-existent type ${fieldDef.name}`);
|
|
137
145
|
if (fieldDef.kind == "application" && !builtIns.generics.includes(fieldDef.value))
|
|
138
|
-
errors.push(`field ${fieldName} of type ${typeName} applies non-existent generic
|
|
146
|
+
errors.push(`field ${fieldName} of type ${typeName} applies non-existent generic type ${fieldDef.value}`);
|
|
139
147
|
if (fieldDef.kind == "application" && fieldDef.args.some(argType => !builtIns.primitives.includes(argType) && !types.has(argType)))
|
|
140
148
|
errors.push(`field ${fieldName} of type ${typeName} applies non-existent type arguments ${JSON.stringify(fieldDef.args.filter(argType => !builtIns.primitives.has(argType) && !types.has(argType)))}`);
|
|
141
149
|
if (fieldDef.kind == "application" && fieldDef.value == "SingleSelection") {
|
|
@@ -240,7 +248,12 @@ export const FormsConfig = {
|
|
|
240
248
|
}
|
|
241
249
|
const fieldTypeDef = formTypeDef?.fields.get(fieldName)
|
|
242
250
|
if (fieldTypeDef?.kind == "primitive") {
|
|
243
|
-
if (fieldTypeDef.value == "
|
|
251
|
+
if (fieldTypeDef.value == "maybeBoolean") {
|
|
252
|
+
// alert(JSON.stringify(fieldConfig["renderer"]))
|
|
253
|
+
// alert(JSON.stringify(builtIns.renderers.MaybeBooleanViews))
|
|
254
|
+
if (!builtIns.renderers.MaybeBooleanViews.has(fieldConfig["renderer"]))
|
|
255
|
+
errors.push(`field ${fieldName} of form ${formName} references non-existing ${fieldTypeDef.value} 'renderer' ${fieldConfig["renderer"]}`);
|
|
256
|
+
} else if (fieldTypeDef.value == "boolean") {
|
|
244
257
|
if (!builtIns.renderers.BooleanViews.has(fieldConfig["renderer"]))
|
|
245
258
|
errors.push(`field ${fieldName} of form ${formName} references non-existing ${fieldTypeDef.value} 'renderer' ${fieldConfig["renderer"]}`);
|
|
246
259
|
} else if (fieldTypeDef.value == "number") {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Collection, Map, OrderedMap, OrderedSet, Set } from "immutable";
|
|
2
|
-
import { BoolExpr, Unit, PromiseRepo, Guid, LeafPredicatesEvaluators, Predicate, FormsConfig, BuiltIns, FormDef, Sum, BasicFun, Template, unit, EditFormState, EditFormTemplate, ApiErrors, CreateFormTemplate, EntityFormTemplate, SharedFormState } from "../../../../main";
|
|
2
|
+
import { BoolExpr, Unit, PromiseRepo, Guid, LeafPredicatesEvaluators, Predicate, FormsConfig, BuiltIns, FormDef, Sum, BasicFun, Template, unit, EditFormState, EditFormTemplate, ApiErrors, CreateFormTemplate, EntityFormTemplate, SharedFormState, CreateFormState, Entity, EditFormContext, CreateFormContext } from "../../../../main";
|
|
3
3
|
import { Value } from "../../../value/state";
|
|
4
4
|
import { CollectionReference } from "../collection/domains/reference/state";
|
|
5
5
|
import { CollectionSelection } from "../collection/domains/selection/state";
|
|
6
|
-
import { BooleanForm } from "../primitives/domains/boolean/template";
|
|
6
|
+
import { BooleanForm, MaybeBooleanForm } from "../primitives/domains/boolean/template";
|
|
7
7
|
import { DateFormState } from "../primitives/domains/date/state";
|
|
8
8
|
import { DateForm } from "../primitives/domains/date/template";
|
|
9
9
|
import { EnumMultiselectForm } from "../primitives/domains/enum-multiselect/template";
|
|
@@ -25,47 +25,51 @@ const parseOptions = (leafPredicates: any, options: any) => {
|
|
|
25
25
|
|
|
26
26
|
export const FieldView = //<Context, FieldViews extends DefaultFieldViews, EnumFieldConfigs extends {}, EnumSources extends {}>() => <ViewType extends keyof FieldViews, ViewName extends keyof FieldViews[ViewType]>
|
|
27
27
|
(fieldViews: any, viewType: any, viewName: any, fieldName: string, enumFieldConfigs: any, enumSources: any, leafPredicates: any): any => // FieldView<Context, FieldViews, ViewType, ViewName> =>
|
|
28
|
-
viewType == "
|
|
29
|
-
|
|
28
|
+
viewType == "MaybeBooleanViews" ?
|
|
29
|
+
MaybeBooleanForm<any & FormLabel, Unit>(_ => PromiseRepo.Default.mock(() => []))
|
|
30
30
|
.withView(((fieldViews as any)[viewType] as any)[viewName]() as any)
|
|
31
31
|
.mapContext<any & SharedFormState & Value<boolean>>(_ => ({ ..._, label: fieldName })) as any
|
|
32
|
-
: viewType == "
|
|
33
|
-
|
|
32
|
+
: viewType == "BooleanViews" ?
|
|
33
|
+
BooleanForm<any & FormLabel, Unit>(_ => PromiseRepo.Default.mock(() => []))
|
|
34
34
|
.withView(((fieldViews as any)[viewType] as any)[viewName]() as any)
|
|
35
|
-
.mapContext<any &
|
|
36
|
-
: viewType == "
|
|
37
|
-
|
|
35
|
+
.mapContext<any & SharedFormState & Value<boolean>>(_ => ({ ..._, label: fieldName })) as any
|
|
36
|
+
: viewType == "DateViews" ?
|
|
37
|
+
DateForm<any & FormLabel, Unit>(_ => PromiseRepo.Default.mock(() => []))
|
|
38
38
|
.withView(((fieldViews as any)[viewType] as any)[viewName]() as any)
|
|
39
|
-
.mapContext<any &
|
|
40
|
-
: viewType == "
|
|
41
|
-
|
|
39
|
+
.mapContext<any & DateFormState & Value<Date>>(_ => ({ ..._, label: fieldName })) as any
|
|
40
|
+
: viewType == "NumberViews" ?
|
|
41
|
+
NumberForm<any & FormLabel, Unit>(_ => PromiseRepo.Default.mock(() => []))
|
|
42
42
|
.withView(((fieldViews as any)[viewType] as any)[viewName]() as any)
|
|
43
|
-
.mapContext<any & SharedFormState & Value<
|
|
44
|
-
: viewType == "
|
|
45
|
-
|
|
43
|
+
.mapContext<any & SharedFormState & Value<number>>(_ => ({ ..._, label: fieldName })) as any
|
|
44
|
+
: viewType == "StringViews" ?
|
|
45
|
+
StringForm<any & FormLabel, Unit>(_ => PromiseRepo.Default.mock(() => []))
|
|
46
46
|
.withView(((fieldViews as any)[viewType] as any)[viewName]() as any)
|
|
47
|
-
.mapContext<any &
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
: viewType == "EnumMultiselectViews" ?
|
|
51
|
-
EnumMultiselectForm<any & FormLabel & BaseEnumContext<any, CollectionReference>, Unit, CollectionReference>(_ => PromiseRepo.Default.mock(() => []))
|
|
47
|
+
.mapContext<any & SharedFormState & Value<string>>(_ => ({ ..._, label: fieldName })) as any
|
|
48
|
+
: viewType == "EnumViews" ?
|
|
49
|
+
EnumForm<any & FormLabel & BaseEnumContext<any, CollectionReference>, Unit, CollectionReference>(_ => PromiseRepo.Default.mock(() => []))
|
|
52
50
|
.withView(((fieldViews as any)[viewType] as any)[viewName]() as any)
|
|
53
|
-
.mapContext<any & EnumFormState<any & BaseEnumContext<any, CollectionReference>, CollectionReference> & Value<
|
|
51
|
+
.mapContext<any & EnumFormState<any & BaseEnumContext<any, CollectionReference>, CollectionReference> & Value<CollectionSelection<CollectionReference>>>(_ => ({
|
|
54
52
|
..._, label: fieldName, getOptions: () => ((enumFieldConfigs as any)((enumSources as any)[fieldName]) as Promise<any>).then(options => parseOptions(leafPredicates, options))
|
|
55
53
|
})) as any
|
|
56
|
-
: viewType == "
|
|
57
|
-
|
|
54
|
+
: viewType == "EnumMultiselectViews" ?
|
|
55
|
+
EnumMultiselectForm<any & FormLabel & BaseEnumContext<any, CollectionReference>, Unit, CollectionReference>(_ => PromiseRepo.Default.mock(() => []))
|
|
58
56
|
.withView(((fieldViews as any)[viewType] as any)[viewName]() as any)
|
|
59
|
-
.mapContext<any &
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
.mapContext<any & EnumFormState<any & BaseEnumContext<any, CollectionReference>, CollectionReference> & Value<OrderedMap<Guid, CollectionReference>>>(_ => ({
|
|
58
|
+
..._, label: fieldName, getOptions: () => ((enumFieldConfigs as any)((enumSources as any)[fieldName]) as Promise<any>).then(options => parseOptions(leafPredicates, options))
|
|
59
|
+
})) as any
|
|
60
|
+
: viewType == "InfiniteStreamViews" ?
|
|
61
|
+
SearchableInfiniteStreamForm<CollectionReference, any & FormLabel, Unit>(_ => PromiseRepo.Default.mock(() => []))
|
|
62
62
|
.withView(((fieldViews as any)[viewType] as any)[viewName]() as any)
|
|
63
|
-
.mapContext<any &
|
|
64
|
-
:
|
|
63
|
+
.mapContext<any & SearchableInfiniteStreamState<CollectionReference> & Value<CollectionSelection<CollectionReference>>>(_ => ({ ..._, label: fieldName })) as any
|
|
64
|
+
: viewType == "InfiniteStreamMultiselectViews" ?
|
|
65
|
+
InfiniteMultiselectDropdownForm<CollectionReference, any & FormLabel, Unit>(_ => PromiseRepo.Default.mock(() => []))
|
|
66
|
+
.withView(((fieldViews as any)[viewType] as any)[viewName]() as any)
|
|
67
|
+
.mapContext<any & FormLabel & SharedFormState & SearchableInfiniteStreamState<CollectionReference> & Value<OrderedMap<Guid, CollectionReference>>>(_ => ({ ..._, label: fieldName })) as any
|
|
68
|
+
: `error: the view for ${viewType as string}::${viewName as string} cannot be found`;
|
|
65
69
|
|
|
66
70
|
export const FieldFormState = //<Context, FieldViews extends DefaultFieldViews, InfiniteStreamSources extends {}, InfiniteStreamConfigs extends {}>() => <ViewType extends keyof FieldViews, ViewName extends keyof FieldViews[ViewType]>
|
|
67
71
|
(fieldViews: any, viewType: any, viewName: any, fieldName: string, InfiniteStreamSources: any, infiniteStreamConfigs: any): any => {
|
|
68
|
-
if (viewType == "BooleanViews" || viewType == "NumberViews" || viewType == "StringViews")
|
|
72
|
+
if (viewType == "MaybeBooleanViews" || viewType == "BooleanViews" || viewType == "NumberViews" || viewType == "StringViews")
|
|
69
73
|
return SharedFormState.Default();
|
|
70
74
|
if (viewType == "DateViews")
|
|
71
75
|
return DateFormState.Default("");
|
|
@@ -116,7 +120,6 @@ export const ParseForm = (
|
|
|
116
120
|
otherForms.get(viewName)?.initialFormState ??
|
|
117
121
|
FieldFormState(fieldViews, fieldNameToViewCategory(fieldName) as any, (fieldsViewsConfig as any)[fieldName], fieldName, InfiniteStreamSources, fieldsInfiniteStreamsConfig);
|
|
118
122
|
if (typeof initialFormState[fieldName] == "string") {
|
|
119
|
-
formConfig[fieldName] = (props: any) => <>Error: field {fieldName} with {viewName} could not be instantiated</>
|
|
120
123
|
throw `cannot resolve initial state ${viewName} of field ${fieldName}`
|
|
121
124
|
}
|
|
122
125
|
});
|
|
@@ -151,10 +154,11 @@ export const parseVisibleFields = (
|
|
|
151
154
|
|
|
152
155
|
export const builtInsFromFieldViews = (fieldViews: any): BuiltIns => {
|
|
153
156
|
let builtins: BuiltIns = {
|
|
154
|
-
"primitives": Set(["string", "number", "boolean", "Date", "CollectionReference"]),
|
|
157
|
+
"primitives": Set(["string", "number", "boolean", "maybeBoolean", "Date", "CollectionReference"]),
|
|
155
158
|
"generics": Set(["SingleSelection", "Multiselection"]),
|
|
156
159
|
"renderers": {
|
|
157
160
|
"BooleanViews": Set(),
|
|
161
|
+
"MaybeBooleanViews": Set(),
|
|
158
162
|
"DateViews": Set(),
|
|
159
163
|
"EnumMultiselectViews": Set(),
|
|
160
164
|
"EnumViews": Set(),
|
|
@@ -175,9 +179,38 @@ export const builtInsFromFieldViews = (fieldViews: any): BuiltIns => {
|
|
|
175
179
|
return builtins
|
|
176
180
|
}
|
|
177
181
|
|
|
182
|
+
export type EditLauncherContext<Entity, FormState, ExtraContext> =
|
|
183
|
+
Omit<
|
|
184
|
+
EditFormContext<Entity, FormState> &
|
|
185
|
+
EditFormState<Entity, FormState> & {
|
|
186
|
+
extraContext: ExtraContext,
|
|
187
|
+
containerFormView: any
|
|
188
|
+
}, "api" | "actualForm">
|
|
189
|
+
|
|
190
|
+
export type CreateLauncherContext<Entity, FormState, ExtraContext> =
|
|
191
|
+
Omit<
|
|
192
|
+
CreateFormContext<Entity, FormState> &
|
|
193
|
+
CreateFormState<Entity, FormState> & {
|
|
194
|
+
extraContext: ExtraContext,
|
|
195
|
+
containerFormView: any
|
|
196
|
+
submitButtonWrapper: any
|
|
197
|
+
}, "api" | "actualForm">
|
|
198
|
+
|
|
178
199
|
export type ParsedLaunchers = {
|
|
179
|
-
create:Map<string,
|
|
180
|
-
|
|
200
|
+
create: Map<string, <Entity, FormState, ExtraContext, Context extends CreateLauncherContext<Entity, FormState, ExtraContext>>() =>
|
|
201
|
+
{
|
|
202
|
+
form:
|
|
203
|
+
Template<CreateLauncherContext<Entity, FormState, ExtraContext> & CreateFormState<Entity, FormState>,
|
|
204
|
+
CreateFormState<Entity, FormState>, Unit>,
|
|
205
|
+
initialState: CreateFormState<Entity, FormState>
|
|
206
|
+
}>,
|
|
207
|
+
edit: Map<string, <Entity, FormState, ExtraContext, Context extends EditLauncherContext<Entity, FormState, ExtraContext>>() =>
|
|
208
|
+
{
|
|
209
|
+
form:
|
|
210
|
+
Template<EditLauncherContext<Entity, FormState, ExtraContext> & EditFormState<Entity, FormState>,
|
|
211
|
+
EditFormState<Entity, FormState>, Unit>,
|
|
212
|
+
initialState: EditFormState<Entity, FormState>
|
|
213
|
+
}>,
|
|
181
214
|
}
|
|
182
215
|
export type ParsedForms = Map<string, ParsedForm & { form: EntityFormTemplate<any, any, any, any, any> }>
|
|
183
216
|
export type FormParsingErrors = Array<string>
|
|
@@ -186,10 +219,10 @@ export type StreamName = string
|
|
|
186
219
|
export type InfiniteStreamSources = BasicFun<StreamName, SearchableInfiniteStreamState<CollectionReference>["getChunk"]>
|
|
187
220
|
export type EntityName = string
|
|
188
221
|
export type EntityApis = {
|
|
189
|
-
create:BasicFun<EntityName, BasicFun<any, Promise<Unit>>>
|
|
190
|
-
default:BasicFun<EntityName, BasicFun<Unit,Promise<any>>>
|
|
191
|
-
update:BasicFun<EntityName, BasicFun<any,Promise<ApiErrors>>>
|
|
192
|
-
get:BasicFun<EntityName, BasicFun<Guid,Promise<any>>>
|
|
222
|
+
create: BasicFun<EntityName, BasicFun<any, Promise<Unit>>>
|
|
223
|
+
default: BasicFun<EntityName, BasicFun<Unit, Promise<any>>>
|
|
224
|
+
update: BasicFun<EntityName, BasicFun<any, Promise<ApiErrors>>>
|
|
225
|
+
get: BasicFun<EntityName, BasicFun<Guid, Promise<any>>>
|
|
193
226
|
}
|
|
194
227
|
export type EnumName = string
|
|
195
228
|
export type EnumOptionsSources = BasicFun<EnumName, Promise<Array<[CollectionReference, BoolExpr<Unit>]>>>
|
|
@@ -199,16 +232,16 @@ export const parseForms =
|
|
|
199
232
|
fieldViews: any,
|
|
200
233
|
infiniteStreamSources: InfiniteStreamSources,
|
|
201
234
|
enumOptionsSources: EnumOptionsSources,
|
|
202
|
-
entityApis:EntityApis,
|
|
235
|
+
entityApis: EntityApis,
|
|
203
236
|
leafPredicates: LeafPredicates) =>
|
|
204
237
|
(formsConfig: FormsConfig):
|
|
205
238
|
FormParsingResult => {
|
|
206
239
|
let errors: FormParsingErrors = []
|
|
207
240
|
let seen = Set<string>()
|
|
208
241
|
let formProcessingOrder = OrderedSet<string>()
|
|
209
|
-
let parsedLaunchers:ParsedLaunchers = {
|
|
210
|
-
create:Map(),
|
|
211
|
-
edit:Map(),
|
|
242
|
+
let parsedLaunchers: ParsedLaunchers = {
|
|
243
|
+
create: Map(),
|
|
244
|
+
edit: Map(),
|
|
212
245
|
}
|
|
213
246
|
let parsedForms: ParsedForms = Map()
|
|
214
247
|
const traverse = (formDef: FormDef) => {
|
|
@@ -268,16 +301,16 @@ export const parseForms =
|
|
|
268
301
|
update: entityApis.update(launcher.api)
|
|
269
302
|
}
|
|
270
303
|
parsedLaunchers.edit = parsedLaunchers.edit.set(
|
|
271
|
-
launcherName,
|
|
272
|
-
|
|
273
|
-
(
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
304
|
+
launcherName,
|
|
305
|
+
<Entity, FormState, ExtraContext, Context extends EditLauncherContext<Entity, FormState, ExtraContext>>() => ({
|
|
306
|
+
form: EditFormTemplate<Entity, FormState>().mapContext((parentContext: Context) =>
|
|
307
|
+
({
|
|
308
|
+
...parentContext,
|
|
309
|
+
api: api,
|
|
310
|
+
actualForm: form.withView(parentContext.containerFormView).mapContext((_: any) => ({ ..._, rootValue: _.value, ...parentContext.extraContext }))
|
|
311
|
+
}) as any),
|
|
312
|
+
initialState: EditFormState<Entity, FormState>().Default(initialState),
|
|
313
|
+
})
|
|
281
314
|
)
|
|
282
315
|
})
|
|
283
316
|
|
|
@@ -290,22 +323,22 @@ export const parseForms =
|
|
|
290
323
|
default: entityApis.default(launcher.api)
|
|
291
324
|
}
|
|
292
325
|
parsedLaunchers.create = parsedLaunchers.create.set(
|
|
293
|
-
launcherName,
|
|
294
|
-
|
|
295
|
-
(
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
326
|
+
launcherName,
|
|
327
|
+
<Entity, FormState, ExtraContext, Context extends CreateLauncherContext<Entity, FormState, ExtraContext>>() => ({
|
|
328
|
+
form: CreateFormTemplate<Entity, FormState>().mapContext((parentContext: Context) =>
|
|
329
|
+
({
|
|
330
|
+
...parentContext,
|
|
331
|
+
api: api,
|
|
332
|
+
actualForm: form.withView(parentContext.containerFormView).mapContext((_: any) => ({ ..._, rootValue: _.value, ...parentContext.extraContext }))
|
|
333
|
+
}) as any)
|
|
300
334
|
.withViewFromProps(props => props.context.submitButtonWrapper)
|
|
301
335
|
.mapForeignMutationsFromProps(props => props.foreignMutations as any),
|
|
302
|
-
initialState:
|
|
303
|
-
actualForm:form,
|
|
304
|
-
}
|
|
336
|
+
initialState: CreateFormState<any, any>().Default(initialState),
|
|
337
|
+
actualForm: form,
|
|
338
|
+
})
|
|
305
339
|
)
|
|
306
340
|
})
|
|
307
341
|
|
|
308
342
|
if (errors.length > 0) return Sum.Default.right(errors)
|
|
309
343
|
return Sum.Default.left(parsedLaunchers)
|
|
310
344
|
}
|
|
311
|
-
|
|
@@ -11,3 +11,10 @@ export type BooleanView<Context extends FormLabel, ForeignMutationsExpected> =
|
|
|
11
11
|
SharedFormState,
|
|
12
12
|
ForeignMutationsExpected & { onChange: OnChange<boolean>; setNewValue: SimpleCallback<boolean> }
|
|
13
13
|
>;
|
|
14
|
+
|
|
15
|
+
export type MaybeBooleanView<Context extends FormLabel, ForeignMutationsExpected> =
|
|
16
|
+
View<
|
|
17
|
+
Context & Value<boolean | undefined> & SharedFormState,
|
|
18
|
+
SharedFormState,
|
|
19
|
+
ForeignMutationsExpected & { onChange: OnChange<boolean | undefined>; setNewValue: SimpleCallback<boolean | undefined> }
|
|
20
|
+
>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { List } from "immutable";
|
|
2
|
-
import { BasicFun, BooleanView, CoTypedFactory, Debounce, Debounced, replaceWith, Synchronize, Unit, ValidateRunner } from "../../../../../../main";
|
|
2
|
+
import { BasicFun, BooleanView, CoTypedFactory, Debounce, Debounced, MaybeBooleanView, replaceWith, Synchronize, Unit, ValidateRunner } from "../../../../../../main";
|
|
3
3
|
import { Template } from "../../../../../template/state";
|
|
4
4
|
import { Value } from "../../../../../value/state";
|
|
5
5
|
import { FormLabel } from "../../../singleton/domains/form-label/state";
|
|
@@ -22,3 +22,20 @@ export const BooleanForm = <Context extends FormLabel, ForeignMutationsExpected>
|
|
|
22
22
|
),
|
|
23
23
|
]);
|
|
24
24
|
}
|
|
25
|
+
|
|
26
|
+
export const MaybeBooleanForm = <Context extends FormLabel, ForeignMutationsExpected>(
|
|
27
|
+
validation: BasicFun<boolean | undefined, Promise<FieldValidation>>
|
|
28
|
+
) => {
|
|
29
|
+
return Template.Default<Context & Value<boolean | undefined>, SharedFormState, ForeignMutationsExpected & { onChange: OnChange<boolean | undefined>; }, MaybeBooleanView<Context, ForeignMutationsExpected>>(props => <>
|
|
30
|
+
<props.view {...props}
|
|
31
|
+
foreignMutations={{
|
|
32
|
+
...props.foreignMutations,
|
|
33
|
+
setNewValue: (_) => props.foreignMutations.onChange(replaceWith(_), List())
|
|
34
|
+
}} />
|
|
35
|
+
</>
|
|
36
|
+
).any([
|
|
37
|
+
ValidateRunner<Context, SharedFormState, ForeignMutationsExpected, boolean | undefined>(
|
|
38
|
+
_ => validation(_).then(FieldValidationWithPath.Default.fromFieldValidation)
|
|
39
|
+
),
|
|
40
|
+
]);
|
|
41
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { OrderedMap } from "immutable";
|
|
2
|
+
import { Guid, Unit } from "../../../../../../main";
|
|
3
|
+
import { BasicFun } from "../../../../../fun/state";
|
|
4
|
+
import { CollectionSelection } from "../../../collection/domains/selection/state";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export type UntypedPath = Array<string | number | symbol>;
|
|
8
|
+
export type MappingPaths<Entity> = {
|
|
9
|
+
[field in keyof Entity]: Entity[field] extends (string | boolean | number | undefined | Date | CollectionSelection<infer _> | OrderedMap<Guid, infer _>) ? UntypedPath : MappingPaths<Entity[field]>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type MappingBuilder<Source, Target, mappedTargetFields extends keyof Target> =
|
|
13
|
+
(<field extends Exclude<keyof Target, mappedTargetFields>>(field: field) => Target[field] extends (string | boolean | number | undefined | Date | CollectionSelection<infer _> | OrderedMap<Guid, infer _>) ? BasicFun<
|
|
14
|
+
BasicFun<
|
|
15
|
+
PathBuilder<Source>, PathBuilder<Target[field]>
|
|
16
|
+
>, MappingBuilder<Source, Target, mappedTargetFields | field>> : BasicFun<
|
|
17
|
+
MappingBuilder<Source, Target[field], keyof Target[field]>, MappingBuilder<Source, Target, mappedTargetFields | field>>) & { kind: "mappingBuilder"; paths: MappingPaths<Pick<Target, mappedTargetFields>>; };
|
|
18
|
+
|
|
19
|
+
export type PathBuilder<Entity> =
|
|
20
|
+
(<field extends keyof Entity>(field: field) => PathBuilder<Entity[field]>) & {
|
|
21
|
+
path: UntypedPath;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const PathBuilder = {
|
|
25
|
+
Default: <Entity,>(path: UntypedPath): PathBuilder<Entity> => Object.assign(
|
|
26
|
+
<field extends keyof Entity>(field: field): PathBuilder<Entity[field]> => PathBuilder.Default<Entity[field]>([...path, field]),
|
|
27
|
+
{
|
|
28
|
+
path: path,
|
|
29
|
+
kind: "pathBuilder"
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const MappingBuilder = {
|
|
35
|
+
Default: <Source, Target, mappedTargetFields extends keyof Target = never>(paths: MappingPaths<Pick<Target, mappedTargetFields>>): MappingBuilder<Source, Target, mappedTargetFields> => Object.assign(
|
|
36
|
+
<field extends keyof Target>(field: field): any => ((_: MappingBuilder<Source, Unit, never> | BasicFun<
|
|
37
|
+
PathBuilder<Source>, PathBuilder<Target[field]>>
|
|
38
|
+
): MappingBuilder<Source, Target, mappedTargetFields | field> => {
|
|
39
|
+
if ("kind" in _ == false || _.kind != "mappingBuilder") {
|
|
40
|
+
const fieldPathBuilder = _ as BasicFun<PathBuilder<Source>, PathBuilder<Target[field]>>;
|
|
41
|
+
const pathToField = fieldPathBuilder(PathBuilder.Default<Source>([])).path;
|
|
42
|
+
const extendedPaths: MappingPaths<Pick<Target, mappedTargetFields | field>> = {
|
|
43
|
+
...paths,
|
|
44
|
+
[field]: pathToField
|
|
45
|
+
} as any;
|
|
46
|
+
const remainingMappingBuilder = MappingBuilder.Default<Source, Target, mappedTargetFields | field>(extendedPaths);
|
|
47
|
+
return remainingMappingBuilder;
|
|
48
|
+
}
|
|
49
|
+
const fieldEntityMappingBuilder = _ as MappingBuilder<Source, Unit, never>;
|
|
50
|
+
const extendedPaths: MappingPaths<Pick<Target, mappedTargetFields | field>> = {
|
|
51
|
+
...paths,
|
|
52
|
+
[field]: fieldEntityMappingBuilder.paths
|
|
53
|
+
} as any;
|
|
54
|
+
const remainingMappingBuilder = MappingBuilder.Default<Source, Target, mappedTargetFields | field>(extendedPaths);
|
|
55
|
+
return remainingMappingBuilder;
|
|
56
|
+
}),
|
|
57
|
+
{
|
|
58
|
+
paths: paths,
|
|
59
|
+
kind: "mappingBuilder" as const
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type Mapping<Source, Target> = {
|
|
65
|
+
from: BasicFun<Source, Target>;
|
|
66
|
+
to: BasicFun<[Source, Target], Source>;
|
|
67
|
+
pathFrom: BasicFun<Array<string>, Array<string>>;
|
|
68
|
+
};
|
|
69
|
+
const dynamicLookup = (e: any, path: UntypedPath): any => path.length <= 0 ? e : dynamicLookup(e[path[0]], path.slice(1));
|
|
70
|
+
const dynamicAssignment = (e: any, v: any, path: UntypedPath): any => {
|
|
71
|
+
if (path.length <= 0) return v;
|
|
72
|
+
if (path.length <= 1) {
|
|
73
|
+
return { ...e, [path[0]]: v };
|
|
74
|
+
}
|
|
75
|
+
return { ...e, [path[0]]: dynamicAssignment(e[path[0]], v, path.slice(1)) };
|
|
76
|
+
};
|
|
77
|
+
export const Mapping = {
|
|
78
|
+
Default: <Source, Target>(completedBuilder: MappingBuilder<Source, Target, keyof Target>): Mapping<Source, Target> => ({
|
|
79
|
+
from: s => {
|
|
80
|
+
const traversePaths = (paths: MappingPaths<any>) => {
|
|
81
|
+
const result_t = {} as any;
|
|
82
|
+
Object.keys(paths).forEach(_ => {
|
|
83
|
+
const field_t = _ as keyof Target;
|
|
84
|
+
if (Array.isArray(paths[field_t])) {
|
|
85
|
+
const pathToFieldT = paths[field_t] as UntypedPath;
|
|
86
|
+
result_t[field_t] = dynamicLookup(s, pathToFieldT);
|
|
87
|
+
} else {
|
|
88
|
+
result_t[field_t] = traversePaths(paths[field_t]);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
return result_t;
|
|
92
|
+
};
|
|
93
|
+
return traversePaths(completedBuilder.paths);
|
|
94
|
+
},
|
|
95
|
+
to: ([s, t]) => {
|
|
96
|
+
let result_s = s;
|
|
97
|
+
const traversePaths = (t: any, paths: MappingPaths<any>) => {
|
|
98
|
+
Object.keys(paths).forEach(_ => {
|
|
99
|
+
const field_t = _ as keyof Target;
|
|
100
|
+
if (Array.isArray(paths[field_t])) {
|
|
101
|
+
result_s = dynamicAssignment(result_s, t[field_t], paths[field_t]);
|
|
102
|
+
} else {
|
|
103
|
+
traversePaths(t[field_t], paths[field_t]);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
traversePaths(t, completedBuilder.paths);
|
|
108
|
+
return result_s;
|
|
109
|
+
},
|
|
110
|
+
pathFrom: (pathInTarget: Array<string>): Array<string> => dynamicLookup(completedBuilder.paths, pathInTarget),
|
|
111
|
+
}),
|
|
112
|
+
};
|
|
113
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { List } from "immutable";
|
|
2
|
+
import { Template } from "../../../../../template/state";
|
|
3
|
+
import { Value } from "../../../../../value/state";
|
|
4
|
+
import { EntityFormContext, EntityFormState, EntityFormForeignMutationsExpected, OnChange, EntityFormView, EntityFormTemplate } from "../../state";
|
|
5
|
+
import { Mapping } from "./state";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export type MappedEntityFormTemplate<SourceEntity, Entity, FieldStates, ExtraContext, ExtraForeignMutationsExpected> = Template<
|
|
9
|
+
Omit<EntityFormContext<Entity, (keyof Entity) & (keyof FieldStates), FieldStates, ExtraContext, ExtraForeignMutationsExpected>, "value"> & Value<SourceEntity>, EntityFormState<Entity, (keyof Entity) & (keyof FieldStates), FieldStates, ExtraContext, ExtraForeignMutationsExpected>, Omit<EntityFormForeignMutationsExpected<Entity, (keyof Entity) & (keyof FieldStates), FieldStates, ExtraContext, ExtraForeignMutationsExpected>, "onChange"> & { onChange: OnChange<SourceEntity>; }, EntityFormView<Entity, (keyof Entity) & (keyof FieldStates), FieldStates, ExtraContext, ExtraForeignMutationsExpected>
|
|
10
|
+
>;
|
|
11
|
+
|
|
12
|
+
export const MappedEntityFormTemplate = <SourceEntity, Entity, FieldStates, ExtraContext, ExtraForeignMutationsExpected>(
|
|
13
|
+
mapping: Mapping<SourceEntity, Entity>,
|
|
14
|
+
form: EntityFormTemplate<
|
|
15
|
+
Entity, (keyof Entity) & (keyof FieldStates), FieldStates, ExtraContext, ExtraForeignMutationsExpected>
|
|
16
|
+
): MappedEntityFormTemplate<
|
|
17
|
+
SourceEntity, Entity, FieldStates, ExtraContext, ExtraForeignMutationsExpected> => form
|
|
18
|
+
.mapContext((_: Omit<EntityFormContext<Entity, (keyof Entity) & (keyof FieldStates), FieldStates, ExtraContext, ExtraForeignMutationsExpected>, "value"> & Value<SourceEntity>) => ({ ..._, value: mapping.from(_.value) }) as any)
|
|
19
|
+
.mapForeignMutations((_: any) => ({
|
|
20
|
+
..._,
|
|
21
|
+
onChange: (u, path) => {
|
|
22
|
+
_.onChange(
|
|
23
|
+
(current: any) => mapping.to([
|
|
24
|
+
current,
|
|
25
|
+
u(mapping.from(current))
|
|
26
|
+
]),
|
|
27
|
+
List(mapping.pathFrom(path.toArray()))
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}));
|
|
@@ -35,7 +35,7 @@ export const Form = <Entity, FieldStates, Context, ForeignMutationsExpected>() =
|
|
|
35
35
|
props.foreignMutations.onChange((current: Entity): Entity => ({
|
|
36
36
|
...current,
|
|
37
37
|
[field]: _(current[field])
|
|
38
|
-
}), path.
|
|
38
|
+
}), path.unshift(field as string)),
|
|
39
39
|
0)
|
|
40
40
|
}
|
|
41
41
|
}))
|