issue: Issue sharing feature - support notification
@679c6c0c5fd26078b0e19a75fbf67d49abcbaee1
--- app/assets/stylesheets/less/_page.less
+++ app/assets/stylesheets/less/_page.less
... | ... | @@ -3066,6 +3066,14 @@ |
3066 | 3066 |
&.rejected {background: #F39C12;} |
3067 | 3067 |
&.conflict {background: #C0392B;} |
3068 | 3068 |
&.resolved {background: #468847;} |
3069 |
+ &.sharer-added { |
|
3070 |
+ background-color: #B2EBF2; |
|
3071 |
+ color: #0097A7; |
|
3072 |
+ } |
|
3073 |
+ &.sharer-deleted { |
|
3074 |
+ background-color: #FFCCBC; |
|
3075 |
+ color: #F4511E; |
|
3076 |
+ } |
|
3069 | 3077 |
} |
3070 | 3078 |
|
3071 | 3079 |
em { font-size:12px; color:#7F8C8D; margin-right:5px;} |
--- app/controllers/api/IssueApi.java
+++ app/controllers/api/IssueApi.java
... | ... | @@ -39,6 +39,7 @@ |
39 | 39 |
import java.util.*; |
40 | 40 |
|
41 | 41 |
import static controllers.UserApp.MAX_FETCH_USERS; |
42 |
+import static controllers.UserApp.currentUser; |
|
42 | 43 |
import static controllers.api.UserApi.createUserNode; |
43 | 44 |
import static play.libs.Json.toJson; |
44 | 45 |
|
... | ... | @@ -602,10 +603,6 @@ |
602 | 603 |
return badRequest(Json.newObject().put("message", "Expecting Json data")); |
603 | 604 |
} |
604 | 605 |
|
605 |
- if(noSharer(json.findValue("sharer"))){ |
|
606 |
- return badRequest(Json.newObject().put("message", "No sharer")); |
|
607 |
- } |
|
608 |
- |
|
609 | 606 |
Project project = Project.findByOwnerAndProjectName(owner, projectName); |
610 | 607 |
Issue issue = Issue.findByNumber(project, number); |
611 | 608 |
if (!AccessControl.isAllowed(UserApp.currentUser(), issue.asResource(), |
... | ... | @@ -613,26 +610,53 @@ |
613 | 610 |
return forbidden(Json.newObject().put("message", "Permission denied")); |
614 | 611 |
} |
615 | 612 |
|
616 |
- ObjectNode result = Json.newObject(); |
|
617 |
- String action = json.findValue("action").asText(); |
|
613 |
+ JsonNode sharer = json.findValue("sharer"); |
|
614 |
+ if(noSharer(sharer)){ |
|
615 |
+ return badRequest(Json.newObject().put("message", "No sharer")); |
|
616 |
+ } |
|
618 | 617 |
|
619 |
- for(JsonNode sharerLoginId: json.findValue("sharer")){ |
|
620 |
- switch (action) { |
|
621 |
- case "delete": |
|
622 |
- removeSharer(issue, sharerLoginId.asText()); |
|
623 |
- result.put("action", "deleted"); |
|
624 |
- break; |
|
625 |
- case "add": |
|
626 |
- addSharer(issue, sharerLoginId.asText()); |
|
627 |
- result.put("action", "added"); |
|
628 |
- break; |
|
629 |
- default: |
|
630 |
- result.put("action", "Do nothing"); |
|
618 |
+ final String action = json.findValue("action").asText(); |
|
619 |
+ |
|
620 |
+ ObjectNode result = changeSharer(sharer, issue, action); |
|
621 |
+ sendNotification(sharer, issue, action); |
|
622 |
+ |
|
623 |
+ return ok(result); |
|
624 |
+ } |
|
625 |
+ |
|
626 |
+ private static ObjectNode changeSharer(JsonNode sharer, Issue issue, String action) { |
|
627 |
+ ObjectNode result = Json.newObject(); |
|
628 |
+ for (JsonNode sharerLoginId : sharer) { |
|
629 |
+ if ("add".equalsIgnoreCase(action)) { |
|
630 |
+ addSharer(issue, sharerLoginId.asText()); |
|
631 |
+ result.put("action", "added"); |
|
632 |
+ } else if ("delete".equalsIgnoreCase(action)) { |
|
633 |
+ result.put("action", "deleted"); |
|
634 |
+ removeSharer(issue, sharerLoginId.asText()); |
|
635 |
+ } else { |
|
636 |
+ play.Logger.error("Unknown issue sharing action: " + issue + ":" + action + " by " + currentUser()); |
|
637 |
+ result.put("action", "Do nothing. Unsupported action: " + action); |
|
631 | 638 |
} |
632 | 639 |
result.put("sharer", User.findByLoginId(sharerLoginId.asText()).getDisplayName()); |
633 | 640 |
} |
641 |
+ return result; |
|
642 |
+ } |
|
634 | 643 |
|
635 |
- return ok(result); |
|
644 |
+ private static void sendNotification(JsonNode sharer, Issue issue, String action) { |
|
645 |
+ Runnable preUpdateHook = new Runnable() { |
|
646 |
+ @Override |
|
647 |
+ public void run() { |
|
648 |
+ for(JsonNode sharerLoginId: sharer){ |
|
649 |
+ addSharerChangedNotification(issue, sharerLoginId.asText(), action); |
|
650 |
+ } |
|
651 |
+ } |
|
652 |
+ }; |
|
653 |
+ preUpdateHook.run(); |
|
654 |
+ } |
|
655 |
+ |
|
656 |
+ |
|
657 |
+ private static void addSharerChangedNotification(Issue issue, String sharerLoginId, String action) { |
|
658 |
+ NotificationEvent notiEvent = NotificationEvent.afterIssueSharerChanged(issue, sharerLoginId, action); |
|
659 |
+ IssueEvent.addFromNotificationEventWithoutSkipEvent(notiEvent, issue, UserApp.currentUser().loginId); |
|
636 | 660 |
} |
637 | 661 |
|
638 | 662 |
private static boolean noSharer(JsonNode sharers) { |
--- app/models/IssueEvent.java
+++ app/models/IssueEvent.java
... | ... | @@ -1,23 +1,10 @@ |
1 | 1 |
/** |
2 |
- * Yobi, Project Hosting SW |
|
3 |
- * |
|
4 |
- * Copyright 2013 NAVER Corp. |
|
5 |
- * http://yobi.io |
|
6 |
- * |
|
7 |
- * @author Yi EungJun |
|
8 |
- * |
|
9 |
- * Licensed under the Apache License, Version 2.0 (the "License"); |
|
10 |
- * you may not use this file except in compliance with the License. |
|
11 |
- * You may obtain a copy of the License at |
|
12 |
- * |
|
13 |
- * http://www.apache.org/licenses/LICENSE-2.0 |
|
14 |
- * |
|
15 |
- * Unless required by applicable law or agreed to in writing, software |
|
16 |
- * distributed under the License is distributed on an "AS IS" BASIS, |
|
17 |
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
18 |
- * See the License for the specific language governing permissions and |
|
19 |
- * limitations under the License. |
|
20 |
- */ |
|
2 |
+ * Yona, 21st Century Project Hosting SW |
|
3 |
+ * <p> |
|
4 |
+ * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. |
|
5 |
+ * https://yona.io |
|
6 |
+ **/ |
|
7 |
+ |
|
21 | 8 |
package models; |
22 | 9 |
|
23 | 10 |
import models.enumeration.EventType; |
... | ... | @@ -91,8 +78,7 @@ |
91 | 78 |
.orderBy("id desc").setMaxRows(1).findUnique(); |
92 | 79 |
|
93 | 80 |
if (lastEvent != null) { |
94 |
- if (lastEvent.eventType == event.eventType && |
|
95 |
- StringUtils.equals(event.senderLoginId, lastEvent.senderLoginId)) { |
|
81 |
+ if (isSameUserEventAsPrevious(event, lastEvent)) { |
|
96 | 82 |
// A -> B, B -> C ==> A -> C |
97 | 83 |
event.oldValue = lastEvent.oldValue; |
98 | 84 |
lastEvent.delete(); |
... | ... | @@ -107,6 +93,45 @@ |
107 | 93 |
} |
108 | 94 |
|
109 | 95 |
event.save(); |
96 |
+ } |
|
97 |
+ |
|
98 |
+ |
|
99 |
+ /** |
|
100 |
+ * It is nearly same as {@link #add(IssueEvent)} except that it doesn't skip waypoint events. |
|
101 |
+ * |
|
102 |
+ * For example, if events of an issue are occurred continuously |
|
103 |
+ * A -> B -> C, then {@link #add(IssueEvent)} method skip B. |
|
104 |
+ * |
|
105 |
+ * This method doesn't skip B and leave it. |
|
106 |
+ * |
|
107 |
+ * @param event |
|
108 |
+ */ |
|
109 |
+ public static void addWithoutSkipEvent(IssueEvent event) { |
|
110 |
+ Date draftDate = DateTime.now().minusMillis(DRAFT_TIME_IN_MILLIS).toDate(); |
|
111 |
+ |
|
112 |
+ IssueEvent lastEvent = IssueEvent.find.where() |
|
113 |
+ .eq("issue.id", event.issue.id) |
|
114 |
+ .gt("created", draftDate) |
|
115 |
+ .orderBy("id desc").setMaxRows(1).findUnique(); |
|
116 |
+ |
|
117 |
+ if (lastEvent != null) { |
|
118 |
+ if (isSameUserEventAsPrevious(event, lastEvent) && |
|
119 |
+ isRevertingTheValue(event, lastEvent)) { |
|
120 |
+ lastEvent.delete(); |
|
121 |
+ return; |
|
122 |
+ } |
|
123 |
+ } |
|
124 |
+ event.save(); |
|
125 |
+ } |
|
126 |
+ |
|
127 |
+ private static boolean isRevertingTheValue(IssueEvent event, IssueEvent lastEvent) { |
|
128 |
+ return StringUtils.equals(event.oldValue, lastEvent.newValue) && |
|
129 |
+ StringUtils.equals(event.newValue, lastEvent.oldValue); |
|
130 |
+ } |
|
131 |
+ |
|
132 |
+ private static boolean isSameUserEventAsPrevious(IssueEvent event, IssueEvent lastEvent) { |
|
133 |
+ return lastEvent.eventType == event.eventType && |
|
134 |
+ StringUtils.equals(event.senderLoginId, lastEvent.senderLoginId); |
|
110 | 135 |
} |
111 | 136 |
|
112 | 137 |
/** |
... | ... | @@ -130,6 +155,18 @@ |
130 | 155 |
add(event); |
131 | 156 |
} |
132 | 157 |
|
158 |
+ public static void addFromNotificationEventWithoutSkipEvent(NotificationEvent notiEvent, Issue updatedIssue, |
|
159 |
+ String senderLoginId) { |
|
160 |
+ IssueEvent event = new IssueEvent(); |
|
161 |
+ event.created = notiEvent.created; |
|
162 |
+ event.senderLoginId = senderLoginId; |
|
163 |
+ event.issue = updatedIssue; |
|
164 |
+ event.eventType = notiEvent.eventType; |
|
165 |
+ event.oldValue = notiEvent.oldValue; |
|
166 |
+ event.newValue = notiEvent.newValue; |
|
167 |
+ addWithoutSkipEvent(event); |
|
168 |
+ } |
|
169 |
+ |
|
133 | 170 |
@Override |
134 | 171 |
public Date getDate() { |
135 | 172 |
return created; |
--- app/models/NotificationEvent.java
+++ app/models/NotificationEvent.java
... | ... | @@ -1,7 +1,7 @@ |
1 | 1 |
/** |
2 | 2 |
* Yona, 21st Century Project Hosting SW |
3 | 3 |
* <p> |
4 |
- * Copyright Yona & Yobi Authors & NAVER Corp. |
|
4 |
+ * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. |
|
5 | 5 |
* https://yona.io |
6 | 6 |
**/ |
7 | 7 |
package models; |
... | ... | @@ -14,7 +14,6 @@ |
14 | 14 |
import models.resource.GlobalResource; |
15 | 15 |
import models.resource.Resource; |
16 | 16 |
import models.resource.ResourceConvertible; |
17 |
-import models.Webhook; |
|
18 | 17 |
import org.apache.commons.collections.CollectionUtils; |
19 | 18 |
import org.apache.commons.collections.Predicate; |
20 | 19 |
import org.apache.commons.lang3.StringUtils; |
... | ... | @@ -108,6 +107,10 @@ |
108 | 107 |
return oldValue; |
109 | 108 |
} |
110 | 109 |
|
110 |
+ public String getNewValue() { |
|
111 |
+ return newValue; |
|
112 |
+ } |
|
113 |
+ |
|
111 | 114 |
@Transient |
112 | 115 |
public String getMessage() { |
113 | 116 |
return getMessage(Lang.defaultLang()); |
... | ... | @@ -189,7 +192,14 @@ |
189 | 192 |
} |
190 | 193 |
case ISSUE_MOVED: |
191 | 194 |
return Messages.get(lang, "notification.type.issue.moved", oldValue, newValue); |
195 |
+ case ISSUE_SHARER_CHANGED: |
|
196 |
+ if (StringUtils.isNotBlank(newValue)) { |
|
197 |
+ return Messages.get(lang, "notification.issue.sharer.added", User.findByLoginId(newValue).getDisplayName()); |
|
198 |
+ } else if (StringUtils.isNotBlank(oldValue)) { |
|
199 |
+ return Messages.get(lang, "notification.issue.sharer.deleted"); |
|
200 |
+ } |
|
192 | 201 |
default: |
202 |
+ play.Logger.error("Unknown event message: " + this); |
|
193 | 203 |
return null; |
194 | 204 |
} |
195 | 205 |
} |
... | ... | @@ -360,8 +370,7 @@ |
360 | 370 |
.orderBy("id desc").setMaxRows(1).findUnique(); |
361 | 371 |
|
362 | 372 |
if (lastEvent != null) { |
363 |
- if (lastEvent.eventType == event.eventType && |
|
364 |
- event.senderId.equals(lastEvent.senderId)) { |
|
373 |
+ if (isSameUserEventAsPrevious(event, lastEvent)) { |
|
365 | 374 |
// If the last event is A -> B and the current event is B -> C, |
366 | 375 |
// they are merged into the new event A -> C. |
367 | 376 |
event.oldValue = lastEvent.getOldValue(); |
... | ... | @@ -381,6 +390,55 @@ |
381 | 390 |
} |
382 | 391 |
event.save(); |
383 | 392 |
event.saveManyToManyAssociations("receivers"); |
393 |
+ } |
|
394 |
+ |
|
395 |
+ public static void addWithoutSkipEvent(NotificationEvent event) { |
|
396 |
+ if (event.notificationMail == null) { |
|
397 |
+ event.notificationMail = new NotificationMail(); |
|
398 |
+ event.notificationMail.notificationEvent = event; |
|
399 |
+ } |
|
400 |
+ |
|
401 |
+ Date draftDate = DateTime.now().minusMillis(EventConstants.DRAFT_TIME_IN_MILLIS).toDate(); |
|
402 |
+ |
|
403 |
+ NotificationEvent lastEvent = NotificationEvent.find.where() |
|
404 |
+ .eq("resourceId", event.resourceId) |
|
405 |
+ .eq("resourceType", event.resourceType) |
|
406 |
+ .gt("created", draftDate) |
|
407 |
+ .orderBy("id desc").setMaxRows(1).findUnique(); |
|
408 |
+ |
|
409 |
+ if (lastEvent != null) { |
|
410 |
+ if (isSameUserEventAsPrevious(event, lastEvent) && |
|
411 |
+ isRevertingTheValue(event, lastEvent)) { |
|
412 |
+ lastEvent.delete(); |
|
413 |
+ return; |
|
414 |
+ } |
|
415 |
+ } |
|
416 |
+ |
|
417 |
+ if(isAddingSharerEvent(event)){ |
|
418 |
+ filterReceivers(event); |
|
419 |
+ } |
|
420 |
+ |
|
421 |
+ if (event.receivers.isEmpty()) { |
|
422 |
+ return; |
|
423 |
+ } |
|
424 |
+ event.save(); |
|
425 |
+ event.saveManyToManyAssociations("receivers"); |
|
426 |
+ } |
|
427 |
+ |
|
428 |
+ private static boolean isSameUserEventAsPrevious(NotificationEvent event, NotificationEvent lastEvent) { |
|
429 |
+ return lastEvent.eventType == event.eventType && |
|
430 |
+ event.senderId.equals(lastEvent.senderId); |
|
431 |
+ } |
|
432 |
+ |
|
433 |
+ private static boolean isRevertingTheValue(NotificationEvent event, NotificationEvent lastEvent) { |
|
434 |
+ return StringUtils.equals(event.oldValue, lastEvent.newValue) && |
|
435 |
+ StringUtils.equals(event.newValue, lastEvent.oldValue); |
|
436 |
+ } |
|
437 |
+ |
|
438 |
+ private static boolean isAddingSharerEvent(NotificationEvent event) { |
|
439 |
+ return event.eventType.equals(EventType.ISSUE_SHARER_CHANGED) |
|
440 |
+ && StringUtils.isBlank(event.oldValue) |
|
441 |
+ && StringUtils.isNotBlank(event.newValue); |
|
384 | 442 |
} |
385 | 443 |
|
386 | 444 |
private static void filterReceivers(final NotificationEvent event) { |
... | ... | @@ -791,6 +849,30 @@ |
791 | 849 |
NotificationEvent.add(notiEvent); |
792 | 850 |
|
793 | 851 |
return notiEvent; |
852 |
+ } |
|
853 |
+ |
|
854 |
+ public static NotificationEvent afterIssueSharerChanged(Issue issue, String sharerLoginId, String action) { |
|
855 |
+ NotificationEvent notiEvent = createFromCurrentUser(issue); |
|
856 |
+ notiEvent.title = formatReplyTitle(issue); |
|
857 |
+ notiEvent.receivers = findSharer(sharerLoginId); |
|
858 |
+ notiEvent.eventType = ISSUE_SHARER_CHANGED; |
|
859 |
+ if (IssueSharer.ADD.equalsIgnoreCase(action)) { |
|
860 |
+ notiEvent.oldValue = ""; |
|
861 |
+ notiEvent.newValue = sharerLoginId; |
|
862 |
+ } else if (IssueSharer.DELETE.equalsIgnoreCase(action)) { |
|
863 |
+ notiEvent.oldValue = sharerLoginId; |
|
864 |
+ notiEvent.newValue = ""; |
|
865 |
+ } |
|
866 |
+ |
|
867 |
+ NotificationEvent.addWithoutSkipEvent(notiEvent); |
|
868 |
+ |
|
869 |
+ return notiEvent; |
|
870 |
+ } |
|
871 |
+ |
|
872 |
+ private static Set<User> findSharer(String sharerLoginId) { |
|
873 |
+ Set<User> receivers = new HashSet<>(); |
|
874 |
+ receivers.add(User.findByLoginId(sharerLoginId)); |
|
875 |
+ return receivers; |
|
794 | 876 |
} |
795 | 877 |
|
796 | 878 |
private static Set<User> getReceiversForIssueBodyChanged(String oldBody, Issue issue) { |
... | ... | @@ -1266,4 +1348,21 @@ |
1266 | 1348 |
webhookRequest(COMMENT_UPDATED, comment, false); |
1267 | 1349 |
NotificationEvent.add(forUpdatedComment(comment, UserApp.currentUser())); |
1268 | 1350 |
} |
1351 |
+ |
|
1352 |
+ @Override |
|
1353 |
+ public String toString() { |
|
1354 |
+ return "NotificationEvent{" + |
|
1355 |
+ "id=" + id + |
|
1356 |
+ ", title='" + title + '\'' + |
|
1357 |
+ ", senderId=" + senderId + |
|
1358 |
+ ", receivers=" + receivers + |
|
1359 |
+ ", created=" + created + |
|
1360 |
+ ", resourceType=" + resourceType + |
|
1361 |
+ ", resourceId='" + resourceId + '\'' + |
|
1362 |
+ ", eventType=" + eventType + |
|
1363 |
+ ", oldValue='" + oldValue + '\'' + |
|
1364 |
+ ", newValue='" + newValue + '\'' + |
|
1365 |
+ ", notificationMail=" + notificationMail + |
|
1366 |
+ '}'; |
|
1367 |
+ } |
|
1269 | 1368 |
} |
--- app/models/enumeration/EventType.java
+++ app/models/enumeration/EventType.java
... | ... | @@ -1,7 +1,7 @@ |
1 | 1 |
/** |
2 | 2 |
* Yona, 21st Century Project Hosting SW |
3 | 3 |
* <p> |
4 |
- * Copyright Yona & Yobi Authors & NAVER Corp. |
|
4 |
+ * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. |
|
5 | 5 |
* https://yona.io |
6 | 6 |
**/ |
7 | 7 |
package models.enumeration; |
... | ... | @@ -34,7 +34,8 @@ |
34 | 34 |
REVIEW_THREAD_STATE_CHANGED("notification.type.review.state.changed", 18), |
35 | 35 |
ORGANIZATION_MEMBER_ENROLL_REQUEST("notification.organization.type.member.enroll",19), |
36 | 36 |
COMMENT_UPDATED("notification.type.comment.updated", 20), |
37 |
- ISSUE_MOVED("notification.type.issue.is.moved", 21); |
|
37 |
+ ISSUE_MOVED("notification.type.issue.is.moved", 21), |
|
38 |
+ ISSUE_SHARER_CHANGED("notification.type.issue.sharer.changed", 22); |
|
38 | 39 |
|
39 | 40 |
private String descr; |
40 | 41 |
|
--- app/views/issue/partial_comments.scala.html
+++ app/views/issue/partial_comments.scala.html
... | ... | @@ -1,7 +1,7 @@ |
1 | 1 |
@** |
2 | 2 |
* Yona, 21st Century Project Hosting SW |
3 | 3 |
* |
4 |
-* Copyright Yona & Yobi Authors & NAVER Corp. |
|
4 |
+* Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp. |
|
5 | 5 |
* https://yona.io |
6 | 6 |
**@ |
7 | 7 |
@(project:Project, issue:Issue) |
... | ... | @@ -178,6 +178,15 @@ |
178 | 178 |
@Html(Messages("issue.event.referred",linkToUser(user.loginId, user.getDisplayName),linkToPullRequest(pull))) |
179 | 179 |
} |
180 | 180 |
} |
181 |
+ case EventType.ISSUE_SHARER_CHANGED => { |
|
182 |
+ @if(StringUtils.isBlank(event.oldValue) && StringUtils.isNotBlank(event.newValue)){ |
|
183 |
+ <span class="state sharer-added">@Messages("issue.sharer")</span> |
|
184 |
+ @Html(Messages("issue.event.sharer.added", linkToUser(user.loginId, user.getPureNameOnly), linkToUser(event.newValue, User.findByLoginId(event.newValue).getPureNameOnly))) |
|
185 |
+ } else { |
|
186 |
+ <span class="state sharer-deleted">@Messages("issue.event.sharer.deleted.title")</span> |
|
187 |
+ @Html(Messages("issue.event.sharer.deleted", linkToUser(user.loginId, user.getPureNameOnly), linkToUser(event.oldValue, User.findByLoginId(event.oldValue).getPureNameOnly))) |
|
188 |
+ } |
|
189 |
+ } |
|
181 | 190 |
case _ => { |
182 | 191 |
@event.newValue by @linkToUser(user.loginId, user.getDisplayName) |
183 | 192 |
} |
--- conf/messages
+++ conf/messages
... | ... | @@ -284,6 +284,10 @@ |
284 | 284 |
issue.event.open = {0} reopened this issue |
285 | 285 |
issue.event.referred = {0} mentioned this issue in {1} |
286 | 286 |
issue.event.referred.title = mentioned |
287 |
+issue.event.sharer.added = {0} shared current issue to {1} |
|
288 |
+issue.event.sharer.added.title = Issue sharing |
|
289 |
+issue.event.sharer.deleted = {0} cancelled issue sharing with {1} |
|
290 |
+issue.event.sharer.deleted.title = Cancelled |
|
287 | 291 |
issue.event.unassigned = {0} set assignee to unassigned |
288 | 292 |
issue.is.empty = No issue found |
289 | 293 |
issue.label = Issue Label |
... | ... | @@ -405,6 +409,8 @@ |
405 | 409 |
notification.issue.closed = Issue has been closed |
406 | 410 |
notification.issue.reopened = Issue has been reopened |
407 | 411 |
notification.issue.unassigned = Issue has been unassigned |
412 |
+notification.issue.sharer.added = Issue is shared with {0} |
|
413 |
+notification.issue.sharer.deleted = Issue sharing state is changed |
|
408 | 414 |
notification.linkToView = View it on {0} |
409 | 415 |
notification.linkToViewHtml = <a href="{1}" target="{2}">View it on {0}</a> |
410 | 416 |
notification.member.enroll.accept = Accepted as a member. |
--- conf/messages.ko-KR
+++ conf/messages.ko-KR
... | ... | @@ -284,6 +284,10 @@ |
284 | 284 |
issue.event.open = {0}님이 이 이슈를 다시 열었습니다. |
285 | 285 |
issue.event.referred = {0} 님이 {1} 에서 이 이슈를 언급했습니다. |
286 | 286 |
issue.event.referred.title = 언급됨 |
287 |
+issue.event.sharer.added = {0} 님이 현재 이슈를 {1}님에게 공유했습니다. |
|
288 |
+issue.event.sharer.added.title = 이슈 공유 |
|
289 |
+issue.event.sharer.deleted = {0} 님이 {1}님을 공유대상에서 제외했습니다. |
|
290 |
+issue.event.sharer.deleted.title = 공유 취소 |
|
287 | 291 |
issue.event.unassigned = {0}님이 이 이슈의 담당자를 "없음"으로 설정하였습니다. |
288 | 292 |
issue.is.empty = 등록된 이슈가 없습니다. |
289 | 293 |
issue.label = 이슈 라벨 |
... | ... | @@ -404,6 +408,8 @@ |
404 | 408 |
notification.issue.assigned = {0}에게 이슈 할당됨 |
405 | 409 |
notification.issue.closed = 이슈 닫힘 |
406 | 410 |
notification.issue.reopened = 이슈 다시 열림 |
411 |
+notification.issue.sharer.added = {0} 님에게 이슈가 공유되었습니다. |
|
412 |
+notification.issue.sharer.deleted = 이슈 공유 상태가 변경되었습니다. |
|
407 | 413 |
notification.issue.unassigned = 이슈 담당자 없음 |
408 | 414 |
notification.linkToView = {0}에서 보기 |
409 | 415 |
notification.linkToViewHtml = <a href="{1}" target="{2}">View it on {0}</a> |
... | ... | @@ -446,6 +452,7 @@ |
446 | 452 |
notification.type.issue.moved = 이슈가 {0}에서 {1}로 이동되었습니다 |
447 | 453 |
notification.type.issue.referred.from.commit = 커밋에서의 이슈 언급 |
448 | 454 |
notification.type.issue.referred.from.pullrequest = 코드 주고받기에서의 이슈 언급 |
455 |
+notification.type.issue.sharer.changed = 이슈 공유 변경 |
|
449 | 456 |
notification.type.issue.state.changed = 이슈 상태 변경 |
450 | 457 |
notification.type.member.enroll = 멤버 등록 요청 |
451 | 458 |
notification.type.new.comment = 새 댓글 등록 |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?