items_editor.php

main 221 lines · 8.4 KB Raw
Alex Alex Commit Initial commit 01/10/2026 09:20
1<?php
2/**
3 * Title: Items
4 * Icon: fa-shield
5 * Group: Server Info
6 * Order: 12
7 * Description: Add or edit a single item without re-uploading the whole items.xml.
8 * Hidden: true
9 */
10
11
12if (!defined('ACP_ROOT')) {
13 http_response_code(403);
14 die('Direct access denied.');
15}
16
17const ITMED_TYPE = 'item';
18
19/** Stored attribute map -> editable {key, value} rows. */
20function itmed_attr_rows(array $attributes): array {
21 $rows = array();
22 foreach ($attributes as $key => $value) {
23 $rows[] = array('key' => (string)$key, 'value' => is_scalar($value) ? (string)$value : '');
24 }
25 return $rows;
26}
27
28/** Posted {key, value} rows -> attribute map, blank/duplicate keys dropped. */
29function itmed_attr_map_from_post(array $rowsRaw): array {
30 $map = array();
31 foreach ($rowsRaw as $row) {
32 if (!is_array($row)) continue;
33 $key = trim((string)($row['key'] ?? ''));
34 if ($key === '') continue;
35 $map[$key] = trim((string)($row['value'] ?? ''));
36 }
37 return $map;
38}
39
40$items = serverdata_load('items');
41$items = is_array($items) ? $items : array();
42
43if (!$items && !serverdata_override_table_exists()) {
44 acp_flash_error(t_default('acp.itmed.no_data', 'Upload an items.xml on Server Info first - there is nothing published to edit yet.'));
45 acp_redirect('serverinfo');
46}
47
48if ($_SERVER['REQUEST_METHOD'] === 'POST') {
49 $adminName = (string)($GLOBALS['user_data']['name'] ?? '');
50 $action = (string)($_POST['itmed_action'] ?? '');
51
52 if ($action === 'save') {
53 $id = intv($_POST['id'] ?? 0);
54 $name = trim((string)($_POST['name'] ?? ''));
55
56 if ($id <= 0 || $name === '') {
57 acp_flash_error(t_default('acp.itmed.missing_fields', 'ID and name are required.'));
58 acp_redirect('items_editor');
59 }
60
61 $record = array(
62 'id' => (string)$id,
63 'name' => $name,
64 'attributes' => itmed_attr_map_from_post((array)($_POST['attr'] ?? array())),
65 );
66
67 if (serverdata_override_set('items', ITMED_TYPE . ':' . $id, $record, $adminName)) {
68 acp_log('serverdata.item_save', $name, ['type' => ITMED_TYPE, 'id' => $id]);
69 acp_flash_success(t_default('acp.itmed.saved', 'Item #{id} saved.', ['id' => $id]));
70 } else {
71 acp_flash_error(t_default('acp.itmed.save_failed', 'Could not save - run the pending database migration first (Admin Panel > Migrations).'));
72 }
73 acp_redirect('items_editor');
74 }
75
76 if ($action === 'delete') {
77 $id = intv($_POST['id'] ?? 0);
78 if (serverdata_override_delete('items', ITMED_TYPE . ':' . $id, $adminName)) {
79 acp_log('serverdata.item_delete', '#' . $id, ['type' => ITMED_TYPE, 'id' => $id]);
80 acp_flash_success(t_default('acp.itmed.removed', 'Item #{id} removed.', ['id' => $id]));
81 } else {
82 acp_flash_error(t_default('acp.itmed.remove_failed', 'Could not remove that item.'));
83 }
84 acp_redirect('items_editor');
85 }
86}
87
88$editId = intv($_GET['edit'] ?? 0);
89$editing = null;
90if ($editId > 0 && isset($items[ITMED_TYPE][(string)$editId])) {
91 $editing = $items[ITMED_TYPE][(string)$editId];
92}
93
94$attrRows = itmed_attr_rows($editing['attributes'] ?? array());
95
96$attrColumns = array(
97 'key' => array('label' => t_default('acp.itmed.col_attr_key', 'Attribute'), 'type' => 'text'),
98 'value' => array('label' => t_default('acp.itmed.col_attr_value', 'Value'), 'type' => 'text'),
99);
100
101$list = $items[ITMED_TYPE] ?? array();
102ksort($list, SORT_NUMERIC);
103?>
104
105<section class="acp-card">
106 <header class="acp-card-head">
107 <h2><?= $editing ? h(t_default('acp.itmed.edit_title', 'Edit item #{id}', ['id' => $editId])) : h(t_default('acp.itmed.add_title', 'Add an item')) ?></h2>
108 <p><?= h(t_default('acp.itmed.sub', 'Saved separately from items.xml - a later re-upload will not overwrite this.')) ?></p>
109 </header>
110 <div class="acp-card-body">
111 <form method="post">
112 <?= acp_csrf_field() ?>
113 <input type="hidden" name="itmed_action" value="save">
114
115 <div class="acp-row">
116 <div class="acp-field">
117 <label class="acp-label" for="itmed_id"><?= t_default('acp.itmed.field_id', 'Item ID') ?></label>
118 <input class="acp-input" type="number" min="1" id="itmed_id" name="id" value="<?= $editing ? (int)$editId : '' ?>" <?= $editing ? 'readonly' : '' ?> required>
119 </div>
120 <div class="acp-field">
121 <label class="acp-label" for="itmed_name"><?= t_default('acp.itmed.field_name', 'Name') ?></label>
122 <input class="acp-input" type="text" id="itmed_name" name="name" value="<?= h((string)($editing['name'] ?? '')) ?>" required>
123 </div>
124 </div>
125
126 <div class="acp-field">
127 <label class="acp-label"><?= t_default('acp.itmed.field_attrs', 'Attributes') ?></label>
128 <table class="acp-table acp-table--editable" data-acp-table="itmedAttrs">
129 <thead>
130 <tr>
131 <?php foreach ($attrColumns as $colDef): ?>
132 <th><?= h($colDef['label']) ?></th>
133 <?php endforeach; ?>
134 <th></th>
135 </tr>
136 </thead>
137 <tbody data-acp-table-body>
138 <?php foreach ($attrRows as $i => $row): ?>
139 <tr>
140 <?php foreach ($attrColumns as $colKey => $colDef): ?>
141 <td><?= acp_table_cell_input($colDef, 'attr[' . (int)$i . '][' . $colKey . ']', (string)($row[$colKey] ?? '')) ?></td>
142 <?php endforeach; ?>
143 <td><button type="button" class="acp-btn acp-btn--sm acp-btn--red" data-acp-table-remove>&times;</button></td>
144 </tr>
145 <?php endforeach; ?>
146 </tbody>
147 </table>
148 <button type="button" class="acp-btn acp-btn--sm" data-acp-table-add="itmedAttrs">
149 <i class="fa fa-plus"></i> <?= t_default('acp.itmed.add_attr', 'Add attribute') ?>
150 </button>
151 <template data-acp-table-template="itmedAttrs">
152 <tr>
153 <?php foreach ($attrColumns as $colKey => $colDef): ?>
154 <td><?= acp_table_cell_input($colDef, 'attr[__ROWIDX__][' . $colKey . ']', '') ?></td>
155 <?php endforeach; ?>
156 <td><button type="button" class="acp-btn acp-btn--sm acp-btn--red" data-acp-table-remove>&times;</button></td>
157 </tr>
158 </template>
159 </div>
160
161 <div class="acp-actions">
162 <button class="acp-btn acp-btn--green" type="submit"><i class="fa fa-check"></i> <?= $editing ? t_default('acp.itmed.save_btn', 'Save') : t_default('acp.itmed.add_btn', 'Add item') ?></button>
163 <?php if ($editing): ?>
164 <a class="acp-btn acp-btn--ghost" href="<?= h(acp_url('items_editor')) ?>"><?= t_default('acp.itmed.cancel', 'Cancel edit') ?></a>
165 <?php endif; ?>
166 </div>
167 </form>
168 </div>
169</section>
170
171<section class="acp-card">
172 <header class="acp-card-head">
173 <h2><?= t_default('acp.itmed.list_title', 'Published items') ?></h2>
174 </header>
175 <div class="acp-card-body is-flush">
176 <?php if ($list): ?>
177 <?php if (count($list) > 8): ?>
178 <div class="acp-toolbar">
179 <div style="display:flex;gap:8px;flex:1 1 320px;max-width:460px;">
180 <input class="acp-input" type="search" data-acp-search-input="itmedTable"
181 placeholder="<?= h(t_default('acp.itmed.search_placeholder', 'Search items...')) ?>">
182 </div>
183 <span class="is-muted" data-acp-search-count="itmedTable"></span>
184 </div>
185 <?php endif; ?>
186 <div class="acp-table-wrap">
187 <table class="acp-table" data-sortable id="itmedTable">
188 <thead>
189 <tr>
190 <th>ID</th>
191 <th><?= t_default('acp.itmed.col_name', 'Name') ?></th>
192 <th class="is-num"><?= t_default('acp.itmed.col_actions', 'Actions') ?></th>
193 </tr>
194 </thead>
195 <tbody>
196 <?php foreach ($list as $id => $rec): ?>
197 <tr data-acp-search="<?= h(strtolower($id . ' ' . (string)($rec['name'] ?? ''))) ?>">
198 <td class="is-muted"><?= h((string)$id) ?></td>
199 <td><?= h((string)($rec['name'] ?? '')) ?></td>
200 <td class="is-nowrap is-num">
201 <a class="acp-btn acp-btn--ghost acp-btn--sm" href="<?= h(acp_url('items_editor', array('edit' => $id))) ?>"><i class="fa fa-pencil"></i></a>
202 <form class="acp-inline-form" method="post" data-confirm="<?= h(t_default('acp.itmed.confirm_remove', 'Remove this item?')) ?>">
203 <?= acp_csrf_field() ?>
204 <input type="hidden" name="itmed_action" value="delete">
205 <input type="hidden" name="id" value="<?= h((string)$id) ?>">
206 <button class="acp-btn acp-btn--red acp-btn--sm" type="submit"><i class="fa fa-times"></i></button>
207 </form>
208 </td>
209 </tr>
210 <?php endforeach; ?>
211 </tbody>
212 </table>
213 </div>
214 <?php else: ?>
215 <?php acp_empty(t_default('acp.itmed.empty', 'No items published yet.'), 'fa-shield'); ?>
216 <?php endif; ?>
217 </div>
218</section>
219
220<p class="acp-hint"><a href="<?= h(acp_url('serverinfo')) ?>"><i class="fa fa-arrow-left"></i> <?= t_default('acp.itmed.back', 'Back to Server Info') ?></a></p>
221
Top