Optimize table view and export for software lists added. excelexport fixed - first line
This commit is contained in:
@@ -108,6 +108,12 @@ BASE_TRANSLATIONS = {
|
||||
"categories.create": ("Create category", "Kategorie anlegen"), "categories.active_fields": ("Active fields", "Aktive Felder"),
|
||||
"status.help": ("Define which status is set when assets are issued and returned.", "Lege hier fest, welcher Status beim Ausgeben und Zurücknehmen gesetzt wird."),
|
||||
"status.on_issue": ("On issue", "Beim Ausgeben"), "status.on_return": ("On return", "Beim Zurücknehmen"),
|
||||
"status.show_in_software_list": ("In software lists", "In Softwarelisten"),
|
||||
"status.software_list_help": (
|
||||
"Only statuses with this option disabled are excluded from the global software lists. Assets without an assigned status remain included.",
|
||||
"Nur Statuswerte ohne diesen Haken werden aus den globalen Softwarelisten ausgeschlossen. Assets ohne zugewiesenen Status bleiben enthalten."
|
||||
),
|
||||
"status.software_list_help": ("‘In software lists’ controls whether assets with this status are included in the global software lists, software statistics and Excel exports. Software shown directly on an individual asset is not affected.", "„In Softwarelisten“ steuert, ob Assets mit diesem Status in den globalen Softwarelisten, Software-Statistiken und Excel-Exporten berücksichtigt werden. Die Softwareanzeige direkt im einzelnen Asset bleibt davon unberührt."),
|
||||
"status.delete_confirm": ("Really delete this status value?", "Statuswert wirklich löschen?"), "status.new_value": ("New status value", "Neuer Statuswert"),
|
||||
"mesh.title": ("MeshCentral synchronization", "MeshCentral-Synchronisierung"), "mesh.start": ("Start synchronization", "Synchronisierung starten"),
|
||||
"mesh.linked_assets": ("linked assets", "verknüpfte Assets"), "mesh.conflicts": ("Conflicts", "Konflikte"), "mesh.missing": ("no longer found", "nicht mehr gefunden"),
|
||||
|
||||
+51
-10
@@ -1950,9 +1950,8 @@ def _xlsx_response(filename: str, headers: list[str], rows: list[list]) -> Respo
|
||||
workbook = Workbook()
|
||||
sheet = workbook.active
|
||||
sheet.title = "Export"
|
||||
sheet.freeze_panes = "A2"
|
||||
sheet.auto_filter.ref = f"A1:{sheet.cell(row=1, column=max(1, len(headers))).column_letter}1"
|
||||
sheet.append(headers)
|
||||
sheet.freeze_panes = "A2"
|
||||
header_fill = PatternFill("solid", fgColor="D9EAF7")
|
||||
for cell in sheet[1]:
|
||||
cell.font = Font(bold=True)
|
||||
@@ -1960,6 +1959,8 @@ def _xlsx_response(filename: str, headers: list[str], rows: list[list]) -> Respo
|
||||
cell.alignment = Alignment(vertical="top")
|
||||
for row in rows:
|
||||
sheet.append([_display_value(value) for value in row])
|
||||
last_column = sheet.cell(row=1, column=max(1, len(headers))).column_letter
|
||||
sheet.auto_filter.ref = f"A1:{last_column}{max(1, sheet.max_row)}"
|
||||
for column in sheet.columns:
|
||||
max_length = min(max((len(str(cell.value or "")) for cell in column), default=10) + 2, 60)
|
||||
sheet.column_dimensions[column[0].column_letter].width = max_length
|
||||
@@ -4582,8 +4583,17 @@ async def status_options_save(request: Request, db: Session = Depends(get_db)):
|
||||
name = (form.get(f"name_{option.id}") or "").strip()
|
||||
if not name:
|
||||
continue
|
||||
old_name = option.name
|
||||
if name != old_name:
|
||||
# Asset.status stores the status name as text. Keep existing assets
|
||||
# linked when an administrator renames a status option.
|
||||
db.query(Asset).filter(Asset.status == old_name).update(
|
||||
{Asset.status: name},
|
||||
synchronize_session=False,
|
||||
)
|
||||
option.name = name
|
||||
option.active = form.get(f"active_{option.id}") == "on"
|
||||
option.show_in_software_list = form.get(f"software_list_{option.id}") == "on"
|
||||
try:
|
||||
option.sort_order = int(form.get(f"sort_{option.id}") or option.sort_order)
|
||||
except ValueError:
|
||||
@@ -4596,7 +4606,12 @@ async def status_options_save(request: Request, db: Session = Depends(get_db)):
|
||||
new_name = (form.get("new_name") or "").strip()
|
||||
if new_name:
|
||||
max_order = max((item.sort_order for item in existing), default=-1)
|
||||
db.add(StatusOption(name=new_name, sort_order=max_order + 1, active=True))
|
||||
db.add(StatusOption(
|
||||
name=new_name,
|
||||
sort_order=max_order + 1,
|
||||
active=True,
|
||||
show_in_software_list=form.get("new_show_in_software_list") == "on",
|
||||
))
|
||||
try:
|
||||
db.commit()
|
||||
except IntegrityError as exc:
|
||||
@@ -5080,6 +5095,27 @@ def software_callback_health():
|
||||
}
|
||||
|
||||
|
||||
def _software_list_query(db: Session, *entities):
|
||||
"""Base query for global software lists and statistics.
|
||||
|
||||
Status options explicitly disabled for software lists are excluded.
|
||||
Assets without a status, or with a legacy/unmatched status value, remain
|
||||
visible so enabling this feature cannot silently hide existing inventory.
|
||||
The software inventory on an individual asset remains unchanged.
|
||||
"""
|
||||
return (
|
||||
db.query(*entities)
|
||||
.join(Asset, InstalledSoftware.asset_id == Asset.id)
|
||||
.outerjoin(StatusOption, StatusOption.name == Asset.status)
|
||||
.filter(
|
||||
or_(
|
||||
StatusOption.id.is_(None),
|
||||
StatusOption.show_in_software_list.is_(True),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _software_overview_statistics(db: Session) -> dict[str, int]:
|
||||
"""Return only the counters required by the software overview.
|
||||
|
||||
@@ -5087,7 +5123,8 @@ def _software_overview_statistics(db: Session) -> dict[str, int]:
|
||||
PostgreSQL performs the counting and grouping and returns only scalar values.
|
||||
"""
|
||||
total_entries, initialized_assets = (
|
||||
db.query(
|
||||
_software_list_query(
|
||||
db,
|
||||
func.count(InstalledSoftware.id),
|
||||
func.count(func.distinct(InstalledSoftware.asset_id)),
|
||||
)
|
||||
@@ -5098,7 +5135,8 @@ def _software_overview_statistics(db: Session) -> dict[str, int]:
|
||||
# intentionally preserves the exact grouping semantics used by the existing
|
||||
# detailed summary (Unicode casefold is stronger than SQL LOWER).
|
||||
summary_rows = (
|
||||
db.query(
|
||||
_software_list_query(
|
||||
db,
|
||||
InstalledSoftware.name,
|
||||
InstalledSoftware.publisher,
|
||||
InstalledSoftware.platform,
|
||||
@@ -5158,7 +5196,8 @@ def _software_summary_rows(db: Session, grouping: str) -> list[dict[str, Any]]:
|
||||
distinct(func.nullif(func.trim(func.coalesce(InstalledSoftware.version, "")), ""))
|
||||
)
|
||||
query_rows = (
|
||||
db.query(
|
||||
_software_list_query(
|
||||
db,
|
||||
name_key.label("name_key"),
|
||||
platform_key.label("platform_key"),
|
||||
func.min(func.coalesce(InstalledSoftware.name, "")).label("name"),
|
||||
@@ -5198,7 +5237,8 @@ def _software_summary_rows(db: Session, grouping: str) -> list[dict[str, Any]]:
|
||||
|
||||
if grouping == "version":
|
||||
query_rows = (
|
||||
db.query(
|
||||
_software_list_query(
|
||||
db,
|
||||
*common_columns,
|
||||
version_key.label("version_key"),
|
||||
func.min(func.coalesce(InstalledSoftware.version, "")).label("version"),
|
||||
@@ -5232,7 +5272,8 @@ def _software_summary_rows(db: Session, grouping: str) -> list[dict[str, Any]]:
|
||||
distinct(func.nullif(func.trim(func.coalesce(InstalledSoftware.version, "")), ""))
|
||||
)
|
||||
query_rows = (
|
||||
db.query(
|
||||
_software_list_query(
|
||||
db,
|
||||
*common_columns,
|
||||
versions_array.label("versions"),
|
||||
func.count(func.distinct(InstalledSoftware.asset_id)).label("asset_count"),
|
||||
@@ -5266,7 +5307,8 @@ def _software_summary_rows(db: Session, grouping: str) -> list[dict[str, Any]]:
|
||||
def _software_installation_rows(db: Session):
|
||||
"""Return only columns rendered by the detailed installation table."""
|
||||
return (
|
||||
db.query(
|
||||
_software_list_query(
|
||||
db,
|
||||
InstalledSoftware.id.label("id"),
|
||||
InstalledSoftware.asset_id.label("asset_id"),
|
||||
Asset.name.label("asset_name"),
|
||||
@@ -5278,7 +5320,6 @@ def _software_installation_rows(db: Session):
|
||||
InstalledSoftware.install_date.label("install_date"),
|
||||
InstalledSoftware.last_seen_at.label("last_seen_at"),
|
||||
)
|
||||
.join(Asset, InstalledSoftware.asset_id == Asset.id)
|
||||
.order_by(
|
||||
InstalledSoftware.name.asc(),
|
||||
Asset.name.asc(),
|
||||
|
||||
@@ -35,6 +35,7 @@ CATEGORY_FIELD_COLUMNS = {
|
||||
STATUS_COLUMNS = {
|
||||
"use_for_issue": "BOOLEAN DEFAULT FALSE",
|
||||
"use_for_return": "BOOLEAN DEFAULT FALSE",
|
||||
"show_in_software_list": "BOOLEAN NOT NULL DEFAULT TRUE",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -193,6 +193,7 @@ class StatusOption(Base):
|
||||
active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
use_for_issue: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
use_for_return: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
show_in_software_list: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
|
||||
|
||||
class SyncRun(Base):
|
||||
|
||||
@@ -3241,3 +3241,84 @@ html[data-theme="dark"] .advanced-filter-popup select,html[data-theme="dark"] .a
|
||||
.software-package-start-option{display:inline-flex!important;flex-direction:row!important;align-items:center!important;justify-content:flex-start!important;gap:.55rem!important;width:auto!important;max-width:100%!important;margin:0!important;}
|
||||
.software-package-start-application input[type=checkbox],
|
||||
.software-package-start-option input[type=checkbox]{width:auto!important;flex:0 0 auto!important;margin:0!important;}
|
||||
|
||||
/* v0.5.5.87+: one table implementation across all interactive data lists.
|
||||
table-tools.js adds standard-data-table-scroll automatically, including to
|
||||
older views that did not have an explicit table-scroll wrapper. */
|
||||
.standard-data-table-scroll{
|
||||
width:100%;
|
||||
max-width:100%;
|
||||
max-height:calc(100vh - 230px);
|
||||
min-height:0;
|
||||
overflow:auto;
|
||||
overscroll-behavior:contain;
|
||||
border:1px solid var(--am-border,#d8dde2);
|
||||
border-radius:6px;
|
||||
background:var(--am-surface,#fff);
|
||||
}
|
||||
|
||||
.standard-data-table-scroll > .data-table{
|
||||
margin:0;
|
||||
}
|
||||
|
||||
/* v0.5.5.88 correction: keep the complete THEAD sticky.
|
||||
The TH cells stay position:relative so resize handles keep the correct
|
||||
positioning context. This combines the sticky behavior that already
|
||||
worked in 0.5.5.86 with the unified resize logic from 0.5.5.87. */
|
||||
.standard-data-table-scroll > .data-table > thead{
|
||||
position:sticky !important;
|
||||
top:0 !important;
|
||||
z-index:32;
|
||||
/* Opaque background closes the tiny collapsed-border seam between the
|
||||
title row and filter row so scrolled table content cannot shine through. */
|
||||
background:#f5f7f9;
|
||||
}
|
||||
html[data-theme="dark"] .standard-data-table-scroll > .data-table > thead{
|
||||
background:#232e39;
|
||||
}
|
||||
|
||||
/* The THEAD owns vertical sticking. Individual header cells must therefore
|
||||
not be sticky themselves, but they must remain positioned for resizers. */
|
||||
.standard-data-table-scroll > .data-table > thead > tr > th{
|
||||
position:relative !important;
|
||||
top:auto !important;
|
||||
z-index:auto;
|
||||
overflow:visible !important;
|
||||
background-clip:padding-box;
|
||||
}
|
||||
.standard-data-table-scroll > .data-table > thead > tr:first-child > th{
|
||||
background:#e7ecf0;
|
||||
}
|
||||
.standard-data-table-scroll > .data-table > thead > .column-filter-row > th{
|
||||
background:#f5f7f9;
|
||||
}
|
||||
.standard-data-table-scroll > .data-table > thead > .column-filter-row > th.filter-cell-active{
|
||||
background:#fff4c2;
|
||||
}
|
||||
html[data-theme="dark"] .standard-data-table-scroll > .data-table > thead > tr:first-child > th{
|
||||
background:#293541;
|
||||
}
|
||||
html[data-theme="dark"] .standard-data-table-scroll > .data-table > thead > .column-filter-row > th{
|
||||
background:#232e39;
|
||||
}
|
||||
html[data-theme="dark"] .standard-data-table-scroll > .data-table > thead > .column-filter-row > th.filter-cell-active{
|
||||
background:#554612;
|
||||
}
|
||||
|
||||
/* Sticky THEAD and resize handles must coexist: every resizable header remains
|
||||
the absolute-positioning context for its handle. */
|
||||
.standard-data-table th.table-resizable-column-header,
|
||||
.standard-data-table th.table-row-number-header{
|
||||
position:relative !important;
|
||||
overflow:visible !important;
|
||||
}
|
||||
.standard-data-table th.table-resizable-column-header > .table-column-resizer,
|
||||
.standard-data-table th.table-row-number-header > .table-row-number-resizer{
|
||||
position:absolute !important;
|
||||
top:0 !important;
|
||||
right:0 !important;
|
||||
bottom:0 !important;
|
||||
width:10px !important;
|
||||
z-index:40 !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -165,14 +165,69 @@
|
||||
return collator.compare(String(left.value), String(right.value));
|
||||
}
|
||||
|
||||
function ensureStandardTableShell(table) {
|
||||
table.classList.add('standard-data-table');
|
||||
|
||||
const shellSelector = '.standard-data-table-scroll, .table-scroll, .table-wrap, .sticky-table, .management-table-scroll';
|
||||
const directParent = table.parentElement;
|
||||
let shell = directParent?.matches(shellSelector) ? directParent : null;
|
||||
|
||||
if (!shell) {
|
||||
const parent = table.parentNode;
|
||||
if (!parent) return null;
|
||||
shell = document.createElement('div');
|
||||
shell.className = 'table-scroll standard-data-table-scroll';
|
||||
parent.insertBefore(shell, table);
|
||||
shell.appendChild(table);
|
||||
} else {
|
||||
shell.classList.add('standard-data-table-scroll');
|
||||
}
|
||||
|
||||
return shell;
|
||||
}
|
||||
|
||||
function ensureStableTableStorageKey(table) {
|
||||
if (table.dataset.storageKey) return table.dataset.storageKey;
|
||||
if (table.id) {
|
||||
table.dataset.storageKey = table.id;
|
||||
return table.id;
|
||||
}
|
||||
const tables = [...document.querySelectorAll('main table.data-table')];
|
||||
const index = Math.max(0, tables.indexOf(table));
|
||||
const key = `${window.location.pathname}:data-table-${index}`;
|
||||
table.dataset.storageKey = key;
|
||||
return key;
|
||||
}
|
||||
|
||||
function initTable(table) {
|
||||
const thead = table.tHead;
|
||||
const tbody = table.tBodies[0];
|
||||
if (!thead || !tbody || !thead.rows.length) return;
|
||||
|
||||
const tableShell = ensureStandardTableShell(table);
|
||||
ensureStableTableStorageKey(table);
|
||||
|
||||
if (table.dataset.tableToolsReady === '1') return;
|
||||
|
||||
const headerRow = thead.rows[0];
|
||||
|
||||
// Sticky title/filter rows and resizable columns must coexist. Sticky is
|
||||
// applied to the TH cells in CSS; this custom property supplies the exact
|
||||
// offset for the filter row from the rendered title-row height.
|
||||
function updateStickyHeaderMetrics() {
|
||||
const measured = headerRow.getBoundingClientRect().height || headerRow.offsetHeight || 0;
|
||||
if (measured > 0) {
|
||||
table.style.setProperty('--table-title-row-height', `${Math.ceil(measured)}px`);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof ResizeObserver === 'function') {
|
||||
const observer = new ResizeObserver(() => updateStickyHeaderMetrics());
|
||||
observer.observe(headerRow);
|
||||
table._stickyHeaderResizeObserver = observer;
|
||||
}
|
||||
window.addEventListener('resize', updateStickyHeaderMetrics);
|
||||
|
||||
// Add a purely visual row-number column before every real table column.
|
||||
// It is recalculated after filtering, sorting and partial server reloads and
|
||||
// therefore never represents a database identifier.
|
||||
@@ -188,7 +243,7 @@
|
||||
}
|
||||
|
||||
const numberHeader = headerRow.querySelector('th[data-row-number-column="1"]');
|
||||
const rowNumberStoragePart = table.dataset.storageKey || table.id || window.location.pathname;
|
||||
const rowNumberStoragePart = ensureStableTableStorageKey(table);
|
||||
const rowNumberWidthKey = `table:${rowNumberStoragePart}:row-number-width`;
|
||||
const defaultRowNumberWidth = 64;
|
||||
const minimumRowNumberWidth = 48;
|
||||
@@ -1189,10 +1244,15 @@
|
||||
});
|
||||
|
||||
thead.appendChild(filterRow);
|
||||
if (isAssetTable) {
|
||||
installColumnResizers();
|
||||
applyStoredColumnWidths();
|
||||
}
|
||||
updateStickyHeaderMetrics();
|
||||
requestAnimationFrame(updateStickyHeaderMetrics);
|
||||
|
||||
// All AssetManager data tables use the same resize implementation.
|
||||
// This keeps width changes, table growth and user-state persistence
|
||||
// identical across Assets, Software, Jobs, Settings and detail lists.
|
||||
installColumnResizers();
|
||||
applyStoredColumnWidths();
|
||||
requestAnimationFrame(updateStickyHeaderMetrics);
|
||||
|
||||
// Editable cells are always read live, so changed values immediately
|
||||
// participate in filtering without a stale cache.
|
||||
@@ -1210,6 +1270,7 @@
|
||||
table.assetUpdateRowNumbers = updateRowNumbers;
|
||||
table.assetRebuildFilterCache = rebuildMissingCellKeys;
|
||||
table.assetApplyStoredColumnWidths = applyStoredColumnWidths;
|
||||
table.assetUpdateStickyHeaderMetrics = updateStickyHeaderMetrics;
|
||||
table.dataset.tableToolsReady = '1';
|
||||
ensureFilterStatus();
|
||||
|
||||
@@ -1352,7 +1413,7 @@
|
||||
|
||||
const initializeTableTools = () => {
|
||||
document.querySelectorAll('table.data-table').forEach(initTable);
|
||||
document.querySelectorAll('main table:not(#asset-table):not([data-column-resize="off"])').forEach(initUniversalTableResize);
|
||||
document.querySelectorAll('main table:not(.data-table):not([data-column-resize="off"])').forEach(initUniversalTableResize);
|
||||
};
|
||||
if (uiState?.ready) uiState.ready.finally(initializeTableTools);
|
||||
else initializeTableTools();
|
||||
@@ -1369,6 +1430,7 @@
|
||||
}
|
||||
table.assetUpdateRowNumbers?.();
|
||||
table.assetApplyStoredColumnWidths?.();
|
||||
requestAnimationFrame(() => table.assetUpdateStickyHeaderMetrics?.());
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="panel"><h2>{{t('mesh.recent_runs')}}</h2><table><thead><tr><th>{{t('mesh.start_time')}}</th><th>{{t('assets.status')}}</th><th>{{t('mesh.found')}}</th><th>{{t('mesh.created')}}</th><th>{{t('mesh.changed')}}</th><th>{{t('mesh.conflicts')}}</th><th>{{t('mesh.errors')}}</th><th>{{t('mesh.message')}}</th></tr></thead><tbody>{% for run in runs %}<tr><td><time class="local-time" datetime="{{ utc_iso(run.started_at) }}" data-utc="{{ utc_iso(run.started_at) }}">{{ run.started_at }}</time></td><td>{{run.status}}</td><td>{{run.devices_found}}</td><td>{{run.created_count}}</td><td>{{run.updated_count}}</td><td>{{run.conflict_count}}</td><td>{{run.error_count}}</td><td>{{run.message or ''}}</td></tr>{% else %}<tr><td colspan="8">{{t('mesh.no_runs')}}</td></tr>{% endfor %}</tbody></table></section>
|
||||
<section class="panel"><h2>{{t('mesh.recent_runs')}}</h2><table class="data-table" data-storage-key="meshcentral-recent-runs"><thead><tr><th>{{t('mesh.start_time')}}</th><th>{{t('assets.status')}}</th><th>{{t('mesh.found')}}</th><th>{{t('mesh.created')}}</th><th>{{t('mesh.changed')}}</th><th>{{t('mesh.conflicts')}}</th><th>{{t('mesh.errors')}}</th><th>{{t('mesh.message')}}</th></tr></thead><tbody>{% for run in runs %}<tr><td><time class="local-time" datetime="{{ utc_iso(run.started_at) }}" data-utc="{{ utc_iso(run.started_at) }}">{{ run.started_at }}</time></td><td>{{run.status}}</td><td>{{run.devices_found}}</td><td>{{run.created_count}}</td><td>{{run.updated_count}}</td><td>{{run.conflict_count}}</td><td>{{run.error_count}}</td><td>{{run.message or ''}}</td></tr>{% else %}<tr><td colspan="8">{{t('mesh.no_runs')}}</td></tr>{% endfor %}</tbody></table></section>
|
||||
<script>(()=>{
|
||||
const startButton=document.getElementById('sync-start');
|
||||
const dryButton=document.getElementById('sync-dry-run');
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{% extends 'base.html' %}{% block content %}
|
||||
<div class="toolbar"><h1>{{ t('app.status_values') }}</h1></div><p>{{ t('status.help') }}</p>
|
||||
<form method="post"><div class="table-scroll management-table-scroll"><table class="data-table"><thead><tr><th>{{ t('common.order') }}</th><th>{{ t('assets.status') }}</th><th>{{ t('common.active') }}</th><th>{{ t('status.on_issue') }}</th><th>{{ t('status.on_return') }}</th><th>{{ t('common.actions') }}</th></tr></thead><tbody>
|
||||
{% for option in options %}<tr><td><input type="number" name="sort_{{option.id}}" value="{{option.sort_order}}"></td><td><input name="name_{{option.id}}" value="{{option.name}}" required></td><td><input class="checkbox" type="checkbox" name="active_{{option.id}}" {% if option.active %}checked{% endif %}></td><td><input type="radio" name="issue_status_id" value="{{ option.id }}" {% if option.use_for_issue %}checked{% endif %}></td><td><input type="radio" name="return_status_id" value="{{ option.id }}" {% if option.use_for_return %}checked{% endif %}></td><td><button formmethod="post" formaction="/status-options/{{option.id}}/delete" class="icon-button button-danger" onclick="return confirm({{ t('status.delete_confirm')|tojson }});">🗑</button></td></tr>{% endfor %}
|
||||
<tr><td>{{ t('common.new') }}</td><td><input name="new_name" placeholder="{{ t('status.new_value') }}"></td><td colspan="4"></td></tr></tbody></table></div><div class="form-actions"><button class="button button-save" type="submit">💾 {{ t('common.save') }}</button></div></form>
|
||||
<p class="muted">{{ t('status.software_list_help') }}</p>
|
||||
<form method="post"><div class="table-scroll management-table-scroll"><table class="data-table"><thead><tr><th>{{ t('common.order') }}</th><th>{{ t('assets.status') }}</th><th>{{ t('common.active') }}</th><th>{{ t('status.on_issue') }}</th><th>{{ t('status.on_return') }}</th><th>{{ t('status.show_in_software_list') }}</th><th>{{ t('common.actions') }}</th></tr></thead><tbody>
|
||||
{% for option in options %}<tr><td><input type="number" name="sort_{{option.id}}" value="{{option.sort_order}}"></td><td><input name="name_{{option.id}}" value="{{option.name}}" required></td><td><input class="checkbox" type="checkbox" name="active_{{option.id}}" {% if option.active %}checked{% endif %}></td><td><input type="radio" name="issue_status_id" value="{{ option.id }}" {% if option.use_for_issue %}checked{% endif %}></td><td><input type="radio" name="return_status_id" value="{{ option.id }}" {% if option.use_for_return %}checked{% endif %}></td><td><input class="checkbox" type="checkbox" name="software_list_{{option.id}}" {% if option.show_in_software_list %}checked{% endif %}></td><td><button formmethod="post" formaction="/status-options/{{option.id}}/delete" class="icon-button button-danger" onclick="return confirm({{ t('status.delete_confirm')|tojson }});">🗑</button></td></tr>{% endfor %}
|
||||
<tr><td>{{ t('common.new') }}</td><td><input name="new_name" placeholder="{{ t('status.new_value') }}"></td><td colspan="3"></td><td><input class="checkbox" type="checkbox" name="new_show_in_software_list" checked></td><td></td></tr></tbody></table></div><div class="form-actions"><button class="button button-save" type="submit">💾 {{ t('common.save') }}</button></div></form>
|
||||
<p class="form-help">{{ t('status.software_list_help') }}</p>
|
||||
{% endblock %}
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
APP_VERSION = "0.5.5.84"
|
||||
APP_VERSION = "0.5.5.88"
|
||||
__version__ = APP_VERSION
|
||||
|
||||
Reference in New Issue
Block a user