@rebasepro/plugin-ai 0.17.3-canary.gdd23447 → 0.18.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/LICENSE +0 -1
- package/README.md +4 -0
- package/dist/api.d.ts +8 -0
- package/dist/index.es.js.map +1 -1
- package/package.json +35 -21
- package/src/api.ts +0 -320
- package/src/components/AutofillReviewDialog.tsx +0 -209
- package/src/components/DataEnhancementControllerProvider.tsx +0 -320
- package/src/components/FormEnhanceAction.tsx +0 -307
- package/src/editor/useEditorAIController.tsx +0 -24
- package/src/index.ts +0 -9
- package/src/tests/AutofillReviewDialog.test.tsx +0 -340
- package/src/tests/api.test.ts +0 -382
- package/src/tests/properties.test.ts +0 -420
- package/src/tests/request_agreement.test.ts +0 -240
- package/src/tests/review.test.tsx +0 -596
- package/src/tests/useDataEnhancementPlugin.test.tsx +0 -72
- package/src/tests/useEditorAIController.test.ts +0 -98
- package/src/tests/values.test.ts +0 -87
- package/src/types/data_enhancement_controller.tsx +0 -142
- package/src/useDataEnhancementPlugin.tsx +0 -68
- package/src/utils/properties.ts +0 -168
- package/src/utils/values.ts +0 -72
- package/src/vite-env.d.ts +0 -1
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { TextEncoder, TextDecoder } from "util";
|
|
2
|
-
Object.assign(global, { TextEncoder,
|
|
3
|
-
TextDecoder });
|
|
4
|
-
|
|
5
|
-
import { renderHook } from "@testing-library/react";
|
|
6
|
-
import { useEditorAIController } from "../editor/useEditorAIController";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* The editor's inline continuation.
|
|
10
|
-
*
|
|
11
|
-
* Small, but it was the FireCMS-era code that demanded a Firebase ID token and
|
|
12
|
-
* threw `"Firebase token is required"` without one — in a Rebase app there is
|
|
13
|
-
* no such thing. The property worth pinning is that it now needs no token at
|
|
14
|
-
* all, and that its streamed deltas reach the editor in order.
|
|
15
|
-
*/
|
|
16
|
-
jest.mock("@rebasepro/cms", () => ({}));
|
|
17
|
-
|
|
18
|
-
function streamingResponse(chunks: string[]): any {
|
|
19
|
-
const encoder = new TextEncoder();
|
|
20
|
-
let i = 0;
|
|
21
|
-
return {
|
|
22
|
-
ok: true,
|
|
23
|
-
status: 200,
|
|
24
|
-
body: {
|
|
25
|
-
getReader: () => ({
|
|
26
|
-
read: async () => i < chunks.length
|
|
27
|
-
? { done: false,
|
|
28
|
-
value: encoder.encode(chunks[i++]) }
|
|
29
|
-
: { done: true,
|
|
30
|
-
value: undefined }
|
|
31
|
-
})
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
afterEach(() => jest.restoreAllMocks());
|
|
37
|
-
|
|
38
|
-
describe("useEditorAIController", () => {
|
|
39
|
-
|
|
40
|
-
it("streams deltas in order and resolves with the whole continuation", async () => {
|
|
41
|
-
(global as any).fetch = jest.fn().mockResolvedValue(streamingResponse([
|
|
42
|
-
'event: delta\ndata: {"text":"made of "}\n\n',
|
|
43
|
-
'event: delta\ndata: {"text":"stainless steel"}\n\n',
|
|
44
|
-
"event: done\ndata: {}\n\n"
|
|
45
|
-
]));
|
|
46
|
-
|
|
47
|
-
const { result } = renderHook(() => useEditorAIController());
|
|
48
|
-
const seen: string[] = [];
|
|
49
|
-
const text = await result.current.autocomplete("The burrs are ", " Ships worldwide.", d => seen.push(d));
|
|
50
|
-
|
|
51
|
-
expect(seen).toEqual(["made of ", "stainless steel"]);
|
|
52
|
-
expect(text).toBe("made of stainless steel");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("needs no auth token — it does not send one, and does not demand one", async () => {
|
|
56
|
-
const fetchMock = jest.fn().mockResolvedValue(streamingResponse(["event: done\ndata: {}\n\n"]));
|
|
57
|
-
(global as any).fetch = fetchMock;
|
|
58
|
-
|
|
59
|
-
const { result } = renderHook(() => useEditorAIController());
|
|
60
|
-
await expect(result.current.autocomplete("a", "b", () => undefined)).resolves.toBe("");
|
|
61
|
-
|
|
62
|
-
const [, init] = fetchMock.mock.calls[0];
|
|
63
|
-
expect(init.headers).toEqual({ "Content-Type": "application/json" });
|
|
64
|
-
expect(JSON.stringify(init)).not.toMatch(/Bearer|Basic|token/i);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("sends the caret context the editor gave it", async () => {
|
|
68
|
-
const fetchMock = jest.fn().mockResolvedValue(streamingResponse(["event: done\ndata: {}\n\n"]));
|
|
69
|
-
(global as any).fetch = fetchMock;
|
|
70
|
-
|
|
71
|
-
const { result } = renderHook(() => useEditorAIController());
|
|
72
|
-
await result.current.autocomplete("before", "after", () => undefined);
|
|
73
|
-
|
|
74
|
-
expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({ textBefore: "before",
|
|
75
|
-
textAfter: "after" });
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("honours a custom endpoint, so a self-hoster can proxy it", async () => {
|
|
79
|
-
const fetchMock = jest.fn().mockResolvedValue(streamingResponse(["event: done\ndata: {}\n\n"]));
|
|
80
|
-
(global as any).fetch = fetchMock;
|
|
81
|
-
|
|
82
|
-
const { result } = renderHook(() => useEditorAIController({ endpoint: "https://ai.example.com" }));
|
|
83
|
-
await result.current.autocomplete("a", "b", () => undefined);
|
|
84
|
-
|
|
85
|
-
expect(fetchMock.mock.calls[0][0]).toBe("https://ai.example.com/autocomplete");
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("surfaces the service's message when it refuses", async () => {
|
|
89
|
-
(global as any).fetch = jest.fn().mockResolvedValue({
|
|
90
|
-
ok: false,
|
|
91
|
-
status: 429,
|
|
92
|
-
json: async () => ({ error: { message: "The free AI quota for today has been used up." } })
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
const { result } = renderHook(() => useEditorAIController());
|
|
96
|
-
await expect(result.current.autocomplete("a", "b", () => undefined)).rejects.toThrow(/quota for today/);
|
|
97
|
-
});
|
|
98
|
-
});
|
package/src/tests/values.test.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { flatMapEntityValues } from "../utils/values";
|
|
2
|
-
|
|
3
|
-
describe("flatMapEntityValues", () => {
|
|
4
|
-
it("returns flat object unchanged", () => {
|
|
5
|
-
const values = { name: "John",
|
|
6
|
-
age: 30 };
|
|
7
|
-
const result = flatMapEntityValues(values);
|
|
8
|
-
expect(result).toEqual({ name: "John",
|
|
9
|
-
age: 30 });
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it("flattens nested object to dot-notation keys", () => {
|
|
13
|
-
const values = {
|
|
14
|
-
address: { city: "NYC",
|
|
15
|
-
zip: "10001" }
|
|
16
|
-
};
|
|
17
|
-
const result = flatMapEntityValues(values);
|
|
18
|
-
expect(result).toEqual({
|
|
19
|
-
"address.city": "NYC",
|
|
20
|
-
"address.zip": "10001"
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("flattens deeply nested objects", () => {
|
|
25
|
-
const values = {
|
|
26
|
-
a: { b: { c: { d: "deep" } } }
|
|
27
|
-
};
|
|
28
|
-
const result = flatMapEntityValues(values);
|
|
29
|
-
expect(result).toEqual({ "a.b.c.d": "deep" });
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("returns empty object for empty input", () => {
|
|
33
|
-
expect(flatMapEntityValues({})).toEqual({});
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("returns empty object for null input", () => {
|
|
37
|
-
expect(flatMapEntityValues(null as unknown as object)).toEqual({});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("returns empty object for undefined input", () => {
|
|
41
|
-
expect(flatMapEntityValues(undefined as unknown as object)).toEqual({});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("handles mixed nested and flat values", () => {
|
|
45
|
-
const values = {
|
|
46
|
-
name: "John",
|
|
47
|
-
address: { city: "NYC" },
|
|
48
|
-
active: true
|
|
49
|
-
};
|
|
50
|
-
const result = flatMapEntityValues(values);
|
|
51
|
-
expect(result).toEqual({
|
|
52
|
-
name: "John",
|
|
53
|
-
"address.city": "NYC",
|
|
54
|
-
active: true
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("handles multiple nested objects", () => {
|
|
59
|
-
const values = {
|
|
60
|
-
home: { city: "NYC" },
|
|
61
|
-
work: { city: "SF" }
|
|
62
|
-
};
|
|
63
|
-
const result = flatMapEntityValues(values);
|
|
64
|
-
expect(result).toEqual({
|
|
65
|
-
"home.city": "NYC",
|
|
66
|
-
"work.city": "SF"
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("handles numeric values in nested objects", () => {
|
|
71
|
-
const values = {
|
|
72
|
-
stats: { visits: 100,
|
|
73
|
-
clicks: 50 }
|
|
74
|
-
};
|
|
75
|
-
const result = flatMapEntityValues(values);
|
|
76
|
-
expect(result).toEqual({
|
|
77
|
-
"stats.visits": 100,
|
|
78
|
-
"stats.clicks": 50
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("uses custom path prefix", () => {
|
|
83
|
-
const values = { name: "John" };
|
|
84
|
-
const result = flatMapEntityValues(values, "user");
|
|
85
|
-
expect(result).toEqual({ "user.name": "John" });
|
|
86
|
-
});
|
|
87
|
-
});
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import { EntityValues } from "@rebasepro/types";
|
|
2
|
-
import { EditorAIController } from "@rebasepro/cms";
|
|
3
|
-
|
|
4
|
-
export type GenerateParams<M extends Record<string, unknown>> = {
|
|
5
|
-
values: EntityValues<M>;
|
|
6
|
-
/** Free-text instruction from the operator, if they gave one. */
|
|
7
|
-
instructions?: string;
|
|
8
|
-
/** Restrict the run to one field. */
|
|
9
|
-
propertyKey?: string;
|
|
10
|
-
propertyInstructions?: string;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* One field the model wants to write, awaiting the operator's decision.
|
|
15
|
-
*
|
|
16
|
-
* Nothing here has touched the form. That is the entire point of the type: the
|
|
17
|
-
* previous design streamed generated text straight into the live fields, which
|
|
18
|
-
* meant a half-written sentence was indistinguishable from a bug, the
|
|
19
|
-
* operator's own words were overwritten by heuristics that tried to guess
|
|
20
|
-
* whether to append or replace, and there was no way back other than retyping.
|
|
21
|
-
*/
|
|
22
|
-
export type ProposedField = {
|
|
23
|
-
/** Dotted property path, e.g. `seo.title`. */
|
|
24
|
-
key: string;
|
|
25
|
-
/** The property's display name, falling back to its key. */
|
|
26
|
-
label: string;
|
|
27
|
-
/** What is in the form right now — shown so an overwrite is visible. */
|
|
28
|
-
currentValue: unknown;
|
|
29
|
-
/** What the model proposes. Grows while `pending`. */
|
|
30
|
-
proposed: unknown;
|
|
31
|
-
/** Still streaming. */
|
|
32
|
-
pending: boolean;
|
|
33
|
-
/** Whether Apply will write this one. */
|
|
34
|
-
selected: boolean;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export type AutofillReview = {
|
|
38
|
-
status: "generating" | "ready" | "failed";
|
|
39
|
-
/** Set when `status` is `failed`. */
|
|
40
|
-
error?: string;
|
|
41
|
-
/** In arrival order, so the list reads as the model works. */
|
|
42
|
-
fields: ProposedField[];
|
|
43
|
-
/** What was asked for, shown back to the operator while they review. */
|
|
44
|
-
instructions?: string;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export type DataEnhancementController = {
|
|
48
|
-
/**
|
|
49
|
-
* Whether autofill can actually be used right now.
|
|
50
|
-
*
|
|
51
|
-
* The conjunction of two separate things: the host app allows it for this
|
|
52
|
-
* collection ({@link DataEnhancementPluginProps.getConfigForPath}), *and*
|
|
53
|
-
* the service reported itself available. The second half is what the
|
|
54
|
-
* FireCMS-era plugin lacked — it rendered its button unconditionally
|
|
55
|
-
* against a host that no longer existed, so every click 404'd.
|
|
56
|
-
*/
|
|
57
|
-
enabled: boolean;
|
|
58
|
-
|
|
59
|
-
/** The run in flight or awaiting review; `null` when there is neither. */
|
|
60
|
-
review: AutofillReview | null;
|
|
61
|
-
|
|
62
|
-
/** Start a run. Opens {@link review}; never writes to the form. */
|
|
63
|
-
generate: <M extends Record<string, unknown>>(params: GenerateParams<M>) => Promise<void>;
|
|
64
|
-
|
|
65
|
-
/** Include or exclude one field from what Apply will write. */
|
|
66
|
-
toggleField: (key: string) => void;
|
|
67
|
-
|
|
68
|
-
/** Select or deselect every field at once. */
|
|
69
|
-
toggleAll: (selected: boolean) => void;
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Write the selected fields to the form and close the review.
|
|
73
|
-
*
|
|
74
|
-
* The only path by which this plugin mutates a record, and it runs once per
|
|
75
|
-
* run rather than once per token — so it is a single undo step and a single
|
|
76
|
-
* dirty transition, not hundreds.
|
|
77
|
-
*/
|
|
78
|
-
applyReview: () => void;
|
|
79
|
-
|
|
80
|
-
/** Close the review, writing nothing. The record is untouched. */
|
|
81
|
-
dismissReview: () => void;
|
|
82
|
-
|
|
83
|
-
getSamplePrompts: (entityName: string, input?: string) => Promise<SamplePromptsResult>;
|
|
84
|
-
|
|
85
|
-
editorAIController?: EditorAIController;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
/** What `GET /status` answers. Everything else is gated on `available`. */
|
|
89
|
-
export type AiStatus = {
|
|
90
|
-
available: boolean;
|
|
91
|
-
model?: string;
|
|
92
|
-
features?: string[];
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
export type AutofillResult = {
|
|
96
|
-
/** Every field the service completed, keyed by property path. */
|
|
97
|
-
suggestions: Record<string, unknown>;
|
|
98
|
-
usage?: { inputTokens?: number; outputTokens?: number };
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
export type SamplePrompt = {
|
|
102
|
-
prompt: string;
|
|
103
|
-
type: "recent" | "sample";
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
export type SamplePromptsResult = {
|
|
107
|
-
prompts: SamplePrompt[];
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* The autofill request body.
|
|
112
|
-
*
|
|
113
|
-
* The property schema travels with every request because the service has no
|
|
114
|
-
* access to the caller's collections — it is a hosted endpoint reachable from
|
|
115
|
-
* any self-hosted admin panel. That is the cost of not putting an LLM
|
|
116
|
-
* dependency in `@rebasepro/server`; had the route lived in the backend it
|
|
117
|
-
* could have read the collection registry and this would be three fields.
|
|
118
|
-
*/
|
|
119
|
-
export type AutofillRequest = {
|
|
120
|
-
entityName: string;
|
|
121
|
-
entityDescription?: string;
|
|
122
|
-
values: Record<string, unknown>;
|
|
123
|
-
properties: Record<string, InputProperty>;
|
|
124
|
-
propertyKey?: string;
|
|
125
|
-
propertyInstructions?: string;
|
|
126
|
-
instructions?: string;
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
export type InputProperty = {
|
|
130
|
-
name?: string;
|
|
131
|
-
description?: string;
|
|
132
|
-
type: string;
|
|
133
|
-
fieldConfigId: string;
|
|
134
|
-
enum?: string[];
|
|
135
|
-
disabled?: boolean;
|
|
136
|
-
of?: InputProperty;
|
|
137
|
-
oneOf?: {
|
|
138
|
-
properties: Record<string, InputProperty>;
|
|
139
|
-
typeField?: string;
|
|
140
|
-
valueField?: string;
|
|
141
|
-
};
|
|
142
|
-
};
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
|
|
3
|
-
import { CollectionConfig, User } from "@rebasepro/types";
|
|
4
|
-
import { RebasePlugin } from "@rebasepro/cms-types";
|
|
5
|
-
import { DataEnhancementControllerProvider } from "./components/DataEnhancementControllerProvider";
|
|
6
|
-
import { FormEnhanceAction } from "./components/FormEnhanceAction";
|
|
7
|
-
|
|
8
|
-
export interface DataEnhancementPluginProps {
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Use this function to determine if the data enhancement plugin should be enabled for a given path.
|
|
12
|
-
* If this function is not provided, the plugin will be enabled for all paths.
|
|
13
|
-
* If the function returns false, the plugin will be disabled for the given path.
|
|
14
|
-
*
|
|
15
|
-
* @param path
|
|
16
|
-
* @param collection
|
|
17
|
-
*/
|
|
18
|
-
getConfigForPath?: (props: {
|
|
19
|
-
path: string,
|
|
20
|
-
collection: CollectionConfig,
|
|
21
|
-
user: User | null
|
|
22
|
-
}) => boolean;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Base URL of the AI service.
|
|
26
|
-
*
|
|
27
|
-
* Defaults to the one Rebase hosts, which is free to use and needs no
|
|
28
|
-
* configuration. Point it at your own deployment to keep generation inside
|
|
29
|
-
* your infrastructure — the wire format is documented in `src/api.ts`, and
|
|
30
|
-
* the reference implementation is `saas/backend/functions/ai.ts`.
|
|
31
|
-
*
|
|
32
|
-
* Whatever it points at, the plugin renders nothing until that host's
|
|
33
|
-
* `GET /status` reports itself available.
|
|
34
|
-
*/
|
|
35
|
-
endpoint?: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Use this hook to initialise the data enhancement plugin.
|
|
40
|
-
* This is likely the only hook you will need to use.
|
|
41
|
-
* @param props
|
|
42
|
-
*/
|
|
43
|
-
export function useDataEnhancementPlugin(props?: DataEnhancementPluginProps): RebasePlugin {
|
|
44
|
-
|
|
45
|
-
const getConfigForPath = props?.getConfigForPath;
|
|
46
|
-
const endpoint = props?.endpoint;
|
|
47
|
-
|
|
48
|
-
return React.useMemo(() => ({
|
|
49
|
-
key: "data_enhancement",
|
|
50
|
-
slots: [
|
|
51
|
-
{
|
|
52
|
-
slot: "form.actions",
|
|
53
|
-
Component: FormEnhanceAction,
|
|
54
|
-
order: 40
|
|
55
|
-
}
|
|
56
|
-
],
|
|
57
|
-
providers: [
|
|
58
|
-
{
|
|
59
|
-
scope: "form" as const,
|
|
60
|
-
Component: DataEnhancementControllerProvider as React.ComponentType<any>,
|
|
61
|
-
props: {
|
|
62
|
-
getConfigForPath,
|
|
63
|
-
endpoint
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
]
|
|
67
|
-
}), [getConfigForPath, endpoint]);
|
|
68
|
-
}
|
package/src/utils/properties.ts
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import { getFieldId } from "@rebasepro/cms";
|
|
2
|
-
import { EnumValues, Properties, Property } from "@rebasepro/types";
|
|
3
|
-
import { isPropertyBuilder } from "@rebasepro/common";
|
|
4
|
-
import { InputProperty } from "../types/data_enhancement_controller";
|
|
5
|
-
import { getValueInPath } from "@rebasepro/utils";
|
|
6
|
-
|
|
7
|
-
export function getSimplifiedProperties<M extends Record<string, any>>(properties: Properties, values: M, path = ""): Record<string, InputProperty> {
|
|
8
|
-
if (!properties) return {};
|
|
9
|
-
return Object.entries(properties)
|
|
10
|
-
.map(([key, property]) => {
|
|
11
|
-
if (isPropertyBuilder(property)) return {};
|
|
12
|
-
const fullKey = path ? `${path}.${key}` : key;
|
|
13
|
-
const valueInPath = getValueInPath(values, fullKey);
|
|
14
|
-
return getSimplifiedProperty(property, fullKey, valueInPath)
|
|
15
|
-
})
|
|
16
|
-
.reduce((a, b) => ({ ...a,
|
|
17
|
-
...b }), {});
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function getSimpleProperty(property: Property): InputProperty {
|
|
21
|
-
const fieldId = getFieldId(property);
|
|
22
|
-
if (!fieldId) {
|
|
23
|
-
console.error("No fieldId found for property", property);
|
|
24
|
-
throw new Error("Field id not found");
|
|
25
|
-
}
|
|
26
|
-
return {
|
|
27
|
-
name: property.name,
|
|
28
|
-
description: property.description,
|
|
29
|
-
type: property.type,
|
|
30
|
-
fieldConfigId: fieldId,
|
|
31
|
-
enum: "enum" in property && property.enum
|
|
32
|
-
? getSimpleEnumValues(property.enum)
|
|
33
|
-
: undefined,
|
|
34
|
-
disabled: Boolean(property.admin?.disabled || property.admin?.readOnly)
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function getSimplifiedProperty(property: Property, path: string, value?: unknown): Record<string, InputProperty> {
|
|
39
|
-
if (isPropertyBuilder(property)) return {};
|
|
40
|
-
if (property.type === "array") {
|
|
41
|
-
|
|
42
|
-
if (property.of && !Array.isArray(property.of) && !isPropertyBuilder(property.of)) {
|
|
43
|
-
const arrayParentProperty: InputProperty = {
|
|
44
|
-
name: property.name,
|
|
45
|
-
description: property.description,
|
|
46
|
-
type: property.type,
|
|
47
|
-
fieldConfigId: "repeat",
|
|
48
|
-
disabled: Boolean(property.admin?.disabled || property.admin?.readOnly),
|
|
49
|
-
of: getSimpleProperty(property.of as Property)
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const result = { [path]: arrayParentProperty };
|
|
53
|
-
// if (Array.isArray(value)) {
|
|
54
|
-
// result = {
|
|
55
|
-
// ...result,
|
|
56
|
-
// ...value
|
|
57
|
-
// .map((v, i) => getSimplifiedProperty(property.of, `${path}.${i}`, v))
|
|
58
|
-
// .reduce((a, b) => ({ ...a, ...b }), {})
|
|
59
|
-
// };
|
|
60
|
-
// }
|
|
61
|
-
//
|
|
62
|
-
// const existingValuesCount = Array.isArray(value) ? value.length : 0;
|
|
63
|
-
//
|
|
64
|
-
// const newValuesCount = property.of && !isPropertyBuilder<any, any>(property.of) && (property.of as Property).type === "map" ? 1 : 3;
|
|
65
|
-
// result = {
|
|
66
|
-
// ...result,
|
|
67
|
-
// // ...Array.from(Array(newValuesCount))
|
|
68
|
-
// // .map((v, i) => getSimplifiedProperty(property.of, `${path}.${i + existingValuesCount}`, v))
|
|
69
|
-
// // .reduce((a, b) => ({ ...a, ...b }), {})
|
|
70
|
-
// }
|
|
71
|
-
|
|
72
|
-
return result;
|
|
73
|
-
} else if (property.oneOf) {
|
|
74
|
-
|
|
75
|
-
const arrayParentProperty: InputProperty = {
|
|
76
|
-
name: property.name,
|
|
77
|
-
description: property.description,
|
|
78
|
-
type: property.type,
|
|
79
|
-
fieldConfigId: "block",
|
|
80
|
-
disabled: Boolean(property.admin?.disabled || property.admin?.readOnly),
|
|
81
|
-
oneOf: {
|
|
82
|
-
typeField: property.oneOf.typeField,
|
|
83
|
-
valueField: property.oneOf.valueField,
|
|
84
|
-
properties: Object.entries(property.oneOf.properties)
|
|
85
|
-
.map(([key, prop]) => ({ [key]: getSimpleProperty(prop) }))
|
|
86
|
-
.reduce((a, b) => ({ ...a,
|
|
87
|
-
...b }), {})
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
if (!Array.isArray(value)) {
|
|
92
|
-
return { [path]: arrayParentProperty };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return value.map((v, i) => {
|
|
96
|
-
if (v == null) return {};
|
|
97
|
-
const typeKey = property.oneOf!.typeField ?? "type";
|
|
98
|
-
const oneOfType = v[typeKey];
|
|
99
|
-
const valueKey = property.oneOf!.valueField ?? "value";
|
|
100
|
-
const oneOfValue = v[valueKey];
|
|
101
|
-
const childProperty = property.oneOf!.properties[oneOfType];
|
|
102
|
-
if (childProperty === undefined) {
|
|
103
|
-
console.error(`No property found for type ${oneOfType}`, property.oneOf!.properties);
|
|
104
|
-
return {};
|
|
105
|
-
}
|
|
106
|
-
const simplifiedProperty = getSimplifiedProperty(childProperty, `${path}.${i}.${valueKey}`, oneOfValue);
|
|
107
|
-
return {
|
|
108
|
-
[`${path}.${i}.${typeKey}`]: oneOfType,
|
|
109
|
-
...simplifiedProperty
|
|
110
|
-
};
|
|
111
|
-
}).reduce((a, b) => ({ ...a,
|
|
112
|
-
...b }), { [path]: arrayParentProperty });
|
|
113
|
-
}
|
|
114
|
-
} else if (property.type === "map") {
|
|
115
|
-
if (property.properties) {
|
|
116
|
-
const mapProperties: Record<string, InputProperty> = Object.entries(property.properties)
|
|
117
|
-
.map(([key, childProperty]) => {
|
|
118
|
-
const childValue = value && typeof value === "object" ? (value as Record<string, unknown>)[key] : undefined;
|
|
119
|
-
return getSimplifiedProperty(childProperty, key, childValue);
|
|
120
|
-
})
|
|
121
|
-
.map(o => attachPathToKeys(o, path))
|
|
122
|
-
.reduce((a, b) => ({ ...a,
|
|
123
|
-
...b }), {});
|
|
124
|
-
|
|
125
|
-
if (Object.keys(mapProperties).length === 0) return {};
|
|
126
|
-
const mapParentProperty: InputProperty = {
|
|
127
|
-
name: property.name,
|
|
128
|
-
description: property.description,
|
|
129
|
-
type: property.type,
|
|
130
|
-
fieldConfigId: "group",
|
|
131
|
-
disabled: Boolean(property.admin?.disabled || property.admin?.readOnly)
|
|
132
|
-
};
|
|
133
|
-
return {
|
|
134
|
-
[path]: mapParentProperty,
|
|
135
|
-
...mapProperties
|
|
136
|
-
} as Record<string, InputProperty>;
|
|
137
|
-
}
|
|
138
|
-
} else {
|
|
139
|
-
const fieldId = getFieldId(property);
|
|
140
|
-
if (!fieldId) {
|
|
141
|
-
console.warn(`No fieldId found for property ${path} with type ${property.type}`);
|
|
142
|
-
return {};
|
|
143
|
-
}
|
|
144
|
-
return {
|
|
145
|
-
[path]: getSimpleProperty(property)
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
return {};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// attach a path to every key in an object
|
|
152
|
-
function attachPathToKeys(obj: Record<string, InputProperty>, path = ""): Record<string, InputProperty> {
|
|
153
|
-
return Object.entries(obj)
|
|
154
|
-
.map(([key, value]) => {
|
|
155
|
-
const fullKey = path ? `${path}.${key}` : key;
|
|
156
|
-
return { [fullKey]: value };
|
|
157
|
-
})
|
|
158
|
-
.reduce((a, b) => ({ ...a,
|
|
159
|
-
...b }), {});
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function getSimpleEnumValues(enumValues: EnumValues): string[] {
|
|
163
|
-
if (Array.isArray(enumValues))
|
|
164
|
-
return enumValues.map(v => String(v.id));
|
|
165
|
-
if (typeof enumValues === "object")
|
|
166
|
-
return Object.keys(enumValues);
|
|
167
|
-
throw Error("getSimpleEnumValues: Invalid enumValues");
|
|
168
|
-
}
|
package/src/utils/values.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { InputProperty } from "../types/data_enhancement_controller";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Flatten a record onto the dotted paths the property map uses.
|
|
5
|
-
*
|
|
6
|
-
* The two halves of an autofill request have to be keyed the same way: the
|
|
7
|
-
* service decides a field is empty by looking up `values[key]` for every `key`
|
|
8
|
-
* in `properties`, so a value filed under a key the property map has never
|
|
9
|
-
* heard of is a value the service cannot see.
|
|
10
|
-
*
|
|
11
|
-
* Only plain objects are containers. This used to recurse into anything
|
|
12
|
-
* `typeof value === "object"`, which is both an array and a `Date` — so
|
|
13
|
-
* `tags: ["a", "b"]` was sent as `tags.0`/`tags.1` while the property map still
|
|
14
|
-
* called it `tags`, and a `Date` disappeared entirely (`Object.entries(date)` is
|
|
15
|
-
* `[]`). Both then read as empty on the far side and came back in the review
|
|
16
|
-
* pre-ticked to replace a value the record already had. `getSimplifiedProperties`
|
|
17
|
-
* names an array by its own path and never descends into one, so neither does
|
|
18
|
-
* this.
|
|
19
|
-
*/
|
|
20
|
-
export function flatMapEntityValues<M extends object>(values: M, path = ""): Record<string, unknown> {
|
|
21
|
-
if (!values) return {};
|
|
22
|
-
return Object.entries(values).flatMap(([key, value]) => {
|
|
23
|
-
const currentPath = path ? `${path}.${key}` : key;
|
|
24
|
-
if (isPlainObject(value)) {
|
|
25
|
-
return flatMapEntityValues(value, currentPath);
|
|
26
|
-
} else {
|
|
27
|
-
return { [currentPath]: value };
|
|
28
|
-
}
|
|
29
|
-
}).reduce((acc, curr) => ({ ...acc,
|
|
30
|
-
...curr }), {});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* A container, as opposed to a leaf value.
|
|
35
|
-
*
|
|
36
|
-
* Arrays, dates, files and every other class instance are values in their own
|
|
37
|
-
* right — a map property is the only thing whose children are separate fields.
|
|
38
|
-
*/
|
|
39
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
40
|
-
if (value === null || typeof value !== "object") return false;
|
|
41
|
-
const proto = Object.getPrototypeOf(value);
|
|
42
|
-
return proto === Object.prototype || proto === null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Drop the values of properties the panel will not let anyone edit.
|
|
47
|
-
*
|
|
48
|
-
* A `readOnly` or `disabled` property is already excluded from what the service
|
|
49
|
-
* may fill, but the values map was built from the whole record, and the prompt
|
|
50
|
-
* includes every value it is given as context. So a field marked read-only
|
|
51
|
-
* because a backend hook owns it — an internal note, a customer id — was still
|
|
52
|
-
* being transmitted and pasted into the prompt. The collection config lives
|
|
53
|
-
* here, so this is the honest place to decide it: disabled means neither
|
|
54
|
-
* fillable nor context.
|
|
55
|
-
*
|
|
56
|
-
* Prefixes match too: a disabled map takes its children with it.
|
|
57
|
-
*/
|
|
58
|
-
export function omitDisabledValues(
|
|
59
|
-
values: Record<string, unknown>,
|
|
60
|
-
properties: Record<string, InputProperty>
|
|
61
|
-
): Record<string, unknown> {
|
|
62
|
-
const disabled = Object.entries(properties ?? {})
|
|
63
|
-
// A property map can carry a non-object at a path (see the `oneOf`
|
|
64
|
-
// branch of `getSimplifiedProperty`), and `"disabled" in aString` throws.
|
|
65
|
-
.filter(([, property]) => property && typeof property === "object" && property.disabled)
|
|
66
|
-
.map(([key]) => key);
|
|
67
|
-
if (disabled.length === 0) return values;
|
|
68
|
-
return Object.fromEntries(
|
|
69
|
-
Object.entries(values).filter(([key]) =>
|
|
70
|
-
!disabled.some((prefix) => key === prefix || key.startsWith(`${prefix}.`)))
|
|
71
|
-
);
|
|
72
|
-
}
|
package/src/vite-env.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
/// <reference types="vite/client" />
|