Skip to content

Commit

Permalink
Merge pull request #184 from mrc-ide/mrc-3700
Browse files Browse the repository at this point in the history
mrc-3700 Delete session from front end storage
  • Loading branch information
EmmaLRussell authored Sep 8, 2023
2 parents 9d4d572 + bd98068 commit 45f4d57
Show file tree
Hide file tree
Showing 11 changed files with 262 additions and 37 deletions.
50 changes: 50 additions & 0 deletions app/static/src/app/components/ConfirmModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<template>
<div>
<div v-if="open" class="modal-backdrop fade show"></div>
<div class="modal" :class="{show: open}" :style="modalStyle">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ title }}</h5>
</div>
<div class="modal-body">
{{ props.text }}
</div>
<div class="modal-footer">
<button class="btn btn-primary"
id="confirm-yes"
@click="confirm">Yes</button>
<button class="btn btn-outline"
id="confirm-no"
@click="close">No</button>
</div>
</div>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import { computed, defineProps, defineEmits } from "vue";
const props = defineProps({
open: Boolean,
title: String,
text: String
});
const emit = defineEmits(["close", "confirm"]);
const modalStyle = computed(() => {
return { display: props.open ? "block" : "none" };
});
const close = () => {
emit("close");
};
const confirm = () => {
emit("confirm");
close();
};
</script>
47 changes: 42 additions & 5 deletions app/static/src/app/components/sessions/SessionsPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
<template v-if="sessionsMetadata">
<template v-if="sessionsMetadata.length">
<div class="row fw-bold py-2">
<div class="col-3 session-col-header">Saved</div>
<div class="col-2 session-col-header">Saved</div>
<div class="col-2 session-col-header">Label</div>
<div class="col-2 text-center session-col-header">Edit Label</div>
<div class="col-1 text-center session-col-header">Load</div>
<div class="col-1 text-center session-col-header">Delete</div>
<div class="col-4 text-center session-col-header">Shareable Link</div>
</div>
<div class="row py-2" v-for="session in sessionsMetadata" :key="session.id">
<div class="col-3 session-col-value session-time">
<div class="col-2 session-col-value session-time">
{{formatDateTime(session.time)}}
<div v-if="isCurrentSession(session.id)" class="small text-muted">(current session)</div>
</div>
Expand All @@ -34,6 +35,12 @@
<vue-feather class="inline-icon brand" type="upload"></vue-feather>
</a>
</div>
<div class="col-1 text-center session-col-value session-delete">
<vue-feather v-if="!isCurrentSession(session.id)"
class="inline-icon brand clickable"
type="trash-2"
@click="confirmDeleteSession(session.id)"></vue-feather>
</div>
<div class="col-4 session-col-value session-share brand">
<div class="mx-auto" style="width: 16rem">
<span class="session-copy-link clickable" @click="copyLink(session)" @mouseleave="clearLastCopied">
Expand Down Expand Up @@ -66,7 +73,15 @@
:session-id="selectedSessionId"
:session-label="selectedSessionLabel"
@close="toggleEditSessionLabelOpen(false)"
@confirm="deleteSession"
></edit-session-label>
<confirm-modal id="confirm-delete-session"
:title="'Delete session'"
:text="'Do you want to delete this session?'"
:open="confirmDeleteSessionOpen"
@close="toggleConfirmDeleteSessionOpen(false)"
@confirm="deleteSession"
></confirm-modal>
</div>
</template>

