opensignalbox-interface 0.1.0__py3-none-any.whl

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 (209) hide show
  1. opensignalbox/interface/__init__.py +42 -0
  2. opensignalbox/interface/adapters/__init__.py +3 -0
  3. opensignalbox/interface/adapters/base.py +148 -0
  4. opensignalbox/interface/adapters/controller.py +286 -0
  5. opensignalbox/interface/adapters/modbus/__init__.py +3 -0
  6. opensignalbox/interface/adapters/modbus/adapter.py +485 -0
  7. opensignalbox/interface/assets/favicon.ico +0 -0
  8. opensignalbox/interface/interfaces/__init__.py +3 -0
  9. opensignalbox/interface/interfaces/base.py +170 -0
  10. opensignalbox/interface/interfaces/basicio/__init__.py +3 -0
  11. opensignalbox/interface/interfaces/basicio/connections.py +239 -0
  12. opensignalbox/interface/interfaces/basicio/handler.py +1096 -0
  13. opensignalbox/interface/interfaces/basicio/models.py +165 -0
  14. opensignalbox/interface/interfaces/bell/__init__.py +3 -0
  15. opensignalbox/interface/interfaces/bell/connections.py +279 -0
  16. opensignalbox/interface/interfaces/bell/handler.py +802 -0
  17. opensignalbox/interface/interfaces/bell/models.py +103 -0
  18. opensignalbox/interface/interfaces/controller.py +467 -0
  19. opensignalbox/interface/main.py +187 -0
  20. opensignalbox/interface/models.py +155 -0
  21. opensignalbox/interface/routes/__init__.py +4 -0
  22. opensignalbox/interface/routes/adapters.py +119 -0
  23. opensignalbox/interface/routes/interfaces.py +104 -0
  24. opensignalbox/interface/routes.py +204 -0
  25. opensignalbox/interface/utils.py +29 -0
  26. opensignalbox/interface/version.py +5 -0
  27. opensignalbox/interface/web_ui/.gitignore +24 -0
  28. opensignalbox/interface/web_ui/README.md +5 -0
  29. opensignalbox/interface/web_ui/WEB_UI_REFACTORING.md +136 -0
  30. opensignalbox/interface/web_ui/components.json +17 -0
  31. opensignalbox/interface/web_ui/dist/assets/index-DhsEc6uo.css +2020 -0
  32. opensignalbox/interface/web_ui/dist/assets/index-t__5JZjf.js +39056 -0
  33. opensignalbox/interface/web_ui/dist/assets/index-t__5JZjf.js.map +1 -0
  34. opensignalbox/interface/web_ui/dist/index.html +14 -0
  35. opensignalbox/interface/web_ui/index.html +13 -0
  36. opensignalbox/interface/web_ui/package-lock.json +3710 -0
  37. opensignalbox/interface/web_ui/package.json +39 -0
  38. opensignalbox/interface/web_ui/src/App.vue +32 -0
  39. opensignalbox/interface/web_ui/src/assets/favicon.ico +0 -0
  40. opensignalbox/interface/web_ui/src/assets/index.css +83 -0
  41. opensignalbox/interface/web_ui/src/components/SharedVariablePicker.vue +112 -0
  42. opensignalbox/interface/web_ui/src/components/adapters/AdapterList.vue +183 -0
  43. opensignalbox/interface/web_ui/src/components/adapters/types.ts +35 -0
  44. opensignalbox/interface/web_ui/src/components/interfaces/InterfaceList.vue +200 -0
  45. opensignalbox/interface/web_ui/src/components/ui/badge/Badge.vue +16 -0
  46. opensignalbox/interface/web_ui/src/components/ui/badge/index.ts +25 -0
  47. opensignalbox/interface/web_ui/src/components/ui/breadcrumb/Breadcrumb.vue +13 -0
  48. opensignalbox/interface/web_ui/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue +22 -0
  49. opensignalbox/interface/web_ui/src/components/ui/breadcrumb/BreadcrumbItem.vue +16 -0
  50. opensignalbox/interface/web_ui/src/components/ui/breadcrumb/BreadcrumbLink.vue +19 -0
  51. opensignalbox/interface/web_ui/src/components/ui/breadcrumb/BreadcrumbList.vue +16 -0
  52. opensignalbox/interface/web_ui/src/components/ui/breadcrumb/BreadcrumbPage.vue +19 -0
  53. opensignalbox/interface/web_ui/src/components/ui/breadcrumb/BreadcrumbSeparator.vue +21 -0
  54. opensignalbox/interface/web_ui/src/components/ui/breadcrumb/index.ts +7 -0
  55. opensignalbox/interface/web_ui/src/components/ui/button/Button.vue +26 -0
  56. opensignalbox/interface/web_ui/src/components/ui/button/index.ts +35 -0
  57. opensignalbox/interface/web_ui/src/components/ui/card/Card.vue +21 -0
  58. opensignalbox/interface/web_ui/src/components/ui/card/CardContent.vue +14 -0
  59. opensignalbox/interface/web_ui/src/components/ui/card/CardDescription.vue +14 -0
  60. opensignalbox/interface/web_ui/src/components/ui/card/CardFooter.vue +14 -0
  61. opensignalbox/interface/web_ui/src/components/ui/card/CardHeader.vue +14 -0
  62. opensignalbox/interface/web_ui/src/components/ui/card/CardTitle.vue +18 -0
  63. opensignalbox/interface/web_ui/src/components/ui/card/index.ts +6 -0
  64. opensignalbox/interface/web_ui/src/components/ui/checkbox/Checkbox.vue +33 -0
  65. opensignalbox/interface/web_ui/src/components/ui/checkbox/index.ts +1 -0
  66. opensignalbox/interface/web_ui/src/components/ui/collapsible/Collapsible.vue +15 -0
  67. opensignalbox/interface/web_ui/src/components/ui/collapsible/CollapsibleContent.vue +11 -0
  68. opensignalbox/interface/web_ui/src/components/ui/collapsible/CollapsibleTrigger.vue +11 -0
  69. opensignalbox/interface/web_ui/src/components/ui/collapsible/index.ts +3 -0
  70. opensignalbox/interface/web_ui/src/components/ui/command/Command.vue +30 -0
  71. opensignalbox/interface/web_ui/src/components/ui/command/CommandDialog.vue +21 -0
  72. opensignalbox/interface/web_ui/src/components/ui/command/CommandEmpty.vue +20 -0
  73. opensignalbox/interface/web_ui/src/components/ui/command/CommandGroup.vue +29 -0
  74. opensignalbox/interface/web_ui/src/components/ui/command/CommandInput.vue +33 -0
  75. opensignalbox/interface/web_ui/src/components/ui/command/CommandItem.vue +26 -0
  76. opensignalbox/interface/web_ui/src/components/ui/command/CommandList.vue +27 -0
  77. opensignalbox/interface/web_ui/src/components/ui/command/CommandSeparator.vue +23 -0
  78. opensignalbox/interface/web_ui/src/components/ui/command/CommandShortcut.vue +14 -0
  79. opensignalbox/interface/web_ui/src/components/ui/command/index.ts +9 -0
  80. opensignalbox/interface/web_ui/src/components/ui/dialog/Dialog.vue +14 -0
  81. opensignalbox/interface/web_ui/src/components/ui/dialog/DialogClose.vue +11 -0
  82. opensignalbox/interface/web_ui/src/components/ui/dialog/DialogContent.vue +50 -0
  83. opensignalbox/interface/web_ui/src/components/ui/dialog/DialogDescription.vue +24 -0
  84. opensignalbox/interface/web_ui/src/components/ui/dialog/DialogFooter.vue +19 -0
  85. opensignalbox/interface/web_ui/src/components/ui/dialog/DialogHeader.vue +16 -0
  86. opensignalbox/interface/web_ui/src/components/ui/dialog/DialogScrollContent.vue +59 -0
  87. opensignalbox/interface/web_ui/src/components/ui/dialog/DialogTitle.vue +29 -0
  88. opensignalbox/interface/web_ui/src/components/ui/dialog/DialogTrigger.vue +11 -0
  89. opensignalbox/interface/web_ui/src/components/ui/dialog/index.ts +9 -0
  90. opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenu.vue +14 -0
  91. opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +40 -0
  92. opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuContent.vue +38 -0
  93. opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuGroup.vue +11 -0
  94. opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuItem.vue +28 -0
  95. opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuLabel.vue +24 -0
  96. opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +19 -0
  97. opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +41 -0
  98. opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue +22 -0
  99. opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue +14 -0
  100. opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuSub.vue +19 -0
  101. opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue +30 -0
  102. opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +33 -0
  103. opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue +13 -0
  104. opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/index.ts +16 -0
  105. opensignalbox/interface/web_ui/src/components/ui/form/FormControl.vue +16 -0
  106. opensignalbox/interface/web_ui/src/components/ui/form/FormDescription.vue +20 -0
  107. opensignalbox/interface/web_ui/src/components/ui/form/FormItem.vue +19 -0
  108. opensignalbox/interface/web_ui/src/components/ui/form/FormLabel.vue +23 -0
  109. opensignalbox/interface/web_ui/src/components/ui/form/FormMessage.vue +16 -0
  110. opensignalbox/interface/web_ui/src/components/ui/form/index.ts +7 -0
  111. opensignalbox/interface/web_ui/src/components/ui/form/injectionKeys.ts +4 -0
  112. opensignalbox/interface/web_ui/src/components/ui/form/useFormField.ts +30 -0
  113. opensignalbox/interface/web_ui/src/components/ui/input/Input.vue +24 -0
  114. opensignalbox/interface/web_ui/src/components/ui/input/index.ts +1 -0
  115. opensignalbox/interface/web_ui/src/components/ui/label/Label.vue +27 -0
  116. opensignalbox/interface/web_ui/src/components/ui/label/index.ts +1 -0
  117. opensignalbox/interface/web_ui/src/components/ui/number-field/NumberField.vue +23 -0
  118. opensignalbox/interface/web_ui/src/components/ui/number-field/NumberFieldContent.vue +14 -0
  119. opensignalbox/interface/web_ui/src/components/ui/number-field/NumberFieldDecrement.vue +25 -0
  120. opensignalbox/interface/web_ui/src/components/ui/number-field/NumberFieldIncrement.vue +25 -0
  121. opensignalbox/interface/web_ui/src/components/ui/number-field/NumberFieldInput.vue +16 -0
  122. opensignalbox/interface/web_ui/src/components/ui/number-field/index.ts +5 -0
  123. opensignalbox/interface/web_ui/src/components/ui/popover/Popover.vue +15 -0
  124. opensignalbox/interface/web_ui/src/components/ui/popover/PopoverContent.vue +48 -0
  125. opensignalbox/interface/web_ui/src/components/ui/popover/PopoverTrigger.vue +11 -0
  126. opensignalbox/interface/web_ui/src/components/ui/popover/index.ts +3 -0
  127. opensignalbox/interface/web_ui/src/components/ui/radio-group/RadioGroup.vue +25 -0
  128. opensignalbox/interface/web_ui/src/components/ui/radio-group/RadioGroupItem.vue +39 -0
  129. opensignalbox/interface/web_ui/src/components/ui/radio-group/index.ts +2 -0
  130. opensignalbox/interface/web_ui/src/components/ui/scroll-area/ScrollArea.vue +29 -0
  131. opensignalbox/interface/web_ui/src/components/ui/scroll-area/ScrollBar.vue +30 -0
  132. opensignalbox/interface/web_ui/src/components/ui/scroll-area/index.ts +2 -0
  133. opensignalbox/interface/web_ui/src/components/ui/select/Select.vue +15 -0
  134. opensignalbox/interface/web_ui/src/components/ui/select/SelectContent.vue +53 -0
  135. opensignalbox/interface/web_ui/src/components/ui/select/SelectGroup.vue +19 -0
  136. opensignalbox/interface/web_ui/src/components/ui/select/SelectItem.vue +44 -0
  137. opensignalbox/interface/web_ui/src/components/ui/select/SelectItemText.vue +11 -0
  138. opensignalbox/interface/web_ui/src/components/ui/select/SelectLabel.vue +13 -0
  139. opensignalbox/interface/web_ui/src/components/ui/select/SelectScrollDownButton.vue +24 -0
  140. opensignalbox/interface/web_ui/src/components/ui/select/SelectScrollUpButton.vue +24 -0
  141. opensignalbox/interface/web_ui/src/components/ui/select/SelectSeparator.vue +17 -0
  142. opensignalbox/interface/web_ui/src/components/ui/select/SelectTrigger.vue +31 -0
  143. opensignalbox/interface/web_ui/src/components/ui/select/SelectValue.vue +11 -0
  144. opensignalbox/interface/web_ui/src/components/ui/select/index.ts +11 -0
  145. opensignalbox/interface/web_ui/src/components/ui/separator/Separator.vue +35 -0
  146. opensignalbox/interface/web_ui/src/components/ui/separator/index.ts +1 -0
  147. opensignalbox/interface/web_ui/src/components/ui/sheet/Sheet.vue +14 -0
  148. opensignalbox/interface/web_ui/src/components/ui/sheet/SheetClose.vue +11 -0
  149. opensignalbox/interface/web_ui/src/components/ui/sheet/SheetContent.vue +56 -0
  150. opensignalbox/interface/web_ui/src/components/ui/sheet/SheetDescription.vue +22 -0
  151. opensignalbox/interface/web_ui/src/components/ui/sheet/SheetFooter.vue +19 -0
  152. opensignalbox/interface/web_ui/src/components/ui/sheet/SheetHeader.vue +16 -0
  153. opensignalbox/interface/web_ui/src/components/ui/sheet/SheetTitle.vue +22 -0
  154. opensignalbox/interface/web_ui/src/components/ui/sheet/SheetTrigger.vue +11 -0
  155. opensignalbox/interface/web_ui/src/components/ui/sheet/index.ts +31 -0
  156. opensignalbox/interface/web_ui/src/components/ui/table/Table.vue +16 -0
  157. opensignalbox/interface/web_ui/src/components/ui/table/TableBody.vue +14 -0
  158. opensignalbox/interface/web_ui/src/components/ui/table/TableCaption.vue +14 -0
  159. opensignalbox/interface/web_ui/src/components/ui/table/TableCell.vue +21 -0
  160. opensignalbox/interface/web_ui/src/components/ui/table/TableEmpty.vue +37 -0
  161. opensignalbox/interface/web_ui/src/components/ui/table/TableFooter.vue +14 -0
  162. opensignalbox/interface/web_ui/src/components/ui/table/TableHead.vue +14 -0
  163. opensignalbox/interface/web_ui/src/components/ui/table/TableHeader.vue +14 -0
  164. opensignalbox/interface/web_ui/src/components/ui/table/TableRow.vue +14 -0
  165. opensignalbox/interface/web_ui/src/components/ui/table/index.ts +9 -0
  166. opensignalbox/interface/web_ui/src/components/ui/tabs/Tabs.vue +15 -0
  167. opensignalbox/interface/web_ui/src/components/ui/tabs/TabsContent.vue +22 -0
  168. opensignalbox/interface/web_ui/src/components/ui/tabs/TabsList.vue +25 -0
  169. opensignalbox/interface/web_ui/src/components/ui/tabs/TabsTrigger.vue +29 -0
  170. opensignalbox/interface/web_ui/src/components/ui/tabs/index.ts +4 -0
  171. opensignalbox/interface/web_ui/src/components/ui/tags-input/TagsInput.vue +22 -0
  172. opensignalbox/interface/web_ui/src/components/ui/tags-input/TagsInputInput.vue +19 -0
  173. opensignalbox/interface/web_ui/src/components/ui/tags-input/TagsInputItem.vue +22 -0
  174. opensignalbox/interface/web_ui/src/components/ui/tags-input/TagsInputItemDelete.vue +24 -0
  175. opensignalbox/interface/web_ui/src/components/ui/tags-input/TagsInputItemText.vue +19 -0
  176. opensignalbox/interface/web_ui/src/components/ui/tags-input/index.ts +5 -0
  177. opensignalbox/interface/web_ui/src/components/ui/textarea/Textarea.vue +24 -0
  178. opensignalbox/interface/web_ui/src/components/ui/textarea/index.ts +1 -0
  179. opensignalbox/interface/web_ui/src/components/ui/tooltip/Tooltip.vue +14 -0
  180. opensignalbox/interface/web_ui/src/components/ui/tooltip/TooltipContent.vue +31 -0
  181. opensignalbox/interface/web_ui/src/components/ui/tooltip/TooltipProvider.vue +11 -0
  182. opensignalbox/interface/web_ui/src/components/ui/tooltip/TooltipTrigger.vue +11 -0
  183. opensignalbox/interface/web_ui/src/components/ui/tooltip/index.ts +4 -0
  184. opensignalbox/interface/web_ui/src/libs/utils.ts +6 -0
  185. opensignalbox/interface/web_ui/src/main.ts +141 -0
  186. opensignalbox/interface/web_ui/src/views/Overview.vue +11 -0
  187. opensignalbox/interface/web_ui/src/views/Settings.vue +6 -0
  188. opensignalbox/interface/web_ui/src/views/SleafordEast.vue +205 -0
  189. opensignalbox/interface/web_ui/src/views/adapters/modbus/ModbusAdapterEdit.vue +343 -0
  190. opensignalbox/interface/web_ui/src/views/adapters/modbus/ModbusAdapterView.vue +270 -0
  191. opensignalbox/interface/web_ui/src/views/interfaces/InterfaceCreate.vue +0 -0
  192. opensignalbox/interface/web_ui/src/views/interfaces/basicio/BasicIOInterfaceEdit.vue +795 -0
  193. opensignalbox/interface/web_ui/src/views/interfaces/basicio/BasicIOInterfaceView.vue +648 -0
  194. opensignalbox/interface/web_ui/src/views/interfaces/bell/BellInterfaceEdit.vue +790 -0
  195. opensignalbox/interface/web_ui/src/views/interfaces/bell/BellInterfaceView.vue +437 -0
  196. opensignalbox/interface/web_ui/src/vite-env.d.ts +1 -0
  197. opensignalbox/interface/web_ui/tailwind.config.js +94 -0
  198. opensignalbox/interface/web_ui/tsconfig.app.json +27 -0
  199. opensignalbox/interface/web_ui/tsconfig.json +30 -0
  200. opensignalbox/interface/web_ui/tsconfig.node.json +12 -0
  201. opensignalbox/interface/web_ui/tsconfig.tsbuildinfo +1 -0
  202. opensignalbox/interface/web_ui/vite.config.d.ts +2 -0
  203. opensignalbox/interface/web_ui/vite.config.js +60 -0
  204. opensignalbox/interface/web_ui/vite.config.ts +62 -0
  205. opensignalbox_interface-0.1.0.dist-info/METADATA +49 -0
  206. opensignalbox_interface-0.1.0.dist-info/RECORD +209 -0
  207. opensignalbox_interface-0.1.0.dist-info/WHEEL +4 -0
  208. opensignalbox_interface-0.1.0.dist-info/entry_points.txt +2 -0
  209. opensignalbox_interface-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,795 @@
