Skip to content

Commit

Permalink
(feat) Offline Registered Patients in Offline Tools Card | Sync Queue…
Browse files Browse the repository at this point in the history
… Functions for Retrieving Full Sync Item | Sync Queue Manipulation Tests (#402)

* Precache importmap refs as part of the static dependency lifecycle. Remove obsolete XMLHttpRequest patches.

* Updated translations.

* Consider offline registered patients in the offline tools patient overview card.

* Provide API for retrieving full sync items.

* Export new full sync item API members.

* Create test cases covering the manipulation of the sync queue.

* Import fixup.

* Code simplifications.
  • Loading branch information
manuelroemer authored Apr 21, 2022
1 parent dfe11f1 commit c66b9f1
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { useOfflinePatientDataStore } from "../hooks/offline-patient-data-hooks"
import HeaderedQuickInfo from "../components/headered-quick-info.component";
import OverviewCard from "../components/overview-card.component";
import { routes } from "../constants";
import useSWR from "swr";
import { getSynchronizationItems } from "@openmrs/esm-framework";

const PatientsOverviewCard: React.FC = () => {
const { t } = useTranslation();
const downloaded = useDownloadedOfflinePatients();
const { data } = useDownloadedOfflinePatients();

return (
<OverviewCard
Expand All @@ -16,19 +18,42 @@ const PatientsOverviewCard: React.FC = () => {
>
<HeaderedQuickInfo
header={t("homeOverviewCardPatientsDownloaded", "Downloaded")}
content={downloaded}
content={data?.downloadedCount}
isLoading={!data}
/>
<HeaderedQuickInfo
header={t(
"homeOverviewCardPatientsNewlyRegistered",
"Newly registered"
)}
content={data?.registeredCount}
isLoading={!data}
/>
</OverviewCard>
);
};

function useDownloadedOfflinePatients() {
const store = useOfflinePatientDataStore();
return Object.values(store.offlinePatientDataSyncState).filter(
(patientSyncState) =>
patientSyncState.failedHandlers.length === 0 &&
patientSyncState.syncingHandlers.length === 0
).length;
return useSWR(["offlinePatientsTotalCount", store], async () => {
const downloadedCount = Object.values(
store.offlinePatientDataSyncState
).filter(
(patientSyncState) =>
patientSyncState.failedHandlers.length === 0 &&
patientSyncState.syncingHandlers.length === 0
).length;

const patientRegistrationSyncItems = await getSynchronizationItems(
"patient-registration"
);
const registeredCount = patientRegistrationSyncItems.length;

return {
downloadedCount,
registeredCount,
};
});
}

export default PatientsOverviewCard;
3 changes: 3 additions & 0 deletions packages/framework/esm-offline/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@ module.exports = {
transform: {
"^.+\\.tsx?$": ["@swc/jest"],
},
moduleNameMapper: {
"lodash-es": "lodash",
},
};
2 changes: 2 additions & 0 deletions packages/framework/esm-offline/src/public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export {
queueSynchronizationItem,
getSynchronizationItem,
getSynchronizationItems,
getFullSynchronizationItems,
getFullSynchronizationItemsFor,
getOfflineDb,
canBeginEditSynchronizationItemsOfType,
beginEditSynchronizationItem,
Expand Down
153 changes: 153 additions & 0 deletions packages/framework/esm-offline/src/sync.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import "fake-indexeddb/auto";
import { getLoggedInUser, LoggedInUser } from "@openmrs/esm-api";
import {
getFullSynchronizationItems,
getFullSynchronizationItemsFor,
getOfflineDb,
getSynchronizationItems,
getSynchronizationItemsFor,
QueueItemDescriptor,
queueSynchronizationItem,
queueSynchronizationItemFor,
deleteSynchronizationItem,
getSynchronizationItem,
} from "./sync";

interface MockSyncItem {
value: number;
}

const systemTime = new Date();
const mockUserId = "00000000-0000-0000-0000-000000000000";
const mockSyncItemType = "mock-sync-item";
const defaultMockSyncItem: MockSyncItem = {
value: 123,
};
const defaultMockSyncItemDescriptor: QueueItemDescriptor = {
dependencies: [],
id: "123",
displayName: "Mock Sync Item",
patientUuid: "00000000-0000-0000-0000-000000000001",
};

jest.mock("@openmrs/esm-api", () => ({
getLoggedInUser: jest.fn(async () => ({ uuid: mockUserId })),
}));

afterEach(async () => {
// We want each test case to start fresh with a clean sync queue.
await getOfflineDb().syncQueue.clear();
});

describe("Sync Queue", () => {
beforeAll(() => {
// We want to control the timers to ensure that we can test the `createdOn` attribute
// of the sync item (which is created using `new Date()`).
jest.useFakeTimers("modern");
jest.setSystemTime(systemTime);
});

afterAll(() => {
jest.useRealTimers();
});

it("enqueues sync item with expected attributes", async () => {
const id = await queueSynchronizationItemFor(
mockUserId,
mockSyncItemType,
defaultMockSyncItem,
defaultMockSyncItemDescriptor
);
const queuedItems = await getFullSynchronizationItemsFor<MockSyncItem>(
mockUserId,
mockSyncItemType
);

expect(queuedItems).toHaveLength(1);
expect(queuedItems[0].id).toBe(id);
expect(queuedItems[0].type).toBe(mockSyncItemType);
expect(queuedItems[0].userId).toBe(mockUserId);
expect(queuedItems[0].createdOn).toStrictEqual(systemTime);
expect(queuedItems[0].content).toStrictEqual(defaultMockSyncItem);
expect(queuedItems[0].descriptor).toStrictEqual(
defaultMockSyncItemDescriptor
);
});
});

describe("Logged-in user specific functions", () => {
it("enqueue and return sync items of currently logged-in user", async () => {
const loggedInUserId = (await getLoggedInUser()).uuid;
await queueSynchronizationItem(mockSyncItemType, defaultMockSyncItem);
const queuedItems = await getFullSynchronizationItems(mockSyncItemType);

expect(queuedItems).toHaveLength(1);
expect(queuedItems[0].userId).toBe(loggedInUserId);
});
});

describe("getSynchronizationItems", () => {
it("returns `content` of corresponding `getFullSynchronizationItems` call", async () => {
await queueSynchronizationItem(mockSyncItemType, defaultMockSyncItem);
const items = await getSynchronizationItems(mockSyncItemType);
const fullItems = await getFullSynchronizationItems(mockSyncItemType);
expect(items).toHaveLength(1);
expect(fullItems).toHaveLength(1);
expect(items[0]).toStrictEqual(fullItems[0].content);
});
});

describe("getSynchronizationItemsFor", () => {
it("returns `content` of corresponding `getFullSynchronizationItemsFor` call", async () => {
await queueSynchronizationItemFor(
mockUserId,
mockSyncItemType,
defaultMockSyncItem
);
const items = await getSynchronizationItemsFor(
mockUserId,
mockSyncItemType
);
const fullItems = await getFullSynchronizationItemsFor(
mockUserId,
mockSyncItemType
);

expect(items).toHaveLength(1);
expect(fullItems).toHaveLength(1);
expect(items[0]).toStrictEqual(fullItems[0].content);
});
});

describe("getSynchronizationItem", () => {
it("returns the specific sync item with given ID", async () => {
const id = await queueSynchronizationItem(
mockSyncItemType,
defaultMockSyncItem
);
const items = await getFullSynchronizationItems(mockSyncItemType);
const item = await getSynchronizationItem(id);
expect(item).toStrictEqual(items[0]);
});

it("returns undefined when no item with given ID exists", async () => {
const item = await getSynchronizationItem(404);
expect(item).toBeUndefined();
});
});

describe("deleteSynchronizationItem", () => {
it("deletes sync item with given ID", async () => {
const id = await queueSynchronizationItem(
mockSyncItemType,
defaultMockSyncItem
);
await deleteSynchronizationItem(id);
const items = await getSynchronizationItems(mockSyncItemType);
expect(items).toHaveLength(0);
});

it("does not throw when no item with given ID exists", async () => {
await deleteSynchronizationItem(404);
});
});
30 changes: 26 additions & 4 deletions packages/framework/esm-offline/src/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,33 +300,55 @@ export async function queueSynchronizationItem<T>(
}

/**
* Returns all currently queued up sync items of a given user.
* Returns the content of all currently queued up sync items of a given user.
* @param userId The ID of the user whose synchronization items should be returned.
* @param type The identifying type of the synchronization items to be returned..
*/
export async function getSynchronizationItemsFor<T>(
userId: string,
type: string
) {
const fullItems = await getFullSynchronizationItemsFor<T>(userId, type);
return fullItems.map((item) => item.content);
}

/**
* Returns all currently queued up sync items of a given user.
* @param userId The ID of the user whose synchronization items should be returned.
* @param type The identifying type of the synchronization items to be returned..
*/
export async function getFullSynchronizationItemsFor<T>(
userId: string,
type: string
) {
const table = db.syncQueue;
const items: Array<T> = [];
const items: Array<SyncItem<T>> = [];

await table.where({ type, userId }).each((item) => {
items.push(item.content);
items.push(item);
});

return items;
}

/**
* Returns all currently queued up sync items of the currently signed in user.
* Returns the content of all currently queued up sync items of the currently signed in user.
* @param type The identifying type of the synchronization items to be returned.
*/
export async function getSynchronizationItems<T>(type: string) {
const userId = await getUserId();
return await getSynchronizationItemsFor<T>(userId, type);
}

/**
* Returns all currently queued up sync items of the currently signed in user.
* @param type The identifying type of the synchronization items to be returned.
*/
export async function getFullSynchronizationItems<T>(type: string) {
const userId = await getUserId();
return await getFullSynchronizationItemsFor<T>(userId, type);
}

/**
* Returns a queued sync item with the given ID or `undefined` if no such item exists.
* @param id The ID of the requested sync item.
Expand Down

0 comments on commit c66b9f1

Please sign in to comment.