mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-08 13:44:53 +08:00
feat(messages): floating reply area in message-list
Summary: reply text now scrolls to bottom on new draft tests for reply type Test Plan: edgehill --test Reviewers: bengotow Reviewed By: bengotow Differential Revision: https://review.inboxapp.com/D1338
This commit is contained in:
parent
27345e7719
commit
e1ec298d4b
11 changed files with 251 additions and 134 deletions
|
@ -92,26 +92,30 @@ MessageItem = React.createClass
|
|||
@_renderMessageActions()
|
||||
|
||||
_renderMessageActionsTooltip: ->
|
||||
## TODO: Use Tooltip UI Component
|
||||
<span className="msg-actions-tooltip"
|
||||
onClick={=> @setState detailedHeaders: true}>
|
||||
<RetinaImg name={"message-show-more.png"}/></span>
|
||||
return <span></span>
|
||||
## TODO: For now leave blank. There may be an alternative UI in the
|
||||
#future.
|
||||
# <span className="msg-actions-tooltip"
|
||||
# onClick={=> @setState detailedHeaders: true}>
|
||||
# <RetinaImg name={"message-show-more.png"}/></span>
|
||||
|
||||
_renderMessageActions: ->
|
||||
messageActions = ComponentRegistry.findAllViewsByRole('MessageAction')
|
||||
<div className="message-actions">
|
||||
<button className="btn btn-icon" onClick={@_onReply}>
|
||||
<RetinaImg name={"message-reply.png"}/>
|
||||
</button>
|
||||
<button className="btn btn-icon" onClick={@_onReplyAll}>
|
||||
<RetinaImg name={"message-reply-all.png"}/>
|
||||
</button>
|
||||
<button className="btn btn-icon" onClick={@_onForward}>
|
||||
<RetinaImg name={"message-forward.png"}/>
|
||||
</button>
|
||||
<div className="message-actions-wrap">
|
||||
<div className="message-actions">
|
||||
<button className="btn btn-icon" onClick={@_onReply}>
|
||||
<RetinaImg name={"message-reply.png"}/>
|
||||
</button>
|
||||
<button className="btn btn-icon" onClick={@_onReplyAll}>
|
||||
<RetinaImg name={"message-reply-all.png"}/>
|
||||
</button>
|
||||
<button className="btn btn-icon" onClick={@_onForward}>
|
||||
<RetinaImg name={"message-forward.png"}/>
|
||||
</button>
|
||||
|
||||
{<Action thread={@props.thread} message={@props.message} /> for Action in messageActions}
|
||||
{<Action thread={@props.thread} message={@props.message} /> for Action in messageActions}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
_onReply: ->
|
||||
|
@ -129,13 +133,13 @@ MessageItem = React.createClass
|
|||
_renderCollapseControl: ->
|
||||
if @state.detailedHeaders
|
||||
<div className="collapse-control"
|
||||
style={top: "-1px", left: "-17px"}
|
||||
style={top: "4px", left: "-17px"}
|
||||
onClick={=> @setState detailedHeaders: false}>
|
||||
<RetinaImg name={"message-disclosure-triangle-active.png"}/>
|
||||
</div>
|
||||
else
|
||||
<div className="collapse-control inactive"
|
||||
style={top: "-2px"}
|
||||
style={top: "3px"}
|
||||
onClick={=> @setState detailedHeaders: true}>
|
||||
<RetinaImg name={"message-disclosure-triangle.png"}/>
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,7 @@ _ = require 'underscore-plus'
|
|||
React = require 'react'
|
||||
MessageItem = require "./message-item"
|
||||
{Actions, ThreadStore, MessageStore, ComponentRegistry} = require("inbox-exports")
|
||||
{Spinner, ResizableRegion} = require('ui-components')
|
||||
{Spinner, ResizableRegion, RetinaImg} = require('ui-components')
|
||||
|
||||
module.exports =
|
||||
MessageList = React.createClass
|
||||
|
@ -39,6 +39,7 @@ MessageList = React.createClass
|
|||
|
||||
else if didAddDraft
|
||||
@_focusDraft(@refs["composerItem-#{addedDraftIds[0]}"])
|
||||
@_prepareContentForDisplay()
|
||||
|
||||
_focusDraft: (draftDOMNode) ->
|
||||
# We need a 100ms delay so the DOM can finish painting the elements on
|
||||
|
@ -54,6 +55,7 @@ MessageList = React.createClass
|
|||
|
||||
wrapClass = React.addons.classSet
|
||||
"messages-wrap": true
|
||||
"has-reply-area": @_hasReplyArea()
|
||||
"ready": @state.ready
|
||||
|
||||
<div className="message-list" id="message-list">
|
||||
|
@ -65,9 +67,36 @@ MessageList = React.createClass
|
|||
{@_messageListHeaders()}
|
||||
{@_messageComponents()}
|
||||
</div>
|
||||
{@_renderReplyArea()}
|
||||
<Spinner visible={!@state.ready} />
|
||||
</div>
|
||||
|
||||
_renderReplyArea: ->
|
||||
if @_hasReplyArea()
|
||||
<div className="footer-reply-area-wrap" onClick={@_onClickReplyArea}>
|
||||
<div className="footer-reply-area">
|
||||
<RetinaImg name="#{@_replyType()}-footer.png" /><span className="reply-text">Write a reply…</span>
|
||||
</div>
|
||||
</div>
|
||||
else return <div></div>
|
||||
|
||||
_hasReplyArea: ->
|
||||
not _.last(@state.messages)?.draft
|
||||
|
||||
# Either returns "reply" or "reply-all"
|
||||
_replyType: ->
|
||||
lastMsg = _.last(_.filter((@state.messages ? []), (m) -> not m.draft))
|
||||
if lastMsg?.cc.length is 0 and lastMsg?.to.length is 1
|
||||
return "reply"
|
||||
else return "reply-all"
|
||||
|
||||
_onClickReplyArea: ->
|
||||
return unless @state.currentThread?.id
|
||||
if @_replyType() is "reply-all"
|
||||
Actions.composeReplyAll(threadId: @state.currentThread.id)
|
||||
else
|
||||
Actions.composeReply(threadId: @state.currentThread.id)
|
||||
|
||||
# There may be a lot of iframes to load which may take an indeterminate
|
||||
# amount of time. As long as there is more content being painted onto
|
||||
# the page and our height is changing, keep waiting. Then scroll to message.
|
||||
|
@ -141,6 +170,9 @@ MessageList = React.createClass
|
|||
className={className}
|
||||
thread_participants={@_threadParticipants()} />
|
||||
|
||||
unless idx is @state.messages.length - 1
|
||||
components.push <hr className="message-item-divider" />
|
||||
|
||||
components
|
||||
|
||||
_onChange: ->
|
||||
|
|
|
@ -18,7 +18,7 @@ MessageParticipants = React.createClass
|
|||
_renderCollapsed: ->
|
||||
<span className="collapsed-participants">
|
||||
<span className="participant-name from-contact">{@_shortNames(@props.from)}</span>
|
||||
<span className="participant-label to-label"> > </span>
|
||||
<span className="participant-label to-label">To: </span>
|
||||
<span className="participant-name to-contact">{@_shortNames(@props.to)}</span>
|
||||
<span style={if @props.cc?.length > 0 then display:"inline" else display:"none"}>
|
||||
<span className="participant-label cc-label">Cc: </span>
|
||||
|
@ -33,7 +33,6 @@ MessageParticipants = React.createClass
|
|||
_renderExpanded: ->
|
||||
<div className="expanded-participants">
|
||||
<div className="participant-type">
|
||||
<div className="participant-label from-label">From: </div>
|
||||
<div className="participant-name from-contact">{@_fullContact(@props.from)}</div>
|
||||
</div>
|
||||
|
||||
|
@ -54,11 +53,6 @@ MessageParticipants = React.createClass
|
|||
<div className="participant-name cc-contact">{@_fullContact(@props.bcc)}</div>
|
||||
</div>
|
||||
|
||||
<div className="participant-type">
|
||||
<div className="subject-label">Subject: </div>
|
||||
<div className="subject">{@props.subject}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
_shortNames: (contacts=[]) ->
|
||||
|
|
|
@ -66,93 +66,92 @@ user_5 = _.extend _.clone(user_headers),
|
|||
name: "User Five"
|
||||
email: "user5@inboxapp.com"
|
||||
|
||||
testMessages = [
|
||||
(new Message).fromJSON({
|
||||
"id" : "111",
|
||||
"from" : [ user_1 ],
|
||||
"to" : [ user_2 ],
|
||||
"cc" : [ user_3, user_4 ],
|
||||
"bcc" : null,
|
||||
"body" : "Body One",
|
||||
"date" : 1415814587,
|
||||
"draft" : false
|
||||
"files" : [],
|
||||
"unread" : false,
|
||||
"object" : "message",
|
||||
"snippet" : "snippet one...",
|
||||
"subject" : "Subject One",
|
||||
"thread_id" : "thread_12345",
|
||||
"namespace_id" : "nsid"
|
||||
}),
|
||||
(new Message).fromJSON({
|
||||
"id" : "222",
|
||||
"from" : [ user_2 ],
|
||||
"to" : [ user_1 ],
|
||||
"cc" : [ user_3, user_4 ],
|
||||
"bcc" : null,
|
||||
"body" : "Body Two",
|
||||
"date" : 1415814587,
|
||||
"draft" : false
|
||||
"files" : [],
|
||||
"unread" : false,
|
||||
"object" : "message",
|
||||
"snippet" : "snippet Two...",
|
||||
"subject" : "Subject Two",
|
||||
"thread_id" : "thread_12345",
|
||||
"namespace_id" : "nsid"
|
||||
}),
|
||||
(new Message).fromJSON({
|
||||
"id" : "333",
|
||||
"from" : [ user_3 ],
|
||||
"to" : [ user_1 ],
|
||||
"cc" : [ user_2, user_4 ],
|
||||
"bcc" : [],
|
||||
"body" : "Body Three",
|
||||
"date" : 1415814587,
|
||||
"draft" : false
|
||||
"files" : [],
|
||||
"unread" : false,
|
||||
"object" : "message",
|
||||
"snippet" : "snippet Three...",
|
||||
"subject" : "Subject Three",
|
||||
"thread_id" : "thread_12345",
|
||||
"namespace_id" : "nsid"
|
||||
}),
|
||||
(new Message).fromJSON({
|
||||
"id" : "444",
|
||||
"from" : [ user_4 ],
|
||||
"to" : [ user_1 ],
|
||||
"cc" : [],
|
||||
"bcc" : [ user_5 ],
|
||||
"body" : "Body Four",
|
||||
"date" : 1415814587,
|
||||
"draft" : false
|
||||
"files" : [],
|
||||
"unread" : false,
|
||||
"object" : "message",
|
||||
"snippet" : "snippet Four...",
|
||||
"subject" : "Subject Four",
|
||||
"thread_id" : "thread_12345",
|
||||
"namespace_id" : "nsid"
|
||||
}),
|
||||
(new Message).fromJSON({
|
||||
"id" : "555",
|
||||
"from" : [ user_1 ],
|
||||
"to" : [ user_4 ],
|
||||
"cc" : [],
|
||||
"bcc" : [],
|
||||
"body" : "Body Five",
|
||||
"date" : 1415814587,
|
||||
"draft" : false
|
||||
"files" : [],
|
||||
"unread" : false,
|
||||
"object" : "message",
|
||||
"snippet" : "snippet Five...",
|
||||
"subject" : "Subject Five",
|
||||
"thread_id" : "thread_12345",
|
||||
"namespace_id" : "nsid"
|
||||
}),
|
||||
]
|
||||
m1 = (new Message).fromJSON({
|
||||
"id" : "111",
|
||||
"from" : [ user_1 ],
|
||||
"to" : [ user_2 ],
|
||||
"cc" : [ user_3, user_4 ],
|
||||
"bcc" : null,
|
||||
"body" : "Body One",
|
||||
"date" : 1415814587,
|
||||
"draft" : false
|
||||
"files" : [],
|
||||
"unread" : false,
|
||||
"object" : "message",
|
||||
"snippet" : "snippet one...",
|
||||
"subject" : "Subject One",
|
||||
"thread_id" : "thread_12345",
|
||||
"namespace_id" : "nsid"
|
||||
})
|
||||
m2 = (new Message).fromJSON({
|
||||
"id" : "222",
|
||||
"from" : [ user_2 ],
|
||||
"to" : [ user_1 ],
|
||||
"cc" : [ user_3, user_4 ],
|
||||
"bcc" : null,
|
||||
"body" : "Body Two",
|
||||
"date" : 1415814587,
|
||||
"draft" : false
|
||||
"files" : [],
|
||||
"unread" : false,
|
||||
"object" : "message",
|
||||
"snippet" : "snippet Two...",
|
||||
"subject" : "Subject Two",
|
||||
"thread_id" : "thread_12345",
|
||||
"namespace_id" : "nsid"
|
||||
})
|
||||
m3 = (new Message).fromJSON({
|
||||
"id" : "333",
|
||||
"from" : [ user_3 ],
|
||||
"to" : [ user_1 ],
|
||||
"cc" : [ user_2, user_4 ],
|
||||
"bcc" : [],
|
||||
"body" : "Body Three",
|
||||
"date" : 1415814587,
|
||||
"draft" : false
|
||||
"files" : [],
|
||||
"unread" : false,
|
||||
"object" : "message",
|
||||
"snippet" : "snippet Three...",
|
||||
"subject" : "Subject Three",
|
||||
"thread_id" : "thread_12345",
|
||||
"namespace_id" : "nsid"
|
||||
})
|
||||
m4 = (new Message).fromJSON({
|
||||
"id" : "444",
|
||||
"from" : [ user_4 ],
|
||||
"to" : [ user_1 ],
|
||||
"cc" : [],
|
||||
"bcc" : [ user_5 ],
|
||||
"body" : "Body Four",
|
||||
"date" : 1415814587,
|
||||
"draft" : false
|
||||
"files" : [],
|
||||
"unread" : false,
|
||||
"object" : "message",
|
||||
"snippet" : "snippet Four...",
|
||||
"subject" : "Subject Four",
|
||||
"thread_id" : "thread_12345",
|
||||
"namespace_id" : "nsid"
|
||||
})
|
||||
m5 = (new Message).fromJSON({
|
||||
"id" : "555",
|
||||
"from" : [ user_1 ],
|
||||
"to" : [ user_4 ],
|
||||
"cc" : [],
|
||||
"bcc" : [],
|
||||
"body" : "Body Five",
|
||||
"date" : 1415814587,
|
||||
"draft" : false
|
||||
"files" : [],
|
||||
"unread" : false,
|
||||
"object" : "message",
|
||||
"snippet" : "snippet Five...",
|
||||
"subject" : "Subject Five",
|
||||
"thread_id" : "thread_12345",
|
||||
"namespace_id" : "nsid"
|
||||
})
|
||||
testMessages = [m1, m2, m3, m4, m5]
|
||||
draftMessages = [
|
||||
(new Message).fromJSON({
|
||||
"id" : "666",
|
||||
|
@ -273,3 +272,27 @@ describe "MessageList", ->
|
|||
expect(items.length).toBe 1
|
||||
|
||||
expect(items.length).toBe 1
|
||||
|
||||
describe "reply type", ->
|
||||
it "prompts for a reply when there's only one participant", ->
|
||||
MessageStore._items = [m3, m5]
|
||||
MessageStore.trigger()
|
||||
@message_list.setState currentThread: test_thread
|
||||
expect(@message_list._replyType()).toBe "reply"
|
||||
cs = TestUtils.scryRenderedDOMComponentsWithClass(@message_list, "footer-reply-area")
|
||||
expect(cs.length).toBe 1
|
||||
|
||||
it "prompts for a reply-all when there's more then one participant", ->
|
||||
MessageStore._items = [m5, m3]
|
||||
MessageStore.trigger()
|
||||
@message_list.setState currentThread: test_thread
|
||||
expect(@message_list._replyType()).toBe "reply-all"
|
||||
cs = TestUtils.scryRenderedDOMComponentsWithClass(@message_list, "footer-reply-area")
|
||||
expect(cs.length).toBe 1
|
||||
|
||||
it "hides the reply type if the last message is a draft", ->
|
||||
MessageStore._items = [m5, m3, draftMessages[0]]
|
||||
MessageStore.trigger()
|
||||
@message_list.setState currentThread: test_thread
|
||||
cs = TestUtils.scryRenderedDOMComponentsWithClass(@message_list, "footer-reply-area")
|
||||
expect(cs.length).toBe 0
|
||||
|
|
|
@ -82,6 +82,9 @@
|
|||
// top:0; left:0; right:0; bottom:0;
|
||||
opacity:0;
|
||||
transition: opacity 0s;
|
||||
&.has-reply-area {
|
||||
padding-bottom: 63px; // height of reply footer
|
||||
}
|
||||
}
|
||||
.messages-wrap.ready {
|
||||
opacity:1;
|
||||
|
@ -93,37 +96,61 @@
|
|||
width: 100%;
|
||||
|
||||
margin: 0 auto;
|
||||
padding: @spacing-double 0;
|
||||
padding: @spacing-standard 0 @spacing-double 0;
|
||||
&:first-child { padding-top: 0; }
|
||||
|
||||
border-bottom: 1px solid @border-primary-bg;
|
||||
&:last-child { border-bottom: 0; }
|
||||
|
||||
&.collapsed {
|
||||
padding: @spacing-standard @spacing-double;
|
||||
}
|
||||
}
|
||||
|
||||
.message-item-divider {
|
||||
border-top: 2px solid @border-secondary-bg;
|
||||
height: 3px;
|
||||
background: @background-secondary;
|
||||
border-bottom: 1px solid @border-primary-bg;
|
||||
margin: 0;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
position: relative;
|
||||
font-size: @font-size-small;
|
||||
border-bottom: 1px solid @border-color-divider;
|
||||
padding-bottom: @spacing-standard;
|
||||
padding-top: 5px;
|
||||
|
||||
.message-actions-wrap {
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.message-actions {
|
||||
display: inline-block;
|
||||
width: 120px;
|
||||
height: 22px;
|
||||
border: 1px solid @border-color-divider;
|
||||
border-radius: 11px;
|
||||
|
||||
z-index: 4;
|
||||
margin-top: 0.35em;
|
||||
margin-left: 0.5em;
|
||||
text-align: right;
|
||||
text-align: center;
|
||||
|
||||
.btn-icon {
|
||||
opacity: 0.75;
|
||||
padding: 0 @spacing-half;
|
||||
&:last-child { padding-right: 0; }
|
||||
margin-left: 10px;
|
||||
margin-right: 0;
|
||||
height: 20px;
|
||||
line-height: 10px;
|
||||
border-radius: 0;
|
||||
border-right: 1px solid @border-color-divider;
|
||||
&:last-child { border-right: 0; }
|
||||
margin: 0;
|
||||
&:active {background: transparent;}
|
||||
}
|
||||
}
|
||||
|
||||
.message-time {
|
||||
.message-time {
|
||||
z-index: 2; position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
@ -141,6 +168,7 @@
|
|||
.message-header-right {
|
||||
z-index: 4; position: relative;
|
||||
float: right;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -149,7 +177,7 @@
|
|||
width: 100%;
|
||||
max-width: @message-max-width;
|
||||
margin: 0 auto;
|
||||
padding: @spacing-standard @spacing-double;
|
||||
padding: 0 @spacing-double @spacing-standard @spacing-double;
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
|
@ -175,6 +203,34 @@
|
|||
|
||||
.collapse-control:hover {cursor: pointer;}
|
||||
|
||||
.footer-reply-area-wrap {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
color: @text-color-very-subtle;
|
||||
border-top: 1px solid @border-color-divider;
|
||||
background: @background-primary;
|
||||
z-index: 10;
|
||||
|
||||
&:hover {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.footer-reply-area {
|
||||
width: 100%;
|
||||
max-width: @message-max-width;
|
||||
margin: 0 auto;
|
||||
padding: 20px @spacing-double;
|
||||
}
|
||||
.reply-text {
|
||||
display: inline-block;
|
||||
margin-left: 0.5em;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.attachments-area {
|
||||
padding-top: @spacing-standard;
|
||||
|
@ -195,8 +251,7 @@
|
|||
.from-label, .to-label, .cc-label, .bcc-label, .subject-label {
|
||||
color: @text-color-very-subtle;
|
||||
}
|
||||
.to-label { font-weight: 600; }
|
||||
.cc-label, .bcc-label, .subject-label { margin-left: @spacing-standard; }
|
||||
.to-label, .cc-label, .bcc-label, .subject-label { margin-left: @spacing-standard; }
|
||||
.to-contact, .cc-contact, .to-everyone {
|
||||
color: @text-color-very-subtle;
|
||||
}
|
||||
|
@ -218,25 +273,32 @@
|
|||
.from-label, .to-label, .cc-label, .bcc-label, .subject-label {
|
||||
float: left;
|
||||
display: block;
|
||||
font-weight: 400;
|
||||
font-weight: @font-weight-normal;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.from-contact, .to-contact, .cc-contact, .subject {
|
||||
padding-left: 4.4em;
|
||||
font-weight: 500;
|
||||
.from-contact, .subject {
|
||||
font-weight: @font-weight-semi-bold;
|
||||
}
|
||||
|
||||
.from-label { margin-right: 1em; }
|
||||
.to-label, .cc-label { margin-right: 2.15em; }
|
||||
.bcc-label { margin-right: 1.7em; }
|
||||
// .from-label { margin-right: 1em; }
|
||||
.to-label, .cc-label { margin-right: 0.5em; }
|
||||
.bcc-label { margin-right: 0; }
|
||||
|
||||
.participant-primary {
|
||||
color: @text-color;
|
||||
color: @text-color-very-subtle;
|
||||
}
|
||||
.participant-secondary {
|
||||
color: @text-color-very-subtle;
|
||||
}
|
||||
.from-contact {
|
||||
.participant-primary {
|
||||
color: @text-color;
|
||||
}
|
||||
.participant-secondary {
|
||||
color: @text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
BIN
static/images/message-list/forward-message-header@2x.png
Normal file
BIN
static/images/message-list/forward-message-header@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 348 B |
BIN
static/images/message-list/reply-all-footer@2x.png
Normal file
BIN
static/images/message-list/reply-all-footer@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 894 B |
BIN
static/images/message-list/reply-all-message-header@2x.png
Normal file
BIN
static/images/message-list/reply-all-message-header@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 921 B |
BIN
static/images/message-list/reply-footer@2x.png
Normal file
BIN
static/images/message-list/reply-footer@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 537 B |
BIN
static/images/message-list/reply-message-header@2x.png
Normal file
BIN
static/images/message-list/reply-message-header@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 547 B |
|
@ -128,7 +128,9 @@ body.is-blurred {
|
|||
border-bottom: 1px solid @border-color-divider;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
|
||||
|
||||
box-shadow: 0px 0px 6px rgba(0,0,0,0.09);
|
||||
|
||||
// prevent flexbox from ever, ever resizing toolbars, no matter
|
||||
// how much it thinks other content is being squished
|
||||
min-height: 50px;
|
||||
|
@ -229,4 +231,4 @@ body.platform-win32 {
|
|||
.flexbox-handle-horizontal {
|
||||
cursor:ew-resize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue