Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SDESK-6851] Refactor locking mechanism #1789

Merged
merged 14 commits into from
May 23, 2023
81 changes: 7 additions & 74 deletions client/actions/assignments/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import {get, cloneDeep, has, pick} from 'lodash';

import {appConfig} from 'appConfig';
import {IAssignmentItem} from '../../interfaces';
import {planningApi} from '../../superdeskApi';

import * as selectors from '../../selectors';
import * as actions from '../';
import {ASSIGNMENTS, ALL_DESKS, SORT_DIRECTION} from '../../constants';
import planningUtils from '../../utils/planning';
import {lockUtils, getErrorMessage, isExistingItem, gettext} from '../../utils';
import {getErrorMessage, isExistingItem, gettext} from '../../utils';
import planning from '../planning';
import {assignmentsViewRequiresArchiveItems} from '../../components/Assignments/AssignmentItem/fields';

Expand Down Expand Up @@ -240,24 +241,6 @@ const fetchAssignmentById = (id, force = false, recieve = true) => (
}
);

/**
* Action dispatcher to query the API for all Assignments that are currently locked
* @return Array of locked Assignments
*/
const queryLockedAssignments = () => (
(dispatch, getState, {api}) => (
api('assignments').query({
source: JSON.stringify(
{query: {constant_score: {filter: {exists: {field: 'lock_session'}}}}}
),
})
.then(
(data) => Promise.resolve(data._items),
(error) => Promise.reject(error)
)
)
);

/**
* Action to receive the list of Assignments and store them in the store
* Also loads all the associated contacts (if any)
Expand Down Expand Up @@ -394,53 +377,6 @@ const revert = (item) => (
)
);

/**
* Action to lock an assignment
* @param {IAssignmentItem} assignment - Assignment to be unlocked
* @param {String} action - The action to assign to the lock
* @return Promise
*/
const lock = (assignment: IAssignmentItem, action: string = 'edit') => (
(dispatch, getState, {api, notify}) => {
if (lockUtils.isItemLockedInThisSession(
assignment,
selectors.general.session(getState()),
selectors.locks.getLockedItems(getState())
)) {
return Promise.resolve(assignment);
}

return api('assignments_lock', assignment).save({}, {lock_action: action})
.then(
(lockedItem: IAssignmentItem) => lockedItem,
(error) => {
const msg = get(error, 'data._message') || 'Could not lock the assignment.';

notify.error(msg);
if (error) throw error;
});
}
);

/**
* Action to unlock an assignment
* @param {IAssignmentItem} assignment - Assignment to be unlocked
* @return Promise
*/
const unlock = (assignment: IAssignmentItem) => (
(dispatch, getState, {api, notify}) => (
api('assignments_unlock', assignment).save({})
.then(
(unlockedItem: IAssignmentItem) => unlockedItem,
(error) => {
const msg = get(error, 'data._message') || 'Could not unlock the assignment.';

notify.error(msg);
throw error;
})
)
);