Expand All @@ -83,17 +98,20 @@ import userMessages from "../../userMessages";
import ErrorsAlert from "../ErrorsAlert.vue";
import EditSessionLabel from "./EditSessionLabel.vue";
import { SessionMetadata } from "../../types/responseTypes";
import ConfirmModal from "../ConfirmModal.vue";
export default defineComponent({
name: "SessionsPage",
components: {
ErrorsAlert,
EditSessionLabel,
VueFeather,
RouterLink
RouterLink,
ConfirmModal
},
setup() {
const store = useStore();
const namespace = "sessions";
const sessionsMetadata = computed(() => store.state.sessions.sessionsMetadata);
const baseUrl = computed(() => store.state.baseUrl);
Expand All @@ -108,6 +126,8 @@ export default defineComponent({
const lastCopySessionId = ref<string | null>(null);
const lastCopyMsg = ref<string | null>(null); // Feedback message to show under last copy control clicked
const sessionIdToDelete = ref("");
const formatDateTime = (isoUTCString: string) => {
return utc(isoUTCString).local().format("DD/MM/YYYY HH:mm:ss");
};
Expand All @@ -133,7 +153,7 @@ export default defineComponent({
return session.friendlyId;
}
lastCopyMsg.value = "Fetching code...";
await store.dispatch(`sessions/${SessionsAction.GenerateFriendlyId}`, session.id);
await store.dispatch(`${namespace}/${SessionsAction.GenerateFriendlyId}`, session.id);
await nextTick();
const friendlyId = sessionsMetadata.value.find((m: SessionMetadata) => m.id === session.id)?.friendlyId;
if (!friendlyId) {
Expand Down Expand Up @@ -171,8 +191,21 @@ export default defineComponent({
lastCopyMsg.value = null;
};
const confirmDeleteSessionOpen = ref(false);
const toggleConfirmDeleteSessionOpen = (open: boolean) => {
confirmDeleteSessionOpen.value = open;
};
const confirmDeleteSession = (sessionId: string) => {
sessionIdToDelete.value = sessionId;
toggleConfirmDeleteSessionOpen(true);
};
const deleteSession = () => {
store.dispatch(`${namespace}/${SessionsAction.DeleteSession}`, sessionIdToDelete.value);
};
onMounted(() => {
store.dispatch(`sessions/${SessionsAction.GetSessions}`);
store.dispatch(`${namespace}/${SessionsAction.GetSessions}`);
});
const messages = userMessages.sessions;
Expand All @@ -187,12 +220,16 @@ export default defineComponent({
selectedSessionLabel,
lastCopySessionId,
lastCopyMsg,
confirmDeleteSessionOpen,
editSessionLabel,
toggleEditSessionLabelOpen,
copyLink,
copyCode,
getCopyMsg,
clearLastCopied,
confirmDeleteSession,
toggleConfirmDeleteSessionOpen,
deleteSession,
messages
};
}
Expand Down
12 changes: 11 additions & 1 deletion app/static/src/app/localStorageManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,20 @@ class LocalStorageManager {
return (serialised ? JSON.parse(serialised) : []) as string[];
}

saveSessionIds = (appName: string, basePath: string, sessionIds: string[]) => {
window.localStorage.setItem(LocalStorageManager._sessionIdsKey(appName, basePath), JSON.stringify(sessionIds));
}

addSessionId = (appName: string, basePath: string, sessionId: string) => {
const sessionIds = this.getSessionIds(appName, basePath);
sessionIds.unshift(sessionId); // prepends the id
window.localStorage.setItem(LocalStorageManager._sessionIdsKey(appName, basePath), JSON.stringify(sessionIds));
this.saveSessionIds(appName, basePath, sessionIds);
}

deleteSessionId = (appName: string, basePath: string, sessionId: string) => {
let sessionIds = this.getSessionIds(appName, basePath);
sessionIds = sessionIds.filter((s) => s !== sessionId);
this.saveSessionIds(appName, basePath, sessionIds);
}
}

Expand Down
10 changes: 9 additions & 1 deletion app/static/src/app/store/sessions/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export enum SessionsAction {
GetSessions = "GetSessions",
Rehydrate = "Rehydrate",
SaveSessionLabel = "SaveSessionLabel",
GenerateFriendlyId = "GenerateFriendlyId"
GenerateFriendlyId = "GenerateFriendlyId",
DeleteSession = "DeleteSession"
}

interface SaveSessionLabelPayload {
Expand Down Expand Up @@ -99,5 +100,12 @@ export const actions: ActionTree<SessionsState, AppState> = {
if (response) {
commit(SessionsMutation.SetSessionFriendlyId, { sessionId, friendlyId: response.data });
}
},

async [SessionsAction.DeleteSession](context, sessionId: string) {
const { rootState, rootGetters, commit } = context;
const { appName } = rootState;
localStorageManager.deleteSessionId(appName!, rootGetters[AppStateGetter.baseUrlPath], sessionId);
commit(SessionsMutation.RemoveSessionId, sessionId);
}
};
9 changes: 8 additions & 1 deletion app/static/src/app/store/sessions/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { SessionMetadata } from "../../types/responseTypes";

export enum SessionsMutation {
SetSessionsMetadata = "SetSessionsMetadata",
SetSessionFriendlyId = "SetSessionFriendlyId"
SetSessionFriendlyId = "SetSessionFriendlyId",
RemoveSessionId = "RemoveSessionId"
}

export interface SetSessionFriendlyIdPayload {
Expand All @@ -22,5 +23,11 @@ export const mutations: MutationTree<SessionsState> = {
if (sessionMetadata) {
sessionMetadata.friendlyId = payload.friendlyId;
}
},

[SessionsMutation.RemoveSessionId](state: SessionsState, payload: string) {
if (state.sessionsMetadata) {
state.sessionsMetadata = state.sessionsMetadata.filter((s) => s.id !== payload);
}
}
};
17 changes: 15 additions & 2 deletions app/static/tests/e2e/sessions.etest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const enterSessionLabel = async (page: Page, dialogId: string, newLabel: string)
test.describe("Sessions tests", () => {
const { timeout } = PlaywrightConfig;

test("can navigate to Sessions page from navbar, and load a session", async () => {
test("can use Sessions page", async () => {
// We need to use a browser with persistent context instead of the default incognito browser so that
// we can use the session ids in local storage
const userDataDir = os.tmpdir();
Expand Down Expand Up @@ -83,7 +83,8 @@ test.describe("Sessions tests", () => {
await expect(await page.innerText(":nth-match(.session-col-header, 2)")).toBe("Label");
await expect(await page.innerText(":nth-match(.session-col-header, 3)")).toBe("Edit Label");
await expect(await page.innerText(":nth-match(.session-col-header, 4)")).toBe("Load");
await expect(await page.innerText(":nth-match(.session-col-header, 5)")).toBe("Shareable Link");
await expect(await page.innerText(":nth-match(.session-col-header, 5)")).toBe("Delete");
await expect(await page.innerText(":nth-match(.session-col-header, 6)")).toBe("Shareable Link");

await expect(await page.innerText(".session-label")).toBe("--no label--");

Expand Down Expand Up @@ -185,6 +186,18 @@ test.describe("Sessions tests", () => {
await page.goto(copiedLinkText);
await expect(await page.innerText("#data-upload-success")).toBe(" Uploaded 32 rows and 2 columns");

// can delete session
await page.goto(`${appUrl}/sessions`);
await expect(await page.locator("#app .container .row").count()).toBeGreaterThan(3);
await page.locator(":nth-match(#app .container .row, 4) .session-edit-label i").click();
await enterSessionLabel(page, "page-edit-session-label", "delete me");

await expect(await page.locator(".row:has-text('delete me')")).toBeVisible({ timeout });
await page.locator(".row:has-text('delete me') .session-delete i").click();
await expect(await page.locator("#confirm-yes")).toBeVisible();
await page.click("#confirm-yes");
await expect(await page.locator("#app")).not.toContainText("delete me");

await browser.close();
});
});
36 changes: 36 additions & 0 deletions app/static/tests/unit/components/confirmModal.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { mount } from "@vue/test-utils";
import ConfirmModal from "../../../src/app/components/ConfirmModal.vue";

describe("ConfirmModal", () => {
const getWrapper = (open = true, title = "Delete something", text = "Really?") => {
return mount(ConfirmModal, { props: { title, text, open } });
};

it("renders as expected", () => {
const wrapper = getWrapper();
expect(wrapper.find(".modal-title").text()).toBe("Delete something");
expect(wrapper.find(".modal-body").text()).toBe("Really?");
expect(wrapper.find("button#confirm-yes").text()).toBe("Yes");
expect(wrapper.find("button#confirm-no").text()).toBe("No");
expect((wrapper.find("div.modal").element as HTMLDivElement).style.display).toBe("block");
});

it("hides modal when not open", () => {
const wrapper = getWrapper(false);
expect((wrapper.find("div.modal").element as HTMLDivElement).style.display).toBe("none");
});

it("No button emits close", async () => {
const wrapper = getWrapper();
await wrapper.find("button#confirm-no").trigger("click");
expect(wrapper.emitted("close")!.length).toBe(1);
expect(wrapper.emitted("confirm")).toBe(undefined);
});

it("Yes button emits close and confirm", async () => {
const wrapper = getWrapper();
await wrapper.find("button#confirm-yes").trigger("click");
expect(wrapper.emitted("close")!.length).toBe(1);
expect(wrapper.emitted("confirm")!.length).toBe(1);
});
});
Loading

0 comments on commit 45f4d57

Please sign in to comment.