Merge branch 'discourse:main' into patch-1

This commit is contained in:
Firepup650 2024-02-15 16:18:47 -06:00 committed by GitHub
commit eeea2875ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
130 changed files with 3153 additions and 2532 deletions

View File

@ -1,4 +1,5 @@
3.1.999: 0cbf10b8055370445bd36536e51986bf48bdc57e
< 3.2.0.beta2-dev: ac930c509e2a5b0c37b84bcea28d332e686add95
3.1.999: a304cd2028ccf1f5b00f5137633aa7027a1fd334
3.1.0.beta3: 9c270cac9abc1c2b30574d8c655fb3a90546236b
2.9.0.beta8: 28bc8ab78a09551548c87f511ade3d64e1b04bc3
2.9.0.beta3: 46f200935dc9e5750c3f2740abd993e27a9b3f6c

View File

@ -1,3 +0,0 @@
{
"extends": "eslint-config-discourse"
}

1
.eslintrc.cjs Normal file
View File

@ -0,0 +1 @@
module.exports = require("@discourse/lint-configs/eslint");

7
.gitignore vendored
View File

@ -1,4 +1,3 @@
.bundle/
auto_generated
.DS_Store
node_modules/
node_modules
/gems
/auto_generated

View File

@ -1 +0,0 @@
{}

1
.prettierrc.cjs Normal file
View File

@ -0,0 +1 @@
module.exports = require("@discourse/lint-configs/prettier");

1
.template-lintrc.cjs Normal file
View File

@ -0,0 +1 @@
module.exports = require("@discourse/lint-configs/template-lint");

View File

@ -1,4 +0,0 @@
module.exports = {
plugins: ["ember-template-lint-plugin-discourse"],
extends: "discourse:recommended",
};

View File

@ -2,43 +2,45 @@ GEM
remote: https://rubygems.org/
specs:
ast (2.4.2)
json (2.6.3)
parallel (1.23.0)
parser (3.2.2.3)
json (2.7.1)
language_server-protocol (3.17.0.3)
parallel (1.24.0)
parser (3.3.0.3)
ast (~> 2.4.1)
racc
prettier_print (1.2.1)
racc (1.7.1)
racc (1.7.3)
rainbow (3.1.1)
regexp_parser (2.8.1)
rexml (3.2.5)
rubocop (1.52.1)
regexp_parser (2.9.0)
rexml (3.2.6)
rubocop (1.59.0)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.2.2.3)
parser (>= 3.2.2.4)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.28.0, < 2.0)
rubocop-ast (>= 1.30.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.29.0)
rubocop-ast (1.30.0)
parser (>= 3.2.1.0)
rubocop-capybara (2.18.0)
rubocop-capybara (2.20.0)
rubocop (~> 1.41)
rubocop-discourse (3.2.0)
rubocop (>= 1.1.0)
rubocop-rspec (>= 2.0.0)
rubocop-factory_bot (2.23.1)
rubocop (~> 1.33)
rubocop-rspec (2.22.0)
rubocop (~> 1.33)
rubocop-discourse (3.6.0)
rubocop (>= 1.59.0)
rubocop-rspec (>= 2.25.0)
rubocop-factory_bot (2.25.1)
rubocop (~> 1.41)
rubocop-rspec (2.26.1)
rubocop (~> 1.40)
rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22)
ruby-progressbar (1.13.0)
syntax_tree (6.1.1)
syntax_tree (6.2.0)
prettier_print (>= 1.2.0)
unicode-display_width (2.4.2)
unicode-display_width (2.5.0)
PLATFORMS
ruby
@ -48,4 +50,4 @@ DEPENDENCIES
syntax_tree
BUNDLED WITH
2.3.4
2.5.4

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Civilized Discourse Construction Kit
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -159,43 +159,6 @@ module DiscourseAssign
}
end
def user_menu_assigns
assign_notifications =
Notification.unread_type(current_user, Notification.types[:assigned], user_menu_limit)
if assign_notifications.size < user_menu_limit
opts = {}
ignored_assignment_ids =
assign_notifications.filter_map { |notification| notification.data_hash[:assignment_id] }
opts[:ignored_assignment_ids] = ignored_assignment_ids if ignored_assignment_ids.present?
assigns_list =
TopicQuery.new(
current_user,
per_page: user_menu_limit - assign_notifications.size,
).list_messages_assigned(current_user, ignored_assignment_ids)
end
if assign_notifications.present?
serialized_notifications =
ActiveModel::ArraySerializer.new(
assign_notifications,
each_serializer: NotificationSerializer,
scope: guardian,
)
end
if assigns_list
serialized_assigns =
serialize_data(assigns_list, TopicListSerializer, scope: guardian, root: false)[:topics]
end
render json: {
notifications: serialized_notifications || [],
topics: serialized_assigns || [],
}
end
private
def translate_failure(reason, assign_to)
@ -261,10 +224,6 @@ module DiscourseAssign
raise Discourse::InvalidAccess.new unless current_user.can_assign?
end
def user_menu_limit
UsersController::USER_MENU_LIST_LIMIT
end
def recent_assignees
User
.where("users.id <> ?", current_user.id)

View File

@ -3,79 +3,8 @@
module Jobs
class AssignNotification < ::Jobs::Base
def execute(args)
raise Discourse::InvalidParameters.new(:topic_id) if args[:topic_id].nil?
raise Discourse::InvalidParameters.new(:post_id) if args[:post_id].nil?
raise Discourse::InvalidParameters.new(:assigned_to_id) if args[:assigned_to_id].nil?
raise Discourse::InvalidParameters.new(:assigned_to_type) if args[:assigned_to_type].nil?
raise Discourse::InvalidParameters.new(:assigned_by_id) if args[:assigned_by_id].nil?
raise Discourse::InvalidParameters.new(:assignment_id) if args[:assignment_id].nil?
if args[:skip_small_action_post].nil?
raise Discourse::InvalidParameters.new(:skip_small_action_post)
end
topic = Topic.find(args[:topic_id])
post = Post.find(args[:post_id])
assigned_by = User.find(args[:assigned_by_id])
assigned_to =
(
if args[:assigned_to_type] == "User"
User.find(args[:assigned_to_id])
else
Group.find(args[:assigned_to_id])
end
)
assigned_to_users = args[:assigned_to_type] == "User" ? [assigned_to] : assigned_to.users
assigned_to_users.each do |user|
Assigner.publish_topic_tracking_state(topic, user.id)
next if assigned_by == user
assigned_to_user = args[:assigned_to_type] == "User"
PostAlerter.new(post).create_notification_alert(
user: user,
post: post,
username: assigned_by.username,
notification_type: Notification.types[:assigned] || Notification.types[:custom],
excerpt:
I18n.t(
(
if assigned_to_user
"discourse_assign.topic_assigned_excerpt"
else
"discourse_assign.topic_group_assigned_excerpt"
end
),
title: topic.title,
group: assigned_to.name,
locale: user.effective_locale,
),
)
next if args[:skip_small_action_post]
Notification.create!(
notification_type: Notification.types[:assigned] || Notification.types[:custom],
user_id: user.id,
topic_id: topic.id,
post_number: post.post_number,
high_priority: true,
data: {
message:
(
if assigned_to_user
"discourse_assign.assign_notification"
else
"discourse_assign.assign_group_notification"
end
),
display_username: assigned_to_user ? assigned_by.username : assigned_to.name,
topic_title: topic.title,
assignment_id: args[:assignment_id],
}.to_json,
)
end
Assignment.find(args[:assignment_id]).create_missing_notifications!
end
end
end

View File

@ -3,34 +3,18 @@
module Jobs
class UnassignNotification < ::Jobs::Base
def execute(args)
raise Discourse::InvalidParameters.new(:topic_id) if args[:topic_id].nil?
raise Discourse::InvalidParameters.new(:assigned_to_id) if args[:assigned_to_id].nil?
raise Discourse::InvalidParameters.new(:assigned_to_type) if args[:assigned_to_type].nil?
topic = Topic.find(args[:topic_id])
assigned_to_users =
(
if args[:assigned_to_type] == "User"
[User.find(args[:assigned_to_id])]
else
Group.find(args[:assigned_to_id]).users
end
)
assigned_to_users.each do |user|
Assigner.publish_topic_tracking_state(topic, user.id)
Notification
.where(
notification_type: Notification.types[:assigned] || Notification.types[:custom],
user_id: user.id,
topic_id: topic.id,
)
.where(
"data like '%discourse_assign.assign_notification%' OR data like '%discourse_assign.assign_group_notification%'",
)
.destroy_all
%i[topic_id assigned_to_id assigned_to_type assignment_id].each do |argument|
raise Discourse::InvalidParameters.new(argument) if args[argument].nil?
end
assignment = Assignment.new(args.slice(:topic_id, :assigned_to_id, :assigned_to_type))
assignment.assigned_users.each do |user|
Assigner.publish_topic_tracking_state(assignment.topic, user.id)
end
Notification
.for_assignment(args[:assignment_id])
.where(user: assignment.assigned_users, topic: assignment.topic)
.destroy_all
end
end
end

View File

@ -9,13 +9,15 @@ class Assignment < ActiveRecord::Base
belongs_to :target, polymorphic: true
scope :joins_with_topics,
-> {
-> do
joins(
"INNER JOIN topics ON topics.id = assignments.target_id AND assignments.target_type = 'Topic' AND topics.deleted_at IS NULL",
)
}
end
scope :active_for_group, ->(group) { where(assigned_to: group, active: true) }
scope :active_for_group, ->(group) { active.where(assigned_to: group) }
scope :active, -> { where(active: true) }
scope :inactive, -> { where(active: false) }
before_validation :default_status
@ -38,11 +40,31 @@ class Assignment < ActiveRecord::Base
end
def assigned_to_user?
assigned_to_type == "User"
assigned_to.is_a?(User)
end
def assigned_to_group?
assigned_to_type == "Group"
assigned_to.is_a?(Group)
end
def assigned_users
Array.wrap(assigned_to.try(:users) || assigned_to)
end
def post
return target.posts.find_by(post_number: 1) if target.is_a?(Topic)
target
end
def create_missing_notifications!
assigned_users.each do |user|
next if user.notifications.for_assignment(self).exists?
DiscourseAssign::CreateNotification.call(
assignment: self,
user: user,
mark_as_read: assigned_by_user == user,
)
end
end
private

View File

