mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2025-08-23 18:31:43 +02:00
Merge branch 'aa-framework-js-functions' into 'master'
[ADD] JS functions to the AA framework See merge request allianceauth/allianceauth!1747
This commit is contained in:
commit
3c1bae463e
@ -15,6 +15,10 @@
|
||||
ul#nav-right:has(li) + ul#nav-right-character-control > li:first-child {
|
||||
display: list-item !important;
|
||||
}
|
||||
|
||||
form.is-submitting button[type="submit"] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 991px) {
|
||||
|
@ -0,0 +1,300 @@
|
||||
/**
|
||||
* Functions and utilities for the Alliance Auth framework.
|
||||
*/
|
||||
|
||||
/* jshint -W097 */
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Checks if the given item is an array.
|
||||
*
|
||||
* @usage
|
||||
* ```javascript
|
||||
* if (isArray(someVariable)) {
|
||||
* console.log('This is an array');
|
||||
* } else {
|
||||
* console.log('This is not an array');
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param {*} item - The item to check.
|
||||
* @returns {boolean} True if the item is an array, false otherwise.
|
||||
*/
|
||||
const isArray = (item) => {
|
||||
return Array.isArray(item);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the given item is a plain object, excluding arrays and dates.
|
||||
*
|
||||
* @usage
|
||||
* ```javascript
|
||||
* if (isObject(someVariable)) {
|
||||
* console.log('This is a plain object');
|
||||
* } else {
|
||||
* console.log('This is not a plain object');
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param {*} item - The item to check.
|
||||
* @returns {boolean} True if the item is a plain object, false otherwise.
|
||||
*/
|
||||
const isObject = (item) => {
|
||||
return (
|
||||
item && typeof item === 'object' && !isArray(item) && !(item instanceof Date)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch data from an ajax URL
|
||||
*
|
||||
* Do not call this function directly, use `fetchGet` or `fetchPost` instead.
|
||||
*
|
||||
* @param {string} url The URL to fetch data from
|
||||
* @param {string} method The HTTP method to use for the request (default: 'get')
|
||||
* @param {string|null} csrfToken The CSRF token to include in the request headers (default: null)
|
||||
* @param {string|null} payload The payload (JSON|Object) to send with the request (default: null)
|
||||
* @param {boolean} responseIsJson Whether the response is expected to be JSON or not (default: true)
|
||||
* @returns {Promise<string>} The fetched data
|
||||
* @throws {Error} Throws an error when:
|
||||
* - The method is not valid (only `get` and `post` are allowed).
|
||||
* - The CSRF token is required but not provided for POST requests.
|
||||
* - The payload is not an object when using POST method.
|
||||
* - The response status is not OK (HTTP 200-299).
|
||||
* - There is a network error or if the response cannot be parsed as JSON.
|
||||
*/
|
||||
const _fetchAjaxData = async ({
|
||||
url,
|
||||
method = 'get',
|
||||
csrfToken = null,
|
||||
payload = null,
|
||||
responseIsJson = true
|
||||
}) => {
|
||||
const normalizedMethod = method.toLowerCase();
|
||||
|
||||
// Validate the method
|
||||
const validMethods = ['get', 'post'];
|
||||
|
||||
if (!validMethods.includes(normalizedMethod)) {
|
||||
throw new Error(`Invalid method: ${method}. Valid methods are: get, post`);
|
||||
}
|
||||
|
||||
const headers = {};
|
||||
|
||||
// Set headers based on response type
|
||||
if (responseIsJson) {
|
||||
headers['Accept'] = 'application/json'; // jshint ignore:line
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
|
||||
let requestUrl = url;
|
||||
let body = null;
|
||||
|
||||
if (normalizedMethod === 'post') {
|
||||
if (!csrfToken) {
|
||||
throw new Error('CSRF token is required for POST requests');
|
||||
}
|
||||
|
||||
headers['X-CSRFToken'] = csrfToken;
|
||||
|
||||
if (payload !== null && !isObject(payload)) {
|
||||
throw new Error('Payload must be an object when using POST method');
|
||||
}
|
||||
|
||||
body = payload ? JSON.stringify(payload) : null;
|
||||
} else if (normalizedMethod === 'get' && payload) {
|
||||
const queryParams = new URLSearchParams(payload).toString(); // jshint ignore:line
|
||||
|
||||
requestUrl += (url.includes('?') ? '&' : '?') + queryParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an error with a formatted message.
|
||||
*
|
||||
* @param {Response} response The error object containing the message to throw.
|
||||
*/
|
||||
const throwHTTPStatusError = (response) => {
|
||||
throw new Error(`Error: ${response.status} - ${response.statusText}`);
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(requestUrl, {
|
||||
method: method.toUpperCase(),
|
||||
headers: headers,
|
||||
body: body
|
||||
});
|
||||
|
||||
/**
|
||||
* Throws an error if the response status is not OK (HTTP 200-299).
|
||||
* This is used to handle HTTP errors gracefully.
|
||||
*/
|
||||
if (!response.ok) {
|
||||
throwHTTPStatusError(response);
|
||||
}
|
||||
|
||||
return responseIsJson ? await response.json() : await response.text();
|
||||
} catch (error) {
|
||||
// Log the error message to the console
|
||||
console.log(`Error: ${error.message}`);
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch data from an ajax URL using the GET method.
|
||||
* This function is a wrapper around _fetchAjaxData to simplify GET requests.
|
||||
*
|
||||
* @usage
|
||||
* ```javascript
|
||||
* fetchGet({
|
||||
* url: url,
|
||||
* responseIsJson: false
|
||||
* }).then((data) => {
|
||||
* // Process the fetched data
|
||||
* }).catch((error) => {
|
||||
* console.error(`Error: ${error.message}`);
|
||||
*
|
||||
* // Handle the error appropriately
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param {string} url The URL to fetch data from
|
||||
* @param {string|null} payload The payload (JSON) to send with the request (default: null)
|
||||
* @param {boolean} responseIsJson Whether the response is expected to be JSON or not (default: true)
|
||||
* @return {Promise<string>} The fetched data
|
||||
*/
|
||||
const fetchGet = async ({
|
||||
url,
|
||||
payload = null,
|
||||
responseIsJson = true
|
||||
}) => {
|
||||
return await _fetchAjaxData({
|
||||
url: url,
|
||||
method: 'get',
|
||||
payload: payload,
|
||||
responseIsJson: responseIsJson
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch data from an ajax URL using the POST method.
|
||||
* This function is a wrapper around _fetchAjaxData to simplify POST requests.
|
||||
* It requires a CSRF token for security purposes.
|
||||
*
|
||||
* @usage
|
||||
* ```javascript
|
||||
* fetchPost({
|
||||
* url: url,
|
||||
* csrfToken: csrfToken,
|
||||
* payload: {
|
||||
* key: 'value',
|
||||
* anotherKey: 'anotherValue'
|
||||
* },
|
||||
* responseIsJson: true
|
||||
* }).then((data) => {
|
||||
* // Process the fetched data
|
||||
* }).catch((error) => {
|
||||
* console.error(`Error: ${error.message}`);
|
||||
*
|
||||
* // Handle the error appropriately
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param {string} url The URL to fetch data from
|
||||
* @param {string|null} csrfToken The CSRF token to include in the request headers (default: null)
|
||||
* @param {string|null} payload The payload (JSON) to send with the request (default: null)
|
||||
* @param {boolean} responseIsJson Whether the response is expected to be JSON or not (default: true)
|
||||
* @return {Promise<string>} The fetched data
|
||||
*/
|
||||
const fetchPost = async ({
|
||||
url,
|
||||
csrfToken,
|
||||
payload = null,
|
||||
responseIsJson = true
|
||||
}) => {
|
||||
return await _fetchAjaxData({
|
||||
url: url,
|
||||
method: 'post',
|
||||
csrfToken: csrfToken,
|
||||
payload: payload,
|
||||
responseIsJson: responseIsJson
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Recursively merges properties from source objects into a target object. If a property at the current level is an object,
|
||||
* and both target and source have it, the property is merged. Otherwise, the source property overwrites the target property.
|
||||
* This function does not modify the source objects and prevents prototype pollution by not allowing __proto__, constructor,
|
||||
* and prototype property names.
|
||||
*
|
||||
* @usage
|
||||
* ```javascript
|
||||
* const target = {a: 1, b: {c: 2}};
|
||||
* const source1 = {b: {d: 3}, e: 4 };
|
||||
* const source2 = {a: 5, b: {c: 6}};
|
||||
*
|
||||
* const merged = objectDeepMerge(target, source1, source2);
|
||||
*
|
||||
* console.log(merged); // {a: 5, b: {c: 6, d: 3}, e: 4}
|
||||
* ```
|
||||
*
|
||||
* @param {Object} target The target object to merge properties into.
|
||||
* @param {...Object} sources One or more source objects from which to merge properties.
|
||||
* @returns {Object} The target object after merging properties from sources.
|
||||
*/
|
||||
function objectDeepMerge (target, ...sources) {
|
||||
if (!sources.length) {
|
||||
return target;
|
||||
}
|
||||
|
||||
// Iterate through each source object without modifying the `sources` array.
|
||||
sources.forEach(source => {
|
||||
if (isObject(target) && isObject(source)) {
|
||||
for (const key in source) {
|
||||
if (isObject(source[key])) {
|
||||
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
||||
continue; // Skip potentially dangerous keys to prevent prototype pollution.
|
||||
}
|
||||
|
||||
if (!target[key] || !isObject(target[key])) {
|
||||
target[key] = {};
|
||||
}
|
||||
|
||||
objectDeepMerge(target[key], source[key]);
|
||||
} else {
|
||||
target[key] = source[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the document is ready …
|
||||
*/
|
||||
$(document).ready(() => {
|
||||
/**
|
||||
* Prevent double form submits by adding a class to the form
|
||||
* when it is submitted.
|
||||
*
|
||||
* This class can be used to show a visual indicator that the form is being
|
||||
* submitted, such as a spinner.
|
||||
*
|
||||
* This is useful to prevent users from double-clicking the submit button
|
||||
* and submitting the form multiple times.
|
||||
*/
|
||||
document.querySelectorAll('form').forEach((form) => {
|
||||
form.addEventListener('submit', (e) => {
|
||||
// Prevent if already submitting
|
||||
if (form.classList.contains('is-submitting')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Add class to hook our visual indicator on
|
||||
form.classList.add('is-submitting');
|
||||
});
|
||||
});
|
||||
});
|
@ -1,4 +1,4 @@
|
||||
/* global notificationUpdateSettings */
|
||||
/* global notificationUpdateSettings, fetchGet */
|
||||
|
||||
/**
|
||||
* This script refreshed the notification icon in the top menu
|
||||
@ -19,22 +19,9 @@ $(() => {
|
||||
* Update the notification icon in the top menu
|
||||
*/
|
||||
const updateNotificationIcon = () => {
|
||||
fetch(userNotificationCountViewUrl)
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
throw new Error('Something went wrong');
|
||||
})
|
||||
.then((responseJson) => {
|
||||
const unreadCount = responseJson.unread_count;
|
||||
|
||||
if (unreadCount > 0) {
|
||||
elementNotificationIcon.addClass('text-danger');
|
||||
} else {
|
||||
elementNotificationIcon.removeClass('text-danger');
|
||||
}
|
||||
fetchGet({url: userNotificationCountViewUrl})
|
||||
.then((data) => {
|
||||
elementNotificationIcon.toggleClass('text-danger', data.unread_count > 0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(`Failed to load HTMl to render notifications item. Error: ${error.message}`);
|
||||
|
@ -8,30 +8,22 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const elemCard = document.getElementById("esi-alert");
|
||||
const elemMessage = document.getElementById("esi-data");
|
||||
const elemCode = document.getElementById("esi-code");
|
||||
const elemCard = document.getElementById('esi-alert');
|
||||
const elemMessage = document.getElementById('esi-data');
|
||||
const elemCode = document.getElementById('esi-code');
|
||||
|
||||
fetch('{% url "authentication:esi_check" %}')
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
throw new Error("Something went wrong");
|
||||
})
|
||||
.then((responseJson) => {
|
||||
console.log("ESI Check: ", JSON.stringify(responseJson, null, 2));
|
||||
fetchGet({url: '{% url "authentication:esi_check" %}'})
|
||||
.then((data) => {
|
||||
console.log('ESI Check: ', JSON.stringify(data, null, 2));
|
||||
|
||||
const status = responseJson.status;
|
||||
if (status !== 200) {
|
||||
elemCode.textContent = status
|
||||
elemMessage.textContent = responseJson.data.error;
|
||||
new bootstrap.Collapse(elemCard, {
|
||||
toggle: true
|
||||
})
|
||||
if (data.status !== 200) {
|
||||
elemCode.textContent = data.status;
|
||||
elemMessage.textContent = data.data.error;
|
||||
|
||||
new bootstrap.Collapse(elemCard, {toggle: true});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
console.error('Error fetching ESI check:', error);
|
||||
});
|
||||
</script>
|
||||
|
@ -136,34 +136,24 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const elemRunning = document.getElementById("task-counts");
|
||||
const elemQueued = document.getElementById("queued-tasks-count");
|
||||
const elemRunning = document.getElementById('task-counts');
|
||||
const elemQueued = document.getElementById('queued-tasks-count');
|
||||
|
||||
fetch('{% url "authentication:task_counts" %}')
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
throw new Error("Something went wrong");
|
||||
})
|
||||
.then((responseJson) => {
|
||||
const running = responseJson.tasks_running;
|
||||
if (running == null) {
|
||||
elemRunning.textContent = "N/A";
|
||||
} else {
|
||||
elemRunning.textContent = running.toLocaleString();
|
||||
}
|
||||
fetchGet({url: '{% url "authentication:task_counts" %}'})
|
||||
.then((data) => {
|
||||
const running = data.tasks_running;
|
||||
const queued = data.tasks_queued;
|
||||
|
||||
const queued = responseJson.tasks_queued;
|
||||
if (queued == null) {
|
||||
elemQueued.textContent = "N/A";
|
||||
} else {
|
||||
elemQueued.textContent = queued.toLocaleString();
|
||||
}
|
||||
const updateTaskCount = (element, value) => {
|
||||
element.textContent = value == null ? 'N/A' : value.toLocaleString();
|
||||
};
|
||||
|
||||
updateTaskCount(elemRunning, running);
|
||||
updateTaskCount(elemQueued, queued);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
elemRunning.textContent = "ERROR";
|
||||
elemQueued.textContent = "ERROR";
|
||||
console.error('Error fetching task queue:', error);
|
||||
|
||||
[elemRunning, elemQueued].forEach(elem => elem.textContent = 'ERROR');
|
||||
});
|
||||
</script>
|
||||
|
@ -23,6 +23,9 @@
|
||||
{% include 'bundles/fontawesome.html' %}
|
||||
{% include 'bundles/auth-framework-css.html' %}
|
||||
|
||||
{% include 'bundles/jquery-js.html' %}
|
||||
{% include 'bundles/auth-framework-js.html' %}
|
||||
|
||||
<style>
|
||||
@media all {
|
||||
.nav-padding {
|
||||
@ -137,8 +140,6 @@
|
||||
})();
|
||||
</script>
|
||||
|
||||
{% include 'bundles/jquery-js.html' %}
|
||||
|
||||
{% theme_js %}
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
|
3
allianceauth/templates/bundles/auth-framework-js.html
Normal file
3
allianceauth/templates/bundles/auth-framework-js.html
Normal file
@ -0,0 +1,3 @@
|
||||
{% load sri %}
|
||||
|
||||
{% sri_static 'allianceauth/framework/js/auth-framework.js' %}
|
@ -13,6 +13,7 @@ The Alliance Auth framework is split into several submodules, each of which is d
|
||||
|
||||
framework/api
|
||||
framework/css
|
||||
framework/js
|
||||
framework/templates
|
||||
framework/svg-sprite
|
||||
:::
|
||||
|
124
docs/development/custom/framework/js.md
Normal file
124
docs/development/custom/framework/js.md
Normal file
@ -0,0 +1,124 @@
|
||||
# JavaScript Framework
|
||||
|
||||
This contains some simple JavaScript functions that are used throughout Alliance Auth,
|
||||
so they can be used by community app developers as well.
|
||||
|
||||
The JS file is already loaded in the base template, so it is globally available.
|
||||
|
||||
## Functions
|
||||
|
||||
The following functions are available in the JavaScript framework:
|
||||
|
||||
### isArray()
|
||||
|
||||
Checks if the given value is an array.
|
||||
|
||||
Usage:
|
||||
|
||||
```javascript
|
||||
/* global isArray */
|
||||
|
||||
if (isArray(someVariable)) {
|
||||
console.log('This is an array');
|
||||
} else {
|
||||
console.log('This is not an array');
|
||||
}
|
||||
```
|
||||
|
||||
### isObject()
|
||||
|
||||
Checks if the given value is an object.
|
||||
|
||||
Usage:
|
||||
|
||||
```javascript
|
||||
/* global isObject */
|
||||
|
||||
if (isObject(someVariable)) {
|
||||
console.log('This is a plain object');
|
||||
} else {
|
||||
console.log('This is not a plain object');
|
||||
}
|
||||
```
|
||||
|
||||
### fetchGet()
|
||||
|
||||
Performs a GET request to the given URL and returns a Promise that resolves with the response data.
|
||||
|
||||
Usage:
|
||||
|
||||
```javascript
|
||||
/* global fetchGet */
|
||||
|
||||
fetchGet({
|
||||
url: url,
|
||||
responseIsJson: false
|
||||
}).then((data) => {
|
||||
// Process the fetched data
|
||||
}).catch((error) => {
|
||||
console.error(`Error: ${error.message}`);
|
||||
|
||||
// Handle the error appropriately
|
||||
});
|
||||
```
|
||||
|
||||
#### fetchGet() Parameters
|
||||
|
||||
- `url`: The URL to fetch data from.
|
||||
- `payload`: Optional data to send with the request. Can be an object or a string.
|
||||
- `responseIsJson`: Optional boolean indicating if the response should be parsed as JSON (default is `true`).
|
||||
|
||||
### fetchPost()
|
||||
|
||||
Performs a POST request to the given URL with the provided data and returns a Promise that resolves with the response data.
|
||||
|
||||
Usage:
|
||||
|
||||
```javascript
|
||||
/* global fetchPost */
|
||||
|
||||
fetchPost({
|
||||
url: url,
|
||||
csrfToken: csrfToken,
|
||||
payload: {
|
||||
key: 'value',
|
||||
anotherKey: 'anotherValue'
|
||||
},
|
||||
responseIsJson: true
|
||||
}).then((data) => {
|
||||
// Process the fetched data
|
||||
}).catch((error) => {
|
||||
console.error(`Error: ${error.message}`);
|
||||
|
||||
// Handle the error appropriately
|
||||
});
|
||||
```
|
||||
|
||||
#### fetchPost() Parameters
|
||||
|
||||
- `url`: The URL to send the POST request to.
|
||||
- `csrfToken`: The CSRF token to include in the request headers.
|
||||
- `payload`: The data as JS object to send with the request.
|
||||
- `responseIsJson`: Optional boolean indicating if the response should be parsed as JSON (default is `true`).
|
||||
|
||||
### objectDeepMerge()
|
||||
|
||||
Recursively merges properties from source objects into a target object. If a property at the current level is an object,
|
||||
and both target and source have it, the property is merged. Otherwise, the source property overwrites the target property.
|
||||
|
||||
This function does not modify the source objects and prevents prototype pollution by not allowing `__proto__`, `constructor`,
|
||||
and `prototype` property names.
|
||||
|
||||
Usage:
|
||||
|
||||
```javascript
|
||||
/* global objectDeepMerge */
|
||||
|
||||
const target = {a: 1, b: {c: 2}};
|
||||
const source1 = {b: {d: 3}, e: 4 };
|
||||
const source2 = {a: 5, b: {c: 6}};
|
||||
|
||||
const merged = objectDeepMerge(target, source1, source2);
|
||||
|
||||
console.log(merged); // {a: 5, b: {c: 6, d: 3}, e: 4}
|
||||
```
|
Loading…
x
Reference in New Issue
Block a user