mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-22 17:10:10 +00:00
Initial features of accounts tab
It's rough right now, but the accounts tab shows a list of users and info. Right now the only action available is to apply settings (from template or another user) to a selection of users. More to come.
This commit is contained in:
parent
a8b4842895
commit
cd61989495
85
api.go
85
api.go
@ -575,22 +575,24 @@ func (app *appContext) SetOmbiDefaults(gc *gin.Context) {
|
||||
}
|
||||
|
||||
type defaultsReq struct {
|
||||
Username string `json:"username"`
|
||||
From string `json:"from"`
|
||||
ApplyTo []string `json:"apply_to"`
|
||||
ID string `json:"id"`
|
||||
Homescreen bool `json:"homescreen"`
|
||||
}
|
||||
|
||||
func (app *appContext) SetDefaults(gc *gin.Context) {
|
||||
var req defaultsReq
|
||||
gc.BindJSON(&req)
|
||||
app.info.Printf("Getting user defaults from \"%s\"", req.Username)
|
||||
user, status, err := app.jf.userByName(req.Username, false)
|
||||
userID := req.ID
|
||||
user, status, err := app.jf.userById(userID, false)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
app.err.Printf("Failed to get user from Jellyfin: Code %d", status)
|
||||
app.debug.Printf("Error: %s", err)
|
||||
respond(500, "Couldn't get user", gc)
|
||||
return
|
||||
}
|
||||
userID := user["Id"].(string)
|
||||
app.info.Printf("Getting user defaults from \"%s\"", user["Name"].(string))
|
||||
policy := user["Policy"].(map[string]interface{})
|
||||
app.storage.policy = policy
|
||||
app.storage.storePolicy()
|
||||
@ -615,6 +617,81 @@ func (app *appContext) SetDefaults(gc *gin.Context) {
|
||||
gc.JSON(200, map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
func (app *appContext) ApplySettings(gc *gin.Context) {
|
||||
var req defaultsReq
|
||||
gc.BindJSON(&req)
|
||||
applyingFrom := "template"
|
||||
var policy, configuration, displayprefs map[string]interface{}
|
||||
if req.From == "template" {
|
||||
if len(app.storage.policy) == 0 {
|
||||
respond(500, "No policy template available", gc)
|
||||
return
|
||||
}
|
||||
app.storage.loadPolicy()
|
||||
policy = app.storage.policy
|
||||
if req.Homescreen {
|
||||
if len(app.storage.configuration) == 0 || len(app.storage.displayprefs) == 0 {
|
||||
respond(500, "No homescreen template available", gc)
|
||||
return
|
||||
}
|
||||
configuration = app.storage.configuration
|
||||
displayprefs = app.storage.displayprefs
|
||||
}
|
||||
} else if req.From == "user" {
|
||||
applyingFrom = "user"
|
||||
user, status, err := app.jf.userById(req.ID, false)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
app.err.Printf("Failed to get user from Jellyfin: Code %d", status)
|
||||
app.debug.Printf("Error: %s", err)
|
||||
respond(500, "Couldn't get user", gc)
|
||||
return
|
||||
}
|
||||
applyingFrom = "\"" + user["Name"].(string) + "\""
|
||||
policy = user["Policy"].(map[string]interface{})
|
||||
if req.Homescreen {
|
||||
displayprefs, status, err = app.jf.getDisplayPreferences(req.ID)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
app.err.Printf("Failed to get DisplayPrefs: Code %d", status)
|
||||
app.debug.Printf("Error: %s", err)
|
||||
respond(500, "Couldn't get displayprefs", gc)
|
||||
return
|
||||
}
|
||||
configuration = user["Configuration"].(map[string]interface{})
|
||||
}
|
||||
}
|
||||
app.info.Printf("Applying settings to %d user(s) from %s", len(req.ApplyTo), applyingFrom)
|
||||
errors := map[string]map[string]string{
|
||||
"policy": map[string]string{},
|
||||
"homescreen": map[string]string{},
|
||||
}
|
||||
for _, id := range req.ApplyTo {
|
||||
status, err := app.jf.setPolicy(id, policy)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
errors["policy"][id] = fmt.Sprintf("%d: %s", status, err)
|
||||
}
|
||||
if req.Homescreen {
|
||||
status, err = app.jf.setConfiguration(id, configuration)
|
||||
errorString := ""
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
errorString += fmt.Sprintf("Configuration %d: %s ", status, err)
|
||||
} else {
|
||||
status, err = app.jf.setDisplayPreferences(id, displayprefs)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
errorString += fmt.Sprintf("Displayprefs %d: %s ", status, err)
|
||||
}
|
||||
}
|
||||
if errorString != "" {
|
||||
errors["homescreen"][id] = errorString
|
||||
}
|
||||
}
|
||||
}
|
||||
code := 200
|
||||
if len(errors["policy"]) == len(req.ApplyTo) || len(errors["homescreen"]) == len(req.ApplyTo) {
|
||||
code = 500
|
||||
}
|
||||
gc.JSON(code, errors)
|
||||
}
|
||||
|
||||
func (app *appContext) GetConfig(gc *gin.Context) {
|
||||
app.info.Println("Config requested")
|
||||
resp := map[string]interface{}{}
|
||||
|
135
data/static/accounts.js
Normal file
135
data/static/accounts.js
Normal file
@ -0,0 +1,135 @@
|
||||
document.getElementById('selectAll').onclick = function() {
|
||||
const checkboxes = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]');
|
||||
for (check of checkboxes) {
|
||||
check.checked = this.checked;
|
||||
}
|
||||
checkCheckboxes();
|
||||
};
|
||||
|
||||
function checkCheckboxes() {
|
||||
const defaultsButton = document.getElementById('accountsTabSetDefaults');
|
||||
const checkboxes = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]');
|
||||
let checked = false;
|
||||
for (check of checkboxes) {
|
||||
if (check.checked) {
|
||||
checked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!checked) {
|
||||
defaultsButton.classList.add('unfocused');
|
||||
} else if (defaultsButton.classList.contains('unfocused')) {
|
||||
defaultsButton.classList.remove('unfocused');
|
||||
}
|
||||
}
|
||||
|
||||
var jfUsers = [];
|
||||
|
||||
function populateUsers() {
|
||||
const acList = document.getElementById('accountsList');
|
||||
acList.innerHTML = `
|
||||
<div class="d-flex align-items-center">
|
||||
<strong>Getting Users...</strong>
|
||||
<div class="spinner-border ml-auto" role="status" aria-hidden="true"></div>
|
||||
</div>`;
|
||||
acList.parentNode.querySelector('thead').classList.add('unfocused');
|
||||
const accountsList = document.createElement('tbody');
|
||||
accountsList.id = 'accountsList';
|
||||
const template = function(id, username, email, lastActive, admin) {
|
||||
let isAdmin = "No";
|
||||
if (admin) {
|
||||
isAdmin = "Yes";
|
||||
}
|
||||
return `
|
||||
<td scope="row"><input class="form-check-input" type="checkbox" value="" id="select_${id}" onclick="checkCheckboxes();"></td>
|
||||
<td>${username}</td>
|
||||
<td>${email}</td>
|
||||
<td>${lastActive}</td>
|
||||
<td>${isAdmin}</td>
|
||||
<td><i class="fa fa-eye icon-button" id="viewConfig_${id}"></i></td>`;
|
||||
};
|
||||
|
||||
let req = new XMLHttpRequest();
|
||||
req.responseType = 'json';
|
||||
req.open("GET", "/getUsers", true);
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
if (this.status == 200) {
|
||||
jfUsers = req.response['users'];
|
||||
for (user of jfUsers) {
|
||||
let tr = document.createElement('tr');
|
||||
tr.innerHTML = template(user['id'], user['name'], user['email'], user['last_active'], user['admin']);
|
||||
accountsList.appendChild(tr);
|
||||
}
|
||||
const header = acList.parentNode.querySelector('thead');
|
||||
if (header.classList.contains('unfocused')) {
|
||||
header.classList.remove('unfocused');
|
||||
}
|
||||
acList.replaceWith(accountsList);
|
||||
}
|
||||
}
|
||||
};
|
||||
req.send();
|
||||
}
|
||||
|
||||
document.getElementById('selectAll').checked = false;
|
||||
|
||||
document.getElementById('accountsTabSetDefaults').onclick = function() {
|
||||
const checkboxes = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]');
|
||||
let userIDs = [];
|
||||
for (check of checkboxes) {
|
||||
if (check.checked) {
|
||||
userIDs.push(check.id.replace('select_', ''));
|
||||
}
|
||||
}
|
||||
if (userIDs.length == 0) {
|
||||
return;
|
||||
}
|
||||
let radioList = document.getElementById('defaultUserRadios');
|
||||
radioList.textContent = '';
|
||||
let first = true;
|
||||
for (user of jfUsers) {
|
||||
let radio = document.createElement('div');
|
||||
radio.classList.add('radio');
|
||||
let checked = 'checked';
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
checked = '';
|
||||
}
|
||||
radio.innerHTML = `
|
||||
<label><input type="radio" name="defaultRadios" id="select_${user['id']}" style="margin-right: 1rem;" ${checked}>${user['name']}</label>`;
|
||||
radioList.appendChild(radio);
|
||||
}
|
||||
let userstring = 'user';
|
||||
if (userIDs.length > 1) {
|
||||
userstring += 's';
|
||||
}
|
||||
document.getElementById('defaultsTitle').textContent = `Apply settings to ${userIDs.length} ${userstring}`;
|
||||
document.getElementById('userDefaultsDescription').textContent = `
|
||||
Create an account and configure it to your liking, then choose it from below to apply to your selected users.`;
|
||||
document.getElementById('storeHomescreenLabel').textContent = `Apply homescreen layout`;
|
||||
if (document.getElementById('defaultsSourceSection').classList.contains('unfocused')) {
|
||||
document.getElementById('defaultsSourceSection').classList.remove('unfocused');
|
||||
}
|
||||
document.getElementById('defaultsSource').value = 'userTemplate';
|
||||
document.getElementById('defaultUserRadios').classList.add('unfocused');
|
||||
document.getElementById('storeDefaults').onclick = function() {
|
||||
storeDefaults(userIDs);
|
||||
};
|
||||
userDefaultsModal.show();
|
||||
};
|
||||
|
||||
document.getElementById('defaultsSource').addEventListener('change', function() {
|
||||
const radios = document.getElementById('defaultUserRadios');
|
||||
if (this.value == 'userTemplate') {
|
||||
radios.classList.add('unfocused');
|
||||
} else if (radios.classList.contains('unfocused')) {
|
||||
radios.classList.remove('unfocused');
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
@ -1,3 +1,36 @@
|
||||
const tabs = {
|
||||
invitesEl: document.getElementById('invitesTab'),
|
||||
accountsEl: document.getElementById('accountsTab'),
|
||||
invitesTabButton: document.getElementById('invitesTabButton'),
|
||||
accountsTabButton: document.getElementById('accountsTabButton'),
|
||||
invites: function() {
|
||||
if (tabs.invitesEl.classList.contains('unfocused')) {
|
||||
tabs.accountsEl.classList.add('unfocused');
|
||||
tabs.invitesEl.classList.remove('unfocused');
|
||||
}
|
||||
if (tabs.invitesTabButton.classList.contains("text-muted")) {
|
||||
tabs.invitesTabButton.classList.remove("text-muted");
|
||||
tabs.accountsTabButton.classList.add("text-muted");
|
||||
}
|
||||
},
|
||||
accounts: function() {
|
||||
populateUsers();
|
||||
if (tabs.accountsEl.classList.contains('unfocused')) {
|
||||
tabs.invitesEl.classList.add('unfocused');
|
||||
tabs.accountsEl.classList.remove('unfocused');
|
||||
}
|
||||
if (tabs.accountsTabButton.classList.contains("text-muted")) {
|
||||
tabs.accountsTabButton.classList.remove("text-muted");
|
||||
tabs.invitesTabButton.classList.add("text-muted");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
tabs.invitesTabButton.onclick = tabs.invites;
|
||||
tabs.accountsTabButton.onclick = tabs.accounts;
|
||||
|
||||
tabs.invites();
|
||||
|
||||
// Used for theme change animation
|
||||
function whichTransitionEvent() {
|
||||
let t;
|
||||
@ -638,7 +671,7 @@ document.getElementById('openDefaultsWizard').onclick = function() {
|
||||
checked = '';
|
||||
}
|
||||
radio.innerHTML =
|
||||
`<label><input type="radio" name="defaultRadios" id="default_${user['name']}" style="margin-right: 1rem;" ${checked}>${user['name']}</label>`;
|
||||
`<label><input type="radio" name="defaultRadios" id="select_${user['id']}" style="margin-right: 1rem;" ${checked}>${user['name']}</label>`;
|
||||
radioList.appendChild(radio);
|
||||
}
|
||||
let button = document.getElementById('openDefaultsWizard');
|
||||
@ -655,6 +688,19 @@ document.getElementById('openDefaultsWizard').onclick = function() {
|
||||
submitButton.classList.add('btn-primary');
|
||||
}
|
||||
settingsModal.hide();
|
||||
document.getElementById('defaultsTitle').textContent = `New user defaults`;
|
||||
document.getElementById('userDefaultsDescription').textContent = `
|
||||
Create an account and configure it to your liking, then choose it from below to store the settings as a template for all new users.`;
|
||||
document.getElementById('storeHomescreenLabel').textContent = `Store homescreen layout`;
|
||||
document.getElementById('defaultsSource').value = 'fromUser';
|
||||
document.getElementById('defaultsSourceSection').classList.add('unfocused');
|
||||
document.getElementById('storeDefaults').onclick = function() {
|
||||
storeDefaults('all');
|
||||
};
|
||||
const list = document.getElementById('defaultUserRadios');
|
||||
if (list.classList.contains('unfocused')) {
|
||||
list.classList.remove('unfocused');
|
||||
}
|
||||
userDefaultsModal.show();
|
||||
}
|
||||
}
|
||||
@ -662,23 +708,39 @@ document.getElementById('openDefaultsWizard').onclick = function() {
|
||||
req.send();
|
||||
};
|
||||
|
||||
document.getElementById('storeDefaults').onclick = function () {
|
||||
function storeDefaults(users) {
|
||||
this.disabled = true;
|
||||
this.innerHTML =
|
||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
||||
'Loading...';
|
||||
let button = document.getElementById('storeDefaults');
|
||||
let radios = document.getElementsByName('defaultRadios');
|
||||
let id = '';
|
||||
for (let radio of radios) {
|
||||
if (radio.checked) {
|
||||
id = radio.id.replace('select_', '');
|
||||
break;
|
||||
}
|
||||
}
|
||||
let route = '/setDefaults';
|
||||
let data = {
|
||||
'username': radio.id.slice(8),
|
||||
'homescreen': false};
|
||||
'from': 'user',
|
||||
'id': id,
|
||||
'homescreen': false
|
||||
};
|
||||
if (document.getElementById('defaultsSource').value == 'userTemplate') {
|
||||
data['from'] = 'template';
|
||||
}
|
||||
if (users != 'all') {
|
||||
data['apply_to'] = users;
|
||||
route = '/applySettings';
|
||||
}
|
||||
if (document.getElementById('storeDefaultHomescreen').checked) {
|
||||
data['homescreen'] = true;
|
||||
}
|
||||
let req = new XMLHttpRequest();
|
||||
req.open("POST", "/setDefaults", true);
|
||||
req.open("POST", route, true);
|
||||
req.responseType = 'json';
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = function() {
|
||||
@ -692,9 +754,22 @@ document.getElementById('storeDefaults').onclick = function () {
|
||||
}
|
||||
button.classList.add('btn-success');
|
||||
button.disabled = false;
|
||||
setTimeout(function() { userDefaultsModal.hide(); }, 1000);
|
||||
setTimeout(function() {
|
||||
let button = document.getElementById('storeDefaults');
|
||||
button.textContent = "Submit";
|
||||
button.classList.remove('btn-success');
|
||||
button.classList.add('btn-primary');
|
||||
button.disabled = false;
|
||||
userDefaultsModal.hide();
|
||||
}, 1000);
|
||||
} else {
|
||||
if ("error" in req.response) {
|
||||
button.textContent = req.response["error"];
|
||||
} else if (("policy" in req.response) || ("homescreen" in req.response)) {
|
||||
button.textContent = "Failed (Check JS Console)";
|
||||
} else {
|
||||
button.textContent = "Failed";
|
||||
}
|
||||
button.classList.remove('btn-primary');
|
||||
button.classList.add('btn-danger');
|
||||
setTimeout(function() {
|
||||
@ -708,8 +783,6 @@ document.getElementById('storeDefaults').onclick = function () {
|
||||
}
|
||||
};
|
||||
req.send(JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var ombiDefaultsModal = '';
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -150,16 +150,24 @@
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="defaultsTitle">New user defaults</h5>
|
||||
<h5 class="modal-title" id="defaultsTitle"></h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Create an account and configure it to your liking, then choose it from below to store the settings as a template for all new users.</p>
|
||||
<p id="userDefaultsDescription"></p>
|
||||
<div class="mb-3" id="defaultsSourceSection">
|
||||
<label for="defaultsSource">Use settings from:</label>
|
||||
<select class="form-select" id="defaultsSource" aria-label="User settings source">
|
||||
<option value="userTemplate" selected>Use existing user template</option>
|
||||
<option value="fromUser">Source from existing user</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="defaultUserRadios"></div>
|
||||
<div class="checkbox">
|
||||
<label><input type="checkbox" value="" style="margin-right: 1rem;" id="storeDefaultHomescreen" checked>Store homescreen layout</label>
|
||||
<input type="checkbox" value="" style="margin-right: 1rem;" id="storeDefaultHomescreen" checked>
|
||||
<label for="storeDefaultHomescreen" id="storeHomescreenLabel"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer" id="defaultsFooter">
|
||||
@ -240,7 +248,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="pageContainer">
|
||||
<h1><a class="text-button">Invites </a></h1>
|
||||
<h1><a id="invitesTabButton" class="text-button">invites </a><a id="accountsTabButton" class="text-button text-muted">accounts</a></h1>
|
||||
<div class="btn-group" role="group" id="headerButtons">
|
||||
<button type="button" class="btn btn-primary" id="openSettings">
|
||||
Settings <i class="fa fa-cog"></i>
|
||||
@ -249,7 +257,8 @@
|
||||
Logout <i class="fa fa-sign-out"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card mb-3 linkGroup">
|
||||
<div id="invitesTab">
|
||||
<div class="card mb-3 tabGroup">
|
||||
<div class="card-header">Current Invites</div>
|
||||
<ul class="list-group list-group-flush" id="invites">
|
||||
</ul>
|
||||
@ -322,6 +331,34 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="accountsTab" class="unfocused">
|
||||
<div class="card mb-3 tabGroup">
|
||||
<div class="card-header">
|
||||
Accounts
|
||||
<div class="btn-group d-flex float-right" role="group" aria-label="Account control buttons">
|
||||
<button type="button" class="btn btn-secondary">Add User</button>
|
||||
<button type="button" class="btn btn-primary unfocused" id="accountsTabSetDefaults">Set Defaults</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover table-striped table-borderless">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"><input class="form-check-input" type="checkbox" value="" id="selectAll"></th>
|
||||
<th scope="col">Username</th>
|
||||
<th scope="col">Email Address</th>
|
||||
<th scope="col">Last Active</th>
|
||||
<th scope="col">Admin?</th>
|
||||
<th scope="col"><i>View settings</i></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="accountsList">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="contactBox">
|
||||
<p>{{ .contactMessage }}</p>
|
||||
</div>
|
||||
@ -401,6 +438,7 @@
|
||||
const notifications_enabled = false;
|
||||
{{ end }}
|
||||
</script>
|
||||
<script src="accounts.js"></script>
|
||||
<script src="admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
3
main.go
3
main.go
@ -322,7 +322,7 @@ func start(asDaemon, firstCall bool) {
|
||||
app.storage.policy_path = app.config.Section("files").Key("user_template").String()
|
||||
app.storage.loadPolicy()
|
||||
// app.storage.configuration_path = filepath.Join(app.data_path, "user_configuration.json")
|
||||
app.storage.policy_path = app.config.Section("files").Key("user_configuration").String()
|
||||
app.storage.configuration_path = app.config.Section("files").Key("user_configuration").String()
|
||||
app.storage.loadConfiguration()
|
||||
// app.storage.displayprefs_path = filepath.Join(app.data_path, "user_displayprefs.json")
|
||||
app.storage.displayprefs_path = app.config.Section("files").Key("user_displayprefs").String()
|
||||
@ -442,6 +442,7 @@ func start(asDaemon, firstCall bool) {
|
||||
api.GET("/getUsers", app.GetUsers)
|
||||
api.POST("/modifyUsers", app.ModifyEmails)
|
||||
api.POST("/setDefaults", app.SetDefaults)
|
||||
api.POST("/applySettings", app.ApplySettings)
|
||||
api.GET("/getConfig", app.GetConfig)
|
||||
api.POST("/modifyConfig", app.ModifyConfig)
|
||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||
|
@ -10,7 +10,7 @@ h1 {
|
||||
/*margin: 20%;*/
|
||||
margin-bottom: 5%;
|
||||
}
|
||||
.linkGroup {
|
||||
.tabGroup {
|
||||
/*margin: 20%;*/
|
||||
margin-bottom: 5%;
|
||||
margin-top: 5%;
|
||||
@ -84,6 +84,7 @@ body.modal-open {
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
font-family: inherit;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,3 +95,11 @@ body.modal-open {
|
||||
.text-button {
|
||||
@extend %scut-link-unstyled;
|
||||
}
|
||||
|
||||
.text-button:hover {
|
||||
@extend %scut-link-unstyled;
|
||||
}
|
||||
|
||||
.unfocused {
|
||||
display: none;
|
||||
}
|
||||
|
@ -1,2 +1,15 @@
|
||||
@import "../../node_modules/bootstrap4/scss/bootstrap";
|
||||
|
||||
.icon-button {
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
.icon-button:hover {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.icon-button:active {
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
@import "../base.scss";
|
||||
|
@ -1,2 +1,15 @@
|
||||
@import "../../node_modules/bootstrap/scss/bootstrap";
|
||||
|
||||
.icon-button {
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
.icon-button:hover {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.icon-button:active {
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
@import "../base.scss";
|
||||
|
Loading…
Reference in New Issue
Block a user