@instantdb/components 1.0.37 → 1.0.38

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@instantdb/components",
3
3
  "private": false,
4
- "version": "1.0.37",
4
+ "version": "1.0.38",
5
5
  "type": "module",
6
6
  "description": "Instant's UI components",
7
7
  "license": "Apache-2.0",
@@ -94,11 +94,11 @@
94
94
  "swr": "^2.2.4",
95
95
  "tailwind-merge": "^2.2.1",
96
96
  "uuid": "^11.1.0",
97
- "@instantdb/admin": "1.0.37",
98
- "@instantdb/core": "1.0.37",
99
- "@instantdb/platform": "1.0.37",
100
- "@instantdb/react": "1.0.37",
101
- "@instantdb/version": "1.0.37"
97
+ "@instantdb/admin": "1.0.38",
98
+ "@instantdb/platform": "1.0.38",
99
+ "@instantdb/core": "1.0.38",
100
+ "@instantdb/version": "1.0.38",
101
+ "@instantdb/react": "1.0.38"
102
102
  },
103
103
  "scripts": {
104
104
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -52,7 +52,7 @@ import {
52
52
  jobIsCompleted,
53
53
  jobIsErrored,
54
54
  } from '@lib/utils/indexingJobs';
55
- import { useExplorerProps, useExplorerState } from './index';
55
+ import { EditSchemaScreen, useExplorerProps, useExplorerState } from './index';
56
56
  import { useClose } from '@headlessui/react';
57
57
  import {
58
58
  PendingJob,
@@ -69,6 +69,8 @@ export function EditNamespaceDialog({
69
69
  namespaces,
70
70
  onClose,
71
71
  isSystemCatalogNs,
72
+ screen,
73
+ onScreenChange,
72
74
  }: {
73
75
  db: InstantReactWebDatabase<any>;
74
76
  namespace: SchemaNamespace;
@@ -76,18 +78,20 @@ export function EditNamespaceDialog({
76
78
  onClose: (p?: { ok: boolean }) => void;
77
79
  readOnly: boolean;
78
80
  isSystemCatalogNs: boolean;
81
+ screen: EditSchemaScreen;
82
+ onScreenChange: (screen: EditSchemaScreen) => void;
79
83
  }) {
80
84
  const props = useExplorerProps();
81
85
  const appId = props.appId;
82
86
  const { history, explorerState } = useExplorerState();
83
87
  const { mutate } = useSWRConfig();
84
- const [screen, setScreen] = useState<
85
- | { type: 'main' }
86
- | { type: 'delete' }
87
- | { type: 'rename' }
88
- | { type: 'add' }
89
- | { type: 'edit'; attrId: string; isForward: boolean }
90
- >({ type: 'main' });
88
+
89
+ const [isDeleting, setIsDeleting] = useState(false);
90
+
91
+ const setScreen = (s: EditSchemaScreen) => {
92
+ setIsDeleting(false);
93
+ onScreenChange(s);
94
+ };
91
95
 
92
96
  const [renameNsInput, setRenameNsInput] = useState(namespace.name);
93
97
  const [renameNsErrorText, setRenameNsErrorText] = useState<string | null>(
@@ -121,189 +125,183 @@ export function EditNamespaceDialog({
121
125
  );
122
126
  successToast('Renamed namespace to ' + newName);
123
127
  setRenameNsInput('');
124
- setScreen({ type: 'main' });
128
+ setScreen({ kind: 'main' });
125
129
  }
126
130
 
127
131
  const notes = useAttrNotes();
128
132
 
129
133
  const screenAttr = useMemo(() => {
130
- return (
131
- screen.type === 'edit' &&
132
- namespace.attrs.find(
133
- (a) => a.id === screen.attrId && a.isForward === screen.isForward,
134
- )
134
+ if (screen.kind !== 'edit-attr') return undefined;
135
+ return namespace.attrs.find(
136
+ (a) => a.id === screen.attrId && a.isForward === screen.isForward,
135
137
  );
136
138
  }, [
137
- screen.type === 'edit' ? screen.attrId : null,
138
- screen.type === 'edit' ? screen.isForward : null,
139
+ screen.kind === 'edit-attr' ? screen.attrId : null,
140
+ screen.kind === 'edit-attr' ? screen.isForward : null,
139
141
  namespace.attrs,
140
142
  ]);
141
143
 
142
- return (
143
- <>
144
- {screen.type === 'rename' && (
145
- <div className="px-2">
146
- <button
147
- onClick={() => {
148
- setScreen({
149
- type: 'main',
150
- });
151
- }}
152
- className="mb-3"
153
- >
154
- <ArrowLeftIcon className="h-4 w-4 cursor-pointer" />
155
- </button>
156
- <h6 className="text-md pb-2 font-bold">Rename {namespace.name}</h6>
157
- <form
158
- onSubmit={(e) => {
159
- e.preventDefault();
160
- renameNs(renameNsInput);
161
- }}
144
+ return isDeleting ? (
145
+ <DeleteForm
146
+ name={namespace.name}
147
+ type="namespace"
148
+ onClose={onClose}
149
+ onConfirm={deleteNs}
150
+ />
151
+ ) : screen.kind === 'rename' ? (
152
+ <div className="px-2">
153
+ <button
154
+ onClick={() => {
155
+ setScreen({ kind: 'main' });
156
+ }}
157
+ className="mb-3"
158
+ >
159
+ <ArrowLeftIcon className="h-4 w-4 cursor-pointer" />
160
+ </button>
161
+ <h6 className="text-md pb-2 font-bold">Rename {namespace.name}</h6>
162
+ <form
163
+ onSubmit={(e) => {
164
+ e.preventDefault();
165
+ renameNs(renameNsInput);
166
+ }}
167
+ >
168
+ <Content className="pb-2 text-sm">
169
+ This will immediately rename the namespace. You'll need to{' '}
170
+ <strong className="dark:text-white">update your code</strong> to the
171
+ new name.
172
+ </Content>
173
+ <TextInput
174
+ disabled={isSystemCatalogNs}
175
+ value={renameNsInput}
176
+ onChange={(n) => setRenameNsInput(n)}
177
+ />
178
+ <div className="flex flex-col gap-2 rounded-sm py-2">
179
+ <Button
180
+ type="submit"
181
+ disabled={
182
+ renameNsInput.startsWith('$') || renameNsInput.length === 0
183
+ }
162
184
  >
163
- <Content className="pb-2 text-sm">
164
- This will immediately rename the namespace. You'll need to{' '}
165
- <strong className="dark:text-white">update your code</strong> to
166
- the new name.
167
- </Content>
168
- <TextInput
169
- disabled={isSystemCatalogNs}
170
- value={renameNsInput}
171
- onChange={(n) => setRenameNsInput(n)}
172
- />
173
- <div className="flex flex-col gap-2 rounded-sm py-2">
174
- <Button
175
- type="submit"
176
- disabled={
177
- renameNsInput.startsWith('$') || renameNsInput.length === 0
178
- }
179
- >
180
- Rename {namespace.name} → {renameNsInput}
181
- </Button>
182
- </div>
183
- </form>{' '}
185
+ Rename {namespace.name} → {renameNsInput}
186
+ </Button>
184
187
  </div>
185
- )}
188
+ </form>
189
+ </div>
190
+ ) : screen.kind === 'main' ? (
191
+ <div className="flex flex-col gap-4 px-2">
192
+ <div className="mr-8 flex gap-1">
193
+ <h5 className="flex items-center text-lg font-bold">
194
+ {namespace.name}
195
+ </h5>
196
+ <IconButton
197
+ variant="subtle"
198
+ onClick={() => {
199
+ setScreen({ kind: 'rename' });
200
+ }}
201
+ icon={
202
+ <PencilSquareIcon className="h-4 w-4 opacity-50"></PencilSquareIcon>
203
+ }
204
+ label="Rename"
205
+ ></IconButton>
186
206
 
187
- {screen.type === 'main' ? (
188
- <div className="flex flex-col gap-4 px-2">
189
- <div className="mr-8 flex gap-1">
190
- <h5 className="flex items-center text-lg font-bold">
191
- {namespace.name}
192
- </h5>
193
- <IconButton
194
- variant="subtle"
195
- onClick={() => {
196
- setScreen({ type: 'rename' });
197
- }}
198
- icon={
199
- <PencilSquareIcon className="h-4 w-4 opacity-50"></PencilSquareIcon>
200
- }
201
- label="Rename"
202
- ></IconButton>
203
-
204
- <Button
205
- className="ml-4"
206
- disabled={isSystemCatalogNs}
207
- title={
208
- isSystemCatalogNs
209
- ? `The ${namespace.name} namespace can't be deleted.`
210
- : undefined
211
- }
212
- size="mini"
213
- variant="secondary"
214
- onClick={() => setScreen({ type: 'delete' })}
215
- >
216
- <TrashIcon className="inline" height="1rem" />
217
- Delete
218
- </Button>
219
- </div>
207
+ <Button
208
+ className="ml-4"
209
+ disabled={isSystemCatalogNs}
210
+ title={
211
+ isSystemCatalogNs
212
+ ? `The ${namespace.name} namespace can't be deleted.`
213
+ : undefined
214
+ }
215
+ size="mini"
216
+ variant="secondary"
217
+ onClick={() => setIsDeleting(true)}
218
+ >
219
+ <TrashIcon className="inline" height="1rem" />
220
+ Delete
221
+ </Button>
222
+ </div>
220
223
 
221
- <div className="flex flex-col gap-2">
222
- {namespace.attrs.map((attr) => (
223
- <div
224
- key={attr.id + '-' + attr.name}
225
- className="flex justify-between"
224
+ <div className="flex flex-col gap-2">
225
+ {namespace.attrs.map((attr) => (
226
+ <div key={attr.id + '-' + attr.name} className="flex justify-between">
227
+ <div className="flex items-center gap-3">
228
+ <span className="py-0.5 font-bold">{attr.name}</span>
229
+ {notes.notes[attr.id]?.message && (
230
+ <InfoTip>
231
+ <div className="px-2 text-xs text-gray-500 dark:text-neutral-400">
232
+ {notes.notes[attr.id].message}
233
+ </div>
234
+ </InfoTip>
235
+ )}
236
+ </div>
237
+ {attr.name !== 'id' ? (
238
+ <Button
239
+ className="px-2"
240
+ size="mini"
241
+ variant="subtle"
242
+ onClick={() => {
243
+ notes.removeNote(attr.id);
244
+ setScreen({
245
+ kind: 'edit-attr',
246
+ attrId: attr.id,
247
+ isForward: attr.isForward,
248
+ });
249
+ }}
226
250
  >
227
- <div className="flex items-center gap-3">
228
- <span className="py-0.5 font-bold">{attr.name}</span>
229
- {notes.notes[attr.id]?.message && (
230
- <InfoTip>
231
- <div className="px-2 text-xs text-gray-500 dark:text-neutral-400">
232
- {notes.notes[attr.id].message}
233
- </div>
234
- </InfoTip>
235
- )}
236
- </div>
237
- {attr.name !== 'id' ? (
238
- <Button
239
- className="px-2"
240
- size="mini"
241
- variant="subtle"
242
- onClick={() => {
243
- notes.removeNote(attr.id);
244
- setScreen({
245
- type: 'edit',
246
- attrId: attr.id,
247
- isForward: attr.isForward,
248
- });
249
- }}
250
- >
251
- Edit
252
- </Button>
253
- ) : null}
254
- </div>
255
- ))}
251
+ Edit
252
+ </Button>
253
+ ) : null}
256
254
  </div>
255
+ ))}
256
+ </div>
257
257
 
258
- <div>
259
- <Button
260
- size="mini"
261
- variant="secondary"
262
- onClick={() => setScreen({ type: 'add' })}
263
- >
264
- <PlusIcon className="inline" height="12px" />
265
- New attribute
266
- </Button>
267
- </div>
268
- <RecentlyDeletedAttrs
269
- notes={notes}
270
- db={db}
271
- appId={appId}
272
- namespace={namespace}
273
- />
274
- </div>
275
- ) : screen.type === 'add' ? (
276
- <AddAttrForm
277
- db={db}
278
- namespace={namespace}
279
- namespaces={namespaces}
280
- onClose={() => setScreen({ type: 'main' })}
281
- constraints={getSystemConstraints({
282
- namespaceName: namespace.name,
283
- isSystemCatalogNs,
284
- })}
285
- />
286
- ) : screen.type === 'delete' ? (
287
- <DeleteForm
288
- name={namespace.name}
289
- type="namespace"
290
- onClose={onClose}
291
- onConfirm={deleteNs}
292
- />
293
- ) : screen.type === 'edit' && screenAttr ? (
294
- <EditAttrForm
295
- db={db}
296
- attr={screenAttr}
297
- onClose={() => setScreen({ type: 'main' })}
298
- constraints={getSystemConstraints({
299
- namespaceName: namespace.name,
300
- isSystemCatalogNs: isSystemCatalogNs,
301
- attr: screenAttr,
302
- })}
303
- />
304
- ) : null}
305
- </>
306
- );
258
+ <div>
259
+ <Button
260
+ size="mini"
261
+ variant="secondary"
262
+ onClick={() => setScreen({ kind: 'add-attr', attrKind: 'data' })}
263
+ >
264
+ <PlusIcon className="inline" height="12px" />
265
+ New attribute
266
+ </Button>
267
+ </div>
268
+ <RecentlyDeletedAttrs
269
+ notes={notes}
270
+ db={db}
271
+ appId={appId}
272
+ namespace={namespace}
273
+ />
274
+ </div>
275
+ ) : screen.kind === 'add-attr' ? (
276
+ <AddAttrForm
277
+ db={db}
278
+ namespace={namespace}
279
+ namespaces={namespaces}
280
+ onClose={() => setScreen({ kind: 'main' })}
281
+ constraints={getSystemConstraints({
282
+ namespaceName: namespace.name,
283
+ isSystemCatalogNs,
284
+ })}
285
+ attrType={screen.attrKind === 'link' ? 'ref' : 'blob'}
286
+ onAttrTypeChange={(t) =>
287
+ onScreenChange({
288
+ kind: 'add-attr',
289
+ attrKind: t === 'ref' ? 'link' : 'data',
290
+ })
291
+ }
292
+ />
293
+ ) : screen.kind === 'edit-attr' && screenAttr ? (
294
+ <EditAttrForm
295
+ db={db}
296
+ attr={screenAttr}
297
+ onClose={() => setScreen({ kind: 'main' })}
298
+ constraints={getSystemConstraints({
299
+ namespaceName: namespace.name,
300
+ isSystemCatalogNs: isSystemCatalogNs,
301
+ attr: screenAttr,
302
+ })}
303
+ />
304
+ ) : null;
307
305
  }
308
306
 
309
307
  function DeleteForm({
@@ -350,12 +348,16 @@ function AddAttrForm({
350
348
  namespaces,
351
349
  onClose,
352
350
  constraints,
351
+ attrType,
352
+ onAttrTypeChange,
353
353
  }: {
354
354
  db: InstantReactWebDatabase<any>;
355
355
  namespace: SchemaNamespace;
356
356
  namespaces: SchemaNamespace[];
357
357
  onClose: () => void;
358
358
  constraints: SystemConstraints;
359
+ attrType: 'blob' | 'ref';
360
+ onAttrTypeChange: (attrType: 'blob' | 'ref') => void;
359
361
  }) {
360
362
  const [isRequired, setIsRequired] = useState(false);
361
363
  const [isIndex, setIsIndex] = useState(false);
@@ -364,7 +366,6 @@ function AddAttrForm({
364
366
  const [isCascadeReverse, setIsCascadeReverse] = useState(false);
365
367
  const [checkedDataType, setCheckedDataType] =
366
368
  useState<CheckedDataType | null>(null);
367
- const [attrType, setAttrType] = useState<'blob' | 'ref'>('blob');
368
369
  const [relationship, setRelationship] =
369
370
  useState<RelationshipKinds>('many-many');
370
371
 
@@ -455,7 +456,7 @@ function AddAttrForm({
455
456
  { id: 'blob', label: 'Data' },
456
457
  { id: 'ref', label: 'Link' },
457
458
  ]}
458
- onChange={(item) => setAttrType(item.id as 'blob' | 'ref')}
459
+ onChange={(item) => onAttrTypeChange(item.id as 'blob' | 'ref')}
459
460
  />
460
461
  </div>
461
462
  {attrType === 'blob' ? (
@@ -1,7 +1,7 @@
1
1
  import React, { useEffect, useRef, useState } from 'react';
2
- import { useExplorerProps } from '.';
2
+ import { useExplorerDialog, useExplorerProps } from '.';
3
3
  import { SchemaNamespace } from '@lib/types';
4
- import { Button, cn, Dialog, ToggleCollection, useDialog } from '../ui';
4
+ import { Button, cn, Dialog, ToggleCollection } from '../ui';
5
5
  import {
6
6
  RecentlyDeletedNamespaces,
7
7
  useRecentlyDeletedNamespaces,
@@ -28,9 +28,19 @@ export const ExplorerLayout = ({
28
28
  appId: string;
29
29
  }) => {
30
30
  const props = useExplorerProps();
31
+ const { dialog, setDialog } = useExplorerDialog();
31
32
 
32
- const recentlyDeletedNsDialog = useDialog();
33
- const newNsDialog = useDialog();
33
+ const recentlyDeletedNsDialog = {
34
+ open: dialog?.type === 'recently-deleted-ns',
35
+ onOpen: () => setDialog({ type: 'recently-deleted-ns' }),
36
+ onClose: () => setDialog(null),
37
+ };
38
+
39
+ const newNsDialog = {
40
+ open: dialog?.type === 'new-namespace',
41
+ onOpen: () => setDialog({ type: 'new-namespace' }),
42
+ onClose: () => setDialog(null),
43
+ };
34
44
 
35
45
  const selectedNamespace = namespaces.find(
36
46
  (ns) => ns.id === props.explorerState?.namespace,
@@ -58,20 +68,24 @@ export const ExplorerLayout = ({
58
68
  );
59
69
 
60
70
  // Auto-select first namespace if none selected
71
+ const { setExplorerState } = props;
61
72
  useEffect(() => {
62
73
  if (!selectedNamespace && namespaces.length > 0) {
63
- if (recentExplorerNamespaceId) {
64
- const savedNamespace = namespaces.find(
65
- (ns) => ns.id === recentExplorerNamespaceId,
66
- );
67
- if (savedNamespace) {
68
- props.setExplorerState({ namespace: savedNamespace.id });
69
- }
70
- } else {
71
- props.setExplorerState({ namespace: namespaces[0].id });
72
- }
74
+ const savedNamespace = recentExplorerNamespaceId
75
+ ? namespaces.find((ns) => ns.id === recentExplorerNamespaceId)
76
+ : undefined;
77
+ const namespaceId = savedNamespace?.id ?? namespaces[0].id;
78
+ setExplorerState((prev) => ({
79
+ ...(prev ?? {}),
80
+ namespace: namespaceId,
81
+ }));
73
82
  }
74
- }, [selectedNamespace, namespaces, props]);
83
+ }, [
84
+ selectedNamespace,
85
+ namespaces,
86
+ recentExplorerNamespaceId,
87
+ setExplorerState,
88
+ ]);
75
89
 
76
90
  const deletedNamespaces = useRecentlyDeletedNamespaces(props.appId);
77
91
 
@@ -90,7 +104,6 @@ export const ExplorerLayout = ({
90
104
  db={db}
91
105
  onClose={(p) => {
92
106
  newNsDialog.onClose();
93
-
94
107
  if (p?.name) {
95
108
  props.setExplorerState({ namespace: p.name });
96
109
  }
@@ -16,6 +16,17 @@ import { useSchemaQuery } from '@lib/hooks/explorer';
16
16
  import { useStableDB } from '@lib/hooks/useStableDB';
17
17
  import ErrorBoundary from '@lib/components/error-boundary';
18
18
 
19
+ export type SetExplorerStateOptions = {
20
+ // Use 'replace' for transitions that shouldn't add a back-button step,
21
+ // such as switching screens within an already-open dialog. Defaults to 'push'.
22
+ history?: 'push' | 'replace';
23
+ };
24
+
25
+ export type SetExplorerState = (
26
+ action: React.SetStateAction<ExplorerNav | null>,
27
+ options?: SetExplorerStateOptions,
28
+ ) => void;
29
+
19
30
  interface ExplorerProps {
20
31
  appId: string;
21
32
  adminToken: string;
@@ -30,9 +41,7 @@ interface ExplorerProps {
30
41
  // When null: controlled mode with no selection
31
42
  // When ExplorerNav: controlled mode with a selection
32
43
  explorerState: HasDefault<ExplorerNav | null | undefined>;
33
- setExplorerState: HasDefault<
34
- React.Dispatch<React.SetStateAction<ExplorerNav | null>>
35
- >;
44
+ setExplorerState: HasDefault<SetExplorerState>;
36
45
  useShadowDOM: HasDefault<boolean>;
37
46
  }
38
47
 
@@ -68,6 +77,27 @@ export const useExplorerState = () => {
68
77
  return { explorerState: ctx.props.explorerState, history: ctx.history };
69
78
  };
70
79
 
80
+ export const useExplorerDialog = () => {
81
+ const ctx = useContext(ExplorerPropsContext);
82
+ if (!ctx.props) {
83
+ throw new Error(
84
+ 'useExplorerDialog must be used within an Explorer component',
85
+ );
86
+ }
87
+ const props = ctx.props;
88
+ const dialog = props.explorerState?.dialog ?? null;
89
+ const setDialog = useCallback(
90
+ (d: ExplorerDialog | null, options?: SetExplorerStateOptions) => {
91
+ props.setExplorerState(
92
+ (prev) => (prev ? { ...prev, dialog: d } : prev),
93
+ options,
94
+ );
95
+ },
96
+ [props],
97
+ );
98
+ return { dialog, setDialog };
99
+ };
100
+
71
101
  const isControlled = (props: WithOptional<ExplorerProps>): boolean => {
72
102
  // Component is controlled if explorerState prop is explicitly provided
73
103
  // (even if null - that means "no selection" in controlled mode)
@@ -79,7 +109,7 @@ const isControlled = (props: WithOptional<ExplorerProps>): boolean => {
79
109
  const fillPropsWithDefaults = (
80
110
  input: WithOptional<ExplorerProps>,
81
111
  _explorerState: ExplorerNav | null,
82
- setExplorerState: React.Dispatch<React.SetStateAction<ExplorerNav | null>>,
112
+ setExplorerState: SetExplorerState,
83
113
  ): WithDefaults<ExplorerProps> => {
84
114
  const controlled = isControlled(input);
85
115
  return {
@@ -105,6 +135,19 @@ export type SearchFilterOp =
105
135
 
106
136
  export type SearchFilter = [string, SearchFilterOp, any];
107
137
 
138
+ export type EditSchemaScreen =
139
+ | { kind: 'main' }
140
+ | { kind: 'rename' }
141
+ | { kind: 'add-attr'; attrKind: 'data' | 'link' }
142
+ | { kind: 'edit-attr'; attrId: string; isForward: boolean };
143
+
144
+ export type ExplorerDialog =
145
+ | { type: 'add-row' }
146
+ | { type: 'edit-row'; rowId: string }
147
+ | { type: 'edit-schema'; screen: EditSchemaScreen }
148
+ | { type: 'new-namespace' }
149
+ | { type: 'recently-deleted-ns' };
150
+
108
151
  export interface ExplorerNav {
109
152
  namespace: string;
110
153
  where?: [string, any];
@@ -113,6 +156,7 @@ export interface ExplorerNav {
113
156
  filters?: SearchFilter[];
114
157
  limit?: number;
115
158
  page?: number;
159
+ dialog?: ExplorerDialog | null;
116
160
  }
117
161
 
118
162
  export const Explorer = (_props: WithOptional<ExplorerProps>) => {