fwtoolkit 0.1.0-alpha.7 → 0.1.0-beta.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.
Files changed (107) hide show
  1. package/dist/basic.d.ts +49 -36
  2. package/dist/basic.d.ts.map +1 -1
  3. package/dist/basic.js +58 -39
  4. package/dist/basic.js.map +1 -1
  5. package/dist/blob.d.ts +1 -1
  6. package/dist/blob.d.ts.map +1 -1
  7. package/dist/blob.js +0 -1
  8. package/dist/blob.js.map +1 -1
  9. package/dist/content_menu.d.ts +63 -20
  10. package/dist/content_menu.d.ts.map +1 -1
  11. package/dist/content_menu.js +21 -18
  12. package/dist/content_menu.js.map +1 -1
  13. package/dist/datatable_bulk.d.ts +34 -6
  14. package/dist/datatable_bulk.d.ts.map +1 -1
  15. package/dist/datatable_bulk.js +4 -5
  16. package/dist/datatable_bulk.js.map +1 -1
  17. package/dist/dialog.d.ts +82 -7
  18. package/dist/dialog.d.ts.map +1 -1
  19. package/dist/dialog.js +21 -16
  20. package/dist/dialog.js.map +1 -1
  21. package/dist/events.d.ts +1 -1
  22. package/dist/events.d.ts.map +1 -1
  23. package/dist/events.js +3 -3
  24. package/dist/events.js.map +1 -1
  25. package/dist/faq_dialog.d.ts +13 -4
  26. package/dist/faq_dialog.d.ts.map +1 -1
  27. package/dist/faq_dialog.js +4 -2
  28. package/dist/faq_dialog.js.map +1 -1
  29. package/dist/file/dialog.d.ts +33 -13
  30. package/dist/file/dialog.d.ts.map +1 -1
  31. package/dist/file/dialog.js +6 -8
  32. package/dist/file/dialog.js.map +1 -1
  33. package/dist/file/index.d.ts.map +1 -1
  34. package/dist/file/index.js +0 -1
  35. package/dist/file/index.js.map +1 -1
  36. package/dist/file/new_folder_dialog.d.ts +5 -2
  37. package/dist/file/new_folder_dialog.d.ts.map +1 -1
  38. package/dist/file/new_folder_dialog.js +1 -2
  39. package/dist/file/new_folder_dialog.js.map +1 -1
  40. package/dist/file/selector.d.ts +47 -14
  41. package/dist/file/selector.d.ts.map +1 -1
  42. package/dist/file/selector.js +11 -9
  43. package/dist/file/selector.js.map +1 -1
  44. package/dist/file/templates.d.ts +1 -1
  45. package/dist/file/templates.d.ts.map +1 -1
  46. package/dist/file/templates.js +0 -1
  47. package/dist/file/templates.js.map +1 -1
  48. package/dist/file/tools.d.ts +4 -4
  49. package/dist/file/tools.d.ts.map +1 -1
  50. package/dist/file/tools.js +4 -4
  51. package/dist/file/tools.js.map +1 -1
  52. package/dist/focus.d.ts +1 -1
  53. package/dist/focus.d.ts.map +1 -1
  54. package/dist/focus.js +5 -5
  55. package/dist/focus.js.map +1 -1
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +0 -1
  58. package/dist/index.js.map +1 -1
  59. package/dist/network.d.ts +19 -7
  60. package/dist/network.d.ts.map +1 -1
  61. package/dist/network.js +15 -12
  62. package/dist/network.js.map +1 -1
  63. package/dist/overview_menu.d.ts +77 -18
  64. package/dist/overview_menu.d.ts.map +1 -1
  65. package/dist/overview_menu.js +53 -34
  66. package/dist/overview_menu.js.map +1 -1
  67. package/dist/settings.d.ts +7 -2
  68. package/dist/settings.d.ts.map +1 -1
  69. package/dist/settings.js +0 -1
  70. package/dist/settings.js.map +1 -1
  71. package/dist/user.d.ts +8 -2
  72. package/dist/user.d.ts.map +1 -1
  73. package/dist/user.js +1 -2
  74. package/dist/user.js.map +1 -1
  75. package/dist/worker.d.ts +1 -1
  76. package/dist/worker.d.ts.map +1 -1
  77. package/dist/worker.js +1 -2
  78. package/dist/worker.js.map +1 -1
  79. package/dist/ws.d.ts +59 -25
  80. package/dist/ws.d.ts.map +1 -1
  81. package/dist/ws.js +19 -15
  82. package/dist/ws.js.map +1 -1
  83. package/package.json +1 -1
  84. package/src/basic.ts +136 -69
  85. package/src/blob.ts +1 -2
  86. package/src/content_menu.ts +125 -42
  87. package/src/datatable_bulk.ts +72 -35
  88. package/src/dialog.ts +156 -61
  89. package/src/diff-dom.d.ts +16 -0
  90. package/src/events.ts +3 -3
  91. package/src/faq_dialog.ts +25 -11
  92. package/src/file/dialog.ts +48 -14
  93. package/src/file/index.ts +0 -1
  94. package/src/file/new_folder_dialog.ts +7 -5
  95. package/src/file/selector.ts +86 -36
  96. package/src/file/templates.ts +2 -3
  97. package/src/file/tools.ts +17 -8
  98. package/src/focus.ts +11 -13
  99. package/src/global.d.ts +11 -4
  100. package/src/index.ts +0 -1
  101. package/src/network.ts +58 -20
  102. package/src/overview_menu.ts +182 -108
  103. package/src/settings.ts +9 -4
  104. package/src/user.ts +10 -5
  105. package/src/w3c-keyname.d.ts +3 -0
  106. package/src/worker.ts +1 -2
  107. package/src/ws.ts +115 -50
