apostrophe 4.6.1 → 4.7.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 (124) hide show
  1. package/.github/workflows/main.yml +1 -1
  2. package/CHANGELOG.md +39 -1
  3. package/lib/big-upload-client.js +100 -0
  4. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBar.vue +5 -3
  5. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarLocale.vue +6 -3
  6. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarUser.vue +4 -1
  7. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +24 -16
  8. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue +1 -0
  9. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposSavingIndicator.vue +7 -5
  10. package/modules/@apostrophecms/area/index.js +5 -2
  11. package/modules/@apostrophecms/area/ui/apos/components/AposAreaContextualMenu.vue +20 -12
  12. package/modules/@apostrophecms/area/ui/apos/components/AposAreaExpandedMenu.vue +11 -7
  13. package/modules/@apostrophecms/area/ui/apos/components/AposAreaMenu.vue +20 -12
  14. package/modules/@apostrophecms/area/ui/apos/components/AposAreaMenuItem.vue +3 -1
  15. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +15 -11
  16. package/modules/@apostrophecms/attachment/index.js +4 -2
  17. package/modules/@apostrophecms/command-menu/ui/apos/components/AposCommandMenuKey.vue +28 -24
  18. package/modules/@apostrophecms/command-menu/ui/apos/components/AposCommandMenuShortcut.vue +17 -11
  19. package/modules/@apostrophecms/doc/index.js +22 -19
  20. package/modules/@apostrophecms/doc-type/index.js +6 -2
  21. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +9 -5
  22. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocLocalePicker.vue +10 -5
  23. package/modules/@apostrophecms/doc-type/ui/apos/logic/AposDocContextMenu.js +12 -0
  24. package/modules/@apostrophecms/http/index.js +19 -3
  25. package/modules/@apostrophecms/http/lib/big-upload-middleware.js +251 -0
  26. package/modules/@apostrophecms/i18n/i18n/de.json +1 -1
  27. package/modules/@apostrophecms/i18n/i18n/en.json +9 -1
  28. package/modules/@apostrophecms/i18n/i18n/es.json +1 -1
  29. package/modules/@apostrophecms/i18n/i18n/fr.json +1 -1
  30. package/modules/@apostrophecms/i18n/i18n/it.json +1 -1
  31. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +1 -1
  32. package/modules/@apostrophecms/i18n/i18n/sk.json +1 -1
  33. package/modules/@apostrophecms/i18n/index.js +3 -0
  34. package/modules/@apostrophecms/i18n/ui/apos/components/AposI18nLocalize.vue +30 -16
  35. package/modules/@apostrophecms/i18n/ui/apos/components/AposI18nLocalizeErrors.vue +7 -5
  36. package/modules/@apostrophecms/image/ui/apos/components/AposImageCropper.vue +5 -1
  37. package/modules/@apostrophecms/image/ui/apos/components/AposImageRelationshipEditor.vue +10 -6
  38. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +40 -18
  39. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerDisplay.vue +35 -25
  40. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerEditor.vue +11 -5
  41. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerSelections.vue +15 -9
  42. package/modules/@apostrophecms/image/ui/apos/components/AposMediaUploader.vue +39 -31
  43. package/modules/@apostrophecms/job/index.js +1 -1
  44. package/modules/@apostrophecms/login/ui/apos/components/AposLoginForm.vue +9 -7
  45. package/modules/@apostrophecms/login/ui/apos/components/TheAposLogin.vue +17 -13
  46. package/modules/@apostrophecms/login/ui/apos/components/TheAposLoginHeader.vue +30 -20
  47. package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +5 -0
  48. package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +4 -1
  49. package/modules/@apostrophecms/modal/ui/apos/components/AposModalBreadcrumbs.vue +8 -4
  50. package/modules/@apostrophecms/modal/ui/apos/components/AposModalConfirm.vue +14 -8
  51. package/modules/@apostrophecms/modal/ui/apos/components/AposModalShareDraft.vue +32 -22
  52. package/modules/@apostrophecms/modal/ui/apos/components/AposModalTabs.vue +16 -14
  53. package/modules/@apostrophecms/modal/ui/apos/components/AposWidgetModalTabs.vue +16 -14
  54. package/modules/@apostrophecms/notification/ui/apos/components/AposNotification.vue +93 -91
  55. package/modules/@apostrophecms/page/index.js +482 -13
  56. package/modules/@apostrophecms/page/ui/apos/components/AposPagesManager.vue +43 -23
  57. package/modules/@apostrophecms/page/ui/apos/logic/AposPagesManager.js +248 -156
  58. package/modules/@apostrophecms/permission/ui/apos/components/AposPermissionGrid.vue +9 -5
  59. package/modules/@apostrophecms/piece-type/index.js +7 -7
  60. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +92 -36
  61. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +30 -24
  62. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapDivider.vue +4 -2
  63. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapLink.vue +2 -1
  64. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapMarks.vue +5 -3
  65. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapStyles.vue +5 -3
  66. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapTable.vue +5 -2
  67. package/modules/@apostrophecms/schema/index.js +26 -5
  68. package/modules/@apostrophecms/schema/lib/addFieldTypes.js +42 -9
  69. package/modules/@apostrophecms/schema/ui/apos/components/AposInputColor.vue +4 -2
  70. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRange.vue +8 -4
  71. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRelationship.vue +6 -4
  72. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +5 -3
  73. package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +19 -13
  74. package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +6 -2
  75. package/modules/@apostrophecms/schema/ui/apos/components/AposSubform.vue +6 -4
  76. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputSlug.js +28 -25
  77. package/modules/@apostrophecms/schema/ui/apos/scss/AposInputArray.scss +13 -7
  78. package/modules/@apostrophecms/settings/ui/apos/components/AposSettingsManager.vue +11 -6
  79. package/modules/@apostrophecms/translation/ui/apos/components/AposTranslationIndicator.vue +5 -3
  80. package/modules/@apostrophecms/ui/ui/apos/components/AposAvatar.vue +14 -12
  81. package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +14 -11
  82. package/modules/@apostrophecms/ui/ui/apos/components/AposButtonSplit.vue +7 -3
  83. package/modules/@apostrophecms/ui/ui/apos/components/AposCellContextMenu.vue +4 -2
  84. package/modules/@apostrophecms/ui/ui/apos/components/AposCombo.vue +23 -17
  85. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +25 -10
  86. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuDialog.vue +7 -5
  87. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuItem.vue +10 -8
  88. package/modules/@apostrophecms/ui/ui/apos/components/AposEmptyState.vue +9 -5
  89. package/modules/@apostrophecms/ui/ui/apos/components/AposFile.vue +9 -6
  90. package/modules/@apostrophecms/ui/ui/apos/components/AposIndicator.vue +5 -0
  91. package/modules/@apostrophecms/ui/ui/apos/components/AposLoadingBlock.vue +3 -1
  92. package/modules/@apostrophecms/ui/ui/apos/components/AposLocale.vue +3 -1
  93. package/modules/@apostrophecms/ui/ui/apos/components/AposLocalePicker.vue +11 -9
  94. package/modules/@apostrophecms/ui/ui/apos/components/AposMinMaxCount.vue +5 -3
  95. package/modules/@apostrophecms/ui/ui/apos/components/AposPager.vue +4 -2
  96. package/modules/@apostrophecms/ui/ui/apos/components/AposPagerDots.vue +8 -6
  97. package/modules/@apostrophecms/ui/ui/apos/components/AposSlat.vue +25 -17
  98. package/modules/@apostrophecms/ui/ui/apos/components/AposSlatList.vue +5 -9
  99. package/modules/@apostrophecms/ui/ui/apos/components/AposSubformPreview.vue +10 -6
  100. package/modules/@apostrophecms/ui/ui/apos/components/AposTag.vue +9 -7
  101. package/modules/@apostrophecms/ui/ui/apos/components/AposTagApply.vue +8 -4
  102. package/modules/@apostrophecms/ui/ui/apos/components/AposTagList.vue +4 -2
  103. package/modules/@apostrophecms/ui/ui/apos/components/AposTagListItem.vue +7 -5
  104. package/modules/@apostrophecms/ui/ui/apos/components/AposToggle.vue +16 -0
  105. package/modules/@apostrophecms/ui/ui/apos/components/AposTree.vue +3 -1
  106. package/modules/@apostrophecms/ui/ui/apos/components/AposTreeRows.vue +11 -9
  107. package/modules/@apostrophecms/ui/ui/apos/mixins/AposArchiveMixin.js +2 -2
  108. package/modules/@apostrophecms/ui/ui/apos/mixins/AposPublishMixin.js +6 -6
  109. package/modules/@apostrophecms/ui/ui/apos/scss/global/_inputs.scss +30 -22
  110. package/modules/@apostrophecms/ui/ui/apos/scss/global/_theme.scss +22 -18
  111. package/modules/@apostrophecms/ui/ui/apos/scss/global/_tooltips.scss +18 -15
  112. package/modules/@apostrophecms/ui/ui/apos/scss/mixins/_input_mixins.scss +8 -6
  113. package/modules/@apostrophecms/ui/ui/apos/scss/mixins/_mixins.scss +3 -1
  114. package/modules/@apostrophecms/ui/ui/apos/scss/mixins/_theme_mixins.scss +34 -19
  115. package/modules/@apostrophecms/ui/ui/apos/scss/mixins/_type_mixins.scss +3 -1
  116. package/modules/@apostrophecms/ui/ui/apos/utils/index.js +140 -51
  117. package/modules/@apostrophecms/widget-type/index.js +3 -2
  118. package/modules/@apostrophecms/widget-type/ui/apos/mixins/AposWidgetMixin.js +5 -1
  119. package/package.json +5 -6
  120. package/test/big-upload.js +111 -0
  121. package/test/change-doc-ids.js +60 -1
  122. package/test/pages.js +488 -0
  123. package/test/schemas.js +327 -0
  124. package/test/utils.js +266 -5
