jazz-tools 0.19.0 → 0.19.2
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/.turbo/turbo-build.log +53 -53
- package/CHANGELOG.md +21 -0
- package/dist/{chunk-P3YLNFN4.js → chunk-NCNM6UDZ.js} +61 -22
- package/dist/chunk-NCNM6UDZ.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/inspector/{custom-element-QESCMFY7.js → custom-element-ABVPHX53.js} +1118 -465
- package/dist/inspector/custom-element-ABVPHX53.js.map +1 -0
- package/dist/inspector/index.js +1090 -437
- package/dist/inspector/index.js.map +1 -1
- package/dist/inspector/register-custom-element.js +1 -1
- package/dist/inspector/tests/utils/history.test.d.ts +2 -0
- package/dist/inspector/tests/utils/history.test.d.ts.map +1 -0
- package/dist/inspector/tests/viewer/co-value-editor.test.d.ts +2 -0
- package/dist/inspector/tests/viewer/co-value-editor.test.d.ts.map +1 -0
- package/dist/inspector/tests/viewer/comap-view.test.d.ts +2 -0
- package/dist/inspector/tests/viewer/comap-view.test.d.ts.map +1 -0
- package/dist/inspector/ui/icon.d.ts +6 -0
- package/dist/inspector/ui/icon.d.ts.map +1 -1
- package/dist/inspector/ui/icons/add-icon.d.ts +2 -0
- package/dist/inspector/ui/icons/add-icon.d.ts.map +1 -0
- package/dist/inspector/ui/icons/edit-icon.d.ts +2 -0
- package/dist/inspector/ui/icons/edit-icon.d.ts.map +1 -0
- package/dist/inspector/ui/icons/history.d.ts +2 -0
- package/dist/inspector/ui/icons/history.d.ts.map +1 -0
- package/dist/inspector/utils/history.d.ts +3 -0
- package/dist/inspector/utils/history.d.ts.map +1 -0
- package/dist/inspector/utils/transactions-changes.d.ts +38 -0
- package/dist/inspector/utils/transactions-changes.d.ts.map +1 -0
- package/dist/inspector/viewer/co-map-view.d.ts +9 -0
- package/dist/inspector/viewer/co-map-view.d.ts.map +1 -0
- package/dist/inspector/viewer/co-value-editor.d.ts +10 -0
- package/dist/inspector/viewer/co-value-editor.d.ts.map +1 -0
- package/dist/inspector/viewer/grid-view.d.ts +3 -2
- package/dist/inspector/viewer/grid-view.d.ts.map +1 -1
- package/dist/inspector/viewer/history-view.d.ts.map +1 -1
- package/dist/inspector/viewer/page.d.ts.map +1 -1
- package/dist/testing.js +1 -1
- package/dist/tools/coValues/CoFieldInit.d.ts +2 -1
- package/dist/tools/coValues/CoFieldInit.d.ts.map +1 -1
- package/dist/tools/coValues/deepLoading.d.ts +8 -7
- package/dist/tools/coValues/deepLoading.d.ts.map +1 -1
- package/dist/tools/coValues/interfaces.d.ts +3 -3
- package/dist/tools/coValues/interfaces.d.ts.map +1 -1
- package/dist/tools/coValues/schemaUnion.d.ts +6 -9
- package/dist/tools/coValues/schemaUnion.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/CoDiscriminatedUnionSchema.d.ts +18 -7
- package/dist/tools/implementation/zodSchema/schemaTypes/CoDiscriminatedUnionSchema.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/typeConverters/CoFieldSchemaInit.d.ts +3 -2
- package/dist/tools/implementation/zodSchema/typeConverters/CoFieldSchemaInit.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/unionUtils.d.ts.map +1 -1
- package/dist/tools/subscribe/SubscriptionScope.d.ts +1 -0
- package/dist/tools/subscribe/SubscriptionScope.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/inspector/tests/utils/history.test.ts +401 -0
- package/src/inspector/tests/viewer/co-value-editor.test.tsx +903 -0
- package/src/inspector/tests/viewer/comap-view.test.tsx +581 -0
- package/src/inspector/ui/icon.tsx +6 -0
- package/src/inspector/ui/icons/add-icon.tsx +21 -0
- package/src/inspector/ui/icons/edit-icon.tsx +17 -0
- package/src/inspector/ui/icons/history.tsx +28 -0
- package/src/inspector/ui/modal.tsx +3 -3
- package/src/inspector/utils/history.ts +49 -0
- package/src/inspector/utils/transactions-changes.ts +98 -0
- package/src/inspector/viewer/co-map-view.tsx +312 -0
- package/src/inspector/viewer/co-value-editor.tsx +164 -0
- package/src/inspector/viewer/grid-view.tsx +139 -10
- package/src/inspector/viewer/history-view.tsx +16 -118
- package/src/inspector/viewer/page.tsx +13 -0
- package/src/react-core/tests/usePassPhraseAuth.test.ts +1 -1
- package/src/tools/coValues/CoFieldInit.ts +6 -3
- package/src/tools/coValues/coList.ts +1 -1
- package/src/tools/coValues/deepLoading.ts +85 -71
- package/src/tools/coValues/interfaces.ts +3 -3
- package/src/tools/coValues/schemaUnion.ts +19 -14
- package/src/tools/implementation/zodSchema/schemaTypes/CoDiscriminatedUnionSchema.ts +69 -9
- package/src/tools/implementation/zodSchema/typeConverters/CoFieldSchemaInit.ts +12 -7
- package/src/tools/implementation/zodSchema/unionUtils.ts +35 -4
- package/src/tools/subscribe/SubscriptionScope.ts +3 -14
- package/src/tools/tests/coDiscriminatedUnion.test.ts +347 -5
- package/src/tools/tests/coVector.test.ts +43 -0
- package/src/tools/tests/deepLoading.test.ts +55 -59
- package/src/tools/tests/schema.resolved.test.ts +70 -1
- package/dist/chunk-P3YLNFN4.js.map +0 -1
- package/dist/inspector/custom-element-QESCMFY7.js.map +0 -1
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { JsonObject, JsonValue, RawCoMap, Role } from "cojson";
|
|
2
|
+
import type { MapOpPayload } from "cojson/dist/coValues/coMap.js";
|
|
3
|
+
|
|
4
|
+
export function restoreCoMapToTimestamp(
|
|
5
|
+
coValue: RawCoMap,
|
|
6
|
+
timestamp: number,
|
|
7
|
+
removeUnknownProperties: boolean,
|
|
8
|
+
): void {
|
|
9
|
+
const myRole = coValue.group.myRole();
|
|
10
|
+
|
|
11
|
+
if (
|
|
12
|
+
myRole === undefined ||
|
|
13
|
+
!(["admin", "manager", "writer", "writerOnly"] as Role[]).includes(myRole)
|
|
14
|
+
) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const newCoValue = coValue.atTime(timestamp).toJSON() as JsonObject | null;
|
|
19
|
+
const oldCoValue = coValue.toJSON() as JsonObject;
|
|
20
|
+
|
|
21
|
+
if (newCoValue === null) return;
|
|
22
|
+
|
|
23
|
+
let changes: MapOpPayload<string, JsonValue | undefined>[] = [];
|
|
24
|
+
|
|
25
|
+
if (removeUnknownProperties) {
|
|
26
|
+
for (const key in oldCoValue) {
|
|
27
|
+
if (!(key in newCoValue)) {
|
|
28
|
+
changes.push({
|
|
29
|
+
op: "del",
|
|
30
|
+
key,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const key in newCoValue) {
|
|
37
|
+
if (newCoValue[key] !== oldCoValue[key]) {
|
|
38
|
+
changes.push({
|
|
39
|
+
op: "set",
|
|
40
|
+
key,
|
|
41
|
+
value: newCoValue[key],
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (changes.length > 0) {
|
|
47
|
+
coValue.core.makeTransaction(changes, "private");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AccountRole,
|
|
3
|
+
BinaryStreamStart,
|
|
4
|
+
CoID,
|
|
5
|
+
RawCoValue,
|
|
6
|
+
Role,
|
|
7
|
+
} from "cojson";
|
|
8
|
+
import type { ListOpPayload } from "cojson/dist/coValues/coList.js";
|
|
9
|
+
import type { MapOpPayload } from "cojson/dist/coValues/coMap.js";
|
|
10
|
+
import type {
|
|
11
|
+
BinaryStreamChunk,
|
|
12
|
+
BinaryStreamEnd,
|
|
13
|
+
} from "cojson/dist/coValues/coStream.js";
|
|
14
|
+
import { isCoId } from "../viewer/types";
|
|
15
|
+
|
|
16
|
+
export const isGroupExtension = (
|
|
17
|
+
change: any,
|
|
18
|
+
): change is Extract<
|
|
19
|
+
MapOpPayload<`child_${string}`, "extend">,
|
|
20
|
+
{ op: "set" }
|
|
21
|
+
> => {
|
|
22
|
+
return change?.op === "set" && change?.value === "extend";
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const isGroupExtendRevocation = (
|
|
26
|
+
change: any,
|
|
27
|
+
): change is Extract<
|
|
28
|
+
MapOpPayload<`child_${string}`, "revoked">,
|
|
29
|
+
{ op: "set" }
|
|
30
|
+
> => {
|
|
31
|
+
return change?.op === "set" && change?.value === "revoked";
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const isGroupPromotion = (
|
|
35
|
+
change: any,
|
|
36
|
+
): change is Extract<
|
|
37
|
+
MapOpPayload<`parent_co_${string}`, AccountRole>,
|
|
38
|
+
{ op: "set" }
|
|
39
|
+
> => {
|
|
40
|
+
return change?.op === "set" && change?.key.startsWith("parent_co_");
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const isUserPromotion = (
|
|
44
|
+
change: any,
|
|
45
|
+
): change is Extract<MapOpPayload<CoID<RawCoValue>, Role>, { op: "set" }> => {
|
|
46
|
+
return (
|
|
47
|
+
change?.op === "set" && (isCoId(change?.key) || change?.key === "everyone")
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const isKeyRevelation = (
|
|
52
|
+
change: any,
|
|
53
|
+
): change is Extract<
|
|
54
|
+
MapOpPayload<`${string}_for_${string}`, string>,
|
|
55
|
+
{ op: "set" }
|
|
56
|
+
> => {
|
|
57
|
+
return change?.op === "set" && change?.key.includes("_for_");
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const isPropertySet = (
|
|
61
|
+
change: any,
|
|
62
|
+
): change is Extract<MapOpPayload<string, any>, { op: "set" }> => {
|
|
63
|
+
return change?.op === "set" && "key" in change && "value" in change;
|
|
64
|
+
};
|
|
65
|
+
export const isPropertyDeletion = (
|
|
66
|
+
change: any,
|
|
67
|
+
): change is Extract<MapOpPayload<string, any>, { op: "del" }> => {
|
|
68
|
+
return change?.op === "del" && "key" in change;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const isItemAppend = (
|
|
72
|
+
change: any,
|
|
73
|
+
): change is Extract<ListOpPayload<any>, { op: "app" }> => {
|
|
74
|
+
return change?.op === "app" && "after" in change && "value" in change;
|
|
75
|
+
};
|
|
76
|
+
export const isItemPrepend = (
|
|
77
|
+
change: any,
|
|
78
|
+
): change is Extract<ListOpPayload<any>, { op: "pre" }> => {
|
|
79
|
+
return change?.op === "pre" && "before" in change && "value" in change;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const isItemDeletion = (
|
|
83
|
+
change: any,
|
|
84
|
+
): change is Extract<ListOpPayload<any>, { op: "del" }> => {
|
|
85
|
+
return change?.op === "del" && "insertion" in change;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const isStreamStart = (change: any): change is BinaryStreamStart => {
|
|
89
|
+
return change?.type === "start" && "mimeType" in change;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const isStreamChunk = (change: any): change is BinaryStreamChunk => {
|
|
93
|
+
return change?.type === "chunk" && "chunk" in change;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const isStreamEnd = (change: any): change is BinaryStreamEnd => {
|
|
97
|
+
return change?.type === "end";
|
|
98
|
+
};
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { JsonObject, LocalNode, RawCoMap } from "cojson";
|
|
2
|
+
import { PageInfo } from "./types";
|
|
3
|
+
import { GridView } from "./grid-view.js";
|
|
4
|
+
import { useState, useMemo } from "react";
|
|
5
|
+
import { Button, Icon, Input, Modal } from "../ui";
|
|
6
|
+
import { styled } from "goober";
|
|
7
|
+
import { restoreCoMapToTimestamp } from "../utils/history";
|
|
8
|
+
import { CoValueEditor } from "./co-value-editor.js";
|
|
9
|
+
|
|
10
|
+
export function CoMapView({
|
|
11
|
+
coValue,
|
|
12
|
+
data,
|
|
13
|
+
node,
|
|
14
|
+
onNavigate,
|
|
15
|
+
}: {
|
|
16
|
+
coValue: RawCoMap;
|
|
17
|
+
data: JsonObject;
|
|
18
|
+
node: LocalNode;
|
|
19
|
+
onNavigate: (pages: PageInfo[]) => void;
|
|
20
|
+
}) {
|
|
21
|
+
return (
|
|
22
|
+
<>
|
|
23
|
+
<GridView
|
|
24
|
+
data={data}
|
|
25
|
+
onNavigate={onNavigate}
|
|
26
|
+
node={node}
|
|
27
|
+
coValue={coValue}
|
|
28
|
+
/>
|
|
29
|
+
<div>
|
|
30
|
+
<AddPropertyModal coValue={coValue} node={node} />{" "}
|
|
31
|
+
<RestoreSnapshotModal coValue={coValue} />
|
|
32
|
+
</div>
|
|
33
|
+
</>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function AddPropertyModal({
|
|
38
|
+
coValue,
|
|
39
|
+
node,
|
|
40
|
+
}: {
|
|
41
|
+
coValue: RawCoMap;
|
|
42
|
+
node: LocalNode;
|
|
43
|
+
}) {
|
|
44
|
+
const [isAddPropertyModalOpen, setIsAddPropertyModalOpen] = useState(false);
|
|
45
|
+
const [propertyName, setPropertyName] = useState("");
|
|
46
|
+
|
|
47
|
+
const openAddPropertyModal = () => {
|
|
48
|
+
setIsAddPropertyModalOpen(true);
|
|
49
|
+
setPropertyName("");
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const handleCancel = () => {
|
|
53
|
+
setIsAddPropertyModalOpen(false);
|
|
54
|
+
setPropertyName("");
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<>
|
|
59
|
+
<Button
|
|
60
|
+
title="Add Property"
|
|
61
|
+
variant="secondary"
|
|
62
|
+
onClick={openAddPropertyModal}
|
|
63
|
+
>
|
|
64
|
+
<Icon name="edit" />
|
|
65
|
+
</Button>
|
|
66
|
+
|
|
67
|
+
<Modal
|
|
68
|
+
isOpen={isAddPropertyModalOpen}
|
|
69
|
+
onClose={handleCancel}
|
|
70
|
+
heading="Add Property"
|
|
71
|
+
showButtons={false}
|
|
72
|
+
>
|
|
73
|
+
<Input
|
|
74
|
+
label="Property Name"
|
|
75
|
+
value={propertyName}
|
|
76
|
+
onChange={(e) => setPropertyName(e.target.value)}
|
|
77
|
+
placeholder="Enter property name"
|
|
78
|
+
/>
|
|
79
|
+
{propertyName && (
|
|
80
|
+
<EditorContainer>
|
|
81
|
+
<CoValueEditor
|
|
82
|
+
node={node}
|
|
83
|
+
property={propertyName}
|
|
84
|
+
value={undefined}
|
|
85
|
+
coValue={coValue}
|
|
86
|
+
onCancel={handleCancel}
|
|
87
|
+
/>
|
|
88
|
+
</EditorContainer>
|
|
89
|
+
)}
|
|
90
|
+
</Modal>
|
|
91
|
+
</>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function RestoreSnapshotModal({ coValue }: { coValue: RawCoMap }) {
|
|
96
|
+
const [isRestoreModalOpen, setIsRestoreModalOpen] = useState(false);
|
|
97
|
+
const [selectedIndex, setSelectedIndex] = useState<number>(-1);
|
|
98
|
+
const [removeUnknownProperties, setRemoveUnknownProperties] = useState(false);
|
|
99
|
+
|
|
100
|
+
const timestamps = useMemo(
|
|
101
|
+
() => coValue.core.verifiedTransactions.map((tx) => tx.madeAt),
|
|
102
|
+
[coValue.core.verifiedTransactions.length],
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const coMapAtSelectedIndex = useMemo(() => {
|
|
106
|
+
if (selectedIndex === -1) return null;
|
|
107
|
+
return coValue.atTime(timestamps[selectedIndex]!).toJSON() as JsonObject;
|
|
108
|
+
}, [coValue, timestamps, selectedIndex]);
|
|
109
|
+
|
|
110
|
+
const openRestoreModal = () => {
|
|
111
|
+
setIsRestoreModalOpen(true);
|
|
112
|
+
setSelectedIndex(timestamps.length - 1);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const handleRestore = () => {
|
|
116
|
+
if (timestamps.length < 2) return;
|
|
117
|
+
if (timestamps.length === 0) return;
|
|
118
|
+
|
|
119
|
+
const selectedTimestamp = timestamps[selectedIndex];
|
|
120
|
+
if (selectedTimestamp === undefined) return;
|
|
121
|
+
|
|
122
|
+
restoreCoMapToTimestamp(
|
|
123
|
+
coValue,
|
|
124
|
+
selectedTimestamp,
|
|
125
|
+
removeUnknownProperties,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
setIsRestoreModalOpen(false);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const handleClose = () => {
|
|
132
|
+
setIsRestoreModalOpen(false);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<>
|
|
137
|
+
<Button title="Timeline" variant="secondary" onClick={openRestoreModal}>
|
|
138
|
+
<Icon name="history" />
|
|
139
|
+
</Button>
|
|
140
|
+
|
|
141
|
+
<Modal
|
|
142
|
+
isOpen={isRestoreModalOpen}
|
|
143
|
+
onClose={handleClose}
|
|
144
|
+
heading="Timeline"
|
|
145
|
+
confirmText="Restore"
|
|
146
|
+
cancelText="Cancel"
|
|
147
|
+
onConfirm={handleRestore}
|
|
148
|
+
onCancel={handleClose}
|
|
149
|
+
showButtons={timestamps.length > 1}
|
|
150
|
+
>
|
|
151
|
+
{timestamps.length > 1 && (
|
|
152
|
+
<>
|
|
153
|
+
<RangeContainer>
|
|
154
|
+
<RangeLabel>Select Timestamp</RangeLabel>
|
|
155
|
+
<RangeInput
|
|
156
|
+
type="range"
|
|
157
|
+
min={0}
|
|
158
|
+
max={Math.max(0, timestamps.length - 1)}
|
|
159
|
+
value={selectedIndex}
|
|
160
|
+
onChange={(e) => setSelectedIndex(Number(e.target.value))}
|
|
161
|
+
disabled={timestamps.length === 0}
|
|
162
|
+
/>
|
|
163
|
+
<TimestampDisplay>
|
|
164
|
+
{timestamps[selectedIndex] !== undefined
|
|
165
|
+
? new Date(timestamps[selectedIndex]!).toISOString()
|
|
166
|
+
: "No timestamps available"}
|
|
167
|
+
</TimestampDisplay>
|
|
168
|
+
</RangeContainer>
|
|
169
|
+
|
|
170
|
+
<CheckboxContainer>
|
|
171
|
+
<CheckboxInput
|
|
172
|
+
type="checkbox"
|
|
173
|
+
id="remove-unknown-properties"
|
|
174
|
+
checked={removeUnknownProperties}
|
|
175
|
+
onChange={(e) => setRemoveUnknownProperties(e.target.checked)}
|
|
176
|
+
/>
|
|
177
|
+
<CheckboxLabel htmlFor="remove-unknown-properties">
|
|
178
|
+
Remove unknown properties (properties that don't exist in the
|
|
179
|
+
selected snapshot)
|
|
180
|
+
</CheckboxLabel>
|
|
181
|
+
</CheckboxContainer>
|
|
182
|
+
</>
|
|
183
|
+
)}
|
|
184
|
+
|
|
185
|
+
{timestamps.length > 0 && timestamps[selectedIndex] !== undefined && (
|
|
186
|
+
<PreviewSection>
|
|
187
|
+
<PreviewLabel>State at that time:</PreviewLabel>
|
|
188
|
+
<PreviewPre>
|
|
189
|
+
{JSON.stringify(coMapAtSelectedIndex, null, 2)}
|
|
190
|
+
</PreviewPre>
|
|
191
|
+
</PreviewSection>
|
|
192
|
+
)}
|
|
193
|
+
|
|
194
|
+
{timestamps.length < 2 && (
|
|
195
|
+
<div style={{ color: "var(--j-text-color)" }}>
|
|
196
|
+
At least 2 timestamps are required to restore a snapshot.
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
</Modal>
|
|
200
|
+
</>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const PreviewSection = styled("div")`
|
|
205
|
+
margin-top: 1.5rem;
|
|
206
|
+
`;
|
|
207
|
+
|
|
208
|
+
const PreviewLabel = styled("div")`
|
|
209
|
+
font-weight: 500;
|
|
210
|
+
margin-bottom: 0.5rem;
|
|
211
|
+
color: var(--j-text-color-strong);
|
|
212
|
+
`;
|
|
213
|
+
|
|
214
|
+
const PreviewPre = styled("pre")`
|
|
215
|
+
background-color: var(--j-foreground);
|
|
216
|
+
border: 1px solid var(--j-border-color);
|
|
217
|
+
border-radius: var(--j-radius-md);
|
|
218
|
+
padding: 1rem;
|
|
219
|
+
overflow-x: auto;
|
|
220
|
+
font-size: 0.875rem;
|
|
221
|
+
max-height: 400px;
|
|
222
|
+
overflow-y: auto;
|
|
223
|
+
color: var(--j-text-color);
|
|
224
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
225
|
+
`;
|
|
226
|
+
|
|
227
|
+
const RangeContainer = styled("div")`
|
|
228
|
+
display: flex;
|
|
229
|
+
flex-direction: column;
|
|
230
|
+
gap: 0.75rem;
|
|
231
|
+
`;
|
|
232
|
+
|
|
233
|
+
const RangeLabel = styled("label")`
|
|
234
|
+
font-weight: 500;
|
|
235
|
+
color: var(--j-text-color-strong);
|
|
236
|
+
font-size: 0.875rem;
|
|
237
|
+
`;
|
|
238
|
+
|
|
239
|
+
const RangeInput = styled("input")`
|
|
240
|
+
width: 100%;
|
|
241
|
+
height: 0.5rem;
|
|
242
|
+
border-radius: var(--j-radius-sm);
|
|
243
|
+
outline: none;
|
|
244
|
+
-webkit-appearance: none;
|
|
245
|
+
appearance: none;
|
|
246
|
+
background: var(--j-foreground);
|
|
247
|
+
cursor: pointer;
|
|
248
|
+
|
|
249
|
+
&::-webkit-slider-thumb {
|
|
250
|
+
-webkit-appearance: none;
|
|
251
|
+
appearance: none;
|
|
252
|
+
width: 1.25rem;
|
|
253
|
+
height: 1.25rem;
|
|
254
|
+
border-radius: 50%;
|
|
255
|
+
background: var(--j-primary-color);
|
|
256
|
+
cursor: pointer;
|
|
257
|
+
border: 2px solid var(--j-background);
|
|
258
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
&::-moz-range-thumb {
|
|
262
|
+
width: 1.25rem;
|
|
263
|
+
height: 1.25rem;
|
|
264
|
+
border-radius: 50%;
|
|
265
|
+
background: var(--j-primary-color);
|
|
266
|
+
cursor: pointer;
|
|
267
|
+
border: 2px solid var(--j-background);
|
|
268
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
&:disabled {
|
|
272
|
+
opacity: 0.5;
|
|
273
|
+
cursor: not-allowed;
|
|
274
|
+
}
|
|
275
|
+
`;
|
|
276
|
+
|
|
277
|
+
const TimestampDisplay = styled("div")`
|
|
278
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
279
|
+
font-size: 0.875rem;
|
|
280
|
+
color: var(--j-text-color);
|
|
281
|
+
padding: 0.5rem;
|
|
282
|
+
background-color: var(--j-foreground);
|
|
283
|
+
border: 1px solid var(--j-border-color);
|
|
284
|
+
border-radius: var(--j-radius-md);
|
|
285
|
+
text-align: center;
|
|
286
|
+
`;
|
|
287
|
+
|
|
288
|
+
const CheckboxContainer = styled("div")`
|
|
289
|
+
display: flex;
|
|
290
|
+
align-items: flex-start;
|
|
291
|
+
gap: 0.5rem;
|
|
292
|
+
margin-top: 1rem;
|
|
293
|
+
`;
|
|
294
|
+
|
|
295
|
+
const CheckboxInput = styled("input")`
|
|
296
|
+
width: 1rem;
|
|
297
|
+
height: 1rem;
|
|
298
|
+
margin-top: 0.125rem;
|
|
299
|
+
cursor: pointer;
|
|
300
|
+
accent-color: var(--j-primary-color);
|
|
301
|
+
`;
|
|
302
|
+
|
|
303
|
+
const CheckboxLabel = styled("label")`
|
|
304
|
+
font-size: 0.875rem;
|
|
305
|
+
color: var(--j-text-color);
|
|
306
|
+
cursor: pointer;
|
|
307
|
+
line-height: 1.25rem;
|
|
308
|
+
`;
|
|
309
|
+
|
|
310
|
+
const EditorContainer = styled("div")`
|
|
311
|
+
margin-top: 1rem;
|
|
312
|
+
`;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { LocalNode, RawCoValue } from "cojson";
|
|
2
|
+
import { JsonValue } from "cojson";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { styled } from "goober";
|
|
5
|
+
import { Button } from "../ui/button.js";
|
|
6
|
+
import { Select } from "../ui/select.js";
|
|
7
|
+
|
|
8
|
+
type ValueType =
|
|
9
|
+
| "number"
|
|
10
|
+
| "string"
|
|
11
|
+
| "true"
|
|
12
|
+
| "false"
|
|
13
|
+
| "object"
|
|
14
|
+
| "null"
|
|
15
|
+
| "undefined";
|
|
16
|
+
|
|
17
|
+
export function CoValueEditor({
|
|
18
|
+
node,
|
|
19
|
+
property,
|
|
20
|
+
value,
|
|
21
|
+
coValue,
|
|
22
|
+
onCancel,
|
|
23
|
+
}: {
|
|
24
|
+
node: LocalNode;
|
|
25
|
+
property: string;
|
|
26
|
+
value: JsonValue | undefined;
|
|
27
|
+
coValue: RawCoValue;
|
|
28
|
+
onCancel: () => void;
|
|
29
|
+
}) {
|
|
30
|
+
const getInitialType = (): ValueType => {
|
|
31
|
+
if (value === null) return "null";
|
|
32
|
+
if (value === undefined) return "undefined";
|
|
33
|
+
if (typeof value === "number") return "number";
|
|
34
|
+
if (typeof value === "string") return "string";
|
|
35
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
36
|
+
if (typeof value === "object") return "object";
|
|
37
|
+
return "undefined";
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const [selectedType, setSelectedType] = useState<ValueType>(getInitialType());
|
|
41
|
+
const [editValue, setEditValue] = useState(
|
|
42
|
+
value === undefined || value === null
|
|
43
|
+
? ""
|
|
44
|
+
: typeof value === "object"
|
|
45
|
+
? JSON.stringify(value, null, 2)
|
|
46
|
+
: String(value),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
e.stopPropagation();
|
|
52
|
+
|
|
53
|
+
let newValue;
|
|
54
|
+
switch (selectedType) {
|
|
55
|
+
case "null":
|
|
56
|
+
newValue = null;
|
|
57
|
+
break;
|
|
58
|
+
case "undefined":
|
|
59
|
+
newValue = undefined;
|
|
60
|
+
break;
|
|
61
|
+
case "true":
|
|
62
|
+
newValue = true;
|
|
63
|
+
break;
|
|
64
|
+
case "false":
|
|
65
|
+
newValue = false;
|
|
66
|
+
break;
|
|
67
|
+
case "number":
|
|
68
|
+
newValue = parseFloat(editValue);
|
|
69
|
+
break;
|
|
70
|
+
case "string":
|
|
71
|
+
newValue = editValue;
|
|
72
|
+
break;
|
|
73
|
+
case "object":
|
|
74
|
+
newValue = JSON.parse(editValue);
|
|
75
|
+
break;
|
|
76
|
+
default:
|
|
77
|
+
throw new Error(`Invalid type: ${selectedType}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
coValue.core.makeTransaction(
|
|
81
|
+
[
|
|
82
|
+
{
|
|
83
|
+
op: "set",
|
|
84
|
+
key: property,
|
|
85
|
+
value: newValue,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
"private",
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
onCancel();
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const showTextarea =
|
|
95
|
+
selectedType === "number" ||
|
|
96
|
+
selectedType === "string" ||
|
|
97
|
+
selectedType === "object";
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<EditForm onSubmit={handleSubmit}>
|
|
101
|
+
<Select
|
|
102
|
+
label="Type"
|
|
103
|
+
value={selectedType}
|
|
104
|
+
onChange={(e) => {
|
|
105
|
+
setSelectedType(e.target.value as ValueType);
|
|
106
|
+
}}
|
|
107
|
+
onClick={(e) => e.stopPropagation()}
|
|
108
|
+
>
|
|
109
|
+
<option value="number">number</option>
|
|
110
|
+
<option value="string">string</option>
|
|
111
|
+
<option value="true">true</option>
|
|
112
|
+
<option value="false">false</option>
|
|
113
|
+
<option value="object">object</option>
|
|
114
|
+
<option value="null">null</option>
|
|
115
|
+
<option value="undefined">undefined</option>
|
|
116
|
+
</Select>
|
|
117
|
+
{showTextarea && (
|
|
118
|
+
<StyledTextarea
|
|
119
|
+
value={editValue}
|
|
120
|
+
onChange={(e) => setEditValue(e.target.value)}
|
|
121
|
+
onClick={(e) => e.stopPropagation()}
|
|
122
|
+
/>
|
|
123
|
+
)}
|
|
124
|
+
<FormActions>
|
|
125
|
+
<Button type="button" variant="secondary" onClick={onCancel}>
|
|
126
|
+
Cancel
|
|
127
|
+
</Button>
|
|
128
|
+
<Button type="submit" variant="primary">
|
|
129
|
+
Submit
|
|
130
|
+
</Button>
|
|
131
|
+
</FormActions>
|
|
132
|
+
</EditForm>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const EditForm = styled("form")`
|
|
137
|
+
display: flex;
|
|
138
|
+
flex-direction: column;
|
|
139
|
+
gap: 0.75rem;
|
|
140
|
+
`;
|
|
141
|
+
|
|
142
|
+
const StyledTextarea = styled("textarea")`
|
|
143
|
+
width: 100%;
|
|
144
|
+
min-height: 120px;
|
|
145
|
+
border-radius: var(--j-radius-md);
|
|
146
|
+
border: 1px solid var(--j-border-color);
|
|
147
|
+
padding: 0.5rem 0.875rem;
|
|
148
|
+
box-shadow: var(--j-shadow-sm);
|
|
149
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
150
|
+
font-size: 0.875rem;
|
|
151
|
+
background-color: white;
|
|
152
|
+
color: var(--j-text-color-strong);
|
|
153
|
+
resize: vertical;
|
|
154
|
+
|
|
155
|
+
@media (prefers-color-scheme: dark) {
|
|
156
|
+
background-color: var(--j-foreground);
|
|
157
|
+
}
|
|
158
|
+
`;
|
|
159
|
+
|
|
160
|
+
const FormActions = styled("div")`
|
|
161
|
+
display: flex;
|
|
162
|
+
gap: 0.5rem;
|
|
163
|
+
justify-content: flex-end;
|
|
164
|
+
`;
|