glib-web 3.12.2 → 3.14.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 +14 -6
- package/actions/cables/push.js +9 -24
- package/actions/dialogs/show.js +8 -0
- package/actions/labels/set.js +16 -0
- package/actions/lists/append.js +15 -0
- package/actions/windows/open.js +1 -1
- package/app.vue +20 -2
- package/components/component.vue +2 -0
- package/components/composable/socket.js +97 -0
- package/components/fields/_select.vue +0 -1
- package/components/fields/multipleUpload.vue +393 -0
- package/components/fields/text.vue +1 -1
- package/components/fields/textarea.vue +3 -3
- package/components/mixins/generic.js +1 -1
- package/components/p.vue +7 -1
- package/index.js +4 -6
- package/nav/dialog.vue +10 -5
- package/package.json +2 -2
- package/store.js +1 -1
- package/utils/form.js +1 -1
- package/utils/http.js +4 -4
- package/utils/launch.js +4 -6
package/action.js
CHANGED
|
@@ -63,6 +63,10 @@ import ActionPopoversClose from "./actions/popovers/close";
|
|
|
63
63
|
import ActionComponentsUpdate from "./actions/components/update";
|
|
64
64
|
import ActionComponentsFind from "./actions/components/find";
|
|
65
65
|
|
|
66
|
+
import ActionLabelsSet from "./actions/labels/set";
|
|
67
|
+
|
|
68
|
+
import ActionListsAppend from "./actions/lists/append";
|
|
69
|
+
|
|
66
70
|
import { vueApp } from "./store";
|
|
67
71
|
|
|
68
72
|
const actions = {
|
|
@@ -126,7 +130,10 @@ const actions = {
|
|
|
126
130
|
"popovers/close": ActionPopoversClose,
|
|
127
131
|
|
|
128
132
|
"components/update": ActionComponentsUpdate,
|
|
129
|
-
"components/find": ActionComponentsFind
|
|
133
|
+
"components/find": ActionComponentsFind,
|
|
134
|
+
|
|
135
|
+
"labels/set": ActionLabelsSet,
|
|
136
|
+
"lists/append": ActionListsAppend
|
|
130
137
|
};
|
|
131
138
|
|
|
132
139
|
const customActions = {};
|
|
@@ -182,17 +189,18 @@ export default class Action {
|
|
|
182
189
|
}
|
|
183
190
|
}
|
|
184
191
|
|
|
185
|
-
static handleResponse(response, component) {
|
|
192
|
+
static handleResponse(response, component, windowMode = true) {
|
|
193
|
+
// The execution goes in the following order: onResponse, page render, onLoad
|
|
186
194
|
vueApp.temp.analytics = response.analytics;
|
|
187
195
|
GLib.action.execute(response.onResponse, component);
|
|
188
196
|
vueApp.temp.analytics = null;
|
|
189
197
|
|
|
190
|
-
// TODO: The execution should be in the following order: onResponse, page render, onLoad
|
|
191
198
|
if (response.header || response.body || response.footer) {
|
|
192
199
|
Utils.http.forceComponentUpdate(() => {
|
|
193
|
-
const dialog = component.$closest("dialog")
|
|
194
|
-
|
|
195
|
-
|
|
200
|
+
const dialog = component.$closest("dialog")
|
|
201
|
+
const updateDialog = windowMode ? false : Utils.type.isObject(dialog)
|
|
202
|
+
if (updateDialog) {
|
|
203
|
+
dialog.updateContent(response);
|
|
196
204
|
} else {
|
|
197
205
|
vueApp.page = response;
|
|
198
206
|
}
|
package/actions/cables/push.js
CHANGED
|
@@ -1,31 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { channels } from "../../components/composable/socket";
|
|
2
|
+
|
|
2
3
|
|
|
3
4
|
export default class {
|
|
4
5
|
execute(properties, component) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const channel = ws.channels[channelName];
|
|
8
|
-
|
|
9
|
-
// TODO: Use logDisabled() that reads params from server to decide whether we want to print the log or not.
|
|
10
|
-
// This is because `push` action's logs are a bit sensitive.
|
|
11
|
-
console.debug("Pushing to", channel);
|
|
6
|
+
const { event, payload, channel } = properties;
|
|
7
|
+
const ch = channels[channel];
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
});
|
|
18
|
-
console.debug(`Pushing to '${channelName}/${eventName}'`, payload);
|
|
9
|
+
if (!event || !channel) {
|
|
10
|
+
console.error('No event name or channel');
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
19
13
|
|
|
20
|
-
|
|
21
|
-
} else {
|
|
22
|
-
console.error(`Channel not joined: '${channelName}'`);
|
|
23
|
-
Utils.launch.snackbar.error(
|
|
24
|
-
"Something went wrong and we have been notified",
|
|
25
|
-
component
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
});
|
|
14
|
+
ch.perform(event, payload);
|
|
30
15
|
}
|
|
31
16
|
}
|
package/actions/dialogs/show.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
export default class {
|
|
2
2
|
execute(spec, component) {
|
|
3
|
+
let dialog = null
|
|
4
|
+
if (spec.updateExisting) {
|
|
5
|
+
dialog = component.$closest("dialog")
|
|
6
|
+
if (dialog) {
|
|
7
|
+
dialog.updateContent(spec)
|
|
8
|
+
return
|
|
9
|
+
}
|
|
10
|
+
}
|
|
3
11
|
Utils.launch.dialog.open(spec, component);
|
|
4
12
|
}
|
|
5
13
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import Action from "../../action";
|
|
2
|
+
|
|
3
|
+
export default class {
|
|
4
|
+
execute(properties, component) {
|
|
5
|
+
let targetComponent = component;
|
|
6
|
+
if (properties.targetId) {
|
|
7
|
+
targetComponent = GLib.component.findById(properties.targetId);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
targetComponent.text = properties.text;
|
|
11
|
+
|
|
12
|
+
if (properties.onSet) {
|
|
13
|
+
Action.execute(properties.onSet, targetComponent);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export default class {
|
|
2
|
+
execute(properties, component) {
|
|
3
|
+
let targetComponent = component;
|
|
4
|
+
if (properties.targetId) {
|
|
5
|
+
targetComponent = GLib.component.findById(properties.targetId);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (!targetComponent.sections || !targetComponent.sections[0].rows) {
|
|
9
|
+
console.warn('there is no sections or rows');
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
targetComponent.sections[0].rows.push(properties.row);
|
|
14
|
+
}
|
|
15
|
+
}
|
package/actions/windows/open.js
CHANGED
package/app.vue
CHANGED
|
@@ -34,10 +34,14 @@ import Utils from "./utils/helper";
|
|
|
34
34
|
import FormPanel from "./components/panels/form.vue";
|
|
35
35
|
import { vueApp } from "./store";
|
|
36
36
|
import { useDirtyState } from "./components/composable/dirtyState";
|
|
37
|
+
import { useSocket } from "./components/composable/socket";
|
|
37
38
|
|
|
38
39
|
const { watchDirtyState } = useDirtyState();
|
|
39
40
|
|
|
40
41
|
export default {
|
|
42
|
+
setup() {
|
|
43
|
+
return { vueApp };
|
|
44
|
+
},
|
|
41
45
|
components: {
|
|
42
46
|
"nav-appbar": NavAppBar,
|
|
43
47
|
"panels-form": FormPanel,
|
|
@@ -50,7 +54,7 @@ export default {
|
|
|
50
54
|
return {
|
|
51
55
|
title: "...",
|
|
52
56
|
mainHeight: 0,
|
|
53
|
-
|
|
57
|
+
actionCableCustomer: null,
|
|
54
58
|
};
|
|
55
59
|
},
|
|
56
60
|
computed: {
|
|
@@ -84,10 +88,24 @@ export default {
|
|
|
84
88
|
"vueApp.indicator": function (val, oldVal) {
|
|
85
89
|
document.title = val ? "..." : this.page.title;
|
|
86
90
|
},
|
|
91
|
+
"vueApp.page": {
|
|
92
|
+
handler(val, oldVal) {
|
|
93
|
+
if (!val) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const { actionCable } = val;
|
|
98
|
+
if (actionCable) {
|
|
99
|
+
const { customer } = useSocket(actionCable, this.actionCableCustomer);
|
|
100
|
+
this.actionCableCustomer = customer;
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
immediate: true
|
|
104
|
+
}
|
|
87
105
|
},
|
|
88
106
|
created() {
|
|
89
107
|
console.debug(
|
|
90
|
-
`
|
|
108
|
+
`Version: ${Utils.settings.appVersion} (${Utils.settings.env})`
|
|
91
109
|
);
|
|
92
110
|
Utils.history.saveInitialContent(this.page);
|
|
93
111
|
Utils.history.restoreOnBackOrForward();
|
package/components/component.vue
CHANGED
|
@@ -71,6 +71,7 @@ import TextAreaField from "./fields/textarea.vue";
|
|
|
71
71
|
const RichTextField = defineAsyncComponent(() => import("./fields/richText.vue"));
|
|
72
72
|
// import NewRichTextField from "./fields/newRichText.vue";
|
|
73
73
|
const FileField = defineAsyncComponent(() => import("./fields/file.vue"));
|
|
74
|
+
const MultipleUploadField = defineAsyncComponent(() => import("./fields/multipleUpload.vue"));
|
|
74
75
|
import AutocompleteField from "./fields/autocomplete.vue";
|
|
75
76
|
import SelectField from "./fields/select.vue";
|
|
76
77
|
import TimeZoneField from "./fields/timeZone.vue";
|
|
@@ -163,6 +164,7 @@ export default {
|
|
|
163
164
|
"fields-richText": RichTextField,
|
|
164
165
|
// "fields-newRichText": NewRichTextField,
|
|
165
166
|
"fields-file": FileField,
|
|
167
|
+
"fields-multipleUpload": MultipleUploadField,
|
|
166
168
|
"fields-autocomplete": AutocompleteField,
|
|
167
169
|
"fields-select": SelectField,
|
|
168
170
|
"fields-timeZone": TimeZoneField,
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createConsumer } from '@rails/actioncable';
|
|
2
|
+
import { reactive } from "vue";
|
|
3
|
+
import Action from "../../action";
|
|
4
|
+
|
|
5
|
+
// Handler implementation
|
|
6
|
+
class SocketHandler {
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.targetId = options.targetId;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
initialized() {
|
|
12
|
+
console.log(`${this.targetId} initialized`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
rejected() {
|
|
16
|
+
console.error(`${this.targetId} rejected`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
received(data) {
|
|
20
|
+
Action.execute(data, this.component);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
connected() {
|
|
24
|
+
console.log(`${this.targetId} connected`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
disconnected() {
|
|
28
|
+
console.log(`${this.targetId} disconnected`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get component() {
|
|
32
|
+
if (this.targetId) {
|
|
33
|
+
return GLib.component.findById(this.targetId);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
class ExampleSocketHandler extends SocketHandler {
|
|
38
|
+
received(data) {
|
|
39
|
+
this.component.text = data.text;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// you can implement your own handler or just use the default one
|
|
44
|
+
const socketHandlers = reactive({
|
|
45
|
+
ExampleSocketHandler
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const channels = reactive({});
|
|
49
|
+
|
|
50
|
+
// API
|
|
51
|
+
const useSocket = (config, defaultConsumer = null) => {
|
|
52
|
+
const consumer = defaultConsumer || createConsumer();
|
|
53
|
+
|
|
54
|
+
consumer.connection.events.error = function () {
|
|
55
|
+
console.error('Websocket error');
|
|
56
|
+
if (!config.onError) return;
|
|
57
|
+
Action.execute(config.onError, {});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
config.channels.forEach(ch => {
|
|
61
|
+
if (!channels[ch.name]) {
|
|
62
|
+
let handler = null;
|
|
63
|
+
|
|
64
|
+
if (ch.handler && socketHandlers[ch.handler]) {
|
|
65
|
+
handler = new socketHandlers[ch.handler](ch.options);
|
|
66
|
+
} else {
|
|
67
|
+
handler = new SocketHandler(ch.options);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const channel = consumer.subscriptions.create(
|
|
71
|
+
{ channel: ch.name, ...ch.options },
|
|
72
|
+
{
|
|
73
|
+
received(data) {
|
|
74
|
+
handler.received(data);
|
|
75
|
+
},
|
|
76
|
+
initialized() {
|
|
77
|
+
handler.initialized();
|
|
78
|
+
},
|
|
79
|
+
connected() {
|
|
80
|
+
handler.connected();
|
|
81
|
+
},
|
|
82
|
+
rejected() {
|
|
83
|
+
handler.rejected();
|
|
84
|
+
},
|
|
85
|
+
disconnected({ willAttemptReconnect: boolean }) {
|
|
86
|
+
handler.disconnected();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
channels[ch.name] = channel;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return { consumer };
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export { useSocket, channels };
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :style="$styles()" :class="$classes()">
|
|
3
|
+
<div ref="container" @click="handleClick" @drop="handleDrop" @dragover="handleDragOver" @dragleave="handleDragLeave"
|
|
4
|
+
class="gdrop-file border-[2px]">
|
|
5
|
+
<input ref="fileSelect" type="file" multiple style="display: none">
|
|
6
|
+
|
|
7
|
+
<div class="cloud" style="pointer-events: none;">
|
|
8
|
+
<v-icon ref="icon" size="48" class="icon">cloud_upload</v-icon>
|
|
9
|
+
<h4 class="title">{{ spec.title }}</h4>
|
|
10
|
+
<p class="subtitle">{{ spec.subtitle }}</p>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<input v-for="(file, index) in Object.values(files)" type="hidden" :name="spec.name" :value="file.signedId"
|
|
14
|
+
:disabled="inputDisabled" :key="`hidden-${index}`">
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div class="guploaded-file" v-if="showUploadedFile">
|
|
18
|
+
<p class="title" v-if="uploadTitle">{{ uploadTitle }} </p>
|
|
19
|
+
|
|
20
|
+
<template v-for="(file, index) in Object.entries(files)" :key="`added-${index}`">
|
|
21
|
+
<div :class="`file-container ${file[1].status == 'failed' ? 'opacity-50' : ''}`">
|
|
22
|
+
<div class="file">
|
|
23
|
+
<div @click.stop="handleRemoveFile(file[0])" class="close-btn">
|
|
24
|
+
<v-icon size="12" color="white">close</v-icon>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="status">
|
|
27
|
+
<v-icon class="icon">{{ file[1].isImage() ? 'photo_library' : 'description' }}</v-icon>
|
|
28
|
+
<div class="progress">
|
|
29
|
+
<label class="label">{{ `${file[1].name} ${file[1].status === 'failed' ? uploadFailedText : ''}`
|
|
30
|
+
}}</label>
|
|
31
|
+
<div class="background" v-show="file[1].progress.value > 0">
|
|
32
|
+
<div class="value" :style="{ width: `${file[1].progress.value}%` }"></div>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</template>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
</div>
|
|
42
|
+
</template>
|
|
43
|
+
|
|
44
|
+
<style>
|
|
45
|
+
.border-\[2px\] {
|
|
46
|
+
border-width: 2px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.border-\[4px\] {
|
|
50
|
+
border-width: 4px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.\!font-semibold {
|
|
54
|
+
font-weight: 600px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.opacity-50 {
|
|
58
|
+
opacity: 0.5;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.guploaded-file {
|
|
62
|
+
/* pt-6 w-full */
|
|
63
|
+
padding-top: 24px;
|
|
64
|
+
width: 100%;
|
|
65
|
+
|
|
66
|
+
.title {
|
|
67
|
+
color: #6b7280;
|
|
68
|
+
font-weight: 600 !important;
|
|
69
|
+
margin-bottom: 8px;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.file-container {
|
|
73
|
+
display: flex;
|
|
74
|
+
align-items: center;
|
|
75
|
+
border-radius: 6px;
|
|
76
|
+
background-color: #f3f4f6;
|
|
77
|
+
width: 100%;
|
|
78
|
+
padding: 16px;
|
|
79
|
+
margin-bottom: 16px;
|
|
80
|
+
min-height: 72px;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.file {
|
|
84
|
+
display: flex;
|
|
85
|
+
width: 100%;
|
|
86
|
+
position: relative;
|
|
87
|
+
|
|
88
|
+
.status {
|
|
89
|
+
display: flex;
|
|
90
|
+
width: 100%;
|
|
91
|
+
align-items: center;
|
|
92
|
+
|
|
93
|
+
.icon {
|
|
94
|
+
margin-right: 8px;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.progress {
|
|
98
|
+
display: flex;
|
|
99
|
+
width: 100%;
|
|
100
|
+
flex-direction: column;
|
|
101
|
+
justify-content: space-between;
|
|
102
|
+
|
|
103
|
+
.label {
|
|
104
|
+
/* mb-1 */
|
|
105
|
+
margin-bottom: 4px;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.background {
|
|
109
|
+
width: 100%;
|
|
110
|
+
background-color: #e5e7eb;
|
|
111
|
+
border-radius: 9999px;
|
|
112
|
+
height: 6px;
|
|
113
|
+
|
|
114
|
+
.value {
|
|
115
|
+
background-color: #4b5563;
|
|
116
|
+
height: inherit;
|
|
117
|
+
border-radius: inherit;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.close-btn {
|
|
124
|
+
border-radius: 9999px;
|
|
125
|
+
background-color: #6b7280;
|
|
126
|
+
position: absolute;
|
|
127
|
+
top: -24px;
|
|
128
|
+
right: -24px;
|
|
129
|
+
display: flex;
|
|
130
|
+
justify-content: center;
|
|
131
|
+
align-items: center;
|
|
132
|
+
padding: 2px;
|
|
133
|
+
cursor: pointer;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.gdrop-file {
|
|
139
|
+
/* px-6 py-10 border-[2px] border-dashed border-gray-400 w-full rounded-2xl cursor-pointer */
|
|
140
|
+
padding: 40px 24px;
|
|
141
|
+
border-style: dashed;
|
|
142
|
+
border-color: #9ca3af;
|
|
143
|
+
width: 100%;
|
|
144
|
+
border-radius: 16px;
|
|
145
|
+
cursor: pointer;
|
|
146
|
+
|
|
147
|
+
.cloud {
|
|
148
|
+
/* w-full flex flex-col justify-center items-center */
|
|
149
|
+
width: 100%;
|
|
150
|
+
display: flex;
|
|
151
|
+
flex-direction: column;
|
|
152
|
+
justify-content: center;
|
|
153
|
+
align-items: center;
|
|
154
|
+
|
|
155
|
+
.icon {
|
|
156
|
+
/* mb-3 text-gray-400 */
|
|
157
|
+
margin-bottom: 12px;
|
|
158
|
+
color: #9ca3af;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.title {
|
|
162
|
+
/* text-center mb-2 */
|
|
163
|
+
text-align: center;
|
|
164
|
+
margin-bottom: 8px;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.subtitle {
|
|
168
|
+
/* text-color-light text-center */
|
|
169
|
+
color: #A7ADB5;
|
|
170
|
+
text-align: center;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
</style>
|
|
175
|
+
|
|
176
|
+
<script>
|
|
177
|
+
|
|
178
|
+
import Uploader from "../../utils/uploader";
|
|
179
|
+
import { watchEffect } from 'vue';
|
|
180
|
+
import { ref, computed, defineComponent } from 'vue';
|
|
181
|
+
import { VIcon } from 'vuetify/components';
|
|
182
|
+
const makeKey = () => Math.random().toString(36).slice(2, 7);
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class Item {
|
|
186
|
+
constructor(options) {
|
|
187
|
+
this.progress = { value: 0 };
|
|
188
|
+
this.status = options.status;
|
|
189
|
+
this.signedId = options.signedId;
|
|
190
|
+
this._name = options.name;
|
|
191
|
+
this._url = options.url;
|
|
192
|
+
this._type = options.type;
|
|
193
|
+
this.el = options.el;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
isImage() {
|
|
197
|
+
return this.type.startsWith('image');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
get type() {
|
|
201
|
+
if (this._type) {
|
|
202
|
+
return this._type;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (this.el) {
|
|
206
|
+
return this.el.type;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return '';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
get name() {
|
|
213
|
+
if (this._name) {
|
|
214
|
+
return this._name;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (this.el) {
|
|
218
|
+
return this.el.name;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return '';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
get url() {
|
|
225
|
+
if (this._url) {
|
|
226
|
+
return this._url;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return URL.createObjectURL(this.el);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export default defineComponent({
|
|
234
|
+
props: { spec: { type: Object } },
|
|
235
|
+
components: { VIcon },
|
|
236
|
+
setup(props) {
|
|
237
|
+
const spec = props.spec;
|
|
238
|
+
const fileSelect = ref(null);
|
|
239
|
+
const container = ref(null);
|
|
240
|
+
const icon = ref(null);
|
|
241
|
+
|
|
242
|
+
let files = null;
|
|
243
|
+
if (spec.files) {
|
|
244
|
+
files = ref(spec.files.reduce((prev, curr) => {
|
|
245
|
+
const key = makeKey();
|
|
246
|
+
|
|
247
|
+
prev[key] = new Item({
|
|
248
|
+
status: null, // pending, completed, failed
|
|
249
|
+
url: curr.url,
|
|
250
|
+
name: curr.name,
|
|
251
|
+
signedId: curr.signed_id,
|
|
252
|
+
type: curr.type,
|
|
253
|
+
el: null
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
return prev;
|
|
257
|
+
}, {}));
|
|
258
|
+
} else {
|
|
259
|
+
files = ref([]);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const setBusy = (value) => {
|
|
263
|
+
const event = new Event('forms/setBusy', { bubbles: true });
|
|
264
|
+
event.data = { value };
|
|
265
|
+
container.value.dispatchEvent(event);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const uploading = computed(() => Object.values(files.value).reduce((prev, curr) => prev || curr.status == 'pending', false));
|
|
269
|
+
|
|
270
|
+
watchEffect(() => {
|
|
271
|
+
if (!container.value) return null;
|
|
272
|
+
|
|
273
|
+
if (uploading.value) {
|
|
274
|
+
setBusy(true);
|
|
275
|
+
} else {
|
|
276
|
+
setBusy(false);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const showUploadedFile = computed(() => {
|
|
281
|
+
return Object.keys(files.value).length > 0;
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
function uploadFiles(droppedFiles) {
|
|
285
|
+
for (let index = 0; index < droppedFiles.length; index++) {
|
|
286
|
+
// show new dropped file and track progress
|
|
287
|
+
const key = makeKey();
|
|
288
|
+
files.value[key] = new Item({ el: droppedFiles[index], status: 'pending' });
|
|
289
|
+
|
|
290
|
+
const uploader = new Uploader(files.value[key].el, spec.directUploadUrl, files.value[key].progress);
|
|
291
|
+
|
|
292
|
+
// track progress
|
|
293
|
+
uploader.directUploadWillStoreFileWithXHR = (request) => {
|
|
294
|
+
request.upload.addEventListener("progress", event => {
|
|
295
|
+
files.value[key].progress.value = (event.loaded / event.total) * 100;
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// can be use for aborting request
|
|
299
|
+
files.value[key].xhr = request;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// validate per file, skip if invalid
|
|
303
|
+
if (!uploader.validateFile(spec)) {
|
|
304
|
+
delete files.value[key];
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// upload per file
|
|
309
|
+
uploader.start((error, blob) => {
|
|
310
|
+
if (!error) {
|
|
311
|
+
Object.assign(files.value[key], {
|
|
312
|
+
status: 'completed',
|
|
313
|
+
signedId: blob.signed_id
|
|
314
|
+
});
|
|
315
|
+
} else {
|
|
316
|
+
Object.assign(files.value[key], {
|
|
317
|
+
status: 'failed'
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function handleDrop(e) {
|
|
325
|
+
e.preventDefault();
|
|
326
|
+
uploadFiles(e.dataTransfer.files);
|
|
327
|
+
|
|
328
|
+
e.currentTarget.classList.remove('border-[4px]');
|
|
329
|
+
container.value.classList.add('border-[2px]');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function handleRemoveFile(key) {
|
|
333
|
+
// abort upload
|
|
334
|
+
const xhr = files.value[key].xhr;
|
|
335
|
+
if (xhr) {
|
|
336
|
+
xhr.abort();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
delete files.value[key];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function handleDragOver(e) {
|
|
343
|
+
e.preventDefault();
|
|
344
|
+
if (container.value.classList.contains('border-[2px]')) {
|
|
345
|
+
container.value.classList.remove('border-[2px]');
|
|
346
|
+
}
|
|
347
|
+
if (!container.value.classList.contains('border-[4px]')) {
|
|
348
|
+
container.value.classList.add('border-[4px]');
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function handleDragLeave(e) {
|
|
353
|
+
e.preventDefault();
|
|
354
|
+
if (container.value.classList.contains('border-[4px]')) {
|
|
355
|
+
e.currentTarget.classList.remove('border-[4px]');
|
|
356
|
+
}
|
|
357
|
+
if (!container.value.classList.contains('border-[2px]')) {
|
|
358
|
+
container.value.classList.add('border-[2px]');
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function handleClick(e) {
|
|
363
|
+
const onchange = () => {
|
|
364
|
+
uploadFiles(e.target.files);
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
fileSelect.value.onchange = onchange;
|
|
368
|
+
fileSelect.value.click();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const uploadFailedText = spec.uploadFailedText || '(UPLOAD FAILED)';
|
|
372
|
+
const uploadTitle = spec.uploadTitle || 'File added';
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
files,
|
|
376
|
+
fileSelect,
|
|
377
|
+
container,
|
|
378
|
+
icon,
|
|
379
|
+
handleDragLeave,
|
|
380
|
+
handleDragOver,
|
|
381
|
+
handleDrop,
|
|
382
|
+
handleClick,
|
|
383
|
+
handleRemoveFile,
|
|
384
|
+
showUploadedFile,
|
|
385
|
+
uploadFailedText,
|
|
386
|
+
uploadTitle
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
</script>
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
:disabled="inputDisabled" :type="config.type" :rules="rules" :prepend-inner-icon="leftIconName"
|
|
6
6
|
:append-inner-icon="config.appendIcon" :prefix="spec.leftText" :suffix="spec.rightText" :min="spec.min"
|
|
7
7
|
:max="spec.max" :autofocus="spec.autoFocus || false" validate-on="blur" @click:appendInner="onRightIconClick"
|
|
8
|
-
@input="onChange" :variant="variant" persistent-placeholder />
|
|
8
|
+
@input="onChange" :variant="variant" persistent-placeholder @keyup="$onTyping({ duration: 2000 })" />
|
|
9
9
|
</div>
|
|
10
10
|
</template>
|
|
11
11
|
|
|
@@ -30,9 +30,9 @@ export default {
|
|
|
30
30
|
}
|
|
31
31
|
},
|
|
32
32
|
methods: {
|
|
33
|
-
$ready() {
|
|
34
|
-
|
|
35
|
-
},
|
|
33
|
+
// $ready() {
|
|
34
|
+
// this.fieldModel = this.spec.value;
|
|
35
|
+
// },
|
|
36
36
|
styles() {
|
|
37
37
|
const styles = this.$styles();
|
|
38
38
|
this.height = styles.remove("height");
|
package/components/p.vue
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<!-- <p :style="genericStyles()" :class="$classes()" v-html="compiledText" /> -->
|
|
3
|
-
<p :style="genericStyles()" :class="$classes()">{{
|
|
3
|
+
<p :style="genericStyles()" :class="$classes()">{{ text }}</p>
|
|
4
4
|
</template>
|
|
5
5
|
|
|
6
6
|
<script>
|
|
7
7
|
// import marked from "marked";
|
|
8
8
|
|
|
9
9
|
export default {
|
|
10
|
+
expose: ['text'],
|
|
11
|
+
data: function () {
|
|
12
|
+
return {
|
|
13
|
+
text: this.spec.text
|
|
14
|
+
};
|
|
15
|
+
},
|
|
10
16
|
props: {
|
|
11
17
|
spec: { type: Object, required: true }
|
|
12
18
|
}
|
package/index.js
CHANGED
|
@@ -8,17 +8,15 @@ import { useTheme } from "vuetify";
|
|
|
8
8
|
import merge from 'lodash.merge';
|
|
9
9
|
|
|
10
10
|
const Vue = createApp({
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
themeConfig: useTheme()
|
|
15
|
-
};
|
|
11
|
+
setup() {
|
|
12
|
+
const themeConfig = useTheme();
|
|
13
|
+
return { themeConfig, vueApp };
|
|
16
14
|
},
|
|
17
15
|
created() {
|
|
18
16
|
this.themeConfig.themes = merge(this.themeConfig.themes, settings.themes);
|
|
19
17
|
},
|
|
20
18
|
render: function () {
|
|
21
|
-
return h(App, { page: this
|
|
19
|
+
return h(App, { page: this.vueApp.page });
|
|
22
20
|
}
|
|
23
21
|
});
|
|
24
22
|
|
package/nav/dialog.vue
CHANGED
|
@@ -178,17 +178,17 @@ export default {
|
|
|
178
178
|
this.formSpec = response.fullPageForm;
|
|
179
179
|
this.disableCloseButton = this.spec.disableCloseButton || false;
|
|
180
180
|
this.updateContent(response)
|
|
181
|
-
|
|
182
|
-
Action.execute(response.onLoad, this);
|
|
183
181
|
});
|
|
184
182
|
});
|
|
185
183
|
}
|
|
186
184
|
} else {
|
|
187
|
-
this.title = this.spec.title;
|
|
185
|
+
// this.title = this.spec.title;
|
|
186
|
+
// this.body = this.spec.body;
|
|
187
|
+
|
|
188
188
|
this.message = this.spec.message;
|
|
189
|
-
this.body = this.spec.body;
|
|
190
189
|
this.disableCloseButton = this.spec.disableCloseButton || false;
|
|
191
|
-
|
|
190
|
+
|
|
191
|
+
this.updateContent(this.spec);
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
this.model = true;
|
|
@@ -198,6 +198,11 @@ export default {
|
|
|
198
198
|
this.header = response.header;
|
|
199
199
|
this.body = response.body;
|
|
200
200
|
this.footer = response.footer;
|
|
201
|
+
|
|
202
|
+
// Make sure action only executes after the content has finished populating.
|
|
203
|
+
this.$nextTick(() => {
|
|
204
|
+
Action.execute(response.onLoad, this);
|
|
205
|
+
})
|
|
201
206
|
},
|
|
202
207
|
updateMainHeight() {
|
|
203
208
|
this.mainHeight = window.innerHeight - 140;
|
package/package.json
CHANGED
package/store.js
CHANGED
package/utils/form.js
CHANGED
package/utils/http.js
CHANGED
|
@@ -59,7 +59,7 @@ export default class {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
static load(properties, component) {
|
|
62
|
+
static load(properties, component, windowMode) {
|
|
63
63
|
const urlString = properties["url"];
|
|
64
64
|
let url;
|
|
65
65
|
try {
|
|
@@ -80,8 +80,8 @@ export default class {
|
|
|
80
80
|
const htmlUrl = Utils.url.htmlUrl(properties["url"]);
|
|
81
81
|
|
|
82
82
|
Utils.http.execute(properties, "GET", component, (data, response) => {
|
|
83
|
-
const
|
|
84
|
-
if (htmlUrl !== currentUrl &&
|
|
83
|
+
const pushHistory = windowMode ? true : !Utils.type.isObject(component.$closest("dialog"))
|
|
84
|
+
if (htmlUrl !== currentUrl && pushHistory) {
|
|
85
85
|
const redirectUrl = Utils.url.htmlUrl(response.url);
|
|
86
86
|
Utils.history.pushPage(data, redirectUrl);
|
|
87
87
|
}
|
|
@@ -89,7 +89,7 @@ export default class {
|
|
|
89
89
|
this.forceComponentUpdate(() => {
|
|
90
90
|
Utils.history.resetScroll();
|
|
91
91
|
|
|
92
|
-
GLib.action.handleResponse(data, component)
|
|
92
|
+
GLib.action.handleResponse(data, component, windowMode)
|
|
93
93
|
|
|
94
94
|
Action.execute(properties["onOpen"], component);
|
|
95
95
|
});
|
package/utils/launch.js
CHANGED
|
@@ -5,6 +5,7 @@ import Snackbar from "../nav/snackbar.vue";
|
|
|
5
5
|
import Popover from "../components/popover.vue";
|
|
6
6
|
import { computePosition, flip, offset } from '@floating-ui/dom';
|
|
7
7
|
import bus from "./eventBus";
|
|
8
|
+
import { nextTick } from "vue";
|
|
8
9
|
|
|
9
10
|
import { createApp, h } from "vue";
|
|
10
11
|
|
|
@@ -105,7 +106,6 @@ class LaunchDialog {
|
|
|
105
106
|
this.stack = [];
|
|
106
107
|
}
|
|
107
108
|
|
|
108
|
-
|
|
109
109
|
// https://css-tricks.com/creating-vue-js-component-instances-programmatically/
|
|
110
110
|
const props = {
|
|
111
111
|
spec: properties,
|
|
@@ -149,11 +149,9 @@ class LaunchDialog {
|
|
|
149
149
|
dialog.close();
|
|
150
150
|
});
|
|
151
151
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
// Action.execute(it, component);
|
|
156
|
-
// });
|
|
152
|
+
nextTick(() => {
|
|
153
|
+
Action.execute(properties.onClose, component);
|
|
154
|
+
})
|
|
157
155
|
}
|
|
158
156
|
|
|
159
157
|
// This is only meant to be used internally
|