Lint / PHP (phpcs PSR-12) (push) Successful in 42s
Lint / JS (eslint) (push) Successful in 13s
Lint / PHP requirements (version + extensions) (push) Successful in 27s
Lint / Notify on failure (push) Skipped
Security / PHP Security (semgrep) (push) Successful in 1m24s
Lint / Deploy (push) Successful in 3s
CustomFieldModel::setValue()/setValues()/getValuesForTicket() were never called anywhere outside api/custom_fields.php's own admin CRUD for field *definitions* — CreateTicketView.php never rendered custom fields, TicketController::create() never collected or saved them, and TicketView.php never displayed them. The whole feature (admin defines fields at /admin/custom-fields, including marking them Required) was config-only with zero consumer; is_required was enforced nowhere. Added: - api/ticket_custom_fields.php: new endpoint (bootstrap.php-based, so any ticket editor can use it, not just admins) that saves values for a ticket. Only considers fields applicable to the ticket's current category (or category-less fields); enforces is_required, validates select values against the field's configured options, and validates number fields are numeric. Values for fields that don't apply are silently ignored rather than persisted, so a value typed before a category change can't linger as orphaned data. - CreateTicketView.php: renders every active field definition (all categories, since the ticket doesn't exist yet), grouped with a data-custom-field-category attribute and toggled client-side as the Category select changes, matching the existing visibility-groups toggle pattern. Submitted as part of the same form. - TicketController::create(): validates is_required server-side against the fields applicable to the *submitted* category (not just whatever was visible client-side) before creating the ticket, then persists via CustomFieldModel::setValues() after a successful create. - TicketView.php: new "Custom Fields" tab (only shown when the ticket's category has applicable fields) rendering current values with a single Save button, calling the new endpoint — a panel-plus-save interaction rather than per-field inline auto-save, to keep scope contained across 6 field types. Verified against real MariaDB and a real running server: a required field left blank is correctly rejected (both at ticket-creation time and when editing an existing ticket) with no partial write; a valid submission persists exactly the fields applicable to that ticket's category; an invalid select value is rejected without touching previously-saved values; and each rejection correctly returns a rotated recovery csrf_token (initially missed on the validation-error paths, since they used a plain echo/exit instead of the bootstrap.php apiRespond() helper every other endpoint's error paths use for this). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lhz7pGMaoTfL5sdYS5XiKv
458 lines
22 KiB
PHP
458 lines
22 KiB
PHP
<?php
|
|
|
|
/**
|
|
* CreateTicketView.php — New ticket creation form, redesigned for TDS v1.2
|
|
* Variables: $templates (array), $allUsers (array), $error (string|null)
|
|
*/
|
|
|
|
require_once __DIR__ . '/../middleware/SecurityHeadersMiddleware.php';
|
|
require_once __DIR__ . '/../middleware/CsrfMiddleware.php';
|
|
|
|
$nonce = SecurityHeadersMiddleware::getNonce();
|
|
$pageTitle = 'New Ticket';
|
|
$activeNav = 'dashboard';
|
|
$_v = $GLOBALS['config']['ASSET_VERSION'] ?? '1';
|
|
$pageStyles = ["/assets/css/dashboard.css?v={$_v}", "/assets/css/ticket.css?v={$_v}"];
|
|
$pageScripts = [
|
|
"/assets/js/keyboard-shortcuts.js?v={$_v}",
|
|
];
|
|
|
|
include __DIR__ . '/layout_header.php';
|
|
?>
|
|
|
|
<!-- Page header -->
|
|
<div class="lt-page-header">
|
|
<div class="lt-flex lt-flex-gap-sm lt-flex-align-center">
|
|
<a href="/" class="lt-btn lt-btn-ghost lt-btn-sm">← Dashboard</a>
|
|
<span class="lt-text-muted lt-text-xs">/</span>
|
|
<span class="lt-text-muted lt-text-xs">New Ticket</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════════════════════════════════════════════════════
|
|
CREATE TICKET FORM
|
|
═══════════════════════════════════════════════════════════ -->
|
|
<form method="POST"
|
|
action="<?= htmlspecialchars($GLOBALS['config']['BASE_URL'], ENT_QUOTES, 'UTF-8') ?>/ticket/create"
|
|
class="create-ticket-form"
|
|
novalidate>
|
|
|
|
<input type="hidden" name="csrf_token"
|
|
value="<?= htmlspecialchars(CsrfMiddleware::getToken(), ENT_QUOTES, 'UTF-8') ?>">
|
|
<input type="hidden" name="link_duplicate_of" id="linkDuplicateOf" value="">
|
|
|
|
<?php if (isset($error)) : ?>
|
|
<div class="lt-msg lt-msg-danger lt-mb-md" role="alert">
|
|
<strong>Error:</strong> <?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?>
|
|
</div>
|
|
<?php endif ?>
|
|
|
|
<!-- ── SECTION 1: Template ───────────────────────────────── -->
|
|
<div class="lt-frame lt-mb-md">
|
|
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
|
<div class="lt-section-header">Template (Optional)</div>
|
|
<div class="lt-section-body">
|
|
<div class="lt-form-group">
|
|
<label class="lt-label" for="templateSelect">Use a Template</label>
|
|
<select id="templateSelect" class="lt-select" data-action="load-template">
|
|
<option value="">— No Template —</option>
|
|
<?php if (!empty($templates)) : ?>
|
|
<?php foreach ($templates as $tpl) : ?>
|
|
<option value="<?= (int)$tpl['template_id'] ?>">
|
|
<?= htmlspecialchars($tpl['template_name']) ?>
|
|
</option>
|
|
<?php endforeach ?>
|
|
<?php endif ?>
|
|
</select>
|
|
<p class="lt-form-hint">Selecting a template pre-fills the form fields.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── SECTION 2: Title ─────────────────────────────────── -->
|
|
<div class="lt-frame lt-mb-md">
|
|
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
|
<div class="lt-section-header">Title *</div>
|
|
<div class="lt-section-body">
|
|
<div class="lt-form-group">
|
|
<label class="lt-label lt-sr-only" for="title">Ticket Title</label>
|
|
<input type="text"
|
|
id="title"
|
|
name="title"
|
|
class="lt-input"
|
|
required
|
|
autocomplete="off"
|
|
placeholder="Enter a clear, concise title for this ticket"
|
|
aria-required="true"
|
|
aria-describedby="duplicateWarning">
|
|
</div>
|
|
|
|
<!-- Duplicate warning (shown by JS when similar tickets exist) -->
|
|
<div id="duplicateWarning" class="lt-msg lt-msg-warning is-hidden"
|
|
role="alert" aria-live="polite" aria-atomic="true">
|
|
<strong class="lt-text-amber">Possible Duplicates Found</strong>
|
|
<div id="duplicatesList" aria-live="polite"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── SECTION 3: Metadata ──────────────────────────────── -->
|
|
<div class="lt-frame lt-mb-md">
|
|
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
|
<div class="lt-section-header">Metadata</div>
|
|
<div class="lt-section-body">
|
|
<div class="create-ticket-meta-grid">
|
|
|
|
<div class="lt-form-group">
|
|
<label class="lt-label" for="status">Status</label>
|
|
<select id="status" name="status" class="lt-select">
|
|
<option value="Open" selected>Open</option>
|
|
<option value="Closed">Closed</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="lt-form-group">
|
|
<label class="lt-label" for="priority">Priority</label>
|
|
<select id="priority" name="priority" class="lt-select">
|
|
<option value="1">P1 — Critical Impact</option>
|
|
<option value="2">P2 — High Impact</option>
|
|
<option value="3">P3 — Medium Impact</option>
|
|
<option value="4" selected>P4 — Low Impact</option>
|
|
<option value="5">P5 — Minimal Impact</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="lt-form-group">
|
|
<label class="lt-label" for="category">Category</label>
|
|
<select id="category" name="category" class="lt-select" data-action="toggle-custom-fields">
|
|
<option value="Hardware">Hardware</option>
|
|
<option value="Software">Software</option>
|
|
<option value="Network">Network</option>
|
|
<option value="Security">Security</option>
|
|
<option value="General" selected>General</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="lt-form-group">
|
|
<label class="lt-label" for="type">Type</label>
|
|
<select id="type" name="type" class="lt-select">
|
|
<option value="Maintenance">Maintenance</option>
|
|
<option value="Install">Install</option>
|
|
<option value="Task">Task</option>
|
|
<option value="Upgrade">Upgrade</option>
|
|
<option value="Issue" selected>Issue</option>
|
|
<option value="Problem">Problem</option>
|
|
</select>
|
|
</div>
|
|
|
|
</div><!-- /.create-ticket-meta-grid -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── SECTION 4: Assignment ────────────────────────────── -->
|
|
<div class="lt-frame lt-mb-md">
|
|
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
|
<div class="lt-section-header">Assignment</div>
|
|
<div class="lt-section-body">
|
|
<div class="lt-form-group">
|
|
<label class="lt-label" for="assigned_to">Assign To</label>
|
|
<select id="assigned_to" name="assigned_to" class="lt-select">
|
|
<option value="">— Unassigned —</option>
|
|
<?php if (!empty($allUsers)) : ?>
|
|
<?php foreach ($allUsers as $u) : ?>
|
|
<option value="<?= (int)$u['user_id'] ?>">
|
|
<?= htmlspecialchars($u['display_name'] ?? $u['username']) ?>
|
|
</option>
|
|
<?php endforeach ?>
|
|
<?php endif ?>
|
|
</select>
|
|
<p class="lt-form-hint">Leave blank to create as unassigned.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── SECTION 5: Visibility ────────────────────────────── -->
|
|
<div class="lt-frame lt-mb-md">
|
|
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
|
<div class="lt-section-header">Visibility</div>
|
|
<div class="lt-section-body">
|
|
<div class="lt-form-group">
|
|
<label class="lt-label" for="visibility">Who can see this ticket?</label>
|
|
<select id="visibility" name="visibility" class="lt-select" data-action="toggle-visibility-groups">
|
|
<option value="public" selected>Public — All authenticated users</option>
|
|
<option value="internal">Internal — Specific groups only</option>
|
|
<option value="confidential">Confidential — Creator, assignee, and admins only</option>
|
|
</select>
|
|
<p id="visibilityHint" class="lt-form-hint">Everyone who is logged in can view this ticket.</p>
|
|
</div>
|
|
|
|
<div id="visibilityGroupsContainer" class="lt-form-group is-hidden" aria-live="polite" aria-describedby="visibilityGroupsHint">
|
|
<label class="lt-label lt-text-cyan">Allowed Groups</label>
|
|
<div class="visibility-groups-list lt-flex lt-flex-wrap lt-flex-gap-sm">
|
|
<?php
|
|
require_once __DIR__ . '/../models/UserModel.php';
|
|
$userModel = new UserModel($conn);
|
|
$allGroups = $userModel->getAllGroups();
|
|
foreach ($allGroups as $group) :
|
|
?>
|
|
<label class="lt-filter-option">
|
|
<input type="checkbox" class="lt-checkbox visibility-group-checkbox"
|
|
name="visibility_groups[]"
|
|
value="<?= htmlspecialchars($group, ENT_QUOTES, 'UTF-8') ?>">
|
|
<span class="lt-badge"><?= htmlspecialchars($group) ?></span>
|
|
</label>
|
|
<?php endforeach ?>
|
|
<?php if (empty($allGroups)) : ?>
|
|
<span class="lt-text-muted lt-text-sm">No groups available</span>
|
|
<?php endif ?>
|
|
</div>
|
|
<p id="visibilityGroupsHint" class="lt-form-hint lt-form-hint--warn">Select at least one group for Internal visibility.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if (!empty($allCustomFieldDefs)) : ?>
|
|
<!-- ── SECTION 5b: Custom Fields ─────────────────────────── -->
|
|
<div class="lt-frame lt-mb-md">
|
|
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
|
<div class="lt-section-header">Additional Fields</div>
|
|
<div class="lt-section-body">
|
|
<?php foreach ($allCustomFieldDefs as $cfDef) : ?>
|
|
<div class="lt-form-group custom-field-group"
|
|
data-custom-field-category="<?= htmlspecialchars($cfDef['category'] ?? '', ENT_QUOTES, 'UTF-8') ?>">
|
|
<?php
|
|
$cfName = 'custom_fields[' . (int)$cfDef['field_id'] . ']';
|
|
$cfId = 'custom_field_' . (int)$cfDef['field_id'];
|
|
?>
|
|
<label class="lt-label" for="<?= $cfId ?>">
|
|
<?= htmlspecialchars($cfDef['field_label'], ENT_QUOTES, 'UTF-8') ?><?= $cfDef['is_required'] ? ' *' : '' ?>
|
|
</label>
|
|
<?php if ($cfDef['field_type'] === 'textarea') : ?>
|
|
<textarea id="<?= $cfId ?>" name="<?= $cfName ?>" class="lt-input lt-textarea" rows="3"
|
|
<?= $cfDef['is_required'] ? 'data-custom-field-required="1"' : '' ?>></textarea>
|
|
<?php elseif ($cfDef['field_type'] === 'select') : ?>
|
|
<select id="<?= $cfId ?>" name="<?= $cfName ?>" class="lt-select"
|
|
<?= $cfDef['is_required'] ? 'data-custom-field-required="1"' : '' ?>>
|
|
<option value="">— Select —</option>
|
|
<?php foreach (($cfDef['field_options']['options'] ?? []) as $opt) : ?>
|
|
<option value="<?= htmlspecialchars($opt, ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($opt, ENT_QUOTES, 'UTF-8') ?></option>
|
|
<?php endforeach ?>
|
|
</select>
|
|
<?php elseif ($cfDef['field_type'] === 'checkbox') : ?>
|
|
<label class="lt-filter-option">
|
|
<input type="checkbox" class="lt-checkbox" id="<?= $cfId ?>" name="<?= $cfName ?>" value="1">
|
|
<?= htmlspecialchars($cfDef['field_label'], ENT_QUOTES, 'UTF-8') ?>
|
|
</label>
|
|
<?php elseif ($cfDef['field_type'] === 'date') : ?>
|
|
<input type="date" id="<?= $cfId ?>" name="<?= $cfName ?>" class="lt-input"
|
|
<?= $cfDef['is_required'] ? 'data-custom-field-required="1"' : '' ?>>
|
|
<?php elseif ($cfDef['field_type'] === 'number') : ?>
|
|
<input type="number" id="<?= $cfId ?>" name="<?= $cfName ?>" class="lt-input"
|
|
<?= $cfDef['is_required'] ? 'data-custom-field-required="1"' : '' ?>>
|
|
<?php else : ?>
|
|
<input type="text" id="<?= $cfId ?>" name="<?= $cfName ?>" class="lt-input"
|
|
<?= $cfDef['is_required'] ? 'data-custom-field-required="1"' : '' ?>>
|
|
<?php endif ?>
|
|
</div>
|
|
<?php endforeach ?>
|
|
<p class="lt-form-hint">Fields shown depend on the selected Category.</p>
|
|
</div>
|
|
</div>
|
|
<?php endif ?>
|
|
|
|
<!-- ── SECTION 6: Description ───────────────────────────── -->
|
|
<div class="lt-frame lt-mb-md">
|
|
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
|
<div class="lt-section-header">Description *</div>
|
|
<div class="lt-section-body">
|
|
<div class="lt-form-group">
|
|
<label class="lt-sr-only lt-label" for="description">Description</label>
|
|
<textarea id="description"
|
|
name="description"
|
|
class="lt-input lt-textarea"
|
|
rows="16"
|
|
required
|
|
aria-required="true"
|
|
placeholder="Provide a detailed description of the issue, steps to reproduce, expected vs. actual behavior, and any relevant context…"></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── SECTION 7: Actions ───────────────────────────────── -->
|
|
<div class="lt-frame">
|
|
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
|
<div class="lt-section-header">Actions</div>
|
|
<div class="lt-section-body">
|
|
<div class="lt-btn-group">
|
|
<button type="submit" class="lt-btn lt-btn-primary">CREATE TICKET</button>
|
|
<a href="/" class="lt-btn lt-btn-ghost">CANCEL</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</form>
|
|
|
|
<!-- Page-specific script: duplicate detection + visibility toggle -->
|
|
<script nonce="<?= $nonce ?>">
|
|
(function () {
|
|
'use strict';
|
|
|
|
// ── Duplicate detection ───────────────────────────────────
|
|
var _dupTimer = null;
|
|
|
|
document.getElementById('title').addEventListener('input', function () {
|
|
clearTimeout(_dupTimer);
|
|
var title = this.value.trim();
|
|
if (title.length < 5) {
|
|
document.getElementById('duplicateWarning').classList.add('is-hidden');
|
|
return;
|
|
}
|
|
_dupTimer = setTimeout(function () { checkDuplicates(title); }, 500);
|
|
});
|
|
|
|
function checkDuplicates(title) {
|
|
if (!window.lt || typeof lt.api === 'undefined') return;
|
|
lt.api.get('/api/check_duplicates.php?title=' + encodeURIComponent(title))
|
|
.then(function (data) {
|
|
var warn = document.getElementById('duplicateWarning');
|
|
var list = document.getElementById('duplicatesList');
|
|
if (data.success && data.duplicates && data.duplicates.length > 0) {
|
|
var ul = document.createElement('ul');
|
|
ul.className = 'duplicate-list lt-text-sm';
|
|
data.duplicates.forEach(function (dup) {
|
|
var li = document.createElement('li');
|
|
li.className = 'lt-flex lt-flex-align-center lt-flex-gap-sm lt-mb-xs';
|
|
var a = document.createElement('a');
|
|
a.href = '/ticket/' + encodeURIComponent(dup.ticket_id);
|
|
a.target = '_blank';
|
|
a.textContent = '#' + dup.ticket_id;
|
|
var dash = document.createTextNode(' \u2014 ' + dup.title + ' ');
|
|
var badge = document.createElement('span');
|
|
badge.className = 'lt-text-muted';
|
|
badge.textContent = '(' + dup.similarity + '% match, ' + dup.status + ')';
|
|
var linkBtn = document.createElement('button');
|
|
linkBtn.type = 'button';
|
|
linkBtn.className = 'lt-btn lt-btn-ghost lt-btn-xs';
|
|
linkBtn.dataset.dupId = dup.ticket_id;
|
|
linkBtn.textContent = 'Link as duplicate';
|
|
linkBtn.title = 'After creating, this ticket will be linked as a duplicate of #' + dup.ticket_id;
|
|
linkBtn.addEventListener('click', function () {
|
|
var chosen = this.dataset.dupId;
|
|
document.getElementById('linkDuplicateOf').value = chosen;
|
|
// Update all buttons to show current selection
|
|
ul.querySelectorAll('[data-dup-id]').forEach(function (b) {
|
|
b.textContent = b.dataset.dupId === chosen ? '\u2713 Will link' : 'Link as duplicate';
|
|
b.classList.toggle('lt-btn-primary', b.dataset.dupId === chosen);
|
|
});
|
|
});
|
|
li.appendChild(a);
|
|
li.appendChild(dash);
|
|
li.appendChild(badge);
|
|
li.appendChild(linkBtn);
|
|
ul.appendChild(li);
|
|
});
|
|
var hint = document.createElement('p');
|
|
hint.className = 'lt-text-xs lt-text-muted lt-mt-sm';
|
|
hint.textContent = 'Check these before creating. Use "Link as duplicate" to auto-link after create.';
|
|
list.innerHTML = '';
|
|
list.appendChild(ul);
|
|
list.appendChild(hint);
|
|
warn.classList.remove('is-hidden');
|
|
} else {
|
|
warn.classList.add('is-hidden');
|
|
}
|
|
})
|
|
.catch(function () { /* silent — duplicate check is non-critical */ });
|
|
}
|
|
|
|
// ── Custom fields: show only the selected category's fields ──
|
|
function toggleCustomFields() {
|
|
var category = document.getElementById('category').value;
|
|
document.querySelectorAll('.custom-field-group').forEach(function (group) {
|
|
var fieldCategory = group.getAttribute('data-custom-field-category');
|
|
group.classList.toggle('is-hidden', fieldCategory !== '' && fieldCategory !== category);
|
|
});
|
|
}
|
|
|
|
// ── Visibility groups toggle ──────────────────────────────
|
|
var visibilityHints = {
|
|
'public': 'Everyone who is logged in can view this ticket.',
|
|
'internal': 'Only members of the selected groups (plus admins) can view this ticket.',
|
|
'confidential': 'Only you, the assignee, and admins can view this ticket.'
|
|
};
|
|
function toggleVisibilityGroups() {
|
|
var vis = document.getElementById('visibility').value;
|
|
var container = document.getElementById('visibilityGroupsContainer');
|
|
var hint = document.getElementById('visibilityHint');
|
|
if (vis === 'internal') {
|
|
container.classList.remove('is-hidden');
|
|
} else {
|
|
container.classList.add('is-hidden');
|
|
container.querySelectorAll('.visibility-group-checkbox').forEach(function (cb) { cb.checked = false; });
|
|
}
|
|
if (hint) hint.textContent = visibilityHints[vis] || '';
|
|
}
|
|
|
|
// ── Template loader ───────────────────────────────────────
|
|
function loadTemplate() {
|
|
var tplId = document.getElementById('templateSelect').value;
|
|
if (!tplId) return;
|
|
|
|
// Warn before overwriting content the user has already typed
|
|
var existingTitle = (document.getElementById('title').value || '').trim();
|
|
var existingDesc = (document.getElementById('description').value || '').trim();
|
|
if (existingTitle || existingDesc) {
|
|
showConfirmModal(
|
|
'Overwrite content?',
|
|
'Applying this template will overwrite your current title and description. Continue?',
|
|
'warning',
|
|
applyTemplate,
|
|
function () { document.getElementById('templateSelect').value = ''; }
|
|
);
|
|
return;
|
|
}
|
|
|
|
applyTemplate();
|
|
}
|
|
|
|
function applyTemplate() {
|
|
var tplId = document.getElementById('templateSelect').value;
|
|
if (!tplId) return;
|
|
|
|
lt.api.get('/api/get_template.php?template_id=' + encodeURIComponent(tplId))
|
|
.then(function (data) {
|
|
if (!data.success || !data.template) {
|
|
lt.toast.error('Failed to load template.');
|
|
return;
|
|
}
|
|
var t = data.template;
|
|
if (t.title) document.getElementById('title').value = t.title;
|
|
if (t.description) document.getElementById('description').value = t.description;
|
|
if (t.priority) document.getElementById('priority').value = t.priority;
|
|
if (t.category) document.getElementById('category').value = t.category;
|
|
if (t.type) document.getElementById('type').value = t.type;
|
|
// Trigger duplicate check after template fill
|
|
document.getElementById('title').dispatchEvent(new Event('input'));
|
|
lt.toast.success('Template applied.');
|
|
})
|
|
.catch(function () { lt.toast.error('Could not load template.'); });
|
|
}
|
|
|
|
// ── Event delegation ──────────────────────────────────────
|
|
document.addEventListener('change', function (e) {
|
|
var target = e.target.closest('[data-action]');
|
|
if (!target) return;
|
|
switch (target.getAttribute('data-action')) {
|
|
case 'load-template': loadTemplate(); break;
|
|
case 'toggle-visibility-groups': toggleVisibilityGroups(); break;
|
|
case 'toggle-custom-fields': toggleCustomFields(); break;
|
|
}
|
|
});
|
|
|
|
toggleCustomFields();
|
|
if (window.lt) lt.keys.initDefaults();
|
|
}());
|
|
</script>
|
|
|
|
<?php include __DIR__ . '/layout_footer.php'; ?>
|