/**
* Fetch history of an assignment
* @param {object} assignment - The Assignment to load history for
Expand Down Expand Up @@ -605,21 +541,21 @@ const removeAssignment = (assignment) => (
)
);

const unlink = (assignment) => (
(dispatch, getState, {api, notify}) => (
function unlink(assignment: IAssignmentItem) {
return (dispatch, getState, {api, notify}) => (
api('assignments_unlink').save({}, {
assignment_id: assignment._id,
item_id: get(assignment, 'item_ids[0]'),
})
.then(() => {
notify.success(gettext('Assignment reverted.'));
return dispatch(self.unlock(assignment));
return planningApi.locks.unlockItem(assignment);
petrjasek marked this conversation as resolved.
Show resolved Hide resolved
}, (error) => {
notify.error(get(error, 'data._message') || gettext('Could not unlock the assignment.'));
throw error;
})
)
);
);
}

// eslint-disable-next-line consistent-this
const self = {
Expand All @@ -631,9 +567,6 @@ const self = {
createFromTemplateAndShow,
complete,
revert,
lock,
unlock,
queryLockedAssignments,
loadPlanningAndEvent,
loadArchiveItems,
loadArchiveItem,
Expand Down
35 changes: 24 additions & 11 deletions client/actions/assignments/notifications.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import {get, cloneDeep} from 'lodash';

import {IWebsocketMessageData} from '../../interfaces';

import {planningApi} from '../../superdeskApi';
import {ASSIGNMENTS, WORKSPACE, MODALS} from '../../constants';
import {lockUtils, assignmentUtils, gettext, isExistingItem} from '../../utils';

import * as selectors from '../../selectors';
import assignments from './index';
import main from '../main';
import {get, cloneDeep} from 'lodash';
import planning from '../planning';
import {ASSIGNMENTS, WORKSPACE, MODALS} from '../../constants';
import {lockUtils, assignmentUtils, gettext, isExistingItem} from '../../utils';
import {hideModal, showModal} from '../index';
import * as actions from '../../actions';

Expand Down Expand Up @@ -149,6 +154,12 @@ const onAssignmentUpdated = (_e, data) => (
lock_time: null,
};

planningApi.locks.setItemAsUnlocked({
item: data.item,
etag: data.etag,
from_ingest: false,
type: 'assignment',
});
dispatch({
type: ASSIGNMENTS.ACTIONS.UNLOCK_ASSIGNMENT,
payload: {assignment: item},
Expand Down Expand Up @@ -185,9 +196,10 @@ const _updatePlannigRelatedToAssignment = (data) => (
}
);

const onAssignmentLocked = (_e, data) => (
(dispatch) => {
function onAssignmentLocked(_e, data: IWebsocketMessageData['ITEM_LOCKED']) {
return (dispatch) => {
if (get(data, 'item')) {
planningApi.locks.setItemAsLocked(data);
return dispatch(assignments.api.fetchAssignmentById(data.item, false))
.then((assignmentInStore) => {
let item = {
Expand All @@ -209,8 +221,8 @@ const onAssignmentLocked = (_e, data) => (
}

return Promise.resolve();
}
);
};
}

/**
* WS Action when a Planning item gets unlocked
Expand All @@ -220,9 +232,10 @@ const onAssignmentLocked = (_e, data) => (
* @param {object} _e - Event object
* @param {object} data - Planning and User IDs
*/
const onAssignmentUnlocked = (_e, data) => (
(dispatch, getState) => {
function onAssignmentUnlocked(_e, data: IWebsocketMessageData['ITEM_UNLOCKED']) {
return (dispatch, getState) => {
if (get(data, 'item')) {
planningApi.locks.setItemAsUnlocked(data);
return dispatch(assignments.api.fetchAssignmentById(data.item, false))
.then((assignmentInStore) => {
const locks = selectors.locks.getLockedItems(getState());
Expand Down Expand Up @@ -265,8 +278,8 @@ const onAssignmentUnlocked = (_e, data) => (
return Promise.resolve();
});
}
}
);
};
}

/**
* WS Action when an Assignment is deleted
Expand Down
51 changes: 1 addition & 50 deletions client/actions/assignments/tests/api_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ describe('actions.assignments.api', () => {
});

describe('queryLockedAssignments', () => {
it('queries for locked assignments', (done) => (
xit('queries for locked assignments', (done) => (
store.test(done, assignmentsApi.queryLockedAssignments())
.then(() => {
const query = {constant_score: {filter: {exists: {field: 'lock_session'}}}};
Expand Down Expand Up @@ -528,55 +528,6 @@ describe('actions.assignments.api', () => {
});
});

describe('assignments_lock', () => {
beforeEach(() => {
services.api('assignments_lock').save = sinon.spy(() => Promise.resolve(data.assignments[0]));
services.api('assignments_unlock').save = sinon.spy(() => Promise.resolve(data.assignments[0]));
});

afterEach(() => {
restoreSinonStub(services.api('assignments_lock').save);
restoreSinonStub(services.api('assignments_unlock').save);
});

it('calls lock endpoint if assignment not locked', (done) => {
store.test(done, assignmentsApi.lock(data.assignments[0]))
.then(() => {
expect(services.api('assignments_lock').save.callCount).toBe(1);
expect(services.api('assignments_lock').save.args[0]).toEqual([
{},
{lock_action: 'edit'},
]);
done();
})
.catch(done.fail);
});

it('does not call lock endpoint if assignment already locked', (done) => {
store.initialState.assignment.assignments['1'] = {
...store.initialState.assignment.assignments['1'],
lock_user: 'ident1',
lock_session: 'session1',
};
store.test(done, assignmentsApi.lock(store.initialState.assignment.assignments['1']))
.then((item) => {
expect(services.api('assignments_lock').save.callCount).toBe(0);
expect(item).toEqual(store.initialState.assignment.assignments[1]);
done();
})
.catch(done.fail);
});

it('calls unlock endpoint', (done) => {
store.test(done, assignmentsApi.unlock(data.assignments[0]))
.then(() => {
expect(services.api('assignments_unlock').save.callCount).toBe(1);
done();
})
.catch(done.fail);
});
});

it('removeAssignment', (done) => (
store.test(done, assignmentsApi.removeAssignment(data.assignments[0]))
.then(() => {
Expand Down
28 changes: 19 additions & 9 deletions client/actions/assignments/tests/notification_test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import sinon from 'sinon';

import {planningApi} from '../../../superdeskApi';
import {getTestActionStore, restoreSinonStub} from '../../../utils/testUtils';
import {createTestStore, assignmentUtils} from '../../../utils';
import {registerNotifications} from '../../../utils/notifications';
Expand All @@ -8,7 +9,7 @@ import assignmentsUi from '../ui';
import assignmentsApi from '../api';
import main from '../../main';
import assignmentNotifications from '../notifications';
import planningApi from '../../planning/api';
import planningApis from '../../planning/api';

describe('actions.assignments.notification', () => {
let store;
Expand Down Expand Up @@ -134,7 +135,7 @@ describe('actions.assignments.notification', () => {
() => () => Promise.resolve()
);
sinon.stub(assignmentUtils, 'getCurrentSelectedDeskId').returns('desk1');
sinon.stub(planningApi, 'loadPlanningByIds').callsFake(
sinon.stub(planningApis, 'loadPlanningByIds').callsFake(
() => () => Promise.resolve()
);
});
Expand All @@ -143,7 +144,7 @@ describe('actions.assignments.notification', () => {
restoreSinonStub(assignmentsUi.reloadAssignments);
restoreSinonStub(assignmentUtils.getCurrentSelectedDeskId);
restoreSinonStub(main.fetchItemHistory);
restoreSinonStub(planningApi.loadPlanningByIds);
restoreSinonStub(planningApis.loadPlanningByIds);
});

it('update planning on assignment update', (done) => {
Expand All @@ -164,8 +165,8 @@ describe('actions.assignments.notification', () => {

testStore.dispatch(assignmentNotifications.onAssignmentUpdated({}, payload))
.then(() => {
expect(planningApi.loadPlanningByIds.callCount).toBe(1);
expect(planningApi.loadPlanningByIds.args).toEqual([
expect(planningApis.loadPlanningByIds.callCount).toBe(1);
expect(planningApis.loadPlanningByIds.args).toEqual([
[['p1']],
]);
expect(assignmentsUi.reloadAssignments.callCount).toBe(2);
Expand Down Expand Up @@ -234,11 +235,15 @@ describe('actions.assignments.notification', () => {

describe('`assignment lock`', () => {
beforeEach(() => {
sinon.stub(planningApi.locks, 'setItemAsLocked').returns(undefined);
sinon.stub(planningApi.locks, 'setItemAsUnlocked').returns(undefined);
sinon.stub(assignmentsApi, 'fetchAssignmentById').callsFake(() => (
Promise.resolve(store.initialState.assignment.assignments.as1)));
});

afterEach(() => {
restoreSinonStub(planningApi.locks.setItemAsLocked);
restoreSinonStub(planningApi.locks.setItemAsUnlocked);
restoreSinonStub(assignmentsApi.fetchAssignmentById);
});

Expand All @@ -254,6 +259,7 @@ describe('actions.assignments.notification', () => {

return store.test(done, assignmentNotifications.onAssignmentLocked({}, payload))
.then(() => {
expect(planningApi.locks.setItemAsLocked.callCount).toBe(1);
expect(store.dispatch.callCount).toBe(2);
expect(assignmentsApi.fetchAssignmentById.callCount).toBe(1);
expect(store.dispatch.args[1]).toEqual([{
Expand Down Expand Up @@ -282,6 +288,7 @@ describe('actions.assignments.notification', () => {

return store.test(done, assignmentNotifications.onAssignmentUnlocked({}, payload))
.then(() => {
expect(planningApi.locks.setItemAsUnlocked.callCount).toBe(1);
expect(store.dispatch.callCount).toBe(2);
expect(assignmentsApi.fetchAssignmentById.callCount).toBe(1);
expect(store.dispatch.args[1]).toEqual([{
Expand All @@ -305,20 +312,22 @@ describe('actions.assignments.notification', () => {

describe('`assignment:completed`', () => {
beforeEach(() => {
sinon.stub(planningApi.locks, 'setItemAsUnlocked').returns(undefined);
sinon.stub(assignmentsUi, 'queryAndGetMyAssignments').callsFake(
() => () => (Promise.resolve())
);
sinon.stub(assignmentUtils, 'getCurrentSelectedDeskId').returns('desk1');
sinon.stub(planningApi, 'loadPlanningByIds').callsFake(
sinon.stub(planningApis, 'loadPlanningByIds').callsFake(
() => () => (Promise.resolve())
);
});

afterEach(() => {
restoreSinonStub(planningApi.locks.setItemAsUnlocked);
restoreSinonStub(assignmentsUi.reloadAssignments);
restoreSinonStub(assignmentsUi.queryAndGetMyAssignments);
restoreSinonStub(assignmentUtils.getCurrentSelectedDeskId);
restoreSinonStub(planningApi.loadPlanningByIds);
restoreSinonStub(planningApis.loadPlanningByIds);
});

it('update planning on assignment complete', (done) => {
Expand All @@ -344,8 +353,8 @@ describe('actions.assignments.notification', () => {
.then(() => {
coverage1 = getCoverage(payload);

expect(planningApi.loadPlanningByIds.callCount).toBe(1);
expect(planningApi.loadPlanningByIds.args).toEqual([
expect(planningApis.loadPlanningByIds.callCount).toBe(1);
expect(planningApis.loadPlanningByIds.args).toEqual([
[['p1']],
]);
expect(assignmentsUi.reloadAssignments.callCount).toBe(2);
Expand Down Expand Up @@ -375,6 +384,7 @@ describe('actions.assignments.notification', () => {

return store.test(done, assignmentNotifications.onAssignmentUpdated({}, payload))
.then(() => {
expect(planningApi.locks.setItemAsUnlocked.callCount).toBe(1);
expect(assignmentsApi.fetchAssignmentById.callCount).toBe(1);
expect(store.dispatch.args[5]).toEqual([{
type: 'UNLOCK_ASSIGNMENT',
Expand Down
Loading