apostrophe 3.4.1 → 3.8.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 (111) hide show
  1. package/.eslintrc +4 -0
  2. package/.scratch.md +2 -0
  3. package/CHANGELOG.md +114 -2
  4. package/README.md +1 -1
  5. package/deploy-test-count +1 -1
  6. package/index.js +125 -5
  7. package/lib/moog-require.js +41 -3
  8. package/lib/moog.js +20 -8
  9. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarLocale.vue +42 -23
  10. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +30 -14
  11. package/modules/@apostrophecms/area/index.js +9 -0
  12. package/modules/@apostrophecms/area/lib/custom-tags/area.js +1 -1
  13. package/modules/@apostrophecms/area/lib/custom-tags/widget.js +1 -1
  14. package/modules/@apostrophecms/area/ui/apos/apps/AposAreas.js +3 -0
  15. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +6 -6
  16. package/modules/@apostrophecms/asset/index.js +85 -21
  17. package/modules/@apostrophecms/asset/lib/globalIcons.js +2 -0
  18. package/modules/@apostrophecms/asset/lib/webpack/src/webpack.scss.js +5 -2
  19. package/modules/@apostrophecms/attachment/index.js +1 -0
  20. package/modules/@apostrophecms/db/index.js +5 -6
  21. package/modules/@apostrophecms/doc/index.js +13 -3
  22. package/modules/@apostrophecms/doc-type/index.js +24 -4
  23. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue +13 -1
  24. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +3 -0
  25. package/modules/@apostrophecms/i18n/i18n/en.json +26 -6
  26. package/modules/@apostrophecms/i18n/i18n/es.json +382 -0
  27. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +379 -0
  28. package/modules/@apostrophecms/i18n/i18n/sk.json +380 -0
  29. package/modules/@apostrophecms/i18n/index.js +10 -1
  30. package/modules/@apostrophecms/i18n/ui/apos/components/AposI18nLocalize.vue +153 -121
  31. package/modules/@apostrophecms/image/index.js +2 -1
  32. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +6 -3
  33. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerEditor.vue +24 -13
  34. package/modules/@apostrophecms/image-widget/index.js +2 -1
  35. package/modules/@apostrophecms/image-widget/views/widget.html +12 -2
  36. package/modules/@apostrophecms/job/index.js +164 -212
  37. package/modules/@apostrophecms/login/index.js +36 -17
  38. package/modules/@apostrophecms/login/ui/apos/components/TheAposLogin.vue +8 -0
  39. package/modules/@apostrophecms/migration/index.js +1 -1
  40. package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +151 -61
  41. package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +6 -2
  42. package/modules/@apostrophecms/modal/ui/apos/components/AposModalConfirm.vue +9 -7
  43. package/modules/@apostrophecms/modal/ui/apos/mixins/AposDocsManagerMixin.js +12 -15
  44. package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +6 -0
  45. package/modules/@apostrophecms/module/index.js +1 -1
  46. package/modules/@apostrophecms/notification/index.js +116 -8
  47. package/modules/@apostrophecms/notification/ui/apos/components/AposNotification.vue +89 -11
  48. package/modules/@apostrophecms/notification/ui/apos/components/TheAposNotifications.vue +1 -1
  49. package/modules/@apostrophecms/page/index.js +37 -30
  50. package/modules/@apostrophecms/permission/index.js +1 -1
  51. package/modules/@apostrophecms/permission/ui/apos/components/AposInputRole.vue +4 -2
  52. package/modules/@apostrophecms/piece-type/index.js +178 -61
  53. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +179 -47
  54. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerDisplay.vue +1 -3
  55. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerSelectBox.vue +138 -0
  56. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +42 -10
  57. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapStyles.vue +3 -0
  58. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Classes.js +6 -10
  59. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Default.js +64 -0
  60. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Document.js +15 -0
  61. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Heading.js +23 -0
  62. package/modules/@apostrophecms/schema/index.js +97 -20
  63. package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +1 -0
  64. package/modules/@apostrophecms/schema/ui/apos/components/AposInputArray.vue +4 -1
  65. package/modules/@apostrophecms/schema/ui/apos/components/AposInputAttachment.vue +11 -160
  66. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRadio.vue +8 -5
  67. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRelationship.vue +24 -2
  68. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue +24 -6
  69. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +0 -4
  70. package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +0 -7
  71. package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +25 -3
  72. package/modules/@apostrophecms/schema/ui/apos/mixins/AposInputMixin.js +10 -2
  73. package/modules/@apostrophecms/task/index.js +2 -2
  74. package/modules/@apostrophecms/template/index.js +63 -36
  75. package/modules/@apostrophecms/template/lib/custom-tags/component.js +1 -1
  76. package/modules/@apostrophecms/template/lib/custom-tags/render.js +6 -2
  77. package/modules/@apostrophecms/ui/index.js +6 -2
  78. package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +21 -3
  79. package/modules/@apostrophecms/ui/ui/apos/components/AposCellContextMenu.vue +1 -1
  80. package/modules/@apostrophecms/ui/ui/apos/components/AposFile.vue +205 -0
  81. package/modules/@apostrophecms/ui/ui/apos/components/AposIndicator.vue +5 -0
  82. package/modules/@apostrophecms/ui/ui/apos/lib/i18next.js +16 -2
  83. package/modules/@apostrophecms/ui/ui/apos/scss/global/_tables.scss +4 -3
  84. package/modules/@apostrophecms/ui/ui/apos/scss/global/_theme.scss +3 -0
  85. package/modules/@apostrophecms/ui/ui/apos/scss/global/_widgets.scss +3 -0
  86. package/modules/@apostrophecms/ui/ui/apos/scss/global/import-all.scss +2 -1
  87. package/modules/@apostrophecms/user/index.js +21 -0
  88. package/modules/@apostrophecms/util/index.js +2 -2
  89. package/modules/@apostrophecms/util/ui/src/http.js +12 -8
  90. package/modules/@apostrophecms/util/ui/src/util.js +15 -0
  91. package/modules/@apostrophecms/widget-type/index.js +1 -1
  92. package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +1 -0
  93. package/modules/@apostrophecms/widget-type/ui/apos/mixins/AposWidgetMixin.js +15 -7
  94. package/package.json +4 -4
  95. package/test/extra_node_modules/improve-global/index.js +7 -0
  96. package/test/extra_node_modules/improve-piece-type/index.js +7 -0
  97. package/test/improve-overrides.js +30 -0
  98. package/test/job.js +224 -0
  99. package/test/login.js +183 -0
  100. package/test/modules/@apostrophecms/global/index.js +8 -0
  101. package/test/modules/fragment-all/views/aux-test.html +7 -0
  102. package/test/modules/fragment-all/views/fragment.html +5 -0
  103. package/test/moog.js +47 -0
  104. package/test/package.json +5 -4
  105. package/test/pieces.js +17 -0
  106. package/test/reverse-relationship.js +170 -0
  107. package/test/subdir-project/app.js +3 -0
  108. package/test/subdir-project.js +26 -0
  109. package/test/templates.js +7 -1
  110. package/test-lib/test.js +23 -12
  111. package/test-lib/util.js +33 -0