@ -2,7 +2,7 @@ export default {
resource: "user.userPrivateMessages",
map() {
this.route("assigned", { path: "/assigned" }, function () {
this.route("assigned", function () {
this.route("index", { path: "/" });
});
},

View File

@ -1,6 +1,6 @@
import { action } from "@ember/object";
import I18n from "I18n";
import DropdownSelectBoxComponent from "select-kit/components/dropdown-select-box";
import { action } from "@ember/object";
export default DropdownSelectBoxComponent.extend({
classNames: ["assign-actions-dropdown"],

View File

@ -36,7 +36,7 @@
@id="assign-status"
@content={{this.availableStatuses}}
@value={{this.status}}
@onChange={{action (mut @model.status)}}
@onChange={{fn (mut @model.status)}}
/>
</div>
{{/if}}
@ -51,7 +51,6 @@
<Textarea
id="assign-modal-note"
@value={{@model.note}}
{{! template-lint-disable no-down-event-binding }}
{{on "keydown" this.handleTextAreaKeydown}}
/>
</div>

View File

@ -9,8 +9,8 @@ export default class AssignUserForm extends Component {
@service capabilities;
@tracked assigneeError = false;
@tracked assigneeName =
this.args.model.username || this.args.model.group_name;
@tracked
assigneeName = this.args.model.username || this.args.model.group_name;
constructor() {
super(...arguments);
@ -26,9 +26,7 @@ export default class AssignUserForm extends Component {
get status() {
return (
this.args.model.status ||
this.args.model.target.assignment_status ||
this.siteSettings.assign_statuses.split("|")[0]
this.args.model.status || this.siteSettings.assign_statuses.split("|")[0]
);
}

View File

@ -0,0 +1,43 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import AssignActionsDropdown from "./assign-actions-dropdown";
export default class AssignedTopicListColumn extends Component {
@service taskActions;
@service router;
@action
async unassign(targetId, targetType = "Topic") {
await this.taskActions.unassign(targetId, targetType);
this.router.refresh();
}
@action
reassign(topic) {
this.taskActions.showAssignModal(topic, {
onSuccess: () => this.router.refresh(),
});
}
<template>
{{#if @topic.assigned_to_user}}
<AssignActionsDropdown
@topic={{@topic}}
@assignee={{@topic.assigned_to_user.username}}
@unassign={{this.unassign}}
@reassign={{this.reassign}}
/>
{{else if @topic.assigned_to_group}}
<AssignActionsDropdown
@topic={{@topic}}
@assignee={{@topic.assigned_to_group.name}}
@group={{true}}
@unassign={{this.unassign}}
@reassign={{this.reassign}}
/>
{{else}}
<AssignActionsDropdown @topic={{@topic}} @unassign={{this.unassign}} />
{{/if}}
</template>
}

View File

@ -1,86 +0,0 @@
{{!
The `~` syntax strip spaces between the elements, making it produce
`<a class=topic-post-badges>Some text</a><span class=topic-post-badges>`,
with no space between them.
This causes the topic-post-badge to be considered the same word as "text"
at the end of the link, preventing it from line wrapping onto its own line.
}}
{{#if this.bulkSelectEnabled}}
<td class="bulk-select topic-list-data">
<input type="checkbox" class="bulk-select" />
</td>
{{/if}}
<td class="main-link clearfix topic-list-data" colspan="1">
<span class="link-top-line">
{{~raw "topic-status" topic=this.topic}}
{{~#if this.isPrivateMessage}}
{{~d-icon "envelope" class="private-message-icon"}}
{{~/if}}
{{~topic-link this.topic class="raw-link raw-topic-link"}}
{{~#if this.topic.featured_link}}
{{~topic-featured-link this.topic}}
{{~/if}}
{{~#if this.showTopicPostBadges}}
{{~raw
"topic-post-badges"
unread=this.topic.unread
unseen=this.topic.unseen
url=this.topic.lastUnreadUrl
newDotText=this.newDotText
}}
{{~/if}}
</span>
<div class="link-bottom-line">
{{#if (or (not this.hideCategory) (not this.topic.isPinnedUncategorized))}}
{{category-link this.topic.category}}
{{/if}}
{{discourse-tags this.topic mode="list" tagsForUser=this.tagsForUser}}
{{raw
"list/action-list"
topic=this.topic
postNumbers=this.topic.liked_post_numbers
className="likes"
icon="heart"
}}
</div>
{{#if this.expandPinned}}
{{raw "list/topic-excerpt" topic=this.topic}}
{{/if}}
</td>
{{#if this.showPosters}}
{{raw "list/posters-column" posters=this.topic.featuredUsers}}
{{/if}}
{{raw "list/posts-count-column" topic=this.topic}}
<td class="num views {{this.topic.viewsHeat}} topic-list-data">{{number
this.topic.views
numberKey="views_long"
}}</td>
{{raw
"list/activity-column"
topic=this.topic
class="num topic-list-data"
tagName="td"
}}
<td class="topic-list-data">
{{#if this.topic.assigned_to_user}}
<AssignActionsDropdown
@topic={{this.topic}}
@assignee={{this.topic.assigned_to_user.username}}
@unassign={{this.unassign}}
@reassign={{this.reassign}}
/>
{{else if this.topic.assigned_to_group}}
<AssignActionsDropdown
@topic={{this.topic}}
@assignee={{this.topic.assigned_to_group.name}}
@group={{true}}
@unassign={{this.unassign}}
@reassign={{this.reassign}}
/>
{{else}}
<AssignActionsDropdown @topic={{this.topic}} @unassign={{this.unassign}} />
{{/if}}
</td>

View File

@ -1,8 +0,0 @@
import TopicListItem from "discourse/components/topic-list-item";
import { equal } from "@ember/object/computed";
export default class AssignedTopicListItem extends TopicListItem {
classNames = ["assigned-topic-list-item"];
@equal("topic.archetype", "private_message") isPrivateMessage;
}

View File

@ -1,46 +0,0 @@
{{#unless this.skipHeader}}
<thead class="topic-list-header assigned-topic-list-header">
{{raw
"topic-list-header"
canBulkSelect=this.canBulkSelect
canDoBulkActions=this.canDoBulkActions
toggleInTitle=this.toggleInTitle
hideCategory=this.hideCategory
showPosters=true
showLikes=this.showLikes
showOpLikes=this.showOpLikes
order=this.order
ascending=this.ascending
sortable=this.sortable
listTitle=this.listTitle
bulkSelectEnabled=this.bulkSelectEnabled
}}
</thead>
{{/unless}}
<tbody class="topic-list-body assigned-topic-list-body">
{{#each this.filteredTopics as |topic|}}
<AssignedTopicListItem
@topic={{topic}}
@bulkSelectEnabled={{this.bulkSelectEnabled}}
@showTopicPostBadges={{this.showTopicPostBadges}}
@hideCategory={{this.hideCategory}}
@showPosters={{true}}
@showLikes={{this.showLikes}}
@showOpLikes={{this.showOpLikes}}
@expandGloballyPinned={{this.expandGloballyPinned}}
@expandAllPinned={{this.expandAllPinned}}
@lastVisitedTopic={{this.lastVisitedTopic}}
@selected={{this.selected}}
@tagsForUser={{this.tagsForUser}}
@unassign={{this.unassign}}
@reassign={{this.reassign}}
/>
{{raw
"list/visited-line"
lastVisitedTopic=this.lastVisitedTopic
topic=topic
}}
{{/each}}
</tbody>

View File

@ -1,3 +0,0 @@
import TopicList from "discourse/components/topic-list";
export default class AssignedTopicList extends TopicList {}

View File

@ -1,40 +0,0 @@
<ConditionalLoadingSpinner @condition={{this.loading}}>
{{#if this.hasIncoming}}
<div class="show-mores">
<a href class="alert alert-info clickable" {{action this.showInserted}}>
<CountI18n
@key="topic_count_"
@suffix="latest"
@count={{this.incomingCount}}
/>
</a>
</div>
{{/if}}
{{#if this.topics}}
<AssignedTopicList
@showPosters={{this.showPosters}}
@hideCategory={{this.hideCategory}}
@topics={{this.topics}}
@expandExcerpts={{this.expandExcerpts}}
@bulkSelectEnabled={{this.bulkSelectEnabled}}
@canBulkSelect={{this.canBulkSelect}}
@bulkSelectAction={{this.bulkSelectAction}}
@selected={{this.selected}}
@skipHeader={{this.skipHeader}}
@tagsForUser={{this.tagsForUser}}
@changeSort={{this.changeSort}}
@toggleBulkSelect={{this.toggleBulkSelect}}
@unassign={{this.unassign}}
@reassign={{this.reassign}}
@onScroll={{this.onScroll}}
@scrollOnLoad={{this.scrollOnLoad}}
/>
{{else}}
{{#unless this.loadingMore}}
<div class="alert alert-info">
{{i18n "choose_topic.none_found"}}
</div>
{{/unless}}
{{/if}}
</ConditionalLoadingSpinner>

View File

@ -1,3 +0,0 @@
import BasicTopicList from "discourse/components/basic-topic-list";
export default class BasicAssignedTopicList extends BasicTopicList {}

View File

@ -1,8 +1,9 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { TrackedObject } from "@ember-compat/tracked-built-ins";
export default class AssignUser extends Component {
model = {};
model = new TrackedObject({});
// `submit` property will be mutated by the `AssignUserForm` component
formApi = {
@ -14,6 +15,7 @@ export default class AssignUser extends Component {
return this.args.performAndRefresh({
type: "assign",
username: this.model.username,
status: this.model.status,
note: this.model.note,
});
}

View File

@ -1,7 +1,7 @@
<DModal class="assign" @title={{this.title}} @closeModal={{@closeModal}}>
<:body>
<AssignUserForm
@model={{@model}}
@model={{this.model}}
@onSubmit={{this.onSubmit}}
@formApi={{this.formApi}}
/>
@ -12,7 +12,7 @@
class="btn-primary"
@action={{this.formApi.submit}}
@label={{if
@model.reassign
this.model.reassign
"discourse_assign.reassign.title"
"discourse_assign.assign_modal.assign"
}}

View File

@ -1,11 +1,14 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { TrackedObject } from "@ember-compat/tracked-built-ins";
import I18n from "I18n";
export default class AssignUser extends Component {
@service taskActions;
model = new TrackedObject(this.args.model);
// `submit` property will be mutated by the `AssignUserForm` component
formApi = {
submit() {},
@ -14,7 +17,7 @@ export default class AssignUser extends Component {
get title() {
let i18nSuffix;
switch (this.args.model.targetType) {
switch (this.model.targetType) {
case "Post":
i18nSuffix = "_post_modal";
break;
@ -25,7 +28,7 @@ export default class AssignUser extends Component {
return I18n.t(
`discourse_assign.assign${i18nSuffix}.${
this.args.model.reassign ? "reassign_title" : "title"
this.model.reassign ? "reassign_title" : "title"
}`
);
}
@ -33,6 +36,6 @@ export default class AssignUser extends Component {
@action
async onSubmit() {
this.args.closeModal();
await this.taskActions.assign(this.args.model);
await this.taskActions.assign(this.model);
}
}

View File

@ -1,6 +1,6 @@
import Component from "@ember/component";
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import I18n from "I18n";
export default class RemindAssignsFrequency extends Component {
@discourseComputed(

View File

@ -1,10 +1,7 @@
import { set } from "@ember/object";
import { sort } from "@ember/object/computed";
import UserMenuNotificationsList from "discourse/components/user-menu/notifications-list";
import { ajax } from "discourse/lib/ajax";
import UserMenuNotificationItem from "discourse/lib/user-menu/notification-item";
import UserMenuAssignItem from "../../lib/user-menu/assign-item";
import Notification from "discourse/models/notification";
import I18n from "I18n";
import Topic from "discourse/models/topic";
import UserMenuAssignsListEmptyState from "./assigns-list-empty-state";
export default class UserMenuAssignNotificationsList extends UserMenuNotificationsList {
@ -56,25 +53,20 @@ export default class UserMenuAssignNotificationsList extends UserMenuNotificatio
}
async fetchItems() {
const data = await ajax("/assign/user-menu-assigns.json");
const content = [];
const notifications = data.notifications.map((n) => Notification.create(n));
await Notification.applyTransformations(notifications);
notifications.forEach((notification) => {
content.push(
new UserMenuNotificationItem({
notification,
currentUser: this.currentUser,
siteSettings: this.siteSettings,
site: this.site,
})
);
});
const topics = data.topics.map((t) => Topic.create(t));
await Topic.applyTransformations(topics);
content.push(...topics.map((assign) => new UserMenuAssignItem({ assign })));
return content;
return new SortedItems(await super.fetchItems()).sortedItems;
}
}
class SortedItems {
itemsSorting = [
"notification.read",
"notification.data.message:desc",
"notification.created_at:desc",
];
@sort("items", "itemsSorting") sortedItems;
constructor(items) {
set(this, "items", items);
}
}

View File

@ -3,7 +3,7 @@
{{i18n "discourse_assign.admin.groups.manage.interaction.assign"}}
</label>
<label for="visiblity">
<label for="visibility">
{{i18n
"discourse_assign.admin.groups.manage.interaction.assignable_levels.title"
}}
@ -14,7 +14,7 @@
@valueProperty="value"
@value={{this.assignableLevel}}
@content={{this.assignableLevelOptions}}
@class="groups-form-assignable-level"
@onChange={{action (mut @outletArgs.model.assignable_level)}}
class="groups-form-assignable-level"
/>
</div>

View File

@ -0,0 +1 @@
{{raw "assign-topic-buttons" topic=context.topic}}

View File

@ -1,13 +1,14 @@
import UserTopicsList from "discourse/controllers/user-topics-list";
import { alias } from "@ember/object/computed";
import discourseDebounce from "discourse-common/lib/debounce";
import { INPUT_DELAY } from "discourse-common/config/environment";
import { inject as controller } from "@ember/controller";
import { inject as service } from "@ember/service";
import { action } from "@ember/object";
import { alias } from "@ember/object/computed";
import { inject as service } from "@ember/service";
import UserTopicsList from "discourse/controllers/user-topics-list";
import { INPUT_DELAY } from "discourse-common/config/environment";
import discourseDebounce from "discourse-common/lib/debounce";
export default class GroupAssignedShow extends UserTopicsList {
@service taskActions;
@service router;
@controller user;
queryParams = ["order", "ascending", "search"];
@ -45,13 +46,13 @@ export default class GroupAssignedShow extends UserTopicsList {
@action
async unassign(targetId, targetType = "Topic") {
await this.taskActions.unassign(targetId, targetType);
this.send("changeAssigned");
this.router.refresh();
}
@action
reassign(topic) {
this.taskActions.showAssignModal(topic, {
onSuccess: () => this.send("changeAssigned"),
onSuccess: () => this.router.refresh(),
});
}

View File

@ -1,10 +1,10 @@
import { inject as service } from "@ember/service";
import Controller, { inject as controller } from "@ember/controller";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { ajax } from "discourse/lib/ajax";
import discourseComputed from "discourse-common/utils/decorators";
import discourseDebounce from "discourse-common/lib/debounce";
import { INPUT_DELAY } from "discourse-common/config/environment";
import discourseDebounce from "discourse-common/lib/debounce";
import discourseComputed from "discourse-common/utils/decorators";
export default class GroupAssigned extends Controller {
@service router;
@ -47,7 +47,11 @@ export default class GroupAssigned extends Controller {
})
.then((result) => {
if (this.router.currentRoute.params.filter !== "everyone") {
this.transitionToRoute("group.assigned.show", groupName, "everyone");
this.router.transitionTo(
"group.assigned.show",
groupName,
"everyone"
);
}
this.set("members", result.members);
})

View File

@ -1,14 +1,14 @@
import UserTopicsList from "discourse/controllers/user-topics-list";
import discourseComputed from "discourse-common/utils/decorators";
import discourseDebounce from "discourse-common/lib/debounce";
import { INPUT_DELAY } from "discourse-common/config/environment";
import { inject as controller } from "@ember/controller";
import { inject as service } from "@ember/service";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import UserTopicsList from "discourse/controllers/user-topics-list";
import { INPUT_DELAY } from "discourse-common/config/environment";
import discourseDebounce from "discourse-common/lib/debounce";
import getURL from "discourse-common/lib/get-url";
import { iconHTML } from "discourse-common/lib/icon-library";
import discourseComputed from "discourse-common/utils/decorators";
import I18n from "I18n";
import { htmlSafe } from "@ember/template";
export default class UserActivityAssigned extends UserTopicsList {
@service taskActions;
@ -56,19 +56,6 @@ export default class UserActivityAssigned extends UserTopicsList {
});
}
@action
async unassign(targetId, targetType = "Topic") {
await this.taskActions.unassign(targetId, targetType);
this.send("changeAssigned");
}
@action
reassign(topic) {
this.taskActions.showAssignModal(topic, {
onSuccess: () => this.send("changeAssigned"),
});
}
@action
changeSort(sortBy) {
if (sortBy === this.order) {

View File

@ -1,5 +1,5 @@
import I18n from "I18n";
import { withPluginApi } from "discourse/lib/plugin-api";
import I18n from "I18n";
export default {
name: "assign-extend-user-messages",

View File

@ -1,4 +1,7 @@
import { htmlSafe } from "@ember/template";
import { withPluginApi } from "discourse/lib/plugin-api";
import { emojiUnescape } from "discourse/lib/text";
import I18n from "I18n";
import UserMenuAssignNotificationsList from "../components/user-menu/assigns-list";
export default {
@ -6,16 +9,74 @@ export default {
initialize(container) {
withPluginApi("1.2.0", (api) => {
if (api.registerUserMenuTab) {
const siteSettings = container.lookup("service:site-settings");
if (!siteSettings.assign_enabled) {
return;
}
const siteSettings = container.lookup("service:site-settings");
if (!siteSettings.assign_enabled) {
return;
}
const currentUser = api.getCurrentUser();
if (!currentUser?.can_assign) {
return;
}
const currentUser = api.getCurrentUser();
if (!currentUser?.can_assign) {
return;
}
if (api.registerNotificationTypeRenderer) {
api.registerNotificationTypeRenderer(
"assigned",
(NotificationItemBase) => {
return class extends NotificationItemBase {
get linkTitle() {
if (this.isGroup()) {
return I18n.t(
`user.assigned_to_group.${this.postOrTopic()}`,
{
group_name: this.notification.data.display_username,
}
);
}
return I18n.t(`user.assigned_to_you.${this.postOrTopic()}`);
}
get icon() {
return this.isGroup() ? "group-plus" : "user-plus";
}
get label() {
if (!this.isGroup()) {
return "";
}
return this.notification.data.display_username;
}
get description() {
return htmlSafe(
emojiUnescape(
I18n.t(
`user.assignment_description.${this.postOrTopic()}`,
{
topic_title: this.notification.fancy_title,
post_number: this.notification.post_number,
}
)
)
);
}
isGroup() {
return (
this.notification.data.message ===
"discourse_assign.assign_group_notification"
);
}
postOrTopic() {
return this.notification.post_number === 1 ? "topic" : "post";
}
};
}
);
}
if (api.registerUserMenuTab) {
api.registerUserMenuTab((UserMenuTab) => {
return class extends UserMenuTab {
id = "assign-list";

View File

@ -1,17 +1,17 @@
import { getOwner } from "@ember/application";
import { htmlSafe } from "@ember/template";
import { isEmpty } from "@ember/utils";
import { h } from "virtual-dom";
import SearchAdvancedOptions from "discourse/components/search-advanced-options";
import { renderAvatar } from "discourse/helpers/user-avatar";
import { withPluginApi } from "discourse/lib/plugin-api";
import discourseComputed from "discourse-common/utils/decorators";
import { iconHTML, iconNode } from "discourse-common/lib/icon-library";
import { escapeExpression } from "discourse/lib/utilities";
import { h } from "virtual-dom";
import { getOwner } from "discourse-common/lib/get-owner";
import { htmlSafe } from "@ember/template";
import getURL from "discourse-common/lib/get-url";
import SearchAdvancedOptions from "discourse/components/search-advanced-options";
import I18n from "I18n";
import { isEmpty } from "@ember/utils";
import { registerTopicFooterDropdown } from "discourse/lib/register-topic-footer-dropdown";
import { escapeExpression } from "discourse/lib/utilities";
import RawHtml from "discourse/widgets/raw-html";
import getURL from "discourse-common/lib/get-url";
import { iconHTML, iconNode } from "discourse-common/lib/icon-library";
import discourseComputed from "discourse-common/utils/decorators";
import I18n from "I18n";
import BulkAssign from "../components/bulk-actions/assign-user";
const PLUGIN_ID = "discourse-assign";
@ -409,7 +409,7 @@ function registerTopicFooterButtons(api) {
}
function initialize(api) {
const siteSettings = api.container.lookup("site-settings:main");
const siteSettings = api.container.lookup("service:site-settings");
const currentUser = api.getCurrentUser();
if (siteSettings.assigns_public || currentUser?.can_assign) {
@ -616,7 +616,6 @@ function initialize(api) {
topicAssignee,
assignedToIndirectly.map((assigned) => ({
assignee: assigned.assigned_to,
status: assigned.assignment_status,
note: assigned.assignment_note,
}))
)
@ -879,12 +878,12 @@ export default {
name: "extend-for-assign",
initialize(container) {
const siteSettings = container.lookup("site-settings:main");
const siteSettings = container.lookup("service:site-settings");
if (!siteSettings.assign_enabled) {
return;
}
const currentUser = container.lookup("current-user:main");
const currentUser = container.lookup("service:current-user");
if (currentUser?.can_assign) {
SearchAdvancedOptions.reopen({
updateSearchTermForAssignedUsername() {
@ -928,57 +927,23 @@ export default {
api.addUserSearchOption("assignableGroups");
if (api.addBulkActionButton) {
api.addBulkActionButton({
label: "topics.bulk.assign",
icon: "user-plus",
class: "btn-default assign-topics",
action({ setComponent }) {
setComponent(BulkAssign);
},
});
api.addBulkActionButton({
label: "topics.bulk.assign",
icon: "user-plus",
class: "btn-default assign-topics",
action({ setComponent }) {
setComponent(BulkAssign);
},
});
api.addBulkActionButton({
label: "topics.bulk.unassign",
icon: "user-times",
class: "btn-default unassign-topics",
action({ performAndRefresh }) {
performAndRefresh({ type: "unassign" });
},
});
} else {
// TODO: Remove this path after core 3.1.0.beta7 is released
const {
default: TopicButtonAction,
addBulkButton,
} = require("discourse/controllers/topic-bulk-actions");
TopicButtonAction.reopen({
actions: {
showReAssign() {
const controller = getOwner(this).lookup(
"controller:bulk-assign"
);
controller.set("model", { username: "", note: "" });
this.send("changeBulkTemplate", "modal/bulk-assign");
},
unassignTopics() {
this.performAndRefresh({ type: "unassign" });
},
},
});
addBulkButton("showReAssign", "assign", {
icon: "user-plus",
class: "btn-default assign-topics",
});
addBulkButton("unassignTopics", "unassign", {
icon: "user-times",
class: "btn-default unassign-topics",
});
}
api.addBulkActionButton({
label: "topics.bulk.unassign",
icon: "user-times",
class: "btn-default unassign-topics",
action({ performAndRefresh }) {
performAndRefresh({ type: "unassign" });
},
});
});
},
};

View File

@ -1,59 +0,0 @@
import UserMenuBaseItem from "discourse/lib/user-menu/base-item";
import { postUrl } from "discourse/lib/utilities";
import { htmlSafe } from "@ember/template";
import { emojiUnescape } from "discourse/lib/text";
import I18n from "I18n";
const ICON = "user-plus";
const GROUP_ICON = "group-plus";
export default class UserMenuAssignItem extends UserMenuBaseItem {
constructor({ assign }) {
super(...arguments);
this.assign = assign;
}
get className() {
return "assign";
}
get linkHref() {
return postUrl(
this.assign.slug,
this.assign.id,
(this.assign.last_read_post_number || 0) + 1
);
}
get linkTitle() {
if (this.assign.assigned_to_group) {
return I18n.t("user.assigned_to_group", {
group_name:
this.assign.assigned_to_group.full_name ||
this.assign.assigned_to_group.name,
});
} else {
return I18n.t("user.assigned_to_you");
}
}
get icon() {
if (this.assign.assigned_to_group) {
return GROUP_ICON;
} else {
return ICON;
}
}
get label() {
return null;
}
get description() {
return htmlSafe(emojiUnescape(this.assign.fancy_title));
}
get topicId() {
return this.assign.id;
}
}

View File

@ -1,5 +1,5 @@
import Category from "discourse/models/category";
import { computed } from "@ember/object";
import Category from "discourse/models/category";
export default {
name: "extend-category-for-assign",

View File

@ -0,0 +1 @@
{{{view.html}}}

View File

@ -0,0 +1,21 @@
import EmberObject from "@ember/object";
import { inject as service } from "@ember/service";
import rawRenderGlimmer from "discourse/lib/raw-render-glimmer";
import AssignedTopicListColumn from "../components/assigned-topic-list-column";
const ASSIGN_LIST_ROUTES = ["userActivity.assigned", "group.assigned.show"];
export default class extends EmberObject {
@service router;
get html() {
if (ASSIGN_LIST_ROUTES.includes(this.router.currentRouteName)) {
return rawRenderGlimmer(
this,
"td.assign-topic-buttons",
<template><AssignedTopicListColumn @topic={{@data.topic}} /></template>,
{ topic: this.topic }
);
}
}
}

View File

@ -1,13 +1,7 @@
import DiscourseRoute from "discourse/routes/discourse";
import { findOrResetCachedTopicList } from "discourse/lib/cached-topic-list";
import DiscourseRoute from "discourse/routes/discourse";
export default class GroupAssignedShow extends DiscourseRoute {
beforeModel(transition) {
if (transition.from?.localName === "show") {
this.session.set("topicListScrollPosition", 1);
}
}
model(params) {
let filter;
if (["everyone", this.modelFor("group").name].includes(params.filter)) {
@ -36,8 +30,4 @@ export default class GroupAssignedShow extends DiscourseRoute {
search: this.currentModel.params.search,
});
}
renderTemplate() {
this.render("group-topics-list");
}
}

View File

@ -1,8 +1,10 @@
import DiscourseRoute from "discourse/routes/discourse";
import { inject as service } from "@ember/service";
import { ajax } from "discourse/lib/ajax";
import { action } from "@ember/object";
import DiscourseRoute from "discourse/routes/discourse";
export default class GroupAssigned extends DiscourseRoute {
@service router;
model() {
return ajax(`/assign/members/${this.modelFor("group").name}`);
}
@ -22,15 +24,8 @@ export default class GroupAssigned extends DiscourseRoute {
}
redirect(model, transition) {
if (transition.to.params.hasOwnProperty("filter")) {
this.transitionTo("group.assigned.show", transition.to.params.filter);
} else {
this.transitionTo("group.assigned.show", "everyone");
if (!transition.to.params.hasOwnProperty("filter")) {
this.router.transitionTo("group.assigned.show", "everyone");
}
}
@action
changeAssigned() {
this.refresh();
}
}

View File

@ -1,9 +1,10 @@
import I18n from "I18n";
import UserTopicListRoute from "discourse/routes/user-topic-list";
import { inject as service } from "@ember/service";
import cookie from "discourse/lib/cookie";
import { action } from "@ember/object";
import UserTopicListRoute from "discourse/routes/user-topic-list";
import I18n from "I18n";
export default class UserActivityAssigned extends UserTopicListRoute {
@service router;
templateName = "user-activity-assigned";
controllerName = "user-activity-assigned";
@ -13,7 +14,7 @@ export default class UserActivityAssigned extends UserTopicListRoute {
beforeModel() {
if (!this.currentUser) {
cookie("destination_url", window.location.href);
this.transitionTo("login");
this.router.transitionTo("login");
}
}
@ -34,9 +35,4 @@ export default class UserActivityAssigned extends UserTopicListRoute {
titleToken() {
return I18n.t("discourse_assign.assigned");
}
@action
changeAssigned() {
this.refresh();
}
}

View File

@ -1,9 +1,9 @@
import Service, { inject as service } from "@ember/service";
import { ajax } from "discourse/lib/ajax";
import AssignUser from "../components/modal/assign-user";
import { tracked } from "@glimmer/tracking";
import Service, { inject as service } from "@ember/service";
import { isEmpty } from "@ember/utils";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import AssignUser from "../components/modal/assign-user";
export default class TaskActions extends Service {
@service modal;

View File

@ -1,22 +1,22 @@
<div class="topic-search-div">
<div class="inline-form full-width">
<Input
class="no-blur"
{{on "input" (action "onChangeFilter" value="target.value")}}
@value={{readonly this.search}}
@type="search"
placeholder={{i18n "discourse_assign.topic_search_placeholder"}}
autocomplete="off"
@type="search"
{{on "input" (action "onChangeFilter" value="target.value")}}
class="no-blur"
/>
</div>
</div>
<LoadMore
@class="paginated-topics-list"
@selector=".paginated-topics-list .topic-list tr"
@action={{action "loadMore"}}
class="paginated-topics-list"
>
<BasicAssignedTopicList
<BasicTopicList
@topicList={{this.model}}
@hideCategory={{this.hideCategory}}
@showPosters={{this.showPosters}}

View File

@ -1,18 +1,18 @@
<section class="user-secondary-navigation group-assignments">
<MobileNav
@class="activity-nav"
@desktopClass="action-list activity-list nav-stacked"
class="activity-nav"
>
{{#if this.isDesktop}}
<div class="search-div">
<Input
{{on "input" (action this.onChangeFilterName value="target.value")}}
@type="text"
@value={{readonly this.filterName}}
placeholder={{i18n
"discourse_assign.sidebar_name_filter_placeholder"
}}
@value={{readonly this.filterName}}
class="search"
{{on "input" (action this.onChangeFilterName value="target.value")}}
/>
</div>
{{/if}}

View File

@ -7,22 +7,22 @@
<div class="topic-search-div">
<div class="inline-form full-width">
<Input
{{on "input" (action "onChangeFilter" value="target.value")}}
@value={{readonly this.search}}
@type="search"
{{on "input" (action "onChangeFilter" value="target.value")}}
class="no-blur"
placeholder={{i18n "discourse_assign.topic_search_placeholder"}}
autocomplete="off"
class="no-blur"
/>
</div>
</div>
<LoadMore
@class="paginated-topics-list"
@selector=".paginated-topics-list .topic-list tr"
@action={{action "loadMore"}}
class="paginated-topics-list"
>
<BasicAssignedTopicList
<BasicTopicList
@topicList={{this.model}}
@hideCategory={{this.hideCategory}}
@showPosters={{true}}

View File

@ -66,12 +66,8 @@
margin-left: 5px;
}
.modal.assign {
.modal-inner-container {
width: 400px;
}
.modal-body {
.d-modal.assign {
.d-modal__body {
overflow-y: unset;
}

View File

@ -5,6 +5,11 @@
# https://translate.discourse.org/
ar:
admin_js:
admin:
site_settings:
categories:
discourse_assign: "تعيين Discourse"
js:
filters:
unassigned:
@ -35,9 +40,8 @@ ar:
assigned_to: "معيَّنة إلى"
assigned_topic_to: "تم تعيين الموضوع إلى المستخدم <a href='%{path}'>%{username}</a>"
assign_post_to: "تم تعيين المنشور #%{post_number} إلى المستخدم %{username}"
assign_post_to_multiple: "#%{post_number} إلى %{username}"
assigned_to_w_ellipsis: "معيَّنة إلى..."
assign_notification: "<p><span>%{username}</span> %{description}</p>"
assign_group_notification: "<p><span>%{username}</span> %{description}</p>"
unassign:
title: "إلغاء التعيين"
title_w_ellipsis: "إلغاء تعيين..."
@ -99,13 +103,21 @@ ar:
no_assignments_body: >
سيتم إدراج الموضوعات والرسائل المعيَّنة إليك هنا. ستتلقى أيضًا إشعار تذكير دوريًا بتعييناتك، والذي يمكنك تعديله في <a href='%{preferencesUrl}'>تفضيلات المستخدم</a>. <br><br> لتعيين موضوع أو رسالة إلى نفسك أو إلى شخص آخر، ابحث عن %{icon} في الأسفل.
dismiss_assigned_tooltip: "وضع علامة مقروءة على كل إشعارات التعيين"
assigned_to_group: "معيَّن إلى %{group_name}"
assigned_to_you: "معيَّنة إليك"
assigned_to_group:
post: "تم تعيين المنشور إلى %{group_name}"
topic: "تم تعيين الموضوع إلى %{group_name}"
assigned_to_you:
post: "تم تعيين المنشور إليك"
topic: "تم تعيين الموضوع إليك"
assignment_description:
post: "%{topic_title} (#%{post_number})"
topic: "%{topic_title}"
admin:
web_hooks:
assign_event:
name: "تعيين الحدث"
details: "عندما يعيِّن مستخدم موضوعًا أو يلغي تعيينه."
group_name: "أحداث التعيين"
assigned: "عندما يقوم المستخدم بتعيين موضوع"
unassigned: "عندما يلغي المستخدم تعيين موضوع"
search:
advanced:
in:

View File

@ -23,6 +23,8 @@ be:
user:
messages:
assigned: "прызначаны"
assignment_description:
topic: "%{topic_title}"
notifications:
titles:
assigned: "прызначаны"

View File

@ -22,8 +22,6 @@ ca:
group_everyone: "Tothom"
assigned_to: "Assignat a"
assigned_to_w_ellipsis: "Assignat a..."
assign_notification: "<p><span>%{username}</span> %{description}</p>"
assign_group_notification: "<p><span>%{username}</span> %{description}</p>"
unassign:
title: "Desassigna"
title_w_ellipsis: "Desassigna..."
@ -60,6 +58,8 @@ ca:
messages:
assigned_title: "Assignats (%{count})"
assigned: "Assignats"
assignment_description:
topic: "%{topic_title}"
search:
advanced:
assigned:

View File

@ -5,13 +5,40 @@
# https://translate.discourse.org/
cs:
admin_js:
admin:
site_settings:
categories:
discourse_assign: "Discourse Assign"
js:
filters:
unassigned:
title: "Nepřiděleno"
help: "Témata, která nejsou přidělena"
action_codes:
assigned: "Zadáno %{who} %{when}"
assigned_group: "Zadáno %{who} %{when}"
assigned_to_post: "Uživateli %{who} přidělen <a href='%{path}'>příspěvek</a> %{when}"
assigned_group_to_post: "Skupině %{who} přidělen <a href='%{path}'>příspěvek</a> %{when}"
unassigned: "%{who} zrušeno přidělení %{when}"
unassigned_group: "%{who} zrušeno přidělení %{when}"
unassigned_from_post: "Uživateli %{who} zrušeno přidělení <a href='%{path}'>příspěvku</a> %{when}"
unassigned_group_from_post: "Skupině %{who} zrušeno přidělení <a href='%{path}'>příspěvku</a> %{when}"
reassigned: "Uživateli %{who} znovu přiděleno %{when}"
reassigned_group: "Skupině %{who} znovu přiděleno %{when}"
details_change: "Změněny podrobnosti přidělení pro %{who} %{when}"
note_change: "Změněna poznámka přidělení pro %{who} %{when}"
status_change: "Změněn stav přidělení pro %{who} %{when}"
discourse_assign:
add_unassigned_filter: "Přidat do kategorie filtr \"Nepřiděleno\""
cant_act: "Nemůžete reagovat na nahlášení, která byla přiřazena jiným uživatelům"
cant_act_unclaimed: "Než začnete reagovat na nahlášení, musíte si toto téma nárokovat."
topic_search_placeholder: "Hledat témata podle názvu nebo obsahu příspěvku"
sidebar_name_filter_placeholder: "Jméno/Uživatelské jméno"
assigned: "Zadáno"
group_everyone: "Všichni"
assigned_to: "Přiděleno"
assigned_topic_to: "Téma přiděleno <a href='%{path}'>%{username}</a>"
unassign:
title: "Odebrat zadání"
title_w_ellipsis: "Odebrat zadání..."

View File

@ -27,8 +27,6 @@ da:
group_everyone: "Alle"
assigned_to: "Tildelt til"
assigned_to_w_ellipsis: "Tildelt til..."
assign_notification: "<p><span>%{username}</span> %{description}</p>"
assign_group_notification: "<p><span>%{username}</span> %{description}</p>"
unassign:
title: "Fjern tildeling"
title_w_ellipsis: "Fjern tildeling..."
@ -66,11 +64,8 @@ da:
messages:
assigned_title: "Tildelt (%{count})"
assigned: "Tildelt"
admin:
web_hooks:
assign_event:
name: "Tildel Begivenhed"
details: "Når en bruger tildeler eller fradeler et emne."
assignment_description:
topic: "%{topic_title}"
search:
advanced:
in:

View File

@ -5,6 +5,11 @@
# https://translate.discourse.org/
de:
admin_js:
admin:
site_settings:
categories:
discourse_assign: "Discourse Zuordnen"
js:
filters:
unassigned:
@ -35,9 +40,8 @@ de:
assigned_to: "Zugeordnet zu"
assigned_topic_to: "Thema zugeordnet zu <a href='%{path}'>%{username}</a>"
assign_post_to: "#%{post_number} %{username} zugeordnet"
assign_post_to_multiple: "#%{post_number} zu %{username}"
assigned_to_w_ellipsis: "Zugeordnet zu …"
assign_notification: "<p><span>%{username}</span> %{description}</p>"
assign_group_notification: "<p><span>%{username}</span> %{description}</p>"
unassign:
title: "Zuordnung aufheben"
title_w_ellipsis: "Zuordnung aufheben …"
@ -99,13 +103,21 @@ de:
no_assignments_body: >
Deine zugeordneten Themen und Nachrichten werden hier aufgelistet. Außerdem erhältst du regelmäßig eine Erinnerungsbenachrichtigung betreffend deine Zuordnungen, die du in deinen <a href='%{preferencesUrl}'>Benutzereinstellungen</a> anpassen kannst. <br><br> Um dir selbst oder einer anderen Person ein Thema oder eine Nachricht zuzuordnen, verwende die Zuordnungsschaltfläche %{icon} unten.
dismiss_assigned_tooltip: "Alle ungelesenen Zuordnungsbenachrichtigungen als gelesen markieren"
assigned_to_group: "%{group_name} zugeordnet"
assigned_to_you: "dir zugeordnet"
assigned_to_group:
post: "Beitrag zugeordnet zu %{group_name}"
topic: "Thema zugeordnet zu %{group_name}"
assigned_to_you:
post: "Beitrag wurde dir zugeordnet"
topic: "Thema wurde dir zugeordnet"
assignment_description:
post: "%{topic_title} (#%{post_number})"
topic: "%{topic_title}"
admin:
web_hooks:
assign_event:
name: "Ereignis zuordnen"
details: "Wenn ein Benutzer ein Thema zuordnet oder die Zuordnung aufhebt."
group_name: "Ereignisse zuordnen"
assigned: "Wenn ein Benutzer ein Thema zuordnet"
unassigned: "Wenn ein Benutzer die Zuordnung eines Themas aufhebt"
search:
advanced:
in:

View File

@ -22,5 +22,8 @@ el:
weekly: "Εβδομαδιαία"
monthly: "Μηνιαία"
quarterly: "Τριμηνιαία"
user:
assignment_description:
topic: "%{topic_title}"
notifications:
assigned: "<span>%{username}</span> %{description}"

View File

@ -1,4 +1,9 @@
en:
admin_js:
admin:
site_settings:
categories:
discourse_assign: "Discourse Assign"
js:
filters:
unassigned:
@ -32,8 +37,6 @@ en:
# assign_post_to_multiple used in list form, example: "Assigned topic to username0, [#2 to username1], [#10 to username2]"
assign_post_to_multiple: "#%{post_number} to %{username}"
assigned_to_w_ellipsis: "Assigned to..."
assign_notification: "<p><span>%{username}</span> %{description}</p>"
assign_group_notification: "<p><span>%{username}</span> %{description}</p>"
unassign:
title: "Unassign"
title_w_ellipsis: "Unassign..."
@ -97,13 +100,21 @@ en:
<br><br>
To assign a topic or message to yourself or to someone else, look for the %{icon} assign button at the bottom.
dismiss_assigned_tooltip: "Mark all unread assign notifications as read"
assigned_to_group: "assigned to %{group_name}"
assigned_to_you: "assigned to you"
assigned_to_group:
post: "post assigned to %{group_name}"
topic: "topic assigned to %{group_name}"
assigned_to_you:
post: "post assigned to you"
topic: "topic assigned to you"
assignment_description:
post: "%{topic_title} (#%{post_number})"
topic: "%{topic_title}"
admin:
web_hooks:
assign_event:
name: "Assign Event"
details: "When a user assigns or unassigns a topic."
group_name: "Assign Events"
assigned: "When an user assigns a topic"
unassigned: "When an user unassigns a topic"
search:
advanced:
in:

View File

@ -5,6 +5,11 @@
# https://translate.discourse.org/
es:
admin_js:
admin:
site_settings:
categories:
discourse_assign: "Asignación de Discourse"
js:
filters:
unassigned:
@ -35,9 +40,8 @@ es:
assigned_to: "Asignado a"
assigned_topic_to: "Tema asignado a <a href='%{path}'>%{username}</a>"
assign_post_to: "Asignado #%{post_number} a %{username}"
assign_post_to_multiple: "#%{post_number} a %{username}"
assigned_to_w_ellipsis: "Asignado a..."
assign_notification: "<p><span>%{username}</span> %{description}</p>"
assign_group_notification: "<p><span>%{username}</span> %{description}</p>"
unassign:
title: "Anular asignación"
title_w_ellipsis: "Anular asignación..."
@ -99,13 +103,21 @@ es:
no_assignments_body: >
Tus temas y mensajes asignados aparecerán aquí. También recibirás una notificación periódica de recordatorio de tus asignaciones, que puedes ajustar en tus <a href='%{preferencesUrl}'>preferencias de usuario</a>. <br><br> Para asignar un tema o mensaje a ti mismo o a otra persona, busca el botón %{icon} de asignar en la parte inferior.
dismiss_assigned_tooltip: "Marcar todas las notificaciones de asignación no leídas como leídas"
assigned_to_group: "asignada a %{group_name}"
assigned_to_you: "asignada a ti"
assigned_to_group:
post: "publicación asignada a %{group_name}"
topic: "tema asignado a %{group_name}"
assigned_to_you:
post: "publicación asignada a ti"
topic: "tema asignado a ti"
assignment_description:
post: "%{topic_title} (#%{post_number})"
topic: "%{topic_title}"
admin:
web_hooks:
assign_event:
name: "Asignar evento"
details: "Cuando un usuario asigna o anula la asignación de un tema."
group_name: "Asignar eventos"
assigned: "Cuando un usuario asigna un tema"
unassigned: "Cuando un usuario anula la asignación de un tema"
search:
advanced:
in:

View File

@ -42,6 +42,8 @@ et:
user:
messages:
assigned: "Määratud"
assignment_description:
topic: "%{topic_title}"
search:
advanced:
assigned:

View File

@ -31,5 +31,8 @@ fa_IR:
weekly: "هفتگی"
monthly: "ماهانه"
quarterly: "فصلی"
user:
assignment_description:
topic: "%{topic_title}"
notifications:
assigned: "<span>%{username}</span> %{description}"

View File

@ -5,6 +5,11 @@
# https://translate.discourse.org/
fi:
admin_js:
admin:
site_settings:
categories:
discourse_assign: "Discourse-osoitus"
js:
filters:
unassigned:
@ -35,9 +40,8 @@ fi:
assigned_to: "Osoitettu käyttäjälle"
assigned_topic_to: "Osoitti ketjun käyttäjälle <a href='%{path}'>%{username}</a>"
assign_post_to: "Osoitti viestin %{post_number} käyttäjälle %{username}"
assign_post_to_multiple: "#%{post_number} käyttäjälle %{username}"
assigned_to_w_ellipsis: "Osoitettu käyttäjälle..."
assign_notification: "<p><span>%{username}</span> %{description}</p>"
assign_group_notification: "<p><span>%{username}</span> %{description}</p>"
unassign:
title: "Peru osoitus"
title_w_ellipsis: "Peru osoitus..."
@ -99,13 +103,21 @@ fi:
no_assignments_body: >
Sinulle osoitetut ketjut ja viestit luetellaan tässä. Saat myös ajoittain muistutusilmoituksia osoituksistasi, joita voit muuttaa <a href='%{preferencesUrl}'>käyttäjäasetuksissasi</a>. <br><br> Jos haluat osoittaa ketjun tai viestin itsellesi tai jollekulle toiselle, etsi osoituspainike %{icon} alaosasta.
dismiss_assigned_tooltip: "Merkitse kaikki lukemattomat osoitusilmoitukset luetuiksi"
assigned_to_group: "osoitettu ryhmälle %{group_name}"
assigned_to_you: "osoitettu sinulle"
assigned_to_group:
post: "viesti osoitettu ryhmälle %{group_name}"
topic: "ketju osoitettu ryhmälle %{group_name}"
assigned_to_you:
post: "viesti osoitettu sinulle"
topic: "ketju osoitettu sinulle"
assignment_description:
post: "%{topic_title} (#%{post_number})"
topic: "%{topic_title}"
admin:
web_hooks:
assign_event:
name: "Osoitustapahtuma"
details: "Kun käyttäjä osoittaa ketjun tai peruu ketjun osoituksen."
group_name: "Osoitustapahtumat"
assigned: "Kun käyttäjä osoittaa ketjun"
unassigned: "Kun käyttäjä peruu ketjun osoituksen"
search:
advanced:
in:

View File

@ -5,6 +5,11 @@
# https://translate.discourse.org/
fr:
admin_js:
admin:
site_settings:
categories:
discourse_assign: "Affectation de Discourse"
js:
filters:
unassigned:
@ -35,9 +40,8 @@ fr:
assigned_to: "Attribué à"
assigned_topic_to: "A attribué le message à <a href='%{path}'>%{username}</a>"
assign_post_to: "A attribué %{post_number} à %{username}"
assign_post_to_multiple: "#%{post_number} à %{username}"
assigned_to_w_ellipsis: "Attribué à…"
assign_notification: "<p><span>%{username}</span> %{description}</p>"
assign_group_notification: "<p><span>%{username}</span> %{description}</p>"
unassign:
title: "Annuler l'attribution"
title_w_ellipsis: "Annuler l'attribution…"
@ -99,13 +103,21 @@ fr:
no_assignments_body: >
Vos sujets et messages attribués seront répertoriés ici. Vous recevrez également une notification de rappel périodique de vos attributions que vous pouvez ajuster dans vos <a href='%{preferencesUrl}'>préférences d'utilisateur</a>. <br><br> Pour attribuer un sujet ou un message à vous-même ou à quelqu'un d'autre, recherchez le bouton d'attribution %{icon} en bas de l'écran.
dismiss_assigned_tooltip: "Marquer toutes les notifications d'attribution non lues comme lues"
assigned_to_group: "attribué à %{group_name}"
assigned_to_you: "attribué à vous"
assigned_to_group:
post: "message attribué à %{group_name}"
topic: "sujet attribué à %{group_name}"
assigned_to_you:
post: "message attribué à vous"
topic: "sujet attribué à vous"
assignment_description:
post: "%{topic_title} (#%{post_number})"
topic: "%{topic_title}"
admin:
web_hooks:
assign_event:
name: "Événement d'attribution"
details: "Lorsqu'un utilisateur attribue ou annule l'attribution d'un sujet."
group_name: "Événements d'attribution"
assigned: "Lorsqu'un utilisateur attribue un sujet"
unassigned: "Lorsqu'un utilisateur annule l'attribution d'un sujet"
search:
advanced:
in:

View File

@ -26,6 +26,8 @@ gl:
user:
messages:
assigned: "Asignado"
assignment_description:
topic: "%{topic_title}"
notifications:
assigned: "<span>%{username}</span> %{description}"
titles:

View File

@ -5,6 +5,11 @@
# https://translate.discourse.org/
he:
admin_js:
admin:
site_settings:
categories:
discourse_assign: "Discourse הקצאה"
js:
filters:
unassigned:
@ -37,8 +42,6 @@ he:
assign_post_to: "מס׳ %{post_number} הוקצה לטובת %{username}"
assign_post_to_multiple: "מס׳ %{post_number} אל %{username}"
assigned_to_w_ellipsis: "מוקצה לטובת..."
assign_notification: "<p><span>%{username}</span> %{description}</p>"
assign_group_notification: "<p><span>%{username}</span> %{description}</p>"
unassign:
title: "ביטול הקצאה"
title_w_ellipsis: "ביטול הקצאה..."
@ -100,13 +103,21 @@ he:
no_assignments_body: >
הנושאים שמוקצים אליך יופיעו כאן. תישלחנה אליך הודעות תזכורת מדי פעם בפעם כדי להזכיר לך את המטלות שלך, ניתן להגדיר אותן <a href='%{preferencesUrl}'>בהעדפות המשתמש</a> שלך. <br><br> כדי להקצות נושא או הודעה לעצמך או למישהו אחר יש לחפש את הכפתור %{icon} הקצאה למטה.
dismiss_assigned_tooltip: "סימון כל התראות ההקצאה שלא נקראו כהתראות שנקראו"
assigned_to_group: "מוקצה לטובת %{group_name}"
assigned_to_you: "מוקצה לך"
assigned_to_group:
post: "הפוסט הוקצה לטובת %{group_name}"
topic: "הנושא הוקצה לטובת %{group_name}"
assigned_to_you:
post: "הפוסט הוקצה לך"
topic: "הנושא הוקצה לך"
assignment_description:
post: "%{topic_title} (מס׳ %{post_number})"
topic: "%{topic_title}"
admin:
web_hooks:
assign_event:
name: "הקצאת אירוע"
details: "כאשר משתמש מקצה או מבטל הקצאה של נושא."
group_name: "הקצאת אירועים"
assigned: "בעת הקצאת נושא על ידי משתמש"
unassigned: "בעת ביטול הקצאת נושא על ידי משתמש"
search:
advanced:
in:

View File

@ -31,8 +31,6 @@ hu:
group_everyone: "Mindenki"
assigned_to: "Kiosztva:"
assigned_to_w_ellipsis: "Kiosztva…"
assign_notification: "<p><span>%{username}</span> %{description}</p>"
assign_group_notification: "<p><span>%{username}</span> %{description}</p>"
unassign:
title: "Elvétel"
title_w_ellipsis: "Elvétel…"
@ -87,11 +85,8 @@ hu:
no_assignments_title: "Még nincsenek kiosztott feladatai"
no_assignments_body: >
A kiosztott témák és üzenetek itt jelennek meg. Rendszeresen kap egy emlékeztető értesítést is a kiosztott feladatokról, amelyeket a <a href='%{preferencesUrl}'>felhasználói beállításokban</a> módosíthat. <br><br>Téma vagy üzenet saját magának vagy valaki másnak történő kiosztásához keresse a lenti %{icon} kiosztás gombot.
admin:
web_hooks:
assign_event:
name: "Esemény kiosztása"
details: "Amikor egy felhasználó kioszt vagy elvesz egy témát."
assignment_description:
topic: "%{topic_title}"
search:
advanced:
assigned:

View File

@ -52,6 +52,8 @@ hy:
messages:
assigned_title: "Վերագրված (%{count})"
assigned: "Վերագրված"
assignment_description:
topic: "%{topic_title}"
search:
advanced:
assigned:

View File

@ -5,6 +5,11 @@
# https://translate.discourse.org/
it:
admin_js:
admin:
site_settings:
categories:
discourse_assign: "Assegnazione Discourse"
js:
filters:
unassigned:
@ -35,9 +40,8 @@ it:
assigned_to: "Assegnato a"
assigned_topic_to: "Argomento assegnato a <a href='%{path}'>%{username}</a>"
assign_post_to: "#%{post_number} assegnato a %{username}"
assign_post_to_multiple: "n°%{post_number} a %{username}"
assigned_to_w_ellipsis: "Assegnato a..."
assign_notification: "<p> <span>%{username}</span> %{description} </p>"
assign_group_notification: "<p> <span>%{username}</span> %{description} </p>"
unassign:
title: "Annulla assegnazione"
title_w_ellipsis: "Annullamento assegnazione..."
@ -99,13 +103,21 @@ it:
no_assignments_body: >
Gli argomenti e i messaggi a te assegnati verranno elencati qui. Riceverai anche una notifica di promemoria periodica delle tue assegnazioni, che puoi modificare nelle tue <a href='%{preferencesUrl}'>preferenze utente</a>. <br><br> Per assegnare un argomento o un messaggio a te stesso o a qualcun altro, cerca il pulsante Assegna %{icon} in fondo.
dismiss_assigned_tooltip: "Imposta tutte le notifiche di assegnazione non lette come lette"
assigned_to_group: "assegnato a %{group_name}"
assigned_to_you: "assegnato a te"
assigned_to_group:
post: "messaggio assegnato a %{group_name}"
topic: "argomento assegnato a %{group_name}"
assigned_to_you:
post: "messaggio assegnato a te"
topic: "argomento assegnato a te"
assignment_description:
post: "%{topic_title} (n°%{post_number})"
topic: "%{topic_title}"
admin:
web_hooks:
assign_event:
name: "Evento di assegnazione"
details: "Quando un utente assegna o annulla l'assegnazione di un argomento."
group_name: "Assegna eventi"
assigned: "Quando un utente assegna un argomento"
unassigned: "Quando un utente rimuove l'assegnazione di un argomento"
search:
advanced:
in:

View File

@ -5,6 +5,11 @@
# https://translate.discourse.org/
ja:
admin_js:
admin:
site_settings:
categories:
discourse_assign: "Discourse 割り当て"
js:
filters:
unassigned:
@ -35,9 +40,8 @@ ja:
assigned_to: "割り当て先"
assigned_topic_to: "トピックを <a href='%{path}'>%{username}</a> に割り当てました"
assign_post_to: "#%{post_number} を %{username} に割り当てました"
assign_post_to_multiple: "#%{post_number} を %{username}"
assigned_to_w_ellipsis: "割り当て先..."
assign_notification: "<p><span>%{username}</span> %{description}</p>"
assign_group_notification: "<p><span>%{username}</span> %{description}</p>"
unassign:
title: "割り当て解除"
title_w_ellipsis: "割り当て解除..."
@ -99,13 +103,21 @@ ja:
no_assignments_body: >
割り当てられたトピックとメッセージがここに一覧表示されます。また、割り当てのリマインダーが定期的に通知されます。この通知頻度は<a href='%{preferencesUrl}'>ユーザー設定</a>で変更できます。 <br><br>トピックまたはメッセージを自分または別のユーザーに割り当てるには、下にある %{icon} 割り当てボタンを探してください。
dismiss_assigned_tooltip: "すべての未読の割り当てを既読にします"
assigned_to_group: "が %{group_name} に割り当てました"
assigned_to_you: "があなたに割り当てました"
assigned_to_group:
post: "%{group_name} に割り当てられた投稿"
topic: "%{group_name} に割り当てられたトピック"
assigned_to_you:
post: "あなたに割り当てられた投稿"
topic: "あなたに割り当てらたトピック"
assignment_description:
post: "%{topic_title} (#%{post_number})"
topic: "%{topic_title}"
admin:
web_hooks:
assign_event:
name: "割り当てイベント"
details: "ユーザーがトピックを割り当てたか割り当てを解除したとき。"
group_name: "割り当てイベント"
assigned: "ユーザーがトピックを割り当てたとき"
unassigned: "ユーザーがトピックの割り当てを解除したとき"
search:
advanced:
in:

View File

@ -59,11 +59,8 @@ ko:
messages:
assigned_title: "할당됨 (%{count})"
assigned: "할당됨"
admin:
web_hooks:
assign_event:
name: "이벤트 할당"
details: "사용자가 주제를 할당 또는 할당 해제 할 때"
assignment_description:
topic: "%{topic_title}"
search:
advanced:
assigned:

View File

@ -31,6 +31,8 @@ lt:
user:
messages:
assigned: "Priskirta"
assignment_description:
topic: "%{topic_title}"
notifications:
assigned: "<span>%{username}</span> %{description}"
titles:

View File

@ -5,6 +5,11 @@
# https://translate.discourse.org/
nl:
admin_js:
admin:
site_settings:
categories:
discourse_assign: "Discourse-toewijzing"
js:
filters:
unassigned:
@ -35,9 +40,8 @@ nl:
assigned_to: "Toegewezen aan"
assigned_topic_to: "Topic toegewezen aan <a href='%{path}'>%{username}</a>"
assign_post_to: "#%{post_number} toegewezen aan %{username}"
assign_post_to_multiple: "#%{post_number} aan %{username}"
assigned_to_w_ellipsis: "Toegewezen aan..."
assign_notification: "<p><span>%{username}</span> %{description}</p>"
assign_group_notification: "<p><span>%{username}</span> %{description}</p>"
unassign:
title: "Toewijzing opheffen"
title_w_ellipsis: "Toewijzing opheffen..."
@ -99,13 +103,21 @@ nl:
no_assignments_body: >
Je toegewezen topics en berichten worden hier weergegeven. Ook ontvang je periodiek een herinneringsmelding over je toewijzingen, die je kunt aanpassen in je <a href='%{preferencesUrl}'>gebruikersvoorkeuren</a>. <br><br> Om een topic of bericht aan jezelf of iemand anders toe te wijzen, zoek je de knop %{icon} Toewijzen onderaan.
dismiss_assigned_tooltip: "Alle ongelezen toewijzingsmeldingen markeren als gelezen"
assigned_to_group: "toegewezen aan %{group_name}"
assigned_to_you: "aan jou toegewezen"
assigned_to_group:
post: "bericht toegewezen aan %{group_name}"
topic: "topic toegewezen aan %{group_name}"
assigned_to_you:
post: "bericht toegewezen aan jou"
topic: "topic toegewezen aan jou"
assignment_description:
post: "%{topic_title} (#%{post_number})"
topic: "%{topic_title}"
admin:
web_hooks:
assign_event:
name: "Toewijzingsgebeurtenis"
details: "Wanneer een gebruiker een topic toewijst of de toewijzing ervan opheft."
group_name: "Toewijzingsgebeurtenissen"
assigned: "Wanneer een gebruiker een topic toewijst"
unassigned: "Wanneer een gebruiker de toewijzing van een topic opheft"
search:
advanced:
in:

View File

@ -36,8 +36,6 @@ pl_PL:
assigned_topic_to: "Przypisano temat do <a href='%{path}'>%{username}</a>"
assign_post_to: "Przypisano #%{post_number} do %{username}"
assigned_to_w_ellipsis: "Przypisano do..."
assign_notification: "<p><span>%{username}</span> %{description}</p>"
assign_group_notification: "<p><span>%{username}</span> %{description}</p>"
unassign:
title: "Cofnij przypisanie"
title_w_ellipsis: "Cofnij przypisanie..."
@ -98,13 +96,19 @@ pl_PL:
no_assignments_body: >
Tutaj zostaną wyświetlone Twoje przypisane tematy i wiadomości. Będziesz również otrzymywać okresowe przypomnienie o swoich przypisach, które możesz dostosować w swoich <a href='%{preferencesUrl}'>preferencjach użytkownika</a>. <br><br> Aby przypisać temat lub wiadomość do siebie lub innej osoby, poszukaj przycisku %{icon} przypisywania na dole.
dismiss_assigned_tooltip: "Oznacz wszystkie nieprzeczytane powiadomienia o przypisaniu jako przeczytane"
assigned_to_group: "przypisany do %{group_name}"
assigned_to_you: "przypisany do Ciebie"
assigned_to_group:
post: "post przypisany do %{group_name}"
topic: "temat przypisany do %{group_name}"
assigned_to_you:
post: "post przypisany do Ciebie"
topic: "temat przypisany do Ciebie"
assignment_description:
topic: "%{topic_title}"
admin:
web_hooks:
assign_event:
name: "Przypisz wydarzenie"
details: "Gdy użytkownik przypisuje lub cofa przypisanie tematu."
assigned: "Gdy użytkownik przypisze temat"
unassigned: "Gdy użytkownik anuluje przypisanie tematu"
search:
advanced:
in:
@ -147,5 +151,6 @@ pl_PL:
titles:
assigned: "Przypisany"
user_menu:
view_all_assigned: "wyświetl wszystkie przypisania"
tabs:
assign_list: "Lista przypisań"

View File

@ -36,8 +36,6 @@ pt:
assigned_topic_to: "Tópico atribuído a <a href='%{path}'>%{username}</a>"
assign_post_to: "Atribuiu #%{post_number} a %{username}"
assigned_to_w_ellipsis: "Atribuído(a) a..."
assign_notification: "<p><span>%{username}</span> %{description}</p>"
assign_group_notification: "<p><span>%{username}</span> %{description}</p>"
unassign:
title: "Cancelar atribuição"
title_w_ellipsis: "Cancelar atribuição..."
@ -99,13 +97,8 @@ pt:
no_assignments_body: >
Seus tópicos e mensagens atribuídos serão listados aqui. Você receberá uma notificação periódica com um lembrete das suas atribuições, que pode ser ajustada nas suas <a href='%{preferencesUrl}'>preferências do(a) usuário(a)</a>. <br><br> Para atribuir um tópico ou mensagem a si ou a outra pessoa, procure o botão de atribuir %{icon} na parte inferior.
dismiss_assigned_tooltip: "Marcar como lidas todas as notificações não atribuídas não lidas"
assigned_to_group: "atribuiu a %{group_name}"
assigned_to_you: "atribuiu a você"
admin:
web_hooks:
assign_event:
name: "Atribuir evento"
details: "Quando um(a) usuário(a) atribui ou cancela atribuição de um tópico."
assignment_description:
topic: "%{topic_title}"
search:
advanced:
in:

View File

@ -5,6 +5,11 @@
# https://translate.discourse.org/
pt_BR:
admin_js:
admin:
site_settings:
categories:
discourse_assign: "Atribuição do Discourse"
js:
filters:
unassigned:
@ -35,9 +40,8 @@ pt_BR:
assigned_to: "Atribuiu a"
assigned_topic_to: "Tópico atribuído a <a href='%{path}'>%{username}</a>"
assign_post_to: "Atribuiu #%{post_number} a %{username}"
assign_post_to_multiple: "#%{post_number} para %{username}"
assigned_to_w_ellipsis: "Atribuiu a..."
assign_notification: "<p><span>%{username}</span> %{description}</p>"
assign_group_notification: "<p><span>%{username}</span> %{description}</p>"
unassign:
title: "Remover atribuição"
title_w_ellipsis: "Remover atribuição..."
@ -99,13 +103,21 @@ pt_BR:
no_assignments_body: >
Seus tópicos e mensagens atribuídas serão listados aqui. Você receberá uma notificação periódica de lembrete das suas atribuições, que você pode ajustar em suas <a href='%{preferencesUrl}'>preferências de usuário(a)/a>. <br><br> Para atribuir uma mensagem ou tópico a si ou a outra pessoa, procure o botão de atribuir %{icon} na parte inferior.
dismiss_assigned_tooltip: "Marcar todas as notificações atribuídas não lidas como lidas"
assigned_to_group: "atribuiu a %{group_name}"
assigned_to_you: "atribuiu a você"
assigned_to_group:
post: "postagem atribuída a %{group_name}"
topic: "tópico atribuído a %{group_name}"
assigned_to_you:
post: "postagem atribuída a você"
topic: "tópico atribuído a você"
assignment_description:
post: "%{topic_title} (#%{post_number})"
topic: "%{topic_title}"
admin:
web_hooks:
assign_event:
name: "Evento Atribuir"
details: "Quando usuário(a) atribui ou remove atribuição de um tópico."
group_name: "Eventos de atribuição"
assigned: "Quando um(a) usuário(a) atribuir um tópico"
unassigned: "Quando um(a) usuário(a) cancelar a atribuição de um tópico"
search:
advanced:
in:

View File

@ -22,5 +22,8 @@ ro:
weekly: "Săptămânal"
monthly: "Lunar"
quarterly: "Trimestrial"
user:
assignment_description:
topic: "%{topic_title}"
notifications:
assigned: "<span>%{username}</span> %{description}"

View File

@ -5,6 +5,11 @@
# https://translate.discourse.org/
ru:
admin_js:
admin:
site_settings:
categories:
discourse_assign: "Плагин Discourse «Назначение»"
js:
filters:
unassigned:
@ -35,9 +40,8 @@ ru:
assigned_to: "Назначено:"
assigned_topic_to: "Назначил(а) тему пользователю <a href='%{path}'>%{username}</a>"
assign_post_to: "Назначил(а) ответственным %{username} в сообщение #%{post_number}"
assign_post_to_multiple: "#%{post_number} для пользователя %{username}"
assigned_to_w_ellipsis: "Назначено..."
assign_notification: "<p><span>%{username}</span> %{description}</p>"
assign_group_notification: "<p><span>%{username}</span> %{description}</p>"
unassign:
title: "Снять ответственного"
title_w_ellipsis: "Отменить назначение..."
@ -99,13 +103,21 @@ ru:
no_assignments_body: >
Здесь будут перечислены назначенные вами темы и сообщения. Вы также будете получать периодические напоминания о ваших назначениях, которые вы можете изменить в <a href='%{preferencesUrl}'>настройках пользователя</a>. <br><br> Чтобы назначить тему или сообщение себе или кому-то ещё, используйте кнопку %{icon}, расположенную в нижней части темы.
dismiss_assigned_tooltip: "Пометить все непрочитанные уведомления о назначении как прочитанные"
assigned_to_group: "назначен(а) группе %{group_name}"
assigned_to_you: "назначен(а) вам"
assigned_to_group:
post: "публикация назначена группе %{group_name}"
topic: "тема назначена группе %{group_name}"
assigned_to_you:
post: "публикация, назначенная вам"
topic: "тема, назначенная вам"
assignment_description:
post: "%{topic_title} (#%{post_number})"
topic: "%{topic_title}"
admin:
web_hooks:
assign_event:
name: "Событие назначения"
details: "При назначении или отмене назначения ответственного в тему"
group_name: "Назначение событий"
assigned: "Когда пользователь назначает тему"
unassigned: "Когда пользователь отменяет назначение темы"
search:
advanced:
in:

View File

@ -48,6 +48,8 @@ sk:
user:
messages:
assigned: "Priradené"
assignment_description:
topic: "%{topic_title}"
search:
advanced:
assigned:

View File

@ -22,5 +22,8 @@ sl:
weekly: "Tedensko"
monthly: "Mesečno"
quarterly: "V četrtletju"
user:
assignment_description:
topic: "%{topic_title}"
notifications:
assigned: "<span>%{username}</span> %{description}"

View File

@ -19,3 +19,6 @@ sq:
weekly: "Javore"
monthly: "Mujore"
quarterly: "Tremujorsh"
user:
assignment_description:
topic: "%{topic_title}"

View File

@ -34,8 +34,6 @@ sv:
group_everyone: "Alla"
assigned_to: "Tilldelat till"
assigned_to_w_ellipsis: "Tilldelad till..."
assign_notification: "<p><span>%{username}</span>%{description}</p>"
assign_group_notification: "<p><span>%{username}</span> %{description}</p>"
unassign:
title: "Otilldela"
title_w_ellipsis: "Ta bort tilldelning..."
@ -94,13 +92,8 @@ sv:
no_assignments_body: >
Dina tilldelade ämnen och meddelanden visas här i en lista. Du kommer också att få en periodisk påminnelse om dina tilldelningar, som du kan justera i dina <a href='%{preferencesUrl}'>användarinställningar</a>. <br><br> Om du vill tilldela ett ämne eller ett meddelande till dig själv eller till någon annan letar du efter tilldelningsknappen %{icon} längst ned.
dismiss_assigned_tooltip: "Markera alla olästa tilldelningsmeddelanden som lästa"
assigned_to_group: "tilldelad till %{group_name}"
assigned_to_you: "tilldelad till dig"
admin:
web_hooks:
assign_event:
name: "Tilldela händelse"
details: "När en användare tilldelar eller återtar tilldelning för ett ämne."
assignment_description:
topic: "%{topic_title}"
search:
advanced:
in:

View File

@ -51,6 +51,8 @@ sw:
messages:
assigned_title: "Amekabidhiwa (%{count})"
assigned: "Imekabidhiwa"
assignment_description:
topic: "%{topic_title}"
search:
advanced:
assigned:

View File

@ -5,6 +5,11 @@
# https://translate.discourse.org/
tr_TR:
admin_js:
admin:
site_settings:
categories:
discourse_assign: "Discourse Atama"
js:
filters:
unassigned:
@ -35,9 +40,8 @@ tr_TR:
assigned_to: "Şuraya atandı:"
assigned_topic_to: "Konu <a href='%{path}'>%{username}</a> adlı kullanıcıya atandı"
assign_post_to: "#%{post_number}, %{username} adlı kullanıcıya atandı"
assign_post_to_multiple: "%{username} adlı kullanıcıya #%{post_number}"
assigned_to_w_ellipsis: "Şuraya atandı..."
assign_notification: "<p><span>%{username}</span> %{description}</p>"
assign_group_notification: "<p><span>%{username}</span> %{description}</p>"
unassign:
title: "Atamayı kaldır"
title_w_ellipsis: "Atamayı kaldır..."
@ -99,13 +103,21 @@ tr_TR:
no_assignments_body: >
Atanan konularınız ve mesajlarınız burada listelenir. Ayrıca, atamalarınızla ilgili periyodik bir anımsatma bildirimi alırsınız; bunu <a href='%{preferencesUrl}'>kullanıcı tercihlerinizden</a> ayarlayabilirsiniz. <br><br>Kendinize veya bir başkasına bir konu veya mesaj atamak için alttaki %{icon} ata düğmesini kullanın.
dismiss_assigned_tooltip: "Okunmamış tüm atama bildirimlerini okundu olarak işaretle"
assigned_to_group: "%{group_name} adlı gruba atandı"
assigned_to_you: "size atandı"
assigned_to_group:
post: "gönderi %{group_name} adlı gruba atandı"
topic: "konu %{group_name} adlı gruba atandı"
assigned_to_you:
post: "gönderi size atandı"
topic: "konu size atandı"
assignment_description:
post: "%{topic_title} (#%{post_number})"
topic: "%{topic_title}"
admin:
web_hooks:
assign_event:
name: "Etkinlik Ata"
details: "Bir kullanıcı bir konuyu atadığında veya atamasını kaldırdığında."
group_name: "Etkinlik Ata"
assigned: "Bir kullanıcı bir konu atadığında"
unassigned: "Bir kullanıcı bir konunun atamasını kaldırdığında"
search:
advanced:
in:

View File

@ -59,6 +59,8 @@ uk:
messages:
assigned_title: "Призначено (%{count})"
assigned: "Призначено"
assignment_description:
topic: "%{topic_title}"
search:
advanced:
assigned:

View File

@ -31,8 +31,6 @@ ur:
group_everyone: "ہر کوئی"
assigned_to: "جس کو اسائین کیا گیا"
assigned_to_w_ellipsis: "کو تفویض..."
assign_notification: "<p><span>%{username}</span> %{description}</p>"
assign_group_notification: "<p><span>%{username}</span> %{description}</p>"
unassign:
title: "غیر اسائین کریں"
title_w_ellipsis: "غیر تفویض کریں..."
@ -87,11 +85,8 @@ ur:
no_assignments_title: "آپ کے پاس ابھی تک کوئی اسائنمنٹ نہیں ہے"
no_assignments_body: >
آپ کے تفویض کردہ عنوانات اور پیغامات یہاں درج ہوں گے۔ آپ کو اپنے اسائنمنٹس کی وقفے وقفہ سے یاد دہانی کا نوٹیفکیشن بھی ملے گا ، جسے آپ اپنی <a href='%{preferencesUrl}'>صارف ترجیحات</a>میں ایڈجسٹ کرسکتے ہیں۔ <br><br> اپنے آپ کو یا کسی اور کو موضوع یا پیغام تفویض کرنے کے لئے ، %{icon} تفویض والے بٹن کو تلاش کریں۔
admin:
web_hooks:
assign_event:
name: "واقعہ تفویض کریں"
details: "جب کوئی صارف کسی موضوع کو تفویض یا غیر متعین کرتا ہے۔"
assignment_description:
topic: "%{topic_title}"
search:
advanced:
in:

View File

@ -29,6 +29,8 @@ vi:
user:
messages:
assigned: "Được chỉ định"
assignment_description:
topic: "%{topic_title}"
notifications:
assigned: "<span>%{username}</span> %{description}"
titles:

View File

@ -5,6 +5,11 @@
# https://translate.discourse.org/
zh_CN:
admin_js:
admin:
site_settings:
categories:
discourse_assign: "Discourse Assign"
js:
filters:
unassigned:
@ -35,9 +40,8 @@ zh_CN:
assigned_to: "已分配给"
assigned_topic_to: "将话题分配给 <a href='%{path}'>%{username}</a>"
assign_post_to: "将 #%{post_number} 分配给 %{username}"
assign_post_to_multiple: "#%{post_number} 被分配给 %{username}"
assigned_to_w_ellipsis: "已分配给…"
assign_notification: "<p><span>%{username}</span> %{description}</p>"
assign_group_notification: "<p><span>%{username}</span> %{description}</p>"
unassign:
title: "取消分配"
title_w_ellipsis: "取消分配…"
@ -99,13 +103,21 @@ zh_CN:
no_assignments_body: >
分配给您的话题和消息将在此处列出。您还将收到关于您的任务分配的定期提醒通知,您可以在<a href='%{preferencesUrl}'>用户偏好设置</a>中进行调整。 <br><br>要将话题或消息分配给自己或其他人,请查找底部的 %{icon} 分配按钮。
dismiss_assigned_tooltip: "将所有未读分配通知标记为已读"
assigned_to_group: "已被分配至%{group_name}"
assigned_to_you: "已被分配给您"
assigned_to_group:
post: "帖子被分配给 %{group_name}"
topic: "话题被分配给 %{group_name}"
assigned_to_you:
post: "帖子被分配给您"
topic: "话题被分配给您"
assignment_description:
post: "%{topic_title} (#%{post_number})"
topic: "%{topic_title}"
admin:
web_hooks:
assign_event:
name: "分配事件"
details: "当用户分配或取消分配话题时。"
group_name: "分配事件"
assigned: "当用户分配主题时"
unassigned: "当用户取消分配主题时"
search:
advanced:
in:

View File

@ -50,6 +50,8 @@ zh_TW:
user:
messages:
assigned: "已指派"
assignment_description:
topic: "%{topic_title}"
search:
advanced:
assigned:

View File

@ -43,6 +43,7 @@ pl_PL:
flag_assigned: "Przepraszamy, temat, w którym jest ta flaga, został przypisany do innego użytkownika."
flag_unclaimed: "Musisz przejąć ten temat, zanim zaczniesz działać na fladze."
topic_assigned_excerpt: "przypisano ci temat '%{title}'"
topic_group_assigned_excerpt: "przypisano temat '%{title}' do @%{group}"
reminders_frequency:
never: "nigdy"
daily: "dziennie"

View File

@ -7,7 +7,6 @@ DiscourseAssign::Engine.routes.draw do
get "/suggestions" => "assign#suggestions"
get "/assigned" => "assign#assigned"
get "/members/:group_name" => "assign#group_members"
get "/user-menu-assigns" => "assign#user_menu_assigns"
end
Discourse::Application.routes.draw do

View File

@ -1,4 +1,4 @@
plugins:
discourse_assign:
assign_enabled:
default: false
client: true

View File

@ -0,0 +1,81 @@
# frozen_string_literal: true
class EnsureNotificationsConsistency < ActiveRecord::Migration[7.0]
def up
DB.exec(<<~SQL)
DELETE FROM notifications
WHERE id IN (
SELECT notifications.id FROM notifications
LEFT OUTER JOIN assignments ON assignments.id = ((notifications.data::jsonb)->'assignment_id')::int
WHERE (notification_type = 34 AND assignments.id IS NULL OR assignments.active = FALSE)
)
SQL
DB.exec(<<~SQL)
WITH tmp AS (
SELECT
assignments.assigned_to_id AS user_id,
assignments.created_at,
assignments.updated_at,
assignments.topic_id,
(
CASE WHEN assignments.target_type = 'Topic' THEN 1
ELSE (SELECT posts.post_number FROM posts WHERE posts.id = assignments.target_id)
END
) AS post_number,
json_strip_nulls(json_build_object(
'message', 'discourse_assign.assign_notification',
'display_username', (SELECT users.username FROM users WHERE users.id = assignments.assigned_by_user_id),
'topic_title', topics.title,
'assignment_id', assignments.id
))::text AS data
FROM assignments
LEFT OUTER JOIN topics ON topics.deleted_at IS NULL AND topics.id = assignments.topic_id
LEFT OUTER JOIN users ON users.id = assignments.assigned_to_id AND assignments.assigned_to_type = 'User'
LEFT OUTER JOIN notifications ON ((data::jsonb)->'assignment_id')::int = assignments.id
WHERE assignments.active = TRUE
AND NOT (topics.id IS NULL OR users.id IS NULL)
AND assignments.assigned_to_type = 'User'
AND notifications.id IS NULL
)
INSERT INTO notifications (notification_type, high_priority, read, user_id, created_at, updated_at, topic_id, post_number, data)
SELECT 34, TRUE, TRUE, tmp.* FROM tmp
SQL
DB.exec(<<~SQL)
WITH tmp AS (
SELECT
users.id AS user_id,
assignments.created_at,
assignments.updated_at,
assignments.topic_id,
(
CASE WHEN assignments.target_type = 'Topic' THEN 1
ELSE (SELECT posts.post_number FROM posts WHERE posts.id = assignments.target_id)
END
) AS post_number,
json_strip_nulls(json_build_object(
'message', 'discourse_assign.assign_group_notification',
'display_username', (SELECT groups.name FROM groups WHERE groups.id = assignments.assigned_to_id),
'topic_title', topics.title,
'assignment_id', assignments.id
))::text AS data
FROM assignments
LEFT OUTER JOIN topics ON topics.deleted_at IS NULL AND topics.id = assignments.topic_id
LEFT OUTER JOIN groups ON groups.id = assignments.assigned_to_id AND assignments.assigned_to_type = 'Group'
LEFT OUTER JOIN group_users ON groups.id = group_users.group_id
LEFT OUTER JOIN users ON users.id = group_users.user_id
LEFT OUTER JOIN notifications ON ((data::jsonb)->'assignment_id')::int = assignments.id AND notifications.user_id = users.id
WHERE assignments.active = TRUE
AND NOT (topics.id IS NULL OR groups.id IS NULL)
AND assignments.assigned_to_type = 'Group'
AND notifications.id IS NULL
)
INSERT INTO notifications (notification_type, high_priority, read, user_id, created_at, updated_at, topic_id, post_number, data)
SELECT 34, TRUE, TRUE, tmp.* FROM tmp
SQL
end
def down
end
end

View File

@ -195,7 +195,7 @@ class ::Assigner
topic.posts.where(post_number: 1).first
end
def forbidden_reasons(assign_to:, type:, note:, status:)
def forbidden_reasons(assign_to:, type:, note:, status:, allow_self_reassign:)
case
when assign_to.is_a?(User) && !can_assignee_see_target?(assign_to)
if topic.private_message?
@ -211,7 +211,7 @@ class ::Assigner
end
when !can_be_assigned?(assign_to)
assign_to.is_a?(User) ? :forbidden_assign_to : :forbidden_group_assign_to
when already_assigned?(assign_to, type, note, status)
when !allow_self_reassign && already_assigned?(assign_to, type, note, status)
assign_to.is_a?(User) ? :already_assigned : :group_already_assigned
when Assignment.where(topic: topic, active: true).count >= ASSIGNMENTS_PER_TOPIC_LIMIT &&
!reassign?
@ -223,14 +223,15 @@ class ::Assigner
def update_details(assign_to, note, status, skip_small_action_post: false)
case
when @target.assignment.note != note && @target.assignment.status != status && status.present?
when note.present? && status.present? && @target.assignment.note != note &&
@target.assignment.status != status
small_action_text = <<~TEXT
Status: #{@target.assignment.status} → #{status}
#{note}
TEXT
change_type = "details"
when @target.assignment.note != note
when note.present? && @target.assignment.note != note
small_action_text = note
change_type = "note"
when @target.assignment.status != status
@ -239,11 +240,8 @@ class ::Assigner
end
@target.assignment.update!(note: note, status: status)
queue_notification(assign_to, skip_small_action_post, @target.assignment)
assignment = @target.assignment
publish_assignment(assignment, assign_to, note, status)
queue_notification(@target.assignment)
publish_assignment(@target.assignment, assign_to, note, status)
# email is skipped, for now
@ -255,7 +253,13 @@ class ::Assigner
{ success: true }
end
def assign(assign_to, note: nil, skip_small_action_post: false, status: nil)
def assign(
assign_to,
note: nil,
skip_small_action_post: false,
status: nil,
allow_self_reassign: false
)
assigned_to_type = assign_to.is_a?(User) ? "User" : "Group"
if topic.private_message? && SiteSetting.invite_on_assign
@ -263,7 +267,13 @@ class ::Assigner
end
forbidden_reason =
forbidden_reasons(assign_to: assign_to, type: assigned_to_type, note: note, status: status)
forbidden_reasons(
assign_to: assign_to,
type: assigned_to_type,
note: note,
status: status,
allow_self_reassign: allow_self_reassign,
)
return { success: false, reason: forbidden_reason } if forbidden_reason
if no_assignee_change?(assign_to) && details_change?(note, status)
@ -274,33 +284,31 @@ class ::Assigner
action_code[:user] = topic.assignment.present? ? "reassigned" : "assigned"
action_code[:group] = topic.assignment.present? ? "reassigned_group" : "assigned_group"
skip_small_action_post = skip_small_action_post || no_assignee_change?(assign_to)
skip_small_action_post =
skip_small_action_post || (!allow_self_reassign && no_assignee_change?(assign_to))
if topic.assignment.present?
if @target.assignment
Jobs.enqueue(
:unassign_notification,
topic_id: topic.id,
assigned_to_id: topic.assignment.assigned_to_id,
assigned_to_type: topic.assignment.assigned_to_type,
assigned_to_id: @target.assignment.assigned_to_id,
assigned_to_type: @target.assignment.assigned_to_type,
assignment_id: @target.assignment.id,
)
@target.assignment.destroy!
end
@target.assignment&.destroy!
assignment =
@target.create_assignment!(
assigned_to_id: assign_to.id,
assigned_to_type: assigned_to_type,
assigned_by_user_id: @assigned_by.id,
topic_id: topic.id,
assigned_to: assign_to,
assigned_by_user: @assigned_by,
topic: topic,
note: note,
status: status,
)
first_post.publish_change_to_clients!(:revised, reload_topic: true)
queue_notification(assign_to, skip_small_action_post, assignment)
queue_notification(assignment)
publish_assignment(assignment, assign_to, note, status)
if assignment.assigned_to_user?
@ -335,7 +343,7 @@ class ::Assigner
end
# Create a webhook event
if WebHook.active_web_hooks(:assign).exists?
if WebHook.active_web_hooks(:assigned).exists?
assigned_to_type = :assigned
payload = {
type: assigned_to_type,
@ -370,24 +378,9 @@ class ::Assigner
topic_id: topic.id,
assigned_to_id: assignment.assigned_to.id,
assigned_to_type: assignment.assigned_to_type,
assignment_id: assignment.id,
)
if assignment.assigned_to_user?
if TopicUser.exists?(
user_id: assignment.assigned_to_id,
topic: topic,
notification_level: TopicUser.notification_levels[:watching],
notifications_reason_id: TopicUser.notification_reasons[:plugin_changed],
)
TopicUser.change(
assignment.assigned_to_id,
topic.id,
notification_level: TopicUser.notification_levels[:tracking],
notifications_reason_id: TopicUser.notification_reasons[:plugin_changed],
)
end
end
assigned_to = assignment.assigned_to
if SiteSetting.unassign_creates_tracking_post && !silent
@ -413,7 +406,7 @@ class ::Assigner
end
# Create a webhook event
if WebHook.active_web_hooks(:assign).exists?
if WebHook.active_web_hooks(:unassigned).exists?
type = :unassigned
payload = {
type: type,
@ -477,17 +470,8 @@ class ::Assigner
@guardian ||= Guardian.new(@assigned_by)
end
def queue_notification(assign_to, skip_small_action_post, assignment)
Jobs.enqueue(
:assign_notification,
topic_id: topic.id,
post_id: topic_target? ? first_post.id : @target.id,
assigned_to_id: assign_to.id,
assigned_to_type: assign_to.is_a?(User) ? "User" : "Group",
assigned_by_id: @assigned_by.id,
skip_small_action_post: skip_small_action_post,
assignment_id: assignment.id,
)
def queue_notification(assignment)
Jobs.enqueue(:assign_notification, assignment_id: assignment.id)
end
def add_small_action_post(action_code, assign_to, text)
@ -546,7 +530,7 @@ class ::Assigner
""
end
return "unassigned#{suffix}" if assignment.assigned_to_user?
return "unassigned_group#{suffix}" if assignment.assigned_to_group?
"unassigned_group#{suffix}" if assignment.assigned_to_group?
end
def already_assigned?(assign_to, type, note, status)

View File

@ -0,0 +1,103 @@
# frozen_string_literal: true
module DiscourseAssign
class CreateNotification
class UserAssignment
attr_reader :assignment
def initialize(assignment)
@assignment = assignment
end
def excerpt_key
"discourse_assign.topic_assigned_excerpt"
end
def notification_message
"discourse_assign.assign_notification"
end
def display_username
assignment.assigned_by_user.username
end
end
class GroupAssignment < UserAssignment
def excerpt_key
"discourse_assign.topic_group_assigned_excerpt"
end
def notification_message
"discourse_assign.assign_group_notification"
end
def display_username
assignment.assigned_to.name
end
end
def self.call(...)
new(...).call
end
attr_reader :assignment, :user, :mark_as_read, :assignment_type
alias mark_as_read? mark_as_read
delegate :topic,
:post,
:assigned_by_user,
:assigned_to,
:created_at,
:updated_at,
:assigned_to_user?,
:id,
to: :assignment,
private: true
delegate :excerpt_key,
:notification_message,
:display_username,
to: :assignment_type,
private: true
def initialize(assignment:, user:, mark_as_read:)
@assignment = assignment
@user = user
@mark_as_read = mark_as_read
@assignment_type =
"#{self.class}::#{assignment.assigned_to.class}Assignment".constantize.new(assignment)
end
def call
Assigner.publish_topic_tracking_state(topic, user.id)
unless mark_as_read?
PostAlerter.new(post).create_notification_alert(
user: user,
post: post,
username: assigned_by_user.username,
notification_type: Notification.types[:assigned],
excerpt:
I18n.t(
excerpt_key,
title: topic.title,
group: assigned_to.name,
locale: user.effective_locale,
),
)
end
user.notifications.assigned.create!(
created_at: created_at,
updated_at: updated_at,
topic: topic,
post_number: post.post_number,
high_priority: true,
read: mark_as_read?,
data: {
message: notification_message,
display_username: display_username,
topic_title: topic.title,
assignment_id: id,
}.to_json,
)
end
end
end

View File

@ -2,12 +2,15 @@
module DiscourseAssign
module GroupExtension
def self.prepended(base)
base.class_eval do
scope :assignable,
->(user) {
where(
"assignable_level in (:levels) OR
extend ActiveSupport::Concern
prepended do
has_many :assignments, as: :assigned_to
scope :assignable,
->(user) do
where(
"assignable_level in (:levels) OR
(
assignable_level = #{Group::ALIAS_LEVELS[:members_mods_and_admins]} AND id in (
SELECT group_id FROM group_users WHERE user_id = :user_id)
@ -15,11 +18,10 @@ module DiscourseAssign
assignable_level = #{Group::ALIAS_LEVELS[:owners_mods_and_admins]} AND id in (
SELECT group_id FROM group_users WHERE user_id = :user_id AND owner IS TRUE)
)",
levels: alias_levels(user),
user_id: user&.id,
)
}
end
levels: alias_levels(user),
user_id: user&.id,
)
end
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module DiscourseAssign
module NotificationExtension
extend ActiveSupport::Concern
prepended do
scope :assigned, -> { where(notification_type: Notification.types[:assigned]) }
scope :for_assignment,
->(assignment) do
assigned.where("((data::jsonb)->'assignment_id')::int IN (?)", assignment)
end
end
end
end

View File

@ -5,9 +5,7 @@ module DiscourseAssign
def self.prepended(base)
base.class_eval do
def self.enqueue_assign_hooks(event, payload)
if active_web_hooks("assign").exists?
WebHook.enqueue_hooks(:assign, event, payload: payload)
end
WebHook.enqueue_hooks(:assign, event, payload: payload) if active_web_hooks(event).exists?
end
end
end

View File

@ -1,130 +1,190 @@
# frozen_string_literal: true
class RandomAssignUtils
def self.raise_error(automation, message)
raise("[discourse-automation id=#{automation.id}] #{message}.")
attr_reader :context, :fields, :automation, :topic, :group
def self.automation_script!(...)
new(...).automation_script!
end
def self.log_info(automation, message)
Rails.logger.info("[discourse-automation id=#{automation.id}] #{message}.")
end
def self.automation_script!(context, fields, automation)
raise_error(automation, "discourse-assign is not enabled") unless SiteSetting.assign_enabled?
def initialize(context, fields, automation)
@context = context
@fields = fields
@automation = automation
raise_error("discourse-assign is not enabled") unless SiteSetting.assign_enabled?
unless topic_id = fields.dig("assigned_topic", "value")
raise_error(automation, "`assigned_topic` not provided")
raise_error("`assigned_topic` not provided")
end
unless topic = Topic.find_by(id: topic_id)
raise_error(automation, "Topic(#{topic_id}) not found")
end
min_hours = fields.dig("minimum_time_between_assignments", "value").presence
if min_hours &&
TopicCustomField
.where(name: "assigned_to_id", topic_id: topic_id)
.where("created_at < ?", min_hours.to_i.hours.ago)
.exists?
log_info(automation, "Topic(#{topic_id}) has already been assigned recently")
return
unless @topic = Topic.find_by(id: topic_id)
raise_error("Topic(#{topic_id}) not found")
end
unless group_id = fields.dig("assignees_group", "value")
raise_error(automation, "`assignees_group` not provided")
raise_error("`assignees_group` not provided")
end
unless group = Group.find_by(id: group_id)
raise_error(automation, "Group(#{group_id}) not found")
unless @group = Group.find_by(id: group_id)
raise_error("Group(#{group_id}) not found")
end
assignable_user_ids = User.assign_allowed.pluck(:id)
users_on_holiday =
Set.new(
User.where(
id: UserCustomField.where(name: "on_holiday", value: "t").select(:user_id),
).pluck(:id),
)
group_users = group.group_users.joins(:user)
if skip_new_users_for_days = fields.dig("skip_new_users_for_days", "value").presence
group_users = group_users.where("users.created_at < ?", skip_new_users_for_days.to_i.days.ago)
end
group_users_ids =
group_users
.pluck("users.id")
.filter { |user_id| assignable_user_ids.include?(user_id) }
.reject { |user_id| users_on_holiday.include?(user_id) }
if group_users_ids.empty?
RandomAssignUtils.no_one!(topic_id, group.name)
return
end
max_recently_assigned_days =
(fields.dig("max_recently_assigned_days", "value").presence || 180).to_i.days.ago
last_assignees_ids =
RandomAssignUtils.recently_assigned_users_ids(topic_id, max_recently_assigned_days)
users_ids = group_users_ids - last_assignees_ids
if users_ids.blank?
min_recently_assigned_days =
(fields.dig("min_recently_assigned_days", "value").presence || 14).to_i.days.ago
recently_assigned_users_ids =
RandomAssignUtils.recently_assigned_users_ids(topic_id, min_recently_assigned_days)
users_ids = group_users_ids - recently_assigned_users_ids
end
if users_ids.blank?
RandomAssignUtils.no_one!(topic_id, group.name)
return
end
if fields.dig("in_working_hours", "value")
assign_to_user_id =
users_ids.shuffle.find { |user_id| RandomAssignUtils.in_working_hours?(user_id) }
end
assign_to_user_id ||= users_ids.sample
if assign_to_user_id.blank?
RandomAssignUtils.no_one!(topic_id, group.name)
return
end
assign_to = User.find(assign_to_user_id)
result = nil
if raw = fields.dig("post_template", "value").presence
post =
PostCreator.new(
Discourse.system_user,
raw: raw,
skip_validations: true,
topic_id: topic.id,
).create!
result = Assigner.new(post, Discourse.system_user).assign(assign_to)
PostDestroyer.new(Discourse.system_user, post).destroy if !result[:success]
else
result = Assigner.new(topic, Discourse.system_user).assign(assign_to)
end
RandomAssignUtils.no_one!(topic_id, group.name) if !result[:success]
end
def self.recently_assigned_users_ids(topic_id, from)
posts =
Post
.joins(:user)
.where(topic_id: topic_id, action_code: %w[assigned reassigned assigned_to_post])
.where("posts.created_at > ?", from)
.order(created_at: :desc)
def automation_script!
return log_info("Topic(#{topic.id}) has already been assigned recently") if assigned_recently?
return no_one! unless assigned_user
assign_user!
end
def recently_assigned_users_ids(from)
usernames =
Post.custom_fields_for_ids(posts, [:action_code_who]).map { |_, v| v["action_code_who"] }.uniq
User.where(username: usernames).limit(100).pluck(:id)
PostCustomField
.joins(:post)
.where(
name: "action_code_who",
posts: {
topic: topic,
action_code: %w[assigned reassigned assigned_to_post],
},
)
.where("posts.created_at > ?", from)
.order("posts.created_at DESC")
.pluck(:value)
.uniq
User
.where(username: usernames)
.joins(
"JOIN unnest('{#{usernames.join(",")}}'::text[]) WITH ORDINALITY t(username, ord) USING(username)",
)
.limit(100)
.order("ord")
.pluck(:id)
end
def self.user_tzinfo(user_id)
private
def assigned_user
@assigned_user ||=
begin
group_users_ids = group_users.pluck(:id)
return if group_users_ids.empty?
last_assignees_ids = recently_assigned_users_ids(max_recently_assigned_days)
users_ids = group_users_ids - last_assignees_ids
if users_ids.blank?
recently_assigned_users_ids = recently_assigned_users_ids(min_recently_assigned_days)
users_ids = group_users_ids - recently_assigned_users_ids
end
users_ids << last_assignees_ids.intersection(group_users_ids).last if users_ids.blank?
if fields.dig("in_working_hours", "value")
assign_to_user_id = users_ids.shuffle.detect { |user_id| in_working_hours?(user_id) }
end
assign_to_user_id ||= users_ids.sample
User.find(assign_to_user_id)
end
end
def assign_user!
return create_post_template if post_template
Assigner
.new(topic, Discourse.system_user)
.assign(assigned_user, allow_self_reassign: true)
.then do |result|
next if result[:success]
no_one!
end
end
def create_post_template
post =
PostCreator.new(
Discourse.system_user,
raw: post_template,
skip_validations: true,
topic_id: topic.id,
).create!
Assigner
.new(post, Discourse.system_user)
.assign(assigned_user, allow_self_reassign: true)
.then do |result|
next if result[:success]
PostDestroyer.new(Discourse.system_user, post).destroy
no_one!
end
end
def group_users
users =
group
.users
.where(id: User.assign_allowed.select(:id))
.where.not(
id:
User
.joins(:_custom_fields)
.where(user_custom_fields: { name: "on_holiday", value: "t" })
.select(:id),
)
return users unless skip_new_users_for_days
users.where("users.created_at < ?", skip_new_users_for_days)
end
def raise_error(message)
raise("[discourse-automation id=#{automation.id}] #{message}.")
end
def log_info(message)
Rails.logger.info("[discourse-automation id=#{automation.id}] #{message}.")
end
def no_one!
PostCreator.create!(
Discourse.system_user,
topic_id: topic.id,
raw: I18n.t("discourse_automation.scriptables.random_assign.no_one", group: group.name),
validate: false,
)
end
def assigned_recently?
return unless min_hours
TopicCustomField
.where(name: "assigned_to_id", topic: topic)
.where("created_at < ?", min_hours)
.exists?
end
def skip_new_users_for_days
days = fields.dig("skip_new_users_for_days", "value").presence
return unless days
days.to_i.days.ago
end
def max_recently_assigned_days
@max_days ||= (fields.dig("max_recently_assigned_days", "value").presence || 180).to_i.days.ago
end
def min_recently_assigned_days
@min_days ||= (fields.dig("min_recently_assigned_days", "value").presence || 14).to_i.days.ago
end
def post_template
@post_template ||= fields.dig("post_template", "value").presence
end
def min_hours
hours = fields.dig("minimum_time_between_assignments", "value").presence
return unless hours
hours.to_i.hours.ago
end
def in_working_hours?(user_id)
tzinfo = user_tzinfo(user_id)
tztime = tzinfo.now
!tztime.saturday? && !tztime.sunday? && tztime.hour > 7 && tztime.hour < 11
end
def user_tzinfo(user_id)
timezone = UserOption.where(user_id: user_id).pluck(:timezone).first || "UTC"
tzinfo = nil
@ -140,20 +200,4 @@ class RandomAssignUtils
tzinfo
end
def self.no_one!(topic_id, group)
PostCreator.create!(
Discourse.system_user,
topic_id: topic_id,
raw: I18n.t("discourse_automation.scriptables.random_assign.no_one", group: group),
validate: false,
)
end
def self.in_working_hours?(user_id)
tzinfo = RandomAssignUtils.user_tzinfo(user_id)
tztime = tzinfo.now
!tztime.saturday? && !tztime.sunday? && tztime.hour > 7 && tztime.hour < 11
end
end

View File

@ -1,10 +1,10 @@
{
"name": "discourse",
"version": "1.0.1",
"repository": "https://github.com/discourse/discourse-assign",
"author": "Discourse",
"license": "MIT",
"name": "discourse-assign",
"private": true,
"devDependencies": {
"eslint-config-discourse": "^3.4.0"
"@discourse/lint-configs": "^1.3.5",
"ember-template-lint": "^5.13.0",
"eslint": "^8.56.0",
"prettier": "^2.8.8"
}
}

112
plugin.rb
View File

@ -1,7 +1,8 @@
# frozen_string_literal: true
# name: discourse-assign
# about: Assign users to topics
# about: Provides the ability to assign topics and individual posts to a user or group.
# meta_topic_id: 58044
# version: 1.0.1
# authors: Sam Saffron
# url: https://github.com/discourse/discourse-assign
@ -26,10 +27,12 @@ after_initialize do
require_relative "app/jobs/regular/unassign_notification"
require_relative "app/jobs/scheduled/enqueue_reminders"
require_relative "lib/assigner"
require_relative "lib/discourse_assign/create_notification"
require_relative "lib/discourse_assign/discourse_calendar"
require_relative "lib/discourse_assign/group_extension"
require_relative "lib/discourse_assign/helpers"
require_relative "lib/discourse_assign/list_controller_extension"
require_relative "lib/discourse_assign/notification_extension"
require_relative "lib/discourse_assign/post_extension"
require_relative "lib/discourse_assign/topic_extension"
require_relative "lib/discourse_assign/web_hook_extension"
@ -38,11 +41,12 @@ after_initialize do
require_relative "lib/topic_assigner"
reloadable_patch do |plugin|
Group.class_eval { prepend DiscourseAssign::GroupExtension }
ListController.class_eval { prepend DiscourseAssign::ListControllerExtension }
Post.class_eval { prepend DiscourseAssign::PostExtension }
Topic.class_eval { prepend DiscourseAssign::TopicExtension }
WebHook.class_eval { prepend DiscourseAssign::WebHookExtension }
Group.prepend(DiscourseAssign::GroupExtension)
ListController.prepend(DiscourseAssign::ListControllerExtension)
Post.prepend(DiscourseAssign::PostExtension)
Topic.prepend(DiscourseAssign::TopicExtension)
WebHook.prepend(DiscourseAssign::WebHookExtension)
Notification.prepend(DiscourseAssign::NotificationExtension)
end
register_group_param(:assignable_level)
@ -52,7 +56,7 @@ after_initialize do
frequency_field = PendingAssignsReminder::REMINDERS_FREQUENCY
register_editable_user_custom_field frequency_field
User.register_custom_field_type frequency_field, :integer
register_user_custom_field_type(frequency_field, :integer, max_length: 10)
DiscoursePluginRegistry.serialized_current_user_fields << frequency_field
add_to_serializer(:user, :reminders_frequency) { RemindAssignsFrequencySiteSettings.values }
@ -88,13 +92,10 @@ after_initialize do
end
add_to_class(:user, :can_assign?) do
@can_assign ||=
begin
return true if admin?
allowed_groups = SiteSetting.assign_allowed_on_groups.split("|").compact
allowed_groups.present? && groups.where(id: allowed_groups).exists? ? :true : :false
end
@can_assign == :true
return @can_assign if defined?(@can_assign)
allowed_groups = SiteSetting.assign_allowed_on_groups.split("|").compact
@can_assign = admin? || (allowed_groups.present? && groups.where(id: allowed_groups).exists?)
end
add_to_serializer(:current_user, :never_auto_track_topics) do
@ -167,7 +168,12 @@ after_initialize do
on(:unassign_topic) { |topic, unassigning_user| Assigner.new(topic, unassigning_user).unassign }
Site.preloaded_category_custom_fields << "enable_unassigned_filter"
if respond_to?(:register_preloaded_category_custom_fields)
register_preloaded_category_custom_fields("enable_unassigned_filter")
else
# TODO: Drop the if-statement and this if-branch in Discourse v3.2
Site.preloaded_category_custom_fields << "enable_unassigned_filter"
end
BookmarkQuery.on_preload do |bookmarks, bookmark_query|
if SiteSetting.assign_enabled?
@ -201,7 +207,8 @@ after_initialize do
allowed_access = SiteSetting.assigns_public || can_assign
if allowed_access && topics.length > 0
assignments = Assignment.strict_loading.where(topic: topics, active: true).includes(:target)
assignments =
Assignment.strict_loading.active.where(topic: topics).includes(:target, :assigned_to)
assignments_map = assignments.group_by(&:topic_id)
user_ids =
@ -503,7 +510,7 @@ after_initialize do
:topic_view,
:assigned_to_user,
include_condition: -> do
(SiteSetting.assigns_public || scope.can_assign?) && object.topic.assigned_to&.is_a?(User)
(SiteSetting.assigns_public || scope.can_assign?) && object.topic.assigned_to.is_a?(User)
end,
) { DiscourseAssign::Helpers.build_assigned_to_user(object.topic.assigned_to, object.topic) }
@ -511,7 +518,7 @@ after_initialize do
:topic_view,
:assigned_to_group,
include_condition: -> do
(SiteSetting.assigns_public || scope.can_assign?) && object.topic.assigned_to&.is_a?(Group)
(SiteSetting.assigns_public || scope.can_assign?) && object.topic.assigned_to.is_a?(Group)
end,
) { DiscourseAssign::Helpers.build_assigned_to_group(object.topic.assigned_to, object.topic) }
@ -551,7 +558,7 @@ after_initialize do
:suggested_topic,
:assigned_to_user,
include_condition: -> do
(SiteSetting.assigns_public || scope.can_assign?) && object.assigned_to&.is_a?(User)
(SiteSetting.assigns_public || scope.can_assign?) && object.assigned_to.is_a?(User)
end,
) { DiscourseAssign::Helpers.build_assigned_to_user(object.assigned_to, object) }
@ -559,7 +566,7 @@ after_initialize do
:suggested_topic,
:assigned_to_group,
include_condition: -> do
(SiteSetting.assigns_public || scope.can_assign?) && object.assigned_to&.is_a?(Group)
(SiteSetting.assigns_public || scope.can_assign?) && object.assigned_to.is_a?(Group)
end,
) { DiscourseAssign::Helpers.build_assigned_to_group(object.assigned_to, object) }
@ -584,7 +591,7 @@ after_initialize do
:topic_list_item,
:assigned_to_user,
include_condition: -> do
(SiteSetting.assigns_public || scope.can_assign?) && object.assigned_to&.is_a?(User)
(SiteSetting.assigns_public || scope.can_assign?) && object.assigned_to.is_a?(User)
end,
) { BasicUserSerializer.new(object.assigned_to, scope: scope, root: false).as_json }
@ -592,7 +599,7 @@ after_initialize do
:topic_list_item,
:assigned_to_group,
include_condition: -> do
(SiteSetting.assigns_public || scope.can_assign?) && object.assigned_to&.is_a?(Group)
(SiteSetting.assigns_public || scope.can_assign?) && object.assigned_to.is_a?(Group)
end,
) { AssignedGroupSerializer.new(object.assigned_to, scope: scope, root: false).as_json }
@ -610,7 +617,7 @@ after_initialize do
:search_topic_list_item,
:assigned_to_user,
include_condition: -> do
(SiteSetting.assigns_public || scope.can_assign?) && object.assigned_to&.is_a?(User)
(SiteSetting.assigns_public || scope.can_assign?) && object.assigned_to.is_a?(User)
end,
) { DiscourseAssign::Helpers.build_assigned_to_user(object.assigned_to, object) }
@ -618,7 +625,7 @@ after_initialize do
:search_topic_list_item,
:assigned_to_group,
include_condition: -> do
(SiteSetting.assigns_public || scope.can_assign?) && object.assigned_to&.is_a?(Group)
(SiteSetting.assigns_public || scope.can_assign?) && object.assigned_to.is_a?(Group)
end,
) { AssignedGroupSerializer.new(object.assigned_to, scope: scope, root: false).as_json }
@ -635,7 +642,11 @@ after_initialize do
if @user.can_assign?
assign_user = User.find_by_username(@operation[:username])
topics.each do |topic|
Assigner.new(topic, @user).assign(assign_user, note: @operation[:note])
Assigner.new(topic, @user).assign(
assign_user,
status: @operation[:status],
note: @operation[:note],
)
end
end
end
@ -647,6 +658,7 @@ after_initialize do
end
register_permitted_bulk_action_parameter :username
register_permitted_bulk_action_parameter :status
register_permitted_bulk_action_parameter :note
add_to_class(:user_bookmark_base_serializer, :assigned_to) do
@ -663,7 +675,7 @@ after_initialize do
:assigned_to_user,
include_condition: -> do
return false if !can_have_assignment?
(SiteSetting.assigns_public || scope.can_assign?) && assigned_to&.is_a?(User)
(SiteSetting.assigns_public || scope.can_assign?) && assigned_to.is_a?(User)
end,
) do
return if !can_have_assignment?
@ -675,7 +687,7 @@ after_initialize do
:assigned_to_group,
include_condition: -> do
return false if !can_have_assignment?
(SiteSetting.assigns_public || scope.can_assign?) && assigned_to&.is_a?(Group)
(SiteSetting.assigns_public || scope.can_assign?) && assigned_to.is_a?(Group)
end,
) do
return if !can_have_assignment?
@ -694,7 +706,7 @@ after_initialize do
:assigned_to_user,
include_condition: -> do
(SiteSetting.assigns_public || scope.can_assign?) &&
object.assignment&.assigned_to&.is_a?(User) && object.assignment.active
object.assignment&.assigned_to.is_a?(User) && object.assignment.active
end,
) { BasicUserSerializer.new(object.assignment.assigned_to, scope: scope, root: false).as_json }
@ -703,7 +715,7 @@ after_initialize do
:assigned_to_group,
include_condition: -> do
(SiteSetting.assigns_public || scope.can_assign?) &&
object.assignment&.assigned_to&.is_a?(Group) && object.assignment.active
object.assignment&.assigned_to.is_a?(Group) && object.assignment.active
end,
) do
AssignedGroupSerializer.new(object.assignment.assigned_to, scope: scope, root: false).as_json
@ -733,13 +745,13 @@ after_initialize do
add_to_serializer(
:flagged_topic,
:assigned_to_user,
include_condition: -> { object.assigned_to && object.assigned_to&.is_a?(User) },
include_condition: -> { object.assigned_to && object.assigned_to.is_a?(User) },
) { DiscourseAssign::Helpers.build_assigned_to_user(object.assigned_to, object) }
add_to_serializer(
:flagged_topic,
:assigned_to_group,
include_condition: -> { object.assigned_to && object.assigned_to&.is_a?(Group) },
include_condition: -> { object.assigned_to && object.assigned_to.is_a?(Group) },
) { DiscourseAssign::Helpers.build_assigned_to_group(object.assigned_to, object) }
# Reviewable
@ -836,20 +848,12 @@ after_initialize do
next if !info[:group]
Assignment
.where(topic_id: topic.id, active: false)
.inactive
.where(topic: topic)
.find_each do |assignment|
next unless assignment.target
assignment.update!(active: true)
Jobs.enqueue(
:assign_notification,
topic_id: topic.id,
post_id: assignment.target_type.is_a?(Topic) ? topic.first_post.id : assignment.target.id,
assigned_to_id: assignment.assigned_to_id,
assigned_to_type: assignment.assigned_to_type,
assigned_by_id: assignment.assigned_by_user_id,
skip_small_action_post: true,
assignment_id: assignment.id,
)
Jobs.enqueue(:assign_notification, assignment_id: assignment.id)
end
end
@ -863,32 +867,30 @@ after_initialize do
next if !info[:group]
Assignment
.where(topic_id: topic.id, active: true)
.active
.where(topic: topic)
.find_each do |assignment|
assignment.update!(active: false)
Jobs.enqueue(
:unassign_notification,
topic_id: topic.id,
assigned_to_id: assignment.assigned_to.id,
assigned_to_id: assignment.assigned_to_id,
assigned_to_type: assignment.assigned_to_type,
assignment_id: assignment.id,
)
end
end
on(:user_removed_from_group) do |user, group|
assign_allowed_groups = SiteSetting.assign_allowed_on_groups.split("|").map(&:to_i)
if assign_allowed_groups.include?(group.id)
groups = GroupUser.where(user: user).pluck(:group_id)
if (groups & assign_allowed_groups).empty?
topics = Topic.joins(:assignment).where("assignments.assigned_to_id = ?", user.id)
topics.each { |topic| Assigner.new(topic, Discourse.system_user).unassign }
end
on(:user_added_to_group) do |user, group, automatic:|
group.assignments.active.find_each do |assignment|
Jobs.enqueue(:assign_notification, assignment_id: assignment.id)
end
end
on(:user_removed_from_group) do |user, group|
user.notifications.for_assignment(group.assignments.select(:id)).destroy_all
end
on(:post_moved) do |post, original_topic_id|
assignment =
Assignment.where(topic_id: original_topic_id, target_type: "Post", target_id: post.id).first

Some files were not shown because too many files have changed in this diff Show More