From 41356d9c38bf78d2297e03e2d284d61d6770a7bd Mon Sep 17 00:00:00 2001 From: nichenqin Date: Wed, 4 Sep 2024 15:18:02 +0800 Subject: [PATCH 1/4] feat: delete form --- .../blocks/forms/form-option.svelte | 54 ++++++++++++++++++- .../delete-table-form.command-handler.ts | 20 +++++++ .../command-handlers/src/handlers/index.ts | 2 + packages/commands/src/delete-form.command.ts | 16 ++++++ packages/commands/src/index.ts | 1 + .../src/table/table.filter-visitor.ts | 4 ++ .../src/table/table.mutation-visitor.ts | 11 ++++ .../src/table/table.reference-visitor.ts | 2 + .../underlying-table-spec.visitor.ts | 2 + .../table/src/methods/delete-form.method.ts | 24 +++++++++ .../src/modules/forms/dto/delete-form.dto.ts | 11 ++++ packages/table/src/modules/forms/dto/index.ts | 1 + .../methods/delete-table-form.method.ts | 14 +++++ packages/table/src/services/table.service.ts | 4 ++ .../table-forms.specification.ts | 17 ++++++ .../specifications/table-visitor.interface.ts | 2 + packages/table/src/table.do.ts | 2 + packages/trpc/src/router.ts | 6 +++ 18 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 packages/command-handlers/src/handlers/delete-table-form.command-handler.ts create mode 100644 packages/commands/src/delete-form.command.ts create mode 100644 packages/table/src/methods/delete-form.method.ts create mode 100644 packages/table/src/modules/forms/dto/delete-form.dto.ts create mode 100644 packages/table/src/services/methods/delete-table-form.method.ts diff --git a/apps/frontend/src/lib/components/blocks/forms/form-option.svelte b/apps/frontend/src/lib/components/blocks/forms/form-option.svelte index fc4011bb6..d8df2712e 100644 --- a/apps/frontend/src/lib/components/blocks/forms/form-option.svelte +++ b/apps/frontend/src/lib/components/blocks/forms/form-option.svelte @@ -10,6 +10,9 @@ import { invalidate } from "$app/navigation" import { Checkbox } from "$lib/components/ui/checkbox/index.js" import { Label } from "$lib/components/ui/label/index.js" + import * as DropdownMenu from "$lib/components/ui/dropdown-menu" + import { EllipsisIcon } from "lucide-svelte" + import * as AlertDialog from "$lib/components/ui/alert-dialog" const table = getTable() export let form: FormVO @@ -26,6 +29,7 @@ mutationKey: ["table", $table.id.value, "setForm"], mutationFn: trpc.table.form.set.mutate, async onSuccess() { + await goto(`/t/${$table.id.value}`) await invalidate(`table:${$table.id.value}`) }, }) @@ -37,10 +41,45 @@ form: form.toJSON(), }) } + + const deleteFormMutation = createMutation({ + mutationKey: ["table", $table.id.value, "deleteForm"], + mutationFn: trpc.table.form.delete.mutate, + async onSuccess() { + await invalidate(`table:${$table.id.value}`) + }, + }) + + const deleteForm = async () => { + await $deleteFormMutation.mutateAsync({ + tableId: $table.id.value, + id: form.id, + }) + confirmDelete = false + } + + let confirmDelete = false
-

Form Setting

+
+

Form Setting

+ + + + + + + (confirmDelete = true)} + class="hover:text-500 flex items-center text-xs text-red-500 transition-colors hover:bg-red-100" + > + Delete form + + + + +

Background color

