├── .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 | 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 | 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 | 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 | {{ __t('apostrophe:imagePlaceholder') }} 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 | {{ attachment._alt or '' }} 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 | 9 | 23 | 24 | 31 | -------------------------------------------------------------------------------- /modules/@apostrophecms/modal/ui/apos/components/AposModalTabsBody.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | 17 | 33 | -------------------------------------------------------------------------------- /modules/@apostrophecms/modal/ui/apos/components/AposModalToolbar.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 |
7 | {{ pagerPage(1, options, pagerClass, url) }} 8 | {% if apos.pager.showHeadGap(options) %} 9 | 10 | {% endif %} 11 | 12 | {% for page in apos.pager.pageRange({ 13 | page: options.page, 14 | total: options.total, 15 | shown: options.shown or 5 16 | }) %} 17 | {{ pagerPageInner(page, options, pagerClass, url) }} 18 | {% endfor %} 19 | 20 | {% if apos.pager.showTailGap(options) %} 21 | 22 | {% endif %} 23 | {{ pagerPage(options.total, options, pagerClass, url) }} 24 |
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 |

{{ piece.title }}

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 | 19 | 20 | 61 | 62 | 64 | -------------------------------------------------------------------------------- /modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapDivider.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | 20 | 32 | -------------------------------------------------------------------------------- /modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapUndefined.vue: -------------------------------------------------------------------------------- 1 | 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 | 36 | 37 | 44 | 45 | 57 | -------------------------------------------------------------------------------- /modules/@apostrophecms/schema/ui/apos/components/AposInputAttachment.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 32 | -------------------------------------------------------------------------------- /modules/@apostrophecms/schema/ui/apos/components/AposInputCheckboxes.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 39 | -------------------------------------------------------------------------------- /modules/@apostrophecms/schema/ui/apos/components/AposInputObject.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 41 | 42 | 55 | -------------------------------------------------------------------------------- /modules/@apostrophecms/schema/ui/apos/components/AposInputPassword.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 36 | -------------------------------------------------------------------------------- /modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue: -------------------------------------------------------------------------------- 1 | 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 |
6 | {% if data.filters %} 7 |
{{ __t('apostrophe:filterResultsByType') }}
8 | 18 | {% endif %} 19 |
{{ __t('apostrophe:newSearch') }}
20 | 21 | 22 |
23 | {% for doc in data.docs %} 24 |

{% if doc._url %}{% endif %}{{ doc.title }}{% if doc._url %}{% endif %}

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 |

{% if doc._url %}{% endif %}{{ doc.title }}{% if doc._url %}{% endif %}

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 |

{% if doc._url %}{% endif %}{{ doc.title }}{% if doc._url %}{% endif %}

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 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /modules/@apostrophecms/ui/ui/apos/components/AposCellButton.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 19 | -------------------------------------------------------------------------------- /modules/@apostrophecms/ui/ui/apos/components/AposCellLink.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /modules/@apostrophecms/ui/ui/apos/components/AposCellType.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 23 | -------------------------------------------------------------------------------- /modules/@apostrophecms/ui/ui/apos/components/AposCloudUploadIcon.vue: -------------------------------------------------------------------------------- 1 | 2 | 44 | 45 | 50 | -------------------------------------------------------------------------------- /modules/@apostrophecms/ui/ui/apos/components/AposColorCheckerboard.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 21 | -------------------------------------------------------------------------------- /modules/@apostrophecms/ui/ui/apos/components/AposContextMenuTip.vue: -------------------------------------------------------------------------------- 1 | 2 | 34 | 35 | 38 | 39 | 44 | -------------------------------------------------------------------------------- /modules/@apostrophecms/ui/ui/apos/components/AposEmptyState.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 35 | 36 | 68 | -------------------------------------------------------------------------------- /modules/@apostrophecms/ui/ui/apos/components/AposIndicator.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 55 | 66 | -------------------------------------------------------------------------------- /modules/@apostrophecms/ui/ui/apos/components/AposLoadingBlock.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 30 | 31 | 56 | -------------------------------------------------------------------------------- /modules/@apostrophecms/ui/ui/apos/components/AposLocale.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 25 | 26 | 39 | -------------------------------------------------------------------------------- /modules/@apostrophecms/ui/ui/apos/components/AposPagerDots.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 49 | 50 | 81 | -------------------------------------------------------------------------------- /modules/@apostrophecms/ui/ui/apos/components/AposTag.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | 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 |

Please confirm your account by following this link.

6 | 7 | -------------------------------------------------------------------------------- /test/modules/event-widget/views/widget.html: -------------------------------------------------------------------------------- 1 | {% for piece in data.widget._featured %} 2 |

3 | {% if piece._url %}{% endif %} 4 | {{ piece.title }} 5 | {% if piece._url %}{% endif %} 6 |

7 | {% endfor %} 8 | 9 | {% for piece in data.widget._pieces %} 10 |

11 | {% if piece._url %}{% endif %} 12 | {{ piece.title }} 13 | {% if piece._url %}{% endif %} 14 |

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 | 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 | --------------------------------------------------------------------------------