mirror of
https://github.com/grafana/grafana.git
synced 2025-08-01 12:12:11 +08:00
feat(invite): more work on invite, basic creation works, added new tab directive from angular-ui and made new tab style, #2353
This commit is contained in:
@ -89,6 +89,10 @@ func Register(r *macaron.Macaron) {
|
|||||||
r.Get("/users", wrap(GetOrgUsersForCurrentOrg))
|
r.Get("/users", wrap(GetOrgUsersForCurrentOrg))
|
||||||
r.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUserForCurrentOrg))
|
r.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUserForCurrentOrg))
|
||||||
r.Delete("/users/:userId", wrap(RemoveOrgUserForCurrentOrg))
|
r.Delete("/users/:userId", wrap(RemoveOrgUserForCurrentOrg))
|
||||||
|
|
||||||
|
// invites
|
||||||
|
r.Get("/invites", wrap(GetPendingOrgInvites))
|
||||||
|
r.Post("/invites", bind(dtos.AddInviteForm{}), wrap(AddOrgInvite))
|
||||||
}, regOrgAdmin)
|
}, regOrgAdmin)
|
||||||
|
|
||||||
// create new org
|
// create new org
|
||||||
|
9
pkg/api/dtos/invite.go
Normal file
9
pkg/api/dtos/invite.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package dtos
|
||||||
|
|
||||||
|
import m "github.com/grafana/grafana/pkg/models"
|
||||||
|
|
||||||
|
type AddInviteForm struct {
|
||||||
|
Email string `json:"email" binding:"Required"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Role m.RoleType `json:"role" binding:"Required"`
|
||||||
|
}
|
39
pkg/api/org_invite.go
Normal file
39
pkg/api/org_invite.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetPendingOrgInvites(c *middleware.Context) Response {
|
||||||
|
query := m.GetTempUsersForOrgQuery{OrgId: c.OrgId}
|
||||||
|
|
||||||
|
if err := bus.Dispatch(&query); err != nil {
|
||||||
|
return ApiError(500, "Failed to get invites from db", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Json(200, query.Result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddOrgInvite(c *middleware.Context, inviteDto dtos.AddInviteForm) Response {
|
||||||
|
if !inviteDto.Role.IsValid() {
|
||||||
|
return ApiError(400, "Invalid role specified", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := m.CreateTempUserCommand{}
|
||||||
|
cmd.OrgId = c.OrgId
|
||||||
|
cmd.Email = inviteDto.Email
|
||||||
|
cmd.Name = inviteDto.Name
|
||||||
|
cmd.IsInvite = true
|
||||||
|
cmd.InvitedByUserId = c.UserId
|
||||||
|
cmd.Code = util.GetRandomString(30)
|
||||||
|
|
||||||
|
if err := bus.Dispatch(&cmd); err != nil {
|
||||||
|
return ApiError(500, "Failed to save invite to database", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiSuccess("ok, done!")
|
||||||
|
}
|
@ -10,7 +10,7 @@ var (
|
|||||||
ErrTempUserNotFound = errors.New("User not found")
|
ErrTempUserNotFound = errors.New("User not found")
|
||||||
)
|
)
|
||||||
|
|
||||||
// TempUser holds data for org invites and new sign ups
|
// TempUser holds data for org invites and unconfirmed sign ups
|
||||||
type TempUser struct {
|
type TempUser struct {
|
||||||
Id int64
|
Id int64
|
||||||
OrgId int64
|
OrgId int64
|
||||||
@ -19,6 +19,7 @@ type TempUser struct {
|
|||||||
Name string
|
Name string
|
||||||
Role string
|
Role string
|
||||||
IsInvite bool
|
IsInvite bool
|
||||||
|
InvitedByUserId int64
|
||||||
|
|
||||||
EmailSent bool
|
EmailSent bool
|
||||||
EmailSentOn time.Time
|
EmailSentOn time.Time
|
||||||
@ -36,6 +37,7 @@ type CreateTempUserCommand struct {
|
|||||||
Name string
|
Name string
|
||||||
OrgId int64
|
OrgId int64
|
||||||
IsInvite bool
|
IsInvite bool
|
||||||
|
InvitedByUserId int64
|
||||||
Code string
|
Code string
|
||||||
|
|
||||||
Result *TempUser
|
Result *TempUser
|
||||||
|
@ -14,7 +14,7 @@ func addTempUserMigrations(mg *Migrator) {
|
|||||||
{Name: "role", Type: DB_NVarchar, Length: 20, Nullable: true},
|
{Name: "role", Type: DB_NVarchar, Length: 20, Nullable: true},
|
||||||
{Name: "code", Type: DB_NVarchar, Length: 255},
|
{Name: "code", Type: DB_NVarchar, Length: 255},
|
||||||
{Name: "is_invite", Type: DB_Bool},
|
{Name: "is_invite", Type: DB_Bool},
|
||||||
{Name: "invited_by", Type: DB_NVarchar, Length: 255, Nullable: true},
|
{Name: "invited_by_user_id", Type: DB_BigInt, Nullable: true},
|
||||||
{Name: "email_sent", Type: DB_Bool},
|
{Name: "email_sent", Type: DB_Bool},
|
||||||
{Name: "email_sent_on", Type: DB_DateTime, Nullable: true},
|
{Name: "email_sent_on", Type: DB_DateTime, Nullable: true},
|
||||||
{Name: "created", Type: DB_DateTime},
|
{Name: "created", Type: DB_DateTime},
|
||||||
@ -28,7 +28,7 @@ func addTempUserMigrations(mg *Migrator) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create table
|
// create table
|
||||||
mg.AddMigration("create temp user table v1", NewAddTableMigration(tempUserV1))
|
mg.AddMigration("create temp user table v1-3", NewAddTableMigration(tempUserV1))
|
||||||
|
|
||||||
addTableIndicesMigrations(mg, "v1-1", tempUserV1)
|
addTableIndicesMigrations(mg, "v1-3", tempUserV1)
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ func CreateTempUser(cmd *m.CreateTempUserCommand) error {
|
|||||||
OrgId: cmd.OrgId,
|
OrgId: cmd.OrgId,
|
||||||
Code: cmd.Code,
|
Code: cmd.Code,
|
||||||
IsInvite: cmd.IsInvite,
|
IsInvite: cmd.IsInvite,
|
||||||
|
InvitedByUserId: cmd.InvitedByUserId,
|
||||||
Created: time.Now(),
|
Created: time.Now(),
|
||||||
Updated: time.Now(),
|
Updated: time.Now(),
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ define([
|
|||||||
'angular-sanitize',
|
'angular-sanitize',
|
||||||
'angular-strap',
|
'angular-strap',
|
||||||
'angular-dragdrop',
|
'angular-dragdrop',
|
||||||
|
'angular-ui',
|
||||||
'extend-jquery',
|
'extend-jquery',
|
||||||
'bindonce',
|
'bindonce',
|
||||||
],
|
],
|
||||||
@ -64,7 +65,8 @@ function (angular, $, _, appLevelRequire) {
|
|||||||
'$strap.directives',
|
'$strap.directives',
|
||||||
'ang-drag-drop',
|
'ang-drag-drop',
|
||||||
'grafana',
|
'grafana',
|
||||||
'pasvaz.bindonce'
|
'pasvaz.bindonce',
|
||||||
|
'ui.bootstrap.tabs',
|
||||||
];
|
];
|
||||||
|
|
||||||
var module_types = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes'];
|
var module_types = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes'];
|
||||||
|
@ -17,6 +17,7 @@ require.config({
|
|||||||
'angular-sanitize': '../vendor/angular-sanitize/angular-sanitize',
|
'angular-sanitize': '../vendor/angular-sanitize/angular-sanitize',
|
||||||
'angular-dragdrop': '../vendor/angular-native-dragdrop/draganddrop',
|
'angular-dragdrop': '../vendor/angular-native-dragdrop/draganddrop',
|
||||||
'angular-strap': '../vendor/angular-other/angular-strap',
|
'angular-strap': '../vendor/angular-other/angular-strap',
|
||||||
|
'angular-ui': '../vendor/angular-ui/angular-bootstrap',
|
||||||
timepicker: '../vendor/angular-other/timepicker',
|
timepicker: '../vendor/angular-other/timepicker',
|
||||||
datepicker: '../vendor/angular-other/datepicker',
|
datepicker: '../vendor/angular-other/datepicker',
|
||||||
bindonce: '../vendor/angular-bindonce/bindonce',
|
bindonce: '../vendor/angular-bindonce/bindonce',
|
||||||
@ -90,6 +91,7 @@ require.config({
|
|||||||
'angular-dragdrop': ['jquery', 'angular'],
|
'angular-dragdrop': ['jquery', 'angular'],
|
||||||
'angular-mocks': ['angular'],
|
'angular-mocks': ['angular'],
|
||||||
'angular-sanitize': ['angular'],
|
'angular-sanitize': ['angular'],
|
||||||
|
'angular-ui': ['angular'],
|
||||||
'angular-route': ['angular'],
|
'angular-route': ['angular'],
|
||||||
'angular-strap': ['angular', 'bootstrap','timepicker', 'datepicker'],
|
'angular-strap': ['angular', 'bootstrap','timepicker', 'datepicker'],
|
||||||
'bindonce': ['angular'],
|
'bindonce': ['angular'],
|
||||||
|
@ -13,6 +13,9 @@ function (angular) {
|
|||||||
role: 'Viewer',
|
role: 'Viewer',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.users = [];
|
||||||
|
$scope.pendingInvites = [];
|
||||||
|
|
||||||
$scope.init = function() {
|
$scope.init = function() {
|
||||||
$scope.get();
|
$scope.get();
|
||||||
$scope.editor = { index: 0 };
|
$scope.editor = { index: 0 };
|
||||||
@ -22,6 +25,9 @@ function (angular) {
|
|||||||
backendSrv.get('/api/org/users').then(function(users) {
|
backendSrv.get('/api/org/users').then(function(users) {
|
||||||
$scope.users = users;
|
$scope.users = users;
|
||||||
});
|
});
|
||||||
|
backendSrv.get('/api/org/invites').then(function(pendingInvites) {
|
||||||
|
$scope.pendingInvites = pendingInvites;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.updateOrgUser = function(user) {
|
$scope.updateOrgUser = function(user) {
|
||||||
|
@ -15,12 +15,8 @@
|
|||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<div ng-model="editor.index" bs-tabs style="text-transform:capitalize;">
|
<tabset>
|
||||||
<div ng-repeat="tab in ['Users', 'Pending Invitations']" data-title="{{tab}}">
|
<tab heading="Users ({{users.length}})">
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-if="editor.index == 0">
|
|
||||||
<table class="grafana-options-table form-inline">
|
<table class="grafana-options-table form-inline">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Login</th>
|
<th>Login</th>
|
||||||
@ -42,12 +38,33 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
</tab>
|
||||||
|
<tab heading="Pending Invitations ({{pendingInvites.length}})">
|
||||||
|
<table class="grafana-options-table form-inline">
|
||||||
|
<tr>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Role</th>
|
||||||
|
<th>Created on</th>
|
||||||
|
<th>Invited by</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
<tr ng-repeat="invite in pendingInvites">
|
||||||
|
<td>{{invite.email}}</td>
|
||||||
|
<td>{{invite.name}}</td>
|
||||||
|
<td>{{invite.role}}</td>
|
||||||
|
<td>{{invite.createdOn | date:'medium'}}</td>
|
||||||
|
<td>{{invite.invitedBy}}</td>
|
||||||
|
<td style="width: 1%">
|
||||||
|
<a ng-click="removeInvite(invite)" class="btn btn-danger btn-mini">
|
||||||
|
<i class="fa fa-remove"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</tab>
|
||||||
|
</tabset>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-if="editor.index == 1">
|
|
||||||
Pending invitaitons
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ function (angular, _) {
|
|||||||
|
|
||||||
var module = angular.module('grafana.controllers');
|
var module = angular.module('grafana.controllers');
|
||||||
|
|
||||||
module.controller('UserInviteCtrl', function($scope) {
|
module.controller('UserInviteCtrl', function($scope, backendSrv) {
|
||||||
|
|
||||||
$scope.invites = [
|
$scope.invites = [
|
||||||
{name: '', email: '', role: 'Editor'},
|
{name: '', email: '', role: 'Editor'},
|
||||||
@ -27,6 +27,10 @@ function (angular, _) {
|
|||||||
$scope.sendInvites = function() {
|
$scope.sendInvites = function() {
|
||||||
if (!$scope.inviteForm.$valid) { return; }
|
if (!$scope.inviteForm.$valid) { return; }
|
||||||
|
|
||||||
|
_.each($scope.invites, function(invite) {
|
||||||
|
backendSrv.post('/api/org/invites', invite);
|
||||||
|
});
|
||||||
|
|
||||||
$scope.dismiss();
|
$scope.dismiss();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
3
public/app/partials/bootstrap/tab.html
Normal file
3
public/app/partials/bootstrap/tab.html
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<li ng-class="{active: active, disabled: disabled}">
|
||||||
|
<a href ng-click="select()" tab-heading-transclude>{{heading}}</a>
|
||||||
|
</li>
|
10
public/app/partials/bootstrap/tabset.html
Normal file
10
public/app/partials/bootstrap/tabset.html
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<div>
|
||||||
|
<ul class="nav nav-{{type || 'tabs'}} nav-tabs-alt" ng-class="{'nav-stacked': vertical, 'nav-justified': justified}" ng-transclude></ul>
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="tab-pane"
|
||||||
|
ng-repeat="tab in tabs"
|
||||||
|
ng-class="{active: tab.active}"
|
||||||
|
tab-content-transclude="tab">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -220,6 +220,7 @@ div.subnav {
|
|||||||
|
|
||||||
li > a:hover,
|
li > a:hover,
|
||||||
li.active > a,
|
li.active > a,
|
||||||
|
li.active > a:focus,
|
||||||
li.active > a:hover {
|
li.active > a:hover {
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
@ -159,6 +159,7 @@ div.subnav {
|
|||||||
|
|
||||||
li > a:hover,
|
li > a:hover,
|
||||||
li.active > a,
|
li.active > a,
|
||||||
|
li.active > a:focus,
|
||||||
li.active > a:hover {
|
li.active > a:hover {
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
@import "admin.less";
|
@import "admin.less";
|
||||||
@import "validation.less";
|
@import "validation.less";
|
||||||
@import "fonts.less";
|
@import "fonts.less";
|
||||||
|
@import "tabs.less";
|
||||||
|
|
||||||
.row-control-inner {
|
.row-control-inner {
|
||||||
padding:0px;
|
padding:0px;
|
||||||
|
28
public/css/less/tabs.less
Normal file
28
public/css/less/tabs.less
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
.nav-tabs-alt {
|
||||||
|
border-bottom: @grafanaTriggerBorder;
|
||||||
|
padding-left: 10px;
|
||||||
|
|
||||||
|
& > li > a {
|
||||||
|
.border-radius(3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
li > a:hover,
|
||||||
|
li.active > a,
|
||||||
|
li.active > a:focus,
|
||||||
|
li.active > a:hover {
|
||||||
|
border: @grafanaTriggerBorder;
|
||||||
|
background-color: transparent;
|
||||||
|
border-bottom: 1px solid @grafanaPanelBackground;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.disabled > a {
|
||||||
|
color: @textColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open .dropdown-toggle {
|
||||||
|
background-color: #060606;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,6 +22,7 @@ require.config({
|
|||||||
'angular-sanitize': '../vendor/angular-sanitize/angular-sanitize',
|
'angular-sanitize': '../vendor/angular-sanitize/angular-sanitize',
|
||||||
angularMocks: '../vendor/angular-mocks/angular-mocks',
|
angularMocks: '../vendor/angular-mocks/angular-mocks',
|
||||||
'angular-dragdrop': '../vendor/angular-native-dragdrop/draganddrop',
|
'angular-dragdrop': '../vendor/angular-native-dragdrop/draganddrop',
|
||||||
|
'angular-ui': '../vendor/angular-ui/angular-bootstrap',
|
||||||
'angular-strap': '../vendor/angular-other/angular-strap',
|
'angular-strap': '../vendor/angular-other/angular-strap',
|
||||||
timepicker: '../vendor/angular-other/timepicker',
|
timepicker: '../vendor/angular-other/timepicker',
|
||||||
datepicker: '../vendor/angular-other/datepicker',
|
datepicker: '../vendor/angular-other/datepicker',
|
||||||
@ -83,6 +84,7 @@ require.config({
|
|||||||
|
|
||||||
'angular-route': ['angular'],
|
'angular-route': ['angular'],
|
||||||
'angular-sanitize': ['angular'],
|
'angular-sanitize': ['angular'],
|
||||||
|
'angular-ui': ['angular'],
|
||||||
'angular-dragdrop': ['jquery', 'angular'],
|
'angular-dragdrop': ['jquery', 'angular'],
|
||||||
'angular-mocks': ['angular'],
|
'angular-mocks': ['angular'],
|
||||||
'angular-strap': ['angular', 'bootstrap','timepicker', 'datepicker'],
|
'angular-strap': ['angular', 'bootstrap','timepicker', 'datepicker'],
|
||||||
|
6
public/vendor/angular-ui/angular-bootstrap.js
vendored
Normal file
6
public/vendor/angular-ui/angular-bootstrap.js
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
define([
|
||||||
|
'angular',
|
||||||
|
'../vendor/angular-ui/tabs',
|
||||||
|
], function() {
|
||||||
|
});
|
||||||
|
|
293
public/vendor/angular-ui/tabs.js
vendored
Normal file
293
public/vendor/angular-ui/tabs.js
vendored
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc overview
|
||||||
|
* @name ui.bootstrap.tabs
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* AngularJS version of the tabs directive.
|
||||||
|
*/
|
||||||
|
|
||||||
|
angular.module('ui.bootstrap.tabs', [])
|
||||||
|
|
||||||
|
.controller('TabsetController', ['$scope', function TabsetCtrl($scope) {
|
||||||
|
var ctrl = this,
|
||||||
|
tabs = ctrl.tabs = $scope.tabs = [];
|
||||||
|
|
||||||
|
ctrl.select = function(selectedTab) {
|
||||||
|
angular.forEach(tabs, function(tab) {
|
||||||
|
if (tab.active && tab !== selectedTab) {
|
||||||
|
tab.active = false;
|
||||||
|
tab.onDeselect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
selectedTab.active = true;
|
||||||
|
selectedTab.onSelect();
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl.addTab = function addTab(tab) {
|
||||||
|
tabs.push(tab);
|
||||||
|
// we can't run the select function on the first tab
|
||||||
|
// since that would select it twice
|
||||||
|
if (tabs.length === 1 && tab.active !== false) {
|
||||||
|
tab.active = true;
|
||||||
|
} else if (tab.active) {
|
||||||
|
ctrl.select(tab);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
tab.active = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl.removeTab = function removeTab(tab) {
|
||||||
|
var index = tabs.indexOf(tab);
|
||||||
|
//Select a new tab if the tab to be removed is selected and not destroyed
|
||||||
|
if (tab.active && tabs.length > 1 && !destroyed) {
|
||||||
|
//If this is the last tab, select the previous tab. else, the next tab.
|
||||||
|
var newActiveIndex = index == tabs.length - 1 ? index - 1 : index + 1;
|
||||||
|
ctrl.select(tabs[newActiveIndex]);
|
||||||
|
}
|
||||||
|
tabs.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
var destroyed;
|
||||||
|
$scope.$on('$destroy', function() {
|
||||||
|
destroyed = true;
|
||||||
|
});
|
||||||
|
}])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc directive
|
||||||
|
* @name ui.bootstrap.tabs.directive:tabset
|
||||||
|
* @restrict EA
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Tabset is the outer container for the tabs directive
|
||||||
|
*
|
||||||
|
* @param {boolean=} vertical Whether or not to use vertical styling for the tabs.
|
||||||
|
* @param {boolean=} justified Whether or not to use justified styling for the tabs.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
<example module="ui.bootstrap">
|
||||||
|
<file name="index.html">
|
||||||
|
<tabset>
|
||||||
|
<tab heading="Tab 1"><b>First</b> Content!</tab>
|
||||||
|
<tab heading="Tab 2"><i>Second</i> Content!</tab>
|
||||||
|
</tabset>
|
||||||
|
<hr />
|
||||||
|
<tabset vertical="true">
|
||||||
|
<tab heading="Vertical Tab 1"><b>First</b> Vertical Content!</tab>
|
||||||
|
<tab heading="Vertical Tab 2"><i>Second</i> Vertical Content!</tab>
|
||||||
|
</tabset>
|
||||||
|
<tabset justified="true">
|
||||||
|
<tab heading="Justified Tab 1"><b>First</b> Justified Content!</tab>
|
||||||
|
<tab heading="Justified Tab 2"><i>Second</i> Justified Content!</tab>
|
||||||
|
</tabset>
|
||||||
|
</file>
|
||||||
|
</example>
|
||||||
|
*/
|
||||||
|
.directive('tabset', function() {
|
||||||
|
return {
|
||||||
|
restrict: 'EA',
|
||||||
|
transclude: true,
|
||||||
|
replace: true,
|
||||||
|
scope: {
|
||||||
|
type: '@'
|
||||||
|
},
|
||||||
|
controller: 'TabsetController',
|
||||||
|
templateUrl: 'app/partials/bootstrap/tabset.html',
|
||||||
|
link: function(scope, element, attrs) {
|
||||||
|
scope.vertical = angular.isDefined(attrs.vertical) ? scope.$parent.$eval(attrs.vertical) : false;
|
||||||
|
scope.justified = angular.isDefined(attrs.justified) ? scope.$parent.$eval(attrs.justified) : false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc directive
|
||||||
|
* @name ui.bootstrap.tabs.directive:tab
|
||||||
|
* @restrict EA
|
||||||
|
*
|
||||||
|
* @param {string=} heading The visible heading, or title, of the tab. Set HTML headings with {@link ui.bootstrap.tabs.directive:tabHeading tabHeading}.
|
||||||
|
* @param {string=} select An expression to evaluate when the tab is selected.
|
||||||
|
* @param {boolean=} active A binding, telling whether or not this tab is selected.
|
||||||
|
* @param {boolean=} disabled A binding, telling whether or not this tab is disabled.
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Creates a tab with a heading and content. Must be placed within a {@link ui.bootstrap.tabs.directive:tabset tabset}.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
<example module="ui.bootstrap">
|
||||||
|
<file name="index.html">
|
||||||
|
<div ng-controller="TabsDemoCtrl">
|
||||||
|
<button class="btn btn-small" ng-click="items[0].active = true">
|
||||||
|
Select item 1, using active binding
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-small" ng-click="items[1].disabled = !items[1].disabled">
|
||||||
|
Enable/disable item 2, using disabled binding
|
||||||
|
</button>
|
||||||
|
<br />
|
||||||
|
<tabset>
|
||||||
|
<tab heading="Tab 1">First Tab</tab>
|
||||||
|
<tab select="alertMe()">
|
||||||
|
<tab-heading><i class="icon-bell"></i> Alert me!</tab-heading>
|
||||||
|
Second Tab, with alert callback and html heading!
|
||||||
|
</tab>
|
||||||
|
<tab ng-repeat="item in items"
|
||||||
|
heading="{{item.title}}"
|
||||||
|
disabled="item.disabled"
|
||||||
|
active="item.active">
|
||||||
|
{{item.content}}
|
||||||
|
</tab>
|
||||||
|
</tabset>
|
||||||
|
</div>
|
||||||
|
</file>
|
||||||
|
<file name="script.js">
|
||||||
|
function TabsDemoCtrl($scope) {
|
||||||
|
$scope.items = [
|
||||||
|
{ title:"Dynamic Title 1", content:"Dynamic Item 0" },
|
||||||
|
{ title:"Dynamic Title 2", content:"Dynamic Item 1", disabled: true }
|
||||||
|
];
|
||||||
|
|
||||||
|
$scope.alertMe = function() {
|
||||||
|
setTimeout(function() {
|
||||||
|
alert("You've selected the alert tab!");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
</file>
|
||||||
|
</example>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc directive
|
||||||
|
* @name ui.bootstrap.tabs.directive:tabHeading
|
||||||
|
* @restrict EA
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Creates an HTML heading for a {@link ui.bootstrap.tabs.directive:tab tab}. Must be placed as a child of a tab element.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
<example module="ui.bootstrap">
|
||||||
|
<file name="index.html">
|
||||||
|
<tabset>
|
||||||
|
<tab>
|
||||||
|
<tab-heading><b>HTML</b> in my titles?!</tab-heading>
|
||||||
|
And some content, too!
|
||||||
|
</tab>
|
||||||
|
<tab>
|
||||||
|
<tab-heading><i class="icon-heart"></i> Icon heading?!?</tab-heading>
|
||||||
|
That's right.
|
||||||
|
</tab>
|
||||||
|
</tabset>
|
||||||
|
</file>
|
||||||
|
</example>
|
||||||
|
*/
|
||||||
|
.directive('tab', ['$parse', '$log', function($parse, $log) {
|
||||||
|
return {
|
||||||
|
require: '^tabset',
|
||||||
|
restrict: 'EA',
|
||||||
|
replace: true,
|
||||||
|
templateUrl: 'app/partials/bootstrap/tab.html',
|
||||||
|
transclude: true,
|
||||||
|
scope: {
|
||||||
|
active: '=?',
|
||||||
|
heading: '@',
|
||||||
|
onSelect: '&select', //This callback is called in contentHeadingTransclude
|
||||||
|
//once it inserts the tab's content into the dom
|
||||||
|
onDeselect: '&deselect'
|
||||||
|
},
|
||||||
|
controller: function() {
|
||||||
|
//Empty controller so other directives can require being 'under' a tab
|
||||||
|
},
|
||||||
|
compile: function(elm, attrs, transclude) {
|
||||||
|
return function postLink(scope, elm, attrs, tabsetCtrl) {
|
||||||
|
scope.$watch('active', function(active) {
|
||||||
|
if (active) {
|
||||||
|
tabsetCtrl.select(scope);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
scope.disabled = false;
|
||||||
|
if ( attrs.disable ) {
|
||||||
|
scope.$parent.$watch($parse(attrs.disable), function(value) {
|
||||||
|
scope.disabled = !! value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecation support of "disabled" parameter
|
||||||
|
// fix(tab): IE9 disabled attr renders grey text on enabled tab #2677
|
||||||
|
// This code is duplicated from the lines above to make it easy to remove once
|
||||||
|
// the feature has been completely deprecated
|
||||||
|
if ( attrs.disabled ) {
|
||||||
|
$log.warn('Use of "disabled" attribute has been deprecated, please use "disable"');
|
||||||
|
scope.$parent.$watch($parse(attrs.disabled), function(value) {
|
||||||
|
scope.disabled = !! value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.select = function() {
|
||||||
|
if ( !scope.disabled ) {
|
||||||
|
scope.active = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tabsetCtrl.addTab(scope);
|
||||||
|
scope.$on('$destroy', function() {
|
||||||
|
tabsetCtrl.removeTab(scope);
|
||||||
|
});
|
||||||
|
|
||||||
|
//We need to transclude later, once the content container is ready.
|
||||||
|
//when this link happens, we're inside a tab heading.
|
||||||
|
scope.$transcludeFn = transclude;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}])
|
||||||
|
|
||||||
|
.directive('tabHeadingTransclude', [function() {
|
||||||
|
return {
|
||||||
|
restrict: 'A',
|
||||||
|
require: '^tab',
|
||||||
|
link: function(scope, elm, attrs, tabCtrl) {
|
||||||
|
scope.$watch('headingElement', function updateHeadingElement(heading) {
|
||||||
|
if (heading) {
|
||||||
|
elm.html('');
|
||||||
|
elm.append(heading);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}])
|
||||||
|
|
||||||
|
.directive('tabContentTransclude', function() {
|
||||||
|
return {
|
||||||
|
restrict: 'A',
|
||||||
|
require: '^tabset',
|
||||||
|
link: function(scope, elm, attrs) {
|
||||||
|
var tab = scope.$eval(attrs.tabContentTransclude);
|
||||||
|
|
||||||
|
//Now our tab is ready to be transcluded: both the tab heading area
|
||||||
|
//and the tab content area are loaded. Transclude 'em both.
|
||||||
|
tab.$transcludeFn(tab.$parent, function(contents) {
|
||||||
|
angular.forEach(contents, function(node) {
|
||||||
|
if (isTabHeading(node)) {
|
||||||
|
//Let tabHeadingTransclude know.
|
||||||
|
tab.headingElement = node;
|
||||||
|
} else {
|
||||||
|
elm.append(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
function isTabHeading(node) {
|
||||||
|
return node.tagName && (
|
||||||
|
node.hasAttribute('tab-heading') ||
|
||||||
|
node.hasAttribute('data-tab-heading') ||
|
||||||
|
node.tagName.toLowerCase() === 'tab-heading' ||
|
||||||
|
node.tagName.toLowerCase() === 'data-tab-heading'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
;
|
Reference in New Issue
Block a user