apostrophe 4.20.0 → 4.21.1

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 (54) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/README.md +112 -30
  3. package/index.js +10 -1
  4. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarMenu.vue +1 -0
  5. package/modules/@apostrophecms/area/index.js +8 -4
  6. package/modules/@apostrophecms/area/lib/custom-tags/area.js +1 -1
  7. package/modules/@apostrophecms/area/ui/apos/components/AposAreaContextualMenu.vue +31 -4
  8. package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +40 -59
  9. package/modules/@apostrophecms/area/ui/apos/components/AposAreaExpandedMenu.vue +97 -10
  10. package/modules/@apostrophecms/area/ui/apos/components/AposAreaMenu.vue +6 -1
  11. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +7 -3
  12. package/modules/@apostrophecms/area/ui/apos/components/AposWidgetControls.vue +14 -9
  13. package/modules/@apostrophecms/area/ui/apos/lib/clone-widget.js +39 -0
  14. package/modules/@apostrophecms/asset/lib/build/external-module-api.js +5 -3
  15. package/modules/@apostrophecms/asset/lib/build/task.js +5 -3
  16. package/modules/@apostrophecms/asset/lib/globalIcons.js +1 -0
  17. package/modules/@apostrophecms/attachment/index.js +2 -1
  18. package/modules/@apostrophecms/doc/ui/apos/apps/AposDoc.js +7 -2
  19. package/modules/@apostrophecms/doc-type/index.js +3 -3
  20. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +11 -1
  21. package/modules/@apostrophecms/i18n/i18n/de.json +7 -1
  22. package/modules/@apostrophecms/i18n/i18n/en.json +7 -1
  23. package/modules/@apostrophecms/i18n/i18n/es.json +7 -1
  24. package/modules/@apostrophecms/i18n/i18n/fr.json +7 -1
  25. package/modules/@apostrophecms/i18n/i18n/it.json +7 -1
  26. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +7 -1
  27. package/modules/@apostrophecms/i18n/i18n/sk.json +7 -1
  28. package/modules/@apostrophecms/image/ui/apos/components/AposImageRelationshipEditor.vue +44 -22
  29. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerEditor.vue +7 -3
  30. package/modules/@apostrophecms/image/ui/apos/components/AposMediaUploader.vue +0 -7
  31. package/modules/@apostrophecms/image/ui/apos/components/AposMediaUploaderUi.vue +386 -0
  32. package/modules/@apostrophecms/image-widget/index.js +18 -3
  33. package/modules/@apostrophecms/image-widget/ui/apos/components/AposImageWidget.vue +217 -0
  34. package/modules/@apostrophecms/login/index.js +36 -26
  35. package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +6 -51
  36. package/modules/@apostrophecms/piece-type/ui/apos/components/AposUtilityOperations.vue +2 -2
  37. package/modules/@apostrophecms/piece-type/ui/apos/lib/postprocessRelationships.js +68 -0
  38. package/modules/@apostrophecms/schema/lib/addFieldTypes.js +3 -6
  39. package/modules/@apostrophecms/schema/ui/apos/lib/detectChange.js +1 -1
  40. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRelationship.js +32 -16
  41. package/modules/@apostrophecms/schema/ui/apos/logic/AposSchema.js +4 -1
  42. package/modules/@apostrophecms/submitted-draft/ui/apos/components/AposSubmittedDraftIcon.vue +7 -0
  43. package/modules/@apostrophecms/ui/ui/apos/apps/AposTransformers.js +12 -0
  44. package/modules/@apostrophecms/ui/ui/apos/composables/AposTheme.js +8 -7
  45. package/modules/@apostrophecms/ui/ui/apos/mixins/AposThemeMixin.js +3 -4
  46. package/modules/@apostrophecms/ui/ui/apos/stores/modal.js +6 -0
  47. package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +1 -9
  48. package/modules/@apostrophecms/widget-type/ui/apos/composables/AposWidget.js +97 -0
  49. package/modules/@apostrophecms/widget-type/ui/apos/composables/AposWidgetProps.js +28 -0
  50. package/modules/@apostrophecms/widget-type/ui/apos/mixins/AposWidgetMixin.js +22 -86
  51. package/package.json +2 -2
  52. package/test/add-missing-schema-fields.js +15 -9
  53. package/test/login.js +73 -22
  54. package/test/widgets.js +0 -27
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.21.1 (2025-09-26)
4
+
5
+ ### Adds
6
+
7
+ * The `exit` option to the main `apostrophe()` function now supports the new string value `exit: 'throw'`. If this value is specified and the apostrophe startup procedure fails with an error, the actual error is re-thrown for the benefit of the caller.
8
+ * For backwards compatibility, the existing `exit: false` option to the main `apostrophe()` function is still supported, but now logs the error that took place before returning `undefined` as before. This is more useful than the previous behavior, but `exit: 'throw'` is the more logical choice if you need to avoid a process exit.
9
+ * The default behavior is still to log the error and exit the process, which isthe only sensible move in most single-site projects.
10
+
11
+ ## 4.21.0 (2025-09-03)
12
+
13
+ ### Adds
14
+
15
+ * Modules can now call `apos.area.addCreateWidgetOperation` to register a custom operation that invokes a modal and inserts the widget returned by that modal. These operations are offered as choices in all "add widget" menus, both regular and expanded.
16
+ * `AposDocEditor` now accepts a `values` prop, which can be used to pass an object of initial values for some or all fields. Use of this prop is optional. It is not supported when editing existing documents.
17
+ * `apos.doc.edit` now accepts an optional `values` object as the final parameter, containing initial values for some or all fields. This is supported only when editing existing documents.
18
+ * When specifying a modal name to be executed, developers may now register "transformers" to be invoked first, using pipe syntax. For example, the modal name `aposSectionTemplateLibraryWidgetToDoc|AposDocEditor` will invoke the transformer `aposSectionTemplateLibraryWidgetToDoc` with the original props, and pass the returned result to `AposDocEditor`. Note that transformers are awaited. Transformers are registered in frontend admin UI code by passing a name and a function to `apos.ui.addTransformer`.
19
+ * Adds quick image upload UI to `@apostrophecms/image-widget`.
20
+ * Makes autocropping work when uploading or selecting images from the new quick image upload UI.
21
+
22
+ ### Fixes
23
+
24
+ * The `?render-areas=1` API feature now correctly disregards areas in separate documents loaded via relationship fields. Formerly their presence resulted in an error, not a rendering.
25
+ * Make conditional fields work in Image Editor.
26
+ * Importing a custom icon from an npm module using a `~` path per the admin UI now works per the documentation, as long as the Vue component used for the icon is structured like those found in `@apostrophecms/vue-material-design-icons`.
27
+ * The `button: true` flag works again for piece module utility operations. Previously the button appeared but did not trigger the desired operation.
28
+ * Fix the fact that area options `minSize` and `aspectRatio` weren't passed to the image cropper when coming directly from the area and the widget controls (without passing through the widget editor).
29
+ * Fixes the widget data being cloned to be saved before the `postprocess` method being called, which leads to a loss of data in `AposWidgetEditor` (like the autocrop data).
30
+ * In editors like `AposWidgetEditor` relationships are now post processed after they are updated in `AposInputRelationship` only for the relationship that has been updated.
31
+ It allows live preview to work well with it, it also avoids complexity and fixes updated data not being properly synced between the editor and the `AposSchema`.
32
+ * Deeply nested widgets can now be edited properly via the editor dialog box. This longstanding issue did not affect on-page editing.
33
+
34
+ ### Changes
35
+
36
+ * Rolled back a change in 4.16.0 that strictly enforced `required` and `min` for relationship fields. Because the related document can be archived or deleted at any time, it is misleading to offer such enforcement. Also, it greatly complicates adding these constraints to existing schemas, resulting in surprising and unwanted behaviors. Therefore it is better for these constraints to be soft constraints on the front end. `max` is still a hard constraint.
37
+ * The `@apostrophecms/login/whoami` route now accepts both `POST` (recommended) and `GET` requests. Previously, it only supported `GET`. This maintains backwards compatibility while aligning with the documentation’s recommendation to use `POST`.
38
+
3
39
  ## 4.20.0 (2025-08-06)
