@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.
Files changed (28) hide show
  1. package/dist/components/dataTable/fieldCell.svelte +38 -37
  2. package/dist/components/dataTable/polymorphicFieldCell.svelte +43 -0
  3. package/dist/components/dataTable/polymorphicFieldCell.svelte.d.ts +9 -0
  4. package/dist/components/dataTable/utils.js +19 -3
  5. package/dist/components/detailView/create/createDetailView.svelte +29 -37
  6. package/dist/components/detailView/fieldInput.svelte +27 -11
  7. package/dist/components/detailView/fieldInput.svelte.d.ts +1 -1
  8. package/dist/components/detailView/update/updateDetailView.svelte +26 -30
  9. package/dist/components/detailView/utils.js +28 -3
  10. package/dist/components/polymorphicInput.svelte +141 -0
  11. package/dist/components/polymorphicInput.svelte.d.ts +10 -0
  12. package/dist/relations.d.ts +14 -0
  13. package/dist/relations.js +47 -0
  14. package/dist/store.types.d.ts +1 -0
  15. package/dist/utils.d.ts +0 -3
  16. package/dist/utils.js +0 -21
  17. package/package.json +2 -2
  18. package/src/lib/components/dataTable/fieldCell.svelte +38 -37
  19. package/src/lib/components/dataTable/polymorphicFieldCell.svelte +43 -0
  20. package/src/lib/components/dataTable/utils.ts +14 -2
  21. package/src/lib/components/detailView/create/createDetailView.svelte +29 -37
  22. package/src/lib/components/detailView/fieldInput.svelte +27 -11
  23. package/src/lib/components/detailView/update/updateDetailView.svelte +26 -30
  24. package/src/lib/components/detailView/utils.ts +24 -3
  25. package/src/lib/components/polymorphicInput.svelte +141 -0
  26. package/src/lib/relations.ts +52 -0
  27. package/src/lib/store.types.ts +1 -0
  28. 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
- {@const field = getField(ctx, fieldName, collectionName)}
117
- {@const FieldIcon = getFieldIcon(ctx, fieldName, collectionName)}
118
- <div
119
- class="flex flex-col gap-2"
120
- >
121
- <div
122
- class="flex flex-1 items-end justify-between gap-2 text-xs"
123
- >
124
- <div class="flex gap-2">
125
- <div class="h-fit">{field.label}</div>
126
- <div
127
- class="flex h-fit items-center gap-1 text-[0.7rem] text-muted-foreground"
128
- >
129
- <FieldIcon size="12" />
130
- {field.type}
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
- <div>
134
- <ExtensionsComponents
135
- name="dvFields.topRight.{collectionName}.{fieldName}"
136
- utils={getExtensionUtils(lobb, ctx)}
137
- bind:value={entry[fieldName]}
138
- />
139
- </div>
136
+ <FieldInput
137
+ {collectionName}
138
+ {fieldName}
139
+ bind:value={entry[fieldName]}
140
+ bind:entry
141
+ errorMessages={fieldsErrors[fieldName]}
142
+ />
140
143
  </div>
141
- <FieldInput
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 { getFieldRelation } from "../../utils";
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 = Boolean(getFieldRelation(ctx, collectionName, fieldName));
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
+ };
@@ -14,6 +14,7 @@ interface Collection {
14
14
  singleton: boolean;
15
15
  virtual?: boolean;
16
16
  ui?: {
17
+ icon?: string;
17
18
  tabs?: CollectionTab[];
18
19
  };
19
20
  }
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");