Compare commits

...

3 Commits

Author SHA1 Message Date
6db57cdb8f Update dependency @effect/language-service to ^0.46.0
Some checks failed
Lint / lint (push) Failing after 40s
Test build / test-build (pull_request) Failing after 11s
2025-10-22 08:53:25 +02:00
Julien Valverdé
8642619a6a Form work
Some checks failed
Lint / lint (push) Failing after 9s
2025-10-21 14:49:53 +02:00
Julien Valverdé
e8b8df9449 Form work
Some checks failed
Lint / lint (push) Failing after 11s
2025-10-21 14:01:19 +02:00
4 changed files with 64 additions and 36 deletions

View File

@@ -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=="],

View File

@@ -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",

View File

@@ -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> {}
} }

View File

@@ -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" &&