@@ -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 {
@@ -10,8 +10,9 @@
10
10
  <component
11
11
  v-show="displayComponent(field.name)"
12
12
  v-model="fieldState[field.name]"
13
- :following-values="followingValues[field.name]"
14
13
  :is="fieldComponentMap[field.type]"
14
+ :following-values="followingValues[field.name]"
15
+ :condition-met="conditionalFields[field.name]"
15
16
  :field="fields[field.name].field"
16
17
  :modifiers="fields[field.name].modifiers"
17
18
  :display-options="getDisplayOptions(field.name)"
@@ -89,7 +90,11 @@ export default {
89
90
  }
90
91
  }
91
92
  },
92
- emits: [ 'input', 'reset' ],
93
+ emits: [
94
+ 'input',
95
+ 'reset',
96
+ 'validate'
97
+ ],
93
98
  data() {
94
99
  return {
95
100
  schemaReady: false,
@@ -112,7 +117,10 @@ export default {
112
117
  data: this.value[item.name]
113
118
  };
114
119
  fields[item.name].serverError = this.serverErrors && this.serverErrors[item.name];
115
- fields[item.name].modifiers = this.modifiers;
120
+ fields[item.name].modifiers = [
121
+ ...(this.modifiers || []),
122
+ ...(item.modifiers || [])
123
+ ];
116
124
  });
117
125
  return fields;
118
126
  }
@@ -142,6 +150,20 @@ export default {
142
150
  this.populateDocData();
143
151
  }
144
152
  }
