This is an old revision of the document!


User type

User type (projection) was added in CzechIdM version 10.2.0. Projection defines frontend form to read, create and edit user. We can create and edit user by different form. For example externe and internal employee can be created and edited differently (different attributes has to be filled). Used projection for user creation is set as user type.

Form projection with configurable features was added into product core module:

  • Frontend:
    • Form projection content is placed on path src/content/identity/projection/IdentityProjection.js.
    • Route (= frontent target) form/identity-projection/:entityId' is registed in routes.js.
    • Component identity-projection with route usage is registered in component-descriptor.js
  • Backend:
    • IdmIdentityProjectionDto was added - contains all projection data sent between backend and frontend, contains:
      • identity - identity.
      • contract - first (~ prime) contract. If currenly logged user has permission to read prime contract. First other contract is shown otherwise. Contract are sorted by priority the same way, as prime contract is evaluated.
      • otherContracts - all other contracts.
      • otherPositions - all other positions.
      • identityRoles - all assigned identity roles.
    • IdentityProjectionManager was added and support get and save identity projection. Projection is processed by event and processors (see IdentityProjectionSaveProcessor). Processors can be registed to process IdmIdentityProjectionDto content (custom validations, generators etc. can be added non invasively). Manager contains overridable protected method to get and save all parts of projection (methods can be overidden invasively to add or change product behavior).
    • IdmIdentityProjectionController is exposed on url '<server api>/identity-projection' and support get and post IdmIdentityProjectionDto - calls IdentityProjectionManagermethods.

New projection can be added with the same structure in custom module. Is needed to add new content representing form projection (e.g. copy or reuse form projection from product), register new route in routes.js, register new component in component-descriptor.js and expose new edpoint on backend if needed (if provided IdmIdentityProjectionDto is insuffiscient).

Projection will obtain entityId route parameter with identity codeable identifier (username or uuid).

Example

Source codes are placed in example module. Product backend is reused and example provides new frontend form for new projection. Form saves identity username only.

Create new form src/content/identity/projection/ExampleIdentityProjection.js with content:

import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import Helmet from 'react-helmet';
//
import { Basic, Advanced, Utils, Managers, Content } from 'czechidm-core';
 
const identityManager = new Managers.IdentityManager();
const identityProjectionManager = new Managers.IdentityProjectionManager();
const formProjectionManager = new Managers.FormProjectionManager();
 
 
/**
 * Example  form for identity projection.
 */
class ExampleIdentityProjection extends Basic.AbstractContent {
 
 
  constructor(props, context) {
    super(props, context);
    this.state = {
      generatePassword: false,
      generatePasswordShowLoading: false
    };
  }
 
  getContentKey() {
    return 'content.identity.projection';
  }
 
  getNavigationKey() {
    return 'identities';
  }
 
  componentDidMount() {
    super.componentDidMount();
    //
    const { entityId } = this.props.match.params;
    const { identityProjection, location } = this.props;
    const isNew = !!Utils.Ui.getUrlParameter(location, 'new');
    //
    if (isNew) {
      let formProjectionId;
      if (identityProjection && identityProjection.identity && identityProjection.identity.formProjection) {
        formProjectionId = identityProjection.identity.formProjection;
      } else {
        formProjectionId = Utils.Ui.getUrlParameter(this.props.location, 'projection');
      }
      if (!formProjectionId) {
        // form projection not found - default will be shown
        this._initProjection(entityId, identityProjection, {});
      } else {
        // fetch projection definition
        this.context.store.dispatch(formProjectionManager.autocompleteEntityIfNeeded(formProjectionId, null, (entity, error) => {
          if (error) {
            this.addError(error);
          } else {
            this._initProjection(entityId, identityProjection, entity);
          }
        }));
      }
    } else {
      this.getLogger().debug(`[FormDetail] loading entity detail [id:${ entityId }]`);
      this.context.store.dispatch(identityProjectionManager.fetchProjection(entityId, null, (entity, error) => {
        if (error) {
          this.addError(error);
        } else {
          this._initProjection(entityId, entity);
        }
      }));
    }
  }
 
