Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
一杯枸杞茶ya
csdn-workflow
提交
47f244bc
C
csdn-workflow
项目概览
一杯枸杞茶ya
/
csdn-workflow
与 Fork 源项目一致
Fork自
gitcode_dev / csdn-workflow
通知
2
Star
0
Fork
1
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
DevOps
流水线
流水线任务
计划
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
C
csdn-workflow
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
DevOps
DevOps
流水线
流水线任务
计划
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
流水线任务
提交
Issue看板
体验新版 GitCode,发现更多精彩内容 >>
提交
47f244bc
编写于
6月 30, 2021
作者:
T
Tomas Vik
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
feat: indicate which changed files have MR discussions
上级
06888a24
变更
12
显示空白变更内容
内联
并排
Showing
12 changed file
with
177 addition
and
78 deletion
+177
-78
src/__mocks__/vscode.js
src/__mocks__/vscode.js
+4
-2
src/constants.ts
src/constants.ts
+3
-0
src/data_providers/items/changed_file_item.test.ts
src/data_providers/items/changed_file_item.test.ts
+27
-1
src/data_providers/items/changed_file_item.ts
src/data_providers/items/changed_file_item.ts
+54
-40
src/data_providers/items/mr_item_model.test.ts
src/data_providers/items/mr_item_model.test.ts
+10
-11
src/data_providers/items/mr_item_model.ts
src/data_providers/items/mr_item_model.ts
+33
-14
src/extension.js
src/extension.js
+4
-2
src/review/change_type_decoration_provider.test.ts
src/review/change_type_decoration_provider.test.ts
+4
-4
src/review/change_type_decoration_provider.ts
src/review/change_type_decoration_provider.ts
+3
-3
src/review/has_comments_decoration_provider.test.ts
src/review/has_comments_decoration_provider.test.ts
+18
-0
src/review/has_comments_decoration_provider.ts
src/review/has_comments_decoration_provider.ts
+16
-0
src/test_utils/uri.ts
src/test_utils/uri.ts
+1
-1
未找到文件。
src/__mocks__/vscode.js
浏览文件 @
47f244bc
...
...
@@ -2,8 +2,10 @@ const { Uri } = require('../test_utils/uri');
const
{
EventEmitter
}
=
require
(
'
../test_utils/event_emitter
'
);
module
.
exports
=
{
TreeItem
:
function
TreeItem
(
label
,
collapsibleState
)
{
return
{
label
,
collapsibleState
};
TreeItem
:
function
TreeItem
(
labelOrUri
,
collapsibleState
)
{
return
typeof
labelOrUri
===
'
string
'
?
{
label
:
labelOrUri
,
collapsibleState
}
:
{
resourceUri
:
labelOrUri
,
collapsibleState
};
},
ThemeIcon
:
function
ThemeIcon
(
id
)
{
return
{
id
};
...
...
src/constants.ts
浏览文件 @
47f244bc
...
...
@@ -9,3 +9,6 @@ export const MODIFIED = 'modified';
export
const
DO_NOT_SHOW_VERSION_WARNING
=
'
DO_NOT_SHOW_VERSION_WARNING
'
;
// NOTE: This needs to _always_ be a 3 digits
export
const
MINIMUM_VERSION
=
'
13.5.0
'
;
export
const
CHANGE_TYPE_QUERY_KEY
=
'
changeType
'
;
export
const
HAS_COMMENTS_QUERY_KEY
=
'
hasComments
'
;
src/data_providers/items/changed_file_item.test.ts
浏览文件 @
47f244bc
import
{
PROGRAMMATIC_COMMANDS
}
from
'
../../command_names
'
;
import
{
CHANGE_TYPE_QUERY_KEY
,
HAS_COMMENTS_QUERY_KEY
}
from
'
../../constants
'
;
import
{
diffFile
,
mr
,
mrVersion
}
from
'
../../test_utils/entities
'
;
import
{
ChangedFileItem
}
from
'
./changed_file_item
'
;
...
...
@@ -8,9 +9,34 @@ describe('ChangedFileItem', () => {
'
should not show diff for %s
'
,
extension
=>
{
const
changedImageFile
=
{
...
diffFile
,
new_path
:
`file
${
extension
}
`
};
const
item
=
new
ChangedFileItem
(
mr
,
mrVersion
,
changedImageFile
,
'
/repository/fsPath
'
);
const
item
=
new
ChangedFileItem
(
mr
,
mrVersion
,
changedImageFile
,
'
/repo
'
,
()
=>
false
);
expect
(
item
.
command
?.
command
).
toBe
(
PROGRAMMATIC_COMMANDS
.
NO_IMAGE_REVIEW
);
},
);
it
(
'
should indicate change type
'
,
()
=>
{
const
changedImageFile
=
{
...
diffFile
,
new_path
:
`file.jpg`
};
const
item
=
new
ChangedFileItem
(
mr
,
mrVersion
,
changedImageFile
,
'
/repo
'
,
()
=>
false
);
expect
(
item
.
resourceUri
?.
query
).
toContain
(
`
${
CHANGE_TYPE_QUERY_KEY
}
=`
);
});
});
describe
(
'
captures whether there are comments on the changes
'
,
()
=>
{
let
areThereChanges
:
boolean
;
const
createItem
=
()
=>
new
ChangedFileItem
(
mr
,
mrVersion
,
diffFile
,
'
/repository/fsPath
'
,
()
=>
areThereChanges
);
it
(
'
indicates there are comments
'
,
()
=>
{
areThereChanges
=
true
;
expect
(
createItem
().
resourceUri
?.
query
).
toMatch
(
`
${
HAS_COMMENTS_QUERY_KEY
}
=true`
);
});
it
(
'
indicates there are no comments
'
,
()
=>
{
areThereChanges
=
false
;
expect
(
createItem
().
resourceUri
?.
query
).
toMatch
(
`
${
HAS_COMMENTS_QUERY_KEY
}
=false`
);
});
});
});
src/data_providers/items/changed_file_item.ts
浏览文件 @
47f244bc
import
{
TreeItem
,
Uri
}
from
'
vscode
'
;
import
*
as
vscode
from
'
vscode
'
;
import
{
posix
as
path
}
from
'
path
'
;
import
{
toReviewUri
,
ReviewParams
}
from
'
../../review/review_uri
'
;
import
{
PROGRAMMATIC_COMMANDS
,
VS_COMMANDS
}
from
'
../../command_names
'
;
import
{
ADDED
,
DELETED
,
RENAMED
,
MODIFIED
}
from
'
../../constants
'
;
import
{
ADDED
,
DELETED
,
RENAMED
,
MODIFIED
,
CHANGE_TYPE_QUERY_KEY
,
HAS_COMMENTS_QUERY_KEY
,
}
from
'
../../constants
'
;
export
type
ChangeType
=
typeof
ADDED
|
typeof
DELETED
|
typeof
RENAMED
|
typeof
MODIFIED
;
export
type
HasCommentsFn
=
(
reviewUri
:
vscode
.
Uri
)
=>
boolean
;
const
getChangeType
=
(
file
:
RestDiffFile
):
ChangeType
=>
{
if
(
file
.
new_file
)
return
ADDED
;
...
...
@@ -28,36 +36,12 @@ const imageExtensions = [
const
looksLikeImage
=
(
filePath
:
string
)
=>
imageExtensions
.
includes
(
path
.
extname
(
filePath
).
toLowerCase
());
export
class
ChangedFileItem
extends
TreeItem
{
mr
:
RestMr
;
mrVersion
:
RestMrVersion
;
repositoryPath
:
string
;
file
:
RestDiffFile
;
constructor
(
mr
:
RestMr
,
mrVersion
:
RestMrVersion
,
file
:
RestDiffFile
,
repositoryPath
:
string
)
{
const
changeType
=
getChangeType
(
file
);
const
query
=
new
URLSearchParams
([[
'
changeType
'
,
changeType
]]).
toString
();
super
(
Uri
.
file
(
file
.
new_path
).
with
({
query
}));
this
.
description
=
path
.
dirname
(
`/
${
file
.
new_path
}
`
)
.
split
(
'
/
'
)
.
slice
(
1
)
.
join
(
'
/
'
);
this
.
mr
=
mr
;
this
.
mrVersion
=
mrVersion
;
this
.
repositoryPath
=
repositoryPath
;
this
.
file
=
file
;
if
(
looksLikeImage
(
file
.
old_path
)
||
looksLikeImage
(
file
.
new_path
))
{
this
.
command
=
{
title
:
'
Images are not supported
'
,
command
:
PROGRAMMATIC_COMMANDS
.
NO_IMAGE_REVIEW
,
};
return
;
}
const
getBaseAndHeadUri
=
(
mr
:
RestMr
,
mrVersion
:
RestMrVersion
,
file
:
RestDiffFile
,
repositoryPath
:
string
,
)
=>
{
const
commonParams
:
ReviewParams
=
{
repositoryRoot
:
repositoryPath
,
projectId
:
mr
.
project_id
,
...
...
@@ -78,7 +62,37 @@ export class ChangedFileItem extends TreeItem {
path
:
file
.
new_path
,
commit
:
mrVersion
.
head_commit_sha
,
});
return
{
baseFileUri
,
headFileUri
};
};
export
class
ChangedFileItem
extends
vscode
.
TreeItem
{
constructor
(
mr
:
RestMr
,
mrVersion
:
RestMrVersion
,
file
:
RestDiffFile
,
repositoryPath
:
string
,
hasComment
:
HasCommentsFn
,
)
{
super
(
vscode
.
Uri
.
file
(
file
.
new_path
));
this
.
description
=
path
.
dirname
(
`/
${
file
.
new_path
}
`
)
.
split
(
'
/
'
)
.
slice
(
1
)
.
join
(
'
/
'
);
const
{
baseFileUri
,
headFileUri
}
=
getBaseAndHeadUri
(
mr
,
mrVersion
,
file
,
repositoryPath
);
const
hasComments
=
hasComment
(
baseFileUri
)
||
hasComment
(
headFileUri
);
const
query
=
new
URLSearchParams
([
[
CHANGE_TYPE_QUERY_KEY
,
getChangeType
(
file
)],
[
HAS_COMMENTS_QUERY_KEY
,
String
(
hasComments
)],
]).
toString
();
this
.
resourceUri
=
this
.
resourceUri
?.
with
({
query
});
if
(
looksLikeImage
(
file
.
old_path
)
||
looksLikeImage
(
file
.
new_path
))
{
this
.
command
=
{
title
:
'
Images are not supported
'
,
command
:
PROGRAMMATIC_COMMANDS
.
NO_IMAGE_REVIEW
,
};
return
;
}
this
.
command
=
{
title
:
'
Show changes
'
,
command
:
VS_COMMANDS
.
DIFF
,
...
...
src/data_providers/items/mr_item_model.test.ts
浏览文件 @
47f244bc
...
...
@@ -6,10 +6,12 @@ import {
noteOnDiffTextSnippet
,
multipleNotes
,
}
from
'
../../../test/integration/fixtures/graphql/discussions.js
'
;
import
*
as
mrVersion
from
'
../../../test/integration/fixtures/rest/mr_version.json
'
;
import
{
CommentingRangeProvider
}
from
'
../../review/commenting_range_provider
'
;
import
{
createWrappedRepository
}
from
'
../../test_utils/create_wrapped_repository
'
;
import
{
fromReviewUri
}
from
'
../../review/review_uri
'
;
import
{
WrappedRepository
}
from
'
../../git/wrapped_repository
'
;
import
{
CHANGE_TYPE_QUERY_KEY
,
HAS_COMMENTS_QUERY_KEY
}
from
'
../../constants
'
;
const
createCommentControllerMock
=
vscode
.
comments
.
createCommentController
as
jest
.
Mock
;
...
...
@@ -94,6 +96,14 @@ describe('MrItemModel', () => {
expect
(
path
).
toBe
(
discussionPosition
.
oldPath
);
});
it
(
'
should return changed file items as children
'
,
async
()
=>
{
gitLabService
.
getMrDiff
=
jest
.
fn
().
mockResolvedValue
(
mrVersion
);
const
[
overview
,
changedItem
]
=
await
item
.
getChildren
();
expect
(
changedItem
.
resourceUri
?.
path
).
toBe
(
'
.deleted.yml
'
);
expect
(
changedItem
.
resourceUri
?.
query
).
toMatch
(
`
${
CHANGE_TYPE_QUERY_KEY
}
=deleted`
);
expect
(
changedItem
.
resourceUri
?.
query
).
toMatch
(
`
${
HAS_COMMENTS_QUERY_KEY
}
=false`
);
});
describe
(
'
commenting range
'
,
()
=>
{
it
(
'
should not add a commenting range provider if user does not have permission to comment
'
,
async
()
=>
{
canUserCommentOnMr
=
false
;
...
...
@@ -111,17 +121,6 @@ describe('MrItemModel', () => {
expect
(
commentController
.
commentingRangeProvider
).
toBeInstanceOf
(
CommentingRangeProvider
);
});
// this test ensures that we add comment controller to disposables before calling API.
it
(
'
comment controller can be disposed regardless of API failures
'
,
async
()
=>
{
gitLabService
.
getDiscussions
=
()
=>
Promise
.
reject
(
new
Error
());
await
item
.
getChildren
();
expect
(
commentController
.
dispose
).
not
.
toHaveBeenCalled
();
item
.
dispose
();
expect
(
commentController
.
dispose
).
toHaveBeenCalled
();
});
it
(
'
when we create comment controller for the same MR, we dispose the previously created controller
'
,
async
()
=>
{
await
item
.
getChildren
();
...
...
src/data_providers/items/mr_item_model.ts
浏览文件 @
47f244bc
...
...
@@ -62,17 +62,23 @@ export class MrItemModel extends ItemModel {
return
item
;
}
async
getChildren
():
Promise
<
vscode
.
TreeItem
[]
>
{
const
overview
=
new
vscode
.
TreeItem
(
'
Overview
'
);
overview
.
iconPath
=
new
vscode
.
ThemeIcon
(
'
note
'
);
overview
.
command
=
{
private
get
overviewItem
()
{
const
result
=
new
vscode
.
TreeItem
(
'
Overview
'
);
result
.
iconPath
=
new
vscode
.
ThemeIcon
(
'
note
'
);
result
.
command
=
{
command
:
PROGRAMMATIC_COMMANDS
.
SHOW_RICH_CONTENT
,
arguments
:
[
this
.
mr
,
this
.
repository
.
rootFsPath
],
title
:
'
Show MR Overview
'
,
};
const
{
mrVersion
}
=
await
this
.
repository
.
reloadMr
(
this
.
mr
);
return
result
;
}
private
async
getMrDiscussions
():
Promise
<
GqlTextDiffDiscussion
[]
>
{
try
{
await
this
.
initializeMrDiscussions
(
mrVersion
);
const
discussions
=
await
this
.
repository
.
getGitLabService
().
getDiscussions
({
issuable
:
this
.
mr
,
});
return
discussions
.
filter
(
isTextDiffDiscussion
);
}
catch
(
e
)
{
handleError
(
new
UserFriendlyError
(
...
...
@@ -83,14 +89,31 @@ export class MrItemModel extends ItemModel {
),
);
}
return
[];
}
async
getChildren
():
Promise
<
vscode
.
TreeItem
[]
>
{
const
{
mrVersion
}
=
await
this
.
repository
.
reloadMr
(
this
.
mr
);
const
discussions
=
await
this
.
getMrDiscussions
();
await
this
.
addAllCommentsToVsCode
(
mrVersion
,
discussions
);
const
allUrisWithComments
=
discussions
.
map
(
d
=>
uriForDiscussion
(
this
.
repository
,
this
.
mr
,
d
).
toString
(),
);
const
changedFiles
=
mrVersion
.
diffs
.
map
(
d
=>
new
ChangedFileItem
(
this
.
mr
,
mrVersion
,
d
,
this
.
repository
.
rootFsPath
),
diff
=>
new
ChangedFileItem
(
this
.
mr
,
mrVersion
,
diff
,
this
.
repository
.
rootFsPath
,
uri
=>
allUrisWithComments
.
includes
(
uri
.
toString
()),
),
);
return
[
overview
,
...
changedFiles
];
return
[
this
.
overviewItem
,
...
changedFiles
];
}
private
async
initializeMrDiscussions
(
mrVersion
:
RestMrVersion
):
Promise
<
void
>
{
private
async
addAllCommentsToVsCode
(
mrVersion
:
RestMrVersion
,
discussions
:
GqlTextDiffDiscussion
[],
):
Promise
<
void
>
{
const
gitlabService
=
this
.
repository
.
getGitLabService
();
const
userCanComment
=
await
gitlabService
.
canUserCommentOnMr
(
this
.
mr
);
...
...
@@ -101,11 +124,7 @@ export class MrItemModel extends ItemModel {
);
this
.
setDisposableChildren
([
commentController
]);
const
discussions
=
await
gitlabService
.
getDiscussions
({
issuable
:
this
.
mr
,
});
const
discussionsOnDiff
=
discussions
.
filter
(
isTextDiffDiscussion
);
discussionsOnDiff
.
forEach
(
discussion
=>
{
discussions
.
forEach
(
discussion
=>
{
const
{
position
}
=
firstNoteFrom
(
discussion
);
const
vsThread
=
commentController
.
createCommentThread
(
uriForDiscussion
(
this
.
repository
,
this
.
mr
,
discussion
),
...
...
src/extension.js
浏览文件 @
47f244bc
...
...
@@ -27,7 +27,8 @@ const {
submitEdit
,
createComment
,
}
=
require
(
'
./commands/mr_discussion_commands
'
);
const
{
fileDecorationProvider
}
=
require
(
'
./review/file_decoration_provider
'
);
const
{
hasCommentsDecorationProvider
}
=
require
(
'
./review/has_comments_decoration_provider
'
);
const
{
changeTypeDecorationProvider
}
=
require
(
'
./review/change_type_decoration_provider
'
);
const
{
checkVersion
}
=
require
(
'
./utils/check_version
'
);
const
{
checkoutMrBranch
}
=
require
(
'
./commands/checkout_mr_branch
'
);
...
...
@@ -126,7 +127,8 @@ const activate = context => {
registerCiCompletion
(
context
);
gitExtensionWrapper
.
init
();
context
.
subscriptions
.
push
(
gitExtensionWrapper
);
vscode
.
window
.
registerFileDecorationProvider
(
fileDecorationProvider
);
vscode
.
window
.
registerFileDecorationProvider
(
hasCommentsDecorationProvider
);
vscode
.
window
.
registerFileDecorationProvider
(
changeTypeDecorationProvider
);
checkVersion
(
gitExtensionWrapper
,
context
);
};
...
...
src/review/
fil
e_decoration_provider.test.ts
→
src/review/
change_typ
e_decoration_provider.test.ts
浏览文件 @
47f244bc
import
*
as
vscode
from
'
vscode
'
;
import
{
fileDecorationProvider
,
decorations
}
from
'
./fil
e_decoration_provider
'
;
import
{
ADDED
,
DELETED
,
RENAMED
,
MODIFIED
}
from
'
../constants
'
;
import
{
changeTypeDecorationProvider
,
decorations
}
from
'
./change_typ
e_decoration_provider
'
;
import
{
ADDED
,
DELETED
,
RENAMED
,
MODIFIED
,
CHANGE_TYPE_QUERY_KEY
}
from
'
../constants
'
;
describe
(
'
FileDecoratorProvider
'
,
()
=>
{
it
.
each
`
...
...
@@ -10,9 +10,9 @@ describe('FileDecoratorProvider', () => {
${
RENAMED
}
|
${
decorations
[
RENAMED
]}
${
MODIFIED
}
|
${
decorations
[
MODIFIED
]}
`
(
'
Correctly maps changeType to decorator
'
,
({
changeType
,
decoration
})
=>
{
const
uri
:
vscode
.
Uri
=
vscode
.
Uri
.
file
(
`./test?
changeType
=
${
changeType
}
`
);
const
uri
:
vscode
.
Uri
=
vscode
.
Uri
.
file
(
`./test?
${
CHANGE_TYPE_QUERY_KEY
}
=
${
changeType
}
`
);
const
{
token
}
=
new
vscode
.
CancellationTokenSource
();
const
returnValue
=
fil
eDecorationProvider
.
provideFileDecoration
(
uri
,
token
);
const
returnValue
=
changeTyp
eDecorationProvider
.
provideFileDecoration
(
uri
,
token
);
expect
(
returnValue
).
toEqual
(
decoration
);
});
...
...
src/review/
fil
e_decoration_provider.ts
→
src/review/
change_typ
e_decoration_provider.ts
浏览文件 @
47f244bc
import
*
as
vscode
from
'
vscode
'
;
import
{
ADDED
,
DELETED
,
RENAMED
,
MODIFIED
}
from
'
../constants
'
;
import
{
ADDED
,
DELETED
,
RENAMED
,
MODIFIED
,
CHANGE_TYPE_QUERY_KEY
}
from
'
../constants
'
;
export
const
decorations
:
Record
<
string
,
vscode
.
FileDecoration
|
undefined
>
=
{
[
ADDED
]:
{
...
...
@@ -19,11 +19,11 @@ export const decorations: Record<string, vscode.FileDecoration | undefined> = {
},
};
export
const
fil
eDecorationProvider
:
vscode
.
FileDecorationProvider
=
{
export
const
changeTyp
eDecorationProvider
:
vscode
.
FileDecorationProvider
=
{
provideFileDecoration
:
uri
=>
{
if
(
uri
.
scheme
===
'
file
'
)
{
const
params
=
new
URLSearchParams
(
uri
.
query
);
const
changeType
=
params
.
get
(
'
changeType
'
);
const
changeType
=
params
.
get
(
CHANGE_TYPE_QUERY_KEY
);
if
(
changeType
)
{
return
decorations
[
changeType
];
}
...
...
src/review/has_comments_decoration_provider.test.ts
0 → 100644
浏览文件 @
47f244bc
import
*
as
vscode
from
'
vscode
'
;
import
{
HAS_COMMENTS_QUERY_KEY
}
from
'
../constants
'
;
import
{
hasCommentsDecorationProvider
}
from
'
./has_comments_decoration_provider
'
;
describe
(
'
FileDecoratorProvider
'
,
()
=>
{
it
.
each
`
urlQuery | decoration
${
`?
${
HAS_COMMENTS_QUERY_KEY
}
=true`
}
|
${
'
💬
'
}
${
`?
${
HAS_COMMENTS_QUERY_KEY
}
=false`
}
|
${
undefined
}
${
''
}
|
${
undefined
}
`
(
'
Correctly maps hasComments query to decorator
'
,
async
({
urlQuery
,
decoration
})
=>
{
const
uri
:
vscode
.
Uri
=
vscode
.
Uri
.
file
(
`./test
${
urlQuery
}
`
);
const
{
token
}
=
new
vscode
.
CancellationTokenSource
();
const
returnValue
=
await
hasCommentsDecorationProvider
.
provideFileDecoration
(
uri
,
token
);
expect
(
returnValue
?.
badge
).
toEqual
(
decoration
);
});
});
src/review/has_comments_decoration_provider.ts
0 → 100644
浏览文件 @
47f244bc
import
*
as
vscode
from
'
vscode
'
;
import
{
HAS_COMMENTS_QUERY_KEY
}
from
'
../constants
'
;
export
const
hasCommentsDecorationProvider
:
vscode
.
FileDecorationProvider
=
{
provideFileDecoration
:
uri
=>
{
if
(
uri
.
scheme
!==
'
file
'
)
{
return
undefined
;
}
const
params
=
new
URLSearchParams
(
uri
.
query
);
const
hasComments
=
params
.
get
(
HAS_COMMENTS_QUERY_KEY
)
===
'
true
'
;
if
(
hasComments
)
{
return
{
badge
:
'
💬
'
};
}
return
undefined
;
},
};
src/test_utils/uri.ts
浏览文件 @
47f244bc
...
...
@@ -54,7 +54,7 @@ export class Uri implements vscode.Uri {
}
toJSON
():
string
{
return
JSON
.
stringify
(
this
);
return
JSON
.
stringify
(
{
...
this
}
);
}
static
parse
(
stringUri
:
string
):
Uri
{
...
...
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录