153
+ },
154
+ conditionalFields(newVal, oldVal) {
155
+ for (const field in oldVal) {
156
+ if (!this.fieldState[field] || (newVal[field] === oldVal[field]) || !this.fieldState[field].ranValidation) {
157
+ continue;
158
+ }
159
+
160
+ if (
161
+ (newVal[field] === false) ||
162
+ (newVal[field] && this.fieldState[field].ranValidation)
163
+ ) {
164
+ this.$emit('validate');
165
+ }
166
+ }
145
167
  }
146
168
  },
147
169
  created() {
@@ -22,6 +22,10 @@ export default {
22
22
  type: Object,
23
23
  required: false
24
24
  },
25
+ conditionMet: {
26
+ type: Boolean,
27
+ required: false
28
+ },
25
29
  triggerValidation: {
26
30
  type: Boolean,
27
31
  default: false
@@ -117,10 +121,14 @@ export default {
117
121
  // You must supply the validate method. It receives the
118
122
  // internal representation used for editing (a string, for instance)
119
123
  validateAndEmit () {
120
- const error = this.validate(this.next);
124
+ // If the field is conditional and isn't shown, disregard any errors.
125
+ const error = this.conditionMet === false ? false
126
+ : this.validate(this.next);
121
127
  this.$emit('input', {
122
128
  data: error ? this.next : this.convert(this.next),
123
- error: this.validate(this.next)
129
+ error,
130
+ ranValidation: this.conditionMet === false ? this.value.ranValidation
131
+ : true
124
132
  });
125
133
  },
126
134
  watchValue () {
@@ -219,8 +219,8 @@ module.exports = {
219
219
  };
220
220
  addCloneMethod(req);
221
221
  req.res.__ = req.__;
222
- const { role, ..._properties } = options || {};
223
- Object.assign(req, _properties);
222
+ const { _role, ...properties } = options || {};
223
+ Object.assign(req, properties);
224
224
  self.apos.i18n.setPrefixUrls(req);
225
225
  return req;
226
226
 
@@ -61,11 +61,14 @@ module.exports = {
61
61
  prefix: self.apos.prefix
62
62
  };
63
63
 
64
+ self.envs = {};
65
+
64
66
  self.filters = {};
65
67
 
66
68
  self.nunjucks = self.options.language || require('nunjucks');
67
69
 
68
70
  self.insertions = {};
71
+
69
72
  },
70
73
  handlers(self) {
71
74
  return {
@@ -248,6 +251,34 @@ module.exports = {
248
251
 
249
252
  let result;
250
253
 
254
+ const args = self.getRenderArgs(req, data, module);
255
+
256
+ const env = self.getEnv(req, module);
257
+
258
+ if (type === 'file') {
259
+ let finalName = s;
260
+ if (!finalName.match(/\.\w+$/)) {
261
+ finalName += '.html';
262
+ }
263
+ result = await Promise.promisify(function (finalName, args, callback) {
264
+ return env.getTemplate(finalName).render(args, callback);
265
+ })(finalName, args);
266
+ } else if (type === 'string') {
267
+ result = await Promise.promisify(function (s, args, callback) {
268
+ return env.renderString(s, args, callback);
269
+ })(s, args);
270
+ } else {
271
+ throw new Error('renderBody does not support the type ' + type);
272
+ }
273
+ return result;
274
+ },
275
+
276
+ // Implementation detail of `renderBody` responsible for
277
+ // creating the input object passed to Nunjucks for rendering,
278
+ // with `data` merged into the `.data` property,
279
+ // `apos` available separately, `__req` available separately, etc.
280
+
281
+ getRenderArgs(req, data, module) {
251
282
  const merged = {};
252
283
 
253
284
  if (data) {
@@ -278,8 +309,6 @@ module.exports = {
278
309
 
279
310
  args.data.locale = args.data.locale || req.locale;
280
311
 
281
- const env = self.getEnv(req, module);
282
-
283
312
  args.apos = self.templateApos;
284
313
  args.__t = req.t;
285
314
  args.__ = key => {
@@ -289,39 +318,39 @@ module.exports = {
289
318
  `);
290
319
  return key;
291
320
  };
292
- if (type === 'file') {
293
- let finalName = s;
294
- if (!finalName.match(/\.\w+$/)) {
295
- finalName += '.html';
321
+ args.__req = req;
322
+ args.getOption = (key, def) => {
323
+ const colonAt = key.indexOf(':');
324
+ let optionModule = self.apos.modules[module.__meta.name];
325
+ if (colonAt !== -1) {
326
+ const name = key.substring(0, colonAt);
327
+ key = key.substring(colonAt + 1);
328
+ optionModule = self.apos.modules[name];
296
329
  }
297
- result = await Promise.promisify(function (finalName, args, callback) {
298
- return env.getTemplate(finalName).render(args, callback);
299
- })(finalName, args);
300
- } else if (type === 'string') {
301
- result = await Promise.promisify(function (s, args, callback) {
302
- return env.renderString(s, args, callback);
303
- })(s, args);
304
- } else {
305
- throw new Error('renderBody does not support the type ' + type);
306
- }
307
- return result;
330
+ return optionModule.getOption(req, key, def);
331
+ };
332
+ return args;
308
333
  },
309
334
 
310
335
  // Fetch a nunjucks environment in which `include`, `extends`, etc. search
311
336
  // the views directories of the specified module and its ancestors.
312
337
  // Typically you will call `self.render` or `self.partial` on your module
313
338
  // object rather than calling this directly.
339
+ //
340
+ // `req` is effectively here for bc purposes only. This method
341
+ // does NOT always pass `req` to `newEnv` for every new release, as
342
+ // `req` is separately supplied to each request to fix a memory leak
343
+ // that occurs when Nunjucks environments are created for every request.
314
344
 
315
345
  getEnv(req, module) {
316
346
  const name = module.__meta.name;
317
-
318
- req.envs = req.envs || {};
319
- // Cache for performance
320
- if (_.has(req.envs, name)) {
321
- return req.envs[name];
347
+ if (!_.has(self.envs, name)) {
348
+ // Pass the original req for bc purposes only,
349
+ // note that due to the reuse of envs there is
350
+ // no guarantee newEnv will be called for every req
351
+ self.envs[name] = self.newEnv(req, name, self.getViewFolders(module));
322
352
  }
323
- req.envs[name] = self.newEnv(req, name, self.getViewFolders(module));
324
- return req.envs[name];
353
+ return self.envs[name];
325
354
  },
326
355
 
327
356
  getViewFolders(module) {
@@ -343,7 +372,14 @@ module.exports = {
343
372
  // specified directories are searched for includes,
344
373
  // etc. Don't call this directly, use:
345
374
  //
346
- // apos.template.getEnv(module)
375
+ // apos.template.getEnv(req, module)
376
+ //
377
+ // `req` is effectively here for bc purposes only. Apostrophe
378
+ // does NOT always pass `req` to `newEnv` for every new release, as
379
+ // `req` is separately supplied to each request to fix a memory leak
380
+ // that occurs when Nunjucks environments are created for every request.
381
+ // If you must access `req` in a custom Nunjucks tag use
382
+ // `context.ctx.__req`, NOT `env.opts.req` which is no longer provided.
347
383
 
348
384
  newEnv(req, moduleName, dirs) {
349
385
 
@@ -351,22 +387,11 @@ module.exports = {
351
387
 
352
388
  const env = new self.nunjucks.Environment(loader, {
353
389
  autoescape: true,
354
- req,
355
390
  module: self.apos.modules[moduleName]
356
391
  });
357
392
 
358
393
  env.addGlobal('apos', self.templateApos);
359
394
  env.addGlobal('module', self.templateApos.modules[moduleName]);
360
- env.addGlobal('getOption', function(key, def) {
361
- const colonAt = key.indexOf(':');
362
- let optionModule = self.apos.modules[moduleName];
363
- if (colonAt !== -1) {
364
- const name = key.substring(0, colonAt);
365
- key = key.substring(colonAt + 1);
366
- optionModule = self.apos.modules[name];
367
- }
368
- return optionModule.getOption(req, key, def);
369
- });
370
395
 
371
396
  self.addStandardFilters(env);
372
397
 
@@ -627,6 +652,8 @@ module.exports = {
627
652
  locale: req.locale,
628
653
  csrfCookieName: self.apos.csrfCookieName,
629
654
  tabId: self.apos.util.generateId(),
655
+ uploadsUrl: self.apos.attachment.uploadfs.getUrl(),
656
+ assetBaseUrl: self.apos.asset.getAssetBaseUrl(),
630
657
  scene
631
658
  };
632
659
  if (req.user) {
@@ -23,7 +23,7 @@ module.exports = function(self) {
23
23
  },
24
24
  // Do the actual work
25
25
  async run(context, name, data) {
26
- const req = context.env.opts.req;
26
+ const req = context.ctx.__req;
27
27
  if (!data) {
28
28
  data = {};
29
29
  }
@@ -98,9 +98,8 @@ module.exports = (self) => {
98
98
  const source = info.body();
99
99
  const input = createRenderInput(info);
100
100
 
101
- const req = context.env.opts.req;
101
+ const req = context.ctx.__req;
102
102
  const env = self.getEnv(req, context.env.opts.module);
103
- input.apos = self.templateApos;
104
103
 
105
104
  // attach the render caller as a function
106
105
  // it's just a string, but we keep
@@ -108,6 +107,11 @@ module.exports = (self) => {
108
107
  input.rendercaller = rendercaller;
109
108
 
110
109
  const result = await require('util').promisify((s, args, callback) => {
110
+ args = {
111
+ ...self.getRenderArgs(req, {}, context.env.opts.module),
112
+ // Parameters to fragments are top level, they are not in `data`
113
+ ...args
114
+ };
111
115
  return env.renderString(s, args, callback);
112
116
  })(source, input);
113
117
  return result;
@@ -1,6 +1,7 @@
1
1
  module.exports = {
2
2
  options: {
3
- alias: 'ui'
3
+ alias: 'ui',
4
+ widgetMargin: '20px 0'
4
5
  },
5
6
  icons: {
6
7
  'earth-icon': 'Earth',
@@ -21,7 +22,10 @@ module.exports = {
21
22
  if (req.data.user && req.data.user.aposThemePrimary) {
22
23
  theme.primary = req.data.user.aposThemePrimary;
23
24
  }
24
- return { theme };
25
+ return {
26
+ theme,
27
+ widgetMargin: self.options.widgetMargin
28
+ };
25
29
  }
26
30
  };
27
31
  }
@@ -1,5 +1,9 @@
1
1
  <template>
2
- <span v-apos-tooltip="tooltip" class="apos-button__wrapper">
2
+ <span
3
+ v-apos-tooltip="tooltip"
4
+ class="apos-button__wrapper"
5
+ :class="{ 'apos-button__wrapper--block': modifiers.includes('block') }"
6
+ >
3
7
  <component
4
8
  :is="href ? 'a' : 'button'"
5
9
  v-on="href ? {} : {click: click}"
@@ -11,6 +15,7 @@
11
15
  :type="buttonType"
12
16
  :role="role"
13
17
  :id="attrs.id ? attrs.id : id"
18
+ :style="{color: textColor}"
14
19
  v-bind="attrs"
15
20
  >
16
21
  <transition name="fade">
@@ -67,6 +72,10 @@ export default {
67
72
  type: String,
68
73
  default: null
69
74
  },
75
+ textColor: {
76
+ type: String,
77
+ default: null
78
+ },
70
79
  href: {
71
80
  type: [ String, Boolean ],
72
81
  default: false
@@ -332,6 +341,9 @@ export default {
332
341
  color: var(--a-base-5);
333
342
  }
334
343
  }
344
+ .apos-button__label {
345
+ line-height: var(--a-line-tall);
346
+ }
335
347
  }
336
348
 
337
349
  .apos-button--subtle {
@@ -395,8 +407,6 @@ export default {
395
407
  box-sizing: border-box;
396
408
  display: block;
397
409
  width: 100%;
398
- height: 47px;
399
- max-width: 400px;
400
410
  }
401
411
 
402
412
  .apos-button--icon-right {
@@ -574,6 +584,10 @@ export default {
574
584
  padding: 3px;
575
585
  }
576
586
 
587
+ .apos-button--uppercase .apos-button__label {
588
+ text-transform: uppercase;
589
+ }
590
+
577
591
  .apos-button--inline {
578
592
  padding: 0;
579
593
  &, &[disabled], &:hover, &:active, &:focus {
@@ -605,6 +619,10 @@ export default {
605
619
  display: inline-block;
606
620
  }
607
621
 
622
+ .apos-button__wrapper--block {
623
+ display: block;
624
+ }
625
+
608
626
  @keyframes animateGradient {
609
627
  0% {
610
628
  background-position: 0% 50%;
@@ -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>