bt-core-app 0.0.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/.vscode/extensions.json +3 -0
- package/README.md +45 -0
- package/index.html +13 -0
- package/package.json +39 -0
- package/public/vite.svg +1 -0
- package/src/assets/vue.svg +1 -0
- package/src/components/BT-Btn.vue +40 -0
- package/src/components/BT-Col.vue +36 -0
- package/src/components/BT-Span.vue +21 -0
- package/src/components/Dialog-Confirm.vue +47 -0
- package/src/components/Dialog-Select-Date.vue +89 -0
- package/src/components/Dialog-Select.vue +140 -0
- package/src/components/Dialog-Text.vue +59 -0
- package/src/composables/actions-tracker.ts +99 -0
- package/src/composables/actions.ts +354 -0
- package/src/composables/api.ts +471 -0
- package/src/composables/auth.ts +382 -0
- package/src/composables/cosmetics.ts +178 -0
- package/src/composables/csv.ts +198 -0
- package/src/composables/dates.ts +79 -0
- package/src/composables/demo.ts +25 -0
- package/src/composables/dialogs.ts +115 -0
- package/src/composables/document-meta.ts +40 -0
- package/src/composables/draggable.ts +189 -0
- package/src/composables/filters.ts +256 -0
- package/src/composables/forage.ts +49 -0
- package/src/composables/helpers.ts +694 -0
- package/src/composables/id.ts +20 -0
- package/src/composables/list.ts +601 -0
- package/src/composables/navigation.ts +214 -0
- package/src/composables/presets.ts +20 -0
- package/src/composables/pwa.ts +89 -0
- package/src/composables/resizable.ts +382 -0
- package/src/composables/rules.ts +40 -0
- package/src/composables/stores.ts +797 -0
- package/src/composables/track.ts +55 -0
- package/src/composables/urls.ts +11 -0
- package/src/core.ts +92 -0
- package/src/index.ts +16 -0
- package/src/types.ts +13 -0
- package/src/useApi.ts +68 -0
- package/src/vite-env.d.ts +1 -0
- package/test/api.test.ts +84 -0
- package/test/forage.test.ts +31 -0
- package/test/helpers.test.ts +231 -0
- package/test/navigation.test.ts +99 -0
- package/test/stores-last-update.test.ts +138 -0
- package/test/stores-session.test.ts +118 -0
- package/test/track.test.ts +29 -0
- package/test/utils.ts +15 -0
- package/tsconfig.json +33 -0
- package/tsconfig.node.json +11 -0
- package/vite.config.ts +19 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import { toValue } from 'vue'
|
|
2
|
+
import { appendUrl } from '@/composables/helpers'
|
|
3
|
+
import { type BTAuth } from './auth'
|
|
4
|
+
|
|
5
|
+
export interface QueryParams {
|
|
6
|
+
filterBy?: string,
|
|
7
|
+
includeCount?: boolean,
|
|
8
|
+
includeDetails?: boolean,
|
|
9
|
+
includeInactive?: boolean,
|
|
10
|
+
lastUpdate?: string,
|
|
11
|
+
other?: any //for other random params
|
|
12
|
+
properties?: string[] | string,
|
|
13
|
+
query?: string,
|
|
14
|
+
searchString?: string,
|
|
15
|
+
sortOrder?: string,
|
|
16
|
+
sortBy?: string,
|
|
17
|
+
takeFrom?: number,
|
|
18
|
+
takeAmount?: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PathOptions {
|
|
22
|
+
/**always added to end before query {url}/{additionalUrl}?{query} */
|
|
23
|
+
additionalUrl?: string
|
|
24
|
+
/**application/json or other options */
|
|
25
|
+
contentType?: string
|
|
26
|
+
/**that data to send in the body for POST, PATCH, and DELETE requests */
|
|
27
|
+
data?: any
|
|
28
|
+
/** resulting url after building {nav | url}/{additionalUrl}?{params} */
|
|
29
|
+
finalUrl?: string,
|
|
30
|
+
/**override default headers */
|
|
31
|
+
headers?: HeadersInit
|
|
32
|
+
/**id will be added as a url paramter (/{id}?) rather than as a query parameter */
|
|
33
|
+
id?: string
|
|
34
|
+
/**where to find the url */
|
|
35
|
+
nav?: string
|
|
36
|
+
/**headers will not be calculated. Only the given headers in these options will be used */
|
|
37
|
+
overrideHeaders?: boolean
|
|
38
|
+
/**query parameters */
|
|
39
|
+
params?: QueryParams
|
|
40
|
+
/**attaches a proxyID to the request */
|
|
41
|
+
proxyID?: string
|
|
42
|
+
/**whether to refresh the store data and go straight to the server */
|
|
43
|
+
refresh?: boolean
|
|
44
|
+
/**returns result as a json object. Defaults to true. */
|
|
45
|
+
returnJson?: boolean
|
|
46
|
+
/**returns result as a string */
|
|
47
|
+
returnText?: boolean
|
|
48
|
+
/**If false, returns response no matter the status code */
|
|
49
|
+
throwError?: boolean
|
|
50
|
+
/**if exists then overrides default nav */
|
|
51
|
+
url?: string
|
|
52
|
+
/**whether to preference using the local cache */
|
|
53
|
+
useLocalCache?: boolean
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type FindPath = (navName?: string) => string | undefined
|
|
57
|
+
// type BuildHeaders = (path: PathOptions) => HeadersInit
|
|
58
|
+
// type BuildQuery = (params: any) => string
|
|
59
|
+
// type BuildUrl = (path: PathOptions) => string
|
|
60
|
+
|
|
61
|
+
interface UseApiOptions {
|
|
62
|
+
auth?: BTAuth
|
|
63
|
+
/**overrides the default */
|
|
64
|
+
buildHeaders?: (path: PathOptions) => HeadersInit
|
|
65
|
+
/**build a query. Overrides the default */
|
|
66
|
+
buildQuery?: (params: any) => string
|
|
67
|
+
/**overrides the default */
|
|
68
|
+
buildUrl?: (path: PathOptions) => string
|
|
69
|
+
/**defaults to 'application/json' */
|
|
70
|
+
defaultContentType?: string
|
|
71
|
+
/**returns result as a json object. Defaults to true. */
|
|
72
|
+
defaultReturnJson?: boolean
|
|
73
|
+
/**returns result as a string */
|
|
74
|
+
defaultReturnText?: boolean
|
|
75
|
+
/**defaults to true. Will throw an error on fail */
|
|
76
|
+
defaultThrowError?: boolean
|
|
77
|
+
/**defaults to a function that returns '' */
|
|
78
|
+
findPath?: FindPath
|
|
79
|
+
/**if true and logged in then will set an authorization header with 'bearer [token]' */
|
|
80
|
+
useBearerToken?: boolean
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface BTApi {
|
|
84
|
+
buildHeaders: (options: PathOptions) => HeadersInit
|
|
85
|
+
buildQuery: (params: any) => string
|
|
86
|
+
buildUrl: (path: PathOptions) => string
|
|
87
|
+
deleteItem: (pathOptions: PathOptions) => Promise<string | undefined>
|
|
88
|
+
get: <T>(pathOptions: PathOptions) => Promise<T>
|
|
89
|
+
getAll: <T>(pathOptions: PathOptions) => Promise<T>
|
|
90
|
+
post: <T>(pathOptions: PathOptions) => Promise<T | undefined>
|
|
91
|
+
patch: <T>(pathOptions: PathOptions) => Promise<T | undefined>
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function createApi(options?: UseApiOptions): BTApi {
|
|
95
|
+
const buildHeaders = options?.buildHeaders ?? defaultBuildHeaders
|
|
96
|
+
const buildQuery = options?.buildQuery ?? defaultBuildQuery
|
|
97
|
+
const buildUrl = options?.buildUrl ?? defaultBuildUrl
|
|
98
|
+
|
|
99
|
+
function defaultBuildQuery(params: QueryParams): string {
|
|
100
|
+
let query = new URLSearchParams()
|
|
101
|
+
|
|
102
|
+
Object.entries(params).forEach(x => {
|
|
103
|
+
if (x[1] != undefined) {
|
|
104
|
+
query.append(x[0], x[1])
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
if (params.filterBy) {
|
|
109
|
+
query.set('filterBy', params.filterBy)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (params.includeCount)
|
|
113
|
+
query.set('includeCount', params.includeCount.toString())
|
|
114
|
+
|
|
115
|
+
if (params.includeDetails != undefined)
|
|
116
|
+
query.set('includeDetails', params.includeDetails.toString())
|
|
117
|
+
|
|
118
|
+
if (params.includeInactive)
|
|
119
|
+
query.set('includeInactive', params.includeInactive.toString())
|
|
120
|
+
|
|
121
|
+
if (params.properties)
|
|
122
|
+
query.set('properties', params.properties.toString())
|
|
123
|
+
|
|
124
|
+
if (params.query)
|
|
125
|
+
query.set('query', params.query.toString())
|
|
126
|
+
|
|
127
|
+
if (params.searchString)
|
|
128
|
+
query.set('searchString', params.searchString)
|
|
129
|
+
|
|
130
|
+
if (params.sortOrder)
|
|
131
|
+
query.set('sortOrder', params.sortOrder)
|
|
132
|
+
|
|
133
|
+
if (params.sortBy)
|
|
134
|
+
query.set('sortBy', params.sortBy)
|
|
135
|
+
|
|
136
|
+
if (params.takeFrom)
|
|
137
|
+
query.set('takeFrom', params.takeFrom.toString())
|
|
138
|
+
|
|
139
|
+
if (params.takeAmount)
|
|
140
|
+
query.set('takeAmount', params.takeAmount.toString())
|
|
141
|
+
|
|
142
|
+
if (params.other) {
|
|
143
|
+
Object.entries(params.other).forEach(x => {
|
|
144
|
+
if (x[1] != undefined) {
|
|
145
|
+
query.append(x[0], x[1].toString())
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return query.toString()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**returns path and sets final url */
|
|
154
|
+
function defaultBuildUrl(path: PathOptions) {
|
|
155
|
+
let url: string | undefined = toValue(path.url) ?? undefined
|
|
156
|
+
let id = toValue(path.id)
|
|
157
|
+
|
|
158
|
+
if (url == null && path.nav != null && options?.findPath != null)
|
|
159
|
+
url = options.findPath(path.nav)
|
|
160
|
+
|
|
161
|
+
if (path.additionalUrl != null) {
|
|
162
|
+
if (url == null)
|
|
163
|
+
url = path.additionalUrl
|
|
164
|
+
else
|
|
165
|
+
url = appendUrl(url, path.additionalUrl)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (id != null) {
|
|
169
|
+
if (url?.includes('{id}'))
|
|
170
|
+
url = url.replaceAll('{id}', id)
|
|
171
|
+
else
|
|
172
|
+
url = appendUrl(url, id)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (path.params != null)
|
|
176
|
+
url = `${url}?${buildQuery(path.params)}`
|
|
177
|
+
|
|
178
|
+
path.finalUrl = url
|
|
179
|
+
|
|
180
|
+
return path.finalUrl ?? ''
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function defaultBuildHeaders(path: PathOptions): HeadersInit {
|
|
184
|
+
let fetchOptions:any = { ...path.headers }
|
|
185
|
+
const auth = options?.auth?.credentials
|
|
186
|
+
|
|
187
|
+
if (path.proxyID)
|
|
188
|
+
fetchOptions.ManagedCompanyAccountID ??= path.proxyID
|
|
189
|
+
|
|
190
|
+
if (options?.useBearerToken != false && auth?.isLoggedIn == true)
|
|
191
|
+
fetchOptions.authorization ??= `bearer ${auth.token}`
|
|
192
|
+
|
|
193
|
+
fetchOptions['Content-Type'] ??= (path.contentType ?? options?.defaultContentType ?? 'application/json')
|
|
194
|
+
|
|
195
|
+
return fetchOptions
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function get<T>(pathOptions: PathOptions): Promise<T> {
|
|
199
|
+
const throwError = pathOptions.throwError ?? options?.defaultThrowError ?? true
|
|
200
|
+
const returnJson = pathOptions.returnJson ?? options?.defaultReturnJson ?? true
|
|
201
|
+
const returnText = pathOptions.returnText ?? options?.defaultReturnText ?? false
|
|
202
|
+
|
|
203
|
+
let url = pathOptions.finalUrl
|
|
204
|
+
let headers = pathOptions.headers
|
|
205
|
+
|
|
206
|
+
if (url == null)
|
|
207
|
+
url = buildUrl(pathOptions)
|
|
208
|
+
|
|
209
|
+
if (pathOptions.overrideHeaders !== true)
|
|
210
|
+
headers = buildHeaders(pathOptions)
|
|
211
|
+
|
|
212
|
+
console.log(`Get from ${url}`)
|
|
213
|
+
|
|
214
|
+
let res: any = undefined
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
res = await fetch(url, {
|
|
218
|
+
method: 'GET',
|
|
219
|
+
mode: 'cors',
|
|
220
|
+
cache: 'no-cache',
|
|
221
|
+
headers
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// if (!throwError) {
|
|
225
|
+
// return res
|
|
226
|
+
// }
|
|
227
|
+
|
|
228
|
+
if (!res.ok) {
|
|
229
|
+
let errorContent = await res.text()
|
|
230
|
+
throw new Error(errorContent ?? res.statusText ?? 'Get error')
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (returnText)
|
|
234
|
+
return await res.text()
|
|
235
|
+
if (returnJson)
|
|
236
|
+
return await res.json()
|
|
237
|
+
|
|
238
|
+
return res
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
if (!throwError) {
|
|
242
|
+
return res
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
throw new Error(err as string)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function getAll<T>(pathOptions: PathOptions): Promise<T> {
|
|
250
|
+
const throwError = pathOptions.throwError ?? options?.defaultThrowError ?? true
|
|
251
|
+
const returnJson = pathOptions.returnJson ?? options?.defaultReturnJson ?? true
|
|
252
|
+
const returnText = pathOptions.returnText ?? options?.defaultReturnText ?? false
|
|
253
|
+
|
|
254
|
+
let url = pathOptions.finalUrl
|
|
255
|
+
let headers = pathOptions.headers
|
|
256
|
+
|
|
257
|
+
if (url == null)
|
|
258
|
+
url = buildUrl(pathOptions)
|
|
259
|
+
|
|
260
|
+
if (pathOptions.overrideHeaders !== true)
|
|
261
|
+
headers = buildHeaders(pathOptions)
|
|
262
|
+
|
|
263
|
+
console.log(`Get all from ${url}`)
|
|
264
|
+
|
|
265
|
+
let res: any = undefined
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
res = await fetch(url, {
|
|
269
|
+
method: 'GET',
|
|
270
|
+
mode: 'cors',
|
|
271
|
+
cache: 'no-cache',
|
|
272
|
+
headers
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// if (!throwError) {
|
|
276
|
+
// return res
|
|
277
|
+
// }
|
|
278
|
+
|
|
279
|
+
if (!res.ok) {
|
|
280
|
+
let errorContent = await res.text()
|
|
281
|
+
throw new Error(errorContent ?? res.statusText ?? 'Get all error')
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (returnText)
|
|
285
|
+
return await res.text()
|
|
286
|
+
if (returnJson)
|
|
287
|
+
return await res.json()
|
|
288
|
+
|
|
289
|
+
return res
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
if (!throwError) {
|
|
293
|
+
return res
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
throw new Error(err as string)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function post<T>(pathOptions: PathOptions): Promise<T | undefined> {
|
|
302
|
+
const throwError = pathOptions.throwError ?? options?.defaultThrowError ?? true
|
|
303
|
+
const returnJson = pathOptions.returnJson ?? options?.defaultReturnJson ?? true
|
|
304
|
+
const returnText = pathOptions.returnText ?? options?.defaultReturnText ?? false
|
|
305
|
+
|
|
306
|
+
let url = pathOptions.finalUrl
|
|
307
|
+
let headers = pathOptions.headers
|
|
308
|
+
|
|
309
|
+
if (url == null)
|
|
310
|
+
url = buildUrl(pathOptions)
|
|
311
|
+
|
|
312
|
+
if (pathOptions.overrideHeaders !== true)
|
|
313
|
+
headers = buildHeaders(pathOptions)
|
|
314
|
+
|
|
315
|
+
console.log(`Post to ${url}`)
|
|
316
|
+
|
|
317
|
+
let res: any = undefined
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
res = await fetch(url, {
|
|
321
|
+
method: 'POST',
|
|
322
|
+
mode: 'cors',
|
|
323
|
+
cache: 'no-cache',
|
|
324
|
+
headers,
|
|
325
|
+
body: JSON.stringify(pathOptions.data)
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
// if (!throwError) {
|
|
329
|
+
// return res
|
|
330
|
+
// }
|
|
331
|
+
|
|
332
|
+
if (!res.ok) {
|
|
333
|
+
let errorContent = await res.text()
|
|
334
|
+
throw new Error(errorContent ?? res.statusText ?? 'Post error!')
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (returnText)
|
|
338
|
+
return await res.text()
|
|
339
|
+
if (returnJson)
|
|
340
|
+
return await res.json()
|
|
341
|
+
|
|
342
|
+
return res
|
|
343
|
+
}
|
|
344
|
+
catch (err) {
|
|
345
|
+
if (!throwError) {
|
|
346
|
+
return res
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (res.status == 200)
|
|
350
|
+
return undefined
|
|
351
|
+
|
|
352
|
+
throw new Error(err as string)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function patch<T>(pathOptions: PathOptions): Promise<T | undefined> {
|
|
357
|
+
const throwError = pathOptions.throwError ?? options?.defaultThrowError ?? true
|
|
358
|
+
const returnJson = pathOptions.returnJson ?? options?.defaultReturnJson ?? true
|
|
359
|
+
const returnText = pathOptions.returnText ?? options?.defaultReturnText ?? false
|
|
360
|
+
|
|
361
|
+
let url = pathOptions.finalUrl
|
|
362
|
+
let headers = pathOptions.headers
|
|
363
|
+
|
|
364
|
+
if (url == null)
|
|
365
|
+
url = buildUrl(pathOptions)
|
|
366
|
+
|
|
367
|
+
if (pathOptions.overrideHeaders !== true)
|
|
368
|
+
headers = buildHeaders(pathOptions)
|
|
369
|
+
|
|
370
|
+
console.log(`Patch to ${url}`)
|
|
371
|
+
|
|
372
|
+
let res: any = undefined
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
res = await fetch(url, {
|
|
376
|
+
method: 'PATCH',
|
|
377
|
+
mode: 'cors',
|
|
378
|
+
cache: 'no-cache',
|
|
379
|
+
headers,
|
|
380
|
+
body: JSON.stringify(pathOptions.data)
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
// if (!throwError) {
|
|
384
|
+
// return res
|
|
385
|
+
// }
|
|
386
|
+
|
|
387
|
+
if (!res.ok) {
|
|
388
|
+
let errorContent = await res.text()
|
|
389
|
+
throw new Error(errorContent ?? res.statusText ?? 'Patch error!')
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (returnText)
|
|
393
|
+
return await res.text()
|
|
394
|
+
if (returnJson)
|
|
395
|
+
return await res.json()
|
|
396
|
+
|
|
397
|
+
return res
|
|
398
|
+
}
|
|
399
|
+
catch (err) {
|
|
400
|
+
if (!throwError) {
|
|
401
|
+
return res
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (res.status == 200)
|
|
405
|
+
return undefined
|
|
406
|
+
|
|
407
|
+
throw new Error(err as string)
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
*
|
|
413
|
+
* @param options
|
|
414
|
+
* returns an error msg if failed
|
|
415
|
+
*/
|
|
416
|
+
async function deleteItem(pathOptions: PathOptions): Promise<string | undefined> {
|
|
417
|
+
const throwError = pathOptions.throwError ?? options?.defaultThrowError ?? true
|
|
418
|
+
|
|
419
|
+
let url = pathOptions.finalUrl
|
|
420
|
+
let headers = pathOptions.headers
|
|
421
|
+
|
|
422
|
+
if (url == null)
|
|
423
|
+
url = buildUrl(pathOptions)
|
|
424
|
+
|
|
425
|
+
if (pathOptions.overrideHeaders !== true)
|
|
426
|
+
headers = buildHeaders(pathOptions)
|
|
427
|
+
|
|
428
|
+
console.log(`Delete ${url}`)
|
|
429
|
+
|
|
430
|
+
let res: any = undefined
|
|
431
|
+
|
|
432
|
+
try {
|
|
433
|
+
res = await fetch(url, {
|
|
434
|
+
method: 'DELETE',
|
|
435
|
+
mode: 'cors',
|
|
436
|
+
cache: 'no-cache',
|
|
437
|
+
headers,
|
|
438
|
+
body: JSON.stringify(pathOptions.data)
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
// if (!throwError) {
|
|
442
|
+
// return res
|
|
443
|
+
// }
|
|
444
|
+
|
|
445
|
+
if (res.status == 200) {
|
|
446
|
+
return undefined
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
return res.statusText
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
catch (err) {
|
|
453
|
+
if (!throwError) {
|
|
454
|
+
return res
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
throw new Error(err as string)
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
buildHeaders,
|
|
463
|
+
buildQuery,
|
|
464
|
+
buildUrl,
|
|
465
|
+
deleteItem,
|
|
466
|
+
get,
|
|
467
|
+
getAll,
|
|
468
|
+
patch,
|
|
469
|
+
post
|
|
470
|
+
}
|
|
471
|
+
}
|