on master help sign in
invites: simplified CSV to button now that status is shown earlier, corrected getInvites to be point specific
2ecd8f9Hunter Miller
src/components/L2/Headers/L2PointHeader.tsx+3−2
@@ -24,9 +24,10 @@ const L2PointHeader = ({
24 showMigrate = false,
24 showMigrate = false,
25}: L2PointHeaderProps) => {
25}: L2PointHeaderProps) => {
26 const { point, pointList } = useRollerStore();
26 const { point, pointList } = useRollerStore();
27− const { invites, loading: invitesLoading } = useInviteStore();
27+ const { invites, inviteJobs } = useInviteStore();
28 const { pointCursor, setPointCursor }: any = usePointCursor();
28 const { pointCursor, setPointCursor }: any = usePointCursor();
29 const numInvites = invites[point.value]?.length;
29 const numInvites = invites[point.value]?.length;
30+ const loading = inviteJobs[point.value]?.status === 'generating';
30
31
31 const { popTo, push, names }: any = useHistory();
32 const { popTo, push, names }: any = useHistory();
32
33
@@ -50,7 +51,7 @@ const L2PointHeader = ({
50 const renderInviteButton = () => {
51 const renderInviteButton = () => {
51 if (!showInvites || !point.showInvites) {
52 if (!showInvites || !point.showInvites) {
52 return null;
53 return null;
53− } else if (invitesLoading) {
54+ } else if (loading) {
54 return (
55 return (
55 <Row onClick={goToInvites} className="invites">
56 <Row onClick={goToInvites} className="invites">
56 <LoadingSpinner foreground="rgba(0,0,0,0.3)" background="white" />
57 <LoadingSpinner foreground="rgba(0,0,0,0.3)" background="white" />
src/views/Invite/CSVButton.tsxnew file+54
@@ -0,0 +1,54 @@
1+import { Row, Box, Icon, BaseButton } from '@tlon/indigo-react';
2+
3+import { useEffect, useState } from 'react';
4+import { useInvites } from './useInvites';
5+import { generateCsvName } from 'lib/utils/invite';
6+import { DEFAULT_CSV_NAME } from 'lib/constants';
7+import Point from 'lib/types/Point';
8+
9+interface CSVModalProps {
10+ point: Point;
11+ invitesUpdating: boolean;
12+}
13+
14+export const CSVButton = ({ point, invitesUpdating }: CSVModalProps) => {
15+ const { generateCsv } = useInvites();
16+ const [csv, setCsv] = useState<string | null>(null);
17+ const [error, setError] = useState('');
18+
19+ useEffect(() => {
20+ if (invitesUpdating) {
21+ return;
22+ }
23+
24+ try {
25+ const csv = generateCsv();
26+ setCsv(csv);
27+ } catch (e) {
28+ setError((e as Error).message);
29+ }
30+ }, [generateCsv, invitesUpdating]);
31+
32+ if (!csv || invitesUpdating) {
33+ return (
34+ <BaseButton display="flex" className="download-csv" disabled>
35+ <Icon icon="Download" />
36+ <Box>CSV</Box>
37+ </BaseButton>
38+ );
39+ }
40+
41+ return (
42+ <a
43+ href={`data:text/csv;charset=utf-8,${encodeURIComponent(csv)}`}
44+ download={generateCsvName(
45+ DEFAULT_CSV_NAME,
46+ point.patp?.slice(1) || 'bridge'
47+ )}>
48+ <Row className="download-csv">
49+ <Icon icon="Download" />
50+ <Box>CSV</Box>
51+ </Row>
52+ </a>
53+ );
54+};
src/views/Invite/CSVModal.tsxdeleted−70
@@ -1,70 +0,0 @@
1−import { Button } from 'indigo-react';
2−import { Row, Box, LoadingSpinner } from '@tlon/indigo-react';
3−
4−import Modal, { ModalProps } from 'components/L2/Modal';
5−import { useEffect, useState } from 'react';
6−import { useInvites, useInviteStore } from './useInvites';
7−
8−interface CSVModalProps extends Omit<ModalProps, 'children'> {
9− point: number;
10−}
11−
12−export const CSVModal = ({ show, hide, hideClose, point }: CSVModalProps) => {
13− const [loading, setLoading] = useState(false);
14− const [csvElement, setCsvElement] = useState<HTMLAnchorElement | null>();
15− const [error, setError] = useState('');
16− const { invites, generatingNum } = useInviteStore();
17− const { generateCsv } = useInvites();
18− const invitePoints = invites[point];
19− const generatingCodesText = `Generating ${generatingNum} of ${invitePoints.length} codes...`;
20−
21− useEffect(() => {
22− const generate = async () => {
23− setLoading(true);
24−
25− try {
26− const element = await generateCsv();
27− setCsvElement(element);
28− } catch (e) {
29− setError((e as Error).message);
30− }
31−
32− setLoading(false);
33− };
34−
35− generate();
36− }, [generateCsv]);
37−
38− return (
39− <Modal show={show} hide={hide} hideClose={hideClose}>
40− <Box className="download-csv-modal">
41− <Box>
42− <Box>Download CSV</Box>
43− {loading && <Box>{generatingCodesText}</Box>}
44− </Box>
45− <Row className="download-buttons">
46− {/*
47− // @ts-ignore */}
48− <Button center className="ph4 close-button" solid onClick={hide}>
49− Close
50− </Button>
51− {/*
52− // @ts-ignore */}
53− <Button
54− center
55− className="ph4 download-button"
56− disabled={loading}
57− solid
58− onClick={() => csvElement?.click()}>
59− Download CSV
60− </Button>
61− </Row>
62− </Box>
63− {loading && (
64− <Box className="csv-loading">
65− <LoadingSpinner background="#BCDCFF" foreground="#219DFF" />
66− </Box>
67− )}
68− </Modal>
69− );
70−};
src/views/Invite/Cohort.scss+24−50
@@ -20,6 +20,15 @@
20 align-items: center;
20 align-items: center;
21 cursor: pointer;
21 cursor: pointer;
22
22
23+ &:disabled {
24+ cursor: not-allowed;
25+ color: $light-gray;
26+
27+ svg * {
28+ fill: $light-gray;
29+ }
30+ }
31+
23 svg {
32 svg {
24 height: 14px;
33 height: 14px;
25 width: 20px;
34 width: 20px;
@@ -50,7 +59,7 @@
50 width: 100%;
59 width: 100%;
51 font-size: 14px;
60 font-size: 14px;
52 line-height: 24px;
61 line-height: 24px;
53−
62+
54 &.center {
63 &.center {
55 text-align: center;
64 text-align: center;
56 margin-top: 20px;
65 margin-top: 20px;
@@ -83,7 +92,8 @@
83 }
92 }
84 }
93 }
85
94
95+ .copy-invite,
86− .copy-invite, .ticket-loader {
96+ .ticket-loader {
87 background-color: $washed-gray;
97 background-color: $washed-gray;
88 border-radius: $medium-border-radius;
98 border-radius: $medium-border-radius;
89 width: 32px;
99 width: 32px;
@@ -128,7 +138,8 @@
128
138
129 &.show-invite-form {
139 &.show-invite-form {
130 .body-pane {
140 .body-pane {
131− .upper, .lower {
141+ .upper,
142+ .lower {
132 width: 100%;
143 width: 100%;
133 }
144 }
134
145
@@ -139,14 +150,15 @@
139 .points-input {
150 .points-input {
140 line-height: 28px;
151 line-height: 28px;
141 margin-bottom: 24px;
152 margin-bottom: 24px;
142−
153+
143 .input-box {
154 .input-box {
144 margin: 0 6px;
155 margin: 0 6px;
145 height: 28px;
156 height: 28px;
146 width: 40px;
157 width: 40px;
147 padding: 2px 6px;
158 padding: 2px 6px;
148 font-weight: 800;
159 font-weight: 800;
149− font-family: "Inter", -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji";
160+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, Roboto,
161+ Helvetica, Arial, sans-serif, 'Apple Color Emoji';
150 text-align: center;
162 text-align: center;
151
163
152 &.over-quota {
164 &.over-quota {
@@ -154,22 +166,22 @@
154 }
166 }
155 }
167 }
156 }
168 }
157−
169+
158 .ship-or-address {
170 .ship-or-address {
159 margin: 6px 0;
171 margin: 6px 0;
160 color: $dark-gray;
172 color: $dark-gray;
161 }
173 }
162−
174+
163 .dropdown .address {
175 .dropdown .address {
164 display: flex;
176 display: flex;
165 flex-direction: row;
177 flex-direction: row;
166 justify-content: space-between;
178 justify-content: space-between;
167 align-items: center;
179 align-items: center;
168 cursor: pointer;
180 cursor: pointer;
169−
181+
170 padding: 8px 12px 8px 16px;
182 padding: 8px 12px 8px 16px;
171 }
183 }
172−
184+
173 .next-roll {
185 .next-roll {
174 width: 100%;
186 width: 100%;
175 display: flex;
187 display: flex;
@@ -178,7 +190,7 @@
178 align-items: center;
190 align-items: center;
179 font-weight: 500;
191 font-weight: 500;
180 }
192 }
181−
193+
182 .generate-codes {
194 .generate-codes {
183 border-radius: $medium-border-radius;
195 border-radius: $medium-border-radius;
184 width: 100%;
196 width: 100%;
@@ -186,7 +198,8 @@
186 font-size: 14px;
198 font-size: 14px;
187 background-color: black;
199 background-color: black;
188 color: white;
200 color: white;
189− font-family: "Inter", -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji";
201+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, Roboto,
202+ Helvetica, Arial, sans-serif, 'Apple Color Emoji';
190 border: none;
203 border: none;
191 margin-top: 8px;
204 margin-top: 8px;
192 padding: 8px;
205 padding: 8px;
@@ -220,43 +233,4 @@
220 font-size: 14px;
233 font-size: 14px;
221 line-height: 1.4em;
234 line-height: 1.4em;
222 }
235 }
223−
224− .download-csv-modal {
225− height: 167px;
226− width: 333px;
227− display: flex;
228− flex-direction: column;
229− justify-content: space-between;
230− font-weight: 500;
231− max-width: 100%;
232−
233− .download-buttons {
234− justify-content: flex-end;
235−
236− .close-button, .download-button {
237− padding: 4px 16px;
238−
239− &:hover {
240− opacity: 0.8;
241− }
242− }
243−
244− .close-button {
245− color: black;
246− background-color: $washed-gray;
247− margin-right: 8px;
248− }
249− }
250− }
251−
252− .csv-loading {
253− position: absolute;
254− top: 24px;
255− right: 24px;
256−
257− svg {
258− width: 16px;
259− height: 16px;
260− }
261− }
262}
236}
src/views/Invite/Cohort.tsx+12−18
@@ -1,6 +1,6 @@
1import { useCallback, useState } from 'react';
1import { useCallback, useState } from 'react';
2import { Button } from 'indigo-react';
2import { Button } from 'indigo-react';
3−import { Icon, Row, Box, LoadingSpinner, Text, H3 } from '@tlon/indigo-react';
3+import { Icon, Row, Box, LoadingSpinner, H3 } from '@tlon/indigo-react';
4
4
5import { useRollerStore } from 'store/rollerStore';
5import { useRollerStore } from 'store/rollerStore';
6import { useLocalRouter } from 'lib/LocalRouter';
6import { useLocalRouter } from 'lib/LocalRouter';
@@ -20,7 +20,7 @@ import Paginator from 'components/L2/Paginator';
20
20
21import './Cohort.scss';
21import './Cohort.scss';
22import { useInviteStore } from './useInvites';
22import { useInviteStore } from './useInvites';
23−import { CSVModal } from './CSVModal';
23+import { CSVButton } from './CSVButton';
24
24
25export default function InviteCohort() {
25export default function InviteCohort() {
26 const { names, pop, push } = useLocalRouter();
26 const { names, pop, push } = useLocalRouter();
@@ -28,14 +28,14 @@ export default function InviteCohort() {
28
28
29 const { nextRoll } = useTimerStore();
29 const { nextRoll } = useTimerStore();
30
30
31− const { invites, loading } = useInviteStore();
31+ const { invites, inviteJobs } = useInviteStore();
32 const currentL2 = Boolean(point.isL2Spawn);
32 const currentL2 = Boolean(point.isL2Spawn);
33
33
34− const [showCsvModal, setShowCsvModal] = useState(false);
35 // TODO: do we need to read this error? currently unused
34 // TODO: do we need to read this error? currently unused
36 const [page, setPage] = useState(0);
35 const [page, setPage] = useState(0);
37
36
38 const invitePoints = invites[point.value];
37 const invitePoints = invites[point.value];
38+ const inviteJob = inviteJobs[point.value];
39
39
40 const invitesToDisplay = invitePoints.slice(
40 const invitesToDisplay = invitePoints.slice(
41 page * INVITES_PER_PAGE,
41 page * INVITES_PER_PAGE,
@@ -113,11 +113,13 @@ export default function InviteCohort() {
113 };
113 };
114
114
115 const header = () => {
115 const header = () => {
116− if (loading) {
116+ if (inviteJob?.status === 'generating') {
117 return (
117 return (
118 <Row alignItems="center">
118 <Row alignItems="center">
119 <LoadingSpinner dark />
119 <LoadingSpinner dark />
120+ <H3 ml={2}>
120− <H3 ml={2}>Checking and Updating Invites...</H3>
121+ Checking and Updating Invite #{inviteJob.generatingNum}...
122+ </H3>
121 </Row>
123 </Row>
122 );
124 );
123 }
125 }
@@ -149,12 +151,10 @@ export default function InviteCohort() {
149 ) : (
151 ) : (
150 <Row className="has-invites-header">
152 <Row className="has-invites-header">
151 {header()}
153 {header()}
152− <Row
154+ <CSVButton
153− className="download-csv"
155+ point={point}
154− onClick={() => setShowCsvModal(true)}>
156+ invitesUpdating={inviteJob?.status === 'generating'}
155− <Icon icon="Download" />
157+ />
156− <Box>CSV</Box>
157− </Row>
158 </Row>
158 </Row>
159 )}
159 )}
160 </HeaderPane>
160 </HeaderPane>
@@ -199,12 +199,6 @@ export default function InviteCohort() {
199 <strong>{` ${ddmmmYYYY(nextQuotaTime)}`}</strong>.
199 <strong>{` ${ddmmmYYYY(nextQuotaTime)}`}</strong>.
200 </Box>
200 </Box>
201 ) : null}
201 ) : null}
202− <CSVModal
203− point={point.value}
204− show={showCsvModal}
205− hide={() => setShowCsvModal(false)}
206− hideClose
207− />
208 </View>
202 </View>
209 );
203 );
210}
204}
src/views/Invite/GenerateInvites.tsx+2−2
@@ -27,7 +27,7 @@ export const GenerateInvites = () => {
27 const { point, nextQuotaTime } = useRollerStore();
27 const { point, nextQuotaTime } = useRollerStore();
28 const { getPendingTransactions, getAndUpdatePoint } = useRoller();
28 const { getPendingTransactions, getAndUpdatePoint } = useRoller();
29 const { nextRoll } = useTimerStore();
29 const { nextRoll } = useTimerStore();
30− const { generatingNum } = useInviteStore();
30+ const { inviteJobs } = useInviteStore();
31 const { generateInviteCodes } = useInvites();
31 const { generateInviteCodes } = useInvites();
32 const [generatingStatus, setGeneratingStatus] = useState<
32 const [generatingStatus, setGeneratingStatus] = useState<
33 InviteGeneratingStatus
33 InviteGeneratingStatus
@@ -152,7 +152,7 @@ export const GenerateInvites = () => {
152 </Window>
152 </Window>
153 <GeneratingModal
153 <GeneratingModal
154 status={generatingStatus}
154 status={generatingStatus}
155− current={generatingNum}
155+ current={inviteJobs[point.value]?.generatingNum || 0}
156 total={numInvites}
156 total={numInvites}
157 error={error}
157 error={error}
158 hide={() => {
158 hide={() => {
src/views/Invite/useInvites.ts+45−41
@@ -1,5 +1,5 @@
1import { UnspawnedPoints } from '@urbit/roller-api';
1import { UnspawnedPoints } from '@urbit/roller-api';
2−import { DEFAULT_CSV_NAME, POINT_DOMINIONS } from 'lib/constants';
2+import { POINT_DOMINIONS } from 'lib/constants';
3import { deriveNetworkSeedFromUrbitWallet } from 'lib/keys';
3import { deriveNetworkSeedFromUrbitWallet } from 'lib/keys';
4import * as need from 'lib/need';
4import * as need from 'lib/need';
5import * as azimuth from 'azimuth-js';
5import * as azimuth from 'azimuth-js';
@@ -25,7 +25,7 @@ import {
25import { useWallet } from 'store/wallet';
25import { useWallet } from 'store/wallet';
26import { usePointCache } from 'store/pointCache';
26import { usePointCache } from 'store/pointCache';
27import create from 'zustand';
27import create from 'zustand';
28−import { generateCsvLine, generateCsvName } from 'lib/utils/invite';
28+import { generateCsvLine } from 'lib/utils/invite';
29
29
30const inviteTemplate = (
30const inviteTemplate = (
31 planet: number,
31 planet: number,
@@ -45,20 +45,24 @@ export type InviteGeneratingStatus =
45 | 'finished'
45 | 'finished'
46 | 'errored';
46 | 'errored';
47
47
48+interface InviteJob {
49+ generatingNum: number;
50+ status: InviteGeneratingStatus;
51+}
52+
48interface InviteStore {
53interface InviteStore {
49 invites: Invites;
54 invites: Invites;
50− generatingStatus: InviteGeneratingStatus;
55+ inviteJobs: {
51− generatingNum: number;
56+ [point: number]: InviteJob;
52− loading: boolean;
57+ };
53 addInvite: (point: number, invite: Invite) => void;
58 addInvite: (point: number, invite: Invite) => void;
54 setInvites: (points: number, invites: Invite[]) => void;
59 setInvites: (points: number, invites: Invite[]) => void;
60+ updateJob: (point: number, updates: Partial<InviteJob>) => void;
55}
61}
56
62
57export const useInviteStore = create<InviteStore>((set, get) => ({
63export const useInviteStore = create<InviteStore>((set, get) => ({
58 invites: {},
64 invites: {},
59− generatingStatus: 'initial',
65+ inviteJobs: {},
60− generatingNum: 0,
61− loading: false,
62 addInvite: (point: number, invite: Invite) => {
66 addInvite: (point: number, invite: Invite) => {
63 set(state => {
67 set(state => {
64 const newInvites: Invites = {};
68 const newInvites: Invites = {};
@@ -76,6 +80,17 @@ export const useInviteStore = create<InviteStore>((set, get) => ({
76 newInvites[point] = invites;
80 newInvites[point] = invites;
77 return { invites: Object.assign(state.invites, newInvites) };
81 return { invites: Object.assign(state.invites, newInvites) };
78 }),
82 }),
83+ updateJob: (point: number, updates: Partial<InviteJob>) => {
84+ debugger;
85+ const jobs = get().inviteJobs;
86+ const job = jobs[point] || { status: 'initial', generatingNum: 0 };
87+ const newJob = Object.assign({}, job, updates);
88+ jobs[point] = newJob;
89+
90+ //console.log({ point, oldJob: job, updates, newJob });
91+
92+ set({ inviteJobs: jobs });
93+ },
79}));
94}));
80
95
81export function useInvites() {
96export function useInvites() {
@@ -87,8 +102,13 @@ export function useInvites() {
87 const getDetails = allPoints?.getDetails;
102 const getDetails = allPoints?.getDetails;
88 const { api, ls } = useRoller();
103 const { api, ls } = useRoller();
89 const { point, pointList, setPendingTransactions } = useRollerStore();
104 const { point, pointList, setPendingTransactions } = useRollerStore();
105+ const {
106+ invites,
107+ inviteJobs,
108+ addInvite,
109+ setInvites,
110+ updateJob,
90− const { addInvite, setInvites } = useInviteStore();
111+ } = useInviteStore();
91− const updatingInvites = useRef<number[]>([]);
92
112
93 const generateInviteInfo = async (planet: number, _authToken: string) => {
113 const generateInviteInfo = async (planet: number, _authToken: string) => {
94 const { ticket, inviteWallet } = await generateInviteWallet(
114 const { ticket, inviteWallet } = await generateInviteWallet(
@@ -100,13 +120,14 @@ export function useInvites() {
100 };
120 };
101
121
102 const getInvites = useCallback(async () => {
122 const getInvites = useCallback(async () => {
103− if (updatingInvites.current.includes(point.value)) {
123+ const inviteJob = inviteJobs[point.value];
124+ if (inviteJob?.status === 'generating') {
104 return;
125 return;
105 }
126 }
106
127
107− updatingInvites.current.push(point.value);
108 try {
128 try {
109− useInviteStore.setState({ loading: true });
129+ updateJob(point.value, { status: 'generating' });
130+
110 const curPoint: number = Number(need.point(pointCursor));
131 const curPoint: number = Number(need.point(pointCursor));
111 const _authToken = authToken.getOrElse(null);
132 const _authToken = authToken.getOrElse(null);
112 const _contracts = contracts.getOrElse(null);
133 const _contracts = contracts.getOrElse(null);
@@ -145,7 +166,7 @@ export function useInvites() {
145 const storedInvites = getStoredInvites(ls);
166 const storedInvites = getStoredInvites(ls);
146
167
147 for (let i = 0; i < invitePlanets.length; i++) {
168 for (let i = 0; i < invitePlanets.length; i++) {
148− useInviteStore.setState({ generatingNum: i + 1 });
169+ updateJob(point.value, { generatingNum: i + 1 });
149 const planet = invitePlanets[i];
170 const planet = invitePlanets[i];
150 const storedInvite = storedInvites[planet];
171 const storedInvite = storedInvites[planet];
151 const invite =
172 const invite =
@@ -169,16 +190,13 @@ export function useInvites() {
169 }
190 }
170 }
191 }
171
192
172− useInviteStore.setState({ loading: false, generatingNum: 0 });
193+ updateJob(point.value, { status: 'finished', generatingNum: 0 });
173 return newInvites;
194 return newInvites;
174 }
195 }
175 } catch (error) {
196 } catch (error) {
176 console.warn('ERROR GETTING INVITES', error);
197 console.warn('ERROR GETTING INVITES', error);
177− useInviteStore.setState({ loading: false, generatingNum: 0 });
198+ updateJob(point.value, { status: 'errored', generatingNum: 0 });
178 }
199 }
179−
180− const pointIndex = updatingInvites.current.indexOf(point.value);
181− updatingInvites.current.splice(pointIndex, 1);
182 }, [
200 }, [
183 api,
201 api,
184 authToken,
202 authToken,
@@ -190,6 +208,7 @@ export function useInvites() {
190 addInvite,
208 addInvite,
191 setInvites,
209 setInvites,
192 setPendingTransactions,
210 setPendingTransactions,
211+ updateJob,
193 ]);
212 ]);
194
213
195 const generateInviteCodes = useCallback(
214 const generateInviteCodes = useCallback(
@@ -233,7 +252,7 @@ export function useInvites() {
233 });
252 });
234
253
235 for (let i = 0; i < numInvites && planets[i]; i++) {
254 for (let i = 0; i < numInvites && planets[i]; i++) {
236− useInviteStore.setState({ generatingNum: i + 1 });
255+ updateJob(point.value, { generatingNum: i + 1 });
237 const planet = planets[i];
256 const planet = planets[i];
238 const nonceInc = i + nonce;
257 const nonceInc = i + nonce;
239
258
@@ -327,38 +346,23 @@ export function useInvites() {
327 getDetails,
346 getDetails,
328 ls,
347 ls,
329 getInvites,
348 getInvites,
349+ updateJob,
330 ]
350 ]
331 );
351 );
332
352
333− const generateCsv = useCallback(async () => {
353+ const generateCsv = useCallback(() => {
334− const invites = await getInvites();
354+ const pointInvites = invites[point.value];
335
355
336− if (!invites) {
356+ if (!pointInvites) {
337 throw new Error('There was an error creating the CSV');
357 throw new Error('There was an error creating the CSV');
338 }
358 }
339
359
340− const csv = invites.reduce(
360+ return pointInvites.reduce(
341 (csvData, { ticket, planet }, ind) =>
361 (csvData, { ticket, planet }, ind) =>
342 (csvData += generateCsvLine(ind, ticket, planet)),
362 (csvData += generateCsvLine(ind, ticket, planet)),
343 'Number,Planet,Invite URL,Point,Ticket\n'
363 'Number,Planet,Invite URL,Point,Ticket\n'
344 );
364 );
345−
365+ }, [invites, point]);
346− document.getElementById('csv')?.remove();
347− const hiddenElement = document.createElement('a');
348− hiddenElement.href = `data:text/csv;charset=utf-8,${encodeURIComponent(
349− csv
350− )}`;
351− hiddenElement.target = '_blank';
352− hiddenElement.id = 'csv';
353−
354− //provide the name for the CSV file to be downloaded
355− hiddenElement.download = generateCsvName(
356− DEFAULT_CSV_NAME,
357− point.patp?.slice(1) || 'bridge'
358− );
359−
360− return hiddenElement;
361− }, [getInvites, point]);
362
366
363 return {
367 return {
364 getInvites,
368 getInvites,