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.
Files changed (71) hide show
  1. package/.eslintrc +4 -0
  2. package/.github/workflows/main.yml +45 -0
  3. package/CHANGELOG.md +92 -3
  4. package/README.md +2 -3
  5. package/index.js +104 -3
  6. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +5 -1
  7. package/modules/@apostrophecms/area/ui/apos/apps/AposAreas.js +15 -10
  8. package/modules/@apostrophecms/asset/index.js +105 -15
  9. package/modules/@apostrophecms/attachment/index.js +1 -4
  10. package/modules/@apostrophecms/db/index.js +5 -6
  11. package/modules/@apostrophecms/doc/index.js +2 -0
  12. package/modules/@apostrophecms/doc-type/index.js +39 -16
  13. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue +13 -1
  14. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +0 -1
  15. package/modules/@apostrophecms/i18n/i18n/en.json +23 -4
  16. package/modules/@apostrophecms/i18n/i18n/es.json +1 -2
  17. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +0 -1
  18. package/modules/@apostrophecms/i18n/i18n/sk.json +3 -4
  19. package/modules/@apostrophecms/i18n/index.js +36 -6
  20. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +6 -3
  21. package/modules/@apostrophecms/image-widget/index.js +2 -1
  22. package/modules/@apostrophecms/image-widget/views/widget.html +12 -2
  23. package/modules/@apostrophecms/job/index.js +165 -220
  24. package/modules/@apostrophecms/login/index.js +0 -15
  25. package/modules/@apostrophecms/migration/index.js +1 -1
  26. package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +151 -61
  27. package/modules/@apostrophecms/modal/ui/apos/components/AposModalConfirm.vue +8 -6
  28. package/modules/@apostrophecms/modal/ui/apos/mixins/AposDocsManagerMixin.js +12 -15
  29. package/modules/@apostrophecms/module/index.js +1 -4
  30. package/modules/@apostrophecms/notification/index.js +116 -8
  31. package/modules/@apostrophecms/notification/ui/apos/components/AposNotification.vue +89 -11
  32. package/modules/@apostrophecms/notification/ui/apos/components/TheAposNotifications.vue +1 -1
  33. package/modules/@apostrophecms/page/index.js +84 -52
  34. package/modules/@apostrophecms/page-type/index.js +5 -1
  35. package/modules/@apostrophecms/piece-type/index.js +183 -61
  36. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +180 -50
  37. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerDisplay.vue +1 -3
  38. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerSelectBox.vue +141 -0
  39. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +35 -6
  40. package/modules/@apostrophecms/schema/index.js +81 -25
  41. package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +9 -3
  42. package/modules/@apostrophecms/schema/ui/apos/components/AposInputAttachment.vue +11 -160
  43. package/modules/@apostrophecms/schema/ui/apos/components/AposInputPassword.vue +11 -4
  44. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRange.vue +2 -2
  45. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue +24 -6
  46. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +0 -4
  47. package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +0 -7
  48. package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +0 -2
  49. package/modules/@apostrophecms/schema/ui/apos/components/AposLogo.vue +1 -1
  50. package/modules/@apostrophecms/schema/ui/apos/components/AposLogoIcon.vue +1 -1
  51. package/modules/@apostrophecms/schema/ui/apos/components/AposLogoPadless.vue +1 -1
  52. package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +0 -1
  53. package/modules/@apostrophecms/search/index.js +53 -33
  54. package/modules/@apostrophecms/task/index.js +7 -3
  55. package/modules/@apostrophecms/template/index.js +7 -11
  56. package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +5 -0
  57. package/modules/@apostrophecms/ui/ui/apos/components/AposCellContextMenu.vue +1 -1
  58. package/modules/@apostrophecms/ui/ui/apos/components/AposFile.vue +205 -0
  59. package/modules/@apostrophecms/ui/ui/apos/components/AposMinMaxCount.vue +9 -3
  60. package/modules/@apostrophecms/ui/ui/apos/lib/i18next.js +16 -2
  61. package/modules/@apostrophecms/ui/ui/apos/mixins/AposPublishMixin.js +3 -2
  62. package/modules/@apostrophecms/ui/ui/apos/scss/global/_tables.scss +4 -3
  63. package/modules/@apostrophecms/util/ui/src/util.js +15 -0
  64. package/modules/@apostrophecms/widget-type/index.js +1 -1
  65. package/modules/@apostrophecms/widget-type/ui/apos/mixins/AposWidgetMixin.js +5 -19
  66. package/package.json +2 -2
  67. package/test/job.js +224 -0
  68. package/test/pieces.js +34 -0
  69. package/test-lib/util.js +32 -0
  70. package/.circleci/config.yml +0 -94
  71. 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 { message: `Minimum of ${this.field.min} characters` };
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 { message: `Maximum of ${this.field.max} characters` };
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
- Min:
23
+ {{ $t('apostrophe:minLabel') }}
24
24
  </span>
25
25
  {{ minLabel }}
26
26
  </span>
27
27
  <span>
28
28
  <span class="apos-sr-only">
29
- Max:
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 && !this.field.choices.find(choice => {
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(this.field.choices);
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.field.choices[0] != null)) {
67
- this.next = this.field.choices[0].value;
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.field.choices.find(choice => choice.value === value)) {
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"
@@ -60,4 +60,4 @@
60
60
  fill="url(#triangle)"
61
61
  />
62
62
  </svg>
63
- </template>
63
+ </template>
@@ -57,4 +57,4 @@
57
57
  fill="url(#triangle)"
58
58
  />
59
59
  </svg>
60
- </template>
60
+ </template>
@@ -54,4 +54,4 @@
54
54
  fill="url(#triangle)"
55
55
  />
56
56
  </svg>
57
- </template>
57
+ </template>
@@ -148,6 +148,5 @@ export default {
148
148
  &.apos-search__item--disabled {
149
149
  @include disabled;
150
150
  }
151
-
152
151
  }
153
152
  </style>
@@ -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
- return self.apos.doc.db.updateOne({ _id: doc._id }, doc);
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: 'Rebuild the search index. Normally this happens automatically.\nThis should only be needed if you have changed the\n"searchable" property for various fields or types.',
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 { role, ..._properties } = options || {};
223
- Object.assign(req, _properties);
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` and `self.partial` methods of *your own* module**,
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` or `self.partial`
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. If you are trying to implement a Nunjucks helper function, use module.partial.');
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. If you are trying to implement a Nunjucks helper function, use module.partial.');
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` or `self.partial` on your module
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
@@ -64,7 +64,7 @@ export default {
64
64
  .apos-table__cell-field--context-menu__content {
65
65
  @include apos-transition();
66
66
  display: inline-block;
67
- opacity: 0;
67
+ opacity: 0.3;
68
68
  &.apos-is-visible {
69
69
  opacity: 1;
70
70
  }
@@ -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 }}&nbsp;
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 `${this.value.length} Added`;
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 `Min: ${this.effectiveMin}`;
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 `Max: ${this.field.max}`;
83
+ return this.$t('apostrophe:maxUi', {
84
+ number: this.field.max
85
+ });
80
86
  } else {
81
87
  return false;
82
88
  }