Merge pull request #416 from dod-ccpo/request-filters
Searching, filtering and sorting for requests page
This commit is contained in:
commit
9c22a57780
@ -207,3 +207,7 @@ class Requests(object):
|
||||
comment = RequestInternalComment(request=request, text=comment_text, user=user)
|
||||
RequestsQuery.add_and_commit(comment)
|
||||
return request
|
||||
|
||||
@classmethod
|
||||
def possible_statuses(cls):
|
||||
return [s[1].value for s in RequestStatus.__members__.items()]
|
||||
|
@ -15,10 +15,12 @@ class RequestsIndex(object):
|
||||
Permissions.REVIEW_AND_APPROVE_JEDI_WORKSPACE_REQUEST
|
||||
in self.user.atat_permissions
|
||||
):
|
||||
return self._ccpo_view(self.user)
|
||||
context = self._ccpo_view(self.user)
|
||||
|
||||
else:
|
||||
return self._non_ccpo_view(self.user)
|
||||
context = self._non_ccpo_view(self.user)
|
||||
|
||||
return {**context, "possible_statuses": Requests.possible_statuses()}
|
||||
|
||||
def _ccpo_view(self, user):
|
||||
requests = Requests.get_many()
|
||||
@ -55,6 +57,14 @@ class RequestsIndex(object):
|
||||
"extended_view": False,
|
||||
}
|
||||
|
||||
def _workspace_link_for_request(self, request):
|
||||
if request.is_approved:
|
||||
return url_for(
|
||||
"workspaces.workspace_projects", workspace_id=request.workspace_id
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
def _map_request(self, request, viewing_role):
|
||||
time_created = pendulum.instance(request.time_created)
|
||||
is_new = time_created.add(days=1) > pendulum.now()
|
||||
@ -67,6 +77,7 @@ class RequestsIndex(object):
|
||||
"workspace_id": request.workspace.id if request.workspace else None,
|
||||
"name": request.displayname,
|
||||
"is_new": is_new,
|
||||
"is_approved": request.is_approved,
|
||||
"status": request.status_displayname,
|
||||
"app_count": app_count,
|
||||
"last_submission_timestamp": request.last_submission_timestamp,
|
||||
@ -76,6 +87,7 @@ class RequestsIndex(object):
|
||||
"edit_link": url_for("requests.edit", request_id=request.id),
|
||||
"action_required": request.action_required_by == viewing_role,
|
||||
"dod_component": request.latest_revision.dod_component,
|
||||
"workspace_link": self._workspace_link_for_request(request),
|
||||
}
|
||||
|
||||
|
||||
|
139
js/components/forms/requests_list.js
Normal file
139
js/components/forms/requests_list.js
Normal file
@ -0,0 +1,139 @@
|
||||
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: [],
|
||||
},
|
||||
},
|
||||
|
||||
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: '',
|
||||
sort: {
|
||||
columnName: '',
|
||||
isAscending: true
|
||||
},
|
||||
columns: indexBy(prop('attr'), columnList),
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
filteredRequests: function () {
|
||||
return pipe(
|
||||
partial(this.applySearch, [this.searchValue]),
|
||||
partial(this.applyFilters, [this.statusValue]),
|
||||
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, requests) => {
|
||||
return requests.filter(
|
||||
(request) => status !== '' ?
|
||||
request.status === status :
|
||||
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;
|
||||
},
|
||||
},
|
||||
}
|
@ -22,6 +22,7 @@ import SpendTable from './components/tables/spend_table'
|
||||
import CcpoApproval from './components/forms/ccpo_approval'
|
||||
import MembersList from './components/forms/members_list'
|
||||
import LocalDatetime from './components/local_datetime'
|
||||
import RequestsList from './components/forms/requests_list'
|
||||
|
||||
Vue.use(VTooltip)
|
||||
|
||||
@ -46,6 +47,7 @@ const app = new Vue({
|
||||
LocalDatetime,
|
||||
EditEnvironmentRole,
|
||||
EditProjectRoles,
|
||||
RequestsList,
|
||||
},
|
||||
|
||||
mounted: function() {
|
||||
|
@ -17,6 +17,7 @@
|
||||
"date-fns": "^1.29.0",
|
||||
"npm": "^6.0.1",
|
||||
"parcel": "^1.9.7",
|
||||
"ramda": "^0.25.0",
|
||||
"svg-innerhtml": "^1.1.0",
|
||||
"text-mask-addons": "^3.8.0",
|
||||
"uswds": "^1.6.3",
|
||||
|
@ -3,6 +3,7 @@
|
||||
{% from "components/alert.html" import Alert %}
|
||||
{% from "components/modal.html" import Modal %}
|
||||
{% from "components/empty_state.html" import EmptyState %}
|
||||
{% from "components/icon.html" import Icon %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@ -36,6 +37,9 @@
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
<requests-list inline-template v-bind:requests='{{ requests | tojson }}' v-bind:is-extended='{{ extended_view | tojson }}' v-bind:statuses='{{ possible_statuses | tojson }}'>
|
||||
<div>
|
||||
|
||||
{% if num_action_required %}
|
||||
{% set title -%}
|
||||
Action required on {{ num_action_required }} requests.
|
||||
@ -55,7 +59,6 @@
|
||||
) }}
|
||||
|
||||
{% else %}
|
||||
|
||||
{% if extended_view %}
|
||||
<div class="row kpi">
|
||||
<div class="kpi__item col col--grow">
|
||||
@ -76,22 +79,21 @@
|
||||
<div class="col col--grow">
|
||||
|
||||
{% if extended_view %}
|
||||
<form class='search-bar'>
|
||||
<form @submit.prevent class='search-bar'>
|
||||
<div class='usa-input search-input'>
|
||||
<label for='requests-search'>Search requests by Order ID</label>
|
||||
<input type='search' id='requests-search' name='requests-search' placeholder="Search by Order ID"/>
|
||||
<button type="submit">
|
||||
<label for='requests-search'>Search requests by name</label>
|
||||
<input v-model='searchValue' type='search' id='requests-search' name='requests-search' placeholder="Search by name"/>
|
||||
<button>
|
||||
<span class="hide">Search</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class='usa-input'>
|
||||
<label for='filter-status'>Filter requests by status</label>
|
||||
<select id="filter-status" name="filter-status">
|
||||
<select v-model="statusValue" id="filter-status" name="filter-status">
|
||||
<option value="" selected disabled>Filter by status</option>
|
||||
<option value="">Active</option>
|
||||
<option value="">Pending</option>
|
||||
<option value="">Denied</option>
|
||||
<option value="">All</option>
|
||||
<option v-for="status in statuses" :value="status">!{ status }</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
@ -101,52 +103,49 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">JEDI Cloud Request Name</th>
|
||||
<th scope="col">Date Request Submitted</th>
|
||||
{% if extended_view %}
|
||||
<th scope="col">Date Request Last Edited</th>
|
||||
<th scope="col">Requester</th>
|
||||
{% endif %}
|
||||
<th scope="col">Projected Annual Usage ($)</th>
|
||||
<th scope="col">Request Status</th>
|
||||
{% if extended_view %}
|
||||
<th scope="col">DOD Component</th>
|
||||
{% endif %}
|
||||
<th @click.prevent="updateSortValue(column.attr)" v-for="column in getColumns()"scope="col">
|
||||
!{ column.displayName }
|
||||
<span v-if="column.attr === sort.columnName && sort.isAscending">
|
||||
{{ Icon("caret_down") }}
|
||||
</span>
|
||||
<span v-else-if="column.attr === sort.columnName && !sort.isAscending">
|
||||
{{ Icon("caret_up") }}
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in requests %}
|
||||
<tbody v-for="r in filteredRequests">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<a class='icon-link icon-link--large' href="{{ r.edit_link }}">{{ r.name }}</a>
|
||||
{% if r.action_required %}<span class="label label--info">Action Required</span>{% endif %}
|
||||
<a class='icon-link icon-link--large' :href="r.edit_link">!{ r.name }</a>
|
||||
<span v-if="r.action_required" class="label label--info">Action Required</span>
|
||||
</th>
|
||||
<td>{{ r.last_submission_timestamp | formattedDate }}</td>
|
||||
{% if extended_view %}
|
||||
<td>{{ r.last_edited_timestamp | formattedDate }}</td>
|
||||
<td>{{ r.full_name }}</td>
|
||||
<td><local-datetime :timestamp="r.last_submission_timestamp" format="M/D/YYYY"></td>
|
||||
{% if extended_view %}
|
||||
<td><local-datetime :timestamp="r.last_edited_timestamp" format="M/D/YYYY"></td>
|
||||
<td>!{ r.full_name }</td>
|
||||
{% endif %}
|
||||
<td>{{ r.annual_usage | dollars }}</td>
|
||||
<td>!{ dollars(r.annual_usage) }</td>
|
||||
<td>
|
||||
{% if r.status == 'Approved' %}
|
||||
<a class="icon-link icon-link--large" href="{{ url_for('workspaces.workspace_projects', workspace_id=r.workspace_id) }}">
|
||||
{{ r.status }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ r.status }}
|
||||
{% endif %}
|
||||
<a v-if="r.is_approved" class="icon-link icon-link--large" :href="r.workspace_link">
|
||||
!{ r.status }
|
||||
</a>
|
||||
<span v-else>
|
||||
!{ r.status }
|
||||
</span>
|
||||
</td>
|
||||
{% if extended_view %}
|
||||
<td>{{ r.dod_component }}</td>
|
||||
<td>!{ r.dod_component }</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</requests-list>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
@ -6101,6 +6101,11 @@ qw@~1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/qw/-/qw-1.0.1.tgz#efbfdc740f9ad054304426acb183412cc8b996d4"
|
||||
integrity sha1-77/cdA+a0FQwRCassYNBLMi5ltQ=
|
||||
|
||||
ramda@^0.25.0:
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.25.0.tgz#8fdf68231cffa90bc2f9460390a0cb74a29b29a9"
|
||||
integrity sha512-GXpfrYVPwx3K7RQ6aYT8KPS8XViSXUVJT1ONhoKPE9VAleW42YE+U+8VEyGWt41EnEQW7gwecYJriTI0pKoecQ==
|
||||
|
||||
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80"
|
||||
|
Loading…
x
Reference in New Issue
Block a user