Skip to content

Commit 646f743

Browse files
authored
Merge pull request #375 from qa-guru/QAGDEV-721
QAGDEV-721 - Удаление файла и возвращение его через command+Z / cntrl+Z
2 parents 5d9678c + 6e79f23 commit 646f743

File tree

15 files changed

+528
-189
lines changed

15 files changed

+528
-189
lines changed

src/features/edit-training/views/edit-lecture/edit-lecture.tsx

Lines changed: 79 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ import { LECTURE_FILE_GET_URI, LECTURE_HOMEWORK_FILE_GET_URI } from "config";
88

99
import { InputText } from "shared/components/form";
1010
import { Editor } from "shared/components/text-editor";
11-
import { RichTextEditorRef } from "shared/lib/mui-tiptap";
11+
import {
12+
blobUrlToFile,
13+
collectFileIds,
14+
findNodeByUrl,
15+
RichTextEditorRef,
16+
} from "shared/lib/mui-tiptap";
1217
import { UserRole } from "api/graphql/generated/graphql";
1318
import { PendingFile } from "shared/components/text-editor/types";
1419
import { createUrlWithParams } from "shared/utils";
@@ -66,64 +71,98 @@ const EditLecture: FC<IEditLecture> = ({
6671

6772
const onSubmit: SubmitHandler<LectureInput> = async (data) => {
6873
const { speakers, ...restData } = data;
74+
const emails = speakers?.map((s) => s?.email);
75+
if (!lectureId) return;
6976

70-
const emails = speakers?.map((speaker) => speaker?.email);
77+
let content = rteRefContent.current?.editor?.getHTML().trim() || "";
78+
let contentHomework =
79+
rteRefContentHomeWork.current?.editor?.getHTML().trim() || "";
7180

72-
let content = rteRefContent.current?.editor?.getHTML().trim();
73-
let contentHomework = rteRefContentHomeWork.current?.editor
74-
?.getHTML()
75-
.trim();
81+
const editorContent = rteRefContent.current?.editor!;
82+
const editorHomework = rteRefContentHomeWork.current?.editor!;
7683

77-
if (!lectureId) {
78-
return;
79-
}
84+
const contentBlobUrls = Array.from(
85+
content.matchAll(/(blob:[^"'\s>]+)/g)
86+
).map((m) => m[1]);
87+
const homeworkBlobUrls = Array.from(
88+
contentHomework.matchAll(/(blob:[^"'\s>]+)/g)
89+
).map((m) => m[1]);
8090

81-
const lectureFiles = pendingFiles.filter(
82-
(file) => file.source === "lecture"
91+
const recoveredLecture = await Promise.all(
92+
contentBlobUrls
93+
.filter((url) => !pendingFiles.find((f) => f.localUrl === url))
94+
.map(async (url) => {
95+
const node = findNodeByUrl(editorContent.state.doc, url);
96+
const fileName = node?.fileName || "recovered_file";
97+
const file = await blobUrlToFile(url, fileName);
98+
return { file, localUrl: url, source: "lecture" as const };
99+
})
83100
);
84-
const homeworkFiles = pendingFiles.filter(
85-
(file) => file.source === "lectureHomework"
101+
102+
const recoveredHomework = await Promise.all(
103+
homeworkBlobUrls
104+
.filter((url) => !pendingFiles.find((f) => f.localUrl === url))
105+
.map(async (url) => {
106+
const node = findNodeByUrl(editorHomework.state.doc, url);
107+
const fileName = node?.fileName || "recovered_file";
108+
const file = await blobUrlToFile(url, fileName);
109+
return { file, localUrl: url, source: "lectureHomework" as const };
110+
})
86111
);
87112

88-
const lectureUploadPromises = lectureFiles.map(
89-
async ({ file, localUrl }) => {
90-
const uploadedFile = await uploadLectureFile(file, lectureId);
113+
const allFiles = [
114+
...pendingFiles,
115+
...recoveredLecture,
116+
...recoveredHomework,
117+
];
118+
const lectureFiles = allFiles.filter((f) => f.source === "lecture");
119+
const homeworkFiles = allFiles.filter(
120+
(f) => f.source === "lectureHomework"
121+
);
122+
123+
const uploadedLectureFiles = await Promise.all(
124+
lectureFiles.map(async ({ file, localUrl }) => {
125+
const uploaded = await uploadLectureFile(file, lectureId);
91126
return {
92127
localUrl,
93128
realUrl: createUrlWithParams(LECTURE_FILE_GET_URI, {
94129
lectureId,
95-
fileId: uploadedFile?.id!,
130+
fileId: uploaded?.id!,
96131
}),
97132
};
98-
}
133+
})
99134
);
100135

101-
const lectureHomeworkUploadPromises = homeworkFiles.map(
102-
async ({ file, localUrl }) => {
103-
const uploadedFile = await uploadLectureHomeworkFile(file, lectureId);
136+
const uploadedHomeworkFiles = await Promise.all(
137+
homeworkFiles.map(async ({ file, localUrl }) => {
138+
const uploaded = await uploadLectureHomeworkFile(file, lectureId);
104139
return {
105140
localUrl,
106141
realUrl: createUrlWithParams(LECTURE_HOMEWORK_FILE_GET_URI, {
107142
lectureId,
108-
fileId: uploadedFile?.id!,
143+
fileId: uploaded?.id!,
109144
}),
110145
};
111-
}
112-
);
113-
114-
const uploadedLectureFiles = await Promise.all(lectureUploadPromises);
115-
const uploadedLectureHomeworkFiles = await Promise.all(
116-
lectureHomeworkUploadPromises
146+
})
117147
);
118148

119149
uploadedLectureFiles.forEach(({ localUrl, realUrl }) => {
120-
content = content?.replaceAll(localUrl, realUrl);
150+
content = content.replaceAll(localUrl, realUrl);
121151
});
122152

123-
uploadedLectureHomeworkFiles.forEach(({ localUrl, realUrl }) => {
124-
contentHomework = contentHomework?.replaceAll(localUrl, realUrl);
153+
uploadedHomeworkFiles.forEach(({ localUrl, realUrl }) => {
154+
contentHomework = contentHomework.replaceAll(localUrl, realUrl);
125155
});
126156

157+
const contentNode = rteRefContent.current?.editor?.state.doc;
158+
const contentHomeworkNode =
159+
rteRefContentHomeWork.current?.editor?.state.doc;
160+
161+
const currentFileIds = [
162+
...collectFileIds(contentNode!),
163+
...collectFileIds(contentHomeworkNode!),
164+
];
165+
127166
const submissionData = {
128167
...restData,
129168
speakers: emails,
@@ -133,24 +172,24 @@ const EditLecture: FC<IEditLecture> = ({
133172
};
134173

135174
await updateLecture({
136-
variables: {
137-
input: submissionData,
138-
},
175+
variables: { input: submissionData },
139176
onCompleted: async () => {
140177
for (const fileId of deletedLectureFileIds) {
141-
await deleteLectureFile(lectureId, fileId);
178+
if (!currentFileIds.includes(fileId)) {
179+
await deleteLectureFile(lectureId, fileId);
180+
}
142181
}
182+
143183
for (const fileId of deletedHomeworkFileIds) {
144-
await deleteLectureHomeworkFile(lectureId, fileId);
184+
if (!currentFileIds.includes(fileId)) {
185+
await deleteLectureHomeworkFile(lectureId, fileId);
186+
}
145187
}
146188

147189
enqueueSnackbar("Урок обновлен", { variant: "success" });
148190
},
149191
onError: () => {
150-
enqueueSnackbar(
151-
"Не удалось обновить данные. Пожалуйста, попробуйте снова",
152-
{ variant: "error" }
153-
);
192+
enqueueSnackbar("Ошибка при обновлении", { variant: "error" });
154193
},
155194
});
156195

src/shared/components/text-editor/hooks/use-extensions.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { TextAlign } from "@tiptap/extension-text-align";
3131
import { TextStyle } from "@tiptap/extension-text-style";
3232
import { Underline } from "@tiptap/extension-underline";
3333
import { Youtube } from "@tiptap/extension-youtube";
34+
import { ReactNodeViewRenderer } from "@tiptap/react";
3435
import { CodeBlockLowlight } from "@tiptap/extension-code-block-lowlight";
3536
import { useMemo } from "react";
3637
import { common, createLowlight } from "lowlight";
@@ -43,6 +44,7 @@ import {
4344
} from "shared/lib/mui-tiptap/extensions";
4445
import { HeadingWithAnchor } from "shared/lib/mui-tiptap/hooks";
4546
import { FileDeletionTracker } from "shared/lib/mui-tiptap/extensions/file-deletion-tracker";
47+
import FileNodeView from "shared/lib/mui-tiptap/extensions/file-node-view";
4648

4749
import { mentionSuggestionOptions } from "../utils/mention-suggestion-options";
4850

@@ -106,7 +108,7 @@ const Iframe = Node.create({
106108
},
107109
});
108110

109-
export const FileNode = Node.create<FileNodeOptions>({
111+
export const FileNode = Node.create({
110112
name: "file",
111113

112114
group: "inline",
@@ -127,36 +129,44 @@ export const FileNode = Node.create<FileNodeOptions>({
127129
parseHTML() {
128130
return [
129131
{
130-
tag: "a[data-file]",
132+
tag: "file-node",
133+
getAttrs: (el) => {
134+
if (!(el instanceof HTMLElement)) return false;
135+
136+
return {
137+
href: el.getAttribute("href"),
138+
fileName: el.getAttribute("fileName") || el.textContent,
139+
};
140+
},
131141
},
132142
];
133143
},
134144

135145
renderHTML({ HTMLAttributes }) {
136146
return [
137-
"a",
147+
"file-node",
138148
mergeAttributes(HTMLAttributes, {
139-
"data-file": "",
140-
href: HTMLAttributes.href,
141-
download: HTMLAttributes.fileName,
142-
target: "_blank",
149+
"data-file-node": "true",
143150
}),
144-
HTMLAttributes.fileName || "Download file",
145151
];
146152
},
147153

148154
addCommands() {
149155
return {
150156
setFile:
151-
(options) =>
157+
(attrs) =>
152158
({ commands }) => {
153159
return commands.insertContent({
154160
type: this.name,
155-
attrs: options,
161+
attrs,
156162
});
157163
},
158164
};
159165
},
166+
167+
addNodeView() {
168+
return ReactNodeViewRenderer(FileNodeView);
169+
},
160170
});
161171

162172
export type UseExtensionsOptions = {

src/shared/components/text-editor/types/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export type FileSourceType =
1818
export type PendingFile = {
1919
file: File;
2020
localUrl: string;
21-
source: FileSourceType;
21+
source?: FileSourceType;
2222
};
2323

2424
export type SuggestionListRef = {

0 commit comments

Comments
 (0)