Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Organizational Chart #26893

Merged
merged 49 commits into from
Aug 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
506ecb4
feat: Organizational Chart
ruchamahabal Jun 20, 2021
8a33a03
feat: org chart mobile interactions
ruchamahabal Jun 21, 2021
5046cb0
feat(mobile): sibling node group expansion and rendering
ruchamahabal Jun 29, 2021
25c5cff
fix: expanded node group interactions and visibility
ruchamahabal Jun 29, 2021
fb9b628
feat: connectors for mobile node cards
ruchamahabal Jun 29, 2021
e9c6ea0
fix: don't refresh connections for same node
ruchamahabal Jun 29, 2021
281241d
chore: create separate files for Desktop and Mobile view and bundle a…
ruchamahabal Jun 29, 2021
249621a
refactor: add options to chart
ruchamahabal Jun 29, 2021
aacb649
feat: setup node edit action
ruchamahabal Jun 29, 2021
e179cd9
fix: revert changes in employee descendants query
ruchamahabal Jun 29, 2021
4e7cda6
refactor: use arcs instead of bezier curves for cleaner connectors
ruchamahabal Jun 29, 2021
6d06d8c
feat: add arc to connectors in mobile view
ruchamahabal Jun 29, 2021
d363f9d
fix: edit node button overflowing
ruchamahabal Jun 29, 2021
781d1bf
fix: sider
ruchamahabal Jul 2, 2021
6a4cce2
fix: removing orphaned connectors
ruchamahabal Jul 6, 2021
97d2bab
fix: unnecessary variables
ruchamahabal Jul 7, 2021
31a0f36
feat: handle multiple root / orphan nodes
ruchamahabal Jul 7, 2021
a48b23e
perf: Optimise Rendering
ruchamahabal Jul 8, 2021
b671518
fix: do not sort by number of connections
ruchamahabal Jul 8, 2021
1a3c335
feat: use icon for connections on mobile view
ruchamahabal Jul 8, 2021
c79316a
fix: exclude active node while fetching sibling group
ruchamahabal Jul 8, 2021
9270de5
fix: sibling group expansion not working for root nodes
ruchamahabal Jul 8, 2021
9e7302a
fix(mobile): collapsed nodes not expanding
ruchamahabal Jul 8, 2021
e1cf771
fix: sider
ruchamahabal Jul 8, 2021
3cc85e1
test: UI tests for org chart desktop
ruchamahabal Jul 14, 2021
a1d379d
test: UI tests for org chart mobile
ruchamahabal Jul 15, 2021
525d4d4
fix: sider
ruchamahabal Jul 15, 2021
9dfddca
ci(cypress): use env variable for key
ruchamahabal Jul 16, 2021
e126b37
fix(tests): clear filter before typing
ruchamahabal Jul 19, 2021
e5406ec
fix(tests): apply filters correctly
ruchamahabal Jul 19, 2021
2c7b500
fix: tests
ruchamahabal Jul 19, 2021
982a097
fix: tests
ruchamahabal Jul 19, 2021
76d192f
fix: tests
ruchamahabal Jul 19, 2021
d32da55
fix(test): increase timeout for record creation
ruchamahabal Jul 20, 2021
9c63fcb
fix: sider
ruchamahabal Jul 20, 2021
951b3a4
fix: sider
ruchamahabal Jul 20, 2021
6f799d1
feat: Expand All nodes option in Desktop view
ruchamahabal Jul 21, 2021
58c31c7
feat: add html2canvas for easily exporting html to images using canvas
ruchamahabal Jul 25, 2021
78f50a9
feat: Export chart option in desktop view
ruchamahabal Jul 25, 2021
5f0edca
fix(style): longer titles overflowing
ruchamahabal Jul 25, 2021
c1bb4ee
fix: remove unnecessary imports
ruchamahabal Jul 25, 2021
f9afade
fix: test
ruchamahabal Jul 25, 2021
52cd007
fix: make bundled assets for hierarchy chart
ruchamahabal Aug 10, 2021
15cb248
fix(style): apply svg container margin only in desktop view
ruchamahabal Aug 10, 2021
1514916
fix: Nest `.level` class style under `.hierarchy` class (#26905)
surajshetty3416 Aug 12, 2021
4981746
Merge branch 'develop' into org-chart-develop
ruchamahabal Aug 15, 2021
4c612b7
fix: add z-index to filter to avoid svg wrapper overlapping
ruchamahabal Aug 15, 2021
0b49599
fix: expand all nodes not working when there are only 2 levels
ruchamahabal Aug 16, 2021
33032ce
fix: test
ruchamahabal Aug 16, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@
"before": true,
"beforeEach": true,
"onScan": true,
"html2canvas": true,
"extend_cscript": true,
"localforage": true,
}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ui-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ jobs:
- name: UI Tests
run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests erpnext --headless
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
CYPRESS_RECORD_KEY: 60a8e3bf-08f5-45b1-9269-2b207d7d30cd

