@firecms/core 3.0.0-canary.4 → 3.0.0-canary.40
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/README.md +2 -2
- package/dist/components/ClearFilterSortButton.d.ts +5 -0
- package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +1 -1
- package/dist/components/EntityCollectionTable/EntityCollectionTable.d.ts +2 -2
- package/dist/components/EntityCollectionTable/PropertyTableCell.d.ts +2 -2
- package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +1 -4
- package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +1 -2
- package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +11 -0
- package/dist/components/EntityCollectionView/useSelectionController.d.ts +2 -0
- package/dist/components/EntityPreview.d.ts +25 -7
- package/dist/components/EntityView.d.ts +11 -0
- package/dist/components/FieldCaption.d.ts +5 -0
- package/dist/components/HomePage/NavigationCard.d.ts +8 -0
- package/dist/components/HomePage/{NavigationCollectionCard.d.ts → NavigationCardBinding.d.ts} +2 -2
- package/dist/components/HomePage/SmallNavigationCard.d.ts +6 -0
- package/dist/components/HomePage/index.d.ts +3 -1
- package/dist/components/VirtualTable/VirtualTableProps.d.ts +1 -1
- package/dist/components/index.d.ts +4 -3
- package/dist/contexts/AuthControllerContext.d.ts +1 -1
- package/dist/{internal/EntityView.d.ts → core/EntityEditView.d.ts} +2 -2
- package/dist/core/SideEntityView.d.ts +7 -0
- package/dist/core/index.d.ts +0 -2
- package/dist/form/EntityForm.d.ts +1 -1
- package/dist/form/components/StorageItemPreview.d.ts +3 -2
- package/dist/form/components/StorageUploadProgress.d.ts +1 -1
- package/dist/form/components/index.d.ts +1 -0
- package/dist/form/field_bindings/KeyValueFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/MapFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/StorageUploadFieldBinding.d.ts +4 -3
- package/dist/form/field_bindings/TextFieldBinding.d.ts +2 -2
- package/dist/form/index.d.ts +1 -0
- package/dist/form/validation.d.ts +1 -1
- package/dist/hooks/data/delete.d.ts +2 -2
- package/dist/hooks/data/save.d.ts +1 -1
- package/dist/hooks/data/useDataSource.d.ts +2 -2
- package/dist/hooks/data/useEntityFetch.d.ts +3 -3
- package/dist/hooks/index.d.ts +3 -1
- package/dist/{core → hooks}/useBuildModeController.d.ts +1 -1
- package/dist/hooks/useBuildNavigationController.d.ts +6 -4
- package/dist/hooks/useProjectLog.d.ts +6 -2
- package/dist/hooks/useStorageSource.d.ts +2 -2
- package/dist/hooks/useValidateAuthenticator.d.ts +25 -0
- package/dist/index.es.js +8343 -7846
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +5 -5
- package/dist/index.umd.js.map +1 -1
- package/dist/internal/useBuildDataSource.d.ts +4 -0
- package/dist/preview/PropertyPreview.d.ts +1 -1
- package/dist/preview/PropertyPreviewProps.d.ts +1 -4
- package/dist/preview/components/BooleanPreview.d.ts +5 -1
- package/dist/preview/components/EnumValuesChip.d.ts +1 -1
- package/dist/preview/components/ReferencePreview.d.ts +1 -7
- package/dist/types/analytics.d.ts +1 -1
- package/dist/types/auth.d.ts +37 -1
- package/dist/types/collections.d.ts +22 -5
- package/dist/types/datasource.d.ts +1 -1
- package/dist/types/entities.d.ts +1 -1
- package/dist/types/entity_callbacks.d.ts +2 -2
- package/dist/types/entity_overrides.d.ts +6 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/navigation.d.ts +14 -13
- package/dist/types/permissions.d.ts +5 -1
- package/dist/types/plugins.d.ts +20 -20
- package/dist/types/properties.d.ts +2 -2
- package/dist/types/property_config.d.ts +2 -2
- package/dist/types/roles.d.ts +31 -0
- package/dist/types/storage.d.ts +11 -3
- package/dist/types/user.d.ts +5 -0
- package/dist/util/collections.d.ts +9 -1
- package/dist/util/entities.d.ts +1 -1
- package/dist/util/icons.d.ts +8 -2
- package/dist/util/permissions.d.ts +4 -4
- package/dist/util/references.d.ts +4 -2
- package/dist/util/resolutions.d.ts +1 -1
- package/dist/util/useTraceUpdate.d.ts +1 -0
- package/package.json +24 -24
- package/src/components/ClearFilterSortButton.tsx +41 -0
- package/src/components/DeleteEntityDialog.tsx +4 -4
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
- package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +268 -277
- package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +1 -1
- package/src/components/EntityCollectionTable/PropertyTableCell.tsx +13 -13
- package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +9 -16
- package/src/components/EntityCollectionTable/fields/TableStorageUpload.tsx +3 -3
- package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +27 -32
- package/src/components/EntityCollectionTable/internal/default_entity_actions.tsx +9 -5
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +39 -49
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +5 -6
- package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +68 -0
- package/src/components/EntityCollectionView/useSelectionController.tsx +30 -0
- package/src/components/EntityPreview.tsx +207 -70
- package/src/components/EntityView.tsx +84 -0
- package/src/components/FieldCaption.tsx +14 -0
- package/src/components/FireCMSAppBar.tsx +8 -0
- package/src/components/HomePage/DefaultHomePage.tsx +14 -10
- package/src/components/HomePage/NavigationCard.tsx +69 -0
- package/src/components/HomePage/NavigationCardBinding.tsx +116 -0
- package/src/components/HomePage/SmallNavigationCard.tsx +45 -0
- package/src/components/HomePage/index.tsx +3 -1
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +3 -4
- package/src/components/ReferenceWidget.tsx +4 -4
- package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +23 -8
- package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +35 -24
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +35 -15
- package/src/components/VirtualTable/VirtualTableProps.tsx +1 -1
- package/src/components/VirtualTable/fields/VirtualTableDateField.tsx +1 -1
- package/src/components/common/useDataSourceEntityCollectionTableController.tsx +1 -1
- package/src/components/index.tsx +4 -3
- package/src/contexts/AuthControllerContext.tsx +1 -1
- package/src/core/Drawer.tsx +66 -39
- package/src/{internal/EntityView.tsx → core/EntityEditView.tsx} +22 -39
- package/src/core/EntitySidePanel.tsx +2 -2
- package/src/core/FireCMS.tsx +18 -2
- package/src/core/NavigationRoutes.tsx +8 -0
- package/src/core/SideEntityView.tsx +38 -0
- package/src/core/field_configs.tsx +1 -2
- package/src/core/index.tsx +0 -2
- package/src/form/EntityForm.tsx +20 -12
- package/src/form/components/StorageItemPreview.tsx +5 -3
- package/src/form/components/StorageUploadProgress.tsx +6 -5
- package/src/form/components/index.tsx +1 -0
- package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +2 -3
- package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +12 -15
- package/src/form/field_bindings/BlockFieldBinding.tsx +2 -3
- package/src/form/field_bindings/DateTimeFieldBinding.tsx +4 -4
- package/src/form/field_bindings/KeyValueFieldBinding.tsx +18 -18
- package/src/form/field_bindings/MapFieldBinding.tsx +17 -17
- package/src/form/field_bindings/MarkdownFieldBinding.tsx +1 -2
- package/src/form/field_bindings/MultiSelectBinding.tsx +2 -3
- package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +3 -3
- package/src/form/field_bindings/ReferenceFieldBinding.tsx +5 -3
- package/src/form/field_bindings/RepeatFieldBinding.tsx +3 -3
- package/src/form/field_bindings/SelectFieldBinding.tsx +2 -3
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +15 -6
- package/src/form/field_bindings/SwitchFieldBinding.tsx +2 -3
- package/src/form/field_bindings/TextFieldBinding.tsx +10 -9
- package/src/form/index.tsx +1 -0
- package/src/form/validation.ts +3 -4
- package/src/hooks/data/delete.ts +3 -3
- package/src/hooks/data/save.ts +1 -1
- package/src/hooks/data/useCollectionFetch.tsx +1 -1
- package/src/hooks/data/useDataSource.tsx +8 -3
- package/src/hooks/data/useEntityFetch.tsx +4 -4
- package/src/hooks/index.tsx +5 -1
- package/src/{core → hooks}/useBuildLocalConfigurationPersistence.tsx +9 -10
- package/src/{core → hooks}/useBuildModeController.tsx +12 -6
- package/src/hooks/useBuildNavigationController.tsx +190 -72
- package/src/hooks/useProjectLog.tsx +16 -6
- package/src/hooks/useReferenceDialog.tsx +2 -2
- package/src/hooks/useStorageSource.tsx +7 -2
- package/src/hooks/useValidateAuthenticator.tsx +135 -0
- package/src/internal/useBuildDataSource.ts +6 -1
- package/src/internal/useBuildSideEntityController.tsx +18 -12
- package/src/preview/PropertyPreview.tsx +1 -1
- package/src/preview/PropertyPreviewProps.tsx +1 -11
- package/src/preview/components/BooleanPreview.tsx +19 -4
- package/src/preview/components/EnumValuesChip.tsx +1 -1
- package/src/preview/components/ReferencePreview.tsx +55 -147
- package/src/preview/property_previews/StringPropertyPreview.tsx +8 -7
- package/src/types/analytics.ts +1 -0
- package/src/types/auth.tsx +50 -1
- package/src/types/collections.ts +24 -5
- package/src/types/datasource.ts +1 -1
- package/src/types/entities.ts +1 -1
- package/src/types/entity_actions.tsx +4 -0
- package/src/types/entity_callbacks.ts +2 -2
- package/src/types/entity_overrides.tsx +7 -0
- package/src/types/firecms.tsx +0 -1
- package/src/types/index.ts +2 -0
- package/src/types/navigation.ts +17 -16
- package/src/types/permissions.ts +6 -1
- package/src/types/plugins.tsx +26 -28
- package/src/types/properties.ts +3 -2
- package/src/types/property_config.tsx +2 -2
- package/src/types/roles.ts +41 -0
- package/src/types/side_entity_controller.tsx +1 -0
- package/src/types/storage.ts +12 -3
- package/src/types/user.ts +7 -0
- package/src/util/collections.ts +22 -0
- package/src/util/entities.ts +1 -1
- package/src/util/icons.tsx +11 -3
- package/src/util/permissions.ts +11 -8
- package/src/util/references.ts +36 -5
- package/src/util/strings.ts +2 -2
- package/src/util/useTraceUpdate.tsx +2 -1
- package/src/components/HomePage/NavigationCollectionCard.tsx +0 -146
- /package/dist/{components → form/components}/LabelWithIcon.d.ts +0 -0
- /package/dist/{core → hooks}/useBuildLocalConfigurationPersistence.d.ts +0 -0
- /package/src/{components → form/components}/LabelWithIcon.tsx +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useCallback, useState } from "react";
|
|
2
2
|
import equal from "react-fast-compare"
|
|
3
3
|
|
|
4
|
-
import { ReferencePreview
|
|
4
|
+
import { ReferencePreview } from "../../../preview";
|
|
5
5
|
import { CollectionSize, Entity, EntityCollection, EntityReference, FilterValues } from "../../../types";
|
|
6
6
|
|
|
7
7
|
import { getPreviewSizeFrom } from "../../../preview/util";
|
|
@@ -9,6 +9,7 @@ import { getReferenceFrom } from "../../../util";
|
|
|
9
9
|
import { useCustomizationController, useNavigationController, useReferenceDialog } from "../../../hooks";
|
|
10
10
|
import { ErrorView } from "../../ErrorView";
|
|
11
11
|
import { Button } from "@firecms/ui";
|
|
12
|
+
import { EntityPreviewContainer } from "../../EntityPreview";
|
|
12
13
|
|
|
13
14
|
type TableReferenceFieldProps = {
|
|
14
15
|
name: string;
|
|
@@ -28,7 +29,7 @@ export function TableReferenceField(props: TableReferenceFieldProps) {
|
|
|
28
29
|
|
|
29
30
|
const navigationController = useNavigationController();
|
|
30
31
|
const { path } = props;
|
|
31
|
-
const collection = navigationController.getCollection
|
|
32
|
+
const collection = navigationController.getCollection(path);
|
|
32
33
|
if (!collection) {
|
|
33
34
|
if (customizationController.components?.missingReference) {
|
|
34
35
|
return <customizationController.components.missingReference path={path}/>;
|
|
@@ -57,11 +58,6 @@ export const TableReferenceFieldSuccess = React.memo(
|
|
|
57
58
|
collection
|
|
58
59
|
} = props;
|
|
59
60
|
|
|
60
|
-
const [onHover, setOnHover] = useState(false);
|
|
61
|
-
|
|
62
|
-
const hoverTrue = useCallback(() => setOnHover(true), []);
|
|
63
|
-
const hoverFalse = useCallback(() => setOnHover(false), []);
|
|
64
|
-
|
|
65
61
|
const onSingleEntitySelected = useCallback((entity: Entity<any>) => {
|
|
66
62
|
updateValue(entity ? getReferenceFrom(entity) : null);
|
|
67
63
|
}, [updateValue]);
|
|
@@ -101,29 +97,29 @@ export const TableReferenceFieldSuccess = React.memo(
|
|
|
101
97
|
onClick={disabled ? undefined : handleOpen}
|
|
102
98
|
size={getPreviewSizeFrom(size)}
|
|
103
99
|
reference={internalValue as EntityReference}
|
|
104
|
-
|
|
100
|
+
hover={!disabled}
|
|
105
101
|
disabled={!path}
|
|
106
102
|
previewProperties={previewProperties}
|
|
107
103
|
/>;
|
|
108
104
|
else
|
|
109
|
-
return <
|
|
105
|
+
return <EntityPreviewContainer
|
|
110
106
|
onClick={disabled ? undefined : handleOpen}
|
|
111
107
|
size={getPreviewSizeFrom(size)}>
|
|
112
108
|
<ErrorView title="Value is not a reference." error={"Click to edit"}/>
|
|
113
|
-
</
|
|
109
|
+
</EntityPreviewContainer>;
|
|
114
110
|
};
|
|
115
111
|
|
|
116
112
|
const buildMultipleReferenceField = () => {
|
|
117
113
|
if (Array.isArray(internalValue))
|
|
118
114
|
return <>
|
|
119
115
|
{internalValue.map((reference, index) =>
|
|
120
|
-
<div className="
|
|
116
|
+
<div className="w-full my-0.5"
|
|
121
117
|
key={`preview_array_ref_${name}_${index}`}>
|
|
122
118
|
<ReferencePreview
|
|
123
119
|
onClick={disabled ? undefined : handleOpen}
|
|
124
120
|
size={"tiny"}
|
|
125
121
|
reference={reference}
|
|
126
|
-
|
|
122
|
+
hover={!disabled}
|
|
127
123
|
disabled={!path}
|
|
128
124
|
previewProperties={previewProperties}
|
|
129
125
|
/>
|
|
@@ -139,10 +135,7 @@ export const TableReferenceFieldSuccess = React.memo(
|
|
|
139
135
|
return <ErrorView error={"The specified collection does not exist"}/>;
|
|
140
136
|
|
|
141
137
|
return (
|
|
142
|
-
<div className="w-full"
|
|
143
|
-
onMouseEnter={hoverTrue}
|
|
144
|
-
onMouseMove={hoverTrue}
|
|
145
|
-
onMouseLeave={hoverFalse}>
|
|
138
|
+
<div className="w-full group">
|
|
146
139
|
|
|
147
140
|
{internalValue && !multiselect && buildSingleReferenceField()}
|
|
148
141
|
|
|
@@ -51,7 +51,7 @@ export function TableStorageUpload(props: {
|
|
|
51
51
|
entity,
|
|
52
52
|
path,
|
|
53
53
|
previewSize,
|
|
54
|
-
updateValue
|
|
54
|
+
updateValue,
|
|
55
55
|
} = props;
|
|
56
56
|
|
|
57
57
|
const storageSource = useStorageSource();
|
|
@@ -137,7 +137,7 @@ function StorageUpload({
|
|
|
137
137
|
storage,
|
|
138
138
|
onFilesAdded,
|
|
139
139
|
onFileUploadComplete,
|
|
140
|
-
storagePathBuilder
|
|
140
|
+
storagePathBuilder,
|
|
141
141
|
}: StorageUploadProps) {
|
|
142
142
|
|
|
143
143
|
const [onHover, setOnHover] = useState(false);
|
|
@@ -293,7 +293,7 @@ export function TableStorageItemPreview({
|
|
|
293
293
|
|
|
294
294
|
return (
|
|
295
295
|
<div
|
|
296
|
-
className={"relative
|
|
296
|
+
className={"relative p-2 max-w-full"}
|
|
297
297
|
>
|
|
298
298
|
|
|
299
299
|
{value &&
|
|
@@ -16,20 +16,27 @@ import { useLargeLayout } from "../../../hooks";
|
|
|
16
16
|
|
|
17
17
|
interface CollectionTableToolbarProps {
|
|
18
18
|
size: CollectionSize;
|
|
19
|
-
filterIsSet: boolean;
|
|
20
19
|
loading: boolean;
|
|
21
|
-
forceFilter?: boolean;
|
|
22
20
|
actionsStart?: React.ReactNode;
|
|
23
21
|
actions?: React.ReactNode;
|
|
24
22
|
title?: React.ReactNode,
|
|
25
23
|
onTextSearchClick?: () => void;
|
|
26
24
|
onTextSearch?: (searchString?: string) => void;
|
|
27
25
|
onSizeChanged: (size: CollectionSize) => void;
|
|
28
|
-
clearFilter?: () => void;
|
|
29
26
|
textSearchLoading?: boolean;
|
|
30
27
|
}
|
|
31
28
|
|
|
32
|
-
export function CollectionTableToolbar(
|
|
29
|
+
export function CollectionTableToolbar({
|
|
30
|
+
actions,
|
|
31
|
+
actionsStart,
|
|
32
|
+
loading,
|
|
33
|
+
onSizeChanged,
|
|
34
|
+
onTextSearch,
|
|
35
|
+
onTextSearchClick,
|
|
36
|
+
size,
|
|
37
|
+
textSearchLoading,
|
|
38
|
+
title
|
|
39
|
+
}: CollectionTableToolbarProps) {
|
|
33
40
|
|
|
34
41
|
const searchInputRef = React.useRef<HTMLInputElement>(null);
|
|
35
42
|
const largeLayout = useLargeLayout();
|
|
@@ -37,30 +44,20 @@ export function CollectionTableToolbar(props: CollectionTableToolbarProps) {
|
|
|
37
44
|
const searchLoading = React.useRef<boolean>(false);
|
|
38
45
|
|
|
39
46
|
useEffect(() => {
|
|
40
|
-
if (searchInputRef.current && searchLoading.current && !
|
|
47
|
+
if (searchInputRef.current && searchLoading.current && !textSearchLoading) {
|
|
41
48
|
searchInputRef.current.focus();
|
|
42
49
|
}
|
|
43
|
-
searchLoading.current =
|
|
44
|
-
}, [
|
|
45
|
-
|
|
46
|
-
const clearFilterButton = !props.forceFilter && props.filterIsSet && props.clearFilter &&
|
|
47
|
-
<Button
|
|
48
|
-
variant={"outlined"}
|
|
49
|
-
className="h-fit-content"
|
|
50
|
-
aria-label="filter clear"
|
|
51
|
-
onClick={props.clearFilter}
|
|
52
|
-
size="small">
|
|
53
|
-
<FilterListOffIcon/>
|
|
54
|
-
Clear filter
|
|
55
|
-
</Button>;
|
|
50
|
+
searchLoading.current = textSearchLoading ?? false;
|
|
51
|
+
}, [textSearchLoading]);
|
|
52
|
+
|
|
56
53
|
|
|
57
54
|
const sizeSelect = (
|
|
58
55
|
<Tooltip title={"Table row size"} side={"right"} sideOffset={4}>
|
|
59
56
|
<Select
|
|
60
|
-
value={
|
|
57
|
+
value={size as string}
|
|
61
58
|
className="w-16 h-10"
|
|
62
59
|
size={"small"}
|
|
63
|
-
onValueChange={(v) =>
|
|
60
|
+
onValueChange={(v) => onSizeChanged(v as CollectionSize)}
|
|
64
61
|
renderValue={(v) => <div className={"font-medium"}>{v.toUpperCase()}</div>}
|
|
65
62
|
>
|
|
66
63
|
{["xs", "s", "m", "l", "xl"].map((size) => (
|
|
@@ -78,36 +75,34 @@ export function CollectionTableToolbar(props: CollectionTableToolbarProps) {
|
|
|
78
75
|
|
|
79
76
|
<div className="flex items-center gap-2 md:mr-4 mr-2">
|
|
80
77
|
|
|
81
|
-
{
|
|
82
|
-
{
|
|
78
|
+
{title && <div className={"hidden lg:block"}>
|
|
79
|
+
{title}
|
|
83
80
|
</div>}
|
|
84
81
|
|
|
85
82
|
{sizeSelect}
|
|
86
83
|
|
|
87
|
-
{
|
|
88
|
-
|
|
89
|
-
{clearFilterButton}
|
|
84
|
+
{actionsStart}
|
|
90
85
|
|
|
91
86
|
</div>
|
|
92
87
|
|
|
93
88
|
<div className="flex items-center gap-2">
|
|
94
89
|
|
|
95
90
|
{largeLayout && <div className="w-[22px]">
|
|
96
|
-
{
|
|
91
|
+
{loading &&
|
|
97
92
|
<CircularProgress size={"small"}/>}
|
|
98
93
|
</div>}
|
|
99
94
|
|
|
100
|
-
{(
|
|
95
|
+
{(onTextSearch || onTextSearchClick) &&
|
|
101
96
|
<SearchBar
|
|
102
97
|
key={"search-bar"}
|
|
103
98
|
inputRef={searchInputRef}
|
|
104
|
-
loading={
|
|
105
|
-
disabled={Boolean(
|
|
106
|
-
onClick={
|
|
107
|
-
onTextSearch={
|
|
99
|
+
loading={textSearchLoading}
|
|
100
|
+
disabled={Boolean(onTextSearchClick)}
|
|
101
|
+
onClick={onTextSearchClick}
|
|
102
|
+
onTextSearch={onTextSearchClick ? undefined : onTextSearch}
|
|
108
103
|
expandable={true}/>}
|
|
109
104
|
|
|
110
|
-
{
|
|
105
|
+
{actions}
|
|
111
106
|
|
|
112
107
|
</div>
|
|
113
108
|
|
|
@@ -9,21 +9,23 @@ export const editEntityAction: EntityAction = {
|
|
|
9
9
|
onClick({
|
|
10
10
|
entity,
|
|
11
11
|
collection,
|
|
12
|
+
fullPath,
|
|
12
13
|
context,
|
|
13
14
|
highlightEntity,
|
|
14
|
-
unhighlightEntity
|
|
15
|
+
unhighlightEntity,
|
|
15
16
|
}): Promise<void> {
|
|
16
17
|
highlightEntity?.(entity);
|
|
17
18
|
context.analyticsController?.onAnalyticsEvent?.("entity_click", {
|
|
18
19
|
path: entity.path,
|
|
19
20
|
entityId: entity.id
|
|
20
21
|
});
|
|
22
|
+
const path = collection?.collectionGroup ? entity.path : (fullPath ?? entity.path);
|
|
21
23
|
context.sideEntityController.open({
|
|
22
24
|
entityId: entity.id,
|
|
23
|
-
path
|
|
25
|
+
path,
|
|
24
26
|
collection,
|
|
25
27
|
updateUrl: true,
|
|
26
|
-
onClose: () => unhighlightEntity?.(entity)
|
|
28
|
+
onClose: () => unhighlightEntity?.(entity),
|
|
27
29
|
});
|
|
28
30
|
return Promise.resolve(undefined);
|
|
29
31
|
}
|
|
@@ -37,7 +39,7 @@ export const copyEntityAction: EntityAction = {
|
|
|
37
39
|
collection,
|
|
38
40
|
context,
|
|
39
41
|
highlightEntity,
|
|
40
|
-
unhighlightEntity
|
|
42
|
+
unhighlightEntity,
|
|
41
43
|
}): Promise<void> {
|
|
42
44
|
highlightEntity?.(entity);
|
|
43
45
|
context.analyticsController?.onAnalyticsEvent?.("copy_entity_click", {
|
|
@@ -50,11 +52,12 @@ export const copyEntityAction: EntityAction = {
|
|
|
50
52
|
copy: true,
|
|
51
53
|
collection,
|
|
52
54
|
updateUrl: true,
|
|
53
|
-
onClose: () => unhighlightEntity?.(entity)
|
|
55
|
+
onClose: () => unhighlightEntity?.(entity),
|
|
54
56
|
});
|
|
55
57
|
return Promise.resolve(undefined);
|
|
56
58
|
}
|
|
57
59
|
}
|
|
60
|
+
|
|
58
61
|
export const archiveEntityAction: EntityAction = {
|
|
59
62
|
icon: <ArchiveIcon/>,
|
|
60
63
|
name: "Archive",
|
|
@@ -69,6 +72,7 @@ export const archiveEntityAction: EntityAction = {
|
|
|
69
72
|
return Promise.resolve(undefined);
|
|
70
73
|
}
|
|
71
74
|
}
|
|
75
|
+
|
|
72
76
|
export const openWebsiteAction: EntityAction = {
|
|
73
77
|
icon: <OpenInNewIcon/>,
|
|
74
78
|
name: "See in website",
|
|
@@ -12,8 +12,7 @@ import {
|
|
|
12
12
|
PartialEntityCollection,
|
|
13
13
|
PropertyOrBuilder,
|
|
14
14
|
ResolvedProperty,
|
|
15
|
-
SaveEntityProps
|
|
16
|
-
SelectionController
|
|
15
|
+
SaveEntityProps
|
|
17
16
|
} from "../../types";
|
|
18
17
|
import {
|
|
19
18
|
EntityCollectionRowActions,
|
|
@@ -25,7 +24,6 @@ import {
|
|
|
25
24
|
canCreateEntity,
|
|
26
25
|
canDeleteEntity,
|
|
27
26
|
canEditEntity,
|
|
28
|
-
fullPathToCollectionSegments,
|
|
29
27
|
getPropertyInPath,
|
|
30
28
|
mergeDeep,
|
|
31
29
|
resolveCollection,
|
|
@@ -74,6 +72,9 @@ import {
|
|
|
74
72
|
} from "../EntityCollectionTable/internal/default_entity_actions";
|
|
75
73
|
import { DeleteEntityDialog } from "../DeleteEntityDialog";
|
|
76
74
|
import { useAnalyticsController } from "../../hooks/useAnalyticsController";
|
|
75
|
+
import { useSelectionController } from "./useSelectionController";
|
|
76
|
+
import { EntityCollectionViewStartActions } from "./EntityCollectionViewStartActions";
|
|
77
|
+
import { ClearFilterSortButton } from "../ClearFilterSortButton";
|
|
77
78
|
|
|
78
79
|
const COLLECTION_GROUP_PARENT_ID = "collectionGroupParent";
|
|
79
80
|
|
|
@@ -121,7 +122,7 @@ export const EntityCollectionView = React.memo(
|
|
|
121
122
|
}: EntityCollectionViewProps<M>
|
|
122
123
|
) {
|
|
123
124
|
|
|
124
|
-
const dataSource = useDataSource();
|
|
125
|
+
const dataSource = useDataSource(collectionProp);
|
|
125
126
|
const navigation = useNavigationController();
|
|
126
127
|
const sideEntityController = useSideEntityController();
|
|
127
128
|
const authController = useAuthController();
|
|
@@ -141,7 +142,7 @@ export const EntityCollectionView = React.memo(
|
|
|
141
142
|
collectionRef.current = collection;
|
|
142
143
|
}, [collection]);
|
|
143
144
|
|
|
144
|
-
const canCreateEntities = canCreateEntity(collection, authController,
|
|
145
|
+
const canCreateEntities = canCreateEntity(collection, authController, fullPath, null);
|
|
145
146
|
const [selectedNavigationEntity, setSelectedNavigationEntity] = useState<Entity<M> | undefined>(undefined);
|
|
146
147
|
const [deleteEntityClicked, setDeleteEntityClicked] = React.useState<Entity<M> | Entity<M>[] | undefined>(undefined);
|
|
147
148
|
|
|
@@ -160,7 +161,7 @@ export const EntityCollectionView = React.memo(
|
|
|
160
161
|
|
|
161
162
|
const checkInlineEditing = useCallback((entity?: Entity<any>): boolean => {
|
|
162
163
|
const collection = collectionRef.current;
|
|
163
|
-
if (!canEditEntity(collection, authController,
|
|
164
|
+
if (!canEditEntity(collection, authController, fullPath, entity ?? null)) {
|
|
164
165
|
return false;
|
|
165
166
|
}
|
|
166
167
|
return collection.inlineEditing === undefined || collection.inlineEditing;
|
|
@@ -199,6 +200,7 @@ export const EntityCollectionView = React.memo(
|
|
|
199
200
|
}, [tableController.setPopupCell]);
|
|
200
201
|
|
|
201
202
|
const onEntityClick = useCallback((clickedEntity: Entity<M>) => {
|
|
203
|
+
console.log("Entity clicked", clickedEntity)
|
|
202
204
|
const collection = collectionRef.current;
|
|
203
205
|
setSelectedNavigationEntity(clickedEntity);
|
|
204
206
|
analyticsController.onAnalyticsEvent?.("edit_entity_clicked", {
|
|
@@ -210,9 +212,9 @@ export const EntityCollectionView = React.memo(
|
|
|
210
212
|
path: clickedEntity.path,
|
|
211
213
|
collection,
|
|
212
214
|
updateUrl: true,
|
|
213
|
-
onClose: unselectNavigatedEntity
|
|
215
|
+
onClose: unselectNavigatedEntity,
|
|
214
216
|
});
|
|
215
|
-
}, [unselectNavigatedEntity]);
|
|
217
|
+
}, [unselectNavigatedEntity, sideEntityController]);
|
|
216
218
|
|
|
217
219
|
const onNewClick = useCallback(() => {
|
|
218
220
|
|
|
@@ -224,9 +226,9 @@ export const EntityCollectionView = React.memo(
|
|
|
224
226
|
path: fullPath,
|
|
225
227
|
collection,
|
|
226
228
|
updateUrl: true,
|
|
227
|
-
onClose: unselectNavigatedEntity
|
|
229
|
+
onClose: unselectNavigatedEntity,
|
|
228
230
|
});
|
|
229
|
-
}, [fullPath]);
|
|
231
|
+
}, [fullPath, sideEntityController]);
|
|
230
232
|
|
|
231
233
|
const onMultipleDeleteClick = () => {
|
|
232
234
|
analyticsController.onAnalyticsEvent?.("multiple_delete_dialog_open", {
|
|
@@ -288,7 +290,7 @@ export const EntityCollectionView = React.memo(
|
|
|
288
290
|
onCollectionModifiedForUser(fullPath, { defaultSize: size })
|
|
289
291
|
}, [onCollectionModifiedForUser, fullPath, userConfigPersistence]);
|
|
290
292
|
|
|
291
|
-
const createEnabled = canCreateEntity(collection, authController,
|
|
293
|
+
const createEnabled = canCreateEntity(collection, authController, fullPath, null);
|
|
292
294
|
|
|
293
295
|
const uniqueFieldValidator: UniqueFieldValidator = useCallback(
|
|
294
296
|
({
|
|
@@ -387,7 +389,7 @@ export const EntityCollectionView = React.memo(
|
|
|
387
389
|
entityId: entity.id,
|
|
388
390
|
selectedSubPath: subcollection.id ?? subcollection.path,
|
|
389
391
|
collection,
|
|
390
|
-
updateUrl: true
|
|
392
|
+
updateUrl: true,
|
|
391
393
|
});
|
|
392
394
|
}}>
|
|
393
395
|
{subcollection.name}
|
|
@@ -425,7 +427,7 @@ export const EntityCollectionView = React.memo(
|
|
|
425
427
|
...subcollectionColumns,
|
|
426
428
|
...collectionGroupParentCollections
|
|
427
429
|
];
|
|
428
|
-
}, [collection, fullPath]);
|
|
430
|
+
}, [collection, fullPath, sideEntityController]);
|
|
429
431
|
|
|
430
432
|
const updateLastDeleteTimestamp = useCallback(() => {
|
|
431
433
|
setLastDeleteTimestamp(Date.now());
|
|
@@ -433,11 +435,14 @@ export const EntityCollectionView = React.memo(
|
|
|
433
435
|
|
|
434
436
|
const largeLayout = useLargeLayout();
|
|
435
437
|
|
|
436
|
-
const getActionsForEntity = ({
|
|
438
|
+
const getActionsForEntity = ({
|
|
439
|
+
entity,
|
|
440
|
+
customEntityActions
|
|
441
|
+
}: {
|
|
437
442
|
entity?: Entity<M>,
|
|
438
443
|
customEntityActions?: EntityAction[]
|
|
439
444
|
}): EntityAction[] => {
|
|
440
|
-
const deleteEnabled = entity ? canDeleteEntity(collection, authController,
|
|
445
|
+
const deleteEnabled = entity ? canDeleteEntity(collection, authController, fullPath, entity) : true;
|
|
441
446
|
const actions: EntityAction[] = [editEntityAction];
|
|
442
447
|
if (createEnabled)
|
|
443
448
|
actions.push(copyEntityAction);
|
|
@@ -448,13 +453,13 @@ export const EntityCollectionView = React.memo(
|
|
|
448
453
|
return actions;
|
|
449
454
|
};
|
|
450
455
|
|
|
451
|
-
const getIdColumnWidth =
|
|
456
|
+
const getIdColumnWidth = () => {
|
|
452
457
|
const entityActions = getActionsForEntity({});
|
|
453
458
|
const collapsedActions = entityActions.filter(a => a.collapsed !== false);
|
|
454
459
|
const uncollapsedActions = entityActions.filter(a => a.collapsed === false);
|
|
455
460
|
const actionsWidth = uncollapsedActions.length * (largeLayout ? 40 : 30);
|
|
456
461
|
return (largeLayout ? (80 + actionsWidth) : (70 + actionsWidth)) + (collapsedActions.length > 0 ? (largeLayout ? 40 : 30) : 0);
|
|
457
|
-
}
|
|
462
|
+
};
|
|
458
463
|
|
|
459
464
|
const tableRowActionsBuilder = ({
|
|
460
465
|
entity,
|
|
@@ -470,7 +475,10 @@ export const EntityCollectionView = React.memo(
|
|
|
470
475
|
|
|
471
476
|
const isSelected = isEntitySelected(entity);
|
|
472
477
|
|
|
473
|
-
const actions = getActionsForEntity({
|
|
478
|
+
const actions = getActionsForEntity({
|
|
479
|
+
entity,
|
|
480
|
+
customEntityActions: collection.entityActions
|
|
481
|
+
});
|
|
474
482
|
|
|
475
483
|
return (
|
|
476
484
|
<EntityCollectionRowActions
|
|
@@ -577,7 +585,7 @@ export const EntityCollectionView = React.memo(
|
|
|
577
585
|
});
|
|
578
586
|
|
|
579
587
|
return (
|
|
580
|
-
<div className={cn("overflow-hidden h-full w-full", className)}
|
|
588
|
+
<div className={cn("overflow-hidden h-full w-full rounded-md", className)}
|
|
581
589
|
ref={containerRef}>
|
|
582
590
|
<EntityCollectionTable
|
|
583
591
|
key={`collection_table_${fullPath}`}
|
|
@@ -599,6 +607,14 @@ export const EntityCollectionView = React.memo(
|
|
|
599
607
|
onTextSearchClick={textSearchInitialised ? undefined : onTextSearchClick}
|
|
600
608
|
textSearchLoading={textSearchLoading}
|
|
601
609
|
textSearchEnabled={textSearchEnabled}
|
|
610
|
+
actionsStart={<EntityCollectionViewStartActions
|
|
611
|
+
parentCollectionIds={parentCollectionIds ?? []}
|
|
612
|
+
collection={collection}
|
|
613
|
+
tableController={tableController}
|
|
614
|
+
path={fullPath}
|
|
615
|
+
relativePath={collection.path}
|
|
616
|
+
selectionController={usedSelectionController}
|
|
617
|
+
collectionEntitiesCount={docsCount}/>}
|
|
602
618
|
actions={<EntityCollectionViewActions
|
|
603
619
|
parentCollectionIds={parentCollectionIds ?? []}
|
|
604
620
|
collection={collection}
|
|
@@ -676,39 +692,13 @@ export const EntityCollectionView = React.memo(
|
|
|
676
692
|
equal(a.selectionController, b.selectionController) &&
|
|
677
693
|
equal(a.Actions, b.Actions) &&
|
|
678
694
|
equal(a.defaultSize, b.defaultSize) &&
|
|
695
|
+
equal(a.initialFilter, b.initialFilter) &&
|
|
696
|
+
equal(a.initialSort, b.initialSort) &&
|
|
679
697
|
equal(a.textSearchEnabled, b.textSearchEnabled) &&
|
|
680
698
|
equal(a.additionalFields, b.additionalFields) &&
|
|
681
699
|
equal(a.forceFilter, b.forceFilter);
|
|
682
700
|
}) as React.FunctionComponent<EntityCollectionViewProps<any>>
|
|
683
701
|
|
|
684
|
-
export function useSelectionController<M extends Record<string, any> = any>(
|
|
685
|
-
onSelectionChange?: (entity: Entity<M>, selected: boolean) => void
|
|
686
|
-
): SelectionController<M> {
|
|
687
|
-
|
|
688
|
-
const [selectedEntities, setSelectedEntities] = useState<Entity<M>[]>([]);
|
|
689
|
-
|
|
690
|
-
const toggleEntitySelection = useCallback((entity: Entity<M>) => {
|
|
691
|
-
let newValue;
|
|
692
|
-
if (selectedEntities.map(e => e.id).includes(entity.id)) {
|
|
693
|
-
onSelectionChange?.(entity, false);
|
|
694
|
-
newValue = selectedEntities.filter((item: Entity<M>) => item.id !== entity.id);
|
|
695
|
-
} else {
|
|
696
|
-
onSelectionChange?.(entity, true);
|
|
697
|
-
newValue = [...selectedEntities, entity];
|
|
698
|
-
}
|
|
699
|
-
setSelectedEntities(newValue);
|
|
700
|
-
}, [selectedEntities]);
|
|
701
|
-
|
|
702
|
-
const isEntitySelected = useCallback((entity: Entity<M>) => selectedEntities.map(e => e.id).includes(entity.id), [selectedEntities]);
|
|
703
|
-
|
|
704
|
-
return {
|
|
705
|
-
selectedEntities,
|
|
706
|
-
setSelectedEntities,
|
|
707
|
-
isEntitySelected,
|
|
708
|
-
toggleEntitySelection
|
|
709
|
-
};
|
|
710
|
-
}
|
|
711
|
-
|
|
712
702
|
function EntitiesCount({
|
|
713
703
|
fullPath,
|
|
714
704
|
collection,
|
|
@@ -723,7 +713,7 @@ function EntitiesCount({
|
|
|
723
713
|
onCountChange?: (count: number) => void,
|
|
724
714
|
}) {
|
|
725
715
|
|
|
726
|
-
const dataSource = useDataSource();
|
|
716
|
+
const dataSource = useDataSource(collection);
|
|
727
717
|
const navigation = useNavigationController();
|
|
728
718
|
const [count, setCount] = useState<number | undefined>(undefined);
|
|
729
719
|
const [error, setError] = useState<Error | undefined>(undefined);
|
|
@@ -799,7 +789,7 @@ function EntityIdHeaderWidget({
|
|
|
799
789
|
entityId: searchString,
|
|
800
790
|
path,
|
|
801
791
|
collection,
|
|
802
|
-
updateUrl: true
|
|
792
|
+
updateUrl: true,
|
|
803
793
|
});
|
|
804
794
|
}}
|
|
805
795
|
className={"text-gray-900 dark:text-white w-96 max-w-full"}>
|
|
@@ -44,7 +44,7 @@ export function EntityCollectionViewActions<M extends Record<string, any>>({
|
|
|
44
44
|
|
|
45
45
|
const selectedEntities = selectionController.selectedEntities;
|
|
46
46
|
|
|
47
|
-
const addButton = canCreateEntity(collection, authController,
|
|
47
|
+
const addButton = canCreateEntity(collection, authController, path, null) &&
|
|
48
48
|
onNewClick && (largeLayout
|
|
49
49
|
? <Button
|
|
50
50
|
id={`add_entity_${path}`}
|
|
@@ -57,14 +57,13 @@ export function EntityCollectionViewActions<M extends Record<string, any>>({
|
|
|
57
57
|
: <Button
|
|
58
58
|
id={`add_entity_${path}`}
|
|
59
59
|
onClick={onNewClick}
|
|
60
|
-
size="medium"
|
|
61
60
|
variant="filled"
|
|
62
61
|
color="primary"
|
|
63
62
|
>
|
|
64
63
|
<AddIcon/>
|
|
65
64
|
</Button>);
|
|
66
65
|
|
|
67
|
-
const multipleDeleteEnabled = canDeleteEntity(collection, authController,
|
|
66
|
+
const multipleDeleteEnabled = canDeleteEntity(collection, authController, path, null);
|
|
68
67
|
|
|
69
68
|
let multipleDeleteButton: React.ReactNode | undefined;
|
|
70
69
|
if (selectionEnabled) {
|
|
@@ -112,11 +111,11 @@ export function EntityCollectionViewActions<M extends Record<string, any>>({
|
|
|
112
111
|
|
|
113
112
|
if (plugins) {
|
|
114
113
|
plugins.forEach((plugin, i) => {
|
|
115
|
-
if (plugin.
|
|
116
|
-
actions.push(...toArray(plugin.
|
|
114
|
+
if (plugin.collectionView?.CollectionActions) {
|
|
115
|
+
actions.push(...toArray(plugin.collectionView?.CollectionActions)
|
|
117
116
|
.map((Action, j) => (
|
|
118
117
|
<ErrorBoundary key={`plugin_actions_${i}_${j}`}>
|
|
119
|
-
<Action {...actionProps} {...plugin.
|
|
118
|
+
<Action {...actionProps} {...plugin.collectionView?.collectionActionsProps}/>
|
|
120
119
|
</ErrorBoundary>
|
|
121
120
|
)));
|
|
122
121
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useCustomizationController, useFireCMSContext } from "../../hooks";
|
|
3
|
+
import { CollectionActionsProps, EntityCollection, EntityTableController, SelectionController } from "../../types";
|
|
4
|
+
import { toArray } from "../../util/arrays";
|
|
5
|
+
import { ErrorBoundary } from "../ErrorBoundary";
|
|
6
|
+
import { ClearFilterSortButton } from "../ClearFilterSortButton";
|
|
7
|
+
|
|
8
|
+
export type EntityCollectionViewStartActionsProps<M extends Record<string, any>> = {
|
|
9
|
+
collection: EntityCollection<M>;
|
|
10
|
+
path: string;
|
|
11
|
+
relativePath: string;
|
|
12
|
+
parentCollectionIds: string[];
|
|
13
|
+
selectionController: SelectionController<M>;
|
|
14
|
+
tableController: EntityTableController<M>;
|
|
15
|
+
collectionEntitiesCount: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function EntityCollectionViewStartActions<M extends Record<string, any>>({
|
|
19
|
+
collection,
|
|
20
|
+
relativePath,
|
|
21
|
+
parentCollectionIds,
|
|
22
|
+
path,
|
|
23
|
+
selectionController,
|
|
24
|
+
tableController,
|
|
25
|
+
collectionEntitiesCount
|
|
26
|
+
}: EntityCollectionViewStartActionsProps<M>) {
|
|
27
|
+
|
|
28
|
+
const context = useFireCMSContext();
|
|
29
|
+
|
|
30
|
+
const customizationController = useCustomizationController();
|
|
31
|
+
const plugins = customizationController.plugins ?? [];
|
|
32
|
+
|
|
33
|
+
const actionProps: CollectionActionsProps = {
|
|
34
|
+
path,
|
|
35
|
+
relativePath,
|
|
36
|
+
parentCollectionIds,
|
|
37
|
+
collection,
|
|
38
|
+
selectionController,
|
|
39
|
+
context,
|
|
40
|
+
tableController,
|
|
41
|
+
collectionEntitiesCount
|
|
42
|
+
};
|
|
43
|
+
const actions: React.ReactNode[] = [
|
|
44
|
+
<ClearFilterSortButton
|
|
45
|
+
key={"clear_filter"}
|
|
46
|
+
tableController={tableController}
|
|
47
|
+
enabled={!collection.forceFilter}/>
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
if (plugins) {
|
|
51
|
+
plugins.forEach((plugin, i) => {
|
|
52
|
+
if (plugin.collectionView?.CollectionActionsStart) {
|
|
53
|
+
actions.push(...toArray(plugin.collectionView?.CollectionActionsStart)
|
|
54
|
+
.map((Action, j) => (
|
|
55
|
+
<ErrorBoundary key={`plugin_actions_${i}_${j}`}>
|
|
56
|
+
<Action {...actionProps} {...plugin.collectionView?.collectionActionsStartProps}/>
|
|
57
|
+
</ErrorBoundary>
|
|
58
|
+
)));
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<>
|
|
65
|
+
{actions}
|
|
66
|
+
</>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
2
|
+
import { Entity, SelectionController } from "../../types";
|
|
3
|
+
|
|
4
|
+
export function useSelectionController<M extends Record<string, any> = any>(
|
|
5
|
+
onSelectionChange?: (entity: Entity<M>, selected: boolean) => void
|
|
6
|
+
): SelectionController<M> {
|
|
7
|
+
|
|
8
|
+
const [selectedEntities, setSelectedEntities] = useState<Entity<M>[]>([]);
|
|
9
|
+
|
|
10
|
+
const toggleEntitySelection = useCallback((entity: Entity<M>) => {
|
|
11
|
+
let newValue;
|
|
12
|
+
if (selectedEntities.map(e => e.id).includes(entity.id)) {
|
|
13
|
+
onSelectionChange?.(entity, false);
|
|
14
|
+
newValue = selectedEntities.filter((item: Entity<M>) => item.id !== entity.id);
|
|
15
|
+
} else {
|
|
16
|
+
onSelectionChange?.(entity, true);
|
|
17
|
+
newValue = [...selectedEntities, entity];
|
|
18
|
+
}
|
|
19
|
+
setSelectedEntities(newValue);
|
|
20
|
+
}, [selectedEntities]);
|
|
21
|
+
|
|
22
|
+
const isEntitySelected = useCallback((entity: Entity<M>) => selectedEntities.map(e => e.id).includes(entity.id), [selectedEntities]);
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
selectedEntities,
|
|
26
|
+
setSelectedEntities,
|
|
27
|
+
isEntitySelected,
|
|
28
|
+
toggleEntitySelection
|
|
29
|
+
};
|
|
30
|
+
}
|