@rebasepro/cms 0.22.0 → 0.24.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.
Files changed (90) hide show
  1. package/dist/{CollectionEditorDialog-CUtfS_49.js → CollectionEditorDialog-p5-I-Apu.js} +181 -97
  2. package/dist/CollectionEditorDialog-p5-I-Apu.js.map +1 -0
  3. package/dist/{PropertyEditView-BpEbC0Q2.js → PropertyEditView-DDB_X-00.js} +55 -23
  4. package/dist/PropertyEditView-DDB_X-00.js.map +1 -0
  5. package/dist/{RouterCollectionsStudioView-EK8Knq5I.js → RouterCollectionsStudioView-SGBV4CMg.js} +34 -10
  6. package/dist/RouterCollectionsStudioView-SGBV4CMg.js.map +1 -0
  7. package/dist/{SchemaChangeDialog-BeP0A4MJ.js → SchemaChangeDialog-CcFSXvkK.js} +33 -12
  8. package/dist/SchemaChangeDialog-CcFSXvkK.js.map +1 -0
  9. package/dist/collection_editor/_cms_internals.d.ts +3 -1
  10. package/dist/collection_editor/liveSchemaClient.d.ts +38 -18
  11. package/dist/collection_editor/relation_kinds.d.ts +11 -0
  12. package/dist/collection_editor/serializable_types.d.ts +2 -0
  13. package/dist/collection_editor/types/config_controller.d.ts +11 -0
  14. package/dist/collection_editor/ui/collection_editor/CollectionStudioView.d.ts +8 -1
  15. package/dist/collection_editor/useLiveSchemaEditing.d.ts +5 -5
  16. package/dist/collection_editor/useLocalCollectionsConfigController.d.ts +7 -0
  17. package/dist/collection_editor_ui.js +5 -5
  18. package/dist/components/ClearFilterSortButton.d.ts +7 -1
  19. package/dist/components/CollectionTableBinding/fields/CompactEntityCellField.d.ts +12 -13
  20. package/dist/components/CollectionTableBinding/fields/TableMultipleRelationField.d.ts +6 -3
  21. package/dist/components/CollectionTableBinding/fields/TableReferenceField.d.ts +8 -1
  22. package/dist/components/CollectionTableBinding/fields/TableRelationField.d.ts +8 -1
  23. package/dist/components/CollectionTableBinding/fields/TableRelationSelectorField.d.ts +13 -3
  24. package/dist/components/CollectionTableBinding/fields/VirtualTableSelect.d.ts +10 -0
  25. package/dist/components/CollectionTableBinding/fields/VirtualTableUserSelect.d.ts +10 -0
  26. package/dist/components/CollectionTableBinding/internal/EntityTableCell.d.ts +21 -0
  27. package/dist/components/CollectionTableBinding/internal/TableCellOpener.d.ts +45 -0
  28. package/dist/components/CollectionTableBinding/table_bindings.d.ts +24 -0
  29. package/dist/components/CollectionViewBinding/CollectionViewActions.d.ts +6 -1
  30. package/dist/components/CollectionViewBinding/CollectionViewBinding.d.ts +12 -3
  31. package/dist/components/CollectionViewBinding/FiltersDialog.d.ts +5 -0
  32. package/dist/components/EditViewBinding.d.ts +7 -1
  33. package/dist/components/EntityIdentityBar.d.ts +29 -4
  34. package/dist/components/EntityPreviewBinding.d.ts +8 -1
  35. package/dist/components/EntityPreviewNesting.d.ts +15 -0
  36. package/dist/components/NavigationLoadingErrorView.d.ts +16 -0
  37. package/dist/components/PickerTrigger.d.ts +17 -0
  38. package/dist/components/ReferenceTable/SelectionTableBinding.d.ts +12 -0
  39. package/dist/components/RelationSelector.d.ts +24 -7
  40. package/dist/components/UserSelector.d.ts +8 -1
  41. package/dist/components/common/ActionMenuLabel.d.ts +15 -0
  42. package/dist/components/common/default_entity_actions.d.ts +37 -0
  43. package/dist/data_export/export/additional_fields.d.ts +13 -0
  44. package/dist/data_export/export/export.d.ts +6 -0
  45. package/dist/data_import/components/ImportConversionProblems.d.ts +11 -0
  46. package/dist/data_import/components/index.d.ts +1 -0
  47. package/dist/data_import/utils/csv.d.ts +3 -2
  48. package/dist/data_import/utils/data.d.ts +81 -4
  49. package/dist/data_import/utils/id_column.d.ts +16 -0
  50. package/dist/data_import/utils/import_columns.d.ts +48 -0
  51. package/dist/editor/editor.d.ts +7 -1
  52. package/dist/editor/extensions/Image.d.ts +22 -1
  53. package/dist/editor/hooks/useProseMirror.d.ts +4 -1
  54. package/dist/editor.js +69 -7
  55. package/dist/editor.js.map +1 -1
  56. package/dist/{export-LvIpE7sE.js → export-DCHuPD1a.js} +65 -211
  57. package/dist/export-DCHuPD1a.js.map +1 -0
  58. package/dist/form/form_utils.d.ts +37 -0
  59. package/dist/form/resolve_form_properties.d.ts +26 -0
  60. package/dist/form/validation.d.ts +13 -0
  61. package/dist/{history-DG6Ln7CV.js → history-CBd6SBRs.js} +3 -2
  62. package/dist/{history-DG6Ln7CV.js.map → history-CBd6SBRs.js.map} +1 -1
  63. package/dist/hooks/useEntityDisplay.d.ts +4 -5
  64. package/dist/hooks/useRecordActions.d.ts +28 -6
  65. package/dist/hooks/useReferencePaths.d.ts +21 -0
  66. package/dist/hooks/useStorageReferenceResolver.d.ts +23 -0
  67. package/dist/{import-Cxnmpyjl.js → import-DjQoRrIU.js} +35 -54
  68. package/dist/import-DjQoRrIU.js.map +1 -0
  69. package/dist/index.js +191 -117
  70. package/dist/index.js.map +1 -1
  71. package/dist/preview/components/StorageMarkdown.d.ts +10 -0
  72. package/dist/preview/util.d.ts +29 -0
  73. package/dist/util/caller_scopes.d.ts +15 -0
  74. package/dist/util/entity_actions.d.ts +37 -1
  75. package/dist/util/fit_inline_actions.d.ts +20 -0
  76. package/dist/util/navigation_utils.d.ts +14 -2
  77. package/dist/util/view_mode.d.ts +12 -18
  78. package/dist/{util-CyIcgnv2.js → util-BRvKFMLa.js} +5775 -3665
  79. package/dist/util-BRvKFMLa.js.map +1 -0
  80. package/dist/{util-DB31IoJE.js → util-D7ebttKT.js} +39 -2
  81. package/dist/util-D7ebttKT.js.map +1 -0
  82. package/package.json +9 -9
  83. package/dist/CollectionEditorDialog-CUtfS_49.js.map +0 -1
  84. package/dist/PropertyEditView-BpEbC0Q2.js.map +0 -1
  85. package/dist/RouterCollectionsStudioView-EK8Knq5I.js.map +0 -1
  86. package/dist/SchemaChangeDialog-BeP0A4MJ.js.map +0 -1
  87. package/dist/export-LvIpE7sE.js.map +0 -1
  88. package/dist/import-Cxnmpyjl.js.map +0 -1
  89. package/dist/util-CyIcgnv2.js.map +0 -1
  90. package/dist/util-DB31IoJE.js.map +0 -1
