@lobb-js/studio 0.19.1 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/dataTable/fieldCell.svelte +38 -37
- package/dist/components/dataTable/polymorphicFieldCell.svelte +43 -0
- package/dist/components/dataTable/polymorphicFieldCell.svelte.d.ts +9 -0
- package/dist/components/dataTable/utils.js +19 -3
- package/dist/components/detailView/create/createDetailView.svelte +29 -37
- package/dist/components/detailView/fieldInput.svelte +27 -11
- package/dist/components/detailView/fieldInput.svelte.d.ts +1 -1
- package/dist/components/detailView/update/updateDetailView.svelte +26 -30
- package/dist/components/detailView/utils.js +28 -3
- package/dist/components/polymorphicInput.svelte +141 -0
- package/dist/components/polymorphicInput.svelte.d.ts +10 -0
- package/dist/relations.d.ts +14 -0
- package/dist/relations.js +47 -0
- package/dist/store.types.d.ts +1 -0
- package/dist/utils.d.ts +0 -3
- package/dist/utils.js +0 -21
- package/package.json +2 -2
- package/src/lib/components/dataTable/fieldCell.svelte +38 -37
- package/src/lib/components/dataTable/polymorphicFieldCell.svelte +43 -0
- package/src/lib/components/dataTable/utils.ts +14 -2
- package/src/lib/components/detailView/create/createDetailView.svelte +29 -37
- package/src/lib/components/detailView/fieldInput.svelte +27 -11
- package/src/lib/components/detailView/update/updateDetailView.svelte +26 -30
- package/src/lib/components/detailView/utils.ts +24 -3
- package/src/lib/components/polymorphicInput.svelte +141 -0
- package/src/lib/relations.ts +52 -0
- package/src/lib/store.types.ts +1 -0
- package/src/lib/utils.ts +0 -21
|
@@ -113,39 +113,35 @@
|
|
|
113
113
|
<div class="flex-1 overflow-y-auto">
|
|
114
114
|
<div class="flex flex-col gap-4 p-4">
|
|
115
115
|
{#each fieldNames as fieldName}
|
|
116
|
-
{
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
class="flex flex-col gap-2"
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
>
|
|
129
|
-
<
|
|
130
|
-
|
|
116
|
+
{#if !ctx.meta.collections[collectionName].fields[fieldName]?.ui?.hidden}
|
|
117
|
+
{@const field = getField(ctx, fieldName, collectionName)}
|
|
118
|
+
{@const FieldIcon = getFieldIcon(ctx, fieldName, collectionName)}
|
|
119
|
+
<div class="flex flex-col gap-2">
|
|
120
|
+
<div class="flex flex-1 items-end justify-between gap-2 text-xs">
|
|
121
|
+
<div class="flex gap-2">
|
|
122
|
+
<div class="h-fit">{field.label}</div>
|
|
123
|
+
<div class="flex h-fit items-center gap-1 text-[0.7rem] text-muted-foreground">
|
|
124
|
+
<FieldIcon size="12" />
|
|
125
|
+
{field.type}
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
<div>
|
|
129
|
+
<ExtensionsComponents
|
|
130
|
+
name="dvFields.topRight.{collectionName}.{fieldName}"
|
|
131
|
+
utils={getExtensionUtils(lobb, ctx)}
|
|
132
|
+
bind:value={entry[fieldName]}
|
|
133
|
+
/>
|
|
131
134
|
</div>
|
|
132
135
|
</div>
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
136
|
+
<FieldInput
|
|
137
|
+
{collectionName}
|
|
138
|
+
{fieldName}
|
|
139
|
+
bind:value={entry[fieldName]}
|
|
140
|
+
bind:entry
|
|
141
|
+
errorMessages={fieldsErrors[fieldName]}
|
|
142
|
+
/>
|
|
140
143
|
</div>
|
|
141
|
-
|
|
142
|
-
{collectionName}
|
|
143
|
-
{fieldName}
|
|
144
|
-
bind:value={entry[fieldName]}
|
|
145
|
-
{entry}
|
|
146
|
-
errorMessages={fieldsErrors[fieldName]}
|
|
147
|
-
/>
|
|
148
|
-
</div>
|
|
144
|
+
{/if}
|
|
149
145
|
{/each}
|
|
150
146
|
</div>
|
|
151
147
|
{#if showRelatedRecords}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Mustache from "mustache";
|
|
2
2
|
import type { CTX } from "../../store.types";
|
|
3
|
-
import {
|
|
3
|
+
import { isRelationField } from "../../relations";
|
|
4
4
|
import { getField } from "../dataTable/utils";
|
|
5
5
|
import type { DetailFormField } from "./detailViewForm.svelte";
|
|
6
6
|
|
|
@@ -37,12 +37,23 @@ export function serializeEntry(
|
|
|
37
37
|
|
|
38
38
|
// serialize the foreign key field's value
|
|
39
39
|
for (const [fieldName, fieldValue] of Object.entries(entry)) {
|
|
40
|
-
const isRefrenceField =
|
|
40
|
+
const isRefrenceField = isRelationField(ctx, collectionName, fieldName);
|
|
41
41
|
if (isRefrenceField && fieldValue !== null && fieldValue.id !== undefined) {
|
|
42
42
|
entry[fieldName] = fieldValue.id;
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// serialize polymorphic id_field values ({ id: 5, title: "..." } → 5)
|
|
47
|
+
for (const relation of ctx.meta.relations) {
|
|
48
|
+
if (relation.type !== "polymorphic") continue;
|
|
49
|
+
if ((relation as any).from.collection !== collectionName) continue;
|
|
50
|
+
const idField = (relation as any).from.id_field;
|
|
51
|
+
const fieldValue = entry[idField];
|
|
52
|
+
if (fieldValue !== null && typeof fieldValue === "object" && fieldValue.id !== undefined) {
|
|
53
|
+
entry[idField] = fieldValue.id;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
46
57
|
// check for related collections properties and serialize them too
|
|
47
58
|
if (!rollback) {
|
|
48
59
|
const childrenRelations = ctx.meta.relations.filter((relation) => relation.to.collection === collectionName);
|
|
@@ -131,7 +142,7 @@ export function generateTransactionBody(
|
|
|
131
142
|
|
|
132
143
|
export function parseDetailViewValues(ctx: CTX, collectionName: string, values: Record<string, any>) {
|
|
133
144
|
const forignFieldNames = ctx.meta.relations
|
|
134
|
-
.filter((relation) => relation.from.collection === collectionName)
|
|
145
|
+
.filter((relation) => relation.type !== "polymorphic" && relation.from.collection === collectionName)
|
|
135
146
|
.map((relation) => relation.from.field);
|
|
136
147
|
const childCollectionNames = ctx.meta.relations
|
|
137
148
|
.filter((relation) => relation.to.collection === collectionName)
|
|
@@ -150,6 +161,16 @@ export function parseDetailViewValues(ctx: CTX, collectionName: string, values:
|
|
|
150
161
|
}
|
|
151
162
|
}
|
|
152
163
|
}
|
|
164
|
+
|
|
165
|
+
// wrap polymorphic id_field scalar values into { id: value }
|
|
166
|
+
for (const relation of ctx.meta.relations) {
|
|
167
|
+
if (relation.type !== "polymorphic") continue;
|
|
168
|
+
if ((relation as any).from.collection !== collectionName) continue;
|
|
169
|
+
const idField = (relation as any).from.id_field;
|
|
170
|
+
if (idField in values && typeof values[idField] === 'number') {
|
|
171
|
+
values[idField] = { id: values[idField] };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
153
174
|
}
|
|
154
175
|
|
|
155
176
|
export function getCollectionFields(ctx: CTX, collectionName: string) {
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Button from "./ui/button/button.svelte";
|
|
3
|
+
import DataTable from "./dataTable/dataTable.svelte";
|
|
4
|
+
import Drawer from "./drawer.svelte";
|
|
5
|
+
import * as Popover from "./ui/popover/index";
|
|
6
|
+
import { getCollectionPrimaryField } from "./dataTable/utils";
|
|
7
|
+
import { getStudioContext } from "../context";
|
|
8
|
+
import { ArrowLeft, Link, ChevronDown } from "lucide-svelte";
|
|
9
|
+
|
|
10
|
+
const { ctx } = getStudioContext();
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
collectionField: string;
|
|
14
|
+
idField: string;
|
|
15
|
+
targetCollections: string[];
|
|
16
|
+
entry: Record<string, any>;
|
|
17
|
+
destructive?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
collectionField,
|
|
22
|
+
idField,
|
|
23
|
+
targetCollections,
|
|
24
|
+
entry = $bindable(),
|
|
25
|
+
destructive,
|
|
26
|
+
}: Props = $props();
|
|
27
|
+
|
|
28
|
+
const selectedCollection = $derived(entry[collectionField] ?? null);
|
|
29
|
+
const selectedId = $derived(entry[idField]?.id ?? entry[idField] ?? null);
|
|
30
|
+
const primaryField = $derived(entry[idField] ? Object.values(entry[idField])[1] : null);
|
|
31
|
+
|
|
32
|
+
let collectionPopoverOpen = $state(false);
|
|
33
|
+
let recordDrawerOpen = $state(false);
|
|
34
|
+
|
|
35
|
+
function onCollectionChange(col: string) {
|
|
36
|
+
collectionPopoverOpen = false;
|
|
37
|
+
if (entry[collectionField] !== col) {
|
|
38
|
+
entry = { ...entry, [collectionField]: col, [idField]: null };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function onIdChange(e: Event) {
|
|
43
|
+
const raw = (e.target as HTMLInputElement).value;
|
|
44
|
+
const id = raw === "" ? null : Number(raw);
|
|
45
|
+
entry = { ...entry, [idField]: id === null ? null : { id } };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function onRecordSelect(record: any) {
|
|
49
|
+
const primaryFieldName = getCollectionPrimaryField(ctx, selectedCollection!);
|
|
50
|
+
const value: any = { id: record.id };
|
|
51
|
+
if (primaryFieldName) value[primaryFieldName] = record[primaryFieldName];
|
|
52
|
+
entry = { ...entry, [idField]: value };
|
|
53
|
+
recordDrawerOpen = false;
|
|
54
|
+
}
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<div class="flex h-9 w-full items-center gap-1.5 rounded-md border pl-1.5 pr-9 text-xs bg-muted/30 {destructive ? 'border-destructive bg-destructive/10' : ''}">
|
|
58
|
+
<!-- Collection picker -->
|
|
59
|
+
<Popover.Root bind:open={collectionPopoverOpen}>
|
|
60
|
+
<Popover.Trigger>
|
|
61
|
+
{#snippet child({ props })}
|
|
62
|
+
<button
|
|
63
|
+
{...props}
|
|
64
|
+
class="flex shrink-0 items-center gap-1 h-6 px-2 rounded-sm border bg-muted text-xs text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
|
|
65
|
+
>
|
|
66
|
+
{selectedCollection ?? "NULL"}
|
|
67
|
+
<ChevronDown size="11" />
|
|
68
|
+
</button>
|
|
69
|
+
{/snippet}
|
|
70
|
+
</Popover.Trigger>
|
|
71
|
+
<Popover.Content class="w-48 p-2">
|
|
72
|
+
<div class="flex flex-col gap-1">
|
|
73
|
+
{#each targetCollections as col}
|
|
74
|
+
<Button
|
|
75
|
+
variant={selectedCollection === col ? "default" : "ghost"}
|
|
76
|
+
class="justify-start text-xs font-normal h-7 px-2"
|
|
77
|
+
onclick={() => onCollectionChange(col)}
|
|
78
|
+
>
|
|
79
|
+
{col}
|
|
80
|
+
</Button>
|
|
81
|
+
{/each}
|
|
82
|
+
</div>
|
|
83
|
+
</Popover.Content>
|
|
84
|
+
</Popover.Root>
|
|
85
|
+
|
|
86
|
+
<!-- Transparent id input -->
|
|
87
|
+
<input
|
|
88
|
+
placeholder="NULL"
|
|
89
|
+
type="number"
|
|
90
|
+
class="min-w-0 flex-1 bg-transparent outline-none text-xs placeholder:text-muted-foreground"
|
|
91
|
+
value={selectedId ?? ""}
|
|
92
|
+
oninput={onIdChange}
|
|
93
|
+
/>
|
|
94
|
+
|
|
95
|
+
<!-- Primary field badge -->
|
|
96
|
+
{#if primaryField}
|
|
97
|
+
<div class="flex shrink-0 items-center bg-background rounded-full border h-6 px-3 shadow-sm">
|
|
98
|
+
{primaryField}
|
|
99
|
+
</div>
|
|
100
|
+
{/if}
|
|
101
|
+
|
|
102
|
+
<!-- Select record button -->
|
|
103
|
+
{#if selectedCollection}
|
|
104
|
+
<Button
|
|
105
|
+
class="h-6 shrink-0 px-2 font-normal text-xs"
|
|
106
|
+
variant="outline"
|
|
107
|
+
onclick={() => (recordDrawerOpen = true)}
|
|
108
|
+
>
|
|
109
|
+
<Link size="13" />
|
|
110
|
+
Select Record
|
|
111
|
+
</Button>
|
|
112
|
+
{/if}
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
{#if recordDrawerOpen}
|
|
116
|
+
<Drawer onHide={async () => { recordDrawerOpen = false }}>
|
|
117
|
+
<div class="flex h-12 items-center gap-4 border-b px-4">
|
|
118
|
+
<Button
|
|
119
|
+
variant="outline"
|
|
120
|
+
onclick={() => (recordDrawerOpen = false)}
|
|
121
|
+
class="h-8 w-8 rounded-full text-xs font-normal"
|
|
122
|
+
Icon={ArrowLeft}
|
|
123
|
+
/>
|
|
124
|
+
<div class="flex items-center gap-2">
|
|
125
|
+
<div class="text-sm">Select record from</div>
|
|
126
|
+
<span class="rounded-md border bg-muted px-2 py-0.5 text-sm">
|
|
127
|
+
{selectedCollection}
|
|
128
|
+
</span>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
<div class="flex-1 overflow-y-auto bg-muted">
|
|
132
|
+
<DataTable
|
|
133
|
+
collectionName={selectedCollection!}
|
|
134
|
+
tableProps={{
|
|
135
|
+
showCheckboxes: false,
|
|
136
|
+
select: { onSelect: onRecordSelect },
|
|
137
|
+
}}
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
140
|
+
</Drawer>
|
|
141
|
+
{/if}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { CTX } from "./store.types";
|
|
2
|
+
|
|
3
|
+
function getFieldRelation(ctx: CTX, collectionName: string, fieldName: string) {
|
|
4
|
+
const relations = ctx.meta.relations;
|
|
5
|
+
for (let index = 0; index < relations.length; index++) {
|
|
6
|
+
const relation = relations[index];
|
|
7
|
+
if (relation.type === "polymorphic") continue;
|
|
8
|
+
if (relation.from.collection === collectionName && relation.from.field === fieldName) {
|
|
9
|
+
return relation;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function isRelationField(ctx: CTX, collectionName: string, fieldName: string): boolean {
|
|
16
|
+
return Boolean(getFieldRelation(ctx, collectionName, fieldName));
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function getFieldRelationTarget(ctx: CTX, collectionName: string, fieldName: string): string | null {
|
|
20
|
+
return getFieldRelation(ctx, collectionName, fieldName)?.to.collection ?? null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function getPolymorphicRelation(ctx: CTX, collectionName: string, fieldName: string) {
|
|
24
|
+
const relations = ctx.meta.relations;
|
|
25
|
+
for (let index = 0; index < relations.length; index++) {
|
|
26
|
+
const relation = relations[index];
|
|
27
|
+
if (
|
|
28
|
+
relation.type === "polymorphic" &&
|
|
29
|
+
relation.from.collection === collectionName &&
|
|
30
|
+
relation.from.virtual_field === fieldName
|
|
31
|
+
) {
|
|
32
|
+
return relation as {
|
|
33
|
+
type: "polymorphic";
|
|
34
|
+
from: { collection: string; virtual_field: string; collection_field: string; id_field: string };
|
|
35
|
+
to: string[];
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
export function recordHasChildrean(ctx: CTX, collectionName: string) {
|
|
44
|
+
for (let index = 0; index < ctx.meta.relations.length; index++) {
|
|
45
|
+
const relation = ctx.meta.relations[index];
|
|
46
|
+
if (relation.type === "polymorphic") continue;
|
|
47
|
+
if (relation.to.collection === collectionName) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
};
|
package/src/lib/store.types.ts
CHANGED
package/src/lib/utils.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { clsx, type ClassValue } from "clsx";
|
|
|
2
2
|
import { twMerge } from "tailwind-merge";
|
|
3
3
|
|
|
4
4
|
import { MediaQuery } from 'svelte/reactivity';
|
|
5
|
-
import type { CTX } from "./store.types";
|
|
6
5
|
|
|
7
6
|
export function cn(...inputs: ClassValue[]) {
|
|
8
7
|
return twMerge(clsx(inputs));
|
|
@@ -24,26 +23,6 @@ export const mediaQueries = {
|
|
|
24
23
|
'2xl': new MediaQuery('min-width: 1536px'),
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
export function getFieldRelation(ctx: CTX, collectionName: string, fieldName: string) {
|
|
28
|
-
const relations = ctx.meta.relations;
|
|
29
|
-
for (let index = 0; index < relations.length; index++) {
|
|
30
|
-
const relation = relations[index];
|
|
31
|
-
if (relation.from.collection === collectionName && relation.from.field === fieldName) {
|
|
32
|
-
return relation;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return null;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export function recordHasChildrean(ctx: CTX, collectionName: string) {
|
|
39
|
-
for (let index = 0; index < ctx.meta.relations.length; index++) {
|
|
40
|
-
const relation = ctx.meta.relations[index];
|
|
41
|
-
if (relation.to.collection === collectionName) {
|
|
42
|
-
return true;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return false;
|
|
46
|
-
};
|
|
47
26
|
|
|
48
27
|
export function calculateDrawerWidth() {
|
|
49
28
|
const backgroundDrawerButtons = document.querySelectorAll(".backgroundDrawerButton");
|