diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e12e753c..0f6dde428 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## v1.0.0-117 + ## v1.0.0-116 diff --git a/README.md b/README.md index 8f5ae74ea..058ff549c 100644 --- a/README.md +++ b/README.md @@ -50,8 +50,9 @@ UNDB is a no-code platform that can also serve as a Backend as a Service (BaaS). ![kanban](./docs/images/kanban.jpeg) ![gallery](./docs/images/gallery.jpeg) -![form](./docs/images/form.jpeg) ![calendar](./docs/images/calendar.jpeg) +![pivot](./docs/images/pivot.jpeg) +![form](./docs/images/form.jpeg) ![openapi](./docs/images/openapi.jpeg) ## Quick start diff --git a/apps/backend/assets/templates/remoteWorkManagement/cover.jpg b/apps/backend/assets/templates/remoteWorkManagement/cover.jpg new file mode 100644 index 000000000..a69e8371b Binary files /dev/null and b/apps/backend/assets/templates/remoteWorkManagement/cover.jpg differ diff --git a/apps/backend/assets/templates/remoteWorkManagement/image1.png b/apps/backend/assets/templates/remoteWorkManagement/image1.png new file mode 100644 index 000000000..aae8a5438 Binary files /dev/null and b/apps/backend/assets/templates/remoteWorkManagement/image1.png differ diff --git a/apps/backend/assets/templates/remoteWorkManagement/image2.png b/apps/backend/assets/templates/remoteWorkManagement/image2.png new file mode 100644 index 000000000..c17982fe1 Binary files /dev/null and b/apps/backend/assets/templates/remoteWorkManagement/image2.png differ diff --git a/apps/backend/src/modules/openapi/record.openapi.ts b/apps/backend/src/modules/openapi/record.openapi.ts index a74e68f2e..ef00a534b 100644 --- a/apps/backend/src/modules/openapi/record.openapi.ts +++ b/apps/backend/src/modules/openapi/record.openapi.ts @@ -14,7 +14,7 @@ import { CommandBus, QueryBus } from "@undb/cqrs" import { inject, singleton } from "@undb/di" import { Option, type ICommandBus, type IQueryBus, type PaginatedDTO } from "@undb/domain" import { injectQueryBuilder, type IQueryBuilder } from "@undb/persistence" -import { GetReadableRecordByIdQuery, GetReadableRecordsQuery } from "@undb/queries" +import { GetPivotDataQuery, GetReadableRecordByIdQuery, GetReadableRecordsQuery } from "@undb/queries" import { RecordDO, type IRecordReadableValueDTO } from "@undb/table" import Elysia, { t } from "elysia" import { withTransaction } from "../../db" @@ -79,6 +79,24 @@ export class RecordOpenApi { }, }, ) + .get( + "/views/:viewName/data", + async (ctx) => { + const baseName = decodeURIComponent(ctx.params.baseName) + const tableName = decodeURIComponent(ctx.params.tableName) + const viewName = decodeURIComponent(ctx.params.viewName) + + return await this.queryBus.execute(new GetPivotDataQuery({ baseName, tableName, viewName })) + }, + { + params: t.Object({ baseName: t.String(), tableName: t.String(), viewName: t.String() }), + detail: { + tags: ["Record"], + summary: "Get pivot view data", + description: "Get pivot view data", + }, + }, + ) .get( "/views/:viewName/records/:recordId", async (ctx) => { diff --git a/apps/frontend/schema.graphql b/apps/frontend/schema.graphql index 97b347b56..d4d905da9 100644 --- a/apps/frontend/schema.graphql +++ b/apps/frontend/schema.graphql @@ -137,6 +137,13 @@ type OAuthSettings { google: OAuthSetting } +type PivotOption { + aggregate: String + columnLabel: String + rowLabel: String + value: String +} + type Query { base(id: ID!): Base! baseByShare(shareId: ID!): Base @@ -283,6 +290,7 @@ type View { kanban: KanbanOption name: String! option: ViewOption + pivot: PivotOption share: Share shareId: ID sort: JSON @@ -304,6 +312,7 @@ enum ViewType { grid kanban list + pivot } type Widget { diff --git a/apps/frontend/src/lib/components/blocks/calendar-view/calendar-view-month.svelte b/apps/frontend/src/lib/components/blocks/calendar-view/calendar-view-month.svelte index 172f3c622..7644ec598 100644 --- a/apps/frontend/src/lib/components/blocks/calendar-view/calendar-view-month.svelte +++ b/apps/frontend/src/lib/components/blocks/calendar-view/calendar-view-month.svelte @@ -40,9 +40,11 @@ $endTimestamp?.toISOString(), ], enabled: view?.type === "calendar" && !!$startTimestamp && !!$endTimestamp && !disableRecordQuery, - queryFn: () => - trpc.record.list.query({ - tableId: $table?.id.value, + queryFn: () => { + if (shareId) { + return trpc.shareData.records.query({ + shareId, + tableId: $table?.id.value, viewId: $viewId, q: $q ?? undefined, filters: { @@ -51,8 +53,14 @@ { field: field.id.value, op: "is_after", value: $startTimestamp!.toISOString() }, { field: field.id.value, op: "is_before", value: $endTimestamp!.toISOString() }, ], - }, - }), + }, + }) + } + return trpc.record.list.query({ + tableId: $table?.id.value, + viewId: $viewId, + }) + }, } }), ) diff --git a/apps/frontend/src/lib/components/blocks/calendar-view/calendar-view.svelte b/apps/frontend/src/lib/components/blocks/calendar-view/calendar-view.svelte index 9d197c482..d7ed339f5 100644 --- a/apps/frontend/src/lib/components/blocks/calendar-view/calendar-view.svelte +++ b/apps/frontend/src/lib/components/blocks/calendar-view/calendar-view.svelte @@ -42,16 +42,4 @@ {/if} {/if} - - {#await import("$lib/components/blocks/create-record/create-record-sheet.svelte") then { default: CreateRecordSheet }} - - {/await} - - {#await import("$lib/components/blocks/record-detail/table-record-detail-sheet.svelte") then { default: TableRecordDetailSheet }} - - {/await} - - {#await import("$lib/components/blocks/view-widget/view-widget-sheet.svelte") then { default: ViewWidgetSheet }} - - {/await} {/key} diff --git a/apps/frontend/src/lib/components/blocks/date/date-macro-picker.svelte b/apps/frontend/src/lib/components/blocks/date/date-macro-picker.svelte new file mode 100644 index 000000000..1b28423fa --- /dev/null +++ b/apps/frontend/src/lib/components/blocks/date/date-macro-picker.svelte @@ -0,0 +1,47 @@ + + + { + if (v) { + value = v.value + } else { + value = undefined + } + + onValueChange(v?.value) + }} +> + + + + + {#each macros as macro} + + {macro.label} + + {/each} + + diff --git a/apps/frontend/src/lib/components/blocks/date/date-macro.svelte b/apps/frontend/src/lib/components/blocks/date/date-macro.svelte new file mode 100644 index 000000000..78d10ec0c --- /dev/null +++ b/apps/frontend/src/lib/components/blocks/date/date-macro.svelte @@ -0,0 +1,9 @@ + + +{#if value} + {value} +{/if} diff --git a/apps/frontend/src/lib/components/blocks/field-control/date-control.svelte b/apps/frontend/src/lib/components/blocks/field-control/date-control.svelte index 915e38ac7..086c59361 100644 --- a/apps/frontend/src/lib/components/blocks/field-control/date-control.svelte +++ b/apps/frontend/src/lib/components/blocks/field-control/date-control.svelte @@ -6,7 +6,9 @@ import { Calendar } from "$lib/components/ui/calendar" import * as Popover from "$lib/components/ui/popover" import { isDate, isString } from "radash" - import { DateField } from "@undb/table" + import { DateField, isDateFieldMacro } from "@undb/table" + import DateMacroPicker from "../date/date-macro-picker.svelte" + import DateMacro from "../date/date-macro.svelte" export let readonly = false export let disabled = false @@ -16,6 +18,8 @@ export let value: string | Date | undefined = undefined function parse(value: string) { + if (isDateFieldMacro(value)) return value + try { return parseDate(value) } catch { @@ -38,13 +42,25 @@ > {#if value} - {formatter(value)} + {#if isString(value) && isDateFieldMacro(value)} + + {:else} + {formatter(value)} + {/if} {/if} +
+ { + open = false + }} + bind:value + /> +
{ if (v) { value = v.toString() diff --git a/apps/frontend/src/lib/components/blocks/field-options/date-field-option.svelte b/apps/frontend/src/lib/components/blocks/field-options/date-field-option.svelte index 3e36bbbd8..567bf2f1a 100644 --- a/apps/frontend/src/lib/components/blocks/field-options/date-field-option.svelte +++ b/apps/frontend/src/lib/components/blocks/field-options/date-field-option.svelte @@ -1,9 +1,8 @@ + +
+
+ + + + + + + { + const start = v.start?.toDate(getLocalTimeZone()) + const end = v.end?.toDate(getLocalTimeZone()) + + const startDate = start ? start.toISOString() : undefined + const endDate = end ? end.toISOString() : undefined + + defaultValue = [startDate, endDate] + open = false + }} + initialFocus + numberOfMonths={2} + /> + + +
+ {#if constraint} +
+ +
+
+ + +
+ {/if} +
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 c467e0493..6fe108338 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 @@ -20,6 +20,7 @@ import DurationFieldOption from "./duration-field-option.svelte" import PercentageFieldOption from "./percentage-field-option.svelte" import FormulaFieldOption from "./formula-field-option.svelte" + import DateRangeFieldOption from "./date-range-field-option.svelte" export let constraint: IFieldConstraint | undefined export let option: any | undefined @@ -49,6 +50,7 @@ duration: DurationFieldOption, percentage: PercentageFieldOption, formula: FormulaFieldOption, + dateRange: DateRangeFieldOption, } export let type: NoneSystemFieldType diff --git a/apps/frontend/src/lib/components/blocks/field-picker/field-picker.svelte b/apps/frontend/src/lib/components/blocks/field-picker/field-picker.svelte index fd9eb9fd9..221b7c575 100644 --- a/apps/frontend/src/lib/components/blocks/field-picker/field-picker.svelte +++ b/apps/frontend/src/lib/components/blocks/field-picker/field-picker.svelte @@ -82,7 +82,9 @@ }} > - No field found. + + No field found. +
{#each filteredFields as field} diff --git a/apps/frontend/src/lib/components/blocks/gallery-view/gallery-view.svelte b/apps/frontend/src/lib/components/blocks/gallery-view/gallery-view.svelte index d0c07190b..9b04af119 100644 --- a/apps/frontend/src/lib/components/blocks/gallery-view/gallery-view.svelte +++ b/apps/frontend/src/lib/components/blocks/gallery-view/gallery-view.svelte @@ -91,15 +91,3 @@ {#if field} {/if} - -{#await import("$lib/components/blocks/create-record/create-record-sheet.svelte") then { default: CreateRecordSheet }} - -{/await} - -{#await import("$lib/components/blocks/record-detail/table-record-detail-sheet.svelte") then { default: TableRecordDetailSheet }} - -{/await} - -{#await import("$lib/components/blocks/view-widget/view-widget-sheet.svelte") then { default: ViewWidgetSheet }} - -{/await} diff --git a/apps/frontend/src/lib/components/blocks/grid-view/editable-cell/user-cell.svelte b/apps/frontend/src/lib/components/blocks/grid-view/editable-cell/user-cell.svelte index c24c6b696..6bc3ec761 100644 --- a/apps/frontend/src/lib/components/blocks/grid-view/editable-cell/user-cell.svelte +++ b/apps/frontend/src/lib/components/blocks/grid-view/editable-cell/user-cell.svelte @@ -9,6 +9,8 @@ import { builderActions, getAttrs } from "bits-ui" import { ChevronDownIcon } from "lucide-svelte" import UserMacro from "../../user/user-macro.svelte" + import { getRecordsStore } from "$lib/store/records.store" + import { getTable } from "$lib/store/table.store" export let tableId: string export let field: UserField @@ -24,9 +26,20 @@ open = true } + const table = getTable() + + const store = getRecordsStore() + const updateCell = createMutation({ mutationKey: ["record", tableId, field.id.value, recordId], mutationFn: trpc.record.update.mutate, + async onSuccess(data, variables) { + console.log(variables) + const value = variables.values[field.id.value] + if (isUserFieldMacro(value)) { + await store.invalidateRecord($table, recordId) + } + }, onError(error: Error) { toast.error(error.message) }, diff --git a/apps/frontend/src/lib/components/blocks/grid-view/grid-view.svelte b/apps/frontend/src/lib/components/blocks/grid-view/grid-view.svelte index 105d90090..7dd5a7891 100644 --- a/apps/frontend/src/lib/components/blocks/grid-view/grid-view.svelte +++ b/apps/frontend/src/lib/components/blocks/grid-view/grid-view.svelte @@ -82,15 +82,3 @@ isLoading={$getRecords.isLoading} total={$getRecords.data?.total ?? 0} /> - -{#await import("$lib/components/blocks/create-record/create-record-sheet.svelte") then { default: CreateRecordSheet }} - -{/await} - -{#await import("$lib/components/blocks/record-detail/table-record-detail-sheet.svelte") then { default: TableRecordDetailSheet }} - -{/await} - -{#await import("$lib/components/blocks/view-widget/view-widget-sheet.svelte") then { default: ViewWidgetSheet }} - -{/await} diff --git a/apps/frontend/src/lib/components/blocks/kanban-view/kanban-view.svelte b/apps/frontend/src/lib/components/blocks/kanban-view/kanban-view.svelte index 8d0bcd0d8..a425e51fb 100644 --- a/apps/frontend/src/lib/components/blocks/kanban-view/kanban-view.svelte +++ b/apps/frontend/src/lib/components/blocks/kanban-view/kanban-view.svelte @@ -50,16 +50,4 @@ {/if} {/if} - - {#await import("$lib/components/blocks/create-record/create-record-sheet.svelte") then { default: CreateRecordSheet }} - - {/await} - - {#await import("$lib/components/blocks/record-detail/table-record-detail-sheet.svelte") then { default: TableRecordDetailSheet }} - - {/await} - - {#await import("$lib/components/blocks/view-widget/view-widget-sheet.svelte") then { default: ViewWidgetSheet }} - - {/await} {/key} diff --git a/apps/frontend/src/lib/components/blocks/list-view/list-view.svelte b/apps/frontend/src/lib/components/blocks/list-view/list-view.svelte index cf6682ffa..8437a3491 100644 --- a/apps/frontend/src/lib/components/blocks/list-view/list-view.svelte +++ b/apps/frontend/src/lib/components/blocks/list-view/list-view.svelte @@ -82,15 +82,3 @@ {/if}
- -{#await import("$lib/components/blocks/create-record/create-record-sheet.svelte") then { default: CreateRecordSheet }} - -{/await} - -{#await import("$lib/components/blocks/record-detail/table-record-detail-sheet.svelte") then { default: TableRecordDetailSheet }} - -{/await} - -{#await import("$lib/components/blocks/view-widget/view-widget-sheet.svelte") then { default: ViewWidgetSheet }} - -{/await} diff --git a/apps/frontend/src/lib/components/blocks/pivot-view/pivot-aggregate-picker.svelte b/apps/frontend/src/lib/components/blocks/pivot-view/pivot-aggregate-picker.svelte new file mode 100644 index 000000000..3081b686d --- /dev/null +++ b/apps/frontend/src/lib/components/blocks/pivot-view/pivot-aggregate-picker.svelte @@ -0,0 +1,26 @@ + + + { + value = v?.value + onValueChange(v?.value) + }} +> + + + + + {#each PIVOT_AGGREGATE as aggregate} + {aggregate} + {/each} + + diff --git a/apps/frontend/src/lib/components/blocks/pivot-view/pivot-option-button.svelte b/apps/frontend/src/lib/components/blocks/pivot-view/pivot-option-button.svelte new file mode 100644 index 000000000..d6d604175 --- /dev/null +++ b/apps/frontend/src/lib/components/blocks/pivot-view/pivot-option-button.svelte @@ -0,0 +1,41 @@ + + + + + + + + + {#if !readonly} + Update pivot view + {:else} + Pivot view + {/if} + + + + diff --git a/apps/frontend/src/lib/components/blocks/pivot-view/pivot-view-data.svelte b/apps/frontend/src/lib/components/blocks/pivot-view/pivot-view-data.svelte new file mode 100644 index 000000000..541e4c86a --- /dev/null +++ b/apps/frontend/src/lib/components/blocks/pivot-view/pivot-view-data.svelte @@ -0,0 +1,160 @@ + + +
+ {#if columnField && rowField} + + + + + {rowField.name.value} + + {#each options as option} + + + {/each} + {aggregate?.toUpperCase() ?? "Total"} ({rowField.name.value}) + + + + {#if $getPivotData.isPending} + {#each Array(10) as _} + + +
+ + {#each options as _} + +
+ + {/each} + +
+ + + {/each} + {:else} + {#each data as row} + {@const rowTotal = row["agg"]} + {@const label = row.label} + {@const labelValues = row.labelValues} + + + {#if label === null} + (Is Empty) + {:else if rowField.type === "select"} + {@const optionId = label} + {@const option = rowField.options.find((o) => o.id === optionId)} + {#if option} + + {#each options as option} + {@const value = row.values[option.name]} + + {#if value} + {#if valueField?.type === "currency" && aggregate !== "count"} + {valueField.symbol} + {/if} + {value} + {:else} +
+ {/if} +
+ {/each} + + {#if valueField?.type === "currency" && aggregate !== "count"} + {valueField.symbol} + {/if} + {rowTotal} + +
+ {/each} + {#if total} + + {aggregate?.toUpperCase() ?? "Total"} ({columnField.name.value}) + {@const totalRowTotal = total["agg"]} + {#each options as option} + {@const value = total.values[option.name]} + + {#if value} + {#if valueField?.type === "currency" && aggregate !== "count"} + {valueField.symbol} + {/if} + {value} + {:else} +
+ {/if} +
+ {/each} + + {#if valueField?.type === "currency" && aggregate !== "count"} + {valueField.symbol} + {/if} + + {totalRowTotal} + +
+ {/if} + {/if} + + + {/if} +
diff --git a/apps/frontend/src/lib/components/blocks/pivot-view/pivot-view-option-form.svelte b/apps/frontend/src/lib/components/blocks/pivot-view/pivot-view-option-form.svelte new file mode 100644 index 000000000..20b67129e --- /dev/null +++ b/apps/frontend/src/lib/components/blocks/pivot-view/pivot-view-option-form.svelte @@ -0,0 +1,217 @@ + + +
+
+
+
+ + + Column Label + { + if ($formData.pivot) { + $formData.pivot.columnLabel = field + } else { + $formData.pivot = { columnLabel: field } + } + }} + filter={(f) => $columnFields.map((f) => f.id.value).includes(f.id) && f.id !== $formData.pivot?.rowLabel} + /> + + + + + + + +
+ Row Label + {#if !readonly} + + + + + +

Swap with Column Label

+
+
+ {/if} +
+ $rowFields.map((f) => f.id.value).includes(f.id) && f.id !== $formData.pivot?.columnLabel} + onValueChange={(field) => { + if ($formData.pivot) { + $formData.pivot.rowLabel = field + } else { + $formData.pivot = { rowLabel: field } + } + }} + /> +
+ + +
+ + + + Aggregate + { + if ($formData.pivot) { + $formData.pivot.aggregate = v + if (v === "count") { + $formData.pivot.value = undefined + } + } else { + if (v !== "count") { + $formData.pivot = { aggregate: v } + } + } + }} + /> + + + + + + {#if $formData.pivot?.aggregate !== "count"} + + + Value + { + const valueFields = $table.schema.getPivotValueFields($formData.pivot?.aggregate ?? "sum") + return valueFields.map((f) => f.id.value).includes(f.id) + }} + onValueChange={(field) => { + if ($formData.pivot) { + $formData.pivot.value = field + } else { + $formData.pivot = { value: field } + } + }} + /> + + + + + {/if} + + {#if !readonly} + Save + {/if} +
+
+
+
diff --git a/apps/frontend/src/lib/components/blocks/pivot-view/pivot-view-option.svelte b/apps/frontend/src/lib/components/blocks/pivot-view/pivot-view-option.svelte new file mode 100644 index 000000000..9535b80fc --- /dev/null +++ b/apps/frontend/src/lib/components/blocks/pivot-view/pivot-view-option.svelte @@ -0,0 +1,17 @@ + + + + + Config pivot view + Configure pivot view column label, row label, value and aggregate. + + + + + diff --git a/apps/frontend/src/lib/components/blocks/pivot-view/pivot-view-toolbar.svelte b/apps/frontend/src/lib/components/blocks/pivot-view/pivot-view-toolbar.svelte new file mode 100644 index 000000000..017c0c231 --- /dev/null +++ b/apps/frontend/src/lib/components/blocks/pivot-view/pivot-view-toolbar.svelte @@ -0,0 +1,30 @@ + + +
+
+ {#if !readonly} + + {/if} + + + +
+
+ {#if !readonly} + + {/if} +
+
diff --git a/apps/frontend/src/lib/components/blocks/pivot-view/pivot-view.svelte b/apps/frontend/src/lib/components/blocks/pivot-view/pivot-view.svelte new file mode 100644 index 000000000..5a33e99e5 --- /dev/null +++ b/apps/frontend/src/lib/components/blocks/pivot-view/pivot-view.svelte @@ -0,0 +1,28 @@ + + +{#key $table.id.value} + {#if view.type === "pivot"} + + {#if !view.isValid && !readonly && !shareId} +
+ +
+ {:else} + + {/if} + {/if} +{/key} diff --git a/apps/frontend/src/lib/components/blocks/share/share-pivot-view.svelte b/apps/frontend/src/lib/components/blocks/share/share-pivot-view.svelte new file mode 100644 index 000000000..fa79513a2 --- /dev/null +++ b/apps/frontend/src/lib/components/blocks/share/share-pivot-view.svelte @@ -0,0 +1,14 @@ + + + + +{#await import("$lib/components/blocks/view-widget/view-widget-sheet.svelte") then { default: ViewWidgetSheet }} + +{/await} diff --git a/apps/frontend/src/lib/components/blocks/share/share-view-page.svelte b/apps/frontend/src/lib/components/blocks/share/share-view-page.svelte index eac404bda..61a08283d 100644 --- a/apps/frontend/src/lib/components/blocks/share/share-view-page.svelte +++ b/apps/frontend/src/lib/components/blocks/share/share-view-page.svelte @@ -6,6 +6,7 @@ import ShareGalleryView from "./share-gallery-view.svelte" import ShareListView from "./share-list-view.svelte" import ShareCalendarView from "./share-calendar-view.svelte" + import SharePivotView from "./share-pivot-view.svelte" const table = getTable() export let viewId: Readable @@ -24,4 +25,6 @@ {:else if view.type === "calendar"} +{:else if view.type === "pivot"} + {/if} diff --git a/apps/frontend/src/lib/components/blocks/user/user-macro.svelte b/apps/frontend/src/lib/components/blocks/user/user-macro.svelte index 3e5c4194a..e18f1e80f 100644 --- a/apps/frontend/src/lib/components/blocks/user/user-macro.svelte +++ b/apps/frontend/src/lib/components/blocks/user/user-macro.svelte @@ -8,7 +8,7 @@ {#if value}
- - {$LL.table.macros[value]()} + + {$LL.table.macros[value]()}
{/if} diff --git a/apps/frontend/src/lib/components/blocks/user/user-picker.svelte b/apps/frontend/src/lib/components/blocks/user/user-picker.svelte index 5139932ac..340b77b3c 100644 --- a/apps/frontend/src/lib/components/blocks/user/user-picker.svelte +++ b/apps/frontend/src/lib/components/blocks/user/user-picker.svelte @@ -83,7 +83,7 @@ {/if} {#each userFieldMacros as macro} { value = currentValue === value ? null : currentValue diff --git a/apps/frontend/src/lib/components/blocks/view/create-view-button.svelte b/apps/frontend/src/lib/components/blocks/view/create-view-button.svelte index 50d0009b1..5c9636d2a 100644 --- a/apps/frontend/src/lib/components/blocks/view/create-view-button.svelte +++ b/apps/frontend/src/lib/components/blocks/view/create-view-button.svelte @@ -16,9 +16,15 @@ import { LoaderCircleIcon } from "lucide-svelte" import ViewTypePicker from "./view-type-picker.svelte" import { goto, invalidate } from "$app/navigation" + import { getTable } from "$lib/store/table.store" + import FieldPicker from "../field-picker/field-picker.svelte" + import * as Tooltip from "$lib/components/ui/tooltip" + import { CircleHelpIcon } from "lucide-svelte" let open = false + const table = getTable() + export let tableId: string export let viewNames: string[] @@ -88,11 +94,118 @@ - + View Type + { + if (type === "calendar") { + const field = $table.schema.getCalendarFields().at(0)?.id.value + formData.update((f) => ({ ...f, calendar: { field } })) + } else if (type === "gallery") { + const field = $table.schema.getGalleryFields().at(0)?.id.value + formData.update((f) => ({ ...f, gallery: { field } })) + } else if (type === "kanban") { + const field = $table.schema.getKanbanFields().at(0)?.id.value + formData.update((f) => ({ ...f, kanban: { field } })) + } + }} + /> + {#if $formData.type === "calendar" && $formData.calendar} + + + + Calendar Field + + + + + +

Group calendar by a date type field

+
+
+
+ + $table.schema + .getCalendarFields() + .map((f) => f.id.value) + .includes(f.id)} + > +
No date field found.
+
+
+ + +
+ {:else if $formData.type === "gallery" && $formData.gallery} + + + + Gallery Field + + + + + +

Gallery view will display a banner of images grouped by an attachment type field

+
+
+
+ + $table.schema + .getGalleryFields() + .map((f) => f.id.value) + .includes(f.id)} + > +
No attachment field found.
+
+
+ + +
+ {:else if $formData.type === "kanban" && $formData.kanban} + + + + Kanban Field + + + + + +

Group kanban by a select type field

+
+
+
+ + $table.schema + .getKanbanFields() + .map((f) => f.id.value) + .includes(f.id)} + > +
No select field found.
+
+
+ + +
+ {/if} {#if $createViewMutation.isPending} diff --git a/apps/frontend/src/lib/components/blocks/view/view-icon.svelte b/apps/frontend/src/lib/components/blocks/view/view-icon.svelte index 9974b9be6..8dd5c3732 100644 --- a/apps/frontend/src/lib/components/blocks/view/view-icon.svelte +++ b/apps/frontend/src/lib/components/blocks/view/view-icon.svelte @@ -1,6 +1,13 @@ diff --git a/apps/frontend/src/lib/components/blocks/view/view.svelte b/apps/frontend/src/lib/components/blocks/view/view.svelte index e64a1fae8..401f6f47f 100644 --- a/apps/frontend/src/lib/components/blocks/view/view.svelte +++ b/apps/frontend/src/lib/components/blocks/view/view.svelte @@ -5,11 +5,14 @@ import KanbanView from "../kanban-view/kanban-view.svelte" import GalleryView from "../gallery-view/gallery-view.svelte" import ListView from "../list-view/list-view.svelte" - import { r } from "$lib/store/records.store" import CalendarView from "../calendar-view/calendar-view.svelte" + import PivotView from "../pivot-view/pivot-view.svelte" + + import { r } from "$lib/store/records.store" const table = getTable() export let viewId: Readable + export let shareId: string | undefined $: view = $table.views.getViewById($viewId) @@ -26,6 +29,20 @@ {:else if view.type === "calendar"} + {:else if view.type === "pivot"} + {/if} {/if} + + {#await import("$lib/components/blocks/create-record/create-record-sheet.svelte") then { default: CreateRecordSheet }} + + {/await} + + {#await import("$lib/components/blocks/record-detail/table-record-detail-sheet.svelte") then { default: TableRecordDetailSheet }} + + {/await} + + {#await import("$lib/components/blocks/view-widget/view-widget-sheet.svelte") then { default: ViewWidgetSheet }} + + {/await} {/key} diff --git a/apps/frontend/src/routes/(authed)/(space)/t/[tableId]/+layout.gql b/apps/frontend/src/routes/(authed)/(space)/t/[tableId]/+layout.gql index 2e5327977..f02a015e0 100644 --- a/apps/frontend/src/routes/(authed)/(space)/t/[tableId]/+layout.gql +++ b/apps/frontend/src/routes/(authed)/(space)/t/[tableId]/+layout.gql @@ -42,6 +42,12 @@ query GetTableQuery($tableId: ID!, $viewId: ID) { calendar { field } + pivot { + columnLabel + rowLabel + value + aggregate + } share { enabled diff --git a/apps/frontend/src/routes/(share)/s/b/[shareId]/+layout.gql b/apps/frontend/src/routes/(share)/s/b/[shareId]/+layout.gql index 98b1def01..26a5c56f1 100644 --- a/apps/frontend/src/routes/(share)/s/b/[shareId]/+layout.gql +++ b/apps/frontend/src/routes/(share)/s/b/[shareId]/+layout.gql @@ -45,6 +45,12 @@ query GetShareBaseData($shareId: ID!) { gallery { field } + pivot { + columnLabel + rowLabel + value + aggregate + } widgets { id name diff --git a/apps/frontend/src/routes/(share)/s/b/[shareId]/t/[tableId]/+layout.gql b/apps/frontend/src/routes/(share)/s/b/[shareId]/t/[tableId]/+layout.gql index 760637e15..6f2b75572 100644 --- a/apps/frontend/src/routes/(share)/s/b/[shareId]/t/[tableId]/+layout.gql +++ b/apps/frontend/src/routes/(share)/s/b/[shareId]/t/[tableId]/+layout.gql @@ -37,6 +37,12 @@ query GetBaseTableShareData($shareId: ID!, $tableId: ID!) { gallery { field } + pivot { + columnLabel + rowLabel + value + aggregate + } calendar { field } diff --git a/apps/frontend/src/routes/(share)/s/v/[shareId]/+layout.gql b/apps/frontend/src/routes/(share)/s/v/[shareId]/+layout.gql index 869c0e694..6e75292cd 100644 --- a/apps/frontend/src/routes/(share)/s/v/[shareId]/+layout.gql +++ b/apps/frontend/src/routes/(share)/s/v/[shareId]/+layout.gql @@ -54,6 +54,12 @@ query GetViewShareData($shareId: ID!) { calendar { field } + pivot { + columnLabel + rowLabel + value + aggregate + } widgets { id name diff --git a/docs/images/pivot.jpeg b/docs/images/pivot.jpeg new file mode 100644 index 000000000..9e1d5d1c1 Binary files /dev/null and b/docs/images/pivot.jpeg differ diff --git a/package.json b/package.json index 6c83c7ae3..a99cf1536 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undb", - "version": "1.0.0-116", + "version": "1.0.0-117", "private": true, "scripts": { "build": "NODE_ENV=production bun --bun turbo build", diff --git a/packages/commands/src/update-view.command.ts b/packages/commands/src/update-view.command.ts index 20de39ebd..af8e7771e 100644 --- a/packages/commands/src/update-view.command.ts +++ b/packages/commands/src/update-view.command.ts @@ -1,9 +1,10 @@ -import { Command, type CommandProps } from "@undb/domain" +import { Command,type CommandProps } from "@undb/domain" import { updateViewDTO, type ICalendarOption, type IGalleryOption, type IKanbanOption, + type IPivotOption, type ViewType, } from "@undb/table" import { z } from "@undb/zod" @@ -20,6 +21,7 @@ export class UpdateViewCommand extends Command { public readonly kanban?: IKanbanOption public readonly gallery?: IGalleryOption public readonly calendar?: ICalendarOption + public readonly pivot?: IPivotOption constructor(props: CommandProps) { super(props) @@ -39,5 +41,9 @@ export class UpdateViewCommand extends Command { // @ts-ignore this.calendar = props.calendar } + if (props.type === "pivot") { + // @ts-ignore + this.pivot = props.pivot + } } } diff --git a/packages/graphql/src/index.ts b/packages/graphql/src/index.ts index 5b28d0d9e..a1e5548e2 100644 --- a/packages/graphql/src/index.ts +++ b/packages/graphql/src/index.ts @@ -160,6 +160,7 @@ export class Graphql { gallery list calendar + pivot } type ViewOption { @@ -182,6 +183,13 @@ export class Graphql { field: String } + type PivotOption { + columnLabel: String + rowLabel: String + value: String + aggregate: String + } + type Widget { id: ID! name: String! @@ -204,6 +212,7 @@ export class Graphql { kanban: KanbanOption gallery: GalleryOption calendar: CalendarOption + pivot: PivotOption widgets: [Widget] shareId: ID diff --git a/packages/i18n/src/i18n/en/index.ts b/packages/i18n/src/i18n/en/index.ts index 20025704b..da99bf8ec 100644 --- a/packages/i18n/src/i18n/en/index.ts +++ b/packages/i18n/src/i18n/en/index.ts @@ -111,7 +111,11 @@ const workspaceRoles: Record = { } const macros: Record = { - "@me": "Current User" + "@me": "Current User", + "@now": "Now", + "@today": "Today", + "@yesterday": "Yesterday", + "@tomorrow": "Tomorrow", } const viewTypes: Record = { @@ -119,7 +123,8 @@ const viewTypes: Record = { kanban: "Kanban", gallery: "Gallery", list: "List", - calendar: "Calendar" + calendar: "Calendar", + pivot: "Pivot" } const widgetTypes: Record = { diff --git a/packages/i18n/src/i18n/i18n-types.ts b/packages/i18n/src/i18n/i18n-types.ts index d9da2e50c..1d84d1ece 100644 --- a/packages/i18n/src/i18n/i18n-types.ts +++ b/packages/i18n/src/i18n/i18n-types.ts @@ -401,6 +401,22 @@ type RootTranslation = { * C​u​r​r​e​n​t​ ​U​s​e​r */ '@me': string + /** + * N​o​w + */ + '@now': string + /** + * T​o​d​a​y + */ + '@today': string + /** + * Y​e​s​t​e​r​d​a​y + */ + '@yesterday': string + /** + * T​o​m​o​r​r​o​w + */ + '@tomorrow': string } viewTypes: { /** @@ -830,6 +846,22 @@ export type TranslationFunctions = { * Current User */ '@me': () => LocalizedString + /** + * Now + */ + '@now': () => LocalizedString + /** + * Today + */ + '@today': () => LocalizedString + /** + * Yesterday + */ + '@yesterday': () => LocalizedString + /** + * Tomorrow + */ + '@tomorrow': () => LocalizedString } viewTypes: { /** diff --git a/packages/openapi/src/index.ts b/packages/openapi/src/index.ts index 4ffc533fd..561a4266b 100644 --- a/packages/openapi/src/index.ts +++ b/packages/openapi/src/index.ts @@ -12,15 +12,18 @@ import type { Base } from "@undb/base" import { type IReadableRecordDTO, type TableDo, type View } from "@undb/table" import { RECORD_COMPONENT, + VIEW_PIVOT_COMPONENT, VIEW_RECORD_COMPONENT, bulkDeleteRecords, bulkDuplicateRecords, bulkUpdateRecords, + createPivotViewDateComponent, createRecord, createRecordComponent, createRecords, deleteRecordById, duplicateRecordById, + getPivotData, getRecordById, getRecords, getViewRecordById, @@ -74,14 +77,21 @@ export const createOpenApiSpec = ( } for (const { view, record } of views) { - const viewRecordSchema = createRecordComponent(table, view, record) - registry.register( - view.name.value + ":" + VIEW_RECORD_COMPONENT, - viewRecordSchema.openapi({ description: table.name.value + " " + view.name.value + " view record schema" }), - ) - - routes.push(getViewRecords(base, table, view, viewRecordSchema)) - routes.push(getViewRecordById(base, table, view, viewRecordSchema)) + if (view.type === "pivot") { + const pivotViewDataComponent = createPivotViewDateComponent(table, view) + registry.register(view.name.value + ":" + VIEW_PIVOT_COMPONENT, pivotViewDataComponent) + + routes.push(getPivotData(base, table, view)) + } else { + const viewRecordSchema = createRecordComponent(table, view, record) + registry.register( + view.name.value + ":" + VIEW_RECORD_COMPONENT, + viewRecordSchema.openapi({ description: table.name.value + " " + view.name.value + " view record schema" }), + ) + + routes.push(getViewRecords(base, table, view, viewRecordSchema)) + routes.push(getViewRecordById(base, table, view, viewRecordSchema)) + } } const apiKeyAuth = registry.registerComponent("securitySchemes", "apiKeyAuth", { diff --git a/packages/openapi/src/openapi/record.openapi.ts b/packages/openapi/src/openapi/record.openapi.ts index fda32b6dd..aa74cbbd9 100644 --- a/packages/openapi/src/openapi/record.openapi.ts +++ b/packages/openapi/src/openapi/record.openapi.ts @@ -1,6 +1,15 @@ import type { RouteConfig } from "@asteasolutions/zod-to-openapi" import type { Base } from "@undb/base" -import { ButtonField, FormVO, recordId, type IReadableRecordDTO, type TableDo, type View } from "@undb/table" +import { + ButtonField, + FormVO, + getPivotDataOutput, + PivotView, + recordId, + type IReadableRecordDTO, + type TableDo, + type View, +} from "@undb/table" import { z, type ZodTypeAny } from "@undb/zod" export const RECORD_ID_COMPONENT = "RecordId" @@ -8,7 +17,9 @@ export const RECORD_COMPONENT = "Record" export const BUTTON_COMPONENT = "Button" export const FORM_COMPONENT = "Form" export const VIEW_COMPONENT = "View" +export const PIVOT_COMPONENT = "Pivot" export const VIEW_RECORD_COMPONENT = "ViewRecord" +export const VIEW_PIVOT_COMPONENT = "ViewPivotData" export const FORM_SUBMIT_RECORD_COMPONENT = "FormSubmitRecord" export const RECORD_VALUES_COMPONENT = "RecordValues" export const VIEW_RECORD_VALUES_COMPONENT = "ViewRecordValues" @@ -38,6 +49,32 @@ export const createRecordComponent = (table: TableDo, view?: View, record?: IRea }) } +export const createPivotViewDateComponent = (table: TableDo, view: PivotView) => { + return getPivotDataOutput.openapi(view.name.value + ":" + VIEW_PIVOT_COMPONENT, { + description: `pivot view data in ${view.name.value} view`, + }) +} + +export const getPivotData = (base: Base, table: TableDo, view: PivotView): RouteConfig => { + return { + method: "get", + path: `/bases/${base.name.value}/tables/${table.name.value}/views/${view.name.value}/data`, + description: `Get pivot view data in ${view.name.value} view`, + summary: `Get pivot view data in ${view.name.value} view`, + tags: [PIVOT_COMPONENT, VIEW_COMPONENT], + responses: { + 200: { + description: "pivot view data", + content: { + "application/json": { + schema: getPivotDataOutput, + }, + }, + }, + }, + } +} + export const getRecords = (base: Base, table: TableDo, recordSchema: ZodTypeAny): RouteConfig => { return { method: "get", diff --git a/packages/persistence/src/record/record.filter-visitor.ts b/packages/persistence/src/record/record.filter-visitor.ts index 219c0ae4d..fd2a2fb11 100644 --- a/packages/persistence/src/record/record.filter-visitor.ts +++ b/packages/persistence/src/record/record.filter-visitor.ts @@ -130,6 +130,33 @@ export class RecordFilterVisitor extends AbstractQBVisitor implements this.addCond(cond) } dateEqual(spec: DateEqual): void { + if (spec.date === "@now") { + const now = new Date() + const start = startOfDay(now).getTime() + const end = endOfDay(now).getTime() + const cond = this.eb.between(this.getFieldId(spec), start, end) + this.addCond(cond) + return + } else if (spec.date === "@today") { + const start = startOfToday().getTime() + const end = endOfToday().getTime() + const cond = this.eb.between(this.getFieldId(spec), start, end) + this.addCond(cond) + return + } else if (spec.date === "@yesterday") { + const start = startOfYesterday().getTime() + const end = endOfYesterday().getTime() + const cond = this.eb.between(this.getFieldId(spec), start, end) + this.addCond(cond) + return + } else if (spec.date === "@tomorrow") { + const start = startOfTomorrow().getTime() + const end = endOfTomorrow().getTime() + const cond = this.eb.between(this.getFieldId(spec), start, end) + this.addCond(cond) + return + } + const time = spec.date?.getTime() if (time === null) { const cond = this.eb.eb(this.getFieldId(spec), "is", null) diff --git a/packages/persistence/src/record/record.mutate-visitor.ts b/packages/persistence/src/record/record.mutate-visitor.ts index 0db94c796..3842d2ee3 100644 --- a/packages/persistence/src/record/record.mutate-visitor.ts +++ b/packages/persistence/src/record/record.mutate-visitor.ts @@ -72,6 +72,7 @@ import { type UserEmpty, type UserEqual, } from "@undb/table" +import { startOfDay, startOfToday, startOfTomorrow, startOfYesterday } from "date-fns" import { sql, type ExpressionBuilder } from "kysely" import { unique } from "radash" import { AbstractQBMutationVisitor } from "../abstract-qb.visitor" @@ -122,7 +123,21 @@ export class RecordMutateVisitor extends AbstractQBMutationVisitor implements IR this.setData(spec.fieldId.value, null) } dateEqual(spec: DateEqual): void { - this.setData(spec.fieldId.value, spec.date?.getTime() ?? null) + if (spec.date === "@now") { + const start = startOfDay(new Date()) + this.setData(spec.fieldId.value, start.getTime()) + } else if (spec.date === "@today") { + const start = startOfToday() + this.setData(spec.fieldId.value, start.getTime()) + } else if (spec.date === "@yesterday") { + const start = startOfYesterday() + this.setData(spec.fieldId.value, start.getTime()) + } else if (spec.date === "@tomorrow") { + const start = startOfTomorrow() + this.setData(spec.fieldId.value, start.getTime()) + } else { + this.setData(spec.fieldId.value, spec.date?.getTime() ?? null) + } } dateRangeEqual(spec: DateRangeEqual): void { const field = this.table.schema.getFieldById(new FieldIdVo(spec.fieldId.value)).expect("No field found") @@ -303,16 +318,19 @@ export class RecordMutateVisitor extends AbstractQBMutationVisitor implements IR if (value === "@me") { return this.context.getCurrentUserId() } + + return null } return value } if (Array.isArray(value)) { - const converted = unique(value.map(convertMacro)) + const converted = unique(value.map(convertMacro).filter(Boolean)) this.setData(spec.fieldId.value, JSON.stringify(converted)) } else { - this.setData(spec.fieldId.value, value ? convertMacro(value) : null) + const converted = value ? convertMacro(value) : null + this.setData(spec.fieldId.value, converted) } } userEmpty(spec: UserEmpty): void { diff --git a/packages/persistence/src/record/record.query-repository.ts b/packages/persistence/src/record/record.query-repository.ts index 772073515..5fd069a5f 100644 --- a/packages/persistence/src/record/record.query-repository.ts +++ b/packages/persistence/src/record/record.query-repository.ts @@ -4,11 +4,14 @@ import { None, Option, Some, type PaginatedDTO } from "@undb/domain" import { AUTO_INCREMENT_TYPE, ID_TYPE, + SelectField, TableIdVo, injectTableRepository, type AggregateResult, type CountQueryArgs, type Field, + type IGetPivotDataOutput, + type IPivotAggregate, type IRecordDTO, type IRecordQueryRepository, type ITableRepository, @@ -22,9 +25,11 @@ import { type View, type ViewId, } from "@undb/table" -import { type AliasedExpression, type Expression } from "kysely" +import { getTableName } from "drizzle-orm" +import { sql, type AliasedExpression, type Expression, type ExpressionBuilder } from "kysely" import type { IRecordQueryBuilder } from "../qb" import { injectQueryBuilder } from "../qb.provider" +import { users } from "../tables" import { UnderlyingTable } from "../underlying/underlying-table" import { RecordQueryHelper } from "./record-query.helper" import { RecordReferenceVisitor } from "./record-reference-visitor" @@ -127,6 +132,126 @@ export class RecordQueryRepository implements IRecordQueryRepository { return { values: records, total: Number(total) } } + async getPivotData(table: TableDo, viewId: string): Promise { + const view = table.views.getViewById(viewId) + + if (view.type !== "pivot") { + throw new Error("Invalid view type") + } + if (!view.isValid) { + throw new Error("Invalid view") + } + + const columnLabel = view.columnLabel.unwrap()! + const rowLabel = view.rowLabel.unwrap()! + const value = view.value.unwrap()! + const aggregate = view.pivotAggregate.unwrap()! + + const columnField = table.schema.getFieldByIdOrName(columnLabel).into(undefined) as SelectField | undefined + const rowField = table.schema.getFieldByIdOrName(rowLabel).into(undefined) + const valueField = table.schema.getFieldByIdOrName(value).into(undefined) + if (!columnField || !rowField) { + throw new Error("Invalid view") + } + + function convertAggFn(aggFn: IPivotAggregate) { + aggFn = aggFn.toLowerCase() as IPivotAggregate + if (aggFn === "average") { + return "avg" + } + return aggFn + } + + const options = columnField.options + const aggFn = convertAggFn(aggregate) + + const t = new UnderlyingTable(table) + const createSelects = + (isTotal = false) => + (eb: ExpressionBuilder) => { + const selects: AliasedExpression[] = [ + isTotal ? sql.raw("'Total'").as("label") : eb.ref(`${t.name}.${rowField.id.value}`).as("label"), + ] + + if (rowField.type === "user") { + if (!isTotal) { + const user = getTableName(users) + + const q = eb + .selectFrom(user) + .select( + eb + .fn("json_object", [ + sql.raw("'username'"), + eb.fn.coalesce(`${user}.${users.username.name}`, sql`NULL`), + sql.raw("'email'"), + eb.fn.coalesce(`${user}.${users.email.name}`, sql`NULL`), + ]) + .as("labelValues"), + ) + .whereRef(rowField.id.value, "=", `${user}.${users.id.name}`) + .limit(1) + .as("labelValues") + + selects.push(q) + } else { + selects.push(sql`NULL`.as("labelValues")) + } + } + + const columnSelects = options + .map((option) => { + if (aggFn === "count") { + const caseString = `count(CASE WHEN "${t.name}"."${columnField.id.value}" = '${option.id}' THEN 1 END)` + return [sql.raw(`'${option.name}'`), sql.raw(caseString)] + } else { + if (!valueField) { + throw new Error("value field is required") + } + + let valueFieldAlias = `${t.name}.${valueField.id.value}` + if (valueField.type === "currency") { + valueFieldAlias = `${t.name}.${valueField.id.value} / 100` + } + + const caseString = + `${aggFn}(CASE WHEN ` + + `"${t.name}"."${columnField.id.value}" = '${option.id}' ` + + `THEN ${valueFieldAlias} ` + + `END)` + return [sql.raw(`'${option.name}'`), sql.raw(caseString)] + } + }) + .flat() + + selects.push(eb.fn("json_object", columnSelects).as("values")) + + if (aggFn === "count") { + selects.push(sql.raw(`sum(CASE WHEN "${t.name}"."${columnField.id.value}" IS NOT NULL THEN 1 END)`).as(`agg`)) + } else { + if (!valueField) { + throw new Error("value field is required") + } + const rowTotalString = + valueField.type === "currency" + ? `${aggFn}("${t.name}"."${valueField.id.value}") / 100` + : `${aggFn}("${t.name}"."${valueField.id.value}")` + selects.push(sql.raw(rowTotalString).as(`agg`)) + } + + return selects + } + + const result = await this.qb + .selectFrom(t.name) + .select(createSelects()) + .groupBy(`${t.name}.${rowField.id.value}`) + .unionAll((qb) => qb.selectFrom(t.name).select(createSelects(true))) + .execute() + + return result as IGetPivotDataOutput + } + async aggregate( table: TableDo, viewId: Option, diff --git a/packages/persistence/src/template/template-data.ts b/packages/persistence/src/template/template-data.ts index cbed464d8..593cc05d3 100644 --- a/packages/persistence/src/template/template-data.ts +++ b/packages/persistence/src/template/template-data.ts @@ -387,6 +387,57 @@ export const templateData: ITemplateDTO[] = [ template: templates.socialMediaContent as IBaseTemplateDTO, }, }, + { + id: "6ba7b812-9dad-11d1-80b4-00c04fd430c8", + icon: "💼", + name: "Remote Work Management", + categories: ["development", "hr"], + cover: getTemplateImage("remoteWorkManagement", "cover.jpg"), + images: [ + getTemplateImage("remoteWorkManagement", "image1.png"), + getTemplateImage("remoteWorkManagement", "image2.png"), + ], + description: "A template for managing remote workers, including tasks, time tracking, and equipment inventory.", + detail: ` +

UnDB Remote Work Management Template: Optimize Your Distributed Team Operations

+ +

Comprehensive Remote Work Management Features for Modern Organizations

+ +

UnDB's Remote Work Management template provides essential tools to effectively manage distributed teams:

+ +

Remote Employee Management

+
    +
  • Track remote worker profiles, time zones, and work schedules
  • +
  • Manage virtual team structures and reporting relationships
  • +
  • Monitor remote employee performance and productivity
  • +
+ +

Time and Project Tracking

+
    +
  • Record work hours across different time zones
  • +
  • Track project progress and task completion
  • +
  • Generate detailed productivity reports
  • +
+ +

Equipment and Resource Management

+
    +
  • Track company equipment assigned to remote workers
  • +
  • Manage software licenses and digital resources
  • +
  • Monitor equipment maintenance and updates
  • +
+ +

Virtual Communication Tools

+
    +
  • Schedule and track virtual meetings and team events
  • +
  • Facilitate remote team collaboration
  • +
  • Maintain clear communication channels
  • +
+ `, + template: { + type: "base", + template: templates.remoteWorkManagement as IBaseTemplateDTO, + }, + }, ] if (env.NODE_ENV === "development") { diff --git a/packages/queries/src/get-pivot-data.query.ts b/packages/queries/src/get-pivot-data.query.ts new file mode 100644 index 000000000..796b96fc8 --- /dev/null +++ b/packages/queries/src/get-pivot-data.query.ts @@ -0,0 +1,24 @@ +import { Query, type QueryProps } from "@undb/domain" +import { getPivotDataDTO } from "@undb/table" +import { z } from "@undb/zod" + +export const getPivotDataQuery = getPivotDataDTO + +export type IGetPivotDataQuery = z.infer + +export class GetPivotDataQuery extends Query implements IGetPivotDataQuery { + public readonly tableId?: string + public readonly baseName?: string + public readonly tableName?: string + public readonly viewId?: string + public readonly viewName?: string + + constructor(props: QueryProps) { + super() + this.tableId = props.tableId + this.viewId = props.viewId + this.baseName = props.baseName + this.tableName = props.tableName + this.viewName = props.viewName + } +} diff --git a/packages/queries/src/get-share-pivot-data.query.ts b/packages/queries/src/get-share-pivot-data.query.ts new file mode 100644 index 000000000..27ce1d949 --- /dev/null +++ b/packages/queries/src/get-share-pivot-data.query.ts @@ -0,0 +1,29 @@ +import { Query, type QueryProps } from "@undb/domain" +import { shareIdSchema } from "@undb/share" +import { getPivotDataOutput, tableId, viewId } from "@undb/table" +import { z } from "@undb/zod" + +export const getSharePivotDataQuery = z.object({ + shareId: shareIdSchema, + tableId: tableId.optional(), + viewId: viewId.optional(), +}) + +export type IGetSharePivotDataQuery = z.infer + +export const getSharePivotDataOutput = getPivotDataOutput + +export type IGetSharePivotDataOutput = z.infer + +export class GetSharePivotDataQuery extends Query implements IGetSharePivotDataQuery { + public readonly shareId: string + public readonly tableId: string | undefined + public readonly viewId: string | undefined + + constructor(props: QueryProps) { + super() + this.shareId = props.shareId + this.tableId = props.tableId + this.viewId = props.viewId + } +} diff --git a/packages/queries/src/index.ts b/packages/queries/src/index.ts index d6ae52a77..3afb1d82d 100644 --- a/packages/queries/src/index.ts +++ b/packages/queries/src/index.ts @@ -13,6 +13,7 @@ export * from "./get-member-by-id.query" export * from "./get-member-spaces.query" export * from "./get-members-by-ids.query" export * from "./get-members.query" +export * from "./get-pivot-data.query" export * from "./get-readable-record-by-id.query" export * from "./get-readable-records.query" export * from "./get-record-audits.query" @@ -20,6 +21,7 @@ export * from "./get-record-by-id.query" export * from "./get-records.query" export * from "./get-rollup-foreign-tables.query" export * from "./get-share-aggregate.query" +export * from "./get-share-pivot-data.query" export * from "./get-share-record-by-id.query" export * from "./get-share-records.query" export * from "./get-share.query" diff --git a/packages/query-handlers/src/handlers/get-pivot-data.query-handler.ts b/packages/query-handlers/src/handlers/get-pivot-data.query-handler.ts new file mode 100644 index 000000000..bc9e23a04 --- /dev/null +++ b/packages/query-handlers/src/handlers/get-pivot-data.query-handler.ts @@ -0,0 +1,20 @@ +import { queryHandler } from "@undb/cqrs" +import { singleton } from "@undb/di" +import type { IQueryHandler } from "@undb/domain" +import { GetPivotDataQuery, type IGetPivotDataQuery } from "@undb/queries" +import { injectRecordsQueryService, type IGetPivotDataOutput, type IRecordsQueryService } from "@undb/table" + +@queryHandler(GetPivotDataQuery) +@singleton() +export class GetPivotDataQueryHandler implements IQueryHandler { + constructor( + @injectRecordsQueryService() + private readonly svc: IRecordsQueryService, + ) {} + + async execute(query: IGetPivotDataQuery): Promise { + const data = await this.svc.getPivotData(query) + + return data + } +} diff --git a/packages/query-handlers/src/handlers/get-share-pivot-data.query-handler.ts b/packages/query-handlers/src/handlers/get-share-pivot-data.query-handler.ts new file mode 100644 index 000000000..21ee51545 --- /dev/null +++ b/packages/query-handlers/src/handlers/get-share-pivot-data.query-handler.ts @@ -0,0 +1,26 @@ +import { setContextValue } from "@undb/context/server" +import { queryHandler } from "@undb/cqrs" +import { singleton } from "@undb/di" +import type { IQueryHandler } from "@undb/domain" +import { GetSharePivotDataQuery, type IGetSharePivotDataOutput, type IGetSharePivotDataQuery } from "@undb/queries" +import { injectShareService, type IShareService } from "@undb/share" +import { injectSpaceService, type ISpaceService } from "@undb/space" + +@queryHandler(GetSharePivotDataQuery) +@singleton() +export class GetSharePivotDataQueryHandler implements IQueryHandler { + constructor( + @injectShareService() + private readonly svc: IShareService, + @injectSpaceService() + private readonly spaceService: ISpaceService, + ) {} + + async execute(query: IGetSharePivotDataQuery): Promise { + const { shareId } = query + await this.spaceService.setSpaceContext(setContextValue, { shareId }) + const data = await this.svc.getSharePivotData(shareId, query) + + return data + } +} diff --git a/packages/query-handlers/src/handlers/index.ts b/packages/query-handlers/src/handlers/index.ts index 33dac1a41..89ea70890 100644 --- a/packages/query-handlers/src/handlers/index.ts +++ b/packages/query-handlers/src/handlers/index.ts @@ -13,6 +13,7 @@ import { GetMemberByIdQueryHandler } from "./get-member-by-id.query-handler" import { GetMemberSpacesQueryHandler } from "./get-member-spaces.query-handler" import { GetMembersByIdsQueryHandler } from "./get-members-by-ids.query-handler" import { GetMembersQueryHandler } from "./get-members.query-handler" +import { GetPivotDataQueryHandler } from "./get-pivot-data.query-handler" import { GetReadableRecordByIdHandler } from "./get-readable-record-by-id.query-handler" import { GetReadableRecordsHandler } from "./get-readable-records.query-handler" import { GetRecordAuditsQueryHandler } from "./get-record-audits.query-handler" @@ -20,6 +21,7 @@ import { GetRecordByIdQueryHandler } from "./get-record-by-id.query-handler" import { GetRecordsQueryHandler } from "./get-records.query-handler" import { GetRollupForeignTablesTablesQueryHandler } from "./get-rollup-foreign-tables.query-handler" import { GetShareAggregatesQueryHandler } from "./get-share-aggregates.query-handler" +import { GetSharePivotDataQueryHandler } from "./get-share-pivot-data.query-handler" import { GetShareRecordByIdQueryHandler } from "./get-share-record-by-id.query-handler" import { GetShareRecordsQueryHandler } from "./get-share-records.query-handler" import { GetShareQueryHandler } from "./get-share.query-handler" @@ -74,4 +76,6 @@ export const queryHandlers = [ GetDashboardByIdQueryHandler, GetDashboardByShareQueryHandler, GetTableByShareDashboardQueryHandler, + GetPivotDataQueryHandler, + GetSharePivotDataQueryHandler, ] diff --git a/packages/share/src/services/share.service.ts b/packages/share/src/services/share.service.ts index 6edb50b8e..068016def 100644 --- a/packages/share/src/services/share.service.ts +++ b/packages/share/src/services/share.service.ts @@ -15,6 +15,9 @@ import { injectRecordsQueryService, injectTableQueryRepository, injectTableRepository, + withUniqueTable, + type IGetPivotDataDTO, + type IGetPivotDataOutput, type IRecordDTO, type IRecordQueryRepository, type IRecordsQueryService, @@ -55,6 +58,7 @@ export interface IShareService { select?: string[], pagination?: IPagination, ): Promise> + getSharePivotData(shareId: string, dto: IGetPivotDataDTO): Promise getShareRecordById(id: string, recordId: string, tableId?: string, viewId?: string): Promise> } @@ -172,6 +176,22 @@ export class ShareService implements IShareService { return (await this.tableQueryRepo.findOne(spec)).expect("table not found") } + async getSharePivotData(shareId: string, dto: IGetPivotDataDTO): Promise { + const share = (await this.repo.findOneById(shareId)).expect("share not found") + if (share.target.type !== "view") { + throw new Error("invalid share target") + } + + const spec = withUniqueTable(dto).expect("invalid unique table specification") + const table = (await this.tableRepo.findOne(Some(spec))).expect("table not found") + const view = table.views.getViewById(share.target.id) + if (!view) { + throw new Error("view not found") + } + + return this.recordsService.getPivotData({ tableId: table.id.value, viewId: view.id.value }) + } + async getShareRecords( shareId: string, tableId?: string, diff --git a/packages/table/src/dto/update-view.dto.ts b/packages/table/src/dto/update-view.dto.ts index 679533da0..1c0c92c1a 100644 --- a/packages/table/src/dto/update-view.dto.ts +++ b/packages/table/src/dto/update-view.dto.ts @@ -5,6 +5,7 @@ import { updateGridViewDTO, updateKanbanViewDTO, updateListViewDTO, + updatePivotViewDTO, } from "../modules" export const updateViewDTO = z.discriminatedUnion("type", [ @@ -13,6 +14,7 @@ export const updateViewDTO = z.discriminatedUnion("type", [ updateGalleryViewDTO, updateListViewDTO, updateCalendarViewDTO, + updatePivotViewDTO, ]) export type IUpdateViewDTO = z.infer diff --git a/packages/table/src/modules/aggregate/aggregate.vo.ts b/packages/table/src/modules/aggregate/aggregate.vo.ts index d2aa97df3..97741bd79 100644 --- a/packages/table/src/modules/aggregate/aggregate.vo.ts +++ b/packages/table/src/modules/aggregate/aggregate.vo.ts @@ -1,8 +1,10 @@ import { ValueObject } from "@undb/domain" import { z } from "@undb/zod" -import { fieldId, fieldName, type FieldType } from "../schema" import { createConditionGroup } from "../schema/fields/condition/condition.type" import { parseValidCondition } from "../schema/fields/condition/condition.util" +import { fieldId } from "../schema/fields/field-id.vo" +import { fieldName } from "../schema/fields/field-name.vo" +import type { FieldType } from "../schema/fields/field.type" const aggregateCondition = z.undefined() export type IAggregateCondition = typeof aggregateCondition diff --git a/packages/table/src/modules/chart/chart.vo.ts b/packages/table/src/modules/chart/chart.vo.ts index 9bd2facb4..054e6eb63 100644 --- a/packages/table/src/modules/chart/chart.vo.ts +++ b/packages/table/src/modules/chart/chart.vo.ts @@ -1,6 +1,6 @@ import { ValueObject } from "@undb/domain" import { z } from "@undb/zod" -import { fieldId } from "../schema" +import { fieldId } from "../schema/fields/field-id.vo" const pieChart = z.object({ type: z.literal("pie"), diff --git a/packages/table/src/modules/records/dto/get-pivot-data.dto.ts b/packages/table/src/modules/records/dto/get-pivot-data.dto.ts new file mode 100644 index 000000000..030e6deb3 --- /dev/null +++ b/packages/table/src/modules/records/dto/get-pivot-data.dto.ts @@ -0,0 +1,22 @@ +import { z } from "@undb/zod" +import { uniqueTableDTO } from "../../../dto/unique-table.dto" +import { pivotAggregateSchema, viewId, viewName } from "../../views" + +export const getPivotDataDTO = z + .object({ + viewId: viewId.optional(), + viewName: viewName.optional(), + }) + .merge(uniqueTableDTO) + +export type IGetPivotDataDTO = z.infer + +export const getPivotDataOutput = z + .object({ + label: z.string(), + values: z.record(z.number()), + agg: pivotAggregateSchema, + }) + .array() + +export type IGetPivotDataOutput = z.infer diff --git a/packages/table/src/modules/records/dto/get-record-by-id.dto.ts b/packages/table/src/modules/records/dto/get-record-by-id.dto.ts index dcad8fd4d..7b357d504 100644 --- a/packages/table/src/modules/records/dto/get-record-by-id.dto.ts +++ b/packages/table/src/modules/records/dto/get-record-by-id.dto.ts @@ -1,6 +1,6 @@ import { z } from "@undb/zod" import { uniqueTableDTO } from "../../../dto/unique-table.dto" -import { fieldId } from "../../schema" +import { fieldId } from "../../schema/fields/field-id.vo" import { viewId } from "../../views/view/view-id.vo" import { viewName } from "../../views/view/view-name.vo" import { recordId } from "../record" diff --git a/packages/table/src/modules/records/dto/get-records.dto.ts b/packages/table/src/modules/records/dto/get-records.dto.ts index 612d6265f..26aaabc78 100644 --- a/packages/table/src/modules/records/dto/get-records.dto.ts +++ b/packages/table/src/modules/records/dto/get-records.dto.ts @@ -1,7 +1,7 @@ import { pagniationSchema } from "@undb/domain" import { z } from "@undb/zod" import { uniqueTableDTO } from "../../../dto/unique-table.dto" -import { fieldId } from "../../schema" +import { fieldId } from "../../schema/fields/field-id.vo" import { viewFilterGroup, viewId, viewName } from "../../views" export const getRecordsDTO = z diff --git a/packages/table/src/modules/records/dto/index.ts b/packages/table/src/modules/records/dto/index.ts index 8f409f5db..b11c9e1f0 100644 --- a/packages/table/src/modules/records/dto/index.ts +++ b/packages/table/src/modules/records/dto/index.ts @@ -1,4 +1,5 @@ export * from "./count-records.dto" +export * from "./get-pivot-data.dto" export * from "./get-record-by-id.dto" export * from "./get-records.dto" export * from "./record-aggregate.dto" diff --git a/packages/table/src/modules/records/events/record-events-meta.ts b/packages/table/src/modules/records/events/record-events-meta.ts index 2d4eca61a..b873a8dfe 100644 --- a/packages/table/src/modules/records/events/record-events-meta.ts +++ b/packages/table/src/modules/records/events/record-events-meta.ts @@ -1,6 +1,7 @@ import { z } from "@undb/zod" import { tableName } from "../../../table-name.vo" -import { fieldId, fieldName } from "../../schema" +import { fieldId } from "../../schema/fields/field-id.vo" +import { fieldName } from "../../schema/fields/field-name.vo" export const recordEventTableMeta = z.object({ name: tableName, diff --git a/packages/table/src/modules/records/record/dto/trigger-record-button.dto.ts b/packages/table/src/modules/records/record/dto/trigger-record-button.dto.ts index 3cf475055..62578eb28 100644 --- a/packages/table/src/modules/records/record/dto/trigger-record-button.dto.ts +++ b/packages/table/src/modules/records/record/dto/trigger-record-button.dto.ts @@ -1,5 +1,6 @@ import { z } from "@undb/zod" -import { fieldId, fieldName } from "../../../schema" +import { fieldId } from "../../../schema/fields/field-id.vo" +import { fieldName } from "../../../schema/fields/field-name.vo" import { recordId } from "../record-id.vo" export const triggerRecordButtonDTO = z.object({ diff --git a/packages/table/src/modules/records/record/record.repository.ts b/packages/table/src/modules/records/record/record.repository.ts index bdbb94768..38a548e51 100644 --- a/packages/table/src/modules/records/record/record.repository.ts +++ b/packages/table/src/modules/records/record/record.repository.ts @@ -3,7 +3,7 @@ import type { TableId } from "../../../table-id.vo" import type { TableDo } from "../../../table.do" import { getSpec } from "../../schema/fields/condition" import type { IViewAggregate, View, ViewId } from "../../views" -import type { AggregateResult, ICountRecordsDTO, IGetRecordsDTO } from "../dto" +import type { AggregateResult, ICountRecordsDTO, IGetPivotDataOutput, IGetRecordsDTO } from "../dto" import { withQ } from "../specification/with-q.specification" import type { IRecordDTO } from "./dto" import type { RecordId } from "./record-id.vo" @@ -50,6 +50,7 @@ export interface IRecordQueryRepository { findOneById(table: TableDo, id: RecordId, query: Option): Promise> count(tableId: TableId): Promise countWhere(table: TableDo, spec: Option): Promise + getPivotData(table: TableDo, viewId: string): Promise aggregate( table: TableDo, diff --git a/packages/table/src/modules/records/services/methods/get-pivot-data.method.ts b/packages/table/src/modules/records/services/methods/get-pivot-data.method.ts new file mode 100644 index 000000000..54b40296a --- /dev/null +++ b/packages/table/src/modules/records/services/methods/get-pivot-data.method.ts @@ -0,0 +1,14 @@ +import { Some } from "@undb/domain" +import { withUniqueTable } from "../../../../specifications" +import type { IGetPivotDataDTO, IGetPivotDataOutput } from "../../dto" +import type { RecordsQueryService } from "../records.query-service" + +export async function getPivotData(this: RecordsQueryService, dto: IGetPivotDataDTO): Promise { + const spec = withUniqueTable(dto).expect("Invalid unique table specification") + + const table = (await this.tableRepository.findOne(Some(spec))).expect("Table not found") + const view = table.views.getViewByNameOrId(dto.viewName, dto.viewId) + + const data = await this.repo.getPivotData(table, view.id.value) + return data +} diff --git a/packages/table/src/modules/records/services/records.query-service.ts b/packages/table/src/modules/records/services/records.query-service.ts index 5b8ad751a..bf6af8b18 100644 --- a/packages/table/src/modules/records/services/records.query-service.ts +++ b/packages/table/src/modules/records/services/records.query-service.ts @@ -1,12 +1,20 @@ import { singleton } from "@undb/di" -import type { Option, PaginatedDTO } from "@undb/domain" +import type { Option,PaginatedDTO } from "@undb/domain" import { group } from "radash" import type { TableDo } from "../../../table.do" import type { ITableRepository } from "../../../table.repository" import { injectTableRepository } from "../../../table.repository.provider" import { type IAttachmentFieldValue } from "../../schema" -import { injectObjectStorage, type IObjectStorage } from "../../storage" -import type { AggregateResult, ICountRecordsDTO, IGetAggregatesDTO, IGetRecordByIdDTO, IGetRecordsDTO } from "../dto" +import { injectObjectStorage,type IObjectStorage } from "../../storage" +import type { + AggregateResult, + ICountRecordsDTO, + IGetAggregatesDTO, + IGetPivotDataDTO, + IGetPivotDataOutput, + IGetRecordByIdDTO, + IGetRecordsDTO, +} from "../dto" import { injectRecordQueryRepository, type IReadableRecordDTO, @@ -16,6 +24,7 @@ import { } from "../record" import { countRecords } from "./methods/count-records.method" import { getAggregates } from "./methods/get-aggregates.method" +import { getPivotData } from "./methods/get-pivot-data.method" import { getReadableRecordById } from "./methods/get-readable-record-by-id.method" import { getReadableRecords } from "./methods/get-readable-records.method" import { getRecordById } from "./methods/get-record-by-id.method" @@ -30,6 +39,7 @@ export interface IRecordsQueryService { getAggregates(query: IGetAggregatesDTO): Promise> populateAttachments(dto: IGetRecordsDTO, table: TableDo, records: IRecordDTO[]): Promise populateAttachment(dto: IGetRecordsDTO, table: TableDo, value: IRecordDTO["values"]): Promise + getPivotData(query: IGetPivotDataDTO): Promise } @singleton() @@ -49,6 +59,7 @@ export class RecordsQueryService implements IRecordsQueryService { getReadableRecords = getReadableRecords getReadableRecordById = getReadableRecordById getAggregates = getAggregates + getPivotData = getPivotData async populateAttachments( this: RecordsQueryService, diff --git a/packages/table/src/modules/schema/fields/field.type.ts b/packages/table/src/modules/schema/fields/field.type.ts index dec27a2a0..1d16a4813 100644 --- a/packages/table/src/modules/schema/fields/field.type.ts +++ b/packages/table/src/modules/schema/fields/field.type.ts @@ -83,6 +83,7 @@ import type { ICurrencyFieldConstraint, ICurrencyFieldOption, } from "./variants/currency-field" +import type { IDateFieldMacro } from "./variants/date-field/date-field-macro" import type { DateRangeFieldValue } from "./variants/date-range-field/date-range-field-value.vo" import type { DATE_RANGE_TYPE, DateRangeField } from "./variants/date-range-field/date-range-field.vo" import type { @@ -327,4 +328,4 @@ export type IFieldOption = | ICurrencyFieldOption | IFormulaFieldOption -export type IFieldMacro = IUserFieldMacro +export type IFieldMacro = IUserFieldMacro | IDateFieldMacro diff --git a/packages/table/src/modules/schema/fields/variants/date-field/date-field-constraint.vo.ts b/packages/table/src/modules/schema/fields/variants/date-field/date-field-constraint.vo.ts index 286d56816..8bccccbdd 100644 --- a/packages/table/src/modules/schema/fields/variants/date-field/date-field-constraint.vo.ts +++ b/packages/table/src/modules/schema/fields/variants/date-field/date-field-constraint.vo.ts @@ -2,6 +2,7 @@ import { Some } from "@undb/domain" import { z } from "@undb/zod" import type { FormFieldVO } from "../../../../forms/form/form-field.vo" import { FieldConstraintVO, baseFieldConstraint } from "../../field-constraint.vo" +import { dateFieldMacroSchema } from "./date-field-macro" export const dateFieldConstraint = z .object({ @@ -20,7 +21,13 @@ export class DateFieldConstraint extends FieldConstraintVO }) } override get schema() { - let base: z.ZodTypeAny = z.string().date().or(z.string().datetime()).or(z.string().date()).or(z.date()) + let base: z.ZodTypeAny = z + .string() + .date() + .or(z.string().datetime()) + .or(z.string().date()) + .or(z.date()) + .or(dateFieldMacroSchema) if (!this.props.required) { base = base.optional().nullable() } diff --git a/packages/table/src/modules/schema/fields/variants/date-field/date-field-macro.ts b/packages/table/src/modules/schema/fields/variants/date-field/date-field-macro.ts new file mode 100644 index 000000000..55a63fabd --- /dev/null +++ b/packages/table/src/modules/schema/fields/variants/date-field/date-field-macro.ts @@ -0,0 +1,11 @@ +import { z } from "@undb/zod" + +export const dateFieldMacroSchema = z.enum(["@now", "@today", "@yesterday", "@tomorrow"]) + +export const dateFieldMacros = dateFieldMacroSchema.options + +export type IDateFieldMacro = z.infer + +export function isDateFieldMacro(value: string): value is IDateFieldMacro { + return dateFieldMacros.includes(value as any) +} diff --git a/packages/table/src/modules/schema/fields/variants/date-field/date-field-value.vo.ts b/packages/table/src/modules/schema/fields/variants/date-field/date-field-value.vo.ts index 0e65f84f1..b12728a1d 100644 --- a/packages/table/src/modules/schema/fields/variants/date-field/date-field-value.vo.ts +++ b/packages/table/src/modules/schema/fields/variants/date-field/date-field-value.vo.ts @@ -1,13 +1,23 @@ import { z } from "@undb/zod" import { isString } from "radash" import { FieldValueObject } from "../../field-value" +import { dateFieldMacroSchema, isDateFieldMacro } from "./date-field-macro" -export const dateFieldValue = z.union([z.date(), z.string().datetime(), z.string().date(), z.null(), z.undefined()]) +export const dateFieldValue = z.union([ + z.date(), + z.string().datetime(), + z.string().date(), + dateFieldMacroSchema, + z.null(), + z.undefined(), +]) export type IDateFieldValue = z.infer export class DateFieldValue extends FieldValueObject { constructor(value: IDateFieldValue) { - super({ value: isString(value) ? new Date(value) : value ?? null }) + super({ + value: isString(value) ? (isDateFieldMacro(value) ? value : new Date(value)) : (value ?? null), + }) } isEmpty() { diff --git a/packages/table/src/modules/schema/fields/variants/date-field/date-field.specification.ts b/packages/table/src/modules/schema/fields/variants/date-field/date-field.specification.ts index 4dd9c7ef1..8acb05eb4 100644 --- a/packages/table/src/modules/schema/fields/variants/date-field/date-field.specification.ts +++ b/packages/table/src/modules/schema/fields/variants/date-field/date-field.specification.ts @@ -4,18 +4,24 @@ import type { IRecordVisitor } from "../../../../records/record/record-visitor.i import { RecordComositeSpecification } from "../../../../records/record/record.composite-specification" import type { RecordDO } from "../../../../records/record/record.do" import type { FieldId } from "../../field-id.vo" +import type { IDateFieldMacro } from "./date-field-macro" import { DateFieldValue } from "./date-field-value.vo" export class DateEqual extends RecordComositeSpecification { constructor( - readonly date: Date | null, + readonly date: Date | null | IDateFieldMacro, readonly fieldId: FieldId, ) { super(fieldId) } isSatisfiedBy(t: RecordDO): boolean { const value = t.getValue(this.fieldId) - return value.mapOr(false, (v) => v.value instanceof Date && this.date !== null && isEqual(v.value, this.date)) + return value.mapOr(false, (v) => { + if (this.date === "@now") { + return v.value instanceof Date && isEqual(v.value, new Date()) + } + return v.value instanceof Date && this.date !== null && isEqual(v.value, this.date) + }) } mutate(t: RecordDO): Result { t.values.setValue(this.fieldId, new DateFieldValue(this.date)) diff --git a/packages/table/src/modules/schema/fields/variants/date-field/date-field.vo.ts b/packages/table/src/modules/schema/fields/variants/date-field/date-field.vo.ts index 0e2789096..24a279c39 100644 --- a/packages/table/src/modules/schema/fields/variants/date-field/date-field.vo.ts +++ b/packages/table/src/modules/schema/fields/variants/date-field/date-field.vo.ts @@ -10,6 +10,7 @@ import { AbstractField, baseFieldDTO, createBaseFieldDTO } from "../abstract-fie import { createAbstractDateConditionMather } from "../abstractions/abstract-date-field.condition" import { abstractDateAggregate } from "../abstractions/abstract-date.aggregate" import { dateFieldConstraint, DateFieldConstraint } from "./date-field-constraint.vo" +import { isDateFieldMacro } from "./date-field-macro" import { dateFieldValue, DateFieldValue } from "./date-field-value.vo" import { createDateFieldCondition, @@ -106,6 +107,15 @@ export class DateField extends AbstractField { } override getMutationSpec(value: DateFieldValue): Option { - return Some(new DateEqual(isString(value.value) ? new Date(value.value) : (value.value ?? null), this.id)) + return Some( + new DateEqual( + isString(value.value) + ? isDateFieldMacro(value.value) + ? value.value + : new Date(value.value) + : (value.value ?? null), + this.id, + ), + ) } } diff --git a/packages/table/src/modules/schema/fields/variants/date-field/index.ts b/packages/table/src/modules/schema/fields/variants/date-field/index.ts index 9a1c44c5b..943e10d7f 100644 --- a/packages/table/src/modules/schema/fields/variants/date-field/index.ts +++ b/packages/table/src/modules/schema/fields/variants/date-field/index.ts @@ -1,4 +1,5 @@ export * from "./date-field-constraint.vo" +export * from "./date-field-macro" export * from "./date-field-value.visitor" export * from "./date-field-value.vo" export * from "./date-field.condition" diff --git a/packages/table/src/modules/schema/schema.vo.ts b/packages/table/src/modules/schema/schema.vo.ts index abebe0a21..6b32368bc 100644 --- a/packages/table/src/modules/schema/schema.vo.ts +++ b/packages/table/src/modules/schema/schema.vo.ts @@ -11,7 +11,8 @@ import { import type { TableDo } from "../../table.do" import type { FormVO } from "../forms" import type { IRecordValues } from "../records/record/record-values.vo" -import type { View } from "../views" +import { type IPivotAggregate, type View } from "../views" +import { isValidColumnLabel, isValidRowLabel, isValidValueField } from "../views/view/variants/pivot-view.vo" import type { ICreateSchemaDTO } from "./dto" import type { ISchemaDTO } from "./dto/schema.dto" import { @@ -368,4 +369,16 @@ export class Schema extends ValueObject { getGalleryFields(fields: Field[] = this.fields) { return fields.filter((f) => f.type === "attachment") } + + getPivotFields(type: "column" | "row", fields: Field[] = this.fields) { + if (type === "column") { + return fields.filter(isValidColumnLabel) + } + + return fields.filter(isValidRowLabel) + } + + getPivotValueFields(aggregate: IPivotAggregate, fields: Field[] = this.fields) { + return fields.filter((field) => isValidValueField(aggregate, field)) + } } diff --git a/packages/table/src/modules/views/dto/create-view.dto.ts b/packages/table/src/modules/views/dto/create-view.dto.ts index 2ba376535..70aacd73a 100644 --- a/packages/table/src/modules/views/dto/create-view.dto.ts +++ b/packages/table/src/modules/views/dto/create-view.dto.ts @@ -14,6 +14,7 @@ import { } from "../view" import { createGalleryViewDTO } from "../view/variants/gallery-view.vo" import { createKanbanViewDTO } from "../view/variants/kanban-view.vo" +import { createPivotViewDTO } from "../view/variants/pivot-view.vo" import { viewId } from "../view/view-id.vo" import { viewName } from "../view/view-name.vo" import { viewType } from "../view/view.type" @@ -38,6 +39,7 @@ export const createViewDTO = z.discriminatedUnion("type", [ createGalleryViewDTO, createListViewDTO, createCalendarViewDTO, + createPivotViewDTO, ]) export const createViewsDTO = z.array(createViewDTO).refine( @@ -55,6 +57,7 @@ export const createViewWithoutNameDTO = z.discriminatedUnion("type", [ createGalleryViewDTO.omit({ name: true }), createListViewDTO.omit({ name: true }), createCalendarViewDTO.omit({ name: true }), + createPivotViewDTO.omit({ name: true }), ]) export const createViewsWithoutNameDTO = z.array(createViewWithoutNameDTO).refine( @@ -74,6 +77,7 @@ export const createTableViewDTO = z.discriminatedUnion("type", [ createGalleryViewDTO.merge(z.object({ tableId })), createListViewDTO.merge(z.object({ tableId })), createCalendarViewDTO.merge(z.object({ tableId })), + createPivotViewDTO.merge(z.object({ tableId })), ]) export type ICreateTableViewDTO = z.infer diff --git a/packages/table/src/modules/views/view/dto/view.dto.ts b/packages/table/src/modules/views/view/dto/view.dto.ts index 5171dccf2..8b3f0ba5c 100644 --- a/packages/table/src/modules/views/view/dto/view.dto.ts +++ b/packages/table/src/modules/views/view/dto/view.dto.ts @@ -4,6 +4,7 @@ import { galleryViewDTO } from "../variants/gallery-view.vo" import { gridViewDTO } from "../variants/grid-view.vo" import { kanbanViewDTO } from "../variants/kanban-view.vo" import { listViewDTO } from "../variants/list-view.vo" +import { pivotViewDTO } from "../variants/pivot-view.vo" export const viewDTO = z.discriminatedUnion("type", [ gridViewDTO, @@ -11,6 +12,7 @@ export const viewDTO = z.discriminatedUnion("type", [ galleryViewDTO, listViewDTO, calendarViewDTO, + pivotViewDTO, ]) export type IViewDTO = z.infer diff --git a/packages/table/src/modules/views/view/variants/abstract-view.vo.ts b/packages/table/src/modules/views/view/variants/abstract-view.vo.ts index c4d733bc5..2ed49c952 100644 --- a/packages/table/src/modules/views/view/variants/abstract-view.vo.ts +++ b/packages/table/src/modules/views/view/variants/abstract-view.vo.ts @@ -22,13 +22,13 @@ import type { Field } from "../../../schema" import { WidgetVO, widgetDTO, type IWidgetDTO } from "../../../widgets/widget.vo" import type { IViewDTO } from "../dto" import { ViewAggregateVO, viewAggregate, type IViewAggregate } from "../view-aggregate/view-aggregate.vo" -import { ViewColor, viewColorGroup, type IRootViewColor } from "../view-color" -import { ViewFields, viewFields, type IViewFields } from "../view-fields" +import { ViewColor, viewColorGroup, type IRootViewColor } from "../view-color/view-color.vo" +import { ViewFields, viewFields, type IViewFields } from "../view-fields/view-fields.vo" import { ViewFilter, viewFilterGroup, type IRootViewFilter } from "../view-filter/view-filter.vo" import { ViewIdVo, viewId, type ViewId } from "../view-id.vo" import { ViewNameVo, viewName } from "../view-name.vo" import { ViewOption, viewOption, type IViewOption } from "../view-option.vo" -import { ViewSort, viewSort, type IViewSort } from "../view-sort" +import { ViewSort, viewSort, type IViewSort } from "../view-sort/view-sort.vo" import type { View, ViewType } from "../view.type" export const createBaseViewDTO = z.object({ diff --git a/packages/table/src/modules/views/view/variants/calendar-view.vo.ts b/packages/table/src/modules/views/view/variants/calendar-view.vo.ts index 17226ad71..b7aa39e7c 100644 --- a/packages/table/src/modules/views/view/variants/calendar-view.vo.ts +++ b/packages/table/src/modules/views/view/variants/calendar-view.vo.ts @@ -3,7 +3,7 @@ import { z } from "@undb/zod" import type { IDuplicateViewDTO } from "../../../../dto/duplicate-view.dto" import { WithNewView, WithView } from "../../../../specifications/table-view.specification" import type { TableDo } from "../../../../table.do" -import { fieldId } from "../../../schema" +import { fieldId } from "../../../schema/fields/field-id.vo" import { ViewIdVo } from "../view-id.vo" import { AbstractView, baseViewDTO, createBaseViewDTO, updateBaseViewDTO } from "./abstract-view.vo" diff --git a/packages/table/src/modules/views/view/variants/gallery-view.vo.ts b/packages/table/src/modules/views/view/variants/gallery-view.vo.ts index 73a28119c..60fe78642 100644 --- a/packages/table/src/modules/views/view/variants/gallery-view.vo.ts +++ b/packages/table/src/modules/views/view/variants/gallery-view.vo.ts @@ -3,7 +3,7 @@ import { z } from "@undb/zod" import type { IDuplicateViewDTO } from "../../../../dto/duplicate-view.dto" import { WithNewView, WithView } from "../../../../specifications/table-view.specification" import type { TableDo } from "../../../../table.do" -import { fieldId } from "../../../schema" +import { fieldId } from "../../../schema/fields/field-id.vo" import { ViewIdVo } from "../view-id.vo" import { AbstractView, baseViewDTO, createBaseViewDTO, updateBaseViewDTO } from "./abstract-view.vo" diff --git a/packages/table/src/modules/views/view/variants/index.ts b/packages/table/src/modules/views/view/variants/index.ts index 422f5bd83..1d3f3d582 100644 --- a/packages/table/src/modules/views/view/variants/index.ts +++ b/packages/table/src/modules/views/view/variants/index.ts @@ -3,3 +3,5 @@ export * from "./gallery-view.vo" export * from "./grid-view.vo" export * from "./kanban-view.vo" export * from "./list-view.vo" +export * from "./pivot-view.vo" + diff --git a/packages/table/src/modules/views/view/variants/kanban-view.vo.ts b/packages/table/src/modules/views/view/variants/kanban-view.vo.ts index 2231d211d..3f047d3b7 100644 --- a/packages/table/src/modules/views/view/variants/kanban-view.vo.ts +++ b/packages/table/src/modules/views/view/variants/kanban-view.vo.ts @@ -3,7 +3,7 @@ import { z } from "@undb/zod" import type { IDuplicateViewDTO } from "../../../../dto/duplicate-view.dto" import { WithNewView, WithView } from "../../../../specifications/table-view.specification" import type { TableDo } from "../../../../table.do" -import { fieldId } from "../../../schema" +import { fieldId } from "../../../schema/fields/field-id.vo" import { ViewIdVo } from "../view-id.vo" import { AbstractView, baseViewDTO, createBaseViewDTO, updateBaseViewDTO } from "./abstract-view.vo" diff --git a/packages/table/src/modules/views/view/variants/pivot-view.vo.ts b/packages/table/src/modules/views/view/variants/pivot-view.vo.ts new file mode 100644 index 000000000..f51214053 --- /dev/null +++ b/packages/table/src/modules/views/view/variants/pivot-view.vo.ts @@ -0,0 +1,167 @@ +import { None, Option, Some } from "@undb/domain" +import { z } from "@undb/zod" +import type { IDuplicateViewDTO } from "../../../../dto/duplicate-view.dto" +import { WithNewView, WithView } from "../../../../specifications/table-view.specification" +import type { TableDo } from "../../../../table.do" +import { fieldId } from "../../../schema/fields/field-id.vo" +import type { Field } from "../../../schema/fields/field.type" +import { ViewIdVo } from "../view-id.vo" +import { AbstractView, baseViewDTO, createBaseViewDTO, updateBaseViewDTO } from "./abstract-view.vo" + +export const PIVOT_TYPE = "pivot" as const + +export const PIVOT_AGGREGATE = ["sum", "count", "average", "max", "min"] as const +export const DEFAULT_PIVOT_AGGREGATE = "sum" + +export type IPivotAggregate = (typeof PIVOT_AGGREGATE)[number] + +export const pivotAggregateSchema = z.enum(PIVOT_AGGREGATE) + +export const pivotOption = z.object({ + columnLabel: fieldId.optional(), + rowLabel: fieldId.optional(), + value: fieldId.optional(), + aggregate: pivotAggregateSchema.optional(), +}) + +export type IPivotOption = z.infer + +export const createPivotViewDTO = createBaseViewDTO.extend({ + type: z.literal(PIVOT_TYPE), + pivot: pivotOption.optional(), +}) + +export type ICreatePivotViewDTO = z.infer + +export const pivotViewDTO = baseViewDTO.extend({ + type: z.literal(PIVOT_TYPE), + pivot: pivotOption.optional(), +}) + +export type IPivotViewDTO = z.infer + +export const updatePivotViewDTO = updateBaseViewDTO.merge( + z.object({ + type: z.literal(PIVOT_TYPE), + pivot: pivotOption.optional(), + }), +) + +export type IUpdatePivotViewDTO = z.infer + +export class PivotView extends AbstractView { + pivot: Option = None + + get columnLabel() { + return this.pivot.map((x) => x.columnLabel) + } + + get rowLabel() { + return this.pivot.map((x) => x.rowLabel) + } + + get value() { + return this.pivot.map((x) => x.value) + } + + get pivotAggregate() { + return this.pivot.map((x) => x.aggregate) + } + + get isValid() { + const pivotAggregate = this.pivotAggregate.into(undefined) + if (pivotAggregate === "count") { + return ( + this.columnLabel.isSome() && !!this.columnLabel.unwrap() && this.rowLabel.isSome() && !!this.rowLabel.unwrap() + ) + } + + return ( + this.columnLabel.isSome() && + !!this.columnLabel.unwrap() && + this.rowLabel.isSome() && + !!this.rowLabel.unwrap() && + this.value.isSome() && + !!this.value.unwrap() && + this.pivotAggregate.isSome() && + !!this.pivotAggregate.unwrap() + ) + } + + constructor(table: TableDo, dto: IPivotViewDTO) { + super(table, dto) + this.pivot = Option(dto.pivot) + } + + static create(table: TableDo, dto: ICreatePivotViewDTO) { + const fields = table.getOrderedFields() + const columnFields = table.schema.getPivotFields("column", fields) + const rowFields = table.schema.getPivotFields("row", fields) + const valueFields = table.schema.getPivotValueFields("sum", fields) + + return new PivotView(table, { + ...dto, + id: ViewIdVo.fromStringOrCreate(dto.id).value, + fields: fields.map((f, index) => ({ fieldId: f.id.value, hidden: index > 5 })), + pivot: { + columnLabel: columnFields.at(0)?.id.value, + rowLabel: rowFields.at(0)?.id.value, + value: valueFields.at(0)?.id.value, + aggregate: DEFAULT_PIVOT_AGGREGATE, + }, + }) + } + + override type = PIVOT_TYPE + + override $update(table: TableDo, input: IUpdatePivotViewDTO): Option { + const json = this.toJSON() + const view = new PivotView(table, { + ...json, + name: input.name, + id: this.id.value, + type: PIVOT_TYPE, + pivot: input.pivot ?? this.pivot.into(undefined), + }) + + return Some(new WithView(this, view)) + } + + override $duplicate(table: TableDo, dto: IDuplicateViewDTO): Option { + const json = this.toJSON() + + return Some( + new WithNewView( + new PivotView(table, { + ...json, + name: dto.name, + pivot: this.pivot.into(undefined), + isDefault: false, + id: ViewIdVo.create().value, + type: PIVOT_TYPE, + }), + ), + ) + } + + toJSON() { + return { ...super.toJSON(), pivot: this.pivot.into(undefined) } + } +} + +export function isValidColumnLabel(field: Field) { + return field.type === "select" && field.isSingle +} + +export function isValidRowLabel(field: Field) { + return ( + field.type === "string" || (field.type === "select" && field.isSingle) || (field.type === "user" && field.isSingle) + ) +} + +export function isValidValueField(aggregate: IPivotAggregate, field: Field) { + if (aggregate === "count") { + return true + } + return field.type === "number" || field.type === "percentage" || field.type === "currency" || field.type === "rating" +} diff --git a/packages/table/src/modules/views/view/view-fields/view-fields.vo.ts b/packages/table/src/modules/views/view/view-fields/view-fields.vo.ts index efe3efcfe..65e0d2235 100644 --- a/packages/table/src/modules/views/view/view-fields/view-fields.vo.ts +++ b/packages/table/src/modules/views/view/view-fields/view-fields.vo.ts @@ -2,7 +2,8 @@ import { ValueObject } from "@undb/domain" import { z } from "@undb/zod" import { isEqual } from "radash" import type { TableDo } from "../../../../table.do" -import { fieldId, type Field } from "../../../schema" +import { fieldId } from "../../../schema/fields/field-id.vo" +import type { Field } from "../../../schema/fields/field.type" export const viewField = z.object({ fieldId, diff --git a/packages/table/src/modules/views/view/view-sort/view-sort.vo.ts b/packages/table/src/modules/views/view/view-sort/view-sort.vo.ts index f7f2bc6fa..f0cac07d3 100644 --- a/packages/table/src/modules/views/view/view-sort/view-sort.vo.ts +++ b/packages/table/src/modules/views/view/view-sort/view-sort.vo.ts @@ -1,7 +1,8 @@ import { ValueObject } from "@undb/domain" import { z } from "@undb/zod" import { isEqual } from "radash" -import { fieldId, type Field } from "../../../schema" +import { fieldId } from "../../../schema/fields/field-id.vo" +import type { Field } from "../../../schema/fields/field.type" export const viewSortOption = z.object({ fieldId, diff --git a/packages/table/src/modules/views/view/view.factory.ts b/packages/table/src/modules/views/view/view.factory.ts index 4ae709c43..9b48bc1f7 100644 --- a/packages/table/src/modules/views/view/view.factory.ts +++ b/packages/table/src/modules/views/view/view.factory.ts @@ -7,6 +7,7 @@ import { GalleryView } from "./variants/gallery-view.vo" import { GridView } from "./variants/grid-view.vo" import { KanbanView } from "./variants/kanban-view.vo" import { ListView } from "./variants/list-view.vo" +import { PivotView } from "./variants/pivot-view.vo" export class ViewFactory { static create(table: TableDo, dto: ICreateViewDTO) { @@ -16,6 +17,7 @@ export class ViewFactory { .with({ type: "gallery" }, (dto) => GalleryView.create(table, dto)) .with({ type: "list" }, (dto) => ListView.create(table, dto)) .with({ type: "calendar" }, (dto) => CalendarView.create(table, dto)) + .with({ type: "pivot" }, (dto) => PivotView.create(table, dto)) .exhaustive() } @@ -26,6 +28,7 @@ export class ViewFactory { .with({ type: "gallery" }, (dto) => new GalleryView(table, dto)) .with({ type: "list" }, (dto) => new ListView(table, dto)) .with({ type: "calendar" }, (dto) => new CalendarView(table, dto)) + .with({ type: "pivot" }, (dto) => new PivotView(table, dto)) .exhaustive() } } diff --git a/packages/table/src/modules/views/view/view.type.ts b/packages/table/src/modules/views/view/view.type.ts index 873154088..c4941bfe5 100644 --- a/packages/table/src/modules/views/view/view.type.ts +++ b/packages/table/src/modules/views/view/view.type.ts @@ -4,15 +4,17 @@ import { GALLERY_TYPE, type GalleryView } from "./variants/gallery-view.vo" import { GRID_TYPE, type GridView } from "./variants/grid-view.vo" import { KANBAN_TYPE, type KanbanView } from "./variants/kanban-view.vo" import { LIST_TYPE, type ListView } from "./variants/list-view.vo" +import { PIVOT_TYPE, type PivotView } from "./variants/pivot-view.vo" -export type View = GridView | KanbanView | GalleryView | ListView | CalendarView +export type View = GridView | KanbanView | GalleryView | ListView | CalendarView | PivotView export type ViewType = | typeof GRID_TYPE | typeof KANBAN_TYPE | typeof GALLERY_TYPE | typeof LIST_TYPE | typeof CALENDAR_TYPE + | typeof PIVOT_TYPE -export const viewType = z.enum([GRID_TYPE, KANBAN_TYPE, GALLERY_TYPE, LIST_TYPE, CALENDAR_TYPE]) +export const viewType = z.enum([GRID_TYPE, KANBAN_TYPE, GALLERY_TYPE, LIST_TYPE, CALENDAR_TYPE, PIVOT_TYPE]) -export const viewTypes = [GRID_TYPE, KANBAN_TYPE, GALLERY_TYPE, LIST_TYPE, CALENDAR_TYPE] as const +export const viewTypes = [GRID_TYPE, KANBAN_TYPE, GALLERY_TYPE, LIST_TYPE, CALENDAR_TYPE, PIVOT_TYPE] as const diff --git a/packages/template/src/templates/index.ts b/packages/template/src/templates/index.ts index 07a8332df..0c3856637 100644 --- a/packages/template/src/templates/index.ts +++ b/packages/template/src/templates/index.ts @@ -4,6 +4,7 @@ import { default as eventPlaningList } from "./eventPlaning.base.json" import { default as hr } from "./hr.base.json" import { default as officeInventoryManagement } from "./officeInventoryManagement.base.json" import { default as projectManagement } from "./projectManagement.base.json" +import { default as remoteWorkManagement } from "./remoteWorkManagement.base.json" import { default as salesCrm } from "./salesCrm.base.json" import { default as socialMediaContent } from "./socialMediaContent.base.json" import { default as test } from "./test.base.json" @@ -20,7 +21,7 @@ const templates = { socialMediaContent, hr, agileDevelopment, + remoteWorkManagement, } as const export { templates } - diff --git a/packages/template/src/templates/remoteWorkManagement.base.json b/packages/template/src/templates/remoteWorkManagement.base.json new file mode 100644 index 000000000..bb6443ba0 --- /dev/null +++ b/packages/template/src/templates/remoteWorkManagement.base.json @@ -0,0 +1,282 @@ +{ + "Remote Work Management": { + "tablesOrder": [ + "Remote Workers", + "Time Tracking", + "Equipment Inventory", + "Virtual Meetings", + "Performance Metrics" + ], + "tables": { + "Remote Workers": { + "fieldsOrder": [ + "Name", + "Position", + "Time Zone", + "Department", + "Department Name", + "Work Schedule", + "Start Date", + "Salary", + "Reports To", + "Manager Name", + "Email", + "Phone", + "Remote Status", + "Equipment Assigned" + ], + "schema": { + "Name": { + "id": "name", + "type": "string", + "constraint": { + "required": true + }, + "display": true + }, + "Position": { + "id": "position", + "type": "select", + "constraint": { + "max": 1, + "required": true + }, + "option": { + "options": [ + { + "id": "remote_developer", + "name": "Remote Developer", + "color": "blue" + }, + { + "id": "remote_designer", + "name": "Remote Designer", + "color": "purple" + }, + { + "id": "remote_manager", + "name": "Remote Manager", + "color": "green" + }, + { + "id": "remote_support", + "name": "Remote Support", + "color": "orange" + } + ] + } + }, + "Time Zone": { + "id": "timezone", + "type": "select", + "constraint": { + "max": 1, + "required": true + }, + "option": { + "options": [ + { + "id": "utc_plus_8", + "name": "UTC+8 (Asia)", + "color": "blue" + }, + { + "id": "utc_0", + "name": "UTC+0 (Europe)", + "color": "green" + }, + { + "id": "utc_minus_5", + "name": "UTC-5 (US East)", + "color": "orange" + }, + { + "id": "utc_minus_8", + "name": "UTC-8 (US West)", + "color": "purple" + } + ] + } + }, + "Remote Status": { + "id": "remote_status", + "type": "select", + "constraint": { + "max": 1, + "required": true + }, + "option": { + "options": [ + { + "id": "online", + "name": "Online", + "color": "green" + }, + { + "id": "offline", + "name": "Offline", + "color": "gray" + }, + { + "id": "meeting", + "name": "In Meeting", + "color": "blue" + }, + { + "id": "break", + "name": "On Break", + "color": "orange" + } + ] + } + } + }, + "views": { + "All Remote Workers": { + "type": "grid", + "sort": [ + { + "fieldId": "name", + "direction": "asc" + } + ] + }, + "By Time Zone": { + "type": "kanban", + "kanban": { + "field": "timezone" + } + }, + "By Status": { + "type": "kanban", + "kanban": { + "field": "remote_status" + } + } + }, + "records": [ + { + "id": "RW001", + "name": "Alice Johnson", + "position": "remote_developer", + "timezone": "utc_minus_8", + "department": ["tech_dept"], + "start_date": "2023-01-15", + "salary": 85000, + "email": "alice@remote.com", + "phone": "123-456-7890", + "remote_status": "online" + }, + { + "id": "RW002", + "name": "Bob Chen", + "position": "remote_manager", + "timezone": "utc_plus_8", + "department": ["product_dept"], + "start_date": "2022-05-01", + "salary": 95000, + "email": "bob@remote.com", + "phone": "234-567-8901", + "remote_status": "meeting" + }, + { + "id": "RW003", + "name": "Carol White", + "position": "remote_designer", + "timezone": "utc_0", + "department": ["design_dept"], + "start_date": "2023-03-10", + "salary": 78000, + "email": "carol@remote.com", + "phone": "345-678-9012", + "remote_status": "online" + }, + { + "id": "RW004", + "name": "David Kumar", + "position": "remote_developer", + "timezone": "utc_plus_8", + "department": ["tech_dept"], + "start_date": "2023-02-01", + "salary": 82000, + "email": "david@remote.com", + "phone": "456-789-0123", + "remote_status": "break" + }, + { + "id": "RW005", + "name": "Emma Garcia", + "position": "remote_support", + "timezone": "utc_minus_5", + "department": ["support_dept"], + "start_date": "2022-11-15", + "salary": 65000, + "email": "emma@remote.com", + "phone": "567-890-1234", + "remote_status": "online" + }, + { + "id": "RW006", + "name": "Frank Wilson", + "position": "remote_developer", + "timezone": "utc_0", + "department": ["tech_dept"], + "start_date": "2023-03-20", + "salary": 83000, + "email": "frank@remote.com", + "phone": "678-901-2345", + "remote_status": "offline" + }, + { + "id": "RW007", + "name": "Grace Taylor", + "position": "remote_designer", + "timezone": "utc_minus_5", + "department": ["design_dept"], + "start_date": "2022-09-01", + "salary": 76000, + "email": "grace@remote.com", + "phone": "789-012-3456", + "remote_status": "meeting" + }, + { + "id": "RW008", + "name": "Henry Zhang", + "position": "remote_support", + "timezone": "utc_plus_8", + "department": ["support_dept"], + "start_date": "2023-04-10", + "salary": 67000, + "email": "henry@remote.com", + "phone": "890-123-4567", + "remote_status": "online" + }, + { + "id": "RW009", + "name": "Isabel Rodriguez", + "position": "remote_manager", + "timezone": "utc_minus_5", + "department": ["product_dept"], + "start_date": "2022-07-15", + "salary": 92000, + "email": "isabel@remote.com", + "phone": "901-234-5678", + "remote_status": "online" + }, + { + "id": "RW010", + "name": "Jack Thompson", + "position": "remote_developer", + "timezone": "utc_minus_8", + "department": ["tech_dept"], + "start_date": "2023-01-01", + "salary": 84000, + "email": "jack@remote.com", + "phone": "012-345-6789", + "remote_status": "break" + } + ] + } + } + } +} diff --git a/packages/trpc/src/router.ts b/packages/trpc/src/router.ts index e90791879..d1524e1fa 100644 --- a/packages/trpc/src/router.ts +++ b/packages/trpc/src/router.ts @@ -147,9 +147,11 @@ import { GetApiTokensQuery, GetDashboardByIdQuery, GetMemberSpacesQuery, + GetPivotDataQuery, GetRecordByIdQuery, GetRecordsQuery, GetShareAggregatesQuery, + GetSharePivotDataQuery, GetShareRecordByIdQuery, GetShareRecordsQuery, GetTableQuery, @@ -164,9 +166,11 @@ import { getDashboardByIdQuery, getMemberSpacesOutput, getMemberSpacesQuery, + getPivotDataQuery, getRecordByIdQuery, getRecordsQuery, getShareAggregatesQuery, + getSharePivotDataQuery, getShareRecordByIdQuery, getShareRecordsQuery, getTableQuery, @@ -349,6 +353,10 @@ const recordRouter = t.router({ .use(authz("record:read")) .input(getRecordByIdQuery) .query(({ input }) => queryBus.execute(new GetRecordByIdQuery(input))), + pivot: privateProcedure + .use(authz("record:read")) + .input(getPivotDataQuery) + .query(({ input }) => queryBus.execute(new GetPivotDataQuery(input))), count: privateProcedure .use(authz("record:read")) .input(countRecordsQuery) @@ -467,6 +475,9 @@ const shareDataRouter = t.router({ aggregate: publicProcedure .input(getShareAggregatesQuery) .query(({ input }) => queryBus.execute(new GetShareAggregatesQuery(input))), + pivot: publicProcedure + .input(getSharePivotDataQuery) + .query(({ input }) => queryBus.execute(new GetSharePivotDataQuery(input))), }) const authzRouter = t.router({ diff --git a/packages/ui/.gitignore b/packages/ui/.gitignore deleted file mode 100644 index 9b1ee42e8..000000000 --- a/packages/ui/.gitignore +++ /dev/null @@ -1,175 +0,0 @@ -# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore - -# Logs - -logs -_.log -npm-debug.log_ -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Caches - -.cache - -# Diagnostic reports (https://nodejs.org/api/report.html) - -report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json - -# Runtime data - -pids -_.pid -_.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover - -lib-cov - -# Coverage directory used by tools like istanbul - -coverage -*.lcov - -# nyc test coverage - -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) - -.grunt - -# Bower dependency directory (https://bower.io/) - -bower_components - -# node-waf configuration - -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) - -build/Release - -# Dependency directories - -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) - -web_modules/ - -# TypeScript cache - -*.tsbuildinfo - -# Optional npm cache directory - -.npm - -# Optional eslint cache - -.eslintcache - -# Optional stylelint cache - -.stylelintcache - -# Microbundle cache - -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history - -.node_repl_history - -# Output of 'npm pack' - -*.tgz - -# Yarn Integrity file - -.yarn-integrity - -# dotenv environment variable files - -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) - -.parcel-cache - -# Next.js build output - -.next -out - -# Nuxt.js build / generate output - -.nuxt -dist - -# Gatsby files - -# Comment in the public line in if your project uses Gatsby and not Next.js - -# https://nextjs.org/blog/next-9-1#public-directory-support - -# public - -# vuepress build output - -.vuepress/dist - -# vuepress v2.x temp and cache directory - -.temp - -# Docusaurus cache and generated files - -.docusaurus - -# Serverless directories - -.serverless/ - -# FuseBox cache - -.fusebox/ - -# DynamoDB Local files - -.dynamodb/ - -# TernJS port file - -.tern-port - -# Stores VSCode versions used for testing VSCode extensions - -.vscode-test - -# yarn v2 - -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* - -# IntelliJ based IDEs -.idea - -# Finder (MacOS) folder config -.DS_Store diff --git a/packages/ui/README.md b/packages/ui/README.md deleted file mode 100644 index 12813134e..000000000 --- a/packages/ui/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# @undb/ui - -To install dependencies: - -```bash -bun install -``` - -To run: - -```bash -bun run src/index.ts -``` - -This project was created using `bun init` in bun v1.1.8. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/packages/ui/package.json b/packages/ui/package.json deleted file mode 100644 index 22497a360..000000000 --- a/packages/ui/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "@undb/ui", - "module": "src/index.ts", - "types": "src/index.d.ts", - "type": "module", - "peerDependencies": { - "@elysiajs/html": "^1.1.0", - "@kitajs/ts-html-plugin": "latest", - "typescript": "^5.0.0" - } -} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts deleted file mode 100644 index a129474fe..000000000 --- a/packages/ui/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -import "@elysiajs/html" diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json deleted file mode 100644 index 054ddc4e3..000000000 --- a/packages/ui/tsconfig.json +++ /dev/null @@ -1,109 +0,0 @@ -{ - "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ - - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - - /* Language and Environment */ - "target": "ES2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - "jsx": "react", - "jsxFactory": "Html.createElement", - "jsxFragmentFactory": "Html.Fragment", - - // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - - /* Modules */ - "module": "ES2022" /* Specify what module code is generated. */, - // "rootDir": "./", /* Specify the root folder within your source files. */ - "moduleResolution": "Bundler" /* Specify how TypeScript looks up a file from a given module specifier. */, - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - "types": ["bun-types"] /* Specify type package names to be included without being referenced in a source file. */, - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - "resolveJsonModule": true /* Enable importing .json files. */, - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - // "outDir": "./", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, - - /* Type Checking */ - "strict": true /* Enable all strict type-checking options. */, - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "plugins": [{ "name": "@kitajs/ts-html-plugin" }] - } -}