4
40
 
5
41
  ### Adds
@@ -9,8 +45,11 @@
9
45
 
10
46
  ### Changes
11
47
 
48
+ * A `clone-widget.js` file has been factored out, providing a universal way to return a clone of an existing widget which is distinct from the original.
49
+ * Adds any alt text found in an attribute to the media library attachment during import of rich text inline images by API
50
+ * Adds `prependNodes` and `appendNodes` methods to every module. These methods allow you to inject HTML to every page using a `node` declaration.
12
51
  * Changes handling of `order` and `groups` in the `admin-bar` module to respect, rather that reverse, the order of items
13
- * Interacting with the text inside a rich text widget will hide the widget controls to prevent awkawrd text selection.
52
+ * Interacting with the text inside a rich text widget will hide the widget controls to prevent awkward text selection.
14
53
 
15
54
  ### Fixes
16
55
 
package/README.md CHANGED
@@ -1,60 +1,142 @@
1
- ![Unit Tests](https://github.com/apostrophecms/apostrophe/actions/workflows/main.yml/badge.svg)
2
- [![Chat on Discord](https://img.shields.io/discord/517772094482677790.svg)](https://chat.apostrophecms.org)
3
-
4
- <p align="center">
1
+ <div align="center">
5
2
  <a href="https://github.com/apostrophecms/apostrophe">
6
3
  <img src="logo.svg" alt="ApostropheCMS logo" width="80" height="80">
7
4
  </a>
8
5
 
9
- <h3 align="center">ApostropheCMS 3</h3>
6
+ <h1>ApostropheCMS</h1>
7
+
8
+ <p>
9
+ <a aria-label="Join the community on Discord" href="http://chat.apostrophecms.org">
10
+ <img alt="" src="https://img.shields.io/discord/517772094482677790?color=5865f2&label=Join%20the%20Discord&logo=discord&logoColor=fff&labelColor=000&style=for-the-badge&logoWidth=20" />
11
+ </a>
12
+ <a aria-label="License" href="https://github.com/apostrophecms/apostrophe/blob/main/LICENSE.md">
13
+ <img alt="" src="https://img.shields.io/static/v1?style=for-the-badge&labelColor=000000&label=License&message=MIT&color=3DA639" />
14
+ </a>
15
+ </p>
10
16
 
11
- <p align="center">
12
- ApostropheCMS is a full-featured, open source CMS built with Node.js that seeks to empower organizations by combining in-context editing and headless architecture in a full-stack JS environment.
17
+ <p>
18
+ <strong>Full-stack CMS for developers and content teams</strong><br />
19
+ Build websites with in-context editing and headless flexibility using Node.js and MongoDB.
13
20
  <br />
14
- <a href="https://v3.docs.apostrophecms.org/"><strong>Documentation »</strong></a>
21
+ <a href="https://docs.apostrophecms.org/"><strong>Documentation »</strong></a>
15
22
  <br />
16
23
  <br />
17
24
  <a href="http://demo.apostrophecms.com">Demo</a>
18
25
  ·
19
- <a href="https://portal.productboard.com/apostrophecms/1-product-roadmap/tabs/2-planned">Roadmap</a>
26
+ <a href="https://productlane.com/edit-roadmap">Roadmap</a>
20
27
  ·
21
28
  <a href="https://github.com/apostrophecms/apostrophe/issues/new?assignees=&labels=bug,3.0&template=bug_report.md&title=">Report Bug</a>
22
29
  </p>
23
- </p>
30
+ </div>
31
+
32
+ ## About
33
+
34
+ ApostropheCMS is a full-stack content management system built with Node.js and MongoDB. Content creators can edit directly on live pages without switching between admin interfaces, while developers can build with modern JavaScript or use it headlessly with any frontend framework.
35
+
36
+ ### Key Features
37
+
38
+ - **🎯 In-Context Editing** - Content creators edit directly on the live page, seeing changes instantly
39
+ - **⚡ Headless-Ready** - Use any frontend framework while keeping the powerful admin experience
40
+ - **🛠️ Developer-First** - Built with Node.js and MongoDB for full-stack JavaScript development
41
+ - **📈 Scales Beautifully** - From small sites to enterprise applications handling millions of pages
42
+ - **🔐 Enterprise Features** - Advanced permissions, workflow management, automated translations, and more
43
+
44
+ ## System Requirements
45
+
46
+ | Requirement | Version | Installation Notes |
47
+ |-------------|---------|-------------------|
48
+ | **Node.js** | 20.x+ | Use [NVM](https://github.com/nvm-sh/nvm) for version management |
49
+ | **MongoDB** | 6.0+ | [MongoDB Atlas](https://www.mongodb.com/atlas) (cloud) or local install |
50
+ | **npm** | 10.x+ | Included with Node.js |
51
+
52
+ See our [setup guides](https://docs.apostrophecms.org/guide/development-setup.html) for installation instructions.
53
+
54
+ ## Quick Start
55
+
56
+ Get ApostropheCMS running locally in minutes:
57
+
58
+ ```bash
59
+ # Option 1: Install CLI globally (recommended for multiple projects)
60
+ npm install -g @apostrophecms/cli
61
+ apos create my-website
62
+ cd my-website
63
+ npm run dev
64
+
65
+ # Option 2: Use npx for one-time project creation
66
+ npx @apostrophecms/cli create my-website
67
+ cd my-website
68
+ npm run dev
69
+ ```
24
70
 
25
- ## About ApostropheCMS
71
+ Your new ApostropheCMS site will be available at `http://localhost:3000` with a powerful admin interface at `/login`.
26
72
 
27
- ApostropheCMS is content software for everyone in an organization. It helps teams of all sizes create dynamic digital experiences with elegance and efficiency by blending powerful features, developer happiness, and a low learning curve for content creators. Apostrophe has powered websites and web apps for organizations large and small for over a decade.
73
+ ### Prefer to Go Headless?
28
74
 
29
- #### Built With
75
+ **Get started with Astro integration** - the easiest way to build headless sites while keeping visual editing:
30
76
 
31
- * [Node](https://nodejs.org/en/)
32
- * [MongoDB](https://www.mongodb.com/)
33
- * [Nunjucks](https://mozilla.github.io/nunjucks/)
77
+ - **[Apollo Starter Kit (Astro)](https://apostrophecms.com/starter-kits/apollo-starter-kit-for-astro-cms)** - Production-ready foundation with beautiful design system and rich content features
78
+ - **[Essentials Starter Kit (Astro)](git clone https://github.com/apostrophecms/starter-kit-astro-essentials)** - Minimal, clean foundation for building custom designs from scratch
34
79
 
35
- ## Getting Started
80
+ Both starter kits provide headless CMS power with in-context editing, letting content creators edit directly on the live site while you build with modern frontend tools. Our Astro integration handles all the content fetching automatically—no REST API calls to write.
36
81
 
37
- To get started with Apostrophe 3, follow these steps to set up a local development environment. For more detail, refer to the [A3 getting started guide](https://a3.docs.apostrophecms.org/guide/setting-up.html) in the documentation.
82
+ **Desire a different frontend framework?** Use our REST APIs with React, Vue, Svelte, or any other framework:
38
83
 
39
- #### Prerequisites
84
+ - **[REST API Documentation](https://docs.apostrophecms.org/reference/api/pieces.html)** - Complete API reference
85
+ - **[Headless CMS Guide](https://docs.apostrophecms.org/guide/headless-cms.html)** - Integration walkthrough for any framework
40
86
 
41
- We recommend installing the following with [Homebrew](https://brew.sh/) on macOS. If you're on Linux, you should use your package manager (apt or yum). If you're on Windows, we recommend the Windows Subsystem for Linux.
87
+ ### Hosting & Deployment
42
88
 
43
- | Software | Minimum Version | Notes
44
- | ------------- | ------------- | -----
45
- | Node.js | 12.x | Or better
46
- | npm | 6.x | Or better
47
- | MongoDB | 3.6 | Or better
48
- | Imagemagick | Any | Faster image uploads, GIF support (optional)
89
+ Choose [ApostropheCMS hosting](https://apostrophecms.com/hosting) for turnkey solutions with optimized performance and dedicated support, or deploy to [any platform where Node.js runs](https://docs.apostrophecms.org/guide/hosting.html).
49
90
 
50
- ## Community
91
+ ## Built With Modern Tech
51
92
 
52
- [Discord](https://discord.com/invite/XkbRNq7) - [Twitter](https://twitter.com/apostrophecms) - [Discussions](https://github.com/apostrophecms/apostrophe/discussions)
93
+ - **[Node.js](https://nodejs.org/)** - JavaScript runtime for server-side development
94
+ - **[MongoDB](https://www.mongodb.com/)** - Flexible document database for content storage
95
+ - **ESM Modules** - Native ES6 module support for modern JavaScript
96
+ - **Vite** - Lightning-fast build tool and development server
97
+ - **Modern JavaScript** - ES6+, async/await, and contemporary development patterns
98
+
99
+ ## Community & Support
100
+
101
+ **Join other developers and content creators using ApostropheCMS:**
102
+
103
+ - **[Discord](https://discord.com/invite/XkbRNq7)** - Get help, share projects, and connect with other users
104
+ - **[GitHub Discussions](https://github.com/apostrophecms/apostrophe/discussions)** - Feature requests, technical discussions, and community support
105
+ - **[Documentation](https://docs.apostrophecms.org/)** - Comprehensive guides, tutorials, and API references
53
106
 
54
107
  ## Contributing
55
108
 
56
- We eagerly welcome open source contributions. Before submitting a PR, please read through our [Contribution Guide](https://github.com/apostrophecms/apostrophe/blob/main/CONTRIBUTING.md)
109
+ We welcome contributions from the community! Whether you're fixing bugs, adding features, or improving documentation, your help makes ApostropheCMS better for everyone.
110
+
111
+ - **[Contribution Guide](https://github.com/apostrophecms/apostrophe/blob/main/CONTRIBUTING.md)** - How to contribute code, documentation, and feedback
112
+ - **[Good First Issues](https://github.com/apostrophecms/apostrophe/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)** - Perfect starting points for new contributors
113
+
114
+
115
+ ## Pro Features
116
+
117
+ **For teams and organizations requiring additional features:**
118
+
119
+ - **🔐 Advanced User Management** - Granular permissions, user groups, and access controls
120
+ - **🌍 Automated Translation** - AI-powered translation with DeepL, Google Translate, and Azure
121
+ - **📊 Analytics & SEO** - Built-in SEO optimization and content analytics
122
+ - **⚡ Performance Optimization** - Advanced caching, CDN integration, and performance monitoring
123
+ - **🏢 Multisite Management** - Manage multiple sites from a single dashboard with shared resources
124
+ - **💼 Professional Support** - Dedicated support, training, and consultation services
125
+
126
+ [Explore all the pro extensions](https://apostrophecms.com/extensions?autocomplete=&license=assembly&license=pro) and [sign up](https://app.apostrophecms.com/login) for a Pro license in our self-service Apostrophe Workspaces, or [contact us](https://apostrophecms.com/contact-us) to learn about licensing and support options.
57
127
 
58
128
  ## License
59
129
 
60
- ApostropheCMS is released under the [ MIT License](https://github.com/apostrophecms/apostrophe/blob/main/LICENSE.md).
130
+ ApostropheCMS is open source software licensed under the [MIT License](https://github.com/apostrophecms/apostrophe/blob/main/LICENSE.md). This means you're free to use, modify, and distribute it for both personal and commercial projects.
131
+
132
+ ---
133
+
134
+ <div align="center">
135
+ <p>
136
+ <strong>Ready to build something amazing?</strong><br>
137
+ <a href="https://docs.apostrophecms.org/">Get started with our documentation</a> or <a href="https://apostrophecms.com/contact-us">talk to our team</a>
138
+ </p>
139
+ <p>
140
+ <em>Built with ❤️ by the <a href="https://apostrophecms.com">ApostropheCMS team</a></em>
141
+ </p>
142
+ </div>
package/index.js CHANGED
@@ -350,7 +350,16 @@ async function apostrophe(options, telemetry, rootSpan) {
350
350
 
351
351
  return self;
352
352
  } catch (e) {
353
- if (options.exit !== false) {
353
+ if (options.exit === false) {
354
+ console.error('apostrophe: error occurred during startup, continuing:');
355
+ console.error(e);
356
+ // returns undefined, for legacy reasons
357
+ } else if (options.exit === 'throw') {
358
+ // A more sensible approach for those who want to do something
359
+ // if initialization fails
360
+ throw e;
361
+ } else {
362
+ // Longstanding default behavior
354
363
  console.error(e);
355
364
  await self._exit(1, e);
356
365
  }
@@ -72,6 +72,7 @@
72
72
  :is="item.options.component"
73
73
  v-if="item.options.component"
74
74
  :key="`${item.name}.component`"
75
+ :action="item.action"
75
76
  />
76
77
  <AposButton
77
78
  v-else
@@ -88,6 +88,7 @@ module.exports = {
88
88
 
89
89
  self.enableBrowserData();
90
90
  self.addDeduplicateWidgetIdsMigration();
91
+ self.createWidgetOperations = [];
91
92
  },
92
93
  apiRoutes(self) {
93
94
  return {
@@ -382,8 +383,8 @@ module.exports = {
382
383
  return;
383
384
  }
384
385
  // We're only rendering areas on the document, not ancestor or
385
- // child page documents.
386
- const regex = /^_(ancestors|children)|\._(ancestors|children)/;
386
+ // child page or related documents.
387
+ const regex = /^_|\._/;
387
388
  if (dotPath.match(regex)) {
388
389
  return;
389
390
  }
@@ -786,7 +787,6 @@ module.exports = {
786
787
  manager.options.initialModal !== false;
787
788
  contextualWidgetDefaultData[name] = manager.options.defaultData || {};
788
789
  });
789
-
790
790
  return {
791
791
  components: {
792
792
  editor: 'AposAreaEditor',
@@ -799,7 +799,8 @@ module.exports = {
799
799
  widgetPreview,
800
800
  contextualWidgetDefaultData,
801
801
  widgetManagers,
802
- action: self.action
802
+ action: self.action,
803
+ createWidgetOperations: self.createWidgetOperations
803
804
  };
804
805
  },
805
806
  async addDeduplicateWidgetIdsMigration() {
@@ -825,6 +826,9 @@ module.exports = {
825
826
  }
826
827
  });
827
828
  });
829
+ },
830
+ addCreateWidgetOperation(operation) {
831
+ self.createWidgetOperations.push(operation);
828
832
  }
829
833
  };
830
834
  },
@@ -69,7 +69,7 @@ module.exports = function(self) {
69
69
  items: []
70
70
  };
71
71
  doc[name] = area;
72
- const docId = doc._docId || doc._id;
72
+ const docId = doc._docId || ((doc.metaType === 'doc') ? doc._id : null);
73
73
  if (docId) {
74
74
  let mainDoc = await self.apos.doc.db.findOne({ _id: docId });
75
75
  if (!mainDoc) {
@@ -72,7 +72,7 @@
72
72
  <AposAreaMenuItem
73
73
  :item="child"
74
74
  :tabbable="itemIndex === active"
75
- @click="add(child)"
75
+ @click="action(child)"
76
76
  @up="switchItem(`child-${itemIndex}-${childIndex - 1}`, -1)"
77
77
  @down="switchItem(`child-${itemIndex}-${childIndex + 1}`, 1)"
78
78
  />
@@ -83,7 +83,7 @@
83
83
  <AposAreaMenuItem
84
84
  v-else
85
85
  :item="item"
86
- @click="add(item)"
86
+ @click="action(item)"
87
87
  @up="switchItem(`item-${itemIndex - 1}`, -1)"
88
88
  @down="switchItem(`item-${itemIndex + 1}`, 1)"
89
89
  />
@@ -130,6 +130,10 @@ export default {
130
130
  return {};
131
131
  }
132
132
  },
133
+ fieldId: {
134
+ type: String,
135
+ required: true
136
+ },
133
137
  menuId: {
134
138
  type: String,
135
139
  default() {
@@ -196,8 +200,14 @@ export default {
196
200
  // If the menu is not open, we don't need to compute it right now
197
201
  return [];
198
202
  }
199
- const clipboard = apos.area.widgetClipboard.get();
200
203
  const menu = [ ...this.contextMenuOptions.menu ];
204
+ for (const createWidgetOperation of this.moduleOptions.createWidgetOperations) {
205
+ menu.unshift({
206
+ type: 'operation',
207
+ ...createWidgetOperation
208
+ });
209
+ }
210
+ const clipboard = apos.area.widgetClipboard.get();
201
211
  if (clipboard) {
202
212
  const widget = clipboard;
203
213
  const matchingChoice = menu.find(option => option.name === widget.type);
@@ -226,7 +236,24 @@ export default {
226
236
  this.inContext = !apos.util.closest(this.$el, '[data-apos-schema-area]');
227
237
  },
228
238
  methods: {
229
- async add(item) {
239
+ async action(item) {
240
+ if (item.type === 'operation') {
241
+ const props = {
242
+ ...item.props,
243
+ options: this.options,
244
+ fieldId: this.fieldId
245
+ };
246
+ this.$refs.contextMenu.hide();
247
+ const widget = await apos.modal.execute(item.modal, props);
248
+ if (widget) {
249
+ // Insert the widget at the appropriate insertion point, like we normally would
250
+ this.$emit('add', {
251
+ widget,
252
+ index: this.index
253
+ });
254
+ }
255
+ return;
256
+ }
230
257
  // Potential TODO: If we find ourselves manually flipping these bits in
231
258
  // other AposContextMenu overrides we should consider refactoring
232
259
  // contextmenus to be able to self close when any click takes place within
@@ -32,6 +32,7 @@
32
32
  :empty="true"
33
33
  :index="0"
34
34
  :options="options"
35
+ :field-id="fieldId"
35
36
  :max-reached="maxReached"
36
37
  :disabled="field && field.readOnly"
37
38
  :widget-options="options.widgets"
@@ -80,9 +81,9 @@
80
81
 
81
82
  <script>
82
83
  import { createId } from '@paralleldrive/cuid2';
83
- import { klona } from 'klona';
84
84
  import AposThemeMixin from 'Modules/@apostrophecms/ui/mixins/AposThemeMixin';
85
85
  import newInstance from 'apostrophe/modules/@apostrophecms/schema/lib/newInstance.js';
86
+ import cloneWidget from 'Modules/@apostrophecms/area/lib/clone-widget.js';
86
87
 
87
88
  export default {
88
89
  name: 'AposAreaEditor',
@@ -546,12 +547,7 @@ export default {
546
547
  }
547
548
  },
548
549
  clone(index) {
549
- const widget = klona(this.next[index]);
550
- delete widget._id;
551
- this.regenerateIds(
552
- apos.modules[apos.area.widgetManagers[widget.type]].schema,
553
- widget
554
- );
550
+ const widget = cloneWidget(this.next[index]);
555
551
  this.insert({
556
552
  widget,
557
553
  index: index + 1
@@ -572,30 +568,6 @@ export default {
572
568
  }
573
569
  }
574
570
  },
575
- // Regenerate all array item, area, object and widget ids so they are considered
576
- // new. Useful when copying a widget with nested content.
577
- regenerateIds(schema, object) {
578
- object._id = createId();
579
- for (const field of schema) {
580
- if (field.type === 'array') {
581
- for (const item of (object[field.name] || [])) {
582
- this.regenerateIds(field.schema, item);
583
- }
584
- } else if (field.type === 'object') {
585
- this.regenerateIds(field.schema, object[field.name] || {});
586
- } else if (field.type === 'area') {
587
- if (object[field.name]) {
588
- object[field.name]._id = createId();
589
- for (const item of (object[field.name].items || [])) {
590
- const schema = apos.modules[apos.area.widgetManagers[item.type]].schema;
591
- this.regenerateIds(schema, item);
592
- }
593
- }
594
- }
595
- // We don't want to regenerate attachment ids. They correspond to
596
- // actual files, and the reference count will update automatically
597
- }
598
- },
599
571
  async update(updated, { autosave = true, reverting = false } = {}) {
600
572
  if (!reverting) {
601
573
  updated.aposPlaceholder = false;
@@ -616,22 +588,30 @@ export default {
616
588
  });
617
589
  this.edited[updated._id] = true;
618
590
  },
619
- // Add a widget into an area.
591
+ // Add a widget into an area. index is required, along
592
+ // with one and only one of name, widget or clipboard.
593
+ // If widget is passed it is inserted directly. If
594
+ // clipboard is passed it is cloned and inserted.
620
595
  async add({
621
596
  index,
622
597
  name,
598
+ widget,
623
599
  clipboard
624
600
  }) {
625
601
  if (clipboard) {
626
- this.regenerateIds(
627
- apos.modules[apos.area.widgetManagers[clipboard.type]].schema,
628
- clipboard
629
- );
602
+ clipboard = cloneWidget(clipboard);
630
603
  return this.insert({
631
604
  widget: clipboard,
632
605
  index
633
606
  });
634
- } else if (this.widgetIsContextual(name)) {
607
+ }
608
+ if (widget) {
609
+ return this.insert({
610
+ widget,
611
+ index
612
+ });
613
+ }
614
+ if (this.widgetIsContextual(name)) {
635
615
  return this.insert({
636
616
  widget: {
637
617
  type: name,
@@ -640,35 +620,36 @@ export default {
640
620
  },
641
621
  index
642
622
  });
643
- } else if (!this.widgetHasInitialModal(name)) {
644
- const widget = this.newWidget(name);
623
+ }
624
+ if (!this.widgetHasInitialModal(name)) {
625
+ const newWidget = this.newWidget(name);
645
626
  return this.insert({
646
627
  widget: {
647
- ...widget,
628
+ ...newWidget,
648
629
  aposPlaceholder: this.widgetHasPlaceholder(name)
649
630
  },
650
631
  index
651
632
  });
652
- } else {
653
- const componentName = this.widgetEditorComponent(name);
654
- apos.area.activeEditor = this;
655
- const preview = this.widgetPreview(name, index, true);
656
- const widget = await apos.modal.execute(componentName, {
657
- modelValue: null,
658
- options: this.widgetOptionsByType(name),
659
- type: name,
660
- docId: this.docId,
661
- areaFieldId: this.fieldId,
662
- parentFollowingValues: this.followingValues,
663
- preview
633
+ }
634
+
635
+ const componentName = this.widgetEditorComponent(name);
636
+ apos.area.activeEditor = this;
637
+ const preview = this.widgetPreview(name, index, true);
638
+ const newWidget = await apos.modal.execute(componentName, {
639
+ modelValue: null,
640
+ options: this.widgetOptionsByType(name),
641
+ type: name,
642
+ docId: this.docId,
643
+ areaFieldId: this.fieldId,
644
+ parentFollowingValues: this.followingValues,
645
+ preview
646
+ });
647
+ apos.area.activeEditor = null;
648
+ if (newWidget) {
649
+ return this.insert({
650
+ widget: newWidget,
651
+ index
664
652
  });
665
- apos.area.activeEditor = null;
666
- if (widget) {
667
- return this.insert({
668
- widget,
669
- index
670
- });
671
- }
672
653
  }
673
654
  },
674
655
  widgetOptionsByType(name) {