Skip to content

Commit 35dd11b

Browse files
committed
Consolidate Vue components
1 parent 3474351 commit 35dd11b

17 files changed

Lines changed: 968 additions & 479 deletions

astra_app/core/templates/core/account_invitations_vue.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ <h1 class="m-0">Account Invitations</h1>
3737
data-account-invitations-bulk-api-url="{% url 'api-account-invitations-bulk' %}"
3838
data-account-invitations-list-page-url="{% url 'account-invitations' %}"
3939
data-account-invitations-upload-page-url="{% url 'account-invitations-upload' %}"
40-
data-account-invitations-page-size="50"
40+
data-account-invitations-page-size="25"
4141
data-account-invitations-can-manage="{% if can_manage_invitations %}true{% else %}false{% endif %}"
4242
data-account-invitations-can-refresh="{% if can_manage_invitations %}true{% else %}false{% endif %}"
4343
data-account-invitations-can-resend="{% if can_manage_invitations %}true{% else %}false{% endif %}"

astra_app/core/templates/core/membership_requests.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ <h1 class="m-0">Membership Requests</h1>
2121
data-membership-requests-clear-filter-url="{{ clear_filter_url }}"
2222
data-membership-requests-pending-api-url="{% url 'api-membership-requests-pending' %}"
2323
data-membership-requests-on-hold-api-url="{% url 'api-membership-requests-on-hold' %}"
24+
data-membership-requests-pending-page-size="25"
25+
data-membership-requests-on-hold-page-size="10"
2426
data-membership-requests-bulk-url="{{ membership_requests_bulk_url }}"
2527
data-membership-request-id-sentinel="{{ membership_request_id_sentinel }}"
2628
data-membership-request-detail-template="{{ membership_request_detail_template }}"

