mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-10 08:52:39 +01:00
Allow projects to be "watched", sort of a super-subscribe
Summary: Ref T4967. Adds a "Watch" relationship to projects, which is stronger than member/subscribed. Specifically, when a task is tagged with a project, we'll include all project watchers in the email/notifications. Normally we don't include projects unless they're explicitly CC'd, or have some other active role in the object (like being a reviewer or auditor). This allows you to closely follow a project without needing to write a Herald rule for every project you care about. Test Plan: - Watched/unwatched a project. - Tested the watch/subscribe/member relationships: - Watching implies subscribe. - Joining implies subscribe. - Leaving implies unsubscribe + unwatch. - You can't unsubscribe until you unwatch (slightly better would be unsubscribe implies unwatch, but this is a bit tricky). - Watched a project, then recevied email about a tagged task without otherwise being involved. Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Maniphest Tasks: T4967 Differential Revision: https://secure.phabricator.com/D9185
This commit is contained in:
parent
af0edf883d
commit
3a31554268
10 changed files with 312 additions and 23 deletions
|
@ -1962,6 +1962,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorProjectTransactionEditor' => 'applications/project/editor/PhabricatorProjectTransactionEditor.php',
|
||||
'PhabricatorProjectTransactionQuery' => 'applications/project/query/PhabricatorProjectTransactionQuery.php',
|
||||
'PhabricatorProjectUpdateController' => 'applications/project/controller/PhabricatorProjectUpdateController.php',
|
||||
'PhabricatorProjectWatchController' => 'applications/project/controller/PhabricatorProjectWatchController.php',
|
||||
'PhabricatorQuery' => 'infrastructure/query/PhabricatorQuery.php',
|
||||
'PhabricatorRecaptchaConfigOptions' => 'applications/config/option/PhabricatorRecaptchaConfigOptions.php',
|
||||
'PhabricatorRedirectController' => 'applications/base/controller/PhabricatorRedirectController.php',
|
||||
|
@ -4776,6 +4777,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
|
||||
'PhabricatorProjectTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
|
||||
'PhabricatorProjectUpdateController' => 'PhabricatorProjectController',
|
||||
'PhabricatorProjectWatchController' => 'PhabricatorProjectController',
|
||||
'PhabricatorRecaptchaConfigOptions' => 'PhabricatorApplicationConfigOptions',
|
||||
'PhabricatorRedirectController' => 'PhabricatorController',
|
||||
'PhabricatorRefreshCSRFController' => 'PhabricatorAuthController',
|
||||
|
|
|
@ -333,6 +333,10 @@ final class ManiphestTransactionEditor
|
|||
$phids[] = $phid;
|
||||
}
|
||||
|
||||
foreach (parent::getMailCC($object) as $phid) {
|
||||
$phids[] = $phid;
|
||||
}
|
||||
|
||||
foreach ($this->heraldEmailPHIDs as $phid) {
|
||||
$phids[] = $phid;
|
||||
}
|
||||
|
|
|
@ -62,6 +62,8 @@ final class PhabricatorApplicationProject extends PhabricatorApplication {
|
|||
'update/(?P<id>[1-9]\d*)/(?P<action>[^/]+)/'
|
||||
=> 'PhabricatorProjectUpdateController',
|
||||
'history/(?P<id>[1-9]\d*)/' => 'PhabricatorProjectHistoryController',
|
||||
'(?P<action>watch|unwatch)/(?P<id>[1-9]\d*)/'
|
||||
=> 'PhabricatorProjectWatchController',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ final class PhabricatorProjectProfileController
|
|||
->setViewer($user)
|
||||
->withIDs(array($this->id))
|
||||
->needMembers(true)
|
||||
->needWatchers(true)
|
||||
->needImages(true)
|
||||
->executeOne();
|
||||
if (!$project) {
|
||||
|
@ -222,14 +223,32 @@ final class PhabricatorProjectProfileController
|
|||
->setIcon('fa-plus')
|
||||
->setDisabled(!$can_join)
|
||||
->setName(pht('Join Project'));
|
||||
$view->addAction($action);
|
||||
} else {
|
||||
$action = id(new PhabricatorActionView())
|
||||
->setWorkflow(true)
|
||||
->setHref('/project/update/'.$project->getID().'/leave/')
|
||||
->setIcon('fa-times')
|
||||
->setName(pht('Leave Project...'));
|
||||
$view->addAction($action);
|
||||
|
||||
if (!$project->isUserWatcher($viewer->getPHID())) {
|
||||
$action = id(new PhabricatorActionView())
|
||||
->setWorkflow(true)
|
||||
->setHref('/project/watch/'.$project->getID().'/')
|
||||
->setIcon('fa-eye')
|
||||
->setName(pht('Watch Project'));
|
||||
$view->addAction($action);
|
||||
} else {
|
||||
$action = id(new PhabricatorActionView())
|
||||
->setWorkflow(true)
|
||||
->setHref('/project/unwatch/'.$project->getID().'/')
|
||||
->setIcon('fa-eye-slash')
|
||||
->setName(pht('Unwatch Project'));
|
||||
$view->addAction($action);
|
||||
}
|
||||
}
|
||||
$view->addAction($action);
|
||||
|
||||
|
||||
return $view;
|
||||
}
|
||||
|
@ -240,7 +259,10 @@ final class PhabricatorProjectProfileController
|
|||
$request = $this->getRequest();
|
||||
$viewer = $request->getUser();
|
||||
|
||||
$this->loadHandles($project->getMemberPHIDs());
|
||||
$this->loadHandles(
|
||||
array_merge(
|
||||
$project->getMemberPHIDs(),
|
||||
$project->getWatcherPHIDs()));
|
||||
|
||||
$view = id(new PHUIPropertyListView())
|
||||
->setUser($viewer)
|
||||
|
@ -250,8 +272,14 @@ final class PhabricatorProjectProfileController
|
|||
$view->addProperty(
|
||||
pht('Members'),
|
||||
$project->getMemberPHIDs()
|
||||
? $this->renderHandlesForPHIDs($project->getMemberPHIDs(), ',')
|
||||
: phutil_tag('em', array(), pht('None')));
|
||||
? $this->renderHandlesForPHIDs($project->getMemberPHIDs(), ',')
|
||||
: phutil_tag('em', array(), pht('None')));
|
||||
|
||||
$view->addProperty(
|
||||
pht('Watchers'),
|
||||
$project->getWatcherPHIDs()
|
||||
? $this->renderHandlesForPHIDs($project->getWatcherPHIDs(), ',')
|
||||
: phutil_tag('em', array(), pht('None')));
|
||||
|
||||
$field_list = PhabricatorCustomField::getObjectFields(
|
||||
$project,
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectWatchController
|
||||
extends PhabricatorProjectController {
|
||||
|
||||
private $id;
|
||||
private $action;
|
||||
|
||||
public function willProcessRequest(array $data) {
|
||||
$this->id = $data['id'];
|
||||
$this->action = $data['action'];
|
||||
}
|
||||
|
||||
public function processRequest() {
|
||||
$request = $this->getRequest();
|
||||
$viewer = $request->getUser();
|
||||
|
||||
$project = id(new PhabricatorProjectQuery())
|
||||
->setViewer($viewer)
|
||||
->withIDs(array($this->id))
|
||||
->needMembers(true)
|
||||
->needWatchers(true)
|
||||
->executeOne();
|
||||
if (!$project) {
|
||||
return new Aphront404Response();
|
||||
}
|
||||
|
||||
$project_uri = '/project/view/'.$project->getID().'/';
|
||||
|
||||
// You must be a member of a project to
|
||||
if (!$project->isUserMember($viewer->getPHID())) {
|
||||
return new Aphront400Response();
|
||||
}
|
||||
|
||||
if ($request->isDialogFormPost()) {
|
||||
$edge_action = null;
|
||||
switch ($this->action) {
|
||||
case 'watch':
|
||||
$edge_action = '+';
|
||||
$force_subscribe = true;
|
||||
break;
|
||||
case 'unwatch':
|
||||
$edge_action = '-';
|
||||
$force_subscribe = false;
|
||||
break;
|
||||
}
|
||||
|
||||
$type_member = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_WATCHER;
|
||||
$member_spec = array(
|
||||
$edge_action => array($viewer->getPHID() => $viewer->getPHID()),
|
||||
);
|
||||
|
||||
$xactions = array();
|
||||
$xactions[] = id(new PhabricatorProjectTransaction())
|
||||
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
|
||||
->setMetadataValue('edge:type', $type_member)
|
||||
->setNewValue($member_spec);
|
||||
|
||||
$editor = id(new PhabricatorProjectTransactionEditor($project))
|
||||
->setActor($viewer)
|
||||
->setContentSourceFromRequest($request)
|
||||
->setContinueOnNoEffect(true)
|
||||
->setContinueOnMissingFields(true)
|
||||
->applyTransactions($project, $xactions);
|
||||
|
||||
return id(new AphrontRedirectResponse())->setURI($project_uri);
|
||||
}
|
||||
|
||||
$dialog = null;
|
||||
switch ($this->action) {
|
||||
case 'watch':
|
||||
$title = pht('Watch Project?');
|
||||
$body = pht(
|
||||
'Watching a project will let you monitor it closely. You will '.
|
||||
'receive email and notifications about changes to every object '.
|
||||
'associated with projects you watch.');
|
||||
$submit = pht('Watch Project');
|
||||
break;
|
||||
case 'unwatch':
|
||||
$title = pht('Unwatch Project?');
|
||||
$body = pht(
|
||||
'You will no longer receive email or notifications about every '.
|
||||
'object associated with this project.');
|
||||
$submit = pht('Unwatch Project');
|
||||
break;
|
||||
default:
|
||||
return new Aphront404Response();
|
||||
}
|
||||
|
||||
return $this->newDialog()
|
||||
->setTitle($title)
|
||||
->appendParagraph($body)
|
||||
->addCancelButton($project_uri)
|
||||
->addSubmitButton($submit);
|
||||
}
|
||||
|
||||
}
|
|
@ -125,14 +125,24 @@ final class PhabricatorProjectTransactionEditor
|
|||
case PhabricatorProjectTransaction::TYPE_IMAGE:
|
||||
return;
|
||||
case PhabricatorTransactions::TYPE_EDGE:
|
||||
switch ($xaction->getMetadataValue('edge:type')) {
|
||||
$edge_type = $xaction->getMetadataValue('edge:type');
|
||||
switch ($edge_type) {
|
||||
case PhabricatorEdgeConfig::TYPE_PROJ_MEMBER:
|
||||
// When project members are added or removed, add or remove their
|
||||
// subscriptions.
|
||||
case PhabricatorEdgeConfig::TYPE_OBJECT_HAS_WATCHER:
|
||||
$old = $xaction->getOldValue();
|
||||
$new = $xaction->getNewValue();
|
||||
|
||||
// When adding members or watchers, we add subscriptions.
|
||||
$add = array_keys(array_diff_key($new, $old));
|
||||
$rem = array_keys(array_diff_key($old, $new));
|
||||
|
||||
// When removing members, we remove their subscription too.
|
||||
// When unwatching, we leave subscriptions, since it's fine to be
|
||||
// subscribed to a project but not be a member of it.
|
||||
if ($edge_type == PhabricatorEdgeConfig::TYPE_PROJ_MEMBER) {
|
||||
$rem = array_keys(array_diff_key($old, $new));
|
||||
} else {
|
||||
$rem = array();
|
||||
}
|
||||
|
||||
// NOTE: The subscribe is "explicit" because there's no implicit
|
||||
// unsubscribe, so Join -> Leave -> Join doesn't resubscribe you
|
||||
|
@ -142,12 +152,28 @@ final class PhabricatorProjectTransactionEditor
|
|||
// this, which is a fairly weird edge case and pretty arguable both
|
||||
// ways.
|
||||
|
||||
// Subscriptions caused by watches should also clearly be explicit,
|
||||
// and that case is unambiguous.
|
||||
|
||||
id(new PhabricatorSubscriptionsEditor())
|
||||
->setActor($this->requireActor())
|
||||
->setObject($object)
|
||||
->subscribeExplicit($add)
|
||||
->unsubscribe($rem)
|
||||
->save();
|
||||
|
||||
if ($rem) {
|
||||
// When removing members, also remove any watches on the project.
|
||||
$edge_editor = id(new PhabricatorEdgeEditor())
|
||||
->setSuppressEvents(true);
|
||||
foreach ($rem as $rem_phid) {
|
||||
$edge_editor->removeEdge(
|
||||
$object->getPHID(),
|
||||
PhabricatorEdgeConfig::TYPE_OBJECT_HAS_WATCHER,
|
||||
$rem_phid);
|
||||
}
|
||||
$edge_editor->save();
|
||||
}
|
||||
break;
|
||||
}
|
||||
return;
|
||||
|
|
|
@ -17,6 +17,7 @@ final class PhabricatorProjectQuery
|
|||
const STATUS_ARCHIVED = 'status-archived';
|
||||
|
||||
private $needMembers;
|
||||
private $needWatchers;
|
||||
private $needImages;
|
||||
|
||||
public function withIDs(array $ids) {
|
||||
|
@ -54,6 +55,11 @@ final class PhabricatorProjectQuery
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function needWatchers($need_watchers) {
|
||||
$this->needWatchers = $need_watchers;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function needImages($need_images) {
|
||||
$this->needImages = $need_images;
|
||||
return $this;
|
||||
|
@ -100,19 +106,14 @@ final class PhabricatorProjectQuery
|
|||
|
||||
if ($projects) {
|
||||
$viewer_phid = $this->getViewer()->getPHID();
|
||||
$project_phids = mpull($projects, 'getPHID');
|
||||
|
||||
$member_type = PhabricatorEdgeConfig::TYPE_PROJ_MEMBER;
|
||||
$watcher_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_WATCHER;
|
||||
|
||||
$need_edge_types = array();
|
||||
if ($this->needMembers) {
|
||||
$etype = PhabricatorEdgeConfig::TYPE_PROJ_MEMBER;
|
||||
$members = id(new PhabricatorEdgeQuery())
|
||||
->withSourcePHIDs(mpull($projects, 'getPHID'))
|
||||
->withEdgeTypes(array($etype))
|
||||
->execute();
|
||||
foreach ($projects as $project) {
|
||||
$phid = $project->getPHID();
|
||||
$project->attachMemberPHIDs(array_keys($members[$phid][$etype]));
|
||||
$project->setIsUserMember(
|
||||
$viewer_phid,
|
||||
isset($members[$phid][$etype][$viewer_phid]));
|
||||
}
|
||||
$need_edge_types[] = $member_type;
|
||||
} else {
|
||||
foreach ($data as $row) {
|
||||
$projects[$row['id']]->setIsUserMember(
|
||||
|
@ -120,6 +121,39 @@ final class PhabricatorProjectQuery
|
|||
($row['viewerIsMember'] !== null));
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->needWatchers) {
|
||||
$need_edge_types[] = $watcher_type;
|
||||
}
|
||||
|
||||
if ($need_edge_types) {
|
||||
$edges = id(new PhabricatorEdgeQuery())
|
||||
->withSourcePHIDs($project_phids)
|
||||
->withEdgeTypes($need_edge_types)
|
||||
->execute();
|
||||
|
||||
if ($this->needMembers) {
|
||||
foreach ($projects as $project) {
|
||||
$phid = $project->getPHID();
|
||||
$project->attachMemberPHIDs(
|
||||
array_keys($edges[$phid][$member_type]));
|
||||
$project->setIsUserMember(
|
||||
$viewer_phid,
|
||||
isset($edges[$phid][$member_type][$viewer_phid]));
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->needWatchers) {
|
||||
foreach ($projects as $project) {
|
||||
$phid = $project->getPHID();
|
||||
$project->attachWatcherPHIDs(
|
||||
array_keys($edges[$phid][$watcher_type]));
|
||||
$project->setIsUserWatcher(
|
||||
$viewer_phid,
|
||||
isset($edges[$phid][$watcher_type][$viewer_phid]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $projects;
|
||||
|
|
|
@ -19,6 +19,8 @@ final class PhabricatorProject extends PhabricatorProjectDAO
|
|||
protected $joinPolicy;
|
||||
|
||||
private $memberPHIDs = self::ATTACHABLE;
|
||||
private $watcherPHIDs = self::ATTACHABLE;
|
||||
private $sparseWatchers = self::ATTACHABLE;
|
||||
private $sparseMembers = self::ATTACHABLE;
|
||||
private $customFields = self::ATTACHABLE;
|
||||
private $profileImageFile = self::ATTACHABLE;
|
||||
|
@ -159,6 +161,32 @@ final class PhabricatorProject extends PhabricatorProjectDAO
|
|||
}
|
||||
|
||||
|
||||
public function isUserWatcher($user_phid) {
|
||||
if ($this->watcherPHIDs !== self::ATTACHABLE) {
|
||||
return in_array($user_phid, $this->watcherPHIDs);
|
||||
}
|
||||
return $this->assertAttachedKey($this->sparseWatchers, $user_phid);
|
||||
}
|
||||
|
||||
public function setIsUserWatcher($user_phid, $is_watcher) {
|
||||
if ($this->sparseWatchers === self::ATTACHABLE) {
|
||||
$this->sparseWatchers = array();
|
||||
}
|
||||
$this->sparseWatchers[$user_phid] = $is_watcher;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function attachWatcherPHIDs(array $phids) {
|
||||
$this->watcherPHIDs = $phids;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getWatcherPHIDs() {
|
||||
return $this->assertAttached($this->watcherPHIDs);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* -( PhabricatorSubscribableInterface )----------------------------------- */
|
||||
|
||||
|
||||
|
@ -171,7 +199,8 @@ final class PhabricatorProject extends PhabricatorProjectDAO
|
|||
}
|
||||
|
||||
public function shouldAllowSubscription($phid) {
|
||||
return $this->isUserMember($phid);
|
||||
return $this->isUserMember($phid) &&
|
||||
!$this->isUserWatcher($phid);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1891,10 +1891,65 @@ abstract class PhabricatorApplicationTransactionEditor
|
|||
* @task mail
|
||||
*/
|
||||
protected function getMailCC(PhabricatorLiskDAO $object) {
|
||||
$phids = array();
|
||||
$has_support = false;
|
||||
|
||||
if ($object instanceof PhabricatorSubscribableInterface) {
|
||||
return $this->subscribers;
|
||||
$phids[] = $this->subscribers;
|
||||
$has_support = true;
|
||||
}
|
||||
throw new Exception("Capability not supported.");
|
||||
|
||||
// TODO: This should be some interface which specifies that the object
|
||||
// has project associations.
|
||||
if ($object instanceof ManiphestTask) {
|
||||
|
||||
// TODO: This is what normal objects would do, but Maniphest is still
|
||||
// behind the times.
|
||||
if (false) {
|
||||
$project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
|
||||
$object->getPHID(),
|
||||
PhabricatorEdgeConfig::TYPE_OBJECT_HAS_PROJECT);
|
||||
} else {
|
||||
$project_phids = $object->getProjectPHIDs();
|
||||
}
|
||||
|
||||
if ($project_phids) {
|
||||
$watcher_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_WATCHER;
|
||||
|
||||
$query = id(new PhabricatorEdgeQuery())
|
||||
->withSourcePHIDs($project_phids)
|
||||
->withEdgeTypes(array($watcher_type));
|
||||
$query->execute();
|
||||
|
||||
$watcher_phids = $query->getDestinationPHIDs();
|
||||
|
||||
// We need to do a visibility check for all the watchers, as
|
||||
// watching a project is not a guarantee that you can see objects
|
||||
// associated with it.
|
||||
$users = id(new PhabricatorPeopleQuery())
|
||||
->setViewer($this->requireActor())
|
||||
->withPHIDs($watcher_phids)
|
||||
->execute();
|
||||
|
||||
foreach ($users as $user) {
|
||||
$can_see = PhabricatorPolicyFilter::hasCapability(
|
||||
$user,
|
||||
$object,
|
||||
PhabricatorPolicyCapability::CAN_VIEW);
|
||||
if ($can_see) {
|
||||
$phids[] = $user->getPHID();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$has_support = true;
|
||||
}
|
||||
|
||||
if (!$has_support) {
|
||||
throw new Exception('Capability not supported.');
|
||||
}
|
||||
|
||||
return array_mergev($phids);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -72,6 +72,9 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants {
|
|||
const TYPE_DASHBOARD_HAS_PANEL = 45;
|
||||
const TYPE_PANEL_HAS_DASHBOARD = 46;
|
||||
|
||||
const TYPE_OBJECT_HAS_WATCHER = 47;
|
||||
const TYPE_WATCHER_HAS_OBJECT = 48;
|
||||
|
||||
const TYPE_TEST_NO_CYCLE = 9000;
|
||||
|
||||
const TYPE_PHOB_HAS_ASANATASK = 80001;
|
||||
|
@ -159,6 +162,9 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants {
|
|||
|
||||
self::TYPE_PANEL_HAS_DASHBOARD => self::TYPE_DASHBOARD_HAS_PANEL,
|
||||
self::TYPE_DASHBOARD_HAS_PANEL => self::TYPE_PANEL_HAS_DASHBOARD,
|
||||
|
||||
self::TYPE_OBJECT_HAS_WATCHER => self::TYPE_WATCHER_HAS_OBJECT,
|
||||
self::TYPE_WATCHER_HAS_OBJECT => self::TYPE_OBJECT_HAS_WATCHER
|
||||
);
|
||||
|
||||
return idx($map, $edge_type);
|
||||
|
@ -343,6 +349,8 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants {
|
|||
return '%s added %d panel(s): %s.';
|
||||
case self::TYPE_PANEL_HAS_DASHBOARD:
|
||||
return '%s added %d dashboard(s): %s.';
|
||||
case self::TYPE_OBJECT_HAS_WATCHER:
|
||||
return '%s added %d watcher(s): %s.';
|
||||
case self::TYPE_SUBSCRIBED_TO_OBJECT:
|
||||
case self::TYPE_UNSUBSCRIBED_FROM_OBJECT:
|
||||
case self::TYPE_FILE_HAS_OBJECT:
|
||||
|
@ -418,6 +426,8 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants {
|
|||
return '%s removed %d panel(s): %s.';
|
||||
case self::TYPE_PANEL_HAS_DASHBOARD:
|
||||
return '%s removed %d dashboard(s): %s.';
|
||||
case self::TYPE_OBJECT_HAS_WATCHER:
|
||||
return '%s removed %d watcher(s): %s.';
|
||||
case self::TYPE_SUBSCRIBED_TO_OBJECT:
|
||||
case self::TYPE_UNSUBSCRIBED_FROM_OBJECT:
|
||||
case self::TYPE_FILE_HAS_OBJECT:
|
||||
|
@ -491,6 +501,8 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants {
|
|||
return '%s updated panels for %s.';
|
||||
case self::TYPE_PANEL_HAS_DASHBOARD:
|
||||
return '%s updated dashboards for %s.';
|
||||
case self::TYPE_OBJECT_HAS_WATCHER:
|
||||
return '%s updated watchers for %s.';
|
||||
case self::TYPE_SUBSCRIBED_TO_OBJECT:
|
||||
case self::TYPE_UNSUBSCRIBED_FROM_OBJECT:
|
||||
case self::TYPE_FILE_HAS_OBJECT:
|
||||
|
|
Loading…
Reference in a new issue