Skip to content

Commit

Permalink
Merge pull request #3077 from NationalSecurityAgency/t#2517/approval_…
Browse files Browse the repository at this point in the history
…feedback

T#2517/approval feedback
  • Loading branch information
sudo-may authored Jan 10, 2025
2 parents 4fd084d + 8fb6972 commit cd48cdd
Show file tree
Hide file tree
Showing 28 changed files with 282 additions and 122 deletions.
44 changes: 30 additions & 14 deletions dashboard/src/components/skills/selfReport/RejectSkillModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,29 @@ import {object, string} from "yup";
import { useAppConfig } from '@/common-components/stores/UseAppConfig.js'
import SkillsInputFormDialog from "@/components/utils/inputForm/SkillsInputFormDialog.vue";
import SelfReportService from '@/components/skills/selfReport/SelfReportService';
import { computed } from 'vue';
const emit = defineEmits(['do-reject']);
const emit = defineEmits(['do-approve', 'do-reject', 'done']);
const model = defineModel();
const appConfig = useAppConfig()
const route = useRoute();
const props = defineProps({
requestType: {
type: String,
required: true,
validator: (value) => ['Approve', 'Reject'].includes(value)
},
selectedItems: {
type: Array,
required: true,
}
})
const isApprove = computed(() => props.requestType === 'Approve')
const isReject = computed(() => props.requestType === 'Reject')
const modalTitle = computed(() => isApprove.value ? 'Approve Skills' : 'Reject Skills')
const initialData = {
approvalRequiredMsg: ''
}
Expand All @@ -42,11 +52,17 @@ const schema = object({
.max(appConfig.maxSelfReportRejectionMessageLength)
.label('Rejection Message')
})
const rejectSkills = (values) => {
const rejectOrApproveSkills = (values) => {
const ids = props.selectedItems.map((item) => item.id);
return SelfReportService.reject(route.params.projectId, ids, values.approvalRequiredMsg).then(() => {
emit('do-reject', ids);
});
if (isReject.value) {
return SelfReportService.reject(route.params.projectId, ids, values.approvalRequiredMsg).then(() => {
emit('do-reject', ids);
});
} else {
return SelfReportService.approve(route.params.projectId, ids, values.approvalRequiredMsg).then(() => {
emit('do-approve', ids);
});
}
}
const done = () => {
Expand All @@ -61,23 +77,23 @@ const done = () => {
:enable-return-focus="true"
:initial-values="initialData"
:style="{ width: '40rem !important' }"
:ok-button-icon="'fas fa-arrow-alt-circle-right'"
ok-button-label="Reject"
ok-button-icon="fas fa-arrow-alt-circle-right"
:ok-button-label="isApprove ? 'Approve' : 'Reject'"
:validation-schema="schema"
:save-data-function="rejectSkills"
:save-data-function="rejectOrApproveSkills"
@on-cancel="done"
header="Reject Skills">
<div id="rejectionTitleInModal" class="flex gap-2" data-cy="rejectionTitle">
:header="modalTitle">
<div id="rejectionApprovalTitleInModal" class="flex gap-2" :data-cy="isReject ? 'rejectionTitle' : 'approvalTitle'">
<div class="flex text-center">
<i class="far fa-thumbs-down text-warning" style="font-size: 3rem"/>
</div>
<div class="flex flex-1">
<p class="h6">This will reject user's request(s) to get points. Users will be notified and you can provide an optional message below.</p>
<p class="h6">This will {{ isReject ? 'reject' : 'approve' }} user's request(s) to get points. Users will be notified and you can provide an optional message below.</p>
</div>
</div>
<SkillsTextarea data-cy="rejectionInputMsg"
aria-describedby="rejectionTitleInModal"
aria-label="Optional Rejection Message"
<SkillsTextarea :data-cy="isReject ? 'rejectionInputMsg' : 'approvalInputMsg'"
aria-describedby="rejectionApprovalTitleInModal"
:aria-label="isReject ? 'Optional Rejection Message' : 'Optional Approval Message'"
rows="5"
name="approvalRequiredMsg">
</SkillsTextarea>
Expand Down
46 changes: 27 additions & 19 deletions dashboard/src/components/skills/selfReport/SelfReportApproval.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ const expandedRows = ref({});
const totalRows = ref(null);
const emailSubscribed = ref(true);
const isEmailEnabled = computed(() => appInfo.emailEnabled)
const showRejectModal = ref(false);
const showApproveOrRejectModal = ref(false);
const requestType = ref('Reject');
onMounted(() => {
loadApprovals();
Expand Down Expand Up @@ -88,31 +89,38 @@ const loadApprovals = () => {
});
};
const approve = () => {
const showApproveModal = () => {
requestType.value = 'Approve';
showApproveOrRejectModal.value = true;
}
const doApprove = (idsToApprove) => {
loading.value = true;
const idsToApprove = selectedItems.value.map((item) => item.id);
SelfReportService.approve(route.params.projectId, idsToApprove)
.then(() => {
loadApprovals().then(() => {
setTimeout(() => announcer.polite(`approved ${idsToApprove.length} skill approval request${idsToApprove.length > 1 ? 's' : ''}`), 0);
});
emit('approval-action', 'approved');
selectedItems.value = [];
});
loadApprovals().then(() => {
setTimeout(() => announcer.polite(`approved ${idsToApprove.length} skill approval request${idsToApprove.length > 1 ? 's' : ''}`), 0);
emit('approval-action', 'approved');
selectedItems.value = [];
});
closeModal();
};
const doReject = (rejectedIds) => {
const doReject = (idsToReject) => {
loading.value = true;
loadApprovals().then(() => {
setTimeout(() => announcer.polite(`rejected ${rejectedIds.length} skill approval request${rejectedIds.length > 1 ? 's' : ''}`), 0);
setTimeout(() => announcer.polite(`rejected ${idsToReject.length} skill approval request${idsToReject.length > 1 ? 's' : ''}`), 0);
emit('approval-action', 'rejected');
selectedItems.value = [];
});
closeModal();
};
const showRejectModal = () => {
requestType.value = 'Reject';
showApproveOrRejectModal.value = true;
}
const closeModal = () => {
showRejectModal.value = false;
showApproveOrRejectModal.value = false;
}
const checkEmailSubscriptionStatus = () => {
Expand Down Expand Up @@ -170,8 +178,8 @@ const toggleRow = (row) => {
<SkillsButton size="small" @click="loadApprovals" aria-label="Sync Records" data-cy="syncApprovalsBtn" class="" icon="fas fa-sync-alt" />
</div>
<div class="flex flex-1 justify-content-center sm:justify-content-end">
<SkillsButton size="small" @click="showRejectModal=true" data-cy="rejectBtn" class="" :disabled="selectedItems.length === 0" icon="fa fa-times-circle" label="Reject" />
<SkillsButton size="small" @click="approve" data-cy="approveBtn" class="ml-2" :disabled="selectedItems.length === 0" icon="fa fa-check" label="Approve" />
<SkillsButton size="small" @click="showRejectModal" data-cy="rejectBtn" class="" :disabled="selectedItems.length === 0" icon="fa fa-times-circle" label="Reject" />
<SkillsButton size="small" @click="showApproveModal" data-cy="approveBtn" class="ml-2" :disabled="selectedItems.length === 0" icon="fa fa-check" label="Approve" />
</div>
</div>

Expand Down Expand Up @@ -222,15 +230,15 @@ const toggleRow = (row) => {
</SkillsButton>
</template>
</Column>
<Column field="userId" sortable :class="{'flex': responsive.md.value }">
<Column field="userId" :sortable="true" :class="{'flex': responsive.md.value }">
<template #header>
<span class="mr-1"><i class="fas fa-hand-pointer" :class="colors.getTextClass(2)"/> For User</span>
</template>
<template #body="slotProps">
{{ slotProps.data.userIdForDisplay }}
</template>
</Column>
<Column field="requestedOn" sortable :class="{'flex': responsive.md.value }">
<Column field="requestedOn" :sortable="true" :class="{'flex': responsive.md.value }">
<template #header>
<span class="mr-1"><i class="fas fa-clock" :class="colors.getTextClass(3)" /> Requested On</span>
</template>
Expand Down Expand Up @@ -264,7 +272,7 @@ const toggleRow = (row) => {
</SkillsDataTable>
</template>
</Card>
<RejectSkillModal v-model="showRejectModal" @do-reject="doReject" @done="closeModal" :selected-items="selectedItems"/>
<RejectSkillModal v-model="showApproveOrRejectModal" @do-reject="doReject" @do-approve="doApprove" @done="closeModal" :selected-items="selectedItems" :request-type="requestType"/>
</template>

<style scoped>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ defineExpose( {
<div v-else><Badge variant="success"><i class="fas fa-thumbs-up"></i> <span class="text-uppercase">Approved</span></Badge></div>
<div class="text-primary">by</div>
<div class="font-italic"><span v-if="slotProps.data.approverUserIdHtml" v-html="slotProps.data.approverUserIdHtml"></span><span v-else>{{ slotProps.data.approverUserIdForDisplay }}</span></div>
<div v-if="slotProps.data.rejectionMsg"><span class="text-primary text-break">Explanation:</span> <show-more :text="slotProps.data.rejectionMsg"/></div>
<div v-if="slotProps.data.message"><span class="text-primary text-break">Explanation:</span> <show-more :text="slotProps.data.message"/></div>
</template>
</Column>
<Column field="requestedOn" sortable :class="{'flex': responsive.md.value }">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ export default {
return axios.get(url, { params })
.then((response) => response.data);
},
approve(projectId, approvalIds) {
approve(projectId, approvalIds, approvalMsg) {
const url = `/admin/projects/${encodeURIComponent(projectId)}/approvals/approve`;
return axios.post(url, { skillApprovalIds: approvalIds })
return axios.post(url, { skillApprovalIds: approvalIds, approvalMessage: approvalMsg })
.then((response) => response.data);
},
reject(projectId, approvalIds, rejectMsg) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ const toggleShowMessage = () => {
severity="info"
size="small"
@click="toggleShowMessage"
data-cy="viewTimeline"/>
<markdown-text v-if="isDescriptionShowing" :text="props.message" :instance-id="props.id" />
data-cy="toggleShowMessageBtn"/>
<markdown-text v-if="isDescriptionShowing" :text="props.message" :instance-id="props.id" data-cy="approvalEventMessage" />
</div>
</template>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,7 @@ const displayApprovalJustificationInput = () => {
}
const isPendingApproval = () => {
const res = skillInternal.value.selfReporting && skillInternal.value.selfReporting.requestedOn !== null && skillInternal.value.selfReporting.requestedOn !== undefined && !isRejected.value
return res
return skillInternal.value.selfReporting && skillInternal.value.selfReporting.requestedOn !== null && skillInternal.value.selfReporting.requestedOn !== undefined && !isRejected.value
}
const selfReportConfigured = () => {
return skillInternal.value.selfReporting && skillInternal.value.selfReporting && skillInternal.value.selfReporting.enabled
Expand Down Expand Up @@ -126,7 +125,7 @@ const reportSkill = (approvalRequestedMsg) => {
} else {
if (skillInternal.value.selfReporting) {
skillInternal.value.selfReporting.rejectedOn = null
skillInternal.value.selfReporting.rejectionMsg = null
skillInternal.value.selfReporting.message = null
}
selfReport.value.msgHidden = false
Expand Down Expand Up @@ -345,12 +344,12 @@ defineExpose({
<template #container>
<div class="flex p-3 align-content-center">
<div class="flex-1 align-content-center">
<i class="fas fa-heart-broken text-xl" aria-hidden=""></i>
<i class="fas fa-heart-broken text-xl" aria-hidden="true"></i>
Unfortunately your request from
<b>{{ timeUtils.formatDate(skillInternal.selfReporting.requestedOn, 'MM/DD/YYYY') }}</b> was rejected
<span
class="text-info">{{ timeUtils.relativeTime(skillInternal.selfReporting.rejectedOn) }}</span>.
<span v-if="skillInternal.selfReporting.rejectionMsg">The reason is: <b>"{{ skillInternal.selfReporting.rejectionMsg}}"</b></span>
<span v-if="skillInternal.selfReporting.message">The reason is: <b>"{{ skillInternal.selfReporting.message}}"</b></span>
</div>
<div class="">
<SkillsButton
Expand Down
4 changes: 2 additions & 2 deletions e2e-tests/cypress/e2e/approver/approver_role_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -456,8 +456,8 @@ describe('Approver Role Tests', () => {
const approvalHistoryTableSelector = '[data-cy="selfReportApprovalHistoryTable"]';

cy.get('[data-cy="skillsReportApprovalTable"] [data-p-index="0"] [data-pc-name="rowcheckbox"]').click()
cy.get('[data-cy="approveBtn"]')
.click();
cy.get('[data-cy="approveBtn"]').click();
cy.get('[data-cy="saveDialogBtn"]').click();
cy.validateTable(approvalHistoryTableSelector, [
[{
colIndex: 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@ describe('Client Display Self Report Skills Tests', () => {
cy.createProject(1);
cy.createSubject(1, 1);

Cypress.Commands.add('approveRequest', (requestNum = 0) => {
Cypress.Commands.add('approveRequest', (requestNum = 0, approvalMsg = '') => {
cy.request('/admin/projects/proj1/approvals?limit=10&ascending=true&page=1&orderBy=userId')
.then((response) => {
cy.request('POST', '/admin/projects/proj1/approvals/approve', {
skillApprovalIds: [response.body.data[requestNum].id],
approvalMessage: approvalMsg,
});
});
});
Expand Down Expand Up @@ -304,7 +305,7 @@ describe('Client Display Self Report Skills Tests', () => {
.should('contain.text', 'Approval Requested')

// approve and then visit page again
cy.approveRequest();
cy.approveRequest(0, 'I approve this message!');
cy.cdVisit('/');
cy.cdClickSubj(0);
cy.cdClickSkill(0);
Expand All @@ -319,6 +320,27 @@ describe('Client Display Self Report Skills Tests', () => {
.children('.p-timeline-event')
.eq(1)
.should('contain.text', 'Approval Requested')
cy.get('[data-cy="approvalHistoryTimeline"]')
.children('.p-timeline-event')
.eq(1)
.get('[data-cy="toggleShowMessageBtn"]')
.click();
cy.get('[data-cy="approvalHistoryTimeline"]')
.children('.p-timeline-event')
.eq(1)
.get('[data-cy="approvalEventMessage"]')
.should('contain.text', 'I approve this message!')
cy.get('[data-cy="approvalHistoryTimeline"]')
.children('.p-timeline-event')
.eq(1)
.get('[data-cy="toggleShowMessageBtn"]')
.click();
cy.get('[data-cy="approvalHistoryTimeline"]')
.children('.p-timeline-event')
.eq(1)
.get('[data-cy="approvalEventMessage"]')
.should('not.exist')

cy.get('[data-cy="overallPointsEarnedCard"] [data-cy="mediaInfoCardTitle"]')
.contains('50');
cy.get('[data-cy="pointsAchievedTodayCard"] [data-cy="mediaInfoCardTitle"]')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ describe('Self Report Approval History Tests', () => {
cy.createSkill(1, 1, 1, { selfReportingType: 'Approval' });
cy.createSkill(1, 1, 2, { selfReportingType: 'Approval' });
cy.createSkill(1, 1, 3, { selfReportingType: 'Approval' });
cy.createSkill(1, 1, 4, { selfReportingType: 'Approval' });
// cy.reportSkill(1, 2, 'user6', '2020-09-11 11:00')
// cy.reportSkill(1, 2, 'user5', '2020-09-12 11:00')
// cy.reportSkill(1, 2, 'user4', '2020-09-13 11:00')
Expand All @@ -68,9 +69,35 @@ describe('Self Report Approval History Tests', () => {
cy.approveAllRequests();
cy.reportSkill(1, 2, 'user2', '2020-09-16 11:00');
cy.rejectRequest(0);
cy.reportSkill(1, 4, 'user2', '2024-09-16 11:00');
cy.approveRequest(1, 0, 'I approve this message!');

cy.visit('/administrator/projects/proj1/self-report');
cy.validateTable(approvalHistoryTableSelector, [
[
{
colIndex: 0,
value: 'Very Great Skill 4'
}, {
colIndex: 0,
value: 'user2'
},
{
colIndex: 1,
value: 'Approved'
}, {
colIndex: 1,
value: 'Explanation: I approve this message!'
},
{
colIndex: 2,
value: '2024-09-16 11:00'
},
{
colIndex: 3,
value: 'Today'
}
],
[
{
colIndex: 0,
Expand Down Expand Up @@ -886,8 +913,8 @@ describe('Self Report Approval History Tests', () => {
.contains(('There are no records to show'));

cy.get('[data-p-index="0"] [data-pc-name="rowcheckbox"]').click()
cy.get('[data-cy="approveBtn"]')
.click();
cy.get('[data-cy="approveBtn"]').click();
cy.get('[data-cy="saveDialogBtn"]').click();
cy.validateTable(approvalHistoryTableSelector, [
[{
colIndex: 0,
Expand Down Expand Up @@ -923,9 +950,8 @@ describe('Self Report Approval History Tests', () => {
cy.wait(1000);

cy.get('[data-p-index="0"] [data-pc-name="rowcheckbox"]').click()
cy.get('[data-cy="approveBtn"]')
.click();

cy.get('[data-cy="approveBtn"]').click();
cy.get('[data-cy="saveDialogBtn"]').click();

cy.validateTable(approvalHistoryTableSelector, [
[{
Expand Down
Loading

0 comments on commit cd48cdd

Please sign in to comment.