@ea-lab/reactive-json 0.0.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 (66) hide show
  1. package/README.md +83 -0
  2. package/dist/reactive-json.css +5 -0
  3. package/dist/reactive-json.js +56303 -0
  4. package/dist/reactive-json.umd.cjs +382 -0
  5. package/lib/component/action/HashChangeListener.jsx +66 -0
  6. package/lib/component/action/Hide.jsx +14 -0
  7. package/lib/component/action/MessageListener.jsx +62 -0
  8. package/lib/component/action/Popover.jsx +53 -0
  9. package/lib/component/action/ReactOnEvent.jsx +118 -0
  10. package/lib/component/action/Redirect.jsx +26 -0
  11. package/lib/component/action/Tooltip.jsx +27 -0
  12. package/lib/component/action/VisuallyHide.jsx +15 -0
  13. package/lib/component/element/chart/BarChart.jsx +40 -0
  14. package/lib/component/element/chart/DoughnutChart.jsx +32 -0
  15. package/lib/component/element/chart/LineChart.jsx +40 -0
  16. package/lib/component/element/chart/PolarAreaChart.jsx +32 -0
  17. package/lib/component/element/form/CheckBoxField.jsx +215 -0
  18. package/lib/component/element/form/DateField.jsx +42 -0
  19. package/lib/component/element/form/NumberField.jsx +29 -0
  20. package/lib/component/element/form/SelectField.jsx +130 -0
  21. package/lib/component/element/form/TextAreaField.jsx +48 -0
  22. package/lib/component/element/form/TextField.jsx +65 -0
  23. package/lib/component/element/form/formElementsCommon.jsx +54 -0
  24. package/lib/component/element/html/AccordionItem.jsx +42 -0
  25. package/lib/component/element/html/FolderSortableTree.jsx +307 -0
  26. package/lib/component/element/html/FormatNumeral.jsx +118 -0
  27. package/lib/component/element/html/Html.jsx +107 -0
  28. package/lib/component/element/html/LabelFromValue.jsx +89 -0
  29. package/lib/component/element/html/Modal.jsx +77 -0
  30. package/lib/component/element/html/ModalForm.jsx +30 -0
  31. package/lib/component/element/html/Paragraph.jsx +10 -0
  32. package/lib/component/element/html/PreformattedMarkup.jsx +54 -0
  33. package/lib/component/element/html/SortableTreeItemCollapseButton.jsx +20 -0
  34. package/lib/component/element/html/Tabs.jsx +55 -0
  35. package/lib/component/element/special/BootstrapElement.jsx +32 -0
  36. package/lib/component/element/special/Count.jsx +46 -0
  37. package/lib/component/element/special/DataFilter.jsx +156 -0
  38. package/lib/component/element/special/DelayedActions.jsx +119 -0
  39. package/lib/component/element/special/PageControls.jsx +19 -0
  40. package/lib/component/element/special/Phantom.jsx +25 -0
  41. package/lib/component/element/special/Switch.jsx +131 -0
  42. package/lib/component/hook/usePagination.jsx +184 -0
  43. package/lib/component/reaction/addData.jsx +23 -0
  44. package/lib/component/reaction/fetchData.jsx +83 -0
  45. package/lib/component/reaction/moveData.jsx +52 -0
  46. package/lib/component/reaction/postMessage.jsx +43 -0
  47. package/lib/component/reaction/redirectNow.jsx +17 -0
  48. package/lib/component/reaction/removeData.jsx +48 -0
  49. package/lib/component/reaction/setClipboardData.jsx +20 -0
  50. package/lib/component/reaction/setData.jsx +23 -0
  51. package/lib/component/reaction/submitData.jsx +136 -0
  52. package/lib/component/reaction/triggerEvent.jsx +62 -0
  53. package/lib/component/utility/formatString.jsx +59 -0
  54. package/lib/engine/Actions.jsx +392 -0
  55. package/lib/engine/EventDispatcherContext.jsx +16 -0
  56. package/lib/engine/EventDispatcherProvider.jsx +80 -0
  57. package/lib/engine/GlobalDataContext.jsx +13 -0
  58. package/lib/engine/GlobalDataContextProvider.jsx +33 -0
  59. package/lib/engine/PaginationContext.jsx +10 -0
  60. package/lib/engine/PaginationProvider.jsx +61 -0
  61. package/lib/engine/ReactiveJsonRoot.jsx +315 -0
  62. package/lib/engine/TemplateContext.jsx +13 -0
  63. package/lib/engine/TemplateSystem.jsx +302 -0
  64. package/lib/engine/View.jsx +240 -0
  65. package/lib/main.jsx +41 -0
  66. package/package.json +72 -0
