diff options
Diffstat (limited to 'sdc-workflow-designer-ui/src/main/frontend/src')
185 files changed, 14224 insertions, 0 deletions
diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/App.js b/sdc-workflow-designer-ui/src/main/frontend/src/App.js new file mode 100644 index 00000000..5b7c154d --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/App.js @@ -0,0 +1,106 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { hot } from 'react-hot-loader'; +import React, { Component } from 'react'; +import { Route, withRouter } from 'react-router-dom'; +import qs from 'qs'; +import { connect } from 'react-redux'; +import { PluginPubSub } from 'shared/pubsub/plugin-pubsub'; +import 'resources/scss/style.scss'; +import 'bpmn-js-properties-panel/styles/properties.less'; +import { routes } from 'wfapp/routes'; +import { USER_ID } from 'wfapp/appConstants'; +import { getVersionsAction } from 'features/workflow/overview/overviewConstansts'; +import { setOperationModeAction } from 'features/version/versionConstants'; +import { setPluginContext } from './pluginContext/pluginContextActions'; +import { notificationType } from 'wfapp/pluginContext/pluginContextConstants'; +const RouteWithSubRoutes = route => ( + <Route + path={route.path} + exact={route.exact} + render={props => <route.component {...props} routes={route.routes} />} + /> +); + +function mapActionsToProps(dispatch) { + return { + getOverview: workflowId => { + dispatch(getVersionsAction(workflowId)); + dispatch(setOperationModeAction()); + }, + setPluginContext: payload => dispatch(setPluginContext(payload)) + }; +} + +class App extends Component { + constructor(props) { + super(props); + + this.searchParams = qs.parse(location.search, { + ignoreQueryPrefix: true + }); + + if (this.searchParams && this.searchParams.userId) { + localStorage.setItem(USER_ID, this.searchParams.userId); + } + } + + componentDidMount() { + if (this.searchParams) { + const { + eventsClientId, + parentUrl, + workflowId, + versionId + } = this.searchParams; + + if (eventsClientId && parentUrl) { + this.props.setPluginContext({ + eventsClientId, + parentUrl + }); + const client = new PluginPubSub(eventsClientId, parentUrl); + client.notify(notificationType.READY); + } + if (workflowId && versionId) { + this.props.getOverview(workflowId); + this.props.history.push( + `/workflow/${workflowId}/version/${versionId}/composition` + ); + } + } + } + + render() { + return ( + <div className="workflow-app"> + {routes.map((route, i) => ( + <RouteWithSubRoutes key={`App.route.${i}`} {...route} /> + ))} + </div> + ); + } +} + +export default hot(module)( + withRouter( + connect( + null, + mapActionsToProps + )(App) + ) +); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/appConstants.js b/sdc-workflow-designer-ui/src/main/frontend/src/appConstants.js new file mode 100644 index 00000000..710c4d8c --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/appConstants.js @@ -0,0 +1,37 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { createAction } from 'redux-actions'; +export const LANG = 'en'; +export const USER_ID = 'USER_ID'; +export const VERSION_LEVEL_LIST = [ + { + id: '2', + name: 'Major', + value: '2' + }, + { + id: '1', + name: 'Minor', + value: '1' + } +]; + +export const NETWORK_GENERIC_ERROR = 'NETWORK_GENERIC_ERROR'; +export const genericNetworkErrorAction = createAction( + NETWORK_GENERIC_ERROR, + error => error +); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/appSelectors.js b/sdc-workflow-designer-ui/src/main/frontend/src/appSelectors.js new file mode 100644 index 00000000..657429e4 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/appSelectors.js @@ -0,0 +1,16 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +export const i18nSelector = state => state && state.i18n.locale; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/config/Configuration.js b/sdc-workflow-designer-ui/src/main/frontend/src/config/Configuration.js new file mode 100644 index 00000000..36ad33d7 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/config/Configuration.js @@ -0,0 +1,63 @@ +/* + * Copyright © 2018 European Support Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import configData from './config.json'; + +class Configuration { + get(key) { + return configData[key]; + } + + set(key, value) { + var prev = configData[key]; + configData[key] = value; + return prev; + } + + setCatalogApiRoot(CatalogApiRoot) { + let restCatalogPrefix = CatalogApiRoot, + restPrefix = CatalogApiRoot.replace( + /\/feProxy\b[^:]*$/, + '/feProxy/onboarding-api' + ); + + this.set('restPrefix', restPrefix); + this.set('restCatalogPrefix', restCatalogPrefix); + } + + setCatalogApiHeaders(CatalogApiHeaders) { + this.set('CatalogApiHeaders', CatalogApiHeaders); + + let { userId: { value: UserID } = {} } = CatalogApiHeaders; + this.set('UserID', UserID); + } +} + +const configuration = new Configuration(); + +(function setDefaultRestPrefixes(configuration) { + configuration.set('restPrefix', configuration.get('defaultRestPrefix')); + configuration.set( + 'restCatalogPrefix', + configuration.get('defaultRestCatalogPrefix') + ); + configuration.set( + 'activitiesRestPrefix', + configuration.get('activitiesRestPrefix') + ); + configuration.set('appContextPath', configuration.get('appContextPath')); +})(configuration); + +export default configuration; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/config/config.json b/sdc-workflow-designer-ui/src/main/frontend/src/config/config.json new file mode 100644 index 00000000..96a7ca5b --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/config/config.json @@ -0,0 +1,6 @@ +{ + "version": "0.1", + "appContextPath" : "/", + "defaultRestPrefix": "/workflows/wf", + "activitiesRestPrefix": "/workflows/v1.0" +} diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/activities/activitiesActions.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/activities/activitiesActions.js new file mode 100644 index 00000000..833f6e5f --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/activities/activitiesActions.js @@ -0,0 +1,25 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { SET_ACTIVITIES_LIST, GET_ACTIVITIES } from './activitiesConstants'; + +export const setActivitiesList = payload => ({ + type: SET_ACTIVITIES_LIST, + payload +}); + +export const getActivitiesList = () => ({ + type: GET_ACTIVITIES +}); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/activities/activitiesApi.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/activities/activitiesApi.js new file mode 100644 index 00000000..10141e09 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/activities/activitiesApi.js @@ -0,0 +1,35 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import RestfulAPIUtil from 'services/restAPIUtil'; +import Configuration from 'config/Configuration.js'; +import { activityStatus } from './activitiesConstants'; + +function baseUrl() { + const restPrefix = Configuration.get('activitiesRestPrefix'); + return `${restPrefix}/activity-spec`; +} + +export default { + fetchActivities: () => { + return RestfulAPIUtil.fetch( + `${baseUrl()}?status=${activityStatus.CERTIFIED}` + ); + }, + + fetchActivity: id => { + return RestfulAPIUtil.fetch(`${baseUrl()}/${id}/versions/latest`); + } +}; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/activities/activitiesConstants.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/activities/activitiesConstants.js new file mode 100644 index 00000000..f99789c2 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/activities/activitiesConstants.js @@ -0,0 +1,24 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +export const SET_ACTIVITIES_LIST = 'activites/SET_ACTIVITIES_LIST'; +export const GET_ACTIVITIES = 'activities/GET_ACTIVITIES'; + +export const activityStatus = { + CERTIFIED: 'Certified', + DRAFT: 'Draft', + DEPRICATED: 'Depricated', + DELETED: 'Deleted' +}; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/activities/activitiesReducer.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/activities/activitiesReducer.js new file mode 100644 index 00000000..5c361401 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/activities/activitiesReducer.js @@ -0,0 +1,26 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { SET_ACTIVITIES_LIST } from './activitiesConstants'; + +export default (state = [], action) => { + switch (action.type) { + case SET_ACTIVITIES_LIST: { + return action.payload; + } + default: + return state; + } +}; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/activities/activitiesSaga.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/activities/activitiesSaga.js new file mode 100644 index 00000000..2350b9c3 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/activities/activitiesSaga.js @@ -0,0 +1,41 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { call, put, takeEvery, all } from 'redux-saga/effects'; +import { genericNetworkErrorAction } from 'src/appConstants'; +import { GET_ACTIVITIES } from './activitiesConstants'; +import activitiesApi from './activitiesApi'; +import { setActivitiesList } from './activitiesActions'; + +function* fetchActivities() { + try { + const activitiesList = yield call(activitiesApi.fetchActivities); + const updatedActivitiesList = yield all( + activitiesList.items.map(item => + call(activitiesApi.fetchActivity, item.id) + ) + ); + + yield put(setActivitiesList(updatedActivitiesList)); + } catch (error) { + yield put(genericNetworkErrorAction(error)); + } +} + +function* activitiesSaga() { + yield takeEvery(GET_ACTIVITIES, fetchActivities); +} + +export default activitiesSaga; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/activities/activitiesSelectors.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/activities/activitiesSelectors.js new file mode 100644 index 00000000..fc0c55ab --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/activities/activitiesSelectors.js @@ -0,0 +1,22 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +export const activitiesSelector = state => + state && + state.activities.map(item => ({ + ...item, + value: item.name + })); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/Catalog.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/Catalog.js new file mode 100644 index 00000000..b675b220 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/Catalog.js @@ -0,0 +1,53 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http: //www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { connect } from 'react-redux'; + +import CatalogView from 'features/catalog/CatalogView'; +import { + fetchWorkflow, + searchChangedAction +} from 'features/catalog/catalogActions'; + +import { showCustomModalAction } from 'shared/modal/modalWrapperActions'; +import { NEW_WORKFLOW_MODAL } from 'shared/modal/modalWrapperComponents'; +import { clearWorkflowAction } from 'features/workflow/workflowConstants'; + +const mapStateToProps = state => ({ + catalog: state.catalog +}); + +const mapDispatchToProps = dispatch => ({ + handleFetchWorkflow: ({ sort, offset, searchNameFilter, status }) => + dispatch(fetchWorkflow({ sort, offset, searchNameFilter, status })), + clearWorkflow: () => dispatch(clearWorkflowAction), + showNewWorkflowModal: () => + dispatch( + showCustomModalAction({ + customComponentName: NEW_WORKFLOW_MODAL, + title: 'New Workflow' + }) + ), + searchInputChanged: searchValue => + dispatch(searchChangedAction(searchValue)) +}); + +const Catalog = connect( + mapStateToProps, + mapDispatchToProps +)(CatalogView); + +export default Catalog; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/CatalogView.jsx b/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/CatalogView.jsx new file mode 100644 index 00000000..edaa64f3 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/CatalogView.jsx @@ -0,0 +1,182 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http: //www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import InfiniteScroll from 'shared/scroll/InfiniteScroll'; +import Workflows from 'features/catalog/views/Workflows'; +import AddWorkflow from 'features/catalog/views/AddWorkflow'; +import { WORKFLOW_STATUS } from 'features/workflow/workflowConstants'; + +import Header from 'features/catalog/views/Header'; +import Main from 'features/catalog/views/Main'; +import { NAME, ASC, DESC } from 'features/catalog/catalogConstants'; + +class CatalogView extends Component { + constructor(props) { + super(props); + } + + componentDidMount() { + const { clearWorkflow } = this.props; + clearWorkflow(); + this.fetchWorkflows(); + } + + fetchWorkflows = () => { + const { + catalog: { sort, status, searchNameFilter }, + handleFetchWorkflow + } = this.props; + handleFetchWorkflow({ + sort, + searchNameFilter, + status + }); + }; + + handleAlphabeticalOrderByClick = e => { + e.preventDefault(); + + const { + handleFetchWorkflow, + catalog: { sort, status, searchNameFilter } + } = this.props; + + const payload = { + ...sort + }; + + payload[NAME] = payload[NAME] === ASC ? DESC : ASC; + handleFetchWorkflow({ + sort: payload, + searchNameFilter, + status + }); + }; + handleStatusChange = value => { + const { + handleFetchWorkflow, + catalog: { sort, searchNameFilter } + } = this.props; + + handleFetchWorkflow({ + sort, + searchNameFilter, + status: value + }); + }; + + handleScroll = () => { + const { + catalog: { + paging: { offset }, + sort, + status, + searchNameFilter + }, + handleFetchWorkflow + } = this.props; + handleFetchWorkflow({ + sort, + offset, + searchNameFilter, + status + }); + }; + + goToOverviewPage = id => { + const { history } = this.props; + const { location } = history; + history.push(`${location.pathname}workflow/${id}/overview`); + }; + + searchChange = searchValue => { + this.setState({ searchValue: searchValue }); + this.dispatchChange(searchValue); + }; + + dispatchChange = searchValue => { + const { searchInputChanged, catalog } = this.props; + searchInputChanged({ + ...catalog, + searchNameFilter: searchValue + }); + sessionStorage.setItem('searchNameFilter', searchValue); + }; + + render() { + const { catalog, showNewWorkflowModal } = this.props; + const { + sort, + paging: { hasMore, total }, + items, + status, + searchNameFilter + } = catalog; + const alphabeticalOrder = sort[NAME]; + + return ( + <div className="wf-catalog"> + <Header + status={status} + statusChange={this.handleStatusChange} + searchChange={this.searchChange} + searchValue={searchNameFilter} + /> + <InfiniteScroll + useWindow={false} + loadMore={this.handleScroll} + hasMore={hasMore}> + <Main + total={total} + alphabeticalOrder={alphabeticalOrder} + onAlphabeticalOrderByClick={ + this.handleAlphabeticalOrderByClick + }> + <div className="main__content"> + {status === WORKFLOW_STATUS.ACTIVE && ( + <AddWorkflow onClick={showNewWorkflowModal} /> + )} + <Workflows + items={items} + onWorkflowClick={this.goToOverviewPage} + /> + </div> + </Main> + </InfiniteScroll> + </div> + ); + } +} + +CatalogView.propTypes = { + history: PropTypes.object, + catalog: PropTypes.object, + handleResetWorkflow: PropTypes.func, + handleFetchWorkflow: PropTypes.func, + showNewWorkflowModal: PropTypes.func, + clearWorkflow: PropTypes.func, + searchInputChanged: PropTypes.func, + searchNameFilter: PropTypes.string +}; + +CatalogView.defaultProps = { + showNewWorkflowModal: () => {}, + clearWorkflow: () => {} +}; + +export default CatalogView; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/__tests__/catalogActions-test.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/__tests__/catalogActions-test.js new file mode 100644 index 00000000..bd2b0006 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/__tests__/catalogActions-test.js @@ -0,0 +1,65 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +'use strict'; + +import { + FETCH_WORKFLOW, + UPDATE_WORKFLOW, + LIMIT, + NAME, + ASC +} from 'features/catalog/catalogConstants'; +import { WORKFLOW_STATUS } from 'features/workflow/workflowConstants'; +import { fetchWorkflow, updateWorkflow } from 'features/catalog/catalogActions'; + +describe('Catalog Actions', () => { + it('should have `fetchWorkflow` action', () => { + const sort = { [NAME]: ASC }; + const offset = 0; + const status = WORKFLOW_STATUS.ACTIVE; + expect(fetchWorkflow({ sort, offset, status })).toEqual({ + type: FETCH_WORKFLOW, + payload: { + sort, + limit: LIMIT, + status, + offset + } + }); + }); + + it('should have `updateWorkflow` action', () => { + const payload = { + paging: { + offset: 1, + limit: 1, + count: 1, + hasMore: false, + total: 2 + }, + items: [], + sort: { + name: 'asc' + } + }; + + expect(updateWorkflow(payload)).toEqual({ + type: UPDATE_WORKFLOW, + payload + }); + }); +}); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/__tests__/catalogReducer-test.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/__tests__/catalogReducer-test.js new file mode 100644 index 00000000..264e8112 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/__tests__/catalogReducer-test.js @@ -0,0 +1,112 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http: //www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +'use strict'; + +import { NAME, ASC, DESC } from 'features/catalog/catalogConstants'; +import catalogReducer, { initialState } from 'features/catalog/catalogReducer'; +import { updateWorkflow } from 'features/catalog/catalogActions'; +import { WORKFLOW_STATUS } from 'features/workflow/workflowConstants'; + +describe('Catalog Reducer', () => { + const state = { + paging: { + offset: 1, + limit: 1, + count: 1, + hasMore: false, + total: 2 + }, + status: WORKFLOW_STATUS.ACTIVE, + sort: { + [NAME]: ASC + }, + searchNameFilter: '', + items: [ + { + id: 'c5b7ca1a0f7944bfa948b85b32c5f314', + name: 'Workflow_2', + description: null, + versionStates: ['DRAFT'], + versions: null + }, + { + id: '221336ef3f1645c686bc81899368ac27', + name: 'Workflow_1', + description: null, + versionStates: ['DRAFT'], + versions: null + } + ] + }; + + const sort = { + [NAME]: DESC + }; + + const offset = 0; + + const dataPayload = { + paging: { + offset, + limit: 10, + count: 2, + hasMore: false, + total: 2 + }, + items: [ + { + id: 'c5b7ca1a0f7944bfa948b85b32c5f314', + name: 'Workflow_2', + description: null, + versionStates: ['DRAFT'], + versions: null + }, + { + id: '221336ef3f1645c686bc81899368ac27', + name: 'Workflow_1', + description: null, + versionStates: ['DRAFT'], + versions: null + } + ] + }; + + it('returns the initial state', () => { + expect(catalogReducer(undefined, {})).toEqual(initialState); + }); + + it('should replace results when page is first', () => { + expect( + catalogReducer(state, updateWorkflow({ sort, ...dataPayload })) + ).toEqual({ + ...initialState, + sort, + ...dataPayload + }); + }); + + it('should add results when page is not first', () => { + expect( + catalogReducer( + state, + updateWorkflow({ sort, ...{ ...dataPayload, offset: 2 } }) + ).items + ).toEqual( + expect.arrayContaining([...dataPayload.items, ...state.items]) + ); + }); +}); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/__tests__/catalogSagas-test.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/__tests__/catalogSagas-test.js new file mode 100644 index 00000000..d3e9bda2 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/__tests__/catalogSagas-test.js @@ -0,0 +1,109 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http: //www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +'use strict'; + +import { runSaga } from 'redux-saga'; +import { takeLatest } from 'redux-saga/effects'; + +import { + NAME, + DESC, + LIMIT, + SEARCH_CHANGED +} from 'features/catalog/catalogConstants'; +import { WORKFLOW_STATUS } from 'features/workflow/workflowConstants'; +import catalogApi from '../catalogApi'; +import { fetchWorkflow, updateWorkflow } from 'features/catalog/catalogActions'; +import catalogSaga, { + fetchWorkflowSaga, + debounceSearchChanged +} from 'features/catalog/catalogSagas'; + +jest.mock('../catalogApi'); + +describe('Catalog Sagas', () => { + it('should watch for `fetchWorkflow` action', () => { + const gen = catalogSaga(); + + expect(gen.next().value).toEqual( + takeLatest(fetchWorkflow, fetchWorkflowSaga) + ); + expect(gen.next().value).toEqual( + takeLatest(SEARCH_CHANGED, debounceSearchChanged) + ); + expect(gen.next().done).toBe(true); + }); + + it('should get workflows and put `updateWorkflow` action', async () => { + const sort = { + [NAME]: DESC + }; + const status = WORKFLOW_STATUS.ACTIVE; + const offset = 0; + const searchNameFilter = undefined; + const data = { + paging: { + offset, + limit: 10, + count: 2, + hasMore: false, + total: 2 + }, + status: WORKFLOW_STATUS.ACTIVE, + searchNameFilter: 'w', + items: [ + { + id: 'c5b7ca1a0f7944bfa948b85b32c5f314', + name: 'Workflow_2', + description: null, + versionStates: ['DRAFT'], + versions: null + }, + { + id: '221336ef3f1645c686bc81899368ac27', + name: 'Workflow_1', + description: null, + versionStates: ['DRAFT'], + versions: null + } + ] + }; + const dispatched = []; + + catalogApi.getWorkflows.mockReturnValue(data); + + await runSaga( + { + dispatch: action => dispatched.push(action) + }, + fetchWorkflowSaga, + fetchWorkflow({ sort, offset, status }) + ).done; + + expect(dispatched).toEqual( + expect.arrayContaining([updateWorkflow({ sort, ...data })]) + ); + + expect(catalogApi.getWorkflows).toBeCalledWith({ + sort, + status, + limit: LIMIT, + offset: offset + LIMIT, + searchNameFilter + }); + }); +}); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/catalogActions.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/catalogActions.js new file mode 100644 index 00000000..81c22848 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/catalogActions.js @@ -0,0 +1,48 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { createActions, createAction } from 'redux-actions'; + +import { + NAMESPACE, + LIMIT, + SEARCH_CHANGED, + FETCH_WORKFLOW +} from 'features/catalog/catalogConstants'; + +export const { + [NAMESPACE]: { updateWorkflow } +} = createActions({ + [NAMESPACE]: { + UPDATE_WORKFLOW: undefined + } +}); + +export const fetchWorkflow = createAction( + FETCH_WORKFLOW, + ({ sort, offset, searchNameFilter, status }) => ({ + sort, + limit: LIMIT, + offset, + searchNameFilter, + status + }) +); + +export const searchChangedAction = createAction( + SEARCH_CHANGED, + payload => payload +); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/catalogApi.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/catalogApi.js new file mode 100644 index 00000000..b15f3d8e --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/catalogApi.js @@ -0,0 +1,45 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http: //www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import qs from 'qs'; + +import RestfulAPIUtil from 'services/restAPIUtil'; +import Configuration from 'config/Configuration.js'; + +function baseUrl() { + const restPrefix = Configuration.get('restPrefix'); + return `${restPrefix}/workflows`; +} + +const Api = { + getWorkflows: ({ sort, limit, offset, searchNameFilter, status }) => { + const queryParams = { + sort: Object.keys(sort).map(key => `${key}:${sort[key]}`), + limit, + offset, + archiving: status + }; + if (searchNameFilter) queryParams.searchNameFilter = searchNameFilter; + const queryString = qs.stringify(queryParams, { + indices: false, + addQueryPrefix: true + }); + + return RestfulAPIUtil.fetch(`${baseUrl()}${queryString}`); + } +}; + +export default Api; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/catalogConstants.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/catalogConstants.js new file mode 100644 index 00000000..44bdc6bb --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/catalogConstants.js @@ -0,0 +1,31 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +export const NAMESPACE = 'catalog'; + +export const NAME = 'name'; +export const ASC = 'asc'; +export const DESC = 'desc'; + +//Limit = max tiles in a standard screen +export const LIMIT = 31; + +export const SEARCH_BUFFER = 1000; +export const SEARCH_CHANGED = `catalog/SEARCH_CHANGED`; + +export const FETCH_WORKFLOW = `${NAMESPACE}/FETCH_WORKFLOW`; +export const UPDATE_WORKFLOW = `${NAMESPACE}/UPDATE_WORKFLOW`; +export const RESET_WORKFLOW = `${NAMESPACE}/RESET_WORKFLOW`; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/catalogReducer.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/catalogReducer.js new file mode 100644 index 00000000..db7bc43b --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/catalogReducer.js @@ -0,0 +1,60 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http: //www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { + NAME, + ASC, + UPDATE_WORKFLOW, + SEARCH_CHANGED +} from 'features/catalog/catalogConstants'; +import { WORKFLOW_STATUS } from 'features/workflow/workflowConstants'; +export const initialState = { + paging: { + hasMore: true, + total: 0 + }, + sort: { + [NAME]: ASC + }, + status: WORKFLOW_STATUS.ACTIVE, + //In order to save state inside iframe session + searchNameFilter: sessionStorage.getItem('searchNameFilter') || '' +}; + +const catalogReducer = (state = initialState, action) => { + const { type, payload } = action; + + switch (type) { + case UPDATE_WORKFLOW: + return { + ...state, + ...payload, + items: + payload.paging.offset === 0 + ? [...payload.items] + : [...state.items, ...payload.items] + }; + case SEARCH_CHANGED: + return { + ...state, + searchNameFilter: action.payload.searchNameFilter + }; + default: + return state; + } +}; + +export default catalogReducer; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/catalogSagas.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/catalogSagas.js new file mode 100644 index 00000000..dd77bc97 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/catalogSagas.js @@ -0,0 +1,56 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http: //www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { call, put, takeLatest } from 'redux-saga/effects'; +import { delay } from 'redux-saga'; + +import catalogApi from 'features/catalog/catalogApi'; +import { fetchWorkflow, updateWorkflow } from 'features/catalog/catalogActions'; +import { + SEARCH_CHANGED, + LIMIT, + SEARCH_BUFFER +} from 'features/catalog/catalogConstants'; + +const noOp = () => {}; + +export function* fetchWorkflowSaga({ payload }) { + const { sort, limit, offset, searchNameFilter, status } = payload; + try { + const data = yield call(catalogApi.getWorkflows, { + sort, + limit: LIMIT, + offset: offset === undefined ? 0 : offset + limit, + searchNameFilter, + status + }); + yield put(updateWorkflow({ sort, status, ...data })); + } catch (e) { + noOp(); + } +} + +export function* debounceSearchChanged({ payload }) { + yield call(delay, SEARCH_BUFFER); + yield call(fetchWorkflowSaga, { payload }); +} + +function* catalogSaga() { + yield takeLatest(fetchWorkflow, fetchWorkflowSaga); + yield takeLatest(SEARCH_CHANGED, debounceSearchChanged); +} + +export default catalogSaga; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/views/AddWorkflow.jsx b/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/views/AddWorkflow.jsx new file mode 100644 index 00000000..1700d92e --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/views/AddWorkflow.jsx @@ -0,0 +1,45 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { Translate } from 'react-redux-i18n'; +import { SVGIcon } from 'onap-ui-react'; + +class AddWorkflow extends React.Component { + render() { + const { onClick } = this.props; + return ( + <div + className="add-workflow" + data-test-id="wf-catalog-add-workflow" + onClick={onClick}> + <div className="add-workflow__icon"> + <SVGIcon name="plusCircleThick" /> + </div> + <div className="add-workflow__label"> + <Translate value="catalog.addWorkflow" /> + </div> + </div> + ); + } +} + +AddWorkflow.propTypes = { + onClick: PropTypes.func +}; + +export default AddWorkflow; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/views/Header.jsx b/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/views/Header.jsx new file mode 100644 index 00000000..b70c0a50 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/views/Header.jsx @@ -0,0 +1,46 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http: //www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import SearchInput from 'shared/searchInput/SearchInput'; +import StatusSelect from './StatusSelector'; + +const Header = ({ searchChange, searchValue, statusChange, status }) => ( + <div className="header"> + <StatusSelect status={status} onChange={statusChange} /> + <div className="header__search"> + <SearchInput + dataTestId="wf-catalog-search" + onChange={searchChange} + value={searchValue} + /> + </div> + </div> +); + +Header.propTypes = { + searchChange: PropTypes.func, + searchValue: PropTypes.string, + statusChange: PropTypes.func, + status: PropTypes.string +}; + +Header.defaultProps = { + searchChange: () => {}, + searchValue: '' +}; +export default Header; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/views/Main.jsx b/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/views/Main.jsx new file mode 100644 index 00000000..b4288f3b --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/views/Main.jsx @@ -0,0 +1,79 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http: //www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Translate } from 'react-redux-i18n'; + +import { ASC } from 'features/catalog/catalogConstants'; +import { SVGIcon } from 'onap-ui-react'; + +class Main extends Component { + render() { + const { + total, + alphabeticalOrder, + onAlphabeticalOrderByClick, + children + } = this.props; + + return ( + <div className="main"> + <div className="main__header"> + <div className="main__header__total"> + <Translate value="catalog.elementFound" count={total} /> + </div> + <div className="main__header__order"> + <div className="main__header__order__label"> + <Translate value="catalog.orderBy" />: + </div> + <div + className="main__header__order__alphabetical" + data-test-id="wf-catalog-alphabetical-order" + onClick={onAlphabeticalOrderByClick}> + <div className="main__header__order__alphabetical__label"> + <Translate value="catalog.alphabeticalOrder" /> + </div> + <div + className={ + (alphabeticalOrder === ASC && + 'main__header__order__alphabetical__icon') || + 'main__header__order__alphabetical__icon main__header__order__alphabetical__icon--flip' + }> + <SVGIcon name="caretDown" /> + </div> + </div> + </div> + </div> + {children} + </div> + ); + } +} + +Main.propTypes = { + total: PropTypes.number, + alphabeticalOrder: PropTypes.string, + onAlphabeticalOrderByClick: PropTypes.func, + handleSort: PropTypes.func, + children: PropTypes.node +}; + +Main.defaultProps = { + total: 0 +}; + +export default Main; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/views/StatusSelector.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/views/StatusSelector.js new file mode 100644 index 00000000..5b77bd5d --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/views/StatusSelector.js @@ -0,0 +1,24 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { WORKFLOW_STATUS } from 'features/workflow/workflowConstants'; + +const StatusSelect = ({ status, onChange }) => ( + <select + className="wf-status-select" + value={status} + data-test-id="status-select" + onChange={e => onChange(e.target.value)}> + {Object.keys(WORKFLOW_STATUS).map((type, i) => ( + <option key={`type.${i}`} value={WORKFLOW_STATUS[type]}> + {type} + </option> + ))} + </select> +); + +StatusSelect.propTypes = { + status: PropTypes.string, + onChange: PropTypes.func +}; + +export default StatusSelect; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/views/Workflows.jsx b/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/views/Workflows.jsx new file mode 100644 index 00000000..d69ec47e --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/catalog/views/Workflows.jsx @@ -0,0 +1,47 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http: //www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import { Tile, TileInfo, TileInfoLine } from 'onap-ui-react'; + +const Workflows = ({ items, onWorkflowClick }) => + items.map((workflow, index) => ( + <Tile + key={`workflow.${index}`} + dataTestId="wf-catalog-workflow-item" + headerText="WF" + headerColor="blue" + iconName="workflow" + iconColor="blue" + onClick={() => onWorkflowClick(workflow.id)}> + <TileInfo> + <TileInfoLine type="title">{workflow.name}</TileInfoLine> + </TileInfo> + </Tile> + )); + +Workflows.propTypes = { + items: PropTypes.array, + onWorkflowClick: PropTypes.func +}; + +Workflows.defaultProps = { + items: [] +}; + +export default Workflows; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/Version.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/Version.js new file mode 100644 index 00000000..5df68759 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/Version.js @@ -0,0 +1,19 @@ +import { connect } from 'react-redux'; +import VersionView from 'features/version/VersionView'; +import { workflowVersionFetchRequestedAction } from 'features/version/versionConstants'; + +const mapStateToProps = ({ currentVersion: { operationMode } }) => { + return { + operationMode + }; +}; + +const mapDispatchToProps = dispatch => ({ + loadSelectedVersion: payload => + dispatch(workflowVersionFetchRequestedAction(payload)) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(VersionView); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/VersionView.jsx b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/VersionView.jsx new file mode 100644 index 00000000..48c671e2 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/VersionView.jsx @@ -0,0 +1,127 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { Route, matchPath } from 'react-router-dom'; +import { I18n } from 'react-redux-i18n'; + +import NavigationSideBar from 'shared/navigationSideBar/index'; +import VersionController from 'features/version/versionController/VersionController'; + +class VersionView extends React.Component { + componentDidMount() { + const { loadSelectedVersion, match } = this.props; + const workflowId = match.params.workflowId; + const versionId = match.params.versionId; + loadSelectedVersion({ workflowId, versionId }); + } + + onSelect = item => { + const { history, match } = this.props; + + if (!item.disabled) { + history.push( + item.path === '/' ? match.url : `${match.url}${item.path}` + ); + } + }; + + getGroups = () => { + const { routes } = this.props; + + const items = routes.map(route => { + return route.i18nName + ? { + ...route, + name: I18n.t(route.i18nName) + } + : route; + }); + + return [ + { + id: 'WORKFLOW', + items + } + ]; + }; + + getActiveItemIdProps = () => { + const { location, routes, match } = this.props; + + const activeItem = routes.find(route => + matchPath(location.pathname, { + path: `${match.path}${route.path}`, + exact: true, + strict: false + }) + ); + + return activeItem && activeItem.id; + }; + + render() { + const { match, routes, history, operationMode } = this.props; + + const groups = this.getGroups(); + const activeItemId = this.getActiveItemIdProps(); + + return ( + <div className="version-wrapper"> + <VersionController + operationMode={operationMode} + history={history} + match={match} + key="versionControllerView" + /> + + <div + className={`${operationMode ? '' : 'workflow-view'}`} + key="workflowView"> + {!operationMode && ( + <div className="workflow-navigation-side-bar"> + <NavigationSideBar + groups={groups} + activeItemId={activeItemId} + onSelect={this.onSelect} + /> + </div> + )} + {routes.map((route, i) => ( + <Route + key={`Version.route.${i}`} + exact={route.exact} + path={`${match.url}${route.path}`} + component={route.component} + /> + ))} + </div> + </div> + ); + } +} + +VersionView.propTypes = { + history: PropTypes.object, + location: PropTypes.object, + match: PropTypes.object, + routes: PropTypes.array, + loadSelectedVersion: PropTypes.func, + operationMode: PropTypes.bool +}; + +export default VersionView; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/Composition.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/Composition.js new file mode 100644 index 00000000..97697c70 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/Composition.js @@ -0,0 +1,62 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { connect } from 'react-redux'; +import { I18n } from 'react-redux-i18n'; +import { updateComposition, updateValidation } from './compositionActions'; +import CompositionView from './CompositionView'; +import { showErrorModalAction } from '../../../shared/modal/modalWrapperActions'; +import { getComposition, getErrors } from './compositionSelectors'; +import { getWorkflowName } from '../../workflow/workflowSelectors'; +import { activitiesSelector } from 'features/activities/activitiesSelectors'; +import { getInputOutputForComposition } from 'features/version/inputOutput/inputOutputSelectors'; +import { getVersionInfo } from 'features/version/general/generalSelectors'; +import { getIsCertified } from 'features/version/general/generalSelectors'; +import { isWorkflowArchive } from 'features/workflow/workflowSelectors'; + +function mapStateToProps(state) { + return { + composition: getComposition(state), + name: getWorkflowName(state), + versionName: getVersionInfo(state).name, + activities: activitiesSelector(state), + inputOutput: getInputOutputForComposition(state), + errors: getErrors(state), + isReadOnly: getIsCertified(state) || isWorkflowArchive(state) + }; +} + +function mapDispatchToProps(dispatch) { + return { + compositionUpdate: composition => + dispatch(updateComposition(composition)), + showErrorModal: msg => + dispatch( + showErrorModalAction({ + title: I18n.t('workflow.composition.bpmnError'), + body: msg, + withButtons: true, + closeButtonText: I18n.t('buttons.okBtn') + }) + ), + validationUpdate: (element, isValid) => + dispatch(updateValidation({ element, isValid })) + }; +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(CompositionView); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/CompositionUpdate.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/CompositionUpdate.js new file mode 100644 index 00000000..e5756eb5 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/CompositionUpdate.js @@ -0,0 +1,131 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +*http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { I18n } from 'react-redux-i18n'; + +import CustomModeler from 'features/version/composition/custom-modeler'; +import camundaModuleDescriptor from 'features/version/composition/custom-properties-provider/descriptors/camunda'; +import { setElementInputsOutputs } from 'features/version/composition/bpmnUtils.js'; + +import { connect } from 'react-redux'; +import { updateComposition } from 'features/version/composition/compositionActions'; +import { showErrorModalAction } from 'shared/modal/modalWrapperActions'; +import { getComposition } from 'features/version/composition/compositionSelectors'; +import { getWorkflowName } from 'features/workflow/workflowSelectors'; +import { activitiesSelector } from 'features/activities/activitiesSelectors'; +import { getInputOutputForComposition } from 'features/version/inputOutput/inputOutputSelectors'; + +class CompositionUpdate extends Component { + static propTypes = { + compositionUpdate: PropTypes.func, + showErrorModal: PropTypes.func, + composition: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), + inputOutput: PropTypes.object, + activities: PropTypes.object, + certifyBack: PropTypes.func + }; + + constructor(props) { + super(props); + this.generatedId = 'bpmn-container' + Date.now(); + this.fileInput = React.createRef(); + this.bpmnContainer = React.createRef(); + } + + componentDidMount() { + const { composition, activities, inputOutput } = this.props; + + this.modeler = new CustomModeler({ + moddleExtensions: { + camunda: camundaModuleDescriptor + }, + workflow: { + activities: activities, + workflowInputOutput: inputOutput + } + }); + + this.setDiagramToBPMN(composition); + } + + setDiagramToBPMN = diagram => { + let modeler = this.modeler; + this.modeler.importXML(diagram, err => { + if (err) { + return this.props.showErrorModal( + I18n.t('workflow.composition.importErrorMsg') + ); + } + const canvas = modeler.get('canvas'); + const { businessObject } = canvas._rootElement; + + setElementInputsOutputs( + businessObject, + this.props.inputOutput, + this.modeler.get('moddle') + ); + + this.exportDiagramToStore(); + }); + }; + + exportDiagramToStore = () => { + this.modeler.saveXML({ format: true }, (err, xml) => { + if (err) { + return this.props.showErrorModal( + I18n.t('workflow.composition.saveErrorMsg') + ); + } + this.props.compositionUpdate(xml); + this.props.certifyBack(); + }); + }; + + render() { + return <div />; + } +} + +function mapStateToProps(state) { + return { + composition: getComposition(state), + name: getWorkflowName(state), + activities: activitiesSelector(state), + inputOutput: getInputOutputForComposition(state) + }; +} + +function mapDispatchToProps(dispatch) { + return { + compositionUpdate: composition => + dispatch(updateComposition(composition)), + showErrorModal: msg => + dispatch( + showErrorModalAction({ + title: I18n.t('workflow.composition.bpmnError'), + body: msg, + withButtons: true, + closeButtonText: I18n.t('buttons.okBtn') + }) + ) + }; +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(CompositionUpdate); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/CompositionView.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/CompositionView.js new file mode 100644 index 00000000..e444d98c --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/CompositionView.js @@ -0,0 +1,284 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +*http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import React, { Component } from 'react'; +import fileSaver from 'file-saver'; +import isEqual from 'lodash.isequal'; +import PropTypes from 'prop-types'; +import propertiesPanelModule from 'bpmn-js-properties-panel'; +import { I18n } from 'react-redux-i18n'; + +import CustomModeler from './custom-modeler'; +import propertiesProviderModule from './custom-properties-provider/provider/camunda'; +import camundaModuleDescriptor from './custom-properties-provider/descriptors/camunda'; +import newDiagramXML from './newDiagram.bpmn'; +import CompositionButtons from './components/CompositionButtonsPanel'; +import { setElementInputsOutputs } from './bpmnUtils.js'; +import { + PROCESS_DEFAULT_ID, + COMPOSITION_ERROR_COLOR, + COMPOSITION_VALID_COLOR, + CAMUNDA_PANEL_INPUTS_NAMES +} from './compositionConstants'; +import readOnly from './readOnly'; + +function setStatusToElement(type, status, parent) { + let elements = parent.getElementsByTagName(type); + for (let item of elements) { + if (item.name !== 'selectedExtensionElement') { + item.readOnly = status; + item.disabled = status; + } + } +} + +function disablePanelInputs(status) { + let panel = document.getElementById('js-properties-panel'); + + if (panel) { + setStatusToElement('input', status, panel); + setStatusToElement('button', status, panel); + setStatusToElement('select', status, panel); + + //distinguish editable and clickable fields using attr and style + CAMUNDA_PANEL_INPUTS_NAMES.map(name => { + const div = document.getElementById(name); + if (div) { + div.setAttribute('editable-readonly', !status); + } + }); + } +} +class CompositionView extends Component { + static propTypes = { + compositionUpdate: PropTypes.func, + showErrorModal: PropTypes.func, + composition: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), + name: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), + versionName: PropTypes.string, + inputOutput: PropTypes.object, + activities: PropTypes.array, + validationUpdate: PropTypes.func, + errors: PropTypes.array, + isReadOnly: PropTypes.bool + }; + + constructor(props) { + super(props); + this.generatedId = 'bpmn-container' + Date.now(); + this.fileInput = React.createRef(); + this.bpmnContainer = React.createRef(); + this.versionChanged = false; + } + componentDidUpdate(prevProps) { + const { errors, isReadOnly, versionName, composition } = this.props; + if (!isEqual(prevProps.errors, errors)) { + errors.map(item => { + this.modeling.setColor([item.element], { + fill: item.isValid + ? COMPOSITION_VALID_COLOR + : COMPOSITION_ERROR_COLOR + }); + }); + } + if (prevProps.isReadOnly !== isReadOnly) { + this.modeler.get('readOnly').readOnly(isReadOnly); + disablePanelInputs(isReadOnly); + } + + if (prevProps.versionName !== versionName) { + this.versionChanged = true; + } + if ( + !isEqual(prevProps.composition, composition) && + this.versionChanged + ) { + this.setDiagramToBPMN(composition); + this.versionChanged = false; + } + } + componentDidMount() { + const { + composition, + activities, + inputOutput, + validationUpdate, + isReadOnly + } = this.props; + + const readOnlyModule = { + __init__: ['readOnly'], + readOnly: ['type', readOnly] + }; + this.modeler = new CustomModeler({ + propertiesPanel: { + parent: '#js-properties-panel' + }, + additionalModules: [ + propertiesPanelModule, + propertiesProviderModule, + readOnlyModule + ], + moddleExtensions: { + camunda: camundaModuleDescriptor + }, + workflow: { + activities: activities, + getActivityInputsOutputs: this.getActivityInputsOutputs, + workflowInputOutput: inputOutput, + validationUpdate: validationUpdate + } + }); + + this.modeler.attachTo('#' + this.generatedId); + this.setDiagramToBPMN(composition); + this.modeler.on('element.out', () => this.exportDiagramToStore()); + this.modeler.on('element.click', this.handleCompositionStatus); + this.modeler.on( + 'propertiesPanel.changed', + this.handleCompositionStatus + ); + this.modeling = this.modeler.get('modeling'); + this.modeler.get('readOnly').readOnly(isReadOnly); + } + handleCompositionStatus = () => { + disablePanelInputs(this.props.isReadOnly); + }; + getActivityInputsOutputs = selectedValue => { + const selectedActivity = this.props.activities.find( + el => el.name === selectedValue + ); + + if (selectedActivity) { + const inputsOutputs = { + inputs: selectedActivity.inputs, + outputs: selectedActivity.outputs + }; + return inputsOutputs; + } else return { inputs: [], outputs: [] }; + }; + + setDiagramToBPMN = diagram => { + let modeler = this.modeler; + this.modeler.importXML(diagram, err => { + if (err) { + return this.props.showErrorModal( + I18n.t('workflow.composition.importErrorMsg') + ); + } + const canvas = modeler.get('canvas'); + canvas.zoom('fit-viewport'); + const { businessObject } = canvas._rootElement; + + this.setDefaultIdAndName(businessObject); + setElementInputsOutputs( + businessObject, + this.props.inputOutput, + this.modeler.get('moddle') + ); + disablePanelInputs(this.props.isReadOnly); + }); + }; + setDefaultIdAndName = businessObject => { + const { name = '' } = this.props; + if (!businessObject.name) { + businessObject.name = name; + } + + if (businessObject.id === PROCESS_DEFAULT_ID || !businessObject.id) { + businessObject.id = name.toLowerCase().replace(/\s/g, '_'); + } + }; + exportDiagramToStore = () => { + this.modeler.saveXML({ format: true }, (err, xml) => { + if (err) { + return this.props.showErrorModal( + I18n.t('workflow.composition.saveErrorMsg') + ); + } + return this.props.compositionUpdate(xml); + }); + }; + + exportDiagram = () => { + const { name, showErrorModal, versionName } = this.props; + this.modeler.saveXML({ format: true }, (err, xml) => { + if (err) { + return showErrorModal( + I18n.t('workflow.composition.exportErrorMsg') + ); + } + const blob = new Blob([xml], { type: 'text/html;charset=utf-8' }); + fileSaver.saveAs( + blob, + `${name.replace(/\s/g, '').toLowerCase()}-${versionName}.bpmn` + ); + }); + }; + + loadNewDiagram = () => { + this.setDiagramToBPMN(newDiagramXML); + }; + + uploadDiagram = () => { + this.fileInput.current.click(); + }; + + handleFileInputChange = filesList => { + const file = filesList[0]; + const reader = new FileReader(); + reader.onloadend = event => { + var xml = event.target.result; + this.setDiagramToBPMN(xml); + this.fileInput.value = ''; + }; + reader.readAsText(file); + }; + + render() { + return ( + <div className="composition-view content"> + <input + ref={this.fileInput} + onChange={e => this.handleFileInputChange(e.target.files)} + id="file-input" + accept=".bpmn, .xml" + type="file" + name="file-input" + style={{ display: 'none' }} + /> + <div + ref={this.bpmnContainer} + className="bpmn-container" + id={this.generatedId} + /> + <div className="bpmn-sidebar"> + <div + className="properties-panel" + id="js-properties-panel" + /> + <CompositionButtons + isReadOnly={this.props.isReadOnly} + onClean={this.loadNewDiagram} + onDownload={this.exportDiagram} + onUpload={this.uploadDiagram} + /> + </div> + </div> + ); + } +} + +export default CompositionView; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/bpmnUtils.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/bpmnUtils.js new file mode 100644 index 00000000..ada2bdc4 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/bpmnUtils.js @@ -0,0 +1,93 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +*http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { bpmnElementsTypes } from './compositionConstants'; +function getExtension(element, type) { + if (!element.extensionElements || !element.extensionElements.values) { + return null; + } + + return element.extensionElements.values.filter(function(e) { + return e.$instanceOf(type); + })[0]; +} + +export function updatedData(moddle, inputData, existingArray, type, parent) { + return inputData.map(item => { + const existingInput = existingArray.find(el => el.name === item.name); + const updatedElement = moddle.create( + type, + existingInput ? { ...item, value: existingInput.value } : item + ); + updatedElement.$parent = parent; + return updatedElement; + }); +} + +export function setElementInputsOutputs( + businessObject, + inputOutput, + moddle, + cleanInputsOutpus +) { + const { inputs = [], outputs = [] } = inputOutput; + + if (!businessObject.extensionElements) { + businessObject.extensionElements = moddle.create( + bpmnElementsTypes.EXTENSION_ElEMENTS + ); + businessObject.extensionElements.$parent = businessObject.id; + } + + const existingInputOutput = getExtension( + businessObject, + bpmnElementsTypes.INPUT_OUTPUT + ); + + const processInputs = updatedData( + moddle, + inputs, + cleanInputsOutpus + ? [] + : (existingInputOutput && existingInputOutput.inputParameters) || + [], + bpmnElementsTypes.INPUT_PARAMETER, + businessObject.id + ); + + const processOutputs = updatedData( + moddle, + outputs, + cleanInputsOutpus + ? [] + : (existingInputOutput && existingInputOutput.outputParameters) || + [], + bpmnElementsTypes.OUTPUT_PARAMETER, + businessObject.id + ); + + const processInputOutput = moddle.create(bpmnElementsTypes.INPUT_OUTPUT); + processInputOutput.$parent = businessObject.id; + processInputOutput.inputParameters = [...processInputs]; + processInputOutput.outputParameters = [...processOutputs]; + + const extensionElements = businessObject.extensionElements.get('values'); + businessObject.extensionElements.set( + 'values', + extensionElements + .filter(item => item.$type !== bpmnElementsTypes.INPUT_OUTPUT) + .concat(processInputOutput) + ); +} diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/components/CompositionButton.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/components/CompositionButton.js new file mode 100644 index 00000000..62500d25 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/components/CompositionButton.js @@ -0,0 +1,36 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import React from 'react'; +import PropTypes from 'prop-types'; +import { SVGIcon } from 'onap-ui-react'; + +const CompositionButton = ({ onClick, name, title, disabled }) => ( + <div + onClick={disabled ? () => {} : onClick} + className={`diagram-btn ${disabled ? 'disabled' : ''}`}> + <SVGIcon title={title} name={name} /> + </div> +); + +CompositionButton.propTypes = { + onClick: PropTypes.func, + className: PropTypes.string, + name: PropTypes.string, + title: PropTypes.string, + disabled: PropTypes.bool +}; + +export default CompositionButton; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/components/CompositionButtonsPanel.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/components/CompositionButtonsPanel.js new file mode 100644 index 00000000..0292fd4e --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/components/CompositionButtonsPanel.js @@ -0,0 +1,56 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import React from 'react'; +import PropTypes from 'prop-types'; +import CompositionButton from './CompositionButton'; + +const Divider = () => <div className="divider" />; + +const CompositionButtons = ({ onClean, onUpload, onDownload, isReadOnly }) => ( + <div className="composition-buttons"> + <CompositionButton + disabled={isReadOnly} + data-test-id="composition-clear-btn" + onClick={onClean} + name="trashO" + title="clear" + /> + + <Divider /> + <CompositionButton + data-test-id="composition-download-btn" + onClick={onDownload} + name="download" + title="download" + /> + <Divider /> + <CompositionButton + disabled={isReadOnly} + data-test-id="composition-download-upload" + onClick={onUpload} + name="upload" + title="upload" + /> + </div> +); + +CompositionButtons.propTypes = { + onClean: PropTypes.func, + onUpload: PropTypes.func, + onDownload: PropTypes.func, + isReadOnly: PropTypes.bool +}; +export default CompositionButtons; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionActions.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionActions.js new file mode 100644 index 00000000..fe74ba0d --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionActions.js @@ -0,0 +1,34 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +*http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { + SET_COMPOSITION, + UPDATE_ERRORS, + DELETE_COMPOSITION +} from './compositionConstants'; + +export const updateComposition = payload => ({ + type: SET_COMPOSITION, + payload +}); + +export const deleteCompositionArtifact = () => ({ + type: DELETE_COMPOSITION +}); + +export const updateValidation = payload => ({ + type: UPDATE_ERRORS, + payload +}); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionConstants.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionConstants.js new file mode 100644 index 00000000..1db40210 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionConstants.js @@ -0,0 +1,38 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +*http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +export const SET_COMPOSITION = 'composition/SET_COMPOSITION'; +export const DELETE_COMPOSITION = 'composition/DELETE_COMPOSITION'; +export const UPDATE_ERRORS = 'composition/UPDATE_ERRORS'; + +export const bpmnElementsTypes = { + EXTENSION_ElEMENTS: 'bpmn:ExtensionElements', + INPUT_OUTPUT: 'camunda:InputOutput', + INPUT_PARAMETER: 'camunda:InputParameter', + OUTPUT_PARAMETER: 'camunda:OutputParameter' +}; + +export const PROCESS_DEFAULT_ID = 'Process_1'; + +export const COMPOSITION_ERROR_COLOR = '#f0c2c2'; +export const COMPOSITION_VALID_COLOR = 'white'; + +//list of field ids (contenteditable) that has separate treatment +export const CAMUNDA_PANEL_INPUTS_NAMES = [ + 'camunda-parameterType-text', + 'camunda-documentation', + 'camunda-name', + 'camunda-listener-field-value' +]; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionReducer.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionReducer.js new file mode 100644 index 00000000..9deb9cbd --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionReducer.js @@ -0,0 +1,44 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +*http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { SET_COMPOSITION, DELETE_COMPOSITION } from './compositionConstants'; +import { UPDATE_ERRORS } from './compositionConstants'; +import newDiagramXML from './newDiagram.bpmn'; + +export default (state = { diagram: newDiagramXML, errors: [] }, action) => { + switch (action.type) { + case SET_COMPOSITION: + return { + ...state, + diagram: action.payload + }; + case DELETE_COMPOSITION: + return { + ...state, + diagram: newDiagramXML + }; + case UPDATE_ERRORS: { + const filteredErrors = state.errors.filter( + item => item.element.id !== action.payload.element.id + ); + return { + ...state, + errors: [...filteredErrors, action.payload] + }; + } + default: + return state; + } +}; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionSelectors.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionSelectors.js new file mode 100644 index 00000000..e6b51f1a --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionSelectors.js @@ -0,0 +1,32 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +*http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +export const getComposition = state => + state && state.currentVersion && state.currentVersion.composition.diagram; + +export const getCompositionHasErrors = state => + state && + state.currentVersion && + state.currentVersion.composition && + state.currentVersion.composition.errors && + Boolean( + state.currentVersion.composition.errors.find(item => !item.isValid) + ); + +export const getErrors = state => + state && + state.currentVersion && + state.currentVersion.composition && + state.currentVersion.composition.errors; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomContextPadProvider.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomContextPadProvider.js new file mode 100644 index 00000000..0f2ba528 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomContextPadProvider.js @@ -0,0 +1,43 @@ +import inherits from 'inherits';
+
+import ContextPadProvider from 'bpmn-js/lib/features/context-pad/ContextPadProvider';
+
+import { isAny } from 'bpmn-js/lib/features/modeling/util/ModelingUtil';
+
+import { assign, bind } from 'min-dash';
+
+export default function CustomContextPadProvider(injector, connect, translate) {
+ injector.invoke(ContextPadProvider, this);
+
+ var cached = bind(this.getContextPadEntries, this);
+
+ this.getContextPadEntries = function(element) {
+ var actions = cached(element);
+
+ var businessObject = element.businessObject;
+
+ function startConnect(event, element, autoActivate) {
+ connect.start(event, element, autoActivate);
+ }
+
+ if (isAny(businessObject, ['custom:triangle', 'custom:circle'])) {
+ assign(actions, {
+ connect: {
+ group: 'connect',
+ className: 'bpmn-icon-connection-multi',
+ title: translate('Connect using custom connection'),
+ action: {
+ click: startConnect,
+ dragstart: startConnect
+ }
+ }
+ });
+ }
+
+ return actions;
+ };
+}
+
+inherits(CustomContextPadProvider, ContextPadProvider);
+
+CustomContextPadProvider.$inject = ['injector', 'connect', 'translate'];
diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomElementFactory.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomElementFactory.js new file mode 100644 index 00000000..01d4d278 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomElementFactory.js @@ -0,0 +1,101 @@ +import { assign } from 'min-dash'; + +import inherits from 'inherits'; + +import BpmnElementFactory from 'bpmn-js/lib/features/modeling/ElementFactory'; +import { DEFAULT_LABEL_SIZE } from 'bpmn-js/lib/util/LabelUtil'; + +/** + * A custom factory that knows how to create BPMN _and_ custom elements. + */ +export default function CustomElementFactory(bpmnFactory, moddle) { + BpmnElementFactory.call(this, bpmnFactory, moddle); + + var self = this; + + /** + * Create a diagram-js element with the given type (any of shape, connection, label). + * + * @param {String} elementType + * @param {Object} attrs + * + * @return {djs.model.Base} + */ + this.create = function(elementType, attrs) { + var type = attrs.type; + + if (elementType === 'label') { + return self.baseCreate( + elementType, + assign({ type: 'label' }, DEFAULT_LABEL_SIZE, attrs) + ); + } + + // add type to businessObject if custom + if (/^custom:/.test(type)) { + if (!attrs.businessObject) { + attrs.businessObject = { + type: type + }; + + if (attrs.id) { + assign(attrs.businessObject, { + id: attrs.id + }); + } + } + + // add width and height if shape + if (!/:connection$/.test(type)) { + assign(attrs, self._getCustomElementSize(type)); + } + + if (!('$instanceOf' in attrs.businessObject)) { + // ensure we can use ModelUtil#is for type checks + Object.defineProperty(attrs.businessObject, '$instanceOf', { + value: function(type) { + return this.type === type; + } + }); + } + + return self.baseCreate(elementType, attrs); + } + + return self.createBpmnElement(elementType, attrs); + }; +} + +inherits(CustomElementFactory, BpmnElementFactory); + +CustomElementFactory.$inject = ['bpmnFactory', 'moddle']; + +/** + * Returns the default size of custom shapes. + * + * The following example shows an interface on how + * to setup the custom shapes's dimensions. + * + * @example + * + * var shapes = { + * triangle: { width: 40, height: 40 }, + * rectangle: { width: 100, height: 20 } + * }; + * + * return shapes[type]; + * + * + * @param {String} type + * + * @return {Dimensions} a {width, height} object representing the size of the element + */ +CustomElementFactory.prototype._getCustomElementSize = function(type) { + var shapes = { + __default: { width: 100, height: 80 }, + 'custom:triangle': { width: 40, height: 40 }, + 'custom:circle': { width: 140, height: 140 } + }; + + return shapes[type] || shapes.__default; +}; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomPalette.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomPalette.js new file mode 100644 index 00000000..a8adb2fd --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomPalette.js @@ -0,0 +1,151 @@ +import { assign } from 'min-dash'; + +/** + * A palette that allows you to create BPMN _and_ custom elements. + */ +export default function PaletteProvider( + palette, + create, + elementFactory, + spaceTool, + lassoTool, + handTool, + globalConnect, + translate +) { + this._create = create; + this._elementFactory = elementFactory; + this._spaceTool = spaceTool; + this._lassoTool = lassoTool; + this._handTool = handTool; + this._globalConnect = globalConnect; + this._translate = translate; + + palette.registerProvider(this); +} + +PaletteProvider.$inject = [ + 'palette', + 'create', + 'elementFactory', + 'spaceTool', + 'lassoTool', + 'handTool', + 'globalConnect', + 'translate' +]; + +PaletteProvider.prototype.getPaletteEntries = function() { + var actions = {}, + create = this._create, + elementFactory = this._elementFactory, + spaceTool = this._spaceTool, + lassoTool = this._lassoTool, + handTool = this._handTool, + globalConnect = this._globalConnect, + translate = this._translate; + + function createAction(type, group, className, title, options) { + function createListener(event) { + var shape = elementFactory.createShape( + assign({ type: type }, options) + ); + + if (options) { + shape.businessObject.di.isExpanded = options.isExpanded; + } + + create.start(event, shape); + } + + var shortType = type.replace(/^bpmn:/, ''); + + return { + group: group, + className: className, + title: title || 'Create ' + shortType, + action: { + dragstart: createListener, + click: createListener + } + }; + } + + assign(actions, { + 'hand-tool': { + group: 'tools', + className: 'bpmn-icon-hand-tool', + title: translate('Activate the hand tool'), + action: { + click: function(event) { + handTool.activateHand(event); + } + } + }, + 'lasso-tool': { + group: 'tools', + className: 'bpmn-icon-lasso-tool', + title: translate('Activate the lasso tool'), + action: { + click: function(event) { + lassoTool.activateSelection(event); + } + } + }, + 'space-tool': { + group: 'tools', + className: 'bpmn-icon-space-tool', + title: translate('Activate the create/remove space tool'), + action: { + click: function(event) { + spaceTool.activateSelection(event); + } + } + }, + 'global-connect-tool': { + group: 'tools', + className: 'bpmn-icon-connection-multi', + title: translate('Activate the global connect tool'), + action: { + click: function(event) { + globalConnect.toggle(event); + } + } + }, + 'tool-separator': { + group: 'tools', + separator: true + }, + 'create.start-event': createAction( + 'bpmn:StartEvent', + 'event', + 'bpmn-icon-start-event-none' + ), + 'create.intermediate-event': createAction( + 'bpmn:IntermediateThrowEvent', + 'event', + 'bpmn-icon-intermediate-event-none', + translate('Create Intermediate/Boundary Event') + ), + 'create.end-event': createAction( + 'bpmn:EndEvent', + 'event', + 'bpmn-icon-end-event-none' + ), + 'create.exclusive-gateway': createAction( + 'bpmn:ExclusiveGateway', + 'gateway', + 'bpmn-icon-gateway-none', + translate('Create Gateway') + ), + 'create.task': createAction('bpmn:Task', 'activity', 'bpmn-icon-task'), + 'create.subprocess-expanded': createAction( + 'bpmn:SubProcess', + 'activity', + 'bpmn-icon-subprocess-expanded', + translate('Create expanded SubProcess'), + { isExpanded: true } + ) + }); + return actions; +}; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomRenderer.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomRenderer.js new file mode 100644 index 00000000..f397fed9 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomRenderer.js @@ -0,0 +1,176 @@ +import inherits from 'inherits'; + +import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer'; + +import { componentsToPath, createLine } from 'diagram-js/lib/util/RenderUtil'; + +import { + append as svgAppend, + attr as svgAttr, + create as svgCreate +} from 'tiny-svg'; + +/** + * A renderer that knows how to render custom elements. + */ +export default function CustomRenderer(eventBus, styles) { + BaseRenderer.call(this, eventBus, 2000); + + var computeStyle = styles.computeStyle; + + this.drawTriangle = function(p, side) { + var halfSide = side / 2, + points, + attrs; + + points = [halfSide, 0, side, side, 0, side]; + + attrs = computeStyle(attrs, { + stroke: '#3CAA82', + strokeWidth: 2, + fill: '#3CAA82' + }); + + var polygon = svgCreate('polygon'); + + svgAttr(polygon, { + points: points + }); + + svgAttr(polygon, attrs); + + svgAppend(p, polygon); + + return polygon; + }; + + this.getTrianglePath = function(element) { + var x = element.x, + y = element.y, + width = element.width, + height = element.height; + + var trianglePath = [ + ['M', x + width / 2, y], + ['l', width / 2, height], + ['l', -width, 0], + ['z'] + ]; + + return componentsToPath(trianglePath); + }; + + this.drawCircle = function(p, width, height) { + var cx = width / 2, + cy = height / 2; + + var attrs = computeStyle(attrs, { + stroke: '#4488aa', + strokeWidth: 4, + fill: 'white' + }); + + var circle = svgCreate('circle'); + + svgAttr(circle, { + cx: cx, + cy: cy, + r: Math.round((width + height) / 4) + }); + + svgAttr(circle, attrs); + + svgAppend(p, circle); + + return circle; + }; + + this.getCirclePath = function(shape) { + var cx = shape.x + shape.width / 2, + cy = shape.y + shape.height / 2, + radius = shape.width / 2; + + var circlePath = [ + ['M', cx, cy], + ['m', 0, -radius], + ['a', radius, radius, 0, 1, 1, 0, 2 * radius], + ['a', radius, radius, 0, 1, 1, 0, -2 * radius], + ['z'] + ]; + + return componentsToPath(circlePath); + }; + + this.drawCustomConnection = function(p, element) { + var attrs = computeStyle(attrs, { + stroke: '#ff471a', + strokeWidth: 2 + }); + + return svgAppend(p, createLine(element.waypoints, attrs)); + }; + + this.getCustomConnectionPath = function(connection) { + var waypoints = connection.waypoints.map(function(p) { + return p.original || p; + }); + + var connectionPath = [['M', waypoints[0].x, waypoints[0].y]]; + + waypoints.forEach(function(waypoint, index) { + if (index !== 0) { + connectionPath.push(['L', waypoint.x, waypoint.y]); + } + }); + + return componentsToPath(connectionPath); + }; +} + +inherits(CustomRenderer, BaseRenderer); + +CustomRenderer.$inject = ['eventBus', 'styles']; + +CustomRenderer.prototype.canRender = function(element) { + return /^custom:/.test(element.type); +}; + +CustomRenderer.prototype.drawShape = function(p, element) { + var type = element.type; + + if (type === 'custom:triangle') { + return this.drawTriangle(p, element.width); + } + + if (type === 'custom:circle') { + return this.drawCircle(p, element.width, element.height); + } +}; + +CustomRenderer.prototype.getShapePath = function(shape) { + var type = shape.type; + + if (type === 'custom:triangle') { + return this.getTrianglePath(shape); + } + + if (type === 'custom:circle') { + return this.getCirclePath(shape); + } +}; + +CustomRenderer.prototype.drawConnection = function(p, element) { + var type = element.type; + + if (type === 'custom:connection') { + return this.drawCustomConnection(p, element); + } +}; + +CustomRenderer.prototype.getConnectionPath = function(connection) { + var type = connection.type; + + if (type === 'custom:connection') { + return this.getCustomConnectionPath(connection); + } +}; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomRules.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomRules.js new file mode 100644 index 00000000..1dce143d --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomRules.js @@ -0,0 +1,136 @@ +import { reduce } from 'min-dash'; + +import inherits from 'inherits'; + +import { is } from 'bpmn-js/lib/util/ModelUtil'; + +import RuleProvider from 'diagram-js/lib/features/rules/RuleProvider'; + +var HIGH_PRIORITY = 1500; + +function isCustom(element) { + return element && /^custom:/.test(element.type); +} + +/** + * Specific rules for custom elements + */ +export default function CustomRules(eventBus) { + RuleProvider.call(this, eventBus); +} + +inherits(CustomRules, RuleProvider); + +CustomRules.$inject = ['eventBus']; + +CustomRules.prototype.init = function() { + /** + * Can shape be created on target container? + */ + function canCreate(shape, target) { + // only judge about custom elements + if (!isCustom(shape)) { + return; + } + + // allow creation on processes + return ( + is(target, 'bpmn:Process') || + is(target, 'bpmn:Participant') || + is(target, 'bpmn:Collaboration') + ); + } + + /** + * Can source and target be connected? + */ + function canConnect(source, target) { + // only judge about custom elements + if (!isCustom(source) && !isCustom(target)) { + return; + } + + // allow connection between custom shape and task + if (isCustom(source)) { + if (is(target, 'bpmn:Task')) { + return { type: 'custom:connection' }; + } else { + return false; + } + } else if (isCustom(target)) { + if (is(source, 'bpmn:Task')) { + return { type: 'custom:connection' }; + } else { + return false; + } + } + } + + this.addRule('elements.move', HIGH_PRIORITY, function(context) { + var target = context.target, + shapes = context.shapes; + + var type; + + // do not allow mixed movements of custom / BPMN shapes + // if any shape cannot be moved, the group cannot be moved, too + var allowed = reduce( + shapes, + function(result, s) { + if (type === undefined) { + type = isCustom(s); + } + + if (type !== isCustom(s) || result === false) { + return false; + } + + return canCreate(s, target); + }, + undefined + ); + + // reject, if we have at least one + // custom element that cannot be moved + return allowed; + }); + + this.addRule('shape.create', HIGH_PRIORITY, function(context) { + var target = context.target, + shape = context.shape; + + return canCreate(shape, target); + }); + + this.addRule('shape.resize', HIGH_PRIORITY, function(context) { + var shape = context.shape; + + if (isCustom(shape)) { + // cannot resize custom elements + return false; + } + }); + + this.addRule('connection.create', HIGH_PRIORITY, function(context) { + var source = context.source, + target = context.target; + + return canConnect(source, target); + }); + + this.addRule('connection.reconnectStart', HIGH_PRIORITY, function(context) { + var connection = context.connection, + source = context.hover || context.source, + target = connection.target; + + return canConnect(source, target, connection); + }); + + this.addRule('connection.reconnectEnd', HIGH_PRIORITY, function(context) { + var connection = context.connection, + source = connection.source, + target = context.hover || context.target; + + return canConnect(source, target, connection); + }); +}; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomUpdater.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomUpdater.js new file mode 100644 index 00000000..532c24f3 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomUpdater.js @@ -0,0 +1,136 @@ +import inherits from 'inherits'; + +import { pick, assign } from 'min-dash'; + +import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor'; + +import { + add as collectionAdd, + remove as collectionRemove +} from 'diagram-js/lib/util/Collections'; + +/** + * A handler responsible for updating the custom element's businessObject + * once changes on the diagram happen. + */ +export default function CustomUpdater(eventBus, bpmnjs) { + CommandInterceptor.call(this, eventBus); + + function updateCustomElement(e) { + var context = e.context, + shape = context.shape, + businessObject = shape.businessObject; + + if (!isCustom(shape)) { + return; + } + + var parent = shape.parent; + + var customElements = bpmnjs._customElements; + + // make sure element is added / removed from bpmnjs.customElements + if (!parent) { + collectionRemove(customElements, businessObject); + } else { + collectionAdd(customElements, businessObject); + } + + // save custom element position + assign(businessObject, pick(shape, ['x', 'y'])); + } + + function updateCustomConnection(e) { + var context = e.context, + connection = context.connection, + source = connection.source, + target = connection.target, + businessObject = connection.businessObject; + + var parent = connection.parent; + + var customElements = bpmnjs._customElements; + + // make sure element is added / removed from bpmnjs.customElements + if (!parent) { + collectionRemove(customElements, businessObject); + } else { + collectionAdd(customElements, businessObject); + } + + // update waypoints + assign(businessObject, { + waypoints: copyWaypoints(connection) + }); + + if (source && target) { + assign(businessObject, { + source: source.id, + target: target.id + }); + } + } + + this.executed( + ['shape.create', 'shape.move', 'shape.delete'], + ifCustomElement(updateCustomElement) + ); + + this.reverted( + ['shape.create', 'shape.move', 'shape.delete'], + ifCustomElement(updateCustomElement) + ); + + this.executed( + [ + 'connection.create', + 'connection.reconnectStart', + 'connection.reconnectEnd', + 'connection.updateWaypoints', + 'connection.delete', + 'connection.layout', + 'connection.move' + ], + ifCustomElement(updateCustomConnection) + ); + + this.reverted( + [ + 'connection.create', + 'connection.reconnectStart', + 'connection.reconnectEnd', + 'connection.updateWaypoints', + 'connection.delete', + 'connection.layout', + 'connection.move' + ], + ifCustomElement(updateCustomConnection) + ); +} + +inherits(CustomUpdater, CommandInterceptor); + +CustomUpdater.$inject = ['eventBus', 'bpmnjs']; + +/////// helpers /////////////////////////////////// + +function copyWaypoints(connection) { + return connection.waypoints.map(function(p) { + return { x: p.x, y: p.y }; + }); +} + +function isCustom(element) { + return element && /custom:/.test(element.type); +} + +function ifCustomElement(fn) { + return function(event) { + var context = event.context, + element = context.shape || context.connection; + + if (isCustom(element)) { + fn(event); + } + }; +} diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/index.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/index.js new file mode 100644 index 00000000..f1085390 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/index.js @@ -0,0 +1,22 @@ +import CustomElementFactory from './CustomElementFactory'; +import CustomRenderer from './CustomRenderer'; +import CustomPalette from './CustomPalette'; +import CustomRules from './CustomRules'; +import CustomUpdater from './CustomUpdater'; +import CustomContextPadProvider from './CustomContextPadProvider'; + +export default { + __init__: [ + 'customRenderer', + 'paletteProvider', + 'customRules', + 'customUpdater', + 'contextPadProvider' + ], + elementFactory: ['type', CustomElementFactory], + customRenderer: ['type', CustomRenderer], + paletteProvider: ['type', CustomPalette], + customRules: ['type', CustomRules], + customUpdater: ['type', CustomUpdater], + contextPadProvider: ['type', CustomContextPadProvider] +}; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/index.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/index.js new file mode 100644 index 00000000..86fbff6a --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/index.js @@ -0,0 +1,99 @@ +import Modeler from 'bpmn-js/lib/Modeler'; + +import { assign, isArray } from 'min-dash'; + +import inherits from 'inherits'; + +import CustomModule from './custom'; + +export default function CustomModeler(options) { + Modeler.call(this, options); + + this._customElements = []; +} + +inherits(CustomModeler, Modeler); + +CustomModeler.prototype._modules = [].concat(CustomModeler.prototype._modules, [ + CustomModule +]); + +/** + * Add a single custom element to the underlying diagram + * + * @param {Object} customElement + */ +CustomModeler.prototype._addCustomShape = function(customElement) { + this._customElements.push(customElement); + + var canvas = this.get('canvas'), + elementFactory = this.get('elementFactory'); + + var customAttrs = assign({ businessObject: customElement }, customElement); + + var customShape = elementFactory.create('shape', customAttrs); + + return canvas.addShape(customShape); +}; + +CustomModeler.prototype._addCustomConnection = function(customElement) { + this._customElements.push(customElement); + + var canvas = this.get('canvas'), + elementFactory = this.get('elementFactory'), + elementRegistry = this.get('elementRegistry'); + + var customAttrs = assign({ businessObject: customElement }, customElement); + + var connection = elementFactory.create( + 'connection', + assign(customAttrs, { + source: elementRegistry.get(customElement.source), + target: elementRegistry.get(customElement.target) + }), + elementRegistry.get(customElement.source).parent + ); + + return canvas.addConnection(connection); +}; + +/** + * Add a number of custom elements and connections to the underlying diagram. + * + * @param {Array<Object>} customElements + */ +CustomModeler.prototype.addCustomElements = function(customElements) { + if (!isArray(customElements)) { + throw new Error('argument must be an array'); + } + + var shapes = [], + connections = []; + + customElements.forEach(function(customElement) { + if (isCustomConnection(customElement)) { + connections.push(customElement); + } else { + shapes.push(customElement); + } + }); + + // add shapes before connections so that connections + // can already rely on the shapes being part of the diagram + shapes.forEach(this._addCustomShape, this); + + connections.forEach(this._addCustomConnection, this); +}; + +/** + * Get custom elements with their current status. + * + * @return {Array<Object>} custom elements on the diagram + */ +CustomModeler.prototype.getCustomElements = function() { + return this._customElements; +}; + +function isCustomConnection(element) { + return element.type === 'custom:connection'; +} diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/descriptors/camunda.json b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/descriptors/camunda.json new file mode 100644 index 00000000..6613c4bf --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/descriptors/camunda.json @@ -0,0 +1,1025 @@ +{ + "name": "Camunda", + "uri": "http://camunda.org/schema/1.0/bpmn", + "prefix": "camunda", + "xml": { + "tagAlias": "lowerCase" + }, + "associations": [], + "types": [ + { + "name": "InOutBinding", + "superClass": ["Element"], + "isAbstract": true, + "properties": [ + { + "name": "source", + "isAttr": true, + "type": "String" + }, + { + "name": "sourceExpression", + "isAttr": true, + "type": "String" + }, + { + "name": "target", + "isAttr": true, + "type": "String" + }, + { + "name": "businessKey", + "isAttr": true, + "type": "String" + }, + { + "name": "local", + "isAttr": true, + "type": "Boolean", + "default": false + }, + { + "name": "variables", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "In", + "superClass": ["InOutBinding"], + "meta": { + "allowedIn": ["bpmn:CallActivity"] + } + }, + { + "name": "Out", + "superClass": ["InOutBinding"], + "meta": { + "allowedIn": ["bpmn:CallActivity"] + } + }, + { + "name": "AsyncCapable", + "isAbstract": true, + "extends": ["bpmn:Activity", "bpmn:Gateway", "bpmn:Event"], + "properties": [ + { + "name": "async", + "isAttr": true, + "type": "Boolean", + "default": false + }, + { + "name": "asyncBefore", + "isAttr": true, + "type": "Boolean", + "default": false + }, + { + "name": "asyncAfter", + "isAttr": true, + "type": "Boolean", + "default": false + }, + { + "name": "exclusive", + "isAttr": true, + "type": "Boolean", + "default": true + } + ] + }, + { + "name": "JobPriorized", + "isAbstract": true, + "extends": ["bpmn:Process", "camunda:AsyncCapable"], + "properties": [ + { + "name": "jobPriority", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "SignalEventDefinition", + "isAbstract": true, + "extends": ["bpmn:SignalEventDefinition"], + "properties": [ + { + "name": "async", + "isAttr": true, + "type": "Boolean", + "default": false + } + ] + }, + { + "name": "ErrorEventDefinition", + "isAbstract": true, + "extends": ["bpmn:ErrorEventDefinition"], + "properties": [ + { + "name": "errorCodeVariable", + "isAttr": true, + "type": "String" + }, + { + "name": "errorMessageVariable", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "PotentialStarter", + "superClass": ["Element"], + "properties": [ + { + "name": "resourceAssignmentExpression", + "type": "bpmn:ResourceAssignmentExpression" + } + ] + }, + { + "name": "FormSupported", + "isAbstract": true, + "extends": ["bpmn:StartEvent", "bpmn:UserTask"], + "properties": [ + { + "name": "formHandlerClass", + "isAttr": true, + "type": "String" + }, + { + "name": "formKey", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "TemplateSupported", + "isAbstract": true, + "extends": ["bpmn:Process", "bpmn:FlowElement"], + "properties": [ + { + "name": "modelerTemplate", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "Initiator", + "isAbstract": true, + "extends": ["bpmn:StartEvent"], + "properties": [ + { + "name": "initiator", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "ScriptTask", + "isAbstract": true, + "extends": ["bpmn:ScriptTask"], + "properties": [ + { + "name": "resultVariable", + "isAttr": true, + "type": "String" + }, + { + "name": "resource", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "Process", + "isAbstract": true, + "extends": ["bpmn:Process"], + "properties": [ + { + "name": "candidateStarterGroups", + "isAttr": true, + "type": "String" + }, + { + "name": "candidateStarterUsers", + "isAttr": true, + "type": "String" + }, + { + "name": "versionTag", + "isAttr": true, + "type": "String" + }, + { + "name": "historyTimeToLive", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "EscalationEventDefinition", + "isAbstract": true, + "extends": ["bpmn:EscalationEventDefinition"], + "properties": [ + { + "name": "escalationCodeVariable", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "FormalExpression", + "isAbstract": true, + "extends": ["bpmn:FormalExpression"], + "properties": [ + { + "name": "resource", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "Assignable", + "extends": ["bpmn:UserTask"], + "properties": [ + { + "name": "assignee", + "isAttr": true, + "type": "String" + }, + { + "name": "candidateUsers", + "isAttr": true, + "type": "String" + }, + { + "name": "candidateGroups", + "isAttr": true, + "type": "String" + }, + { + "name": "dueDate", + "isAttr": true, + "type": "String" + }, + { + "name": "followUpDate", + "isAttr": true, + "type": "String" + }, + { + "name": "priority", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "CallActivity", + "extends": ["bpmn:CallActivity"], + "properties": [ + { + "name": "calledElementBinding", + "isAttr": true, + "type": "String", + "default": "latest" + }, + { + "name": "calledElementVersion", + "isAttr": true, + "type": "String" + }, + { + "name": "calledElementTenantId", + "isAttr": true, + "type": "String" + }, + { + "name": "caseRef", + "isAttr": true, + "type": "String" + }, + { + "name": "caseBinding", + "isAttr": true, + "type": "String", + "default": "latest" + }, + { + "name": "caseVersion", + "isAttr": true, + "type": "String" + }, + { + "name": "caseTenantId", + "isAttr": true, + "type": "String" + }, + { + "name": "variableMappingClass", + "isAttr": true, + "type": "String" + }, + { + "name": "variableMappingDelegateExpression", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "ServiceTaskLike", + "extends": [ + "bpmn:ServiceTask", + "bpmn:BusinessRuleTask", + "bpmn:SendTask", + "bpmn:MessageEventDefinition" + ], + "properties": [ + { + "name": "expression", + "isAttr": true, + "type": "String" + }, + { + "name": "class", + "isAttr": true, + "type": "String" + }, + { + "name": "delegateExpression", + "isAttr": true, + "type": "String" + }, + { + "name": "resultVariable", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "DmnCapable", + "extends": ["bpmn:BusinessRuleTask"], + "properties": [ + { + "name": "decisionRef", + "isAttr": true, + "type": "String" + }, + { + "name": "decisionRefBinding", + "isAttr": true, + "type": "String", + "default": "latest" + }, + { + "name": "decisionRefVersion", + "isAttr": true, + "type": "String" + }, + { + "name": "mapDecisionResult", + "isAttr": true, + "type": "String", + "default": "resultList" + }, + { + "name": "decisionRefTenantId", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "ExternalCapable", + "extends": ["camunda:ServiceTaskLike"], + "properties": [ + { + "name": "type", + "isAttr": true, + "type": "String" + }, + { + "name": "topic", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "TaskPriorized", + "extends": ["bpmn:Process", "camunda:ExternalCapable"], + "properties": [ + { + "name": "taskPriority", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "Properties", + "superClass": ["Element"], + "meta": { + "allowedIn": ["*"] + }, + "properties": [ + { + "name": "values", + "type": "Property", + "isMany": true + } + ] + }, + { + "name": "Property", + "superClass": ["Element"], + "properties": [ + { + "name": "id", + "type": "String", + "isAttr": true + }, + { + "name": "name", + "type": "String", + "isAttr": true + }, + { + "name": "value", + "type": "String", + "isAttr": true + } + ] + }, + { + "name": "Connector", + "superClass": ["Element"], + "meta": { + "allowedIn": [ + "bpmn:ServiceTask", + "bpmn:BusinessRuleTask", + "bpmn:SendTask" + ] + }, + "properties": [ + { + "name": "inputOutput", + "type": "InputOutput" + }, + { + "name": "connectorId", + "type": "String" + } + ] + }, + { + "name": "WorkflowActivity", + "superClass": ["Element"], + "meta": { + "allowedIn": [ + "bpmn:ServiceTask", + "bpmn:BusinessRuleTask", + "bpmn:SendTask" + ] + }, + "properties": [ + { + "name": "activityId", + "type": "String" + } + ] + }, + { + "name": "InputOutput", + "superClass": ["Element"], + "meta": { + "allowedIn": [ + "bpmn:Task", + "bpmn:UserTask", + "bpmn:ServiceTask", + "bpmn:SendTask", + "bpmn:BusinessRuleTask", + "bpmn:ReceiveTask", + "bpmn:ScriptTask", + "bpmn:ManualTask", + "bpmn:GlobalUserTask", + "bpmn:GlobalScriptTask", + "bpmn:GlobalBusinessRuleTask", + "bpmn:GlobalTask", + "bpmn:GlobalManualTask", + "bpmn:SubProcess", + "bpmn:Transaction", + "bpmn:IntermediateCatchEvent", + "bpmn:IntermediateThrowEvent", + "bpmn:EndEvent", + "bpmn:ThrowEvent", + "bpmn:CatchEvent", + "bpmn:ImplicitThrowEvent", + "bpmn:CallActivity" + ] + }, + "properties": [ + { + "name": "inputOutput", + "type": "InputOutput" + }, + { + "name": "connectorId", + "type": "String" + }, + { + "name": "inputParameters", + "isMany": true, + "type": "InputParameter" + }, + { + "name": "outputParameters", + "isMany": true, + "type": "OutputParameter" + } + ] + }, + { + "name": "InputOutputParameter", + "properties": [ + { + "name": "name", + "isAttr": true, + "type": "String" + }, + { + "name": "workflowSource", + "isAttr": true, + "type": "String" + }, + { + "name": "workflowTarget", + "isAttr": true, + "type": "String" + }, + { + "name": "value", + "isBody": true, + "type": "String" + }, + { + "name": "definition", + "type": "InputOutputParameterDefinition" + } + ] + }, + { + "name": "InputOutputParameterDefinition", + "isAbstract": true + }, + { + "name": "List", + "superClass": ["InputOutputParameterDefinition"], + "properties": [ + { + "name": "items", + "isMany": true, + "type": "InputOutputParameterDefinition" + } + ] + }, + { + "name": "Map", + "superClass": ["InputOutputParameterDefinition"], + "properties": [ + { + "name": "entries", + "isMany": true, + "type": "Entry" + } + ] + }, + { + "name": "Entry", + "properties": [ + { + "name": "key", + "isAttr": true, + "type": "String" + }, + { + "name": "value", + "isBody": true, + "type": "String" + }, + { + "name": "definition", + "type": "InputOutputParameterDefinition" + } + ] + }, + { + "name": "Value", + "superClass": ["InputOutputParameterDefinition"], + "properties": [ + { + "name": "id", + "isAttr": true, + "type": "String" + }, + { + "name": "name", + "isAttr": true, + "type": "String" + }, + { + "name": "value", + "isBody": true, + "type": "String" + } + ] + }, + { + "name": "Script", + "superClass": ["InputOutputParameterDefinition"], + "properties": [ + { + "name": "scriptFormat", + "isAttr": true, + "type": "String" + }, + { + "name": "resource", + "isAttr": true, + "type": "String" + }, + { + "name": "value", + "isBody": true, + "type": "String" + } + ] + }, + { + "name": "Field", + "superClass": ["Element"], + "meta": { + "allowedIn": [ + "bpmn:ServiceTask", + "bpmn:BusinessRuleTask", + "bpmn:SendTask" + ] + }, + "properties": [ + { + "name": "name", + "isAttr": true, + "type": "String" + }, + { + "name": "expression", + "type": "String" + }, + { + "name": "stringValue", + "isAttr": true, + "type": "String" + }, + { + "name": "string", + "type": "String" + } + ] + }, + { + "name": "InputParameter", + "superClass": ["InputOutputParameter"] + }, + { + "name": "OutputParameter", + "superClass": ["InputOutputParameter"] + }, + { + "name": "Collectable", + "isAbstract": true, + "extends": ["bpmn:MultiInstanceLoopCharacteristics"], + "superClass": ["camunda:AsyncCapable"], + "properties": [ + { + "name": "collection", + "isAttr": true, + "type": "String" + }, + { + "name": "elementVariable", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "FailedJobRetryTimeCycle", + "superClass": ["Element"], + "meta": { + "allowedIn": [ + "bpmn:Task", + "bpmn:ServiceTask", + "bpmn:SendTask", + "bpmn:UserTask", + "bpmn:BusinessRuleTask", + "bpmn:ScriptTask", + "bpmn:ReceiveTask", + "bpmn:CallActivity", + "bpmn:TimerEventDefinition", + "bpmn:SignalEventDefinition", + "bpmn:MultiInstanceLoopCharacteristics" + ] + }, + "properties": [ + { + "name": "body", + "isBody": true, + "type": "String" + } + ] + }, + { + "name": "ExecutionListener", + "superClass": ["Element"], + "meta": { + "allowedIn": [ + "bpmn:Task", + "bpmn:ServiceTask", + "bpmn:UserTask", + "bpmn:BusinessRuleTask", + "bpmn:ScriptTask", + "bpmn:ReceiveTask", + "bpmn:ManualTask", + "bpmn:ExclusiveGateway", + "bpmn:SequenceFlow", + "bpmn:ParallelGateway", + "bpmn:InclusiveGateway", + "bpmn:EventBasedGateway", + "bpmn:StartEvent", + "bpmn:IntermediateCatchEvent", + "bpmn:IntermediateThrowEvent", + "bpmn:EndEvent", + "bpmn:BoundaryEvent", + "bpmn:CallActivity", + "bpmn:SubProcess" + ] + }, + "properties": [ + { + "name": "expression", + "isAttr": true, + "type": "String" + }, + { + "name": "class", + "isAttr": true, + "type": "String" + }, + { + "name": "delegateExpression", + "isAttr": true, + "type": "String" + }, + { + "name": "event", + "isAttr": true, + "type": "String" + }, + { + "name": "script", + "type": "Script" + }, + { + "name": "fields", + "type": "Field", + "isMany": true + } + ] + }, + { + "name": "TaskListener", + "superClass": ["Element"], + "meta": { + "allowedIn": ["bpmn:UserTask"] + }, + "properties": [ + { + "name": "expression", + "isAttr": true, + "type": "String" + }, + { + "name": "class", + "isAttr": true, + "type": "String" + }, + { + "name": "delegateExpression", + "isAttr": true, + "type": "String" + }, + { + "name": "event", + "isAttr": true, + "type": "String" + }, + { + "name": "script", + "type": "Script" + }, + { + "name": "fields", + "type": "Field", + "isMany": true + } + ] + }, + { + "name": "FormProperty", + "superClass": ["Element"], + "meta": { + "allowedIn": ["bpmn:StartEvent", "bpmn:UserTask"] + }, + "properties": [ + { + "name": "id", + "type": "String", + "isAttr": true + }, + { + "name": "name", + "type": "String", + "isAttr": true + }, + { + "name": "type", + "type": "String", + "isAttr": true + }, + { + "name": "required", + "type": "String", + "isAttr": true + }, + { + "name": "readable", + "type": "String", + "isAttr": true + }, + { + "name": "writable", + "type": "String", + "isAttr": true + }, + { + "name": "variable", + "type": "String", + "isAttr": true + }, + { + "name": "expression", + "type": "String", + "isAttr": true + }, + { + "name": "datePattern", + "type": "String", + "isAttr": true + }, + { + "name": "default", + "type": "String", + "isAttr": true + }, + { + "name": "values", + "type": "Value", + "isMany": true + } + ] + }, + { + "name": "FormData", + "superClass": ["Element"], + "meta": { + "allowedIn": ["bpmn:StartEvent", "bpmn:UserTask"] + }, + "properties": [ + { + "name": "fields", + "type": "FormField", + "isMany": true + }, + { + "name": "businessKey", + "type": "String", + "isAttr": true + } + ] + }, + { + "name": "FormField", + "superClass": ["Element"], + "properties": [ + { + "name": "id", + "type": "String", + "isAttr": true + }, + { + "name": "label", + "type": "String", + "isAttr": true + }, + { + "name": "type", + "type": "String", + "isAttr": true + }, + { + "name": "datePattern", + "type": "String", + "isAttr": true + }, + { + "name": "defaultValue", + "type": "String", + "isAttr": true + }, + { + "name": "properties", + "type": "Properties" + }, + { + "name": "validation", + "type": "Validation" + }, + { + "name": "values", + "type": "Value", + "isMany": true + } + ] + }, + { + "name": "Validation", + "superClass": ["Element"], + "properties": [ + { + "name": "constraints", + "type": "Constraint", + "isMany": true + } + ] + }, + { + "name": "Constraint", + "superClass": ["Element"], + "properties": [ + { + "name": "name", + "type": "String", + "isAttr": true + }, + { + "name": "config", + "type": "String", + "isAttr": true + } + ] + }, + { + "name": "ConditionalEventDefinition", + "isAbstract": true, + "extends": ["bpmn:ConditionalEventDefinition"], + "properties": [ + { + "name": "variableName", + "isAttr": true, + "type": "String" + }, + { + "name": "variableEvent", + "isAttr": true, + "type": "String" + } + ] + } + ], + "emumerations": [] +} diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/WorkflowPropertiesProvider.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/WorkflowPropertiesProvider.js new file mode 100644 index 00000000..d6b3b274 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/WorkflowPropertiesProvider.js @@ -0,0 +1,300 @@ +import inherits from 'inherits'; +import CamundaPropertiesProvider from 'bpmn-js-properties-panel/lib/provider/camunda/CamundaPropertiesProvider'; + +import { is, getBusinessObject } from 'bpmn-js/lib/util/ModelUtil'; + +import asyncCapableHelper from 'bpmn-js-properties-panel/lib/helper/AsyncCapableHelper'; +import eventDefinitionHelper from 'bpmn-js-properties-panel/lib/helper/EventDefinitionHelper'; +import implementationTypeHelper from 'bpmn-js-properties-panel/lib/helper/ImplementationTypeHelper'; + +import idProps from 'bpmn-js-properties-panel/lib/provider/bpmn/parts/IdProps'; +import nameProps from 'bpmn-js-properties-panel/lib/provider/bpmn/parts/NameProps'; +import processProps from 'bpmn-js-properties-panel/lib/provider/bpmn/parts/ProcessProps'; +import linkProps from 'bpmn-js-properties-panel/lib/provider/bpmn/parts/LinkProps'; +import eventProps from 'bpmn-js-properties-panel/lib/provider/bpmn/parts/EventProps'; +import documentationProps from 'bpmn-js-properties-panel/lib/provider/bpmn/parts/DocumentationProps'; + +import elementTemplateChooserProps from 'bpmn-js-properties-panel/lib/provider/camunda/element-templates/parts/ChooserProps'; +import elementTemplateCustomProps from 'bpmn-js-properties-panel/lib/provider/camunda/element-templates/parts/CustomProps'; + +import versionTag from 'bpmn-js-properties-panel/lib/provider/camunda/parts/VersionTagProps'; +import userTaskProps from 'bpmn-js-properties-panel/lib/provider/camunda/parts/UserTaskProps'; +import scriptProps from 'bpmn-js-properties-panel/lib/provider/camunda/parts/ScriptTaskProps'; +import callActivityProps from 'bpmn-js-properties-panel/lib/provider/camunda/parts/CallActivityProps'; +import conditionalProps from 'bpmn-js-properties-panel/lib/provider/camunda/parts/ConditionalProps'; +import startEventInitiator from 'bpmn-js-properties-panel/lib/provider/camunda/parts/StartEventInitiator'; +import multiInstanceProps from 'bpmn-js-properties-panel/lib/provider/camunda/parts/MultiInstanceLoopProps'; +import asynchronousContinuationProps from 'bpmn-js-properties-panel/lib/provider/camunda/parts/AsynchronousContinuationProps'; +import jobConfiguration from 'bpmn-js-properties-panel/lib/provider/camunda/parts/JobConfigurationProps'; +import externalTaskConfiguration from 'bpmn-js-properties-panel/lib/provider/camunda/parts/ExternalTaskConfigurationProps'; +import candidateStarter from 'bpmn-js-properties-panel/lib/provider/camunda/parts/CandidateStarterProps'; +import historyTimeToLive from 'bpmn-js-properties-panel/lib/provider/camunda/parts/HistoryTimeToLiveProps'; + +import createInputOutputTabGroups from './parts/createInputOutputTabGroups'; +import workflowServiceTaskDelegateProps from './parts/WorkflowServiceTaskDelegateProps'; + +const PROCESS_KEY_HINT = 'This maps to the process definition key.'; + +const isExternalTaskPriorityEnabled = function(element) { + const businessObject = getBusinessObject(element); + + // show only if element is a process, a participant ... + if ( + is(element, 'bpmn:Process') || + (is(element, 'bpmn:Participant') && businessObject.get('processRef')) + ) { + return true; + } + + const externalBo = implementationTypeHelper.getServiceTaskLikeBusinessObject( + element + ), + isExternalTask = + implementationTypeHelper.getImplementationType(externalBo) === + 'external'; + + // ... or an external task with selected external implementation type + return ( + !!implementationTypeHelper.isExternalCapable(externalBo) && + isExternalTask + ); +}; + +const isJobConfigEnabled = element => { + const businessObject = getBusinessObject(element); + + if ( + is(element, 'bpmn:Process') || + (is(element, 'bpmn:Participant') && businessObject.get('processRef')) + ) { + return true; + } + + // async behavior + const bo = getBusinessObject(element); + if ( + asyncCapableHelper.isAsyncBefore(bo) || + asyncCapableHelper.isAsyncAfter(bo) + ) { + return true; + } + + // timer definition + if (is(element, 'bpmn:Event')) { + return !!eventDefinitionHelper.getTimerEventDefinition(element); + } + + return false; +}; + +function createGeneralTabGroups( + element, + config, + bpmnFactory, + elementRegistry, + elementTemplates, + translate +) { + // refer to target element for external labels + element = element.labelTarget || element; + + const generalGroup = { + id: 'general', + label: translate('General'), + entries: [] + }; + + let idOptions; + let processOptions; + + if (is(element, 'bpmn:Process')) { + idOptions = { description: PROCESS_KEY_HINT }; + } + + if (is(element, 'bpmn:Participant')) { + processOptions = { processIdDescription: PROCESS_KEY_HINT }; + } + + idProps(generalGroup, element, translate, idOptions); + nameProps(generalGroup, element, translate); + processProps(generalGroup, element, translate, processOptions); + versionTag(generalGroup, element, translate); + elementTemplateChooserProps( + generalGroup, + element, + elementTemplates, + translate + ); + + const customFieldsGroups = elementTemplateCustomProps( + element, + elementTemplates, + bpmnFactory, + translate + ); + + const detailsGroup = { + id: 'details', + label: translate('Details'), + entries: [] + }; + workflowServiceTaskDelegateProps( + detailsGroup, + element, + config, + bpmnFactory, + translate + ); + userTaskProps(detailsGroup, element, translate); + scriptProps(detailsGroup, element, bpmnFactory, translate); + linkProps(detailsGroup, element, translate); + callActivityProps(detailsGroup, element, bpmnFactory, translate); + eventProps(detailsGroup, element, bpmnFactory, elementRegistry, translate); + conditionalProps(detailsGroup, element, bpmnFactory, translate); + startEventInitiator(detailsGroup, element, translate); // this must be the last element of the details group! + + const multiInstanceGroup = { + id: 'multiInstance', + label: translate('Multi Instance'), + entries: [] + }; + multiInstanceProps(multiInstanceGroup, element, bpmnFactory, translate); + + const asyncGroup = { + id: 'async', + label: translate('Asynchronous Continuations'), + entries: [] + }; + asynchronousContinuationProps(asyncGroup, element, bpmnFactory, translate); + + const jobConfigurationGroup = { + id: 'jobConfiguration', + label: translate('Job Configuration'), + entries: [], + enabled: isJobConfigEnabled + }; + jobConfiguration(jobConfigurationGroup, element, bpmnFactory, translate); + + const externalTaskGroup = { + id: 'externalTaskConfiguration', + label: translate('External Task Configuration'), + entries: [], + enabled: isExternalTaskPriorityEnabled + }; + externalTaskConfiguration( + externalTaskGroup, + element, + bpmnFactory, + translate + ); + + const candidateStarterGroup = { + id: 'candidateStarterConfiguration', + label: translate('Candidate Starter Configuration'), + entries: [] + }; + candidateStarter(candidateStarterGroup, element, bpmnFactory, translate); + + const historyTimeToLiveGroup = { + id: 'historyConfiguration', + label: translate('History Configuration'), + entries: [] + }; + historyTimeToLive(historyTimeToLiveGroup, element, bpmnFactory, translate); + + const documentationGroup = { + id: 'documentation', + label: translate('Documentation'), + entries: [] + }; + documentationProps(documentationGroup, element, bpmnFactory, translate); + + const groups = []; + groups.push(generalGroup); + customFieldsGroups.forEach(function(group) { + groups.push(group); + }); + groups.push(detailsGroup); + groups.push(externalTaskGroup); + groups.push(multiInstanceGroup); + groups.push(asyncGroup); + groups.push(jobConfigurationGroup); + groups.push(candidateStarterGroup); + groups.push(historyTimeToLiveGroup); + groups.push(documentationGroup); + + return groups; +} + +function WorkflowPropertiesProvider( + config, + eventBus, + bpmnFactory, + elementRegistry, + elementTemplates, + translate +) { + CamundaPropertiesProvider.call( + this, + eventBus, + bpmnFactory, + elementRegistry, + elementTemplates, + translate + ); + + this.getTabs = function(element) { + const camundaPropertiesProvider = new CamundaPropertiesProvider( + eventBus, + bpmnFactory, + elementRegistry, + elementTemplates, + translate + ); + + const tabs = camundaPropertiesProvider + .getTabs(element) + .filter(tab => tab.id !== 'general' && tab.id !== 'input-output'); + + const generalTab = { + id: 'general', + label: translate('General'), + groups: createGeneralTabGroups( + element, + config, + bpmnFactory, + elementRegistry, + elementTemplates, + translate + ) + }; + + const inputOutputTab = { + id: 'input-output', + label: translate('Input/Output'), + groups: createInputOutputTabGroups( + element, + bpmnFactory, + elementRegistry, + translate, + config + ) + }; + tabs.unshift(inputOutputTab); + tabs.unshift(generalTab); + return tabs; + }; +} + +WorkflowPropertiesProvider.$inject = [ + 'config.workflow', + 'eventBus', + 'bpmnFactory', + 'elementRegistry', + 'elementTemplates', + 'translate' +]; + +inherits(WorkflowPropertiesProvider, CamundaPropertiesProvider); + +export default WorkflowPropertiesProvider; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/index.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/index.js new file mode 100644 index 00000000..bc60c581 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/index.js @@ -0,0 +1,10 @@ +import ElementTemplates from 'bpmn-js-properties-panel/lib/provider/camunda/element-templates'; +import Translate from 'diagram-js/lib/i18n/translate'; + +import WorkflowPropertiesProvider from './WorkflowPropertiesProvider'; + +export default { + __depends__: [ElementTemplates, Translate], + __init__: ['propertiesProvider'], + propertiesProvider: ['type', WorkflowPropertiesProvider] +}; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/InputOutputParameterProps.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/InputOutputParameterProps.js new file mode 100644 index 00000000..d221c6e3 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/InputOutputParameterProps.js @@ -0,0 +1,21 @@ +import inputOutputParameter from './implementation/InputOutputParameter'; +import assign from 'lodash.assign'; + +export default function( + group, + element, + bpmnFactory, + options, + translate, + config +) { + group.entries = group.entries.concat( + inputOutputParameter( + element, + bpmnFactory, + assign({}, options), + translate, + config + ) + ); +} diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/InputOutputProps.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/InputOutputProps.js new file mode 100644 index 00000000..bdcbab46 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/InputOutputProps.js @@ -0,0 +1,13 @@ +'use strict'; + +import inputOutput from './implementation/InputOutput'; + +export default function(group, element, bpmnFactory, translate) { + const inputOutputEntry = inputOutput(element, bpmnFactory, {}, translate); + + group.entries = group.entries.concat(inputOutputEntry.entries); + + return { + getSelectedParameter: inputOutputEntry.getSelectedParameter + }; +} diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/WorkflowServiceTaskDelegateProps.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/WorkflowServiceTaskDelegateProps.js new file mode 100644 index 00000000..bc871357 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/WorkflowServiceTaskDelegateProps.js @@ -0,0 +1,123 @@ +import inherits from 'inherits'; + +import ImplementationTypeHelper from 'bpmn-js-properties-panel/lib/helper/ImplementationTypeHelper'; +import ServiceTaskDelegateProps from 'bpmn-js-properties-panel/lib/provider/camunda/parts/ServiceTaskDelegateProps'; +import workflowImplementationType from './implementation/WorkflowImplementationType'; +import workflowActivity from './implementation/WorkflowActivity'; +import { + implementationType as implementationTypeConst, + serviceTaskEntries +} from './implementation/implementationConstants'; +import Delegate from './implementation/Delegate'; +import ResultVariable from './implementation/ResultVariable'; + +const getImplementationType = element => { + let implementationType = ImplementationTypeHelper.getImplementationType( + element + ); + + if (!implementationType || implementationType === 'expression') { + const bo = getBusinessObject(element); + if (bo) { + if ( + typeof bo.get(implementationTypeConst.ACTIVITY) !== 'undefined' + ) { + return 'workflowActivity'; + } + } + } + + return implementationType; +}; + +const hideResultVariable = element => { + return getImplementationType(element) !== 'expression'; +}; + +const getBusinessObject = element => + ImplementationTypeHelper.getServiceTaskLikeBusinessObject(element); + +const isDmnCapable = element => ImplementationTypeHelper.isDmnCapable(element); + +const isExternalCapable = element => + ImplementationTypeHelper.isExternalCapable(element); + +const isServiceTaskLike = element => + ImplementationTypeHelper.isServiceTaskLike(element); + +function WorkflowServiceTaskDelegateProps( + group, + element, + config, + bpmnFactory, + translate +) { + ServiceTaskDelegateProps.call(this, group, element, bpmnFactory, translate); + + if (isServiceTaskLike(getBusinessObject(element))) { + group.entries = group.entries.filter( + entry => + entry.id !== serviceTaskEntries.IMPLEMENTATION && + entry.id !== serviceTaskEntries.DELEGATE && + entry.id !== serviceTaskEntries.RESULT_VARIABLE + ); + + group.entries = group.entries.concat( + workflowActivity( + element, + config, + bpmnFactory, + { + getBusinessObject: getBusinessObject, + getImplementationType: getImplementationType + }, + translate + ) + ); + + group.entries = group.entries.concat( + Delegate( + element, + bpmnFactory, + { + getBusinessObject: getBusinessObject, + getImplementationType: getImplementationType + }, + translate + ) + ); + + group.entries = group.entries.concat( + ResultVariable( + element, + bpmnFactory, + { + getBusinessObject: getBusinessObject, + getImplementationType: getImplementationType, + hideResultVariable: hideResultVariable + }, + translate + ) + ); + group.entries = group.entries.concat( + workflowImplementationType( + element, + bpmnFactory, + { + getBusinessObject: getBusinessObject, + getImplementationType: getImplementationType, + hasDmnSupport: isDmnCapable(element), + hasExternalSupport: isExternalCapable( + getBusinessObject(element) + ), + hasServiceTaskLikeSupport: true + }, + translate + ) + ); + } +} + +inherits(WorkflowServiceTaskDelegateProps, ServiceTaskDelegateProps); + +export default WorkflowServiceTaskDelegateProps; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/createInputOutputTabGroups.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/createInputOutputTabGroups.js new file mode 100644 index 00000000..3dede1a9 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/createInputOutputTabGroups.js @@ -0,0 +1,58 @@ +import inputOutputParameter from './InputOutputParameterProps'; +import inputOutput from './InputOutputProps'; +const is = require('bpmn-js/lib/util/ModelUtil').is; + +var getInputOutputParameterLabel = function(param, translate) { + if (is(param, 'camunda:InputParameter')) { + return translate('Input Parameter'); + } + + if (is(param, 'camunda:OutputParameter')) { + return translate('Output Parameter'); + } + + return ''; +}; + +export default function createInputOutputTabGroups( + element, + bpmnFactory, + elementRegistry, + translate, + config +) { + var inputOutputGroup = { + id: 'input-output', + label: translate('Parameters'), + entries: [] + }; + + var options = inputOutput( + inputOutputGroup, + element, + bpmnFactory, + translate + ); + var inputOutputParameterGroup = { + id: 'input-output-parameter', + entries: [], + enabled: function(element, node) { + return options.getSelectedParameter(element, node); + }, + label: function(element, node) { + var param = options.getSelectedParameter(element, node); + return getInputOutputParameterLabel(param, translate); + } + }; + + inputOutputParameter( + inputOutputParameterGroup, + element, + bpmnFactory, + options, + translate, + config + ); + + return [inputOutputGroup, inputOutputParameterGroup]; +} diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/implementation/Delegate.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/implementation/Delegate.js new file mode 100644 index 00000000..f6a0b247 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/implementation/Delegate.js @@ -0,0 +1,78 @@ +'use strict'; + +import entryFactory from 'bpmn-js-properties-panel/lib/factory/EntryFactory'; +import cmdHelper from 'bpmn-js-properties-panel/lib/helper/CmdHelper'; + +const DELEGATE_TYPES = ['class', 'expression', 'delegateExpression']; + +const PROPERTIES = { + class: 'camunda:class', + expression: 'camunda:expression', + delegateExpression: 'camunda:delegateExpression' +}; + +function isDelegate(type) { + return DELEGATE_TYPES.indexOf(type) !== -1; +} + +function getAttribute(type) { + return PROPERTIES[type]; +} + +export default function(element, bpmnFactory, options, translate) { + var getImplementationType = options.getImplementationType, + getBusinessObject = options.getBusinessObject; + + function getDelegationLabel(type) { + switch (type) { + case 'class': + return translate('Java Class'); + case 'expression': + return translate('Expression'); + case 'delegateExpression': + return translate('Delegate Expression'); + default: + return ''; + } + } + + var delegateEntry = entryFactory.textField({ + id: 'delegate', + label: translate('Value'), + dataValueLabel: 'delegationLabel', + modelProperty: 'delegate', + + get: function(element) { + var bo = getBusinessObject(element); + var type = getImplementationType(element); + var attr = getAttribute(type); + var label = getDelegationLabel(type); + return { + delegate: bo.get(attr), + delegationLabel: label + }; + }, + + set: function(element, values) { + var bo = getBusinessObject(element); + var type = getImplementationType(element); + var attr = getAttribute(type); + var prop = {}; + prop[attr] = values.delegate || ''; + return cmdHelper.updateBusinessObject(element, bo, prop); + }, + + validate: function(element, values) { + return isDelegate(getImplementationType(element)) && + !values.delegate + ? { delegate: 'Must provide a value' } + : {}; + }, + + hidden: function(element) { + return !isDelegate(getImplementationType(element)); + } + }); + + return [delegateEntry]; +} diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/implementation/InputOutput.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/implementation/InputOutput.js new file mode 100644 index 00000000..2bbef4f2 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/implementation/InputOutput.js @@ -0,0 +1,289 @@ +import inputOutputHelper from './InputOutputHelper'; +var getBusinessObject = require('bpmn-js/lib/util/ModelUtil').getBusinessObject; + +var elementHelper = require('bpmn-js-properties-panel/lib/helper/ElementHelper'), + extensionElementsHelper = require('bpmn-js-properties-panel/lib/helper/ExtensionElementsHelper'), + cmdHelper = require('bpmn-js-properties-panel/lib/helper/CmdHelper'); + +var extensionElementsEntry = require('bpmn-js-properties-panel/lib/provider/camunda/parts/implementation//ExtensionElements'); + +function getInputOutput(element, insideConnector) { + return inputOutputHelper.getInputOutput(element, insideConnector); +} + +function getConnector(element) { + return inputOutputHelper.getConnector(element); +} + +function getInputParameters(element, insideConnector) { + return inputOutputHelper.getInputParameters(element, insideConnector); +} + +function getOutputParameters(element, insideConnector) { + return inputOutputHelper.getOutputParameters(element, insideConnector); +} + +function getInputParameter(element, insideConnector, idx) { + return inputOutputHelper.getInputParameter(element, insideConnector, idx); +} + +function getOutputParameter(element, insideConnector, idx) { + return inputOutputHelper.getOutputParameter(element, insideConnector, idx); +} + +export function createElement(type, parent, factory, properties) { + const el = elementHelper.createElement(type, properties, parent, factory); + return el; +} + +export function createInputOutput(parent, bpmnFactory, properties) { + return createElement( + 'camunda:InputOutput', + parent, + bpmnFactory, + properties + ); +} + +function createParameter(type, parent, bpmnFactory, properties) { + return createElement(type, parent, bpmnFactory, properties); +} + +function ensureInputOutputSupported(element, insideConnector) { + return inputOutputHelper.isInputOutputSupported(element, insideConnector); +} + +function ensureOutparameterSupported(element, insideConnector) { + return inputOutputHelper.areOutputParametersSupported( + element, + insideConnector + ); +} + +export default function(element, bpmnFactory, options, translate) { + var TYPE_LABEL = { + 'camunda:Map': translate('Map'), + 'camunda:List': translate('List'), + 'camunda:Script': translate('Script') + }; + + options = options || {}; + + var insideConnector = !!options.insideConnector, + idPrefix = options.idPrefix || ''; + + var getSelected = function(element, node) { + var selection = (inputEntry && + inputEntry.getSelected(element, node)) || { idx: -1 }; + + var parameter = getInputParameter( + element, + insideConnector, + selection.idx + ); + if (!parameter && outputEntry) { + selection = outputEntry.getSelected(element, node); + parameter = getOutputParameter( + element, + insideConnector, + selection.idx + ); + } + return parameter; + }; + + var result = { + getSelectedParameter: getSelected + }; + + var entries = (result.entries = []); + + if (!ensureInputOutputSupported(element)) { + return result; + } + + var newElement = function(type, prop, elementData) { + return function(element, extensionElements, value) { + var commands = []; + + var inputOutput = getInputOutput(element, insideConnector); + if (!inputOutput) { + var parent = !insideConnector + ? extensionElements + : getConnector(element); + + inputOutput = createInputOutput(parent, bpmnFactory, { + inputParameters: [], + outputParameters: [] + }); + + if (!insideConnector) { + commands.push( + cmdHelper.addAndRemoveElementsFromList( + element, + extensionElements, + 'values', + 'extensionElements', + [inputOutput], + [] + ) + ); + } else { + commands.push( + cmdHelper.updateBusinessObject(element, parent, { + inputOutput: inputOutput + }) + ); + } + } + + var newElem = elementData + ? createParameter(type, inputOutput, bpmnFactory, elementData) + : createParameter(type, inputOutput, bpmnFactory, { + name: value + }); + + commands.push( + cmdHelper.addElementsTolist(element, inputOutput, prop, [ + newElem + ]) + ); + + return commands; + }; + }; + + var removeElement = function(getter, prop, otherProp) { + return function(element, extensionElements, value, idx) { + var inputOutput = getInputOutput(element, insideConnector); + var parameter = getter(element, insideConnector, idx); + + var commands = []; + commands.push( + cmdHelper.removeElementsFromList( + element, + inputOutput, + prop, + null, + [parameter] + ) + ); + + var firstLength = inputOutput.get(prop).length - 1; + var secondLength = (inputOutput.get(otherProp) || []).length; + + if (!firstLength && !secondLength) { + if (!insideConnector) { + commands.push( + extensionElementsHelper.removeEntry( + getBusinessObject(element), + element, + inputOutput + ) + ); + } else { + var connector = getConnector(element); + commands.push( + cmdHelper.updateBusinessObject(element, connector, { + inputOutput: undefined + }) + ); + } + } + + return commands; + }; + }; + + var setOptionLabelValue = function(getter) { + return function(element, node, option, property, value, idx) { + var parameter = getter(element, insideConnector, idx); + + var suffix = 'Text'; + + var definition = parameter.get('definition'); + if (typeof definition !== 'undefined') { + var type = definition.$type; + suffix = TYPE_LABEL[type]; + } + + option.text = (value || '') + ' : ' + suffix; + }; + }; + + // input parameters /////////////////////////////////////////////////////////////// + + var inputEntry = extensionElementsEntry(element, bpmnFactory, { + id: idPrefix + 'inputs', + label: translate('Input Parameters'), + modelProperty: 'name', + prefix: 'Input', + resizable: true, + + createExtensionElement: inputOutputHelper.isCreateDeleteSupported( + element + ) + ? newElement('camunda:InputParameter', 'inputParameters') + : undefined, + removeExtensionElement: inputOutputHelper.isCreateDeleteSupported( + element + ) + ? removeElement( + getInputParameter, + 'inputParameters', + 'outputParameters' + ) + : undefined, + + getExtensionElements: function(element) { + return getInputParameters(element, insideConnector); + }, + + onSelectionChange: function(element, node) { + outputEntry && outputEntry.deselect(element, node); + }, + + setOptionLabelValue: setOptionLabelValue(getInputParameter) + }); + entries.push(inputEntry); + + // output parameters /////////////////////////////////////////////////////// + + if (ensureOutparameterSupported(element, insideConnector)) { + var outputEntry = extensionElementsEntry(element, bpmnFactory, { + id: idPrefix + 'outputs', + label: translate('Output Parameters'), + modelProperty: 'name', + prefix: 'Output', + resizable: true, + + createExtensionElement: inputOutputHelper.isCreateDeleteSupported( + element + ) + ? newElement('camunda:OutputParameter', 'outputParameters') + : undefined, + removeExtensionElement: inputOutputHelper.isCreateDeleteSupported( + element + ) + ? removeElement( + getOutputParameter, + 'outputParameters', + 'inputParameters' + ) + : inputOutputHelper.isCreateDeleteSupported(element), + + getExtensionElements: function(element) { + return getOutputParameters(element, insideConnector); + }, + + onSelectionChange: function(element, node) { + inputEntry.deselect(element, node); + }, + + setOptionLabelValue: setOptionLabelValue(getOutputParameter) + }); + entries.push(outputEntry); + } + + return result; +} diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/implementation/InputOutputHelper.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/implementation/InputOutputHelper.js new file mode 100644 index 00000000..595ab799 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/implementation/InputOutputHelper.js @@ -0,0 +1,173 @@ +var ModelUtil = require('bpmn-js/lib/util/ModelUtil'), + is = ModelUtil.is, + getBusinessObject = ModelUtil.getBusinessObject; + +var extensionElementsHelper = require('bpmn-js-properties-panel/lib/helper/ExtensionElementsHelper'), + implementationTypeHelper = require('bpmn-js-properties-panel/lib/helper/ImplementationTypeHelper'); +import { implementationType } from './implementationConstants'; + +var InputOutputHelper = {}; + +function getElements(bo, type, prop) { + var elems = extensionElementsHelper.getExtensionElements(bo, type) || []; + return !prop ? elems : (elems[0] || {})[prop] || []; +} + +function getParameters(element, prop, insideConnector) { + var inputOutput = InputOutputHelper.getInputOutput( + element, + insideConnector + ); + return (inputOutput && inputOutput.get(prop)) || []; +} + +/** + * Get a inputOutput from the business object + * + * @param {djs.model.Base} element + * @param {boolean} insideConnector + * + * @return {ModdleElement} the inputOutput object + */ +InputOutputHelper.getInputOutput = function(element, insideConnector) { + if (!insideConnector) { + var bo = getBusinessObject(element); + return (getElements(bo, 'camunda:InputOutput') || [])[0]; + } + var connector = this.getConnector(element); + + return connector && connector.get('inputOutput'); +}; + +/** + * Get a connector from the business object + * + * @param {djs.model.Base} element + * + * @return {ModdleElement} the connector object + */ +InputOutputHelper.getConnector = function(element) { + var bo = implementationTypeHelper.getServiceTaskLikeBusinessObject(element); + return bo && (getElements(bo, 'camunda:Connector') || [])[0]; +}; + +/** + * Return all input parameters existing in the business object, and + * an empty array if none exist. + * + * @param {djs.model.Base} element + * @param {boolean} insideConnector + * + * @return {Array} a list of input parameter objects + */ +InputOutputHelper.getInputParameters = function(element, insideConnector) { + return getParameters.apply(this, [ + element, + 'inputParameters', + insideConnector + ]); +}; + +/** + * Return all output parameters existing in the business object, and + * an empty array if none exist. + * + * @param {djs.model.Base} element + * @param {boolean} insideConnector + * + * @return {Array} a list of output parameter objects + */ +InputOutputHelper.getOutputParameters = function(element, insideConnector) { + return getParameters.apply(this, [ + element, + 'outputParameters', + insideConnector + ]); +}; + +/** + * Get a input parameter from the business object at given index + * + * @param {djs.model.Base} element + * @param {boolean} insideConnector + * @param {number} idx + * + * @return {ModdleElement} input parameter + */ +InputOutputHelper.getInputParameter = function(element, insideConnector, idx) { + return this.getInputParameters(element, insideConnector)[idx]; +}; + +/** + * Get a output parameter from the business object at given index + * + * @param {djs.model.Base} element + * @param {boolean} insideConnector + * @param {number} idx + * + * @return {ModdleElement} output parameter + */ +InputOutputHelper.getOutputParameter = function(element, insideConnector, idx) { + return this.getOutputParameters(element, insideConnector)[idx]; +}; + +/** + * Returns 'true' if the given element supports inputOutput + * + * @param {djs.model.Base} element + * @param {boolean} insideConnector + * + * @return {boolean} a boolean value + */ +InputOutputHelper.isInputOutputSupported = function(element, insideConnector) { + var bo = getBusinessObject(element); + return ( + insideConnector || + is(bo, 'bpmn:Process') || + (is(bo, 'bpmn:FlowNode') && + !is(bo, 'bpmn:StartEvent') && + !is(bo, 'bpmn:BoundaryEvent') && + !(is(bo, 'bpmn:SubProcess') && bo.get('triggeredByEvent'))) + ); +}; + +/** + * Returns 'true' if the given element supports output parameters + * + * @param {djs.model.Base} element + * @param {boolean} insideConnector + * + * @return {boolean} a boolean value + */ +InputOutputHelper.areOutputParametersSupported = function( + element, + insideConnector +) { + var bo = getBusinessObject(element); + return ( + insideConnector || (!is(bo, 'bpmn:EndEvent') && !bo.loopCharacteristics) + ); +}; + +InputOutputHelper.isCreateDeleteSupported = function(element) { + const bo = getBusinessObject(element); + return ( + (element.type !== 'bpmn:ServiceTask' || + !bo[implementationType.ACTIVITY]) && + element.type !== 'bpmn:Process' + ); +}; + +InputOutputHelper.isWorkflowTargetSupported = function(element, selected) { + const bo = getBusinessObject(element); + return ( + is(bo, 'bpmn:ServiceTask') && is(selected, 'camunda:OutputParameter') + ); +}; + +InputOutputHelper.isWorkflowSourceSupported = function(element, selected) { + const bo = getBusinessObject(element); + return is(bo, 'bpmn:ServiceTask') && is(selected, 'camunda:InputParameter'); +}; + +export default InputOutputHelper; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/implementation/InputOutputParameter.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/implementation/InputOutputParameter.js new file mode 100644 index 00000000..10e258e3 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/implementation/InputOutputParameter.js @@ -0,0 +1,424 @@ +import inputOutputHelper from './InputOutputHelper'; + +var is = require('bpmn-js/lib/util/ModelUtil').is; + +var elementHelper = require('bpmn-js-properties-panel/lib/helper/ElementHelper'), + cmdHelper = require('bpmn-js-properties-panel/lib/helper/CmdHelper'), + utils = require('bpmn-js-properties-panel/lib/Utils'); + +var entryFactory = require('bpmn-js-properties-panel/lib/factory/EntryFactory'), + script = require('bpmn-js-properties-panel/lib/provider/camunda/parts/implementation/Script')( + 'scriptFormat', + 'value', + true + ); + +function createElement(type, parent, factory, properties) { + return elementHelper.createElement(type, properties, parent, factory); +} + +function isScript(elem) { + return is(elem, 'camunda:Script'); +} + +function isList(elem) { + return is(elem, 'camunda:List'); +} + +function isMap(elem) { + return is(elem, 'camunda:Map'); +} + +function ensureInputOutputSupported(element, insideConnector) { + return inputOutputHelper.isInputOutputSupported(element, insideConnector); +} + +export default function(element, bpmnFactory, options, translate) { + var typeInfo = { + 'camunda:Map': { + value: 'map', + label: translate('Map') + }, + 'camunda:List': { + value: 'list', + label: translate('List') + }, + 'camunda:Script': { + value: 'script', + label: translate('Script') + } + }; + + options = options || {}; + + var insideConnector = !!options.insideConnector, + idPrefix = options.idPrefix || ''; + + var getSelected = options.getSelectedParameter; + + if (!ensureInputOutputSupported(element, insideConnector)) { + return []; + } + + var entries = []; + + var isSelected = function(element, node) { + return getSelected(element, node); + }; + + // parameter name //////////////////////////////////////////////////////// + + entries.push( + entryFactory.validationAwareTextField({ + id: idPrefix + 'parameterName', + label: 'Name', + modelProperty: 'name', + + getProperty: function(element, node) { + return (getSelected(element, node) || {}).name; + }, + + setProperty: function(element, values, node) { + var param = getSelected(element, node); + return cmdHelper.updateBusinessObject(element, param, values); + }, + + validate: function(element, values, node) { + var bo = getSelected(element, node); + + var validation = {}; + if (bo) { + var nameValue = values.name; + + if (nameValue) { + if (utils.containsSpace(nameValue)) { + validation.name = 'Name must not contain spaces'; + } + } else { + validation.name = 'Parameter must have a name'; + } + } + + return validation; + }, + + hidden: function(element, node) { + return !isSelected(element, node); + }, + disabled: function(element) { + return !inputOutputHelper.isCreateDeleteSupported(element); + } + }) + ); + + // parameter type ////////////////////////////////////////////////////// + + var selectOptions = [ + { value: 'text', name: 'Text' }, + { value: 'script', name: 'Script' }, + { value: 'list', name: 'List' }, + { value: 'map', name: 'Map' } + ]; + + entries.push( + entryFactory.selectBox({ + id: idPrefix + 'parameterType', + label: 'Type', + selectOptions: selectOptions, + modelProperty: 'parameterType', + + get: function(element, node) { + var bo = getSelected(element, node); + + var parameterType = 'text'; + + if (typeof bo !== 'undefined') { + var definition = bo.get('definition'); + if (typeof definition !== 'undefined') { + var type = definition.$type; + parameterType = typeInfo[type].value; + } + } + + return { + parameterType: parameterType + }; + }, + + set: function(element, values, node) { + var bo = getSelected(element, node); + + var properties = { + value: undefined, + definition: undefined + }; + + var createParameterTypeElem = function(type) { + return createElement(type, bo, bpmnFactory); + }; + + var parameterType = values.parameterType; + + if (parameterType === 'script') { + properties.definition = createParameterTypeElem( + 'camunda:Script' + ); + } else if (parameterType === 'list') { + properties.definition = createParameterTypeElem( + 'camunda:List' + ); + } else if (parameterType === 'map') { + properties.definition = createParameterTypeElem( + 'camunda:Map' + ); + } + + return cmdHelper.updateBusinessObject(element, bo, properties); + }, + + show: function(element, node) { + return isSelected(element, node); + }, + disabled: function(element) { + return !inputOutputHelper.isCreateDeleteSupported(element); + } + }) + ); + + // parameter value (type = text) /////////////////////////////////////////////////////// + + entries.push( + entryFactory.textBox({ + id: idPrefix + 'parameterType-text', + label: 'Value', + modelProperty: 'value', + get: function(element, node) { + return { + value: (getSelected(element, node) || {}).value + }; + }, + + set: function(element, values, node) { + var param = getSelected(element, node); + values.value = values.value || undefined; + return cmdHelper.updateBusinessObject(element, param, values); + }, + + show: function(element, node) { + var bo = getSelected(element, node); + return bo && !bo.definition; + } + }) + ); + + // parameter value (type = script) /////////////////////////////////////////////////////// + + entries.push({ + id: idPrefix + 'parameterType-script', + html: '<div data-show="isScript">' + script.template + '</div>', + get: function(element, node) { + var bo = getSelected(element, node); + return bo && isScript(bo.definition) + ? script.get(element, bo.definition) + : {}; + }, + + set: function(element, values, node) { + var bo = getSelected(element, node); + var update = script.set(element, values); + return cmdHelper.updateBusinessObject( + element, + bo.definition, + update + ); + }, + + validate: function(element, values, node) { + var bo = getSelected(element, node); + return bo && isScript(bo.definition) + ? script.validate(element, bo.definition) + : {}; + }, + + isScript: function(element, node) { + var bo = getSelected(element, node); + return bo && isScript(bo.definition); + }, + + script: script + }); + + // parameter value (type = list) /////////////////////////////////////////////////////// + + entries.push( + entryFactory.table({ + id: idPrefix + 'parameterType-list', + modelProperties: ['value'], + labels: ['Value'], + + getElements: function(element, node) { + var bo = getSelected(element, node); + + if (bo && isList(bo.definition)) { + return bo.definition.items; + } + + return []; + }, + + updateElement: function(element, values, node, idx) { + var bo = getSelected(element, node); + var item = bo.definition.items[idx]; + return cmdHelper.updateBusinessObject(element, item, values); + }, + + addElement: function(element, node) { + var bo = getSelected(element, node); + var newValue = createElement( + 'camunda:Value', + bo.definition, + bpmnFactory, + { value: undefined } + ); + return cmdHelper.addElementsTolist( + element, + bo.definition, + 'items', + [newValue] + ); + }, + + removeElement: function(element, node, idx) { + var bo = getSelected(element, node); + return cmdHelper.removeElementsFromList( + element, + bo.definition, + 'items', + null, + [bo.definition.items[idx]] + ); + }, + + editable: function(element, node, prop, idx) { + var bo = getSelected(element, node); + var item = bo.definition.items[idx]; + return !isMap(item) && !isList(item) && !isScript(item); + }, + + setControlValue: function(element, node, input, prop, value, idx) { + var bo = getSelected(element, node); + var item = bo.definition.items[idx]; + + if (!isMap(item) && !isList(item) && !isScript(item)) { + input.value = value; + } else { + input.value = typeInfo[item.$type].label; + } + }, + + show: function(element, node) { + var bo = getSelected(element, node); + return bo && bo.definition && isList(bo.definition); + } + }) + ); + + // parameter value (type = map) /////////////////////////////////////////////////////// + + entries.push( + entryFactory.table({ + id: idPrefix + 'parameterType-map', + modelProperties: ['key', 'value'], + labels: ['Key', 'Value'], + addLabel: 'Add Entry', + + getElements: function(element, node) { + var bo = getSelected(element, node); + + if (bo && isMap(bo.definition)) { + return bo.definition.entries; + } + + return []; + }, + + updateElement: function(element, values, node, idx) { + var bo = getSelected(element, node); + var entry = bo.definition.entries[idx]; + + if ( + isMap(entry.definition) || + isList(entry.definition) || + isScript(entry.definition) + ) { + values = { + key: values.key + }; + } + + return cmdHelper.updateBusinessObject(element, entry, values); + }, + + addElement: function(element, node) { + var bo = getSelected(element, node); + var newEntry = createElement( + 'camunda:Entry', + bo.definition, + bpmnFactory, + { key: undefined, value: undefined } + ); + return cmdHelper.addElementsTolist( + element, + bo.definition, + 'entries', + [newEntry] + ); + }, + + removeElement: function(element, node, idx) { + var bo = getSelected(element, node); + return cmdHelper.removeElementsFromList( + element, + bo.definition, + 'entries', + null, + [bo.definition.entries[idx]] + ); + }, + + editable: function(element, node, prop, idx) { + var bo = getSelected(element, node); + var entry = bo.definition.entries[idx]; + return ( + prop === 'key' || + (!isMap(entry.definition) && + !isList(entry.definition) && + !isScript(entry.definition)) + ); + }, + + setControlValue: function(element, node, input, prop, value, idx) { + var bo = getSelected(element, node); + var entry = bo.definition.entries[idx]; + + if ( + prop === 'key' || + (!isMap(entry.definition) && + !isList(entry.definition) && + !isScript(entry.definition)) + ) { + input.value = value; + } else { + input.value = typeInfo[entry.definition.$type].label; + } + }, + + show: function(element, node) { + var bo = getSelected(element, node); + return bo && bo.definition && isMap(bo.definition); + } + }) + ); + + return entries; +} diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/implementation/InputOutputUpdater.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/implementation/InputOutputUpdater.js new file mode 100644 index 00000000..056a2dba --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/implementation/InputOutputUpdater.js @@ -0,0 +1,74 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +*http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import cmdHelper from 'bpmn-js-properties-panel/lib/helper/CmdHelper'; +import { createInputOutput, createElement } from './InputOutput'; +import InputOutputHelper from './InputOutputHelper'; +import { INPUT, OUTPUT } from './implementationConstants'; + +export default ({ element, bo, bpmnFactory, activityInputsOutputs }) => { + const commands = []; + const existedInputOutput = InputOutputHelper.getInputOutput(element); + + let newInputOutput = createInputOutput(element, bpmnFactory, { + inputParameters: [], + outputParameters: [] + }); + + const inputs = activityInputsOutputs.inputs.map(({ name, value }) => + createElement(INPUT, newInputOutput, bpmnFactory, { + name, + type: 'Text', + value + }) + ); + + const outputs = activityInputsOutputs.outputs.map(({ name, value }) => + createElement(OUTPUT, newInputOutput, bpmnFactory, { + name, + type: 'Text', + value + }) + ); + + newInputOutput.inputParameters = inputs; + newInputOutput.outputParameters = outputs; + + const objectToRemove = existedInputOutput ? [existedInputOutput] : []; + const extensionElements = + bo.extensionElements || + createElement('bpmn:ExtensionElements', bo, bpmnFactory, []); + + if (!bo.extensionElements) { + commands.push( + cmdHelper.updateBusinessObject(element, bo, { + extensionElements + }) + ); + } + + commands.push( + cmdHelper.addAndRemoveElementsFromList( + element, + extensionElements, + 'values', + 'extensionElements', + [newInputOutput], + objectToRemove + ) + ); + return commands; +}; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/implementation/ResultVariable.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/implementation/ResultVariable.js new file mode 100644 index 00000000..a0b425fe --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/implementation/ResultVariable.js @@ -0,0 +1,52 @@ +'use strict'; + +import { is } from 'bpmn-js/lib/util/ModelUtil'; + +import assign from 'lodash.assign'; + +var entryFactory = require('bpmn-js-properties-panel/lib/factory/EntryFactory'), + cmdHelper = require('bpmn-js-properties-panel/lib/helper/CmdHelper'); + +export default function(element, bpmnFactory, options, translate) { + var getBusinessObject = options.getBusinessObject, + hideResultVariable = options.hideResultVariable, + id = options.id || 'resultVariable'; + + var resultVariableEntry = entryFactory.textField({ + id: id, + label: translate('Result Variable'), + modelProperty: 'resultVariable', + + get: function(element) { + var bo = getBusinessObject(element); + return { resultVariable: bo.get('camunda:resultVariable') }; + }, + + set: function(element, values) { + var bo = getBusinessObject(element); + + var resultVariable = values.resultVariable || undefined; + + var props = { + 'camunda:resultVariable': resultVariable + }; + + if (is(bo, 'camunda:DmnCapable') && !resultVariable) { + props = assign( + { 'camunda:mapDecisionResult': 'resultList' }, + props + ); + } + + return cmdHelper.updateBusinessObject(element, bo, props); + }, + + hidden: function() { + if (typeof hideResultVariable === 'function') { + return hideResultVariable.apply(resultVariableEntry, arguments); + } + } + }); + + return [resultVariableEntry]; +} diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/implementation/WorkflowActivity.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/implementation/WorkflowActivity.js new file mode 100644 index 00000000..6616f6a4 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/implementation/WorkflowActivity.js @@ -0,0 +1,89 @@ +import entryFactory from 'bpmn-js-properties-panel/lib/factory/EntryFactory'; +import cmdHelper from 'bpmn-js-properties-panel/lib/helper/CmdHelper'; +import { + implementationType, + IMPLEMENTATION_TYPE_VALUE, + SERVICE_TASK_NAME +} from './implementationConstants'; + +import InputOutputUpdater from './InputOutputUpdater'; + +const workflowActivity = (element, config, bpmnFactory, options, translate) => { + const { getImplementationType, getBusinessObject } = options; + + const isWorkflowActivity = element => + getImplementationType(element) === 'workflowActivity'; + + const workflowActivityEntry = entryFactory.selectBox({ + id: 'activitySelect', + label: translate('Activity Spec'), + selectOptions: config.activities, + emptyParameter: true, + modelProperty: 'workflowActivity', + + get: function(element) { + var bo = getBusinessObject(element); + const value = bo.get(implementationType.ACTIVITY); + const activityValue = + value && value.indexOf(IMPLEMENTATION_TYPE_VALUE) > -1 + ? value.substr(IMPLEMENTATION_TYPE_VALUE.length) + : ''; + + return { + workflowActivity: activityValue + }; + }, + + set: function(element, values) { + var bo = getBusinessObject(element); + + const commands = []; + const dataForUpdate = {}; + + const activityInputsOutputs = config.getActivityInputsOutputs( + values.workflowActivity + ); + + dataForUpdate[ + implementationType.ACTIVITY + ] = `${IMPLEMENTATION_TYPE_VALUE}${values.workflowActivity}`; + + dataForUpdate[implementationType.EXPRESSION] = + implementationType.EXPRESSION_VALUE; + + dataForUpdate[SERVICE_TASK_NAME] = values.workflowActivity; + + commands.push( + cmdHelper.updateBusinessObject(element, bo, dataForUpdate) + ); + return [ + ...commands, + ...InputOutputUpdater({ + element, + bo, + bpmnFactory, + activityInputsOutputs, + commands + }) + ]; + }, + + validate: function(element, values) { + const hasErrors = + isWorkflowActivity(element) && !values.workflowActivity; + config.validationUpdate(element, !hasErrors); + + return hasErrors + ? { workflowActivity: 'Must provide a value' } + : {}; + }, + + hidden: function(element) { + return !isWorkflowActivity(element); + } + }); + + return [workflowActivityEntry]; +}; + +export default workflowActivity; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/implementation/WorkflowImplementationType.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/implementation/WorkflowImplementationType.js new file mode 100644 index 00000000..729cc22b --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/implementation/WorkflowImplementationType.js @@ -0,0 +1,226 @@ +var entryFactory = require('bpmn-js-properties-panel/lib/factory/EntryFactory'), + cmdHelper = require('bpmn-js-properties-panel/lib/helper/CmdHelper'), + extensionElementsHelper = require('bpmn-js-properties-panel/lib/helper/ExtensionElementsHelper'), + elementHelper = require('bpmn-js-properties-panel/lib/helper/ElementHelper'); + +var assign = require('lodash.assign'); +var map = require('lodash.map'); +import { implementationType } from './implementationConstants'; + +var DEFAULT_DELEGATE_PROPS = ['class', 'expression', 'delegateExpression']; + +var DELEGATE_PROPS = { + 'camunda:class': undefined, + 'camunda:expression': undefined, + 'camunda:delegateExpression': undefined, + 'camunda:resultVariable': undefined +}; + +var DMN_CAPABLE_PROPS = { + 'camunda:decisionRef': undefined, + 'camunda:decisionRefBinding': 'latest', + 'camunda:decisionRefVersion': undefined, + 'camunda:mapDecisionResult': 'resultList', + 'camunda:decisionRefTenantId': undefined +}; + +var EXTERNAL_CAPABLE_PROPS = { + 'camunda:type': undefined, + 'camunda:topic': undefined +}; + +const ACTIVITY_PROPS = {}; + +ACTIVITY_PROPS[implementationType] = undefined; + +export default function(element, bpmnFactory, options, translate) { + var DEFAULT_OPTIONS = [ + { value: 'class', name: translate('Java Class') }, + { value: 'expression', name: translate('Expression') }, + { value: 'delegateExpression', name: translate('Delegate Expression') } + ]; + + var DMN_OPTION = [{ value: 'dmn', name: translate('DMN') }]; + + var EXTERNAL_OPTION = [{ value: 'external', name: translate('External') }]; + + var CONNECTOR_OPTION = [ + { value: 'connector', name: translate('Connector') } + ]; + + var SCRIPT_OPTION = [{ value: 'script', name: translate('Script') }]; + + var ACTIVITY_OPTION = [ + { value: 'workflowActivity', name: translate('Activity') } + ]; + + var getType = options.getImplementationType, + getBusinessObject = options.getBusinessObject; + + var hasDmnSupport = options.hasDmnSupport, + hasExternalSupport = options.hasExternalSupport, + hasServiceTaskLikeSupport = options.hasServiceTaskLikeSupport, + hasScriptSupport = options.hasScriptSupport; + + var entries = []; + + var selectOptions = DEFAULT_OPTIONS.concat([]); + + if (hasDmnSupport) { + selectOptions = selectOptions.concat(DMN_OPTION); + } + + if (hasExternalSupport) { + selectOptions = selectOptions.concat(EXTERNAL_OPTION); + } + + if (hasServiceTaskLikeSupport) { + selectOptions = selectOptions.concat(CONNECTOR_OPTION); + } + + if (hasScriptSupport) { + selectOptions = selectOptions.concat(SCRIPT_OPTION); + } + + selectOptions = selectOptions.concat(ACTIVITY_OPTION); + + selectOptions.push({ value: '' }); + + entries.push( + entryFactory.selectBox({ + id: 'implementation', + label: translate('Implementation'), + selectOptions: selectOptions, + modelProperty: 'implType', + + get: function(element) { + return { + implType: getType(element) || '' + }; + }, + + set: function(element, values) { + var bo = getBusinessObject(element); + var oldType = getType(element); + var newType = values.implType; + var props = assign({}, DELEGATE_PROPS); + + if (DEFAULT_DELEGATE_PROPS.indexOf(newType) !== -1) { + var newValue = ''; + if (DEFAULT_DELEGATE_PROPS.indexOf(oldType) !== -1) { + newValue = bo.get('camunda:' + oldType); + } + + props['camunda:' + newType] = newValue; + } + + if (hasDmnSupport) { + props = assign(props, DMN_CAPABLE_PROPS); + if (newType === 'dmn') { + props['camunda:decisionRef'] = ''; + } + } + + if (hasExternalSupport) { + props = assign(props, EXTERNAL_CAPABLE_PROPS); + if (newType === 'external') { + props['camunda:type'] = 'external'; + props['camunda:topic'] = ''; + } + } + + if (hasScriptSupport) { + props['camunda:script'] = undefined; + + if (newType === 'script') { + props['camunda:script'] = elementHelper.createElement( + 'camunda:Script', + {}, + bo, + bpmnFactory + ); + } + } + props = assign(props, ACTIVITY_PROPS); + props[implementationType.ACTIVITY] = undefined; + + var commands = []; + if (newType === 'workflowActivity') { + props[implementationType.ACTIVITY] = ''; + props[implementationType.RESULT_VARIABLE] = undefined; + props[implementationType.EXPRESSION] = undefined; + } else { + var inputsOutputs = extensionElementsHelper.getExtensionElements( + bo, + 'camunda:InputOutput' + ); + commands.push( + map(inputsOutputs, function(inputOutput) { + return extensionElementsHelper.removeEntry( + bo, + element, + inputOutput + ); + }) + ); + } + + commands.push( + cmdHelper.updateBusinessObject(element, bo, props) + ); + + if (hasServiceTaskLikeSupport) { + var connectors = extensionElementsHelper.getExtensionElements( + bo, + 'camunda:Connector' + ); + commands.push( + map(connectors, function(connector) { + return extensionElementsHelper.removeEntry( + bo, + element, + connector + ); + }) + ); + + if (newType === 'connector') { + var extensionElements = bo.get('extensionElements'); + if (!extensionElements) { + extensionElements = elementHelper.createElement( + 'bpmn:ExtensionElements', + { values: [] }, + bo, + bpmnFactory + ); + commands.push( + cmdHelper.updateBusinessObject(element, bo, { + extensionElements: extensionElements + }) + ); + } + var connector = elementHelper.createElement( + 'camunda:Connector', + {}, + extensionElements, + bpmnFactory + ); + commands.push( + cmdHelper.addAndRemoveElementsFromList( + element, + extensionElements, + 'values', + 'extensionElements', + [connector], + [] + ) + ); + } + } + return commands; + } + }) + ); + + return entries; +} diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/implementation/implementationConstants.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/implementation/implementationConstants.js new file mode 100644 index 00000000..efc70800 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-properties-provider/provider/camunda/parts/implementation/implementationConstants.js @@ -0,0 +1,34 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +*http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +export const implementationType = { + ACTIVITY: 'implementation', + EXPRESSION: 'camunda:expression', + EXPRESSION_VALUE: '${ExecuteActivity.execute(execution)}', + RESULT_VARIABLE: 'camunda:resultVariable' +}; + +export const IMPLEMENTATION_TYPE_VALUE = 'activity:'; +export const SERVICE_TASK_NAME = 'name'; + +export const serviceTaskEntries = { + IMPLEMENTATION: 'implementation', + DELEGATE: 'delegate', + RESULT_VARIABLE: 'resultVariable' +}; + +export const INPUT = 'camunda:InputParameter'; +export const OUTPUT = 'camunda:OutputParameter'; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/newDiagram.bpmn b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/newDiagram.bpmn new file mode 100644 index 00000000..6abaf8df --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/newDiagram.bpmn @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<bpmn2:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL BPMN20.xsd" id="sample-diagram" targetNamespace="http://bpmn.io/schema/bpmn"> + <bpmn2:process id="Process_1" isExecutable="true"> + </bpmn2:process> + <bpmndi:BPMNDiagram id="BPMNDiagram_1"> + <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1"> + </bpmndi:BPMNPlane> + </bpmndi:BPMNDiagram> +</bpmn2:definitions> diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/readOnly.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/readOnly.js new file mode 100644 index 00000000..f1da7dcf --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/composition/readOnly.js @@ -0,0 +1,134 @@ +import forEach from 'lodash.foreach'; + +const HIGH_PRIORITY = 10001; + +function ReadOnly( + eventBus, + contextPad, + dragging, + directEditing, + editorActions, + modeling, + palette, + paletteProvider +) { + this._readOnly = false; + this._eventBus = eventBus; + + let self = this; + eventBus.on('readOnly.changed', HIGH_PRIORITY, function(e) { + self._readOnly = e.readOnly; + + if (e.readOnly) { + directEditing.cancel(); + contextPad.close(); + dragging.cancel(); + } + + palette._update(); + }); + + function intercept(obj, fnName, cb) { + var fn = obj[fnName]; + obj[fnName] = function() { + return cb.call(this, fn, arguments); + }; + } + + function ignoreWhenReadOnly(obj, fnName) { + intercept(obj, fnName, function(fn, args) { + if (self._readOnly) { + return; + } + + return fn.apply(this, args); + }); + } + + function throwIfReadOnly(obj, fnName) { + intercept(obj, fnName, function(fn, args) { + if (self._readOnly) { + throw new Error('model is read-only'); + } + + return fn.apply(this, args); + }); + } + + ignoreWhenReadOnly(contextPad, 'open'); + + ignoreWhenReadOnly(dragging, 'init'); + + ignoreWhenReadOnly(directEditing, 'activate'); + + ignoreWhenReadOnly(editorActions._actions, 'undo'); + ignoreWhenReadOnly(editorActions._actions, 'redo'); + ignoreWhenReadOnly(editorActions._actions, 'copy'); + ignoreWhenReadOnly(editorActions._actions, 'paste'); + ignoreWhenReadOnly(editorActions._actions, 'removeSelection'); + // BpmnEditorActions + ignoreWhenReadOnly(editorActions._actions, 'spaceTool'); + ignoreWhenReadOnly(editorActions._actions, 'lassoTool'); + ignoreWhenReadOnly(editorActions._actions, 'globalConnectTool'); + ignoreWhenReadOnly(editorActions._actions, 'distributeElements'); + ignoreWhenReadOnly(editorActions._actions, 'alignElements'); + ignoreWhenReadOnly(editorActions._actions, 'directEditing'); + + throwIfReadOnly(modeling, 'moveShape'); + throwIfReadOnly(modeling, 'updateAttachment'); + throwIfReadOnly(modeling, 'moveElements'); + throwIfReadOnly(modeling, 'moveConnection'); + throwIfReadOnly(modeling, 'layoutConnection'); + throwIfReadOnly(modeling, 'createConnection'); + throwIfReadOnly(modeling, 'createShape'); + throwIfReadOnly(modeling, 'createLabel'); + throwIfReadOnly(modeling, 'appendShape'); + throwIfReadOnly(modeling, 'removeElements'); + throwIfReadOnly(modeling, 'distributeElements'); + throwIfReadOnly(modeling, 'removeShape'); + throwIfReadOnly(modeling, 'removeConnection'); + throwIfReadOnly(modeling, 'replaceShape'); + throwIfReadOnly(modeling, 'pasteElements'); + throwIfReadOnly(modeling, 'alignElements'); + throwIfReadOnly(modeling, 'resizeShape'); + throwIfReadOnly(modeling, 'createSpace'); + throwIfReadOnly(modeling, 'updateWaypoints'); + throwIfReadOnly(modeling, 'reconnectStart'); + throwIfReadOnly(modeling, 'reconnectEnd'); + + intercept(paletteProvider, 'getPaletteEntries', function(fn, args) { + var entries = fn.apply(this, args); + if (self._readOnly) { + forEach(entries, function(value, key) { + delete entries[key]; + }); + } + return entries; + }); +} + +ReadOnly.$inject = [ + 'eventBus', + 'contextPad', + 'dragging', + 'directEditing', + 'editorActions', + 'modeling', + 'palette', + 'paletteProvider' +]; + +module.exports = ReadOnly; + +ReadOnly.prototype.readOnly = function(readOnly) { + var newValue = !!readOnly, + oldValue = !!this._readOnly; + + if (readOnly === undefined || newValue === oldValue) { + return oldValue; + } + + this._readOnly = newValue; + this._eventBus.fire('readOnly.changed', { readOnly: newValue }); + return newValue; +}; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/create/CreateVersion.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/create/CreateVersion.js new file mode 100644 index 00000000..2635fa36 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/create/CreateVersion.js @@ -0,0 +1,53 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { connect } from 'react-redux'; +import { withRouter } from 'react-router-dom'; + +import { hideModalAction } from 'shared/modal/modalWrapperActions'; +import CreateVersionView from 'features/version/create/CreateVersionView'; +import { + newVersionAction, + submitVersionAction +} from 'features/version/create/createVersionConstants'; +import { + getWorkflowId, + getLatestBaseId +} from 'features/workflow/overview/overviewSelectors'; + +function mapStateToProps(state) { + return { + workflowId: getWorkflowId(state), + baseVersionId: getLatestBaseId(state) + }; +} + +function mapDispatchToProps(dispatch) { + return { + submitNewVersion: payload => { + dispatch(submitVersionAction(payload)); + dispatch(hideModalAction()); + }, + closeCreateVersionModal: () => dispatch(hideModalAction()), + versionDetailsChanged: payload => dispatch(newVersionAction(payload)) + }; +} + +export default withRouter( + connect( + mapStateToProps, + mapDispatchToProps + )(CreateVersionView) +); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/create/CreateVersionView.jsx b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/create/CreateVersionView.jsx new file mode 100644 index 00000000..101b442d --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/create/CreateVersionView.jsx @@ -0,0 +1,95 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { I18n } from 'react-redux-i18n'; +import { Button } from 'onap-ui-react'; +import Description from 'shared/components/Description'; +import Select from 'shared/components/Select/index'; +import { VERSION_LEVEL_LIST } from 'wfapp/appConstants'; + +class CreateVersionView extends Component { + static propTypes = { + versionCategories: PropTypes.array, + closeCreateVersionModal: PropTypes.func, + versionDetailsChanged: PropTypes.func, + submitNewVersion: PropTypes.func, + workflowId: PropTypes.string, + baseVersionId: PropTypes.string, + history: PropTypes.object + }; + + constructor(props) { + super(props); + this.state = { + newVersion: '' + }; + } + handleSubmitForm = () => { + const { + submitNewVersion, + workflowId, + baseVersionId, + history + } = this.props; + submitNewVersion({ + description: this.state.newVersion.description, + workflowId: workflowId, + baseId: baseVersionId || null, + history: history + }); + }; + + versionDetailsChanged = val => { + this.setState({ newVersion: val }); + }; + + render() { + const { closeCreateVersionModal } = this.props; + return ( + <form onSubmit={this.handleSubmitForm} autoComplete="off"> + <div className="new-version-page custom-modal-wrapper"> + <div className="form-custom-modal"> + <Select + dataObj={VERSION_LEVEL_LIST} + selectedItem={VERSION_LEVEL_LIST[0].value} + label={I18n.t('version.category')} + disabled + /> + <Description + name="version-description" + description={this.state.newVersion.description} + dataTestId="new-version-description" + onDataChange={this.versionDetailsChanged} + /> + </div> + <div className="modal-action-bar sdc-modal__footer"> + <Button btnType="primary"> + {I18n.t('buttons.createBtn')} + </Button> + <Button + btnType="secondary" + onClick={closeCreateVersionModal}> + {I18n.t('buttons.closeBtn')} + </Button> + </div> + </div> + </form> + ); + } +} + +export default CreateVersionView; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/create/__tests__/CreateVersionView_snapshot-test.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/create/__tests__/CreateVersionView_snapshot-test.js new file mode 100644 index 00000000..13faac14 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/create/__tests__/CreateVersionView_snapshot-test.js @@ -0,0 +1,28 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import React from 'react'; +import renderer from 'react-test-renderer'; + +import CreateVersionView from '../CreateVersionView'; + +describe('Create new version snapshot', () => { + it('renders correctly', () => { + const tree = renderer.create(<CreateVersionView />).toJSON(); + + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/create/__tests__/__snapshots__/CreateVersionView_snapshot-test.js.snap b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/create/__tests__/__snapshots__/CreateVersionView_snapshot-test.js.snap new file mode 100644 index 00000000..4e17b936 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/create/__tests__/__snapshots__/CreateVersionView_snapshot-test.js.snap @@ -0,0 +1,81 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Create new version snapshot renders correctly 1`] = ` +<form + autoComplete="off" + onSubmit={[Function]} +> + <div + className="new-version-page custom-modal-wrapper" + > + <div + className="form-custom-modal" + > + <div + className="select-wrapper version-selector sdc-input" + > + <label> + category + </label> + <select + className="inputinput-selector" + data-test-id="vc-select-box" + disabled={true} + value={undefined} + > + <option + data-test-id="vc-option" + value="2" + > + Major + </option> + <option + data-test-id="vc-option" + value="1" + > + Minor + </option> + </select> + </div> + <div + className="description-part" + > + <div + className="sdc-textarea" + > + <div + className="sdc-textarea__label" + > + description + </div> + <textarea + className="custom-textarea field-section sdc-textarea__textarea" + data-test-id="new-version-description" + disabled={false} + onChange={[Function]} + value={undefined} + /> + </div> + </div> + </div> + <div + className="modal-action-bar sdc-modal__footer" + > + <button + className="sdc-button sdc-button__primary " + disabled={false} + onClick={undefined} + > + createBtn + </button> + <button + className="sdc-button sdc-button__secondary " + disabled={false} + onClick={undefined} + > + closeBtn + </button> + </div> + </div> +</form> +`; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/create/createVersionConstants.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/create/createVersionConstants.js new file mode 100644 index 00000000..5f72ff7a --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/create/createVersionConstants.js @@ -0,0 +1,30 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { createAction } from 'redux-actions'; + +export const VERSION_DETAILS_CHANGED = 'create/DETAILS_CHANGED'; +export const SUBMIT_VERSION = 'create/SUBMIT_VERSION'; + +export const newVersionAction = createAction( + VERSION_DETAILS_CHANGED, + payload => payload +); + +export const submitVersionAction = createAction( + SUBMIT_VERSION, + payload => payload +); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/create/views/NewVersionContainer.jsx b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/create/views/NewVersionContainer.jsx new file mode 100644 index 00000000..4d884730 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/create/views/NewVersionContainer.jsx @@ -0,0 +1,45 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import React from 'react'; +import PropTypes from 'prop-types'; +import { I18n } from 'react-redux-i18n'; +import { SVGIcon } from 'onap-ui-react'; + +const NewVersionContainer = ({ + onCreateVersion, + isVersionsCertifies, + isArchive +}) => { + const disableClass = isVersionsCertifies && !isArchive; + const newVersionDisabledClass = disableClass ? '' : 'newVersionDisabled'; + const svgColor = disableClass ? 'primary' : 'secondary'; + return ( + <div className={`create-new-version ${newVersionDisabledClass}`}> + <div className="create-item-plus-icon" onClick={onCreateVersion}> + <SVGIcon name="plus" color={svgColor} /> + {I18n.t('workflow.overview.newVersion')} + </div> + </div> + ); +}; + +NewVersionContainer.propTypes = { + onCreateVersion: PropTypes.func, + isVersionsCertifies: PropTypes.bool, + isArchive: PropTypes.bool +}; + +export default NewVersionContainer; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/general/General.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/general/General.js new file mode 100644 index 00000000..ecf84750 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/general/General.js @@ -0,0 +1,39 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { connect } from 'react-redux'; + +import GeneralView from 'features/version/general/GeneralView'; +import { + getVersionInfo, + getIsCertified +} from 'features/version/general/generalSelectors'; +import { workflowVersionDetailsChangedAction } from 'features/version/versionConstants'; +import { isWorkflowArchive } from 'features/workflow/workflowSelectors'; +const mapStateToProps = state => ({ + versionInfo: getVersionInfo(state), + isReadOnly: getIsCertified(state) || isWorkflowArchive(state) +}); + +const mapDispatchToProps = dispatch => ({ + onDataChange: payload => + dispatch(workflowVersionDetailsChangedAction(payload)) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(GeneralView); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/general/GeneralView.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/general/GeneralView.js new file mode 100644 index 00000000..e069ffb0 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/general/GeneralView.js @@ -0,0 +1,61 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { I18n } from 'react-redux-i18n'; + +import Description from 'shared/components/Description'; +import { VersionInfo, LabeledValue } from 'shared/components/VersionInfo'; + +const GeneralView = ({ onDataChange, versionInfo, isReadOnly }) => { + const modifiedValue = I18n.l(versionInfo.modificationTime, { + dateFormat: 'date.short' + }); + const createdValue = I18n.l(versionInfo.creationTime, { + dateFormat: 'date.short' + }); + + return ( + <div className="general-page"> + <div className="general-page-content"> + <Description + description={versionInfo.description} + onDataChange={onDataChange} + disabled={isReadOnly} + /> + <VersionInfo> + <LabeledValue + title={I18n.t('workflow.general.created')} + value={createdValue} + /> + <LabeledValue + title={I18n.t('workflow.general.modified')} + value={modifiedValue} + /> + </VersionInfo> + </div> + </div> + ); +}; + +GeneralView.propTypes = { + onDataChange: PropTypes.func, + versionInfo: PropTypes.object, + isReadOnly: PropTypes.bool +}; + +export default GeneralView; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/general/generalSelectors.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/general/generalSelectors.js new file mode 100644 index 00000000..cc817fc2 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/general/generalSelectors.js @@ -0,0 +1,33 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { createSelector } from 'reselect'; + +import { versionState } from 'features/version/versionConstants'; + +export const getVersionInfo = state => state && state.currentVersion.general; + +export const getVersionsState = createSelector( + state => state && state.currentVersion.general.state +); + +export const getIsCertified = createSelector( + getVersionInfo, + versionInfo => + versionInfo && + versionInfo.state && + versionInfo.state.toLowerCase() === versionState.CERTIFIED +); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/InputOutput.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/InputOutput.js new file mode 100644 index 00000000..c9cd5756 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/InputOutput.js @@ -0,0 +1,83 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http: //www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { connect } from 'react-redux'; +import { + showAlertModalAction, + hideModalAction +} from 'shared/modal/modalWrapperActions'; + +import { + getIsShowInputs, + getSearch, + getDataRows, + getTypes, + getError +} from 'features/version/inputOutput/inputOutputSelectors'; +import { getIsCertified } from 'features/version/general/generalSelectors'; +import { + changeError, + showInputs, + showOutputs, + search, + add, + changeName, + changeType, + changeMandatory, + remove +} from 'features/version/inputOutput/inputOutputActions'; +import { isWorkflowArchive } from 'features/workflow/workflowSelectors'; +import InputOutputView from 'features/version/inputOutput/InputOutputView'; + +const mapStateToProps = state => ({ + isShowInputs: getIsShowInputs(state), + search: getSearch(state), + dataRows: getDataRows(state), + types: getTypes(state), + error: getError(state), + isReadOnly: getIsCertified(state) || isWorkflowArchive(state) +}); + +const mapDispatchToProps = dispatch => ({ + handleChangeError: payload => dispatch(changeError(payload)), + handleShowInputs: () => dispatch(showInputs()), + handleShowOutputs: () => dispatch(showOutputs()), + handleSearch: value => dispatch(search(value)), + handleAdd: () => dispatch(add()), + handleNameChange: (name, key) => dispatch(changeName(name, key)), + handleTypeChange: (type, key) => dispatch(changeType(type, key)), + handleMandatoryChange: (mandatory, key) => + dispatch(changeMandatory(mandatory, key)), + handleRemove: (alertProps, key) => { + if (alertProps) { + return dispatch( + showAlertModalAction({ + ...alertProps, + withButtons: true, + actionButtonClick: () => + dispatch(hideModalAction()) && dispatch(remove(key)) + }) + ); + } + + return dispatch(remove(key)); + } +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(InputOutputView); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/InputOutputView.jsx b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/InputOutputView.jsx new file mode 100644 index 00000000..61e34990 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/InputOutputView.jsx @@ -0,0 +1,234 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http: //www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { Translate, I18n } from 'react-redux-i18n'; +import cn from 'classnames'; +import { SVGIcon } from 'onap-ui-react'; + +import Scrollbars from 'shared/scroll/Scrollbars'; +import SearchInput from 'shared/searchInput/SearchInput'; +import { getValidationsError } from 'features/version/inputOutput/inputOutputValidations'; +import Tab from 'features/version/inputOutput/views/Tab'; +import TableHead from 'features/version/inputOutput/views/TableHead'; +import TableBody from 'features/version/inputOutput/views/TableBody'; +import NoDataRow from 'features/version/inputOutput/views/NoDataRow'; +import DataRow from 'features/version/inputOutput/views/DataRow'; + +class InputOutputView extends React.Component { + componentDidUpdate() { + const { dataRows, error, handleChangeError } = this.props; + + const validationsError = getValidationsError(dataRows); + + const isDiff = Object.keys(validationsError).some(errorKey => { + if (!error.hasOwnProperty(errorKey)) { + return true; + } + + return ( + JSON.stringify(validationsError[errorKey].sort()) !== + JSON.stringify(error[errorKey].sort()) + ); + }); + + if (isDiff) { + handleChangeError(validationsError); + } + } + + handleInputsTabClick = () => { + if (!this.props.isShowInputs) { + this.props.handleShowInputs(); + } + }; + + handleOutputsTabClick = () => { + if (this.props.isShowInputs) { + this.props.handleShowOutputs(); + } + }; + + handleSearchChange = value => { + this.props.handleSearch(value); + }; + + handleNameChange = key => name => { + this.props.handleNameChange(name, key); + }; + + handleTypeChange = key => event => { + this.props.handleTypeChange(event.target.value, key); + }; + + handleMandatoryChange = key => value => { + this.props.handleMandatoryChange(value, key); + }; + + handleRemoveClick = key => () => { + const { name } = this.props.dataRows[key]; + + let alertProps = false; + + if (name.replace(/\s+/g, '')) { + const title = I18n.t('workflow.inputOutput.DELETE'); + const body = I18n.t('workflow.inputOutput.confirmDelete', { + name: name.replace(/s+$/g, '') + }); + const closeButtonText = I18n.t('workflow.inputOutput.CANCEL'); + const actionButtonText = title; + + alertProps = { + title, + body, + closeButtonText, + actionButtonText + }; + } + + this.props.handleRemove(alertProps, key); + }; + + renderDataRows = () => { + const { dataRows, types, error } = this.props; + + if (dataRows.length < 1) { + return ( + <NoDataRow> + <Translate value="workflow.inputOutput.noData" /> + </NoDataRow> + ); + } + + return dataRows.map((data, i) => { + let errorMessage = ''; + + if ( + error.invalidCharacters && + error.invalidCharacters.includes(i) + ) { + errorMessage = I18n.t( + 'workflow.errorMessages.invalidCharacters' + ); + } else if (error.alreadyExists && error.alreadyExists.includes(i)) { + errorMessage = I18n.t('workflow.errorMessages.alreadyExists'); + } else if (error.emptyName && error.emptyName.includes(i)) { + errorMessage = I18n.t('workflow.errorMessages.emptyName'); + } + + return ( + <DataRow + key={`data.${i}`} + data={data} + types={types} + nameErrorMessage={errorMessage} + dataTestId="wf-input-output-row" + handleNameChange={this.handleNameChange(i)} + handleTypeChange={this.handleTypeChange(i)} + handleMandatoryChange={this.handleMandatoryChange(i)} + handleRemoveClick={this.handleRemoveClick(i)} + /> + ); + }); + }; + + render() { + const { isShowInputs, search, handleAdd, isReadOnly } = this.props; + + const addLabel = isShowInputs + ? I18n.t('workflow.inputOutput.addInput') + : I18n.t('workflow.inputOutput.addOutput'); + + const dataRowsView = this.renderDataRows(); + + return ( + <div className="input-output"> + <div className="input-output__header"> + <Tab + isActive={isShowInputs} + dataTestId="wf-input-output-inputs" + handleTabClick={this.handleInputsTabClick}> + <Translate value="workflow.inputOutput.inputs" /> + </Tab> + <Tab + isActive={!isShowInputs} + dataTestId="wf-input-output-outputs" + handleTabClick={this.handleOutputsTabClick}> + <Translate value="workflow.inputOutput.outputs" /> + </Tab> + <div className="input-output__header__right"> + <div className="input-output__search"> + <SearchInput + dataTestId="wf-input-output-search" + onChange={this.handleSearchChange} + value={search} + /> + </div> + <div + className={cn('input-output__add', { + disabled: isReadOnly + })} + data-test-id="wf-input-output-add" + onClick={handleAdd}> + <SVGIcon + label={addLabel} + labelPosition="right" + color="primary" + name="plusThin" + /> + </div> + </div> + </div> + <div className="input-output__table"> + <TableHead /> + <TableBody isReadOnly={isReadOnly}> + <Scrollbars className="scrollbars"> + {dataRowsView} + </Scrollbars> + </TableBody> + </div> + </div> + ); + } +} + +InputOutputView.propTypes = { + isShowInputs: PropTypes.bool, + search: PropTypes.string, + dataRows: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string, + type: PropTypes.string, + mandatory: PropTypes.bool + }) + ), + types: PropTypes.array, + error: PropTypes.object, + isReadOnly: PropTypes.bool, + handleChangeError: PropTypes.func, + handleShowInputs: PropTypes.func, + handleShowOutputs: PropTypes.func, + handleSearch: PropTypes.func, + handleAdd: PropTypes.func, + handleCurrentDataChange: PropTypes.func, + handleNameChange: PropTypes.func, + handleTypeChange: PropTypes.func, + handleMandatoryChange: PropTypes.func, + handleRemove: PropTypes.func +}; + +export default InputOutputView; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/__tests__/inputOutputActions-test.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/__tests__/inputOutputActions-test.js new file mode 100644 index 00000000..465602fc --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/__tests__/inputOutputActions-test.js @@ -0,0 +1,150 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { + NAME_MAX_LEN, + STRING, + SET_INPUTS_OUTPUTS, + CHANGE_ERROR, + SHOW_INPUTS, + SHOW_OUTPUTS, + SEARCH, + ADD, + CHANGE_NAME, + CHANGE_TYPE, + CHANGE_MANDATORY, + REMOVE +} from 'features/version/inputOutput/inputOutputConstants'; +import { + setInputsOutputs, + changeError, + showInputs, + showOutputs, + search, + add, + changeName, + changeType, + changeMandatory, + remove +} from 'features/version/inputOutput/inputOutputActions'; + +describe('Input/Output Actions', () => { + it('should have `setInputsOutputs` action', () => { + const inputs = [ + { + name: 'Input', + type: STRING, + mandatory: false + } + ]; + + const outputs = [ + { + name: 'Output', + type: STRING, + mandatory: false + } + ]; + + const expected = { + type: SET_INPUTS_OUTPUTS, + payload: { + inputs, + outputs + } + }; + + expect(setInputsOutputs({ inputs, outputs })).toEqual(expected); + }); + + it('should have `changeError` action', () => { + const payload = { key: 'value' }; + + const expected = { type: CHANGE_ERROR, payload }; + + expect(changeError(payload)).toEqual(expected); + }); + + it('should have `showInputs` action', () => { + const expected = { type: SHOW_INPUTS }; + + expect(showInputs()).toEqual(expected); + }); + + it('should have `showOutputs` action', () => { + const expected = { type: SHOW_OUTPUTS }; + + expect(showOutputs()).toEqual(expected); + }); + + it('should have `search` action', () => { + const payload = 'Search Value'; + + const expected = { type: SEARCH, payload }; + + expect(search(payload)).toEqual(expected); + }); + + it('should have `add` action', () => { + const expected = { type: ADD }; + + expect(add()).toEqual(expected); + }); + + it('should have `changeName` action', () => { + let name = 'This is a long name more that more more more and more'; + let key = 1; + + const expected = { + type: CHANGE_NAME, + payload: { + name: name.substr(0, NAME_MAX_LEN), + key + } + }; + + expect(changeName(name, key)).toEqual(expected); + }); + + it('should have `changeType` action', () => { + const type = 'String'; + const key = 1; + + const expected = { type: CHANGE_TYPE, payload: { type, key } }; + + expect(changeType(type, key)).toEqual(expected); + }); + + it('should have `changeMandatory` action', () => { + const mandatory = true; + const key = 1; + + const expected = { + type: CHANGE_MANDATORY, + payload: { mandatory, key } + }; + + expect(changeMandatory(mandatory, key)).toEqual(expected); + }); + + it('should have `remove` action', () => { + const payload = 1; + + const expected = { type: REMOVE, payload }; + + expect(remove(payload)).toEqual(expected); + }); +}); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/__tests__/inputOutputReducer-test.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/__tests__/inputOutputReducer-test.js new file mode 100644 index 00000000..b923f4a4 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/__tests__/inputOutputReducer-test.js @@ -0,0 +1,191 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { + STRING, + INPUTS, + OUTPUTS +} from 'features/version/inputOutput/inputOutputConstants'; +import inputOutputReducer, { + initialState, + defaultInputOutput +} from 'features/version/inputOutput/inputOutputReducer'; +import { + setInputsOutputs, + changeError, + showInputs, + showOutputs, + search, + add, + changeName, + changeType, + changeMandatory, + remove +} from 'features/version/inputOutput/inputOutputActions'; + +describe('Input/Output Reducer', () => { + it('should return initialState', () => { + expect(inputOutputReducer(undefined, {})).toEqual(initialState); + }); + + it('should set inputs/outputs', () => { + const payload = { + inputs: [ + { + name: 'Input', + type: STRING, + mandatory: false + } + ], + outputs: { + name: 'Output', + type: STRING, + mandatory: false + } + }; + + expect( + inputOutputReducer(undefined, setInputsOutputs(payload)) + ).toEqual({ ...initialState, ...payload }); + }); + + it('should change input/output error', () => { + const payload = { + alreadyExists: [1, 2], + invalidCharacteres: [3, 4] + }; + [INPUTS, OUTPUTS].forEach(current => { + const state = { ...initialState, current }; + expect(inputOutputReducer(state, changeError(payload))).toEqual({ + ...state, + error: { + ...state.error, + [current]: payload + } + }); + }); + }); + + it('should show inputs', () => { + expect(inputOutputReducer(undefined, showInputs())).toEqual({ + ...initialState, + current: INPUTS + }); + }); + + it('should show outputs', () => { + expect(inputOutputReducer(undefined, showOutputs())).toEqual({ + ...initialState, + current: OUTPUTS + }); + }); + + it('should add input/output', () => { + [INPUTS, OUTPUTS].forEach(current => { + const state = { ...initialState, current }; + expect(inputOutputReducer(state, add())).toEqual({ + ...state, + [current]: [...state[current], defaultInputOutput[current]] + }); + }); + }); + + it('should add search', () => { + const payload = 'Search string'; + expect(inputOutputReducer(undefined, search(payload))).toEqual({ + ...initialState, + search: payload + }); + }); + + it('should change input/output name/type/mandatory', () => { + const name = 'New name'; + const type = 'New Type'; + const mandatory = true; + const key = 0; + const state = { + ...initialState, + [INPUTS]: [ + { + name: 'Old name', + type: 'Old type', + mandatory: false + } + ], + [OUTPUTS]: [ + { + name: 'Old name', + type: 'Old type', + mandatory: false + } + ] + }; + [INPUTS, OUTPUTS].forEach(current => { + [ + { + action: changeName(name, key), + field: 'name', + value: name + }, + { + action: changeType(type, key), + field: 'type', + value: type + }, + { + action: changeMandatory(mandatory, key), + field: 'mandatory', + value: mandatory + } + ].forEach(actionMap => { + const actual = inputOutputReducer( + { ...state, current }, + actionMap.action + )[current][key][actionMap.field]; + + const expected = actionMap.value; + + expect(actual).toEqual(expected); + }); + }); + }); + + it('should remove input/output ', () => { + const key = 0; + const state = { + ...initialState, + [INPUTS]: [ + { + name: 'Name', + type: 'String', + mandatory: true + } + ], + [OUTPUTS]: [ + { + name: 'Name', + type: 'String', + mandatory: true + } + ] + }; + [INPUTS, OUTPUTS].forEach(current => { + expect( + inputOutputReducer({ ...state, current }, remove(key))[current] + ).toEqual([]); + }); + }); +}); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/__tests__/inputOutputSelectors-test.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/__tests__/inputOutputSelectors-test.js new file mode 100644 index 00000000..8e9bdbd4 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/__tests__/inputOutputSelectors-test.js @@ -0,0 +1,149 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { + getInputOutput, + getCurrent, + getIsShowInputs, + getSearch, + getDataRows, + getTypes, + getError +} from 'features/version/inputOutput/inputOutputSelectors'; + +describe('Input/Output Selectors', () => { + const state = { + currentVersion: { + general: { + id: '1e659854c7e240c881f1dd8d5bd833cc', + name: '1.0', + description: 'Initial version', + baseId: null, + creationTime: '2018-07-19T13:09:39.066+0000', + modificationTime: '2018-07-19T13:09:39.355+0000', + state: 'DRAFT', + inputs: [], + outputs: [] + }, + inputOutput: { + current: 'outputs', + inputs: [ + { + name: 'IP Address', + value: 'String', + mandatory: true, + type: 'Integer' + }, + { + name: 'MAC Address', + value: 'String', + mandatory: false, + type: 'Integer' + }, + { + name: 'IP', + value: 'String', + mandatory: true, + type: 'Boolean' + }, + { + name: 'IP', + value: 'String', + mandatory: false + }, + { + name: '', + value: 'String', + mandatory: false + }, + { + name: '', + value: 'String', + mandatory: false + } + ], + outputs: [ + { + name: 'IP Address', + value: 'String', + mandatory: true + }, + { + name: 'IP', + value: 'String', + mandatory: true + }, + { + name: 'IP', + value: 'String', + mandatory: false, + type: 'Boolean' + } + ], + search: 'IP', + types: ['String', 'Boolean', 'Integer', 'Float'], + error: { + inputs: { + alreadyExists: [1, 2], + invalidCharacters: [] + }, + outputs: { + alreadyExists: [1, 2], + invalidCharacters: [] + } + } + } + } + }; + + it('should `getInputOutput`', () => { + expect(getInputOutput(state)).toEqual(state.currentVersion.inputOutput); + }); + + it('should `getCurrent`', () => { + expect(getCurrent(state)).toEqual( + state.currentVersion.inputOutput.current + ); + }); + + it('should `getIsShowInputs`', () => { + expect(getIsShowInputs(state)).toBeFalsy(); + }); + + it('should `getSearch`', () => { + expect(getSearch(state)).toEqual( + state.currentVersion.inputOutput.search + ); + }); + + it('should `getDataRows`', () => { + expect(getDataRows(state)).toEqual( + state.currentVersion.inputOutput.outputs + ); + }); + + it('should `getTypes`', () => { + expect(getTypes(state)).toEqual(state.currentVersion.inputOutput.types); + }); + + it('should `getError`', () => { + expect(getError(state)).toEqual( + state.currentVersion.inputOutput.error[ + state.currentVersion.inputOutput.current + ] + ); + }); +}); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/inputOutputActions.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/inputOutputActions.js new file mode 100644 index 00000000..a9548592 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/inputOutputActions.js @@ -0,0 +1,53 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { createActions } from 'redux-actions'; + +import { + NAME_MAX_LEN, + NAMESPACE +} from 'features/version/inputOutput/inputOutputConstants'; + +export const { + [NAMESPACE]: { + setInputsOutputs, + changeError, + showInputs, + showOutputs, + search, + add, + changeName, + changeType, + changeMandatory, + remove + } +} = createActions({ + [NAMESPACE]: { + SET_INPUTS_OUTPUTS: undefined, + CHANGE_ERROR: undefined, + SHOW_INPUTS: undefined, + SHOW_OUTPUTS: undefined, + SEARCH: undefined, + ADD: undefined, + CHANGE_NAME: (name, key) => ({ + name: name.substr(0, NAME_MAX_LEN), + key + }), + CHANGE_TYPE: (type, key) => ({ type, key }), + CHANGE_MANDATORY: (mandatory, key) => ({ mandatory, key }), + REMOVE: undefined + } +}); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/inputOutputConstants.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/inputOutputConstants.js new file mode 100644 index 00000000..30f80a6b --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/inputOutputConstants.js @@ -0,0 +1,39 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +export const NAME_MAX_LEN = 50; + +export const INPUTS = 'inputs'; +export const OUTPUTS = 'outputs'; + +export const STRING = 'string'; +export const DEFAULT_STRING = 'STRING'; +export const BOOLEAN = 'boolean'; +export const INTEGER = 'integer'; +export const FLOAT = 'float'; + +export const NAMESPACE = 'inputOutput'; + +export const SET_INPUTS_OUTPUTS = `${NAMESPACE}/SET_INPUTS_OUTPUTS`; +export const CHANGE_ERROR = `${NAMESPACE}/CHANGE_ERROR`; +export const SHOW_INPUTS = `${NAMESPACE}/SHOW_INPUTS`; +export const SHOW_OUTPUTS = `${NAMESPACE}/SHOW_OUTPUTS`; +export const SEARCH = `${NAMESPACE}/SEARCH`; +export const ADD = `${NAMESPACE}/ADD`; +export const CHANGE_NAME = `${NAMESPACE}/CHANGE_NAME`; +export const CHANGE_TYPE = `${NAMESPACE}/CHANGE_TYPE`; +export const CHANGE_MANDATORY = `${NAMESPACE}/CHANGE_MANDATORY`; +export const REMOVE = `${NAMESPACE}/REMOVE`; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/inputOutputReducer.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/inputOutputReducer.js new file mode 100644 index 00000000..881322fa --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/inputOutputReducer.js @@ -0,0 +1,130 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { + INPUTS, + OUTPUTS, + STRING, + DEFAULT_STRING, + BOOLEAN, + INTEGER, + FLOAT, + SET_INPUTS_OUTPUTS, + CHANGE_ERROR, + SHOW_INPUTS, + SHOW_OUTPUTS, + SEARCH, + ADD, + CHANGE_NAME, + CHANGE_TYPE, + CHANGE_MANDATORY, + REMOVE +} from 'features/version/inputOutput/inputOutputConstants'; + +export const defaultInputOutput = { + [INPUTS]: { + name: '', + type: DEFAULT_STRING, + mandatory: false + }, + [OUTPUTS]: { + name: '', + type: DEFAULT_STRING, + mandatory: false + } +}; + +export const initialState = { + current: INPUTS, + [INPUTS]: [], + [OUTPUTS]: [], + search: '', + types: [STRING, BOOLEAN, INTEGER, FLOAT], + error: { + [INPUTS]: {}, + [OUTPUTS]: {} + } +}; + +const inputOutputReducer = (state = initialState, action) => { + const { type, payload } = action; + switch (type) { + case SET_INPUTS_OUTPUTS: + return { + ...initialState, + ...payload + }; + + case CHANGE_ERROR: + return { + ...state, + error: { + ...state.error, + [state.current]: payload + } + }; + + case SHOW_INPUTS: + return { ...state, current: INPUTS }; + + case SHOW_OUTPUTS: + return { ...state, current: OUTPUTS }; + + case SEARCH: + return { ...state, search: payload }; + + case ADD: + return { + ...state, + [state.current]: [ + ...state[state.current], + defaultInputOutput[state.current] + ] + }; + + /* eslint-disable no-case-declarations */ + case CHANGE_NAME: + case CHANGE_TYPE: + case CHANGE_MANDATORY: + const { key, ...rest } = payload; + return { + ...state, + [state.current]: state[state.current].map( + (row, index) => + key === index + ? { + ...row, + ...rest + } + : row + ) + }; + /* eslint-enable no-case-declarations */ + + case REMOVE: + return { + ...state, + [state.current]: state[state.current].filter( + (_, index) => index !== payload + ) + }; + + default: + return state; + } +}; + +export default inputOutputReducer; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/inputOutputSelectors.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/inputOutputSelectors.js new file mode 100644 index 00000000..3a6c9e8f --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/inputOutputSelectors.js @@ -0,0 +1,106 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { createSelector } from 'reselect'; +import isEmpty from 'lodash.isempty'; + +import { INPUTS } from 'features/version/inputOutput/inputOutputConstants'; + +export const getInputOutput = state => state.currentVersion.inputOutput; +export const getInputs = createSelector(getInputOutput, data => data.inputs); +export const getOutputs = createSelector(getInputOutput, data => data.outputs); +export const getInputOutputForComposition = state => ({ + inputs: getInputs(state).map(item => ({ + ...item, + type: item.type.toLowerCase() + })), + outputs: getOutputs(state).map(item => ({ + ...item, + type: item.type.toLowerCase() + })) +}); +export const getCurrent = createSelector( + getInputOutput, + inputOutput => inputOutput.current +); + +export const getIsShowInputs = createSelector( + getCurrent, + current => current === INPUTS +); + +export const getSearch = createSelector( + getInputOutput, + inputOutput => inputOutput.search +); + +export const getDataRows = createSelector( + [getInputOutput, getCurrent], + (inputOutput, current) => { + if (inputOutput.search) { + return inputOutput[current].filter(dataRow => + dataRow.name + .toLowerCase() + .includes(inputOutput.search.toLowerCase()) + ); + } + + return inputOutput[current]; + } +); + +export const getTypes = createSelector( + getInputOutput, + inputOutput => inputOutput.types +); + +export const getError = createSelector( + [getInputOutput, getCurrent], + (inputOutput, current) => inputOutput.error[current] +); + +export const getErrorsInputOutput = createSelector( + getInputOutput, + ({ error }) => error +); + +export const getInputErrors = createSelector( + getErrorsInputOutput, + ({ inputs }) => + !isEmpty(inputs) && + Boolean( + inputs.alreadyExists.length || + inputs.invalidCharacters.length || + inputs.emptyName.length + ) +); + +export const getOutputErrors = createSelector( + getErrorsInputOutput, + ({ outputs }) => + !isEmpty(outputs) && + Boolean( + outputs.alreadyExists.length || + outputs.invalidCharacters.length || + outputs.emptyName.length + ) +); + +export const getIOErrors = createSelector( + getInputErrors, + getOutputErrors, + (inputsErrors, outputsErrors) => inputsErrors || outputsErrors +); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/inputOutputValidations.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/inputOutputValidations.js new file mode 100644 index 00000000..d4057879 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/inputOutputValidations.js @@ -0,0 +1,64 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +export const getValidationsError = dataRows => { + const error = {}; + + const groupBy = dataRows.reduce((result, value, key) => { + const groupKey = value.name.toLowerCase(); + + if (groupKey) { + if (result.hasOwnProperty(groupKey)) { + result[groupKey].push(key); + } else { + result[groupKey] = [key]; + } + } + return result; + }, {}); + + error.alreadyExists = Object.keys(groupBy).reduce((result, value) => { + if (groupBy[value].length > 1) { + result = [...result, ...groupBy[value]]; + } + + return result; + }, []); + + error.emptyName = dataRows.reduce((result, value, key) => { + const name = value.name; + + if (!name) { + result.push(key); + } + + return result; + }, []); + + error.invalidCharacters = dataRows.reduce((result, value, key) => { + const groupKey = value.name; + + if (groupKey) { + if (!/^[\w\d]+$/.test(groupKey)) { + result.push(key); + } + } + + return result; + }, []); + + return error; +}; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/views/DataRow.jsx b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/views/DataRow.jsx new file mode 100644 index 00000000..70103f87 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/views/DataRow.jsx @@ -0,0 +1,87 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import { Input, Checkbox, SVGIcon } from 'onap-ui-react'; + +const DataRow = ({ + data: { name, type, mandatory }, + types, + nameErrorMessage, + dataTestId, + handleNameChange, + handleNameBlur, + handleTypeChange, + handleMandatoryChange, + handleRemoveClick +}) => ( + <div className="input-output__tr"> + <div className="input-output__td"> + <Input + errorMessage={nameErrorMessage} + data-test-id={`${dataTestId}-name`} + onChange={handleNameChange} + onBlur={handleNameBlur} + type="text" + value={name} + /> + </div> + <div className="input-output__td"> + <select + className="input-output-select" + value={type} + data-test-id={`${dataTestId}-select`} + onChange={handleTypeChange}> + {types.map((type, i) => ( + <option key={`type.${i}`} value={type.toUpperCase()}> + {type} + </option> + ))} + </select> + </div> + <div className="input-output__td input-output__td--unflex"> + <Checkbox + value="myVal" + data-test-id={`${dataTestId}-mandatory`} + onChange={handleMandatoryChange} + checked={mandatory} + /> + </div> + <div className="input-output__td input-output__td--unflex input-output__td--icon"> + <SVGIcon + name="trashO" + data-test-id={`${dataTestId}-delete`} + onClick={handleRemoveClick} + /> + </div> + </div> +); + +DataRow.propTypes = { + data: PropTypes.object, + types: PropTypes.array, + nameErrorMessage: PropTypes.string, + dataTestId: PropTypes.string, + handleNameChange: PropTypes.func, + handleNameBlur: PropTypes.func, + handleTypeChange: PropTypes.func, + handleMandatoryChange: PropTypes.func, + handleRemoveClick: PropTypes.func +}; + +export default DataRow; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/views/NoDataRow.jsx b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/views/NoDataRow.jsx new file mode 100644 index 00000000..af75c79e --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/views/NoDataRow.jsx @@ -0,0 +1,32 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; + +const NoDataRow = ({ children }) => ( + <div className="input-output__tr input-output__tr--no-hover"> + <div className="input-output__td input-output__td--empty"> + {children} + </div> + </div> +); + +NoDataRow.propTypes = { + children: PropTypes.node +}; + +export default NoDataRow; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/views/Tab.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/views/Tab.js new file mode 100644 index 00000000..ad56dc15 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/views/Tab.js @@ -0,0 +1,43 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import cn from 'classnames'; + +const Tab = ({ children, isActive, dataTestId, handleTabClick }) => { + const className = cn('input-output__tab', { + 'input-output__tab--active': isActive + }); + + return ( + <div + className={className} + data-test-id={`${dataTestId}-tab`} + onClick={handleTabClick}> + {children} + </div> + ); +}; + +Tab.propTypes = { + children: PropTypes.node, + isActive: PropTypes.bool, + dataTestId: PropTypes.string, + handleTabClick: PropTypes.func +}; + +export default Tab; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/views/TableBody.jsx b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/views/TableBody.jsx new file mode 100644 index 00000000..ed11bbc5 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/views/TableBody.jsx @@ -0,0 +1,50 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http: //www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +/* eslint-disable no-unused-vars */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import cn from 'classnames'; + +class TableBody extends React.Component { + handleNameInputChange = params => { + console.log('handleNameInputChange', { params }); + }; + + handleMandatoryCheckboxChange = params => { + console.log('handleMandatoryCheckboxChange: ', { params }); + }; + + render() { + const { isReadOnly, children } = this.props; + + return ( + <div + className={cn('input-output__table__tbody', { + disabled: isReadOnly + })}> + {children} + </div> + ); + } +} + +TableBody.propTypes = { + isReadOnly: PropTypes.bool, + children: PropTypes.node +}; + +export default TableBody; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/views/TableHead.jsx b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/views/TableHead.jsx new file mode 100644 index 00000000..37a8cb43 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/inputOutput/views/TableHead.jsx @@ -0,0 +1,41 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http: //www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import React from 'react'; +import { Translate } from 'react-redux-i18n'; + +export default class TableHead extends React.Component { + render() { + return ( + <div className="input-output__table__thead"> + <div className="input-output__tr input-output__tr--no-hover"> + <div className="input-output__th"> + <Translate value="workflow.inputOutput.name" /> + </div> + <div className="input-output__th"> + <Translate value="workflow.inputOutput.type" /> + </div> + <div className="input-output__th input-output__th--unflex"> + <Translate value="workflow.inputOutput.mandatory" /> + </div> + <div className="input-output__th input-output__th--unflex input-output__th--icon"> + ••• + </div> + </div> + </div> + ); + } +} diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionApi.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionApi.js new file mode 100644 index 00000000..b3a2e13a --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionApi.js @@ -0,0 +1,82 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import RestfulAPIUtil from 'services/restAPIUtil'; +import Configuration from 'config/Configuration.js'; +import { CERTIFY_JSON } from 'features/version/versionController/versionControllerConstants'; + +function baseUrl(workflowId) { + const restPrefix = Configuration.get('restPrefix'); + return `${restPrefix}/workflows/${workflowId}/versions`; +} + +const Api = { + fetchVersion: ({ workflowId, versionId }) => { + return RestfulAPIUtil.fetch(`${baseUrl(workflowId)}/${versionId}`); + }, + createNewVersion: ({ workflowId, baseId, description }) => { + const urlParams = baseId ? `?baseVersionId=${baseId}` : ``; + return RestfulAPIUtil.post(`${baseUrl(workflowId)}${urlParams}`, { + description + }); + }, + updateVersion: ({ workflowId, ...payload }) => { + return RestfulAPIUtil.put( + `${baseUrl(workflowId)}/${payload.params.id}`, + { + ...payload.params + } + ); + }, + fetchVersionArtifact: ({ workflowId, versionId }) => { + return RestfulAPIUtil.fetch( + `${baseUrl(workflowId)}/${versionId}/artifact` + ); + }, + updateVersionArtifact: ({ + workflowId, + versionId, + workflowName, + versionName, + payload + }) => { + let formData = new FormData(); + var blob = new Blob([payload], { type: 'text/xml' }); + formData.append( + 'fileToUpload', + blob, + `${workflowName}-${versionName}.bpmn` + ); + + return RestfulAPIUtil.put( + `${baseUrl(workflowId)}/${versionId}/artifact`, + formData + ); + }, + deleteVersionArtifact: ({ workflowId, versionId }) => { + return RestfulAPIUtil.delete( + `${baseUrl(workflowId)}/${versionId}/artifact` + ); + }, + certifyVersion: ({ workflowId, versionId }) => { + return RestfulAPIUtil.post( + `${baseUrl(workflowId)}/${versionId}/state`, + CERTIFY_JSON + ); + } +}; + +export default Api; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionConstants.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionConstants.js new file mode 100644 index 00000000..5b945a02 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionConstants.js @@ -0,0 +1,59 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { createAction } from 'redux-actions'; + +export const SET_CURRENT_VERSION = 'workflow/version/SET_CURRENT_VERSION'; +export const FETCH_REQUESTED = 'workflow/version/FETCH_REQUESTED'; +export const DETAILS_CHANGED = 'workflow/version/DETAILS_CHANGED'; +export const FETCH_REQUESTED_FAILED = 'workflow/version/FETCH_REQUESTED_FAILED'; +export const VERSION_STATE_CHANGED = 'workflow/version/VERSION_STATE_CHANGED'; +export const TOGGLE_COMPOSITION_UPDATE = + 'workflow/version/TOGGLE_COMPOSITION_UPDATE'; +export const SET_OPERRATION_MODE = 'workflow/version/SET_OPERRATION_MODE'; + +export const workflowVersionFetchRequestedAction = createAction( + FETCH_REQUESTED +); + +export const workflowVersionDetailsChangedAction = createAction( + DETAILS_CHANGED +); + +export const setWorkflowVersionAction = createAction(SET_CURRENT_VERSION); +export const fetchWorkflowVersionActionFailed = createAction( + FETCH_REQUESTED_FAILED, + error => error +); + +export const versionStateChangedAction = createAction( + VERSION_STATE_CHANGED, + payload => payload +); + +export const toggleCompositionUpdate = createAction( + TOGGLE_COMPOSITION_UPDATE, + payload => ({ isCompositionUpdating: payload }) +); + +export const setOperationModeAction = createAction(SET_OPERRATION_MODE); + +export const getIsCompositionUpdating = state => + state.currentVersion.general.isCompositionUpdating; + +export const versionState = { + DRAFT: 'draft', + CERTIFIED: 'certified' +}; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/VersionController.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/VersionController.js new file mode 100644 index 00000000..8c37a0e3 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/VersionController.js @@ -0,0 +1,73 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { connect } from 'react-redux'; +import { getSavedObjParams } from 'features/version/versionController/versionControllerSelectors'; +import VersionControllerView from 'features/version/versionController/VersionControllerView'; +import { + getVersions, + getSortedVersions +} from 'features/workflow/overview/overviewSelectors'; +import { + isWorkflowArchive, + getWorkflowId, + getWorkflowName +} from 'features/workflow/workflowSelectors'; +import { + saveParamsAction, + certifyVersionAction +} from 'features/version/versionController/versionControllerConstants'; +import { + workflowVersionFetchRequestedAction, + toggleCompositionUpdate, + getIsCompositionUpdating +} from 'features/version/versionConstants'; +import { getIsCertified } from 'features/version/general/generalSelectors'; +import { getIOErrors } from 'features/version/inputOutput/inputOutputSelectors'; +import { getCompositionHasErrors } from 'features/version/composition/compositionSelectors'; +import { pluginContextSelector } from 'wfapp/pluginContext/pluginContextSelector'; + +function mapStateToProps(state) { + return { + workflowName: getWorkflowName(state), + workflowId: getWorkflowId(state), + versionsList: getSortedVersions(state), + savedParams: getSavedObjParams(state), + hasErrors: getIOErrors(state) || getCompositionHasErrors(state), + isCertifyDisable: getIsCertified(state), + isArchive: isWorkflowArchive(state), + currentWorkflowVersion: state.currentVersion.general, + pluginContext: pluginContextSelector(state), + isCompositionUpdating: getIsCompositionUpdating(state) + }; +} + +function mapDispatchToProps(dispatch) { + return { + getVersions: () => dispatch(getVersions), + saveParamsToServer: params => dispatch(saveParamsAction(params)), + certifyVersion: payload => dispatch(certifyVersionAction(payload)), + changeVersion: payload => + dispatch(workflowVersionFetchRequestedAction(payload)), + toggleCompositionUpdate: payload => + dispatch(toggleCompositionUpdate(payload)) + }; +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(VersionControllerView); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/VersionControllerView.jsx b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/VersionControllerView.jsx new file mode 100644 index 00000000..730d92fb --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/VersionControllerView.jsx @@ -0,0 +1,171 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import ActionButtons from 'features/version/versionController/views/ActionButtons'; +import OperationModeButtons from 'features/version/versionController/views/OperationModeButtons'; +import VersionContainer from 'features/version/versionController/views/VersionsContainer'; +import WorkflowTitle from 'features/version/versionController/views/WorkflowTitle'; +import { PluginPubSub } from 'shared/pubsub/plugin-pubsub.ts'; +import { + notificationType, + CATALOG_PATH +} from 'wfapp/pluginContext/pluginContextConstants'; +export default class VersionControllerView extends Component { + static propTypes = { + location: PropTypes.object, + workflowName: PropTypes.string, + currentWorkflowVersion: PropTypes.object, + viewableVersions: PropTypes.arrayOf(Object), + getVersions: PropTypes.func, + versionsList: PropTypes.array, + history: PropTypes.object, + getOverview: PropTypes.func, + match: PropTypes.object, + savedParams: PropTypes.object, + saveParamsToServer: PropTypes.func, + workflowId: PropTypes.string, + certifyVersion: PropTypes.func, + changeVersion: PropTypes.func, + isCertifyDisable: PropTypes.bool, + hasErrors: PropTypes.bool, + isArchive: PropTypes.bool, + operationMode: PropTypes.bool, + pluginContext: PropTypes.object, + isCompositionUpdating: PropTypes.bool, + toggleCompositionUpdate: PropTypes.func + }; + + constructor(props) { + super(props); + } + + routeToOverview = () => { + const { history, match } = this.props; + const workflowId = match.params.workflowId; + history.push(`/workflows/workflow/${workflowId}/overview`); + }; + + sendSaveParamsToServer = () => { + const { + savedParams, + saveParamsToServer, + workflowId, + workflowName + } = this.props; + saveParamsToServer({ params: savedParams, workflowId, workflowName }); + }; + handleSendMsgToCatalog = () => { + const { + pluginContext: { eventsClientId, parentUrl }, + workflowId, + isCertifyDisable + } = this.props; + const client = new PluginPubSub(eventsClientId, parentUrl); + client.notify(notificationType.CLOSE, { + isCompleted: isCertifyDisable, + workflowId, + path: CATALOG_PATH + }); + }; + certifyVersion = () => { + const { + certifyVersion, + workflowId, + currentWorkflowVersion, + savedParams, + workflowName + } = this.props; + certifyVersion({ + workflowId, + workflowName, + versionId: currentWorkflowVersion.id, + params: savedParams + }); + }; + + versionChangeCallback = versionId => { + const { changeVersion, workflowId } = this.props; + changeVersion({ versionId, workflowId }); + }; + + undoClickCallback = () => { + const { + currentWorkflowVersion, + changeVersion, + workflowId + } = this.props; + changeVersion({ versionId: currentWorkflowVersion.id, workflowId }); + }; + + render() { + const { + currentWorkflowVersion, + workflowName, + versionsList, + hasErrors, + isCertifyDisable, + isArchive, + operationMode, + isCompositionUpdating, + toggleCompositionUpdate + } = this.props; + const isReadonly = isCertifyDisable || hasErrors || isArchive; + return ( + <div className="version-controller-bar"> + <WorkflowTitle workflowName={workflowName} /> + <div + className={`vc-container ${ + operationMode ? 'vs-container-operation' : '' + }`}> + {!operationMode && ( + <VersionContainer + currentWorkflowVersion={currentWorkflowVersion} + viewableVersions={versionsList} + onOverviewClick={this.routeToOverview} + onVersionSelectChange={this.versionChangeCallback} + isArchive={isArchive} + /> + )} + {operationMode && ( + <OperationModeButtons + sendMsgToCatalog={this.handleSendMsgToCatalog} + saveDisabled={isReadonly} + onSaveClick={this.sendSaveParamsToServer} + onCertifyClick={this.certifyVersion} + /> + )} + {!operationMode && ( + <ActionButtons + isCompositionUpdating={isCompositionUpdating} + saveDisabled={isReadonly} + onSaveClick={this.sendSaveParamsToServer} + certifyDisabled={isReadonly} + onCertifyClick={this.certifyVersion} + onUndoClick={this.undoClickCallback} + toggleCompositionUpdate={toggleCompositionUpdate} + /> + )} + </div> + </div> + ); + } +} + +VersionControllerView.defaultProps = { + getVersions: () => {} +}; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/__tests__/VersionControllerView_snapshot-test.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/__tests__/VersionControllerView_snapshot-test.js new file mode 100644 index 00000000..2bcfa300 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/__tests__/VersionControllerView_snapshot-test.js @@ -0,0 +1,59 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import React from 'react'; +import renderer from 'react-test-renderer'; + +import VersionsContainer from 'features/version/versionController/views/VersionsContainer'; + +describe('Version Controller View Snapshot', () => { + it('renders correctly', () => { + const versionList = [ + { + id: '7b5f6b086613470985082df2c0f6c713', + name: '1.0', + description: + 'Initial version, bug fix for previous version that fixed an exception when the port was occupied', + status: 'Draft', + state: 'Draft', + creationTime: 1530687330460, + modificationTime: 1530687330575, + archivedStatus: 'ACTIVE' + }, + { + id: '7b5f6b086613470985082df2c0f6c666', + name: '2.0', + description: + 'Test version, bug fix for previous version that fixed an exception when the port was occupied', + status: 'Draft', + state: 'Draft', + creationTime: 1530687330461, + modificationTime: 1530687330576, + archivedStatus: 'ACTIVE', + baseId: '7b5f6b086613470985082df2c0f6c713' + } + ]; + const tree = renderer + .create( + <VersionsContainer + viewableVersions={versionList} + currentWorkflowVersion={versionList[0]} + /> + ) + .toJSON(); + + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/__tests__/__snapshots__/VersionControllerView_snapshot-test.js.snap b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/__tests__/__snapshots__/VersionControllerView_snapshot-test.js.snap new file mode 100644 index 00000000..ea135fce --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/__tests__/__snapshots__/VersionControllerView_snapshot-test.js.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Version Controller View Snapshot renders correctly 1`] = ` +<div + className="version-section-wrapper" +> + <div + className="version-status-container" + > + version + <select + className="version-selector" + data-test-id="vc-versions-select-box" + onChange={[Function]} + value="7b5f6b086613470985082df2c0f6c713" + > + <option + data-test-id="vc-version-option" + value="7b5f6b086613470985082df2c0f6c713" + > + 1.0 DRAFT + </option> + <option + data-test-id="vc-version-option" + value="7b5f6b086613470985082df2c0f6c666" + > + 2.0 Draft + </option> + </select> + <span + className="version-selector-more-versions" + data-test-id="vc-versions-page-link" + onClick={undefined} + > + viewOverview + </span> + </div> +</div> +`; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/versionControllerConstants.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/versionControllerConstants.js new file mode 100644 index 00000000..57aef602 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/versionControllerConstants.js @@ -0,0 +1,29 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { createAction } from 'redux-actions'; +export const SAVE_ACTION = 'versionController/SAVE'; +export const CERTIFY_ACTION = 'versionController/CERTIFY'; +export const UNDO_ACTION = 'versionController/UNDO'; +export const CERTIFY_JSON = { + name: 'CERTIFIED' +}; + +export const saveParamsAction = createAction(SAVE_ACTION, payload => payload); +export const certifyVersionAction = createAction( + CERTIFY_ACTION, + payload => payload +); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/versionControllerSelectors.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/versionControllerSelectors.js new file mode 100644 index 00000000..19c8bdc2 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/versionControllerSelectors.js @@ -0,0 +1,36 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { createSelector } from 'reselect'; +import { + getInputs, + getOutputs +} from 'features/version/inputOutput/inputOutputSelectors'; +import { getVersionInfo } from 'features/version/general/generalSelectors'; +import { getComposition } from 'features/version/composition/compositionSelectors'; + +export const getSavedObjParams = createSelector( + getOutputs, + getInputs, + getComposition, + getVersionInfo, + (outputs, inputs, composition, general) => ({ + outputs, + inputs, + composition, + ...general + }) +); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/views/ActionButtons.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/views/ActionButtons.js new file mode 100644 index 00000000..0a9c2b1a --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/views/ActionButtons.js @@ -0,0 +1,92 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import React from 'react'; +import { I18n } from 'react-redux-i18n'; +import { Button } from 'onap-ui-react'; +import PropTypes from 'prop-types'; +import SvgButton from 'features/version/versionController/views/SvgButton'; +import CompositionUpdate from 'features/version/composition/CompositionUpdate'; + +const ActionButtons = props => { + const { + onSaveClick, + certifyDisabled, + onCertifyClick, + isCompositionUpdating, + toggleCompositionUpdate, + onUndoClick, + saveDisabled + } = props; + + return ( + <div className="save-submit-cancel-container"> + <div className="action-buttons"> + <div className="select-action-buttons"> + <div className={'separator vc-separator'} /> + <SvgButton + dataTestId="vc-save-btn" + name="version-controller-save" + tooltipText={I18n.t('buttons.saveBtn')} + disabled={saveDisabled} + onClick={onSaveClick} + /> + + <div className={'separator vc-separator'} /> + + <SvgButton + dataTestId="vc-undo-btn" + name="version-controller-undo" + tooltipText={I18n.t('buttons.undoBtn')} + disabled={certifyDisabled} + onClick={onUndoClick} + /> + + <div className={'separator vc-separator'} /> + + <Button + className="certifyBtn" + btnType="primary" + disabled={certifyDisabled} + onClick={() => toggleCompositionUpdate(true)}> + {I18n.t('buttons.certifyBtn')} + </Button> + + {isCompositionUpdating && ( + <CompositionUpdate + certifyBack={() => { + toggleCompositionUpdate(false); + onCertifyClick(); + }} + /> + )} + </div> + </div> + </div> + ); +}; + +ActionButtons.propTypes = { + onSaveClick: PropTypes.func, + certifyDisabled: PropTypes.bool, + onCertifyClick: PropTypes.func, + onUndoClick: PropTypes.func, + saveDisabled: PropTypes.bool, + isCompositionUpdating: PropTypes.bool, + toggleCompositionUpdate: PropTypes.func +}; + +export default ActionButtons; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/views/OperationModeButtons.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/views/OperationModeButtons.js new file mode 100644 index 00000000..4d992adc --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/views/OperationModeButtons.js @@ -0,0 +1,70 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import React from 'react'; +import { I18n } from 'react-redux-i18n'; +import { Button } from 'onap-ui-react'; +import PropTypes from 'prop-types'; +import SvgButton from 'features/version/versionController/views/SvgButton'; + +const OperationModeButtons = props => { + const { + onSaveClick, + saveDisabled, + sendMsgToCatalog, + onCertifyClick + } = props; + return ( + <div className="save-submit-cancel-container"> + <div className="action-buttons"> + <div className="select-action-buttons"> + <SvgButton + dataTestId="vc-save-btn" + name="version-controller-save" + tooltipText={I18n.t('buttons.saveBtn')} + disabled={saveDisabled} + onClick={onSaveClick} + /> + + <Button + disabled={saveDisabled} + className="certifyBtn" + btnType="primary" + onClick={onCertifyClick}> + {I18n.t('buttons.completeBtn')} + </Button> + + <SvgButton + tooltipText={I18n.t('buttons.backToCatalog')} + className="vs-back-btn" + dataTestId="vc-back-btn" + name="upload" + onClick={sendMsgToCatalog} + /> + </div> + </div> + </div> + ); +}; + +OperationModeButtons.propTypes = { + onSaveClick: PropTypes.func, + saveDisabled: PropTypes.bool, + sendMsgToCatalog: PropTypes.func, + onCertifyClick: PropTypes.func +}; + +export default OperationModeButtons; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/views/SvgButton.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/views/SvgButton.js new file mode 100644 index 00000000..41bdeb81 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/views/SvgButton.js @@ -0,0 +1,62 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import React from 'react'; +import { SVGIcon } from 'onap-ui-react'; +import PropTypes from 'prop-types'; + +const SvgButton = props => { + const { + className = '', + name, + tooltipText, + disabled, + onClick, + dataTestId, + actiontype + } = props; + let onClickAction = disabled ? () => {} : () => onClick(actiontype); + return ( + <div + className={`action-button-wrapper ${ + disabled ? 'disabled' : 'clickable' + }`} + onClick={onClickAction}> + <div className="action-buttons-svg"> + <SVGIcon + className={className} + label={tooltipText} + labelPosition="bottom" + labelClassName="action-button-label" + data-test-id={dataTestId} + name={name} + disabled={disabled} + /> + </div> + </div> + ); +}; + +SvgButton.propTypes = { + name: PropTypes.string, + tooltipText: PropTypes.string, + disabled: PropTypes.bool, + onClick: PropTypes.func, + dataTestId: PropTypes.string, + actiontype: PropTypes.string, + className: PropTypes.string +}; + +export default SvgButton; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/views/VersionButton.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/views/VersionButton.js new file mode 100644 index 00000000..19e148cb --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/views/VersionButton.js @@ -0,0 +1,38 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import React from 'react'; +import { Button } from 'onap-ui-react'; +import { I18n } from 'react-redux-i18n'; +import PropTypes from 'prop-types'; + +const VersionButton = props => { + const { onClick, actiontype } = props; + let onClickAction = () => onClick(actiontype); + return ( + <div> + <Button btnType="primary" onClick={onClickAction}> + {I18n.t('buttons.certifyBtn')} + </Button> + </div> + ); +}; + +VersionButton.propTypes = { + onClick: PropTypes.func, + actiontype: PropTypes.string +}; + +export default VersionButton; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/views/VersionSelect.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/views/VersionSelect.js new file mode 100644 index 00000000..d8a6d02e --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/views/VersionSelect.js @@ -0,0 +1,69 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import isEmpty from 'lodash.isempty'; + +const VersionSelect = props => { + const { + currentWorkflowVersion, + viewableVersions, + onVersionSelectChange + } = props; + + function onChangeHandler(ev) { + const versionIndex = Object.keys(viewableVersions).find( + key => viewableVersions[key].id === ev.target.value + ); + const currentVersion = viewableVersions[versionIndex].id; + onVersionSelectChange(currentVersion); + } + + return ( + <select + className="version-selector" + key={'selector'} + value={currentWorkflowVersion.id} + onChange={onChangeHandler} + data-test-id="vc-versions-select-box"> + {!isEmpty(viewableVersions) && + viewableVersions.map(item => { + const displayedName = `${item.name} ${ + currentWorkflowVersion.id === item.id + ? currentWorkflowVersion.state.toUpperCase() + : item.state + }`; + return ( + <option + key={'versionSelect' + item.id} + value={item.id} + data-test-id="vc-version-option"> + {displayedName} + </option> + ); + })} + </select> + ); +}; + +VersionSelect.propTypes = { + currentWorkflowVersion: PropTypes.object, + viewableVersions: PropTypes.arrayOf(Object), + onVersionSelectChange: PropTypes.func +}; + +export default VersionSelect; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/views/VersionsContainer.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/views/VersionsContainer.js new file mode 100644 index 00000000..c84ab31d --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/views/VersionsContainer.js @@ -0,0 +1,60 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import React from 'react'; +import VersionSelect from 'features/version/versionController/views/VersionSelect'; +import { I18n } from 'react-redux-i18n'; +import PropTypes from 'prop-types'; +import ArchiveLabel from 'shared/archiveLabel/ArchiveLabel'; + +const VersionContainer = props => { + const { + currentWorkflowVersion, + viewableVersions, + onOverviewClick, + onVersionSelectChange, + isArchive + } = props; + + return ( + <div className="version-section-wrapper"> + <div className="version-status-container"> + {I18n.t('workflow.version')} + <VersionSelect + currentWorkflowVersion={currentWorkflowVersion} + viewableVersions={viewableVersions} + onVersionSelectChange={onVersionSelectChange} + /> + <span + className="version-selector-more-versions" + data-test-id="vc-versions-page-link" + onClick={onOverviewClick}> + {I18n.t('workflow.overview.viewOverview')} + </span> + {isArchive && <ArchiveLabel />} + </div> + </div> + ); +}; + +VersionContainer.propTypes = { + currentWorkflowVersion: PropTypes.object, + viewableVersions: PropTypes.arrayOf(Object), + onOverviewClick: PropTypes.func, + onVersionSelectChange: PropTypes.func, + isArchive: PropTypes.bool +}; + +export default VersionContainer; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/views/WorkflowTitle.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/views/WorkflowTitle.js new file mode 100644 index 00000000..230ecf08 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionController/views/WorkflowTitle.js @@ -0,0 +1,34 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import React from 'react'; +import PropTypes from 'prop-types'; + +const WorkflowTitle = props => { + const { workflowName } = props; + return ( + <div className="version-section-wrapper"> + <div className="group-name-wrapper"> + <div className="group-name">{workflowName || ''}</div> + </div> + </div> + ); +}; + +WorkflowTitle.propTypes = { + workflowName: PropTypes.string +}; + +export default WorkflowTitle; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionModeReducer.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionModeReducer.js new file mode 100644 index 00000000..9d4d67ea --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionModeReducer.js @@ -0,0 +1,10 @@ +import { SET_OPERRATION_MODE } from './versionConstants'; + +export default (state = false, action) => { + switch (action.type) { + case SET_OPERRATION_MODE: + return true; + default: + return state; + } +}; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionReducer.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionReducer.js new file mode 100644 index 00000000..d05af2d7 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionReducer.js @@ -0,0 +1,51 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { + SET_CURRENT_VERSION, + DETAILS_CHANGED, + VERSION_STATE_CHANGED, + TOGGLE_COMPOSITION_UPDATE +} from 'features/version/versionConstants'; + +const initialState = { + isCompositionUpdating: false +}; + +function versionReducer(state = initialState, action) { + switch (action.type) { + case SET_CURRENT_VERSION: + return action.payload; + case DETAILS_CHANGED: + return { + ...state, + ...action.payload + }; + case VERSION_STATE_CHANGED: + return { + ...state, + ...action.payload + }; + case TOGGLE_COMPOSITION_UPDATE: + return { + ...state, + isCompositionUpdating: action.payload.isCompositionUpdating + }; + default: + return state; + } +} + +export default versionReducer; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionSaga.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionSaga.js new file mode 100644 index 00000000..56dd7a5d --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/version/versionSaga.js @@ -0,0 +1,167 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { all, call, put, takeEvery, takeLatest } from 'redux-saga/effects'; +import { I18n } from 'react-redux-i18n'; + +import { genericNetworkErrorAction } from 'src/appConstants'; +import { + setWorkflowVersionAction, + versionStateChangedAction, + FETCH_REQUESTED +} from 'features/version/versionConstants'; +import { setInputsOutputs } from 'features/version/inputOutput/inputOutputActions'; +import { SUBMIT_VERSION } from 'features/version/create/createVersionConstants'; +import { + SAVE_ACTION, + CERTIFY_ACTION +} from 'features/version/versionController/versionControllerConstants'; +import versionApi from 'features/version/versionApi'; +import { notificationActions } from 'shared/notifications/notificationsActions'; +import { versionState } from 'features/version/versionConstants'; +import overviewApi from '../workflow/overview/overviewApi'; +import { versionListFetchAction } from '../workflow/overview/overviewConstansts'; +import { + updateComposition, + deleteCompositionArtifact +} from 'features/version/composition/compositionActions'; +import { getActivitiesList } from 'features/activities/activitiesActions'; + +/** + * Composition validation - converting artifact string to xml + * and checking if bpmn diagram has only one child + * @param composition + * @returns {boolean} + */ +function validateCurrentArtifact(composition) { + const parser = new DOMParser(); + const xml = parser.parseFromString(composition, 'text/xml'); + return Boolean( + xml.getElementsByTagName('bpmndi:BPMNPlane').BPMNPlane_1.children.length + ); +} + +function* fetchVersion(action) { + try { + yield put(getActivitiesList()); + const data = yield call(versionApi.fetchVersion, action.payload); + const { inputs, outputs, ...rest } = data; + let composition; + + if (rest.hasArtifact) { + composition = yield call( + versionApi.fetchVersionArtifact, + action.payload + ); + } else { + //Clearing the store from old artifact using init the default + yield put(deleteCompositionArtifact()); + } + yield all([ + put(setWorkflowVersionAction(rest)), + put(setInputsOutputs({ inputs, outputs })), + composition && put(updateComposition(composition)) + ]); + } catch (error) { + yield put(genericNetworkErrorAction(error)); + } +} + +function* watchSubmitVersion(action) { + try { + const { workflowId, history } = action.payload; + const data = yield call(versionApi.createNewVersion, action.payload); + const versions = yield call(overviewApi.getVersions, workflowId); + yield put(versionListFetchAction(versions)); + yield call( + history.push(`/workflows/workflow/${workflowId}/version/${data.id}`) + ); + } catch (error) { + yield put(genericNetworkErrorAction(error)); + } +} + +function* watchUpdateVersion(action) { + try { + const { + workflowId, + workflowName, + params: { composition, ...versionData } + } = action.payload; + const isArtifactValid = validateCurrentArtifact(composition); + yield call(versionApi.updateVersion, { + workflowId, + params: versionData + }); + yield put( + notificationActions.showSuccess({ + title: I18n.t('workflow.confirmationMessages.updateTitle'), + message: I18n.t('workflow.confirmationMessages.updateMessage') + }) + ); + if (isArtifactValid) { + yield call(versionApi.updateVersionArtifact, { + workflowId, + workflowName, + versionName: versionData.name.split('.').join('_'), + versionId: versionData.id, + payload: composition + }); + } else { + yield call(versionApi.deleteVersionArtifact, { + workflowId, + versionId: versionData.id + }); + } + return isArtifactValid; + } catch (error) { + yield put(genericNetworkErrorAction(error)); + } +} + +function* watchCertifyVersion(action) { + try { + const isArtifactValid = yield call(watchUpdateVersion, action); + if (!isArtifactValid) + throw new Error('Could not update empty artifact'); + yield call(versionApi.certifyVersion, { + ...action.payload + }); + yield put(versionStateChangedAction({ state: versionState.CERTIFIED })); + yield put( + notificationActions.showSuccess({ + title: I18n.t('workflow.confirmationMessages.certifyTitle'), + message: I18n.t('workflow.confirmationMessages.certifyMessage') + }) + ); + } catch (error) { + yield put( + notificationActions.showError({ + title: I18n.t('workflow.confirmationMessages.certifyTitle'), + message: I18n.t('workflow.composition.certifyArtifact') + }) + ); + yield put(genericNetworkErrorAction(error)); + } +} + +function* versionSaga() { + yield takeLatest(FETCH_REQUESTED, fetchVersion); + yield takeEvery(SUBMIT_VERSION, watchSubmitVersion); + yield takeEvery(SAVE_ACTION, watchUpdateVersion); + yield takeEvery(CERTIFY_ACTION, watchCertifyVersion); +} + +export default versionSaga; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/create/CreateWorkflow.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/create/CreateWorkflow.js new file mode 100644 index 00000000..190677fe --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/create/CreateWorkflow.js @@ -0,0 +1,61 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { connect } from 'react-redux'; +import { withRouter } from 'react-router-dom'; + +import { i18nSelector } from 'wfapp/appSelectors'; +import { hideModalAction } from 'shared/modal/modalWrapperActions'; +import CreateWorkflowView from 'features/workflow/create/CreateWorkflowView'; +import { getWorkflowParams } from 'features/workflow/create/createWorkflowSelector'; +import { + getWorkflowDescription, + getWorkflowName +} from 'features/workflow/workflowSelectors'; +import { + inputChangeAction, + submitWorkflowAction, + clearValidationError +} from 'features/workflow/create/createWorkflowConstants'; +import { clearWorkflowAction } from 'features/workflow/workflowConstants'; + +function mapStateToProps(state) { + return { + translation: i18nSelector(state), + workflowDescription: getWorkflowDescription(state), + workflowName: getWorkflowName(state), + workflowParams: getWorkflowParams(state), + errorMessage: state.workflow.data.error + }; +} + +function mapDispatchToProps(dispatch) { + return { + submitWorkflow: payload => { + dispatch(submitWorkflowAction(payload)); + }, + closeCreateWorkflowModal: () => dispatch(hideModalAction()), + clearValidationError: () => dispatch(clearValidationError()), + workflowInputChange: payload => dispatch(inputChangeAction(payload)), + clearWorkflow: () => dispatch(clearWorkflowAction) + }; +} + +export default withRouter( + connect( + mapStateToProps, + mapDispatchToProps + )(CreateWorkflowView) +); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/create/CreateWorkflowView.jsx b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/create/CreateWorkflowView.jsx new file mode 100644 index 00000000..9af92dbb --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/create/CreateWorkflowView.jsx @@ -0,0 +1,101 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Input, Button } from 'onap-ui-react'; +import { I18n } from 'react-redux-i18n'; +import Description from 'shared/components/Description'; + +class CreateWorkflowView extends Component { + static propTypes = { + submitWorkflow: PropTypes.func, + workflowInputChange: PropTypes.func, + workflowDescription: PropTypes.string, + workflowName: PropTypes.string, + closeCreateWorkflowModal: PropTypes.func, + workflowParams: PropTypes.object, + history: PropTypes.object, + errorMessage: PropTypes.string, + clearValidationError: PropTypes.func, + clearWorkflow: PropTypes.func + }; + + componentDidMount() { + const { clearValidationError, clearWorkflow } = this.props; + clearValidationError(); + clearWorkflow(); + } + handleSubmitForm = e => { + e.preventDefault(); + const { workflowParams, history, submitWorkflow } = this.props; + submitWorkflow({ ...workflowParams, history }); + }; + + render() { + const { + workflowInputChange, + workflowDescription, + workflowName, + closeCreateWorkflowModal, + errorMessage + } = this.props; + return ( + <form onSubmit={this.handleSubmitForm} autoComplete="off"> + <div className="new-workflow-page custom-modal-wrapper"> + <div className="form-custom-modal"> + <Input + name="workflowName" + value={workflowName || ''} + type="text" + label={I18n.t('workflow.general.name')} + onChange={val => + workflowInputChange({ + name: val + }) + } + errorMessage={errorMessage} + isRequired + /> + <Description + value={workflowDescription || ''} + label={I18n.t('workflow.general.description')} + onDataChange={workflowInputChange} + /> + </div> + <div className="modal-action-bar sdc-modal__footer"> + <Button btnType="primary"> + {I18n.t('buttons.createBtn')} + </Button> + <Button + btnType="secondary" + onClick={closeCreateWorkflowModal}> + {I18n.t('buttons.closeBtn')} + </Button> + </div> + </div> + </form> + ); + } +} + +CreateWorkflowView.defaultProps = { + submitWorkflow: () => {}, + workflowInputChange: () => {}, + closeCreateWorkflowModal: () => {}, + clearWorkflow: () => {} +}; + +export default CreateWorkflowView; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/create/__tests__/CreateWorkflowView_snapshot-test.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/create/__tests__/CreateWorkflowView_snapshot-test.js new file mode 100644 index 00000000..e34cea96 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/create/__tests__/CreateWorkflowView_snapshot-test.js @@ -0,0 +1,36 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +'use strict'; + +import React from 'react'; +import renderer from 'react-test-renderer'; + +import CreateWorkflowView from 'features/workflow/create/CreateWorkflowView'; + +describe('New Workflow View Snapshot', () => { + it('renders correctly', () => { + const tree = renderer + .create( + <CreateWorkflowView + clearValidationError={() => {}} + clearWorkflow={() => {}} + /> + ) + .toJSON(); + + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/create/__tests__/__snapshots__/CreateWorkflowView_snapshot-test.js.snap b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/create/__tests__/__snapshots__/CreateWorkflowView_snapshot-test.js.snap new file mode 100644 index 00000000..e26da0d3 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/create/__tests__/__snapshots__/CreateWorkflowView_snapshot-test.js.snap @@ -0,0 +1,83 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`New Workflow View Snapshot renders correctly 1`] = ` +<form + autoComplete="off" + onSubmit={[Function]} +> + <div + className="new-workflow-page custom-modal-wrapper" + > + <div + className="form-custom-modal" + > + <div + className="sdc-input " + > + <label + className="sdc-input__label required" + htmlFor="workflowName" + > + name + </label> + <div + className="sdc-input-wrapper" + > + <input + className="sdc-input__input " + data-test-id={undefined} + disabled={false} + id="workflowName" + name="workflowName" + onBlur={[Function]} + onChange={[Function]} + onKeyDown={[Function]} + placeholder={undefined} + readOnly={false} + type="text" + value="" + /> + </div> + </div> + <div + className="description-part" + > + <div + className="sdc-textarea" + > + <div + className="sdc-textarea__label" + > + description + </div> + <textarea + className="custom-textarea field-section sdc-textarea__textarea" + data-test-id="description" + disabled={false} + onChange={[Function]} + value={undefined} + /> + </div> + </div> + </div> + <div + className="modal-action-bar sdc-modal__footer" + > + <button + className="sdc-button sdc-button__primary " + disabled={false} + onClick={undefined} + > + createBtn + </button> + <button + className="sdc-button sdc-button__secondary " + disabled={false} + onClick={[Function]} + > + closeBtn + </button> + </div> + </div> +</form> +`; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/create/__tests__/createWorkflowSaga-test.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/create/__tests__/createWorkflowSaga-test.js new file mode 100644 index 00000000..244a66de --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/create/__tests__/createWorkflowSaga-test.js @@ -0,0 +1,76 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { call, takeEvery } from 'redux-saga/effects'; +import { + watchWorkflow, + watchSubmitWorkflow +} from 'features/workflow/create/createWorkflowSaga'; +import { put } from 'redux-saga/effects'; +import newWorkflowApi from 'features/workflow/create/createWorkflowApi'; +import { SUBMIT_WORKFLOW } from 'features/workflow/create/createWorkflowConstants'; +import { submitVersionAction } from 'features/version/create/createVersionConstants'; +import { NEW_VERSION } from 'features/workflow/create/createWorkflowConstants'; +import { + setWorkflowAction, + clearWorkflowAction +} from 'features/workflow/workflowConstants'; +import { genericNetworkErrorAction } from 'wfapp/appConstants'; + +describe('New workflow saga test', () => { + it('Create new workflow', () => { + const gen = watchWorkflow(); + expect(gen.next().value).toEqual( + takeEvery(SUBMIT_WORKFLOW, watchSubmitWorkflow) + ); + expect(gen.next().done).toEqual(true); + }); + + it('Submit new workflow', () => { + const action = { + payload: { + name: 'workflow1', + description: 'description' + } + }; + const gen = watchSubmitWorkflow(action); + + /** + * expecting the error message to return as undefined + * from validateNameField method + */ + expect(gen.next().value).toEqual(undefined); + expect(gen.next().value).toEqual( + call(newWorkflowApi.createNewWorkflow, action.payload) + ); + const history = undefined, + workflowId = undefined; + expect(gen.next(action.payload).value).toEqual( + put(submitVersionAction({ history, workflowId, ...NEW_VERSION })) + ); + expect(gen.next().value).toEqual( + put(setWorkflowAction({ ...action.payload, id: undefined })) + ); + //handling errors + expect(gen.throw({ error: 'error' }).value).toEqual( + put(clearWorkflowAction) + ); + expect(gen.next().value).toEqual( + put(genericNetworkErrorAction({ error: 'error' })) + ); + expect(gen.next().done).toBe(true); + }); +}); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/create/__tests__/createWorkflowSelector-test.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/create/__tests__/createWorkflowSelector-test.js new file mode 100644 index 00000000..909c876a --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/create/__tests__/createWorkflowSelector-test.js @@ -0,0 +1,51 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +'use strict'; +import { + getWorkflowName, + getWorkflowDescription +} from 'features/workflow/workflowSelectors'; +import { getWorkflowParams } from 'features/workflow/create/createWorkflowSelector'; + +describe('New workflow selectors', () => { + const workflow = { + data: { + name: 'workflow1', + description: 'description' + } + }; + + it('return workflow name', () => { + const state = { workflow }; + expect(getWorkflowName(state)).toEqual(workflow.data.name); + }); + + it('return workflow description', () => { + const state = { workflow }; + expect(getWorkflowDescription(state)).toEqual( + workflow.data.description + ); + }); + + it('return workflow server params', () => { + const state = { workflow }; + expect(getWorkflowParams(state)).toEqual({ + name: getWorkflowName(state), + description: getWorkflowDescription(state) + }); + }); +}); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/create/createWorkflowApi.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/create/createWorkflowApi.js new file mode 100644 index 00000000..b97226a3 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/create/createWorkflowApi.js @@ -0,0 +1,30 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import RestfulAPIUtil from 'services/restAPIUtil'; +import Configuration from 'config/Configuration.js'; + +function baseUrl() { + const restPrefix = Configuration.get('restPrefix'); + return `${restPrefix}/workflows/`; +} + +const Api = { + createNewWorkflow: data => { + return RestfulAPIUtil.post(baseUrl(), data); + } +}; + +export default Api; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/create/createWorkflowConstants.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/create/createWorkflowConstants.js new file mode 100644 index 00000000..de18a1b4 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/create/createWorkflowConstants.js @@ -0,0 +1,44 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +export const NEW_VERSION = { + baseId: null, + description: null +}; +export const MIN_NAME_LENGTH = 6; +export const MAX_NAME_LENGTH = 40; +export const CHARS_VALIDATION_EXP = /^[\w\s\d]+$/; +export const WORKFLOW_INPUT_CHANGE = 'createWorkflow/INPUT_CHANGE'; +export const SUBMIT_WORKFLOW = 'createWorkflow/SUBMIT_WORKFLOW'; +export const VALIDATION_ERROR = 'createWorkflow/VALIDATION_ERROR'; +export const CLEAR_VALIDATION_ERROR = 'createWorkflow/CLEAR_VALIDATION_ERROR'; + +export const inputChangeAction = payload => ({ + type: WORKFLOW_INPUT_CHANGE, + payload +}); + +export const submitWorkflowAction = payload => ({ + type: SUBMIT_WORKFLOW, + payload +}); + +export const putValidationError = payload => ({ + type: VALIDATION_ERROR, + payload +}); + +export const clearValidationError = () => ({ type: CLEAR_VALIDATION_ERROR }); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/create/createWorkflowSaga.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/create/createWorkflowSaga.js new file mode 100644 index 00000000..e918556b --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/create/createWorkflowSaga.js @@ -0,0 +1,79 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { takeEvery, call, put } from 'redux-saga/effects'; +import { I18n } from 'react-redux-i18n'; + +import { + SUBMIT_WORKFLOW, + NEW_VERSION, + MAX_NAME_LENGTH, + MIN_NAME_LENGTH, + CHARS_VALIDATION_EXP, + putValidationError +} from 'features/workflow/create/createWorkflowConstants'; +import { + setWorkflowAction, + clearWorkflowAction +} from 'features/workflow/workflowConstants'; +import { hideModalAction } from 'shared/modal/modalWrapperActions'; +import newWorkflowApi from 'features/workflow/create/createWorkflowApi'; +import { genericNetworkErrorAction } from 'wfapp/appConstants'; +import { submitVersionAction } from 'features/version/create/createVersionConstants'; + +export function* watchSubmitWorkflow(action) { + try { + const { name } = action.payload; + const validationError = yield validateNameField(name); + if (validationError) { + yield put(putValidationError(validationError)); + } else { + const workflow = yield call( + newWorkflowApi.createNewWorkflow, + action.payload + ); + //Calling to create empty version + const workflowId = workflow.id; + const { history } = action.payload; + yield put( + submitVersionAction({ history, workflowId, ...NEW_VERSION }) + ); + yield put(setWorkflowAction(workflow)); + yield put(hideModalAction()); + } + } catch (error) { + yield put(clearWorkflowAction); + yield put(genericNetworkErrorAction(error)); + } +} + +export function validateNameField(name) { + let errorMessage; + if (!name) { + errorMessage = I18n.t('workflow.errorMessages.emptyName'); + } else if (!CHARS_VALIDATION_EXP.test(name)) { + errorMessage = I18n.t('workflow.errorMessages.invalidCharacters'); + } else if (name.length < MIN_NAME_LENGTH || name.length > MAX_NAME_LENGTH) { + errorMessage = I18n.t('workflow.errorMessages.nameFieldLength', { + minValue: 6, + maxValue: 40 + }); + } + return errorMessage; +} + +export function* watchWorkflow() { + yield takeEvery(SUBMIT_WORKFLOW, watchSubmitWorkflow); +} diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/create/createWorkflowSelector.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/create/createWorkflowSelector.js new file mode 100644 index 00000000..01f52fc6 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/create/createWorkflowSelector.js @@ -0,0 +1,31 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { createSelector } from 'reselect'; +import { + getTrimWorkflowName, + getWorkflowDescription +} from 'features/workflow/workflowSelectors'; + +export const getWorkflowParams = createSelector( + getTrimWorkflowName, + getWorkflowDescription, + (name, description) => { + return { + name, + description + }; + } +); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/Overview.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/Overview.js new file mode 100644 index 00000000..90b69763 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/Overview.js @@ -0,0 +1,86 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { connect } from 'react-redux'; +import { I18n } from 'react-redux-i18n'; + +import OverviewView from 'features/workflow/overview/OverviewView'; +import { + getSortedVersions, + getSelectedVersionId, + getWorkflowData, + getAllIsVersionsCertifies +} from 'features/workflow/overview/overviewSelectors'; +import { isWorkflowArchive } from 'features/workflow/workflowSelectors'; +import { + getVersionsAction, + updateWorkflowAction, + archiveWorkflowAction, + restoreWorkflowAction +} from 'features/workflow/overview/overviewConstansts'; +import { NEW_VERSION_MODAL } from 'shared/modal/modalWrapperComponents'; +import { + showCustomModalAction, + showAlertModalAction, + hideModalAction +} from 'shared/modal/modalWrapperActions'; +import { inputChangeAction } from 'features/workflow/create/createWorkflowConstants'; + +function mapStateToProps(state) { + return { + versions: getSortedVersions(state), + selectedVersion: getSelectedVersionId(state), + workflow: getWorkflowData(state), + isVersionsCertifies: getAllIsVersionsCertifies(state), + isArchive: isWorkflowArchive(state) + }; +} + +function mapDispatchToProps(dispatch) { + return { + getOverview: workflowId => dispatch(getVersionsAction(workflowId)), + showNewVersionModal: () => + dispatch( + showCustomModalAction({ + customComponentName: NEW_VERSION_MODAL, + title: 'New Version' + }) + ), + workflowInputChange: payload => dispatch(inputChangeAction(payload)), + updateWorkflow: payload => dispatch(updateWorkflowAction(payload)), + archiveWorkflow: payload => { + dispatch( + showAlertModalAction({ + title: I18n.t('workflow.overview.archive'), + body: I18n.t('workflow.overview.confirmArchive', { + name: payload.name + }), + withButtons: true, + actionButtonText: I18n.t('workflow.overview.archive'), + actionButtonClick: () => { + dispatch(archiveWorkflowAction(payload)); + dispatch(hideModalAction()); + } + }) + ); + }, + restoreWorkflow: payload => dispatch(restoreWorkflowAction(payload)) + }; +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(OverviewView); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/OverviewView.jsx b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/OverviewView.jsx new file mode 100644 index 00000000..952e93cf --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/OverviewView.jsx @@ -0,0 +1,142 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import WorkflowDetails from 'features/workflow/overview/views/WorkflowDetails'; +import WorkflowVersions from 'features/workflow/overview/views/WorkflowVersions'; +import WorkflowHeader from 'features/workflow/overview/views/WorkflowHeader'; + +class OverviewView extends Component { + static propTypes = { + getOverview: PropTypes.func, + versions: PropTypes.array, + onCreateVersion: PropTypes.func, + selectedVersion: PropTypes.string, + workflow: PropTypes.object, + history: PropTypes.object, + showNewVersionModal: PropTypes.func, + isVersionsCertifies: PropTypes.bool, + location: PropTypes.object, + match: PropTypes.object, + updateWorkflow: PropTypes.func, + workflowInputChange: PropTypes.func, + archiveWorkflow: PropTypes.func, + restoreWorkflow: PropTypes.func, + isArchive: PropTypes.bool + }; + + constructor(props) { + super(props); + } + + getOverviewFromRouter = () => { + const { match } = this.props; + const workflowId = match.params.workflowId; + this.props.getOverview(workflowId); + }; + + componentDidMount() { + this.getOverviewFromRouter(); + } + + onSelectVersionFromTable = data => { + const { history } = this.props; + history.push('version/' + data.id); + }; + + onCreateNewVersionFromTable = () => { + const { showNewVersionModal } = this.props; + showNewVersionModal(); + }; + + onUpdateWorkflow = payload => { + const { updateWorkflow, workflow } = this.props; + updateWorkflow({ ...workflow, ...payload }); + }; + + workflowDetailsChanged = payload => { + const { workflowInputChange } = this.props; + workflowInputChange({ ...payload }); + }; + onArchiveWorkflow = () => { + const { archiveWorkflow, workflow, history } = this.props; + + archiveWorkflow({ id: workflow.id, name: workflow.name, history }); + }; + onRestoreWorkflow = () => { + const { restoreWorkflow, workflow, history } = this.props; + restoreWorkflow({ id: workflow.id, history }); + }; + render() { + const { + versions, + selectedVersion, + workflow, + isVersionsCertifies, + history, + isArchive + } = this.props; + const nodeVersions = versions.map(version => ({ + id: version.id, + name: version.name, + parent: version.baseId || '' + })); + + return ( + <div className="overview-page"> + <WorkflowHeader + isArchive={isArchive} + archiveWorkflow={this.onArchiveWorkflow} + restoreWorkflow={this.onRestoreWorkflow} + history={history} + name={workflow.name} + /> + <div className="overview-content"> + <WorkflowDetails + isArchive={isArchive} + name={workflow.name} + description={workflow.description} + modified={workflow.modified} + created={workflow.created} + workflowDetailsChanged={this.workflowDetailsChanged} + updateWorkflow={this.onUpdateWorkflow} + /> + + <div className={'separator overview-separator'} /> + <WorkflowVersions + isArchive={isArchive} + nodeVersions={nodeVersions} + versions={versions} + onCreateVersion={this.onCreateNewVersionFromTable} + onSelectVersion={this.onSelectVersionFromTable} + selectedVersion={selectedVersion} + isVersionsCertifies={isVersionsCertifies} + /> + </div> + </div> + ); + } +} + +OverviewView.defaultProps = { + versions: [], + getOverview: () => {}, + selectedVersion: '' +}; + +export default OverviewView; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/__tests__/OverviewView_snapshot-test.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/__tests__/OverviewView_snapshot-test.js new file mode 100644 index 00000000..5897a91f --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/__tests__/OverviewView_snapshot-test.js @@ -0,0 +1,39 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import React from 'react'; +import renderer from 'react-test-renderer'; + +import OverviewView from 'features/workflow/overview/OverviewView'; + +describe('OverviewView Snapshot', () => { + it('renders correctly', () => { + const workflow = { + name: 'wf1', + description: 'desc 1', + id: 'id1' + }; + const match = { + params: { + workflowId: 'id1' + } + }; + const tree = renderer + .create(<OverviewView workflow={workflow} match={match} />) + .toJSON(); + + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/__tests__/__snapshots__/OverviewView_snapshot-test.js.snap b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/__tests__/__snapshots__/OverviewView_snapshot-test.js.snap new file mode 100644 index 00000000..a54b832c --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/__tests__/__snapshots__/OverviewView_snapshot-test.js.snap @@ -0,0 +1,217 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`OverviewView Snapshot renders correctly 1`] = ` +<div + className="overview-page" +> + <div + className="overview-header" + > + <div + className="title" + > + wf1 + - + title + </div> + <div + className="header-buttons" + > + <div + className="svg-icon-wrapper go-catalog-btn clickable right" + disabled={undefined} + onClick={[Function]} + > + <test-file-stub + className="svg-icon __back" + /> + <span + className="svg-icon-label " + > + backBtnLabel + </span> + </div> + <div + className="svg-icon-wrapper archive-btn clickable bottom" + disabled={undefined} + onClick={[Function]} + title="Archive workflow" + > + <test-file-stub + className="svg-icon __archiveBox" + /> + + </div> + </div> + </div> + <div + className="overview-content" + > + <div + className="workflow-details" + > + <form + onSubmit={[Function]} + > + <div + className="sdc-input " + > + <label + className="sdc-input__label required" + htmlFor="workflowName" + > + name + </label> + <div + className="sdc-input-wrapper" + > + <input + className="sdc-input__input " + data-test-id={undefined} + disabled={true} + id="workflowName" + name="workflowName" + onBlur={[Function]} + onChange={[Function]} + onKeyDown={[Function]} + placeholder={undefined} + readOnly={false} + type="text" + value="wf1" + /> + </div> + </div> + <div + className="description-part" + > + <div + className="sdc-textarea" + > + <div + className="sdc-textarea__label" + > + description + </div> + <textarea + className="custom-textarea field-section sdc-textarea__textarea" + data-test-id="description" + disabled={false} + onChange={[Function]} + value="desc 1" + /> + </div> + </div> + <div + className="save-description" + > + <button + className="sdc-button sdc-button__primary " + disabled={false} + onClick={undefined} + > + saveBtn + </button> + </div> + </form> + </div> + <div + className="separator overview-separator" + /> + <div + className="workflow-versions" + > + <div + className="versions-page-view" + > + <div + className="create-new-version newVersionDisabled" + > + <div + className="create-item-plus-icon" + onClick={[Function]} + > + <div + className="svg-icon-wrapper __secondary bottom" + disabled={undefined} + onClick={undefined} + > + <test-file-stub + className="svg-icon __plus" + /> + + </div> + newVersion + </div> + </div> + <div + className="versions-page-list-and-tree" + > + <div + className="version-tree-wrapper" + > + <div + className="version-tree-title-container" + > + <div + className="version-tree-title" + > + Version Tree + </div> + </div> + <div + className="tree-view versions-tree-container " + > + <svg + className="versions-tree" + width={200} + /> + </div> + </div> + <div + className="version-list" + > + <div + className="version-item-row header-row " + data-test-id="version-item-row" + onClick={[Function]} + > + <div + className="version-item-field header-field item-version" + > + Version + </div> + <div + className="version-item-field header-field item-status" + > + Status + </div> + <div + className="version-item-field header-field" + > + <span + className={undefined} + style={undefined} + > + lastEdited + </span> + </div> + <div + className="version-item-field header-field" + > + <div + className="description-text" + > + Description + </div> + </div> + </div> + <div + className="version-list-items" + /> + </div> + </div> + </div> + </div> + </div> +</div> +`; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/__tests__/overviewReducer-test.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/__tests__/overviewReducer-test.js new file mode 100644 index 00000000..87600be2 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/__tests__/overviewReducer-test.js @@ -0,0 +1,96 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import overviewReducer from '../overviewReducer'; +import { versionListFetchAction } from '../overviewConstansts'; + +describe('Overview reducer', () => { + it('check fetch versions', () => { + const versionResponse = { + total: 2, + size: 0, + page: 0, + items: [ + { + id: '99adf5bc36764628b8018033d285b591', + name: '1.0', + description: 'Initial versionewewe', + baseId: '', + state: 'CERTIFIED', + inputs: [ + { + id: '08274a71d7e34d4e96878aa5fb1ed9bd', + name: 'wewe', + type: 'INTEGER', + mandatory: true + }, + { + id: '7a0b9e33ea0244c2a05c03b96207f1c8', + name: 'eee', + type: 'BOOLEAN', + mandatory: false + } + ], + outputs: [ + { + id: 'a5314bbd67ff4e6091385aaa82ebb266', + name: 'e', + type: 'FLOAT', + mandatory: false + } + ], + creationTime: '2018-07-25T07:36:10.112+0000', + modificationTime: '2018-07-25T07:36:48.663+0000' + }, + { + id: 'cd8156bfb250475dac1e2681a9f2a74f', + name: '2.0', + description: 'versio2neee', + baseId: '99adf5bc36764628b8018033d285b591', + state: 'CERTIFIED', + inputs: [ + { + id: '08274a71d7e34d4e96878aa5fb1ed9bd', + name: 'wewe', + type: 'INTEGER', + mandatory: true + }, + { + id: '7a0b9e33ea0244c2a05c03b96207f1c8', + name: 'eee', + type: 'BOOLEAN', + mandatory: false + } + ], + outputs: [ + { + id: 'a5314bbd67ff4e6091385aaa82ebb266', + name: 'e', + type: 'FLOAT', + mandatory: false + } + ], + creationTime: '2018-07-25T07:36:58.978+0000', + modificationTime: '2018-07-25T07:37:09.041+0000' + } + ] + }; + + expect( + overviewReducer([], versionListFetchAction(versionResponse)) + ).toEqual([...versionResponse.items]); + }); +}); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/overviewApi.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/overviewApi.js new file mode 100644 index 00000000..d48c214b --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/overviewApi.js @@ -0,0 +1,46 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import RestfulAPIUtil from 'services/restAPIUtil'; +import Configuration from 'config/Configuration.js'; + +function baseUrl() { + const restPrefix = Configuration.get('restPrefix'); + + return `${restPrefix}/workflows/`; +} + +const Api = { + getVersions: id => { + return RestfulAPIUtil.get(`${baseUrl()}${id}/versions`); + }, + getWorkflow: id => { + return RestfulAPIUtil.get(`${baseUrl()}${id}`); + }, + updateWorkflow: ({ id, description, name }) => { + return RestfulAPIUtil.put(`${baseUrl()}${id}`, { + name, + description + }); + }, + archiveRestoreWorkflow: ({ id, type }) => { + return RestfulAPIUtil.post(`${baseUrl()}${id}/archiving`, { + status: type + }); + } +}; + +export default Api; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/overviewConstansts.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/overviewConstansts.js new file mode 100644 index 00000000..17bd2fd2 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/overviewConstansts.js @@ -0,0 +1,55 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { createAction } from 'redux-actions'; +import { WORKFLOW_STATUS } from 'features/workflow/workflowConstants'; + +export const FETCH_VERSION_LIST = 'overview/FETCH_VERSION_LIST'; +export const GET_OVERVIEW = 'overview/GET_OVERVIEW'; +export const UPDATE_WORKFLOW = 'overview/UPDATE_WORKFLOW'; +export const ARCHIVE_WORKFLOW = 'overview/ARCHIVE_WORKFLOW'; +export const RESTORE_WORKFLOW = 'overview/RESTORE_WORKFLOW'; + +export const versionListFetchAction = payload => ({ + type: FETCH_VERSION_LIST, + payload +}); + +export const getVersionsAction = workflowId => ({ + type: GET_OVERVIEW, + payload: workflowId +}); + +export const updateWorkflowAction = createAction( + UPDATE_WORKFLOW, + payload => payload +); + +export const archiveWorkflowAction = payload => ({ + type: ARCHIVE_WORKFLOW, + payload: { + ...payload, + type: WORKFLOW_STATUS.ARCHIVE + } +}); + +export const restoreWorkflowAction = payload => ({ + type: RESTORE_WORKFLOW, + payload: { + ...payload, + type: WORKFLOW_STATUS.ACTIVE + } +}); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/overviewReducer.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/overviewReducer.js new file mode 100644 index 00000000..bab482a4 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/overviewReducer.js @@ -0,0 +1,26 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { FETCH_VERSION_LIST } from 'features/workflow/overview/overviewConstansts'; + +export default function overviewReducer(state = [], action) { + switch (action.type) { + case FETCH_VERSION_LIST: + return [...action.payload.items]; + default: + return state; + } +} diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/overviewSagas.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/overviewSagas.js new file mode 100644 index 00000000..792d4c0c --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/overviewSagas.js @@ -0,0 +1,104 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { call, takeEvery, put, select } from 'redux-saga/effects'; + +import { genericNetworkErrorAction } from 'wfapp/appConstants'; +import overviewApi from 'features/workflow/overview/overviewApi'; +import { + versionListFetchAction, + getVersionsAction, + GET_OVERVIEW, + UPDATE_WORKFLOW, + ARCHIVE_WORKFLOW, + RESTORE_WORKFLOW +} from 'features/workflow/overview/overviewConstansts'; +import { setWorkflowAction } from 'features/workflow/workflowConstants'; +import { notificationActions } from 'shared/notifications/notificationsActions'; +import { fetchWorkflow } from 'features/catalog/catalogActions'; +import { I18n } from 'react-redux-i18n'; + +export function* getOverview({ payload }) { + try { + const versions = yield call(overviewApi.getVersions, payload); + yield put(versionListFetchAction(versions)); + const workflow = yield call(overviewApi.getWorkflow, payload); + yield put(setWorkflowAction(workflow)); + } catch (error) { + yield put(genericNetworkErrorAction(error)); + } +} + +export function* updateWorkflow(action) { + try { + yield call(overviewApi.updateWorkflow, action.payload); + yield put( + notificationActions.showSuccess({ + title: I18n.t('workflow.overview.updateTitle'), + message: I18n.t('workflow.overview.updateNotification') + }) + ); + } catch (e) { + yield put(genericNetworkErrorAction(e)); + } +} + +export function* archiveRestoreWorkflow(action) { + try { + const { ...data } = action.payload; + yield call(overviewApi.archiveRestoreWorkflow, data); + const { + catalog: { sort, status }, + searchNameFilter = '' + } = yield select(); + + yield put( + fetchWorkflow({ + sort, + searchNameFilter, + status: status + }) + ); + } catch (e) { + yield put(genericNetworkErrorAction(e)); + } +} + +export function* restoreWorkflow(action) { + const { id } = action.payload; + yield archiveRestoreWorkflow(action); + yield put(getVersionsAction(id)); +} + +export function* archiveWorkflow(action) { + const { history } = action.payload; + yield archiveRestoreWorkflow(action); + yield put( + notificationActions.showSuccess({ + title: I18n.t('workflow.overview.archiveTitle'), + message: I18n.t('workflow.overview.archiveNotification', { + name: action.payload.name + }) + }) + ); + history.push('/workflows/'); +} + +export function* watchOverview() { + yield takeEvery(GET_OVERVIEW, getOverview); + yield takeEvery(UPDATE_WORKFLOW, updateWorkflow); + yield takeEvery(ARCHIVE_WORKFLOW, archiveWorkflow); + yield takeEvery(RESTORE_WORKFLOW, restoreWorkflow); +} diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/overviewSelectors.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/overviewSelectors.js new file mode 100644 index 00000000..d6bb1915 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/overviewSelectors.js @@ -0,0 +1,62 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { createSelector } from 'reselect'; +import { getWorkflowParams } from 'features/workflow/create/createWorkflowSelector'; +import { versionState } from 'features/version/versionConstants'; + +export const getVersions = state => state && state.workflow.versions; +export const getSortedVersions = createSelector( + getVersions, + versions => versions && versions.sort((a, b) => a.name - b.name) +); +export const getSelectedVersionId = state => + state && state.currentVersion.general.id; +export const getWorkflowCreationTime = state => + state && state.workflow.data.creationTime; + +export const getWorkflowModificationTime = state => + state && state.workflow.data.modificationTime; + +export const getWorkflowId = state => state && state.workflow.data.id; +export const getAllIsVersionsCertifies = createSelector( + getSortedVersions, + versions => + versions && + versions.filter( + version => version.state.toLowerCase() === versionState.CERTIFIED + ).length === versions.length +); + +export const getWorkflowData = createSelector( + getWorkflowParams, + getWorkflowCreationTime, + getWorkflowModificationTime, + getWorkflowId, + (params, creationTime, modificationTime, id) => { + return { + ...params, + created: creationTime, + modified: modificationTime, + id + }; + } +); + +export const getLatestBaseId = createSelector( + getSortedVersions, + versions => versions.length && versions[versions.length - 1].id +); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/views/VersionList/VersionListItem.jsx b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/views/VersionList/VersionListItem.jsx new file mode 100644 index 00000000..aa852922 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/views/VersionList/VersionListItem.jsx @@ -0,0 +1,74 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Localize, Translate } from 'react-redux-i18n'; + +const VersionListItem = ({ data, onSelectVersion, isHeader, isSelected }) => { + let { modificationTime, name, state, description } = data; + const modificationText = !isHeader ? ( + <Localize value={modificationTime} dateFormat="date.long" /> + ) : ( + <Translate value="workflow.overview.lastEdited" /> + ); + + function onVersionClick() { + onSelectVersion(data); + } + + return ( + <div + data-test-id="version-item-row" + className={`version-item-row ${ + isHeader ? 'header-row' : 'clickable' + } ${isSelected ? 'selected' : ''}`} + onClick={onVersionClick}> + <div + className={`version-item-field ${ + isHeader ? 'header-field item-version' : 'item-version' + }`}> + {name} + </div> + <div + className={`version-item-field ${ + isHeader ? 'header-field item-status' : 'item-status' + }`}> + {state} + </div> + <div + className={`version-item-field ${ + isHeader ? 'header-field' : 'item-last-edited' + }`}> + {modificationText} + </div> + <div + className={`version-item-field ${ + isHeader ? 'header-field' : 'item-description' + }`}> + <div className="description-text">{description}</div> + </div> + </div> + ); +}; + +VersionListItem.propTypes = { + data: PropTypes.object, + onSelectVersion: PropTypes.func, + isHeader: PropTypes.bool, + isSelected: PropTypes.bool +}; + +export default VersionListItem; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/views/VersionList/index.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/views/VersionList/index.js new file mode 100644 index 00000000..801c6693 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/views/VersionList/index.js @@ -0,0 +1,51 @@ +/*! + * Copyright © 2016-2018 European Support Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import VersionListItem from 'features/workflow/overview/views/VersionList/VersionListItem'; + +const VersionList = ({ versions, onSelectVersion, selectedVersion }) => ( + <div className="version-list"> + <VersionListItem + data={{ + name: 'Version', + state: 'Status', + description: 'Description' + }} + isHeader + /> + <div className="version-list-items"> + {versions.map(version => ( + <VersionListItem + key={version.id} + data={version} + onSelectVersion={onSelectVersion} + isSelected={selectedVersion === version.id} + /> + ))} + </div> + </div> +); + +VersionList.propTypes = { + versions: PropTypes.array, + onSelectVersion: PropTypes.func, + selectedVersion: PropTypes.string +}; + +export default VersionList; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/views/VersionTree.jsx b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/views/VersionTree.jsx new file mode 100644 index 00000000..cd3be771 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/views/VersionTree.jsx @@ -0,0 +1,43 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import React from 'react'; +import Tree from 'shared/tree/Tree'; +import PropTypes from 'prop-types'; + +const VersionTree = ({ nodeVersions, selectedVersion }) => { + return ( + <div className="version-tree-wrapper"> + <div className="version-tree-title-container"> + <div className="version-tree-title">Version Tree</div> + </div> + <Tree + name={'versions-tree'} + width={200} + allowScaleWidth={false} + nodes={nodeVersions} + selectedNodeId={selectedVersion} + /> + </div> + ); +}; + +VersionTree.propTypes = { + nodeVersions: PropTypes.array, + selectedVersion: PropTypes.string +}; + +export default VersionTree; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/views/WorkflowDetails.jsx b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/views/WorkflowDetails.jsx new file mode 100644 index 00000000..38f6af06 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/views/WorkflowDetails.jsx @@ -0,0 +1,75 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import React, { Component } from 'react'; +import { I18n } from 'react-redux-i18n'; +import { Input, Button } from 'onap-ui-react'; +import PropTypes from 'prop-types'; + +import Description from 'shared/components/Description'; + +class WorkflowDetails extends Component { + handleSubmitForm = e => { + e.preventDefault(); + const { description } = this.props; + this.props.updateWorkflow(description); + }; + + render() { + const { + name, + description, + workflowDetailsChanged, + isArchive + } = this.props; + return ( + <div className="workflow-details"> + <form onSubmit={this.handleSubmitForm}> + <Input + name="workflowName" + value={name || ''} + type="text" + label={I18n.t('workflow.general.name')} + isRequired + disabled + /> + <Description + disabled={isArchive} + description={description} + onDataChange={workflowDetailsChanged} + /> + <div className="save-description"> + <Button disabled={isArchive} btnType="primary"> + {I18n.t('buttons.saveBtn')} + </Button> + </div> + </form> + </div> + ); + } +} + +WorkflowDetails.propTypes = { + name: PropTypes.string, + created: PropTypes.string, + modified: PropTypes.string, + description: PropTypes.string, + defaultDescription: PropTypes.string, + updateWorkflow: PropTypes.func, + workflowDetailsChanged: PropTypes.func, + isArchive: PropTypes.bool +}; + +export default WorkflowDetails; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/views/WorkflowHeader.jsx b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/views/WorkflowHeader.jsx new file mode 100644 index 00000000..46a1d94a --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/views/WorkflowHeader.jsx @@ -0,0 +1,106 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import React from 'react'; +import PropTypes from 'prop-types'; +import { I18n } from 'react-redux-i18n'; +import { SVGIcon, Button } from 'onap-ui-react'; +import ArchiveLabel from 'shared/archiveLabel/ArchiveLabel'; + +const Buttons = ({ history, archiveWorkflow, restoreWorkflow, isArchive }) => ( + <div className="header-buttons"> + <SVGIcon + onClick={() => history.push('/workflows/')} + label={I18n.t('workflow.overview.backBtnLabel')} + className="go-catalog-btn" + labelPosition="right" + name="back" + /> + <ArchiveBtn + isArchive={isArchive} + restoreWorkflow={restoreWorkflow} + archiveWorkflow={archiveWorkflow} + /> + </div> +); + +Buttons.propTypes = { + history: PropTypes.object, + archiveWorkflow: PropTypes.func, + isArchive: PropTypes.bool, + restoreWorkflow: PropTypes.func +}; + +const Title = ({ name, isArchive }) => ( + <div className="title"> + {name} - {I18n.t('workflow.overview.title')} + {isArchive && <ArchiveLabel />} + </div> +); +Title.propTypes = { + name: PropTypes.string, + isArchive: PropTypes.bool +}; + +const ArchiveBtn = ({ isArchive, archiveWorkflow, restoreWorkflow }) => { + return !isArchive ? ( + <SVGIcon + onClick={archiveWorkflow} + title="Archive workflow" + className="archive-btn" + name="archiveBox" + /> + ) : ( + <Button className="restore-btn" onClick={restoreWorkflow}> + RESTORE + </Button> + ); +}; +ArchiveBtn.propTypes = { + status: PropTypes.string, + archiveWorkflow: PropTypes.func, + restoreWorkflow: PropTypes.func, + isArchive: PropTypes.bool +}; + +const WorkflowHeader = ({ + name, + history, + archiveWorkflow, + restoreWorkflow, + isArchive +}) => { + return ( + <div className="overview-header"> + <Title isArchive={isArchive} name={name} /> + <Buttons + restoreWorkflow={restoreWorkflow} + archiveWorkflow={archiveWorkflow} + isArchive={isArchive} + history={history} + /> + </div> + ); +}; + +WorkflowHeader.propTypes = { + name: PropTypes.string, + history: PropTypes.object, + archiveWorkflow: PropTypes.func, + restoreWorkflow: PropTypes.func, + isArchive: PropTypes.bool +}; + +export default WorkflowHeader; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/views/WorkflowVersions.jsx b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/views/WorkflowVersions.jsx new file mode 100644 index 00000000..18e33bf1 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/overview/views/WorkflowVersions.jsx @@ -0,0 +1,67 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import VersionList from 'features/workflow/overview/views/VersionList'; +import VersionTree from 'features/workflow/overview/views//VersionTree'; +import NewVersionContainer from 'features/version/create/views/NewVersionContainer'; + +const WorkflowVersions = ({ + nodeVersions, + versions, + onCreateVersion, + onSelectVersion, + selectedVersion, + isVersionsCertifies, + isArchive +}) => { + return ( + <div className="workflow-versions"> + <div className="versions-page-view"> + <NewVersionContainer + onCreateVersion={onCreateVersion} + isVersionsCertifies={isVersionsCertifies} + isArchive={isArchive} + /> + <div className="versions-page-list-and-tree"> + <VersionTree + nodeVersions={nodeVersions} + selectedVersion={selectedVersion} + /> + <VersionList + versions={versions} + selectedVersion={selectedVersion} + onSelectVersion={onSelectVersion} + /> + </div> + </div> + </div> + ); +}; + +WorkflowVersions.propTypes = { + nodeVersions: PropTypes.array, + versions: PropTypes.array, + onCreateVersion: PropTypes.func, + onSelectVersion: PropTypes.func, + selectedVersion: PropTypes.string, + isVersionsCertifies: PropTypes.bool, + isArchive: PropTypes.bool +}; + +export default WorkflowVersions; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/workflowConstants.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/workflowConstants.js new file mode 100644 index 00000000..a9f4aa51 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/workflowConstants.js @@ -0,0 +1,27 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { createAction } from 'redux-actions'; + +export const WORKFLOW_STATUS = { + ACTIVE: 'ACTIVE', + ARCHIVE: 'ARCHIVED' +}; + +export const CLEAR_WORKFLOW_DATA = 'workflow/CLEAR_WORKFLOW_DATA'; +export const SET_WORKFLOW = 'workflow/SET_WORKFLOW'; + +export const setWorkflowAction = createAction(SET_WORKFLOW, payload => payload); +export const clearWorkflowAction = { type: CLEAR_WORKFLOW_DATA }; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/workflowReducer.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/workflowReducer.js new file mode 100644 index 00000000..70d6e5be --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/workflowReducer.js @@ -0,0 +1,60 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { + WORKFLOW_INPUT_CHANGE, + VALIDATION_ERROR, + CLEAR_VALIDATION_ERROR +} from 'features/workflow/create/createWorkflowConstants'; +import { + SET_WORKFLOW, + CLEAR_WORKFLOW_DATA +} from 'features/workflow/workflowConstants'; + +function workflowReducer(state = {}, action) { + switch (action.type) { + case WORKFLOW_INPUT_CHANGE: + return { + ...state, + ...action.payload + }; + case CLEAR_WORKFLOW_DATA: + return {}; + case SET_WORKFLOW: + return { + ...action.payload + }; + //TODO change it when BE is done + // return { + // ...action.payload, + // status: 'archive' + // }; + case VALIDATION_ERROR: + return { + ...state, + error: action.payload + }; + case CLEAR_VALIDATION_ERROR: + return { + ...state, + error: '' + }; + default: + return state; + } +} + +export default workflowReducer; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/workflowSelectors.js b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/workflowSelectors.js new file mode 100644 index 00000000..53487012 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/features/workflow/workflowSelectors.js @@ -0,0 +1,34 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { WORKFLOW_STATUS } from './workflowConstants'; +export const getWorkflowName = state => + state && state.workflow.data.name && state.workflow.data.name; +export const getTrimWorkflowName = state => + state && state.workflow.data.name && state.workflow.data.name.trim(); +export const getWorkflowId = state => state && state.workflow.data.id; +export const getWorkflowDescription = state => + state && state.workflow.data.description; +export const getWorkflowStatus = state => + state && + state.workflow && + state.workflow.data && + state.workflow.data.status; +export const isWorkflowArchive = state => + state && + state.workflow && + state.workflow.data && + state.workflow.data.archiving && + state.workflow.data.archiving === WORKFLOW_STATUS.ARCHIVE; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/i18n/I18n.js b/sdc-workflow-designer-ui/src/main/frontend/src/i18n/I18n.js new file mode 100644 index 00000000..fdf28ffd --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/i18n/I18n.js @@ -0,0 +1,31 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import languagesData from 'wfapp/i18n/languages.json'; +import merge from 'lodash.merge'; +import setPath from 'lodash.set'; + +let languagesObj = {}; +let language = null; +let key = null; +for (language in languagesData) { + let langData = {}; + for (key in languagesData[language]) { + setPath(langData, key, languagesData[language][key]); + } + languagesObj[language] = langData; +} +export default merge({}, languagesObj); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/i18n/i18next-scanner.config.js b/sdc-workflow-designer-ui/src/main/frontend/src/i18n/i18next-scanner.config.js new file mode 100644 index 00000000..a626211a --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/i18n/i18next-scanner.config.js @@ -0,0 +1,44 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +module.exports = { + options: { + debug: true, + func: { + list: ['i18next.t', 'i18n.t'], + extensions: ['.js', '.jsx'] + }, + trans: { + extensions: ['.js', '.jsx'], + fallbackKey: (ns, value) => { + return value; + } + }, + lngs: ['en', 'de'], + ns: ['locale', 'resource'], + defaultNs: 'resource', + defaultValue: '__STRING_NOT_TRANSLATED__', + resource: { + loadPath: 'i18n/{{lng}}/{{ns}}.json', + savePath: 'i18n/{{lng}}/{{ns}}.json' + }, + nsSeparator: false, // namespace separator + keySeparator: false, // key separator + interpolation: { + prefix: '{{', + suffix: '}}' + } + } +}; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/i18n/languages.json b/sdc-workflow-designer-ui/src/main/frontend/src/i18n/languages.json new file mode 100644 index 00000000..9485681a --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/i18n/languages.json @@ -0,0 +1,97 @@ +{ + "en": { + "catalog": { + "elementFound": "%{count} Elements found", + "elementFound_0": "No Elements found", + "elementFound_1": "%{count} Element found", + "orderBy": "Order By", + "alphabeticalOrder": "Alphabetical order", + "addWorkflow": "Add Workflow" + }, + "date": { + "short": "D/MM/YYYY hh:mm A", + "long": "MMMM Do, YYYY, hh:mm A" + }, + "buttons": { + "submitBtn": "Submit", + "saveBtn": "Save", + "createBtn": "Create", + "cancelBtn": "Cancel", + "certifyBtn": "Certify", + "completeBtn": "Complete", + "undoBtn": "Undo", + "closeBtn": "Close", + "okBtn": "Ok", + "backToCatalog": "Catalog" + }, + "form": { + "name": "Name", + "description": "Description" + }, + "workflow": { + "general": { + "headerTitle": "General", + "name": "Name", + "description": "Description", + "created": "Created", + "modified": "Modified" + }, + "version": "Version", + "sideBar": { + "general": "General", + "inputOutput": "Input / Output", + "composition": "Composition" + }, + "overview": { + "viewOverview": "Workflow Overview", + "versionList": "Versions", + "newVersion": "Create New Version", + "title": "Overview", + "lastEdited": "Last Edited On", + "backBtnLabel": "WORKFLOW CATALOG", + "archived": "Archived", + "archive": "ARCHIVE", + "archiveTitle": "Archive", + "updateTitle": "Update Workflow", + "updateNotification": "Successfully updated", + "archiveNotification": "\"%{name}\" successfully archived", + "confirmArchive": "Are you sure you want to archive \"%{name}\"?" + }, + "inputOutput": { + "name": "Name", + "type": "Type", + "mandatory": "Mandatory", + "noData": "No Parameters to Show", + "addInput": "Add Input", + "addOutput": "Add Output", + "inputs": "Inputs", + "outputs": "Outputs", + "DELETE": "DELETE", + "CANCEL": "CANCEL", + "confirmDelete": "Are you sure you want to delete \"%{name}\"?" + }, + "errorMessages": { + "alreadyExists": "Already exists", + "invalidCharacters": "Alphanumeric and underscore only", + "emptyName": "Field is required", + "nameFieldLength": "Name must be at least %{minValue} characters and no more than %{maxValue} characters" + }, + "composition": { + "bpmnError" : "BPMN.IO Error", + "exportErrorMsg": "Could not export diagram", + "saveErrorMsg": "Could not save diagram", + "importErrorMsg": "Could not import diagram", + "certifyArtifact": "Could not certify empty artifact" + }, + "confirmationMessages": { + "certifyTitle": "Certify Version", + "certifyMessage": "Successfully certified", + "updateTitle": "Update Workflow Version", + "updateMessage": "Successfully updated" + } + }, + "version": { + "category": "Version Category" + } + } +} diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/i18n/translationSaga.js b/sdc-workflow-designer-ui/src/main/frontend/src/i18n/translationSaga.js new file mode 100644 index 00000000..9c972ea7 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/i18n/translationSaga.js @@ -0,0 +1,24 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { put } from 'redux-saga/effects'; +import { loadTranslations, setLocale } from 'react-redux-i18n'; + +import translationsObject from 'i18n/I18n'; +import { LANG } from 'wfapp/appConstants'; +export function* initTranslationSaga() { + yield put(loadTranslations(translationsObject)); + yield put(setLocale(LANG)); +} diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/index.js b/sdc-workflow-designer-ui/src/main/frontend/src/index.js new file mode 100644 index 00000000..46a87603 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/index.js @@ -0,0 +1,40 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http: //www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Provider } from 'react-redux'; +import { BrowserRouter } from 'react-router-dom'; + +import Notifications from 'shared/notifications/Notifications'; +import Loader from 'shared/loader/Loader'; +import App from 'wfapp/App'; +import ModalWrapper from 'shared/modal/ModalWrapper'; +import store from './store'; + +ReactDOM.render( + <Provider store={store}> + <BrowserRouter> + <React.Fragment> + <App /> + <Notifications /> + <ModalWrapper /> + <Loader /> + </React.Fragment> + </BrowserRouter> + </Provider>, + document.getElementById('root') +); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/pluginContext/pluginContextActions.js b/sdc-workflow-designer-ui/src/main/frontend/src/pluginContext/pluginContextActions.js new file mode 100644 index 00000000..553b8911 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/pluginContext/pluginContextActions.js @@ -0,0 +1,21 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +*http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { SET_CONTEXT } from './pluginContextConstants'; + +export const setPluginContext = payload => ({ + type: SET_CONTEXT, + payload +}); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/pluginContext/pluginContextConstants.js b/sdc-workflow-designer-ui/src/main/frontend/src/pluginContext/pluginContextConstants.js new file mode 100644 index 00000000..0f8f409f --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/pluginContext/pluginContextConstants.js @@ -0,0 +1,22 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +*http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +export const SET_CONTEXT = 'pluginContext/SET_CONTEXT'; + +export const notificationType = { + READY: 'READY', + CLOSE: 'CLOSE' +}; +export const CATALOG_PATH = 'workspace.interface_operation'; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/pluginContext/pluginContextReducer.js b/sdc-workflow-designer-ui/src/main/frontend/src/pluginContext/pluginContextReducer.js new file mode 100644 index 00000000..6e61a3bd --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/pluginContext/pluginContextReducer.js @@ -0,0 +1,25 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +*http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { SET_CONTEXT } from './pluginContextConstants'; +export default (state = {}, action) => { + switch (action.type) { + case SET_CONTEXT: { + return action.payload; + } + default: + return state; + } +}; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/pluginContext/pluginContextSelector.js b/sdc-workflow-designer-ui/src/main/frontend/src/pluginContext/pluginContextSelector.js new file mode 100644 index 00000000..65599c55 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/pluginContext/pluginContextSelector.js @@ -0,0 +1,16 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +*http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +export const pluginContextSelector = state => state && state.pluginContext; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/rootReducers.js b/sdc-workflow-designer-ui/src/main/frontend/src/rootReducers.js new file mode 100644 index 00000000..33fd8a82 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/rootReducers.js @@ -0,0 +1,51 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { combineReducers } from 'redux'; +import { i18nReducer } from 'react-redux-i18n'; + +import catalog from 'features/catalog/catalogReducer'; +import inputOutput from 'features/version/inputOutput/inputOutputReducer'; +import notificationsReducer from 'shared/notifications/notificationsReducer'; +import versionReducer from 'features/version/versionReducer'; +import loader from 'shared/loader/LoaderReducer'; +import modal from 'shared/modal/modalWrapperReducer'; +import overviewReducer from 'features/workflow/overview/overviewReducer'; +import workflowReducer from 'features/workflow/workflowReducer'; +import compositionReducer from 'features/version/composition/compositionReducer'; +import activitiesReducer from 'features/activities/activitiesReducer'; +import operationModeReducer from 'features/version/versionModeReducer'; +import pluginContextReducer from './pluginContext/pluginContextReducer'; + +export default combineReducers({ + i18n: i18nReducer, + catalog, + notifications: notificationsReducer, + currentVersion: combineReducers({ + general: versionReducer, + inputOutput, + composition: compositionReducer, + operationMode: operationModeReducer + }), + workflow: combineReducers({ + data: workflowReducer, + versions: overviewReducer + }), + pluginContext: pluginContextReducer, + activities: activitiesReducer, + loader, + modal +}); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/rootSaga.js b/sdc-workflow-designer-ui/src/main/frontend/src/rootSaga.js new file mode 100644 index 00000000..0ea0782a --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/rootSaga.js @@ -0,0 +1,38 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { all, fork } from 'redux-saga/effects'; + +import { initTranslationSaga } from 'i18n/translationSaga'; +import catalogSaga from 'features/catalog/catalogSagas'; +import { watchWorkflow } from 'features/workflow/create/createWorkflowSaga'; +import { watchNotifications } from 'shared/notifications/notificationsSagas'; +import versionSaga from 'features/version/versionSaga'; +import activitiesSaga from 'features/activities/activitiesSaga'; + +import { watchOverview } from 'features/workflow/overview/overviewSagas'; + +export default function* rootSaga() { + yield all([ + fork(initTranslationSaga), + fork(catalogSaga), + fork(watchWorkflow), + fork(watchNotifications), + fork(versionSaga), + fork(watchOverview), + fork(activitiesSaga) + ]); +} diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/routes.js b/sdc-workflow-designer-ui/src/main/frontend/src/routes.js new file mode 100644 index 00000000..4be77016 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/routes.js @@ -0,0 +1,59 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import Catalog from 'features/catalog/Catalog'; +import Version from 'features/version/Version'; +import GeneralView from 'features/version/general/General'; +import OverviewView from 'features/workflow/overview/Overview'; +import InputOutput from 'features/version/inputOutput/InputOutput'; +import Composition from 'features/version/composition/Composition'; + +export const routes = [ + { + path: '/workflows/workflow/:workflowId/version/:versionId', + component: Version, + routes: [ + { + path: '/', + exact: true, + component: GeneralView, + i18nName: 'workflow.sideBar.general', + id: 'GENERAL' + }, + { + path: '/input-output', + component: InputOutput, + i18nName: 'workflow.sideBar.inputOutput', + id: 'INPUT_OUTPUT' + }, + { + path: '/composition', + component: Composition, + i18nName: 'workflow.sideBar.composition', + id: 'COMPOSITION' + } + ] + }, + { + path: '/workflows', + exact: true, + component: Catalog + }, + { + path: '/workflows/workflow/:workflowId/overview', + component: OverviewView + } +]; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/services/restAPIUtil.js b/sdc-workflow-designer-ui/src/main/frontend/src/services/restAPIUtil.js new file mode 100644 index 00000000..08262fed --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/services/restAPIUtil.js @@ -0,0 +1,150 @@ +/* + * Copyright © 2018 European Support Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import uuid from 'uuid-js'; +import md5 from 'md5'; +import axios from 'axios'; + +import store from 'wfapp/store'; +import { USER_ID } from 'wfapp/appConstants'; + +import { + sendRequestAction, + recieveResponseAction +} from 'shared/loader/LoaderConstants'; + +import Configuration from 'config/Configuration.js'; +import errorResponseHandler from 'shared/errorResponseHandler/errorResponseHandler'; + +//methods +const GET = 'GET'; +const POST = 'POST'; +const PUT = 'PUT'; +const DELETE = 'DELETE'; +const PATCH = 'PATCH'; +const BINARY = 'binary'; + +const AUTHORIZATION_HEADER = 'X-AUTH-TOKEN'; +const STORAGE_AUTH_KEY = 'sdc-auth-token'; +const REQUEST_ID_HEADER = 'X-ECOMP-RequestID'; +const CONTENT_MD5_HEADER = 'Content-MD5'; + +function applySecurity(options, data) { + let headers = options.headers || (options.headers = {}); + if (options.isAnonymous) { + return; + } + + let authToken = localStorage.getItem(STORAGE_AUTH_KEY); + if (authToken) { + headers[AUTHORIZATION_HEADER] = authToken; + } + + let catalogApiHeaders = Configuration.get('CatalogApiHeaders'), + catalogUidHeader = catalogApiHeaders && catalogApiHeaders.userId; + if (catalogUidHeader) { + headers[catalogUidHeader.name] = catalogUidHeader.value; + } + + headers[REQUEST_ID_HEADER] = uuid.create().toString(); + if (options.md5) { + let headers = options.headers; + headers[CONTENT_MD5_HEADER] = window.btoa( + md5(JSON.stringify(data)).toLowerCase() + ); + } + + if (localStorage.getItem(USER_ID)) { + headers[USER_ID] = localStorage.getItem(USER_ID); + } +} + +function handleSuccess(responseHeaders, requestHeaders) { + let authToken = responseHeaders[AUTHORIZATION_HEADER]; + let prevToken = requestHeaders && requestHeaders[AUTHORIZATION_HEADER]; + if (authToken && authToken !== prevToken) { + if (authToken === 'null') { + localStorage.removeItem(STORAGE_AUTH_KEY); + } else { + localStorage.setItem(STORAGE_AUTH_KEY, authToken); + } + } +} + +class RestAPIUtil { + async handleRequest(url, type, options = {}, data = {}) { + applySecurity(options, data); + + let config = { + method: type, + url: url, + headers: options.headers, + data: data + }; + + store.dispatch(sendRequestAction(url)); + + if (options.dataType === BINARY) { + config.responseType = 'arraybuffer'; + } + try { + const result = await axios(config); + store.dispatch(recieveResponseAction(url)); + if (options.dataType !== BINARY) { + handleSuccess(result.headers, result.config.headers); + } + return options.dataType === BINARY + ? { + blob: new Blob([result.data]), + headers: result.headers + } + : result.data; + } catch (error) { + store.dispatch(recieveResponseAction(url)); + errorResponseHandler(error.response); + return Promise.reject({ + responseJSON: error.response.data + }); + } + } + + fetch(url, options) { + return this.handleRequest(url, GET, options); + } + + get(url, options) { + return this.fetch(url, options); + } + + post(url, data, options) { + return this.handleRequest(url, POST, options, data); + } + + put(url, data, options) { + return this.handleRequest(url, PUT, options, data); + } + + delete(url, options) { + return this.handleRequest(url, DELETE, options); + } + + patch(url, options) { + return this.handleRequest(url, PATCH, options); + } +} + +const instance = new RestAPIUtil(); + +export default instance; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/archiveLabel/ArchiveLabel.js b/sdc-workflow-designer-ui/src/main/frontend/src/shared/archiveLabel/ArchiveLabel.js new file mode 100644 index 00000000..45b39090 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/archiveLabel/ArchiveLabel.js @@ -0,0 +1,8 @@ +import React from 'react'; +import { I18n } from 'react-redux-i18n'; + +const ArchiveLabel = () => ( + <div className="archive-label">{I18n.t('workflow.overview.archived')}</div> +); + +export default ArchiveLabel; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/components/Description/index.js b/sdc-workflow-designer-ui/src/main/frontend/src/shared/components/Description/index.js new file mode 100644 index 00000000..7ff4d328 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/components/Description/index.js @@ -0,0 +1,50 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import React from 'react'; +import PropTypes from 'prop-types'; +import { I18n } from 'react-redux-i18n'; + +const Description = ({ description, onDataChange, dataTestId, disabled }) => ( + <div className="description-part"> + <div className="sdc-textarea"> + <div className="sdc-textarea__label"> + {I18n.t('workflow.general.description')} + </div> + <textarea + value={description} + data-test-id={dataTestId || 'description'} + onChange={event => { + onDataChange({ description: event.target.value }); + }} + className="custom-textarea field-section sdc-textarea__textarea" + disabled={disabled} + /> + </div> + </div> +); + +Description.propTypes = { + description: PropTypes.string, + onDataChange: PropTypes.func, + dataTestId: PropTypes.string, + disabled: PropTypes.bool +}; + +Description.defaultProps = { + disabled: false +}; + +export default Description; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/components/Select/index.js b/sdc-workflow-designer-ui/src/main/frontend/src/shared/components/Select/index.js new file mode 100644 index 00000000..df30fad9 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/components/Select/index.js @@ -0,0 +1,54 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import React from 'react'; +import PropTypes from 'prop-types'; +import isEmpty from 'lodash.isempty'; + +const Select = props => { + const { dataObj, selectedItemValue, disabled, label } = props; + return ( + <div className="select-wrapper version-selector sdc-input"> + {label && <label>{label}</label>} + <select + className="inputinput-selector" + key={'selector'} + value={selectedItemValue} + disabled={disabled} + data-test-id="vc-select-box"> + {!isEmpty(dataObj) && + dataObj.map(item => { + return ( + <option + key={'select-item' + item.id} + value={item.id} + data-test-id="vc-option"> + {item.displayed || item.name} + </option> + ); + })} + </select> + </div> + ); +}; + +Select.propTypes = { + dataObj: PropTypes.arrayOf(Object), + selectedItemValue: PropTypes.string, + disabled: PropTypes.bool, + label: PropTypes.string +}; + +export default Select; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/components/VersionInfo/index.js b/sdc-workflow-designer-ui/src/main/frontend/src/shared/components/VersionInfo/index.js new file mode 100644 index 00000000..2c6bba24 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/components/VersionInfo/index.js @@ -0,0 +1,43 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import React from 'react'; +import PropTypes from 'prop-types'; +export const LabeledValue = ({ title, value }) => ( + <React.Fragment> + <div className="label">{title}</div> + <div className="value selectable">{value}</div> + </React.Fragment> +); + +LabeledValue.propTypes = { + title: PropTypes.string, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.object]) +}; + +export const VersionInfo = ({ children }) => ( + <div className="version-info-part">{children}</div> +); + +VersionInfo.defaultProps = { + created: '', + modified: '' +}; + +VersionInfo.propTypes = { + created: PropTypes.string, + modified: PropTypes.string, + children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]) +}; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/errorResponseHandler/errorResponseHandler.js b/sdc-workflow-designer-ui/src/main/frontend/src/shared/errorResponseHandler/errorResponseHandler.js new file mode 100644 index 00000000..f8c8f327 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/errorResponseHandler/errorResponseHandler.js @@ -0,0 +1,31 @@ +/* + * Copyright © 2018 European Support Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import store from 'wfapp/store'; +import { showErrorModalAction } from 'shared/modal/modalWrapperActions'; + +export default error => { + const errorData = { + title: error.statusText, + body: error.data ? error.data.message : 'GENERIC ERROR' + }; + store.dispatch( + showErrorModalAction({ + ...errorData, + withButtons: true, + closeButtonText: 'Ok' + }) + ); +}; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/loader/Loader.jsx b/sdc-workflow-designer-ui/src/main/frontend/src/shared/loader/Loader.jsx new file mode 100644 index 00000000..0b4c9f42 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/loader/Loader.jsx @@ -0,0 +1,50 @@ +/* + * Copyright © 2016-2017 European Support Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +export const mapStateToProps = ({ loader: { isLoading } }) => { + return { + isLoading + }; +}; + +export class Loader extends React.Component { + shouldComponentUpdate(nextProps) { + return nextProps.isLoading !== this.props.isLoading; + } + + render() { + let { isLoading } = this.props; + return ( + <div className="onboarding-loader"> + {isLoading && ( + <div className="onboarding-loader-backdrop"> + <div className="tlv-loader large" /> + </div> + )} + </div> + ); + } +} + +Loader.propTypes = { + isLoading: PropTypes.bool +}; + +export default connect(mapStateToProps)(Loader); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/loader/LoaderConstants.js b/sdc-workflow-designer-ui/src/main/frontend/src/shared/loader/LoaderConstants.js new file mode 100644 index 00000000..02ce84ca --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/loader/LoaderConstants.js @@ -0,0 +1,38 @@ +/* + * Copyright © 2016-2017 European Support Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export const actionTypes = { + SHOW: 'SHOW', + HIDE: 'HIDE', + + SEND_REQUEST: 'loader/SEND_REQUEST', + RECEIVE_RESPONSE: 'loader/RECEIVE_RESPONSE' +}; + +export const initialState = { + fetchingRequests: 0, + currentlyFetching: [], + isLoading: false +}; + +export const sendRequestAction = url => ({ + type: actionTypes.SEND_REQUEST, + url +}); + +export const recieveResponseAction = url => ({ + type: actionTypes.RECEIVE_RESPONSE, + url +}); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/loader/LoaderReducer.js b/sdc-workflow-designer-ui/src/main/frontend/src/shared/loader/LoaderReducer.js new file mode 100644 index 00000000..d883492e --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/loader/LoaderReducer.js @@ -0,0 +1,49 @@ +/* + * Copyright © 2016-2017 European Support Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { actionTypes, initialState } from 'shared/loader/LoaderConstants.js'; + +export default (state = initialState, action) => { + let fetchingRequests = state.fetchingRequests; + let newArray; + switch (action.type) { + case actionTypes.SEND_REQUEST: + fetchingRequests++; + newArray = state.currentlyFetching.slice(); + newArray.splice(0, 0, action.url); + return { + fetchingRequests: fetchingRequests, + currentlyFetching: newArray, + isLoading: true + }; + case actionTypes.RECEIVE_RESPONSE: + fetchingRequests--; + + newArray = state.currentlyFetching.filter(item => { + return item !== action.url; + }); + return { + currentlyFetching: newArray, + fetchingRequests: fetchingRequests, + isLoading: fetchingRequests !== 0 + }; + case actionTypes.SHOW: + return { isLoading: true }; + case actionTypes.HIDE: + return { isLoading: false }; + default: + return state; + } +}; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/loader/__tests__/loaderReducer-test.js b/sdc-workflow-designer-ui/src/main/frontend/src/shared/loader/__tests__/loaderReducer-test.js new file mode 100644 index 00000000..3ae40361 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/loader/__tests__/loaderReducer-test.js @@ -0,0 +1,56 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +'use strict'; + +import loaderReducer from 'shared/loader/LoaderReducer'; +import { + initialState, + sendRequestAction, + recieveResponseAction +} from 'shared/loader/LoaderConstants'; + +describe('Loader reducer', () => { + it('return initial state', () => { + expect(loaderReducer(undefined, {})).toEqual(initialState); + }); + + it('send request action test', () => { + const url = 'TEST'; + const loaderData = { + fetchingRequests: 1, + currentlyFetching: [url], + isLoading: true + }; + + expect(loaderReducer(initialState, sendRequestAction(url))).toEqual( + loaderData + ); + }); + + it('recieve response action test', () => { + const url = 'TEST'; + const loaderData = { + fetchingRequests: 1, + currentlyFetching: [url], + isLoading: true + }; + + expect(loaderReducer(loaderData, recieveResponseAction(url))).toEqual( + initialState + ); + }); +}); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/modal/ModalWrapper.js b/sdc-workflow-designer-ui/src/main/frontend/src/shared/modal/ModalWrapper.js new file mode 100644 index 00000000..899ff217 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/modal/ModalWrapper.js @@ -0,0 +1,35 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { connect } from 'react-redux'; + +import ModalWrapperView from 'shared/modal/ModalWrapperView'; +import { hideModalAction } from 'shared/modal/modalWrapperActions'; + +const mapStateToProps = state => ({ + modal: state.modal +}); + +const mapDispatchToProps = dispatch => ({ + hideModal: () => dispatch(hideModalAction()) +}); + +const ModalWrapper = connect( + mapStateToProps, + mapDispatchToProps +)(ModalWrapperView); + +export default ModalWrapper; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/modal/ModalWrapperView.jsx b/sdc-workflow-designer-ui/src/main/frontend/src/shared/modal/ModalWrapperView.jsx new file mode 100644 index 00000000..c83a0acc --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/modal/ModalWrapperView.jsx @@ -0,0 +1,99 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import { + Modal, + ModalHeader, + ModalTitle, + ModalBody, + ModalFooter +} from 'onap-ui-react'; + +import modalWrapperComponents from 'shared/modal/modalWrapperComponents'; + +class ModalWrapperView extends React.Component { + constructor(props) { + super(props); + + this.handleClose = this.handleClose.bind(this); + } + + handleClose() { + const { hideModal, onClose } = this.props; + + hideModal(); + + if (typeof onClose === 'function') { + onClose(); + } + } + + render() { + const { modal } = this.props; + + if (!modal || !modal.type) { + return null; + } + + const { + size, + type, + title, + body, + withButtons, + actionButtonText, + actionButtonClick, + closeButtonText, + customComponentName, + customComponentProps + } = modal; + + const CustomComponent = + customComponentName && modalWrapperComponents[customComponentName]; + + return ( + <Modal show size={size} type={type}> + <ModalHeader onClose={this.handleClose} type={type}> + {title && <ModalTitle>{title}</ModalTitle>} + </ModalHeader> + {body && <ModalBody>{body}</ModalBody>} + {CustomComponent && ( + <CustomComponent {...customComponentProps} /> + )} + {withButtons && ( + <ModalFooter + actionButtonText={actionButtonText} + actionButtonClick={actionButtonClick} + closeButtonText={closeButtonText} + onClose={this.handleClose} + withButtons + /> + )} + </Modal> + ); + } +} + +ModalWrapperView.propTypes = { + hideModal: PropTypes.func, + onClose: PropTypes.func, + modal: PropTypes.object +}; + +export default ModalWrapperView; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/modal/__tests__/modalWrapperActions-test.js b/sdc-workflow-designer-ui/src/main/frontend/src/shared/modal/__tests__/modalWrapperActions-test.js new file mode 100644 index 00000000..0be53de4 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/modal/__tests__/modalWrapperActions-test.js @@ -0,0 +1,85 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +'use strict'; + +import { + SHOW_MODAL, + HIDE_MODAL, + showInfoModalAction, + showAlertModalAction, + showErrorModalAction, + showCustomModalAction, + hideModalAction +} from 'shared/modal/modalWrapperActions'; + +describe('Modal Wrapper Actions', () => { + const actions = [ + { fn: showInfoModalAction, type: 'info', size: 'small' }, + { fn: showAlertModalAction, type: 'alert', size: 'small' }, + { fn: showErrorModalAction, type: 'error', size: 'small' }, + { fn: showCustomModalAction, type: 'custom', size: 'medium' } + ]; + + it('returns correct action', () => { + actions.forEach(action => { + const payload = { + title: 'Title', + body: 'Body', + withButtons: true, + actionButtonText: 'Action Button Text', + actionButtonClick: () => {}, + closeButtonText: 'Close Button Text' + }; + + const expected = { + type: SHOW_MODAL, + payload: { + size: action.size, + ...payload, + type: action.type + } + }; + + expect(action.fn(payload)).toEqual(expected); + }); + }); + + it('returns correct size in action', () => { + actions.forEach(action => { + const size = 'large'; + + const payload = { + size + }; + + const expected = { + type: SHOW_MODAL, + payload: { size, type: action.type } + }; + + expect(action.fn(payload)).toEqual(expected); + }); + }); + + it('returns hide modal action', () => { + const expected = { + type: HIDE_MODAL + }; + + expect(hideModalAction()).toEqual(expected); + }); +}); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/modal/__tests__/modalWrapperReducer-test.js b/sdc-workflow-designer-ui/src/main/frontend/src/shared/modal/__tests__/modalWrapperReducer-test.js new file mode 100644 index 00000000..c33190c5 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/modal/__tests__/modalWrapperReducer-test.js @@ -0,0 +1,61 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +'use strict'; + +import modalWrapperReducer from 'shared/modal/modalWrapperReducer'; +import { + showInfoModalAction, + hideModalAction +} from 'shared/modal/modalWrapperActions'; + +describe('Modal Wrapper Reducer', () => { + it('returns the initial state', () => { + const state = { + type: 'info', + title: 'Title' + }; + expect(modalWrapperReducer(state, {})).toEqual(state); + }); + + it('returns correct state for show modal action', () => { + const payload = { + size: 'medium', + title: 'Title' + }; + + const action = showInfoModalAction(payload); + + const expected = { + ...payload, + type: 'info' + }; + + expect(modalWrapperReducer({}, action)).toEqual(expected); + }); + + it('returns correct state for hide modal action', () => { + const state = { + size: 'medium', + title: 'Title', + type: 'info' + }; + + const action = hideModalAction(); + + expect(modalWrapperReducer(state, action)).toEqual({}); + }); +}); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/modal/modalWrapperActions.js b/sdc-workflow-designer-ui/src/main/frontend/src/shared/modal/modalWrapperActions.js new file mode 100644 index 00000000..fe0ca0a5 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/modal/modalWrapperActions.js @@ -0,0 +1,46 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { createAction } from 'redux-actions'; + +export const SHOW_MODAL = 'modal/SHOW_MODAL'; +export const HIDE_MODAL = 'modal/HIDE_MODAL'; + +export const showInfoModalAction = createAction(SHOW_MODAL, payload => ({ + size: 'small', + ...payload, + type: 'info' +})); + +export const showAlertModalAction = createAction(SHOW_MODAL, payload => ({ + size: 'small', + ...payload, + type: 'alert' +})); + +export const showErrorModalAction = createAction(SHOW_MODAL, payload => ({ + size: 'small', + ...payload, + type: 'error' +})); + +export const showCustomModalAction = createAction(SHOW_MODAL, payload => ({ + size: 'medium', + ...payload, + type: 'custom' +})); + +export const hideModalAction = createAction(HIDE_MODAL); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/modal/modalWrapperComponents.js b/sdc-workflow-designer-ui/src/main/frontend/src/shared/modal/modalWrapperComponents.js new file mode 100644 index 00000000..1e051b2f --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/modal/modalWrapperComponents.js @@ -0,0 +1,27 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import NewWorkflow from 'features/workflow/create/CreateWorkflow'; +import NewVersion from 'features/version/create/CreateVersion'; + +export const NEW_WORKFLOW_MODAL = 'NEW_WORKFLOW_MODAL'; +export const NEW_VERSION_MODAL = 'NEW_VERSION_MODAL'; + +const modalWrapperComponents = { + NEW_WORKFLOW_MODAL: NewWorkflow, + NEW_VERSION_MODAL: NewVersion +}; + +export default modalWrapperComponents; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/modal/modalWrapperReducer.js b/sdc-workflow-designer-ui/src/main/frontend/src/shared/modal/modalWrapperReducer.js new file mode 100644 index 00000000..600998b6 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/modal/modalWrapperReducer.js @@ -0,0 +1,32 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { SHOW_MODAL, HIDE_MODAL } from 'shared/modal/modalWrapperActions'; + +const modalWrapperReducer = (state = {}, action) => { + const { type, payload } = action; + + switch (type) { + case SHOW_MODAL: + return { ...payload }; + case HIDE_MODAL: + return {}; + default: + return state; + } +}; + +export default modalWrapperReducer; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/navigationSideBar/NavigationLink.jsx b/sdc-workflow-designer-ui/src/main/frontend/src/shared/navigationSideBar/NavigationLink.jsx new file mode 100644 index 00000000..6688cb55 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/navigationSideBar/NavigationLink.jsx @@ -0,0 +1,48 @@ +/*! + * Copyright © 2016-2018 European Support Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; + +function getItemDataTestId(itemId) { + return itemId.split('|')[0]; +} + +const NavigationLink = ({ item, activeItemId, onClick }) => { + return ( + <div + key={'navAction_' + item.id} + className={classnames('navigation-group-item-name', { + selected: item.id === activeItemId, + disabled: item.disabled, + 'bold-name': item.expanded, + hidden: item.hidden + })} + onClick={event => onClick(event, item)} + data-test-id={'navbar-group-item-' + getItemDataTestId(item.id)}> + {item.name} + </div> + ); +}; + +NavigationLink.propTypes = { + item: PropTypes.object, + activeItemId: PropTypes.string, + onClick: PropTypes.func +}; + +export default NavigationLink; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/navigationSideBar/NavigationMenu.jsx b/sdc-workflow-designer-ui/src/main/frontend/src/shared/navigationSideBar/NavigationMenu.jsx new file mode 100644 index 00000000..f8a7d46f --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/navigationSideBar/NavigationMenu.jsx @@ -0,0 +1,39 @@ +/*! + * Copyright © 2016-2018 European Support Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import NavigationMenuItems from './NavigationMenuItems'; + +const NavigationMenu = ({ menu, activeItemId, onNavigationItemClick }) => { + return ( + <div className="navigation-group" key={menu.id}> + <NavigationMenuItems + items={menu.items} + activeItemId={activeItemId} + onNavigationItemClick={onNavigationItemClick} + /> + </div> + ); +}; + +NavigationMenu.propTypes = { + menu: PropTypes.object, + activeItemId: PropTypes.string.isRequired, + onNavigationItemClick: PropTypes.func +}; + +export default NavigationMenu; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/navigationSideBar/NavigationMenuItem.jsx b/sdc-workflow-designer-ui/src/main/frontend/src/shared/navigationSideBar/NavigationMenuItem.jsx new file mode 100644 index 00000000..540e09c7 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/navigationSideBar/NavigationMenuItem.jsx @@ -0,0 +1,44 @@ +/*! + * Copyright © 2016-2018 European Support Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import NavigationLink from './NavigationLink'; + +const NavigationMenuItem = ({ onNavigationItemClick, item, activeItemId }) => { + return ( + <div + className={classnames('navigation-group-item', { + 'selected-item': item.id === activeItemId + })} + key={'item_' + item.id}> + <NavigationLink + item={item} + activeItemId={activeItemId} + onClick={onNavigationItemClick} + /> + </div> + ); +}; + +NavigationMenuItem.propTypes = { + onNavigationItemClick: PropTypes.func, + item: PropTypes.object, + activeItemId: PropTypes.string +}; + +export default NavigationMenuItem; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/navigationSideBar/NavigationMenuItems.jsx b/sdc-workflow-designer-ui/src/main/frontend/src/shared/navigationSideBar/NavigationMenuItems.jsx new file mode 100644 index 00000000..56bad084 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/navigationSideBar/NavigationMenuItems.jsx @@ -0,0 +1,47 @@ +/*! + * Copyright © 2016-2018 European Support Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import NavigationMenuItem from './NavigationMenuItem'; + +const NavigationMenuItems = ({ + items, + activeItemId, + onNavigationItemClick +}) => { + return ( + <div className="navigation-group-items"> + {items && + items.map(item => ( + <NavigationMenuItem + key={'menuItem_' + item.id} + item={item} + activeItemId={activeItemId} + onNavigationItemClick={onNavigationItemClick} + /> + ))} + </div> + ); +}; + +NavigationMenuItems.propTypes = { + items: PropTypes.array, + activeItemId: PropTypes.string, + onNavigationItemClick: PropTypes.func +}; + +export default NavigationMenuItems; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/navigationSideBar/index.js b/sdc-workflow-designer-ui/src/main/frontend/src/shared/navigationSideBar/index.js new file mode 100644 index 00000000..b3af6f47 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/navigationSideBar/index.js @@ -0,0 +1,73 @@ +/*! + * Copyright © 2016-2018 European Support Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import NavigationMenu from './NavigationMenu'; + +class NavigationSideBar extends Component { + static propTypes = { + activeItemId: PropTypes.string, + onSelect: PropTypes.func, + onToggle: PropTypes.func, + groups: PropTypes.array, + disabled: PropTypes.bool, + onNavigationItemClick: PropTypes.func + }; + + constructor(props) { + super(props); + this.state = { + activeItemId: null + }; + this.handleItemClicked = this.handleItemClicked.bind(this); + } + + render() { + let { groups, activeItemId, disabled = false } = this.props; + + return ( + <div + className={`navigation-side-content ${ + disabled ? 'disabled' : '' + }`}> + {groups.map(group => ( + <NavigationMenu + menu={group} + activeItemId={activeItemId} + onNavigationItemClick={this.handleItemClicked} + key={'menu_' + group.id} + /> + ))} + </div> + ); + } + + handleItemClicked(event, item) { + event.stopPropagation(); + if (this.props.onToggle) { + this.props.onToggle(this.props.groups, item.id); + } + if (item.onSelect) { + item.onSelect(); + } + if (this.props.onSelect) { + this.props.onSelect(item); + } + } +} + +export default NavigationSideBar; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/notifications/Notifications.js b/sdc-workflow-designer-ui/src/main/frontend/src/shared/notifications/Notifications.js new file mode 100644 index 00000000..5be899a9 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/notifications/Notifications.js @@ -0,0 +1,36 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { connect } from 'react-redux'; +import NotificationsView from 'shared/notifications/NotificationsView'; +import { notificationActions } from 'shared/notifications/notificationsActions'; + +const mapStateToProps = state => { + return { + notifications: state.notifications + }; +}; + +function mapDispatchToProps(dispatch) { + return { + removeNotifications: payload => + dispatch(notificationActions.removeNotification(payload)) + }; +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(NotificationsView); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/notifications/NotificationsView.jsx b/sdc-workflow-designer-ui/src/main/frontend/src/shared/notifications/NotificationsView.jsx new file mode 100644 index 00000000..44812897 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/notifications/NotificationsView.jsx @@ -0,0 +1,56 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { Notification } from 'onap-ui-react'; +import { CSSTransition, TransitionGroup } from 'react-transition-group'; + +export default class NotificationsView extends PureComponent { + static propTypes = { + removeNotifications: PropTypes.func, + notifications: PropTypes.array + }; + + constructor(props) { + super(props); + } + + render() { + const { notifications, removeNotifications } = this.props; + return ( + <div className="workflow-notifications-container position-top-right"> + <TransitionGroup> + {notifications.map(item => ( + <CSSTransition + in={true} + timeout={500} + unmountOnExit + classNames="react-transition" + key={`notification-transition-${item.id}`}> + <Notification + key={item.id} + type={item.type} + title={item.title} + message={item.message} + onClick={() => removeNotifications(item)} + /> + </CSSTransition> + ))} + </TransitionGroup> + </div> + ); + } +} diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/notifications/__tests__/NotificationView_snapshot-test.js b/sdc-workflow-designer-ui/src/main/frontend/src/shared/notifications/__tests__/NotificationView_snapshot-test.js new file mode 100644 index 00000000..616f10ff --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/notifications/__tests__/NotificationView_snapshot-test.js @@ -0,0 +1,30 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import React from 'react'; +import renderer from 'react-test-renderer'; + +import NotificationsView from 'shared/notifications/NotificationsView'; + +describe('Notification View Snapshot', () => { + it('renders correctly', () => { + const tree = renderer + .create(<NotificationsView notifications={[]} />) + .toJSON(); + + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/notifications/__tests__/__snapshots__/NotificationView_snapshot-test.js.snap b/sdc-workflow-designer-ui/src/main/frontend/src/shared/notifications/__tests__/__snapshots__/NotificationView_snapshot-test.js.snap new file mode 100644 index 00000000..eb4a573f --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/notifications/__tests__/__snapshots__/NotificationView_snapshot-test.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Notification View Snapshot renders correctly 1`] = ` +<div + className="workflow-notifications-container position-top-right" +> + <div /> +</div> +`; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/notifications/__tests__/notificationsReducer-test.js b/sdc-workflow-designer-ui/src/main/frontend/src/shared/notifications/__tests__/notificationsReducer-test.js new file mode 100644 index 00000000..d308d22d --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/notifications/__tests__/notificationsReducer-test.js @@ -0,0 +1,61 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import notificationsReducer from 'shared/notifications/notificationsReducer'; +import { actionTypes } from 'shared/notifications/notificationsConstants'; + +describe('Notifications Reducer', () => { + it('Returns empty state array', () => { + expect(notificationsReducer(undefined, {})).toEqual([]); + }); + it('Add notification obj to the notification arr in store', () => { + const action = { + type: actionTypes.ADD_NOTIFICATION, + payload: { + title: 'Notification1', + message: 'This is a test', + type: 'info', + id: '4a2b0038-e442-4ccb-b577-5d04eee6fdfb', + timeout: 200 + } + }; + expect(notificationsReducer([], action)).toEqual([action.payload]); + }); + it('Remove notification instance from store', () => { + const resultState = [ + { + title: 'Notification1', + message: 'This is a test', + type: 'info', + id: '4a2b0038-e442-4ccb-b577-5d04eee6fdfb', + timeout: 200 + } + ]; + const removeAction = { + type: actionTypes.REMOVE_NOTIFICATION, + payload: { + title: 'Notification1', + message: 'This is a test', + type: 'info', + id: '4a2b0038-e442-4ccb-b577-5d04eee6fdfb', + timeout: 200 + } + }; + expect(notificationsReducer([], removeAction)).toEqual([]); + expect(notificationsReducer(resultState, removeAction)).toEqual( + resultState.filter(item => item.id !== removeAction.payload.id) + ); + }); +}); diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/notifications/notificationsActions.js b/sdc-workflow-designer-ui/src/main/frontend/src/shared/notifications/notificationsActions.js new file mode 100644 index 00000000..84f9da83 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/notifications/notificationsActions.js @@ -0,0 +1,60 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { actionTypes } from 'shared/notifications/notificationsConstants'; +import UUID from 'uuid-js'; + +export const notificationActions = { + showNotification: item => ({ + type: actionTypes.ADD_NOTIFICATION, + payload: { + ...item, + id: UUID.create().toString() + } + }), + + showSuccess: ({ title, message, timeout }) => + notificationActions.showNotification({ + title, + message, + timeout, + type: 'success' + }), + showInfo: ({ title, message, timeout }) => + notificationActions.showNotification({ + title, + message, + timeout, + type: 'info' + }), + showWarning: ({ title, message, timeout }) => + notificationActions.showNotification({ + title, + message, + timeout, + type: 'warning' + }), + showError: ({ title, message, timeout }) => + notificationActions.showNotification({ + title, + message, + timeout, + type: 'error' + }), + removeNotification: item => ({ + type: actionTypes.REMOVE_NOTIFICATION, + payload: item + }) +}; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/notifications/notificationsConstants.js b/sdc-workflow-designer-ui/src/main/frontend/src/shared/notifications/notificationsConstants.js new file mode 100644 index 00000000..3e166424 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/notifications/notificationsConstants.js @@ -0,0 +1,21 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +export const actionTypes = { + ADD_NOTIFICATION: 'notification/ADD_NOTIFICATION', + REMOVE_NOTIFICATION: 'notification/REMOVE_NOTIFICATION' +}; +export const notificationTimeout = 4000; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/notifications/notificationsReducer.js b/sdc-workflow-designer-ui/src/main/frontend/src/shared/notifications/notificationsReducer.js new file mode 100644 index 00000000..f0989cf5 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/notifications/notificationsReducer.js @@ -0,0 +1,30 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { actionTypes } from 'shared/notifications/notificationsConstants'; + +function notificationsReducer(state = [], action) { + switch (action.type) { + case actionTypes.ADD_NOTIFICATION: + return [...state, action.payload]; + case actionTypes.REMOVE_NOTIFICATION: + return state.filter(item => item.id !== action.payload.id); + default: + return state; + } +} + +export default notificationsReducer; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/notifications/notificationsSagas.js b/sdc-workflow-designer-ui/src/main/frontend/src/shared/notifications/notificationsSagas.js new file mode 100644 index 00000000..c30930a2 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/notifications/notificationsSagas.js @@ -0,0 +1,33 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* + * http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { takeEvery, put } from 'redux-saga/effects'; +import { delay } from 'redux-saga'; +import { + actionTypes, + notificationTimeout +} from 'shared/notifications/notificationsConstants.js'; +import { notificationActions } from 'shared/notifications/notificationsActions'; + +export function* handleComingNotifications(params) { + const { timeout, ...data } = params; + const interval = timeout || notificationTimeout; + yield delay(interval); + yield put(notificationActions.removeNotification(data.payload)); +} + +export function* watchNotifications() { + yield takeEvery(actionTypes.ADD_NOTIFICATION, handleComingNotifications); +} diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/pubsub/base-pubsub.ts b/sdc-workflow-designer-ui/src/main/frontend/src/shared/pubsub/base-pubsub.ts new file mode 100644 index 00000000..41e8039d --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/pubsub/base-pubsub.ts @@ -0,0 +1,127 @@ +declare const window: Window; + +export class BasePubSub { + + subscribers: Map<string, ISubscriber>; + eventsCallbacks: Array<Function>; + clientId: string; + eventsToWait: Map<string, Array<string>>; + lastEventNotified: string; + + constructor(pluginId: string) { + this.subscribers = new Map<string, ISubscriber>(); + this.eventsCallbacks = []; + this.eventsToWait = new Map<string, Array<string>>(); + this.clientId = pluginId; + this.lastEventNotified = ""; + this.onMessage = this.onMessage.bind(this); + + window.addEventListener("message", this.onMessage); + } + + public register(subscriberId: string, subscriberWindow: Window, subscriberUrl: string) { + const subscriber = { + window: subscriberWindow, + locationUrl: subscriberUrl || subscriberWindow.location.href + } as ISubscriber; + + this.subscribers.set(subscriberId, subscriber); + } + + public unregister(subscriberId: string) { + this.subscribers.delete(subscriberId); + } + + public on(callback: Function) { + let functionExists = this.eventsCallbacks.find((func: Function) => { + return callback.toString() == func.toString() + }); + + if (!functionExists) { + this.eventsCallbacks.push(callback); + } + } + + public off(callback: Function) { + let index = this.eventsCallbacks.indexOf(callback); + this.eventsCallbacks.splice(index, 1) + } + + public notify(eventType:string, eventData?:any) { + let eventObj = { + type: eventType, + data: eventData, + originId: this.clientId + } as IPubSubEvent; + + this.subscribers.forEach( (subscriber: ISubscriber, subscriberId: string) => { + subscriber.window.postMessage(eventObj, subscriber.locationUrl); + }); + + this.lastEventNotified = eventType; + + return { + subscribe: function(callbackFn) { + + if(this.subscribers.size !== 0) { + let subscribersToNotify = Array.from(this.subscribers.keys()); + + const checkNotifyComplete = (subscriberId: string) => { + + let index = subscribersToNotify.indexOf(subscriberId); + subscribersToNotify.splice(index, 1); + + if (subscribersToNotify.length === 0) { + callbackFn(); + } + }; + + this.subscribers.forEach((subscriber: ISubscriber, subscriberId: string) => { + if (this.eventsToWait.has(subscriberId) && this.eventsToWait.get(subscriberId).indexOf(eventType) !== -1) { + + const actionCompletedFunction = (eventData, subId = subscriberId) => { + if (eventData.type == "ACTION_COMPLETED") { + checkNotifyComplete(subId); + } + this.off(actionCompletedFunction); + + }; + this.on(actionCompletedFunction); + } + else { + checkNotifyComplete(subscriberId); + } + }); + } + else { + callbackFn(); + } + }.bind(this) + } + } + + public isWaitingForEvent(eventName: string) : boolean { + return Array.from(this.eventsToWait.values()).some((eventsList: Array<string>) => + eventsList.indexOf(eventName) !== -1 + ); + } + + protected onMessage(event: any) { + if (this.subscribers.has(event.data.originId)) { + this.eventsCallbacks.forEach((callback: Function) => { + callback(event.data, event); + }) + } + } +} + +export interface IPubSubEvent { + type: string; + originId: string; + data: any; +} + +export interface ISubscriber { + window: Window; + locationUrl: string; +} diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/pubsub/plugin-pubsub.ts b/sdc-workflow-designer-ui/src/main/frontend/src/shared/pubsub/plugin-pubsub.ts new file mode 100644 index 00000000..8f99e659 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/pubsub/plugin-pubsub.ts @@ -0,0 +1,30 @@ +import {BasePubSub} from 'shared/pubsub/base-pubsub'; + +declare const window: Window; + +export class PluginPubSub extends BasePubSub { + + + constructor(pluginId: string, parentUrl: string, eventsToWait?: Array<string>) { + super(pluginId); + this.register('sdc-hub', window.parent, parentUrl); + this.subscribe(eventsToWait); + } + + public subscribe(eventsToWait?: Array<string>) { + const registerData = { + pluginId: this.clientId, + eventsToWait: eventsToWait || [] + }; + + this.notify('PLUGIN_REGISTER', registerData); + } + + public unsubscribe() { + const unregisterData = { + pluginId: this.clientId + }; + + this.notify('PLUGIN_UNREGISTER', unregisterData); + } +} diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/scroll/InfiniteScroll.js b/sdc-workflow-designer-ui/src/main/frontend/src/shared/scroll/InfiniteScroll.js new file mode 100644 index 00000000..04d00120 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/scroll/InfiniteScroll.js @@ -0,0 +1,161 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; + +class InfiniteScroll extends React.Component { + constructor(props) { + super(props); + this.state = { + initialLoad: false + }; + } + componentDidMount() { + this.scrollEl = this.getScrollElement(); + this.attachScrollListener(); + this.setState({ initialLoad: true }); + } + + componentDidUpdate() { + this.attachScrollListener(); + } + + componentWillUnmount() { + this.detachScrollListener(); + } + + getParentElement(el) { + return el.parentNode; + } + + getScrollElement() { + if (this.props.useWindow) { + return window; + } + + return this.getParentElement(this.scrollComponent); + } + + detachScrollListener() { + this.scrollEl.removeEventListener( + 'scroll', + this.scrollListener, + this.props.useCapture + ); + window.removeEventListener( + 'resize', + this.scrollListener, + this.props.useCapture + ); + } + + attachScrollListener() { + if (!this.props.hasMore || !this.scrollEl) { + return; + } + + const options = { + capture: this.props.useCapture, + passive: true + }; + + this.scrollEl.addEventListener('scroll', this.scrollListener, options); + window.addEventListener('resize', this.scrollListener, options); + + this.scrollListener(); + } + + scrollListener = () => { + const el = this.scrollComponent; + const scrollEl = window; + const parentNode = this.getParentElement(el); + + let offset; + if (this.props.useWindow) { + const doc = + document.documentElement || + document.body.parentNode || + document.body; + const scrollTop = + scrollEl.pageYOffset !== undefined + ? scrollEl.pageYOffset + : doc.scrollTop; + + offset = + this.calculateTopPosition(el) + + (el.offsetHeight - scrollTop - window.innerHeight); + } else { + offset = + el.scrollHeight - + parentNode.scrollTop - + parentNode.clientHeight; + } + + // Here we make sure the element is visible as well as checking the offset + if (offset < Number(this.props.threshold) && el.offsetParent !== null) { + this.detachScrollListener(); + // Call loadMore after detachScrollListener to allow for non-async loadMore functions + if ( + typeof this.props.loadMore === 'function' && + this.state.initialLoad + ) { + this.props.loadMore(); + } + } + }; + + calculateTopPosition(el) { + if (!el) { + return 0; + } + return el.offsetTop + this.calculateTopPosition(el.offsetParent); + } + + render() { + const { children, element } = this.props; + + const props = { + ref: node => { + this.scrollComponent = node; + } + }; + + const childrenArray = [children]; + + return React.createElement(element, props, childrenArray); + } +} + +InfiniteScroll.propTypes = { + children: PropTypes.node.isRequired, + element: PropTypes.node, + hasMore: PropTypes.bool, + loadMore: PropTypes.func.isRequired, + threshold: PropTypes.number, + useCapture: PropTypes.bool, + useWindow: PropTypes.bool +}; + +InfiniteScroll.defaultProps = { + element: 'div', + hasMore: false, + threshold: 250, + useWindow: true, + useCapture: false +}; + +export default InfiniteScroll; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/scroll/Scrollbars.js b/sdc-workflow-designer-ui/src/main/frontend/src/shared/scroll/Scrollbars.js new file mode 100644 index 00000000..e5ec8cee --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/scroll/Scrollbars.js @@ -0,0 +1,58 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import PerfectScrollbar from 'perfect-scrollbar'; + +class Scrollbars extends React.Component { + containerRef = React.createRef(); + scrollbars = null; + + componentDidMount() { + const container = this.containerRef.current; + + this.scrollbars = new PerfectScrollbar(container); + } + + componentDidUpdate() { + if (this.scrollbars) { + this.scrollbars.update(); + } + } + + componentWillUnmount() { + this.scrollbars.destroy(); + this.scrollbars = null; + } + + render() { + const { children, className } = this.props; + + return ( + <div className={className} ref={this.containerRef}> + {children} + </div> + ); + } +} + +Scrollbars.propTypes = { + children: PropTypes.node, + className: PropTypes.string +}; + +export default Scrollbars; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/searchInput/SearchInput.jsx b/sdc-workflow-designer-ui/src/main/frontend/src/shared/searchInput/SearchInput.jsx new file mode 100644 index 00000000..a4ae2cd9 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/searchInput/SearchInput.jsx @@ -0,0 +1,131 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { SVGIcon } from 'onap-ui-react'; + +class ExpandableInput extends React.Component { + static propTypes = { + dataTestId: PropTypes.string, + onChange: PropTypes.func, + value: PropTypes.string + }; + + state = { showInput: !!this.props.value }; + + handleRef = input => { + this.domNode = input; + + this.domNode && this.domNode.focus(); + }; + + showInput = () => { + this.setState({ showInput: true }); + }; + + hideInput = () => { + this.setState({ showInput: false }); + }; + + closeInput = () => { + if (!this.props.value) { + this.hideInput(); + } + }; + + getValue = () => { + return this.props.value; + }; + + handleChange = e => { + this.props.onChange(e.target.value); + }; + + handleClose() { + this.props.onChange(''); + + this.hideInput(); + } + + handleKeyDown = e => { + if (e.key === 'Escape') { + e.preventDefault(); + + this.handleClose(); + } + }; + + handleBlur = () => { + if (!this.props.value) { + this.setState({ showInput: false }); + } + }; + + render() { + let { value, dataTestId } = this.props; + + const { showInput } = this.state; + + if (!showInput) { + return ( + <div className="search-input-top"> + <SVGIcon + className="search-input-wrapper closed" + name="search" + data-test-id={dataTestId} + onClick={this.showInput} + /> + </div> + ); + } + + return ( + <div className="search-input-top"> + <div className="search-input-wrapper opened"> + <div className="search-input-control"> + <input + type="text" + value={value} + ref={this.handleRef} + className="input-control" + data-test-id={`${dataTestId}-input-control`} + onChange={this.handleChange} + onKeyDown={this.handleKeyDown} + onBlur={this.handleBlur} + /> + </div> + {value && ( + <SVGIcon + data-test-id={`${dataTestId}-close-search`} + onClick={() => this.handleClose()} + name="close" + /> + )} + {!value && ( + <SVGIcon + name="search" + data-test-id={`${dataTestId}-blur-search`} + onClick={this.handleBlur} + /> + )} + </div> + </div> + ); + } +} + +export default ExpandableInput; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/shared/tree/Tree.jsx b/sdc-workflow-designer-ui/src/main/frontend/src/shared/tree/Tree.jsx new file mode 100644 index 00000000..60ef9646 --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/shared/tree/Tree.jsx @@ -0,0 +1,231 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { select } from 'd3-selection'; +import { tree, stratify } from 'd3-hierarchy'; +function diagonal(d) { + const offset = 50; + return ( + 'M' + + d.y + + ',' + + d.x + + 'C' + + (d.parent.y + offset) + + ',' + + d.x + + ' ' + + (d.parent.y + offset) + + ',' + + d.parent.x + + ' ' + + d.parent.y + + ',' + + d.parent.x + ); +} + +const nodeRadius = 8; +const verticalSpaceBetweenNodes = 70; +const NARROW_HORIZONTAL_SPACES = 47; +const WIDE_HORIZONTAL_SPACES = 65; + +const stratifyFn = stratify() + .id(d => d.id) + .parentId(d => d.parent); + +class Tree extends Component { + static propTypes = { + name: PropTypes.string, + width: PropTypes.number, + allowScaleWidth: PropTypes.bool, + nodes: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string, + name: PropTypes.string, + parent: PropTypes.string + }) + ), + selectedNodeId: PropTypes.string, + onNodeClick: PropTypes.func, + onRenderedBeyondWidth: PropTypes.func, + scrollable: PropTypes.bool, + toWiden: PropTypes.bool + }; + + static defaultProps = { + width: 500, + allowScaleWidth: true, + name: 'default-name' + }; + + render() { + let { width, name, scrollable = false } = this.props; + return ( + <div + className={`tree-view ${name}-container ${ + scrollable ? 'scrollable' : '' + }`}> + <svg width={width} className={name} /> + </div> + ); + } + + componentDidMount() { + this.renderTree(); + } + + componentDidUpdate(prevProps) { + if ( + this.props.nodes.length !== prevProps.nodes.length || + this.props.selectedNodeId !== prevProps.selectedNodeId + ) { + this.renderTree(); + } + } + + renderTree() { + let { + width, + nodes, + name, + allowScaleWidth, + selectedNodeId, + onRenderedBeyondWidth, + toWiden + } = this.props; + if (nodes.length > 0) { + let horizontalSpaceBetweenLeaves = toWiden + ? WIDE_HORIZONTAL_SPACES + : NARROW_HORIZONTAL_SPACES; + const treeFn = tree().nodeSize([ + horizontalSpaceBetweenLeaves, + verticalSpaceBetweenNodes + ]); //.size([width - 50, height - 50]) + let root = stratifyFn(nodes).sort((a, b) => + a.data.name.localeCompare(b.data.name) + ); + let svgHeight = + verticalSpaceBetweenNodes * root.height + nodeRadius * 6; + + treeFn(root); + + let nodesXValue = root.descendants().map(node => node.x); + let maxX = Math.max(...nodesXValue); + let minX = Math.min(...nodesXValue); + + let svgTempWidth = + ((maxX - minX) / 30) * horizontalSpaceBetweenLeaves; + let svgWidth = svgTempWidth < width ? width - 5 : svgTempWidth; + const svgEL = select(`svg.${name}`); + const container = select(`.tree-view.${name}-container`); + svgEL.html(''); + svgEL.attr('height', svgHeight); + let canvasWidth = width; + if (svgTempWidth > width) { + if (allowScaleWidth) { + canvasWidth = svgTempWidth; + } + // we seems to have a margin of 25px that we can still see with text + if ( + svgTempWidth - 25 > width && + onRenderedBeyondWidth !== undefined + ) { + onRenderedBeyondWidth(); + } + } + svgEL.attr('width', canvasWidth); + let rootGroup = svgEL + .append('g') + .attr( + 'transform', + `translate(${svgWidth / 2 + nodeRadius},${nodeRadius * + 4}) rotate(90)` + ); + + // handle link + rootGroup + .selectAll('.link') + .data(root.descendants().slice(1)) + .enter() + .append('path') + .attr('class', 'link') + .attr('d', diagonal); + + let node = rootGroup + .selectAll('.node') + .data(root.descendants()) + .enter() + .append('g') + .attr( + 'class', + node => + `node ${node.children ? ' has-children' : ' leaf'} ${ + node.id === selectedNodeId ? 'selectedNode' : '' + } ${this.props.onNodeClick ? 'clickable' : ''}` + ) + .attr( + 'transform', + node => 'translate(' + node.y + ',' + node.x + ')' + ) + .on('click', node => this.onNodeClick(node)); + + node.append('circle') + .attr('r', nodeRadius) + .attr('class', 'outer-circle'); + node.append('circle') + .attr('r', nodeRadius - 3) + .attr('class', 'inner-circle'); + + node.append('text') + .attr('y', nodeRadius / 4 + 1) + .attr('x', -nodeRadius * 1.8) + .text(node => node.data.name) + .attr('transform', 'rotate(-90)'); + + let selectedNode = selectedNodeId + ? root.descendants().find(node => node.id === selectedNodeId) + : null; + if (selectedNode) { + container.property( + 'scrollLeft', + svgWidth / 4 + + (svgWidth / 4 - 100) - + (selectedNode.x / 30) * horizontalSpaceBetweenLeaves + ); + container.property( + 'scrollTop', + (selectedNode.y / 100) * verticalSpaceBetweenNodes + ); + } else { + container.property( + 'scrollLeft', + svgWidth / 4 + (svgWidth / 4 - 100) + ); + } + } + } + + onNodeClick(node) { + if (this.props.onNodeClick) { + this.props.onNodeClick(node.data); + } + } +} + +export default Tree; diff --git a/sdc-workflow-designer-ui/src/main/frontend/src/store.js b/sdc-workflow-designer-ui/src/main/frontend/src/store.js new file mode 100644 index 00000000..90b55bbd --- /dev/null +++ b/sdc-workflow-designer-ui/src/main/frontend/src/store.js @@ -0,0 +1,42 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import { createStore, applyMiddleware, compose } from 'redux'; +import thunk from 'redux-thunk'; +import createSagaMiddleware from 'redux-saga'; +import { syncTranslationWithStore } from 'react-redux-i18n'; + +import reducers from './rootReducers'; +import rootSaga from './rootSaga'; + +const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; +const sagaMiddleware = createSagaMiddleware(); + +const middleware = [thunk, sagaMiddleware]; + +const store = createStore( + reducers, + composeEnhancers(applyMiddleware(...middleware)) +); + +const rootSagaTask = sagaMiddleware.run(rootSaga); +syncTranslationWithStore(store); + +rootSagaTask.done.catch(function(err) { + console.log(err); +}); + +export default store; |