glib-web 4.8.3 → 4.9.0

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/action.js CHANGED
@@ -36,7 +36,6 @@ import ActionsWindowsPrint from "./actions/windows/print";
36
36
  import ActionsPanelsScrollToBottom from "./actions/panels/scrollToBottom";
37
37
  import ActionsPanelsScrollTo from "./actions/panels/scrollTo";
38
38
 
39
- import ActionsWsPush from "./actions/ws/push";
40
39
  import ActionsCablesPush from "./actions/cables/push";
41
40
 
42
41
  import ActionsTimeoutsSet from "./actions/timeouts/set";
@@ -138,7 +137,6 @@ const actions = {
138
137
  "timeouts/set": ActionsTimeoutsSet,
139
138
  "timeouts/clear": ActionsTimeoutsClear,
140
139
 
141
- "ws/push": ActionsWsPush,
142
140
  "cables/push": ActionsCablesPush,
143
141
 
144
142
  "auth/saveCsrfToken": ActionsAuthSaveCsrfToken,
@@ -22,6 +22,31 @@ const nullishCoalescing = function (a, b) {
22
22
  };
23
23
  jsonLogic.add_operation("??", nullishCoalescing);
24
24
 
25
+ const sum = function (...args) {
26
+ return args.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
27
+ };
28
+ jsonLogic.add_operation("sum", sum);
29
+
30
+ const countNonNull = function (...args) {
31
+ return args.reduce((accumulator, currentValue) => {
32
+ if (currentValue) {
33
+ accumulator++;
34
+ }
35
+ }, 0);
36
+ };
37
+ jsonLogic.add_operation("countNonNull", countNonNull);
38
+
39
+ const printf = function (template, ...args) {
40
+ // See https://www.geeksforgeeks.org/what-are-the-equivalent-of-printf-string-format-in-javascript/
41
+ // const args = arguments;
42
+ return template.replace(/{(\d+)}/g, function (match, number) {
43
+ return typeof args[number] != 'undefined'
44
+ ? args[number]
45
+ : match;
46
+ });
47
+ }
48
+ jsonLogic.add_operation("printf", printf);
49
+
25
50
  export default class {
26
51
  execute(spec, component) {
27
52
  let targetComponent;
@@ -107,13 +107,14 @@ import BulkEditPanel from "./panels/bulkEdit.vue";
107
107
  import BulkEditPanel2 from "./panels/bulkEdit2.vue";
108
108
  import CustomPanel from "./panels/custom.vue";
109
109
  import ColumnPanel from "./panels/column.vue";
110
- import ResponsivePanel from "./panels/responsive.vue";
110
+ import ResponsivePanel from "./responsive.vue";
111
111
  import UlPanel from "./panels/ul.vue";
112
112
  import WebPanel from "./panels/web.vue";
113
113
  import GridPanel from "./panels/grid.vue";
114
114
  import TimelinePanel from "./panels/timeline.vue";
115
115
  import AssociationPanel from "./panels/association.vue";
116
116
  import TreePanel from "./panels/tree.vue";
117
+ import PaginationPanel from "./panels/pagination.vue";
117
118
 
118
119
  import MultimediaVideo from "./multimedia/video.vue";
119
120
 
@@ -214,6 +215,7 @@ export default {
214
215
  "panels-timeline": TimelinePanel,
215
216
  "panels-association": AssociationPanel,
216
217
  "panels-tree": TreePanel,
218
+ "panels-pagination": PaginationPanel,
217
219
 
218
220
  "multimedia-video": MultimediaVideo,
219
221
 
@@ -37,5 +37,10 @@ export function parseCsv(csvString) {
37
37
  // return r[0] !== "undefined"
38
38
  // });
39
39
 
40
- return result;
40
+ const columns = result.shift();
41
+ return result.map(row => {
42
+ return columns.reduce((prev, curr, index) => {
43
+ return Object.assign({}, prev, { [curr]: row[index] });
44
+ }, {});
45
+ });
41
46
  }
@@ -28,7 +28,8 @@ export default {
28
28
  display: flex;
29
29
  align-items: center;
30
30
  justify-content: center;
31
- width: 240px;
31
+ width: 100%;
32
+ min-height: 230px;
32
33
  height: 100%;
33
34
  transition: border-color 0.3s, box-shadow 0.3s, color 0.3s;
34
35
  text-align: center;
@@ -45,7 +46,6 @@ export default {
45
46
  }
46
47
 
47
48
  .custom-radio:hover .custom-radio-label {
48
- font-size: 22px;
49
49
  color: #0A2A9E;
50
50
  }
51
51
 
@@ -55,27 +55,21 @@ export default {
55
55
  align-items: center;
56
56
  justify-content: center;
57
57
  width: 100%;
58
- padding: 40px;
58
+ min-height: 226px;
59
+ padding: 32px;
59
60
  }
60
61
 
61
62
  .custom-radio .custom-radio-label {
62
- font-size: 22px;
63
+ font-size: 18px;
63
64
  color: inherit;
64
65
  margin-top: 24px;
65
66
  word-break: break-word;
66
- min-width: 180px;
67
67
  }
68
68
 
69
69
  .custom-radio ::v-deep .v-selection-control__input {
70
- width: 240px;
71
- height: 254px;
70
+ width: 100%;
71
+ height: 100%;
72
72
  background-color: transparent;
73
- left: 50%;
74
-
75
- >.v-icon {
76
- opacity: 0;
77
- }
78
-
79
73
  }
80
74
 
81
75
  .custom-radio ::v-deep .v-selection-control__input::before {
@@ -103,7 +97,6 @@ export default {
103
97
  .radio--active .custom-radio-label {
104
98
  color: #0A2A9E;
105
99
  font-weight: 700;
106
- font-size: 22px;
107
100
  }
108
101
 
109
102
  .radio--active .v-icon {
@@ -37,8 +37,9 @@ export default {
37
37
  display: flex;
38
38
  align-items: center;
39
39
  justify-content: center;
40
- width: 240px;
41
- min-height: 254px;
40
+ width: 100%;
41
+ min-height: 230px;
42
+ height: 100%;
42
43
  transition: border-color 0.3s, box-shadow 0.3s, color 0.3s;
43
44
  text-align: center;
44
45
  cursor: pointer;
@@ -53,36 +54,20 @@ export default {
53
54
  border-color: #0A2A9E;
54
55
  }
55
56
 
56
- .custom-radio:hover .custom-radio-label {
57
- font-size: 22px;
58
- color: #0A2A9E;
59
- }
60
-
61
57
  .custom-radio .custom-radio-content {
62
58
  display: flex;
63
59
  flex-direction: column;
64
60
  align-items: center;
65
61
  justify-content: center;
66
62
  width: 100%;
67
- padding: 40px;
68
- }
69
-
70
- .custom-radio .custom-radio-label {
71
- font-size: 22px;
72
- color: inherit;
73
- margin-top: 24px;
63
+ min-height: 226px;
64
+ padding: 32px;
74
65
  }
75
66
 
76
67
  .custom-radio ::v-deep .v-selection-control__input {
77
- width: 240px;
78
- height: 254px;
68
+ width: 100%;
69
+ height: 100%;
79
70
  background-color: transparent;
80
- left: 50%;
81
-
82
- >.v-icon {
83
- opacity: 0;
84
- }
85
-
86
71
  }
87
72
 
88
73
  .custom-radio ::v-deep .v-selection-control__input::before {
@@ -107,12 +92,6 @@ export default {
107
92
  border-color: #0A2A9E;
108
93
  }
109
94
 
110
- .radio--active .custom-radio-label {
111
- color: #0A2A9E;
112
- font-weight: 700;
113
- font-size: 22px;
114
- }
115
-
116
95
  .radio--active .v-icon {
117
96
  color: #0A2A9E;
118
97
  }
package/components/h1.vue CHANGED
@@ -1,20 +1,12 @@
1
1
  <template>
2
- <h1
3
- :style="textStyles()"
4
- :class="$classes()"
5
- :href="$href()"
6
- :rel="$rel()"
7
- @click="$onClick()"
8
- >
2
+ <h1 :style="$styles()" :class="$classes()" :href="$href()" :rel="$rel()" @click="$onClick()">
9
3
  {{ spec.text }}
10
4
  </h1>
11
5
  </template>
12
6
 
13
7
  <script>
14
- import textMixin from "./mixins/text.js";
15
8
 
16
9
  export default {
17
- mixins: [textMixin],
18
10
  props: {
19
11
  spec: { type: Object, required: true }
20
12
  }
package/components/h2.vue CHANGED
@@ -1,20 +1,12 @@
1
1
  <template>
2
- <h2
3
- :style="textStyles()"
4
- :class="$classes()"
5
- :href="$href()"
6
- :rel="$rel()"
7
- @click="$onClick()"
8
- >
2
+ <h2 :style="$styles()" :class="$classes()" :href="$href()" :rel="$rel()" @click="$onClick()">
9
3
  {{ spec.text }}
10
4
  </h2>
11
5
  </template>
12
6
 
13
7
  <script>
14
- import textMixin from "./mixins/text.js";
15
8
 
16
9
  export default {
17
- mixins: [textMixin],
18
10
  props: {
19
11
  spec: { type: Object, required: true }
20
12
  }
package/components/h3.vue CHANGED
@@ -1,20 +1,12 @@
1
1
  <template>
2
- <h3
3
- :style="textStyles()"
4
- :class="$classes()"
5
- :href="$href()"
6
- :rel="$rel()"
7
- @click="$onClick()"
8
- >
2
+ <h3 :style="$styles()" :class="$classes()" :href="$href()" :rel="$rel()" @click="$onClick()">
9
3
  {{ spec.text }}
10
4
  </h3>
11
5
  </template>
12
6
 
13
7
  <script>
14
- import textMixin from "./mixins/text.js";
15
8
 
16
9
  export default {
17
- mixins: [textMixin],
18
10
  props: {
19
11
  spec: { type: Object, required: true }
20
12
  }
package/components/h4.vue CHANGED
@@ -1,14 +1,12 @@
1
1
  <template>
2
- <h4 :style="textStyles()" :class="$classes()" :href="$href()" :rel="$rel()" @click="$onClick()">
2
+ <h4 :style="$styles()" :class="$classes()" :href="$href()" :rel="$rel()" @click="$onClick()">
3
3
  {{ spec.text }}
4
4
  </h4>
5
5
  </template>
6
6
 
7
7
  <script>
8
- import textMixin from "./mixins/text.js";
9
8
 
10
9
  export default {
11
- mixins: [textMixin],
12
10
  props: {
13
11
  spec: { type: Object, required: true }
14
12
  }
package/components/h5.vue CHANGED
@@ -1,20 +1,12 @@
1
1
  <template>
2
- <h5
3
- :style="textStyles()"
4
- :class="$classes()"
5
- :href="$href()"
6
- :rel="$rel()"
7
- @click="$onClick()"
8
- >
2
+ <h5 :style="$styles()" :class="$classes()" :href="$href()" :rel="$rel()" @click="$onClick()">
9
3
  {{ spec.text }}
10
4
  </h5>
11
5
  </template>
12
6
 
13
7
  <script>
14
- import textMixin from "./mixins/text.js";
15
8
 
16
9
  export default {
17
- mixins: [textMixin],
18
10
  props: {
19
11
  spec: { type: Object, required: true }
20
12
  }
package/components/h6.vue CHANGED
@@ -1,20 +1,11 @@
1
1
  <template>
2
- <h4
3
- :style="textStyles()"
4
- :class="$classes()"
5
- :href="$href()"
6
- :rel="$rel()"
7
- @click="$onClick()"
8
- >
2
+ <h4 :style="$styles()" :class="$classes()" :href="$href()" :rel="$rel()" @click="$onClick()">
9
3
  {{ spec.text }}
10
4
  </h4>
11
5
  </template>
12
6
 
13
7
  <script>
14
- import textMixin from "./mixins/text.js";
15
-
16
8
  export default {
17
- mixins: [textMixin],
18
9
  props: {
19
10
  spec: { type: Object, required: true }
20
11
  }
@@ -1,15 +1,12 @@
1
1
  <template>
2
- <a v-if="spec.onClick" :href="$href()" :rel="$rel()" :style="textStyles()" :class="$classes()" @click="$onClick()">{{
2
+ <a v-if="spec.onClick" :href="$href()" :rel="$rel()" :style="$styles()" :class="$classes()" @click="$onClick()">{{
3
3
  spec.text }}</a>
4
4
  <span v-else :style="$styles()" :class="$classes()">{{ spec.text }}</span>
5
5
  </template>
6
6
 
7
7
  <script>
8
- // import actionCableMixin from "./mixins/ws/actionCable";
9
- import textMixin from "./mixins/text.js";
10
8
 
11
9
  export default {
12
- mixins: [textMixin],
13
10
  props: {
14
11
  spec: { type: Object, required: true }
15
12
  }
@@ -0,0 +1,54 @@
1
+ export default {
2
+ computed: {
3
+ viewId() {
4
+ if (this.spec && this.spec.id) {
5
+ const id = this.spec.id;
6
+
7
+ if (id.includes('{{entry_index}}')) {
8
+ const dynamicGroupEntry = this.$closest("fields/internalDynamicGroupEntry");
9
+ if (dynamicGroupEntry) {
10
+ return dynamicGroupEntry.$populateIndexes(id);
11
+ }
12
+ }
13
+
14
+ return id;
15
+ }
16
+ }
17
+ },
18
+ methods: {
19
+ $ready() {
20
+ let spec = this.spec;
21
+ if (spec && spec.id && this.$registryEnabled()) {
22
+
23
+ const id = this.viewId;
24
+
25
+ const existingComponent = GLib.component.findById(id);
26
+ // A component with the same ID in a different page shouldn't be considered a
27
+ // duplicate. See `utils/components#deregister` for more details.
28
+ if (existingComponent) {
29
+ console.warn(
30
+ "Duplicate component ID:",
31
+ id,
32
+ "Existing:",
33
+ GLib.component.vueName(existingComponent),
34
+ "New:",
35
+ GLib.component.vueName(this)
36
+ );
37
+ }
38
+ const newComponent = this;
39
+ GLib.component.register(id, newComponent);
40
+ }
41
+ },
42
+ $tearDown() {
43
+ let spec = this.spec;
44
+
45
+ if (spec && spec.id && this.$registryEnabled()) {
46
+ GLib.component.deregister(spec.id, this);
47
+ }
48
+ },
49
+ $registryEnabled() {
50
+ // Common classes such as `_select` need to return false so that it doesn't override its parent (e.g. `select`).
51
+ return false;
52
+ }
53
+ }
54
+ };
@@ -2,11 +2,14 @@
2
2
  <div :style="$styles()" :class="$classes()" v-if="loadIf">
3
3
  <div>Rows: {{ loadedRowCount }} Selected: {{ selectedRowCount }}</div>
4
4
  <input ref="fileInput" style="display: none;" type="file" accept=".csv" @change="handleClick" />
5
+
5
6
  <div class="scrollable">
6
- <table :class="rowLoaded ? 'loaded' : 'nonLoaded'">
7
+ <table :class="rowLoaded ? 'loaded' : 'nonLoaded'" width="100%">
7
8
  <thead v-if="props.spec.viewHeaders">
8
9
  <tr>
9
- <th class="cell-selection" v-if="rowLoaded"><v-checkbox v-model="checkbox" color="primary" @change="checkAll"></v-checkbox></th>
10
+ <th class="cell-selection" v-if="rowLoaded"><v-checkbox v-model="checkbox" color="primary"
11
+ @change="checkAll"></v-checkbox></th>
12
+ <th></th>
10
13
  <th :class="`cell-column${cellIndex}`" v-for="(cell, cellIndex) in props.spec.viewHeaders" :key="cell.id"
11
14
  :style="{ minWidth: `${cell.minWidth || 100}px` }">
12
15
  <span>{{ cell.text }}</span>
@@ -18,6 +21,7 @@
18
21
  <template v-if="rowLoaded">
19
22
  <tr v-for="(row, rowIndex) in rows" :key="`row_${rowIndex}`">
20
23
  <td class="cell-selection"><v-checkbox v-model="row.selected" color="primary"></v-checkbox></td>
24
+ <td class="cell-status"><glib-component :spec="row.iconSpec" /></td>
21
25
  <td :class="`cell-column${cellIndex}`" v-for="(cell, cellIndex) in row.columns" :key="`cell_${cellIndex}`"
22
26
  @change="(e) => handleCellChange(e, cell)">
23
27
  <glib-component :spec="cell.view"></glib-component>
@@ -25,19 +29,21 @@
25
29
  </tr>
26
30
  </template>
27
31
 
28
- <tr v-else>
29
- <td colspan="100%" style="padding-top: 8px">
30
- <div class="gdrop-file border-[2px]" @dragover="(e) => e.preventDefault()" @drop="handleDrop"
31
- @click="fileInput.click()">
32
-
33
- <div class="cloud" style="pointer-events: none;">
34
- <v-icon ref="icon" size="48" class="icon">cloud_upload</v-icon>
35
- <h4 class="title">Drag your CSV file here</h4>
36
- <p class="subtitle">or click to browse</p>
32
+ <template v-else>
33
+ <tr>
34
+ <td colspan="100%" style="padding-top: 8px">
35
+ <div class="gdrop-file border-[2px]" @dragover="(e) => e.preventDefault()" @drop="handleDrop"
36
+ @click="fileInput.click()">
37
+
38
+ <div class="cloud" style="pointer-events: none;">
39
+ <v-icon ref="icon" size="48" class="icon">cloud_upload</v-icon>
40
+ <h4 class="title">Drag your CSV file here</h4>
41
+ <p class="subtitle">or click to browse</p>
42
+ </div>
37
43
  </div>
38
- </div>
39
- </td>
40
- </tr>
44
+ </td>
45
+ </tr>
46
+ </template>
41
47
  </tbody>
42
48
  </table>
43
49
  </div>
@@ -51,10 +57,12 @@ table thead th {
51
57
  z-index: 1005;
52
58
  background-color: white;
53
59
  }
60
+
54
61
  .scrollable {
55
62
  width: 100%;
56
63
  overflow: auto;
57
64
  }
65
+
58
66
  .cell-selection {
59
67
  min-width: 40px;
60
68
  }
@@ -65,6 +73,28 @@ import { computed, getCurrentInstance, onMounted, ref, watch } from "vue";
65
73
  import { parseCsv } from "../composable/parser";
66
74
  import Action from "../../action";
67
75
 
76
+ class Row {
77
+ constructor({ id, columns, selected, index }) {
78
+ this.id = id;
79
+ this.selected = selected;
80
+ this.columns = columns;
81
+
82
+ this.statusCompId = statusCompId(index);
83
+ }
84
+
85
+ get iconSpec() {
86
+ return {
87
+ view: 'icon',
88
+ id: this.statusCompId,
89
+ material: { name: 'preview' },
90
+ styleClasses: ['warning'],
91
+ tooltip: {
92
+ text: "Review"
93
+ }
94
+ };
95
+ }
96
+ }
97
+
68
98
  class Cell {
69
99
  constructor({ viewHeaders, cellIndex, rowId, view, value }) {
70
100
  this.viewHeaders = viewHeaders;
@@ -87,19 +117,23 @@ function handleDrop(e) {
87
117
  loadFile(e.dataTransfer.files);
88
118
  }
89
119
 
120
+ function statusCompId(rowIndex) {
121
+ return (props.spec.statusViewIdPrefix || 'status') + `-${rowIndex}`;
122
+ }
123
+
90
124
  function makeRows(dataRows) {
91
125
  if (!dataRows || dataRows.length <= 0) return [];
92
- const rows = dataRows.map((dataRow) => {
126
+ const rows = dataRows.map((dataRow, index) => {
93
127
  const selected = false;
94
- const rowId = dataRow.rowId;
128
+ const id = dataRow.rowId;
95
129
  const columns = props.spec.viewCells.map((viewCells, i) => {
96
130
  const view = Object.assign({}, viewCells, dataRow.columns[i]);
97
131
  const cellIndex = i;
98
132
  const viewHeaders = props.spec.viewHeaders;
99
133
  const value = dataRow.columns[i].value;
100
- return new Cell({ rowId, view, cellIndex, viewHeaders, value });
134
+ return new Cell({ rowId: id, view, cellIndex, viewHeaders, value });
101
135
  });
102
- return { selected, columns, rowId };
136
+ return new Row({ selected, columns, id, index });
103
137
  });
104
138
  return rows;
105
139
  }
@@ -109,15 +143,12 @@ const fileInput = ref(null);
109
143
  const checkbox = ref(false);
110
144
  const instance = getCurrentInstance();
111
145
 
112
- const fillableColumnIndexes = props.spec.viewHeaders.reduce((prev, curr, index) => {
113
- if (curr.importable) prev.push(index);
114
- return prev;
115
- }, []);
116
-
117
146
  const rows = ref(makeRows(props.spec.dataRows));
118
147
 
119
148
  watch(props, (value) => {
120
- rows.value = makeRows(value.spec.dataRows);
149
+ if (value.spec.dataRows && value.spec.dataRows.length > 0) {
150
+ rows.value = makeRows(value.spec.dataRows);
151
+ }
121
152
  });
122
153
 
123
154
  const selectedRows = computed(() => {
@@ -173,22 +204,23 @@ function handleCellChange(e, cell) {
173
204
  Action.execute(data, instance.ctx);
174
205
  }
175
206
 
207
+ function getColumnIndexById(id) {
208
+ return props.spec.viewHeaders.map(v => v.id).indexOf(id);
209
+ }
210
+
176
211
  function loadFile(files) {
177
212
  const reader = new FileReader();
178
213
  reader.readAsText(files[0]);
179
214
 
180
215
  reader.onload = (ev) => {
181
216
  const csvData = parseCsv(ev.target.result);
182
- const dataRows = csvData.map((columns, rowIndex) => {
183
- let i = 0;
184
- const cols = [];
185
- props.spec.viewCells.forEach((viewCell, index) => {
186
- const obj = {};
187
- if (fillableColumnIndexes.includes(index)) {
188
- cols.push(Object.assign(obj, viewCell, { value: columns[i] }));
189
- i++;
190
- } else {
191
- cols.push(Object.assign(obj, viewCell, { id: `cell-${rowIndex}-${i}` }));
217
+
218
+ const dataRows = csvData.map((row) => {
219
+ const cols = JSON.parse(JSON.stringify(props.spec.viewCells));
220
+ Object.entries(row).forEach((v) => {
221
+ const index = getColumnIndexById(v[0]);
222
+ if (cols[index]) {
223
+ cols[index].value = v[1];
192
224
  }
193
225
  });
194
226
  return { rowId: null, columns: cols };
@@ -204,8 +236,21 @@ function submitRows(rows) {
204
236
  if (!rows || rows.length <= 0) return;
205
237
 
206
238
  const row = rows.shift();
207
- row.columns = row.columns.map((v) => ({ rowId: v.rowId, cellId: v.cellId, value: v.value, compId: v.view.id }));
208
239
 
240
+ // change row status to pending
241
+ Action.execute({
242
+ action: 'components/set',
243
+ targetId: row.statusCompId,
244
+ data: {
245
+ material: { name: 'pending' },
246
+ styleClasses: ['info']
247
+ },
248
+ tooltip: {
249
+ text: 'pending'
250
+ },
251
+ }, instance.ctx);
252
+
253
+ row.columns = row.columns.map((v) => ({ rowId: v.rowId, cellId: v.cellId, value: v.value }));
209
254
  const { submitUrl, paramName } = props.spec.import;
210
255
 
211
256
  const data = {
@@ -0,0 +1,47 @@
1
+ <template>
2
+ <v-pagination :style="$styles()" :class="$classes()"
3
+ :density="density" :variant="variant"
4
+ :length="spec.length" v-model="spec.value" @update:model-value="updatePage">
5
+ </v-pagination>
6
+ </template>
7
+
8
+ <script>
9
+ import { determineDensity, determineVariant } from "../../utils/constant";
10
+
11
+ // import { computed, getCurrentInstance, onMounted, ref, watch } from "vue";
12
+ // import { getCurrentInstance } from "vue";
13
+
14
+ // const props = defineProps(['spec']);
15
+ // const instance = getCurrentInstance();
16
+
17
+ // function updatePage(pageIndex) {
18
+ // this.$executeOnChange(pageIndex)
19
+
20
+ // // const url = `${props.spec.baseUrl}
21
+ // // alert(pageIndex)
22
+ // // console.log("P1", pageIndex)
23
+ // // Action.execute(props.spec.onChange, instance.ctx);
24
+ // }
25
+
26
+ export default {
27
+ props: {
28
+ spec: { type: Object, required: true }
29
+ },
30
+ computed: {
31
+ density() {
32
+ return determineDensity(this.spec.styleClasses);
33
+ },
34
+ variant() {
35
+ return determineVariant(this.spec.styleClasses);
36
+ }
37
+ },
38
+ methods: {
39
+ updatePage(pageIndex) {
40
+ this.$executeOnChange(pageIndex)
41
+ }
42
+ }
43
+ }
44
+ </script>
45
+
46
+ <style lang="scss" scoped>
47
+ </style>
@@ -0,0 +1,113 @@
1
+ <template>
2
+ <div class="scrollable">
3
+ <table :style="$styles()" :class="$classes()" v-if="loadIf">
4
+ <thead>
5
+ <tr v-if="section.header" :style="$styles(section.header)">
6
+ <th v-for="(cell, index) in section.header.cellViews" :key="index" :colSpan="colSpan(section.header, index)">
7
+ <glib-component :spec="cell" />
8
+ </th>
9
+ </tr>
10
+ </thead>
11
+
12
+ <tbody>
13
+ <template v-for="(row, rowIndex) in rows" :key="`row_${rowIndex}`">
14
+ <tr :class="row.onClick ? 'clickable' : ''" @[dstart(row.dragData)]="(e) => handleDragStart(e, row.dragData)"
15
+ :draggable="!!row.dragData">
16
+ <td v-for="(cell, cellIndex) in row.cellViews" :key="`cell_${cellIndex}`" :colSpan="colSpan(row, cellIndex)"
17
+ :style="colStyles(row, cellIndex)">
18
+ <span>
19
+ <!-- Prevent double links -->
20
+ <glib-component v-if="$href(cell)" :spec="cell" />
21
+ <!-- without "|| null" the browser will reload strangely -->
22
+ <a v-else :href="$href(row) || null" @click="$onClick($event, row)">
23
+ <glib-component :spec="cell" />
24
+ </a>
25
+ </span>
26
+ </td>
27
+ </tr>
28
+ </template>
29
+ </tbody>
30
+ </table>
31
+ </div>
32
+ </template>
33
+
34
+ <script>
35
+
36
+ export default {
37
+ props: {
38
+ spec: { type: Object, required: true }
39
+ },
40
+ methods: {
41
+ colSpan(row, index) {
42
+ const spans = row.colSpans || [];
43
+ return spans[index] || 1;
44
+ },
45
+ colStyles(row, index) {
46
+ const colStyles = row.colStyles || [];
47
+ return this.$styles(colStyles[index] || {});
48
+ }
49
+ }
50
+ };
51
+ </script>
52
+
53
+ <style lang="scss" scoped>
54
+ table {
55
+ border-spacing: 0;
56
+ }
57
+
58
+ tbody {
59
+ tr.clickable {
60
+ td>a {
61
+ cursor: pointer;
62
+ }
63
+
64
+ &:hover {
65
+ background: #eee;
66
+ }
67
+ }
68
+
69
+ td {
70
+ border-top: 1px solid rgba(0, 0, 0, 0.12);
71
+
72
+ span {
73
+ display: block;
74
+ color: inherit;
75
+ cursor: default;
76
+ }
77
+ }
78
+ }
79
+
80
+ .scrollable {
81
+ width: 100%;
82
+ overflow: auto;
83
+ }
84
+
85
+ .data-cell {
86
+ white-space: pre-line;
87
+ }
88
+
89
+ table.table--grid {
90
+ tbody {
91
+ td {
92
+ border-right: 1px solid rgba(0, 0, 0, 0.12);
93
+ }
94
+ }
95
+ }
96
+ </style>
97
+
98
+ <!-- Overridable -->
99
+ <style lang="scss">
100
+ .panels-table {
101
+ th {
102
+ padding: 10px 4px;
103
+ border-top: 1px solid rgba(0, 0, 0, 0.12);
104
+ // border-left: 1px solid rgba(0, 0, 0, 0.12);
105
+ }
106
+
107
+ td {
108
+ >span {
109
+ padding: 10px 24px;
110
+ }
111
+ }
112
+ }
113
+ </style>
@@ -5,7 +5,7 @@
5
5
  <div class="content">
6
6
  <v-btn style="z-index: 11;" size="24" :icon="node.expand ? 'arrow_drop_down' : 'arrow_right'"
7
7
  v-if="props.node.rows" @click="node.expand = !node.expand" variant="plain" @click.stop></v-btn>
8
- <div style="width: 24px" v-else></div>
8
+ <div class="expand-none" v-else></div>
9
9
  <div class="text">
10
10
  <v-icon v-if="node.icon" :size="node.icon.size || 24" :icon="node.icon.name"
11
11
  :color="node.icon.color"></v-icon>
@@ -27,6 +27,12 @@
27
27
  </template>
28
28
 
29
29
  <style lang="scss">
30
+ .panels-tree {
31
+ .expand-none {
32
+ width: 24px;
33
+ }
34
+ }
35
+
30
36
  .gtree-row .node {
31
37
  position: relative;
32
38
  width: 100%;
@@ -39,6 +39,8 @@ export default {
39
39
  return this.$classes(this.spec, "panels/responsive");
40
40
  },
41
41
  componentName() {
42
+ if (!this.spec) return 'div';
43
+
42
44
  return this.spec.onClick ? "a" : "div";
43
45
  },
44
46
  header() {
package/index.js CHANGED
@@ -26,7 +26,8 @@ Vue.use(vuetify);
26
26
  import { gmapPlugin } from "./plugins/gmap";
27
27
  Vue.use(gmapPlugin, {
28
28
  key: import.meta.env.VITE_GMAPS_API_KEY,
29
- libraries: 'places'
29
+ libraries: 'places',
30
+ loading: 'async'
30
31
  });
31
32
 
32
33
  // import VueAnalytics from 'vue-analytics'
@@ -40,7 +41,7 @@ import "./extensions/array.js";
40
41
 
41
42
  // Recursive components must be global
42
43
  import VerticalPanel from "./components/panels/vertical.vue";
43
- import ResponsivePanel from "./components/panels/responsive.vue";
44
+ import ResponsivePanel from "./components/responsive.vue";
44
45
  import Component from "./components/component.vue";
45
46
  import CommonIcon from "./components/_icon.vue";
46
47
  import CommonBadge from "./components/_badge.vue";
@@ -49,7 +50,7 @@ import CommonButton from "./components/_button.vue";
49
50
  import CommonChip from "./components/_chip.vue";
50
51
  import CommonMessage from "./components/_message.vue";
51
52
  import CommonDropdownMenu from "./components/_dropdownMenu.vue";
52
- import CommonResponsive from "./components/_responsive.vue";
53
+ import CommonResponsive from "./components/responsive.vue";
53
54
  import CommonTemplateMenu from "./templates/_menu.vue";
54
55
  import RichButton from "./components/button.vue";
55
56
  Vue.component("panels-vertical", VerticalPanel);
@@ -82,6 +83,9 @@ Vue.mixin(stylesMixin);
82
83
  import scrollingMixin from "./components/mixins/scrolling.js";
83
84
  Vue.mixin(scrollingMixin);
84
85
 
86
+ import updatableComponent from "./components/mixins/updatableComponent";
87
+ Vue.mixin(updatableComponent);
88
+
85
89
  Vue.config.globalProperties.extension = {};
86
90
  import extension from "./components/mixins/extension.js";
87
91
  Vue.mixin(extension);
@@ -117,9 +121,6 @@ window.GLib = Framework;
117
121
  import driverCustomBehavior from "./plugins/driverCustomBehavior";
118
122
  Vue.use(driverCustomBehavior);
119
123
 
120
- import updatableComponent from "./plugins/updatableComponent";
121
- Vue.use(updatableComponent);
122
-
123
124
  import 'flag-icons/css/flag-icons.min.css';
124
125
  import 'v-phone-input/dist/v-phone-input.css';
125
126
  import { createVPhoneInput } from 'v-phone-input';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glib-web",
3
- "version": "4.8.3",
3
+ "version": "4.9.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <component :is="componentName" :href="$href()" class="thumbnail" :class="cssClasses" @[clickCondition]="$onClick()">
3
- <panels-responsive :spec="spec.header" />
3
+ <panels-responsive v-if="spec.header" :spec="spec.header" />
4
4
  <div style="display:flex;">
5
5
  <!-- <div v-if="spec.leftOuterButtons" style="display:flex; margin-top:10px;">
6
6
  <template v-for="(item, index) in spec.leftOuterButtons" :key="index">
@@ -70,9 +70,9 @@
70
70
  </template>
71
71
 
72
72
  </v-list-item>
73
- <panels-responsive :spec="spec.right" />
73
+ <panels-responsive v-if="spec.right" :spec="spec.right" />
74
74
  </div>
75
- <panels-responsive :spec="spec.footer" />
75
+ <panels-responsive v-if="spec.footer" :spec="spec.footer" />
76
76
  </component>
77
77
  </template>
78
78
 
package/utils/hash.js CHANGED
@@ -1,3 +1,4 @@
1
+ import merge from 'lodash.merge';
1
2
  export default class Hash {
2
3
  constructor(object) {
3
4
  for (const key in object) {
@@ -51,4 +52,8 @@ export default class Hash {
51
52
  handler(key, this[key]);
52
53
  }
53
54
  }
55
+
56
+ deepMerge(hash) {
57
+ return merge(this, hash)
58
+ }
54
59
  }
package/utils/http.js CHANGED
@@ -2,6 +2,7 @@ import Type from "./type";
2
2
  import Action from "../action";
3
3
  import { nextTick } from 'vue';
4
4
  import { ctx, dialogs, jsonView, vueApp } from "../store";
5
+ import Hash from "./hash";
5
6
 
6
7
  let loading = false;
7
8
 
@@ -59,9 +60,21 @@ export default class {
59
60
  }
60
61
  }
61
62
 
63
+ // Merge params in `url` and `formData` that have the same name.
64
+ static mergeDuplicateParamsIntoUrl(url, properties) {
65
+ const baseUrl = `${url.protocol}//${url.host}${url.pathname}`;
66
+ const hash = new Hash()
67
+ url.searchParams.forEach((value, name) => {
68
+ hash[name] = value;
69
+ })
70
+
71
+ properties.url = Utils.url.appendParams(baseUrl, hash.deepMerge(properties.formData || {}))
72
+ properties.formData = {}
73
+ }
74
+
62
75
  // Set `forcePushHistory` when it is important to clear the forward history.
63
76
  static load(properties, component, windowMode, forcePushHistory) {
64
- const urlString = properties["url"];
77
+ const urlString = properties.url;
65
78
  let url;
66
79
  try {
67
80
  url = new URL(urlString);
@@ -70,6 +83,7 @@ export default class {
70
83
  throw e;
71
84
  }
72
85
  const domainMatched = window.location.hostname == url.hostname;
86
+ this.mergeDuplicateParamsIntoUrl(url, properties)
73
87
 
74
88
  // If this is an external domain, we rely on `onUnload()` instead.
75
89
  // if (domainMatched && !this.proceedEvenWhenDirty()) {
@@ -78,12 +92,12 @@ export default class {
78
92
 
79
93
  if (Utils.settings.reactive && domainMatched) {
80
94
  const currentUrl = window.location.href;
81
- const htmlUrl = Utils.url.htmlUrl(properties["url"]);
95
+ const htmlUrl = Utils.url.htmlUrl(properties.url);
82
96
  const sameUrl = htmlUrl === currentUrl;
83
97
  const topOfDialog = Utils.type.isObject(dialogs.value.last());
84
98
  const windowOrDialog = windowMode ? true : !topOfDialog;
85
99
 
86
- Utils.http.execute(properties, "GET", component, (data, response) => {
100
+ this.execute(properties, "GET", component, (data, response) => {
87
101
  // TODO: Check if it is okay to remove this `if` statement so we always push even if it's the same URL.
88
102
  if (forcePushHistory || (windowOrDialog && !sameUrl && !properties.updateExisting)) {
89
103
  const redirectUrl = Utils.url.htmlUrl(response.url);
@@ -1,38 +0,0 @@
1
- import { vueApp } from "../../store";
2
-
3
- export default class {
4
- execute(properties, component, params) {
5
- Utils.type.ifString(properties.topic, topicName => {
6
- const ws = vueApp.webSocket;
7
- const channel = ws.channels[topicName];
8
-
9
- Utils.type.ifString(properties.event, eventName => {
10
- if (channel) {
11
- const payload = Object.assign({}, properties.payload, ws.header, {
12
- formData: properties.formData
13
- });
14
- console.debug(`Pushing to '${topicName}/${eventName}'`, payload);
15
- channel
16
- .push(eventName, payload)
17
- .receive("ok", resp => {
18
- console.debug(
19
- `Push to '${topicName}/${eventName}' succeeded`,
20
- resp
21
- );
22
- Utils.ws.handleResponse(resp.onResponse, component);
23
- })
24
- .receive("error", resp => {
25
- console.debug(`Push to '${topicName}/${eventName}' failed`, resp);
26
- Utils.ws.handleResponse(resp.onResponse, component);
27
- });
28
- } else {
29
- console.error(`Topic not joined: '${topicName}'`);
30
- Utils.launch.snackbar.error(
31
- "Something went wrong and we have been notified",
32
- component
33
- );
34
- }
35
- });
36
- });
37
- }
38
- }
@@ -1,73 +0,0 @@
1
- <template>
2
- <v-menu v-if="spec.childButtons" left bottom>
3
- <template v-slot:activator="{ on: onMenu }">
4
- <v-tooltip :disabled="tooltip.disabled" :top="tooltipPositionMatches('top')"
5
- :right="tooltipPositionMatches('right')" :bottom="tooltipPositionMatches('bottom')"
6
- :left="tooltipPositionMatches('left')">
7
- <template v-slot:activator="{ on: onTooltip }">
8
- <slot name="activator" :on="{ ...onMenu, ...onTooltip }" />
9
- </template>
10
- <span> {{ tooltip.text }} </span>
11
- </v-tooltip>
12
- </template>
13
-
14
- <v-list>
15
- <v-list-item v-for="(childItem, childIndex) in spec.childButtons" :key="childIndex">
16
- <common-button :spec="buttonSpec(childItem)" :disabled="childItem.disabled" />
17
- </v-list-item>
18
- </v-list>
19
- </v-menu>
20
- <v-tooltip v-else :disabled="tooltip.disabled" :top="tooltipPositionMatches('top')"
21
- :right="tooltipPositionMatches('right')" :bottom="tooltipPositionMatches('bottom')"
22
- :left="tooltipPositionMatches('left')">
23
- <template v-slot:activator="{ on: onTooltip }">
24
- <slot name="activator" :on="{ ...onTooltip }" />
25
- </template>
26
- <span> {{ tooltip.text }} </span>
27
- </v-tooltip>
28
- </template>
29
-
30
- <script>
31
- export default {
32
- props: {
33
- spec: { type: Object, required: true }
34
- },
35
- data() {
36
- return {
37
- tooltip: {}
38
- // childSpec: Object.assign({}, this.spec, { id: undefined })
39
- };
40
- },
41
- methods: {
42
- $ready() {
43
- this.updateData();
44
- // this.tooltip = this.spec.tooltip || { disabled: true };
45
- // this.childSpec = Object.assign({}, this.spec, { id: undefined });
46
- },
47
- // $initAccessories() {
48
- // this.tooltip = this.spec.tooltip || { disabled: true };
49
- // },
50
- tooltipPositionMatches(position) {
51
- if (this.spec.tooltip && this.spec.tooltip.position) {
52
- return position == this.spec.tooltip.position;
53
- } else {
54
- return position == "bottom";
55
- }
56
- },
57
- buttonSpec(item) {
58
- return Object.assign({}, item, {
59
- view: "button-v1",
60
- styleClasses: ["text"]
61
- });
62
- },
63
- $registryEnabled() {
64
- return false;
65
- },
66
- updateData() {
67
- this.tooltip = this.spec.tooltip || { disabled: true };
68
- }
69
- }
70
- };
71
- </script>
72
-
73
- <style lang="scss" scoped></style>
@@ -1,62 +0,0 @@
1
- <template>
2
- <line-chart :style="$styles()" :class="$classes()" :data="series"></line-chart>
3
- </template>
4
-
5
- <script>
6
- // import annotation from "../mixins/chart/annotation.js";
7
- // import tooltip from "../mixins/chart/tooltip.js";
8
-
9
- export default {
10
- // mixins: [annotation],
11
- props: {
12
- spec: { type: Object, required: true }
13
- },
14
- data: function () {
15
- return {
16
- series: [],
17
- dataName: "dataSeries"
18
- };
19
- },
20
- methods: {
21
- $ready() {
22
- this.series.clear();
23
- this.renderData(this.spec.dataSeries);
24
- this.fetchNext(this.spec.nextPage);
25
- },
26
- fetchData(url) {
27
- const vm = this;
28
- Utils.type.ifString(url, val => {
29
- Utils.http.execute({ url: val }, "GET", this, response => {
30
- vm.renderData(response.dataSeries);
31
- vm.fetchNext(response.nextPage);
32
- });
33
- });
34
- },
35
- fetchNext(nextPage) {
36
- if (!Utils.type.isObject(nextPage)) {
37
- return;
38
- }
39
- if (nextPage.autoload != "all") {
40
- return;
41
- }
42
- this.fetchData(nextPage.url);
43
- },
44
- renderData(series) {
45
- if (!Utils.type.isArray(series)) {
46
- return;
47
- }
48
-
49
- const data = series.map(element => {
50
- const dataPoints = {};
51
- element.points.forEach(point => {
52
- dataPoints[point.x] = point.y;
53
- });
54
- return { name: element.title, data: dataPoints };
55
- });
56
- this.series = this.series.concat(data);
57
- }
58
- }
59
- };
60
- </script>
61
-
62
- <style scoped></style>
@@ -1,20 +0,0 @@
1
- export default {
2
- methods: {
3
- textStyles(spec) {
4
- const styles = this.$styles(spec);
5
-
6
- switch (this.spec.textAlign) {
7
- case "center":
8
- styles["text-align"] = "center";
9
- break;
10
- case "right":
11
- styles["text-align"] = "right";
12
- break;
13
- default:
14
- styles["text-align"] = "left";
15
- }
16
-
17
- return styles;
18
- }
19
- }
20
- };
@@ -1,49 +0,0 @@
1
- import { createConsumer } from "@rails/actioncable";
2
- import { vueApp } from "../../../store";
3
-
4
- const consumer = createConsumer();
5
-
6
- export default {
7
- data: function () {
8
- return {
9
- _wsSocket: null
10
- };
11
- },
12
- methods: {
13
- $wsInitActionCable(spec) {
14
- const component = this;
15
- Utils.type.ifObject(
16
- spec,
17
- ws => {
18
- const channelName = ws.channel;
19
- const subscription = Object.assign({}, ws.params, {
20
- channel: channelName
21
- });
22
-
23
- console.debug("Connecting to channel", subscription);
24
-
25
- consumer.subscriptions.create(subscription, {
26
- connected() {
27
- const ws = vueApp.actionCable;
28
- ws.channels[channelName] = this;
29
- console.debug("Connected to channel", channelName);
30
- },
31
-
32
- disconnected() { },
33
-
34
- received(data) {
35
- const action = data.action;
36
- const payload = { ...action, filterKey: ws.filterKey };
37
- if (ws.filterKey === data.filterKey) {
38
- GLib.action.execute(payload, component);
39
- }
40
- }
41
- });
42
- },
43
- () => {
44
- // this._wsDisconnectSocket();
45
- }
46
- );
47
- }
48
- }
49
- };
@@ -1,23 +0,0 @@
1
- <template>
2
- <common-responsive ref="delegate" :spec="spec" />
3
- </template>
4
-
5
- <script>
6
- export default {
7
- props: {
8
- spec: {
9
- type: Object,
10
- required: true,
11
- default: function () {
12
- return {};
13
- },
14
- },
15
- },
16
- };
17
- </script>
18
-
19
- <style scoped>
20
- .hover {
21
- width: 8rem;
22
- }
23
- </style>
@@ -1,58 +0,0 @@
1
- export default {
2
- install: (Vue, options) => {
3
- Vue.mixin({
4
- computed: {
5
- viewId() {
6
- if (this.spec && this.spec.id) {
7
- const id = this.spec.id;
8
-
9
- if (id.includes('{{entry_index}}')) {
10
- const dynamicGroupEntry = this.$closest("fields/internalDynamicGroupEntry");
11
- if (dynamicGroupEntry) {
12
- return dynamicGroupEntry.$populateIndexes(id);
13
- }
14
- }
15
-
16
- return id;
17
- }
18
- }
19
- },
20
- methods: {
21
- $ready() {
22
- let spec = this.spec;
23
- if (spec && spec.id && this.$registryEnabled()) {
24
-
25
- const id = this.viewId;
26
-
27
- const existingComponent = GLib.component.findById(id);
28
- // A component with the same ID in a different page shouldn't be considered a
29
- // duplicate. See `utils/components#deregister` for more details.
30
- if (existingComponent) {
31
- console.warn(
32
- "Duplicate component ID:",
33
- id,
34
- "Existing:",
35
- GLib.component.vueName(existingComponent),
36
- "New:",
37
- GLib.component.vueName(this)
38
- );
39
- }
40
- const newComponent = this;
41
- GLib.component.register(id, newComponent);
42
- }
43
- },
44
- $tearDown() {
45
- let spec = this.spec;
46
-
47
- if (spec && spec.id && this.$registryEnabled()) {
48
- GLib.component.deregister(spec.id, this);
49
- }
50
- },
51
- $registryEnabled() {
52
- // Common classes such as `_select` need to return false so that it doesn't override its parent (e.g. `select`).
53
- return false;
54
- }
55
- }
56
- });
57
- }
58
- };