Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
U
upgrade-data-crawler-fe
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ThinhNC
upgrade-data-crawler-fe
Commits
2c208555
Commit
2c208555
authored
Oct 17, 2026
by
ThinhNC
Browse files
Options
Browse Files
Download
Plain Diff
Merge develop into staging: comprehensive bilingual support for exports modal and toasts
parents
433d5d7a
276ff0f9
Pipeline
#53762
passed with stage
in 1 minute and 23 seconds
Changes
4
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
105 additions
and
30 deletions
+105
-30
page.tsx
src/app/exports/page.tsx
+1
-1
create-export-modal.tsx
src/components/exports/create-export-modal.tsx
+20
-23
use-crawl-exports.ts
src/hooks/use-crawl-exports.ts
+12
-6
translations.ts
src/lib/i18n/translations.ts
+72
-0
No files found.
src/app/exports/page.tsx
View file @
2c208555
...
@@ -276,7 +276,7 @@ export default function ExportsPage() {
...
@@ -276,7 +276,7 @@ export default function ExportsPage() {
)
:
isError
?
(
)
:
isError
?
(
<
div
className=
"p-8 text-center rounded-3xl border border-red-500/20 bg-red-500/5 space-y-3"
>
<
div
className=
"p-8 text-center rounded-3xl border border-red-500/20 bg-red-500/5 space-y-3"
>
<
p
className=
"text-sm text-red-500 font-medium"
>
<
p
className=
"text-sm text-red-500 font-medium"
>
Đã có lỗi xảy ra khi tải danh sách tệp xuất.
{
t
.
exports
.
loadError
}
</
p
>
</
p
>
<
Button
<
Button
variant=
"outline"
variant=
"outline"
...
...
src/components/exports/create-export-modal.tsx
View file @
2c208555
...
@@ -52,45 +52,41 @@ export function isJobExportable(job?: {
...
@@ -52,45 +52,41 @@ export function isJobExportable(job?: {
);
);
}
}
const
EXPORT_FORMATS
:
{
interface
FormatMeta
{
type
:
ExportType
;
type
:
ExportType
;
label
:
string
;
key
:
"csv"
|
"json"
|
"xlsx"
|
"markdown"
|
"zip"
;
desc
:
string
;
icon
:
React
.
ElementType
;
icon
:
React
.
ElementType
;
badgeColor
:
string
;
badgeColor
:
string
;
}[]
=
[
}
const
EXPORT_FORMAT_OPTIONS
:
FormatMeta
[]
=
[
{
{
type
:
"CSV"
,
type
:
"CSV"
,
label
:
"CSV (Comma-Separated)"
,
key
:
"csv"
,
desc
:
"Bảng tính chuẩn, phân tích dữ liệu nhanh"
,
icon
:
FileText
,
icon
:
FileText
,
badgeColor
:
"bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20"
,
badgeColor
:
"bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20"
,
},
},
{
{
type
:
"JSON"
,
type
:
"JSON"
,
label
:
"JSON (Structured)"
,
key
:
"json"
,
desc
:
"Cấu trúc lồng ghép, tích hợp API & lập trình"
,
icon
:
FileJson
,
icon
:
FileJson
,
badgeColor
:
"bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20"
,
badgeColor
:
"bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20"
,
},
},
{
{
type
:
"XLSX"
,
type
:
"XLSX"
,
label
:
"Excel Spreadsheet (XLSX)"
,
key
:
"xlsx"
,
desc
:
"Định dạng bảng Microsoft Excel chuyên nghiệp"
,
icon
:
FileSpreadsheet
,
icon
:
FileSpreadsheet
,
badgeColor
:
"bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20"
,
badgeColor
:
"bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20"
,
},
},
{
{
type
:
"MARKDOWN"
,
type
:
"MARKDOWN"
,
label
:
"Markdown (.md)"
,
key
:
"markdown"
,
desc
:
"Tài liệu văn bản sạch cho RAG và mô hình LLM"
,
icon
:
FileCode
,
icon
:
FileCode
,
badgeColor
:
"bg-purple-500/10 text-purple-600 dark:text-purple-400 border-purple-500/20"
,
badgeColor
:
"bg-purple-500/10 text-purple-600 dark:text-purple-400 border-purple-500/20"
,
},
},
{
{
type
:
"ZIP"
,
type
:
"ZIP"
,
label
:
"ZIP Archive"
,
key
:
"zip"
,
desc
:
"Gói nén toàn bộ tài nguyên hình ảnh và tệp"
,
icon
:
Archive
,
icon
:
Archive
,
badgeColor
:
"bg-slate-500/10 text-slate-600 dark:text-slate-400 border-slate-500/20"
,
badgeColor
:
"bg-slate-500/10 text-slate-600 dark:text-slate-400 border-slate-500/20"
,
},
},
...
@@ -221,7 +217,7 @@ export function CreateExportModal({
...
@@ -221,7 +217,7 @@ export function CreateExportModal({
</
Label
>
</
Label
>
{
displayJobs
.
length
>
0
&&
(
{
displayJobs
.
length
>
0
&&
(
<
span
className=
"text-[11px] text-muted-foreground font-medium"
>
<
span
className=
"text-[11px] text-muted-foreground font-medium"
>
(
{
displayJobs
.
length
}
tác vụ có thể xuất
)
(
{
displayJobs
.
length
}
{
t
.
exports
.
modal
.
exportableTasksCount
}
)
</
span
>
</
span
>
)
}
)
}
</
div
>
</
div
>
...
@@ -244,11 +240,11 @@ export function CreateExportModal({
...
@@ -244,11 +240,11 @@ export function CreateExportModal({
{
job
.
startUrl
}
{
job
.
startUrl
}
</
p
>
</
p
>
<
span
className=
"text-[10px] font-semibold px-2 py-0.5 rounded-full bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 shrink-0"
>
<
span
className=
"text-[10px] font-semibold px-2 py-0.5 rounded-full bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 shrink-0"
>
{
job
.
successPages
??
0
}
trang
{
job
.
successPages
??
0
}
{
t
.
exports
.
modal
.
pages
}
</
span
>
</
span
>
</
div
>
</
div
>
<
p
className=
"text-[10px] text-muted-foreground font-mono truncate pt-0.5"
>
<
p
className=
"text-[10px] text-muted-foreground font-mono truncate pt-0.5"
>
ID:
{
job
.
id
.
slice
(
0
,
16
)
}
...
{
job
.
status
?
` • ${job.status === "COMPLETED" ?
"Hoàn tất"
: job.status}`
:
""
}{
job
.
createdAt
?
` • ${formatDate(job.createdAt, locale)}`
:
""
}
ID:
{
job
.
id
.
slice
(
0
,
16
)
}
...
{
job
.
status
?
` • ${job.status === "COMPLETED" ?
t.exports.status.completed
: job.status}`
:
""
}{
job
.
createdAt
?
` • ${formatDate(job.createdAt, locale)}`
:
""
}
</
p
>
</
p
>
</
div
>
</
div
>
{
selectedJobId
===
job
.
id
&&
(
{
selectedJobId
===
job
.
id
&&
(
...
@@ -260,7 +256,7 @@ export function CreateExportModal({
...
@@ -260,7 +256,7 @@ export function CreateExportModal({
)
:
(
)
:
(
<
div
className=
"rounded-2xl border border-amber-500/30 bg-amber-500/10 p-3.5 text-xs text-amber-700 dark:text-amber-400 flex items-center gap-2.5"
>
<
div
className=
"rounded-2xl border border-amber-500/30 bg-amber-500/10 p-3.5 text-xs text-amber-700 dark:text-amber-400 flex items-center gap-2.5"
>
<
AlertCircle
className=
"h-4 w-4 shrink-0 text-amber-500"
/>
<
AlertCircle
className=
"h-4 w-4 shrink-0 text-amber-500"
/>
<
span
>
Không có tác vụ cào nào có trang thu thập thành công để xuất dữ liệu.
</
span
>
<
span
>
{
t
.
exports
.
modal
.
emptyExportableJobs
}
</
span
>
</
div
>
</
div
>
)
}
)
}
</
div
>
</
div
>
...
@@ -271,9 +267,10 @@ export function CreateExportModal({
...
@@ -271,9 +267,10 @@ export function CreateExportModal({
{
t
.
exports
.
modal
.
selectFormat
}
{
t
.
exports
.
modal
.
selectFormat
}
</
Label
>
</
Label
>
<
div
className=
"grid grid-cols-1 sm:grid-cols-2 gap-2"
>
<
div
className=
"grid grid-cols-1 sm:grid-cols-2 gap-2"
>
{
EXPORT_FORMATS
.
map
((
fmt
)
=>
{
{
EXPORT_FORMAT
_OPTION
S
.
map
((
fmt
)
=>
{
const
Icon
=
fmt
.
icon
;
const
Icon
=
fmt
.
icon
;
const
isSelected
=
selectedFormat
===
fmt
.
type
;
const
isSelected
=
selectedFormat
===
fmt
.
type
;
const
formatTrans
=
t
.
exports
.
modal
.
formats
[
fmt
.
key
];
return
(
return
(
<
button
<
button
key=
{
fmt
.
type
}
key=
{
fmt
.
type
}
...
@@ -291,12 +288,12 @@ export function CreateExportModal({
...
@@ -291,12 +288,12 @@ export function CreateExportModal({
<
div
className=
"min-w-0 flex-1"
>
<
div
className=
"min-w-0 flex-1"
>
<
div
className=
"flex items-center justify-between"
>
<
div
className=
"flex items-center justify-between"
>
<
span
className=
"text-xs font-bold text-foreground"
>
<
span
className=
"text-xs font-bold text-foreground"
>
{
fmt
.
type
}
{
f
ormatTrans
?.
label
||
f
mt
.
type
}
</
span
>
</
span
>
{
isSelected
&&
<
Check
className=
"h-3.5 w-3.5 text-emerald-500"
/>
}
{
isSelected
&&
<
Check
className=
"h-3.5 w-3.5 text-emerald-500"
/>
}
</
div
>
</
div
>
<
p
className=
"text-[10px] text-muted-foreground line-clamp-1 mt-0.5"
>
<
p
className=
"text-[10px] text-muted-foreground line-clamp-1 mt-0.5"
>
{
f
mt
.
desc
}
{
f
ormatTrans
?
.
desc
}
</
p
>
</
p
>
</
div
>
</
div
>
</
button
>
</
button
>
...
@@ -308,12 +305,12 @@ export function CreateExportModal({
...
@@ -308,12 +305,12 @@ export function CreateExportModal({
{
/* Custom File Name (Optional) */
}
{
/* Custom File Name (Optional) */
}
<
div
className=
"space-y-1 pt-1"
>
<
div
className=
"space-y-1 pt-1"
>
<
Label
className=
"text-[11px] text-muted-foreground"
>
<
Label
className=
"text-[11px] text-muted-foreground"
>
Tên tệp tùy chỉnh (Tùy chọn)
{
t
.
exports
.
modal
.
customFileName
}
</
Label
>
</
Label
>
<
Input
<
Input
value=
{
customFileName
}
value=
{
customFileName
}
onChange=
{
(
e
)
=>
setCustomFileName
(
e
.
target
.
value
)
}
onChange=
{
(
e
)
=>
setCustomFileName
(
e
.
target
.
value
)
}
placeholder=
"crawl_dataset_custom"
placeholder=
{
t
.
exports
.
modal
.
customFileNamePlaceholder
}
className=
"rounded-2xl text-xs"
className=
"rounded-2xl text-xs"
/>
/>
</
div
>
</
div
>
...
...
src/hooks/use-crawl-exports.ts
View file @
2c208555
...
@@ -5,6 +5,8 @@ import { crawlExportService } from "@/services/crawl-export.service";
...
@@ -5,6 +5,8 @@ import { crawlExportService } from "@/services/crawl-export.service";
import
{
CrawlExportQueryDto
,
ExportType
}
from
"@/types/export"
;
import
{
CrawlExportQueryDto
,
ExportType
}
from
"@/types/export"
;
import
{
toast
}
from
"sonner"
;
import
{
toast
}
from
"sonner"
;
import
{
useLanguage
}
from
"@/providers/language-provider"
;
export
const
CRAWL_EXPORTS_KEYS
=
{
export
const
CRAWL_EXPORTS_KEYS
=
{
all
:
[
"crawl-exports"
]
as
const
,
all
:
[
"crawl-exports"
]
as
const
,
lists
:
()
=>
[...
CRAWL_EXPORTS_KEYS
.
all
,
"list"
]
as
const
,
lists
:
()
=>
[...
CRAWL_EXPORTS_KEYS
.
all
,
"list"
]
as
const
,
...
@@ -21,44 +23,48 @@ export function useCrawlExportsList(params?: CrawlExportQueryDto) {
...
@@ -21,44 +23,48 @@ export function useCrawlExportsList(params?: CrawlExportQueryDto) {
export
function
useCreateCrawlExport
()
{
export
function
useCreateCrawlExport
()
{
const
queryClient
=
useQueryClient
();
const
queryClient
=
useQueryClient
();
const
{
t
}
=
useLanguage
();
return
useMutation
({
return
useMutation
({
mutationFn
:
({
jobId
,
exportType
,
fileName
}:
{
jobId
:
string
;
exportType
:
ExportType
;
fileName
?:
string
})
=>
mutationFn
:
({
jobId
,
exportType
,
fileName
}:
{
jobId
:
string
;
exportType
:
ExportType
;
fileName
?:
string
})
=>
crawlExportService
.
createExport
(
jobId
,
{
exportType
,
fileName
}),
crawlExportService
.
createExport
(
jobId
,
{
exportType
,
fileName
}),
onSuccess
:
()
=>
{
onSuccess
:
()
=>
{
toast
.
success
(
"Khởi tạo yêu cầu xuất dữ liệu thành công!"
);
toast
.
success
(
t
.
exports
.
toasts
.
createSuccess
);
queryClient
.
invalidateQueries
({
queryKey
:
CRAWL_EXPORTS_KEYS
.
all
});
queryClient
.
invalidateQueries
({
queryKey
:
CRAWL_EXPORTS_KEYS
.
all
});
},
},
onError
:
(
error
:
Error
)
=>
{
onError
:
(
error
:
Error
)
=>
{
toast
.
error
(
`
Không thể tạo yêu cầu xuất:
${
error
.
message
}
`
);
toast
.
error
(
`
${
t
.
exports
.
toasts
.
createError
}
${
error
.
message
}
`
);
},
},
});
});
}
}
export
function
useDeleteCrawlExport
()
{
export
function
useDeleteCrawlExport
()
{
const
queryClient
=
useQueryClient
();
const
queryClient
=
useQueryClient
();
const
{
t
}
=
useLanguage
();
return
useMutation
({
return
useMutation
({
mutationFn
:
(
id
:
string
)
=>
crawlExportService
.
deleteExport
(
id
),
mutationFn
:
(
id
:
string
)
=>
crawlExportService
.
deleteExport
(
id
),
onSuccess
:
()
=>
{
onSuccess
:
()
=>
{
toast
.
success
(
"Đã xóa tệp dữ liệu đã xuất!"
);
toast
.
success
(
t
.
exports
.
toasts
.
deleteSuccess
);
queryClient
.
invalidateQueries
({
queryKey
:
CRAWL_EXPORTS_KEYS
.
all
});
queryClient
.
invalidateQueries
({
queryKey
:
CRAWL_EXPORTS_KEYS
.
all
});
},
},
onError
:
(
error
:
Error
)
=>
{
onError
:
(
error
:
Error
)
=>
{
toast
.
error
(
`
Không thể xóa tệp:
${
error
.
message
}
`
);
toast
.
error
(
`
${
t
.
exports
.
toasts
.
deleteError
}
${
error
.
message
}
`
);
},
},
});
});
}
}
export
function
useDownloadExport
()
{
export
function
useDownloadExport
()
{
const
{
t
}
=
useLanguage
();
return
useMutation
({
return
useMutation
({
mutationFn
:
({
exportId
,
fileName
}:
{
exportId
:
string
;
fileName
?:
string
})
=>
mutationFn
:
({
exportId
,
fileName
}:
{
exportId
:
string
;
fileName
?:
string
})
=>
crawlExportService
.
downloadExport
(
exportId
,
fileName
),
crawlExportService
.
downloadExport
(
exportId
,
fileName
),
onSuccess
:
()
=>
{
onSuccess
:
()
=>
{
toast
.
success
(
"Đang tải tệp về máy tính của bạn..."
);
toast
.
success
(
t
.
exports
.
toasts
.
downloadSuccess
);
},
},
onError
:
(
error
:
Error
)
=>
{
onError
:
(
error
:
Error
)
=>
{
toast
.
error
(
`
Tải tệp thất bại:
${
error
.
message
}
`
);
toast
.
error
(
`
${
t
.
exports
.
toasts
.
downloadError
}
${
error
.
message
}
`
);
},
},
});
});
}
}
src/lib/i18n/translations.ts
View file @
2c208555
...
@@ -622,6 +622,7 @@ export const translations = {
...
@@ -622,6 +622,7 @@ export const translations = {
emptyTitle
:
"Chưa có tệp xuất nào"
,
emptyTitle
:
"Chưa có tệp xuất nào"
,
emptyDesc
:
"Khởi tạo yêu cầu xuất dữ liệu từ các Crawl Job đã hoàn tất để tải về máy tính."
,
emptyDesc
:
"Khởi tạo yêu cầu xuất dữ liệu từ các Crawl Job đã hoàn tất để tải về máy tính."
,
retry
:
"Tải lại"
,
retry
:
"Tải lại"
,
loadError
:
"Đã có lỗi xảy ra khi tải danh sách tệp xuất."
,
stats
:
{
stats
:
{
total
:
"Tổng Tệp Xuất"
,
total
:
"Tổng Tệp Xuất"
,
totalDesc
:
"Tệp dữ liệu đã sinh"
,
totalDesc
:
"Tệp dữ liệu đã sinh"
,
...
@@ -656,11 +657,46 @@ export const translations = {
...
@@ -656,11 +657,46 @@ export const translations = {
desc
:
"Chọn tác vụ cào đã hoàn tất và định dạng tệp mong muốn."
,
desc
:
"Chọn tác vụ cào đã hoàn tất và định dạng tệp mong muốn."
,
selectJob
:
"Chọn Tác Vụ Cào"
,
selectJob
:
"Chọn Tác Vụ Cào"
,
selectJobPlaceholder
:
"Chọn từ danh sách Job đã hoàn thành..."
,
selectJobPlaceholder
:
"Chọn từ danh sách Job đã hoàn thành..."
,
exportableTasksCount
:
"tác vụ có thể xuất"
,
pages
:
"trang"
,
emptyExportableJobs
:
"Không có tác vụ cào nào có trang thu thập thành công để xuất dữ liệu."
,
selectFormat
:
"Định dạng tệp đầu ra"
,
selectFormat
:
"Định dạng tệp đầu ra"
,
formats
:
{
csv
:
{
label
:
"CSV (Comma-Separated)"
,
desc
:
"Bảng tính chuẩn, phân tích dữ liệu nhanh"
,
},
json
:
{
label
:
"JSON (Structured)"
,
desc
:
"Cấu trúc lồng ghép, tích hợp API & lập trình"
,
},
xlsx
:
{
label
:
"Excel Spreadsheet (XLSX)"
,
desc
:
"Định dạng bảng Microsoft Excel chuyên nghiệp"
,
},
markdown
:
{
label
:
"Markdown (.md)"
,
desc
:
"Tài liệu văn bản sạch cho RAG và mô hình LLM"
,
},
zip
:
{
label
:
"ZIP Archive"
,
desc
:
"Gói nén toàn bộ tài nguyên hình ảnh và tệp"
,
},
},
customFileName
:
"Tên tệp tùy chỉnh (Tùy chọn)"
,
customFileNamePlaceholder
:
"vd: crawl_dataset_custom"
,
cancel
:
"Hủy bỏ"
,
cancel
:
"Hủy bỏ"
,
submit
:
"Bắt Đầu Xuất File"
,
submit
:
"Bắt Đầu Xuất File"
,
submitting
:
"Đang gửi yêu cầu..."
,
submitting
:
"Đang gửi yêu cầu..."
,
},
},
toasts
:
{
createSuccess
:
"Khởi tạo yêu cầu xuất dữ liệu thành công!"
,
createError
:
"Không thể tạo yêu cầu xuất:"
,
deleteSuccess
:
"Đã xóa tệp dữ liệu đã xuất!"
,
deleteError
:
"Không thể xóa tệp:"
,
downloadSuccess
:
"Đang tải tệp về máy tính của bạn..."
,
downloadError
:
"Không thể tải tệp:"
,
},
},
},
// Developer Settings Module
// Developer Settings Module
...
@@ -2089,6 +2125,7 @@ export const translations = {
...
@@ -2089,6 +2125,7 @@ export const translations = {
emptyTitle
:
"No export files found"
,
emptyTitle
:
"No export files found"
,
emptyDesc
:
"Generate export files from completed crawl jobs to download them directly."
,
emptyDesc
:
"Generate export files from completed crawl jobs to download them directly."
,
retry
:
"Reload"
,
retry
:
"Reload"
,
loadError
:
"An error occurred while loading export files."
,
stats
:
{
stats
:
{
total
:
"Total Exports"
,
total
:
"Total Exports"
,
totalDesc
:
"Generated data files"
,
totalDesc
:
"Generated data files"
,
...
@@ -2123,11 +2160,46 @@ export const translations = {
...
@@ -2123,11 +2160,46 @@ export const translations = {
desc
:
"Select a completed crawl job and desired output format."
,
desc
:
"Select a completed crawl job and desired output format."
,
selectJob
:
"Select Crawl Job"
,
selectJob
:
"Select Crawl Job"
,
selectJobPlaceholder
:
"Choose from completed crawl jobs..."
,
selectJobPlaceholder
:
"Choose from completed crawl jobs..."
,
exportableTasksCount
:
"exportable tasks"
,
pages
:
"pages"
,
emptyExportableJobs
:
"No crawl jobs with successfully collected pages available for export."
,
selectFormat
:
"Export Format"
,
selectFormat
:
"Export Format"
,
formats
:
{
csv
:
{
label
:
"CSV (Comma-Separated)"
,
desc
:
"Standard spreadsheet, fast data analysis"
,
},
json
:
{
label
:
"JSON (Structured)"
,
desc
:
"Structured nested data for APIs & developers"
,
},
xlsx
:
{
label
:
"Excel Spreadsheet (XLSX)"
,
desc
:
"Professional Microsoft Excel workbook format"
,
},
markdown
:
{
label
:
"Markdown (.md)"
,
desc
:
"Clean text document for RAG & LLM pipelines"
,
},
zip
:
{
label
:
"ZIP Archive"
,
desc
:
"Compressed archive of full datasets and media"
,
},
},
customFileName
:
"Custom File Name (Optional)"
,
customFileNamePlaceholder
:
"e.g. crawl_dataset_custom"
,
cancel
:
"Cancel"
,
cancel
:
"Cancel"
,
submit
:
"Start Export"
,
submit
:
"Start Export"
,
submitting
:
"Submitting..."
,
submitting
:
"Submitting..."
,
},
},
toasts
:
{
createSuccess
:
"Export request initiated successfully!"
,
createError
:
"Failed to create export request:"
,
deleteSuccess
:
"Export file deleted successfully!"
,
deleteError
:
"Failed to delete export file:"
,
downloadSuccess
:
"Downloading file to your computer..."
,
downloadError
:
"Failed to download file:"
,
},
},
},
// Developer Settings Module
// Developer Settings Module
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment