Skip to content

Commit b4da8d3

Browse files
Added endpoint to mark users as stale, added cronjob to call once weekly, setting in admin panel to set as stale, updated team member search to omit stale users, updated tests
1 parent d42beb0 commit b4da8d3

21 files changed

Lines changed: 319 additions & 6 deletions

File tree

app/frontend/src/components/admin/AdministerUser.vue

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script setup>
22
import { storeToRefs } from 'pinia';
3-
import { onMounted } from 'vue';
3+
import { onMounted, ref } from 'vue';
44
import { useI18n } from 'vue-i18n';
55
66
import { useAdminStore } from '~/store/admin';
@@ -17,16 +17,35 @@ const properties = defineProps({
1717
const adminStore = useAdminStore();
1818
1919
const { user } = storeToRefs(adminStore);
20+
const updatingStale = ref(false);
2021
2122
onMounted(async () => {
2223
await adminStore.readUser(properties.userId);
2324
});
25+
26+
async function updateStale(stale) {
27+
updatingStale.value = true;
28+
try {
29+
await adminStore.updateUser(properties.userId, { stale });
30+
} finally {
31+
updatingStale.value = false;
32+
}
33+
}
2434
</script>
2535

2636
<template>
2737
<div>
2838
<h3>{{ user.fullName }}</h3>
2939
<h4 :lang="locale">{{ $t('trans.administerUser.userDetails') }}</h4>
40+
<v-switch
41+
data-test="stale-user-switch"
42+
color="warning"
43+
:disabled="updatingStale"
44+
label="Mark as Stale"
45+
:loading="updatingStale"
46+
:model-value="Boolean(user.stale)"
47+
@update:model-value="updateStale"
48+
/>
3049
<pre>{{ user }}</pre>
3150
</div>
3251
</template>

app/frontend/src/components/forms/manage/AddTeamMember.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ async function searchUsers(input) {
126126
isLoading.value = true;
127127
try {
128128
let params = {};
129+
params.stale = false;
129130
params.idpCode = selectedIdp.value;
130131
let teamMembershipConfig = idpStore.teamMembershipSearch(selectedIdp.value);
131132
if (teamMembershipConfig) {

app/frontend/src/services/adminService.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,19 @@ export default {
143143
return appAxios().get(`${ApiRoutes.ADMIN}${ApiRoutes.USERS}/${userId}`);
144144
},
145145

146+
/**
147+
* Update a user's administrable fields.
148+
* @param {string} userId The user GUID
149+
* @param {{stale: boolean}} data The user fields to update
150+
* @returns {Promise} An axios response
151+
*/
152+
updateUser(userId, data) {
153+
return appAxios().patch(
154+
`${ApiRoutes.ADMIN}${ApiRoutes.USERS}/${userId}`,
155+
data
156+
);
157+
},
158+
146159
//
147160
// External API calls
148161
//

app/frontend/src/store/admin.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,21 @@ export const useAdminStore = defineStore('admin', {
208208
});
209209
}
210210
},
211+
async updateUser(userId, data) {
212+
try {
213+
const response = await adminService.updateUser(userId, data);
214+
this.user = response.data;
215+
} catch (error) {
216+
const notificationStore = useNotificationStore();
217+
notificationStore.addNotification({
218+
text: i18n.t('trans.store.admin.getUserErrMsg'),
219+
consoleError: i18n.t('trans.store.admin.getUserConsErrMsg', {
220+
userId: userId,
221+
error: error,
222+
}),
223+
});
224+
}
225+
},
211226

212227
//
213228
// External APIs

app/frontend/tests/unit/components/admin/AdministerUser.spec.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,31 @@ describe('AdministerUser.vue', () => {
4444
await flushPromises();
4545
expect(wrapper.text()).toContain('alice');
4646
});
47+
48+
it('updates the stale status', async () => {
49+
adminStore.readUser.mockImplementation(() => {});
50+
adminStore.updateUser.mockResolvedValue();
51+
adminStore.user = {
52+
fullName: 'alice',
53+
keycloakId: '1',
54+
stale: false,
55+
};
56+
const wrapper = mount(AdministerUser, {
57+
props: {
58+
userId: 'me',
59+
},
60+
global: {
61+
plugins: [pinia],
62+
stubs: {},
63+
},
64+
});
65+
66+
await flushPromises();
67+
wrapper
68+
.findComponent({ name: 'VSwitch' })
69+
.vm.$emit('update:modelValue', true);
70+
await flushPromises();
71+
72+
expect(adminStore.updateUser).toHaveBeenCalledWith('me', { stale: true });
73+
});
4774
});

app/frontend/tests/unit/services/adminService.spec.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,15 @@ describe('Admin Service', () => {
131131
expect(result).toBeTruthy();
132132
expect(mockAxios.history.get).toHaveLength(1);
133133
});
134+
135+
it('calls patch endpoint', async () => {
136+
mockAxios.onPatch(endpoint).reply(200);
137+
138+
const result = await adminService.updateUser(zeroUuid, { stale: true });
139+
expect(result).toBeTruthy();
140+
expect(mockAxios.history.patch).toHaveLength(1);
141+
expect(JSON.parse(mockAxios.history.patch[0].data)).toEqual({ stale: true });
142+
});
134143
});
135144

136145
//

app/frontend/tests/unit/store/modules/admin.actions.spec.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,16 @@ describe('admin actions', () => {
244244
});
245245
});
246246

247+
it('updateUser should replace the current user', async () => {
248+
const updatedUser = { id: 'userId', stale: true };
249+
adminService.updateUser.mockResolvedValue({ data: updatedUser });
250+
251+
await mockStore.updateUser('userId', { stale: true });
252+
253+
expect(adminService.updateUser).toHaveBeenCalledWith('userId', { stale: true });
254+
expect(mockStore.user).toEqual(updatedUser);
255+
256+
});
247257
it('addFCProactiveHelp should commit to SET_FCPROACTIVEHELP', async () => {
248258
mockStore.fcProactiveHelp = undefined;
249259
adminService.addFCProactiveHelp.mockResolvedValue({ data: {} });

app/src/components/idpService.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ class IdpService {
215215
.modify('filterFirstName', params.firstName)
216216
.modify('filterLastName', params.lastName)
217217
.modify('filterEmail', params.email, false, false)
218+
.modify('filterStale', params.stale)
218219
.modify('filterSearch', params.search)
219220
.modify('orderLastFirstAscending');
220221
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
exports.up = async function (knex) {
2+
await knex.schema.alterTable('user', (table) => {
3+
table.boolean('stale').notNullable().defaultTo(false).comment('Whether the user has been identified as stale.');
4+
});
5+
};
6+
7+
exports.down = async function (knex) {
8+
await knex.schema.alterTable('user', (table) => {
9+
table.dropColumn('stale');
10+
});
11+
};

app/src/forms/admin/routes.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ routes.get('/users/:userId', async (req, res, next) => {
6767
await userController.read(req, res, next);
6868
});
6969

70+
routes.patch('/users/:userId', async (req, res, next) => {
71+
await userController.update(req, res, next);
72+
});
73+
7074
//
7175
// External APIs
7276
//

0 commit comments

Comments
 (0)