@@ -1,14 +1,14 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { A as getFieldId, At as convertDataToEntity, Ft as ImportSaveInProgress, Gt as SearchIconsView, Kt as FieldCaption, Nt as getInferenceType, Ot as ImportNewPropertyFieldPreview, Pt as useImportConfig, Rt as ImportFileUpload, S as isSchemaChangeCancelled, Sn as useUrlController, Tt as useCollectionEditorController, Wt as PropertyConfigBadge, _ as useCollectionsConfigController, _n as CollectionTableBinding, ct as useSafeSnackbarController, er as useCollectionRegistryController, hn as useSelectionController, i as namespaceToPropertiesOrderPath, k as getFieldConfig, kt as DataNewPropertiesMapping, l as validateCollectionJson, m as toSerializableCollectionConfig, n as getFullIdPath, o as CollectionGenerationApiError, r as idToPropertiesPath, t as getFullId, wn as useNavigationStateController } from "./util-CyIcgnv2.js";
3
- import { a as updatePropertyFromWidget, c as useAIModifiedPaths, i as supportedFields, n as PropertyForm, o as PropertyTree, r as PropertyFormDialog, s as AIModifiedPathsProvider } from "./PropertyEditView-BpEbC0Q2.js";
2
+ import { A as getFieldId, At as ImportNewPropertyFieldPreview, Bn as useUrlController, Dt as useCollectionEditorController, Ft as convertImportData, Hn as useNavigationStateController, Ht as getInferenceType, Mn as useSelectionController, Mt as guessIdColumn, Nt as ImportConversionProblems, Pn as CollectionTableBinding, S as isSchemaChangeCancelled, Ut as useImportConfig, Wt as ImportSaveInProgress, _ as useCollectionsConfigController, cn as SearchIconsView, ct as useSafeSnackbarController, i as namespaceToPropertiesOrderPath, jt as DataNewPropertiesMapping, k as getFieldConfig, l as validateCollectionJson, ln as FieldCaption, m as toSerializableCollectionConfig, n as getFullIdPath, o as CollectionGenerationApiError, qt as ImportFileUpload, r as idToPropertiesPath, sn as PropertyConfigBadge, t as getFullId, yr as useCollectionRegistryController } from "./util-BRvKFMLa.js";
3
+ import { a as updatePropertyFromWidget, c as useAIModifiedPaths, i as supportedFields, n as PropertyForm, o as PropertyTree, r as PropertyFormDialog, s as AIModifiedPathsProvider } from "./PropertyEditView-DDB_X-00.js";
4
4
  import * as React$1 from "react";
5
5
  import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
6
6
  import { Alert, AppWindow, ArrowLeftIcon, BooleanSwitchWithLabel, Button, Card, CheckIcon, Chip, CircularProgress, CircularProgressCenter, CodeIcon, ColumnsIcon, Container, CopyIcon, DebouncedTextField, Dialog, DialogActions, DialogContent, DialogTitle, ErrorBoundary, FileIcon, FileSearchIcon, FileTextIcon, IconButton, KanbanIcon, KeyIcon, LayoutGridIcon, ListIcon, LoadingButton, Menu, MultiSelect, MultiSelectItem, PanelLeftIcon, Paper, PlusIcon, Select, SelectItem, ShoppingCartIcon, SquareIcon, Tab, TableIcon, Tabs, TextField, ToggleButtonGroup, Tooltip, Trash2Icon, Typography, UserIcon, XIcon, cls, coolIconKeys, defaultBorderMixin, iconSize } from "@rebasepro/ui";
