1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-06-01 15:17:47 +02: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:
Harvey Tindall 2020-09-17 16:51:19 +01:00
parent a8b4842895
commit cd61989495
Signed by: hrfee
GPG Key ID: BBC65952848FB1A2
12 changed files with 481 additions and 122 deletions

87
api.go
View File

@ -575,22 +575,24 @@ func (app *appContext) SetOmbiDefaults(gc *gin.Context) {
}
type defaultsReq struct {
Username string `json:"username"`
Homescreen bool `json:"homescreen"`
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
View 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');
}
})

View File

@ -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,54 +708,81 @@ 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) {
let data = {
'username': radio.id.slice(8),
'homescreen': false};
if (document.getElementById('storeDefaultHomescreen').checked) {
data['homescreen'] = true;
}
let req = new XMLHttpRequest();
req.open("POST", "/setDefaults", true);
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
req.onreadystatechange = function() {
if (this.readyState == 4) {
if (this.status == 200 || this.status == 204) {
button.textContent = "Success";
if (button.classList.contains('btn-danger')) {
button.classList.remove('btn-danger');
} else if (button.classList.contains('btn-primary')) {
button.classList.remove('btn-primary');
}
button.classList.add('btn-success');
button.disabled = false;
setTimeout(function() { userDefaultsModal.hide(); }, 1000);
} else {
button.textContent = "Failed";
button.classList.remove('btn-primary');
button.classList.add('btn-danger');
setTimeout(function() {
let button = document.getElementById('storeDefaults');
button.textContent = "Submit";
button.classList.remove('btn-danger');
button.classList.add('btn-primary');
button.disabled = false;
}, 1000);
}
}
};
req.send(JSON.stringify(data));
id = radio.id.replace('select_', '');
break;
}
}
let route = '/setDefaults';
let data = {
'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", route, true);
req.responseType = 'json';
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
req.onreadystatechange = function() {
if (this.readyState == 4) {
if (this.status == 200 || this.status == 204) {
button.textContent = "Success";
if (button.classList.contains('btn-danger')) {
button.classList.remove('btn-danger');
} else if (button.classList.contains('btn-primary')) {
button.classList.remove('btn-primary');
}
button.classList.add('btn-success');
button.disabled = false;
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() {
let button = document.getElementById('storeDefaults');
button.textContent = "Submit";
button.classList.remove('btn-danger');
button.classList.add('btn-primary');
button.disabled = false;
}, 1000);
}
}
};
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

View File

