mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 12:05:37 +00:00
Merge pull request #8606 from creative-commoners/pulls/4.3/history-viewer-docs
DOCS Add documentation for configuring a HistoryViewerField for custom DataObjects
This commit is contained in:
commit
2a0e868260
@ -838,7 +838,425 @@ $obj = MyRecord::getComplexObjectRetrieval(); // returns 'Stage' records
|
||||
Versioned::set_reading_mode($origMode); // reset current mode
|
||||
```
|
||||
|
||||
## Using the history viewer
|
||||
|
||||
Since SilverStripe 4.3 you can use the React and GraphQL driven history viewer UI to display historic changes and
|
||||
comparisons for a versioned DataObject. This is automatically enabled for SiteTree objects and content blocks in
|
||||
[dnadesign/silverstripe-elemental](https://github.com/dnadesign/silverstripe-elemental).
|
||||
|
||||
If you want to enable the history viewer for a custom versioned DataObject, you will need to:
|
||||
|
||||
* Expose GraphQL scaffolding
|
||||
* Add the necessary GraphQL queries and mutations to your module
|
||||
* Register your GraphQL queries and mutations with Injector
|
||||
* Add a HistoryViewerField to the DataObject's `getCMSFields`
|
||||
|
||||
**Please note:** these examples are given in the context of project-level customisation. You may need to adjust
|
||||
the webpack configuration slightly for use in a module. They are also designed to be used on SilverStripe 4.3 or
|
||||
later.
|
||||
|
||||
For these examples, you can use this simple DataObject and create a ModelAdmin for it:
|
||||
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\Versioned\Versioned;
|
||||
|
||||
class MyVersionedObject extends DataObject
|
||||
{
|
||||
private static $db = [
|
||||
'Title' => 'Varchar',
|
||||
];
|
||||
|
||||
private static $extensions = [
|
||||
Versioned::class,
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### Configure frontend asset building
|
||||
|
||||
If you haven't already configured frontend asset building for your project, you will need to configure some basic
|
||||
packages to be built via webpack in order to enable history viewer functionality. If you have this configured for
|
||||
your project already, ensure you have the `react-apollo` and `graphql-tag` libraries in your `package.json`
|
||||
requirements, and skip this section.
|
||||
|
||||
You can configure your directory structure like so:
|
||||
|
||||
**package.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-project",
|
||||
"scripts": {
|
||||
"build": "yarn && NODE_ENV=production webpack -p --bail --progress",
|
||||
"watch": "yarn && NODE_ENV=development webpack --watch --progress"
|
||||
},
|
||||
"dependencies": {
|
||||
"react-apollo": "^0.7.1",
|
||||
"graphql-tag": "^0.1.17"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@silverstripe/webpack-config": "^0.4.1",
|
||||
"webpack": "^2.6.1"
|
||||
},
|
||||
"jest": {
|
||||
"roots": [
|
||||
"client/src"
|
||||
],
|
||||
"moduleDirectories": [
|
||||
"app/client/src",
|
||||
"node_modules",
|
||||
"node_modules/@silverstripe/webpack-config/node_modules",
|
||||
"vendor/silverstripe/admin/client/src",
|
||||
"vendor/silverstripe/admin/node_modules"
|
||||
]
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
"env",
|
||||
"react"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-object-rest-spread"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": "^6.x"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**webpack.config.js**
|
||||
|
||||
```json
|
||||
const Path = require('path');
|
||||
// Import the core config
|
||||
const webpackConfig = require('@silverstripe/webpack-config');
|
||||
const {
|
||||
resolveJS,
|
||||
externalJS,
|
||||
moduleJS,
|
||||
pluginJS,
|
||||
} = webpackConfig;
|
||||
|
||||
const ENV = process.env.NODE_ENV;
|
||||
const PATHS = {
|
||||
MODULES: 'node_modules',
|
||||
FILES_PATH: '../',
|
||||
ROOT: Path.resolve(),
|
||||
SRC: Path.resolve('app/client/src'),
|
||||
DIST: Path.resolve('app/client/dist'),
|
||||
};
|
||||
|
||||
const config = [
|
||||
{
|
||||
name: 'js',
|
||||
entry: {
|
||||
bundle: `${PATHS.SRC}/boot/index.js`,
|
||||
},
|
||||
output: {
|
||||
path: PATHS.DIST,
|
||||
filename: 'js/[name].js',
|
||||
},
|
||||
devtool: (ENV !== 'production') ? 'source-map' : '',
|
||||
resolve: resolveJS(ENV, PATHS),
|
||||
externals: externalJS(ENV, PATHS),
|
||||
module: moduleJS(ENV, PATHS),
|
||||
plugins: pluginJS(ENV, PATHS),
|
||||
}
|
||||
];
|
||||
|
||||
// Use WEBPACK_CHILD=js or WEBPACK_CHILD=css env var to run a single config
|
||||
module.exports = (process.env.WEBPACK_CHILD)
|
||||
? config.find((entry) => entry.name === process.env.WEBPACK_CHILD)
|
||||
: module.exports = config;
|
||||
```
|
||||
|
||||
**composer.json**
|
||||
|
||||
```json
|
||||
"extra": {
|
||||
"expose": [
|
||||
"app/client/dist"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**app/client/src/boot/index.js**
|
||||
|
||||
```js
|
||||
console.log('Hello world');
|
||||
```
|
||||
|
||||
**.eslintrc.js**
|
||||
|
||||
```js
|
||||
module.exports = require('@silverstripe/webpack-config/.eslintrc');
|
||||
```
|
||||
|
||||
At this stage, running `yarn build` should show you a linting warning for the console statement, and correctly build
|
||||
`app/client/dist/js/bundle.js`.
|
||||
|
||||
### Expose GraphQL scaffolding
|
||||
|
||||
Only a minimal amount of data is required to be exposed via GraphQL scaffolding, and only to the "admin" GraphQL
|
||||
schema. For more information, see [ReactJS, Redux and GraphQL](../../customising_the_admin_interface/react_redux_and_graphql).
|
||||
|
||||
**app/_config/graphql.yml**
|
||||
|
||||
```yaml
|
||||
SilverStripe\GraphQL\Manager:
|
||||
schemas:
|
||||
admin:
|
||||
scaffolding:
|
||||
types:
|
||||
MyVersionedObject:
|
||||
fields: [ID, LastEdited]
|
||||
operations:
|
||||
copyToStage: true
|
||||
readOne: true
|
||||
SilverStripe\Security\Member:
|
||||
fields: [ID, FirstName, Surname]
|
||||
operations:
|
||||
readOne: true
|
||||
```
|
||||
|
||||
Once configured, flush your cache and explore the new GraphQL schema to ensure it loads correctly. You can use a GraphQL
|
||||
application such as GraphiQL, or [silverstripe-graphql-devtools](https://github.com/silverstripe/silverstripe-graphql-devtools)
|
||||
for a browser solution:
|
||||
|
||||
```
|
||||
composer require --dev silverstripe/graphql-devtools dev-master
|
||||
```
|
||||
|
||||
### Configure the necessary GraphQL queries and mutations
|
||||
|
||||
The history viewer interface uses two main operations:
|
||||
|
||||
* Read a list of versions for a DataObject
|
||||
* Revert to an older version of a DataObject
|
||||
|
||||
For this we need one query and one mutation:
|
||||
|
||||
**app/client/src/state/readOneMyVersionedObjectQuery.js**
|
||||
|
||||
```js
|
||||
import { graphql } from 'react-apollo';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
// GraphQL query for retrieving the version history of a specific object. The results of
|
||||
// the query must be set to the "versions" prop on the component that this HOC is
|
||||
// applied to for binding implementation.
|
||||
const query = gql`
|
||||
query ReadHistoryViewerMyVersionedObject ($id: ID!, $limit: Int!, $offset: Int!) {
|
||||
readOneMyVersionedObject(
|
||||
Versioning: {
|
||||
Mode: LATEST
|
||||
},
|
||||
ID: $id
|
||||
) {
|
||||
ID
|
||||
Versions (limit: $limit, offset: $offset) {
|
||||
pageInfo {
|
||||
totalCount
|
||||
}
|
||||
edges {
|
||||
node {
|
||||
Version
|
||||
Author {
|
||||
FirstName
|
||||
Surname
|
||||
}
|
||||
Publisher {
|
||||
FirstName
|
||||
Surname
|
||||
}
|
||||
Published
|
||||
LiveVersion
|
||||
LatestDraftVersion
|
||||
LastEdited
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const config = {
|
||||
options({recordId, limit, page}) {
|
||||
return {
|
||||
variables: {
|
||||
limit,
|
||||
offset: ((page || 1) - 1) * limit,
|
||||
block_id: recordId,
|
||||
}
|
||||
};
|
||||
},
|
||||
props(
|
||||
{
|
||||
data: {
|
||||
error,
|
||||
refetch,
|
||||
readOneMyVersionedObject,
|
||||
loading: networkLoading,
|
||||
},
|
||||
ownProps: {
|
||||
actions = {
|
||||
versions: {}
|
||||
},
|
||||
limit,
|
||||
recordId,
|
||||
},
|
||||
}
|
||||
) {
|
||||
const versions = readOneMyVersionedObject || null;
|
||||
|
||||
const errors = error && error.graphQLErrors &&
|
||||
error.graphQLErrors.map((graphQLError) => graphQLError.message);
|
||||
|
||||
return {
|
||||
loading: networkLoading || !versions,
|
||||
versions,
|
||||
graphQLErrors: errors,
|
||||
actions: {
|
||||
...actions,
|
||||
versions: {
|
||||
...versions,
|
||||
goToPage(page) {
|
||||
refetch({
|
||||
offset: ((page || 1) - 1) * limit,
|
||||
limit,
|
||||
block_id: recordId,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export { query, config };
|
||||
|
||||
export default graphql(query, config);
|
||||
```
|
||||
|
||||
**app/client/src/state/revertToMyVersionedObjectVersionMutation.js**
|
||||
|
||||
```js
|
||||
import { graphql } from 'react-apollo';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
const mutation = gql`
|
||||
mutation revertMyVersionedObjectToVersion($id:ID!, $fromStage:VersionedStage!, $toStage:VersionedStage!, $fromVersion:Int!) {
|
||||
copyMyVersionedObjectToStage(Input: {
|
||||
ID: $id
|
||||
FromVersion: $fromVersion
|
||||
FromStage: $fromStage
|
||||
ToStage: $toStage
|
||||
}) {
|
||||
ID
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const config = {
|
||||
props: ({ mutate, ownProps: { actions } }) => {
|
||||
const revertToVersion = (id, fromVersion, fromStage, toStage) => mutate({
|
||||
variables: {
|
||||
id,
|
||||
fromVersion,
|
||||
fromStage,
|
||||
toStage,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
actions: {
|
||||
...actions,
|
||||
revertToVersion,
|
||||
},
|
||||
};
|
||||
},
|
||||
options: {
|
||||
// Refetch versions after mutation is completed
|
||||
refetchQueries: ['ReadHistoryViewerMyVersionedObject']
|
||||
}
|
||||
};
|
||||
|
||||
export { mutation, config };
|
||||
|
||||
export default graphql(mutation, config);
|
||||
````
|
||||
|
||||
### Register your GraphQL query and mutation with Injector
|
||||
|
||||
Once your GraphQL query and mutation are created, you will need to tell the JavaScript Injector about them.
|
||||
This does two things:
|
||||
|
||||
* Allow them to be loaded by core components.
|
||||
* Allow Injector to provide them in certain contexts. They should be available for `MyVersionedObject` history viewer
|
||||
instances, but not for CMS pages for example.
|
||||
|
||||
**app/client/src/boot/index.js**
|
||||
|
||||
```js
|
||||
/* global window */
|
||||
import Injector from 'lib/Injector';
|
||||
import readOneMyVersionedObjectQuery from 'state/readOneMyVersionedObjectQuery';
|
||||
import revertToMyVersionedObjectVersionMutation from 'state/revertToMyVersionedObjectVersionMutation';
|
||||
|
||||
window.document.addEventListener('DOMContentLoaded', () => {
|
||||
// Register GraphQL operations with Injector as transformations
|
||||
Injector.transform(
|
||||
'myversionedobject-history', (updater) => {
|
||||
updater.component(
|
||||
'HistoryViewer.Form_ItemEditForm',
|
||||
readOneMyVersionedObjectQuery, 'ElementHistoryViewer');
|
||||
}
|
||||
);
|
||||
|
||||
Injector.transform(
|
||||
'myversionedobject-history-revert', (updater) => {
|
||||
updater.component(
|
||||
'HistoryViewerToolbar.VersionedAdmin.HistoryViewer.MyVersionedObject.HistoryViewerVersionDetail',
|
||||
revertToMyVersionedObjectVersionMutation,
|
||||
'MyVersionedObjectRevertMutation'
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
For more information, see [ReactJS, Redux and GraphQL](../../customising_the_admin_interface/react_redux_and_graphql).
|
||||
|
||||
### Adding the HistoryViewerField
|
||||
|
||||
You can add the [HistoryViewerField](api:SilverStripe\VersionedAdmin\Forms\HistoryViewerField) to your object's CMS
|
||||
fields in the same way as any other form field:
|
||||
|
||||
```php
|
||||
use SilverStripe\VersionedAdmin\Forms\HistoryViewerField;
|
||||
use SilverStripe\View\Requirements;
|
||||
|
||||
public function getCMSFields()
|
||||
{
|
||||
$fields = parent::getCMSFields();
|
||||
|
||||
Requirements::javascript('app/client/dist/js/bundle.js');
|
||||
$fields->addFieldToTab('Root.History', HistoryViewerField::create('MyObjectHistory'));
|
||||
|
||||
return $fields;
|
||||
}
|
||||
```
|
||||
|
||||
### Previewable DataObjects
|
||||
|
||||
History viewer will automatically detect and render a side-by-side preview panel for DataObjects that implement
|
||||
[CMSPreviewable](api:SilverStripe\ORM\CMSPreviewable). Please note that if you are adding this functionality, you
|
||||
will also need to expose the `AbsoluteLink` field in your GraphQL read scaffolding, and add it to the fields in
|
||||
`readOneMyVersionedObjectQuery`.
|
||||
|
||||
## API Documentation
|
||||
|
||||
* [Versioned](api:SilverStripe\Versioned\Versioned)
|
||||
* [HistoryViewerField](api:SilverStripe\VersionedAdmin\Forms\HistoryViewerField)
|
||||
|
@ -67,4 +67,9 @@ SilverStripe\Core\Injector\Injector:
|
||||
App\MySite\MyCustomControllerFactory
|
||||
```
|
||||
|
||||
[Implementing a _Factory_ with the Injector](/developer_guides/extending/injector/#factories)
|
||||
[Implementing a _Factory_ with the Injector](/developer_guides/extending/injector/#factories).
|
||||
|
||||
### Using the history viewer for custom DataObjects
|
||||
|
||||
For information on how to implement the history viewer UI in your own versioned DataObjects, please refer to
|
||||
[the Versioning documentation](../developer_guides/model/versioning).
|
||||
|
Loading…
x
Reference in New Issue
Block a user