glib-web 4.26.1 → 4.27.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,6 +7,28 @@
7
7
  - `CTRL+SHIFT+P` -> `Preferences: Open User Settings (JSON)` -- This will open an editor for `settings.json`.
8
8
  - Copy content of `settings.json.example` into the editor.
9
9
 
10
+ ## Set up Javascript CI
11
+
12
+ - Install typescript `yarn add typescript --dev`
13
+ - Install cypress, use same version as cypress `package.json`. E.g. `yarn add cypress@13.13.1 --dev`
14
+ - Add `cypress.config.js` to your project
15
+ ```javascript
16
+ const { defineConfig } = require('cypress');
17
+
18
+ module.exports = defineConfig({
19
+ e2e: {
20
+ specPattern: 'node_modules/glib-web/cypress/e2e',
21
+ defaultBrowser: 'chrome',
22
+ supportFile: 'node_modules/glib-web/cypress/support/e2e.{js,jsx,ts,tsx}'
23
+ },
24
+ });
25
+ ```
26
+ - Copy `cypress.yml.example` to `YOUR_PROJECT/.github/workflows/cypress.yml`
27
+ - Make sure the workflow is compatible with the project
28
+ - Run the test
29
+ - Run rails server `bin/rails s`
30
+ - Execute `yarn run cypress run`
31
+
10
32
  ## Best practices
11
33
 
12
34
  - To prevent circular dependencies between components:
package/app.vue CHANGED
@@ -170,12 +170,6 @@ export default {
170
170
  },
171
171
  true
172
172
  );
173
-
174
- if (isRerender()) {
175
- GLib.action.execute(this.page.onRerender, this);
176
- } else {
177
- GLib.action.execute(this.page.onLoad, this);
178
- }
179
173
  },
180
174
  $ready() {
181
175
  this.$type.ifString(this.page.title, (title) => {
@@ -219,6 +213,12 @@ export default {
219
213
  "animate": true
220
214
  }, this);
221
215
  }
216
+
217
+ if (isRerender()) {
218
+ GLib.action.execute(this.page.onRerender, this);
219
+ } else {
220
+ GLib.action.execute(this.page.onLoad, this);
221
+ }
222
222
  },
223
223
  updateMainHeight() {
224
224
  this.mainHeight = window.innerHeight - this.$refs.navBar.$el.offsetHeight;
@@ -69,6 +69,7 @@ import TextField from "./fields/text.vue";
69
69
  import SubmitField from "./fields/submit.vue";
70
70
  import TextAreaField from "./fields/textarea.vue";
71
71
  const RichTextField = defineAsyncComponent(() => import("./fields/richText.vue"));
72
+ const RichTextField2 = defineAsyncComponent(() => import("./fields/richText2.vue"));
72
73
  // import NewRichTextField from "./fields/newRichText.vue";
73
74
  const FileField = defineAsyncComponent(() => import("./fields/file.vue"));
74
75
  const MultiUploadField = defineAsyncComponent(() => import("./fields/multiUpload.vue"));
@@ -170,7 +171,7 @@ export default {
170
171
  "fields-text": TextField,
171
172
  "fields-submit": SubmitField,
172
173
  "fields-textarea": TextAreaField,
173
- "fields-richText": RichTextField,
174
+ "fields-richText": RichTextField2,
174
175
  // "fields-newRichText": NewRichTextField,
175
176
  "fields-file": FileField,
176
177
  "fields-multiUpload": MultiUploadField,
@@ -106,8 +106,8 @@ function useGlibInput({ props, cacheValue = true }) {
106
106
 
107
107
  // save fieldModel to spec so data still intact even if component unmounted
108
108
  onBeforeUpdate(() => {
109
- if (!instance.ctx.spec || !cacheValue) return;
110
- instance.ctx.spec.value = instance.ctx.fieldModel;
109
+ if (!instance.props.spec || !cacheValue) return;
110
+ instance.props.spec.value = instance.data.fieldModel;
111
111
  });
112
112
 
113
113
  }
@@ -32,7 +32,7 @@ function setBusyWhenUploading({ files }) {
32
32
  });
33
33
  }
34
34
 
