Showing
31 changed files
with
1705 additions
and
97 deletions
1 | 1 | <ul> |
2 | 2 | <li><b>msg:</b> <code>{[key: string]: any}</code> - is a Message payload key/value object. |
3 | 3 | </li> |
4 | - <li><b>metadata:</b> <code>{[key: string]: string}</code> - is a Message metadata key/value object. | |
4 | + <li><b>metadata:</b> <code>{[key: string]: string}</code> - is a Message metadata key/value map, where both keys and values are strings. | |
5 | 5 | </li> |
6 | - <li><b>msgType:</b> <code>string</code> - is a string Message type. See <a href="https://github.com/thingsboard/thingsboard/blob/ea039008b148453dfa166cf92bc40b26e487e660/ui-ngx/src/app/shared/models/rule-node.models.ts#L338" target="_blank">MessageType</a> enum for common used values. | |
6 | + <li><b>msgType:</b> <code>string</code> - is a string containing Message type. See <a href="https://github.com/thingsboard/thingsboard/blob/ea039008b148453dfa166cf92bc40b26e487e660/ui-ngx/src/app/shared/models/rule-node.models.ts#L338" target="_blank">MessageType</a> enum for common used values. | |
7 | 7 | </li> |
8 | 8 | </ul> |
9 | + | |
10 | +Enable 'debug mode' for your rule node to see the messages that arrive in near real-time. | |
11 | +See <a href="https://thingsboard.io/docs/user-guide/rule-engine-2-0/overview/#debugging" target="_blank">Debugging</a> for more information. | |
\ No newline at end of file | ... | ... |
... | ... | @@ -5,7 +5,7 @@ |
5 | 5 | |
6 | 6 | *function Filter(msg, metadata, msgType): boolean* |
7 | 7 | |
8 | -JavaScript function evaluating **true/false** condition on incoming Message. | |
8 | +JavaScript function defines a boolean expression based on the incoming Message and Metadata. | |
9 | 9 | |
10 | 10 | **Parameters:** |
11 | 11 | |
... | ... | @@ -13,19 +13,34 @@ JavaScript function evaluating **true/false** condition on incoming Message. |
13 | 13 | |
14 | 14 | **Returns:** |
15 | 15 | |
16 | -Should return `boolean` value. If `true` - send Message via **True** chain, otherwise **False** chain is used. | |
16 | +Must return a `boolean` value. If `true` - routes Message to subsequent rule nodes that are related via **True** link, | |
17 | +otherwise sends Message to rule nodes related via **False** link. | |
18 | +Uses 'Failure' link in case of any failures to evaluate the expression. | |
17 | 19 | |
18 | 20 | <div class="divider"></div> |
19 | 21 | |
20 | 22 | ##### Examples |
21 | 23 | |
22 | -* Forward all messages with `temperature` value greater than `20` to the **True** chain and all other messages to the **False** chain: | |
24 | +* Forward all messages with `temperature` value greater than `20` to the **True** link and all other messages to the **False** link. | |
25 | + Assumes that incoming messages always contain the 'temperature' field: | |
23 | 26 | |
24 | 27 | ```javascript |
25 | 28 | return msg.temperature > 20; |
26 | 29 | {:copy-code} |
27 | 30 | ``` |
28 | 31 | |
32 | + | |
33 | +Example of the rule chain configuration: | |
34 | + | |
35 | + | |
36 | + | |
37 | +* Same as above, but checks that the message has 'temperature' field to **avoid failures** on unexpected messages: | |
38 | + | |
39 | +```javascript | |
40 | +return typeof msg.temperature !== 'undefined' && msg.temperature > 20; | |
41 | +{:copy-code} | |
42 | +``` | |
43 | + | |
29 | 44 | * Forward all messages with type `ATTRIBUTES_UPDATED` to the **True** chain and all other messages to the **False** chain: |
30 | 45 | |
31 | 46 | ```javascript | ... | ... |
... | ... | @@ -5,7 +5,7 @@ |
5 | 5 | |
6 | 6 | *function Generate(prevMsg, prevMetadata, prevMsgType): {msg: object, metadata: object, msgType: string}* |
7 | 7 | |
8 | -JavaScript function generating new message using previous Message payload, Metadata and Message type as input arguments. | |
8 | +JavaScript function generating new Message using previous Message payload, Metadata and Message type as input arguments. | |
9 | 9 | |
10 | 10 | **Parameters:** |
11 | 11 | |
... | ... | @@ -24,13 +24,13 @@ Should return the object with the following structure: |
24 | 24 | |
25 | 25 | ```javascript |
26 | 26 | { |
27 | - msg?: {[key: string]: any}, | |
28 | - metadata?: {[key: string]: string}, | |
29 | - msgType?: string | |
27 | + msg: {[key: string]: any}, | |
28 | + metadata: {[key: string]: string}, | |
29 | + msgType: string | |
30 | 30 | } |
31 | 31 | ``` |
32 | 32 | |
33 | -All fields in resulting object are optional and will be taken from previously generated Message if not specified. | |
33 | +All fields in resulting object are mandatory. | |
34 | 34 | |
35 | 35 | <div class="divider"></div> |
36 | 36 | |
... | ... | @@ -39,14 +39,11 @@ All fields in resulting object are optional and will be taken from previously ge |
39 | 39 | * Generate message of type `POST_TELEMETRY_REQUEST` with random `temperature` value from `18` to `32`: |
40 | 40 | |
41 | 41 | ```javascript |
42 | -var temperature = 18 + Math.random() * 14; | |
42 | +var temperature = 18 + Math.random() * (32 - 18); | |
43 | 43 | // Round to at most 2 decimal places (optional) |
44 | 44 | temperature = Math.round( temperature * 100 ) / 100; |
45 | 45 | var msg = { temperature: temperature }; |
46 | -var metadata = {}; | |
47 | -var msgType = "POST_TELEMETRY_REQUEST"; | |
48 | - | |
49 | -return { msg: msg, metadata: metadata, msgType: msgType }; | |
46 | +return { msg: msg, metadata: {}, msgType: "POST_TELEMETRY_REQUEST" }; | |
50 | 47 | {:copy-code} |
51 | 48 | ``` |
52 | 49 | |
... | ... | @@ -62,9 +59,7 @@ and <strong>metadata</strong> with field <code>data</code> having value <code>40 |
62 | 59 | ```javascript |
63 | 60 | var msg = { temp: 42, humidity: 77 }; |
64 | 61 | var metadata = { data: 40 }; |
65 | -var msgType = "POST_TELEMETRY_REQUEST"; | |
66 | - | |
67 | -return { msg: msg, metadata: metadata, msgType: msgType }; | |
62 | +return { msg: msg, metadata: metadata, msgType: "POST_TELEMETRY_REQUEST" }; | |
68 | 63 | {:copy-code} |
69 | 64 | ``` |
70 | 65 | |
... | ... | @@ -108,9 +103,8 @@ if (isDecrement === 'true') { |
108 | 103 | |
109 | 104 | var msg = { temperature: temperature }; |
110 | 105 | var metadata = { isDecrement: isDecrement }; |
111 | -var msgType = "POST_TELEMETRY_REQUEST"; | |
112 | 106 | |
113 | -return { msg: msg, metadata: metadata, msgType: msgType }; | |
107 | +return { msg: msg, metadata: metadata, msgType: "POST_TELEMETRY_REQUEST" }; | |
114 | 108 | {:copy-code} |
115 | 109 | ``` |
116 | 110 | ... | ... |
... | ... | @@ -5,7 +5,7 @@ |
5 | 5 | |
6 | 6 | *function Switch(msg, metadata, msgType): string[]* |
7 | 7 | |
8 | -JavaScript function computing **an array of next Relation names** for incoming Message. | |
8 | +JavaScript function computing **an array of Link names** to forward the incoming Message. | |
9 | 9 | |
10 | 10 | **Parameters:** |
11 | 11 | |
... | ... | @@ -13,8 +13,9 @@ JavaScript function computing **an array of next Relation names** for incoming M |
13 | 13 | |
14 | 14 | **Returns:** |
15 | 15 | |
16 | -Should return an array of `string` values presenting **next Relation names** where Message should be routed.<br> | |
17 | -If returned array is empty - message will not be routed to any Node and discarded. | |
16 | +Should return an array of `string` values presenting **link names** that the Rule Engine should use to further route the incoming Message.<br> | |
17 | +If the result is an empty array - message will not be routed to any Node and will be immediately | |
18 | +<a href="https://thingsboard.io/docs/user-guide/rule-engine-2-0/overview/#message-processing-result" target="_blank">acknowledged</a>. | |
18 | 19 | |
19 | 20 | <div class="divider"></div> |
20 | 21 | |
... | ... | @@ -24,7 +25,7 @@ If returned array is empty - message will not be routed to any Node and discarde |
24 | 25 | <li> |
25 | 26 | Forward all messages with <code>temperature</code> value greater than <code>30</code> to the <strong>'High temperature'</strong> chain,<br> |
26 | 27 | with <code>temperature</code> value lower than <code>20</code> to the <strong>'Low temperature'</strong> chain and all other messages<br> |
27 | -to the <strong>'Normal temperature'</strong> chain: | |
28 | +to the <strong>'Other'</strong> chain: | |
28 | 29 | </li> |
29 | 30 | </ul> |
30 | 31 | |
... | ... | @@ -34,11 +35,15 @@ if (msg.temperature > 30) { |
34 | 35 | } else if (msg.temperature < 20) { |
35 | 36 | return ['Low temperature']; |
36 | 37 | } else { |
37 | - return ['Normal temperature']; | |
38 | + return ['Other']; | |
38 | 39 | } |
39 | 40 | {:copy-code} |
40 | 41 | ``` |
41 | 42 | |
43 | +Example of the rule chain configuration: | |
44 | + | |
45 | + | |
46 | + | |
42 | 47 | <ul> |
43 | 48 | <li> |
44 | 49 | For messages with type <code>POST_TELEMETRY_REQUEST</code>: | ... | ... |
... | ... | @@ -5,7 +5,7 @@ |
5 | 5 | |
6 | 6 | *function Transform(msg, metadata, msgType): {msg: object, metadata: object, msgType: string}* |
7 | 7 | |
8 | -JavaScript function transforming input Message payload, Metadata or Message type. | |
8 | +The JavaScript function to transform input Message payload, Metadata and/or Message type to the output message. | |
9 | 9 | |
10 | 10 | **Parameters:** |
11 | 11 | |
... | ... | @@ -29,11 +29,29 @@ All fields in resulting object are optional and will be taken from original mess |
29 | 29 | |
30 | 30 | ##### Examples |
31 | 31 | |
32 | -* Change message type to `CUSTOM_REQUEST`: | |
32 | +* Add sum of two fields ('a' and 'b') as a new field ('sum') of existing message: | |
33 | 33 | |
34 | 34 | ```javascript |
35 | -return { msgType: 'CUSTOM_REQUEST' }; | |
36 | -{:copy-code} | |
35 | +if(typeof msg.a !== "undefined" && typeof msg.b !== "undefined"){ | |
36 | + msg.sum = msg.a + msg.b; | |
37 | +} | |
38 | +return {msg: msg}; | |
39 | +``` | |
40 | + | |
41 | +* Transform value of the 'temperature' field from °F to °C: | |
42 | + | |
43 | +```javascript | |
44 | +msg.temperature = (msg.temperature - 32) * 5 / 9; | |
45 | +return {msg: msg}; | |
46 | +``` | |
47 | + | |
48 | +* Replace the incoming message with the new message that contains only one field - count of properties in the original message: | |
49 | + | |
50 | +```javascript | |
51 | +var newMsg = { | |
52 | + count: Object.keys(msg).length | |
53 | +}; | |
54 | +return {msg: newMsg}; | |
37 | 55 | ``` |
38 | 56 | |
39 | 57 | <ul> | ... | ... |
... | ... | @@ -17,67 +17,65 @@ A JavaScript function performing custom action. |
17 | 17 | |
18 | 18 | ##### Examples |
19 | 19 | |
20 | -* Display alert dialog with entity information: | |
21 | - | |
22 | -```javascript | |
23 | -{:code-style="max-height: 300px;"} | |
24 | -var title; | |
25 | -var content; | |
26 | -if (entityName) { | |
27 | - title = entityName + ' details'; | |
28 | - content = '<b>Entity name</b>: ' + entityName; | |
29 | - if (additionalParams && additionalParams.entity) { | |
30 | - var entity = additionalParams.entity; | |
31 | - if (entity.id) { | |
32 | - content += '<br><b>Entity type</b>: ' + entity.id.entityType; | |
33 | - } | |
34 | - if (!isNaN(entity.temperature) && entity.temperature !== '') { | |
35 | - content += '<br><b>Temperature</b>: ' + entity.temperature + ' °C'; | |
36 | - } | |
37 | - } | |
38 | -} else { | |
39 | - title = 'No entity information available'; | |
40 | - content = '<b>No entity information available</b>'; | |
41 | -} | |
42 | - | |
43 | -showAlertDialog(title, content); | |
44 | - | |
45 | -function showAlertDialog(title, content) { | |
46 | - setTimeout(function() { | |
47 | - widgetContext.dialogs.alert(title, content).subscribe(); | |
48 | - }, 100); | |
49 | -} | |
50 | -{:copy-code} | |
51 | -``` | |
52 | - | |
53 | -* Delete device after confirmation: | |
54 | - | |
55 | -```javascript | |
56 | -{:code-style="max-height: 300px;"} | |
57 | -var $injector = widgetContext.$scope.$injector; | |
58 | -var dialogs = $injector.get(widgetContext.servicesMap.get('dialogs')); | |
59 | -var deviceService = $injector.get(widgetContext.servicesMap.get('deviceService')); | |
60 | - | |
61 | -openDeleteDeviceDialog(); | |
62 | - | |
63 | -function openDeleteDeviceDialog() { | |
64 | - var title = 'Are you sure you want to delete the device ' + entityName + '?'; | |
65 | - var content = 'Be careful, after the confirmation, the device and all related data will become unrecoverable!'; | |
66 | - dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe( | |
67 | - function(result) { | |
68 | - if (result) { | |
69 | - deleteDevice(); | |
70 | - } | |
71 | - } | |
72 | - ); | |
73 | -} | |
74 | - | |
75 | -function deleteDevice() { | |
76 | - deviceService.deleteDevice(entityId.id).subscribe( | |
77 | - function() { | |
78 | - widgetContext.updateAliases(); | |
79 | - } | |
80 | - ); | |
81 | -} | |
82 | -{:copy-code} | |
83 | -``` | |
20 | +<br> | |
21 | + | |
22 | +<div style="padding-left: 32px;" | |
23 | + tb-help-popup="widget/action/examples_custom_action/custom_action_display_alert" | |
24 | + tb-help-popup-placement="top" | |
25 | + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}" | |
26 | + trigger-style="font-size: 16px;" | |
27 | + trigger-text="Display alert dialog with entity information"> | |
28 | +</div> | |
29 | + | |
30 | +<br> | |
31 | + | |
32 | +<div style="padding-left: 32px;" | |
33 | + tb-help-popup="widget/action/examples_custom_action/custom_action_delete_device_confirm" | |
34 | + tb-help-popup-placement="top" | |
35 | + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}" | |
36 | + trigger-style="font-size: 16px;" | |
37 | + trigger-text="Delete device after confirmation"> | |
38 | +</div> | |
39 | + | |
40 | +<br> | |
41 | + | |
42 | +<div style="padding-left: 32px;" | |
43 | + tb-help-popup="widget/action/examples_custom_action/custom_action_return_previous_state" | |
44 | + tb-help-popup-placement="top" | |
45 | + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}" | |
46 | + trigger-style="font-size: 16px;" | |
47 | + trigger-text="Return to the previous state"> | |
48 | +</div> | |
49 | + | |
50 | +<br> | |
51 | + | |
52 | +<div style="padding-left: 32px;" | |
53 | + tb-help-popup="widget/action/examples_custom_action/custom_action_open_state_save_parameters" | |
54 | + tb-help-popup-placement="top" | |
55 | + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}" | |
56 | + trigger-style="font-size: 16px;" | |
57 | + trigger-text="Open state conditionally with saving particular state parameters"> | |
58 | +</div> | |
59 | + | |
60 | +<br> | |
61 | + | |
62 | +<div style="padding-left: 32px;" | |
63 | + tb-help-popup="widget/action/examples_custom_action/custom_action_back_first_and_open_state" | |
64 | + tb-help-popup-placement="top" | |
65 | + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}" | |
66 | + trigger-style="font-size: 16px;" | |
67 | + trigger-text="Go back to the first state, after this go to the target state"> | |
68 | +</div> | |
69 | + | |
70 | +<br> | |
71 | + | |
72 | +<div style="padding-left: 32px;" | |
73 | + tb-help-popup="widget/action/examples_custom_action/custom_action_copy_access_token" | |
74 | + tb-help-popup-placement="top" | |
75 | + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}" | |
76 | + trigger-style="font-size: 16px;" | |
77 | + trigger-text="Copy device access token to buffer"> | |
78 | +</div> | |
79 | + | |
80 | +<br> | |
81 | +<br> | ... | ... |
... | ... | @@ -42,7 +42,7 @@ A JavaScript function performing custom action with defined HTML template to ren |
42 | 42 | <br> |
43 | 43 | |
44 | 44 | <div style="padding-left: 64px;" |
45 | - tb-help-popup="widget/action/examples/custom_pretty_create_dialog_js" | |
45 | + tb-help-popup="widget/action/examples_custom_pretty/custom_pretty_create_dialog_js" | |
46 | 46 | tb-help-popup-placement="top" |
47 | 47 | [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}" |
48 | 48 | trigger-style="font-size: 16px;" |
... | ... | @@ -52,7 +52,7 @@ A JavaScript function performing custom action with defined HTML template to ren |
52 | 52 | <br> |
53 | 53 | |
54 | 54 | <div style="padding-left: 64px;" |
55 | - tb-help-popup="widget/action/examples/custom_pretty_create_dialog_html" | |
55 | + tb-help-popup="widget/action/examples_custom_pretty/custom_pretty_create_dialog_html" | |
56 | 56 | tb-help-popup-placement="top" |
57 | 57 | [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}" |
58 | 58 | trigger-style="font-size: 16px;" |
... | ... | @@ -64,7 +64,7 @@ A JavaScript function performing custom action with defined HTML template to ren |
64 | 64 | <br> |
65 | 65 | |
66 | 66 | <div style="padding-left: 64px;" |
67 | - tb-help-popup="widget/action/examples/custom_pretty_edit_dialog_js" | |
67 | + tb-help-popup="widget/action/examples_custom_pretty/custom_pretty_edit_dialog_js" | |
68 | 68 | tb-help-popup-placement="top" |
69 | 69 | [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}" |
70 | 70 | trigger-style="font-size: 16px;" |
... | ... | @@ -74,7 +74,73 @@ A JavaScript function performing custom action with defined HTML template to ren |
74 | 74 | <br> |
75 | 75 | |
76 | 76 | <div style="padding-left: 64px;" |
77 | - tb-help-popup="widget/action/examples/custom_pretty_edit_dialog_html" | |
77 | + tb-help-popup="widget/action/examples_custom_pretty/custom_pretty_edit_dialog_html" | |
78 | + tb-help-popup-placement="top" | |
79 | + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}" | |
80 | + trigger-style="font-size: 16px;" | |
81 | + trigger-text="HTML code"> | |
82 | +</div> | |
83 | + | |
84 | +###### Display dialog to created new user | |
85 | + | |
86 | +<br> | |
87 | + | |
88 | +<div style="padding-left: 64px;" | |
89 | + tb-help-popup="widget/action/examples_custom_pretty/custom_pretty_create_user_js" | |
90 | + tb-help-popup-placement="top" | |
91 | + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}" | |
92 | + trigger-style="font-size: 16px;" | |
93 | + trigger-text="JavaScript function"> | |
94 | +</div> | |
95 | + | |
96 | +<br> | |
97 | + | |
98 | +<div style="padding-left: 64px;" | |
99 | + tb-help-popup="widget/action/examples_custom_pretty/custom_pretty_create_user_html" | |
100 | + tb-help-popup-placement="top" | |
101 | + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}" | |
102 | + trigger-style="font-size: 16px;" | |
103 | + trigger-text="HTML code"> | |
104 | +</div> | |
105 | + | |
106 | +###### Display dialog to add/edit image in entity attribute | |
107 | + | |
108 | +<br> | |
109 | + | |
110 | +<div style="padding-left: 64px;" | |
111 | + tb-help-popup="widget/action/examples_custom_pretty/custom_pretty_edit_image_js" | |
112 | + tb-help-popup-placement="top" | |
113 | + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}" | |
114 | + trigger-style="font-size: 16px;" | |
115 | + trigger-text="JavaScript function"> | |
116 | +</div> | |
117 | + | |
118 | +<br> | |
119 | + | |
120 | +<div style="padding-left: 64px;" | |
121 | + tb-help-popup="widget/action/examples_custom_pretty/custom_pretty_edit_image_html" | |
122 | + tb-help-popup-placement="top" | |
123 | + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}" | |
124 | + trigger-style="font-size: 16px;" | |
125 | + trigger-text="HTML code"> | |
126 | +</div> | |
127 | + | |
128 | +###### Display dialog to clone device | |
129 | + | |
130 | +<br> | |
131 | + | |
132 | +<div style="padding-left: 64px;" | |
133 | + tb-help-popup="widget/action/examples_custom_pretty/custom_pretty_clone_device_js" | |
134 | + tb-help-popup-placement="top" | |
135 | + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}" | |
136 | + trigger-style="font-size: 16px;" | |
137 | + trigger-text="JavaScript function"> | |
138 | +</div> | |
139 | + | |
140 | +<br> | |
141 | + | |
142 | +<div style="padding-left: 64px;" | |
143 | + tb-help-popup="widget/action/examples_custom_pretty/custom_pretty_clone_device_html" | |
78 | 144 | tb-help-popup-placement="top" |
79 | 145 | [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}" |
80 | 146 | trigger-style="font-size: 16px;" | ... | ... |
1 | +#### Function go back to the first state, after this go to the target state | |
2 | + | |
3 | +```javascript | |
4 | +{:code-style="max-height: 400px;"} | |
5 | +var stateIndex = widgetContext.stateController.getStateIndex(); | |
6 | +while (stateIndex > 0) { | |
7 | + stateIndex -= 1; | |
8 | + backToPrevState(stateIndex); | |
9 | +} | |
10 | +openDashboardState('devices'); | |
11 | + | |
12 | +function backToPrevState(stateIndex) { | |
13 | + widgetContext.stateController.navigatePrevState(stateIndex); | |
14 | +} | |
15 | + | |
16 | +function openDashboardState(statedId) { | |
17 | + var currentState = widgetContext.stateController.getStateId(); | |
18 | + if (currentState !== statedId) { | |
19 | + var params = {}; | |
20 | + widgetContext.stateController.updateState(statedId, params, false); | |
21 | + } | |
22 | +} | |
23 | +{:copy-code} | |
24 | +``` | |
25 | + | |
26 | +<br> | |
27 | +<br> | ... | ... |
ui-ngx/src/assets/help/en_US/widget/action/examples_custom_action/custom_action_copy_access_token.md
0 → 100644
1 | +#### Function copy device access token to buffer | |
2 | + | |
3 | +```javascript | |
4 | +{:code-style="max-height: 400px;"} | |
5 | +var $injector = widgetContext.$scope.$injector; | |
6 | +var deviceService = $injector.get(widgetContext.servicesMap.get('deviceService')); | |
7 | +var $translate = $injector.get(widgetContext.servicesMap.get('translate')); | |
8 | +var $scope = widgetContext.$scope; | |
9 | +if (entityId.id && entityId.entityType === 'DEVICE') { | |
10 | + deviceService.getDeviceCredentials(entityId.id, true).subscribe( | |
11 | + (deviceCredentials) => { | |
12 | + var credentialsId = deviceCredentials.credentialsId; | |
13 | + if (copyToClipboard(credentialsId)) { | |
14 | + $scope.showSuccessToast($translate.instant('device.accessTokenCopiedMessage'), 750, "top", "left"); | |
15 | + } | |
16 | + } | |
17 | + ); | |
18 | +} | |
19 | + | |
20 | +function copyToClipboard(text) { | |
21 | + if (window.clipboardData && window.clipboardData.setData) { | |
22 | + return window.clipboardData.setData("Text", text); | |
23 | + } | |
24 | + else if (document.queryCommandSupported && document.queryCommandSupported("copy")) { | |
25 | + var textarea = document.createElement("textarea"); | |
26 | + textarea.textContent = text; | |
27 | + textarea.style.position = "fixed"; | |
28 | + document.body.appendChild(textarea); | |
29 | + textarea.select(); | |
30 | + try { | |
31 | + return document.execCommand("copy"); | |
32 | + } | |
33 | + catch (ex) { | |
34 | + console.warn("Copy to clipboard failed.", ex); | |
35 | + return false; | |
36 | + } | |
37 | + document.body.removeChild(textarea); | |
38 | + } | |
39 | +} | |
40 | +{:copy-code} | |
41 | +``` | |
42 | + | |
43 | +<br> | |
44 | +<br> | ... | ... |
1 | +#### Function delete device after confirmation | |
2 | + | |
3 | +```javascript | |
4 | +var $injector = widgetContext.$scope.$injector; | |
5 | +var dialogs = $injector.get(widgetContext.servicesMap.get('dialogs')); | |
6 | +var deviceService = $injector.get(widgetContext.servicesMap.get('deviceService')); | |
7 | + | |
8 | +openDeleteDeviceDialog(); | |
9 | + | |
10 | +function openDeleteDeviceDialog() { | |
11 | + var title = 'Are you sure you want to delete the device ' + entityName + '?'; | |
12 | + var content = 'Be careful, after the confirmation, the device and all related data will become unrecoverable!'; | |
13 | + dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe( | |
14 | + function(result) { | |
15 | + if (result) { | |
16 | + deleteDevice(); | |
17 | + } | |
18 | + } | |
19 | + ); | |
20 | +} | |
21 | + | |
22 | +function deleteDevice() { | |
23 | + deviceService.deleteDevice(entityId.id).subscribe( | |
24 | + function() { | |
25 | + widgetContext.updateAliases(); | |
26 | + } | |
27 | + ); | |
28 | +} | |
29 | +{:copy-code} | |
30 | +``` | |
31 | + | |
32 | +<br> | |
33 | +<br> | ... | ... |
ui-ngx/src/assets/help/en_US/widget/action/examples_custom_action/custom_action_display_alert.md
0 → 100644
1 | +#### Function display alert dialog with entity information | |
2 | + | |
3 | +```javascript | |
4 | +{:code-style="max-height: 400px;"} | |
5 | +var title; | |
6 | +var content; | |
7 | +if (entityName) { | |
8 | + title = entityName + ' details'; | |
9 | + content = '<b>Entity name</b>: ' + entityName; | |
10 | + if (additionalParams && additionalParams.entity) { | |
11 | + var entity = additionalParams.entity; | |
12 | + if (entity.id) { | |
13 | + content += '<br><b>Entity type</b>: ' + entity.id.entityType; | |
14 | + } | |
15 | + if (!isNaN(entity.temperature) && entity.temperature !== '') { | |
16 | + content += '<br><b>Temperature</b>: ' + entity.temperature + ' °C'; | |
17 | + } | |
18 | + } | |
19 | +} else { | |
20 | + title = 'No entity information available'; | |
21 | + content = '<b>No entity information available</b>'; | |
22 | +} | |
23 | + | |
24 | +showAlertDialog(title, content); | |
25 | + | |
26 | +function showAlertDialog(title, content) { | |
27 | + setTimeout(function() { | |
28 | + widgetContext.dialogs.alert(title, content).subscribe(); | |
29 | + }, 100); | |
30 | +} | |
31 | +{:copy-code} | |
32 | +``` | |
33 | + | |
34 | +<br> | |
35 | +<br> | ... | ... |
1 | +#### Function open state conditionally with saving particular state parameters | |
2 | + | |
3 | +```javascript | |
4 | +{:code-style="max-height: 400px;"} | |
5 | +var entitySubType; | |
6 | +var $injector = widgetContext.$scope.$injector; | |
7 | +$injector.get(widgetContext.servicesMap.get('entityService')).getEntity(entityId.entityType, entityId.id) | |
8 | + .subscribe(function(data) { | |
9 | + entitySubType = data.type; | |
10 | + if (entitySubType == 'energy meter') { | |
11 | + openDashboardStates('energy_meter_details_view'); | |
12 | + } else if (entitySubType == 'thermometer') { | |
13 | + openDashboardStates('thermometer_details_view'); | |
14 | + } | |
15 | + }); | |
16 | + | |
17 | +function openDashboardStates(statedId) { | |
18 | + var stateParams = widgetContext.stateController.getStateParams(); | |
19 | + var params = { | |
20 | + entityId: entityId, | |
21 | + entityName: entityName | |
22 | + }; | |
23 | + | |
24 | + if (stateParams.city) { | |
25 | + params.city = stateParams.city; | |
26 | + } | |
27 | + | |
28 | + widgetContext.stateController.openState(statedId, params, false); | |
29 | +} | |
30 | +{:copy-code} | |
31 | +``` | |
32 | + | |
33 | +<br> | |
34 | +<br> | ... | ... |
1 | +#### Function return to the previous state | |
2 | + | |
3 | +```javascript | |
4 | +{:code-style="max-height: 400px;"} | |
5 | +let stateIndex = widgetContext.stateController.getStateIndex(); | |
6 | +if (stateIndex > 0) { | |
7 | + stateIndex -= 1; | |
8 | + backToPrevState(stateIndex); | |
9 | +} | |
10 | + | |
11 | +function backToPrevState(stateIndex) { | |
12 | + widgetContext.stateController.navigatePrevState(stateIndex); | |
13 | +} | |
14 | +{:copy-code} | |
15 | +``` | |
16 | + | |
17 | +<br> | |
18 | +<br> | ... | ... |
ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_clone_device_html.md
0 → 100644
1 | +#### HTML template of dialog to clone device | |
2 | + | |
3 | +```html | |
4 | +{:code-style="max-height: 400px;"} | |
5 | +<form [formGroup]="cloneDeviceFormGroup" (ngSubmit)="save()" style="min-width:320px;"> | |
6 | + <mat-toolbar fxLayout="row" color="primary"> | |
7 | + <h2>Clone device: {{ deviceName }}</h2> | |
8 | + <span fxFlex></span> | |
9 | + <button mat-icon-button (click)="cancel()" | |
10 | + type="button"> | |
11 | + <mat-icon class="material-icons">close | |
12 | + </mat-icon> | |
13 | + </button> | |
14 | + </mat-toolbar> | |
15 | + <mat-progress-bar color="warn" mode="indeterminate" | |
16 | + *ngIf="isLoading$ | async"> | |
17 | + </mat-progress-bar> | |
18 | + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> | |
19 | + <div mat-dialog-content fxLayout="column"> | |
20 | + <mat-form-field fxFlex class="mat-block"> | |
21 | + <mat-label>Clone device name</mat-label> | |
22 | + <input matInput formControlName="cloneName" required> | |
23 | + <mat-error *ngIf="cloneDeviceFormGroup.get('cloneName').hasError('required')"> | |
24 | + Clone device name is required | |
25 | + </mat-error> | |
26 | + </mat-form-field> | |
27 | + </div> | |
28 | + <div mat-dialog-actions fxLayout="row" | |
29 | + fxLayoutAlign="end center"> | |
30 | + <button mat-button color="primary" type="button" | |
31 | + [disabled]="(isLoading$ | async)" | |
32 | + (click)="cancel()" cdkFocusInitial> | |
33 | + Cancel | |
34 | + </button> | |
35 | + <button mat-button mat-raised-button color="primary" | |
36 | + type="submit" | |
37 | + [disabled]="(isLoading$ | async) || cloneDeviceFormGroup.invalid || !cloneDeviceFormGroup.dirty"> | |
38 | + Save | |
39 | + </button> | |
40 | + </div> | |
41 | +</form> | |
42 | +{:copy-code} | |
43 | +``` | |
44 | + | |
45 | +<br> | |
46 | +<br> | ... | ... |
ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_clone_device_js.md
0 → 100644
1 | +#### Function displaying dialog to clone device | |
2 | + | |
3 | +```javascript | |
4 | +{:code-style="max-height: 400px;"} | |
5 | +const $injector = widgetContext.$scope.$injector; | |
6 | +const customDialog = $injector.get(widgetContext.servicesMap.get('customDialog')); | |
7 | +const attributeService = $injector.get(widgetContext.servicesMap.get('attributeService')); | |
8 | +const deviceService = $injector.get(widgetContext.servicesMap.get('deviceService')); | |
9 | +const rxjs = widgetContext.rxjs; | |
10 | + | |
11 | +openCloneDeviceDialog(); | |
12 | + | |
13 | +function openCloneDeviceDialog() { | |
14 | + customDialog.customDialog(htmlTemplate, CloneDeviceDialogController).subscribe(); | |
15 | +} | |
16 | + | |
17 | +function CloneDeviceDialogController(instance) { | |
18 | + let vm = instance; | |
19 | + vm.deviceName = entityName; | |
20 | + | |
21 | + vm.cloneDeviceFormGroup = vm.fb.group({ | |
22 | + cloneName: ['', [vm.validators.required]] | |
23 | + }); | |
24 | + | |
25 | + vm.save = function() { | |
26 | + deviceService.getDevice(entityId.id).pipe( | |
27 | + rxjs.mergeMap((origDevice) => { | |
28 | + let cloneDevice = { | |
29 | + name: vm.cloneDeviceFormGroup.get('cloneName').value, | |
30 | + type: origDevice.type | |
31 | + }; | |
32 | + return deviceService.saveDevice(cloneDevice).pipe( | |
33 | + rxjs.mergeMap((newDevice) => { | |
34 | + return attributeService.getEntityAttributes(origDevice.id, 'SERVER_SCOPE').pipe( | |
35 | + rxjs.mergeMap((origAttributes) => { | |
36 | + return attributeService.saveEntityAttributes(newDevice.id, 'SERVER_SCOPE', origAttributes); | |
37 | + }) | |
38 | + ); | |
39 | + }) | |
40 | + ); | |
41 | + }) | |
42 | + ).subscribe(() => { | |
43 | + widgetContext.updateAliases(); | |
44 | + vm.dialogRef.close(null); | |
45 | + }); | |
46 | + }; | |
47 | + | |
48 | + vm.cancel = function() { | |
49 | + vm.dialogRef.close(null); | |
50 | + }; | |
51 | +} | |
52 | +{:copy-code} | |
53 | +``` | |
54 | + | |
55 | +<br> | |
56 | +<br> | ... | ... |
1 | +#### HTML template of dialog to create a device or an asset | |
2 | + | |
3 | +```html | |
4 | +{:code-style="max-height: 400px;"} | |
5 | +<form #addEntityForm="ngForm" [formGroup]="addEntityFormGroup" | |
6 | + (ngSubmit)="save()" class="add-entity-form"> | |
7 | + <mat-toolbar fxLayout="row" color="primary"> | |
8 | + <h2>Add entity</h2> | |
9 | + <span fxFlex></span> | |
10 | + <button mat-icon-button (click)="cancel()" type="button"> | |
11 | + <mat-icon class="material-icons">close</mat-icon> | |
12 | + </button> | |
13 | + </mat-toolbar> | |
14 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | |
15 | + </mat-progress-bar> | |
16 | + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> | |
17 | + <div mat-dialog-content fxLayout="column"> | |
18 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> | |
19 | + <mat-form-field fxFlex class="mat-block"> | |
20 | + <mat-label>Entity Name</mat-label> | |
21 | + <input matInput formControlName="entityName" required> | |
22 | + <mat-error *ngIf="addEntityFormGroup.get('entityName').hasError('required')"> | |
23 | + Entity name is required. | |
24 | + </mat-error> | |
25 | + </mat-form-field> | |
26 | + <mat-form-field fxFlex class="mat-block"> | |
27 | + <mat-label>Entity Label</mat-label> | |
28 | + <input matInput formControlName="entityLabel" > | |
29 | + </mat-form-field> | |
30 | + </div> | |
31 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> | |
32 | + <tb-entity-type-select | |
33 | + class="mat-block" | |
34 | + formControlName="entityType" | |
35 | + [showLabel]="true" | |
36 | + [allowedEntityTypes]="allowedEntityTypes" | |
37 | + ></tb-entity-type-select> | |
38 | + <tb-entity-subtype-autocomplete | |
39 | + fxFlex *ngIf="addEntityFormGroup.get('entityType').value == 'ASSET'" | |
40 | + class="mat-block" | |
41 | + formControlName="type" | |
42 | + [required]="true" | |
43 | + [entityType]="'ASSET'" | |
44 | + ></tb-entity-subtype-autocomplete> | |
45 | + <tb-entity-subtype-autocomplete | |
46 | + fxFlex *ngIf="addEntityFormGroup.get('entityType').value != 'ASSET'" | |
47 | + class="mat-block" | |
48 | + formControlName="type" | |
49 | + [required]="true" | |
50 | + [entityType]="'DEVICE'" | |
51 | + ></tb-entity-subtype-autocomplete> | |
52 | + </div> | |
53 | + <div formGroupName="attributes" fxLayout="column"> | |
54 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> | |
55 | + <mat-form-field fxFlex class="mat-block"> | |
56 | + <mat-label>Latitude</mat-label> | |
57 | + <input type="number" step="any" matInput formControlName="latitude"> | |
58 | + </mat-form-field> | |
59 | + <mat-form-field fxFlex class="mat-block"> | |
60 | + <mat-label>Longitude</mat-label> | |
61 | + <input type="number" step="any" matInput formControlName="longitude"> | |
62 | + </mat-form-field> | |
63 | + </div> | |
64 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> | |
65 | + <mat-form-field fxFlex class="mat-block"> | |
66 | + <mat-label>Address</mat-label> | |
67 | + <input matInput formControlName="address"> | |
68 | + </mat-form-field> | |
69 | + <mat-form-field fxFlex class="mat-block"> | |
70 | + <mat-label>Owner</mat-label> | |
71 | + <input matInput formControlName="owner"> | |
72 | + </mat-form-field> | |
73 | + </div> | |
74 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> | |
75 | + <mat-form-field fxFlex class="mat-block"> | |
76 | + <mat-label>Integer Value</mat-label> | |
77 | + <input type="number" step="1" matInput formControlName="number"> | |
78 | + <mat-error *ngIf="addEntityFormGroup.get('attributes.number').hasError('pattern')"> | |
79 | + Invalid integer value. | |
80 | + </mat-error> | |
81 | + </mat-form-field> | |
82 | + <div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex> | |
83 | + <label class="checkbox-label">Boolean Value</label> | |
84 | + <mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;"> | |
85 | + | |
86 | + </mat-checkbox> | |
87 | + </div> | |
88 | + </div> | |
89 | + </div> | |
90 | + <div class="relations-list"> | |
91 | + <div class="mat-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">Relations</div> | |
92 | + <div class="body" [fxShow]="relations().length"> | |
93 | + <div class="row" fxLayout="row" fxLayoutAlign="start center" formArrayName="relations" *ngFor="let relation of relations().controls; let i = index;"> | |
94 | + <div [formGroupName]="i" class="mat-elevation-z2" fxFlex fxLayout="row" style="padding: 5px 0 5px 5px;"> | |
95 | + <div fxFlex fxLayout="column"> | |
96 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> | |
97 | + <mat-form-field class="mat-block" style="min-width: 100px;"> | |
98 | + <mat-label>Direction</mat-label> | |
99 | + <mat-select formControlName="direction" name="direction"> | |
100 | + <mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value"> | |
101 | + | |
102 | + </mat-option> | |
103 | + </mat-select> | |
104 | + <mat-error *ngIf="relation.get('direction').hasError('required')"> | |
105 | + Relation direction is required. | |
106 | + </mat-error> | |
107 | + </mat-form-field> | |
108 | + <tb-relation-type-autocomplete | |
109 | + fxFlex class="mat-block" | |
110 | + formControlName="relationType" | |
111 | + [required]="true"> | |
112 | + </tb-relation-type-autocomplete> | |
113 | + </div> | |
114 | + <div fxLayout="row" fxLayout.xs="column"> | |
115 | + <tb-entity-select | |
116 | + fxFlex class="mat-block" | |
117 | + [required]="true" | |
118 | + formControlName="relatedEntity"> | |
119 | + </tb-entity-select> | |
120 | + </div> | |
121 | + </div> | |
122 | + <div fxLayout="column" fxLayoutAlign="center center"> | |
123 | + <button mat-icon-button color="primary" | |
124 | + aria-label="Remove" | |
125 | + type="button" | |
126 | + (click)="removeRelation(i)" | |
127 | + matTooltip="Remove relation" | |
128 | + matTooltipPosition="above"> | |
129 | + <mat-icon>close</mat-icon> | |
130 | + </button> | |
131 | + </div> | |
132 | + </div> | |
133 | + </div> | |
134 | + </div> | |
135 | + <div> | |
136 | + <button mat-raised-button color="primary" | |
137 | + type="button" | |
138 | + (click)="addRelation()" | |
139 | + matTooltip="Add Relation" | |
140 | + matTooltipPosition="above"> | |
141 | + Add | |
142 | + </button> | |
143 | + </div> | |
144 | + </div> | |
145 | + </div> | |
146 | + <div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center"> | |
147 | + <button mat-button color="primary" | |
148 | + type="button" | |
149 | + [disabled]="(isLoading$ | async)" | |
150 | + (click)="cancel()" cdkFocusInitial> | |
151 | + Cancel | |
152 | + </button> | |
153 | + <button mat-button mat-raised-button color="primary" | |
154 | + type="submit" | |
155 | + [disabled]="(isLoading$ | async) || addEntityForm.invalid || !addEntityForm.dirty"> | |
156 | + Create | |
157 | + </button> | |
158 | + </div> | |
159 | +</form> | |
160 | +{:copy-code} | |
161 | +``` | |
162 | + | |
163 | +<br> | |
164 | +<br> | ... | ... |
ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_create_dialog_js.md
0 → 100644
1 | +#### Function displaying dialog to create a device or an asset | |
2 | + | |
3 | +```javascript | |
4 | +{:code-style="max-height: 400px;"} | |
5 | +let $injector = widgetContext.$scope.$injector; | |
6 | +let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog')); | |
7 | +let assetService = $injector.get(widgetContext.servicesMap.get('assetService')); | |
8 | +let deviceService = $injector.get(widgetContext.servicesMap.get('deviceService')); | |
9 | +let attributeService = $injector.get(widgetContext.servicesMap.get('attributeService')); | |
10 | +let entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService')); | |
11 | + | |
12 | +openAddEntityDialog(); | |
13 | + | |
14 | +function openAddEntityDialog() { | |
15 | + customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe(); | |
16 | +} | |
17 | + | |
18 | +function AddEntityDialogController(instance) { | |
19 | + let vm = instance; | |
20 | + | |
21 | + vm.allowedEntityTypes = ['ASSET', 'DEVICE']; | |
22 | + vm.entitySearchDirection = { | |
23 | + from: "FROM", | |
24 | + to: "TO" | |
25 | + } | |
26 | + | |
27 | + vm.addEntityFormGroup = vm.fb.group({ | |
28 | + entityName: ['', [vm.validators.required]], | |
29 | + entityType: ['DEVICE'], | |
30 | + entityLabel: [null], | |
31 | + type: ['', [vm.validators.required]], | |
32 | + attributes: vm.fb.group({ | |
33 | + latitude: [null], | |
34 | + longitude: [null], | |
35 | + address: [null], | |
36 | + owner: [null], | |
37 | + number: [null, [vm.validators.pattern(/^-?[0-9]+$/)]], | |
38 | + booleanValue: [null] | |
39 | + }), | |
40 | + relations: vm.fb.array([]) | |
41 | + }); | |
42 | + | |
43 | + vm.cancel = function () { | |
44 | + vm.dialogRef.close(null); | |
45 | + }; | |
46 | + | |
47 | + vm.relations = function () { | |
48 | + return vm.addEntityFormGroup.get('relations'); | |
49 | + }; | |
50 | + | |
51 | + vm.addRelation = function () { | |
52 | + vm.relations().push(vm.fb.group({ | |
53 | + relatedEntity: [null, [vm.validators.required]], | |
54 | + relationType: [null, [vm.validators.required]], | |
55 | + direction: [null, [vm.validators.required]] | |
56 | + })); | |
57 | + }; | |
58 | + | |
59 | + vm.removeRelation = function (index) { | |
60 | + vm.relations().removeAt(index); | |
61 | + vm.relations().markAsDirty(); | |
62 | + }; | |
63 | + | |
64 | + vm.save = function () { | |
65 | + vm.addEntityFormGroup.markAsPristine(); | |
66 | + saveEntityObservable().subscribe( | |
67 | + function (entity) { | |
68 | + widgetContext.rxjs.forkJoin([ | |
69 | + saveAttributes(entity.id), | |
70 | + saveRelations(entity.id) | |
71 | + ]).subscribe( | |
72 | + function () { | |
73 | + widgetContext.updateAliases(); | |
74 | + vm.dialogRef.close(null); | |
75 | + } | |
76 | + ); | |
77 | + } | |
78 | + ); | |
79 | + }; | |
80 | + | |
81 | + function saveEntityObservable() { | |
82 | + const formValues = vm.addEntityFormGroup.value; | |
83 | + let entity = { | |
84 | + name: formValues.entityName, | |
85 | + type: formValues.type, | |
86 | + label: formValues.entityLabel | |
87 | + }; | |
88 | + if (formValues.entityType == 'ASSET') { | |
89 | + return assetService.saveAsset(entity); | |
90 | + } else if (formValues.entityType == 'DEVICE') { | |
91 | + return deviceService.saveDevice(entity); | |
92 | + } | |
93 | + } | |
94 | + | |
95 | + function saveAttributes(entityId) { | |
96 | + let attributes = vm.addEntityFormGroup.get('attributes').value; | |
97 | + let attributesArray = []; | |
98 | + for (let key in attributes) { | |
99 | + if (attributes[key] !== null) { | |
100 | + attributesArray.push({key: key, value: attributes[key]}); | |
101 | + } | |
102 | + } | |
103 | + if (attributesArray.length > 0) { | |
104 | + return attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributesArray); | |
105 | + } | |
106 | + return widgetContext.rxjs.of([]); | |
107 | + } | |
108 | + | |
109 | + function saveRelations(entityId) { | |
110 | + let relations = vm.addEntityFormGroup.get('relations').value; | |
111 | + let tasks = []; | |
112 | + for (let i = 0; i < relations.length; i++) { | |
113 | + let relation = { | |
114 | + type: relations[i].relationType, | |
115 | + typeGroup: 'COMMON' | |
116 | + }; | |
117 | + if (relations[i].direction == 'FROM') { | |
118 | + relation.to = relations[i].relatedEntity; | |
119 | + relation.from = entityId; | |
120 | + } else { | |
121 | + relation.to = entityId; | |
122 | + relation.from = relations[i].relatedEntity; | |
123 | + } | |
124 | + tasks.push(entityRelationService.saveRelation(relation)); | |
125 | + } | |
126 | + if (tasks.length > 0) { | |
127 | + return widgetContext.rxjs.forkJoin(tasks); | |
128 | + } | |
129 | + return widgetContext.rxjs.of([]); | |
130 | + } | |
131 | +} | |
132 | +{:copy-code} | |
133 | +``` | |
134 | + | |
135 | +<br> | |
136 | +<br> | ... | ... |
ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_create_user_html.md
0 → 100644
1 | +#### HTML template of dialog to create new user | |
2 | + | |
3 | +```html | |
4 | +{:code-style="max-height: 400px;"} | |
5 | +<form [formGroup]="addEntityFormGroup" (ngSubmit)="save()" style="min-width:480px;"> | |
6 | + <mat-toolbar fxLayout="row" color="primary"> | |
7 | + <h2>Add new User</h2> | |
8 | + <span fxFlex></span> | |
9 | + <button mat-icon-button (click)="cancel()" type="button"> | |
10 | + <mat-icon class="material-icons">close</mat-icon> | |
11 | + </button> | |
12 | + </mat-toolbar> | |
13 | + <mat-progress-bar color="warn" mode="indeterminate" | |
14 | + *ngIf="isLoading$ | async"> | |
15 | + </mat-progress-bar> | |
16 | + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"> | |
17 | + </div> | |
18 | + <div mat-dialog-content fxLayout="column"> | |
19 | + <div fxLayout="row" fxLayoutGap="8px" | |
20 | + fxLayout.xs="column" fxLayoutGap.xs="0"> | |
21 | + <mat-form-field fxFlex class="mat-block"> | |
22 | + <mat-label>Email</mat-label> | |
23 | + <input matInput formControlName="email" required | |
24 | + ng-pattern='/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\_\-0-9]+\.)+[a-zA-Z]{2,}))$/'> | |
25 | + <mat-error *ngIf="addEntityFormGroup.get('email').hasError('required')"> | |
26 | + Email is required | |
27 | + </mat-error> | |
28 | + <mat-error *ngIf="addEntityFormGroup.get('email').hasError('pattern')"> | |
29 | + Invalid email format | |
30 | + </mat-error> | |
31 | + </mat-form-field> | |
32 | + </div> | |
33 | + <div fxLayout="row" fxLayoutGap="8px" | |
34 | + fxLayout.xs="column" fxLayoutGap.xs="0"> | |
35 | + <mat-form-field fxFlex class="mat-block"> | |
36 | + <mat-label>First Name</mat-label> | |
37 | + <input matInput | |
38 | + formControlName="firstName"> | |
39 | + </mat-form-field> | |
40 | + </div> | |
41 | + <div fxLayout="row" fxLayoutGap="8px" | |
42 | + fxLayout.xs="column" fxLayoutGap.xs="0"> | |
43 | + <mat-form-field fxFlex class="mat-block"> | |
44 | + <mat-label>Last Name</mat-label> | |
45 | + <input matInput | |
46 | + formControlName="lastName"> | |
47 | + </mat-form-field> | |
48 | + </div> | |
49 | + <div fxLayout="row" fxLayoutGap="8px" | |
50 | + fxLayout.xs="column" fxLayoutGap.xs="0"> | |
51 | + <mat-form-field fxFlex class="mat-block"> | |
52 | + <mat-label>User activation method</mat-label> | |
53 | + <mat-select matInput formControlName="userActivationMethod"> | |
54 | + <mat-option *ngFor="let activationMethod of activationMethods" [value]="activationMethod.value"> | |
55 | + {{activationMethod.name}} | |
56 | + </mat-option> | |
57 | + </mat-select> | |
58 | + <mat-error *ngIf="addEntityFormGroup.get('userActivationMethod').hasError('required')">Please choose activation method</mat-error> | |
59 | + <mat-hint>e.g. Send activation email</mat-hint> | |
60 | + </mat-form-field> | |
61 | + </div> | |
62 | + </div> | |
63 | + <div mat-dialog-actions fxLayout="row" | |
64 | + fxLayoutAlign="end center"> | |
65 | + <button mat-button color="primary" type="button" | |
66 | + [disabled]="(isLoading$ | async)" | |
67 | + (click)="cancel()" cdkFocusInitial> | |
68 | + Cancel | |
69 | + </button> | |
70 | + <button mat-button mat-raised-button color="primary" | |
71 | + type="submit" | |
72 | + [disabled]="(isLoading$ | async) || addEntityFormGroup.invalid || !addEntityFormGroup.dirty"> | |
73 | + Create | |
74 | + </button> | |
75 | + </div> | |
76 | +</form> | |
77 | +{:copy-code} | |
78 | +``` | |
79 | + | |
80 | +<br> | |
81 | +<br> | ... | ... |
ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_create_user_js.md
0 → 100644
1 | +#### Function displaying dialog to create new user | |
2 | + | |
3 | +```javascript | |
4 | +{:code-style="max-height: 400px;"} | |
5 | +const $injector = widgetContext.$scope.$injector; | |
6 | +const customDialog = $injector.get(widgetContext.servicesMap.get('customDialog')); | |
7 | +const userService = $injector.get(widgetContext.servicesMap.get('userService')); | |
8 | +const $scope = widgetContext.$scope; | |
9 | +const rxjs = widgetContext.rxjs; | |
10 | + | |
11 | +openAddUserDialog(); | |
12 | + | |
13 | +function openAddUserDialog() { | |
14 | + customDialog.customDialog(htmlTemplate, AddUserDialogController).subscribe(); | |
15 | +} | |
16 | + | |
17 | +function AddUserDialogController(instance) { | |
18 | + let vm = instance; | |
19 | + | |
20 | + vm.currentUser = widgetContext.currentUser; | |
21 | + | |
22 | + vm.addEntityFormGroup = vm.fb.group({ | |
23 | + email: ['', [vm.validators.required, vm.validators.pattern(/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\_\-0-9]+\.)+[a-zA-Z]{2,}))$/)]], | |
24 | + firstName: [''], | |
25 | + lastName: ['', ], | |
26 | + userActivationMethod: ['', [vm.validators.required]] | |
27 | + }); | |
28 | + | |
29 | + vm.activationMethods = [ | |
30 | + { | |
31 | + value: 'displayActivationLink', | |
32 | + name: 'Display activation link' | |
33 | + }, | |
34 | + { | |
35 | + value: 'sendActivationMail', | |
36 | + name: 'Send activation email' | |
37 | + } | |
38 | + ]; | |
39 | + | |
40 | + vm.cancel = function() { | |
41 | + vm.dialogRef.close(null); | |
42 | + }; | |
43 | + | |
44 | + vm.save = function() { | |
45 | + let formObj = vm.addEntityFormGroup.getRawValue(); | |
46 | + let attributes = []; | |
47 | + let sendActivationMail = false; | |
48 | + let newUser = { | |
49 | + email: formObj.email, | |
50 | + firstName: formObj.firstName, | |
51 | + lastName: formObj.lastName, | |
52 | + authority: 'TENANT_ADMIN' | |
53 | + }; | |
54 | + | |
55 | + if (formObj.userActivationMethod === 'sendActivationMail') { | |
56 | + sendActivationMail = true; | |
57 | + } | |
58 | + | |
59 | + userService.saveUser(newUser, sendActivationMail).pipe( | |
60 | + rxjs.mergeMap((user) => { | |
61 | + let activationObs; | |
62 | + if (sendActivationMail) { | |
63 | + activationObs = rxjs.of(null); | |
64 | + } else { | |
65 | + activationObs = userService.getActivationLink(user.id.id); | |
66 | + } | |
67 | + return activationObs.pipe( | |
68 | + rxjs.mergeMap((activationLink) => { | |
69 | + return activationLink ? customDialog.customDialog(activationLinkDialogTemplate, ActivationLinkDialogController, {"activationLink": activationLink}) : rxjs.of(null); | |
70 | + }) | |
71 | + ); | |
72 | + }) | |
73 | + ).subscribe(() => { | |
74 | + vm.dialogRef.close(null); | |
75 | + }); | |
76 | + }; | |
77 | +} | |
78 | + | |
79 | +function ActivationLinkDialogController(instance) { | |
80 | + let vm = instance; | |
81 | + | |
82 | + vm.activationLink = vm.data.activationLink; | |
83 | + | |
84 | + vm.onActivationLinkCopied = () => { | |
85 | + $scope.showSuccessToast("User activation link has been copied to clipboard", 1200, "bottom", "left", "activationLinkDialogContent"); | |
86 | + }; | |
87 | + | |
88 | + vm.close = () => { | |
89 | + vm.dialogRef.close(null); | |
90 | + }; | |
91 | +} | |
92 | + | |
93 | +let activationLinkDialogTemplate = `<form style="min-width: 400px; position: relative;"> | |
94 | + <mat-toolbar color="primary"> | |
95 | + <h2 translate>user.activation-link</h2> | |
96 | + <span fxFlex></span> | |
97 | + <button mat-button mat-icon-button | |
98 | + (click)="close()" | |
99 | + type="button"> | |
100 | + <mat-icon class="material-icons">close</mat-icon> | |
101 | + </button> | |
102 | + </mat-toolbar> | |
103 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | |
104 | + </mat-progress-bar> | |
105 | + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> | |
106 | + <div mat-dialog-content tb-toast toastTarget="activationLinkDialogContent"> | |
107 | + <div class="mat-content" fxLayout="column"> | |
108 | + <span [innerHTML]="'user.activation-link-text' | translate: {activationLink: activationLink}"></span> | |
109 | + <div fxLayout="row" fxLayoutAlign="start center"> | |
110 | + <pre class="tb-highlight" fxFlex><code>{{ activationLink }}</code></pre> | |
111 | + <button mat-icon-button | |
112 | + color="primary" | |
113 | + ngxClipboard | |
114 | + cbContent="{{ activationLink }}" | |
115 | + (cbOnSuccess)="onActivationLinkCopied()" | |
116 | + matTooltip="{{ 'user.copy-activation-link' | translate }}" | |
117 | + matTooltipPosition="above"> | |
118 | + <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon> | |
119 | + </button> | |
120 | + </div> | |
121 | + </div> | |
122 | + </div> | |
123 | + <div mat-dialog-actions fxLayoutAlign="end center"> | |
124 | + <button mat-button color="primary" | |
125 | + type="button" | |
126 | + cdkFocusInitial | |
127 | + [disabled]="(isLoading$ | async)" | |
128 | + (click)="close()"> | |
129 | + {{ 'action.ok' | translate }} | |
130 | + </button> | |
131 | + </div> | |
132 | +</form>`; | |
133 | +{:copy-code} | |
134 | +``` | |
135 | + | |
136 | +<br> | |
137 | +<br> | ... | ... |
ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_edit_dialog_html.md
0 → 100644
1 | +#### HTML template of dialog to edit a device or an asset | |
2 | + | |
3 | +```html | |
4 | +{:code-style="max-height: 400px;"} | |
5 | +<form #editEntityForm="ngForm" [formGroup]="editEntityFormGroup" | |
6 | + (ngSubmit)="save()" class="edit-entity-form"> | |
7 | + <mat-toolbar fxLayout="row" color="primary"> | |
8 | + <h2>Edit </h2> | |
9 | + <span fxFlex></span> | |
10 | + <button mat-icon-button (click)="cancel()" type="button"> | |
11 | + <mat-icon class="material-icons">close</mat-icon> | |
12 | + </button> | |
13 | + </mat-toolbar> | |
14 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | |
15 | + </mat-progress-bar> | |
16 | + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> | |
17 | + <div mat-dialog-content fxLayout="column"> | |
18 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> | |
19 | + <mat-form-field fxFlex class="mat-block"> | |
20 | + <mat-label>Entity Name</mat-label> | |
21 | + <input matInput formControlName="entityName" required readonly=""> | |
22 | + </mat-form-field> | |
23 | + <mat-form-field fxFlex class="mat-block"> | |
24 | + <mat-label>Entity Label</mat-label> | |
25 | + <input matInput formControlName="entityLabel"> | |
26 | + </mat-form-field> | |
27 | + </div> | |
28 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> | |
29 | + <mat-form-field fxFlex class="mat-block"> | |
30 | + <mat-label>Entity Type</mat-label> | |
31 | + <input matInput formControlName="entityType" readonly> | |
32 | + </mat-form-field> | |
33 | + <mat-form-field fxFlex class="mat-block"> | |
34 | + <mat-label>Type</mat-label> | |
35 | + <input matInput formControlName="type" readonly> | |
36 | + </mat-form-field> | |
37 | + </div> | |
38 | + <div formGroupName="attributes" fxLayout="column"> | |
39 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> | |
40 | + <mat-form-field fxFlex class="mat-block"> | |
41 | + <mat-label>Latitude</mat-label> | |
42 | + <input type="number" step="any" matInput formControlName="latitude"> | |
43 | + </mat-form-field> | |
44 | + <mat-form-field fxFlex class="mat-block"> | |
45 | + <mat-label>Longitude</mat-label> | |
46 | + <input type="number" step="any" matInput formControlName="longitude"> | |
47 | + </mat-form-field> | |
48 | + </div> | |
49 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> | |
50 | + <mat-form-field fxFlex class="mat-block"> | |
51 | + <mat-label>Address</mat-label> | |
52 | + <input matInput formControlName="address"> | |
53 | + </mat-form-field> | |
54 | + <mat-form-field fxFlex class="mat-block"> | |
55 | + <mat-label>Owner</mat-label> | |
56 | + <input matInput formControlName="owner"> | |
57 | + </mat-form-field> | |
58 | + </div> | |
59 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> | |
60 | + <mat-form-field fxFlex class="mat-block"> | |
61 | + <mat-label>Integer Value</mat-label> | |
62 | + <input type="number" step="1" matInput formControlName="number"> | |
63 | + <mat-error *ngIf="editEntityFormGroup.get('attributes.number').hasError('pattern')"> | |
64 | + Invalid integer value. | |
65 | + </mat-error> | |
66 | + </mat-form-field> | |
67 | + <div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex> | |
68 | + <label class="checkbox-label">Boolean Value</label> | |
69 | + <mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;"> | |
70 | + | |
71 | + </mat-checkbox> | |
72 | + </div> | |
73 | + </div> | |
74 | + </div> | |
75 | + <div class="relations-list old-relations"> | |
76 | + <div class="mat-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">Relations</div> | |
77 | + <div class="body" [fxShow]="oldRelations().length"> | |
78 | + <div class="row" fxLayout="row" fxLayoutAlign="start center" formArrayName="oldRelations" | |
79 | + *ngFor="let relation of oldRelations().controls; let i = index;"> | |
80 | + <div [formGroupName]="i" class="mat-elevation-z2" fxFlex fxLayout="row" style="padding: 5px 0 5px 5px;"> | |
81 | + <div fxFlex fxLayout="column"> | |
82 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> | |
83 | + <mat-form-field class="mat-block" style="min-width: 100px;"> | |
84 | + <mat-label>Direction</mat-label> | |
85 | + <mat-select formControlName="direction" name="direction"> | |
86 | + <mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value"> | |
87 | + | |
88 | + </mat-option> | |
89 | + </mat-select> | |
90 | + <mat-error *ngIf="relation.get('direction').hasError('required')"> | |
91 | + Relation direction is required. | |
92 | + </mat-error> | |
93 | + </mat-form-field> | |
94 | + <tb-relation-type-autocomplete | |
95 | + fxFlex class="mat-block" | |
96 | + formControlName="relationType" | |
97 | + required="true"> | |
98 | + </tb-relation-type-autocomplete> | |
99 | + </div> | |
100 | + <div fxLayout="row" fxLayout.xs="column"> | |
101 | + <tb-entity-select | |
102 | + fxFlex class="mat-block" | |
103 | + required="true" | |
104 | + formControlName="relatedEntity"> | |
105 | + </tb-entity-select> | |
106 | + </div> | |
107 | + </div> | |
108 | + <div fxLayout="column" fxLayoutAlign="center center"> | |
109 | + <button mat-icon-button color="primary" | |
110 | + aria-label="Remove" | |
111 | + type="button" | |
112 | + (click)="removeOldRelation(i)" | |
113 | + matTooltip="Remove relation" | |
114 | + matTooltipPosition="above"> | |
115 | + <mat-icon>close</mat-icon> | |
116 | + </button> | |
117 | + </div> | |
118 | + </div> | |
119 | + </div> | |
120 | + </div> | |
121 | + </div> | |
122 | + <div class="relations-list"> | |
123 | + <div class="mat-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">New Relations</div> | |
124 | + <div class="body" [fxShow]="relations().length"> | |
125 | + <div class="row" fxLayout="row" fxLayoutAlign="start center" formArrayName="relations" *ngFor="let relation of relations().controls; let i = index;"> | |
126 | + <div [formGroupName]="i" class="mat-elevation-z2" fxFlex fxLayout="row" style="padding: 5px 0 5px 5px;"> | |
127 | + <div fxFlex fxLayout="column"> | |
128 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> | |
129 | + <mat-form-field class="mat-block" style="min-width: 100px;"> | |
130 | + <mat-label>Direction</mat-label> | |
131 | + <mat-select formControlName="direction" name="direction"> | |
132 | + <mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value"> | |
133 | + | |
134 | + </mat-option> | |
135 | + </mat-select> | |
136 | + <mat-error *ngIf="relation.get('direction').hasError('required')"> | |
137 | + Relation direction is required. | |
138 | + </mat-error> | |
139 | + </mat-form-field> | |
140 | + <tb-relation-type-autocomplete | |
141 | + fxFlex class="mat-block" | |
142 | + formControlName="relationType" | |
143 | + [required]="true"> | |
144 | + </tb-relation-type-autocomplete> | |
145 | + </div> | |
146 | + <div fxLayout="row" fxLayout.xs="column"> | |
147 | + <tb-entity-select | |
148 | + fxFlex class="mat-block" | |
149 | + [required]="true" | |
150 | + formControlName="relatedEntity"> | |
151 | + </tb-entity-select> | |
152 | + </div> | |
153 | + </div> | |
154 | + <div fxLayout="column" fxLayoutAlign="center center"> | |
155 | + <button mat-icon-button color="primary" | |
156 | + aria-label="Remove" | |
157 | + type="button" | |
158 | + (click)="removeRelation(i)" | |
159 | + matTooltip="Remove relation" | |
160 | + matTooltipPosition="above"> | |
161 | + <mat-icon>close</mat-icon> | |
162 | + </button> | |
163 | + </div> | |
164 | + </div> | |
165 | + </div> | |
166 | + </div> | |
167 | + <div> | |
168 | + <button mat-raised-button color="primary" | |
169 | + type="button" | |
170 | + (click)="addRelation()" | |
171 | + matTooltip="Add Relation" | |
172 | + matTooltipPosition="above"> | |
173 | + Add | |
174 | + </button> | |
175 | + </div> | |
176 | + </div> | |
177 | + </div> | |
178 | + <div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center"> | |
179 | + <button mat-button color="primary" | |
180 | + type="button" | |
181 | + [disabled]="(isLoading$ | async)" | |
182 | + (click)="cancel()" cdkFocusInitial> | |
183 | + Cancel | |
184 | + </button> | |
185 | + <button mat-button mat-raised-button color="primary" | |
186 | + type="submit" | |
187 | + [disabled]="(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty"> | |
188 | + Save | |
189 | + </button> | |
190 | + </div> | |
191 | +</form> | |
192 | +{:copy-code} | |
193 | +``` | |
194 | + | |
195 | +<br> | |
196 | +<br> | ... | ... |
ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_edit_dialog_js.md
0 → 100644
1 | +#### Function displaying dialog to edit a device or an asset | |
2 | + | |
3 | +```javascript | |
4 | +{:code-style="max-height: 400px;"} | |
5 | +let $injector = widgetContext.$scope.$injector; | |
6 | +let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog')); | |
7 | +let entityService = $injector.get(widgetContext.servicesMap.get('entityService')); | |
8 | +let assetService = $injector.get(widgetContext.servicesMap.get('assetService')); | |
9 | +let deviceService = $injector.get(widgetContext.servicesMap.get('deviceService')); | |
10 | +let attributeService = $injector.get(widgetContext.servicesMap.get('attributeService')); | |
11 | +let entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService')); | |
12 | + | |
13 | +openEditEntityDialog(); | |
14 | + | |
15 | +function openEditEntityDialog() { | |
16 | + customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe(); | |
17 | +} | |
18 | + | |
19 | +function EditEntityDialogController(instance) { | |
20 | + let vm = instance; | |
21 | + | |
22 | + vm.entityName = entityName; | |
23 | + vm.entityType = entityId.entityType; | |
24 | + vm.entitySearchDirection = { | |
25 | + from: "FROM", | |
26 | + to: "TO" | |
27 | + }; | |
28 | + vm.attributes = {}; | |
29 | + vm.oldRelationsData = []; | |
30 | + vm.relationsToDelete = []; | |
31 | + vm.entity = {}; | |
32 | + | |
33 | + vm.editEntityFormGroup = vm.fb.group({ | |
34 | + entityName: ['', [vm.validators.required]], | |
35 | + entityType: [null], | |
36 | + entityLabel: [null], | |
37 | + type: ['', [vm.validators.required]], | |
38 | + attributes: vm.fb.group({ | |
39 | + latitude: [null], | |
40 | + longitude: [null], | |
41 | + address: [null], | |
42 | + owner: [null], | |
43 | + number: [null, [vm.validators.pattern(/^-?[0-9]+$/)]], | |
44 | + booleanValue: [false] | |
45 | + }), | |
46 | + oldRelations: vm.fb.array([]), | |
47 | + relations: vm.fb.array([]) | |
48 | + }); | |
49 | + | |
50 | + getEntityInfo(); | |
51 | + | |
52 | + vm.cancel = function() { | |
53 | + vm.dialogRef.close(null); | |
54 | + }; | |
55 | + | |
56 | + vm.relations = function() { | |
57 | + return vm.editEntityFormGroup.get('relations'); | |
58 | + }; | |
59 | + | |
60 | + vm.oldRelations = function() { | |
61 | + return vm.editEntityFormGroup.get('oldRelations'); | |
62 | + }; | |
63 | + | |
64 | + vm.addRelation = function() { | |
65 | + vm.relations().push(vm.fb.group({ | |
66 | + relatedEntity: [null, [vm.validators.required]], | |
67 | + relationType: [null, [vm.validators.required]], | |
68 | + direction: [null, [vm.validators.required]] | |
69 | + })); | |
70 | + }; | |
71 | + | |
72 | + function addOldRelation() { | |
73 | + vm.oldRelations().push(vm.fb.group({ | |
74 | + relatedEntity: [{value: null, disabled: true}, [vm.validators.required]], | |
75 | + relationType: [{value: null, disabled: true}, [vm.validators.required]], | |
76 | + direction: [{value: null, disabled: true}, [vm.validators.required]] | |
77 | + })); | |
78 | + } | |
79 | + | |
80 | + vm.removeRelation = function(index) { | |
81 | + vm.relations().removeAt(index); | |
82 | + vm.relations().markAsDirty(); | |
83 | + }; | |
84 | + | |
85 | + vm.removeOldRelation = function(index) { | |
86 | + vm.oldRelations().removeAt(index); | |
87 | + vm.relationsToDelete.push(vm.oldRelationsData[index]); | |
88 | + vm.oldRelations().markAsDirty(); | |
89 | + }; | |
90 | + | |
91 | + vm.save = function() { | |
92 | + vm.editEntityFormGroup.markAsPristine(); | |
93 | + widgetContext.rxjs.forkJoin([ | |
94 | + saveAttributes(entityId), | |
95 | + saveRelations(entityId), | |
96 | + saveEntity() | |
97 | + ]).subscribe( | |
98 | + function () { | |
99 | + widgetContext.updateAliases(); | |
100 | + vm.dialogRef.close(null); | |
101 | + } | |
102 | + ); | |
103 | + }; | |
104 | + | |
105 | + function getEntityAttributes(attributes) { | |
106 | + for (var i = 0; i < attributes.length; i++) { | |
107 | + vm.attributes[attributes[i].key] = attributes[i].value; | |
108 | + } | |
109 | + } | |
110 | + | |
111 | + function getEntityRelations(relations) { | |
112 | + let relationsFrom = relations[0]; | |
113 | + let relationsTo = relations[1]; | |
114 | + for (let i=0; i < relationsFrom.length; i++) { | |
115 | + let relation = { | |
116 | + direction: 'FROM', | |
117 | + relationType: relationsFrom[i].type, | |
118 | + relatedEntity: relationsFrom[i].to | |
119 | + }; | |
120 | + vm.oldRelationsData.push(relation); | |
121 | + addOldRelation(); | |
122 | + } | |
123 | + for (let i=0; i < relationsTo.length; i++) { | |
124 | + let relation = { | |
125 | + direction: 'TO', | |
126 | + relationType: relationsTo[i].type, | |
127 | + relatedEntity: relationsTo[i].from | |
128 | + }; | |
129 | + vm.oldRelationsData.push(relation); | |
130 | + addOldRelation(); | |
131 | + } | |
132 | + } | |
133 | + | |
134 | + function getEntityInfo() { | |
135 | + widgetContext.rxjs.forkJoin([ | |
136 | + entityRelationService.findInfoByFrom(entityId), | |
137 | + entityRelationService.findInfoByTo(entityId), | |
138 | + attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE'), | |
139 | + entityService.getEntity(entityId.entityType, entityId.id) | |
140 | + ]).subscribe( | |
141 | + function (data) { | |
142 | + getEntityRelations(data.slice(0,2)); | |
143 | + getEntityAttributes(data[2]); | |
144 | + vm.entity = data[3]; | |
145 | + vm.editEntityFormGroup.patchValue({ | |
146 | + entityName: vm.entity.name, | |
147 | + entityType: vm.entityType, | |
148 | + entityLabel: vm.entity.label, | |
149 | + type: vm.entity.type, | |
150 | + attributes: vm.attributes, | |
151 | + oldRelations: vm.oldRelationsData | |
152 | + }, {emitEvent: false}); | |
153 | + } | |
154 | + ); | |
155 | + } | |
156 | + | |
157 | + function saveEntity() { | |
158 | + const formValues = vm.editEntityFormGroup.value; | |
159 | + if (vm.entity.label !== formValues.entityLabel){ | |
160 | + vm.entity.label = formValues.entityLabel; | |
161 | + if (formValues.entityType == 'ASSET') { | |
162 | + return assetService.saveAsset(vm.entity); | |
163 | + } else if (formValues.entityType == 'DEVICE') { | |
164 | + return deviceService.saveDevice(vm.entity); | |
165 | + } | |
166 | + } | |
167 | + return widgetContext.rxjs.of([]); | |
168 | + } | |
169 | + | |
170 | + function saveAttributes(entityId) { | |
171 | + let attributes = vm.editEntityFormGroup.get('attributes').value; | |
172 | + let attributesArray = []; | |
173 | + for (let key in attributes) { | |
174 | + if (attributes[key] !== vm.attributes[key]) { | |
175 | + attributesArray.push({key: key, value: attributes[key]}); | |
176 | + } | |
177 | + } | |
178 | + if (attributesArray.length > 0) { | |
179 | + return attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributesArray); | |
180 | + } | |
181 | + return widgetContext.rxjs.of([]); | |
182 | + } | |
183 | + | |
184 | + function saveRelations(entityId) { | |
185 | + let relations = vm.editEntityFormGroup.get('relations').value; | |
186 | + let tasks = []; | |
187 | + for(let i=0; i < relations.length; i++) { | |
188 | + let relation = { | |
189 | + type: relations[i].relationType, | |
190 | + typeGroup: 'COMMON' | |
191 | + }; | |
192 | + if (relations[i].direction == 'FROM') { | |
193 | + relation.to = relations[i].relatedEntity; | |
194 | + relation.from = entityId; | |
195 | + } else { | |
196 | + relation.to = entityId; | |
197 | + relation.from = relations[i].relatedEntity; | |
198 | + } | |
199 | + tasks.push(entityRelationService.saveRelation(relation)); | |
200 | + } | |
201 | + for (let i=0; i < vm.relationsToDelete.length; i++) { | |
202 | + let relation = { | |
203 | + type: vm.relationsToDelete[i].relationType | |
204 | + }; | |
205 | + if (vm.relationsToDelete[i].direction == 'FROM') { | |
206 | + relation.to = vm.relationsToDelete[i].relatedEntity; | |
207 | + relation.from = entityId; | |
208 | + } else { | |
209 | + relation.to = entityId; | |
210 | + relation.from = vm.relationsToDelete[i].relatedEntity; | |
211 | + } | |
212 | + tasks.push(entityRelationService.deleteRelation(relation.from, relation.type, relation.to)); | |
213 | + } | |
214 | + if (tasks.length > 0) { | |
215 | + return widgetContext.rxjs.forkJoin(tasks); | |
216 | + } | |
217 | + return widgetContext.rxjs.of([]); | |
218 | + } | |
219 | +} | |
220 | +{:copy-code} | |
221 | +``` | |
222 | + | |
223 | +<br> | |
224 | +<br> | ... | ... |
ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_edit_image_html.md
0 → 100644
1 | +#### HTML template of dialog to add/edit image in entity attribute | |
2 | + | |
3 | +```html | |
4 | +{:code-style="max-height: 400px;"} | |
5 | +<form [formGroup]="editEntity" (ngSubmit)="save()" class="edit-entity-form"> | |
6 | + <mat-toolbar fxLayout="row" color="primary"> | |
7 | + <h2>Edit {{entityName}} image</h2> | |
8 | + <span fxFlex></span> | |
9 | + <button mat-icon-button (click)="cancel()" type="button"> | |
10 | + <mat-icon class="material-icons">close</mat-icon> | |
11 | + </button> | |
12 | + </mat-toolbar> | |
13 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="(isLoading$ | async) || loading"> | |
14 | + </mat-progress-bar> | |
15 | + <div style="height: 4px;" *ngIf="!(isLoading$ | async) && !loading"></div> | |
16 | + <div mat-dialog-content fxLayout="column"> | |
17 | + <div formGroupName="attributes" fxLayout="column"> | |
18 | + <tb-image-input | |
19 | + label="Entity image" | |
20 | + formControlName="image" | |
21 | + ></tb-image-input> | |
22 | + </div> | |
23 | + </div> | |
24 | + <div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center"> | |
25 | + <button mat-button mat-raised-button color="primary" | |
26 | + type="submit" | |
27 | + [disabled]="(isLoading$ | async) || editEntity.invalid || !editEntity.dirty"> | |
28 | + Save | |
29 | + </button> | |
30 | + <button mat-button color="primary" | |
31 | + type="button" | |
32 | + [disabled]="(isLoading$ | async)" | |
33 | + (click)="cancel()" cdkFocusInitial> | |
34 | + Cancel | |
35 | + </button> | |
36 | + </div> | |
37 | +</form> | |
38 | +{:copy-code} | |
39 | +``` | |
40 | + | |
41 | +<br> | |
42 | +<br> | ... | ... |
ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_edit_image_js.md
0 → 100644
1 | +#### Function displaying dialog to add/edit image in entity attribute | |
2 | + | |
3 | +```javascript | |
4 | +{:code-style="max-height: 400px;"} | |
5 | +let $injector = widgetContext.$scope.$injector; | |
6 | +let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog')); | |
7 | +let assetService = $injector.get(widgetContext.servicesMap.get('assetService')); | |
8 | +let attributeService = $injector.get(widgetContext.servicesMap.get('attributeService')); | |
9 | +let entityService = $injector.get(widgetContext.servicesMap.get('entityService')); | |
10 | + | |
11 | +openAddEntityDialog(); | |
12 | + | |
13 | +function openAddEntityDialog() { | |
14 | + customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe(() => {}); | |
15 | +} | |
16 | + | |
17 | +function AddEntityDialogController(instance) { | |
18 | + let vm = instance; | |
19 | + | |
20 | + vm.entityName = entityName; | |
21 | + | |
22 | + vm.attributes = {}; | |
23 | + | |
24 | + vm.editEntity = vm.fb.group({ | |
25 | + attributes: vm.fb.group({ | |
26 | + image: [null] | |
27 | + }) | |
28 | + }); | |
29 | + | |
30 | + getEntityInfo(); | |
31 | + | |
32 | + vm.cancel = function() { | |
33 | + vm.dialogRef.close(null); | |
34 | + }; | |
35 | + | |
36 | + vm.save = function() { | |
37 | + vm.loading = true; | |
38 | + saveAttributes(entityId).subscribe( | |
39 | + () => { | |
40 | + vm.dialogRef.close(null); | |
41 | + }, () =>{ | |
42 | + vm.loading = false; | |
43 | + } | |
44 | + ); | |
45 | + }; | |
46 | + | |
47 | + function getEntityAttributes(attributes) { | |
48 | + for (var i = 0; i < attributes.length; i++) { | |
49 | + vm.attributes[attributes[i].key] = attributes[i].value; | |
50 | + } | |
51 | + } | |
52 | + | |
53 | + function getEntityInfo() { | |
54 | + vm.loading = true; | |
55 | + attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE').subscribe( | |
56 | + function (data) { | |
57 | + getEntityAttributes(data); | |
58 | + | |
59 | + vm.editEntity.patchValue({ | |
60 | + attributes: vm.attributes | |
61 | + }, {emitEvent: false}); | |
62 | + vm.loading = false; | |
63 | + } | |
64 | + ); | |
65 | + } | |
66 | + | |
67 | + function saveAttributes(entityId) { | |
68 | + let attributes = vm.editEntity.get('attributes').value; | |
69 | + let attributesArray = []; | |
70 | + for (let key in attributes) { | |
71 | + if (attributes[key] !== vm.attributes[key]) { | |
72 | + attributesArray.push({key: key, value: attributes[key]}); | |
73 | + } | |
74 | + } | |
75 | + if (attributesArray.length > 0) { | |
76 | + return attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributesArray); | |
77 | + } | |
78 | + return widgetContext.rxjs.of([]); | |
79 | + } | |
80 | +} | |
81 | +{:copy-code} | |
82 | +``` | |
83 | + | |
84 | +<br> | |
85 | +<br> | ... | ... |
... | ... | @@ -54,6 +54,33 @@ if (prevOrigValue) { |
54 | 54 | } |
55 | 55 | {:copy-code} |
56 | 56 | ``` |
57 | +* Formatting data to time format | |
58 | + | |
59 | +```javascript | |
60 | +if (value) { | |
61 | + return moment(value).format("DD/MM/YYYY HH:mm:ss"); | |
62 | +} | |
63 | +return ''; | |
64 | +{:copy-code} | |
65 | +``` | |
66 | + | |
67 | +* Creates line-breaks for 0 values, when used in line chart | |
68 | + | |
69 | +```javascript | |
70 | +if (value === 0) { | |
71 | + return null; | |
72 | +} else { | |
73 | + return value; | |
74 | +} | |
75 | +{:copy-code} | |
76 | +``` | |
77 | + | |
78 | +* Display data point of the HTML value card under the condition | |
79 | + | |
80 | +```javascript | |
81 | +return value ? '<div class="info"><b>Temperature: </b>'+value+' °C</div>' : ''; | |
82 | +{:copy-code} | |
83 | +``` | |
57 | 84 | |
58 | 85 | <br> |
59 | 86 | <br> | ... | ... |
... | ... | @@ -57,5 +57,87 @@ return '<div style="border: 2px solid #0072ff; ' + |
57 | 57 | {:copy-code} |
58 | 58 | ``` |
59 | 59 | |
60 | +* Colored circles instead of boolean value: | |
61 | + | |
62 | +```javascript | |
63 | +var color; | |
64 | +var active = value; | |
65 | +if (active == 'true') { // all key values here are strings | |
66 | + color = '#27AE60'; | |
67 | +} else { | |
68 | + color = '#EB5757'; | |
69 | +} | |
70 | +return '<span style="font-size: 18px; color: ' + color + '">⬤</span>'; | |
71 | +{:copy-code} | |
72 | +``` | |
73 | + | |
74 | +* Decimal value format (1196 => 1,196.0): | |
75 | + | |
76 | +```javascript | |
77 | +var value = value / 1; | |
78 | +function numberWithCommas(x) { | |
79 | + return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); | |
80 | +} | |
81 | +return value ? numberWithCommas(value.toFixed(1)) : ''; | |
82 | +{:copy-code} | |
83 | +``` | |
84 | + | |
85 | +* Show device status and icon this : | |
86 | + | |
87 | +```javascript | |
88 | +{:code-style="max-height: 200px; max-width: 850px;"} | |
89 | +function getIcon(value) { | |
90 | + if (value == 'QUEUED') { | |
91 | + return '<mat-icon class="mat-icon material-icons mat-icon-no-color" data-mat-icon-type="font" style="color: #000;">' + | |
92 | + '<svg style="width:24px;height:24px" viewBox="0 0 24 24">' + | |
93 | + '<path fill="currentColor" d="M6,2V8H6V8L10,12L6,16V16H6V22H18V16H18V16L14,12L18,8V8H18V2H6M16,16.5V20H8V16.5L12,12.5L16,16.5M12,11.5L8,7.5V4H16V7.5L12,11.5Z" />' + | |
94 | + '</svg>' + | |
95 | + '</mat-icon>'; | |
96 | + } | |
97 | + if (value == 'UPDATED' ) { | |
98 | + return '<mat-icon class="mat-icon notranslate material-icons mat-icon-no-color" data-mat-icon-type="font" style="color: #000">' + | |
99 | + 'update' + | |
100 | + '</mat-icon>'; | |
101 | + } | |
102 | + return ''; | |
103 | +} | |
104 | +function capitalize (s) { | |
105 | + if (typeof s !== 'string') return ''; | |
106 | + return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase(); | |
107 | +} | |
108 | +var status = value; | |
109 | +return getIcon(status) + '<span style="vertical-align: super;padding-left: 8px;">' + capitalize(status) + '</span>'; | |
110 | +{:copy-code} | |
111 | +``` | |
112 | + | |
113 | +* Display device attribute value on progress bar: | |
114 | + | |
115 | +```javascript | |
116 | +{:code-style="max-height: 200px; max-width: 850px;"} | |
117 | +var progress = value; | |
118 | +if (value !== '') { | |
119 | + return `<mat-progress-bar style="height: 8px; padding-right: 30px" ` + | |
120 | + `role="progressbar" aria-valuemin="0" aria-valuemax="100" ` + | |
121 | + `tabindex="-1" mode="determinate" value="${progress}" ` + | |
122 | + `class="mat-progress-bar mat-primary" aria-valuenow="${progress}">` + | |
123 | + `<div aria-hidden="true">` + | |
124 | + `<svg width="100%" height="8" focusable="false" ` + | |
125 | + `class="mat-progress-bar-background mat-progress-bar-element">` + | |
126 | + `<defs>` + | |
127 | + `<pattern x="4" y="0" width="8" height="4" patternUnits="userSpaceOnUse" id="mat-progress-bar-0">` + | |
128 | + `<circle cx="2" cy="2" r="2"></circle>` + | |
129 | + `</pattern>` + | |
130 | + `</defs>` + | |
131 | + `<rect width="100%" height="100%" fill="url("/components/progress-bar/overview#mat-progress-bar-0")"></rect>` + | |
132 | + `</svg>` + | |
133 | + `<div class="mat-progress-bar-buffer mat-progress-bar-element"></div>` + | |
134 | + `<div class="mat-progress-bar-primary mat-progress-bar-fill mat-progress-bar-element" style="transform: scale3d(${progress / 100}, 1, 1);"></div>` + | |
135 | + `<div class="mat-progress-bar-secondary mat-progress-bar-fill mat-progress-bar-element"></div>` + | |
136 | + `</div>` + | |
137 | + `</mat-progress-bar>`; | |
138 | +} | |
139 | +{:copy-code} | |
140 | +``` | |
141 | + | |
60 | 142 | <br> |
61 | 143 | <br> | ... | ... |
... | ... | @@ -29,6 +29,16 @@ Should return key/value object presenting style attributes. |
29 | 29 | |
30 | 30 | ##### Examples |
31 | 31 | |
32 | +* Set color and font-weight table cell content: | |
33 | + | |
34 | +```javascript | |
35 | +return { | |
36 | + color:'rgb(0, 132, 214)', | |
37 | + fontWeight: 600 | |
38 | +} | |
39 | +{:copy-code} | |
40 | +``` | |
41 | + | |
32 | 42 | * Set color depending on device temperature value: |
33 | 43 | |
34 | 44 | ```javascript | ... | ... |
... | ... | @@ -27,6 +27,16 @@ Should return key/value object presenting style attributes. |
27 | 27 | |
28 | 28 | ##### Examples |
29 | 29 | |
30 | +* Set color and font-weight table row: | |
31 | + | |
32 | +```javascript | |
33 | +return { | |
34 | + color:'rgb(0, 132, 214)', | |
35 | + fontWeight: 600 | |
36 | +} | |
37 | +{:copy-code} | |
38 | +``` | |
39 | + | |
30 | 40 | * Set row background color depending on device type: |
31 | 41 | |
32 | 42 | ```javascript | ... | ... |
... | ... | @@ -43,6 +43,17 @@ return ''; |
43 | 43 | {:copy-code} |
44 | 44 | ``` |
45 | 45 | |
46 | +* Present ticks in decimal format (1196 => 1,196.0): | |
47 | + | |
48 | +```javascript | |
49 | +var value = value / 1; | |
50 | +function numberWithCommas(x) { | |
51 | + return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); | |
52 | +} | |
53 | +return value ? numberWithCommas(value.toFixed(1)) : ''; | |
54 | +{:copy-code} | |
55 | +``` | |
56 | + | |
46 | 57 | <ul> |
47 | 58 | <li> |
48 | 59 | To present axis ticks for true / false or 1 / 0 data.<br> | ... | ... |
... | ... | @@ -36,5 +36,16 @@ return value.toFixed(2) + ' A'; |
36 | 36 | {:copy-code} |
37 | 37 | ``` |
38 | 38 | |
39 | +* Present the datapoint value in decimal format (1196 => 1,196.0): | |
40 | + | |
41 | +```javascript | |
42 | +var value = value / 1; | |
43 | +function numberWithCommas(x) { | |
44 | + return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); | |
45 | +} | |
46 | +return value ? numberWithCommas(value.toFixed(1)) : ''; | |
47 | +{:copy-code} | |
48 | +``` | |
49 | + | |
39 | 50 | <br> |
40 | 51 | <br> | ... | ... |
21.4 KB
36.6 KB