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

fix: Infinite loading on admin approved list #2674

Merged
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 3 additions & 4 deletions src/components/Proposal/ProposalActions.jsx
Expand Up @@ -88,11 +88,10 @@ const PublicActions = ({
const isUnderDiscussion = isUnderDiscussionProposal(proposal, voteSummary);
const isApproved = isApprovedProposal(proposal, voteSummary);
const { numbillingstatuschanges } = billingStatusChangeMetadata || {};
const needsBillingStatus =
!isLegacy && !isRfp && isApproved && currentUser?.isadmin;
const isSetBillingStatusAllowed =
!isLegacy &&
!isRfp &&
isApproved &&
numbillingstatuschanges < billingstatuschangesmax;
needsBillingStatus && numbillingstatuschanges < billingstatuschangesmax;
Comment on lines +91 to +94
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


const withProposal = (fn, cb) => () => {
fn(proposal, cb);
Expand Down
9 changes: 6 additions & 3 deletions src/containers/Proposal/Vetted/Vetted.jsx
Expand Up @@ -16,9 +16,12 @@ import RecordsView from "src/components/RecordsView";
import { LIST_HEADER_VETTED, INELIGIBLE } from "src/constants";
import usePolicy from "src/hooks/api/usePolicy";

const renderProposal = (record) => (
<Proposal key={record.censorshiprecord.token} proposal={record} />
);
const renderProposal = (record) =>
record?.forceLoad ? (
<ProposalLoader />
) : (
<Proposal key={record.censorshiprecord.token} proposal={record} />
);

const tabLabels = [
tabValues.UNDER_REVIEW,
Expand Down
5 changes: 2 additions & 3 deletions src/containers/Proposal/helpers.js
Expand Up @@ -372,9 +372,8 @@ export const getRfpLinkedProposals = (
return set([shortProposalToken, "proposedFor"], linkedProposal.name)(acc);
}
if (isRfp) {
const linkedFrom = proposal.linkedfrom.map((val) =>
shortRecordToken(val)
);
const linkedFrom =
proposal.linkedfrom?.map((val) => shortRecordToken(val)) || [];
const rfpSubmissions = linkedFrom && {
proposals: values(pick(proposalsByToken, linkedFrom)),
voteSummaries: pick(voteSummaries, linkedFrom),
Expand Down
77 changes: 54 additions & 23 deletions src/hooks/api/useProposalsBatch.js
Expand Up @@ -81,6 +81,33 @@ const proposalWithCacheVotetatus = (proposals) => {
}, {});
};

const tagProposalsToForceReload = (tokensToLoad, proposalsByToken) => {
tokensToLoad &&
tokensToLoad.forEach((token) => {
if (proposalsByToken[token]) {
proposalsByToken[token].forceLoad = true;
}
});
return proposalsByToken;
};

function getProposals(
proposals,
voteSummaries,
proposalSummaries,
missingBillingStatusChangesTokens
) {
const linkedProposals = getRfpLinkedProposals(
proposalWithCacheVotetatus(proposals),
voteSummaries,
proposalSummaries
);
return tagProposalsToForceReload(
missingBillingStatusChangesTokens,
linkedProposals
);
}

export default function useProposalsBatch({
fetchRfpLinks,
fetchVoteSummary = false,
Expand Down Expand Up @@ -127,6 +154,9 @@ export default function useProposalsBatch({
() => allByStatus[status] || [],
[allByStatus, status]
);
const tokensFetched = useMemo(() => {
return proposals ? tokens.filter((t) => !!proposals[t]) : [];
}, [proposals, tokens]);
const page = useMemo(() => {
return tokens ? Math.floor(+tokens.length / inventoryPageSize) : 0;
}, [tokens, inventoryPageSize]);
Expand All @@ -144,10 +174,11 @@ export default function useProposalsBatch({

const missingBillingStatusChangesTokens = useMemo(() => {
if (!isAdmin || !isStatusApproved) return [];
return tokens.filter((token) =>
const missingTokens = tokensFetched.filter((token) =>
isEmpty(billingStatusChangesByToken[shortRecordToken(token)])
);
}, [isAdmin, isStatusApproved, tokens, billingStatusChangesByToken]);
return missingTokens;
}, [isAdmin, isStatusApproved, tokensFetched, billingStatusChangesByToken]);

const scanNextStatusTokens = (index, oldTokens) => {
const status = currentStatuses[index];
Expand Down Expand Up @@ -183,7 +214,8 @@ export default function useProposalsBatch({
const scanNextStatus =
initializedInventory &&
(!(tokens.length % inventoryPageSize === 0 && tokens.length > 0) ||
remainingTokens.length === proposalPageSize);
(remainingTokens.length === proposalPageSize &&
currentStatuses[statusIndex + 1]));
if (scanNextStatus) {
const { index, tokens } = scanNextStatusTokens(
statusIndex + 1,
Expand Down Expand Up @@ -254,14 +286,10 @@ export default function useProposalsBatch({
verify: () => {
if (!hasRemainingTokens && !missingBillingStatusChangesTokens.length)
return send(RESOLVE, { proposals });
const [tokensToFetch, next] = getTokensForProposalsPagination(
remainingTokens,
proposalPageSize
);
// If proposals are loaded but there are still missing billing status
// changes. It happens when you have a proposal list loaded and then, as
// admin, navigate to approved proposals tab.
if (missingBillingStatusChangesTokens.length && !tokensToFetch.length) {
if (missingBillingStatusChangesTokens.length) {
const [billingsToFetch] = getTokensForProposalsPagination(
missingBillingStatusChangesTokens,
proposalPageSize
Expand All @@ -271,25 +299,27 @@ export default function useProposalsBatch({
.catch((e) => send(REJECT, e));
return send(FETCH);
}
const [tokensToFetch, next] = getTokensForProposalsPagination(
remainingTokens,
proposalPageSize
);
// If fetch tokens of approved proposals and current user is admin, and
// there are no billing status changes for the tokensToFetch, fetch the
// billing status changes metadata if doesn't exist in the redux store.
let missingBatchBillingStatusChangesTokens;
let batchBillingTokensToFetch;
if (isStatusApproved && isAdmin) {
missingBatchBillingStatusChangesTokens = tokensToFetch.filter(
(token) =>
isEmpty(billingStatusChangesByToken[shortRecordToken(token)])
batchBillingTokensToFetch = tokensToFetch.filter((token) =>
isEmpty(billingStatusChangesByToken[shortRecordToken(token)])
);
}
Promise.all([
missingBatchBillingStatusChangesTokens?.length &&
onFetchBillingStatusChanges(missingBatchBillingStatusChangesTokens),
tokensToFetch &&
onFetchProposalsBatch({
tokens: tokensToFetch,
fetchVoteSummary,
fetchProposalSummary
})
batchBillingTokensToFetch?.length &&
onFetchBillingStatusChanges(batchBillingTokensToFetch),
onFetchProposalsBatch({
tokens: tokensToFetch,
fetchVoteSummary,
fetchProposalSummary
})
])
.then(([, [fetchedProposals]]) => {
if (isEmpty(fetchedProposals)) {
Expand Down Expand Up @@ -401,10 +431,11 @@ export default function useProposalsBatch({
}, [proposalPageSize, send]);

return {
proposals: getRfpLinkedProposals(
proposalWithCacheVotetatus(proposals),
proposals: getProposals(
proposals,
voteSummaries,
proposalSummaries
proposalSummaries,
missingBillingStatusChangesTokens
),
onFetchProposalsBatch,
proposalsTokens: allByStatus,
Expand Down
182 changes: 123 additions & 59 deletions teste2e/cypress/e2e/proposal/list.js
Expand Up @@ -207,39 +207,6 @@ describe("General pagination", () => {
});
});

describe("Given an empty proposals list", () => {
it("should render loading placeholders properly", () => {
cy.ticketvoteMiddleware("inventory", {});
cy.visit("/");
cy.get("[data-testid='loading-placeholders'] > div", {
timeout: 100
}).should("have.length", 5);
cy.findByTestId("help-message", { timeout: 1250 })
.should("be.visible")
.then(() => {
cy.get("[data-testid='loading-placeholders'] > div", {
timeout: 1
}).should("not.exist");
});
});
it("should switch tabs and show empty message", () => {
// Test
cy.visit("/");
cy.wait("@ticketvote.inventory");
cy.findByTestId("help-message").should("be.visible");
cy.scrollTo("bottom");
// switch to another tab
cy.findByTestId("tab-1").click();
// assert empty list
cy.assertListLengthByTestId("record-title", 0);
// back to Under Review tab
cy.findByTestId("tab-0").click();
// wait to see if no requests are done, since inventory is fully fetched
cy.wait(1000);
cy.findByTestId("help-message").should("be.visible");
});
});

describe("Given 1 under-review proposal", () => {
it("should render loading placeholders only once", () => {
cy.ticketvoteMiddleware("inventory", { amountByStatus: { started: 1 } });
Expand Down Expand Up @@ -361,31 +328,10 @@ describe("Admin proposals list", () => {
});
});

describe("Additional page content", () => {
it("should load sidebar according to screen resolution", () => {
cy.ticketvoteMiddleware("inventory");
cy.visit("/");
cy.findByTestId("sidebar").should("be.visible");
cy.viewport("iphone-6");
cy.findByTestId("sidebar").should("be.hidden");
// sidebar breakpoint
cy.viewport(1000, 500);
cy.findByTestId("sidebar").should("be.hidden");
cy.viewport(1001, 500);
cy.findByTestId("sidebar").should("be.visible");
});
});

describe("Given some previously loaded approved proposals", () => {
beforeEach(() => {
cy.ticketvoteMiddleware("inventory", {
fixedInventory: {
vetted: {
approved: ["token01", "token02", "token03", "token04", "token05"]
}
}
});
cy.ticketvoteMiddleware("summaries", { amountByStatus: { approved: 5 } });
cy.userEnvironment("noLogin");
cy.ticketvoteMiddleware("inventory", { amountByStatus: { approved: 25 } });
cy.piMiddleware("billingstatuschanges", {
amountByStatus: { 3: 5 },
billingChangesAmount: 0
Expand All @@ -394,17 +340,135 @@ describe("Given some previously loaded approved proposals", () => {
cy.intercept("/api/v1/login", (req) => {
req.reply({});
});
cy.ticketvoteMiddleware("summaries", { amountByStatus: { approved: 5 } });
});
it("should fetch the billing changes after admin login", () => {
it(
"should fetch the billing changes after admin login",
{ scrollBehavior: false },
() => {
cy.visit("/?tab=approved");
cy.wait("@ticketvote.inventory");
cy.wait("@ticketvote.summaries");
cy.wait("@records.records");
cy.findByTestId("nav-login").click();
cy.userEnvironment("admin");
cy.findByLabelText(/email/i).type("email@email.com");
cy.findByLabelText(/password/i).type("123123123");
cy.ticketvoteMiddleware("inventory", {
amountByStatus: { unauthorized: 5 }
});
cy.ticketvoteMiddleware("summaries", {
amountByStatus: { unauthorized: 5 }
});
cy.findByTestId("login-form-button").click();
cy.wait("@ticketvote.inventory");
cy.wait("@ticketvote.summaries");
cy.wait("@records.records");
cy.ticketvoteMiddleware("summaries", { amountByStatus: { approved: 5 } });
cy.findByTestId("tab-1").click();
cy.wait("@ticketvote.summaries");
cy.wait("@records.records");
cy.wait("@pi.billingstatuschanges");
cy.wait("@pi.billingstatuschanges");
cy.get("@pi.billingstatuschanges.all").should("have.length", 2);
cy.get("@ticketvote.inventory.all").should("have.length", 2);
cy.get("@records.records.all").should("have.length", 3);
cy.get("@ticketvote.summaries.all").should("have.length", 3);
}
);
it("should fetch paginated billing status after admin login", () => {
cy.visit("/?tab=approved");
// fetch 20 approved proposals
cy.wait("@ticketvote.summaries");
cy.wait("@comments.count");
cy.wait("@records.records");
cy.userEnvironment("admin");
cy.assertListLengthByTestId("record-title", 5);
cy.scrollTo("bottom");
cy.wait("@ticketvote.summaries");
cy.wait("@comments.count");
cy.wait("@records.records");
cy.assertListLengthByTestId("record-title", 10);
cy.scrollTo("bottom");
cy.wait("@records.records");
cy.wait("@ticketvote.summaries");
cy.wait("@comments.count");
cy.assertListLengthByTestId("record-title", 15);
cy.scrollTo("bottom");
cy.wait("@records.records");
cy.wait("@ticketvote.summaries");
cy.wait("@comments.count");
cy.assertListLengthByTestId("record-title", 20);
// login as admin
cy.findByTestId("nav-login").click();
cy.userEnvironment("admin");
cy.findByLabelText(/email/i).type("email@email.com");
cy.findByLabelText(/password/i).type("123123123");
cy.ticketvoteMiddleware("inventory", {
amountByStatus: { unauthorized: 5 }
});
cy.ticketvoteMiddleware("summaries", {
amountByStatus: { unauthorized: 5 }
});
// back to under review tab
cy.findByTestId("login-form-button").click();
cy.wait("@records.records");
cy.wait("@ticketvote.summaries");
cy.wait(1000);
// navigate to approved tab, now logged in as admin
cy.findByTestId("tab-1").click();
cy.wait(5000);
cy.wait("@pi.billingstatuschanges");
cy.wait("@pi.billingstatuschanges");
cy.wait("@pi.billingstatuschanges");
cy.wait("@pi.billingstatuschanges");
cy.get("@pi.billingstatuschanges.all").should("have.length", 4);
});
});

describe("Given an empty proposals list", () => {
it("should render loading placeholders properly", () => {
cy.ticketvoteMiddleware("inventory", {});
cy.visit("/");
cy.get("[data-testid='loading-placeholders'] > div", {
timeout: 100
}).should("have.length", 5);
cy.findByTestId("help-message", { timeout: 1250 })
.should("be.visible")
.then(() => {
cy.get("[data-testid='loading-placeholders'] > div", {
timeout: 1
}).should("not.exist");
});
});
it("should switch tabs and show empty message", () => {
// Test
cy.visit("/");
cy.wait("@ticketvote.inventory");
cy.findByTestId("help-message").should("be.visible");
cy.scrollTo("bottom");
// switch to another tab
cy.findByTestId("tab-1").click();
// assert empty list
cy.assertListLengthByTestId("record-title", 0);
// back to Under Review tab
cy.findByTestId("tab-0").click();
// wait to see if no requests are done, since inventory is fully fetched
cy.wait(1000);
cy.get("@pi.billingstatuschanges.all").should("have.length", 1);
cy.findByTestId("help-message").should("be.visible");
});
});

describe("Additional page content", () => {
it("should load sidebar according to screen resolution", () => {
cy.ticketvoteMiddleware("inventory");
cy.visit("/");
cy.findByTestId("sidebar").should("be.visible");
cy.viewport("iphone-6");
cy.findByTestId("sidebar").should("be.hidden");
// sidebar breakpoint
cy.viewport(1000, 500);
cy.findByTestId("sidebar").should("be.hidden");
cy.viewport(1001, 500);
cy.findByTestId("sidebar").should("be.visible");
});
});