-
-
-
+
);
}
+ /**
+ * Renders a list of items in a Campaign.
+ *
+ * @return object
+ */
+ renderItemListView() {
+ const props = {
+ campaignId: this.props.campaignId,
+ itemListViewEndpoint: this.props.config.itemListViewEndpoint,
+ };
+
+ return (
+
+ );
+ }
+
+ renderDetailEditView() {
+ return
Edit
;
+ }
+
+ /**
+ * Hook to allow customisation of components being constructed by FormBuilder.
+ *
+ * @param object Component - Component constructor.
+ * @param object props - Props passed from FormBuilder.
+ *
+ * @return object - Instanciated React component
+ */
+ createFn(Component, props) {
+ const campaignViewRoute = this.props.config.campaignViewRoute;
+
+ if (props.component === 'GridField') {
+ const extendedProps = Object.assign({}, props, {
+ data: Object.assign({}, props.data, {
+ handleDrillDown: (event, record) => {
+ // Set url and set list
+ const path = campaignViewRoute
+ .replace(/:type\?/, 'set')
+ .replace(/:id\?/, record.ID)
+ .replace(/:view\?/, 'show');
+
+ window.ss.router.show(path);
+ },
+ }),
+ });
+
+ return
;
+ }
+
+ return
;
+ }
+
+ /**
+ * Gets preview URL for itemid
+ * @param int id
+ * @returns string
+ */
+ previewURLForItem(id) {
+ if (!id) {
+ return '';
+ }
+
+ // hard code in baseurl for any itemid preview url
+ return document.getElementsByTagName('base')[0].href;
+ }
+
addCampaign() {
// Add campaign
}
@@ -50,7 +150,15 @@ CampaignAdminContainer.propTypes = {
function mapStateToProps(state, ownProps) {
return {
config: state.config.sections[ownProps.sectionConfigKey],
+ campaignId: state.campaign.campaignId,
+ view: state.campaign.view,
};
}
-export default connect(mapStateToProps)(CampaignAdminContainer);
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators(actions, dispatch),
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(CampaignAdminContainer);
diff --git a/admin/javascript/src/sections/campaign-admin/item.js b/admin/javascript/src/sections/campaign-admin/item.js
new file mode 100644
index 000000000..8ee05e458
--- /dev/null
+++ b/admin/javascript/src/sections/campaign-admin/item.js
@@ -0,0 +1,49 @@
+import React from 'react';
+import SilverStripeComponent from 'silverstripe-component';
+
+/**
+ * Describes an individual campaign item
+ */
+class CampaignItem extends SilverStripeComponent {
+ render() {
+ let thumbnail = '';
+ let badge = '';
+ const item = this.props.item;
+
+ // change badge
+ switch (item.ChangeType) {
+ case 'created':
+ badge =
Draft;
+ break;
+ case 'modified':
+ badge =
Modified;
+ break;
+ case 'deleted':
+ badge =
Removed;
+ break;
+ case 'none':
+ default:
+ badge =
Already published;
+ break;
+ }
+
+ // Linked items
+ let links =
[lk] 3 links;
+
+ // Thumbnail
+ if (item.Thumbnail) {
+ thumbnail =

;
+ }
+
+
+ return (
+
+ {thumbnail}
+
{item.Title}
+ {links}
+ {badge}
+
+ );
+ }
+}
+export default CampaignItem;
diff --git a/admin/javascript/src/sections/campaign-admin/list.js b/admin/javascript/src/sections/campaign-admin/list.js
new file mode 100644
index 000000000..5c86d20be
--- /dev/null
+++ b/admin/javascript/src/sections/campaign-admin/list.js
@@ -0,0 +1,156 @@
+import React from 'react';
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+import * as actions from 'state/records/actions';
+import SilverStripeComponent from 'silverstripe-component';
+import Accordion from 'components/accordion/index';
+import AccordionGroup from 'components/accordion/group';
+import AccordionItem from 'components/accordion/item';
+import NorthHeader from 'components/north-header/index';
+import CampaignItem from './item';
+import CampaignPreview from './preview';
+
+/**
+ * Represents a campaign list view
+ */
+class CampaignListContainer extends SilverStripeComponent {
+
+ componentDidMount() {
+ const fetchURL = this.props.itemListViewEndpoint.replace(/:id/, this.props.campaignId);
+ super.componentDidMount();
+ this.props.actions.fetchRecord('ChangeSet', 'get', fetchURL);
+ }
+
+ /**
+ * Renders a list of items in a Campaign.
+ *
+ * @return object
+ */
+ render() {
+ const itemID = 1; // todo - hook up to "click" handler for changesetitems
+ const campaignId = this.props.campaignId;
+
+ // Trigger different layout when preview is enabled
+ const previewUrl = this.previewURLForItem(itemID);
+ const itemGroups = this.groupItemsForSet();
+ const classNames = previewUrl ? 'cms-middle with-preview' : 'cms-middle no-preview';
+
+ // Get items in this set
+ let accordionGroups = [];
+
+ Object.keys(itemGroups).forEach(className => {
+ const group = itemGroups[className];
+ const groupCount = group.items.length;
+
+ let accordionItems = [];
+ let title = `${groupCount} ${groupCount === 1 ? group.singular : group.plural}`;
+ let groupid = `Set_${campaignId}_Group_${className}`;
+
+ // Create items for this group
+ group.items.forEach(item => {
+ // Add extra css class for published items
+ let itemClassName = '';
+
+ if (item.ChangeType === 'none') {
+ itemClassName = 'list-group-item--published';
+ }
+
+ accordionItems.push(
+
+
+
+ );
+ });
+
+ // Merge into group
+ accordionGroups.push(
+
+ {accordionItems}
+
+ );
+ });
+
+ return (
+
+
+
+
+
+ {accordionGroups}
+
+
+
+ { previewUrl &&
}
+
+ );
+ }
+
+
+ /**
+ * Gets preview URL for itemid
+ * @param int id
+ * @returns string
+ */
+ previewURLForItem(id) {
+ if (!id) {
+ return '';
+ }
+
+ // hard code in baseurl for any itemid preview url
+ return document.getElementsByTagName('base')[0].href;
+ }
+
+ /**
+ * Group items for changeset display
+ *
+ * @return array
+ */
+ groupItemsForSet() {
+ const groups = {};
+ if (!this.props.record || !this.props.record._embedded) {
+ return groups;
+ }
+ const items = this.props.record._embedded.ChangeSetItems;
+
+ // group by whatever
+ items.forEach(item => {
+ // Create new group if needed
+ const classname = item.BaseClass;
+
+ if (!groups[classname]) {
+ groups[classname] = {
+ singular: item.Singular,
+ plural: item.Plural,
+ items: [],
+ };
+ }
+
+ // Push items
+ groups[classname].items.push(item);
+ });
+
+ return groups;
+ }
+
+}
+
+function mapStateToProps(state, ownProps) {
+ // Find record specific to this item
+ let record = null;
+ if (state.records && state.records.ChangeSet && ownProps.campaignId) {
+ record = state.records.ChangeSet.find(
+ (nextRecord) => (nextRecord.ID === parseInt(ownProps.campaignId, 10))
+ );
+ }
+ return {
+ record: record || [],
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators(actions, dispatch),
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(CampaignListContainer);
diff --git a/admin/javascript/src/sections/campaign-admin/preview.js b/admin/javascript/src/sections/campaign-admin/preview.js
new file mode 100644
index 000000000..e5e22c991
--- /dev/null
+++ b/admin/javascript/src/sections/campaign-admin/preview.js
@@ -0,0 +1,15 @@
+import React from 'react';
+import SilverStripeComponent from 'silverstripe-component';
+
+class CampaignPreview extends SilverStripeComponent {
+
+ render() {
+ return (
+
+
+
+ );
+ }
+}
+
+export default CampaignPreview;
diff --git a/admin/javascript/src/sections/campaign-admin/styles.scss b/admin/javascript/src/sections/campaign-admin/styles.scss
index 956a32f21..f6f517c97 100644
--- a/admin/javascript/src/sections/campaign-admin/styles.scss
+++ b/admin/javascript/src/sections/campaign-admin/styles.scss
@@ -1,3 +1,210 @@
.CampaignAdmin {
- overflow-y: auto;
-}
\ No newline at end of file
+ overflow-y: auto;
+ display: block;
+
+ // Contains campaign form and preview layout
+ .cms-middle {
+ padding-left: 0;
+ height: 100%;
+ position: relative;
+ transition: padding .2s;
+
+ &.with-preview {
+ @media (min-width: 992px) { /* lg */
+ padding-left: 316px;
+ .cms-campaigns {
+ width: 316px;
+ }
+ }
+ @media (min-width: 1200px) { /* xl */
+ padding-left: 448px;
+ .cms-campaigns {
+ width: 448px;
+ }
+ }
+ }
+ }
+
+ /* CAMPAIGNS */
+ .cms-campaigns {
+ width: 100%;
+ position: absolute;
+ left: 0;
+ top: 0;
+ height: 100%;
+ overflow: hidden;
+ background-color: #f6f7f8;
+ z-index: 2;
+ transition: width .2s;
+ padding-bottom: 53px;
+
+ .campaign-items {
+ height: calc(100% - 53px);
+ margin-bottom: 40px;
+ overflow: auto;
+
+ h4 {
+ font-weight: 400;
+ font-size: 14px;
+ margin: 2px 0 5px;
+ }
+
+ .list-group {
+ margin-left: -0.9375rem;
+ margin-right: -0.9375rem;
+
+ border-bottom: 1px solid #ddd;
+ margin-bottom: .75rem;
+
+ .list-group-item {
+ padding-left: 0.9375rem; /* would normally be set in variables as 20px */
+ padding-right: 0.9375rem;
+ min-height: 64px;
+ cursor: pointer;
+ text-decoration: none;
+
+ &:first-child {
+ border-top: none;
+ }
+
+ .item__thumbnail {
+ width: 64px;
+ height: 64px;
+ display: block;
+ background: #ccc;
+ float: left;
+ margin: -12px 12px 0 -12px;
+ }
+
+ .label {
+ text-transform: uppercase;
+ font-size: 10px;
+ font-weight: 400;
+ letter-spacing: .4px;
+ }
+
+ // On hover show all linked items
+ .list-group-item__linked {
+ color: $body-color;
+ float: right;
+ }
+ }
+ }
+ }
+ }
+
+ // Hover items
+ .list-group-item--published {
+ opacity: .6;
+ }
+ .list-group-item--published:hover {
+ opacity: 1;
+ }
+ .item_visible-hovered {
+ opacity: 0;
+ transition: opacity .2s ease-in-out;
+ }
+ a:hover .item_visible-hovered,
+ a.active.list-group-item--published,
+ a.active.list-group-item--published .item_visible-hovered {
+ opacity: 1;
+ }
+
+ // Accordion styles
+ .accordion-group {
+ margin-top: 16px;
+
+ &__title {
+ margin-bottom: 0;
+
+ a {
+ font-size: 12px; /* extend table header */
+ font-weight: 400;
+ text-transform: uppercase;
+ padding: 1rem 0.9375rem;
+ display: block;
+ margin-left: -0.9375rem;
+ margin-right: -0.9375rem;
+ color: #555;
+ text-decoration: none;
+ position: relative;
+ border-bottom: 1px solid #ddd;
+
+
+ &::before {
+ content: "-";
+ width: 44px;
+ height: 44px;
+ padding: 10px 0.9375rem;
+ position: absolute;
+ right: 0;
+ top: 0;
+ font-size: 16px;
+ line-height: 20px;
+ text-align: center;
+ }
+
+ &.collapsed::before {
+ content: "+";
+ }
+ }
+ }
+ }
+
+// TO MOVE
+
+ /* Preview panel */
+ .pages-preview {
+ display: block;
+ position: relative;
+ background-color: #BBB;
+ border-left: 1px solid #dbdde0;
+ height: 100%;
+
+ iframe {
+ width: 100%;
+ height: calc(100% - 53px);
+ border: none;
+ }
+ }
+
+ ///* btn toolbar */
+ //.cms-south-actions {
+ // height: 53px;
+ // position: absolute;
+ // bottom: 0;
+ // width: 100%;
+ // border-top: 1px solid #ddd;
+ // background-color: #f6f7f8;
+ // padding: 8px 15px;
+ //}
+ //.btn-toolbar {
+ //
+ //}
+ //.btn-toolbar .btn {
+ // font-size: 13px;
+ // line-height: 20px;
+ //}
+ //
+
+ //
+ //.popover-content a {
+ // display: inline-block;
+ // width: 100%;
+ //}
+ //.icon-back {
+ // display: inline-block;
+ // margin-right: 12px;
+ //}
+ //.icon-back + h1 {
+ // display: inline-block;
+ //}
+ //
+
+ //.btn .label_empty {
+ // border-radius: 50%;
+ // height: 10px;
+ // width: 10px;
+ // top: 1px;
+ //}
+}
diff --git a/admin/javascript/src/silverstripe-backend.js b/admin/javascript/src/silverstripe-backend.js
index 5c7c4e609..d789f4ba8 100644
--- a/admin/javascript/src/silverstripe-backend.js
+++ b/admin/javascript/src/silverstripe-backend.js
@@ -307,11 +307,20 @@ class SilverStripeBackend {
/**
* Makes a network request using the GET HTTP verb.
*
+ * @experimental
+ *
* @param string url - Endpoint URL.
+ * @param object data - Data to send with the request.
+ * @param Array headers
* @return object - Promise
*/
- get(url) {
- return this.fetch(url, { method: 'get', credentials: 'same-origin' })
+ get(url, data = {}, headers = {}) {
+ return this.fetch(url, {
+ method: 'get',
+ credentials: 'same-origin',
+ headers,
+ body: data,
+ })
.then(checkStatus);
}
@@ -320,14 +329,14 @@ class SilverStripeBackend {
*
* @param string url - Endpoint URL.
* @param object data - Data to send with the request.
+ * @param Array headers
* @return object - Promise
*/
- post(url, data) {
+ post(url, data = {}, headers = {}) {
+ const defaultHeaders = { 'Content-Type': 'application/x-www-form-urlencoded' };
return this.fetch(url, {
method: 'post',
- headers: new Headers({
- 'Content-Type': 'application/x-www-form-urlencoded',
- }),
+ headers: Object.assign({}, defaultHeaders, headers),
credentials: 'same-origin',
body: data,
})
@@ -339,10 +348,11 @@ class SilverStripeBackend {
*
* @param string url - Endpoint URL.
* @param object data - Data to send with the request.
+ * @param Array headers
* @return object - Promise
*/
- put(url, data) {
- return this.fetch(url, { method: 'put', credentials: 'same-origin', body: data })
+ put(url, data = {}, headers = {}) {
+ return this.fetch(url, { method: 'put', credentials: 'same-origin', body: data, headers })
.then(checkStatus);
}
@@ -351,10 +361,11 @@ class SilverStripeBackend {
*
* @param string url - Endpoint URL.
* @param object data - Data to send with the request.
+ * @param Array headers
* @return object - Promise
*/
- delete(url, data) {
- return this.fetch(url, { method: 'delete', credentials: 'same-origin', body: data })
+ delete(url, data = {}, headers = {}) {
+ return this.fetch(url, { method: 'delete', credentials: 'same-origin', body: data, headers })
.then(checkStatus);
}
diff --git a/admin/javascript/src/state/campaign/action-types.js b/admin/javascript/src/state/campaign/action-types.js
new file mode 100644
index 000000000..2aa52e7dc
--- /dev/null
+++ b/admin/javascript/src/state/campaign/action-types.js
@@ -0,0 +1,3 @@
+export default {
+ SET_CAMPAIGN_ACTIVE_CHANGESET: 'SET_CAMPAIGN_ACTIVE_CHANGESET',
+};
diff --git a/admin/javascript/src/state/campaign/actions.js b/admin/javascript/src/state/campaign/actions.js
new file mode 100644
index 000000000..5d2700895
--- /dev/null
+++ b/admin/javascript/src/state/campaign/actions.js
@@ -0,0 +1,15 @@
+import ACTION_TYPES from './action-types';
+
+/**
+ * Show specified campaign set
+ *
+ * @param number campaignId - ID of the Campaign to show.
+ * @param string view - The view mode to display the Campaign in.
+ */
+export function showCampaignView(campaignId, view) {
+ return {
+ type: ACTION_TYPES.SET_CAMPAIGN_ACTIVE_CHANGESET,
+ payload: { campaignId, view },
+ };
+}
+
diff --git a/admin/javascript/src/state/campaign/reducer.js b/admin/javascript/src/state/campaign/reducer.js
new file mode 100644
index 000000000..5bad3aeb6
--- /dev/null
+++ b/admin/javascript/src/state/campaign/reducer.js
@@ -0,0 +1,24 @@
+import deepFreeze from 'deep-freeze';
+import ACTION_TYPES from './action-types';
+
+const initialState = {
+ campaignId: null,
+ view: null,
+};
+
+function campaignReducer(state = initialState, action) {
+ switch (action.type) {
+
+ case ACTION_TYPES.SET_CAMPAIGN_ACTIVE_CHANGESET:
+ return deepFreeze(Object.assign({}, state, {
+ campaignId: action.payload.campaignId,
+ view: action.payload.view,
+ }));
+
+ default:
+ return state;
+
+ }
+}
+
+export default campaignReducer;
diff --git a/admin/javascript/src/state/records/action-types.js b/admin/javascript/src/state/records/action-types.js
index 186cae2f2..684d468be 100644
--- a/admin/javascript/src/state/records/action-types.js
+++ b/admin/javascript/src/state/records/action-types.js
@@ -5,6 +5,9 @@ export default {
FETCH_RECORDS_REQUEST: 'FETCH_RECORDS_REQUEST',
FETCH_RECORDS_FAILURE: 'FETCH_RECORDS_FAILURE',
FETCH_RECORDS_SUCCESS: 'FETCH_RECORDS_SUCCESS',
+ FETCH_RECORD_REQUEST: 'FETCH_RECORD_REQUEST',
+ FETCH_RECORD_FAILURE: 'FETCH_RECORD_FAILURE',
+ FETCH_RECORD_SUCCESS: 'FETCH_RECORD_SUCCESS',
DELETE_RECORD_REQUEST: 'DELETE_RECORD_REQUEST',
DELETE_RECORD_FAILURE: 'DELETE_RECORD_FAILURE',
DELETE_RECORD_SUCCESS: 'DELETE_RECORD_SUCCESS',
diff --git a/admin/javascript/src/state/records/actions.js b/admin/javascript/src/state/records/actions.js
index 44de8aa15..0fa7b8b20 100644
--- a/admin/javascript/src/state/records/actions.js
+++ b/admin/javascript/src/state/records/actions.js
@@ -26,12 +26,13 @@ function populate(str, params) {
*/
export function fetchRecords(recordType, method, url) {
const payload = { recordType };
+ const headers = { Accept: 'text/json' };
return (dispatch) => {
dispatch({
type: ACTION_TYPES.FETCH_RECORDS_REQUEST,
payload,
});
- return backend[method.toLowerCase()](populate(url, payload))
+ return backend[method.toLowerCase()](populate(url, payload), {}, headers)
.then(response => response.json())
.then(json => {
dispatch({
@@ -48,6 +49,39 @@ export function fetchRecords(recordType, method, url) {
};
}
+
+/**
+ * Fetches a single record
+ *
+ * @param string recordType Type of record (the "class name")
+ * @param string method HTTP method
+ * @param string url API endpoint
+ */
+export function fetchRecord(recordType, method, url) {
+ const payload = { recordType };
+ const headers = { Accept: 'text/json' };
+ return (dispatch) => {
+ dispatch({
+ type: ACTION_TYPES.FETCH_RECORD_REQUEST,
+ payload,
+ });
+ return backend[method.toLowerCase()](populate(url, payload), {}, headers)
+ .then(response => response.json())
+ .then(json => {
+ dispatch({
+ type: ACTION_TYPES.FETCH_RECORD_SUCCESS,
+ payload: { recordType, data: json },
+ });
+ })
+ .catch((err) => {
+ dispatch({
+ type: ACTION_TYPES.FETCH_RECORD_FAILURE,
+ payload: { error: err, recordType },
+ });
+ });
+ };
+}
+
/**
* Deletes a record
*
diff --git a/admin/javascript/src/state/records/reducer.js b/admin/javascript/src/state/records/reducer.js
index 7babf062b..16e1190c9 100644
--- a/admin/javascript/src/state/records/reducer.js
+++ b/admin/javascript/src/state/records/reducer.js
@@ -1,29 +1,24 @@
import deepFreeze from 'deep-freeze';
import ACTION_TYPES from './action-types';
-const initialState = {
-};
+const initialState = {};
function recordsReducer(state = initialState, action) {
let records;
let recordType;
+ let record;
+ let recordIndex;
switch (action.type) {
case ACTION_TYPES.CREATE_RECORD:
- return deepFreeze(Object.assign({}, state, {
-
- }));
+ return deepFreeze(Object.assign({}, state, {}));
case ACTION_TYPES.UPDATE_RECORD:
- return deepFreeze(Object.assign({}, state, {
-
- }));
+ return deepFreeze(Object.assign({}, state, {}));
case ACTION_TYPES.DELETE_RECORD:
- return deepFreeze(Object.assign({}, state, {
-
- }));
+ return deepFreeze(Object.assign({}, state, {}));
case ACTION_TYPES.FETCH_RECORDS_REQUEST:
return state;
@@ -39,6 +34,29 @@ function recordsReducer(state = initialState, action) {
[recordType]: records,
}));
+ case ACTION_TYPES.FETCH_RECORD_REQUEST:
+ return state;
+
+ case ACTION_TYPES.FETCH_RECORD_FAILURE:
+ return state;
+
+ case ACTION_TYPES.FETCH_RECORD_SUCCESS:
+ recordType = action.payload.recordType;
+ record = action.payload.data;
+ records = state[recordType] ? state[recordType] : [];
+
+ // Update or insert
+ recordIndex = records.findIndex((nextRecord) => (nextRecord.ID === record.ID));
+ if (recordIndex > -1) {
+ records[recordIndex] = record;
+ } else {
+ records.push(record);
+ }
+
+ return deepFreeze(Object.assign({}, state, {
+ [recordType]: records,
+ }));
+
case ACTION_TYPES.DELETE_RECORD_REQUEST:
return state;
@@ -48,7 +66,7 @@ function recordsReducer(state = initialState, action) {
case ACTION_TYPES.DELETE_RECORD_SUCCESS:
recordType = action.payload.recordType;
records = state[recordType]
- .filter(record => record.ID !== action.payload.id);
+ .filter(nextRecord => nextRecord.ID !== action.payload.id);
return deepFreeze(Object.assign({}, state, {
[recordType]: records,
diff --git a/admin/javascript/src/tests/silverstripe-backend-test.js b/admin/javascript/src/tests/silverstripe-backend-test.js
index 997212006..918a7b1ee 100644
--- a/admin/javascript/src/tests/silverstripe-backend-test.js
+++ b/admin/javascript/src/tests/silverstripe-backend-test.js
@@ -32,10 +32,11 @@ describe('SilverStripeBackend', () => {
it('should send a GET request to an endpoint', () => {
backend.get('http://example.com');
- expect(backend.fetch).toBeCalledWith(
- 'http://example.com',
- { method: 'get', credentials: 'same-origin' }
- );
+ expect(backend.fetch).toBeCalled();
+ expect(backend.fetch.mock.calls[0][0]).toEqual('http://example.com');
+ expect(backend.fetch.mock.calls[0][1]).toEqual(jasmine.objectContaining({
+ method: 'get'
+ }));
});
});
@@ -51,12 +52,10 @@ describe('SilverStripeBackend', () => {
backend.post('http://example.com', postData);
expect(backend.fetch).toBeCalled();
-
expect(backend.fetch.mock.calls[0][0]).toEqual('http://example.com');
expect(backend.fetch.mock.calls[0][1]).toEqual(jasmine.objectContaining({
method: 'post',
- body: postData,
- credentials: 'same-origin',
+ body: postData
}));
});
});
@@ -72,10 +71,12 @@ describe('SilverStripeBackend', () => {
backend.put('http://example.com', putData);
- expect(backend.fetch).toBeCalledWith(
- 'http://example.com',
- { method: 'put', body: putData, credentials: 'same-origin' }
- );
+ expect(backend.fetch).toBeCalled();
+ expect(backend.fetch.mock.calls[0][0]).toEqual('http://example.com');
+ expect(backend.fetch.mock.calls[0][1]).toEqual(jasmine.objectContaining({
+ method: 'put',
+ body: putData
+ }));
});
});
@@ -90,10 +91,12 @@ describe('SilverStripeBackend', () => {
backend.delete('http://example.com', deleteData);
- expect(backend.fetch).toBeCalledWith(
- 'http://example.com',
- { method: 'delete', body: deleteData, credentials: 'same-origin' }
- );
+ expect(backend.fetch).toBeCalled();
+ expect(backend.fetch.mock.calls[0][0]).toEqual('http://example.com');
+ expect(backend.fetch.mock.calls[0][1]).toEqual(jasmine.objectContaining({
+ method: 'delete',
+ body: deleteData
+ }));
});
});
@@ -143,7 +146,7 @@ describe('SilverStripeBackend', () => {
method: 'post',
payloadFormat: 'json',
responseFormat: 'json',
- });
+ });
const promise = endpoint({ id: 1, values: { a: 'aye', b: 'bee' } });
expect(mock.post.mock.calls[0][0]).toEqual('http://example.org');
diff --git a/admin/scss/_spritey.scss b/admin/scss/_spritey.scss
index 65fa7596f..61ff060b2 100644
--- a/admin/scss/_spritey.scss
+++ b/admin/scss/_spritey.scss
@@ -120,6 +120,18 @@ $sprites-64x64-2x-tab-list-hover: -0px -160px 80px 80px;
$sprites-64x64-2x-tab-list: -0px -240px 80px 80px;
$sprites-64x64-2x-tab-tree-hover: -0px -320px 80px 80px;
$sprites-64x64-2x-tab-tree: -0px -400px 80px 80px;
+$menu-icons-24x24-home: -0px -0px 24px 24px;
+$menu-icons-24x24-blog: -0px -24px 24px 24px;
+$menu-icons-24x24-community: -0px -48px 24px 24px;
+$menu-icons-24x24-db: -0px -72px 24px 24px;
+$menu-icons-24x24-document: -0px -96px 24px 24px;
+$menu-icons-24x24-gears: -0px -120px 24px 24px;
+$menu-icons-24x24-collection: -0px -144px 24px 24px;
+$menu-icons-24x24-information: -0px -168px 24px 24px;
+$menu-icons-24x24-network: -0px -192px 24px 24px;
+$menu-icons-24x24-pencil: -0px -216px 24px 24px;
+$menu-icons-24x24-picture: -0px -240px 24px 24px;
+$menu-icons-24x24-pie-chart: -0px -264px 24px 24px;
$menu-icons-16x16-home: -0px -0px 16px 16px;
$menu-icons-16x16-blog: -0px -16px 16px 16px;
$menu-icons-16x16-community: -0px -32px 16px 16px;
@@ -144,18 +156,6 @@ $menu-icons-16x16-2x-network: -0px -256px 32px 32px;
$menu-icons-16x16-2x-pencil: -0px -288px 32px 32px;
$menu-icons-16x16-2x-picture: -0px -320px 32px 32px;
$menu-icons-16x16-2x-pie-chart: -0px -352px 32px 32px;
-$menu-icons-24x24-home: -0px -0px 24px 24px;
-$menu-icons-24x24-blog: -0px -24px 24px 24px;
-$menu-icons-24x24-community: -0px -48px 24px 24px;
-$menu-icons-24x24-db: -0px -72px 24px 24px;
-$menu-icons-24x24-document: -0px -96px 24px 24px;
-$menu-icons-24x24-gears: -0px -120px 24px 24px;
-$menu-icons-24x24-collection: -0px -144px 24px 24px;
-$menu-icons-24x24-information: -0px -168px 24px 24px;
-$menu-icons-24x24-network: -0px -192px 24px 24px;
-$menu-icons-24x24-pencil: -0px -216px 24px 24px;
-$menu-icons-24x24-picture: -0px -240px 24px 24px;
-$menu-icons-24x24-pie-chart: -0px -264px 24px 24px;
$menu-icons-24x24-2x-home: -0px -0px 48px 48px;
$menu-icons-24x24-2x-blog: -0px -48px 48px 48px;
$menu-icons-24x24-2x-db: -0px -96px 48px 48px;
@@ -174,10 +174,10 @@ $menu-icons-24x24-2x-pie-chart: -0px -480px 48px 48px;
height: nth($sprite, 4);
}
@function sprite-width($sprite) {
- @return nth($sprite, 3);
+ @return nth($sprite, 3);
}
@function sprite-height($sprite) {
- @return nth($sprite, 4);
+ @return nth($sprite, 4);
}
@mixin sprite-position($sprite) {
$sprite-offset-x: nth($sprite, 1);
@@ -185,7 +185,7 @@ $menu-icons-24x24-2x-pie-chart: -0px -480px 48px 48px;
background-position: $sprite-offset-x $sprite-offset-y;
}
@mixin sprite($sprite, $display: block) {
- @include sprite-position($sprite);
+ @include sprite-position($sprite);
background-repeat: no-repeat;
overflow: hidden;
display: $display;
@@ -213,6 +213,10 @@ $menu-icons-24x24-2x-pie-chart: -0px -480px 48px 48px;
background-image: url('../images/sprites/dist/sprite-sprites-64x64-2x.png');
}
+.icon-menu-icons-24x24 {
+ background-image: url('../images/sprites/dist/sprite-menu-icons-24x24.png');
+}
+
.icon-menu-icons-16x16 {
background-image: url('../images/sprites/dist/sprite-menu-icons-16x16.png');
}
@@ -221,10 +225,7 @@ $menu-icons-24x24-2x-pie-chart: -0px -480px 48px 48px;
background-image: url('../images/sprites/dist/sprite-menu-icons-16x16-2x.png');
}
-.icon-menu-icons-24x24 {
- background-image: url('../images/sprites/dist/sprite-menu-icons-24x24.png');
-}
.icon-menu-icons-24x24-2x {
background-image: url('../images/sprites/dist/sprite-menu-icons-24x24-2x.png');
-}
+}
\ No newline at end of file
diff --git a/admin/scss/bootstrap/_variables.scss b/admin/scss/bootstrap/_variables.scss
index 79bf69c24..ad0c9a66d 100644
--- a/admin/scss/bootstrap/_variables.scss
+++ b/admin/scss/bootstrap/_variables.scss
@@ -32,7 +32,7 @@ $gray-light: #d3d9dd;
$gray-lighter: #e8e9ea;
// $gray-lightest: #f7f7f9;
//
-// $brand-primary: #0275d8;
+$brand-primary: #29abe2; //#0275d8;
$brand-success: #3fa142;
// $brand-info: #5bc0de;
// $brand-warning: #f0ad4e;
@@ -57,28 +57,28 @@ $brand-danger: #D40404;
// Control the default styling of most Bootstrap elements by modifying these
// variables. Mostly focused on spacing.
-// $spacer: 1rem;
-// $spacer-x: $spacer;
-// $spacer-y: $spacer;
-// $spacers: (
-// 0: (
-// x: 0,
-// y: 0
-// ),
-// 1: (
-// x: $spacer-x,
-// y: $spacer-y
-// ),
-// 2: (
-// x: ($spacer-x * 1.5),
-// y: ($spacer-y * 1.5)
-// ),
-// 3: (
-// x: ($spacer-x * 3),
-// y: ($spacer-y * 3)
-// )
-// );
-// $border-width: 1px;
+$spacer: 1rem;
+$spacer-x: $spacer;
+$spacer-y: $spacer;
+$spacers: (
+ 0: (
+ x: 0,
+ y: 0
+ ),
+ 1: (
+ x: $spacer-x,
+ y: $spacer-y
+ ),
+ 2: (
+ x: ($spacer-x * 1.5),
+ y: ($spacer-y * 1.5)
+ ),
+ 3: (
+ x: ($spacer-x * 3),
+ y: ($spacer-y * 3)
+ )
+);
+$border-width: 1px;
// Body
@@ -86,7 +86,7 @@ $brand-danger: #D40404;
// Settings for the `` element.
// $body-bg: #fff;
-// $body-color: $gray-dark;
+$body-color: $gray-dark;
// Links
@@ -160,7 +160,7 @@ $font-size-h2: 18px; /* 2rem; */
$font-size-h3: 16px; /* 1.75rem; */
$font-size-h4: 14px; /* 1.5rem; */
$font-size-h5: 13px; /* 1.25rem; */
-$font-size-h6: 1rem;
+$font-size-h6: 12px; /* 1rem; */
// $display1-size: 6rem;
// $display2-size: 5.5rem;
@@ -174,7 +174,7 @@ $font-size-h6: 1rem;
$line-height: 1.538;
-// $headings-margin-bottom: ($spacer / 2);
+$headings-margin-bottom: $spacer;
// $headings-font-family: inherit;
// $headings-font-weight: 500;
// $headings-line-height: 1.1;
@@ -183,7 +183,7 @@ $line-height: 1.538;
// $lead-font-size: 1.25rem;
// $lead-font-weight: 300;
//
-// $text-muted: $gray-light;
+$text-muted: #7f8b97;
//
// $abbr-border-color: $gray-light;
//
diff --git a/core/Injectable.php b/core/Injectable.php
index 2a280bcf0..37c6fbf15 100644
--- a/core/Injectable.php
+++ b/core/Injectable.php
@@ -47,19 +47,13 @@ trait Injectable {
* way to access instance methods which don't rely on instance
* data (e.g. the custom SilverStripe static handling).
*
- * @param string $className Optional classname (if called on Object directly)
+ * @param string $class Optional classname to create, if the called class should not be used
* @return static The singleton instance
*/
- public static function singleton() {
- $args = func_get_args();
-
- // Singleton to create should be the calling class if not Object,
- // otherwise the first parameter
- $class = get_called_class();
- if($class === 'Object') {
- $class = array_shift($args);
+ public static function singleton($class = null) {
+ if(!$class) {
+ $class = get_called_class();
}
-
return Injector::inst()->get($class);
}
}
diff --git a/filesystem/File.php b/filesystem/File.php
index 6e273cbe5..e605ccc5d 100644
--- a/filesystem/File.php
+++ b/filesystem/File.php
@@ -1,8 +1,8 @@
` }))
diff --git a/javascript/src/router.js b/javascript/src/router.js
index 41559cd5c..967360eac 100644
--- a/javascript/src/router.js
+++ b/javascript/src/router.js
@@ -24,6 +24,19 @@ function show(pageShow) {
};
}
+/**
+ * Checks if the passed route applies to the current location.
+ *
+ * @param string route - The route to check.
+ *
+ * @return boolean
+ */
+function routeAppliesToCurrentLocation(route) {
+ const r = new page.Route(route);
+ return r.match(page.current, {});
+}
+
page.show = show(page.show);
+page.routeAppliesToCurrentLocation = routeAppliesToCurrentLocation;
export default page;
diff --git a/model/versioning/ChangeSet.php b/model/versioning/ChangeSet.php
index 9d1e25b77..8a3716098 100644
--- a/model/versioning/ChangeSet.php
+++ b/model/versioning/ChangeSet.php
@@ -31,7 +31,7 @@ class ChangeSet extends DataObject {
private static $db = array(
'Name' => 'Varchar',
- 'State' => "Enum('open,published,reverted')",
+ 'State' => "Enum('open,published,reverted','open')",
);
private static $has_many = array(
@@ -46,6 +46,21 @@ class ChangeSet extends DataObject {
'Owner' => 'Member',
);
+ private static $casting = array(
+ 'Description' => 'Text',
+ );
+
+ /**
+ * List of classes to set apart in description
+ *
+ * @config
+ * @var array
+ */
+ private static $important_classes = array(
+ 'SiteTree',
+ 'File',
+ );
+
/**
* Default permission to require for publishers.
* Publishers must either be able to use the campaign admin, or have all admin access.
diff --git a/model/versioning/ChangeSetItem.php b/model/versioning/ChangeSetItem.php
index 76a7173a6..071e2b907 100644
--- a/model/versioning/ChangeSetItem.php
+++ b/model/versioning/ChangeSetItem.php
@@ -2,6 +2,9 @@
// namespace SilverStripe\Framework\Model\Versioning
+use SilverStripe\Filesystem\Thumbnail;
+
+
/**
* A single line in a changeset
*
@@ -12,7 +15,7 @@
* @method ManyManyList References() List of implicit items required by this change
* @method ChangeSet ChangeSet()
*/
-class ChangeSetItem extends DataObject {
+class ChangeSetItem extends DataObject implements Thumbnail {
const EXPLICITLY = 'explicitly';
@@ -62,6 +65,33 @@ class ChangeSetItem extends DataObject {
)
);
+ public function getTitle() {
+ // Get title of modified object
+ $object = $this->getObjectLatestVersion();
+ if($object) {
+ return $object->getTitle();
+ }
+ return $this->i18n_singular_name() . ' #' . $this->ID;
+ }
+
+
+
+ /**
+ * Get a thumbnail for this object
+ *
+ * @param int $width Preferred width of the thumbnail
+ * @param int $height Preferred height of the thumbnail
+ * @return string URL to the thumbnail, if available
+ */
+ public function ThumbnailURL($width, $height) {
+ $object = $this->getObjectLatestVersion();
+ if($object instanceof Thumbnail) {
+ return $object->ThumbnailURL($width, $height);
+ }
+ return null;
+ }
+
+
/**
* Get the type of change: none, created, deleted, modified, manymany
*
@@ -99,10 +129,19 @@ class ChangeSetItem extends DataObject {
* @param string $stage
* @return Versioned|DataObject
*/
- private function getObjectInStage($stage) {
+ protected function getObjectInStage($stage) {
return Versioned::get_by_stage($this->ObjectClass, $stage)->byID($this->ObjectID);
}
+ /**
+ * Find latest version of this object
+ *
+ * @return Versioned|DataObject
+ */
+ protected function getObjectLatestVersion() {
+ return Versioned::get_latest_version($this->ObjectClass, $this->ObjectID);
+ }
+
/**
* Get all implicit objects for this change
*
@@ -195,7 +234,7 @@ class ChangeSetItem extends DataObject {
public function canRevert($member) {
// Just get the best version as this object may not even exist on either stage anymore.
/** @var Versioned|DataObject $object */
- $object = Versioned::get_latest_version($this->ObjectClass, $this->ObjectID);
+ $object = $this->getObjectLatestVersion();
if(!$object) {
return false;
}
@@ -276,5 +315,4 @@ class ChangeSetItem extends DataObject {
// Default permissions
return (bool)Permission::checkMember($member, ChangeSet::config()->required_permission);
}
-
}