apostrophe 3.6.0 → 3.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/.eslintrc +4 -0
- package/.github/workflows/main.yml +45 -0
- package/CHANGELOG.md +92 -3
- package/README.md +2 -3
- package/index.js +104 -3
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +5 -1
- package/modules/@apostrophecms/area/ui/apos/apps/AposAreas.js +15 -10
- package/modules/@apostrophecms/asset/index.js +105 -15
- package/modules/@apostrophecms/attachment/index.js +1 -4
- package/modules/@apostrophecms/db/index.js +5 -6
- package/modules/@apostrophecms/doc/index.js +2 -0
- package/modules/@apostrophecms/doc-type/index.js +39 -16
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue +13 -1
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +0 -1
- package/modules/@apostrophecms/i18n/i18n/en.json +23 -4
- package/modules/@apostrophecms/i18n/i18n/es.json +1 -2
- package/modules/@apostrophecms/i18n/i18n/pt-BR.json +0 -1
- package/modules/@apostrophecms/i18n/i18n/sk.json +3 -4
- package/modules/@apostrophecms/i18n/index.js +36 -6
- package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +6 -3
- package/modules/@apostrophecms/image-widget/index.js +2 -1
- package/modules/@apostrophecms/image-widget/views/widget.html +12 -2
- package/modules/@apostrophecms/job/index.js +165 -220
- package/modules/@apostrophecms/login/index.js +0 -15
- package/modules/@apostrophecms/migration/index.js +1 -1
- package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +151 -61
- package/modules/@apostrophecms/modal/ui/apos/components/AposModalConfirm.vue +8 -6
- package/modules/@apostrophecms/modal/ui/apos/mixins/AposDocsManagerMixin.js +12 -15
- package/modules/@apostrophecms/module/index.js +1 -4
- package/modules/@apostrophecms/notification/index.js +116 -8
- package/modules/@apostrophecms/notification/ui/apos/components/AposNotification.vue +89 -11
- package/modules/@apostrophecms/notification/ui/apos/components/TheAposNotifications.vue +1 -1
- package/modules/@apostrophecms/page/index.js +84 -52
- package/modules/@apostrophecms/page-type/index.js +5 -1
- package/modules/@apostrophecms/piece-type/index.js +183 -61
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +180 -50
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerDisplay.vue +1 -3
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerSelectBox.vue +141 -0
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +35 -6
- package/modules/@apostrophecms/schema/index.js +81 -25
- package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +9 -3
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputAttachment.vue +11 -160
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputPassword.vue +11 -4
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputRange.vue +2 -2
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue +24 -6
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +0 -4
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +0 -7
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +0 -2
- package/modules/@apostrophecms/schema/ui/apos/components/AposLogo.vue +1 -1
- package/modules/@apostrophecms/schema/ui/apos/components/AposLogoIcon.vue +1 -1
- package/modules/@apostrophecms/schema/ui/apos/components/AposLogoPadless.vue +1 -1
- package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +0 -1
- package/modules/@apostrophecms/search/index.js +53 -33
- package/modules/@apostrophecms/task/index.js +7 -3
- package/modules/@apostrophecms/template/index.js +7 -11
- package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +5 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposCellContextMenu.vue +1 -1
- package/modules/@apostrophecms/ui/ui/apos/components/AposFile.vue +205 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposMinMaxCount.vue +9 -3
- package/modules/@apostrophecms/ui/ui/apos/lib/i18next.js +16 -2
- package/modules/@apostrophecms/ui/ui/apos/mixins/AposPublishMixin.js +3 -2
- package/modules/@apostrophecms/ui/ui/apos/scss/global/_tables.scss +4 -3
- package/modules/@apostrophecms/util/ui/src/util.js +15 -0
- package/modules/@apostrophecms/widget-type/index.js +1 -1
- package/modules/@apostrophecms/widget-type/ui/apos/mixins/AposWidgetMixin.js +5 -19
- package/package.json +2 -2
- package/test/job.js +224 -0
- package/test/pieces.js +34 -0
- package/test-lib/util.js +32 -0
- package/.circleci/config.yml +0 -94
- package/.scratch.md +0 -2
|
@@ -28,7 +28,7 @@ import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin'
|
|
|
28
28
|
export default {
|
|
29
29
|
name: 'AposInputPassword',
|
|
30
30
|
mixins: [ AposInputMixin ],
|
|
31
|
-
emits: ['return'],
|
|
31
|
+
emits: [ 'return' ],
|
|
32
32
|
computed: {
|
|
33
33
|
tabindex () {
|
|
34
34
|
return this.field.disableFocus ? '-1' : '0';
|
|
@@ -43,12 +43,20 @@ export default {
|
|
|
43
43
|
}
|
|
44
44
|
if (this.field.min) {
|
|
45
45
|
if (value.length && (value.length < this.field.min)) {
|
|
46
|
-
return {
|
|
46
|
+
return {
|
|
47
|
+
message: this.$t('apostrophe:passwordErrorMin', {
|
|
48
|
+
min: this.field.min
|
|
49
|
+
})
|
|
50
|
+
};
|
|
47
51
|
}
|
|
48
52
|
}
|
|
49
53
|
if (this.field.max) {
|
|
50
54
|
if (value.length && (value.length > this.field.max)) {
|
|
51
|
-
return {
|
|
55
|
+
return {
|
|
56
|
+
message: this.$t('apostrophe:passwordErrorMax', {
|
|
57
|
+
max: this.field.max
|
|
58
|
+
})
|
|
59
|
+
};
|
|
52
60
|
}
|
|
53
61
|
}
|
|
54
62
|
return false;
|
|
@@ -56,4 +64,3 @@ export default {
|
|
|
56
64
|
}
|
|
57
65
|
};
|
|
58
66
|
</script>
|
|
59
|
-
|
|
@@ -20,13 +20,13 @@
|
|
|
20
20
|
<div class="apos-range__scale">
|
|
21
21
|
<span>
|
|
22
22
|
<span class="apos-sr-only">
|
|
23
|
-
|
|
23
|
+
{{ $t('apostrophe:minLabel') }}
|
|
24
24
|
</span>
|
|
25
25
|
{{ minLabel }}
|
|
26
26
|
</span>
|
|
27
27
|
<span>
|
|
28
28
|
<span class="apos-sr-only">
|
|
29
|
-
|
|
29
|
+
{{ $t('apostrophe:maxLabel') }}
|
|
30
30
|
</span>
|
|
31
31
|
{{ maxLabel }}
|
|
32
32
|
</span>
|
|
@@ -49,9 +49,27 @@ export default {
|
|
|
49
49
|
choices: []
|
|
50
50
|
};
|
|
51
51
|
},
|
|
52
|
-
mounted() {
|
|
52
|
+
async mounted() {
|
|
53
|
+
let choices;
|
|
54
|
+
if (typeof this.field.choices === 'string') {
|
|
55
|
+
const action = this.options.action;
|
|
56
|
+
const response = await apos.http.get(
|
|
57
|
+
`${action}/choices`,
|
|
58
|
+
{
|
|
59
|
+
qs: {
|
|
60
|
+
fieldId: this.field._id
|
|
61
|
+
},
|
|
62
|
+
busy: true
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
if (response.choices) {
|
|
66
|
+
choices = response.choices;
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
choices = this.field.choices;
|
|
70
|
+
}
|
|
53
71
|
// Add an null option if there isn't one already
|
|
54
|
-
if (!this.field.required && !
|
|
72
|
+
if (!this.field.required && !choices.find(choice => {
|
|
55
73
|
return choice.value === null;
|
|
56
74
|
})) {
|
|
57
75
|
this.choices.push({
|
|
@@ -59,12 +77,12 @@ export default {
|
|
|
59
77
|
value: null
|
|
60
78
|
});
|
|
61
79
|
}
|
|
62
|
-
this.choices = this.choices.concat(
|
|
80
|
+
this.choices = this.choices.concat(choices);
|
|
63
81
|
this.$nextTick(() => {
|
|
64
82
|
// this has to happen on nextTick to avoid emitting before schemaReady is
|
|
65
83
|
// set in AposSchema
|
|
66
|
-
if (this.field.required && (this.next == null) && (this.
|
|
67
|
-
this.next = this.
|
|
84
|
+
if (this.field.required && (this.next == null) && (this.choices[0] != null)) {
|
|
85
|
+
this.next = this.choices[0].value;
|
|
68
86
|
}
|
|
69
87
|
});
|
|
70
88
|
},
|
|
@@ -74,7 +92,7 @@ export default {
|
|
|
74
92
|
return 'required';
|
|
75
93
|
}
|
|
76
94
|
|
|
77
|
-
if (value && !this.
|
|
95
|
+
if (value && !this.choices.find(choice => choice.value === value)) {
|
|
78
96
|
return 'invalid';
|
|
79
97
|
}
|
|
80
98
|
|
|
@@ -73,10 +73,6 @@ export default {
|
|
|
73
73
|
icon () {
|
|
74
74
|
if (this.error) {
|
|
75
75
|
return 'circle-medium-icon';
|
|
76
|
-
} else if (this.field.type === 'date') {
|
|
77
|
-
return 'calendar-icon';
|
|
78
|
-
} else if (this.field.type === 'time') {
|
|
79
|
-
return 'clock-icon';
|
|
80
76
|
} else if (this.field.icon) {
|
|
81
77
|
return this.field.icon;
|
|
82
78
|
} else {
|
|
@@ -71,10 +71,6 @@ export default {
|
|
|
71
71
|
icon () {
|
|
72
72
|
if (this.error) {
|
|
73
73
|
return 'circle-medium-icon';
|
|
74
|
-
} else if (this.field.type === 'date') {
|
|
75
|
-
return 'calendar-icon';
|
|
76
|
-
} else if (this.field.type === 'time') {
|
|
77
|
-
return 'clock-icon';
|
|
78
74
|
} else if (this.field.icon) {
|
|
79
75
|
return this.field.icon;
|
|
80
76
|
} else {
|
|
@@ -207,9 +203,6 @@ export default {
|
|
|
207
203
|
// height of date/time input is slightly larger than others due to the browser spinner ui
|
|
208
204
|
height: 46px;
|
|
209
205
|
padding-right: 40px;
|
|
210
|
-
&::-webkit-calendar-picker-indicator {
|
|
211
|
-
background: none;
|
|
212
|
-
}
|
|
213
206
|
}
|
|
214
207
|
.apos-input--date {
|
|
215
208
|
&::-webkit-clear-button {
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
<div class="apos-field__wrapper">
|
|
3
3
|
<component :is="wrapEl" :class="classList">
|
|
4
4
|
<div class="apos-field__info">
|
|
5
|
-
<!-- TODO i18n -->
|
|
6
5
|
<component
|
|
7
6
|
v-if="field.label" :class="{'apos-sr-only': field.hideLabel }"
|
|
8
7
|
class="apos-field__label"
|
|
@@ -32,7 +31,6 @@
|
|
|
32
31
|
/>
|
|
33
32
|
</span>
|
|
34
33
|
</component>
|
|
35
|
-
<!-- TODO i18n -->
|
|
36
34
|
<p
|
|
37
35
|
v-if="(field.help || field.htmlHelp) && !displayOptions.helpTooltip"
|
|
38
36
|
class="apos-field__help"
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
// are edge cases not relevant enough to explicitly offer a filter for, but
|
|
45
45
|
// which should nevertheless be included in results.
|
|
46
46
|
|
|
47
|
+
const { stripIndent } = require('common-tags');
|
|
47
48
|
const _ = require('lodash');
|
|
48
49
|
|
|
49
50
|
module.exports = {
|
|
@@ -98,37 +99,7 @@ module.exports = {
|
|
|
98
99
|
},
|
|
99
100
|
'@apostrophecms/doc-type:beforeSave': {
|
|
100
101
|
indexDoc(req, doc) {
|
|
101
|
-
|
|
102
|
-
const texts = self.getSearchTexts(doc);
|
|
103
|
-
|
|
104
|
-
_.each(texts, function (text) {
|
|
105
|
-
if (text.text === undefined) {
|
|
106
|
-
text.text = '';
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
const highTexts = _.filter(texts, function (text) {
|
|
111
|
-
return text.weight > 10;
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
const searchSummary = _.map(_.filter(texts, function (text) {
|
|
115
|
-
return !text.silent;
|
|
116
|
-
}), function (text) {
|
|
117
|
-
return text.text;
|
|
118
|
-
}).join(' ');
|
|
119
|
-
const highText = self.boilTexts(highTexts);
|
|
120
|
-
const lowText = self.boilTexts(texts);
|
|
121
|
-
const titleSortified = self.apos.util.sortify(doc.title);
|
|
122
|
-
const highWords = _.uniq(highText.split(/ /));
|
|
123
|
-
|
|
124
|
-
// merge our doc with its various search texts
|
|
125
|
-
_.assign(doc, {
|
|
126
|
-
titleSortified: titleSortified,
|
|
127
|
-
highSearchText: highText,
|
|
128
|
-
highSearchWords: highWords,
|
|
129
|
-
lowSearchText: lowText,
|
|
130
|
-
searchSummary: searchSummary
|
|
131
|
-
});
|
|
102
|
+
self.indexDoc(req, doc);
|
|
132
103
|
}
|
|
133
104
|
}
|
|
134
105
|
};
|
|
@@ -261,13 +232,58 @@ module.exports = {
|
|
|
261
232
|
self.dispatch('/', self.indexPage);
|
|
262
233
|
},
|
|
263
234
|
|
|
235
|
+
indexDoc(req, doc) {
|
|
236
|
+
|
|
237
|
+
const texts = self.getSearchTexts(doc);
|
|
238
|
+
|
|
239
|
+
_.each(texts, function (text) {
|
|
240
|
+
if (text.text === undefined) {
|
|
241
|
+
text.text = '';
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const highTexts = _.filter(texts, function (text) {
|
|
246
|
+
return text.weight > 10;
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const searchSummary = _.map(_.filter(texts, function (text) {
|
|
250
|
+
return !text.silent;
|
|
251
|
+
}), function (text) {
|
|
252
|
+
return text.text;
|
|
253
|
+
}).join(' ');
|
|
254
|
+
const highText = self.boilTexts(highTexts);
|
|
255
|
+
const lowText = self.boilTexts(texts);
|
|
256
|
+
const titleSortified = self.apos.util.sortify(doc.title);
|
|
257
|
+
const highWords = _.uniq(highText.split(/ /));
|
|
258
|
+
|
|
259
|
+
// merge our doc with its various search texts
|
|
260
|
+
_.assign(doc, {
|
|
261
|
+
titleSortified: titleSortified,
|
|
262
|
+
highSearchText: highText,
|
|
263
|
+
highSearchWords: highWords,
|
|
264
|
+
lowSearchText: lowText,
|
|
265
|
+
searchSummary: searchSummary
|
|
266
|
+
});
|
|
267
|
+
},
|
|
268
|
+
|
|
264
269
|
// Indexes just one document as part of the implementation of the
|
|
265
270
|
// `@apostrophecms/search:index` task. This isn't the method you want to
|
|
266
271
|
// override. See `indexDoc` and `getSearchTexts`
|
|
267
272
|
|
|
268
273
|
async indexTaskOne(req, doc) {
|
|
269
274
|
self.indexDoc(req, doc);
|
|
270
|
-
|
|
275
|
+
|
|
276
|
+
return self.apos.doc.db.updateOne({
|
|
277
|
+
_id: doc._id
|
|
278
|
+
}, {
|
|
279
|
+
$set: {
|
|
280
|
+
titleSortified: doc.titleSortified,
|
|
281
|
+
highSearchText: doc.highSearchText,
|
|
282
|
+
highSearchWords: doc.highSearchWords,
|
|
283
|
+
lowSearchText: doc.lowSearchText,
|
|
284
|
+
searchSummary: doc.searchSummary
|
|
285
|
+
}
|
|
286
|
+
});
|
|
271
287
|
},
|
|
272
288
|
|
|
273
289
|
// Returns texts which are a reasonable basis for
|
|
@@ -348,7 +364,11 @@ module.exports = {
|
|
|
348
364
|
tasks(self) {
|
|
349
365
|
return {
|
|
350
366
|
index: {
|
|
351
|
-
usage:
|
|
367
|
+
usage: stripIndent`
|
|
368
|
+
Rebuild the search index. Normally this happens automatically.
|
|
369
|
+
This should only be needed if you have changed the"searchable" property
|
|
370
|
+
for various fields or types.
|
|
371
|
+
`,
|
|
352
372
|
task(argv) {
|
|
353
373
|
const req = self.apos.task.getReq();
|
|
354
374
|
return self.apos.migration.eachDoc({}, _.partial(self.indexTaskOne, req));
|
|
@@ -190,7 +190,11 @@ module.exports = {
|
|
|
190
190
|
role: options.role
|
|
191
191
|
}
|
|
192
192
|
}),
|
|
193
|
-
res: {
|
|
193
|
+
res: {
|
|
194
|
+
redirect(url) {
|
|
195
|
+
req.res.redirectedTo = url;
|
|
196
|
+
}
|
|
197
|
+
},
|
|
194
198
|
t(key, options = {}) {
|
|
195
199
|
return self.apos.i18n.i18next.t(key, {
|
|
196
200
|
...options,
|
|
@@ -219,8 +223,8 @@ module.exports = {
|
|
|
219
223
|
};
|
|
220
224
|
addCloneMethod(req);
|
|
221
225
|
req.res.__ = req.__;
|
|
222
|
-
const {
|
|
223
|
-
Object.assign(req,
|
|
226
|
+
const { _role, ...properties } = options || {};
|
|
227
|
+
Object.assign(req, properties);
|
|
224
228
|
self.apos.i18n.setPrefixUrls(req);
|
|
225
229
|
return req;
|
|
226
230
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Implements template rendering via Nunjucks. **You should use the
|
|
2
|
-
// `self.render`
|
|
2
|
+
// `self.render` method of *your own* module**,
|
|
3
3
|
// which exist courtesy of [@apostrophecms/module](../@apostrophecms/module/index.html)
|
|
4
4
|
// and invoke methods of this module more conveniently for you.
|
|
5
5
|
//
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// you have a custom version of Nunjucks that is compatible.
|
|
22
22
|
//
|
|
23
23
|
// ### `viewsFolderFallback`: specifies a folder to be checked for templates
|
|
24
|
-
// if they are not found in the module that called `self.render`
|
|
24
|
+
// if they are not found in the module that called `self.render`
|
|
25
25
|
// or those it extends. This is a handy place for project-wide macro files.
|
|
26
26
|
// Often set to `__dirname + '/views'` in `app.js`.
|
|
27
27
|
|
|
@@ -203,7 +203,7 @@ module.exports = {
|
|
|
203
203
|
|
|
204
204
|
async renderForModule(req, name, data, module) {
|
|
205
205
|
if (typeof req !== 'object') {
|
|
206
|
-
throw new Error('The first argument to module.render must be req.
|
|
206
|
+
throw new Error('The first argument to module.render must be req.');
|
|
207
207
|
}
|
|
208
208
|
return self.renderBody(req, 'file', name, data, module);
|
|
209
209
|
},
|
|
@@ -214,7 +214,7 @@ module.exports = {
|
|
|
214
214
|
|
|
215
215
|
async renderStringForModule(req, s, data, module) {
|
|
216
216
|
if (typeof req !== 'object') {
|
|
217
|
-
throw new Error('The first argument to module.render must be req.
|
|
217
|
+
throw new Error('The first argument to module.render must be req.');
|
|
218
218
|
}
|
|
219
219
|
return self.renderBody(req, 'string', s, data, module);
|
|
220
220
|
},
|
|
@@ -289,12 +289,6 @@ module.exports = {
|
|
|
289
289
|
|
|
290
290
|
args.data = merged;
|
|
291
291
|
|
|
292
|
-
// // Allows templates to render other templates in an independent
|
|
293
|
-
// // nunjucks environment, rather than including them
|
|
294
|
-
// args.partial = function(name, data) {
|
|
295
|
-
// return self.partialForModule(name, data, module);
|
|
296
|
-
// };
|
|
297
|
-
|
|
298
292
|
if (req.data) {
|
|
299
293
|
_.defaults(merged, req.data);
|
|
300
294
|
}
|
|
@@ -334,7 +328,7 @@ module.exports = {
|
|
|
334
328
|
|
|
335
329
|
// Fetch a nunjucks environment in which `include`, `extends`, etc. search
|
|
336
330
|
// the views directories of the specified module and its ancestors.
|
|
337
|
-
// Typically you will call `self.render`
|
|
331
|
+
// Typically you will call `self.render` on your module
|
|
338
332
|
// object rather than calling this directly.
|
|
339
333
|
//
|
|
340
334
|
// `req` is effectively here for bc purposes only. This method
|
|
@@ -652,6 +646,8 @@ module.exports = {
|
|
|
652
646
|
locale: req.locale,
|
|
653
647
|
csrfCookieName: self.apos.csrfCookieName,
|
|
654
648
|
tabId: self.apos.util.generateId(),
|
|
649
|
+
uploadsUrl: self.apos.attachment.uploadfs.getUrl(),
|
|
650
|
+
assetBaseUrl: self.apos.asset.getAssetBaseUrl(),
|
|
655
651
|
scene
|
|
656
652
|
};
|
|
657
653
|
if (req.user) {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
:type="buttonType"
|
|
16
16
|
:role="role"
|
|
17
17
|
:id="attrs.id ? attrs.id : id"
|
|
18
|
+
:style="{color: textColor}"
|
|
18
19
|
v-bind="attrs"
|
|
19
20
|
>
|
|
20
21
|
<transition name="fade">
|
|
@@ -71,6 +72,10 @@ export default {
|
|
|
71
72
|
type: String,
|
|
72
73
|
default: null
|
|
73
74
|
},
|
|
75
|
+
textColor: {
|
|
76
|
+
type: String,
|
|
77
|
+
default: null
|
|
78
|
+
},
|
|
74
79
|
href: {
|
|
75
80
|
type: [ String, Boolean ],
|
|
76
81
|
default: false
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<label
|
|
4
|
+
class="apos-input-wrapper apos-file-dropzone"
|
|
5
|
+
:class="{
|
|
6
|
+
'apos-file-dropzone--dragover': dragging,
|
|
7
|
+
'apos-is-disabled': disabled || fileOrAttachment
|
|
8
|
+
}"
|
|
9
|
+
@drop.prevent="uploadFile"
|
|
10
|
+
@dragover="dragHandler"
|
|
11
|
+
@dragleave="dragging = false"
|
|
12
|
+
>
|
|
13
|
+
<p class="apos-file-instructions">
|
|
14
|
+
<template v-if="dragging">
|
|
15
|
+
<cloud-upload-icon :size="38" />
|
|
16
|
+
</template>
|
|
17
|
+
<AposSpinner v-else-if="uploading" />
|
|
18
|
+
<template v-else>
|
|
19
|
+
<paperclip-icon :size="14" class="apos-file-icon" />
|
|
20
|
+
{{ messages.primary }}
|
|
21
|
+
<span class="apos-file-highlight" v-if="messages.highlighted">
|
|
22
|
+
{{ messages.highlighted }}
|
|
23
|
+
</span>
|
|
24
|
+
</template>
|
|
25
|
+
</p>
|
|
26
|
+
<input
|
|
27
|
+
type="file"
|
|
28
|
+
class="apos-sr-only"
|
|
29
|
+
:disabled="disabled || fileOrAttachment"
|
|
30
|
+
@input="uploadFile"
|
|
31
|
+
:accept="allowedExtensions"
|
|
32
|
+
>
|
|
33
|
+
</label>
|
|
34
|
+
<div v-if="fileOrAttachment" class="apos-file-files">
|
|
35
|
+
<AposSlatList
|
|
36
|
+
:value="[fileOrAttachment]"
|
|
37
|
+
@input="update"
|
|
38
|
+
:disabled="readOnly"
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</template>
|
|
43
|
+
|
|
44
|
+
<script>
|
|
45
|
+
export default {
|
|
46
|
+
props: {
|
|
47
|
+
uploading: {
|
|
48
|
+
type: Boolean,
|
|
49
|
+
default: false
|
|
50
|
+
},
|
|
51
|
+
disabled: {
|
|
52
|
+
type: Boolean,
|
|
53
|
+
default: false
|
|
54
|
+
},
|
|
55
|
+
attachment: {
|
|
56
|
+
type: Object,
|
|
57
|
+
default: null
|
|
58
|
+
},
|
|
59
|
+
allowedExtensions: {
|
|
60
|
+
type: String,
|
|
61
|
+
default: '*'
|
|
62
|
+
},
|
|
63
|
+
readOnly: {
|
|
64
|
+
type: Boolean,
|
|
65
|
+
default: false
|
|
66
|
+
},
|
|
67
|
+
def: {
|
|
68
|
+
type: String,
|
|
69
|
+
default: null
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
emits: [ 'upload-file', 'update' ],
|
|
73
|
+
data () {
|
|
74
|
+
return {
|
|
75
|
+
selectedFile: null,
|
|
76
|
+
dragging: false
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
computed: {
|
|
80
|
+
fileOrAttachment () {
|
|
81
|
+
return this.selectedFile || this.attachment;
|
|
82
|
+
},
|
|
83
|
+
messages () {
|
|
84
|
+
const msgs = {
|
|
85
|
+
primary: 'Drop a file here or',
|
|
86
|
+
highlighted: 'click to open the file explorer'
|
|
87
|
+
};
|
|
88
|
+
if (this.disabled) {
|
|
89
|
+
msgs.primary = 'Field is disabled';
|
|
90
|
+
msgs.highlighted = '';
|
|
91
|
+
}
|
|
92
|
+
if (this.fileOrAttachment) {
|
|
93
|
+
msgs.primary = 'Attachment limit reached';
|
|
94
|
+
msgs.highlighted = '';
|
|
95
|
+
}
|
|
96
|
+
return msgs;
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
methods: {
|
|
100
|
+
async uploadFile ({ target, dataTransfer }) {
|
|
101
|
+
this.dragging = false;
|
|
102
|
+
const [ file ] = target.files ? target.files : (dataTransfer.files || []);
|
|
103
|
+
|
|
104
|
+
const extension = file.name.split('.').pop();
|
|
105
|
+
const allowedFile = await this.checkFileGroup(`.${extension}`);
|
|
106
|
+
|
|
107
|
+
if (!allowedFile) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.selectedFile = {
|
|
112
|
+
_id: file.name,
|
|
113
|
+
title: file.name,
|
|
114
|
+
extension,
|
|
115
|
+
_url: URL.createObjectURL(file)
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
this.$emit('upload-file', file);
|
|
119
|
+
},
|
|
120
|
+
dragHandler (event) {
|
|
121
|
+
event.preventDefault();
|
|
122
|
+
|
|
123
|
+
if (!this.disabled && !this.dragging) {
|
|
124
|
+
this.dragging = true;
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
update(items) {
|
|
128
|
+
if (this.selectedFile && this.selectedFile._url) {
|
|
129
|
+
URL.revokeObjectURL(this.selectedFile._url);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this.selectedFile = null;
|
|
133
|
+
this.$emit('update', items);
|
|
134
|
+
},
|
|
135
|
+
async checkFileGroup(fileExt) {
|
|
136
|
+
const allowedExt = this.allowedExtensions.split(',');
|
|
137
|
+
const allowed = allowedExt.includes(fileExt);
|
|
138
|
+
|
|
139
|
+
if (!allowed) {
|
|
140
|
+
await apos.notify('apostrophe:fileTypeNotAccepted', {
|
|
141
|
+
type: 'warning',
|
|
142
|
+
icon: 'alert-circle-icon',
|
|
143
|
+
interpolate: {
|
|
144
|
+
extensions: this.allowedExtensions
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return allowed;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
</script>
|
|
154
|
+
<style scoped lang='scss'>
|
|
155
|
+
.apos-file-dropzone {
|
|
156
|
+
@include apos-button-reset();
|
|
157
|
+
@include type-base;
|
|
158
|
+
display: block;
|
|
159
|
+
margin: 10px 0;
|
|
160
|
+
padding: 20px;
|
|
161
|
+
border: 2px dashed var(--a-base-8);
|
|
162
|
+
border-radius: var(--a-border-radius);
|
|
163
|
+
transition: all 0.2s ease;
|
|
164
|
+
&:hover {
|
|
165
|
+
border-color: var(--a-primary);
|
|
166
|
+
background-color: var(--a-base-10);
|
|
167
|
+
}
|
|
168
|
+
&:active,
|
|
169
|
+
&:focus {
|
|
170
|
+
border: 2px solid var(--a-primary);
|
|
171
|
+
}
|
|
172
|
+
&.apos-is-disabled {
|
|
173
|
+
color: var(--a-base-4);
|
|
174
|
+
background-color: var(--a-base-7);
|
|
175
|
+
border-color: var(--a-base-4);
|
|
176
|
+
|
|
177
|
+
&:hover {
|
|
178
|
+
cursor: not-allowed;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.apos-file-dropzone--dragover {
|
|
184
|
+
border: 2px dashed var(--a-primary);
|
|
185
|
+
background-color: var(--a-base-10);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.apos-file-instructions {
|
|
189
|
+
display: flex;
|
|
190
|
+
flex-wrap: wrap;
|
|
191
|
+
align-items: center;
|
|
192
|
+
justify-content: center;
|
|
193
|
+
pointer-events: none;
|
|
194
|
+
// v-html goofiness
|
|
195
|
+
& ::v-deep .apos-file-highlight {
|
|
196
|
+
color: var(--a-primary);
|
|
197
|
+
font-weight: var(--a-weight-bold);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.apos-file-icon {
|
|
202
|
+
transform: rotate(45deg);
|
|
203
|
+
margin-right: 5px;
|
|
204
|
+
}
|
|
205
|
+
</style>
|
|
@@ -61,7 +61,9 @@ export default {
|
|
|
61
61
|
return maxError;
|
|
62
62
|
},
|
|
63
63
|
countLabel() {
|
|
64
|
-
return
|
|
64
|
+
return this.$t('apostrophe:numberAdded', {
|
|
65
|
+
count: this.value.length
|
|
66
|
+
});
|
|
65
67
|
},
|
|
66
68
|
// Here in the array editor we use effectiveMin to factor in the
|
|
67
69
|
// required property because there is no other good place to do that,
|
|
@@ -69,14 +71,18 @@ export default {
|
|
|
69
71
|
// representation of "required".
|
|
70
72
|
minLabel() {
|
|
71
73
|
if (this.effectiveMin) {
|
|
72
|
-
return
|
|
74
|
+
return this.$t('apostrophe:minUi', {
|
|
75
|
+
number: this.effectiveMin
|
|
76
|
+
});
|
|
73
77
|
} else {
|
|
74
78
|
return false;
|
|
75
79
|
}
|
|
76
80
|
},
|
|
77
81
|
maxLabel() {
|
|
78
82
|
if ((typeof this.field.max) === 'number') {
|
|
79
|
-
return
|
|
83
|
+
return this.$t('apostrophe:maxUi', {
|
|
84
|
+
number: this.field.max
|
|
85
|
+
});
|
|
80
86
|
} else {
|
|
81
87
|
return false;
|
|
82
88
|
}
|