diff --git a/js/components/__tests__/members_list.test.js b/js/components/__tests__/members_list.test.js new file mode 100644 index 00000000..a386c5ce --- /dev/null +++ b/js/components/__tests__/members_list.test.js @@ -0,0 +1,54 @@ +import { shallowMount } from '@vue/test-utils' + +import MembersList from '../members_list' + +describe('MembersList', () => { + const members = [{ + name: 'Luke Skywalker', + num_env: 2, + status: 'active', + role: 'developer' + }, { + name: 'Chewie', + num_env: 3, + status: 'pending', + role: 'admin' + }] + const role_choices = [ + { display_name: 'Developer', name: 'developer' }, + { display_name: 'Admin', name: 'admin' }, + ] + const status_choices = [ + { display_name: 'Active', name: 'active' }, + { display_name: 'Pending', name: 'pending' }, + ] + + const createWrapper = () => shallowMount(MembersList, { + propsData: { + members, role_choices, status_choices + } + }) + + it('should sort by name by default', () => { + const wrapper = createWrapper() + const listedMembers = wrapper.vm.searchedList + const memberNames = listedMembers.map(member => member.name) + expect(memberNames).toEqual(['Chewie', 'Luke Skywalker']) + }) + + it('should reverse sort by name when updated with Name', () => { + const wrapper = createWrapper() + wrapper.vm.updateSort('Name') + const listedMembers = wrapper.vm.searchedList + const memberNames = listedMembers.map(member => member.name) + expect(memberNames).toEqual(['Luke Skywalker', 'Chewie']) + }) + + it('should sort by number of environments when environments selected', () => { + const wrapper = createWrapper() + wrapper.vm.updateSort('Environments') + const listedMembers = wrapper.vm.searchedList + const memberEnvs = listedMembers.map(member => member.num_env) + expect(memberEnvs).toEqual([2, 3]) + }) +}) diff --git a/js/components/__tests__/requests_list.test.js b/js/components/__tests__/requests_list.test.js new file mode 100644 index 00000000..e7d681bc --- /dev/null +++ b/js/components/__tests__/requests_list.test.js @@ -0,0 +1,59 @@ +import { shallowMount } from '@vue/test-utils' + +import RequestsList from '../requests_list' + +describe('RequestsList', () => { + + describe('isExtended', () => { + it('should disallow sorting if not extended', () => { + const wrapper = shallowMount(RequestsList, { propsData: { isExtended: false } }) + expect(wrapper.vm.sort.columnName).toEqual('') + wrapper.vm.updateSortValue('full_name') + expect(wrapper.vm.sort.columnName).toEqual('') + }) + + it('should allow sorting when in extended mode', () => { + const wrapper = shallowMount(RequestsList, { propsData: { isExtended: true } }) + expect(wrapper.vm.sort.columnName).toEqual('last_submission_timestamp') + wrapper.vm.updateSortValue('full_name') + expect(wrapper.vm.sort.columnName).toEqual('full_name') + }) + }) + + describe('sorting', () => { + const requests = [{ + name: 'X Wing', + last_edited_timestamp: 'Mon, 2 Jan 2017 12:34:56 GMT', + last_submission_timestamp: 'Mon, 2 Jan 2017 12:34:56 GMT', + full_name: 'Luke Skywalker', + annual_usage: '80000', + status: 'Approved', + dod_component: 'Rebels' + }, { + name: 'TIE Fighter', + last_edited_timestamp: 'Mon, 12 Nov 2018 12:34:56 GMT', + last_submission_timestamp: 'Mon, 12 Nov 2018 12:34:56 GMT', + full_name: 'Darth Vader', + annual_usage: '999999', + status: 'Approved', + dod_component: 'Empire' + }] + + const mountWrapper = () => shallowMount(RequestsList, { propsData: { requests, isExtended: true } }) + + it('should default to sorting by submission recency', () => { + const wrapper = mountWrapper() + const displayedRequests = wrapper.vm.filteredRequests + const requestNames = displayedRequests.map(req => req.name) + expect(requestNames).toEqual(['TIE Fighter', 'X Wing']) + }) + + it('should reverse sort by submission time when selected', () => { + const wrapper = mountWrapper() + wrapper.vm.updateSortValue('last_submission_timestamp') + const displayedRequests = wrapper.vm.filteredRequests + const requestNames = displayedRequests.map(req => req.name) + expect(requestNames).toEqual(['X Wing', 'TIE Fighter']) + }) + }) +}) diff --git a/js/components/forms/requests_list.js b/js/components/forms/requests_list.js deleted file mode 100644 index 1e3d586f..00000000 --- a/js/components/forms/requests_list.js +++ /dev/null @@ -1,148 +0,0 @@ -import LocalDatetime from '../../components/local_datetime' -import { formatDollars } from '../../lib/dollars' -import { parse } from 'date-fns' -import { compose, partial, indexBy, prop, sortBy, reverse, pipe } from 'ramda' - -export default { - name: 'requests-list', - - components: { - LocalDatetime, - }, - - props: { - requests: { - type: Array, - default: [], - }, - isExtended: { - type: Boolean, - default: false, - }, - statuses: { - type: Array, - default: [], - }, - dodComponents: { - type: Array, - default: [], - } - }, - - data: function () { - const defaultSort = (sort, requests) => sortBy(prop(sort.columnName), requests) - const dateSort = (sort, requests) => { - const parseDate = compose(partial(parse), prop(sort.columnName)) - return sortBy(parseDate, requests) - } - - const columnList = [ - { - displayName: 'JEDI Cloud Request Name', - attr: 'name', - sortFunc: defaultSort, - }, - { - displayName: 'Date Request Submitted', - attr: 'last_submission_timestamp', - sortFunc: dateSort, - }, - { - displayName: 'Date Request Last Edited', - attr: 'last_edited_timestamp', - extendedOnly: true, - sortFunc: dateSort, - }, - { - displayName: 'Requester', - attr: 'full_name', - extendedOnly: true, - sortFunc: defaultSort, - }, - { - displayName: 'Projected Annual Usage ($)', - attr: 'annual_usage', - sortFunc: defaultSort, - }, - { - displayName: 'Request Status', - attr: 'status', - sortFunc: defaultSort, - }, - { - displayName: 'DOD Component', - attr: 'dod_component', - extendedOnly: true, - sortFunc: defaultSort, - }, - ] - - return { - searchValue: '', - statusValue: '', - dodComponentValue: '', - sort: { - columnName: '', - isAscending: true - }, - columns: indexBy(prop('attr'), columnList), - } - }, - - computed: { - filteredRequests: function () { - return pipe( - partial(this.applySearch, [this.searchValue]), - partial(this.applyFilters, [this.statusValue, this.dodComponentValue]), - partial(this.applySort, [this.sort]), - )(this.requests) - } - }, - - methods: { - getColumns: function() { - return Object.values(this.columns) - .filter((column) => !column.extendedOnly || this.isExtended) - }, - applySearch: (query, requests) => { - return requests.filter( - (request) => query !== '' ? - request.name.toLowerCase().includes(query.toLowerCase()) : - true - ) - }, - applyFilters: (status, dodComponent, requests) => { - return requests.filter( - (request) => status !== '' ? - request.status === status : - true - ).filter( - (request) => dodComponent !== '' ? - request.dod_component === dodComponent : - true - ) - }, - applySort: function(sort, requests) { - if (sort.columnName === '') { - return requests - } else { - const { sortFunc } = this.columns[sort.columnName] - const sorted = sortFunc(sort, requests) - return sort.isAscending ? - sorted : - reverse(sorted) - } - }, - dollars: (value) => formatDollars(value, false), - updateSortValue: function(columnName) { - if (!this.isExtended) { return } - - // toggle ascending / descending if column is clicked twice - if (columnName === this.sort.columnName) { - this.sort.isAscending = !this.sort.isAscending - } - - this.sort.columnName = columnName; - }, - }, - } diff --git a/js/components/forms/members_list.js b/js/components/members_list.js similarity index 96% rename from js/components/forms/members_list.js rename to js/components/members_list.js index 7de99020..925895c6 100644 --- a/js/components/forms/members_list.js +++ b/js/components/members_list.js @@ -90,6 +90,8 @@ export default { }, ] + const defaultSortColumn = 'Name' + return { searchValue: '', status: '', @@ -97,7 +99,7 @@ export default { role: '', rolesByDisplayName: indexBy(prop('display_name'), this.role_choices), sortInfo: { - columnName: '', + columnName: defaultSortColumn, isAscending: true, columns: indexBy(prop('displayName'), columns) }, @@ -127,5 +129,7 @@ export default { getColumns: function() { return Object.values(this.sortInfo.columns) } - } + }, + + template: '
' } diff --git a/js/components/requests_list.js b/js/components/requests_list.js new file mode 100644 index 00000000..ec9f9282 --- /dev/null +++ b/js/components/requests_list.js @@ -0,0 +1,151 @@ +import LocalDatetime from '../components/local_datetime' +import { formatDollars } from '../lib/dollars' +import { parse } from 'date-fns' +import { compose, partial, indexBy, prop, sortBy, reverse, pipe } from 'ramda' + +export default { + name: 'requests-list', + + components: { + LocalDatetime, + }, + + props: { + requests: { + type: Array, + default: () => [], + }, + isExtended: { + type: Boolean, + default: false, + }, + statuses: { + type: Array, + default: () => [], + }, + dodComponents: { + type: Array, + default: () => [], + } + }, + + data: function () { + const defaultSort = (sort, requests) => sortBy(prop(sort.columnName), requests) + const dateSort = (sort, requests) => { + const parseDate = compose(partial(parse), prop(sort.columnName)) + return sortBy(parseDate, requests) + } + + const columnList = [ + { + displayName: 'JEDI Cloud Request Name', + attr: 'name', + sortFunc: defaultSort, + }, + { + displayName: 'Date Request Submitted', + attr: 'last_submission_timestamp', + sortFunc: dateSort, + }, + { + displayName: 'Date Request Last Edited', + attr: 'last_edited_timestamp', + extendedOnly: true, + sortFunc: dateSort, + }, + { + displayName: 'Requester', + attr: 'full_name', + extendedOnly: true, + sortFunc: defaultSort, + }, + { + displayName: 'Projected Annual Usage ($)', + attr: 'annual_usage', + sortFunc: defaultSort, + }, + { + displayName: 'Request Status', + attr: 'status', + sortFunc: defaultSort, + }, + { + displayName: 'DOD Component', + attr: 'dod_component', + extendedOnly: true, + sortFunc: defaultSort, + }, + ] + + const defaultSortColumn = this.isExtended ? 'last_submission_timestamp' : '' + return { + searchValue: '', + statusValue: '', + dodComponentValue: '', + sort: { + columnName: defaultSortColumn, + isAscending: false + }, + columns: indexBy(prop('attr'), columnList), + } + }, + + computed: { + filteredRequests: function () { + return pipe( + partial(this.applySearch, [this.searchValue]), + partial(this.applyFilters, [this.statusValue, this.dodComponentValue]), + partial(this.applySort, [this.sort]), + )(this.requests) + } + }, + + methods: { + getColumns: function() { + return Object.values(this.columns) + .filter((column) => !column.extendedOnly || this.isExtended) + }, + applySearch: (query, requests) => { + return requests.filter( + (request) => query !== '' ? + request.name.toLowerCase().includes(query.toLowerCase()) : + true + ) + }, + applyFilters: (status, dodComponent, requests) => { + return requests.filter( + (request) => status !== '' ? + request.status === status : + true + ).filter( + (request) => dodComponent !== '' ? + request.dod_component === dodComponent : + true + ) + }, + applySort: function(sort, requests) { + if (sort.columnName === '') { + return requests + } else { + const { sortFunc } = this.columns[sort.columnName] + const sorted = sortFunc(sort, requests) + return sort.isAscending ? + sorted : + reverse(sorted) + } + }, + dollars: (value) => formatDollars(value, false), + updateSortValue: function(columnName) { + if (!this.isExtended) { return } + + // toggle ascending / descending if column is clicked twice + if (columnName === this.sort.columnName) { + this.sort.isAscending = !this.sort.isAscending + } + + this.sort.columnName = columnName; + }, + }, + + template: '
' +} diff --git a/js/index.js b/js/index.js index 2d5b9ff2..c31f8273 100644 --- a/js/index.js +++ b/js/index.js @@ -20,9 +20,9 @@ import selector from './components/selector' import BudgetChart from './components/charts/budget_chart' import SpendTable from './components/tables/spend_table' import CcpoApproval from './components/forms/ccpo_approval' -import MembersList from './components/forms/members_list' +import MembersList from './components/members_list' import LocalDatetime from './components/local_datetime' -import RequestsList from './components/forms/requests_list' +import RequestsList from './components/requests_list' import ConfirmationPopover from './components/confirmation_popover' Vue.config.productionTip = false diff --git a/package.json b/package.json index 21cb6968..6dfc9d2a 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "build": "parcel build js/index.js -d static/assets --public-url /static/assets -o index.js", "test": "jest", "test:coverage": "jest --coverage", - "test:watch": "jest --watch" + "test:watch": "jest --watch --no-cache" }, "author": "", "license": "MIT",