@vertesia/ui 0.79.3 → 0.79.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/lib/esm/core/components/MenuList.js +2 -5
  2. package/lib/esm/core/components/MenuList.js.map +1 -1
  3. package/lib/esm/core/components/shadcn/dialog.js +16 -2
  4. package/lib/esm/core/components/shadcn/dialog.js.map +1 -1
  5. package/lib/esm/core/components/shadcn/filters/filter/SelectFilter.js +6 -9
  6. package/lib/esm/core/components/shadcn/filters/filter/SelectFilter.js.map +1 -1
  7. package/lib/esm/core/components/shadcn/filters/filterBar.js +1 -1
  8. package/lib/esm/core/components/shadcn/filters/filterBar.js.map +1 -1
  9. package/lib/esm/core/components/shadcn/selectBox.js +1 -1
  10. package/lib/esm/core/components/shadcn/selectBox.js.map +1 -1
  11. package/lib/esm/env/index.js +4 -1
  12. package/lib/esm/env/index.js.map +1 -1
  13. package/lib/esm/features/facets/CollectionsFacetsNav.js +5 -1
  14. package/lib/esm/features/facets/CollectionsFacetsNav.js.map +1 -1
  15. package/lib/esm/features/layout/GenericPageNavHeader.js +5 -2
  16. package/lib/esm/features/layout/GenericPageNavHeader.js.map +1 -1
  17. package/lib/esm/features/store/collections/EditCollectionView.js +1 -1
  18. package/lib/esm/features/store/collections/EditCollectionView.js.map +1 -1
  19. package/lib/esm/features/store/objects/DocumentSearchResults.js +2 -1
  20. package/lib/esm/features/store/objects/DocumentSearchResults.js.map +1 -1
  21. package/lib/esm/router/HistoryNavigator.js +22 -2
  22. package/lib/esm/router/HistoryNavigator.js.map +1 -1
  23. package/lib/esm/shell/login/UserInfo.js +2 -1
  24. package/lib/esm/shell/login/UserInfo.js.map +1 -1
  25. package/lib/esm/shell/login/UserSessionMenu.js +7 -1
  26. package/lib/esm/shell/login/UserSessionMenu.js.map +1 -1
  27. package/lib/esm/widgets/form/Form.js +5 -1
  28. package/lib/esm/widgets/form/Form.js.map +1 -1
  29. package/lib/esm/widgets/schema-editor/ManagedSchema.js +0 -3
  30. package/lib/esm/widgets/schema-editor/ManagedSchema.js.map +1 -1
  31. package/lib/esm/widgets/schema-editor/json-schema4-utils.js +1 -1
  32. package/lib/esm/widgets/schema-editor/json-schema4-utils.js.map +1 -1
  33. package/lib/tsconfig.tsbuildinfo +1 -1
  34. package/lib/types/core/components/shadcn/dialog.d.ts +2 -1
  35. package/lib/types/core/components/shadcn/dialog.d.ts.map +1 -1
  36. package/lib/types/core/components/shadcn/filters/filterBar.d.ts.map +1 -1
  37. package/lib/types/core/components/shadcn/selectBox.d.ts.map +1 -1
  38. package/lib/types/env/index.d.ts +3 -1
  39. package/lib/types/env/index.d.ts.map +1 -1
  40. package/lib/types/features/facets/CollectionsFacetsNav.d.ts.map +1 -1
  41. package/lib/types/features/layout/GenericPageNavHeader.d.ts.map +1 -1
  42. package/lib/types/features/store/objects/DocumentSearchResults.d.ts.map +1 -1
  43. package/lib/types/router/HistoryNavigator.d.ts +3 -0
  44. package/lib/types/router/HistoryNavigator.d.ts.map +1 -1
  45. package/lib/types/shell/login/UserInfo.d.ts.map +1 -1
  46. package/lib/types/shell/login/UserSessionMenu.d.ts.map +1 -1
  47. package/lib/types/widgets/form/Form.d.ts.map +1 -1
  48. package/lib/types/widgets/schema-editor/ManagedSchema.d.ts.map +1 -1
  49. package/lib/vertesia-ui-core.js +1 -1
  50. package/lib/vertesia-ui-core.js.map +1 -1
  51. package/lib/vertesia-ui-env.js +1 -1
  52. package/lib/vertesia-ui-env.js.map +1 -1
  53. package/lib/vertesia-ui-features.js +1 -1
  54. package/lib/vertesia-ui-features.js.map +1 -1
  55. package/lib/vertesia-ui-router.js +1 -1
  56. package/lib/vertesia-ui-router.js.map +1 -1
  57. package/lib/vertesia-ui-shell.js +1 -1
  58. package/lib/vertesia-ui-shell.js.map +1 -1
  59. package/lib/vertesia-ui-widgets.js +1 -1
  60. package/lib/vertesia-ui-widgets.js.map +1 -1
  61. package/package.json +7 -7
  62. package/src/core/components/MenuList.tsx +3 -6
  63. package/src/core/components/shadcn/dialog.tsx +19 -1
  64. package/src/core/components/shadcn/filters/filter/SelectFilter.tsx +31 -31
  65. package/src/core/components/shadcn/filters/filterBar.tsx +1 -0
  66. package/src/core/components/shadcn/selectBox.tsx +1 -0
  67. package/src/env/index.ts +7 -2
  68. package/src/features/facets/CollectionsFacetsNav.tsx +5 -1
  69. package/src/features/layout/GenericPageNavHeader.tsx +5 -2
  70. package/src/features/store/collections/EditCollectionView.tsx +2 -2
  71. package/src/features/store/objects/DocumentSearchResults.tsx +2 -1
  72. package/src/router/HistoryNavigator.ts +30 -2
  73. package/src/shell/login/UserInfo.tsx +2 -0
  74. package/src/shell/login/UserSessionMenu.tsx +12 -1
  75. package/src/widgets/form/Form.tsx +6 -1
  76. package/src/widgets/schema-editor/ManagedSchema.ts +0 -3
  77. package/src/widgets/schema-editor/json-schema4-utils.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vertesia/ui",
