@netlify/spark-ui 1.29.0 → 1.30.0
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/dist/components/preact/DropZone/DropZone.d.ts +11 -4
- package/dist/components/preact/DropZone/DropZone.js +52 -47
- 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 +1 -0
- package/dist/components/preact/DropZone/drop-core/index.js +17 -15
- package/dist/components/preact/DropZone/index.d.ts +1 -1
- package/dist/components/preact/DropZone/index.js +6 -4
- package/dist/components/preact/DropZone/useDropDeploy.d.ts +1 -1
- package/dist/components/preact/DropZone/useDropDeploy.js +52 -46
- package/package.json +1 -1
- package/packages/components/preact/DropZone/DropZone.tsx +25 -7
- package/packages/components/preact/DropZone/drop-core/fileTypes.ts +34 -0
- package/packages/components/preact/DropZone/drop-core/index.ts +1 -0
- package/packages/components/preact/DropZone/index.tsx +1 -0
- package/packages/components/preact/DropZone/useDropDeploy.ts +24 -4
|
@@ -46,11 +46,18 @@ interface Props {
|
|
|
46
46
|
* properties. Stays provider-agnostic — wire it to Segment/Amplitude/GA in the
|
|
47
47
|
* consuming app, e.g. `onTrack={(name, props) => new Analytics().track(name, props)}`.
|
|
48
48
|
* Emitted events (all include `dropzone_id` when `analyticsId` is set):
|
|
49
|
-
* - `dropzone_files_dropped` — files dropped via drag ({ method: 'drag', file_count })
|
|
49
|
+
* - `dropzone_files_dropped` — files dropped via drag ({ method: 'drag', file_count, file_types })
|
|
50
50
|
* - `dropzone_browse_opened` — file picker opened ({ method: 'keyboard' | 'click' })
|
|
51
|
-
* - `dropzone_build_required` — drop needs a build; handed off to signup ({ method, reason, file_count })
|
|
52
|
-
* - `dropzone_deploy_succeeded` — deploy went live ({ method, site_name, file_count })
|
|
53
|
-
* - `dropzone_deploy_failed` — deploy errored out ({ method, reason, file_count
|
|
51
|
+
* - `dropzone_build_required` — drop needs a build; handed off to signup ({ method, reason, file_count, file_types })
|
|
52
|
+
* - `dropzone_deploy_succeeded` — deploy went live ({ method, site_name, file_count, file_types })
|
|
53
|
+
* - `dropzone_deploy_failed` — deploy errored out ({ method, reason, file_count?, file_types })
|
|
54
|
+
*
|
|
55
|
+
* `file_types` is the sorted, de-duplicated list of MIME types in the
|
|
56
|
+
* selection, with `application/octet-stream` standing in wherever the browser
|
|
57
|
+
* reports none (a dragged folder, an `.exe`). It is omitted when the browser
|
|
58
|
+
* exposes no files at all. Note `dropzone_files_dropped` only fires for drag —
|
|
59
|
+
* a file-picker selection reports its types on the outcome events, as
|
|
60
|
+
* `dropzone_browse_opened` fires before any file exists.
|
|
54
61
|
*/
|
|
55
62
|
onTrack?: TrackFn;
|
|
56
63
|
/**
|
|
@@ -1,72 +1,73 @@
|
|
|
1
1
|
import { u as e } from "../../../chunks/jsxRuntime.module.Bzuv3cXw.js";
|
|
2
|
-
import { A
|
|
3
|
-
import { readFileList as
|
|
2
|
+
import { A, d as I, y as P } from "../../../chunks/hooks.module.DRcxknIz.js";
|
|
3
|
+
import { readFileList as U, readDataTransfer as j } from "./drop-core/readFiles.js";
|
|
4
|
+
import { getFileTypes as D } from "./drop-core/fileTypes.js";
|
|
4
5
|
import "./drop-core/detectBuild.js";
|
|
5
|
-
import { useDropDeploy as
|
|
6
|
-
import { Fragment as
|
|
7
|
-
import '../../../assets/DropZone.css';const
|
|
8
|
-
function
|
|
6
|
+
import { useDropDeploy as B } from "./useDropDeploy.js";
|
|
7
|
+
import { Fragment as w } from "preact";
|
|
8
|
+
import '../../../assets/DropZone.css';const M = 120, N = (n) => `https://app.netlify.com/drop/${n}`, O = "https://app.netlify.com/signup?next=/drop&utm_campaign=drop";
|
|
9
|
+
function W({
|
|
9
10
|
children: n,
|
|
10
11
|
apiBase: l,
|
|
11
12
|
client: i,
|
|
12
|
-
claimUrl: s =
|
|
13
|
-
buildSignupUrl: u =
|
|
14
|
-
onBuildRequired:
|
|
15
|
-
onDeploy:
|
|
16
|
-
onError:
|
|
13
|
+
claimUrl: s = N,
|
|
14
|
+
buildSignupUrl: u = O,
|
|
15
|
+
onBuildRequired: T,
|
|
16
|
+
onDeploy: C,
|
|
17
|
+
onError: _,
|
|
17
18
|
showStatus: f = !0,
|
|
18
|
-
onTrack:
|
|
19
|
+
onTrack: d,
|
|
19
20
|
analyticsId: g,
|
|
20
|
-
className:
|
|
21
|
+
className: S
|
|
21
22
|
}) {
|
|
22
|
-
const
|
|
23
|
+
const m = (r, t = {}) => d == null ? void 0 : d(r, g ? { dropzone_id: g, ...t } : t), { status: a, warning: h, busy: o, deploy: v, reset: R } = B({
|
|
23
24
|
apiBase: l,
|
|
24
25
|
client: i,
|
|
25
26
|
claimUrl: s,
|
|
26
27
|
buildSignupUrl: u,
|
|
27
|
-
onBuildRequired:
|
|
28
|
-
onDeploy:
|
|
29
|
-
onError:
|
|
30
|
-
track:
|
|
31
|
-
}), { active: p, keepActive:
|
|
28
|
+
onBuildRequired: T,
|
|
29
|
+
onDeploy: C,
|
|
30
|
+
onError: _,
|
|
31
|
+
track: m
|
|
32
|
+
}), { active: p, keepActive: $, endDrag: x } = V(), y = A(null), b = (r) => {
|
|
32
33
|
var t;
|
|
33
|
-
|
|
34
|
-
},
|
|
34
|
+
m("dropzone_browse_opened", { method: r }), (t = y.current) == null || t.click();
|
|
35
|
+
}, E = f && (p || o || a.kind === "error"), F = [
|
|
35
36
|
"n-dropzone",
|
|
36
37
|
p && "is-active",
|
|
37
38
|
o && "is-busy",
|
|
38
39
|
!f && "is-bare",
|
|
39
|
-
|
|
40
|
+
S
|
|
40
41
|
].filter(Boolean).join(" ");
|
|
41
42
|
return /* @__PURE__ */ e(
|
|
42
43
|
"div",
|
|
43
44
|
{
|
|
44
|
-
class:
|
|
45
|
+
class: F,
|
|
45
46
|
"data-state": a.kind,
|
|
46
47
|
role: "button",
|
|
47
48
|
tabIndex: o ? -1 : 0,
|
|
48
49
|
"aria-label": "Drop a site folder or zip here to deploy it, or press Enter to browse for files",
|
|
49
50
|
"aria-busy": o,
|
|
50
51
|
onDragOver: (r) => {
|
|
51
|
-
o || (r.preventDefault(),
|
|
52
|
+
o || (r.preventDefault(), $());
|
|
52
53
|
},
|
|
53
54
|
onDrop: (r) => {
|
|
54
|
-
if (r.preventDefault(),
|
|
55
|
+
if (r.preventDefault(), x(), o) return;
|
|
55
56
|
const t = r.dataTransfer;
|
|
56
|
-
t && v(() =>
|
|
57
|
+
t && v(() => j(t), "drag", D(t.files));
|
|
57
58
|
},
|
|
58
59
|
onClick: (r) => {
|
|
59
|
-
!o && r.target === r.currentTarget &&
|
|
60
|
+
!o && r.target === r.currentTarget && b("click");
|
|
60
61
|
},
|
|
61
62
|
onKeyDown: (r) => {
|
|
62
|
-
o || (r.key === "Enter" || r.key === " ") && (r.preventDefault(),
|
|
63
|
+
o || (r.key === "Enter" || r.key === " ") && (r.preventDefault(), b("keyboard"));
|
|
63
64
|
},
|
|
64
65
|
children: [
|
|
65
66
|
/* @__PURE__ */ e("div", { class: "n-dropzone-content", children: n }),
|
|
66
67
|
/* @__PURE__ */ e(
|
|
67
68
|
"input",
|
|
68
69
|
{
|
|
69
|
-
ref:
|
|
70
|
+
ref: y,
|
|
70
71
|
type: "file",
|
|
71
72
|
hidden: !0,
|
|
72
73
|
multiple: !0,
|
|
@@ -75,17 +76,21 @@ function H({
|
|
|
75
76
|
onChange: (r) => {
|
|
76
77
|
var z;
|
|
77
78
|
const t = r.currentTarget;
|
|
78
|
-
(z = t.files) != null && z.length
|
|
79
|
+
if ((z = t.files) != null && z.length) {
|
|
80
|
+
const k = t.files;
|
|
81
|
+
v(() => U(k), "browse", D(k));
|
|
82
|
+
}
|
|
83
|
+
t.value = "";
|
|
79
84
|
}
|
|
80
85
|
}
|
|
81
86
|
),
|
|
82
|
-
|
|
87
|
+
E && /* @__PURE__ */ e(
|
|
83
88
|
"div",
|
|
84
89
|
{
|
|
85
90
|
class: "n-dropzone-overlay n-fadeIn",
|
|
86
91
|
children: [
|
|
87
|
-
/* @__PURE__ */ e(
|
|
88
|
-
|
|
92
|
+
/* @__PURE__ */ e(G, { status: a, active: p, onReset: R }),
|
|
93
|
+
h && a.kind !== "error" && o && /* @__PURE__ */ e("p", { class: "n-dropzone-warning", role: "status", children: h })
|
|
89
94
|
]
|
|
90
95
|
},
|
|
91
96
|
a.kind === "idle" ? "active" : a.kind
|
|
@@ -94,15 +99,15 @@ function H({
|
|
|
94
99
|
}
|
|
95
100
|
);
|
|
96
101
|
}
|
|
97
|
-
function
|
|
98
|
-
const [n, l] =
|
|
99
|
-
return
|
|
100
|
-
l(!0), clearTimeout(i.current), i.current = setTimeout(() => l(!1),
|
|
102
|
+
function V() {
|
|
103
|
+
const [n, l] = I(!1), i = A();
|
|
104
|
+
return P(() => () => clearTimeout(i.current), []), { active: n, keepActive: () => {
|
|
105
|
+
l(!0), clearTimeout(i.current), i.current = setTimeout(() => l(!1), M);
|
|
101
106
|
}, endDrag: () => {
|
|
102
107
|
clearTimeout(i.current), l(!1);
|
|
103
108
|
} };
|
|
104
109
|
}
|
|
105
|
-
function
|
|
110
|
+
function G({
|
|
106
111
|
status: n,
|
|
107
112
|
active: l,
|
|
108
113
|
onReset: i
|
|
@@ -112,34 +117,34 @@ function O({
|
|
|
112
117
|
case "idle":
|
|
113
118
|
return l ? /* @__PURE__ */ e("p", { class: "n-dropzone-hint", children: "Drop to deploy" }) : null;
|
|
114
119
|
case "reading":
|
|
115
|
-
return /* @__PURE__ */ e(
|
|
120
|
+
return /* @__PURE__ */ e(c, { label: "Reading files…" });
|
|
116
121
|
case "creating":
|
|
117
|
-
return /* @__PURE__ */ e(
|
|
122
|
+
return /* @__PURE__ */ e(c, { label: "Creating deploy…" });
|
|
118
123
|
case "processing":
|
|
119
|
-
return /* @__PURE__ */ e(
|
|
124
|
+
return /* @__PURE__ */ e(c, { label: "Processing — almost live…" });
|
|
120
125
|
case "redirecting":
|
|
121
126
|
return /* @__PURE__ */ e(
|
|
122
|
-
|
|
127
|
+
c,
|
|
123
128
|
{
|
|
124
129
|
label: n.target === "signup" ? "This project needs a build — taking you to Netlify…" : "Site’s live — taking you to claim it…"
|
|
125
130
|
}
|
|
126
131
|
);
|
|
127
132
|
case "uploading": {
|
|
128
133
|
const s = n.total ? Math.round(n.uploaded / n.total * 100) : 0;
|
|
129
|
-
return /* @__PURE__ */ e(
|
|
130
|
-
/* @__PURE__ */ e(
|
|
134
|
+
return /* @__PURE__ */ e(w, { children: [
|
|
135
|
+
/* @__PURE__ */ e(c, { label: `Uploading ${n.uploaded}/${n.total} files…` }),
|
|
131
136
|
/* @__PURE__ */ e("div", { class: "n-dropzone-progress", role: "progressbar", "aria-valuenow": s, children: /* @__PURE__ */ e("div", { class: "n-dropzone-progress-bar", style: { width: `${s}%` } }) })
|
|
132
137
|
] });
|
|
133
138
|
}
|
|
134
139
|
case "error":
|
|
135
|
-
return /* @__PURE__ */ e(
|
|
140
|
+
return /* @__PURE__ */ e(w, { children: [
|
|
136
141
|
/* @__PURE__ */ e("p", { class: "n-dropzone-title n-dropzone-title--error", children: "Something went wrong" }),
|
|
137
142
|
/* @__PURE__ */ e("p", { class: "n-dropzone-subtitle", role: "alert", children: n.message }),
|
|
138
143
|
/* @__PURE__ */ e("button", { type: "button", class: "n-dropzone-browse", onClick: i, children: "Try again" })
|
|
139
144
|
] });
|
|
140
145
|
}
|
|
141
146
|
}
|
|
142
|
-
function
|
|
147
|
+
function c({ label: n }) {
|
|
143
148
|
return /* @__PURE__ */ e("div", { class: "n-dropzone-loading", children: [
|
|
144
149
|
/* @__PURE__ */ e("span", { class: "n-dropzone-spinner", "aria-hidden": "true" }),
|
|
145
150
|
/* @__PURE__ */ e("p", { class: "n-dropzone-subtitle", "aria-live": "polite", children: [
|
|
@@ -150,5 +155,5 @@ function d({ label: n }) {
|
|
|
150
155
|
] });
|
|
151
156
|
}
|
|
152
157
|
export {
|
|
153
|
-
|
|
158
|
+
W as default
|
|
154
159
|
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Distinct MIME types across a drop or file-picker selection, sorted so the same
|
|
3
|
+
* selection always yields the same array.
|
|
4
|
+
*
|
|
5
|
+
* Read from the raw `File.type` at the drop boundary rather than from
|
|
6
|
+
* `ProcessedFile`s: the MIME type only exists on the original `File`, and reading
|
|
7
|
+
* can throw before any `ProcessedFile` does (an unsupported single file) — the
|
|
8
|
+
* very case the file type is most worth knowing about.
|
|
9
|
+
*
|
|
10
|
+
* Browsers leave `File.type` empty for anything they don't recognise — a dragged
|
|
11
|
+
* folder, an `.exe`, an extensionless file — so those collapse to
|
|
12
|
+
* `application/octet-stream` and the property never carries an empty string.
|
|
13
|
+
*
|
|
14
|
+
* Call this synchronously inside the drop handler: a `DataTransfer` detaches once
|
|
15
|
+
* the event turn ends.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getFileTypes(files: FileList | null | undefined): string[];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const n = "application/octet-stream";
|
|
2
|
+
function e(t) {
|
|
3
|
+
if (!(t != null && t.length)) return [];
|
|
4
|
+
const r = /* @__PURE__ */ new Set();
|
|
5
|
+
for (const o of Array.from(t))
|
|
6
|
+
r.add(o.type || n);
|
|
7
|
+
return Array.from(r).sort().slice(0, 20);
|
|
8
|
+
}
|
|
9
|
+
export {
|
|
10
|
+
e as getFileTypes
|
|
11
|
+
};
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { getDigest as o } from "./digest.js";
|
|
2
|
-
import { getSingleNonIndexHtmlFileName as
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
2
|
+
import { getSingleNonIndexHtmlFileName as l, hasMultipleHtmlFilesWithoutIndex as n, readDataTransfer as p, readFileList as i, renameSingleNonIndexHtmlToIndex as m } from "./readFiles.js";
|
|
3
|
+
import { getFileTypes as d } from "./fileTypes.js";
|
|
4
|
+
import { detectBuild as s } from "./detectBuild.js";
|
|
5
|
+
import { AnonymousDropClient as g } from "./client.js";
|
|
6
|
+
import { deployFiles as D } from "./deploy.js";
|
|
7
|
+
import { UnsupportedDropError as I, humanizeDropError as h } from "./errors.js";
|
|
7
8
|
export {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
g as AnonymousDropClient,
|
|
10
|
+
I as UnsupportedDropError,
|
|
11
|
+
D as deployFiles,
|
|
12
|
+
s as detectBuild,
|
|
12
13
|
o as getDigest,
|
|
13
|
-
|
|
14
|
-
l as
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
d as getFileTypes,
|
|
15
|
+
l as getSingleNonIndexHtmlFileName,
|
|
16
|
+
n as hasMultipleHtmlFilesWithoutIndex,
|
|
17
|
+
h as humanizeDropError,
|
|
18
|
+
p as readDataTransfer,
|
|
19
|
+
i as readFileList,
|
|
20
|
+
m as renameSingleNonIndexHtmlToIndex
|
|
19
21
|
};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { default as DropZone } from './DropZone';
|
|
2
|
-
export { AnonymousDropClient, deployFiles, detectBuild, readDataTransfer, readFileList, } from './drop-core';
|
|
2
|
+
export { AnonymousDropClient, deployFiles, detectBuild, getFileTypes, readDataTransfer, readFileList, } from './drop-core';
|
|
3
3
|
export type { BuildDetection, BuildReason, DropClient, DropResponse, Digest, DeployResult, DeployProgress, DeployPhase, ProcessedFile, } from './drop-core';
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { default as r } from "./DropZone.js";
|
|
2
|
-
import { readDataTransfer as p, readFileList as
|
|
2
|
+
import { readDataTransfer as p, readFileList as f } from "./drop-core/readFiles.js";
|
|
3
|
+
import { getFileTypes as l } from "./drop-core/fileTypes.js";
|
|
3
4
|
import { detectBuild as d } from "./drop-core/detectBuild.js";
|
|
4
|
-
import { AnonymousDropClient as
|
|
5
|
+
import { AnonymousDropClient as s } from "./drop-core/client.js";
|
|
5
6
|
import { deployFiles as n } from "./drop-core/deploy.js";
|
|
6
7
|
export {
|
|
7
|
-
|
|
8
|
+
s as AnonymousDropClient,
|
|
8
9
|
r as DropZone,
|
|
9
10
|
n as deployFiles,
|
|
10
11
|
d as detectBuild,
|
|
12
|
+
l as getFileTypes,
|
|
11
13
|
p as readDataTransfer,
|
|
12
|
-
|
|
14
|
+
f as readFileList
|
|
13
15
|
};
|
|
@@ -55,7 +55,7 @@ export declare function useDropDeploy({ apiBase, client, claimUrl, buildSignupUr
|
|
|
55
55
|
status: DeployStatus;
|
|
56
56
|
warning: string | null;
|
|
57
57
|
busy: boolean;
|
|
58
|
-
deploy: (read: () => Promise<ProcessedFile[]>, source: DeploySource) => Promise<void>;
|
|
58
|
+
deploy: (read: () => Promise<ProcessedFile[]>, source: DeploySource, fileTypes: string[]) => Promise<void>;
|
|
59
59
|
reset: () => void;
|
|
60
60
|
};
|
|
61
61
|
export {};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { d as
|
|
2
|
-
import { renameSingleNonIndexHtmlToIndex as
|
|
3
|
-
import { detectBuild as
|
|
4
|
-
import { AnonymousDropClient as
|
|
5
|
-
import { deployFiles as
|
|
6
|
-
import { humanizeDropError as
|
|
7
|
-
function
|
|
1
|
+
import { d as y, A as W } from "../../../chunks/hooks.module.DRcxknIz.js";
|
|
2
|
+
import { renameSingleNonIndexHtmlToIndex as $, getSingleNonIndexHtmlFileName as v, hasMultipleHtmlFilesWithoutIndex as A } from "./drop-core/readFiles.js";
|
|
3
|
+
import { detectBuild as C } from "./drop-core/detectBuild.js";
|
|
4
|
+
import { AnonymousDropClient as P } from "./drop-core/client.js";
|
|
5
|
+
import { deployFiles as q } from "./drop-core/deploy.js";
|
|
6
|
+
import { humanizeDropError as M } from "./drop-core/errors.js";
|
|
7
|
+
function T(n) {
|
|
8
8
|
switch (n.phase) {
|
|
9
9
|
case "creating":
|
|
10
10
|
return { kind: "creating" };
|
|
@@ -15,75 +15,81 @@ function q(n) {
|
|
|
15
15
|
return { kind: "processing" };
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
-
function
|
|
19
|
-
const l =
|
|
20
|
-
return l ? `No index.html found — "${l}" won’t be served at the site root.` :
|
|
18
|
+
function j(n) {
|
|
19
|
+
const l = v(n);
|
|
20
|
+
return l ? `No index.html found — "${l}" won’t be served at the site root.` : A(n) ? "No index.html found — your site may not have a landing page." : null;
|
|
21
21
|
}
|
|
22
|
-
function
|
|
22
|
+
function O({
|
|
23
23
|
apiBase: n,
|
|
24
24
|
client: l,
|
|
25
|
-
claimUrl:
|
|
26
|
-
buildSignupUrl:
|
|
27
|
-
onBuildRequired:
|
|
28
|
-
onDeploy:
|
|
29
|
-
onError:
|
|
25
|
+
claimUrl: k,
|
|
26
|
+
buildSignupUrl: w,
|
|
27
|
+
onBuildRequired: f,
|
|
28
|
+
onDeploy: u,
|
|
29
|
+
onError: c,
|
|
30
30
|
track: s
|
|
31
31
|
}) {
|
|
32
|
-
const [
|
|
33
|
-
async function
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
},
|
|
32
|
+
const [m, o] = y({ kind: "idle" }), [N, p] = y(null), b = W(l ?? new P(n)).current, x = m.kind !== "idle" && m.kind !== "error";
|
|
33
|
+
async function I(z, i, h) {
|
|
34
|
+
p(null), o({ kind: "reading" });
|
|
35
|
+
const a = h.length ? { file_types: h } : {}, _ = (e, t, d) => {
|
|
36
|
+
o({ kind: "error", message: e }), s("dropzone_deploy_failed", { method: i, reason: t, ...a, ...d }), c == null || c({ message: e, reason: t });
|
|
37
|
+
}, F = (e, t) => {
|
|
38
38
|
const d = e instanceof Error ? e.message : String(e);
|
|
39
|
-
|
|
39
|
+
_(M(e), d, t);
|
|
40
40
|
};
|
|
41
41
|
try {
|
|
42
|
-
const e =
|
|
42
|
+
const e = $(await z());
|
|
43
43
|
if (!e.length) {
|
|
44
|
-
|
|
44
|
+
_("No files found to deploy.", "No files found to deploy.", { file_count: 0 });
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
i === "drag" && s("dropzone_files_dropped", {
|
|
48
|
+
method: i,
|
|
49
|
+
file_count: e.length,
|
|
50
|
+
...a
|
|
51
|
+
});
|
|
52
|
+
const t = C(e);
|
|
49
53
|
if (t.buildRequired) {
|
|
50
54
|
if (s("dropzone_build_required", {
|
|
51
|
-
method:
|
|
55
|
+
method: i,
|
|
52
56
|
reason: t.reason,
|
|
53
|
-
file_count: e.length
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
file_count: e.length,
|
|
58
|
+
...a
|
|
59
|
+
}), f) {
|
|
60
|
+
o({ kind: "idle" }), f(t);
|
|
56
61
|
return;
|
|
57
62
|
}
|
|
58
|
-
|
|
63
|
+
o({ kind: "redirecting", target: "signup" }), window.location.assign(w);
|
|
59
64
|
return;
|
|
60
65
|
}
|
|
61
|
-
|
|
66
|
+
p(j(e));
|
|
62
67
|
const {
|
|
63
68
|
url: d,
|
|
64
|
-
token:
|
|
69
|
+
token: g,
|
|
65
70
|
deploy: r
|
|
66
|
-
} = await
|
|
67
|
-
onProgress: (
|
|
71
|
+
} = await q(b, e, {
|
|
72
|
+
onProgress: (H) => o(T(H))
|
|
68
73
|
});
|
|
69
74
|
try {
|
|
70
|
-
localStorage.setItem("drop-token",
|
|
75
|
+
localStorage.setItem("drop-token", g), localStorage.setItem("dropSiteName", r.subdomain);
|
|
71
76
|
} catch {
|
|
72
77
|
}
|
|
73
|
-
|
|
74
|
-
method:
|
|
78
|
+
o({ kind: "redirecting", target: "claim" }), s("dropzone_deploy_succeeded", {
|
|
79
|
+
method: i,
|
|
75
80
|
site_name: r.subdomain,
|
|
76
|
-
file_count: e.length
|
|
77
|
-
|
|
81
|
+
file_count: e.length,
|
|
82
|
+
...a
|
|
83
|
+
}), u == null || u({ url: d, token: g, siteName: r.subdomain, siteId: r.id }), window.location.assign(`${k(r.subdomain)}#drop_token=${g}`);
|
|
78
84
|
} catch (e) {
|
|
79
|
-
|
|
85
|
+
F(e);
|
|
80
86
|
}
|
|
81
87
|
}
|
|
82
|
-
function
|
|
83
|
-
|
|
88
|
+
function S() {
|
|
89
|
+
p(null), o({ kind: "idle" });
|
|
84
90
|
}
|
|
85
|
-
return { status:
|
|
91
|
+
return { status: m, warning: N, busy: x, deploy: I, reset: S };
|
|
86
92
|
}
|
|
87
93
|
export {
|
|
88
|
-
|
|
94
|
+
O as useDropDeploy
|
|
89
95
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
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 BuildDetection,
|
|
8
|
+
type DropClient,
|
|
9
|
+
} from './drop-core';
|
|
4
10
|
import {
|
|
5
11
|
useDropDeploy,
|
|
6
12
|
type DeployedSite,
|
|
@@ -68,11 +74,18 @@ interface Props {
|
|
|
68
74
|
* properties. Stays provider-agnostic — wire it to Segment/Amplitude/GA in the
|
|
69
75
|
* consuming app, e.g. `onTrack={(name, props) => new Analytics().track(name, props)}`.
|
|
70
76
|
* Emitted events (all include `dropzone_id` when `analyticsId` is set):
|
|
71
|
-
* - `dropzone_files_dropped` — files dropped via drag ({ method: 'drag', file_count })
|
|
77
|
+
* - `dropzone_files_dropped` — files dropped via drag ({ method: 'drag', file_count, file_types })
|
|
72
78
|
* - `dropzone_browse_opened` — file picker opened ({ method: 'keyboard' | 'click' })
|
|
73
|
-
* - `dropzone_build_required` — drop needs a build; handed off to signup ({ method, reason, file_count })
|
|
74
|
-
* - `dropzone_deploy_succeeded` — deploy went live ({ method, site_name, file_count })
|
|
75
|
-
* - `dropzone_deploy_failed` — deploy errored out ({ method, reason, file_count
|
|
79
|
+
* - `dropzone_build_required` — drop needs a build; handed off to signup ({ method, reason, file_count, file_types })
|
|
80
|
+
* - `dropzone_deploy_succeeded` — deploy went live ({ method, site_name, file_count, file_types })
|
|
81
|
+
* - `dropzone_deploy_failed` — deploy errored out ({ method, reason, file_count?, file_types })
|
|
82
|
+
*
|
|
83
|
+
* `file_types` is the sorted, de-duplicated list of MIME types in the
|
|
84
|
+
* selection, with `application/octet-stream` standing in wherever the browser
|
|
85
|
+
* reports none (a dragged folder, an `.exe`). It is omitted when the browser
|
|
86
|
+
* exposes no files at all. Note `dropzone_files_dropped` only fires for drag —
|
|
87
|
+
* a file-picker selection reports its types on the outcome events, as
|
|
88
|
+
* `dropzone_browse_opened` fires before any file exists.
|
|
76
89
|
*/
|
|
77
90
|
onTrack?: TrackFn;
|
|
78
91
|
/**
|
|
@@ -156,7 +169,8 @@ export default function DropZone({
|
|
|
156
169
|
if (!dt) return;
|
|
157
170
|
// Capture entries synchronously — readDataTransfer reads them before
|
|
158
171
|
// its first await, so the DataTransfer isn't detached out from under us.
|
|
159
|
-
|
|
172
|
+
// Same reason the MIME types are read here and not inside `deploy`.
|
|
173
|
+
deploy(() => readDataTransfer(dt), 'drag', getFileTypes(dt.files));
|
|
160
174
|
}}
|
|
161
175
|
onClick={e => {
|
|
162
176
|
// Only the zone's own empty area opens the picker; clicks that land on
|
|
@@ -184,7 +198,11 @@ export default function DropZone({
|
|
|
184
198
|
directory=""
|
|
185
199
|
onChange={e => {
|
|
186
200
|
const input = e.currentTarget as HTMLInputElement;
|
|
187
|
-
|
|
201
|
+
// Read the types before `input.value = ''` below clears the FileList.
|
|
202
|
+
if (input.files?.length) {
|
|
203
|
+
const files = input.files;
|
|
204
|
+
deploy(() => readFileList(files), 'browse', getFileTypes(files));
|
|
205
|
+
}
|
|
188
206
|
input.value = '';
|
|
189
207
|
}}
|
|
190
208
|
/>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** What we report when the browser gives us no MIME type at all. */
|
|
2
|
+
const UNKNOWN_TYPE = 'application/octet-stream';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Distinct types kept per event. Deduplication alone keeps this small for a real
|
|
6
|
+
* site (a handful of types across hundreds of files); the cap is a backstop so an
|
|
7
|
+
* unusual folder can't push an oversized property into the analytics payload.
|
|
8
|
+
*/
|
|
9
|
+
const MAX_TYPES = 20;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Distinct MIME types across a drop or file-picker selection, sorted so the same
|
|
13
|
+
* selection always yields the same array.
|
|
14
|
+
*
|
|
15
|
+
* Read from the raw `File.type` at the drop boundary rather than from
|
|
16
|
+
* `ProcessedFile`s: the MIME type only exists on the original `File`, and reading
|
|
17
|
+
* can throw before any `ProcessedFile` does (an unsupported single file) — the
|
|
18
|
+
* very case the file type is most worth knowing about.
|
|
19
|
+
*
|
|
20
|
+
* Browsers leave `File.type` empty for anything they don't recognise — a dragged
|
|
21
|
+
* folder, an `.exe`, an extensionless file — so those collapse to
|
|
22
|
+
* `application/octet-stream` and the property never carries an empty string.
|
|
23
|
+
*
|
|
24
|
+
* Call this synchronously inside the drop handler: a `DataTransfer` detaches once
|
|
25
|
+
* the event turn ends.
|
|
26
|
+
*/
|
|
27
|
+
export function getFileTypes(files: FileList | null | undefined): string[] {
|
|
28
|
+
if (!files?.length) return [];
|
|
29
|
+
const types = new Set<string>();
|
|
30
|
+
for (const file of Array.from(files)) {
|
|
31
|
+
types.add(file.type || UNKNOWN_TYPE);
|
|
32
|
+
}
|
|
33
|
+
return Array.from(types).sort().slice(0, MAX_TYPES);
|
|
34
|
+
}
|
|
@@ -103,22 +103,36 @@ export function useDropDeploy({
|
|
|
103
103
|
|
|
104
104
|
const busy = status.kind !== 'idle' && status.kind !== 'error';
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
/**
|
|
107
|
+
* `fileTypes` is the MIME types of the selection, read at the drop boundary
|
|
108
|
+
* (see `getFileTypes`) and passed in rather than derived here — it has to be
|
|
109
|
+
* captured before `read()` runs so it survives a read that throws. Omitted
|
|
110
|
+
* from events when empty rather than sent as `[]`.
|
|
111
|
+
*/
|
|
112
|
+
async function deploy(
|
|
113
|
+
read: () => Promise<ProcessedFile[]>,
|
|
114
|
+
source: DeploySource,
|
|
115
|
+
fileTypes: string[]
|
|
116
|
+
) {
|
|
107
117
|
setWarning(null);
|
|
108
118
|
setStatus({ kind: 'reading' });
|
|
109
119
|
|
|
120
|
+
const typeProps = fileTypes.length ? { file_types: fileTypes } : {};
|
|
121
|
+
|
|
110
122
|
// Surface a failure: move to the error state and emit the matching
|
|
111
123
|
// analytics event. Centralised so every failure path (empty selection, a
|
|
112
124
|
// read/deploy throw) reports uniformly — the mirror of the
|
|
113
125
|
// `dropzone_deploy_succeeded` event on the happy path. `file_count` is only
|
|
114
|
-
// included where the file set is known (it isn't if `read()` itself threw)
|
|
126
|
+
// included where the file set is known (it isn't if `read()` itself threw);
|
|
127
|
+
// `file_types` rides along on every failure precisely because it *is* known
|
|
128
|
+
// in that case — a rejected single file is the main thing we want to see.
|
|
115
129
|
// `reason` goes to analytics as-is (raw, e.g. "drop failed: 429"); `message`
|
|
116
130
|
// is what the user sees, which for a caught error is humanized separately.
|
|
117
131
|
// `onError` gets both so a consumer (e.g. with `showStatus={false}`) can show
|
|
118
132
|
// the humanized message while keeping the raw reason for their own logging.
|
|
119
133
|
const fail = (message: string, reason: string, extra?: Record<string, unknown>) => {
|
|
120
134
|
setStatus({ kind: 'error', message });
|
|
121
|
-
track('dropzone_deploy_failed', { method: source, reason, ...extra });
|
|
135
|
+
track('dropzone_deploy_failed', { method: source, reason, ...typeProps, ...extra });
|
|
122
136
|
onError?.({ message, reason });
|
|
123
137
|
};
|
|
124
138
|
|
|
@@ -141,7 +155,11 @@ export function useDropDeploy({
|
|
|
141
155
|
// A drop is the "user provided files" signal; the browse path is already
|
|
142
156
|
// tracked when the picker opens.
|
|
143
157
|
if (source === 'drag') {
|
|
144
|
-
track('dropzone_files_dropped', {
|
|
158
|
+
track('dropzone_files_dropped', {
|
|
159
|
+
method: source,
|
|
160
|
+
file_count: files.length,
|
|
161
|
+
...typeProps,
|
|
162
|
+
});
|
|
145
163
|
}
|
|
146
164
|
|
|
147
165
|
// Build-required projects can't deploy through the anonymous drop API —
|
|
@@ -155,6 +173,7 @@ export function useDropDeploy({
|
|
|
155
173
|
method: source,
|
|
156
174
|
reason: build.reason,
|
|
157
175
|
file_count: files.length,
|
|
176
|
+
...typeProps,
|
|
158
177
|
});
|
|
159
178
|
if (onBuildRequired) {
|
|
160
179
|
// The consumer owns the hand-off now; drop back to idle so the zone
|
|
@@ -197,6 +216,7 @@ export function useDropDeploy({
|
|
|
197
216
|
method: source,
|
|
198
217
|
site_name: result.subdomain,
|
|
199
218
|
file_count: files.length,
|
|
219
|
+
...typeProps,
|
|
200
220
|
});
|
|
201
221
|
onDeploy?.({ url, token, siteName: result.subdomain, siteId: result.id });
|
|
202
222
|
|