@lobb-js/studio 0.42.0 → 0.44.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 (38) hide show
  1. package/dist/components/confirmationDialog/confirmationDialog.svelte +1 -1
  2. package/dist/components/dataTable/dataTable.svelte +49 -17
  3. package/dist/components/dataTable/filter.svelte +26 -23
  4. package/dist/components/dataTable/header.svelte +17 -13
  5. package/dist/components/dataTable/header.svelte.d.ts +1 -0
  6. package/dist/components/dataTable/listViewChildren.svelte +60 -77
  7. package/dist/components/dataTable/listViewChildren.svelte.d.ts +1 -1
  8. package/dist/components/dataTable/table.svelte +20 -61
  9. package/dist/components/dataTable/table.svelte.d.ts +2 -2
  10. package/dist/components/detailView/changeTreeUtils.d.ts +7 -0
  11. package/dist/components/detailView/changeTreeUtils.js +47 -0
  12. package/dist/components/detailView/create/createDetailView.svelte +17 -13
  13. package/dist/components/detailView/detailView.svelte +7 -2
  14. package/dist/components/detailView/detailView.svelte.d.ts +1 -0
  15. package/dist/components/detailView/fieldInput.svelte +10 -9
  16. package/dist/components/detailView/fieldInput.svelte.d.ts +1 -0
  17. package/dist/components/detailView/update/updateDetailView.svelte +22 -18
  18. package/dist/components/drawer.svelte +15 -2
  19. package/dist/components/foreingKeyInput.svelte +32 -60
  20. package/dist/components/foreingKeyInput.svelte.d.ts +1 -1
  21. package/dist/components/polymorphicInput.svelte +42 -66
  22. package/dist/components/polymorphicInput.svelte.d.ts +1 -0
  23. package/package.json +2 -2
  24. package/src/app.css +2 -2
  25. package/src/lib/components/confirmationDialog/confirmationDialog.svelte +1 -1
  26. package/src/lib/components/dataTable/dataTable.svelte +49 -17
  27. package/src/lib/components/dataTable/filter.svelte +26 -23
  28. package/src/lib/components/dataTable/header.svelte +17 -13
  29. package/src/lib/components/dataTable/listViewChildren.svelte +60 -77
  30. package/src/lib/components/dataTable/table.svelte +20 -61
  31. package/src/lib/components/detailView/changeTreeUtils.ts +39 -0
  32. package/src/lib/components/detailView/create/createDetailView.svelte +17 -13
  33. package/src/lib/components/detailView/detailView.svelte +7 -2
  34. package/src/lib/components/detailView/fieldInput.svelte +10 -9
  35. package/src/lib/components/detailView/update/updateDetailView.svelte +22 -18
  36. package/src/lib/components/drawer.svelte +15 -2
  37. package/src/lib/components/foreingKeyInput.svelte +32 -60
  38. package/src/lib/components/polymorphicInput.svelte +42 -66
@@ -4,6 +4,7 @@
4
4
  import { fade } from "svelte/transition";
5
5
  import { cubicOut } from "svelte/easing";
6
6
  import Portal from "svelte-portal";
7
+ import { getContext, setContext, onDestroy } from "svelte";
7
8
 