  _initProjection(entityId, identityProjection = null, formProjection = null) {
    //
    // prepare form friendly projection
    let _identityProjection = null;
    if (identityProjection) {
      if (!formProjection && identityProjection.identity && identityProjection.identity._embedded) {
        formProjection = identityProjection.identity._embedded.formProjection;
      }
      //
      _identityProjection = {
        ...identityProjection.identity
      };
    } else {
      // new projection
      _identityProjection = {
        id: entityId,
        identity: {
          id: entityId,
          formProjection: formProjection ? formProjection.id : Utils.Ui.getUrlParameter(this.props.location, 'projection')
        }
      };
    }
    //
    this.setState({
      identityProjection: _identityProjection,
      formProjection
    }, () => {
      if (this.refs.username) {
        this.refs.username.focus();
      }
    });
  }
 
  save(event) {
    if (event) {
      event.preventDefault();
    }
    if (!this.refs.form.isFormValid()) {
      return;
    }
    const { identityProjection } = this.state;
    //
    this.refs.form.processStarted();
    const data = this.refs.form.getData();
    // construct projection
    const _identityProjection = {
      ...identityProjection,
      id: data.id,
      identity: data
    };
    //
    // post => save
    this.context.store.dispatch(identityProjectionManager.saveProjection(_identityProjection, null, this._afterSave.bind(this)));
  }
 
  /**
  * Just set showloading to false and set processEnded to form.
  * Call after save/create
  */
  _afterSave(identityProjection, error) {
    if (error) {
      this.setState({
        validationError: error,
        validationDefinition: false
      }, () => {
        this.addError(error);
        if (this.refs.form) {
          this.refs.form.processEnded();
        }
      });
    } else {
      this.addMessage({
        message: this.i18n('action.save.success', { record: identityProjection.identity.username, count: 1 })
      });
      this.context.history.replace(identityManager.getDetailLink(identityProjection.identity));
      if (this.refs.form) {
        this.refs.form.processEnded();
      }
    }
  }
 
  render() {
    const {
      match,
      location,
      showLoading,
      _imageUrl
    } = this.props;
    const { entityId } = match.params;
    const {
      identityProjection,
      formProjection
    } = this.state;
    const isNew = !!Utils.Ui.getUrlParameter(location, 'new');
    //
    return (
      <Basic.Div>
        <Helmet title={ isNew ? this.i18n('create.title') : this.i18n('edit.title') } />
        <Basic.Row>
          <Basic.Div className="col-lg-offset-2 col-lg-8">
            <Advanced.DetailHeader
              entity={ isNew ? null : identityProjection }
              back="/identities"
              buttons={[
                <Basic.Icon
                  value="fa:angle-double-right"
                  style={{ marginRight: 5, cursor: 'pointer' }}
                  title={ this.i18n('component.advanced.IdentityInfo.link.detail.default.label') }
                  onClick={ () => this.context.history.push(`/identity/${ encodeURIComponent(identityProjection.username) }/profile`) }
                  rendered={ !isNew }/>
              ]}>
              {
                _imageUrl
                ?
                <img src={ _imageUrl } alt="profile" className="img-circle img-thumbnail" style={{ height: 40, padding: 0 }} />
                :
                <Basic.Icon
                  icon={ formProjection ? formProjectionManager.getLocalization(formProjection, 'icon', 'component:identity') : 'component:identity' }
                  identity={ identityProjection }/>
              }
              { ' ' }
              { identityProjectionManager.getNiceLabel(identityProjection) }
              <small>
                { ' ' }
                { isNew ? this.i18n('create.title') : this.i18n('edit.title') }
              </small>
            </Advanced.DetailHeader>
 
            <Content.OrganizationPosition identity={ entityId } rendered={ !isNew }/>
 
            <form onSubmit={ this.save.bind(this) }>
 
              <Basic.Panel rendered={ identityProjection === null || identityProjection === undefined }>
                <Basic.Loading isStatic show/>
              </Basic.Panel>
 
              <Basic.Panel className={ identityProjection === null || identityProjection === undefined ? 'hidden' : 'last' }>
 
                <Basic.PanelBody>
                  <Basic.AbstractForm
                    ref="form"
                    data={ identityProjection }
                    readOnly={ !identityProjectionManager.canSave(isNew ? null : identityProjection) }
                    style={{ padding: 0 }}>
 
                    <Basic.TextField
                      ref="username"
                      label={ this.i18n('identity.username.label') }
                      max={ 255 }/>
 
                  </Basic.AbstractForm>
                </Basic.PanelBody>
                <Basic.PanelFooter>
                  <Basic.Button type="button" level="link" onClick={ this.context.history.goBack }>
                    { this.i18n('button.back') }
                  </Basic.Button>
                  <Basic.Button
                    type="submit"
                    level="success"
                    showLoading={ showLoading }
                    showLoadingIcon
                    showLoadingText={ this.i18n('button.saving') }
                    rendered={ identityProjectionManager.canSave(isNew ? null : identityProjection) }>
                    { this.i18n('button.save') }
                  </Basic.Button>
                </Basic.PanelFooter>
              </Basic.Panel>
            </form>
          </Basic.Div>
        </Basic.Row>
      </Basic.Div>
    );
  }
}
 
