@sentroy-co/client-sdk 2.4.4 → 2.5.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/http.d.ts +13 -0
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +62 -0
- package/dist/http.js.map +1 -1
- package/dist/react/MediaManager.d.ts +14 -0
- package/dist/react/MediaManager.d.ts.map +1 -1
- package/dist/react/MediaManager.js +21 -14
- package/dist/react/MediaManager.js.map +1 -1
- package/dist/react/MediaManagerTrigger.d.ts.map +1 -1
- package/dist/react/MediaManagerTrigger.js +4 -1
- package/dist/react/MediaManagerTrigger.js.map +1 -1
- package/dist/react/crop/CropDialog.d.ts +15 -0
- package/dist/react/crop/CropDialog.d.ts.map +1 -0
- package/dist/react/crop/CropDialog.js +126 -0
- package/dist/react/crop/CropDialog.js.map +1 -0
- package/dist/react/crop/index.d.ts +7 -0
- package/dist/react/crop/index.d.ts.map +1 -0
- package/dist/react/crop/index.js +11 -0
- package/dist/react/crop/index.js.map +1 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +5 -1
- package/dist/react/index.js.map +1 -1
- package/dist/react/lib/UploadQueuePanel.d.ts +20 -0
- package/dist/react/lib/UploadQueuePanel.d.ts.map +1 -0
- package/dist/react/lib/UploadQueuePanel.js +39 -0
- package/dist/react/lib/UploadQueuePanel.js.map +1 -0
- package/dist/react/lib/use-upload-queue.d.ts +51 -0
- package/dist/react/lib/use-upload-queue.d.ts.map +1 -0
- package/dist/react/lib/use-upload-queue.js +131 -0
- package/dist/react/lib/use-upload-queue.js.map +1 -0
- package/dist/resources/media.d.ts +8 -1
- package/dist/resources/media.d.ts.map +1 -1
- package/dist/resources/media.js +6 -2
- package/dist/resources/media.js.map +1 -1
- package/package.json +10 -1
- package/src/http.ts +85 -0
- package/src/react/MediaManager.tsx +41 -11
- package/src/react/MediaManagerTrigger.tsx +5 -1
- package/src/react/crop/CropDialog.tsx +344 -0
- package/src/react/crop/index.ts +6 -0
- package/src/react/index.ts +10 -0
- package/src/react/lib/UploadQueuePanel.tsx +273 -0
- package/src/react/lib/use-upload-queue.ts +211 -0
- package/src/resources/media.ts +13 -4
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { useCallback, useRef, useState } from "react"
|
|
2
|
+
import type { Sentroy } from "../.."
|
|
3
|
+
import type { Media } from "../../types"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tek dosyalık upload entry — UI tarafında queue listesi olarak render edilir.
|
|
7
|
+
*
|
|
8
|
+
* State machine:
|
|
9
|
+
* queued → uploading → done | error | canceled
|
|
10
|
+
* `error` ve `canceled` terminal; `done` terminal + `media` set olur.
|
|
11
|
+
*/
|
|
12
|
+
export interface UploadEntry {
|
|
13
|
+
id: string
|
|
14
|
+
file: File
|
|
15
|
+
status: "queued" | "uploading" | "done" | "error" | "canceled"
|
|
16
|
+
loaded: number
|
|
17
|
+
total: number
|
|
18
|
+
/** Server response — done iken non-null. */
|
|
19
|
+
media?: Media
|
|
20
|
+
error?: string
|
|
21
|
+
/** Per-entry cancel handle (XHR abort'u tetikler). */
|
|
22
|
+
cancel: () => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface UseUploadQueueOptions {
|
|
26
|
+
/** Aynı anda kaç dosya yüklensin. Default 3. */
|
|
27
|
+
concurrency?: number
|
|
28
|
+
/** Dosya başarıyla yüklendiğinde tetiklenir — caller refresh edebilir. */
|
|
29
|
+
onUploaded?: (media: Media) => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface UseUploadQueueResult {
|
|
33
|
+
entries: UploadEntry[]
|
|
34
|
+
/** Yeni dosyaları queue'ya ekler ve worker'ı tetikler. */
|
|
35
|
+
enqueue: (bucketSlug: string, files: File[]) => void
|
|
36
|
+
/** Bir entry'i iptal eder (uploading ise XHR abort, queued ise drop). */
|
|
37
|
+
cancel: (id: string) => void
|
|
38
|
+
/** Tek bir entry'i listeden temizler (terminal state'tekiler için). */
|
|
39
|
+
remove: (id: string) => void
|
|
40
|
+
/** Done/error/canceled olanları listeden temizler. */
|
|
41
|
+
clearDone: () => void
|
|
42
|
+
/** Aktif (queued + uploading) entry sayısı. */
|
|
43
|
+
activeCount: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Upload queue hook — concurrency-pooled, per-entry progress + cancel.
|
|
48
|
+
*
|
|
49
|
+
* Storage app'in `apps/storage/lib/upload-client.ts` + `FileUploader` queue
|
|
50
|
+
* davranışını SDK'ya taşır. SDK consumer'ı (MediaManager veya 3rd-party)
|
|
51
|
+
* `entries` array'ini render eder, `enqueue`/`cancel`/`remove` çağırır.
|
|
52
|
+
*
|
|
53
|
+
* onUploaded: caller refresh için kullanır (örn `setRefreshKey + 1`).
|
|
54
|
+
*/
|
|
55
|
+
export function useUploadQueue(
|
|
56
|
+
client: Sentroy,
|
|
57
|
+
opts: UseUploadQueueOptions = {},
|
|
58
|
+
): UseUploadQueueResult {
|
|
59
|
+
const concurrency = opts.concurrency ?? 3
|
|
60
|
+
const [entries, setEntries] = useState<UploadEntry[]>([])
|
|
61
|
+
const entriesRef = useRef<UploadEntry[]>([])
|
|
62
|
+
entriesRef.current = entries
|
|
63
|
+
const onUploadedRef = useRef(opts.onUploaded)
|
|
64
|
+
onUploadedRef.current = opts.onUploaded
|
|
65
|
+
|
|
66
|
+
// Worker pump — queued entry varsa ve aktif < concurrency ise başlat.
|
|
67
|
+
const pumpRef = useRef<() => void>(() => {})
|
|
68
|
+
|
|
69
|
+
const updateEntry = useCallback(
|
|
70
|
+
(id: string, patch: Partial<UploadEntry>) => {
|
|
71
|
+
setEntries((prev) =>
|
|
72
|
+
prev.map((e) => (e.id === id ? { ...e, ...patch } : e)),
|
|
73
|
+
)
|
|
74
|
+
},
|
|
75
|
+
[],
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
pumpRef.current = () => {
|
|
79
|
+
const list = entriesRef.current
|
|
80
|
+
const active = list.filter((e) => e.status === "uploading").length
|
|
81
|
+
if (active >= concurrency) return
|
|
82
|
+
const next = list.find((e) => e.status === "queued")
|
|
83
|
+
if (!next) return
|
|
84
|
+
|
|
85
|
+
// Mark uploading
|
|
86
|
+
updateEntry(next.id, { status: "uploading" })
|
|
87
|
+
|
|
88
|
+
// Bucket slug entry içine save edilemiyor (entry shape'i bilmesi
|
|
89
|
+
// gerekmez); enqueue closure'unda capture edilir, ayrı bucket map.
|
|
90
|
+
const bucketSlug = bucketMapRef.current[next.id]
|
|
91
|
+
if (!bucketSlug) {
|
|
92
|
+
updateEntry(next.id, { status: "error", error: "No bucket" })
|
|
93
|
+
pumpRef.current?.()
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const controller = new AbortController()
|
|
98
|
+
cancelMapRef.current[next.id] = () => controller.abort()
|
|
99
|
+
|
|
100
|
+
client.media
|
|
101
|
+
.upload(
|
|
102
|
+
bucketSlug,
|
|
103
|
+
{ body: next.file, filename: next.file.name },
|
|
104
|
+
{
|
|
105
|
+
onProgress: (loaded, total) => {
|
|
106
|
+
updateEntry(next.id, { loaded, total })
|
|
107
|
+
},
|
|
108
|
+
signal: controller.signal,
|
|
109
|
+
},
|
|
110
|
+
)
|
|
111
|
+
.then((media) => {
|
|
112
|
+
updateEntry(next.id, {
|
|
113
|
+
status: "done",
|
|
114
|
+
media,
|
|
115
|
+
loaded: next.file.size,
|
|
116
|
+
total: next.file.size,
|
|
117
|
+
})
|
|
118
|
+
onUploadedRef.current?.(media)
|
|
119
|
+
})
|
|
120
|
+
.catch((err: unknown) => {
|
|
121
|
+
const aborted =
|
|
122
|
+
(err as { message?: string })?.message === "Upload aborted"
|
|
123
|
+
updateEntry(next.id, {
|
|
124
|
+
status: aborted ? "canceled" : "error",
|
|
125
|
+
error: aborted
|
|
126
|
+
? undefined
|
|
127
|
+
: (err as Error)?.message ?? "Upload failed",
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
.finally(() => {
|
|
131
|
+
delete cancelMapRef.current[next.id]
|
|
132
|
+
// Yeni slot açıldı; başka queued varsa al.
|
|
133
|
+
pumpRef.current?.()
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// Aynı tick'te birden fazla slot doldur.
|
|
137
|
+
if (active + 1 < concurrency) pumpRef.current?.()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const bucketMapRef = useRef<Record<string, string>>({})
|
|
141
|
+
const cancelMapRef = useRef<Record<string, () => void>>({})
|
|
142
|
+
|
|
143
|
+
const enqueue = useCallback(
|
|
144
|
+
(bucketSlug: string, files: File[]) => {
|
|
145
|
+
if (files.length === 0) return
|
|
146
|
+
const newEntries: UploadEntry[] = files.map((file) => {
|
|
147
|
+
const id = `up-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
148
|
+
bucketMapRef.current[id] = bucketSlug
|
|
149
|
+
return {
|
|
150
|
+
id,
|
|
151
|
+
file,
|
|
152
|
+
status: "queued",
|
|
153
|
+
loaded: 0,
|
|
154
|
+
total: file.size,
|
|
155
|
+
cancel: () => {
|
|
156
|
+
const c = cancelMapRef.current[id]
|
|
157
|
+
if (c) c()
|
|
158
|
+
else
|
|
159
|
+
setEntries((prev) =>
|
|
160
|
+
prev.map((e) =>
|
|
161
|
+
e.id === id && e.status === "queued"
|
|
162
|
+
? { ...e, status: "canceled" }
|
|
163
|
+
: e,
|
|
164
|
+
),
|
|
165
|
+
)
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
setEntries((prev) => [...prev, ...newEntries])
|
|
170
|
+
// pump on next tick — state update'ten sonra entriesRef güncel olsun
|
|
171
|
+
Promise.resolve().then(() => pumpRef.current?.())
|
|
172
|
+
},
|
|
173
|
+
[],
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
const cancel = useCallback((id: string) => {
|
|
177
|
+
const c = cancelMapRef.current[id]
|
|
178
|
+
if (c) c()
|
|
179
|
+
else
|
|
180
|
+
setEntries((prev) =>
|
|
181
|
+
prev.map((e) =>
|
|
182
|
+
e.id === id && e.status === "queued"
|
|
183
|
+
? { ...e, status: "canceled" }
|
|
184
|
+
: e,
|
|
185
|
+
),
|
|
186
|
+
)
|
|
187
|
+
}, [])
|
|
188
|
+
|
|
189
|
+
const remove = useCallback((id: string) => {
|
|
190
|
+
setEntries((prev) => prev.filter((e) => e.id !== id))
|
|
191
|
+
delete bucketMapRef.current[id]
|
|
192
|
+
delete cancelMapRef.current[id]
|
|
193
|
+
}, [])
|
|
194
|
+
|
|
195
|
+
const clearDone = useCallback(() => {
|
|
196
|
+
setEntries((prev) =>
|
|
197
|
+
prev.filter(
|
|
198
|
+
(e) =>
|
|
199
|
+
e.status !== "done" &&
|
|
200
|
+
e.status !== "error" &&
|
|
201
|
+
e.status !== "canceled",
|
|
202
|
+
),
|
|
203
|
+
)
|
|
204
|
+
}, [])
|
|
205
|
+
|
|
206
|
+
const activeCount = entries.filter(
|
|
207
|
+
(e) => e.status === "queued" || e.status === "uploading",
|
|
208
|
+
).length
|
|
209
|
+
|
|
210
|
+
return { entries, enqueue, cancel, remove, clearDone, activeCount }
|
|
211
|
+
}
|
package/src/resources/media.ts
CHANGED
|
@@ -52,6 +52,14 @@ export class MediaResource {
|
|
|
52
52
|
async upload(
|
|
53
53
|
bucketSlug: string,
|
|
54
54
|
params: UploadMediaParams,
|
|
55
|
+
opts?: {
|
|
56
|
+
/** Bytes loaded / total — XHR `upload.onprogress` event'inden gelir.
|
|
57
|
+
* Verilmesse fetch tabanlı path kullanılır (progress yok). */
|
|
58
|
+
onProgress?: (loaded: number, total: number) => void
|
|
59
|
+
/** AbortController.signal — kullanıcı iptal ettiğinde XHR cancel
|
|
60
|
+
* edilir, promise reject. */
|
|
61
|
+
signal?: AbortSignal
|
|
62
|
+
},
|
|
55
63
|
): Promise<Media> {
|
|
56
64
|
const form = new FormData()
|
|
57
65
|
const filename =
|
|
@@ -67,10 +75,11 @@ export class MediaResource {
|
|
|
67
75
|
if (params.caption) form.append("caption", params.caption)
|
|
68
76
|
if (params.tags?.length) form.append("tags", params.tags.join(","))
|
|
69
77
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
form,
|
|
73
|
-
|
|
78
|
+
const path = `/buckets/${encodeURIComponent(bucketSlug)}/media`
|
|
79
|
+
if (opts?.onProgress || opts?.signal) {
|
|
80
|
+
return this.http.postFormWithProgress<Media>(path, form, opts)
|
|
81
|
+
}
|
|
82
|
+
return this.http.postForm<Media>(path, form)
|
|
74
83
|
}
|
|
75
84
|
|
|
76
85
|
/**
|