@@ -1,9 +1,11 @@
1
- // @ts-nocheck
2
1
  import {Dialog} from "../dialog.js"
3
2
  import {newFolderTemplate} from "./templates.js"
4
3
 
5
4
  export class NewFolderDialog {
6
- constructor(callback = _foldername => {}) {
5
+ callback: (folderName: string) => void
6
+ dialog: Dialog
7
+
8
+ constructor(callback: (folderName: string) => void = () => {}) {
7
9
  this.callback = callback
8
10
  this.dialog = new Dialog({
9
11
  title: gettext("New folder"),
@@ -18,9 +20,9 @@ export class NewFolderDialog {
18
20
  classes: "fw-dark",
19
21
  click: () => {
20
22
  const folderName =
21
- this.dialog.dialogEl.querySelector(
23
+ (this.dialog.dialogEl!.querySelector(
22
24
  "#new-folder-name"
23
- ).value
25
+ ) as HTMLInputElement).value
24
26
  this.dialog.close()
25
27
  if (!folderName.length) {
26
28
  return
@@ -32,7 +34,7 @@ export class NewFolderDialog {
32
34
  })
33
35
  }
34
36
 
35
- open() {
37
+ open(): void {
36
38
  return this.dialog.open()
37
39
  }
38
40
  }
@@ -1,18 +1,65 @@
1
- // @ts-nocheck
2
1
  import {escapeText, findTarget} from "../basic.js"
3
2
  import {ensureCSS} from "../network.js"
4
3
 
4
+ export interface FileDescriptor {
5
+ id?: number
6
+ title?: string
7
+ path: string
8
+ [key: string]: unknown
9
+ }
10
+
11
+ export interface FileSelectorFolder {
12
+ name: string
13
+ type: "folder"
14
+ open: boolean
15
+ selected: boolean
16
+ path: string
17
+ children: FileSelectorEntry[]
18
+ }
19
+
20
+ export interface FileSelectorFile {
21
+ name: string
22
+ type: "file"
23
+ path: string
24
+ selected?: boolean
25
+ file: FileDescriptor
26
+ }
27
+
28
+ export type FileSelectorEntry = FileSelectorFolder | FileSelectorFile
29
+
30
+ export interface FileSelectorOptions {
31
+ dom: HTMLElement
32
+ files: FileDescriptor[]
33
+ showFiles?: boolean
34
+ selectFolders?: boolean
35
+ multiSelect?: boolean
36
+ selectDir?: (path: string) => void
37
+ selectFile?: (path: string) => void
38
+ fileIcon?: string
39
+ }
40
+
5
41
  export class FileSelector {
42
+ dom: HTMLElement
43
+ files: FileDescriptor[]
44
+ showFiles: boolean
45
+ selectFolders: boolean
46
+ multiSelect: boolean
47
+ selectDir: (path: string) => void
48
+ selectFile: (path: string) => void
49
+ fileIcon: string
50
+ root: FileSelectorFolder
51
+ selected: FileSelectorEntry[]
52
+
6
53
  constructor({
7
54
  dom,
8
55
  files,
9
56
  showFiles = true,
10
57
  selectFolders = true,
11
58
  multiSelect = false,
12
- selectDir = _path => {},
13
- selectFile = _path => {},
59
+ selectDir = () => {},
60
+ selectFile = () => {},
14
61
  fileIcon = "far fa-file-alt"
15
- }) {
62
+ }: FileSelectorOptions) {
16
63
  this.dom = dom
17
64
  this.files = files
18
65
  this.showFiles = showFiles // Whether to show existing files or only folders
@@ -36,7 +83,7 @@ export class FileSelector {
36
83
  }
37
84
  }
38
85
 
39
- init() {
86
+ init(): void {
40
87
  this.readDirStructure()
41
88
  this.sortDirStructure()
42
89
  ensureCSS(staticUrl("css/file_selector.css"))
@@ -45,7 +92,7 @@ export class FileSelector {
45
92
  this.bind()
46
93
  }
47
94
 
48
- readDirStructure() {
95
+ readDirStructure(): void {
49
96
  // Read directory structure from existing file paths.
50
97
  // A file's title is used as the final path segment when the file has
51
98
  // no explicit folder path. We strip any "/" from that segment because
@@ -80,7 +127,7 @@ export class FileSelector {
80
127
  }
81
128
  let folder = treeWalker.find(
82
129
  item => item.name === pathPart && item.type === "folder"
83
- )
130
+ ) as FileSelectorFolder | undefined
84
131
  if (!folder) {
85
132
  folder = {
86
133
  name: pathPart,
@@ -97,7 +144,7 @@ export class FileSelector {
97
144
  })
98
145
  }
99
146
 
100
- sortDirStructure(entries = this.root.children) {
147
+ sortDirStructure(entries = this.root.children): void {
101
148
  entries.sort((a, b) => {
102
149
  if (a.type !== b.type) {
103
150
  return a.type === "folder" ? -1 : 1
@@ -111,32 +158,33 @@ export class FileSelector {
111
158
  })
112
159
  }
113
160
 
114
- addFolder(rawName) {
161
+ addFolder(rawName: string): void {
115
162
  const name = rawName.replace(/\//g, "")
116
163
  // Add a new folder as a subfolder to the currently selected folder
117
164
  if (
118
165
  !this.selected.length ||
119
166
  this.selected[0].type !== "folder" ||
120
- this.selected[0].children.find(
167
+ (this.selected[0] as FileSelectorFolder).children.find(
121
168
  child => child.type === "folder" && child.name === name
122
169
  )
123
170
  ) {
124
171
  // A file is selected. Give up.
125
172
  return
126
173
  }
127
- const newFolder = {
174
+ const selectedFolder = this.selected[0] as FileSelectorFolder
175
+ const newFolder: FileSelectorFolder = {
128
176
  name,
129
177
  type: "folder",
130
178
  open: true,
131
179
  selected: true,
132
- path: this.selected[0].path + name + "/",
180
+ path: selectedFolder.path + name + "/",
133
181
  children: []
134
182
  }
135
- this.selected[0].children.push(newFolder)
136
- this.sortDirStructure(this.selected[0].children)
137
- this.selected[0].open = true
183
+ selectedFolder.children.push(newFolder)
184
+ this.sortDirStructure(selectedFolder.children)
185
+ selectedFolder.open = true
138
186
  if (!this.multiSelect) {
139
- this.selected[0].selected = false
187
+ selectedFolder.selected = false
140
188
  this.selected = []
141
189
  }
142
190
  this.selected.push(newFolder)
@@ -144,17 +192,17 @@ export class FileSelector {
144
192
  this.render()
145
193
  }
146
194
 
147
- deselectAll() {
195
+ deselectAll(): void {
148
196
  this.selected.forEach(entry => (entry.selected = false))
149
197
  this.selected = []
150
198
  this.render()
151
199
  }
152
200
 
153
- render() {
201
+ render(): void {
154
202
  this.dom.innerHTML = this.renderFolder(this.root)
155
203
  }
156
204
 
157
- renderFolder(folder, indentLevel = 0) {
205
+ renderFolder(folder: FileSelectorFolder, indentLevel = 0): string {
158
206
  let returnString = ""
159
207
  returnString += `<div class="folder${folder.open ? "" : " closed"}">`
160
208
  returnString += `<p style="margin-left:${indentLevel * 10}px;">${
@@ -179,42 +227,44 @@ export class FileSelector {
179
227
  return returnString
180
228
  }
181
229
 
182
- findEntry(dom) {
183
- const searchPath = []
184
- let seekItem = dom
185
- while (seekItem.closest("div.folder, p.file")) {
230
+ findEntry(dom: Element): FileSelectorEntry {
231
+ const searchPath: number[] = []
232
+ let seekItem: Element | null = dom
233
+ let closest = seekItem.closest("div.folder, p.file")
234
+ while (closest) {
235
+ seekItem = closest
186
236
  let itemNumber = 0
187
- seekItem = seekItem.closest("div.folder, p.file")
188
237
  while (seekItem.previousElementSibling) {
189
238
  itemNumber++
190
239
  seekItem = seekItem.previousElementSibling
191
240
  }
192
241
  searchPath.push(itemNumber)
193
242
  seekItem = seekItem.parentElement
243
+ closest = seekItem?.closest("div.folder, p.file") ?? null
194
244
  }
195
- let entry = this.root
245
+ let entry: FileSelectorEntry = this.root
196
246
  searchPath.pop()
197
247
  while (searchPath.length) {
198
- entry = entry.children[searchPath.pop()]
248
+ entry = (entry as FileSelectorFolder).children[searchPath.pop()!]
199
249
  }
200
250
  return entry
201
251
  }
202
252
 
203
- bind() {
253
+ bind(): void {
204
254
  this.dom.addEventListener("click", event => {
205
- const el = {}
255
+ const el: {target?: Element | null} = {}
206
256
  switch (true) {
207
257
  case findTarget(event, ".fa-plus-square", el): {
208
258
  event.preventDefault()
209
- const entry = this.findEntry(el.target)
210
- entry.open = true
259
+ const entry = this.findEntry(el.target!)
260
+ ;(entry as FileSelectorFolder).open = true
211
261
  this.render()
212
262
  break
213
263
  }
214
264
  case findTarget(event, ".fa-minus-square", el): {
215
265
  event.preventDefault()
216
- const entry = this.findEntry(el.target)
217
- entry.open = false
266
+ const entry = this.findEntry(el.target!)
267
+ ;(entry as FileSelectorFolder).open = false
218
268
  this.render()
219
269
  break
220
270
  }
@@ -224,7 +274,7 @@ export class FileSelector {
224
274
  // Folders cannot be selected
225
275
  return
226
276
  }
227
- const entry = this.findEntry(el.target)
277
+ const entry = this.findEntry(el.target!)
228
278
  if (this.selected.includes(entry)) {
229
279
  entry.selected = false
230
280
  this.selected = this.selected.filter(e => e !== entry)
@@ -236,13 +286,13 @@ export class FileSelector {
236
286
  }
237
287
  this.selected.push(entry)
238
288
  this.render()
239
- this.selectDir(entry.path)
289
+ this.selectDir((entry as FileSelectorFolder).path)
240
290
  }
241
291
  break
242
292
  }
243
293
  case findTarget(event, ".file-name", el): {
244
294
  event.preventDefault()
245
- const entry = this.findEntry(el.target)
295
+ const entry = this.findEntry(el.target!)
246
296
  if (this.selected.includes(entry)) {
247
297
  entry.selected = false
248
298
  this.selected = this.selected.filter(e => e !== entry)
@@ -254,7 +304,7 @@ export class FileSelector {
254
304
  }
255
305
  this.selected.push(entry)
256
306
  this.render()
257
- this.selectFile(entry.path)
307
+ this.selectFile((entry as FileSelectorFile).path)
258
308
  }
259
309
  break
260
310
  }
@@ -1,12 +1,11 @@
1
- // @ts-nocheck
2
1
  import {escapeText} from "../index.js"
3
2
 
4
- export const moveTemplate = ({path}) =>
3
+ export const moveTemplate = ({path}: {path: string}): string =>
5
4
  `<div>
6
5
  <span>${gettext("Path")}:</span>
7
6
  <input type="text" value="${escapeText(path)}" id="path" placeholder="${gettext("Insert path")}">
8
7
  <div class="file-selector"></div>
9
8
  </div>`
10
9
 
11
- export const newFolderTemplate = () =>
10
+ export const newFolderTemplate = (): string =>
12
11
  `<div><input type="text" id="new-folder-name" placeholder="${gettext("Insert folder name")}"></div>`
package/src/file/tools.ts CHANGED
@@ -1,15 +1,18 @@
1
- // @ts-nocheck
2
1
  import {escapeText} from "../basic.js"
3
2
  import {postJson} from "../network.js"
4
3
 
5
- export const shortFileTitle = (title, path) => {
4
+ export const shortFileTitle = (title: string, path: string): string => {
6
5
  if (!path.length || path.endsWith("/")) {
7
6
  return escapeText(title || gettext("Untitled"))
8
7
  }
9
- return escapeText(path.split("/").pop())
8
+ return escapeText(path.split("/").pop() || "")
10
9
  }
11
10
 
12
- export const longFilePath = (title, path, prefix = "") => {
11
+ export const longFilePath = (
12
+ title: string,
13
+ path: string,
14
+ prefix = ""
15
+ ): string => {
13
16
  if (!path.length) {
14
17
  path = "/"
15
18
  }
@@ -26,7 +29,7 @@ export const longFilePath = (title, path, prefix = "") => {
26
29
  return path
27
30
  }
28
31
 
29
- export const cleanPath = (title, path) => {
32
+ export const cleanPath = (title: string, path: string): string => {
30
33
  if (!path.startsWith("/")) {
31
34
  path = "/" + path
32
35
  }
@@ -45,14 +48,20 @@ export const cleanPath = (title, path) => {
45
48
  return path
46
49
  }
47
50
 
48
- export const moveFile = (fileId, title, path, moveUrl) => {
51
+ export const moveFile = (
52
+ fileId: number,
53
+ title: string,
54
+ path: string,
55
+ moveUrl: string
56
+ ): Promise<string> => {
49
57
  path = cleanPath(title, path)
50
58
  return new Promise((resolve, reject) => {
51
59
  postJson(moveUrl, {id: fileId, path}).then(({json}) => {
52
- if (json.done) {
60
+ const response = json as {done?: boolean}
61
+ if (response.done) {
53
62
  resolve(path)
54
63
  } else {
55
- reject()
64
+ reject(new Error("Could not move file"))
56
65
  }
57
66
  })
58
67
  })
package/src/focus.ts CHANGED
@@ -1,21 +1,19 @@
1
- // @ts-nocheck
2
- // Get the index number of currently focused selement. This it to set tyhe focus close by after doing some dom changes.
1
+ const FOCUSABLE_SELECTOR =
2
+ "a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex], [contenteditable]"
3
3
 
4
- export const getFocusIndex = () => {
5
- return Array.from(
6
- document.querySelectorAll(
7
- "a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex], [contenteditable]"
8
- )
9
- ).findIndex(el => el === document.activeElement)
4
+ // Get the index number of currently focused element. This is to set the focus close by after doing some dom changes.
5
+
6
+ export const getFocusIndex = (): number => {
7
+ return Array.from(document.querySelectorAll(FOCUSABLE_SELECTOR)).findIndex(
8
+ el => el === document.activeElement
9
+ )
10
10
  }
11
11
 
12
- export const setFocusIndex = index => {
12
+ export const setFocusIndex = (index: number): void => {
13
13
  const focusableElements = Array.from(
14
- document.querySelectorAll(
15
- "a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex], [contenteditable]"
16
- )
14
+ document.querySelectorAll(FOCUSABLE_SELECTOR)
17
15
  )
18
16
  if (index >= 0 && index < focusableElements.length) {
19
- focusableElements[index].focus()
17
+ (focusableElements[index] as HTMLElement).focus()
20
18
  }
21
19
  }
package/src/global.d.ts CHANGED
@@ -1,11 +1,18 @@
1
1
  // Globals provided by the Fidus Writer host page.
2
2
 
3
- declare const gettext: (msgid: string) => string
3
+ declare function gettext(msgid: string): string
4
4
 
5
- declare const staticUrl: (path: string) => string
5
+ declare function interpolate(
6
+ fmt: string,
7
+ args: unknown[],
8
+ named?: boolean
9
+ ): string
6
10
 
7
- declare const settings: Record<string, any>
11
+ declare function staticUrl(path: string): string
12
+
13
+ declare const settings: Record<string, unknown>
8
14
 
9
15
  interface Window {
10
- settings?: Record<string, any>
16
+ settings?: Record<string, unknown>
17
+ csrfToken?: string
11
18
  }
package/src/index.ts CHANGED
@@ -1,4 +1,3 @@
1
- // @ts-nocheck
2
1
  export {OverviewMenuView} from "./overview_menu.js"
3
2
  export {
4
3
  dropdownSelect,
package/src/network.ts CHANGED
@@ -1,10 +1,21 @@
1
- // @ts-nocheck
2
1
  import {getSettings} from "./settings.js"
3
2
 
3
+ export interface FileUploadValue {
4
+ file: Blob
5
+ filename: string
6
+ }
7
+
8
+ export type PostFiles = Record<string, Blob | File | FileUploadValue | Blob[] | File[] | string[] | string>
9
+
10
+ export interface PostOptions {
11
+ csrfToken?: string
12
+ keepalive?: boolean
13
+ }
14
+
4
15
  /** Get cookie to set as part of the request header of all AJAX requests to the server.
5
16
  * @param name The name of the token to look for in the cookie.
6
17
  */
7
- export const getCookie = name => {
18
+ export const getCookie = (name: string): string | null => {
8
19
  if (!document.cookie || document.cookie === "") {
9
20
  return null
10
21
  }
@@ -25,18 +36,20 @@ export const getCookie = name => {
25
36
  }
26
37
 
27
38
  /* from https://www.tjvantoll.com/2015/09/13/fetch-and-errors/ */
28
- const handleFetchErrors = response => {
39
+ const handleFetchErrors = (response: Response): Response => {
29
40
  if (!response.ok) {
30
41
  throw response
31
42
  }
32
43
  return response
33
44
  }
34
45
 
35
- export const get = (url, params = {}, csrfToken = false) => {
46
+ export const get = (
47
+ url: string,
48
+ params: Record<string, string> = {},
49
+ csrfToken: string | false = false
50
+ ): Promise<Response> => {
36
51
  const settings = getSettings()
37
- if (!csrfToken) {
38
- csrfToken = settings.getCsrfToken() // Won't work in web worker.
39
- }
52
+ const token = csrfToken || settings.getCsrfToken() // Won't work in web worker.
40
53
  const queryString = Object.keys(params)
41
54
  .map(
42
55
  key =>
@@ -49,7 +62,7 @@ export const get = (url, params = {}, csrfToken = false) => {
49
62
  return fetch(settings.apiUrl(url), {
50
63
  method: "GET",
51
64
  headers: {
52
- "X-CSRFToken": csrfToken,
65
+ "X-CSRFToken": token,
53
66
  Accept: "application/json",
54
67
  "X-Requested-With": "XMLHttpRequest"
55
68
  },
@@ -57,16 +70,24 @@ export const get = (url, params = {}, csrfToken = false) => {
57
70
  }).then(handleFetchErrors)
58
71
  }
59
72
 
60
- export const getJson = (url, params = {}, csrfToken = false) =>
61
- get(url, params, csrfToken).then(response => response.json())
73
+ export const getJson = (
74
+ url: string,
75
+ params: Record<string, string> = {},
76
+ csrfToken: string | false = false
77
+ ): Promise<unknown> => get(url, params, csrfToken).then(response => response.json())
62
78
 
63
- export const postBare = (url, object = {}, files = {}, options = {}) => {
79
+ export const postBare = (
80
+ url: string,
81
+ object: Record<string, unknown> = {},
82
+ files: PostFiles = {},
83
+ options: PostOptions = {}
84
+ ): Promise<Response> => {
64
85
  const settings = getSettings()
65
86
 
66
87
  const {csrfToken: csrfTokenOpt, keepalive = false} = options
67
88
  const csrfToken = csrfTokenOpt || settings.getCsrfToken() // Won't work in web worker.
68
89
 
69
- const fetchOptions = {
90
+ const fetchOptions: RequestInit = {
70
91
  method: "POST",
71
92
  headers: {
72
93
  "X-CSRFToken": csrfToken,
@@ -83,34 +104,51 @@ export const postBare = (url, object = {}, files = {}, options = {}) => {
83
104
  body.append("json", JSON.stringify(object))
84
105
  Object.keys(files).forEach(key => {
85
106
  const value = files[key]
86
- if (typeof value === "object" && value.file && value.filename) {
87
- body.append(key, value.file, value.filename)
107
+ if (
108
+ typeof value === "object" &&
109
+ value !== null &&
110
+ "file" in value &&
111
+ "filename" in value
112
+ ) {
113
+ const uploadValue = value as FileUploadValue
114
+ body.append(key, uploadValue.file, uploadValue.filename)
88
115
  } else if (Array.isArray(value)) {
89
116
  value.forEach(item => body.append(`${key}[]`, item))
90
- } else {
91
- body.append(key, value)
117
+ } else if (value !== undefined && value !== null) {
118
+ body.append(key, value as Blob | string)
92
119
  }
93
120
  })
94
121
  fetchOptions.body = body
95
122
  } else {
96
- fetchOptions.headers["Content-Type"] = "application/json"
123
+ ;(fetchOptions.headers as Record<string, string>)["Content-Type"] =
124
+ "application/json"
97
125
  fetchOptions.body = JSON.stringify(object)
98
126
  }
99
127
 
100
128
  return fetch(settings.apiUrl(url), fetchOptions)
101
129
  }
102
130
 
103
- export const post = (url, object = {}, files = {}, options = {}) => {
131
+ export const post = (
132
+ url: string,
133
+ object: Record<string, unknown> = {},
134
+ files: PostFiles = {},
135
+ options: PostOptions = {}
136
+ ): Promise<Response> => {
104
137
  return postBare(url, object, files, options).then(handleFetchErrors)
105
138
  }
106
139
 
107
140
  // post json object and return json and status
108
- export const postJson = (url, object = {}, files = {}, options = {}) =>
141
+ export const postJson = (
142
+ url: string,
143
+ object: Record<string, unknown> = {},
144
+ files: PostFiles = {},
145
+ options: PostOptions = {}
146
+ ): Promise<{json: unknown; status: number}> =>
109
147
  post(url, object, files, options).then(response =>
110
148
  response.json().then(json => ({json, status: response.status}))
111
149
  )
112
150
 
113
- export const ensureCSS = cssUrl => {
151
+ export const ensureCSS = (cssUrl: string | string[]): boolean | void => {
114
152
  if (typeof cssUrl === "object") {
115
153
  cssUrl.forEach(url => ensureCSS(url))
116
154
  return