@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.
- package/dist/admin/AdminAppIndex.pack.js +417 -196
- package/dist/admin/AdminAppIndex.pack.js.map +1 -1
- package/dist/admin/AdminAppIndex.pack.min.js +14 -7
- package/dist/admin/AdminAppIndex.pack.min.js.map +1 -1
- package/dist/common/FileUploader.d.ts +32 -0
- package/dist/common/FileUploader.d.ts.map +1 -0
- package/dist/common/FileUploader.js +133 -0
- package/dist/common/FileUploader.js.map +1 -0
- package/dist/model/model.d.ts +1 -0
- package/dist/model/model.d.ts.map +1 -1
- package/dist/model/model.js.map +1 -1
- package/dist/services/files/LocalFileService.d.ts +2 -0
- package/dist/services/files/LocalFileService.d.ts.map +1 -1
- package/dist/services/files/LocalFileService.js +66 -11
- package/dist/services/files/LocalFileService.js.map +1 -1
- package/dist/style/GlobalStyle.js +1 -0
- package/dist/style/GlobalStyle.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/web/AppIndex.pack.js +417 -196
- package/dist/web/AppIndex.pack.js.map +1 -1
- package/dist/web/AppIndex.pack.min.js +14 -7
- package/dist/web/AppIndex.pack.min.js.map +1 -1
- package/dist/web/page/files/FolderPage.d.ts +1 -0
- package/dist/web/page/files/FolderPage.d.ts.map +1 -1
- package/dist/web/page/files/FolderPage.js +34 -11
- package/dist/web/page/files/FolderPage.js.map +1 -1
- package/node_modules/@web-atoms/core/dist/services/FetchBuilder.d.ts.map +1 -1
- package/node_modules/@web-atoms/core/dist/services/FetchBuilder.js +6 -0
- package/node_modules/@web-atoms/core/dist/services/FetchBuilder.js.map +1 -1
- package/node_modules/@web-atoms/core/dist/tsconfig.tsbuildinfo +1 -1
- package/node_modules/@web-atoms/core/index.js +89 -0
- package/node_modules/@web-atoms/core/package.json +1 -1
- package/node_modules/@web-atoms/core/src/services/FetchBuilder.ts +6 -0
- package/package.json +2 -2
- package/src/common/FileUploader.ts +140 -0
- package/src/model/model.ts +1 -0
- package/src/services/files/LocalFileService.tsx +70 -9
- package/src/style/GlobalStyle.ts +1 -0
- package/src/web/page/files/FolderPage.tsx +38 -13
|
@@ -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.
|
|
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.
|
|
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
|
+
}
|
package/src/model/model.ts
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
172
|
-
|
|
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();
|
package/src/style/GlobalStyle.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
257
|
-
|
|
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
|
|