@sentroy-co/client-sdk 2.4.5 → 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.
Files changed (41) hide show
  1. package/dist/http.d.ts +13 -0
  2. package/dist/http.d.ts.map +1 -1
  3. package/dist/http.js +62 -0
  4. package/dist/http.js.map +1 -1
  5. package/dist/react/MediaManager.d.ts +14 -0
  6. package/dist/react/MediaManager.d.ts.map +1 -1
  7. package/dist/react/MediaManager.js +21 -14
  8. package/dist/react/MediaManager.js.map +1 -1
  9. package/dist/react/crop/CropDialog.d.ts +15 -0
  10. package/dist/react/crop/CropDialog.d.ts.map +1 -0
  11. package/dist/react/crop/CropDialog.js +126 -0
  12. package/dist/react/crop/CropDialog.js.map +1 -0
  13. package/dist/react/crop/index.d.ts +7 -0
  14. package/dist/react/crop/index.d.ts.map +1 -0
  15. package/dist/react/crop/index.js +11 -0
  16. package/dist/react/crop/index.js.map +1 -0
  17. package/dist/react/index.d.ts +2 -0
  18. package/dist/react/index.d.ts.map +1 -1
  19. package/dist/react/index.js +5 -1
  20. package/dist/react/index.js.map +1 -1
  21. package/dist/react/lib/UploadQueuePanel.d.ts +20 -0
  22. package/dist/react/lib/UploadQueuePanel.d.ts.map +1 -0
  23. package/dist/react/lib/UploadQueuePanel.js +39 -0
  24. package/dist/react/lib/UploadQueuePanel.js.map +1 -0
  25. package/dist/react/lib/use-upload-queue.d.ts +51 -0
  26. package/dist/react/lib/use-upload-queue.d.ts.map +1 -0
  27. package/dist/react/lib/use-upload-queue.js +131 -0
  28. package/dist/react/lib/use-upload-queue.js.map +1 -0
  29. package/dist/resources/media.d.ts +8 -1
  30. package/dist/resources/media.d.ts.map +1 -1
  31. package/dist/resources/media.js +6 -2
  32. package/dist/resources/media.js.map +1 -1
  33. package/package.json +10 -1
  34. package/src/http.ts +85 -0
  35. package/src/react/MediaManager.tsx +41 -11
  36. package/src/react/crop/CropDialog.tsx +344 -0
  37. package/src/react/crop/index.ts +6 -0
  38. package/src/react/index.ts +10 -0
  39. package/src/react/lib/UploadQueuePanel.tsx +273 -0
  40. package/src/react/lib/use-upload-queue.ts +211 -0
  41. 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
+ }
@@ -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
- return this.http.postForm<Media>(
71
- `/buckets/${encodeURIComponent(bucketSlug)}/media`,
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
  /**