ExampleIdentityProjection.propTypes = {
  identityProjection: PropTypes.object
};
ExampleIdentityProjection.defaultProps = {
  identityProjection: null,
  _imageUrl: null
};
 
function select(state, component) {
  const { entityId } = component.match.params;
  const profileUiKey = identityManager.resolveProfileUiKey(entityId);
  const profile = Managers.DataManager.getData(state, profileUiKey);
  const identityProjection = identityProjectionManager.getEntity(state, entityId);
  //
  return {
    identityProjection,
    showLoading: identityProjectionManager.isShowLoading(state, null, !identityProjection ? entityId : identityProjection.id),
    _imageUrl: profile ? profile.imageUrl : null,
    passwordChangeType: Managers.ConfigurationManager.getPublicValue(state, 'idm.pub.core.identity.passwordChange')
  };
}
 
export default connect(select)(ExampleIdentityProjection);

Register new route in 'routes.js':

{
  path: 'example/form/identity-projection/:entityId',
  component: require('./src/content/identity/projection/ExampleIdentityProjection'),
  access: [ { type: 'HAS_ANY_AUTHORITY', authorities: ['IDENTITY_READ' ] } ]
}

Register new component in component-descriptor.js

{
  id: 'example-identity-projection',
  type: 'form-projection',
  ownerType: 'IdmIdentity',
  route: '/example/form/identity-projection'
}

Two projections are localized by default in product. If projection with code identity-externe or identity-internal will be configured, then localization will be used.

...
  "eav": { 
    "form-projection": {
      "identity-externe": {
        "label": "Externe user",
        "help": "Create externe user",
        "icon": "fa:walking",
        "level": "primary"
      },
      "identity-internal": {
        "label": "Internal employee",
        "help": "Create internal user",
        "icon": "fa:user",
        "level": "success"
      }
    }
  },
...

Localization can be added to newly configured projection in custom module. Projection localization is based on the same convetions as localization for [form definition].

Supported properties:

  • label - projection name. Used in select boxes and as label for button to create user with projection usage.
  • help - Used as title (tooltip) for button to create user with projection usage.
  • icon - Used in select boxes and as icon for button to create user with projection usage (optional, fa:user-plus will be used as default).
  • level - Used as level for button to create user with projection usage (optional, default will be used as default).
  • by tomiskar