3
- "version": "0.79.3",
3
+ "version": "0.79.4",
4
4
  "description": "Vertesia UI components and and hooks",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -50,10 +50,10 @@
50
50
  "lucide-react": "^0.511.0",
51
51
  "monaco-editor": "^0.52.2",
52
52
  "motion": "^12.12.1",
53
- "react": "^19.1.0",
53
+ "react": "^19.1.2",
54
54
  "react-calendar": "^6.0.0",
55
55
  "react-date-picker": "^11.0.0",
56
- "react-dom": "^19.1.0",
56
+ "react-dom": "^19.1.2",
57
57
  "react-error-boundary": "^6.0.0",
58
58
  "react-markdown": "^10.1.0",
59
59
  "react-resizable-panels": "^3.0.6",
@@ -61,9 +61,9 @@
61
61
  "tailwind-merge": "^3.3.0",
62
62
  "ts-md5": "^1.3.1",
63
63
  "unist-util-visit": "^5.0.0",
64
- "@vertesia/client": "0.79.3",
65
- "@vertesia/json": "0.79.3",
66
- "@vertesia/common": "0.79.3"
64
+ "@vertesia/common": "0.79.4",
65
+ "@vertesia/json": "0.79.4",
66
+ "@vertesia/client": "0.79.4"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@eslint/js": "^9.27.0",
@@ -73,7 +73,7 @@
73
73
  "@types/json-schema": "^7.0.15",
74
74
  "@types/lodash-es": "^4.17.12",
75
75
  "@types/node": "^22.15.21",
76
- "@types/react": "^19.1.0",
76
+ "@types/react": "^19.1.2",
77
77
  "@types/react-dom": "^19.1.1",
78
78
  "eslint": "^9.27.0",
79
79
  "eslint-import-resolver-typescript": "^4.4.4",
@@ -7,7 +7,7 @@ interface MenuListProps {
7
7
  }
