Compare commits
3 Commits
ccd655e3c5
...
6db57cdb8f
| Author | SHA1 | Date | |
|---|---|---|---|
| 6db57cdb8f | |||
|
|
8642619a6a | ||
|
|
e8b8df9449 |
6
bun.lock
6
bun.lock
@@ -5,7 +5,7 @@
|
|||||||
"name": "@effect-fc/monorepo",
|
"name": "@effect-fc/monorepo",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.2.5",
|
"@biomejs/biome": "^2.2.5",
|
||||||
"@effect/language-service": "^0.45.0",
|
"@effect/language-service": "^0.46.0",
|
||||||
"@types/bun": "^1.2.23",
|
"@types/bun": "^1.2.23",
|
||||||
"npm-check-updates": "^19.0.0",
|
"npm-check-updates": "^19.0.0",
|
||||||
"npm-sort": "^0.0.4",
|
"npm-sort": "^0.0.4",
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
},
|
},
|
||||||
"packages/effect-fc": {
|
"packages/effect-fc": {
|
||||||
"name": "effect-fc",
|
"name": "effect-fc",
|
||||||
"version": "0.1.4",
|
"version": "0.1.5",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typed/async-data": "^0.13.1",
|
"@typed/async-data": "^0.13.1",
|
||||||
},
|
},
|
||||||
@@ -135,7 +135,7 @@
|
|||||||
|
|
||||||
"@effect-fc/example": ["@effect-fc/example@workspace:packages/example"],
|
"@effect-fc/example": ["@effect-fc/example@workspace:packages/example"],
|
||||||
|
|
||||||
"@effect/language-service": ["@effect/language-service@0.45.1", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-SEZ9TaVCpRKYumTQJPApg3os9O94bN2lCYQLgZbyK/xD+NSfYPPJZQ+6T5LkpcNgW8BRk1ACI7S1W2/noxm7Qg=="],
|
"@effect/language-service": ["@effect/language-service@0.46.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-eWMuy/RNvDMdhi8NJ/pfHS1UHd5R7adXlO4ClRYMgF6cUqN6FdXw1HgJHF7gJILVPD0Mdo/XQYNJ5gZbsdaImg=="],
|
||||||
|
|
||||||
"@effect/platform": ["@effect/platform@0.92.1", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.18.1" } }, "sha512-XXWCBVwyhaKZISN7aM1fv/3fWDGyxr84ObywnUrL8aHvJLoIeskWFAP/fqw3c5MFCrJ3ZV97RWLbv6JiBQugdg=="],
|
"@effect/platform": ["@effect/platform@0.92.1", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.18.1" } }, "sha512-XXWCBVwyhaKZISN7aM1fv/3fWDGyxr84ObywnUrL8aHvJLoIeskWFAP/fqw3c5MFCrJ3ZV97RWLbv6JiBQugdg=="],
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.2.5",
|
"@biomejs/biome": "^2.2.5",
|
||||||
"@effect/language-service": "^0.45.0",
|
"@effect/language-service": "^0.46.0",
|
||||||
"@types/bun": "^1.2.23",
|
"@types/bun": "^1.2.23",
|
||||||
"npm-check-updates": "^19.0.0",
|
"npm-check-updates": "^19.0.0",
|
||||||
"npm-sort": "^0.0.4",
|
"npm-sort": "^0.0.4",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ extends Pipeable.Pipeable {
|
|||||||
|
|
||||||
readonly schema: Schema.Schema<A, I, R>
|
readonly schema: Schema.Schema<A, I, R>
|
||||||
readonly onSubmit: (value: NoInfer<A>) => Effect.Effect<SA, SE, SR>
|
readonly onSubmit: (value: NoInfer<A>) => Effect.Effect<SA, SE, SR>
|
||||||
|
readonly autosubmit: boolean
|
||||||
readonly debounce: Option.Option<Duration.DurationInput>
|
readonly debounce: Option.Option<Duration.DurationInput>
|
||||||
|
|
||||||
readonly valueRef: SubscriptionRef.SubscriptionRef<Option.Option<A>>
|
readonly valueRef: SubscriptionRef.SubscriptionRef<Option.Option<A>>
|
||||||
@@ -36,6 +37,7 @@ extends Pipeable.Class() implements Form<A, I, R, SA, SE, SR> {
|
|||||||
constructor(
|
constructor(
|
||||||
readonly schema: Schema.Schema<A, I, R>,
|
readonly schema: Schema.Schema<A, I, R>,
|
||||||
readonly onSubmit: (value: NoInfer<A>) => Effect.Effect<SA, SE, SR>,
|
readonly onSubmit: (value: NoInfer<A>) => Effect.Effect<SA, SE, SR>,
|
||||||
|
readonly autosubmit: boolean,
|
||||||
readonly debounce: Option.Option<Duration.DurationInput>,
|
readonly debounce: Option.Option<Duration.DurationInput>,
|
||||||
|
|
||||||
readonly valueRef: SubscriptionRef.SubscriptionRef<Option.Option<A>>,
|
readonly valueRef: SubscriptionRef.SubscriptionRef<Option.Option<A>>,
|
||||||
@@ -53,11 +55,15 @@ extends Pipeable.Class() implements Form<A, I, R, SA, SE, SR> {
|
|||||||
export const isForm = (u: unknown): u is Form<unknown, unknown, unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, FormTypeId)
|
export const isForm = (u: unknown): u is Form<unknown, unknown, unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, FormTypeId)
|
||||||
|
|
||||||
export namespace make {
|
export namespace make {
|
||||||
export interface Options<in out A, in out I, out R, in out SA = void, in out SE = A, out SR = never> {
|
export interface Options<in out A, in out I, in out R, in out SA = void, in out SE = A, out SR = never> {
|
||||||
readonly schema: Schema.Schema<A, I, R>
|
readonly schema: Schema.Schema<A, I, R>
|
||||||
readonly initialEncodedValue: NoInfer<I>
|
readonly initialEncodedValue: NoInfer<I>
|
||||||
readonly onSubmit: (value: NoInfer<A>) => Effect.Effect<SA, SE, SR>,
|
readonly onSubmit: (
|
||||||
readonly debounce?: Duration.DurationInput,
|
this: Form<NoInfer<A>, NoInfer<I>, NoInfer<R>, unknown, unknown, unknown>,
|
||||||
|
value: NoInfer<A>,
|
||||||
|
) => Effect.Effect<SA, SE, SR>
|
||||||
|
readonly autosubmit?: boolean
|
||||||
|
readonly debounce?: Duration.DurationInput
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +82,7 @@ export const make: {
|
|||||||
return new FormImpl(
|
return new FormImpl(
|
||||||
options.schema,
|
options.schema,
|
||||||
options.onSubmit,
|
options.onSubmit,
|
||||||
|
options.autosubmit ?? false,
|
||||||
Option.fromNullable(options.debounce),
|
Option.fromNullable(options.debounce),
|
||||||
|
|
||||||
valueRef,
|
valueRef,
|
||||||
@@ -114,20 +121,24 @@ export const run = <A, I, R, SA, SE, SR>(
|
|||||||
Effect.exit,
|
Effect.exit,
|
||||||
Effect.andThen(flow(
|
Effect.andThen(flow(
|
||||||
Exit.matchEffect({
|
Exit.matchEffect({
|
||||||
onSuccess: v => Effect.andThen(
|
onSuccess: v => SubscriptionRef.set(self.valueRef, Option.some(v)).pipe(
|
||||||
SubscriptionRef.set(self.valueRef, Option.some(v)),
|
Effect.andThen(SubscriptionRef.set(self.errorRef, Option.none())),
|
||||||
SubscriptionRef.set(self.errorRef, Option.none()),
|
Effect.as(Option.some(v)),
|
||||||
),
|
),
|
||||||
onFailure: c => Option.match(
|
onFailure: c => Option.match(
|
||||||
Chunk.findFirst(Cause.failures(c), e => e._tag === "ParseError"),
|
Chunk.findFirst(Cause.failures(c), e => e._tag === "ParseError"),
|
||||||
{
|
{
|
||||||
onSome: e => SubscriptionRef.set(self.errorRef, Option.some(e)),
|
onSome: e => Effect.as(SubscriptionRef.set(self.errorRef, Option.some(e)), Option.none()),
|
||||||
onNone: () => Effect.void,
|
onNone: () => Effect.succeed(Option.none()),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
Effect.uninterruptible,
|
Effect.uninterruptible,
|
||||||
)),
|
)),
|
||||||
|
Effect.andThen(value => Option.isSome(value) && self.autosubmit
|
||||||
|
?
|
||||||
|
: Effect.void
|
||||||
|
),
|
||||||
Effect.scoped,
|
Effect.scoped,
|
||||||
Effect.forkScoped,
|
Effect.forkScoped,
|
||||||
)
|
)
|
||||||
@@ -158,7 +169,7 @@ export const submit = <A, I, R, SA, SE, SR>(
|
|||||||
)
|
)
|
||||||
|
|
||||||
export namespace service {
|
export namespace service {
|
||||||
export interface Options<in out A, in out I, out R, in out SA = void, in out SE = A, out SR = never>
|
export interface Options<in out A, in out I, in out R, in out SA = void, in out SE = A, out SR = never>
|
||||||
extends make.Options<A, I, R, SA, SE, SR> {}
|
extends make.Options<A, I, R, SA, SE, SR> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,16 @@ import { Component, Form, Hooks, Memoized, Subscribable, SubscriptionSubRef } fr
|
|||||||
import { FaArrowDown, FaArrowUp } from "react-icons/fa"
|
import { FaArrowDown, FaArrowUp } from "react-icons/fa"
|
||||||
import { FaDeleteLeft } from "react-icons/fa6"
|
import { FaDeleteLeft } from "react-icons/fa6"
|
||||||
import * as Domain from "@/domain"
|
import * as Domain from "@/domain"
|
||||||
|
import { TextFieldFormInput } from "@/lib/form/TextFieldFormInput"
|
||||||
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
|
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
|
||||||
import { TodosState } from "./TodosState.service"
|
import { TodosState } from "./TodosState.service"
|
||||||
|
|
||||||
|
|
||||||
|
const TodoFormSchema = Schema.compose(Schema.Struct({
|
||||||
|
...Domain.Todo.Todo.fields,
|
||||||
|
completedAt: Schema.OptionFromSelf(DateTimeUtcFromZonedInput),
|
||||||
|
}), Domain.Todo.Todo)
|
||||||
|
|
||||||
const makeTodo = makeUuid4.pipe(
|
const makeTodo = makeUuid4.pipe(
|
||||||
Effect.map(id => Domain.Todo.Todo.make({
|
Effect.map(id => Domain.Todo.Todo.make({
|
||||||
id,
|
id,
|
||||||
@@ -28,51 +34,62 @@ export class Todo extends Component.makeUntraced("Todo")(function*(props: TodoPr
|
|||||||
const runtime = yield* Effect.runtime()
|
const runtime = yield* Effect.runtime()
|
||||||
const state = yield* TodosState
|
const state = yield* TodosState
|
||||||
|
|
||||||
const { ref, indexRef, contentRef, completedAtRef } = yield* Hooks.useMemo(() => Match.value(props).pipe(
|
const [indexRef, form, contentField, completedAtField] = yield* Component.useOnChange(() => Effect.gen(function*() {
|
||||||
Match.tag("new", () => Effect.Do.pipe(
|
const indexRef = Match.value(props).pipe(
|
||||||
Effect.bind("ref", () => Effect.andThen(makeTodo, SubscriptionRef.make)),
|
Match.tag("new", () => Subscribable.make({ get: Effect.succeed(-1), changes: Stream.empty })),
|
||||||
Effect.let("indexRef", () => Subscribable.make({ get: Effect.succeed(-1), changes: Stream.empty })),
|
Match.tag("edit", ({ id }) => state.getIndexSubscribable(id)),
|
||||||
)),
|
|
||||||
Match.tag("edit", ({ id }) => Effect.Do.pipe(
|
|
||||||
Effect.let("ref", () => state.getElementRef(id)),
|
|
||||||
Effect.let("indexRef", () => state.getIndexSubscribable(id)),
|
|
||||||
)),
|
|
||||||
Match.exhaustive,
|
Match.exhaustive,
|
||||||
|
)
|
||||||
|
|
||||||
Effect.let("contentRef", ({ ref }) => SubscriptionSubRef.makeFromPath(ref, ["content"])),
|
|
||||||
Effect.let("completedAtRef", ({ ref }) => SubscriptionSubRef.makeFromPath(ref, ["completedAt"])),
|
|
||||||
), [props._tag, props._tag === "edit" ? props.id : undefined])
|
|
||||||
|
|
||||||
const { form } = yield* Component.useOnChange(() => Effect.gen(function*() {
|
|
||||||
const form = yield* Form.service({
|
const form = yield* Form.service({
|
||||||
schema: Domain.Todo.TodoFromJson,
|
schema: TodoFormSchema,
|
||||||
initialEncodedValue: yield* Schema.encode(Domain.Todo.TodoFromJson)(
|
initialEncodedValue: yield* Schema.encode(TodoFormSchema)(
|
||||||
yield* Match.value(props).pipe(
|
yield* Match.value(props).pipe(
|
||||||
Match.tag("new", () => makeTodo),
|
Match.tag("new", () => makeTodo),
|
||||||
Match.tag("edit", ({ id }) => state.getElementRef(id)),
|
Match.tag("edit", ({ id }) => state.getElementRef(id)),
|
||||||
Match.exhaustive,
|
Match.exhaustive,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
onSubmit: v => Effect.void,
|
onSubmit: function(todo) {
|
||||||
|
return Match.value(props).pipe(
|
||||||
|
Match.tag("new", () => Ref.update(state.ref, Chunk.prepend(todo)).pipe(
|
||||||
|
Effect.andThen(makeTodo),
|
||||||
|
Effect.andThen(Schema.encode(TodoFormSchema)),
|
||||||
|
Effect.andThen(v => Ref.set(this.encodedValueRef, v)),
|
||||||
|
)),
|
||||||
|
Match.tag("edit", ({ id }) => Ref.set(state.getElementRef(id), todo)),
|
||||||
|
Match.exhaustive,
|
||||||
|
)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return { form }
|
return [
|
||||||
|
indexRef,
|
||||||
|
form,
|
||||||
|
Form.field(form, ["content"]),
|
||||||
|
Form.field(form, ["completedAt"]),
|
||||||
|
] as const
|
||||||
}), [props._tag, props._tag === "edit" ? props.id : undefined])
|
}), [props._tag, props._tag === "edit" ? props.id : undefined])
|
||||||
|
|
||||||
const [index, size] = yield* Hooks.useSubscribables(indexRef, state.sizeSubscribable)
|
const [index, size] = yield* Hooks.useSubscribables(indexRef, state.sizeSubscribable)
|
||||||
|
const submit = yield* Form.useSubmit(form)
|
||||||
|
const TextFieldFormInputFC = yield* TextFieldFormInput
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="row" align="center" gap="2">
|
<Flex direction="row" align="center" gap="2">
|
||||||
<Box flexGrow="1">
|
<Box flexGrow="1">
|
||||||
<Flex direction="column" align="stretch" gap="2">
|
<Flex direction="column" align="stretch" gap="2">
|
||||||
<StringTextAreaInputFC ref={contentRef} />
|
<TextFieldFormInputFC
|
||||||
|
field={contentField}
|
||||||
|
/>
|
||||||
|
|
||||||
<Flex direction="row" justify="center" align="center" gap="2">
|
<Flex direction="row" justify="center" align="center" gap="2">
|
||||||
<OptionalDateTimeInputFC
|
<TextFieldFormInputFC
|
||||||
|
optional
|
||||||
|
field={completedAtField}
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
ref={completedAtRef}
|
defaultValue=""
|
||||||
defaultValue={yield* Hooks.useOnce(() => DateTime.now)}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{props._tag === "new" &&
|
{props._tag === "new" &&
|
||||||
|
|||||||
Reference in New Issue
Block a user