7
- import { buildCollectionFromTableMetadata, getGeneratedPolicyNames, getSubcollections, getTableName, isPropertyBuilder } from "@rebasepro/common";
7
+ import { buildCollectionFromTableMetadata, getGeneratedPolicyNames, getSubcollections, getTableName, isPropertyBuilder, policyToSecurityRule } from "@rebasepro/common";
8
8
  import { AIIcon, ConfirmationDialog, ErrorView, IconForView, UnsavedChangesDialog, getIcon, removeInitialAndTrailingSlashes, useAuthController, useCustomizationController, useLargeLayout, useRebaseContext, useTranslation, useUnsavedChangesDialog } from "@rebasepro/app";
9
9
  import { Field, Formex, clone, getIn, useCreateFormex, useFormex } from "@rebasepro/forms";
10
10
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
- import { RLS_ROLES_SQL, RLS_UID_SQL, getDataSourceCapabilities } from "@rebasepro/types";
11
+ import { RLS_ROLES_SQL, RLS_UID_SQL, getDataSourceCapabilities, isPostgresCollectionConfig } from "@rebasepro/types";
12
12
  import { camelCase, getPolicyNamesForRule, isEmptyObject, mergeDeep, prettifyIdentifier, randomString, removeUndefined, singular, slugify, toSnakeCase, unslugify } from "@rebasepro/utils";
13
13
  import { z } from "zod";
14
14
  import { buildEntityPropertiesFromData, buildPropertyFromData } from "@rebasepro/inference";