- name: Show bench console if tests failed
if: ${{ failure() }}
Expand Down
114 changes: 114 additions & 0 deletions cypress/integration/test_organizational_chart_desktop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
context('Organizational Chart', () => {
before(() => {
cy.login();
cy.visit('/app/website');
cy.awesomebar('Organizational Chart');
cy.wait(500);
cy.url().should('include', '/organizational-chart');

cy.window().its('frappe.csrf_token').then(csrf_token => {
return cy.request({
url: `/api/method/erpnext.tests.ui_test_helpers.create_employee_records`,
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-Frappe-CSRF-Token': csrf_token
},
timeout: 60000
}).then(res => {
expect(res.status).eq(200);
cy.get('.frappe-control[data-fieldname=company] input').focus().as('input');

cy.get('@input')
.clear({ force: true })
.type('Test Org Chart{enter}', { force: true })
.blur({ force: true });
});
});
});

it('renders root nodes and loads children for the first expandable node', () => {
// check rendered root nodes and the node name, title, connections
cy.get('.hierarchy').find('.root-level ul.node-children').children()
.should('have.length', 2)
.first()
.as('first-child');

cy.get('@first-child').get('.node-name').contains('Test Employee 1');
cy.get('@first-child').get('.node-info').find('.node-title').contains('CEO');
cy.get('@first-child').get('.node-info').find('.node-connections').contains('· 2 Connections');

cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
// children of 1st root visible
cy.get(`div[data-parent="${employee_records.message[0]}"]`).as('child-node');
cy.get('@child-node')
.should('have.length', 1)
.should('be.visible');
cy.get('@child-node').get('.node-name').contains('Test Employee 3');

// connectors between first root node and immediate child
cy.get(`path[data-parent="${employee_records.message[0]}"]`)
.should('be.visible')
.invoke('attr', 'data-child')
.should('equal', employee_records.message[2]);
});
});

it('hides active nodes children and connectors on expanding sibling node', () => {
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
// click sibling
cy.get(`#${employee_records.message[1]}`)
.click()
.should('have.class', 'active');

// child nodes and connectors hidden
cy.get(`[data-parent="${employee_records.message[0]}"]`).should('not.be.visible');
cy.get(`path[data-parent="${employee_records.message[0]}"]`).should('not.be.visible');
});
});

it('collapses previous level nodes and refreshes connectors on expanding child node', () => {
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
// click child node
cy.get(`#${employee_records.message[3]}`)
.click()
.should('have.class', 'active');

// previous level nodes: parent should be on active-path; other nodes should be collapsed
cy.get(`#${employee_records.message[0]}`).should('have.class', 'collapsed');
cy.get(`#${employee_records.message[1]}`).should('have.class', 'active-path');

// previous level connectors refreshed
cy.get(`path[data-parent="${employee_records.message[1]}"]`)
.should('have.class', 'collapsed-connector');

// child node's children and connectors rendered
cy.get(`[data-parent="${employee_records.message[3]}"]`).should('be.visible');
cy.get(`path[data-parent="${employee_records.message[3]}"]`).should('be.visible');
});
});

it('expands previous level nodes', () => {
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
cy.get(`#${employee_records.message[0]}`)
.click()
.should('have.class', 'active');

cy.get(`[data-parent="${employee_records.message[0]}"]`)
.should('be.visible');

cy.get('ul.hierarchy').children().should('have.length', 2);
cy.get(`#connectors`).children().should('have.length', 1);
});
});

it('edit node navigates to employee master', () => {
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
cy.get(`#${employee_records.message[0]}`).find('.btn-edit-node')
.click();

cy.url().should('include', `/employee/${employee_records.message[0]}`);
});
});
});
190 changes: 190 additions & 0 deletions cypress/integration/test_organizational_chart_mobile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
context('Organizational Chart Mobile', () => {
before(() => {
cy.login();
cy.viewport(375, 667);
cy.visit('/app/website');
cy.awesomebar('Organizational Chart');

cy.window().its('frappe.csrf_token').then(csrf_token => {
return cy.request({
url: `/api/method/erpnext.tests.ui_test_helpers.create_employee_records`,
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-Frappe-CSRF-Token': csrf_token
},
timeout: 60000
}).then(res => {
expect(res.status).eq(200);
cy.get('.frappe-control[data-fieldname=company] input').focus().as('input');
cy.get('@input')
.clear({ force: true })
.type('Test Org Chart{enter}', { force: true })
.blur({ force: true });
});
});
});

it('renders root nodes', () => {
// check rendered root nodes and the node name, title, connections
cy.get('.hierarchy-mobile').find('.root-level').children()
.should('have.length', 2)
.first()
.as('first-child');

cy.get('@first-child').get('.node-name').contains('Test Employee 1');
cy.get('@first-child').get('.node-info').find('.node-title').contains('CEO');
cy.get('@first-child').get('.node-info').find('.node-connections').contains('· 2');
});

it('expands root node', () => {
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
cy.get(`#${employee_records.message[1]}`)
.click()
.should('have.class', 'active');

// other root node removed
cy.get(`#${employee_records.message[0]}`).should('not.exist');

// children of active root node
cy.get('.hierarchy-mobile').find('.level').first().find('ul.node-children').children()
.should('have.length', 2);

cy.get(`div[data-parent="${employee_records.message[1]}"]`).first().as('child-node');
cy.get('@child-node').should('be.visible');

cy.get('@child-node')
.get('.node-name')
.contains('Test Employee 4');

// connectors between root node and immediate children
cy.get(`path[data-parent="${employee_records.message[1]}"]`).as('connectors');
cy.get('@connectors')
.should('have.length', 2)
.should('be.visible');

cy.get('@connectors')
.first()
.invoke('attr', 'data-child')
.should('eq', employee_records.message[3]);
});
});