@ -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">&times;</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,76 +257,105 @@
Logout <i class="fa fa-sign-out"></i>
</button>
</div>
<div class="card mb-3 linkGroup">
<div class="card-header">Current Invites</div>
<ul class="list-group list-group-flush" id="invites">
</ul>
<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>
</div>
<div class="linkForm">
<div class="card mb-3">
<div class="card-header">Generate Invite</div>
<div class="card-body">
<form action="#" method="POST" id="inviteForm" class="container">
<div class="row align-items-start">
<div class="col-sm">
<div class="form-group">
<label for="days">Days</label>
<select class="form-control form-select" id="days" name="days">
</select>
</div>
<div class="form-group">
<label for="hours">Hours</label>
<select class="form-control form-select" id="hours" name="hours">
</select>
</div>
<div class="form-group">
<label for="minutes">Minutes</label>
<select class="form-control form-select" id="minutes" name="minutes">
</select>
</div>
</div>
<div class="col">
<div class="form-group">
<label for="multiUseCount">
Multiple uses
</label>
<div class="input-group">
<div class="input-group-text">
<input class="form-check-input" type="checkbox" onchange="document.getElementById('multiUseCount').disabled = !this.checked; document.getElementById('noUseLimit').disabled = !this.checked" aria-label="Checkbox to allow choice of invite usage limit" name="multiple-uses" id="multiUseEnabled">
</div>
<input type="number" class="form-control" name="remaining-uses" id="multiUseCount">
</div>
</div>
<div class="form-group form-check" style="margin-top: 1rem; margin-bottom: 1rem;">
<input class="form-check-input" type="checkbox" value="" name="no-limit" id="noUseLimit" onchange="document.getElementById('multiUseCount').disabled = this.checked; if (this.checked) { document.getElementById('noLimitWarning').style = 'display: block;' } else { document.getElementById('noLimitWarning').style = 'display: none;'; }">
<label class="form-check-label" for="noUseLimit">
No use limit
</label>
<div id="noLimitWarning" class="form-text" style="display: none;">Warning: Unlimited usage invites pose a risk if published online.</div>
</div>
{{ if .email_enabled }}
<div class="form-group">
<label for="send_to_address">Send invite to address</label>
<div class="input-group">
<div class="input-group-text">
<input class="form-check-input" type="checkbox" onchange="document.getElementById('send_to_address').disabled = !this.checked;" aria-label="Checkbox to allow input of email address" id="send_to_address_enabled">
</div>
<input type="email" class="form-control" placeholder="example@example.com" id="send_to_address" disabled>
</div>
</div>
{{ end }}
</div>
</div>
<div class="row">
<div class="col">
<div class="form-group d-flex float-right">
<button type="submit" id="generateSubmit" class="btn btn-primary" style="margin-top: 1rem;">
Generate
</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="linkForm">
<div class="card mb-3">
<div class="card-header">Generate Invite</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">
<form action="#" method="POST" id="inviteForm" class="container">
<div class="row align-items-start">
<div class="col-sm">
<div class="form-group">
<label for="days">Days</label>
<select class="form-control form-select" id="days" name="days">
</select>
</div>
<div class="form-group">
<label for="hours">Hours</label>
<select class="form-control form-select" id="hours" name="hours">
</select>
</div>
<div class="form-group">
<label for="minutes">Minutes</label>
<select class="form-control form-select" id="minutes" name="minutes">
</select>
</div>
</div>
<div class="col">
<div class="form-group">
<label for="multiUseCount">
Multiple uses
</label>
<div class="input-group">
<div class="input-group-text">
<input class="form-check-input" type="checkbox" onchange="document.getElementById('multiUseCount').disabled = !this.checked; document.getElementById('noUseLimit').disabled = !this.checked" aria-label="Checkbox to allow choice of invite usage limit" name="multiple-uses" id="multiUseEnabled">
</div>
<input type="number" class="form-control" name="remaining-uses" id="multiUseCount">
</div>
</div>
<div class="form-group form-check" style="margin-top: 1rem; margin-bottom: 1rem;">
<input class="form-check-input" type="checkbox" value="" name="no-limit" id="noUseLimit" onchange="document.getElementById('multiUseCount').disabled = this.checked; if (this.checked) { document.getElementById('noLimitWarning').style = 'display: block;' } else { document.getElementById('noLimitWarning').style = 'display: none;'; }">
<label class="form-check-label" for="noUseLimit">
No use limit
</label>
<div id="noLimitWarning" class="form-text" style="display: none;">Warning: Unlimited usage invites pose a risk if published online.</div>
</div>
{{ if .email_enabled }}
<div class="form-group">
<label for="send_to_address">Send invite to address</label>
<div class="input-group">
<div class="input-group-text">
<input class="form-check-input" type="checkbox" onchange="document.getElementById('send_to_address').disabled = !this.checked;" aria-label="Checkbox to allow input of email address" id="send_to_address_enabled">
</div>
<input type="email" class="form-control" placeholder="example@example.com" id="send_to_address" disabled>
</div>
</div>
{{ end }}
</div>
</div>
<div class="row">
<div class="col">
<div class="form-group d-flex float-right">
<button type="submit" id="generateSubmit" class="btn btn-primary" style="margin-top: 1rem;">
Generate
</button>
</div>
</div>
</div>
</form>
<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>
@ -401,6 +438,7 @@
const notifications_enabled = false;
{{ end }}
</script>
<script src="accounts.js"></script>
<script src="admin.js"></script>
</body>
</html>

View File

@ -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) {

View File

@ -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;
}

View File

@ -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";

View File

@ -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";