/**
* Product:
* - with unique code
* - can be disabled (e.g. not available)
* - price can be {@code null} - product is for free
*/
@Entity
@Table(name = "example_product", indexes = {
@Index(name = "ux_example_product_code", columnList = "code", unique = true),
@Index(name = "idx_example_product_name", columnList = "name")
})
public class ExampleProduct extends AbstractEntity implements Disableable, Codeable {
private static final long serialVersionUID = 1L;
@Audited
@NotEmpty
@Size(min = 0, max = DefaultFieldLengths.NAME)
@Column(name = "code", length = DefaultFieldLengths.NAME, nullable = false)
private String code;
@Audited
@NotEmpty
@Size(min = 0, max = DefaultFieldLengths.NAME)
@Column(name = "name", length = DefaultFieldLengths.NAME, nullable = false)
private String name;
@Audited
@Size(max = DefaultFieldLengths.DESCRIPTION)
@Column(name = "description", length = DefaultFieldLengths.DESCRIPTION)
private String description;
@Audited
@Column(name = "double_value", nullable = true, precision = 38, scale = 4)
private BigDecimal price;
@Audited
@NotNull
@Column(name = "disabled", nullable = false)
private boolean disabled;
... getters and setters
}
==== Flyway script ====
We are using [[https://wiki.czechidm.com/7.3/dev/architecture/flyway|flyway scripts]] to update database schema. To persist the new entity to ''product_example'' table we add change script with [[https://wiki.czechidm.com/7.3/dev/architecture/flyway|name by convention]] to path ''
CREATE TABLE example_product
(
id bytea NOT NULL,
created timestamp without time zone NOT NULL,
creator character varying(255) NOT NULL,
creator_id bytea,
modified timestamp without time zone,
modifier character varying(255),
modifier_id bytea,
original_creator character varying(255),
original_creator_id bytea,
original_modifier character varying(255),
original_modifier_id bytea,
realm_id bytea,
transaction_id bytea,
code character varying(255) NOT NULL,
description character varying(2000),
disabled boolean NOT NULL,
name character varying(255) NOT NULL,
double_value numeric(38,4),
CONSTRAINT example_product_pkey PRIMARY KEY (id),
CONSTRAINT ux_example_product_code UNIQUE (code)
);
CREATE INDEX idx_example_product_name
ON example_product
USING btree
(name);
CREATE TABLE example_product_a
(
id bytea NOT NULL,
rev bigint NOT NULL,
revtype smallint,
created timestamp without time zone,
created_m boolean,
creator character varying(255),
creator_m boolean,
creator_id bytea,
creator_id_m boolean,
modified timestamp without time zone,
modified_m boolean,
modifier character varying(255),
modifier_m boolean,
modifier_id bytea,
modifier_id_m boolean,
original_creator character varying(255),
original_creator_m boolean,
original_creator_id bytea,
original_creator_id_m boolean,
original_modifier character varying(255),
original_modifier_m boolean,
original_modifier_id bytea,
original_modifier_id_m boolean,
realm_id bytea,
realm_id_m boolean,
transaction_id bytea,
transaction_id_m boolean,
code character varying(255),
code_m boolean,
description character varying(2000),
description_m boolean,
disabled boolean,
disabled_m boolean,
name character varying(255),
name_m boolean,
double_value numeric(38,4),
price_m boolean,
CONSTRAINT example_product_a_pkey PRIMARY KEY (id, rev),
CONSTRAINT fk_l8tlj6h1v48rav5nulvesvbg1 FOREIGN KEY (rev)
REFERENCES idm_audit (id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION
);
==== Repository ====
We are using [[http://docs.spring.io/spring-data/jpa/docs/current/reference/html/|Spring Data]] as data access layer. So we create simple repository to our entity:
/**
* Example product repository
*/
public interface ExampleProductRepository extends AbstractEntityRepository {
/**
* Returns product by unique code.
*
* @param code
* @return
*/
ExampleProduct findOneByCode(String code);
}
==== Filter ====
''ExampleProductFilter'' is used for finding products. The filter is used in the repository above:
/**
* Filter for example products
*
* @author Radek Tomiška
*
*/
public class ExampleProductFilter extends DataFilter {
public ExampleProductFilter() {
this(new LinkedMultiValueMap<>());
}
public ExampleProductFilter(MultiValueMap data) {
super(ExampleProductDto.class, data);
}
}
''DataFilter'' super class contains filtering by ''text'' and ''id'' (uuid) parameter. Filtering by ''id'' parameter is implemented in core (''UuidFilter'') for all abstract entities. We need to add the implementation for ''text'' parameter, which is custom for every abstract entity:
/**
* Example product filter - by text.
*/
@Component
@Description("Example product filter - by text. Search as \"like\" in name, code and description - lower, case insensitive.")
public class TextExampleProductFilter extends AbstractFilterBuilder {
@Autowired
public TextExampleProductFilter(ExampleProductRepository repository) {
super(repository);
}
@Override
public String getName() {
return ExampleProductFilter.PARAMETER_TEXT;
}
@Override
public Predicate getPredicate(Root root, CriteriaQuery> query, CriteriaBuilder builder, ExampleProductFilter filter) {
if (StringUtils.isEmpty(filter.getText())) {
return null;
}
return builder.or(
builder.like(builder.lower(root.get(ExampleProduct_.name)), "%" + filter.getText().toLowerCase() + "%"),
builder.like(builder.lower(root.get(ExampleProduct_.code)), "%" + filter.getText().toLowerCase() + "%"),
builder.like(builder.lower(root.get(ExampleProduct_.description)), "%" + filter.getText().toLowerCase() + "%")
);
}
}
==== Dto ====
The Dto layer creates the bridge between the persistence and the REST layer. An entity is transformed to a dto in the service layer, so we prepare dto class for the example product:
/**
* Example product
*/
@Relation(collectionRelation = "exampleProducts")
@ApiModel(description = "Example product")
public class ExampleProductDto extends AbstractDto implements Codeable, Disableable {
private static final long serialVersionUID = 1L;
@NotEmpty
@Size(min = 0, max = DefaultFieldLengths.NAME)
@ApiModelProperty(required = true, notes = "Unique example product's code. Could be used as identifier in rest endpoints.")
private String code;
@NotEmpty
@Size(min = 0, max = DefaultFieldLengths.NAME)
private String name;
@Size(max = DefaultFieldLengths.DESCRIPTION)
private String description;
@ApiModelProperty(notes = "Price can be null - product is for free")
private BigDecimal price;
@ApiModelProperty(notes = "Disabled product is not available for ordering.")
private boolean disabled;
... getters and setters
}
==== Service ====
The service contains business logic, adds transactions etc. to work (CRUD) with an entity (~repository). Services can only be used in the REST layer (controllers). Using repositories directly in controllers is prohibited.
=== Service - interface ===
Every service should have an interface. We are using default interface-based JDK proxy approach, so creating an interface is needed for appropriate Spring autowiring (e.g. with ''@Transactional'' usage).
/**
* Example product service
*/
public interface ExampleProductService extends
ReadWriteDtoService,
CodeableService,
AuthorizableService {
}
=== Service - default implementation ===
/**
* Default product service implementation
*/
@Service("exampleProductService")
public class DefaultExampleProductService
extends AbstractReadWriteDtoService
implements ExampleProductService {
private final ExampleProductRepository repository;
@Autowired
public DefaultExampleProductService(ExampleProductRepository repository) {
super(repository);
//
this.repository = repository;
}
@Override
public AuthorizableType getAuthorizableType() {
return new AuthorizableType(ExampleGroupPermission.EXAMPLEPRODUCT, getEntityClass());
}
@Override
@Transactional(readOnly = true)
public ExampleProductDto getByCode(String code) {
return toDto(repository.findOneByCode(code));
}
}
==== Security ====
''ExampleGroupPermission'' is used in the service implementation - we want to support [[https://wiki.czechidm.com/7.3/dev/security/authorization|authorization policies]] in our example agenda. So we create a new group:
/**
* Aggregate base permission. Name can't contain character '_' - its used for joining to authority name.
*/
public enum ExampleGroupPermission implements GroupPermission {
EXAMPLEPRODUCT(
IdmBasePermission.ADMIN,
IdmBasePermission.AUTOCOMPLETE,
IdmBasePermission.READ,
IdmBasePermission.CREATE,
IdmBasePermission.UPDATE,
IdmBasePermission.DELETE);
// String constants could be used in pre / post authotize SpEl expressions
public static final String EXAMPLE_PRODUCT_ADMIN = "EXAMPLEPRODUCT" + BasePermission.SEPARATOR + "ADMIN";
public static final String EXAMPLE_PRODUCT_AUTOCOMPLETE = "EXAMPLEPRODUCT" + BasePermission.SEPARATOR + "AUTOCOMPLETE";
public static final String EXAMPLE_PRODUCT_READ = "EXAMPLEPRODUCT" + BasePermission.SEPARATOR + "READ";
public static final String EXAMPLE_PRODUCT_CREATE = "EXAMPLEPRODUCT" + BasePermission.SEPARATOR + "CREATE";
public static final String EXAMPLE_PRODUCT_UPDATE = "EXAMPLEPRODUCT" + BasePermission.SEPARATOR + "UPDATE";
public static final String EXAMPLE_PRODUCT_DELETE = "EXAMPLEPRODUCT" + BasePermission.SEPARATOR + "DELETE";
private final List permissions;
private ExampleGroupPermission(BasePermission... permissions) {
this.permissions = Arrays.asList(permissions);
}
@Override
public List getPermissions() {
return permissions;
}
@Override
public String getName() {
return name();
}
@Override
public String getModule() {
return ExampleModuleDescriptor.MODULE_ID;
}
}
This group will be used in the REST layer to secure our endpoint.
=== Authorization evaluator ===
Example product can have empty ''price'' (product for free) - this product will be available for logged identity by custom [[https://wiki.czechidm.com/7.3/dev/security/authorization|authorization policy]]:
/**
* Adds permissions to products for free.
*/
@Component
@Description("Adds permissions to products for free..")
public class FreeProductEvaluator extends AbstractAuthorizationEvaluator {
@Override
public Predicate getPredicate(Root root, CriteriaQuery> query, CriteriaBuilder builder, AuthorizationPolicy policy, BasePermission... permission) {
return builder.or(
builder.isNull(root.get(ExampleProduct_.price)),
builder.equal(root.get(ExampleProduct_.price), BigDecimal.ZERO)
);
}
@Override
public Set getPermissions(ExampleProduct authorizable, AuthorizationPolicy policy) {
Set permissions = super.getPermissions(authorizable, policy);
if (authorizable.getPrice() == null || BigDecimal.ZERO.equals(authorizable.getPrice())) {
permissions.addAll(policy.getPermissions());
}
return permissions;
}
}
==== Rest controller ====
At last, we can expose a new REST endpoint (controller) with our example product.
/**
* RESTful example product endpoint
*/
@RestController
@Enabled(ExampleModuleDescriptor.MODULE_ID)
@RequestMapping(value = BaseController.BASE_PATH + "/example-products")
@Api(
value = ExampleProductController.TAG,
description = "Example products",
tags = { ExampleProductController.TAG })
public class ExampleProductController extends DefaultReadWriteDtoController {
protected static final String TAG = "Example products";
@Autowired
public ExampleProductController(ExampleProductService service) {
super(service);
}
@Override
@ResponseBody
@RequestMapping(method = RequestMethod.GET)
@PreAuthorize("hasAuthority('" + ExampleGroupPermission.EXAMPLE_PRODUCT_READ + "')")
@ApiOperation(
value = "Search example products (/search/quick alias)",
nickname = "searchExampleProducts",
tags = { ExampleProductController.TAG },
authorizations = {
@Authorization(value = SwaggerConfig.AUTHENTICATION_BASIC, scopes = {
@AuthorizationScope(scope = ExampleGroupPermission.EXAMPLE_PRODUCT_READ, description = "") }),
@Authorization(value = SwaggerConfig.AUTHENTICATION_CIDMST, scopes = {
@AuthorizationScope(scope = ExampleGroupPermission.EXAMPLE_PRODUCT_READ, description = "") })
})
public Resources> find(
@RequestParam(required = false) MultiValueMap parameters,
@PageableDefault Pageable pageable) {
return super.find(parameters, pageable);
}
@Override
@ResponseBody
@RequestMapping(value = "/search/quick", method = RequestMethod.GET)
@PreAuthorize("hasAuthority('" + ExampleGroupPermission.EXAMPLE_PRODUCT_READ + "')")
@ApiOperation(
value = "Search example products",
nickname = "searchQuickExampleProducts",
tags = { ExampleProductController.TAG },
authorizations = {
@Authorization(value = SwaggerConfig.AUTHENTICATION_BASIC, scopes = {
@AuthorizationScope(scope = ExampleGroupPermission.EXAMPLE_PRODUCT_READ, description = "") }),
@Authorization(value = SwaggerConfig.AUTHENTICATION_CIDMST, scopes = {
@AuthorizationScope(scope = ExampleGroupPermission.EXAMPLE_PRODUCT_READ, description = "") })
})
public Resources> findQuick(
@RequestParam(required = false) MultiValueMap parameters,
@PageableDefault Pageable pageable) {
return super.findQuick(parameters, pageable);
}
@Override
@ResponseBody
@RequestMapping(value = "/search/autocomplete", method = RequestMethod.GET)
@PreAuthorize("hasAuthority('" + ExampleGroupPermission.EXAMPLE_PRODUCT_AUTOCOMPLETE + "')")
@ApiOperation(
value = "Autocomplete example products (selectbox usage)",
nickname = "autocompleteExampleProducts",
tags = { ExampleProductController.TAG },
authorizations = {
@Authorization(value = SwaggerConfig.AUTHENTICATION_BASIC, scopes = {
@AuthorizationScope(scope = ExampleGroupPermission.EXAMPLE_PRODUCT_AUTOCOMPLETE, description = "") }),
@Authorization(value = SwaggerConfig.AUTHENTICATION_CIDMST, scopes = {
@AuthorizationScope(scope = ExampleGroupPermission.EXAMPLE_PRODUCT_AUTOCOMPLETE, description = "") })
})
public Resources> autocomplete(
@RequestParam(required = false) MultiValueMap parameters,
@PageableDefault Pageable pageable) {
return super.autocomplete(parameters, pageable);
}
@Override
@ResponseBody
@RequestMapping(value = "/{backendId}", method = RequestMethod.GET)
@PreAuthorize("hasAuthority('" + ExampleGroupPermission.EXAMPLE_PRODUCT_READ + "')")
@ApiOperation(
value = "Example product detail",
nickname = "getExampleProduct",
response = ExampleProductDto.class,
tags = { ExampleProductController.TAG },
authorizations = {
@Authorization(value = SwaggerConfig.AUTHENTICATION_BASIC, scopes = {
@AuthorizationScope(scope = ExampleGroupPermission.EXAMPLE_PRODUCT_READ, description = "") }),
@Authorization(value = SwaggerConfig.AUTHENTICATION_CIDMST, scopes = {
@AuthorizationScope(scope = ExampleGroupPermission.EXAMPLE_PRODUCT_READ, description = "") })
})
public ResponseEntity> get(
@ApiParam(value = "Example product's uuid identifier or code.", required = true)
@PathVariable @NotNull String backendId) {
return super.get(backendId);
}
@Override
@ResponseBody
@RequestMapping(method = RequestMethod.POST)
@PreAuthorize("hasAuthority('" + ExampleGroupPermission.EXAMPLE_PRODUCT_CREATE + "') or hasAuthority('" + ExampleGroupPermission.EXAMPLE_PRODUCT_UPDATE + "')")
@ApiOperation(
value = "Create / update example product",
nickname = "postExampleProduct",
response = ExampleProductDto.class,
tags = { ExampleProductController.TAG },
authorizations = {
@Authorization(value = SwaggerConfig.AUTHENTICATION_BASIC, scopes = {
@AuthorizationScope(scope = ExampleGroupPermission.EXAMPLE_PRODUCT_CREATE, description = ""),
@AuthorizationScope(scope = ExampleGroupPermission.EXAMPLE_PRODUCT_UPDATE, description = "")}),
@Authorization(value = SwaggerConfig.AUTHENTICATION_CIDMST, scopes = {
@AuthorizationScope(scope = ExampleGroupPermission.EXAMPLE_PRODUCT_CREATE, description = ""),
@AuthorizationScope(scope = ExampleGroupPermission.EXAMPLE_PRODUCT_UPDATE, description = "")})
})
public ResponseEntity> post(@Valid @RequestBody ExampleProductDto dto) {
return super.post(dto);
}
@Override
@ResponseBody
@RequestMapping(value = "/{backendId}", method = RequestMethod.PUT)
@PreAuthorize("hasAuthority('" + ExampleGroupPermission.EXAMPLE_PRODUCT_UPDATE + "')")
@ApiOperation(
value = "Update example product",
nickname = "putExampleProduct",
response = ExampleProductDto.class,
tags = { ExampleProductController.TAG },
authorizations = {
@Authorization(value = SwaggerConfig.AUTHENTICATION_BASIC, scopes = {
@AuthorizationScope(scope = ExampleGroupPermission.EXAMPLE_PRODUCT_UPDATE, description = "") }),
@Authorization(value = SwaggerConfig.AUTHENTICATION_CIDMST, scopes = {
@AuthorizationScope(scope = ExampleGroupPermission.EXAMPLE_PRODUCT_UPDATE, description = "") })
})
public ResponseEntity> put(
@ApiParam(value = "Example product's uuid identifier or code.", required = true)
@PathVariable @NotNull String backendId,
@Valid @RequestBody ExampleProductDto dto) {
return super.put(backendId, dto);
}
@Override
@ResponseBody
@RequestMapping(value = "/{backendId}", method = RequestMethod.DELETE)
@PreAuthorize("hasAuthority('" + ExampleGroupPermission.EXAMPLE_PRODUCT_DELETE + "')")
@ApiOperation(
value = "Delete example product",
nickname = "deleteExampleProduct",
tags = { ExampleProductController.TAG },
authorizations = {
@Authorization(value = SwaggerConfig.AUTHENTICATION_BASIC, scopes = {
@AuthorizationScope(scope = ExampleGroupPermission.EXAMPLE_PRODUCT_DELETE, description = "") }),
@Authorization(value = SwaggerConfig.AUTHENTICATION_CIDMST, scopes = {
@AuthorizationScope(scope = ExampleGroupPermission.EXAMPLE_PRODUCT_DELETE, description = "") })
})
public ResponseEntity> delete(
@ApiParam(value = "Example product's uuid identifier or code.", required = true)
@PathVariable @NotNull String backendId) {
return super.delete(backendId);
}
@Override
@ResponseBody
@RequestMapping(value = "/{backendId}/permissions", method = RequestMethod.GET)
@PreAuthorize("hasAuthority('" + ExampleGroupPermission.EXAMPLE_PRODUCT_READ + "')"
+ " or hasAuthority('" + ExampleGroupPermission.EXAMPLE_PRODUCT_AUTOCOMPLETE + "')")
@ApiOperation(
value = "What logged identity can do with given record",
nickname = "getPermissionsOnExampleProduct",
tags = { ExampleProductController.TAG },
authorizations = {
@Authorization(value = SwaggerConfig.AUTHENTICATION_BASIC, scopes = {
@AuthorizationScope(scope = ExampleGroupPermission.EXAMPLE_PRODUCT_READ, description = ""),
@AuthorizationScope(scope = ExampleGroupPermission.EXAMPLE_PRODUCT_AUTOCOMPLETE, description = "")}),
@Authorization(value = SwaggerConfig.AUTHENTICATION_CIDMST, scopes = {
@AuthorizationScope(scope = ExampleGroupPermission.EXAMPLE_PRODUCT_READ, description = ""),
@AuthorizationScope(scope = ExampleGroupPermission.EXAMPLE_PRODUCT_AUTOCOMPLETE, description = "")})
})
public Set getPermissions(
@ApiParam(value = "Example product's uuid identifier or code.", required = true)
@PathVariable @NotNull String backendId) {
return super.getPermissions(backendId);
}
@Override
protected ExampleProductFilter toFilter(MultiValueMap parameters) {
return new ExampleProductFilter(parameters);
}
}
As you can see, security and [[https://wiki.czechidm.com/7.3/dev/architecture/swagger|swagger]] annotations are used in the REST controller.
===== Frontend =====
Now we have all parts on the backend prepared and we can move to the frontend. The first thing what we need to do is to create a service, which will consume the REST endpoint on the backend.
==== Service ====
import { Services } from 'czechidm-core';
import { Domain } from 'czechidm-core';
/**
* Example products
*/
export default class ExampleProductService extends Services.AbstractService {
getApiPath() {
return '/example-products';
}
getNiceLabel(entity) {
if (!entity) {
return '';
}
return `${entity.name} (${entity.code})`;
}
/**
* Agenda supports authorization policies
*/
supportsAuthorization() {
return true;
}
/**
* Group permission - all base permissions (`READ`, `WRITE`, ...) will be evaluated under this group
*/
getGroupPermission() {
return 'EXAMPLEPRODUCT';
}
/**
* Almost all dtos doesn§t support rest `patch` method
*/
supportsPatch() {
return false;
}
/**
* Returns default searchParameters for current entity type
*
* @return {object} searchParameters
*/
getDefaultSearchParameters() {
return super.getDefaultSearchParameters().setName(Domain.SearchParameters.NAME_QUICK).clearSort().setSort('name');
}
}
We can register services and managers in ''index.js'' files placed in folders, where services and managers are defined - it's simpler to import services from one file:
...
import { ExampleProductService } from '../services';
...
where the relative path leads to the module service's ''index.js'':
import ExampleProductService from './ExampleProductService';
const ServiceRoot = {
ExampleProductService
};
ServiceRoot.version = '0.1.0';
module.exports = ServiceRoot;
==== Manager ====
The service can be used in the manager:
import { Managers } from 'czechidm-core';
import { ExampleProductService } from '../services';
/**
* Example product manager
*
* @author Radek Tomiška
*/
export default class ExampleProductManager extends Managers.EntityManager {
constructor() {
super();
this.service = new ExampleProductService();
}
getModule() {
return 'example';
}
getService() {
return this.service;
}
/**
* Controlled entity
*/
getEntityType() {
return 'ExampleProduct';
}
/**
* Collection name in search / find response
*/
getCollectionType() {
return 'exampleProducts';
}
}
==== Route and navigation ====
We define new routes in ''routes.js'' descriptor:
module.exports = {
module: 'example',
childRoutes: [
...
{
path: '/example/products',
component: require('./src/content/example-product/ExampleProducts'),
access: [ { 'type': 'HAS_ANY_AUTHORITY', 'authorities': ['EXAMPLEPRODUCT_READ'] } ]
},
{
path: 'example/product/:entityId/',
component: require('./src/content/example-product/ExampleProductRoute'),
access: [ { 'type': 'HAS_ANY_AUTHORITY', 'authorities': ['EXAMPLEPRODUCT_READ'] } ],
childRoutes: [
{
path: 'detail',
component: require('./src/content/example-product/ExampleProductContent'),
access: [ { 'type': 'HAS_ANY_AUTHORITY', 'authorities': ['EXAMPLEPRODUCT_READ'] } ]
}
]
},
{
path: 'example/product/:entityId/new',
component: require('./src/content/example-product/ExampleProductContent'),
access: [ { 'type': 'HAS_ANY_AUTHORITY', 'authorities': ['EXAMPLEPRODUCT_CREATE'] } ]
}
]
};
and create new navigation items in ''module-descriptor.js'':
module.exports = {
'id': 'example',
'npmName': 'czechidm-example',
'backendId': 'example',
'name': 'Example module for CzechIdM devstack.',
'description': 'Example module for CzechIdM devstack. This module can be duplicated and renamed for create new optional CzechIdM module.',
'mainRouteFile': 'routes.js',
'mainComponentDescriptorFile': 'component-descriptor.js',
'mainLocalePath': 'src/locales/',
'navigation': {
'items': [
{
'id': 'example-main-menu',
'labelKey': 'example:content.examples.label',
'titleKey': 'example:content.examples.title',
'icon': 'gift',
'iconColor': '#FF8A80',
'order': 9,
'items': [
{
'id': 'example-content',
'type': 'DYNAMIC',
'section': 'main',
'labelKey': 'example:content.example.label',
'titleKey': 'example:content.example.title',
'order': 10,
'path': '/example/content',
'priority': 0
},
{
'id': 'example-products',
'type': 'DYNAMIC',
'section': 'main',
'icon': 'gift',
'labelKey': 'example:content.example-products.label',
'titleKey': 'example:content.example-products.title',
'order': 20,
'path': '/example/products',
'access': [ { 'type': 'HAS_ANY_AUTHORITY', 'authorities': ['EXAMPLEPRODUCT_READ'] } ],
'items': [
{
'id': 'example-product-detail',
'type': 'TAB',
'labelKey': 'example:content.example-product.detail.basic',
'order': 10,
'path': '/example/product/:entityId/detail',
'icon': 'gift',
'access': [ { 'type': 'HAS_ANY_AUTHORITY', 'authorities': ['EXAMPLEPRODUCT_READ'] } ]
},
]
}
]
}
...
]
}
};
==== Contents ====
We create new contents on paths as added routes above says:
==== ExampleProducts ====
The route for the table of products:
import React from 'react';
//
import { Basic } from 'czechidm-core';
import ExampleProductTable from './ExampleProductTable';
/**
* List of example products
*/
export default class ExampleProducts extends Basic.AbstractContent {
constructor(props, context) {
super(props, context);
}
/**
* "Shorcut" for localization
*/
getContentKey() {
return 'example:content.example-products';
}
/**
* Selected navigation item
*/
getNavigationKey() {
return 'example-products';
}
render() {
return (
{ this.renderPageHeader() }
);
}
}
==== ExampleProductTable ====
The table of products:
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import _ from 'lodash';
import uuid from 'uuid';
//
import { Basic, Advanced, Utils, Managers, Domain } from 'czechidm-core';
import { ExampleProductManager } from '../../redux';
const manager = new ExampleProductManager();
/**
* Table of example products
*/
export class ExampleProductTable extends Advanced.AbstractTableContent {
constructor(props, context) {
super(props, context);
this.state = {
filterOpened: this.props.filterOpened,
};
}
/**
* "Shorcut" for localization
*/
getContentKey() {
return 'example:content.example-products';
}
/**
* Base manager for this agenda (used in `AbstractTableContent`)
*/
getManager() {
return manager;
}
/**
* Submit filter action
*/
useFilter(event) {
if (event) {
event.preventDefault();
}
this.refs.table.getWrappedInstance().useFilterForm(this.refs.filterForm);
}
/**
* Cancel filter action
*/
cancelFilter(event) {
if (event) {
event.preventDefault();
}
this.refs.table.getWrappedInstance().cancelFilter(this.refs.filterForm);
}
/**
* Link to detail / create
*/
showDetail(entity) {
if (Utils.Entity.isNew(entity)) {
const uuidId = uuid.v1();
this.context.router.push(`/example/product/${uuidId}/new?new=1`);
} else {
this.context.router.push(`/example/product/${entity.id}/detail`);
}
}
render() {
const { uiKey, columns, forceSearchParameters, showAddButton, showRowSelection } = this.props;
const { filterOpened } = this.state;
return (
{ return Utils.Ui.getRowClass(data[rowIndex]); } }
filterOpened={filterOpened}
forceSearchParameters={ forceSearchParameters }
showRowSelection={ Managers.SecurityManager.hasAuthority('EXAMPLEPRODUCT_DELETE') && showRowSelection }
filter={
}
actions={
[
{ value: 'delete', niceLabel: this.i18n('action.delete.action'), action: this.onDelete.bind(this), disabled: false },
]
}
buttons={
[
{' '}
{this.i18n('button.add')}
]
}
_searchParameters={ this.getSearchParameters() }
>
{
return (
);
}
}
sort={false}/>
);
}
}
ExampleProductTable.propTypes = {
/**
* Entities, permissions etc. fro this content are stored in redux under given key
*/
uiKey: PropTypes.string.isRequired,
/**
* Rendered columns (all by default)
*/
columns: PropTypes.arrayOf(PropTypes.string),
/**
* Show filter or collapse
*/
filterOpened: PropTypes.bool,
/**
* "Hard filters"
*/
forceSearchParameters: PropTypes.object,
/**
* Show add button to create new product
*/
showAddButton: PropTypes.bool,
/**
* Show row selection for bulk actions
*/
showRowSelection: PropTypes.bool
};
ExampleProductTable.defaultProps = {
columns: ['code', 'name', 'description', 'price', 'disabled'],
filterOpened: false,
forceSearchParameters: new Domain.SearchParameters(),
showAddButton: true,
showRowSelection: true
};
function select(state, component) {
return {
_searchParameters: Utils.Ui.getSearchParameters(state, component.uiKey), // persisted filter state in redux
};
}
export default connect(select, null, null, { withRef: true })(ExampleProductTable);
==== ExampleProductRoute ====
Route for product detail with tabs:
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
//
import { Basic, Advanced } from 'czechidm-core';
import { ExampleProductManager } from '../../redux';
const manager = new ExampleProductManager();
/**
* ExampleProduct detail with tabs
*/
class ExampleProductRoute extends Basic.AbstractContent {
componentDidMount() {
const { entityId } = this.props.params;
// load entity from BE - for nice labels etc.
this.context.store.dispatch(manager.fetchEntityIfNeeded(entityId));
}
render() {
const { entity, showLoading } = this.props;
return (
{' '}
{ this.i18n('example:content.example-product.detail.edit.header', { name: manager.getNiceLabel(entity), escape: false }) }
{this.props.children}
);
}
}
ExampleProductRoute.propTypes = {
entity: PropTypes.object,
showLoading: PropTypes.bool
};
ExampleProductRoute.defaultProps = {
entity: null,
showLoading: false
};
function select(state, component) {
const { entityId } = component.params;
return {
entity: manager.getEntity(state, entityId),
showLoading: manager.isShowLoading(state, null, entityId)
};
}
export default connect(select)(ExampleProductRoute);
==== ExampleProductContent ====
Product detail with form. It's a simple reusable form wrapper - loads entity info form:
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
//
import { Basic } from 'czechidm-core';
import { ExampleProductManager } from '../../redux';
import ExampleProductDetail from './ExampleProductDetail';
const manager = new ExampleProductManager();
/**
* Example product detait wrapper
*/
class ExampleProductContent extends Basic.AbstractContent {
constructor(props, context) {
super(props, context);
}
/**
* "Shorcut" for localization
*/
getContentKey() {
return 'acc:content.system.detail';
}
/**
* Selected navigation item
*/
getNavigationKey() {
return 'example-product-detail';
}
componentDidMount() {
super.componentDidMount();
//
const { entityId } = this.props.params;
if (this._isNew()) {
// persist new entity to redux
this.context.store.dispatch(manager.receiveEntity(entityId, { }));
} else {
// load entity from BE - we need load actual entity and set her to the form
this.getLogger().debug(`[ExampleProductContent] loading entity detail [id:${entityId}]`);
this.context.store.dispatch(manager.fetchEntity(entityId));
}
}
/**
* Helper - returns `true`, when new entity is created
*/
_isNew() {
const { query } = this.props.location;
return (query) ? query.new : null;
}
render() {
const { entity, showLoading } = this.props;
return (
{
showLoading
?
:
}
);
}
}
ExampleProductContent.propTypes = {
/**
* Loaded entity
*/
entity: PropTypes.object,
/**
* Entity is currently loaded from BE
*/
showLoading: PropTypes.bool
};
ExampleProductContent.defaultProps = {
};
function select(state, component) {
const { entityId } = component.params;
//
return {
entity: manager.getEntity(state, entityId),
showLoading: manager.isShowLoading(state, null, entityId)
};
}
export default connect(select)(ExampleProductContent);
==== ExampleProductDetail ====
Product form:
import React, { PropTypes } from 'react';
import Helmet from 'react-helmet';
import { connect } from 'react-redux';
import Joi from 'joi';
//
import { Basic, Utils } from 'czechidm-core';
import { ExampleProductManager } from '../../redux';
const manager = new ExampleProductManager();
/**
* Example product form
*/
class ExampleProductDetail extends Basic.AbstractContent {
constructor(props, context) {
super(props, context);
//
this.state = {
showLoading: false
};
}
/**
* "Shorcut" for localization
*/
getContentKey() {
return 'example:content.example-product.detail';
}
componentDidMount() {
const { entity } = this.props;
// set loaded entity to form
this.refs.form.setData(entity);
this.refs.code.focus();
}
/**
* Component will receive new props, try to compare with actual,
* then init form
*/
componentWillReceiveProps(nextProps) {
const { entity } = this.props;
if (entity && entity.id !== nextProps.entity.id) {
this.refs.form.setData(nextProps.entity);
}
}
/**
* Save entity to BE
*/
save(afterAction, event) {
if (event) {
event.preventDefault();
}
// get entity from form
const entity = this.refs.form.getData();
// check form validity
if (!this.refs.form.isFormValid()) {
return;
}
// ui redux store identifier
const { uiKey } = this.props;
//
this.setState({
showLoading: true
}, () => {
const saveEntity = {
...entity,
};
if (Utils.Entity.isNew(saveEntity)) {
this.context.store.dispatch(manager.createEntity(saveEntity, `${uiKey}-detail`, (createdEntity, newError) => {
this._afterSave(createdEntity, newError, afterAction);
}));
} else {
this.context.store.dispatch(manager.updateEntity(saveEntity, `${uiKey}-detail`, (patchedEntity, newError) => {
this._afterSave(patchedEntity, newError, afterAction);
}));
}
});
}
/**
* `Callback` after save action ends
*/
_afterSave(entity, error, afterAction = 'CLOSE') {
this.setState({
showLoading: false
}, () => {
if (error) {
this.addError(error);
return;
}
this.addMessage({ message: this.i18n('save.success', { name: entity.name }) });
//
if (afterAction === 'CLOSE') {
// reload options with remote connectors
this.context.router.replace(`example/products`);
} else {
this.context.router.replace(`example/product/${entity.id}/detail`);
}
});
}
render() {
const { uiKey, entity, _permissions } = this.props;
const { showLoading } = this.state;
//
return (
);
}
}
ExampleProductDetail.propTypes = {
/**
* Loaded entity
*/
entity: PropTypes.object,
/**
* Entity, permissions etc. fro this content are stored in redux under given key
*/
uiKey: PropTypes.string.isRequired,
/**
* Logged identity permissions - what can do with currently loaded entity
*/
_permissions: PropTypes.arrayOf(PropTypes.string)
};
ExampleProductDetail.defaultProps = {
_permissions: null
};
function select(state, component) {
if (!component.entity) {
return {};
}
return {
_permissions: manager.getPermissions(state, null, component.entity.id)
};
}
export default connect(select)(ExampleProductDetail);
==== Localization ====
Localization keys are used in contents and components. We add localization:
{
"module": {
"name": "Example module",
"author": "BCV solutions s.r.o."
},
"entity": {
"ExampleProduct": {
"_type": "Example produkt",
"code": {
"label": "Code"
},
"name": {
"label": "Name"
},
"description": {
"label": "Description"
},
"price": {
"label": "Price",
"placeholder": "Product's price",
"help": "Price can be empty - produkt for free."
},
"disabled": {
"label": "Disabled",
"help": "Product is not available."
}
}
},
"permission": {
"group": {
"EXAMPLEPRODUCT": "Ukázkové produkty"
}
},
"content": {
"identity":{
"example": "Example tab"
},
"dashboard": {
"exampleDashboard": {
"header": "Dashboard example",
"title": "Dashbord from example module.",
"text": "Example dashboard, added by czechidm-example module."
}
},
"examples": {
"header": "Examples",
"label": "Examples",
"title": "Examples"
},
"example": {
"header": "Example content",
"label": "Example content",
"title": "Example content available from navigation, added by czechidm-example module.",
"text": "New example content.",
"error": {
"header": "Localization for error codes",
"parameter": {
"label": "Error parameter value"
},
"button": {
"client": "Client error (validation)",
"server": "Server error"
}
}
},
"example-products": {
"header": "Products",
"label": "Products",
"title": "Products",
"info": "Example agenda with basic operations with products.",
"filter": {
"text": {
"placeholder": "Code, name or description"
}
}
},
"example-product": {
"detail" : {
"basic": "Basic information",
"header": "Product",
"title": "$t(example:content.example-product.detail.header)",
"edit": {
"title": "Produkt detail",
"header": "{{name}} product detail"
},
"create": {
"header": "New product"
},
"save": {
"success": "Product [{{name}}] successfully saved"
}
}
}
},
"error": {
"EXAMPLE_SERVER_ERROR": {
"title": "Example server error",
"message": "Example server with parameter [{{parameter}}]."
},
"EXAMPLE_CLIENT_ERROR": {
"title": "Example client error",
"message": "Example client error, bad value given [{{parameter}}]."
}
}
}