@@ -16,7 +16,8 @@
16
16
  line-height: var(--a-line-tall);
17
17
  }
18
18
 
19
- ul, ol {
19
+ ul,
20
+ ol {
20
21
  padding-left: $spacing-base;
21
22
  }
22
23
 
@@ -30,7 +31,7 @@
30
31
  }
31
32
 
32
33
  ul li::before {
33
- content: '-';
34
+ content: "-";
34
35
  position: relative;
35
36
  left: -$spacing-half;
36
37
  }
@@ -39,13 +40,15 @@
39
40
  .apos-tooltip__inner {
40
41
  @include type-small;
41
42
 
42
- z-index: $z-index-default;
43
- position: relative;
44
- padding: 8px 10px;
45
- border-radius: 3px;
46
- border-width: 0;
47
- color: var(--a-text-inverted);
48
- background: var(--a-background-inverted);
43
+ & {
44
+ z-index: $z-index-default;
45
+ position: relative;
46
+ padding: 8px 10px;
47
+ border-radius: 3px;
48
+ border-width: 0;
49
+ color: var(--a-text-inverted);
50
+ background: var(--a-background-inverted);
51
+ }
49
52
  }
50
53
 
51
54
  .apos-tooltip__arrow {
@@ -61,31 +64,31 @@
61
64
  transform: rotate(45deg);
62
65
  }
