From 534e1420ba300eaac282d81519f6672f6632f0e5 Mon Sep 17 00:00:00 2001 From: John Sandoval Date: Fri, 22 May 2026 10:52:34 -0600 Subject: [PATCH 1/3] Handle optional permission unsetting directly --- .../Users/UserRowEdit/UserRowEdit.ts | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/webroot/src/components/Users/UserRowEdit/UserRowEdit.ts b/webroot/src/components/Users/UserRowEdit/UserRowEdit.ts index fa8a3171a..7a2291c72 100644 --- a/webroot/src/components/Users/UserRowEdit/UserRowEdit.ts +++ b/webroot/src/components/Users/UserRowEdit/UserRowEdit.ts @@ -537,15 +537,18 @@ class UserRowEdit extends mixins(MixinForm) { // Server endpoints may not be idempotent so the frontend needs to handle statefulness; // e.g. if the user's server permisson is already false, we may get a server error if we try to send false again. - const existingCompactPermission = this.getCompactPermission(this.rowUserCompactPermission); + const existingCompactPermission: CompactPermission | null = this.rowUserCompactPermission; - if (existingCompactPermission !== Permission.READ_PRIVATE && !compactData.isReadPrivate) { - delete compactData.isReadPrivate; - } - if (existingCompactPermission !== Permission.ADMIN && !compactData.isAdmin) { + if (!existingCompactPermission?.isAdmin && !compactData.isAdmin) { delete compactData.isAdmin; } - // End: Handle compact statefulness + if (!existingCompactPermission?.isReadSsn && !compactData.isReadSsn) { + delete compactData.isReadSsn; + } + if (!existingCompactPermission?.isReadPrivate && !compactData.isReadPrivate) { + delete compactData.isReadPrivate; + } + // End: Handle compact permission statefulness stateKeys.forEach((stateKey) => { const keyNum = stateKey.split('-').pop(); @@ -558,19 +561,22 @@ class UserRowEdit extends mixins(MixinForm) { // Server endpoints may not be idempotent so the frontend needs to handle statefulness; // e.g. if the user's server permisson is already false, we may get a server error if we try to send false again. - const existingStatePermission = this.getStatePermission(this.userStatePermissions.find((permission) => - permission.state?.abbrev === stateAbbrev) || null); + const existingStatePermission = existingCompactPermission?.states?.find((permission) => + permission.state?.abbrev === stateAbbrev); - if (existingStatePermission !== Permission.READ_PRIVATE && !stateData.isReadPrivate) { - delete stateData.isReadPrivate; + if (!existingStatePermission?.isAdmin && !stateData.isAdmin) { + delete stateData.isAdmin; } - if (existingStatePermission !== Permission.WRITE && !stateData.isWrite) { + if (!existingStatePermission?.isWrite && !stateData.isWrite) { delete stateData.isWrite; } - if (existingStatePermission !== Permission.ADMIN && !stateData.isAdmin) { - delete stateData.isAdmin; + if (!existingStatePermission?.isReadSsn && !stateData.isReadSsn) { + delete stateData.isReadSsn; + } + if (!existingStatePermission?.isReadPrivate && !stateData.isReadPrivate) { + delete stateData.isReadPrivate; } - // End: Handle state statefulness + // End: Handle state permission statefulness compactData.states.push(stateData); }); From edb5ed0723b2456db0a80b8bb852fa015b7ee329 Mon Sep 17 00:00:00 2001 From: John Sandoval Date: Fri, 22 May 2026 11:12:33 -0600 Subject: [PATCH 2/3] Update model serializer checks --- webroot/src/models/StaffUser/StaffUser.model.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webroot/src/models/StaffUser/StaffUser.model.ts b/webroot/src/models/StaffUser/StaffUser.model.ts index e8cc2685b..73020587c 100644 --- a/webroot/src/models/StaffUser/StaffUser.model.ts +++ b/webroot/src/models/StaffUser/StaffUser.model.ts @@ -439,7 +439,7 @@ export class StaffUserSerializer { const hasStateWriteSetting = Object.prototype.hasOwnProperty.call(statePermission, 'isWrite'); const hasStateAdminSetting = Object.prototype.hasOwnProperty.call(statePermission, 'isAdmin'); - if (hasStateReadPrivateSetting || hasStateWriteSetting || hasStateAdminSetting) { + if (hasStateReadPrivateSetting || hasStateReadSsnSetting || hasStateWriteSetting || hasStateAdminSetting) { const actions: any = {}; jurisdictions[statePermission.abbrev] = { actions }; @@ -465,7 +465,7 @@ export class StaffUserSerializer { const hasCompactAdminSetting = Object.prototype.hasOwnProperty.call(compactPermission, 'isAdmin'); const hasStates = Boolean(compactPermission.states?.length); - if (hasCompactReadPrivateSetting || hasCompactAdminSetting) { + if (hasCompactReadPrivateSetting || hasCompactReadSsnSetting || hasCompactAdminSetting) { const actions: any = {}; serverData.permissions[compactPermission.compact] = { actions }; From 141ff6ffb2415e18b360f05ae039d4f21e4dfbd3 Mon Sep 17 00:00:00 2001 From: John Sandoval Date: Wed, 27 May 2026 14:40:40 -0600 Subject: [PATCH 3/3] PR review feedback --- .../Users/UserRowEdit/UserRowEdit.spec.ts | 196 +++++++++++++++++- .../models/StaffUser/StaffUser.model.spec.ts | 193 +++++++++++++++++ 2 files changed, 388 insertions(+), 1 deletion(-) diff --git a/webroot/src/components/Users/UserRowEdit/UserRowEdit.spec.ts b/webroot/src/components/Users/UserRowEdit/UserRowEdit.spec.ts index 9e52c1fd9..04b3f00f7 100644 --- a/webroot/src/components/Users/UserRowEdit/UserRowEdit.spec.ts +++ b/webroot/src/components/Users/UserRowEdit/UserRowEdit.spec.ts @@ -8,7 +8,54 @@ import { expect } from 'chai'; import { mountShallow } from '@tests/helpers/setup'; import UserRowEdit from '@components/Users/UserRowEdit/UserRowEdit.vue'; -import { StaffUser } from '@models/StaffUser/StaffUser.model'; +import { Permission } from '@/app.config'; +import { Compact, CompactType } from '@models/Compact/Compact.model'; +import { MutationTypes } from '@store/user/user.mutations'; +import { + StaffUser, + CompactPermission, + StatePermission +} from '@models/StaffUser/StaffUser.model'; + +const compactType = CompactType.ASLP; +const buildCompactPermission = ( + compactOverrides: Partial = {}, + stateOverrides: Array> = [] +): CompactPermission => ({ + compact: new Compact({ type: compactType }), + isReadPrivate: false, + isReadSsn: false, + isAdmin: false, + states: stateOverrides.map((stateOverride) => ({ + state: { abbrev: 'ky' } as any, + isReadPrivate: false, + isReadSsn: false, + isWrite: false, + isAdmin: false, + ...stateOverride, + })), + ...compactOverrides, +}); +const setFormData = (wrapper, data: Record) => { + wrapper.vm.formData = Object.keys(data).reduce((preppedData, key) => ({ + ...preppedData, + [key]: { + value: data[key], + isDisabled: false, + isSubmitInput: false, + }, + }), {}); +}; +const storeSetup = (wrapper) => { + wrapper.vm.$store.commit(`user/${MutationTypes.STORE_UPDATE_CURRENT_COMPACT}`, new Compact({ type: compactType })); + wrapper.vm.$store.commit(`user/${MutationTypes.STORE_UPDATE_USER}`, new StaffUser({ + permissions: [buildCompactPermission({ + isReadPrivate: true, + isReadSsn: true, + isAdmin: true, + })], + })); +}; describe('UserRowEdit component', async () => { it('should mount the component', async () => { @@ -21,4 +68,151 @@ describe('UserRowEdit component', async () => { expect(wrapper.exists()).to.equal(true); expect(wrapper.findComponent(UserRowEdit).exists()).to.equal(true); }); + it('should successfully preserve missing compact permissions', async () => { + const wrapper = await mountShallow(UserRowEdit, { + props: { + user: new StaffUser(), + }, + }); + + storeSetup(wrapper); + setFormData(wrapper, { + compact: compactType, + compactPermission: Permission.NONE, + }); + + const preppedData = wrapper.vm.prepFormData(); + + expect(preppedData).to.deep.equal({ + compact: compactType, + states: [], + }); + }); + it('should successfully remove compact permissions', async () => { + const wrapper = await mountShallow(UserRowEdit, { + props: { + user: new StaffUser({ + permissions: [buildCompactPermission({ + isReadPrivate: true, + isReadSsn: false, + isAdmin: true, + })], + }), + }, + }); + + storeSetup(wrapper); + setFormData(wrapper, { + compact: compactType, + compactPermission: Permission.NONE, + }); + + const preppedData = wrapper.vm.prepFormData(); + + expect(preppedData).to.deep.equal({ + compact: compactType, + isReadPrivate: false, + isAdmin: false, + states: [], + }); + }); + it('should successfully preserve missing state permissions', async () => { + const wrapper = await mountShallow(UserRowEdit, { + props: { + user: new StaffUser({ + permissions: [buildCompactPermission({}, [ + { state: { abbrev: 'ky' }}, + ])], + }), + }, + }); + + storeSetup(wrapper); + setFormData(wrapper, { + compact: compactType, + compactPermission: Permission.NONE, + 'state-option-0': 'ky', + 'state-permission-0': Permission.NONE, + }); + + const preppedData = wrapper.vm.prepFormData(); + + expect(preppedData).to.deep.equal({ + compact: compactType, + states: [ + { abbrev: 'ky' }, + ], + }); + }); + it('should successfully remove state permissions', async () => { + const wrapper = await mountShallow(UserRowEdit, { + props: { + user: new StaffUser({ + permissions: [buildCompactPermission({}, [ + { + state: { abbrev: 'ky' } as any, + isReadPrivate: true, + isReadSsn: true, + isWrite: true, + isAdmin: true, + }, + ])], + }), + }, + }); + + storeSetup(wrapper); + setFormData(wrapper, { + compact: compactType, + compactPermission: Permission.NONE, + 'state-option-0': 'ky', + 'state-permission-0': Permission.READ_PRIVATE, + }); + + const preppedData = wrapper.vm.prepFormData(); + + expect(preppedData).to.deep.equal({ + compact: compactType, + states: [ + { + abbrev: 'ky', + isReadPrivate: true, + isReadSsn: false, + isWrite: false, + isAdmin: false, + }, + ], + }); + }); + it('should successfully update state permissions', async () => { + const wrapper = await mountShallow(UserRowEdit, { + props: { + user: new StaffUser({ + permissions: [buildCompactPermission()], + }), + }, + }); + + storeSetup(wrapper); + setFormData(wrapper, { + compact: compactType, + compactPermission: Permission.NONE, + 'state-option-0': 'ky', + 'state-permission-0': Permission.WRITE, + }); + + const preppedData = wrapper.vm.prepFormData(); + + expect(preppedData).to.deep.equal({ + compact: compactType, + states: [ + { + abbrev: 'ky', + isReadPrivate: true, + isReadSsn: true, + isWrite: true, + }, + ], + }); + }); }); diff --git a/webroot/src/models/StaffUser/StaffUser.model.spec.ts b/webroot/src/models/StaffUser/StaffUser.model.spec.ts index b4dcb06a2..aeaee08b3 100644 --- a/webroot/src/models/StaffUser/StaffUser.model.spec.ts +++ b/webroot/src/models/StaffUser/StaffUser.model.spec.ts @@ -407,4 +407,197 @@ describe('Staff User model', () => { }, }); }); + it('should prepare a Staff User for server request through serializer (compact readPrivate key only)', () => { + const data = { + permissions: [ + { + compact: CompactType.ASLP, + isReadPrivate: false, + states: [], + }, + ], + }; + const requestData = StaffUserSerializer.toServer(data); + + expect(requestData).to.matchPattern({ + permissions: { + [CompactType.ASLP]: { + actions: { + readPrivate: false, + }, + }, + }, + '...': '', + }); + }); + it('should prepare a Staff User for server request through serializer (compact readSSN key only)', () => { + const data = { + permissions: [ + { + compact: CompactType.ASLP, + isReadSsn: false, + states: [], + }, + ], + }; + const requestData = StaffUserSerializer.toServer(data); + + expect(requestData).to.matchPattern({ + permissions: { + [CompactType.ASLP]: { + actions: { + readSSN: false, + }, + }, + }, + '...': '', + }); + }); + it('should prepare a Staff User for server request through serializer (compact admin key only)', () => { + const data = { + permissions: [ + { + compact: CompactType.ASLP, + isAdmin: false, + states: [], + }, + ], + }; + const requestData = StaffUserSerializer.toServer(data); + + expect(requestData).to.matchPattern({ + permissions: { + [CompactType.ASLP]: { + actions: { + admin: false, + }, + }, + }, + '...': '', + }); + }); + it('should prepare a Staff User for server request through serializer (state readPrivate key only)', () => { + const data = { + permissions: [ + { + compact: CompactType.ASLP, + states: [ + { + abbrev: 'co', + isReadPrivate: false, + }, + ], + }, + ], + }; + const requestData = StaffUserSerializer.toServer(data); + + expect(requestData).to.matchPattern({ + permissions: { + [CompactType.ASLP]: { + jurisdictions: { + co: { + actions: { + readPrivate: false, + }, + }, + }, + }, + }, + '...': '', + }); + }); + it('should prepare a Staff User for server request through serializer (state readSSN key only)', () => { + const data = { + permissions: [ + { + compact: CompactType.ASLP, + states: [ + { + abbrev: 'co', + isReadSsn: false, + }, + ], + }, + ], + }; + const requestData = StaffUserSerializer.toServer(data); + + expect(requestData).to.matchPattern({ + permissions: { + [CompactType.ASLP]: { + jurisdictions: { + co: { + actions: { + readSSN: false, + }, + }, + }, + }, + }, + '...': '', + }); + }); + it('should prepare a Staff User for server request through serializer (state write key only)', () => { + const data = { + permissions: [ + { + compact: CompactType.ASLP, + states: [ + { + abbrev: 'co', + isWrite: false, + }, + ], + }, + ], + }; + const requestData = StaffUserSerializer.toServer(data); + + expect(requestData).to.matchPattern({ + permissions: { + [CompactType.ASLP]: { + jurisdictions: { + co: { + actions: { + write: false, + }, + }, + }, + }, + }, + '...': '', + }); + }); + it('should prepare a Staff User for server request through serializer (state admin key only)', () => { + const data = { + permissions: [ + { + compact: CompactType.ASLP, + states: [ + { + abbrev: 'co', + isAdmin: false, + }, + ], + }, + ], + }; + const requestData = StaffUserSerializer.toServer(data); + + expect(requestData).to.matchPattern({ + permissions: { + [CompactType.ASLP]: { + jurisdictions: { + co: { + actions: { + admin: false, + }, + }, + }, + }, + }, + '...': '', + }); + }); });