vue.rules.js 37 KB

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