diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 07a1318aad..938aa7a99c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -119,7 +119,7 @@ jobs: with: php-version: ${{ matrix.php_version }} coverage: none - extensions: mbstring, intl, pdo, pdo_sqlite, sqlite3 + extensions: mbstring, intl, pdo, pdo_sqlite, sqlite3, fileinfo ini-values: date.timezone=UTC tools: composer:2.8 diff --git a/assets/css/easyadmin-theme/forms.css b/assets/css/easyadmin-theme/forms.css index 29852b8d99..e0d196d25b 100644 --- a/assets/css/easyadmin-theme/forms.css +++ b/assets/css/easyadmin-theme/forms.css @@ -130,7 +130,6 @@ body.ea-dark-scheme .form-widget .form-select { .form-widget input.form-control:focus, .form-widget textarea.form-control:focus, .form-widget .form-select:focus, -.form-widget .custom-file-input:focus ~ .custom-file-label, .form-widget input.form-check-input:focus { border-color: var(--form-input-hover-border-color); box-shadow: var(--form-input-hover-shadow); @@ -190,31 +189,6 @@ label.form-check-label { margin-inline-start: 15px; } -/* in Bootstrap custom file widgets, the label is used to simulate the file input */ -.field-file .custom-file, -.field-file .custom-file-input { - block-size: 30px; -} -.field-file .custom-file label.custom-file-label { - block-size: 30px; - margin: 0; - max-inline-size: 350px; - overflow: hidden; - padding: 3px 7px 5px; - text-align: left; -} -.field-file .custom-file label.custom-file-label:after { - color: var(--text-color); - content: "\f07c"; - display: inline-block; - font-family: "Font Awesome 6 Free", sans-serif; - font-size: 17px; - block-size: 28px; - line-height: 28px; - padding: 0 8px; - vertical-align: middle; -} - /* Date-time widgets */ .field-date .form-widget, .field-time .form-widget, @@ -624,104 +598,145 @@ form .invalid-feedback > .d-block + .d-block { } /* fileupload widgets */ -.ea-fileupload .custom-file { - block-size: 30px; -} -.ea-fileupload .input-group { - flex-wrap: nowrap; +.ea-fileupload { + display: flex; + flex-direction: column; + gap: 0.5rem; } -.ea-fileupload .input-group .btn, -.ea-fileupload .input-group .btn:hover { - background: var(--form-input-group-text-bg); - box-shadow: none !important; - border-radius: 0; - color: var(--text-color); - font-size: 17px; - block-size: 28px; - line-height: 28px; - margin: 0; - padding: 0 8px; - vertical-align: middle; + +/* File Upload - Toolbar (Add button + Clear All) */ +.ea-fileupload-toolbar { + display: flex; + align-items: center; + gap: 0.75rem; } -.ea-fileupload .input-group .btn:first-child, -.ea-fileupload .input-group .btn:hover:first-child { - margin-inline-start: 5px; +.ea-fileupload-add-btn { + display: inline-flex; + align-items: center; + gap: 0.375rem; + font-size: 0.875rem; + font-weight: 500; + padding: 0.375rem 0.875rem; } -.ea-fileupload .input-group .btn:last-child, -.ea-fileupload .input-group .btn:hover:last-child { - border-start-end-radius: var(--border-radius); - border-end-end-radius: var(--border-radius); +.ea-fileupload-add-btn .btn-icon { + inline-size: 0.875rem; + block-size: 0.875rem; } - -.ea-fileupload .custom-file-input { +.ea-fileupload-clear-all-btn { + margin-inline-start: auto; + background: none; + border: none; + color: var(--text-color); + font-size: 0.875rem; + font-weight: 500; cursor: pointer; - block-size: calc(1.5em + .75rem + 2px); - margin: 0; - overflow: hidden; - opacity: 0; - position: relative; - inline-size: 100%; - z-index: 2; + padding: 0.375rem 0; +} +.ea-fileupload-clear-all-btn:hover { + text-decoration: underline; } -.ea-fileupload .custom-file-label { - background: var(--form-control-bg); - border-radius: var(--border-radius); - border: 1px solid var(--form-input-border-color); - box-shadow: var(--form-input-shadow); - color: var(--form-input-text-color); - block-size: 30px; - inset-inline-start: 0; - margin: 0 !important; - overflow: hidden; - padding: 3px 40px 3px 7px !important; - position: absolute; - text-align: left !important; - text-overflow: ellipsis; - inset-block-start: 0; - white-space: nowrap; - inline-size: 100% !important; +/* File Upload - Cards container */ +.ea-fileupload-cards { + display: flex; + flex-direction: column; + gap: 0.375rem; } -.ea-fileupload .custom-file-label::after { +.ea-fileupload-cards:empty { display: none; } -.ea-fileupload .input-group-text { - background: var(--form-input-group-text-bg); - border: 1px solid var(--form-input-border-color); - box-shadow: none; - color: var(--text-muted); - block-size: 30px; - padding: 7px 0 7px 7px; - position: absolute; - inset-inline-end: 0; - z-index: 3; +/* File Upload - Individual card */ +.ea-fileupload-card { + display: flex; + align-items: center; + gap: 0.625rem; + border: 1px solid var(--border-tertiary-color); + border-radius: var(--border-radius); + padding: 0.5rem 0.75rem; } -.ea-fileupload .fileupload-list { - block-size: auto; - margin-block-start: 7px; - padding: 0; - border-color: var(--form-input-border-color); +/* File Upload - Card preview (left side: icon or thumbnail) */ +.ea-fileupload-card-preview { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + inline-size: 2.25rem; + block-size: 2.25rem; +} +.ea-fileupload-card-thumbnail { + inline-size: 2.25rem; + block-size: 2.25rem; + object-fit: cover; + border-radius: calc(var(--border-radius) * 0.5); } -.ea-fileupload .fileupload-list .fileupload-table { +.ea-fileupload-card .ea-fileupload-card-icon { + inline-size: 2rem; + block-size: 2rem; +} +.ea-fileupload-card .ea-fileupload-card-icon svg { inline-size: 100%; + block-size: 100%; + max-inline-size: unset; + max-block-size: unset; } -.ea-fileupload .fileupload-list .fileupload-table td { - padding: 3px 7px; - border-radius: 3px; + +/* File Upload - Card info (center: name + size) */ +.ea-fileupload-card-info { + flex: 1; + min-inline-size: 0; + display: flex; + flex-direction: column; } -.ea-fileupload .fileupload-list .fileupload-table td:first-child { +.ea-fileupload-card-name { + font-weight: 600; + font-size: 0.875rem; + line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - max-inline-size: 300px; + color: var(--text-color); } -.ea-fileupload .fileupload-list .fileupload-table tr:nth-child(odd) td { - background-color: var(--form-control-bg); +.ea-fileupload-card-size { + font-size: 0.75rem; + line-height: 1.3; + color: var(--text-secondary-color); } -.ea-fileupload .fileupload-list .fileupload-table td.file-size { - color: var(--text-muted); + +/* File Upload - Card actions (right side) */ +.ea-fileupload-card-actions { + flex-shrink: 0; + display: flex; + align-items: center; + gap: 0.5em; +} +.ea-fileupload-card-actions .ea-fileupload-action-btn { + display: inline-flex; + align-items: center; + justify-content: center; + background: none; + border: none; + color: var(--text-secondary-color); + cursor: pointer; + padding: 0.5em; + border-radius: var(--border-radius); + text-decoration: none; + line-height: 1; +} +.ea-fileupload-card-actions .ea-fileupload-action-btn:hover { + color: var(--text-color); + background-color: var(--secondary-bg); +} +.ea-fileupload-card-actions .ea-fileupload-action-btn .icon { + inline-size: 1.125em; + block-size: 1.125em; +} +.ea-fileupload-card-actions .ea-fileupload-action-btn svg { + inline-size: 100%; + block-size: 100%; + max-inline-size: unset; + max-block-size: unset; } /* Image/file vich uploads */ @@ -786,17 +801,3 @@ form .invalid-feedback > .d-block + .d-block { text-align: right; inset-block-start: 0; } - -/* Fix "Browse" button for regular file inputs */ -.form-control::file-selector-button, -.form-control::-webkit-file-upload-button { - color: var(--button-secondary-color); - background-color: var(--button-secondary-bg); - box-shadow: var(--button-shadow); -} - -.form-control:hover:not(:disabled):not([readonly])::file-selector-button, -.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button { - background-color: var(--button-secondary-bg); - box-shadow: var(--button-hover-shadow); -} diff --git a/assets/icons/filetypes/LICENSE.txt b/assets/icons/filetypes/LICENSE.txt new file mode 100644 index 0000000000..dd4a044fe6 --- /dev/null +++ b/assets/icons/filetypes/LICENSE.txt @@ -0,0 +1,26 @@ +Icons from https://onedrive.live.com/ + +Author: +Microsoft Corporation + +Taken from: +https://commons.wikimedia.org/wiki/File:.xlsx_icon.svg +https://commons.wikimedia.org/wiki/File:.docx_icon.svg +https://commons.wikimedia.org/wiki/File:.pptx_icon_(2019).svg +https://commons.wikimedia.org/wiki/File:.pdf_OneDrive_icon.svg +https://commons.wikimedia.org/wiki/File:Vector_OneDrive_icon.svg +https://commons.wikimedia.org/wiki/File:Photo_OneDrive_icon.svg +https://commons.wikimedia.org/wiki/File:Video_OneDrive_icon.svg +https://commons.wikimedia.org/wiki/File:.txt_OneDrive_icon.svg +https://commons.wikimedia.org/wiki/File:.rtf_OneDrive_icon.svg +https://commons.wikimedia.org/wiki/File:Audio_OneDrive_icon.svg +https://commons.wikimedia.org/wiki/File:Code_OneDrive_icon.svg +https://commons.wikimedia.org/wiki/File:Html_OneDrive_icon.svg +https://commons.wikimedia.org/wiki/File:Generic_File_OneDrive_icon.svg +https://commons.wikimedia.org/wiki/File:.zip_OneDrive_icon.svg +https://commons.wikimedia.org/wiki/File:OneDrive_Folder_Icon.svg + +Licensing: +These logo images consist only of simple geometric shapes or text. +They do not meet the threshold of originality needed for copyright protection, +and are therefore in the public domain. diff --git a/assets/icons/filetypes/audio.svg b/assets/icons/filetypes/audio.svg new file mode 100644 index 0000000000..04a212d2b4 --- /dev/null +++ b/assets/icons/filetypes/audio.svg @@ -0,0 +1,10 @@ + + + diff --git a/assets/icons/filetypes/code.svg b/assets/icons/filetypes/code.svg new file mode 100644 index 0000000000..51bc775f17 --- /dev/null +++ b/assets/icons/filetypes/code.svg @@ -0,0 +1,10 @@ + + + diff --git a/assets/icons/filetypes/document.svg b/assets/icons/filetypes/document.svg new file mode 100644 index 0000000000..7131b44bb8 --- /dev/null +++ b/assets/icons/filetypes/document.svg @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/assets/icons/filetypes/generic.svg b/assets/icons/filetypes/generic.svg new file mode 100644 index 0000000000..4e5febd48e --- /dev/null +++ b/assets/icons/filetypes/generic.svg @@ -0,0 +1,10 @@ + + + diff --git a/assets/icons/filetypes/image.svg b/assets/icons/filetypes/image.svg new file mode 100644 index 0000000000..77cbb2ac53 --- /dev/null +++ b/assets/icons/filetypes/image.svg @@ -0,0 +1,10 @@ + + + diff --git a/assets/icons/filetypes/pdf.svg b/assets/icons/filetypes/pdf.svg new file mode 100644 index 0000000000..e21010b9f4 --- /dev/null +++ b/assets/icons/filetypes/pdf.svg @@ -0,0 +1,10 @@ + + + diff --git a/assets/icons/filetypes/presentation.svg b/assets/icons/filetypes/presentation.svg new file mode 100644 index 0000000000..ea7acec439 --- /dev/null +++ b/assets/icons/filetypes/presentation.svg @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/assets/icons/filetypes/spreadsheet.svg b/assets/icons/filetypes/spreadsheet.svg new file mode 100644 index 0000000000..89c503e02a --- /dev/null +++ b/assets/icons/filetypes/spreadsheet.svg @@ -0,0 +1,11 @@ + + + diff --git a/assets/icons/filetypes/vector.svg b/assets/icons/filetypes/vector.svg new file mode 100644 index 0000000000..c29c9ccdce --- /dev/null +++ b/assets/icons/filetypes/vector.svg @@ -0,0 +1,11 @@ + + + diff --git a/assets/icons/filetypes/video.svg b/assets/icons/filetypes/video.svg new file mode 100644 index 0000000000..e8757beba2 --- /dev/null +++ b/assets/icons/filetypes/video.svg @@ -0,0 +1,10 @@ + + + diff --git a/assets/icons/filetypes/zip.svg b/assets/icons/filetypes/zip.svg new file mode 100644 index 0000000000..a827b5956b --- /dev/null +++ b/assets/icons/filetypes/zip.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/assets/icons/internal/download.svg b/assets/icons/internal/download.svg new file mode 100644 index 0000000000..a4c6786bbe --- /dev/null +++ b/assets/icons/internal/download.svg @@ -0,0 +1 @@ + diff --git a/assets/js/field-file-upload.js b/assets/js/field-file-upload.js index 46a9874ab5..c50b70153c 100644 --- a/assets/js/field-file-upload.js +++ b/assets/js/field-file-upload.js @@ -1,92 +1,299 @@ -import { toggleVisibilityClasses } from './helpers'; - -const eaFileUploadHandler = (event) => { - document.querySelectorAll('.ea-fileupload input[type="file"]').forEach((fileUploadElement) => { - new FileUploadField(fileUploadElement); +document.addEventListener('DOMContentLoaded', () => { + document.querySelectorAll('[data-ea-fileupload-field]').forEach((container) => { + new FileUploadField(container); }); -}; +}); -window.addEventListener('DOMContentLoaded', eaFileUploadHandler); -document.addEventListener('ea.collection.item-added', eaFileUploadHandler); +document.addEventListener('ea.collection.item-added', (event) => { + event.detail.newElement.querySelectorAll('[data-ea-fileupload-field]').forEach((container) => { + new FileUploadField(container); + }); +}); class FileUploadField { - #fieldContainerElement; + #container; + #fileInput; + #cardsContainer; + #addButton; + #clearAllButton; + #deleteCheckbox; + #deletedFilesInput; + #isMultiple; + /** @type {File[]} Accumulated selected files in multiple mode */ + #selectedFiles = []; - constructor(field) { - this.field = field; - this.#fieldContainerElement = this.field.closest('.ea-fileupload'); - this.field.addEventListener('change', this.#updateField.bind(this)); + constructor(container) { + this.#container = container; + this.#fileInput = container.querySelector('[data-ea-fileupload-input]'); + this.#cardsContainer = container.querySelector('[data-ea-fileupload-cards]'); + this.#addButton = container.querySelector('[data-ea-fileupload-add]'); + this.#clearAllButton = container.querySelector('[data-ea-fileupload-clear-all]'); + this.#deleteCheckbox = container.querySelector('input[type=checkbox].form-check-input'); + this.#deletedFilesInput = container.querySelector('[id$="_deleted_files"]'); + this.#isMultiple = container.hasAttribute('data-multiple'); - const deleteButton = this.#getFieldDeleteButton(); - if (deleteButton) { - deleteButton.addEventListener('click', this.#resetField.bind(this)); - } + this.#bindEvents(); + } + + #bindEvents() { + this.#addButton?.addEventListener('click', () => this.#fileInput.click()); + + this.#fileInput.addEventListener('change', () => this.#onFilesSelected()); + + this.#clearAllButton?.addEventListener('click', () => this.#clearAll()); + + this.#cardsContainer?.addEventListener('click', (e) => { + const deleteBtn = e.target.closest('[data-ea-fileupload-delete-card]'); + if (deleteBtn) { + this.#deleteCard(deleteBtn.closest('[data-ea-fileupload-card]')); + } + }); } - #updateField() { - if (0 === this.field.files.length) { + #onFilesSelected() { + if (0 === this.#fileInput.files.length) { return; } - const filename = - 1 === this.field.files.length - ? this.field.files[0].name - : `${this.field.files.length} ${this.field.getAttribute('data-files-label')}`; + const newFiles = Array.from(this.#fileInput.files); - let totalSizeInBytes = 0; - for (const file of this.field.files) { - totalSizeInBytes += file.size; + if (!this.#isMultiple) { + // single mode: remove any existing cards and replace + this.#revokeAllObjectUrls(); + this.#removeAllCards(); + this.#selectedFiles = newFiles; + } else { + // multiple mode: accumulate files across multiple selections + this.#selectedFiles = this.#selectedFiles.concat(newFiles); + this.#syncFileInput(); } - this.#getFieldCustomInput().innerHTML = filename; - this.#getFieldDeleteButton().style.display = 'block'; - this.#getFieldSizeLabel().childNodes.forEach((fileUploadFileSizeLabelChild) => { - if (fileUploadFileSizeLabelChild.nodeType === Node.TEXT_NODE) { - this.#getFieldSizeLabel().removeChild(fileUploadFileSizeLabelChild); + // create cards for the newly selected files + const startIndex = this.#selectedFiles.length - newFiles.length; + for (let i = 0; i < newFiles.length; i++) { + const objectUrl = URL.createObjectURL(newFiles[i]); + const card = this.#createCardElement(newFiles[i], objectUrl, startIndex + i); + this.#cardsContainer.appendChild(card); + } + + this.#updateToolbarVisibility(); + } + + #deleteCard(cardElement) { + if (!cardElement) { + return; + } + + const fileName = cardElement.getAttribute('data-filename'); + + if (fileName) { + // existing server-side file: track for per-file deletion + this.#addToDeletedFiles(fileName); + } else { + // newly selected file: remove from our tracked list + const fileIndex = Number.parseInt(cardElement.getAttribute('data-file-index'), 10); + if (!Number.isNaN(fileIndex) && fileIndex >= 0 && fileIndex < this.#selectedFiles.length) { + this.#selectedFiles.splice(fileIndex, 1); + this.#syncFileInput(); + this.#reindexNewCards(); } - }); - this.#getFieldSizeLabel().prepend(this.#humanizeFileSize(totalSizeInBytes)); + } + + // revoke any object URL to prevent memory leaks + const thumbnail = cardElement.querySelector('.ea-fileupload-card-thumbnail'); + if (thumbnail?.src.startsWith('blob:')) { + URL.revokeObjectURL(thumbnail.src); + } + + cardElement.remove(); + this.#updateToolbarVisibility(); } - #resetField() { - const fieldDeleteCheckbox = this.#fieldContainerElement.querySelector('input[type=checkbox].form-check-input'); - const fieldListOfFiles = this.#fieldContainerElement.querySelector('.fileupload-list'); + #clearAll() { + // mark all existing files for deletion via checkbox + if (this.#deleteCheckbox) { + this.#deleteCheckbox.checked = true; + } + + // clear the per-file deletion tracking + if (this.#deletedFilesInput) { + this.#deletedFilesInput.value = ''; + } + + // clear selected files and file input + this.#selectedFiles = []; + this.#fileInput.value = ''; - if (fieldDeleteCheckbox) { - fieldDeleteCheckbox.checked = true; - fieldDeleteCheckbox.click(); + // revoke object URLs and remove all cards + this.#revokeAllObjectUrls(); + this.#removeAllCards(); + this.#updateToolbarVisibility(); + } + + /** + * Syncs the file input's FileList with our #selectedFiles array using DataTransfer. + */ + #syncFileInput() { + const dt = new DataTransfer(); + for (const file of this.#selectedFiles) { + dt.items.add(file); } - this.field.value = ''; - this.#getFieldCustomInput().innerHTML = ''; - toggleVisibilityClasses(this.#getFieldDeleteButton(), true); + this.#fileInput.files = dt.files; + } + + #addToDeletedFiles(fileName) { + if (!this.#deletedFilesInput) { + return; + } + + let deletedFiles = []; + try { + deletedFiles = JSON.parse(this.#deletedFilesInput.value) || []; + } catch { + deletedFiles = []; + } + + if (!deletedFiles.includes(fileName)) { + deletedFiles.push(fileName); + } + + this.#deletedFilesInput.value = JSON.stringify(deletedFiles); + } - this.#getFieldSizeLabel().childNodes.forEach((fileSizeLabelChild) => { - if (fileSizeLabelChild.nodeType === Node.TEXT_NODE) { - this.#getFieldSizeLabel().removeChild(fileSizeLabelChild); + /** + * Re-indexes all newly-added cards so their data-file-index matches + * their position in the #selectedFiles array. + */ + #reindexNewCards() { + let index = 0; + this.#cardsContainer.querySelectorAll('[data-ea-fileupload-card][data-new-file]').forEach((card) => { + card.setAttribute('data-file-index', String(index++)); + }); + } + + #removeAllCards() { + this.#cardsContainer.querySelectorAll('[data-ea-fileupload-card]').forEach((card) => card.remove()); + } + + #revokeAllObjectUrls() { + this.#cardsContainer.querySelectorAll('.ea-fileupload-card-thumbnail').forEach((thumbnail) => { + if (thumbnail.src.startsWith('blob:')) { + URL.revokeObjectURL(thumbnail.src); } }); + } + + #updateToolbarVisibility() { + const hasCards = this.#cardsContainer.querySelectorAll('[data-ea-fileupload-card]').length > 0; + + if (this.#addButton) { + if (this.#isMultiple) { + this.#addButton.classList.remove('d-none'); + } else { + this.#addButton.classList.toggle('d-none', hasCards); + } + } - if (null !== fieldListOfFiles) { - toggleVisibilityClasses(fieldListOfFiles, true); + if (this.#clearAllButton) { + this.#clearAllButton.classList.toggle('d-none', !hasCards); } } - #humanizeFileSize(bytes) { - const unit = ['B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; - const factor = Math.trunc(Math.floor(Math.log(bytes) / Math.log(1024))); + #createCardElement(file, objectUrl, fileIndex) { + const card = document.createElement('div'); + card.className = 'ea-fileupload-card'; + card.setAttribute('data-ea-fileupload-card', ''); + card.setAttribute('data-new-file', ''); + card.setAttribute('data-file-index', String(fileIndex)); - return Math.trunc(bytes / 1024 ** factor) + unit[factor]; - } + // preview (left side) + const preview = document.createElement('div'); + preview.className = 'ea-fileupload-card-preview'; + + if (file.type.startsWith('image/')) { + const img = document.createElement('img'); + img.src = objectUrl; + img.alt = file.name; + img.className = 'ea-fileupload-card-thumbnail'; + preview.appendChild(img); + } else { + const iconFragment = this.#parseIconHtml(this.#container.getAttribute('data-icon-generic')); + if (iconFragment) { + preview.appendChild(iconFragment); + } + } + card.appendChild(preview); + + // info (center) + const info = document.createElement('div'); + info.className = 'ea-fileupload-card-info'; + + const nameSpan = document.createElement('span'); + nameSpan.className = 'ea-fileupload-card-name'; + nameSpan.textContent = file.name; + info.appendChild(nameSpan); + + const sizeSpan = document.createElement('span'); + sizeSpan.className = 'ea-fileupload-card-size'; + sizeSpan.textContent = this.#humanizeFileSize(file.size); + info.appendChild(sizeSpan); - #getFieldCustomInput() { - return this.#fieldContainerElement.querySelector('.custom-file-label'); + card.appendChild(info); + + // actions (right side): only delete for newly selected files + const actions = document.createElement('div'); + actions.className = 'ea-fileupload-card-actions'; + + if (this.#container.hasAttribute('data-allow-delete')) { + const deleteBtn = document.createElement('button'); + deleteBtn.type = 'button'; + deleteBtn.className = 'ea-fileupload-action-btn ea-fileupload-action-delete'; + deleteBtn.setAttribute('data-ea-fileupload-delete-card', ''); + const deleteIcon = this.#parseIconHtml(this.#container.getAttribute('data-icon-delete')); + if (deleteIcon) { + deleteBtn.appendChild(deleteIcon); + } + actions.appendChild(deleteBtn); + } + + card.appendChild(actions); + + return card; } - #getFieldDeleteButton() { - return this.#fieldContainerElement.querySelector('.ea-fileupload-delete-btn'); + /** + * Safely converts an HTML string (from a trusted Twig-rendered data attribute) into DOM nodes. + */ + #parseIconHtml(htmlString) { + if (!htmlString) { + return null; + } + + const doc = new DOMParser().parseFromString(htmlString, 'text/html'); + const fragment = document.createDocumentFragment(); + while (doc.body.firstChild) { + fragment.appendChild(doc.body.firstChild); + } + + return fragment; } - #getFieldSizeLabel() { - return this.#fieldContainerElement.querySelector('.input-group-text'); + #humanizeFileSize(bytes) { + const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + if (0 === bytes) { + return '0 B'; + } + + const factor = Math.trunc(Math.floor(Math.log(bytes) / Math.log(1024))); + + if (0 === factor) { + return `${Math.trunc(bytes)} ${units[0]}`; + } + + const scaledValue = Math.round((bytes / 1024 ** factor) * 10) / 10; + const formatted = scaledValue % 1 === 0 ? scaledValue.toFixed(0) : scaledValue.toFixed(1); + + return `${formatted} ${units[factor]}`; } } diff --git a/composer.json b/composer.json index 6aa177758d..f1deff34b1 100644 --- a/composer.json +++ b/composer.json @@ -61,6 +61,7 @@ "symfony/process": "^6.4.33|^7.0|^8.0", "symfony/web-link": "^6.4.32|^7.0|^8.0", "vincentlanglet/twig-cs-fixer": "^3.10", + "league/flysystem": "^3.0", "zenstruck/foundry": "^2.3" }, "config": { diff --git a/config/services.php b/config/services.php index b620fde529..0283e423e8 100644 --- a/config/services.php +++ b/config/services.php @@ -43,6 +43,7 @@ use EasyCorp\Bundle\EasyAdminBundle\Field\Configurator\CurrencyConfigurator; use EasyCorp\Bundle\EasyAdminBundle\Field\Configurator\DateTimeConfigurator; use EasyCorp\Bundle\EasyAdminBundle\Field\Configurator\EmailConfigurator; +use EasyCorp\Bundle\EasyAdminBundle\Field\Configurator\FileConfigurator; use EasyCorp\Bundle\EasyAdminBundle\Field\Configurator\FormConfigurator; use EasyCorp\Bundle\EasyAdminBundle\Field\Configurator\IdConfigurator; use EasyCorp\Bundle\EasyAdminBundle\Field\Configurator\ImageConfigurator; @@ -404,8 +405,13 @@ ->set(IdConfigurator::class) + ->set(FileConfigurator::class) + ->arg(0, param('kernel.project_dir')) + ->arg(1, tagged_locator(EasyAdminExtension::TAG_FLYSYSTEM_STORAGE)) + ->set(ImageConfigurator::class) ->arg(0, param('kernel.project_dir')) + ->arg(1, tagged_locator(EasyAdminExtension::TAG_FLYSYSTEM_STORAGE)) ->set(IntegerConfigurator::class) diff --git a/doc/.doctor-rst.yaml b/doc/.doctor-rst.yaml index 02f2d40c1f..1e0a1f21ce 100644 --- a/doc/.doctor-rst.yaml +++ b/doc/.doctor-rst.yaml @@ -39,13 +39,13 @@ rules: no_app_bundle: ~ versionadded_directive_major_version: - major_version: 4 + major_version: 5 versionadded_directive_min_version: - min_version: '4.0' + min_version: '5.0' deprecated_directive_major_version: - major_version: 4 + major_version: 5 deprecated_directive_min_version: - min_version: '4.0' + min_version: '5.0' diff --git a/doc/fields.rst b/doc/fields.rst index 7cb502cba4..13e2414c3e 100644 --- a/doc/fields.rst +++ b/doc/fields.rst @@ -703,6 +703,7 @@ These are all the built-in fields provided by EasyAdmin: * :doc:`DateField ` * :doc:`DateTimeField ` * :doc:`EmailField ` +* :doc:`FileField ` * :doc:`HiddenField ` * :doc:`IdField ` * :doc:`ImageField ` @@ -761,7 +762,7 @@ Doctrine Type Recommended EasyAdmin Fields In addition to these, EasyAdmin includes other field types for specific values: * ``AvatarField``, ``ColorField``, ``CountryField``, ``CurrencyField``, ``EmailField``, - ``IdField``, ``ImageField``, ``LanguageField``, ``LocaleField``, ``SlugField``, + ``FileField``, ``IdField``, ``ImageField``, ``LanguageField``, ``LocaleField``, ``SlugField``, ``TelephoneField``, ``TimezoneField`` and ``UrlField`` work well with Doctrine's ``string`` type. * ``MoneyField`` and ``PercentField`` work well with Doctrine's ``decimal``, ``float`` diff --git a/doc/fields/FileField.rst b/doc/fields/FileField.rst new file mode 100644 index 0000000000..78838427b7 --- /dev/null +++ b/doc/fields/FileField.rst @@ -0,0 +1,308 @@ +EasyAdmin File Field +==================== + +This field is used to manage the uploading of files (PDFs, documents, etc.) to the +backend. The entity property only stores the path to the file (relative to the +upload directory). The actual file contents are stored on the server filesystem +or on any remote system configured via the `league/flysystem-bundle`_. + +In :ref:`form pages (edit and new) ` it looks like this: + +.. code-block:: html + + + + +Basic Information +----------------- + +* **PHP Class**: ``EasyCorp\Bundle\EasyAdminBundle\Field\FileField`` +* **Doctrine DBAL Type** used to store this value: ``string`` +* **Symfony Form Type** used to render the field: ``FileUploadType``, a custom + form type created by EasyAdmin +* **Rendered as**: + + .. code-block:: html + + + + +Options +------- + +setBasePath +~~~~~~~~~~~ + +By default, files are linked in read-only pages (``index`` and ``detail``) "as is", +without changing their path. If you serve your files under some path (e.g. +``uploads/files/``) use this option to configure that:: + + yield FileField::new('...')->setBasePath('uploads/files/'); + +setUploadDir +~~~~~~~~~~~~ + +**This option is required.** Use it to set the directory where uploaded files are +stored. The argument is the directory relative to your project root:: + + yield FileField::new('...')->setUploadDir('public/uploads/files/'); + // the property will only store the file path relative to this dir + // (e.g. 'catalog.pdf', 'venue/contract.docx') + +``FileField`` does not define a default upload directory. If you don't call this +method, an exception will be thrown. + +setFileConstraints +~~~~~~~~~~~~~~~~~~ + +By default, no validation constraints are applied to the uploaded file. Use this +option to define the constraints applied to the uploaded file:: + + use Symfony\Component\Validator\Constraints\File; + + yield FileField::new('...')->setFileConstraints(new File(filenameCharset: 'ASCII')); + +setUploadedFileNamePattern +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, uploaded files are stored with the same file name and extension as +the original files. Use this option to rename the files after uploading. +The string pattern passed as argument can include the following special values: + +* ``[DD]``, the day part of the current date (with leading zeros, obtained as ``date('d')``) +* ``[MM]``, the month part of the current date (with leading zeros, obtained as ``date('m')``) +* ``[YYYY]``, the full year of the current date (obtained as ``date('Y')``) +* ``[YY]``, the two-digit year of the current date (obtained as ``date('y')``) +* ``[hh]``, the hour of the current time in 24h format (with leading zeros, obtained as ``date('H')``) +* ``[mm]``, the minutes of the current time (with leading zeros, obtained as ``date('i')``) +* ``[ss]``, the seconds of the current time (with leading zeros, obtained as ``date('s')``) +* ``[timestamp]``, the current timestamp (obtained as ``time()``; e.g. ``1773256492``) +* ``[name]``, the original name of the uploaded file +* ``[slug]``, the slug of the original name of the uploaded file generated with Symfony's + String component (all lowercase and using ``-`` as the separator) +* ``[extension]``, the original extension of the uploaded file (without the leading dot, e.g. ``png``) + (if the file has multiple extensions, only the last one is returned) +* ``[contenthash]``, a SHA1 hash of the original file contents (40-char hexadecimal + string, e.g. ``3dfd6a9fbb83413b7f47c913ce2a95416dc6da88``) +* ``[randomhash]``, a random hash not related in any way to the original file contents + (40-char hexadecimal string, e.g. ``8ff61576fb5f07f82dd9dbb7874cef74e24fcb26``) +* ``[uuid]``, a random UUID v4 value formatted as RFC 4122 (36-char hexadecimal string, + e.g. ``d9e7a184-5d5b-11ea-a62a-3499710062d0``) (generated with Symfony's Uid component) +* ``[uuid32]``, a random UUID v4 value formatted as Base 32 (26-char string, + e.g. ``6SWYGR8QAV27NACAHMK5RG0RPG``) (generated with Symfony's Uid component) +* ``[uuid58]``, a random ULID value formatted as Base 58 (22-char string, + e.g. ``TuetYWNHhmuSQ3xPoVLv9M``) (generated with Symfony's Uid component) +* ``[ulid]``, a random ULID value (26-char string, e.g. ``01AN4Z07BY79KA1307SR9X4MV3``) + (generated with Symfony's Uid component) + +You can combine them in any way:: + + yield FileField::new('...') + ->setUploadedFileNamePattern('[YYYY]/[MM]/[DD]/[slug]-[contenthash].[extension]'); + +The argument of this method also accepts a closure that receives the Symfony's +``UploadedFile`` instance and the **current entity instance** as arguments:: + + yield FileField::new('...')->setUploadedFileNamePattern( + fn (UploadedFile $file): string => sprintf('upload_%d_%s.%s', random_int(1, 999), $file->getFilename(), $file->guessExtension())) + ); + +The ``FileField`` closure also receives the entity as a second argument. This +allows naming files based on entity data. On the ``new`` page, the entity is a +fresh instance (possibly without an ID); on the ``edit`` page, it has its +current database values:: + + yield FileField::new('...')->setUploadedFileNamePattern( + static fn (UploadedFile $file, MyEntity $entity): string => sprintf('%s/[name].[extension]', $entity->getSlug())) + ); + +isDeletable +~~~~~~~~~~~ + +By default, the file upload widget shows a "delete" checkbox that allows users +to remove the uploaded file. Use this option to hide that checkbox:: + + yield FileField::new('...')->isDeletable(false); + +isDownloadable +~~~~~~~~~~~~~~ + +By default, a link to download the uploaded file is displayed next to the form +field. Use this option to hide that link:: + + yield FileField::new('...')->isDownloadable(false); + +isViewable +~~~~~~~~~~ + +By default, a link to view the uploaded file is displayed next to the form field. +Use this option to hide that link:: + + yield FileField::new('...')->isViewable(false); + +maxSize +~~~~~~~ + +Use this option to set the maximum allowed file size. The value can be an integer +(number of bytes) or a suffixed string (e.g. ``'200k'``, ``'2M'``, ``'1G'`` for +SI units or ``'1Ki'``, ``'1Mi'`` for binary units):: + + yield FileField::new('...')->maxSize('10M'); + yield FileField::new('...')->maxSize(1048576); // 1 MB in bytes + +You can customize the error message by passing a second argument:: + + yield FileField::new('...')->maxSize('5M', 'The file "{{ name }}" is too large ({{ size }} {{ suffix }}). Maximum allowed: {{ limit }} {{ suffix }}.'); + +The available placeholders for the error message are: ``{{ file }}`` (the absolute +file path), ``{{ name }}`` (the base file name), ``{{ size }}`` (the file size), +``{{ limit }}`` (the maximum allowed size) and ``{{ suffix }}`` (the size unit, +e.g. ``kB``, ``MB``). + +mimeTypes +~~~~~~~~~ + +By default, all file types are accepted. Use this option to restrict the allowed +MIME types. The value is a string with a comma-separated list of file extensions +or MIME types. You can use any value valid in the `HTML "accept" attribute`_:: + + yield FileField::new('...')->mimeTypes('.pdf,.doc,.docx'); + yield FileField::new('...')->mimeTypes('video/*'); + yield FileField::new('...')->mimeTypes('image/*'); + yield FileField::new('...')->mimeTypes('.doc,.docx,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document'); + +When this option is set, the corresponding MIME types are also added +automatically as validation constraints. You can customize the error message +shown when the validation fails by passing a second argument:: + + yield FileField::new('...')->mimeTypes('.pdf', 'The file "{{ name }}" has MIME type "{{ type }}" but only "{{ types }}" are allowed.'); + +The available placeholders for the error message are: ``{{ file }}`` (the absolute +file path), ``{{ name }}`` (the base file name), ``{{ type }}`` (the MIME type of +the uploaded file) and ``{{ types }}`` (the list of allowed MIME types). + +Replaced File Behavior +~~~~~~~~~~~~~~~~~~~~~~ + +When a user uploads a new file to replace an existing one, ``FileField`` +controls what happens to the old file on disk. There are three behaviors: + +``deleteReplacedFile`` + This is the **default** behavior. The old file is deleted from disk. If the + new file has the same name as an existing file, a numeric suffix (``_1``, + ``_2``, etc.) is appended to avoid conflicts:: + + yield FileField::new('...')->deleteReplacedFile(); + +``keepReplacedFile`` + The old file is kept on disk. If you upload a new file with the same name, + the contents are silently overwritten:: + + yield FileField::new('...')->keepReplacedFile(); + +``keepReplacedFileOrFail`` + The old file is kept on disk. If the new file's name conflicts with an + existing file, an error is thrown:: + + yield FileField::new('...')->keepReplacedFileOrFail(); + +Flysystem Integration (Remote Storage) +-------------------------------------- + +By default, ``FileField`` stores uploaded files on the local filesystem. If you +need to store files in a remote storage service (Amazon S3, Google Cloud Storage, +Azure Blob Storage, etc.) you can integrate with `Flysystem`_ via the +`league/flysystem-bundle`_. + +Installation +~~~~~~~~~~~~ + +Install the Flysystem bundle and the adapter for your storage service: + +.. code-block:: terminal + + $ composer require league/flysystem-bundle + +Then install the adapter you need (e.g. for Amazon S3): + +.. code-block:: terminal + + $ composer require league/flysystem-aws-s3-v3 + +Configure Flysystem in your application: + +.. code-block:: yaml + + # config/packages/flysystem.yaml + flysystem: + storages: + default.storage: + adapter: 'aws' + options: + client: 'Aws\S3\S3Client' + bucket: 'my-bucket' + +Usage +~~~~~ + +Use the ``setFlysystemStorage()`` method to tell EasyAdmin which Flysystem storage +to use. The argument is the service ID of the storage as defined in your Flysystem +configuration (e.g. ``default.storage``):: + + yield FileField::new('attachment') + ->setFlysystemStorage('default.storage') + ->setFlysystemUrlPrefix('https://cdn.example.com/uploads') + ->setUploadDir('files/') + ->setUploadedFileNamePattern('[uuid].[extension]'); + +setFlysystemStorage +~~~~~~~~~~~~~~~~~~~ + +Sets the Flysystem storage service ID to use for uploading and deleting files. +This is the key you defined under ``flysystem.storages`` in your Flysystem +configuration:: + + yield FileField::new('...')->setFlysystemStorage('default.storage'); + +When this option is set, EasyAdmin automatically replaces the local upload, +delete, and validation callables with Flysystem equivalents. The upload directory +configured with ``setUploadDir()`` is used as a path prefix inside the Flysystem +storage (not as a local directory). + +setFlysystemUrlPrefix +~~~~~~~~~~~~~~~~~~~~~ + +Sets the URL prefix used to build the public URLs for files stored in Flysystem. +This is typically a CDN URL or a public URL pointing to your storage bucket:: + + yield FileField::new('...')->setFlysystemUrlPrefix('https://cdn.example.com/uploads'); + +This prefix is combined with the file path to generate the full URL shown in the +``index`` and ``detail`` pages. + +.. note:: + + When using Flysystem, the ``setBasePath()`` option is ignored. Use + ``setFlysystemUrlPrefix()`` instead. + +How It Works +~~~~~~~~~~~~ + +When Flysystem is configured for a field: + +* **Upload**: new files are written to the Flysystem storage using + ``writeStream()`` instead of being moved to a local directory. +* **Delete**: files are removed from the Flysystem storage using ``delete()`` + instead of ``unlink()``. +* **Validation**: file existence is checked using ``fileExists()`` instead of + the local filesystem. +* **Display**: file URLs are built from the configured URL prefix instead of + using the Symfony ``asset()`` function. + +All existing options (``setUploadedFileNamePattern()``, ``setFileConstraints()``, +``mimeTypes()``, ``maxSize()``, replaced file behaviors, ``isDeletable()``) continue +to work exactly the same way with Flysystem. + +.. _`HTML "accept" attribute`: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/accept +.. _`Flysystem`: https://flysystem.thephpleague.com +.. _`league/flysystem-bundle`: https://github.com/thephpleague/flysystem-bundle diff --git a/doc/fields/ImageField.rst b/doc/fields/ImageField.rst index 3eb7181e5a..2092de4a2a 100644 --- a/doc/fields/ImageField.rst +++ b/doc/fields/ImageField.rst @@ -2,8 +2,9 @@ EasyAdmin Image Field ===================== This field is used to manage the uploading of images to the backend. The entity -property only stores the path to the image and not its binary contents, which -are stored in a file. +property only stores the path to the image (relative to the upload directory). +The actual image contents are stored on the server filesystem or on any remote +system configured via the `league/flysystem-bundle`_. In :ref:`form pages (edit and new) ` it looks like this: @@ -44,6 +45,8 @@ By default, the contents of uploaded images are stored into files inside the change that location. The argument is the directory relative to your project root:: yield ImageField::new('...')->setUploadDir('assets/images/'); + // the property will only store the file path relative to this dir + // (e.g. 'logo.png', 'venue/layout.jpg') setFileConstraints ~~~~~~~~~~~~~~~~~~ @@ -52,7 +55,7 @@ By default, the uploaded file is validated using an empty `Image constraint`_ (which means it only validates that the uploaded file is of type image). Use this option to define the constraints applied to the uploaded file:: - yield ImageField::new('...')->setFileConstraints(new Image(maxSize: '100k')); + yield ImageField::new('...')->setFileConstraints(new Image(filenameCharset: 'ASCII')); setUploadedFileNamePattern ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -61,21 +64,41 @@ By default, uploaded images are stored with the same file name and extension as the original files. Use this option to rename the image files after uploading. The string pattern passed as argument can include the following special values: -* ``[day]``, the day part of the current date (obtained as ``date('d')``) -* ``[month]``, the month part of the current date (obtained as ``date('m')``) -* ``[year]``, the year part of the current date (obtained as ``date('Y')``) -* ``[timestamp]``, the current timestamp (obtained as ``time()``) +* ``[DD]``, the day part of the current date (with leading zeros, obtained as ``date('d')``) +* ``[MM]``, the month part of the current date (with leading zeros, obtained as ``date('m')``) +* ``[YYYY]``, the full year of the current date (obtained as ``date('Y')``) +* ``[YY]``, the two-digit year of the current date (obtained as ``date('y')``) +* ``[hh]``, the hour of the current time in 24h format (with leading zeros, obtained as ``date('H')``) +* ``[mm]``, the minutes of the current time (with leading zeros, obtained as ``date('i')``) +* ``[ss]``, the seconds of the current time (with leading zeros, obtained as ``date('s')``) +* ``[timestamp]``, the current timestamp (obtained as ``time()``; e.g. ``1773256492``) * ``[name]``, the original name of the uploaded file -* ``[slug]``, the slug of the original name of the uploaded file (generated with Symfony's String component) -* ``[extension]``, the original extension of the uploaded file (e.g. ``png``) -* ``[contenthash]``, a SHA1 hash of the original file contents +* ``[slug]``, the slug of the original name of the uploaded file generated with Symfony's + String component (all lowercase and using ``-`` as the separator) +* ``[extension]``, the original extension of the uploaded file (without the leading dot, e.g. ``png``) +* ``[contenthash]``, a SHA1 hash of the original file contents (40-char hexadecimal + string, e.g. ``3dfd6a9fbb83413b7f47c913ce2a95416dc6da88``) * ``[randomhash]``, a random hash not related in any way to the original file contents -* ``[uuid]``, a random UUID v4 value (generated with Symfony's Uid component) -* ``[ulid]``, a random ULID value (generated with Symfony's Uid component) + (40-char hexadecimal string, e.g. ``8ff61576fb5f07f82dd9dbb7874cef74e24fcb26``) +* ``[uuid]``, a random UUID v4 value formatted as RFC 4122 (36-char hexadecimal string, + e.g. ``d9e7a184-5d5b-11ea-a62a-3499710062d0``) (generated with Symfony's Uid component) +* ``[uuid32]``, a random UUID v4 value formatted as Base 32 (26-char string, + e.g. ``6SWYGR8QAV27NACAHMK5RG0RPG``) (generated with Symfony's Uid component) +* ``[uuid58]``, a random ULID value formatted as Base 58 (22-char string, + e.g. ``TuetYWNHhmuSQ3xPoVLv9M``) (generated with Symfony's Uid component) +* ``[ulid]``, a random ULID value (26-char string, e.g. ``01AN4Z07BY79KA1307SR9X4MV3``) + (generated with Symfony's Uid component) + +.. deprecated:: 5.1 + + The ``[day]``, ``[month]`` and ``[year]`` placeholders are deprecated. Use + ``[DD]``, ``[MM]`` and ``[YYYY]`` instead. The old placeholders will be + removed in EasyAdmin 6.0. You can combine them in any way:: - yield ImageField::new('...')->setUploadedFileNamePattern('[year]/[month]/[day]/[slug]-[contenthash].[extension]'); + yield ImageField::new('...') + ->setUploadedFileNamePattern('[YYYY]/[MM]/[DD]/[slug]-[contenthash].[extension]'); The argument of this method also accepts a closure that receives as its first argument the Symfony's UploadedFile instance:: @@ -84,4 +107,156 @@ argument the Symfony's UploadedFile instance:: fn (UploadedFile $file): string => sprintf('upload_%d_%s.%s', random_int(1, 999), $file->getFilename(), $file->guessExtension())) ); +isDeletable +~~~~~~~~~~~ + +By default, the image upload widget shows a "delete" checkbox that allows users +to remove the uploaded image. Use this option to hide that checkbox:: + + yield ImageField::new('...')->isDeletable(false); + +isDownloadable +~~~~~~~~~~~~~~ + +By default, a link to download the uploaded image is displayed next to the form +field. Use this option to hide that link:: + + yield ImageField::new('...')->isDownloadable(false); + +isViewable +~~~~~~~~~~ + +By default, a link to view the uploaded image is displayed next to the form field. +Use this option to hide that link:: + + yield ImageField::new('...')->isViewable(false); + +maxSize +~~~~~~~ + +Use this option to set the maximum allowed image size. The value can be an integer +(number of bytes) or a suffixed string (e.g. ``'200k'``, ``'2M'``, ``'1G'`` for +SI units or ``'1Ki'``, ``'1Mi'`` for binary units):: + + yield ImageField::new('...')->maxSize('5M'); + yield ImageField::new('...')->maxSize(1048576); // 1 MB in bytes + +You can customize the error message by passing a second argument:: + + yield ImageField::new('...')->maxSize('2M', 'The image "{{ name }}" is too large ({{ size }} {{ suffix }}). Maximum allowed: {{ limit }} {{ suffix }}.'); + +The available placeholders for the error message are: ``{{ file }}`` (the absolute +file path), ``{{ name }}`` (the base file name), ``{{ size }}`` (the file size), +``{{ limit }}`` (the maximum allowed size) and ``{{ suffix }}`` (the size unit, +e.g. ``kB``, ``MB``). + +mimeTypes +~~~~~~~~~ + +By default, the accepted MIME types are set to ``image/*``, which restricts the +browser's file dialog to image files. Use this option to customize the accepted +file types. The value is a string with a comma-separated list of file extensions +or MIME types. You can use any value valid in the `HTML "accept" attribute`_:: + + yield ImageField::new('...')->mimeTypes('.png,.jpg,.webp'); + yield ImageField::new('...')->mimeTypes('image/png,image/jpeg'); + +When this option is set, the corresponding MIME types are also added +automatically as validation constraints. You can customize the error message +shown when the validation fails by passing a second argument:: + + yield ImageField::new('...')->mimeTypes('.png,.jpg', 'The image "{{ name }}" has MIME type "{{ type }}" but only "{{ types }}" are allowed.'); + +The available placeholders for the error message are: ``{{ file }}`` (the absolute +file path), ``{{ name }}`` (the base file name), ``{{ type }}`` (the MIME type of +the uploaded file) and ``{{ types }}`` (the list of allowed MIME types). + +Replaced File Behavior +~~~~~~~~~~~~~~~~~~~~~~ + +When a user uploads a new image to replace an existing one, ``ImageField`` +controls what happens to the old file on disk. There are three behaviors: + +``deleteReplacedFile`` + This is the **default** behavior. The old file is deleted from disk. If the + new file has the same name as an existing file, a numeric suffix (``_1``, + ``_2``, etc.) is appended to avoid conflicts:: + + yield ImageField::new('...')->deleteReplacedFile(); + +``keepReplacedFile`` + The old file is kept on disk. If you upload a new file with the same name, + the contents are silently overwritten:: + + yield ImageField::new('...')->keepReplacedFile(); + +``keepReplacedFileOrFail`` + The old file is kept on disk. If the new file's name conflicts with an + existing file, an error is thrown:: + + yield ImageField::new('...')->keepReplacedFileOrFail(); + +Flysystem Integration (Remote Storage) +-------------------------------------- + +By default, ``ImageField`` stores uploaded images on the local filesystem. If you +need to store images in a remote storage service (Amazon S3, Google Cloud Storage, +Azure Blob Storage, etc.) you can integrate with `Flysystem`_ via the +`league/flysystem-bundle`_. + +Refer to the :doc:`FileField documentation ` for the +installation and Flysystem configuration steps. + +Usage +~~~~~ + +Use the ``setFlysystemStorage()`` method to tell EasyAdmin which Flysystem storage +to use. The argument is the service ID of the storage as defined in your Flysystem +configuration (e.g. ``default.storage``):: + + yield ImageField::new('photo') + ->setFlysystemStorage('default.storage') + ->setFlysystemUrlPrefix('https://cdn.example.com/uploads') + ->setUploadDir('images/') + ->setUploadedFileNamePattern('[uuid].[extension]'); + +setFlysystemStorage +~~~~~~~~~~~~~~~~~~~ + +Sets the Flysystem storage service ID to use for uploading and deleting images. +This is the key you defined under ``flysystem.storages`` in your Flysystem +configuration:: + + yield ImageField::new('...')->setFlysystemStorage('default.storage'); + +When this option is set, EasyAdmin automatically replaces the local upload, +delete, and validation callables with Flysystem equivalents. The upload directory +configured with ``setUploadDir()`` is used as a path prefix inside the Flysystem +storage (not as a local directory). + +setFlysystemUrlPrefix +~~~~~~~~~~~~~~~~~~~~~ + +Sets the URL prefix used to build the public URLs for images stored in Flysystem. +This is typically a CDN URL or a public URL pointing to your storage bucket:: + + yield ImageField::new('...')->setFlysystemUrlPrefix('https://cdn.example.com/uploads'); + +This prefix is combined with the image path to generate the full URL shown in the +``index`` and ``detail`` pages. + +.. note:: + + When using Flysystem, the ``setBasePath()`` option is ignored. Use + ``setFlysystemUrlPrefix()`` instead. + +All existing options (``setUploadedFileNamePattern()``, ``setFileConstraints()``, +``mimeTypes()``, ``maxSize()``, replaced file behaviors, ``isDeletable()``) continue +to work exactly the same way with Flysystem. See the +:doc:`FileField documentation ` for details about how +Flysystem integration works internally. + +.. _`HTML "accept" attribute`: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/accept .. _`Image constraint`: https://symfony.com/doc/current/reference/constraints/Image.html +.. _`Flysystem`: https://flysystem.thephpleague.com +.. _`league/flysystem-bundle`: https://github.com/thephpleague/flysystem-bundle diff --git a/public/app.912702e5.css b/public/app.ef57b823.css similarity index 90% rename from public/app.912702e5.css rename to public/app.ef57b823.css index f2be2dd558..8eb339336b 100644 --- a/public/app.912702e5.css +++ b/public/app.ef57b823.css @@ -19,4 +19,4 @@ * Font Awesome Free 6.7.1 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) * Copyright 2024 Fonticons, Inc. - */@font-face{font-display:block;font-family:Font Awesome\ 5 Brands;font-weight:400;src:url(fonts/fa-brands-400.fdbb5585.woff2) format("woff2"),url(fonts/fa-brands-400.26b80c88.ttf) format("truetype")}@font-face{font-display:block;font-family:Font Awesome\ 5 Free;font-weight:900;src:url(fonts/fa-solid-900.83a538a0.woff2) format("woff2"),url(fonts/fa-solid-900.ad1782c7.ttf) format("truetype")}@font-face{font-display:block;font-family:Font Awesome\ 5 Free;font-weight:400;src:url(fonts/fa-regular-400.4f6a2dab.woff2) format("woff2"),url(fonts/fa-regular-400.05fdd87b.ttf) format("truetype")}:root{--black:#000;--white:#fff;--rose-50:#fff1f2;--rose-100:#ffe4e6;--rose-200:#fecdd3;--rose-300:#fda4af;--rose-400:#fb7185;--rose-500:#f43f5e;--rose-600:#e11d48;--rose-700:#be123c;--rose-800:#9f1239;--rose-900:#881337;--pink-50:#fdf2f8;--pink-100:#fce7f3;--pink-200:#fbcfe8;--pink-300:#f9a8d4;--pink-400:#f472b6;--pink-500:#ec4899;--pink-600:#db2777;--pink-700:#be185d;--pink-800:#9d174d;--pink-900:#831843;--fuchsia-50:#fdf4ff;--fuchsia-100:#fae8ff;--fuchsia-200:#f5d0fe;--fuchsia-300:#f0abfc;--fuchsia-400:#e879f9;--fuchsia-500:#d946ef;--fuchsia-600:#c026d3;--fuchsia-700:#a21caf;--fuchsia-800:#86198f;--fuchsia-900:#701a75;--purple-50:#faf5ff;--purple-100:#f3e8ff;--purple-200:#e9d5ff;--purple-300:#d8b4fe;--purple-400:#c084fc;--purple-500:#a855f7;--purple-600:#9333ea;--purple-700:#7e22ce;--purple-800:#6b21a8;--purple-900:#581c87;--violet-50:#f5f3ff;--violet-100:#ede9fe;--violet-200:#ddd6fe;--violet-300:#c4b5fd;--violet-400:#a78bfa;--violet-500:#8b5cf6;--violet-600:#7c3aed;--violet-700:#6d28d9;--violet-800:#5b21b6;--violet-900:#4c1d95;--indigo-50:#eef2ff;--indigo-100:#e0e7ff;--indigo-200:#c7d2fe;--indigo-300:#a5b4fc;--indigo-400:#818cf8;--indigo-500:#6366f1;--indigo-600:#4f46e5;--indigo-700:#4338ca;--indigo-800:#3730a3;--indigo-900:#312e81;--blue-50:#eff6ff;--blue-100:#dbeafe;--blue-200:#bfdbfe;--blue-300:#93c5fd;--blue-400:#60a5fa;--blue-500:#3b82f6;--blue-600:#2563eb;--blue-700:#1d4ed8;--blue-800:#1e40af;--blue-900:#1e3a8a;--sky-50:#f0f9ff;--sky-100:#e0f2fe;--sky-200:#bae6fd;--sky-300:#7dd3fc;--sky-400:#38bdf8;--sky-500:#0ea5e9;--sky-600:#0284c7;--sky-700:#0369a1;--sky-800:#075985;--sky-900:#0c4a6e;--cyan-50:#ecfeff;--cyan-100:#cffafe;--cyan-200:#a5f3fc;--cyan-300:#67e8f9;--cyan-400:#22d3ee;--cyan-500:#06b6d4;--cyan-600:#0891b2;--cyan-700:#0e7490;--cyan-800:#155e75;--cyan-900:#164e63;--teal-50:#f0fdfa;--teal-100:#ccfbf1;--teal-200:#99f6e4;--teal-300:#5eead4;--teal-400:#2dd4bf;--teal-500:#14b8a6;--teal-600:#0d9488;--teal-700:#0f766e;--teal-800:#115e59;--teal-900:#134e4a;--emerald-50:#ecfdf5;--emerald-100:#d1fae5;--emerald-200:#a7f3d0;--emerald-300:#6ee7b7;--emerald-400:#34d399;--emerald-500:#10b981;--emerald-600:#059669;--emerald-700:#047857;--emerald-800:#065f46;--emerald-900:#064e3b;--green-50:#f0fdf4;--green-100:#dcfce7;--green-200:#bbf7d0;--green-300:#86efac;--green-400:#4ade80;--green-500:#22c55e;--green-600:#16a34a;--green-700:#15803d;--green-800:#166534;--green-900:#14532d;--lime-50:#f7fee7;--lime-100:#ecfccb;--lime-200:#d9f99d;--lime-300:#bef264;--lime-400:#a3e635;--lime-500:#84cc16;--lime-600:#65a30d;--lime-700:#4d7c0f;--lime-800:#3f6212;--lime-900:#365314;--yellow-50:#fefce8;--yellow-100:#fef9c3;--yellow-200:#fef08a;--yellow-300:#fde047;--yellow-400:#facc15;--yellow-500:#eab308;--yellow-600:#ca8a04;--yellow-700:#a16207;--yellow-800:#854d0e;--yellow-900:#713f12;--amber-50:#fffbeb;--amber-100:#fef3c7;--amber-200:#fde68a;--amber-300:#fcd34d;--amber-400:#fbbf24;--amber-500:#f59e0b;--amber-600:#d97706;--amber-700:#b45309;--amber-800:#92400e;--amber-900:#78350f;--orange-50:#fff7ed;--orange-100:#ffedd5;--orange-200:#fed7aa;--orange-300:#fdba74;--orange-400:#fb923c;--orange-500:#f97316;--orange-600:#ea580c;--orange-700:#c2410c;--orange-800:#9a3412;--orange-900:#7c2d12;--red-50:#fef2f2;--red-100:#fee2e2;--red-200:#fecaca;--red-300:#fca5a5;--red-400:#f87171;--red-500:#ef4444;--red-600:#dc2626;--red-700:#b91c1c;--red-800:#991b1b;--red-900:#7f1d1d;--warm-gray-50:#fafaf9;--warm-gray-100:#f5f5f4;--warm-gray-200:#e7e5e4;--warm-gray-300:#d6d3d1;--warm-gray-400:#a8a29e;--warm-gray-500:#78716c;--warm-gray-600:#57534e;--warm-gray-700:#44403c;--warm-gray-800:#292524;--warm-gray-900:#1c1917;--warm-gray-950:#0c0a09;--true-gray-50:#fafafa;--true-gray-100:#f5f5f5;--true-gray-200:#e5e5e5;--true-gray-300:#d4d4d4;--true-gray-400:#a3a3a3;--true-gray-500:#737373;--true-gray-600:#525252;--true-gray-700:#404040;--true-gray-800:#262626;--true-gray-900:#171717;--true-gray-950:#0a0a0a;--neutral-gray-50:#fafafa;--neutral-gray-100:#f4f4f5;--neutral-gray-200:#e4e4e7;--neutral-gray-300:#d4d4d8;--neutral-gray-400:#a1a1aa;--neutral-gray-500:#71717a;--neutral-gray-600:#52525b;--neutral-gray-700:#3f3f46;--neutral-gray-800:#27272a;--neutral-gray-900:#18181b;--neutral-gray-950:#09090b;--cool-gray-50:#f9fafb;--cool-gray-100:#f3f4f6;--cool-gray-200:#e5e7eb;--cool-gray-300:#d1d5db;--cool-gray-400:#9ca3af;--cool-gray-500:#6b7280;--cool-gray-600:#4b5563;--cool-gray-700:#374151;--cool-gray-800:#1f2937;--cool-gray-900:#111827;--cool-gray-950:#030712;--blue-gray-50:#f8fafc;--blue-gray-100:#f1f5f9;--blue-gray-200:#e2e8f0;--blue-gray-300:#cbd5e1;--blue-gray-400:#94a3b8;--blue-gray-500:#64748b;--blue-gray-600:#475569;--blue-gray-700:#334155;--blue-gray-800:#1e293b;--blue-gray-900:#0f172a;--blue-gray-950:#020617;--gray-50:var(--blue-gray-50);--gray-100:var(--blue-gray-100);--gray-200:var(--blue-gray-200);--gray-300:var(--blue-gray-300);--gray-400:var(--blue-gray-400);--gray-500:var(--blue-gray-500);--gray-600:var(--blue-gray-600);--gray-700:var(--blue-gray-700);--gray-800:var(--blue-gray-800);--gray-900:var(--blue-gray-900);--font-family-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:"JetBrains Mono",ui-monospace,"Roboto Mono",SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--font-family-base:var(--font-family-sans-serif);--font-size-xs:12px;--font-size-sm:13px;--font-size-base:14px;--font-size-lg:16px;--font-size-xl:18px;--font-size-xxl:24px;--font-size-xxxl:28px;--shadow-md:0 4px 6px -1px rgba(15,23,43,.1),0 2px 4px -2px rgba(15,23,42,.1);--shadow-lg:0 10px 15px -3px rgba(15,23,43,.1),0 4px 6px -4px rgba(15,23,42,.1);--shadow-xl:0 20px 25px -5px rgba(15,23,42,.2),0 8px 10px -6px rgba(15,23,42,.2);--width-sm:576px;--width-md:768px;--width-lg:992px;--width-xl:1200px;--width-xxl:1400px;--zindex-modal-backdrop:2020;--form-tabs-gutter-x:5px;--text-primary-color:var(--text-color);--text-secondary-color:var(--text-muted);--text-tertiary-color:var(--gray-400);--border-primary-color:var(--gray-500);--border-secondary-color:var(--gray-300);--border-tertiary-color:var(--gray-100);--primary-bg:var(--gray-300);--secondary-bg:var(--gray-100);--tertiary-bg:var(--gray-50);--body-max-width:1440px;--body-bg:var(--white);--responsive-header-bg:var(--gray-50);--responsive-header-border-color:var(--gray-200);--responsive-header-logo-color:var(--gray-800);--responsive-table-label-color:var(--gray-500);--responsive-table-row-border-color:var(--gray-300);--sidebar-max-width:230px;--sidebar-bg:var(--gray-50);--sidebar-border-color:var(--gray-200);--sidebar-logo-color:var(--gray-800);--sidebar-padding-left:10px;--sidebar-padding-right:10px;--sidebar-menu-items-padding-left:6px;--sidebar-menu-items-padding-right:10px;--sidebar-menu-color:var(--gray-700);--sidebar-menu-badge-bg:var(--indigo-100);--sidebar-menu-badge-color:var(--gray-500);--sidebar-menu-badge-active-bg:var(--color-primary);--sidebar-menu-badge-active-color:var(--indigo-50);--sidebar-menu-submenu-color:var(--gray-600);--sidebar-menu-header-color:var(--gray-400);--sidebar-menu-icon-color:var(--gray-500);--sidebar-menu-active-item-bg:var(--gray-200);--sidebar-menu-active-item-color:var(--color-primary);--sidebar-menu-compact-hover-box-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--resize-handler-width:10px;--content-section-border-color:var(--gray-200);--resize-handler-hover-bg:var(--indigo-600);--content-search-input-bg:var(--body-bg);--content-search-icon-color:var(--gray-500);--content-search-reset-button-bg:var(--gray-300);--content-search-reset-button-color:var(--gray-600);--content-search-reset-button-hover-bg:var(--gray-600);--content-search-reset-button-hover-color:var(--gray-200);--content-top-border-color:var(--gray-200);--content-bg:var(--white);--content-padding-left:15px;--content-padding-right:15px;--lg-content-padding-left:35px;--lg-content-padding-right:25px;--lg-content-padding-bottom:45px;--user-avatar-icon-bg:var(--gray-200);--user-avatar-icon-color:var(--gray-500);--user-name-color:var(--gray-500);--user-menu-impersonated-link-color:var(--color-primary);--popover-bg:var(--gray-100);--popover-border-color:var(--gray-300);--popover-color:var(--text-color);--popover-shadow:var(--shadow-xl);--popover-max-width:480px;--dropdown-toggle-bg:var(--white);--dropdown-toggle-color:var(--gray-600);--dropdown-toggle-border-color:var(--gray-300);--dropdown-toggle-hover-border-color:var(--gray-400);--dropdown-bg:var(--white);--dropdown-color:var(--gray-600);--dropdown-border-color:var(--gray-200);--dropdown-link-color:var(--gray-700);--dropdown-link-hover-bg:var(--gray-100);--dropdown-icon-color:var(--gray-600);--dropdown-settings-icon-color:var(--gray-400);--dropdown-settings-active-item-bg:var(--gray-100);--dropdown-settings-active-item-color:var(--color-primary);--dropdown-settings-active-item-shadow:inset 0 0 0 1px #5368d580;--dropdown-item-success-bg:#dafbe1;--dropdown-item-warning-bg:#fff8c5;--dropdown-item-danger-bg:#ffebe9;--dropdown-item-success-color:#1a7f37;--dropdown-item-warning-color:#9a6700;--dropdown-item-danger-color:#d1242f;--datagrid-noresults-placeholder-bg:var(--gray-100);--datagrid-hidden-results-gradient-bg:var(--gray-50);--table-thead-color:var(--gray-800);--table-cell-color:var(--gray-600);--table-thead-marker-color:var(--gray-400);--table-cell-border-color:var(--gray-200);--table-hover-cell-bg:var(--gray-50);--table-selected-cell-bg:var(--indigo-50);--table-thead-sorted-color:var(--gray-900);--table-thead-sorted-marker-color:var(--color-primary);--datalist-border-color:var(--gray-200);--datalist-label-color:var(--gray-500);--datalist-value-color:var(--gray-600);--pagination-color:var(--gray-600);--pagination-hover-border-color:var(--gray-300);--pagination-disabled-color:var(--gray-400);--pagination-active-bg:var(--color-primary);--pagination-active-color:var(--white);--field-language-badge-border-color:var(--gray-300);--field-country-flag-border-color:var(--gray-200);--modal-bg:var(--white);--modal-border-color:var(--gray-200);--modal-header-bg:var(--gray-50);--modal-header-border-color:var(--gray-300);--modal-footer-bg:var(--gray-100);--modal-title-color:var(--gray-700);--detail-label-tooltip-underline-color:var(--gray-400);--form-label-color:var(--gray-800);--form-control-bg:var(--white);--form-control-disabled-bg:var(--gray-200);--form-control-disabled-color:var(--gray-600);--form-input-border-color:var(--gray-300);--form-input-error-legend-color:var(--red-600);--form-input-error-border-color:var(--red-600);--form-input-hover-border-color:var(--gray-400);--form-input-shadow:0 1px 2px 0 var(--gray-50);--form-input-hover-shadow:0 0 0 4px var(--gray-100);--form-input-error-shadow:0 0 0 3px var(--red-100);--form-input-text-color:var(--gray-700);--form-input-group-text-bg:var(--form-control-bg);--form-input-group-text-border-color:var(--form-input-border-color);--form-switch-bg:var(--body-bg);--form-switch-border-color:var(--gray-400);--form-switch-checked-bg:var(--indigo-500);--form-type-check-input-border-color:var(--gray-400);--form-type-check-input-box-shadow:0 1px 2px 0 var(--gray-50);--form-type-check-input-checked-bg:var(--indigo-500);--form-type-text-editor-toolbar-bg:var(--white);--form-type-text-editor-toolbar-button-color:var(--gray-600);--form-type-text-editor-toolbar-button-hover-color:var(--gray-100);--form-type-text-editor-toolbar-button-active-bg:var(--gray-200);--form-type-text-editor-toolbar-button-active-color:var(--gray-700);--form-type-text-editor-dialog-bg:var(--white);--form-type-text-editor-dialog-box-shadow:0 4px 12px var(--gray-300);--form-type-text-editor-content-pre-bg:var(--gray-200);--form-type-text-editor-content-pre-color:var(--text-color);--form-type-collection-item-collapsed-hover-bg:var(--gray-100);--form-type-autocomplete-dropdown-bg:var(--white);--form-type-autocomplete-dropdown-input-wrapper-bg:var(--gray-100);--form-type-autocomplete-dropdown-input-border-color:var(--form-input-border-color);--form-type-autocomplete-dropdown-active-item-bg:var(--gray-200);--form-type-autocomplete-close-button-bg:var(--gray-500);--form-type-autocomplete-close-button-hover-bg:var(--gray-700);--form-type-autocomplete-optgroup-bg:var(--body-bg);--form-type-autocomplete-optgroup-color:var(--gray-500);--form-type-autocomplete-multi-item-bg:var(--gray-100);--form-type-autocomplete-multi-item-border-color:var(--white);--form-type-autocomplete-multi-item-remove-button-hover-bg:var(--gray-200);--form-global-error-bg:var(--red-100);--form-global-error-color:var(--color-danger);--form-global-error-border:1px solid transparent;--form-help-color:var(--gray-600);--form-help-error-color:var(--gray-800);--form-help-active-color:var(--gray-800);--form-tabs-border-color:var(--gray-200);--form-tabs-help-color:var(--gray-600);--form-column-header-color:var(--gray-700);--form-column-help-color:var(--gray-600);--form-column-icon-color:var(--gray-500);--form-fieldset-header-color:var(--gray-700);--form-fieldset-help-color:var(--gray-600);--form-fieldset-border-color:var(--gray-200);--form-fieldset-header-border-color:var(--gray-200);--form-fieldset-icon-color:var(--gray-500);--form-fieldset-collapse-marker-color:var(--gray-400);--form-collection-item-collapse-marker-color:var(--gray-400);--badge-border:0;--badge-boolean-false-bg:var(--gray-200);--badge-boolean-false-box-shadow:inset 0 0 0 1px var(--gray-300);--badge-boolean-false-color:var(--text-color);--badge-boolean-true-bg:var(--color-primary);--badge-boolean-true-box-shadow:none;--badge-boolean-true-color:var(--white);--badge-success-bg:var(--green-100);--badge-success-box-shadow:none;--badge-success-color:var(--text-green-600);--badge-warning-bg:var(--yellow-100);--badge-warning-box-shadow:none;--badge-warning-color:var(--text-yellow-600);--badge-danger-bg:var(--red-100);--badge-danger-box-shadow:none;--badge-danger-color:var(--text-red-600);--badge-info-bg:var(--blue-100);--badge-info-box-shadow:none;--badge-info-color:var(--text-blue-600);--badge-primary-bg:var(--indigo-100);--badge-primary-box-shadow:none;--badge-primary-color:var(--text-indigo-600);--badge-secondary-bg:var(--gray-200);--badge-secondary-box-shadow:none;--badge-secondary-color:var(--gray-600);--badge-light-bg:var(--gray-50);--badge-light-box-shadow:none;--badge-light-color:var(--text-color);--badge-dark-bg:var(--gray-900);--badge-dark-box-shadow:none;--badge-dark-color:var(--gray-50);--badge-outline-box-shadow:inset 0 0 0 1px var(--gray-300);--badge-outline-color:var(--datalist-value-color);--alert-primary-bg:var(--indigo-100);--alert-primary-color:var(--indigo-800);--alert-primary-border-color:var(--indigo-200);--alert-secondary-bg:var(--gray-100);--alert-secondary-color:var(--gray-800);--alert-secondary-border-color:var(--gray-200);--alert-success-bg:var(--emerald-100);--alert-success-color:var(--emerald-900);--alert-success-border-color:var(--emerald-200);--alert-info-bg:var(--sky-100);--alert-info-color:var(--sky-800);--alert-info-border-color:var(--sky-200);--alert-warning-bg:var(--orange-100);--alert-warning-color:var(--orange-800);--alert-warning-border-color:var(--orange-200);--alert-danger-bg:var(--rose-100);--alert-danger-color:var(--rose-800);--alert-danger-border-color:var(--rose-200);--alert-light-bg:var(--white);--alert-light-color:var(--gray-800);--alert-light-border-color:var(--gray-200);--alert-dark-bg:var(--gray-800);--alert-dark-color:var(--gray-50);--alert-dark-border-color:var(--gray-500);--button-padding-y-sm:0px;--button-padding-x-sm:8px;--button-padding-y-md:0;--button-padding-x-md:12px;--button-padding-y-lg:8px;--button-padding-x-lg:16px;--button-font-size-sm:12px;--button-font-size-md:14px;--button-font-size-lg:16px;--button-line-height:1.5;--button-transition-duration:80ms;--button-transition-timing:cubic-bezier(0.65,0,0.35,1);--button-disabled-opacity:0.9;--button-focus-outline-color:var(--indigo-600);--button-primary-box-shadow:0px 1px 1px 0px #1f23280f,0px 1px 3px 0px #1f23280f;--button-primary-bg:linear-gradient(180deg,#566cdb,#5368d5);--button-primary-color:var(--white);--button-primary-icon-color:inherit;--button-primary-border-color:#1f232826;--button-primary-hover-bg:linear-gradient(180deg,#5368d5,#5064cc);--button-primary-hover-color:var(--white);--button-primary-hover-border-color:#1f232826;--button-primary-active-box-shadow:inset 0 1px 0 0 #002d114d;--button-primary-active-bg:linear-gradient(180deg,#5064cc,#4c5fc2);--button-primary-active-color:var(--white);--button-primary-active-border-color:#1f232826;--button-secondary-box-shadow:0 1px 0 0 #1f23280a;--button-secondary-bg:linear-gradient(180deg,#fafdff,#f6f8fa);--button-secondary-color:var(--text-primary-color);--button-secondary-icon-color:var(--gray-700);--button-secondary-border-color:#d1d9e0;--button-secondary-hover-bg:linear-gradient(180deg,#f6f8fa,#eff2f5);--button-secondary-hover-color:var(--text-primary-color);--button-secondary-hover-border-color:#d1d9e0;--button-secondary-active-box-shadow:inset 0 1px 0 0 #dee6ed;--button-secondary-active-bg:linear-gradient(180deg,#eff2f5,#e6eaef);--button-secondary-active-color:var(--text-primary-color);--button-secondary-active-border-color:#d1d9e0;--button-success-box-shadow:0 1px 0 0 #1f23280a;--button-success-bg:linear-gradient(180deg,#fff,#f6f8fa);--button-success-color:#1f883d;--button-success-icon-color:inherit;--button-success-border-color:#1f232826;--button-success-hover-bg:#1f883d;--button-success-hover-color:var(--white);--button-success-hover-border-color:#1f232826;--button-success-active-box-shadow:inset 0px 1px 0px 0px #002d114d;--button-success-active-bg:#197935;--button-success-active-color:var(--white);--button-success-active-border-color:#1f232826;--button-warning-box-shadow:0 1px 0 0 #1f23280a;--button-warning-bg:linear-gradient(180deg,#fff,#f6f8fa);--button-warning-color:#a67a00;--button-warning-icon-color:inherit;--button-warning-border-color:#1f232826;--button-warning-hover-bg:#b88700;--button-warning-hover-color:var(--white);--button-warning-hover-border-color:#1f232826;--button-warning-active-box-shadow:inset 0px 1px 0px 0px #2d24004d;--button-warning-active-bg:#b88700;--button-warning-active-color:var(--white);--button-warning-active-border-color:#2d24004d;--button-danger-box-shadow:0 1px 0 0 #1f23280a;--button-danger-bg:linear-gradient(180deg,#fff,#f6f8fa);--button-danger-color:#d1242f;--button-danger-icon-color:inherit;--button-danger-border-color:#d1d9e0;--button-danger-hover-bg:#cf222e;--button-danger-hover-color:var(--white);--button-danger-hover-border-color:#1f23280a;--button-danger-active-box-shadow:inset 0 1px 0 0 #4c001433;--button-danger-active-bg:#a40e26;--button-danger-active-color:var(--white);--button-danger-active-border-color:#1f23280a;--button-invisible-box-shadow:none;--button-invisible-bg:transparent;--button-invisible-color:inherit;--button-invisible-icon-color:inherit;--button-invisible-border-color:transparent;--button-invisible-hover-bg:#00000026;--button-invisible-hover-color:inherit;--button-invisible-hover-border-color:transparent;--button-invisible-active-bg:#00000026;--button-invisible-active-color:inherit;--button-invisible-active-box-shadow:none;--button-invisible-active-border-color:transparent;--button-invisible-danger-color:#cf222e;--button-invisible-danger-hover-color:#cf222e;--button-invisible-danger-hover-icon-color:inherit;--button-invisible-danger-hover-hover-bg:#ffebe9;--button-invisible-danger-active-color:#a40e26;--button-invisible-danger-hover-active-bg:#ffdad6;--text-color:var(--gray-800);--text-color-rgb:30,41,59;--text-color-dark:#292d42;--text-color-light:#9fa9b7;--box-shadow-lg:0 10px 15px -3px rgba(15,23,41,.1),0 4px 6px -2px rgba(15,23,41,.05);--content-panel-bg:#f8fafc;--fieldset-bg:#f5f7fa;--code-color:#c44c34;--code-editor-string-color:#032f62;--code-editor-keyword-color:#d73a49;--code-editor-comment-color:#22863a;--code-editor-definition-color:#e36209;--code-editor-variable-color:var(--form-input-text-color);--code-editor-number-color:var(--form-input-text-color);--code-editor-argument-color:#6f42c1;--code-editor-key-color:#005cc5;--code-editor-attribute-color:#22863a;--code-editor-addition-bg:#e6ffed;--code-editor-deletion-bg:#ffeef0;--code-editor-selection-bg:#d7d7d7;--page-login-bg:var(--gray-100);--page-login-form-bg:var(--white);--page-login-form-control-bg:var(--form-control-bg);--page-login-form-control-border-color:var(--form-input-border-color);--page-login-form-control-button-bg:var(--button-primary-bg);--zindex-700:777;--zindex-800:888;--zindex-900:999;--zindex-1050:1050;--text-blue-600:#075692;--text-green-600:#0d5e42;--text-indigo-600:#3c4caa;--text-red-600:#a11b4c;--text-yellow-600:#943505;--color-primary:#5368d5;--color-success:#1ea471;--color-info:#0679b7;--color-warning:#d97817;--color-danger:var(--red-600);--color-danger-rgb:220,38,38;--highlight-bg:#feff3f;--highlight-color:var(--text-color);--text-on-primary:var(--white);--text-muted:var(--gray-500);--link-color:#5c70d6;--link-color-rgb:92,112,214;--link-hover-color:#99a6e6;--link-hover-color-rgb:153,166,230;--link-hover-decoration:none;--link-danger-color:var(--red-600);--link-danger-hover-color:var(--red-500);--border-radius:4px;--border-radius-lg:8px;--border-radius-sm:2px;--border-width:1px;--border-style:solid;--border-color:#e3e7ee}.ea-dark-scheme{--text-primary-color:var(--text-color);--text-secondary-color:var(--text-muted);--text-tertiary-color:var(--true-gray-600);--border-primary-color:var(--true-gray-600);--border-secondary-color:var(--true-gray-700);--border-tertiary-color:var(--true-gray-800);--primary-bg:var(--true-gray-600);--secondary-bg:var(--true-gray-800);--tertiary-bg:var(--true-gray-900);--shadow-md:0 4px 6px -1px rgba(0,0,0,.3),0 2px 4px -2px rgba(0,0,0,.3);--shadow-lg:0 10px 15px -3px rgba(0,0,0,.3),0 4px 6px -4px rgba(0,0,0,.3);--shadow-xl:0 20px 25px -5px rgba(0,0,0,.4),0 8px 10px -6px rgba(0,0,0,.4);--body-bg:var(--true-gray-950);--responsive-header-bg:var(--true-gray-800);--responsive-header-border-color:var(--true-gray-600);--responsive-header-logo-color:var(--true-gray-300);--responsive-table-label-color:var(--true-gray-500);--responsive-table-row-border-color:var(--true-gray-700);--sidebar-bg:var(--true-gray-900);--sidebar-border-color:var(--true-gray-800);--sidebar-logo-color:var(--true-gray-200);--sidebar-menu-color:var(--true-gray-300);--sidebar-menu-badge-bg:var(--true-gray-800);--sidebar-menu-badge-color:var(--true-gray-300);--sidebar-menu-badge-active-bg:var(--blue-800);--sidebar-menu-badge-active-color:var(--true-gray-300);--sidebar-menu-submenu-color:var(--true-gray-400);--sidebar-menu-header-color:var(--true-gray-400);--sidebar-menu-icon-color:var(--true-gray-400);--sidebar-menu-active-item-bg:var(--true-gray-300);--sidebar-menu-active-item-color:var(--true-gray-950);--sidebar-menu-compact-hover-box-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--resize-handler-hover-bg:var(--indigo-400);--content-section-border-color:var(--true-gray-700);--content-search-input-bg:var(--body-bg);--content-search-icon-color:var(--true-gray-500);--content-search-reset-button-bg:var(--true-gray-800);--content-search-reset-button-color:var(--true-gray-300);--content-search-reset-button-hover-bg:var(--true-gray-700);--content-search-reset-button-hover-color:var(--true-gray-200);--content-top-border-color:var(--true-gray-700);--content-bg:var(--true-gray-900);--user-avatar-icon-bg:var(--true-gray-700);--user-avatar-icon-color:var(--true-gray-400);--user-name-color:var(--true-gray-400);--user-menu-impersonated-link-color:var(--color-primary);--popover-bg:var(--true-gray-900);--popover-border-color:var(--true-gray-700);--popover-color:var(--text-color);--popover-shadow:var(--shadow-xl);--popover-max-width:480px;--dropdown-toggle-bg:var(--true-gray-800);--dropdown-toggle-color:var(--true-gray-200);--dropdown-toggle-border-color:var(--true-gray-700);--dropdown-toggle-hover-border-color:var(--true-gray-600);--dropdown-bg:var(--true-gray-900);--dropdown-color:var(--true-gray-300);--dropdown-border-color:var(--true-gray-700);--dropdown-link-color:var(--true-gray-300);--dropdown-link-hover-bg:var(--true-gray-800);--dropdown-icon-color:var(--true-gray-400);--dropdown-settings-icon-color:var(--true-gray-500);--dropdown-settings-active-item-bg:var(--true-gray-950);--dropdown-settings-active-item-color:var(--color-primary);--dropdown-settings-active-item-shadow:inset 0 0 0 1px var(--true-gray-800);--dropdown-item-success-bg:#2ea04326;--dropdown-item-warning-bg:#bb800926;--dropdown-item-danger-bg:#f851491a;--dropdown-item-success-color:#3fb950;--dropdown-item-warning-color:#d29922;--dropdown-item-danger-color:#ff7b72;--datagrid-noresults-placeholder-bg:var(--true-gray-700);--datagrid-hidden-results-gradient-bg:var(--true-gray-700);--table-thead-color:var(--true-gray-200);--table-cell-color:var(--true-gray-300);--table-thead-marker-color:var(--true-gray-500);--table-cell-border-color:var(--true-gray-800);--table-hover-cell-bg:var(--true-gray-900);--table-selected-cell-bg:rgba(3,102,214,.25);--table-thead-sorted-color:var(--color-primary);--table-thead-sorted-marker-color:var(--color-primary);--datalist-border-color:var(--true-gray-600);--datalist-label-color:var(--true-gray-400);--datalist-value-color:var(--true-gray-300);--pagination-color:var(--true-gray-400);--pagination-hover-border-color:var(--true-gray-600);--pagination-active-bg:var(--blue-500);--pagination-active-color:var(--white);--field-language-badge-border-color:var(--true-gray-600);--field-country-flag-border-color:var(--true-gray-600);--modal-bg:var(--true-gray-800);--modal-border-color:var(--true-gray-600);--modal-header-bg:var(--true-gray-900);--modal-header-border-color:var(--true-gray-600);--modal-footer-bg:var(--true-gray-700);--modal-title-color:var(--true-gray-400);--pagination-disabled-color:var(--true-gray-600);--detail-label-tooltip-underline-color:var(--true-gray-500);--form-label-color:var(--true-gray-300);--form-control-bg:var(--true-gray-900);--form-control-disabled-bg:var(--true-gray-900);--form-control-disabled-color:var(--true-gray-500);--form-input-border-color:var(--true-gray-700);--form-input-error-legend-color:var(--red-500);--form-input-error-border-color:var(--red-500);--form-input-hover-border-color:var(--true-gray-500);--form-input-shadow:none;--form-input-hover-shadow:none;--form-input-error-shadow:0 0 0 3px var(--red-900);--form-input-text-color:var(--true-gray-200);--form-input-group-text-bg:var(--true-gray-800);--form-input-group-text-border-color:var(--true-gray-600);--form-switch-bg:var(--true-gray-600);--form-switch-border-color:var(--true-gray-700);--form-switch-checked-bg:var(--blue-600);--form-type-check-input-border-color:var(--true-gray-400);--form-type-check-input-box-shadow:0 1px 2px 0 var(--true-gray-800);--form-type-check-input-checked-bg:var(--blue-600);--form-type-text-editor-toolbar-bg:var(--true-gray-800);--form-type-text-editor-toolbar-button-color:var(--true-gray-400);--form-type-text-editor-toolbar-button-hover-color:var(--true-gray-700);--form-type-text-editor-toolbar-button-active-bg:var(--true-gray-700);--form-type-text-editor-toolbar-button-active-color:var(--true-gray-300);--form-type-text-editor-dialog-bg:var(--true-gray-800);--form-type-text-editor-dialog-box-shadow:0 4px 12px var(--true-gray-900);--form-type-text-editor-content-pre-bg:var(--true-gray-800);--form-type-text-editor-content-pre-color:var(--true-gray-300);--form-type-collection-item-collapsed-hover-bg:var(--true-gray-800);--form-type-autocomplete-dropdown-bg:var(--true-gray-800);--form-type-autocomplete-dropdown-input-wrapper-bg:var(--true-gray-900);--form-type-autocomplete-dropdown-input-border-color:transparent;--form-type-autocomplete-dropdown-active-item-bg:var(--true-gray-700);--form-type-autocomplete-close-button-bg:var(--true-gray-500);--form-type-autocomplete-close-button-hover-bg:var(--true-gray-800);--form-type-autocomplete-optgroup-bg:var(--form-type-autocomplete-dropdown-bg);--form-type-autocomplete-optgroup-color:var(--true-gray-400);--form-type-autocomplete-multi-item-bg:var(--true-gray-700);--form-type-autocomplete-multi-item-border-color:var(--true-gray-500);--form-type-autocomplete-multi-item-remove-button-hover-bg:var(--true-gray-800);--form-global-error-bg:transparent;--form-global-error-color:var(--red-400);--form-global-error-border:1px solid var(--red-400);--form-help-color:var(--true-gray-500);--form-help-error-color:var(--true-gray-200);--form-help-active-color:var(--true-gray-300);--form-tabs-border-color:var(--true-gray-600);--form-tabs-help-color:var(--true-gray-500);--form-column-header-color:var(--true-gray-300);--form-column-help-color:var(--true-gray-500);--form-column-icon-color:var(--true-gray-400);--form-fieldset-header-color:var(--true-gray-300);--form-fieldset-help-color:var(--true-gray-500);--form-fieldset-border-color:var(--true-gray-700);--form-fieldset-header-border-color:var(--true-gray-600);--form-fieldset-icon-color:var(--true-gray-400);--form-fieldset-collapse-marker-color:var(--true-gray-500);--form-collection-item-collapse-marker-color:var(--true-gray-500);--badge-box-shadow:inset 0 0 0 1px hsla(0,0%,96%,.3);--badge-boolean-false-bg:hsla(0,0%,96%,.1);--badge-boolean-false-box-shadow:inset 0 0 0 1px hsla(0,0%,96%,.3);--badge-boolean-false-color:var(--true-gray-200);--badge-boolean-true-bg:rgba(3,102,214,.18);--badge-boolean-true-box-shadow:inset 0 0 0 1px rgba(90,168,252,.3);--badge-boolean-true-color:#5aa8fc;--badge-success-bg:rgba(22,135,0,.18);--badge-success-box-shadow:inset 0 0 0 1px rgba(39,236,0,.3);--badge-success-color:var(--green-300);--badge-warning-bg:rgba(251,202,4,.18);--badge-warning-box-shadow:inset 0 0 0 1px rgba(250,201,5,.3);--badge-warning-color:var(--yellow-400);--badge-danger-bg:rgba(182,2,5,.18);--badge-danger-box-shadow:inset 0 0 0 1px rgba(253,155,157,.3);--badge-danger-color:var(--red-300);--badge-info-bg:rgba(3,102,214,.18);--badge-info-box-shadow:inset 0 0 0 1px rgba(90,168,252,.3);--badge-info-color:#5aa8fc;--badge-primary-bg:rgba(3,102,214,.18);--badge-primary-box-shadow:inset 0 0 0 1px rgba(90,168,252,.3);--badge-primary-color:#5aa8fc;--badge-secondary-bg:hsla(0,0%,96%,.1);--badge-secondary-box-shadow:inset 0 0 0 1px hsla(0,0%,96%,.3);--badge-secondary-color:var(--true-gray-200);--badge-light-bg:hsla(0,0%,100%,.18);--badge-light-box-shadow:inset 0 0 0 1px hsla(0,0%,100%,.3);--badge-light-color:#fff;--badge-dark-bg:rgba(0,0,0,.18);--badge-dark-box-shadow:inset 0 0 0 1px hsla(0,0%,60%,.3);--badge-dark-color:#999;--badge-outline-box-shadow:inset 0 0 0 1px var(--true-gray-500);--badge-outline-color:var(--datalist-value-color);--alert-primary-bg:var(--indigo-900);--alert-primary-color:var(--indigo-100);--alert-primary-border-color:var(--indigo-800);--alert-secondary-bg:var(--true-gray-700);--alert-secondary-color:var(--true-gray-300);--alert-secondary-border-color:var(--true-gray-600);--alert-success-bg:var(--emerald-800);--alert-success-color:var(--emerald-100);--alert-success-border-color:var(--emerald-700);--alert-info-bg:var(--sky-800);--alert-info-color:var(--sky-100);--alert-info-border-color:var(--sky-700);--alert-warning-bg:var(--orange-800);--alert-warning-color:var(--orange-100);--alert-warning-border-color:var(--orange-700);--alert-danger-bg:var(--red-800);--alert-danger-color:var(--red-100);--alert-danger-border-color:var(--red-700);--alert-light-bg:var(--true-gray-300);--alert-light-color:var(--true-gray-800);--alert-light-border-color:var(--true-gray-200);--alert-dark-bg:var(--true-gray-900);--alert-dark-color:var(--true-gray-200);--alert-dark-border-color:var(--true-gray-700);--button-focus-outline-color:#1f6febb3;--button-primary-box-shadow:none;--button-primary-bg:#1447e6;--button-primary-color:var(--white);--button-primary-icon-color:hsla(0,0%,100%,.85);--button-primary-border-color:#ffffff26;--button-primary-hover-bg:#135af2;--button-primary-hover-color:var(--white);--button-primary-hover-border-color:#ffffff26;--button-primary-active-box-shadow:none;--button-primary-active-bg:#1f66ff;--button-primary-active-color:var(--white);--button-primary-active-border-color:#ffffff26;--button-secondary-box-shadow:none;--button-secondary-bg:#242424;--button-secondary-color:var(--text-primary-color);--button-secondary-icon-color:var(--text-muted);--button-secondary-border-color:#3d3d3d;--button-secondary-hover-bg:#2c2c2c;--button-secondary-hover-color:var(--text-primary-color);--button-secondary-hover-border-color:#3d3d3d;--button-secondary-active-box-shadow:none;--button-secondary-active-bg:#313131;--button-secondary-active-color:var(--text-primary-color);--button-secondary-active-border-color:#4d4d4d;--button-success-box-shadow:none;--button-success-bg:#242424;--button-success-color:#56d364;--button-success-icon-color:inherit;--button-success-border-color:#3d3d3d;--button-success-hover-bg:#29903b;--button-success-hover-color:var(--white);--button-success-hover-border-color:#ffffff26;--button-success-active-box-shadow:none;--button-success-active-bg:#2e9a40;--button-success-active-color:var(--white);--button-success-active-border-color:#ffffff26;--button-warning-box-shadow:0 1px 0 0 #1f23280a;--button-warning-bg:#242424;--button-warning-color:#e3b341;--button-warning-icon-color:inherit;--button-warning-border-color:#3d3d3d;--button-warning-hover-bg:#9e6a03;--button-warning-hover-color:var(--white);--button-warning-hover-border-color:#ffffff26;--button-warning-active-box-shadow:none;--button-warning-active-bg:#bb8009;--button-warning-active-color:var(--white);--button-warning-active-border-color:#ffffff26;--button-danger-box-shadow:none;--button-danger-bg:#242424;--button-danger-color:#fa5e55;--button-danger-icon-color:inherit;--button-danger-border-color:#3d3d3d;--button-danger-hover-bg:#b62324;--button-danger-hover-color:var(--white);--button-danger-hover-border-color:#ffffff26;--button-danger-active-box-shadow:none;--button-danger-active-bg:#da3633;--button-danger-active-color:var(--white);--button-danger-active-border-color:#ffffff26;--button-invisible-box-shadow:none;--button-invisible-bg:transparent;--button-invisible-color:inherit;--button-invisible-icon-color:inherit;--button-invisible-border-color:transparent;--button-invisible-hover-bg:#ffffff40;--button-invisible-hover-color:inherit;--button-invisible-hover-border-color:transparent;--button-invisible-active-bg:#ffffff40;--button-invisible-active-color:inherit;--button-invisible-active-box-shadow:none;--button-invisible-active-border-color:transparent;--button-invisible-danger-color:#fa5e55;--button-invisible-danger-hover-color:var(--white);--button-invisible-danger-hover-icon-color:inherit;--button-invisible-danger-hover-hover-bg:#b62324;--button-invisible-danger-active-color:var(--white);--button-invisible-danger-hover-active-bg:#da3633;--text-color:var(--true-gray-300);--text-color-rgb:212,212,212;--text-color-dark:var(--true-gray-200);--text-color-light:var(--true-gray-400);--box-shadow-lg:0 10px 15px -3px rgba(15,23,41,.1),0 4px 6px -2px rgba(15,23,41,.05);--content-panel-bg:#f8fafc;--fieldset-bg:#f5f7fa;--code-color:#c44c34;--code-editor-string-color:#a5d6ff;--code-editor-keyword-color:#ff7b72;--code-editor-comment-color:#7ee787;--code-editor-definition-color:#e36209;--code-editor-variable-color:var(--form-input-text-color);--code-editor-number-color:var(--form-input-text-color);--code-editor-argument-color:#d2a8ff;--code-editor-key-color:#a5d6ff;--code-editor-attribute-color:#7ee787;--code-editor-addition-bg:rgba(46,160,67,.3);--code-editor-deletion-bg:rgba(218,54,51,.3);--code-editor-selection-bg:#203e6f;--page-login-bg:var(--true-gray-800);--page-login-form-bg:var(--true-gray-700);--page-login-form-control-bg:var(--true-gray-800);--page-login-form-control-border-color:var(--true-gray-600);--page-login-form-control-button-bg:var(--blue-700);--text-blue-600:#075692;--text-green-600:#0d5e42;--text-indigo-600:#3c4caa;--text-red-600:#a11b4c;--text-yellow-600:#943505;--color-primary:#70aefb;--color-success:#1ea471;--color-info:#0679b7;--color-warning:#d97817;--color-danger:var(--red-500);--bs-danger-rgb:239,68,68;--highlight-bg:#feff3f;--highlight-color:var(--true-gray-900);--text-on-primary:var(--white);--text-muted:var(--true-gray-500);--link-color:var(--blue-400);--link-hover-color:var(--blue-300);--link-hover-decoration:none;--border-color:#e3e7ee}:root,[data-bs-theme=dark],[data-bs-theme=light]{--bs-body-bg:var(--body-bg);--bs-body-color-rgb:var(--text-color-rgb);--bs-body-color:var(--text-color);--bs-body-font-family:var(--font-family-base);--bs-body-font-size:var(--font-size-base);--bs-body-font-weight:normal;--bs-border-color:var(--border-color);--bs-border-radius-lg:var(--border-radius-lg);--bs-border-radius-sm:var(--border-radius-sm);--bs-border-radius:var(--border-radius);--bs-border-width:var(--border-width);--bs-code-color:var(--code-color);--bs-danger-rgb:var(--color-danger-rgb);--bs-danger:var(--color-danger);--bs-emphasis-color-rgb:var(--text-color-rgb);--bs-emphasis-color:var(--text-color);--bs-font-monospace:var(--font-family-monospace);--bs-form-invalid-border-color:var(--color-danger);--bs-form-invalid-color:var(--color-danger);--bs-form-valid-border-color:var(--color-success);--bs-form-valid-color:var(--color-success);--bs-heading-color:var(--text-color);--bs-highlight-bg:var(--highlight-bg);--bs-highlight-color:inherit;--bs-info:var(--color-info);--bs-link-color-rgb:var(--link-color-rgb);--bs-link-decoration:none;--bs-link-hover-color-rgb:var(--link-hover-color-rgb);--bs-link-opacity:1;--bs-primary:var(--color-primary);--bs-secondary-bg:var(--secondary-bg);--bs-secondary-color:var(--text-secondary-color);--bs-secondary:var(--text-muted);--bs-success:var(--color-success);--bs-tertiary-bg:var(--tertiary-bg);--bs-tertiary-color:var(--text-tertiary-color);--bs-warning:var(--color-warning)}.btn{--bs-btn-padding-x:8px;--bs-btn-padding-y:4px;--bs-btn-font-size:0.875rem;--bs-btn-font-weight:400;--bs-btn-border-width:0;--bs-btn-border-radius:var(--border-radius)}.dropdown-menu{--bs-dropdown-font-size:0.875rem}.table{--bs-table-active-bg:var(--table-selected-cell-bg);--bs-table-active-color:var(--table-cell-color);--bs-table-bg:var(--body-bg);--bs-table-border-color:var(--table-cell-border-color);--bs-table-color:var(--table-cell-color);--bs-table-hover-bg:var(--table-hover-cell-bg);--bs-table-hover-color:var(--table-cell-color)}.pagination{--bs-pagination-padding-y:4px;--bs-pagination-padding-x:10px;--bs-pagination-color:var(--pagination-color);--bs-pagination-line-height:1.5;--bs-pagination-bg:var(--body-bg);--bs-pagination-border-width:1px;--bs-pagination-border-color:transparent;--bs-pagination-focus-box-shadow:none;--bs-pagination-focus-outline:0;--bs-pagination-hover-color:var(--text-color);--bs-pagination-hover-bg:var(--body-bg);--bs-pagination-hover-border-color:var(--pagination-hover-border-color);--bs-pagination-disabled-color:var(--text-muted);--bs-pagination-disabled-bg:var(--body-bg);--bs-pagination-disabled-border-color:transparent}.modal{--bs-modal-zindex:2040;--bs-modal-width:500px;--bs-modal-padding:1rem 1.25rem;--bs-modal-margin:0.5rem;--bs-modal-color:var(--text-color);--bs-modal-bg:var(--modal-bg);--bs-modal-border-color:var(--modal-border-color);--bs-modal-border-width:var(--border-width);--bs-modal-border-radius:var(--border-radius);--bs-modal-box-shadow:var(--bs-box-shadow-sm);--bs-modal-inner-border-radius:calc(var(--bs-border-radius-lg) - var(--bs-border-width));--bs-modal-header-padding-x:1.25rem;--bs-modal-header-padding-y:1rem;--bs-modal-header-padding:1rem 1.25rem;--bs-modal-header-border-color:var(--modal-border-color);--bs-modal-header-border-width:var(--border-width);--bs-modal-title-line-height:1.2;--bs-modal-footer-gap:0.5rem;--bs-modal-footer-bg:var(--modal-footer-bg);--bs-modal-footer-border-color:var(--modal-border-color);--bs-modal-footer-border-width:var(--border-width)}.nav-tabs{--bs-nav-tabs-border-width:var(--border-width);--bs-nav-tabs-border-color:var(--form-tabs-border-color);--bs-nav-tabs-border-radius:var(--border-radius);--bs-nav-tabs-link-hover-border-color:var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color);--bs-nav-tabs-link-active-color:var(--link-color);--bs-nav-tabs-link-active-bg:transparent;--bs-nav-tabs-link-active-border-color:var(--border-color) var(--border-color) transparent var(--border-color)}.badge{--bs-badge-padding-x:5px;--bs-badge-padding-y:1px;--bs-badge-font-size:var(--font-size-xs);--bs-badge-font-weight:500;--bs-badge-color:var(--text-color);--bs-badge-border-radius:var(--bs-border-radius)}.offcanvas,.offcanvas-lg,.offcanvas-md,.offcanvas-sm,.offcanvas-xl,.offcanvas-xxl{--bs-offcanvas-padding-x:20px;--bs-offcanvas-padding-y:15px}.alert{--bs-alert-padding-x:1rem;--bs-alert-padding-y:1rem;--bs-alert-margin-block-end:0;--bs-alert-border-radius:0;--bs-alert-link-color:inherit}:root{color-scheme:light dark}body,html{block-size:100vh}body{background-color:var(--body-bg);color:var(--text-color);font-family:var(--font-family-base);font-size:var(--font-size-base)}i.fa,i.far,i.fas{font-family:Font Awesome\ 6 Free,sans-serif!important}i.fab{font-family:Font Awesome\ 6 Brands,sans-serif!important}i.fal{font-family:Font Awesome\ 6 Pro,sans-serif!important}i.fad{font-family:Font Awesome\ 6 Duotone,sans-serif!important}span.icon{display:inline-block;inline-size:1.25em;text-align:center}span.icon svg{block-size:100%;inline-size:100%;max-block-size:1em;max-inline-size:1em;vertical-align:text-bottom}body[data-ea-icon-prefix=tabler] span.icon svg{max-block-size:1.15em;max-inline-size:1.15em}a{color:var(--link-color);text-decoration:none}a:hover{color:var(--link-hover-color);text-decoration:var(--link-hover-decoration)}code,pre{color:var(--code-color);font-family:var(--font-family-monospace);font-size:13px}pre{line-height:1.8}.text-left{text-align:left}.text-right{text-align:right}@media (min-width:992px){.wrapper{display:grid;grid-template-columns:var(--sidebar-max-width) auto;min-block-size:100vh}}@media (min-width:1280px){.wrapper{grid-column-gap:0}}body:not(.ea-content-width-full) .content-wrapper{max-inline-size:var(--body-max-width)}@media (min-width:992px){body.ea-sidebar-width-compact .wrapper{grid-template-columns:44px auto}}.responsive-header{align-items:center;background:var(--responsive-header-bg);box-shadow:inset 0 -1px 0 var(--responsive-header-border-color);display:flex;justify-content:space-between;padding:8px 15px}@media (min-width:992px){.responsive-header{display:none}}.responsive-header #responsive-header-logo{font-size:var(--font-size-base);font-weight:500;margin:0;padding:0 15px}.responsive-header #responsive-header-logo a{color:var(--responsive-header-logo-color)}.responsive-header .dropdown-settings{display:block}.main-header{display:none}@media (min-width:992px){.main-header{display:block}}.main-header .navbar{display:block;padding:0 0 0 var(--sidebar-menu-items-padding-left)}.main-header #header-logo{overflow:hidden}.main-header #header-logo a{color:var(--sidebar-logo-color);display:block;font-size:var(--font-size-lg);font-weight:500;line-height:24px;padding:17px 0 28px}.main-header #header-logo img,.main-header #header-logo svg{max-inline-size:100%}.main-header #header-logo .logo-custom{display:block}.main-header #header-logo .logo-compact{display:none}@media (min-width:992px){body.ea-sidebar-width-compact .main-header #header-logo .logo-custom{display:none}body.ea-sidebar-width-compact .main-header #header-logo .logo-compact{display:block}}#navigation-toggler{margin-inline-start:-5px}@media (min-width:992px){#navigation-toggler{display:none}}.sidebar-wrapper{position:relative}.sidebar{background:var(--sidebar-bg);block-size:100%;inline-size:calc(40px + var(--sidebar-max-width));inset-block-start:0;inset-inline-start:calc(-40px - var(--sidebar-max-width));min-block-size:100vh;overflow-block:auto;overflow-inline:hidden;overscroll-behavior:contain;padding:15px 20px;position:fixed;transition:left .3s;z-index:calc(var(--zindex-modal-backdrop) + 1)}@media (min-width:992px){.sidebar{box-shadow:inset -1px 0 0 var(--sidebar-border-color);inline-size:auto;max-inline-size:var(--sidebar-max-width);overscroll-behavior:auto;padding:0 var(--sidebar-padding-right) 0 var(--sidebar-padding-left);position:static;z-index:calc(var(--zindex-modal-backdrop) - 1)}}body.ea-mobile-sidebar-visible .sidebar{box-shadow:20px 0 25px -5px rgba(0,0,0,.1),10px 0 10px -5px rgba(0,0,0,.04);inset-inline-start:0}.dropdown-toggle.dropdown-toggle-hidden-marker:after{display:none}.dropdown-toggle.dropdown-toggle-hidden-marker:hover{cursor:pointer}.user-menu-wrapper a.user-details,a.user-menu-wrapper .user-details:hover{align-items:center;-webkit-appearance:none;color:var(--user-name-color);cursor:pointer;display:flex}.user-menu-wrapper.user-is-impersonated a.user-details,.user-menu-wrapper.user-is-impersonated a.user-details:hover{color:var(--user-menu-impersonated-link-color);font-weight:500}.user-menu-wrapper .user-details .user-name{margin-inline-start:6px}.user-menu-wrapper .user-avatar{background:var(--user-avatar-icon-bg);block-size:21px;border-radius:var(--border-radius);color:var(--user-avatar-icon-color);display:block;inline-size:2em;max-inline-size:21px;text-align:center}.user-menu-wrapper .dropdown-user-details .user-avatar .icon{display:block}.user-menu-wrapper .dropdown-menu{max-inline-size:480px;min-inline-size:200px}.user-menu-wrapper .dropdown-menu .dropdown-user-details{align-items:flex-start;display:flex;padding:0 5px}.user-menu-wrapper .dropdown-menu .dropdown-user-details .user-avatar{block-size:39px;inline-size:auto;margin-block-start:2px;margin-inline-end:10px;max-inline-size:39px}.user-menu-wrapper .dropdown-menu .dropdown-user-details .user-avatar .icon{font-size:25px}.user-menu-wrapper .dropdown-menu .dropdown-user-details .user-label{color:var(--text-muted);display:block;font-size:var(--font-size-sm);margin-block-end:2px}.dropdown-settings{display:none}@media (min-width:992px){.dropdown-settings{display:block}}.dropdown-settings .dropdown-settings-button{color:var(--dropdown-settings-icon-color);font-size:16px;padding-inline-start:15px}.dropdown-settings .dropdown-header{color:var(--text-muted);display:block;font-size:var(--font-size-sm)}.dropdown-settings .dropdown-item.active{background:var(--dropdown-settings-active-item-bg);box-shadow:var(--dropdown-settings-active-item-shadow)}.dropdown-settings .dropdown-item.active,.dropdown-settings .dropdown-item.active .icon,.dropdown-settings .dropdown-item.active i{color:var(--dropdown-settings-active-item-color)}.content-wrapper{padding:0 var(--content-padding-right) 15px var(--content-padding-left)}@media (min-width:992px){.content-wrapper{display:grid;grid-template-columns:auto var(--resize-handler-width);padding:0 var(--lg-content-padding-right) var(--lg-content-padding-bottom) var(--lg-content-padding-left)}}.content{margin-block-start:1px}.resizer-handler{display:none}@media (min-width:992px){.resizer-handler{cursor:col-resize;display:block;inline-size:3px;margin:0 0 0 7px;min-block-size:100vh;transition:background .7s}.resizer-handler:hover{background:var(--resize-handler-hover-bg)}}#sidebar-resizer-handler{inset-block-end:0;inset-block-start:0;inset-inline-end:0;min-block-size:100vh;position:absolute}#content-resizer-handler{min-block-size:calc(100vh - 56px - var(--lg-content-padding-bottom))}.content-top{align-items:center;box-shadow:0 1px 0 var(--content-top-border-color);display:flex;padding:5px 15px 5px var(--content-padding-left)}@media (max-width:992px){.content-top.ea-search-disabled{box-shadow:none}}@media (min-width:992px){.content-top{block-size:56px;display:flex;justify-content:space-between;padding:11px calc(var(--lg-content-padding-right) + var(--resize-handler-width)) 11px var(--lg-content-padding-left);position:relative}}.content-top .navbar-custom-menu{display:none}@media (min-width:992px){.content-top .navbar-custom-menu{display:block}}.content-top .content-search{flex:1}.content-top .content-search .form-group{flex-basis:100%;padding:2px 0}.content-top .content-search .form-widget{align-items:center;display:flex;flex:unset}@media (min-width:992px){.content-top .content-search .form-widget{display:block}}.content-top .content-search .content-search-icon{color:var(--content-search-icon-color);margin-inline-end:0}.content-top .content-search .content-search-reset{background:var(--content-search-reset-button-bg);border-radius:var(--border-radius);color:var(--content-search-reset-button-color);font-size:13px;padding:2px}.content-top .content-search .content-search-reset:hover{background:var(--content-search-reset-button-hover-bg);color:var(--content-search-reset-button-hover-color)}.content-top .content-search input[type=search][name=query]{background:var(--content-search-input-bg);border:0;box-shadow:none;max-inline-size:unset}.content-top .content-search input[type=search][name=query]::-webkit-search-cancel-button,.content-top .content-search input[type=search][name=query]::-webkit-search-decoration,.content-top .content-search input[type=search][name=query]::-webkit-search-results-button,.content-top .content-search input[type=search][name=query]::-webkit-search-results-decoration{-webkit-appearance:none}.content-top .content-search input[type=search][name=query]:active,.content-top .content-search input[type=search][name=query]:focus{box-shadow:none;outline:none}.content-top .content-search .content-search-label{align-items:center;display:inline-grid;margin:0;padding:0;@media (min-width:992px){max-inline-size:600px}}.content-top .content-search .content-search-label input,.content-top .content-search .content-search-label:after{grid-area:1/2;inline-size:auto;resize:none}.content-top .content-search .content-search-label input.is-blank{min-inline-size:300px}.content-top .content-search .content-search-label:after{block-size:30px;content:attr(data-value) " ";visibility:hidden;white-space:pre-wrap}.content-header{padding:26px 0 16px}@media (min-width:768px){.content-header{align-items:flex-start;background:var(--body-bg);display:flex;flex-direction:row;justify-content:space-between;padding:36px 0 16px}}@media (min-width:992px){body.ea-edit .content-header,body.ea-new .content-header{inset-block-start:-20px;position:sticky;z-index:999}}.content-header-title{flex:1}.content-header-title .title{font-size:var(--font-size-xxl);font-weight:700;line-height:1.2;margin:0;padding-inline-end:15px}@media (min-width:992px){.content-header-title .title{font-size:var(--font-size-xxxl)}}.content-header-title .title small{color:var(--gray-600);font-size:var(--font-size-lg);font-weight:500;line-height:var(--font-size-lg)}.content-header-help{cursor:pointer}.content-header-help i{color:var(--text-muted);font-size:21px}.popover.ea-content-help-popover{--bs-popover-border-radius:var(--border-radius);border-color:var(--popover-border-color);box-shadow:var(--popover-shadow);max-inline-size:var(--popover-max-width)}.popover.ea-content-help-popover .popover-body{background:var(--popover-bg);border-radius:var(--border-radius);color:var(--popover-color);font-size:var(--font-size-base);padding:15px;text-align:left}.bs-popover-top>.popover-arrow:before,.popover.ea-content-help-popover.bs-popover-auto[data-popper-placement^=top]>.popover-arrow:before{border-block-start-color:var(--popover-border-color)}.bs-popover-top>.popover-arrow:before,.popover.ea-content-help-popover.bs-popover-auto[data-popper-placement^=top]>.popover-arrow:after{border-block-start-color:var(--popover-bg)}.bs-popover-end>.popover-arrow:before,.popover.ea-content-help-popover.bs-popover-auto[data-popper-placement^=right]>.popover-arrow:before{border-inline-end-color:var(--popover-border-color)}.bs-popover-end>.popover-arrow:after,.popover.ea-content-help-popover.bs-popover-auto[data-popper-placement^=right]>.popover-arrow:after{border-inline-end-color:var(--popover-bg)}.bs-popover-bottom>.popover-arrow,.popover.ea-content-help-popover.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow{inset-block-start:-.5rem}.bs-popover-bottom>.popover-arrow:before,.popover.ea-content-help-popover.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow:before{border-block-end-color:var(--popover-border-color)}.bs-popover-bottom>.popover-arrow:after,.popover.ea-content-help-popover.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow:after{border-block-end-color:var(--popover-bg)}.bs-popover-start>.popover-arrow:before,.popover.ea-content-help-popover.bs-popover-auto[data-popper-placement^=left]>.popover-arrow:before{border-inline-start-color:var(--popover-border-color)}.bs-popover-start>.popover-arrow:after,.popover.ea-content-help-popover.bs-popover-auto[data-popper-placement^=left]>.popover-arrow:after{border-inline-start-color:var(--popover-bg)}.ea-content-help-popover.tooltip.show{opacity:1}.content-header .global-actions,.content-header .page-actions{display:flex;flex-direction:row;flex-wrap:wrap;gap:10px;justify-content:right}.content-header .page-actions{margin:10px 0 15px}@media (min-width:768px){.content-header .page-actions{margin:2px 1px 0 10px}}.content-header .page-actions:empty{display:none}.batch-actions form{display:flex}.batch-actions .btn+.btn{margin-inline-start:15px}.with-rounded-top{border-start-end-radius:var(--border-radius);border-start-start-radius:var(--border-radius)}.with-rounded-bottom{border-end-end-radius:var(--border-radius);border-end-start-radius:var(--border-radius)}.datagrid.with-rounded-top thead tr:first-child th:first-child{border-start-start-radius:var(--border-radius)}.datagrid.with-rounded-top thead tr:first-child th:last-child{border-start-end-radius:var(--border-radius)}.content-footer{margin-block-start:15px;padding:15px 0}.content-panel{margin-block-end:20px}.content-panel-header{border-block-end:var(--border-width) var(--border-style) var(--content-section-border-color);font-size:var(--font-size-lg);line-height:24px;margin:0;padding:15px 17px 15px 20px}.content-panel-header.collapsible{padding:0}.content-panel-header.collapsible>a{color:inherit;display:block;padding:15px 17px 15px 20px}.content-panel-header.collapsible.with-help>a{padding:15px 17px 1px 20px}.content-panel-header.collapsible .collapse-icon{color:var(--color-primary);margin-inline-end:5px;transition:all .1s linear}.content-panel-collapse:not(.collapsed) .collapse-icon{transform:rotate(90deg)}.content-panel-header.collapsible.with-help .content-panel-header-help{padding:0 17px 15px 20px}.content-panel-header-help{color:var(--gray-500);font-size:var(--font-size-base)}.content-panel-body{background:var(--white);padding:15px 20px}@media (min-width:992px){.content-panel-body{padding:18px 25px}}.content-panel-body.with-min-h-250{min-block-size:250px}.content-panel-body.with-background{background:var(--content-panel-bg)}.content-panel-body.without-padding{padding:0}.content-panel-body.without-header{border-start-end-radius:var(--border-radius);border-start-start-radius:var(--border-radius)}.content-panel-body.without-footer{border-end-end-radius:var(--border-radius);border-end-start-radius:var(--border-radius)}.content-panel-footer{border-block-start:var(--border-width) var(--border-style) var(--border-color);border-end-end-radius:var(--border-radius);border-end-start-radius:var(--border-radius);margin:0;padding:15px 17px 15px 20px}.content-panel-footer.without-border{border-block-start:0}.content-panel-footer.without-padding{padding:0}.dropdown-menu{--dropdown-padding:4px;background-color:var(--dropdown-bg);border-color:var(--dropdown-border-color);box-shadow:var(--shadow-xl);color:var(--dropdown-color);max-inline-size:240px;padding:5px}.dropdown-menu.dropdown-has-submenus{padding-inline-start:25px}.dropdown-menu li{border-radius:var(--border-radius)}.dropdown-menu a,.dropdown-menu a:active,.dropdown-menu a:hover{border-radius:var(--border-radius);color:var(--dropdown-link-color)}.dropdown-menu a:hover{background:var(--dropdown-link-hover-bg)}.dropdown-menu .icon,.dropdown-menu i{color:var(--dropdown-icon-color);font-size:15px;margin:0 8px 0 0}.dropdown-menu .icon i{margin:0}.dropdown-menu .icon{display:inline-flex;justify-content:center}.dropdown-menu .dropdown-header,.dropdown-menu .dropdown-item{align-items:center;block-size:28px;display:flex;overflow:hidden;padding:0 12px 0 6px;text-overflow:ellipsis;white-space:nowrap}.dropdown-menu .dropdown-divider{background:transparent;block-size:1px;border:0;border-radius:0;box-shadow:0 -1px 0 var(--dropdown-border-color);margin:6px calc(var(--dropdown-padding)*-1);opacity:1}.dropdown-menu .dropdown-item-color-scheme{color:var(--dropdown-color)}.dropdown-menu .dropdown-item-color-scheme:hover{background:transparent}.dropdown-menu .dropdown-item-color-scheme label{align-items:center;display:flex}.dropdown-menu .dropdown-item-color-scheme i{margin-block-start:0}.dropdown-menu .dropdown-item-color-scheme select{background:var(--dropdown-bg);border:1px solid var(--dropdown-border-color);border-radius:var(--border-radius);color:var(--dropdown-color);margin-inline-start:10px;outline:none;padding:0 4px}.dropdown-menu .dropdown-submenu .dropdown-item.dropdown-toggle{border:0;display:flex;padding:0 12px 0 6px;position:relative}.dropdown-menu .dropdown-submenu .dropdown-item.dropdown-toggle:not(.dropdown-toggle-split):hover{cursor:default}.list-pagination{background:var(--table-footer-bg);border-end-end-radius:var(--border-radius);border-end-start-radius:var(--border-radius);color:var(--pagination-color);padding:15px 0}@media (min-width:768px){.list-pagination{align-items:center;display:flex;flex-direction:row;justify-content:space-between}}.list-pagination-counter{color:var(--pagination-color)}.pager .pagination{--bs-pagination-font-size:var(--font-size-sm);--bs-pagination-focus-box-shadow:none;margin-block-end:0}@media (max-width:992px){.pager .pagination{margin-block-start:15px}}.page-item .page-link{white-space:nowrap}.page-item.active .page-link,.page-item.active .page-link:hover{background:var(--pagination-active-bg);border-color:var(--pagination-active-bg);color:var(--pagination-active-color)}.page-item.disabled .page-link{background:transparent;color:var(--pagination-disabled-color)}.page-item .page-link,.page-item .page-link:focus,.page-item .page-link:hover{background:transparent;border:var(--border-width) var(--border-style) transparent;border-radius:var(--border-radius);color:inherit;margin:0 1px}.page-item:not(:first-child) .page-link{margin:0 1px}.page-item .page-link:focus,.page-item .page-link:hover{border-color:var(--pagination-hover-border-color)}@media (max-width:768px){.pager .page-item:not(.page-item-previous,.page-item-next,.page-item.active){display:none}.pager .page-item.active{margin:0 1em}.pager .page-item-next,.pager .page-item-previous{flex:1}.pager .page-item-next .page-link,.pager .page-item-previous .page-link{border:var(--border-width) var(--border-style) var(--border-secondary-color);border-radius:var(--border-radius)}.pager .page-item-next:not(.disabled):hover .page-link,.pager .page-item-previous:not(.disabled):hover .page-link{border-color:var(--link-color)}.pager .page-item-previous .page-link{padding-inline-start:calc(var(--bs-pagination-padding-x)/2)}.pager .page-item-next .page-link{padding-inline-end:calc(var(--bs-pagination-padding-x)/2);text-align:right}}.modal-content{border-color:var(--modal-border-color)}.modal-body{background:var(--modal-bg)}.modal-body h4{font-size:var(--font-size-lg)}.modal-footer{background:var(--modal-footer-bg);border-color:var(--modal-border-color);padding:8px 10px}#flash-messages{background:transparent}.alert{border-width:0 0 var(--border-width);margin-block-end:0}.alert:last-of-type{border-block-end-width:2px}.alert-dismissible .btn-close{--bs-btn-close-opacity:1;--bs-btn-close-hover-opacity:1;inset-block-start:10px;inset-inline-end:5px;padding:var(--button-padding-y-md) var(--button-padding-x-md)}[data-bs-theme=dark] .btn-close{filter:none}.alert.alert-primary{--bs-alert-bg:var(--alert-primary-bg);--bs-alert-border-color:var(--alert-primary-border-color);--bs-alert-color:var(--alert-primary-color)}.alert.alert-secondary{--bs-alert-bg:var(--alert-secondary-bg);--bs-alert-border-color:var(--alert-secondary-border-color);--bs-alert-color:var(--alert-secondary-color)}.alert.alert-success{--bs-alert-bg:var(--alert-success-bg);--bs-alert-border-color:var(--alert-success-border-color);--bs-alert-color:var(--alert-success-color)}.alert.alert-info{--bs-alert-bg:var(--alert-info-bg);--bs-alert-border-color:var(--alert-info-border-color);--bs-alert-color:var(--alert-info-color)}.alert.alert-warning{--bs-alert-bg:var(--alert-warning-bg);--bs-alert-border-color:var(--alert-warning-border-color);--bs-alert-color:var(--alert-warning-color)}.alert.alert-danger{--bs-alert-bg:var(--alert-danger-bg);--bs-alert-border-color:var(--alert-danger-border-color);--bs-alert-color:var(--alert-danger-color)}.alert.alert-light{--bs-alert-bg:var(--alert-light-bg);--bs-alert-border-color:var(--alert-light-border-color);--bs-alert-color:var(--alert-light-color)}.alert.alert-dark{--bs-alert-bg:var(--alert-dark-bg);--bs-alert-border-color:var(--alert-dark-border-color);--bs-alert-color:var(--alert-dark-color)}.text-primary{color:var(--text-primary-color)!important}.text-secondary{color:var(--text-secondary-color)!important}.text-tertiary{color:var(--text-tertiary-color)!important}.border-primary{border:1px solid var(--border-primary-color)!important}.border-secondary{border:1px solid var(--border-secondary-color)!important}.border-tertiary{border:1px solid var(--border-tertiary-color)!important}.background-primary{background-color:var(--primary-bg)!important}.background-secondary{background-color:var(--secondary-bg)!important}.background-tertiary{background-color:var(--tertiary-bg)!important}#main-menu{padding:0 0 20px}#main-menu .menu{padding-inline-start:0}#main-menu .menu li{list-style:none}#main-menu .menu .menu-header{color:var(--sidebar-menu-header-color);font-size:12px;font-weight:500;line-height:15px;margin-block-start:15px;padding:7px 5px 7px var(--sidebar-menu-items-padding-left);text-transform:uppercase}#main-menu .menu .menu-header:first-child{margin-block-start:0}#main-menu .menu .menu-header .menu-icon{color:inherit;margin:0 8px 0 0}#main-menu .menu .menu-header .menu-header-contents{display:block}#main-menu .menu .menu-header .menu-item-badge{float:right;inset-block-start:0;margin-inline-start:16px}#main-menu .menu .menu-item{border-radius:var(--border-radius);padding-inline-end:5px;padding-inline-start:var(--sidebar-menu-items-padding-left);position:relative}#main-menu .menu .menu-item.active{background:var(--sidebar-menu-active-item-bg)}#main-menu .menu .menu-item.active .menu-item-label{font-weight:500}.ea-light-scheme #main-menu .menu .menu-item .menu-item-badge{box-shadow:inset 0 0 0 1px rgba(0,0,0,.1)}.ea-light-scheme #main-menu .menu .menu-item.active .menu-item-badge{box-shadow:inset 0 0 0 1px rgba(0,0,0,.2)}.ea-dark-scheme #main-menu .menu .menu-item.active .menu-item-badge{background:var(--sidebar-bg);box-shadow:inset 0 0 0 1px transparent}#main-menu .menu .menu-item.active:not(.expanded) .menu-icon,#main-menu .menu .menu-item.active:not(.expanded) a{color:var(--sidebar-menu-active-item-color)}#main-menu .menu .menu-item.has-submenu.expanded .submenu-toggle-icon{transform:rotate(90deg)}#main-menu .menu .menu-item.has-submenu:not(.expanded) .submenu{max-block-size:0}#main-menu .menu .menu-item .submenu-toggle .submenu-toggle-icon{color:var(--sidebar-menu-icon-color);inline-size:auto;transition:transform .25s ease}#main-menu .menu .menu-item-contents{align-items:flex-start;color:var(--sidebar-menu-color);display:flex;padding:4px 0}#main-menu .menu .menu-icon{block-size:16px;color:var(--sidebar-menu-icon-color);flex-shrink:0;inline-size:1.25em;margin-inline-end:10px;text-align:center}#main-menu .menu .menu-icon svg{color:var(--sidebar-menu-icon-color);max-block-size:16px;max-inline-size:20px;vertical-align:sub}#main-menu .menu .menu-item-badge{float:right;inset-block-start:2px;margin:0 0 0 8px;min-inline-size:25px;position:relative}#main-menu .menu .menu-item-badge.badge-secondary{background:var(--sidebar-menu-badge-bg);color:var(--sidebar-menu-badge-color)}#main-menu .menu .submenu-toggle-icon{float:right;margin-inline-start:8px}#main-menu .menu .submenu{overflow:hidden;padding:0;transition:max-block-size .15s linear}#main-menu .menu .submenu a{color:var(--sidebar-menu-submenu-color);padding:3px 0 3px 26px}#main-menu .menu .submenu .menu-header{padding-inline-start:26px}#main-menu .menu .submenu .menu-item{margin:5px 0;padding-inline-end:0}#main-menu .menu .submenu .menu-item.active{margin-inline-start:0;padding-inline-start:6px}#main-menu .menu .submenu .menu-icon{font-size:var(--font-size-base);margin-inline-end:5px}#main-menu .menu .submenu .menu-item-badge{margin-inline-end:4px}body.ea-sidebar-width-compact .sidebar{overflow:visible;padding:0}body.ea-sidebar-width-compact .sidebar #main-menu .menu .menu-item,body.ea-sidebar-width-compact .sidebar .main-header .navbar{padding-inline-start:var(--sidebar-padding-left)}@media (min-width:992px){body.ea-sidebar-width-compact #main-menu .menu .menu-item{border-radius:0 var(--border-radius) var(--border-radius) 0;padding-inline-end:0}body.ea-sidebar-width-compact #main-menu .menu .menu-item .menu-item-badge,body.ea-sidebar-width-compact #main-menu .menu .menu-item .menu-item-label,body.ea-sidebar-width-compact #main-menu .menu .menu-item .submenu,body.ea-sidebar-width-compact #main-menu .menu .menu-item .submenu-toggle-icon{display:none}body.ea-sidebar-width-compact #main-menu .menu .menu-item .menu-item-label{flex:1}body.ea-sidebar-width-compact #main-menu .menu .menu-item .menu-item-contents{align-items:center;border-radius:0 var(--border-radius) var(--border-radius) 0;display:flex;min-inline-size:max-content;padding:7px 5px 7px 0}body.ea-sidebar-width-compact #main-menu .menu .menu-item:hover{background:var(--body-bg);box-shadow:var(--sidebar-menu-compact-hover-box-shadow);min-inline-size:max-content;padding-inline-start:var(--sidebar-padding-left);z-index:var(--zindex-modal-backdrop)}body.ea-sidebar-width-compact #main-menu .menu .menu-item:hover i{color:var(--sidebar-menu-icon-color)!important}body.ea-sidebar-width-compact #main-menu .menu .menu-item:hover .menu-item-badge,body.ea-sidebar-width-compact #main-menu .menu .menu-item:hover .menu-item-label,body.ea-sidebar-width-compact #main-menu .menu .menu-item:hover .submenu,body.ea-sidebar-width-compact #main-menu .menu .menu-item:hover .submenu-toggle-icon{display:block}body.ea-sidebar-width-compact #main-menu .menu .menu-item:hover .menu-item-contents{background:var(--body-bg);color:var(--text-color)}body.ea-sidebar-width-compact #main-menu .menu .menu-item:hover .submenu{background:var(--body-bg);border-radius:0 var(--border-radius) var(--border-radius) var(--border-radius);inline-size:max-content;inset-block-start:0;margin-inline-start:34px;padding:2px 10px 0 0;position:absolute}body.ea-sidebar-width-compact #main-menu .menu .menu-item:hover .submenu a{padding:3px 5px 3px 13px}body.ea-sidebar-width-compact #main-menu .menu .menu-item.has-submenu:hover .submenu-toggle .menu-item-label{display:none}body.ea-sidebar-width-compact #main-menu .menu .menu-item.has-submenu:hover .submenu-toggle-icon{display:inline-block;font-size:18px;inset-block-start:0;inset-inline-start:-7px;transform:rotate(0);z-index:9999}body.ea-sidebar-width-compact #main-menu .menu .menu-item.has-submenu:hover .submenu .menu-icon{margin-inline-end:8px}body.ea-sidebar-width-compact #main-menu .menu .menu-item .menu-icon{block-size:21px;font-size:18px;line-height:normal;max-inline-size:21px}body.ea-sidebar-width-compact #main-menu .menu .menu-item .submenu .menu-icon{font-size:16px;inline-size:21px;inset-inline-start:-4px;position:relative}body.ea-sidebar-width-compact #main-menu .menu .menu-item .submenu{box-shadow:var(--sidebar-menu-compact-hover-box-shadow);max-block-size:none!important;padding-block-end:5px;padding-block-start:5px}body.ea-sidebar-width-compact #main-menu .menu .menu-item .submenu .menu-item:hover{box-shadow:none}body.ea-sidebar-width-compact #main-menu .menu .menu-header{block-size:0;inline-size:0;overflow:hidden;padding:0}}table.datagrid{border-collapse:collapse;border-spacing:0;color:var(--table-cell-color);inline-size:100%;margin-block-end:0}table.datagrid:not(.datagrid-empty) tr:not(.empty-row) td.actions,table.datagrid:not(.datagrid-empty) tr:not(.empty-row) td.actions.actions-as-dropdown{min-inline-size:50px}@media (max-width:767px){table.datagrid:not(.datagrid-empty) tbody,table.datagrid:not(.datagrid-empty) td,table.datagrid:not(.datagrid-empty) tr{display:block}table.datagrid:not(.datagrid-empty) tbody,table.datagrid:not(.datagrid-empty) tr{border-radius:var(--border-radius)}table.datagrid:not(.datagrid-empty) tbody tr td:first-of-type{border-start-end-radius:var(--border-radius);border-start-start-radius:var(--border-radius)}table.datagrid:not(.datagrid-empty) tbody tr td:last-of-type{border-end-end-radius:var(--border-radius);border-end-start-radius:var(--border-radius)}table.datagrid:not(.datagrid-empty) thead{display:none}table.datagrid:not(.datagrid-empty) tr{border:1px solid var(--responsive-table-row-border-color);margin-block-end:30px}table.datagrid:not(.datagrid-empty) tr:not(.empty-row) td{box-shadow:inset 0 1px 0 var(--table-cell-border-color);min-block-size:36px;padding-inline-start:35%;position:relative}table.datagrid:not(.datagrid-empty) tr:not(.empty-row) td:first-child{box-shadow:none}table.datagrid:not(.datagrid-empty) tr:not(.empty-row) td.batch-actions-selector{padding:8px}table.datagrid:not(.datagrid-empty) tr:not(.empty-row) td.batch-actions-selector:before{display:none}table.datagrid:not(.datagrid-empty) tr:not(.empty-row) td.actions,table.datagrid:not(.datagrid-empty) tr:not(.empty-row) td.actions.actions-as-dropdown{padding:8px}table.datagrid:not(.datagrid-empty) tr:not(.empty-row) td.actions.actions-as-dropdown:before{display:none}table.datagrid:not(.datagrid-empty) td{text-align:left!important}table.datagrid:not(.datagrid-empty) td:before{color:var(--responsive-table-label-color);content:attr(data-label);font-weight:500;inline-size:35%;inset-block-end:0;inset-block-start:0;inset-inline-start:0;overflow:hidden;padding:8px;position:absolute;text-align:left;text-overflow:ellipsis;white-space:nowrap}table.datagrid:not(.datagrid-empty) td.field-boolean{padding-inline-start:8px}table.datagrid:not(.datagrid-empty) td.field-boolean:before{color:var(--table-cell-color);font-weight:400;inset-inline-start:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}table.datagrid:not(.datagrid-empty) td.actions:before{display:none}}.datagrid thead th{border:0;box-shadow:inset 0 -2px 0 var(--table-cell-border-color);padding:0}.datagrid thead a,.datagrid thead span:not(.icon){color:var(--table-thead-color);display:block;font-weight:500;line-height:1.357;padding:12px 8px;white-space:nowrap}.datagrid td{box-shadow:inset 0 1px 0 var(--table-cell-border-color);line-height:20px;padding:8px}.datagrid tbody{box-shadow:0 1px 0 var(--table-cell-border-color)}@media (min-width:992px){.datagrid thead+tbody tr:first-child td{box-shadow:none}}.datagrid td.field-avatar{padding:4px 8px}.datagrid thead .sorted a,.datagrid thead .sorted span{font-weight:700}.datagrid thead .icon,.datagrid thead i{color:var(--table-thead-marker-color);margin-inline-start:2px}.datagrid thead .sorted{box-shadow:inset 0 -2px 0 var(--color-primary)}.datagrid thead .sorted a,.datagrid thead .sorted span{color:var(--table-thead-sorted-color)}.datagrid thead .sorted .icon,.datagrid thead .sorted i{color:var(--table-thead-sorted-marker-color);display:inline-block}.datagrid td,.datagrid th{border:none;vertical-align:middle}@media (min-width:992px){.datagrid tbody tr:hover td,.datagrid tbody tr:hover th{background:var(--table-hover-cell-bg)}}.datagrid tbody tr.selected-row td{background:var(--table-selected-cell-bg)}.datagrid tbody tr.selected-row td ::-moz-selection{background:transparent}.datagrid tr.ea-clickable-row{cursor:pointer}.datagrid tr.ea-clickable-row td.actions,.datagrid tr.ea-clickable-row td.batch-actions-selector{cursor:default}.datagrid tr.ea-clickable-row td.actions a,.datagrid tr.ea-clickable-row td.actions button,.datagrid tr.ea-clickable-row td.batch-actions-selector .form-check{cursor:pointer}.datagrid td.actions{text-align:right}.datagrid td.actions:not(.actions-as-dropdown) form{display:inline;margin-inline-end:10px;margin-inline-start:10px}.datagrid td.actions a:not(.dropdown-item){font-size:var(--font-size-sm);font-weight:500}.datagrid td.actions a:not(.dropdown-item)+a:not(.dropdown-item){margin-inline-start:10px}.datagrid td.actions a:not(.dropdown-item) .action-icon{font-size:var(--font-size-base);margin-inline-end:2px}.datagrid td.actions .dropdown-item-variant-success:hover,.page-actions .dropdown-item-variant-success:hover{--dropdown-icon-color:var(--dropdown-item-success-color);background:var(--dropdown-item-success-bg);color:var(--dropdown-item-success-color)}.datagrid td.actions .dropdown-item-variant-warning:hover,.page-actions .dropdown-item-variant-warning:hover{--dropdown-icon-color:var(--dropdown-item-warning-color);background:var(--dropdown-item-warning-bg);color:var(--dropdown-item-warning-color)}.datagrid td.actions .dropdown-item-variant-danger:hover,.page-actions .dropdown-item-variant-danger:hover{--dropdown-icon-color:var(--dropdown-item-danger-color);background:var(--dropdown-item-danger-bg);color:var(--dropdown-item-danger-color)}@media (min-width:992px){.datagrid td.actions-as-dropdown{padding:2px 8px}}.datagrid td.actions-as-dropdown-table-head{inline-size:10px}.datagrid tr:not(.selected-row):hover .actions-as-dropdown .dropdown-actions>.dropdown-toggle{background:var(--dropdown-toggle-bg);border-color:var(--dropdown-toggle-border-color)}.datagrid tr:hover .actions-as-dropdown .dropdown-actions>.dropdown-toggle:hover{border-color:var(--dropdown-toggle-hover-border-color)}.datagrid .dropdown-toggle.show,.datagrid .dropdown-toggle:active,.datagrid .dropdown-toggle:active:focus,.datagrid .dropdown-toggle:focus,.datagrid tr .dropdown-toggle.show,.datagrid tr:hover .dropdown-toggle.show,.datagrid tr:hover .dropdown-toggle:active,.datagrid tr:hover .dropdown-toggle:active:focus,.datagrid tr:hover .dropdown-toggle:focus{border-color:var(--dropdown-toggle-hover-border-color);box-shadow:var(--button-active-shadow);outline:none}.datagrid .dropdown-actions{display:inline-block}.datagrid .dropdown-actions .dropdown-toggle{border:1px solid transparent;border-radius:var(--border-radius);color:var(--dropdown-toggle-color);display:block;overflow:visible;padding:1px 5px}.datagrid .dropdown-actions .dropdown-toggle .icon{display:block;font-size:21px;inline-size:unset}.datagrid .dropdown-actions .dropdown-menu{z-index:var(--zindex-900)}.datagrid .dropdown-actions .dropdown-menu .dropstart{position:relative}.datagrid .dropdown-actions .dropstart .dropdown-toggle:before{margin-inline-start:-20px;position:absolute}.datagrid .dropdown-actions .dropdown-menu .dropstart>.dropdown-menu{inset-block-start:0;inset-inline-end:100%;inset-inline-start:auto;margin-block-end:0;margin-block-start:0;margin-inline-end:-.125rem;margin-inline-start:0}.datagrid .dropdown-actions .dropdown-menu .dropstart:has(.dropdown-toggle-split)>.dropdown-menu{margin-inline-end:1.125rem}.datagrid .dropdown-actions .dropdown-menu .dropstart:has(.dropdown-toggle-split)>.dropdown-menu:hover,.datagrid .dropdown-actions .dropdown-menu .dropstart:has(.dropdown-toggle-split:hover)>.dropdown-menu,.datagrid .dropdown-actions .dropdown-menu .dropstart:not(:has(.dropdown-toggle-split)):hover>.dropdown-menu,.datagrid .dropdown-actions .dropdown-menu .dropstart:not(:has(.dropdown-toggle-split))>.dropdown-menu:hover,.datagrid .dropdown-actions .dropdown-menu .dropstart:not(:has(.dropdown-toggle-split))>.dropdown-toggle:focus .dropdown-menu{display:block}.datagrid .dropdown-actions .dropdown-menu .dropstart .dropdown-toggle-split{inset-inline-start:-22px;padding-inline-end:.5rem;padding-inline-start:.5rem;position:absolute}.datagrid .dropdown-actions .dropdown-menu .dropstart .dropdown-toggle-split:before{display:none}.datagrid .dropdown-actions .dropdown-menu .dropstart .dropdown-toggle-split .dropdown-toggle-marker{border-block-end:.3em solid transparent;border-block-start:.3em solid transparent;border-inline-end:.3em solid;content:"";display:inline-block}.datagrid .ea-lightbox-thumbnail img{background:var(--white);border:1px solid transparent;border-radius:var(--border-radius);max-block-size:50px;max-inline-size:100px;padding:2px 4px}.datagrid tr:hover .ea-lightbox-thumbnail img{border-color:var(--border-color)}.datagrid mark{background:var(--highlight-bg);border-radius:0;color:var(--highlight-color);padding:0}.datagrid .field-boolean,.datagrid .header-for-field-boolean{text-align:center}.datagrid .field-boolean.has-switch{padding:6px 8px}.datagrid .field-boolean .form-switch{display:inline-flex;justify-content:center;margin-block-end:0;padding-inline-start:0}.datagrid .field-boolean .form-switch input{inset-block-start:3px;margin-block-start:0;position:relative}@media (max-width:992px){.datagrid .field-country{text-align:left!important}}.datagrid .form-check{margin-block-end:0;min-block-size:15px;padding-inline-start:0}.datagrid .no-results td{font-size:var(--font-size-lg);padding:24px 0;text-align:center}.datagrid .empty-row:hover td,.datagrid .no-results:hover td{background:transparent}.datagrid .empty-row td{padding:0 10px}.datagrid .empty-row td:first-child{inline-size:20%}.datagrid .empty-row td:nth-child(2){display:none}@media (min-width:992px){.datagrid .empty-row td:nth-child(2){inline-size:5%}}.datagrid .empty-row td:nth-child(3){inline-size:10%}.datagrid .empty-row td:nth-child(4){inline-size:25%}.datagrid .empty-row td:nth-child(5){inline-size:10%}.datagrid .empty-row td:nth-child(6){inline-size:30%}.datagrid .empty-row td span{background:var(--datagrid-noresults-placeholder-bg);block-size:10px;border-radius:var(--border-radius);display:block;inline-size:100%;margin:13px 0}.datagrid tbody .datagrid-row-empty:hover td,.datagrid-row-empty td{background-color:transparent;background-image:linear-gradient(135deg,var(--datagrid-hidden-results-gradient-bg) 25%,transparent 25%,transparent 50%,var(--datagrid-hidden-results-gradient-bg) 50%,var(--datagrid-hidden-results-gradient-bg) 75%,transparent 75%,transparent 100%);background-size:40px 40px;padding-block-end:15px;padding-block-start:15px}.datagrid-row-empty-message{background:var(--body-bg);border-radius:var(--border-radius);padding:2px 4px}.datagrid-header-tools{display:flex;padding:0 0 10px}.datagrid-header-tools .datagrid-search{flex:1;margin-inline-end:15px;max-inline-size:480px}.datagrid-header-tools .datagrid-search .form-group,.datagrid-header-tools .datagrid-search .form-group .form-widget{flex:1;margin:0;padding:0}.datagrid-header-tools .datagrid-search input[type=search].form-control{background-color:var(--white);background-image:url('data:image/svg+xml;utf8,');background-position:10px 8px;background-repeat:no-repeat;background-size:13px 13px;min-inline-size:100%;padding:0 32px}.datagrid-header-tools .datagrid-search .form-widget{position:relative}.datagrid-header-tools .datagrid-search a.action-search-reset{color:var(--gray-500);inset-block-start:1px;inset-inline-end:1px;padding:4px 7px;position:absolute;text-decoration:none}.datagrid-header-tools .datagrid-search a.action-search-reset:hover{color:var(--gray-700)}#modal-filters .modal-dialog{max-inline-size:400px}#modal-filters .modal-content{background:var(--modal-bg);border:1px solid var(--modal-border-color);border-radius:var(--border-radius)}#modal-filters .modal-header{background:var(--modal-header-bg);border-block-end-color:transparent;padding:10px 15px}#modal-filters .modal-title{color:var(--modal-title-color);font-size:var(--font-size-base)}#modal-filters .modal-body{background:var(--modal-bg);border-block-end:0;border-radius:var(--border-radius);padding:15px}.action-filters-button .icon{color:var(--text-color-light)}.action-filters-button.action-filters-applied i{color:var(--color-primary)}.action-filters-button .action-filters-button-count{color:var(--color-primary);font-weight:600}.action-filters-reset i{color:var(--text-color-light)}.filter-field{border-block-start:1px solid var(--modal-border-color)}.filter-heading{align-items:center;display:flex;padding:10px 0}.filter-heading a{color:var(--link-color);cursor:pointer;flex:1;margin-inline-start:7px}.filter-content{margin:-5px 0 0 15px;padding:0 0 10px}.filter-content .form-group,.filter-content .form-widget-compound .form-group{display:block;padding:4px 0}.filter-content .form-widget-compound label{display:none}.filter-content .form-widget-compound label.form-check-label{display:inline-block}.filter-content .form-check-inline{align-items:flex-start;display:inline-flex}.filter-content .form-check.form-check-inline{margin-block-start:0}.filter-content .form-group label.required:after{content:none}.filter-content .field-choice .form-check+.form-check{margin-block-start:4px}.filter-content .field-choice .form-check-label{margin-block-start:0}.table.datagrid>:not(:first-child){border-block-start-style:none}.ea-detail .form-column .form-fieldset-body{padding-block-end:7px;padding-block-start:5px}.ea-detail .form-column .form-fieldset-body.without-header{padding-block-end:10px;padding-block-start:var(--bs-gutter-x)}.ea-detail .field-group{display:flex;margin-block-end:12px}.ea-detail .field-group .field-label{color:var(--form-label-color);font-size:var(--font-size-base);font-weight:500;inline-size:130px;margin:0 15px 0 0;padding:0 0 1px;text-align:right}.ea-detail .field-group .field-label:empty{display:none}.ea-detail .field-group .field-label div[data-bs-toggle=tooltip]{cursor:pointer;text-decoration:underline;text-decoration-color:var(--detail-label-tooltip-underline-color);text-decoration-style:dotted;text-underline-offset:2px}.tooltip.ea-detail-label-tooltip{--bs-tooltip-max-width:350px;--bs-tooltip-border-radius:var(--border-radius);--bs-tooltip-padding-x:20px;--bs-tooltip-padding-y:10px;--bs-tooltip-opacity:1}.tooltip.ea-detail-label-tooltip .tooltip-inner{font-size:13px;text-align:start}.ea-detail .field-group .field-value{flex:1;min-inline-size:66%}.ea-detail .field-group.field-text_editor .field-value,.ea-detail .field-group.field-textarea .field-value{max-block-size:350px;max-inline-size:80ch;overflow-block:auto}.ea-detail .field-group.field-boolean{flex-direction:row-reverse}.ea-detail .field-group.field-boolean .field-label{flex:1;margin:0 0 0 15px;min-inline-size:66%;text-align:left}.ea-detail .field-group.field-boolean .field-value{flex:unset;inline-size:130px;min-inline-size:0;text-align:right}.field-array ul{margin-block-end:0;padding-inline-start:1.2em}.field-array li+li{margin-block-start:4px}.field-avatar .image-avatar{border:0;border-radius:var(--border-radius);box-shadow:none}.field-boolean .badge{min-inline-size:33px;text-transform:uppercase}.field-boolean .badge-boolean-false{background:var(--badge-boolean-false-bg);border:0;box-shadow:var(--badge-boolean-false-box-shadow);color:var(--badge-boolean-false-color)}.field-boolean .badge-boolean-true{background:var(--badge-boolean-true-bg);border:0;box-shadow:var(--badge-boolean-true-box-shadow);color:var(--badge-boolean-true-color)}.field-code_editor .form-widget{flex:1}.field-code_editor dt{max-block-size:480px;overflow-block:auto}.form-widget-compound .collection-empty{margin-block-end:10px;padding-block-start:5px}.form-group.field-collection label:empty{display:none}.form-group.field-array .form-widget .form-group{padding:6px 0}.form-group.field-array .form-widget .form-group label{display:none}.form-group.field-array .field-collection-item+.field-collection-item{margin-block-start:5px}.form-group.field-array .field-collection-item{display:flex}.form-group.field-collection .accordion{border-radius:var(--border-radius);box-shadow:inset 0 0 0 1px var(--form-input-border-color)}.form-group.field-collection .accordion .form-group{padding:0}.form-group.field-collection .accordion-header{padding-inline-end:28px;position:relative}.form-group.field-collection .accordion-header:hover{background:var(--form-type-collection-item-collapsed-hover-bg);box-shadow:inset 0 0 0 1px var(--form-input-border-color)}.form-group.field-collection .accordion-header .accordion-button{font-size:var(--font-size-base)}.form-group.field-collection .accordion-item{background:transparent;border:0;border-radius:0;box-shadow:inset 0 -1px 0 var(--form-input-border-color)}.form-group.field-collection .field-collection-item-first .accordion-header,.form-group.field-collection .field-collection-item-first .accordion-item{border-start-end-radius:var(--border-radius);border-start-start-radius:var(--border-radius)}.form-group.field-collection .field-collection-item-last .accordion-header,.form-group.field-collection .field-collection-item-last .accordion-item{border-end-end-radius:var(--border-radius);border-end-start-radius:var(--border-radius)}.form-group.field-collection .field-collection-item.field-collection-item-last .accordion-item{box-shadow:none}.form-group.field-collection .accordion-item .form-group{align-items:flex-start;display:flex;padding:12px 0}.form-group.field-collection .accordion-item .form-group legend.col-form-label,.form-group.field-collection .accordion-item .form-group>label{font-weight:500;inline-size:20%;margin:3px 10px 0 0;padding:0}.form-group.field-collection .accordion-item .accordion-body .form-widget{flex:1}.form-group.field-collection .accordion-button,.form-group.field-collection .accordion-button:hover{background:transparent;border-radius:0;box-shadow:none;color:var(--text-color);flex:1;padding:8px 7px}.form-group.field-collection .accordion-button:after{display:none}.form-group.field-collection .accordion-button i{transition:transform .2s ease-in-out}.form-group.field-collection .accordion-button:not(.collapsed) i{transform:rotate(90deg)}.form-group.field-collection .accordion-button .form-collection-item-collapse-marker{color:var(--form-collection-item-collapse-marker-color);margin:0 8px 0 4px}.form-group.field-collection .field-collection-add-button{margin-block-start:5px}.form-group.field-collection .field-collection-delete-button{inset-block-start:1px;inset-inline-end:5px;position:absolute}.field-color .color-sample{block-size:19px;border-radius:var(--border-radius);box-shadow:0 0 0 2px var(--border-tertiary-color),0 0 0 3px var(--border-secondary-color);display:inline-block;inline-size:45px}.field-country .country-flag{border-radius:2px;margin:0 6px 1px 0;max-block-size:17px;outline:1px solid rgba(0,0,0,.2);outline-offset:-1px;vertical-align:text-top}.ea-dark-scheme .field-country .country-flag{outline-color:var(--border-secondary-color);outline-offset:0}.datagrid .field-country>span+span,.datalist .field-country dd>span+span{margin-inline-start:10px}.field-country .ts-control .country-name-flag,.field-country .ts-dropdown-content .country-name-flag .country-flag{margin-block-end:0}.field-country .ts-wrapper.multi .ts-control>div{margin-block-end:5px}.field-country .ts-wrapper.multi .ts-control .country-name-flag{margin-inline-end:25px}.field-country .ts-wrapper.multi.plugin-remove_button .item .remove{border-color:var(--form-type-autocomplete-multi-item-border-color)}.field-currency .badge-currency{border:2px solid var(--gray-300);display:inline-block;font-size:12px;padding:2px 4px;text-transform:uppercase}.field-date input[type=date].form-control,.field-datetime input[type=datetime-local].form-control,.field-time input[type=time].form-control{inline-size:auto;max-inline-size:100%}.field-language .badge-language{border:2px solid var(--field-language-badge-border-color);box-shadow:none;display:inline-block;font-size:12px;padding:2px 4px;text-transform:uppercase}.field-text_editor dt{max-block-size:480px;overflow-block:auto}.detail .field-image .form-control{background:transparent;block-size:auto;border:0;padding:0}.ea-detail .field-image .ea-lightbox-thumbnail{display:block;max-inline-size:400px}.ea-detail .field-image img{border:1px solid transparent;border-radius:var(--border-radius);max-block-size:300px;padding:8px}.ea-detail .field-image img:hover{border-color:var(--datalist-border-color)}.ea-lightbox-thumbnail img:hover{cursor:zoom-in}.ea-lightbox{display:none}.ea-lightbox img{inline-size:100%;max-inline-size:100%}.basicLightbox{align-items:center;block-size:100vh;display:flex;inline-size:100%;inset-block-start:0;inset-inline-start:0;justify-content:center;opacity:.01;position:fixed;transition:opacity .4s ease;will-change:opacity;z-index:1000}.basicLightbox--visible{opacity:1}.basicLightbox__placeholder{max-inline-size:100%;transform:scale(.9);transition:transform .4s ease;will-change:transform;z-index:1}.basicLightbox__placeholder>iframe:first-child:last-child,.basicLightbox__placeholder>img:first-child:last-child,.basicLightbox__placeholder>video:first-child:last-child{display:block;inset-block-end:0;inset-block-start:0;inset-inline-end:0;inset-inline-start:0;margin:auto;max-block-size:95%;max-inline-size:95%;position:absolute}.basicLightbox__placeholder>iframe:first-child:last-child,.basicLightbox__placeholder>video:first-child:last-child{pointer-events:auto}.basicLightbox__placeholder>img:first-child:last-child,.basicLightbox__placeholder>video:first-child:last-child{block-size:auto;inline-size:auto}.basicLightbox--iframe .basicLightbox__placeholder,.basicLightbox--img .basicLightbox__placeholder,.basicLightbox--video .basicLightbox__placeholder{block-size:100%;inline-size:100%;pointer-events:none}.basicLightbox--visible .basicLightbox__placeholder{transform:scale(1)}.basicLightbox{background:rgba(0,0,0,.8);transition:opacity .3s ease;z-index:10000}.basicLightbox__placeholder{margin-inline-end:5%;margin-inline-start:5%;max-block-size:95%;transition:opacity .3s ease}.basicLightbox__placeholder img{background:#fff;padding:25px}.basicLightbox__placeholder img:hover{cursor:zoom-out}input[disabled]{cursor:not-allowed}.form-inline{align-items:center;display:flex;flex-flow:row wrap}.form-group{padding:0 0 24px}.form-group label,.form-group legend.col-form-label{color:var(--form-label-color);font-size:var(--font-size-base);font-weight:500;margin:0;padding:0 0 8px}.form-check .form-check-input{block-size:15px;border-color:var(--form-type-check-input-border-color);inline-size:15px}.form-check:not(.form-switch) .form-check-input:not(:checked){background-color:unset}label.form-check-label{cursor:pointer;font-weight:400}.form-group label.form-check-label.required:after{display:none}.form-widget .form-check+.form-check{margin-block-start:5px}.form-group .col-form-label.required:after,.form-group label.required:after{background:var(--color-danger);block-size:4px;border-radius:50%;content:"";display:inline-block;filter:opacity(75%);inline-size:4px;inset-block-start:-8px;inset-inline-end:-2px;position:relative;z-index:var(--zindex-700)}.form-widget .form-help{color:var(--form-help-color);display:block;font-size:var(--font-size-sm);margin-block-start:5px;transition:color .5s ease}.form-widget:focus-within .form-help{color:var(--form-help-active-color)}.form-widget .form-select,.form-widget input.form-control,.form-widget textarea.form-control{background-color:var(--form-control-bg);background-repeat:no-repeat;block-size:30px;border:1px solid var(--form-input-border-color);box-shadow:var(--form-input-shadow);color:var(--form-input-text-color);font-size:.875rem;padding:3px 7px 4px;transition:box-shadow .08s ease-in,color .08s ease-in;white-space:nowrap;word-break:keep-all}.field-collection-item.field-collection-item-complex.is-invalid,.field-collection-item.field-collection-item-complex.is-invalid:focus,.form-widget .form-select.is-invalid,.form-widget .form-select.is-invalid:focus,.form-widget input.form-control.is-invalid,.form-widget input.form-control.is-invalid:focus,.form-widget textarea.form-control.is-invalid,.form-widget textarea.form-control.is-invalid:focus{background-image:none;border:1px solid var(--form-input-error-border-color);box-shadow:var(--form-input-error-shadow)}.form-widget input.form-check-input.is-invalid{border:1px solid var(--form-input-error-border-color);box-shadow:var(--form-input-error-shadow)}.form-widget .form-control:disabled,.form-widget .form-control[readonly],.form-widget .form-select:disabled,.form-widget .form-select[readonly]{background-color:var(--form-control-disabled-bg);border-color:var(--form-input-border-color)!important;box-shadow:none!important;color:var(--form-control-disabled-color);cursor:not-allowed}body.ea-dark-scheme .form-widget .form-select{background-image:url("data:image/svg+xml,%3csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 16 16%27%3e%3cpath fill=%27none%27 stroke=%27%23adb5bd%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27 stroke-width=%272%27 d=%27m2 5 6 6 6-6%27/%3e%3c/svg%3e")}.form-widget .form-select[multiple]{background-image:none;block-size:auto;padding:0}.form-widget input.form-check-input{border:1px solid var(--form-type-check-input-border-color);box-shadow:var(--form-type-check-input-box-shadow)}.form-widget .custom-file-input:focus~.custom-file-label,.form-widget .form-select:focus,.form-widget input.form-check-input:focus,.form-widget input.form-control:focus,.form-widget textarea.form-control:focus{border-color:var(--form-input-hover-border-color);box-shadow:var(--form-input-hover-shadow);outline:0}.form-check-input:checked{background-color:var(--form-type-check-input-checked-bg)}.form-check-input:focus{box-shadow:var(--form-input-hover-shadow)}.form-widget .form-control+.input-group-append{block-size:30px;color:var(--gray-600)}.form-widget .form-control+.input-group-append i{color:var(--gray-600)}.form-widget input.form-control[data-ea-align=right]{text-align:right}.form-widget input.form-control.is-invalid[data-ea-align=right]{padding-inline-end:30px}.form-widget textarea.form-control{block-size:auto;line-height:1.6;white-space:pre-wrap}.form-widget .form-select{background-position:right 5px center;padding:3px 28px 4px 7px}.ts-dropdown.form-select{block-size:auto}.form-widget .form-check{margin:0;padding:0}label.form-check-label{margin:0;padding-inline-start:5px}.form-check .form-check-input{float:none;margin-block-start:2px;margin-inline-start:0}.form-check-inline+.form-check-inline{margin-inline-start:15px}.field-file .custom-file,.field-file .custom-file-input{block-size:30px}.field-file .custom-file label.custom-file-label{block-size:30px;margin:0;max-inline-size:350px;overflow:hidden;padding:3px 7px 5px;text-align:left}.field-file .custom-file label.custom-file-label:after{block-size:28px;color:var(--text-color);content:"\f07c";display:inline-block;font-family:Font Awesome\ 6 Free,sans-serif;font-size:17px;line-height:28px;padding:0 8px;vertical-align:middle}.field-date .form-widget,.field-datetime .form-widget,.field-time .form-widget{margin:0}.datetime-widget .input-group>.form-select,.datetime-widget select{-webkit-appearance:none;min-inline-size:max-content}.datetime-widget+.datetime-widget{margin-inline-start:10px}.datetime-widget select+select{margin-inline-start:4px}.datetime-widget-time select{margin:0 0 0 2px}.datetime-widget-time select:first-child{margin-inline-start:0}.datetime-widget-time select:last-child{margin-inline-end:0}.short .form-widget{flex:0 0 20%!important}.large .form-control,.long .form-control{max-inline-size:unset!important}.large .input.form-control{font-size:18px!important}.large textarea.form-control{block-size:500px;max-inline-size:unset!important}.code input.form-control,.code textarea.form-control{font-family:monospace!important}.field-group .large .form-control,.field-group .large textarea.form-control,.field-group .long .form-control{flex:0 0 100%!important;max-inline-size:unset!important}.field-group .large textarea.form-control{block-size:500px}.form-tabs-tablist .nav-tabs{background:transparent;border:0;box-shadow:0 2px 0 var(--form-tabs-border-color);margin:0 0 20px;padding-inline-start:0}.form-tabs-tablist .nav-tabs a,.form-tabs-tablist .nav-tabs a:hover{border:0;color:var(--text-color);font-size:var(--font-size-base);font-weight:500;margin:0;padding:4px 14px 8px}.form-tabs-tablist .nav-tabs .nav-item:first-child a,.form-tabs-tablist .nav-tabs .nav-item:first-child a:hover{padding-inline-start:0}.form-tabs-tablist .nav-tabs .tab-nav-item-icon{color:var(--text-muted);margin-inline-end:5px}.form-tabs-tablist .nav-tabs .nav-link:focus-visible{box-shadow:none;outline:0}.form-tabs-tablist .nav-tabs .nav-link.active{background:transparent;color:var(--link-color);position:relative}.form-tabs-tablist .nav-tabs .nav-link.active .tab-nav-item-icon{color:var(--link-color)}.form-tabs-tablist .nav-tabs .nav-link.active:before{background:var(--body-bg);block-size:2px;content:"";inline-size:100%;inset-block-end:-2px;inset-inline-start:0;position:absolute}.form-tabs-tablist .nav-tabs .nav-link.active:after{background:var(--link-color);block-size:2px;content:"";inline-size:calc(100% - var(--form-tabs-gutter-x)*2);inset-block-end:-2px;inset-inline-start:var(--form-tabs-gutter-x);position:absolute}.form-tabs-tablist .nav-tabs .nav-item:first-child .nav-link.active:after{inline-size:calc(100% - var(--form-tabs-gutter-x));inset-inline-start:0}.form-tabs-tablist .nav-tabs .nav-item .badge{line-height:1;margin-inline-start:4px;padding:3px 6px}.form-tabs-content .tab-help{color:var(--form-tabs-help-color);margin-block-end:15px;margin-block-start:-10px}.form-column .form-column-title{display:flex;flex-direction:column;margin-block-end:15px}.form-column .form-column-title .form-column-title-content{align-items:center;color:var(--form-column-header-color);display:flex;font-size:17px;font-weight:700;padding:0 0 2px}.form-column .form-column-title .form-column-icon{color:var(--form-column-icon-color);margin-inline-end:10px}.form-column .form-column-title .form-column-help{color:var(--form-column-help-color);flex:1;margin:0}.form-column .field-form_fieldset{margin-block-end:var(--bs-gutter-x)}.form-column .form-fieldset{border-radius:var(--border-radius);box-shadow:inset 0 0 0 1px var(--form-fieldset-border-color)}.form-column .form-fieldset-header{box-shadow:none;padding:calc(var(--bs-gutter-x) - 5px) var(--bs-gutter-x) calc(var(--bs-gutter-x)/2)}.form-column .form-fieldset-header .form-fieldset-title .form-fieldset-title-content{box-shadow:none;padding:0}.form-column .form-fieldset-header .form-fieldset-title .form-fieldset-help{margin-block-start:2px}.form-column .form-fieldset-body{padding:5px var(--bs-gutter-x) 0}.form-column .form-fieldset-body.without-header{padding:var(--bs-gutter-x) var(--bs-gutter-x) 0}.field-form_fieldset{margin-block-end:calc(var(--bs-gutter-x)*1.5)}.form-section-empty{padding:25px 10px}.form-fieldset-header{align-items:flex-start;display:flex;flex-wrap:nowrap;padding:0 0 15px;position:relative}.form-fieldset-header .form-fieldset-collapse-marker{color:var(--form-fieldset-collapse-marker-color);font-size:90%;margin:0 10px 0 2px;transform:rotate(90deg);transition:transform .2s ease-out}.form-fieldset-header .form-fieldset-title{flex:1}.form-fieldset-header .form-fieldset-title .form-fieldset-title-content{align-items:center;box-shadow:0 1px 0 var(--form-fieldset-header-border-color);color:var(--form-fieldset-header-color);display:flex;font-size:17px;font-weight:700;padding:0 0 5px}.form-fieldset-header .form-fieldset-title .form-fieldset-title-content.not-collapsible{cursor:default}.form-fieldset-header .form-fieldset-title .form-fieldset-title-content.collapsed .form-fieldset-collapse-marker{transform:rotate(0deg)}.form-fieldset-header .form-fieldset-title .form-fieldset-title-content .collapsible:after{block-size:100%;content:"";inline-size:100%;inset-block-start:0;inset-inline-start:0;position:absolute}.form-fieldset-header .form-fieldset-title .form-fieldset-icon{color:var(--form-fieldset-icon-color);margin-inline-end:10px}.form-fieldset-header .form-fieldset-title .form-fieldset-help{color:var(--form-fieldset-help-color);margin-block-start:6px}.form-fieldset-title-content .badge-danger{margin-inline-start:8px}.form-fieldset.has-fieldset-error{border:1px solid var(--form-input-error-border-color);border-radius:var(--border-radius);box-shadow:var(--form-input-error-shadow)}.form-fieldset-body{display:grid;grid-template-rows:1fr;overflow:clip;transition:grid-template-rows .2s ease-out}.form-fieldset-body.collapse:not(.show){display:grid;grid-template-rows:0fr}.form-fieldset-body.collapsing{block-size:auto!important;display:grid;overflow:clip}.form-fieldset-body>.row{min-block-size:0;overflow:clip}.form-fieldset-body.show:not(.collapsing){overflow-block:visible;overflow-inline:clip}.form-fieldset-body.show:not(.collapsing)>.row{overflow:visible}@media (prefers-reduced-motion:reduce){.form-column .form-fieldset-header,.form-fieldset-body,.form-fieldset-header .form-fieldset-collapse-marker{transition-duration:.01ms!important}}.form-actions{display:flex;justify-content:flex-end;padding:0}.form-actions .btn{margin-inline-start:10px}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .form-help,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:var(--form-help-error-color)}.has-error .CodeMirror,.has-error .btn.input-file-container,.has-error .ea-fileupload .input-group,.has-error .form-widget .form-select,.has-error .form-widget input.form-check-input,.has-error .form-widget input.form-control,.has-error .form-widget textarea.form-control,.has-error.ea-text-editor-wrapper,.has-error.form-group .ea-text-editor-wrapper{border-color:var(--form-input-error-border-color);box-shadow:var(--form-input-error-shadow)}.form-group.has-error label,.form-group.has-error legend{color:var(--form-input-error-legend-color)}.has-error .ea-fileupload .input-group{border-radius:var(--border-radius)}.global-invalid-feedback{background:var(--form-global-error-bg);border:var(--form-global-error-border);border-radius:var(--border-radius);color:var(--form-global-error-color);font-size:14px;margin:5px 0;padding:6px 12px}form .invalid-feedback{color:var(--color-danger);font-size:1em;font-weight:500;padding-block-start:6px}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:not([type=checkbox]):invalid~.form-check-label{color:inherit}form .invalid-feedback .badge-danger{font-size:.6875rem;margin-inline-end:2px;padding:3px 4px}form .invalid-feedback>.d-block+.d-block{margin-block-start:5px}.input-group-text{background-color:var(--form-input-group-text-bg);block-size:30px;border:1px solid var(--form-input-group-text-border-color);box-shadow:var(--form-input-box-shadow);color:var(--form-input-text-color);padding:3px 10px 5px}.input-group button,.input-group button:active,.input-group button:focus,.input-group button:hover{block-size:28px;margin-block-start:1px}.input-group-append{margin-inline-start:0}.input-group-prepend{margin-inline-end:0}.ea-fileupload .custom-file{block-size:30px}.ea-fileupload .input-group{flex-wrap:nowrap}.ea-fileupload .input-group .btn,.ea-fileupload .input-group .btn:hover{background:var(--form-input-group-text-bg);block-size:28px;border-radius:0;box-shadow:none!important;color:var(--text-color);font-size:17px;line-height:28px;margin:0;padding:0 8px;vertical-align:middle}.ea-fileupload .input-group .btn:first-child,.ea-fileupload .input-group .btn:hover:first-child{margin-inline-start:5px}.ea-fileupload .input-group .btn:hover:last-child,.ea-fileupload .input-group .btn:last-child{border-end-end-radius:var(--border-radius);border-start-end-radius:var(--border-radius)}.ea-fileupload .custom-file-input{block-size:calc(1.5em + .75rem + 2px);cursor:pointer;inline-size:100%;margin:0;opacity:0;overflow:hidden;position:relative;z-index:2}.ea-fileupload .custom-file-label{background:var(--form-control-bg);block-size:30px;border:1px solid var(--form-input-border-color);border-radius:var(--border-radius);box-shadow:var(--form-input-shadow);color:var(--form-input-text-color);inline-size:100%!important;inset-block-start:0;inset-inline-start:0;margin:0!important;overflow:hidden;padding:3px 40px 3px 7px!important;position:absolute;text-align:left!important;text-overflow:ellipsis;white-space:nowrap}.ea-fileupload .custom-file-label:after{display:none}.ea-fileupload .input-group-text{background:var(--form-input-group-text-bg);block-size:30px;border:1px solid var(--form-input-border-color);box-shadow:none;color:var(--text-muted);inset-inline-end:0;padding:7px 0 7px 7px;position:absolute;z-index:3}.ea-fileupload .fileupload-list{block-size:auto;border-color:var(--form-input-border-color);margin-block-start:7px;padding:0}.ea-fileupload .fileupload-list .fileupload-table{inline-size:100%}.ea-fileupload .fileupload-list .fileupload-table td{border-radius:3px;padding:3px 7px}.ea-fileupload .fileupload-list .fileupload-table td:first-child{max-inline-size:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ea-fileupload .fileupload-list .fileupload-table tr:nth-child(odd) td{background-color:var(--form-control-bg)}.ea-fileupload .fileupload-list .fileupload-table td.file-size{color:var(--text-muted)}.ea-vich-image img{box-shadow:0 0 0 4px var(--white),0 0 4px 3px var(--gray-600);margin:6px 4px 12px;max-block-size:300px;max-inline-size:100%}.ea-vich-file-name{display:block;margin:4px 0 8px}.ea-vich-file-name .fa{font-size:18px}.ea-vich-file-actions>div,.ea-vich-image-actions>div{float:left;margin-inline-end:4px}.ea-vich-file-actions:after,.ea-vich-image-actions:after{clear:left;content:"";display:block}.ea-vich-file-actions .field-checkbox,.ea-vich-image-actions .field-checkbox{padding-block-start:4px}.ea-vich-image-actions .form-widget{flex-basis:100%}.input-file-container{overflow:hidden;position:relative}.input-file-container [type=file]{cursor:inherit;display:block;filter:opacity(0);font-size:999px;inset-block-start:0;inset-inline-end:0;min-block-size:100%;min-inline-size:100%;opacity:0;position:absolute;text-align:right}.form-control::-webkit-file-upload-button,.form-control::file-selector-button{background-color:var(--button-secondary-bg);box-shadow:var(--button-shadow);color:var(--button-secondary-color)}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button,.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:var(--button-secondary-bg);box-shadow:var(--button-hover-shadow)}.btn{--button-bg:transparent;--button-border-color:transparent;--button-color:var(--text-color);--button-box-shadow:none;align-items:center;appearance:none;background:var(--button-bg);block-size:var(--button-height,2rem);border:var(--button-border-width,.0625rem) solid;border-color:var(--button-border-color);border-radius:var(--button-border-radius,.375rem);box-shadow:var(--button-box-shadow);color:var(--button-color);cursor:pointer;display:inline-flex;font-family:inherit;font-size:var(--button-font-size,.875rem);font-weight:var(--button-font-weight,500);gap:var(--button-icon-gap,.5rem);justify-content:space-between;line-height:var(--button-line-height);min-inline-size:max-content;padding:var(--button-padding-y,var(--button-padding-y-md)) var(--button-padding-x,var(--button-padding-x-md));position:relative;text-align:center;text-decoration:none;transition:var(--button-transition-duration) var(--button-transition-timing);transition-property:background-color,border-color,color,opacity,fill;user-select:none;white-space:nowrap}.btn:not(:disabled):not(.disabled):focus,.btn:not(:disabled):not(.disabled):focus-visible,.btn:not(:disabled):not(.disabled):hover{background:var(--button-hover-bg,var(--button-bg));border-color:var(--button-hover-border-color,var(--button-border-color));color:var(--button-hover-color,var(--button-color));text-decoration:none}.btn:not(:disabled):not(.disabled):active{background:var(--button-active-bg,var(--button-bg));border-color:var(--button-active-border-color,var(--button-border-color));box-shadow:var(--button-active-box-shadow,var(--button-box-shadow));color:var(--button-active-color,var(--button-color));outline:none}.btn:not(:disabled):not(.disabled):focus-visible:not(:active),.btn:not(:disabled):not(.disabled):focus:not(:active){box-shadow:none;outline:2px solid var(--button-focus-outline-color);outline-offset:-2px}.btn-primary,.btn-primary.btn.disabled,.btn-primary.btn:disabled{--button-box-shadow:var(--button-primary-box-shadow);--button-bg:var(--button-primary-bg);--button-color:var(--button-primary-color);--button-icon-color:var(--button-primary-icon-color);--button-border-color:var(--button-primary-border-color);--button-hover-bg:var(--button-primary-hover-bg);--button-hover-color:var(--button-primary-hover-color);--button-hover-border-color:var(--button-primary-hover-border-color);--button-active-box-shadow:var(--button-primary-active-box-shadow);--button-active-color:var(--button-primary-active-color);--button-active-bg:var(--button-primary-active-bg);--button-active-border-color:var(--button-primary-active-border-color)}.btn-secondary,.btn-secondary.btn.disabled,.btn-secondary.btn:disabled{--button-box-shadow:var(--button-secondary-box-shadow);--button-bg:var(--button-secondary-bg);--button-color:var(--button-secondary-color);--button-icon-color:var(--button-secondary-icon-color);--button-border-color:var(--button-secondary-border-color);--button-hover-bg:var(--button-secondary-hover-bg);--button-hover-color:var(--button-secondary-hover-color);--button-hover-border-color:var(--button-secondary-hover-border-color);--button-active-box-shadow:var(--button-secondary-active-box-shadow);--button-active-color:var(--button-secondary-active-color);--button-active-bg:var(--button-secondary-active-bg);--button-active-border-color:var(--button-secondary-active-border-color)}.btn-success,.btn-success.btn.disabled,.btn-success.btn:disabled{--button-box-shadow:var(--button-success-box-shadow);--button-bg:var(--button-success-bg);--button-color:var(--button-success-color);--button-icon-color:var(--button-success-icon-color);--button-border-color:var(--button-success-border-color);--button-hover-bg:var(--button-success-hover-bg);--button-hover-color:var(--button-success-hover-color);--button-hover-border-color:var(--button-success-hover-border-color);--button-active-box-shadow:var(--button-success-active-box-shadow);--button-active-color:var(--button-success-active-color);--button-active-bg:var(--button-success-active-bg);--button-active-border-color:var(--button-success-active-border-color)}.btn-warning,.btn-warning.btn.disabled,.btn-warning.btn:disabled{--button-box-shadow:var(--button-warning-box-shadow);--button-bg:var(--button-warning-bg);--button-color:var(--button-warning-color);--button-icon-color:var(--button-warning-icon-color);--button-border-color:var(--button-warning-border-color);--button-hover-bg:var(--button-warning-hover-bg);--button-hover-color:var(--button-warning-hover-color);--button-hover-border-color:var(--button-warning-hover-border-color);--button-active-box-shadow:var(--button-warning-active-box-shadow);--button-active-color:var(--button-warning-active-color);--button-active-bg:var(--button-warning-active-bg);--button-active-border-color:var(--button-warning-active-border-color)}.btn-danger,.btn-danger.btn.disabled,.btn-danger.btn:disabled{--button-box-shadow:var(--button-danger-box-shadow);--button-bg:var(--button-danger-bg);--button-color:var(--button-danger-color);--button-icon-color:var(--button-danger-icon-color);--button-border-color:var(--button-danger-border-color);--button-hover-bg:var(--button-danger-hover-bg);--button-hover-color:var(--button-danger-hover-color);--button-hover-border-color:var(--button-danger-hover-border-color);--button-active-box-shadow:var(--button-danger-active-box-shadow);--button-active-color:var(--button-danger-active-color);--button-active-bg:var(--button-danger-active-bg);--button-active-border-color:var(--button-danger-active-border-color)}.btn-invisible,.btn-invisible.btn.disabled,.btn-invisible.btn:disabled{--button-box-shadow:var(--button-invisible-box-shadow);--button-bg:var(--button-invisible-bg);--button-color:var(--button-invisible-color);--button-icon-color:var(--button-invisible-icon-color);--button-border-color:var(--button-invisible-border-color);--button-hover-bg:var(--button-invisible-hover-bg);--button-hover-color:var(--button-invisible-hover-color);--button-hover-border-color:var(--button-invisible-hover-border-color);--button-active-box-shadow:var(--button-invisible-active-box-shadow);--button-active-color:var(--button-invisible-active-color);--button-active-bg:var(--button-invisible-active-bg);--button-active-border-color:var(--button-invisible-active-border-color)}.btn-invisible:active,.btn-invisible:focus,.btn-invisible:focus-visible,.btn-invisible:hover{box-shadow:none}.btn-invisible.btn-danger,.btn-invisible.btn-danger.btn.disabled,.btn-invisible.btn-danger.btn:disabled{--button-color:var(--button-invisible-danger-color);--button-icon-color:var(--button-invisible-danger-hover-icon-color);--button-hover-color:var(--button-invisible-danger-hover-color);--button-hover-bg:var(--button-invisible-danger-hover-hover-bg);--button-active-color:var(--button-invisible-danger-active-color);--button-active-bg:var(--button-invisible-danger-hover-active-bg)}.btn-invisible.btn-danger:active,.btn-invisible.btn-danger:focus,.btn-invisible.btn-danger:focus-visible,.btn-invisible.btn-danger:hover{box-shadow:none}.btn-sm{--button-font-size:var(--button-font-size-sm);--button-padding-y:var(--button-padding-y-sm);--button-padding-x:var(--button-padding-x-sm);--button-icon-gap:.25rem;--button-height:1.75rem}.btn-lg{--button-font-size:var(--button-font-size-lg);--button-padding-y:var(--button-padding-y-lg);--button-padding-x:var(--button-padding-x-lg);block-size:2.5rem}.btn-block{display:block;place-content:center}.btn.disabled,.btn:disabled{background:var(--button-active-bg,var(--button-bg));border-color:var(--button-active-border-color,var(--button-border-color));box-shadow:none;color:var(--button-active-color,var(--button-color));cursor:not-allowed;opacity:var(--button-disabled-opacity);pointer-events:none}a.btn.disabled,fieldset:disabled a.btn{pointer-events:unset}.btn>.btn-label{align-items:center;display:inline-flex;margin:0}.btn>.btn-icon{color:var(--button-icon-color,currentColor);display:grid;flex-shrink:0;inline-size:1em;place-content:center}.btn .btn-icon svg{color:var(--button-icon-color,currentColor);fill:var(--button-icon-color,currentColor)}.btn>.btn-icon+.btn-label,.btn>.btn-label+.btn-icon,.btn>.btn-label+i,.btn>i+.btn-label{margin-inline-start:0}.btn>.btn-icon+.btn-label:empty{display:none}.btn-sm:not(:has(.btn-label)){padding:var(--button-padding-y-sm)}.btn-lg:not(:has(.btn-label)){padding:var(--button-padding-y-lg)}.btn-group>.btn-group:not(:first-child),.btn-group>:not(.btn-check:first-child)+.btn{margin-inline-start:0}.btn-group>.btn.dropdown-toggle.dropdown-toggle-split{margin-inline-start:-1px;padding-inline-end:.5625rem;padding-inline-start:.5625rem}.dropdown-menu .dropdown-submenu .dropdown-toggle.dropdown-toggle-split{border:0;inline-size:auto}.btn-block{display:flex;inline-size:100%}.btn .badge{margin-inline-start:var(--button-icon-gap)}.btn.is-loading{color:transparent;pointer-events:none;position:relative}.btn.is-loading:after{animation:button-spin .5s linear infinite;block-size:1em;border:2px solid;border-block-start-color:transparent;border-inline-end-color:transparent;border-radius:50%;content:"";inline-size:1em;inset-block-start:50%;inset-inline-start:50%;position:absolute;transform:translate(-50%,-50%)}@keyframes button-spin{to{transform:translate(-50%,-50%) rotate(1turn)}}.btn>i{font-size:inherit;line-height:inherit;vertical-align:middle}[dir=rtl] .btn>.btn-icon+.btn-label,[dir=rtl] .btn>.btn-label+.btn-icon,[dir=rtl] .btn>.btn-label+i,[dir=rtl] .btn>i+.btn-label{margin-inline-end:0;margin-inline-start:0}.btn:focus-visible{outline:2px solid var(--link-color);outline-offset:2px}.btn{overflow:hidden;text-overflow:ellipsis}.btn.text-wrap{overflow:visible;text-overflow:unset;white-space:normal}.badge+.badge{margin-inline-start:8px}.badge.badge-pill{border-radius:20px;font-size:var(--font-size-xs);line-height:16px;padding:1px 6px}.badge{box-shadow:var(--badge-box-shadow);line-height:16px}.badge.badge-success{background-color:var(--badge-success-bg);box-shadow:var(--badge-success-box-shadow);color:var(--badge-success-color)}.badge.badge-warning{background-color:var(--badge-warning-bg);box-shadow:var(--badge-warning-box-shadow);color:var(--badge-warning-color)}.badge.badge-danger{background-color:var(--badge-danger-bg);box-shadow:var(--badge-danger-box-shadow);color:var(--badge-danger-color)}.badge.badge-info{background-color:var(--badge-info-bg);box-shadow:var(--badge-info-box-shadow);color:var(--badge-info-color)}.badge.badge-primary{background-color:var(--badge-primary-bg);box-shadow:var(--badge-primary-box-shadow);color:var(--badge-primary-color)}.badge.badge-secondary{background-color:var(--badge-secondary-bg);box-shadow:var(--badge-secondary-box-shadow);color:var(--badge-secondary-color)}.badge.badge-light{background-color:var(--badge-light-bg);box-shadow:var(--badge-light-box-shadow);color:var(--badge-light-color)}.badge.badge-dark{background-color:var(--badge-dark-bg);box-shadow:var(--badge-dark-box-shadow);color:var(--badge-dark-color)}.badge.badge-outline{background-color:transparent;box-shadow:var(--badge-outline-box-shadow);color:var(--badge-outline-color)}.form-switch .form-check-input{-webkit-appearance:none;background-color:var(--form-switch-bg);background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%27-4 -4 8 8%27%3E%3Ccircle r=%273%27 fill=%27rgba%28148, 163, 184, 0.8%29%27/%3E%3C/svg%3E");block-size:18px;border-color:var(--form-switch-border-color);cursor:pointer;inline-size:32px}.ea-dark-scheme .form-switch .form-check-input:checked,.form-switch .form-check-input:checked{background-color:var(--form-switch-checked-bg);background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%27-4 -4 8 8%27%3E%3Ccircle r=%273%27 fill=%27rgb%28255, 255, 255%29%27/%3E%3C/svg%3E");border-color:var(--form-switch-checked-bg)}.ea-dark-scheme .form-switch .form-check-input:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%27-4 -4 8 8%27%3E%3Ccircle r=%273%27 fill=%27rgba%28255, 255, 255, 0.8%29%27/%3E%3C/svg%3E")}.ea-dark-scheme .form-switch .form-check-input{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%27-4 -4 8 8%27%3E%3Ccircle r=%273%27 fill=%27rgba%28163, 163, 163, 0.8%29%27/%3E%3C/svg%3E")}.form-switch .form-check-input[disabled],.form-switch.disabled{cursor:not-allowed}.form-switch .form-check-input:focus{box-shadow:none}:root{--ts-pr-clear-button:0;--ts-pr-caret:0;--ts-pr-min:.75rem}.ts-wrapper.single .ts-control,.ts-wrapper.single .ts-control input{cursor:pointer}.ts-control{padding-inline-end:max(var(--ts-pr-min),var(--ts-pr-clear-button) + var(--ts-pr-caret))!important}.ts-wrapper.plugin-drag_drop.multi>.ts-control>div.ui-sortable-placeholder{background:#f2f2f2!important;background:rgba(0,0,0,.06)!important;border:0!important;box-shadow:inset 0 0 12px 4px #fff;visibility:visible!important}.ts-wrapper.plugin-drag_drop .ui-sortable-placeholder:after{content:"!";visibility:hidden}.ts-wrapper.plugin-drag_drop .ui-sortable-helper{box-shadow:0 2px 5px rgba(0,0,0,.2)}.plugin-checkbox_options .option input{margin-inline-end:.5rem}.plugin-clear_button{--ts-pr-clear-button:1em}.plugin-clear_button .clear-button{background:transparent!important;cursor:pointer;inset-block-start:50%;inset-inline-end:calc(.75rem - 5px);margin-inline-end:0!important;opacity:0;position:absolute;transform:translateY(-50%);transition:opacity .5s}.plugin-clear_button.form-select .clear-button,.plugin-clear_button.single .clear-button{inset-inline-end:max(var(--ts-pr-caret),.75rem)}.plugin-clear_button.focus.has-items .clear-button,.plugin-clear_button:not(.disabled):hover.has-items .clear-button{opacity:1}.ts-wrapper .dropdown-header{background:#f8f8f8;border-block-end:1px solid #d0d0d0;border-radius:.375rem .375rem 0 0;padding:6px .75rem;position:relative}.ts-wrapper .dropdown-header-close{color:#343a40;font-size:20px!important;inset-block-start:50%;inset-inline-end:.75rem;line-height:20px;margin-block-start:-12px;opacity:.4;position:absolute}.ts-wrapper .dropdown-header-close:hover{color:#000}.plugin-dropdown_input.focus.dropdown-active .ts-control{border:1px solid #ced4da;box-shadow:none;box-shadow:inset 0 1px 2px rgba(0,0,0,.075)}.plugin-dropdown_input .dropdown-input{background:transparent;border:solid #d0d0d0;border-width:0 0 1px;box-shadow:none;display:block;inline-size:100%;padding:.375rem .75rem}.plugin-dropdown_input.focus .ts-dropdown .dropdown-input{border-color:#86b7fe;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);outline:0}.plugin-dropdown_input .items-placeholder{border:0!important;box-shadow:none!important;inline-size:100%}.plugin-dropdown_input.dropdown-active .items-placeholder,.plugin-dropdown_input.has-items .items-placeholder{display:none!important}.ts-wrapper.plugin-input_autogrow.has-items .ts-control>input{min-inline-size:0}.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control>input{flex:none;min-inline-size:4px}.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control>input::-ms-input-placeholder{color:transparent}.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control>input::placeholder{color:transparent}.ts-dropdown.plugin-optgroup_columns .ts-dropdown-content{display:flex}.ts-dropdown.plugin-optgroup_columns .optgroup{border-block-start:0;border-inline-end:1px solid #f2f2f2;flex-basis:0;flex-grow:1;min-inline-size:0}.ts-dropdown.plugin-optgroup_columns .optgroup:last-child{border-inline-end:0}.ts-dropdown.plugin-optgroup_columns .optgroup:before{display:none}.ts-dropdown.plugin-optgroup_columns .optgroup-header{border-block-start:0}.ts-wrapper.plugin-remove_button .item{align-items:center;display:inline-flex;padding-inline-end:0!important}.ts-wrapper.plugin-remove_button .item .remove{border-radius:0 2px 2px 0;box-sizing:border-box;color:inherit;display:inline-block;padding:0 5px;text-decoration:none;vertical-align:middle}.ts-wrapper.plugin-remove_button .item .remove:hover{background:rgba(0,0,0,.05)}.ts-wrapper.plugin-remove_button.disabled .item .remove:hover{background:none}.ts-wrapper.plugin-remove_button .remove-single{font-size:23px;inset-block-start:0;inset-inline-end:0;position:absolute}.ts-wrapper.plugin-remove_button:not(.rtl) .item .remove{border-inline-start:1px solid #dee2e6;margin-inline-start:5px}.ts-wrapper.plugin-remove_button:not(.rtl) .item.active .remove{border-inline-start-color:transparent}.ts-wrapper.plugin-remove_button:not(.rtl).disabled .item .remove{border-inline-start-color:#fff}.ts-wrapper.plugin-remove_button.rtl .item .remove{border-inline-end:1px solid #dee2e6;margin-inline-end:5px}.ts-wrapper.plugin-remove_button.rtl .item.active .remove{border-inline-end-color:transparent}.ts-wrapper.plugin-remove_button.rtl.disabled .item .remove{border-inline-end-color:#fff}.ts-wrapper{position:relative}.ts-control,.ts-control input,.ts-dropdown{-webkit-font-smoothing:inherit;color:#343a40;font-family:inherit;font-size:inherit;line-height:1.5}.ts-control,.ts-wrapper.single.input-active .ts-control{background:#fff;cursor:text}.ts-control{border:1px solid #ced4da;border-radius:.375rem;box-shadow:none;box-sizing:border-box;flex-wrap:wrap;inline-size:100%;overflow:hidden;padding:.375rem .75rem;position:relative;z-index:1}.ts-wrapper.multi.has-items .ts-control{padding:calc(.375rem - 1px) .75rem calc(.375rem - 4px)}.full .ts-control{background-color:#fff}.disabled .ts-control,.disabled .ts-control *{cursor:default!important}.focus .ts-control{box-shadow:none}.ts-control>*{display:inline-block;vertical-align:baseline}.ts-wrapper.multi .ts-control>div{background:#efefef;border:0 solid #dee2e6;color:#343a40;cursor:pointer;margin:0 3px 3px 0;padding:1px 5px}.ts-wrapper.multi .ts-control>div.active{background:#0d6efd;border:0 solid transparent;color:#fff}.ts-wrapper.multi.disabled .ts-control>div,.ts-wrapper.multi.disabled .ts-control>div.active{background:#fff;border:0 solid #fff;color:#878787}.ts-control>input{background:none!important;border:0!important;box-shadow:none!important;display:inline-block!important;flex:1 1 auto;line-height:inherit!important;margin:0!important;max-block-size:none!important;max-inline-size:100%!important;min-block-size:0!important;min-inline-size:7rem;padding:0!important;text-indent:0!important;-webkit-user-select:auto!important;-moz-user-select:auto!important;-ms-user-select:auto!important;user-select:auto!important}.ts-control>input::-ms-clear{display:none}.ts-control>input:focus{outline:none!important}.has-items .ts-control>input{margin:0 4px!important}.ts-control.rtl{text-align:right}.ts-control.rtl.single .ts-control:after{inset-inline-end:auto;inset-inline-start:calc(.75rem + 5px)}.ts-control.rtl .ts-control>input{margin:0 4px 0 -2px!important}.disabled .ts-control{background-color:#e9ecef;opacity:.5}.input-hidden .ts-control>input{inset-inline-start:-10000px;opacity:0;position:absolute}.ts-dropdown{background:#fff;border:1px solid #d0d0d0;border-block-start:0;border-radius:0 0 .375rem .375rem;box-shadow:0 1px 3px rgba(0,0,0,.1);box-sizing:border-box;inline-size:100%;inset-block-start:100%;inset-inline-start:0;margin:.25rem 0 0;position:absolute;z-index:10}.ts-dropdown [data-selectable]{cursor:pointer;overflow:hidden}.ts-dropdown [data-selectable] .highlight{background:rgba(255,237,40,.4);border-radius:1px}.ts-dropdown .create,.ts-dropdown .no-results,.ts-dropdown .optgroup-header,.ts-dropdown .option{padding:3px .75rem}.ts-dropdown .option,.ts-dropdown [data-disabled],.ts-dropdown [data-disabled] [data-selectable].option{cursor:inherit;opacity:.5}.ts-dropdown [data-selectable].option{cursor:pointer;opacity:1}.ts-dropdown .optgroup:first-child .optgroup-header{border-block-start:0}.ts-dropdown .optgroup-header{background:#fff;color:#6c757d;cursor:default}.ts-dropdown .active{background-color:#e9ecef;color:#1e2125}.ts-dropdown .active.create{color:#1e2125}.ts-dropdown .create{color:rgba(52,58,64,.5)}.ts-dropdown .spinner{block-size:30px;display:inline-block;inline-size:30px;margin:3px .75rem}.ts-dropdown .spinner:after{animation:lds-dual-ring 1.2s linear infinite;block-size:24px;border-color:#d0d0d0 transparent;border-radius:50%;border-style:solid;border-width:5px;content:" ";display:block;inline-size:24px;margin:3px}@keyframes lds-dual-ring{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.ts-dropdown-content{-webkit-overflow-scrolling:touch;max-block-size:200px;overflow-block:auto;overflow-inline:hidden;scroll-behavior:smooth}.ts-hidden-accessible{clip:rect(0 0 0 0)!important;border:0!important;-webkit-clip-path:inset(50%)!important;clip-path:inset(50%)!important;inline-size:1px!important;overflow:hidden!important;padding:0!important;position:absolute!important;white-space:nowrap!important}.ts-wrapper.form-control,.ts-wrapper.form-select{block-size:auto;box-shadow:none;display:flex;padding:0!important}.ts-dropdown,.ts-dropdown.form-control,.ts-dropdown.form-select{background:#fff;block-size:auto;border:1px solid var(--bs-border-color-translucent);border-radius:.375rem;box-shadow:0 6px 12px rgba(0,0,0,.175);padding:0;z-index:1000}.ts-dropdown .optgroup-header{font-size:.875rem;line-height:1.5}.ts-dropdown .optgroup:first-child:before{display:none}.ts-dropdown .optgroup:before{block-size:0;border-block-start:1px solid var(--bs-border-color-translucent);content:" ";display:block;margin:.5rem -.75rem;overflow:hidden}.ts-dropdown .create{padding-inline-start:.75rem}.ts-dropdown-content{padding:5px 0}.ts-control{align-items:center;display:flex;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.ts-control{transition:none}}.ts-control.dropdown -active{border-radius:.375rem}.focus .ts-control{border-color:#86b7fe;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);outline:0}.ts-control .item{align-items:center;display:flex}.ts-wrapper.is-invalid,.was-validated .invalid,.was-validated :invalid+.ts-wrapper{border-color:#dc3545}.ts-wrapper.is-invalid:not(.single),.was-validated .invalid:not(.single),.was-validated :invalid+.ts-wrapper:not(.single){background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 inline-size=%2712%27 block-size=%2712%27 fill=%27none%27 stroke=%27%23dc3545%27%3E%3Ccircle cx=%276%27 cy=%276%27 r=%274.5%27/%3E%3Cpath stroke-linejoin=%27round%27 d=%27M5.8 3.6h.4L6 6.5z%27/%3E%3Ccircle cx=%276%27 cy=%278.2%27 r=%27.6%27 fill=%27%23dc3545%27 stroke=%27none%27/%3E%3C/svg%3E");background-position:right calc(.375em + .1875rem) center;background-repeat:no-repeat;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.ts-wrapper.is-invalid.single,.was-validated .invalid.single,.was-validated :invalid+.ts-wrapper.single{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 16 16%27%3E%3Cpath fill=%27none%27 stroke=%27%23343a40%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27 stroke-width=%272%27 d=%27m2 5 6 6 6-6%27/%3E%3C/svg%3E"),url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 inline-size=%2712%27 block-size=%2712%27 fill=%27none%27 stroke=%27%23dc3545%27%3E%3Ccircle cx=%276%27 cy=%276%27 r=%274.5%27/%3E%3Cpath stroke-linejoin=%27round%27 d=%27M5.8 3.6h.4L6 6.5z%27/%3E%3Ccircle cx=%276%27 cy=%278.2%27 r=%27.6%27 fill=%27%23dc3545%27 stroke=%27none%27/%3E%3C/svg%3E");background-position:right .75rem center,center right 2.25rem;background-repeat:no-repeat;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.ts-wrapper.is-invalid.focus .ts-control,.was-validated .invalid.focus .ts-control,.was-validated :invalid+.ts-wrapper.focus .ts-control{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.ts-wrapper.is-valid,.was-validated .valid,.was-validated :valid+.ts-wrapper{border-color:#198754}.ts-wrapper.is-valid:not(.single),.was-validated .valid:not(.single),.was-validated :valid+.ts-wrapper:not(.single){background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 8 8%27%3E%3Cpath fill=%27%23198754%27 d=%27M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z%27/%3E%3C/svg%3E");background-position:right calc(.375em + .1875rem) center;background-repeat:no-repeat;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.ts-wrapper.is-valid.single,.was-validated .valid.single,.was-validated :valid+.ts-wrapper.single{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 16 16%27%3E%3Cpath fill=%27none%27 stroke=%27%23343a40%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27 stroke-width=%272%27 d=%27m2 5 6 6 6-6%27/%3E%3C/svg%3E"),url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 8 8%27%3E%3Cpath fill=%27%23198754%27 d=%27M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z%27/%3E%3C/svg%3E");background-position:right .75rem center,center right 2.25rem;background-repeat:no-repeat;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.ts-wrapper.is-valid.focus .ts-control,.was-validated .valid.focus .ts-control,.was-validated :valid+.ts-wrapper.focus .ts-control{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.ts-wrapper{display:flex;min-block-size:calc(1.5em + .75rem + 2px)}.input-group-sm>.ts-wrapper,.ts-wrapper.form-control-sm,.ts-wrapper.form-select-sm{min-block-size:calc(1.5em + .5rem + 2px)}.input-group-sm>.ts-wrapper .ts-control,.ts-wrapper.form-control-sm .ts-control,.ts-wrapper.form-select-sm .ts-control{border-radius:.25rem;font-size:.875rem}.input-group-sm>.ts-wrapper.has-items .ts-control,.ts-wrapper.form-control-sm.has-items .ts-control,.ts-wrapper.form-select-sm.has-items .ts-control{font-size:.875rem;padding-block-end:0}.input-group-sm>.ts-wrapper.multi.has-items .ts-control,.ts-wrapper.form-control-sm.multi.has-items .ts-control,.ts-wrapper.form-select-sm.multi.has-items .ts-control{padding-block-start:calc(.75em - .40625rem - 1px)!important}.ts-wrapper.multi.has-items .ts-control{--ts-pr-min:calc(0.75rem - 5px);padding-inline-start:calc(.75rem - 5px)}.ts-wrapper.multi .ts-control>div{border-radius:calc(.375rem - 1px)}.input-group-lg>.ts-wrapper,.ts-wrapper.form-control-lg,.ts-wrapper.form-select-lg{min-block-size:calc(1.5em + 1rem + 2px)}.input-group-lg>.ts-wrapper .ts-control,.ts-wrapper.form-control-lg .ts-control,.ts-wrapper.form-select-lg .ts-control{border-radius:.5rem;font-size:1.25rem}.ts-wrapper:not(.form-control):not(.form-select){background:none;block-size:auto;border:none;box-shadow:none;padding:0}.ts-wrapper:not(.form-control):not(.form-select).single .ts-control{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 16 16%27%3E%3Cpath fill=%27none%27 stroke=%27%23343a40%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27 stroke-width=%272%27 d=%27m2 5 6 6 6-6%27/%3E%3C/svg%3E");background-position:right .75rem center;background-repeat:no-repeat;background-size:16px 12px}.ts-wrapper.form-select,.ts-wrapper.single{--ts-pr-caret:2.25rem}.ts-wrapper.form-control .ts-control,.ts-wrapper.form-control.single.input-active .ts-control,.ts-wrapper.form-select .ts-control,.ts-wrapper.form-select.single.input-active .ts-control{border:none!important}.ts-wrapper.form-control:not(.disabled) .ts-control,.ts-wrapper.form-control:not(.disabled).single.input-active .ts-control,.ts-wrapper.form-select:not(.disabled) .ts-control,.ts-wrapper.form-select:not(.disabled).single.input-active .ts-control{background:transparent!important}.input-group>.ts-wrapper{flex-grow:1}.input-group>.ts-wrapper:not(:nth-child(2))>.ts-control{border-end-start-radius:0;border-start-start-radius:0}.input-group>.ts-wrapper:not(:last-child)>.ts-control{border-end-end-radius:0;border-start-end-radius:0}.ts-wrapper{min-block-size:unset}.ts-wrapper .ts-control{block-size:unset;min-block-size:30px;padding:3px 28px 4px 7px}.ts-wrapper.input-active{border-color:var(--form-input-hover-border-color);box-shadow:var(--form-input-hover-shadow);outline:0}.ts-wrapper.focus .ts-control{box-shadow:none;outline:0}.dropdown-input-wrap{background:var(--form-type-autocomplete-dropdown-input-wrapper-bg);border-block-end:1px solid var(--form-input-border-color);border-start-end-radius:var(--border-radius);border-start-start-radius:var(--border-radius);padding:7px 10px}.dropdown-input,.plugin-dropdown_input.focus .dropdown-input{background:var(--form-control-bg);block-size:30px;border:1px solid var(--form-type-autocomplete-dropdown-input-border-color);border-radius:var(--border-radius);box-shadow:var(--form-input-box-shadow);color:var(--form-input-text-color);position:relative}.dropdown-input:focus{border:0;box-shadow:0 0 0 1px rgba(43,45,80,0),0 0 0 1px rgba(6,122,184,.2),0 0 0 2px rgba(6,122,184,.25),0 1px 1px rgba(0,0,0,.08);outline:0}.ts-dropdown,.ts-dropdown.form-control,.ts-dropdown.form-select{background:var(--form-type-autocomplete-dropdown-bg);border:1px solid var(--form-input-border-color);box-shadow:var(--shadow-xl);color:var(--form-input-text-color)}.ts-dropdown .active,.ts-dropdown .create:hover,.ts-dropdown .option:hover{background-color:var(--form-type-autocomplete-dropdown-active-item-bg);color:var(--form-input-text-color)}.ts-dropdown [data-selectable] .highlight{background:var(--highlight-bg);color:var(--highlight-color)}.ts-control,.ts-control input,.ts-dropdown{color:var(--form-input-text-color)}.ts-dropdown-content{padding:4px 5px}.ts-dropdown [data-selectable].option{border-radius:var(--border-radius);margin:2px 0}.ts-dropdown .optgroup-header{background:var(--form-type-autocomplete-optgroup-bg);color:var(--form-type-autocomplete-optgroup-color);font-size:13px;font-weight:700}.ts-wrapper.multi,.ts-wrapper.multi.has-items .ts-control{block-size:auto}.ts-wrapper.multi .ts-control,.ts-wrapper.multi.has-items .ts-control{padding:2px 15px 3px 7px}.ts-wrapper.plugin-remove_button.multi.has-items .ts-control{padding-inline-end:55px}.ts-wrapper.multi .ts-control>div{background:var(--form-type-autocomplete-multi-item-bg);border-radius:var(--border-radius);box-shadow:0 0 0 1px var(--form-type-autocomplete-multi-item-border-color);color:var(--form-input-text-color);margin:2px 5px 2px 0;padding:0 4px}.ts-wrapper.plugin-remove_button .item .remove,.ts-wrapper.plugin-remove_button:not(.rtl) .item .remove{border-inline-start:1px solid var(--form-type-autocomplete-multi-item-border-color);border-radius:0 var(--border-radius) var(--border-radius) 0}.ts-wrapper.plugin-remove_button .item .remove:hover{background:var(--form-type-autocomplete-multi-item-remove-button-hover-bg)}.plugin-clear_button.ts-wrapper .clear-button,.plugin-clear_button.ts-wrapper.multi .clear-button{align-content:center;background:var(--form-type-autocomplete-close-button-bg)!important;block-size:14px;border-radius:50%;color:#fff;cursor:pointer;display:flex;font-size:16px;font-weight:700;inline-size:14px;inset-block-start:calc(50% - 2px);inset-inline-end:32px;justify-content:center;line-height:.65;padding:0}.ts-wrapper.plugin-clear_button.multi .clear-button{inset-inline-end:10px}.ts-wrapper.plugin-remove_button.plugin-clear_button.multi.has-items .clear-button{inset-inline-end:32px}.plugin-clear_button.ts-wrapper .clear-button:hover,.plugin-clear_button.ts-wrapper.multi .clear-button:hover{background:var(--form-type-autocomplete-close-button-hover-bg)!important}.ts-wrapper.disabled .ts-control{background-color:var(--form-control-disabled-bg)}.ts-dropdown .optgroup-header:empty{display:none}body.error .error-message{max-inline-size:500px;min-block-size:400px;padding:45px 15px}@media (min-width:992px){body.error .error-message{padding:45px}}body.error .error-message h1{align-items:center;color:var(--color-danger);display:flex;font-size:var(--font-size-lg);font-weight:600;margin-block-end:1em}body.error .error-message h1 .icon{font-size:110%;line-height:1;margin-inline-end:6px}body,html{block-size:100%;margin:0}body.page-login{background:var(--body-bg)}@media (min-width:576px){body.page-login{background:var(--page-login-bg);display:grid;min-block-size:100vh;place-items:center}}body.page-login #flash-messages{inline-size:100%;inset-block-start:0;inset-inline-start:0;position:absolute}@media (min-width:576px){.login-wrapper{inline-size:25rem;margin:0 auto}}.login-wrapper .main-header{display:block;padding-inline-end:0}.login-wrapper .main-header #header-logo{border-block-end:var(--border-width) var(--border-style) var(--border-secondary-color);margin:1.5rem 0 1rem;padding-block-end:1rem}@media (min-width:576px){.login-wrapper .main-header #header-logo{border-block-end:none}}.login-wrapper .main-header #header-logo a{font-size:var(--font-size-lg);margin:0;padding:0;text-align:center}@media (min-width:576px){.login-wrapper .main-header #header-logo a{font-size:var(--font-size-xl)}}.login-wrapper .content{background-color:var(--body-bg);inline-size:100%;padding:15px 30px}@media (min-width:576px){.login-wrapper .content{background:var(--page-login-form-bg);border-radius:var(--border-radius-lg);box-shadow:var(--shadow-lg);padding:2.5rem 3rem}}.login-wrapper .form-widget input{background-color:var(--page-login-form-control-bg);block-size:38px;border-color:var(--page-login-form-control-border-color);font-size:var(--font-size-lg);line-height:38px}.login-wrapper .form-group label.required:after{display:none}.login-wrapper .btn-primary{background-color:var(--page-login-form-control-button-bg);font-size:var(--font-size-base);margin-block-start:1rem}.login-wrapper .form-text{font-size:inherit;margin-block-start:5px} \ No newline at end of file + */@font-face{font-display:block;font-family:Font Awesome\ 5 Brands;font-weight:400;src:url(fonts/fa-brands-400.fdbb5585.woff2) format("woff2"),url(fonts/fa-brands-400.26b80c88.ttf) format("truetype")}@font-face{font-display:block;font-family:Font Awesome\ 5 Free;font-weight:900;src:url(fonts/fa-solid-900.83a538a0.woff2) format("woff2"),url(fonts/fa-solid-900.ad1782c7.ttf) format("truetype")}@font-face{font-display:block;font-family:Font Awesome\ 5 Free;font-weight:400;src:url(fonts/fa-regular-400.4f6a2dab.woff2) format("woff2"),url(fonts/fa-regular-400.05fdd87b.ttf) format("truetype")}:root{--black:#000;--white:#fff;--rose-50:#fff1f2;--rose-100:#ffe4e6;--rose-200:#fecdd3;--rose-300:#fda4af;--rose-400:#fb7185;--rose-500:#f43f5e;--rose-600:#e11d48;--rose-700:#be123c;--rose-800:#9f1239;--rose-900:#881337;--pink-50:#fdf2f8;--pink-100:#fce7f3;--pink-200:#fbcfe8;--pink-300:#f9a8d4;--pink-400:#f472b6;--pink-500:#ec4899;--pink-600:#db2777;--pink-700:#be185d;--pink-800:#9d174d;--pink-900:#831843;--fuchsia-50:#fdf4ff;--fuchsia-100:#fae8ff;--fuchsia-200:#f5d0fe;--fuchsia-300:#f0abfc;--fuchsia-400:#e879f9;--fuchsia-500:#d946ef;--fuchsia-600:#c026d3;--fuchsia-700:#a21caf;--fuchsia-800:#86198f;--fuchsia-900:#701a75;--purple-50:#faf5ff;--purple-100:#f3e8ff;--purple-200:#e9d5ff;--purple-300:#d8b4fe;--purple-400:#c084fc;--purple-500:#a855f7;--purple-600:#9333ea;--purple-700:#7e22ce;--purple-800:#6b21a8;--purple-900:#581c87;--violet-50:#f5f3ff;--violet-100:#ede9fe;--violet-200:#ddd6fe;--violet-300:#c4b5fd;--violet-400:#a78bfa;--violet-500:#8b5cf6;--violet-600:#7c3aed;--violet-700:#6d28d9;--violet-800:#5b21b6;--violet-900:#4c1d95;--indigo-50:#eef2ff;--indigo-100:#e0e7ff;--indigo-200:#c7d2fe;--indigo-300:#a5b4fc;--indigo-400:#818cf8;--indigo-500:#6366f1;--indigo-600:#4f46e5;--indigo-700:#4338ca;--indigo-800:#3730a3;--indigo-900:#312e81;--blue-50:#eff6ff;--blue-100:#dbeafe;--blue-200:#bfdbfe;--blue-300:#93c5fd;--blue-400:#60a5fa;--blue-500:#3b82f6;--blue-600:#2563eb;--blue-700:#1d4ed8;--blue-800:#1e40af;--blue-900:#1e3a8a;--sky-50:#f0f9ff;--sky-100:#e0f2fe;--sky-200:#bae6fd;--sky-300:#7dd3fc;--sky-400:#38bdf8;--sky-500:#0ea5e9;--sky-600:#0284c7;--sky-700:#0369a1;--sky-800:#075985;--sky-900:#0c4a6e;--cyan-50:#ecfeff;--cyan-100:#cffafe;--cyan-200:#a5f3fc;--cyan-300:#67e8f9;--cyan-400:#22d3ee;--cyan-500:#06b6d4;--cyan-600:#0891b2;--cyan-700:#0e7490;--cyan-800:#155e75;--cyan-900:#164e63;--teal-50:#f0fdfa;--teal-100:#ccfbf1;--teal-200:#99f6e4;--teal-300:#5eead4;--teal-400:#2dd4bf;--teal-500:#14b8a6;--teal-600:#0d9488;--teal-700:#0f766e;--teal-800:#115e59;--teal-900:#134e4a;--emerald-50:#ecfdf5;--emerald-100:#d1fae5;--emerald-200:#a7f3d0;--emerald-300:#6ee7b7;--emerald-400:#34d399;--emerald-500:#10b981;--emerald-600:#059669;--emerald-700:#047857;--emerald-800:#065f46;--emerald-900:#064e3b;--green-50:#f0fdf4;--green-100:#dcfce7;--green-200:#bbf7d0;--green-300:#86efac;--green-400:#4ade80;--green-500:#22c55e;--green-600:#16a34a;--green-700:#15803d;--green-800:#166534;--green-900:#14532d;--lime-50:#f7fee7;--lime-100:#ecfccb;--lime-200:#d9f99d;--lime-300:#bef264;--lime-400:#a3e635;--lime-500:#84cc16;--lime-600:#65a30d;--lime-700:#4d7c0f;--lime-800:#3f6212;--lime-900:#365314;--yellow-50:#fefce8;--yellow-100:#fef9c3;--yellow-200:#fef08a;--yellow-300:#fde047;--yellow-400:#facc15;--yellow-500:#eab308;--yellow-600:#ca8a04;--yellow-700:#a16207;--yellow-800:#854d0e;--yellow-900:#713f12;--amber-50:#fffbeb;--amber-100:#fef3c7;--amber-200:#fde68a;--amber-300:#fcd34d;--amber-400:#fbbf24;--amber-500:#f59e0b;--amber-600:#d97706;--amber-700:#b45309;--amber-800:#92400e;--amber-900:#78350f;--orange-50:#fff7ed;--orange-100:#ffedd5;--orange-200:#fed7aa;--orange-300:#fdba74;--orange-400:#fb923c;--orange-500:#f97316;--orange-600:#ea580c;--orange-700:#c2410c;--orange-800:#9a3412;--orange-900:#7c2d12;--red-50:#fef2f2;--red-100:#fee2e2;--red-200:#fecaca;--red-300:#fca5a5;--red-400:#f87171;--red-500:#ef4444;--red-600:#dc2626;--red-700:#b91c1c;--red-800:#991b1b;--red-900:#7f1d1d;--warm-gray-50:#fafaf9;--warm-gray-100:#f5f5f4;--warm-gray-200:#e7e5e4;--warm-gray-300:#d6d3d1;--warm-gray-400:#a8a29e;--warm-gray-500:#78716c;--warm-gray-600:#57534e;--warm-gray-700:#44403c;--warm-gray-800:#292524;--warm-gray-900:#1c1917;--warm-gray-950:#0c0a09;--true-gray-50:#fafafa;--true-gray-100:#f5f5f5;--true-gray-200:#e5e5e5;--true-gray-300:#d4d4d4;--true-gray-400:#a3a3a3;--true-gray-500:#737373;--true-gray-600:#525252;--true-gray-700:#404040;--true-gray-800:#262626;--true-gray-900:#171717;--true-gray-950:#0a0a0a;--neutral-gray-50:#fafafa;--neutral-gray-100:#f4f4f5;--neutral-gray-200:#e4e4e7;--neutral-gray-300:#d4d4d8;--neutral-gray-400:#a1a1aa;--neutral-gray-500:#71717a;--neutral-gray-600:#52525b;--neutral-gray-700:#3f3f46;--neutral-gray-800:#27272a;--neutral-gray-900:#18181b;--neutral-gray-950:#09090b;--cool-gray-50:#f9fafb;--cool-gray-100:#f3f4f6;--cool-gray-200:#e5e7eb;--cool-gray-300:#d1d5db;--cool-gray-400:#9ca3af;--cool-gray-500:#6b7280;--cool-gray-600:#4b5563;--cool-gray-700:#374151;--cool-gray-800:#1f2937;--cool-gray-900:#111827;--cool-gray-950:#030712;--blue-gray-50:#f8fafc;--blue-gray-100:#f1f5f9;--blue-gray-200:#e2e8f0;--blue-gray-300:#cbd5e1;--blue-gray-400:#94a3b8;--blue-gray-500:#64748b;--blue-gray-600:#475569;--blue-gray-700:#334155;--blue-gray-800:#1e293b;--blue-gray-900:#0f172a;--blue-gray-950:#020617;--gray-50:var(--blue-gray-50);--gray-100:var(--blue-gray-100);--gray-200:var(--blue-gray-200);--gray-300:var(--blue-gray-300);--gray-400:var(--blue-gray-400);--gray-500:var(--blue-gray-500);--gray-600:var(--blue-gray-600);--gray-700:var(--blue-gray-700);--gray-800:var(--blue-gray-800);--gray-900:var(--blue-gray-900);--font-family-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:"JetBrains Mono",ui-monospace,"Roboto Mono",SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--font-family-base:var(--font-family-sans-serif);--font-size-xs:12px;--font-size-sm:13px;--font-size-base:14px;--font-size-lg:16px;--font-size-xl:18px;--font-size-xxl:24px;--font-size-xxxl:28px;--shadow-md:0 4px 6px -1px rgba(15,23,43,.1),0 2px 4px -2px rgba(15,23,42,.1);--shadow-lg:0 10px 15px -3px rgba(15,23,43,.1),0 4px 6px -4px rgba(15,23,42,.1);--shadow-xl:0 20px 25px -5px rgba(15,23,42,.2),0 8px 10px -6px rgba(15,23,42,.2);--width-sm:576px;--width-md:768px;--width-lg:992px;--width-xl:1200px;--width-xxl:1400px;--zindex-modal-backdrop:2020;--form-tabs-gutter-x:5px;--text-primary-color:var(--text-color);--text-secondary-color:var(--text-muted);--text-tertiary-color:var(--gray-400);--border-primary-color:var(--gray-500);--border-secondary-color:var(--gray-300);--border-tertiary-color:var(--gray-100);--primary-bg:var(--gray-300);--secondary-bg:var(--gray-100);--tertiary-bg:var(--gray-50);--body-max-width:1440px;--body-bg:var(--white);--responsive-header-bg:var(--gray-50);--responsive-header-border-color:var(--gray-200);--responsive-header-logo-color:var(--gray-800);--responsive-table-label-color:var(--gray-500);--responsive-table-row-border-color:var(--gray-300);--sidebar-max-width:230px;--sidebar-bg:var(--gray-50);--sidebar-border-color:var(--gray-200);--sidebar-logo-color:var(--gray-800);--sidebar-padding-left:10px;--sidebar-padding-right:10px;--sidebar-menu-items-padding-left:6px;--sidebar-menu-items-padding-right:10px;--sidebar-menu-color:var(--gray-700);--sidebar-menu-badge-bg:var(--indigo-100);--sidebar-menu-badge-color:var(--gray-500);--sidebar-menu-badge-active-bg:var(--color-primary);--sidebar-menu-badge-active-color:var(--indigo-50);--sidebar-menu-submenu-color:var(--gray-600);--sidebar-menu-header-color:var(--gray-400);--sidebar-menu-icon-color:var(--gray-500);--sidebar-menu-active-item-bg:var(--gray-200);--sidebar-menu-active-item-color:var(--color-primary);--sidebar-menu-compact-hover-box-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--resize-handler-width:10px;--content-section-border-color:var(--gray-200);--resize-handler-hover-bg:var(--indigo-600);--content-search-input-bg:var(--body-bg);--content-search-icon-color:var(--gray-500);--content-search-reset-button-bg:var(--gray-300);--content-search-reset-button-color:var(--gray-600);--content-search-reset-button-hover-bg:var(--gray-600);--content-search-reset-button-hover-color:var(--gray-200);--content-top-border-color:var(--gray-200);--content-bg:var(--white);--content-padding-left:15px;--content-padding-right:15px;--lg-content-padding-left:35px;--lg-content-padding-right:25px;--lg-content-padding-bottom:45px;--user-avatar-icon-bg:var(--gray-200);--user-avatar-icon-color:var(--gray-500);--user-name-color:var(--gray-500);--user-menu-impersonated-link-color:var(--color-primary);--popover-bg:var(--gray-100);--popover-border-color:var(--gray-300);--popover-color:var(--text-color);--popover-shadow:var(--shadow-xl);--popover-max-width:480px;--dropdown-toggle-bg:var(--white);--dropdown-toggle-color:var(--gray-600);--dropdown-toggle-border-color:var(--gray-300);--dropdown-toggle-hover-border-color:var(--gray-400);--dropdown-bg:var(--white);--dropdown-color:var(--gray-600);--dropdown-border-color:var(--gray-200);--dropdown-link-color:var(--gray-700);--dropdown-link-hover-bg:var(--gray-100);--dropdown-icon-color:var(--gray-600);--dropdown-settings-icon-color:var(--gray-400);--dropdown-settings-active-item-bg:var(--gray-100);--dropdown-settings-active-item-color:var(--color-primary);--dropdown-settings-active-item-shadow:inset 0 0 0 1px #5368d580;--dropdown-item-success-bg:#dafbe1;--dropdown-item-warning-bg:#fff8c5;--dropdown-item-danger-bg:#ffebe9;--dropdown-item-success-color:#1a7f37;--dropdown-item-warning-color:#9a6700;--dropdown-item-danger-color:#d1242f;--datagrid-noresults-placeholder-bg:var(--gray-100);--datagrid-hidden-results-gradient-bg:var(--gray-50);--table-thead-color:var(--gray-800);--table-cell-color:var(--gray-600);--table-thead-marker-color:var(--gray-400);--table-cell-border-color:var(--gray-200);--table-hover-cell-bg:var(--gray-50);--table-selected-cell-bg:var(--indigo-50);--table-thead-sorted-color:var(--gray-900);--table-thead-sorted-marker-color:var(--color-primary);--datalist-border-color:var(--gray-200);--datalist-label-color:var(--gray-500);--datalist-value-color:var(--gray-600);--pagination-color:var(--gray-600);--pagination-hover-border-color:var(--gray-300);--pagination-disabled-color:var(--gray-400);--pagination-active-bg:var(--color-primary);--pagination-active-color:var(--white);--field-language-badge-border-color:var(--gray-300);--field-country-flag-border-color:var(--gray-200);--modal-bg:var(--white);--modal-border-color:var(--gray-200);--modal-header-bg:var(--gray-50);--modal-header-border-color:var(--gray-300);--modal-footer-bg:var(--gray-100);--modal-title-color:var(--gray-700);--detail-label-tooltip-underline-color:var(--gray-400);--form-label-color:var(--gray-800);--form-control-bg:var(--white);--form-control-disabled-bg:var(--gray-200);--form-control-disabled-color:var(--gray-600);--form-input-border-color:var(--gray-300);--form-input-error-legend-color:var(--red-600);--form-input-error-border-color:var(--red-600);--form-input-hover-border-color:var(--gray-400);--form-input-shadow:0 1px 2px 0 var(--gray-50);--form-input-hover-shadow:0 0 0 4px var(--gray-100);--form-input-error-shadow:0 0 0 3px var(--red-100);--form-input-text-color:var(--gray-700);--form-input-group-text-bg:var(--form-control-bg);--form-input-group-text-border-color:var(--form-input-border-color);--form-switch-bg:var(--body-bg);--form-switch-border-color:var(--gray-400);--form-switch-checked-bg:var(--indigo-500);--form-type-check-input-border-color:var(--gray-400);--form-type-check-input-box-shadow:0 1px 2px 0 var(--gray-50);--form-type-check-input-checked-bg:var(--indigo-500);--form-type-text-editor-toolbar-bg:var(--white);--form-type-text-editor-toolbar-button-color:var(--gray-600);--form-type-text-editor-toolbar-button-hover-color:var(--gray-100);--form-type-text-editor-toolbar-button-active-bg:var(--gray-200);--form-type-text-editor-toolbar-button-active-color:var(--gray-700);--form-type-text-editor-dialog-bg:var(--white);--form-type-text-editor-dialog-box-shadow:0 4px 12px var(--gray-300);--form-type-text-editor-content-pre-bg:var(--gray-200);--form-type-text-editor-content-pre-color:var(--text-color);--form-type-collection-item-collapsed-hover-bg:var(--gray-100);--form-type-autocomplete-dropdown-bg:var(--white);--form-type-autocomplete-dropdown-input-wrapper-bg:var(--gray-100);--form-type-autocomplete-dropdown-input-border-color:var(--form-input-border-color);--form-type-autocomplete-dropdown-active-item-bg:var(--gray-200);--form-type-autocomplete-close-button-bg:var(--gray-500);--form-type-autocomplete-close-button-hover-bg:var(--gray-700);--form-type-autocomplete-optgroup-bg:var(--body-bg);--form-type-autocomplete-optgroup-color:var(--gray-500);--form-type-autocomplete-multi-item-bg:var(--gray-100);--form-type-autocomplete-multi-item-border-color:var(--white);--form-type-autocomplete-multi-item-remove-button-hover-bg:var(--gray-200);--form-global-error-bg:var(--red-100);--form-global-error-color:var(--color-danger);--form-global-error-border:1px solid transparent;--form-help-color:var(--gray-600);--form-help-error-color:var(--gray-800);--form-help-active-color:var(--gray-800);--form-tabs-border-color:var(--gray-200);--form-tabs-help-color:var(--gray-600);--form-column-header-color:var(--gray-700);--form-column-help-color:var(--gray-600);--form-column-icon-color:var(--gray-500);--form-fieldset-header-color:var(--gray-700);--form-fieldset-help-color:var(--gray-600);--form-fieldset-border-color:var(--gray-200);--form-fieldset-header-border-color:var(--gray-200);--form-fieldset-icon-color:var(--gray-500);--form-fieldset-collapse-marker-color:var(--gray-400);--form-collection-item-collapse-marker-color:var(--gray-400);--badge-border:0;--badge-boolean-false-bg:var(--gray-200);--badge-boolean-false-box-shadow:inset 0 0 0 1px var(--gray-300);--badge-boolean-false-color:var(--text-color);--badge-boolean-true-bg:var(--color-primary);--badge-boolean-true-box-shadow:none;--badge-boolean-true-color:var(--white);--badge-success-bg:var(--green-100);--badge-success-box-shadow:none;--badge-success-color:var(--text-green-600);--badge-warning-bg:var(--yellow-100);--badge-warning-box-shadow:none;--badge-warning-color:var(--text-yellow-600);--badge-danger-bg:var(--red-100);--badge-danger-box-shadow:none;--badge-danger-color:var(--text-red-600);--badge-info-bg:var(--blue-100);--badge-info-box-shadow:none;--badge-info-color:var(--text-blue-600);--badge-primary-bg:var(--indigo-100);--badge-primary-box-shadow:none;--badge-primary-color:var(--text-indigo-600);--badge-secondary-bg:var(--gray-200);--badge-secondary-box-shadow:none;--badge-secondary-color:var(--gray-600);--badge-light-bg:var(--gray-50);--badge-light-box-shadow:none;--badge-light-color:var(--text-color);--badge-dark-bg:var(--gray-900);--badge-dark-box-shadow:none;--badge-dark-color:var(--gray-50);--badge-outline-box-shadow:inset 0 0 0 1px var(--gray-300);--badge-outline-color:var(--datalist-value-color);--alert-primary-bg:var(--indigo-100);--alert-primary-color:var(--indigo-800);--alert-primary-border-color:var(--indigo-200);--alert-secondary-bg:var(--gray-100);--alert-secondary-color:var(--gray-800);--alert-secondary-border-color:var(--gray-200);--alert-success-bg:var(--emerald-100);--alert-success-color:var(--emerald-900);--alert-success-border-color:var(--emerald-200);--alert-info-bg:var(--sky-100);--alert-info-color:var(--sky-800);--alert-info-border-color:var(--sky-200);--alert-warning-bg:var(--orange-100);--alert-warning-color:var(--orange-800);--alert-warning-border-color:var(--orange-200);--alert-danger-bg:var(--rose-100);--alert-danger-color:var(--rose-800);--alert-danger-border-color:var(--rose-200);--alert-light-bg:var(--white);--alert-light-color:var(--gray-800);--alert-light-border-color:var(--gray-200);--alert-dark-bg:var(--gray-800);--alert-dark-color:var(--gray-50);--alert-dark-border-color:var(--gray-500);--button-padding-y-sm:0px;--button-padding-x-sm:8px;--button-padding-y-md:0;--button-padding-x-md:12px;--button-padding-y-lg:8px;--button-padding-x-lg:16px;--button-font-size-sm:12px;--button-font-size-md:14px;--button-font-size-lg:16px;--button-line-height:1.5;--button-transition-duration:80ms;--button-transition-timing:cubic-bezier(0.65,0,0.35,1);--button-disabled-opacity:0.9;--button-focus-outline-color:var(--indigo-600);--button-primary-box-shadow:0px 1px 1px 0px #1f23280f,0px 1px 3px 0px #1f23280f;--button-primary-bg:linear-gradient(180deg,#566cdb,#5368d5);--button-primary-color:var(--white);--button-primary-icon-color:inherit;--button-primary-border-color:#1f232826;--button-primary-hover-bg:linear-gradient(180deg,#5368d5,#5064cc);--button-primary-hover-color:var(--white);--button-primary-hover-border-color:#1f232826;--button-primary-active-box-shadow:inset 0 1px 0 0 #002d114d;--button-primary-active-bg:linear-gradient(180deg,#5064cc,#4c5fc2);--button-primary-active-color:var(--white);--button-primary-active-border-color:#1f232826;--button-secondary-box-shadow:0 1px 0 0 #1f23280a;--button-secondary-bg:linear-gradient(180deg,#fafdff,#f6f8fa);--button-secondary-color:var(--text-primary-color);--button-secondary-icon-color:var(--gray-700);--button-secondary-border-color:#d1d9e0;--button-secondary-hover-bg:linear-gradient(180deg,#f6f8fa,#eff2f5);--button-secondary-hover-color:var(--text-primary-color);--button-secondary-hover-border-color:#d1d9e0;--button-secondary-active-box-shadow:inset 0 1px 0 0 #dee6ed;--button-secondary-active-bg:linear-gradient(180deg,#eff2f5,#e6eaef);--button-secondary-active-color:var(--text-primary-color);--button-secondary-active-border-color:#d1d9e0;--button-success-box-shadow:0 1px 0 0 #1f23280a;--button-success-bg:linear-gradient(180deg,#fff,#f6f8fa);--button-success-color:#1f883d;--button-success-icon-color:inherit;--button-success-border-color:#1f232826;--button-success-hover-bg:#1f883d;--button-success-hover-color:var(--white);--button-success-hover-border-color:#1f232826;--button-success-active-box-shadow:inset 0px 1px 0px 0px #002d114d;--button-success-active-bg:#197935;--button-success-active-color:var(--white);--button-success-active-border-color:#1f232826;--button-warning-box-shadow:0 1px 0 0 #1f23280a;--button-warning-bg:linear-gradient(180deg,#fff,#f6f8fa);--button-warning-color:#a67a00;--button-warning-icon-color:inherit;--button-warning-border-color:#1f232826;--button-warning-hover-bg:#b88700;--button-warning-hover-color:var(--white);--button-warning-hover-border-color:#1f232826;--button-warning-active-box-shadow:inset 0px 1px 0px 0px #2d24004d;--button-warning-active-bg:#b88700;--button-warning-active-color:var(--white);--button-warning-active-border-color:#2d24004d;--button-danger-box-shadow:0 1px 0 0 #1f23280a;--button-danger-bg:linear-gradient(180deg,#fff,#f6f8fa);--button-danger-color:#d1242f;--button-danger-icon-color:inherit;--button-danger-border-color:#d1d9e0;--button-danger-hover-bg:#cf222e;--button-danger-hover-color:var(--white);--button-danger-hover-border-color:#1f23280a;--button-danger-active-box-shadow:inset 0 1px 0 0 #4c001433;--button-danger-active-bg:#a40e26;--button-danger-active-color:var(--white);--button-danger-active-border-color:#1f23280a;--button-invisible-box-shadow:none;--button-invisible-bg:transparent;--button-invisible-color:inherit;--button-invisible-icon-color:inherit;--button-invisible-border-color:transparent;--button-invisible-hover-bg:#00000026;--button-invisible-hover-color:inherit;--button-invisible-hover-border-color:transparent;--button-invisible-active-bg:#00000026;--button-invisible-active-color:inherit;--button-invisible-active-box-shadow:none;--button-invisible-active-border-color:transparent;--button-invisible-danger-color:#cf222e;--button-invisible-danger-hover-color:#cf222e;--button-invisible-danger-hover-icon-color:inherit;--button-invisible-danger-hover-hover-bg:#ffebe9;--button-invisible-danger-active-color:#a40e26;--button-invisible-danger-hover-active-bg:#ffdad6;--text-color:var(--gray-800);--text-color-rgb:30,41,59;--text-color-dark:#292d42;--text-color-light:#9fa9b7;--box-shadow-lg:0 10px 15px -3px rgba(15,23,41,.1),0 4px 6px -2px rgba(15,23,41,.05);--content-panel-bg:#f8fafc;--fieldset-bg:#f5f7fa;--code-color:#c44c34;--code-editor-string-color:#032f62;--code-editor-keyword-color:#d73a49;--code-editor-comment-color:#22863a;--code-editor-definition-color:#e36209;--code-editor-variable-color:var(--form-input-text-color);--code-editor-number-color:var(--form-input-text-color);--code-editor-argument-color:#6f42c1;--code-editor-key-color:#005cc5;--code-editor-attribute-color:#22863a;--code-editor-addition-bg:#e6ffed;--code-editor-deletion-bg:#ffeef0;--code-editor-selection-bg:#d7d7d7;--page-login-bg:var(--gray-100);--page-login-form-bg:var(--white);--page-login-form-control-bg:var(--form-control-bg);--page-login-form-control-border-color:var(--form-input-border-color);--page-login-form-control-button-bg:var(--button-primary-bg);--zindex-700:777;--zindex-800:888;--zindex-900:999;--zindex-1050:1050;--text-blue-600:#075692;--text-green-600:#0d5e42;--text-indigo-600:#3c4caa;--text-red-600:#a11b4c;--text-yellow-600:#943505;--color-primary:#5368d5;--color-success:#1ea471;--color-info:#0679b7;--color-warning:#d97817;--color-danger:var(--red-600);--color-danger-rgb:220,38,38;--highlight-bg:#feff3f;--highlight-color:var(--text-color);--text-on-primary:var(--white);--text-muted:var(--gray-500);--link-color:#5c70d6;--link-color-rgb:92,112,214;--link-hover-color:#99a6e6;--link-hover-color-rgb:153,166,230;--link-hover-decoration:none;--link-danger-color:var(--red-600);--link-danger-hover-color:var(--red-500);--border-radius:4px;--border-radius-lg:8px;--border-radius-sm:2px;--border-width:1px;--border-style:solid;--border-color:#e3e7ee}.ea-dark-scheme{--text-primary-color:var(--text-color);--text-secondary-color:var(--text-muted);--text-tertiary-color:var(--true-gray-600);--border-primary-color:var(--true-gray-600);--border-secondary-color:var(--true-gray-700);--border-tertiary-color:var(--true-gray-800);--primary-bg:var(--true-gray-600);--secondary-bg:var(--true-gray-800);--tertiary-bg:var(--true-gray-900);--shadow-md:0 4px 6px -1px rgba(0,0,0,.3),0 2px 4px -2px rgba(0,0,0,.3);--shadow-lg:0 10px 15px -3px rgba(0,0,0,.3),0 4px 6px -4px rgba(0,0,0,.3);--shadow-xl:0 20px 25px -5px rgba(0,0,0,.4),0 8px 10px -6px rgba(0,0,0,.4);--body-bg:var(--true-gray-950);--responsive-header-bg:var(--true-gray-800);--responsive-header-border-color:var(--true-gray-600);--responsive-header-logo-color:var(--true-gray-300);--responsive-table-label-color:var(--true-gray-500);--responsive-table-row-border-color:var(--true-gray-700);--sidebar-bg:var(--true-gray-900);--sidebar-border-color:var(--true-gray-800);--sidebar-logo-color:var(--true-gray-200);--sidebar-menu-color:var(--true-gray-300);--sidebar-menu-badge-bg:var(--true-gray-800);--sidebar-menu-badge-color:var(--true-gray-300);--sidebar-menu-badge-active-bg:var(--blue-800);--sidebar-menu-badge-active-color:var(--true-gray-300);--sidebar-menu-submenu-color:var(--true-gray-400);--sidebar-menu-header-color:var(--true-gray-400);--sidebar-menu-icon-color:var(--true-gray-400);--sidebar-menu-active-item-bg:var(--true-gray-300);--sidebar-menu-active-item-color:var(--true-gray-950);--sidebar-menu-compact-hover-box-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--resize-handler-hover-bg:var(--indigo-400);--content-section-border-color:var(--true-gray-700);--content-search-input-bg:var(--body-bg);--content-search-icon-color:var(--true-gray-500);--content-search-reset-button-bg:var(--true-gray-800);--content-search-reset-button-color:var(--true-gray-300);--content-search-reset-button-hover-bg:var(--true-gray-700);--content-search-reset-button-hover-color:var(--true-gray-200);--content-top-border-color:var(--true-gray-700);--content-bg:var(--true-gray-900);--user-avatar-icon-bg:var(--true-gray-700);--user-avatar-icon-color:var(--true-gray-400);--user-name-color:var(--true-gray-400);--user-menu-impersonated-link-color:var(--color-primary);--popover-bg:var(--true-gray-900);--popover-border-color:var(--true-gray-700);--popover-color:var(--text-color);--popover-shadow:var(--shadow-xl);--popover-max-width:480px;--dropdown-toggle-bg:var(--true-gray-800);--dropdown-toggle-color:var(--true-gray-200);--dropdown-toggle-border-color:var(--true-gray-700);--dropdown-toggle-hover-border-color:var(--true-gray-600);--dropdown-bg:var(--true-gray-900);--dropdown-color:var(--true-gray-300);--dropdown-border-color:var(--true-gray-700);--dropdown-link-color:var(--true-gray-300);--dropdown-link-hover-bg:var(--true-gray-800);--dropdown-icon-color:var(--true-gray-400);--dropdown-settings-icon-color:var(--true-gray-500);--dropdown-settings-active-item-bg:var(--true-gray-950);--dropdown-settings-active-item-color:var(--color-primary);--dropdown-settings-active-item-shadow:inset 0 0 0 1px var(--true-gray-800);--dropdown-item-success-bg:#2ea04326;--dropdown-item-warning-bg:#bb800926;--dropdown-item-danger-bg:#f851491a;--dropdown-item-success-color:#3fb950;--dropdown-item-warning-color:#d29922;--dropdown-item-danger-color:#ff7b72;--datagrid-noresults-placeholder-bg:var(--true-gray-700);--datagrid-hidden-results-gradient-bg:var(--true-gray-700);--table-thead-color:var(--true-gray-200);--table-cell-color:var(--true-gray-300);--table-thead-marker-color:var(--true-gray-500);--table-cell-border-color:var(--true-gray-800);--table-hover-cell-bg:var(--true-gray-900);--table-selected-cell-bg:rgba(3,102,214,.25);--table-thead-sorted-color:var(--color-primary);--table-thead-sorted-marker-color:var(--color-primary);--datalist-border-color:var(--true-gray-600);--datalist-label-color:var(--true-gray-400);--datalist-value-color:var(--true-gray-300);--pagination-color:var(--true-gray-400);--pagination-hover-border-color:var(--true-gray-600);--pagination-active-bg:var(--blue-500);--pagination-active-color:var(--white);--field-language-badge-border-color:var(--true-gray-600);--field-country-flag-border-color:var(--true-gray-600);--modal-bg:var(--true-gray-800);--modal-border-color:var(--true-gray-600);--modal-header-bg:var(--true-gray-900);--modal-header-border-color:var(--true-gray-600);--modal-footer-bg:var(--true-gray-700);--modal-title-color:var(--true-gray-400);--pagination-disabled-color:var(--true-gray-600);--detail-label-tooltip-underline-color:var(--true-gray-500);--form-label-color:var(--true-gray-300);--form-control-bg:var(--true-gray-900);--form-control-disabled-bg:var(--true-gray-900);--form-control-disabled-color:var(--true-gray-500);--form-input-border-color:var(--true-gray-700);--form-input-error-legend-color:var(--red-500);--form-input-error-border-color:var(--red-500);--form-input-hover-border-color:var(--true-gray-500);--form-input-shadow:none;--form-input-hover-shadow:none;--form-input-error-shadow:0 0 0 3px var(--red-900);--form-input-text-color:var(--true-gray-200);--form-input-group-text-bg:var(--true-gray-800);--form-input-group-text-border-color:var(--true-gray-600);--form-switch-bg:var(--true-gray-600);--form-switch-border-color:var(--true-gray-700);--form-switch-checked-bg:var(--blue-600);--form-type-check-input-border-color:var(--true-gray-400);--form-type-check-input-box-shadow:0 1px 2px 0 var(--true-gray-800);--form-type-check-input-checked-bg:var(--blue-600);--form-type-text-editor-toolbar-bg:var(--true-gray-800);--form-type-text-editor-toolbar-button-color:var(--true-gray-400);--form-type-text-editor-toolbar-button-hover-color:var(--true-gray-700);--form-type-text-editor-toolbar-button-active-bg:var(--true-gray-700);--form-type-text-editor-toolbar-button-active-color:var(--true-gray-300);--form-type-text-editor-dialog-bg:var(--true-gray-800);--form-type-text-editor-dialog-box-shadow:0 4px 12px var(--true-gray-900);--form-type-text-editor-content-pre-bg:var(--true-gray-800);--form-type-text-editor-content-pre-color:var(--true-gray-300);--form-type-collection-item-collapsed-hover-bg:var(--true-gray-800);--form-type-autocomplete-dropdown-bg:var(--true-gray-800);--form-type-autocomplete-dropdown-input-wrapper-bg:var(--true-gray-900);--form-type-autocomplete-dropdown-input-border-color:transparent;--form-type-autocomplete-dropdown-active-item-bg:var(--true-gray-700);--form-type-autocomplete-close-button-bg:var(--true-gray-500);--form-type-autocomplete-close-button-hover-bg:var(--true-gray-800);--form-type-autocomplete-optgroup-bg:var(--form-type-autocomplete-dropdown-bg);--form-type-autocomplete-optgroup-color:var(--true-gray-400);--form-type-autocomplete-multi-item-bg:var(--true-gray-700);--form-type-autocomplete-multi-item-border-color:var(--true-gray-500);--form-type-autocomplete-multi-item-remove-button-hover-bg:var(--true-gray-800);--form-global-error-bg:transparent;--form-global-error-color:var(--red-400);--form-global-error-border:1px solid var(--red-400);--form-help-color:var(--true-gray-500);--form-help-error-color:var(--true-gray-200);--form-help-active-color:var(--true-gray-300);--form-tabs-border-color:var(--true-gray-600);--form-tabs-help-color:var(--true-gray-500);--form-column-header-color:var(--true-gray-300);--form-column-help-color:var(--true-gray-500);--form-column-icon-color:var(--true-gray-400);--form-fieldset-header-color:var(--true-gray-300);--form-fieldset-help-color:var(--true-gray-500);--form-fieldset-border-color:var(--true-gray-700);--form-fieldset-header-border-color:var(--true-gray-600);--form-fieldset-icon-color:var(--true-gray-400);--form-fieldset-collapse-marker-color:var(--true-gray-500);--form-collection-item-collapse-marker-color:var(--true-gray-500);--badge-box-shadow:inset 0 0 0 1px hsla(0,0%,96%,.3);--badge-boolean-false-bg:hsla(0,0%,96%,.1);--badge-boolean-false-box-shadow:inset 0 0 0 1px hsla(0,0%,96%,.3);--badge-boolean-false-color:var(--true-gray-200);--badge-boolean-true-bg:rgba(3,102,214,.18);--badge-boolean-true-box-shadow:inset 0 0 0 1px rgba(90,168,252,.3);--badge-boolean-true-color:#5aa8fc;--badge-success-bg:rgba(22,135,0,.18);--badge-success-box-shadow:inset 0 0 0 1px rgba(39,236,0,.3);--badge-success-color:var(--green-300);--badge-warning-bg:rgba(251,202,4,.18);--badge-warning-box-shadow:inset 0 0 0 1px rgba(250,201,5,.3);--badge-warning-color:var(--yellow-400);--badge-danger-bg:rgba(182,2,5,.18);--badge-danger-box-shadow:inset 0 0 0 1px rgba(253,155,157,.3);--badge-danger-color:var(--red-300);--badge-info-bg:rgba(3,102,214,.18);--badge-info-box-shadow:inset 0 0 0 1px rgba(90,168,252,.3);--badge-info-color:#5aa8fc;--badge-primary-bg:rgba(3,102,214,.18);--badge-primary-box-shadow:inset 0 0 0 1px rgba(90,168,252,.3);--badge-primary-color:#5aa8fc;--badge-secondary-bg:hsla(0,0%,96%,.1);--badge-secondary-box-shadow:inset 0 0 0 1px hsla(0,0%,96%,.3);--badge-secondary-color:var(--true-gray-200);--badge-light-bg:hsla(0,0%,100%,.18);--badge-light-box-shadow:inset 0 0 0 1px hsla(0,0%,100%,.3);--badge-light-color:#fff;--badge-dark-bg:rgba(0,0,0,.18);--badge-dark-box-shadow:inset 0 0 0 1px hsla(0,0%,60%,.3);--badge-dark-color:#999;--badge-outline-box-shadow:inset 0 0 0 1px var(--true-gray-500);--badge-outline-color:var(--datalist-value-color);--alert-primary-bg:var(--indigo-900);--alert-primary-color:var(--indigo-100);--alert-primary-border-color:var(--indigo-800);--alert-secondary-bg:var(--true-gray-700);--alert-secondary-color:var(--true-gray-300);--alert-secondary-border-color:var(--true-gray-600);--alert-success-bg:var(--emerald-800);--alert-success-color:var(--emerald-100);--alert-success-border-color:var(--emerald-700);--alert-info-bg:var(--sky-800);--alert-info-color:var(--sky-100);--alert-info-border-color:var(--sky-700);--alert-warning-bg:var(--orange-800);--alert-warning-color:var(--orange-100);--alert-warning-border-color:var(--orange-700);--alert-danger-bg:var(--red-800);--alert-danger-color:var(--red-100);--alert-danger-border-color:var(--red-700);--alert-light-bg:var(--true-gray-300);--alert-light-color:var(--true-gray-800);--alert-light-border-color:var(--true-gray-200);--alert-dark-bg:var(--true-gray-900);--alert-dark-color:var(--true-gray-200);--alert-dark-border-color:var(--true-gray-700);--button-focus-outline-color:#1f6febb3;--button-primary-box-shadow:none;--button-primary-bg:#1447e6;--button-primary-color:var(--white);--button-primary-icon-color:hsla(0,0%,100%,.85);--button-primary-border-color:#ffffff26;--button-primary-hover-bg:#135af2;--button-primary-hover-color:var(--white);--button-primary-hover-border-color:#ffffff26;--button-primary-active-box-shadow:none;--button-primary-active-bg:#1f66ff;--button-primary-active-color:var(--white);--button-primary-active-border-color:#ffffff26;--button-secondary-box-shadow:none;--button-secondary-bg:#242424;--button-secondary-color:var(--text-primary-color);--button-secondary-icon-color:var(--text-muted);--button-secondary-border-color:#3d3d3d;--button-secondary-hover-bg:#2c2c2c;--button-secondary-hover-color:var(--text-primary-color);--button-secondary-hover-border-color:#3d3d3d;--button-secondary-active-box-shadow:none;--button-secondary-active-bg:#313131;--button-secondary-active-color:var(--text-primary-color);--button-secondary-active-border-color:#4d4d4d;--button-success-box-shadow:none;--button-success-bg:#242424;--button-success-color:#56d364;--button-success-icon-color:inherit;--button-success-border-color:#3d3d3d;--button-success-hover-bg:#29903b;--button-success-hover-color:var(--white);--button-success-hover-border-color:#ffffff26;--button-success-active-box-shadow:none;--button-success-active-bg:#2e9a40;--button-success-active-color:var(--white);--button-success-active-border-color:#ffffff26;--button-warning-box-shadow:0 1px 0 0 #1f23280a;--button-warning-bg:#242424;--button-warning-color:#e3b341;--button-warning-icon-color:inherit;--button-warning-border-color:#3d3d3d;--button-warning-hover-bg:#9e6a03;--button-warning-hover-color:var(--white);--button-warning-hover-border-color:#ffffff26;--button-warning-active-box-shadow:none;--button-warning-active-bg:#bb8009;--button-warning-active-color:var(--white);--button-warning-active-border-color:#ffffff26;--button-danger-box-shadow:none;--button-danger-bg:#242424;--button-danger-color:#fa5e55;--button-danger-icon-color:inherit;--button-danger-border-color:#3d3d3d;--button-danger-hover-bg:#b62324;--button-danger-hover-color:var(--white);--button-danger-hover-border-color:#ffffff26;--button-danger-active-box-shadow:none;--button-danger-active-bg:#da3633;--button-danger-active-color:var(--white);--button-danger-active-border-color:#ffffff26;--button-invisible-box-shadow:none;--button-invisible-bg:transparent;--button-invisible-color:inherit;--button-invisible-icon-color:inherit;--button-invisible-border-color:transparent;--button-invisible-hover-bg:#ffffff40;--button-invisible-hover-color:inherit;--button-invisible-hover-border-color:transparent;--button-invisible-active-bg:#ffffff40;--button-invisible-active-color:inherit;--button-invisible-active-box-shadow:none;--button-invisible-active-border-color:transparent;--button-invisible-danger-color:#fa5e55;--button-invisible-danger-hover-color:var(--white);--button-invisible-danger-hover-icon-color:inherit;--button-invisible-danger-hover-hover-bg:#b62324;--button-invisible-danger-active-color:var(--white);--button-invisible-danger-hover-active-bg:#da3633;--text-color:var(--true-gray-300);--text-color-rgb:212,212,212;--text-color-dark:var(--true-gray-200);--text-color-light:var(--true-gray-400);--box-shadow-lg:0 10px 15px -3px rgba(15,23,41,.1),0 4px 6px -2px rgba(15,23,41,.05);--content-panel-bg:#f8fafc;--fieldset-bg:#f5f7fa;--code-color:#c44c34;--code-editor-string-color:#a5d6ff;--code-editor-keyword-color:#ff7b72;--code-editor-comment-color:#7ee787;--code-editor-definition-color:#e36209;--code-editor-variable-color:var(--form-input-text-color);--code-editor-number-color:var(--form-input-text-color);--code-editor-argument-color:#d2a8ff;--code-editor-key-color:#a5d6ff;--code-editor-attribute-color:#7ee787;--code-editor-addition-bg:rgba(46,160,67,.3);--code-editor-deletion-bg:rgba(218,54,51,.3);--code-editor-selection-bg:#203e6f;--page-login-bg:var(--true-gray-800);--page-login-form-bg:var(--true-gray-700);--page-login-form-control-bg:var(--true-gray-800);--page-login-form-control-border-color:var(--true-gray-600);--page-login-form-control-button-bg:var(--blue-700);--text-blue-600:#075692;--text-green-600:#0d5e42;--text-indigo-600:#3c4caa;--text-red-600:#a11b4c;--text-yellow-600:#943505;--color-primary:#70aefb;--color-success:#1ea471;--color-info:#0679b7;--color-warning:#d97817;--color-danger:var(--red-500);--bs-danger-rgb:239,68,68;--highlight-bg:#feff3f;--highlight-color:var(--true-gray-900);--text-on-primary:var(--white);--text-muted:var(--true-gray-500);--link-color:var(--blue-400);--link-hover-color:var(--blue-300);--link-hover-decoration:none;--border-color:#e3e7ee}:root,[data-bs-theme=dark],[data-bs-theme=light]{--bs-body-bg:var(--body-bg);--bs-body-color-rgb:var(--text-color-rgb);--bs-body-color:var(--text-color);--bs-body-font-family:var(--font-family-base);--bs-body-font-size:var(--font-size-base);--bs-body-font-weight:normal;--bs-border-color:var(--border-color);--bs-border-radius-lg:var(--border-radius-lg);--bs-border-radius-sm:var(--border-radius-sm);--bs-border-radius:var(--border-radius);--bs-border-width:var(--border-width);--bs-code-color:var(--code-color);--bs-danger-rgb:var(--color-danger-rgb);--bs-danger:var(--color-danger);--bs-emphasis-color-rgb:var(--text-color-rgb);--bs-emphasis-color:var(--text-color);--bs-font-monospace:var(--font-family-monospace);--bs-form-invalid-border-color:var(--color-danger);--bs-form-invalid-color:var(--color-danger);--bs-form-valid-border-color:var(--color-success);--bs-form-valid-color:var(--color-success);--bs-heading-color:var(--text-color);--bs-highlight-bg:var(--highlight-bg);--bs-highlight-color:inherit;--bs-info:var(--color-info);--bs-link-color-rgb:var(--link-color-rgb);--bs-link-decoration:none;--bs-link-hover-color-rgb:var(--link-hover-color-rgb);--bs-link-opacity:1;--bs-primary:var(--color-primary);--bs-secondary-bg:var(--secondary-bg);--bs-secondary-color:var(--text-secondary-color);--bs-secondary:var(--text-muted);--bs-success:var(--color-success);--bs-tertiary-bg:var(--tertiary-bg);--bs-tertiary-color:var(--text-tertiary-color);--bs-warning:var(--color-warning)}.btn{--bs-btn-padding-x:8px;--bs-btn-padding-y:4px;--bs-btn-font-size:0.875rem;--bs-btn-font-weight:400;--bs-btn-border-width:0;--bs-btn-border-radius:var(--border-radius)}.dropdown-menu{--bs-dropdown-font-size:0.875rem}.table{--bs-table-active-bg:var(--table-selected-cell-bg);--bs-table-active-color:var(--table-cell-color);--bs-table-bg:var(--body-bg);--bs-table-border-color:var(--table-cell-border-color);--bs-table-color:var(--table-cell-color);--bs-table-hover-bg:var(--table-hover-cell-bg);--bs-table-hover-color:var(--table-cell-color)}.pagination{--bs-pagination-padding-y:4px;--bs-pagination-padding-x:10px;--bs-pagination-color:var(--pagination-color);--bs-pagination-line-height:1.5;--bs-pagination-bg:var(--body-bg);--bs-pagination-border-width:1px;--bs-pagination-border-color:transparent;--bs-pagination-focus-box-shadow:none;--bs-pagination-focus-outline:0;--bs-pagination-hover-color:var(--text-color);--bs-pagination-hover-bg:var(--body-bg);--bs-pagination-hover-border-color:var(--pagination-hover-border-color);--bs-pagination-disabled-color:var(--text-muted);--bs-pagination-disabled-bg:var(--body-bg);--bs-pagination-disabled-border-color:transparent}.modal{--bs-modal-zindex:2040;--bs-modal-width:500px;--bs-modal-padding:1rem 1.25rem;--bs-modal-margin:0.5rem;--bs-modal-color:var(--text-color);--bs-modal-bg:var(--modal-bg);--bs-modal-border-color:var(--modal-border-color);--bs-modal-border-width:var(--border-width);--bs-modal-border-radius:var(--border-radius);--bs-modal-box-shadow:var(--bs-box-shadow-sm);--bs-modal-inner-border-radius:calc(var(--bs-border-radius-lg) - var(--bs-border-width));--bs-modal-header-padding-x:1.25rem;--bs-modal-header-padding-y:1rem;--bs-modal-header-padding:1rem 1.25rem;--bs-modal-header-border-color:var(--modal-border-color);--bs-modal-header-border-width:var(--border-width);--bs-modal-title-line-height:1.2;--bs-modal-footer-gap:0.5rem;--bs-modal-footer-bg:var(--modal-footer-bg);--bs-modal-footer-border-color:var(--modal-border-color);--bs-modal-footer-border-width:var(--border-width)}.nav-tabs{--bs-nav-tabs-border-width:var(--border-width);--bs-nav-tabs-border-color:var(--form-tabs-border-color);--bs-nav-tabs-border-radius:var(--border-radius);--bs-nav-tabs-link-hover-border-color:var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color);--bs-nav-tabs-link-active-color:var(--link-color);--bs-nav-tabs-link-active-bg:transparent;--bs-nav-tabs-link-active-border-color:var(--border-color) var(--border-color) transparent var(--border-color)}.badge{--bs-badge-padding-x:5px;--bs-badge-padding-y:1px;--bs-badge-font-size:var(--font-size-xs);--bs-badge-font-weight:500;--bs-badge-color:var(--text-color);--bs-badge-border-radius:var(--bs-border-radius)}.offcanvas,.offcanvas-lg,.offcanvas-md,.offcanvas-sm,.offcanvas-xl,.offcanvas-xxl{--bs-offcanvas-padding-x:20px;--bs-offcanvas-padding-y:15px}.alert{--bs-alert-padding-x:1rem;--bs-alert-padding-y:1rem;--bs-alert-margin-block-end:0;--bs-alert-border-radius:0;--bs-alert-link-color:inherit}:root{color-scheme:light dark}body,html{block-size:100vh}body{background-color:var(--body-bg);color:var(--text-color);font-family:var(--font-family-base);font-size:var(--font-size-base)}i.fa,i.far,i.fas{font-family:Font Awesome\ 6 Free,sans-serif!important}i.fab{font-family:Font Awesome\ 6 Brands,sans-serif!important}i.fal{font-family:Font Awesome\ 6 Pro,sans-serif!important}i.fad{font-family:Font Awesome\ 6 Duotone,sans-serif!important}span.icon{display:inline-block;inline-size:1.25em;text-align:center}span.icon svg{block-size:100%;inline-size:100%;max-block-size:1em;max-inline-size:1em;vertical-align:text-bottom}body[data-ea-icon-prefix=tabler] span.icon svg{max-block-size:1.15em;max-inline-size:1.15em}a{color:var(--link-color);text-decoration:none}a:hover{color:var(--link-hover-color);text-decoration:var(--link-hover-decoration)}code,pre{color:var(--code-color);font-family:var(--font-family-monospace);font-size:13px}pre{line-height:1.8}.text-left{text-align:left}.text-right{text-align:right}@media (min-width:992px){.wrapper{display:grid;grid-template-columns:var(--sidebar-max-width) auto;min-block-size:100vh}}@media (min-width:1280px){.wrapper{grid-column-gap:0}}body:not(.ea-content-width-full) .content-wrapper{max-inline-size:var(--body-max-width)}@media (min-width:992px){body.ea-sidebar-width-compact .wrapper{grid-template-columns:44px auto}}.responsive-header{align-items:center;background:var(--responsive-header-bg);box-shadow:inset 0 -1px 0 var(--responsive-header-border-color);display:flex;justify-content:space-between;padding:8px 15px}@media (min-width:992px){.responsive-header{display:none}}.responsive-header #responsive-header-logo{font-size:var(--font-size-base);font-weight:500;margin:0;padding:0 15px}.responsive-header #responsive-header-logo a{color:var(--responsive-header-logo-color)}.responsive-header .dropdown-settings{display:block}.main-header{display:none}@media (min-width:992px){.main-header{display:block}}.main-header .navbar{display:block;padding:0 0 0 var(--sidebar-menu-items-padding-left)}.main-header #header-logo{overflow:hidden}.main-header #header-logo a{color:var(--sidebar-logo-color);display:block;font-size:var(--font-size-lg);font-weight:500;line-height:24px;padding:17px 0 28px}.main-header #header-logo img,.main-header #header-logo svg{max-inline-size:100%}.main-header #header-logo .logo-custom{display:block}.main-header #header-logo .logo-compact{display:none}@media (min-width:992px){body.ea-sidebar-width-compact .main-header #header-logo .logo-custom{display:none}body.ea-sidebar-width-compact .main-header #header-logo .logo-compact{display:block}}#navigation-toggler{margin-inline-start:-5px}@media (min-width:992px){#navigation-toggler{display:none}}.sidebar-wrapper{position:relative}.sidebar{background:var(--sidebar-bg);block-size:100%;inline-size:calc(40px + var(--sidebar-max-width));inset-block-start:0;inset-inline-start:calc(-40px - var(--sidebar-max-width));min-block-size:100vh;overflow-block:auto;overflow-inline:hidden;overscroll-behavior:contain;padding:15px 20px;position:fixed;transition:left .3s;z-index:calc(var(--zindex-modal-backdrop) + 1)}@media (min-width:992px){.sidebar{box-shadow:inset -1px 0 0 var(--sidebar-border-color);inline-size:auto;max-inline-size:var(--sidebar-max-width);overscroll-behavior:auto;padding:0 var(--sidebar-padding-right) 0 var(--sidebar-padding-left);position:static;z-index:calc(var(--zindex-modal-backdrop) - 1)}}body.ea-mobile-sidebar-visible .sidebar{box-shadow:20px 0 25px -5px rgba(0,0,0,.1),10px 0 10px -5px rgba(0,0,0,.04);inset-inline-start:0}.dropdown-toggle.dropdown-toggle-hidden-marker:after{display:none}.dropdown-toggle.dropdown-toggle-hidden-marker:hover{cursor:pointer}.user-menu-wrapper a.user-details,a.user-menu-wrapper .user-details:hover{align-items:center;-webkit-appearance:none;color:var(--user-name-color);cursor:pointer;display:flex}.user-menu-wrapper.user-is-impersonated a.user-details,.user-menu-wrapper.user-is-impersonated a.user-details:hover{color:var(--user-menu-impersonated-link-color);font-weight:500}.user-menu-wrapper .user-details .user-name{margin-inline-start:6px}.user-menu-wrapper .user-avatar{background:var(--user-avatar-icon-bg);block-size:21px;border-radius:var(--border-radius);color:var(--user-avatar-icon-color);display:block;inline-size:2em;max-inline-size:21px;text-align:center}.user-menu-wrapper .dropdown-user-details .user-avatar .icon{display:block}.user-menu-wrapper .dropdown-menu{max-inline-size:480px;min-inline-size:200px}.user-menu-wrapper .dropdown-menu .dropdown-user-details{align-items:flex-start;display:flex;padding:0 5px}.user-menu-wrapper .dropdown-menu .dropdown-user-details .user-avatar{block-size:39px;inline-size:auto;margin-block-start:2px;margin-inline-end:10px;max-inline-size:39px}.user-menu-wrapper .dropdown-menu .dropdown-user-details .user-avatar .icon{font-size:25px}.user-menu-wrapper .dropdown-menu .dropdown-user-details .user-label{color:var(--text-muted);display:block;font-size:var(--font-size-sm);margin-block-end:2px}.dropdown-settings{display:none}@media (min-width:992px){.dropdown-settings{display:block}}.dropdown-settings .dropdown-settings-button{color:var(--dropdown-settings-icon-color);font-size:16px;padding-inline-start:15px}.dropdown-settings .dropdown-header{color:var(--text-muted);display:block;font-size:var(--font-size-sm)}.dropdown-settings .dropdown-item.active{background:var(--dropdown-settings-active-item-bg);box-shadow:var(--dropdown-settings-active-item-shadow)}.dropdown-settings .dropdown-item.active,.dropdown-settings .dropdown-item.active .icon,.dropdown-settings .dropdown-item.active i{color:var(--dropdown-settings-active-item-color)}.content-wrapper{padding:0 var(--content-padding-right) 15px var(--content-padding-left)}@media (min-width:992px){.content-wrapper{display:grid;grid-template-columns:auto var(--resize-handler-width);padding:0 var(--lg-content-padding-right) var(--lg-content-padding-bottom) var(--lg-content-padding-left)}}.content{margin-block-start:1px}.resizer-handler{display:none}@media (min-width:992px){.resizer-handler{cursor:col-resize;display:block;inline-size:3px;margin:0 0 0 7px;min-block-size:100vh;transition:background .7s}.resizer-handler:hover{background:var(--resize-handler-hover-bg)}}#sidebar-resizer-handler{inset-block-end:0;inset-block-start:0;inset-inline-end:0;min-block-size:100vh;position:absolute}#content-resizer-handler{min-block-size:calc(100vh - 56px - var(--lg-content-padding-bottom))}.content-top{align-items:center;box-shadow:0 1px 0 var(--content-top-border-color);display:flex;padding:5px 15px 5px var(--content-padding-left)}@media (max-width:992px){.content-top.ea-search-disabled{box-shadow:none}}@media (min-width:992px){.content-top{block-size:56px;display:flex;justify-content:space-between;padding:11px calc(var(--lg-content-padding-right) + var(--resize-handler-width)) 11px var(--lg-content-padding-left);position:relative}}.content-top .navbar-custom-menu{display:none}@media (min-width:992px){.content-top .navbar-custom-menu{display:block}}.content-top .content-search{flex:1}.content-top .content-search .form-group{flex-basis:100%;padding:2px 0}.content-top .content-search .form-widget{align-items:center;display:flex;flex:unset}@media (min-width:992px){.content-top .content-search .form-widget{display:block}}.content-top .content-search .content-search-icon{color:var(--content-search-icon-color);margin-inline-end:0}.content-top .content-search .content-search-reset{background:var(--content-search-reset-button-bg);border-radius:var(--border-radius);color:var(--content-search-reset-button-color);font-size:13px;padding:2px}.content-top .content-search .content-search-reset:hover{background:var(--content-search-reset-button-hover-bg);color:var(--content-search-reset-button-hover-color)}.content-top .content-search input[type=search][name=query]{background:var(--content-search-input-bg);border:0;box-shadow:none;max-inline-size:unset}.content-top .content-search input[type=search][name=query]::-webkit-search-cancel-button,.content-top .content-search input[type=search][name=query]::-webkit-search-decoration,.content-top .content-search input[type=search][name=query]::-webkit-search-results-button,.content-top .content-search input[type=search][name=query]::-webkit-search-results-decoration{-webkit-appearance:none}.content-top .content-search input[type=search][name=query]:active,.content-top .content-search input[type=search][name=query]:focus{box-shadow:none;outline:none}.content-top .content-search .content-search-label{align-items:center;display:inline-grid;margin:0;padding:0;@media (min-width:992px){max-inline-size:600px}}.content-top .content-search .content-search-label input,.content-top .content-search .content-search-label:after{grid-area:1/2;inline-size:auto;resize:none}.content-top .content-search .content-search-label input.is-blank{min-inline-size:300px}.content-top .content-search .content-search-label:after{block-size:30px;content:attr(data-value) " ";visibility:hidden;white-space:pre-wrap}.content-header{padding:26px 0 16px}@media (min-width:768px){.content-header{align-items:flex-start;background:var(--body-bg);display:flex;flex-direction:row;justify-content:space-between;padding:36px 0 16px}}@media (min-width:992px){body.ea-edit .content-header,body.ea-new .content-header{inset-block-start:-20px;position:sticky;z-index:999}}.content-header-title{flex:1}.content-header-title .title{font-size:var(--font-size-xxl);font-weight:700;line-height:1.2;margin:0;padding-inline-end:15px}@media (min-width:992px){.content-header-title .title{font-size:var(--font-size-xxxl)}}.content-header-title .title small{color:var(--gray-600);font-size:var(--font-size-lg);font-weight:500;line-height:var(--font-size-lg)}.content-header-help{cursor:pointer}.content-header-help i{color:var(--text-muted);font-size:21px}.popover.ea-content-help-popover{--bs-popover-border-radius:var(--border-radius);border-color:var(--popover-border-color);box-shadow:var(--popover-shadow);max-inline-size:var(--popover-max-width)}.popover.ea-content-help-popover .popover-body{background:var(--popover-bg);border-radius:var(--border-radius);color:var(--popover-color);font-size:var(--font-size-base);padding:15px;text-align:left}.bs-popover-top>.popover-arrow:before,.popover.ea-content-help-popover.bs-popover-auto[data-popper-placement^=top]>.popover-arrow:before{border-block-start-color:var(--popover-border-color)}.bs-popover-top>.popover-arrow:before,.popover.ea-content-help-popover.bs-popover-auto[data-popper-placement^=top]>.popover-arrow:after{border-block-start-color:var(--popover-bg)}.bs-popover-end>.popover-arrow:before,.popover.ea-content-help-popover.bs-popover-auto[data-popper-placement^=right]>.popover-arrow:before{border-inline-end-color:var(--popover-border-color)}.bs-popover-end>.popover-arrow:after,.popover.ea-content-help-popover.bs-popover-auto[data-popper-placement^=right]>.popover-arrow:after{border-inline-end-color:var(--popover-bg)}.bs-popover-bottom>.popover-arrow,.popover.ea-content-help-popover.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow{inset-block-start:-.5rem}.bs-popover-bottom>.popover-arrow:before,.popover.ea-content-help-popover.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow:before{border-block-end-color:var(--popover-border-color)}.bs-popover-bottom>.popover-arrow:after,.popover.ea-content-help-popover.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow:after{border-block-end-color:var(--popover-bg)}.bs-popover-start>.popover-arrow:before,.popover.ea-content-help-popover.bs-popover-auto[data-popper-placement^=left]>.popover-arrow:before{border-inline-start-color:var(--popover-border-color)}.bs-popover-start>.popover-arrow:after,.popover.ea-content-help-popover.bs-popover-auto[data-popper-placement^=left]>.popover-arrow:after{border-inline-start-color:var(--popover-bg)}.ea-content-help-popover.tooltip.show{opacity:1}.content-header .global-actions,.content-header .page-actions{display:flex;flex-direction:row;flex-wrap:wrap;gap:10px;justify-content:right}.content-header .page-actions{margin:10px 0 15px}@media (min-width:768px){.content-header .page-actions{margin:2px 1px 0 10px}}.content-header .page-actions:empty{display:none}.batch-actions form{display:flex}.batch-actions .btn+.btn{margin-inline-start:15px}.with-rounded-top{border-start-end-radius:var(--border-radius);border-start-start-radius:var(--border-radius)}.with-rounded-bottom{border-end-end-radius:var(--border-radius);border-end-start-radius:var(--border-radius)}.datagrid.with-rounded-top thead tr:first-child th:first-child{border-start-start-radius:var(--border-radius)}.datagrid.with-rounded-top thead tr:first-child th:last-child{border-start-end-radius:var(--border-radius)}.content-footer{margin-block-start:15px;padding:15px 0}.content-panel{margin-block-end:20px}.content-panel-header{border-block-end:var(--border-width) var(--border-style) var(--content-section-border-color);font-size:var(--font-size-lg);line-height:24px;margin:0;padding:15px 17px 15px 20px}.content-panel-header.collapsible{padding:0}.content-panel-header.collapsible>a{color:inherit;display:block;padding:15px 17px 15px 20px}.content-panel-header.collapsible.with-help>a{padding:15px 17px 1px 20px}.content-panel-header.collapsible .collapse-icon{color:var(--color-primary);margin-inline-end:5px;transition:all .1s linear}.content-panel-collapse:not(.collapsed) .collapse-icon{transform:rotate(90deg)}.content-panel-header.collapsible.with-help .content-panel-header-help{padding:0 17px 15px 20px}.content-panel-header-help{color:var(--gray-500);font-size:var(--font-size-base)}.content-panel-body{background:var(--white);padding:15px 20px}@media (min-width:992px){.content-panel-body{padding:18px 25px}}.content-panel-body.with-min-h-250{min-block-size:250px}.content-panel-body.with-background{background:var(--content-panel-bg)}.content-panel-body.without-padding{padding:0}.content-panel-body.without-header{border-start-end-radius:var(--border-radius);border-start-start-radius:var(--border-radius)}.content-panel-body.without-footer{border-end-end-radius:var(--border-radius);border-end-start-radius:var(--border-radius)}.content-panel-footer{border-block-start:var(--border-width) var(--border-style) var(--border-color);border-end-end-radius:var(--border-radius);border-end-start-radius:var(--border-radius);margin:0;padding:15px 17px 15px 20px}.content-panel-footer.without-border{border-block-start:0}.content-panel-footer.without-padding{padding:0}.dropdown-menu{--dropdown-padding:4px;background-color:var(--dropdown-bg);border-color:var(--dropdown-border-color);box-shadow:var(--shadow-xl);color:var(--dropdown-color);max-inline-size:240px;padding:5px}.dropdown-menu.dropdown-has-submenus{padding-inline-start:25px}.dropdown-menu li{border-radius:var(--border-radius)}.dropdown-menu a,.dropdown-menu a:active,.dropdown-menu a:hover{border-radius:var(--border-radius);color:var(--dropdown-link-color)}.dropdown-menu a:hover{background:var(--dropdown-link-hover-bg)}.dropdown-menu .icon,.dropdown-menu i{color:var(--dropdown-icon-color);font-size:15px;margin:0 8px 0 0}.dropdown-menu .icon i{margin:0}.dropdown-menu .icon{display:inline-flex;justify-content:center}.dropdown-menu .dropdown-header,.dropdown-menu .dropdown-item{align-items:center;block-size:28px;display:flex;overflow:hidden;padding:0 12px 0 6px;text-overflow:ellipsis;white-space:nowrap}.dropdown-menu .dropdown-divider{background:transparent;block-size:1px;border:0;border-radius:0;box-shadow:0 -1px 0 var(--dropdown-border-color);margin:6px calc(var(--dropdown-padding)*-1);opacity:1}.dropdown-menu .dropdown-item-color-scheme{color:var(--dropdown-color)}.dropdown-menu .dropdown-item-color-scheme:hover{background:transparent}.dropdown-menu .dropdown-item-color-scheme label{align-items:center;display:flex}.dropdown-menu .dropdown-item-color-scheme i{margin-block-start:0}.dropdown-menu .dropdown-item-color-scheme select{background:var(--dropdown-bg);border:1px solid var(--dropdown-border-color);border-radius:var(--border-radius);color:var(--dropdown-color);margin-inline-start:10px;outline:none;padding:0 4px}.dropdown-menu .dropdown-submenu .dropdown-item.dropdown-toggle{border:0;display:flex;padding:0 12px 0 6px;position:relative}.dropdown-menu .dropdown-submenu .dropdown-item.dropdown-toggle:not(.dropdown-toggle-split):hover{cursor:default}.list-pagination{background:var(--table-footer-bg);border-end-end-radius:var(--border-radius);border-end-start-radius:var(--border-radius);color:var(--pagination-color);padding:15px 0}@media (min-width:768px){.list-pagination{align-items:center;display:flex;flex-direction:row;justify-content:space-between}}.list-pagination-counter{color:var(--pagination-color)}.pager .pagination{--bs-pagination-font-size:var(--font-size-sm);--bs-pagination-focus-box-shadow:none;margin-block-end:0}@media (max-width:992px){.pager .pagination{margin-block-start:15px}}.page-item .page-link{white-space:nowrap}.page-item.active .page-link,.page-item.active .page-link:hover{background:var(--pagination-active-bg);border-color:var(--pagination-active-bg);color:var(--pagination-active-color)}.page-item.disabled .page-link{background:transparent;color:var(--pagination-disabled-color)}.page-item .page-link,.page-item .page-link:focus,.page-item .page-link:hover{background:transparent;border:var(--border-width) var(--border-style) transparent;border-radius:var(--border-radius);color:inherit;margin:0 1px}.page-item:not(:first-child) .page-link{margin:0 1px}.page-item .page-link:focus,.page-item .page-link:hover{border-color:var(--pagination-hover-border-color)}@media (max-width:768px){.pager .page-item:not(.page-item-previous,.page-item-next,.page-item.active){display:none}.pager .page-item.active{margin:0 1em}.pager .page-item-next,.pager .page-item-previous{flex:1}.pager .page-item-next .page-link,.pager .page-item-previous .page-link{border:var(--border-width) var(--border-style) var(--border-secondary-color);border-radius:var(--border-radius)}.pager .page-item-next:not(.disabled):hover .page-link,.pager .page-item-previous:not(.disabled):hover .page-link{border-color:var(--link-color)}.pager .page-item-previous .page-link{padding-inline-start:calc(var(--bs-pagination-padding-x)/2)}.pager .page-item-next .page-link{padding-inline-end:calc(var(--bs-pagination-padding-x)/2);text-align:right}}.modal-content{border-color:var(--modal-border-color)}.modal-body{background:var(--modal-bg)}.modal-body h4{font-size:var(--font-size-lg)}.modal-footer{background:var(--modal-footer-bg);border-color:var(--modal-border-color);padding:8px 10px}#flash-messages{background:transparent}.alert{border-width:0 0 var(--border-width);margin-block-end:0}.alert:last-of-type{border-block-end-width:2px}.alert-dismissible .btn-close{--bs-btn-close-opacity:1;--bs-btn-close-hover-opacity:1;inset-block-start:10px;inset-inline-end:5px;padding:var(--button-padding-y-md) var(--button-padding-x-md)}[data-bs-theme=dark] .btn-close{filter:none}.alert.alert-primary{--bs-alert-bg:var(--alert-primary-bg);--bs-alert-border-color:var(--alert-primary-border-color);--bs-alert-color:var(--alert-primary-color)}.alert.alert-secondary{--bs-alert-bg:var(--alert-secondary-bg);--bs-alert-border-color:var(--alert-secondary-border-color);--bs-alert-color:var(--alert-secondary-color)}.alert.alert-success{--bs-alert-bg:var(--alert-success-bg);--bs-alert-border-color:var(--alert-success-border-color);--bs-alert-color:var(--alert-success-color)}.alert.alert-info{--bs-alert-bg:var(--alert-info-bg);--bs-alert-border-color:var(--alert-info-border-color);--bs-alert-color:var(--alert-info-color)}.alert.alert-warning{--bs-alert-bg:var(--alert-warning-bg);--bs-alert-border-color:var(--alert-warning-border-color);--bs-alert-color:var(--alert-warning-color)}.alert.alert-danger{--bs-alert-bg:var(--alert-danger-bg);--bs-alert-border-color:var(--alert-danger-border-color);--bs-alert-color:var(--alert-danger-color)}.alert.alert-light{--bs-alert-bg:var(--alert-light-bg);--bs-alert-border-color:var(--alert-light-border-color);--bs-alert-color:var(--alert-light-color)}.alert.alert-dark{--bs-alert-bg:var(--alert-dark-bg);--bs-alert-border-color:var(--alert-dark-border-color);--bs-alert-color:var(--alert-dark-color)}.text-primary{color:var(--text-primary-color)!important}.text-secondary{color:var(--text-secondary-color)!important}.text-tertiary{color:var(--text-tertiary-color)!important}.border-primary{border:1px solid var(--border-primary-color)!important}.border-secondary{border:1px solid var(--border-secondary-color)!important}.border-tertiary{border:1px solid var(--border-tertiary-color)!important}.background-primary{background-color:var(--primary-bg)!important}.background-secondary{background-color:var(--secondary-bg)!important}.background-tertiary{background-color:var(--tertiary-bg)!important}#main-menu{padding:0 0 20px}#main-menu .menu{padding-inline-start:0}#main-menu .menu li{list-style:none}#main-menu .menu .menu-header{color:var(--sidebar-menu-header-color);font-size:12px;font-weight:500;line-height:15px;margin-block-start:15px;padding:7px 5px 7px var(--sidebar-menu-items-padding-left);text-transform:uppercase}#main-menu .menu .menu-header:first-child{margin-block-start:0}#main-menu .menu .menu-header .menu-icon{color:inherit;margin:0 8px 0 0}#main-menu .menu .menu-header .menu-header-contents{display:block}#main-menu .menu .menu-header .menu-item-badge{float:right;inset-block-start:0;margin-inline-start:16px}#main-menu .menu .menu-item{border-radius:var(--border-radius);padding-inline-end:5px;padding-inline-start:var(--sidebar-menu-items-padding-left);position:relative}#main-menu .menu .menu-item.active{background:var(--sidebar-menu-active-item-bg)}#main-menu .menu .menu-item.active .menu-item-label{font-weight:500}.ea-light-scheme #main-menu .menu .menu-item .menu-item-badge{box-shadow:inset 0 0 0 1px rgba(0,0,0,.1)}.ea-light-scheme #main-menu .menu .menu-item.active .menu-item-badge{box-shadow:inset 0 0 0 1px rgba(0,0,0,.2)}.ea-dark-scheme #main-menu .menu .menu-item.active .menu-item-badge{background:var(--sidebar-bg);box-shadow:inset 0 0 0 1px transparent}#main-menu .menu .menu-item.active:not(.expanded) .menu-icon,#main-menu .menu .menu-item.active:not(.expanded) a{color:var(--sidebar-menu-active-item-color)}#main-menu .menu .menu-item.has-submenu.expanded .submenu-toggle-icon{transform:rotate(90deg)}#main-menu .menu .menu-item.has-submenu:not(.expanded) .submenu{max-block-size:0}#main-menu .menu .menu-item .submenu-toggle .submenu-toggle-icon{color:var(--sidebar-menu-icon-color);inline-size:auto;transition:transform .25s ease}#main-menu .menu .menu-item-contents{align-items:flex-start;color:var(--sidebar-menu-color);display:flex;padding:4px 0}#main-menu .menu .menu-icon{block-size:16px;color:var(--sidebar-menu-icon-color);flex-shrink:0;inline-size:1.25em;margin-inline-end:10px;text-align:center}#main-menu .menu .menu-icon svg{color:var(--sidebar-menu-icon-color);max-block-size:16px;max-inline-size:20px;vertical-align:sub}#main-menu .menu .menu-item-badge{float:right;inset-block-start:2px;margin:0 0 0 8px;min-inline-size:25px;position:relative}#main-menu .menu .menu-item-badge.badge-secondary{background:var(--sidebar-menu-badge-bg);color:var(--sidebar-menu-badge-color)}#main-menu .menu .submenu-toggle-icon{float:right;margin-inline-start:8px}#main-menu .menu .submenu{overflow:hidden;padding:0;transition:max-block-size .15s linear}#main-menu .menu .submenu a{color:var(--sidebar-menu-submenu-color);padding:3px 0 3px 26px}#main-menu .menu .submenu .menu-header{padding-inline-start:26px}#main-menu .menu .submenu .menu-item{margin:5px 0;padding-inline-end:0}#main-menu .menu .submenu .menu-item.active{margin-inline-start:0;padding-inline-start:6px}#main-menu .menu .submenu .menu-icon{font-size:var(--font-size-base);margin-inline-end:5px}#main-menu .menu .submenu .menu-item-badge{margin-inline-end:4px}body.ea-sidebar-width-compact .sidebar{overflow:visible;padding:0}body.ea-sidebar-width-compact .sidebar #main-menu .menu .menu-item,body.ea-sidebar-width-compact .sidebar .main-header .navbar{padding-inline-start:var(--sidebar-padding-left)}@media (min-width:992px){body.ea-sidebar-width-compact #main-menu .menu .menu-item{border-radius:0 var(--border-radius) var(--border-radius) 0;padding-inline-end:0}body.ea-sidebar-width-compact #main-menu .menu .menu-item .menu-item-badge,body.ea-sidebar-width-compact #main-menu .menu .menu-item .menu-item-label,body.ea-sidebar-width-compact #main-menu .menu .menu-item .submenu,body.ea-sidebar-width-compact #main-menu .menu .menu-item .submenu-toggle-icon{display:none}body.ea-sidebar-width-compact #main-menu .menu .menu-item .menu-item-label{flex:1}body.ea-sidebar-width-compact #main-menu .menu .menu-item .menu-item-contents{align-items:center;border-radius:0 var(--border-radius) var(--border-radius) 0;display:flex;min-inline-size:max-content;padding:7px 5px 7px 0}body.ea-sidebar-width-compact #main-menu .menu .menu-item:hover{background:var(--body-bg);box-shadow:var(--sidebar-menu-compact-hover-box-shadow);min-inline-size:max-content;padding-inline-start:var(--sidebar-padding-left);z-index:var(--zindex-modal-backdrop)}body.ea-sidebar-width-compact #main-menu .menu .menu-item:hover i{color:var(--sidebar-menu-icon-color)!important}body.ea-sidebar-width-compact #main-menu .menu .menu-item:hover .menu-item-badge,body.ea-sidebar-width-compact #main-menu .menu .menu-item:hover .menu-item-label,body.ea-sidebar-width-compact #main-menu .menu .menu-item:hover .submenu,body.ea-sidebar-width-compact #main-menu .menu .menu-item:hover .submenu-toggle-icon{display:block}body.ea-sidebar-width-compact #main-menu .menu .menu-item:hover .menu-item-contents{background:var(--body-bg);color:var(--text-color)}body.ea-sidebar-width-compact #main-menu .menu .menu-item:hover .submenu{background:var(--body-bg);border-radius:0 var(--border-radius) var(--border-radius) var(--border-radius);inline-size:max-content;inset-block-start:0;margin-inline-start:34px;padding:2px 10px 0 0;position:absolute}body.ea-sidebar-width-compact #main-menu .menu .menu-item:hover .submenu a{padding:3px 5px 3px 13px}body.ea-sidebar-width-compact #main-menu .menu .menu-item.has-submenu:hover .submenu-toggle .menu-item-label{display:none}body.ea-sidebar-width-compact #main-menu .menu .menu-item.has-submenu:hover .submenu-toggle-icon{display:inline-block;font-size:18px;inset-block-start:0;inset-inline-start:-7px;transform:rotate(0);z-index:9999}body.ea-sidebar-width-compact #main-menu .menu .menu-item.has-submenu:hover .submenu .menu-icon{margin-inline-end:8px}body.ea-sidebar-width-compact #main-menu .menu .menu-item .menu-icon{block-size:21px;font-size:18px;line-height:normal;max-inline-size:21px}body.ea-sidebar-width-compact #main-menu .menu .menu-item .submenu .menu-icon{font-size:16px;inline-size:21px;inset-inline-start:-4px;position:relative}body.ea-sidebar-width-compact #main-menu .menu .menu-item .submenu{box-shadow:var(--sidebar-menu-compact-hover-box-shadow);max-block-size:none!important;padding-block-end:5px;padding-block-start:5px}body.ea-sidebar-width-compact #main-menu .menu .menu-item .submenu .menu-item:hover{box-shadow:none}body.ea-sidebar-width-compact #main-menu .menu .menu-header{block-size:0;inline-size:0;overflow:hidden;padding:0}}table.datagrid{border-collapse:collapse;border-spacing:0;color:var(--table-cell-color);inline-size:100%;margin-block-end:0}table.datagrid:not(.datagrid-empty) tr:not(.empty-row) td.actions,table.datagrid:not(.datagrid-empty) tr:not(.empty-row) td.actions.actions-as-dropdown{min-inline-size:50px}@media (max-width:767px){table.datagrid:not(.datagrid-empty) tbody,table.datagrid:not(.datagrid-empty) td,table.datagrid:not(.datagrid-empty) tr{display:block}table.datagrid:not(.datagrid-empty) tbody,table.datagrid:not(.datagrid-empty) tr{border-radius:var(--border-radius)}table.datagrid:not(.datagrid-empty) tbody tr td:first-of-type{border-start-end-radius:var(--border-radius);border-start-start-radius:var(--border-radius)}table.datagrid:not(.datagrid-empty) tbody tr td:last-of-type{border-end-end-radius:var(--border-radius);border-end-start-radius:var(--border-radius)}table.datagrid:not(.datagrid-empty) thead{display:none}table.datagrid:not(.datagrid-empty) tr{border:1px solid var(--responsive-table-row-border-color);margin-block-end:30px}table.datagrid:not(.datagrid-empty) tr:not(.empty-row) td{box-shadow:inset 0 1px 0 var(--table-cell-border-color);min-block-size:36px;padding-inline-start:35%;position:relative}table.datagrid:not(.datagrid-empty) tr:not(.empty-row) td:first-child{box-shadow:none}table.datagrid:not(.datagrid-empty) tr:not(.empty-row) td.batch-actions-selector{padding:8px}table.datagrid:not(.datagrid-empty) tr:not(.empty-row) td.batch-actions-selector:before{display:none}table.datagrid:not(.datagrid-empty) tr:not(.empty-row) td.actions,table.datagrid:not(.datagrid-empty) tr:not(.empty-row) td.actions.actions-as-dropdown{padding:8px}table.datagrid:not(.datagrid-empty) tr:not(.empty-row) td.actions.actions-as-dropdown:before{display:none}table.datagrid:not(.datagrid-empty) td{text-align:left!important}table.datagrid:not(.datagrid-empty) td:before{color:var(--responsive-table-label-color);content:attr(data-label);font-weight:500;inline-size:35%;inset-block-end:0;inset-block-start:0;inset-inline-start:0;overflow:hidden;padding:8px;position:absolute;text-align:left;text-overflow:ellipsis;white-space:nowrap}table.datagrid:not(.datagrid-empty) td.field-boolean{padding-inline-start:8px}table.datagrid:not(.datagrid-empty) td.field-boolean:before{color:var(--table-cell-color);font-weight:400;inset-inline-start:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}table.datagrid:not(.datagrid-empty) td.actions:before{display:none}}.datagrid thead th{border:0;box-shadow:inset 0 -2px 0 var(--table-cell-border-color);padding:0}.datagrid thead a,.datagrid thead span:not(.icon){color:var(--table-thead-color);display:block;font-weight:500;line-height:1.357;padding:12px 8px;white-space:nowrap}.datagrid td{box-shadow:inset 0 1px 0 var(--table-cell-border-color);line-height:20px;padding:8px}.datagrid tbody{box-shadow:0 1px 0 var(--table-cell-border-color)}@media (min-width:992px){.datagrid thead+tbody tr:first-child td{box-shadow:none}}.datagrid td.field-avatar{padding:4px 8px}.datagrid thead .sorted a,.datagrid thead .sorted span{font-weight:700}.datagrid thead .icon,.datagrid thead i{color:var(--table-thead-marker-color);margin-inline-start:2px}.datagrid thead .sorted{box-shadow:inset 0 -2px 0 var(--color-primary)}.datagrid thead .sorted a,.datagrid thead .sorted span{color:var(--table-thead-sorted-color)}.datagrid thead .sorted .icon,.datagrid thead .sorted i{color:var(--table-thead-sorted-marker-color);display:inline-block}.datagrid td,.datagrid th{border:none;vertical-align:middle}@media (min-width:992px){.datagrid tbody tr:hover td,.datagrid tbody tr:hover th{background:var(--table-hover-cell-bg)}}.datagrid tbody tr.selected-row td{background:var(--table-selected-cell-bg)}.datagrid tbody tr.selected-row td ::-moz-selection{background:transparent}.datagrid tr.ea-clickable-row{cursor:pointer}.datagrid tr.ea-clickable-row td.actions,.datagrid tr.ea-clickable-row td.batch-actions-selector{cursor:default}.datagrid tr.ea-clickable-row td.actions a,.datagrid tr.ea-clickable-row td.actions button,.datagrid tr.ea-clickable-row td.batch-actions-selector .form-check{cursor:pointer}.datagrid td.actions{text-align:right}.datagrid td.actions:not(.actions-as-dropdown) form{display:inline;margin-inline-end:10px;margin-inline-start:10px}.datagrid td.actions a:not(.dropdown-item){font-size:var(--font-size-sm);font-weight:500}.datagrid td.actions a:not(.dropdown-item)+a:not(.dropdown-item){margin-inline-start:10px}.datagrid td.actions a:not(.dropdown-item) .action-icon{font-size:var(--font-size-base);margin-inline-end:2px}.datagrid td.actions .dropdown-item-variant-success:hover,.page-actions .dropdown-item-variant-success:hover{--dropdown-icon-color:var(--dropdown-item-success-color);background:var(--dropdown-item-success-bg);color:var(--dropdown-item-success-color)}.datagrid td.actions .dropdown-item-variant-warning:hover,.page-actions .dropdown-item-variant-warning:hover{--dropdown-icon-color:var(--dropdown-item-warning-color);background:var(--dropdown-item-warning-bg);color:var(--dropdown-item-warning-color)}.datagrid td.actions .dropdown-item-variant-danger:hover,.page-actions .dropdown-item-variant-danger:hover{--dropdown-icon-color:var(--dropdown-item-danger-color);background:var(--dropdown-item-danger-bg);color:var(--dropdown-item-danger-color)}@media (min-width:992px){.datagrid td.actions-as-dropdown{padding:2px 8px}}.datagrid td.actions-as-dropdown-table-head{inline-size:10px}.datagrid tr:not(.selected-row):hover .actions-as-dropdown .dropdown-actions>.dropdown-toggle{background:var(--dropdown-toggle-bg);border-color:var(--dropdown-toggle-border-color)}.datagrid tr:hover .actions-as-dropdown .dropdown-actions>.dropdown-toggle:hover{border-color:var(--dropdown-toggle-hover-border-color)}.datagrid .dropdown-toggle.show,.datagrid .dropdown-toggle:active,.datagrid .dropdown-toggle:active:focus,.datagrid .dropdown-toggle:focus,.datagrid tr .dropdown-toggle.show,.datagrid tr:hover .dropdown-toggle.show,.datagrid tr:hover .dropdown-toggle:active,.datagrid tr:hover .dropdown-toggle:active:focus,.datagrid tr:hover .dropdown-toggle:focus{border-color:var(--dropdown-toggle-hover-border-color);box-shadow:var(--button-active-shadow);outline:none}.datagrid .dropdown-actions{display:inline-block}.datagrid .dropdown-actions .dropdown-toggle{border:1px solid transparent;border-radius:var(--border-radius);color:var(--dropdown-toggle-color);display:block;overflow:visible;padding:1px 5px}.datagrid .dropdown-actions .dropdown-toggle .icon{display:block;font-size:21px;inline-size:unset}.datagrid .dropdown-actions .dropdown-menu{z-index:var(--zindex-900)}.datagrid .dropdown-actions .dropdown-menu .dropstart{position:relative}.datagrid .dropdown-actions .dropstart .dropdown-toggle:before{margin-inline-start:-20px;position:absolute}.datagrid .dropdown-actions .dropdown-menu .dropstart>.dropdown-menu{inset-block-start:0;inset-inline-end:100%;inset-inline-start:auto;margin-block-end:0;margin-block-start:0;margin-inline-end:-.125rem;margin-inline-start:0}.datagrid .dropdown-actions .dropdown-menu .dropstart:has(.dropdown-toggle-split)>.dropdown-menu{margin-inline-end:1.125rem}.datagrid .dropdown-actions .dropdown-menu .dropstart:has(.dropdown-toggle-split)>.dropdown-menu:hover,.datagrid .dropdown-actions .dropdown-menu .dropstart:has(.dropdown-toggle-split:hover)>.dropdown-menu,.datagrid .dropdown-actions .dropdown-menu .dropstart:not(:has(.dropdown-toggle-split)):hover>.dropdown-menu,.datagrid .dropdown-actions .dropdown-menu .dropstart:not(:has(.dropdown-toggle-split))>.dropdown-menu:hover,.datagrid .dropdown-actions .dropdown-menu .dropstart:not(:has(.dropdown-toggle-split))>.dropdown-toggle:focus .dropdown-menu{display:block}.datagrid .dropdown-actions .dropdown-menu .dropstart .dropdown-toggle-split{inset-inline-start:-22px;padding-inline-end:.5rem;padding-inline-start:.5rem;position:absolute}.datagrid .dropdown-actions .dropdown-menu .dropstart .dropdown-toggle-split:before{display:none}.datagrid .dropdown-actions .dropdown-menu .dropstart .dropdown-toggle-split .dropdown-toggle-marker{border-block-end:.3em solid transparent;border-block-start:.3em solid transparent;border-inline-end:.3em solid;content:"";display:inline-block}.datagrid .ea-lightbox-thumbnail img{background:var(--white);border:1px solid transparent;border-radius:var(--border-radius);max-block-size:50px;max-inline-size:100px;padding:2px 4px}.datagrid tr:hover .ea-lightbox-thumbnail img{border-color:var(--border-color)}.datagrid mark{background:var(--highlight-bg);border-radius:0;color:var(--highlight-color);padding:0}.datagrid .field-boolean,.datagrid .header-for-field-boolean{text-align:center}.datagrid .field-boolean.has-switch{padding:6px 8px}.datagrid .field-boolean .form-switch{display:inline-flex;justify-content:center;margin-block-end:0;padding-inline-start:0}.datagrid .field-boolean .form-switch input{inset-block-start:3px;margin-block-start:0;position:relative}@media (max-width:992px){.datagrid .field-country{text-align:left!important}}.datagrid .form-check{margin-block-end:0;min-block-size:15px;padding-inline-start:0}.datagrid .no-results td{font-size:var(--font-size-lg);padding:24px 0;text-align:center}.datagrid .empty-row:hover td,.datagrid .no-results:hover td{background:transparent}.datagrid .empty-row td{padding:0 10px}.datagrid .empty-row td:first-child{inline-size:20%}.datagrid .empty-row td:nth-child(2){display:none}@media (min-width:992px){.datagrid .empty-row td:nth-child(2){inline-size:5%}}.datagrid .empty-row td:nth-child(3){inline-size:10%}.datagrid .empty-row td:nth-child(4){inline-size:25%}.datagrid .empty-row td:nth-child(5){inline-size:10%}.datagrid .empty-row td:nth-child(6){inline-size:30%}.datagrid .empty-row td span{background:var(--datagrid-noresults-placeholder-bg);block-size:10px;border-radius:var(--border-radius);display:block;inline-size:100%;margin:13px 0}.datagrid tbody .datagrid-row-empty:hover td,.datagrid-row-empty td{background-color:transparent;background-image:linear-gradient(135deg,var(--datagrid-hidden-results-gradient-bg) 25%,transparent 25%,transparent 50%,var(--datagrid-hidden-results-gradient-bg) 50%,var(--datagrid-hidden-results-gradient-bg) 75%,transparent 75%,transparent 100%);background-size:40px 40px;padding-block-end:15px;padding-block-start:15px}.datagrid-row-empty-message{background:var(--body-bg);border-radius:var(--border-radius);padding:2px 4px}.datagrid-header-tools{display:flex;padding:0 0 10px}.datagrid-header-tools .datagrid-search{flex:1;margin-inline-end:15px;max-inline-size:480px}.datagrid-header-tools .datagrid-search .form-group,.datagrid-header-tools .datagrid-search .form-group .form-widget{flex:1;margin:0;padding:0}.datagrid-header-tools .datagrid-search input[type=search].form-control{background-color:var(--white);background-image:url('data:image/svg+xml;utf8,');background-position:10px 8px;background-repeat:no-repeat;background-size:13px 13px;min-inline-size:100%;padding:0 32px}.datagrid-header-tools .datagrid-search .form-widget{position:relative}.datagrid-header-tools .datagrid-search a.action-search-reset{color:var(--gray-500);inset-block-start:1px;inset-inline-end:1px;padding:4px 7px;position:absolute;text-decoration:none}.datagrid-header-tools .datagrid-search a.action-search-reset:hover{color:var(--gray-700)}#modal-filters .modal-dialog{max-inline-size:400px}#modal-filters .modal-content{background:var(--modal-bg);border:1px solid var(--modal-border-color);border-radius:var(--border-radius)}#modal-filters .modal-header{background:var(--modal-header-bg);border-block-end-color:transparent;padding:10px 15px}#modal-filters .modal-title{color:var(--modal-title-color);font-size:var(--font-size-base)}#modal-filters .modal-body{background:var(--modal-bg);border-block-end:0;border-radius:var(--border-radius);padding:15px}.action-filters-button .icon{color:var(--text-color-light)}.action-filters-button.action-filters-applied i{color:var(--color-primary)}.action-filters-button .action-filters-button-count{color:var(--color-primary);font-weight:600}.action-filters-reset i{color:var(--text-color-light)}.filter-field{border-block-start:1px solid var(--modal-border-color)}.filter-heading{align-items:center;display:flex;padding:10px 0}.filter-heading a{color:var(--link-color);cursor:pointer;flex:1;margin-inline-start:7px}.filter-content{margin:-5px 0 0 15px;padding:0 0 10px}.filter-content .form-group,.filter-content .form-widget-compound .form-group{display:block;padding:4px 0}.filter-content .form-widget-compound label{display:none}.filter-content .form-widget-compound label.form-check-label{display:inline-block}.filter-content .form-check-inline{align-items:flex-start;display:inline-flex}.filter-content .form-check.form-check-inline{margin-block-start:0}.filter-content .form-group label.required:after{content:none}.filter-content .field-choice .form-check+.form-check{margin-block-start:4px}.filter-content .field-choice .form-check-label{margin-block-start:0}.table.datagrid>:not(:first-child){border-block-start-style:none}.ea-detail .form-column .form-fieldset-body{padding-block-end:7px;padding-block-start:5px}.ea-detail .form-column .form-fieldset-body.without-header{padding-block-end:10px;padding-block-start:var(--bs-gutter-x)}.ea-detail .field-group{display:flex;margin-block-end:12px}.ea-detail .field-group .field-label{color:var(--form-label-color);font-size:var(--font-size-base);font-weight:500;inline-size:130px;margin:0 15px 0 0;padding:0 0 1px;text-align:right}.ea-detail .field-group .field-label:empty{display:none}.ea-detail .field-group .field-label div[data-bs-toggle=tooltip]{cursor:pointer;text-decoration:underline;text-decoration-color:var(--detail-label-tooltip-underline-color);text-decoration-style:dotted;text-underline-offset:2px}.tooltip.ea-detail-label-tooltip{--bs-tooltip-max-width:350px;--bs-tooltip-border-radius:var(--border-radius);--bs-tooltip-padding-x:20px;--bs-tooltip-padding-y:10px;--bs-tooltip-opacity:1}.tooltip.ea-detail-label-tooltip .tooltip-inner{font-size:13px;text-align:start}.ea-detail .field-group .field-value{flex:1;min-inline-size:66%}.ea-detail .field-group.field-text_editor .field-value,.ea-detail .field-group.field-textarea .field-value{max-block-size:350px;max-inline-size:80ch;overflow-block:auto}.ea-detail .field-group.field-boolean{flex-direction:row-reverse}.ea-detail .field-group.field-boolean .field-label{flex:1;margin:0 0 0 15px;min-inline-size:66%;text-align:left}.ea-detail .field-group.field-boolean .field-value{flex:unset;inline-size:130px;min-inline-size:0;text-align:right}.field-array ul{margin-block-end:0;padding-inline-start:1.2em}.field-array li+li{margin-block-start:4px}.field-avatar .image-avatar{border:0;border-radius:var(--border-radius);box-shadow:none}.field-boolean .badge{min-inline-size:33px;text-transform:uppercase}.field-boolean .badge-boolean-false{background:var(--badge-boolean-false-bg);border:0;box-shadow:var(--badge-boolean-false-box-shadow);color:var(--badge-boolean-false-color)}.field-boolean .badge-boolean-true{background:var(--badge-boolean-true-bg);border:0;box-shadow:var(--badge-boolean-true-box-shadow);color:var(--badge-boolean-true-color)}.field-code_editor .form-widget{flex:1}.field-code_editor dt{max-block-size:480px;overflow-block:auto}.form-widget-compound .collection-empty{margin-block-end:10px;padding-block-start:5px}.form-group.field-collection label:empty{display:none}.form-group.field-array .form-widget .form-group{padding:6px 0}.form-group.field-array .form-widget .form-group label{display:none}.form-group.field-array .field-collection-item+.field-collection-item{margin-block-start:5px}.form-group.field-array .field-collection-item{display:flex}.form-group.field-collection .accordion{border-radius:var(--border-radius);box-shadow:inset 0 0 0 1px var(--form-input-border-color)}.form-group.field-collection .accordion .form-group{padding:0}.form-group.field-collection .accordion-header{padding-inline-end:28px;position:relative}.form-group.field-collection .accordion-header:hover{background:var(--form-type-collection-item-collapsed-hover-bg);box-shadow:inset 0 0 0 1px var(--form-input-border-color)}.form-group.field-collection .accordion-header .accordion-button{font-size:var(--font-size-base)}.form-group.field-collection .accordion-item{background:transparent;border:0;border-radius:0;box-shadow:inset 0 -1px 0 var(--form-input-border-color)}.form-group.field-collection .field-collection-item-first .accordion-header,.form-group.field-collection .field-collection-item-first .accordion-item{border-start-end-radius:var(--border-radius);border-start-start-radius:var(--border-radius)}.form-group.field-collection .field-collection-item-last .accordion-header,.form-group.field-collection .field-collection-item-last .accordion-item{border-end-end-radius:var(--border-radius);border-end-start-radius:var(--border-radius)}.form-group.field-collection .field-collection-item.field-collection-item-last .accordion-item{box-shadow:none}.form-group.field-collection .accordion-item .form-group{align-items:flex-start;display:flex;padding:12px 0}.form-group.field-collection .accordion-item .form-group legend.col-form-label,.form-group.field-collection .accordion-item .form-group>label{font-weight:500;inline-size:20%;margin:3px 10px 0 0;padding:0}.form-group.field-collection .accordion-item .accordion-body .form-widget{flex:1}.form-group.field-collection .accordion-button,.form-group.field-collection .accordion-button:hover{background:transparent;border-radius:0;box-shadow:none;color:var(--text-color);flex:1;padding:8px 7px}.form-group.field-collection .accordion-button:after{display:none}.form-group.field-collection .accordion-button i{transition:transform .2s ease-in-out}.form-group.field-collection .accordion-button:not(.collapsed) i{transform:rotate(90deg)}.form-group.field-collection .accordion-button .form-collection-item-collapse-marker{color:var(--form-collection-item-collapse-marker-color);margin:0 8px 0 4px}.form-group.field-collection .field-collection-add-button{margin-block-start:5px}.form-group.field-collection .field-collection-delete-button{inset-block-start:1px;inset-inline-end:5px;position:absolute}.field-color .color-sample{block-size:19px;border-radius:var(--border-radius);box-shadow:0 0 0 2px var(--border-tertiary-color),0 0 0 3px var(--border-secondary-color);display:inline-block;inline-size:45px}.field-country .country-flag{border-radius:2px;margin:0 6px 1px 0;max-block-size:17px;outline:1px solid rgba(0,0,0,.2);outline-offset:-1px;vertical-align:text-top}.ea-dark-scheme .field-country .country-flag{outline-color:var(--border-secondary-color);outline-offset:0}.datagrid .field-country>span+span,.datalist .field-country dd>span+span{margin-inline-start:10px}.field-country .ts-control .country-name-flag,.field-country .ts-dropdown-content .country-name-flag .country-flag{margin-block-end:0}.field-country .ts-wrapper.multi .ts-control>div{margin-block-end:5px}.field-country .ts-wrapper.multi .ts-control .country-name-flag{margin-inline-end:25px}.field-country .ts-wrapper.multi.plugin-remove_button .item .remove{border-color:var(--form-type-autocomplete-multi-item-border-color)}.field-currency .badge-currency{border:2px solid var(--gray-300);display:inline-block;font-size:12px;padding:2px 4px;text-transform:uppercase}.field-date input[type=date].form-control,.field-datetime input[type=datetime-local].form-control,.field-time input[type=time].form-control{inline-size:auto;max-inline-size:100%}.field-language .badge-language{border:2px solid var(--field-language-badge-border-color);box-shadow:none;display:inline-block;font-size:12px;padding:2px 4px;text-transform:uppercase}.field-text_editor dt{max-block-size:480px;overflow-block:auto}.detail .field-image .form-control{background:transparent;block-size:auto;border:0;padding:0}.ea-detail .field-image .ea-lightbox-thumbnail{display:block;max-inline-size:400px}.ea-detail .field-image img{border:1px solid transparent;border-radius:var(--border-radius);max-block-size:300px;padding:8px}.ea-detail .field-image img:hover{border-color:var(--datalist-border-color)}.ea-lightbox-thumbnail img:hover{cursor:zoom-in}.ea-lightbox{display:none}.ea-lightbox img{inline-size:100%;max-inline-size:100%}.basicLightbox{align-items:center;block-size:100vh;display:flex;inline-size:100%;inset-block-start:0;inset-inline-start:0;justify-content:center;opacity:.01;position:fixed;transition:opacity .4s ease;will-change:opacity;z-index:1000}.basicLightbox--visible{opacity:1}.basicLightbox__placeholder{max-inline-size:100%;transform:scale(.9);transition:transform .4s ease;will-change:transform;z-index:1}.basicLightbox__placeholder>iframe:first-child:last-child,.basicLightbox__placeholder>img:first-child:last-child,.basicLightbox__placeholder>video:first-child:last-child{display:block;inset-block-end:0;inset-block-start:0;inset-inline-end:0;inset-inline-start:0;margin:auto;max-block-size:95%;max-inline-size:95%;position:absolute}.basicLightbox__placeholder>iframe:first-child:last-child,.basicLightbox__placeholder>video:first-child:last-child{pointer-events:auto}.basicLightbox__placeholder>img:first-child:last-child,.basicLightbox__placeholder>video:first-child:last-child{block-size:auto;inline-size:auto}.basicLightbox--iframe .basicLightbox__placeholder,.basicLightbox--img .basicLightbox__placeholder,.basicLightbox--video .basicLightbox__placeholder{block-size:100%;inline-size:100%;pointer-events:none}.basicLightbox--visible .basicLightbox__placeholder{transform:scale(1)}.basicLightbox{background:rgba(0,0,0,.8);transition:opacity .3s ease;z-index:10000}.basicLightbox__placeholder{margin-inline-end:5%;margin-inline-start:5%;max-block-size:95%;transition:opacity .3s ease}.basicLightbox__placeholder img{background:#fff;padding:25px}.basicLightbox__placeholder img:hover{cursor:zoom-out}input[disabled]{cursor:not-allowed}.form-inline{align-items:center;display:flex;flex-flow:row wrap}.form-group{padding:0 0 24px}.form-group label,.form-group legend.col-form-label{color:var(--form-label-color);font-size:var(--font-size-base);font-weight:500;margin:0;padding:0 0 8px}.form-check .form-check-input{block-size:15px;border-color:var(--form-type-check-input-border-color);inline-size:15px}.form-check:not(.form-switch) .form-check-input:not(:checked){background-color:unset}label.form-check-label{cursor:pointer;font-weight:400}.form-group label.form-check-label.required:after{display:none}.form-widget .form-check+.form-check{margin-block-start:5px}.form-group .col-form-label.required:after,.form-group label.required:after{background:var(--color-danger);block-size:4px;border-radius:50%;content:"";display:inline-block;filter:opacity(75%);inline-size:4px;inset-block-start:-8px;inset-inline-end:-2px;position:relative;z-index:var(--zindex-700)}.form-widget .form-help{color:var(--form-help-color);display:block;font-size:var(--font-size-sm);margin-block-start:5px;transition:color .5s ease}.form-widget:focus-within .form-help{color:var(--form-help-active-color)}.form-widget .form-select,.form-widget input.form-control,.form-widget textarea.form-control{background-color:var(--form-control-bg);background-repeat:no-repeat;block-size:30px;border:1px solid var(--form-input-border-color);box-shadow:var(--form-input-shadow);color:var(--form-input-text-color);font-size:.875rem;padding:3px 7px 4px;transition:box-shadow .08s ease-in,color .08s ease-in;white-space:nowrap;word-break:keep-all}.field-collection-item.field-collection-item-complex.is-invalid,.field-collection-item.field-collection-item-complex.is-invalid:focus,.form-widget .form-select.is-invalid,.form-widget .form-select.is-invalid:focus,.form-widget input.form-control.is-invalid,.form-widget input.form-control.is-invalid:focus,.form-widget textarea.form-control.is-invalid,.form-widget textarea.form-control.is-invalid:focus{background-image:none;border:1px solid var(--form-input-error-border-color);box-shadow:var(--form-input-error-shadow)}.form-widget input.form-check-input.is-invalid{border:1px solid var(--form-input-error-border-color);box-shadow:var(--form-input-error-shadow)}.form-widget .form-control:disabled,.form-widget .form-control[readonly],.form-widget .form-select:disabled,.form-widget .form-select[readonly]{background-color:var(--form-control-disabled-bg);border-color:var(--form-input-border-color)!important;box-shadow:none!important;color:var(--form-control-disabled-color);cursor:not-allowed}body.ea-dark-scheme .form-widget .form-select{background-image:url("data:image/svg+xml,%3csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 16 16%27%3e%3cpath fill=%27none%27 stroke=%27%23adb5bd%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27 stroke-width=%272%27 d=%27m2 5 6 6 6-6%27/%3e%3c/svg%3e")}.form-widget .form-select[multiple]{background-image:none;block-size:auto;padding:0}.form-widget input.form-check-input{border:1px solid var(--form-type-check-input-border-color);box-shadow:var(--form-type-check-input-box-shadow)}.form-widget .form-select:focus,.form-widget input.form-check-input:focus,.form-widget input.form-control:focus,.form-widget textarea.form-control:focus{border-color:var(--form-input-hover-border-color);box-shadow:var(--form-input-hover-shadow);outline:0}.form-check-input:checked{background-color:var(--form-type-check-input-checked-bg)}.form-check-input:focus{box-shadow:var(--form-input-hover-shadow)}.form-widget .form-control+.input-group-append{block-size:30px;color:var(--gray-600)}.form-widget .form-control+.input-group-append i{color:var(--gray-600)}.form-widget input.form-control[data-ea-align=right]{text-align:right}.form-widget input.form-control.is-invalid[data-ea-align=right]{padding-inline-end:30px}.form-widget textarea.form-control{block-size:auto;line-height:1.6;white-space:pre-wrap}.form-widget .form-select{background-position:right 5px center;padding:3px 28px 4px 7px}.ts-dropdown.form-select{block-size:auto}.form-widget .form-check{margin:0;padding:0}label.form-check-label{margin:0;padding-inline-start:5px}.form-check .form-check-input{float:none;margin-block-start:2px;margin-inline-start:0}.form-check-inline+.form-check-inline{margin-inline-start:15px}.field-date .form-widget,.field-datetime .form-widget,.field-time .form-widget{margin:0}.datetime-widget .input-group>.form-select,.datetime-widget select{-webkit-appearance:none;min-inline-size:max-content}.datetime-widget+.datetime-widget{margin-inline-start:10px}.datetime-widget select+select{margin-inline-start:4px}.datetime-widget-time select{margin:0 0 0 2px}.datetime-widget-time select:first-child{margin-inline-start:0}.datetime-widget-time select:last-child{margin-inline-end:0}.short .form-widget{flex:0 0 20%!important}.large .form-control,.long .form-control{max-inline-size:unset!important}.large .input.form-control{font-size:18px!important}.large textarea.form-control{block-size:500px;max-inline-size:unset!important}.code input.form-control,.code textarea.form-control{font-family:monospace!important}.field-group .large .form-control,.field-group .large textarea.form-control,.field-group .long .form-control{flex:0 0 100%!important;max-inline-size:unset!important}.field-group .large textarea.form-control{block-size:500px}.form-tabs-tablist .nav-tabs{background:transparent;border:0;box-shadow:0 2px 0 var(--form-tabs-border-color);margin:0 0 20px;padding-inline-start:0}.form-tabs-tablist .nav-tabs a,.form-tabs-tablist .nav-tabs a:hover{border:0;color:var(--text-color);font-size:var(--font-size-base);font-weight:500;margin:0;padding:4px 14px 8px}.form-tabs-tablist .nav-tabs .nav-item:first-child a,.form-tabs-tablist .nav-tabs .nav-item:first-child a:hover{padding-inline-start:0}.form-tabs-tablist .nav-tabs .tab-nav-item-icon{color:var(--text-muted);margin-inline-end:5px}.form-tabs-tablist .nav-tabs .nav-link:focus-visible{box-shadow:none;outline:0}.form-tabs-tablist .nav-tabs .nav-link.active{background:transparent;color:var(--link-color);position:relative}.form-tabs-tablist .nav-tabs .nav-link.active .tab-nav-item-icon{color:var(--link-color)}.form-tabs-tablist .nav-tabs .nav-link.active:before{background:var(--body-bg);block-size:2px;content:"";inline-size:100%;inset-block-end:-2px;inset-inline-start:0;position:absolute}.form-tabs-tablist .nav-tabs .nav-link.active:after{background:var(--link-color);block-size:2px;content:"";inline-size:calc(100% - var(--form-tabs-gutter-x)*2);inset-block-end:-2px;inset-inline-start:var(--form-tabs-gutter-x);position:absolute}.form-tabs-tablist .nav-tabs .nav-item:first-child .nav-link.active:after{inline-size:calc(100% - var(--form-tabs-gutter-x));inset-inline-start:0}.form-tabs-tablist .nav-tabs .nav-item .badge{line-height:1;margin-inline-start:4px;padding:3px 6px}.form-tabs-content .tab-help{color:var(--form-tabs-help-color);margin-block-end:15px;margin-block-start:-10px}.form-column .form-column-title{display:flex;flex-direction:column;margin-block-end:15px}.form-column .form-column-title .form-column-title-content{align-items:center;color:var(--form-column-header-color);display:flex;font-size:17px;font-weight:700;padding:0 0 2px}.form-column .form-column-title .form-column-icon{color:var(--form-column-icon-color);margin-inline-end:10px}.form-column .form-column-title .form-column-help{color:var(--form-column-help-color);flex:1;margin:0}.form-column .field-form_fieldset{margin-block-end:var(--bs-gutter-x)}.form-column .form-fieldset{border-radius:var(--border-radius);box-shadow:inset 0 0 0 1px var(--form-fieldset-border-color)}.form-column .form-fieldset-header{box-shadow:none;padding:calc(var(--bs-gutter-x) - 5px) var(--bs-gutter-x) calc(var(--bs-gutter-x)/2)}.form-column .form-fieldset-header .form-fieldset-title .form-fieldset-title-content{box-shadow:none;padding:0}.form-column .form-fieldset-header .form-fieldset-title .form-fieldset-help{margin-block-start:2px}.form-column .form-fieldset-body{padding:5px var(--bs-gutter-x) 0}.form-column .form-fieldset-body.without-header{padding:var(--bs-gutter-x) var(--bs-gutter-x) 0}.field-form_fieldset{margin-block-end:calc(var(--bs-gutter-x)*1.5)}.form-section-empty{padding:25px 10px}.form-fieldset-header{align-items:flex-start;display:flex;flex-wrap:nowrap;padding:0 0 15px;position:relative}.form-fieldset-header .form-fieldset-collapse-marker{color:var(--form-fieldset-collapse-marker-color);font-size:90%;margin:0 10px 0 2px;transform:rotate(90deg);transition:transform .2s ease-out}.form-fieldset-header .form-fieldset-title{flex:1}.form-fieldset-header .form-fieldset-title .form-fieldset-title-content{align-items:center;box-shadow:0 1px 0 var(--form-fieldset-header-border-color);color:var(--form-fieldset-header-color);display:flex;font-size:17px;font-weight:700;padding:0 0 5px}.form-fieldset-header .form-fieldset-title .form-fieldset-title-content.not-collapsible{cursor:default}.form-fieldset-header .form-fieldset-title .form-fieldset-title-content.collapsed .form-fieldset-collapse-marker{transform:rotate(0deg)}.form-fieldset-header .form-fieldset-title .form-fieldset-title-content .collapsible:after{block-size:100%;content:"";inline-size:100%;inset-block-start:0;inset-inline-start:0;position:absolute}.form-fieldset-header .form-fieldset-title .form-fieldset-icon{color:var(--form-fieldset-icon-color);margin-inline-end:10px}.form-fieldset-header .form-fieldset-title .form-fieldset-help{color:var(--form-fieldset-help-color);margin-block-start:6px}.form-fieldset-title-content .badge-danger{margin-inline-start:8px}.form-fieldset.has-fieldset-error{border:1px solid var(--form-input-error-border-color);border-radius:var(--border-radius);box-shadow:var(--form-input-error-shadow)}.form-fieldset-body{display:grid;grid-template-rows:1fr;overflow:clip;transition:grid-template-rows .2s ease-out}.form-fieldset-body.collapse:not(.show){display:grid;grid-template-rows:0fr}.form-fieldset-body.collapsing{block-size:auto!important;display:grid;overflow:clip}.form-fieldset-body>.row{min-block-size:0;overflow:clip}.form-fieldset-body.show:not(.collapsing){overflow-block:visible;overflow-inline:clip}.form-fieldset-body.show:not(.collapsing)>.row{overflow:visible}@media (prefers-reduced-motion:reduce){.form-column .form-fieldset-header,.form-fieldset-body,.form-fieldset-header .form-fieldset-collapse-marker{transition-duration:.01ms!important}}.form-actions{display:flex;justify-content:flex-end;padding:0}.form-actions .btn{margin-inline-start:10px}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .form-help,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:var(--form-help-error-color)}.has-error .CodeMirror,.has-error .btn.input-file-container,.has-error .ea-fileupload .input-group,.has-error .form-widget .form-select,.has-error .form-widget input.form-check-input,.has-error .form-widget input.form-control,.has-error .form-widget textarea.form-control,.has-error.ea-text-editor-wrapper,.has-error.form-group .ea-text-editor-wrapper{border-color:var(--form-input-error-border-color);box-shadow:var(--form-input-error-shadow)}.form-group.has-error label,.form-group.has-error legend{color:var(--form-input-error-legend-color)}.has-error .ea-fileupload .input-group{border-radius:var(--border-radius)}.global-invalid-feedback{background:var(--form-global-error-bg);border:var(--form-global-error-border);border-radius:var(--border-radius);color:var(--form-global-error-color);font-size:14px;margin:5px 0;padding:6px 12px}form .invalid-feedback{color:var(--color-danger);font-size:1em;font-weight:500;padding-block-start:6px}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:not([type=checkbox]):invalid~.form-check-label{color:inherit}form .invalid-feedback .badge-danger{font-size:.6875rem;margin-inline-end:2px;padding:3px 4px}form .invalid-feedback>.d-block+.d-block{margin-block-start:5px}.input-group-text{background-color:var(--form-input-group-text-bg);block-size:30px;border:1px solid var(--form-input-group-text-border-color);box-shadow:var(--form-input-box-shadow);color:var(--form-input-text-color);padding:3px 10px 5px}.input-group button,.input-group button:active,.input-group button:focus,.input-group button:hover{block-size:28px;margin-block-start:1px}.input-group-append{margin-inline-start:0}.input-group-prepend{margin-inline-end:0}.ea-fileupload{display:flex;flex-direction:column;gap:.5rem}.ea-fileupload-toolbar{align-items:center;display:flex;gap:.75rem}.ea-fileupload-add-btn{align-items:center;display:inline-flex;font-size:.875rem;font-weight:500;gap:.375rem;padding:.375rem .875rem}.ea-fileupload-add-btn .btn-icon{block-size:.875rem;inline-size:.875rem}.ea-fileupload-clear-all-btn{background:none;border:none;color:var(--text-color);cursor:pointer;font-size:.875rem;font-weight:500;margin-inline-start:auto;padding:.375rem 0}.ea-fileupload-clear-all-btn:hover{text-decoration:underline}.ea-fileupload-cards{display:flex;flex-direction:column;gap:.375rem}.ea-fileupload-cards:empty{display:none}.ea-fileupload-card{align-items:center;border:1px solid var(--border-tertiary-color);border-radius:var(--border-radius);display:flex;gap:.625rem;padding:.5rem .75rem}.ea-fileupload-card-preview{align-items:center;block-size:2.25rem;display:flex;flex-shrink:0;inline-size:2.25rem;justify-content:center}.ea-fileupload-card-thumbnail{block-size:2.25rem;border-radius:calc(var(--border-radius)*.5);inline-size:2.25rem;object-fit:cover}.ea-fileupload-card .ea-fileupload-card-icon{block-size:2rem;inline-size:2rem}.ea-fileupload-card .ea-fileupload-card-icon svg{block-size:100%;inline-size:100%;max-block-size:unset;max-inline-size:unset}.ea-fileupload-card-info{display:flex;flex:1;flex-direction:column;min-inline-size:0}.ea-fileupload-card-name{color:var(--text-color);font-size:.875rem;font-weight:600;line-height:1.3;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ea-fileupload-card-size{color:var(--text-secondary-color);font-size:.75rem;line-height:1.3}.ea-fileupload-card-actions{align-items:center;display:flex;flex-shrink:0;gap:.5em}.ea-fileupload-card-actions .ea-fileupload-action-btn{align-items:center;background:none;border:none;border-radius:var(--border-radius);color:var(--text-secondary-color);cursor:pointer;display:inline-flex;justify-content:center;line-height:1;padding:.5em;text-decoration:none}.ea-fileupload-card-actions .ea-fileupload-action-btn:hover{background-color:var(--secondary-bg);color:var(--text-color)}.ea-fileupload-card-actions .ea-fileupload-action-btn .icon{block-size:1.125em;inline-size:1.125em}.ea-fileupload-card-actions .ea-fileupload-action-btn svg{block-size:100%;inline-size:100%;max-block-size:unset;max-inline-size:unset}.ea-vich-image img{box-shadow:0 0 0 4px var(--white),0 0 4px 3px var(--gray-600);margin:6px 4px 12px;max-block-size:300px;max-inline-size:100%}.ea-vich-file-name{display:block;margin:4px 0 8px}.ea-vich-file-name .fa{font-size:18px}.ea-vich-file-actions>div,.ea-vich-image-actions>div{float:left;margin-inline-end:4px}.ea-vich-file-actions:after,.ea-vich-image-actions:after{clear:left;content:"";display:block}.ea-vich-file-actions .field-checkbox,.ea-vich-image-actions .field-checkbox{padding-block-start:4px}.ea-vich-image-actions .form-widget{flex-basis:100%}.input-file-container{overflow:hidden;position:relative}.input-file-container [type=file]{cursor:inherit;display:block;filter:opacity(0);font-size:999px;inset-block-start:0;inset-inline-end:0;min-block-size:100%;min-inline-size:100%;opacity:0;position:absolute;text-align:right}.btn{--button-bg:transparent;--button-border-color:transparent;--button-color:var(--text-color);--button-box-shadow:none;align-items:center;appearance:none;background:var(--button-bg);block-size:var(--button-height,2rem);border:var(--button-border-width,.0625rem) solid;border-color:var(--button-border-color);border-radius:var(--button-border-radius,.375rem);box-shadow:var(--button-box-shadow);color:var(--button-color);cursor:pointer;display:inline-flex;font-family:inherit;font-size:var(--button-font-size,.875rem);font-weight:var(--button-font-weight,500);gap:var(--button-icon-gap,.5rem);justify-content:space-between;line-height:var(--button-line-height);min-inline-size:max-content;padding:var(--button-padding-y,var(--button-padding-y-md)) var(--button-padding-x,var(--button-padding-x-md));position:relative;text-align:center;text-decoration:none;transition:var(--button-transition-duration) var(--button-transition-timing);transition-property:background-color,border-color,color,opacity,fill;user-select:none;white-space:nowrap}.btn:not(:disabled):not(.disabled):focus,.btn:not(:disabled):not(.disabled):focus-visible,.btn:not(:disabled):not(.disabled):hover{background:var(--button-hover-bg,var(--button-bg));border-color:var(--button-hover-border-color,var(--button-border-color));color:var(--button-hover-color,var(--button-color));text-decoration:none}.btn:not(:disabled):not(.disabled):active{background:var(--button-active-bg,var(--button-bg));border-color:var(--button-active-border-color,var(--button-border-color));box-shadow:var(--button-active-box-shadow,var(--button-box-shadow));color:var(--button-active-color,var(--button-color));outline:none}.btn:not(:disabled):not(.disabled):focus-visible:not(:active),.btn:not(:disabled):not(.disabled):focus:not(:active){box-shadow:none;outline:2px solid var(--button-focus-outline-color);outline-offset:-2px}.btn-primary,.btn-primary.btn.disabled,.btn-primary.btn:disabled{--button-box-shadow:var(--button-primary-box-shadow);--button-bg:var(--button-primary-bg);--button-color:var(--button-primary-color);--button-icon-color:var(--button-primary-icon-color);--button-border-color:var(--button-primary-border-color);--button-hover-bg:var(--button-primary-hover-bg);--button-hover-color:var(--button-primary-hover-color);--button-hover-border-color:var(--button-primary-hover-border-color);--button-active-box-shadow:var(--button-primary-active-box-shadow);--button-active-color:var(--button-primary-active-color);--button-active-bg:var(--button-primary-active-bg);--button-active-border-color:var(--button-primary-active-border-color)}.btn-secondary,.btn-secondary.btn.disabled,.btn-secondary.btn:disabled{--button-box-shadow:var(--button-secondary-box-shadow);--button-bg:var(--button-secondary-bg);--button-color:var(--button-secondary-color);--button-icon-color:var(--button-secondary-icon-color);--button-border-color:var(--button-secondary-border-color);--button-hover-bg:var(--button-secondary-hover-bg);--button-hover-color:var(--button-secondary-hover-color);--button-hover-border-color:var(--button-secondary-hover-border-color);--button-active-box-shadow:var(--button-secondary-active-box-shadow);--button-active-color:var(--button-secondary-active-color);--button-active-bg:var(--button-secondary-active-bg);--button-active-border-color:var(--button-secondary-active-border-color)}.btn-success,.btn-success.btn.disabled,.btn-success.btn:disabled{--button-box-shadow:var(--button-success-box-shadow);--button-bg:var(--button-success-bg);--button-color:var(--button-success-color);--button-icon-color:var(--button-success-icon-color);--button-border-color:var(--button-success-border-color);--button-hover-bg:var(--button-success-hover-bg);--button-hover-color:var(--button-success-hover-color);--button-hover-border-color:var(--button-success-hover-border-color);--button-active-box-shadow:var(--button-success-active-box-shadow);--button-active-color:var(--button-success-active-color);--button-active-bg:var(--button-success-active-bg);--button-active-border-color:var(--button-success-active-border-color)}.btn-warning,.btn-warning.btn.disabled,.btn-warning.btn:disabled{--button-box-shadow:var(--button-warning-box-shadow);--button-bg:var(--button-warning-bg);--button-color:var(--button-warning-color);--button-icon-color:var(--button-warning-icon-color);--button-border-color:var(--button-warning-border-color);--button-hover-bg:var(--button-warning-hover-bg);--button-hover-color:var(--button-warning-hover-color);--button-hover-border-color:var(--button-warning-hover-border-color);--button-active-box-shadow:var(--button-warning-active-box-shadow);--button-active-color:var(--button-warning-active-color);--button-active-bg:var(--button-warning-active-bg);--button-active-border-color:var(--button-warning-active-border-color)}.btn-danger,.btn-danger.btn.disabled,.btn-danger.btn:disabled{--button-box-shadow:var(--button-danger-box-shadow);--button-bg:var(--button-danger-bg);--button-color:var(--button-danger-color);--button-icon-color:var(--button-danger-icon-color);--button-border-color:var(--button-danger-border-color);--button-hover-bg:var(--button-danger-hover-bg);--button-hover-color:var(--button-danger-hover-color);--button-hover-border-color:var(--button-danger-hover-border-color);--button-active-box-shadow:var(--button-danger-active-box-shadow);--button-active-color:var(--button-danger-active-color);--button-active-bg:var(--button-danger-active-bg);--button-active-border-color:var(--button-danger-active-border-color)}.btn-invisible,.btn-invisible.btn.disabled,.btn-invisible.btn:disabled{--button-box-shadow:var(--button-invisible-box-shadow);--button-bg:var(--button-invisible-bg);--button-color:var(--button-invisible-color);--button-icon-color:var(--button-invisible-icon-color);--button-border-color:var(--button-invisible-border-color);--button-hover-bg:var(--button-invisible-hover-bg);--button-hover-color:var(--button-invisible-hover-color);--button-hover-border-color:var(--button-invisible-hover-border-color);--button-active-box-shadow:var(--button-invisible-active-box-shadow);--button-active-color:var(--button-invisible-active-color);--button-active-bg:var(--button-invisible-active-bg);--button-active-border-color:var(--button-invisible-active-border-color)}.btn-invisible:active,.btn-invisible:focus,.btn-invisible:focus-visible,.btn-invisible:hover{box-shadow:none}.btn-invisible.btn-danger,.btn-invisible.btn-danger.btn.disabled,.btn-invisible.btn-danger.btn:disabled{--button-color:var(--button-invisible-danger-color);--button-icon-color:var(--button-invisible-danger-hover-icon-color);--button-hover-color:var(--button-invisible-danger-hover-color);--button-hover-bg:var(--button-invisible-danger-hover-hover-bg);--button-active-color:var(--button-invisible-danger-active-color);--button-active-bg:var(--button-invisible-danger-hover-active-bg)}.btn-invisible.btn-danger:active,.btn-invisible.btn-danger:focus,.btn-invisible.btn-danger:focus-visible,.btn-invisible.btn-danger:hover{box-shadow:none}.btn-sm{--button-font-size:var(--button-font-size-sm);--button-padding-y:var(--button-padding-y-sm);--button-padding-x:var(--button-padding-x-sm);--button-icon-gap:.25rem;--button-height:1.75rem}.btn-lg{--button-font-size:var(--button-font-size-lg);--button-padding-y:var(--button-padding-y-lg);--button-padding-x:var(--button-padding-x-lg);block-size:2.5rem}.btn-block{display:block;place-content:center}.btn.disabled,.btn:disabled{background:var(--button-active-bg,var(--button-bg));border-color:var(--button-active-border-color,var(--button-border-color));box-shadow:none;color:var(--button-active-color,var(--button-color));cursor:not-allowed;opacity:var(--button-disabled-opacity);pointer-events:none}a.btn.disabled,fieldset:disabled a.btn{pointer-events:unset}.btn>.btn-label{align-items:center;display:inline-flex;margin:0}.btn>.btn-icon{color:var(--button-icon-color,currentColor);display:grid;flex-shrink:0;inline-size:1em;place-content:center}.btn .btn-icon svg{color:var(--button-icon-color,currentColor);fill:var(--button-icon-color,currentColor)}.btn>.btn-icon+.btn-label,.btn>.btn-label+.btn-icon,.btn>.btn-label+i,.btn>i+.btn-label{margin-inline-start:0}.btn>.btn-icon+.btn-label:empty{display:none}.btn-sm:not(:has(.btn-label)){padding:var(--button-padding-y-sm)}.btn-lg:not(:has(.btn-label)){padding:var(--button-padding-y-lg)}.btn-group>.btn-group:not(:first-child),.btn-group>:not(.btn-check:first-child)+.btn{margin-inline-start:0}.btn-group>.btn.dropdown-toggle.dropdown-toggle-split{margin-inline-start:-1px;padding-inline-end:.5625rem;padding-inline-start:.5625rem}.dropdown-menu .dropdown-submenu .dropdown-toggle.dropdown-toggle-split{border:0;inline-size:auto}.btn-block{display:flex;inline-size:100%}.btn .badge{margin-inline-start:var(--button-icon-gap)}.btn.is-loading{color:transparent;pointer-events:none;position:relative}.btn.is-loading:after{animation:button-spin .5s linear infinite;block-size:1em;border:2px solid;border-block-start-color:transparent;border-inline-end-color:transparent;border-radius:50%;content:"";inline-size:1em;inset-block-start:50%;inset-inline-start:50%;position:absolute;transform:translate(-50%,-50%)}@keyframes button-spin{to{transform:translate(-50%,-50%) rotate(1turn)}}.btn>i{font-size:inherit;line-height:inherit;vertical-align:middle}[dir=rtl] .btn>.btn-icon+.btn-label,[dir=rtl] .btn>.btn-label+.btn-icon,[dir=rtl] .btn>.btn-label+i,[dir=rtl] .btn>i+.btn-label{margin-inline-end:0;margin-inline-start:0}.btn:focus-visible{outline:2px solid var(--link-color);outline-offset:2px}.btn{overflow:hidden;text-overflow:ellipsis}.btn.text-wrap{overflow:visible;text-overflow:unset;white-space:normal}.badge+.badge{margin-inline-start:8px}.badge.badge-pill{border-radius:20px;font-size:var(--font-size-xs);line-height:16px;padding:1px 6px}.badge{box-shadow:var(--badge-box-shadow);line-height:16px}.badge.badge-success{background-color:var(--badge-success-bg);box-shadow:var(--badge-success-box-shadow);color:var(--badge-success-color)}.badge.badge-warning{background-color:var(--badge-warning-bg);box-shadow:var(--badge-warning-box-shadow);color:var(--badge-warning-color)}.badge.badge-danger{background-color:var(--badge-danger-bg);box-shadow:var(--badge-danger-box-shadow);color:var(--badge-danger-color)}.badge.badge-info{background-color:var(--badge-info-bg);box-shadow:var(--badge-info-box-shadow);color:var(--badge-info-color)}.badge.badge-primary{background-color:var(--badge-primary-bg);box-shadow:var(--badge-primary-box-shadow);color:var(--badge-primary-color)}.badge.badge-secondary{background-color:var(--badge-secondary-bg);box-shadow:var(--badge-secondary-box-shadow);color:var(--badge-secondary-color)}.badge.badge-light{background-color:var(--badge-light-bg);box-shadow:var(--badge-light-box-shadow);color:var(--badge-light-color)}.badge.badge-dark{background-color:var(--badge-dark-bg);box-shadow:var(--badge-dark-box-shadow);color:var(--badge-dark-color)}.badge.badge-outline{background-color:transparent;box-shadow:var(--badge-outline-box-shadow);color:var(--badge-outline-color)}.form-switch .form-check-input{-webkit-appearance:none;background-color:var(--form-switch-bg);background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%27-4 -4 8 8%27%3E%3Ccircle r=%273%27 fill=%27rgba%28148, 163, 184, 0.8%29%27/%3E%3C/svg%3E");block-size:18px;border-color:var(--form-switch-border-color);cursor:pointer;inline-size:32px}.ea-dark-scheme .form-switch .form-check-input:checked,.form-switch .form-check-input:checked{background-color:var(--form-switch-checked-bg);background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%27-4 -4 8 8%27%3E%3Ccircle r=%273%27 fill=%27rgb%28255, 255, 255%29%27/%3E%3C/svg%3E");border-color:var(--form-switch-checked-bg)}.ea-dark-scheme .form-switch .form-check-input:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%27-4 -4 8 8%27%3E%3Ccircle r=%273%27 fill=%27rgba%28255, 255, 255, 0.8%29%27/%3E%3C/svg%3E")}.ea-dark-scheme .form-switch .form-check-input{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%27-4 -4 8 8%27%3E%3Ccircle r=%273%27 fill=%27rgba%28163, 163, 163, 0.8%29%27/%3E%3C/svg%3E")}.form-switch .form-check-input[disabled],.form-switch.disabled{cursor:not-allowed}.form-switch .form-check-input:focus{box-shadow:none}:root{--ts-pr-clear-button:0;--ts-pr-caret:0;--ts-pr-min:.75rem}.ts-wrapper.single .ts-control,.ts-wrapper.single .ts-control input{cursor:pointer}.ts-control{padding-inline-end:max(var(--ts-pr-min),var(--ts-pr-clear-button) + var(--ts-pr-caret))!important}.ts-wrapper.plugin-drag_drop.multi>.ts-control>div.ui-sortable-placeholder{background:#f2f2f2!important;background:rgba(0,0,0,.06)!important;border:0!important;box-shadow:inset 0 0 12px 4px #fff;visibility:visible!important}.ts-wrapper.plugin-drag_drop .ui-sortable-placeholder:after{content:"!";visibility:hidden}.ts-wrapper.plugin-drag_drop .ui-sortable-helper{box-shadow:0 2px 5px rgba(0,0,0,.2)}.plugin-checkbox_options .option input{margin-inline-end:.5rem}.plugin-clear_button{--ts-pr-clear-button:1em}.plugin-clear_button .clear-button{background:transparent!important;cursor:pointer;inset-block-start:50%;inset-inline-end:calc(.75rem - 5px);margin-inline-end:0!important;opacity:0;position:absolute;transform:translateY(-50%);transition:opacity .5s}.plugin-clear_button.form-select .clear-button,.plugin-clear_button.single .clear-button{inset-inline-end:max(var(--ts-pr-caret),.75rem)}.plugin-clear_button.focus.has-items .clear-button,.plugin-clear_button:not(.disabled):hover.has-items .clear-button{opacity:1}.ts-wrapper .dropdown-header{background:#f8f8f8;border-block-end:1px solid #d0d0d0;border-radius:.375rem .375rem 0 0;padding:6px .75rem;position:relative}.ts-wrapper .dropdown-header-close{color:#343a40;font-size:20px!important;inset-block-start:50%;inset-inline-end:.75rem;line-height:20px;margin-block-start:-12px;opacity:.4;position:absolute}.ts-wrapper .dropdown-header-close:hover{color:#000}.plugin-dropdown_input.focus.dropdown-active .ts-control{border:1px solid #ced4da;box-shadow:none;box-shadow:inset 0 1px 2px rgba(0,0,0,.075)}.plugin-dropdown_input .dropdown-input{background:transparent;border:solid #d0d0d0;border-width:0 0 1px;box-shadow:none;display:block;inline-size:100%;padding:.375rem .75rem}.plugin-dropdown_input.focus .ts-dropdown .dropdown-input{border-color:#86b7fe;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);outline:0}.plugin-dropdown_input .items-placeholder{border:0!important;box-shadow:none!important;inline-size:100%}.plugin-dropdown_input.dropdown-active .items-placeholder,.plugin-dropdown_input.has-items .items-placeholder{display:none!important}.ts-wrapper.plugin-input_autogrow.has-items .ts-control>input{min-inline-size:0}.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control>input{flex:none;min-inline-size:4px}.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control>input::-ms-input-placeholder{color:transparent}.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control>input::placeholder{color:transparent}.ts-dropdown.plugin-optgroup_columns .ts-dropdown-content{display:flex}.ts-dropdown.plugin-optgroup_columns .optgroup{border-block-start:0;border-inline-end:1px solid #f2f2f2;flex-basis:0;flex-grow:1;min-inline-size:0}.ts-dropdown.plugin-optgroup_columns .optgroup:last-child{border-inline-end:0}.ts-dropdown.plugin-optgroup_columns .optgroup:before{display:none}.ts-dropdown.plugin-optgroup_columns .optgroup-header{border-block-start:0}.ts-wrapper.plugin-remove_button .item{align-items:center;display:inline-flex;padding-inline-end:0!important}.ts-wrapper.plugin-remove_button .item .remove{border-radius:0 2px 2px 0;box-sizing:border-box;color:inherit;display:inline-block;padding:0 5px;text-decoration:none;vertical-align:middle}.ts-wrapper.plugin-remove_button .item .remove:hover{background:rgba(0,0,0,.05)}.ts-wrapper.plugin-remove_button.disabled .item .remove:hover{background:none}.ts-wrapper.plugin-remove_button .remove-single{font-size:23px;inset-block-start:0;inset-inline-end:0;position:absolute}.ts-wrapper.plugin-remove_button:not(.rtl) .item .remove{border-inline-start:1px solid #dee2e6;margin-inline-start:5px}.ts-wrapper.plugin-remove_button:not(.rtl) .item.active .remove{border-inline-start-color:transparent}.ts-wrapper.plugin-remove_button:not(.rtl).disabled .item .remove{border-inline-start-color:#fff}.ts-wrapper.plugin-remove_button.rtl .item .remove{border-inline-end:1px solid #dee2e6;margin-inline-end:5px}.ts-wrapper.plugin-remove_button.rtl .item.active .remove{border-inline-end-color:transparent}.ts-wrapper.plugin-remove_button.rtl.disabled .item .remove{border-inline-end-color:#fff}.ts-wrapper{position:relative}.ts-control,.ts-control input,.ts-dropdown{-webkit-font-smoothing:inherit;color:#343a40;font-family:inherit;font-size:inherit;line-height:1.5}.ts-control,.ts-wrapper.single.input-active .ts-control{background:#fff;cursor:text}.ts-control{border:1px solid #ced4da;border-radius:.375rem;box-shadow:none;box-sizing:border-box;flex-wrap:wrap;inline-size:100%;overflow:hidden;padding:.375rem .75rem;position:relative;z-index:1}.ts-wrapper.multi.has-items .ts-control{padding:calc(.375rem - 1px) .75rem calc(.375rem - 4px)}.full .ts-control{background-color:#fff}.disabled .ts-control,.disabled .ts-control *{cursor:default!important}.focus .ts-control{box-shadow:none}.ts-control>*{display:inline-block;vertical-align:baseline}.ts-wrapper.multi .ts-control>div{background:#efefef;border:0 solid #dee2e6;color:#343a40;cursor:pointer;margin:0 3px 3px 0;padding:1px 5px}.ts-wrapper.multi .ts-control>div.active{background:#0d6efd;border:0 solid transparent;color:#fff}.ts-wrapper.multi.disabled .ts-control>div,.ts-wrapper.multi.disabled .ts-control>div.active{background:#fff;border:0 solid #fff;color:#878787}.ts-control>input{background:none!important;border:0!important;box-shadow:none!important;display:inline-block!important;flex:1 1 auto;line-height:inherit!important;margin:0!important;max-block-size:none!important;max-inline-size:100%!important;min-block-size:0!important;min-inline-size:7rem;padding:0!important;text-indent:0!important;-webkit-user-select:auto!important;-moz-user-select:auto!important;-ms-user-select:auto!important;user-select:auto!important}.ts-control>input::-ms-clear{display:none}.ts-control>input:focus{outline:none!important}.has-items .ts-control>input{margin:0 4px!important}.ts-control.rtl{text-align:right}.ts-control.rtl.single .ts-control:after{inset-inline-end:auto;inset-inline-start:calc(.75rem + 5px)}.ts-control.rtl .ts-control>input{margin:0 4px 0 -2px!important}.disabled .ts-control{background-color:#e9ecef;opacity:.5}.input-hidden .ts-control>input{inset-inline-start:-10000px;opacity:0;position:absolute}.ts-dropdown{background:#fff;border:1px solid #d0d0d0;border-block-start:0;border-radius:0 0 .375rem .375rem;box-shadow:0 1px 3px rgba(0,0,0,.1);box-sizing:border-box;inline-size:100%;inset-block-start:100%;inset-inline-start:0;margin:.25rem 0 0;position:absolute;z-index:10}.ts-dropdown [data-selectable]{cursor:pointer;overflow:hidden}.ts-dropdown [data-selectable] .highlight{background:rgba(255,237,40,.4);border-radius:1px}.ts-dropdown .create,.ts-dropdown .no-results,.ts-dropdown .optgroup-header,.ts-dropdown .option{padding:3px .75rem}.ts-dropdown .option,.ts-dropdown [data-disabled],.ts-dropdown [data-disabled] [data-selectable].option{cursor:inherit;opacity:.5}.ts-dropdown [data-selectable].option{cursor:pointer;opacity:1}.ts-dropdown .optgroup:first-child .optgroup-header{border-block-start:0}.ts-dropdown .optgroup-header{background:#fff;color:#6c757d;cursor:default}.ts-dropdown .active{background-color:#e9ecef;color:#1e2125}.ts-dropdown .active.create{color:#1e2125}.ts-dropdown .create{color:rgba(52,58,64,.5)}.ts-dropdown .spinner{block-size:30px;display:inline-block;inline-size:30px;margin:3px .75rem}.ts-dropdown .spinner:after{animation:lds-dual-ring 1.2s linear infinite;block-size:24px;border-color:#d0d0d0 transparent;border-radius:50%;border-style:solid;border-width:5px;content:" ";display:block;inline-size:24px;margin:3px}@keyframes lds-dual-ring{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.ts-dropdown-content{-webkit-overflow-scrolling:touch;max-block-size:200px;overflow-block:auto;overflow-inline:hidden;scroll-behavior:smooth}.ts-hidden-accessible{clip:rect(0 0 0 0)!important;border:0!important;-webkit-clip-path:inset(50%)!important;clip-path:inset(50%)!important;inline-size:1px!important;overflow:hidden!important;padding:0!important;position:absolute!important;white-space:nowrap!important}.ts-wrapper.form-control,.ts-wrapper.form-select{block-size:auto;box-shadow:none;display:flex;padding:0!important}.ts-dropdown,.ts-dropdown.form-control,.ts-dropdown.form-select{background:#fff;block-size:auto;border:1px solid var(--bs-border-color-translucent);border-radius:.375rem;box-shadow:0 6px 12px rgba(0,0,0,.175);padding:0;z-index:1000}.ts-dropdown .optgroup-header{font-size:.875rem;line-height:1.5}.ts-dropdown .optgroup:first-child:before{display:none}.ts-dropdown .optgroup:before{block-size:0;border-block-start:1px solid var(--bs-border-color-translucent);content:" ";display:block;margin:.5rem -.75rem;overflow:hidden}.ts-dropdown .create{padding-inline-start:.75rem}.ts-dropdown-content{padding:5px 0}.ts-control{align-items:center;display:flex;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.ts-control{transition:none}}.ts-control.dropdown -active{border-radius:.375rem}.focus .ts-control{border-color:#86b7fe;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);outline:0}.ts-control .item{align-items:center;display:flex}.ts-wrapper.is-invalid,.was-validated .invalid,.was-validated :invalid+.ts-wrapper{border-color:#dc3545}.ts-wrapper.is-invalid:not(.single),.was-validated .invalid:not(.single),.was-validated :invalid+.ts-wrapper:not(.single){background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 inline-size=%2712%27 block-size=%2712%27 fill=%27none%27 stroke=%27%23dc3545%27%3E%3Ccircle cx=%276%27 cy=%276%27 r=%274.5%27/%3E%3Cpath stroke-linejoin=%27round%27 d=%27M5.8 3.6h.4L6 6.5z%27/%3E%3Ccircle cx=%276%27 cy=%278.2%27 r=%27.6%27 fill=%27%23dc3545%27 stroke=%27none%27/%3E%3C/svg%3E");background-position:right calc(.375em + .1875rem) center;background-repeat:no-repeat;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.ts-wrapper.is-invalid.single,.was-validated .invalid.single,.was-validated :invalid+.ts-wrapper.single{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 16 16%27%3E%3Cpath fill=%27none%27 stroke=%27%23343a40%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27 stroke-width=%272%27 d=%27m2 5 6 6 6-6%27/%3E%3C/svg%3E"),url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 inline-size=%2712%27 block-size=%2712%27 fill=%27none%27 stroke=%27%23dc3545%27%3E%3Ccircle cx=%276%27 cy=%276%27 r=%274.5%27/%3E%3Cpath stroke-linejoin=%27round%27 d=%27M5.8 3.6h.4L6 6.5z%27/%3E%3Ccircle cx=%276%27 cy=%278.2%27 r=%27.6%27 fill=%27%23dc3545%27 stroke=%27none%27/%3E%3C/svg%3E");background-position:right .75rem center,center right 2.25rem;background-repeat:no-repeat;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.ts-wrapper.is-invalid.focus .ts-control,.was-validated .invalid.focus .ts-control,.was-validated :invalid+.ts-wrapper.focus .ts-control{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.ts-wrapper.is-valid,.was-validated .valid,.was-validated :valid+.ts-wrapper{border-color:#198754}.ts-wrapper.is-valid:not(.single),.was-validated .valid:not(.single),.was-validated :valid+.ts-wrapper:not(.single){background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 8 8%27%3E%3Cpath fill=%27%23198754%27 d=%27M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z%27/%3E%3C/svg%3E");background-position:right calc(.375em + .1875rem) center;background-repeat:no-repeat;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.ts-wrapper.is-valid.single,.was-validated .valid.single,.was-validated :valid+.ts-wrapper.single{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 16 16%27%3E%3Cpath fill=%27none%27 stroke=%27%23343a40%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27 stroke-width=%272%27 d=%27m2 5 6 6 6-6%27/%3E%3C/svg%3E"),url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 8 8%27%3E%3Cpath fill=%27%23198754%27 d=%27M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z%27/%3E%3C/svg%3E");background-position:right .75rem center,center right 2.25rem;background-repeat:no-repeat;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.ts-wrapper.is-valid.focus .ts-control,.was-validated .valid.focus .ts-control,.was-validated :valid+.ts-wrapper.focus .ts-control{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.ts-wrapper{display:flex;min-block-size:calc(1.5em + .75rem + 2px)}.input-group-sm>.ts-wrapper,.ts-wrapper.form-control-sm,.ts-wrapper.form-select-sm{min-block-size:calc(1.5em + .5rem + 2px)}.input-group-sm>.ts-wrapper .ts-control,.ts-wrapper.form-control-sm .ts-control,.ts-wrapper.form-select-sm .ts-control{border-radius:.25rem;font-size:.875rem}.input-group-sm>.ts-wrapper.has-items .ts-control,.ts-wrapper.form-control-sm.has-items .ts-control,.ts-wrapper.form-select-sm.has-items .ts-control{font-size:.875rem;padding-block-end:0}.input-group-sm>.ts-wrapper.multi.has-items .ts-control,.ts-wrapper.form-control-sm.multi.has-items .ts-control,.ts-wrapper.form-select-sm.multi.has-items .ts-control{padding-block-start:calc(.75em - .40625rem - 1px)!important}.ts-wrapper.multi.has-items .ts-control{--ts-pr-min:calc(0.75rem - 5px);padding-inline-start:calc(.75rem - 5px)}.ts-wrapper.multi .ts-control>div{border-radius:calc(.375rem - 1px)}.input-group-lg>.ts-wrapper,.ts-wrapper.form-control-lg,.ts-wrapper.form-select-lg{min-block-size:calc(1.5em + 1rem + 2px)}.input-group-lg>.ts-wrapper .ts-control,.ts-wrapper.form-control-lg .ts-control,.ts-wrapper.form-select-lg .ts-control{border-radius:.5rem;font-size:1.25rem}.ts-wrapper:not(.form-control):not(.form-select){background:none;block-size:auto;border:none;box-shadow:none;padding:0}.ts-wrapper:not(.form-control):not(.form-select).single .ts-control{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 16 16%27%3E%3Cpath fill=%27none%27 stroke=%27%23343a40%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27 stroke-width=%272%27 d=%27m2 5 6 6 6-6%27/%3E%3C/svg%3E");background-position:right .75rem center;background-repeat:no-repeat;background-size:16px 12px}.ts-wrapper.form-select,.ts-wrapper.single{--ts-pr-caret:2.25rem}.ts-wrapper.form-control .ts-control,.ts-wrapper.form-control.single.input-active .ts-control,.ts-wrapper.form-select .ts-control,.ts-wrapper.form-select.single.input-active .ts-control{border:none!important}.ts-wrapper.form-control:not(.disabled) .ts-control,.ts-wrapper.form-control:not(.disabled).single.input-active .ts-control,.ts-wrapper.form-select:not(.disabled) .ts-control,.ts-wrapper.form-select:not(.disabled).single.input-active .ts-control{background:transparent!important}.input-group>.ts-wrapper{flex-grow:1}.input-group>.ts-wrapper:not(:nth-child(2))>.ts-control{border-end-start-radius:0;border-start-start-radius:0}.input-group>.ts-wrapper:not(:last-child)>.ts-control{border-end-end-radius:0;border-start-end-radius:0}.ts-wrapper{min-block-size:unset}.ts-wrapper .ts-control{block-size:unset;min-block-size:30px;padding:3px 28px 4px 7px}.ts-wrapper.input-active{border-color:var(--form-input-hover-border-color);box-shadow:var(--form-input-hover-shadow);outline:0}.ts-wrapper.focus .ts-control{box-shadow:none;outline:0}.dropdown-input-wrap{background:var(--form-type-autocomplete-dropdown-input-wrapper-bg);border-block-end:1px solid var(--form-input-border-color);border-start-end-radius:var(--border-radius);border-start-start-radius:var(--border-radius);padding:7px 10px}.dropdown-input,.plugin-dropdown_input.focus .dropdown-input{background:var(--form-control-bg);block-size:30px;border:1px solid var(--form-type-autocomplete-dropdown-input-border-color);border-radius:var(--border-radius);box-shadow:var(--form-input-box-shadow);color:var(--form-input-text-color);position:relative}.dropdown-input:focus{border:0;box-shadow:0 0 0 1px rgba(43,45,80,0),0 0 0 1px rgba(6,122,184,.2),0 0 0 2px rgba(6,122,184,.25),0 1px 1px rgba(0,0,0,.08);outline:0}.ts-dropdown,.ts-dropdown.form-control,.ts-dropdown.form-select{background:var(--form-type-autocomplete-dropdown-bg);border:1px solid var(--form-input-border-color);box-shadow:var(--shadow-xl);color:var(--form-input-text-color)}.ts-dropdown .active,.ts-dropdown .create:hover,.ts-dropdown .option:hover{background-color:var(--form-type-autocomplete-dropdown-active-item-bg);color:var(--form-input-text-color)}.ts-dropdown [data-selectable] .highlight{background:var(--highlight-bg);color:var(--highlight-color)}.ts-control,.ts-control input,.ts-dropdown{color:var(--form-input-text-color)}.ts-dropdown-content{padding:4px 5px}.ts-dropdown [data-selectable].option{border-radius:var(--border-radius);margin:2px 0}.ts-dropdown .optgroup-header{background:var(--form-type-autocomplete-optgroup-bg);color:var(--form-type-autocomplete-optgroup-color);font-size:13px;font-weight:700}.ts-wrapper.multi,.ts-wrapper.multi.has-items .ts-control{block-size:auto}.ts-wrapper.multi .ts-control,.ts-wrapper.multi.has-items .ts-control{padding:2px 15px 3px 7px}.ts-wrapper.plugin-remove_button.multi.has-items .ts-control{padding-inline-end:55px}.ts-wrapper.multi .ts-control>div{background:var(--form-type-autocomplete-multi-item-bg);border-radius:var(--border-radius);box-shadow:0 0 0 1px var(--form-type-autocomplete-multi-item-border-color);color:var(--form-input-text-color);margin:2px 5px 2px 0;padding:0 4px}.ts-wrapper.plugin-remove_button .item .remove,.ts-wrapper.plugin-remove_button:not(.rtl) .item .remove{border-inline-start:1px solid var(--form-type-autocomplete-multi-item-border-color);border-radius:0 var(--border-radius) var(--border-radius) 0}.ts-wrapper.plugin-remove_button .item .remove:hover{background:var(--form-type-autocomplete-multi-item-remove-button-hover-bg)}.plugin-clear_button.ts-wrapper .clear-button,.plugin-clear_button.ts-wrapper.multi .clear-button{align-content:center;background:var(--form-type-autocomplete-close-button-bg)!important;block-size:14px;border-radius:50%;color:#fff;cursor:pointer;display:flex;font-size:16px;font-weight:700;inline-size:14px;inset-block-start:calc(50% - 2px);inset-inline-end:32px;justify-content:center;line-height:.65;padding:0}.ts-wrapper.plugin-clear_button.multi .clear-button{inset-inline-end:10px}.ts-wrapper.plugin-remove_button.plugin-clear_button.multi.has-items .clear-button{inset-inline-end:32px}.plugin-clear_button.ts-wrapper .clear-button:hover,.plugin-clear_button.ts-wrapper.multi .clear-button:hover{background:var(--form-type-autocomplete-close-button-hover-bg)!important}.ts-wrapper.disabled .ts-control{background-color:var(--form-control-disabled-bg)}.ts-dropdown .optgroup-header:empty{display:none}body.error .error-message{max-inline-size:500px;min-block-size:400px;padding:45px 15px}@media (min-width:992px){body.error .error-message{padding:45px}}body.error .error-message h1{align-items:center;color:var(--color-danger);display:flex;font-size:var(--font-size-lg);font-weight:600;margin-block-end:1em}body.error .error-message h1 .icon{font-size:110%;line-height:1;margin-inline-end:6px}body,html{block-size:100%;margin:0}body.page-login{background:var(--body-bg)}@media (min-width:576px){body.page-login{background:var(--page-login-bg);display:grid;min-block-size:100vh;place-items:center}}body.page-login #flash-messages{inline-size:100%;inset-block-start:0;inset-inline-start:0;position:absolute}@media (min-width:576px){.login-wrapper{inline-size:25rem;margin:0 auto}}.login-wrapper .main-header{display:block;padding-inline-end:0}.login-wrapper .main-header #header-logo{border-block-end:var(--border-width) var(--border-style) var(--border-secondary-color);margin:1.5rem 0 1rem;padding-block-end:1rem}@media (min-width:576px){.login-wrapper .main-header #header-logo{border-block-end:none}}.login-wrapper .main-header #header-logo a{font-size:var(--font-size-lg);margin:0;padding:0;text-align:center}@media (min-width:576px){.login-wrapper .main-header #header-logo a{font-size:var(--font-size-xl)}}.login-wrapper .content{background-color:var(--body-bg);inline-size:100%;padding:15px 30px}@media (min-width:576px){.login-wrapper .content{background:var(--page-login-form-bg);border-radius:var(--border-radius-lg);box-shadow:var(--shadow-lg);padding:2.5rem 3rem}}.login-wrapper .form-widget input{background-color:var(--page-login-form-control-bg);block-size:38px;border-color:var(--page-login-form-control-border-color);font-size:var(--font-size-lg);line-height:38px}.login-wrapper .form-group label.required:after{display:none}.login-wrapper .btn-primary{background-color:var(--page-login-form-control-button-bg);font-size:var(--font-size-base);margin-block-start:1rem}.login-wrapper .form-text{font-size:inherit;margin-block-start:5px} \ No newline at end of file diff --git a/public/entrypoints.json b/public/entrypoints.json index b59d805001..164978535d 100644 --- a/public/entrypoints.json +++ b/public/entrypoints.json @@ -2,7 +2,7 @@ "entrypoints": { "app": { "css": [ - "/app.912702e5.css" + "/app.ef57b823.css" ], "js": [ "/app.e8e4fe24.js" @@ -43,7 +43,7 @@ }, "field-file-upload": { "js": [ - "/field-file-upload.5c32db38.js" + "/field-file-upload.e3ad5433.js" ] }, "field-image": { diff --git a/public/field-file-upload.5c32db38.js b/public/field-file-upload.5c32db38.js deleted file mode 100644 index 82d6080461..0000000000 --- a/public/field-file-upload.5c32db38.js +++ /dev/null @@ -1 +0,0 @@ -(()=>{"use strict";function t(t,e){e?(t.classList.remove("d-block"),t.classList.add("d-none")):(t.classList.remove("d-none"),t.classList.add("d-block"))}function e(t){return e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},e(t)}function n(t,e){var n="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(!n){if(Array.isArray(t)||(n=function(t,e){if(t){if("string"==typeof t)return i(t,e);var n={}.toString.call(t).slice(8,-1);return"Object"===n&&t.constructor&&(n=t.constructor.name),"Map"===n||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?i(t,e):void 0}}(t))||e&&t&&"number"==typeof t.length){n&&(t=n);var r=0,o=function(){};return{s:o,n:function(){return r>=t.length?{done:!0}:{done:!1,value:t[r++]}},e:function(t){throw t},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var l,a=!0,c=!1;return{s:function(){n=n.call(t)},n:function(){var t=n.next();return a=t.done,t},e:function(t){c=!0,l=t},f:function(){try{a||null==n.return||n.return()}finally{if(c)throw l}}}}function i(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,i=Array(e);n{function t(e){return t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},t(e)}function e(t,e){var i="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(!i){if(Array.isArray(t)||(i=function(t,e){if(t){if("string"==typeof t)return a(t,e);var i={}.toString.call(t).slice(8,-1);return"Object"===i&&t.constructor&&(i=t.constructor.name),"Map"===i||"Set"===i?Array.from(t):"Arguments"===i||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i)?a(t,e):void 0}}(t))||e&&t&&"number"==typeof t.length){i&&(t=i);var n=0,r=function(){};return{s:r,n:function(){return n>=t.length?{done:!0}:{done:!1,value:t[n++]}},e:function(t){throw t},f:r}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var l,o=!0,c=!1;return{s:function(){i=i.call(t)},n:function(){var t=i.next();return o=t.done,t},e:function(t){c=!0,l=t},f:function(){try{o||null==i.return||i.return()}finally{if(c)throw l}}}}function a(t,e){(null==e||e>t.length)&&(e=t.length);for(var a=0,i=Array(e);a=0&&a0;c(p,this)&&(c(b,this)?c(p,this).classList.remove("d-none"):c(p,this).classList.toggle("d-none",t)),c(v,this)&&c(v,this).classList.toggle("d-none",!t)}function x(t,e,a){var i=document.createElement("div");i.className="ea-fileupload-card",i.setAttribute("data-ea-fileupload-card",""),i.setAttribute("data-new-file",""),i.setAttribute("data-file-index",String(a));var n=document.createElement("div");if(n.className="ea-fileupload-card-preview",t.type.startsWith("image/")){var r=document.createElement("img");r.src=e,r.alt=t.name,r.className="ea-fileupload-card-thumbnail",n.appendChild(r)}else{var l=u(w,this,B).call(this,c(d,this).getAttribute("data-icon-generic"));l&&n.appendChild(l)}i.appendChild(n);var o=document.createElement("div");o.className="ea-fileupload-card-info";var s=document.createElement("span");s.className="ea-fileupload-card-name",s.textContent=t.name,o.appendChild(s);var h=document.createElement("span");h.className="ea-fileupload-card-size",h.textContent=u(w,this,O).call(this,t.size),o.appendChild(h),i.appendChild(o);var f=document.createElement("div");if(f.className="ea-fileupload-card-actions",c(d,this).hasAttribute("data-allow-delete")){var p=document.createElement("button");p.type="button",p.className="ea-fileupload-action-btn ea-fileupload-action-delete",p.setAttribute("data-ea-fileupload-delete-card","");var v=u(w,this,B).call(this,c(d,this).getAttribute("data-icon-delete"));v&&p.appendChild(v),f.appendChild(p)}return i.appendChild(f),i}function B(t){if(!t)return null;for(var e=(new DOMParser).parseFromString(t,"text/html"),a=document.createDocumentFragment();e.body.firstChild;)a.appendChild(e.body.firstChild);return a}function O(t){var e=["B","KB","MB","GB","TB","PB","EB","ZB","YB"];if(0===t)return"0 B";var a=Math.trunc(Math.floor(Math.log(t)/Math.log(1024)));if(0===a)return"".concat(Math.trunc(t)," ").concat(e[0]);var i=Math.round(t/Math.pow(1024,a)*10)/10,n=i%1==0?i.toFixed(0):i.toFixed(1);return"".concat(n," ").concat(e[a])}})(); \ No newline at end of file diff --git a/public/manifest.json b/public/manifest.json index f2caf5beaa..9f8ba86490 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,5 +1,5 @@ { - "app.css": "app.912702e5.css", + "app.css": "app.ef57b823.css", "app.js": "app.e8e4fe24.js", "form.js": "form.4f66b3e8.js", "page-layout.js": "page-layout.6e9fe55d.js", @@ -8,7 +8,7 @@ "field-code-editor.css": "field-code-editor.cdcf15eb.css", "field-code-editor.js": "field-code-editor.877c61fa.js", "field-collection.js": "field-collection.b4d3688b.js", - "field-file-upload.js": "field-file-upload.5c32db38.js", + "field-file-upload.js": "field-file-upload.e3ad5433.js", "field-image.js": "field-image.c338d2ad.js", "field-slug.js": "field-slug.ba7fb8e5.js", "field-textarea.js": "field-textarea.98322d83.js", diff --git a/src/Config/Option/ReplacedFileBehavior.php b/src/Config/Option/ReplacedFileBehavior.php new file mode 100644 index 0000000000..47d19ff8bf --- /dev/null +++ b/src/Config/Option/ReplacedFileBehavior.php @@ -0,0 +1,13 @@ + + */ +final class ReplacedFileBehavior +{ + public const DELETE = 'delete'; + public const KEEP = 'keep'; + public const KEEP_OR_FAIL = 'keep_or_fail'; +} diff --git a/src/Controller/AbstractCrudController.php b/src/Controller/AbstractCrudController.php index 373b588f26..33e906486f 100644 --- a/src/Controller/AbstractCrudController.php +++ b/src/Controller/AbstractCrudController.php @@ -15,6 +15,7 @@ use EasyCorp\Bundle\EasyAdminBundle\Config\Filters; use EasyCorp\Bundle\EasyAdminBundle\Config\KeyValueStore; use EasyCorp\Bundle\EasyAdminBundle\Config\Option\EA; +use EasyCorp\Bundle\EasyAdminBundle\Config\Option\ReplacedFileBehavior; use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext; use EasyCorp\Bundle\EasyAdminBundle\Contracts\Controller\CrudControllerInterface; use EasyCorp\Bundle\EasyAdminBundle\Contracts\Orm\EntityRepositoryInterface; @@ -687,10 +688,13 @@ protected function processUploadedFiles(FormInterface $form): void } $uploadDelete = $config->getOption('upload_delete'); + $replacedFileBehavior = $config->getOption('replaced_file_behavior'); if ($state->hasCurrentFiles() && ($state->isDelete() || (!$state->isAddAllowed() && $state->hasUploadedFiles()))) { - foreach ($state->getCurrentFiles() as $file) { - $uploadDelete($file); + if ($state->isDelete() || ReplacedFileBehavior::DELETE === $replacedFileBehavior) { + foreach ($state->getCurrentFiles() as $file) { + $uploadDelete($file); + } } $state->setCurrentFiles([]); } diff --git a/src/DependencyInjection/EasyAdminExtension.php b/src/DependencyInjection/EasyAdminExtension.php index fd6027784c..3a97ca3f8a 100644 --- a/src/DependencyInjection/EasyAdminExtension.php +++ b/src/DependencyInjection/EasyAdminExtension.php @@ -26,6 +26,7 @@ class EasyAdminExtension extends Extension implements PrependExtensionInterface public const TAG_FIELD_CONFIGURATOR = 'ea.field_configurator'; public const TAG_FILTER_CONFIGURATOR = 'ea.filter_configurator'; public const TAG_ACTIONS_EXTENSION = 'ea.actions_extension'; + public const TAG_FLYSYSTEM_STORAGE = 'ea.flysystem_storage'; public function load(array $configs, ContainerBuilder $container): void { @@ -50,6 +51,11 @@ static function (Definition $definition, AdminRoute $attribute, \ReflectionClass $container->registerForAutoconfiguration(ActionsExtensionInterface::class) ->addTag(self::TAG_ACTIONS_EXTENSION); + if (interface_exists('League\Flysystem\FilesystemOperator')) { + $container->registerForAutoconfiguration(\League\Flysystem\FilesystemOperator::class) + ->addTag(self::TAG_FLYSYSTEM_STORAGE); + } + $loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../../config')); $loader->load('services.php'); } diff --git a/src/Field/Configurator/FileConfigurator.php b/src/Field/Configurator/FileConfigurator.php new file mode 100644 index 0000000000..e8c789a615 --- /dev/null +++ b/src/Field/Configurator/FileConfigurator.php @@ -0,0 +1,250 @@ + + */ +final readonly class FileConfigurator implements FieldConfiguratorInterface +{ + public function __construct( + private string $projectDir, + private ?ContainerInterface $flysystemLocator = null, + ) { + } + + public function supports(FieldDto $field, EntityDto $entityDto): bool + { + return FileField::class === $field->getFieldFqcn(); + } + + public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $context): void + { + $flysystemStorageName = $field->getCustomOption(FileField::OPTION_FLYSYSTEM_STORAGE); + $flysystemUrlPrefix = $field->getCustomOption(FileField::OPTION_FLYSYSTEM_URL_PREFIX); + $filesystem = null; + + if (null !== $flysystemStorageName) { + $filesystem = $this->resolveFlysystemStorage($flysystemStorageName); + } + + $configuredBasePath = $field->getCustomOption(FileField::OPTION_BASE_PATH); + + if (null !== $filesystem && null !== $flysystemUrlPrefix) { + $formattedValue = \is_array($field->getValue()) + ? $this->getFlysystemFilesPaths($field->getValue(), $flysystemUrlPrefix) + : $this->getFlysystemFilePath($field->getValue(), $flysystemUrlPrefix); + } else { + $formattedValue = \is_array($field->getValue()) + ? $this->getFilesPaths($field->getValue(), $configuredBasePath) + : $this->getFilePath($field->getValue(), $configuredBasePath); + } + $field->setFormattedValue($formattedValue); + + $pattern = $field->getCustomOption(FileField::OPTION_UPLOADED_FILE_NAME_PATTERN); + + if (\is_callable($pattern)) { + $entityInstance = $entityDto->getInstance(); + $originalCallback = $pattern; + $pattern = static function (UploadedFile $file) use ($originalCallback, $entityInstance) { + return $originalCallback($file, $entityInstance); + }; + } + + $field->setFormTypeOption('upload_filename', $pattern); + + // this check is needed to avoid displaying broken links when file properties are optional + if (null === $formattedValue || '' === $formattedValue || (\is_array($formattedValue) && 0 === \count($formattedValue)) || $formattedValue === rtrim($configuredBasePath ?? '', '/')) { + $field->setTemplateName('label/empty'); + } + + if (!\in_array($context->getCrud()->getCurrentPage(), [Crud::PAGE_EDIT, Crud::PAGE_NEW], true)) { + return; + } + + $relativeUploadDir = $field->getCustomOption(FileField::OPTION_UPLOAD_DIR); + if (null === $relativeUploadDir) { + throw new \InvalidArgumentException(sprintf('The "%s" file field must define the directory where the files are uploaded using the setUploadDir() method.', $field->getProperty())); + } + + if (null !== $filesystem) { + // For Flysystem, use the upload dir as-is (it's a Flysystem path, not a local path) + $relativeUploadDir = u($relativeUploadDir)->trimStart('/')->ensureEnd('/')->toString(); + $field->setFormTypeOption('upload_dir', $relativeUploadDir); + $field->setFormTypeOption('flysystem_storage', $filesystem); + $field->setFormTypeOption('flysystem_url_prefix', $flysystemUrlPrefix); + + // Override upload callables to use Flysystem + $field->setFormTypeOption('upload_new', static function (UploadedFile $file, string $uploadDir, string $fileName) use ($filesystem) { + $path = rtrim($uploadDir, '/').'/'.$fileName; + $stream = fopen($file->getPathname(), 'r'); + try { + $filesystem->writeStream($path, $stream); + } finally { + if (\is_resource($stream)) { + fclose($stream); + } + } + }); + + $field->setFormTypeOption('upload_delete', static function (FlysystemFile|File $file) use ($filesystem) { + $path = $file instanceof FlysystemFile ? $file->getPathname() : $file->getFilename(); + try { + $filesystem->delete($path); + } catch (\Throwable) { + // ignore delete errors for remote storage + } + }); + + $field->setFormTypeOption('upload_validate', static function (string $filename) use ($filesystem) { + if (!$filesystem->fileExists($filename)) { + return $filename; + } + + $index = 1; + $pathInfo = pathinfo($filename); + $dir = '.' === $pathInfo['dirname'] ? '' : $pathInfo['dirname'].'/'; + while ($filesystem->fileExists($filename = sprintf('%s%s_%d.%s', $dir, $pathInfo['filename'], $index, $pathInfo['extension']))) { + ++$index; + } + + return $filename; + }); + + // Disable download_path for Flysystem (URLs are built via flysystem_url_prefix) + $field->setFormTypeOption('download_path', null); + } else { + $relativeUploadDir = u($relativeUploadDir)->trimStart(\DIRECTORY_SEPARATOR)->ensureEnd(\DIRECTORY_SEPARATOR)->toString(); + $isStreamWrapper = filter_var($relativeUploadDir, \FILTER_VALIDATE_URL); + if (false !== $isStreamWrapper) { + $absoluteUploadDir = $relativeUploadDir; + } else { + $absoluteUploadDir = u($relativeUploadDir)->ensureStart($this->projectDir.\DIRECTORY_SEPARATOR)->toString(); + } + $field->setFormTypeOption('upload_dir', $absoluteUploadDir); + } + + $mimeTypes = $field->getCustomOption(FileField::OPTION_MIME_TYPES); + if (null !== $mimeTypes) { + $field->setFormTypeOption('attr.accept', $mimeTypes); + + $processedMimeTypes = []; + foreach (explode(',', $mimeTypes) as $token) { + $token = trim($token); + if (str_starts_with($token, '.')) { + $processedMimeTypes = array_merge($processedMimeTypes, MimeTypes::getDefault()->getMimeTypes(ltrim($token, '.'))); + } else { + $processedMimeTypes[] = $token; + } + } + + if ([] !== $processedMimeTypes) { + $constraints = $field->getCustomOption(FileField::OPTION_FILE_CONSTRAINTS); + $mimeTypesMessage = $field->getCustomOption(FileField::OPTION_MIME_TYPES_MESSAGE); + $constraints[] = new FileConstraint(mimeTypes: $processedMimeTypes, mimeTypesMessage: $mimeTypesMessage); + $field->setCustomOption(FileField::OPTION_FILE_CONSTRAINTS, $constraints); + } + } + + $maxSize = $field->getCustomOption(FileField::OPTION_MAX_SIZE); + if (null !== $maxSize) { + $constraints = $field->getCustomOption(FileField::OPTION_FILE_CONSTRAINTS); + $maxSizeMessage = $field->getCustomOption(FileField::OPTION_MAX_SIZE_MESSAGE); + $constraints[] = new FileConstraint(maxSize: $maxSize, maxSizeMessage: $maxSizeMessage); + $field->setCustomOption(FileField::OPTION_FILE_CONSTRAINTS, $constraints); + } + + $field->setFormTypeOption('file_constraints', $field->getCustomOption(FileField::OPTION_FILE_CONSTRAINTS)); + $field->setFormTypeOption('replaced_file_behavior', $field->getCustomOption(FileField::OPTION_REPLACED_FILE_BEHAVIOR)); + $field->setFormTypeOption('allow_delete', $field->getCustomOption(FileField::OPTION_DELETABLE)); + $field->setFormTypeOption('allow_view', $field->getCustomOption(FileField::OPTION_VIEWABLE)); + $field->setFormTypeOption('allow_download', $field->getCustomOption(FileField::OPTION_DOWNLOADABLE)); + } + + private function resolveFlysystemStorage(string $storageName): FilesystemOperator + { + if (!interface_exists(FilesystemOperator::class)) { + throw new \LogicException(sprintf('You configured Flysystem storage "%s" but the "league/flysystem-bundle" package is not installed. Run "composer require league/flysystem-bundle".', $storageName)); + } + + if (null === $this->flysystemLocator || !$this->flysystemLocator->has($storageName)) { + throw new \InvalidArgumentException(sprintf('The Flysystem storage "%s" is not configured. Make sure the service exists and implements "%s".', $storageName, FilesystemOperator::class)); + } + + return $this->flysystemLocator->get($storageName); + } + + /** + * @param array|null $files + * + * @return array + */ + private function getFilesPaths(?array $files, ?string $basePath): array + { + $filesPaths = []; + foreach ($files as $file) { + $filesPaths[] = $this->getFilePath($file, $basePath); + } + + return $filesPaths; + } + + private function getFilePath(?string $filePath, ?string $basePath): ?string + { + // add the base path only to files that are not absolute URLs (http or https) or protocol-relative URLs (//) + if (null === $filePath || 0 !== preg_match('/^(http[s]?|\/\/)/i', $filePath)) { + return $filePath; + } + + // remove project path from filepath + $filePath = str_replace($this->projectDir.\DIRECTORY_SEPARATOR.'public'.\DIRECTORY_SEPARATOR, '', $filePath); + + return isset($basePath) + ? rtrim($basePath, '/').'/'.ltrim($filePath, '/') + : '/'.ltrim($filePath, '/'); + } + + /** + * @param array|null $files + * + * @return array + */ + private function getFlysystemFilesPaths(?array $files, string $urlPrefix): array + { + $filesPaths = []; + foreach ($files as $file) { + $filesPaths[] = $this->getFlysystemFilePath($file, $urlPrefix); + } + + return $filesPaths; + } + + private function getFlysystemFilePath(?string $filePath, string $urlPrefix): ?string + { + if (null === $filePath) { + return null; + } + + // If it's already an absolute URL, return as-is + if (0 !== preg_match('/^(http[s]?|\/\/)/i', $filePath)) { + return $filePath; + } + + return rtrim($urlPrefix, '/').'/'.ltrim($filePath, '/'); + } +} diff --git a/src/Field/Configurator/ImageConfigurator.php b/src/Field/Configurator/ImageConfigurator.php index a9c2f389c0..061ca92b16 100644 --- a/src/Field/Configurator/ImageConfigurator.php +++ b/src/Field/Configurator/ImageConfigurator.php @@ -8,6 +8,13 @@ use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto; use EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto; use EasyCorp\Bundle\EasyAdminBundle\Field\ImageField; +use EasyCorp\Bundle\EasyAdminBundle\Form\Type\Model\FlysystemFile; +use League\Flysystem\FilesystemOperator; +use Psr\Container\ContainerInterface; +use Symfony\Component\HttpFoundation\File\File; +use Symfony\Component\HttpFoundation\File\UploadedFile; +use Symfony\Component\Mime\MimeTypes; +use Symfony\Component\Validator\Constraints\File as FileConstraint; use function Symfony\Component\String\u; /** @@ -15,8 +22,10 @@ */ final readonly class ImageConfigurator implements FieldConfiguratorInterface { - public function __construct(private string $projectDir) - { + public function __construct( + private string $projectDir, + private ?ContainerInterface $flysystemLocator = null, + ) { } public function supports(FieldDto $field, EntityDto $entityDto): bool @@ -26,11 +35,25 @@ public function supports(FieldDto $field, EntityDto $entityDto): bool public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $context): void { + $flysystemStorageName = $field->getCustomOption(ImageField::OPTION_FLYSYSTEM_STORAGE); + $flysystemUrlPrefix = $field->getCustomOption(ImageField::OPTION_FLYSYSTEM_URL_PREFIX); + $filesystem = null; + + if (null !== $flysystemStorageName) { + $filesystem = $this->resolveFlysystemStorage($flysystemStorageName); + } + $configuredBasePath = $field->getCustomOption(ImageField::OPTION_BASE_PATH); - $formattedValue = \is_array($field->getValue()) - ? $this->getImagesPaths($field->getValue(), $configuredBasePath) - : $this->getImagePath($field->getValue(), $configuredBasePath); + if (null !== $filesystem && null !== $flysystemUrlPrefix) { + $formattedValue = \is_array($field->getValue()) + ? $this->getFlysystemImagesPaths($field->getValue(), $flysystemUrlPrefix) + : $this->getFlysystemImagePath($field->getValue(), $flysystemUrlPrefix); + } else { + $formattedValue = \is_array($field->getValue()) + ? $this->getImagesPaths($field->getValue(), $configuredBasePath) + : $this->getImagePath($field->getValue(), $configuredBasePath); + } $field->setFormattedValue($formattedValue); $field->setFormTypeOption('upload_filename', $field->getCustomOption(ImageField::OPTION_UPLOADED_FILE_NAME_PATTERN)); @@ -48,16 +71,112 @@ public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $c if (null === $relativeUploadDir) { throw new \InvalidArgumentException(sprintf('The "%s" image field must define the directory where the images are uploaded using the setUploadDir() method.', $field->getProperty())); } - $relativeUploadDir = u($relativeUploadDir)->trimStart(\DIRECTORY_SEPARATOR)->ensureEnd(\DIRECTORY_SEPARATOR)->toString(); - $isStreamWrapper = filter_var($relativeUploadDir, \FILTER_VALIDATE_URL); - if (false !== $isStreamWrapper) { - $absoluteUploadDir = $relativeUploadDir; + + if (null !== $filesystem) { + // For Flysystem, use the upload dir as-is (it's a Flysystem path, not a local path) + $relativeUploadDir = u($relativeUploadDir)->trimStart('/')->ensureEnd('/')->toString(); + $field->setFormTypeOption('upload_dir', $relativeUploadDir); + $field->setFormTypeOption('flysystem_storage', $filesystem); + $field->setFormTypeOption('flysystem_url_prefix', $flysystemUrlPrefix); + + // Override upload callables to use Flysystem + $field->setFormTypeOption('upload_new', static function (UploadedFile $file, string $uploadDir, string $fileName) use ($filesystem) { + $path = rtrim($uploadDir, '/').'/'.$fileName; + $stream = fopen($file->getPathname(), 'r'); + try { + $filesystem->writeStream($path, $stream); + } finally { + if (\is_resource($stream)) { + fclose($stream); + } + } + }); + + $field->setFormTypeOption('upload_delete', static function (FlysystemFile|File $file) use ($filesystem) { + $path = $file instanceof FlysystemFile ? $file->getPathname() : $file->getFilename(); + try { + $filesystem->delete($path); + } catch (\Throwable) { + // ignore delete errors for remote storage + } + }); + + $field->setFormTypeOption('upload_validate', static function (string $filename) use ($filesystem) { + if (!$filesystem->fileExists($filename)) { + return $filename; + } + + $index = 1; + $pathInfo = pathinfo($filename); + $dir = '.' === $pathInfo['dirname'] ? '' : $pathInfo['dirname'].'/'; + while ($filesystem->fileExists($filename = sprintf('%s%s_%d.%s', $dir, $pathInfo['filename'], $index, $pathInfo['extension']))) { + ++$index; + } + + return $filename; + }); + + // Disable download_path for Flysystem (URLs are built via flysystem_url_prefix) + $field->setFormTypeOption('download_path', null); } else { - $absoluteUploadDir = u($relativeUploadDir)->ensureStart($this->projectDir.\DIRECTORY_SEPARATOR)->toString(); + $relativeUploadDir = u($relativeUploadDir)->trimStart(\DIRECTORY_SEPARATOR)->ensureEnd(\DIRECTORY_SEPARATOR)->toString(); + $isStreamWrapper = filter_var($relativeUploadDir, \FILTER_VALIDATE_URL); + if (false !== $isStreamWrapper) { + $absoluteUploadDir = $relativeUploadDir; + } else { + $absoluteUploadDir = u($relativeUploadDir)->ensureStart($this->projectDir.\DIRECTORY_SEPARATOR)->toString(); + } + $field->setFormTypeOption('upload_dir', $absoluteUploadDir); + } + + $mimeTypes = $field->getCustomOption(ImageField::OPTION_MIME_TYPES); + if (null !== $mimeTypes) { + $field->setFormTypeOption('attr.accept', $mimeTypes); + + $processedMimeTypes = []; + foreach (explode(',', $mimeTypes) as $token) { + $token = trim($token); + if (str_starts_with($token, '.')) { + $processedMimeTypes = array_merge($processedMimeTypes, MimeTypes::getDefault()->getMimeTypes(ltrim($token, '.'))); + } else { + $processedMimeTypes[] = $token; + } + } + + if ([] !== $processedMimeTypes) { + $constraints = $field->getCustomOption(ImageField::OPTION_FILE_CONSTRAINTS); + $mimeTypesMessage = $field->getCustomOption(ImageField::OPTION_MIME_TYPES_MESSAGE); + $constraints[] = new FileConstraint(mimeTypes: $processedMimeTypes, mimeTypesMessage: $mimeTypesMessage); + $field->setCustomOption(ImageField::OPTION_FILE_CONSTRAINTS, $constraints); + } + } + + $maxSize = $field->getCustomOption(ImageField::OPTION_MAX_SIZE); + if (null !== $maxSize) { + $constraints = $field->getCustomOption(ImageField::OPTION_FILE_CONSTRAINTS); + $maxSizeMessage = $field->getCustomOption(ImageField::OPTION_MAX_SIZE_MESSAGE); + $constraints[] = new FileConstraint(maxSize: $maxSize, maxSizeMessage: $maxSizeMessage); + $field->setCustomOption(ImageField::OPTION_FILE_CONSTRAINTS, $constraints); } - $field->setFormTypeOption('upload_dir', $absoluteUploadDir); $field->setFormTypeOption('file_constraints', $field->getCustomOption(ImageField::OPTION_FILE_CONSTRAINTS)); + $field->setFormTypeOption('replaced_file_behavior', $field->getCustomOption(ImageField::OPTION_REPLACED_FILE_BEHAVIOR)); + $field->setFormTypeOption('allow_delete', $field->getCustomOption(ImageField::OPTION_DELETABLE)); + $field->setFormTypeOption('allow_view', $field->getCustomOption(ImageField::OPTION_VIEWABLE)); + $field->setFormTypeOption('allow_download', $field->getCustomOption(ImageField::OPTION_DOWNLOADABLE)); + } + + private function resolveFlysystemStorage(string $storageName): FilesystemOperator + { + if (!interface_exists(FilesystemOperator::class)) { + throw new \LogicException(sprintf('You configured Flysystem storage "%s" but the "league/flysystem-bundle" package is not installed. Run "composer require league/flysystem-bundle".', $storageName)); + } + + if (null === $this->flysystemLocator || !$this->flysystemLocator->has($storageName)) { + throw new \InvalidArgumentException(sprintf('The Flysystem storage "%s" is not configured. Make sure the service exists and implements "%s".', $storageName, FilesystemOperator::class)); + } + + return $this->flysystemLocator->get($storageName); } /** @@ -89,4 +208,33 @@ private function getImagePath(?string $imagePath, ?string $basePath): ?string ? rtrim($basePath, '/').'/'.ltrim($imagePath, '/') : '/'.ltrim($imagePath, '/'); } + + /** + * @param array|null $images + * + * @return array + */ + private function getFlysystemImagesPaths(?array $images, string $urlPrefix): array + { + $imagesPaths = []; + foreach ($images as $image) { + $imagesPaths[] = $this->getFlysystemImagePath($image, $urlPrefix); + } + + return $imagesPaths; + } + + private function getFlysystemImagePath(?string $imagePath, string $urlPrefix): ?string + { + if (null === $imagePath) { + return null; + } + + // If it's already an absolute URL, return as-is + if (0 !== preg_match('/^(http[s]?|\/\/)/i', $imagePath)) { + return $imagePath; + } + + return rtrim($urlPrefix, '/').'/'.ltrim($imagePath, '/'); + } } diff --git a/src/Field/FileField.php b/src/Field/FileField.php new file mode 100644 index 0000000000..a350288903 --- /dev/null +++ b/src/Field/FileField.php @@ -0,0 +1,258 @@ + + */ +final class FileField implements FieldInterface +{ + use FieldTrait; + + public const OPTION_BASE_PATH = 'basePath'; + public const OPTION_UPLOAD_DIR = 'uploadDir'; + public const OPTION_UPLOADED_FILE_NAME_PATTERN = 'uploadedFileNamePattern'; + public const OPTION_FILE_CONSTRAINTS = 'fileConstraints'; + public const OPTION_REPLACED_FILE_BEHAVIOR = 'replacedFileBehavior'; + public const OPTION_DELETABLE = 'deletable'; + public const OPTION_VIEWABLE = 'viewable'; + public const OPTION_DOWNLOADABLE = 'downloadable'; + public const OPTION_MIME_TYPES = 'mimeTypes'; + public const OPTION_MIME_TYPES_MESSAGE = 'mimeTypesMessage'; + public const OPTION_MAX_SIZE = 'maxSize'; + public const OPTION_MAX_SIZE_MESSAGE = 'maxSizeMessage'; + public const OPTION_FLYSYSTEM_STORAGE = 'flysystemStorage'; + public const OPTION_FLYSYSTEM_URL_PREFIX = 'flysystemUrlPrefix'; + + public static function new(string $propertyName, TranslatableInterface|string|bool|null $label = null): self + { + return (new self()) + ->setProperty($propertyName) + ->setLabel($label) + ->setTemplateName('crud/field/file') + ->setFormType(FileUploadType::class) + ->addCssClass('field-file') + ->addJsFiles(Asset::fromEasyAdminAssetPackage('field-file-upload.js')) + ->setDefaultColumns('col-md-7 col-xxl-5') + ->setCustomOption(self::OPTION_BASE_PATH, null) + ->setCustomOption(self::OPTION_UPLOAD_DIR, null) + ->setCustomOption(self::OPTION_UPLOADED_FILE_NAME_PATTERN, '[name].[extension]') + ->setCustomOption(self::OPTION_FILE_CONSTRAINTS, []) + ->setCustomOption(self::OPTION_MIME_TYPES, null) + ->setCustomOption(self::OPTION_REPLACED_FILE_BEHAVIOR, ReplacedFileBehavior::DELETE) + ->setCustomOption(self::OPTION_VIEWABLE, true) + ->setCustomOption(self::OPTION_DOWNLOADABLE, true) + ->setCustomOption(self::OPTION_DELETABLE, true) + ->setCustomOption(self::OPTION_MIME_TYPES_MESSAGE, null) + ->setCustomOption(self::OPTION_MAX_SIZE, null) + ->setCustomOption(self::OPTION_MAX_SIZE_MESSAGE, null) + ->setCustomOption(self::OPTION_FLYSYSTEM_STORAGE, null) + ->setCustomOption(self::OPTION_FLYSYSTEM_URL_PREFIX, null); + } + + /** + * Sets the path prepended to the file name to build the URL used + * to display the file in the detail and index pages (e.g. 'uploads/files/'). + */ + public function setBasePath(string $path): self + { + $this->setCustomOption(self::OPTION_BASE_PATH, $path); + + return $this; + } + + /** + * Relative to project's root directory (e.g. use 'public/uploads/' for `/public/uploads/`) + * Default upload dir: `/public/uploads/files/`. + */ + public function setUploadDir(string $uploadDirPath): self + { + $this->setCustomOption(self::OPTION_UPLOAD_DIR, $uploadDirPath); + + return $this; + } + + /** + * @param string|\Closure(UploadedFile, object): string $patternOrCallable + * + * If it's a string, uploaded files will be renamed according to the given pattern. + * The pattern can include the following special values: + * + * [DD] [MM] [YYYY] [YY] [hh] [mm] [ss] [timestamp] + * [name] [slug] [extension] [contenthash] + * [randomhash] [uuid] [ulid] + * + * e.g. [YYYY]/[MM]/[DD]/[slug]-[contenthash].[extension] + * + * If it's a callable, you will be passed the UploadedFile instance and the + * current entity instance, and you must return a string with the new filename + * (which can include subdirectories). On the NEW page, the entity is a fresh + * instance (possibly without an ID). On the EDIT page, it has its current DB values. + * Example: + * + * fn (UploadedFile $file, MyEntity $entity) => sprintf('%s/%s.%s', $entity->getSlug(), $file->getFilename(), $file->guessExtension()) + */ + public function setUploadedFileNamePattern(string|\Closure $patternOrCallable): self + { + $this->setCustomOption(self::OPTION_UPLOADED_FILE_NAME_PATTERN, $patternOrCallable); + + return $this; + } + + /** + * @param Constraint|array $constraints + * + * Define constraints to be validated on the FileType + */ + public function setFileConstraints($constraints): self + { + $this->setCustomOption(self::OPTION_FILE_CONSTRAINTS, $constraints); + + return $this; + } + + /** + * Defines the allowed MIME types for this file (by default, all types are accepted). + * + * @param string $mimeTypes a comma-separated list of one or more file types. + * You can use any value considered valid in the HTML `accept` attribute + * https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/accept + * Examples: + * + * '.pdf' (single extension) + * '.doc,.docx' (multiple extensions) + * 'image/*' (any image type) + * 'image/png,image/jpeg' (specific MIME types) + * 'video/*' (any video type) + * 'audio/*' (any audio type) + * '.pdf,image/*' (mix of extensions and MIME types) + * @param string|null $errorMessage Custom error message shown when the MIME type is invalid. + * Available placeholders: + * + * {{ file }} // absolute file path + * {{ name }} // base file name + * {{ type }} // the MIME type of the given file + * {{ types }} // the list of allowed MIME types + */ + public function mimeTypes(string $mimeTypes, ?string $errorMessage = null): self + { + $this->setCustomOption(self::OPTION_MIME_TYPES, $mimeTypes); + $this->setCustomOption(self::OPTION_MIME_TYPES_MESSAGE, $errorMessage); + + return $this; + } + + /** + * Sets the maximum allowed size per uploaded file. + * + * @param int|string $maxSize an integer (bytes) or a suffixed string: `'200k'`, `'2M'`, `'1G'` (SI units) or `'1Ki'`, `'1Mi'` (binary units) + * @param string|null $maxSizeMessage Custom error message shown when the file exceeds the maximum size. + * Available placeholders: + * + * {{ file }} // absolute file path + * {{ limit }} // maximum file size allowed + * {{ name }} // base file name + * {{ size }} // file size of the given file + * {{ suffix }} // suffix for the used file size unit + */ + public function maxSize(int|string $maxSize, ?string $maxSizeMessage = null): self + { + $this->setCustomOption(self::OPTION_MAX_SIZE, $maxSize); + $this->setCustomOption(self::OPTION_MAX_SIZE_MESSAGE, $maxSizeMessage); + + return $this; + } + + /** + * When a file is replaced by uploading a new one, the old file is deleted + * from the filesystem (this is the default behavior). + */ + public function deleteReplacedFile(): self + { + $this->setCustomOption(self::OPTION_REPLACED_FILE_BEHAVIOR, ReplacedFileBehavior::DELETE); + + return $this; + } + + /** + * When a file is replaced by uploading a new one, the old file is kept + * in the filesystem (renamed to avoid collisions). + */ + public function keepReplacedFile(): self + { + $this->setCustomOption(self::OPTION_REPLACED_FILE_BEHAVIOR, ReplacedFileBehavior::KEEP); + + return $this; + } + + /** + * When a file is replaced by uploading a new one, the old file is kept + * and an exception is thrown if the new file name conflicts with an existing one. + */ + public function keepReplacedFileOrFail(): self + { + $this->setCustomOption(self::OPTION_REPLACED_FILE_BEHAVIOR, ReplacedFileBehavior::KEEP_OR_FAIL); + + return $this; + } + + /** + * If true (default), a link to view the file is displayed next to the form field. + */ + public function isViewable(bool $isViewable = true): self + { + $this->setCustomOption(self::OPTION_VIEWABLE, $isViewable); + + return $this; + } + + /** + * If true (default), a link to download the file is displayed next to the form field. + */ + public function isDownloadable(bool $isDownloadable = true): self + { + $this->setCustomOption(self::OPTION_DOWNLOADABLE, $isDownloadable); + + return $this; + } + + /** + * If true (default), a button to delete the file is displayed next to the form field. + */ + public function isDeletable(bool $isDeletable = true): self + { + $this->setCustomOption(self::OPTION_DELETABLE, $isDeletable); + + return $this; + } + + /** + * Sets the Flysystem storage service ID to use for uploading/deleting files + * (e.g. 'default.storage' as registered by league/flysystem-bundle). + */ + public function setFlysystemStorage(string $storageName): self + { + $this->setCustomOption(self::OPTION_FLYSYSTEM_STORAGE, $storageName); + + return $this; + } + + /** + * Sets the URL prefix used to generate public URLs for files stored in Flysystem + * (e.g. 'https://cdn.example.com/uploads'). + */ + public function setFlysystemUrlPrefix(string $urlPrefix): self + { + $this->setCustomOption(self::OPTION_FLYSYSTEM_URL_PREFIX, $urlPrefix); + + return $this; + } +} diff --git a/src/Field/ImageField.php b/src/Field/ImageField.php index 6f4fddb1f0..2e1cd56bef 100644 --- a/src/Field/ImageField.php +++ b/src/Field/ImageField.php @@ -3,9 +3,11 @@ namespace EasyCorp\Bundle\EasyAdminBundle\Field; use EasyCorp\Bundle\EasyAdminBundle\Config\Asset; +use EasyCorp\Bundle\EasyAdminBundle\Config\Option\ReplacedFileBehavior; use EasyCorp\Bundle\EasyAdminBundle\Config\Option\TextAlign; use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldInterface; use EasyCorp\Bundle\EasyAdminBundle\Form\Type\FileUploadType; +use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Image; use Symfony\Contracts\Translation\TranslatableInterface; @@ -21,6 +23,16 @@ final class ImageField implements FieldInterface public const OPTION_UPLOAD_DIR = 'uploadDir'; public const OPTION_UPLOADED_FILE_NAME_PATTERN = 'uploadedFileNamePattern'; public const OPTION_FILE_CONSTRAINTS = 'fileConstraints'; + public const OPTION_REPLACED_FILE_BEHAVIOR = 'replacedFileBehavior'; + public const OPTION_DELETABLE = 'deletable'; + public const OPTION_VIEWABLE = 'viewable'; + public const OPTION_DOWNLOADABLE = 'downloadable'; + public const OPTION_MIME_TYPES = 'mimeTypes'; + public const OPTION_MIME_TYPES_MESSAGE = 'mimeTypesMessage'; + public const OPTION_MAX_SIZE = 'maxSize'; + public const OPTION_MAX_SIZE_MESSAGE = 'maxSizeMessage'; + public const OPTION_FLYSYSTEM_STORAGE = 'flysystemStorage'; + public const OPTION_FLYSYSTEM_URL_PREFIX = 'flysystemUrlPrefix'; public static function new(string $propertyName, TranslatableInterface|string|bool|null $label = null): self { @@ -36,9 +48,23 @@ public static function new(string $propertyName, TranslatableInterface|string|bo ->setCustomOption(self::OPTION_BASE_PATH, null) ->setCustomOption(self::OPTION_UPLOAD_DIR, null) ->setCustomOption(self::OPTION_UPLOADED_FILE_NAME_PATTERN, '[name].[extension]') - ->setCustomOption(self::OPTION_FILE_CONSTRAINTS, [new Image()]); + ->setCustomOption(self::OPTION_FILE_CONSTRAINTS, [new Image()]) + ->setCustomOption(self::OPTION_MIME_TYPES, 'image/*') + ->setCustomOption(self::OPTION_REPLACED_FILE_BEHAVIOR, ReplacedFileBehavior::DELETE) + ->setCustomOption(self::OPTION_VIEWABLE, true) + ->setCustomOption(self::OPTION_DOWNLOADABLE, true) + ->setCustomOption(self::OPTION_DELETABLE, true) + ->setCustomOption(self::OPTION_MIME_TYPES_MESSAGE, null) + ->setCustomOption(self::OPTION_MAX_SIZE, null) + ->setCustomOption(self::OPTION_MAX_SIZE_MESSAGE, null) + ->setCustomOption(self::OPTION_FLYSYSTEM_STORAGE, null) + ->setCustomOption(self::OPTION_FLYSYSTEM_URL_PREFIX, null); } + /** + * Sets the path prepended to the image name to build the URL used + * to display the image in the detail and index pages (e.g. 'uploads/images/'). + */ public function setBasePath(string $path): self { $this->setCustomOption(self::OPTION_BASE_PATH, $path); @@ -58,20 +84,29 @@ public function setUploadDir(string $uploadDirPath): self } /** - * @param string|\Closure $patternOrCallable + * @param string|\Closure(UploadedFile, object): string $patternOrCallable * - * If it's a string, uploaded files will be renamed according to the given pattern. + * If it's a string, image files will be renamed according to the given pattern. * The pattern can include the following special values: - * [day] [month] [year] [timestamp] - * [name] [slug] [extension] [contenthash] - * [randomhash] [uuid] [ulid] - * (e.g. [year]/[month]/[day]/[slug]-[contenthash].[extension]) * - * If it's a callable, you will be passed the Symfony's UploadedFile instance and you must - * return a string with the new filename. - * (e.g. fn(UploadedFile $file) => sprintf('upload_%d_%s.%s', random_int(1, 999), $file->getFilename(), $file->guessExtension())) + * [DD] [MM] [YYYY] [YY] [hh] [mm] [ss] [timestamp] + * [name] [slug] [extension] [contenthash] + * [randomhash] [uuid] [ulid] + * + * e.g. [YYYY]/[MM]/[DD]/[slug]-[contenthash].[extension] + * + * Note: [day], [month] and [year] are deprecated since 5.1 (use [DD], [MM], [YYYY] instead). + * They will be removed in 6.0. + * + * If it's a callable, you will be passed the UploadedFile instance and the + * current entity instance, and you must return a string with the new filename + * (which can include subdirectories). On the NEW page, the entity is a fresh + * instance (possibly without an ID). On the EDIT page, it has its current DB values. + * Example: + * + * fn (UploadedFile $image, MyEntity $entity) => sprintf('%s/%s.%s', $entity->getSlug(), $image->getFilename(), $image->guessExtension()) */ - public function setUploadedFileNamePattern($patternOrCallable): self + public function setUploadedFileNamePattern(string|\Closure $patternOrCallable): self { $this->setCustomOption(self::OPTION_UPLOADED_FILE_NAME_PATTERN, $patternOrCallable); @@ -90,4 +125,137 @@ public function setFileConstraints($constraints): self return $this; } + + /** + * Defines the allowed MIME types for this image (by default, all image types are accepted). + * + * @param string $mimeTypes a comma-separated list of one or more file types. + * You can use any value considered valid in the HTML `accept` attribute + * https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/accept + * Examples: + * + * '.png' (single extension) + * '.jpg,.jpeg' (multiple extensions) + * 'image/png,image/jpeg' (specific MIME types) + * @param string|null $errorMessage Custom error message shown when the MIME type is invalid. + * Available placeholders: + * + * {{ file }} // absolute file path + * {{ name }} // base file name + * {{ type }} // the MIME type of the given file + * {{ types }} // the list of allowed MIME types + */ + public function mimeTypes(string $mimeTypes, ?string $errorMessage = null): self + { + $this->setCustomOption(self::OPTION_MIME_TYPES, $mimeTypes); + $this->setCustomOption(self::OPTION_MIME_TYPES_MESSAGE, $errorMessage); + + return $this; + } + + /** + * Sets the maximum allowed size for the uploaded image. + * + * @param int|string $maxSize an integer (bytes) or a suffixed string: `'200k'`, `'2M'`, `'1G'` (SI units) or `'1Ki'`, `'1Mi'` (binary units) + * @param string|null $maxSizeMessage Custom error message shown when the image exceeds the maximum size. + * Available placeholders: + * + * {{ file }} // absolute file path + * {{ limit }} // maximum file size allowed + * {{ name }} // base file name + * {{ size }} // file size of the given file + * {{ suffix }} // suffix for the used file size unit + */ + public function maxSize(int|string $maxSize, ?string $maxSizeMessage = null): self + { + $this->setCustomOption(self::OPTION_MAX_SIZE, $maxSize); + $this->setCustomOption(self::OPTION_MAX_SIZE_MESSAGE, $maxSizeMessage); + + return $this; + } + + /** + * When an image is replaced by uploading a new one, the old image is deleted + * from the filesystem (this is the default behavior). + */ + public function deleteReplacedFile(): self + { + $this->setCustomOption(self::OPTION_REPLACED_FILE_BEHAVIOR, ReplacedFileBehavior::DELETE); + + return $this; + } + + /** + * When an image is replaced by uploading a new one, the old image is kept + * in the filesystem (renamed to avoid collisions). + */ + public function keepReplacedFile(): self + { + $this->setCustomOption(self::OPTION_REPLACED_FILE_BEHAVIOR, ReplacedFileBehavior::KEEP); + + return $this; + } + + /** + * When an image is replaced by uploading a new one, the old image is kept + * and an exception is thrown if the new image name conflicts with an existing one. + */ + public function keepReplacedFileOrFail(): self + { + $this->setCustomOption(self::OPTION_REPLACED_FILE_BEHAVIOR, ReplacedFileBehavior::KEEP_OR_FAIL); + + return $this; + } + + /** + * If true (default), a link to view the image is displayed next to the form field. + */ + public function isViewable(bool $isViewable = true): self + { + $this->setCustomOption(self::OPTION_VIEWABLE, $isViewable); + + return $this; + } + + /** + * If true (default), a link to download the image is displayed next to the form field. + */ + public function isDownloadable(bool $isDownloadable = true): self + { + $this->setCustomOption(self::OPTION_DOWNLOADABLE, $isDownloadable); + + return $this; + } + + /** + * If true (default), a button to delete the image is displayed next to the form field. + */ + public function isDeletable(bool $isDeletable = true): self + { + $this->setCustomOption(self::OPTION_DELETABLE, $isDeletable); + + return $this; + } + + /** + * Sets the Flysystem storage service ID to use for uploading/deleting images + * (e.g. 'default.storage' as registered by league/flysystem-bundle). + */ + public function setFlysystemStorage(string $storageName): self + { + $this->setCustomOption(self::OPTION_FLYSYSTEM_STORAGE, $storageName); + + return $this; + } + + /** + * Sets the URL prefix used to generate public URLs for images stored in Flysystem + * (e.g. 'https://cdn.example.com/uploads'). + */ + public function setFlysystemUrlPrefix(string $urlPrefix): self + { + $this->setCustomOption(self::OPTION_FLYSYSTEM_URL_PREFIX, $urlPrefix); + + return $this; + } } diff --git a/src/Form/DataTransformer/StringToFileTransformer.php b/src/Form/DataTransformer/StringToFileTransformer.php index fa337028c9..76584b633d 100644 --- a/src/Form/DataTransformer/StringToFileTransformer.php +++ b/src/Form/DataTransformer/StringToFileTransformer.php @@ -2,6 +2,8 @@ namespace EasyCorp\Bundle\EasyAdminBundle\Form\DataTransformer; +use EasyCorp\Bundle\EasyAdminBundle\Form\Type\Model\FlysystemFile; +use League\Flysystem\FilesystemOperator; use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\Exception\TransformationFailedException; use Symfony\Component\HttpFoundation\File\File; @@ -22,6 +24,7 @@ public function __construct( callable $uploadFilename, callable $uploadValidate, private readonly bool $multiple, + private readonly ?FilesystemOperator $flysystemStorage = null, ) { $this->uploadFilename = $uploadFilename; $this->uploadValidate = $uploadValidate; @@ -47,7 +50,7 @@ public function transform(mixed $value): mixed public function reverseTransform(mixed $value): mixed { if (null === $value || [] === $value) { - return null; + return $this->multiple ? [] : ''; } if (!$this->multiple) { @@ -61,13 +64,13 @@ public function reverseTransform(mixed $value): mixed return array_map([$this, 'doReverseTransform'], $value); } - private function doTransform(mixed $value): ?File + private function doTransform(mixed $value): File|FlysystemFile|null { if (null === $value) { return null; } - if ($value instanceof File) { + if ($value instanceof File || $value instanceof FlysystemFile) { return $value; } @@ -75,6 +78,23 @@ private function doTransform(mixed $value): ?File throw new TransformationFailedException('Expected a string or null.'); } + if (null !== $this->flysystemStorage) { + try { + if ($this->flysystemStorage->fileExists($value)) { + $size = null; + try { + $size = $this->flysystemStorage->fileSize($value); + } catch (\Throwable) { + } + + return new FlysystemFile($value, null, $size); + } + } catch (\Throwable) { + } + + return null; + } + if (is_file($this->uploadDir.$value)) { return new File($this->uploadDir.$value); } @@ -98,10 +118,16 @@ private function doReverseTransform(mixed $value): ?string return ($this->uploadValidate)($filename); } + if ($value instanceof FlysystemFile) { + return $value->getPathname(); + } + if ($value instanceof File) { - return $value->getFilename(); + return str_starts_with($value->getPathname(), $this->uploadDir) + ? mb_substr($value->getPathname(), mb_strlen($this->uploadDir)) + : $value->getFilename(); } - throw new TransformationFailedException('Expected an instance of File or null.'); + throw new TransformationFailedException('Expected an instance of File, FlysystemFile, or null.'); } } diff --git a/src/Form/EventListener/CrudAutocompleteSubscriber.php b/src/Form/EventListener/CrudAutocompleteSubscriber.php index 2b6e8e13e2..6c2e032287 100644 --- a/src/Form/EventListener/CrudAutocompleteSubscriber.php +++ b/src/Form/EventListener/CrudAutocompleteSubscriber.php @@ -31,9 +31,6 @@ public static function getSubscribedEvents(): array ]; } - /** - * @return void - */ public function preSetData(FormEvent $event): void { $form = $event->getForm(); @@ -69,9 +66,6 @@ public function preSetData(FormEvent $event): void $form->add('autocomplete', EntityType::class, $options); } - /** - * @return void - */ public function preSubmit(FormEvent $event): void { $data = $event->getData(); diff --git a/src/Form/EventListener/FormLayoutSubscriber.php b/src/Form/EventListener/FormLayoutSubscriber.php index c241fa5054..0e77491ae5 100644 --- a/src/Form/EventListener/FormLayoutSubscriber.php +++ b/src/Form/EventListener/FormLayoutSubscriber.php @@ -27,8 +27,6 @@ public static function getSubscribedEvents(): array /** * Deal with the errors of fields inside form tabs and fieldsets. This method has to * be executed with a negative priority to make sure that the validation process is done. - * - * @return void */ public function handleLayoutErrors(FormEvent $event): void { diff --git a/src/Form/Type/FileUploadType.php b/src/Form/Type/FileUploadType.php index 0bf59d2dc4..60f4dcdf65 100644 --- a/src/Form/Type/FileUploadType.php +++ b/src/Form/Type/FileUploadType.php @@ -2,13 +2,18 @@ namespace EasyCorp\Bundle\EasyAdminBundle\Form\Type; +use EasyCorp\Bundle\EasyAdminBundle\Config\Option\ReplacedFileBehavior; use EasyCorp\Bundle\EasyAdminBundle\Form\DataTransformer\StringToFileTransformer; use EasyCorp\Bundle\EasyAdminBundle\Form\Type\Model\FileUploadState; +use EasyCorp\Bundle\EasyAdminBundle\Form\Type\Model\FlysystemFile; +use League\Flysystem\FilesystemOperator; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\DataMapperInterface; +use Symfony\Component\Form\Exception\TransformationFailedException; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\FileType; +use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; @@ -39,16 +44,42 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $uploadDir = $options['upload_dir']; $uploadFilename = $options['upload_filename']; $uploadValidate = $options['upload_validate']; + $replacedFileBehavior = $options['replaced_file_behavior']; $allowAdd = $options['allow_add']; + $flysystemStorage = $options['flysystem_storage']; + + if (ReplacedFileBehavior::KEEP === $replacedFileBehavior) { + $uploadValidate = static fn (string $filename): string => $filename; + } elseif (ReplacedFileBehavior::KEEP_OR_FAIL === $replacedFileBehavior) { + if (null !== $flysystemStorage) { + $uploadValidate = static function (string $filename) use ($flysystemStorage): string { + if ($flysystemStorage->fileExists($filename)) { + throw new TransformationFailedException(sprintf('The file "%s" already exists.', basename($filename))); + } + + return $filename; + }; + } else { + $uploadValidate = static function (string $filename): string { + if (file_exists($filename)) { + throw new TransformationFailedException(sprintf('The file "%s" already exists.', basename($filename))); + } + + return $filename; + }; + } + } + $options['constraints'] = (bool) $options['multiple'] ? new All($options['file_constraints']) : $options['file_constraints']; - unset($options['upload_dir'], $options['upload_new'], $options['upload_delete'], $options['upload_filename'], $options['upload_validate'], $options['download_path'], $options['allow_add'], $options['allow_delete'], $options['compound'], $options['file_constraints']); + unset($options['upload_dir'], $options['upload_new'], $options['upload_delete'], $options['upload_filename'], $options['upload_validate'], $options['download_path'], $options['allow_add'], $options['allow_delete'], $options['allow_view'], $options['allow_download'], $options['compound'], $options['file_constraints'], $options['replaced_file_behavior'], $options['flysystem_storage'], $options['flysystem_url_prefix']); $builder->add('file', FileType::class, $options); $builder->add('delete', CheckboxType::class, ['required' => false]); + $builder->add('deleted_files', HiddenType::class, ['required' => false, 'mapped' => false]); $builder->setDataMapper($this); $builder->setAttribute('state', new FileUploadState($allowAdd)); - $builder->addModelTransformer(new StringToFileTransformer($uploadDir, $uploadFilename, $uploadValidate, $options['multiple'])); + $builder->addModelTransformer(new StringToFileTransformer($uploadDir, $uploadFilename, $uploadValidate, $options['multiple'], $flysystemStorage)); } public function buildView(FormView $view, FormInterface $form, array $options): void @@ -70,16 +101,38 @@ public function buildView(FormView $view, FormInterface $form, array $options): } } + $uploadDir = $options['upload_dir']; + $flysystemUrlPrefix = $options['flysystem_url_prefix']; + $currentFileNames = []; + foreach ($currentFiles as $file) { + if ($file instanceof FlysystemFile) { + $currentFileNames[] = $file->getPathname(); + } elseif ($file instanceof File) { + $currentFileNames[] = str_starts_with($file->getPathname(), $uploadDir) + ? mb_substr($file->getPathname(), mb_strlen($uploadDir)) + : $file->getFilename(); + } + } + $view->vars['currentFiles'] = $currentFiles; + $view->vars['currentFileNames'] = $currentFileNames; $view->vars['multiple'] = $options['multiple']; $view->vars['allow_add'] = $options['allow_add']; $view->vars['allow_delete'] = $options['allow_delete']; + $view->vars['allow_view'] = $options['allow_view']; + $view->vars['allow_download'] = $options['allow_download']; $view->vars['download_path'] = $options['download_path']; + $view->vars['flysystem_url_prefix'] = $flysystemUrlPrefix; } public function configureOptions(OptionsResolver $resolver): void { $uploadNew = static function (UploadedFile $file, string $uploadDir, string $fileName) { + $subDir = \dirname($fileName); + if ('.' !== $subDir) { + $uploadDir = rtrim($uploadDir, \DIRECTORY_SEPARATOR).\DIRECTORY_SEPARATOR.$subDir; + $fileName = basename($fileName); + } $file->move($uploadDir, $fileName); }; @@ -120,6 +173,9 @@ public function configureOptions(OptionsResolver $resolver): void 'download_path' => $downloadPath, 'allow_add' => $allowAdd, 'allow_delete' => true, + 'allow_view' => true, + 'allow_download' => true, + 'replaced_file_behavior' => ReplacedFileBehavior::DELETE, 'data_class' => $dataClass, 'empty_data' => $emptyData, 'multiple' => false, @@ -127,6 +183,8 @@ public function configureOptions(OptionsResolver $resolver): void 'error_bubbling' => false, 'allow_file_upload' => true, 'file_constraints' => [], + 'flysystem_storage' => null, + 'flysystem_url_prefix' => null, ]); $resolver->setAllowedTypes('upload_dir', 'string'); @@ -137,9 +195,23 @@ public function configureOptions(OptionsResolver $resolver): void $resolver->setAllowedTypes('download_path', ['null', 'string']); $resolver->setAllowedTypes('allow_add', 'bool'); $resolver->setAllowedTypes('allow_delete', 'bool'); + $resolver->setAllowedTypes('allow_view', 'bool'); + $resolver->setAllowedTypes('allow_download', 'bool'); + $resolver->setAllowedValues('replaced_file_behavior', [ReplacedFileBehavior::DELETE, ReplacedFileBehavior::KEEP, ReplacedFileBehavior::KEEP_OR_FAIL]); $resolver->setAllowedTypes('file_constraints', [Constraint::class, Constraint::class.'[]']); + $resolver->setAllowedTypes('flysystem_storage', ['null', FilesystemOperator::class]); + $resolver->setAllowedTypes('flysystem_url_prefix', ['null', 'string']); $resolver->setNormalizer('upload_dir', function (Options $options, string $value): string { + if (null !== $options['flysystem_storage']) { + // For Flysystem, just ensure trailing separator and skip local filesystem checks + if (\DIRECTORY_SEPARATOR !== mb_substr($value, -1) && '/' !== mb_substr($value, -1)) { + $value .= '/'; + } + + return $value; + } + if (\DIRECTORY_SEPARATOR !== mb_substr($value, -1)) { $value .= \DIRECTORY_SEPARATOR; } @@ -161,15 +233,17 @@ public function configureOptions(OptionsResolver $resolver): void return $value; }); $resolver->setNormalizer('upload_filename', static function (Options $options, $fileNamePatternOrCallable) { - if (\is_callable($fileNamePatternOrCallable)) { - return $fileNamePatternOrCallable; - } + $resolvePatternPlaceholders = static function (string $filename, UploadedFile $file): string { + $uuid = Uuid::v4(); - return static function (UploadedFile $file) use ($fileNamePatternOrCallable) { - return strtr($fileNamePatternOrCallable, [ + return strtr($filename, [ '[contenthash]' => sha1_file($file->getRealPath()), + '[DD]' => date('d'), '[day]' => date('d'), '[extension]' => $file->guessExtension(), + '[hh]' => date('H'), + '[mm]' => date('i'), + '[MM]' => date('m'), '[month]' => date('m'), '[name]' => pathinfo($file->getClientOriginalName(), \PATHINFO_FILENAME), '[randomhash]' => bin2hex(random_bytes(20)), @@ -177,12 +251,35 @@ public function configureOptions(OptionsResolver $resolver): void ->slug(pathinfo($file->getClientOriginalName(), \PATHINFO_FILENAME)) ->lower() ->toString(), + '[ss]' => date('s'), '[timestamp]' => time(), - '[uuid]' => Uuid::v4()->toRfc4122(), + '[uuid]' => $uuid->toRfc4122(), + '[uuid32]' => $uuid->toBase32(), + '[uuid58]' => $uuid->toBase58(), '[ulid]' => new Ulid(), + '[YY]' => date('y'), + '[YYYY]' => date('Y'), '[year]' => date('Y'), ]); }; + + if (\is_callable($fileNamePatternOrCallable)) { + return static function (UploadedFile $file) use ($fileNamePatternOrCallable, $resolvePatternPlaceholders) { + return $resolvePatternPlaceholders($fileNamePatternOrCallable($file), $file); + }; + } + + $deprecatedPlaceholders = ['[day]' => '[DD]', '[month]' => '[MM]', '[year]' => '[YYYY]']; + foreach ($deprecatedPlaceholders as $old => $new) { + if (str_contains($fileNamePatternOrCallable, $old)) { + @trigger_deprecation('easycorp/easyadmin-bundle', '5.1.0', + 'The "%s" placeholder in file upload name patterns is deprecated, use "%s" instead. It will be removed in EasyAdmin 6.0.', $old, $new); + } + } + + return static function (UploadedFile $file) use ($fileNamePatternOrCallable, $resolvePatternPlaceholders) { + return $resolvePatternPlaceholders($fileNamePatternOrCallable, $file); + }; }); $resolver->setNormalizer('allow_add', static function (Options $options, string $value): bool { if ((bool) $value && !$options['multiple']) { @@ -220,11 +317,39 @@ public function mapFormsToData($forms, &$currentFiles): void $state->setUploadedFiles($uploadedFiles); $state->setDelete($children['delete']->getData()); + $deletedFilesJson = $children['deleted_files']->getData(); + $deletedFileNames = \is_string($deletedFilesJson) ? json_decode($deletedFilesJson, true) : []; + $state->setDeletedFiles(\is_array($deletedFileNames) ? $deletedFileNames : []); + if (!$state->isModified()) { return; } - if ($state->isAddAllowed() && !$state->isDelete()) { + if ($state->isDelete()) { + $currentFiles = $uploadedFiles; + } elseif ([] !== $state->getDeletedFiles()) { + if (\is_array($currentFiles)) { + $currentFilesArray = $currentFiles; + } elseif (null !== $currentFiles && false !== $currentFiles) { + $currentFilesArray = [$currentFiles]; + } else { + $currentFilesArray = []; + } + $remainingFiles = array_values(array_filter($currentFilesArray, static function ($file) use ($state) { + $fileName = $file instanceof FlysystemFile ? $file->getPathname() : $file->getFilename(); + + return !\in_array($fileName, $state->getDeletedFiles(), true); + })); + if ($state->isAddAllowed()) { + $currentFiles = array_merge($remainingFiles, $uploadedFiles); + } elseif ([] !== $uploadedFiles) { + $currentFiles = $uploadedFiles; + } else { + // in single-file mode, normalize empty result to null + // (consistent with the delete-all checkbox behavior) + $currentFiles = [] === $remainingFiles ? null : $remainingFiles; + } + } elseif ($state->isAddAllowed()) { $currentFiles = array_merge($currentFiles, $uploadedFiles); } else { $currentFiles = $uploadedFiles; diff --git a/src/Form/Type/Model/FileUploadState.php b/src/Form/Type/Model/FileUploadState.php index ea0f892dff..c32ec35dde 100644 --- a/src/Form/Type/Model/FileUploadState.php +++ b/src/Form/Type/Model/FileUploadState.php @@ -10,7 +10,7 @@ */ class FileUploadState { - /** @var File[] */ + /** @var array */ private array $currentFiles = []; /** @var UploadedFile[] */ @@ -18,12 +18,15 @@ class FileUploadState private bool $delete = false; + /** @var string[] */ + private array $deletedFiles = []; + public function __construct(private bool $allowAdd = false) { } /** - * @return File[] + * @return array */ public function getCurrentFiles(): array { @@ -31,9 +34,9 @@ public function getCurrentFiles(): array } /** - * @param File|array|null $currentFiles + * @param File|FlysystemFile|array|null $currentFiles */ - public function setCurrentFiles(File|array|null $currentFiles): void + public function setCurrentFiles(File|FlysystemFile|array|null $currentFiles): void { if (null === $currentFiles) { $currentFiles = []; @@ -108,8 +111,24 @@ public function setDelete(bool $delete): void $this->delete = $delete; } + /** + * @return string[] + */ + public function getDeletedFiles(): array + { + return $this->deletedFiles; + } + + /** + * @param string[] $deletedFiles + */ + public function setDeletedFiles(array $deletedFiles): void + { + $this->deletedFiles = $deletedFiles; + } + public function isModified(): bool { - return [] !== $this->uploadedFiles || $this->delete; + return [] !== $this->uploadedFiles || $this->delete || [] !== $this->deletedFiles; } } diff --git a/src/Form/Type/Model/FlysystemFile.php b/src/Form/Type/Model/FlysystemFile.php new file mode 100644 index 0000000000..759500ab5c --- /dev/null +++ b/src/Form/Type/Model/FlysystemFile.php @@ -0,0 +1,35 @@ + + */ +class FlysystemFile +{ + public function __construct( + private readonly string $path, + private readonly ?string $filename = null, + private readonly ?int $size = null, + ) { + } + + public function getPathname(): string + { + return $this->path; + } + + public function getFilename(): string + { + return $this->filename ?? basename($this->path); + } + + public function getSize(): ?int + { + return $this->size; + } +} diff --git a/src/Registry/TemplateRegistry.php b/src/Registry/TemplateRegistry.php index 6f82132fea..a60bf2e93b 100644 --- a/src/Registry/TemplateRegistry.php +++ b/src/Registry/TemplateRegistry.php @@ -37,6 +37,7 @@ final class TemplateRegistry 'crud/field/datetimetz' => '@EasyAdmin/crud/field/datetimetz.html.twig', 'crud/field/decimal' => '@EasyAdmin/crud/field/decimal.html.twig', 'crud/field/email' => '@EasyAdmin/crud/field/email.html.twig', + 'crud/field/file' => '@EasyAdmin/crud/field/file.html.twig', 'crud/field/float' => '@EasyAdmin/crud/field/float.html.twig', 'crud/field/generic' => '@EasyAdmin/crud/field/generic.html.twig', 'crud/field/hidden' => '@EasyAdmin/crud/field/hidden.html.twig', diff --git a/src/Twig/Component/Icon.php b/src/Twig/Component/Icon.php index 5215e9b1d9..3d901601eb 100644 --- a/src/Twig/Component/Icon.php +++ b/src/Twig/Component/Icon.php @@ -44,7 +44,7 @@ private function getDefaultIconPrefix(): string private function getIconDto(string $iconName, string $iconSet): IconDto { - if (str_starts_with($iconName, IconSet::Internal.':')) { + if (str_starts_with($iconName, IconSet::Internal.':') || str_starts_with($iconName, 'filetypes:')) { return $this->getInternalIcon($iconName); } diff --git a/src/Twig/EasyAdminTwigExtension.php b/src/Twig/EasyAdminTwigExtension.php index bf769cdccb..34e29dfa74 100644 --- a/src/Twig/EasyAdminTwigExtension.php +++ b/src/Twig/EasyAdminTwigExtension.php @@ -46,6 +46,7 @@ public function getFilters(): array new TwigFilter('ea_filesize', [$this, 'fileSize']), new TwigFilter('ea_as_string', [$this, 'representAsString']), new TwigFilter('ea_html_attrs', [$this, 'processHtmlAttributes']), + new TwigFilter('ea_filetype_icon', [$this, 'getFiletypeIcon']), ]; } @@ -104,20 +105,46 @@ public function processHtmlAttributes(array $attributes): array return $processed; } + public function getFiletypeIcon(string $filename): string + { + $extension = strtolower(pathinfo($filename, \PATHINFO_EXTENSION)); + + return match ($extension) { + 'mp3', 'wav', 'ogg', 'flac', 'aac', 'wma', 'm4a', 'opus', 'aiff' => 'audio', + 'mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'webm', 'm4v', 'mpeg', 'mpg' => 'video', + 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'ico', 'tiff', 'tif', 'avif', 'heic', 'heif' => 'image', + 'pdf' => 'pdf', + 'doc', 'dot', 'docx', 'dotx', 'odt', 'rtf', 'txt' => 'document', + 'xls', 'xlsx', 'xltx', 'xltm', 'ods', 'csv' => 'spreadsheet', + 'ppt', 'pps', 'pot', 'pptx', 'potx', 'potm', 'odp', 'key' => 'presentation', + 'htm', 'html', 'xhtml', 'js', 'ts', 'jsx', 'tsx', 'php', 'py', 'java', 'c', 'cpp', 'h', 'cs', 'rb', 'go', 'rs', 'swift', 'kt', 'sh', 'bash', 'json', 'xml', 'yaml', 'yml', 'toml', 'ini', 'sql', 'css', 'scss', 'less' => 'code', + 'svg', 'ai', 'eps', 'svgz' => 'vector', + 'zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'tgz' => 'zip', + default => 'generic', + }; + } + public function fileSize(int $bytes): string { - $size = ['B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; + $size = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; if (0 === $bytes) { - return '0B'; + return '0 B'; } $factor = (int) floor(log($bytes) / log(1024)); $factor = min($factor, \count($size) - 1); - $scaledValue = (int) ($bytes / (1024 ** $factor)); + $scaledValue = $bytes / (1024 ** $factor); + + if (0 === $factor) { + return sprintf('%d %s', $scaledValue, $size[$factor]); + } + + $scaledValue = round($scaledValue, 1); + $format = 0.0 === fmod($scaledValue, 1.0) ? '%d %s' : '%.1f %s'; - return sprintf('%d%s', $scaledValue, $size[$factor]); + return sprintf($format, $scaledValue, $size[$factor]); } public function representAsString(mixed $value, string|callable|null $toStringMethod = null): string diff --git a/templates/crud/field/file.html.twig b/templates/crud/field/file.html.twig new file mode 100644 index 0000000000..a6f3efb6ad --- /dev/null +++ b/templates/crud/field/file.html.twig @@ -0,0 +1,12 @@ +{# @var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #} +{# @var field \EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto #} +{# @var entity \EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto #} +{% set files = field.formattedValue %} +{% if files is not iterable %} + {% set files = [files] %} +{% endif %} + +{% for file in files %} + {{ file|split('/')|last }} + {% if not loop.last %}
{% endif %} +{% endfor %} diff --git a/templates/crud/form_theme.html.twig b/templates/crud/form_theme.html.twig index 2d123d1c74..898b87eb99 100644 --- a/templates/crud/form_theme.html.twig +++ b/templates/crud/form_theme.html.twig @@ -686,134 +686,95 @@ {% endblock ea_datetime_filter_widget %} {% block ea_fileupload_widget %} -
-
- {% set placeholder = t('action.choose_file', {}, 'EasyAdminBundle') %} - {% set title = '' %} - {% set filesLabel = 'files'|trans({}, 'EasyAdminBundle') %} - {% if currentFiles %} - {% if multiple %} - {% set placeholder = currentFiles|length ~ ' ' ~ filesLabel %} - {% else %} - {% set placeholder = (currentFiles|first).filename %} - {% set title = (currentFiles|first).mTime|date %} - {% endif %} + {% set isFlysystem = flysystem_url_prefix is defined and flysystem_url_prefix is not null %} + +
+ {{ form_widget(form.file, {attr: form.file.vars.attr|merge({class: 'd-none', 'data-ea-fileupload-input': ''})}) }} + +
+ {% set showAddButton = multiple or currentFiles is empty %} + + + {% if multiple and allow_delete %} + {% endif %} -
- {{ form_widget(form.file, {attr: form.file.vars.attr|merge({placeholder: placeholder, title: title, 'data-files-label': filesLabel, class: 'd-none'})}) }} - {# don't pass 'placeholder' as the 2nd argument of form_label(); Twig calls testEmpty() on it internally, which triggers the TranslatableMessage::__toString() deprecation #} - {{ form_label(form.file, null, {label: placeholder, label_attr: {class: 'custom-file-label'}}) }} -
-
- {%- if currentFiles -%} - {% if multiple %} - {{ (currentFiles|reduce((carry, file) => carry + file.size))|ea_filesize }} - {% else %} - {{ (currentFiles|first).size|ea_filesize }} - {% endif %} - {%- endif -%} - {% if allow_delete %} - - {% endif %} - -
- {% if multiple and currentFiles %} -
- - - {% for file in currentFiles %} - - - - - {% endfor %} - -
- {% if download_path %}{% endif %} - - {{ file.filename }} - - {% if download_path %}{% endif %} - {{ file.size|ea_filesize }}
-
- {% endif %} - {% if allow_delete %} -
{{ form_widget(form.delete, {label: false}) }}
- {% endif %} -
- {{ form_errors(form.file) }} -{% endblock %} -{% block TODO_ea_fileupload_widget %} - {% set placeholder = '' %} - {% set title = '' %} - {% set filesLabel = 'files'|trans({}, 'EasyAdminBundle') %} - {% if currentFiles %} - {% if multiple %} - {% set placeholder = currentFiles|length ~ ' ' ~ filesLabel %} - {% else %} - {% set placeholder = (currentFiles|first).filename %} - {% set title = (currentFiles|first).mTime|date %} - {% endif %} - {% endif %} - -
-
- {{ form_widget(form.file, {attr: form.file.vars.attr|merge({placeholder: placeholder, title: title, 'data-files-label': filesLabel, class: 'form-control'})}) }} +
+ {% for i, file in currentFiles %} + {% set fileName = file.filename %} + {% set fileSize = file.size ? file.size|ea_filesize : '' %} + {% set filetypeIcon = fileName|ea_filetype_icon %} + {% set isImage = filetypeIcon == 'image' %} + {% set fileUrl = null %} + {% if isFlysystem %} + {% set fileUrl = flysystem_url_prefix|trim('/') ~ '/' ~ currentFileNames[i] %} + {% elseif download_path %} + {% set fileUrl = asset(download_path ~ currentFileNames[i]) %} + {% endif %} - {% if currentFiles %} - - {% if multiple %} - {{ (currentFiles|reduce((carry, file) => carry + file.size))|ea_filesize }} - {% else %} - {{ (currentFiles|first).size|ea_filesize }} - {% endif %} - - {% endif %} +
+
+ {% if isImage and fileUrl %} + {{ fileName }} + {% else %} + + {% endif %} +
- {% if currentFiles and allow_delete %} - - {% endif %} +
+ {{ fileName }} + {% if fileSize %} + {{ fileSize }} + {% endif %} +
- {% if currentFiles %} - - {% endif %} +
+ {% if allow_view and fileUrl %} + + + + {% endif %} + {% if allow_download and fileUrl %} + + + + {% endif %} + {% if allow_delete %} + + {% endif %} +
+
+ {% endfor %}
- {% if multiple and currentFiles %} -
- - - {% for file in currentFiles %} - - - - - {% endfor %} - -
- {% if download_path %}{% endif %} - - {{ file.filename }} - - {% if download_path %}{% endif %} - {{ file.size|ea_filesize }}
-
- {% endif %} {% if allow_delete %}
{{ form_widget(form.delete, {label: false}) }}
{% endif %} +
{{ form_widget(form.deleted_files) }}
- {{ form_errors(form.file) }} {% endblock %} diff --git a/tests/Unit/Field/Configurator/FileConfiguratorFlysystemTest.php b/tests/Unit/Field/Configurator/FileConfiguratorFlysystemTest.php new file mode 100644 index 0000000000..b12e4b72f3 --- /dev/null +++ b/tests/Unit/Field/Configurator/FileConfiguratorFlysystemTest.php @@ -0,0 +1,320 @@ +filesystem = $this->createMock(FilesystemOperator::class); + + $locator = $this->createMock(ContainerInterface::class); + $locator->method('has')->willReturnCallback(static fn (string $id) => 'default.storage' === $id); + $locator->method('get')->willReturnCallback(fn (string $id) => match ($id) { + 'default.storage' => $this->filesystem, + default => throw new \RuntimeException("Unknown storage: $id"), + }); + + $this->configurator = new FileConfigurator('/project', $locator); + } + + // --- URL generation (INDEX page) --- + + public function testSingleFileUrlWithPrefix(): void + { + $field = FileField::new('photo') + ->setFlysystemStorage('default.storage') + ->setFlysystemUrlPrefix('https://cdn.example.com') + ->setUploadDir('photos/'); + $field->getAsDto()->setValue('photos/cat.jpg'); + + $dto = $this->configure($field); + + $this->assertSame('https://cdn.example.com/photos/cat.jpg', $dto->getFormattedValue()); + } + + public function testMultipleFilesUrlWithPrefix(): void + { + $field = FileField::new('photos') + ->setFlysystemStorage('default.storage') + ->setFlysystemUrlPrefix('https://cdn.example.com') + ->setUploadDir('photos/'); + $field->getAsDto()->setValue(['a.jpg', 'b.jpg']); + + $dto = $this->configure($field); + + $this->assertSame([ + 'https://cdn.example.com/a.jpg', + 'https://cdn.example.com/b.jpg', + ], $dto->getFormattedValue()); + } + + public function testNullValueSetsEmptyTemplate(): void + { + $field = FileField::new('photo') + ->setFlysystemStorage('default.storage') + ->setFlysystemUrlPrefix('https://cdn.example.com') + ->setUploadDir('photos/'); + $field->getAsDto()->setValue(null); + + $dto = $this->configure($field); + + $this->assertNull($dto->getFormattedValue()); + $this->assertSame('label/empty', $dto->getTemplateName()); + } + + public function testAbsoluteUrlReturnedAsIs(): void + { + $field = FileField::new('photo') + ->setFlysystemStorage('default.storage') + ->setFlysystemUrlPrefix('https://cdn.example.com') + ->setUploadDir('photos/'); + $field->getAsDto()->setValue('https://other.com/image.jpg'); + + $dto = $this->configure($field); + + $this->assertSame('https://other.com/image.jpg', $dto->getFormattedValue()); + } + + public function testTrailingSlashNormalization(): void + { + $field = FileField::new('photo') + ->setFlysystemStorage('default.storage') + ->setFlysystemUrlPrefix('https://cdn.example.com/') + ->setUploadDir('photos/'); + $field->getAsDto()->setValue('photos/cat.jpg'); + + $dto = $this->configure($field); + + $this->assertSame('https://cdn.example.com/photos/cat.jpg', $dto->getFormattedValue()); + } + + // --- Form type options (EDIT page) --- + + public function testUploadDirIsFlysystemPath(): void + { + $field = FileField::new('photo') + ->setFlysystemStorage('default.storage') + ->setFlysystemUrlPrefix('https://cdn.example.com') + ->setUploadDir('photos'); + + $dto = $this->configure($field, Crud::PAGE_EDIT); + + $this->assertSame('photos/', $dto->getFormTypeOption('upload_dir')); + } + + public function testFlysystemStorageOption(): void + { + $field = FileField::new('photo') + ->setFlysystemStorage('default.storage') + ->setFlysystemUrlPrefix('https://cdn.example.com') + ->setUploadDir('photos/'); + + $dto = $this->configure($field, Crud::PAGE_EDIT); + + $this->assertSame($this->filesystem, $dto->getFormTypeOption('flysystem_storage')); + } + + public function testFlysystemUrlPrefixPassedThrough(): void + { + $field = FileField::new('photo') + ->setFlysystemStorage('default.storage') + ->setFlysystemUrlPrefix('https://cdn.example.com') + ->setUploadDir('photos/'); + + $dto = $this->configure($field, Crud::PAGE_EDIT); + + $this->assertSame('https://cdn.example.com', $dto->getFormTypeOption('flysystem_url_prefix')); + } + + public function testDownloadPathSetToNull(): void + { + $field = FileField::new('photo') + ->setFlysystemStorage('default.storage') + ->setFlysystemUrlPrefix('https://cdn.example.com') + ->setUploadDir('photos/'); + + $dto = $this->configure($field, Crud::PAGE_EDIT); + + $this->assertNull($dto->getFormTypeOption('download_path')); + } + + public function testUploadCallablesAreSet(): void + { + $field = FileField::new('photo') + ->setFlysystemStorage('default.storage') + ->setFlysystemUrlPrefix('https://cdn.example.com') + ->setUploadDir('photos/'); + + $dto = $this->configure($field, Crud::PAGE_EDIT); + + $this->assertIsCallable($dto->getFormTypeOption('upload_new')); + $this->assertIsCallable($dto->getFormTypeOption('upload_delete')); + $this->assertIsCallable($dto->getFormTypeOption('upload_validate')); + } + + // --- upload_new callable --- + + public function testUploadNewCallsWriteStream(): void + { + $this->filesystem->expects($this->once()) + ->method('writeStream') + ->with( + 'photos/report.pdf', + $this->isType('resource') + ); + + $field = FileField::new('photo') + ->setFlysystemStorage('default.storage') + ->setFlysystemUrlPrefix('https://cdn.example.com') + ->setUploadDir('photos/'); + + $dto = $this->configure($field, Crud::PAGE_EDIT); + + $tmpFile = tempnam(sys_get_temp_dir(), 'ea_test_'); + file_put_contents($tmpFile, 'test content'); + + $uploaded = new UploadedFile($tmpFile, 'report.pdf', 'application/pdf', null, true); + + $uploadNew = $dto->getFormTypeOption('upload_new'); + $uploadNew($uploaded, 'photos/', 'report.pdf'); + + @unlink($tmpFile); + } + + // --- upload_delete callable --- + + public function testUploadDeleteCallsDelete(): void + { + $this->filesystem->expects($this->once()) + ->method('delete') + ->with('photos/cat.jpg'); + + $field = FileField::new('photo') + ->setFlysystemStorage('default.storage') + ->setFlysystemUrlPrefix('https://cdn.example.com') + ->setUploadDir('photos/'); + + $dto = $this->configure($field, Crud::PAGE_EDIT); + + $uploadDelete = $dto->getFormTypeOption('upload_delete'); + $uploadDelete(new FlysystemFile('photos/cat.jpg')); + } + + public function testUploadDeleteSwallowsExceptions(): void + { + $this->filesystem->method('delete') + ->willThrowException(new \RuntimeException('Network error')); + + $field = FileField::new('photo') + ->setFlysystemStorage('default.storage') + ->setFlysystemUrlPrefix('https://cdn.example.com') + ->setUploadDir('photos/'); + + $dto = $this->configure($field, Crud::PAGE_EDIT); + + $uploadDelete = $dto->getFormTypeOption('upload_delete'); + // Should not throw + $uploadDelete(new FlysystemFile('photos/cat.jpg')); + + $this->addToAssertionCount(1); + } + + // --- upload_validate callable --- + + public function testUploadValidateFileDoesNotExistReturnsUnchanged(): void + { + $this->filesystem->method('fileExists')->willReturn(false); + + $field = FileField::new('photo') + ->setFlysystemStorage('default.storage') + ->setFlysystemUrlPrefix('https://cdn.example.com') + ->setUploadDir('photos/'); + + $dto = $this->configure($field, Crud::PAGE_EDIT); + + $validate = $dto->getFormTypeOption('upload_validate'); + $this->assertSame('doc.pdf', $validate('doc.pdf')); + } + + public function testUploadValidateFileExistsAppendsIndex(): void + { + $this->filesystem->method('fileExists') + ->willReturnCallback(static fn (string $path) => match ($path) { + 'doc.pdf' => true, + 'doc_1.pdf' => false, + default => false, + }); + + $field = FileField::new('photo') + ->setFlysystemStorage('default.storage') + ->setFlysystemUrlPrefix('https://cdn.example.com') + ->setUploadDir('photos/'); + + $dto = $this->configure($field, Crud::PAGE_EDIT); + + $validate = $dto->getFormTypeOption('upload_validate'); + $this->assertSame('doc_1.pdf', $validate('doc.pdf')); + } + + public function testUploadValidatePreservesDirectoryPrefix(): void + { + $this->filesystem->method('fileExists') + ->willReturnCallback(static fn (string $path) => match ($path) { + 'uploads/doc.pdf' => true, + 'uploads/doc_1.pdf' => false, + default => false, + }); + + $field = FileField::new('photo') + ->setFlysystemStorage('default.storage') + ->setFlysystemUrlPrefix('https://cdn.example.com') + ->setUploadDir('photos/'); + + $dto = $this->configure($field, Crud::PAGE_EDIT); + + $validate = $dto->getFormTypeOption('upload_validate'); + $this->assertSame('uploads/doc_1.pdf', $validate('uploads/doc.pdf')); + } + + // --- Error paths --- + + public function testFlysystemStorageNotFoundInLocatorThrows(): void + { + $field = FileField::new('photo') + ->setFlysystemStorage('unknown.storage') + ->setFlysystemUrlPrefix('https://cdn.example.com') + ->setUploadDir('photos/'); + + $this->expectException(\InvalidArgumentException::class); + $this->configure($field); + } + + public function testNullLocatorThrows(): void + { + $configurator = new FileConfigurator('/project', null); + + $field = FileField::new('photo') + ->setFlysystemStorage('default.storage') + ->setFlysystemUrlPrefix('https://cdn.example.com') + ->setUploadDir('photos/'); + + $this->configurator = $configurator; + + $this->expectException(\InvalidArgumentException::class); + $this->configure($field); + } +} diff --git a/tests/Unit/Field/FileFieldTest.php b/tests/Unit/Field/FileFieldTest.php new file mode 100644 index 0000000000..4913be2777 --- /dev/null +++ b/tests/Unit/Field/FileFieldTest.php @@ -0,0 +1,362 @@ +configurator = new class implements FieldConfiguratorInterface { + public function supports(FieldDto $field, EntityDto $entityDto): bool + { + return FileField::class === $field->getFieldFqcn(); + } + + public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $context): void + { + // no-op for basic option testing + } + }; + } + + public function testDefaultOptions(): void + { + $field = FileField::new('document'); + $fieldDto = $this->configure($field); + + self::assertNull($fieldDto->getCustomOption(FileField::OPTION_BASE_PATH)); + self::assertNull($fieldDto->getCustomOption(FileField::OPTION_UPLOAD_DIR)); + self::assertSame('[name].[extension]', $fieldDto->getCustomOption(FileField::OPTION_UPLOADED_FILE_NAME_PATTERN)); + self::assertSame(FileUploadType::class, $fieldDto->getFormType()); + self::assertStringContainsString('field-file', $fieldDto->getCssClass()); + } + + public function testDefaultFileConstraints(): void + { + $field = FileField::new('document'); + $fieldDto = $this->configure($field); + + $constraints = $fieldDto->getCustomOption(FileField::OPTION_FILE_CONSTRAINTS); + self::assertIsArray($constraints); + self::assertCount(0, $constraints); + } + + public function testFieldWithNullValue(): void + { + $field = FileField::new('document'); + $field->setValue(null); + $fieldDto = $this->configure($field); + + self::assertNull($fieldDto->getValue()); + } + + public function testFieldWithFilename(): void + { + $field = FileField::new('document'); + $field->setValue('report.pdf'); + $fieldDto = $this->configure($field); + + self::assertSame('report.pdf', $fieldDto->getValue()); + } + + public function testSetBasePath(): void + { + $field = FileField::new('document'); + $field->setBasePath('/uploads/files/'); + $fieldDto = $this->configure($field); + + self::assertSame('/uploads/files/', $fieldDto->getCustomOption(FileField::OPTION_BASE_PATH)); + } + + public function testSetUploadDir(): void + { + $field = FileField::new('document'); + $field->setUploadDir('public/uploads/files/'); + $fieldDto = $this->configure($field); + + self::assertSame('public/uploads/files/', $fieldDto->getCustomOption(FileField::OPTION_UPLOAD_DIR)); + } + + public function testSetUploadedFileNamePatternWithString(): void + { + $field = FileField::new('document'); + $field->setUploadedFileNamePattern('[year]/[month]/[slug].[extension]'); + $fieldDto = $this->configure($field); + + self::assertSame('[year]/[month]/[slug].[extension]', $fieldDto->getCustomOption(FileField::OPTION_UPLOADED_FILE_NAME_PATTERN)); + } + + public function testSetUploadedFileNamePatternWithClosure(): void + { + $pattern = static fn ($file) => 'custom_'.$file->getFilename(); + $field = FileField::new('document'); + $field->setUploadedFileNamePattern($pattern); + $fieldDto = $this->configure($field); + + self::assertSame($pattern, $fieldDto->getCustomOption(FileField::OPTION_UPLOADED_FILE_NAME_PATTERN)); + } + + public function testSetFileConstraintsWithSingleConstraint(): void + { + $constraint = new File(maxSize: '10M'); + + $field = FileField::new('document'); + $field->setFileConstraints($constraint); + $fieldDto = $this->configure($field); + + self::assertSame($constraint, $fieldDto->getCustomOption(FileField::OPTION_FILE_CONSTRAINTS)); + } + + public function testSetFileConstraintsWithMultipleConstraints(): void + { + $constraints = [ + new File(maxSize: '10M'), + new NotNull(), + ]; + $field = FileField::new('document'); + $field->setFileConstraints($constraints); + $fieldDto = $this->configure($field); + + self::assertSame($constraints, $fieldDto->getCustomOption(FileField::OPTION_FILE_CONSTRAINTS)); + } + + public function testUploadPatternPlaceholders(): void + { + $patterns = [ + '[DD]', + '[MM]', + '[YYYY]', + '[YY]', + '[hh]', + '[mm]', + '[ss]', + '[timestamp]', + '[name]', + '[slug]', + '[extension]', + '[contenthash]', + '[randomhash]', + '[uuid]', + '[ulid]', + ]; + + foreach ($patterns as $pattern) { + $field = FileField::new('document'); + $field->setUploadedFileNamePattern($pattern); + $fieldDto = $this->configure($field); + + self::assertSame($pattern, $fieldDto->getCustomOption(FileField::OPTION_UPLOADED_FILE_NAME_PATTERN)); + } + } + + public function testComplexUploadPattern(): void + { + $pattern = '[YYYY]/[MM]/[DD]/[slug]-[contenthash].[extension]'; + $field = FileField::new('document'); + $field->setUploadedFileNamePattern($pattern); + $fieldDto = $this->configure($field); + + self::assertSame($pattern, $fieldDto->getCustomOption(FileField::OPTION_UPLOADED_FILE_NAME_PATTERN)); + } + + public function testSetFileAccept(): void + { + $field = FileField::new('document'); + $field->mimeTypes('.pdf,.docx,.xlsx'); + $fieldDto = $this->configure($field); + + self::assertSame('.pdf,.docx,.xlsx', $fieldDto->getCustomOption(FileField::OPTION_MIME_TYPES)); + } + + public function testDefaultAcceptIsNull(): void + { + $field = FileField::new('document'); + $fieldDto = $this->configure($field); + + self::assertNull($fieldDto->getCustomOption(FileField::OPTION_MIME_TYPES)); + } + + public function testSetFileAcceptWithMimeTypes(): void + { + $field = FileField::new('document'); + $field->mimeTypes('image/*,application/pdf'); + $fieldDto = $this->configure($field); + + self::assertSame('image/*,application/pdf', $fieldDto->getCustomOption(FileField::OPTION_MIME_TYPES)); + } + + public function testSetFileAcceptWithMixedTokens(): void + { + $field = FileField::new('document'); + $field->mimeTypes('.pdf, image/*, .docx'); + $fieldDto = $this->configure($field); + + self::assertSame('.pdf, image/*, .docx', $fieldDto->getCustomOption(FileField::OPTION_MIME_TYPES)); + } + + public function testMimeTypesWithErrorMessage(): void + { + $field = FileField::new('document'); + $field->mimeTypes('.pdf,.docx', 'Only PDF and Word files are allowed (got {{ type }})'); + $fieldDto = $this->configure($field); + + self::assertSame('.pdf,.docx', $fieldDto->getCustomOption(FileField::OPTION_MIME_TYPES)); + self::assertSame('Only PDF and Word files are allowed (got {{ type }})', $fieldDto->getCustomOption(FileField::OPTION_MIME_TYPES_MESSAGE)); + } + + public function testDefaultMimeTypesMessageIsNull(): void + { + $field = FileField::new('document'); + $fieldDto = $this->configure($field); + + self::assertNull($fieldDto->getCustomOption(FileField::OPTION_MIME_TYPES_MESSAGE)); + } + + public function testSetMaxSize(): void + { + $field = FileField::new('document'); + $field->maxSize('10M'); + $fieldDto = $this->configure($field); + + self::assertSame('10M', $fieldDto->getCustomOption(FileField::OPTION_MAX_SIZE)); + self::assertNull($fieldDto->getCustomOption(FileField::OPTION_MAX_SIZE_MESSAGE)); + } + + public function testSetMaxSizeWithInteger(): void + { + $field = FileField::new('document'); + $field->maxSize(1048576); + $fieldDto = $this->configure($field); + + self::assertSame(1048576, $fieldDto->getCustomOption(FileField::OPTION_MAX_SIZE)); + } + + public function testSetMaxSizeWithErrorMessage(): void + { + $field = FileField::new('document'); + $field->maxSize('5M', 'File {{ name }} is too large ({{ size }} {{ suffix }})'); + $fieldDto = $this->configure($field); + + self::assertSame('5M', $fieldDto->getCustomOption(FileField::OPTION_MAX_SIZE)); + self::assertSame('File {{ name }} is too large ({{ size }} {{ suffix }})', $fieldDto->getCustomOption(FileField::OPTION_MAX_SIZE_MESSAGE)); + } + + public function testDefaultMaxSizeIsNull(): void + { + $field = FileField::new('document'); + $fieldDto = $this->configure($field); + + self::assertNull($fieldDto->getCustomOption(FileField::OPTION_MAX_SIZE)); + self::assertNull($fieldDto->getCustomOption(FileField::OPTION_MAX_SIZE_MESSAGE)); + } + + public function testDefaultViewable(): void + { + $field = FileField::new('document'); + $fieldDto = $this->configure($field); + + self::assertTrue($fieldDto->getCustomOption(FileField::OPTION_VIEWABLE)); + } + + public function testIsViewableFalse(): void + { + $field = FileField::new('document'); + $field->isViewable(false); + $fieldDto = $this->configure($field); + + self::assertFalse($fieldDto->getCustomOption(FileField::OPTION_VIEWABLE)); + } + + public function testDefaultDownloadable(): void + { + $field = FileField::new('document'); + $fieldDto = $this->configure($field); + + self::assertTrue($fieldDto->getCustomOption(FileField::OPTION_DOWNLOADABLE)); + } + + public function testIsDownloadableFalse(): void + { + $field = FileField::new('document'); + $field->isDownloadable(false); + $fieldDto = $this->configure($field); + + self::assertFalse($fieldDto->getCustomOption(FileField::OPTION_DOWNLOADABLE)); + } + + public function testDefaultReplacedFileBehavior(): void + { + $field = FileField::new('document'); + $fieldDto = $this->configure($field); + + self::assertSame(ReplacedFileBehavior::DELETE, $fieldDto->getCustomOption(FileField::OPTION_REPLACED_FILE_BEHAVIOR)); + } + + public function testDeleteReplacedFile(): void + { + $field = FileField::new('document'); + $field->deleteReplacedFile(); + $fieldDto = $this->configure($field); + + self::assertSame(ReplacedFileBehavior::DELETE, $fieldDto->getCustomOption(FileField::OPTION_REPLACED_FILE_BEHAVIOR)); + } + + public function testKeepReplacedFile(): void + { + $field = FileField::new('document'); + $field->keepReplacedFile(); + $fieldDto = $this->configure($field); + + self::assertSame(ReplacedFileBehavior::KEEP, $fieldDto->getCustomOption(FileField::OPTION_REPLACED_FILE_BEHAVIOR)); + } + + public function testKeepReplacedFileOrFail(): void + { + $field = FileField::new('document'); + $field->keepReplacedFileOrFail(); + $fieldDto = $this->configure($field); + + self::assertSame(ReplacedFileBehavior::KEEP_OR_FAIL, $fieldDto->getCustomOption(FileField::OPTION_REPLACED_FILE_BEHAVIOR)); + } + + public function testDefaultDeletable(): void + { + $field = FileField::new('document'); + $fieldDto = $this->configure($field); + + self::assertTrue($fieldDto->getCustomOption(FileField::OPTION_DELETABLE)); + } + + public function testIsDeletableFalse(): void + { + $field = FileField::new('document'); + $field->isDeletable(false); + $fieldDto = $this->configure($field); + + self::assertFalse($fieldDto->getCustomOption(FileField::OPTION_DELETABLE)); + } + + public function testIsDeletableTrue(): void + { + $field = FileField::new('document'); + $field->isDeletable(false); + $field->isDeletable(true); + $fieldDto = $this->configure($field); + + self::assertTrue($fieldDto->getCustomOption(FileField::OPTION_DELETABLE)); + } +} diff --git a/tests/Unit/Field/ImageFieldTest.php b/tests/Unit/Field/ImageFieldTest.php index 80a5f1cd63..e00347b455 100644 --- a/tests/Unit/Field/ImageFieldTest.php +++ b/tests/Unit/Field/ImageFieldTest.php @@ -2,6 +2,7 @@ namespace EasyCorp\Bundle\EasyAdminBundle\Tests\Unit\Field; +use EasyCorp\Bundle\EasyAdminBundle\Config\Option\ReplacedFileBehavior; use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext; use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldConfiguratorInterface; use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto; @@ -172,6 +173,13 @@ public function testUploadPatternPlaceholders(): void { // test various placeholders that can be used $patterns = [ + '[DD]', + '[MM]', + '[YYYY]', + '[YY]', + '[hh]', + '[mm]', + '[ss]', '[day]', '[month]', '[year]', @@ -182,6 +190,8 @@ public function testUploadPatternPlaceholders(): void '[contenthash]', '[randomhash]', '[uuid]', + '[uuid32]', + '[uuid58]', '[ulid]', ]; @@ -196,11 +206,171 @@ public function testUploadPatternPlaceholders(): void public function testComplexUploadPattern(): void { - $pattern = '[year]/[month]/[day]/[slug]-[contenthash].[extension]'; + $pattern = '[YYYY]/[MM]/[DD]/[slug]-[contenthash].[extension]'; $field = ImageField::new('image'); $field->setUploadedFileNamePattern($pattern); $fieldDto = $this->configure($field); self::assertSame($pattern, $fieldDto->getCustomOption(ImageField::OPTION_UPLOADED_FILE_NAME_PATTERN)); } + + public function testDefaultMimeTypes(): void + { + $field = ImageField::new('image'); + $fieldDto = $this->configure($field); + + self::assertSame('image/*', $fieldDto->getCustomOption(ImageField::OPTION_MIME_TYPES)); + } + + public function testMimeTypesWithErrorMessage(): void + { + $field = ImageField::new('image'); + $field->mimeTypes('image/png,image/jpeg', 'Only PNG and JPEG images are allowed (got {{ type }})'); + $fieldDto = $this->configure($field); + + self::assertSame('image/png,image/jpeg', $fieldDto->getCustomOption(ImageField::OPTION_MIME_TYPES)); + self::assertSame('Only PNG and JPEG images are allowed (got {{ type }})', $fieldDto->getCustomOption(ImageField::OPTION_MIME_TYPES_MESSAGE)); + } + + public function testDefaultMimeTypesMessageIsNull(): void + { + $field = ImageField::new('image'); + $fieldDto = $this->configure($field); + + self::assertNull($fieldDto->getCustomOption(ImageField::OPTION_MIME_TYPES_MESSAGE)); + } + + public function testSetMaxSize(): void + { + $field = ImageField::new('image'); + $field->maxSize('5M'); + $fieldDto = $this->configure($field); + + self::assertSame('5M', $fieldDto->getCustomOption(ImageField::OPTION_MAX_SIZE)); + self::assertNull($fieldDto->getCustomOption(ImageField::OPTION_MAX_SIZE_MESSAGE)); + } + + public function testSetMaxSizeWithInteger(): void + { + $field = ImageField::new('image'); + $field->maxSize(2097152); + $fieldDto = $this->configure($field); + + self::assertSame(2097152, $fieldDto->getCustomOption(ImageField::OPTION_MAX_SIZE)); + } + + public function testSetMaxSizeWithErrorMessage(): void + { + $field = ImageField::new('image'); + $field->maxSize('2M', 'Image {{ name }} is too large ({{ size }} {{ suffix }})'); + $fieldDto = $this->configure($field); + + self::assertSame('2M', $fieldDto->getCustomOption(ImageField::OPTION_MAX_SIZE)); + self::assertSame('Image {{ name }} is too large ({{ size }} {{ suffix }})', $fieldDto->getCustomOption(ImageField::OPTION_MAX_SIZE_MESSAGE)); + } + + public function testDefaultMaxSizeIsNull(): void + { + $field = ImageField::new('image'); + $fieldDto = $this->configure($field); + + self::assertNull($fieldDto->getCustomOption(ImageField::OPTION_MAX_SIZE)); + self::assertNull($fieldDto->getCustomOption(ImageField::OPTION_MAX_SIZE_MESSAGE)); + } + + public function testDefaultViewable(): void + { + $field = ImageField::new('image'); + $fieldDto = $this->configure($field); + + self::assertTrue($fieldDto->getCustomOption(ImageField::OPTION_VIEWABLE)); + } + + public function testIsViewableFalse(): void + { + $field = ImageField::new('image'); + $field->isViewable(false); + $fieldDto = $this->configure($field); + + self::assertFalse($fieldDto->getCustomOption(ImageField::OPTION_VIEWABLE)); + } + + public function testDefaultDownloadable(): void + { + $field = ImageField::new('image'); + $fieldDto = $this->configure($field); + + self::assertTrue($fieldDto->getCustomOption(ImageField::OPTION_DOWNLOADABLE)); + } + + public function testIsDownloadableFalse(): void + { + $field = ImageField::new('image'); + $field->isDownloadable(false); + $fieldDto = $this->configure($field); + + self::assertFalse($fieldDto->getCustomOption(ImageField::OPTION_DOWNLOADABLE)); + } + + public function testDefaultReplacedFileBehavior(): void + { + $field = ImageField::new('image'); + $fieldDto = $this->configure($field); + + self::assertSame(ReplacedFileBehavior::DELETE, $fieldDto->getCustomOption(ImageField::OPTION_REPLACED_FILE_BEHAVIOR)); + } + + public function testDeleteReplacedFile(): void + { + $field = ImageField::new('image'); + $field->deleteReplacedFile(); + $fieldDto = $this->configure($field); + + self::assertSame(ReplacedFileBehavior::DELETE, $fieldDto->getCustomOption(ImageField::OPTION_REPLACED_FILE_BEHAVIOR)); + } + + public function testKeepReplacedFile(): void + { + $field = ImageField::new('image'); + $field->keepReplacedFile(); + $fieldDto = $this->configure($field); + + self::assertSame(ReplacedFileBehavior::KEEP, $fieldDto->getCustomOption(ImageField::OPTION_REPLACED_FILE_BEHAVIOR)); + } + + public function testKeepReplacedFileOrFail(): void + { + $field = ImageField::new('image'); + $field->keepReplacedFileOrFail(); + $fieldDto = $this->configure($field); + + self::assertSame(ReplacedFileBehavior::KEEP_OR_FAIL, $fieldDto->getCustomOption(ImageField::OPTION_REPLACED_FILE_BEHAVIOR)); + } + + public function testDefaultDeletable(): void + { + $field = ImageField::new('image'); + $fieldDto = $this->configure($field); + + self::assertTrue($fieldDto->getCustomOption(ImageField::OPTION_DELETABLE)); + } + + public function testIsDeletableFalse(): void + { + $field = ImageField::new('image'); + $field->isDeletable(false); + $fieldDto = $this->configure($field); + + self::assertFalse($fieldDto->getCustomOption(ImageField::OPTION_DELETABLE)); + } + + public function testIsDeletableTrue(): void + { + $field = ImageField::new('image'); + $field->isDeletable(false); + $field->isDeletable(true); + $fieldDto = $this->configure($field); + + self::assertTrue($fieldDto->getCustomOption(ImageField::OPTION_DELETABLE)); + } } diff --git a/tests/Unit/Form/DataTransformer/StringToFileTransformerFlysystemTest.php b/tests/Unit/Form/DataTransformer/StringToFileTransformerFlysystemTest.php new file mode 100644 index 0000000000..8a13ce2e9f --- /dev/null +++ b/tests/Unit/Form/DataTransformer/StringToFileTransformerFlysystemTest.php @@ -0,0 +1,181 @@ + $f->getClientOriginalName(), + uploadValidate: $uploadValidate ?? static fn (string $filename): string => $filename, + multiple: $multiple, + flysystemStorage: $filesystem, + ); + } + + // --- transform() tests --- + + public function testTransformNullReturnsNull(): void + { + $fs = $this->createMock(FilesystemOperator::class); + + $this->assertNull($this->createTransformer($fs)->transform(null)); + } + + public function testTransformStringFileExistsReturnsFlysystemFileWithSize(): void + { + $fs = $this->createMock(FilesystemOperator::class); + $fs->method('fileExists')->with('photos/cat.jpg')->willReturn(true); + $fs->method('fileSize')->with('photos/cat.jpg')->willReturn(1234); + + $result = $this->createTransformer($fs)->transform('photos/cat.jpg'); + + $this->assertInstanceOf(FlysystemFile::class, $result); + $this->assertSame('photos/cat.jpg', $result->getPathname()); + $this->assertSame(1234, $result->getSize()); + } + + public function testTransformStringFileExistsFileSizeThrowsReturnsFlysystemFileWithNullSize(): void + { + $fs = $this->createMock(FilesystemOperator::class); + $fs->method('fileExists')->with('photos/cat.jpg')->willReturn(true); + $fs->method('fileSize')->willThrowException(new \RuntimeException('Cannot read size')); + + $result = $this->createTransformer($fs)->transform('photos/cat.jpg'); + + $this->assertInstanceOf(FlysystemFile::class, $result); + $this->assertSame('photos/cat.jpg', $result->getPathname()); + $this->assertNull($result->getSize()); + } + + public function testTransformStringFileDoesNotExistReturnsNull(): void + { + $fs = $this->createMock(FilesystemOperator::class); + $fs->method('fileExists')->with('photos/missing.jpg')->willReturn(false); + + $this->assertNull($this->createTransformer($fs)->transform('photos/missing.jpg')); + } + + public function testTransformStringFileExistsThrowsReturnsNull(): void + { + $fs = $this->createMock(FilesystemOperator::class); + $fs->method('fileExists')->willThrowException(new \RuntimeException('Connection error')); + + $this->assertNull($this->createTransformer($fs)->transform('photos/cat.jpg')); + } + + public function testTransformExistingFlysystemFileReturnedAsIs(): void + { + $fs = $this->createMock(FilesystemOperator::class); + $existing = new FlysystemFile('photos/cat.jpg', null, 999); + + $result = $this->createTransformer($fs)->transform($existing); + + $this->assertSame($existing, $result); + } + + public function testTransformMultipleModeWithArrayOfStrings(): void + { + $fs = $this->createMock(FilesystemOperator::class); + $fs->method('fileExists')->willReturn(true); + $fs->method('fileSize')->willReturn(100); + + $result = $this->createTransformer($fs, multiple: true)->transform(['a.jpg', 'b.jpg']); + + $this->assertCount(2, $result); + $this->assertInstanceOf(FlysystemFile::class, $result[0]); + $this->assertSame('a.jpg', $result[0]->getPathname()); + $this->assertInstanceOf(FlysystemFile::class, $result[1]); + $this->assertSame('b.jpg', $result[1]->getPathname()); + } + + // --- reverseTransform() tests --- + + public function testReverseTransformNullReturnsEmptyString(): void + { + $fs = $this->createMock(FilesystemOperator::class); + + $this->assertSame('', $this->createTransformer($fs)->reverseTransform(null)); + } + + public function testReverseTransformNullReturnsEmptyArrayWhenMultiple(): void + { + $fs = $this->createMock(FilesystemOperator::class); + + $this->assertSame([], $this->createTransformer($fs, multiple: true)->reverseTransform(null)); + } + + public function testReverseTransformFlysystemFileReturnsPathname(): void + { + $fs = $this->createMock(FilesystemOperator::class); + $file = new FlysystemFile('photos/cat.jpg'); + + $result = $this->createTransformer($fs)->reverseTransform($file); + + $this->assertSame('photos/cat.jpg', $result); + } + + public function testReverseTransformValidUploadedFileCallsCallables(): void + { + $fs = $this->createMock(FilesystemOperator::class); + + $tmpFile = tempnam(sys_get_temp_dir(), 'ea_test_'); + file_put_contents($tmpFile, 'test content'); + + $uploaded = new UploadedFile($tmpFile, 'report.pdf', 'application/pdf', null, true); + + $filenameCalled = false; + $validateCalled = false; + + $transformer = $this->createTransformer( + $fs, + uploadFilename: static function (UploadedFile $f) use (&$filenameCalled): string { + $filenameCalled = true; + + return 'custom_'.$f->getClientOriginalName(); + }, + uploadValidate: static function (string $filename) use (&$validateCalled): string { + $validateCalled = true; + + return $filename; + }, + ); + + $result = $transformer->reverseTransform($uploaded); + + $this->assertTrue($filenameCalled); + $this->assertTrue($validateCalled); + $this->assertSame('custom_report.pdf', $result); + + @unlink($tmpFile); + } + + public function testReverseTransformInvalidUploadedFileThrows(): void + { + $fs = $this->createMock(FilesystemOperator::class); + + $tmpFile = tempnam(sys_get_temp_dir(), 'ea_test_'); + file_put_contents($tmpFile, 'test'); + + // error code 1 = UPLOAD_ERR_INI_SIZE → invalid + $uploaded = new UploadedFile($tmpFile, 'report.pdf', 'application/pdf', \UPLOAD_ERR_INI_SIZE, false); + + $this->expectException(TransformationFailedException::class); + $this->createTransformer($fs)->reverseTransform($uploaded); + + @unlink($tmpFile); + } +} diff --git a/tests/Unit/Form/Type/FileUploadTypeFlysystemTest.php b/tests/Unit/Form/Type/FileUploadTypeFlysystemTest.php new file mode 100644 index 0000000000..ca640a3427 --- /dev/null +++ b/tests/Unit/Form/Type/FileUploadTypeFlysystemTest.php @@ -0,0 +1,121 @@ +projectDir = sys_get_temp_dir().'/ea_test_'.bin2hex(random_bytes(4)); + mkdir($this->projectDir.'/public/uploads/files', 0777, true); + parent::setUp(); + } + + protected function tearDown(): void + { + parent::tearDown(); + + if (is_dir($this->projectDir)) { + (new Filesystem())->remove($this->projectDir); + } + } + + protected function getExtensions(): array + { + $type = new FileUploadType($this->projectDir, new Filesystem()); + $validator = Validation::createValidator(); + + return [ + new PreloadedExtension([$type], []), + new ValidatorExtension($validator), + ]; + } + + public function testUploadDirNormalizerSkipsLocalCheckForFlysystem(): void + { + $fs = $this->createMock(FilesystemOperator::class); + + // This should not throw even though the directory doesn't exist locally + $form = $this->factory->create(FileUploadType::class, null, [ + 'upload_dir' => 'remote/uploads', + 'flysystem_storage' => $fs, + ]); + + $this->assertSame('remote/uploads/', $form->getConfig()->getOption('upload_dir')); + } + + public function testUploadDirNormalizerAddsTrailingSlashForFlysystem(): void + { + $fs = $this->createMock(FilesystemOperator::class); + + $form = $this->factory->create(FileUploadType::class, null, [ + 'upload_dir' => 'remote/uploads', + 'flysystem_storage' => $fs, + ]); + + $this->assertStringEndsWith('/', $form->getConfig()->getOption('upload_dir')); + } + + public function testKeepOrFailFlysystemFileDoesNotExistReturnsFilename(): void + { + $fs = $this->createMock(FilesystemOperator::class); + $fs->method('fileExists')->willReturn(false); + + $form = $this->factory->create(FileUploadType::class, null, [ + 'upload_dir' => 'remote/uploads', + 'flysystem_storage' => $fs, + 'replaced_file_behavior' => ReplacedFileBehavior::KEEP_OR_FAIL, + ]); + + // The KEEP_OR_FAIL callable is wired into the StringToFileTransformer in buildForm. + // Test it via the model transformer: reverseTransform an UploadedFile → should return the filename. + $tmpFile = tempnam(sys_get_temp_dir(), 'ea_test_'); + file_put_contents($tmpFile, 'test'); + $uploaded = new UploadedFile($tmpFile, 'test.pdf', 'application/pdf', null, true); + + $transformers = $form->getConfig()->getModelTransformers(); + $result = $transformers[0]->reverseTransform($uploaded); + + $this->assertSame('test.pdf', $result); + + @unlink($tmpFile); + } + + public function testKeepOrFailFlysystemFileExistsThrows(): void + { + $fs = $this->createMock(FilesystemOperator::class); + $fs->method('fileExists')->willReturn(true); + + $form = $this->factory->create(FileUploadType::class, null, [ + 'upload_dir' => 'remote/uploads', + 'flysystem_storage' => $fs, + 'replaced_file_behavior' => ReplacedFileBehavior::KEEP_OR_FAIL, + ]); + + $tmpFile = tempnam(sys_get_temp_dir(), 'ea_test_'); + file_put_contents($tmpFile, 'test'); + $uploaded = new UploadedFile($tmpFile, 'test.pdf', 'application/pdf', null, true); + + $transformers = $form->getConfig()->getModelTransformers(); + + $this->expectException(TransformationFailedException::class); + try { + $transformers[0]->reverseTransform($uploaded); + } finally { + @unlink($tmpFile); + } + } +} diff --git a/tests/Unit/Form/Type/Model/FlysystemFileTest.php b/tests/Unit/Form/Type/Model/FlysystemFileTest.php new file mode 100644 index 0000000000..c24b288ae8 --- /dev/null +++ b/tests/Unit/Form/Type/Model/FlysystemFileTest.php @@ -0,0 +1,44 @@ +assertSame('photos/cat.jpg', $file->getPathname()); + } + + public function testGetFilenameExplicit(): void + { + $file = new FlysystemFile('photos/cat.jpg', 'my-cat.jpg'); + + $this->assertSame('my-cat.jpg', $file->getFilename()); + } + + public function testGetFilenameDerivedFromPath(): void + { + $file = new FlysystemFile('photos/cat.jpg'); + + $this->assertSame('cat.jpg', $file->getFilename()); + } + + public function testGetSize(): void + { + $file = new FlysystemFile('photos/cat.jpg', null, 1234); + + $this->assertSame(1234, $file->getSize()); + } + + public function testGetSizeReturnsNullByDefault(): void + { + $file = new FlysystemFile('photos/cat.jpg'); + + $this->assertNull($file->getSize()); + } +} diff --git a/tests/Unit/Twig/EasyAdminTwigExtensionTest.php b/tests/Unit/Twig/EasyAdminTwigExtensionTest.php index 5b3fe37690..e90f7065fb 100644 --- a/tests/Unit/Twig/EasyAdminTwigExtensionTest.php +++ b/tests/Unit/Twig/EasyAdminTwigExtensionTest.php @@ -68,21 +68,21 @@ public function testFileSize(int $bytes, string $expected): void public static function provideValuesForFileSize(): iterable { - yield [0, '0B']; - yield [1, '1B']; - yield [1023, '1023B']; - yield [1024, '1K']; - yield [999_900, '976K']; - yield [1024 ** 2 - 100, '1023K']; - yield [1024 ** 2, '1M']; - yield [1024 ** 2 + 100, '1M']; - yield [1024 ** 3 - 1, '1023M']; - yield [1024 ** 3, '1G']; - yield [1024 ** 3 + 1, '1G']; - yield [1024 ** 4, '1T']; - yield [1024 ** 5, '1P']; - yield [1024 ** 6, '1E']; - yield [\PHP_INT_MAX, '8E']; + yield [0, '0 B']; + yield [1, '1 B']; + yield [1023, '1023 B']; + yield [1024, '1 KB']; + yield [999_900, '976.5 KB']; + yield [1024 ** 2 - 100, '1023.9 KB']; + yield [1024 ** 2, '1 MB']; + yield [1024 ** 2 + 100, '1 MB']; + yield [1024 ** 3 - 1, '1024 MB']; + yield [1024 ** 3, '1 GB']; + yield [1024 ** 3 + 1, '1 GB']; + yield [1024 ** 4, '1 TB']; + yield [1024 ** 5, '1 PB']; + yield [1024 ** 6, '1 EB']; + yield [\PHP_INT_MAX, '8 EB']; } public static function provideValuesForRepresentAsString(): iterable diff --git a/translations/EasyAdminBundle.en.php b/translations/EasyAdminBundle.en.php index 2dee724542..01a5a0ac83 100644 --- a/translations/EasyAdminBundle.en.php +++ b/translations/EasyAdminBundle.en.php @@ -54,6 +54,7 @@ 'remove_item' => 'Remove the item', 'choose_file' => 'Choose file', 'close' => 'Close', + 'download' => 'Download', 'create' => 'Create', 'create_and_add_another' => 'Create and add another', 'create_and_continue' => 'Create and continue editing', @@ -147,6 +148,12 @@ 'general_500' => 'An internal error occurred while processing your request.', ], + 'file_upload' => [ + 'add_file' => 'Add file', + 'add_files' => 'Add files', + 'clear_all' => 'Clear all', + ], + 'autocomplete' => [ 'no-results-found' => 'No results found', 'no-more-results' => 'No more results',