@@ -90,3 +129,16 @@
+ + + + + Delete form: {form.name}? + Form will be deleted permanently. + + + Cancel + Continue + + + diff --git a/packages/command-handlers/src/handlers/delete-table-form.command-handler.ts b/packages/command-handlers/src/handlers/delete-table-form.command-handler.ts new file mode 100644 index 000000000..da1f57de2 --- /dev/null +++ b/packages/command-handlers/src/handlers/delete-table-form.command-handler.ts @@ -0,0 +1,20 @@ +import { DeleteFormCommand } from "@undb/commands" +import { commandHandler } from "@undb/cqrs" +import { singleton } from "@undb/di" +import type { ICommandHandler } from "@undb/domain" +import { injectTableService, type ITableService } from "@undb/table" + +@commandHandler(DeleteFormCommand) +@singleton() +export class DeleteFormCommandHandler implements ICommandHandler { + constructor( + @injectTableService() + private readonly service: ITableService, + ) {} + + async execute(command: DeleteFormCommand): Promise { + await this.service.deleteTableForm(command.input) + + return { success: true } + } +} diff --git a/packages/command-handlers/src/handlers/index.ts b/packages/command-handlers/src/handlers/index.ts index db9c20272..33c323deb 100644 --- a/packages/command-handlers/src/handlers/index.ts +++ b/packages/command-handlers/src/handlers/index.ts @@ -18,6 +18,7 @@ import { DeleteInvitationCommandHandler } from "./delete-invitation.command-hand import { DeleteRecordCommandHandler } from "./delete-record.command-handler" import { DeleteSpaceCommandHandler } from "./delete-space.command-handler" import { DeleteTableFieldCommandHandler } from "./delete-table-field.command-handler" +import { DeleteFormCommandHandler } from "./delete-table-form.command-handler" import { DeleteTableCommandHandler } from "./delete-table.command-handler" import { DeleteViewCommandHandler } from "./delete-view.command-handler" import { DeleteWebhookCommandHandler } from "./delete-webhook.command-handler" @@ -98,4 +99,5 @@ export const commandHandlers = [ DuplicateBaseCommandHandler, DeleteBaseCommandHandler, TriggerRecordButtonCommandHandler, + DeleteFormCommandHandler, ] diff --git a/packages/commands/src/delete-form.command.ts b/packages/commands/src/delete-form.command.ts new file mode 100644 index 000000000..ce11b331f --- /dev/null +++ b/packages/commands/src/delete-form.command.ts @@ -0,0 +1,16 @@ +import { Command, type CommandProps } from "@undb/domain" +import { deleteTableFormDTO, type IDeleteTableFormDTO } from "@undb/table" +import { z } from "@undb/zod" + +export const deleteFormCommand = deleteTableFormDTO + +export type IDeleteFormCommand = z.infer + +export class DeleteFormCommand extends Command { + public readonly input: IDeleteTableFormDTO + + constructor(props: CommandProps) { + super(props) + this.input = props + } +} diff --git a/packages/commands/src/index.ts b/packages/commands/src/index.ts index f19d89db0..31781ed58 100644 --- a/packages/commands/src/index.ts +++ b/packages/commands/src/index.ts @@ -14,6 +14,7 @@ export * from "./create-table-view.command" export * from "./create-table.command" export * from "./create-webhook.command" export * from "./delete-base.command" +export * from "./delete-form.command" export * from "./delete-invitation.command" export * from "./delete-record.command" export * from "./delete-space.command" diff --git a/packages/persistence/src/table/table.filter-visitor.ts b/packages/persistence/src/table/table.filter-visitor.ts index 03d740b94..45a6c0658 100644 --- a/packages/persistence/src/table/table.filter-visitor.ts +++ b/packages/persistence/src/table/table.filter-visitor.ts @@ -20,6 +20,7 @@ import type { WithNewFormSpecification, WithNewView, WithoutFieldSpecification, + WithoutFormSpecification, WithoutView, WithTableForeignTablesSpec, WithTableRLS, @@ -139,6 +140,9 @@ export class TableFilterVisitor extends AbstractQBVisitor implements IT withNewForm(views: WithNewFormSpecification): void { throw new Error("Method not implemented.") } + withoutForm(spec: WithoutFormSpecification): void { + throw new Error("Method not implemented.") + } withForm(views: WithFormSpecification): void { throw new Error("Method not implemented.") } diff --git a/packages/persistence/src/table/table.mutation-visitor.ts b/packages/persistence/src/table/table.mutation-visitor.ts index efce220b4..d4543ad52 100644 --- a/packages/persistence/src/table/table.mutation-visitor.ts +++ b/packages/persistence/src/table/table.mutation-visitor.ts @@ -30,6 +30,7 @@ import type { WithViewOption, WithViewSort, WithoutFieldSpecification, + WithoutFormSpecification, WithoutView, } from "@undb/table" import { AbstractQBMutationVisitor } from "../abstract-qb.visitor" @@ -211,6 +212,16 @@ export class TableMutationVisitor extends AbstractQBMutationVisitor implements I this.addSql(sql) } + withoutForm(spec: WithoutFormSpecification): void { + this.setData(tables.forms.name, this.table.forms ? json(this.table.forms?.toJSON()) : null) + + const deleteQuery = this.qb + .deleteFrom("undb_table_id_mapping") + .where((eb) => eb.eb("subject_id", "=", spec.formId)) + .compile() + + this.addSql(deleteQuery) + } withForm(views: WithFormSpecification): void { this.setData(tables.forms.name, this.table.forms ? json(this.table.forms?.toJSON()) : null) } diff --git a/packages/persistence/src/table/table.reference-visitor.ts b/packages/persistence/src/table/table.reference-visitor.ts index 305810832..02d7c2cfb 100644 --- a/packages/persistence/src/table/table.reference-visitor.ts +++ b/packages/persistence/src/table/table.reference-visitor.ts @@ -20,6 +20,7 @@ import type { WithNewFormSpecification, WithNewView, WithoutFieldSpecification, + WithoutFormSpecification, WithoutView, WithTableForeignTablesSpec, WithTableRLS, @@ -73,6 +74,7 @@ export class TableReferenceVisitor implements ITableSpecVisitor { this.sqb = this.sqb.leftJoin("undb_table_id_mapping", "undb_table_id_mapping.table_id", "undb_table.id") } withNewForm(views: WithNewFormSpecification): void {} + withoutForm(spec: WithoutFormSpecification): void {} withForm(views: WithFormSpecification): void {} withForeignRollupField(spec: WithForeignRollupFieldSpec): void {} withTableForeignTables(spec: WithTableForeignTablesSpec): void {} diff --git a/packages/persistence/src/underlying/underlying-table-spec.visitor.ts b/packages/persistence/src/underlying/underlying-table-spec.visitor.ts index edb700649..251ab384c 100644 --- a/packages/persistence/src/underlying/underlying-table-spec.visitor.ts +++ b/packages/persistence/src/underlying/underlying-table-spec.visitor.ts @@ -41,6 +41,7 @@ import type { WithFormIdSpecification, WithFormSpecification, WithNewFormSpecification, + WithoutFormSpecification, } from "@undb/table/src/specifications/table-forms.specification" import type { WithTableRLS } from "@undb/table/src/specifications/table-rls.specification" import { AlterTableBuilder, AlterTableColumnAlteringBuilder, CompiledQuery, CreateTableBuilder, sql } from "kysely" @@ -194,6 +195,7 @@ export class UnderlyingTableSpecVisitor implements ITableSpecVisitor { withForm(views: WithFormSpecification): void {} withForms(views: TableFormsSpecification): void {} withNewForm(views: WithNewFormSpecification): void {} + withoutForm(spec: WithoutFormSpecification): void {} withId(id: TableIdSpecification): void {} withName(name: TableNameSpecification): void {} withSchema(schema: TableSchemaSpecification): void {} diff --git a/packages/table/src/methods/delete-form.method.ts b/packages/table/src/methods/delete-form.method.ts new file mode 100644 index 000000000..b75f80ccf --- /dev/null +++ b/packages/table/src/methods/delete-form.method.ts @@ -0,0 +1,24 @@ +import { None, Some, type Option } from "@undb/domain" +import type { IDeleteTableFormDTO } from "../modules/forms/dto/delete-form.dto" +import { WithoutFormSpecification, type TableComositeSpecification } from "../specifications" +import type { TableDo } from "../table.do" + +export function deleteFormMethod( + this: TableDo, + { id: formId }: IDeleteTableFormDTO, +): Option { + const form = this.forms?.props.find((f) => f.id === formId) + if (!form) return None + + const spec = new WithoutFormSpecification(formId) + + spec.mutate(this) + + // const event = new FormDeletedEvent({ + // tableId: this.id.value, + // formId: form.id, + // }) + // this.addDomainEvent(event) + + return Some(spec) +} diff --git a/packages/table/src/modules/forms/dto/delete-form.dto.ts b/packages/table/src/modules/forms/dto/delete-form.dto.ts new file mode 100644 index 000000000..47606ced6 --- /dev/null +++ b/packages/table/src/modules/forms/dto/delete-form.dto.ts @@ -0,0 +1,11 @@ +import { z } from "@undb/zod" +import { tableId } from "../../../table-id.vo" +import { formId } from "../form/form-id.vo" + +export const deleteFormDTO = z.object({ + id: formId, +}) +export type IDeleteFormDTO = z.infer + +export const deleteTableFormDTO = deleteFormDTO.merge(z.object({ tableId })) +export type IDeleteTableFormDTO = z.infer diff --git a/packages/table/src/modules/forms/dto/index.ts b/packages/table/src/modules/forms/dto/index.ts index 31cf22b25..fc32a477c 100644 --- a/packages/table/src/modules/forms/dto/index.ts +++ b/packages/table/src/modules/forms/dto/index.ts @@ -1,2 +1,3 @@ export * from "./create-form.dto" +export * from "./delete-form.dto" export * from "./forms.dto" diff --git a/packages/table/src/services/methods/delete-table-form.method.ts b/packages/table/src/services/methods/delete-table-form.method.ts new file mode 100644 index 000000000..926ba676b --- /dev/null +++ b/packages/table/src/services/methods/delete-table-form.method.ts @@ -0,0 +1,14 @@ +import type { IDeleteTableFormDTO } from "../../modules/forms/dto/delete-form.dto" +import { TableIdVo } from "../../table-id.vo" +import type { TableDo } from "../../table.do" +import type { TableService } from "../table.service" + +export async function deleteTableFormMethod(this: TableService, dto: IDeleteTableFormDTO): Promise { + const table = (await this.repository.findOneById(new TableIdVo(dto.tableId))).expect("Not found table") + + const spec = table.$deleteForm(dto) + + await this.repository.updateOneById(table, spec) + + return table +} diff --git a/packages/table/src/services/table.service.ts b/packages/table/src/services/table.service.ts index 699306c66..f40558cdc 100644 --- a/packages/table/src/services/table.service.ts +++ b/packages/table/src/services/table.service.ts @@ -16,6 +16,7 @@ import { injectRecordQueryRepository, injectRecordRepository, type ICreateTableViewDTO, + type IDeleteTableFormDTO, type IExportViewDTO, type IReadableRecordDTO, type IRecordQueryRepository, @@ -31,6 +32,7 @@ import { createTableFormMethod } from "./methods/create-table-form.method" import { createTableViewMethod } from "./methods/create-table-view.method" import { createTableMethod } from "./methods/create-table.method" import { deleteTableFieldMethod } from "./methods/delete-table-field.method" +import { deleteTableFormMethod } from "./methods/delete-table-form.method" import { deleteTableMethod } from "./methods/delete-table.method" import { duplicateBaseMethod } from "./methods/duplicate-base.method" import { duplicateTableFieldMethod } from "./methods/duplicate-table-field.method" @@ -52,6 +54,7 @@ export interface ITableService { duplicateTableField(dto: IDuplicateTableFieldDTO): Promise createTableForm(dto: ICreateTableFormDTO): Promise + deleteTableForm(dto: IDeleteTableFormDTO): Promise createTableView(dto: ICreateTableViewDTO): Promise exportView(tableId: string, dto: IExportViewDTO): Promise<{ table: TableDo; records: IReadableRecordDTO[] }> @@ -89,6 +92,7 @@ export class TableService implements ITableService { updateTableField = updateTableFieldMethod createTableForm = createTableFormMethod + deleteTableForm = deleteTableFormMethod createTableView = createTableViewMethod exportView = exportViewMethod diff --git a/packages/table/src/specifications/table-forms.specification.ts b/packages/table/src/specifications/table-forms.specification.ts index 20e7562cc..11b3b687e 100644 --- a/packages/table/src/specifications/table-forms.specification.ts +++ b/packages/table/src/specifications/table-forms.specification.ts @@ -56,6 +56,23 @@ export class WithNewFormSpecification extends TableComositeSpecification { } } +export class WithoutFormSpecification extends TableComositeSpecification { + constructor(public readonly formId: string) { + super() + } + isSatisfiedBy(t: TableDo): boolean { + throw new WontImplementException(WithoutFormSpecification.name + ".isSatisfiedBy") + } + mutate(t: TableDo): Result { + t.forms = t.forms ? new FormsVO(t.forms?.props.filter((f) => f.id !== this.formId)) : undefined + return Ok(t) + } + accept(v: ITableSpecVisitor): Result { + v.withoutForm(this) + return Ok(undefined) + } +} + export class WithFormSpecification extends TableComositeSpecification { constructor( public readonly previous: IFormDTO | undefined, diff --git a/packages/table/src/specifications/table-visitor.interface.ts b/packages/table/src/specifications/table-visitor.interface.ts index ed9d9336e..b4f414d9a 100644 --- a/packages/table/src/specifications/table-visitor.interface.ts +++ b/packages/table/src/specifications/table-visitor.interface.ts @@ -5,6 +5,7 @@ import type { WithFormIdSpecification, WithFormSpecification, WithNewFormSpecification, + WithoutFormSpecification, } from "./table-forms.specification" import type { TableIdSpecification, TableIdsSpecification } from "./table-id.specification" import type { TableNameSpecification, TableUniqueNameSpecification } from "./table-name.specification" @@ -59,6 +60,7 @@ export interface ITableSpecVisitor extends ISpecVisitor { withViewAggregate(viewColor: WithViewAggregate): void withViewFields(fields: WithViewFields): void withForms(views: TableFormsSpecification): void + withoutForm(spec: WithoutFormSpecification): void withFormId(spec: WithFormIdSpecification): void withNewForm(views: WithNewFormSpecification): void withForm(views: WithFormSpecification): void diff --git a/packages/table/src/table.do.ts b/packages/table/src/table.do.ts index 61317e8d8..f5ba78537 100644 --- a/packages/table/src/table.do.ts +++ b/packages/table/src/table.do.ts @@ -6,6 +6,7 @@ import { $createFieldSpec, createFieldMethod } from "./methods/create-field.meth import { createFormMethod } from "./methods/create-form.method" import { createViewMethod } from "./methods/create-view.method" import { deleteFieldMethod } from "./methods/delete-field.method" +import { deleteFormMethod } from "./methods/delete-form.method" import { deleteViewMethod } from "./methods/delete-view.method" import { duplicateFieldMethod } from "./methods/duplicate-field.method" import { duplicateTableMethod } from "./methods/duplicate-table.method" @@ -66,6 +67,7 @@ export class TableDo extends AggregateRoot { $deleteField = deleteFieldMethod $duplicateField = duplicateFieldMethod $createForm = createFormMethod + $deleteForm = deleteFormMethod $createView = createViewMethod $duplicateView = duplicateViewMethod $deleteView = deleteViewMethod diff --git a/packages/trpc/src/router.ts b/packages/trpc/src/router.ts index 310453be4..26de5ea93 100644 --- a/packages/trpc/src/router.ts +++ b/packages/trpc/src/router.ts @@ -14,6 +14,7 @@ import { CreateTableViewCommand, CreateWebhookCommand, DeleteBaseCommand, + DeleteFormCommand, DeleteInvitationCommand, DeleteRecordCommand, DeleteTableCommand, @@ -61,6 +62,7 @@ import { createTableViewCommand, createWebhookCommand, deleteBaseCommand, + deleteFormCommand, deleteInvitationCommand, deleteRecordCommand, deleteTableCommand, @@ -138,6 +140,10 @@ const formRouter = t.router({ .use(authz("form:update")) .input(setTableFormCommand) .mutation(({ input }) => commandBus.execute(new SetTableFormCommand(input))), + delete: privateProcedure + .use(authz("form:delete")) + .input(deleteFormCommand) + .mutation(({ input }) => commandBus.execute(new DeleteFormCommand(input))), }) const viewRouter = t.router({ From 14a956ad3b06d919931d1a5de67c5e19f728bcd5 Mon Sep 17 00:00:00 2001 From: nichenqin Date: Wed, 4 Sep 2024 16:21:22 +0800 Subject: [PATCH 2/4] feat: create by form --- .../create-record/create-record-button.svelte | 82 +++++++++++-------- .../blocks/forms/create-form-button.svelte | 58 +------------ .../blocks/forms/create-form.svelte | 62 ++++++++++++++ 3 files changed, 113 insertions(+), 89 deletions(-) create mode 100644 apps/frontend/src/lib/components/blocks/forms/create-form.svelte diff --git a/apps/frontend/src/lib/components/blocks/create-record/create-record-button.svelte b/apps/frontend/src/lib/components/blocks/create-record/create-record-button.svelte index 245f48fc6..833899899 100644 --- a/apps/frontend/src/lib/components/blocks/create-record/create-record-button.svelte +++ b/apps/frontend/src/lib/components/blocks/create-record/create-record-button.svelte @@ -1,17 +1,20 @@ {#if $hasPermission("record:create")} @@ -24,43 +27,56 @@ toggleModal(CREATE_RECORD_MODAL) }} {...$$restProps} - class={cn(hasForms && "rounded-r-none border-r-0", $$restProps.class)} + class={cn("rounded-r-none border-r-0", $$restProps.class)} > Create Record - {#if hasForms} - - - - - - - - - Create By Form + + + + + + + + Create By Form + + + {#each forms as form} + { + $formId = form.id + toggleModal(CREATE_RECORD_MODAL) + }} > + {form.name} + - {#each forms as form} - { - $formId = form.id - toggleModal(CREATE_RECORD_MODAL) - }} - > - {form.name} - - {/each} - - - - {/if} + {/each} + (createForm = true)}> + + Create Form + + + + {/if} + + + + + Create Form + + + (createForm = false)} /> + + diff --git a/apps/frontend/src/lib/components/blocks/forms/create-form-button.svelte b/apps/frontend/src/lib/components/blocks/forms/create-form-button.svelte index bae61c2f6..05af19229 100644 --- a/apps/frontend/src/lib/components/blocks/forms/create-form-button.svelte +++ b/apps/frontend/src/lib/components/blocks/forms/create-form-button.svelte @@ -1,56 +1,12 @@ {#if $hasPermission("table:update")} @@ -64,17 +20,7 @@ -
- - - Name - - - - - - Create -
+
{/if} diff --git a/apps/frontend/src/lib/components/blocks/forms/create-form.svelte b/apps/frontend/src/lib/components/blocks/forms/create-form.svelte new file mode 100644 index 000000000..260ecab00 --- /dev/null +++ b/apps/frontend/src/lib/components/blocks/forms/create-form.svelte @@ -0,0 +1,62 @@ + + +{#if $hasPermission("table:update")} +
+ + + Name + + + + + + Create +
+{/if} From 035b299206b12bcfd9c9ddaeda6e6f6e15e20445 Mon Sep 17 00:00:00 2001 From: nichenqin Date: Wed, 4 Sep 2024 20:52:41 +0800 Subject: [PATCH 3/4] feat: persontage field --- apps/frontend/schema.graphql | 1 + .../blocks/create-record/create-record.svelte | 2 +- .../blocks/field-control/field-control.svelte | 2 + .../field-control/percentage-control.svelte | 29 +++++ .../blocks/field-icon/field-icon.svelte | 2 + .../blocks/field-options/field-options.svelte | 2 + .../percentage-field-option.svelte | 78 ++++++++++++ .../blocks/filters-editor/filter-input.svelte | 9 ++ .../editable-cell/percentage-cell.svelte | 62 ++++++++++ .../blocks/grid-view/grid-view-cell.svelte | 2 + .../blocks/record-detail/record-detail.svelte | 2 +- packages/graphql/src/index.ts | 1 + packages/i18n/src/i18n/en/index.ts | 1 + packages/i18n/src/i18n/i18n-types.ts | 8 ++ .../record/record-query-creator-visitor.ts | 2 + .../record-query-spec-creator-visitor.ts | 2 + .../src/record/record-reference-visitor.ts | 4 + .../src/record/record-select-field-visitor.ts | 4 + .../record/record-spec-reference-visitor.ts | 2 + .../src/record/record.filter-visitor.ts | 5 + .../src/record/record.mutate-visitor.ts | 4 + .../underlying-table-field.visitor.ts | 5 + .../record/record-visitor.interface.ts | 2 + .../schema/fields/dto/create-field.dto.ts | 14 +-- .../modules/schema/fields/dto/field.dto.ts | 2 + .../schema/fields/dto/update-field.dto.ts | 2 + .../schema/fields/field-value.factory.ts | 3 + .../modules/schema/fields/field.aggregate.ts | 4 +- .../modules/schema/fields/field.factory.ts | 3 + .../src/modules/schema/fields/field.type.ts | 13 ++ .../src/modules/schema/fields/field.util.ts | 15 ++- .../modules/schema/fields/field.visitor.ts | 6 +- .../modules/schema/fields/variants/index.ts | 1 + .../fields/variants/percentage-field/index.ts | 6 + .../percentage-field-constraint.vo.ts | 46 ++++++++ .../percentage-field-value.visitor.ts | 5 + .../percentage-field-value.vo.ts | 15 +++ .../percentage-field.aggregate.ts | 11 ++ .../percentage-field.condition.ts | 18 +++ .../percentage-field.specification.ts | 27 +++++ .../percentage-field/percentage-field.vo.ts | 111 ++++++++++++++++++ 41 files changed, 521 insertions(+), 12 deletions(-) create mode 100644 apps/frontend/src/lib/components/blocks/field-control/percentage-control.svelte create mode 100644 apps/frontend/src/lib/components/blocks/field-options/percentage-field-option.svelte create mode 100644 apps/frontend/src/lib/components/blocks/grid-view/editable-cell/percentage-cell.svelte create mode 100644 packages/table/src/modules/schema/fields/variants/percentage-field/index.ts create mode 100644 packages/table/src/modules/schema/fields/variants/percentage-field/percentage-field-constraint.vo.ts create mode 100644 packages/table/src/modules/schema/fields/variants/percentage-field/percentage-field-value.visitor.ts create mode 100644 packages/table/src/modules/schema/fields/variants/percentage-field/percentage-field-value.vo.ts create mode 100644 packages/table/src/modules/schema/fields/variants/percentage-field/percentage-field.aggregate.ts create mode 100644 packages/table/src/modules/schema/fields/variants/percentage-field/percentage-field.condition.ts create mode 100644 packages/table/src/modules/schema/fields/variants/percentage-field/percentage-field.specification.ts create mode 100644 packages/table/src/modules/schema/fields/variants/percentage-field/percentage-field.vo.ts diff --git a/apps/frontend/schema.graphql b/apps/frontend/schema.graphql index f18809d44..ab67d04cf 100644 --- a/apps/frontend/schema.graphql +++ b/apps/frontend/schema.graphql @@ -42,6 +42,7 @@ enum FieldType { json longText number + percentage rating reference rollup diff --git a/apps/frontend/src/lib/components/blocks/create-record/create-record.svelte b/apps/frontend/src/lib/components/blocks/create-record/create-record.svelte index bde9c4fac..2a0cd1f14 100644 --- a/apps/frontend/src/lib/components/blocks/create-record/create-record.svelte +++ b/apps/frontend/src/lib/components/blocks/create-record/create-record.svelte @@ -106,7 +106,7 @@ {/if} -
+
diff --git a/apps/frontend/src/lib/components/blocks/field-control/percentage-control.svelte b/apps/frontend/src/lib/components/blocks/field-control/percentage-control.svelte new file mode 100644 index 000000000..a093c6a73 --- /dev/null +++ b/apps/frontend/src/lib/components/blocks/field-control/percentage-control.svelte @@ -0,0 +1,29 @@ + + + + +
+ +
+
+ +

{value}

+
+
diff --git a/apps/frontend/src/lib/components/blocks/field-icon/field-icon.svelte b/apps/frontend/src/lib/components/blocks/field-icon/field-icon.svelte index b0262d298..940980bb8 100644 --- a/apps/frontend/src/lib/components/blocks/field-icon/field-icon.svelte +++ b/apps/frontend/src/lib/components/blocks/field-icon/field-icon.svelte @@ -27,6 +27,7 @@ DollarSignIcon, MousePointerClickIcon, TimerIcon, + PercentIcon, } from "lucide-svelte" export let type: FieldType @@ -56,6 +57,7 @@ user: field?.type === "user" && field.isMultiple ? UsersIcon : UserIcon, button: MousePointerClickIcon, duration: TimerIcon, + percentage: PercentIcon, } diff --git a/apps/frontend/src/lib/components/blocks/field-options/field-options.svelte b/apps/frontend/src/lib/components/blocks/field-options/field-options.svelte index 60789b924..252eb6323 100644 --- a/apps/frontend/src/lib/components/blocks/field-options/field-options.svelte +++ b/apps/frontend/src/lib/components/blocks/field-options/field-options.svelte @@ -18,6 +18,7 @@ import CurrencyFieldOption from "./currency-field-option.svelte" import ButtonFieldOption from "./button-field-option.svelte" import DurationFieldOption from "./duration-field-option.svelte" + import PercentageFieldOption from "./percentage-field-option.svelte" export let constraint: IFieldConstraint | undefined export let option: any | undefined @@ -45,6 +46,7 @@ json: JsonFieldOption, date: DateFieldOption, duration: DurationFieldOption, + percentage: PercentageFieldOption, } export let type: NoneSystemFieldType diff --git a/apps/frontend/src/lib/components/blocks/field-options/percentage-field-option.svelte b/apps/frontend/src/lib/components/blocks/field-options/percentage-field-option.svelte new file mode 100644 index 000000000..f7e3ecfc5 --- /dev/null +++ b/apps/frontend/src/lib/components/blocks/field-options/percentage-field-option.svelte @@ -0,0 +1,78 @@ + + +
+
+ + +
+ + {#if !isDefaultValueValid} + + Invalid default value + Your default value is invalid. Default value will not be saved. + + {/if} + {#if constraint} +
+
+ + +
+
+ + +
+
+ +
+ +
+
+ + +
+ +
+ + +
+ {/if} +
diff --git a/apps/frontend/src/lib/components/blocks/filters-editor/filter-input.svelte b/apps/frontend/src/lib/components/blocks/filters-editor/filter-input.svelte index af0e2b347..ce6bd8cda 100644 --- a/apps/frontend/src/lib/components/blocks/filters-editor/filter-input.svelte +++ b/apps/frontend/src/lib/components/blocks/filters-editor/filter-input.svelte @@ -15,6 +15,7 @@ type ILongTextFieldConditionOp, type INumberFieldConditionOp, type IOpType, + type IPercentageFieldConditionOp, type IRatingFieldConditionOp, type ISelectFieldConditionOp, type IStringFieldConditionOp, @@ -257,6 +258,13 @@ is_not_empty: null, } + const percentage: Record = { + eq: NumberInput, + neq: NumberInput, + is_empty: null, + is_not_empty: null, + } + $: filterFieldInput = { string, number, @@ -277,6 +285,7 @@ url, longText, duration, + percentage, } diff --git a/apps/frontend/src/lib/components/blocks/grid-view/editable-cell/percentage-cell.svelte b/apps/frontend/src/lib/components/blocks/grid-view/editable-cell/percentage-cell.svelte new file mode 100644 index 000000000..1b191ef69 --- /dev/null +++ b/apps/frontend/src/lib/components/blocks/grid-view/editable-cell/percentage-cell.svelte @@ -0,0 +1,62 @@ + + + + +
+ {#if isEditing} + + {:else if isNumber(value) || value === undefined || value === null} + + {/if} +
+
+ +

{value}

+
+
diff --git a/apps/frontend/src/lib/components/blocks/grid-view/grid-view-cell.svelte b/apps/frontend/src/lib/components/blocks/grid-view/grid-view-cell.svelte index 6181d4f5d..d917d61cd 100644 --- a/apps/frontend/src/lib/components/blocks/grid-view/grid-view-cell.svelte +++ b/apps/frontend/src/lib/components/blocks/grid-view/grid-view-cell.svelte @@ -26,6 +26,7 @@ import CurrencyCell from "./editable-cell/currency-cell.svelte" import ButtonCell from "./editable-cell/button-cell.svelte" import DurationCell from "./editable-cell/duration-cell.svelte" + import PercentageCell from "./editable-cell/percentage-cell.svelte" const table = getTable() @@ -64,6 +65,7 @@ attachment: AttachmentCell, user: UserCell, duration: DurationCell, + percentage: PercentageCell, } diff --git a/apps/frontend/src/lib/components/blocks/record-detail/record-detail.svelte b/apps/frontend/src/lib/components/blocks/record-detail/record-detail.svelte index acf882fa0..3ec715b2b 100644 --- a/apps/frontend/src/lib/components/blocks/record-detail/record-detail.svelte +++ b/apps/frontend/src/lib/components/blocks/record-detail/record-detail.svelte @@ -129,7 +129,7 @@ {/if}
-
+
{#if field.isSystem || !field.isMutable} = { currency: "Currency", duration: "Duration", button: "Button", + percentage: "Percentage", } const rollupFns: Record = { diff --git a/packages/i18n/src/i18n/i18n-types.ts b/packages/i18n/src/i18n/i18n-types.ts index ea9df0221..171541674 100644 --- a/packages/i18n/src/i18n/i18n-types.ts +++ b/packages/i18n/src/i18n/i18n-types.ts @@ -261,6 +261,10 @@ type RootTranslation = { * B​u​t​t​o​n */ button: string + /** + * P​e​r​c​e​n​t​a​g​e + */ + percentage: string } rollupFns: { /** @@ -626,6 +630,10 @@ export type TranslationFunctions = { * Button */ button: () => LocalizedString + /** + * Percentage + */ + percentage: () => LocalizedString } rollupFns: { /** diff --git a/packages/persistence/src/record/record-query-creator-visitor.ts b/packages/persistence/src/record/record-query-creator-visitor.ts index 00f205e18..9cb49e7b5 100644 --- a/packages/persistence/src/record/record-query-creator-visitor.ts +++ b/packages/persistence/src/record/record-query-creator-visitor.ts @@ -1,5 +1,6 @@ import { ID_TYPE, + PercentageField, type AttachmentField, type AutoIncrementField, type ButtonField, @@ -84,6 +85,7 @@ export class RecordQueryCreatorVisitor implements IFieldVisitor { longText(field: LongTextField): void {} button(field: ButtonField): void {} duration(field: DurationField): void {} + percentage(field: PercentageField): void {} user(field: UserField): void { if (field.isMultiple) { const usersTable = getTableName(users) diff --git a/packages/persistence/src/record/record-query-spec-creator-visitor.ts b/packages/persistence/src/record/record-query-spec-creator-visitor.ts index 2d13035fd..2de7d80ba 100644 --- a/packages/persistence/src/record/record-query-spec-creator-visitor.ts +++ b/packages/persistence/src/record/record-query-spec-creator-visitor.ts @@ -4,6 +4,7 @@ import { ID_TYPE, JsonContains, LongTextEqual, + PercentageEqual, UrlEqual, type AttachmentEmpty, type AttachmentEqual, @@ -65,6 +66,7 @@ export class RecordQuerySpecCreatorVisitor implements IRecordVisitor { return this.#creator || this.qb } + percentageEqual(s: PercentageEqual): void {} longTextEqual(s: LongTextEqual): void {} stringEqual(spec: StringEqual): void {} stringContains(spec: StringContains): void {} diff --git a/packages/persistence/src/record/record-reference-visitor.ts b/packages/persistence/src/record/record-reference-visitor.ts index 0792694f1..3432c7947 100644 --- a/packages/persistence/src/record/record-reference-visitor.ts +++ b/packages/persistence/src/record/record-reference-visitor.ts @@ -1,5 +1,6 @@ import { ID_TYPE, + PercentageField, type AttachmentField, type AutoIncrementField, type ButtonField, @@ -88,6 +89,9 @@ export class RecordReferenceVisitor implements IFieldVisitor { reference(field: ReferenceField): void { this.qb = this.qb.leftJoin(field.id.value, `${this.table.id.value}.${ID_TYPE}`, `${field.id.value}.${ID_TYPE}`) } + percentage(field: PercentageField): void { + throw new Error("Method not implemented.") + } attachment(field: AttachmentField): void { throw new Error("Method not implemented.") } diff --git a/packages/persistence/src/record/record-select-field-visitor.ts b/packages/persistence/src/record/record-select-field-visitor.ts index a1258314d..a0f6ccde8 100644 --- a/packages/persistence/src/record/record-select-field-visitor.ts +++ b/packages/persistence/src/record/record-select-field-visitor.ts @@ -2,6 +2,7 @@ import { ButtonField, DurationField, ID_TYPE, + PercentageField, type AttachmentField, type AutoIncrementField, type CheckboxField, @@ -137,6 +138,9 @@ export class RecordSelectFieldVisitor implements IFieldVisitor { duration(field: DurationField): void { this.addSelect(this.getField(field.id.value)) } + percentage(field: PercentageField): void { + this.addSelect(this.getField(field.id.value)) + } reference(field: ReferenceField): void { const select = `${field.id.value}.${field.id.value} as ${field.id.value}` this.addSelect(select) diff --git a/packages/persistence/src/record/record-spec-reference-visitor.ts b/packages/persistence/src/record/record-spec-reference-visitor.ts index 79bc84524..5c4dbc291 100644 --- a/packages/persistence/src/record/record-spec-reference-visitor.ts +++ b/packages/persistence/src/record/record-spec-reference-visitor.ts @@ -25,6 +25,7 @@ import { NumberGTE, NumberLT, NumberLTE, + PercentageEqual, RatingEqual, ReferenceEqual, SelectContainsAnyOf, @@ -56,6 +57,7 @@ export class RecordSpecReferenceVisitor implements IRecordVisitor { return this.qb } + percentageEqual(s: PercentageEqual): void {} stringEqual(spec: StringEqual): void {} longTextEqual(spec: LongTextEqual): void {} stringContains(spec: StringContains): void {} diff --git a/packages/persistence/src/record/record.filter-visitor.ts b/packages/persistence/src/record/record.filter-visitor.ts index 45034956e..5906670d3 100644 --- a/packages/persistence/src/record/record.filter-visitor.ts +++ b/packages/persistence/src/record/record.filter-visitor.ts @@ -2,6 +2,7 @@ import { getCurrentUserId } from "@undb/context/server" import { NotImplementException } from "@undb/domain" import { DurationEqual, + PercentageEqual, SelectField, isUserFieldMacro, type AttachmentEmpty, @@ -153,6 +154,10 @@ export class RecordFilterVisitor extends AbstractQBVisitor implements const cond = this.eb.eb(this.getFieldId(spec), "=", spec.value === null ? null : spec.value * 100) this.addCond(cond) } + percentageEqual(spec: PercentageEqual): void { + const cond = this.eb.eb(this.getFieldId(spec), "=", spec.value) + this.addCond(cond) + } stringMin(spec: StringMin): void { const cond = this.eb.eb(this.eb.fn("LENGTH", [this.getFieldId(spec)]), ">=", spec.min) this.addCond(cond) diff --git a/packages/persistence/src/record/record.mutate-visitor.ts b/packages/persistence/src/record/record.mutate-visitor.ts index ec6a04edf..993ac681f 100644 --- a/packages/persistence/src/record/record.mutate-visitor.ts +++ b/packages/persistence/src/record/record.mutate-visitor.ts @@ -8,6 +8,7 @@ import { isUserFieldMacro, JsonContains, LongTextEqual, + PercentageEqual, SelectContainsAnyOf, SelectField, SelectFieldValue, @@ -275,6 +276,9 @@ export class RecordMutateVisitor extends AbstractQBMutationVisitor implements IR stringEmpty(spec: StringEmpty): void { throw new Error("Method not implemented.") } + percentageEqual(s: PercentageEqual): void { + this.setData(s.fieldId.value, s.value) + } selectEqual(spec: SelectEqual): void { const field = this.table.schema.getFieldById(spec.fieldId).expect("No field found") as SelectField const fieldValue = new SelectFieldValue(spec.value) diff --git a/packages/persistence/src/underlying/underlying-table-field.visitor.ts b/packages/persistence/src/underlying/underlying-table-field.visitor.ts index 007b960f5..29e5d54af 100644 --- a/packages/persistence/src/underlying/underlying-table-field.visitor.ts +++ b/packages/persistence/src/underlying/underlying-table-field.visitor.ts @@ -8,6 +8,7 @@ import { ID_TYPE, JsonField, LongTextField, + PercentageField, RatingField, ReferenceField, RollupField, @@ -138,6 +139,10 @@ export class UnderlyingTableFieldVisitor const c = this.tb.addColumn(field.id.value, "json") this.addColumn(c) } + percentage(field: PercentageField): void { + const c = this.tb.addColumn(field.id.value, "real") + this.addColumn(c) + } reference(field: ReferenceField): void { const joinTable = new JoinTable(this.t.table, field) diff --git a/packages/table/src/modules/records/record/record-visitor.interface.ts b/packages/table/src/modules/records/record/record-visitor.interface.ts index d22affb05..502c60ce1 100644 --- a/packages/table/src/modules/records/record/record-visitor.interface.ts +++ b/packages/table/src/modules/records/record/record-visitor.interface.ts @@ -6,6 +6,7 @@ import type { IJsonFieldValueVisitor, ILongTextFieldValueVisitor, INumberFieldValueVisitor, + IPercentageFieldValueVisitor, IReferenceFieldValueVisitor, IUpdatedAtFieldValueVisitor, IUrlFieldValueVisitor, @@ -40,4 +41,5 @@ export type IRecordVisitor = IStringFieldValueVisitor & ILongTextFieldValueVisitor & ICurrencyFieldValueVisitor & IDurationFieldValueVisitor & + IPercentageFieldValueVisitor & ISpecVisitor diff --git a/packages/table/src/modules/schema/fields/dto/create-field.dto.ts b/packages/table/src/modules/schema/fields/dto/create-field.dto.ts index 163e04840..f876f1c61 100644 --- a/packages/table/src/modules/schema/fields/dto/create-field.dto.ts +++ b/packages/table/src/modules/schema/fields/dto/create-field.dto.ts @@ -1,22 +1,21 @@ import { z } from "@undb/zod" -import { - createButtonFieldDTO, - createDateFieldDTO, - createDurationFieldDTO, - createJsonFieldDTO, - createUrlFieldDTO, -} from "../variants" import { createAttachmentFieldDTO } from "../variants/attachment-field" +import { createButtonFieldDTO } from "../variants/button-field/button-field.vo" import { createCheckboxFieldDTO } from "../variants/checkbox-field" import { createCurrencyFieldDTO } from "../variants/currency-field" +import { createDateFieldDTO } from "../variants/date-field/date-field.vo" +import { createDurationFieldDTO } from "../variants/duration-field/duration-field.vo" import { createEmailFieldDTO } from "../variants/email-field" +import { createJsonFieldDTO } from "../variants/json-field/json-field.vo" import { createLongTextFieldDTO } from "../variants/long-text-field" import { createNumberFieldDTO } from "../variants/number-field/number-field.vo" +import { createPercentageFieldDTO } from "../variants/percentage-field/percentage-field.vo" import { createRatingFieldDTO } from "../variants/rating-field/rating-field.vo" import { createReferenceFieldDTO } from "../variants/reference-field/reference-field.vo" import { createRollupFieldDTO } from "../variants/rollup-field/rollup-field.vo" import { createSelectFieldDTO } from "../variants/select-field/select-field.vo" import { createStringFieldDTO } from "../variants/string-field/string-field.vo" +import { createUrlFieldDTO } from "../variants/url-field/url-field.vo" import { createUserFieldDTO } from "../variants/user-field" export const createFieldDTO = z.discriminatedUnion("type", [ @@ -37,6 +36,7 @@ export const createFieldDTO = z.discriminatedUnion("type", [ createCurrencyFieldDTO, createButtonFieldDTO, createDurationFieldDTO, + createPercentageFieldDTO, ]) export type ICreateFieldDTO = z.infer diff --git a/packages/table/src/modules/schema/fields/dto/field.dto.ts b/packages/table/src/modules/schema/fields/dto/field.dto.ts index 91861b038..b96b80dd9 100644 --- a/packages/table/src/modules/schema/fields/dto/field.dto.ts +++ b/packages/table/src/modules/schema/fields/dto/field.dto.ts @@ -8,6 +8,7 @@ import { dateFieldDTO, durationFieldDTO, jsonFieldDTO, + percentageFieldDTO, referenceFieldDTO, rollupFieldDTO, selectFieldDTO, @@ -53,6 +54,7 @@ export const fieldDTO = z.discriminatedUnion("type", [ currencyFieldDTO, buttonFieldDTO, durationFieldDTO, + percentageFieldDTO, ]) export type IFieldDTO = z.infer diff --git a/packages/table/src/modules/schema/fields/dto/update-field.dto.ts b/packages/table/src/modules/schema/fields/dto/update-field.dto.ts index ca003aaa0..f8b2d0468 100644 --- a/packages/table/src/modules/schema/fields/dto/update-field.dto.ts +++ b/packages/table/src/modules/schema/fields/dto/update-field.dto.ts @@ -12,6 +12,7 @@ import { updateEmailFieldDTO } from "../variants/email-field" import { updateIdFieldDTO } from "../variants/id-field/id-field.vo" import { updateJsonFieldDTO } from "../variants/json-field/json-field.vo" import { updateNumberFieldDTO } from "../variants/number-field/number-field.vo" +import { updatePercentageFieldDTO } from "../variants/percentage-field/percentage-field.vo" import { updateRatingFieldDTO } from "../variants/rating-field/rating-field.vo" import { updateReferenceFieldDTO } from "../variants/reference-field/reference-field.vo" import { updateRollupFieldDTO } from "../variants/rollup-field/rollup-field.vo" @@ -43,6 +44,7 @@ export const updateFieldDTO = z.discriminatedUnion("type", [ updateCurrencyFieldDTO, updateButtonFieldDTO, updateDurationFieldDTO, + updatePercentageFieldDTO, ]) export type IUpdateFieldDTO = z.infer diff --git a/packages/table/src/modules/schema/fields/field-value.factory.ts b/packages/table/src/modules/schema/fields/field-value.factory.ts index 118ca5a2b..52299b7e7 100644 --- a/packages/table/src/modules/schema/fields/field-value.factory.ts +++ b/packages/table/src/modules/schema/fields/field-value.factory.ts @@ -25,6 +25,7 @@ import { CreatedByFieldValue } from "./variants/created-by-field" import { CurrencyFieldValue } from "./variants/currency-field" import { EmailFieldValue } from "./variants/email-field" import { LongTextFieldValue } from "./variants/long-text-field/long-text-field-value.vo" +import { PercentageFieldValue } from "./variants/percentage-field" // 新增导入 import { RatingFieldValue } from "./variants/rating-field" import { SelectFieldValue } from "./variants/select-field" import { UserFieldValue } from "./variants/user-field" @@ -48,6 +49,7 @@ export class FieldValueFactory { .with({ type: "currency" }, (field) => Some(new CurrencyFieldValue(field.valueSchema.parse(value)))) .with({ type: "button" }, () => Some(new ButtonFieldValue(null))) .with({ type: "duration" }, (field) => Some(new DurationFieldValue(field.valueSchema.parse(value)))) + .with({ type: "percentage" }, (field) => Some(new PercentageFieldValue(field.valueSchema.parse(value)))) // 新增 percentage 处理 .otherwise(() => None) } @@ -76,6 +78,7 @@ export class FieldValueFactory { .with("currency", () => Some(new CurrencyFieldValue(value as number))) .with("button", () => Some(new ButtonFieldValue(null))) .with("duration", () => Some(new DurationFieldValue(value as number))) + .with("percentage", () => Some(new PercentageFieldValue(value as number))) // 新增 percentage 处理 .exhaustive() } } diff --git a/packages/table/src/modules/schema/fields/field.aggregate.ts b/packages/table/src/modules/schema/fields/field.aggregate.ts index 7c39601ac..905a35a8f 100644 --- a/packages/table/src/modules/schema/fields/field.aggregate.ts +++ b/packages/table/src/modules/schema/fields/field.aggregate.ts @@ -1,5 +1,5 @@ import type { z } from "@undb/zod" -import { referenceFieldAggregate, rollupFieldAggregate } from "../.." +import { referenceFieldAggregate,rollupFieldAggregate } from "../.." import { abstractDateAggregate } from "./variants/abstractions/abstract-date.aggregate" import { abstractNumberAggregate } from "./variants/abstractions/abstract-number.aggregate" import { abstractUserAggregate } from "./variants/abstractions/abstract-user.aggregate" @@ -9,6 +9,7 @@ import { durationFieldAggregate } from "./variants/duration-field/duration-field import { emailFieldAggregate } from "./variants/email-field/email-field.aggregate" import { jsonFieldAggregate } from "./variants/json-field/json-field.aggregate" import { longTextFieldAggregate } from "./variants/long-text-field/long-text-field.aggregate" +import { percentageFieldAggregate } from "./variants/percentage-field/percentage-field.aggregate" import { stringFieldAggregate } from "./variants/string-field/string-field.aggregate" import { urlFieldAggregate } from "./variants/url-field/url-field.aggregate" import { userFieldAggregate } from "./variants/user-field/user-field.aggregate" @@ -27,5 +28,6 @@ export const fieldAggregate = stringFieldAggregate .or(longTextFieldAggregate) .or(currencyFieldAggregate) .or(durationFieldAggregate) + .or(percentageFieldAggregate) export type IFieldAggregate = z.infer diff --git a/packages/table/src/modules/schema/fields/field.factory.ts b/packages/table/src/modules/schema/fields/field.factory.ts index 44609c630..1ef657681 100644 --- a/packages/table/src/modules/schema/fields/field.factory.ts +++ b/packages/table/src/modules/schema/fields/field.factory.ts @@ -17,6 +17,7 @@ import { IdField } from "./variants/id-field/id-field.vo" import { JsonField } from "./variants/json-field/json-field.vo" import { LongTextField } from "./variants/long-text-field" import { NumberField } from "./variants/number-field/number-field.vo" +import { PercentageField } from "./variants/percentage-field/percentage-field.vo" import { RatingField } from "./variants/rating-field/rating-field.vo" import { ReferenceField } from "./variants/reference-field/reference-field.vo" import { RollupField } from "./variants/rollup-field/rollup-field.vo" @@ -52,6 +53,7 @@ export class FieldFactory { .with({ type: "currency" }, (dto) => new CurrencyField(dto)) .with({ type: "button" }, (dto) => new ButtonField(dto)) .with({ type: "duration" }, (dto) => new DurationField(dto)) + .with({ type: "percentage" }, (dto) => new PercentageField(dto)) // 新增匹配 .exhaustive() } @@ -74,6 +76,7 @@ export class FieldFactory { .with({ type: "currency" }, (dto) => CurrencyField.create(dto)) .with({ type: "button" }, (dto) => ButtonField.create(dto)) .with({ type: "duration" }, (dto) => DurationField.create(dto)) + .with({ type: "percentage" }, (dto) => PercentageField.create(dto)) // 新增匹配 .otherwise(() => { throw new Error("Field type creation not supported") }) diff --git a/packages/table/src/modules/schema/fields/field.type.ts b/packages/table/src/modules/schema/fields/field.type.ts index b2e1e725b..4a380c56a 100644 --- a/packages/table/src/modules/schema/fields/field.type.ts +++ b/packages/table/src/modules/schema/fields/field.type.ts @@ -106,6 +106,13 @@ import { import type { INumberFieldConstraint } from "./variants/number-field/number-field-constraint.vo" import type { NumberFieldValue } from "./variants/number-field/number-field-value.vo" import type { NUMBER_TYPE, NumberField } from "./variants/number-field/number-field.vo" +import type { + IPercentageFieldConditionSchema, + IPercentageFieldConstraint, + PERCENTAGE_TYPE, + PercentageField, + PercentageFieldValue, +} from "./variants/percentage-field" import type { IRatingFieldConditionSchema, IRatingFieldConstraint, @@ -156,6 +163,7 @@ export type Field = | CurrencyField | ButtonField | DurationField + | PercentageField export type SystemField = | IdField @@ -191,6 +199,7 @@ export type FieldValue = | CurrencyFieldValue | ButtonFieldValue | DurationFieldValue + | PercentageFieldValue export type MutableFieldValue = | StringFieldValue @@ -208,6 +217,7 @@ export type MutableFieldValue = | LongTextFieldValue | CurrencyFieldValue | DurationFieldValue + | PercentageFieldValue export type FieldType = | typeof STRING_TYPE @@ -233,6 +243,7 @@ export type FieldType = | typeof CURRENCY_TYPE | typeof BUTTON_TYPE | typeof DURATION_TYPE + | typeof PERCENTAGE_TYPE export type NoneSystemFieldType = Exclude< FieldType, @@ -266,6 +277,7 @@ export type IFieldConditionSchema = | ILongTextFieldConditionSchema | ICurrencyFieldConditionSchema | IDurationFieldConditionSchema + | IPercentageFieldConditionSchema | z.ZodUnion export type SystemFieldType = Exclude @@ -288,6 +300,7 @@ export type IFieldConstraint = | ILongTextFieldConstraint | ICurrencyFieldConstraint | IDurationFieldConstraint + | IPercentageFieldConstraint export type IFieldOption = | IReferenceFieldOption diff --git a/packages/table/src/modules/schema/fields/field.util.ts b/packages/table/src/modules/schema/fields/field.util.ts index 6ca1d4f40..edcc07188 100644 --- a/packages/table/src/modules/schema/fields/field.util.ts +++ b/packages/table/src/modules/schema/fields/field.util.ts @@ -78,6 +78,7 @@ const sortableFieldTypes: FieldType[] = [ "checkbox", "url", "duration", + "percentage", ] as const export function isFieldSortable(type: FieldType): boolean { @@ -107,6 +108,7 @@ export const fieldTypes: NoneSystemFieldType[] = [ "currency", "button", "duration", + "percentage", ] as const export const systemFieldTypes: SystemFieldType[] = [ @@ -139,6 +141,7 @@ export const filterableFieldTypes = [ "json", "currency", "duration", + "percentage", ] as const export const getIsFilterableFieldType = (type: FieldType): type is IFilterableFieldType => { @@ -161,6 +164,7 @@ export const mutableFieldTypes = [ "longText", "currency", "duration", + "percentage", ] as const export const getIsMutableFieldType = (type: FieldType) => mutableFieldTypes.includes(type as any) @@ -177,6 +181,7 @@ export const fieldsCanBeRollup: FieldType[] = [ "checkbox", "currency", "duration", + "percentage", ] as const export const getIsFieldCanBeRollup = (type: FieldType): type is "number" => { @@ -186,7 +191,14 @@ export const getIsFieldCanBeRollup = (type: FieldType): type is "number" => { export function getRollupFnByType(type: FieldType): IRollupFn[] { return match(type) .returnType() - .with("number", "rating", "currency", "duration", () => ["sum", "average", "max", "min", "count", "lookup"]) + .with("number", "rating", "currency", "duration", "percentage", () => [ + "sum", + "average", + "max", + "min", + "count", + "lookup", + ]) .with("date", () => ["max", "min", "count", "lookup"]) .with("string", "email", "url", "checkbox", () => ["lookup", "count"]) .otherwise(() => []) @@ -222,6 +234,7 @@ export const displayFieldTypes: FieldType[] = [ "rating", "currency", "duration", + "percentage", ] as const export const getIsDisplayFieldType = (type: FieldType): type is (typeof displayFieldTypes)[number] => { diff --git a/packages/table/src/modules/schema/fields/field.visitor.ts b/packages/table/src/modules/schema/fields/field.visitor.ts index 21b937c78..6ccb38205 100644 --- a/packages/table/src/modules/schema/fields/field.visitor.ts +++ b/packages/table/src/modules/schema/fields/field.visitor.ts @@ -1,16 +1,18 @@ -import type { ButtonField, DurationField, UrlField } from "." import type { AttachmentField } from "./variants/attachment-field" import type { AutoIncrementField } from "./variants/autoincrement-field" +import type { ButtonField } from "./variants/button-field/button-field.vo" import type { CheckboxField } from "./variants/checkbox-field" import type { CreatedAtField } from "./variants/created-at-field" import type { CreatedByField } from "./variants/created-by-field" import type { CurrencyField } from "./variants/currency-field" import type { DateField } from "./variants/date-field" +import type { DurationField } from "./variants/duration-field/duration-field.vo" import type { EmailField } from "./variants/email-field" import type { IdField } from "./variants/id-field/id-field.vo" import type { JsonField } from "./variants/json-field" import type { LongTextField } from "./variants/long-text-field" import type { NumberField } from "./variants/number-field/number-field.vo" +import type { PercentageField } from "./variants/percentage-field/percentage-field.vo" import type { RatingField } from "./variants/rating-field" import type { ReferenceField } from "./variants/reference-field/reference-field.vo" import type { RollupField } from "./variants/rollup-field" @@ -18,6 +20,7 @@ import type { SelectField } from "./variants/select-field" import type { StringField } from "./variants/string-field/string-field.vo" import type { UpdatedAtField } from "./variants/updated-at-field/updated-at-field.vo" import type { UpdatedByField } from "./variants/updated-by-field/updated-by-field.vo" +import type { UrlField } from "./variants/url-field/url-field.vo" import type { UserField } from "./variants/user-field" export interface IFieldVisitor { @@ -42,6 +45,7 @@ export interface IFieldVisitor { currency(field: CurrencyField): void button(field: ButtonField): void duration(field: DurationField): void + percentage(field: PercentageField): void reference(field: ReferenceField): void rollup(field: RollupField): void diff --git a/packages/table/src/modules/schema/fields/variants/index.ts b/packages/table/src/modules/schema/fields/variants/index.ts index 928622e0f..7b5e8944c 100644 --- a/packages/table/src/modules/schema/fields/variants/index.ts +++ b/packages/table/src/modules/schema/fields/variants/index.ts @@ -14,6 +14,7 @@ export * from "./id-field" export * from "./json-field" export * from "./long-text-field" export * from "./number-field" +export * from "./percentage-field" export * from "./rating-field" export * from "./select-field" export * from "./string-field" diff --git a/packages/table/src/modules/schema/fields/variants/percentage-field/index.ts b/packages/table/src/modules/schema/fields/variants/percentage-field/index.ts new file mode 100644 index 000000000..ec6d35588 --- /dev/null +++ b/packages/table/src/modules/schema/fields/variants/percentage-field/index.ts @@ -0,0 +1,6 @@ +export * from "./percentage-field-constraint.vo" +export * from "./percentage-field-value.visitor" +export * from "./percentage-field-value.vo" +export * from "./percentage-field.condition" +export * from "./percentage-field.specification" +export * from "./percentage-field.vo" diff --git a/packages/table/src/modules/schema/fields/variants/percentage-field/percentage-field-constraint.vo.ts b/packages/table/src/modules/schema/fields/variants/percentage-field/percentage-field-constraint.vo.ts new file mode 100644 index 000000000..799021e64 --- /dev/null +++ b/packages/table/src/modules/schema/fields/variants/percentage-field/percentage-field-constraint.vo.ts @@ -0,0 +1,46 @@ +import { Some } from "@undb/domain" +import { z } from "@undb/zod" +import { FieldConstraintVO, baseFieldConstraint } from "../../field-constraint.vo" + +export const percentageFieldConstraint = baseFieldConstraint + .partial() + .merge( + z.object({ + min: z.number().nonnegative(), + max: z.number().nonnegative(), + }), + ) + .partial() + .refine((v) => v.min === undefined || v.max === undefined || v.min <= v.max, { + message: "min should be less than or equal to max", + }) + +export type IPercentageFieldConstraint = z.infer + +export class PercentageFieldConstraint extends FieldConstraintVO { + constructor(dto: IPercentageFieldConstraint) { + super({ + required: dto.required, + min: dto.min, + max: dto.max, + }) + } + override get schema() { + let base: z.ZodTypeAny = z.number().nonnegative() + if (!this.props.required) { + base = base.optional().nullable() + } + if (this.props.min) { + base = base.and(z.number().min(this.props.min)) + } + if (this.props.max) { + base = base.and(z.number().max(this.props.max)) + } + + return base + } + + override get mutateSchema() { + return Some(this.schema) + } +} diff --git a/packages/table/src/modules/schema/fields/variants/percentage-field/percentage-field-value.visitor.ts b/packages/table/src/modules/schema/fields/variants/percentage-field/percentage-field-value.visitor.ts new file mode 100644 index 000000000..d035385d2 --- /dev/null +++ b/packages/table/src/modules/schema/fields/variants/percentage-field/percentage-field-value.visitor.ts @@ -0,0 +1,5 @@ +import type { PercentageEqual } from "./percentage-field.specification" + +export interface IPercentageFieldValueVisitor { + percentageEqual(s: PercentageEqual): void +} diff --git a/packages/table/src/modules/schema/fields/variants/percentage-field/percentage-field-value.vo.ts b/packages/table/src/modules/schema/fields/variants/percentage-field/percentage-field-value.vo.ts new file mode 100644 index 000000000..c949e3518 --- /dev/null +++ b/packages/table/src/modules/schema/fields/variants/percentage-field/percentage-field-value.vo.ts @@ -0,0 +1,15 @@ +import { z } from "@undb/zod" +import { FieldValueObject } from "../../field-value" + +export const percentageFieldValue = z.number().nonnegative().nullable() +export type IPercentageFieldValue = z.infer + +export class PercentageFieldValue extends FieldValueObject { + constructor(value: IPercentageFieldValue) { + super({ value: value ?? null }) + } + + isEmpty() { + return this.props?.value === null || this.props?.value === undefined + } +} diff --git a/packages/table/src/modules/schema/fields/variants/percentage-field/percentage-field.aggregate.ts b/packages/table/src/modules/schema/fields/variants/percentage-field/percentage-field.aggregate.ts new file mode 100644 index 000000000..a8635f578 --- /dev/null +++ b/packages/table/src/modules/schema/fields/variants/percentage-field/percentage-field.aggregate.ts @@ -0,0 +1,11 @@ +import { z } from "@undb/zod" + +export const percentageFieldAggregate = z.enum([ + // + "count_empty", + "count_uniq", + "count_not_empty", + "percent_empty", + "percent_not_empty", + "percent_uniq", +]) diff --git a/packages/table/src/modules/schema/fields/variants/percentage-field/percentage-field.condition.ts b/packages/table/src/modules/schema/fields/variants/percentage-field/percentage-field.condition.ts new file mode 100644 index 000000000..deb03379c --- /dev/null +++ b/packages/table/src/modules/schema/fields/variants/percentage-field/percentage-field.condition.ts @@ -0,0 +1,18 @@ +import { z } from "@undb/zod" +import { createBaseConditionSchema } from "../../condition/base.condition" + +export function createPercentageFieldCondition(itemType: ItemType) { + const base = createBaseConditionSchema(itemType) + return z.union([ + z.object({ op: z.literal("eq"), value: z.number() }).merge(base), + z.object({ op: z.literal("neq"), value: z.number() }).merge(base), + // TODO: gt lt etc + z.object({ op: z.literal("is_empty"), value: z.undefined() }).merge(base), + z.object({ op: z.literal("is_not_empty"), value: z.undefined() }).merge(base), + ]) +} + +export type IPercentageFieldConditionSchema = ReturnType +export type IPercentageFieldCondition = z.infer + +export type IPercentageFieldConditionOp = IPercentageFieldCondition["op"] diff --git a/packages/table/src/modules/schema/fields/variants/percentage-field/percentage-field.specification.ts b/packages/table/src/modules/schema/fields/variants/percentage-field/percentage-field.specification.ts new file mode 100644 index 000000000..f81a20016 --- /dev/null +++ b/packages/table/src/modules/schema/fields/variants/percentage-field/percentage-field.specification.ts @@ -0,0 +1,27 @@ +import { Ok, type Result } from "@undb/domain" +import { isNumber } from "radash" +import type { IRecordVisitor, RecordDO } from "../../../../records" +import { RecordComositeSpecification } from "../../../../records/record/record.composite-specification" +import type { FieldId } from "../../field-id.vo" +import { PercentageFieldValue } from "./percentage-field-value.vo" + +export class PercentageEqual extends RecordComositeSpecification { + constructor( + readonly value: number | null, + readonly fieldId: FieldId, + ) { + super(fieldId) + } + isSatisfiedBy(t: RecordDO): boolean { + const value = t.getValue(this.fieldId) + return value.mapOr(false, (v) => isNumber(v.value) && v.value == this.value) + } + mutate(t: RecordDO): Result { + t.values.setValue(this.fieldId, new PercentageFieldValue(this.value)) + return Ok(t) + } + accept(v: IRecordVisitor): Result { + v.percentageEqual(this) + return Ok(undefined) + } +} diff --git a/packages/table/src/modules/schema/fields/variants/percentage-field/percentage-field.vo.ts b/packages/table/src/modules/schema/fields/variants/percentage-field/percentage-field.vo.ts new file mode 100644 index 000000000..03d42faa7 --- /dev/null +++ b/packages/table/src/modules/schema/fields/variants/percentage-field/percentage-field.vo.ts @@ -0,0 +1,111 @@ +import { Option, Some } from "@undb/domain" +import { z } from "@undb/zod" +import { match } from "ts-pattern" +import type { RecordComositeSpecification } from "../../../../records/record/record.composite-specification" +import { fieldId, FieldIdVo } from "../../field-id.vo" +import type { IFieldVisitor } from "../../field.visitor" +import { AbstractField, baseFieldDTO, createBaseFieldDTO } from "../abstract-field.vo" +import { StringEmpty } from "../string-field" +import { PercentageFieldConstraint, percentageFieldConstraint } from "./percentage-field-constraint.vo" +import { percentageFieldValue, PercentageFieldValue } from "./percentage-field-value.vo" +import { percentageFieldAggregate } from "./percentage-field.aggregate" +import { + createPercentageFieldCondition, + type IPercentageFieldCondition, + type IPercentageFieldConditionSchema, +} from "./percentage-field.condition" +import { PercentageEqual } from "./percentage-field.specification" + +export const PERCENTAGE_TYPE = "percentage" as const + +export const createPercentageFieldDTO = createBaseFieldDTO.extend({ + type: z.literal(PERCENTAGE_TYPE), + constraint: percentageFieldConstraint.optional(), + defaultValue: percentageFieldValue.optional(), +}) + +export type ICreatePercentageFieldDTO = z.infer + +export const updatePercentageFieldDTO = createPercentageFieldDTO.setKey("id", fieldId) +export type IUpdatePercentageFieldDTO = z.infer + +export const percentageFieldDTO = baseFieldDTO.extend({ + type: z.literal(PERCENTAGE_TYPE), + constraint: percentageFieldConstraint.optional(), + defaultValue: percentageFieldValue.optional(), +}) + +export type IPercentageFieldDTO = z.infer + +export class PercentageField extends AbstractField { + constructor(dto: IPercentageFieldDTO) { + super(dto) + if (dto.constraint) { + this.constraint = Some(new PercentageFieldConstraint(dto.constraint)) + } + if (dto.defaultValue) { + this.defaultValue = new PercentageFieldValue(dto.defaultValue) + } + } + + static create(dto: ICreatePercentageFieldDTO) { + const field = new PercentageField({ ...dto, id: FieldIdVo.fromStringOrCreate(dto.id).value }) + if (dto.defaultValue) { + field.defaultValue = new PercentageFieldValue(dto.defaultValue) + } + return field + } + + override type = PERCENTAGE_TYPE + + override get #constraint(): PercentageFieldConstraint { + return this.constraint.unwrapOrElse(() => new PercentageFieldConstraint({})) + } + + get symbol() { + return this.option.unwrapOrElse(() => ({ symbol: "$" })).symbol + } + + get max() { + return this.#constraint.props.max + } + + get min() { + return this.#constraint.props.min + } + + override get valueSchema() { + return this.#constraint.schema + } + + override get mutateSchema() { + return this.#constraint.mutateSchema + } + + override accept(visitor: IFieldVisitor): void { + visitor.percentage(this) + } + + override getSpec(condition: IPercentageFieldCondition) { + const spec = match(condition) + .with({ op: "eq" }, ({ value }) => new PercentageEqual(value, this.id)) + .with({ op: "neq" }, ({ value }) => new PercentageEqual(value, this.id).not()) + .with({ op: "is_empty" }, () => new StringEmpty(this.id)) + .with({ op: "is_not_empty" }, () => new StringEmpty(this.id).not()) + .exhaustive() + + return Option(spec) + } + + protected override getConditionSchema(optionType: z.ZodTypeAny): IPercentageFieldConditionSchema { + return createPercentageFieldCondition(optionType) + } + + override getMutationSpec(value: PercentageFieldValue): Option { + return Some(new PercentageEqual(value.value ?? null, this.id)) + } + + override get aggregate() { + return percentageFieldAggregate + } +} From 800ee1737f721cfc07b5e339852b4e95de62db41 Mon Sep 17 00:00:00 2001 From: GitHub actions Date: Wed, 4 Sep 2024 13:43:06 +0000 Subject: [PATCH 4/4] Prepare release v1.0.0-59 --- CHANGELOG.md | 11 +++++++++++ package.json | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 773511c65..6fd404d21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,17 @@ # Changelog +## v1.0.0-59 + + +### 🚀 Enhancements + +- Persontage field ([c8df205](https://github.com/undb-io/undb/commit/c8df205)) + +### ❤️ Contributors + +- Nichenqin ([@nichenqin](http://github.com/nichenqin)) + ## v1.0.0-58 diff --git a/package.json b/package.json index e407c2da4..06784dd94 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undb", - "version": "1.0.0-58", + "version": "1.0.0-59", "private": true, "scripts": { "build": "NODE_ENV=production bun --bun turbo build",