63
66
 
64
- &[x-placement^='top'] {
67
+ &[x-placement^="top"] {
65
68
  .apos-tooltip__arrow {
66
69
  bottom: -4px;
67
70
  }
68
71
  }
69
72
 
70
- &[x-placement^='bottom'] {
73
+ &[x-placement^="bottom"] {
71
74
  .apos-tooltip__arrow {
72
75
  top: -3px;
73
76
  }
74
77
  }
75
78
 
76
- &[x-placement^='right'] {
79
+ &[x-placement^="right"] {
77
80
  .apos-tooltip__arrow {
78
81
  left: -3px;
79
82
  }
80
83
  }
81
84
 
82
- &[x-placement^='left'] {
85
+ &[x-placement^="left"] {
83
86
  .apos-tooltip__arrow {
84
87
  right: -3px;
85
88
  }
86
89
  }
87
90
 
88
- &[aria-hidden='true'] {
91
+ &[aria-hidden="true"] {
89
92
  display: none;
90
93
  visibility: hidden;
91
94
  opacity: 0;
@@ -96,7 +99,7 @@
96
99
  }
97
100
  }
98
101
 
99
- &[aria-hidden='false'] {
102
+ &[aria-hidden="false"] {
100
103
  display: initial;
101
104
  visibility: visible;
102
105
  opacity: 1;
@@ -5,12 +5,14 @@ $box-width: 12px;
5
5
  @include type-base;
6
6
  @include apos-transition(all);
7
7
 
8
- box-sizing: border-box;
9
- width: 100%;
10
- border: 1px solid var(--a-base-8);
11
- color: var(--a-text-primary);
12
- border-radius: var(--a-border-radius);
13
- background-color: var(--a-base-9);
8
+ & {
9
+ box-sizing: border-box;
10
+ width: 100%;
11
+ border: 1px solid var(--a-base-8);
12
+ color: var(--a-text-primary);
13
+ border-radius: var(--a-border-radius);
14
+ background-color: var(--a-base-9);
15
+ }
14
16
 
15
17
  &::placeholder {
16
18
  color: var(--a-base-4);
@@ -2,7 +2,9 @@
2
2
  // until used
3
3
 
4
4
  @mixin apos-transition($what: all, $duration: 0.1s, $ease: ease-in-out) {
5
- transition: $what $duration $ease;
5
+ & {
6
+ transition: $what $duration $ease;
7
+ }
6
8
  }
7
9
 
8
10
  @mixin apos-p-reset() {
@@ -1,4 +1,4 @@
1
- @use 'sass:math';
1
+ @use "sass:math";
2
2
 
3
3
  $brand-red: #ea433a;
4
4
  $brand-gold: #cc9300;
@@ -27,57 +27,72 @@ $input-color-disabled: var(--a-base-4);
27
27
  --a-primary-transparent-10: #{transparentize($color, 0.9)};
28
28
  --a-primary-dark-10: #{mix(#000, $color, 10%)};
29
29
  --a-primary-dark-15: #{mix(#000, $color, 15%)};
30
- --a-primary-light-40: #{mix(#FFF, $color, 40%)};
30
+ --a-primary-light-40: #{mix(#fff, $color, 40%)};
31
31
  }
32
32
 
33
33
  //Application Styles
34
34
 
35
35
  @mixin type-base {
36
- color: var(--a-text-primary);
37
- font-family: var(--a-family-default);
38
- font-size: var(--a-type-base);
39
- font-weight: var(--a-weight-base);
40
- letter-spacing: var(--a-letter-base);
41
- line-height: var(--a-line-base);
36
+ & {
37
+ color: var(--a-text-primary);
38
+ font-family: var(--a-family-default);
39
+ font-size: var(--a-type-base);
40
+ font-weight: var(--a-weight-base);
41
+ letter-spacing: var(--a-letter-base);
42
+ line-height: var(--a-line-base);
43
+ }
42
44
  }
43
45
 
44
46
  @mixin type-help {
45
47
  @include type-base;
46
48
 
47
- font-size: var(--a-type-small);
48
- text-transform: uppercase;
49
+ & {
50
+ font-size: var(--a-type-small);
51
+ text-transform: uppercase;
52
+ }
49
53
  }
50
54
 
51
55
  @mixin type-small {
52
56
  @include type-base;
53
57
 
54
- font-size: var(--a-type-small);
55
- line-height: var(--a-line-tall);
58
+ & {
59
+ font-size: var(--a-type-small);
60
+ line-height: var(--a-line-tall);
61
+ }
56
62
  }
57
63
 
58
64
  @mixin type-label {
59
65
  @include type-base;
60
66
 
61
- font-size: var(--a-type-label);
62
- line-height: var(--a-line-tall);
67
+ & {
68
+ font-size: var(--a-type-label);
69
+ line-height: var(--a-line-tall);
70
+ }
63
71
  }
64
72
 
65
73
  @mixin type-large {
66
74
  @include type-base;
67
75
 
68
- font-size: var(--a-type-large);
69
- line-height: var(--a-line-tall);
76
+ & {
77
+ font-size: var(--a-type-large);
78
+ line-height: var(--a-line-tall);
79
+ }
80
+
70
81
  }
71
82
 
72
83
  @mixin type-title {
73
84
  @include type-base;
74
85
 
75
- font-size: var(--a-type-heading);
76
- line-height: var(--a-line-tall);
86
+ & {
87
+ font-size: var(--a-type-heading);
88
+ line-height: var(--a-line-tall);
89
+ }
77
90
  }
78
91
 
79
92
  @mixin type-display {
80
93
  @include type-base;
81
94
 
82
- font-size: var(--a-type-display);
95
+ & {
96
+ font-size: var(--a-type-display);
97
+ }
83
98
  }
@@ -1,5 +1,7 @@
1
1
  @mixin link-primary {
2
- color: var(--a-primary);
2
+ & {
3
+ color: var(--a-primary);
4
+ }
3
5
 
4
6
  &:hover,
5
7
  &:focus {
@@ -1,53 +1,142 @@
1
1
  module.exports = {
2
- debounce(fn, delay) {
3
- let timer;
4
- let previousDone = true;
5
-
6
- const setTimer = (res, rej, args, delay) => {
7
- return setTimeout(() => {
8
- if (!previousDone) {
9
- clearTimeout(timer);
10
- timer = setTimer(res, rej, args, delay);
11
- return;
12
- }
13
-
14
- previousDone = false;
15
- const returned = fn.apply(this, args);
16
- if (returned instanceof Promise) {
17
- return returned
18
- .then(res)
19
- .catch(rej)
20
- .finally(() => {
21
- previousDone = true;
22
- });
23
- }
24
-
25
- previousDone = true;
26
- res(returned);
27
- }, delay);
28
- };
29
-
30
- return (...args) => {
31
- return new Promise((resolve, reject) => {
32
- clearTimeout(timer);
33
- timer = setTimer(resolve, reject, args, delay);
34
- });
35
- };
36
- },
37
-
38
- throttle(fn, delay) {
39
- let inThrottle;
40
-
41
- return (...args) => {
42
- return new Promise((resolve) => {
43
- if (!inThrottle) {
44
- inThrottle = true;
45
- setTimeout(() => {
46
- inThrottle = false;
47
- resolve(fn.apply(this, args));
48
- }, delay);
49
- }
50
- });
51
- };
52
- }
2
+ debounceAsync,
3
+ // BC alias
4
+ debounce: debounceAsync,
5
+ throttle
53
6
  };
7
+
8
+ // Debounce the async function "fn". For synchronous functions, use "lodash/debounce", not this function.
9
+ //
10
+ // Returns a debounced function that invokes "fn", but no more frequently than every "delay"
11
+ // milliseconds and never while "fn" is already in progress.
12
+ //
13
+ // As always when debouncing, extra calls are discarded, however the most recent call
14
+ // is guaranteed to result in a final invocation if not preempted by a new call, so you may
15
+ // trust that the user's most recent input will eventually be sent etc.
16
+ //
17
+ // ### Avoiding race conditions with onSuccess
18
+ //
19
+ // Race conditions are a challenge. To avoid them, "fn" should have no side effects, and you should
20
+ // pass a synchronous function that accepts the return value of "fn" as the "onSuccess" option.
21
+ // The "onSuccess" function should then implement all needed side effects (e.g. changes to component
22
+ // state, etc) when invoked. The debounced function has no return value when "onSuccess" is used.
23
+ //
24
+ // You can cancel the debounced function and cause all ongoing and any further invocations to be rejected by
25
+ // calling the "cancel()" method attached to it.
26
+ //
27
+ // You can skip the initial delay for a particular invocation by calling the "skipDelay()" method
28
+ // attached to the debounced function, passing any appropriate arguments for "fn" to "skipDelay" instead.
29
+ // This is useful when immediate action is sometimes needed but you still want to use "onSuccess" in
30
+ // a consistent manner and prevent race conditions.
31
+ //
32
+ // ### Detecting Cancellations
33
+ //
34
+ // If "onSuccess" is not provided, then after cancellation any invocations will be rejected
35
+ // with an error such that "e.name === 'debounce.canceled'". Any other errors are passed through
36
+ // as errors in the debounced function.
37
+ //
38
+ // If "onSuccess" is provided then all invocations of the debounced function resolve with "null".
39
+ // If a rejection due to cancelation is detected ("e.name === 'debounce.canceled'") then
40
+ // it will be "muted" internally. Howeever, if any other type of error occurs, it will be passed through,
41
+ // resulting in a rejection of the debounced function.
42
+
43
+ function debounceAsync(fn, delay, options = {}) {
44
+ const canceledRejection = new Error('debounce:canceled');
45
+ canceledRejection.name = 'debounce.canceled';
46
+ let timer;
47
+ let canceled = false;
48
+ let previousDone = true;
49
+ let skipNextDelay = false;
50
+
51
+ const setTimer = (res, rej, args, delay) => {
52
+ function body() {
53
+ if (canceled) {
54
+ return rej(canceledRejection);
55
+ }
56
+ if (!previousDone) {
57
+ clearTimeout(timer);
58
+ // At least 100ms delay to let current invocation finish
59
+ timer = setTimer(res, rej, args, delay || 100);
60
+ return;
61
+ }
62
+
63
+ previousDone = false;
64
+ const returned = fn.apply(this, args);
65
+ if (returned instanceof Promise) {
66
+ return returned
67
+ .then((result) => {
68
+ if (canceled) {
69
+ return rej(canceledRejection);
70
+ }
71
+ res(result);
72
+ })
73
+ .catch(rej)
74
+ .finally(() => {
75
+ previousDone = true;
76
+ });
77
+ }
78
+
79
+ previousDone = true;
80
+ res(returned);
81
+ }
82
+ return setTimeout(body, delay);
83
+ };
84
+
85
+ const wrapper = async (...args) => {
86
+ const promise = new Promise((resolve, reject) => {
87
+ if (canceled) {
88
+ return reject(canceledRejection);
89
+ }
90
+ if (skipNextDelay) {
91
+ skipNextDelay = false;
92
+ timer = setTimer(resolve, reject, args, 0);
93
+ return;
94
+ }
95
+ clearTimeout(timer);
96
+ timer = setTimer(resolve, reject, args, delay);
97
+ });
98
+
99
+ try {
100
+ const result = await promise;
101
+ if (options.onSuccess) {
102
+ await options.onSuccess(result);
103
+ return null;
104
+ }
105
+ return promise;
106
+ } catch (e) {
107
+ if (e.name !== 'debounce.canceled' || !options.onSuccess) {
108
+ throw e;
109
+ }
110
+ return null;
111
+ }
112
+ };
113
+
114
+ wrapper.cancel = () => {
115
+ canceled = true;
116
+ clearTimeout(timer);
117
+ timer = null;
118
+ };
119
+
120
+ wrapper.skipDelay = (...args) => {
121
+ skipNextDelay = true;
122
+ return wrapper(...args);
123
+ };
124
+
125
+ return wrapper;
126
+ }
127
+
128
+ function throttle(fn, delay) {
129
+ let inThrottle;
130
+
131
+ return (...args) => {
132
+ return new Promise((resolve) => {
133
+ if (!inThrottle) {
134
+ inThrottle = true;
135
+ setTimeout(() => {
136
+ inThrottle = false;
137
+ resolve(fn.apply(this, args));
138
+ }, delay);
139
+ }
140
+ });
141
+ };
142
+ }
@@ -311,7 +311,8 @@ module.exports = {
311
311
  //
312
312
  // Returns a new, sanitized widget object.
313
313
 
314
- async sanitize(req, input, options) {
314
+ async sanitize(req, input, options, { fetchRelationships = true } = {}) {
315
+ const convertOptions = { fetchRelationships };
315
316
  if (!input || typeof input !== 'object') {
316
317
  // Do not crash
317
318
  input = {};
@@ -325,7 +326,7 @@ module.exports = {
325
326
  output.aposPlaceholder = self.apos.launder.boolean(input.aposPlaceholder);
326
327
  if (!output.aposPlaceholder) {
327
328
  const schema = self.allowedSchema(req);
328
- await self.apos.schema.convert(req, schema, input, output);
329
+ await self.apos.schema.convert(req, schema, input, output, convertOptions);
329
330
  }
330
331
  return output;
331
332
  },
@@ -7,6 +7,10 @@ export default {
7
7
  type: String,
8
8
  areaFieldId: String,
9
9
  modelValue: Object,
10
+ mode: {
11
+ type: String,
12
+ default: 'draft'
13
+ },
10
14
  meta: {
11
15
  type: Object,
12
16
  default() {
@@ -59,7 +63,7 @@ export default {
59
63
  this.rendered = this.rendering.html;
60
64
  } else {
61
65
  this.rendered = '...';
62
- this.rendered = await apos.http.post(`${apos.area.action}/render-widget?aposEdit=1&aposMode=draft`, {
66
+ this.rendered = await apos.http.post(`${apos.area.action}/render-widget?aposEdit=1&aposMode=${this.mode}`, {
63
67
  busy: true,
64
68
  body: parameters
65
69
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apostrophe",
3
- "version": "4.6.1",
3
+ "version": "4.7.0",
4
4
  "description": "The Apostrophe Content Management System.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -69,7 +69,6 @@
69
69
  "cors": "^2.8.5",
70
70
  "css-loader": "^5.2.4",
71
71
  "dayjs": "^1.9.8",
72
- "debounce-async": "0.0.2",
73
72
  "dompurify": "^2.3.1",
74
73
  "express": "^4.16.4",
75
74
  "express-bearer-token": "^2.4.0",
@@ -77,7 +76,7 @@
77
76
  "express-session": "^1.17.1",
78
77
  "form-data": "^4.0.0",
79
78
  "fs-extra": "^7.0.1",
80
- "glob": "^10.4.1",
79
+ "glob": "^10.0.0",
81
80
  "he": "^1.2.0",
82
81
  "html-to-text": "^9.0.5",
83
82
  "i18next": "^20.3.2",
@@ -110,8 +109,8 @@
110
109
  "resolve": "^1.19.0",
111
110
  "resolve-from": "^5.0.0",
112
111
  "sanitize-html": "^2.12.1",
113
- "sass": "^1.52.3",
114
- "sass-loader": "^10.1.1",
112
+ "sass": "^1.77.8",
113
+ "sass-loader": "^16.0.0",
115
114
  "server-destroy": "^1.0.1",
116
115
  "sluggo": "^1.0.0",
117
116
  "sortablejs": "^1.15.0",
@@ -120,7 +119,7 @@
120
119
  "tinycolor2": "^1.4.2",
121
120
  "tough-cookie": "^4.0.0",
122
121
  "underscore.string": "^3.3.4",
123
- "uploadfs": "^1.22.4",
122
+ "uploadfs": "^1.22.6",
124
123
  "vue": "^3.3.8",
125
124
  "vue-advanced-cropper": "^2.8.8",
126
125
  "vue-loader": "^17.1.0",
@@ -0,0 +1,111 @@
1
+ const assert = require('assert');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+ const qs = require('qs');
5
+
6
+ const t = require('../test-lib/test.js');
7
+ const bigUpload = require('../lib/big-upload-client.js');
8
+
9
+ const buffer = fs.readFileSync(path.resolve(__dirname, 'data/upload_tests/crop_image.png'));
10
+
11
+ describe('Big Upload', function() {
12
+
13
+ let apos;
14
+ let received = false;
15
+
16
+ after(async function () {
17
+ return t.destroy(apos);
18
+ });
19
+
20
+ this.timeout(t.timeout);
21
+
22
+ it('init apos', async function() {
23
+ apos = await t.create({
24
+ root: module,
25
+ modules: {
26
+ test: {
27
+ apiRoutes: (self) => ({
28
+ post: {
29
+ 'big-upload-test': [
30
+ self.apos.http.bigUploadMiddleware(),
31
+ (req) => {
32
+ try {
33
+ received = true;
34
+ assert(req.files);
35
+ assert(req.files.file);
36
+ assert.strictEqual(req.files.file.name, 'crop_image.png');
37
+ const buffer2 = fs.readFileSync(req.files.file.path);
38
+ assert(buffer.equals(buffer2));
39
+ return {
40
+ ok: true
41
+ };
42
+ } finally {
43
+ fs.unlinkSync(req.files.file.path);
44
+ }
45
+ }
46
+ ]
47
+ }
48
+ })
49
+ }
50
+ }
51
+ });
52
+
53
+ assert(apos.http);
54
+ });
55
+
56
+ it('should be able to make a big upload request', async function() {
57
+
58
+ // Must have the same shape as a browser-side File object and be sliceable,
59
+ // returning a Blob
60
+
61
+ const file = {
62
+ size: buffer.byteLength,
63
+ name: 'crop_image.png',
64
+ slice(from, to) {
65
+ return new Blob([ buffer.subarray(from, to) ]);
66
+ }
67
+ };
68
+
69
+ // Emulate the browser-side apos.http object just barely well enough to test
70
+ // big-upload-client server-side.
71
+ //
72
+ // self.apos.http won't work for this task because it is based on node-fetch 2
73
+ // which doesn't have a 100% browser-compatible API for blobs
74
+
75
+ const http = {
76
+ async post(url, options) {
77
+ if (options.qs) {
78
+ url += '?' + qs.stringify(options.qs);
79
+ }
80
+ const isFormData = options.body instanceof FormData;
81
+ const body = isFormData ? options.body : JSON.stringify(options.body);
82
+ const headers = {};
83
+ headers.cookie = `${apos.csrfCookieName}=csrf`;
84
+ if (!isFormData) {
85
+ headers['content-type'] = 'application/json';
86
+ }
87
+ const base = apos.http.getBase();
88
+ const result = await fetch(`${base}${url}`, {
89
+ method: 'POST',
90
+ headers,
91
+ body
92
+ });
93
+ if (result.status >= 400) {
94
+ throw new Error(result.status);
95
+ }
96
+ return result.json();
97
+ }
98
+ };
99
+
100
+ const result = await bigUpload('/api/v1/test/big-upload-test', {
101
+ files: {
102
+ file
103
+ },
104
+ http,
105
+ chunkSize: 1024
106
+ });
107
+
108
+ assert(received);
109
+ assert.strictEqual(result.ok, true);
110
+ });
111
+ });
@@ -104,6 +104,61 @@ describe('change-doc-ids', function() {
104
104
  await sanityCheck(newPageId, newCategoryId);
105
105
  });
106
106
 
107
+ it('changeDocIds should skipReplace', async function() {
108
+
109
+ await sanityCheck();
110
+ const req = apos.task.getReq();
111
+ const pages = await apos.page.find(req, {}).toArray();
112
+ const categories = await apos.category.find(req, {}).toArray();
113
+ const category = categories.find(category => category._id.startsWith('new-test-category-id'));
114
+ const articles = await await apos.article.find(req, {}).toArray();
115
+ const test = pages.find(page => page.slug === '/test');
116
+ const existingPageId = `new-test-page-id:${test.aposLocale}`;
117
+ const oldPageId = `old-test-page-id:${test.aposLocale}`;
118
+ const existingCategoryId = `new-test-category-id:${category.aposLocale}`;
119
+ const oldCategoryId = `old-test-category-id:${category.aposLocale}`;
120
+
121
+ // Update articles to include the OLD category
122
+ for (const article of articles) {
123
+ if (article.categoriesIds.includes('new-test-category-id')) {
124
+ const newIds = article.categoriesIds
125
+ .filter(id => id !== 'new-test-category-id')
126
+ .concat('old-test-category-id');
127
+ await apos.doc.db.updateMany({
128
+ aposDocId: article.aposDocId
129
+ }, {
130
+ $set: {
131
+ categoriesIds: newIds
132
+ }
133
+ });
134
+ }
135
+ }
136
+ // Sanity check - existingCategoryId should not be present in articles
137
+ const articlesBefore = await apos.article.find(req, {}).toArray();
138
+ assert.strictEqual(
139
+ articlesBefore.every(article => !article.categoriesIds.includes('new-test-category-id')),
140
+ true,
141
+ 'new-test-category-id should not be present in any articles'
142
+ );
143
+ assert.strictEqual(test._id, existingPageId);
144
+ assert.strictEqual(category._id, existingCategoryId);
145
+ const pairs = [
146
+ [ oldPageId, existingPageId ],
147
+ [ oldCategoryId, existingCategoryId ]
148
+ ];
149
+
150
+ await apos.doc.changeDocIds(pairs, { skipReplace: true });
151
+
152
+ const articlesAfter = await apos.article.find(req, {}).toArray();
153
+ assert.strictEqual(
154
+ articlesAfter.every(article => !article.categoriesIds.includes('old-test-category-id')),
155
+ true,
156
+ 'old-test-category-id should not be present in any articles'
157
+ );
158
+
159
+ await sanityCheck(existingPageId, existingCategoryId);
160
+ });
161
+
107
162
  async function sanityCheck(newPageId, newCategoryId) {
108
163
  const pages = await apos.page.find(apos.task.getReq(), {}).children(true).toArray();
109
164
  const test = pages.find(page => page.slug === '/test');
@@ -125,7 +180,11 @@ describe('change-doc-ids', function() {
125
180
  assert(articles[0]._categories.find(category => category.slug === 'category-0'));
126
181
  assert(articles[0]._categories.find(category => category.slug === 'category-1'));
127
182
  if (newCategoryId) {
128
- assert.strictEqual(articles[0]._categories[0]._id, newCategoryId);
183
+ assert.strictEqual(
184
+ articles[0]._categories.some(obj => obj._id === newCategoryId),
185
+ true,
186
+ 'newCategoryId not found'
187
+ );
129
188
  const newCategory = articles[0]._categories.find(category => category._id === newCategoryId);
130
189
  assert(newCategory);
131
190
  assert.strictEqual(newCategory._id, newCategoryId);