@@ -24,6 +24,7 @@ var CollectionEditorSchema = z.object({
24
24
  //#region src/collection_editor/ui/collection_editor/GeneralSettingsForm.tsx
25
25
  function GeneralSettingsForm({ isNewCollection, existingPaths, existingIds, parentCollection, renderExtraCollectionFields, standalone }) {
26
26
  const { values, setFieldValue, handleChange, touched, errors, setFieldTouched, submitCount } = useFormex();
27
+ const { t } = useTranslation();
27
28
  const [iconDialogOpen, setIconDialogOpen] = useState(false);
28
29
  const configControllerFromContext = useCollectionsConfigController();
29
30
  const configController = standalone ? { readOnly: false } : configControllerFromContext;
@@ -59,9 +60,10 @@ function GeneralSettingsForm({ isNewCollection, existingPaths, existingIds, pare
59
60
  onDatabaseIdUpdate: updateDatabaseId
60
61
  }),
61
62
  /* @__PURE__ */ jsx(Tooltip, {
62
- title: "Change icon",
63
+ title: t("change_icon"),
63
64
  asChild: true,
64
65
  children: /* @__PURE__ */ jsx(IconButton, {
66
+ "aria-label": t("change_icon"),
65
67
  shape: "square",
66
68
  disabled: configController?.readOnly,
67
69
  onClick: () => setIconDialogOpen(true),
@@ -103,6 +105,7 @@ function GeneralSettingsForm({ isNewCollection, existingPaths, existingIds, pare
103
105
  as: DebouncedTextField,
104
106
  label: "Table name",
105
107
  required: true,
108
+ disabled: !isNewCollection,
106
109
  error: showErrors && Boolean(errors.table)
107
110
  }), /* @__PURE__ */ jsx(FieldCaption, {
108
111
  error: touched.table && Boolean(errors.table),
@@ -787,6 +790,7 @@ function collectionToCode(collection) {
787
790
  function CollectionPropertiesEditorForm({ showErrors, isNewCollection, propertyErrorsRef, onPropertyError, setDirty, extraIcon, getUser, getData, doCollectionInference, propertyConfigs, propertyTypePresets, hiddenPropertyTypes, renderExtraPropertyFields, standalone }) {
788
791
  const { values, setFieldValue, setFieldError, setFieldTouched, errors, dirty } = useFormex();
789
792
  const snackbarController = useSafeSnackbarController();
793
+ const { t } = useTranslation();
790
794
  const configControllerFromContext = useCollectionsConfigController();
791
795
  const configController = standalone ? { readOnly: false } : configControllerFromContext;
792
796
  const largeLayout = useLargeLayout();
@@ -962,6 +966,8 @@ function CollectionPropertiesEditorForm({ showErrors, isNewCollection, propertyE
962
966
  const initialErrors = selectedPropertyKey && propertyErrorsRef?.current?.properties ? propertyErrorsRef.current.properties[selectedPropertyKey] : void 0;
963
967
  const emptyCollection = (values?.propertiesOrder === void 0 || values.propertiesOrder.length === 0) && (!values?.properties || Object.keys(values.properties).length === 0);
964
968
  const usedPropertiesOrder = values.propertiesOrder ? values.propertiesOrder : Object.keys(values.properties);
969
+ const selectedNamespaceOrder = getCurrentPropertiesOrder(selectedPropertyNamespace);
970
+ const otherPropertyKeys = useMemo(() => (selectedNamespaceOrder ?? []).filter((key) => key !== selectedPropertyKey), [selectedNamespaceOrder, selectedPropertyKey]);
965
971
  const owner = useMemo(() => values.ownerId && getUser ? getUser(values.ownerId) : null, [getUser, values.ownerId]);
966
972
  const generationCounter = useAIModifiedPaths()?.generationCounter ?? 0;
967
973
  const onPropertyClick = (propertyKey, namespace) => {
@@ -1006,15 +1012,17 @@ function CollectionPropertiesEditorForm({ showErrors, isNewCollection, propertyE
1006
1012
  title: "Add new properties based on data",
1007
1013
  asChild: true,
1008
1014
  children: /* @__PURE__ */ jsx(IconButton, {
1015
+ "aria-label": "Add new properties based on data",
1009
1016
  size: "small",
1010
1017
  disabled: inferringProperties,
1011
1018
  onClick: inferPropertiesFromData,
1012
1019
  children: inferringProperties ? /* @__PURE__ */ jsx(CircularProgress, { size: "smallest" }) : /* @__PURE__ */ jsx(FileSearchIcon, { size: iconSize.smallest })
1013
1020
  })
1014
1021
  }), /* @__PURE__ */ jsx(Tooltip, {
1015
- title: "Add new property",
1022
+ title: t("studio_property_add_column"),
1016
1023
  asChild: true,
1017
1024
  children: /* @__PURE__ */ jsx(IconButton, {
1025
+ "aria-label": t("studio_property_add_column"),
1018
1026
  size: "small",
1019
1027
  disabled: configController?.readOnly,
1020
1028
  onClick: () => setNewPropertyDialogOpen(true),
@@ -1071,6 +1079,7 @@ function CollectionPropertiesEditorForm({ showErrors, isNewCollection, propertyE
1071
1079
  propertyKey: selectedPropertyKey,
1072
1080
  propertyNamespace: selectedPropertyNamespace,
1073
1081
  property: selectedProperty,
1082
+ existingPropertyKeys: otherPropertyKeys,
1074
1083
  onPropertyChanged,
1075
1084
  onDelete: deleteProperty,
1076
1085
  onError: onPropertyErrorInternal,
@@ -2254,54 +2263,72 @@ function CollectionEditorImportDataPreview({ importConfig, properties, propertie
2254
2263
  const authController = useAuthController();
2255
2264
  const registry = useCollectionRegistryController();
2256
2265
  const [loading, setLoading] = useState(false);
2266
+ const [problems, setProblems] = useState([]);
2257
2267
  async function loadEntities() {
2258
- const mappedData = importConfig.importData.map((d) => convertDataToEntity(authController, registry, d, importConfig.idColumn, importConfig.headersMapping, properties, "TEMP_PATH", importConfig.defaultValues));
2259
- importConfig.setEntities(mappedData);
2268
+ const converted = convertImportData(authController, registry, importConfig.importData, importConfig.idColumn, importConfig.headersMapping, properties, "TEMP_PATH", importConfig.defaultValues);
2269
+ importConfig.setEntities(converted.entities);
2270
+ setProblems(converted.problems);
2260
2271
  }
2261
2272
  useEffect(() => {
2262
2273
  loadEntities().finally(() => setLoading(false));
2263
2274
  }, []);
2264
2275
  const selectionController = useSelectionController();
2265
2276
  if (loading) return /* @__PURE__ */ jsx(CircularProgressCenter, {});
2266
- return /* @__PURE__ */ jsx(CollectionTableBinding, {
2267
- title: /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(Typography, {
2268
- variant: "subtitle2",
2269
- children: "Imported data preview"
2270
- }), /* @__PURE__ */ jsx(Typography, {
2271
- variant: "caption",
2272
- children: "Entities with the same id will be overwritten"
2273
- })] }),
2274
- tableController: {
2275
- data: importConfig.entities,
2276
- dataLoading: false,
2277
- noMoreToLoad: false
2278
- },
2279
- endAdornment: /* @__PURE__ */ jsx("div", { className: "h-12" }),
2280
- filterable: false,
2281
- sortable: false,
2282
- selectionController,
2283
- displayedColumnIds: propertiesOrder.map((p) => ({
2284
- key: p,
2285
- disabled: false
2286
- })),
2287
- openEntityMode: "side_panel",
2288
- properties,
2289
- enablePopupIcon: false
2277
+ return /* @__PURE__ */ jsxs("div", {
2278
+ className: "flex flex-col h-full w-full",
2279
+ children: [/* @__PURE__ */ jsx(ImportConversionProblems, { problems }), /* @__PURE__ */ jsx("div", {
2280
+ className: "flex-1 min-h-0",
2281
+ children: /* @__PURE__ */ jsx(CollectionTableBinding, {
2282
+ title: /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(Typography, {
2283
+ variant: "subtitle2",
2284
+ children: "Imported data preview"
2285
+ }), /* @__PURE__ */ jsx(Typography, {
2286
+ variant: "caption",
2287
+ children: "Entities with the same id will be overwritten"
2288
+ })] }),
2289
+ tableController: {
2290
+ data: importConfig.entities,
2291
+ dataLoading: false,
2292
+ noMoreToLoad: false
2293
+ },
2294
+ endAdornment: /* @__PURE__ */ jsx("div", { className: "h-12" }),
2295
+ filterable: false,
2296
+ sortable: false,
2297
+ selectionController,
2298
+ displayedColumnIds: propertiesOrder.map((p) => ({
2299
+ key: p,
2300
+ disabled: false
2301
+ })),
2302
+ openEntityMode: "side_panel",
2303
+ properties,
2304
+ enablePopupIcon: false
2305
+ })
2306
+ })]
2290
2307
  });
2291
2308
  }
2292
2309
  //#endregion
2293
2310
  //#region src/collection_editor/ui/collection_editor/import/clean_import_data.ts
2294
2311
  function cleanPropertiesFromImport(properties, parentSlug = "") {
2295
- const result = Object.keys(properties).reduce((acc, key) => {
2312
+ const taken = /* @__PURE__ */ new Set();
2313
+ const uniqueSlug = (key, index) => {
2314
+ const base = slugify(key) || `column_${index + 1}`;
2315
+ let slug = base;
2316
+ for (let n = 2; taken.has(slug); n++) slug = `${base}_${n}`;
2317
+ taken.add(slug);
2318
+ return slug;
2319
+ };
2320
+ const result = Object.keys(properties).reduce((acc, key, index) => {
2296
2321
  const property = properties[key];
2297
- const slug = slugify(key);
2322
+ const slug = uniqueSlug(key, index);
2298
2323
  const fullSlug = parentSlug ? `${parentSlug}.${slug}` : slug;
2299
2324
  if (property.type === "map" && property.properties) {
2300
2325
  const slugifiedResult = cleanPropertiesFromImport(property.properties, fullSlug);
2326
+ const childMapping = Object.fromEntries(Object.entries(slugifiedResult.headersMapping).map(([childKey, childSlug]) => [`${key}.${childKey}`, childSlug]));
2301
2327
  return {
2302
2328
  headersMapping: {
2303
2329
  ...acc.headersMapping,
2304
- [key]: fullSlug
2330
+ [key]: fullSlug,
2331
+ ...childMapping
2305
2332
  },
2306
2333
  properties: {
2307
2334
  ...acc.properties,
@@ -2328,12 +2355,9 @@ function cleanPropertiesFromImport(properties, parentSlug = "") {
2328
2355
  headersMapping: {},
2329
2356
  properties: {}
2330
2357
  });
2331
- const firstKey = Object.keys(result.headersMapping)?.[0];
2332
- let idColumn;
2333
- if (firstKey?.includes("id") || firstKey?.includes("key")) idColumn = firstKey;
2334
2358
  return {
2335
2359
  ...result,
2336
- idColumn
2360
+ idColumn: guessIdColumn(Object.keys(result.headersMapping), result.headersMapping, result.properties)
2337
2361
  };
2338
2362
  }
2339
2363
  //#endregion
@@ -2371,48 +2395,88 @@ function toSecurityMode(permissive) {
2371
2395
  return permissive === "RESTRICTIVE" ? "restrictive" : "permissive";
2372
2396
  }
2373
2397
  /**
2374
- * Build a {@link SecurityRule} from a `pg_policies` row.
2398
+ * Build a rule from what the inline editor saved. Its `roles` are application
2399
+ * roles; a live policy's are its `TO` list, which `policyToSecurityRule` reads.
2375
2400
  *
2376
- * `SecurityRule` is a discriminated union and this is the part the local shadow
2377
- * type flattened away: a raw-SQL rule *requires* `using`, so a policy with only
2378
- * a `WITH CHECK` clause — every INSERT-only policy — is not one. It is a
2379
- * roles-only rule carrying the check. Assembling the object field-by-field
2380
- * against `operation?: string` accepted all of these and told the compiler
2381
- * nothing.
2401
+ * The WITH CHECK is kept whether or not there is a USING: every INSERT-only
2402
+ * policy has only a check, and a roles-only rule with no roles compiles to
2403
+ * `WITH CHECK (false)`, which stops every insert.
2382
2404
  */
2383
2405
  function toSecurityRule(policy) {
2384
- const base = {
2406
+ return {
2385
2407
  name: policy.policyname ?? "",
2386
2408
  operation: toSecurityOperation(policy.cmd),
2387
2409
  mode: toSecurityMode(policy.permissive),
2388
- roles: policy.roles ? [...policy.roles] : void 0
2410
+ roles: policy.roles ? [...policy.roles] : void 0,
2411
+ ...policy.qual ? { using: policy.qual } : {},
2412
+ ...policy.with_check ? { withCheck: policy.with_check } : {}
2389
2413
  };
2390
- if (policy.qual) return {
2391
- ...base,
2392
- using: policy.qual,
2393
- withCheck: policy.with_check || void 0
2394
- };
2395
- return base;
2414
+ }
2415
+ /**
2416
+ * The rule with what the editor changed, and nothing else.
2417
+ *
2418
+ * The editor shows a rule as a policy — name, command, mode, application roles
2419
+ * and raw USING / WITH CHECK — and has no field for `ownerField`, `access`, a
2420
+ * structured `condition`, `operations` (it shows them as ALL) or `pgRoles`.
2421
+ * The rule used to be rebuilt from the form, which dropped all of them:
2422
+ * renaming an owner rule saved a roles-only rule with no roles, which compiles
2423
+ * to `USING (false)`. So a field is only written when it differs from what the
2424
+ * editor was opened with. A condition typed in replaces the rule's condition
2425
+ * whole — the forms are exclusive.
2426
+ *
2427
+ * Studio's RLS editor does the same in `policyRules.ts`; there the editor's
2428
+ * roles are the policy's `TO` list, `pgRoles`, and here they are `roles`.
2429
+ */
2430
+ function editedRule(rule, original, edited) {
2431
+ const next = toSecurityRule(edited);
2432
+ const result = { ...rule };
2433
+ if ((edited.policyname ?? "") !== original.policyname) if (next.name) result.name = next.name;
2434
+ else delete result.name;
2435
+ if (edited.cmd !== original.cmd) {
2436
+ delete result.operations;
2437
+ result.operation = next.operation;
2438
+ }
2439
+ if (edited.permissive !== original.permissive) result.mode = next.mode;
2440
+ if (!sameRoles(edited.roles, original.roles)) if (next.roles && next.roles.length > 0) result.roles = next.roles;
2441
+ else delete result.roles;
2442
+ if ((edited.qual || null) !== (original.qual || null) || (edited.with_check || null) !== (original.with_check || null)) {
2443
+ delete result.ownerField;
2444
+ delete result.access;
2445
+ delete result.condition;
2446
+ delete result.check;
2447
+ delete result.using;
2448
+ delete result.withCheck;
2449
+ if (next.using) result.using = next.using;
2450
+ if (next.withCheck) result.withCheck = next.withCheck;
2451
+ }
2452
+ return result;
2453
+ }
2454
+ function sameRoles(a, b) {
2455
+ const left = [...a ?? []].sort();
2456
+ const right = [...b ?? []].sort();
2457
+ return left.length === right.length && left.every((role, i) => role === right[i]);
2396
2458
  }
2397
2459
  function CollectionRLSTab() {
2398
2460
  const { values, setFieldValue } = useFormex();
2399
- const [editingPolicy, setEditingPolicy] = useState(null);
2461
+ const [editing, setEditing] = useState(null);
2400
2462
  const { readOnly, readOnlyReason } = useCollectionsConfigController();
2401
2463
  const readOnlyTitle = readOnlyReason || "This backend does not accept collection edits.";
2402
2464
  const rules = values.securityRules || [];
2403
2465
  const { databaseAdmin } = useRebaseContext();
2404
2466
  const [dbPolicies, setDbPolicies] = useState([]);
2405
2467
  const [isLoadingDb, setIsLoadingDb] = useState(false);
2468
+ const tableName = getTableName(values);
2469
+ const schemaName = isPostgresCollectionConfig(values) && values.schema ? values.schema : "public";
2406
2470
  useEffect(() => {
2407
2471
  const fetchLivePolicies = async () => {
2408
- const tableName = values.id || values.table || values.alias;
2409
2472
  if (!tableName || !databaseAdmin?.executeSql) return;
2410
2473
  setIsLoadingDb(true);
2411
2474
  try {
2475
+ const rawTableName = sanitizeSqlIdentifier(tableName).slice(1, -1);
2412
2476
  const sql = `
2413
2477
  SELECT policyname, permissive, roles, cmd, qual, with_check
2414
2478
  FROM pg_policies
2415
- WHERE tablename = '${sanitizeSqlIdentifier(tableName).slice(1, -1)}' AND schemaname NOT IN ('information_schema', 'pg_catalog');
2479
+ WHERE schemaname = '${sanitizeSqlIdentifier(schemaName).slice(1, -1)}' AND tablename = '${rawTableName}';
2416
2480
  `;
2417
2481
  const result = await databaseAdmin.executeSql(sql);
2418
2482
  const extractRows = (res) => {
@@ -2445,9 +2509,8 @@ function CollectionRLSTab() {
2445
2509
  fetchLivePolicies();
2446
2510
  }, [
2447
2511
  databaseAdmin,
2448
- values.id,
2449
- values.table,
2450
- values.alias
2512
+ tableName,
2513
+ schemaName
2451
2514
  ]);
2452
2515
  /**
2453
2516
  * Application roles available to `SecurityRule.roles`.
@@ -2481,19 +2544,16 @@ function CollectionRLSTab() {
2481
2544
  mounted = false;
2482
2545
  };
2483
2546
  }, [databaseAdmin]);
2484
- const tableName = values.id || values.table || values.alias;
2485
2547
  const generatedPolicyNames = useMemo(() => {
2486
2548
  if (!tableName) return /* @__PURE__ */ new Set();
2487
2549
  return getGeneratedPolicyNames(values);
2488
2550
  }, [tableName, values]);
2489
2551
  const unmappedPolicies = dbPolicies.filter((dp) => !generatedPolicyNames.has(dp.policyname));
2490
- const handleSave = async (newPolicy) => {
2491
- const rule = toSecurityRule(newPolicy);
2492
- let newRules;
2493
- if (editingPolicy === "new") newRules = [...rules, rule];
2494
- else newRules = rules.map((r) => r.name === editingPolicy.policyname ? rule : r);
2552
+ const handleSave = (newPolicy) => {
2553
+ if (!editing) return;
2554
+ const newRules = editing === "new" ? [...rules, toSecurityRule(newPolicy)] : rules.map((r, i) => i === editing.index ? editedRule(r, editing.policy, newPolicy) : r);
2495
2555
  setFieldValue("securityRules", newRules);
2496
- setEditingPolicy(null);
2556
+ setEditing(null);
2497
2557
  };
2498
2558
  return /* @__PURE__ */ jsx("div", {
2499
2559
  className: "overflow-auto my-auto",
@@ -2514,7 +2574,7 @@ function CollectionRLSTab() {
2514
2574
  variant: "filled",
2515
2575
  color: "neutral",
2516
2576
  disabled: readOnly,
2517
- onClick: () => setEditingPolicy("new"),
2577
+ onClick: () => setEditing("new"),
2518
2578
  children: "CREATE POLICY"
2519
2579
  }) })
2520
2580
  })]
@@ -2527,7 +2587,7 @@ function CollectionRLSTab() {
2527
2587
  })
2528
2588
  }) : /* @__PURE__ */ jsx("div", {
2529
2589
  className: "flex flex-col gap-3",
2530
- children: rules.map((rule) => /* @__PURE__ */ jsxs(Paper, {
2590
+ children: rules.map((rule, index) => /* @__PURE__ */ jsxs(Paper, {
2531
2591
  className: "p-4 border border-transparent hover:border-surface-200 dark:hover:border-surface-700 rounded-lg flex flex-col sm:flex-row sm:items-center justify-between gap-4 transition-colors bg-surface-card shadow-sm",
2532
2592
  children: [/* @__PURE__ */ jsxs("div", {
2533
2593
  className: "flex flex-col gap-1.5 min-w-0",
@@ -2559,21 +2619,24 @@ function CollectionRLSTab() {
2559
2619
  size: "small",
2560
2620
  variant: "text",
2561
2621
  disabled: readOnly,
2562
- onClick: () => setEditingPolicy({
2563
- policyname: rule.name ?? "",
2564
- tablename: values.id || values.table || values.alias || "your_table",
2565
- permissive: (rule.mode || "permissive").toUpperCase(),
2566
- cmd: (rule.operation || "ALL").toUpperCase(),
2567
- roles: [...rule.roles ?? []],
2568
- qual: rule.using || null,
2569
- with_check: rule.withCheck || null
2622
+ onClick: () => setEditing({
2623
+ index,
2624
+ policy: {
2625
+ policyname: rule.name ?? "",
2626
+ tablename: tableName || "your_table",
2627
+ permissive: (rule.mode || "permissive").toUpperCase(),
2628
+ cmd: (rule.operation || "ALL").toUpperCase(),
2629
+ roles: [...rule.roles ?? []],
2630
+ qual: rule.using || null,
2631
+ with_check: rule.withCheck || null
2632
+ }
2570
2633
  }),
2571
2634
  children: "EDIT"
2572
2635
  }), /* @__PURE__ */ jsx(IconButton, {
2573
2636
  size: "small",
2574
2637
  disabled: readOnly,
2575
2638
  onClick: () => {
2576
- setFieldValue("securityRules", rules.filter((r) => r.name !== rule.name));
2639
+ setFieldValue("securityRules", rules.filter((_r, i) => i !== index));
2577
2640
  },
2578
2641
  children: /* @__PURE__ */ jsx(Trash2Icon, {
2579
2642
  size: iconSize.smallest,
@@ -2581,7 +2644,7 @@ function CollectionRLSTab() {
2581
2644
  })
2582
2645
  })]
2583
2646
  })]
2584
- }, rule.name))
2647
+ }, `${index}:${rule.name ?? ""}`))
2585
2648
  }),
2586
2649
  isLoadingDb && unmappedPolicies.length === 0 && /* @__PURE__ */ jsx("div", {
2587
2650
  className: "flex justify-center mt-8",
@@ -2648,7 +2711,7 @@ function CollectionRLSTab() {
2648
2711
  color: "primary",
2649
2712
  disabled: readOnly,
2650
2713
  onClick: () => {
2651
- const rule = toSecurityRule(dp);
2714
+ const rule = policyToSecurityRule(dp);
2652
2715
  setFieldValue("securityRules", [...rules, rule]);
2653
2716
  },
2654
2717
  children: "Import to codebase"
@@ -2661,16 +2724,17 @@ function CollectionRLSTab() {
2661
2724
  })
2662
2725
  ]
2663
2726
  }), /* @__PURE__ */ jsx(Dialog, {
2664
- open: !!editingPolicy,
2665
- onOpenChange: (open) => !open && setEditingPolicy(null),
2727
+ open: !!editing,
2728
+ onOpenChange: (open) => !open && setEditing(null),
2666
2729
  maxWidth: "4xl",
2667
- children: editingPolicy && /* @__PURE__ */ jsx(InlinePolicyEditor, {
2668
- policy: editingPolicy === "new" ? void 0 : editingPolicy,
2669
- table: values.id || values.table || values.alias || "your_table",
2730
+ children: editing && /* @__PURE__ */ jsx(InlinePolicyEditor, {
2731
+ policy: editing === "new" ? void 0 : editing.policy,
2732
+ table: tableName || "your_table",
2733
+ schema: schemaName,
2670
2734
  availableRoles,
2671
2735
  rolesUnavailable,
2672
2736
  onSave: handleSave,
2673
- onCancel: () => setEditingPolicy(null)
2737
+ onCancel: () => setEditing(null)
2674
2738
  })
2675
2739
  })]
2676
2740
  })
@@ -2683,7 +2747,7 @@ var COMMAND_OPTIONS = [
2683
2747
  "UPDATE",
2684
2748
  "DELETE"
2685
2749
  ];
2686
- function InlinePolicyEditor({ policy, table, availableRoles, rolesUnavailable, onSave, onCancel }) {
2750
+ function InlinePolicyEditor({ policy, table, schema, availableRoles, rolesUnavailable, onSave, onCancel }) {
2687
2751
  const { t } = useTranslation();
2688
2752
  const [name, setName] = useState(policy?.policyname || "");
2689
2753
  const [behavior, setBehavior] = useState(policy?.permissive || "PERMISSIVE");
@@ -2716,7 +2780,11 @@ function InlinePolicyEditor({ policy, table, availableRoles, rolesUnavailable, o
2716
2780
  className: "text-sm font-normal text-text-secondary dark:text-text-secondary-dark tracking-wide mt-1",
2717
2781
  children: ["Define RLS rules for ", /* @__PURE__ */ jsxs("span", {
2718
2782
  className: "font-mono text-primary bg-primary-bg px-1 py-0.5 rounded",
2719
- children: ["public.", table]
2783
+ children: [
2784
+ schema,
2785
+ ".",
2786
+ table
2787
+ ]
2720
2788
  })]
2721
2789
  })]
2722
2790
  }),
@@ -3052,11 +3120,17 @@ function CollectionEditorInternal({ isNewCollection, configController, editedCol
3052
3120
  if (visibleTabs && !visibleTabs.includes(currentView)) setCurrentView(visibleTabs[0] || "general");
3053
3121
  }, [visibleTabs, currentView]);
3054
3122
  const [error, setError] = React$1.useState();
3055
- const saveCollection = (updatedCollection) => {
3123
+ /**
3124
+ * `baseline` is what the form was loaded with: the controller saves the
3125
+ * difference between it and `updatedCollection`, so only what the person
3126
+ * changed is written. Never for a new collection, which is saved whole.
3127
+ */
3128
+ const saveCollection = (updatedCollection, baseline) => {
3056
3129
  const id = updatedCollection.slug;
3057
3130
  return configController.saveCollection({
3058
3131
  id,
3059
3132
  collectionData: updatedCollection,
3133
+ baseline: isNewCollection ? void 0 : baseline,
3060
3134
  previousId: editedCollectionId,
3061
3135
  parentCollectionSlugs
3062
3136
  }).then(() => {
@@ -3130,7 +3204,7 @@ function CollectionEditorInternal({ isNewCollection, configController, editedCol
3130
3204
  console.debug("Submitting collection", newCollectionState);
3131
3205
  try {
3132
3206
  if (!isNewCollection) {
3133
- if (await saveCollection(newCollectionState)) {
3207
+ if (await saveCollection(newCollectionState, formexController.initialValues)) {
3134
3208
  aiModifiedPaths?.clearAllPaths();
3135
3209
  formexController.resetForm({ values: newCollectionState });
3136
3210
  handleClose(newCollectionState);
@@ -3259,6 +3333,14 @@ function CollectionEditorInternal({ isNewCollection, configController, editedCol
3259
3333
  message: "Collection deleted",
3260
3334
  type: "success"
3261
3335
  });
3336
+ }, (e) => {
3337
+ setDeleteRequested(false);
3338
+ if (isSchemaChangeCancelled(e)) return;
3339
+ console.error(e);
3340
+ snackbarController.open({
3341
+ type: "error",
3342
+ message: "Error deleting collection: " + (e instanceof Error ? e.message : String(e))
3343
+ });
3262
3344
  });
3263
3345
  };
3264
3346
  const onWelcomeScreenContinue = (importData, propertiesOrder) => {
@@ -3386,7 +3468,7 @@ function CollectionEditorInternal({ isNewCollection, configController, editedCol
3386
3468
  type: "info",
3387
3469
  message: "Data imported successfully"
3388
3470
  });
3389
- await saveCollection(values);
3471
+ await saveCollection(values, formController.initialValues);
3390
3472
  handleClose(importedCollection);
3391
3473
  }
3392
3474
  }),
@@ -3422,7 +3504,6 @@ function CollectionEditorInternal({ isNewCollection, configController, editedCol
3422
3504
  hiddenPropertyTypes,
3423
3505
  renderExtraPropertyFields,
3424
3506
  extraIcon: extraView?.icon && /* @__PURE__ */ jsx(IconButton, {
3425
- color: "primary",
3426
3507
  onClick: () => setCurrentView("extra_view"),
3427
3508
  children: extraView.icon
3428
3509
  }),
@@ -3522,11 +3603,14 @@ function CollectionEditorInternal({ isNewCollection, configController, editedCol
3522
3603
  open: deleteRequested,
3523
3604
  onAccept: deleteCollection,
3524
3605
  onCancel: () => setDeleteRequested(false),
3525
- title: /* @__PURE__ */ jsx(Fragment, { children: "Delete the stored config?" }),
3606
+ title: /* @__PURE__ */ jsx(Fragment, { children: "Delete this collection?" }),
3526
3607
  body: /* @__PURE__ */ jsxs(Fragment, { children: [
3527
- " This will ",
3528
- /* @__PURE__ */ jsx("b", { children: "not delete any data" }),
3529
- ", only the stored config, and reset to the code state."
3608
+ "Deletes ",
3609
+ /* @__PURE__ */ jsxs("code", { children: [collection?.slug, ".ts"] }),
3610
+ " from your collections and its entry in",
3611
+ " ",
3612
+ /* @__PURE__ */ jsx("code", { children: "index.ts" }),
3613
+ ", and commits that. The table and its rows stay in the database. Refused while another collection links to this one."
3530
3614
  ] })
3531
3615
  })]
3532
3616
  });
@@ -3582,4 +3666,4 @@ var validateId = (value, isNewCollection, existingPaths, existingIds) => {
3582
3666
  //#endregion
3583
3667
  export { CollectionEditorDialog as n, CollectionEditorDialog_exports as r, CollectionEditor as t };
3584
3668
 
3585
- //# sourceMappingURL=CollectionEditorDialog-CUtfS_49.js.map
3669
+ //# sourceMappingURL=CollectionEditorDialog-p5-I-Apu.js.map