@netlify/spark-ui 1.30.0 → 1.31.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/chunks/browser.Ca2rTNTu.js +769 -0
- package/dist/components/articleCopyActions.d.ts +9 -0
- package/dist/components/articleCopyActions.test.d.ts +1 -0
- package/dist/components/index.d.ts +2 -2
- package/dist/components/preact/DropZone/DropZone.d.ts +76 -14
- package/dist/components/preact/DropZone/DropZone.js +111 -84
- package/dist/components/preact/DropZone/drop-core/authedDropClient.d.ts +80 -0
- package/dist/components/preact/DropZone/drop-core/authedDropClient.js +99 -0
- package/dist/components/preact/DropZone/drop-core/buildClient.d.ts +98 -0
- package/dist/components/preact/DropZone/drop-core/buildClient.js +92 -0
- package/dist/components/preact/DropZone/drop-core/client.d.ts +4 -0
- package/dist/components/preact/DropZone/drop-core/client.js +2 -1
- package/dist/components/preact/DropZone/drop-core/deploy.d.ts +31 -0
- package/dist/components/preact/DropZone/drop-core/deploy.js +36 -18
- package/dist/components/preact/DropZone/drop-core/detectBuild.d.ts +26 -0
- package/dist/components/preact/DropZone/drop-core/detectBuild.js +80 -53
- package/dist/components/preact/DropZone/drop-core/errors.d.ts +38 -0
- package/dist/components/preact/DropZone/drop-core/errors.js +50 -5
- package/dist/components/preact/DropZone/drop-core/index.d.ts +3 -0
- package/dist/components/preact/DropZone/drop-core/index.js +33 -18
- package/dist/components/preact/DropZone/drop-core/readFiles.js +1 -1
- package/dist/components/preact/DropZone/drop-core/types.d.ts +6 -1
- package/dist/components/preact/DropZone/drop-core/zip.d.ts +46 -0
- package/dist/components/preact/DropZone/drop-core/zip.js +43 -0
- package/dist/components/preact/DropZone/index.d.ts +3 -2
- package/dist/components/preact/DropZone/index.js +27 -11
- package/dist/components/preact/DropZone/useDropDeploy.d.ts +35 -4
- package/dist/components/preact/DropZone/useDropDeploy.js +139 -70
- package/dist/components/preact/index.d.ts +2 -1
- package/dist/components/preact/index.js +17 -12
- package/package.json +5 -2
- package/packages/components/ArticleCopyActions.astro +122 -0
- package/packages/components/articleCopyActions.test.ts +143 -0
- package/packages/components/articleCopyActions.ts +41 -0
- package/packages/components/index.ts +15 -0
- package/packages/components/preact/DropZone/DropZone.tsx +123 -20
- package/packages/components/preact/DropZone/drop-core/authedDropClient.ts +194 -0
- package/packages/components/preact/DropZone/drop-core/buildClient.ts +190 -0
- package/packages/components/preact/DropZone/drop-core/client.ts +1 -1
- package/packages/components/preact/DropZone/drop-core/deploy.ts +65 -0
- package/packages/components/preact/DropZone/drop-core/detectBuild.ts +125 -41
- package/packages/components/preact/DropZone/drop-core/errors.ts +84 -1
- package/packages/components/preact/DropZone/drop-core/index.ts +3 -0
- package/packages/components/preact/DropZone/drop-core/readFiles.ts +1 -1
- package/packages/components/preact/DropZone/drop-core/types.ts +12 -1
- package/packages/components/preact/DropZone/drop-core/zip.ts +118 -0
- package/packages/components/preact/DropZone/index.tsx +23 -0
- package/packages/components/preact/DropZone/useDropDeploy.ts +195 -30
- package/packages/components/preact/index.ts +2 -1
- package/dist/chunks/browser.D58rdxmc.js +0 -444
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
url: string;
|
|
4
|
+
content: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
class?: string;
|
|
7
|
+
copyUrlLabel?: string;
|
|
8
|
+
copyContentLabel?: string;
|
|
9
|
+
copiedLabel?: string;
|
|
10
|
+
trackingProps?: Record<string, string>;
|
|
11
|
+
trackingSelectorClass?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const {
|
|
15
|
+
url,
|
|
16
|
+
content,
|
|
17
|
+
title,
|
|
18
|
+
class: className,
|
|
19
|
+
copyUrlLabel = 'Copy URL',
|
|
20
|
+
copyContentLabel = 'Copy page',
|
|
21
|
+
copiedLabel = 'Copied!',
|
|
22
|
+
trackingProps = {},
|
|
23
|
+
trackingSelectorClass,
|
|
24
|
+
} = Astro.props;
|
|
25
|
+
|
|
26
|
+
const copyContent = title ? `# ${title}\n\n${content}` : content;
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
<div class:list={['copy-actions', className]} data-copied-label={copiedLabel}>
|
|
30
|
+
<button
|
|
31
|
+
type="button"
|
|
32
|
+
class:list={['copy-actions-action', 'copy-actions-copy-url', trackingSelectorClass]}
|
|
33
|
+
data-copy="url"
|
|
34
|
+
data-value={url}
|
|
35
|
+
data-track-action="copy_url"
|
|
36
|
+
{...trackingProps}
|
|
37
|
+
>
|
|
38
|
+
<svg
|
|
39
|
+
class="copy-actions-icon"
|
|
40
|
+
viewBox="0 0 16 16"
|
|
41
|
+
fill="none"
|
|
42
|
+
stroke="currentColor"
|
|
43
|
+
stroke-width="1.3"
|
|
44
|
+
stroke-linecap="round"
|
|
45
|
+
stroke-linejoin="round"
|
|
46
|
+
aria-hidden="true"
|
|
47
|
+
role="presentation"
|
|
48
|
+
>
|
|
49
|
+
<path d="M6.5 9.5a2.5 2.5 0 0 0 3.6.1l2-2a2.5 2.5 0 0 0-3.6-3.6l-1.1 1.1"></path>
|
|
50
|
+
<path d="M9.5 6.5a2.5 2.5 0 0 0-3.6-.1l-2 2a2.5 2.5 0 0 0 3.6 3.6l1.1-1.1"></path>
|
|
51
|
+
</svg>
|
|
52
|
+
<span class="copy-actions-label">{copyUrlLabel}</span>
|
|
53
|
+
</button>
|
|
54
|
+
<button
|
|
55
|
+
type="button"
|
|
56
|
+
class:list={['copy-actions-action', 'copy-actions-copy-content', trackingSelectorClass]}
|
|
57
|
+
data-copy="page"
|
|
58
|
+
data-track-action="copy_page"
|
|
59
|
+
{...trackingProps}
|
|
60
|
+
>
|
|
61
|
+
<svg
|
|
62
|
+
class="copy-actions-icon"
|
|
63
|
+
viewBox="0 0 16 16"
|
|
64
|
+
fill="currentColor"
|
|
65
|
+
aria-hidden="true"
|
|
66
|
+
role="presentation"
|
|
67
|
+
>
|
|
68
|
+
<path
|
|
69
|
+
d="M4.83333 1C3.82214 1 3 1.78477 3 2.75V13.25C3 14.2152 3.82214 15 4.83333 15H12.1667C13.1779 15 14 14.2152 14 13.25V5.375H10.3333C9.8263 5.375 9.41667 4.98398 9.41667 4.5V1H4.83333ZM10.3333 1V4.5H14L10.3333 1ZM6.20833 8H10.7917C11.0438 8 11.25 8.19687 11.25 8.4375C11.25 8.67813 11.0438 8.875 10.7917 8.875H6.20833C5.95625 8.875 5.75 8.67813 5.75 8.4375C5.75 8.19687 5.95625 8 6.20833 8ZM6.20833 9.75H10.7917C11.0438 9.75 11.25 9.94687 11.25 10.1875C11.25 10.4281 11.0438 10.625 10.7917 10.625H6.20833C5.95625 10.625 5.75 10.4281 5.75 10.1875C5.75 9.94687 5.95625 9.75 6.20833 9.75ZM6.20833 11.5H10.7917C11.0438 11.5 11.25 11.6969 11.25 11.9375C11.25 12.1781 11.0438 12.375 10.7917 12.375H6.20833C5.95625 12.375 5.75 12.1781 5.75 11.9375C5.75 11.6969 5.95625 11.5 6.20833 11.5Z"
|
|
70
|
+
></path>
|
|
71
|
+
</svg>
|
|
72
|
+
<span class="copy-actions-label">{copyContentLabel}</span>
|
|
73
|
+
</button>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<script is:inline type="text/plain" data-copy-actions-source set:text={copyContent} />
|
|
77
|
+
|
|
78
|
+
<style>
|
|
79
|
+
.copy-actions {
|
|
80
|
+
display: flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
gap: var(--ntl-space-2xs);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.copy-actions-action {
|
|
86
|
+
display: flex;
|
|
87
|
+
align-items: center;
|
|
88
|
+
gap: var(--ntl-space-3xs);
|
|
89
|
+
background: none;
|
|
90
|
+
border: none;
|
|
91
|
+
padding: 0;
|
|
92
|
+
font: inherit;
|
|
93
|
+
color: var(--ntl-color-text-weak);
|
|
94
|
+
cursor: pointer;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.copy-actions-label {
|
|
98
|
+
font-size: var(--ntl-font-size-text-sm);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.copy-actions-icon {
|
|
102
|
+
width: var(--ntl-space-s);
|
|
103
|
+
height: var(--ntl-space-s);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.copy-actions-copy-url::after {
|
|
107
|
+
content: '•';
|
|
108
|
+
margin-inline-start: var(--ntl-space-2xs);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.copy-actions-action:hover {
|
|
112
|
+
color: var(--ntl-color-text);
|
|
113
|
+
}
|
|
114
|
+
</style>
|
|
115
|
+
|
|
116
|
+
<script>
|
|
117
|
+
import { initArticleCopyActions } from './articleCopyActions';
|
|
118
|
+
|
|
119
|
+
// `astro:page-load` fires on the initial load *and* after every client-side
|
|
120
|
+
// navigation (`<ClientRouter />`).
|
|
121
|
+
document.addEventListener('astro:page-load', () => initArticleCopyActions());
|
|
122
|
+
</script>
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
3
|
+
import { initArticleCopyActions } from './articleCopyActions';
|
|
4
|
+
|
|
5
|
+
const ARTICLE_URL = 'https://www.netlify.com/blog/some-post/';
|
|
6
|
+
const MARKDOWN = '# Some post\n\nBody content.';
|
|
7
|
+
|
|
8
|
+
function renderCopyActions({ url = ARTICLE_URL, markdown = MARKDOWN, copiedLabel = 'Copied!' } = {}) {
|
|
9
|
+
document.body.innerHTML = `
|
|
10
|
+
<div class="copy-actions" data-copied-label="${copiedLabel}">
|
|
11
|
+
<button
|
|
12
|
+
type="button"
|
|
13
|
+
class="copy-actions-action copy-actions-copy-url"
|
|
14
|
+
data-copy="url"
|
|
15
|
+
data-value="${url}"
|
|
16
|
+
>
|
|
17
|
+
<span>Copy URL</span>
|
|
18
|
+
</button>
|
|
19
|
+
<button
|
|
20
|
+
type="button"
|
|
21
|
+
class="copy-actions-action copy-actions-copy-content"
|
|
22
|
+
data-copy="page"
|
|
23
|
+
>
|
|
24
|
+
<span>Copy page</span>
|
|
25
|
+
</button>
|
|
26
|
+
</div>
|
|
27
|
+
<script type="text/plain" data-copy-actions-source>${markdown}</script>
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
wrapper: document.querySelector<HTMLElement>('.copy-actions')!,
|
|
32
|
+
copyUrlBtn: document.querySelector<HTMLButtonElement>('.copy-actions-copy-url')!,
|
|
33
|
+
copyPageBtn: document.querySelector<HTMLButtonElement>('.copy-actions-copy-content')!,
|
|
34
|
+
urlLabel: document.querySelector<HTMLElement>('.copy-actions-copy-url span')!,
|
|
35
|
+
pageLabel: document.querySelector<HTMLElement>('.copy-actions-copy-content span')!,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('initArticleCopyActions', () => {
|
|
40
|
+
let writeText: ReturnType<typeof vi.fn>;
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
vi.useFakeTimers();
|
|
44
|
+
writeText = vi.fn().mockResolvedValue(undefined);
|
|
45
|
+
Object.defineProperty(navigator, 'clipboard', {
|
|
46
|
+
value: { writeText },
|
|
47
|
+
configurable: true,
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
vi.runOnlyPendingTimers();
|
|
53
|
+
vi.useRealTimers();
|
|
54
|
+
document.body.innerHTML = '';
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('copies the article URL and shows a temporary confirmation', async () => {
|
|
58
|
+
const { copyUrlBtn, urlLabel } = renderCopyActions();
|
|
59
|
+
initArticleCopyActions();
|
|
60
|
+
|
|
61
|
+
copyUrlBtn.click();
|
|
62
|
+
await vi.waitFor(() => expect(writeText).toHaveBeenCalledWith(ARTICLE_URL));
|
|
63
|
+
expect(urlLabel.textContent).toBe('Copied!');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('copies the markdown source for the "Copy page" button', async () => {
|
|
67
|
+
const { copyPageBtn, pageLabel } = renderCopyActions();
|
|
68
|
+
initArticleCopyActions();
|
|
69
|
+
|
|
70
|
+
copyPageBtn.click();
|
|
71
|
+
await vi.waitFor(() => expect(writeText).toHaveBeenCalledWith(MARKDOWN));
|
|
72
|
+
expect(pageLabel.textContent).toBe('Copied!');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('resets the label back to its original text after the delay', async () => {
|
|
76
|
+
const { copyUrlBtn, urlLabel } = renderCopyActions();
|
|
77
|
+
initArticleCopyActions();
|
|
78
|
+
|
|
79
|
+
copyUrlBtn.click();
|
|
80
|
+
await vi.waitFor(() => expect(urlLabel.textContent).toBe('Copied!'));
|
|
81
|
+
|
|
82
|
+
vi.advanceTimersByTime(1500);
|
|
83
|
+
expect(urlLabel.textContent).toBe('Copy URL');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('uses a custom copied label when data-copied-label is set', async () => {
|
|
87
|
+
const { copyUrlBtn, urlLabel } = renderCopyActions({ copiedLabel: 'Done!' });
|
|
88
|
+
initArticleCopyActions();
|
|
89
|
+
|
|
90
|
+
copyUrlBtn.click();
|
|
91
|
+
await vi.waitFor(() => expect(urlLabel.textContent).toBe('Done!'));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('does not attach duplicate listeners when initialised twice on the same DOM', async () => {
|
|
95
|
+
const { copyUrlBtn } = renderCopyActions();
|
|
96
|
+
initArticleCopyActions();
|
|
97
|
+
initArticleCopyActions(); // e.g. a spurious re-fire
|
|
98
|
+
|
|
99
|
+
copyUrlBtn.click();
|
|
100
|
+
await vi.waitFor(() => expect(writeText).toHaveBeenCalledTimes(1));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('re-binds after a client-side navigation swaps in fresh buttons', async () => {
|
|
104
|
+
// Simulate the ClientRouter bug: first page loads and works...
|
|
105
|
+
const first = renderCopyActions({ url: 'https://example.com/first/' });
|
|
106
|
+
initArticleCopyActions();
|
|
107
|
+
first.copyUrlBtn.click();
|
|
108
|
+
await vi.waitFor(() => expect(writeText).toHaveBeenCalledWith('https://example.com/first/'));
|
|
109
|
+
|
|
110
|
+
// ...then a navigation replaces the DOM with a new post's buttons.
|
|
111
|
+
const second = renderCopyActions({ url: 'https://example.com/second/' });
|
|
112
|
+
// Re-running on `astro:page-load` is what makes the new buttons work.
|
|
113
|
+
initArticleCopyActions();
|
|
114
|
+
|
|
115
|
+
second.copyUrlBtn.click();
|
|
116
|
+
await vi.waitFor(() => expect(writeText).toHaveBeenCalledWith('https://example.com/second/'));
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('scopes the markdown source lookup to each wrapper, not a global selector', async () => {
|
|
120
|
+
document.body.innerHTML = `
|
|
121
|
+
<div class="copy-actions" data-copied-label="Copied!">
|
|
122
|
+
<button type="button" class="copy-actions-action copy-actions-copy-content" data-copy="page">
|
|
123
|
+
<span>Copy page</span>
|
|
124
|
+
</button>
|
|
125
|
+
</div>
|
|
126
|
+
<script type="text/plain" data-copy-actions-source>First post markdown</script>
|
|
127
|
+
<div class="copy-actions" data-copied-label="Copied!">
|
|
128
|
+
<button type="button" class="copy-actions-action copy-actions-copy-content" data-copy="page">
|
|
129
|
+
<span>Copy page</span>
|
|
130
|
+
</button>
|
|
131
|
+
</div>
|
|
132
|
+
<script type="text/plain" data-copy-actions-source>Second post markdown</script>
|
|
133
|
+
`;
|
|
134
|
+
initArticleCopyActions();
|
|
135
|
+
|
|
136
|
+
const buttons = document.querySelectorAll<HTMLButtonElement>('.copy-actions-copy-content');
|
|
137
|
+
buttons[1].click();
|
|
138
|
+
await vi.waitFor(() => expect(writeText).toHaveBeenCalledWith('Second post markdown'));
|
|
139
|
+
|
|
140
|
+
buttons[0].click();
|
|
141
|
+
await vi.waitFor(() => expect(writeText).toHaveBeenCalledWith('First post markdown'));
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const COPIED_LABEL_FALLBACK = 'Copied!';
|
|
2
|
+
const RESET_DELAY_MS = 1500;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Wire up the "Copy URL" / "Copy page" buttons rendered by ArticleCopyActions.astro.
|
|
6
|
+
*
|
|
7
|
+
* This runs on every `astro:page-load` (initial load *and* every client-side
|
|
8
|
+
* navigation via `<ClientRouter />`), so the buttons keep working after routing
|
|
9
|
+
* to another article/post. Each wrapper is guarded so re-initialising the same
|
|
10
|
+
* DOM node never attaches duplicate listeners.
|
|
11
|
+
*/
|
|
12
|
+
export function initArticleCopyActions(root: ParentNode = document): void {
|
|
13
|
+
root.querySelectorAll<HTMLElement>('.copy-actions').forEach(wrapper => {
|
|
14
|
+
if (wrapper.dataset.copyBound === 'true') return;
|
|
15
|
+
wrapper.dataset.copyBound = 'true';
|
|
16
|
+
|
|
17
|
+
const source = wrapper.nextElementSibling as HTMLElement | null;
|
|
18
|
+
const copiedLabel = wrapper.dataset.copiedLabel ?? COPIED_LABEL_FALLBACK;
|
|
19
|
+
|
|
20
|
+
wrapper.querySelectorAll<HTMLButtonElement>('.copy-actions-action').forEach(btn => {
|
|
21
|
+
const label = btn.querySelector<HTMLElement>('span');
|
|
22
|
+
const originalText = label?.textContent;
|
|
23
|
+
|
|
24
|
+
btn.addEventListener('click', async () => {
|
|
25
|
+
const value =
|
|
26
|
+
btn.getAttribute('data-copy') === 'url'
|
|
27
|
+
? btn.getAttribute('data-value')
|
|
28
|
+
: source?.textContent;
|
|
29
|
+
|
|
30
|
+
if (!value || !label) return;
|
|
31
|
+
|
|
32
|
+
await navigator.clipboard.writeText(value);
|
|
33
|
+
|
|
34
|
+
label.textContent = copiedLabel;
|
|
35
|
+
setTimeout(() => {
|
|
36
|
+
label.textContent = originalText ?? '';
|
|
37
|
+
}, RESET_DELAY_MS);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -9,17 +9,32 @@ export { default as SearchModal } from './preact/SearchModal/components/SearchMo
|
|
|
9
9
|
export {
|
|
10
10
|
DropZone,
|
|
11
11
|
AnonymousDropClient,
|
|
12
|
+
AuthenticatedBuildClient,
|
|
13
|
+
AuthenticatedDropClient,
|
|
14
|
+
deployBuild,
|
|
12
15
|
deployFiles,
|
|
16
|
+
detectBuild,
|
|
17
|
+
inferBuildSettings,
|
|
13
18
|
readDataTransfer,
|
|
14
19
|
readFileList,
|
|
20
|
+
zipFiles,
|
|
15
21
|
} from './preact/DropZone';
|
|
16
22
|
export type {
|
|
23
|
+
AuthenticatedBuildClientConfig,
|
|
24
|
+
AuthenticatedDropClientConfig,
|
|
25
|
+
BuildClient,
|
|
26
|
+
BuildDeployResult,
|
|
27
|
+
BuildDetection,
|
|
28
|
+
BuildSite,
|
|
29
|
+
DeployedBuild,
|
|
30
|
+
DeployedSite,
|
|
17
31
|
DropClient,
|
|
18
32
|
DropResponse,
|
|
19
33
|
Digest,
|
|
20
34
|
DeployResult,
|
|
21
35
|
DeployProgress,
|
|
22
36
|
DeployPhase,
|
|
37
|
+
InferredBuildSettings,
|
|
23
38
|
ProcessedFile,
|
|
24
39
|
} from './preact/DropZone';
|
|
25
40
|
export { type IconName } from './preact/Icon';
|
|
@@ -4,11 +4,13 @@ import {
|
|
|
4
4
|
getFileTypes,
|
|
5
5
|
readDataTransfer,
|
|
6
6
|
readFileList,
|
|
7
|
+
type BuildClient,
|
|
7
8
|
type BuildDetection,
|
|
8
9
|
type DropClient,
|
|
9
10
|
} from './drop-core';
|
|
10
11
|
import {
|
|
11
12
|
useDropDeploy,
|
|
13
|
+
type DeployedBuild,
|
|
12
14
|
type DeployedSite,
|
|
13
15
|
type DeployError,
|
|
14
16
|
type DeployStatus,
|
|
@@ -22,6 +24,13 @@ const DRAG_INACTIVE_MS = 120;
|
|
|
22
24
|
|
|
23
25
|
const defaultClaimUrl = (siteName: string) => `https://app.netlify.com/drop/${siteName}`;
|
|
24
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Where an authenticated static deploy lands: the new site's project overview,
|
|
29
|
+
* the same place the app's own logged-in drop sends you.
|
|
30
|
+
*/
|
|
31
|
+
const defaultSiteDashboardUrl = (siteName: string) =>
|
|
32
|
+
`https://app.netlify.com/projects/${siteName}`;
|
|
33
|
+
|
|
25
34
|
/**
|
|
26
35
|
* Where a build-required drop is sent by default. Mirrors the app's own Drop
|
|
27
36
|
* page: signup with `next=/drop`, so after signup + email verification the user
|
|
@@ -29,6 +38,15 @@ const defaultClaimUrl = (siteName: string) => `https://app.netlify.com/drop/${si
|
|
|
29
38
|
*/
|
|
30
39
|
const defaultBuildSignupUrl = 'https://app.netlify.com/signup?next=/drop&utm_campaign=drop';
|
|
31
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Where an authenticated build drop lands: the new site's project overview —
|
|
43
|
+
* the same destination as a static deploy, and where the app's own Drop page
|
|
44
|
+
* sends build drops. The overview surfaces the in-progress deploy; consumers
|
|
45
|
+
* who want the deploy-log page instead can override with `build.deployId`.
|
|
46
|
+
*/
|
|
47
|
+
const defaultBuildDeployUrl = ({ siteName }: DeployedBuild) =>
|
|
48
|
+
`https://app.netlify.com/projects/${siteName}`;
|
|
49
|
+
|
|
32
50
|
interface Props {
|
|
33
51
|
/** Content the drop layer sits on top of. The zone is invisible until you drag over it. */
|
|
34
52
|
children?: ComponentChildren;
|
|
@@ -38,23 +56,81 @@ interface Props {
|
|
|
38
56
|
client?: DropClient;
|
|
39
57
|
/** Build the "claim this site" URL from the assigned site name (subdomain). */
|
|
40
58
|
claimUrl?: (siteName: string) => string;
|
|
59
|
+
/**
|
|
60
|
+
* Where an authenticated static deploy sends the visitor — the site already
|
|
61
|
+
* lives in their account, so there is no claim step. Defaults to the new
|
|
62
|
+
* site's project overview on app.netlify.com.
|
|
63
|
+
*/
|
|
64
|
+
siteDashboardUrl?: (siteName: string) => string;
|
|
41
65
|
/**
|
|
42
66
|
* Where to send a build-required drop (framework source, build command,
|
|
43
|
-
* functions)
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
67
|
+
* functions) that can't be built here — no `getUploadToken`/`buildClient`, or a
|
|
68
|
+
* visitor who isn't logged in. Such projects can't deploy through the anonymous
|
|
69
|
+
* static drop API, so by default the user is redirected here — the app signup
|
|
70
|
+
* page with `next=/drop` — to build on the authenticated Drop page. Override to
|
|
71
|
+
* point at your own destination.
|
|
47
72
|
*/
|
|
48
73
|
buildSignupUrl?: string;
|
|
49
74
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
75
|
+
* Mint a short-lived bearer token for direct api.netlify.com uploads — pass it
|
|
76
|
+
* to switch on the whole authenticated path for logged-in visitors: static
|
|
77
|
+
* drops deploy straight into their account (then `siteDashboardUrl`), and
|
|
78
|
+
* build-required drops build in place instead of being handed off to the app.
|
|
79
|
+
* On netlify.com this reads `GET /access-control/generate-access-control-token`.
|
|
80
|
+
* Resolving `null` means "no session": static drops fall back to the anonymous
|
|
81
|
+
* claim flow, build drops to `buildSignupUrl`.
|
|
82
|
+
* See [Logged-in visitors](#logged-in-visitors).
|
|
83
|
+
*/
|
|
84
|
+
getUploadToken?: () => Promise<string | null>;
|
|
85
|
+
/**
|
|
86
|
+
* Inject a custom `BuildClient` (tests, staging, a different account flow).
|
|
87
|
+
* Takes precedence over `getUploadToken` for the build path.
|
|
88
|
+
*/
|
|
89
|
+
buildClient?: BuildClient;
|
|
90
|
+
/**
|
|
91
|
+
* Inject a custom authenticated `DropClient` for static drops (tests, staging,
|
|
92
|
+
* a different account flow). Takes precedence over `getUploadToken` for the
|
|
93
|
+
* static path. Distinct from `client`, which is the anonymous fallback.
|
|
94
|
+
*/
|
|
95
|
+
authenticatedDropClient?: DropClient;
|
|
96
|
+
/**
|
|
97
|
+
* API root for the authenticated paths' JSON calls, which go through your
|
|
98
|
+
* access-control rewrite so the session cookie authorizes them. Defaults to
|
|
99
|
+
* `/access-control/bb-api/api/v1`. (Uploads always go direct to
|
|
100
|
+
* `api.netlify.com` — the proxy can't carry bodies that large.)
|
|
101
|
+
*/
|
|
102
|
+
proxyBase?: string;
|
|
103
|
+
/** Create the visitor's new project in this account. Omit for their default account. */
|
|
104
|
+
accountSlug?: string;
|
|
105
|
+
/**
|
|
106
|
+
* Where an enqueued build sends the visitor. Defaults to the new site's
|
|
107
|
+
* project overview, same as a static deploy; `build.deployId` is available for
|
|
108
|
+
* consumers who want the deploy-log page instead.
|
|
109
|
+
*/
|
|
110
|
+
buildDeployUrl?: (build: DeployedBuild) => string;
|
|
111
|
+
/**
|
|
112
|
+
* Poll until the build finishes before handing off, instead of redirecting as
|
|
113
|
+
* soon as it's enqueued. The trade-off: waiting means a failing build surfaces
|
|
114
|
+
* its own error message here (via `onError` and the overlay) rather than the
|
|
115
|
+
* visitor discovering it on the deploy page — at the cost of holding them on a
|
|
116
|
+
* spinner for the length of the build, which is minutes. Off by default.
|
|
117
|
+
*/
|
|
118
|
+
waitForBuild?: boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Called when a build-required drop is detected, instead of building it or
|
|
121
|
+
* redirecting. Use it to own the hand-off yourself (custom routing, persisting
|
|
122
|
+
* the drop, gating on auth). When set, the component neither builds nor
|
|
123
|
+
* redirects. The static drop path is unaffected either way.
|
|
54
124
|
*/
|
|
55
125
|
onBuildRequired?: (info: BuildDetection) => void;
|
|
56
126
|
/** Called once the deploy is live. */
|
|
57
127
|
onDeploy?: (result: DeployedSite) => void;
|
|
128
|
+
/**
|
|
129
|
+
* Called once a build-required drop has been zipped, uploaded and enqueued —
|
|
130
|
+
* before the redirect to `buildDeployUrl`. The site exists at this point but
|
|
131
|
+
* won't serve content until the build finishes.
|
|
132
|
+
*/
|
|
133
|
+
onBuildDeploy?: (build: DeployedBuild) => void;
|
|
58
134
|
/**
|
|
59
135
|
* Called when a deploy fails, with a humanized `message` (ready to show a
|
|
60
136
|
* user — the same copy the built-in overlay uses) and the raw `reason`. Pair
|
|
@@ -76,10 +152,14 @@ interface Props {
|
|
|
76
152
|
* Emitted events (all include `dropzone_id` when `analyticsId` is set):
|
|
77
153
|
* - `dropzone_files_dropped` — files dropped via drag ({ method: 'drag', file_count, file_types })
|
|
78
154
|
* - `dropzone_browse_opened` — file picker opened ({ method: 'keyboard' | 'click' })
|
|
79
|
-
* - `dropzone_build_required` — drop needs a build
|
|
80
|
-
* - `dropzone_deploy_succeeded` — deploy went live
|
|
81
|
-
* - `
|
|
155
|
+
* - `dropzone_build_required` — drop needs a build (built here, or handed off) ({ method, reason, file_count, file_types })
|
|
156
|
+
* - `dropzone_deploy_succeeded` — static deploy went live ({ method, site_name, file_count, authenticated, auth_fallback?, file_types })
|
|
157
|
+
* - `dropzone_build_deploy_succeeded` — build enqueued for a logged-in visitor ({ method, reason, site_name, file_count, framework?, file_types })
|
|
158
|
+
* - `dropzone_deploy_failed` — deploy or build errored out ({ method, reason, file_count?, file_types })
|
|
82
159
|
*
|
|
160
|
+
* `authenticated` says whether the deploy landed in the visitor's account;
|
|
161
|
+
* `auth_fallback: true` is sent only when an authenticated attempt bounced
|
|
162
|
+
* (stale auth hint) and the drop completed anonymously — omitted otherwise.
|
|
83
163
|
* `file_types` is the sorted, de-duplicated list of MIME types in the
|
|
84
164
|
* selection, with `application/octet-stream` standing in wherever the browser
|
|
85
165
|
* reports none (a dragged folder, an `.exe`). It is omitted when the browser
|
|
@@ -102,9 +182,18 @@ export default function DropZone({
|
|
|
102
182
|
apiBase,
|
|
103
183
|
client,
|
|
104
184
|
claimUrl = defaultClaimUrl,
|
|
185
|
+
siteDashboardUrl = defaultSiteDashboardUrl,
|
|
105
186
|
buildSignupUrl = defaultBuildSignupUrl,
|
|
187
|
+
getUploadToken,
|
|
188
|
+
buildClient,
|
|
189
|
+
authenticatedDropClient,
|
|
190
|
+
proxyBase,
|
|
191
|
+
accountSlug,
|
|
192
|
+
buildDeployUrl = defaultBuildDeployUrl,
|
|
193
|
+
waitForBuild,
|
|
106
194
|
onBuildRequired,
|
|
107
195
|
onDeploy,
|
|
196
|
+
onBuildDeploy,
|
|
108
197
|
onError,
|
|
109
198
|
showStatus = true,
|
|
110
199
|
onTrack,
|
|
@@ -120,9 +209,18 @@ export default function DropZone({
|
|
|
120
209
|
apiBase,
|
|
121
210
|
client,
|
|
122
211
|
claimUrl,
|
|
212
|
+
siteDashboardUrl,
|
|
123
213
|
buildSignupUrl,
|
|
214
|
+
getUploadToken,
|
|
215
|
+
buildClient,
|
|
216
|
+
authenticatedDropClient,
|
|
217
|
+
proxyBase,
|
|
218
|
+
accountSlug,
|
|
219
|
+
buildDeployUrl,
|
|
220
|
+
waitForBuild,
|
|
124
221
|
onBuildRequired,
|
|
125
222
|
onDeploy,
|
|
223
|
+
onBuildDeploy,
|
|
126
224
|
onError,
|
|
127
225
|
track,
|
|
128
226
|
});
|
|
@@ -267,20 +365,25 @@ function StatusView({
|
|
|
267
365
|
|
|
268
366
|
case 'reading':
|
|
269
367
|
return <Spinner label="Reading files…" />;
|
|
368
|
+
case 'zipping':
|
|
369
|
+
return <Spinner label="Packaging your project…" />;
|
|
270
370
|
case 'creating':
|
|
271
371
|
return <Spinner label="Creating deploy…" />;
|
|
272
372
|
case 'processing':
|
|
273
373
|
return <Spinner label="Processing — almost live…" />;
|
|
374
|
+
case 'building':
|
|
375
|
+
return <Spinner label="Building your project…" />;
|
|
274
376
|
case 'redirecting':
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
label=
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
377
|
+
switch (status.target) {
|
|
378
|
+
case 'signup':
|
|
379
|
+
return <Spinner label="This project needs a build — taking you to Netlify…" />;
|
|
380
|
+
case 'build':
|
|
381
|
+
return <Spinner label="Build started — taking you to your new project…" />;
|
|
382
|
+
case 'claim':
|
|
383
|
+
return <Spinner label="Site’s live — taking you to claim it…" />;
|
|
384
|
+
case 'dashboard':
|
|
385
|
+
return <Spinner label="Site’s live — taking you to your new project…" />;
|
|
386
|
+
}
|
|
284
387
|
|
|
285
388
|
case 'uploading': {
|
|
286
389
|
const pct = status.total ? Math.round((status.uploaded / status.total) * 100) : 0;
|