1
+ <script setup lang="ts">
2
+ import { Button } from '@/components/ui/button'
3
+ import {
4
+ Card,
5
+ CardContent,
6
+ CardDescription,
7
+ CardHeader,
8
+ CardTitle
9
+ } from '@/components/ui/card'
10
+ import { Label } from '@/components/ui/label'
11
+ import { Input } from '@/components/ui/input'
12
+ import { Checkbox } from '@/components/ui/checkbox'
13
+ import {
14
+ NumberField,
15
+ NumberFieldContent,
16
+ NumberFieldDecrement,
17
+ NumberFieldIncrement,
18
+ NumberFieldInput,
19
+ } from '@/components/ui/number-field'
20
+ import {
21
+ Select,
22
+ SelectContent,
23
+ SelectGroup,
24
+ SelectItem,
25
+ SelectTrigger,
26
+ SelectValue,
27
+ } from '@/components/ui/select'
28
+ import {
29
+ Table,
30
+ TableBody,
31
+ TableCell,
32
+ TableHead,
33
+ TableHeader,
34
+ TableRow,
35
+ } from '@/components/ui/table'
36
+ import {
37
+ Collapsible,
38
+ CollapsibleContent,
39
+ } from '@/components/ui/collapsible'
40
+ import {
41
+ TagsInput,
42
+ TagsInputInput,
43
+ TagsInputItem,
44
+ TagsInputItemDelete,
45
+ TagsInputItemText
46
+ } from '@/components/ui/tags-input'
47
+ import {
48
+ Sheet,
49
+ SheetContent,
50
+ SheetDescription,
51
+ SheetHeader,
52
+ SheetTitle
53
+ } from '@/components/ui/sheet'
54
+ import { ChevronDown, ChevronLeft, ChevronUp, Search } from 'lucide-vue-next'
55
+ import SharedVariablePicker from '@/components/SharedVariablePicker.vue'
56
+ import { reactive, computed, ref } from 'vue'
57
+ import { useRoute, useRouter } from 'vue-router'
58
+ import { useForm } from '@tanstack/vue-form';
59
+ import { useQuery } from '@tanstack/vue-query';
60
+ import axios from 'axios'
61
+ import { z } from 'zod'
62
+
63
+ const route = useRoute()
64
+ const router = useRouter()
65
+
66
+ const maxChannelCount = 128
67
+
68
+ type InputMapping = {
69
+ channel: number
70
+ systemName: string
71
+ userName: string
72
+ description: string
73
+ tags: string[]
74
+ defaultValue: boolean
75
+ }
76
+
77
+ type OutputMapping = {
78
+ channel: number
79
+ sharedVariableName: string
80
+ }
81
+
82
+ type interfaceData = {
83
+ systemName: string
84
+ userName: string
85
+ description: string
86
+ connectionType: string
87
+ adapterName: string
88
+ address: number
89
+ inputStartAddress: number
90
+ inputBits: number
91
+ inputByteSwap: boolean
92
+ inputWordSwap: boolean
93
+ outputStartAddress: number
94
+ outputBits: number
95
+ outputByteSwap: boolean
96
+ outputWordSwap: boolean
97
+ inputMap: InputMapping[]
98
+ outputMap: OutputMapping[]
99
+ }
100
+
101
+ const connectionTypes = {
102
+ ModbusAdapter: {
103
+ key: 'ModbusAdapter',
104
+ displayName: 'Modbus',
105
+ adapterType: 'Modbus'
106
+ },
107
+ DingtianModbusAdapter: {
108
+ key: 'DingtianModbusAdapter',
109
+ displayName: 'Dingtian Modbus',
110
+ adapterType: 'Modbus'
111
+ }
112
+ } as const
113
+
114
+ // Extract just the keys for the enum values
115
+ const connectionTypeEnum = Object.fromEntries(
116
+ Object.entries(connectionTypes).map(([key, config]) => [key, config.key])
117
+ ) as Record<keyof typeof connectionTypes, string>
118
+
119
+ const interfaceQuery = useQuery({
120
+ queryKey: ['interface', route.params.name],
121
+ queryFn: async () => axios.get(location.origin + "/api/interfaces/basicio/" + route.params.name),
122
+ select: (response): interfaceData => response.data,
123
+ enabled: "name" in route.params
124
+ });
125
+
126
+ function initInputMap(values: InputMapping[]): InputMapping[] {
127
+ const out: InputMapping[] = [...values]
128
+ for (let i=values.length; i<maxChannelCount; i++) {
129
+ out.push({channel: i+1, systemName:"", userName:"", description:"", tags:[], "defaultValue": false})
130
+ }
131
+ return out
132
+ }
133
+
134
+ function initOutputMap(values: OutputMapping[]): OutputMapping[] {
135
+ const out: OutputMapping[] = [...values]
136
+ for (let i=values.length; i<maxChannelCount; i++) {
137
+ out.push({channel: i+1, sharedVariableName: ""})
138
+ }
139
+ return out
140
+ }
141
+
142
+ function initInputMappingIsOpen() {
143
+ const out: boolean[] = []
144
+ for (let i=0; i<maxChannelCount; i++) {
145
+ out.push(false)
146
+ }
147
+ return out
148
+ }
149
+
150
+ const systemName = computed(() => interfaceQuery.data.value?.systemName || "")
151
+ const userName = computed(() => interfaceQuery.data.value?.userName || "")
152
+ const description = computed(() => interfaceQuery.data.value?.description || "")
153
+ const connectionType = computed(() => interfaceQuery.data.value?.connectionType || connectionTypeEnum.ModbusAdapter)
154
+ const adapterName = computed(() => interfaceQuery.data.value?.adapterName || "")
155
+ const address = computed(() => interfaceQuery.data.value?.address || 0)
156
+ const inputStartAddress = computed(() => interfaceQuery.data.value?.inputStartAddress || 0)
157
+ const inputBits = computed(() => interfaceQuery.data.value?.inputBits || 0)
158
+ const inputByteSwap = computed(() => interfaceQuery.data.value?.inputByteSwap || false)
159
+ const inputWordSwap = computed(() => interfaceQuery.data.value?.inputWordSwap || false)
160
+ const outputStartAddress = computed(() => interfaceQuery.data.value?.outputStartAddress || 0)
161
+ const outputBits = computed(() => interfaceQuery.data.value?.outputBits || 0)
162
+ const outputByteSwap = computed(() => interfaceQuery.data.value?.outputByteSwap || false)
163
+ const outputWordSwap = computed(() => interfaceQuery.data.value?.outputWordSwap || false)
164
+ const inputMap = computed(() => initInputMap(interfaceQuery.data.value?.inputMap || []))
165
+ const outputMap = computed(() => initOutputMap(interfaceQuery.data.value?.outputMap || []))
166
+
167
+ type adapterData = {
168
+ systemName: string
169
+ userName: string
170
+ adapterType: string
171
+ }
172
+
173
+ const adapterQuery = useQuery({
174
+ queryKey: ['adapters', connectionType.value],
175
+ queryFn: async () => axios.get(location.origin + "/api/adapters"),
176
+ select: (response) => {
177
+ const connectionConfig = connectionTypes[connectionType.value as keyof typeof connectionTypes];
178
+ if (!connectionConfig) return [];
179
+ return response.data.filter((adapter: adapterData) => adapter.adapterType === connectionConfig.adapterType);
180
+ }
181
+ });
182
+
183
+ const defaultValues = reactive({
184
+ systemName,
185
+ userName,
186
+ description,
187
+ connectionType,
188
+ adapterName,
189
+ address,
190
+ inputStartAddress,
191
+ inputBits,
192
+ inputByteSwap,
193
+ inputWordSwap,
194
+ outputStartAddress,
195
+ outputBits,
196
+ outputByteSwap,
197
+ outputWordSwap,
198
+ inputMap,
199
+ outputMap
200
+ });
201
+
202
+ const form = useForm<Partial<interfaceData>>({
203
+ defaultValues,
204
+ onSubmit: async ({ value }) => {
205
+ // Do something with form data
206
+ value.inputMap = value.inputMap?.slice(0, value.inputBits)
207
+ value.outputMap = value.outputMap?.slice(0, value.outputBits)
208
+
209
+ try {
210
+ if (route.params.name) {
211
+ axios.patch(location.origin + "/api/interfaces/basicio/" + route.params.name, value)
212
+ } else {
213
+ axios.post(location.origin + "/api/interfaces/basicio/", value)
214
+ }
215
+
216
+ // Navigate back to overview
217
+ router.push("/")
218
+ } catch (error) {
219
+ console.error('Failed to save interface:', error)
220
+ // TODO: Show error message to user
221
+ alert('Failed to save interface')
222
+ }
223
+ },
224
+ });
225
+
226
+ const inputBitsState = form.useStore((state) => state.values.inputBits)
227
+ const outputBitsState = form.useStore((state) => state.values.outputBits)
228
+ const inputMappingIsOpen = ref(initInputMappingIsOpen())
229
+
230
+ const outputPickerOpen = ref(false)
231
+ const editingOutput = ref(0)
232
+
233
+ function updateOutputMapping(variable: string) {
234
+ form.setFieldValue(`outputMap[${editingOutput.value}].sharedVariableName`, variable)
235
+ outputPickerOpen.value = false
236
+ }
237
+
238
+ </script>
239
+
240
+ <template>
241
+ <div class="w-full">
242
+ <p v-if="interfaceQuery.isLoading.value">Loading..</p>
243
+ <form v-else @submit.prevent.stop="form.handleSubmit">
244
+ <div class="flex items-center gap-4 mb-4">
245
+ <Button type="button" variant="outline" size="icon" class="h-7 w-7" @click.prevent="$router.back()">
246
+ <ChevronLeft class="h-4 w-4" />
247
+ <span class="sr-only">Back</span>
248
+ </Button>
249
+ <h1 class="flex-1 shrink-0 whitespace-nowrap text-xl font-semibold tracking-tight sm:grow-0">
250
+ Interface
251
+ </h1>
252
+ <div class="flex items-center gap-2 ml-auto">
253
+ <Button type="button" variant="outline" size="sm" @click.prevent="$router.back()">
254
+ Discard
255
+ </Button>
256
+ <Button size="sm">
257
+ Save Interface
258
+ </Button>
259
+ </div>
260
+ </div>
261
+ <div class="grid auto-rows-max items-start gap-4 md:gap-8 lg:grid-cols-3">
262
+ <Card>
263
+ <CardHeader>
264
+ <CardTitle>General Details</CardTitle>
265
+ <CardDescription>
266
+ Please enter some information about the interface
267
+ </CardDescription>
268
+ </CardHeader>
269
+ <CardContent>
270
+ <div class="grid gap-6">
271
+ <div class="grid gap-3">
272
+ <form.Field
273
+ name="systemName"
274
+ :validators="{
275
+ onBlur: z
276
+ .string()
277
+ .regex(/^([\da-z-])+$/, 'Name can only include a-z, 0-9, and hyphen.'),
278
+ }"
279
+ >
280
+ <template v-slot="{ field }">
281
+ <Label :for="field.name">System Name</Label>
282
+ <Input
283
+ :id="field.name"
284
+ :name="field.name"
285
+ :model-value="field.state.value as string"
286
+ placeholder="Type a name..."
287
+ @input="(e: any) => field.handleChange((e.target as HTMLInputElement).value)"
288
+ @blur="field.handleBlur"
289
+ class="w-full"
290
+ type="text"
291
+ />
292
+ <div role="alert" v-if="field.state.meta.errors.length">{{ field.state.meta.errors.join(', ') }}</div>
293
+ </template>
294
+ </form.Field>
295
+ <form.Field
296
+ name="userName"
297
+ :validators="{
298
+ onBlur: z
299
+ .string(),
300
+ }"
301
+ >
302
+ <template v-slot="{ field }">
303
+ <Label :for="field.name">User Name</Label>
304
+ <Input
305
+ :id="field.name"
306
+ :model-value="field.state.value as string"
307
+ placeholder="Type a name..."
308
+ @input="(e: any) => field.handleChange((e.target as HTMLInputElement).value)"
309
+ @blur="field.handleBlur"
310
+ class="w-full"
311
+ type="text"
312
+ />
313
+ <div role="alert" v-if="field.state.meta.errors.length">{{ field.state.meta.errors.join(', ') }}</div>
314
+ </template>
315
+ </form.Field>
316
+ <form.Field
317
+ name="description"
318
+ :validators="{
319
+ onBlur: z
320
+ .string()
321
+ }"
322
+ >
323
+ <template v-slot="{ field }">
324
+ <Label :for="field.name">Description</Label>
325
+ <Input
326
+ :id="field.name"
327
+ :model-value="field.state.value as string"
328
+ placeholder="Enter a description..."
329
+ @input="(e: any) => field.handleChange((e.target as HTMLInputElement).value)"
330
+ @blur="field.handleBlur"
331
+ class="w-full"
332
+ type="text"
333
+ />
334
+ <em role="alert" v-if="field.state.meta.errors.length">{{ field.state.meta.errors.join(', ') }}</em>
335
+ </template>
336
+ </form.Field>
337
+ <form.Field
338
+ name="connectionType"
339
+ >
340
+ <template v-slot="{ field }">
341
+ <Label :for="field.name">Connection Type</Label>
342
+ <Select
343
+ :id="field.name"
344
+ :model-value="field.state.value as string"
345
+ @update:modelValue="(newValue: string) => field.handleChange(newValue)"
346
+ @blur="field.handleBlur"
347
+ >
348
+ <SelectTrigger>
349
+ <SelectValue placeholder="Select connection type..." />
350
+ </SelectTrigger>
351
+ <SelectContent>
352
+ <SelectGroup>
353
+ <SelectItem v-for="(value, key) in connectionTypeEnum" :key="key" :value="value">
354
+ {{ connectionTypes[key as keyof typeof connectionTypes].displayName }}
355
+ </SelectItem>
356
+ </SelectGroup>
357
+ </SelectContent>
358
+ </Select>
359
+ <em role="alert" v-if="field.state.meta.errors.length">{{ field.state.meta.errors.join(', ') }}</em>
360
+ </template>
361
+
362
+ </form.Field>
363
+ <form.Field
364
+ name="adapterName"
365
+ :validators="{
366
+ onChange: z
367
+ .string({ required_error: 'Must provide adapter name!' })
368
+ }"
369
+ >
370
+ <template v-slot="{ field }">
371
+ <Label :for="field.name">Adapter User Name</Label>
372
+ <Select
373
+ :id="field.name"
374
+ :model-value="field.state.value as string"
375
+ @update:modelValue="(newValue: string) => field.handleChange(newValue)"
376
+ @blur="field.handleBlur"
377
+ >
378
+ <FormControl>
379
+ <SelectTrigger>
380
+ <SelectValue placeholder="Select an adapter..." />
381
+ </SelectTrigger>
382
+ </FormControl>
383
+ <SelectContent>
384
+ <SelectGroup>
385
+ <SelectItem v-for="adapter in adapterQuery.data?.value" :value="adapter.systemName">
386
+ {{ adapter.userName }}
387
+ </SelectItem>
388
+ </SelectGroup>
389
+ </SelectContent>
390
+ </Select>
391
+ <em role="alert" v-if="field.state.meta.errors.length">{{ field.state.meta.errors.join(', ') }}</em>
392
+ </template>
393
+ </form.Field>
394
+ <form.Field
395
+ name="address"
396
+ :validators="{
397
+ onBlur: z
398
+ .number()
399
+ }"
400
+ >
401
+ <template v-slot="{ field }">
402
+ <NumberField
403
+ :id="field.name"
404
+ :model-value="field.state.value as number"
405
+ @update:modelValue="(newValue: number) => field.handleChange(newValue)"
406
+ @blur="field.handleBlur"
407
+ class="max-w-40"
408
+ :min="0"
409
+ :max="247"
410
+ >
411
+ <Label>Address</Label>
412
+ <NumberFieldContent>
413
+ <NumberFieldDecrement />
414
+ <NumberFieldInput />
415
+ <NumberFieldIncrement />
416
+ </NumberFieldContent>
417
+ </NumberField>
418
+ <em role="alert" v-if="field.state.meta.errors.length">{{ field.state.meta.errors.join(', ') }}</em>
419
+ </template>
420
+ </form.Field>
421
+ </div>
422
+ </div>
423
+ </CardContent>
424
+ </Card>
425
+ <Card>
426
+ <CardHeader>
427
+ <CardTitle>Input Configuration</CardTitle>
428
+ <CardDescription>
429
+ Please define the variables that the inputs will update.
430
+ </CardDescription>
431
+ </CardHeader>
432
+ <CardContent>
433
+ <div class="grid gap-3">
434
+ <form.Field
435
+ name="inputBits"
436
+ :validators="{
437
+ onChange: z
438
+ .number()
439
+ .min(0)
440
+ .max(maxChannelCount, `Maximum value is ${maxChannelCount}.`)
441
+ }"
442
+ >
443
+ <template v-slot="{ field }">
444
+ <NumberField
445
+ :id="field.name"
446
+ :model-value="field.state.value as number"
447
+ :min="0"
448
+ :max="maxChannelCount"
449
+ @update:modelValue="(newValue: number) => field.handleChange(newValue)"
450
+ @blur="field.handleBlur"
451
+ class="max-w-40"
452
+ >
453
+ <Label>Input Bits</Label>
454
+ <NumberFieldContent>
455
+ <NumberFieldDecrement />
456
+ <NumberFieldInput />
457
+ <NumberFieldIncrement />
458
+ </NumberFieldContent>
459
+ </NumberField>
460
+ <em role="alert" v-if="field.state.meta.errors.length">{{ field.state.meta.errors.join(', ') }}</em>
461
+ </template>
462
+ </form.Field>
463
+ <form.Field
464
+ name="inputStartAddress"
465
+ :validators="{
466
+ onBlur: z
467
+ .number()
468
+ }"
469
+ >
470
+ <template v-slot="{ field }">
471
+ <NumberField
472
+ :id="field.name"
473
+ :model-value="field.state.value as number"
474
+ @update:modelValue="(newValue: number) => field.handleChange(newValue)"
475
+ @blur="field.handleBlur"
476
+ class="max-w-40"
477
+ :min="0"
478
+ :max="65535"
479
+ >
480
+ <Label>Input Start Address</Label>
481
+ <NumberFieldContent>
482
+ <NumberFieldDecrement />
483
+ <NumberFieldInput />
484
+ <NumberFieldIncrement />
485
+ </NumberFieldContent>
486
+ </NumberField>
487
+ <em role="alert" v-if="field.state.meta.errors.length">{{ field.state.meta.errors.join(', ') }}</em>
488
+ </template>
489
+ </form.Field>
490
+ <form.Field
491
+ name="inputByteSwap">
492
+ <template v-slot="{ field }">
493
+ <span>
494
+ <Label :for="field.name">Input Byte Swap:</Label>
495
+ <Checkbox
496
+ :id="field.name"
497
+ :checked="field.state.value as boolean"
498
+ @update:checked="(newValue: boolean) => field.handleChange(newValue)"
499
+ class="ml-2"
500
+ />
501
+ </span>
502
+ </template>
503
+ </form.Field>
504
+ <form.Field
505
+ name="inputWordSwap">
506
+ <template v-slot="{ field }">
507
+ <span>
508
+ <Label :for="field.name">Input Word Swap:</Label>
509
+ <Checkbox
510
+ :id="field.name"
511
+ :checked="field.state.value as boolean"
512
+ @update:checked="(newValue: boolean) => field.handleChange(newValue)"
513
+ class="ml-2"
514
+ />
515
+ </span>
516
+ </template>
517
+ </form.Field>
518
+ </div>
519
+ <form.Field name="inputMap" v-if="inputBitsState && inputBitsState > 0">
520
+ <template v-slot="{ field }">
521
+ <Table class="mt-4">
522
+ <TableHeader class="border-t">
523
+ <TableRow>
524
+ <TableHead class="w-[100px]">
525
+ Channel
526
+ </TableHead>
527
+ <TableHead>Variable</TableHead>
528
+ </TableRow>
529
+ </TableHeader>
530
+ <TableBody>
531
+ <TableRow
532
+ v-for="(_, i) of (field.state.value as InputMapping[]).slice(0, inputBitsState)"
533
+ :key="i"
534
+ >
535
+ <TableCell class="font-medium align-top">
536
+ <h1 class="text-lg font-semibold tracking-tight">
537
+ {{ (field.state.value as InputMapping[])[i].channel }}
538
+ </h1>
539
+ </TableCell>
540
+ <TableCell>
541
+ <div class="flex items-center justify-between">
542
+ <h1 class="text-lg font-semibold tracking-tight">
543
+ <span v-html="(field.state.value as InputMapping[])[i].userName ? (field.state.value as InputMapping[])[i].userName : '<em>None</em>'"></span>
544
+ </h1>
545
+ <Button type="button" variant="outline" size="icon" class="h-7 w-7 mr-1" @click.prevent="() => inputMappingIsOpen[i] = !inputMappingIsOpen[i]">
546
+ <ChevronDown v-if="!inputMappingIsOpen[i]" class="h-4 w-4" />
547
+ <ChevronUp v-else class="h-4 w-4" />
548
+ </Button>
549
+ </div>
550
+ <Collapsible v-model:open="inputMappingIsOpen[i]">
551
+ <CollapsibleContent>
552
+ <div class="grid gap-3 ml-2 mt-5 p-1">
553
+ <form.Field
554
+ :name="`inputMap[${i}].systemName`"
555
+ :validators="{
556
+ onBlur: z
557
+ .string()
558
+ }"
559
+ >
560
+ <template v-slot="{ field }">
561
+ <Label :for="field.name">System Name</Label>
562
+ <Input
563
+ :id="field.name"
564
+ :name="field.name"
565
+ :model-value="field.state.value as string"
566
+ placeholder="Type a name..."
567
+ @input="(e: any) => field.handleChange((e.target as HTMLInputElement).value)"
568
+ @blur="field.handleBlur"
569
+ class="w-full"
570
+ type="text"
571
+ />
572
+ <div role="alert" v-if="field.state.meta.errors.length">{{ field.state.meta.errors.join(', ') }}</div>
573
+ </template>
574
+ </form.Field>
575
+ <form.Field
576
+ :name="`inputMap[${i}].userName`"
577
+ :validators="{
578
+ onBlur: z
579
+ .string()
580
+ }"
581
+ >
582
+ <template v-slot="{ field }">
583
+ <Label :for="field.name">User Name</Label>
584
+ <Input
585
+ :id="field.name"
586
+ :name="field.name"
587
+ :model-value="field.state.value as string"
588
+ placeholder="Type a name..."
589
+ @input="(e: any) => field.handleChange((e.target as HTMLInputElement).value)"
590
+ @blur="field.handleBlur"
591
+ class="w-full"
592
+ type="text"
593
+ />
594
+ <div role="alert" v-if="field.state.meta.errors.length">{{ field.state.meta.errors.join(', ') }}</div>
595
+ </template>
596
+ </form.Field>
597
+ <form.Field
598
+ :name="`inputMap[${i}].description`"
599
+ :validators="{
600
+ onBlur: z
601
+ .string()
602
+ }"
603
+ >
604
+ <template v-slot="{ field }">
605
+ <Label :for="field.name">Description</Label>
606
+ <Input
607
+ :id="field.name"
608
+ :name="field.name"
609
+ :model-value="field.state.value as string"
610
+ placeholder="Enter a description..."
611
+ @input="(e: any) => field.handleChange((e.target as HTMLInputElement).value)"
612
+ @blur="field.handleBlur"
613
+ class="w-full"
614
+ type="text"
615
+ />
616
+ <div role="alert" v-if="field.state.meta.errors.length">{{ field.state.meta.errors.join(', ') }}</div>
617
+ </template>
618
+ </form.Field>
619
+ <form.Field
620
+ :name="`inputMap[${i}].tags`"
621
+ >
622
+ <template v-slot="{ field }">
623
+ <Label :for="field.name">Tags</Label>
624
+ <TagsInput
625
+ :id="field.name"
626
+ :name="field.name"
627
+ :model-value="field.state.value as string[]"
628
+ @update:model-value="(newValue) => field.handleChange(newValue as string[])"
629
+ @blur="field.handleBlur"
630
+ class="w-full"
631
+ >
632
+ <TagsInputItem v-for="item in field.state.value as string[]" :key="item" :value="item">
633
+ <TagsInputItemText />
634
+ <TagsInputItemDelete />
635
+ </TagsInputItem>
636
+ <TagsInputInput placeholder="Tags..." />
637
+ </TagsInput>
638
+ <div role="alert" v-if="field.state.meta.errors.length">{{ field.state.meta.errors.join(', ') }}</div>
639
+ </template>
640
+ </form.Field>
641
+ </div>
642
+ </CollapsibleContent>
643
+ </Collapsible>
644
+ </TableCell>
645
+ </TableRow>
646
+ </TableBody>
647
+ </Table>
648
+ </template>
649
+ </form.Field>
650
+ </CardContent>
651
+ </Card>
652
+ <Card>
653
+ <CardHeader>
654
+ <CardTitle>Output Configuration</CardTitle>
655
+ <CardDescription>
656
+ Please select the variables that control the outputs.
657
+ </CardDescription>
658
+ </CardHeader>
659
+ <CardContent>
660
+ <div class="grid gap-3">
661
+ <form.Field
662
+ name="outputBits"
663
+ :validators="{
664
+ onBlur: z
665
+ .number()
666
+ .min(0)
667
+ .max(maxChannelCount, `Maximum value is ${maxChannelCount}.`)
668
+ }"
669
+ >
670
+ <template v-slot="{ field }">
671
+ <NumberField
672
+ :id="field.name"
673
+ :model-value="field.state.value as number"
674
+ :min="0"
675
+ :max="maxChannelCount"
676
+ @update:modelValue="(newValue: number) => field.handleChange(newValue)"
677
+ @blur="field.handleBlur"
678
+ class="max-w-40"
679
+ >
680
+ <Label>Output Bits</Label>
681
+ <NumberFieldContent>
682
+ <NumberFieldDecrement />
683
+ <NumberFieldInput />
684
+ <NumberFieldIncrement />
685
+ </NumberFieldContent>
686
+ </NumberField>
687
+ <em role="alert" v-if="field.state.meta.errors.length">{{ field.state.meta.errors.join(', ') }}</em>
688
+ </template>
689
+ </form.Field>
690
+ <form.Field
691
+ name="outputStartAddress"
692
+ :validators="{
693
+ onBlur: z
694
+ .number()
695
+ }"
696
+ >
697
+ <template v-slot="{ field }">
698
+ <NumberField
699
+ :id="field.name"
700
+ :model-value="field.state.value as number"
701
+ @update:modelValue="(newValue: number) => field.handleChange(newValue)"
702
+ @blur="field.handleBlur"
703
+ class="max-w-40"
704
+ :min="0"
705
+ :max="65535"
706
+ >
707
+ <Label>Output Start Address</Label>
708
+ <NumberFieldContent>
709
+ <NumberFieldDecrement />
710
+ <NumberFieldInput />
711
+ <NumberFieldIncrement />
712
+ </NumberFieldContent>
713
+ </NumberField>
714
+ <em role="alert" v-if="field.state.meta.errors.length">{{ field.state.meta.errors.join(', ') }}</em>
715
+ </template>
716
+ </form.Field>
717
+ <form.Field
718
+ name="outputByteSwap">
719
+ <template v-slot="{ field }">
720
+ <span>
721
+ <Label :for="field.name">Output Byte Swap:</Label>
722
+ <Checkbox
723
+ :id="field.name"
724
+ :checked="field.state.value as boolean"
725
+ @update:checked="(newValue: boolean) => field.handleChange(newValue)"
726
+ class="ml-2"
727
+ />
728
+ </span>
729
+ </template>
730
+ </form.Field>
731
+ <form.Field
732
+ name="outputWordSwap">
733
+ <template v-slot="{ field }">
734
+ <span>
735
+ <Label :for="field.name">Output Word Swap:</Label>
736
+ <Checkbox
737
+ :id="field.name"
738
+ :checked="field.state.value as boolean"
739
+ @update:checked="(newValue: boolean) => field.handleChange(newValue)"
740
+ class="ml-2"
741
+ />
742
+ </span>
743
+ </template>
744
+ </form.Field>
745
+ </div>
746
+ <form.Field name="outputMap" v-if="outputBitsState && outputBitsState > 0">
747
+ <template v-slot="{ field }">
748
+ <Table class="mt-4">
749
+ <TableHeader class="border-t">
750
+ <TableRow>
751
+ <TableHead class="w-[100px]">
752
+ Channel
753
+ </TableHead>
754
+ <TableHead>Variable</TableHead>
755
+ </TableRow>
756
+ </TableHeader>
757
+ <TableBody>
758
+ <TableRow v-for="(_, i) of (field.state.value as OutputMapping[]).slice(0, outputBitsState)" :key="i">
759
+ <TableCell class="font-medium align-top">
760
+ <h1 class="text-lg font-semibold tracking-tight">
761
+ {{ (field.state.value as OutputMapping[])[i].channel }}
762
+ </h1>
763
+ </TableCell>
764
+ <TableCell>
765
+ <div class="flex items-center justify-between">
766
+ <h1 class="text-lg font-semibold tracking-tight">
767
+ <span v-html="(field.state.value as OutputMapping[])[i].sharedVariableName ? (field.state.value as OutputMapping[])[i].sharedVariableName : '<em>None</em>'"></span>
768
+ </h1>
769
+ <Button type="button" variant="outline" size="icon" class="h-7 w-7" @click="() => {editingOutput = i;outputPickerOpen = true}">
770
+ <Search class="h-4 w-4" />
771
+ </Button>
772
+ </div>
773
+ </TableCell>
774
+ </TableRow>
775
+ </TableBody>
776
+ </Table>
777
+ </template>
778
+ </form.Field>
779
+ </CardContent>
780
+ </Card>
781
+ </div>
782
+ </form>
783
+ </div>
784
+ <Sheet v-model:open="outputPickerOpen">
785
+ <SheetContent>
786
+ <SheetHeader>
787
+ <SheetTitle>Select a boolean shared variable.</SheetTitle>
788
+ <SheetDescription>
789
+ You can use the filters below to search.
790
+ </SheetDescription>
791
+ </SheetHeader>
792
+ <SharedVariablePicker @selectVariable="updateOutputMapping" typestring_filter="Boolean" typestring_filter_lock/>
793
+ </SheetContent>
794
+ </Sheet>
795
+ </template>