8
8
  export function MenuList({ className, children }: MenuListProps) {
9
9
  return (
10
- <ul className={`${className} space-y-1 flex flex-col items-start dark:px-2`}>
10
+ <ul className={`${className} space-y-1 flex flex-col items-start`}>
11
11
  {children}
12
12
  </ul>
13
13
  )
@@ -21,11 +21,8 @@ interface MenuListItemProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
21
21
  const MenuListItem = forwardRef<HTMLAnchorElement, MenuListItemProps>(function _MenuListItem(props, ref) {
22
22
  const { current, children, className, href = '#', onClick, ...others } = props;
23
23
  return (
24
- <li className={clsx(className, current ?
25
- 'bg-gray-50 text-indigo-600'
26
- :
27
- 'text-gray-700 dark:dark:text-slate-300 hover:text-indigo-600 hover:bg-gray-50 dark:hover:bg-slate-800 dark:hover:text-slate-50 dark:border dark:border-transparent dark:hover:border-slate-50',
28
- 'w-full rounded-md p-2 pl-3 text-sm leading-6 font-semibold')}>
24
+ <li className={clsx(className, current ? 'bg-muted' : '',
25
+ 'w-full p-2 pl-3 text-sm leading-6 font-semibold hover:bg-muted')}>
29
26
  <a ref={ref} href={href} onClick={(e) => {
30
27
  if (onClick) {
31
28
  e.preventDefault();
@@ -16,6 +16,7 @@ interface ModalProps {
16
16
  className?: string;
17
17
  allowOverflow?: boolean;
18
18
  disableCloseOnClickOutside?: boolean;
19
+ size?: "sm" | "md" | "lg" | "xl";
19
20
  }
20
21
  const ModalContext = createContext<boolean>(false)
21
22
  export function useIsInModal() {
@@ -34,12 +35,27 @@ export function VModal({
34
35
  noCloseButton = false,
35
36
  allowOverflow = false,
36
37
  disableCloseOnClickOutside = false,
38
+ size = "md",
37
39
  }: ModalProps) {
38
40
  const handleOpenChange = (open: boolean) => {
39
41
  if (!open) {
40
42
  onClose();
41
43
  }
42
44
  };
45
+ function getSizeClasses() {
46
+ switch (size) {
47
+ case "sm":
48
+ return "max-w-[20vw]";
49
+ case "md":
50
+ return "max-w-[40vw]";
51
+ case "lg":
52
+ return "max-w-[60vw]";
53
+ case "xl":
54
+ return "max-w-[80vw]";
55
+ default:
56
+ return "max-w-[40vw]";
57
+ }
58
+ }
43
59
 
44
60
  return (
45
61
  <Dialog
@@ -49,6 +65,7 @@ export function VModal({
49
65
  handleOpenChange(open);
50
66
  }
51
67
  }}
68
+
52
69
  >
53
70
  {allowOverflow && <DialogOverlay className="z-50 fixed inset-0 bg-black/80" />}
54
71
  <VisuallyHidden>
@@ -57,7 +74,8 @@ export function VModal({
57
74
  <DialogContent
58
75
  className={cn(
59
76
  "min-h-20 p-4",
60
- "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] border bg-background shadow-lg duration-200 sm:rounded-lg",
77
+ "fixed left-[50%] top-[50%] z-50 grid w-full translate-x-[-50%] translate-y-[-50%] border bg-background shadow-lg duration-200 sm:rounded-lg",
78
+ getSizeClasses(),
61
79
  className
62
80
  )}
63
81
  >
@@ -1,5 +1,5 @@
1
1
  import React, { useState } from "react";
2
- import { CommandItem, CommandEmpty } from "../../command";
2
+ import { CommandItem } from "../../command";
3
3
  import { Button } from "../../button";
4
4
  import { Filter, FilterGroup, FilterGroupOption, FilterOption } from "../types";
5
5
  import { DynamicLabel } from "../DynamicLabel";
@@ -52,12 +52,8 @@ export default function SelectFilter({
52
52
  }
53
53
 
54
54
  const options = getFilteredOptions(selectedView);
55
- const selectedGroup = filterGroups.find(g => g.name === selectedView);
56
-
57
- if (options.length === 0) {
58
- return <CommandEmpty>No matching options</CommandEmpty>;
59
- }
60
55
 
56
+ const selectedGroup = filterGroups.find(g => g.name === selectedView);
61
57
  const groupTitle = selectedGroup?.placeholder || selectedGroup?.name;
62
58
 
63
59
  const handleApply = () => {
@@ -117,35 +113,39 @@ export default function SelectFilter({
117
113
  };
118
114
 
119
115
  return (
120
- <>
121
- <div className="flex items-center p-1.5 text-xs text-muted">
116
+ <div className="flex flex-col h-full">
117
+ <div className="flex items-center p-1.5 text-xs text-muted shrink-0">
122
118
  <span>{groupTitle}</span>
123
119
  </div>
124
- <div className="max-h-50 overflow-y-auto">
125
- {options.map((option: FilterGroupOption) => {
126
- const isSelected = selectedOptions.some(opt => opt.value === option.value);
120
+ <div className="flex-1 overflow-hidden min-h-0">
121
+ <div className="max-h-[200px] overflow-y-auto">
122
+ {options.length > 0 && (
123
+ options.map((option: FilterGroupOption) => {
124
+ const isSelected = selectedOptions.some(opt => opt.value === option.value);
127
125
 
128
- return (
129
- <CommandItem
130
- key={option.value || `option-${Math.random()}`}
131
- className={`group flex gap-2 items-center w-full hover:bg-muted ${selectedGroup?.multiple && isSelected ? 'bg-muted' : ''
132
- }`}
133
- onSelect={() => handleOptionToggle(option)}
134
- >
135
- <DynamicLabel
136
- value={option.value || ''}
137
- labelRenderer={option.labelRenderer || selectedGroup?.labelRenderer}
138
- fallbackLabel={option.label}
139
- />
140
- {selectedGroup?.multiple && isSelected && (
141
- <span className="ml-auto text-xs text-success">✓</span>
142
- )}
143
- </CommandItem>
144
- );
145
- })}
126
+ return (
127
+ <CommandItem
128
+ key={option.value || `option-${Math.random()}`}
129
+ className={`group flex gap-2 items-center w-full hover:bg-muted ${selectedGroup?.multiple && isSelected ? 'bg-muted' : ''
130
+ }`}
131
+ onSelect={() => handleOptionToggle(option)}
132
+ >
133
+ <DynamicLabel
134
+ value={option.value || ''}
135
+ labelRenderer={option.labelRenderer || selectedGroup?.labelRenderer}
136
+ fallbackLabel={option.label}
137
+ />
138
+ {selectedGroup?.multiple && isSelected && (
139
+ <span className="ml-auto text-xs text-success">✓</span>
140
+ )}
141
+ </CommandItem>
142
+ );
143
+ })
144
+ )}
145
+ </div>
146
146
  </div>
147
147
  {selectedGroup?.multiple && (
148
- <div className="p-2 border-t">
148
+ <div className="p-2 border-t shrink-0">
149
149
  <div className="flex gap-2 justify-end">
150
150
  <Button variant="ghost" size="sm" onClick={handleClose}>
151
151
  Cancel
@@ -156,6 +156,6 @@ export default function SelectFilter({
156
156
  </div>
157
157
  </div>
158
158
  )}
159
- </>
159
+ </div>
160
160
  );
161
161
  }
@@ -330,6 +330,7 @@ const FilterBtn = ({ className }: { className?: string }) => {
330
330
  )
331
331
  }
332
332
  <CommandList>
333
+ <CommandEmpty>No matching filters</CommandEmpty>
333
334
  <CommandGroup>
334
335
  {!selectedView ? getAvailableFilterGroups() : renderFilterOptions()}
335
336
  </CommandGroup>
@@ -275,6 +275,7 @@ export function VSelectBox<T = any>({ options, optionLabel, value, onChange, add
275
275
  <div className="flex items-center gap-1 group">
276
276
  {isClearable && value && (Array.isArray(value) ? value.length > 0 : true) && (
277
277
  <Button variant={"link"} size={"icon"}
278
+ disabled={disabled}
278
279
  alt="Clear selection"
279
280
  onClick={(e) => {
280
281
  e.stopPropagation();
package/src/env/index.ts CHANGED
@@ -6,6 +6,7 @@ import { AuthTokenPayload } from "@vertesia/common";
6
6
  export interface EnvProps {
7
7
  name: string; // the app name
8
8
  version: string,
9
+ sdkVersion?: string, // the @vertesia/ui package version
9
10
  isLocalDev: boolean,
10
11
  isDocker: boolean,
11
12
  type: "production" | "staging" | "preview" | "development" | string,
@@ -20,7 +21,7 @@ export interface EnvProps {
20
21
  appId?: string,
21
22
  providerType?: string,
22
23
  },
23
- datadog: boolean,
24
+ datadog?: boolean,
24
25
  logger?: {
25
26
  info: (msg: string, ...args: any) => void,
26
27
  warn: (msg: string, ...args: any) => void,
@@ -52,6 +53,10 @@ export class VertesiaEnvironment implements Readonly<EnvProps> {
52
53
  return this.prop("version");
53
54
  }
54
55
 
56
+ get sdkVersion() {
57
+ return this._props?.sdkVersion;
58
+ }
59
+
55
60
  get name() {
56
61
  return this.prop("name");
57
62
  }
@@ -93,7 +98,7 @@ export class VertesiaEnvironment implements Readonly<EnvProps> {
93
98
  }
94
99
 
95
100
  get datadog() {
96
- return this.prop("datadog");
101
+ return this._props?.datadog ?? false;
97
102
  }
98
103
 
99
104
  get logger() {
@@ -40,7 +40,11 @@ export function useCollectionsFilterGroups(facets: CollectionsFacetsNavProps['fa
40
40
  placeholder: 'Type',
41
41
  type: 'select' as const,
42
42
  multiple: true,
43
- options: typeOptions
43
+ options: typeOptions,
44
+ filterBy: (value: string, searchText: string) => {
45
+ const option = typeOptions.find(opt => opt.value === value);
46
+ return option?.label?.toLowerCase().includes(searchText.toLowerCase()) ?? false;
47
+ }
44
48
  };
45
49
  customFilterGroups.push(typeFilterGroup);
46
50
  }
@@ -28,8 +28,11 @@ export function GenericPageNavHeader({ className, children, title, description,
28
28
  const pathSegments: string[] = (cleanHref as string).split('/').filter((segment: string) => segment.length > 0);
29
29
 
30
30
  if (pathSegments.length === 3) {
31
+ if (entry.title !== document.title && entry.title) {
32
+ return entry.title;
33
+ }
31
34
  const secondSegment = pathSegments[1];
32
- return `${capitalize(secondSegment)} Detail`;
35
+ return `${capitalize(secondSegment)} Detail (...${pathSegments[2].slice(-6)})`;
33
36
  } else if (pathSegments.length >= 2) {
34
37
  return capitalize(pathSegments[pathSegments.length - 1]);
35
38
  } else if (pathSegments.length === 1) {
@@ -51,7 +54,7 @@ export function GenericPageNavHeader({ className, children, title, description,
51
54
  items.push({
52
55
  label: buildBreadcrumbLabel(entry),
53
56
  href: entry.href,
54
- onClick: () => window.history.go(-stepsBack)
57
+ onClick: () => navigate(entry.href, { stepsBack: stepsBack })
55
58
  });
56
59
  });
57
60
  }
@@ -265,11 +265,11 @@ function PropertiesEditor({ typeId, collection }: PropertiesEditorProps) {
265
265
 
266
266
  return (
267
267
  <Panel title="Properties" action={
268
- <Button size="lg" isLoading={isUpdating} type="submit">
268
+ <Button size="lg" isLoading={isUpdating} type="submit" onClick={() => _onSave(object?.value)}>
269
269
  Save
270
270
  </Button>}
271
271
  >
272
- <GeneratedForm object={object} onSubmit={_onSave} />
272
+ <GeneratedForm object={object} />
273
273
  </Panel>
274
274
  );
275
275
  }
@@ -27,7 +27,8 @@ const defaultLayout: ColumnLayout[] = [
27
27
 
28
28
  function getTableLayout(registry: TypeRegistry, type: string | undefined): ColumnLayout[] {
29
29
  const layout = type ? registry.getTypeLayout(type) : defaultLayout;
30
- return layout ?? defaultLayout;
30
+ const result = layout ?? defaultLayout;
31
+ return result;
31
32
  }
32
33
 
33
34
  interface DocumentSearchResultsWithDropZoneProps {
@@ -66,6 +66,10 @@ export interface NavigateOptions {
66
66
  * if defined, indicate whether the basePath will be used as a top-level base path or a nested base path.
67
67
  */
68
68
  isBasePathNested?: boolean;
69
+ // Number of steps to go back in history, which will pop the history stack instead of pushing a new entry
70
+ stepsBack?: number;
71
+ // the title to set for the new history entry
72
+ title?: string;
69
73
  }
70
74
 
71
75
  function getElementHrefAsUrl(elem: HTMLElement) {
@@ -113,6 +117,11 @@ export class HistoryNavigator {
113
117
  }
114
118
 
115
119
  navigate(to: string, options: NavigateOptions = {}) {
120
+ if (options.stepsBack && options.stepsBack > 0) {
121
+ this.stepBack(options.stepsBack, options);
122
+ return;
123
+ }
124
+
116
125
  if (options.basePath) {
117
126
  let basePath = options.basePath;
118
127
  if (!basePath.startsWith('/')) {
@@ -124,6 +133,24 @@ export class HistoryNavigator {
124
133
  this._navigate(new URL(to, window.location.href), 'navigate', options);
125
134
  }
126
135
 
136
+ stepBack(steps: number, options: NavigateOptions = {}) {
137
+ const historyChain = window.history.state.historyChain || [];
138
+ const to = historyChain.length >= steps
139
+ ? new URL(historyChain[historyChain.length - steps].href, window.location.href)
140
+ : new URL(window.location.origin, window.location.href);
141
+ this._navigate(to, 'popState', options);
142
+
143
+ const stateToStore = {
144
+ from: window.location.href,
145
+ historyChain: historyChain.slice(0, -steps),
146
+ data: options.state || undefined,
147
+ title: options.title || document.title
148
+ };
149
+
150
+ window.history['replaceState'](stateToStore, '', to.href);
151
+ this.fireLocationChange(new AfterLocationChangeEvent('popState', to, options.state));
152
+ }
153
+
127
154
  _navigate(to: URL, type: LocationChangeType, options: NavigateOptions) {
128
155
  const beforeEvent = new BeforeLocationChangeEvent(type, to, options.state);
129
156
  this.fireLocationChange(beforeEvent);
@@ -133,7 +160,7 @@ export class HistoryNavigator {
133
160
 
134
161
  // Build navigation chain by preserving previous history
135
162
  const currentState = window.history.state;
136
- const currentTitle = document.title;
163
+ const currentTitle = options.title || document.title;
137
164
 
138
165
  // Create new history chain entry
139
166
  const newChainEntry = {
@@ -160,7 +187,8 @@ export class HistoryNavigator {
160
187
  const stateToStore = {
161
188
  from: window.location.href,
162
189
  historyChain: historyChain,
163
- data: options.state || undefined
190
+ data: options.state || undefined,
191
+ title: options.title || document.title
164
192
  };
165
193
 
166
194
  window.history[options.replace ? 'replaceState' : 'pushState'](stateToStore, '', to.href);
@@ -2,6 +2,7 @@ import { getTenantIdFromProject } from "@vertesia/common";
2
2
  import { VTabs, VTabsBar, VTabsPanel, VTooltip } from "@vertesia/ui/core";
3
3
  import { Env } from "@vertesia/ui/env";
4
4
  import { useUserSession } from "@vertesia/ui/session";
5
+ // Package version is now passed as prop from the consuming application
5
6
  import { Check, CopyIcon } from "lucide-react";
6
7
  import { useState } from "react";
7
8
 
@@ -61,6 +62,7 @@ export default function InfoList() {
61
62
  <InfoItems title="Server" value={server} />
62
63
  <InfoItems title="Store" value={store} />
63
64
  <InfoItems title="App Version" value={Env.version} />
65
+ <InfoItems title="SDK Version" value={Env.sdkVersion || 'unknown'} />
64
66
  </div>
65
67
  }
66
68
  ];
@@ -1,4 +1,4 @@
1
- import { AuthTokenPayload } from "@vertesia/common";
1
+ import { AuthTokenPayload, Permission } from "@vertesia/common";
2
2
  import { Avatar, Button, MenuList, ModeToggle, Spinner } from "@vertesia/ui/core";
3
3
  import { useUserSession } from "@vertesia/ui/session";
4
4
  import { Popover } from "@vertesia/ui/widgets";
@@ -6,6 +6,8 @@ import clsx from "clsx";
6
6
  import { useState } from "react";
7
7
  import SignInModal from "./SignInModal";
8
8
  import InfoList from "./UserInfo";
9
+ import { useNavigate } from "@vertesia/ui/router";
10
+ import { useUserPermissions } from "@vertesia/ui/features";
9
11
  interface UserSessionMenuProps {
10
12
  name?: string
11
13
  picture?: string;
@@ -38,9 +40,13 @@ interface UserSessionPopupProps {
38
40
  }
39
41
  function UserSessionPopup({ className, asMenuTrigger = false }: UserSessionPopupProps) {
40
42
  const session = useUserSession();
43
+ const navigate = useNavigate();
44
+ const perms = useUserPermissions();
41
45
  const { user } = session;
42
46
  if (!session || !user) return null;
43
47
 
48
+ const isProjectManager = perms.hasPermission(Permission.project_admin);
49
+
44
50
  return (
45
51
  <Popover strategy='fixed' placement='bottom-start' zIndex={100}>
46
52
  <Popover.Trigger click>
@@ -68,6 +74,11 @@ function UserSessionPopup({ className, asMenuTrigger = false }: UserSessionPopup
68
74
  </div>
69
75
  <div className='py-2'>
70
76
  <MenuList>
77
+ {isProjectManager && (
78
+ <MenuList.Item className='px-2' onClick={() => navigate('/settings', { replace: true })}>
79
+ Settings
80
+ </MenuList.Item>
81
+ )}
71
82
  <MenuList.Item className='px-2' onClick={() => session.logout()}>
72
83
  Sign out
73
84
  </MenuList.Item>
@@ -94,10 +94,15 @@ export function ScalarField({ object, editor, inline = false }: ScalarFieldProps
94
94
  object.value = object.schema.isNumber ? parseFloat(value) : value
95
95
  }
96
96
 
97
+ if (object.isListItem) {
98
+ // List items don't need the FormItem wrapper (no label, description, etc.)
99
+ return <Component object={object} type={inputType} onChange={handleOnChange} />;
100
+ }
101
+
97
102
  return (
98
103
  <FormItem label={object.title} required={object.schema.isRequired} description={object.schema.description}
99
104
  className={clsx('flex', inline ? 'flex-row items-center' : 'flex-col')}>
100
- {!object.isListItem && <Component object={object} type={inputType} onChange={handleOnChange} />}
105
+ <Component object={object} type={inputType} onChange={handleOnChange} />
101
106
  </FormItem>
102
107
  )
103
108
  }
@@ -279,16 +279,13 @@ export class SchemaNode {
279
279
  if (this.schema.editor && data.editor === null) {
280
280
  // explicitly set to null => delete current editor
281
281
  this.schema.editor = undefined;
282
- this.schema.format = undefined;
283
282
  updated = true;
284
283
  } else if (data.editor) { // a new editor is set
285
284
  this.schema.editor = data.editor;
286
- this.schema.format = data.editor;
287
285
  updated = true;
288
286
  } else if (typeChanged) {
289
287
  // preserve editor only if the type didn't change
290
288
  this.schema.editor = undefined;
291
- this.schema.format = undefined;
292
289
  updated = true;
293
290
  }
294
291
  if (data.description !== this.description) {
@@ -132,7 +132,7 @@ export function getTypeSignature(schema: JSONSchema): TypeSignature {
132
132
  typeName = getItemTypeName(schema.items);
133
133
  }
134
134
  let displayTypeName: string = typeName;
135
- switch (schema.editor || schema.format) {
135
+ switch (schema.editor) {
136
136
  case 'textarea': {
137
137
  displayTypeName = 'text'; break;
138
138
  }