Compare commits
80 Commits
dev
...
featuer/26
Author | SHA1 | Date |
---|---|---|
Oliver Günther | afbfb25e70 | 4 years ago |
Oliver Günther | 426f3776a7 | 4 years ago |
Oliver Günther | 65ef084c99 | 4 years ago |
Oliver Günther | 4e3d8734f2 | 4 years ago |
ulferts | 1efa55be81 | 4 years ago |
ulferts | 8add8da243 | 4 years ago |
ulferts | 119cff3213 | 4 years ago |
Oliver Günther | d57674f492 | 4 years ago |
Oliver Günther | 966b3cbe7f | 4 years ago |
Oliver Günther | dc91145047 | 4 years ago |
ulferts | 2e77553d4c | 4 years ago |
ulferts | f9dec255b9 | 4 years ago |
ulferts | cf54fddc28 | 4 years ago |
ulferts | 88fc2ab5c6 | 4 years ago |
ulferts | 8e064e5e1f | 4 years ago |
Oliver Günther | effda48d01 | 4 years ago |
Oliver Günther | 3d6cd9fbe6 | 4 years ago |
Oliver Günther | 5992ab0147 | 4 years ago |
Oliver Günther | be7362fbd2 | 4 years ago |
Oliver Günther | 4dcb508720 | 4 years ago |
Oliver Günther | 494c829b24 | 4 years ago |
Oliver Günther | de428a9d55 | 4 years ago |
ulferts | a32302b60c | 4 years ago |
ulferts | 51c87be322 | 4 years ago |
Oliver Günther | 0d6c0b6bc7 | 4 years ago |
ulferts | 3cf09fd0d5 | 4 years ago |
Oliver Günther | 2cf0396bdb | 4 years ago |
Oliver Günther | b9c713565a | 4 years ago |
Oliver Günther | f8fe3014b7 | 4 years ago |
Oliver Günther | fef1282de0 | 4 years ago |
ulferts | f92c8628fa | 4 years ago |
ulferts | d6baa32cb0 | 4 years ago |
ulferts | 28f0f5e9e9 | 4 years ago |
ulferts | 964e06ff7b | 4 years ago |
ulferts | 1128e1a3eb | 4 years ago |
Oliver Günther | dd03b0c64a | 4 years ago |
Oliver Günther | cc9f0e62c9 | 4 years ago |
Oliver Günther | 14fc45caa1 | 4 years ago |
Oliver Günther | 83152ee177 | 4 years ago |
Oliver Günther | c7e48ba4dd | 4 years ago |
ulferts | 838dee9a04 | 4 years ago |
Oliver Günther | 519cb4e380 | 4 years ago |
Oliver Günther | 4403e6d069 | 4 years ago |
Oliver Günther | eda0daada1 | 4 years ago |
Oliver Günther | 49a7c6e14d | 4 years ago |
Oliver Günther | 27e59cac1c | 4 years ago |
ulferts | 7b0a5300a7 | 4 years ago |
ulferts | edc613bf9b | 4 years ago |
ulferts | b1ef3179b3 | 4 years ago |
ulferts | 6fe151969f | 4 years ago |
ulferts | 0df48b9880 | 4 years ago |
ulferts | 37c9eeb739 | 4 years ago |
ulferts | 8b38494f50 | 4 years ago |
ulferts | 53e480075d | 4 years ago |
Oliver Günther | b78531d839 | 4 years ago |
Oliver Günther | 224727ce76 | 4 years ago |
Oliver Günther | 1e42f5757d | 4 years ago |
Oliver Günther | 875fb79e84 | 4 years ago |
Oliver Günther | 9260e3f53a | 4 years ago |
Oliver Günther | d99bed43da | 4 years ago |
Oliver Günther | cfec3d64b6 | 4 years ago |
Oliver Günther | 3961e21530 | 4 years ago |
Oliver Günther | 10e89ff664 | 4 years ago |
Oliver Günther | 5a5cc51455 | 4 years ago |
Oliver Günther | a878206150 | 4 years ago |
Oliver Günther | 135f499420 | 4 years ago |
Oliver Günther | 93ad8216c8 | 4 years ago |
Oliver Günther | 529dd95991 | 4 years ago |
Oliver Günther | e3c53c28ac | 4 years ago |
Oliver Günther | 671f4eb3f4 | 4 years ago |
Oliver Günther | 105d970425 | 4 years ago |
ulferts | d7e7bd971f | 4 years ago |
Oliver Günther | 87b77167c1 | 4 years ago |
Oliver Günther | eedf1fa1c5 | 4 years ago |
Oliver Günther | 898a502a8a | 4 years ago |
Oliver Günther | 275d319f39 | 4 years ago |
Oliver Günther | 874b9ed5a1 | 4 years ago |
Oliver Günther | b0d51d8375 | 4 years ago |
Oliver Günther | b508e974a1 | 4 years ago |
Oliver Günther | f377213d26 | 4 years ago |
@ -0,0 +1,72 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
module Notifications |
||||
class CreateContract < ::ModelContract |
||||
attribute :recipient |
||||
attribute :subject |
||||
attribute :reason |
||||
attribute :context |
||||
attribute :context_type |
||||
attribute :resource |
||||
attribute :resource_type |
||||
attribute :read_ian |
||||
attribute :read_email |
||||
|
||||
validate :validate_recipient_present |
||||
validate :validate_subject_present |
||||
validate :validate_reason_present |
||||
validate :validate_channels |
||||
|
||||
def validate_recipient_present |
||||
errors.add(:recipient, :blank) if model.recipient.blank? |
||||
end |
||||
|
||||
def validate_subject_present |
||||
errors.add(:subject, :blank) if model.subject.blank? |
||||
end |
||||
|
||||
def validate_reason_present |
||||
errors.add(:reason, :blank) if model.reason.blank? |
||||
end |
||||
|
||||
def validate_channels |
||||
if model.read_ian == nil && model.read_email == nil |
||||
errors.add(:base, :at_least_one_channel) |
||||
end |
||||
|
||||
if model.read_ian |
||||
errors.add(:read_ian, :read_on_creation) |
||||
end |
||||
|
||||
if model.read_email |
||||
errors.add(:read_email, :read_on_creation) |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,46 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
module UserPreferences |
||||
class BaseContract < ::BaseContract |
||||
validate :user_allowed_to_access |
||||
|
||||
protected |
||||
|
||||
## |
||||
# User preferences can only be accessed with the manage_user permission |
||||
# or if an active, logged user is editing their own prefs |
||||
def user_allowed_to_access |
||||
unless user.allowed_to_globally?(:manage_user) || (user.logged? && user.active? && user.id == model.user_id) |
||||
errors.add :base, :error_unauthorized |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,34 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
module UserPreferences |
||||
class UpdateContract < BaseContract |
||||
end |
||||
end |
@ -0,0 +1,11 @@ |
||||
class Notification < ApplicationRecord |
||||
enum reason: { mentioned: 0, involved: 1, watched: 2, subscribed: 3 } |
||||
|
||||
belongs_to :recipient, class_name: 'User' |
||||
belongs_to :actor, class_name: 'User' |
||||
belongs_to :project |
||||
belongs_to :journal |
||||
belongs_to :resource, polymorphic: true |
||||
|
||||
scope :recipient, ->(user) { where(recipient_id: user.is_a?(User) ? user.id : user) } |
||||
end |
@ -0,0 +1,11 @@ |
||||
class NotificationSetting < ApplicationRecord |
||||
enum channel: { in_app: 0, mail: 1 } |
||||
|
||||
belongs_to :project |
||||
belongs_to :user |
||||
|
||||
include Scopes::Scoped |
||||
scopes :applicable |
||||
|
||||
validates :channel, uniqueness: { scope: %i[project user] } |
||||
end |
@ -0,0 +1,57 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
module NotificationSettings::Scopes |
||||
module Applicable |
||||
extend ActiveSupport::Concern |
||||
|
||||
class_methods do |
||||
# Return notifications settings that prevail in the selected context (project) |
||||
# If there is only the global notification setting in place, those are authoritative. |
||||
# If there is a project specific setting in place, it is the project specific setting instead. |
||||
# rubocop:disable Metrics/AbcSize |
||||
def applicable(project) |
||||
global_notifications = NotificationSetting.arel_table |
||||
project_notifications = NotificationSetting.arel_table.alias('project_settings') |
||||
|
||||
subselect = global_notifications |
||||
.where(global_notifications[:project_id].eq(nil)) |
||||
.join(project_notifications, Arel::Nodes::OuterJoin) |
||||
.on(project_notifications[:project_id].eq(project.id), |
||||
global_notifications[:user_id].eq(project_notifications[:user_id]), |
||||
global_notifications[:channel].eq(project_notifications[:channel])) |
||||
.project(global_notifications.coalesce(project_notifications[:id], global_notifications[:id])) |
||||
|
||||
where(global_notifications[:id].in(subselect)) |
||||
end |
||||
# rubocop:enable Metrics/AbcSize |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,43 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2020 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
module Queries::Notifications |
||||
Queries::Register.filter Queries::Notifications::NotificationQuery, |
||||
Queries::Notifications::Filters::ReadIanFilter |
||||
|
||||
Queries::Register.order Queries::Notifications::NotificationQuery, |
||||
Queries::Notifications::Orders::DefaultOrder |
||||
|
||||
Queries::Register.order Queries::Notifications::NotificationQuery, |
||||
Queries::Notifications::Orders::ReasonOrder |
||||
|
||||
Queries::Register.order Queries::Notifications::NotificationQuery, |
||||
Queries::Notifications::Orders::ReadIanOrder |
||||
end |
@ -0,0 +1,37 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
class Queries::Notifications::Filters::NotificationFilter < Queries::Filters::Base |
||||
self.model = Notification |
||||
|
||||
def human_name |
||||
Notification.human_attribute_name(name) |
||||
end |
||||
end |
@ -0,0 +1,37 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
class Queries::Notifications::Filters::ReadIanFilter < Queries::Notifications::Filters::NotificationFilter |
||||
include Queries::Filters::Shared::BooleanFilter |
||||
|
||||
def self.key |
||||
:read_ian |
||||
end |
||||
end |
@ -0,0 +1,39 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2020 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
class Queries::Notifications::NotificationQuery < Queries::BaseQuery |
||||
def self.model |
||||
Notification |
||||
end |
||||
|
||||
def default_scope |
||||
Notification.recipient(user) |
||||
end |
||||
end |
@ -0,0 +1,37 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
class Queries::Notifications::Orders::DefaultOrder < Queries::BaseOrder |
||||
self.model = Notification |
||||
|
||||
def self.key |
||||
/\A(id|created_at|updated_at)\z/ |
||||
end |
||||
end |
@ -0,0 +1,37 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
class Queries::Notifications::Orders::ReadIanOrder < Queries::BaseOrder |
||||
self.model = Notification |
||||
|
||||
def self.key |
||||
:read_ian |
||||
end |
||||
end |
@ -0,0 +1,37 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
class Queries::Notifications::Orders::ReasonOrder < Queries::BaseOrder |
||||
self.model = Notification |
||||
|
||||
def self.key |
||||
:reason |
||||
end |
||||
end |
@ -0,0 +1,71 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
# Returns projects visible for a user. |
||||
# This filter is only useful for admins which want to scope down the list of all the projects to those |
||||
# visible by a user. For a non admin user, the vanilla project query is already limited to the visible projects. |
||||
module Queries |
||||
module Projects |
||||
module Filters |
||||
class VisibleFilter < ::Queries::Projects::Filters::ProjectFilter |
||||
validate :validate_only_single_value |
||||
|
||||
def allowed_values |
||||
# Disregard the need for a proper name (as it is no longer actually displayed) |
||||
# in favor of speed. |
||||
@allowed_values ||= User.pluck(:id, :id) |
||||
end |
||||
|
||||
def scope |
||||
super.where(id: Project.visible(User.find(values.first))) |
||||
end |
||||
|
||||
def where |
||||
# Handled by scope |
||||
'1 = 1' |
||||
end |
||||
|
||||
def type |
||||
:list |
||||
end |
||||
|
||||
def available_operators |
||||
[::Queries::Operators::Equals] |
||||
end |
||||
|
||||
private |
||||
|
||||
def validate_only_single_value |
||||
errors.add(:values, :invalid) if values.length != 1 |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,55 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
# Return all users who want to be notified on every activity within a project. |
||||
# If there is only the global notification setting in place, that one is authoritative. |
||||
# If there is a project specific setting in place, it is the project specific setting instead. |
||||
module Users::Scopes |
||||
module NotifiedOnAll |
||||
extend ActiveSupport::Concern |
||||
|
||||
class_methods do |
||||
def notified_on_all(project) |
||||
global_settings = NotificationSetting |
||||
.where(all: true, project: nil) |
||||
project_settings_not_all = NotificationSetting |
||||
.where(project: project) |
||||
.group(:user_id) |
||||
.having('NOT bool_or("all")') |
||||
project_settings = NotificationSetting |
||||
.where(all: true, project: project) |
||||
|
||||
where(id: global_settings.select(:user_id)) |
||||
.where.not(id: project_settings_not_all.select(:user_id)) |
||||
.or(User.where(id: project_settings.select(:user_id))) |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,42 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
class Notifications::CreateService < ::BaseServices::Create |
||||
protected |
||||
|
||||
def after_perform(call) |
||||
super.tap do |result| |
||||
if result.success? |
||||
Mails::NotificationJob |
||||
.perform_later(result.result, user) |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,57 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
module Notifications |
||||
class SetAttributesService < ::BaseServices::SetAttributes |
||||
private |
||||
|
||||
def set_default_attributes(params) |
||||
super |
||||
|
||||
set_default_subject unless model.subject |
||||
set_default_context unless model.context |
||||
end |
||||
|
||||
def set_default_subject |
||||
# TODO: Work package journal specific. |
||||
# Extract into strategy per event resource |
||||
journable = model.resource.journable |
||||
|
||||
class_name = journable.class.name.underscore |
||||
|
||||
model.subject = I18n.t("notifications.#{class_name.pluralize}.subject.#{model.reason}", |
||||
**{ class_name.to_sym => journable.to_s }) |
||||
end |
||||
|
||||
def set_default_context |
||||
# TODO: Work package journal specific. |
||||
# Extract into strategy per event resource |
||||
model.context = model.resource.data.project |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,39 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
module UserPreferences |
||||
class SetAttributesService < ::BaseServices::SetAttributes |
||||
def set_attributes(params) |
||||
params.each do |k, v| |
||||
model[k] = v |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,92 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
module UserPreferences |
||||
class UpdateService < ::BaseServices::Update |
||||
protected |
||||
|
||||
attr_accessor :notifications |
||||
|
||||
def before_perform(params) |
||||
self.notifications = params&.delete(:notification_settings) |
||||
|
||||
super |
||||
end |
||||
|
||||
def after_perform(service_call) |
||||
return service_call if notifications.nil? |
||||
|
||||
inserted = persist_notifications |
||||
remove_other_notifications(inserted) |
||||
|
||||
service_call |
||||
end |
||||
|
||||
def persist_notifications |
||||
global, project = notifications |
||||
.map { |item| item.merge(user_id: model.user_id) } |
||||
.partition { |setting| setting[:project_id].nil? } |
||||
|
||||
global_ids = upsert_notifications(global, %i[user_id channel], 'project_id IS NULL') |
||||
project_ids = upsert_notifications(project, %i[user_id channel project_id], 'project_id IS NOT NULL') |
||||
|
||||
global_ids + project_ids |
||||
end |
||||
|
||||
def remove_other_notifications(ids) |
||||
NotificationSetting |
||||
.where(user_id: model.user_id) |
||||
.where.not(id: ids) |
||||
.delete_all |
||||
end |
||||
|
||||
## |
||||
# Upsert notification while respecting the partial index on notification_settings |
||||
# depending on the project presence |
||||
# |
||||
# @param notifications The array of notification hashes to upsert |
||||
# @param conflict_target The uniqueness constraint to upsert within |
||||
# @param index_predicate The partial index condition on the project |
||||
def upsert_notifications(notifications, conflict_target, index_predicate) |
||||
return [] if notifications.empty? |
||||
|
||||
NotificationSetting |
||||
.import( |
||||
notifications, |
||||
on_duplicate_key_update: { |
||||
conflict_target: conflict_target, |
||||
index_predicate: index_predicate, |
||||
columns: %i[watched involved mentioned all] |
||||
}, |
||||
validate: false |
||||
).ids |
||||
end |
||||
end |
||||
end |
@ -1,79 +0,0 @@ |
||||
<%#-- copyright |
||||
OpenProject is an open source project management software. |
||||
Copyright (C) 2012-2021 the OpenProject GmbH |
||||
|
||||
This program is free software; you can redistribute it and/or |
||||
modify it under the terms of the GNU General Public License version 3. |
||||
|
||||
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
Copyright (C) 2010-2013 the ChiliProject Team |
||||
|
||||
This program is free software; you can redistribute it and/or |
||||
modify it under the terms of the GNU General Public License |
||||
as published by the Free Software Foundation; either version 2 |
||||
of the License, or (at your option) any later version. |
||||
|
||||
This program is distributed in the hope that it will be useful, |
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
GNU General Public License for more details. |
||||
|
||||
You should have received a copy of the GNU General Public License |
||||
along with this program; if not, write to the Free Software |
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
|
||||
See docs/COPYRIGHT.rdoc for more details. |
||||
|
||||
++#%> |
||||
|
||||
<% active_sections = if @user.mail_notification == 'selected' |
||||
[{key: 'notified_projects'}] |
||||
else |
||||
[] |
||||
end %> |
||||
|
||||
<%= initialize_hide_sections_with [{key: 'notified_projects'}], active_sections %> |
||||
|
||||
|
||||
<div class="form--field"> |
||||
<%= styled_label_tag "user_mail_notification", t(:'user.settings.mail_notifications') %> |
||||
<div class="form--field-container"> |
||||
<div class="form--select-container"> |
||||
<%= styled_select_tag 'user[mail_notification]', |
||||
options_for_select(user_mail_notification_options(@user), @user.mail_notification), |
||||
container_class: '-wide' %> |
||||
<show-section-dropdown data-opt-value="selected" |
||||
data-hide-sec-with-name="notified_projects"> |
||||
</show-section-dropdown> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<section class="hide-section" data-section-name="notified_projects" <%= @user.mail_notification == 'selected' ? '' : 'hidden' %>> |
||||
<div id="notified-projects" class="form--field -no-label"> |
||||
<div class="form--field-container -vertical"> |
||||
<% @user.projects.distinct.each do |project| %> |
||||
<label class="form--label-with-check-box"> |
||||
<%= styled_check_box_tag 'notified_project_ids[]', |
||||
project.id, |
||||
@user.notified_projects_ids.include?(project.id), |
||||
id: "notified_project_ids_#{project.id}" %> |
||||
<%= project.name %> |
||||
</label> |
||||
<% end %> |
||||
</div> |
||||
<div class="form--field-instructions"> |
||||
<%= t(:'user.settings.mail_project_explanaition') %> |
||||
</div> |
||||
</div> |
||||
</section> |
||||
|
||||
<div class="form--field"> |
||||
<%= styled_label_tag 'self_notified', t(:'user.settings.mail_self_notified') %> |
||||
<div class="form--field-container"> |
||||
<div class="form--check-box-container"> |
||||
<%= styled_check_box_tag 'self_notified', 1, @user.pref.self_notified? %> |
||||
</div> |
||||
</div> |
||||
</div> |
@ -1,4 +0,0 @@ |
||||
<section class="form--section"> |
||||
<h3 class="form--section-title"><%= User.human_attribute_name(:mail_notification) %></h3> |
||||
<%= render partial: 'users/mail_notifications' %> |
||||
</section> |
@ -0,0 +1,38 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
class Mails::NotificationJob < ApplicationJob |
||||
queue_with_priority :notification |
||||
|
||||
def perform(notification, sender) |
||||
# TODO: implement properly |
||||
# move sender to database |
||||
Mails::WorkPackageJob |
||||
.perform_now(notification.resource, notification.recipient_id, sender.id) |
||||
end |
||||
end |
@ -0,0 +1,53 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
module Notifications |
||||
class CleanupJob < ::Cron::CronJob |
||||
DEFAULT_RETENTION ||= 30 |
||||
|
||||
# runs at 2:22 nightly |
||||
self.cron_expression = '22 2 * * *' |
||||
|
||||
def perform |
||||
Notification |
||||
.where('updated_at < ?', oldest_notification_retention_time) |
||||
.delete_all |
||||
end |
||||
|
||||
private |
||||
|
||||
def oldest_notification_retention_time |
||||
days_ago = Setting.notification_retention_period_days.to_i |
||||
days_ago = DEFAULT_RETENTION if days_ago <= 0 |
||||
|
||||
Time.zone.today - days_ago.days |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,19 @@ |
||||
class CreateNotifications < ActiveRecord::Migration[6.1] |
||||
def change |
||||
create_table :notifications do |t| |
||||
t.text :subject |
||||
t.boolean :read_ian, default: false, index: true |
||||
t.boolean :read_email, default: false, index: true |
||||
t.integer :reason, limit: 1 |
||||
|
||||
t.references :recipient, null: false, index: true, foreign_key: { to_table: :users } |
||||
t.references :actor, null: true, foreign_key: { to_table: :users } |
||||
|
||||
t.references :resource, polymorphic: true, null: false |
||||
t.references :project |
||||
t.references :journal, index: false |
||||
|
||||
t.timestamps |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,12 @@ |
||||
class AuthorsAsWatchers < ActiveRecord::Migration[6.1] |
||||
def up |
||||
WorkPackage |
||||
.includes(:author, :project) |
||||
.find_each do |work_package| |
||||
Watcher.create(user: work_package.author, watchable: work_package) |
||||
end |
||||
end |
||||
|
||||
# No down since we cannot distinguish between watchers that existed before |
||||
# and the ones that where created by the migration. |
||||
end |
@ -0,0 +1,123 @@ |
||||
class AddNotificationSettings < ActiveRecord::Migration[6.1] |
||||
def up |
||||
create_table :notification_settings do |t| |
||||
t.belongs_to :project, null: true, index: true, foreign_key: true |
||||
t.belongs_to :user, null: false, index: true, foreign_key: true |
||||
t.integer :channel, limit: 1 |
||||
t.boolean :watched, default: false |
||||
t.boolean :involved, default: false |
||||
t.boolean :mentioned, default: false |
||||
t.boolean :all, default: false |
||||
|
||||
t.timestamps default: -> { 'CURRENT_TIMESTAMP' } |
||||
|
||||
t.index %i[user_id channel], |
||||
unique: true, |
||||
where: "project_id IS NULL", |
||||
name: 'index_notification_settings_unique_project_null' |
||||
|
||||
t.index %i[user_id project_id channel], |
||||
unique: true, |
||||
where: "project_id IS NOT NULL", |
||||
name: 'index_notification_settings_unique_project' |
||||
end |
||||
|
||||
insert_project_specific_channel |
||||
insert_default_mail_channel |
||||
insert_default_in_app_channel |
||||
|
||||
remove_column :members, :mail_notification |
||||
end |
||||
|
||||
def down |
||||
add_column :members, :mail_notification, :boolean, default: false, null: false |
||||
add_column :users, :mail_notification, :string, default: '', null: false |
||||
|
||||
update_mail_notifications |
||||
|
||||
drop_table :notification_settings |
||||
end |
||||
|
||||
def insert_default_mail_channel |
||||
execute <<~SQL |
||||
INSERT INTO |
||||
notification_settings |
||||
(user_id, |
||||
channel, |
||||
watched, |
||||
involved, |
||||
mentioned, |
||||
"all") |
||||
SELECT |
||||
id, |
||||
1, |
||||
mail_notification = 'only_my_events', |
||||
mail_notification = 'only_my_events' OR mail_notification = 'only_assigned', |
||||
NOT mail_notification = 'all' AND NOT mail_notification = 'NONE', |
||||
mail_notification = 'all' |
||||
FROM |
||||
users |
||||
WHERE |
||||
mail_notification IS NOT NULL |
||||
SQL |
||||
end |
||||
|
||||
def insert_project_specific_channel |
||||
execute <<~SQL |
||||
INSERT INTO |
||||
notification_settings |
||||
(project_id, |
||||
user_id, |
||||
channel, |
||||
"all") |
||||
SELECT |
||||
project_id, |
||||
user_id, |
||||
1, |
||||
true |
||||
FROM |
||||
members |
||||
WHERE |
||||
mail_notification |
||||
SQL |
||||
end |
||||
|
||||
def insert_default_in_app_channel |
||||
execute <<~SQL |
||||
INSERT INTO |
||||
notification_settings |
||||
(user_id, |
||||
channel, |
||||
involved, |
||||
mentioned) |
||||
SELECT |
||||
id, |
||||
0, |
||||
true, |
||||
true |
||||
FROM |
||||
users |
||||
SQL |
||||
end |
||||
|
||||
def update_mail_notifications |
||||
# We cannot reconstruct the settings completely |
||||
execute <<~SQL |
||||
UPDATE |
||||
users |
||||
SET |
||||
mail_notification = CASE |
||||
WHEN notification_settings.all |
||||
THEN 'all' |
||||
WHEN notification_settings.watched |
||||
THEN 'only_my_events' |
||||
WHEN notification_settings.involved |
||||
THEN 'only_assigned' |
||||
ELSE 'none' |
||||
FROM |
||||
notification_settings |
||||
WHERE |
||||
notification_settings.user_id = users.id |
||||
SQL |
||||
end |
||||
end |
@ -0,0 +1,78 @@ |
||||
# Group Notifications |
||||
|
||||
## View notification [/api/v3/notifications/{id}] |
||||
|
||||
+ Model |
||||
+ Body |
||||
|
||||
{ |
||||
"_type": "Notification", |
||||
"_links": { |
||||
"self": { |
||||
"href": "/api/v3/notifications/1", |
||||
"title": "Work package abc was updated: You are now assignee" |
||||
}, |
||||
"resource": { |
||||
"href": "/api/v3/activity/1", |
||||
"title": "quis numquam qui voluptatum quia praesentium blanditiis nisi" |
||||
}, |
||||
"context": { |
||||
"href": "/api/v3/project/1", |
||||
"title": "quis numquam qui voluptatum quia praesentium blanditiis nisi" |
||||
}, |
||||
"actor": { |
||||
"href": "/api/v3/users/1", |
||||
"title": "John Sheppard" |
||||
} |
||||
}, |
||||
"id": 1, |
||||
"subject": "Work package abc was updated: You are now assignee" |
||||
"readIAN": false, |
||||
"readMail": false, |
||||
"reason": "mentioned", |
||||
"createdAt": "2014-05-21T08:51:20Z", |
||||
"updatedAt": "2014-05-21T08:51:20Z", |
||||
"_embedded:": { |
||||
"resource": { |
||||
"_type": "Activity::Comment", |
||||
"_links": { |
||||
"self": { |
||||
"href": "/api/v3/activity/1", |
||||
"title": "Priority changed from High to Low" |
||||
}, |
||||
"workPackage": { |
||||
"href": "/api/v3/work_packages/1", |
||||
"title": "quis numquam qui voluptatum quia praesentium blanditiis nisi" |
||||
}, |
||||
"user": { |
||||
"href": "/api/v3/users/1", |
||||
"title": "John Sheppard - admin" |
||||
} |
||||
}, |
||||
"id": 1, |
||||
"details": [ |
||||
{ |
||||
"format": "markdown", |
||||
"raw": "Lorem ipsum dolor sit amet.", |
||||
"html": "<p>Lorem ipsum dolor sit amet.</p>" |
||||
} |
||||
], |
||||
"comment": { |
||||
"format": "markdown", |
||||
"raw": "Lorem ipsum dolor sit amet.", |
||||
"html": "<p>Lorem ipsum dolor sit amet.</p>" |
||||
}, |
||||
"createdAt": "2014-05-21T08:51:20Z", |
||||
"version": 31 |
||||
} |
||||
} |
||||
} |
||||
|
||||
## View notification [GET] |
||||
|
||||
+ Parameters |
||||
+ id (required, integer, `1`) ... Notification id |
||||
|
||||
+ Response 200 (application/hal+json) |
||||
|
||||
[View notification][] |
@ -0,0 +1,63 @@ |
||||
//-- copyright
|
||||
// OpenProject is an open source project management software.
|
||||
// Copyright (C) 2012-2021 the OpenProject GmbH
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See docs/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
|
||||
import { APIv3GettableResource } from "core-app/core/apiv3/paths/apiv3-resource"; |
||||
import { Observable } from "rxjs"; |
||||
import { InAppNotification } from "core-app/features/in-app-notifications/store/in-app-notification.model"; |
||||
import { HttpClient } from "@angular/common/http"; |
||||
import { InjectField } from "core-app/shared/helpers/angular/inject-field.decorator"; |
||||
|
||||
export class Apiv3NotificationPaths extends APIv3GettableResource<InAppNotification> { |
||||
@InjectField() http:HttpClient; |
||||
|
||||
public markRead():Observable<unknown> { |
||||
return this |
||||
.http |
||||
.post( |
||||
this.path + '/readIAN', |
||||
{}, |
||||
{ |
||||
withCredentials: true, |
||||
responseType: 'json' |
||||
} |
||||
); |
||||
} |
||||
|
||||
public markUnread():Observable<unknown> { |
||||
return this |
||||
.http |
||||
.post( |
||||
this.path + '/unreadIAN', |
||||
{}, |
||||
{ |
||||
withCredentials: true, |
||||
responseType: 'json' |
||||
} |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,69 @@ |
||||
//-- copyright
|
||||
// OpenProject is an open source project management software.
|
||||
// Copyright (C) 2012-2021 the OpenProject GmbH
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See docs/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
|
||||
import { APIv3ResourceCollection } from "core-app/core/apiv3/paths/apiv3-resource"; |
||||
import { APIV3Service } from "core-app/core/apiv3/api-v3.service"; |
||||
import { Observable } from "rxjs"; |
||||
import { Apiv3ListParameters, listParamsString } from "core-app/core/apiv3/paths/apiv3-list-resource.interface"; |
||||
import { InAppNotification } from "core-app/features/in-app-notifications/store/in-app-notification.model"; |
||||
import { Apiv3NotificationPaths } from "core-app/core/apiv3/endpoints/notifications/apiv3-notification-paths"; |
||||
import { InjectField } from "core-app/shared/helpers/angular/inject-field.decorator"; |
||||
import { HttpClient } from "@angular/common/http"; |
||||
import { IHALCollection } from "core-app/core/apiv3/types/hal-collection.type"; |
||||
|
||||
export class Apiv3NotificationsPaths |
||||
extends APIv3ResourceCollection<InAppNotification, Apiv3NotificationPaths> { |
||||
@InjectField() http:HttpClient; |
||||
|
||||
constructor(protected apiRoot:APIV3Service, |
||||
protected basePath:string) { |
||||
super(apiRoot, basePath, 'notifications', Apiv3NotificationPaths); |
||||
} |
||||
|
||||
/** |
||||
* Load a list of events with a given list parameter filter |
||||
* @param params |
||||
*/ |
||||
public list(params?:Apiv3ListParameters):Observable<IHALCollection<InAppNotification>> { |
||||
return this |
||||
.http |
||||
.get<IHALCollection<InAppNotification>>(this.path + listParamsString(params)); |
||||
} |
||||
|
||||
/** |
||||
* Load unread events |
||||
*/ |
||||
public unread(additional?:Apiv3ListParameters):Observable<IHALCollection<InAppNotification>> { |
||||
const params:Apiv3ListParameters = { |
||||
...additional, |
||||
filters: [["readIAN", "=", false]] |
||||
}; |
||||
|
||||
return this.list(params); |
||||
} |
||||
} |
@ -0,0 +1,62 @@ |
||||
//-- copyright
|
||||
// OpenProject is an open source project management software.
|
||||
// Copyright (C) 2012-2021 the OpenProject GmbH
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See docs/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
|
||||
import { APIv3ResourcePath } from "core-app/core/apiv3/paths/apiv3-resource"; |
||||
import { Observable } from "rxjs"; |
||||
import { InjectField } from "core-app/shared/helpers/angular/inject-field.decorator"; |
||||
import { HttpClient } from "@angular/common/http"; |
||||
import { UserPreferencesModel } from "core-app/features/user-preferences/state/user-preferences.model"; |
||||
|
||||
export class Apiv3UserPreferencesPaths extends APIv3ResourcePath<UserPreferencesModel> { |
||||
@InjectField() http:HttpClient; |
||||
|
||||
|
||||
/** |
||||
* Perform a request to the backend to load preferences |
||||
*/ |
||||
public get():Observable<UserPreferencesModel> { |
||||
return this |
||||
.http |
||||
.get<UserPreferencesModel>( |
||||
this.path, |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Perform a request to update preferences |
||||
*/ |
||||
public patch(payload:Partial<UserPreferencesModel>):Observable<UserPreferencesModel> { |
||||
return this |
||||
.http |
||||
.patch<UserPreferencesModel>( |
||||
this.path, |
||||
payload, |
||||
{ withCredentials: true, responseType: 'json' } |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,10 @@ |
||||
export interface IHALCollection<T> { |
||||
_type:'Collection'; |
||||
count:number; |
||||
total:number; |
||||
pageSize:number; |
||||
offset:number; |
||||
_embedded:{ |
||||
elements:T[]; |
||||
} |
||||
} |
@ -0,0 +1,16 @@ |
||||
<button |
||||
type="button" |
||||
class="op-ian-bell op-app-menu--item-action" |
||||
(click)="openCenter($event)" |
||||
> |
||||
<op-icon icon-classes="icon-bell"> |
||||
</op-icon> |
||||
<ng-container *ngIf="(unreadCount$ | async) as unreadCount"> |
||||
<span |
||||
*ngIf="unreadCount > 0" |
||||
class="op-ian-bell--indicator" |
||||
data-qa-selector="op-ian-notifications-count" |
||||
[textContent]="unreadCount"> |
||||
</span> |
||||
</ng-container> |
||||
</button> |
@ -0,0 +1,18 @@ |
||||
@import "src/assets/sass/helpers" |
||||
|
||||
.op-ian-bell |
||||
position: relative |
||||
|
||||
&--indicator |
||||
position: absolute |
||||
top: 10px |
||||
right: 5px |
||||
display: flex |
||||
justify-content: center |
||||
align-items: center |
||||
height: 16px |
||||
width: 16px |
||||
border-radius: 0.5rem |
||||
font-size: 0.7rem |
||||
background: darkred |
||||
font-weight: bold |
@ -0,0 +1,41 @@ |
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'; |
||||
import { InAppNotificationsQuery } from "core-app/features/in-app-notifications/store/in-app-notifications.query"; |
||||
import { InAppNotificationsService } from "core-app/features/in-app-notifications/store/in-app-notifications.service"; |
||||
import { OpModalService } from "core-app/shared/components/modal/modal.service"; |
||||
import { InAppNotificationCenterComponent } from "core-app/features/in-app-notifications/center/in-app-notification-center.component"; |
||||
import { merge, timer } from "rxjs"; |
||||
import { filter, switchMap } from "rxjs/operators"; |
||||
import { ActiveWindowService } from "core-app/core/active-window/active-window.service"; |
||||
|
||||
export const opInAppNotificationBellSelector = 'op-in-app-notification-bell'; |
||||
const POLLING_INTERVAL = 10000; |
||||
|
||||
@Component({ |
||||
selector: opInAppNotificationBellSelector, |
||||
templateUrl: './in-app-notification-bell.component.html', |
||||
styleUrls: ['./in-app-notification-bell.component.sass'], |
||||
changeDetection: ChangeDetectionStrategy.OnPush, |
||||
}) |
||||
export class InAppNotificationBellComponent { |
||||
polling$ = timer(10, POLLING_INTERVAL) |
||||
.pipe( |
||||
filter(() => this.activeWindow.isActive), |
||||
switchMap(() => this.inAppService.count$()), |
||||
); |
||||
|
||||
unreadCount$ = merge( |
||||
this.polling$, |
||||
this.inAppQuery.unreadCount$ |
||||
); |
||||
|
||||
constructor(readonly inAppQuery:InAppNotificationsQuery, |
||||
readonly inAppService:InAppNotificationsService, |
||||
readonly activeWindow:ActiveWindowService, |
||||
readonly modalService:OpModalService) { |
||||
} |
||||
|
||||
openCenter(event:MouseEvent) { |
||||
this.modalService.show(InAppNotificationCenterComponent, 'global'); |
||||
event.preventDefault(); |
||||
} |
||||
} |
@ -0,0 +1,62 @@ |
||||
<div |
||||
class="op-modal op-modal_fullscreen" |
||||
> |
||||
<op-modal-header (close)="closeMe($event)">{{text.title}}</op-modal-header> |
||||
|
||||
<div class="op-modal--body op-ian-center"> |
||||
<div class="toolbar-container"> |
||||
<div class="toolbar"> |
||||
<div class="title-container"> |
||||
</div> |
||||
<ul class="toolbar-items"> |
||||
<li class="toolbar-item"> |
||||
<button |
||||
type="button" |
||||
class="button" |
||||
(click)="markAllRead()" |
||||
> |
||||
<op-icon icon-classes="button--icon icon-add"> |
||||
</op-icon> |
||||
<span |
||||
class="button--text" |
||||
[textContent]="text.mark_all_read" |
||||
> |
||||
</span> |
||||
</button> |
||||
</li> |
||||
</ul> |
||||
</div> |
||||
</div> |
||||
|
||||
<ul |
||||
*ngIf="hasUnreadItems$ | async; else noResults" |
||||
class="op-ian-center--entries" |
||||
> |
||||
<op-in-app-notification-entry |
||||
*ngFor="let item of unreadItems$ | async" |
||||
class="op-ian-center--entry" |
||||
[notification]="item" |
||||
></op-in-app-notification-entry> |
||||
</ul> |
||||
|
||||
<ng-template #noResults> |
||||
<div class="generic-table--no-results-container"> |
||||
<span |
||||
class="generic-table--no-results-title" |
||||
> |
||||
<op-icon icon-classes="icon-info1"></op-icon> |
||||
{{ text.no_results }} |
||||
</span> |
||||
</div> |
||||
</ng-template> |
||||
</div> |
||||
<div class="op-modal--footer"> |
||||
<button |
||||
type="button" |
||||
class="button" |
||||
(click)="closeMe($event)" |
||||
[textContent]="text.button_close" |
||||
[attr.title]="text.button_close" |
||||
></button> |
||||
</div> |
||||
</div> |
@ -0,0 +1,13 @@ |
||||
.op-ian-center |
||||
//grid-template-columns: 1fr 3fr |
||||
//grid-template-areas: "filters entries" |
||||
|
||||
&--filters |
||||
grid-area: filters |
||||
|
||||
&--entries |
||||
grid-area: entries |
||||
list-style: none |
||||
|
||||
&--entry |
||||
display: list-item |
@ -0,0 +1,46 @@ |
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Inject, OnInit } from '@angular/core'; |
||||
import { OpModalComponent } from "core-app/shared/components/modal/modal.component"; |
||||
import { OpModalLocalsToken } from "core-app/shared/components/modal/modal.service"; |
||||
import { OpModalLocalsMap } from "core-app/shared/components/modal/modal.types"; |
||||
import { I18nService } from "core-app/core/i18n/i18n.service"; |
||||
import { InAppNotificationsQuery } from "core-app/features/in-app-notifications/store/in-app-notifications.query"; |
||||
import { InAppNotificationsService } from "core-app/features/in-app-notifications/store/in-app-notifications.service"; |
||||
import { map } from "rxjs/operators"; |
||||
|
||||
@Component({ |
||||
selector: 'op-in-app-notification-center', |
||||
templateUrl: './in-app-notification-center.component.html', |
||||
styleUrls: ['./in-app-notification-center.component.sass'], |
||||
changeDetection: ChangeDetectionStrategy.OnPush |
||||
}) |
||||
export class InAppNotificationCenterComponent extends OpModalComponent implements OnInit { |
||||
hasUnreadItems$ = this.ianQuery.hasUnread$; |
||||
unreadItems$ = this.ianQuery.unread$; |
||||
|
||||
text = { |
||||
title: 'Notifications', |
||||
mark_all_read: 'Mark all as read', |
||||
button_close: this.I18n.t('js.button_close'), |
||||
no_results: this.I18n.t('js.notice_no_results_to_display'), |
||||
}; |
||||
|
||||
constructor( |
||||
@Inject(OpModalLocalsToken) public locals:OpModalLocalsMap, |
||||
readonly cdRef:ChangeDetectorRef, |
||||
readonly elementRef:ElementRef, |
||||
readonly I18n:I18nService, |
||||
readonly ianService:InAppNotificationsService, |
||||
readonly ianQuery:InAppNotificationsQuery, |
||||
) { |
||||
super(locals, cdRef, elementRef); |
||||
} |
||||
|
||||
ngOnInit():void { |
||||
this.ianService.get(); |
||||
} |
||||
|
||||
markAllRead() { |
||||
this.ianService.markAllRead(); |
||||
this.closeMe(); |
||||
} |
||||
} |
@ -0,0 +1,48 @@ |
||||
<div |
||||
class="op-ian-item" |
||||
attr.data-qa-selector="op-ian-notification-item-{{notification.id}}" |
||||
> |
||||
<button |
||||
type="button" |
||||
class="op-ian-item--row" |
||||
(click)="toggleDetails()" |
||||
> |
||||
<span |
||||
class="op-ian-item--indicator" |
||||
[class.op-ian-item--indicator_read]="!!notification.read" |
||||
></span> |
||||
<div class="op-ian-item--message"> |
||||
<a |
||||
*ngIf="notification._links.context" |
||||
class="op-ian-item--project" |
||||
[href]="notification._links.context.href" |
||||
[textContent]="notification._links.context.title" |
||||
target="_blank" |
||||
></a> |
||||
<span |
||||
class="op-ian-item--title" |
||||
[textContent]="notification.subject" |
||||
></span> |
||||
</div> |
||||
<div |
||||
class="op-ian-item--reason" |
||||
[textContent]="notification.reason" |
||||
></div> |
||||
<div |
||||
class="op-ian-item--date" |
||||
[textContent]="notification.date" |
||||
></div> |
||||
</button> |
||||
<div |
||||
*ngIf="expanded" |
||||
class="op-ian-item--details" |
||||
> |
||||
<ul *ngIf="notification.details"> |
||||
<li |
||||
*ngFor="let detail of notification.details" |
||||
[textContent]="detail" |
||||
> |
||||
</li> |
||||
</ul> |
||||
</div> |
||||
</div> |
@ -0,0 +1,40 @@ |
||||
@import "src/assets/sass/helpers" |
||||
|
||||
.op-ian-item |
||||
background: #F9F9F9 |
||||
margin: 10px 0 |
||||
padding: 5px 10px |
||||
font-size: 1.2rem |
||||
|
||||
&--row |
||||
@include unset-button-styles |
||||
width: 100% |
||||
display: flex |
||||
align-items: center |
||||
|
||||
&--indicator |
||||
width: 10px |
||||
height: 10px |
||||
margin-right: 0.5rem |
||||
display: block |
||||
background: var(--primary-color) |
||||
border-radius: 50% |
||||
|
||||
&_read |
||||
background: #AFAFAF |
||||
|
||||
&--message |
||||
flex: 1 |
||||
display: flex |
||||
flex-direction: column |
||||
line-height: 1.1rem |
||||
|
||||
&--project |
||||
font-size: 0.8rem |
||||
color: var(--gray-dark) |
||||
|
||||
&--reason, |
||||
&--date |
||||
width: 20% |
||||
flex-grow: 0 |
||||
text-align: right |
@ -0,0 +1,21 @@ |
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; |
||||
import { InAppNotification } from "core-app/features/in-app-notifications/store/in-app-notification.model"; |
||||
|
||||
@Component({ |
||||
selector: 'op-in-app-notification-entry', |
||||
templateUrl: './in-app-notification-entry.component.html', |
||||
styleUrls: ['./in-app-notification-entry.component.sass'], |
||||
changeDetection: ChangeDetectionStrategy.OnPush |
||||
}) |
||||
export class InAppNotificationEntryComponent { |
||||
@Input() notification:InAppNotification; |
||||
|
||||
expanded = false; |
||||
|
||||
constructor() { } |
||||
|
||||
toggleDetails() { |
||||
this.expanded = !this.expanded; |
||||
this.notification = { ...this.notification, read: true }; |
||||
} |
||||
} |
@ -0,0 +1,21 @@ |
||||
import { NgModule } from '@angular/core'; |
||||
import { CommonModule } from '@angular/common'; |
||||
import { IconModule } from "core-app/shared/components/icon/icon.module"; |
||||
import { InAppNotificationBellComponent } from "core-app/features/in-app-notifications/bell/in-app-notification-bell.component"; |
||||
import { InAppNotificationCenterComponent } from './center/in-app-notification-center.component'; |
||||
import { OpenprojectModalModule } from "core-app/shared/components/modal/modal.module"; |
||||
import { InAppNotificationEntryComponent } from "core-app/features/in-app-notifications/entry/in-app-notification-entry.component"; |
||||
|
||||
@NgModule({ |
||||
declarations: [ |
||||
InAppNotificationBellComponent, |
||||
InAppNotificationCenterComponent, |
||||
InAppNotificationEntryComponent, |
||||
], |
||||
imports: [ |
||||
CommonModule, |
||||
IconModule, |
||||
OpenprojectModalModule, |
||||
] |
||||
}) |
||||
export class OpenProjectInAppNotificationsModule { } |
@ -0,0 +1,21 @@ |
||||
import { ID } from "@datorama/akita"; |
||||
|
||||
export interface HalResourceLink { |
||||
href:string; |
||||
title:string; |
||||
} |
||||
|
||||
export interface InAppNotification { |
||||
id:ID; |
||||
subject:string; |
||||
date:string; |
||||
reason:string; |
||||
read?:boolean; |
||||
|
||||
details?:string[]; |
||||
|
||||
_links:{ |
||||
context?:HalResourceLink, |
||||
resource:HalResourceLink, |
||||
}; |
||||
} |
@ -0,0 +1,24 @@ |
||||
import { Injectable } from '@angular/core'; |
||||
import { QueryEntity } from '@datorama/akita'; |
||||
import { InAppNotificationsStore, InAppNotificationsState } from './in-app-notifications.store'; |
||||
import { map } from "rxjs/operators"; |
||||
|
||||
@Injectable({ providedIn: 'root' }) |
||||
export class InAppNotificationsQuery extends QueryEntity<InAppNotificationsState> { |
||||
|
||||
/** Get the number of unread items */ |
||||
unreadCount$ = this.select('count'); |
||||
|
||||
/** Do we have any unread items? */ |
||||
hasUnread$ = this.unreadCount$.pipe(map(count => count > 0)); |
||||
|
||||
/** Get the unread items */ |
||||
unread$ = this.selectAll({ |
||||
filterBy: ({ read }) => !read |
||||
}); |
||||
|
||||
constructor(protected store:InAppNotificationsStore) { |
||||
super(store); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,91 @@ |
||||
import { Injectable } from '@angular/core'; |
||||
import { applyTransaction, ID, transaction, withTransaction } from '@datorama/akita'; |
||||
import { InAppNotification } from './in-app-notification.model'; |
||||
import { InAppNotificationsStore } from './in-app-notifications.store'; |
||||
import { forkJoin, Observable, timer } from "rxjs"; |
||||
import { APIV3Service } from "core-app/core/apiv3/api-v3.service"; |
||||
import { map, switchMap, tap } from "rxjs/operators"; |
||||
import { NotificationsService } from "core-app/shared/components/notifications/notifications.service"; |
||||
import { InAppNotificationsQuery } from "core-app/features/in-app-notifications/store/in-app-notifications.query"; |
||||
import { take } from "rxjs/internal/operators/take"; |
||||
import apply = Reflect.apply; |
||||
|
||||
@Injectable({ providedIn: 'root' }) |
||||
export class InAppNotificationsService { |
||||
|
||||
constructor( |
||||
private store:InAppNotificationsStore, |
||||
private query:InAppNotificationsQuery, |
||||
private apiV3Service:APIV3Service, |
||||
private notifications:NotificationsService, |
||||
) { |
||||
} |
||||
|
||||
get():void { |
||||
this.store.setLoading(true); |
||||
this |
||||
.apiV3Service |
||||
.notifications |
||||
.unread() |
||||
.subscribe( |
||||
events => { |
||||
applyTransaction(() => { |
||||
this.store.set(events._embedded.elements); |
||||
this.store.update({ count: events.total }); |
||||
}); |
||||
}, |
||||
error => { |
||||
this.notifications.addError(error); |
||||
}, |
||||
) |
||||
.add( |
||||
() => this.store.setLoading(false) |
||||
); |
||||
} |
||||
|
||||
count$():Observable<number> { |
||||
return this |
||||
.apiV3Service |
||||
.notifications |
||||
.unread({ pageSize: 0 }) |
||||
.pipe( |
||||
map(events => events.total), |
||||
tap(count => this.store.update({ count })) |
||||
); |
||||
} |
||||
|
||||
@transaction() |
||||
add(inAppNotification:InAppNotification):void { |
||||
this.store.add(inAppNotification); |
||||
this.store.update(state => ({ ...state, count: state.count + 1})); |
||||
} |
||||
|
||||
update(id:ID, inAppNotification:Partial<InAppNotification>):void { |
||||
this.store.update(id, inAppNotification); |
||||
} |
||||
|
||||
@transaction() |
||||
remove(id:ID):void { |
||||
this.store.remove(id); |
||||
this.store.update(state => ({ ...state, count: state.count + 1})); |
||||
} |
||||
|
||||
markAllRead():void { |
||||
this.query |
||||
.unread$ |
||||
.pipe( |
||||
take(1), |
||||
switchMap(events => |
||||
forkJoin( |
||||
events.map(event => this.apiV3Service.notifications.id(event.id).markRead()) |
||||
) |
||||
) |
||||
) |
||||
.subscribe(() => { |
||||
applyTransaction(() => { |
||||
this.store.update(null, { read: true }); |
||||
this.store.update({ count: 0 }); |
||||
}); |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,15 @@ |
||||
import { Injectable } from '@angular/core'; |
||||
import { EntityState, EntityStore, StoreConfig } from '@datorama/akita'; |
||||
import { InAppNotification } from './in-app-notification.model'; |
||||
|
||||
export interface InAppNotificationsState extends EntityState<InAppNotification> { |
||||
count:number; |
||||
} |
||||
|
||||
@Injectable({ providedIn: 'root' }) |
||||
@StoreConfig({ name: 'in-app-notifications' }) |
||||
export class InAppNotificationsStore extends EntityStore<InAppNotificationsState> { |
||||
constructor() { |
||||
super(); |
||||
} |
||||
} |
@ -0,0 +1,23 @@ |
||||
<div class="wp-inline-create-button"> |
||||
<button |
||||
*ngIf="!active" |
||||
(click)="active = true" |
||||
class="wp-inline-create--add-link" |
||||
type="button" |
||||
> |
||||
<op-icon icon-classes="icon-context icon-add"></op-icon> |
||||
<span [textContent]="text.add_setting"></span> |
||||
</button> |
||||
|
||||
<op-autocompleter |
||||
*ngIf="active" |
||||
[placeholder]="text.please_select" |
||||
(change)="selectProject($event)" |
||||
(keydown.escape)="active = false" |
||||
[filters]="autocompleterOptions.filters" |
||||
[resource]="autocompleterOptions.resource" |
||||
[getOptionsFn]="autocompleterOptions.getOptionsFn" |
||||
appendTo="body" |
||||
> |
||||
</op-autocompleter> |
||||
</div> |
@ -0,0 +1,69 @@ |
||||
import { EventEmitter, Component, OnInit, ChangeDetectionStrategy, Output, Input } from '@angular/core'; |
||||
import { I18nService } from "core-app/core/i18n/i18n.service"; |
||||
import { Observable, of } from "rxjs"; |
||||
import { map, tap } from "rxjs/operators"; |
||||
import { APIV3Service } from "core-app/core/apiv3/api-v3.service"; |
||||
import { ApiV3FilterBuilder } from "core-app/shared/helpers/api-v3/api-v3-filter-builder"; |
||||
import { HalSourceLink } from "core-app/features/hal/resources/hal-resource"; |
||||
|
||||
export interface NotificationSettingProjectOption { |
||||
name:string; |
||||
href:string; |
||||
} |
||||
|
||||
@Component({ |
||||
selector: 'op-notification-setting-inline-create', |
||||
templateUrl: './notification-setting-inline-create.component.html', |
||||
changeDetection: ChangeDetectionStrategy.OnPush |
||||
}) |
||||
export class NotificationSettingInlineCreateComponent { |
||||
@Input() userId:string; |
||||
@Output() onSelect = new EventEmitter<HalSourceLink>(); |
||||
|
||||
/** Active inline-create mode */ |
||||
active = false; |
||||
|
||||
text = { |
||||
add_setting: this.I18n.t('js.notifications.settings.add'), |
||||
please_select: this.I18n.t('js.placeholders.selection'), |
||||
}; |
||||
|
||||
public autocompleterOptions = { |
||||
filters:[], |
||||
resource:'default', |
||||
getOptionsFn: (query:string):Observable<any[]> => this.autocomplete(query) |
||||
}; |
||||
|
||||
constructor( |
||||
private I18n:I18nService, |
||||
private apiV3Service:APIV3Service, |
||||
) { |
||||
} |
||||
|
||||
|
||||
selectProject($event:NotificationSettingProjectOption) { |
||||
this.onSelect.emit({ title: $event.name, href: $event.href }); |
||||
this.active = false; |
||||
} |
||||
|
||||
private autocomplete(term:string):Observable<NotificationSettingProjectOption[]> { |
||||
if (!term) { |
||||
return of([]); |
||||
} |
||||
|
||||
const filters = new ApiV3FilterBuilder() |
||||
.add('name_and_identifier', '~', [term]) |
||||
.add('principal', '=', [this.userId]); |
||||
|
||||
return this |
||||
.apiV3Service |
||||
.projects |
||||
.filtered(filters) |
||||
.get() |
||||
.pipe( |
||||
map((collection) => { |
||||
return collection.elements.map(project => ({ href: project.href!, name: project.name })); |
||||
}) |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,34 @@ |
||||
<op-notifications-settings-toolbar></op-notifications-settings-toolbar> |
||||
<op-notification-settings-table |
||||
*ngIf="userId" |
||||
[userId]="userId" |
||||
></op-notification-settings-table> |
||||
|
||||
<fieldset class="form--fieldset form--section"> |
||||
<legend class="form--fieldset-legend"> |
||||
{{ text.advanced_settings }} |
||||
</legend> |
||||
<op-form-field |
||||
class="op-form-field_checkbox" |
||||
[noWrapLabel]="false" |
||||
[label]="text.self_notify" |
||||
> |
||||
<input |
||||
slot="input" |
||||
type="checkbox" |
||||
[checked]="(preferences$ | async).selfNotified" |
||||
(change)="updateNotified($event.target.checked)" |
||||
/> |
||||
</op-form-field> |
||||
</fieldset> |
||||
|
||||
<div class="generic-table--action-buttons"> |
||||
<button |
||||
class="button -highlight" |
||||
[textContent]="text.save" |
||||
(click)="saveChanges()" |
||||
> |
||||
</button> |
||||
</div> |
||||
|
||||
|
@ -0,0 +1,61 @@ |
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; |
||||
import { I18nService } from "core-app/core/i18n/i18n.service"; |
||||
import { CurrentUserService } from "core-app/core/current-user/current-user.service"; |
||||
import { take } from "rxjs/internal/operators/take"; |
||||
import { UIRouterGlobals } from "@uirouter/core"; |
||||
import { UserPreferencesService } from "core-app/features/user-preferences/state/user-preferences.service"; |
||||
import { UserPreferencesStore } from "core-app/features/user-preferences/state/user-preferences.store"; |
||||
import { UserPreferencesQuery } from "core-app/features/user-preferences/state/user-preferences.query"; |
||||
|
||||
export const myNotificationsPageComponentSelector = 'op-notifications-page'; |
||||
|
||||
@Component({ |
||||
selector: myNotificationsPageComponentSelector, |
||||
templateUrl: './notifications-settings-page.component.html', |
||||
changeDetection: ChangeDetectionStrategy.OnPush, |
||||
}) |
||||
export class NotificationsSettingsPageComponent implements OnInit { |
||||
@Input() userId:string; |
||||
|
||||
preferences$ = this.query.preferences$; |
||||
|
||||
text = { |
||||
save: this.I18n.t('js.button_save'), |
||||
email: this.I18n.t('js.notifications.email'), |
||||
inApp: this.I18n.t('js.notifications.in_app'), |
||||
default_all_projects: this.I18n.t('js.notifications.settings.default_all_projects'), |
||||
advanced_settings: this.I18n.t('js.forms.advanced_settings'), |
||||
self_notify: this.I18n.t('js.notifications.settings.self_notify') |
||||
}; |
||||
|
||||
constructor( |
||||
private I18n:I18nService, |
||||
private stateService:UserPreferencesService, |
||||
private store:UserPreferencesStore, |
||||
private query:UserPreferencesQuery, |
||||
private currentUserService:CurrentUserService, |
||||
private uiRouterGlobals:UIRouterGlobals |
||||
) { |
||||
} |
||||
|
||||
ngOnInit():void { |
||||
this.userId = this.userId || this.uiRouterGlobals.params.userId; |
||||
this |
||||
.currentUserService |
||||
.user$ |
||||
.pipe(take(1)) |
||||
.subscribe(user => { |
||||
this.userId = this.userId || user.id!; |
||||
this.stateService.get(this.userId); |
||||
}); |
||||
} |
||||
|
||||
public saveChanges():void { |
||||
const prefs = this.query.getValue(); |
||||
this.stateService.update(this.userId, prefs); |
||||
} |
||||
|
||||
updateNotified(checked:boolean) { |
||||
this.store.update({ selfNotified: checked }); |
||||
} |
||||
} |
@ -0,0 +1,66 @@ |
||||
<td |
||||
*ngIf="first" |
||||
rowspan="2" |
||||
> |
||||
<span |
||||
*ngIf="setting._links.project.href; else defaultTitle" |
||||
[textContent]="setting._links.project.title" |
||||
></span> |
||||
<ng-template #defaultTitle> |
||||
<em [textContent]="text.default_all_projects"></em> |
||||
</ng-template> |
||||
</td> |
||||
<td [textContent]="text.channel(setting.channel)"> |
||||
</td> |
||||
<td> |
||||
<input |
||||
type="checkbox" |
||||
[checked]="setting.involved || setting.all" |
||||
[disabled]="setting.all" |
||||
(change)="update({ involved: $event.target.checked })" |
||||
data-qa-notification-type="involved" |
||||
[attr.aria-label]="text.involved_header" |
||||
/> |
||||
</td> |
||||
<td> |
||||
<input |
||||
type="checkbox" |
||||
[checked]="setting.mentioned || setting.all" |
||||
[disabled]="setting.all" |
||||
(change)="update({ mentioned: $event.target.checked })" |
||||
data-qa-notification-type="mentioned" |
||||
[attr.aria-label]="text.mentioned_header" |
||||
/> |
||||
</td> |
||||
<td> |
||||
<input |
||||
type="checkbox" |
||||
[checked]="setting.watched || setting.all" |
||||
[disabled]="setting.all" |
||||
(change)="update({ watched: $event.target.checked })" |
||||
data-qa-notification-type="watched" |
||||
[attr.aria-label]="text.watched_header" |
||||
/> |
||||
</td> |
||||
<td> |
||||
<input |
||||
type="checkbox" |
||||
[checked]="setting.all" |
||||
(change)="update({ all: $event.target.checked })" |
||||
data-qa-notification-type="all" |
||||
[attr.aria-label]="text.any_event_header" |
||||
/> |
||||
</td> |
||||
<td |
||||
*ngIf="first" |
||||
rowspan="2" |
||||
class="buttons" |
||||
> |
||||
<button |
||||
*ngIf="!global" |
||||
class="op-link" |
||||
(click)="remove()" |
||||
> |
||||
<op-icon icon-classes="icon-remove icon-no-color"></op-icon> |
||||
</button> |
||||
</td> |
@ -0,0 +1,67 @@ |
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; |
||||
import { I18nService } from "core-app/core/i18n/i18n.service"; |
||||
import { arrayUpdate } from "@datorama/akita"; |
||||
import { NotificationSetting } from "core-app/features/user-preferences/state/notification-setting.model"; |
||||
import { UserPreferencesStore } from "core-app/features/user-preferences/state/user-preferences.store"; |
||||
|
||||
@Component({ |
||||
selector: '[op-notification-setting-row]', |
||||
templateUrl: './notification-setting-row.component.html', |
||||
changeDetection: ChangeDetectionStrategy.OnPush |
||||
}) |
||||
export class NotificationSettingRowComponent implements OnInit { |
||||
@Input() first = false; |
||||
@Input() setting:NotificationSetting; |
||||
|
||||
/** Whether this setting is global */ |
||||
global = false; |
||||
|
||||
text = { |
||||
title: this.I18n.t('js.notifications.settings.title'), |
||||
save: this.I18n.t('js.button_save'), |
||||
email: this.I18n.t('js.notifications.email'), |
||||
inApp: this.I18n.t('js.notifications.in_app'), |
||||
remove_all: this.I18n.t('js.notifications.settings.remove_all'), |
||||
involved_header: 'I am involved', |
||||
mentioned_header: 'I was mentioned', |
||||
watched_header: 'I am watching', |
||||
any_event_header: 'All events', |
||||
default_all_projects: 'Default for all projects', |
||||
add_setting: 'Add settings for project', |
||||
channel: (channel:string):string => this.I18n.t('js.notifications.channels.' + channel) |
||||
}; |
||||
|
||||
constructor( |
||||
private I18n:I18nService, |
||||
private store:UserPreferencesStore, |
||||
) { |
||||
} |
||||
|
||||
ngOnInit() { |
||||
this.global = this.setting._links.project.href === null; |
||||
} |
||||
|
||||
remove():void { |
||||
this.store.update( |
||||
({ notifications }) => ({ |
||||
notifications: notifications.filter(notification => |
||||
notification._links.project.href !== this.setting._links.project.href) |
||||
}) |
||||
); |
||||
} |
||||
|
||||
update(delta:Partial<NotificationSetting>) { |
||||
this.store.update( |
||||
({ notifications }) => ({ |
||||
notifications: arrayUpdate( |
||||
notifications, this.matcherFn.bind(this), delta |
||||
) |
||||
}) |
||||
); |
||||
} |
||||
|
||||
private matcherFn(notification:NotificationSetting) { |
||||
return notification._links.project.href === this.setting._links.project.href && |
||||
notification.channel === this.setting.channel; |
||||
} |
||||
} |
@ -0,0 +1,76 @@ |
||||
<div class="generic-table--container form--section"> |
||||
<div class="generic-table--results-container"> |
||||
<table class="generic-table"> |
||||
<thead> |
||||
<tr> |
||||
<th> |
||||
<div class="generic-table--empty-header"></div> |
||||
</th> |
||||
<th> |
||||
<div class="generic-table--sort-header-outer"> |
||||
<div class="generic-table--sort-header"> |
||||
<span [textContent]="text.channel_header"></span> |
||||
</div> |
||||
</div> |
||||
</th> |
||||
<th> |
||||
<div class="generic-table--sort-header-outer"> |
||||
<div class="generic-table--sort-header"> |
||||
<span [textContent]="text.involved_header"></span> |
||||
</div> |
||||
</div> |
||||
</th> |
||||
<th> |
||||
<div class="generic-table--sort-header-outer"> |
||||
<div class="generic-table--sort-header"> |
||||
<span [textContent]="text.mentioned_header"></span> |
||||
</div> |
||||
</div> |
||||
</th> |
||||
<th> |
||||
<div class="generic-table--sort-header-outer"> |
||||
<div class="generic-table--sort-header"> |
||||
<span [textContent]="text.watched_header"></span> |
||||
</div> |
||||
</div> |
||||
</th> |
||||
<th> |
||||
<div class="generic-table--sort-header-outer"> |
||||
<div class="generic-table--sort-header"> |
||||
<span [textContent]="text.any_event_header"></span> |
||||
</div> |
||||
</div> |
||||
</th> |
||||
<th> |
||||
<div class="generic-table--empty-header"></div> |
||||
</th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
<ng-container *ngFor="let item of (groupedNotificationSettings$ | async) | keyvalue: projectOrder"> |
||||
<ng-container *ngFor="let setting of item.value; let first = first; let last = last"> |
||||
<tr |
||||
class="-no-highlighting" |
||||
op-notification-setting-row |
||||
[attr.data-qa-notification-project]="item.key" |
||||
[attr.data-qa-notification-channel]="setting.channel" |
||||
[first]="first" |
||||
[setting]="setting" |
||||
> |
||||
</tr> |
||||
<tr *ngIf="last" |
||||
class="op-notifications-settings-table--spacer"> |
||||
<td colspan="7"></td> |
||||
</tr> |
||||
</ng-container> |
||||
</ng-container> |
||||
</tbody> |
||||
</table> |
||||
<op-notification-setting-inline-create |
||||
*ngIf="userId" |
||||
[userId]="userId" |
||||
(onSelect)="addRow($event)" |
||||
data-qa-selector="notification-setting-inline-create" |
||||
></op-notification-setting-inline-create> |
||||
</div> |
||||
</div> |
@ -0,0 +1,6 @@ |
||||
.op-notifications-settings-table |
||||
&--spacer |
||||
|
||||
// The default table styles are a mess |
||||
td |
||||
padding: 0 !important |
@ -0,0 +1,71 @@ |
||||
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core'; |
||||
import { KeyValue } from "@angular/common"; |
||||
import { I18nService } from "core-app/core/i18n/i18n.service"; |
||||
import { UserPreferencesService } from "core-app/features/user-preferences/state/user-preferences.service"; |
||||
import { UserPreferencesStore } from "core-app/features/user-preferences/state/user-preferences.store"; |
||||
import { UserPreferencesQuery } from "core-app/features/user-preferences/state/user-preferences.query"; |
||||
import { CurrentUserService } from "core-app/core/current-user/current-user.service"; |
||||
import { UIRouterGlobals } from "@uirouter/core"; |
||||
import { HalSourceLink } from "core-app/features/hal/resources/hal-resource"; |
||||
import { |
||||
buildNotificationSetting, |
||||
NotificationSetting |
||||
} from "core-app/features/user-preferences/state/notification-setting.model"; |
||||
import { arrayAdd } from "@datorama/akita"; |
||||
|
||||
@Component({ |
||||
selector: 'op-notification-settings-table', |
||||
templateUrl: './notification-settings-table.component.html', |
||||
styleUrls: ['./notification-settings-table.component.sass'], |
||||
changeDetection: ChangeDetectionStrategy.OnPush |
||||
}) |
||||
export class NotificationSettingsTableComponent { |
||||
@Input() userId:string; |
||||
|
||||
groupedNotificationSettings$ = this.query.notificationsGroupedByProject$; |
||||
|
||||
text = { |
||||
save: this.I18n.t('js.button_save'), |
||||
email: this.I18n.t('js.notifications.email'), |
||||
inApp: this.I18n.t('js.notifications.in_app'), |
||||
involved_header: this.I18n.t('js.notifications.settings.involved'), |
||||
channel_header: this.I18n.t('js.notifications.channel'), |
||||
mentioned_header: this.I18n.t('js.notifications.settings.mentioned'), |
||||
watched_header: this.I18n.t('js.notifications.settings.watched'), |
||||
any_event_header: this.I18n.t('js.notifications.settings.all'), |
||||
default_all_projects: this.I18n.t('js.notifications.settings.default_all_projects'), |
||||
}; |
||||
|
||||
projectOrder = (a:KeyValue<string, unknown>, b:KeyValue<string, unknown>):number => { |
||||
if (a.key === 'global') { |
||||
return -1; |
||||
} |
||||
|
||||
if (b.key === 'global') { |
||||
return 1; |
||||
} |
||||
|
||||
return a.key.localeCompare(b.key); |
||||
}; |
||||
|
||||
constructor( |
||||
private I18n:I18nService, |
||||
private stateService:UserPreferencesService, |
||||
private store:UserPreferencesStore, |
||||
private query:UserPreferencesQuery |
||||
) { |
||||
} |
||||
|
||||
addRow(project:HalSourceLink) { |
||||
const added:NotificationSetting[] = [ |
||||
buildNotificationSetting(project, { channel: 'in_app' }), |
||||
buildNotificationSetting(project, { channel: 'mail' }), |
||||
]; |
||||
|
||||
this.store.update( |
||||
({ notifications }) => ({ |
||||
notifications: arrayAdd(notifications, added) |
||||
}) |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,22 @@ |
||||
<div class="toolbar-container"> |
||||
<div class="toolbar"> |
||||
<div class="title-container"> |
||||
<h2 [textContent]="text.title"></h2> |
||||
</div> |
||||
<ul class="toolbar-items"> |
||||
<li class="toolbar-item"> |
||||
<button |
||||
*ngIf="(projectSettings$ | async).length > 0" |
||||
class="button" |
||||
(click)="removeAll()" |
||||
> |
||||
<span |
||||
class="button--text" |
||||
[textContent]="text.remove_projects" |
||||
> |
||||
</span> |
||||
</button> |
||||
</li> |
||||
</ul> |
||||
</div> |
||||
</div> |
@ -0,0 +1,33 @@ |
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'; |
||||
import { UserPreferencesQuery } from "core-app/features/user-preferences/state/user-preferences.query"; |
||||
import { I18nService } from "core-app/core/i18n/i18n.service"; |
||||
import { UserPreferencesStore } from "core-app/features/user-preferences/state/user-preferences.store"; |
||||
|
||||
@Component({ |
||||
selector: 'op-notifications-settings-toolbar', |
||||
templateUrl: './notifications-settings-toolbar.component.html', |
||||
changeDetection: ChangeDetectionStrategy.OnPush |
||||
}) |
||||
export class NotificationsSettingsToolbarComponent { |
||||
projectSettings$ = this.query.projectNotifications$; |
||||
|
||||
text = { |
||||
title: this.I18n.t('js.notifications.settings.title'), |
||||
remove_projects: this.I18n.t('js.notifications.settings.remove_projects'), |
||||
}; |
||||
|
||||
constructor( |
||||
private query:UserPreferencesQuery, |
||||
private store:UserPreferencesStore, |
||||
private I18n:I18nService, |
||||
) { |
||||
} |
||||
|
||||
removeAll():void { |
||||
this.store.update( |
||||
({ notifications }) => ({ |
||||
notifications: notifications.filter(notification => notification._links.project.href === null) |
||||
}) |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,29 @@ |
||||
import { HalSourceLink } from "core-app/features/hal/resources/hal-resource"; |
||||
|
||||
export type NotificationSettingChannel = 'mail'|'in_app'; |
||||
|
||||
export interface NotificationSetting { |
||||
_links:{ project:HalSourceLink }; |
||||
channel:NotificationSettingChannel; |
||||
watched:boolean; |
||||
involved:boolean; |
||||
mentioned:boolean; |
||||
all:boolean; |
||||
} |
||||
|
||||
export function buildNotificationSetting(project:null|HalSourceLink, params:Partial<NotificationSetting>):NotificationSetting { |
||||
return { |
||||
_links: { |
||||
project: { |
||||
href: project ? project.href : null, |
||||
title: project?.title |
||||
} |
||||
}, |
||||
involved: true, |
||||
mentioned: true, |
||||
watched: false, |
||||
all: false, |
||||
channel: "in_app", |
||||
...params |
||||
}; |
||||
} |
@ -0,0 +1,11 @@ |
||||
import { NotificationSetting } from "core-app/features/user-preferences/state/notification-setting.model"; |
||||
|
||||
export interface UserPreferencesModel { |
||||
autoHidePopups:boolean; |
||||
commentSortDescending:boolean; |
||||
hideMail:boolean; |
||||
timeZone:string|null; |
||||
warnOnLeavingUnsaved:boolean; |
||||
selfNotified:boolean; |
||||
notifications:NotificationSetting[]; |
||||
} |
@ -0,0 +1,33 @@ |
||||
import { Injectable } from '@angular/core'; |
||||
|
||||
import { Query } from "@datorama/akita"; |
||||
import { map } from "rxjs/operators"; |
||||
import { Observable } from "rxjs"; |
||||
import { UserPreferencesStore } from "core-app/features/user-preferences/state/user-preferences.store"; |
||||
import { UserPreferencesModel } from "core-app/features/user-preferences/state/user-preferences.model"; |
||||
import { NotificationSetting } from "core-app/features/user-preferences/state/notification-setting.model"; |
||||
|
||||
@Injectable() |
||||
export class UserPreferencesQuery extends Query<UserPreferencesModel> { |
||||
/** All notification settings */ |
||||
notificationSettings$ = this.select('notifications'); |
||||
|
||||
/** Notification settings grouped by Project */ |
||||
notificationsGroupedByProject$:Observable<{ [key:string]:NotificationSetting[] }> = this |
||||
.notificationSettings$ |
||||
.pipe( |
||||
map(notifications => _.groupBy(notifications, setting => setting._links.project.title || 'global')) |
||||
); |
||||
|
||||
projectNotifications$ = this |
||||
.notificationSettings$ |
||||
.pipe( |
||||
map(settings => settings.filter(notification => notification._links.project.href !== null)) |
||||
); |
||||
|
||||
preferences$ = this.select(); |
||||
|
||||
constructor(protected store:UserPreferencesStore) { |
||||
super(store); |
||||
} |
||||
} |
@ -0,0 +1,57 @@ |
||||
import { HttpClient } from '@angular/common/http'; |
||||
import { Injectable } from '@angular/core'; |
||||
import { APIV3Service } from "core-app/core/apiv3/api-v3.service"; |
||||
import { NotificationsService } from "core-app/shared/components/notifications/notifications.service"; |
||||
import { Apiv3UserPreferencesPaths } from "core-app/core/apiv3/endpoints/users/apiv3-user-preferences-paths"; |
||||
import { I18nService } from "core-app/core/i18n/i18n.service"; |
||||
import { UserPreferencesModel } from "core-app/features/user-preferences/state/user-preferences.model"; |
||||
import { UserPreferencesStore } from "core-app/features/user-preferences/state/user-preferences.store"; |
||||
|
||||
@Injectable({ providedIn: 'root' }) |
||||
export class UserPreferencesService { |
||||
|
||||
constructor( |
||||
private store:UserPreferencesStore, |
||||
private http:HttpClient, |
||||
private apiV3Service:APIV3Service, |
||||
private notifications:NotificationsService, |
||||
private I18n:I18nService, |
||||
) { |
||||
} |
||||
|
||||
get(user:string):void { |
||||
this.store.setLoading(true); |
||||
this.preferenceAPI(user) |
||||
.get() |
||||
.subscribe( |
||||
prefs => this.store.update(prefs), |
||||
error => this.notifications.addError(error) |
||||
) |
||||
.add( |
||||
() => this.store.setLoading(false) |
||||
); |
||||
} |
||||
|
||||
update(user:string, delta:Partial<UserPreferencesModel>):void { |
||||
this.store.setLoading(true); |
||||
this |
||||
.preferenceAPI(user) |
||||
.patch(delta) |
||||
.subscribe( |
||||
prefs => { |
||||
this.store.update(prefs); |
||||
this.notifications.addSuccess(this.I18n.t('js.notice_successful_update')); |
||||
}, |
||||
error => this.notifications.addError(error), |
||||
) |
||||
.add(() => this.store.setLoading(false)); |
||||
} |
||||
|
||||
private preferenceAPI(user:string):Apiv3UserPreferencesPaths { |
||||
return this |
||||
.apiV3Service |
||||
.users |
||||
.id(user) |
||||
.preferences; |
||||
} |
||||
} |
@ -0,0 +1,51 @@ |
||||
//-- copyright
|
||||
// OpenProject is an open source project management software.
|
||||
// Copyright (C) 2012-2021 the OpenProject GmbH
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See docs/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
|
||||
import { Injectable } from "@angular/core"; |
||||
import { Store, StoreConfig } from '@datorama/akita'; |
||||
import { UserPreferencesModel } from "core-app/features/user-preferences/state/user-preferences.model"; |
||||
|
||||
function createInitialState():UserPreferencesModel { |
||||
return { |
||||
autoHidePopups: true, |
||||
commentSortDescending: false, |
||||
hideMail: true, |
||||
timeZone: null, |
||||
warnOnLeavingUnsaved: true, |
||||
selfNotified: false, |
||||
notifications: [] |
||||
}; |
||||
} |
||||
|
||||
@Injectable() |
||||
@StoreConfig({ name: 'notification-settings' }) |
||||
export class UserPreferencesStore extends Store<UserPreferencesModel> { |
||||
constructor() { |
||||
super(createInitialState()); |
||||
} |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue