├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── .jshintrc
├── .nycrc
├── .stylelintrc
├── .travis.yml
├── ApostropheCMS_logo.png
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── DEVELOPMENT.md
├── LICENSE.md
├── README.md
├── defaults.js
├── deploy-test-count
├── force-deploy
├── index.js
├── lib
├── check-if-conditions.js
├── escape-host.js
├── glob.js
├── locales.js
├── mongodb-connect.js
├── moog-require.js
├── moog.js
└── opentelemetry.js
├── logo.svg
├── modules
└── @apostrophecms
│ ├── admin-bar
│ ├── index.js
│ ├── ui
│ │ ├── apos
│ │ │ ├── apps
│ │ │ │ └── AposAdminBar.js
│ │ │ └── components
│ │ │ │ ├── TheAposAdminBar.vue
│ │ │ │ ├── TheAposAdminBarLocale.vue
│ │ │ │ ├── TheAposAdminBarMenu.vue
│ │ │ │ ├── TheAposAdminBarUser.vue
│ │ │ │ ├── TheAposContextBar.vue
│ │ │ │ ├── TheAposContextBreakpointPreviewMode.vue
│ │ │ │ ├── TheAposContextModeAndSettings.vue
│ │ │ │ ├── TheAposContextTitle.vue
│ │ │ │ ├── TheAposContextUndoRedo.vue
│ │ │ │ └── TheAposSavingIndicator.vue
│ │ └── src
│ │ │ └── index.js
│ └── views
│ │ └── adminBar.html
│ ├── any-doc-type
│ └── index.js
│ ├── any-page-type
│ └── index.js
│ ├── archive-page
│ └── index.js
│ ├── area
│ ├── index.js
│ ├── lib
│ │ └── custom-tags
│ │ │ ├── area.js
│ │ │ └── widget.js
│ ├── ui
│ │ └── apos
│ │ │ ├── apps
│ │ │ └── AposAreas.js
│ │ │ └── components
│ │ │ ├── AposAreaContextualMenu.vue
│ │ │ ├── AposAreaEditor.vue
│ │ │ ├── AposAreaExpandedMenu.vue
│ │ │ ├── AposAreaMenu.vue
│ │ │ ├── AposAreaMenuItem.vue
│ │ │ ├── AposAreaWidget.vue
│ │ │ └── AposWidgetControls.vue
│ └── views
│ │ └── area.html
│ ├── asset
│ ├── index.js
│ ├── lib
│ │ ├── build
│ │ │ ├── external-module-api.js
│ │ │ ├── internals.js
│ │ │ ├── manager-apos.js
│ │ │ ├── manager-bundled.js
│ │ │ ├── manager-custom.js
│ │ │ ├── manager-index.js
│ │ │ ├── managers.js
│ │ │ ├── task.js
│ │ │ └── utils.js
│ │ ├── globalIcons.js
│ │ ├── refresh-on-restart.js
│ │ └── webpack
│ │ │ ├── apos
│ │ │ ├── webpack.config.js
│ │ │ ├── webpack.js.js
│ │ │ ├── webpack.scss.js
│ │ │ └── webpack.vue.js
│ │ │ ├── src-es5
│ │ │ └── webpack.config.js
│ │ │ ├── src
│ │ │ ├── webpack.config.js
│ │ │ └── webpack.scss.js
│ │ │ └── utils.js
│ └── views
│ │ ├── scripts.html
│ │ └── stylesheets.html
│ ├── attachment
│ ├── index.js
│ ├── lib
│ │ ├── legacy-migrations.js
│ │ └── tasks
│ │ │ ├── download-all.js
│ │ │ └── rescale.js
│ └── public
│ │ └── img
│ │ └── missing-icon.svg
│ ├── busy
│ ├── index.js
│ └── ui
│ │ └── apos
│ │ ├── apps
│ │ └── AposBusy.js
│ │ └── components
│ │ └── TheAposBusy.vue
│ ├── cache
│ └── index.js
│ ├── color-field
│ ├── index.js
│ └── ui
│ │ └── apos
│ │ ├── components
│ │ ├── AposColor.vue
│ │ └── AposInputColor.vue
│ │ ├── lib
│ │ ├── AposColorAlpha.vue
│ │ ├── AposColorEditableInput.vue
│ │ ├── AposColorHue.vue
│ │ ├── AposColorInfo.vue
│ │ └── AposColorSaturation.vue
│ │ ├── logic
│ │ └── AposInputColor.js
│ │ └── mixins
│ │ └── AposColorMixin.js
│ ├── command-menu
│ ├── index.js
│ └── ui
│ │ └── apos
│ │ ├── apps
│ │ └── AposCommandMenu.js
│ │ └── components
│ │ ├── AposCommandMenuKey.vue
│ │ ├── AposCommandMenuKeyList.vue
│ │ ├── AposCommandMenuShortcut.vue
│ │ └── TheAposCommandMenu.vue
│ ├── db
│ └── index.js
│ ├── doc-type
│ ├── index.js
│ ├── lib
│ │ ├── autocomplete.js
│ │ └── extendQueries.js
│ └── ui
│ │ └── apos
│ │ ├── components
│ │ ├── AposDocContextMenu.vue
│ │ ├── AposDocEditor.vue
│ │ └── AposDocLocalePicker.vue
│ │ └── logic
│ │ └── AposDocContextMenu.js
│ ├── doc
│ ├── index.js
│ ├── lib
│ │ ├── legacy-migrations.js
│ │ └── migrations.js
│ └── ui
│ │ └── apos
│ │ ├── apps
│ │ └── AposDoc.js
│ │ └── mixins
│ │ └── AposFieldMetaUtilsMixin.js
│ ├── email
│ └── index.js
│ ├── error
│ └── index.js
│ ├── express
│ └── index.js
│ ├── file-tag
│ └── index.js
│ ├── file
│ └── index.js
│ ├── global
│ └── index.js
│ ├── home-page
│ └── index.js
│ ├── html-widget
│ ├── index.js
│ └── views
│ │ ├── render.html
│ │ └── widget.html
│ ├── http
│ ├── index.js
│ ├── lib
│ │ └── big-upload-middleware.js
│ └── ui
│ │ └── apos
│ │ ├── big-upload-client.js
│ │ └── package.json
│ ├── i18n
│ ├── i18n
│ │ ├── de.json
│ │ ├── en.json
│ │ ├── es.json
│ │ ├── fr.json
│ │ ├── it.json
│ │ ├── pt-BR.json
│ │ └── sk.json
│ ├── index.js
│ └── ui
│ │ ├── apos
│ │ ├── apps
│ │ │ ├── AposI18nBatchReporting.js
│ │ │ └── AposI18nCrossDomainSession.js
│ │ └── components
│ │ │ └── AposI18nLocalize.vue
│ │ └── src
│ │ └── index.js
│ ├── image-tag
│ └── index.js
│ ├── image-widget
│ ├── index.js
│ ├── public
│ │ └── placeholder.jpg
│ ├── ui
│ │ └── src
│ │ │ └── index.scss
│ └── views
│ │ └── widget.html
│ ├── image
│ ├── index.js
│ └── ui
│ │ └── apos
│ │ ├── apps
│ │ └── AposImageRelationshipQueryFilter.js
│ │ ├── components
│ │ ├── AposImageCropper.vue
│ │ ├── AposImageRelationshipEditor.vue
│ │ ├── AposMediaManager.vue
│ │ ├── AposMediaManagerDisplay.vue
│ │ ├── AposMediaManagerEditor.vue
│ │ ├── AposMediaManagerSelections.vue
│ │ └── AposMediaUploader.vue
│ │ └── lib
│ │ └── aspectRatios.js
│ ├── job
│ └── index.js
│ ├── launder
│ └── index.js
│ ├── lock
│ └── index.js
│ ├── log
│ └── index.js
│ ├── login
│ ├── index.js
│ ├── ui
│ │ └── apos
│ │ │ ├── apps
│ │ │ └── AposLogin.js
│ │ │ ├── components
│ │ │ ├── AposForgotPasswordForm.vue
│ │ │ ├── AposLoginForm.vue
│ │ │ ├── AposResetPasswordForm.vue
│ │ │ ├── TheAposLogin.vue
│ │ │ └── TheAposLoginHeader.vue
│ │ │ ├── logic
│ │ │ ├── AposForgotPasswordForm.js
│ │ │ ├── AposLoginForm.js
│ │ │ ├── AposResetPasswordForm.js
│ │ │ └── TheAposLogin.js
│ │ │ └── mixins
│ │ │ └── AposLoginFormMixin.js
│ └── views
│ │ ├── login.html
│ │ └── passwordResetEmail.html
│ ├── migration
│ ├── index.js
│ └── lib
│ │ └── addMissingSchemaFields.js
│ ├── modal
│ ├── index.js
│ └── ui
│ │ └── apos
│ │ ├── apps
│ │ └── AposModals.js
│ │ ├── components
│ │ ├── AposDocsManagerToolbar.vue
│ │ ├── AposModal.vue
│ │ ├── AposModalBody.vue
│ │ ├── AposModalBreadcrumbs.vue
│ │ ├── AposModalConfirm.vue
│ │ ├── AposModalLip.vue
│ │ ├── AposModalRail.vue
│ │ ├── AposModalReport.vue
│ │ ├── AposModalShareDraft.vue
│ │ ├── AposModalTabs.vue
│ │ ├── AposModalTabsBody.vue
│ │ ├── AposModalToolbar.vue
│ │ ├── AposWidgetModalTabs.vue
│ │ └── TheAposModals.vue
│ │ ├── composables
│ │ └── AposFocus.js
│ │ └── mixins
│ │ ├── AposDocErrorsMixin.js
│ │ ├── AposDocsManagerMixin.js
│ │ ├── AposEditorMixin.js
│ │ └── AposModalTabsMixin.js
│ ├── module
│ ├── index.js
│ └── lib
│ │ ├── events.js
│ │ └── log.js
│ ├── multisite-i18n
│ ├── i18n
│ │ └── aposMultisite
│ │ │ └── en.json
│ └── index.js
│ ├── notification
│ ├── index.js
│ └── ui
│ │ └── apos
│ │ ├── apps
│ │ └── AposNotification.js
│ │ └── components
│ │ ├── AposNotification.vue
│ │ └── TheAposNotifications.vue
│ ├── oembed-field
│ ├── index.js
│ └── ui
│ │ └── apos
│ │ └── components
│ │ └── AposInputOembed.vue
│ ├── oembed
│ ├── index.js
│ └── lib
│ │ ├── infogram.js
│ │ ├── vimeo.js
│ │ ├── wufoo.js
│ │ └── youtube.js
│ ├── page-type
│ └── index.js
│ ├── page
│ ├── index.js
│ ├── lib
│ │ └── legacy-migrations.js
│ ├── ui
│ │ └── apos
│ │ │ ├── components
│ │ │ └── AposPagesManager.vue
│ │ │ └── logic
│ │ │ └── AposPagesManager.js
│ └── views
│ │ └── notFound.html
│ ├── pager
│ ├── index.js
│ └── views
│ │ └── macros.html
│ ├── permission
│ ├── index.js
│ ├── lib
│ │ └── legacy-migrations.js
│ └── ui
│ │ └── apos
│ │ └── components
│ │ ├── AposInputRole.vue
│ │ └── AposPermissionGrid.vue
│ ├── piece-page-type
│ ├── index.js
│ └── views
│ │ ├── index.html
│ │ └── show.html
│ ├── piece-type
│ ├── index.js
│ └── ui
│ │ └── apos
│ │ └── components
│ │ ├── AposDocsManager.vue
│ │ ├── AposDocsManagerDisplay.vue
│ │ ├── AposDocsManagerSelectBox.vue
│ │ ├── AposRelationshipEditor.vue
│ │ └── AposUtilityOperations.vue
│ ├── polymorphic-type
│ ├── index.js
│ └── lib
│ │ └── migrations.js
│ ├── rich-text-widget
│ ├── index.js
│ ├── lib
│ │ ├── apiRoutes.js
│ │ └── generateTiptapTable.js
│ ├── ui
│ │ └── apos
│ │ │ ├── apps
│ │ │ └── AposRichTextPermalinkResolver.js
│ │ │ ├── components
│ │ │ ├── AposImageControlDialog.vue
│ │ │ ├── AposRichTextWidgetEditor.vue
│ │ │ ├── AposTiptapAnchor.vue
│ │ │ ├── AposTiptapButton.vue
│ │ │ ├── AposTiptapColor.vue
│ │ │ ├── AposTiptapDivider.vue
│ │ │ ├── AposTiptapImage.vue
│ │ │ ├── AposTiptapImportTable.vue
│ │ │ ├── AposTiptapInsertBtn.vue
│ │ │ ├── AposTiptapInsertItem.vue
│ │ │ ├── AposTiptapLink.vue
│ │ │ ├── AposTiptapMarks.vue
│ │ │ ├── AposTiptapStyles.vue
│ │ │ ├── AposTiptapTableControls.vue
│ │ │ └── AposTiptapUndefined.vue
│ │ │ └── tiptap-extensions
│ │ │ ├── Anchor.js
│ │ │ ├── Classes.js
│ │ │ ├── Color.js
│ │ │ ├── Default.js
│ │ │ ├── Div.js
│ │ │ ├── Document.js
│ │ │ ├── Heading.js
│ │ │ ├── Image.js
│ │ │ ├── Link.js
│ │ │ ├── ListItem.js
│ │ │ └── TextStyle.js
│ └── views
│ │ └── widget.html
│ ├── schema
│ ├── index.js
│ ├── lib
│ │ ├── addFieldTypes.js
│ │ ├── joinr.js
│ │ └── newInstance.js
│ └── ui
│ │ └── apos
│ │ ├── components
│ │ ├── AposArrayEditor.vue
│ │ ├── AposInputArea.vue
│ │ ├── AposInputArray.vue
│ │ ├── AposInputAttachment.vue
│ │ ├── AposInputBoolean.vue
│ │ ├── AposInputCheckboxes.vue
│ │ ├── AposInputDateAndTime.vue
│ │ ├── AposInputObject.vue
│ │ ├── AposInputPassword.vue
│ │ ├── AposInputRadio.vue
│ │ ├── AposInputRange.vue
│ │ ├── AposInputRelationship.vue
│ │ ├── AposInputSelect.vue
│ │ ├── AposInputSlug.vue
│ │ ├── AposInputString.vue
│ │ ├── AposInputWrapper.vue
│ │ ├── AposLogo.vue
│ │ ├── AposLogoIcon.vue
│ │ ├── AposLogoPadless.vue
│ │ ├── AposSchema.vue
│ │ ├── AposSearchList.vue
│ │ └── AposSubform.vue
│ │ ├── lib
│ │ ├── conditionalFields.js
│ │ └── detectChange.js
│ │ ├── logic
│ │ ├── AposArrayEditor.js
│ │ ├── AposInputArea.js
│ │ ├── AposInputArray.js
│ │ ├── AposInputAttachment.js
│ │ ├── AposInputBoolean.js
│ │ ├── AposInputCheckboxes.js
│ │ ├── AposInputDateAndTime.js
│ │ ├── AposInputObject.js
│ │ ├── AposInputPassword.js
│ │ ├── AposInputRadio.js
│ │ ├── AposInputRange.js
│ │ ├── AposInputRelationship.js
│ │ ├── AposInputSelect.js
│ │ ├── AposInputSlug.js
│ │ ├── AposInputString.js
│ │ ├── AposInputWrapper.js
│ │ ├── AposSchema.js
│ │ ├── AposSearchList.js
│ │ └── AposSubform.js
│ │ ├── mixins
│ │ ├── AposInputChoicesMixin.js
│ │ ├── AposInputConditionalFieldsMixin.js
│ │ ├── AposInputFollowingMixin.js
│ │ └── AposInputMixin.js
│ │ └── scss
│ │ └── AposInputArray.scss
│ ├── search
│ ├── index.js
│ └── views
│ │ ├── index.html
│ │ ├── indexAjax.html
│ │ ├── pager.html
│ │ └── suggest.html
│ ├── settings
│ ├── index.js
│ └── ui
│ │ └── apos
│ │ ├── apps
│ │ └── TheAposSettings.js
│ │ ├── components
│ │ └── AposSettingsManager.vue
│ │ └── logic
│ │ └── AposSettingsManager.js
│ ├── soft-redirect
│ └── index.js
│ ├── submitted-draft
│ ├── index.js
│ └── ui
│ │ └── apos
│ │ └── components
│ │ └── AposSubmittedDraftIcon.vue
│ ├── task
│ └── index.js
│ ├── template
│ ├── index.js
│ ├── lib
│ │ ├── bundlesLoader.js
│ │ ├── custom-tags
│ │ │ ├── component.js
│ │ │ ├── fragment.js
│ │ │ └── render.js
│ │ └── nunjucksLoader.js
│ └── views
│ │ ├── inject.html
│ │ ├── outerLayout.html
│ │ ├── outerLayoutBase.html
│ │ ├── refreshLayout.html
│ │ └── templateError.html
│ ├── translation
│ ├── index.js
│ └── ui
│ │ └── apos
│ │ └── components
│ │ └── AposTranslationIndicator.vue
│ ├── ui
│ ├── index.js
│ └── ui
│ │ └── apos
│ │ ├── apps
│ │ └── AposBusEvent.js
│ │ ├── components
│ │ ├── AposAvatar.vue
│ │ ├── AposButton.vue
│ │ ├── AposButtonGroup.vue
│ │ ├── AposButtonSplit.vue
│ │ ├── AposCellBasic.vue
│ │ ├── AposCellButton.vue
│ │ ├── AposCellContextMenu.vue
│ │ ├── AposCellDate.vue
│ │ ├── AposCellLabels.vue
│ │ ├── AposCellLastEdited.vue
│ │ ├── AposCellLink.vue
│ │ ├── AposCellType.vue
│ │ ├── AposCheckbox.vue
│ │ ├── AposCloudUploadIcon.vue
│ │ ├── AposColorCheckerboard.vue
│ │ ├── AposCombo.vue
│ │ ├── AposContextMenu.vue
│ │ ├── AposContextMenuDialog.vue
│ │ ├── AposContextMenuItem.vue
│ │ ├── AposContextMenuTip.vue
│ │ ├── AposEmptyState.vue
│ │ ├── AposFile.vue
│ │ ├── AposFilterMenu.vue
│ │ ├── AposIndicator.vue
│ │ ├── AposLabel.vue
│ │ ├── AposLoading.vue
│ │ ├── AposLoadingBlock.vue
│ │ ├── AposLocale.vue
│ │ ├── AposLocalePicker.vue
│ │ ├── AposMinMaxCount.vue
│ │ ├── AposPager.vue
│ │ ├── AposPagerDots.vue
│ │ ├── AposSelect.vue
│ │ ├── AposSlat.vue
│ │ ├── AposSlatList.vue
│ │ ├── AposSpinner.vue
│ │ ├── AposSubformPreview.vue
│ │ ├── AposTable.vue
│ │ ├── AposTag.vue
│ │ ├── AposTagApply.vue
│ │ ├── AposTagList.vue
│ │ ├── AposTagListItem.vue
│ │ ├── AposToggle.vue
│ │ ├── AposTree.vue
│ │ ├── AposTreeHeader.vue
│ │ └── AposTreeRows.vue
│ │ ├── composables
│ │ ├── AposFocusTrap.js
│ │ └── AposTheme.js
│ │ ├── lib
│ │ ├── click-outside-element.js
│ │ ├── i18next.js
│ │ ├── tooltip.js
│ │ └── vue.js
│ │ ├── mixins
│ │ ├── AposAdvisoryLockMixin.js
│ │ ├── AposArchiveMixin.js
│ │ ├── AposCellMixin.js
│ │ ├── AposModifiedMixin.js
│ │ ├── AposPublishMixin.js
│ │ └── AposThemeMixin.js
│ │ ├── package.json
│ │ ├── scss
│ │ ├── global
│ │ │ ├── _admin.scss
│ │ │ ├── _breakpoint_preview.scss
│ │ │ ├── _inputs.scss
│ │ │ ├── _normalize.scss
│ │ │ ├── _rich-text-table.scss
│ │ │ ├── _scrollbars.scss
│ │ │ ├── _tables.scss
│ │ │ ├── _theme.scss
│ │ │ ├── _tooltips.scss
│ │ │ ├── _utilities.scss
│ │ │ ├── _widgets.scss
│ │ │ └── import-all.scss
│ │ ├── mixins
│ │ │ ├── _admin_mixins.scss
│ │ │ ├── _input_mixins.scss
│ │ │ ├── _mixins.scss
│ │ │ ├── _responsive.scss
│ │ │ ├── _theme_mixins.scss
│ │ │ ├── _type_mixins.scss
│ │ │ ├── _zindex.scss
│ │ │ └── import-all.scss
│ │ └── shared
│ │ │ ├── _table-rows.scss
│ │ │ └── _table-vars.scss
│ │ ├── stores
│ │ ├── modal.js
│ │ └── notification.js
│ │ └── utils
│ │ └── index.js
│ ├── uploadfs
│ └── index.js
│ ├── url
│ └── index.js
│ ├── user
│ ├── index.js
│ └── lib
│ │ ├── legacy-migrations.js
│ │ └── password-hash.js
│ ├── util
│ ├── index.js
│ ├── lib
│ │ └── logger.js
│ └── ui
│ │ └── src
│ │ ├── http.js
│ │ ├── index.js
│ │ └── util.js
│ ├── video-widget
│ ├── index.js
│ ├── ui
│ │ └── src
│ │ │ └── index.js
│ └── views
│ │ └── widget.html
│ └── widget-type
│ ├── index.js
│ └── ui
│ └── apos
│ ├── components
│ ├── AposWidget.vue
│ └── AposWidgetEditor.vue
│ └── mixins
│ └── AposWidgetMixin.js
├── package.json
├── scripts
├── README.txt
├── find-busted-test.bash
├── find-heavy-npm-modules
├── great-renaming.js
└── lint-i18n.js
├── test-lib
├── test.js
└── util.js
└── test
├── add-missing-schema-fields.js
├── admin-bar.js
├── areas.js
├── asset-external.js
├── assets.js
├── attachments.js
├── autocomplete.js
├── base-module.js
├── base-url-env-var.js
├── big-upload.js
├── bootstrapping.js
├── bundle.js
├── caches.js
├── change-doc-ids.js
├── command-menu.js
├── common-js.js
├── concurrent-array-relationships.js
├── content-i18n.js
├── data
├── .gitignore
├── fpw_email_mock.js
├── local.js
├── local_fn.js
├── local_fn_b.js
└── upload_tests
│ ├── bad_test.exe
│ ├── clone.txt
│ ├── crop_image.jpeg
│ ├── crop_image.png
│ ├── tiny.mp4
│ ├── updateTrash_apos_api.txt
│ ├── upload_apos_api.txt
│ ├── upload_express_api.txt
│ └── upload_image.png
├── db.js
├── docs.js
├── draft-published.js
├── email.js
├── esm-project
├── app.js
├── esm.js
└── package.json
├── events.js
├── events2.js
├── express.js
├── external-front.js
├── extra_node_modules
├── @company
│ └── bundle
│ │ ├── index.js
│ │ └── ui
│ │ └── src
│ │ ├── company.js
│ │ └── company.scss
├── before-global
│ └── index.js
├── improve-global
│ └── index.js
└── improve-piece-type
│ └── index.js
├── field-meta.js
├── follow-ancestors.js
├── global.js
├── http.js
├── i18n-batch.js
├── i18n.js
├── images.js
├── improve-overrides.js
├── job.js
├── launder.js
├── locks.js
├── log.js
├── login-requirements.js
├── login.js
├── middleware-and-route-order.js
├── modules-order.js
├── modules
├── @apostrophecms
│ ├── global
│ │ └── index.js
│ ├── home-page
│ │ ├── ui
│ │ │ └── src
│ │ │ │ ├── main.js
│ │ │ │ └── topic.js
│ │ └── views
│ │ │ └── page.html
│ ├── module
│ │ └── index.js
│ ├── page
│ │ └── views
│ │ │ └── notFound.html
│ ├── search
│ │ └── views
│ │ │ └── index.html
│ ├── template
│ │ └── views
│ │ │ ├── included.html
│ │ │ ├── layout.html
│ │ │ └── refreshLayout.html
│ ├── test-module-push
│ │ └── index.js
│ ├── test-module
│ │ └── index.js
│ └── user
│ │ └── index.js
├── @company
│ └── bundle
│ │ ├── index.js
│ │ └── ui
│ │ └── src
│ │ ├── company.js
│ │ └── company.scss
├── apos-fr
│ └── i18n
│ │ └── fr.json
├── args-bad-page
│ ├── index.js
│ └── views
│ │ └── page.html
├── args-good-page
│ ├── index.js
│ └── views
│ │ └── page.html
├── args-widget
│ ├── index.js
│ └── views
│ │ └── widget.html
├── article-page
│ ├── index.js
│ └── ui
│ │ └── src
│ │ ├── index.js
│ │ ├── main.js
│ │ └── main.scss
├── article-widget
│ ├── index.js
│ └── ui
│ │ └── src
│ │ ├── carousel.js
│ │ ├── carousel.scss
│ │ └── topic.js
├── base-type
│ └── i18n
│ │ ├── custom
│ │ └── en.json
│ │ └── en.json
├── bundle-edge
│ ├── index.js
│ └── ui
│ │ └── src
│ │ ├── edge.js
│ │ └── edge.scss
├── bundle-page-type
│ ├── index.js
│ └── ui
│ │ └── src
│ │ ├── another.js
│ │ ├── index.js
│ │ ├── index.scss
│ │ ├── main.js
│ │ └── main.scss
├── bundle-page
│ ├── index.js
│ ├── ui
│ │ └── src
│ │ │ ├── extra.js
│ │ │ ├── extra.scss
│ │ │ ├── main.js
│ │ │ └── main.scss
│ └── views
│ │ ├── index.html
│ │ └── show.html
├── bundle-widget
│ ├── index.js
│ ├── ui
│ │ └── src
│ │ │ └── extra2.js
│ └── views
│ │ └── widget.html
├── bundle
│ └── index.js
├── default-page
│ ├── index.js
│ ├── ui
│ │ ├── apos
│ │ │ └── components
│ │ │ │ └── FakeComponent.vue
│ │ ├── public
│ │ │ ├── index.css
│ │ │ └── index.js
│ │ └── src
│ │ │ ├── index.js
│ │ │ └── index.scss
│ └── views
│ │ └── page.html
├── email-test
│ └── views
│ │ └── welcome.html
├── event-widget
│ └── views
│ │ └── widget.html
├── example
│ └── i18n
│ │ └── en.json
├── express-test
│ └── index.js
├── fragment-all
│ └── views
│ │ ├── aux-test.html
│ │ ├── fragment-print.html
│ │ ├── fragment.html
│ │ ├── macro.html
│ │ ├── page.html
│ │ └── test.html
├── fragment-page
│ └── views
│ │ ├── fragment.html
│ │ ├── page.html
│ │ └── test.html
├── i18n-test-page
│ └── views
│ │ └── page.html
├── inject-test
│ ├── index.js
│ └── views
│ │ ├── appendBodyTest.html
│ │ ├── appendDevTest.html
│ │ ├── appendDevViteTest.html
│ │ ├── appendDevWebpackTest.html
│ │ ├── appendHeadTest.html
│ │ ├── appendProdWebpackTest.html
│ │ ├── prependDevTest.html
│ │ ├── prependDevViteTest.html
│ │ ├── prependDevWebpackTest.html
│ │ ├── prependHeadTest.html
│ │ ├── prependProdTest.html
│ │ ├── prependViteTest.html
│ │ └── prependWebpackTest.html
├── nested-module-subdirs
│ ├── example1
│ │ └── index.js
│ └── modules.js
├── nifty-page
│ └── views
│ │ ├── bar.html
│ │ ├── foo.html
│ │ ├── foo2.html
│ │ └── index.html
├── placeholder-page
│ ├── index.js
│ └── views
│ │ └── page.html
├── placeholder-widget
│ ├── index.js
│ └── views
│ │ └── widget.html
├── recursion-test-page
│ ├── index.js
│ └── views
│ │ ├── page.html
│ │ └── test.html
├── same-name-as-transitive-dependency
│ └── index.js
├── selected-article-widget
│ ├── index.js
│ └── ui
│ │ └── src
│ │ └── tabs.js
├── subtype
│ ├── i18n
│ │ └── custom
│ │ │ └── en.json
│ └── index.js
├── template-options-test
│ ├── index.js
│ └── views
│ │ └── options-test.html
├── template-subclass-test
│ ├── index.js
│ └── views
│ │ └── override-test.html
├── template-test
│ ├── index.js
│ └── views
│ │ ├── inherit-test.html
│ │ ├── override-test.html
│ │ ├── page.html
│ │ ├── pageWithLayout.html
│ │ ├── test.html
│ │ ├── testWithNlbrFilter.html
│ │ ├── testWithNlbrFilterSafe.html
│ │ └── testWithNlpFilter.html
├── test-before
│ └── index.js
├── test-get-option-2
│ └── index.js
├── test-get-option
│ ├── index.js
│ └── views
│ │ └── test.html
├── test-page
│ └── views
│ │ └── page.html
└── with-layout-page
│ └── views
│ └── page.html
├── moog.js
├── oembed.js
├── page-type.js
├── pages-autocomplete.js
├── pages-public-api.js
├── pages-rest.js
├── pages.js
├── parked-pages.js
├── password-hash.js
├── permissions.js
├── pieces-children
└── pieces-malformed-child.js
├── pieces-malformed.js
├── pieces-page-type.js
├── pieces-public-api.js
├── pieces-tasks.js
├── pieces.js
├── public
├── static-test.txt
├── test-image-landscape.jpg
└── test-image.jpg
├── published-pages.js
├── queryBuilders.js
├── recursionGuard.js
├── relationships.js
├── restApiRoutes.js
├── reverse-relationship.js
├── rich-text-widget.js
├── schema-queryBuilders.js
├── schemaBuilders.js
├── schemas.js
├── search.js
├── settings.js
├── soft-redirects.js
├── subdir-project.js
├── subdir-project
└── app.js
├── templates.js
├── test-bundle
├── index.js
└── modules
│ └── test-bundle-sub
│ └── index.js
├── test.html
├── translation.js
├── urls.js
├── users.js
├── utils.js
├── utils
├── commands.js
└── permissions.js
├── weird-api-errors.js
├── widgets-children
└── widgets-malformed-child.js
├── widgets-malformed.js
├── widgets.js
├── with-nested-module-subdirs.js
├── without-nested-module-subdirs.js
├── workspaces-project.js
└── workspaces-project
├── app.js
├── modules
└── @apostrophecms
│ └── log
│ └── index.js
├── package.json
└── workspace-a
└── package.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | end_of_line = lf
8 | indent_size = 2
9 | indent_style = space
10 | insert_final_newline = true
11 | trim_trailing_whitespace = true
12 |
13 | [*.md]
14 | trim_trailing_whitespace = false
15 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/vendor/**/*.js
2 | **/blueimp/**/*.js
3 | **/node_modules
4 | test/public
5 | test/apos-build
6 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | { "extends": "apostrophe" }
2 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | # Controls when the action will run.
4 | on:
5 | push:
6 | branches: ["main", "a3"]
7 | pull_request:
8 | branches: ["*"]
9 |
10 | # Allows you to run this workflow manually from the Actions tab
11 | workflow_dispatch:
12 |
13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
14 | jobs:
15 | # This workflow contains a single job called "build"
16 | build:
17 | # The type of runner that the job will run on
18 | runs-on: ubuntu-latest
19 | strategy:
20 | matrix:
21 | node-version: [18, 20, 22]
22 | mongodb-version: [6.0, 7.0, 8.0]
23 |
24 | # Steps represent a sequence of tasks that will be executed as part of the job
25 | steps:
26 | - name: Git checkout
27 | uses: actions/checkout@v4
28 |
29 | - name: Use Node.js ${{ matrix.node-version }}
30 | uses: actions/setup-node@v4
31 | with:
32 | node-version: ${{ matrix.node-version }}
33 |
34 | - name: Start MongoDB
35 | uses: supercharge/mongodb-github-action@1.11.0
36 | with:
37 | mongodb-version: ${{ matrix.mongodb-version }}
38 |
39 | - run: npm install
40 |
41 | - run: npm test
42 | env:
43 | CI: true
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore MacOS X metadata forks (fusefs)
2 | ._*
3 | # Ignore unison sync files
4 | .unison*
5 | package-lock.json
6 | npm-debug.log
7 | *.DS_Store
8 | *.npmignore
9 | .nyc_output
10 | .vscode
11 | .out
12 | coverage
13 | /node_modules
14 |
15 | # We do not commit CSS, only LESS, with the exception of a few vendor CSS files we don't have LESS for
16 | */public/css/*.css
17 | modules/*/public/css/*.css
18 | */public/css/*.css
19 | modules/*/public/css/*.css
20 | # Never commit a CSS map file, anywhere
21 | *.css.map
22 |
23 | test/public/modules/*
24 | test/public/css/master-*
25 | test/locales
26 | /test/node_modules
27 | /test/workspaces-project/node_modules
28 | /test/esm-project/node_modules
29 | test/package.json
30 | test/public
31 | test/apos-build
32 | test/workspaces-project/node_modules
33 | test/esm-project/node_modules
34 |
35 | # Dont commit test generated css
36 | test/public/css/*.css
37 | test/public/css/master-*.less
38 |
39 | # Dont commit test uploads
40 | test/public/uploads
41 |
42 | # vim swp files
43 | .*.sw*
44 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "curly": true,
3 | "eqeqeq": true,
4 | "immed": true,
5 | "latedef": true,
6 | "newcap": true,
7 | "noarg": true,
8 | "sub": true,
9 | "undef": true,
10 | "boss": true,
11 | "eqnull": true,
12 | "node": true,
13 | "es5": true
14 | }
15 |
--------------------------------------------------------------------------------
/.nycrc:
--------------------------------------------------------------------------------
1 | {
2 | "branches": 100,
3 | "lines": 100,
4 | "functions": 100,
5 | "statements": 100
6 | }
7 |
--------------------------------------------------------------------------------
/.stylelintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "stylelint-config-apostrophe"
3 | }
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "stable"
4 | - "lts/*"
5 | sudo: false
6 | services:
7 | - docker
8 | - mongodb
9 |
10 | # We need to download MongoDB 2.6.10
11 | env:
12 | global:
13 | - MONGODB_VERSION=2.6.10
14 | before_install:
15 | - wget http://fastdl.mongodb.org/linux/mongodb-linux-x86_64-$MONGODB_VERSION.tgz
16 | - tar xfz mongodb-linux-x86_64-$MONGODB_VERSION.tgz
17 | - export PATH=`pwd`/mongodb-linux-x86_64-$MONGODB_VERSION/bin:$PATH
18 | - mkdir -p data/db
19 | - mongod --dbpath=data/db > /dev/null 2>&1 &
20 | - sleep 3
21 |
22 | # whitelist
23 | branches:
24 | only:
25 | - master
26 |
--------------------------------------------------------------------------------
/ApostropheCMS_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apostrophecms/apostrophe/8d77d62a4d0f0cd47c76428f530dff15aac79053/ApostropheCMS_logo.png
--------------------------------------------------------------------------------
/DEVELOPMENT.md:
--------------------------------------------------------------------------------
1 | ## Coding style
2 |
3 | ### UI component naming
4 |
5 | We generally aim to follow [Vue best practices](https://vuejs.org/v2/style-guide/) regarding component naming. Specifically:
6 | - Apostrophe UI components should be named with pascal case and name-spaced with `Apos`, e.g., `AposComponent`, `AposModal`.
7 | - Single-instance components (where only one will ever exist) should be name-spaced with `TheApos`, e.g., `TheAposAdminBar`.
8 | - Tightly coupled components should share name-spacing to reflect their relationships, e.g, `AposModal`, `AposModalHeader`, `AposModalBody`
9 |
10 | ### UI component styles
11 |
12 | As a rule, all user interface components should have their styles scoped (using the `scoped` attribute). This helps us write simpler CSS selectors and avoide a certain amount of style "bleed" across components. Global styles, and styles for top level Vue apps (e.g., `TheAposNotifications`), should be in `.scss` files and imported into the import file: `/modules/@apostrophecms/ui/ui/apos/scss/imports.scss`.
13 |
--------------------------------------------------------------------------------
/deploy-test-count:
--------------------------------------------------------------------------------
1 | 6
2 |
--------------------------------------------------------------------------------
/force-deploy:
--------------------------------------------------------------------------------
1 | 4
2 |
--------------------------------------------------------------------------------
/lib/escape-host.js:
--------------------------------------------------------------------------------
1 | module.exports = host => {
2 | if (host.includes(':')) {
3 | // ipv6
4 | return `[${host}]`;
5 | } else {
6 | return host;
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/lib/glob.js:
--------------------------------------------------------------------------------
1 | const { globSync } = require('glob');
2 |
3 | // synchronous glob 10 but with the sorting semantics of glob 8,
4 | // to ease backwards compatibility in Apostrophe startup logic
5 |
6 | module.exports = (pattern, options) => {
7 | const result = globSync(pattern, options);
8 | if (!options.nosort) {
9 | result.sort((a, b) => a.localeCompare(b, 'en'));
10 | }
11 | return result;
12 | };
13 |
--------------------------------------------------------------------------------
/lib/locales.js:
--------------------------------------------------------------------------------
1 | const { stripIndent } = require('common-tags');
2 |
3 | module.exports = {
4 | // Make sure they are adequately distinguished by
5 | // hostname and prefix
6 | verifyLocales(locales, baseUrl) {
7 | const taken = {};
8 | let hostnamesCount = 0;
9 | for (const [ name, options ] of Object.entries(locales)) {
10 | const hostname = options.hostname || '__none';
11 | const prefix = options.prefix || '__none';
12 | const key = `${hostname}:${prefix}`;
13 |
14 | hostnamesCount += options.hostname ? 1 : 0;
15 |
16 | if (taken[key]) {
17 | throw new Error(stripIndent`
18 | The locale "${name}" cannot be distinguished from earlier locales.
19 | Make sure it is uniquely distinguished by its hostname option,
20 | prefix option or a combination of the two.
21 | One locale per site may be a default with neither hostname nor prefix,
22 | and one locale per hostname may be a default for that hostname without a prefix.
23 | `);
24 | }
25 | taken[key] = true;
26 | }
27 |
28 | if (
29 | hostnamesCount > 0 &&
30 | hostnamesCount < Object.keys(locales).length &&
31 | !baseUrl
32 | ) {
33 | throw new Error(stripIndent`
34 | If some of your locales have hostnames, then they all must have
35 | hostnames, and your top-level baseUrl option must be set.
36 |
37 | In development, you can set baseUrl to http://localhost:3000
38 | for testing purposes. In production it should always be set
39 | to a real base URL for the site.
40 | `);
41 | }
42 | }
43 | };
44 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/admin-bar/ui/apos/apps/AposAdminBar.js:
--------------------------------------------------------------------------------
1 | import createApp from 'Modules/@apostrophecms/ui/lib/vue';
2 |
3 | export default function() {
4 | const component = apos.vueComponents.TheAposAdminBar;
5 | // Careful, login page is in user scene but has no admin bar
6 | const el = document.querySelector('#apos-admin-bar');
7 | if (!apos.adminBar || !el || apos?.adminBar.showAdminBar === false) {
8 | return;
9 | }
10 | const app = createApp(component, { items: apos.adminBar.items || [] });
11 | app.mount(el);
12 | };
13 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarUser.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
52 |
53 |
76 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/admin-bar/ui/src/index.js:
--------------------------------------------------------------------------------
1 | // If the page delivers a logged-out content but we know from session storage
2 | // that a user is logged-in, we force-refresh the page to bypass the cache,
3 | // in order to get the logged-in content (with admin UI).
4 | export default function() {
5 | const isLoggedOutPageContent = !(apos.login && apos.login.user);
6 | const isLoggedInCookie = apos.util.getCookie(`${self.apos.shortName}.loggedIn`) === 'true';
7 |
8 | if (!isLoggedOutPageContent || !isLoggedInCookie) {
9 | sessionStorage.setItem('aposRefreshedPages', '{}');
10 |
11 | return;
12 | }
13 |
14 | const refreshedPages = JSON.parse(sessionStorage.aposRefreshedPages || '{}');
15 |
16 | // Avoid potential refresh loops
17 | if (!refreshedPages[location.href]) {
18 | refreshedPages[location.href] = true;
19 | sessionStorage.setItem('aposRefreshedPages', JSON.stringify(refreshedPages));
20 |
21 | // eslint-disable-next-line no-console
22 | console.info('Received logged-out content from cache while logged-in, refreshing the page');
23 |
24 | location.reload();
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/admin-bar/views/adminBar.html:
--------------------------------------------------------------------------------
1 | {% if data.active %}
2 |
3 | {% endif %}
4 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/any-doc-type/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extend: '@apostrophecms/doc-type',
3 | extendMethods(self) {
4 | return {
5 | find(_super, req, criteria, options) {
6 | return _super(req, criteria, options).type(false);
7 | }
8 | };
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/archive-page/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // This is the page type manager for the main archive, the ancestor of
3 | // all archived pages
4 | extend: '@apostrophecms/page-type'
5 | };
6 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/area/views/area.html:
--------------------------------------------------------------------------------
1 | {# area needs its own copy of the widget options as #}
2 | {# JSON, for adding new widgets #}
3 |
4 |
5 | {%- for item in data.area.items -%}
6 | {%- set widgetOptions = data.options.widgets[item.type] or {} -%}
7 | {%- if data.canEdit -%}
8 |
9 | {%- endif -%}
10 | {% widget item, widgetOptions with data._with %}
11 | {%- if data.canEdit -%}
12 |
13 | {%- endif -%}
14 | {%- endfor -%}
15 |
16 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/asset/lib/build/manager-bundled.js:
--------------------------------------------------------------------------------
1 | const path = require('node:path');
2 | module.exports = (self, entrypoint) => {
3 | const predicates = entrypoint.outputs.reduce((acc, type) => {
4 | acc[type] = (file, entry) => {
5 | return file.startsWith(`${entrypoint.name}/`) && file.endsWith(`.${type}`);
6 | };
7 | return acc;
8 | }, {});
9 |
10 | return {
11 | getSourceFiles(meta) {
12 | return self.apos.asset.findSourceFiles(
13 | meta,
14 | predicates
15 | );
16 | },
17 | async getOutput(sourceFiles) {
18 | throw new Error(`"getOutput" is not supported for entrypoint type: ${entrypoint.type}`);
19 | },
20 | match(relSourcePath, metaEntry) {
21 | const result = self.apos.asset.findSourceFiles(
22 | [ metaEntry ],
23 | predicates
24 | );
25 | const match = Object.values(result).flat()
26 | .some((file) => file.path === path.join(metaEntry.dirname, relSourcePath));
27 |
28 | return match;
29 | }
30 | };
31 | };
32 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/asset/lib/build/managers.js:
--------------------------------------------------------------------------------
1 | const apos = require('./manager-apos.js');
2 | const custom = require('./manager-custom.js');
3 | const index = require('./manager-index.js');
4 | const bundled = require('./manager-bundled.js');
5 |
6 | module.exports = (self) => {
7 | const managers = {
8 | apos,
9 | custom,
10 | index,
11 | bundled
12 | };
13 |
14 | return function getManager(entrypoint) {
15 | if (!managers[entrypoint.type]) {
16 | throw new Error(`Unknown build manager type: ${entrypoint.type}`);
17 | }
18 | return managers[entrypoint.type](self, entrypoint);
19 | };
20 | };
21 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/asset/lib/refresh-on-restart.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | // Note: this script will not appear in the page in production.
3 | //
4 | // eslint-disable-next-line no-var
5 | var reloadId;
6 | // eslint-disable-next-line no-var
7 | var fast = '1';
8 | check();
9 | function check() {
10 | if (!window.apos) {
11 | // Wait for the js bundle to evaluate
12 | return setTimeout(check, 100);
13 | }
14 | if (document.visibilityState === 'hidden') {
15 | // No requests when tab is not active
16 | setTimeout(check, 5000);
17 | } else {
18 | window.apos.http.post(document.querySelector('[data-apos-refresh-on-restart]').getAttribute('data-apos-refresh-on-restart'), {
19 | qs: {
20 | fast
21 | }
22 | }, function(err, result) {
23 | if (err) {
24 | fast = '1';
25 | setTimeout(check, 1000);
26 | return;
27 | }
28 | fast = '';
29 | if (!reloadId) {
30 | reloadId = result;
31 | } else if (result !== reloadId) {
32 | console.log('Apostrophe restarted, refreshing the page');
33 | window.location.reload();
34 | return;
35 | }
36 | setTimeout(check, 1000);
37 | });
38 | }
39 | }
40 | })();
41 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/asset/lib/webpack/apos/webpack.js.js:
--------------------------------------------------------------------------------
1 | module.exports = (options, apos) => {
2 | return {
3 | module: {
4 | rules: [
5 | {
6 | test: /\.(m)?js$/i,
7 | resolve: {
8 | byDependency: {
9 | esm: {
10 | // Be tolerant of imports like lodash/debounce that
11 | // should really be lodash/debounce.js, as dependencies
12 | // like tiptap are not fully on board with that rule yet
13 | fullySpecified: false
14 | }
15 | }
16 | }
17 | }
18 | ]
19 | }
20 | };
21 | };
22 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/asset/lib/webpack/apos/webpack.scss.js:
--------------------------------------------------------------------------------
1 | module.exports = (options, apos) => {
2 | const postcssPlugins = [
3 | 'autoprefixer',
4 | {}
5 | ];
6 |
7 | return {
8 | module: {
9 | rules: [
10 | {
11 | test: /\.css$/,
12 | use: [
13 | 'vue-style-loader',
14 | 'css-loader',
15 | {
16 | loader: 'postcss-loader',
17 | options: {
18 | sourceMap: true,
19 | postcssOptions: {
20 | plugins: [ postcssPlugins ]
21 | }
22 | }
23 | }
24 | ]
25 | },
26 | {
27 | test: /\.s[ac]ss$/,
28 | use: [
29 | 'vue-style-loader',
30 | 'css-loader',
31 | {
32 | loader: 'postcss-loader',
33 | options: {
34 | sourceMap: true,
35 | postcssOptions: {
36 | plugins: [ postcssPlugins ]
37 | }
38 | }
39 | },
40 | {
41 | loader: 'sass-loader',
42 | options: {
43 | sassOptions: {
44 | silenceDeprecations: [ 'import' ]
45 | },
46 | sourceMap: false,
47 | // "use" rules must come first or sass throws an error
48 | additionalData: `
49 | @use 'sass:math';
50 | @use "sass:color";
51 | @use "sass:map";
52 |
53 | @import "Modules/@apostrophecms/ui/scss/mixins/import-all.scss";
54 | `
55 | }
56 | }
57 | ]
58 | }
59 | ]
60 | }
61 | };
62 | };
63 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/asset/lib/webpack/apos/webpack.vue.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const { VueLoaderPlugin } = require('vue-loader');
3 |
4 | module.exports = (options, apos) => {
5 | return {
6 | module: {
7 | rules: [
8 | {
9 | test: /\.vue$/,
10 | loader: 'vue-loader',
11 | options: {
12 | sourceMap: true
13 | }
14 | }
15 | ]
16 | },
17 | plugins: [
18 | // make sure to include the plugin for the magic
19 | new VueLoaderPlugin(),
20 | new webpack.DefinePlugin({
21 | __VUE_OPTIONS_API__: 'true',
22 | __VUE_PROD_DEVTOOLS__: 'false',
23 | __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false'
24 | })
25 | ]
26 | };
27 | };
28 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/asset/lib/webpack/src-es5/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = (options, apos) => {
2 | return require('../src/webpack.config.js')({
3 | ...options,
4 | es5: true
5 | }, apos);
6 | };
7 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/asset/views/scripts.html:
--------------------------------------------------------------------------------
1 | {{ data.placeholder }}
2 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/asset/views/stylesheets.html:
--------------------------------------------------------------------------------
1 | {{ data.placeholder }}
2 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/attachment/lib/legacy-migrations.js:
--------------------------------------------------------------------------------
1 | // Migrations relevant only to those who used early alpha and beta versions of
2 | // 3.x are kept here for tidiness
3 |
4 | module.exports = (self) => {
5 | return {
6 | addLegacyMigrations() {
7 | self.addFixLengthPropertyMigration();
8 | self.addDocReferencesContainedMigration();
9 | },
10 | addFixLengthPropertyMigration() {
11 | self.apos.migration.add('fix-length-property', async () => {
12 | return self.each({
13 | 'length.size': {
14 | $exists: 1
15 | }
16 | }, 5, attachment => {
17 | if (attachment.length && attachment.length.size) {
18 | return self.db.updateOne({
19 | _id: attachment._id
20 | }, {
21 | $set: {
22 | length: attachment.length.size
23 | }
24 | });
25 | }
26 | });
27 | });
28 | },
29 | // This migration is needed because formerly,
30 | // docs that only referenced this attachment via
31 | // a join were counted as "owning" it, which is
32 | // incorrect and leads to failure to make it
33 | // unavailable at the proper time. The name was
34 | // changed to ensure this migration would run
35 | // again after that bug was discovered and fixed.
36 | addDocReferencesContainedMigration() {
37 | self.apos.migration.add(self.__meta.name + '.docReferencesContained', self.recomputeAllDocReferences);
38 | }
39 | };
40 | };
41 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/attachment/public/img/missing-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
10 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/busy/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | options: {
3 | components: {},
4 | alias: 'busy'
5 | },
6 | init(self) {
7 | self.busy = false;
8 | self.enableBrowserData();
9 | },
10 | methods(self) {
11 | return {
12 | getBrowserData(req) {
13 | return {
14 | busy: self.busy,
15 | components: { the: self.options.components.the || 'TheAposBusy' }
16 | };
17 | }
18 | };
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/busy/ui/apos/apps/AposBusy.js:
--------------------------------------------------------------------------------
1 | import createApp from 'Modules/@apostrophecms/ui/lib/vue';
2 |
3 | export default function() {
4 | const component = apos.vueComponents.TheAposBusy;
5 | const el = document.querySelector('#apos-busy');
6 | if (!el) {
7 | return;
8 | }
9 | const app = createApp(component);
10 | app.mount(el);
11 | };
12 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/color-field/ui/apos/lib/AposColorInfo.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 | {{ label }}
8 |
9 |
17 |
23 |
24 |
25 |
26 |
43 |
44 |
66 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/command-menu/ui/apos/apps/AposCommandMenu.js:
--------------------------------------------------------------------------------
1 | import createApp from 'Modules/@apostrophecms/ui/lib/vue';
2 |
3 | export default function() {
4 | const component = apos.vueComponents.TheAposCommandMenu;
5 | // Careful, login page is in user scene but has no command menu
6 | const el = document.querySelector('#apos-command-menu');
7 | if (!apos.commandMenu || !el) {
8 | return;
9 | }
10 | const app = createApp(component, {
11 | modals: apos.commandMenu.modals
12 | });
13 | const theAposCommandMenu = app.mount(el);
14 |
15 | apos.commandMenu.getModal = theAposCommandMenu.getModal;
16 | }
17 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/doc-type/lib/extendQueries.js:
--------------------------------------------------------------------------------
1 | module.exports = extendQueries;
2 |
3 | function extendQueries(queries, extensions) {
4 | for (const [ name, fn ] of Object.entries(extensions)) {
5 | if (typeof fn === 'object' && !Array.isArray(fn) && fn !== null) {
6 | // Nested structure is allowed
7 | queries[name] = queries[name] || {};
8 | return extendQueries(queries[name], fn);
9 | }
10 |
11 | if (typeof fn !== 'function' || typeof queries[name] !== 'function') {
12 | queries[name] = fn;
13 | continue;
14 | }
15 |
16 | const superMethod = queries[name];
17 | queries[name] = function(...args) {
18 | return fn(superMethod, ...args);
19 | };
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
21 |
30 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/error/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | init(self) {
3 | // Actual error method is aliased for brevity, encouraging use of the
4 | // mechanism
5 | self.apos.error = self.error;
6 | },
7 | methods(self) {
8 | return {
9 | // Construct an Error object suitable to throw. The `name` property will
10 | // be the given `name`.
11 | //
12 | // `message` may be skipped completely, or given for a longer
13 | // description. `data` is optional and may contain data about
14 | // the error, safe to share with an untrusted client.
15 | //
16 | // Certain values of `name` match to certain HTTP status codes; see
17 | // the `http` module. If the error is caught by Apostrophe's `apiRoute`
18 | // or `restApiRoute` mechanism, and `name` matches to a status code, an
19 | // appropriate HTTP error is sent, and `data` is sent as a JSON object,
20 | // with `message` as an additional property if present. If it doesn't
21 | // match, a plain 500 error is sent to avoid disclosing inappropriate
22 | // information and the error is only logged by Apostrophe server-side.
23 | //
24 | // For brevity, this method is aliased as `apos.error`.
25 | error(name, message = null, data = {}) {
26 | if ((typeof message) === 'object') {
27 | data = message;
28 | message = null;
29 | }
30 | const error = new Error(message || name);
31 | error.name = name;
32 | error.data = data;
33 |
34 | // Establish a difference between errors built here and those elsewhere.
35 | error.aposError = true;
36 | return error;
37 | }
38 | };
39 | }
40 | };
41 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/file-tag/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extend: '@apostrophecms/piece-type',
3 | options: {
4 | label: 'apostrophe:fileTag',
5 | pluralLabel: 'apostrophe:fileTags',
6 | quickCreate: false,
7 | autopublish: true,
8 | versions: true,
9 | editRole: 'editor',
10 | publishRole: 'editor',
11 | shortcut: 'G,Shift+F',
12 | relationshipSuggestionIcon: 'tag-icon'
13 | },
14 | fields: {
15 | remove: [ 'visibility' ]
16 | }
17 | };
18 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/home-page/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extend: '@apostrophecms/page-type'
3 | };
4 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/html-widget/index.js:
--------------------------------------------------------------------------------
1 | // Provides the "raw HTML widget" (the `@apostrophecms/html` widget).
2 | // Use of this widget is not recommended if it can be avoided. The
3 | // improper use of HTML can easily break pages. If a page becomes
4 | // unusable, add `?safe_mode=1` to the URL to make it work temporarily
5 | // without the offending code being rendered.
6 |
7 | module.exports = {
8 | extend: '@apostrophecms/widget-type',
9 | options: {
10 | label: 'apostrophe:rawHtml',
11 | className: false,
12 | icon: 'code-tags-icon',
13 | preview: false
14 | },
15 | fields: {
16 | add: {
17 | code: {
18 | type: 'string',
19 | label: 'apostrophe:rawHtmlCode',
20 | textarea: true,
21 | help: 'apostrophe:rawHtmlCodeHelp'
22 | }
23 | }
24 | },
25 | components(self) {
26 | return {
27 | render(req, data) {
28 | // Be understanding of the panic that is probably going on in a user's
29 | // mind as they try to remember how to use safe mode. -Tom
30 | const safeModeVariations = [
31 | 'safemode',
32 | 'safeMode',
33 | 'safe_mode',
34 | 'safe-mode',
35 | 'safe mode'
36 | ];
37 | if (req.xhr) {
38 | return {
39 | render: false
40 | };
41 | }
42 | if (req.query) {
43 | let safe = false;
44 | for (const variation of safeModeVariations) {
45 | if (Object.keys(req.query).includes(variation)) {
46 | safe = true;
47 | break;
48 | }
49 | }
50 | if (safe) {
51 | return {
52 | render: 'safeMode'
53 | };
54 | }
55 | }
56 | return {
57 | render: true,
58 | code: data.code
59 | };
60 | }
61 | };
62 | }
63 | };
64 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/html-widget/views/render.html:
--------------------------------------------------------------------------------
1 | {%- if data.render == 'safeMode' -%}
2 | {{ __t('apostrophe:safeModeActive') }}
3 | {%- elif data.render -%}
4 | {{ data.code | safe }}
5 | {% else %}
6 | {{ __t('apostrophe:refreshForRawHtml') }}
7 | {%- endif -%}
8 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/html-widget/views/widget.html:
--------------------------------------------------------------------------------
1 | {% if data.options.className %}
2 | {% set className = data.options.className %}
3 | {% elif data.manager.options.className %}
4 | {% set className = data.manager.options.className %}
5 | {% endif %}
6 |
7 |
8 | {% component '@apostrophecms/html-widget:render' with data.widget %}
9 |
10 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/http/ui/apos/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "NOTE: needed for testing in test/utils.js, for js files to be treated as ESM",
3 | "type": "module"
4 | }
5 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/i18n/ui/apos/apps/AposI18nCrossDomainSession.js:
--------------------------------------------------------------------------------
1 | export default () => {
2 | if (apos.i18n.crossDomainClipboard) {
3 | localStorage.setItem('aposWidgetClipboard', apos.i18n.crossDomainClipboard);
4 | }
5 | };
6 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/i18n/ui/src/index.js:
--------------------------------------------------------------------------------
1 | export default () => {
2 | apos.getActiveLocale = () => apos.modal?.getActiveLocale?.() || apos.locale;
3 | };
4 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/image-tag/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extend: '@apostrophecms/piece-type',
3 | options: {
4 | label: 'apostrophe:imageTag',
5 | pluralLabel: 'apostrophe:imageTags',
6 | quickCreate: false,
7 | autopublish: true,
8 | versions: true,
9 | editRole: 'editor',
10 | publishRole: 'editor',
11 | shortcut: 'G,Shift+I',
12 | relationshipSuggestionIcon: 'tag-icon'
13 | },
14 | fields: {
15 | remove: [ 'visibility' ]
16 | }
17 | };
18 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/image-widget/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extend: '@apostrophecms/widget-type',
3 | options: {
4 | label: 'apostrophe:image',
5 | className: false,
6 | icon: 'image-icon',
7 | dimensionAttrs: false,
8 | placeholder: true,
9 | placeholderClass: false,
10 | placeholderImage: 'jpg',
11 | // 0 means disabled width setting
12 | defaultImageWidth: 100,
13 | imageResizeStep: 5
14 | },
15 | widgetOperations(self, options) {
16 | const {
17 | relationshipEditor = 'AposImageRelationshipEditor',
18 | relationshipEditorLabel = 'apostrophe:editImageAdjustments',
19 | relationshipEditorIcon = 'image-edit-outline'
20 | } = options.apos.image.options || {};
21 | return {
22 | add: {
23 | adjustImage: {
24 | label: relationshipEditorLabel,
25 | icon: relationshipEditorIcon,
26 | modal: relationshipEditor,
27 | tooltip: relationshipEditorLabel,
28 | if: {
29 | '_image.0': {
30 | $exists: true
31 | }
32 | }
33 | }
34 | }
35 | };
36 | },
37 | fields(self) {
38 | return {
39 | add: {
40 | _image: {
41 | type: 'relationship',
42 | label: 'apostrophe:image',
43 | max: 1,
44 | required: true,
45 | withType: '@apostrophecms/image'
46 | }
47 | }
48 | };
49 | },
50 | init(self) {
51 | self.determineBestAssetUrl('placeholder');
52 | }
53 | };
54 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/image-widget/public/placeholder.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apostrophecms/apostrophe/8d77d62a4d0f0cd47c76428f530dff15aac79053/modules/@apostrophecms/image-widget/public/placeholder.jpg
--------------------------------------------------------------------------------
/modules/@apostrophecms/image-widget/ui/src/index.scss:
--------------------------------------------------------------------------------
1 | .image-widget-placeholder {
2 | width: 100%;
3 | }
4 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/image-widget/views/widget.html:
--------------------------------------------------------------------------------
1 | {% set attachment = apos.image.first(data.widget._image) %}
2 |
3 | {% if data.widget.aposPlaceholder and data.manager.options.placeholderUrl %}
4 |
9 | {% else %}
10 | {% set className = data.options.className or data.manager.options.className %}
11 | {% set dimensionAttrs = data.options.dimensionAttrs or data.manager.options.dimensionAttrs %}
12 | {% set loadingType = data.options.loadingType or data.manager.options.loadingType %}
13 | {% set size = data.options.size or data.manager.options.size or 'full' %}
14 |
15 |
16 | {% if attachment %}
17 |
34 | {% endif %}
35 | {% endif %}
36 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/image/ui/apos/apps/AposImageRelationshipQueryFilter.js:
--------------------------------------------------------------------------------
1 | export default () => {
2 | const queryOptions = [ 'minSize' ];
3 |
4 | apos.bus.$on('piece-relationship-query', (query) => {
5 | const [ options = {} ] = apos.area.widgetOptions || [];
6 |
7 | queryOptions.forEach((optName) => {
8 | if (options[optName]) {
9 | query[optName] = options[optName];
10 | }
11 | });
12 | });
13 | };
14 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/image/ui/apos/lib/aspectRatios.js:
--------------------------------------------------------------------------------
1 | const DEFAULT_ASPECT_RATIOS = [
2 | [ 1, 1 ],
3 | [ 2, 3 ],
4 | [ 3, 4 ],
5 | [ 3, 2 ],
6 | [ 4, 3 ],
7 | [ 5, 4 ],
8 | [ 16, 9 ],
9 | [ 9, 16 ],
10 | [ 4, 5 ]
11 | ];
12 |
13 | export default (freeLabel) => {
14 | const freeAspectRatio = {
15 | label: freeLabel,
16 | value: 0
17 | };
18 |
19 | return [
20 | freeAspectRatio,
21 | ...DEFAULT_ASPECT_RATIOS.map(([ width, height ]) => ({
22 | label: `${width}:${height}`,
23 | value: width / height
24 | }))
25 | ];
26 | };
27 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/launder/index.js:
--------------------------------------------------------------------------------
1 | // This module attaches an instance of the [launder](https://npmjs.org/package/launder)
2 | // npm module as `apos.launder`. The `apos.launder` object is then used
3 | // throughout Apostrophe to sanitize user input.
4 |
5 | module.exports = {
6 | init(self) {
7 | self.apos.launder = require('launder')({
8 | // A3 _ids may contain :-separated components
9 | idRegExp: /^[A-Za-z0-9_-]+(:[A-Za-z0-9_-]+)*$/,
10 | ...self.options
11 | });
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/login/ui/apos/apps/AposLogin.js:
--------------------------------------------------------------------------------
1 | import createApp from 'Modules/@apostrophecms/ui/lib/vue';
2 |
3 | export default function() {
4 | const component = apos.vueComponents.TheAposLogin;
5 | const el = document.querySelector('#apos-login');
6 | if (el) {
7 | const app = createApp(component);
8 |
9 | app.mount(el);
10 | }
11 |
12 | apos.bus.$on('admin-menu-click', async (item) => {
13 | if (item !== '@apostrophecms/login-logout') {
14 | return;
15 | }
16 | await apos.http.post(`${apos.modules['@apostrophecms/login'].action}/logout`, {});
17 | window.sessionStorage.setItem('aposStateChange', Date.now());
18 | window.sessionStorage.setItem('aposStateChangeSeen', '{}');
19 | try {
20 | await apos.http.get(location.href, {});
21 | } catch (e) {
22 | if (e.status === 404) {
23 | location.assign('/');
24 | return;
25 | }
26 | }
27 | location.reload();
28 | });
29 | };
30 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/login/ui/apos/mixins/AposLoginFormMixin.js:
--------------------------------------------------------------------------------
1 | // Mixin for login form related common behavior.
2 |
3 | export default {
4 | props: {
5 | contextError: {
6 | type: String,
7 | default: ''
8 | },
9 | context: {
10 | type: Object,
11 | default: function() {
12 | return {};
13 | }
14 | }
15 | },
16 | data() {
17 | return {
18 | error: '',
19 | doc: {
20 | data: {},
21 | hasErrors: false
22 | }
23 | };
24 | },
25 | computed: {
26 | passwordResetEnabled() {
27 | return apos.login.passwordResetEnabled;
28 | }
29 | },
30 | watch: {
31 | contextError(newVal) {
32 | // Copy it only once
33 | if (!this.contextErrorReceived && newVal && !this.error) {
34 | this.error = newVal;
35 | this.contextErrorReceived = true;
36 | }
37 | }
38 | },
39 | mounted() {
40 | this.error = this.contextError;
41 | if (this.contextError) {
42 | this.contextErrorReceived = true;
43 | }
44 | }
45 | };
46 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/login/views/login.html:
--------------------------------------------------------------------------------
1 | {% extends data.outerLayout %}
2 | {% block title %}{{ __t("apostrophe:loginPageTitle") }}{% endblock %}
3 |
4 | {% block bodyClass %}apos-login-page{% endblock %}
5 |
6 | {% block beforeMain %}
7 | {# Hush up project level styles while login loads #}
8 |
13 | {% endblock%}
14 | {% block main %}
15 |
16 | {% endblock %}
17 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/login/views/passwordResetEmail.html:
--------------------------------------------------------------------------------
1 | Resetting your password on {{ data.site }}
2 | Hello {{ data.user.title }},
3 | You are receiving this email because a request to reset your password was made on {{ data.site }}.
4 | If that is your wish, please follow this link to complete the password reset process:
5 | {{ data.url }}
6 |
7 | If you did not request to reset your password, please delete and ignore this email. Someone else may have entered
8 | your email address in error.
9 |
10 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/modal/index.js:
--------------------------------------------------------------------------------
1 | // This module provides a base class for modal dialog boxes and supplies
2 | // related markup and LESS files.
3 |
4 | module.exports = {
5 | options: {
6 | components: {},
7 | alias: 'modal'
8 | },
9 | init(self) {
10 | self.modals = [];
11 | self.enableBrowserData();
12 | },
13 | methods(self) {
14 | return {
15 | // Add a modal that appears when an `admin-menu-click` event corresponding
16 | // to its `itemName` appears on the bus. `props` is merged with the props,
17 | // and the component will be of the type specified by `componentName`.
18 | add(itemName, componentName, props) {
19 | self.modals.push({
20 | itemName,
21 | componentName,
22 | props
23 | });
24 | },
25 | getBrowserData(req) {
26 | return {
27 | modals: self.modals,
28 | components: {
29 | the: self.options.components.the || 'TheAposModals',
30 | confirm: self.options.components.confirm || 'AposModalConfirm',
31 | report: self.options.components.report || 'AposModalReport'
32 | }
33 | };
34 | }
35 | };
36 | }
37 | };
38 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/modal/ui/apos/apps/AposModals.js:
--------------------------------------------------------------------------------
1 | import createApp from 'Modules/@apostrophecms/ui/lib/vue';
2 | import { useModalStore } from 'Modules/@apostrophecms/ui/stores/modal';
3 |
4 | export default function() {
5 | const component = apos.vueComponents.TheAposModals;
6 | const el = document.querySelector('#apos-modals');
7 | if (!el) {
8 | return;
9 | }
10 | const app = createApp(component);
11 | app.mount(el);
12 |
13 | const modalStore = useModalStore();
14 |
15 | apos.modal.execute = modalStore.execute;
16 | apos.modal.get = modalStore.get;
17 | apos.modal.getAt = modalStore.getAt;
18 | apos.modal.getProperties = modalStore.getProperties;
19 | apos.modal.onTopOf = modalStore.onTopOf;
20 | apos.modal.getActiveLocale = modalStore.getActiveLocale;
21 | apos.confirm = modalStore.confirm;
22 | apos.alert = modalStore.alert;
23 | apos.report = modalStore.report;
24 | }
25 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/modal/ui/apos/components/AposModalRail.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
23 |
24 |
31 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/modal/ui/apos/components/AposModalTabsBody.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
16 |
17 |
33 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/modal/ui/apos/components/AposModalToolbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
37 |
38 |
58 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/multisite-i18n/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | i18n: {
3 | aposMultisite: {
4 | browser: true
5 | }
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/notification/ui/apos/apps/AposNotification.js:
--------------------------------------------------------------------------------
1 | import createApp from 'Modules/@apostrophecms/ui/lib/vue';
2 | import { useNotificationStore } from 'Modules/@apostrophecms/ui/stores/notification';
3 |
4 | export default function() {
5 | const component = apos.vueComponents.TheAposNotifications;
6 | const el = document.querySelector('#apos-notification');
7 | if (!apos.login.user || !el) {
8 | // The user scene is being used but no one is logged in
9 | // (example: the login page)
10 | return;
11 | }
12 | const app = createApp(component);
13 | app.mount(el);
14 |
15 | const notifStore = useNotificationStore();
16 | apos.notify = notifStore.notify;
17 | notifStore.poll();
18 | };
19 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/notification/ui/apos/components/TheAposNotifications.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
14 |
15 |
16 |
17 |
34 |
35 |
59 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/oembed/lib/infogram.js:
--------------------------------------------------------------------------------
1 | const cheerio = require('cheerio');
2 |
3 | module.exports = function(self, oembetter) {
4 | // Fake oembed for infogr.am
5 | oembetter.addBefore(async function(url, options, response, cb) {
6 | const parsed = new URL(url);
7 | let title;
8 | if (!oembetter.inDomain('infogr.am', parsed.hostname)) {
9 | return cb(null);
10 | }
11 | const matches = url.match(/infogr\.am\/([^?]+)/);
12 | if (!matches) {
13 | return cb(null);
14 | }
15 | const slug = matches[1];
16 | const anchorId = 'apos_infogram_anchor_0_' + slug;
17 | const body = await self.apos.http.get(url);
18 | const $ = cheerio.load(body);
19 | const $title = $('title');
20 | title = $title.text();
21 | if (title) {
22 | title = title.trim();
23 | }
24 | return cb(null, url, options, {
25 | thumbnail_url: 'https://infogr.am/infogram.png',
26 | title: title || 'Infogram',
27 | type: 'rich',
28 | html: '
' + self.afterScriptLoads('//e.infogr.am/js/embed.js', anchorId, 'infogram_0_' + slug, ';')
29 | });
30 | });
31 | };
32 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/oembed/lib/vimeo.js:
--------------------------------------------------------------------------------
1 | module.exports = function(self, oembetter) {
2 |
3 | // Make vimeo thumbnails bigger
4 |
5 | oembetter.addAfter(function(url, options, response, cb) {
6 | if (!url.match(/vimeo/)) {
7 | return setImmediate(cb);
8 | }
9 | // Fix vimeo thumbnails to be larger
10 | response.thumbnail_url = response.thumbnail_url.replace('640.jpg', '1000.jpg');
11 | return cb(null);
12 | });
13 | };
14 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/pager/index.js:
--------------------------------------------------------------------------------
1 | // Provides markup, a Nunjucks helper and styles for a simple pager,
2 | // used for pagination both on the front end and the back end.
3 |
4 | module.exports = {
5 | options: { alias: 'pager' },
6 | helpers(self) {
7 | return {
8 | // Generate the right range of page numbers to display in the pager.
9 | // Just a little too much math to be comfortable in pure Nunjucks
10 | pageRange: function (options) {
11 | const pages = [];
12 | let fromPage = options.page - (Math.floor(options.shown / 2));
13 |
14 | if (fromPage > options.total - options.shown) {
15 | fromPage = options.total - options.shown;
16 | }
17 |
18 | if (fromPage < 2) {
19 | fromPage = 2;
20 | }
21 |
22 | for (let pg = fromPage; pg < fromPage + options.shown; pg++) {
23 | if (pg < options.total) {
24 | pages.push(pg);
25 | }
26 | }
27 | return pages;
28 | },
29 | showHeadGap: function (options) {
30 | if (options.shown % 2 === 0) {
31 | return ((options.page - (Math.floor(options.shown / 2))) > 2) &&
32 | options.page > (options.shown - Math.floor(options.shown / 2));
33 | } else {
34 | return ((options.page - (Math.floor(options.shown / 2))) > 2) &&
35 | (options.page >= (options.shown - Math.floor(options.shown / 2)));
36 | }
37 | },
38 | showTailGap(options) {
39 | if (options.shown % 2 === 0) {
40 | return (options.page < (options.total - (options.shown - 2))) &&
41 | (options.total > (options.shown + 2));
42 | } else {
43 | return (options.page <= (options.total - (options.shown - 2))) &&
44 | (options.total > (options.shown + 2));
45 | }
46 | }
47 | };
48 | }
49 | };
50 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/pager/views/macros.html:
--------------------------------------------------------------------------------
1 | {# Render an easily styled classic pager #}
2 | {% macro render(options, url) %}
3 | {% if ((options.page > 1) or (options.total > 1)) %}
4 | {% set pagerClass = options.class %}
5 | {% set gapClass = pagerClass + '__gap' if pagerClass else '' %}
6 |
25 | {% endif %}
26 | {% endmacro %}
27 |
28 | {% macro pagerPageInner(page, options, pagerClass, url) %}
29 | {% if ((page > 1) and (page < options.total)) %}
30 | {{ pagerPage(page, options, pagerClass, url) }}
31 | {% endif %}
32 | {% endmacro %}
33 |
34 | {% macro pagerPage(page, options, pagerClass, url) %}
35 | {% set pageClass = pagerClass + '__item' if pagerClass else '' %}
36 |
37 | {% if (page <= options.total) %}
38 |
39 | {%- if (options.page != page) -%}
40 |
41 | {%- endif -%}
42 | {{ page }}
43 | {%- if (options.page != page) -%}
44 |
45 | {%- endif -%}
46 |
47 | {% endif %}
48 | {% endmacro %}
49 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/permission/lib/legacy-migrations.js:
--------------------------------------------------------------------------------
1 | // Migrations relevant only to those who used early alpha and beta versions of
2 | // 3.x are kept here for tidiness
3 |
4 | module.exports = (self) => {
5 | return {
6 | addLegacyMigrations() {
7 | self.addRetirePublishedFieldMigration();
8 | },
9 | addRetirePublishedFieldMigration() {
10 | self.apos.migration.add('retire-published-field', async () => {
11 | await self.apos.migration.eachDoc({}, 5, async (doc) => {
12 | if (doc.published === true) {
13 | doc.visibility = 'public';
14 | } else if (doc.published === false) {
15 | doc.visibility = 'loginRequired';
16 | }
17 | delete doc.published;
18 | return self.apos.doc.db.replaceOne({
19 | _id: doc._id
20 | }, doc);
21 | });
22 | });
23 | }
24 | };
25 | };
26 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/piece-page-type/views/index.html:
--------------------------------------------------------------------------------
1 | {% extends data.outerLayout %}
2 | {% block title %}{{ data.page.title }}{% endblock %}
3 |
4 | {% block main %}
5 | {% for piece in data.pieces %}
6 |
7 | {% endfor %}
8 | {% import '@apostrophecms/pager:macros.html' as pager with context %}
9 | {{ pager.render({
10 | page: data.currentPage,
11 | total: data.totalPages
12 | }, data.url) }}
13 | {% endblock %}
14 |
15 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/piece-page-type/views/show.html:
--------------------------------------------------------------------------------
1 | {% extends data.outerLayout %}
2 | {% block title %}{{ data.piece.title }}{% endblock %}
3 |
4 | {% block main %}
5 | {{ data.piece.title }}
6 | {% endblock %}
7 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/polymorphic-type/index.js:
--------------------------------------------------------------------------------
1 | const migrations = require('./lib/migrations.js');
2 |
3 | module.exports = {
4 | extend: '@apostrophecms/doc-type',
5 | options: {
6 | name: '@apostrophecms/polymorphic-type',
7 | showPermissions: false
8 | },
9 | init(self) {
10 | self.addMigrations();
11 | },
12 | methods(self) {
13 | return {
14 | ...migrations(self)
15 | };
16 | }
17 | };
18 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/polymorphic-type/lib/migrations.js:
--------------------------------------------------------------------------------
1 | module.exports = (self) => {
2 | return {
3 | addMigrations() {
4 | self.removePolymorphicTypeAliasMigration();
5 | },
6 | removePolymorphicTypeAliasMigration() {
7 | self.apos.migration.add('remove-polymorphic-type-alias', () => {
8 | return self.apos.doc.db.updateMany({
9 | type: '@apostrophecms/polymorphic'
10 | }, {
11 | $set: {
12 | type: '@apostrophecms/polymorphic-type'
13 | }
14 | });
15 | });
16 | }
17 | };
18 | };
19 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/rich-text-widget/lib/apiRoutes.js:
--------------------------------------------------------------------------------
1 | const fs = require('node:fs');
2 | const connectMultiparty = require('connect-multiparty');
3 | const { pipeline } = require('stream/promises');
4 | const { parse: csvParse } = require('csv-parse');
5 | const { Transform } = require('stream');
6 | const generateTable = require('./generateTiptapTable');
7 |
8 | module.exports = self => {
9 | return {
10 | post: {
11 | generateCsvTable: [
12 | connectMultiparty(),
13 | async (req) => {
14 | const { file } = req.files || {};
15 | if (!file) {
16 | throw self.apos.error('invalid', 'A file is required');
17 | }
18 |
19 | const extension = file.name.split('.').pop();
20 | if (extension !== 'csv') {
21 | throw self.apos.error('invalid', 'Only csv files are supported');
22 | }
23 |
24 | const data = {
25 | header: [],
26 | rows: []
27 | };
28 | await pipeline(
29 | fs.createReadStream(file.path),
30 | csvParse({
31 | columns: headers => {
32 | data.header = headers;
33 | return headers;
34 | }
35 | }),
36 | new Transform({
37 | objectMode: true,
38 | transform: function (record, encoding, callback) {
39 | const row = Object.values(record);
40 | data.rows.push(row);
41 | callback();
42 | }
43 | })
44 | );
45 |
46 | return generateTable(data);
47 | }
48 | ]
49 | }
50 | };
51 | };
52 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/rich-text-widget/lib/generateTiptapTable.js:
--------------------------------------------------------------------------------
1 | module.exports = ({ header, rows }) => {
2 | return {
3 | type: 'table',
4 | withHeaderRow: true,
5 | content: [
6 | {
7 | type: 'tableRow',
8 | content: header.map((head) => ({
9 | type: 'tableHeader',
10 | content: [ {
11 | type: 'paragraph',
12 | content: head
13 | ? [ {
14 | type: 'text',
15 | text: head
16 | } ]
17 | : []
18 | } ]
19 | }))
20 | },
21 | ...rows.map((row) => ({
22 | type: 'tableRow',
23 | content: row.map((cell) => ({
24 | type: 'tableCell',
25 | content: [
26 | {
27 | type: 'paragraph',
28 | content: cell
29 | ? [ {
30 | type: 'text',
31 | text: cell
32 | } ]
33 | : []
34 | }
35 | ]
36 | }))
37 | }))
38 | ]
39 | };
40 | };
41 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/rich-text-widget/ui/apos/apps/AposRichTextPermalinkResolver.js:
--------------------------------------------------------------------------------
1 | // This code is for EDITORS, to allow them to follow the permalinks
2 | // even though they are not replaced with the actual page URLs in
3 | // the markup on the server side. We do not do that for editors
4 | // because otherwise the permalink would be lost on the next edit.
5 |
6 | export default function() {
7 | window.addEventListener('hashchange', e => {
8 | // Typical case: hash link followed after page load
9 | if (followPermalink()) {
10 | e.stopPropagation();
11 | }
12 | });
13 | // Or, the URL could already contain a permalink
14 | followPermalink();
15 | async function followPermalink() {
16 | const hash = location.hash || '';
17 | const matches = hash.match(/#apostrophe-permalink-(.*)$/);
18 | if (matches) {
19 | const aposDocId = matches[1].replace(/\?.*$/, '');
20 | const doc = await apos.http.get(`${apos.doc.action}/${aposDocId}`, {});
21 | if (doc._url) {
22 | // This way the hashed version does not enter the history
23 | location.replace(doc._url);
24 | return true;
25 | }
26 | }
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
20 |
61 |
62 |
64 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapDivider.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | |
4 |
5 |
6 |
7 |
19 |
20 |
32 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapUndefined.vue:
--------------------------------------------------------------------------------
1 |
2 | Toolbar item has no definition: {{ name }}
3 |
4 |
5 |
17 |
18 |
23 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Anchor.js:
--------------------------------------------------------------------------------
1 | // Implement named anchors as spans with ids (more HTML5-ish).
2 |
3 | import { Mark, mergeAttributes } from '@tiptap/core';
4 |
5 | export default (options) => {
6 | return Mark.create({
7 | name: 'anchor',
8 |
9 | // What does this do? Copied from link
10 | priority: 1000,
11 |
12 | // What does this do? Copied from link
13 | keepOnSplit: false,
14 |
15 | addAttributes() {
16 | return {
17 | id: {
18 | default: null
19 | }
20 | };
21 | },
22 |
23 | parseHTML() {
24 | return [
25 | { tag: 'span[id]' }
26 | ];
27 | },
28 |
29 | renderHTML({ HTMLAttributes }) {
30 | return [
31 | 'span',
32 | mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
33 | 0
34 | ];
35 | },
36 |
37 | addCommands() {
38 | return {
39 | setAnchor: attributes => ({ chain }) => {
40 | return chain()
41 | .setMark(this.name, attributes)
42 | .run();
43 | },
44 |
45 | toggleAnchor: attributes => ({ chain }) => {
46 | return chain()
47 | .toggleMark(this.name, attributes, { extendEmptyMarkRange: true })
48 | .run();
49 | },
50 |
51 | unsetAnchor: () => ({ chain }) => {
52 | return chain()
53 | .unsetMark(this.name, { extendEmptyMarkRange: true })
54 | .run();
55 | }
56 | };
57 | }
58 | });
59 | };
60 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Color.js:
--------------------------------------------------------------------------------
1 | // imports the tiptap extension from node_modules
2 | import Color from '@tiptap/extension-color';
3 | export default (options) => {
4 | return Color.extend({});
5 | };
6 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Div.js:
--------------------------------------------------------------------------------
1 | import { mergeAttributes, Node } from '@tiptap/core';
2 |
3 | // Based on the default heading extension
4 |
5 | export default (options) => {
6 | return Node.create({
7 |
8 | name: 'div',
9 |
10 | addOptions() {
11 | return {
12 | HTMLAttributes: {}
13 | };
14 | },
15 |
16 | content: 'inline*',
17 |
18 | group: 'block',
19 |
20 | defining: true,
21 |
22 | parseHTML() {
23 | return [
24 | { tag: 'div' }
25 | ];
26 | },
27 |
28 | renderHTML({ HTMLAttributes }) {
29 | return [ 'div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0 ];
30 | },
31 |
32 | addCommands() {
33 | return {
34 | setDiv: attributes => ({ commands }) => {
35 | return commands.setNode(this.name, attributes);
36 | },
37 | toggleDiv: attributes => ({ commands }) => {
38 | return commands.toggleNode(this.name, 'paragraph', attributes);
39 | }
40 | };
41 | }
42 | });
43 | };
44 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Document.js:
--------------------------------------------------------------------------------
1 | // Acts as a custom Document extension
2 | import { Node } from '@tiptap/core';
3 | export default (options) => {
4 | const def = options.nodes.filter(style => style.def)[0];
5 | let content = 'block+'; // one or more block nodes (default Document setting)
6 | if (def) {
7 | // one/more defaultNodes (created in ./Default) or one/more other block
8 | // nodes
9 | content = '(defaultNode|block)+';
10 | }
11 | return Node.create({
12 | name: 'doc',
13 | topNode: true,
14 | content
15 | });
16 | };
17 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Heading.js:
--------------------------------------------------------------------------------
1 | // Lock Heading levels down to just those provided via configuration
2 | import Heading from '@tiptap/extension-heading';
3 |
4 | export default (options) => {
5 | const headings = options.nodes.filter(style => style.type === 'heading');
6 | const levels = headings.map(heading => heading.options.level);
7 | const defaultLevel = headings.filter(heading => heading.def).length
8 | ? headings.filter(heading => heading.def)[0].options.level
9 | : levels[0];
10 | return Heading.extend({
11 | addOptions() {
12 | return {
13 | ...this.parent?.(),
14 | levels
15 | };
16 | },
17 | addAttributes() {
18 | return {
19 | ...this.parent?.(),
20 | level: {
21 | default: defaultLevel,
22 | rendered: false
23 | }
24 | };
25 | },
26 | addKeyboardShortcuts() {
27 | const marks = Object.keys(this.editor.schema.marks);
28 | return this.options.levels.reduce((items, level) => ({
29 | ...items,
30 | ...{
31 | [`Mod-Alt-${level}`]: () => this.editor.commands.toggleHeading({ level }),
32 | Enter: () => marks.forEach(mark => this.editor.commands.unsetMark(mark))
33 | }
34 | }), {});
35 | }
36 | });
37 | };
38 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Link.js:
--------------------------------------------------------------------------------
1 | import Link from '@tiptap/extension-link';
2 | export default (options) => {
3 | return Link.extend({
4 | addOptions() {
5 | return {
6 | ...this.parent?.(),
7 | openOnClick: false,
8 | linkOnPaste: true,
9 | HTMLAttributes: {}
10 | };
11 | },
12 | addAttributes() {
13 | return {
14 | ...this.parent?.(),
15 | ...apos.modules['@apostrophecms/rich-text-widget'].linkSchema
16 | .filter(field => !!field.htmlAttribute)
17 | .reduce((obj, field) => {
18 | obj[field.htmlAttribute] = { default: field.def ?? null };
19 | return obj;
20 | }, {})
21 | };
22 | }
23 | });
24 | };
25 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/ListItem.js:
--------------------------------------------------------------------------------
1 | import ListItem from '@tiptap/extension-list-item';
2 | export default (options) => {
3 | return ListItem.extend({
4 | content: 'defaultNode block*'
5 | });
6 | };
7 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/TextStyle.js:
--------------------------------------------------------------------------------
1 | import TextStyle from '@tiptap/extension-text-style';
2 | export default (options) => {
3 | return TextStyle.extend({
4 | parseHTML() {
5 | return [
6 | { tag: 'span' }
7 | ];
8 | }
9 | });
10 | };
11 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/rich-text-widget/views/widget.html:
--------------------------------------------------------------------------------
1 | {% if data.options.className %}
2 | {% set className = data.options.className %}
3 | {% elif data.manager.options.className %}
4 | {% set className = data.manager.options.className %}
5 | {% endif %}
6 |
7 | {{ data.widget.content | safe }}
8 |
9 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/schema/lib/newInstance.js:
--------------------------------------------------------------------------------
1 | const { klona } = require('klona');
2 | const { createId } = require('@paralleldrive/cuid2');
3 |
4 | module.exports = newInstance;
5 |
6 | function newInstance(schema) {
7 | const instance = {};
8 | for (const field of schema) {
9 | if (field.def !== undefined) {
10 | instance[field.name] = klona(field.def);
11 | } else {
12 | // All fields should have an initial value in the database
13 | instance[field.name] = null;
14 | }
15 | // A workaround specifically for areas. They must have a
16 | // unique `_id` which makes `klona` a poor way to establish
17 | // a default, and we don't pass functions in schema
18 | // definitions, but top-level areas should always exist
19 | // for reasonable results if the output of `newInstance`
20 | // is saved without further editing on the front end
21 | if ((field.type === 'area') && (!instance[field.name])) {
22 | instance[field.name] = {
23 | metaType: 'area',
24 | items: [],
25 | _id: createId()
26 | };
27 | }
28 | // A workaround specifically for objects. These too need
29 | // to have reasonable values in parked pages and any other
30 | // situation where the data never passes through the UI
31 | if ((field.type === 'object') && ((!instance[field.name]) || (Object.keys(instance[field.name]).length === 0))) {
32 | instance[field.name] = newInstance(field.schema);
33 | }
34 | }
35 | return instance;
36 | }
37 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/schema/ui/apos/components/AposInputArea.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
17 |
19 |
32 |
33 |
34 |
35 |
36 |
37 |
44 |
45 |
57 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/schema/ui/apos/components/AposInputAttachment.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
21 |
22 |
23 |
24 |
25 |
32 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/schema/ui/apos/components/AposInputCheckboxes.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
17 |
18 |
28 |
29 |
30 |
31 |
32 |
39 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/schema/ui/apos/components/AposInputObject.vue:
--------------------------------------------------------------------------------
1 |
2 |
4 |
11 |
12 |
30 |
31 |
32 |
33 |
34 |
41 |
42 |
55 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/schema/ui/apos/components/AposInputPassword.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
23 |
24 |
25 |
26 |
27 |
28 |
36 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
19 |
20 |
21 |
22 |
23 |
30 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/schema/ui/apos/logic/AposInputBoolean.js:
--------------------------------------------------------------------------------
1 |
2 | import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin';
3 |
4 | export default {
5 | name: 'AposInputBoolean',
6 | mixins: [ AposInputMixin ],
7 | computed: {
8 | classList: function () {
9 | return [
10 | 'apos-input-wrapper',
11 | 'apos-boolean',
12 | {
13 | 'apos-boolean--toggle': this.field.toggle
14 | }
15 | ];
16 | },
17 | trueLabel: function () {
18 | if (this.field.toggle && this.field.toggle.true &&
19 | typeof this.field.toggle.true === 'string') {
20 | return this.field.toggle.true;
21 | } else {
22 | return false;
23 | }
24 | },
25 | falseLabel: function () {
26 | if (this.field.toggle && this.field.toggle &&
27 | typeof this.field.toggle.false === 'string') {
28 | return this.field.toggle.false;
29 | } else {
30 | return false;
31 | }
32 | }
33 | },
34 | methods: {
35 | setValue(val) {
36 | this.next = val;
37 | this.$refs[(!val).toString()].checked = false;
38 | },
39 | validate(value) {
40 | if (this.field.required) {
41 | if (!value && value !== false) {
42 | return 'required';
43 | }
44 | }
45 | return false;
46 | }
47 | }
48 | };
49 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/schema/ui/apos/logic/AposInputDateAndTime.js:
--------------------------------------------------------------------------------
1 | import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin';
2 | import dayjs from 'dayjs';
3 |
4 | export default {
5 | mixins: [ AposInputMixin ],
6 | emits: [ 'return' ],
7 | data() {
8 | return {
9 | next: (this.modelValue && this.modelValue.data) || null,
10 | date: '',
11 | time: '',
12 | disabled: !this.field.required
13 | };
14 | },
15 | mounted () {
16 | this.initDateAndTime();
17 | },
18 | watch: {
19 | 'field.required'(val) {
20 | if (val) {
21 | this.disabled = false;
22 | if (this.date) {
23 | this.setDateAndTime();
24 | }
25 | }
26 | }
27 | },
28 | methods: {
29 | toggle() {
30 | this.disabled = !this.disabled;
31 |
32 | if (this.disabled) {
33 | this.next = null;
34 | }
35 | },
36 | validate() {
37 | if (this.field.required && !this.next) {
38 | return 'required';
39 | }
40 | },
41 | initDateAndTime() {
42 | if (this.next) {
43 | this.date = dayjs(this.next).format('YYYY-MM-DD');
44 | this.time = dayjs(this.next).format('HH:mm:ss');
45 | this.disabled = false;
46 | }
47 | },
48 | setDateAndTime() {
49 | if (this.date) {
50 | this.next = dayjs(`${this.date} ${this.time}`.trim()).toISOString();
51 | this.disabled = false;
52 | } else {
53 | this.next = null;
54 | this.disabled = true;
55 | }
56 | }
57 | }
58 | };
59 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/schema/ui/apos/logic/AposInputPassword.js:
--------------------------------------------------------------------------------
1 |
2 | import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin';
3 |
4 | export default {
5 | name: 'AposInputPassword',
6 | mixins: [ AposInputMixin ],
7 | emits: [ 'return' ],
8 | computed: {
9 | tabindex () {
10 | return this.field.disableFocus ? '-1' : '0';
11 | }
12 | },
13 | methods: {
14 | validate(value) {
15 | if (this.field.required) {
16 | if (!value.length) {
17 | return { message: 'required' };
18 | }
19 | }
20 | if (this.field.min) {
21 | if (value.length && (value.length < this.field.min)) {
22 | return {
23 | message: this.$t('apostrophe:passwordErrorMin', {
24 | min: this.field.min
25 | })
26 | };
27 | }
28 | }
29 | if (this.field.max) {
30 | if (value.length && (value.length > this.field.max)) {
31 | return {
32 | message: this.$t('apostrophe:passwordErrorMax', {
33 | max: this.field.max
34 | })
35 | };
36 | }
37 | }
38 | return false;
39 | },
40 | emitReturn() {
41 | this.$emit('return');
42 | }
43 | }
44 | };
45 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/schema/ui/apos/logic/AposInputRadio.js:
--------------------------------------------------------------------------------
1 | import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin';
2 | import AposInputChoicesMixin from 'Modules/@apostrophecms/schema/mixins/AposInputChoicesMixin';
3 | import InformationIcon from '@apostrophecms/vue-material-design-icons/Information.vue';
4 |
5 | export default {
6 | name: 'AposInputRadio',
7 | components: { InformationIcon },
8 | mixins: [ AposInputMixin, AposInputChoicesMixin ],
9 | methods: {
10 | getChoiceId(uid, value) {
11 | return (uid + JSON.stringify(value)).replace(/\s+/g, '');
12 | },
13 | validate(value) {
14 | const validValue = this.choices.some((choice) => choice.value === value);
15 | if (this.field.required && !validValue && !value) {
16 | return 'required';
17 | }
18 |
19 | if (value && !validValue) {
20 | return 'invalid';
21 | }
22 |
23 | return false;
24 | },
25 | change(value) {
26 | // Allows expression of non-string values
27 | this.next = this.choices.find(choice => choice.value === JSON.parse(value)).value;
28 | }
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/schema/ui/apos/logic/AposInputSelect.js:
--------------------------------------------------------------------------------
1 | import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin';
2 | import AposInputChoicesMixin from 'Modules/@apostrophecms/schema/mixins/AposInputChoicesMixin';
3 |
4 | export default {
5 | name: 'AposInputSelect',
6 | mixins: [ AposInputMixin, AposInputChoicesMixin ],
7 | props: {
8 | icon: {
9 | type: String,
10 | default: 'menu-down-icon'
11 | }
12 | },
13 | data() {
14 | return {
15 | next: (this.modelValue.data == null) ? null : this.modelValue.data,
16 | choices: []
17 | };
18 | },
19 | computed: {
20 | classes() {
21 | return [ this.modelValue?.duplicate && 'apos-input--error' ];
22 | }
23 | },
24 | methods: {
25 | validate(value) {
26 | if (this.field.required && (value === null)) {
27 | return 'required';
28 | }
29 |
30 | if (value && !this.choices.find(choice => choice.value === value)) {
31 | return 'invalid';
32 | }
33 |
34 | return false;
35 | },
36 | change(value) {
37 | // Allows expression of non-string values
38 | this.next = this.choices.find(choice => choice.value === value).value;
39 | }
40 | }
41 | };
42 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/schema/ui/apos/mixins/AposInputFollowingMixin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Provides followingValues computation for fields having
3 | * sub-schema (array, object).
4 | */
5 |
6 | export default {
7 | methods: {
8 | // Accept a `data` object with field values and return an object
9 | // of the following values for each field of the underlying schema.
10 | computeFollowingValues(data) {
11 | const followingValues = {};
12 | const parentFollowing = {};
13 | for (const [ key, val ] of Object.entries(this.followingValues || {})) {
14 | parentFollowing[`<${key}`] = val;
15 | }
16 |
17 | for (const field of this.schema) {
18 | if (field.following) {
19 | const following = Array.isArray(field.following)
20 | ? field.following
21 | : [ field.following ];
22 | followingValues[field.name] = {};
23 | for (const name of following) {
24 | if (name.startsWith('<')) {
25 | followingValues[field.name][name] = parentFollowing[name];
26 | } else {
27 | followingValues[field.name][name] = data[name];
28 | }
29 | }
30 | }
31 | }
32 |
33 | return followingValues;
34 | }
35 | }
36 | };
37 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/search/views/index.html:
--------------------------------------------------------------------------------
1 | {% extends data.outerLayout %}
2 | {% block title %}{{ data.page.title }}{% endblock %}
3 |
4 | {% block main %}
5 |
23 | {% for doc in data.docs %}
24 |
25 | {% endfor %}
26 | {% include "pager.html" %}
27 | {% endblock %}
28 |
29 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/search/views/indexAjax.html:
--------------------------------------------------------------------------------
1 | {% block main %}
2 | {% for doc in data.docs %}
3 |
4 | {% endfor %}
5 | {% endblock %}
6 |
7 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/search/views/pager.html:
--------------------------------------------------------------------------------
1 | {% import '@apostrophecms/pager:macros.html' as pager with context %}
2 | {{ pager.render({
3 | page: data.currentPage,
4 | total: data.totalPages
5 | }, data.url) }}
6 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/search/views/suggest.html:
--------------------------------------------------------------------------------
1 | {% block main %}
2 | {% if data.docs.length %}
3 | {{ __t("apostrophe:suggestionsHeader") }}
4 | {% for doc in data.docs %}
5 |
6 | {% endfor %}
7 | {% endif %}
8 | {% endblock %}
9 |
10 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/settings/ui/apos/apps/TheAposSettings.js:
--------------------------------------------------------------------------------
1 | export default function() {
2 | if (apos.settings.restore) {
3 | apos.modal.execute('AposSettingsManager', {
4 | restore: apos.settings.restore
5 | });
6 | apos.settings.restore = null;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/template/views/inject.html:
--------------------------------------------------------------------------------
1 | {% for c in data.components %}
2 | {% component c %}
3 | {% endfor %}
4 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/template/views/outerLayout.html:
--------------------------------------------------------------------------------
1 | {% extends "outerLayoutBase.html" %}
2 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/template/views/refreshLayout.html:
--------------------------------------------------------------------------------
1 | {% block beforeMain %}{% endblock %}
2 | {% block main %}{% endblock %}
3 | {% block afterMain %}{% endblock %}
4 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | options: {
3 | alias: 'ui',
4 | widgetMargin: '20px 0'
5 | },
6 | icons: {
7 | 'earth-icon': 'Earth',
8 | 'database-check-icon': 'DatabaseCheck'
9 | },
10 | init(self) {
11 | self.enableBrowserData();
12 | },
13 | methods(self) {
14 | return {
15 | getBrowserData(req) {
16 | const theme = {
17 | primary: 'default'
18 | };
19 | if (req.data.global && req.data.global.aposThemePrimary) {
20 | theme.primary = req.data.global.aposThemePrimary;
21 | }
22 | if (req.data.user && req.data.user.aposThemePrimary) {
23 | theme.primary = req.data.user.aposThemePrimary;
24 | }
25 | return {
26 | theme,
27 | widgetMargin: self.options.widgetMargin
28 | };
29 | }
30 | };
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/apps/AposBusEvent.js:
--------------------------------------------------------------------------------
1 | // Global event listener that will emit bus events from anywhere via click
2 | // To use: Create some UI with the below data attributes
3 | // data-apos-bus-event='{"name": "EVENT-NAME", "data": {"FOO": TRUE}}'
4 | // Also accepts a simplified name-only string data-apos-bus-event="NAME"
5 | // `data` parameter is optional, if present will be passed through the emitted
6 | // event
7 |
8 | export default function() {
9 | document.body.addEventListener('click', (e) => {
10 | if (e.target.getAttribute('data-apos-bus-event')) {
11 | const event = e.target.getAttribute('data-apos-bus-event');
12 | let name;
13 | let json = {};
14 | try {
15 | json = JSON.parse(event);
16 | name = json.name || false;
17 | } catch (e) {
18 | name = event;
19 | }
20 | if (name) {
21 | apos.bus.$emit(name, json.data || null);
22 | } else {
23 | // eslint-disable-next-line no-console
24 | console.error('Apostrophe bus events require a name');
25 | apos.notify('apostrophe:error', { type: 'error' });
26 | }
27 | }
28 | }, false);
29 | };
30 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/components/AposCellBasic.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 | {{ get(header.name) }}
7 |
8 |
9 |
10 |
19 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/components/AposCellButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 | {{ get(header.name) }}
8 |
9 |
10 |
11 |
19 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/components/AposCellLink.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
19 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/components/AposCellType.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 | {{ $t(label(get(header.name))) }}
7 |
8 |
9 |
10 |
23 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/components/AposCloudUploadIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
19 |
23 |
27 |
31 |
35 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
50 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/components/AposColorCheckerboard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
21 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuTip.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
33 |
34 |
35 |
38 |
39 |
44 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/components/AposEmptyState.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 | {{ $t(emptyState.title) }}
8 |
9 |
13 | {{ $t(emptyState.message) }}
14 |
15 |
19 | {{ emptyState.emoji }}
20 |
21 |
22 |
23 |
24 |
35 |
36 |
68 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/components/AposIndicator.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
16 |
17 |
18 |
19 |
55 |
66 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/components/AposLoadingBlock.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
12 | {{ $t(label) }}
13 |
14 |
15 |
16 |
17 |
18 |
30 |
31 |
56 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/components/AposLocale.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ $t('apostrophe:locale') }}:
5 |
6 | {{ locale }}
7 |
8 |
9 |
10 |
11 |
25 |
26 |
39 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/components/AposPagerDots.vue:
--------------------------------------------------------------------------------
1 |
2 |
20 |
21 |
22 |
49 |
50 |
81 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/components/AposTag.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
12 |
13 | {{ label }}
14 |
15 |
16 |
17 |
37 |
38 |
78 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/composables/AposTheme.js:
--------------------------------------------------------------------------------
1 | import { computed } from 'vue';
2 |
3 | export function useAposTheme () {
4 | const themeClass = computed(() => {
5 | const classes = [];
6 | classes.push(`apos-theme--primary-${window.apos.ui.theme.primary}`);
7 | return classes;
8 | });
9 |
10 | return { themeClass };
11 | };
12 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/lib/click-outside-element.js:
--------------------------------------------------------------------------------
1 | export default {
2 | install(app, options) {
3 | app.directive('click-outside-element', {
4 | beforeMount(el, binding) {
5 | el.aposClickOutsideHandler = (event) => {
6 | if (
7 | (el !== event.target) &&
8 | !el.contains(event.target) &&
9 | !apos.modal.onTopOf(event.target, el)
10 | ) {
11 | binding.value(event);
12 | }
13 | };
14 | document.body.addEventListener('click', el.aposClickOutsideHandler);
15 | },
16 | beforeUnmount(el, binding) {
17 | document.body.removeEventListener('click', el.aposClickOutsideHandler);
18 | }
19 | });
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/lib/vue.js:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue';
2 | import { createPinia } from 'pinia';
3 | import ClickOutsideElement from './click-outside-element';
4 | import Tooltip from './tooltip';
5 | import VueAposI18Next from './i18next';
6 |
7 | const pinia = createPinia();
8 |
9 | export default (appConfig, props = {}) => {
10 | const app = createApp(appConfig, props);
11 |
12 | app.use(VueAposI18Next, {
13 | // Module aliases are not available yet when this code executes
14 | i18n: apos.modules['@apostrophecms/i18n']
15 | });
16 | app.use(Tooltip);
17 | app.use(ClickOutsideElement);
18 | app.use(pinia);
19 |
20 | const sources = [ window.apos.vueComponents, window.apos.iconComponents ];
21 | for (const source of sources) {
22 | for (const [ name, component ] of Object.entries(source)) {
23 | app.component(name, component);
24 | }
25 | }
26 | return app;
27 | };
28 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/mixins/AposCellMixin.js:
--------------------------------------------------------------------------------
1 | export default {
2 | props: {
3 | header: {
4 | type: Object,
5 | required: true
6 | },
7 | draft: {
8 | type: Object,
9 | required: true
10 | },
11 | published: {
12 | type: Object,
13 | default() {
14 | return null;
15 | }
16 | }
17 | },
18 | methods: {
19 | // Access to property or sub-property via dot path. You can also optionally
20 | // specify a source object other than `item`.
21 | //
22 | // `this.get('title')` gets `this.item.title`.
23 | // `this.get('draft:submitted.by')` gets `this.draft.submitted.by`.
24 | get(fieldName) {
25 | let [ namespace, path ] = fieldName.split(':');
26 | if (!path) {
27 | path = namespace;
28 | namespace = 'item';
29 | }
30 | const components = path.split('.');
31 | let value = this[namespace];
32 | try {
33 | for (const component of components) {
34 | value = value[component];
35 | }
36 | } catch (e) {
37 | // Intentionally tolerant, like _.get
38 | return null;
39 | }
40 | return value;
41 | }
42 | },
43 | computed: {
44 | item() {
45 | return this.published || this.draft;
46 | },
47 | moduleOptions() {
48 | return apos.modules[this.item.type];
49 | },
50 | manuallyPublished() {
51 | return this.moduleOptions.localized && !this.autopublish;
52 | },
53 | autopublish() {
54 | return this.item._aposAutopublish ?? this.moduleOptions.autopublish;
55 | }
56 | }
57 | };
58 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/mixins/AposThemeMixin.js:
--------------------------------------------------------------------------------
1 | // Provides computed classes for decorating top-level Apos vue apps with a UI
2 | // theme
3 |
4 | export default {
5 | computed: {
6 | themeClass() {
7 | const classes = [];
8 | classes.push(`apos-theme--primary-${window.apos.ui.theme.primary}`);
9 | return classes;
10 | }
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "NOTE: needed for testing in test/utils.js, for js files to be treated as ESM",
3 | "type": "module"
4 | }
5 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/scss/global/_admin.scss:
--------------------------------------------------------------------------------
1 | .apos-editor-context {
2 | display: flex;
3 | justify-content: center;
4 | padding-top: 50px;
5 | background-color: var(--a-base-9);
6 | }
7 |
8 | .apos-editor-canvas {
9 | width: $a-canvas-max;
10 | margin: 20px auto;
11 | box-shadow: 0 0 9px 0 rgb(0 0 0 / 15%);
12 | min-height: 100vh;
13 | background-color: var(--a-background-primary);
14 | }
15 |
16 | // TODO: remove these forced focus styles after keyboard accessibility is done.
17 | // This is useful for testing to visually see focused elements
18 | // that may not have the browser-native blue outline when focused.
19 | //
20 | // *:focus, *:focus-visible {
21 | // outline: blue solid 2px!important;
22 | // background-color: rgba(0,0,255,0.3)!important;
23 | // }
24 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/scss/global/_breakpoint_preview.scss:
--------------------------------------------------------------------------------
1 | [data-apos-context-label] {
2 | @include type-help;
3 |
4 | & {
5 | position: relative;
6 | top: $spacing-base * 5;
7 | display: none;
8 | text-align: center;
9 | }
10 | }
11 |
12 | body[data-breakpoint-preview-mode] {
13 | container-type: size;
14 | background: var(--a-base-10);
15 |
16 | [data-apos-context-label] {
17 | display: block;
18 | }
19 |
20 | [data-apos-refreshable] {
21 | position: relative;
22 | container-type: size;
23 | overflow: clip scroll;
24 | flex-grow: unset;
25 | margin: $spacing-base * 6 auto auto;
26 | border: 1px solid var(--a-base-6);
27 | box-shadow: 0 0 12px 0 var(--a-base-7);
28 | background-color: var(--a-background-primary);
29 |
30 | &[data-resizable="false"] {
31 | transition:
32 | width 400ms ease,
33 | height 400ms ease;
34 | }
35 |
36 | &[data-resizable="true"] {
37 | resize: both;
38 | overflow: scroll;
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/scss/global/_normalize.scss:
--------------------------------------------------------------------------------
1 | [class*='apos-'] {
2 | box-sizing: content-box;
3 | }
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/scss/global/_rich-text-table.scss:
--------------------------------------------------------------------------------
1 | [data-rich-text] table:not([class]),
2 | .tiptap table:not([class]),
3 | .tableWrapper table, /* stylelint-disable-line selector-class-pattern */
4 | .apos-rich-text-table {
5 | overflow: hidden;
6 | width: 100%;
7 | margin: 0;
8 | border-collapse: collapse;
9 | table-layout: fixed;
10 |
11 | td,
12 | th {
13 | position: relative;
14 | box-sizing: border-box;
15 | padding: 6px 8px;
16 | border: 1px solid var(--a-base-4);
17 | min-width: 1em;
18 | vertical-align: top;
19 |
20 | > * { /* stylelint-disable-line max-nesting-depth */
21 | margin-bottom: 0;
22 | }
23 | }
24 |
25 | th {
26 | background-color: var(--a-base-8);
27 | font-weight: 700;
28 | text-align: left;
29 | }
30 |
31 | .selectedCell { /* stylelint-disable-line selector-class-pattern */
32 | &::after { /* stylelint-disable-line max-nesting-depth */
33 | z-index: $z-index-manager-display;
34 | position: absolute; inset: 0;
35 | background: var(--a-base-3);
36 | opacity: 0.25;
37 | content: "";
38 | pointer-events: none;
39 | }
40 | }
41 |
42 | .column-resize-handle {
43 | position: absolute;
44 | top: 0;
45 | right: -2px;
46 | bottom: -2px;
47 | width: 4px;
48 | background-color: var(--a-primary-transparent-50);
49 | pointer-events: none;
50 | }
51 |
52 | .resize-cursor {
53 | cursor: col-resize;
54 | }
55 | }
56 |
57 | .tableWrapper { /* stylelint-disable-line selector-class-pattern */
58 | margin: 1.5rem 0;
59 | overflow-x: auto;
60 | }
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/scss/global/_utilities.scss:
--------------------------------------------------------------------------------
1 | .apos-sr-only {
2 | visibility: hidden;
3 | position: absolute;
4 | }
5 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/scss/global/_widgets.scss:
--------------------------------------------------------------------------------
1 | [data-apos-area] [data-apos-area] {
2 | padding: var(--a-widget-margin);
3 | }
4 |
5 | [data-rich-text] p:empty::after {
6 | content: '\00A0';
7 | }
8 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/scss/global/import-all.scss:
--------------------------------------------------------------------------------
1 | // Just like components, the shared stuff needs access to the mixins
2 | @import '../mixins/import-all';
3 |
4 | // One-time imports used project wide, like utility classes, login styles, etc.
5 | @import './_normalize';
6 | @import './_admin';
7 | @import './_theme';
8 | @import './_inputs';
9 | @import './_scrollbars';
10 | @import './_utilities';
11 | @import './_tables';
12 | @import './_tooltips';
13 | @import './_widgets';
14 | @import './_breakpoint_preview';
15 | @import './_rich-text-table';
16 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/scss/mixins/_admin_mixins.scss:
--------------------------------------------------------------------------------
1 | $a-canvas-max: 1168px;
2 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/scss/mixins/_input_mixins.scss:
--------------------------------------------------------------------------------
1 | $input-max-width: 580px;
2 | $box-width: 12px;
3 |
4 | @mixin apos-input() {
5 | @include type-base;
6 | @include apos-transition(all);
7 |
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 | }
16 |
17 | &::placeholder {
18 | color: var(--a-base-4);
19 | font-style: italic;
20 | }
21 |
22 | &:hover,
23 | &:focus {
24 | border-color: var(--a-base-2);
25 | }
26 |
27 | &:focus {
28 | outline: none;
29 | box-shadow: 0 0 3px var(--a-base-2);
30 | border-color: var(--a-base-2);
31 | background-color: var(--a-base-10);
32 | }
33 |
34 | &[disabled] {
35 | color: var(--a-base-4);
36 | background: var(--a-base-7);
37 | border-color: var(--a-base-4);
38 |
39 | &:hover {
40 | cursor: not-allowed;
41 | }
42 | }
43 |
44 | .apos-field--error & {
45 | border-color: var(--a-danger);
46 |
47 | &:focus {
48 | outline: none;
49 | box-shadow: 0 0 3px var(--a-danger);
50 | }
51 | }
52 |
53 | &--text,
54 | &--time,
55 | &--textarea,
56 | &--date,
57 | &--select,
58 | &--password,
59 | &--email {
60 | padding: $input-padding;
61 | padding-right: $input-padding * 2;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/scss/mixins/_mixins.scss:
--------------------------------------------------------------------------------
1 | // Imports all mixins, variables, functions, other meta that generates no classes
2 | // until used
3 |
4 | @mixin apos-transition($what: all, $duration: 0.1s, $ease: ease-in-out) {
5 | & {
6 | transition: $what $duration $ease;
7 | }
8 | }
9 |
10 | @mixin apos-p-reset() {
11 | margin: 0;
12 | }
13 |
14 | @mixin apos-align-icon() {
15 | display: flex;
16 | align-items: center;
17 | }
18 |
19 | @mixin apos-list-reset() {
20 | list-style-type: none;
21 | margin: 0;
22 | padding: 0;
23 | }
24 |
25 | @mixin apos-fieldset-reset() {
26 | margin: 0;
27 | padding: 0;
28 | border: none;
29 | }
30 |
31 | @mixin apos-button-reset() {
32 | overflow: visible;
33 | width: auto;
34 | margin: 0;
35 | padding: 0;
36 | border: none;
37 | color: inherit;
38 | background: transparent;
39 | font-family: inherit;
40 | font-weight: inherit;
41 | line-height: inherit;
42 | letter-spacing: inherit;
43 | text-align: inherit;
44 | -webkit-font-smoothing: inherit;
45 | -moz-osx-font-smoothing: inherit;
46 | // stylelint-disable property-no-vendor-prefix
47 | -moz-appearance: none;
48 | -webkit-appearance: none;
49 | // stylelint-enable property-no-vendor-prefix
50 | &:hover {
51 | cursor: pointer;
52 | }
53 |
54 | &::-moz-focus-inner {
55 | padding: 0;
56 | border: 0;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/scss/mixins/_responsive.scss:
--------------------------------------------------------------------------------
1 | // For more on this naming system, search "ergonomic responsive breakpoints."
2 | $breakpoints: (
3 | palm: 320px,
4 | hands: 600px,
5 | hands-wide: 800px,
6 | lap: 1200px,
7 | desk: 1600px,
8 | );
9 |
10 | $modal-rail-right-w: 22%;
11 |
12 | // We default to mobile first, but make a max-with option available as well.
13 | @mixin media-up($bp) {
14 | $bp-val: map.get($breakpoints, $bp);
15 |
16 | @media screen and (min-width: $bp-val) {
17 | @content;
18 | }
19 | }
20 | // We should use the mixin above. In the unlikely case that a `max-width`
21 | // media query is needed, the mixin below should be used rather than
22 | // hand-rolling something.
23 | // @mixin media-down ($bp) {
24 | // $bpVal: map-get($breakpoints, $bp);
25 | // @media screen and (max-width: $bpVal - 1px) {
26 | // @content;
27 | // }
28 | // }
29 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/scss/mixins/_type_mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin link-primary {
2 | & {
3 | color: var(--a-primary);
4 | }
5 |
6 | &:hover,
7 | &:focus {
8 | color: var(--a-primary-dark-10);
9 | }
10 |
11 | &:active {
12 | color: var(--a-primary-dark-15);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/scss/mixins/_zindex.scss:
--------------------------------------------------------------------------------
1 | // NOTE: These should be kept in rank order for easy comparison.
2 | $z-index-under: -1;
3 | // Use `z-index-base` to simply establish a new stacking context.
4 | $z-index-base: 0;
5 | $z-index-default: 1;
6 | $z-index-model-popup: 1;
7 | $z-index-manager-display: 2;
8 | // WIDGET Z-INDEX NOTE: It's important for all widgets in an area to be in the
9 | // same z-index stacking context. The only aspects that should be above the
10 | // base stacking context are the controls (including the label).
11 | $z-index-widget-label: 2;
12 | $z-index-manager-toolbar: 3;
13 | $z-index-widget-controls: 3;
14 | $z-index-widget-focused-controls: 4;
15 | $z-index-admin-bar: 1000;
16 | $z-index-area-schema-ui: 1005;
17 | $z-index-modal: 2000;
18 | $z-index-notifications: 2003;
19 | $z-index-busy: 2004;
20 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/scss/mixins/import-all.scss:
--------------------------------------------------------------------------------
1 | /*
2 | apostrophe-build uses this to prepend SCSS with globals.
3 | Order here does matter, so make sure vars, mixins, functions are loaded first.
4 | */
5 |
6 | @import './_mixins';
7 | @import './_admin_mixins';
8 | @import './_input_mixins';
9 | @import './_theme_mixins';
10 | @import './_type_mixins';
11 | @import './_zindex';
12 | @import './_responsive';
13 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/scss/shared/_table-rows.scss:
--------------------------------------------------------------------------------
1 | .apos-tree__cell {
2 | display: inline-flex;
3 | box-sizing: border-box;
4 | flex-shrink: 2;
5 | align-items: center;
6 | padding: $cell-padding;
7 | border-bottom: 1px solid var(--a-base-8);
8 | }
9 |
10 | // stylelint-disable-next-line selector-class-pattern
11 | .apos-tree__cell--contextMenu {
12 | padding: 0;
13 | }
14 |
15 | // Let the title cell column grow.
16 | span.apos-tree__cell:first-of-type { // stylelint-disable-line selector-no-qualifying-type
17 | flex-grow: 1;
18 | flex-shrink: 1;
19 | }
20 |
21 | .apos-tree__row-data {
22 | position: relative;
23 | display: flex;
24 | width: 100%;
25 | }
26 |
27 | .apos-tree__cell__icon + .apos-tree__cell__label {
28 | margin-left: 5px;
29 | }
30 |
31 | .apos-tree__cell[disabled='true'] {
32 | color: $input-color-disabled;
33 |
34 | &:hover {
35 | cursor: not-allowed;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/ui/ui/apos/scss/shared/_table-vars.scss:
--------------------------------------------------------------------------------
1 | $row-nested-h-padding: 24px;
2 | $cell-padding: 12px;
--------------------------------------------------------------------------------
/modules/@apostrophecms/user/lib/legacy-migrations.js:
--------------------------------------------------------------------------------
1 | // Migrations relevant only to those who used early alpha and beta versions of
2 | // 3.x are kept here for tidiness
3 |
4 | module.exports = (self) => {
5 | return {
6 | addLegacyMigrations() {
7 | self.addRoleMigration();
8 | },
9 | addRoleMigration() {
10 | self.apos.migration.add('add-role-to-user', async () => {
11 | return self.apos.doc.db.updateMany({
12 | type: '@apostrophecms/user',
13 | role: {
14 | $exists: 0
15 | }
16 | }, {
17 | $set: {
18 | role: 'admin'
19 | }
20 | });
21 | });
22 | }
23 | };
24 | };
25 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/util/ui/src/index.js:
--------------------------------------------------------------------------------
1 | import util from './util.js';
2 | import http from './http.js';
3 |
4 | export default () => {
5 | util();
6 | http();
7 | };
8 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/video-widget/index.js:
--------------------------------------------------------------------------------
1 | // Provides the `@apostrophecms/video` widget, which displays videos, powered
2 | // by [@apostrophecms/video-field](../@apostrophecms/video-field/index.html) and
3 | // [@apostrophecms/oembed](../@apostrophecms/oembed/index.html). The video
4 | // widget accepts the URL of a video on any website that supports the
5 | // [oembed](http://oembed.com/) standard, including vimeo, YouTube, etc.
6 | // In some cases the results are refined by oembetter filters configured
7 | // by the `@apostrophecms/oembed` module. It is possible to configure new
8 | // filters for that module to handle video sites that don't natively support
9 | // oembed.
10 | //
11 | // Videos are not actually hosted or stored by Apostrophe.
12 |
13 | module.exports = {
14 | extend: '@apostrophecms/widget-type',
15 | options: {
16 | label: 'apostrophe:video',
17 | className: false,
18 | icon: 'play-box-icon',
19 | placeholder: true,
20 | placeholderClass: false,
21 | placeholderUrl: 'https://youtu.be/Q5UX9yexEyM'
22 | },
23 | fields: {
24 | add: {
25 | video: {
26 | type: 'oembed',
27 | name: 'video',
28 | oembedType: 'video',
29 | label: 'apostrophe:videoUrl',
30 | help: 'apostrophe:videoUrlHelp',
31 | required: true
32 | }
33 | }
34 | }
35 | };
36 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/video-widget/views/widget.html:
--------------------------------------------------------------------------------
1 | {% if data.widget.aposPlaceholder and data.manager.options.placeholderUrl %}
2 |
6 |
7 | {% else %}
8 | {% if data.options.className %}
9 | {% set className = data.options.className %}
10 | {% elif data.manager.options.className %}
11 | {% set className = data.manager.options.className %}
12 | {% endif %}
13 |
14 | {# oembed repopulates me #}
15 | {% if data.widget.video %}
16 |
21 |
22 | {% elif data.user %}
23 | No video selected
24 | {% endif %}
25 | {% endif %}
26 |
--------------------------------------------------------------------------------
/modules/@apostrophecms/widget-type/ui/apos/components/AposWidget.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
21 |
--------------------------------------------------------------------------------
/scripts/README.txt:
--------------------------------------------------------------------------------
1 | These are utilities, usually one-offs that came in handy during the process of developing
2 | Apostrophe itself, and might come in handy again. You probably don't need them for
3 | other purposes.
--------------------------------------------------------------------------------
/scripts/find-busted-test.bash:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # This is a script to run our mocha tests one by one, rather than automatically
4 | # via mocha, so we can figure out which one is hanging if we have unreleased
5 | # timeouts, etc. at the end of a test script. Not needed often. -Tom
6 |
7 | files=test/*.js
8 | echo $files
9 | for file in $files; do
10 | echo $file
11 | ./node_modules/.bin/mocha $file
12 | done
13 |
14 |
--------------------------------------------------------------------------------
/scripts/find-heavy-npm-modules:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | /* eslint-disable node/no-path-concat */
3 |
4 | const fs = require('fs');
5 |
6 | const trueDeps = JSON.parse(fs.readFileSync(`${__dirname}/../package-lock.json`)).packages;
7 | const deps = {};
8 | for (let [ name, props ] of Object.entries(trueDeps)) {
9 | if (props.dev) {
10 | continue;
11 | }
12 | const lastIndex = name.lastIndexOf('node_modules/');
13 | if (lastIndex !== -1) {
14 | name = name.substring(lastIndex + 13);
15 | }
16 | deps[name] = props;
17 | }
18 | const costs = new Map();
19 | for (const name of Object.keys(deps[''].dependencies)) {
20 | costs.set(name, countWeight(name));
21 | }
22 |
23 | function countWeight(name) {
24 | const subDeps = deps[name].dependencies || {};
25 | let weight = 0;
26 | for (const name of Object.keys(subDeps)) {
27 | weight += countWeight(name);
28 | }
29 | return weight + 1;
30 | }
31 |
32 | const sorted = [ ...costs.entries() ].sort((a, b) => a[1] - b[1]);
33 | for (const [ name, cost ] of sorted) {
34 | console.log(`${name} has ${cost} sub-dependencies`);
35 | }
36 |
37 | const nonDevDeps = Object.keys(trueDeps).filter(name => !trueDeps[name].dev).length;
38 | console.log(`Total dependencies: ${nonDevDeps}`);
39 |
--------------------------------------------------------------------------------
/scripts/lint-i18n.js:
--------------------------------------------------------------------------------
1 | const glob = require('../lib/glob.js');
2 | const fs = require('fs');
3 | let keys = Object.keys(require('../modules/@apostrophecms/i18n/i18n/en.json'));
4 | // Core apostrophe events look like keys
5 | keys = [ ...keys, 'destroy', 'ready', 'modulesRegistered', 'afterInit', 'modulesReady', 'run', 'boot', 'beforeExit' ];
6 | const files = glob('**/*.@(js|vue|html)', { ignore: [ './index.js', '**/node_modules/**/*', 'coverage/**/*' ] });
7 |
8 | const undeclared = new Set();
9 | const used = new Set([ 'afterInit', 'modulesReady' ]);
10 |
11 | for (const file of files) {
12 | const code = fs.readFileSync(file, 'utf8');
13 | const found = code.matchAll(/apostrophe:\w+/g);
14 | for (const match of found) {
15 | const key = match[0].replace('apostrophe:', '');
16 | if (!keys.includes(key)) {
17 | undeclared.add(key);
18 | } else {
19 | used.add(key);
20 | }
21 | }
22 | }
23 |
24 | const ignoreUnused = [ 'boot', 'beforeExit' ];
25 |
26 | const unused = keys.filter(key => !used.has(key)).filter(key => !used.has(key.replace('_plural', ''))).filter(key => !ignoreUnused.includes(key));
27 | if ((!undeclared.size) && (!unused.length)) {
28 | process.exit(0);
29 | }
30 |
31 | console.error('Undefined:\n');
32 | console.error([ ...undeclared ].join('\n'));
33 | console.error('\nUnused:\n');
34 | for (const key of unused) {
35 | console.log(key, used.has(key), used.has(key.replace('_plural', '')), ignoreUnused.includes(key));
36 | }
37 | console.error(unused.join('\n'));
38 | process.exit(1);
39 |
--------------------------------------------------------------------------------
/test/base-module.js:
--------------------------------------------------------------------------------
1 | const t = require('../test-lib/test.js');
2 | const assert = require('assert');
3 |
4 | describe('Base Module', function() {
5 |
6 | let apos;
7 |
8 | this.timeout(t.timeout);
9 |
10 | after(async function() {
11 | return t.destroy(apos);
12 | });
13 |
14 | it('should be subclassable', async function() {
15 | apos = await t.create({
16 | root: module,
17 | modules: {
18 | // will push an asset for us to look for later
19 | '@apostrophecms/test-module-push': {},
20 | // test the getOption method of modules
21 | 'test-get-option': {},
22 | 'test-get-option-2': {}
23 | }
24 | });
25 | assert(apos.test && apos.test.color === 'red');
26 | });
27 |
28 | it('should produce correct responses via the getOption method', async function() {
29 | const mod = apos.modules['test-get-option'];
30 | const req = apos.task.getReq();
31 | assert.strictEqual(mod.getOption(req, 'flavors.grape.sweetness'), 20);
32 | assert.strictEqual(mod.getOption(req, 'flavors.cheese.swarthiness'), undefined);
33 | assert.strictEqual(mod.getOption(req, 'flavors.grape.ingredients.0'), 'chemicals');
34 | const markup = await mod.render(req, 'test.html');
35 | assert(markup.match(/20/));
36 | assert(markup.match(/yup/));
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/test/base-url-env-var.js:
--------------------------------------------------------------------------------
1 | const t = require('../test-lib/test.js');
2 | const assert = require('assert');
3 |
4 | const config = {
5 | root: module,
6 | // Should get overridden by the above
7 | baseUrl: 'http://localhost:3000'
8 | };
9 |
10 | describe('Locales', function() {
11 | this.timeout(t.timeout);
12 | let apos;
13 | let savedBaseUrl;
14 |
15 | before(async function() {
16 | savedBaseUrl = process.env.APOS_BASE_URL;
17 | process.env.APOS_BASE_URL = 'https://madethisup.com';
18 | apos = await t.create(config);
19 | });
20 |
21 | after(function() {
22 | if (savedBaseUrl) {
23 | process.env.APOS_BASE_URL = savedBaseUrl;
24 | } else {
25 | delete process.env.APOS_BASE_URL;
26 | }
27 | return t.destroy(apos);
28 | });
29 |
30 | it('APOS_BASE_URL should take effect', async function() {
31 | const req = apos.task.getReq();
32 | const home = await apos.doc.find(req, { slug: '/' }).toObject();
33 | assert(home);
34 | assert.strictEqual(home._url, 'https://madethisup.com/');
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/test/bootstrapping.js:
--------------------------------------------------------------------------------
1 | const t = require('../test-lib/test.js');
2 | const assert = require('assert');
3 |
4 | describe('bootstrap of Apostrophe core', function() {
5 |
6 | this.timeout(t.timeout);
7 |
8 | // BOOTSTRAP FUNCTIONS ------------------------------------------- //
9 |
10 | it('should merge the options and local.js correctly', async function() {
11 | let apos;
12 | try {
13 | apos = await t.create({
14 | root: module,
15 | overrideTest: 'test' // overriden by data/local.js
16 | });
17 | assert(apos.options.overrideTest === 'foo');
18 | } finally {
19 | await t.destroy(apos);
20 | }
21 | });
22 |
23 | it('should accept a `__localPath` option and invoke local.js as a function if it is provided as one', async function() {
24 | let apos;
25 | try {
26 | apos = await t.create({
27 | root: module,
28 | overrideTest: 'test', // overriden by data/local_fn.js
29 |
30 | __localPath: '/data/local_fn.js'
31 | });
32 | assert(apos.options.overrideTest === 'foo');
33 | } finally {
34 | await t.destroy(apos);
35 | }
36 | });
37 |
38 | it('should invoke local.js as a function with the apos and config object', async function() {
39 | let apos;
40 | try {
41 | apos = await t.create({
42 | root: module,
43 | overrideTest: 'test', // concated in local_fn_b.js
44 |
45 | __localPath: '/data/local_fn_b.js'
46 | });
47 | assert(apos.options.overrideTest === 'test-foo');
48 | } finally {
49 | await t.destroy(apos);
50 | }
51 | });
52 |
53 | });
54 |
--------------------------------------------------------------------------------
/test/common-js.js:
--------------------------------------------------------------------------------
1 | const { strict: assert } = require('node:assert');
2 | const t = require('../test-lib/test.js');
3 |
4 | describe('Apostrophe CommonJS', function() {
5 | this.timeout(t.timeout);
6 |
7 | let apos;
8 |
9 | before(async function() {
10 | apos = await t.create({
11 | root: module
12 | });
13 | });
14 |
15 | after(function() {
16 | return t.destroy(apos);
17 | });
18 |
19 | it('should have root, rootDir, npmRootDir', function() {
20 | const actual = {
21 | root: {
22 | ...apos.root,
23 | filename: apos.root.filename,
24 | import: apos.root.import.toString(),
25 | require: apos.root.require.toString()
26 | },
27 | rootDir: apos.rootDir,
28 | npmRootDir: apos.npmRootDir
29 | };
30 | const expected = {
31 | root: {
32 | filename: module.filename,
33 | import: actual.root.import.toString(),
34 | require: actual.root.require.toString()
35 | },
36 | rootDir: __dirname,
37 | npmRootDir: __dirname
38 | };
39 |
40 | assert.deepEqual(actual, expected);
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/test/data/.gitignore:
--------------------------------------------------------------------------------
1 | temp
2 |
--------------------------------------------------------------------------------
/test/data/fpw_email_mock.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | html: '[test string] Resetting your [ another test string ] password on localhost \n' +
3 | 'Hello cy_admin,
\n' +
4 | 'You are receiving this email because a request to reset your password was made on localhost.
\n' +
5 | 'If that is your wish, please follow this link to complete the password reset process:
\n' +
6 | 'http://localhost:3000/login?reset=clh0f780t0009ezh97wtu03d0&email=admin%40example.com
\n' +
7 | '\n' +
8 | ' If you did not request to reset your password, please delete and ignore this email. Someone else may have entered\n' +
9 | ' your email address in error.\n' +
10 | '
\n',
11 | text: '[test string] RESETTING YOUR [ another test string ] PASSWORD ON LOCALHOST\n' +
12 | '\n' +
13 | 'Hello cy_admin,\n' +
14 | '\n' +
15 | 'You are receiving this email because a request to reset your password was made\n' +
16 | 'on localhost.\n' +
17 | '\n' +
18 | 'If that is your wish, please follow this link to complete the password reset\n' +
19 | 'process:\n' +
20 | '\n' +
21 | 'http://localhost:3000/login?reset=clh0f780t0009ezh97wtu03d0&email=admin%40example.com\n' +
22 | // Improved link formatting via hideLinkHrefIfSameAsText option
23 | // '[http://localhost:3000/login?reset=clh0f780t0009ezh97wtu03d0&email=admin%40example.com]\n' +
24 | '\n' +
25 | 'If you did not request to reset your password, please delete and ignore this\n' +
26 | 'email. Someone else may have entered your email address in error.',
27 | from: 'noreply@example.com',
28 | to: 'admin@example.com',
29 | subject: 'Your request to reset your password on localhost'
30 | };
31 |
--------------------------------------------------------------------------------
/test/data/local.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | overrideTest: 'foo'
3 | };
4 |
--------------------------------------------------------------------------------
/test/data/local_fn.js:
--------------------------------------------------------------------------------
1 | module.exports = function(apos) {
2 | return {
3 | overrideTest: 'foo'
4 | };
5 | };
6 |
--------------------------------------------------------------------------------
/test/data/local_fn_b.js:
--------------------------------------------------------------------------------
1 | module.exports = function(apos, config) {
2 | config.overrideTest += '-foo';
3 | };
4 |
--------------------------------------------------------------------------------
/test/data/upload_tests/bad_test.exe:
--------------------------------------------------------------------------------
1 | foobar
--------------------------------------------------------------------------------
/test/data/upload_tests/clone.txt:
--------------------------------------------------------------------------------
1 | We know that a leadless flat's tennis comes with it the thought that the spleenish cupcake is an insect. A roof is the Saturday of a radio. A stopwatch is a maudlin november. An arrhythmic bottom's gymnast comes with it the thought that the dingy cancer is a bean. The thornless quiver comes from a soundless sense.
--------------------------------------------------------------------------------
/test/data/upload_tests/crop_image.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apostrophecms/apostrophe/8d77d62a4d0f0cd47c76428f530dff15aac79053/test/data/upload_tests/crop_image.jpeg
--------------------------------------------------------------------------------
/test/data/upload_tests/crop_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apostrophecms/apostrophe/8d77d62a4d0f0cd47c76428f530dff15aac79053/test/data/upload_tests/crop_image.png
--------------------------------------------------------------------------------
/test/data/upload_tests/tiny.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apostrophecms/apostrophe/8d77d62a4d0f0cd47c76428f530dff15aac79053/test/data/upload_tests/tiny.mp4
--------------------------------------------------------------------------------
/test/data/upload_tests/updateTrash_apos_api.txt:
--------------------------------------------------------------------------------
1 | I am sitting in a room.
2 |
--------------------------------------------------------------------------------
/test/data/upload_tests/upload_apos_api.txt:
--------------------------------------------------------------------------------
1 | I am sitting in a room.
2 |
--------------------------------------------------------------------------------
/test/data/upload_tests/upload_express_api.txt:
--------------------------------------------------------------------------------
1 | I am sitting in a room.
2 |
--------------------------------------------------------------------------------
/test/data/upload_tests/upload_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apostrophecms/apostrophe/8d77d62a4d0f0cd47c76428f530dff15aac79053/test/data/upload_tests/upload_image.png
--------------------------------------------------------------------------------
/test/db.js:
--------------------------------------------------------------------------------
1 | const t = require('../test-lib/test.js');
2 | const assert = require('assert');
3 |
4 | describe('Db', function() {
5 |
6 | let apos, apos2;
7 |
8 | after(async function () {
9 | await t.destroy(apos);
10 | await t.destroy(apos2);
11 | });
12 |
13 | this.timeout(t.timeout);
14 |
15 | it('should exist on the apos object', async function() {
16 | apos = await t.create({
17 | root: module
18 | });
19 |
20 | assert(apos.db);
21 | // Verify a normal, boring connection to localhost without the db option
22 | // worked
23 | const doc = await apos.doc.db.findOne();
24 |
25 | assert(doc);
26 | });
27 |
28 | it('should be able to launch a second instance reusing the connection', async function() {
29 | // Often takes too long otherwise
30 | this.timeout(10000);
31 | apos2 = await t.create({
32 | root: module,
33 | modules: {
34 | '@apostrophecms/db': {
35 | options: {
36 | client: apos.dbClient,
37 | uri: 'mongodb://this-will-not-work-unless-db-successfully-overrides-it/fail'
38 | }
39 | }
40 | }
41 | });
42 |
43 | const doc = await apos2.doc.db.findOne();
44 |
45 | assert(doc);
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/test/esm-project/app.js:
--------------------------------------------------------------------------------
1 | export default {
2 | root: import.meta,
3 | shortName: 'esm-project',
4 | baseUrl: 'http://localhost:3000',
5 | modules: {
6 | '@apostrophecms/express': {
7 | options: {
8 | address: '127.0.0.1'
9 | }
10 | },
11 | '@apostrophecms/sitemap': {}
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/test/esm-project/esm.js:
--------------------------------------------------------------------------------
1 | import { strict as assert } from 'node:assert';
2 | import path from 'node:path';
3 | import url from 'node:url';
4 | import util from 'node:util';
5 | import { exec } from 'node:child_process';
6 | import t from '../../test-lib/test.js';
7 | import app from './app.js';
8 |
9 | describe('Apostrophe ESM', function() {
10 | this.timeout(t.timeout);
11 |
12 | let apos;
13 |
14 | before(async function() {
15 | await util.promisify(exec)('npm install', { cwd: path.resolve(process.cwd(), 'test/esm-project') });
16 |
17 | apos = await t.create({
18 | ...app,
19 | root: import.meta
20 | });
21 | });
22 |
23 | after(function() {
24 | return t.destroy(apos);
25 | });
26 |
27 | it('should have root, rootDir, npmRootDir', function() {
28 | const actual = {
29 | root: {
30 | ...apos.root,
31 | filename: apos.root.filename,
32 | import: apos.root.import.toString(),
33 | require: apos.root.require.toString()
34 | },
35 | rootDir: apos.rootDir,
36 | npmRootDir: apos.npmRootDir
37 | };
38 | const expected = {
39 | root: {
40 | filename: url.fileURLToPath(import.meta.url),
41 | import: actual.root.import.toString(),
42 | require: actual.root.require.toString()
43 | },
44 | rootDir: path.dirname(url.fileURLToPath(import.meta.url)),
45 | npmRootDir: path.dirname(url.fileURLToPath(import.meta.url))
46 | };
47 |
48 | assert.deepEqual(actual, expected);
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/test/esm-project/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "esm-project",
3 | "type": "module",
4 | "version": "1.0.0",
5 | "description": "",
6 | "main": "app.js",
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {
14 | "apostrophe": "file:../../."
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/test/external-front.js:
--------------------------------------------------------------------------------
1 | const t = require('../test-lib/test.js');
2 | const assert = require('assert');
3 |
4 | describe('External Front', function() {
5 |
6 | let apos;
7 | // Set env var so these tests work even if you have a dev key in your bashrc
8 | // etc.
9 | process.env.APOS_EXTERNAL_FRONT_KEY = 'this is a test external front key';
10 |
11 | this.timeout(t.timeout);
12 |
13 | after(function() {
14 | return t.destroy(apos);
15 | });
16 |
17 | it('apostrophe should initialize normally', async function() {
18 | apos = await t.create({
19 | root: module
20 | });
21 |
22 | assert(apos.page.__meta.name === '@apostrophecms/page');
23 | });
24 |
25 | it('fetch home with external front', async function() {
26 | const data = await await apos.http.get('/', {
27 | headers: {
28 | 'x-requested-with': 'AposExternalFront',
29 | 'apos-external-front-key': process.env.APOS_EXTERNAL_FRONT_KEY
30 | }
31 | });
32 | assert.strictEqual(typeof data, 'object');
33 | assert(data.page);
34 | assert(data.home);
35 | assert(data.page.slug === data.home.slug);
36 | assert(data.page.slug === '/');
37 | });
38 |
39 | it('fetch home normally', async function() {
40 | const data = await await apos.http.get('/', {});
41 | assert.strictEqual(typeof data, 'string');
42 | assert(data.includes('Home Page Template'));
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/test/extra_node_modules/@company/bundle/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extend: '@apostrophecms/page-type',
3 | webpack: {
4 | bundles: {
5 | company: {}
6 | }
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/test/extra_node_modules/@company/bundle/ui/src/company.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | export default () => {
3 | console.log('BUNDLE_COMPANY');
4 | };
5 |
--------------------------------------------------------------------------------
/test/extra_node_modules/@company/bundle/ui/src/company.scss:
--------------------------------------------------------------------------------
1 | .company {
2 | font-weight: bold;
3 | }
4 |
--------------------------------------------------------------------------------
/test/extra_node_modules/before-global/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extend: '@apostrophecms/piece-type',
3 | before: '@apostrophecms/global'
4 | };
5 |
--------------------------------------------------------------------------------
/test/extra_node_modules/improve-global/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | improve: '@apostrophecms/global',
3 | options: {
4 | testGlobalLevelLoaded: true,
5 | testGlobalLevel: true
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/test/extra_node_modules/improve-piece-type/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | improve: '@apostrophecms/piece-type',
3 | options: {
4 | testPieceTypeLevelLoaded: true,
5 | testPieceTypeLevel: true
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/test/global.js:
--------------------------------------------------------------------------------
1 | const t = require('../test-lib/test.js');
2 | const assert = require('assert');
3 |
4 | describe('Global', function() {
5 |
6 | let apos;
7 |
8 | this.timeout(t.timeout);
9 |
10 | after(function() {
11 | return t.destroy(apos);
12 | });
13 |
14 | it('global should exist on the apos object', async function() {
15 | apos = await t.create({
16 | root: module,
17 | modules: {
18 | '@apostrophecms/global': {
19 | fields: {
20 | add: {
21 | spiffiness: {
22 | type: 'integer',
23 | def: 100
24 | }
25 | }
26 | }
27 | },
28 | 'global-tests': {
29 | apiRoutes(self) {
30 | return {
31 | get: {
32 | test(req) {
33 | return req.data.global.test;
34 | }
35 | }
36 | };
37 | }
38 | }
39 | }
40 | });
41 | });
42 |
43 | it('should be able to add a test property', async function() {
44 | return apos.doc.db.updateOne({
45 | slug: 'global',
46 | aposLocale: 'en:published'
47 | }, {
48 | $set: {
49 | test: 'test'
50 | }
51 | });
52 | });
53 |
54 | it('should populate when global.addGlobalToData is awaited', async function() {
55 | const req = apos.task.getAnonReq();
56 | await apos.global.addGlobalToData(req);
57 | assert(req.data.global);
58 | assert(req.data.global.type === '@apostrophecms/global');
59 | assert(req.data.global.test === 'test');
60 | // def is respected
61 | assert(req.data.global.spiffiness === 100);
62 | });
63 |
64 | it('should populate via middleware', async function() {
65 | const body = await apos.http.get('/api/v1/global-tests/test');
66 | assert(body === 'test');
67 | });
68 |
69 | });
70 |
--------------------------------------------------------------------------------
/test/http.js:
--------------------------------------------------------------------------------
1 | const t = require('../test-lib/test.js');
2 | const assert = require('assert');
3 |
4 | describe('Http', function() {
5 |
6 | let apos;
7 | let jar;
8 |
9 | after(async function () {
10 | return t.destroy(apos);
11 | });
12 |
13 | this.timeout(t.timeout);
14 |
15 | it('should exist on the apos object', async function() {
16 | apos = await t.create({
17 | root: module,
18 | modules: {
19 | test: {
20 | apiRoutes: (self) => ({
21 | post: {
22 | '/csrf-test': (req) => {
23 | return {
24 | ok: true
25 | };
26 | }
27 | }
28 | })
29 | }
30 | }
31 | });
32 |
33 | assert(apos.http);
34 | jar = apos.http.jar();
35 | });
36 |
37 | it('should be able to make an http request', async function() {
38 | const result = await apos.http.get('/', {
39 | jar
40 | });
41 | assert(result);
42 | assert(result.match(/logged out/));
43 | });
44 |
45 | it('should be able to make an http POST request with csrf header via default csrf convenience of http.post', async function() {
46 | const response = await apos.http.post('/csrf-test', {
47 | jar,
48 | body: {}
49 | });
50 | assert(response.ok === true);
51 | });
52 |
53 | });
54 |
--------------------------------------------------------------------------------
/test/improve-overrides.js:
--------------------------------------------------------------------------------
1 | const t = require('../test-lib/test.js');
2 | const assert = require('assert');
3 |
4 | describe('Improve Overrides', function() {
5 |
6 | this.timeout(t.timeout);
7 |
8 | it('"improve" should work, but project level should override it', async function() {
9 | let apos;
10 | try {
11 | apos = await t.create({
12 | root: module,
13 | modules: {
14 | 'improve-piece-type': {},
15 | 'improve-global': {}
16 | }
17 | });
18 | assert(apos.global.options.verifyProjectLevelLoaded);
19 | assert.strictEqual(apos.user.options.testPieceTypeLevelLoaded, true);
20 | assert.strictEqual(apos.user.options.testPieceTypeLevel, true);
21 | assert.strictEqual(apos.global.options.testPieceTypeLevelLoaded, true);
22 | assert.strictEqual(apos.global.options.testPieceTypeLevel, false);
23 | assert.strictEqual(apos.global.options.testGlobalLevelLoaded, true);
24 | assert.strictEqual(apos.global.options.testGlobalLevel, false);
25 | } finally {
26 | t.destroy(apos);
27 | }
28 | });
29 |
30 | });
31 |
--------------------------------------------------------------------------------
/test/launder.js:
--------------------------------------------------------------------------------
1 | const t = require('../test-lib/test.js');
2 | const assert = require('assert');
3 |
4 | describe('Launder', function() {
5 |
6 | this.timeout(t.timeout);
7 |
8 | after(function() {
9 | return t.destroy(apos);
10 | });
11 |
12 | let apos;
13 |
14 | it('should exist on the apos object', async function() {
15 | apos = await t.create({
16 | root: module
17 | });
18 |
19 | assert(apos.launder);
20 | });
21 |
22 | // Launder has plenty of unit tests of its own. All we're
23 | // doing here is a sanity test that we're really
24 | // hooked up to launder.
25 |
26 | it('should launder a number to a string', function() {
27 | assert(apos.launder.string(5) === '5');
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/test/modules/@apostrophecms/global/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | options: {
3 | verifyProjectLevelLoaded: true,
4 | // Verify we can override these
5 | testPieceTypeLevel: false,
6 | testGlobalLevel: false
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/test/modules/@apostrophecms/home-page/ui/src/main.js:
--------------------------------------------------------------------------------
1 | export default () => {
2 | window.TESTBED = {
3 | ...(window.TESTBED || {}),
4 | homePageMain: true
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/test/modules/@apostrophecms/home-page/ui/src/topic.js:
--------------------------------------------------------------------------------
1 | export default () => {
2 | window.TESTBED = {
3 | ...(window.TESTBED || {}),
4 | homePageTopic: true
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/test/modules/@apostrophecms/home-page/views/page.html:
--------------------------------------------------------------------------------
1 | {{ data.page.title }}
2 | Home Page Template
3 | {# This is necessary to the login.js tests. -Tom #}
4 | {% if data.user %}
5 | logged in as {{ data.user.title }}
6 | {% else %}
7 | logged out
8 | {% endif %}
9 | {# Necessary to the @apostrophecms/global tests. #}
10 | counts: {{ data.global.counts }}
11 | Translations
12 |
17 |
--------------------------------------------------------------------------------
/test/modules/@apostrophecms/module/index.js:
--------------------------------------------------------------------------------
1 | // Implicit subclass of @apostrophecms/module
2 | module.exports = {
3 | init(self) {
4 | // Set property
5 | // TODO: Probably remove this test option.
6 | self.color = 'blue';
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/test/modules/@apostrophecms/page/views/notFound.html:
--------------------------------------------------------------------------------
1 | {% extends '@apostrophecms/template:layout.html' %}
2 |
3 | {% block title %}Not Found{% endblock %}
4 |
5 | {% block main %}
6 | Home: {{ data.home.slug }}
7 |
8 | {% for tab in data.home._children %}
9 | Tab: {{ tab.slug }}
10 | {% endfor %}
11 |
12 | {% endblock %}
13 |
--------------------------------------------------------------------------------
/test/modules/@apostrophecms/search/views/index.html:
--------------------------------------------------------------------------------
1 | {{ data.docs | json }}
2 |
--------------------------------------------------------------------------------
/test/modules/@apostrophecms/template/views/included.html:
--------------------------------------------------------------------------------
1 | I am included
2 |
--------------------------------------------------------------------------------
/test/modules/@apostrophecms/template/views/layout.html:
--------------------------------------------------------------------------------
1 | {% extends data.outerLayout %}
2 |
3 | {% block main %}
4 | I am in the layout
5 | {% block inner %}{% endblock %}
6 | {% include "included.html" %}
7 | {% endblock %}
8 |
--------------------------------------------------------------------------------
/test/modules/@apostrophecms/template/views/refreshLayout.html:
--------------------------------------------------------------------------------
1 | {% block main %}{% endblock %}
2 |
--------------------------------------------------------------------------------
/test/modules/@apostrophecms/test-module-push/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | init(self) {
3 | // Set property
4 | self.color = 'red';
5 | // Attach to apos
6 | self.apos.test = self;
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/test/modules/@apostrophecms/test-module/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | init(self) {
3 | // Set property
4 | self.color = 'red';
5 |
6 | // Attach to apos
7 | self.apos.test = self;
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/test/modules/@apostrophecms/user/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | options: {
3 | // Accelerates unit tests while still testing all the same
4 | // functionality. Unsafe for other uses
5 | insecurePasswords: true
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/test/modules/@company/bundle/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | options: {
3 | label: 'Company bundle override'
4 | },
5 | webpack: {
6 | bundles: {
7 | company: {}
8 | }
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/test/modules/@company/bundle/ui/src/company.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | export default () => {
3 | console.log('BUNDLE_OVERRIDE_COMPANY');
4 | };
5 |
--------------------------------------------------------------------------------
/test/modules/@company/bundle/ui/src/company.scss:
--------------------------------------------------------------------------------
1 | .override-company {
2 | font-weight: bold;
3 | }
4 |
--------------------------------------------------------------------------------
/test/modules/apos-fr/i18n/fr.json:
--------------------------------------------------------------------------------
1 | {
2 | "richTextAlignCenter": "Aligner Le Centre"
3 | }
--------------------------------------------------------------------------------
/test/modules/args-bad-page/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extend: '@apostrophecms/page-type',
3 | options: {
4 | label: 'Bad Page'
5 | },
6 | fields: {
7 | add: {
8 | main: {
9 | type: 'area',
10 | label: 'Main',
11 | options: {
12 | widgets: {
13 | args: {}
14 | }
15 | }
16 | }
17 | }
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/test/modules/args-bad-page/views/page.html:
--------------------------------------------------------------------------------
1 | Bad args page
2 | {% area data.page, 'baddies', {
3 | 'args': [ '💔', 'illegal', 'outlaw' ]
4 | } %}
5 |
--------------------------------------------------------------------------------
/test/modules/args-good-page/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extend: '@apostrophecms/page-type',
3 | options: {
4 | label: 'Good Page'
5 | },
6 | fields: {
7 | add: {
8 | main: {
9 | type: 'area',
10 | label: 'Main',
11 | options: {
12 | widgets: {
13 | args: {}
14 | }
15 | }
16 | }
17 | }
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/test/modules/args-good-page/views/page.html:
--------------------------------------------------------------------------------
1 | Good args page
2 | {% area data.page, 'main' with {
3 | 'args': {
4 | color: '🟣'
5 | }
6 | } %}
7 |
--------------------------------------------------------------------------------
/test/modules/args-widget/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extend: '@apostrophecms/widget-type',
3 | options: {
4 | label: 'Arguments Testing Widget'
5 | },
6 | fields: {
7 | add: {
8 | snippet: {
9 | type: 'string',
10 | label: 'Snippet'
11 | }
12 | }
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/test/modules/args-widget/views/widget.html:
--------------------------------------------------------------------------------
1 | Arguments test widget
2 | {{ data.widget.snippet }}
3 |
4 | {% for key, val in data.contextOptions %}
5 | {{ key }}: {{ val }}
6 | {% endfor %}
7 |
8 |
--------------------------------------------------------------------------------
/test/modules/article-page/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extend: '@apostrophecms/piece-page-type',
3 | init() { }
4 | };
5 |
--------------------------------------------------------------------------------
/test/modules/article-page/ui/src/index.js:
--------------------------------------------------------------------------------
1 | export default () => {
2 | window.TESTBED = {
3 | ...(window.TESTBED || {}),
4 | articlePageIndex: true
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/test/modules/article-page/ui/src/main.js:
--------------------------------------------------------------------------------
1 | export default () => {
2 | window.TESTBED = {
3 | ...(window.TESTBED || {}),
4 | articlePageMain: true
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/test/modules/article-page/ui/src/main.scss:
--------------------------------------------------------------------------------
1 | .test-article-page-main {
2 | font-weight: 300;
3 | }
4 |
--------------------------------------------------------------------------------
/test/modules/article-widget/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extend: '@apostrophecms/widget-type',
3 | init() { }
4 | };
5 |
--------------------------------------------------------------------------------
/test/modules/article-widget/ui/src/carousel.js:
--------------------------------------------------------------------------------
1 | export default () => {
2 | window.TESTBED = {
3 | ...(window.TESTBED || {}),
4 | articleWidgetCarousel: true
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/test/modules/article-widget/ui/src/carousel.scss:
--------------------------------------------------------------------------------
1 | .test-carousel {
2 | font-weight: bold;
3 | }
4 |
5 |
--------------------------------------------------------------------------------
/test/modules/article-widget/ui/src/topic.js:
--------------------------------------------------------------------------------
1 | export default () => {
2 | window.TESTBED = {
3 | ...(window.TESTBED || {}),
4 | articleWidgetTopic: true
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/test/modules/base-type/i18n/custom/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "customTestOne": "Custom Test One From Base Type",
3 | "customTestTwo": "Custom Test Two From Base Type"
4 | }
5 |
--------------------------------------------------------------------------------
/test/modules/base-type/i18n/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultTestOne": "Default Test One"
3 | }
4 |
--------------------------------------------------------------------------------
/test/modules/bundle-edge/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | options: {
3 | label: 'Bundle edge case test'
4 | },
5 | webpack: {
6 | bundles: {
7 | edge: {}
8 | }
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/test/modules/bundle-edge/ui/src/edge.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | export default () => {
3 | console.log('BUNDLE_EDGE');
4 | };
5 |
--------------------------------------------------------------------------------
/test/modules/bundle-edge/ui/src/edge.scss:
--------------------------------------------------------------------------------
1 | .edge {
2 | font-weight: bold;
3 | }
4 |
--------------------------------------------------------------------------------
/test/modules/bundle-page-type/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extend: '@apostrophecms/piece-page-type',
3 | webpack: {
4 | bundles: {
5 | main: {},
6 | another: {}
7 | }
8 | },
9 | options: {
10 | label: 'Bundle base page type'
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/test/modules/bundle-page-type/ui/src/another.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | export default () => {
3 | console.log('BUNDLE_ANOTHER_PAGE_TYPE');
4 | };
5 |
--------------------------------------------------------------------------------
/test/modules/bundle-page-type/ui/src/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | export default () => {
3 | console.log('BUNDLE_INDEX_PAGE_TYPE');
4 | };
5 |
--------------------------------------------------------------------------------
/test/modules/bundle-page-type/ui/src/index.scss:
--------------------------------------------------------------------------------
1 | .index-page-type {
2 | font-weight: bold;
3 | }
4 |
--------------------------------------------------------------------------------
/test/modules/bundle-page-type/ui/src/main.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | export default () => {
3 | console.log('BUNDLE_MAIN_PAGE_TYPE');
4 | };
5 |
--------------------------------------------------------------------------------
/test/modules/bundle-page-type/ui/src/main.scss:
--------------------------------------------------------------------------------
1 | .main-page-type {
2 | font-weight: bold;
3 | }
4 |
--------------------------------------------------------------------------------
/test/modules/bundle-page/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extend: 'bundle-page-type',
3 | webpack: {
4 | bundles: {
5 | main: {},
6 | extra: {
7 | templates: [ 'show' ]
8 | }
9 | },
10 | extensions: {
11 | ext1: {
12 | resolve: {
13 | alias: {
14 | ext1: 'foo-path'
15 | }
16 | }
17 | }
18 | }
19 | },
20 | fields: {
21 | add: {
22 | main: {
23 | type: 'area',
24 | contextual: true,
25 | options: {
26 | widgets: {
27 | bundle: {}
28 | }
29 | }
30 | }
31 | }
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/test/modules/bundle-page/ui/src/extra.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | export default () => {
3 | console.log('BUNDLE_EXTRA_PAGE');
4 | };
5 |
--------------------------------------------------------------------------------
/test/modules/bundle-page/ui/src/extra.scss:
--------------------------------------------------------------------------------
1 | .extra-page {
2 | font-weight: bold;
3 | }
4 |
--------------------------------------------------------------------------------
/test/modules/bundle-page/ui/src/main.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | export default () => {
3 | console.log('BUNDLE_MAIN_PAGE');
4 | };
5 |
--------------------------------------------------------------------------------
/test/modules/bundle-page/ui/src/main.scss:
--------------------------------------------------------------------------------
1 | .main-page {
2 | font-weight: bold;
3 | }
4 |
--------------------------------------------------------------------------------
/test/modules/bundle-page/views/index.html:
--------------------------------------------------------------------------------
1 | {% extends data.outerLayout %}
2 |
3 | {% block main %}
4 |
5 |
My Index Page
6 | {% area data.page, 'main' %}
7 |
8 | {% endblock %}
9 |
10 |
--------------------------------------------------------------------------------
/test/modules/bundle-page/views/show.html:
--------------------------------------------------------------------------------
1 | {% extends data.outerLayout %}
2 |
3 | {% block main %}
4 |
5 |
6 |
My Show Page
7 |
8 |
9 | {% endblock %}
10 |
11 |
--------------------------------------------------------------------------------
/test/modules/bundle-widget/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extend: '@apostrophecms/widget-type',
3 | options: {
4 | label: 'Bundle Widget'
5 | },
6 | webpack: {
7 | bundles: {
8 | extra2: {}
9 | },
10 | extensions: {
11 | ext1: {
12 | resolve: {
13 | alias: {
14 | ext1Overriden: 'bar-path'
15 | }
16 | }
17 | },
18 | ext2: {
19 | resolve: {
20 | alias: {
21 | ext2: 'ext2-path'
22 | }
23 | }
24 | }
25 | }
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/test/modules/bundle-widget/ui/src/extra2.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | export default () => {
3 | console.log('BUNDLE_WIDGET_EXTRA2');
4 | };
5 |
--------------------------------------------------------------------------------
/test/modules/bundle-widget/views/widget.html:
--------------------------------------------------------------------------------
1 | Bundle Widget
2 |
--------------------------------------------------------------------------------
/test/modules/bundle/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extend: '@apostrophecms/piece-type',
3 | options: {
4 | label: 'Bundles piece',
5 | pluralLabel: 'Bundles Pieces'
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/test/modules/default-page/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extend: '@apostrophecms/page-type'
3 | };
4 |
--------------------------------------------------------------------------------
/test/modules/default-page/ui/apos/components/FakeComponent.vue:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/modules/default-page/ui/public/index.css:
--------------------------------------------------------------------------------
1 | .default-page {color:red;}
2 |
--------------------------------------------------------------------------------
/test/modules/default-page/ui/public/index.js:
--------------------------------------------------------------------------------
1 | export default () => {};
2 |
--------------------------------------------------------------------------------
/test/modules/default-page/ui/src/index.js:
--------------------------------------------------------------------------------
1 | export default () => {};
2 |
--------------------------------------------------------------------------------
/test/modules/default-page/ui/src/index.scss:
--------------------------------------------------------------------------------
1 | .default-page {color:red;}
2 |
--------------------------------------------------------------------------------
/test/modules/default-page/views/page.html:
--------------------------------------------------------------------------------
1 | {{ data.page.title }}
2 | Default Page Template
3 |
4 | Home: {{ data.home.slug }}
5 |
6 | {% for tab in data.home._children %}
7 | Tab: {{ tab.slug }}
8 | {% endfor %}
9 |
10 | {% if data.page.body %}
11 | {% area data.page, 'body' %}
12 | {% endif %}
13 |
14 | Translations
15 |
20 |
--------------------------------------------------------------------------------
/test/modules/email-test/views/welcome.html:
--------------------------------------------------------------------------------
1 | Welcome!
2 |
3 | Welcome to our site, {{ data.name }}!
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/test/modules/event-widget/views/widget.html:
--------------------------------------------------------------------------------
1 | {% for piece in data.widget._featured %}
2 |
7 | {% endfor %}
8 |
9 | {% for piece in data.widget._pieces %}
10 |
15 | {% endfor %}
16 |
--------------------------------------------------------------------------------
/test/modules/example/i18n/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "projectLevelPhrase": "Project Level Phrase"
3 | }
--------------------------------------------------------------------------------
/test/modules/express-test/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | routes(self) {
3 | return {
4 | get: {
5 | '/tests/welcome': (req, res) => {
6 | res.send('ok');
7 | }
8 | },
9 | post: {
10 | '/tests/body': (req, res) => {
11 | res.send(req.body.person.age);
12 | }
13 | }
14 | };
15 | }
16 | };
17 |
--------------------------------------------------------------------------------
/test/modules/fragment-all/views/aux-test.html:
--------------------------------------------------------------------------------
1 | {% extends data.outerLayout %}
2 |
3 | {% import "fragment.html" as fragment %}
4 |
5 | {% block main %}
6 | {% render fragment.auxTest('Gee Whiz') %}
7 | {% endblock %}
8 |
--------------------------------------------------------------------------------
/test/modules/fragment-all/views/fragment-print.html:
--------------------------------------------------------------------------------
1 | {% fragment print(text) %}
2 | {{ text }}
3 | {% endfragment %}
--------------------------------------------------------------------------------
/test/modules/fragment-all/views/fragment.html:
--------------------------------------------------------------------------------
1 | {% import "fragment-print.html" as f %}
2 | {% import "macro.html" as m %}
3 |
4 | {% fragment test(pos1, pos2, kw1 = 'kw1_default', kw2 = 'kw2_default', pos3, kw3 = 'kw3_default') %}
5 | {{ pos1 }}
6 | {{ pos2 }}
7 | {{ pos3 }}
8 | {{ rendercaller() }}
9 | {% render _print(kw1) %}
10 | {% render f.print(kw2) %}
11 | {{ m.print(kw3) }}
12 | {% endfragment %}
13 |
14 | {% fragment listNumbers(first, second, third = 3, fourth = 4) %}
15 | val_{{ first }}
16 | val_{{ second }}
17 | val_{{ third }}
18 | val_{{ fourth }}
19 | {% endfragment %}
20 |
21 | {% fragment printNumbers(number, one = 1, two = 2) %}
22 | val_{{ number }}
23 | val_{{ one }}
24 | val_{{ two }}
25 | val_{{ three }}
26 | {% endfragment %}
27 |
28 | {% fragment _print(text) %}
29 | {{ text }}
30 | {% endfragment %}
31 |
32 | {% fragment auxTest(s) %}
33 | {{ apos.util.slugify(s) }}
34 | {{ __t('apostrophe:modifyOrDelete') }}
35 | {% endfragment %}
36 |
--------------------------------------------------------------------------------
/test/modules/fragment-all/views/macro.html:
--------------------------------------------------------------------------------
1 | {% macro print(text) %}
2 | {{ text }}
3 | {% endmacro %}
--------------------------------------------------------------------------------
/test/modules/fragment-all/views/page.html:
--------------------------------------------------------------------------------
1 | {% extends data.outerLayout %}
2 |
3 | {% import "fragment.html" as fragment %}
4 |
5 | {% block main %}
6 | --test1--
7 | {% render fragment.test('pos1', 'pos2') %}
8 | --endtest1--
9 |
10 |
11 | --test2--
12 | Above Fragment
13 |
14 | {% render fragment.test('pos1', 'pos2', kw2 = 'kw2', 'pos3') %}
15 |
16 | Below Fragment
17 | --endtest2--
18 |
19 |
20 | --test3--
21 | Above Call Fragment
22 |
23 | {% rendercall fragment.test('pos1', 'pos2', kw2 = 'kw2', 'pos3') %}
24 | Start Call Body
25 | {% component "fragment-page:test" with { text: 'called' } %}
26 | End Call Body
27 | {% endrendercall %}
28 |
29 | Below Call Fragment
30 | --endtest3--
31 |
32 |
33 | --issue_3056_1--
34 | {% render fragment.listNumbers(1, third = 9) %}
35 | --endissue_3056_1--
36 |
37 | --issue_3056_2--
38 | {% render fragment.listNumbers(third = 9, 1) %}
39 | --endissue_3056_2--
40 |
41 | --issue_3102--
42 | {% render fragment.printNumbers(three = 3) %}
43 | --endissue_3102--
44 |
45 | {% endblock %}
--------------------------------------------------------------------------------
/test/modules/fragment-all/views/test.html:
--------------------------------------------------------------------------------
1 | Text is {{ data.text }}
2 | {% if data.afterDelay %}
3 | After Delay
4 | {% endif %}
5 |
--------------------------------------------------------------------------------
/test/modules/fragment-page/views/fragment.html:
--------------------------------------------------------------------------------
1 | {% fragment test(input) %}
2 | {{ input.before }}
3 | {% component "fragment-page:test" with input.component %}
4 | {{ input.after }}
5 | {% endfragment %}
6 |
--------------------------------------------------------------------------------
/test/modules/fragment-page/views/page.html:
--------------------------------------------------------------------------------
1 | {% extends data.outerLayout %}
2 |
3 | {% import "fragment.html" as fragment %}
4 |
5 | {% block main %}
6 | Above Fragment
7 |
8 | {% render fragment.test({
9 | before: 'Before Component',
10 | after: 'After Component',
11 | component: {
12 | text: 'Component Text'
13 | }
14 | }) %}
15 |
16 | Below Fragment
17 | {% endblock %}
--------------------------------------------------------------------------------
/test/modules/fragment-page/views/test.html:
--------------------------------------------------------------------------------
1 | Text is {{ data.text }}
2 | {% if data.afterDelay %}
3 | After Delay
4 | {% endif %}
5 |
--------------------------------------------------------------------------------
/test/modules/i18n-test-page/views/page.html:
--------------------------------------------------------------------------------
1 | {% extends data.outerLayout %}
2 |
3 | Test page for i18n tests
4 |
--------------------------------------------------------------------------------
/test/modules/inject-test/views/appendBodyTest.html:
--------------------------------------------------------------------------------
1 | append-body-test
--------------------------------------------------------------------------------
/test/modules/inject-test/views/appendDevTest.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/modules/inject-test/views/appendDevViteTest.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/modules/inject-test/views/appendDevWebpackTest.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/modules/inject-test/views/appendHeadTest.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/modules/inject-test/views/appendProdWebpackTest.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/modules/inject-test/views/prependDevTest.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/modules/inject-test/views/prependDevViteTest.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/modules/inject-test/views/prependDevWebpackTest.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/modules/inject-test/views/prependHeadTest.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/modules/inject-test/views/prependProdTest.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/modules/inject-test/views/prependViteTest.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/modules/inject-test/views/prependWebpackTest.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/modules/nested-module-subdirs/example1/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | init(self) {
3 | self.initialized = true;
4 | }
5 | };
6 |
--------------------------------------------------------------------------------
/test/modules/nested-module-subdirs/modules.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | example1: {
3 | options: {
4 | folderLevelOption: true
5 | }
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/test/modules/nifty-page/views/bar.html:
--------------------------------------------------------------------------------
1 | niftyPages-bar-template-rendered-this
--------------------------------------------------------------------------------
/test/modules/nifty-page/views/foo.html:
--------------------------------------------------------------------------------
1 | niftyPages-foo-template-rendered-this
--------------------------------------------------------------------------------
/test/modules/nifty-page/views/foo2.html:
--------------------------------------------------------------------------------
1 | niftyPages-foo2-template-rendered-this
--------------------------------------------------------------------------------
/test/modules/nifty-page/views/index.html:
--------------------------------------------------------------------------------
1 | niftyPages-index-template-rendered-this
--------------------------------------------------------------------------------
/test/modules/placeholder-page/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extend: '@apostrophecms/page-type',
3 | options: {
4 | label: 'Placeholder Test Page'
5 | },
6 | fields: {
7 | add: {
8 | main: {
9 | type: 'area',
10 | label: 'Main',
11 | options: {
12 | widgets: {
13 | placeholder: {},
14 | '@apostrophecms/image': {},
15 | '@apostrophecms/video': {}
16 | }
17 | }
18 | }
19 | }
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/test/modules/placeholder-page/views/page.html:
--------------------------------------------------------------------------------
1 | Placeholder Test Page
2 |
3 | {% area data.page, 'main' %}
4 |
--------------------------------------------------------------------------------
/test/modules/placeholder-widget/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extend: '@apostrophecms/widget-type',
3 | options: {
4 | label: 'Placeholder Test Widget',
5 | placeholder: true
6 | },
7 | fields: {
8 | add: {
9 | string: {
10 | type: 'string',
11 | label: 'String',
12 | placeholder: 'String PLACEHOLDER'
13 | },
14 | integer: {
15 | type: 'integer',
16 | label: 'Integer',
17 | placeholder: 0
18 | },
19 | float: {
20 | type: 'float',
21 | label: 'Float',
22 | placeholder: 0.1
23 | },
24 | date: {
25 | type: 'date',
26 | label: 'Date',
27 | placeholder: 'YYYY-MM-DD'
28 | },
29 | time: {
30 | type: 'time',
31 | label: 'Time',
32 | placeholder: 'HH:MM:SS'
33 | }
34 | }
35 | }
36 | };
37 |
--------------------------------------------------------------------------------
/test/modules/placeholder-widget/views/widget.html:
--------------------------------------------------------------------------------
1 |
2 | {% for key, val in data.widget %}
3 | {{ data.widget._id }} - {{ key }}: {{ val }}
4 | {% endfor %}
5 |
6 |
--------------------------------------------------------------------------------
/test/modules/recursion-test-page/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | components(self) {
3 | return {
4 | test(req, data) {
5 | return data;
6 | }
7 | };
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/test/modules/recursion-test-page/views/page.html:
--------------------------------------------------------------------------------
1 | Sing to me, Oh Muse.
2 |
3 | {% component "recursion-test-page:test" with { depth: 0 } %}
4 |
--------------------------------------------------------------------------------
/test/modules/recursion-test-page/views/test.html:
--------------------------------------------------------------------------------
1 | At depth {{ data.depth }}
2 | {% component "recursion-test-page:test" with { depth: data.depth + 1 } %}
3 |
--------------------------------------------------------------------------------
/test/modules/same-name-as-transitive-dependency/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | options: {
3 | alias: 'same',
4 | color: 'purple'
5 | },
6 | init(self) {
7 | self.color = self.options.color;
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/test/modules/selected-article-widget/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extend: '@apostrophecms/widget-type',
3 | init() { }
4 | };
5 |
--------------------------------------------------------------------------------
/test/modules/selected-article-widget/ui/src/tabs.js:
--------------------------------------------------------------------------------
1 | export default () => {
2 | window.TESTBED = {
3 | ...(window.TESTBED || {}),
4 | selectedArticleWidgetTabs: true
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/test/modules/subtype/i18n/custom/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "customTestOne": "Custom Test One From Subtype",
3 | "customTestThree": "Custom Test Three From Subtype"
4 | }
--------------------------------------------------------------------------------
/test/modules/subtype/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | i18n: {
3 | custom: {
4 | browser: true
5 | }
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/test/modules/template-options-test/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | options: {
3 | spiffiness: 'nifty'
4 | },
5 | init(self) {
6 | self.addHelpers({
7 | test(a) {
8 | return a * 2;
9 | }
10 | });
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/test/modules/template-options-test/views/options-test.html:
--------------------------------------------------------------------------------
1 | {{ module.options.spiffiness }}
2 | {{ module.test(2) }}
3 |
--------------------------------------------------------------------------------
/test/modules/template-subclass-test/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extend: 'template-test'
3 | };
4 |
--------------------------------------------------------------------------------
/test/modules/template-subclass-test/views/override-test.html:
--------------------------------------------------------------------------------
1 | I am overridden
2 |
--------------------------------------------------------------------------------
/test/modules/template-test/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | options: {
3 | templateData: {
4 | age: 30,
5 | multiline: 'first line\nsecond line\nCSRF attempt ',
6 | multilineSafe: 'first line\nsecond line\nThis is okay '
7 | }
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/test/modules/template-test/views/inherit-test.html:
--------------------------------------------------------------------------------
1 | I am inherited
2 |
--------------------------------------------------------------------------------
/test/modules/template-test/views/override-test.html:
--------------------------------------------------------------------------------
1 | I am a bug
2 |
--------------------------------------------------------------------------------
/test/modules/template-test/views/page.html:
--------------------------------------------------------------------------------
1 | {% extends data.outerLayout %}
2 |
3 | {% block pageTitle %}I am the title{% endblock %}
4 |
5 | {% block title %}I am the title{% endblock %}
6 |
7 | {% block main %}
8 | I am the main content
9 | {% endblock %}
10 |
--------------------------------------------------------------------------------
/test/modules/template-test/views/pageWithLayout.html:
--------------------------------------------------------------------------------
1 | {% extends "@apostrophecms/template:layout.html" %}
2 |
3 | {% block pageTitle %}I am the title{% endblock %}
4 |
5 | {% block title %}I am the title{% endblock %}
6 |
7 | {% block inner %}
8 | I am the inner content
9 | {% endblock %}
10 |
--------------------------------------------------------------------------------
/test/modules/template-test/views/test.html:
--------------------------------------------------------------------------------
1 | {{ data.age }}
2 |
--------------------------------------------------------------------------------
/test/modules/template-test/views/testWithNlbrFilter.html:
--------------------------------------------------------------------------------
1 | {{ data.multiline | nlbr }}
2 |
--------------------------------------------------------------------------------
/test/modules/template-test/views/testWithNlbrFilterSafe.html:
--------------------------------------------------------------------------------
1 | {{ data.multilineSafe | safe | nlbr }}
2 |
--------------------------------------------------------------------------------
/test/modules/template-test/views/testWithNlpFilter.html:
--------------------------------------------------------------------------------
1 | {{ data.multiline | nlp }}
--------------------------------------------------------------------------------
/test/modules/test-before/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | before: '@apostrophecms/image',
3 | init() {}
4 | };
5 |
--------------------------------------------------------------------------------
/test/modules/test-get-option-2/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | options: {
3 | yup: 'yup'
4 | }
5 | };
6 |
--------------------------------------------------------------------------------
/test/modules/test-get-option/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | options: {
3 | flavors: {
4 | grape: {
5 | sweetness: 20,
6 | ingredients: [ 'chemicals', 'ions', 'mysteries' ]
7 | }
8 | }
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/test/modules/test-get-option/views/test.html:
--------------------------------------------------------------------------------
1 | {# Get an option from the module that rendered the template #}
2 | {{ getOption('flavors.grape.sweetness') }}
3 | {# Get an option from a different module #}
4 | {{ getOption('test-get-option-2:yup') }}
5 |
--------------------------------------------------------------------------------
/test/modules/test-page/views/page.html:
--------------------------------------------------------------------------------
1 | {% extends data.outerLayout %}
2 |
3 | {% block main %}
4 | {{ data.page.title }}
5 |
6 | Sing to me, Oh Muse.
7 |
8 | Home: {{ data.home.slug }}
9 |
10 | {% for tab in data.home._children %}
11 | Tab: {{ tab.slug }}
12 | {% endfor %}
13 |
14 | URL: {{ data.page._url }}
15 | {% endblock %}
16 |
--------------------------------------------------------------------------------
/test/modules/with-layout-page/views/page.html:
--------------------------------------------------------------------------------
1 | {% extends data.outerLayout %}
2 |
3 | {% block extraHead %}
4 | {% component '@apostrophecms/template:inject' with { where: 'head', end: 'prepend', when: 'dev' } %}
5 |
6 | {% component '@apostrophecms/template:inject' with { where: 'head', end: 'append', when: 'dev' } %}
7 | {% endblock %}
8 |
9 | Test page with layout
10 |
--------------------------------------------------------------------------------
/test/oembed.js:
--------------------------------------------------------------------------------
1 | const t = require('../test-lib/test.js');
2 | const assert = require('assert');
3 |
4 | describe('Oembed', function() {
5 | this.timeout(t.timeout);
6 |
7 | let apos;
8 |
9 | after(function () {
10 | return t.destroy(apos);
11 | });
12 |
13 | /// ///
14 | // EXISTENCE
15 | /// ///
16 |
17 | it('should initialize', async function() {
18 | apos = await t.create({
19 | root: module
20 | });
21 |
22 | assert(apos.modules['@apostrophecms/oembed']);
23 | assert(apos.oembed.__meta.name === '@apostrophecms/oembed');
24 | });
25 |
26 | // TODO: test this with mocks. Travis CI erratically times out
27 | // when we test against real YouTube, which produces false
28 | // failures that lead us to ignore CI results.
29 | //
30 | // let youtube = 'https://www.youtube.com/watch?v=us00G8oILCM&feature=related';
31 |
32 | // it('YouTube still has the video we like to use for testing', async
33 | // function() { try { const response = await request({ method: 'GET', uri:
34 | // youtube, resolveWithFullResponse: true });
35 |
36 | // assert(response.statusCode === 200);
37 | // } catch (e) {
38 | // assert(false);
39 | // }
40 | // });
41 |
42 | // it('Should deliver an oembed response for YouTube', async function() {
43 | // const queryString = qs.stringify({ url: youtube });
44 | // const uri = `/modules/@apostrophecms/oembed/query?${queryString}`;
45 |
46 | // const response = await request({
47 | // uri,
48 | // method: 'GET',
49 | // resolveWithFullResponse: true
50 | // });
51 |
52 | // assert(response.statusCode === 200);
53 | // const data = JSON.parse(response.body);
54 | // assert(data.type === 'video');
55 | // });
56 | });
57 |
--------------------------------------------------------------------------------
/test/pieces-children/pieces-malformed-child.js:
--------------------------------------------------------------------------------
1 | const t = require('../../test-lib/test.js');
2 |
3 | const apiKey = 'this is a test api key';
4 |
5 | (async function () {
6 | await t.create({
7 | root: module,
8 |
9 | modules: {
10 | '@apostrophecms/express': {
11 | options: {
12 | apiKeys: {
13 | [apiKey]: {
14 | role: 'admin'
15 | }
16 | }
17 | }
18 | },
19 | malformed: {
20 | extend: '@apostrophecms/piece-type',
21 | fields: {
22 | add: {
23 | type: {
24 | label: 'Foo',
25 | type: 'string'
26 | }
27 | }
28 | }
29 | }
30 | }
31 | });
32 | })();
33 |
--------------------------------------------------------------------------------
/test/pieces-malformed.js:
--------------------------------------------------------------------------------
1 | const t = require('../test-lib/test.js');
2 | const { spawn } = require('child_process');
3 | const assert = require('assert');
4 |
5 | describe('Malformed Pieces', function () {
6 | this.timeout(t.timeout);
7 | it('should fail to initialize with a schema containing a field named "type"', function (done) {
8 | let throwsError = true;
9 | const mochaProcess = spawn('node', [ './test/pieces-children/pieces-malformed-child.js' ]);
10 |
11 | mochaProcess.stderr.on('data', (data) => {
12 | const errorMsg = data.toString();
13 | const errorMatch = errorMsg.match(/(?Error:.*\n)/);
14 | if (errorMatch) {
15 | throwsError = true;
16 | assert.equal(errorMatch.groups.error, 'Error: The malformed module contains a forbidden field property name: "type".\n');
17 | } else {
18 | throwsError = false;
19 | }
20 | });
21 |
22 | mochaProcess.on('close', (code) => {
23 | assert.equal(code, 1, 'Mocha process exited with status code 0');
24 | assert.ok(throwsError, 'Error message not found in stderr');
25 | done();
26 | });
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/test/public/static-test.txt:
--------------------------------------------------------------------------------
1 | served
2 |
--------------------------------------------------------------------------------
/test/public/test-image-landscape.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apostrophecms/apostrophe/8d77d62a4d0f0cd47c76428f530dff15aac79053/test/public/test-image-landscape.jpg
--------------------------------------------------------------------------------
/test/public/test-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apostrophecms/apostrophe/8d77d62a4d0f0cd47c76428f530dff15aac79053/test/public/test-image.jpg
--------------------------------------------------------------------------------
/test/subdir-project.js:
--------------------------------------------------------------------------------
1 | const t = require('../test-lib/test.js');
2 | const assert = require('assert');
3 |
4 | describe('Project with package.json in its parent folder works', function() {
5 |
6 | this.timeout(t.timeout);
7 |
8 | /// ///
9 | // EXISTENCE
10 | /// ///
11 |
12 | it('should allow a project relying on a package.json in its parent folder', async function() {
13 | let apos;
14 | try {
15 | apos = await t.create(require('./subdir-project/app.js'));
16 | // Sniff test: a normal apos object
17 | assert(apos.user);
18 | } finally {
19 | if (apos) {
20 | await t.destroy(apos);
21 | }
22 | }
23 | assert(apos);
24 | });
25 |
26 | });
27 |
--------------------------------------------------------------------------------
/test/subdir-project/app.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: module
3 | };
4 |
--------------------------------------------------------------------------------
/test/test-bundle/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | bundle: {
3 | modules: [ 'test-bundle-sub' ],
4 | directory: 'modules'
5 | },
6 | init(self) {
7 | // Set property
8 | self.color = 'red';
9 |
10 | // Attach to apos
11 | self.apos.test = self;
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/test/test-bundle/modules/test-bundle-sub/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | init(self) {
3 | // Set property
4 | self.color = 'red';
5 |
6 | // Attach to apos
7 | self.apos.subtest = self;
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/test/test.html:
--------------------------------------------------------------------------------
1 | // in modules/custom-twitter-bridge/index.js
2 |
3 | components(self) {
4 | return {
5 | async feed(data) {
6 | return {
7 | // In real life you would definitely want to cache this and
8 | // use the real official Twitter APIs to get the data
9 | feed: await request(`https://some-twitter-api?${qs.stringify({ username: data.username })}`)
10 | }
11 | }
12 | };
13 | }
14 |
15 | {# In modules/custom-twitter-bridge/views/feed.html #}
16 |
17 | {% for tweet in data.feed.tweets %}
18 | {{ tweet.text }}
19 | {% endfor %}
20 |
21 | {# Now we can invoke our component to pull a twitter feed into ANY template #}
22 | {% component 'custom-twitter-bridge:feed' with { username: 'cooltweeter' } %}
--------------------------------------------------------------------------------
/test/widgets-children/widgets-malformed-child.js:
--------------------------------------------------------------------------------
1 | const t = require('../../test-lib/test.js');
2 |
3 | const apiKey = 'this is a test api key';
4 |
5 | (async function () {
6 | await t.create({
7 | root: module,
8 |
9 | modules: {
10 | '@apostrophecms/express': {
11 | options: {
12 | apiKeys: {
13 | [apiKey]: {
14 | role: 'admin'
15 | }
16 | }
17 | }
18 | },
19 | malformed: {
20 | extend: '@apostrophecms/widget-type',
21 | fields: {
22 | add: {
23 | type: {
24 | label: 'Foo',
25 | type: 'string'
26 | }
27 | }
28 | }
29 | }
30 | }
31 | });
32 | })();
33 |
--------------------------------------------------------------------------------
/test/widgets-malformed.js:
--------------------------------------------------------------------------------
1 | const t = require('../test-lib/test.js');
2 | const { spawn } = require('child_process');
3 | const assert = require('assert');
4 |
5 | describe('Malformed Widgets', function () {
6 | this.timeout(t.timeout);
7 |
8 | it('should fail to initialize with a schema containing a field named "type"', function (done) {
9 | let throwsError = true;
10 | const mochaProcess = spawn('node', [ './test/widgets-children/widgets-malformed-child.js' ]);
11 |
12 | mochaProcess.stderr.on('data', (data) => {
13 | const errorMsg = data.toString();
14 | const errorMatch = errorMsg.match(/(?Error:.*\n)/);
15 | if (errorMatch) {
16 | throwsError = true;
17 | assert.equal(errorMatch.groups.error, 'Error: The malformed module contains a forbidden field property name: "type".\n');
18 | } else {
19 | throwsError = false;
20 | }
21 | });
22 |
23 | mochaProcess.on('close', (code) => {
24 | assert.equal(code, 1, 'Mocha process exited with status code 0');
25 | assert.ok(throwsError, 'Error message not found in stderr');
26 | done();
27 | });
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/test/with-nested-module-subdirs.js:
--------------------------------------------------------------------------------
1 | const t = require('../test-lib/test.js');
2 | const assert = require('assert');
3 |
4 | describe('With Nested Module Subdirs', function() {
5 | this.timeout(t.timeout);
6 |
7 | let apos;
8 |
9 | after(function () {
10 | return t.destroy(apos);
11 | });
12 |
13 | /// ///
14 | // EXISTENCE
15 | /// ///
16 |
17 | it('should initialize', async function() {
18 | apos = await t.create({
19 | root: module,
20 | nestedModuleSubdirs: true,
21 | modules: {
22 | example1: {}
23 | }
24 | });
25 | assert(apos.modules.example1);
26 | // With nestedModuleSubdirs switched on, the index.js should be found,
27 | // and modules.js should be loaded
28 | assert(apos.modules.example1.options.folderLevelOption);
29 | assert(apos.modules.example1.initialized);
30 | });
31 |
32 | });
33 |
--------------------------------------------------------------------------------
/test/without-nested-module-subdirs.js:
--------------------------------------------------------------------------------
1 | const t = require('../test-lib/test.js');
2 | const assert = require('assert');
3 |
4 | describe('Without Nested Module Subdirs', function() {
5 | this.timeout(t.timeout);
6 |
7 | let apos;
8 |
9 | after(function () {
10 | return t.destroy(apos);
11 | });
12 |
13 | /// ///
14 | // EXISTENCE
15 | /// ///
16 |
17 | it('should initialize', async function() {
18 | apos = await t.create({
19 | root: module,
20 | modules: {
21 | example1: {}
22 | }
23 | });
24 | assert(apos.modules.example1);
25 | // Should fail because we didn't turn on nestedModuleSubdirs,
26 | // so the index.js was not found and modules.js was not loaded
27 | assert(!apos.modules.example1.options.folderLevelOption);
28 | assert(!apos.modules.example1.initialized);
29 | });
30 |
31 | });
32 |
--------------------------------------------------------------------------------
/test/workspaces-project.js:
--------------------------------------------------------------------------------
1 | const assert = require('node:assert').strict;
2 | const util = require('node:util');
3 | const { exec } = require('node:child_process');
4 | const path = require('node:path');
5 | const t = require('../test-lib/test.js');
6 | const app = require('./workspaces-project/app.js');
7 |
8 | describe('workspaces dependencies', function() {
9 | this.timeout(t.timeout);
10 |
11 | before(async function() {
12 | await util.promisify(exec)('npm install', { cwd: path.resolve(process.cwd(), 'test/workspaces-project') });
13 | });
14 |
15 | it('should allow workspaces dependency in the project', async function() {
16 | let apos;
17 |
18 | try {
19 | apos = await t.create(app);
20 | const { server } = apos.modules['@apostrophecms/express'];
21 | const { address, port } = server.address();
22 |
23 | const actual = apos.util.logger.getMessages();
24 | const expected = {
25 | debug: [],
26 | info: [ `Listening at http://${address}:${port}` ],
27 | warn: [],
28 | error: []
29 | };
30 |
31 | assert.deepEqual(actual, expected);
32 | } catch (error) {
33 | assert.fail('Should have found @apostrophecms/sitemap hidden in workspace-a as a valid dependency. '.concat(error.message));
34 | } finally {
35 | apos && await t.destroy(apos);
36 | }
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/test/workspaces-project/app.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: module,
3 | shortName: 'workspaces-project',
4 | baseUrl: 'http://localhost:3000',
5 | modules: {
6 | '@apostrophecms/express': {
7 | options: {
8 | address: '127.0.0.1'
9 | }
10 | },
11 | '@apostrophecms/sitemap': {}
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/test/workspaces-project/modules/@apostrophecms/log/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | const createLogger = () => {
3 | const messages = {
4 | debug: [],
5 | info: [],
6 | warn: [],
7 | error: []
8 | };
9 |
10 | return {
11 | debug: (...args) => {
12 | console.debug(...args);
13 | messages.debug.push(...args);
14 | },
15 | info: (...args) => {
16 | console.info(...args);
17 | messages.info.push(...args);
18 | },
19 | warn: (...args) => {
20 | console.warn(...args);
21 | messages.warn.push(...args);
22 | },
23 | error: (...args) => {
24 | console.error(...args);
25 | messages.error.push(...args);
26 | },
27 | destroy: () => {
28 | delete messages.debug;
29 | delete messages.info;
30 | delete messages.warn;
31 | delete messages.error;
32 | },
33 | getMessages: () => messages
34 | };
35 | };
36 |
37 | module.exports = {
38 | options: {
39 | logger: createLogger()
40 | }
41 | };
42 |
--------------------------------------------------------------------------------
/test/workspaces-project/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "workspace-project",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "app.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "apostrophe": "file:../../."
14 | },
15 | "workspaces": [
16 | "workspace-a"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/test/workspaces-project/workspace-a/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "workspace-a",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "@apostrophecms/sitemap": "^1.0.2"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------