frontend/src/account-invitations/AccountInvitationsPage.vue

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
:total-pages="acceptedTable.totalPages.value"
1010
:is-loading="acceptedTable.isLoading.value"
1111
:error="acceptedTable.error.value"
12-
title="Accepted Invitations"
12+
:build-page-href="acceptedPageHref"
1313
scope="accepted"
1414
@page-change="onAcceptedPageChange"
1515
@bulk-success="onBulkSuccess('accepted')"
@@ -27,7 +27,7 @@
2727
:total-pages="pendingTable.totalPages.value"
2828
:is-loading="pendingTable.isLoading.value"
2929
:error="pendingTable.error.value"
30-
title="Pending Invitations"
30+
:build-page-href="pendingPageHref"
3131
scope="pending"
3232
@page-change="onPendingPageChange"
3333
@bulk-success="onBulkSuccess('pending')"
@@ -67,13 +67,22 @@ const isRefreshing = ref(false);
6767
const showPending = computed(() => props.bootstrap.canManageInvitations);
6868
const showAccepted = computed(() => props.bootstrap.canManageInvitations);
6969
70+
/**
71+
* Read pagination page parameter from URL.
72+
*/
73+
function readPageParam(name: string): number {
74+
const params = new URLSearchParams(window.location.search);
75+
const rawValue = Number.parseInt(params.get(name) || "1", 10);
76+
return Number.isNaN(rawValue) || rawValue < 1 ? 1 : rawValue;
77+
}
78+
7079
/**
7180
* Load both tables on mount.
7281
*/
7382
onMounted(async () => {
7483
await Promise.all([
75-
pendingTable.load(1),
76-
acceptedTable.load(1),
84+
pendingTable.load(readPageParam("pending_page")),
85+
acceptedTable.load(readPageParam("accepted_page")),
7786
]);
7887
});
7988
@@ -93,6 +102,34 @@ async function onAcceptedPageChange(page: number): Promise<void> {
93102
syncUrl();
94103
}
95104
105+
/**
106+
* Build pagination URL for pending invitations.
107+
*/
108+
function pendingPageHref(pageNumber: number): string {
109+
const params = new URLSearchParams(window.location.search);
110+
if (pageNumber <= 1) {
111+
params.delete("pending_page");
112+
} else {
113+
params.set("pending_page", String(pageNumber));
114+
}
115+
const query = params.toString();
116+
return `${window.location.pathname}${query ? `?${query}` : ""}`;
117+
}
118+
119+
/**
120+
* Build pagination URL for accepted invitations.
121+
*/
122+
function acceptedPageHref(pageNumber: number): string {
123+
const params = new URLSearchParams(window.location.search);
124+
if (pageNumber <= 1) {
125+
params.delete("accepted_page");
126+
} else {
127+
params.set("accepted_page", String(pageNumber));
128+
}
129+
const query = params.toString();
130+
return `${window.location.pathname}${query ? `?${query}` : ""}`;
131+
}
132+
96133
/**
97134
* Handle refresh button click.
98135
*/
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { mount } from "@vue/test-utils";
2+
import { describe, expect, it, vi } from "vitest";
3+
4+
import InvitationsTable from "../components/InvitationsTable.vue";
5+
import type { AccountInvitationRow, AccountInvitationsBootstrap } from "../types";
6+
7+
const bootstrap: AccountInvitationsBootstrap = {
8+
pendingApiUrl: "/api/v1/account/invitations/pending",
9+
acceptedApiUrl: "/api/v1/account/invitations/accepted",
10+
refreshApiUrl: "/api/v1/account/invitations/refresh",
11+
resendApiUrl: "/api/v1/account/invitations/123456789/resend",
12+
dismissApiUrl: "/api/v1/account/invitations/123456789/dismiss",
13+
bulkApiUrl: "/api/v1/account/invitations/bulk",
14+
listPageUrl: "/account/invitations/",
15+
pageSize: 50,
16+
canManageInvitations: true,
17+
canRefresh: true,
18+
canResend: true,
19+
canDismiss: true,
20+
canBulkAction: true,
21+
sentinelToken: "123456789",
22+
csrfToken: "csrf-token",
23+
};
24+
25+
const row: AccountInvitationRow = {
26+
invitation_id: 10,
27+
email: "alice@example.com",
28+
full_name: "Alice",
29+
note: "",
30+
invited_by_username: "bob",
31+
invited_at: "2026-04-20T10:00:00",
32+
send_count: 1,
33+
last_sent_at: "2026-04-20T10:00:00",
34+
status: "pending",
35+
organization_id: 33,
36+
organization_name: "Example Org",
37+
};
38+
39+
describe("InvitationsTable", () => {
40+
it("renders pending invitations with tbody-based loading/error states", async () => {
41+
const wrapper = mount(InvitationsTable, {
42+
props: {
43+
bootstrap,
44+
rows: [],
45+
count: 0,
46+
currentPage: 1,
47+
totalPages: 1,
48+
isLoading: true,
49+
error: null,
50+
scope: "pending",
51+
buildPageHref: (page: number) => `?page=${page}`,
52+
},
53+
});
54+
55+
expect(wrapper.find("tbody td").text()).toContain("Loading pending invitations...");
56+
57+
await wrapper.setProps({ isLoading: false, error: "Failed to load invitations." });
58+
expect(wrapper.find("tbody td").text()).toContain("Failed to load invitations.");
59+
expect(wrapper.find(".alert.alert-danger").exists()).toBe(false);
60+
});
61+
62+
it("uses inline errors instead of alert dialogs for bulk-action validation", async () => {
63+
const alertSpy = vi.spyOn(window, "alert").mockImplementation(() => undefined);
64+
65+
const wrapper = mount(InvitationsTable, {
66+
props: {
67+
bootstrap,
68+
rows: [row],
69+
count: 1,
70+
currentPage: 1,
71+
totalPages: 1,
72+
isLoading: false,
73+
error: null,
74+
scope: "pending",
75+
buildPageHref: (page: number) => `?page=${page}`,
76+
},
77+
});
78+
79+
await wrapper.find("form").trigger("submit");
80+
81+
expect(alertSpy).not.toHaveBeenCalled();
82+
expect(wrapper.text()).toContain("Please select an action and at least one invitation.");
83+
84+
alertSpy.mockRestore();
85+
});
86+
});

0 commit comments

Comments
 (0)