├── src
├── Views
│ ├── NotFound
│ │ ├── styles.css
│ │ └── index.js
│ ├── Organigram
│ │ ├── styles.css
│ │ └── index.js
│ └── Home
│ │ ├── styles.css
│ │ └── index.js
├── globals
│ ├── constants.js
│ ├── styles
│ │ ├── globalStyles.css
│ │ └── normalize.css
│ ├── actions.js
│ ├── __tests__
│ │ ├── actions.js
│ │ └── reducers.js
│ └── reducers.js
├── Components
│ ├── AppContainer
│ │ ├── styles.css
│ │ └── index.js
│ └── EmployeeNode
│ │ ├── assets
│ │ └── arrow.svg
│ │ ├── DragContainer.js
│ │ ├── DropContainer.js
│ │ ├── styles.css
│ │ └── index.js
├── index.js
├── index.html
├── store.js
├── utilities
│ ├── exportJson.js
│ ├── __tests__
│ │ ├── moveEmployee.js
│ │ ├── deNormalizeDataStructure.js
│ │ ├── isSupervisor.js
│ │ └── normalizeDataStructure.js
│ ├── moveEmployee.js
│ ├── deNormalizeDataStructure.js
│ ├── isSupervisor.js
│ └── normalizeDataStructure.js
├── routes.js
└── sample_json.json
├── .gitignore
├── previews
├── preview_1.png
└── preview_2.png
├── .babelrc
├── .eslintrc.js
├── LICENSE
├── webpack.config.js
├── package.json
└── README.md
/src/Views/NotFound/styles.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
--------------------------------------------------------------------------------
/src/globals/constants.js:
--------------------------------------------------------------------------------
1 | export const UPDATE_EMPLOYEE_DATA = 'UPDATE_EMPLOYEE_DATA'
--------------------------------------------------------------------------------
/previews/preview_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/proshoumma/organigram/HEAD/previews/preview_1.png
--------------------------------------------------------------------------------
/previews/preview_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/proshoumma/organigram/HEAD/previews/preview_2.png
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["babel-preset-env", "react"],
3 | "plugins": ["transform-object-rest-spread"]
4 | }
5 |
--------------------------------------------------------------------------------
/src/Components/AppContainer/styles.css:
--------------------------------------------------------------------------------
1 | .appContainer {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 |
6 | padding-bottom: 30px;
7 | }
--------------------------------------------------------------------------------
/src/Views/NotFound/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import './styles.css'
3 |
4 | class NotFound extends Component {
5 | render() {
6 | return (
7 |
8 | 404, Not Found
9 |
10 | )
11 | }
12 | }
13 |
14 | export default NotFound
--------------------------------------------------------------------------------
/src/globals/styles/globalStyles.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --content-box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
3 | --theme-color: #e74c3c;
4 | --theme-color-deep: #c0392b;
5 | }
6 |
7 | html,
8 | body {
9 | font-family: 'Quicksand', sans-serif;
10 | background-color: rgb(239, 240, 242);
11 | }
--------------------------------------------------------------------------------
/src/globals/actions.js:
--------------------------------------------------------------------------------
1 | import { UPDATE_EMPLOYEE_DATA } from './constants'
2 |
3 | /**
4 | * update employee list action
5 | * @param {object} data employee list object (normalized)
6 | */
7 | export const updateEmployeeData = (data) => ({
8 | type: UPDATE_EMPLOYEE_DATA,
9 | payload: data,
10 | })
--------------------------------------------------------------------------------
/src/Components/AppContainer/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import propTypes from 'prop-types'
3 |
4 | import './styles.css'
5 |
6 | const AppContainer = ({ children }) => (
7 |
8 | { children }
9 |
10 | )
11 |
12 | AppContainer.propTypes = {
13 | children: propTypes.object
14 | }
15 |
16 | export default AppContainer
--------------------------------------------------------------------------------
/src/globals/__tests__/actions.js:
--------------------------------------------------------------------------------
1 | import { UPDATE_EMPLOYEE_DATA } from '../constants'
2 | import * as actionTypes from '../actions'
3 |
4 | describe('global actions', () => {
5 | it('should provide an action to update employee data', () => {
6 | const expectedAction = {
7 | type: UPDATE_EMPLOYEE_DATA,
8 | payload: {}
9 | }
10 |
11 | expect(actionTypes.updateEmployeeData({})).toEqual(expectedAction)
12 | })
13 | })
--------------------------------------------------------------------------------
/src/Components/EmployeeNode/assets/arrow.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import { Provider } from 'react-redux'
4 |
5 | import Routes from './routes'
6 | import store from './store'
7 | import './globals/styles/normalize.css'
8 | import './globals/styles/globalStyles.css'
9 |
10 | const App = () => (
11 |
12 |
13 |
14 | )
15 |
16 | // render the app component to root div element
17 | ReactDOM.render(
18 | ,
19 | document.getElementById('root')
20 | )
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Organigram
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, compose, combineReducers } from 'redux'
2 |
3 | import globalReducer from './globals/reducers'
4 |
5 | /**
6 | * feature based reducer splitting
7 | * for better scalability and isolation
8 | */
9 | const rootReducer = combineReducers({
10 | global: globalReducer,
11 | })
12 |
13 | // redux dev tool extension
14 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
15 |
16 | // application store
17 | let store = createStore(
18 | rootReducer,
19 | composeEnhancers()
20 | )
21 |
22 | export default store
--------------------------------------------------------------------------------
/src/globals/reducers.js:
--------------------------------------------------------------------------------
1 | import { UPDATE_EMPLOYEE_DATA } from './constants'
2 |
3 | export const defaultState = {
4 | employeeData: {}
5 | }
6 |
7 | /**
8 | * global reducer for handling any global state requirement
9 | * @param {object} state current state of the global reducer
10 | * @param {object} action performed action object
11 | */
12 | const globalAppReducer = (state = defaultState, action) => {
13 | switch(action.type) {
14 | case UPDATE_EMPLOYEE_DATA: {
15 | return {
16 | ...state,
17 | employeeData: action.payload
18 | }
19 | }
20 |
21 | default: {
22 | return state
23 | }
24 | }
25 | }
26 |
27 | export default globalAppReducer
--------------------------------------------------------------------------------
/src/utilities/exportJson.js:
--------------------------------------------------------------------------------
1 | import deNormalizeDataStructure from './deNormalizeDataStructure'
2 |
3 | /**
4 | * convert and export the employee tree to user's local machine
5 | * @param {object} employeeObjList nomalized employee object list from redux-store
6 | */
7 | const exportJson = (employeeObjList) => {
8 | // de-normalize the data first
9 | const deNormalizedData = deNormalizeDataStructure(employeeObjList)
10 |
11 | // perform download operation
12 | const a = document.createElement('a')
13 | const file = new Blob([JSON.stringify(deNormalizedData)], { type: 'application/json' })
14 | a.href = URL.createObjectURL(file)
15 | a.download = 'organigram'
16 | a.click()
17 | }
18 |
19 | export default exportJson
--------------------------------------------------------------------------------
/src/globals/__tests__/reducers.js:
--------------------------------------------------------------------------------
1 | import { UPDATE_EMPLOYEE_DATA } from '../constants'
2 | import globalReducer, { defaultState } from '../reducers'
3 |
4 | describe('global reducers', () => {
5 | it('should return the initial state', () => {
6 | expect(
7 | globalReducer(undefined, {})
8 | ).toEqual(defaultState)
9 | })
10 |
11 | it(`should handle ${UPDATE_EMPLOYEE_DATA}`, () => {
12 | const payload = {
13 | 'Nick': {
14 | name: 'Nick',
15 | position: 'Lead Engineer',
16 | employees: {}
17 | }
18 | }
19 |
20 | const expectedResult = {
21 | employeeData: payload
22 | }
23 |
24 | expect(
25 | globalReducer({}, {
26 | type: UPDATE_EMPLOYEE_DATA,
27 | payload,
28 | })
29 | ).toEqual(expectedResult)
30 | })
31 | })
--------------------------------------------------------------------------------
/src/utilities/__tests__/moveEmployee.js:
--------------------------------------------------------------------------------
1 | import moveEmployee from '../moveEmployee'
2 | import normalizedTree from '../mockData/perfectTreeResult'
3 | import moveResult from '../mockData/treeMoveResult'
4 |
5 | describe('moveEmployee function', () => {
6 | it('should exists and its a function', () => {
7 | expect(moveEmployee).not.toBeFalsy()
8 | expect(moveEmployee).toBeInstanceOf(Function)
9 | })
10 |
11 | it('should throw an error if no parameter is passed', () => {
12 | expect(() => {
13 | moveEmployee()
14 | }).toThrow()
15 | })
16 |
17 | it('should move an employee from its supervisor to new supervisor', () => {
18 | const result = moveEmployee(
19 | 'Nick',
20 | 'Sophie',
21 | 'Jonas',
22 | normalizedTree
23 | )
24 |
25 | expect(result).toEqual(moveResult)
26 | })
27 | })
--------------------------------------------------------------------------------
/src/utilities/__tests__/deNormalizeDataStructure.js:
--------------------------------------------------------------------------------
1 | import deNormalizeDataStructure from '../deNormalizeDataStructure'
2 | import perfectTreeResult from '../mockData/perfectTreeResult'
3 | import perfectTreeDeNormalized from '../mockData/perfectTreeDeNormalized'
4 |
5 | describe('deNormalizeDataStructure function', () => {
6 | it('should exists and its a function', () => {
7 | expect(deNormalizeDataStructure).not.toBeFalsy()
8 | expect(deNormalizeDataStructure).toBeInstanceOf(Function)
9 | })
10 |
11 | it('should throw an error when no parameter is passed', () => {
12 | expect(() => { deNormalizeDataStructure() }).toThrow()
13 | })
14 |
15 | it('should denomalize a normalized employee structrue', () => {
16 | const result = deNormalizeDataStructure(perfectTreeResult)
17 | expect(result).toEqual(perfectTreeDeNormalized)
18 | })
19 | })
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "env": {
3 | "browser": true,
4 | "commonjs": true,
5 | "es6": true,
6 | "node": true,
7 | "jest": true
8 | },
9 | "extends": [
10 | "eslint:recommended",
11 | "plugin:react/recommended"
12 | ],
13 | "parserOptions": {
14 | "ecmaFeatures": {
15 | "jsx": true
16 | },
17 | "ecmaVersion": 2018,
18 | "sourceType": "module"
19 | },
20 | "plugins": [
21 | "react"
22 | ],
23 | "globals": {
24 | "System": true,
25 | },
26 | "rules": {
27 | "linebreak-style": [
28 | "error",
29 | "unix"
30 | ],
31 | "quotes": [
32 | "error",
33 | "single"
34 | ],
35 | "semi": [
36 | "error",
37 | "never"
38 | ]
39 | }
40 | };
--------------------------------------------------------------------------------
/src/utilities/moveEmployee.js:
--------------------------------------------------------------------------------
1 | /**
2 | * move operation for drag and dropped employee objects
3 | * @param {string} sourceId source/dragged employee id
4 | * @param {string} sourceSupervisorId source/dragged employee's supervisor id
5 | * @param {string} destinationId destination/dropped employee id
6 | * @param {object} employeeListObj normalized employee list object from redux-store
7 | */
8 | const moveEmployee = (
9 | sourceId,
10 | sourceSupervisorId,
11 | destinationId,
12 | employeeListObj
13 | ) => {
14 | // define a new employee list object
15 | const newEmployeeList = employeeListObj
16 |
17 | // remove the employee from its current supervisor
18 | delete newEmployeeList[sourceSupervisorId].employees[sourceId]
19 |
20 | // add the employee to its new supervisor
21 | newEmployeeList[destinationId].employees[sourceId] = true
22 |
23 | // return the new list
24 | return newEmployeeList
25 | }
26 |
27 | export default moveEmployee
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Provash Shoumma
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/src/Components/EmployeeNode/DragContainer.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { DragSource } from 'react-dnd'
3 | import propTypes from 'prop-types'
4 |
5 | class DragContainer extends Component {
6 | render() {
7 | const {
8 | children,
9 | className,
10 | connectDragSource
11 | } = this.props
12 |
13 | return connectDragSource (
14 |
15 | { children }
16 |
17 | )
18 | }
19 | }
20 |
21 | DragContainer.propTypes = {
22 | employeeInfo: propTypes.object,
23 | className: propTypes.string,
24 | children: propTypes.any,
25 | connectDragSource: propTypes.func
26 | }
27 |
28 | export default DragSource (
29 | // item type of the dragged item
30 | 'EmployeeNode',
31 | {
32 | // return employeeInfo for drop target to
33 | // catch this data for any drop action
34 | beginDrag: (props) => ({
35 | ...props.employeeInfo
36 | })
37 | },
38 | // props to pass down to the component
39 | (connect, monitor) => ({
40 | connectDragSource: connect.dragSource(),
41 | isDragging: monitor.isDragging()
42 | })
43 | )(DragContainer)
44 |
--------------------------------------------------------------------------------
/src/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Router, hashHistory } from 'react-router'
3 |
4 | import AppContainer from './Components/AppContainer'
5 | import HomeView from './Views/Home'
6 |
7 | /**
8 | * route mapping with code splitting
9 | * to only load necessery javascript code for
10 | * a perticular screen when it loads in the browser :-)
11 | */
12 | const routeMappings = {
13 | component: AppContainer,
14 | path: '/',
15 | indexRoute: {
16 | component: HomeView
17 | },
18 | childRoutes: [
19 | {
20 | path: 'organigram_view',
21 | getComponent(location, cb) {
22 | // async call for loading the view
23 | System.import('./Views/Organigram').then(
24 | module => cb(null, module.default)
25 | )
26 | }
27 | },
28 | {
29 | path: '*',
30 | getComponent(location, cb) {
31 | // async call for loading the view
32 | System.import('./Views/NotFound').then(
33 | module => cb(null, module.default)
34 | )
35 | }
36 | }
37 | ]
38 | }
39 |
40 | // main router component with hashHistory
41 | const Routes = () => (
42 |
46 | )
47 |
48 | export default Routes
--------------------------------------------------------------------------------
/src/Views/Organigram/styles.css:
--------------------------------------------------------------------------------
1 | .organigramViewContainer {
2 | width: 960px;
3 | }
4 |
5 | /**
6 | * top bar styles
7 | */
8 | .topBar {
9 | display: flex;
10 | justify-content: space-between;
11 | align-items: center;
12 | padding: 20px 10px;
13 | }
14 |
15 | .topBarLogo {
16 | font-family: 'Pacifico', cursive;
17 | font-size: 24px;
18 | color: #444;
19 | }
20 |
21 | .navContainer {
22 | display: flex;
23 | }
24 |
25 | .navButton {
26 | padding: 0px 10px;
27 |
28 | font-size: 14px;
29 | font-weight: 500;
30 | text-transform: uppercase;
31 | letter-spacing: 1px;
32 | user-select: none;
33 |
34 | color: var(--theme-color);
35 | cursor: pointer;
36 | }
37 |
38 | .navButton:hover {
39 | color: var(--theme-color-deep);
40 | }
41 |
42 | /**
43 | * tree container styles
44 | */
45 | .treeContainerBox {
46 | padding: 20px 25px;
47 | background-color: white;
48 | border-radius: 5px;
49 | box-shadow: var(--content-box-shadow);
50 | }
51 |
52 | .messageBox {
53 | margin-bottom: 10px;
54 |
55 | font-size: 14px;
56 | letter-spacing: 0.5px;
57 | text-align: center;
58 |
59 | color: #666;
60 | }
61 |
62 | .treeContainer {
63 | display: flex;
64 | padding-left: 30px;
65 | border: 1px dashed #ddd;
66 | }
67 |
68 | .noData {
69 | padding: 10px 0px;
70 | }
--------------------------------------------------------------------------------
/src/utilities/__tests__/isSupervisor.js:
--------------------------------------------------------------------------------
1 | import isSupervisor from '../isSupervisor'
2 | import normalizedTree from '../mockData/perfectTreeResult'
3 |
4 | describe('isSupervisor function', () => {
5 | it('should exists and its a function', () => {
6 | expect(isSupervisor).not.toBeFalsy()
7 | expect(isSupervisor).toBeInstanceOf(Function)
8 | })
9 |
10 | it('should throw error if no parameter is passed', () => {
11 | expect(() => {
12 | isSupervisor()
13 | }).toThrow()
14 | })
15 |
16 | it('should return true if a dragged employee is supervisor of dropped employee', () => {
17 | const result = isSupervisor(
18 | 'Jonas',
19 | 'Sophie',
20 | normalizedTree
21 | )
22 |
23 | expect(result).toBe(true)
24 | })
25 |
26 | it('should return false if a dragged employee is not a supervisor of dropped employee', () => {
27 | const result = isSupervisor(
28 | 'Nick',
29 | 'Jonas',
30 | normalizedTree
31 | )
32 |
33 | expect(result).toBe(false)
34 | })
35 |
36 | it('should return true if a dragged employee is a great-supervisor of dropped employee', () => {
37 | const result = isSupervisor(
38 | 'Jonas',
39 | 'Barbara',
40 | normalizedTree
41 | )
42 |
43 | expect(result).toBe(true)
44 | })
45 | })
--------------------------------------------------------------------------------
/src/Components/EmployeeNode/DropContainer.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { DropTarget } from 'react-dnd'
3 | import propTypes from 'prop-types'
4 |
5 | class DropContainer extends Component {
6 | render() {
7 | const {
8 | children,
9 | className,
10 | isOver,
11 | connectDropTarget
12 | } = this.props
13 |
14 | return connectDropTarget (
15 |
16 | { children }
17 |
18 | )
19 | }
20 | }
21 |
22 | DropContainer.propTypes = {
23 | employeeInfo: propTypes.object,
24 | className: propTypes.string,
25 | children: propTypes.any,
26 | isOver: propTypes.bool,
27 | dropAction: propTypes.func,
28 | connectDropTarget: propTypes.func
29 | }
30 |
31 | export default DropTarget (
32 | // type of item that can be dropped
33 | 'EmployeeNode',
34 | {
35 | // when item is dropped on a target
36 | drop: (props, monitor) => {
37 | // get what is dropped
38 | const item = monitor.getItem()
39 |
40 | // call the drop action
41 | props.dropAction(
42 | item.name,
43 | item.supervisor,
44 | props.employeeInfo.name
45 | )
46 | }
47 | },
48 | // props to include in the component
49 | (connect, monitor) => ({
50 | connectDropTarget: connect.dropTarget(),
51 | isOver: monitor.isOver()
52 | })
53 | )(DropContainer)
54 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack')
2 | var path = require('path')
3 | var HtmlWebpackPlugin = require('html-webpack-plugin')
4 |
5 | // splitting vedor libraries to reduce bundle size
6 | // and take benifit of browser cache mechanism
7 | const VENDOR_LIBS = [
8 | 'react',
9 | 'react-dom',
10 | 'redux',
11 | 'react-redux'
12 | ]
13 |
14 | module.exports = {
15 | entry: {
16 | bundle: './src/index.js',
17 | vendor: VENDOR_LIBS
18 | },
19 | output: {
20 | path: path.join(__dirname, 'dist'),
21 | filename: '[name].[chunkhash].js'
22 | },
23 | module: {
24 | rules: [
25 | {
26 | use: 'babel-loader',
27 | test: /\.js$/,
28 | exclude: /node_modules/
29 | },
30 | {
31 | use: ['style-loader', 'css-loader'],
32 | test: /\.css$/
33 | },
34 | {
35 | test: /\.(jpe?g|png|gif|svg)$/,
36 | use: [
37 | {
38 | loader: 'url-loader',
39 | options: { limit: 40000 }
40 | },
41 | 'image-webpack-loader'
42 | ]
43 | }
44 | ]
45 | },
46 | plugins: [
47 | new webpack.DefinePlugin({
48 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
49 | }),
50 | new webpack.optimize.CommonsChunkPlugin({
51 | names: ['vendor', 'manifest']
52 | }),
53 | new HtmlWebpackPlugin({
54 | template: 'src/index.html'
55 | })
56 | ]
57 | }
58 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "organigram",
3 | "version": "0.0.1",
4 | "main": "index.js",
5 | "scripts": {
6 | "clean": "rimraf dist",
7 | "build": "NODE_ENV=production npm run clean && webpack -p",
8 | "serve:dev": "webpack-dev-server",
9 | "serve": "http-server dist",
10 | "test": "jest --verbose"
11 | },
12 | "license": "MIT",
13 | "private": true,
14 | "devDependencies": {
15 | "babel-core": "^6.17.0",
16 | "babel-jest": "^23.4.2",
17 | "babel-loader": "^6.2.0",
18 | "babel-plugin-transform-object-rest-spread": "^6.26.0",
19 | "babel-preset-env": "^1.1.4",
20 | "babel-preset-react": "^6.16.0",
21 | "css-loader": "^0.26.1",
22 | "eslint": "^5.3.0",
23 | "eslint-plugin-react": "^7.10.0",
24 | "html-webpack-plugin": "^2.24.1",
25 | "image-webpack-loader": "^4.3.1",
26 | "jest": "^23.5.0",
27 | "regenerator-runtime": "^0.12.1",
28 | "rimraf": "^2.5.4",
29 | "style-loader": "^0.13.1",
30 | "url-loader": "^1.0.1",
31 | "webpack": "^2.2.0-rc.0",
32 | "webpack-dev-server": "^2.2.0-rc.0"
33 | },
34 | "dependencies": {
35 | "http-server": "^0.11.1",
36 | "prop-types": "^15.6.2",
37 | "react": "^16.4.2",
38 | "react-dnd": "^5.0.0",
39 | "react-dnd-html5-backend": "^5.0.1",
40 | "react-dom": "^16.4.2",
41 | "react-dropzone": "^4.3.0",
42 | "react-redux": "^4.4.6",
43 | "react-router": "^3.0.0",
44 | "redux": "^3.6.0"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/utilities/deNormalizeDataStructure.js:
--------------------------------------------------------------------------------
1 | /**
2 | * recursive call for denormazlising the data structure
3 | * @param {string} currentEmployeeId current id of the tree node
4 | * @param {object} employeeList list of normalized employee object
5 | * @param {object} supervisorRef reference to supervisor of current node
6 | */
7 | const denormalize = (currentEmployeeId, employeeList, supervisorRef = {}) => {
8 | const position = employeeList[currentEmployeeId].position
9 | const subordinates = Object.keys(employeeList[currentEmployeeId].employees)
10 |
11 | // structure the employee object
12 | const employeeObj = {
13 | [currentEmployeeId]: {
14 | position,
15 | employees: subordinates.map((eachSubordinateId) => {
16 | // recursive call for denormalizing the subordinates
17 | return denormalize(
18 | eachSubordinateId,
19 | employeeList,
20 | employeeObj
21 | )
22 | })
23 | }
24 | }
25 |
26 | // supervisor refence to current node
27 | supervisorRef = employeeObj
28 |
29 | return supervisorRef
30 | }
31 |
32 | /**
33 | * de-normalize the data structure of employee list
34 | * for exporting or making api calls
35 | * @param {object} employeeListObj normalized employee list object from redux store
36 | */
37 | const deNormalizeDataStructure = (employeeListObj) => {
38 | const treeRootId = Object.keys(employeeListObj)[0]
39 | const denormalizedData = denormalize(treeRootId,employeeListObj)
40 |
41 | return denormalizedData
42 | }
43 |
44 | export default deNormalizeDataStructure
--------------------------------------------------------------------------------
/src/utilities/__tests__/normalizeDataStructure.js:
--------------------------------------------------------------------------------
1 | import normalizeDataStructure from '../normalizeDataStructure'
2 | import perfectTreeResult from '../mockData/perfectTreeResult'
3 | import perfectTreeDeNormalized from '../mockData/perfectTreeDeNormalized'
4 | import treeWithInvalidStructure from '../mockData/treeWithInvalidStructure'
5 | import treeWithALoop from '../mockData/treeWithALoop'
6 | import treeWithMultipleRoot from '../mockData/treeWithMultipleRoot'
7 |
8 | describe('normalizeDataStructure function', () => {
9 | it('should exists and its a function', () => {
10 | expect(normalizeDataStructure).not.toBeFalsy()
11 | expect(normalizeDataStructure).toBeInstanceOf(Function)
12 | })
13 |
14 | it('should throw an error if nothing is passed', () => {
15 | expect(() => { normalizeDataStructure() }).toThrow()
16 | })
17 |
18 | it('should normalize a perfectly structured tree', () => {
19 | const result = normalizeDataStructure(perfectTreeDeNormalized)
20 | expect(result).toEqual(perfectTreeResult)
21 | })
22 |
23 | it('should throw an error for a tree with invalid structure', () => {
24 | expect(() => {
25 | normalizeDataStructure(treeWithInvalidStructure)
26 | }).toThrow()
27 | })
28 |
29 | it('should throw an error for a tree with a loop employee object', () => {
30 | expect(() => {
31 | normalizeDataStructure(treeWithALoop)
32 | }).toThrow()
33 | })
34 |
35 | it('should throw an error for a tree with multiple root employees', () => {
36 | expect(() => {
37 | normalizeDataStructure(treeWithMultipleRoot)
38 | }).toThrow()
39 | })
40 | })
--------------------------------------------------------------------------------
/src/utilities/isSupervisor.js:
--------------------------------------------------------------------------------
1 | /**
2 | * recursive function to check if currect node match with target employee
3 | * if its matched, it means that the source employee is a supervisor or
4 | * a great-supervisor of the target employee
5 | *
6 | * @param {string} sourceId employee id
7 | * @param {string} targetId id of the employee where the moved employee will included
8 | * @param {object} employeeList nomalized employee object
9 | * @param {Array} matchArray array saving the result of the check
10 | */
11 | const checkSupervisor = (
12 | sourceId,
13 | targetId,
14 | employeeList,
15 | matchArray = []
16 | ) => {
17 | // check if current node id matches target id
18 | if (sourceId === targetId) matchArray.push(true)
19 | else matchArray.push(false)
20 |
21 | // map over the subordinates
22 | const subordinates = Object.keys(employeeList[sourceId].employees)
23 | subordinates.map(each => {
24 | checkSupervisor(each, targetId, employeeList, matchArray)
25 | })
26 |
27 | return matchArray
28 | }
29 |
30 | /**
31 | * check if the source/dragged employee is a supervisor or
32 | * great supervisor of the targed/dropeed employee
33 | *
34 | * @param {string} sourceId id of the employee that is dragged
35 | * @param {string} targetId id of the employee where the source will get included
36 | * @param {object} employeeList nomalized employee list object
37 | */
38 | const isSupervisor = (sourceId, targetId, employeeList) => {
39 | let result = false
40 |
41 | const checkArray = checkSupervisor(sourceId, targetId, employeeList)
42 | checkArray.map((eachCheck) => {
43 | if (eachCheck === true) result = true
44 | })
45 |
46 | return result
47 | }
48 |
49 | export default isSupervisor
--------------------------------------------------------------------------------
/src/utilities/normalizeDataStructure.js:
--------------------------------------------------------------------------------
1 | /**
2 | * recursive function to nomalize the data structure
3 | * @param {object} dataTree current node of the data tree
4 | * @param {object} newStructure new data structure
5 | */
6 | const normalizeData = (dataTree, newStructure) => {
7 | const currentTreeRootId = Object.keys(dataTree)[0]
8 | const currentTreeRoot = dataTree[currentTreeRootId]
9 |
10 | // validate the object structure
11 | if (
12 | !currentTreeRootId ||
13 | !dataTree[currentTreeRootId].position
14 | ) {
15 | throw Error(
16 | 'Sorry, the structure of the JSON file is not valid.'
17 | )
18 | }
19 |
20 | // check if the employee already in the list
21 | if (newStructure[currentTreeRootId]) {
22 | throw Error(
23 | `Sorry, employee ${currentTreeRootId} is in multiple places in the JSON file.`
24 | )
25 | }
26 |
27 | // new structure for each employee
28 | newStructure[currentTreeRootId] = {
29 | name: currentTreeRootId,
30 | position: currentTreeRoot.position,
31 | employees: {}
32 | }
33 |
34 | // check if current employee have any subordinate
35 | if (currentTreeRoot.employees) {
36 | currentTreeRoot.employees.map(eachEmployee => {
37 | const employeeId = Object.keys(eachEmployee)[0]
38 | newStructure[currentTreeRootId].employees[employeeId] = true
39 |
40 | // recursive call with the new node of the tree
41 | normalizeData(eachEmployee, newStructure)
42 | })
43 | }
44 |
45 | return newStructure
46 | }
47 |
48 | /**
49 | * normalize the employee structure for better managing organigram
50 | * and better performance with O(1) complexity when drag and dropped
51 | * @param {object} employeeRawObject raw parsed json employee object
52 | */
53 | const normalizeDataStructure = (employeeRawObject = {}) => {
54 | const rootLenght = Object.keys(employeeRawObject).length
55 |
56 | // check if the there is multiple root element in the tree
57 | if (rootLenght > 1) {
58 | throw Error(
59 | 'Sorry, the JSON contains multiple core supervisors.'
60 | )
61 | }
62 |
63 | return normalizeData(employeeRawObject, {})
64 | }
65 |
66 | export default normalizeDataStructure
--------------------------------------------------------------------------------
/src/sample_json.json:
--------------------------------------------------------------------------------
1 | {
2 | "Professor Albus Dumbledore": {
3 | "position": "CEO",
4 | "employees": [
5 | {
6 | "Professor McGonagall": {
7 | "position": "VP Engineering",
8 | "employees": [
9 | {
10 | "Harry Potter": {
11 | "position": "Frontend Lead",
12 | "employees": [
13 | {
14 | "Hermione Granger": {
15 | "position": "Frontend Engineer",
16 | "employees": []
17 | }
18 | },
19 | {
20 | "Ron Weasley": {
21 | "position": "Frontend Engineer",
22 | "employees": []
23 | }
24 | }
25 | ]
26 | }
27 | },
28 | {
29 | "Ginny Weasley": {
30 | "position": "Art Director",
31 | "employees": [
32 | {
33 | "Luna Lovegood": {
34 | "position": "UX Designer",
35 | "employees": []
36 | }
37 | },
38 | {
39 | "Neville Longbottom": {
40 | "position": "UX Designer",
41 | "employees": []
42 | }
43 | }
44 | ]
45 | }
46 | }
47 | ]
48 | }
49 | },
50 | {
51 | "Professor Severus Snape": {
52 | "position": "Director of Digital Business Solution",
53 | "employees": [
54 | {
55 | "Draco Malfoy": {
56 | "position": "Project Manager",
57 | "employees": []
58 | }
59 | },
60 | {
61 | "Bellatrix Lestrange": {
62 | "position": "Project Manager",
63 | "employees": []
64 | }
65 | },
66 | {
67 | "Peter Pettigrew": {
68 | "position": "Project Manager",
69 | "employees": []
70 | }
71 | }
72 | ]
73 | }
74 | }
75 | ]
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/Views/Home/styles.css:
--------------------------------------------------------------------------------
1 | .homeContainer {
2 | width: 100vw;
3 | height: 100vh;
4 |
5 | display: flex;
6 | flex-direction: column;
7 | justify-content: center;
8 | align-items: center;
9 | }
10 |
11 | .contentBox {
12 | width: 600px;
13 | height: 350px;
14 | padding: 20px 40px;
15 | margin: 0px 10px;
16 |
17 | display: flex;
18 | flex-direction: column;
19 | justify-content: center;
20 | align-items: center;
21 |
22 | background-color: white;
23 | border-radius: 5px;
24 | box-shadow: var(--content-box-shadow);
25 | }
26 |
27 | .activeFileType {
28 | background-color: rgb(200, 255, 200);
29 | }
30 |
31 | .rejectFileType {
32 | background-color: rgb(255, 200, 255);
33 | }
34 |
35 | .logo {
36 | font-family: 'Pacifico', cursive;
37 | font-size: 36px;
38 |
39 | margin-bottom: 70px;
40 | }
41 |
42 | .info {
43 | padding-left: 10px;
44 | padding-right: 10px;
45 | margin-bottom: 50px;
46 |
47 | font-size: 24px;
48 | text-align: center;
49 | line-height: 1.5em;
50 | color: #444;
51 | }
52 |
53 | /**
54 | * file input styles
55 | */
56 | .fileInput {
57 | width: 0.1px;
58 | height: 0.1px;
59 |
60 | opacity: 0;
61 | overflow: hidden;
62 | position: absolute;
63 |
64 | z-index: -1;
65 | }
66 |
67 | .fileInput + .inputLabel {
68 | padding: 5px 10px;
69 |
70 | color: var(--theme-color);
71 | font-size: 14px;
72 | font-weight: 500;
73 | letter-spacing: 1px;
74 |
75 | border-radius: 10px;
76 | display: inline-block;
77 | cursor: pointer;
78 | }
79 |
80 | .fileInput:focus + .inputLabel,
81 | .fileInput + .inputLabel:hover {
82 | color: var(--theme-color-deep);
83 | }
84 |
85 | .fileInput:focus + .inputLabel {
86 | outline: 1px dotted #000;
87 | outline: -webkit-focus-ring-color auto 5px;
88 | }
89 |
90 | .dummyDataButton {
91 | margin: 0px;
92 | background: none;
93 | border: none;
94 | outline: none;
95 |
96 | padding: 10px 20px;
97 | cursor: pointer;
98 | color: #aaa;
99 | font-weight: 500;
100 |
101 | transition: color 0.3s;
102 | }
103 |
104 | .dummyDataButton:hover {
105 | color: #444;
106 | transition: color 0.3s;
107 | }
108 |
109 | /**
110 | * error message styling
111 | */
112 | .errorMessage {
113 | width: 500px;
114 | margin: 20px 0px;
115 |
116 | font-size: 14px;
117 | font-weight: 500;
118 | letter-spacing: 0.5px;
119 | text-align: center;
120 | }
--------------------------------------------------------------------------------
/src/Components/EmployeeNode/styles.css:
--------------------------------------------------------------------------------
1 | /**
2 | * container styles
3 | */
4 | .employeeNodeContainer {
5 | position: relative;
6 | flex: 1;
7 | display: flex;
8 | flex-direction: column;
9 | }
10 |
11 | .employeeNodeRow {
12 | display: flex;
13 | align-items: center;
14 | margin: 6px 0px;
15 | }
16 |
17 | .employeeNodeRow::before {
18 | content: '';
19 | position: absolute;
20 | left: -29px;
21 | width: 27px;
22 | height: 1px;
23 | border-top: 1px dashed #ddd;
24 | }
25 |
26 | .employeeSubordinatesContainer {
27 | margin-left: 10px;
28 | padding-left: 30px;
29 | border-left: 1px dashed #ddd;
30 | }
31 |
32 | /* .employeeSubordinatesContainer:last-child {
33 | border-left: 1px dashed #fff;
34 | } */
35 |
36 | /**
37 | * collapse icon styles
38 | */
39 | .collapseIconContainer {
40 | position: absolute;
41 | left: -44px;
42 |
43 | width: 28px;
44 | height: 28px;
45 |
46 | display: flex;
47 | justify-content: center;
48 | align-items: center;
49 |
50 | transform: rotate(90deg);
51 | transition: all 0.3s;
52 |
53 | cursor: pointer;
54 | border-radius: 15px;
55 | background-color: white;
56 | box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.1);
57 | }
58 |
59 | .collapseIconContainer:hover {
60 | box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.3);
61 | }
62 |
63 | .collapseIcon {
64 | width: 16px;
65 | height: 16px;
66 | }
67 |
68 | .collapsed {
69 | transform: rotate(0deg);
70 | }
71 |
72 | /**
73 | * node styles
74 | */
75 | .employeeInfoContainer {
76 | display: flex;
77 | flex-direction: column;
78 | justify-content: center;
79 |
80 | padding: 10px 40px 10px 16px;
81 | border: 1px solid rgba(0, 0, 0, 0.1);
82 | border-radius: 10px;
83 | background-color: rgba(0, 0, 0, 0);
84 |
85 | cursor: move;
86 | transition: all 0.3s;
87 | }
88 |
89 | .employeeInfoContainer:hover {
90 | /* background-color: aqua; */
91 | /* background-color: rgba(0, 0, 0, 0.06); */
92 | background-color: aquamarine;
93 | }
94 |
95 | .employeeName {
96 | margin-bottom: 2px;
97 |
98 | font-size: 18px;
99 | letter-spacing: 0.5px;
100 | }
101 |
102 | .employeePosition {
103 | font-size: 12px;
104 | font-weight: 500;
105 | letter-spacing: 0.5px;
106 | color: #444;
107 | }
108 |
109 | .dropContainer {
110 | display: flex;
111 | border-radius: 10px;
112 | background-color: rgba(0, 0, 0, 0);
113 | transition: all 0.3s;
114 | }
115 |
116 | .dropBackground {
117 | background-color: aquamarine;
118 | }
--------------------------------------------------------------------------------
/src/Components/EmployeeNode/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import propTypes from 'prop-types'
3 |
4 | import DragContainer from './DragContainer'
5 | import DropContainer from './DropContainer'
6 | import arrowIcon from './assets/arrow.svg'
7 | import './styles.css'
8 |
9 | class EmployeeNode extends Component {
10 | constructor(props) {
11 | super(props)
12 | this.state = {
13 | collapsed: false,
14 | }
15 | }
16 |
17 | renderCollapseIcon() {
18 | const { collapsed } = this.state
19 | const { subordinates } = this.props
20 |
21 | // check if the employee have any subordinates
22 | if (subordinates.length === 0) return null
23 |
24 | return (
25 | { this.setState({ collapsed: !collapsed }) }}
28 | >
29 |

30 |
31 | )
32 | }
33 |
34 | render() {
35 | const {
36 | name,
37 | position,
38 | supervisor,
39 | moveEmployee,
40 | subordinates,
41 | } = this.props
42 |
43 | return (
44 |
45 |
46 | { this.renderCollapseIcon() }
47 |
48 |
{
56 | moveEmployee (
57 | draggedEmployeeId,
58 | supervisorId,
59 | droppedEmployeeId
60 | )
61 | }}
62 | >
63 |
67 | { name }
68 | { position }
69 |
70 |
71 |
72 |
73 |
74 | { !this.state.collapsed && subordinates }
75 |
76 |
77 | )
78 | }
79 | }
80 |
81 | EmployeeNode.propTypes = {
82 | name: propTypes.string,
83 | position: propTypes.string,
84 | supervisor: propTypes.string,
85 | subordinates: propTypes.any,
86 | moveEmployee: propTypes.func,
87 | connectDragSource: propTypes.func
88 | }
89 |
90 | export default EmployeeNode
--------------------------------------------------------------------------------
/src/Views/Home/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import Dropzone from 'react-dropzone'
3 | import { connect } from 'react-redux'
4 | import propTypes from 'prop-types'
5 |
6 | import sampleJsonData from '../../sample_json.json'
7 | import normalizeDataStructure from '../../utilities/normalizeDataStructure'
8 | import { updateEmployeeData } from '../../globals/actions'
9 | import './styles.css'
10 |
11 | class HomeView extends Component {
12 | constructor(props) {
13 | super(props)
14 | this.state = {
15 | error: ''
16 | }
17 | }
18 |
19 | hanldleFileInput(rawData) {
20 | const { updateEmployeeData } = this.props
21 |
22 | // using FileReader to read the raw data
23 | const fileReader = new FileReader()
24 |
25 | fileReader.onload = (event) => {
26 | try {
27 | // parse the json data
28 | const employeeData = JSON.parse(event.target.result)
29 |
30 | // normalize the data for better performance and data validation
31 | const normalizedData = normalizeDataStructure(employeeData)
32 |
33 | // update redux state with the employee data
34 | updateEmployeeData(normalizedData)
35 |
36 | // push to organigram screen
37 | this.props.router.push('organigram_view')
38 | } catch(error) {
39 | this.setState({ error: error.message })
40 | }
41 | }
42 |
43 | fileReader.readAsText(rawData)
44 | }
45 |
46 | handleFileDrop(files) {
47 | // first item in the files array is the file
48 | // since we defined multiple to false
49 | if (files[0]) this.hanldleFileInput(files[0])
50 | }
51 |
52 | parseDemoData() {
53 | const { updateEmployeeData } = this.props
54 |
55 | // normalize the data for better performance and data validation
56 | const normalizedData = normalizeDataStructure(sampleJsonData)
57 |
58 | // update redux state with the employee data
59 | updateEmployeeData(normalizedData)
60 |
61 | // push to organigram screen
62 | this.props.router.push('organigram_view')
63 | }
64 |
65 | render() {
66 | const { error } = this.state
67 |
68 | return (
69 |
70 |
79 |
80 | Hi Personia, drop a JSON file here
81 | To view the organigram
82 |
83 |
84 |
85 | organigram
86 |
87 |
88 | {
95 | event.preventDefault()
96 | this.hanldleFileInput(event.target.files[0])
97 | }}
98 | />
99 |
105 |
111 |
112 |
113 |
114 | { error }
115 |
116 |
117 | )
118 | }
119 | }
120 |
121 | HomeView.propTypes = {
122 | updateEmployeeData: propTypes.func,
123 | router: propTypes.object,
124 | }
125 |
126 | export default connect(
127 | null,
128 | (dispatch) => ({
129 | updateEmployeeData: (data) => {
130 | dispatch(updateEmployeeData(data))
131 | }
132 | })
133 | )(HomeView)
134 |
--------------------------------------------------------------------------------
/src/Views/Organigram/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import HTML5Backend from 'react-dnd-html5-backend'
3 | import { DragDropContext } from 'react-dnd'
4 | import { connect } from 'react-redux'
5 | import propTypes from 'prop-types'
6 |
7 | import EmployeeNode from '../../Components/EmployeeNode'
8 | import isSupervisor from '../../utilities/isSupervisor'
9 | import moveEmployee from '../../utilities/moveEmployee'
10 | import exportJson from '../../utilities/exportJson'
11 | import { updateEmployeeData } from '../../globals/actions'
12 | import './styles.css'
13 |
14 | const DEFAULT_MESSAGE = 'You can drag and drop employees to re-arrange the structure'
15 |
16 | class OrganigramView extends Component {
17 | constructor(props) {
18 | super(props)
19 | this.state = {
20 | message: DEFAULT_MESSAGE,
21 | }
22 | }
23 |
24 | handleBackPress() {
25 | const { router } = this.props
26 | router.goBack()
27 | }
28 |
29 | handleMoveEmployee (
30 | draggedEmployeeId,
31 | dragSupervisorId,
32 | droppedEmployeeId
33 | ) {
34 | const {
35 | employeeData,
36 | updateEmployeeData
37 | } = this.props
38 |
39 | // check if the dragged employee is supervisor
40 | // or great-supervisor 😁 of the dropeed employee
41 | const supervisor = isSupervisor (
42 | draggedEmployeeId,
43 | droppedEmployeeId,
44 | employeeData
45 | )
46 |
47 | if (supervisor) {
48 | this.setState({
49 | message: `Sorry, ${draggedEmployeeId} cannot be on ${droppedEmployeeId}'s team.`
50 | })
51 | } else {
52 | // perfom move operation
53 | const newEmployeeData = moveEmployee (
54 | draggedEmployeeId,
55 | dragSupervisorId,
56 | droppedEmployeeId,
57 | employeeData
58 | )
59 |
60 | // save new data to store
61 | updateEmployeeData(newEmployeeData)
62 |
63 | // give user some insight
64 | this.setState({
65 | message: `${draggedEmployeeId} successfully joined ${droppedEmployeeId}'s team`
66 | })
67 | }
68 | }
69 |
70 | renderTree (
71 | currentEmployeeId,
72 | employeeData,
73 | supervisorId
74 | ) {
75 | const {
76 | name,
77 | position,
78 | } = employeeData[currentEmployeeId]
79 |
80 | // get all subordinates for current employee to map over them
81 | const subordinates = Object.keys(
82 | employeeData[currentEmployeeId].employees
83 | )
84 |
85 | return (
86 | {
93 | return this.renderTree (
94 | eachEmployeeId,
95 | employeeData,
96 | currentEmployeeId
97 | )
98 | })
99 | }
100 | moveEmployee={(
101 | draggedEmployeeId,
102 | draggedEmployeeSupervisorId,
103 | droppedEmployeeId
104 | ) => {
105 | this.handleMoveEmployee (
106 | draggedEmployeeId,
107 | draggedEmployeeSupervisorId,
108 | droppedEmployeeId
109 | )
110 | }}
111 | />
112 | )
113 | }
114 |
115 | render() {
116 | const { message } = this.state
117 | const { employeeData } = this.props
118 | const firstEmployeeId = Object.keys(employeeData)[0]
119 |
120 | return (
121 |
122 |
123 |
organigram
124 |
125 |
{ this.handleBackPress() }}
128 | >
129 | Home
130 |
131 |
{ exportJson(employeeData) }}
134 | >
135 | Export
136 |
137 |
138 |
139 |
140 |
141 |
142 | { message }
143 |
144 |
145 |
146 | { !firstEmployeeId && (
147 |
148 | No JSON data available, please go back to Home screen and upload a valid json file.
149 |
150 | ) }
151 |
152 | { firstEmployeeId && this.renderTree (
153 | firstEmployeeId,
154 | employeeData
155 | ) }
156 |
157 |
158 |
159 | )
160 | }
161 | }
162 |
163 | OrganigramView.propTypes = {
164 | employeeData: propTypes.object,
165 | updateEmployeeData: propTypes.func,
166 | router: propTypes.object,
167 | }
168 |
169 | // enable drag and drop for the current view
170 | const enableDND = DragDropContext(HTML5Backend)(OrganigramView)
171 |
172 | export default connect (
173 | (state) => ({
174 | employeeData: state.global.employeeData
175 | }),
176 | (dispatch) => ({
177 | updateEmployeeData: (data) => {
178 | dispatch(updateEmployeeData(data))
179 | }
180 | })
181 | )(enableDND)
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | 
3 |
4 | # Organigram
5 | A JSON based tree structure with drag and drop functionally to re-arrange the tree. Show-cases some useful tree operations for deeply nested JSON data and webpack configuration for reducing bundle sizes.
6 |
7 | 
8 | 
9 |
10 | ## Live
11 | You can try the app at [http://organigram.surge.sh](http://organigram.surge.sh). A sample JSON data for testing the functionality can be found [here](./src/sample_json.json).
12 |
13 | ## Technology Stack
14 | * [react](https://reactjs.org/)
15 | * [react-router](https://github.com/ReactTraining/react-router)
16 | * [react-dnd](https://github.com/react-dnd/react-dnd)
17 | * [redux](https://redux.js.org/)
18 | * [jest](https://jestjs.io/)
19 | * [webpack](https://webpack.js.org/)
20 | * [babel](https://babeljs.io/)
21 | * [eslint](https://eslint.org/)
22 |
23 | ## Running on local machine
24 | There are couple of ways to run the project. One is to run the production code server, and another one is to run the webpack-dev-server for developing. Either way, we need to install the libraries first. 🤓
25 |
26 | ### Building the project
27 | To install the libraries, please run the following command:
28 | ```
29 | npm install
30 | ```
31 |
32 | Now, we need to build our project using the following command:
33 | ```
34 | npm run build
35 | ```
36 |
37 | There will be some useful information from `webpack` library like bundle and vendor sizes.
38 |
39 | ### Running local server
40 | To run the project with distribution code, we need to perform the following command:
41 | ```
42 | npm run serve
43 | ```
44 | This command will run the server on localhost with a random port number using the library `http-server`. The host and port number can be found in the generated output of this command.
45 |
46 | ### Running development server
47 | To run the development server with live-reload support, we need perform the following command:
48 | ```
49 | npm run serve:dev
50 | ```
51 |
52 | ### Running the tests
53 | There is a set of unit test provided with the application. To run the tests, please perform the following command:
54 | ```
55 | npm run test
56 | ```
57 |
58 | That's it! You're now running Organigram! 🍻 👍 👏🏿 🤞🏾 🤙🏼 🎉
59 |
60 | ## Code Strategies
61 | There are some useful techniques used in the application to increase the performance and fast loading for user.
62 |
63 | ### Code splitting
64 | The `react-router` configuration utilizes code splitting mechanism for only loading necessery JavaScript code for certain views.
65 | ```js
66 | {
67 | path: 'organigram_view',
68 | getComponent(location, cb) {
69 | // async call for loading the view
70 | System.import('./Views/Organigram').then(
71 | module => cb(null, module.default)
72 | )
73 | }
74 | },
75 | ```
76 | As you might notice the `System.import` call for the required component, which notifies webpack to split the code for this view and make an async call when this view loads in the browser. Webpack genrates different files for related code and only loads required JavaScript file when the view/route is displayed.
77 |
78 | ### Data Normalization
79 | As the JSON tree is structured with deeply nested employee objects, its costly to perform any move/drag-drop operations in a tree. It generally requires O(n) or O(n-square) complexity. I have performed a normalization operation when the JSON is uploaded and then saved it to redux-store. As an example, let's consider the following JSON structure:
80 | ```json
81 | {
82 | "Professor Albus Dumbledore": {
83 | "position": "CEO",
84 | "employees": [
85 | {
86 | "Professor McGonagall": {
87 | "position": "VP Engineering",
88 | "employees": [
89 | {
90 | "Harry Potter": {
91 | "position": "Frontend Engineer",
92 | "employees": []
93 | }
94 | },
95 | {
96 | "Ginny Weasley": {
97 | "position": "Backend Engineer",
98 | "employees": []
99 | }
100 | }
101 | ]
102 | }
103 | }
104 | ]
105 | }
106 | }
107 | ```
108 | The normalization operation will convert the structure into the following structure:
109 | ```json
110 | {
111 | "Professor Albus Dumbledore": {
112 | "name": "Professor Albus Dumbledore",
113 | "position": "CEO",
114 | "employees": {
115 | "Professor McGonagall": true
116 | }
117 | },
118 | "Professor McGonagall": {
119 | "name": "Professor McGonagall",
120 | "position": "VP Engineering",
121 | "employees": {
122 | "Harry Potter": true,
123 | "Ginny Weasley": true,
124 | }
125 | },
126 | "Harry Potter": {
127 | "name": "Harry Potter",
128 | "position": "Frontend Engineer",
129 | "employees": {}
130 | },
131 | "Ginny Weasley": {
132 | "name": "Ginny Weasley",
133 | "position": "Backend Engineer",
134 | "employees": {}
135 | }
136 | }
137 | ```
138 |
139 | This makes move/drag-drop operations more performant with O(1) complexity.
140 | ```javascript
141 | // remove the employee from its current supervisor
142 | delete newEmployeeList[sourceSupervisorId].employees[sourceId]
143 |
144 | // add the employee to its new supervisor
145 | newEmployeeList[destinationId].employees[sourceId] = true
146 | ```
147 |
148 | If user wants to export the JSON structure for futher usage or we need to perform an API call at some point, I've also added the logic for de-normalizing the structure.
149 |
150 | ## LICENSE
151 | MIT. Anything you would like to do.
152 |
--------------------------------------------------------------------------------
/src/globals/styles/normalize.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css */
2 |
3 | /* Document
4 | ========================================================================== */
5 |
6 | /**
7 | * 1. Correct the line height in all browsers.
8 | * 2. Prevent adjustments of font size after orientation changes in iOS.
9 | */
10 |
11 | html {
12 | line-height: 1.15; /* 1 */
13 | -webkit-text-size-adjust: 100%; /* 2 */
14 | }
15 |
16 | /* Sections
17 | ========================================================================== */
18 |
19 | /**
20 | * Remove the margin in all browsers.
21 | */
22 |
23 | body {
24 | margin: 0;
25 | }
26 |
27 | /**
28 | * Correct the font size and margin on `h1` elements within `section` and
29 | * `article` contexts in Chrome, Firefox, and Safari.
30 | */
31 |
32 | h1 {
33 | font-size: 2em;
34 | margin: 0.67em 0;
35 | }
36 |
37 | /* Grouping content
38 | ========================================================================== */
39 |
40 | /**
41 | * 1. Add the correct box sizing in Firefox.
42 | * 2. Show the overflow in Edge and IE.
43 | */
44 |
45 | hr {
46 | box-sizing: content-box; /* 1 */
47 | height: 0; /* 1 */
48 | overflow: visible; /* 2 */
49 | }
50 |
51 | /**
52 | * 1. Correct the inheritance and scaling of font size in all browsers.
53 | * 2. Correct the odd `em` font sizing in all browsers.
54 | */
55 |
56 | pre {
57 | font-family: monospace, monospace; /* 1 */
58 | font-size: 1em; /* 2 */
59 | }
60 |
61 | /* Text-level semantics
62 | ========================================================================== */
63 |
64 | /**
65 | * Remove the gray background on active links in IE 10.
66 | */
67 |
68 | a {
69 | background-color: transparent;
70 | }
71 |
72 | /**
73 | * 1. Remove the bottom border in Chrome 57-
74 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
75 | */
76 |
77 | abbr[title] {
78 | border-bottom: none; /* 1 */
79 | text-decoration: underline; /* 2 */
80 | text-decoration: underline dotted; /* 2 */
81 | }
82 |
83 | /**
84 | * Add the correct font weight in Chrome, Edge, and Safari.
85 | */
86 |
87 | b,
88 | strong {
89 | font-weight: bolder;
90 | }
91 |
92 | /**
93 | * 1. Correct the inheritance and scaling of font size in all browsers.
94 | * 2. Correct the odd `em` font sizing in all browsers.
95 | */
96 |
97 | code,
98 | kbd,
99 | samp {
100 | font-family: monospace, monospace; /* 1 */
101 | font-size: 1em; /* 2 */
102 | }
103 |
104 | /**
105 | * Add the correct font size in all browsers.
106 | */
107 |
108 | small {
109 | font-size: 80%;
110 | }
111 |
112 | /**
113 | * Prevent `sub` and `sup` elements from affecting the line height in
114 | * all browsers.
115 | */
116 |
117 | sub,
118 | sup {
119 | font-size: 75%;
120 | line-height: 0;
121 | position: relative;
122 | vertical-align: baseline;
123 | }
124 |
125 | sub {
126 | bottom: -0.25em;
127 | }
128 |
129 | sup {
130 | top: -0.5em;
131 | }
132 |
133 | /* Embedded content
134 | ========================================================================== */
135 |
136 | /**
137 | * Remove the border on images inside links in IE 10.
138 | */
139 |
140 | img {
141 | border-style: none;
142 | }
143 |
144 | /* Forms
145 | ========================================================================== */
146 |
147 | /**
148 | * 1. Change the font styles in all browsers.
149 | * 2. Remove the margin in Firefox and Safari.
150 | */
151 |
152 | button,
153 | input,
154 | optgroup,
155 | select,
156 | textarea {
157 | font-family: inherit; /* 1 */
158 | font-size: 100%; /* 1 */
159 | line-height: 1.15; /* 1 */
160 | margin: 0; /* 2 */
161 | }
162 |
163 | /**
164 | * Show the overflow in IE.
165 | * 1. Show the overflow in Edge.
166 | */
167 |
168 | button,
169 | input { /* 1 */
170 | overflow: visible;
171 | }
172 |
173 | /**
174 | * Remove the inheritance of text transform in Edge, Firefox, and IE.
175 | * 1. Remove the inheritance of text transform in Firefox.
176 | */
177 |
178 | button,
179 | select { /* 1 */
180 | text-transform: none;
181 | }
182 |
183 | /**
184 | * Correct the inability to style clickable types in iOS and Safari.
185 | */
186 |
187 | button,
188 | [type="button"],
189 | [type="reset"],
190 | [type="submit"] {
191 | -webkit-appearance: button;
192 | }
193 |
194 | /**
195 | * Remove the inner border and padding in Firefox.
196 | */
197 |
198 | button::-moz-focus-inner,
199 | [type="button"]::-moz-focus-inner,
200 | [type="reset"]::-moz-focus-inner,
201 | [type="submit"]::-moz-focus-inner {
202 | border-style: none;
203 | padding: 0;
204 | }
205 |
206 | /**
207 | * Restore the focus styles unset by the previous rule.
208 | */
209 |
210 | button:-moz-focusring,
211 | [type="button"]:-moz-focusring,
212 | [type="reset"]:-moz-focusring,
213 | [type="submit"]:-moz-focusring {
214 | outline: 1px dotted ButtonText;
215 | }
216 |
217 | /**
218 | * Correct the padding in Firefox.
219 | */
220 |
221 | fieldset {
222 | padding: 0.35em 0.75em 0.625em;
223 | }
224 |
225 | /**
226 | * 1. Correct the text wrapping in Edge and IE.
227 | * 2. Correct the color inheritance from `fieldset` elements in IE.
228 | * 3. Remove the padding so developers are not caught out when they zero out
229 | * `fieldset` elements in all browsers.
230 | */
231 |
232 | legend {
233 | box-sizing: border-box; /* 1 */
234 | color: inherit; /* 2 */
235 | display: table; /* 1 */
236 | max-width: 100%; /* 1 */
237 | padding: 0; /* 3 */
238 | white-space: normal; /* 1 */
239 | }
240 |
241 | /**
242 | * Add the correct vertical alignment in Chrome, Firefox, and Opera.
243 | */
244 |
245 | progress {
246 | vertical-align: baseline;
247 | }
248 |
249 | /**
250 | * Remove the default vertical scrollbar in IE 10+.
251 | */
252 |
253 | textarea {
254 | overflow: auto;
255 | }
256 |
257 | /**
258 | * 1. Add the correct box sizing in IE 10.
259 | * 2. Remove the padding in IE 10.
260 | */
261 |
262 | [type="checkbox"],
263 | [type="radio"] {
264 | box-sizing: border-box; /* 1 */
265 | padding: 0; /* 2 */
266 | }
267 |
268 | /**
269 | * Correct the cursor style of increment and decrement buttons in Chrome.
270 | */
271 |
272 | [type="number"]::-webkit-inner-spin-button,
273 | [type="number"]::-webkit-outer-spin-button {
274 | height: auto;
275 | }
276 |
277 | /**
278 | * 1. Correct the odd appearance in Chrome and Safari.
279 | * 2. Correct the outline style in Safari.
280 | */
281 |
282 | [type="search"] {
283 | -webkit-appearance: textfield; /* 1 */
284 | outline-offset: -2px; /* 2 */
285 | }
286 |
287 | /**
288 | * Remove the inner padding in Chrome and Safari on macOS.
289 | */
290 |
291 | [type="search"]::-webkit-search-decoration {
292 | -webkit-appearance: none;
293 | }
294 |
295 | /**
296 | * 1. Correct the inability to style clickable types in iOS and Safari.
297 | * 2. Change font properties to `inherit` in Safari.
298 | */
299 |
300 | ::-webkit-file-upload-button {
301 | -webkit-appearance: button; /* 1 */
302 | font: inherit; /* 2 */
303 | }
304 |
305 | /* Interactive
306 | ========================================================================== */
307 |
308 | /*
309 | * Add the correct display in Edge, IE 10+, and Firefox.
310 | */
311 |
312 | details {
313 | display: block;
314 | }
315 |
316 | /*
317 | * Add the correct display in all browsers.
318 | */
319 |
320 | summary {
321 | display: list-item;
322 | }
323 |
324 | /* Misc
325 | ========================================================================== */
326 |
327 | /**
328 | * Add the correct display in IE 10+.
329 | */
330 |
331 | template {
332 | display: none;
333 | }
334 |
335 | /**
336 | * Add the correct display in IE 10.
337 | */
338 |
339 | [hidden] {
340 | display: none;
341 | }
--------------------------------------------------------------------------------