You've already forked oncall
mirror of
https://github.com/linkedin/oncall.git
synced 2025-11-26 23:10:47 +02:00
Add team pinning
Allow users to pin teams to landing page. Persist data in db to keep view consistent across machines
This commit is contained in:
committed by
Qingping Hou
parent
6fb39e2105
commit
05e815a7a7
@@ -29,6 +29,26 @@ CREATE TABLE IF NOT EXISTS `user` (
|
|||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE INDEX `username_unique` (`name` ASC));
|
UNIQUE INDEX `username_unique` (`name` ASC));
|
||||||
|
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- Table `pinned_team`
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS `pinned_team` (
|
||||||
|
`team_id` BIGINT(20) UNSIGNED NOT NULL,
|
||||||
|
`user_id` BIGINT(20) UNSIGNED NOT NULL,
|
||||||
|
INDEX `team_member_team_id_idx` (`team_id` ASC),
|
||||||
|
INDEX `team_member_user_id_idx` (`user_id` ASC),
|
||||||
|
PRIMARY KEY (`team_id`, `user_id`),
|
||||||
|
CONSTRAINT `pinned_team_team_id_fk`
|
||||||
|
FOREIGN KEY (`team_id`)
|
||||||
|
REFERENCES `team` (`id`)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT `pinned_team_user_id_fk`
|
||||||
|
FOREIGN KEY (`user_id`)
|
||||||
|
REFERENCES `user` (`id`)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
ON UPDATE CASCADE);
|
||||||
|
|
||||||
-- -----------------------------------------------------
|
-- -----------------------------------------------------
|
||||||
-- Table `team_user`
|
-- Table `team_user`
|
||||||
-- -----------------------------------------------------
|
-- -----------------------------------------------------
|
||||||
|
|||||||
65
e2e/test_pin.py
Normal file
65
e2e/test_pin.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import requests
|
||||||
|
from testutils import prefix, api_v0
|
||||||
|
|
||||||
|
|
||||||
|
@prefix('test_v0_pin_team')
|
||||||
|
def test_v0_pin_team(user, team):
|
||||||
|
user_name = user.create()
|
||||||
|
team_name = team.create()
|
||||||
|
team_name_2 = team.create()
|
||||||
|
|
||||||
|
# Test pinning teams
|
||||||
|
re = requests.post(api_v0('users/%s/pinned_teams' % user_name),
|
||||||
|
json={'team': team_name})
|
||||||
|
assert re.status_code == 201
|
||||||
|
re = requests.post(api_v0('users/%s/pinned_teams' % user_name),
|
||||||
|
json={'team': team_name_2})
|
||||||
|
assert re.status_code == 201
|
||||||
|
|
||||||
|
# Test getting pinned teams
|
||||||
|
re = requests.get(api_v0('users/%s/pinned_teams' % user_name))
|
||||||
|
assert re.status_code == 200
|
||||||
|
data = re.json()
|
||||||
|
assert len(data) == 2
|
||||||
|
assert team_name in data
|
||||||
|
assert team_name_2 in data
|
||||||
|
|
||||||
|
# Test deleting pinned teams
|
||||||
|
re = requests.delete(api_v0('users/%s/pinned_teams/%s' % (user_name, team_name)))
|
||||||
|
assert re.status_code == 200
|
||||||
|
|
||||||
|
re = requests.get(api_v0('users/%s/pinned_teams' % user_name))
|
||||||
|
assert re.status_code == 200
|
||||||
|
data = re.json()
|
||||||
|
assert len(data) == 1
|
||||||
|
assert team_name not in data
|
||||||
|
|
||||||
|
|
||||||
|
@prefix('test_v0_pin_invalid')
|
||||||
|
def test_api_v0_pin_invalid(user, team):
|
||||||
|
user_name = user.create()
|
||||||
|
team_name = team.create()
|
||||||
|
|
||||||
|
# Test pinning duplicate team
|
||||||
|
re = requests.post(api_v0('users/%s/pinned_teams' % user_name),
|
||||||
|
json={'team': team_name})
|
||||||
|
assert re.status_code == 201
|
||||||
|
re = requests.post(api_v0('users/%s/pinned_teams' % user_name),
|
||||||
|
json={'team': team_name})
|
||||||
|
assert re.status_code == 400
|
||||||
|
|
||||||
|
# Test pinning nonexistent team
|
||||||
|
re = requests.post(api_v0('users/%s/pinned_teams' % user_name),
|
||||||
|
json={'team': 'nonexistent-team-foobar'})
|
||||||
|
assert re.status_code == 422
|
||||||
|
|
||||||
|
# Test pinning team for nonexistent user
|
||||||
|
re = requests.post(api_v0('users/%s/pinned_teams' % 'nonexistent-user-foobar'),
|
||||||
|
json={'team': team_name})
|
||||||
|
assert re.status_code == 422
|
||||||
|
|
||||||
|
# Test deleting unpinned team
|
||||||
|
re = requests.delete(api_v0('users/%s/pinned_teams/%s' % (user_name, team_name)))
|
||||||
|
assert re.status_code == 200
|
||||||
|
re = requests.delete(api_v0('users/%s/pinned_teams/%s' % (user_name, team_name)))
|
||||||
|
assert re.status_code == 404
|
||||||
@@ -72,6 +72,10 @@ def init(application, config):
|
|||||||
from . import upcoming_shifts
|
from . import upcoming_shifts
|
||||||
application.add_route('/api/v0/users/{user_name}/upcoming', upcoming_shifts)
|
application.add_route('/api/v0/users/{user_name}/upcoming', upcoming_shifts)
|
||||||
|
|
||||||
|
from . import user_pinned_teams, user_pinned_team
|
||||||
|
application.add_route('/api/v0/users/{user_name}/pinned_teams', user_pinned_teams)
|
||||||
|
application.add_route('/api/v0/users/{user_name}/pinned_teams/{team_name}', user_pinned_team)
|
||||||
|
|
||||||
# Optional Iris integration
|
# Optional Iris integration
|
||||||
from . import iris_settings
|
from . import iris_settings
|
||||||
application.add_route('/api/v0/iris_settings', iris_settings)
|
application.add_route('/api/v0/iris_settings', iris_settings)
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ def on_put(req, resp, event_id):
|
|||||||
def on_delete(req, resp, event_id):
|
def on_delete(req, resp, event_id):
|
||||||
"""
|
"""
|
||||||
Delete an event by id, anyone on the team can delete that team's events
|
Delete an event by id, anyone on the team can delete that team's events
|
||||||
|
|
||||||
**Example request:**
|
**Example request:**
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
|
|||||||
34
src/oncall/api/v0/user_pinned_team.py
Normal file
34
src/oncall/api/v0/user_pinned_team.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from ... import db
|
||||||
|
from ...auth import login_required, check_user_auth
|
||||||
|
from falcon import HTTPNotFound
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def on_delete(req, resp, user_name, team_name):
|
||||||
|
'''
|
||||||
|
Delete a pinned team
|
||||||
|
|
||||||
|
**Example request:**
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
DELETE /api/v0/users/jdoe/pinned_teams/team-foo HTTP/1.1
|
||||||
|
|
||||||
|
:statuscode 200: Successful delete
|
||||||
|
:statuscode 403: Delete not allowed; logged in user does not match user_name
|
||||||
|
:statuscode 404: Team not found in user's pinned teams
|
||||||
|
'''
|
||||||
|
check_user_auth(user_name, req)
|
||||||
|
connection = db.connect()
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute('''DELETE FROM `pinned_team`
|
||||||
|
WHERE `user_id` = (SELECT `id` FROM `user` WHERE `name` = %s)
|
||||||
|
AND `team_id` = (SELECT `id` FROM `team` WHERE `name` = %s)''',
|
||||||
|
(user_name, team_name))
|
||||||
|
deleted = cursor.rowcount
|
||||||
|
connection.commit()
|
||||||
|
cursor.close()
|
||||||
|
connection.close()
|
||||||
|
if deleted == 0:
|
||||||
|
raise HTTPNotFound()
|
||||||
|
|
||||||
89
src/oncall/api/v0/user_pinned_teams.py
Normal file
89
src/oncall/api/v0/user_pinned_teams.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
from ... import db
|
||||||
|
from ...utils import load_json_body
|
||||||
|
from ...auth import login_required, check_user_auth
|
||||||
|
from ujson import dumps as json_dumps
|
||||||
|
from falcon import HTTPBadRequest, HTTP_201, HTTPError
|
||||||
|
|
||||||
|
|
||||||
|
def on_get(req, resp, user_name):
|
||||||
|
'''
|
||||||
|
Get all pinned team names for a user
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v0/users/jdoe/pinned_teams HTTP/1.1
|
||||||
|
Host: example.com
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
[
|
||||||
|
"team-foo"
|
||||||
|
]
|
||||||
|
'''
|
||||||
|
connection = db.connect()
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute('''SELECT `team`.`name`
|
||||||
|
FROM `pinned_team` JOIN `team` ON `pinned_team`.`team_id` = `team`.`id`
|
||||||
|
WHERE `pinned_team`.`user_id` = (SELECT `id` FROM `user` WHERE `name` = %s)''',
|
||||||
|
user_name)
|
||||||
|
teams = [r[0] for r in cursor]
|
||||||
|
cursor.close()
|
||||||
|
connection.close()
|
||||||
|
resp.body = json_dumps(teams)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def on_post(req, resp, user_name):
|
||||||
|
'''
|
||||||
|
Pin a team to the landing page for a user
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v0/users/jdoe/pinned_teams HTTP/1.1
|
||||||
|
Host: example.com
|
||||||
|
|
||||||
|
{
|
||||||
|
"team": "team-foo"
|
||||||
|
}
|
||||||
|
|
||||||
|
:statuscode 201: Successful team pin
|
||||||
|
:statuscode 400: Missing team parameter or team already pinned
|
||||||
|
'''
|
||||||
|
check_user_auth(user_name, req)
|
||||||
|
data = load_json_body(req)
|
||||||
|
team = data.get('team')
|
||||||
|
if team is None:
|
||||||
|
raise HTTPBadRequest('Invalid team pin', 'Missing team parameter')
|
||||||
|
connection = db.connect()
|
||||||
|
cursor = connection.cursor()
|
||||||
|
try:
|
||||||
|
cursor.execute('''INSERT INTO `pinned_team` (`user_id`, `team_id`)
|
||||||
|
VALUES ((SELECT `id` FROM `user` WHERE `name` = %s),
|
||||||
|
(SELECT `id` FROM `team` WHERE `name` = %s))''',
|
||||||
|
(user_name, team))
|
||||||
|
connection.commit()
|
||||||
|
except db.IntegrityError as e:
|
||||||
|
# Duplicate key
|
||||||
|
if e.args[0] == 1062:
|
||||||
|
raise HTTPBadRequest('Invalid team pin', 'Team already pinned for this user')
|
||||||
|
# Team/user is null
|
||||||
|
elif e.args[0] == 1048:
|
||||||
|
err_msg = str(e.args[1])
|
||||||
|
if err_msg == 'Column \'user_id\' cannot be null':
|
||||||
|
err_msg = 'user "%s" not found' % user_name
|
||||||
|
elif err_msg == 'Column \'team_id\' cannot be null':
|
||||||
|
err_msg = 'team "%s" not found' % team
|
||||||
|
raise HTTPError('422 Unprocessable Entity', 'IntegrityError', err_msg)
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
connection.close()
|
||||||
|
resp.status = HTTP_201
|
||||||
@@ -537,13 +537,22 @@ nav.subnav li.active {
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recently-viewed h3 {
|
body[data-authenticated="false"] #pinned-teams {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-teams {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-teams h3 {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
border-top: 1px solid #dddedf;
|
border-top: 1px solid #dddedf;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-card .recently-viewed-name {
|
.module-card .landing-teams-name {
|
||||||
width: calc(100% - 60px);
|
width: calc(100% - 60px);
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -759,6 +768,10 @@ nav.subnav li.active {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body[data-authenticated="false"] #pin-team {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.team-name-input {
|
.team-name-input {
|
||||||
background: none;
|
background: none;
|
||||||
color: #FFF;
|
color: #FFF;
|
||||||
|
|||||||
@@ -411,10 +411,12 @@ var oncall = {
|
|||||||
summaryUrl: '/api/v0/teams/',
|
summaryUrl: '/api/v0/teams/',
|
||||||
pageSource: $('#search-template').html(),
|
pageSource: $('#search-template').html(),
|
||||||
searchResultsSource: $('#search-results-template').html(),
|
searchResultsSource: $('#search-results-template').html(),
|
||||||
cardInnerTemplate: $('#recently-viewed-inner-template').html(),
|
cardInnerTemplate: $('#landing-teams-inner-template').html(),
|
||||||
endpointTypes: ['services', 'teams'],
|
endpointTypes: ['services', 'teams'],
|
||||||
searchForm: '.main-search',
|
searchForm: '.main-search',
|
||||||
recentlyViewed: null
|
recentlyViewed: null,
|
||||||
|
pinnedTeams: null,
|
||||||
|
pinnedPromise: $.Deferred()
|
||||||
},
|
},
|
||||||
init: function(query){
|
init: function(query){
|
||||||
var $form,
|
var $form,
|
||||||
@@ -427,15 +429,27 @@ var oncall = {
|
|||||||
self = this;
|
self = this;
|
||||||
|
|
||||||
Handlebars.registerPartial('dashboard-card-inner', this.data.cardInnerTemplate);
|
Handlebars.registerPartial('dashboard-card-inner', this.data.cardInnerTemplate);
|
||||||
|
oncall.callbacks.onLogin = function(){
|
||||||
|
self.init();
|
||||||
|
};
|
||||||
this.data.recentlyViewed = oncall.recentlyViewed.getItems();
|
this.data.recentlyViewed = oncall.recentlyViewed.getItems();
|
||||||
this.renderPage();
|
if (oncall.data.user) {
|
||||||
this.getTeamSummaries();
|
$.get('/api/v0/users/' + oncall.data.user + '/pinned_teams').done(function(response){
|
||||||
|
self.data.pinnedTeams = response;
|
||||||
|
self.data.pinnedPromise.resolve();
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.data.pinnedPromise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
$form = this.data.$page.find(this.data.searchForm);
|
this.data.pinnedPromise.done(function() {
|
||||||
|
self.renderPage();
|
||||||
|
self.getTeamSummaries();
|
||||||
|
$form = self.data.$page.find(self.data.searchForm);
|
||||||
$input = $form.find('.search-input');
|
$input = $form.find('.search-input');
|
||||||
|
|
||||||
if (query) {
|
if (query) {
|
||||||
this.getData.call(this, query);
|
self.getData.call(self, query);
|
||||||
$form.find('.search-input').val(decodeURIComponent(query.query));
|
$form.find('.search-input').val(decodeURIComponent(query.query));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,7 +457,7 @@ var oncall = {
|
|||||||
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
|
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
|
||||||
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
||||||
remote: {
|
remote: {
|
||||||
url: this.data.url + '?keyword=%QUERY',
|
url: self.data.url + '?keyword=%QUERY',
|
||||||
rateLimitWait: 200,
|
rateLimitWait: 200,
|
||||||
wildcard: '%QUERY',
|
wildcard: '%QUERY',
|
||||||
transform: function(resp){
|
transform: function(resp){
|
||||||
@@ -466,7 +480,7 @@ var oncall = {
|
|||||||
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
|
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
|
||||||
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
||||||
remote: {
|
remote: {
|
||||||
url: this.data.url + '?keyword=%QUERY',
|
url: self.data.url + '?keyword=%QUERY',
|
||||||
rateLimitWait: 200,
|
rateLimitWait: 200,
|
||||||
wildcard: '%QUERY',
|
wildcard: '%QUERY',
|
||||||
transform: function(resp){
|
transform: function(resp){
|
||||||
@@ -539,6 +553,7 @@ var oncall = {
|
|||||||
.on('typeahead:selected', function(){
|
.on('typeahead:selected', function(){
|
||||||
router.navigate('/team/' + $(this).val());
|
router.navigate('/team/' + $(this).val());
|
||||||
});
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
events: function(){
|
events: function(){
|
||||||
this.data.$page.on('submit', this.data.searchForm, this.updateSearch.bind(this));
|
this.data.$page.on('submit', this.data.searchForm, this.updateSearch.bind(this));
|
||||||
@@ -555,7 +570,7 @@ var oncall = {
|
|||||||
$.get(this.data.url, param, this.renderResults.bind(this));
|
$.get(this.data.url, param, this.renderResults.bind(this));
|
||||||
},
|
},
|
||||||
getTeamSummaries: function(){
|
getTeamSummaries: function(){
|
||||||
var data = this.data.recentlyViewed,
|
var data = this.data.pinnedTeams ? this.data.recentlyViewed.concat(this.data.pinnedTeams) : this.data.recentlyViewed,
|
||||||
self = this;
|
self = this;
|
||||||
if (data) {
|
if (data) {
|
||||||
for (var i = 0; i < data.length; i++) {
|
for (var i = 0; i < data.length; i++) {
|
||||||
@@ -581,7 +596,31 @@ var oncall = {
|
|||||||
},
|
},
|
||||||
renderPage: function(){
|
renderPage: function(){
|
||||||
var template = Handlebars.compile(this.data.pageSource);
|
var template = Handlebars.compile(this.data.pageSource);
|
||||||
this.data.$page.html(template(this.data.recentlyViewed));
|
this.data.$page.html(template({recent: this.data.recentlyViewed, pinned: this.data.pinnedTeams}));
|
||||||
|
|
||||||
|
this.data.$page.on('click','.remove-card-column', function(){
|
||||||
|
var $teamCard = $(this).closest('.module-card'),
|
||||||
|
$pinnedTeams = $('#pinned-teams'),
|
||||||
|
teamName = $teamCard.attr('data-team');
|
||||||
|
|
||||||
|
oncall.data.$body.addClass('loading-view');
|
||||||
|
$.ajax({
|
||||||
|
type: 'DELETE',
|
||||||
|
url: 'api/v0/users/' + oncall.data.user + '/pinned_teams/' + teamName,
|
||||||
|
dataType: 'html'
|
||||||
|
}).done(function(){
|
||||||
|
$teamCard.hide();
|
||||||
|
if ($teamCard.siblings(':visible').length === 0) {
|
||||||
|
$pinnedTeams.hide()
|
||||||
|
}
|
||||||
|
}).fail(function(data){
|
||||||
|
var error = oncall.isJson(data.responseText) ? JSON.parse(data.responseText).description : data.responseText || 'Could not unpin team.';
|
||||||
|
oncall.alerts.createAlert('Failed: ' + error, 'danger');
|
||||||
|
}).always(function(){
|
||||||
|
oncall.data.$body.removeClass('loading-view');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
this.events();
|
this.events();
|
||||||
},
|
},
|
||||||
renderCardInner: function(data){
|
renderCardInner: function(data){
|
||||||
@@ -612,7 +651,9 @@ var oncall = {
|
|||||||
team: {
|
team: {
|
||||||
data: {
|
data: {
|
||||||
$page: $('.content-wrapper'),
|
$page: $('.content-wrapper'),
|
||||||
|
$pinButton: $('#pin-team'),
|
||||||
url: '/api/v0/teams/',
|
url: '/api/v0/teams/',
|
||||||
|
pinUrl: '/api/v0/users/',
|
||||||
teamSubheaderTemplate: $('#team-subheader-template').html(),
|
teamSubheaderTemplate: $('#team-subheader-template').html(),
|
||||||
subheaderWrapper: '.subheader-wrapper',
|
subheaderWrapper: '.subheader-wrapper',
|
||||||
deleteTeam: '#delete-team',
|
deleteTeam: '#delete-team',
|
||||||
@@ -737,6 +778,28 @@ var oncall = {
|
|||||||
$cta.removeClass('loading disabled').prop('disabled', false);
|
$cta.removeClass('loading disabled').prop('disabled', false);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
pinTeam: function($modal) {
|
||||||
|
var $cta = $modal.find('.modal-cta'),
|
||||||
|
self = this;
|
||||||
|
|
||||||
|
$cta.addClass('loading disabled').prop('disabled', true);
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: self.data.pinUrl + oncall.data.user + '/pinned_teams/',
|
||||||
|
contentType: 'application/json',
|
||||||
|
dataType: 'html',
|
||||||
|
data: JSON.stringify({team:self.data.teamName})
|
||||||
|
}).done(function(){
|
||||||
|
oncall.alerts.removeAlerts();
|
||||||
|
oncall.alerts.createAlert('Pinned team to home page', 'success');
|
||||||
|
}).fail(function(data){
|
||||||
|
var error = oncall.isJson(data.responseText) ? JSON.parse(data.responseText).description : data.responseText || 'Pinning team failed.';
|
||||||
|
oncall.alerts.createAlert(error, 'danger');
|
||||||
|
}).always(function(){
|
||||||
|
$cta.removeClass('loading disabled').prop('disabled', false);
|
||||||
|
$modal.modal('hide');
|
||||||
|
});
|
||||||
|
},
|
||||||
calendar: {
|
calendar: {
|
||||||
data: {
|
data: {
|
||||||
$page: $('.content-wrapper'),
|
$page: $('.content-wrapper'),
|
||||||
|
|||||||
@@ -129,11 +129,39 @@
|
|||||||
<div class="main container-fluid">
|
<div class="main container-fluid">
|
||||||
<ul class="search-results">
|
<ul class="search-results">
|
||||||
</ul>
|
</ul>
|
||||||
{{#if this}}
|
{{#if this.pinned}}
|
||||||
<div class="recently-viewed">
|
<div class="landing-teams" id="pinned-teams">
|
||||||
|
<h3>Pinned Teams</h3>
|
||||||
|
<div class="card-wrap col-3">
|
||||||
|
{{#each this.pinned}}
|
||||||
|
<div class="module module-card dashboard-card card-column" data-team="{{this}}">
|
||||||
|
<div class="module-card-heading border-bottom">
|
||||||
|
<h4>
|
||||||
|
<span>
|
||||||
|
<a href="/team/{{this}}" data-navigo>{{this}}</a>
|
||||||
|
</span>
|
||||||
|
</h4>
|
||||||
|
<span class="remove-card-column">
|
||||||
|
<i class="svg-icon grey-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8" viewBox="0 0 8 8">
|
||||||
|
<path d="M1.41 0l-1.41 1.41.72.72 1.78 1.81-1.78 1.78-.72.69 1.41 1.44.72-.72 1.81-1.81 1.78 1.81.69.72 1.44-1.44-.72-.69-1.81-1.78 1.81-1.81.72-.72-1.44-1.41-.69.72-1.78 1.78-1.81-1.78-.72-.72z" />
|
||||||
|
</svg>
|
||||||
|
</i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-card-inner">
|
||||||
|
<i class="loader"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{#if this.recent}}
|
||||||
|
<div class="landing-teams" id="recent-teams">
|
||||||
<h3>Most Recently Viewed</h3>
|
<h3>Most Recently Viewed</h3>
|
||||||
<div class="card-wrap col-3">
|
<div class="card-wrap col-3">
|
||||||
{{#each .}}
|
{{#each this.recent}}
|
||||||
<div class="module module-card dashboard-card card-column" data-team="{{this}}">
|
<div class="module module-card dashboard-card card-column" data-team="{{this}}">
|
||||||
<div class="module-card-heading border-bottom">
|
<div class="module-card-heading border-bottom">
|
||||||
<h4>
|
<h4>
|
||||||
@@ -199,14 +227,14 @@
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!--// recently viewed partial -->
|
<!--// landing teams partial -->
|
||||||
<script id="recently-viewed-inner-template" type="text/x-handlebars-template">
|
<script id="landing-teams-inner-template" type="text/x-handlebars-template">
|
||||||
<ul class="card-inner-wrapper">
|
<ul class="card-inner-wrapper">
|
||||||
{{#ifNotEmpty data.current.primary}}
|
{{#ifNotEmpty data.current.primary}}
|
||||||
<span class="subheading light">On Call Now</span>
|
<span class="subheading light">On Call Now</span>
|
||||||
<li class="card-inner border-bottom" data-card-name="{{data.current.primary.0.full_name}}">
|
<li class="card-inner border-bottom" data-card-name="{{data.current.primary.0.full_name}}">
|
||||||
<img class="card-picture" src="{{data.current.primary.0.photo_url}}" alt="{{data.current.primary.0.full_name}}">
|
<img class="card-picture" src="{{data.current.primary.0.photo_url}}" alt="{{data.current.primary.0.full_name}}">
|
||||||
<h4><strong><a class="recently-viewed-name" target="_blank" href="mailto:{{data.current.primary.0.user_contacts.email}}">{{data.current.primary.0.full_name}}</a></strong> <span class="badge pull-right" data-role="{{data.current.primary.0.role}}">{{data.current.primary.0.role}}</span></h4>
|
<h4><strong><a class="landing-teams-name" target="_blank" href="mailto:{{data.current.primary.0.user_contacts.email}}">{{data.current.primary.0.full_name}}</a></strong> <span class="badge pull-right" data-role="{{data.current.primary.0.role}}">{{data.current.primary.0.role}}</span></h4>
|
||||||
|
|
||||||
{{#if data.current.primary.0.user_contacts.call}}
|
{{#if data.current.primary.0.user_contacts.call}}
|
||||||
<span class="light"><a href="tel:{{data.current.primary.0.user_contacts.call}}">{{data.current.primary.0.user_contacts.call}}</a></span>
|
<span class="light"><a href="tel:{{data.current.primary.0.user_contacts.call}}">{{data.current.primary.0.user_contacts.call}}</a></span>
|
||||||
@@ -262,11 +290,16 @@
|
|||||||
</h4>
|
</h4>
|
||||||
{% endraw %} {% endif %} {% raw %}
|
{% endraw %} {% endif %} {% raw %}
|
||||||
</div>
|
</div>
|
||||||
<div class="pull-right" data-admin-action="true">
|
<div class="pull-right">
|
||||||
|
<span data-admin-action="true">
|
||||||
{{#isEqual page "info"}}
|
{{#isEqual page "info"}}
|
||||||
<button class="btn btn-white add-roster-btn" data-toggle="modal" data-target="#input-modal" data-modal-action="oncall.team.info.addRoster" data-modal-title="Add roster" data-modal-placeholder="Roster Name"> + add roster </button>
|
<button id="#delete-team" class="btn btn-white" data-toggle="modal" data-target="#confirm-action-modal" data-modal-action="oncall.team.deleteTeam" data-modal-title="Delete {{name}}" data-modal-content="Delete {{name}}?"> Delete team </button>
|
||||||
<button id="#delete-team" class="btn btn-primary" data-toggle="modal" data-target="#confirm-action-modal" data-modal-action="oncall.team.deleteTeam" data-modal-title="Delete {{name}}" data-modal-content="Delete {{name}}?"> Delete Team </button>
|
<button class="btn btn-white add-roster-btn" data-toggle="modal" data-target="#input-modal" data-modal-action="oncall.team.info.addRoster" data-modal-title="Add roster" data-modal-placeholder="Roster Name"> Add roster </button>
|
||||||
{{/isEqual}}
|
{{/isEqual}}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<button id="pin-team" class="btn btn-white" data-toggle="modal" data-target="#confirm-action-modal" data-modal-action="oncall.team.pinTeam" data-modal-title="Pin {{name}}" data-modal-content="Pin {{name}} to landing page?"> Pin to home </button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user