it('expands child node', () => {
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
cy.get(`#${employee_records.message[3]}`)
.click()
.should('have.class', 'active')
.as('expanded_node');

// 2 levels on screen; 1 on active path; 1 collapsed
cy.get('.hierarchy-mobile').children().should('have.length', 2);
cy.get(`#${employee_records.message[1]}`).should('have.class', 'active-path');

// children of expanded node visible
cy.get('@expanded_node')
.next()
.should('have.class', 'node-children')
.as('node-children');

cy.get('@node-children').children().should('have.length', 1);
cy.get('@node-children')
.first()
.get('.node-card')
.should('have.class', 'active-child')
.contains('Test Employee 7');

// orphan connectors removed
cy.get(`#connectors`).children().should('have.length', 2);
});
});

it('renders sibling group', () => {
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
// sibling group visible for parent
cy.get(`#${employee_records.message[1]}`)
.next()
.as('sibling_group');

cy.get('@sibling_group')
.should('have.attr', 'data-parent', 'undefined')
.should('have.class', 'node-group')
.and('have.class', 'collapsed');

cy.get('@sibling_group').get('.avatar-group').children().as('siblings');
cy.get('@siblings').should('have.length', 1);
cy.get('@siblings')
.first()
.should('have.attr', 'title', 'Test Employee 1');

});
});

it('expands previous level nodes', () => {
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
cy.get(`#${employee_records.message[6]}`)
.click()
.should('have.class', 'active');

// clicking on previous level node should remove all the nodes ahead
// and expand that node
cy.get(`#${employee_records.message[3]}`).click();
cy.get(`#${employee_records.message[3]}`)
.should('have.class', 'active')
.should('not.have.class', 'active-path');

cy.get(`#${employee_records.message[6]}`).should('have.class', 'active-child');
cy.get('.hierarchy-mobile').children().should('have.length', 2);
cy.get(`#connectors`).children().should('have.length', 2);
});
});

it('expands sibling group', () => {
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
// sibling group visible for parent
cy.get(`#${employee_records.message[6]}`).click();

cy.get(`#${employee_records.message[3]}`)
.next()
.click();

// siblings of parent should be visible
cy.get('.hierarchy-mobile').prev().as('sibling_group');
cy.get('@sibling_group')
.should('exist')
.should('have.class', 'sibling-group')
.should('not.have.class', 'collapsed');

cy.get(`#${employee_records.message[1]}`)
.should('be.visible')
.should('have.class', 'active');

cy.get(`[data-parent="${employee_records.message[1]}"]`)
.should('be.visible')
.should('have.length', 2)
.should('have.class', 'active-child');
});
});

it('goes to the respective level after clicking on non-collapsed sibling group', () => {
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(() => {
// click on non-collapsed sibling group
cy.get('.hierarchy-mobile')
.prev()
.click();

// should take you to that level
cy.get('.hierarchy-mobile').find('li.level .node-card').should('have.length', 2);
});
});

it('edit node navigates to employee master', () => {
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
cy.get(`#${employee_records.message[0]}`).find('.btn-edit-node')
.click();

cy.url().should('include', `/employee/${employee_records.message[0]}`);
});
});
});
Empty file.
21 changes: 21 additions & 0 deletions erpnext/hr/page/organizational_chart/organizational_chart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
frappe.pages['organizational-chart'].on_page_load = function(wrapper) {
frappe.ui.make_app_page({
parent: wrapper,
title: __('Organizational Chart'),
single_column: true
});

$(wrapper).bind('show', () => {
frappe.require('hierarchy-chart.bundle.js', () => {
let organizational_chart = undefined;
let method = 'erpnext.hr.page.organizational_chart.organizational_chart.get_children';

if (frappe.is_mobile()) {
organizational_chart = new erpnext.HierarchyChartMobile('Employee', wrapper, method);
} else {
organizational_chart = new erpnext.HierarchyChart('Employee', wrapper, method);
}
organizational_chart.show();
});
});
};
26 changes: 26 additions & 0 deletions erpnext/hr/page/organizational_chart/organizational_chart.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"content": null,
"creation": "2021-05-25 10:53:10.107241",
"docstatus": 0,
"doctype": "Page",
"idx": 0,
"modified": "2021-05-25 10:53:18.201931",
"modified_by": "Administrator",
"module": "HR",
"name": "organizational-chart",
"owner": "Administrator",
"page_name": "Organizational Chart",
"roles": [
{
"role": "HR User"
},
{
"role": "HR Manager"
}
],
"script": null,
"standard": "Yes",
"style": null,
"system_page": 0,
"title": "Organizational Chart"
}
Loading