8
9
  interface Props {
9
10
  children?: Snippet<[]>;
@@ -13,6 +14,16 @@
13
14
 
14
15
  let { onHide, children, position = "side" }: Props = $props();
15
16
 
17
+ // Track nesting depth for stacking effect
18
+ const DEPTH_KEY = 'drawer-depth';
19
+ const parentDepth: number = getContext(DEPTH_KEY) ?? 0;
20
+ const depth = parentDepth + 1;
21
+ setContext(DEPTH_KEY, depth);
22
+
23
+ // Side drawers get narrower, bottom drawers get shorter — both offset by 48px per level
24
+ const sideWidth = $derived(calculateDrawerWidth() - (depth - 1) * 48);
25
+ const bottomOffset = $derived((depth - 1) * 48);
26
+
16
27
  function slide(_node: Element, { duration = 250, axis }: { duration?: number; axis: "x" | "y" }) {
17
28
  return {
18
29
  duration,
@@ -39,9 +50,11 @@
39
50
  role="dialog"
40
51
  transition:slide={{ axis: position === "bottom" ? "y" : "x" }}
41
52
  class={position === "bottom"
42
- ? "fixed bottom-0 left-0 z-40 flex h-[60vh] w-full flex-col border-t bg-card"
53
+ ? "fixed bottom-0 left-0 z-40 flex w-full flex-col border-t bg-card"
43
54
  : "fixed right-0 top-0 z-40 flex h-full w-full flex-col border-l bg-card"}
44
- style={position === "side" ? `max-width: ${calculateDrawerWidth()}px;` : ""}
55
+ style={position === "side"
56
+ ? `max-width: ${sideWidth}px;`
57
+ : `height: calc(60vh - ${bottomOffset}px);`}
45
58
  >
46
59
  {@render children?.()}
47
60
  </div>
@@ -2,20 +2,15 @@
2
2
  import { onMount } from "svelte";
3
3
  import Input from "./ui/input/input.svelte";
4
4
  import SelectRecord from "./selectRecord.svelte";
5
- import UpdateDetailViewButton from "./detailView/update/updateDetailViewButton.svelte";
6
5
  import CreateDetailView from "./detailView/create/createDetailView.svelte";
7
- import { getCollectionPrimaryField } from "./dataTable/utils";
8
- import { getStudioContext } from "../context";
9
- import { ExternalLink, Plus } from "lucide-svelte";
6
+ import { Plus } from "lucide-svelte";
10
7
  import Button from "./ui/button/button.svelte";
11
8
 
12
- const { lobb, ctx } = getStudioContext();
13
-
14
9
  interface LocalProps {
15
10
  parentCollectionName: string;
16
11
  collectionName: string;
17
12
  fieldName: string;
18
- value?: number | null;
13
+ value?: any;
19
14
  destructive?: boolean;
20
15
  entry: Record<string, any>;
21
16
  }
@@ -29,57 +24,39 @@
29
24
  entry,
30
25
  }: LocalProps = $props();
31
26
 
32
- let displayName = $state<string | null>(null);
33
27
  let createDrawerOpen = $state(false);
28
+ let initialValue = $state<any>(undefined);
34
29
 
35
- onMount(async () => {
36
- if (value == null) return;
37
- try {
38
- const res = await lobb.findOne(collectionName, value);
39
- const record = (await res.json()).data;
40
- const primaryFieldName = getCollectionPrimaryField(ctx, collectionName);
41
- displayName = primaryFieldName ? String(record[primaryFieldName]) : null;
42
- } catch {
43
- displayName = null;
44
- }
45
- });
30
+ onMount(() => { initialValue = value; });
46
31
 
47
- $effect(() => {
48
- if (value == null) displayName = null;
49
- });
32
+ const isPendingCreate = $derived(value && typeof value === 'object' && value.create);
33
+ const hasRealId = $derived(value != null && typeof value === 'number');
34
+ const isEmpty = $derived(value == null || value === 0);
35
+ const isZeroPlaceholder = $derived(value === 0);
36
+ const isChanged = $derived(initialValue !== undefined && value !== initialValue && !isPendingCreate);
50
37
 
51
- function handleSelect(selectedEntry: any) {
52
- const primaryFieldName = getCollectionPrimaryField(ctx, collectionName);
53
- value = selectedEntry.id;
54
- displayName = primaryFieldName ? String(selectedEntry[primaryFieldName]) : null;
55
- }
38
+ const bgClass = $derived(
39
+ isPendingCreate ? '!bg-green-500/5 border-green-500/40' :
40
+ isChanged ? '!bg-orange-500/5' :
41
+ ''
42
+ );
56
43
 
57
44
  async function handleCreated(record: any) {
58
- const primaryFieldName = getCollectionPrimaryField(ctx, collectionName);
59
- value = record.id;
60
- displayName = primaryFieldName ? String(record[primaryFieldName]) : null;
45
+ if (!record.id) {
46
+ value = { create: record };
47
+ } else {
48
+ value = record.id;
49
+ }
61
50
  }
62
51
 
63
- const idIsZero = $derived(value === 0);
52
+ function handleSelect(selectedEntry: any) {
53
+ value = selectedEntry.id;
54
+ }
64
55
  </script>
65
56
 
66
- {#if !idIsZero}
57
+ {#if !isZeroPlaceholder}
67
58
  <div class="relative">
68
- <div class="flex gap-2 absolute right-0 top-0 mr-9 h-full items-center text-xs">
69
- {#if value != null}
70
- <UpdateDetailViewButton
71
- collectionName={collectionName}
72
- recordId={value}
73
- variant="ghost"
74
- class="h-5 w-5 px-0 py-0 text-muted-foreground hover:bg-transparent"
75
- Icon={ExternalLink}
76
- ></UpdateDetailViewButton>
77
- {/if}
78
- {#if displayName}
79
- <div class="flex items-center bg-background rounded-full border h-6 px-3 shadow-sm">
80
- {displayName}
81
- </div>
82
- {/if}
59
+ <div class="flex gap-1 absolute right-0 top-0 mr-9 h-full items-center text-xs">
83
60
  <Button
84
61
  class="h-6 px-2 font-normal text-xs"
85
62
  variant="outline"
@@ -95,30 +72,24 @@
95
72
  {collectionName}
96
73
  {fieldName}
97
74
  onSelect={handleSelect}
98
- text="Select"
75
+ text="Link"
99
76
  {entry}
100
77
  />
101
78
  </div>
79
+
102
80
  <Input
103
- placeholder={"NULL"}
81
+ placeholder={isPendingCreate ? "AUTO GENERATED" : isEmpty ? "NULL" : ""}
104
82
  type="number"
105
- class="
106
- bg-muted text-xs
107
- {destructive ? 'border-destructive bg-destructive/10' : ''}
108
- "
83
+ class="bg-muted text-xs {bgClass} {destructive ? '!bg-destructive/10 border-destructive' : ''}"
109
84
  bind:value={
110
- () => value ?? "",
111
- (v) => (value = (v === "" || v == null) ? null : Number(v))
85
+ () => hasRealId ? value : "",
86
+ (v) => { value = (v === "" || v == null) ? null : Number(v); }
112
87
  }
113
88
  />
114
89
  </div>
115
90
  {:else}
116
91
  <div class="relative z-10">
117
- <Input
118
- placeholder={"PARENT ID"}
119
- class="bg-muted text-xs"
120
- disabled={true}
121
- />
92
+ <Input placeholder="PARENT ID" class="bg-muted text-xs" disabled={true} />
122
93
  </div>
123
94
  {/if}
124
95
 
@@ -126,6 +97,7 @@
126
97
  <CreateDetailView
127
98
  collectionName={collectionName}
128
99
  onCreated={handleCreated}
100
+ onChanges={() => {}}
129
101
  onCancel={async () => { createDrawerOpen = false; }}
130
102
  />
131
103
  {/if}
@@ -4,16 +4,13 @@
4
4
  import DataTable from "./dataTable/dataTable.svelte";
5
5
  import Drawer from "./drawer.svelte";
6
6
  import * as Popover from "./ui/popover/index";
7
- import { getCollectionPrimaryField } from "./dataTable/utils";
8
- import { getStudioContext } from "../context";
9
7
  import { ArrowLeft, Link, ChevronDown, Plus } from "lucide-svelte";
10
8
  import CreateDetailView from "./detailView/create/createDetailView.svelte";
11
9
 
12
- const { ctx, lobb } = getStudioContext();
13
-
14
10
  interface Props {
15
11
  collectionField: string;
16
12
  idField: string;
13
+ virtualField: string;
17
14
  targetCollections: string[];
18
15
  entry: Record<string, any>;
19
16
  destructive?: boolean;
@@ -22,64 +19,68 @@
22
19
  let {
23
20
  collectionField,
24
21
  idField,
22
+ virtualField,
25
23
  targetCollections,
26
24
  entry = $bindable(),
27
25
  destructive,
28
26
  }: Props = $props();
29
27
 
30
28
  const selectedCollection = $derived(entry[collectionField] ?? null);
31
- const selectedId = $derived(entry[idField] ?? null);
29
+ const selectedId = $derived(entry[idField] ?? null);
30
+ const virtualVal = $derived(entry[virtualField] ?? null);
31
+
32
+ let initialId = $state<number | null | undefined>(undefined);
33
+ let initialCollection = $state<string | null | undefined>(undefined);
34
+ onMount(() => {
35
+ initialId = entry[idField] ?? null;
36
+ initialCollection = entry[collectionField] ?? null;
37
+ });
38
+
39
+ const isPendingCreate = $derived(virtualVal && typeof virtualVal === 'object' && virtualVal.create);
40
+ const isChanged = $derived(
41
+ initialId !== undefined &&
42
+ !isPendingCreate &&
43
+ (selectedId !== initialId || selectedCollection !== initialCollection)
44
+ );
45
+
46
+ const bgClass = $derived(
47
+ isPendingCreate ? '!bg-green-500/5 border-green-500/40' :
48
+ isChanged ? '!bg-orange-500/5' :
49
+ ''
50
+ );
32
51
 
33
- let displayName = $state<string | null>(null);
34
52
  let collectionPopoverOpen = $state(false);
35
53
  let recordDrawerOpen = $state(false);
36
54
  let createDrawerOpen = $state(false);
37
55
 
38
- onMount(async () => {
39
- if (selectedCollection == null || selectedId == null) return;
40
- try {
41
- const res = await lobb.findOne(selectedCollection, selectedId);
42
- const record = await res.json();
43
- const primaryFieldName = getCollectionPrimaryField(ctx, selectedCollection);
44
- displayName = primaryFieldName ? String(record[primaryFieldName]) : null;
45
- } catch {
46
- displayName = null;
47
- }
48
- });
49
-
50
- $effect(() => {
51
- if (entry[idField] == null) displayName = null;
52
- });
53
-
54
56
  function onCollectionChange(col: string) {
55
57
  collectionPopoverOpen = false;
56
58
  if (entry[collectionField] !== col) {
57
- entry = { ...entry, [collectionField]: col, [idField]: null };
59
+ entry = { ...entry, [collectionField]: col, [idField]: null, [virtualField]: undefined };
58
60
  }
59
61
  }
60
62
 
61
63
  function onIdChange(e: Event) {
62
64
  const raw = (e.target as HTMLInputElement).value;
63
65
  const id = raw === "" ? null : Number(raw);
64
- entry = { ...entry, [idField]: id };
66
+ entry = { ...entry, [idField]: id, [virtualField]: undefined };
65
67
  }
66
68
 
67
69
  function onRecordSelect(record: any) {
68
- const primaryFieldName = getCollectionPrimaryField(ctx, selectedCollection!);
69
- entry = { ...entry, [idField]: record.id };
70
- displayName = primaryFieldName ? String(record[primaryFieldName]) : null;
70
+ entry = { ...entry, [idField]: record.id, [virtualField]: undefined };
71
71
  recordDrawerOpen = false;
72
72
  }
73
73
 
74
74
  async function onPolyCreated(record: any) {
75
- const primaryFieldName = getCollectionPrimaryField(ctx, selectedCollection!);
76
- entry = { ...entry, [idField]: record.id };
77
- displayName = primaryFieldName ? String(record[primaryFieldName]) : null;
75
+ if (!record.id) {
76
+ entry = { ...entry, [virtualField]: { collection: selectedCollection, create: record }, [collectionField]: selectedCollection, [idField]: null };
77
+ } else {
78
+ entry = { ...entry, [idField]: record.id, [virtualField]: undefined };
79
+ }
78
80
  }
79
81
  </script>
80
82
 
81
- <div class="flex h-9 w-full items-center gap-1.5 rounded-md border pl-1.5 pr-9 text-xs bg-muted {destructive ? 'border-destructive bg-destructive/10' : ''}">
82
- <!-- Collection picker -->
83
+ <div class="flex h-9 w-full items-center gap-1.5 rounded-md border pl-1.5 pr-9 text-xs bg-muted {bgClass} {destructive ? '!bg-destructive/10 border-destructive' : ''}">
83
84
  <Popover.Root bind:open={collectionPopoverOpen}>
84
85
  <Popover.Trigger>
85
86
  {#snippet child({ props })}
@@ -107,39 +108,22 @@
107
108
  </Popover.Content>
108
109
  </Popover.Root>
109
110
 
110
- <!-- Transparent id input -->
111
111
  <input
112
- placeholder="NULL"
112
+ placeholder={isPendingCreate ? "AUTO GENERATED" : "NULL"}
113
113
  type="number"
114
114
  class="min-w-0 flex-1 bg-transparent outline-none text-xs placeholder:text-muted-foreground"
115
115
  value={selectedId ?? ""}
116
116
  oninput={onIdChange}
117
117
  />
118
118
 
119
- <!-- Primary field badge -->
120
- {#if displayName}
121
- <div class="flex shrink-0 items-center bg-background rounded-full border h-6 px-3 shadow-sm">
122
- {displayName}
123
- </div>
124
- {/if}
125
-
126
- <!-- Create / Select record buttons -->
127
119
  {#if selectedCollection}
128
- <Button
129
- class="h-6 shrink-0 px-2 font-normal text-xs"
130
- variant="outline"
131
- onclick={() => (createDrawerOpen = true)}
132
- >
120
+ <Button class="h-6 shrink-0 px-2 font-normal text-xs" variant="outline" onclick={() => (createDrawerOpen = true)}>
133
121
  <Plus size="13" />
134
122
  Create
135
123
  </Button>
136
- <Button
137
- class="h-6 shrink-0 px-2 font-normal text-xs"
138
- variant="outline"
139
- onclick={() => (recordDrawerOpen = true)}
140
- >
124
+ <Button class="h-6 shrink-0 px-2 font-normal text-xs" variant="outline" onclick={() => (recordDrawerOpen = true)}>
141
125
  <Link size="13" />
142
- Select
126
+ Link
143
127
  </Button>
144
128
  {/if}
145
129
  </div>
@@ -147,26 +131,17 @@
147
131
  {#if recordDrawerOpen}
148
132
  <Drawer onHide={async () => { recordDrawerOpen = false }}>
149
133
  <div class="flex h-12 items-center gap-4 border-b px-4">
150
- <Button
151
- variant="outline"
152
- onclick={() => (recordDrawerOpen = false)}
153
- class="h-8 w-8 rounded-full text-xs font-normal"
154
- Icon={ArrowLeft}
155
- />
134
+ <Button variant="outline" onclick={() => (recordDrawerOpen = false)} class="h-8 w-8 rounded-full text-xs font-normal" Icon={ArrowLeft} />
156
135
  <div class="flex items-center gap-2">
157
136
  <div class="text-sm">Select record from</div>
158
- <span class="rounded-md border bg-muted px-2 py-0.5 text-sm">
159
- {selectedCollection}
160
- </span>
137
+ <span class="rounded-md border bg-muted px-2 py-0.5 text-sm">{selectedCollection}</span>
161
138
  </div>
162
139
  </div>
163
140
  <div class="flex-1 overflow-y-auto bg-muted">
164
141
  <DataTable
165
142
  collectionName={selectedCollection!}
166
- tableProps={{
167
- showCheckboxes: false,
168
- select: { onSelect: onRecordSelect },
169
- }}
143
+ filter={selectedId != null ? { id: { $ne: selectedId } } : undefined}
144
+ tableProps={{ showCheckboxes: false, select: { onSelect: onRecordSelect } }}
170
145
  />
171
146
  </div>
172
147
  </Drawer>
@@ -176,6 +151,7 @@
176
151
  <CreateDetailView
177
152
  collectionName={selectedCollection}
178
153
  onCreated={onPolyCreated}
154
+ onChanges={() => {}}
179
155
  onCancel={async () => { createDrawerOpen = false; }}
180
156
  />
181
157
  {/if}