35
- function uploadFiles({ droppedFiles, files, spec, container }) {
35
+ function uploadFiles({ droppedFiles, files, spec, container, onAfterUploaded }) {
36
36
  let { responseMessages } = spec;
37
37
  responseMessages ||= {};
38
38
 
@@ -41,7 +41,7 @@ function uploadFiles({ droppedFiles, files, spec, container }) {
41
41
  // show new dropped file and track progress
42
42
  key = makeKey();
43
43
  files.value[key] = new Item({ el: droppedFiles[index], status: 'pending' });
44
- uploadOneFile({ files, key, spec, container });
44
+ uploadOneFile({ files, key, spec, container, onAfterUploaded });
45
45
  }
46
46
  }
47
47
 
@@ -18,10 +18,11 @@
18
18
  <v-input :name="fieldName" :rules="$validation()" v-model="model" />
19
19
 
20
20
  <v-dialog v-model="dialog" width="auto">
21
- <v-date-picker :model-value="new Date(model)" @update:modelValue="handleDatePickerChanged"
21
+ <v-date-picker :model-value="model ? new Date(model) : null" @update:modelValue="handleDatePickerChanged"
22
22
  :disabled="inputDisabled" :min="sanitizeValue(spec.min)" :max="sanitizeValue(spec.max)" show-adjacent-months
23
23
  location="center" position="fixed">
24
24
  <template v-slot:actions>
25
+ <v-btn color="error" @click="remove">Remove</v-btn>
25
26
  <v-btn color="primary" @click="dialog = false">Save</v-btn>
26
27
  </template>
27
28
  </v-date-picker>
@@ -69,6 +70,11 @@ export default {
69
70
  model.value = sanitizeValue(val.spec.value);
70
71
  });
71
72
 
