@social-mail/social-mail-client 1.2.26 → 1.2.28

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 (39) hide show
  1. package/dist/admin/AdminAppIndex.pack.js +417 -196
  2. package/dist/admin/AdminAppIndex.pack.js.map +1 -1
  3. package/dist/admin/AdminAppIndex.pack.min.js +14 -7
  4. package/dist/admin/AdminAppIndex.pack.min.js.map +1 -1
  5. package/dist/common/FileUploader.d.ts +32 -0
  6. package/dist/common/FileUploader.d.ts.map +1 -0
  7. package/dist/common/FileUploader.js +133 -0
  8. package/dist/common/FileUploader.js.map +1 -0
  9. package/dist/model/model.d.ts +1 -0
  10. package/dist/model/model.d.ts.map +1 -1
  11. package/dist/model/model.js.map +1 -1
  12. package/dist/services/files/LocalFileService.d.ts +2 -0
  13. package/dist/services/files/LocalFileService.d.ts.map +1 -1
  14. package/dist/services/files/LocalFileService.js +66 -11
  15. package/dist/services/files/LocalFileService.js.map +1 -1
  16. package/dist/style/GlobalStyle.js +1 -0
  17. package/dist/style/GlobalStyle.js.map +1 -1
  18. package/dist/tsconfig.tsbuildinfo +1 -1
  19. package/dist/web/AppIndex.pack.js +417 -196
  20. package/dist/web/AppIndex.pack.js.map +1 -1
  21. package/dist/web/AppIndex.pack.min.js +14 -7
  22. package/dist/web/AppIndex.pack.min.js.map +1 -1
  23. package/dist/web/page/files/FolderPage.d.ts +1 -0
  24. package/dist/web/page/files/FolderPage.d.ts.map +1 -1
  25. package/dist/web/page/files/FolderPage.js +34 -11
  26. package/dist/web/page/files/FolderPage.js.map +1 -1
  27. package/node_modules/@web-atoms/core/dist/services/FetchBuilder.d.ts.map +1 -1
  28. package/node_modules/@web-atoms/core/dist/services/FetchBuilder.js +6 -0
  29. package/node_modules/@web-atoms/core/dist/services/FetchBuilder.js.map +1 -1
  30. package/node_modules/@web-atoms/core/dist/tsconfig.tsbuildinfo +1 -1
  31. package/node_modules/@web-atoms/core/index.js +89 -0
  32. package/node_modules/@web-atoms/core/package.json +1 -1
  33. package/node_modules/@web-atoms/core/src/services/FetchBuilder.ts +6 -0
  34. package/package.json +2 -2
  35. package/src/common/FileUploader.ts +140 -0
  36. package/src/model/model.ts +1 -0
  37. package/src/services/files/LocalFileService.tsx +70 -9
  38. package/src/style/GlobalStyle.ts +1 -0
  39. package/src/web/page/files/FolderPage.tsx +38 -13
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@web-atoms/core",
3
- "version": "2.4.1",
3
+ "version": "2.4.3",
4
4
  "description": "Web Atoms Core",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -57,6 +57,9 @@ export default class FetchBuilder {
57
57
  public form(name: string, value: string): FetchBuilder;
58
58
  public form(name: string, value: Blob, fileName: string): FetchBuilder;
59
59
  public form(name: string, value: string | Blob, fileName?: string ): FetchBuilder {
60
+ if (value === void 0) {
61
+ return this;
62
+ }
60
63
  const body = this.request.body as FormData ?? new FormData();
61
64
  if (fileName) {
62
65
  if (typeof value === "string") {
@@ -94,6 +97,9 @@ export default class FetchBuilder {
94
97
  }
95
98
 
96
99
  public query(name: string, value: any, encode = true) {
100
+ if (value === void 0) {
101
+ return this;
102
+ }
97
103
  let url = this.request.url;
98
104
  if (encode) {
99
105
  value = encodeURIComponent(value);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@social-mail/social-mail-client",
3
- "version": "1.2.26",
3
+ "version": "1.2.28",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -11,7 +11,7 @@
11
11
  "license": "ISC",
12
12
  "bundledDependencies": true,
13
13
  "dependencies": {
14
- "@web-atoms/core": "^2.4.1",
14
+ "@web-atoms/core": "^2.4.3",
15
15
  "@web-atoms/data-styles": "^1.0.115",
16
16
  "@web-atoms/date-time": "^1.1.0",
17
17
  "@web-atoms/email-dom": "^1.0.30",
@@ -0,0 +1,140 @@
1
+ /* eslint-disable no-console */
2
+ import { CancelToken } from "@web-atoms/core/dist/core/types";
3
+ import TaskManager from "@web-atoms/entity/dist/models/TaskManager";
4
+
5
+ export interface IUploadResult {
6
+ appFileID?: number;
7
+ url?: string;
8
+ downloadUrl?: string;
9
+ key?: string;
10
+ contentType?: string;
11
+ fileContentID?: number;
12
+ };
13
+
14
+ export interface IUploadRequest {
15
+ file: File;
16
+ done: number;
17
+ promise: Promise<IUploadResult>;
18
+ }
19
+
20
+ export interface IUploadRequestParams {
21
+ file: File,
22
+ url: string,
23
+ sha256?: string,
24
+ extra?: any,
25
+ folder?: string,
26
+ cancelToken?: CancelToken,
27
+ progress?: (n) => void
28
+ }
29
+
30
+ export default class FileUploader extends TaskManager {
31
+
32
+ rateLimit = 4;
33
+
34
+ upload(req: IUploadRequestParams) {
35
+ const { file, progress } = req;
36
+ const result = {
37
+ file,
38
+ done: 0,
39
+ promise: null
40
+ };
41
+ req.progress = (n) => {
42
+ result.done = n;
43
+ progress?.(n);
44
+ };
45
+ result.promise = this.run(() => this.tryUpload3(req));
46
+ return result as IUploadRequest;
47
+ }
48
+
49
+ private async sha256(blob: Blob) {
50
+ const msgBuffer = await blob.arrayBuffer();
51
+ const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
52
+
53
+ // convert ArrayBuffer to Array
54
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
55
+ // convert bytes to hex string
56
+ const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
57
+ return hashHex;
58
+ }
59
+
60
+
61
+ private async tryUpload3(req: IUploadRequestParams) {
62
+ let lastError;
63
+ if(!req.sha256) {
64
+ req.sha256 = await this.sha256(req.file);
65
+ }
66
+ for (let index = 0; index < 3; index++) {
67
+ try {
68
+ return await this.tryUpload(req);
69
+ } catch (error) {
70
+ lastError = error;
71
+ }
72
+ }
73
+ throw lastError;
74
+ }
75
+
76
+ private tryUpload({ file, url, sha256, extra, folder, cancelToken, progress }: IUploadRequestParams) {
77
+ return new Promise<IUploadResult>((resolve, reject) => {
78
+
79
+ const body = new FormData();
80
+ body.append(file.name, file, file.name);
81
+ body.append("sha256", sha256);
82
+ const xhr = new XMLHttpRequest();
83
+ const onError = () => {
84
+ reject(new Error(xhr.responseText || "Error"));
85
+ };
86
+ cancelToken?.registerForCancel(() => xhr.abort());
87
+ xhr.upload.addEventListener("progress", (event) => {
88
+ if (event.lengthComputable) {
89
+ try {
90
+ progress?.(event.loaded / event.total);
91
+ } catch (error) {
92
+ console.error(error);
93
+ }
94
+ }
95
+ });
96
+ xhr.upload.addEventListener("timeout", onError);
97
+ xhr.upload.addEventListener("error", onError);
98
+ xhr.upload.addEventListener("abort", onError);
99
+ xhr.addEventListener("timeout", onError);
100
+ xhr.addEventListener("abort", onError);
101
+ xhr.addEventListener("error", onError);
102
+ xhr.upload.addEventListener("progress", (event) => {
103
+ if (event.lengthComputable) {
104
+ try {
105
+ progress?.(event.loaded / event.total);
106
+ } catch (error) {
107
+ console.error(error);
108
+ }
109
+ }
110
+ });
111
+ xhr.addEventListener("load", () => {
112
+ if(xhr.status <= 300) {
113
+ const r = { ... JSON.parse(xhr.responseText), file};
114
+ resolve(r);
115
+ }
116
+ });
117
+
118
+ const s = new URLSearchParams();
119
+
120
+ if (extra) {
121
+ for (const key in extra) {
122
+ if (Object.prototype.hasOwnProperty.call(extra, key)) {
123
+ const element = extra[key];
124
+ if (element) {
125
+ s.append(key, element);
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ if (folder) {
132
+ s.append("folder", folder);
133
+ }
134
+
135
+ xhr.open("POST", url + "?" + s.toString(), true);
136
+ xhr.send(body);
137
+ });
138
+ }
139
+
140
+ }
@@ -29,6 +29,7 @@ export interface IAppFile extends IClrEntity {
29
29
  versions?: number | null;
30
30
  url?: string;
31
31
  downloadUrl?: string;
32
+ tempKeys?: string[];
32
33
  fileContent?: IFileContent;
33
34
  creator?: IUser;
34
35
  parent?: IAppFile;
@@ -13,6 +13,8 @@ import PopupService from "@web-atoms/core/dist/web/services/PopupService";
13
13
  import { AppFile, IAppFile } from "../../model/model";
14
14
  import TaskManager from "@web-atoms/entity/dist/models/TaskManager";
15
15
  import EntityService from "../EntityService";
16
+ import FetchBuilder from "@web-atoms/core/dist/services/FetchBuilder";
17
+ import FileUploader, { IUploadRequest, IUploadResult } from "../../common/FileUploader";
16
18
 
17
19
  export const acceptDocumentAndMedia = ".doc, .docx, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document, video/*, application/pdf, .pdf, .txt, audio/*, image/*, .csv, text/csv, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, text/plain, text/html";
18
20
 
@@ -65,6 +67,8 @@ export interface IFileUpload {
65
67
  @DISingleton()
66
68
  export default class LocalFileService extends TaskManager {
67
69
 
70
+ rateLimit = 4;
71
+
68
72
  @Inject
69
73
  private app: App;
70
74
 
@@ -101,16 +105,74 @@ export default class LocalFileService extends TaskManager {
101
105
  return hashHex;
102
106
  }
103
107
 
108
+ async uploadFileParts(file: File, extra, cancelToken?: CancelToken, progress?: (n) => void): Promise<IUploadedFile> {
109
+
110
+ let size = file.size;
111
+ const parts = [] as IUploadRequest[];
112
+ const tasks = [] as Promise<IUploadResult>[];
113
+ const maxSize = 4 * 1024 * 1024;
114
+ let start = 0;
115
+ const fud = new FileUploader();
116
+ while(size > 0) {
117
+ const blobSize = Math.min(maxSize, file.size - start);
118
+ const part = new File([file.slice( start, start + blobSize)], "file.dat");
119
+ size -= blobSize;
120
+ start += blobSize;
121
+ const uploadRequest = fud.upload({
122
+ url: "/api/tmp/upload",
123
+ file:part,
124
+ cancelToken,
125
+ progress: () => {
126
+ const done = (parts.reduce((pv, cv) => pv + cv.done, 0) || 0);
127
+ progress?.( done/ parts.length);
128
+ }
129
+ });
130
+ parts.push(uploadRequest);
131
+ tasks.push(uploadRequest.promise);
132
+ }
133
+ const tempUrls = await Promise.all(tasks);
134
+ const tempKeys = tempUrls.map((x) => x.key);
135
+ const name = file.name;
136
+ const { parentID, appFileID, folder } = extra;
137
+ const appFile = await FetchBuilder.post("/api/upload")
138
+ .form("name", name)
139
+ .form("tempKeys", JSON.stringify(tempKeys))
140
+ .form("contentType", file.type)
141
+ .query("parentID", parentID)
142
+ .query("appFileID", appFileID)
143
+ .query("folder", folder)
144
+ .responseAsJson<any>();
145
+ return {
146
+ url: appFile.url,
147
+ downloadUrl: appFile.url,
148
+ file,
149
+ contentType: appFile.contentType,
150
+ fileContentID: appFile.fileContentID,
151
+ appFileID: appFile.appFileID
152
+ };
153
+ }
154
+
104
155
  async uploadFile(file: File, extra: any, ct?: CancelToken, progress?: (n) => void): Promise<IUploadedFile> {
105
- const sha256 = await this.sha256(file);
156
+
157
+ let folder = void 0;
158
+
159
+ if (file.webkitRelativePath) {
160
+ const folders = file.webkitRelativePath.split("/");
161
+ folders.pop();
162
+ if (folders.length) {
163
+ folder = folders.join("/");
164
+ }
165
+ }
106
166
 
107
167
  // check if file exists???
108
168
  if (typeof extra === "string") {
109
169
  extra = JSON.parse(extra);
110
170
  }
111
171
 
172
+ const sha256 = await this.sha256(file);
173
+
112
174
  if (extra.parentID) {
113
- const existing = await this.entityService.query(AppFile, "children", extra.parentID, file.name, sha256)
175
+ const existing = await this.entityService.query(AppFile, "children", extra.parentID, folder ? folder + "/" + file.name : file.name, sha256)
114
176
  .firstOrDefault();
115
177
  if (existing) {
116
178
  const { appFileID, url } = existing;
@@ -122,6 +184,10 @@ export default class LocalFileService extends TaskManager {
122
184
  }
123
185
  }
124
186
 
187
+ if (file.size > 4 * 1024 * 1024) {
188
+ return await this.uploadFileParts(file, extra, ct, progress);
189
+ }
190
+
125
191
  return new Promise<IUploadedFile>((resolve, reject) => {
126
192
  const body = new FormData();
127
193
  body.append(file.name, file, file.name);
@@ -168,13 +234,8 @@ export default class LocalFileService extends TaskManager {
168
234
  }
169
235
  }
170
236
 
171
- if (file.webkitRelativePath) {
172
- const folders = file.webkitRelativePath.split("/");
173
- folders.pop();
174
- if (folders.length) {
175
- const folder = folders.join("/");
176
- s.append("folder", folder);
177
- }
237
+ if (folder) {
238
+ s.append("folder", folder);
178
239
  }
179
240
 
180
241
  postUrl += "?" + s.toString();
@@ -32,6 +32,7 @@ function addLinkTag(href, integrity?, rel = "stylesheet") {
32
32
  addLinkTag("https://fonts.googleapis.com/css2?family=Source+Sans+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&display=swap");
33
33
  addLinkTag("https://npm-git.azureedge.net/npm/package/@c8private/fa-icons@6.5.1/css/all.min.css");
34
34
  addLinkTag("https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0");
35
+ addLinkTag("https://cdn.jsdelivr.net/npm/file-icon-vectors@1.0.0/dist/file-icon-vivid.min.css");
35
36
 
36
37
  styled.css `
37
38
 
@@ -2,7 +2,7 @@ import { ContentPage } from "@web-atoms/web-controls/dist/mobile-app/MobileApp";
2
2
  import { AppFile, IAppFile } from "../../../model/model";
3
3
  import XNode from "@web-atoms/core/dist/core/XNode";
4
4
  import UploadEvent from "@web-atoms/web-controls/dist/basic/UploadEvent";
5
- import AtomRepeater, { SelectorCheckBox } from "@web-atoms/web-controls/dist/basic/AtomRepeater";
5
+ import AtomRepeater, { SelectAll, SelectorCheckBox } from "@web-atoms/web-controls/dist/basic/AtomRepeater";
6
6
  import TreeView from "../../../common/controls/tree/TreeView";
7
7
  import Bind from "@web-atoms/core/dist/core/Bind";
8
8
  import Action from "@web-atoms/core/dist/view-model/Action";
@@ -33,6 +33,13 @@ const css = styled.css `
33
33
  width: 100%;
34
34
  }
35
35
 
36
+ & a {
37
+ text-decoration: none;
38
+ &:hover {
39
+ text-decoration: underline;
40
+ }
41
+ }
42
+
36
43
  & .file-link {
37
44
  cursor: pointer;
38
45
  &:hover {
@@ -67,6 +74,8 @@ export default class FolderPage extends ContentPage {
67
74
 
68
75
  private selectedFiles: IAppFile[] = [];
69
76
 
77
+ private files: IAppFile[] = [];
78
+
70
79
  @InjectProperty
71
80
  private cloudFileService: CloudFileService;
72
81
 
@@ -126,7 +135,17 @@ export default class FolderPage extends ContentPage {
126
135
  </div>}
127
136
  </div>;
128
137
 
129
- this.renderer = <div class={css}>
138
+ this.renderer = <div
139
+ class={css}
140
+ files={Bind.oneWayAsync((c, e, cancelToken) =>
141
+ this.cloudFileService.files({
142
+ search: this.search,
143
+ folder: this.selectedFolder,
144
+ version: this.version,
145
+ cancelToken
146
+ })
147
+ )}
148
+ >
130
149
  <TreeView
131
150
  class="folder-tree"
132
151
  presenter={Bind.presenter((c) => this.folderTree = c)}
@@ -144,15 +163,14 @@ export default class FolderPage extends ContentPage {
144
163
  data-layout="data-grid"
145
164
  allowMultipleSelection={true}
146
165
  selectedItems={this.selectedFiles}
147
- items={Bind.oneWayAsync((c, e, cancelToken) =>
148
- this.cloudFileService.files({
149
- search: this.search,
150
- folder: this.selectedFolder,
151
- version: this.version,
152
- cancelToken
153
- })
154
- )}
166
+ header={true}
167
+ headerRenderer={() => <header>
168
+ <SelectAll
169
+ items={Bind.oneWay(() => this.files)}
170
+ selectedItems={this.selectedFiles}/>
171
+ </header>}
155
172
  itemRenderer={(file: IAppFile) => this.renderFile(file)}
173
+ items={Bind.oneWay(() => this.files)}
156
174
  />
157
175
  </div>
158
176
  </div>;
@@ -251,10 +269,17 @@ export default class FolderPage extends ContentPage {
251
269
  await this.cloudFileService.expand(item);
252
270
  }
253
271
 
254
- @Action({ onEvent: "delete-files"})
272
+ @Action({ onEvent: "delete-files", confirm: "Are you sure you want to delete selected files?"})
255
273
  async deleteFiles() {
256
- await this.cloudFileService.delete(this.selectedFiles);
257
- this.selectedFiles.clear();
274
+ const copy = [].concat(this.selectedFiles);
275
+ while(copy.length) {
276
+ const slice = copy.splice(0, 10);
277
+ await this.cloudFileService.delete(slice);
278
+ for (const iterator of slice) {
279
+ this.selectedFiles.remove(iterator);
280
+ this.files.remove(iterator);
281
+ }
282
+ }
258
283
  this.version++;
259
284
  }
260
285