├── .circleci
└── config.yml
├── .editorconfig
├── .eslintrc
├── .github
└── stale.yml
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── badges
├── npm-audit-badge.png
└── npm-audit-badge.svg
├── index.js
├── lib
├── api.js
├── browser.js
├── callAll.js
├── helpers.js
├── implementation.js
├── middleware.js
├── modules
│ ├── apostrophe-workflow-admin-bar
│ │ └── index.js
│ ├── apostrophe-workflow-areas
│ │ └── index.js
│ ├── apostrophe-workflow-assets
│ │ └── index.js
│ ├── apostrophe-workflow-docs
│ │ ├── index.js
│ │ └── lib
│ │ │ └── cursor.js
│ ├── apostrophe-workflow-global
│ │ ├── index.js
│ │ └── public
│ │ │ └── js
│ │ │ └── user.js
│ ├── apostrophe-workflow-groups
│ │ └── index.js
│ ├── apostrophe-workflow-images
│ │ ├── index.js
│ │ └── views
│ │ │ └── workflowPreview.html
│ ├── apostrophe-workflow-modified-documents
│ │ ├── index.js
│ │ ├── lib
│ │ │ └── cursor.js
│ │ ├── public
│ │ │ ├── css
│ │ │ │ └── manager.less
│ │ │ └── js
│ │ │ │ └── manager-modal.js
│ │ └── views
│ │ │ ├── manageHeadings.html
│ │ │ ├── manageListPage.html
│ │ │ └── managerModal.html
│ ├── apostrophe-workflow-pages
│ │ ├── index.js
│ │ └── public
│ │ │ └── js
│ │ │ ├── editor.js
│ │ │ └── reorganize.js
│ ├── apostrophe-workflow-permissions
│ │ └── index.js
│ ├── apostrophe-workflow-pieces
│ │ ├── index.js
│ │ ├── public
│ │ │ └── js
│ │ │ │ ├── editor-modal.js
│ │ │ │ └── manager-modal.js
│ │ └── views
│ │ │ └── workflowLastCommitted.html
│ ├── apostrophe-workflow-schemas
│ │ ├── index.js
│ │ ├── public
│ │ │ └── js
│ │ │ │ └── user.js
│ │ └── views
│ │ │ └── workflow-permissions-schema-field.html
│ ├── apostrophe-workflow-tasks
│ │ └── index.js
│ └── apostrophe-workflow-templates
│ │ └── index.js
├── removeDotPathViaSplice.js
├── routes.js
└── tasks.js
├── locales
└── en.json
├── package.json
├── public
├── css
│ ├── expand.less
│ ├── menu.less
│ ├── schemas.less
│ └── user.less
└── js
│ ├── ._committable-modal.js
│ ├── batch-export-modal.js
│ ├── batch-force-export-modal.js
│ ├── commit-modal.js
│ ├── export-modal.js
│ ├── force-export-modal.js
│ ├── force-export-related-modal.js
│ ├── force-export-widget-modal.js
│ ├── history-modal.js
│ ├── lean.js
│ ├── locale-picker-modal.js
│ ├── locale-unavailable-modal.js
│ ├── manage-modal.js
│ ├── review-modal.js
│ └── user.js
├── test
├── .gitignore
├── lib
│ └── modules
│ │ ├── apostrophe-custom-pages
│ │ └── index.js
│ │ ├── apostrophe-pages
│ │ └── views
│ │ │ └── pages
│ │ │ └── home.html
│ │ └── products
│ │ └── index.js
├── missing-prefix-redirect-status-code.js
├── package.json
├── parkedPages.js
├── reorganize.js
├── reorganizeReplicateFalse.js
├── test1.js
├── test2-disable-anon-session.js
├── test2.js
├── test3.js
├── testApi.js
├── testDereplicate.js
├── testGlobalDef.js
├── testOverrideOptions.js
├── testReplicateFalse.js
├── testReplicateRelated.js
└── testResolveRelationships.js
└── views
├── batch-export-modal.html
├── batch-force-export-modal.html
├── commit-modal.html
├── export-modal.html
├── force-export-modal.html
├── force-export-related-modal.html
├── force-export-widget-modal.html
├── history-modal.html
├── locale-picker-modal.html
├── locale-picker.html
├── locale-tree.html
├── locale-unavailable-modal.html
├── manage-modal.html
├── menu.html
├── relatedByType.html
└── review-modal.html
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | jobs:
3 | build-node14-mongo44:
4 | docker:
5 | - image: circleci/node:14-browsers
6 | - image: mongo:4.4
7 | steps:
8 | - checkout
9 | - run:
10 | name: update-npm
11 | command: 'sudo npm install -g npm@7'
12 | - restore_cache:
13 | key: dependency-cache-{{ checksum "package.json" }}
14 | - run:
15 | name: install-npm-wee
16 | command: npm install
17 | - save_cache:
18 | key: dependency-cache-{{ checksum "package.json" }}
19 | paths:
20 | - ./node_modules
21 | - run:
22 | name: test
23 | command: npm test
24 | build-node14-mongo42:
25 | docker:
26 | - image: circleci/node:14-browsers
27 | - image: mongo:4.2
28 | steps:
29 | - checkout
30 | - run:
31 | name: update-npm
32 | command: 'sudo npm install -g npm@7'
33 | - restore_cache:
34 | key: dependency-cache-{{ checksum "package.json" }}
35 | - run:
36 | name: install-npm-wee
37 | command: npm install
38 | - save_cache:
39 | key: dependency-cache-{{ checksum "package.json" }}
40 | paths:
41 | - ./node_modules
42 | - run:
43 | name: test
44 | command: npm test
45 | build-node12:
46 | docker:
47 | - image: circleci/node:12-browsers
48 | - image: mongo:3.6.11
49 | steps:
50 | - checkout
51 | - run:
52 | name: update-npm
53 | command: 'sudo npm install -g npm@6'
54 | - restore_cache:
55 | key: dependency-cache-{{ checksum "package.json" }}
56 | - run:
57 | name: install-npm-wee
58 | command: npm install
59 | - save_cache:
60 | key: dependency-cache-{{ checksum "package.json" }}
61 | paths:
62 | - ./node_modules
63 | - run:
64 | name: test
65 | command: npm test
66 | build-node10:
67 | docker:
68 | - image: circleci/node:10-browsers
69 | - image: mongo:3.6.11
70 | steps:
71 | - checkout
72 | - run:
73 | name: update-npm
74 | command: 'sudo npm install -g npm@6'
75 | - restore_cache:
76 | key: dependency-cache-{{ checksum "package.json" }}
77 | - run:
78 | name: install-npm-wee
79 | command: npm install
80 | - save_cache:
81 | key: dependency-cache-{{ checksum "package.json" }}
82 | paths:
83 | - ./node_modules
84 | - run:
85 | name: test
86 | command: npm test
87 | workflows:
88 | version: 2
89 | build:
90 | jobs:
91 | - build-node14-mongo44
92 | - build-node14-mongo42
93 | - build-node12
94 | - build-node10
95 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "apostrophe"
3 | }
4 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Number of days of inactivity before an issue becomes stale
2 | daysUntilStale: 60
3 | # Number of days of inactivity before a stale issue is closed
4 | daysUntilClose: 14
5 | # Issues with these labels will never be considered stale
6 | exemptLabels:
7 | - pinned
8 | - security
9 | - documentation
10 | - bug
11 | - "3.0"
12 | # Label to use when marking an issue as stale
13 | staleLabel: stale
14 | # Comment to post when marking an issue as stale. Set to `false` to disable
15 | markComment: >
16 | This issue has been automatically marked as stale because it has not had
17 | recent activity. It will be closed if no further activity occurs. Thank you
18 | for your contributions.
19 | # Comment to post when closing a stale issue. Set to `false` to disable
20 | closeComment: false
21 |
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | test/locales
2 | package-lock.json
3 | npm-debug.log
4 | *.DS_Store
5 | node_modules
6 | # We do not commit CSS, only LESS
7 | public/css/*.css
8 | # No Vim swap files
9 | *.sw*
10 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016, 2017 P'unk Avenue LLC
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/badges/npm-audit-badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apostrophecms-legacy/apostrophe-workflow/8ff55dd9083824b4d10db7775dda250d4a5bf4a1/badges/npm-audit-badge.png
--------------------------------------------------------------------------------
/badges/npm-audit-badge.svg:
--------------------------------------------------------------------------------
1 | vulnerabilities vulnerabilities 50 50
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | var async = require('async');
2 | var Promise = require('bluebird');
3 |
4 | var modules = [
5 | 'apostrophe-workflow-areas',
6 | 'apostrophe-workflow-docs',
7 | 'apostrophe-workflow-global',
8 | 'apostrophe-workflow-groups',
9 | 'apostrophe-workflow-pages',
10 | 'apostrophe-workflow-permissions',
11 | 'apostrophe-workflow-pieces',
12 | 'apostrophe-workflow-schemas',
13 | 'apostrophe-workflow-images',
14 | 'apostrophe-workflow-admin-bar',
15 | 'apostrophe-workflow-tasks',
16 | 'apostrophe-workflow-assets',
17 | 'apostrophe-workflow-modified-documents',
18 | 'apostrophe-workflow-templates'
19 | ];
20 |
21 | // ## Options
22 | //
23 | // `includeTypes: [ 'my-blog-post', 'my-event' ]`
24 | //
25 | // Apply workflow only to docs of the specified types. IF WORKFLOW IS ENABLED FOR ANY PAGE TYPE,
26 | // AS OPPOSED TO A PIECE, IT MUST BE ENABLED FOR *ALL* PAGE TYPES.
27 | //
28 | // `excludeTypes: [ 'my-personal-profile' ]`
29 | //
30 | // Apply workflow to everything EXCEPT the specified types. IF WORKFLOW IS ENABLED FOR ANY PAGE TYPE,
31 | // AS OPPOSED TO A PIECE, IT MUST BE ENABLED FOR *ALL* PAGE TYPES.
32 | //
33 | // If both options are present, a type must appear in `includeTypes`
34 | // and NOT appear in `excludeTypes`.
35 | //
36 | // `baseExcludeTypes: [ 'apostrophe-user', 'apostrophe-group' ]`
37 | //
38 | // **Typically not changed.** A short list of types that should never be subject to workflow,
39 | // no matter what the other options say. For security reasons this list contains users and groups
40 | // by default. You will usually leave this alone.
41 | //
42 | // `excludeProperties: [ 'hitCounter' ]`
43 | //
44 | // A list of properties that should not be subject to workflow, but rather should be allowed to
45 | // vary for each locale and never be copied. These are typically properties
46 | // that don't make sense to edit as a "draft" and then submit as the new live version. For
47 | // instance, you wouldn't want to overwrite a page view counter field.
48 | //
49 | // There is no `includeProperties` option. In Apostrophe 2.x workflow applies to properties by default,
50 | // and excluded properties are unique to the locale (that is, either draft or live version of the doc).
51 | //
52 | // `baseExcludeProperties`
53 | //
54 | // Like `baseExcludeTypes`, this overrides a short list of properties that must not be modified
55 | // by workflow. You don't want to change this.
56 |
57 | module.exports = {
58 |
59 | moogBundle: {
60 | modules: modules,
61 | directory: 'lib/modules'
62 | },
63 |
64 | beforeConstruct: function(self, options) {
65 | if (options.replicateAcrossLocales === undefined) {
66 | options.replicateAcrossLocales = true;
67 | }
68 | },
69 |
70 | afterConstruct: function(self, callback) {
71 | self.composeLocales();
72 | self.composeOptions();
73 | self.enableAddMissingLocalesTask();
74 | self.enableAddLocalePrefixesTask();
75 | self.enableRemoveNumberedParkedPagesTask();
76 | self.enableResolveJoinIdsTask();
77 | self.enableHarmonizeWorkflowGuidsByParkedIdTask();
78 | self.enableDiffDraftAndLiveTask();
79 | self.enableReplicateLocaleTask();
80 | self.enableSyncPagesTreeTask();
81 | self.pushAssets();
82 | self.addToAdminBar();
83 | self.apos.pages.addAfterContextMenu(self.menu);
84 | self.enableHelpers();
85 | self.enableCrossDomainSessionCache();
86 | self.refineOptimizeKey();
87 | self.composeApiCalls();
88 | self.addWorkflowModifiedMigration();
89 | self.addWorkflowLastCommittedMigration();
90 |
91 | if (self.options.autoCommitPageMoves) {
92 | self.addPagesTreeCleaningMigration();
93 | }
94 |
95 | self.on('apostrophe-pages:beforeParkAll', 'updateHistoricalPrefixesPromisified', function() {
96 | return Promise.promisify(self.updateHistoricalPrefixes)();
97 | });
98 | self.addRoutes();
99 | return async.series([
100 | self.enableCollection,
101 | self.enableFacts
102 | ], callback);
103 | },
104 |
105 | construct: function(self, options) {
106 | require('./lib/implementation.js')(self, options);
107 | require('./lib/api.js')(self, options);
108 | require('./lib/callAll.js')(self, options);
109 | require('./lib/browser.js')(self, options);
110 | require('./lib/middleware.js')(self, options);
111 | require('./lib/routes.js')(self, options);
112 | require('./lib/tasks.js')(self, options);
113 | require('./lib/helpers.js')(self, options);
114 | }
115 |
116 | };
117 |
--------------------------------------------------------------------------------
/lib/browser.js:
--------------------------------------------------------------------------------
1 | module.exports = function(self, options) {
2 | self.pushAssets = function() {
3 | self.pushAsset('script', 'user', { when: 'user' });
4 | self.pushAsset('script', 'manage-modal', { when: 'user' });
5 | self.pushAsset('script', 'commit-modal', { when: 'user' });
6 | self.pushAsset('script', 'export-modal', { when: 'user' });
7 | self.pushAsset('script', 'review-modal', { when: 'user' });
8 | self.pushAsset('script', 'history-modal', { when: 'user' });
9 | self.pushAsset('script', 'locale-picker-modal', { when: 'user' });
10 | self.pushAsset('script', 'force-export-widget-modal', { when: 'user' });
11 | self.pushAsset('script', 'force-export-modal', { when: 'user' });
12 | self.pushAsset('script', 'force-export-related-modal', { when: 'user' });
13 | self.pushAsset('script', 'batch-export-modal', { when: 'user' });
14 | self.pushAsset('script', 'batch-force-export-modal', { when: 'user' });
15 | self.pushAsset('script', 'locale-unavailable-modal', { when: 'user' });
16 | self.pushAsset('stylesheet', 'user', { when: 'user' });
17 | self.pushAsset('script', 'lean', { when: 'lean' });
18 | };
19 | };
20 |
--------------------------------------------------------------------------------
/lib/helpers.js:
--------------------------------------------------------------------------------
1 | var _ = require('@sailshq/lodash');
2 |
3 | module.exports = function(self, options) {
4 | self.enableHelpers = function() {
5 | self.addHelpers({
6 | localizations: function() {
7 | var localizations = [];
8 | _.each((self.apos.templates.contextReq.data.workflow && self.apos.templates.contextReq.data.workflow.localizations) || [], function(localization, locale) {
9 | if (!self.locales[locale].private) {
10 | localizations.push(localization);
11 | }
12 | });
13 | return localizations;
14 | },
15 | lang: function() {
16 | var locale = self.apos.templates.contextReq.locale || self.defaultLocale;
17 | locale = self.liveify(locale);
18 | if (self.locales[locale] && self.locales[locale].lang) {
19 | return self.locales[locale].lang;
20 | }
21 | return locale.replace(/[-_]\w+$/, '');
22 | },
23 | committable: function(draft) {
24 | return self.filterCommittableDrafts(self.apos.templates.contextReq, [ draft ]).length > 0;
25 | }
26 | });
27 | };
28 | };
29 |
--------------------------------------------------------------------------------
/lib/middleware.js:
--------------------------------------------------------------------------------
1 | var _ = require('@sailshq/lodash');
2 |
3 | module.exports = function(self, options) {
4 |
5 | // Set `req.locale` based on `req.query.workflowLocale`, `req.session.locale`,
6 | // the hostname and/or the URL prefix.
7 | //
8 | // If the locale is not present or is not valid, set `req.locale` to the
9 | // default locale by calling `self.guessLocale`.
10 | //
11 | // Store the locale in `req.session.locale` as well unless disabled.
12 |
13 | self.expressMiddleware = {
14 | before: 'apostrophe-global',
15 | middleware: function(req, res, next) {
16 | var to, host, matches, locale, prefix, hostname, subdomain, domain, candidates, setByUs;
17 |
18 | function setLocale(locale) {
19 | req.locale = locale;
20 | if (locale) {
21 | setByUs = true;
22 | }
23 | if (self.localeInSession(req)) {
24 | req.session.locale = locale;
25 | }
26 | if (self.apos.hasOwnProperty('i18n')) {
27 | self.apos.i18n.setLocale(req, locale);
28 | }
29 | }
30 |
31 | req.data = req.data || {};
32 | req.data.workflow = req.data.workflow || {};
33 | _.assign(req.data.workflow, _.pick(self, 'locales', 'nestedLocales'));
34 |
35 | // Hack to implement bc with the subdomains option
36 | // by populating self.hostnames for each prefix, as soon as we
37 | // actually know what the domain name is
38 | if (self.options.subdomains && (!self.hostnames)) {
39 | host = self.getHost(req);
40 | self.hostnames = {};
41 | matches = host.match(/^([^:.]+)(\.([^:]+))?:?/);
42 | subdomain = matches[1];
43 | domain = matches[3];
44 | if (!_.has(self.locales, subdomain)) {
45 | domain = host;
46 | if (domain.match(/(:80|:443)$/)) {
47 | domain = domain.replace(/:\d+$/, '');
48 | }
49 | }
50 | _.each(self.locales, function(locale, name) {
51 | if (!name.match(/-draft$/)) {
52 | if (!locale.private) {
53 | self.hostnames[name] = name + '.' + domain;
54 | } else {
55 | self.hostnames[name] = domain;
56 | }
57 | }
58 | });
59 | }
60 |
61 | if (req.query.workflowLocale) {
62 | // Switch locale choice in session via query string, then redirect
63 | locale = self.apos.launder.string(req.query.workflowLocale);
64 |
65 | if (_.has(self.locales, locale)) {
66 | setLocale(locale);
67 | }
68 | if (self.hostnames) {
69 | // Don't let a stale hostname just switch it back
70 | to = req.absoluteUrl.replace(/\??workflowLocale=[^&]+&?/, '');
71 | matches = to.match(/^(https?:)?\/\/([^/:]+)/);
72 | hostname = matches[2];
73 | if (hostname) {
74 | to = to.replace('//' + hostname, '//' + self.hostnames[self.liveify(req.query.workflowLocale)]);
75 | }
76 | return res.redirect(to);
77 | } else {
78 | return res.redirect(req.url.replace(/\??workflowLocale=[^&]+&?/, ''));
79 | }
80 | }
81 |
82 | // test reqs might not have a headers object
83 | if (req.headers && req.headers['apostrophe-locale']) {
84 | // API calls use apostrophe-locale headers
85 | if (_.has(self.locales, req.headers['apostrophe-locale'])) {
86 | // To avoid race conditions that can break locale switching,
87 | // we don't modify the session based on the header
88 | if (self.apos.hasOwnProperty('i18n')) {
89 | // Careful, i18n.setLocale does not grasp draft locales
90 | self.apos.i18n.setLocale(req, self.liveify(req.headers['apostrophe-locale']));
91 | }
92 | req.locale = req.headers['apostrophe-locale'];
93 | return next();
94 | }
95 | }
96 |
97 | // Start with all of the non-draft locales as candidates
98 | // (we implement draft vs. live at the end)
99 |
100 | candidates = _.filter(_.keys(self.locales), function(locale) {
101 | return locale === self.liveify(locale);
102 | });
103 |
104 | // Winnow it down by hostname
105 |
106 | if (self.hostnames) {
107 | host = self.getHost(req);
108 | matches = host.match(/^[^:]+/);
109 | if (matches) {
110 | hostname = matches[0];
111 | candidates = _.filter(candidates, function(candidate) {
112 | if (self.locales[candidate] && self.locales[candidate].private) {
113 | if (!self.apos.permissions.can(req, 'private-locales')) {
114 | return false;
115 | }
116 | }
117 | return self.hostnames[candidate] === hostname;
118 | });
119 | }
120 | }
121 |
122 | function extractRootLevelLocale(candidates) {
123 | return _.filter(candidates, function (candidate) {
124 | const locale = self.locales[candidate];
125 | return (!locale.private) && (!_.has(self.prefixes, candidate));
126 | });
127 | }
128 |
129 | // If we're not down to one yet, winnow it down by prefix
130 | if ((candidates.length > 1) && self.prefixes) {
131 | // If we're dealing with a locale which shares the hostname
132 | // with other locales, but doesn't have a prefix, (i.e. it
133 | // exists at the root level of the hostname), figure out
134 | // that locale and set that as the current session's locale.
135 | if (req.url.match(/^\/(\?|$)/)) {
136 | // a locale which resides at the root level of a shared
137 | // hostname won't have a prefix. So we select the candidate
138 | // which doesn't have a prefix in this case.
139 | candidates = extractRootLevelLocale(candidates);
140 | } else {
141 | matches = req.url.match(/^\/[^/]+/);
142 | if (matches) {
143 | prefix = matches[0];
144 |
145 | var filteredCandidates = _.filter(candidates, function (candidate) {
146 | return self.prefixes[candidate] === prefix;
147 | });
148 |
149 | // At this stage, if the list of filtered candidates is
150 | // empty, it means the first fragment of the URI didn't
151 | // match any of the candidates' locale prefixes.
152 | //
153 | // We're here because the hostname filter did not narrow
154 | // the candidates to one, so this is likely to be a URI
155 | // for a root-level locale among candidates filtered by
156 | // hostname.
157 | //
158 | // However, it could also be an API URL which doesn't care
159 | // about the locale or is counting on the existing value
160 | // of req.locale. To resolve this ambiguity we check for
161 | // a standardized list of URI prefixes that do not
162 | // imply a locale. This list can be extended.
163 |
164 | if (filteredCandidates.length === 0) {
165 | if (!self.isApiCall(req)) {
166 | candidates = extractRootLevelLocale(candidates);
167 | }
168 | } else {
169 | // if the candidates list isn't empty, the URI's first fragment
170 | // matched the locale prefix for one of the candidates, in which
171 | // case it's fairly clear how to proceed.
172 | candidates = filteredCandidates;
173 | }
174 | }
175 | }
176 | }
177 |
178 | // If we made it down to one locale, that's the winner
179 | if (candidates.length === 1) {
180 | var privateLocale = self.locales[candidates[0]] && self.locales[candidates[0]].private;
181 | if (privateLocale) {
182 | if (self.apos.permissions.can(req, 'private-locales')) {
183 | setLocale(candidates[0]);
184 | }
185 | } else {
186 | setLocale(candidates[0]);
187 | }
188 | }
189 | const hostnameDefaultLocale = self.getLocaleViaHostnameDefault(req);
190 | if (hostnameDefaultLocale && (!setByUs)) {
191 | if (self.locales[hostnameDefaultLocale] && self.locales[hostnameDefaultLocale].private) {
192 | if (self.apos.permissions.can(req, 'private-locales')) {
193 | setLocale(hostnameDefaultLocale);
194 | }
195 | } else {
196 | setLocale(hostnameDefaultLocale);
197 | }
198 | }
199 |
200 | req.locale = self.localeInSession(req) ? req.session.locale : req.locale;
201 | // Resort to the default locale if (1) there is no indication of what locale to use,
202 | // (2) the locale isn't configured, or (3) the locale is private and we don't have
203 | // permission to view private locales.
204 |
205 | if ((!req.locale) || (!_.has(self.locales, req.locale)) ||
206 | (self.locales[req.locale].private && (!self.apos.permissions.can(req, 'private-locales')))) {
207 | self.guessLocale(req);
208 | setLocale(req.locale);
209 | }
210 |
211 | if (req.user) {
212 | // Handle preview mode first
213 | if (!req.session.workflowMode && self.defaultMode === 'preview') {
214 | req.session.workflowPreview = true;
215 | }
216 |
217 | req.session.workflowMode = req.session.workflowMode || self.defaultMode;
218 | if (req.session.workflowMode === 'live') {
219 | req.locale = self.liveify(req.locale);
220 | req.session.workflowMode = 'live';
221 | } else {
222 | req.locale = self.draftify(req.locale);
223 | req.session.workflowMode = 'draft';
224 | }
225 | }
226 |
227 | if (self.prefixes && self.prefixes[self.liveify(req.locale)] && req.url.match(/^\/(\?|$)/)) {
228 | // Redirect to home page of appropriate locale
229 | let newUrl = self.prefixes[self.liveify(req.locale)] + '/';
230 | const matches = req.url.match(/^\/(\?.*)$/);
231 | if (matches) {
232 | newUrl += matches[1];
233 | }
234 | return res.redirect(self.options.missingPrefixRedirectStatusCode || 302, newUrl);
235 | }
236 |
237 | return next();
238 | }
239 | };
240 |
241 | };
242 |
--------------------------------------------------------------------------------
/lib/modules/apostrophe-workflow-admin-bar/index.js:
--------------------------------------------------------------------------------
1 | var _ = require('@sailshq/lodash');
2 |
3 | module.exports = {
4 | improve: 'apostrophe-admin-bar',
5 | beforeConstruct: function(self, options) {
6 | options.removeWhenLive = [ 'apostrophe-pages', 'apostrophe-tags', 'apostrophe-workflow-manage-modal' ].concat(options.removeWhenLive || []);
7 | },
8 | construct: function(self, options) {
9 | // Hide most admin bar buttons in draft mode, specifically
10 | // those that manage pieces; you must be in draft mode to
11 | // do most things. Later perhaps we'll introduce a manage modal
12 | // for live mode that lets you preview things
13 | var superItemIsVisible = self.itemIsVisible;
14 | self.itemIsVisible = function(req, item) {
15 | var result = superItemIsVisible(req, item);
16 | if (!result) {
17 | return result;
18 | }
19 | if (req.locale && req.locale.match(/-draft$/)) {
20 | return result;
21 | }
22 |
23 | if (_.contains(self.options.removeWhenLive, item.name)) {
24 | return false;
25 | }
26 | // In addition, pieces manage buttons are not safe live
27 | // if the type is included in workflow; look
28 | // for subclasses
29 | var manager = self.apos.modules[item.name];
30 | if (!manager) {
31 | return result;
32 | }
33 | if (!(self.apos.synth.instanceOf(manager, 'apostrophe-pieces'))) {
34 | return result;
35 | }
36 | var workflow = self.apos.modules['apostrophe-workflow'];
37 | if (!workflow.includeType(manager.name)) {
38 | return result;
39 | }
40 | return false;
41 | };
42 | }
43 | };
44 |
--------------------------------------------------------------------------------
/lib/modules/apostrophe-workflow-areas/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | improve: 'apostrophe-areas',
3 | construct: function(self, options) {
4 | var superWidgetControlGroups = self.widgetControlGroups;
5 | self.widgetControlGroups = function(req, widget, options) {
6 | var controlGroups = superWidgetControlGroups(req, widget, options);
7 | var workflow = self.apos.modules['apostrophe-workflow'];
8 | if (!workflow.localized) {
9 | return controlGroups;
10 | }
11 | return controlGroups.concat([
12 | {
13 | controls: [
14 | {
15 | tooltip: 'Force Export',
16 | icon: 'sign-out',
17 | action: 'workflow-force-export-widget'
18 | }
19 | ]
20 | }
21 | ]);
22 | };
23 | }
24 | };
25 |
--------------------------------------------------------------------------------
/lib/modules/apostrophe-workflow-assets/index.js:
--------------------------------------------------------------------------------
1 | var _ = require('@sailshq/lodash');
2 | var fs = require('fs');
3 |
4 | module.exports = {
5 | improve: 'apostrophe-assets',
6 | afterConstruct: function(self) {
7 | self.workflowAddStylesheetRoutes();
8 | },
9 | construct: function(self, options) {
10 | var superStylesheetsHelper = self.stylesheetsHelper;
11 | self.workflowStylesheetCache = {};
12 | self.stylesheetsHelper = function(scene) {
13 | var result = '';
14 | var workflow = self.apos.modules['apostrophe-workflow'];
15 | var locale = self.apos.templates.contextReq.locale;
16 | locale = workflow.liveify(locale);
17 | var prefix = self.apos.prefix + self.action;
18 | if (workflow.locales[locale].stylesheet) {
19 | result = ' ';
20 | } else if (workflow.options.defaultStylesheet) {
21 | result = ' ';
22 | }
23 | return self.apos.templates.safe(result + superStylesheetsHelper(scene).toString());
24 | };
25 |
26 | self.workflowAddStylesheetRoutes = function() {
27 | // For performance reasons we need to run these "routes" before the middleware,
28 | // so push middleware of our own that implements them. The apostrophe-assets
29 | // module already sets up an `expressMiddleware` property scheduled to run
30 | // before `apostrophe-global`, which is what we want. -Tom
31 | self.expressMiddleware.middleware.push(function(req, res, next) {
32 | if (req.path === self.action + '/workflow-stylesheet') {
33 | return self.workflowStylesheet(req, res);
34 | } else if (req.path === self.action + '/workflow-default-stylesheet') {
35 | return self.workflowDefaultStylesheet(req, res);
36 | }
37 | return next();
38 | });
39 | };
40 |
41 | self.workflowStylesheet = function(req, res) {
42 | var workflow = self.apos.modules['apostrophe-workflow'];
43 | var locale = req.query.locale;
44 | if (!locale) {
45 | return req.res.status(404).send('not found');
46 | }
47 | locale = workflow.liveify(locale);
48 | var stylesheet = workflow.locales[locale] && workflow.locales[locale].stylesheet;
49 | return self.workflowSendStylesheet(req, stylesheet);
50 | };
51 |
52 | self.workflowDefaultStylesheet = function(req, res) {
53 | var workflow = self.apos.modules['apostrophe-workflow'];
54 | var stylesheet = workflow.options.defaultStylesheet;
55 | return self.workflowSendStylesheet(req, stylesheet);
56 | };
57 |
58 | self.workflowSendStylesheet = function(req, stylesheet) {
59 | if (!stylesheet) {
60 | return req.res.status(404).send('not found');
61 | }
62 | var css = self.workflowStylesheetCache[stylesheet];
63 | if (!css) {
64 | var path = self.workflowGetStylesheetPath(stylesheet);
65 | if (!path) {
66 | self.apos.utils.error('stylesheet ' + stylesheet + ' was configured for workflow but does not exist in apostrophe-workflow project level');
67 | return req.res.status(404).send('not found');
68 | }
69 |
70 | css = fs.readFileSync(path, 'utf8');
71 | css = self.prefixCssUrlsWith(css, self.assetUrl(''));
72 | self.workflowStylesheetCache[stylesheet] = css;
73 | }
74 | req.res.set('Content-Type', 'text/css');
75 | // Cache for one year, this is safe because the
76 | // asset generation ID is included in the query string
77 | // of the URL, busting the cache automatically on new deploys
78 | req.res.set('Cache-Control', 'public, max-age=31536000');
79 | return req.res.send(css);
80 | };
81 |
82 | self.workflowGetStylesheetPath = function(stylesheet) {
83 | var workflow = self.apos.modules['apostrophe-workflow'];
84 | var chain = workflow.__meta.chain;
85 | var path;
86 | _.each(chain, function(entry) {
87 | var _path = entry.dirname + '/public/css/' + stylesheet;
88 | if (!_path.match(/\.\w+$/)) {
89 | _path += '.css';
90 | }
91 | if (fs.existsSync(_path)) {
92 | path = _path;
93 | return false;
94 | }
95 | });
96 | return path;
97 | };
98 |
99 | }
100 | };
101 |
--------------------------------------------------------------------------------
/lib/modules/apostrophe-workflow-docs/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | improve: 'apostrophe-docs',
4 |
5 | trashInSchema: true,
6 |
7 | construct: function(self, options) {
8 |
9 | self.apos.define('apostrophe-cursor', require('./lib/cursor.js'));
10 |
11 | var superIsUniqueError = self.isUniqueError;
12 | self.isUniqueError = function(err) {
13 | var result = superIsUniqueError(err);
14 | if (!result) {
15 | return result;
16 | }
17 | if (err && err.message && err.message.match(/workflowGuid/)) {
18 | return false;
19 | }
20 | return result;
21 | };
22 |
23 | var superGetSlugIndexParams = self.getSlugIndexParams;
24 | self.getSlugIndexParams = function() {
25 | var params = superGetSlugIndexParams();
26 | params.workflowLocale = 1;
27 | return params;
28 | };
29 |
30 | var superGetPathLevelIndexParams = self.getPathLevelIndexParams;
31 | self.getPathLevelIndexParams = function() {
32 | var params = superGetPathLevelIndexParams();
33 | params.workflowLocale = 1;
34 | return params;
35 | };
36 |
37 | // Solve chicken and egg problem by making sure we have a
38 | // workflow locale before we test insert permissions
39 |
40 | var superTestInsertPermissions = self.testInsertPermissions;
41 | self.testInsertPermissions = function(req, doc, options) {
42 | var workflow = self.apos.modules['apostrophe-workflow'];
43 | // If not enabled yet, this will be a startup task
44 | if (workflow) {
45 | self.ensureSlug(doc);
46 | workflow.ensureWorkflowLocale(req, doc);
47 | }
48 | return superTestInsertPermissions(req, doc, options);
49 | };
50 |
51 | }
52 | };
53 |
--------------------------------------------------------------------------------
/lib/modules/apostrophe-workflow-docs/lib/cursor.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | construct: function(self, options) {
3 | // npm modules should play nice and not use `alias`
4 | var workflow = self.apos.modules['apostrophe-workflow'];
5 |
6 | // Apostrophe will fetch only documents for the locale of req unless
7 | // explicitly asked to do otherwise via .workflowLocale(null).
8 | // The filter can also be called with a specific locale string.
9 |
10 | self.addFilter('workflowLocale', {
11 | def: true,
12 | finalize: function() {
13 | var setting = self.get('workflowLocale');
14 | if (setting === null) {
15 | return;
16 | }
17 | if (setting === true) {
18 | setting = self.get('req').locale;
19 | if (!setting) {
20 | setting = workflow.defaultLocale;
21 | }
22 | } else {
23 | // It's an explicit locale string
24 | }
25 | if (self.get('type')) {
26 | if (workflow.includeType(self.get('type'))) {
27 | // query is restricted by type to a type that definitely involves workflow
28 | self.and({ workflowLocale: setting });
29 | } else {
30 | // Restricted by type to a type that definitely does not involve workflow,
31 | // no criteria needed
32 | }
33 | } else {
34 | // Content not participating in localization will have no locale at all,
35 | // take care not to block access to that. However we can use `$in`,
36 | // which works just like the equality filter and will accept `null` as
37 | // a match for "property does not exist at all". This avoids a slow
38 | // $or query
39 | self.and({
40 | workflowLocale: {
41 | $in: [ setting, null ]
42 | }
43 | });
44 | }
45 | }
46 | });
47 |
48 | }
49 | };
50 |
--------------------------------------------------------------------------------
/lib/modules/apostrophe-workflow-global/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | improve: 'apostrophe-global'
3 | };
4 |
--------------------------------------------------------------------------------
/lib/modules/apostrophe-workflow-global/public/js/user.js:
--------------------------------------------------------------------------------
1 | apos.define('apostrophe-global', {
2 | alwaysShowEditor: true
3 | });
4 |
--------------------------------------------------------------------------------
/lib/modules/apostrophe-workflow-groups/index.js:
--------------------------------------------------------------------------------
1 | var _ = require('@sailshq/lodash');
2 |
3 | module.exports = {
4 |
5 | improve: 'apostrophe-groups',
6 |
7 | construct: function(self, options) {
8 |
9 | var superModulesReady = self.modulesReady;
10 | self.modulesReady = function() {
11 | // Can't be done sooner because the workflow module has to exist first
12 | superModulesReady();
13 | self.workflowModifyPermissionsField();
14 | };
15 |
16 | self.workflowModifyPermissionsField = function() {
17 | var workflow = self.apos.modules['apostrophe-workflow'];
18 | var permissions = _.find(self.schema, { name: 'permissions' });
19 | if (!permissions) {
20 | return;
21 | }
22 | permissions.type = 'apostrophe-workflow-permissions';
23 | permissions.nestedLocales = workflow.nestedLocales;
24 | permissions.locales = {};
25 | permissions.excludeActions = workflow.excludeActions;
26 | _.each(workflow.locales, function(locale, name) {
27 | permissions.locales[name] = _.pick(locale, 'label');
28 | });
29 | };
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/lib/modules/apostrophe-workflow-images/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | improve: 'apostrophe-images',
3 | construct: function(self, options) {
4 | self.workflowPreview = function(req, before, after) {
5 | return self.render(req, 'workflowPreview', { image: after });
6 | };
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/lib/modules/apostrophe-workflow-images/views/workflowPreview.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/modules/apostrophe-workflow-modified-documents/index.js:
--------------------------------------------------------------------------------
1 | var _ = require('@sailshq/lodash');
2 |
3 | module.exports = {
4 | // A virtual "piece type" only used to display a universal manage modal
5 | // for documents that are commit-ready. No "modified-document" piece is ever
6 | // really inserted into the database. -Tom
7 | extend: 'apostrophe-pieces',
8 | name: 'workflow-document',
9 | label: 'Document',
10 | pluralLabel: 'Documents',
11 | forbiddenFields: [],
12 | sort: {
13 | updatedAt: -1
14 | },
15 | beforeConstruct: function(self, options) {
16 | options.addFilters = [
17 | {
18 | name: 'modified'
19 | },
20 | {
21 | name: 'submitted'
22 | },
23 | {
24 | name: 'type'
25 | }
26 | ].concat(options.addFilters || []);
27 | options.addFields = [
28 | {
29 | name: 'type',
30 | type: 'select',
31 | // Patched after all types are known
32 | choices: []
33 | }
34 | ].concat(options.addFields || []);
35 | // To prevent warnings about unarranged fields. We do not actually
36 | // edit this virtual, read-only piece type
37 | options.arrangeFields = [
38 | {
39 | name: 'type',
40 | fields: [ 'type' ]
41 | }
42 | ].concat(options.arrangeFields || []);
43 | // We do not have fully configurable columns for this custom type,
44 | // but we configure these two so the sorts work
45 | options.defaultColumns = [
46 | {
47 | name: 'updatedAt',
48 | label: 'Last Edit',
49 | sort: {
50 | 'updatedAt': -1
51 | }
52 | },
53 | {
54 | name: 'workflowLastCommitted',
55 | label: 'Last Commit',
56 | sort: {
57 | 'workflowLastCommitted.at': -1
58 | }
59 | }
60 | ];
61 | },
62 | construct: function(self, options) {
63 | var superComposeFilters = self.composeFilters;
64 | self.options.batchOperations = _.filter(self.options.batchOperations, function(operation) {
65 | // Only operations that make sense here should come through
66 | return _.includes([ 'submit', 'commit', 'force-export', 'revert-to-live' ], operation.name);
67 | });
68 | self.composeFilters = function() {
69 | // Move trash filter to end of list
70 | self.options.addFilters = _.filter(self.options.addFilters || [], function(filter) {
71 | return (filter.name !== 'trash');
72 | }).concat(_.filter(self.options.addFilters || [], { name: 'trash' }));
73 | superComposeFilters();
74 | };
75 | self.getManagerControls = function(req) {
76 | // Committables are not real things and cannot be "added"
77 | return [
78 | {
79 | type: 'minor',
80 | label: 'Finished',
81 | action: 'cancel'
82 | }
83 | ];
84 | };
85 | // Clarify that this is not a real piece type to be written to the db, it is
86 | // an interface to "manage" all piece types with just workflow operations
87 | self.addGenerateTask = function() {};
88 | self.insert = function() {
89 | throw 'This is a virtual piece type never stored to the db';
90 | };
91 | self.update = function() {
92 | throw 'This is a virtual piece type never stored to the db';
93 | };
94 | self.on('apostrophe:modulesReady', 'populateTypes', function() {
95 | const field = _.find(self.schema, { name: 'type' });
96 | const types = Object.keys(self.apos.docs.managers);
97 | field.choices = types.map(function(type) {
98 | const manager = self.apos.docs.getManager(type);
99 | let label;
100 | if (self.apos.instanceOf(manager, 'apostrophe-custom-pages')) {
101 | const def = _.find(self.apos.pages.options.types, { name: type });
102 | if (def) {
103 | label = def.label;
104 | }
105 | }
106 | return {
107 | value: type,
108 | label: label || manager.pluralLabel || manager.label || type
109 | };
110 | });
111 | });
112 | self.addToAdminBar = function() {
113 | self.apos.adminBar.add(self.__meta.name, 'Manage');
114 | const group = _.find(self.apos.adminBar.groups, { label: 'Workflow' });
115 | if (group) {
116 | group.items.push(self.__meta.name);
117 | }
118 | };
119 | // Since this is an overview of many doc types, all that is required
120 | // is that you be able to potentially edit at least one doc type that
121 | // is subject to workflow
122 | self.requireEditor = function(req, res, next) {
123 | if (!_.find(Object.keys(self.apos.docs.managers), function(name) {
124 | return self.apos.modules['apostrophe-workflow'].includeType(name) && self.apos.permissions.can(req, 'edit-' + name);
125 | })) {
126 | return self.apiResponse(res, 'forbidden');
127 | }
128 | return next();
129 | };
130 | var superGetListProjection = self.getListProjection;
131 | self.getListProjection = function(req) {
132 | var projection = superGetListProjection(req);
133 | projection = Object.assign(
134 | {
135 | workflowLocale: 1,
136 | workflowModified: 1,
137 | workflowSubmitted: 1,
138 | workflowLastCommitted: 1
139 | },
140 | projection
141 | );
142 | return projection;
143 | };
144 | }
145 | };
146 |
--------------------------------------------------------------------------------
/lib/modules/apostrophe-workflow-modified-documents/lib/cursor.js:
--------------------------------------------------------------------------------
1 | const _ = require('@sailshq/lodash');
2 |
3 | module.exports = {
4 | afterConstruct: function(self) {
5 |
6 | const req = self.get('req');
7 |
8 | // We do not care what the document type is,
9 | // by default (although the type filter may be
10 | // used as an actual ui filter)
11 | self.type(null);
12 |
13 | // OK I lied, we care a lot, but specifically it must be a type
14 | // to which workflow applies
15 |
16 | const workflow = self.apos.modules['apostrophe-workflow'];
17 |
18 | const types = Object.keys(self.apos.docs.managers).filter(function(type) {
19 | // Type must be subject to workflow, and user must be able to
20 | // commit (edit the live locale). The trash type we're ignoring
21 | // is the legacy trashcan, not your trashed docs
22 | return (type !== 'trash') && workflow.includeType(type) && self.apos.permissions.can(Object.assign({}, req, { workflowLocale: workflow.liveify(req.locale) }), 'edit-' + type);
23 | });
24 | if (!types.length) {
25 | self.and({ _id: '__iNeverMatch' });
26 | return;
27 | }
28 | self.and({
29 | workflowLocale: { $exists: 1 },
30 | type: { $in: types }
31 | });
32 | self.permission('edit');
33 | },
34 | construct: function(self, options) {
35 | self.apos.schemas.addFilters(_.filter(self.options.module.schema, { name: 'type' }), {
36 | override: [ 'type' ]
37 | }, self);
38 | self.addFilter('modified', {
39 | def: null,
40 | finalize: function() {
41 | const modified = self.get('modified');
42 | if (modified === null) {
43 | return;
44 | }
45 | if (modified) {
46 | self.and({
47 | workflowModified: true
48 | });
49 | } else {
50 | self.and({
51 | workflowModified: { $ne: true }
52 | });
53 | }
54 | },
55 | safeFor: 'manage',
56 | launder: function(s) {
57 | return self.apos.launder.booleanOrNull(s);
58 | },
59 | choices: function(callback) {
60 | var choices = [
61 | {
62 | value: '0',
63 | label: 'No'
64 | },
65 | {
66 | value: '1',
67 | label: 'Yes'
68 | }
69 | ];
70 | return setImmediate(function() {
71 | return callback(null, choices);
72 | });
73 | }
74 | });
75 | self.addFilter('submitted', {
76 | def: null,
77 | finalize: function() {
78 | const submitted = self.get('submitted');
79 | if (submitted === null) {
80 | return;
81 | }
82 | if (submitted) {
83 | self.and({
84 | workflowSubmitted: { $exists: 1 }
85 | });
86 | return;
87 | }
88 | self.and({
89 | workflowSubmitted: { $exists: 0 }
90 | });
91 | },
92 | safeFor: 'manage',
93 | launder: function(s) {
94 | return self.apos.launder.booleanOrNull(s);
95 | },
96 | choices: function(callback) {
97 | var choices = [
98 | {
99 | value: '0',
100 | label: 'No'
101 | },
102 | {
103 | value: '1',
104 | label: 'Yes'
105 | }
106 | ];
107 | return setImmediate(function() {
108 | return callback(null, choices);
109 | });
110 | }
111 | });
112 | }
113 | };
114 |
--------------------------------------------------------------------------------
/lib/modules/apostrophe-workflow-modified-documents/public/css/manager.less:
--------------------------------------------------------------------------------
1 | .apos-ui.apos-modal.workflow-document-manager .apos-modal-header .apos-modal-filters .apos-modal-filter {
2 | margin-right: 20px;
3 | }
4 |
5 |
--------------------------------------------------------------------------------
/lib/modules/apostrophe-workflow-modified-documents/public/js/manager-modal.js:
--------------------------------------------------------------------------------
1 | apos.define('apostrophe-workflow-modified-documents-manager-modal', {
2 | extend: 'apostrophe-pieces-manager-modal',
3 | construct: function(self, option) {
4 | self.onChange = function() {
5 | // We refresh list view on *all* doc changes, not just one piece type
6 | self.refresh();
7 | };
8 | var superBeforeShow = self.beforeShow;
9 | self.beforeShow = function(callback) {
10 | // Watch for more types of changes that should refresh the list
11 | apos.on('workflowSubmitted', self.onChange);
12 | apos.on('workflowCommitted', self.onChange);
13 | apos.on('workflowRevertedToLive', self.onChange);
14 | return superBeforeShow(callback);
15 | };
16 | var superAfterHide = self.afterHide;
17 | self.afterHide = function() {
18 | superAfterHide();
19 | // So we don't leak memory and keep refreshing
20 | // after we're gone
21 | apos.off('workflowSubmitted', self.onChange);
22 | apos.off('workflowCommitted', self.onChange);
23 | apos.off('workflowRevertedToLive', self.onChange);
24 | };
25 | }
26 | });
27 |
--------------------------------------------------------------------------------
/lib/modules/apostrophe-workflow-modified-documents/views/manageHeadings.html:
--------------------------------------------------------------------------------
1 | {%- import "apostrophe-ui:components/fields.html" as fields -%}
2 | {%- macro renderColumn(column) -%}
3 |
{{ __ns('apostrophe', column.label) }}
4 | {%- endmacro -%}
5 |
6 | {{fields.checkbox('select-all')}}
7 | {{ __ns('apostrophe', 'Document') }}
8 | {{ __ns('apostrophe', 'Type') }}
9 | {{ renderColumn({ name: 'updatedAt', label: 'Last Edit', sort: { updatedAt: -1 }, defaultSortDirection: '-1' }) }}
10 | {{ __ns('apostrophe', 'Submitted?') }}
11 | {{ renderColumn({ name: 'workflowLastCommitted', label: 'Last Commit', sort: { workflowLastCommitted: -1 }, defaultSortDirection: '-1' }) }}
12 | {{ __ns('apostrophe', 'Actions') }}
13 |
14 |
--------------------------------------------------------------------------------
/lib/modules/apostrophe-workflow-modified-documents/views/manageListPage.html:
--------------------------------------------------------------------------------
1 | {%- import "apostrophe-ui:components/fields.html" as fields -%}
2 | {% for piece in data.pieces %}
3 |
4 | {{fields.checkbox(data.options.name + '-select')}}
5 | {% if apos.utils.beginsWith(piece.slug, '/') %}
6 | {{ piece.title or piece.slug }}
7 | {% else %}
8 | {{ piece.title or piece.slug }}
9 | {% endif %}
10 | {{ piece.type }}
11 | {{ piece.updatedAt | date('YYYY-MM-DD') }} {{ piece.workflowLastEditor }}
12 | {% if piece.workflowSubmitted %}{{ __ns('apostrophe', 'Yes') }}{% else %}{{ __ns('apostrophe', 'No') }}{% endif %}
13 | {{ piece.workflowLastCommitted.at | date('YYYY-MM-DD') }} {{ piece.workflowLastCommitted.user.title }}
14 |
15 | {% if piece._edit %}
16 | {% if apos.utils.beginsWith(piece.slug, '/') %}
17 | {{ __ns('apostrophe', 'Edit') }}
18 | {% else %}
19 | {{ __ns('apostrophe', 'Edit') }}
20 | {% endif %}
21 | {% if apos.modules['apostrophe-workflow'].committable(piece) %}
22 | {{ __ns('apostrophe', 'Commit') }}
23 | {{ __ns('apostrophe', 'Revert') }}
24 | {% endif %}
25 | {% endif %}
26 |
27 |
28 | {% endfor %}
29 |
--------------------------------------------------------------------------------
/lib/modules/apostrophe-workflow-modified-documents/views/managerModal.html:
--------------------------------------------------------------------------------
1 | {% extends "managerModalBase.html" %}
2 | {% block label %}
3 | Manage Workflow
4 | {% endblock %}
5 | {% block instructions %}
6 | Tip: looking for documents that need to be committed?
7 | Use the "Modified" filter.
8 | {% endblock %}
9 |
10 |
--------------------------------------------------------------------------------
/lib/modules/apostrophe-workflow-pages/index.js:
--------------------------------------------------------------------------------
1 | var _ = require('@sailshq/lodash');
2 | var async = require('async');
3 |
4 | module.exports = {
5 |
6 | improve: 'apostrophe-pages',
7 |
8 | beforeConstruct: function(self, options) {
9 |
10 | options.addBatchOperations = [
11 | {
12 | name: 'submit',
13 | route: 'apostrophe-workflow:submit',
14 | label: 'Submit',
15 | buttonLabel: 'Submit'
16 | },
17 | {
18 | name: 'commit',
19 | route: 'apostrophe-workflow:batch-commit',
20 | label: 'Commit',
21 | buttonLabel: 'Commit'
22 | },
23 | {
24 | name: 'force-export',
25 | route: 'apostrophe-workflow:batch-force-export',
26 | label: 'Force Export',
27 | buttonLabel: 'Force Export'
28 | }
29 | ].concat(options.addBatchOperations || []);
30 |
31 | },
32 |
33 | construct: function(self, options) {
34 | self.on('apostrophe:modulesReady', 'setAutoCommitPageMoves', function() {
35 | self.autoCommitPageMoves = self.apos.modules['apostrophe-workflow']
36 | .options.autoCommitPageMoves || false;
37 | });
38 |
39 | var superGetPathIndexParams = self.getPathIndexParams;
40 | self.getPathIndexParams = function() {
41 | var params = superGetPathIndexParams();
42 | params.workflowLocaleForPathIndex = 1;
43 | return params;
44 | };
45 |
46 | var superRemoveTrailingSlugSlashes = self.removeTrailingSlugSlashes;
47 | self.removeTrailingSlugSlashes = function(req, slug) {
48 | if (arguments.length === 1) {
49 | // bc workaround
50 | slug = req;
51 | req = self.apos.tasks.getAnonReq();
52 | }
53 | var workflow = self.apos.modules['apostrophe-workflow'];
54 | if (!workflow.prefixes) {
55 | return superRemoveTrailingSlugSlashes(slug);
56 | }
57 | var locale = workflow.liveify(req.locale);
58 | if (_.has(workflow.prefixes, locale)) {
59 | var matches = slug.match(/^(\/[^/]+)(\/?)$/);
60 | if (matches && (workflow.prefixes[locale] === matches[1])) {
61 | // Something like /en/, leave it alone,
62 | // it's a localized homepage. However if the
63 | // trailing slash *after* the locale is missing,
64 | // add it and redirect
65 | if (matches[2] === '') {
66 | return slug + '/';
67 | } else {
68 | return slug;
69 | }
70 | }
71 | }
72 | return superRemoveTrailingSlugSlashes(slug);
73 | };
74 |
75 | var superPruneCurrentPageForBrowser = self.pruneCurrentPageForBrowser;
76 | self.pruneCurrentPageForBrowser = function(page) {
77 | var pruned = superPruneCurrentPageForBrowser(page);
78 | pruned.workflowLocale = page.workflowLocale;
79 | pruned.workflowGuid = page.workflowGuid;
80 | return pruned;
81 | };
82 |
83 | var superGetEditControls = self.getEditControls;
84 | self.getEditControls = function(req) {
85 | return upgradeControls(req, superGetEditControls(req), 'edit');
86 | };
87 |
88 | var superGetCreateControls = self.getCreateControls;
89 | self.getCreateControls = function(req) {
90 | return upgradeControls(req, superGetCreateControls(req), 'create');
91 | };
92 |
93 | function upgradeControls(req, controls, verb) {
94 | var workflow = self.apos.modules['apostrophe-workflow'];
95 | if (!workflow.includeType(self.name)) {
96 | // Not subject to workflow
97 | return controls;
98 | }
99 | var save = _.find(controls, { action: 'save' });
100 | if (save) {
101 | save.label = 'Save Draft';
102 | }
103 | controls.push({
104 | type: 'dropdown',
105 | label: 'Workflow',
106 | name: 'workflow',
107 | dropdownOptions: {
108 | direction: 'down'
109 | },
110 | items: [
111 | {
112 | label: 'Submit',
113 | action: 'workflow-submit'
114 | },
115 | {
116 | label: 'Commit',
117 | action: 'workflow-commit'
118 | }
119 | ].concat(
120 | (workflow.localized && (verb === 'edit'))
121 | ? [
122 | {
123 | label: 'History and Export',
124 | action: 'workflow-history'
125 | }
126 | ] : [
127 | {
128 | label: 'History',
129 | action: 'workflow-history'
130 | }
131 | ]
132 | ).concat(workflow.localized
133 | ? [
134 | {
135 | label: 'Force Export',
136 | action: 'workflow-force-export'
137 | },
138 | {
139 | label: 'Force Export Related',
140 | action: 'workflow-force-export-related'
141 | }
142 | ] : []
143 | )
144 | });
145 | return controls;
146 | }
147 |
148 | // On invocation of `apos.pages.move`, modify the criteria and filters
149 | // to ensure only the relevant locale is in play
150 |
151 | var superBeforeMove = self.beforeMove;
152 | self.beforeMove = function(req, moved, target, position, options, callback) {
153 | return superBeforeMove(req, moved, target, position, options, function(err) {
154 | if (err) {
155 | return callback(err);
156 | }
157 | if (moved.workflowLocale) {
158 | options.criteria = _.assign({}, options.criteria || {}, { workflowLocale: moved.workflowLocale });
159 | options.filters = _.assign({}, options.filters || {}, { workflowLocale: moved.workflowLocale });
160 | }
161 | return callback(null);
162 | });
163 | };
164 |
165 | // After a page is moved in one locale, record the action that
166 | // was taken so it can be repeated on a commit or export
167 | // without attempting to reconcile differences in where the
168 | // destination parent page happens to be at the start
169 | // of the operation.
170 | // If the autoCommitPageMoves option has been set, we move the live version
171 | // when we move the draft one, in order to avoid any page tree conflicts.
172 |
173 | var superAfterMove = self.afterMove;
174 | self.afterMove = function(req, moved, info, callback) {
175 | return superAfterMove(req, moved, info, function(err) {
176 | const isDraft = moved.workflowLocale.includes('-draft');
177 |
178 | if (err) {
179 | return callback(err);
180 | }
181 | return self.apos.docs.db.update({
182 | _id: moved._id
183 | }, {
184 | $set: {
185 | workflowModified: true,
186 | workflowMoved: {
187 | target: info.target.workflowGuid,
188 | position: info.position
189 | },
190 | ...isDraft && { workflowMovedIsNew: true }
191 | }
192 | }, (err) => {
193 | if (err || !isDraft || !self.autoCommitPageMoves) {
194 | return callback(err || null);
195 | }
196 |
197 | self.apos.workflow.getDraftAndLive(req, moved._id, {}, (err, draft, live) => {
198 | if (err) {
199 | return callback(err);
200 | }
201 |
202 | return self.apos.workflow.repeatMove(req, draft, live, callback);
203 | });
204 | });
205 | });
206 | };
207 |
208 | // `implementParkAll` must be reimplemented to cover all of
209 | // the locales, and to first invoke the add-missing-locales and
210 | // add-locale-prefixes logic, if needed
211 |
212 | self.implementParkAll = function(callback) {
213 | if (
214 | (self.apos.argv._[0] === 'apostrophe-workflow:add-missing-locales') ||
215 | (self.apos.argv._[0] === 'apostrophe-workflow:add-locale-prefixes') ||
216 | (self.apos.argv._[0] === 'apostrophe-workflow:remove-numbered-parked-pages') ||
217 | (self.apos.argv._[0] === 'apostrophe-workflow:harmonize-workflow-guids-by-parked-id')) {
218 | // If we park pages in this scenario, we'll wind up with
219 | // duplicate pages in the new locales after the task
220 | // fills them in
221 | return setImmediate(callback);
222 | }
223 |
224 | var workflow = self.apos.modules['apostrophe-workflow'];
225 | return async.series([
226 | workflow.updatePerConfiguration,
227 | parkBody
228 | ], function(err) {
229 | return callback(err);
230 | });
231 |
232 | function parkBody(callback) {
233 | var workflow = self.apos.modules['apostrophe-workflow'];
234 | var locales = _.keys(workflow.locales);
235 | return async.eachSeries(locales, function(locale, callback) {
236 | var parked = _.cloneDeep(self.parked);
237 | if (workflow.prefixes) {
238 | fixPrefixes(parked, locale);
239 | }
240 |
241 | var req = self.apos.tasks.getReq();
242 | req.locale = locale;
243 |
244 | return async.eachSeries(parked, function(item, callback) {
245 | convertToLocalizedSlug(item, locale);
246 | return self.implementParkOne(req, item, callback);
247 | }, callback);
248 |
249 | function convertToLocalizedSlug (item, locale) {
250 | if (locale) {
251 | var liveLocale = workflow.liveify(locale);
252 | if (typeof item.slug === 'object') {
253 | if (!item.slug[liveLocale] && !item.slug._default) {
254 | throw new Error('apostrophe-workflow: mising _default value in localized parked page');
255 | }
256 | item.slug = item.slug[liveLocale] || item.slug._default;
257 | if (item.parkedId && item.parkedId === 'home') {
258 | throw new Error('apostrophe-workflow: the homepage cannot have a localized slug');
259 | }
260 | }
261 |
262 | if (item._children) {
263 | _.each(item._children, function(child) {
264 | convertToLocalizedSlug(child, liveLocale);
265 | });
266 | }
267 | }
268 | }
269 |
270 | function fixPrefixes(parked, locale) {
271 | var prefix = workflow.prefixes[workflow.liveify(locale)];
272 | if (!prefix) {
273 | return;
274 | }
275 | _.each(parked, function(item) {
276 | if (item.slug === '/') {
277 | // Hint to the implementParkOne implementation that
278 | // this is still a homepage even if its slug is
279 | // no longer / due to locale prefixes
280 | item.level = 0;
281 | }
282 | if (item.parent) {
283 | item.parent = prefix + item.parent;
284 | } else if (item.level !== 0) {
285 | item.parent = prefix + '/';
286 | }
287 | convertToLocalizedSlug(item, locale);
288 | item.slug = prefix + item.slug;
289 | fixPrefixes(item._children || [], locale);
290 | });
291 | }
292 | }, callback);
293 | }
294 | };
295 |
296 | var superGetBaseUrl = self.getBaseUrl;
297 | // Return the appropriate base URL for constructing absolute
298 | // URLs in the relevant locale, if the `hostnames` option is
299 | // in play for this locale, otherwise fall back to the
300 | // standard behavior
301 | self.getBaseUrl = function(req) {
302 | var workflow = self.apos.modules['apostrophe-workflow'];
303 | var live = workflow.liveify(req.locale || workflow.defaultLocale);
304 | if (!(workflow.hostnames && workflow.hostnames[live])) {
305 | return superGetBaseUrl(req);
306 | }
307 | var url = req.protocol + '://' + workflow.hostnames[live];
308 | // If the request URL was on a particular port, the
309 | // new URL should be on that port too. Helpful for
310 | // debugging locally
311 | var matches = (req.get('host') || '').match(/:(\d+$)/);
312 | var port;
313 | if (matches) {
314 | port = matches[1];
315 | if (((req.protocol === 'http') && (port !== '80')) ||
316 | ((req.protocol === 'https') && (port !== '443'))) {
317 | url += ':' + port;
318 | }
319 | }
320 | return url;
321 | };
322 |
323 | var superGetInfoProjection = self.getInfoProjection;
324 | self.getInfoProjection = function(req, cursor) {
325 | var projection = superGetInfoProjection(req);
326 | projection = Object.assign(
327 | {
328 | workflowLocale: 1,
329 | workflowModified: 1,
330 | workflowSubmitted: 1,
331 | workflowLastCommitted: 1
332 | },
333 | projection
334 | );
335 | return projection;
336 | };
337 |
338 | }
339 |
340 | };
341 |
--------------------------------------------------------------------------------
/lib/modules/apostrophe-workflow-pages/public/js/editor.js:
--------------------------------------------------------------------------------
1 | // Extend the pieces editor modal to implement workflow
2 |
3 | apos.define('apostrophe-pages-editor', {
4 | construct: function(self, options) {
5 | var superBeforeShow = self.beforeShow;
6 | self.beforeShow = function(callback) {
7 | self.link('apos-workflow-submit', function() {
8 | self.submitting = true;
9 | return self.save(function(err) {
10 | if (err) {
11 | self.submitting = false;
12 |
13 | }
14 | // Never reached due to redirect
15 | });
16 | });
17 | self.link('apos-workflow-commit', function() {
18 | self.committing = true;
19 | apos.notify('The page has been created and saved.', { type: 'success', dismiss: true });
20 | return self.save(function(err) {
21 | if (err) {
22 | self.committing = false;
23 |
24 | }
25 | // Never reached due to redirect
26 | });
27 | });
28 | self.link('apos-workflow-force-export', function() {
29 | self.forceExporting = true;
30 | return self.save(function(err) {
31 | if (err) {
32 | self.forceExporting = false;
33 |
34 | }
35 | // Never reached due to redirect
36 | });
37 | });
38 | self.link('apos-workflow-force-export-related', function() {
39 | self.forceExportingRelated = true;
40 | return self.save(function(err) {
41 | if (err) {
42 | self.forceExportingRelated = false;
43 | }
44 | // Never reached due to redirect
45 | });
46 | });
47 | self.link('apos-workflow-history', function() {
48 | return apos.modules['apostrophe-workflow'].history(self.page._id, callback);
49 | });
50 | self.workflowControlsVisibility();
51 | return superBeforeShow(callback);
52 | };
53 |
54 | self.workflowControlsVisibility = function() {
55 | var workflow = apos.modules['apostrophe-workflow'];
56 | var args = {
57 | type: self.page.type
58 | };
59 | if (self.verb === 'update') {
60 | args.id = self.page._id;
61 | }
62 | return workflow.api('committable', args, function(results) {
63 | if (results.status === 'ok') {
64 | self.$controls.addClass('apos-workflow-committable');
65 | }
66 | });
67 | };
68 |
69 | self.afterSave = function(callback) {
70 | if (self.submitting) {
71 | return apos.modules['apostrophe-workflow'].submit([ self.savedPage._id ], callback);
72 | }
73 | if (self.committing) {
74 | return apos.modules['apostrophe-workflow'].commit([ self.savedPage._id ], callback);
75 | }
76 | if (self.forceExporting) {
77 | return apos.modules['apostrophe-workflow'].forceExport(self.savedPage._id, callback);
78 | }
79 | if (self.forceExportingRelated) {
80 | return apos.modules['apostrophe-workflow'].forceExportRelated(self.savedPage._id, callback);
81 | }
82 | if (self.accessingHistory) {
83 | return apos.modules['apostrophe-workflow'].history(self.page._id, callback);
84 | }
85 | return setImmediate(callback);
86 | };
87 | }
88 | });
89 |
--------------------------------------------------------------------------------
/lib/modules/apostrophe-workflow-pages/public/js/reorganize.js:
--------------------------------------------------------------------------------
1 | apos.define('apostrophe-pages-reorganize', {
2 |
3 | construct: function(self, options) {
4 |
5 | var workflow = apos.modules['apostrophe-workflow'];
6 |
7 | self.batchSubmit = function() {
8 | return self.batchSimple(
9 | 'submit',
10 | "Are you sure you want to submit " + self.choices.length + " page(s)?",
11 | {}
12 | );
13 | };
14 |
15 | self.batchCommit = function() {
16 | return self.batchSimple(
17 | 'commit',
18 | "Are you sure you want to commit " + self.choices.length + " page(s)?",
19 | {
20 | success: function(results, callback) {
21 | return workflow.batchExport(_.values(results), callback);
22 | }
23 | }
24 | );
25 | };
26 |
27 | self.batchForceExport = function() {
28 | return self.batchSimple(
29 | 'force-export',
30 | "Are you sure you want to force export " + self.choices.length + " page(s)?",
31 | {
32 | dataSource: workflow.batchForceExportGetLocales,
33 | success: function(result, callback) {
34 | workflow.presentBatchExportResult(result);
35 | return callback(null);
36 | }
37 | }
38 | );
39 | };
40 | }
41 | });
42 |
--------------------------------------------------------------------------------
/lib/modules/apostrophe-workflow-permissions/index.js:
--------------------------------------------------------------------------------
1 | var _ = require('@sailshq/lodash');
2 |
3 | module.exports = {
4 |
5 | improve: 'apostrophe-permissions',
6 |
7 | afterConstruct: function(self) {
8 | self.workflowAddPermissions();
9 | self.apos.on('can', _.partial(self.workflowOnPermissions, 'can'));
10 | self.apos.on('criteria', _.partial(self.workflowOnPermissions, 'criteria'));
11 | },
12 |
13 | construct: function(self, options) {
14 |
15 | self.workflowAddPermissions = function() {
16 | self.add({
17 | value: 'private-locales',
18 | label: 'View Private Locales'
19 | });
20 | };
21 |
22 | self.workflowOnPermissions = function(event, req, action, object, info) {
23 |
24 | var workflow;
25 | workflow = self.apos.modules['apostrophe-workflow'];
26 |
27 | if (!workflow) {
28 | // Workflow module not initialized yet
29 | return;
30 | }
31 |
32 | // Types excluded from workflow should not be subject to any of this
33 | if (!workflow.includeType(info.type)) {
34 | return;
35 | }
36 |
37 | // Flunk any access to a nonexistent locale or, if
38 | // we don't have the private-locale permission,
39 | // any access to a private locale
40 | var locale = workflow.locales[req.locale || workflow.defaultLocale];
41 | if (
42 | (!locale) ||
43 | (locale.private &&
44 | ((!req.user) || (
45 | (!req.user._permissions['private-locales']) &&
46 | (!req.user._permissions['admin'])
47 | ))
48 | )
49 | ) {
50 | info.response = info._false;
51 | return;
52 | }
53 |
54 | if (_.contains(workflow.excludeActions, action)) {
55 | return;
56 | }
57 | if (!info.type) {
58 | return;
59 | }
60 | var manager = self.apos.docs.getManager(info.type);
61 | if (!manager) {
62 | return;
63 | }
64 | var verb = info.verb;
65 | // publish is not a separate verb in workflow since we already control whether you can edit
66 | // in draft vs. live locales
67 | if (verb === 'publish') {
68 | verb = 'edit';
69 | action = action.replace(/^publish-/, 'edit-');
70 | }
71 | if (!_.contains(workflow.includeVerbs, verb)) {
72 | return;
73 | }
74 | if (req.user && req.user._permissions.admin) {
75 | // Sitewide admins aren't restricted by locale because they can edit
76 | // groups, which would allow them to defeat that anyway
77 | return;
78 | }
79 | if (manager.isAdminOnly && manager.isAdminOnly()) {
80 | info.response = info._false;
81 | return;
82 | }
83 |
84 | // OK, now we know this is something we're entitled to an opinion about
85 |
86 | // Rebuild the action string using the effective verb and type name
87 | action = info.verb + '-' + info.type;
88 |
89 | // publish is not a separate verb in workflow since we already control whether you can edit
90 | // in draft vs. live locales
91 | if (info.verb === 'publish') {
92 | action = action.replace(/^publish-/, 'edit-');
93 | }
94 |
95 | if (!(req.user && req.user._permissionsLocales)) {
96 | info.response = info._false;
97 | return;
98 | }
99 |
100 | var adminAction = 'admin-' + info.type;
101 |
102 | // 'VERB', 'VERB-this-type' or 'admin-this-type' is acceptable
103 | var permissionsLocales = _.assign({},
104 | req.user._permissionsLocales[action] || {},
105 | req.user._permissionsLocales[adminAction] || {},
106 | req.user._permissionsLocales[verb] || {}
107 | );
108 |
109 | if (_.isEmpty(permissionsLocales)) {
110 | info.response = info._false;
111 | return;
112 | }
113 |
114 | if (event === 'criteria') {
115 | info.response = { $and: [ info.response, { workflowLocale: { $in: _.keys(permissionsLocales) } } ] };
116 | } else {
117 | object = info.object || info.newObject;
118 | if (object) {
119 | if (!permissionsLocales[object.workflowLocale]) {
120 | info.response = info._false;
121 | }
122 | } else if (!permissionsLocales[req.locale]) {
123 | info.response = info._false;
124 | }
125 | }
126 |
127 | };
128 |
129 | }
130 | };
131 |
--------------------------------------------------------------------------------
/lib/modules/apostrophe-workflow-pieces/index.js:
--------------------------------------------------------------------------------
1 | var _ = require('@sailshq/lodash');
2 |
3 | module.exports = {
4 |
5 | improve: 'apostrophe-pieces',
6 |
7 | canEditTrash: true,
8 |
9 | beforeConstruct: function(self, options) {
10 |
11 | function onlyIf(type) {
12 | return self.apos.modules['apostrophe-workflow'].includeType(type);
13 | }
14 |
15 | options.addBatchOperations = [
16 | {
17 | name: 'submit',
18 | route: 'apostrophe-workflow:submit',
19 | label: 'Submit',
20 | buttonLabel: 'Submit',
21 | onlyIf: onlyIf
22 | },
23 | {
24 | name: 'commit',
25 | route: 'apostrophe-workflow:batch-commit',
26 | label: 'Commit',
27 | buttonLabel: 'Commit',
28 | onlyIf: onlyIf
29 | },
30 | {
31 | name: 'force-export',
32 | route: 'apostrophe-workflow:batch-force-export',
33 | label: 'Force Export',
34 | buttonLabel: 'Force Export',
35 | onlyIf: onlyIf
36 | },
37 | {
38 | name: 'revert-to-live',
39 | route: 'apostrophe-workflow:batch-revert-to-live',
40 | label: 'Revert',
41 | buttonLabel: 'Revert to Live',
42 | onlyIf: onlyIf
43 | }
44 | ].concat(options.addBatchOperations || []);
45 |
46 | options.addColumns = [
47 | {
48 | name: 'workflowLastCommitted',
49 | label: 'Last Commit',
50 | sort: {
51 | 'workflowLastCommitted.at': -1
52 | },
53 | partial: function(value) {
54 | return self.partial('workflowLastCommitted', { value: value });
55 | }
56 | }
57 | ].concat(options.addColumns || []);
58 |
59 | },
60 |
61 | construct: function(self, options) {
62 |
63 | var superGetEditControls = self.getEditControls;
64 | self.getEditControls = function(req) {
65 | return upgradeControls(req, superGetEditControls(req), 'edit');
66 | };
67 |
68 | var superGetCreateControls = self.getCreateControls;
69 | self.getCreateControls = function(req) {
70 | return upgradeControls(req, superGetCreateControls(req), 'create');
71 | };
72 |
73 | function upgradeControls(req, controls, verb) {
74 | var workflow = self.apos.modules['apostrophe-workflow'];
75 | if (!workflow.includeType(self.name)) {
76 | // Not subject to workflow
77 | return controls;
78 | }
79 | var save = _.find(controls, { action: 'save' });
80 | if (save) {
81 | save.label = 'Save Draft';
82 | }
83 |
84 | controls.push({
85 | type: 'dropdown',
86 | label: 'Workflow',
87 | name: 'workflow',
88 | dropdownOptions: {
89 | direction: 'down'
90 | },
91 | // Frontend takes care of visibility decisions for these
92 | items: [
93 | {
94 | label: 'Submit',
95 | action: 'workflow-submit'
96 | },
97 | {
98 | label: 'Commit',
99 | action: 'workflow-commit'
100 | }
101 | ].concat((verb === 'edit')
102 | ? [
103 | {
104 | label: 'History',
105 | action: 'workflow-history'
106 | }
107 | ] : []
108 | ).concat(workflow.localized
109 | ? [
110 | {
111 | label: 'Force Export',
112 | action: 'workflow-force-export'
113 | },
114 | {
115 | label: 'Force Export Related',
116 | action: 'workflow-force-export-related'
117 | }
118 | ] : []
119 | )
120 | });
121 | return controls;
122 | }
123 | }
124 | };
125 |
--------------------------------------------------------------------------------
/lib/modules/apostrophe-workflow-pieces/public/js/editor-modal.js:
--------------------------------------------------------------------------------
1 | // Extend the pieces editor modal to implement workflow
2 |
3 | apos.define('apostrophe-pieces-editor-modal', {
4 | construct: function(self, options) {
5 |
6 | var superBeforeShow = self.beforeShow;
7 | self.beforeShow = function(callback) {
8 | self.link('apos-workflow-submit', function() {
9 | return self.workflowSaveThen(function(callback) {
10 | return apos.modules['apostrophe-workflow'].submit([ self.savedPiece._id ], callback);
11 | });
12 | });
13 | self.link('apos-workflow-commit', function() {
14 | var workflow = apos.modules['apostrophe-workflow'];
15 | return self.workflowSaveThen(function(callback) {
16 | apos.ui.globalBusy(true);
17 | return workflow.getEditable({ ids: [ self.savedPiece._id ], related: true }, function(err, result) {
18 | apos.ui.globalBusy(false);
19 | if (!err) {
20 | workflow.commit(_.intersection(result.committable, result.modified), { leadId: self.savedPiece._id }, callback);
21 | }
22 | });
23 | });
24 | });
25 | self.link('apos-workflow-force-export', function() {
26 | return self.workflowSaveThen(function(callback) {
27 | return apos.modules['apostrophe-workflow'].forceExport(self.savedPiece._id, callback);
28 | });
29 | });
30 | self.link('apos-workflow-force-export-related', function() {
31 | return self.workflowSaveThen(function(callback) {
32 | return apos.modules['apostrophe-workflow'].forceExportRelated(self.savedPiece._id, callback);
33 | });
34 | });
35 | self.link('apos-workflow-history', function() {
36 | if (!self._id) {
37 | return;
38 | }
39 | return apos.modules['apostrophe-workflow'].history(self._id);
40 | });
41 | self.workflowControlsVisibility();
42 | return superBeforeShow(callback);
43 | };
44 |
45 | // Save the modal normally, then invoke the given callback.
46 | // The callback is invoked only if the modal is saved successfully
47 |
48 | self.workflowSaveThen = function(callback) {
49 | self.workflowBeforeDisplayResponse = callback;
50 | return self.save(function(err) {
51 | if (err) {
52 | self.apos.utils.error(err);
53 | }
54 | self.workflowBeforeDisplayResponse = null;
55 | });
56 | };
57 |
58 | var superDisplayResponse = self.displayResponse;
59 |
60 | // Invoke `workflowBeforeDisplayResponse` if present, then
61 | // respond to the saving of the piece in the normal way
62 | self.displayResponse = function(result, callback) {
63 | if (!self.workflowBeforeDisplayResponse) {
64 | return superDisplayResponse(result, callback);
65 | }
66 | return self.workflowBeforeDisplayResponse(function(err) {
67 | if (err) {
68 | self.apos.utils.error(err);
69 | return callback(err);
70 | }
71 | return superDisplayResponse(result, callback);
72 | });
73 | };
74 |
75 | self.workflowControlsVisibility = function() {
76 | var workflow = apos.modules['apostrophe-workflow'];
77 | return workflow.api('committable', { type: self.name, id: self._id }, function(results) {
78 | if (results.status === 'ok') {
79 | self.$controls.addClass('apos-workflow-committable');
80 | }
81 | });
82 | };
83 | }
84 | });
85 |
--------------------------------------------------------------------------------
/lib/modules/apostrophe-workflow-pieces/public/js/manager-modal.js:
--------------------------------------------------------------------------------
1 | apos.define('apostrophe-pieces-manager-modal', {
2 |
3 | construct: function(self, options) {
4 |
5 | var workflow = apos.modules['apostrophe-workflow'];
6 |
7 | self.batchSubmit = function() {
8 | return self.batchSimple(
9 | 'submit',
10 | "Are you sure you want to submit " + self.choices.length + " item(s)?",
11 | {}
12 | );
13 | };
14 |
15 | self.batchCommit = function() {
16 | return self.batchSimple(
17 | 'commit',
18 | "Are you sure you want to commit " + self.choices.length + " item(s)?",
19 | {
20 | success: function(results, callback) {
21 | if (workflow.options.exportAfterCommit !== false) {
22 | return workflow.batchExport(_.values(results), callback);
23 | } else {
24 | return callback(null);
25 | }
26 | }
27 | }
28 | );
29 | };
30 |
31 | self.batchForceExport = function() {
32 | return self.batchSimple(
33 | 'force-export',
34 | "Are you sure you want to force export " + self.choices.length + " item(s)?",
35 | {
36 | dataSource: workflow.batchForceExportGetLocales,
37 | success: function(result, callback) {
38 | workflow.presentBatchExportResult(result);
39 | return callback(null);
40 | }
41 | }
42 | );
43 | };
44 |
45 | self.batchRevertToLive = function() {
46 | return self.batchSimple(
47 | 'revert-to-live',
48 | "Are you sure you want to revert " + self.choices.length + " item(s) to their live content?",
49 | {}
50 | );
51 | };
52 |
53 | }
54 | });
55 |
--------------------------------------------------------------------------------
/lib/modules/apostrophe-workflow-pieces/views/workflowLastCommitted.html:
--------------------------------------------------------------------------------
1 | {{ data.value.user.title or data.value.user.username }} {{ data.value.at | date('YYYY-MM-DD') }}
2 |
--------------------------------------------------------------------------------
/lib/modules/apostrophe-workflow-schemas/index.js:
--------------------------------------------------------------------------------
1 | var _ = require('@sailshq/lodash');
2 |
3 | module.exports = {
4 |
5 | improve: 'apostrophe-schemas',
6 |
7 | afterConstruct: function(self) {
8 | self.workflowAddPermissionsFieldType();
9 | },
10 |
11 | construct: function(self, options) {
12 | self.workflowAddPermissionsFieldType = function() {
13 | self.apos.schemas.addFieldType({
14 | name: 'apostrophe-workflow-permissions',
15 | partial: self.workflowPermissionsPartial,
16 | converters: self.workflowPermissionsConverters
17 | });
18 | };
19 |
20 | self.workflowPermissionsPartial = function(data) {
21 | var workflow = self.apos.modules['apostrophe-workflow'];
22 | _.each(data.choices, function(choice) {
23 | if (_.contains(workflow.excludeActions, choice.value)) {
24 | choice.exempt = true;
25 | }
26 | var matches = choice.value.match(self.apos.permissions.permissionPattern);
27 | if (matches) {
28 | var verb = matches[1];
29 | if (!_.contains(workflow.includeVerbs, verb)) {
30 | choice.exempt = true;
31 | }
32 | }
33 | });
34 | data.choices = _.filter(data.choices, function(choice) {
35 | // Exclude submit- actions from the UI as we're replacing that with edit permissions on the
36 | // draft locales
37 | var matches = choice.value.match(self.apos.permissions.permissionPattern);
38 | if (!matches) {
39 | return true;
40 | }
41 | if (matches[1] === 'submit') {
42 | if (workflow.includeType(matches[2])) {
43 | return false;
44 | }
45 | }
46 | return true;
47 | });
48 | return self.partial('workflow-permissions-schema-field', data);
49 | };
50 |
51 | self.workflowPermissionsConverters = {
52 | string: function(req, data, name, object, field, callback) {
53 | // For now importing permissions is not a concern
54 | return setImmediate(callback);
55 | },
56 | form: function(req, data, name, object, field, callback) {
57 | if (!Array.isArray(data[name])) {
58 | object[name] = [];
59 | return setImmediate(callback);
60 | }
61 |
62 | object[name] = _.filter(data[name], function(choice) {
63 | return _.contains(_.pluck(field.choices, 'value'), choice);
64 | });
65 |
66 | var permissionsLocales = {};
67 | var raw = data[name + 'Locales'];
68 | if ((!raw) || (typeof (raw) !== 'object')) {
69 | return setImmediate(callback);
70 | }
71 | _.each(raw, function(locales, permission) {
72 | permissionsLocales[permission] = {};
73 | _.each(field.locales, function(locale, name) {
74 | if (locales[name]) {
75 | permissionsLocales[permission][name] = true;
76 | }
77 | });
78 | });
79 |
80 | // For bc, this schema field uses a separate property for the extended
81 | // information about the locales for which the user has the permission
82 | object[name + 'Locales'] = permissionsLocales;
83 |
84 | return setImmediate(callback);
85 | }
86 | };
87 | }
88 | };
89 |
--------------------------------------------------------------------------------
/lib/modules/apostrophe-workflow-schemas/views/workflow-permissions-schema-field.html:
--------------------------------------------------------------------------------
1 | {%- import "apostrophe-schemas:macros.html" as schemas -%}
2 | {%- import "apostrophe-workflow:locale-tree.html" as localeTree -%}
3 |
4 | {%- macro body(field) -%}
5 | {%- for choice in field.choices -%}
6 |
7 | {# This can't be label because there are nested checkboxes #}
8 |
9 |
10 |
11 | {{ __ns('apostrophe', choice.label | d('')) }}
12 | {% if not choice.exempt %}
13 | {{ localeTree.tree(
14 | field.name + 'Locales' + '[' + choice.value + ']', [
15 | {
16 | type: 'select',
17 | choices: [
18 | {
19 | label: 'None',
20 | value: ''
21 | },
22 | {
23 | label: 'Edit',
24 | value: 'edit'
25 | },
26 | {
27 | label: 'Commit',
28 | value: 'commit'
29 | }
30 | ]
31 | } ],
32 | field.nestedLocales)
33 | }}
34 | {% endif %}
35 |
36 |
37 | {%- endfor -%}
38 | {%- endmacro -%}
39 |
40 | {{ schemas.fieldset(data, body) }}
41 |
--------------------------------------------------------------------------------
/lib/modules/apostrophe-workflow-tasks/index.js:
--------------------------------------------------------------------------------
1 | var _ = require('@sailshq/lodash');
2 |
3 | module.exports = {
4 | improve: 'apostrophe-tasks',
5 | construct: function(self, options) {
6 | var superGetReq = self.getReq;
7 | self.getReq = function(properties) {
8 | var workflow = self.apos.modules['apostrophe-workflow'];
9 | return superGetReq(_.assign({ locale: self.apos.argv['workflow-locale'] || (workflow && workflow.defaultLocale) }, properties));
10 | };
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/lib/modules/apostrophe-workflow-templates/index.js:
--------------------------------------------------------------------------------
1 | var _ = require('@sailshq/lodash');
2 |
3 | module.exports = {
4 | improve: 'apostrophe-templates',
5 | construct: function(self, options) {
6 | var superShowContextMenu = self.showContextMenu;
7 | self.showContextMenu = function(req) {
8 | var already = superShowContextMenu(req);
9 | if (already) {
10 | return already;
11 | }
12 | // Because the draft/live switch is in this area,
13 | // it doesn't make sense to hide the context menu
14 | // in the presence of workflow, unless they have
15 | // no editing privileges
16 | return _.find(Object.keys(self.apos.docs.managers), function(name) {
17 | return self.apos.permissions.can(req, 'edit-' + name);
18 | });
19 | };
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/lib/removeDotPathViaSplice.js:
--------------------------------------------------------------------------------
1 | // Remove the property at the given dot path. if it is a property of an array,
2 | // remove it with `splice` so there are no holes. If not, remove it
3 | // with `delete`.
4 | //
5 | // If the property does not exist in the first place, or any part of the path
6 | // leading to it does not exist, do nothing.
7 | //
8 | // Returns true if `splice` was used.
9 |
10 | module.exports = function(o, dotPath) {
11 | var elements = dotPath.split(/\./);
12 | var i;
13 | var lastIndex = elements.length - 1;
14 | for (i = 0; (i < lastIndex); i++) {
15 | if (!o) {
16 | return false;
17 | }
18 | o = o[elements[i]];
19 | }
20 | if (!o) {
21 | return false;
22 | }
23 | var last = elements[lastIndex];
24 | if (Array.isArray(o)) {
25 | o.splice(last, 1);
26 | return true;
27 | } else {
28 | delete o[last];
29 | return false;
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/locales/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "Server error, please try again.": "Server error, please try again."
3 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.40.3",
3 | "scripts": {
4 | "test": "eslint . && mocha"
5 | },
6 | "license": "MIT",
7 | "main": "index.js",
8 | "name": "apostrophe-workflow",
9 | "author": {
10 | "name": "Apostrophe Technologies LLC"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/apostrophecms/apostrophe-workflow.git"
15 | },
16 | "homepage": "https://github.com/apostrophecms/apostrophe-workflow#readme",
17 | "keywords": [
18 | "apostrophe",
19 | "apostrophecms",
20 | "apostrophe-cms",
21 | "content management",
22 | "content management system",
23 | "cms",
24 | "workflow",
25 | "localization",
26 | "editorial"
27 | ],
28 | "bugs": {
29 | "url": "https://github.com/apostrophecms/apostrophe-workflow/issues"
30 | },
31 | "deprecated": false,
32 | "description": "Workflow, approvals, localization and internationalization for ApostropheCMS",
33 | "bundleDependencies": false,
34 | "dependencies": {
35 | "@sailshq/lodash": "^3.10.3",
36 | "async": "^1.5.2",
37 | "bluebird": "^3.7.2",
38 | "eslint-config-apostrophe": "^2.0.2",
39 | "express-session": "^1.17.1",
40 | "json-diff": "^0.5.3",
41 | "jsondiffpatch": "^0.2.5",
42 | "minimatch": "^3.0.4",
43 | "qs": "^6.9.4",
44 | "request-promise": "^4.2.6"
45 | },
46 | "devDependencies": {
47 | "apostrophe": "^2.220.7",
48 | "apostrophe-override-options": "^2.2.1",
49 | "eslint": "^4.15.0",
50 | "eslint-config-standard": "^11.0.0-beta.0",
51 | "eslint-plugin-import": "^2.25.2",
52 | "eslint-plugin-node": "^5.2.1",
53 | "eslint-plugin-promise": "^3.6.0",
54 | "eslint-plugin-standard": "^3.0.1",
55 | "mocha": "^8.2.1"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/public/css/expand.less:
--------------------------------------------------------------------------------
1 | .apos-ui .apos-expand-list
2 | {
3 | max-height: 0px;
4 | overflow: hidden;
5 | transition: all 0.4s ease;
6 | font-size: 14px;
7 | padding-left: 50px;
8 |
9 | background-color: lighten(@apos-mid, 5%);
10 | li {
11 | margin-bottom: 5px;
12 | }
13 | }
14 |
15 | .apos-expand-list-container--open .apos-expand-list
16 | {
17 | max-height: 900px;
18 | }
19 |
20 | .apos-ui .apos-expand-info {
21 | padding: 20px 20px 10px 50px;
22 | &:hover { cursor: pointer; background-color: darken(@apos-mid, 5%)}
23 | }
24 |
25 | .apos-ui .apos-expand-list-container {
26 | position: relative;
27 | background-color: @apos-mid;
28 | transition: all 0.3s ease;
29 | p
30 | {
31 | font-size: 12px;
32 | font-style: italic;
33 | }
34 | a { color: @apos-primary; }
35 | }
36 |
37 | .apos-ui .apos-expand-list-container--open .apos-expand-list {
38 | padding: 20px 10px 20px 50px;
39 | }
40 |
41 | .apos-expand-trigger {
42 | &::before {
43 | position: absolute;
44 | left: 21px;
45 | top: 21px;
46 | font-size: 12px;
47 | font-family: "FontAwesome";
48 | content: "";
49 | margin-right: 0.5em;
50 | }
51 | }
52 |
53 | .apos-expand-list-container--open .apos-expand-trigger {
54 | &::before {
55 | content: ""
56 | }
57 | }
--------------------------------------------------------------------------------
/public/css/menu.less:
--------------------------------------------------------------------------------
1 | .apos-workflow-menu
2 | {
3 | display: inline-block;
4 | margin-left: 20px;
5 | .apos-button--group
6 | {
7 | border: 0px;
8 | }
9 | .apos-button
10 | {
11 | margin-left: 5px;
12 | }
13 | // .apos-workflow-state is visible by default because we need
14 | // to get to draft mode to access admin bar functionality in
15 | // a way that isn't extremely difficult to debug
16 | [data-apos-workflow-submit], [data-apos-workflow-commit], [data-apos-workflow-submitted]
17 | {
18 | display: none;
19 | }
20 | &.apos-workflow-editable
21 | {
22 | .apos-workflow-state
23 | {
24 | display: inline-block;
25 | }
26 | }
27 | &.apos-workflow-uncommitted
28 | {
29 | .apos-button--group
30 | {
31 | border: 2px solid;
32 | }
33 | [data-apos-workflow-commit] {
34 | display: inline-block;
35 | }
36 | }
37 | &.apos-workflow-submitted
38 | {
39 | .apos-button--group
40 | {
41 | border: 2px solid;
42 | }
43 | [data-apos-workflow-submitted] {
44 | display: inline-block;
45 | }
46 | }
47 | &.apos-workflow-unsubmitted
48 | {
49 | .apos-button--group
50 | {
51 | border: 2px solid;
52 | }
53 | [data-apos-workflow-submit] {
54 | display: inline-block;
55 | }
56 | &.apos-workflow-submitted {
57 | [data-apos-workflow-submitted] {
58 | display: none;
59 | }
60 | }
61 | }
62 | }
63 |
64 | .apos-ui .apos-workflow-state
65 | {
66 | margin-left: 5px;
67 | box-shadow: none;
68 | .apos-workflow-state-toggle {
69 | margin-left: 0;
70 | }
71 | }
72 |
73 | // Modal case: page settings or pieces editor modal, where
74 | // we already know we can edit the draft, and the only question
75 | // is whether we can perform commit-related actions
76 |
77 | .apos-modal-controls {
78 | [data-apos-workflow-commit],
79 | [data-apos-workflow-history],
80 | [data-apos-workflow-force-export],
81 | [data-apos-workflow-force-export-related] {
82 | display: none;
83 | }
84 | &.apos-workflow-committable {
85 | [data-apos-workflow-commit],
86 | [data-apos-workflow-history],
87 | [data-apos-workflow-force-export],
88 | [data-apos-workflow-force-export-related] {
89 | display: list-item;
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/public/css/schemas.less:
--------------------------------------------------------------------------------
1 | .apos-ui .apos-workflow-field-controls
2 | {
3 | position: relative;
4 | float: right;
5 | font-size: 14px;
6 | text-transform: uppercase;
7 | letter-spacing: 1px;
8 | a
9 | {
10 | color: @apos-primary;
11 | margin-right: 10px;
12 | }
13 |
14 | .apos-workflow-field-current
15 | {
16 | color: @apos-mid;
17 | }
18 |
19 | .apos-workflow-revert-field
20 | {
21 | font-family: 'FontAwesome';
22 | position: absolute;
23 | font-size: 18px;
24 | color: @apos-primary;
25 | right: -28px;
26 | top: 50px;
27 | margin-right: 0;
28 | }
29 | }
30 |
31 | .apos-ui .apos-workflow-field-state-control
32 | {
33 | // -webkit-appearance:none;
34 | outline: none;
35 | border-radius: 0;
36 | border: 1px solid @apos-mid;
37 | padding: 2px;
38 | text-transform: uppercase;
39 | font-size: 14px;
40 | letter-spacing: 1px;
41 | }
42 |
43 | .apos-ui .apos-field-input:disabled
44 | {
45 | opacity: 0.5;
46 | }
47 |
48 | .apos-workflow-live-field
49 | {
50 | position: relative;
51 | .apos-workflow-live-field-mask
52 | {
53 | position: absolute;
54 | z-index: 100;
55 | top: 0;
56 | left: 0;
57 | right: 0;
58 | bottom: 0;
59 | }
60 | // There is also jQuery to prevent keyboard events.
61 | pointer-events: none;
62 | label:first-child {
63 | // Allow clicks on the live/draft toggle
64 | pointer-events: auto;
65 | }
66 | .apos-field:first-child {
67 | display: none;
68 | }
69 | .apos-form-checkbox {
70 | // disabled style (not a native control)
71 | opacity: 0.5;
72 | }
73 | }
74 |
75 |
76 | .apos-ui .apos-field.apos-workflow-field-changed .apos-field-input
77 | {
78 | border: 1px solid purple;
79 | }
--------------------------------------------------------------------------------
/public/css/user.less:
--------------------------------------------------------------------------------
1 | @addColor: #9fecb3;
2 | @removeColor: #f44336;
3 | @movedColor: #ff9800;
4 |
5 |
6 | @import 'schemas.less';
7 | @import 'menu.less';
8 | @import 'expand.less';
9 |
10 | .apos-ui .apos-workflow-preview
11 | {
12 | width: 100%;
13 | height: 100%;
14 | position: relative;
15 | box-sizing: border-box;
16 | border: 0;
17 | margin: 0;
18 | padding: 0;
19 | transform: scale(0.8);
20 | border-bottom-left-radius: 10px;
21 | border-bottom-right-radius: 10px;
22 | box-shadow: 0 10px 190px 0 rgba(0,0,0,.25);
23 | &::before {
24 | content: "● ● ●";
25 | position: absolute;
26 | top: -59px;
27 | left: 0;
28 | width: 100%;
29 | color: #ffffff;
30 | text-align: left;
31 | font-size: 1.5em;
32 | background: #272727;
33 | display: block;
34 | padding: 6px 8px 11px 13px;
35 | border-top-left-radius: 10px;
36 | border-top-right-radius: 10px;
37 | }
38 | }
39 |
40 | .apos-workflow-preview-iframe
41 | {
42 | width: 100%;
43 | height: 100%;
44 | box-sizing: border-box;
45 | border: 0;
46 | margin: 0;
47 | padding: 0;
48 | overflow: scroll;
49 | }
50 |
51 | .apos-workflow-preview-page
52 | {
53 | .apos-ui {
54 | display: none;
55 | }
56 | }
57 |
58 | .apos-workflow-widget-diff
59 | {
60 | position: relative;
61 | &::before {
62 | position: absolute;
63 | height: 100%;
64 | width: 60px;
65 | z-index: 1000;
66 | left: -1px;
67 | top: -3px;
68 | top: 0px;
69 | display: flex;
70 | justify-content: center;
71 | align-items: center;
72 | font-size: 24px;
73 | color: white;
74 | opacity: 0.8;
75 | font-family: 'FontAwesome';
76 | }
77 | &::after {
78 | content: "";
79 | position: absolute;
80 | left: 0;
81 | top: 0;
82 | width: 100%;
83 | height: 100%;
84 | opacity: 0.2;
85 | }
86 | }
87 |
88 | .apos-workflow-widget-diff--new { border: 2px solid @addColor; }
89 | .apos-workflow-widget-diff--new::before { content: ""}
90 | .apos-workflow-widget-diff--new::before,
91 | .apos-workflow-widget-diff--new::after { background-color: @addColor; }
92 |
93 | .apos-workflow-widget-diff--deleted { border: 2px solid @removeColor; }
94 | .apos-workflow-widget-diff--deleted::before { content: ""; }
95 | .apos-workflow-widget-diff--deleted::before,
96 | .apos-workflow-widget-diff--deleted::after { background-color: @removeColor; }
97 |
98 | .apos-workflow-widget-diff--moved { border: 2px solid @movedColor; }
99 | .apos-workflow-widget-diff--moved::before { content: " "; }
100 | .apos-workflow-widget-diff--moved::before,
101 | .apos-workflow-widget-diff--moved::after { background-color: @movedColor; }
102 |
103 | .apos-workflow-widget-diff--changed { border: 2px solid @movedColor; }
104 | .apos-workflow-widget-diff--changed::before { content: ""; }
105 | .apos-workflow-widget-diff--changed::before,
106 | .apos-workflow-widget-diff--changed::after { background-color: @movedColor; }
107 |
108 | .apos-workflow-no-preview
109 | {
110 | display: flex;
111 | justify-content: center;
112 | height: 100%;
113 | align-items: center;
114 | font-size: 28px;
115 | }
116 |
117 | .apos-workflow-live-page {
118 | // Everything except the live/draft toggle should be unavailable in live mode
119 | .apos-ui .apos-context-menu-container .apos-context-menu {
120 | display: none;
121 | }
122 | }
123 |
124 | .apos-ui .apos-workflow-locale-tree {
125 | margin-left: 1em;
126 | li {
127 | margin-top: 4px;
128 | .apos-workflow-locale-input--disabled > label,
129 | .apos-workflow-locale-input--disabled > .apos-workflow-locale-label {
130 | opacity: 0.7;
131 | }
132 | .apos-workflow-locale-control-label--committed {
133 | font-weight: bold;
134 | small {
135 | color: @apos-mid;
136 | font-size: 0.8em;
137 | font-weight: normal;
138 | }
139 | }
140 | }
141 | }
142 |
143 | .apos-ui .apos-workflow-locale-label {
144 | margin-right: 12px;
145 | }
146 |
147 | .apos-ui .apos-workflow-locale-checkbox {
148 | font-size: 80%;
149 | }
150 |
151 | .apos-ui .apos-workflow-locale-checkbox-label {
152 | font-size: 80%;
153 | }
154 |
155 | .apos-ui .apos-workflow-permissions {
156 | margin-bottom: 12px;
157 | .apos-workflow-permissions-label {
158 | margin-top: 12px;
159 | }
160 | .apos-workflow-locale-tree {
161 | display: none;
162 | &.apos-workflow-locale-tree--active {
163 | display: block;
164 | }
165 | }
166 | }
167 |
168 | .apos-ui .apos-workflow-export-option {
169 | margin-top: 6px;
170 | max-width: 740px;
171 | padding-top: 12px;
172 | margin-left: 220px;
173 | padding-left: 24px;
174 | label {
175 | display: block;
176 | }
177 | input {
178 | margin-right: 12px;
179 | }
180 | background-color: @apos-white;
181 | }
182 |
183 | .apos-ui .apos-workflow-locale-picker,
184 | .apos-ui .apos-workflow-export-locales {
185 | max-width: 740px;
186 | margin-left: 220px;
187 | padding-top: @apos-padding-2;
188 | background-color: @apos-white;
189 | }
190 |
191 | .apos-ui .apos-workflow-locale-picker-items,
192 | .apos-ui .apos-workflow-locale-tree {
193 | padding-left: 1em;
194 | margin-left: 7px;
195 |
196 | a { color: #000; }
197 |
198 | ul {
199 | border-left: 2px dashed @apos-primary;
200 | }
201 | }
202 |
203 | .apos-ui.apos-workflow-locale-picker-modal .apos-workflow-locale-unavailable {
204 | text-decoration: line-through;
205 | }
206 |
207 | .apos-ui.apos-workflow-history-modal {
208 | .apos-workflow-history-created-at {
209 | width: 200px;
210 | }
211 |
212 | .apos-table td a {
213 | display: inline;
214 | padding-right: 24px;
215 | }
216 | }
217 |
218 | .apos-ui .apos-workflow-modified-fields, .apos-ui .apos-workflow-related {
219 | // To position/size same as the preview
220 | width: 64%;
221 | margin: 24px 0 12px 18%;
222 | .apos-workflow-hint {
223 | font-weight: normal;
224 | }
225 | }
226 |
227 | .apos-ui .apos-workflow-related {
228 | h3 {
229 | font-size: 125%;
230 | }
231 | p {
232 | font-weight: normal;
233 | }
234 | }
235 |
236 | .apos-ui .apos-workflow-preview-image {
237 | display: block;
238 | margin: auto;
239 | margin-top: 24px;
240 | }
241 |
242 | .apos-area [data-apos-workflow-force-export-widget] {
243 | display: none;
244 | }
245 |
246 | .apos-area.apos-workflow-committable [data-apos-workflow-force-export-widget] {
247 | display: inline-block;
248 | }
249 |
250 | .apos-ui .apos-workflow-shortcut {
251 | display: inline-block;
252 | padding: 8px 16px 0 0;
253 | }
254 |
255 | .apos-ui .apos-workflow-locale-unavailable-body {
256 | padding-top: 24px;
257 | margin: auto;
258 | max-width: 800px;
259 | p {
260 | margin-bottom: 24px;
261 | }
262 | }
263 |
264 | .apos-field-apply-to-subpages .apos-workflow-field-controls {
265 | display: none;
266 | }
267 |
268 | .apos-ui .apos-workflow-export-related-types {
269 | margin: 16px 0;
270 | li {
271 | margin-left: 24px;
272 | }
273 | }
274 |
--------------------------------------------------------------------------------
/public/js/._committable-modal.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apostrophecms-legacy/apostrophe-workflow/8ff55dd9083824b4d10db7775dda250d4a5bf4a1/public/js/._committable-modal.js
--------------------------------------------------------------------------------
/public/js/batch-export-modal.js:
--------------------------------------------------------------------------------
1 | // A modal for exporting the changes in a given commit to other locales
2 |
3 | apos.define('apostrophe-workflow-batch-export-modal', {
4 |
5 | extend: 'apostrophe-workflow-export-modal',
6 |
7 | source: 'batch-export-modal',
8 |
9 | verb: 'batch-export',
10 |
11 | construct: function(self, options) {
12 |
13 | self.saveContent = function(callback) {
14 | var locales = self.getLocales();
15 |
16 | if (!locales.length) {
17 | apos.notify('Select at least one locale to export to.', { type: 'error' });
18 | return callback('user');
19 | }
20 |
21 | var data = _.assign({
22 | locales: locales,
23 | job: true
24 | }, options.body);
25 |
26 | return self.api(self.options.verb, data, function(result) {
27 | if (result.status !== 'ok') {
28 | apos.notify('An error occurred.', { type: 'error' });
29 | return callback(result.status);
30 | }
31 | apos.modules['apostrophe-jobs'].progress(result.jobId);
32 | return callback(null);
33 | }, function(err) {
34 | return callback(err);
35 | });
36 | };
37 |
38 | self.presentResult = function(result) {
39 | var workflow = apos.modules['apostrophe-workflow'];
40 | workflow.presentBatchExportResult(result);
41 | };
42 |
43 | }
44 | });
45 |
--------------------------------------------------------------------------------
/public/js/batch-force-export-modal.js:
--------------------------------------------------------------------------------
1 | // A modal for force-exporting a group of docs
2 | // to other locales. Acts as a dataSource for
3 | // the force-export batch operation, does not
4 | // invoke the force export API on its own
5 |
6 | apos.define('apostrophe-workflow-batch-force-export-modal', {
7 |
8 | extend: 'apostrophe-workflow-export-modal',
9 |
10 | source: 'batch-force-export-modal',
11 |
12 | construct: function(self, options) {
13 |
14 | var superBeforeShow = self.beforeShow;
15 |
16 | self.beforeShow = function(callback) {
17 | return superBeforeShow(function(err) {
18 | if (err) {
19 | return callback(err);
20 | }
21 | self.$el.find('[for="relatedExisting"]').hide();
22 | self.$el.on('change', '[name="related"]', function() {
23 | var value = $(this).prop('checked');
24 | if (!value) {
25 | self.$el.find('[for="relatedExisting"]').hide();
26 | self.$el.find('[data-related-types]').hide();
27 | return;
28 | }
29 | self.$el.find('[for="relatedExisting"]').show();
30 | self.$el.find('[data-related-types]').show();
31 | self.fetchRelatedByType();
32 | });
33 | return callback(null);
34 | });
35 | };
36 |
37 | self.fetchRelatedByType = function() {
38 | apos.ui.globalBusy(true);
39 | self.api('count-related-by-type', {
40 | ids: self.options.body.ids
41 | }, function(data) {
42 | apos.ui.globalBusy(false);
43 | if (data.status !== 'ok') {
44 | apos.utils.error(data.status);
45 | }
46 | self.$el.find('[data-related-types]').html(data.html);
47 | }, function(err) {
48 | apos.ui.globalBusy(false);
49 | apos.utils.error(err);
50 | });
51 | };
52 |
53 | self.saveContent = function(callback) {
54 | var locales = self.getLocales();
55 | if (!locales.length) {
56 | apos.notify('Select at least one locale to export to.', { type: 'error' });
57 | return callback('user');
58 | }
59 | var related = self.$el.findByName('related').prop('checked');
60 | // Modifying the `body` object passed to us
61 | // by batchForceExportGetLocales allows that
62 | // method to see the locales that were chosen
63 | self.options.body.locales = locales;
64 | self.options.body.related = related;
65 | self.options.body.relatedTypes = [];
66 | self.$el.find('[name="relatedTypes"]:checked').each(function() {
67 | self.options.body.relatedTypes.push($(this).attr('value'));
68 | });
69 | self.options.body.relatedExisting = self.$el.find('[name="relatedExisting"]').prop('checked');
70 | return callback(null);
71 | };
72 |
73 | }
74 | });
75 |
--------------------------------------------------------------------------------
/public/js/commit-modal.js:
--------------------------------------------------------------------------------
1 | // A modal for committing draft changes to a live locale.
2 |
3 | apos.define('apostrophe-workflow-commit-modal', {
4 |
5 | extend: 'apostrophe-modal',
6 |
7 | source: 'commit-modal',
8 |
9 | construct: function(self, options) {
10 | self.manager = options.manager;
11 | self.beforeShow = function(callback) {
12 | return apos.areas.saveAllIfNeeded(callback);
13 | };
14 | self.saveContent = function(callback) {
15 | return self.api('commit', { id: options.body.id }, function(result) {
16 | if (result.status !== 'ok') {
17 | apos.notify('An error occurred.', { type: 'error' });
18 | return callback(result.status);
19 | }
20 | if (result.title) {
21 | apos.notify('%s was committed successfully.', result.title, { type: 'success', dismiss: true });
22 | } else {
23 | apos.notify('The document was committed successfully.', { type: 'success', dismiss: true });
24 | }
25 | var commitId = result.commitId;
26 | return self.api('editable-locales', {
27 | id: options.body.id
28 | }, function(result) {
29 | if (result.status !== 'ok') {
30 | return callback(result.status);
31 | }
32 | if ((apos.modules['apostrophe-workflow'].options.exportAfterCommit !== false) && (result.locales.length > 1)) {
33 | return self.manager.export(commitId, callback);
34 | }
35 | return callback(null);
36 | }, function(err) {
37 | return callback(err);
38 | });
39 | }, function(err) {
40 | return callback(err);
41 | });
42 | };
43 | // Let the manager know we're done, so the manager can step through these modals
44 | // for several docs in series if needed
45 | self.afterHide = function() {
46 | return options.after();
47 | };
48 | }
49 | });
50 |
--------------------------------------------------------------------------------
/public/js/export-modal.js:
--------------------------------------------------------------------------------
1 | // A modal for exporting the changes in a given commit to other locales
2 |
3 | apos.define('apostrophe-workflow-export-modal', {
4 |
5 | extend: 'apostrophe-modal',
6 |
7 | source: 'export-modal',
8 |
9 | verb: 'export',
10 |
11 | construct: function(self, options) {
12 |
13 | self.manager = options.manager;
14 |
15 | self.beforeShow = function(callback) {
16 | var workflow = apos.modules['apostrophe-workflow'];
17 | _.each(workflow.nextExportHint || [], function(locale) {
18 | self.$el.find('input[type="checkbox"][name="locales[' + locale + ']"]').prop('checked', true);
19 | });
20 | self.$el.on('change', 'input[type="checkbox"]', function() {
21 | var checked = $(this).prop('checked');
22 | $(this).closest('li').find('input[type="checkbox"]').prop('checked', checked);
23 | });
24 | return callback(null);
25 | };
26 |
27 | self.getLocales = function() {
28 | var locales = [];
29 | var $checkboxes = self.$el.find('input[type="checkbox"]:checked');
30 | $checkboxes.each(function() {
31 | var name = $(this).attr('name');
32 | var matches = name.match(/^locales\[(.*?)\]$/);
33 | if (matches) {
34 | locales.push(matches[1]);
35 | }
36 | });
37 | return locales;
38 | };
39 |
40 | self.saveContent = function(callback) {
41 | var locales = self.getLocales();
42 |
43 | if (!locales.length) {
44 | apos.notify('Select at least one locale to export to.', { type: 'error' });
45 | return callback('user');
46 | }
47 |
48 | var workflow = apos.modules['apostrophe-workflow'];
49 | workflow.nextExportHint = locales;
50 |
51 | return self.exportRelatedUnexported(locales, function(err) {
52 | if (err) {
53 | return callback(err);
54 | }
55 | var data = _.assign({
56 | locales: locales
57 | }, options.body);
58 |
59 | return self.api(self.options.verb, data, function(result) {
60 | if (result.status !== 'ok') {
61 | apos.notify('An error occurred.', { type: 'error' });
62 | return callback(result.status);
63 | }
64 | self.presentResult(result);
65 | return callback(null);
66 | }, function(err) {
67 | return callback(err);
68 | });
69 | });
70 | };
71 |
72 | self.presentResult = function(result) {
73 | _.each(result.errors, function(error) {
74 | apos.notify('%s: ' + error.message, error.locale, { type: 'error' });
75 | });
76 | if (result.success.length) {
77 | apos.notify('Successfully exported to: %s', result.success.join(', '), { type: 'success', dismiss: true });
78 | }
79 | };
80 |
81 | self.exportRelatedUnexported = function(locales, callback) {
82 | return self.manager.getRelatedUnexported({ id: options.body.id, exportLocales: locales }, function(err, result) {
83 | if (err) {
84 | return callback(err);
85 | }
86 | var ids = result.ids;
87 | return async.eachSeries(ids, function(id, callback) {
88 | return self.manager.launchExportModal({ id: id }, callback);
89 | }, function(err) {
90 | return callback && callback(err);
91 | });
92 | });
93 | };
94 |
95 | // Let the manager know we're done, so the manager can step through these modals
96 | // for several docs in series if needed
97 | self.afterHide = function() {
98 | return options.after && options.after();
99 | };
100 | }
101 | });
102 |
--------------------------------------------------------------------------------
/public/js/force-export-modal.js:
--------------------------------------------------------------------------------
1 | // A modal for forcing export of a single widget to other locales
2 |
3 | apos.define('apostrophe-workflow-force-export-modal', {
4 |
5 | extend: 'apostrophe-workflow-export-modal',
6 |
7 | source: 'force-export-modal',
8 |
9 | verb: 'force-export',
10 |
11 | construct: function(self, options) {
12 | var superBeforeShow = self.beforeShow;
13 | self.beforeShow = function(callback) {
14 | superBeforeShow(function(err) {
15 | if (err) {
16 | return callback(err);
17 | }
18 | self.$el.find('[for="relatedExisting"]').hide();
19 | self.$el.on('change', '[name="related"]', function() {
20 | var value = $(this).prop('checked');
21 | if (!value) {
22 | self.$el.find('[for="relatedExisting"]').hide();
23 | return;
24 | }
25 | self.$el.find('[for="relatedExisting"]').show();
26 | });
27 | return callback(null);
28 | });
29 | };
30 | self.exportRelatedUnexported = function(locales, callback) {
31 | if (!options.body.lead) {
32 | // Don't recurse through the entire site
33 | return callback(null);
34 | }
35 | var related = self.$el.find('[name="related"]').prop('checked');
36 | var relatedExisting = self.$el.find('[name="relatedExisting"]').prop('checked');
37 | if (!related) {
38 | return callback(null);
39 | }
40 |
41 | var params = { ids: [ options.body.id ], related: true };
42 | if (!relatedExisting) {
43 | params.onlyIfNewIn = locales;
44 | }
45 | return self.manager.getEditable(params, function(err, result) {
46 | if (err) {
47 | return;
48 | }
49 | var all = result.modified.concat(result.unmodified).filter(function(id) {
50 | return id !== options.body.id;
51 | });
52 | return async.eachSeries(all, function(id, callback) {
53 | if (self.manager.commitAllRelated) {
54 | return self.api('force-export', {
55 | id: id,
56 | locales: locales,
57 | existing: relatedExisting
58 | }, function(info) {
59 | if (info.status !== 'ok') {
60 | return callback(info.status);
61 | }
62 | return callback(null);
63 | }, callback);
64 | } else if (self.manager.skipAllRelated) {
65 | return setImmediate(callback);
66 | } else {
67 | return apos.create('apostrophe-workflow-force-export-modal',
68 | _.assign(
69 | {},
70 | _.omit(options, [ 'source', 'verb' ]),
71 | {
72 | body: {
73 | id: id,
74 | lead: false,
75 | existing: relatedExisting
76 | },
77 | // This is throwing Types do not match in indicateCurrentModal
78 | after: callback
79 | }
80 | )
81 | );
82 | }
83 | }, function(err) {
84 | return callback && callback(err);
85 | });
86 | });
87 | };
88 | }
89 |
90 | // The base class already sends everything in options.body so no
91 | // further modifications are needed on the front end so far
92 |
93 | });
94 |
--------------------------------------------------------------------------------
/public/js/force-export-related-modal.js:
--------------------------------------------------------------------------------
1 | // A modal for exporting the changes in a given commit to other locales
2 |
3 | apos.define('apostrophe-workflow-force-export-related-modal', {
4 |
5 | extend: 'apostrophe-workflow-force-export-modal',
6 |
7 | source: 'force-export-related-modal',
8 |
9 | verb: 'force-export',
10 |
11 | construct: function(self, options) {
12 |
13 | var superBeforeShow = self.beforeShow;
14 | self.beforeShow = function(callback) {
15 | return superBeforeShow(function(err) {
16 | if (err) {
17 | return callback(err);
18 | }
19 | self.$el.find('[name="related"]').prop('checked', true);
20 | self.$el.find('[for="related"]').hide();
21 | self.$el.find('[for="relatedExisting"]').show();
22 | return callback(null);
23 | });
24 | };
25 |
26 | self.saveContent = function(callback) {
27 | var locales = self.getLocales();
28 |
29 | if (!locales.length) {
30 | apos.notify('Select at least one locale to export to.', { type: 'error' });
31 | return callback('user');
32 | }
33 |
34 | var workflow = apos.modules['apostrophe-workflow'];
35 | workflow.nextExportHint = locales;
36 |
37 | return self.exportRelatedUnexported(locales, callback);
38 | };
39 |
40 | }
41 | });
42 |
--------------------------------------------------------------------------------
/public/js/force-export-widget-modal.js:
--------------------------------------------------------------------------------
1 | // A modal for forcing export of a single widget to other locales
2 |
3 | apos.define('apostrophe-workflow-force-export-widget-modal', {
4 |
5 | extend: 'apostrophe-workflow-export-modal',
6 |
7 | source: 'force-export-widget-modal',
8 |
9 | verb: 'force-export-widget',
10 |
11 | construct: function(self, options) {
12 | // Can't be done in the same way because we don't send a commit id
13 | self.exportRelatedUnexported = function(locales, callback) {
14 | return setImmediate(callback);
15 | };
16 | }
17 |
18 | // The base class already sends everything in options.body so no
19 | // further modifications are needed on the front end so far
20 |
21 | });
22 |
--------------------------------------------------------------------------------
/public/js/history-modal.js:
--------------------------------------------------------------------------------
1 | // A modal for browsing past commits of a doc.
2 |
3 | apos.define('apostrophe-workflow-history-modal', {
4 |
5 | extend: 'apostrophe-modal',
6 |
7 | source: 'history-modal',
8 |
9 | construct: function(self, options) {
10 | self.manager = options.manager;
11 | self.beforeShow = function(callback) {
12 | // various event handlers will go here
13 | return callback(null);
14 | };
15 | }
16 | });
17 |
--------------------------------------------------------------------------------
/public/js/lean.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | document.body.addEventListener('apos-before-get', addLocaleHeader);
3 | document.body.addEventListener('apos-before-post', addLocaleHeader);
4 | function addLocaleHeader(event) {
5 | // Easy check for same origin
6 | var link = document.createElement('a');
7 | link.href = event.uri;
8 | if (link.host !== location.host) {
9 | return;
10 | }
11 | event.request.setRequestHeader('Apostrophe-Locale', document.body.getAttribute('data-locale'));
12 | };
13 | })();
14 |
--------------------------------------------------------------------------------
/public/js/locale-picker-modal.js:
--------------------------------------------------------------------------------
1 | // A very simple modal for switching locales. Used as admin UI, for the public
2 | // one more typically builds a custom locale picker that suits the site design
3 |
4 | apos.define('apostrophe-workflow-locale-picker-modal', {
5 | extend: 'apostrophe-modal',
6 | source: 'locale-picker-modal'
7 | // All the business logic is in the route and template, which contains
8 | // ordinary links that switch locales by navigating
9 | });
10 |
--------------------------------------------------------------------------------
/public/js/locale-unavailable-modal.js:
--------------------------------------------------------------------------------
1 | // A modal for displaying information about an unavailable locale.
2 |
3 | apos.define('apostrophe-workflow-locale-unavailable-modal', {
4 |
5 | extend: 'apostrophe-modal',
6 |
7 | source: 'locale-unavailable-modal',
8 |
9 | construct: function(self, options) {
10 | self.manager = options.manager;
11 | self.beforeShow = function(callback) {
12 | // various event handlers could go here
13 | return callback(null);
14 | };
15 | self.saveContent = function(callback) {
16 | return self.api('activate', {
17 | locale: self.options.body.locale,
18 | workflowGuid: self.options.body.workflowGuid
19 | }, function(result) {
20 | if (result.status === 'ok') {
21 | window.location.href = result.url;
22 | } else {
23 | error();
24 | }
25 | return callback(null);
26 | }, function(err) {
27 | apos.utils.error(err);
28 | error();
29 | return callback(null);
30 | });
31 | function error() {
32 | apos.notify('An error occurred activating the document. It may not be available or you may not have permission.', { type: 'error' });
33 | }
34 | };
35 | }
36 | });
37 |
--------------------------------------------------------------------------------
/public/js/manage-modal.js:
--------------------------------------------------------------------------------
1 | // A modal for reviewing workflow submissions.
2 |
3 | apos.define('apostrophe-workflow-manage-modal', {
4 |
5 | extend: 'apostrophe-modal',
6 |
7 | source: 'manage-modal',
8 |
9 | construct: function(self, options) {
10 | self.manager = options.manager;
11 | // The route already rendered the content with normal links, so there's nothing more to do here,
12 | // unless we add pagination
13 | }
14 | });
15 |
--------------------------------------------------------------------------------
/public/js/review-modal.js:
--------------------------------------------------------------------------------
1 | // A modal for reviewing the changes in a past commit.
2 | // The template does a lot of the work via the preview iframe.
3 |
4 | apos.define('apostrophe-workflow-review-modal', {
5 |
6 | extend: 'apostrophe-modal',
7 |
8 | source: 'review-modal',
9 |
10 | construct: function(self, options) {
11 | self.manager = options.manager;
12 | self.beforeShow = function(callback) {
13 | return apos.areas.saveAllIfNeeded(callback);
14 | };
15 | self.saveContent = function(callback) {
16 | return apos.create('apostrophe-workflow-export-modal',
17 | _.assign({
18 | manager: self.manager,
19 | body: { id: options.body.id },
20 | after: callback
21 | }, self.manager.options)
22 | );
23 | };
24 | }
25 | });
26 |
--------------------------------------------------------------------------------
/test/.gitignore:
--------------------------------------------------------------------------------
1 | public
--------------------------------------------------------------------------------
/test/lib/modules/apostrophe-custom-pages/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | beforeConstruct: function(self, options) {
3 | options.addFields = [
4 | {
5 | name: '_related',
6 | type: 'joinByOne',
7 | withType: 'product'
8 | },
9 | {
10 | name: '_coolPages',
11 | type: 'joinByArray',
12 | withType: 'apostrophe-page'
13 | }
14 | ].concat(options.addFields || []);
15 | }
16 | };
17 |
--------------------------------------------------------------------------------
/test/lib/modules/apostrophe-pages/views/pages/home.html:
--------------------------------------------------------------------------------
1 | {% extends data.outerLayout %}
2 | Home Page
3 |
--------------------------------------------------------------------------------
/test/lib/modules/products/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extend: 'apostrophe-pieces',
3 | name: 'product',
4 | alias: 'products',
5 | addFields: [
6 | {
7 | name: '_related',
8 | type: 'joinByOne',
9 | withType: 'product'
10 | }
11 | ],
12 | construct: function(self, options) {
13 | self.afterInsert = function(req, piece, options, callback) {
14 | piece.afterInsertRan = true;
15 | return self.update(req, piece, options, callback);
16 | };
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/test/missing-prefix-redirect-status-code.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 |
3 | describe('Missing prefix redirect status code', function() {
4 |
5 | var apos;
6 |
7 | this.timeout(20000);
8 |
9 | after(function(done) {
10 | require('apostrophe/test-lib/util').destroy(apos, done);
11 | });
12 |
13 | /// ///
14 | // EXISTENCE
15 | /// ///
16 |
17 | it('should be a property of the apos object', function(done) {
18 | apos = require('apostrophe')({
19 | testModule: true,
20 |
21 | modules: {
22 | 'products': {},
23 | 'apostrophe-pages': {
24 | park: [],
25 | types: [
26 | {
27 | name: 'home',
28 | label: 'Home'
29 | },
30 | {
31 | name: 'testPage',
32 | label: 'Test Page'
33 | }
34 | ]
35 | },
36 | 'apostrophe-workflow': {
37 | missingPrefixRedirectStatusCode: 301,
38 | hostnames: {
39 | 'private': 'private.com',
40 | 'fr': 'exemple.fr',
41 | 'default': 'example.com',
42 | 'us': 'example.com',
43 | 'us-en': 'example.com',
44 | 'us-es': 'example.com',
45 | 'us-de': 'example.com',
46 | 'es': 'example.es',
47 | 'es-CO': 'example.es',
48 | 'es-MX': 'example.es',
49 | 'de': 'example.de',
50 | 'de-de': 'example.de',
51 | 'tt-one': 'tt.com',
52 | 'tt-two': 'tt.com',
53 | 'tt-three': 'tt.com'
54 | },
55 | prefixes: {
56 | // Even private locales must be distinguishable by hostname and/or prefix
57 | 'default': '/default',
58 | 'us': '/us-private',
59 | // we don't add a prefix for us-en since that locale
60 | // will reside at the root level and share the hostname
61 | // with us-es and us-de.
62 | 'us-es': '/es',
63 | 'us-de': '/de',
64 | 'es-CO': '/co',
65 | 'es-MX': '/mx',
66 | 'de-de': '/de',
67 | 'tt-one': '/one',
68 | 'tt-two': '/two',
69 | 'tt-three': '/three'
70 | // We don't need prefixes for fr because
71 | // that hostname is not shared with other
72 | // locales
73 | },
74 | locales: [
75 | {
76 | name: 'default',
77 | label: 'Default',
78 | private: true,
79 | children: [
80 | {
81 | name: 'fr'
82 | },
83 | {
84 | name: 'us',
85 | private: true,
86 | children: [
87 | {
88 | name: 'us-en'
89 | },
90 | {
91 | name: 'us-es'
92 | },
93 | {
94 | name: 'us-de'
95 | }
96 | ]
97 | },
98 | {
99 | name: 'es',
100 | children: [
101 | {
102 | name: 'es-CO'
103 | },
104 | {
105 | name: 'es-MX'
106 | }
107 | ]
108 | },
109 | {
110 | name: 'de',
111 | children: [
112 | {
113 | name: 'de-de'
114 | }
115 | ]
116 | },
117 | {
118 | name: 'tt-one'
119 | },
120 | {
121 | name: 'tt-two'
122 | },
123 | {
124 | name: 'tt-three'
125 | },
126 | {
127 | name: 'private',
128 | private: true
129 | },
130 | {
131 | name: 'private2',
132 | private: true
133 | }
134 | ]
135 | }
136 | ],
137 | defaultLocale: 'default',
138 | defaultLocalesByHostname: {
139 | 'tt.com': 'tt-one',
140 | 'private2.com': 'private2'
141 | }
142 | }
143 | },
144 | afterInit: function(callback) {
145 | assert(apos.modules['apostrophe-workflow']);
146 | // Should NOT have an alias!
147 | assert(!apos.workflow);
148 | return callback(null);
149 | },
150 | afterListen: function(err) {
151 | assert(!err);
152 | done();
153 | }
154 | });
155 | });
156 |
157 | function tryMiddleware(url, after) {
158 | return tryMiddlewareBody(url, {}, after);
159 | }
160 |
161 | function tryMiddlewareBody(url, options, after) {
162 | var req;
163 | if (options.admin) {
164 | req = apos.tasks.getReq();
165 | } else {
166 | req = apos.tasks.getAnonReq();
167 | }
168 | req.absoluteUrl = url;
169 | var parsed = require('url').parse(req.absoluteUrl);
170 | req.url = parsed.path;
171 | req.session = {};
172 | req.get = function(propName) {
173 | return {
174 | Host: parsed.host
175 | }[propName];
176 | };
177 | req.res.redirect = function(status, url) {
178 | if (!url) {
179 | url = status;
180 | status = 302;
181 | }
182 | req.res.status = status;
183 | req.url = url;
184 | after(req);
185 | };
186 |
187 | var workflow = apos.modules['apostrophe-workflow'];
188 | assert(workflow);
189 | var middleware = workflow.expressMiddleware.middleware;
190 |
191 | middleware(req, req.res, function() {
192 | after(req);
193 | });
194 | }
195 |
196 | it('can find a defaultLocaleByHostname-determined locale via middleware with a custom status code', function(done) {
197 | tryMiddleware('http://tt.com', function(req) {
198 | assert(req.locale === 'tt-one');
199 | assert(req.url === '/one/');
200 | assert(req.res.status === 301);
201 | done();
202 | });
203 | });
204 |
205 | });
206 |
--------------------------------------------------------------------------------
/test/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "//": "Must exist to satisfy checks, not really used",
3 | "dependencies": {
4 | "apostrophe": "*",
5 | "apostrophe-workflow": "*",
6 | "@sailshq/lodash": "*",
7 | "bluebird": "*",
8 | "async": "*",
9 | "request-promise": "*",
10 | "apostrophe-override-options": "*"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/test/parkedPages.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 |
3 | describe('Workflow Core', function() {
4 |
5 | var apos;
6 |
7 | this.timeout(20000);
8 |
9 | after(function(done) {
10 | require('apostrophe/test-lib/util').destroy(apos, done);
11 | });
12 |
13 | /// ///
14 | // EXISTENCE
15 | /// ///
16 |
17 | it('should be a property of the apos object', function(done) {
18 | apos = require('apostrophe')({
19 | testModule: true,
20 |
21 | modules: {
22 | 'apostrophe-pages': {
23 | park: [
24 | {
25 | slug: '/',
26 | published: true,
27 | _defaults: {
28 | title: 'Home',
29 | type: 'home'
30 | },
31 | _children: [
32 | {
33 | slug: {
34 | 'us-en': '/products-en',
35 | 'fr': '/produits',
36 | '_default': '/products'
37 | },
38 | _defaults: {
39 | type: 'product-page',
40 | title: 'Product'
41 | },
42 | published: true,
43 | parkedId: 'products'
44 | }, {
45 | slug: '/test-page',
46 | type: 'product-page',
47 | published: true,
48 | parkedId: 'test-product-page'
49 | }
50 | ]
51 | }
52 | ],
53 | types: [
54 | {
55 | name: 'home',
56 | label: 'Home'
57 | },
58 | {
59 | name: 'product-page',
60 | label: 'Product'
61 | }
62 | ]
63 | },
64 | 'products': {},
65 | 'product-pages': {},
66 | 'apostrophe-workflow': {
67 | replicateAcrossLocales: false,
68 | prefixes: {
69 | 'fr': '/fr',
70 | 'us-es': '/es',
71 | 'us-de': '/de'
72 | },
73 | locales: [
74 | {
75 | name: 'default',
76 | label: 'Default',
77 | private: true,
78 | children: [
79 | {
80 | name: 'fr'
81 | },
82 | {
83 | name: 'us',
84 | private: true,
85 | children: [
86 | {
87 | name: 'us-en'
88 | },
89 | {
90 | name: 'us-es'
91 | },
92 | {
93 | name: 'us-de'
94 | }
95 | ]
96 | }
97 | ]
98 | }
99 | ],
100 | defaultLocale: 'default'
101 | }
102 | },
103 | afterInit: function(callback) {
104 | assert(apos.modules['apostrophe-workflow']);
105 | // Should NOT have an alias!
106 | assert(!apos.workflow);
107 | return callback(null);
108 | },
109 | afterListen: function(err) {
110 | assert(!err);
111 | done();
112 | }
113 | });
114 | });
115 |
116 | it('should create parked pages with prefixes', function() {
117 | return apos.docs.db.find({ type: 'product-page', workflowLocale: 'fr' }).toArray().then(function(pages) {
118 | assert(pages && pages[0] && pages[1]);
119 | assert(pages[0].slug === '/fr/produits'); // from slug object
120 | assert(pages[1].slug === '/fr/test-page'); // from traditional slug string
121 | });
122 | });
123 | });
124 |
--------------------------------------------------------------------------------
/test/reorganize.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 | var Promise = require('bluebird');
3 |
4 | describe('Workflow Reorganize', function() {
5 |
6 | let apos;
7 |
8 | this.timeout(20000);
9 |
10 | after(function(done) {
11 | require('apostrophe/test-lib/util').destroy(apos, done);
12 | });
13 |
14 | /// ///
15 | // EXISTENCE
16 | /// ///
17 |
18 | it('should be a property of the apos object', function(done) {
19 | apos = require('apostrophe')({
20 | testModule: true,
21 |
22 | modules: {
23 | 'apostrophe-pages': {
24 | types: [
25 | {
26 | name: 'home',
27 | label: 'Home'
28 | },
29 | {
30 | name: 'testPage',
31 | label: 'Test Page'
32 | }
33 | ]
34 | },
35 | products: {},
36 | 'apostrophe-workflow': {
37 | locales: [
38 | {
39 | name: 'default',
40 | children: [
41 | {
42 | name: 'en'
43 | },
44 | {
45 | name: 'fr'
46 | }
47 | ]
48 | }
49 | ]
50 | }
51 | },
52 | afterInit: function(callback) {
53 | assert(apos.modules['apostrophe-workflow']);
54 | return callback(null);
55 | },
56 | afterListen: function(err) {
57 | assert(!err);
58 | done();
59 | }
60 | });
61 | });
62 |
63 | it('insert page1 and page2 as peers initially', function() {
64 | const req = apos.tasks.getReq({ locale: 'default-draft' });
65 | return Promise.try(function() {
66 | return apos.pages.find(req, { slug: '/' }).children({ depth: 2 }).toObject();
67 | }).then(function(home) {
68 | return apos.pages.insert(req, home, {
69 | title: 'page1',
70 | slug: '/page1',
71 | type: 'testPage',
72 | published: true,
73 | trash: false
74 | }).then(function() {
75 | return home;
76 | });
77 | }).then(function(home) {
78 | return apos.pages.insert(req, home, {
79 | title: 'page2',
80 | slug: '/page2',
81 | type: 'testPage',
82 | published: true,
83 | trash: false
84 | });
85 | });
86 | });
87 |
88 | it('page1 and page2 are peers in default-draft locale', function() {
89 | return page1AndPage2ArePeers('default-draft');
90 | });
91 |
92 | it('page1 and page2 are peers in default locale', function() {
93 | return page1AndPage2ArePeers('default');
94 | });
95 |
96 | it('should be able to move page2 under page1 in default-draft', function() {
97 | const req = apos.tasks.getReq({ locale: 'default-draft' });
98 | return Promise.try(function() {
99 | return apos.pages.find(req, { slug: '/' }).children({ depth: 2 }).toObject();
100 | }).then(function(home) {
101 | const page1 = home._children[0];
102 | const page2 = home._children[1];
103 | // req, moved, target, relationship
104 | return apos.pages.move(req, page2._id, page1._id, 'inside');
105 | }).then(function() {
106 | return page2IsNestedUnderPage1('default-draft');
107 | });
108 | });
109 |
110 | it('meanwhile in live locale, page2 should still be a peer', function() {
111 | return page1AndPage2ArePeers('default');
112 | });
113 |
114 | it('should be able to commit page1', function() {
115 | const req = apos.tasks.getReq({ locale: 'default-draft' });
116 | const workflow = apos.modules['apostrophe-workflow'];
117 | return Promise.try(function() {
118 | return apos.pages.find(req, { slug: '/' }).children({ depth: 2 }).toObject();
119 | }).then(function(home) {
120 | const page1 = home._children[0];
121 | return Promise.promisify(workflow.commitLatest)(req, page1._id);
122 | });
123 | });
124 |
125 | it('should be able to commit page2', function() {
126 | const req = apos.tasks.getReq({ locale: 'default-draft' });
127 | const workflow = apos.modules['apostrophe-workflow'];
128 | return Promise.try(function() {
129 | return apos.pages.find(req, { slug: '/' }).children({ depth: 2 }).toObject();
130 | }).then(function(home) {
131 | const page2 = home._children[0]._children[0];
132 | return Promise.promisify(workflow.commitLatest)(req, page2._id);
133 | });
134 | });
135 |
136 | it('now in live locale page2 should be nested under page1', function() {
137 | return page2IsNestedUnderPage1('default');
138 | });
139 |
140 | it('meanwhile in fr-draft page2 is still a peer of page1', function() {
141 | return page1AndPage2ArePeers('fr-draft');
142 | });
143 |
144 | it('export both commits to fr-draft locale', function() {
145 | const workflow = apos.modules['apostrophe-workflow'];
146 | const exporter = Promise.promisify(workflow.export);
147 | let commits;
148 | const req = apos.tasks.getReq({ locale: 'default-draft' });
149 | return Promise.try(function() {
150 | return workflow.db.find().sort({ createdAt: 1 }).toArray();
151 | }).then(function(_commits) {
152 | commits = _commits;
153 | return exporter(req, commits[0]._id, [ 'fr' ]);
154 | }).then(function() {
155 | return exporter(req, commits[1]._id, [ 'fr' ]);
156 | });
157 | });
158 |
159 | it('after exports page2 is a child of page1 in fr-draft', function() {
160 | return page2IsNestedUnderPage1('fr-draft');
161 | });
162 |
163 | it('... but still a peer in fr (live)', function() {
164 | return page1AndPage2ArePeers('fr');
165 | });
166 |
167 | it('... and still a peer in the unrelated en-draft', function() {
168 | return page1AndPage2ArePeers('en-draft');
169 | });
170 |
171 | it('can force export page1 and page2 to en-draft', function() {
172 | const req = apos.tasks.getReq({ locale: 'default-draft' });
173 | const workflow = apos.modules['apostrophe-workflow'];
174 | const forceExport = Promise.promisify(workflow.forceExport);
175 | let home;
176 | return Promise.try(function() {
177 | return apos.pages.find(req, { slug: '/' }).children({ depth: 2 }).toObject();
178 | }).then(function(_home) {
179 | home = _home;
180 | return forceExport(req, home._children[0]._id, [ 'en' ]);
181 | }).then(function() {
182 | return forceExport(req, home._children[0]._children[0]._id, [ 'en' ]);
183 | });
184 | });
185 |
186 | it('after force exports page2 is a child of page1 in en-draft', function() {
187 | return page2IsNestedUnderPage1('en-draft');
188 | });
189 |
190 | function page1AndPage2ArePeers(locale) {
191 | return Promise.try(function() {
192 | return apos.docs.db.find({ workflowLocale: locale }).toArray();
193 | }).then(function(docs) {
194 | return apos.pages.find(apos.tasks.getReq({ locale: locale }), { slug: '/' }).children({ depth: 2, trash: null }).toObject();
195 | }).then(function(home) {
196 | assert(home);
197 | const page1 = home._children[0];
198 | const page2 = home._children[1];
199 | assert(page1.title === 'page1');
200 | assert(page1.path === '/page1');
201 | assert(page1.level === 1);
202 | assert(page2.title === 'page2');
203 | assert(page2.path === '/page2');
204 | assert(page2.level === 1);
205 | });
206 | }
207 |
208 | function page2IsNestedUnderPage1(locale) {
209 | return Promise.try(function() {
210 | return apos.pages.find(apos.tasks.getReq({ locale: locale }), { slug: '/' }).children({ depth: 2, trash: null }).toObject();
211 | }).then(function(home) {
212 | const page1 = home._children[0];
213 | assert(page1.title === 'page1');
214 | assert(page1.path === '/page1');
215 | assert(page1.level === 1);
216 | const page2 = page1._children[0];
217 | assert(page2.title === 'page2');
218 | assert(page2.path === '/page1/page2');
219 | assert(page2.level === 2);
220 | });
221 | }
222 |
223 | });
224 |
--------------------------------------------------------------------------------
/test/reorganizeReplicateFalse.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 | var Promise = require('bluebird');
3 |
4 | describe('Workflow Reorganize with replicate:false', function() {
5 |
6 | let apos;
7 |
8 | this.timeout(20000);
9 |
10 | after(function(done) {
11 | require('apostrophe/test-lib/util').destroy(apos, done);
12 | });
13 |
14 | /// ///
15 | // EXISTENCE
16 | /// ///
17 |
18 | it('should be a property of the apos object', function(done) {
19 | apos = require('apostrophe')({
20 | testModule: true,
21 |
22 | modules: {
23 | 'apostrophe-pages': {
24 | types: [
25 | {
26 | name: 'home',
27 | label: 'Home'
28 | },
29 | {
30 | name: 'testPage',
31 | label: 'Test Page'
32 | }
33 | ]
34 | },
35 | products: {},
36 | 'apostrophe-workflow': {
37 | locales: [
38 | {
39 | name: 'default',
40 | children: [
41 | {
42 | name: 'en'
43 | },
44 | {
45 | name: 'fr'
46 | },
47 | {
48 | name: 'es'
49 | }
50 | ]
51 | }
52 | ],
53 | replicateAcrossLocales: false
54 | }
55 | },
56 | afterInit: function(callback) {
57 | assert(apos.modules['apostrophe-workflow']);
58 | return callback(null);
59 | },
60 | afterListen: function(err) {
61 | assert(!err);
62 | done();
63 | }
64 | });
65 | });
66 |
67 | it('insert page1 and page2 as peers initially', function() {
68 | const req = apos.tasks.getReq({ locale: 'default-draft' });
69 | return Promise.try(function() {
70 | return apos.pages.find(req, { slug: '/' }).children({ depth: 2 }).toObject();
71 | }).then(function(home) {
72 | return apos.pages.insert(req, home, {
73 | title: 'page1',
74 | slug: '/page1',
75 | type: 'testPage',
76 | published: true,
77 | trash: false
78 | }).then(function() {
79 | return home;
80 | });
81 | }).then(function(home) {
82 | return apos.pages.insert(req, home, {
83 | title: 'page2',
84 | slug: '/page2',
85 | type: 'testPage',
86 | published: true,
87 | trash: false
88 | });
89 | });
90 | });
91 |
92 | it('page1 and page2 are peers in default-draft locale', function() {
93 | return page1AndPage2ArePeers('default-draft');
94 | });
95 |
96 | it('page1 and page2 are peers in default locale', function() {
97 | return page1AndPage2ArePeers('default');
98 | });
99 |
100 | it('should be able to move page2 under page1 in default-draft', function() {
101 | const req = apos.tasks.getReq({ locale: 'default-draft' });
102 | return Promise.try(function() {
103 | return apos.pages.find(req, { slug: '/' }).children({ depth: 2 }).toObject();
104 | }).then(function(home) {
105 | const page1 = home._children[0];
106 | const page2 = home._children[1];
107 | // req, moved, target, relationship
108 | return apos.pages.move(req, page2._id, page1._id, 'inside');
109 | }).then(function() {
110 | return page2IsNestedUnderPage1('default-draft');
111 | });
112 | });
113 |
114 | it('meanwhile in live locale, page2 should still be a peer', function() {
115 | return page1AndPage2ArePeers('default');
116 | });
117 |
118 | it('should be able to commit page1', function() {
119 | const req = apos.tasks.getReq({ locale: 'default-draft' });
120 | const workflow = apos.modules['apostrophe-workflow'];
121 | return Promise.try(function() {
122 | return apos.pages.find(req, { slug: '/' }).children({ depth: 2 }).toObject();
123 | }).then(function(home) {
124 | const page1 = home._children[0];
125 | return Promise.promisify(workflow.commitLatest)(req, page1._id);
126 | });
127 | });
128 |
129 | it('should be able to commit page2', function() {
130 | const req = apos.tasks.getReq({ locale: 'default-draft' });
131 | const workflow = apos.modules['apostrophe-workflow'];
132 | return Promise.try(function() {
133 | return apos.pages.find(req, { slug: '/' }).children({ depth: 2 }).toObject();
134 | }).then(function(home) {
135 | const page2 = home._children[0]._children[0];
136 | return Promise.promisify(workflow.commitLatest)(req, page2._id);
137 | });
138 | });
139 |
140 | it('now in live locale page2 should be nested under page1', function() {
141 | return page2IsNestedUnderPage1('default');
142 | });
143 |
144 | it('meanwhile in fr-draft page2 does not exist yet', function() {
145 | return page2DoesNotExist('fr-draft');
146 | });
147 |
148 | it('export both commits to fr-draft locale', function() {
149 | const workflow = apos.modules['apostrophe-workflow'];
150 | const exporter = Promise.promisify(workflow.export);
151 | let commits;
152 | const req = apos.tasks.getReq({ locale: 'default-draft' });
153 | return Promise.try(function() {
154 | return workflow.db.find().sort({ createdAt: 1 }).toArray();
155 | }).then(function(_commits) {
156 | commits = _commits;
157 | return exporter(req, commits[0]._id, [ 'fr' ]);
158 | }).then(function() {
159 | return exporter(req, commits[1]._id, [ 'fr' ]);
160 | });
161 | });
162 |
163 | it('after exports page2 is a child of page1 in fr-draft', function() {
164 | return page2IsNestedUnderPage1('fr-draft');
165 | });
166 |
167 | it('... and in fr (live)', function() {
168 | return page2IsNestedUnderPage1('fr');
169 | });
170 |
171 | it('... and still nonexistent in the unrelated en-draft', function() {
172 | return page2DoesNotExist('en-draft');
173 | });
174 |
175 | it('can force export page1 and page2 to en-draft', function() {
176 | const req = apos.tasks.getReq({ locale: 'default-draft' });
177 | const workflow = apos.modules['apostrophe-workflow'];
178 | const forceExport = Promise.promisify(workflow.forceExport);
179 | let home;
180 | return Promise.try(function() {
181 | return apos.pages.find(req, { slug: '/' }).children({ depth: 2 }).toObject();
182 | }).then(function(_home) {
183 | home = _home;
184 | return forceExport(req, home._children[0]._id, [ 'en' ]);
185 | }).then(function() {
186 | return forceExport(req, home._children[0]._children[0]._id, [ 'en' ]);
187 | });
188 | });
189 |
190 | it('after force exports page2 is a child of page1 in en-draft', function() {
191 | return page2IsNestedUnderPage1('en-draft');
192 | });
193 |
194 | it('... and still nonexistent in the unrelated es-draft', function() {
195 | return page2DoesNotExist('es-draft');
196 | });
197 |
198 | it('can force export page2 and page1 to es-draft, in that order (reversed)', function() {
199 | const req = apos.tasks.getReq({ locale: 'default-draft' });
200 | const workflow = apos.modules['apostrophe-workflow'];
201 | const forceExport = Promise.promisify(workflow.forceExport);
202 | let home;
203 | return Promise.try(function() {
204 | return apos.pages.find(req, { slug: '/' }).children({ depth: 2 }).toObject();
205 | }).then(function(_home) {
206 | home = _home;
207 | return forceExport(req, home._children[0]._children[0]._id, [ 'es' ]);
208 | }).then(function() {
209 | return forceExport(req, home._children[0]._id, [ 'es' ]);
210 | });
211 | });
212 |
213 | it('after force exports page1 and page2 are peers, in reverse order, in es-draft because the parent was not available when the child first arrived', function() {
214 | return page1AndPage2ArePeersReversed('es-draft');
215 | });
216 |
217 | function page1AndPage2ArePeers(locale) {
218 | return Promise.try(function() {
219 | return apos.docs.db.find({ workflowLocale: locale }).toArray();
220 | }).then(function(docs) {
221 | return apos.pages.find(apos.tasks.getReq({ locale: locale }), { slug: '/' }).children({ depth: 2, trash: null }).toObject();
222 | }).then(function(home) {
223 | assert(home);
224 | const page1 = home._children[0];
225 | const page2 = home._children[1];
226 | assert(page1.title === 'page1');
227 | assert(page1.path === '/page1');
228 | assert(page1.level === 1);
229 | assert(page2.title === 'page2');
230 | assert(page2.path === '/page2');
231 | assert(page2.level === 1);
232 | });
233 | }
234 |
235 | function page1AndPage2ArePeersReversed(locale) {
236 | return Promise.try(function() {
237 | return apos.docs.db.find({ workflowLocale: locale }).toArray();
238 | }).then(function(docs) {
239 | return apos.pages.find(apos.tasks.getReq({ locale: locale }), { slug: '/' }).children({ depth: 2, trash: null }).toObject();
240 | }).then(function(home) {
241 | assert(home);
242 | const page2 = home._children[0];
243 | const page1 = home._children[1];
244 | assert(page1.title === 'page1');
245 | assert(page1.path === '/page1');
246 | assert(page1.level === 1);
247 | assert(page2.title === 'page2');
248 | assert(page2.path === '/page2');
249 | assert(page2.level === 1);
250 | });
251 | }
252 |
253 | function page2IsNestedUnderPage1(locale) {
254 | return Promise.try(function() {
255 | return apos.pages.find(apos.tasks.getReq({ locale: locale }), { slug: '/' }).children({ depth: 2, trash: null }).toObject();
256 | }).then(function(home) {
257 | const page1 = home._children[0];
258 | assert(page1.title === 'page1');
259 | assert(page1.path === '/page1');
260 | assert(page1.level === 1);
261 | const page2 = page1._children[0];
262 | assert(page2.title === 'page2');
263 | assert(page2.path === '/page1/page2');
264 | assert(page2.level === 2);
265 | });
266 | }
267 |
268 | function page2DoesNotExist(locale) {
269 | return Promise.try(function() {
270 | return apos.pages.find(apos.tasks.getReq({ locale: locale }), { title: 'page2' }).toObject();
271 | }).then(function(page) {
272 | if (page) {
273 | throw new Error('should not exist in the ' + locale + ' locale');
274 | }
275 | });
276 | }
277 | });
278 |
--------------------------------------------------------------------------------
/test/testApi.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 | var async = require('async');
3 | var revertId;
4 | var _ = require('@sailshq/lodash');
5 |
6 | describe('Workflow API', function() {
7 | this.timeout(20000);
8 | var apos;
9 |
10 | after(function(done) {
11 | require('apostrophe/test-lib/util').destroy(apos, done);
12 | });
13 |
14 | it('should be a property of the apos object', function(done) {
15 | apos = require('apostrophe')({
16 | testModule: true,
17 |
18 | modules: {
19 | 'apostrophe-pages': {
20 | park: [],
21 | types: [
22 | {
23 | name: 'home',
24 | label: 'Home'
25 | },
26 | {
27 | name: 'testPage',
28 | label: 'Test Page'
29 | }
30 | ]
31 | },
32 | 'apostrophe-workflow': {
33 | alias: 'workflow' // for testing only!
34 | },
35 | 'products': {
36 | extend: 'apostrophe-pieces',
37 | name: 'product',
38 | alias: 'products'
39 | }
40 | },
41 | afterInit: function(callback) {
42 | assert(apos.modules['apostrophe-workflow']);
43 | return callback(null);
44 | },
45 | afterListen: function(err) {
46 | assert(!err);
47 | done();
48 | }
49 | });
50 | });
51 |
52 | it('Test add draft product to db as draft', () => {
53 | var req = apos.tasks.getReq();
54 | var product = apos.products.newInstance();
55 | product.title = 'initial title';
56 | product.tags = [];
57 | return apos.products.insert(req, product)
58 | .then(doc => {
59 | assert(doc.type === 'product');
60 | assert(doc.workflowGuid);
61 | assert(doc.workflowLocale === 'default-draft');
62 | });
63 | });
64 |
65 | // block repeats
66 | it('Commit a change', (done) => {
67 | var req = apos.tasks.getReq({locale: 'default-draft'});
68 |
69 | async.waterfall([getProductDraft, updateProductDraft, commitUpdate], (err, res) => {
70 | assert(!err);
71 | assert(typeof res === 'string', 'response should be an id');
72 | done();
73 | });
74 |
75 | function getProductDraft(callback) {
76 | apos.products.find(req).toArray().then(docs => {
77 | assert(docs[0]);
78 | return callback(null, docs[0]);
79 | })
80 | .catch(e => {
81 | return callback(e);
82 | });
83 | }
84 |
85 | function updateProductDraft(product, callback) {
86 | product.title = 'new title';
87 | apos.products.update(req, product, (err, res) => {
88 | return callback(err, res);
89 | });
90 | }
91 |
92 | function commitUpdate(product, callback) {
93 | apos.workflow.commitLatest(req, product._id, (err, res) => {
94 | revertId = res;
95 | return callback(err, res);
96 | });
97 | }
98 | });
99 |
100 | it('Check for live document after commit', done => {
101 | const req = apos.tasks.getReq();
102 | apos.products.find(req).toArray().then(docs => {
103 | assert(docs[0].title === 'new title');
104 | assert(!docs[0].trash);
105 | done();
106 | });
107 | });
108 | // end block repeats
109 |
110 | // block repeats
111 | it('Commmit a change', (done) => {
112 | var req = apos.tasks.getReq({locale: 'default-draft'});
113 |
114 | async.waterfall([getProductDraft, updateProductDraft, commitUpdate], (err, res) => {
115 |
116 | assert(!err);
117 | assert(typeof res === 'string', 'response should be an id');
118 | done();
119 | });
120 |
121 | function getProductDraft(callback) {
122 | apos.products.find(req).toArray().then(docs => {
123 | assert(docs[0]);
124 | return callback(null, docs[0]);
125 | })
126 | .catch(e => {
127 | return callback(e);
128 | });
129 | }
130 |
131 | function updateProductDraft(product, callback) {
132 | product.title = 'new title 2';
133 | apos.products.update(req, product, (err, res) => {
134 | return callback(err, res);
135 | });
136 | }
137 |
138 | function commitUpdate(product, callback) {
139 | apos.workflow.commitLatest(req, product._id, (err, res) => {
140 | return callback(err, res);
141 | });
142 | }
143 | // end block repeats
144 | });
145 |
146 | it('Check for live document after commit', done => {
147 | const req = apos.tasks.getReq();
148 | apos.products.find(req).toArray().then(docs => {
149 | assert(docs[0].title === 'new title 2');
150 | assert(!docs[0].trash);
151 | // Verifies base case for the next group of tests. -Tom
152 | assert(Array.isArray(docs[0].tags));
153 | done();
154 | });
155 | });
156 |
157 | it('Commit a change that deletes a property', (done) => {
158 | var req = apos.tasks.getReq({locale: 'default-draft'});
159 |
160 | async.waterfall([getProductDraft, updateProductDraft, commitUpdate], (err, res) => {
161 |
162 | assert(!err);
163 | assert(typeof res === 'string', 'response should be an id');
164 | done();
165 | });
166 |
167 | function getProductDraft(callback) {
168 | apos.products.find(req).toArray().then(docs => {
169 | assert(docs[0]);
170 | return callback(null, docs[0]);
171 | })
172 | .catch(e => {
173 | return callback(e);
174 | });
175 | }
176 |
177 | function updateProductDraft(product, callback) {
178 | delete product.tags;
179 | apos.products.update(req, product, (err, res) => {
180 | return callback(err, res);
181 | });
182 | }
183 |
184 | function commitUpdate(product, callback) {
185 | apos.workflow.commitLatest(req, product._id, (err, res) => {
186 | return callback(err, res);
187 | });
188 | }
189 | // end block repeats
190 | });
191 |
192 | it('Check for live document after commit: no more tags property', done => {
193 | const req = apos.tasks.getReq();
194 | apos.products.find(req).toArray().then(docs => {
195 | assert(docs[0].title === 'new title 2');
196 | assert(!docs[0].trash);
197 | assert(!_.has(docs[0], 'tags'));
198 | done();
199 | });
200 | });
201 |
202 | it('Test revert', done => {
203 | const req = apos.tasks.getReq();
204 | assert(revertId);
205 |
206 | async.waterfall([revert, check], (err, docs) => {
207 | assert(!err);
208 | assert(docs[0].title === 'new title');
209 | assert(!docs[0].trash);
210 | done();
211 | });
212 |
213 | function revert (callback) {
214 | apos.workflow.revert(req, revertId, (err, res) => {
215 | assert(!err);
216 | callback(err);
217 | });
218 | }
219 |
220 | function check (callback) {
221 | var req = apos.tasks.getReq({locale: 'default-draft'});
222 | apos.products.find(req).toArray().then(docs => {
223 | callback(null, docs);
224 | }).catch(callback);
225 | }
226 | });
227 |
228 | it('Test revert to live', done => {
229 | const req = apos.tasks.getReq({ locale: 'default-draft' });
230 |
231 | async.waterfall([ getProduct, revertToLive, check ], (err, docs) => {
232 | assert(!err);
233 | assert(docs[0].title === 'new title 2');
234 | assert(!docs[0].trash);
235 | done();
236 | });
237 |
238 | function getProduct (callback) {
239 | apos.products.find(req).toObject(callback);
240 | }
241 |
242 | function revertToLive (product, callback) {
243 | apos.workflow.revertToLive(req, product._id, (err, res) => {
244 | assert(!err);
245 | callback(err);
246 | });
247 | }
248 |
249 | function check (callback) {
250 | var req = apos.tasks.getReq({locale: 'default-draft'});
251 | apos.products.find(req).toArray(callback);
252 | }
253 | });
254 |
255 | it('1 doc committable after a modification to product, 0 after commit', done => {
256 | const req = apos.tasks.getReq({ locale: 'default-draft' });
257 | var product;
258 | async.series([ getProductDraft, updateProductDraft, _.partial(checkCommittable, 1, 'new title 3'), commit, _.partial(checkCommittable, 0, false) ], function(err) {
259 | assert(!err);
260 | done();
261 | });
262 |
263 | function getProductDraft(callback) {
264 | apos.products.find(req).toObject(function(err, _product) {
265 | product = _product;
266 | return callback(err);
267 | });
268 | }
269 |
270 | function updateProductDraft(callback) {
271 | product.title = 'new title 3';
272 | apos.products.update(req, product, callback);
273 | }
274 |
275 | function commit(callback) {
276 | apos.workflow.commitLatest(req, product._id, callback);
277 | }
278 |
279 | function checkCommittable(n, title0, callback) {
280 | apos.products.find(req, { workflowModified: true }).toArray(function(err, docs) {
281 | assert(!err);
282 | assert(docs.length === n);
283 | if ((n > 0) && title0) {
284 | assert(docs[0].title === title0);
285 | }
286 | return callback(null);
287 | });
288 | }
289 |
290 | });
291 |
292 | });
293 |
--------------------------------------------------------------------------------
/test/testDereplicate.js:
--------------------------------------------------------------------------------
1 | let assert = require('assert');
2 | let _ = require('@sailshq/lodash');
3 |
4 | describe('Workflow dereplicate task', function() {
5 |
6 | let apos;
7 |
8 | this.timeout(20000);
9 |
10 | after(function(done) {
11 | require('apostrophe/test-lib/util').destroy(apos, done);
12 | });
13 |
14 | /// ///
15 | // EXISTENCE
16 | /// ///
17 |
18 | it('should be a property of the apos object', function(done) {
19 | apos = require('apostrophe')({
20 | testModule: true,
21 |
22 | modules: {
23 | 'apostrophe-pages': {
24 | park: [],
25 | types: [
26 | {
27 | name: 'home',
28 | label: 'Home'
29 | },
30 | {
31 | name: 'testPage',
32 | label: 'Test Page'
33 | }
34 | ]
35 | },
36 | 'products': {},
37 | 'apostrophe-workflow': {
38 | alias: 'workflow',
39 | locales: [
40 | {
41 | name: 'default',
42 | label: 'Default',
43 | private: true,
44 | children: [
45 | {
46 | name: 'fr'
47 | },
48 | {
49 | name: 'us'
50 | },
51 | {
52 | name: 'es'
53 | }
54 | ]
55 | }
56 | ]
57 | // Do replicate, to create the test case
58 | }
59 | },
60 | afterInit: function(callback) {
61 | assert(apos.workflow);
62 | return callback(null);
63 | },
64 | afterListen: function(err) {
65 | assert(!err);
66 | done();
67 | }
68 | });
69 | });
70 |
71 | it('newly inserted subpage gets replicated initially', function() {
72 | // Use a child locale so it is trash in all other locales at first
73 | let req = apos.tasks.getReq({ locale: 'es-draft' });
74 | return apos.pages.find(req, { slug: '/' }).toObject().then(function(home) {
75 | assert(home);
76 | return apos.pages.insert(req, home._id, { title: 'About', slug: '/about', type: 'testPage', published: true });
77 | }).then(function(subpage) {
78 | assert(subpage);
79 | assert(subpage.slug === '/about');
80 | assert(subpage.workflowGuid);
81 | return apos.docs.db.find({ workflowGuid: subpage.workflowGuid }).toArray();
82 | }).then(function(peers) {
83 | assert(peers.length === 8);
84 | });
85 | });
86 |
87 | it('run dereplication task without crashing', function() {
88 | return apos.tasks.invoke('apostrophe-workflow:dereplicate', [], {});
89 | });
90 |
91 | it('newly inserted subpage is no longer replicated to extra locales', function() {
92 | return apos.docs.db.findOne({ slug: '/about' }).then(function(subpage) {
93 | assert(subpage);
94 | assert(subpage.slug === '/about');
95 | assert(subpage.workflowGuid);
96 | return apos.docs.db.find({ workflowGuid: subpage.workflowGuid }).toArray();
97 | }).then(function(peers) {
98 | assert.equal(peers.length, 2);
99 | assert(peers.find(function(peer) {
100 | return peer.workflowLocale === 'es';
101 | }));
102 | assert(peers.find(function(peer) {
103 | return peer.workflowLocale === 'es-draft';
104 | }));
105 | });
106 | });
107 |
108 | it('home page should still be replicated', function() {
109 | return apos.docs.db.find({ level: 0, slug: '/' }).toArray().then(function(homes) {
110 | assert(homes);
111 | assert(homes.length === 8);
112 | const homeLocales = _.pluck(homes, 'workflowLocale');
113 | assert(!Object.keys(apos.workflow.locales).find(function(locale) {
114 | return (!(homeLocales.indexOf(locale) !== -1));
115 | }));
116 | });
117 | });
118 |
119 | it('global doc page should still be replicated', function() {
120 | return apos.docs.db.find({ type: 'apostrophe-global' }).toArray().then(function(globals) {
121 | assert(globals);
122 | assert(globals.length === 8);
123 | const globalLocales = _.pluck(globals, 'workflowLocale');
124 | assert(!Object.keys(apos.workflow.locales).find(function(locale) {
125 | return (!(globalLocales.indexOf(locale) !== -1));
126 | }));
127 | });
128 | });
129 |
130 | });
131 |
--------------------------------------------------------------------------------
/test/testGlobalDef.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 | var apos, apos2;
3 | var _ = require('@sailshq/lodash');
4 |
5 | describe('test global def', function() {
6 | this.timeout(20000);
7 | after(function(done) {
8 | require('apostrophe/test-lib/util').destroy(apos, function() {
9 | require('apostrophe/test-lib/util').destroy(apos2, done);
10 | });
11 | });
12 | it('global should exist on the apos object', function(done) {
13 | apos = require('apostrophe')({
14 | testModule: true,
15 | shortName: 'test',
16 | modules: {
17 | 'apostrophe-express': {
18 | secret: 'xxx',
19 | port: 7900
20 | },
21 | 'products': {},
22 | 'apostrophe-global': {
23 | addFields: [
24 | {
25 | name: 'testString',
26 | type: 'string',
27 | def: 'populated def'
28 | }
29 | ]
30 | },
31 | 'apostrophe-workflow': {
32 | locales: [
33 | {
34 | name: 'en',
35 | children: [
36 | {
37 | name: 'fr'
38 | },
39 | {
40 | name: 'de'
41 | }
42 | ]
43 | }
44 | ],
45 | defaultLocale: 'en',
46 | alias: 'workflow' // for testing only!
47 | }
48 | },
49 | afterInit: function(callback) {
50 | assert(apos.global);
51 | // In tests this will be the name of the test file,
52 | // so override that in order to get apostrophe to
53 | // listen normally and not try to run a task. -Tom
54 | apos.argv._ = [];
55 | return callback(null);
56 | },
57 | afterListen: function(err) {
58 | assert(!err);
59 | done();
60 | }
61 | });
62 | });
63 |
64 | it('should populate def values of schema properties across locales at insert time', function(done) {
65 | return apos.docs.db.find({ slug: 'global' }).toArray(function(err, docs) {
66 | assert(!err);
67 | assert(docs.length === 6);
68 | assert(!_.find(docs, function(doc) {
69 | return doc.testString !== 'populated def';
70 | }));
71 | done();
72 | });
73 | });
74 |
75 | it('global should exist on the second apos object', function(done) {
76 | apos2 = require('apostrophe')({
77 | testModule: true,
78 | // intentionally the same as previous
79 | shortName: 'test',
80 | modules: {
81 | 'apostrophe-express': {
82 | secret: 'xxx',
83 | port: 7901
84 | },
85 | 'products': {},
86 | 'apostrophe-global': {
87 | addFields: [
88 | {
89 | name: 'anotherString',
90 | type: 'string',
91 | def: 'populated anotherString def'
92 | }
93 | ]
94 | },
95 | 'apostrophe-workflow': {
96 | locales: [
97 | {
98 | name: 'en',
99 | children: [
100 | {
101 | name: 'fr'
102 | },
103 | {
104 | name: 'de'
105 | }
106 | ]
107 | }
108 | ],
109 | defaultLocale: 'en',
110 | alias: 'workflow' // for testing only!
111 | }
112 | },
113 | afterInit: function(callback) {
114 | assert(apos2.global);
115 | // In tests this will be the name of the test file,
116 | // so override that in order to get apostrophe to
117 | // listen normally and not try to run a task. -Tom
118 | apos2.argv._ = [];
119 | return callback(null);
120 | },
121 | afterListen: function(err) {
122 | assert(!err);
123 | done();
124 | }
125 | });
126 | });
127 |
128 | it('should populate def values of schema properties at update time', function(done) {
129 | return apos.docs.db.find({ slug: 'global' }).toArray(function(err, docs) {
130 | assert(!err);
131 | assert(docs.length === 6);
132 | assert(!_.find(docs, function(doc) {
133 | return (doc.testString !== 'populated def') || (doc.anotherString !== 'populated anotherString def');
134 | }));
135 | done();
136 | });
137 | });
138 | });
139 |
--------------------------------------------------------------------------------
/test/testOverrideOptions.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const request = require('request-promise');
3 | const _ = require('@sailshq/lodash');
4 |
5 | describe('Override Options', function() {
6 | this.timeout(20000);
7 | let apos;
8 |
9 | after(function(done) {
10 | require('apostrophe/test-lib/util').destroy(apos, done);
11 | });
12 |
13 | it('should be a property of the apos object', function(done) {
14 | apos = require('apostrophe')({
15 | testModule: true,
16 |
17 | modules: {
18 | 'apostrophe-express': {
19 | port: 7900
20 | },
21 | 'products': {},
22 | 'apostrophe-pages': {
23 | park: [],
24 | types: [
25 | {
26 | name: 'home',
27 | label: 'Home'
28 | },
29 | {
30 | name: 'testPage',
31 | label: 'Test Page'
32 | }
33 | ]
34 | },
35 | 'apostrophe-workflow': {
36 | locales: [
37 | {
38 | name: 'en',
39 | children: [
40 | {
41 | name: 'fr'
42 | },
43 | {
44 | name: 'de'
45 | }
46 | ]
47 | }
48 | ],
49 | defaultLocale: 'en',
50 | alias: 'workflow' // for testing only!
51 | },
52 | 'apostrophe-global': {
53 | addFields: [
54 | {
55 | type: 'boolean',
56 | name: 'disableExportAfterCommit',
57 | label: 'Disable Export After Commit',
58 | def: true
59 | }
60 | ],
61 | overrideOptions: {
62 | editable: {
63 | 'apos.apostrophe-workflow.disableExportAfterCommit': 'disableExportAfterCommit'
64 | }
65 | }
66 | },
67 | 'apostrophe-override-options': {},
68 | // For every request act as if an admin were logged in already
69 | 'always-admin': {
70 | construct: function(self, options) {
71 | self.expressMiddleware = function(req, res, next) {
72 | const adminReq = self.apos.tasks.getReq();
73 | req.user = adminReq.user;
74 | return next();
75 | };
76 | }
77 | }
78 | },
79 | afterInit: function(callback) {
80 | assert(apos.modules['apostrophe-workflow']);
81 | return callback(null);
82 | },
83 | afterListen: function(err) {
84 | assert(!err);
85 | done();
86 | }
87 | });
88 | });
89 |
90 | it('verify disableExportAfterCommit === true for all global docs', function() {
91 | return apos.docs.db.find({ type: 'apostrophe-global' }).toArray().then(function(docs) {
92 | return (docs.length === 6) && (!_.find(docs, function(doc) { return doc.disableExportAfterCommit !== true; }));
93 | });
94 | });
95 |
96 | it('verify simulated admin login and that "exportAfterCommit":false comes through', () => {
97 | return request('http://localhost:7900').then((html) => {
98 | assert(html.match(/logout/));
99 | assert(html.match(/"exportAfterCommit":false/));
100 | });
101 | });
102 |
103 | });
104 |
--------------------------------------------------------------------------------
/test/testReplicateFalse.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const _ = require('@sailshq/lodash');
3 | const Promise = require('bluebird');
4 |
5 | describe('Workflow with replicateAcrossLocales set to false: initial locales', function() {
6 |
7 | let apos;
8 |
9 | this.timeout(20000);
10 |
11 | after(function(done) {
12 | // Do not destroy database yet, second block explores what happens
13 | // when we add locales to it
14 | apos.destroy(function(err) {
15 | assert(!err);
16 | done();
17 | });
18 | });
19 |
20 | /// ///
21 | // EXISTENCE
22 | /// ///
23 |
24 | it('should be a property of the apos object', function(done) {
25 | var locales = [
26 | {
27 | name: 'default',
28 | label: 'Default',
29 | private: true,
30 | children: [
31 | {
32 | name: 'fr'
33 | },
34 | {
35 | name: 'us'
36 | },
37 | {
38 | name: 'es'
39 | }
40 | ]
41 | }
42 | ];
43 | return instantiate(locales, function(err, _apos) {
44 | assert(!err);
45 | apos = _apos;
46 | done();
47 | });
48 | });
49 |
50 | it('home page should be replicated', function() {
51 | return apos.docs.db.find({ level: 0, slug: '/' }).toArray().then(function(homes) {
52 | assert(homes);
53 | assert(homes.length === 8);
54 | const homeLocales = _.pluck(homes, 'workflowLocale');
55 | assert(!Object.keys(apos.workflow.locales).find(function(locale) {
56 | return (!(homeLocales.indexOf(locale) !== -1));
57 | }));
58 | });
59 | });
60 |
61 | it('global doc page should be replicated', function() {
62 | return apos.docs.db.find({ type: 'apostrophe-global' }).toArray().then(function(globals) {
63 | assert(globals);
64 | assert(globals.length === 8);
65 | const globalLocales = _.pluck(globals, 'workflowLocale');
66 | assert(!Object.keys(apos.workflow.locales).find(function(locale) {
67 | return (!(globalLocales.indexOf(locale) !== -1));
68 | }));
69 | });
70 | });
71 |
72 | it('parked test page should be replicated', function() {
73 | return apos.docs.db.find({ slug: '/parked-test-page' }).toArray().then(function(test) {
74 | assert(test);
75 | assert(test.length === 8);
76 | const testLocales = _.pluck(test, 'workflowLocale');
77 | assert(!Object.keys(apos.workflow.locales).find(function(locale) {
78 | return (!(testLocales.indexOf(locale) !== -1));
79 | }));
80 | });
81 | });
82 |
83 | it('newly inserted subpage is only replicated draft/live', function() {
84 | let req = apos.tasks.getReq();
85 | return apos.pages.find(req, { slug: '/' }).toObject().then(function(home) {
86 | assert(home);
87 | return apos.pages.insert(req, home._id, { title: 'About', slug: '/about', type: 'testPage', published: true });
88 | }).then(function(subpage) {
89 | assert(subpage);
90 | assert(subpage.slug === '/about');
91 | assert(subpage.workflowGuid);
92 | return apos.docs.db.find({ workflowGuid: subpage.workflowGuid }).toArray();
93 | }).then(function(peers) {
94 | assert(peers.length === 2);
95 | assert(peers.find(function(peer) {
96 | return peer.workflowLocale === apos.workflow.liveify(req.locale);
97 | }));
98 | assert(peers.find(function(peer) {
99 | return peer.workflowLocale === apos.workflow.draftify(req.locale);
100 | }));
101 | });
102 | });
103 |
104 | it('make sure locale of all docs can be distinguished easily for testing who replicated from whom', function() {
105 | return apos.docs.db.find({}).toArray().then(function(docs) {
106 | return Promise.mapSeries(docs, function(doc) {
107 | doc.title = doc.slug + ': original locale: ' + doc.workflowLocale;
108 | return apos.docs.db.update({ _id: doc._id }, doc);
109 | });
110 | });
111 | });
112 |
113 | it('insert test products and establish relationships', function() {
114 | const req = apos.tasks.getReq();
115 | const products = _.range(0, 10).map(function(n) {
116 | return {
117 | title: 'product ' + n
118 | };
119 | });
120 | let last = null;
121 | return Promise.mapSeries(products, function(product, i) {
122 | if (i < 5) {
123 | product.relatedId = last && last._id;
124 | }
125 | return apos.products.insert(req, product);
126 | }).then(function() {
127 | return apos.docs.db.find({ level: 0 }).toArray();
128 | }).then(function(homes) {
129 | return Promise.mapSeries(homes, function(home) {
130 | return Promise.try(function() {
131 | return apos.docs.db.findOne({ title: 'product 0' }, { workflowLocale: home.workflowLocale });
132 | }).then(function(product) {
133 | return apos.docs.db.update({
134 | _id: home._id
135 | }, {
136 | $set: {
137 | relatedId: product._id
138 | }
139 | });
140 | });
141 | });
142 | });
143 | });
144 | });
145 |
146 | describe('Workflow with replicateAcrossLocales set to false: expanded locales', function() {
147 |
148 | let apos;
149 |
150 | this.timeout(20000);
151 |
152 | after(function(done) {
153 | // This time destroy database
154 | require('apostrophe/test-lib/util').destroy(apos, done);
155 | });
156 |
157 | /// ///
158 | // EXISTENCE
159 | /// ///
160 |
161 | it('should be a property of the apos object', function(done) {
162 | var locales = [
163 | {
164 | name: 'default',
165 | label: 'Default',
166 | private: true,
167 | children: [
168 | {
169 | name: 'fr'
170 | },
171 | {
172 | name: 'us'
173 | },
174 | {
175 | name: 'es',
176 | children: [
177 | {
178 | name: 'es-mx'
179 | }
180 | ]
181 | }
182 | ]
183 | }
184 | ];
185 | return instantiate(locales, function(err, _apos) {
186 | assert(!err);
187 | apos = _apos;
188 | done();
189 | });
190 | });
191 |
192 | it('home page should be replicated', function() {
193 | return apos.docs.db.find({ level: 0, slug: '/' }).toArray().then(function(homes) {
194 | assert(homes);
195 | assert(homes.length === 10);
196 | const homeLocales = _.pluck(homes, 'workflowLocale');
197 | assert(!Object.keys(apos.workflow.locales).find(function(locale) {
198 | return (!(homeLocales.indexOf(locale) !== -1));
199 | }));
200 | });
201 | });
202 |
203 | it('global doc page should be replicated', function() {
204 | return apos.docs.db.find({ type: 'apostrophe-global' }).toArray().then(function(globals) {
205 | assert(globals);
206 | assert(globals.length === 10);
207 | const globalLocales = _.pluck(globals, 'workflowLocale');
208 | assert(!Object.keys(apos.workflow.locales).find(function(locale) {
209 | return (!(globalLocales.indexOf(locale) !== -1));
210 | }));
211 | });
212 | });
213 |
214 | it('parked test page should be replicated', function() {
215 | return apos.docs.db.find({ slug: '/parked-test-page' }).toArray().then(function(test) {
216 | assert(test);
217 | assert(test.length === 10);
218 | const testLocales = _.pluck(test, 'workflowLocale');
219 | assert(!Object.keys(apos.workflow.locales).find(function(locale) {
220 | return (!(testLocales.indexOf(locale) !== -1));
221 | }));
222 | });
223 | });
224 |
225 | it('es-mx-draft parked page should get content of es-draft, not default', function() {
226 | return apos.docs.db.find({ slug: '/parked-test-page', workflowLocale: 'es-mx-draft' }).toArray().then(function(pages) {
227 | assert(pages && pages[0]);
228 | assert(pages[0].title === '/parked-test-page: original locale: es-draft');
229 | });
230 | });
231 |
232 | it('Normally inserted subpage exists but was not replicated to new locale', function() {
233 | return apos.docs.db.find({ slug: '/about' }).toArray().then(function(docs) {
234 | // Only default and default-draft
235 | assert(docs.length === 2);
236 | });
237 | });
238 |
239 | });
240 |
241 | function instantiate(locales, callback) {
242 | var apos = require('apostrophe')({
243 | testModule: true,
244 |
245 | modules: {
246 | 'apostrophe-custom-pages': {
247 | addFields: [
248 | {
249 | name: '_featured',
250 | type: 'joinByOne',
251 | withType: 'product'
252 | }
253 | ]
254 | },
255 | 'apostrophe-pages': {
256 | park: [
257 | {
258 | type: 'testPage',
259 | slug: '/parked-test-page',
260 | parkedId: 'parked-test-page'
261 | }
262 | ],
263 | types: [
264 | {
265 | name: 'home',
266 | label: 'Home'
267 | },
268 | {
269 | name: 'testPage',
270 | label: 'Test Page'
271 | }
272 | ]
273 | },
274 | 'products': {},
275 | 'apostrophe-workflow': {
276 | alias: 'workflow',
277 | locales: locales,
278 | replicateAcrossLocales: false
279 | }
280 | },
281 | afterInit: function(callback) {
282 | assert(apos.workflow);
283 | return callback(null);
284 | },
285 | afterListen: function(err) {
286 | assert(!err);
287 | return callback(null, apos);
288 | }
289 | });
290 | }
291 |
--------------------------------------------------------------------------------
/test/testResolveRelationships.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 |
3 | describe('Resolve Relationships', function() {
4 | this.timeout(20000);
5 | var apos;
6 |
7 | after(function(done) {
8 | require('apostrophe/test-lib/util').destroy(apos, done);
9 | });
10 |
11 | it('should be a property of the apos object', function(done) {
12 | apos = require('apostrophe')({
13 | testModule: true,
14 |
15 | modules: {
16 | 'apostrophe-pages': {
17 | park: [],
18 | types: [
19 | {
20 | name: 'home',
21 | label: 'Home'
22 | },
23 | {
24 | name: 'testPage',
25 | label: 'Test Page'
26 | }
27 | ]
28 | },
29 | 'apostrophe-workflow': {
30 | locales: [
31 | {
32 | name: 'en',
33 | children: [
34 | {
35 | name: 'fr'
36 | },
37 | {
38 | name: 'de'
39 | }
40 | ]
41 | }
42 | ],
43 | defaultLocale: 'en',
44 | alias: 'workflow' // for testing only!
45 | },
46 | 'products': {
47 | extend: 'apostrophe-pieces',
48 | name: 'product',
49 | alias: 'products',
50 | addFields: [
51 | {
52 | name: '_specialists',
53 | type: 'joinByArray'
54 | },
55 | {
56 | name: '_expert',
57 | withType: 'specialist',
58 | type: 'joinByOne'
59 | }
60 | ]
61 | },
62 | 'specialists': {
63 | extend: 'apostrophe-pieces',
64 | name: 'specialist',
65 | alias: 'specialists'
66 | }
67 | },
68 | afterInit: function(callback) {
69 | assert(apos.modules['apostrophe-workflow']);
70 | return callback(null);
71 | },
72 | afterListen: function(err) {
73 | assert(!err);
74 | done();
75 | }
76 | });
77 | });
78 |
79 | it('Add products and specialists as drafts, with joins; confirm relationships mapped correctly in new locale', () => {
80 | var req = apos.tasks.getReq();
81 | var specialist1 = apos.specialists.newInstance();
82 | var specialist2 = apos.specialists.newInstance();
83 | var product1 = apos.products.newInstance();
84 | var product2 = apos.products.newInstance();
85 | specialist1.title = 'specialist 1';
86 | specialist2.title = 'specialist 2';
87 | return apos.specialists.insert(req, specialist1).then(function() {
88 | return apos.specialists.insert(req, specialist2);
89 | }).then(function() {
90 | product1.title = 'product 1';
91 | product1.specialistsIds = [ specialist1._id ];
92 | product2.title = 'product 2';
93 | product2.specialistsIds = [ specialist2._id ];
94 | product1.expertId = specialist1._id;
95 | return apos.products.insert(req, product1);
96 | }).then(function(product1) {
97 | assert(product1.workflowLocale === 'en-draft');
98 | return apos.products.insert(req, product2);
99 | }).then(function(product2) {
100 | assert(product2.workflowLocale === 'en-draft');
101 | var frReq = apos.tasks.getReq({ locale: 'fr-draft' });
102 | return apos.products.find(frReq, { title: 'product 1' }).toObject();
103 | }).then(function(product1) {
104 | var frReq = apos.tasks.getReq({ locale: 'fr-draft' });
105 | assert(product1.workflowLocale === 'fr-draft');
106 | assert(product1._specialists[0].title === 'specialist 1');
107 | assert(product1._specialists[0].workflowLocale === 'fr-draft');
108 | assert(product1._expert.title === 'specialist 1');
109 | assert(product1._expert.workflowLocale === 'fr-draft');
110 | return apos.products.find(frReq, { title: 'product 2' }).toObject();
111 | }).then(function(product2) {
112 | assert(product2.workflowLocale === 'fr-draft');
113 | assert(product2._specialists[0].title === 'specialist 2');
114 | assert(product2._specialists[0].workflowLocale === 'fr-draft');
115 | assert(!product2._expert);
116 | });
117 | });
118 |
119 | });
120 |
--------------------------------------------------------------------------------
/views/batch-export-modal.html:
--------------------------------------------------------------------------------
1 | {%- extends "apostrophe-modal:base.html" -%}
2 | {%- import "apostrophe-modal:macros.html" as modals -%}
3 | {%- import "apostrophe-ui:components/buttons.html" as buttons with context -%}
4 | {%- import "locale-tree.html" as localeTree -%}
5 |
6 | {%- block modalClass -%}
7 | apos-workflow-export-modal apos-ui-modal-no-sidebar
8 | {%- endblock -%}
9 |
10 | {%- block controls -%}
11 | {{ buttons.minor('Skip', { action: 'cancel' }) }}
12 | {{ buttons.major('Batch Export', { action: 'save' }) }}
13 | {%- endblock -%}
14 |
15 | {%- block label -%}
16 | {{ __ns('apostrophe', 'Exporting changes for %s item(s) (%s)', data.ids.length, data.locale) }}
17 | {%- endblock -%}
18 |
19 | {% block instructions %}
20 |
21 | {{ __ns('apostrophe', 'This change is already committed for the %s locale. To push this change to additional locales, select them below, then click Export. Selecting the %s locale may still be helpful to select sub-locales.', data.locale, data.locale) }}
22 |
23 | {% endblock %}
24 |
25 | {%- block body -%}
26 |
27 | {{ localeTree.tree(
28 | 'locales',
29 | [
30 | {
31 | name: 'locale',
32 | commitLocale: data.locale
33 | }
34 | ],
35 | data.nestedLocales)
36 | }}
37 |
38 | {%- endblock -%}
39 |
40 | {%- block footerContainer -%}{%- endblock -%}
41 |
--------------------------------------------------------------------------------
/views/batch-force-export-modal.html:
--------------------------------------------------------------------------------
1 | {%- extends "apostrophe-modal:base.html" -%}
2 | {%- import "apostrophe-modal:macros.html" as modals -%}
3 | {%- import "apostrophe-ui:components/buttons.html" as buttons with context -%}
4 | {%- import "locale-tree.html" as localeTree -%}
5 |
6 | {%- block modalClass -%}
7 | apos-workflow-batch-export-modal apos-workflow-export-modal apos-ui-modal-no-sidebar
8 | {%- endblock -%}
9 |
10 | {%- block controls -%}
11 | {{ buttons.minor('Skip', { action: 'cancel' }) }}
12 | {{ buttons.major('Export', { action: 'save' }) }}
13 | {%- endblock -%}
14 |
15 | {%- block label -%}
16 | {{ __ns('apostrophe', 'Forcing export of %s item(s) (%s)', data.ids.length, data.locale) }}
17 | {%- endblock -%}
18 |
19 | {% block instructions %}
20 |
21 | {{ __ns('apostrophe', 'You are forcing an export, which will copy the current draft(s) verbatim to other locales. Select them below, then click Export. Selecting the %s locale may still be helpful to select sub-locales.', data.locale, data.locale) }}
22 |
23 | {% endblock %}
24 |
25 | {%- block body -%}
26 |
33 | {# ajax populates me #}
34 |
35 | {{ localeTree.tree(
36 | 'locales',
37 | [
38 | {
39 | name: 'locale'
40 | }
41 | ],
42 | data.nestedLocales)
43 | }}
44 |
45 | {%- endblock -%}
46 |
47 | {%- block footerContainer -%}{%- endblock -%}
48 |
--------------------------------------------------------------------------------
/views/commit-modal.html:
--------------------------------------------------------------------------------
1 | {%- extends "apostrophe-modal:base.html" -%}
2 | {%- import "apostrophe-modal:macros.html" as modals -%}
3 | {%- import "apostrophe-ui:components/buttons.html" as buttons with context -%}
4 | {%- import "apostrophe-ui:components/dropdowns.html" as dropdowns with context -%}
5 |
6 | {%- block modalClass -%}
7 | apos-workflow-commit-modal apos-ui-modal-no-sidebar
8 | {%- endblock -%}
9 |
10 | {%- block controls -%}
11 | {{ buttons.minor('Cancel' if (data.total == 1) else 'Skip', { action: 'cancel' }) }}
12 | {{ buttons.major('Commit', { action: 'save' }) }}
13 | {%- endblock -%}
14 |
15 | {%- block label -%}
16 | {{ __ns('apostrophe', '[%s of %s] Committing %s', data.index, data.total, data.doc.title or data.doc.slug) }}
17 | {%- endblock -%}
18 |
19 | {% block instructions %}
20 | {% if not data.lead %}
21 |
22 | {{ __ns('apostrophe', 'This related item may also be visible on the page. Please consider it first so that your final commit is more complete.') }}
23 |
24 |
25 | {{ __ns('apostrophe', 'Commit all like this') }}
26 | {{ __ns('apostrophe', 'Skip all like this') }}
27 |
28 | {% else %}
29 |
30 | {{ __ns('apostrophe', 'Click Commit to make these changes live.') }}
31 |
32 | {% endif %}
33 | {% endblock %}
34 |
35 | {%- block body -%}
36 | {% if data.modifiedFields.length %}
37 |
38 |
{{ __ns('apostrophe', 'Modified fields: %s', data.modifiedFields | join(', ')) }}
39 | {% if data.doc.workflowMovedIsNew %}
40 |
The page was moved to a new location.
41 | {% endif %}
42 |
Also see below for content edited in context.
43 |
44 | {% endif %}
45 |
46 | {% if data.preview %}
47 | {{ data.preview }}
48 | {% elseif data.doc._url %}
49 |
50 | {% else %}
51 |
No preview available.
52 | {% endif %}
53 |
54 | {%- endblock -%}
55 |
56 | {%- block footerContainer -%}{%- endblock -%}
57 |
--------------------------------------------------------------------------------
/views/export-modal.html:
--------------------------------------------------------------------------------
1 | {%- extends "apostrophe-modal:base.html" -%}
2 | {%- import "apostrophe-modal:macros.html" as modals -%}
3 | {%- import "apostrophe-ui:components/buttons.html" as buttons with context -%}
4 | {%- import "locale-tree.html" as localeTree -%}
5 |
6 | {%- block modalClass -%}
7 | apos-workflow-export-modal apos-ui-modal-no-sidebar
8 | {%- endblock -%}
9 |
10 | {%- block controls -%}
11 | {{ buttons.minor('Skip', { action: 'cancel' }) }}
12 | {{ buttons.major('Export', { action: 'save' }) }}
13 | {%- endblock -%}
14 |
15 | {%- block label -%}
16 | {{ __ns('apostrophe', 'Exporting changes from %s (%s)', data.commit.to.title or data.commit.to.slug, data.commit.locale) }}
17 | {%- endblock -%}
18 |
19 | {% block instructions %}
20 |
21 | {{ __ns('apostrophe', 'This change is already committed for the %s locale. To push this change to additional locales, select them below, then click Export. Selecting the %s locale may still be helpful to select sub-locales.', data.commit.locale, data.commit.locale) }}
22 |
23 | {% endblock %}
24 |
25 | {%- block body -%}
26 |
27 | {{ localeTree.tree(
28 | 'locales',
29 | [
30 | {
31 | name: 'locale',
32 | commitLocale: data.commit.locale
33 | }
34 | ],
35 | data.nestedLocales)
36 | }}
37 |
38 | {%- endblock -%}
39 |
40 | {%- block footerContainer -%}{%- endblock -%}
41 |
--------------------------------------------------------------------------------
/views/force-export-modal.html:
--------------------------------------------------------------------------------
1 | {%- extends "apostrophe-modal:base.html" -%}
2 | {%- import "apostrophe-modal:macros.html" as modals -%}
3 | {%- import "apostrophe-ui:components/buttons.html" as buttons with context -%}
4 | {%- import "locale-tree.html" as localeTree -%}
5 |
6 | {%- block modalClass -%}
7 | apos-workflow-export-modal apos-ui-modal-no-sidebar
8 | {%- endblock -%}
9 |
10 | {%- block controls -%}
11 | {{ buttons.minor('Skip', { action: 'cancel' }) }}
12 | {{ buttons.major('Force Export', { action: 'save' }) }}
13 | {%- endblock -%}
14 |
15 | {%- block label -%}
16 | {{ __ns('apostrophe', 'Forcing export of %s (%s)', data.doc.title, data.doc.workflowLocale) }}
17 | {%- endblock -%}
18 |
19 | {% block instructions %}
20 |
21 | {{ __ns('apostrophe', 'You are forcing an export, which will copy this draft verbatim to other locales. Select them below, then click Export. Selecting the %s locale may still be helpful to select sub-locales.', data.doc.workflowLocale, data.doc.workflowLocale) }}
22 |
23 | {% if not data.lead %}
24 |
25 | {{ __ns('apostrophe', 'This related item may also be visible. Please consider exporting it too for a more complete result.') }}
26 |
27 |
28 | {# Despite the data attribute name this will do the right operation here #}
29 | {{ __ns('apostrophe', 'Force Export all like this') }}
30 | {{ __ns('apostrophe', 'Skip all like this') }}
31 |
32 | {% endif %}
33 | {% endblock %}
34 |
35 | {%- block body -%}
36 |
37 | Also offer to force export related documents, such as images
38 | {% if data.offerRelatedExisting %}
39 | Offer to force export related documents even if they already exist in the target locale
40 | {% endif %}
41 |
42 |
43 | {{ localeTree.tree(
44 | 'locales',
45 | [
46 | {
47 | name: 'locale'
48 | }
49 | ],
50 | data.nestedLocales)
51 | }}
52 |
53 | {%- endblock -%}
54 |
55 | {%- block footerContainer -%}{%- endblock -%}
56 |
--------------------------------------------------------------------------------
/views/force-export-related-modal.html:
--------------------------------------------------------------------------------
1 | {%- extends "apostrophe-modal:base.html" -%}
2 | {%- import "apostrophe-modal:macros.html" as modals -%}
3 | {%- import "apostrophe-ui:components/buttons.html" as buttons with context -%}
4 | {%- import "locale-tree.html" as localeTree -%}
5 |
6 | {%- block modalClass -%}
7 | apos-workflow-export-modal apos-ui-modal-no-sidebar
8 | {%- endblock -%}
9 |
10 | {%- block controls -%}
11 | {{ buttons.minor('Skip', { action: 'cancel' }) }}
12 | {{ buttons.major('Force Export Related', { action: 'save' }) }}
13 | {%- endblock -%}
14 |
15 | {%- block label -%}
16 | {{ __ns('apostrophe', 'Forcing export of documents related to %s (%s)', data.doc.title, data.doc.workflowLocale) }}
17 | {%- endblock -%}
18 |
19 | {% block instructions %}
20 |
21 | {{ __ns('apostrophe', 'You are forcing an export of documents related to this document, which will copy their drafts verbatim to other locales. Select locales below, and check the box if you wish to force export documents even if they already exist in the other locales.') }}
22 |
23 |
24 | {# Here for compatibility with the inherited logic, will be hidden by JS and always checked #}
25 | Also offer to force export related documents, such as images
26 | {# Really in play #}
27 | {% if data.offerRelatedExisting %}
28 | Offer to force export related documents even if they already exist in the target locale
29 | {% endif %}
30 |
31 | {% endblock %}
32 |
33 | {%- block body -%}
34 |
35 | {{ localeTree.tree(
36 | 'locales',
37 | [
38 | {
39 | name: 'locale'
40 | }
41 | ],
42 | data.nestedLocales)
43 | }}
44 |
45 | {%- endblock -%}
46 |
47 | {%- block footerContainer -%}{%- endblock -%}
48 |
--------------------------------------------------------------------------------
/views/force-export-widget-modal.html:
--------------------------------------------------------------------------------
1 | {%- extends "apostrophe-modal:base.html" -%}
2 | {%- import "apostrophe-modal:macros.html" as modals -%}
3 | {%- import "apostrophe-ui:components/buttons.html" as buttons with context -%}
4 | {%- import "locale-tree.html" as localeTree -%}
5 |
6 | {%- block modalClass -%}
7 | apos-workflow-export-modal apos-ui-modal-no-sidebar
8 | {%- endblock -%}
9 |
10 | {%- block controls -%}
11 | {{ buttons.minor('Skip', { action: 'cancel' }) }}
12 | {{ buttons.major('Export', { action: 'save' }) }}
13 | {%- endblock -%}
14 |
15 | {%- block label -%}
16 | {{ __ns('apostrophe', 'Exporting a single widget from %s (%s)', data.doc.title, data.doc.workflowLocale) }}
17 | {%- endblock -%}
18 |
19 | {% block instructions %}
20 |
21 | {{ __ns('apostrophe', 'To force an export of this widget to additional locales, select them below, then click Export. Selecting the %s locale may still be helpful to select sub-locales.', data.doc.workflowLocale, data.doc.workflowLocale) }}
22 |
23 | {% endblock %}
24 |
25 | {%- block body -%}
26 |
27 | {{ localeTree.tree(
28 | 'locales',
29 | [
30 | {
31 | name: 'locale'
32 | }
33 | ],
34 | data.nestedLocales)
35 | }}
36 |
37 | {%- endblock -%}
38 |
39 | {%- block footerContainer -%}{%- endblock -%}
40 |
--------------------------------------------------------------------------------
/views/history-modal.html:
--------------------------------------------------------------------------------
1 | {%- extends "apostrophe-modal:base.html" -%}
2 | {%- import "apostrophe-modal:macros.html" as modals -%}
3 | {%- import "apostrophe-ui:components/buttons.html" as buttons with context -%}
4 |
5 | {%- block modalClass -%}
6 | apos-workflow-history-modal apos-ui-modal-no-sidebar
7 | {%- endblock -%}
8 |
9 | {%- block controls -%}
10 | {{ buttons.major('Done', { action: 'cancel' }) }}
11 | {%- endblock -%}
12 |
13 | {%- block label -%}
14 | {{ __ns('apostrophe', 'Commit history of %s', data.doc.title) }}
15 | {%- endblock -%}
16 |
17 | {% block instructions %}
18 |
19 | {{ __ns('apostrophe', 'Click on a past commit to review it, revert to it or export it to more locales.' if data.localized else 'Click on a past commit to review it or revert to it.') }}
20 |
21 | {% endblock %}
22 |
23 | {%- block body -%}
24 |
25 | {# Markup follows the pattern of the manage modal markup #}
26 |
27 |
28 | {# May need to add pagination here at some point
29 |
30 |
__ns('apostrophe', 'Showing %s - %s of %s results', from, to, total)
31 |
32 | #}
33 |
52 |
53 |
54 |
55 | {%- endblock -%}
56 |
--------------------------------------------------------------------------------
/views/locale-picker-modal.html:
--------------------------------------------------------------------------------
1 | {%- macro picker(localizations, nestedLocales, crossDomainSessionToken) -%}
2 |
3 | {%- for locale in nestedLocales -%}
4 | {%- if data.workflowMode == 'live' -%}
5 | {%- set localization = localizations[locale.name] -%}
6 | {%- else %}
7 | {%- set localization = localizations[locale.name + '-draft'] -%}
8 | {% endif %}
9 |
10 | {%- if localization and localization._url -%}
11 | {{ locale.label or locale.name }}
17 | {%- elseif data.workflowMode == 'draft' -%}
18 | {{ locale.label or locale.name }}
24 | {%- else -%}
25 | {{ locale.label or locale.name }}
26 | {%- endif -%}
27 | {%- if locale.children -%}
28 | {{ picker(localizations, locale.children, crossDomainSessionToken) }}
29 | {%- endif -%}
30 |
31 | {%- endfor -%}
32 |
33 | {%- endmacro -%}
34 |
35 | {%- extends "apostrophe-modal:base.html" -%}
36 | {%- import "apostrophe-modal:macros.html" as modals -%}
37 | {%- import "apostrophe-ui:components/buttons.html" as buttons with context -%}
38 |
39 | {%- block modalClass -%}
40 | apos-workflow-locale-picker-modal apos-ui-modal-no-sidebar
41 | {%- endblock -%}
42 |
43 | {%- block controls -%}
44 | {{ buttons.minor('Cancel', { action: 'cancel' }) }}
45 | {%- endblock -%}
46 |
47 | {%- block label -%}
48 | {{ __ns('apostrophe', 'Switch Locales') }}
49 | {%- endblock -%}
50 |
51 | {% block instructions %}
52 |
53 | {{ __ns('apostrophe', 'Click on any locale to switch to viewing it.') }}
54 |
55 | {% endblock %}
56 |
57 | {%- block body -%}
58 |
59 | {{ picker(data.localizations, data.nestedLocales, data.crossDomainSessionToken) }}
60 |
61 | {%- endblock -%}
62 |
63 | {%- block footerContainer -%}{%- endblock -%}
64 |
--------------------------------------------------------------------------------
/views/locale-picker.html:
--------------------------------------------------------------------------------
1 | {# This is an example of a public locale picker. It displays only public locales, #}
2 | {# in a flat list based on the original nested tree intended for editors. #}
3 | {# It is expected that you will override this template or write your own. #}
4 |
5 |
6 | {% for localization in apos.workflow.localizations() %}
7 |
8 | {%- macro tree(prefix, checkboxes, nestedLocales) -%}
9 |
25 | {%- endmacro -%}
26 |
--------------------------------------------------------------------------------
/views/locale-tree.html:
--------------------------------------------------------------------------------
1 | {%- macro tree(prefix, controls, nestedLocales) -%}
2 |
37 | {%- endmacro -%}
38 |
--------------------------------------------------------------------------------
/views/locale-unavailable-modal.html:
--------------------------------------------------------------------------------
1 | {%- extends "apostrophe-modal:base.html" -%}
2 | {%- import "apostrophe-modal:macros.html" as modals -%}
3 | {%- import "apostrophe-ui:components/buttons.html" as buttons with context -%}
4 |
5 | {%- block modalClass -%}
6 | apos-workflow-locale-unavailable-modal apos-ui-modal-no-sidebar
7 | {%- endblock -%}
8 |
9 | {%- block controls -%}
10 | {{ buttons.minor('Cancel', { action: 'cancel' }) }}
11 | {{ buttons.major('Activate', { action: 'save' }) }}
12 | {%- endblock -%}
13 |
14 | {%- block label -%}
15 | {{ __ns('apostrophe', 'Activate document for editing in new locale') }}
16 | {%- endblock -%}
17 |
18 | {% block instructions %}
19 |
20 | {{ __ns('apostrophe', 'Click Activate to make the document available for editing in the new locale.') }}
21 |
22 | {% endblock %}
23 |
24 | {%- block body -%}
25 |
26 |
27 | {{ __ns('apostrophe', 'That document is not currently available for editing in the %s locale.', data.locale) }}
28 |
29 |
30 | {% if (data.status == 'notfound') %}
31 | {{ __ns('apostrophe', 'That document does not exist yet in the %s locale. If you click "Activate," the current locale\'s content will be exported to it, and it will become available to edit as a draft.', data.locale) }}
32 | {% elseif (data.status == 'inTrash') %}
33 | {{ __ns('apostrophe', 'It does look like that document has been edited in that locale in the past,
34 | but it is currently in the trash. If you click "Activate," it will be removed
35 | from the trash.') }}
36 | {% elseif (data.status == 'newInTrash') %}
37 | {{ __ns('apostrophe', 'That document has never been active for that locale. If you click "Activate," the current locale\'s content will be exported to it, and it will become available to edit as a draft.') }}
38 | {% endif %}
39 |
40 |
41 | {%- endblock -%}
42 |
43 | {%- block footerContainer -%}{%- endblock -%}
44 |
--------------------------------------------------------------------------------
/views/manage-modal.html:
--------------------------------------------------------------------------------
1 | {%- extends "apostrophe-modal:base.html" -%}
2 | {%- import "apostrophe-modal:macros.html" as modals -%}
3 | {%- import "apostrophe-ui:components/buttons.html" as buttons with context -%}
4 |
5 | {%- block modalClass -%}
6 | {{ 'apos-workflow-submit-modal' | css }} apos-ui-modal-no-sidebar
7 | {%- endblock -%}
8 |
9 | {%- block controls -%}
10 | {{ buttons.minor('Done', { action: 'cancel' }) }}
11 | {%- endblock -%}
12 |
13 | {%- block label -%}
14 | {{ __ns('apostrophe', 'Submitted for ' + data.label) }}
15 | {%- endblock -%}
16 |
17 | {% block instructions %}
18 |
19 | {{ __ns('apostrophe', 'Click on any document to view it. Once there you can edit or commit it.') }}
20 |
21 | {% endblock %}
22 |
23 | {%- block body -%}
24 |
25 |
26 |
27 |
28 |
29 | {{ __ns('apostrophe', 'Document') }}
30 | {{ __ns('apostrophe', 'Type') }}
31 | {{ __ns('apostrophe', 'Last Submitted By') }}
32 | {{ __ns('apostrophe', 'Last Submitted At') }}
33 | {{ __ns('apostrophe', 'Actions') }}
34 |
35 |
36 |
37 | {% for doc in data.submitted %}
38 |
39 | {% if apos.utils.beginsWith(doc.slug, '/') %}
40 | {{ doc.title or doc.slug }}
41 | {% else %}
42 | {{ doc.title or doc.slug }}
43 | {% endif %}
44 | {{ doc.workflowSubmitted.type }}
45 | {{ doc.workflowSubmitted.name }}
46 | {{ doc.workflowSubmitted.when }}
47 |
48 | {% if apos.utils.beginsWith(doc.slug, '/') %}
49 | {{ __ns('apostrophe', 'Edit') }}
50 | {% else %}
51 | {{ __ns('apostrophe', 'Edit') }}
52 | {% endif %}
53 | {{ __ns('apostrophe', 'Dismiss') }}
54 |
55 |
56 | {% endfor %}
57 |
58 |
59 |
60 |
61 | {%- endblock -%}
62 |
63 | {%- block footerContainer -%}{%- endblock -%}
64 |
--------------------------------------------------------------------------------
/views/menu.html:
--------------------------------------------------------------------------------
1 | {%- import 'apostrophe-ui:components/buttons.html' as buttons -%}
2 |
3 | {%- set workflowMode = data.workflowMode -%}
4 | {% if data.workflowPreview %}
5 | {%- set workflowMode = 'preview' -%}
6 | {% endif %}
7 |
8 |
39 |
--------------------------------------------------------------------------------
/views/relatedByType.html:
--------------------------------------------------------------------------------
1 | {% if not data.types.length %}
2 | {{ __ns('apostrophe', 'There are no related documents.') }}
3 | {% else %}
4 | {{ __ns('apostrophe', 'Only related documents of the types you choose below will be exported.') }}
5 | {% for type in data.types %}
6 |
12 | {% endfor %}
13 | {% endif %}
14 |
--------------------------------------------------------------------------------
/views/review-modal.html:
--------------------------------------------------------------------------------
1 | {# Similar to the commit modal, this modal allows us to review an existing commit's diff in the
2 | preview iframe and, if localized, decide whether to export it to more locales. #}
3 |
4 | {%- extends "apostrophe-modal:base.html" -%}
5 | {%- import "apostrophe-modal:macros.html" as modals -%}
6 | {%- import "apostrophe-ui:components/buttons.html" as buttons with context -%}
7 |
8 | {%- block modalClass -%}
9 | apos-workflow-review-modal apos-ui-modal-no-sidebar
10 | {%- endblock -%}
11 |
12 | {%- block controls -%}
13 | {{ buttons.minor('Cancel' if data.localized else 'Finished', { action: 'cancel' }) }}
14 | {%- if data.localized -%}
15 | {{ buttons.major('Export', { action: 'save' }) }}
16 | {%- endif -%}
17 | {%- endblock -%}
18 |
19 | {%- block label -%}
20 | {{ __ns('apostrophe', 'Reviewing past commit to %s (%s)', data.doc.title or data.doc.slug, data.doc.workflowLocale) }}
21 | {%- endblock -%}
22 |
23 | {% block instructions %}
24 |
25 | {{ __ns('apostrophe', 'Click Export to send these past changes to more locales.' if data.localized else 'Review the changes below.') }}
26 |
27 | {% endblock %}
28 |
29 | {%- block body -%}
30 | {% if data.modifiedFields.length %}
31 |
32 |
{{ __ns('apostrophe', 'Modified fields: %s', data.modifiedFields | join(', ')) }}
33 |
Also see below for content edited in context.
34 |
35 | {% endif %}
36 |
37 | {% if data.preview %}
38 | {{ data.preview }}
39 | {% elseif data.doc._url %}
40 |
41 | {% else %}
42 |
No preview available.
43 | {% endif %}
44 |
45 | {%- endblock -%}
46 |
47 | {%- block footerContainer -%}{%- endblock -%}
48 |
--------------------------------------------------------------------------------