@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
|
@@ -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,10 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
collectionField: string;
|
|
3
|
+
idField: string;
|
|
4
|
+
targetCollections: string[];
|
|
5
|
+
entry: Record<string, any>;
|
|
6
|
+
destructive?: boolean;
|
|
7
|
+
}
|
|
8
|
+
declare const PolymorphicInput: import("svelte").Component<Props, {}, "entry">;
|
|
9
|
+
type PolymorphicInput = ReturnType<typeof PolymorphicInput>;
|
|
10
|
+
export default PolymorphicInput;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CTX } from "./store.types";
|
|
2
|
+
export declare function isRelationField(ctx: CTX, collectionName: string, fieldName: string): boolean;
|
|
3
|
+
export declare function getFieldRelationTarget(ctx: CTX, collectionName: string, fieldName: string): string | null;
|
|
4
|
+
export declare function getPolymorphicRelation(ctx: CTX, collectionName: string, fieldName: string): {
|
|
5
|
+
type: "polymorphic";
|
|
6
|
+
from: {
|
|
7
|
+
collection: string;
|
|
8
|
+
virtual_field: string;
|
|
9
|
+
collection_field: string;
|
|
10
|
+
id_field: string;
|
|
11
|
+
};
|
|
12
|
+
to: string[];
|
|
13
|
+
};
|
|
14
|
+
export declare function recordHasChildrean(ctx: CTX, collectionName: string): boolean;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
function getFieldRelation(ctx, collectionName, fieldName) {
|
|
2
|
+
var relations = ctx.meta.relations;
|
|
3
|
+
for (var index = 0; index < relations.length; index++) {
|
|
4
|
+
var relation = relations[index];
|
|
5
|
+
if (relation.type === "polymorphic")
|
|
6
|
+
continue;
|
|
7
|
+
if (relation.from.collection === collectionName && relation.from.field === fieldName) {
|
|
8
|
+
return relation;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
;
|
|
14
|
+
export function isRelationField(ctx, collectionName, fieldName) {
|
|
15
|
+
return Boolean(getFieldRelation(ctx, collectionName, fieldName));
|
|
16
|
+
}
|
|
17
|
+
;
|
|
18
|
+
export function getFieldRelationTarget(ctx, collectionName, fieldName) {
|
|
19
|
+
var _a, _b;
|
|
20
|
+
return (_b = (_a = getFieldRelation(ctx, collectionName, fieldName)) === null || _a === void 0 ? void 0 : _a.to.collection) !== null && _b !== void 0 ? _b : null;
|
|
21
|
+
}
|
|
22
|
+
;
|
|
23
|
+
export function getPolymorphicRelation(ctx, collectionName, fieldName) {
|
|
24
|
+
var relations = ctx.meta.relations;
|
|
25
|
+
for (var index = 0; index < relations.length; index++) {
|
|
26
|
+
var relation = relations[index];
|
|
27
|
+
if (relation.type === "polymorphic" &&
|
|
28
|
+
relation.from.collection === collectionName &&
|
|
29
|
+
relation.from.virtual_field === fieldName) {
|
|
30
|
+
return relation;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
;
|
|
36
|
+
export function recordHasChildrean(ctx, collectionName) {
|
|
37
|
+
for (var index = 0; index < ctx.meta.relations.length; index++) {
|
|
38
|
+
var relation = ctx.meta.relations[index];
|
|
39
|
+
if (relation.type === "polymorphic")
|
|
40
|
+
continue;
|
|
41
|
+
if (relation.to.collection === collectionName) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
;
|
package/dist/store.types.d.ts
CHANGED
package/dist/utils.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { type ClassValue } from "clsx";
|
|
2
2
|
import { MediaQuery } from 'svelte/reactivity';
|
|
3
|
-
import type { CTX } from "./store.types";
|
|
4
3
|
export declare function cn(...inputs: ClassValue[]): string;
|
|
5
4
|
export type WithoutChild<T> = T extends {
|
|
6
5
|
child?: any;
|
|
@@ -19,8 +18,6 @@ export declare const mediaQueries: {
|
|
|
19
18
|
xl: MediaQuery;
|
|
20
19
|
'2xl': MediaQuery;
|
|
21
20
|
};
|
|
22
|
-
export declare function getFieldRelation(ctx: CTX, collectionName: string, fieldName: string): any;
|
|
23
|
-
export declare function recordHasChildrean(ctx: CTX, collectionName: string): boolean;
|
|
24
21
|
export declare function calculateDrawerWidth(): number;
|
|
25
22
|
export declare function getChangedProperties(oldObj: Record<string, any>, newObj: Record<string, any>): Record<string, any>;
|
|
26
23
|
export declare function parseFunction(functionString: string): any;
|
package/dist/utils.js
CHANGED
|
@@ -15,27 +15,6 @@ export var mediaQueries = {
|
|
|
15
15
|
xl: new MediaQuery('min-width: 1280px'),
|
|
16
16
|
'2xl': new MediaQuery('min-width: 1536px'),
|
|
17
17
|
};
|
|
18
|
-
export function getFieldRelation(ctx, collectionName, fieldName) {
|
|
19
|
-
var relations = ctx.meta.relations;
|
|
20
|
-
for (var index = 0; index < relations.length; index++) {
|
|
21
|
-
var relation = relations[index];
|
|
22
|
-
if (relation.from.collection === collectionName && relation.from.field === fieldName) {
|
|
23
|
-
return relation;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
;
|
|
29
|
-
export function recordHasChildrean(ctx, collectionName) {
|
|
30
|
-
for (var index = 0; index < ctx.meta.relations.length; index++) {
|
|
31
|
-
var relation = ctx.meta.relations[index];
|
|
32
|
-
if (relation.to.collection === collectionName) {
|
|
33
|
-
return true;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
;
|
|
39
18
|
export function calculateDrawerWidth() {
|
|
40
19
|
var backgroundDrawerButtons = document.querySelectorAll(".backgroundDrawerButton");
|
|
41
20
|
var drawersCount = Array.from(backgroundDrawerButtons).length;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobb-js/studio",
|
|
3
3
|
"license": "UNLICENSED",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.20.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"postpublish": "./scripts/postpublish.sh"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@lobb-js/core": "^0.
|
|
45
|
+
"@lobb-js/core": "^0.24.0",
|
|
46
46
|
"@chromatic-com/storybook": "^4.1.2",
|
|
47
47
|
"@storybook/addon-a11y": "^10.0.1",
|
|
48
48
|
"@storybook/addon-docs": "^10.0.1",
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { getFieldRelationTarget, getPolymorphicRelation, isRelationField } from "../../relations";
|
|
3
3
|
import { ExternalLink } from "lucide-svelte";
|
|
4
4
|
import UpdateDetailViewButton from "../detailView/update/updateDetailViewButton.svelte";
|
|
5
5
|
import { getField } from "./utils";
|
|
6
6
|
import EnumBadge from "./enumBadge.svelte";
|
|
7
|
-
import
|
|
7
|
+
import PolymorphicFieldCell from "./polymorphicFieldCell.svelte";
|
|
8
8
|
import { getStudioContext } from "../../context";
|
|
9
9
|
|
|
10
10
|
const { ctx } = getStudioContext();
|
|
@@ -26,49 +26,50 @@
|
|
|
26
26
|
}: Props = $props();
|
|
27
27
|
|
|
28
28
|
const field = getField(ctx, fieldName, collectionName);
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
);
|
|
29
|
+
const relationTarget = getFieldRelationTarget(ctx, collectionName, fieldName);
|
|
30
|
+
const polymorphicRelation = getPolymorphicRelation(ctx, collectionName, fieldName);
|
|
31
|
+
const isRefrenceField = isRelationField(ctx, collectionName, fieldName);
|
|
33
32
|
const isPasswordField = ctx.meta.collections[collectionName].fields[fieldName]?.ui?.input?.type === "password";
|
|
34
33
|
</script>
|
|
35
34
|
|
|
36
|
-
{#if
|
|
35
|
+
{#if polymorphicRelation}
|
|
36
|
+
<PolymorphicFieldCell
|
|
37
|
+
collectionField={polymorphicRelation.from.collection_field}
|
|
38
|
+
idField={polymorphicRelation.from.id_field}
|
|
39
|
+
{entry}
|
|
40
|
+
bind:tableParams
|
|
41
|
+
/>
|
|
42
|
+
{:else if isRefrenceField}
|
|
43
|
+
{#if value?.id && value.id !== 0}
|
|
44
|
+
<div class="flex items-center gap-2">
|
|
45
|
+
<div>{value.id}</div>
|
|
46
|
+
{#if Object.values(value)[1]}
|
|
47
|
+
<div class="border bg-muted px-3 py-1 rounded-full">
|
|
48
|
+
{Object.values(value)[1]}
|
|
49
|
+
</div>
|
|
50
|
+
{/if}
|
|
51
|
+
<UpdateDetailViewButton
|
|
52
|
+
collectionName={relationTarget!}
|
|
53
|
+
recordId={value.id}
|
|
54
|
+
variant="ghost"
|
|
55
|
+
class="h-5 w-5 px-0 py-0 text-muted-foreground hover:bg-transparent"
|
|
56
|
+
Icon={ExternalLink}
|
|
57
|
+
onSuccessfullSave={async () => {
|
|
58
|
+
tableParams = { ...tableParams };
|
|
59
|
+
}}
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
{:else if value?.id === 0}
|
|
63
|
+
<div class="text-muted-foreground">PARENT ID</div>
|
|
64
|
+
{:else}
|
|
65
|
+
<div class="text-muted-foreground">NULL</div>
|
|
66
|
+
{/if}
|
|
67
|
+
{:else if isPasswordField}
|
|
37
68
|
<div class="text-muted-foreground tracking-widest">••••••</div>
|
|
38
69
|
{:else if value === ""}
|
|
39
70
|
<div class="text-muted-foreground">EMPTY STRING</div>
|
|
40
71
|
{:else if value === null || value === undefined}
|
|
41
72
|
<div class="text-muted-foreground">NULL</div>
|
|
42
|
-
{:else if isRefrenceField}
|
|
43
|
-
{#if typeof value !== "object"}
|
|
44
|
-
<div>{value}</div>
|
|
45
|
-
{:else if value.id !== 0}
|
|
46
|
-
{@const primaryField = Object.values(value)[1]}
|
|
47
|
-
{#if value.id}
|
|
48
|
-
<div class="flex items-center gap-2">
|
|
49
|
-
<div>{value.id}</div>
|
|
50
|
-
{#if primaryField}
|
|
51
|
-
<div class="border bg-muted px-3 py-1 rounded-full">
|
|
52
|
-
{primaryField}
|
|
53
|
-
</div>
|
|
54
|
-
{/if}
|
|
55
|
-
<UpdateDetailViewButton
|
|
56
|
-
collectionName={relation.to.collection}
|
|
57
|
-
recordId={value.id}
|
|
58
|
-
variant="ghost"
|
|
59
|
-
class="h-5 w-5 px-0 py-0 text-muted-foreground hover:bg-transparent"
|
|
60
|
-
Icon={ExternalLink}
|
|
61
|
-
onSuccessfullSave={async () => {
|
|
62
|
-
tableParams = { ...tableParams };
|
|
63
|
-
}}
|
|
64
|
-
></UpdateDetailViewButton>
|
|
65
|
-
</div>
|
|
66
|
-
{:else}
|
|
67
|
-
<div class="text-muted-foreground">NULL</div>
|
|
68
|
-
{/if}
|
|
69
|
-
{:else}
|
|
70
|
-
<div class="text-muted-foreground">PARENT ID</div>
|
|
71
|
-
{/if}
|
|
72
73
|
{:else if field?.enum}
|
|
73
74
|
<EnumBadge {value} enum={field.enum} />
|
|
74
75
|
{:else if field.type === "datetime"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ExternalLink, Table } from "lucide-svelte";
|
|
3
|
+
import UpdateDetailViewButton from "../detailView/update/updateDetailViewButton.svelte";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
collectionField: string;
|
|
7
|
+
idField: string;
|
|
8
|
+
entry: Record<string, any>;
|
|
9
|
+
tableParams?: any;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
collectionField,
|
|
14
|
+
idField,
|
|
15
|
+
entry,
|
|
16
|
+
tableParams = $bindable(),
|
|
17
|
+
}: Props = $props();
|
|
18
|
+
|
|
19
|
+
const targetCollection = $derived(entry[collectionField] ?? null);
|
|
20
|
+
const targetId = $derived(entry[idField] ?? null);
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
{#if targetId && targetCollection}
|
|
24
|
+
<div class="flex items-center gap-2">
|
|
25
|
+
<div class="flex items-center gap-1 border bg-muted px-2 py-1 rounded-md text-muted-foreground">
|
|
26
|
+
<Table size="11" />
|
|
27
|
+
{targetCollection}
|
|
28
|
+
</div>
|
|
29
|
+
<div>{targetId}</div>
|
|
30
|
+
<UpdateDetailViewButton
|
|
31
|
+
collectionName={targetCollection}
|
|
32
|
+
recordId={targetId}
|
|
33
|
+
variant="ghost"
|
|
34
|
+
class="h-5 w-5 px-0 py-0 text-muted-foreground hover:bg-transparent"
|
|
35
|
+
Icon={ExternalLink}
|
|
36
|
+
onSuccessfullSave={async () => {
|
|
37
|
+
tableParams = { ...tableParams };
|
|
38
|
+
}}
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
{:else}
|
|
42
|
+
<div class="text-muted-foreground">NULL</div>
|
|
43
|
+
{/if}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TableProps } from "./table.svelte";
|
|
2
2
|
import type { CTX } from "../../store.types";
|
|
3
|
+
import { getFieldRelationTarget } from "../../relations";
|
|
3
4
|
|
|
4
5
|
import {
|
|
5
6
|
Binary,
|
|
@@ -10,15 +11,18 @@ import {
|
|
|
10
11
|
Clock,
|
|
11
12
|
Hash,
|
|
12
13
|
Key,
|
|
14
|
+
Link,
|
|
13
15
|
Text,
|
|
14
16
|
Type,
|
|
15
17
|
} from "lucide-svelte/icons";
|
|
18
|
+
import { icons } from "lucide-svelte";
|
|
16
19
|
|
|
17
20
|
export function getCollectionColumns(ctx: CTX, collectionName: string): TableProps['columns'] {
|
|
18
21
|
const collectionFields = getFields(ctx, collectionName);
|
|
19
22
|
const headers: TableProps['columns'] = [];
|
|
20
23
|
for (const fieldName in collectionFields) {
|
|
21
24
|
const field = collectionFields[fieldName];
|
|
25
|
+
if ((field as any).ui?.hidden) continue;
|
|
22
26
|
headers.push({
|
|
23
27
|
id: field.key,
|
|
24
28
|
subtext: field.type,
|
|
@@ -30,6 +34,8 @@ export function getCollectionColumns(ctx: CTX, collectionName: string): TablePro
|
|
|
30
34
|
|
|
31
35
|
export function getFieldIcon(ctx: CTX, fieldName: string, collectionName: string) {
|
|
32
36
|
const field = getField(ctx, fieldName, collectionName);
|
|
37
|
+
const uiIcon = (field as any).ui?.icon;
|
|
38
|
+
if (uiIcon && uiIcon in icons) return (icons as any)[uiIcon];
|
|
33
39
|
if (fieldName === "id") {
|
|
34
40
|
return Key;
|
|
35
41
|
} else if (field.type === "string") {
|
|
@@ -43,6 +49,12 @@ export function getFieldIcon(ctx: CTX, fieldName: string, collectionName: string
|
|
|
43
49
|
} else if (field.type === "bool") {
|
|
44
50
|
return Binary;
|
|
45
51
|
} else if (field.type === "integer") {
|
|
52
|
+
const target = getFieldRelationTarget(ctx, collectionName, fieldName);
|
|
53
|
+
if (target) {
|
|
54
|
+
const targetIcon = ctx.meta.collections[target]?.ui?.icon;
|
|
55
|
+
if (targetIcon && targetIcon in icons) return (icons as any)[targetIcon];
|
|
56
|
+
return Link;
|
|
57
|
+
}
|
|
46
58
|
return Hash;
|
|
47
59
|
} else if (field.type === "long") {
|
|
48
60
|
return Hash;
|
|
@@ -58,7 +70,7 @@ export function getFieldIcon(ctx: CTX, fieldName: string, collectionName: string
|
|
|
58
70
|
return Clock;
|
|
59
71
|
}
|
|
60
72
|
|
|
61
|
-
|
|
73
|
+
return Braces;
|
|
62
74
|
}
|
|
63
75
|
|
|
64
76
|
export function getFields(ctx: CTX, collectionName: string) {
|
|
@@ -91,7 +103,7 @@ export function getCollectionParamsFields(ctx: CTX, collectionName: string, allF
|
|
|
91
103
|
const relations = ctx.meta.relations;
|
|
92
104
|
const foreignFields = relations
|
|
93
105
|
.filter((relation) => {
|
|
94
|
-
return relation.from.collection === collectionName
|
|
106
|
+
return relation.type !== "polymorphic" && relation.from.collection === collectionName;
|
|
95
107
|
})
|
|
96
108
|
.map((relation) => {
|
|
97
109
|
return {
|
|
@@ -161,46 +161,38 @@
|
|
|
161
161
|
<div class="flex-1 overflow-y-auto">
|
|
162
162
|
<div class="flex flex-col gap-4 p-4">
|
|
163
163
|
{#each fieldNames as fieldName}
|
|
164
|
-
{
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
class="flex flex-col gap-2"
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
>
|
|
177
|
-
<
|
|
178
|
-
|
|
164
|
+
{#if !ctx.meta.collections[collectionName].fields[fieldName]?.ui?.hidden}
|
|
165
|
+
{@const field = getField(ctx, fieldName, collectionName)}
|
|
166
|
+
{@const FieldIcon = getFieldIcon(ctx, fieldName, collectionName)}
|
|
167
|
+
<div class="flex flex-col gap-2">
|
|
168
|
+
<div class="flex flex-1 items-end justify-between gap-2 text-xs">
|
|
169
|
+
<div class="flex gap-2">
|
|
170
|
+
<div class="h-fit">{field.label}</div>
|
|
171
|
+
<div class="flex h-fit items-center gap-1 text-[0.7rem] text-muted-foreground">
|
|
172
|
+
<FieldIcon size="12" />
|
|
173
|
+
{field.type}
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
<div>
|
|
177
|
+
<ExtensionsComponents
|
|
178
|
+
name="dvFields.topRight.{collectionName}.{fieldName}"
|
|
179
|
+
utils={getExtensionUtils(lobb, ctx)}
|
|
180
|
+
bind:value={entry[fieldName]}
|
|
181
|
+
/>
|
|
179
182
|
</div>
|
|
180
183
|
</div>
|
|
181
|
-
<
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
184
|
+
<FieldInput
|
|
185
|
+
{collectionName}
|
|
186
|
+
{fieldName}
|
|
187
|
+
bind:value={
|
|
188
|
+
() => entry[fieldName],
|
|
189
|
+
(v) => (entry = { ...entry, [fieldName]: v })
|
|
190
|
+
}
|
|
191
|
+
bind:entry
|
|
192
|
+
errorMessages={fieldsErrors[fieldName]}
|
|
193
|
+
/>
|
|
188
194
|
</div>
|
|
189
|
-
|
|
190
|
-
{collectionName}
|
|
191
|
-
{fieldName}
|
|
192
|
-
bind:value={
|
|
193
|
-
() => entry[fieldName],
|
|
194
|
-
(v) =>
|
|
195
|
-
(entry = {
|
|
196
|
-
...entry,
|
|
197
|
-
[fieldName]: v,
|
|
198
|
-
})
|
|
199
|
-
}
|
|
200
|
-
{entry}
|
|
201
|
-
errorMessages={fieldsErrors[fieldName]}
|
|
202
|
-
/>
|
|
203
|
-
</div>
|
|
195
|
+
{/if}
|
|
204
196
|
{/each}
|
|
205
197
|
</div>
|
|
206
198
|
{#if showRelatedRecords}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { getStudioContext } from "../../context";
|
|
3
|
-
import {
|
|
3
|
+
import { getFieldRelationTarget, getPolymorphicRelation } from "../../relations";
|
|
4
4
|
import { Ban, Check, CircleAlert, X } from "lucide-svelte";
|
|
5
5
|
import { getField } from "../dataTable/utils";
|
|
6
6
|
import Button from "../ui/button/button.svelte";
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import type { EnumOption } from "@lobb-js/core";
|
|
12
12
|
import Textarea from "../ui/textarea/textarea.svelte";
|
|
13
13
|
import ForeingKeyInput from "../foreingKeyInput.svelte";
|
|
14
|
+
import PolymorphicInput from "../polymorphicInput.svelte";
|
|
14
15
|
import ExtensionsComponents from "../extensionsComponents.svelte";
|
|
15
16
|
import { getExtensionUtils } from "../../extensions/extensionUtils";
|
|
16
17
|
|
|
@@ -29,7 +30,7 @@
|
|
|
29
30
|
fieldName,
|
|
30
31
|
value = $bindable(),
|
|
31
32
|
errorMessages = [],
|
|
32
|
-
entry,
|
|
33
|
+
entry = $bindable(),
|
|
33
34
|
}: Props = $props();
|
|
34
35
|
|
|
35
36
|
const ui_input =
|
|
@@ -37,10 +38,11 @@
|
|
|
37
38
|
const ui =
|
|
38
39
|
ctx.meta.collections[collectionName].fields[fieldName].ui;
|
|
39
40
|
const field = getField(ctx, fieldName, collectionName);
|
|
40
|
-
const
|
|
41
|
+
const fieldRelationTarget = getFieldRelationTarget(ctx, collectionName, fieldName);
|
|
42
|
+
const polymorphicRelation = getPolymorphicRelation(ctx, collectionName, fieldName);
|
|
41
43
|
const isDisabled = field.key === 'id' || Boolean(ui?.disabled)
|
|
42
44
|
const disabledClasses = "pointer-events-none opacity-50";
|
|
43
|
-
const destructive: boolean = $derived(Boolean(errorMessages.length));
|
|
45
|
+
const destructive: boolean = $derived(!isDisabled && Boolean(errorMessages.length));
|
|
44
46
|
|
|
45
47
|
</script>
|
|
46
48
|
|
|
@@ -53,13 +55,27 @@
|
|
|
53
55
|
style="flex: 2;"
|
|
54
56
|
>
|
|
55
57
|
<Button
|
|
56
|
-
onclick={() =>
|
|
58
|
+
onclick={() => {
|
|
59
|
+
value = null;
|
|
60
|
+
if (polymorphicRelation && entry) {
|
|
61
|
+
entry[polymorphicRelation.from.collection_field] = null;
|
|
62
|
+
entry[polymorphicRelation.from.id_field] = null;
|
|
63
|
+
}
|
|
64
|
+
}}
|
|
57
65
|
variant="outline"
|
|
58
66
|
class="absolute right-0 top-0 z-10 mr-1.5 mt-1.5 aspect-square h-6 w-6 p-0"
|
|
59
67
|
Icon={Ban}
|
|
60
68
|
tabindex={-1}
|
|
61
69
|
></Button>
|
|
62
|
-
{#if
|
|
70
|
+
{#if polymorphicRelation && entry}
|
|
71
|
+
<PolymorphicInput
|
|
72
|
+
collectionField={polymorphicRelation.from.collection_field}
|
|
73
|
+
idField={polymorphicRelation.from.id_field}
|
|
74
|
+
targetCollections={polymorphicRelation.to}
|
|
75
|
+
bind:entry
|
|
76
|
+
{destructive}
|
|
77
|
+
/>
|
|
78
|
+
{:else if ui_input}
|
|
63
79
|
<FieldCustomInput
|
|
64
80
|
bind:value
|
|
65
81
|
type={ui_input.type}
|
|
@@ -73,12 +89,12 @@
|
|
|
73
89
|
class="bg-muted/30 text-xs"
|
|
74
90
|
bind:value
|
|
75
91
|
/>
|
|
76
|
-
{:else if
|
|
92
|
+
{:else if fieldRelationTarget && entry}
|
|
77
93
|
<ExtensionsComponents
|
|
78
|
-
name="detailView.fields.foreignKey.{
|
|
94
|
+
name="detailView.fields.foreignKey.{fieldRelationTarget}"
|
|
79
95
|
utils={getExtensionUtils(lobb, ctx)}
|
|
80
96
|
parentCollectionName={collectionName}
|
|
81
|
-
collectionName={
|
|
97
|
+
collectionName={fieldRelationTarget}
|
|
82
98
|
bind:value
|
|
83
99
|
{entry}
|
|
84
100
|
fieldName={field.key}
|
|
@@ -86,7 +102,7 @@
|
|
|
86
102
|
>
|
|
87
103
|
<ForeingKeyInput
|
|
88
104
|
parentCollectionName={collectionName}
|
|
89
|
-
collectionName={
|
|
105
|
+
collectionName={fieldRelationTarget}
|
|
90
106
|
bind:value
|
|
91
107
|
{entry}
|
|
92
108
|
fieldName={field.key}
|
|
@@ -263,7 +279,7 @@
|
|
|
263
279
|
bind:value
|
|
264
280
|
/>
|
|
265
281
|
{/if}
|
|
266
|
-
{#if errorMessages}
|
|
282
|
+
{#if !isDisabled && errorMessages}
|
|
267
283
|
{#each errorMessages as message}
|
|
268
284
|
<div class="flex gap-1 text-destructive">
|
|
269
285
|
<CircleAlert size="15" class="translate-y-[0.025rem]" />
|