InputVariablesModal.svelte 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. <script lang="ts">
  2. import { getContext, onMount, tick } from 'svelte';
  3. import { models, config } from '$lib/stores';
  4. import { toast } from 'svelte-sonner';
  5. import { copyToClipboard } from '$lib/utils';
  6. import XMark from '$lib/components/icons/XMark.svelte';
  7. import Modal from '$lib/components/common/Modal.svelte';
  8. import Spinner from '$lib/components/common/Spinner.svelte';
  9. import MapSelector from '$lib/components/common/Valves/MapSelector.svelte';
  10. const i18n = getContext('i18n');
  11. export let show = false;
  12. export let variables = {};
  13. export let onSave = (e) => {};
  14. let loading = false;
  15. let variableValues = {};
  16. const submitHandler = async () => {
  17. onSave(variableValues);
  18. show = false;
  19. };
  20. const init = async () => {
  21. loading = true;
  22. variableValues = {};
  23. for (const variable of Object.keys(variables)) {
  24. if (variables[variable]?.default !== undefined) {
  25. variableValues[variable] = variables[variable].default;
  26. } else {
  27. variableValues[variable] = '';
  28. }
  29. }
  30. loading = false;
  31. await tick();
  32. const firstInputElement = document.getElementById('input-variable-0');
  33. if (firstInputElement) {
  34. console.log('Focusing first input element:', firstInputElement);
  35. firstInputElement.focus();
  36. }
  37. };
  38. $: if (show) {
  39. init();
  40. }
  41. </script>
  42. <Modal bind:show size="md">
  43. <div>
  44. <div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
  45. <div class=" text-lg font-medium self-center">{$i18n.t('Input Variables')}</div>
  46. <button
  47. class="self-center"
  48. on:click={() => {
  49. show = false;
  50. }}
  51. >
  52. <XMark className={'size-5'} />
  53. </button>
  54. </div>
  55. <div class="flex flex-col md:flex-row w-full px-5 pb-4 md:space-x-4 dark:text-gray-200">
  56. <div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
  57. <form
  58. class="flex flex-col w-full"
  59. on:submit|preventDefault={() => {
  60. submitHandler();
  61. }}
  62. >
  63. <div class="px-1">
  64. {#if !loading}
  65. <div class="flex flex-col gap-1">
  66. {#each Object.keys(variables) as variable, idx}
  67. {@const { type, ...variableAttributes } = variables[variable] ?? {}}
  68. <div class=" py-0.5 w-full justify-between">
  69. <div class="flex w-full justify-between mb-1.5">
  70. <div class=" self-center text-xs font-medium">
  71. {variable}
  72. {#if variables[variable]?.required ?? false}
  73. <span class=" text-gray-500">*{$i18n.t('required')}</span>
  74. {/if}
  75. </div>
  76. </div>
  77. <div class="flex mt-0.5 mb-0.5 space-x-2">
  78. <div class=" flex-1">
  79. {#if variables[variable]?.type === 'select'}
  80. {@const options = variableAttributes?.options ?? []}
  81. {@const placeholder = variableAttributes?.placeholder ?? ''}
  82. <select
  83. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
  84. bind:value={variableValues[variable]}
  85. id="input-variable-{idx}"
  86. >
  87. {#if placeholder}
  88. <option value="" disabled selected>
  89. {placeholder}
  90. </option>
  91. {/if}
  92. {#each options as option}
  93. <option value={option} selected={option === variableValues[variable]}>
  94. {option}
  95. </option>
  96. {/each}
  97. </select>
  98. {:else if variables[variable]?.type === 'checkbox'}
  99. <div class="flex items-center space-x-2">
  100. <div class="relative flex justify-center items-center gap-2">
  101. <input
  102. type="checkbox"
  103. bind:checked={variableValues[variable]}
  104. class="size-3.5 rounded cursor-pointer border border-gray-200 dark:border-gray-700"
  105. id="input-variable-{idx}"
  106. {...variableAttributes}
  107. />
  108. <label for="input-variable-{idx}" class="text-sm"
  109. >{variables[variable]?.label ?? variable}</label
  110. >
  111. </div>
  112. <input
  113. type="text"
  114. class="flex-1 py-1 text-sm dark:text-gray-300 bg-transparent outline-hidden"
  115. placeholder={$i18n.t('Enter value (true/false)')}
  116. bind:value={variableValues[variable]}
  117. autocomplete="off"
  118. required={variables[variable]?.required ?? false}
  119. />
  120. </div>
  121. {:else if variables[variable]?.type === 'color'}
  122. <div class="flex items-center space-x-2">
  123. <div class="relative size-6">
  124. <input
  125. type="color"
  126. class="size-6 rounded cursor-pointer border border-gray-200 dark:border-gray-700"
  127. value={variableValues[variable]}
  128. id="input-variable-{idx}"
  129. on:input={(e) => {
  130. // Convert the color value to uppercase immediately
  131. variableValues[variable] = e.target.value.toUpperCase();
  132. }}
  133. {...variableAttributes}
  134. />
  135. </div>
  136. <input
  137. type="text"
  138. class="flex-1 py-2 text-sm dark:text-gray-300 bg-transparent outline-hidden"
  139. placeholder={$i18n.t('Enter hex color (e.g. #FF0000)')}
  140. bind:value={variableValues[variable]}
  141. autocomplete="off"
  142. required={variables[variable]?.required ?? false}
  143. />
  144. </div>
  145. {:else if variables[variable]?.type === 'date'}
  146. <input
  147. type="date"
  148. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
  149. placeholder={variables[variable]?.placeholder ?? ''}
  150. bind:value={variableValues[variable]}
  151. autocomplete="off"
  152. id="input-variable-{idx}"
  153. required={variables[variable]?.required ?? false}
  154. {...variableAttributes}
  155. />
  156. {:else if variables[variable]?.type === 'datetime-local'}
  157. <input
  158. type="datetime-local"
  159. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
  160. placeholder={variables[variable]?.placeholder ?? ''}
  161. bind:value={variableValues[variable]}
  162. autocomplete="off"
  163. id="input-variable-{idx}"
  164. required={variables[variable]?.required ?? false}
  165. {...variableAttributes}
  166. />
  167. {:else if variables[variable]?.type === 'email'}
  168. <input
  169. type="email"
  170. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
  171. placeholder={variables[variable]?.placeholder ?? ''}
  172. bind:value={variableValues[variable]}
  173. autocomplete="off"
  174. id="input-variable-{idx}"
  175. required={variables[variable]?.required ?? false}
  176. {...variableAttributes}
  177. />
  178. {:else if variables[variable]?.type === 'month'}
  179. <input
  180. type="month"
  181. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
  182. placeholder={variables[variable]?.placeholder ?? ''}
  183. bind:value={variableValues[variable]}
  184. autocomplete="off"
  185. id="input-variable-{idx}"
  186. required={variables[variable]?.required ?? false}
  187. {...variableAttributes}
  188. />
  189. {:else if variables[variable]?.type === 'number'}
  190. <input
  191. type="number"
  192. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
  193. placeholder={variables[variable]?.placeholder ?? ''}
  194. bind:value={variableValues[variable]}
  195. autocomplete="off"
  196. id="input-variable-{idx}"
  197. required={variables[variable]?.required ?? false}
  198. {...variableAttributes}
  199. />
  200. {:else if variables[variable]?.type === 'range'}
  201. <div class="flex items-center space-x-2">
  202. <div class="relative flex justify-center items-center gap-2 flex-1">
  203. <input
  204. type="range"
  205. bind:value={variableValues[variable]}
  206. class="w-full rounded-lg py-1 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
  207. id="input-variable-{idx}"
  208. {...variableAttributes}
  209. />
  210. </div>
  211. <input
  212. type="text"
  213. class=" py-1 text-sm dark:text-gray-300 bg-transparent outline-hidden text-right"
  214. placeholder={$i18n.t('Enter value')}
  215. bind:value={variableValues[variable]}
  216. autocomplete="off"
  217. required={variables[variable]?.required ?? false}
  218. />
  219. </div>
  220. <!-- <input
  221. type="range"
  222. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
  223. placeholder={variables[variable]?.placeholder ?? ''}
  224. bind:value={variableValues[variable]}
  225. autocomplete="off"
  226. id="input-variable-{idx}"
  227. required
  228. /> -->
  229. {:else if variables[variable]?.type === 'tel'}
  230. <input
  231. type="tel"
  232. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
  233. placeholder={variables[variable]?.placeholder ?? ''}
  234. bind:value={variableValues[variable]}
  235. autocomplete="off"
  236. id="input-variable-{idx}"
  237. required={variables[variable]?.required ?? false}
  238. {...variableAttributes}
  239. />
  240. {:else if variables[variable]?.type === 'text'}
  241. <input
  242. type="text"
  243. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
  244. placeholder={variables[variable]?.placeholder ?? ''}
  245. bind:value={variableValues[variable]}
  246. autocomplete="off"
  247. id="input-variable-{idx}"
  248. required={variables[variable]?.required ?? false}
  249. {...variableAttributes}
  250. />
  251. {:else if variables[variable]?.type === 'time'}
  252. <input
  253. type="time"
  254. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
  255. placeholder={variables[variable]?.placeholder ?? ''}
  256. bind:value={variableValues[variable]}
  257. autocomplete="off"
  258. id="input-variable-{idx}"
  259. required={variables[variable]?.required ?? false}
  260. {...variableAttributes}
  261. />
  262. {:else if variables[variable]?.type === 'url'}
  263. <input
  264. type="url"
  265. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
  266. placeholder={variables[variable]?.placeholder ?? ''}
  267. bind:value={variableValues[variable]}
  268. autocomplete="off"
  269. id="input-variable-{idx}"
  270. required={variables[variable]?.required ?? false}
  271. {...variableAttributes}
  272. />
  273. {:else if variables[variable]?.type === 'map'}
  274. <!-- EXPERIMENTAL INPUT TYPE, DO NOT USE IN PRODUCTION -->
  275. <div class="flex flex-col items-center gap-1">
  276. <MapSelector
  277. setViewLocation={((variableValues[variable] ?? '').includes(',') ??
  278. false)
  279. ? variableValues[variable].split(',')
  280. : null}
  281. onClick={(value) => {
  282. variableValues[variable] = value;
  283. }}
  284. />
  285. <input
  286. type="text"
  287. class=" w-full py-1 text-left text-sm dark:text-gray-300 bg-transparent outline-hidden"
  288. placeholder={$i18n.t('Enter coordinates (e.g. 51.505, -0.09)')}
  289. bind:value={variableValues[variable]}
  290. autocomplete="off"
  291. required={variables[variable]?.required ?? false}
  292. />
  293. </div>
  294. {:else}
  295. <textarea
  296. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
  297. placeholder={variables[variable]?.placeholder ?? ''}
  298. bind:value={variableValues[variable]}
  299. autocomplete="off"
  300. id="input-variable-{idx}"
  301. required={variables[variable]?.required ?? false}
  302. />
  303. {/if}
  304. </div>
  305. </div>
  306. <!-- {#if (valvesSpec.properties[property]?.description ?? null) !== null}
  307. <div class="text-xs text-gray-500">
  308. {valvesSpec.properties[property].description}
  309. </div>
  310. {/if} -->
  311. </div>
  312. {/each}
  313. </div>
  314. {:else}
  315. <Spinner className="size-5" />
  316. {/if}
  317. </div>
  318. <div class="flex justify-end pt-3 text-sm font-medium">
  319. <button
  320. class="px-3.5 py-1.5 text-sm font-medium bg-white hover:bg-gray-100 text-black dark:bg-black dark:text-white dark:hover:bg-gray-900 transition rounded-full"
  321. type="button"
  322. on:click={() => {
  323. show = false;
  324. }}
  325. >
  326. {$i18n.t('Cancel')}
  327. </button>
  328. <button
  329. class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
  330. type="submit"
  331. >
  332. {$i18n.t('Save')}
  333. </button>
  334. </div>
  335. </form>
  336. </div>
  337. </div>
  338. </div>
  339. </Modal>