@netlify/spark-ui 1.29.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 +84 -15
- package/dist/components/preact/DropZone/DropZone.js +117 -85
- 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/fileTypes.d.ts +17 -0
- package/dist/components/preact/DropZone/drop-core/fileTypes.js +11 -0
- package/dist/components/preact/DropZone/drop-core/index.d.ts +4 -0
- package/dist/components/preact/DropZone/drop-core/index.js +32 -15
- 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 -9
- package/dist/components/preact/DropZone/useDropDeploy.d.ts +36 -5
- package/dist/components/preact/DropZone/useDropDeploy.js +140 -65
- 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 +145 -24
- 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/fileTypes.ts +34 -0
- package/packages/components/preact/DropZone/drop-core/index.ts +4 -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 +24 -0
- package/packages/components/preact/DropZone/useDropDeploy.ts +219 -34
- 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';
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
2
2
|
import type { ComponentChildren } from 'preact';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
getFileTypes,
|
|
5
|
+
readDataTransfer,
|
|
6
|
+
readFileList,
|
|
7
|
+
type BuildClient,
|
|
8
|
+
type BuildDetection,
|
|
9
|
+
type DropClient,
|
|
10
|
+
} from './drop-core';
|
|
4
11
|
import {
|
|
5
12
|
useDropDeploy,
|
|
13
|
+
type DeployedBuild,
|
|
6
14
|
type DeployedSite,
|
|
7
15
|
type DeployError,
|
|
8
16
|
type DeployStatus,
|
|
@@ -16,6 +24,13 @@ const DRAG_INACTIVE_MS = 120;
|
|
|
16
24
|
|
|
17
25
|
const defaultClaimUrl = (siteName: string) => `https://app.netlify.com/drop/${siteName}`;
|
|
18
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
|
+
|
|
19
34
|
/**
|
|
20
35
|
* Where a build-required drop is sent by default. Mirrors the app's own Drop
|
|
21
36
|
* page: signup with `next=/drop`, so after signup + email verification the user
|
|
@@ -23,6 +38,15 @@ const defaultClaimUrl = (siteName: string) => `https://app.netlify.com/drop/${si
|
|
|
23
38
|
*/
|
|
24
39
|
const defaultBuildSignupUrl = 'https://app.netlify.com/signup?next=/drop&utm_campaign=drop';
|
|
25
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
|
+
|
|
26
50
|
interface Props {
|
|
27
51
|
/** Content the drop layer sits on top of. The zone is invisible until you drag over it. */
|
|
28
52
|
children?: ComponentChildren;
|
|
@@ -32,23 +56,81 @@ interface Props {
|
|
|
32
56
|
client?: DropClient;
|
|
33
57
|
/** Build the "claim this site" URL from the assigned site name (subdomain). */
|
|
34
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;
|
|
35
65
|
/**
|
|
36
66
|
* Where to send a build-required drop (framework source, build command,
|
|
37
|
-
* functions)
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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.
|
|
41
72
|
*/
|
|
42
73
|
buildSignupUrl?: string;
|
|
43
74
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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.
|
|
48
124
|
*/
|
|
49
125
|
onBuildRequired?: (info: BuildDetection) => void;
|
|
50
126
|
/** Called once the deploy is live. */
|
|
51
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;
|
|
52
134
|
/**
|
|
53
135
|
* Called when a deploy fails, with a humanized `message` (ready to show a
|
|
54
136
|
* user — the same copy the built-in overlay uses) and the raw `reason`. Pair
|
|
@@ -68,11 +150,22 @@ interface Props {
|
|
|
68
150
|
* properties. Stays provider-agnostic — wire it to Segment/Amplitude/GA in the
|
|
69
151
|
* consuming app, e.g. `onTrack={(name, props) => new Analytics().track(name, props)}`.
|
|
70
152
|
* Emitted events (all include `dropzone_id` when `analyticsId` is set):
|
|
71
|
-
* - `dropzone_files_dropped` — files dropped via drag ({ method: 'drag', file_count })
|
|
153
|
+
* - `dropzone_files_dropped` — files dropped via drag ({ method: 'drag', file_count, file_types })
|
|
72
154
|
* - `dropzone_browse_opened` — file picker opened ({ method: 'keyboard' | 'click' })
|
|
73
|
-
* - `dropzone_build_required` — drop needs a build
|
|
74
|
-
* - `dropzone_deploy_succeeded` — deploy went live
|
|
75
|
-
* - `
|
|
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 })
|
|
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.
|
|
163
|
+
* `file_types` is the sorted, de-duplicated list of MIME types in the
|
|
164
|
+
* selection, with `application/octet-stream` standing in wherever the browser
|
|
165
|
+
* reports none (a dragged folder, an `.exe`). It is omitted when the browser
|
|
166
|
+
* exposes no files at all. Note `dropzone_files_dropped` only fires for drag —
|
|
167
|
+
* a file-picker selection reports its types on the outcome events, as
|
|
168
|
+
* `dropzone_browse_opened` fires before any file exists.
|
|
76
169
|
*/
|
|
77
170
|
onTrack?: TrackFn;
|
|
78
171
|
/**
|
|
@@ -89,9 +182,18 @@ export default function DropZone({
|
|
|
89
182
|
apiBase,
|
|
90
183
|
client,
|
|
91
184
|
claimUrl = defaultClaimUrl,
|
|
185
|
+
siteDashboardUrl = defaultSiteDashboardUrl,
|
|
92
186
|
buildSignupUrl = defaultBuildSignupUrl,
|
|
187
|
+
getUploadToken,
|
|
188
|
+
buildClient,
|
|
189
|
+
authenticatedDropClient,
|
|
190
|
+
proxyBase,
|
|
191
|
+
accountSlug,
|
|
192
|
+
buildDeployUrl = defaultBuildDeployUrl,
|
|
193
|
+
waitForBuild,
|
|
93
194
|
onBuildRequired,
|
|
94
195
|
onDeploy,
|
|
196
|
+
onBuildDeploy,
|
|
95
197
|
onError,
|
|
96
198
|
showStatus = true,
|
|
97
199
|
onTrack,
|
|
@@ -107,9 +209,18 @@ export default function DropZone({
|
|
|
107
209
|
apiBase,
|
|
108
210
|
client,
|
|
109
211
|
claimUrl,
|
|
212
|
+
siteDashboardUrl,
|
|
110
213
|
buildSignupUrl,
|
|
214
|
+
getUploadToken,
|
|
215
|
+
buildClient,
|
|
216
|
+
authenticatedDropClient,
|
|
217
|
+
proxyBase,
|
|
218
|
+
accountSlug,
|
|
219
|
+
buildDeployUrl,
|
|
220
|
+
waitForBuild,
|
|
111
221
|
onBuildRequired,
|
|
112
222
|
onDeploy,
|
|
223
|
+
onBuildDeploy,
|
|
113
224
|
onError,
|
|
114
225
|
track,
|
|
115
226
|
});
|
|
@@ -156,7 +267,8 @@ export default function DropZone({
|
|
|
156
267
|
if (!dt) return;
|
|
157
268
|
// Capture entries synchronously — readDataTransfer reads them before
|
|
158
269
|
// its first await, so the DataTransfer isn't detached out from under us.
|
|
159
|
-
|
|
270
|
+
// Same reason the MIME types are read here and not inside `deploy`.
|
|
271
|
+
deploy(() => readDataTransfer(dt), 'drag', getFileTypes(dt.files));
|
|
160
272
|
}}
|
|
161
273
|
onClick={e => {
|
|
162
274
|
// Only the zone's own empty area opens the picker; clicks that land on
|
|
@@ -184,7 +296,11 @@ export default function DropZone({
|
|
|
184
296
|
directory=""
|
|
185
297
|
onChange={e => {
|
|
186
298
|
const input = e.currentTarget as HTMLInputElement;
|
|
187
|
-
|
|
299
|
+
// Read the types before `input.value = ''` below clears the FileList.
|
|
300
|
+
if (input.files?.length) {
|
|
301
|
+
const files = input.files;
|
|
302
|
+
deploy(() => readFileList(files), 'browse', getFileTypes(files));
|
|
303
|
+
}
|
|
188
304
|
input.value = '';
|
|
189
305
|
}}
|
|
190
306
|
/>
|
|
@@ -249,20 +365,25 @@ function StatusView({
|
|
|
249
365
|
|
|
250
366
|
case 'reading':
|
|
251
367
|
return <Spinner label="Reading files…" />;
|
|
368
|
+
case 'zipping':
|
|
369
|
+
return <Spinner label="Packaging your project…" />;
|
|
252
370
|
case 'creating':
|
|
253
371
|
return <Spinner label="Creating deploy…" />;
|
|
254
372
|
case 'processing':
|
|
255
373
|
return <Spinner label="Processing — almost live…" />;
|
|
374
|
+
case 'building':
|
|
375
|
+
return <Spinner label="Building your project…" />;
|
|
256
376
|
case 'redirecting':
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
label=
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
+
}
|
|
266
387
|
|
|
267
388
|
case 'uploading': {
|
|
268
389
|
const pct = status.total ? Math.round((status.uploaded / status.total) * 100) : 0;
|