73
+ function remove() {
74
+ model.value = '';
75
+ ctx.emit('datePicked', model.value);
76
+ }
77
+
72
78
  function handleChanged(e) {
73
79
  const { value } = e.srcElement;
74
80
  model.value = value;
@@ -83,7 +89,7 @@ export default {
83
89
 
84
90
  const { smAndUp } = useDisplay();
85
91
 
86
- return { text, sanitizeValue, dateInput, handleChanged, model, template, dialog, handleDatePickerChanged, smAndUp };
92
+ return { text, sanitizeValue, dateInput, handleChanged, model, template, dialog, handleDatePickerChanged, smAndUp, remove };
87
93
  },
88
94
  methods: {
89
95
  showPicker(e) {
@@ -56,8 +56,16 @@ export default defineComponent({
56
56
  }
57
57
 
58
58
  function prefixFieldName(index, fieldName) {
59
- if (fieldName.search(/\[.*\]/) >= 0) {
60
- return `${namePrefix}[${index}]${fieldName}`;
59
+ if (fieldName.match(/\[|\]/g)) {
60
+ let suffix = '';
61
+ const indexOfFirstBracket = fieldName.indexOf('[');
62
+ if (indexOfFirstBracket > 0) {
63
+ suffix = '[' + fieldName.slice(0, indexOfFirstBracket) + ']' + fieldName.slice(indexOfFirstBracket, fieldName.length);
64
+ } else {
65
+ suffix = fieldName;
66
+ }
67
+
68
+ return `${namePrefix}[${index}]${suffix}`;
61
69
  } else {
62
70
  return `${namePrefix}[${index}][${fieldName}]`;
63
71
  }
@@ -90,7 +90,7 @@ export default defineComponent({
90
90
  status: null, // pending, completed, failed
91
91
  url: curr.url,
92
92
  name: curr.name,
93
- signedId: curr.signed_id,
93
+ signedId: curr.signed_id || curr.signedId,
94
94
  type: curr.type,
95
95
  el: null
96
96
  });
@@ -0,0 +1,11 @@
1
+ <template>
2
+ <div>Raw Mode</div>
3
+ <v-textarea :value="props.producedValue" auto-grow></v-textarea>
4
+ <ul>
5
+ <li v-for="signedId in signedIds" :key="signedId">{{ signedId }}</li>
6
+ </ul>
7
+ </template>
8
+
9
+ <script setup>
10
+ const props = defineProps(['producedValue', 'signedIds']);
11
+ </script>
@@ -7,13 +7,7 @@
7
7
 
8
8
  <v-progress-linear v-if="showProgress" v-model="progress.value" />
9
9
 
10
- <!-- Remove the editor to avoid circular updating between this editor and the raw field. -->
11
- <!-- <VueEditor v-if="!rawMode" id="rich-editor" v-model="richEditorValue" :editor-toolbar="customToolbar"
12
- use-custom-image-handler :editor-options="editorSettings" @text-change="onRichTextEditorChanged"
13
- @image-added="uploadImage" /> -->
14
- <QuillEditor v-if="!rawMode" ref="quilEditor" theme="snow" :toolbar="customToolbar" :content="richEditorValue"
15
- contentType="html" :modules="modules" @textChange="onRichTextEditorChanged" class="rich-editor"
16
- :placeholder="spec.placeholder" />
10
+ <div v-if="!rawMode" ref="editor" id="editor"></div>
17
11
  <!-- Hide these fields but don't remove them because these are the values that will get submitted. -->
18
12
  <div :style="{ display: rawMode ? 'block' : 'none' }">
19
13
  <v-textarea class="raw-editor" v-model="rawEditorValue" :style="$styles()" :class="$classes()"
@@ -27,20 +21,20 @@
27
21
  </template>
28
22
 
29
23
  <script>
30
- import '@vueup/vue-quill/dist/vue-quill.snow.css';
31
-
32
24
  import Uploader from "../../utils/glibDirectUpload";
33
- import { QuillEditor, Quill } from '@vueup/vue-quill';
34
25
 
35
26
  import TurndownService from "turndown";
36
27
  import { gfm } from "turndown-plugin-gfm";
37
28
  import eventFiltering from "../../utils/eventFiltering";
38
- import QuillImageDropAndPaste from "quill-image-drop-and-paste";
29
+ // import ImageDropAndPaste from "quill-image-drop-and-paste";
39
30
  import bus from "../../utils/eventBus";
40
31
 
41
- import { vueApp } from "../../store";
42
- import { triggerOnInput, useGlibInput } from "../composable/form";
32
+ import { useGlibInput } from "../composable/form";
43
33
  import { validateFile } from "../composable/file";
34
+ import Quill from "quill";
35
+ import "quill/dist/quill.snow.css";
36
+
37
+ // Quill.register('modules/ImageDropAndPaste', ImageDropAndPaste);
44
38
 
45
39
  var ImageBlot = Quill.import("formats/image");
46
40
  ImageBlot.sanitize = function (url) {
@@ -157,23 +151,7 @@ class TextEditor {
157
151
  }
158
152
  }
159
153
 
160
- const modules = {
161
- name: 'imageDropAndPaste',
162
- module: QuillImageDropAndPaste,
163
- options: {
164
- // add an custom image handler
165
- handler: function (imageDataUrl, type, imageData) {
166
- bus.$emit("richText/dropOrPaste", {
167
- file: imageData.toFile(),
168
- editor: this.quill,
169
- cursorLocation: this.getIndex(),
170
- });
171
- },
172
- }
173
- };
174
-
175
154
  export default {
176
- components: { QuillEditor },
177
155
  props: {
178
156
  spec: { type: Object, required: true },
179
157
  },
@@ -182,13 +160,6 @@ export default {
182
160
  },
183
161
  data: function () {
184
162
  return {
185
- customToolbar: [
186
- ["bold", "italic", "strike"],
187
- [{ header: [false, 1, 2, 3, 4, 5] }],
188
- [{ list: "ordered" }, { list: "bullet" }],
189
- ["link"],
190
- ],
191
- modules: this.spec.imageUploader ? modules : null,
192
163
  richEditorValue: "",
193
164
  rawEditorValue: "",
194
165
  producedValue: "",
@@ -238,9 +209,39 @@ export default {
238
209
  );
239
210
  },
240
211
  mounted() {
212
+ const quill = new Quill('#editor', {
213
+ readOnly: this.spec.readOnly,
214
+ placeholder: this.spec.placeholder,
215
+ theme: 'snow',
216
+ modules: {
217
+ clipboard: true,
218
+ toolbar: [
219
+ ["bold", "italic", "strike"],
220
+ [{ header: [false, 1, 2, 3, 4, 5] }],
221
+ [{ list: "ordered" }, { list: "bullet" }],
222
+ ["link"],
223
+ ],
224
+ // ImageDropAndPaste: {
225
+ // // add an custom image handler
226
+ // handler: function (imageDataUrl, type, imageData) {
227
+ // bus.$emit("richText/dropOrPaste", {
228
+ // file: imageData.toFile(),
229
+ // editor: quill,
230
+ // cursorLocation: this.getIndex(),
231
+ // });
232
+ // },
233
+ // }
234
+ }
235
+ });
236
+
237
+ const delta = quill.clipboard.convert({ html: this.richEditorValue });
238
+ quill.updateContents(delta, 'user');
239
+
240
+ quill.on('text-change', this.onRichTextEditorChanged);
241
+
241
242
  if (this.spec.imageUploader) {
242
- bus.$on("richText/dropOrPaste", ({ file, editor, cursorLocation }) =>
243
- this.uploadImage(file, editor, cursorLocation)
243
+ bus.$on("richText/dropOrPaste", ({ file, cursorLocation }) =>
244
+ this.uploadImage(file, quill, cursorLocation)
244
245
  );
245
246
  }
246
247
  },
@@ -287,17 +288,13 @@ export default {
287
288
  );
288
289
  },
289
290
  onRichTextEditorChanged: eventFiltering.debounce(function () {
290
- this.richEditorValue = this.$refs.quilEditor.getHTML();
291
+ this.richEditorValue = this.$refs.editor.getHTML();
291
292
  this.producedValue = this.textEditor.producedValue(
292
293
  this.richEditorValue,
293
294
  "html",
294
295
  this.produce
295
296
  );
296
297
 
297
- if (this.spec.cacheKey) {
298
- vueApp.richTextValues[this.spec.cacheKey] = this.richEditorValue;
299
- }
300
-
301
298
  this.$executeOnChange(this.producedValue);
302
299
 
303
300
  }),
@@ -362,22 +359,8 @@ export default {
362
359
  </script>
363
360
 
364
361
  <style>
365
- /*
366
- @import "~vue2-editor/dist/vue2-editor.css";
367
- @import '~quill/dist/quill.core.css';
368
- @import '~quill/dist/quill.snow.css';
369
-
370
- #editor-container {
371
- height: 375px;
372
- }
373
- */
374
- /* .ql-toolbar.sticky {
375
- position: fixed;
376
- z-index: 99;
377
- background-color: white;
378
- } */
379
- .rich-editor,
380
- .raw-editor {
362
+ .ql-editor {
381
363
  height: 350px;
364
+ overflow-y: auto;
382
365
  }
383
366
  </style>
@@ -0,0 +1,221 @@
1
+ <template>
2
+ <div :style="$styles()" :class="$classes()" v-if="loadIf">
3
+ <div id="editor" ref="editor"></div>
4
+ <input type="hidden" :name="fieldName" :value="producedValue" />
5
+ <template v-if="Object.keys(files).length > 0">
6
+ <input type="hidden" v-for="(file, index) in files" :name="spec.imageUploader.name" :value="file.signedId"
7
+ :key="index" />
8
+ </template>
9
+ <RawText v-if="spec.debug" v-bind="rawTextProps"></RawText>
10
+ </div>
11
+ </template>
12
+
13
+ <script>
14
+ import { computed, defineComponent, getCurrentInstance, nextTick, onMounted, ref } from "vue";
15
+ import Quill from "quill";
16
+ import "quill/dist/quill.snow.css";
17
+ import 'quill-mention/dist/quill.mention.css';
18
+ import { Mention, MentionBlot } from "quill-mention";
19
+ const Link = Quill.import('formats/link');
20
+ // Override the existing property on the Quill global object and add custom protocols
21
+ Link.PROTOCOL_WHITELIST = ['http', 'https', 'mailto', 'tel', 'radar', 'rdar', 'smb', 'sms'];
22
+
23
+ class CustomLinkSanitizer extends Link {
24
+ static sanitize(url) {
25
+ // Run default sanitize method from Quill
26
+ const sanitizedUrl = super.sanitize(url);
27
+
28
+ // Not whitelisted URL based on protocol so, let's return `blank`
29
+ if (!sanitizedUrl || sanitizedUrl === 'about:blank') return sanitizedUrl;
30
+
31
+ // Verify if the URL already have a whitelisted protocol
32
+ const hasWhitelistedProtocol = this.PROTOCOL_WHITELIST.some(function (protocol) {
33
+ return sanitizedUrl.startsWith(protocol);
34
+ });
35
+
36
+ if (hasWhitelistedProtocol) return sanitizedUrl;
37
+
38
+ // if not, then append only 'http' to not to be a relative URL
39
+ return `https://${sanitizedUrl}`;
40
+ }
41
+ }
42
+
43
+ Quill.register(CustomLinkSanitizer, true);
44
+
45
+ import { useGlibInput } from "../composable/form";
46
+ import format from "../../utils/format";
47
+ import Action from "../../action";
48
+ import eventFiltering from "../../utils/eventFiltering";
49
+ import { useFileUtils } from "../composable/file";
50
+ import { setBusyWhenUploading, uploadFiles } from "../composable/upload";
51
+ import dom from "../../utils/dom";
52
+ import RawText from "./rawText.vue";
53
+
54
+ const { Item, makeKey } = useFileUtils();
55
+
56
+ export default defineComponent({
57
+ props: {
58
+ spec: Object
59
+ },
60
+ components: { RawText },
61
+ setup(props) {
62
+ useGlibInput({ props });
63
+
64
+ let quill = undefined;
65
+ const instance = getCurrentInstance();
66
+ const editor = ref(null);
67
+ const producedValue = ref(undefined);
68
+
69
+ const { accept, produce, imageUploader } = props.spec;
70
+
71
+ const files = ref([props.spec.images || []].flat().reduce((prev, curr) => {
72
+ if (!curr) return prev;
73
+ return Object.assign({}, prev, { [makeKey()]: new Item(curr) });
74
+ }, {}));
75
+ setBusyWhenUploading({ files });
76
+
77
+
78
+ function sanitizedValue() {
79
+ // let index = 0;
80
+ // const value = quill.root.innerHTML.replace(/src="([^"]+)"|href="([^"]+)"/g, function (_, g1, g2) {
81
+ // if (g1) {
82
+ // return `src="{{image${++index}}}"`;
83
+ // } else if (g2) {
84
+ // return `href="{{image${++index}}}"`;
85
+ // }
86
+ // });
87
+ const value = quill.root.innerHTML;
88
+
89
+ return produce === 'markdown' ? format.htmlToMarkdown(value) : value;
90
+ }
91
+
92
+ function onAfterUploaded(file) {
93
+ fetch(imageUploader.blobUrlGenerator, {
94
+ method: 'POST',
95
+ body: new URLSearchParams({ signed_id: file.signedId, authenticity_token: dom.getCsrf() })
96
+ }).then(async (res) => {
97
+ if (res.ok) {
98
+ const selection = quill.getSelection();
99
+ const index = selection ? selection.index : quill.getLength() - 1;
100
+
101
+ const data = await res.json();
102
+
103
+ if (!file.isImage()) {
104
+ quill.insertText(index, ' ');
105
+ quill.insertText(index, file.name, 'link', data.url);
106
+ } else {
107
+ quill.insertEmbed(index, 'image', data.url, 'user');
108
+ }
109
+
110
+ } else {
111
+ console.error('Failed to create blob url');
112
+ }
113
+ });
114
+
115
+ }
116
+
117
+ function handlePaste(e) {
118
+ if (
119
+ e.clipboardData &&
120
+ e.clipboardData.items &&
121
+ e.clipboardData.items.length
122
+ ) {
123
+ e.preventDefault();
124
+ uploadFiles({ droppedFiles: e.clipboardData.files, files, spec: props.spec.imageUploader, onAfterUploaded });
125
+ }
126
+ }
127
+
128
+ function handleDrop(e) {
129
+ if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length) {
130
+ e.preventDefault();
131
+ uploadFiles({ droppedFiles: e.dataTransfer.files, files, spec: props.spec.imageUploader, onAfterUploaded });
132
+ }
133
+ }
134
+
135
+ const rawTextProps = computed(() => {
136
+ const signedIds = Object.values(files.value).map((file) => file.signedId);
137
+
138
+ return { producedValue: producedValue.value, signedIds };
139
+ });
140
+
141
+ // init quill
142
+ const modules = {
143
+ uploader: {
144
+ handler(range, files) {
145
+ // disable uploader
146
+ }
147
+ },
148
+ toolbar: [
149
+ ["bold", "italic", "strike"],
150
+ [{ header: [false, 1, 2, 3, 4, 5] }],
151
+ [{ list: "ordered" }, { list: "bullet" }],
152
+ ["link"],
153
+ ]
154
+ };
155
+ if (props.spec.mentionList) {
156
+ Quill.register({ "blots/mention": MentionBlot, "modules/mention": Mention });
157
+ modules.mention = {
158
+ allowedChars: /^[A-Za-z\sÅÄÖåäö]*$/,
159
+ mentionDenotationChars: ["@"],
160
+ source: function (searchTerm, renderList, mentionChar) {
161
+ const values = props.spec.mentionList.map((v, index) => ({ id: index + 1, value: v }));
162
+
163
+ if (searchTerm.length === 0) {
164
+ renderList(values, searchTerm);
165
+ } else {
166
+ renderList(values.filter((v) => v.value.toLowerCase().includes(searchTerm)), searchTerm);
167
+ }
168
+ }
169
+ };
170
+ }
171
+ onMounted(() => {
172
+ quill = new Quill('#editor', {
173
+ readOnly: props.spec.readOnly,
174
+ placeholder: props.spec.placeholder,
175
+ theme: 'snow',
176
+ modules
177
+ });
178
+
179
+ let value = props.spec.value;
180
+ switch (accept) {
181
+ case 'html':
182
+ break;
183
+
184
+ default:
185
+ value = format.markdownForEditor(value);
186
+ break;
187
+ }
188
+
189
+ const delta = quill.clipboard.convert({ html: value });
190
+ quill.updateContents(delta, 'silent');
191
+
192
+ producedValue.value = sanitizedValue();
193
+
194
+ quill.on('text-change', eventFiltering.debounce(function () {
195
+ producedValue.value = sanitizedValue();
196
+ nextTick(() => {
197
+ Action.executeWithFormData(props.spec.onChange || props.spec.onChangeAndLoad, instance.ctx, producedValue.value);
198
+ });
199
+ }));
200
+
201
+ if (imageUploader) {
202
+ quill.root.addEventListener('paste', handlePaste, false);
203
+ quill.root.addEventListener('drop', handleDrop, false);
204
+ }
205
+ });
206
+
207
+ return { producedValue, editor, files, rawTextProps };
208
+ }
209
+ });
210
+ </script>
211
+
212
+ <style>
213
+ .ql-editor {
214
+ height: 350px;
215
+ overflow-y: auto;
216
+ }
217
+
218
+ .ql-container {
219
+ height: fit-content;
220
+ }
221
+ </style>
@@ -1,7 +1,10 @@
1
1
  <template>
2
2
  <v-progress-circular :class="$classes()" :rotate="spec.rotate || 0" :size="spec.size || 100" :width="spec.width || 20"
3
3
  :color="spec.color" :model-value="spec.value" :indeterminate="spec.indeterminate">
4
- <span v-if="spec.text" class="grey--text text--lighten-1 text-h5">{{ spec.text }}</span>
4
+ <div>
5
+ <div class="value-style text-center">{{ spec.value }}%</div>
6
+ <span v-if="spec.text" class="text-style">{{ spec.text }}</span>
7
+ </div>
5
8
  </v-progress-circular>
6
9
  </template>
7
10
 
@@ -13,4 +16,16 @@ export default {
13
16
  };
14
17
  </script>
15
18
 
16
- <style scoped></style>
19
+ <style scoped>
20
+ .value-style {
21
+ font-size: 16px;
22
+ line-height: 150%;
23
+ font-weight: 700;
24
+ }
25
+
26
+ .text-style {
27
+ font-size: 16px;
28
+ line-height: 150%;
29
+ font-weight: 400;
30
+ }
31
+ </style>
@@ -5,8 +5,8 @@ describe('form dynamic', () => {
5
5
  it('add, delete, and submit', () => {
6
6
  cy.visit(url)
7
7
 
8
- cy.get('.question:first').click().type(' edited')
9
- cy.get('.answer_type:first').click()
8
+ cy.get('.question').eq(0).click().type(' edited')
9
+ cy.get('.answer_type').eq(0).click()
10
10
  cy.get('.v-overlay-container').contains('Yes no').click()
11
11
 
12
12
  cy.contains('Add item').click()
@@ -54,7 +54,7 @@ Form Data:
54
54
  it('can execute action just like normal component', () => {
55
55
  cy.visit(url)
56
56
 
57
- cy.get('.answer_type:first').click()
57
+ cy.get('.answer_type').eq(0).click()
58
58
  cy.get('.v-overlay-container').contains('Choices').click()
59
59
 
60
60
  const result = `{
@@ -5,6 +5,9 @@ describe('glib lifecycle hooks', () => {
5
5
  it('execute onLoad and onRerender', () => {
6
6
  cy.visit(url)
7
7
 
8
+ cy.contains('page.onLoad').should('be.exist')
9
+ cy.contains('selectable').click()
10
+ cy.contains('lifecycle').click()
8
11
  cy.contains('page.onLoad').should('be.exist')
9
12
 
10
13
  // check if onLoad executed
@@ -0,0 +1,56 @@
1
+ name: "Cypress"
2
+ on:
3
+ push:
4
+ paths:
5
+ - "yarn.lock"
6
+ - ".github/workflows/cypress.yml"
7
+ jobs:
8
+ build-and-test:
9
+ env:
10
+ RAILS_ENV: test
11
+ POSTGRES_USER: postgres
12
+ POSTGRES_PASSWORD: postgres
13
+ PARALLEL_WORKERS: 2
14
+ RUN_ON: github
15
+ runs-on: ubuntu-latest
16
+ services:
17
+ db:
18
+ image: postgres:11
19
+ ports: ["5432:5432"]
20
+ env:
21
+ POSTGRES_USER: postgres
22
+ POSTGRES_PASSWORD: postgres
23
+ POSTGRES_DB: postgres
24
+ options: >-
25
+ --health-cmd pg_isready
26
+ --health-interval 10s
27
+ --health-timeout 5s
28
+ --health-retries 5
29
+ steps:
30
+ - uses: actions/checkout@v2
31
+ - name: Setup Ruby
32
+ uses: ruby/setup-ruby@v1
33
+ with:
34
+ ruby-version: 3.2.1
35
+ bundler-cache: true
36
+ - name: Setup Node
37
+ uses: actions/setup-node@v2
38
+ with:
39
+ cache: "yarn"
40
+ node-version: 20.0.0
41
+ - run: yarn install --frozen-lockfile
42
+ - run: sudo apt-get -yqq install libpq-dev
43
+ - run: cp config/database.yml.github-actions config/database.yml
44
+ - run: bundle exec rake db:test:prepare
45
+ - run: bundle exec rails server -d
46
+ - name: Cypress run
47
+ uses: cypress-io/github-action@v6
48
+ with:
49
+ browser: chrome
50
+ - name: Upload screenshots
51
+ uses: actions/upload-artifact@v4
52
+ if: failure()
53
+ with:
54
+ name: cypress-screenshots
55
+ path: cypress/screenshots
56
+ - run: yarn run cypress run
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glib-web",
3
- "version": "4.26.1",
3
+ "version": "4.27.2",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -13,7 +13,6 @@
13
13
  "@googlemaps/markerclusterer": "^2.5.0",
14
14
  "@rails/actioncable": "^7.1.1",
15
15
  "@rails/activestorage": "^7.1.1",
16
- "@vueup/vue-quill": "^1.2.0",
17
16
  "awesome-phonenumber": "2.15.0",
18
17
  "chart.js": "^4.3.1",
19
18
  "chartjs-plugin-datalabels": "^2.2.0",
@@ -30,7 +29,8 @@
30
29
  "papaparse": "^5.4.1",
31
30
  "phoenix": "^1.5.3",
32
31
  "push.js": "^1.0.12",
33
- "quill-image-drop-and-paste": "^1.2.14",
32
+ "quill": "2.0.3",
33
+ "quill-mention": "^6.0.2",
34
34
  "tiny-emitter": "^2.1.0",
35
35
  "turndown": "^7.1.1",
36
36
  "turndown-plugin-gfm": "^1.0.2",
package/utils/format.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { marked } from "marked";
2
+ import { gfm } from "turndown-plugin-gfm";
3
+ import TurndownService from "turndown";
2
4
 
3
5
  export default class {
4
6
  static markdownForEditor(text) {
@@ -27,6 +29,25 @@ export default class {
27
29
  return marked.parse(text);
28
30
  }
29
31
 
32
+ static htmlToMarkdown(text) {
33
+ const turndownService = new TurndownService({ headingStyle: "atx" });
34
+ turndownService.use(gfm);
35
+ turndownService.addRule("strikethrough", {
36
+ filter: ["del", "s", "strike"],
37
+ replacement: function (content) {
38
+ return "~~" + content + "~~";
39
+ },
40
+ });
41
+
42
+ turndownService.addRule("codeblock", {
43
+ filter: ["pre"],
44
+ replacement: function (content) {
45
+ return "```\n" + content + "```";
46
+ },
47
+ });
48
+ return turndownService.turndown(text);
49
+ }
50
+
30
51
  static local_iso8601(date) {
31
52
  // See https://stackoverflow.com/a/29774197
32
53
  const offset = date.getTimezoneOffset();