@@ -0,0 +1,131 @@
1
+ import {useContext} from 'react';
2
+ import ActionDependant from "../../../engine/Actions";
3
+ import GlobalDataContext from "../../../engine/GlobalDataContext";
4
+ import PaginationContext from "../../../engine/PaginationContext";
5
+ import TemplateContext from "../../../engine/TemplateContext";
6
+ import {dataLocationToPath, evaluateTemplateValue, isTemplateValue} from "../../../engine/TemplateSystem";
7
+ import View from "../../../engine/View";
8
+ import {usePagination} from "../../hook/usePagination";
9
+
10
+ const Switch = ({props, currentData, path}) => {
11
+ const globalDataContext = useContext(GlobalDataContext);
12
+ const templateContext = useContext(TemplateContext);
13
+
14
+ // The allowed item count. Any value less than 1 means infinite.
15
+ const cardinality = props?.cardinality ?? -1;
16
+
17
+ // The allowed sub items for this switch.
18
+ const options = props?.options ?? [];
19
+ const singleOption = props?.singleOption ?? undefined;
20
+
21
+ // This will tell if singleOption should be used.
22
+ const useSingleOption = !!singleOption;
23
+
24
+ // The content property is used to pinpoint on a template data value.
25
+ // This is not mandatory; the data can still set the selected value(s)
26
+ // by the usual overwrite by using the same render array structure.
27
+ const maybeContent = props?.content ?? null;
28
+ const evaluatedContent = maybeContent && evaluateTemplateValue({
29
+ globalDataContext: globalDataContext,
30
+ templateContext: templateContext,
31
+ valueToEvaluate: maybeContent,
32
+ });
33
+ const finalDataSource = typeof evaluatedContent === "object" ? evaluatedContent : currentData;
34
+
35
+ // This will limit the values count by the config in case the data is wrong.
36
+ const limitedContent = Object
37
+ .entries(finalDataSource)
38
+ .map(([dataIndex, dataEntry]) => {
39
+ const realIndex = dataIndex;
40
+
41
+ if (cardinality >= 1 && realIndex >= cardinality) {
42
+ // Ignore the data entry.
43
+ return null;
44
+ }
45
+
46
+ if (!dataEntry || typeof dataEntry !== "object") {
47
+ // Invalid entries count in the cardinality check.
48
+ // If we don't want them counted, we need to change the cardinality check itself
49
+ // which is currently based on the data index.
50
+ return null;
51
+ }
52
+
53
+ let dataKey;
54
+ let dataValue;
55
+ let selectedOption;
56
+
57
+ if (useSingleOption) {
58
+ dataValue = dataEntry;
59
+ selectedOption = singleOption;
60
+ } else {
61
+ dataKey = Object.keys(dataEntry)[0] ?? undefined;
62
+
63
+ if (dataKey === undefined) {
64
+ // Render nothing.
65
+ return null;
66
+ }
67
+
68
+ dataValue = Object.values(dataEntry)[0] ?? undefined;
69
+ selectedOption = options[dataKey] ?? undefined;
70
+ }
71
+
72
+ if (selectedOption === undefined) {
73
+ // Render nothing.
74
+ return null;
75
+ }
76
+
77
+ let finalPath = ((isTemplateValue(maybeContent) && dataLocationToPath({
78
+ dataLocation: maybeContent,
79
+ currentPath: templateContext.templatePath,
80
+ globalDataContext,
81
+ templateContext
82
+ })) || path) + "." + realIndex;
83
+
84
+ if (!useSingleOption) {
85
+ finalPath += "." + dataKey;
86
+ }
87
+
88
+ return <View
89
+ currentData={dataValue}
90
+ datafield={realIndex}
91
+ key={realIndex}
92
+ path={finalPath}
93
+ props={selectedOption}
94
+ />;
95
+ });
96
+
97
+ const pagination = usePagination(
98
+ {
99
+ dataToPaginate: limitedContent,
100
+ ...(props?.paginationProps ?? {})
101
+ });
102
+
103
+ // Slice for the pagination if in effect.
104
+ const contentAsViews = props?.paginated
105
+ ? limitedContent.slice(pagination.firstShownItemIndex, pagination.maxShownItemIndexExcluded)
106
+ : limitedContent;
107
+
108
+ const toRender = <>
109
+ {props?.before && <View
110
+ currentData={currentData?.["before"] ?? undefined}
111
+ path={path + ".before"}
112
+ datafield={"before"}
113
+ props={props?.before}/>}
114
+ {contentAsViews}
115
+ {props?.after && <View
116
+ currentData={currentData?.["after"] ?? undefined}
117
+ path={path + ".after"}
118
+ datafield={"after"}
119
+ props={props?.after}/>}
120
+ </>
121
+
122
+ return <ActionDependant {...props}>
123
+ {props?.paginated
124
+ ? <PaginationContext.Provider value={{pagination}}>
125
+ {toRender}
126
+ </PaginationContext.Provider>
127
+ : toRender}
128
+ </ActionDependant>
129
+ };
130
+
131
+ export default Switch;
@@ -0,0 +1,184 @@
1
+ import {Pagination} from "react-bootstrap";
2
+ import React, {useState} from "react";
3
+
4
+ /**
5
+ * Use this hook to create paginations.
6
+ *
7
+ * @param [Array] dataToPaginate The complete data to paginate.
8
+ * @param [int] forcePaginationDisplay Set to true to force the pagination even when having less than 2 pages.
9
+ * @param [int] maxPageButtonsCount The maximum page buttons count. Must be at least 1. Defaults to 5 when undefined.
10
+ * @param [int] pageMaxItemCount The maximum item count per page. Defaults to 10 when undefined.
11
+ *
12
+ * @returns {{
13
+ * firstShownItemIndex: number,
14
+ * getPageCountForContent: (function(Array): number),
15
+ * maxShownItemIndexExcluded: number,
16
+ * PageControls: (function({pageCount: *})),
17
+ * pageMaxItemCount: number,
18
+ * sliceVisibleContent: ((function(Array): *)|*),
19
+ * }}
20
+ */
21
+ export const usePagination = ({
22
+ dataToPaginate = [],
23
+ forcePaginationDisplay = false,
24
+ maxPageButtonsCount = 5,
25
+ pageMaxItemCount = 10,
26
+ }) => {
27
+ // TODO: la pagination ne se met pas à jour quand les filtres sont changés (currentData).
28
+ const [activePageNumber0, setActivePageNumber0] = useState(0);
29
+
30
+ /**
31
+ * Count of page buttons that shall appear before and after the current page.
32
+ *
33
+ * When the current page is near the start or the end, this value is ignored
34
+ * to show "maxPageButtonsCount" buttons.
35
+ *
36
+ * For example, when we have maxPageButtonsCount at 5, and 10 pages:
37
+ * - When at active page 1, 2, 3: [1,2,3,4,5].
38
+ * - When at active page 4, 5, 6, 7, 5 page buttons will appear, but shifted accordingly to the active page:
39
+ * - page 4: [2,3,4,5,6]
40
+ * - page 5: [3,4,5,6,7]
41
+ * - page 6: [4,5,6,7,8]
42
+ * - page 7: [5,6,7,8,9]
43
+ * - When at active page 8, 9, 10: [6,7,8,9,10].
44
+ *
45
+ * @type {number}
46
+ */
47
+ const buttonsBeforeAfterMaxCount = Math.floor(maxPageButtonsCount / 2);
48
+
49
+ /**
50
+ * Index of the first item in the given content to show for the active page.
51
+ *
52
+ * This value is ready for slice()'s start.
53
+ *
54
+ * @type {number}
55
+ */
56
+ const firstShownItemIndex = activePageNumber0 * pageMaxItemCount;
57
+
58
+ /**
59
+ * Index of the last item + 1 in the given content to show for the active page.
60
+ *
61
+ * This value is ready for slice()'s end.
62
+ *
63
+ * @type {number}
64
+ */
65
+ const maxShownItemIndexExcluded = firstShownItemIndex + pageMaxItemCount;
66
+
67
+ /**
68
+ * Gets the expected page count for the given content.
69
+ *
70
+ * @param {Array} contentSource The content to paginate.
71
+ *
72
+ * @returns {number} The page count.
73
+ */
74
+ const getPageCountForContent = (contentSource) => {
75
+ // Use ceil to have one last page for remaining items.
76
+ return Math.ceil(contentSource.length / pageMaxItemCount);
77
+ };
78
+
79
+ /**
80
+ * Slices the given content with the parameters of this pagination.
81
+ *
82
+ * Useful if the content array is complete.
83
+ * If not, it may be better to work directly in the component
84
+ * so that the hidden items are not computed for nothing.
85
+ *
86
+ * @param {Array} contentToSlice
87
+ * @returns {*}
88
+ */
89
+ const sliceVisibleContent = (contentToSlice) => {
90
+ if (!Array.isArray(contentToSlice)) {
91
+ // Not an array. Not supported.
92
+ return contentToSlice;
93
+ }
94
+
95
+ return contentToSlice.slice(firstShownItemIndex, maxShownItemIndexExcluded);
96
+ };
97
+
98
+ /**
99
+ * Component which contains the page controls (previous, next, pages).
100
+ *
101
+ * @returns {JSX.Element}
102
+ *
103
+ * @constructor
104
+ */
105
+ const PageControls = () => {
106
+ const pageCount = getPageCountForContent(dataToPaginate);
107
+
108
+ if (!forcePaginationDisplay && (pageCount <= 1)) {
109
+ return null;
110
+ }
111
+
112
+ return <Pagination>
113
+ <Pagination.First
114
+ disabled={activePageNumber0 <= 0}
115
+ onClick={() => {
116
+ setActivePageNumber0(0);
117
+ }}/>
118
+ <Pagination.Prev
119
+ disabled={activePageNumber0 <= 0}
120
+ onClick={() => {
121
+ setActivePageNumber0(activePageNumber0 - 1);
122
+ }}/>
123
+ {Math.min(activePageNumber0 - buttonsBeforeAfterMaxCount, pageCount - maxPageButtonsCount) > 0 ?
124
+ <Pagination.Ellipsis disabled/> : null}
125
+ {(() => {
126
+ const intermediateButtons = [];
127
+
128
+ // The first button is the leftmost visible button.
129
+ // It is either 0,
130
+ // or the current page minus buttonsBeforeAfterMaxCount,
131
+ // or maxPageButtonsCount starting from the end.
132
+ let currentPageButtonNumber0 = Math.min(Math.max(0, pageCount - maxPageButtonsCount), Math.max(0, activePageNumber0 - buttonsBeforeAfterMaxCount));
133
+ let remainingPagesToBuildButton = maxPageButtonsCount;
134
+
135
+ const insertPageButton = (currentPageButtonNumber0, remainingPagesToBuildButton) => {
136
+ intermediateButtons.push(<Pagination.Item
137
+ active={activePageNumber0 === currentPageButtonNumber0}
138
+ key={maxPageButtonsCount - remainingPagesToBuildButton}
139
+ onClick={() => {
140
+ setActivePageNumber0(currentPageButtonNumber0)
141
+ }}
142
+ >
143
+ {currentPageButtonNumber0 + 1}
144
+ </Pagination.Item>);
145
+ };
146
+
147
+ while (remainingPagesToBuildButton) {
148
+ insertPageButton(currentPageButtonNumber0, remainingPagesToBuildButton);
149
+
150
+ ++currentPageButtonNumber0;
151
+ --remainingPagesToBuildButton;
152
+
153
+ if (currentPageButtonNumber0 >= pageCount) {
154
+ // Reached the end of the pages.
155
+ break;
156
+ }
157
+ }
158
+
159
+ return intermediateButtons;
160
+ })()}
161
+ {pageCount > Math.max(buttonsBeforeAfterMaxCount, activePageNumber0) + Math.ceil(maxPageButtonsCount / 2) ?
162
+ <Pagination.Ellipsis disabled/> : null}
163
+ <Pagination.Next
164
+ disabled={activePageNumber0 + 1 >= pageCount}
165
+ onClick={() => {
166
+ setActivePageNumber0(activePageNumber0 + 1);
167
+ }}/>
168
+ <Pagination.Last
169
+ disabled={activePageNumber0 + 1 >= pageCount}
170
+ onClick={() => {
171
+ setActivePageNumber0(pageCount - 1);
172
+ }}/>
173
+ </Pagination>
174
+ };
175
+
176
+ return {
177
+ firstShownItemIndex,
178
+ getPageCountForContent,
179
+ maxShownItemIndexExcluded,
180
+ PageControls,
181
+ pageMaxItemCount,
182
+ sliceVisibleContent,
183
+ };
184
+ };
@@ -0,0 +1,23 @@
1
+ import {dataLocationToPath, evaluateTemplateValue} from "../../engine/TemplateSystem";
2
+ import {cloneDeep} from "lodash";
3
+
4
+ /**
5
+ * Adds data at the specified path.
6
+ *
7
+ * @param {{}} props
8
+ */
9
+ export const addData = (props) => {
10
+ const {globalDataContext, templateContext} = props;
11
+ const {path, value} = props.args;
12
+
13
+ if (path === undefined) {
14
+ return;
15
+ }
16
+
17
+ const dataAbsolutePath = dataLocationToPath({currentPath: templateContext.templatePath, dataLocation: path, globalDataContext, templateContext});
18
+
19
+ const evaluatedValue = evaluateTemplateValue({valueToEvaluate: value, globalDataContext, templateContext});
20
+
21
+ // We clone the value to have distinct instances when the value is an object.
22
+ globalDataContext?.updateData(typeof evaluatedValue !== "object" ? evaluatedValue : cloneDeep(evaluatedValue), dataAbsolutePath, "add");
23
+ };
@@ -0,0 +1,83 @@
1
+ import axios from "axios";
2
+ import {evaluateTemplateValue} from "../../engine/TemplateSystem";
3
+
4
+ /**
5
+ * Fetches data. Similar to submitData, but for GET requests.
6
+ *
7
+ * Will reload the app content if refreshAppOnResponse is true.
8
+ *
9
+ * @param {{args: {refreshAppOnResponse, url}, event, globalDataContext, templateContext}} props Reaction function props.
10
+ */
11
+ export const fetchData = (props) => {
12
+ // Prevent multiple submits / fetches.
13
+ const reactionEvent = props?.event;
14
+
15
+ // Check in realtime if we are already submitting.
16
+ // With this system, only 1 submit can be made concurrently for all roots.
17
+ const body = document.body;
18
+
19
+ if (body.dataset.htmlBuilderIsSubmitting === "true") {
20
+ return;
21
+ }
22
+
23
+ // This will block any attempts to resubmit until receiving the response.
24
+ body.dataset.htmlBuilderIsSubmitting = "true";
25
+
26
+ const currentTarget = reactionEvent.currentTarget;
27
+
28
+ if (currentTarget?.dataset) {
29
+ // Useful for styling.
30
+ currentTarget.dataset.isSubmitting = "true";
31
+ }
32
+
33
+ const {globalDataContext: _globalDataContext, templateContext} = props;
34
+
35
+ // Use the root context when submitting data,
36
+ // not the maybe-filtered one that the DataFilter component may have edited.
37
+ // This could be made configurable if ever needed.
38
+ const globalDataContext = _globalDataContext.getRootContext ? _globalDataContext.getRootContext() : _globalDataContext;
39
+
40
+ /**
41
+ * Tells if the response content will replace the current app content.
42
+ *
43
+ * @type {boolean}
44
+ */
45
+ const refreshAppOnResponse = props?.args?.refreshAppOnResponse ?? true;
46
+
47
+ const url = evaluateTemplateValue({
48
+ valueToEvaluate: props?.args?.url, globalDataContext, templateContext
49
+ });
50
+
51
+ if (!url) {
52
+ return;
53
+ }
54
+
55
+ const headers = globalDataContext.headersForData ?? {};
56
+
57
+ const {setRawAppData} = globalDataContext;
58
+
59
+ axios
60
+ .get(
61
+ url,
62
+ {
63
+ headers
64
+ })
65
+ .then((value) => {
66
+ if (!refreshAppOnResponse) {
67
+ return;
68
+ }
69
+
70
+ // This will trigger a complete re-render.
71
+ setRawAppData(value.data);
72
+ })
73
+ .catch((reason) => {
74
+ console.log("reactionFunction:fetchData : Could not fetch. Reason: " + reason.message);
75
+ })
76
+ .finally(() => {
77
+ delete body.dataset.htmlBuilderIsSubmitting;
78
+
79
+ if (currentTarget?.dataset) {
80
+ delete currentTarget.dataset.isSubmitting;
81
+ }
82
+ });
83
+ };
@@ -0,0 +1,52 @@
1
+ import {dataLocationToPath} from "../../engine/TemplateSystem";
2
+
3
+ /**
4
+ * Moves data at the specified path.
5
+ *
6
+ * @param {{}} props
7
+ */
8
+ export const moveData = (props) => {
9
+ const {globalDataContext, templateContext} = props;
10
+ const {path, target} = props.args;
11
+
12
+ if (path === undefined && target !== "currentTemplateData") {
13
+ return;
14
+ }
15
+
16
+ let dataAbsolutePath;
17
+
18
+ if (path) {
19
+ dataAbsolutePath = dataLocationToPath({
20
+ currentPath: templateContext.templatePath,
21
+ dataLocation: path,
22
+ globalDataContext,
23
+ templateContext
24
+ });
25
+ } else {
26
+ // Target mode.
27
+ dataAbsolutePath = templateContext.templatePath;
28
+
29
+ // Dev note: could this be interesting for the other mode?
30
+ let parentLevel = props.args.parentLevel ?? 0;
31
+
32
+ while (parentLevel > 0) {
33
+ --parentLevel;
34
+
35
+ // Remove a level from the path.
36
+ const lastIndex = dataAbsolutePath.lastIndexOf('.');
37
+
38
+ if (lastIndex < 1) {
39
+ // No valid path to remove. Is there a valid use case where we should remove everything?
40
+ return;
41
+ }
42
+
43
+ dataAbsolutePath = dataAbsolutePath.substring(0, lastIndex);
44
+ }
45
+ }
46
+
47
+ const {increment} = props.args;
48
+
49
+ globalDataContext?.updateData({
50
+ increment,
51
+ }, dataAbsolutePath, "move");
52
+ };
@@ -0,0 +1,43 @@
1
+ import {evaluateTemplateValueCollection} from "../../engine/TemplateSystem";
2
+
3
+ /**
4
+ * Posts a message to the specified target.
5
+ *
6
+ * @param {{args: {includeChangedValue, message, messageTarget, on, targetOrigin}, event, globalDataContext, templateContext}} props
7
+ */
8
+ export const postMessage = (props) => {
9
+ const messageTargets = {
10
+ parent: window.parent,
11
+ self: window,
12
+ };
13
+
14
+ const messageTarget = messageTargets[props?.args?.messageTarget ?? "parent"] ?? window;
15
+
16
+ // The targetOrigin must match the schema and domain where the message will be sent.
17
+ // Otherwise, the message will be discarded for security reasons.
18
+ // When not set, the target will be the current location's origin by default.
19
+ const messageTargetOrigin = props?.args?.targetOrigin ?? window.location.origin;
20
+
21
+ const message_evaluated = evaluateTemplateValueCollection({
22
+ globalDataContext: props.globalDataContext,
23
+ templateContext: props.templateContext,
24
+ valueToEvaluate: props?.args?.message
25
+ });
26
+
27
+ if (props?.args?.on === "change" && typeof message_evaluated === "object" && props?.args?.includeChangedValue && props?.event?.target?.nodeName === "INPUT") {
28
+ let changedValue;
29
+
30
+ switch (props?.event?.target?.type) {
31
+ case "checkbox":
32
+ changedValue = props?.event?.target?.checked;
33
+ break;
34
+
35
+ default:
36
+ // TODO: support other types.
37
+ }
38
+
39
+ message_evaluated["changedValue"] = changedValue;
40
+ }
41
+
42
+ (messageTarget && messageTargetOrigin) && messageTarget.postMessage(message_evaluated, messageTargetOrigin);
43
+ };
@@ -0,0 +1,17 @@
1
+ import {evaluateTemplateValue} from "../../engine/TemplateSystem";
2
+
3
+ /**
4
+ * Redirects to the specified URL.
5
+ *
6
+ * @param {{}} props
7
+ */
8
+ export const redirectNow = (props) => {
9
+ const {globalDataContext, templateContext} = props;
10
+ const {to} = props.args;
11
+
12
+ if (!to || typeof to !== "string") {
13
+ return;
14
+ }
15
+
16
+ window.location.href = evaluateTemplateValue({valueToEvaluate: to, globalDataContext, templateContext});
17
+ };
@@ -0,0 +1,48 @@
1
+ import {dataLocationToPath} from "../../engine/TemplateSystem";
2
+
3
+ /**
4
+ * Removes data at the specified path.
5
+ *
6
+ * @param {{}} props
7
+ */
8
+ export const removeData = (props) => {
9
+ const {globalDataContext, templateContext} = props;
10
+ const {path, target} = props.args;
11
+
12
+ if (path === undefined && target !== "currentTemplateData") {
13
+ return;
14
+ }
15
+
16
+ let dataAbsolutePath;
17
+
18
+ if (path) {
19
+ dataAbsolutePath = dataLocationToPath({
20
+ currentPath: templateContext.templatePath,
21
+ dataLocation: path,
22
+ globalDataContext,
23
+ templateContext
24
+ });
25
+ } else {
26
+ // Target mode.
27
+ dataAbsolutePath = templateContext.templatePath;
28
+
29
+ // Dev note: could this be interesting for the other mode?
30
+ let parentLevel = props.args.parentLevel ?? 0;
31
+
32
+ while (parentLevel > 0) {
33
+ --parentLevel;
34
+
35
+ // Remove a level from the path.
36
+ const lastIndex = dataAbsolutePath.lastIndexOf('.');
37
+
38
+ if (lastIndex < 1) {
39
+ // No valid path to remove. Is there a valid use case where we should remove everything?
40
+ return;
41
+ }
42
+
43
+ dataAbsolutePath = dataAbsolutePath.substring(0, lastIndex);
44
+ }
45
+ }
46
+
47
+ globalDataContext?.updateData(undefined, dataAbsolutePath, "remove");
48
+ };
@@ -0,0 +1,20 @@
1
+ import {evaluateTemplateValue} from "../../engine/TemplateSystem";
2
+
3
+ /**
4
+ * Copies data to the clipboard.
5
+ *
6
+ * @param {{}} props
7
+ */
8
+ export const setClipboardData = async (props) => {
9
+ const {globalDataContext, templateContext} = props;
10
+ const evaluatedValue = evaluateTemplateValue({valueToEvaluate: props?.args?.value, globalDataContext, templateContext});
11
+
12
+ if (typeof evaluatedValue === 'string') {
13
+ try {
14
+ // Attempt to copy to clipboard.
15
+ await navigator.clipboard.writeText(evaluatedValue.toString());
16
+ } catch (error) {
17
+ console.error('Failed to copy data to the clipboard:', error);
18
+ }
19
+ }
20
+ };
@@ -0,0 +1,23 @@
1
+ import {dataLocationToPath, evaluateTemplateValue} from "../../engine/TemplateSystem";
2
+ import {cloneDeep} from "lodash";
3
+
4
+ /**
5
+ * Sets data at the specified path.
6
+ *
7
+ * @param {{}} props
8
+ */
9
+ export const setData = (props) => {
10
+ const {globalDataContext, templateContext} = props;
11
+ const {path, value} = props.args;
12
+
13
+ if (path === undefined) {
14
+ return;
15
+ }
16
+
17
+ const dataAbsolutePath = dataLocationToPath({currentPath: templateContext.templatePath, dataLocation: path, globalDataContext, templateContext});
18
+
19
+ const evaluatedValue = evaluateTemplateValue({valueToEvaluate: value, globalDataContext, templateContext});
20
+
21
+ // We clone the value to have distinct instances when the value is an object.
22
+ globalDataContext?.updateData(typeof evaluatedValue !== "object" ? evaluatedValue : cloneDeep(evaluatedValue), dataAbsolutePath);
23
+ };