vue.rules.js 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268
  1. /**
  2. * The Vue application instance.
  3. * https://vuejs.org/guide/essentials/application.html
  4. * This is the root component
  5. */
  6. const app = Vue.createApp({
  7. /**
  8. * to prevent conflict with jekyll we change the
  9. * delimiters from {{ }} to [[ ]]
  10. */
  11. delimiters: ['[[', ']]'],
  12. /**
  13. * The data object of the root component.
  14. * These variables are available in the html template.
  15. */
  16. data() {
  17. return {
  18. // The rule object
  19. rule: global.rule || {
  20. ruleID: "",
  21. timestamp: "",
  22. icon: "",
  23. name: "",
  24. lineage: "",
  25. summary: "",
  26. config: {},
  27. creator: {
  28. name: "",
  29. url: "",
  30. },
  31. modules: [],
  32. latest_version: 1
  33. },
  34. legacy: false,
  35. rID: false,
  36. loading: false,
  37. publishing: false,
  38. edit: false,
  39. isOwner: false,
  40. isAdmin: false,
  41. view: (global.rule) ? true : false,
  42. preview: (global.rule) ? true : false,
  43. template: (global.rule) ? true : false,
  44. steinAPI: 'https://api.steinhq.com/v1/storages/5e8b937ab88d3d04ae0816a5',
  45. backendUrl: 'http://localhost:3000/api/publish_rule',
  46. backendUrlrule: 'http://localhost:3000/api/get_rule',
  47. backedDeleterule: 'http://localhost:3000/api/delete_rule',
  48. adminMail: "communityrule@medlab.host",
  49. // TODO keep an array of past states for undo/redo
  50. history: [],
  51. // the data of the current module in the editor
  52. editor: {
  53. source: null,
  54. previousState: null,
  55. module: null,
  56. },
  57. // the module that is currently being dragged
  58. holding: false,
  59. // object of icons for easy access
  60. icons: {
  61. plus: '/assets/tabler_icons/circle-plus.svg',
  62. info: '/assets/tabler_icons/info-circle.svg',
  63. chevron: '/assets/tabler_icons/chevrons-down.svg',
  64. blank: '/assets/tabler_icons/circle-dotted.svg',
  65. view: '/assets/tabler_icons/eye.svg',
  66. edit: '/assets/tabler_icons/tool.svg',
  67. plus: '/assets/tabler_icons/plus.svg',
  68. minus: '/assets/tabler_icons/minus.svg',
  69. publish: '/assets/tabler_icons/cloud-upload.svg',
  70. download: '/assets/tabler_icons/download.svg',
  71. export: '/assets/tabler_icons/file-download.svg',
  72. upload: '/assets/tabler_icons/file-upload.svg',
  73. fork: '/assets/tabler_icons/git-fork.svg',
  74. },
  75. // icons available in the editor
  76. moduleIcons: {
  77. culture: '/assets/tabler_icons/palette.svg',
  78. decision: '/assets/tabler_icons/thumb-up.svg',
  79. process: '/assets/tabler_icons/rotate.svg',
  80. structure: '/assets/tabler_icons/building.svg',
  81. relationship: '/assets/tabler_icons/heart.svg',
  82. economic: '/assets/tabler_icons/coin.svg',
  83. legal: '/assets/tabler_icons/license.svg',
  84. map: '/assets/tabler_icons/map.svg',
  85. communications: '/assets/tabler_icons/microphone.svg',
  86. },
  87. // icons available for rules
  88. ruleIcons: {
  89. atom: '/assets/tabler_icons/atom.svg',
  90. bandage: '/assets/tabler_icons/bandage.svg',
  91. book: '/assets/tabler_icons/book.svg',
  92. box: '/assets/tabler_icons/box.svg',
  93. church: '/assets/tabler_icons/building-church.svg',
  94. store: '/assets/tabler_icons/building-store.svg',
  95. brush: '/assets/tabler_icons/brush.svg',
  96. car: '/assets/tabler_icons/car.svg',
  97. clock: '/assets/tabler_icons/clock.svg',
  98. cloud: '/assets/tabler_icons/cloud.svg',
  99. compass: '/assets/tabler_icons/compass.svg',
  100. game: '/assets/tabler_icons/device-gamepad.svg',
  101. flask: '/assets/tabler_icons/flask.svg',
  102. location: '/assets/tabler_icons/location.svg',
  103. moon: '/assets/tabler_icons/moon.svg',
  104. settings: '/assets/tabler_icons/settings.svg',
  105. shield: '/assets/tabler_icons/shield.svg',
  106. star: '/assets/tabler_icons/star.svg',
  107. tool: '/assets/tabler_icons/tool.svg',
  108. world: '/assets/tabler_icons/world.svg',
  109. },
  110. // the template for modules
  111. moduleTemplate: {
  112. moduleID: "",
  113. name: "",
  114. icon: "",
  115. summary: "",
  116. config: {},
  117. type: "",
  118. modules: []
  119. },
  120. // tracks the current module library tab that is open
  121. moduleLibrary: 'culture',
  122. moduleTypes: {
  123. // custom: {
  124. // question: 'Modules that you\'ve created.',
  125. // icon: '/assets/tabler_icons/circle-plus.svg',
  126. // open: true
  127. // },
  128. culture: {
  129. question: 'What are the core missions, values, and norms?',
  130. icon: '/assets/tabler_icons/palette.svg',
  131. open: true
  132. },
  133. decision: {
  134. question: 'Who can make decisions and how?',
  135. icon: '/assets/tabler_icons/thumb-up.svg',
  136. open: false
  137. },
  138. process: {
  139. question: 'How are policies implemented, and how do they evolve?',
  140. icon: '/assets/tabler_icons/rotate.svg ',
  141. open: false
  142. },
  143. structure: {
  144. question: 'What kinds of roles and internal entities are there?',
  145. icon: '/assets/tabler_icons/building.svg',
  146. open: false
  147. }
  148. },
  149. // array of modules that have been created by the user
  150. // TODO: implement custom modules
  151. customModules: [],
  152. // library of existing modules
  153. modules: global.modules,
  154. exports: {
  155. markdown: null,
  156. json: null,
  157. }
  158. }
  159. },
  160. /**
  161. * Vue provide passes data to other components.
  162. * https://vuejs.org/guide/components/provide-inject.html#provide-inject
  163. */
  164. provide() {
  165. return {
  166. editor: this.editor,
  167. icons: this.icons,
  168. }
  169. },
  170. created() {
  171. this.addToEditor(this.newModule());
  172. var urlParams = new URLSearchParams(window.location.search);
  173. if (urlParams.has('r')) {
  174. this.rID = urlParams.get('r');
  175. this.preview = true;
  176. this.view = true;
  177. this.fetchRule(this.rID);
  178. }
  179. },
  180. computed: {
  181. /**
  182. * Exports the current rule into a normalized format
  183. * Cleans up all submodules so they are ready for export
  184. * @returns {Object} a Rule object
  185. */
  186. ruleExport() {
  187. //TODO: test if icon is an absolute url and only add the global if it is not
  188. /**
  189. * Takes a module and recursively cleans it and all submodules up
  190. * @param {Object} module a module object
  191. * @returns
  192. */
  193. function cleanModules(module) {
  194. const newModule = {
  195. moduleID: module.moduleID,
  196. name: module.name,
  197. icon: (module.icon && !module.icon.includes('http')) ? global.url + module.icon : module.icon,
  198. summary: module.summary,
  199. config: module.config,
  200. type: module.type,
  201. modules: (module.modules) ? module.modules.map(cleanModules) : [],
  202. }
  203. return newModule;
  204. }
  205. return {
  206. ruleID: (this.rule.ruleID) ? this.rule.ruleID : this.slugify(this.rule.name),
  207. timestamp: this.timesString(),
  208. icon: (this.rule.icon && !this.rule.icon.includes('http')) ? global.url + this.rule.icon : this.rule.icon,
  209. name: this.rule.name,
  210. lineage: this.rule.lineage,
  211. summary: this.rule.summary,
  212. config: this.rule.config,
  213. creator: {
  214. name: this.rule.creator.name,
  215. url: this.rule.creator.url,
  216. },
  217. modules: this.rule.modules.map(cleanModules)
  218. }
  219. },
  220. /**
  221. * @returns {String} the current rule as a JSON string
  222. */
  223. json() {
  224. return JSON.stringify(this.ruleExport, null, 2);
  225. },
  226. /**
  227. * Creates an array of all moduleIDs in use
  228. * @returns {Array} an array of all module's (in the library and custom modules) moduleID
  229. */
  230. listModuleIds() {
  231. const modules = [...this.rule.modules, ...this.customModules];
  232. return modules.map(module => module.moduleID)
  233. },
  234. /**
  235. * @returns {Object} the current module in the editor
  236. */
  237. moduleInEditor() {
  238. return this.moduleEditor[0]
  239. },
  240. /**
  241. * Tests if the current module in the editor has been modified
  242. * against the editor.previousState object
  243. * @returns {Boolean} true if the module in the editor has been modified
  244. */
  245. //TODO: find a more accurate solution than just turning the object into a string
  246. editorHasEdits() {
  247. return this.editor.module && Object.entries(this.editor.module).toString() !== Object.entries(this.editor.previousState).toString();
  248. },
  249. },
  250. methods: {
  251. // module methods ===========================================================
  252. /**
  253. * @returns {Object} a new module object from the moduleTemplate
  254. */
  255. newModule() {
  256. return JSON.parse(JSON.stringify(this.moduleTemplate));
  257. },
  258. /**
  259. * spreads a source module into a new module from the moduleTemplate
  260. * @param {Object} sourceModule the module to copy
  261. * @param {Boolean} includeSubmodules whether to copy submodules or not
  262. * @returns
  263. */
  264. cloneModule(sourceModule, includeSubmodules) {
  265. let output = {
  266. ...this.moduleTemplate,
  267. ...sourceModule,
  268. //TODO: implement lineage pattern, same as the rule does
  269. source: sourceModule, // keep track of where this module came from
  270. };
  271. if (!includeSubmodules) output.modules = [];
  272. // clear configs
  273. document.getElementById("newConfigKey").value =
  274. "Configuration";
  275. document.getElementById("newConfigValue").value =
  276. "Value";
  277. // delete unnecessary properties
  278. delete output.content;
  279. delete output.readonly;
  280. // TODO: give module a unique id
  281. // if (output.moduleID) output.moduleID = this.getUniqueId(output.moduleID);
  282. return output;
  283. },
  284. /**
  285. * Handles the click event to copy a module
  286. * @param {Event} ev the click event
  287. */
  288. handleClickCopyModule(ev) {
  289. const clickTarget = this.getClosestModule(ev.target);
  290. if (!clickTarget) return;
  291. this.copyModule(clickTarget.module);
  292. },
  293. /**
  294. * Handles the click event to edit a module
  295. * @param {Event} ev the click event
  296. */
  297. handleClickEditModule(ev) {
  298. const clickTarget = this.getClosestModule(ev.target);
  299. if (!clickTarget) return;
  300. this.editModule(clickTarget.module);
  301. },
  302. /**
  303. * Copies a module to the editor
  304. * @param {Object} module the module to copy
  305. */
  306. copyModule(module) {
  307. this.copyToEditor(module);
  308. },
  309. /**
  310. * moves a module to the editor
  311. * @param {Object} module the module to edit
  312. */
  313. editModule(module) {
  314. this.addToEditor(module);
  315. },
  316. /**
  317. * add a module to another module (or to the rule by default) as a submodule
  318. * @param {Object} module to add
  319. * @param {Object} target a module or the rule Object where the module should be added as a submodule
  320. */
  321. addModule(module, target = this.rule) {
  322. target.modules.push(module);
  323. },
  324. /**
  325. * remove a module from another module (or from the rule)
  326. * recursively moves through all submodules in the target
  327. * removes ONLY the first instance of the module
  328. * @param {Object} module the module to remove from target
  329. * @param {Object} target the module or rule to remove the module from (defaults to rule)
  330. */
  331. removeModule(module, target = this.rule) {
  332. if (!this.moduleContains(module, target)) return; // if the module is not in the target, do nothing
  333. //
  334. target.modules.forEach((m, idx) => {
  335. if (m === module) {
  336. target.modules.splice(idx, 1);
  337. return;
  338. } else {
  339. this.removeModule(module, m);
  340. }
  341. });
  342. },
  343. /**
  344. * Deletes custom module from the customModules array and clears the editor
  345. * @param {Object} module the module to be deleted
  346. */
  347. deleteModule(module) {
  348. this.removeCustomModule(module);
  349. // TODO: only clear the editor if the module is present in the editor
  350. this.clearEditor();
  351. },
  352. /**
  353. * Handles the start drag event for a module
  354. * @param {Event} ev the drag event
  355. */
  356. startDragModule(ev) {
  357. const targetModule = this.getClosestModule(ev.target);
  358. if (!targetModule) return;
  359. const module = targetModule.module;
  360. ev.dataTransfer.setDragImage(targetModule, ev.offsetX, ev.offsetY);
  361. this.holding = { module };
  362. },
  363. /**
  364. * Handles the start drag event for a module
  365. * when the module is being rearranged within the rule
  366. * @param {Event} ev the drag event
  367. */
  368. rearrangeModule(ev) {
  369. const targetModule = this.getClosestModule(ev.target);
  370. if (!targetModule) return;
  371. const source = this.getClosestModule(targetModule.parentNode).module;
  372. const module = targetModule.module;
  373. ev.dataTransfer.setDragImage(targetModule, ev.offsetX, ev.offsetY);
  374. this.holding = {
  375. module,
  376. source,
  377. };
  378. },
  379. /**
  380. * Handles the dragend event for a module
  381. */
  382. endDragModule() {
  383. this.holding = false;
  384. },
  385. /**
  386. * Recursively searches modules and their submodules for a module
  387. * @param {Object} needle the module to search for
  388. * @param {Object} haystack the module to search in (defaults to rule)
  389. * @returns {Boolean} true if the module is in the haystack
  390. */
  391. // TODO: return the module location in the haystack (Maybe?)
  392. moduleContains(needle, haystack = this.rule) {
  393. if (!haystack.modules) return false; // does the haystack even have modules?
  394. if (haystack.modules.includes(needle)) return true; // is the needle in the haystack?
  395. return haystack.modules.some(submodule => this.moduleContains(needle, submodule)); // is the needle in any of the submodules?
  396. },
  397. // rule methods ===========================================================
  398. /**
  399. * Handles the drop event for a module
  400. * adds the module to the closest submodule or the rule depending on what it is dropped onto
  401. * then adds the module to the editor
  402. * @param {Event} ev the drop event
  403. */
  404. dropOnRule(ev) {
  405. //TODO browser drag objects that hover over drop zone are showing a 'add' icon
  406. const landingNode = this.getClosestModule(ev.target);
  407. if (!this.holding.module || !landingNode) return; // if there is no module to drop or no landing node, do nothing
  408. const landingModule = landingNode.module; // module is set with the v-bind prop binding
  409. const holdingModule = this.holding.module;
  410. if (holdingModule === landingModule) return; // if the module is the same, do nothing
  411. // if the module being dropped is readyonly clone it, otherwise use the original
  412. const readonly = holdingModule.readonly;
  413. const module = (readonly) ? this.cloneModule(holdingModule) : holdingModule;
  414. if (this.holding.source) {
  415. // if the module has a source, remove it from that source
  416. this.removeModule(holdingModule, this.holding.source);
  417. }
  418. this.addModule(module, landingModule);
  419. this.editModule(module);
  420. this.endDragModule();
  421. },
  422. fetchRule(id) {
  423. this.loading = true;
  424. // handle legacy links
  425. // TODO: handle this at a global level
  426. let redirect = {
  427. 'benevolent_dictator': 'benevolent-dictator',
  428. circles: 'circles',
  429. consensus: 'consensus',
  430. 'do-ocracy': 'do-ocracy',
  431. 'elected_board': 'elected-board',
  432. jury: 'jury',
  433. petition: 'petition',
  434. 'self-appointed_board': 'self-appointed-board',
  435. }
  436. // if the rule is a legacy link, redirect
  437. if (redirect[id]) {
  438. location.href = `/templates/${redirect[id]}`;
  439. return;
  440. }
  441. const store = new SteinStore(
  442. this.steinAPI
  443. );
  444. const backendUrlGetRule = this.backendUrlrule + "?ruleId=" + id;
  445. (async () => {
  446. var rule = [];
  447. // read values from all sheets
  448. await fetch(backendUrlGetRule, {
  449. method: 'GET',
  450. headers: {
  451. 'Content-Type': 'application/json',
  452. // Add any other headers you may need, such as authorization headers
  453. },
  454. }).then(response => {
  455. if (!response.ok) {
  456. throw new Error('Network response was not ok');
  457. }
  458. return response.json(); // This returns another Promise
  459. })
  460. .then(data => {
  461. if (data.rules.length > 0) {
  462. rule = JSON.parse(data.rules)[0];
  463. }
  464. });
  465. // no rule found, exit
  466. // TODO: inform the user that the rule was not found
  467. if (!rule) {
  468. this.loading = false;
  469. return;
  470. }
  471. // if this is a legacy (pre-v3) set it as such
  472. if (rule.version < 3) {
  473. this.loading = false;
  474. this.legacy = true;
  475. this.rule = rule;
  476. Vue.nextTick(() => {
  477. if (rule.version == 2) displayBuilderHTML();
  478. });
  479. return;
  480. }
  481. this.rule = {
  482. ruleID: rule.rule_id,
  483. timestamp: rule.time_stamp,
  484. icon: rule.icon,
  485. name: rule.name,
  486. lineage: rule.lineage,
  487. summary: rule.summary,
  488. config: rule.config,
  489. creator: {
  490. name: rule.creator_name,
  491. url: rule.creator_url,
  492. },
  493. modules: (rule.modules) ? JSON.parse(rule.modules) : [],
  494. latest_version: rule.latest_version
  495. }
  496. if (rule.email == localStorage.getItem('userEmail')) {
  497. this.isOwner = true;
  498. }
  499. if (localStorage.getItem('userEmail') == this.adminMail) {
  500. this.isAdmin = true;
  501. }
  502. /** Add name to <title> for v3+ rules */
  503. document.title = rule.name + " / CommunityRule"
  504. this.loading = false;
  505. })();
  506. /*fetch(backendUrlGetRule, {
  507. method: 'GET',
  508. headers: {
  509. 'Content-Type': 'application/json',
  510. // Add any other headers you may need, such as authorization headers
  511. },
  512. }).then(response => {
  513. if (!response.ok) {
  514. throw new Error('Network response was not ok');
  515. }
  516. return response.json(); // This returns another Promise
  517. }).then (data => {
  518. var rule = [];
  519. if (data.rules.length > 0) {
  520. rule = data.rules;
  521. }
  522. // no rule found, exit
  523. // TODO: inform the user that the rule was not found
  524. if (!rule) {
  525. this.loading = false;
  526. return;
  527. }
  528. // if this is a legacy (pre-v3) set it as such
  529. if (rule.version < 3) {
  530. this.loading = false;
  531. this.legacy = true;
  532. this.rule = rule;
  533. Vue.nextTick(() => {
  534. if (rule.version == 2) displayBuilderHTML();
  535. });
  536. return;
  537. }
  538. this.rule = {
  539. ruleID: rule.ruleID,
  540. timestamp: rule.timestamp,
  541. icon: rule.icon,
  542. name: rule.name,
  543. lineage: rule.lineage,
  544. summary: rule.summary,
  545. config: rule.config,
  546. creator: {
  547. name: rule.creatorName,
  548. url: rule.creatorUrl,
  549. },
  550. modules: (rule.modules) ? JSON.parse(rule.modules) : []
  551. }
  552. console.log('dadsfasdfasdfasdfasdf')
  553. console.log(this.rule)
  554. document.title = rule.name + " / CommunityRule"
  555. this.loading = false;
  556. }); */
  557. },
  558. // editor methods =========================================================
  559. clickDelete() {
  560. (async () => {
  561. // read values from all sheets
  562. console.log("rule to delete " + this.rule.ruleID);
  563. const requestData = {
  564. ruleID: this.rule.ruleID
  565. }
  566. await fetch(this.backedDeleterule, {
  567. method: 'DELETE',
  568. headers: {
  569. 'Content-Type': 'application/json',
  570. // Add any other headers you may need, such as authorization headers
  571. },
  572. body: JSON.stringify(requestData),
  573. }).then(response => {
  574. if (!response.ok) {
  575. throw new Error('Network response was not ok');
  576. }
  577. alert("Error while deleting rule");
  578. return response.json(); // This returns another Promise
  579. }).then(data => {
  580. alert('Rule deleted successfully');
  581. window.open("/library", false);
  582. });
  583. this.loading = false;
  584. })();
  585. },
  586. /**
  587. * Adds a module to the editor
  588. * @param {Object} module the module to add to the editor
  589. */
  590. addToEditor(module) {
  591. this.preventEditorLoss();
  592. this.setEditorSource(module);
  593. this.setEditorPreviousState(module);
  594. this.editor.module = module;
  595. },
  596. /**
  597. * Copies a module to the editor
  598. * @param {Object} module the module to copy to the editor
  599. */
  600. copyToEditor(module) {
  601. const moduleCopy = this.cloneModule(module);
  602. this.preventEditorLoss();
  603. this.setEditorSource(module);
  604. this.setEditorPreviousState(moduleCopy);
  605. this.editor.module = moduleCopy;
  606. },
  607. /**
  608. * Takes a module and clones it into the editor.previousState
  609. * @param {Object} module the module to add to the previous state
  610. */
  611. setEditorPreviousState(module) {
  612. this.editor.previousState = { ...module };
  613. },
  614. /**
  615. * Sets the editor.source to the module
  616. * @param {Object} module the module to set the editor source to
  617. */
  618. setEditorSource(module) {
  619. this.editor.source = module;
  620. },
  621. /**
  622. * Checks if the editor has edits and that the current module in the editor is not present in the rule
  623. * If the module in the editor would be lost, confirm with the user
  624. * then adds the module to the customModules array
  625. */
  626. preventEditorLoss() {
  627. // if the editor has changes and the module isn't in the rule, save it to the custom modules
  628. if (this.editorHasEdits && !this.moduleContains(this.editor.module)) {
  629. this.confirm('You have unsaved changes. Are you sure you want to discard them?')
  630. this.addCustomModule(this.editor.module);
  631. }
  632. },
  633. /**
  634. * Handles the click event for adding a module from the editor to the rule
  635. */
  636. clickAddModule() {
  637. const module = this.editor.module;
  638. this.addModule(module);
  639. this.addToEditor(module);
  640. },
  641. /**
  642. * Handles the click event for removing a module in the editor from the rule
  643. */
  644. clickRemoveModule() {
  645. const module = this.editor.module;
  646. this.removeModule(module);
  647. },
  648. /**
  649. * Clears the editor
  650. */
  651. clearEditor() {
  652. this.preventEditorLoss();
  653. this.editor.module = null;
  654. this.editor.previousState = null;
  655. },
  656. /**
  657. * Saves the module in the editor to customModules array
  658. */
  659. saveEditor() {
  660. this.addCustomModule(this.editor.module);
  661. this.setEditorPreviousState(this.editor.module);
  662. },
  663. // config methods =========================================================
  664. /**
  665. * Add custom config entry to the module
  666. */
  667. addConfig() {
  668. const k = document.getElementById("newConfigKey").value;
  669. const v = document.getElementById("newConfigValue").value;
  670. this.editor.module.config[k] = v;
  671. this.resetNewConfigInputs();
  672. },
  673. /**
  674. * Removes the config entry from the module
  675. */
  676. removeConfig(key) {
  677. delete this.editor.module.config[key];
  678. },
  679. // custom module methods ==================================================
  680. /**
  681. * Adds a module to the customModules array
  682. * @param {Object} module the module to add to the customModules array
  683. */
  684. addCustomModule(module) {
  685. // if module is not in customModules, add it
  686. if (!this.customModules.includes(module)) {
  687. this.customModules.unshift(module);
  688. }
  689. },
  690. /**
  691. * Creates a new module, sets a default name, and adds it to the editor
  692. */
  693. newCustomModule() {
  694. const module = this.newModule();
  695. module.name = 'New Module';
  696. module.config = {};
  697. this.resetNewConfigInputs();
  698. this.addToEditor(module);
  699. },
  700. resetNewConfigInputs() {
  701. document.getElementById("newConfigKey").value = "Configuration";
  702. document.getElementById("newConfigValue").value = "Value";
  703. },
  704. /**
  705. * Removes a module from the customModules array
  706. * @param {Object} module the module to remove from the customModules array
  707. */
  708. removeCustomModule(module) {
  709. this.confirm("are you sure you want to remove this custom module?");
  710. const index = this.customModules.indexOf(module);
  711. this.customModules.splice(index, 1);
  712. },
  713. /**
  714. * Handles confirmation messages for users
  715. * @param {String} msg the message to display in the confirm dialog
  716. */
  717. // TODO: add a confirm dialog and return boolean based on user input
  718. confirm(msg) {
  719. console.log(msg);
  720. },
  721. /**
  722. * Handles click event for publishing the rule
  723. */
  724. handleClickPublish() {
  725. // Confirm user knows what they're getting into
  726. if (!localStorage.getItem('userEmail')) {
  727. alert('Please login first to publish this rule')
  728. return;
  729. }
  730. if (!confirm("Publish to the public Library?")) return;
  731. if (!this.rule.name) {
  732. alert("Please enter a name for this rule.");
  733. return;
  734. }
  735. if (this.rule.modules.length < 1) {
  736. alert("Please add at least one module to this rule.");
  737. return;
  738. }
  739. this.publishing = true;
  740. const rule = this.ruleExport;
  741. var ruleID = new Date().getTime(); // TODO: allow for custom named IDs, check for uniqueness
  742. if (this.edit) {
  743. ruleID = this.rule.ruleID;
  744. }
  745. var latest_ver = 1;
  746. if (this.edit) {
  747. latest_ver = parseInt(this.rule.latest_version) + 1;
  748. }
  749. // ------------------ exisituing code ---------------
  750. // add to database
  751. // const store = new SteinStore(
  752. // this.steinAPI
  753. // );
  754. // store.append('rules', [{
  755. // ruleID: ruleID,
  756. // timestamp: rule.timestamp,
  757. // icon: rule.icon,
  758. // name: rule.name,
  759. // lineage: rule.lineage,
  760. // summary: rule.summary,
  761. // config: this.jsonify(rule.config),
  762. // modules: this.jsonify(rule.modules),
  763. // creatorName: rule.creator.name,
  764. // creatorUrl: rule.creator.url,
  765. // version: 3
  766. // }]).then(data => {
  767. // this.publishing = false;
  768. // window.open("/create/?r=" + ruleID, "_self", false);
  769. // });
  770. // -=---------------------------- updated code -------------------
  771. const requestData = {
  772. ruleID: ruleID,
  773. timestamp: rule.timestamp,
  774. icon: rule.icon,
  775. name: rule.name,
  776. lineage: rule.lineage,
  777. summary: rule.summary,
  778. config: this.jsonify(rule.config),
  779. modules: this.jsonify(rule.modules),
  780. creatorName: rule.creator.name,
  781. creatorUrl: rule.creator.url,
  782. email: localStorage.getItem('userEmail'),
  783. edit: this.edit,
  784. version: 3,
  785. latest_version: latest_ver
  786. }
  787. fetch(this.backendUrl, {
  788. method: 'POST',
  789. headers: {
  790. 'Content-Type': 'application/json',
  791. 'Access-Control-Allow-Methods': 'POST',
  792. 'Access-Control-Allow-Headers': 'Content-Type'
  793. // Add any other headers you may need, such as authorization headers
  794. },
  795. body: JSON.stringify(requestData),
  796. }).then(response => {
  797. if (!response.ok) {
  798. throw new Error(`HTTP error! Status: ${response.status}`);
  799. }
  800. return response.json(); // or response.text() if expecting plain text
  801. }).then(data => {
  802. // Handle the data received from the backend
  803. console.log('Data from backend:', data);
  804. }).then(data => {
  805. this.publishing = false;
  806. window.open("/create/?r=" + ruleID, "_self", false);
  807. }).catch(error => {
  808. // Handle errors that occurred during the fetch
  809. console.error('Fetch error:', error);
  810. });
  811. },
  812. /**
  813. * Handles the click event for downloading the rule as a Markdown file
  814. * Creates a YAML string of the rule
  815. * Then adds it to the bottom of a markdown file
  816. * created from the #rule-export html element
  817. */
  818. handleClickDownload() {
  819. const yaml = jsyaml.dump(this.ruleExport);
  820. const turndown = new TurndownService();
  821. const html = document.getElementById('rule-export');
  822. if (!html) return;
  823. const markdown = turndown.turndown(html);
  824. const output = markdown + '\n\n----\n```yaml\n' + yaml + '\n```';
  825. this.saveFile(`${this.ruleExport.ruleID}.md`, output, 'text/markdown');
  826. },
  827. /**
  828. * IE10+ Firefox, and Chrome method for saving a file
  829. * https://stackoverflow.com/a/33542499
  830. * @param {String} filename name of the file to save
  831. * @param {String} data data to save into a file
  832. * @param {String} type MIME type of the file
  833. */
  834. saveFile(filename, data, type) {
  835. const blob = new Blob([data], { type: type });
  836. if (window.navigator.msSaveOrOpenBlob) {
  837. window.navigator.msSaveBlob(blob, filename);
  838. }
  839. else {
  840. const elem = window.document.createElement('a');
  841. elem.href = window.URL.createObjectURL(blob);
  842. elem.download = filename;
  843. document.body.appendChild(elem);
  844. elem.click();
  845. document.body.removeChild(elem);
  846. }
  847. },
  848. /**
  849. * Handles the click event for importing a rule from a JSON file
  850. */
  851. handleClickImport() {
  852. },
  853. // utility methods ===========================================================
  854. /**
  855. * Takes an html element and finds the closest node (inclusive) that has a data-module-id attribute
  856. * @param {Node} el the html element to check
  857. * @returns {Node} the closest node with a data-module-id attribute
  858. */
  859. getClosestModule(el) {
  860. const parent = el.closest('[data-module-id]');
  861. if (!parent) return false;
  862. if (!parent.module) return false;
  863. return parent;
  864. },
  865. /**
  866. * Handles the click event for activating the rule preview
  867. */
  868. clickPreview() {
  869. if (this.template) this.rule.icon = ''; // TODO: find a less hacky way to reset template icons
  870. this.view = false;
  871. this.preview = !this.preview;
  872. },
  873. /**
  874. * Handles the click event for activating the rule preview for edit rule
  875. */
  876. clickPreviewEdit() {
  877. if (this.template) this.rule.icon = ''; // TODO: find a less hacky way to reset template icons
  878. this.view = false;
  879. this.preview = !this.preview;
  880. this.edit = true;
  881. },
  882. /**
  883. * Filters module library based on the search term
  884. * @param {String} type the name of the type to filter
  885. * @returns {Array} the filtered module library
  886. */
  887. getModulesByType(type) {
  888. return this.modules.filter(module => module.type === type)
  889. },
  890. /**
  891. * Slugifies a string
  892. * https://gist.github.com/codeguy/6684588 (one of the comments)
  893. * @param {String} string the string to slugify
  894. * @returns
  895. */
  896. slugify(string) {
  897. const separator = '-';
  898. return string
  899. .toString()
  900. .normalize('NFD') // split an accented letter in the base letter and the accent
  901. .replace(/[\u0300-\u036f]/g, '') // remove all previously split accents
  902. .toLowerCase()
  903. .replace(/[^a-z0-9 -]/g, '') // remove all chars not letters, numbers and spaces (to be replaced)
  904. .trim()
  905. .replace(/\s+/g, separator);
  906. },
  907. /**
  908. * Creates a human readable timestamp string
  909. * @param {String} date optional date to format
  910. * @returns {String} human readable date '2022.4.12 14:44:56 UTC'
  911. */
  912. timesString(date) {
  913. let now = new Date(date);
  914. if (isNaN(now)) now = new Date();
  915. return now.getUTCFullYear() + '.' + (now.getUTCMonth() + 1) + '.' + now.getUTCDate()
  916. + ' ' + now.getUTCHours() + ":" + now.getUTCMinutes() + ":" + now.getUTCSeconds()
  917. + ' UTC';
  918. },
  919. /**
  920. * stringify an Object
  921. * @param {Object} obj
  922. * @returns JSON string
  923. */
  924. jsonify(obj) {
  925. return JSON.stringify(obj, null, 2);
  926. },
  927. /**
  928. * Takes a moduleID and checks if that moduleID is in use
  929. * if so, returns the moduleID with a number appended to it
  930. * @param {String} test the moduleID to test
  931. * @returns {String} the moduleID, or if in use, with a number appended to it
  932. */
  933. getUniqueId(test) {
  934. let id = test;
  935. let i = 0;
  936. while (this.listModuleIds.includes(id)) {
  937. i++;
  938. id = `${test}-${i}`;
  939. }
  940. return id
  941. },
  942. },
  943. });
  944. /**
  945. * The Module Vue Component
  946. */
  947. app.component('module', {
  948. delimiters: ['[[', ']]'],
  949. inject: ['editor'],
  950. props: {
  951. module: {
  952. type: Object,
  953. required: true,
  954. },
  955. inEditor: {
  956. type: Boolean,
  957. default: false,
  958. },
  959. hideSubmodules: {
  960. type: Boolean,
  961. default: false,
  962. }
  963. },
  964. data() {
  965. return {
  966. defaultIcon: '/assets/tabler_icons/circle-dotted.svg',
  967. mouseOver: false,
  968. dragOver: false
  969. }
  970. },
  971. computed: {
  972. icon() {
  973. return this.module.icon ? this.module.icon : this.defaultIcon;
  974. },
  975. moduleClass() {
  976. return {
  977. 'in-editor': this.editor.source == this.module,
  978. 'mouse-over': this.mouseOver,
  979. // TODO: when dragging over the icon the drag-over class disappears
  980. 'drag-over': this.dragOver
  981. }
  982. }
  983. },
  984. template: `
  985. <div
  986. class="module"
  987. :class="moduleClass"
  988. .module="module"
  989. @mouseover.stop="this.mouseOver = true"
  990. @mouseout="this.mouseOver = false"
  991. @dragenter.self="this.dragOver = true"
  992. @dragleave.self="this.dragOver = false"
  993. @drop.self="this.dragOver = false"
  994. :data-module-id="module.moduleID"
  995. >
  996. <div class="module__icon-wrapper">
  997. <span class="module__grain"><img src="/assets/tabler_icons/grain.svg"></span>
  998. <span class="module__icon"><img :src="icon"></span>
  999. </div>
  1000. [[module.name]]
  1001. <div class="submodules" v-if="!hideSubmodules && module.modules && module.modules.length">
  1002. <module v-for="submodule in module.modules" :module="submodule" draggable="true"></module>
  1003. </div>
  1004. </div>
  1005. `
  1006. })
  1007. /**
  1008. * A non-interactive Module Vue Component
  1009. * Used for displaying the module
  1010. */
  1011. app.component('moduleDisplay', {
  1012. delimiters: ['[[', ']]'],
  1013. props: {
  1014. module: {
  1015. type: Object,
  1016. required: true,
  1017. }
  1018. },
  1019. data() {
  1020. return {
  1021. defaultIcon: '/assets/tabler_icons/circle-dotted.svg'
  1022. }
  1023. },
  1024. computed: {
  1025. icon() {
  1026. return this.module.icon ? this.module.icon : this.defaultIcon;
  1027. }
  1028. },
  1029. template: `
  1030. <div
  1031. class="module"
  1032. .module="module"
  1033. >
  1034. <div class="module__icon-wrapper">
  1035. <span class="module__icon"><img :src="icon"></span>
  1036. </div>
  1037. [[module.name]]
  1038. <div class="submodules" v-if="module.modules && module.modules.length">
  1039. <module-display v-for="submodule in module.modules" :module="submodule"></module-display>
  1040. </div>
  1041. </div>
  1042. `
  1043. })
  1044. /**
  1045. * The Module list Vue Component
  1046. */
  1047. app.component('moduleList', {
  1048. delimiters: ['[[', ']]'],
  1049. props: {
  1050. module: {
  1051. type: Object,
  1052. required: true,
  1053. },
  1054. hideIcon: {
  1055. type: Boolean,
  1056. default: false,
  1057. }
  1058. },
  1059. data() {
  1060. return {
  1061. defaultIcon: '/assets/tabler_icons/circle-dotted.svg'
  1062. }
  1063. },
  1064. computed: {
  1065. icon() {
  1066. return this.module.icon ? this.module.icon : this.defaultIcon;
  1067. }
  1068. },
  1069. template: `
  1070. <li class="module-list-item">
  1071. <span class="module__icon" v-if="!hideIcon"><img :src="icon"> </span><strong>[[module.name]]</strong>: [[module.summary]]
  1072. <span class="module__config">
  1073. <span v-for="(value, key) in module.config">
  1074. <br />[[key]]: [[value]]
  1075. </span>
  1076. </span>
  1077. <ul class="submodules" v-if="module.modules && module.modules.length">
  1078. <module-list v-for="submodule in module.modules" :module="submodule" :hide-icon="hideIcon"></module-list>
  1079. </ul>
  1080. </li>
  1081. `
  1082. })
  1083. /**
  1084. * A simple button Vue Component
  1085. */
  1086. app.component('vueButton', {
  1087. delimiters: ['[[', ']]'],
  1088. props: {
  1089. icon: {
  1090. type: String,
  1091. required: false,
  1092. default: false
  1093. },
  1094. loading: {
  1095. type: Boolean,
  1096. required: false,
  1097. default: false
  1098. }
  1099. },
  1100. computed: {
  1101. classList() {
  1102. return {
  1103. 'has-icon': this.icon,
  1104. 'is-loading': this.loading
  1105. };
  1106. },
  1107. activeIcon() {
  1108. return this.loading ? '/assets/tabler_icons/refresh.svg' : this.icon;
  1109. }
  1110. },
  1111. template: `
  1112. <button class="btn" :class="classList"><img :src="activeIcon" v-if="icon"> <slot>Click Here</slot></button>
  1113. `
  1114. })
  1115. /**
  1116. * A icon Vue Component
  1117. */
  1118. app.component('icon', {
  1119. delimiters: ['[[', ']]'],
  1120. props: {
  1121. icon: {
  1122. type: String,
  1123. required: true,
  1124. default: '/assets/tabler_icons/circle-dotted.svg'
  1125. }
  1126. },
  1127. template: `
  1128. <span class="icon"><img :src="icon"></span>
  1129. `
  1130. })
  1131. /**
  1132. * Mounts the app to the DOM element with the id 'app'
  1133. */
  1134. vm = app.mount('#app')
  1135. /**
  1136. * Legacy functions for displaying old rules
  1137. */
  1138. // Turns RuleBuilder contents into an output-ready nested array
  1139. // Returns empty array if no modules
  1140. function builderArray() {
  1141. var modules = document.getElementById("module-input").children;
  1142. // takes an array of children
  1143. // returns an array with all modules in the array, recursively nested
  1144. function iterateArray(childs) {
  1145. var moduleArray = [];
  1146. if (childs.length > 0) {
  1147. for (var i = 0; i < childs.length; i++) {
  1148. module = childs[i];
  1149. if (module.classList[0] == "module") {
  1150. var moduleName = module.children.item("module-name");
  1151. var moduleData = moduleName.title;
  1152. var moduleChilds = module.children;
  1153. moduleArray.push(
  1154. [stripHTML(moduleName.innerHTML),
  1155. stripHTML(moduleData),
  1156. iterateArray(moduleChilds)]);
  1157. }
  1158. }
  1159. }
  1160. return moduleArray;
  1161. } // end function
  1162. return iterateArray(modules);
  1163. }
  1164. // returns HTML version of Builder content
  1165. function displayBuilderHTML() {
  1166. var output = "";
  1167. var mainArray = builderArray();
  1168. function arrayHTML(thisArray) {
  1169. var thisOutput = "";
  1170. if (thisArray.length > 0) {
  1171. thisOutput += '<ul>\n';
  1172. for (var i = 0; i < thisArray.length; i++) {
  1173. var item = thisArray[i];
  1174. thisOutput += '<li><strong>' + item[0] + '</strong> ';
  1175. thisOutput += item[1] + '</li>\n';
  1176. if (item[2].length > 0) {
  1177. thisOutput += arrayHTML(item[2]);
  1178. }
  1179. }
  1180. thisOutput += '</ul>\n';
  1181. }
  1182. return thisOutput
  1183. }
  1184. if (mainArray.length > 0) {
  1185. output += '<div class="builder-list">\n';
  1186. output += arrayHTML(mainArray) + '\n</div>\n';
  1187. }
  1188. document.getElementById("builder-field").innerHTML = output;
  1189. }
  1190. // Removes all HTML content, replacing line break tags with newlines
  1191. function stripHTML(input) {
  1192. input = input.replace(/<br ?\/?>/ig, "\n").replace(/(<([^>]+)>)/ig, '');
  1193. return input;
  1194. }