glib-web 4.8.0 → 4.8.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/actions/logics/set.js +5 -1
- package/app.scss +172 -0
- package/components/component.vue +4 -0
- package/components/composable/parser.js +41 -0
- package/components/fields/multiUpload.vue +11 -184
- package/components/fields/radio/_featured.vue +9 -10
- package/components/fields/radio.vue +7 -12
- package/components/mixins/table/import.js +45 -9
- package/components/panels/bulkEdit.vue +300 -0
- package/components/panels/bulkEdit2.vue +197 -0
- package/index.js +2 -0
- package/package.json +1 -1
- package/plugins/updatableComponent.js +11 -12
- package/plugins/vuetify.js +4 -3
- package/utils/type.js +3 -0
package/actions/logics/set.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import jsonLogic from 'json-logic-js';
|
|
2
2
|
import { fieldModels } from "../../components/composable/conditional";
|
|
3
3
|
import merge from 'lodash.merge';
|
|
4
|
+
import { nextTick } from "vue";
|
|
4
5
|
|
|
5
6
|
const subscript = function (a, b) {
|
|
6
7
|
if (a) {
|
|
@@ -78,6 +79,9 @@ export default class {
|
|
|
78
79
|
});
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
|
|
82
|
+
// Make sure the update performed by the above `action_merge()` has been reflected.
|
|
83
|
+
nextTick(() => {
|
|
84
|
+
GLib.action.execute(spec.onSet, component);
|
|
85
|
+
})
|
|
82
86
|
}
|
|
83
87
|
}
|
package/app.scss
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
.border-\[2px\] {
|
|
2
|
+
border-width: 2px;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.border-\[4px\] {
|
|
6
|
+
border-width: 4px;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.\!font-semibold {
|
|
10
|
+
font-weight: 600px;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.opacity-50 {
|
|
14
|
+
opacity: 0.5;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.guploaded-file {
|
|
18
|
+
/* pt-6 w-full */
|
|
19
|
+
padding-top: 24px;
|
|
20
|
+
width: 100%;
|
|
21
|
+
|
|
22
|
+
.title {
|
|
23
|
+
color: #6b7280;
|
|
24
|
+
font-weight: 600;
|
|
25
|
+
margin-bottom: 8px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.file-container {
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
flex-direction: column;
|
|
32
|
+
border-radius: 6px;
|
|
33
|
+
background-color: #f3f4f6;
|
|
34
|
+
width: 100%;
|
|
35
|
+
padding: 16px;
|
|
36
|
+
margin-bottom: 16px;
|
|
37
|
+
min-height: 72px;
|
|
38
|
+
gap: 8px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.file {
|
|
42
|
+
display: flex;
|
|
43
|
+
width: 100%;
|
|
44
|
+
position: relative;
|
|
45
|
+
|
|
46
|
+
.status {
|
|
47
|
+
display: flex;
|
|
48
|
+
width: 100%;
|
|
49
|
+
gap: 8px;
|
|
50
|
+
align-items: center;
|
|
51
|
+
|
|
52
|
+
.image-icon {
|
|
53
|
+
width: 40px;
|
|
54
|
+
height: 40px;
|
|
55
|
+
background-color: #fff;
|
|
56
|
+
border-radius: 5px;
|
|
57
|
+
display: flex;
|
|
58
|
+
justify-content: center;
|
|
59
|
+
align-items: center;
|
|
60
|
+
|
|
61
|
+
img {
|
|
62
|
+
width: 20px;
|
|
63
|
+
height: 20px;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.icon {
|
|
68
|
+
margin-right: 8px;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.progress {
|
|
72
|
+
display: flex;
|
|
73
|
+
flex-grow: 1;
|
|
74
|
+
flex-direction: column;
|
|
75
|
+
justify-content: space-between;
|
|
76
|
+
|
|
77
|
+
.label {
|
|
78
|
+
/* mb-1 */
|
|
79
|
+
display: flex;
|
|
80
|
+
flex-direction: column;
|
|
81
|
+
margin-bottom: 4px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.label a {
|
|
85
|
+
color: #47495F;
|
|
86
|
+
font-weight: 600;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.label a:hover {
|
|
90
|
+
text-decoration: underline;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.label a[href=""],
|
|
94
|
+
.label a[href="#"],
|
|
95
|
+
.label a:not([href]) {
|
|
96
|
+
text-decoration: none !important;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.close-btn {
|
|
103
|
+
cursor: pointer;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.background {
|
|
108
|
+
width: 100%;
|
|
109
|
+
background-color: #e6e6e6;
|
|
110
|
+
border-radius: 9999px;
|
|
111
|
+
height: 8px;
|
|
112
|
+
display: flex;
|
|
113
|
+
|
|
114
|
+
.value {
|
|
115
|
+
background-color: #0A2A9E;
|
|
116
|
+
height: inherit;
|
|
117
|
+
border-radius: inherit;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.percentage-wrapper {
|
|
122
|
+
width: 100%;
|
|
123
|
+
display: flex;
|
|
124
|
+
align-items: center;
|
|
125
|
+
justify-content: center;
|
|
126
|
+
gap: 8px;
|
|
127
|
+
|
|
128
|
+
.percentage {
|
|
129
|
+
font-size: 14px;
|
|
130
|
+
font-weight: 600;
|
|
131
|
+
margin-bottom: 2px;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.gdrop-file {
|
|
137
|
+
/* px-6 py-10 border-[2px] border-dashed border-gray-400 w-full rounded-2xl cursor-pointer */
|
|
138
|
+
padding: 40px 24px;
|
|
139
|
+
border-style: dashed;
|
|
140
|
+
border-color: #9ca3af;
|
|
141
|
+
width: 100%;
|
|
142
|
+
border-radius: 16px;
|
|
143
|
+
cursor: pointer;
|
|
144
|
+
|
|
145
|
+
.cloud {
|
|
146
|
+
/* w-full flex flex-col justify-center items-center */
|
|
147
|
+
width: 100%;
|
|
148
|
+
display: flex;
|
|
149
|
+
flex-direction: column;
|
|
150
|
+
justify-content: center;
|
|
151
|
+
align-items: center;
|
|
152
|
+
|
|
153
|
+
.icon {
|
|
154
|
+
/* mb-3 text-gray-400 */
|
|
155
|
+
margin-bottom: 12px;
|
|
156
|
+
color: #9ca3af;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.title {
|
|
160
|
+
/* text-center mb-2 */
|
|
161
|
+
text-align: center;
|
|
162
|
+
margin-bottom: 8px;
|
|
163
|
+
color: #47495F;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.subtitle {
|
|
167
|
+
/* text-color-light text-center */
|
|
168
|
+
color: #A7ADB5;
|
|
169
|
+
text-align: center;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
package/components/component.vue
CHANGED
|
@@ -103,6 +103,8 @@ import FormPanel from "./panels/form.vue";
|
|
|
103
103
|
import ListPanel from "./panels/list.vue";
|
|
104
104
|
import CarouselPanel from "./panels/carousel.vue";
|
|
105
105
|
import TablePanel from "./panels/table.vue";
|
|
106
|
+
import BulkEditPanel from "./panels/bulkEdit.vue";
|
|
107
|
+
import BulkEditPanel2 from "./panels/bulkEdit2.vue";
|
|
106
108
|
import CustomPanel from "./panels/custom.vue";
|
|
107
109
|
import ColumnPanel from "./panels/column.vue";
|
|
108
110
|
import ResponsivePanel from "./panels/responsive.vue";
|
|
@@ -198,6 +200,8 @@ export default {
|
|
|
198
200
|
"panels-list": ListPanel,
|
|
199
201
|
"panels-carousel": CarouselPanel,
|
|
200
202
|
"panels-table": TablePanel,
|
|
203
|
+
"panels-bulkEdit": BulkEditPanel,
|
|
204
|
+
"panels-bulkEdit2": BulkEditPanel2,
|
|
201
205
|
"panels-custom": CustomPanel,
|
|
202
206
|
"panels-responsive": ResponsivePanel,
|
|
203
207
|
"panels-column": ColumnPanel,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export function parseCsv(csvString) {
|
|
2
|
+
// https://stackoverflow.com/a/41563966/9970813
|
|
3
|
+
let prevLetter = "",
|
|
4
|
+
row = [""],
|
|
5
|
+
result = [row],
|
|
6
|
+
columnIndex = 0,
|
|
7
|
+
rowIndex = 0,
|
|
8
|
+
canSplit = true,
|
|
9
|
+
letter;
|
|
10
|
+
for (let i = 0; i <= csvString.length; ++i) {
|
|
11
|
+
letter = csvString[i];
|
|
12
|
+
|
|
13
|
+
if ('"' === letter) {
|
|
14
|
+
if (canSplit && letter === prevLetter) {
|
|
15
|
+
row[columnIndex] += letter;
|
|
16
|
+
}
|
|
17
|
+
canSplit = !canSplit;
|
|
18
|
+
} else if ("," === letter && canSplit) {
|
|
19
|
+
letter = row[++columnIndex] = "";
|
|
20
|
+
} else if ("\n" === letter && canSplit) {
|
|
21
|
+
if ("\r" === prevLetter) {
|
|
22
|
+
row[columnIndex] = row[columnIndex].slice(0, -1);
|
|
23
|
+
}
|
|
24
|
+
row = result[++rowIndex] = [(letter = "")];
|
|
25
|
+
columnIndex = 0;
|
|
26
|
+
} else if (Utils.type.isString(letter)) {
|
|
27
|
+
row[columnIndex] += letter;
|
|
28
|
+
}
|
|
29
|
+
prevLetter = letter;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Remove any blank rows
|
|
33
|
+
result = result.filter(r => {
|
|
34
|
+
return r[0] !== "";
|
|
35
|
+
});
|
|
36
|
+
// result = result.filter(r => {
|
|
37
|
+
// return r[0] !== "undefined"
|
|
38
|
+
// });
|
|
39
|
+
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
@@ -38,12 +38,14 @@
|
|
|
38
38
|
</div>
|
|
39
39
|
</div>
|
|
40
40
|
</div>
|
|
41
|
-
<
|
|
42
|
-
<div class="
|
|
43
|
-
<div class="
|
|
41
|
+
<Transition name="slide-fade">
|
|
42
|
+
<div class="percentage-wrapper " v-show="file[1].progress.value > 0 && !file[1].message">
|
|
43
|
+
<div class="background">
|
|
44
|
+
<div class="value" :style="{ width: `${file[1].progress.value}%` }"></div>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="percentage">{{ parseInt(file[1].progress.value) }}%</div>
|
|
44
47
|
</div>
|
|
45
|
-
|
|
46
|
-
</div>
|
|
48
|
+
</Transition>
|
|
47
49
|
</div>
|
|
48
50
|
</template>
|
|
49
51
|
</div>
|
|
@@ -51,181 +53,6 @@
|
|
|
51
53
|
</div>
|
|
52
54
|
</template>
|
|
53
55
|
|
|
54
|
-
<style>
|
|
55
|
-
.border-\[2px\] {
|
|
56
|
-
border-width: 2px;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
.border-\[4px\] {
|
|
60
|
-
border-width: 4px;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
.\!font-semibold {
|
|
64
|
-
font-weight: 600px;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
.opacity-50 {
|
|
68
|
-
opacity: 0.5;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
.guploaded-file {
|
|
72
|
-
/* pt-6 w-full */
|
|
73
|
-
padding-top: 24px;
|
|
74
|
-
width: 100%;
|
|
75
|
-
|
|
76
|
-
.title {
|
|
77
|
-
color: #6b7280;
|
|
78
|
-
font-weight: 600;
|
|
79
|
-
margin-bottom: 8px;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
.file-container {
|
|
83
|
-
display: flex;
|
|
84
|
-
align-items: center;
|
|
85
|
-
flex-direction: column;
|
|
86
|
-
border-radius: 6px;
|
|
87
|
-
background-color: #f3f4f6;
|
|
88
|
-
width: 100%;
|
|
89
|
-
padding: 16px;
|
|
90
|
-
margin-bottom: 16px;
|
|
91
|
-
min-height: 72px;
|
|
92
|
-
gap: 8px;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
.file {
|
|
96
|
-
display: flex;
|
|
97
|
-
width: 100%;
|
|
98
|
-
position: relative;
|
|
99
|
-
|
|
100
|
-
.status {
|
|
101
|
-
display: flex;
|
|
102
|
-
width: 100%;
|
|
103
|
-
gap: 8px;
|
|
104
|
-
align-items: center;
|
|
105
|
-
|
|
106
|
-
.image-icon {
|
|
107
|
-
width: 40px;
|
|
108
|
-
height: 40px;
|
|
109
|
-
background-color: #fff;
|
|
110
|
-
border-radius: 5px;
|
|
111
|
-
display: flex;
|
|
112
|
-
justify-content: center;
|
|
113
|
-
align-items: center;
|
|
114
|
-
|
|
115
|
-
img {
|
|
116
|
-
width: 20px;
|
|
117
|
-
height: 20px;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
.icon {
|
|
122
|
-
margin-right: 8px;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
.progress {
|
|
126
|
-
display: flex;
|
|
127
|
-
flex-grow: 1;
|
|
128
|
-
flex-direction: column;
|
|
129
|
-
justify-content: space-between;
|
|
130
|
-
|
|
131
|
-
.label {
|
|
132
|
-
/* mb-1 */
|
|
133
|
-
display: flex;
|
|
134
|
-
flex-direction: column;
|
|
135
|
-
margin-bottom: 4px;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
.label a {
|
|
139
|
-
color: #47495F;
|
|
140
|
-
font-weight: 600;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
.label a:hover {
|
|
144
|
-
text-decoration: underline;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
.label a[href=""],
|
|
148
|
-
.label a[href="#"],
|
|
149
|
-
.label a:not([href]) {
|
|
150
|
-
text-decoration: none !important;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
.close-btn {
|
|
157
|
-
cursor: pointer;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
.background {
|
|
162
|
-
width: 100%;
|
|
163
|
-
background-color: #e6e6e6;
|
|
164
|
-
border-radius: 9999px;
|
|
165
|
-
height: 8px;
|
|
166
|
-
display: flex;
|
|
167
|
-
|
|
168
|
-
.value {
|
|
169
|
-
background-color: #0A2A9E;
|
|
170
|
-
height: inherit;
|
|
171
|
-
border-radius: inherit;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
.percentage-wrapper {
|
|
176
|
-
width: 100%;
|
|
177
|
-
display: flex;
|
|
178
|
-
align-items: center;
|
|
179
|
-
justify-content: center;
|
|
180
|
-
gap: 8px;
|
|
181
|
-
|
|
182
|
-
.percentage {
|
|
183
|
-
font-size: 14px;
|
|
184
|
-
font-weight: 600;
|
|
185
|
-
margin-bottom: 2px;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
.gdrop-file {
|
|
191
|
-
/* px-6 py-10 border-[2px] border-dashed border-gray-400 w-full rounded-2xl cursor-pointer */
|
|
192
|
-
padding: 40px 24px;
|
|
193
|
-
border-style: dashed;
|
|
194
|
-
border-color: #9ca3af;
|
|
195
|
-
width: 100%;
|
|
196
|
-
border-radius: 16px;
|
|
197
|
-
cursor: pointer;
|
|
198
|
-
|
|
199
|
-
.cloud {
|
|
200
|
-
/* w-full flex flex-col justify-center items-center */
|
|
201
|
-
width: 100%;
|
|
202
|
-
display: flex;
|
|
203
|
-
flex-direction: column;
|
|
204
|
-
justify-content: center;
|
|
205
|
-
align-items: center;
|
|
206
|
-
|
|
207
|
-
.icon {
|
|
208
|
-
/* mb-3 text-gray-400 */
|
|
209
|
-
margin-bottom: 12px;
|
|
210
|
-
color: #9ca3af;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
.title {
|
|
214
|
-
/* text-center mb-2 */
|
|
215
|
-
text-align: center;
|
|
216
|
-
margin-bottom: 8px;
|
|
217
|
-
color: #47495F;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
.subtitle {
|
|
221
|
-
/* text-color-light text-center */
|
|
222
|
-
color: #A7ADB5;
|
|
223
|
-
text-align: center;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
</style>
|
|
228
|
-
|
|
229
56
|
<script>
|
|
230
57
|
import { ref, computed, defineComponent, watch, getCurrentInstance } from 'vue';
|
|
231
58
|
import { VIcon } from 'vuetify/components';
|
|
@@ -234,9 +61,9 @@ import * as delegateUploader from "../composable/upload_delegator";
|
|
|
234
61
|
import { triggerOnChange } from "../composable/form";
|
|
235
62
|
import { nextTick } from "vue";
|
|
236
63
|
import { useFilesState, useFileUtils } from "../composable/file";
|
|
237
|
-
import doc from "./selectAsset/doc-1.png"
|
|
238
|
-
import pic from "./selectAsset/pic-1.png"
|
|
239
|
-
import pdf from "./selectAsset/pdf-1.png"
|
|
64
|
+
import doc from "./selectAsset/doc-1.png";
|
|
65
|
+
import pic from "./selectAsset/pic-1.png";
|
|
66
|
+
import pdf from "./selectAsset/pdf-1.png";
|
|
240
67
|
const { makeKey, Item, signedIds } = useFileUtils();
|
|
241
68
|
|
|
242
69
|
export default defineComponent({
|
|
@@ -375,7 +202,7 @@ export default defineComponent({
|
|
|
375
202
|
props,
|
|
376
203
|
doc,
|
|
377
204
|
pic,
|
|
378
|
-
pdf
|
|
205
|
+
pdf
|
|
379
206
|
};
|
|
380
207
|
}
|
|
381
208
|
})
|
|
@@ -29,13 +29,12 @@ export default {
|
|
|
29
29
|
align-items: center;
|
|
30
30
|
justify-content: center;
|
|
31
31
|
width: 240px;
|
|
32
|
-
height:
|
|
32
|
+
height: 100%;
|
|
33
33
|
transition: border-color 0.3s, box-shadow 0.3s, color 0.3s;
|
|
34
34
|
text-align: center;
|
|
35
35
|
cursor: pointer;
|
|
36
|
-
padding: 16px;
|
|
37
36
|
position: relative;
|
|
38
|
-
border:
|
|
37
|
+
border: 2px solid #E6E6E6;
|
|
39
38
|
border-radius: 24px;
|
|
40
39
|
}
|
|
41
40
|
|
|
@@ -56,18 +55,15 @@ export default {
|
|
|
56
55
|
align-items: center;
|
|
57
56
|
justify-content: center;
|
|
58
57
|
width: 100%;
|
|
59
|
-
|
|
60
|
-
margin-right: 30px;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
.custom-radio .custom-radio-icon {
|
|
64
|
-
margin-bottom: 8px;
|
|
58
|
+
padding: 40px;
|
|
65
59
|
}
|
|
66
60
|
|
|
67
61
|
.custom-radio .custom-radio-label {
|
|
68
62
|
font-size: 22px;
|
|
69
63
|
color: inherit;
|
|
70
|
-
margin-top:
|
|
64
|
+
margin-top: 24px;
|
|
65
|
+
word-break: break-word;
|
|
66
|
+
min-width: 180px;
|
|
71
67
|
}
|
|
72
68
|
|
|
73
69
|
.custom-radio ::v-deep .v-selection-control__input {
|
|
@@ -90,6 +86,9 @@ export default {
|
|
|
90
86
|
display: none;
|
|
91
87
|
}
|
|
92
88
|
|
|
89
|
+
.custom-radio ::v-deep .v-selection-control__wrapper {
|
|
90
|
+
display: none;
|
|
91
|
+
}
|
|
93
92
|
|
|
94
93
|
.custom-radio .v-ripple__container {
|
|
95
94
|
display: none;
|
|
@@ -38,13 +38,12 @@ export default {
|
|
|
38
38
|
align-items: center;
|
|
39
39
|
justify-content: center;
|
|
40
40
|
width: 240px;
|
|
41
|
-
height: 254px;
|
|
41
|
+
min-height: 254px;
|
|
42
42
|
transition: border-color 0.3s, box-shadow 0.3s, color 0.3s;
|
|
43
43
|
text-align: center;
|
|
44
44
|
cursor: pointer;
|
|
45
|
-
padding: 16px;
|
|
46
45
|
position: relative;
|
|
47
|
-
border:
|
|
46
|
+
border: 2px solid #E6E6E6;
|
|
48
47
|
border-radius: 24px;
|
|
49
48
|
}
|
|
50
49
|
|
|
@@ -65,20 +64,13 @@ export default {
|
|
|
65
64
|
align-items: center;
|
|
66
65
|
justify-content: center;
|
|
67
66
|
width: 100%;
|
|
68
|
-
|
|
69
|
-
margin-right: 30px;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
.custom-radio .custom-radio-icon {
|
|
73
|
-
width: 80px;
|
|
74
|
-
height: 80px;
|
|
75
|
-
margin-bottom: 8px;
|
|
67
|
+
padding: 40px;
|
|
76
68
|
}
|
|
77
69
|
|
|
78
70
|
.custom-radio .custom-radio-label {
|
|
79
71
|
font-size: 22px;
|
|
80
72
|
color: inherit;
|
|
81
|
-
margin-top:
|
|
73
|
+
margin-top: 24px;
|
|
82
74
|
}
|
|
83
75
|
|
|
84
76
|
.custom-radio ::v-deep .v-selection-control__input {
|
|
@@ -101,6 +93,9 @@ export default {
|
|
|
101
93
|
display: none;
|
|
102
94
|
}
|
|
103
95
|
|
|
96
|
+
.custom-radio ::v-deep .v-selection-control__wrapper {
|
|
97
|
+
display: none;
|
|
98
|
+
}
|
|
104
99
|
|
|
105
100
|
.custom-radio .v-ripple__container {
|
|
106
101
|
display: none;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { fieldModels } from "../../composable/conditional";
|
|
2
|
+
|
|
1
3
|
export default {
|
|
2
4
|
data: function() {
|
|
3
5
|
return {
|
|
@@ -15,34 +17,61 @@ export default {
|
|
|
15
17
|
vm.importParamName = obj.paramName;
|
|
16
18
|
});
|
|
17
19
|
},
|
|
20
|
+
rowSelected(sectionIndex, rowIndex) {
|
|
21
|
+
return fieldModels[this.rowCheckId(sectionIndex, rowIndex)];
|
|
22
|
+
},
|
|
23
|
+
selectedRowCount(section) {
|
|
24
|
+
const sectionIndex = section.index;
|
|
25
|
+
let count = 0;
|
|
26
|
+
section.dataRows.forEach((_row, rowIndex) => {
|
|
27
|
+
if (this.rowSelected(sectionIndex, rowIndex)) {
|
|
28
|
+
count++;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
return count;
|
|
32
|
+
},
|
|
18
33
|
submitRows(event, section) {
|
|
19
|
-
const vm = this;
|
|
20
34
|
const keys = section.header.dataCells;
|
|
21
35
|
const rows = [];
|
|
22
|
-
section.
|
|
36
|
+
const sectionIndex = section.index;
|
|
37
|
+
let count = 0;
|
|
38
|
+
section.dataRows.forEach((row, rowIndex) => {
|
|
39
|
+
if (!this.rowSelected(sectionIndex, rowIndex)) { // Don't submit
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
count++;
|
|
23
44
|
const cells = {};
|
|
24
|
-
row.forEach((cell,
|
|
25
|
-
const key = `${
|
|
45
|
+
row.forEach((cell, cellIndex) => {
|
|
46
|
+
const key = `${this.importParamName}[${keys[cellIndex]}]`;
|
|
26
47
|
cells[key] = cell;
|
|
27
48
|
});
|
|
49
|
+
cells['_index'] = rowIndex;
|
|
28
50
|
rows.push(cells);
|
|
29
51
|
});
|
|
30
52
|
|
|
31
|
-
if (
|
|
32
|
-
|
|
53
|
+
if (count > 0) {
|
|
54
|
+
this._submitEachRow(rows);
|
|
55
|
+
} else {
|
|
56
|
+
Utils.launch.dialog.alert("Please select at least one row.", this);
|
|
33
57
|
}
|
|
34
58
|
},
|
|
35
|
-
_submitEachRow(rows
|
|
59
|
+
_submitEachRow(rows) {
|
|
60
|
+
const url = Utils.type.string(this.importSubmitUrl)
|
|
61
|
+
if (!url) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
36
65
|
const vm = this;
|
|
37
66
|
const row = rows.shift();
|
|
38
67
|
if (row) {
|
|
39
68
|
const data = {
|
|
40
|
-
url:
|
|
69
|
+
url: url,
|
|
41
70
|
formData: row
|
|
42
71
|
};
|
|
43
72
|
Utils.http.execute(data, "POST", vm, response => {
|
|
44
73
|
GLib.action.handleResponse(response, vm);
|
|
45
|
-
vm._submitEachRow(rows
|
|
74
|
+
vm._submitEachRow(rows);
|
|
46
75
|
});
|
|
47
76
|
}
|
|
48
77
|
},
|
|
@@ -55,6 +84,13 @@ export default {
|
|
|
55
84
|
return cell.trim();
|
|
56
85
|
});
|
|
57
86
|
section.dataRows = rows;
|
|
87
|
+
|
|
88
|
+
if (rows.length > 0) {
|
|
89
|
+
this.fileLoaded = true;
|
|
90
|
+
GLib.action.execute(this.spec.onLoadRows, this);
|
|
91
|
+
} else {
|
|
92
|
+
Utils.launch.dialog.alert("File doesn't contain valid data.", this);
|
|
93
|
+
}
|
|
58
94
|
};
|
|
59
95
|
|
|
60
96
|
// Reset value so it will trigger again the next time the same file is selected.
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :style="$styles()" :class="$classes()">
|
|
3
|
+
<panels-responsive v-if="header" :spec="header" />
|
|
4
|
+
|
|
5
|
+
<div class="scrollable">
|
|
6
|
+
<div ref="topAnchor"></div>
|
|
7
|
+
|
|
8
|
+
<table v-if="loadIf">
|
|
9
|
+
<template v-for="(section, sectionIndex) in sections" :key="`head_${sectionIndex}`">
|
|
10
|
+
<thead>
|
|
11
|
+
<tr v-if="importable || exportable">
|
|
12
|
+
<td colspan="20">
|
|
13
|
+
<div>
|
|
14
|
+
<template v-if="importable && fileLoaded">
|
|
15
|
+
<span>{{ section.dataRows.length }} rows loaded</span> --
|
|
16
|
+
<span>{{ selectedRowCount(section) }} rows selected</span>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<div class="float-right">
|
|
20
|
+
<v-btn v-if="exportable" :download="exportFile" :href="exportCsv(section)">{{ exportLabel }}</v-btn>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div v-if="output" style="white-space: pre-line;">
|
|
25
|
+
{{ output }}
|
|
26
|
+
</div>
|
|
27
|
+
</td>
|
|
28
|
+
</tr>
|
|
29
|
+
|
|
30
|
+
<tr v-if="section.header" :style="$styles(section.header)">
|
|
31
|
+
<template v-if="section.header.dataCells">
|
|
32
|
+
<th class="status" v-if="fileLoaded">
|
|
33
|
+
<glib-component :spec="headerCheckSpec(section)" />
|
|
34
|
+
</th>
|
|
35
|
+
<th class="fixed-width" v-for="(cell, index) in section.header.dataCells" :key="index"
|
|
36
|
+
:colSpan="colSpan(section.header, index)">
|
|
37
|
+
{{ cell }}
|
|
38
|
+
</th>
|
|
39
|
+
</template>
|
|
40
|
+
<th class="fixed-width" v-for="(cell, index) in section.header.cellViews" v-else :key="index"
|
|
41
|
+
:colSpan="colSpan(section.header, index)">
|
|
42
|
+
<glib-component :spec="cell" />
|
|
43
|
+
</th>
|
|
44
|
+
</tr>
|
|
45
|
+
</thead>
|
|
46
|
+
|
|
47
|
+
<tbody>
|
|
48
|
+
<!-- <template v-for="(row, rowIndex) in section.rows" :key="`row_${rowIndex}`">
|
|
49
|
+
<tr :class="row.onClick ? 'clickable' : ''">
|
|
50
|
+
<td v-for="(cell, cellIndex) in row.cellViews" :key="`cell_${cellIndex}`"
|
|
51
|
+
:colSpan="colSpan(row, cellIndex)" :style="colStyles(row, cellIndex)">
|
|
52
|
+
<glib-component :spec="cell" />
|
|
53
|
+
</td>
|
|
54
|
+
</tr>
|
|
55
|
+
</template> -->
|
|
56
|
+
|
|
57
|
+
<tr v-for="(row, rowIndex) in section.dataRows" :key="`data_row_${rowIndex}`">
|
|
58
|
+
<!-- TODO: Make this first column sticky.
|
|
59
|
+
See https://css-tricks.com/a-table-with-both-a-sticky-header-and-a-sticky-first-column/
|
|
60
|
+
-->
|
|
61
|
+
<td class="status" v-if="fileLoaded">
|
|
62
|
+
<glib-component :spec="rowCheckSpec(sectionIndex, rowIndex)" />
|
|
63
|
+
<glib-component :spec="pendingIconSpec(rowIndex)" />
|
|
64
|
+
</td>
|
|
65
|
+
<td v-for="(cell, cellIndex) in row" :key="`data_cell_${cellIndex}`">
|
|
66
|
+
<v-text-field density="compact" variant="solo-filled" v-model="row[cellIndex]" />
|
|
67
|
+
</td>
|
|
68
|
+
</tr>
|
|
69
|
+
|
|
70
|
+
<tr v-if="importable && section.dataRows.length <= 0">
|
|
71
|
+
<td colspan="20">
|
|
72
|
+
<!-- TODO: Reuse code from multiUpload so it supports drag-and-drop too -->
|
|
73
|
+
<input ref="fileInput" style="display: none;" type="file" accept=".csv"
|
|
74
|
+
@change="loadFile($event, section)" />
|
|
75
|
+
<div
|
|
76
|
+
style="cursor: pointer; border: 1px solid rgba(0, 0, 0, 0.12); text-align: center; padding: 20px; margin: 20px;"
|
|
77
|
+
@click="triggerImport(sectionIndex)">
|
|
78
|
+
Drag your CSV file here<br />
|
|
79
|
+
or click to browse
|
|
80
|
+
</div>
|
|
81
|
+
</td>
|
|
82
|
+
</tr>
|
|
83
|
+
</tbody>
|
|
84
|
+
</template>
|
|
85
|
+
</table>
|
|
86
|
+
|
|
87
|
+
<div ref="bottomAnchor" class="py-3 px-6" :style="bottomAnchorStyles">
|
|
88
|
+
Loading...
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<panels-responsive v-if="footer" :spec="footer" />
|
|
93
|
+
</div>
|
|
94
|
+
</template>
|
|
95
|
+
|
|
96
|
+
<script>
|
|
97
|
+
import autoloadMixin from "../mixins/table/autoload.js";
|
|
98
|
+
import exportMixin from "../mixins/table/export.js";
|
|
99
|
+
import importMixin from "../mixins/table/import.js";
|
|
100
|
+
|
|
101
|
+
export default {
|
|
102
|
+
mixins: [autoloadMixin, exportMixin, importMixin],
|
|
103
|
+
props: {
|
|
104
|
+
spec: { type: Object, required: true }
|
|
105
|
+
},
|
|
106
|
+
data() {
|
|
107
|
+
return {
|
|
108
|
+
sections: [],
|
|
109
|
+
fileLoaded: false
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
computed: {
|
|
113
|
+
header() {
|
|
114
|
+
return this.spec.header;
|
|
115
|
+
},
|
|
116
|
+
footer() {
|
|
117
|
+
return this.spec.footer;
|
|
118
|
+
},
|
|
119
|
+
output() {
|
|
120
|
+
// let str = ""
|
|
121
|
+
// let count = 0
|
|
122
|
+
// for (const section of this.sections) {
|
|
123
|
+
// for (const row of section.dataRows) {
|
|
124
|
+
// const name = row[2]
|
|
125
|
+
// const email = row[3]
|
|
126
|
+
// const createdAt = row[5]
|
|
127
|
+
// const activationState = row[14] ? 'active' : 'pending'
|
|
128
|
+
// count += 1
|
|
129
|
+
// str += `{ name: "${name}".to_s, email: '${email}'.to_s, created_at: '${createdAt}', activation_state: '${activationState}'},\n`
|
|
130
|
+
// }
|
|
131
|
+
// }
|
|
132
|
+
// return `Processing ${count} rows:\n[\n${str}\n]`
|
|
133
|
+
return "";
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
methods: {
|
|
137
|
+
$mounted() {
|
|
138
|
+
this.$onEvent("forms/directSubmit", (e) => {
|
|
139
|
+
for (const section of this.sections) {
|
|
140
|
+
this.submitRows(e.data.url, section);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
$ready() {
|
|
145
|
+
this.sections = this.spec.sections;
|
|
146
|
+
this.sections.forEach((section, sectionIndex) => {
|
|
147
|
+
section.header = section.header || {};
|
|
148
|
+
section.index = sectionIndex;
|
|
149
|
+
// Use Object.assign() to bind the nested property
|
|
150
|
+
Object.assign(section, { dataRows: [] });
|
|
151
|
+
});
|
|
152
|
+
this.autoloadAll(this.spec.nextPage);
|
|
153
|
+
this.initCsvExport();
|
|
154
|
+
this.initCsvImport();
|
|
155
|
+
this.enableInfiniteScrollIfApplicable();
|
|
156
|
+
},
|
|
157
|
+
$tearDown() {
|
|
158
|
+
this.cancelAutoloadRequest();
|
|
159
|
+
},
|
|
160
|
+
pendingIconSpec(rowIndex) {
|
|
161
|
+
const statusViewIdPrefix = this.spec.statusViewIdPrefix || 'data_status_';
|
|
162
|
+
return {
|
|
163
|
+
view: 'icon',
|
|
164
|
+
styleClasses: ['warning'],
|
|
165
|
+
id: `${statusViewIdPrefix}${rowIndex}`,
|
|
166
|
+
// TODO: It seems that logics_set/components_set doesn't work when changing the icon name.
|
|
167
|
+
material: {
|
|
168
|
+
name: 'preview'
|
|
169
|
+
},
|
|
170
|
+
// TODO: Implement this in the frontend. Right now, this is implemented in glib-web backend.
|
|
171
|
+
tooltip: {
|
|
172
|
+
text: "Review before submitting"
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
headerCheckSpec(section) {
|
|
177
|
+
const sectionIndex = section.index;
|
|
178
|
+
const maxIndex = section.dataRows.length;
|
|
179
|
+
const result = {
|
|
180
|
+
view: 'fields/check',
|
|
181
|
+
name: 'check_all',
|
|
182
|
+
checkValue: true,
|
|
183
|
+
onChange: {
|
|
184
|
+
action: 'logics/set',
|
|
185
|
+
targetIds: Array(maxIndex).fill().map((_, i) => `_check_row_${sectionIndex}_${i}`),
|
|
186
|
+
conditionalData: {
|
|
187
|
+
checkValue: true,
|
|
188
|
+
value: { "var": "check_all" }
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
return result;
|
|
193
|
+
},
|
|
194
|
+
rowCheckSpec(sectionIndex, rowIndex) {
|
|
195
|
+
const id = this.rowCheckId(sectionIndex, rowIndex);
|
|
196
|
+
return {
|
|
197
|
+
view: 'fields/check',
|
|
198
|
+
checkValue: true,
|
|
199
|
+
name: `${id}-name`, // For some unknown reason, equating the id and name values causes this function to be executed causing the component to be reset.
|
|
200
|
+
// name: id,
|
|
201
|
+
id: id
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
rowCheckId(sectionIndex, rowIndex) {
|
|
205
|
+
// Use underscore to prevent conflict with server-defined IDs, e.g. for pendingIconSpec()
|
|
206
|
+
return `_check_row_${sectionIndex}_${rowIndex}`;
|
|
207
|
+
},
|
|
208
|
+
colSpan(row, index) {
|
|
209
|
+
const spans = row.colSpans || [];
|
|
210
|
+
return spans[index] || 1;
|
|
211
|
+
},
|
|
212
|
+
colStyles(row, index) {
|
|
213
|
+
const colStyles = row.colStyles || [];
|
|
214
|
+
return this.$styles(colStyles[index] || {});
|
|
215
|
+
},
|
|
216
|
+
rows(section) {
|
|
217
|
+
return section.rows || [];
|
|
218
|
+
},
|
|
219
|
+
triggerImport(index) {
|
|
220
|
+
const input = this.$refs.fileInput[index];
|
|
221
|
+
input.click();
|
|
222
|
+
},
|
|
223
|
+
totalRows(section) {
|
|
224
|
+
return (section.rows || []).length + (section.dataRows || []).length;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
</script>
|
|
229
|
+
|
|
230
|
+
<style lang="scss" scoped>
|
|
231
|
+
table {
|
|
232
|
+
border-spacing: 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// thead {
|
|
236
|
+
// th {
|
|
237
|
+
// padding: 10px 4px;
|
|
238
|
+
// border-top: 1px solid rgba(0, 0, 0, 0.12);
|
|
239
|
+
// // border-left: 1px solid rgba(0, 0, 0, 0.12);
|
|
240
|
+
// }
|
|
241
|
+
// }
|
|
242
|
+
|
|
243
|
+
thead {
|
|
244
|
+
th {
|
|
245
|
+
&.fixed-width {
|
|
246
|
+
min-width: 200px; // TODO: Make this configurable
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
&.status {
|
|
250
|
+
min-width: 80px;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
tbody {
|
|
257
|
+
// tr.clickable {
|
|
258
|
+
// td>a {
|
|
259
|
+
// cursor: pointer;
|
|
260
|
+
// }
|
|
261
|
+
|
|
262
|
+
// &:hover {
|
|
263
|
+
// background: #eee;
|
|
264
|
+
// }
|
|
265
|
+
// }
|
|
266
|
+
|
|
267
|
+
td {
|
|
268
|
+
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
|
269
|
+
|
|
270
|
+
span {
|
|
271
|
+
display: block;
|
|
272
|
+
color: inherit;
|
|
273
|
+
cursor: default;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
&.status {
|
|
277
|
+
display: flex;
|
|
278
|
+
align-items: center;
|
|
279
|
+
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.scrollable {
|
|
285
|
+
width: 100%;
|
|
286
|
+
overflow: auto;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.data-cell {
|
|
290
|
+
white-space: pre-line;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
table.table--grid {
|
|
294
|
+
tbody {
|
|
295
|
+
td {
|
|
296
|
+
border-right: 1px solid rgba(0, 0, 0, 0.12);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
</style>
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :style="$styles()" :class="$classes()" v-if="loadIf">
|
|
3
|
+
<div>Rows: {{ loadedRowCount }} Selected: {{ selectedRowCount }}</div>
|
|
4
|
+
<input ref="fileInput" style="display: none;" type="file" accept=".csv" @change="handleClick" />
|
|
5
|
+
<div class="scrollable">
|
|
6
|
+
<table>
|
|
7
|
+
<thead v-if="props.spec.viewHeaders">
|
|
8
|
+
<tr>
|
|
9
|
+
<th v-if="rowLoaded"><v-checkbox v-model="checkbox" color="primary" @change="checkAll"></v-checkbox></th>
|
|
10
|
+
<th v-for="cell in props.spec.viewHeaders" :key="cell.id"
|
|
11
|
+
:style="{ minWidth: `${cell.minWidth || 100}px` }">
|
|
12
|
+
<span>{{ cell.text }}</span>
|
|
13
|
+
</th>
|
|
14
|
+
</tr>
|
|
15
|
+
</thead>
|
|
16
|
+
|
|
17
|
+
<tbody>
|
|
18
|
+
<template v-if="rowLoaded">
|
|
19
|
+
<template v-for="(section, sectionIndex) in sections" :key="`head_${sectionIndex}`">
|
|
20
|
+
<tr v-for="(row, rowIndex) in section.rows" :key="`row_${rowIndex}`">
|
|
21
|
+
<td><v-checkbox v-model="row.selected" color="primary"></v-checkbox></td>
|
|
22
|
+
<td v-for="(cell, cellIndex) in row.columns" :key="`cell_${cellIndex}`"
|
|
23
|
+
@change="(e) => handleCellChange(e, cell)">
|
|
24
|
+
<glib-component :spec="cell.view"></glib-component>
|
|
25
|
+
</td>
|
|
26
|
+
</tr>
|
|
27
|
+
</template>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<tr v-else>
|
|
31
|
+
<td colspan="100%" style="padding-top: 8px">
|
|
32
|
+
<div class="gdrop-file border-[2px]" @dragover="(e) => e.preventDefault()" @drop="handleDrop"
|
|
33
|
+
@click="fileInput.click()">
|
|
34
|
+
|
|
35
|
+
<div class="cloud" style="pointer-events: none;">
|
|
36
|
+
<v-icon ref="icon" size="48" class="icon">cloud_upload</v-icon>
|
|
37
|
+
<h4 class="title">Drag your CSV file here</h4>
|
|
38
|
+
<p class="subtitle">or click to browse</p>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</td>
|
|
42
|
+
</tr>
|
|
43
|
+
</tbody>
|
|
44
|
+
</table>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</template>
|
|
48
|
+
|
|
49
|
+
<style lang="scss" scoped>
|
|
50
|
+
table thead th {
|
|
51
|
+
position: sticky;
|
|
52
|
+
top: 0px;
|
|
53
|
+
z-index: 1005;
|
|
54
|
+
background-color: white;
|
|
55
|
+
}
|
|
56
|
+
</style>
|
|
57
|
+
|
|
58
|
+
<script setup>
|
|
59
|
+
import { computed, getCurrentInstance, ref, watch } from "vue";
|
|
60
|
+
import { parseCsv } from "../composable/parser";
|
|
61
|
+
import Action from "../../action";
|
|
62
|
+
|
|
63
|
+
class Cell {
|
|
64
|
+
constructor({ viewHeaders, cellIndex, rowId, view, value }) {
|
|
65
|
+
this.viewHeaders = viewHeaders;
|
|
66
|
+
this.cellIndex = cellIndex;
|
|
67
|
+
this.rowId = rowId;
|
|
68
|
+
this.view = view;
|
|
69
|
+
this.value = value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get cellId() {
|
|
73
|
+
return this.viewHeaders[this.cellIndex].id;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function handleClick(e) {
|
|
78
|
+
loadFile(e.target.files);
|
|
79
|
+
e.target.value = '';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function handleDrop(e) {
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
loadFile(e.dataTransfer.files);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function makeSection(section) {
|
|
88
|
+
const rows = section.dataRows.map((dataRow) => {
|
|
89
|
+
const selected = false;
|
|
90
|
+
const rowId = dataRow.rowId;
|
|
91
|
+
const columns = props.spec.viewColumns.map((viewColumn, viewColumnIndex) => {
|
|
92
|
+
const view = Object.assign({}, viewColumn, dataRow.columns[viewColumnIndex]);
|
|
93
|
+
const cellIndex = viewColumnIndex;
|
|
94
|
+
const viewHeaders = props.spec.viewHeaders;
|
|
95
|
+
const value = dataRow.columns[viewColumnIndex].value;
|
|
96
|
+
return new Cell({ rowId, view, cellIndex, viewHeaders, value });
|
|
97
|
+
});
|
|
98
|
+
return { selected, columns, rowId };
|
|
99
|
+
});
|
|
100
|
+
return Object.assign({}, section, { rows });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const props = defineProps(['spec']);
|
|
104
|
+
const fileInput = ref(null);
|
|
105
|
+
const checkbox = ref(false);
|
|
106
|
+
const instance = getCurrentInstance();
|
|
107
|
+
|
|
108
|
+
const fillableColumnIndexes = props.spec.viewHeaders.reduce((prev, curr, index) => {
|
|
109
|
+
if (curr.importable) prev.push(index);
|
|
110
|
+
return prev;
|
|
111
|
+
}, []);
|
|
112
|
+
|
|
113
|
+
const sections = ref(props.spec.sections.map((section) => makeSection(section)));
|
|
114
|
+
|
|
115
|
+
watch(props, (value) => {
|
|
116
|
+
sections.value = value.spec.sections.map((section) => makeSection(section));
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const selectedRows = computed(() => {
|
|
120
|
+
const rows = [];
|
|
121
|
+
sections.value.forEach((section) => section.rows.filter((row) => row.selected).forEach((row) => rows.push(row)));
|
|
122
|
+
return rows;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
watch(selectedRows, (value) => {
|
|
126
|
+
const rows = value.map((row) => {
|
|
127
|
+
const rowId = row.rowId;
|
|
128
|
+
const columns = row.columns.map((col) => ({ cellId: col.cellId, value: col.value }));
|
|
129
|
+
|
|
130
|
+
return { rowId, columns };
|
|
131
|
+
});
|
|
132
|
+
const data = Object.assign(
|
|
133
|
+
{},
|
|
134
|
+
props.spec.onRowSelected,
|
|
135
|
+
{ [props.spec.paramNameForFormData || 'formData']: rows }
|
|
136
|
+
);
|
|
137
|
+
Action.execute(data, instance.ctx);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const loadedRowCount = computed(() => sections.value.reduce((prev, curr) => prev + curr.dataRows.length, 0));
|
|
141
|
+
const selectedRowCount = computed(() => selectedRows.value.length);
|
|
142
|
+
const rowLoaded = computed(() => !!(sections.value[0] && sections.value[0].rows));
|
|
143
|
+
|
|
144
|
+
function checkAll() {
|
|
145
|
+
sections.value.forEach((section) => section.rows.forEach((row) => row.selected = checkbox.value));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function handleCellChange(e, cell) {
|
|
149
|
+
const rowId = cell.rowId;
|
|
150
|
+
const columnId = cell.cellId;
|
|
151
|
+
|
|
152
|
+
let value = null;
|
|
153
|
+
if (e.target instanceof HTMLInputElement) {
|
|
154
|
+
value = e.target.value;
|
|
155
|
+
} else {
|
|
156
|
+
value = e.target.querySelector('input').value;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
cell.value = value;
|
|
160
|
+
|
|
161
|
+
const data = Object.assign(
|
|
162
|
+
{},
|
|
163
|
+
props.spec.onCellChange,
|
|
164
|
+
{ [props.spec.paramNameForFormData || 'formData']: { rowId, columnId, value } }
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
Action.execute(data, instance.ctx);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function loadFile(files) {
|
|
171
|
+
const reader = new FileReader();
|
|
172
|
+
reader.readAsText(files[0]);
|
|
173
|
+
|
|
174
|
+
reader.onload = (ev) => {
|
|
175
|
+
const csvData = parseCsv(ev.target.result);
|
|
176
|
+
const dataRows = csvData.map((columns) => {
|
|
177
|
+
let i = 0;
|
|
178
|
+
const cols = [];
|
|
179
|
+
props.spec.viewColumns.forEach((viewColumn, index) => {
|
|
180
|
+
const obj = {};
|
|
181
|
+
if (fillableColumnIndexes.includes(index)) {
|
|
182
|
+
cols.push(Object.assign(obj, viewColumn, { value: columns[i] }));
|
|
183
|
+
i++;
|
|
184
|
+
} else {
|
|
185
|
+
cols.push(Object.assign(obj, viewColumn));
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
return { rowId: null, columns: cols };
|
|
189
|
+
});
|
|
190
|
+
const newSections = makeSection({ dataRows });
|
|
191
|
+
sections.value.push(newSections);
|
|
192
|
+
|
|
193
|
+
Action.execute(props.spec.onLoadRows, instance.ctx);
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
</script>
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -24,20 +24,19 @@ export default {
|
|
|
24
24
|
|
|
25
25
|
const id = this.viewId;
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
// const existingComponent = GLib.component.findById(id, false);
|
|
27
|
+
const existingComponent = GLib.component.findById(id);
|
|
29
28
|
// A component with the same ID in a different page shouldn't be considered a
|
|
30
29
|
// duplicate. See `utils/components#deregister` for more details.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
+
}
|
|
41
40
|
const newComponent = this;
|
|
42
41
|
GLib.component.register(id, newComponent);
|
|
43
42
|
}
|
package/plugins/vuetify.js
CHANGED
|
@@ -3,9 +3,12 @@ import { createVuetify } from "vuetify";
|
|
|
3
3
|
import { aliases, md } from 'vuetify/iconsets/md';
|
|
4
4
|
import 'vuetify/styles';
|
|
5
5
|
import * as components from 'vuetify/components';
|
|
6
|
+
import { jsonView } from "../store";
|
|
6
7
|
// import * as directives from 'vuetify/directives'
|
|
8
|
+
// import { md3 } from 'vuetify/blueprints';
|
|
7
9
|
|
|
8
10
|
const opts = {
|
|
11
|
+
// blueprint: md3,
|
|
9
12
|
components,
|
|
10
13
|
// directives,
|
|
11
14
|
icons: {
|
|
@@ -15,9 +18,7 @@ const opts = {
|
|
|
15
18
|
md,
|
|
16
19
|
},
|
|
17
20
|
},
|
|
18
|
-
theme: {
|
|
19
|
-
themes: {}
|
|
20
|
-
},
|
|
21
|
+
theme: jsonView.page.theme || {},
|
|
21
22
|
display: {
|
|
22
23
|
smAndDown: true
|
|
23
24
|
}
|