@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.
- package/dist/{CollectionEditorDialog-CUtfS_49.js → CollectionEditorDialog-p5-I-Apu.js} +181 -97
- package/dist/CollectionEditorDialog-p5-I-Apu.js.map +1 -0
- package/dist/{PropertyEditView-BpEbC0Q2.js → PropertyEditView-DDB_X-00.js} +55 -23
- package/dist/PropertyEditView-DDB_X-00.js.map +1 -0
- package/dist/{RouterCollectionsStudioView-EK8Knq5I.js → RouterCollectionsStudioView-SGBV4CMg.js} +34 -10
- package/dist/RouterCollectionsStudioView-SGBV4CMg.js.map +1 -0
- package/dist/{SchemaChangeDialog-BeP0A4MJ.js → SchemaChangeDialog-CcFSXvkK.js} +33 -12
- package/dist/SchemaChangeDialog-CcFSXvkK.js.map +1 -0
- package/dist/collection_editor/_cms_internals.d.ts +3 -1
- package/dist/collection_editor/liveSchemaClient.d.ts +38 -18
- package/dist/collection_editor/relation_kinds.d.ts +11 -0
- package/dist/collection_editor/serializable_types.d.ts +2 -0
- package/dist/collection_editor/types/config_controller.d.ts +11 -0
- package/dist/collection_editor/ui/collection_editor/CollectionStudioView.d.ts +8 -1
- package/dist/collection_editor/useLiveSchemaEditing.d.ts +5 -5
- package/dist/collection_editor/useLocalCollectionsConfigController.d.ts +7 -0
- package/dist/collection_editor_ui.js +5 -5
- package/dist/components/ClearFilterSortButton.d.ts +7 -1
- package/dist/components/CollectionTableBinding/fields/CompactEntityCellField.d.ts +12 -13
- package/dist/components/CollectionTableBinding/fields/TableMultipleRelationField.d.ts +6 -3
- package/dist/components/CollectionTableBinding/fields/TableReferenceField.d.ts +8 -1
- package/dist/components/CollectionTableBinding/fields/TableRelationField.d.ts +8 -1
- package/dist/components/CollectionTableBinding/fields/TableRelationSelectorField.d.ts +13 -3
- package/dist/components/CollectionTableBinding/fields/VirtualTableSelect.d.ts +10 -0
- package/dist/components/CollectionTableBinding/fields/VirtualTableUserSelect.d.ts +10 -0
- package/dist/components/CollectionTableBinding/internal/EntityTableCell.d.ts +21 -0
- package/dist/components/CollectionTableBinding/internal/TableCellOpener.d.ts +45 -0
- package/dist/components/CollectionTableBinding/table_bindings.d.ts +24 -0
- package/dist/components/CollectionViewBinding/CollectionViewActions.d.ts +6 -1
- package/dist/components/CollectionViewBinding/CollectionViewBinding.d.ts +12 -3
- package/dist/components/CollectionViewBinding/FiltersDialog.d.ts +5 -0
- package/dist/components/EditViewBinding.d.ts +7 -1
- package/dist/components/EntityIdentityBar.d.ts +29 -4
- package/dist/components/EntityPreviewBinding.d.ts +8 -1
- package/dist/components/EntityPreviewNesting.d.ts +15 -0
- package/dist/components/NavigationLoadingErrorView.d.ts +16 -0
- package/dist/components/PickerTrigger.d.ts +17 -0
- package/dist/components/ReferenceTable/SelectionTableBinding.d.ts +12 -0
- package/dist/components/RelationSelector.d.ts +24 -7
- package/dist/components/UserSelector.d.ts +8 -1
- package/dist/components/common/ActionMenuLabel.d.ts +15 -0
- package/dist/components/common/default_entity_actions.d.ts +37 -0
- package/dist/data_export/export/additional_fields.d.ts +13 -0
- package/dist/data_export/export/export.d.ts +6 -0
- package/dist/data_import/components/ImportConversionProblems.d.ts +11 -0
- package/dist/data_import/components/index.d.ts +1 -0
- package/dist/data_import/utils/csv.d.ts +3 -2
- package/dist/data_import/utils/data.d.ts +81 -4
- package/dist/data_import/utils/id_column.d.ts +16 -0
- package/dist/data_import/utils/import_columns.d.ts +48 -0
- package/dist/editor/editor.d.ts +7 -1
- package/dist/editor/extensions/Image.d.ts +22 -1
- package/dist/editor/hooks/useProseMirror.d.ts +4 -1
- package/dist/editor.js +69 -7
- package/dist/editor.js.map +1 -1
- package/dist/{export-LvIpE7sE.js → export-DCHuPD1a.js} +65 -211
- package/dist/export-DCHuPD1a.js.map +1 -0
- package/dist/form/form_utils.d.ts +37 -0
- package/dist/form/resolve_form_properties.d.ts +26 -0
- package/dist/form/validation.d.ts +13 -0
- package/dist/{history-DG6Ln7CV.js → history-CBd6SBRs.js} +3 -2
- package/dist/{history-DG6Ln7CV.js.map → history-CBd6SBRs.js.map} +1 -1
- package/dist/hooks/useEntityDisplay.d.ts +4 -5
- package/dist/hooks/useRecordActions.d.ts +28 -6
- package/dist/hooks/useReferencePaths.d.ts +21 -0
- package/dist/hooks/useStorageReferenceResolver.d.ts +23 -0
- package/dist/{import-Cxnmpyjl.js → import-DjQoRrIU.js} +35 -54
- package/dist/import-DjQoRrIU.js.map +1 -0
- package/dist/index.js +191 -117
- package/dist/index.js.map +1 -1
- package/dist/preview/components/StorageMarkdown.d.ts +10 -0
- package/dist/preview/util.d.ts +29 -0
- package/dist/util/caller_scopes.d.ts +15 -0
- package/dist/util/entity_actions.d.ts +37 -1
- package/dist/util/fit_inline_actions.d.ts +20 -0
- package/dist/util/navigation_utils.d.ts +14 -2
- package/dist/util/view_mode.d.ts +12 -18
- package/dist/{util-CyIcgnv2.js → util-BRvKFMLa.js} +5775 -3665
- package/dist/util-BRvKFMLa.js.map +1 -0
- package/dist/{util-DB31IoJE.js → util-D7ebttKT.js} +39 -2
- package/dist/util-D7ebttKT.js.map +1 -0
- package/package.json +9 -9
- package/dist/CollectionEditorDialog-CUtfS_49.js.map +0 -1
- package/dist/PropertyEditView-BpEbC0Q2.js.map +0 -1
- package/dist/RouterCollectionsStudioView-EK8Knq5I.js.map +0 -1
- package/dist/SchemaChangeDialog-BeP0A4MJ.js.map +0 -1
- package/dist/export-LvIpE7sE.js.map +0 -1
- package/dist/import-Cxnmpyjl.js.map +0 -1
- package/dist/util-CyIcgnv2.js.map +0 -1
- 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
|
|
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-
|
|
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: "
|
|
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: "
|
|
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
|
|
2259
|
-
importConfig.setEntities(
|
|
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__ */
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
*
|
|
2377
|
-
*
|
|
2378
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
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 [
|
|
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
|
|
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
|
-
|
|
2449
|
-
|
|
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 =
|
|
2491
|
-
|
|
2492
|
-
|
|
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
|
-
|
|
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: () =>
|
|
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: () =>
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
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((
|
|
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 =
|
|
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: !!
|
|
2665
|
-
onOpenChange: (open) => !open &&
|
|
2727
|
+
open: !!editing,
|
|
2728
|
+
onOpenChange: (open) => !open && setEditing(null),
|
|
2666
2729
|
maxWidth: "4xl",
|
|
2667
|
-
children:
|
|
2668
|
-
policy:
|
|
2669
|
-
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: () =>
|
|
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: [
|
|
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
|
-
|
|
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
|
|
3606
|
+
title: /* @__PURE__ */ jsx(Fragment, { children: "Delete this collection?" }),
|
|
3526
3607
|
body: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3527
|
-
"
|
|
3528
|
-
/* @__PURE__ */
|
|
3529
|
-
"
|
|
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-
|
|
3669
|
+
//# sourceMappingURL=CollectionEditorDialog-p5-I-Apu.js.map
|