+ @foreach (var opt in Model.Options)
+ {
+ var cbId = h.FieldId + "-" + opt.Value;
+
+ }
+
+}
+else
+{
+
+ @if (Model.Options != null)
+ {
+ @foreach (var opt in Model.Options)
+ {
+ var radioId = h.FieldId + "-" + opt.Value;
+
+ }
+ }
+
+@h.Error()
diff --git a/uTPro/Feature/uTPro.Feature.SimpleForm/Views/Partials/SimpleForm/Fields/range.cshtml b/uTPro/Feature/uTPro.Feature.SimpleForm/Views/Partials/SimpleForm/Fields/range.cshtml
new file mode 100644
index 0000000..eaa8088
--- /dev/null
+++ b/uTPro/Feature/uTPro.Feature.SimpleForm/Views/Partials/SimpleForm/Fields/range.cshtml
@@ -0,0 +1,20 @@
+@* Range slider with min/max/step *@
+@using uTPro.Feature.SimpleForm.Helpers
+@model uTPro.Feature.SimpleForm.Models.FormFieldViewModel
+@{
+ var h = new FieldHelper(Model, ViewData);
+ var min = h.Attr("min", "0");
+ var max = h.Attr("max", "100");
+ var step = h.Attr("step", "1");
+ var defaultVal = Model.DefaultValue ?? min;
+}
+
+@h.Label()
+}
+ */
+export async function apiPost(url, body = {}, authContext = null) {
+ const config = authContext?.getOpenApiConfiguration();
+ const headers = { 'Content-Type': 'application/json' };
+ if (config?.token) {
+ const t = await config.token();
+ if (t) headers['Authorization'] = 'Bearer ' + t;
+ }
+ const resp = await fetch(url, {
+ method: 'POST',
+ headers,
+ credentials: config?.credentials || 'same-origin',
+ body: JSON.stringify(body)
+ });
+ if (!resp.ok) {
+ const e = await resp.json().catch(() => ({}));
+ throw new Error(e.message || 'Failed');
+ }
+ return resp.json();
+}
diff --git a/uTPro/Feature/uTPro.Feature.SimpleForm/wwwroot/App_Plugins/simple-form/index.js b/uTPro/Feature/uTPro.Feature.SimpleForm/wwwroot/App_Plugins/simple-form/index.js
new file mode 100644
index 0000000..2459f7e
--- /dev/null
+++ b/uTPro/Feature/uTPro.Feature.SimpleForm/wwwroot/App_Plugins/simple-form/index.js
@@ -0,0 +1,419 @@
+// ── Entry point: Simple Form Dashboard ──
+// Views and styles are split into separate files for maintainability.
+// api.js – API helper & constants
+// styles.js – All CSS styles
+// views/list-view.js – Form list
+// views/editor-view.js – Form editor
+// views/entries-view.js – Entries table
+// views/detail-view.js – Entry detail overlay
+
+import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
+import { html, nothing } from '@umbraco-cms/backoffice/external/lit';
+import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth';
+
+import { API, apiPost } from './api.js';
+import { dashboardStyles } from './styles.js';
+import { renderList } from './views/list-view.js';
+import { renderEditor } from './views/editor-view.js';
+import { renderEntries } from './views/entries-view.js';
+import { renderDetail } from './views/detail-view.js';
+
+export class UtproSimpleFormDashboard extends UmbLitElement {
+
+ // ── Reactive properties ──
+ static properties = {
+ _view: { type: String, state: true },
+ _forms: { type: Array, state: true },
+ _loading: { type: Boolean, state: true },
+ _editForm: { type: Object, state: true },
+ _fieldTypes: { type: Array, state: true },
+ _entries: { type: Array, state: true },
+ _entryTotal: { type: Number, state: true },
+ _entrySkip: { type: Number, state: true },
+ _viewFormId: { type: Number, state: true },
+ _error: { type: String, state: true },
+ _success: { type: String, state: true },
+ _selectedEntries: { type: Array, state: true },
+ _detailEntry: { type: Object, state: true },
+ _permissions: { type: Object, state: true },
+ _search: { type: String, state: true },
+ _dateFrom: { type: String, state: true },
+ _dateTo: { type: String, state: true },
+ _showColumnSettings: { type: Boolean, state: true },
+ _entryCount: { type: Number, state: true },
+ _typePickerIdx: { type: Number, state: true },
+ _typePickerSearch: { type: String, state: true },
+ _typePickerGroupIdx: { type: Number, state: true },
+ _typePickerColIdx: { type: Number, state: true },
+ _fieldSettingsLoc: { type: Object, state: true },
+ };
+
+ // ── Styles ──
+ static styles = dashboardStyles;
+
+ #authContext;
+
+ constructor() {
+ super();
+ this._view = 'list';
+ this._forms = [];
+ this._loading = false;
+ this._editForm = null;
+ this._fieldTypes = [];
+ this._entries = [];
+ this._entryTotal = 0;
+ this._entrySkip = 0;
+ this._viewFormId = 0;
+ this._error = '';
+ this._success = '';
+ this._selectedEntries = [];
+ this._detailEntry = null;
+ this._permissions = { isAdmin: false, canViewSensitive: false };
+ this._search = '';
+ this._dateFrom = '';
+ this._dateTo = '';
+ this._showColumnSettings = false;
+ this._entryCount = 0;
+ this._typePickerIdx = -1;
+ this._typePickerSearch = '';
+ this._typePickerGroupIdx = -1;
+ this._typePickerColIdx = -1;
+ this._fieldSettingsLoc = null;
+ this.consumeContext(UMB_AUTH_CONTEXT, (ctx) => { this.#authContext = ctx; });
+ }
+
+ async connectedCallback() {
+ super.connectedCallback();
+ await this._loadPermissions();
+ await this._loadForms();
+ await this._loadFieldTypes();
+ }
+
+ // ── API helper ──
+ async _api(url, body = {}) {
+ return apiPost(url, body, this.#authContext);
+ }
+
+ _msg(m, err = false) {
+ if (err) { this._error = m; this._success = ''; }
+ else { this._success = m; this._error = ''; }
+ setTimeout(() => { this._error = ''; this._success = ''; }, 3000);
+ }
+
+ // ── Permissions ──
+ async _loadPermissions() {
+ try {
+ this._permissions = await this._api(API + '/permissions');
+ } catch {
+ this._permissions = { isAdmin: false, canViewSensitive: false };
+ }
+ }
+
+ // ── Data loading ──
+ async _loadForms() {
+ this._loading = true;
+ try { this._forms = await this._api(API + '/list'); }
+ catch (e) { this._msg(e.message, true); }
+ this._loading = false;
+ }
+
+ async _loadFieldTypes() {
+ try { this._fieldTypes = await this._api(API + '/field-types'); } catch {}
+ }
+
+ // ── Form CRUD ──
+ _newForm() {
+ this._editForm = {
+ id: 0, name: '', alias: '', fields: [], groups: [],
+ successMessage: 'Thank you!', redirectUrl: '', emailTo: '', emailSubject: '',
+ storeEntries: true, isEnabled: true
+ };
+ this._view = 'edit';
+ }
+
+ async _editExisting(id) {
+ try {
+ this._editForm = await this._api(API + '/get', { id });
+ this._showColumnSettings = false;
+ const res = await this._api(API + '/entries', { formId: id, skip: 0, take: 1 });
+ this._entryCount = res.total || 0;
+ this._view = 'edit';
+ } catch (e) { this._msg(e.message, true); }
+ }
+
+ async _saveForm() {
+ if (!this._editForm.name || !this._editForm.alias) {
+ this._msg('Name and Alias required', true);
+ return;
+ }
+ try {
+ const res = await this._api(API + '/save', this._editForm);
+ this._msg(res.message);
+ this._editForm.id = res.id;
+ await this._loadForms();
+ } catch (e) { this._msg(e.message, true); }
+ }
+
+ async _deleteForm(id) {
+ if (!confirm('Delete this form and all entries?')) return;
+ try {
+ await this._api(API + '/delete', { id });
+ this._msg('Deleted');
+ await this._loadForms();
+ if (this._editForm?.id === id) { this._editForm = null; this._view = 'list'; }
+ } catch (e) { this._msg(e.message, true); }
+ }
+
+ // ── Group management ──
+ _addGroup() {
+ const f = this._editForm;
+ if (!f.groups) f.groups = [];
+ const idx = f.groups.length;
+ f.groups = [...f.groups, {
+ id: crypto.randomUUID?.() || Date.now().toString(36),
+ name: '',
+ cssClass: '',
+ columns: [{ id: crypto.randomUUID?.() || Date.now().toString(36), width: 12, fields: [] }],
+ sortOrder: idx
+ }];
+ this.requestUpdate();
+ }
+
+ _removeGroup(gIdx) {
+ if (!confirm('Remove this group and all its columns/fields?')) return;
+ this._editForm.groups = this._editForm.groups.filter((_, i) => i !== gIdx);
+ this._editForm.groups.forEach((g, i) => g.sortOrder = i);
+ this.requestUpdate();
+ }
+
+ _moveGroup(gIdx, dir) {
+ const arr = [...this._editForm.groups];
+ const newIdx = gIdx + dir;
+ if (newIdx < 0 || newIdx >= arr.length) return;
+ [arr[gIdx], arr[newIdx]] = [arr[newIdx], arr[gIdx]];
+ arr.forEach((g, i) => g.sortOrder = i);
+ this._editForm.groups = arr;
+ this.requestUpdate();
+ }
+
+ _updateGroup(gIdx, key, val) {
+ this._editForm.groups[gIdx][key] = val;
+ this.requestUpdate();
+ }
+
+ // ── Column management within a group ──
+ _addColumn(gIdx) {
+ const g = this._editForm.groups[gIdx];
+ if (!g.columns) g.columns = [];
+ g.columns = [...g.columns, {
+ id: crypto.randomUUID?.() || Date.now().toString(36),
+ width: 6,
+ fields: []
+ }];
+ this.requestUpdate();
+ }
+
+ _removeColumn(gIdx, cIdx) {
+ if (!confirm('Remove this column and all its fields?')) return;
+ this._editForm.groups[gIdx].columns = this._editForm.groups[gIdx].columns.filter((_, i) => i !== cIdx);
+ this.requestUpdate();
+ }
+
+ _updateColumnWidth(gIdx, cIdx, val) {
+ this._editForm.groups[gIdx].columns[cIdx].width = Math.min(12, Math.max(1, parseInt(val) || 1));
+ this.requestUpdate();
+ }
+
+ /**
+ * Move an entire column (with all its fields) to another group.
+ * @param {number} fromGIdx - source group index
+ * @param {number} cIdx - column index within source group
+ * @param {number} toGIdx - destination group index
+ */
+ _moveColumnTo(fromGIdx, cIdx, toGIdx) {
+ const f = this._editForm;
+ const col = f.groups[fromGIdx].columns.splice(cIdx, 1)[0];
+ if (!col) return;
+ f.groups[toGIdx].columns.push(col);
+ this.requestUpdate();
+ }
+
+ _swapColumn(gIdx, cIdx, dir) {
+ const cols = this._editForm.groups[gIdx].columns;
+ const newIdx = cIdx + dir;
+ if (newIdx < 0 || newIdx >= cols.length) return;
+ [cols[cIdx], cols[newIdx]] = [cols[newIdx], cols[cIdx]];
+ this.requestUpdate();
+ }
+
+ // ── Field management within a column ──
+ _addFieldToColumn(gIdx, cIdx) {
+ const col = this._editForm.groups[gIdx].columns[cIdx];
+ const idx = col.fields.length;
+ col.fields = [...col.fields, {
+ id: crypto.randomUUID?.() || Date.now().toString(36),
+ type: 'text', label: '', name: 'field_' + Date.now().toString(36),
+ placeholder: '', cssClass: '', required: false,
+ validation: '', validationMessage: '', defaultValue: '',
+ options: [], sortOrder: idx, attributes: {}
+ }];
+ this.requestUpdate();
+ }
+
+ _removeFieldFromColumn(gIdx, cIdx, fIdx) {
+ const removedName = this._editForm.groups[gIdx].columns[cIdx].fields[fIdx]?.name;
+ this._editForm.groups[gIdx].columns[cIdx].fields = this._editForm.groups[gIdx].columns[cIdx].fields.filter((_, i) => i !== fIdx);
+ if (removedName && this._editForm.visibleColumns) {
+ this._editForm.visibleColumns = this._editForm.visibleColumns.filter(c => c !== removedName);
+ }
+ this.requestUpdate();
+ }
+
+ _moveFieldInColumn(gIdx, cIdx, fIdx, dir) {
+ const arr = [...this._editForm.groups[gIdx].columns[cIdx].fields];
+ const newIdx = fIdx + dir;
+ if (newIdx < 0 || newIdx >= arr.length) return;
+ [arr[fIdx], arr[newIdx]] = [arr[newIdx], arr[fIdx]];
+ arr.forEach((f, i) => f.sortOrder = i);
+ this._editForm.groups[gIdx].columns[cIdx].fields = arr;
+ this.requestUpdate();
+ }
+
+ _updateFieldInColumn(gIdx, cIdx, fIdx, key, val) {
+ this._editForm.groups[gIdx].columns[cIdx].fields[fIdx][key] = val;
+ if (key === 'type' && val === 'password') {
+ this._editForm.groups[gIdx].columns[cIdx].fields[fIdx].isSensitive = true;
+ }
+ this.requestUpdate();
+ }
+
+ _addOptionInColumn(gIdx, cIdx, fIdx) {
+ if (!this._editForm.groups[gIdx].columns[cIdx].fields[fIdx].options)
+ this._editForm.groups[gIdx].columns[cIdx].fields[fIdx].options = [];
+ this._editForm.groups[gIdx].columns[cIdx].fields[fIdx].options.push({ text: '', value: '' });
+ this.requestUpdate();
+ }
+
+ _removeOptionInColumn(gIdx, cIdx, fIdx, oIdx) {
+ this._editForm.groups[gIdx].columns[cIdx].fields[fIdx].options.splice(oIdx, 1);
+ this.requestUpdate();
+ }
+
+ // ── Move field between groups/columns ──
+ /**
+ * Move a field from one location to another.
+ * @param {object} from - { gIdx, cIdx, fIdx } source (-1 for ungrouped)
+ * @param {object} to - { gIdx, cIdx } destination (-1 for ungrouped)
+ */
+ _moveFieldTo(from, to) {
+ const f = this._editForm;
+
+ // Remove from source column
+ const field = f.groups[from.gIdx].columns[from.cIdx].fields.splice(from.fIdx, 1)[0];
+ if (!field) return;
+
+ // Add to destination column
+ const destCol = f.groups[to.gIdx].columns[to.cIdx];
+ field.sortOrder = destCol.fields.length;
+ destCol.fields.push(field);
+
+ this.requestUpdate();
+ }
+
+ // ── Entries ──
+ async _viewEntries(formId) {
+ this._viewFormId = formId;
+ this._entrySkip = 0;
+ this._search = '';
+ this._dateFrom = '';
+ this._dateTo = '';
+ this._selectedEntries = [];
+ this._view = 'entries';
+ await this._loadEntries();
+ }
+
+ async _loadEntries() {
+ try {
+ const body = {
+ formId: this._viewFormId, skip: this._entrySkip, take: 20
+ };
+ if (this._search) body.search = this._search;
+ if (this._dateFrom) body.dateFrom = this._dateFrom;
+ if (this._dateTo) body.dateTo = this._dateTo;
+ const res = await this._api(API + '/entries', body);
+ this._entries = res.items || [];
+ this._entryTotal = res.total || 0;
+ } catch (e) { this._msg(e.message, true); }
+ }
+
+ async _deleteEntry(id) {
+ if (!confirm('Delete this entry?')) return;
+ try {
+ await this._api(API + '/delete-entry', { id });
+ this._msg('Deleted');
+ this._selectedEntries = this._selectedEntries.filter(x => x !== id);
+ await this._loadEntries();
+ } catch (e) { this._msg(e.message, true); }
+ }
+
+ _toggleEntrySelect(id) {
+ if (this._selectedEntries.includes(id))
+ this._selectedEntries = this._selectedEntries.filter(x => x !== id);
+ else
+ this._selectedEntries = [...this._selectedEntries, id];
+ }
+
+ _toggleSelectAll() {
+ if (this._selectedEntries.length === this._entries.length)
+ this._selectedEntries = [];
+ else
+ this._selectedEntries = this._entries.map(s => s.id);
+ }
+
+ async _bulkDelete() {
+ if (!this._selectedEntries.length) return;
+ if (!confirm('Delete ' + this._selectedEntries.length + ' entries?')) return;
+ for (const id of this._selectedEntries) {
+ try { await this._api(API + '/delete-entry', { id }); } catch {}
+ }
+ this._selectedEntries = [];
+ this._msg('Deleted');
+ await this._loadEntries();
+ }
+
+ _exportCsv() {
+ if (!this._entries.length) return;
+ const allKeys = [...new Set(this._entries.flatMap(s => Object.keys(s.data || {})))];
+ const headers = ['Date', 'IP', ...allKeys];
+ const rows = this._entries.map(s => {
+ const date = new Date(s.createdUtc).toLocaleString();
+ const ip = s.ipAddress || '';
+ const fields = allKeys.map(k => '"' + (s.data?.[k] || '').replace(/"/g, '""') + '"');
+ return ['"' + date + '"', '"' + ip + '"', ...fields].join(',');
+ });
+ const csv = headers.join(',') + '\n' + rows.join('\n');
+ const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ const formName = this._forms.find(f => f.id === this._viewFormId)?.alias || 'form';
+ a.href = url; a.download = formName + '-entries.csv'; a.click();
+ URL.revokeObjectURL(url);
+ }
+
+ _viewDetail(entry) { this._detailEntry = entry; }
+ _closeDetail() { this._detailEntry = null; }
+
+ // ── Render ──
+ render() {
+ return html`
+ ${this._error ? html`${this._error}
` : nothing}
+ ${this._success ? html`${this._success}
` : nothing}
+ ${this._view === 'list' ? renderList(this)
+ : this._view === 'edit' ? renderEditor(this)
+ : renderEntries(this)}
+ ${this._detailEntry ? renderDetail(this) : nothing}`;
+ }
+}
+
+customElements.define('utpro-simple-form-dashboard', UtproSimpleFormDashboard);
+export default UtproSimpleFormDashboard;
diff --git a/uTPro/Feature/uTPro.Feature.SimpleForm/wwwroot/App_Plugins/simple-form/lang/en-us.js b/uTPro/Feature/uTPro.Feature.SimpleForm/wwwroot/App_Plugins/simple-form/lang/en-us.js
new file mode 100644
index 0000000..1818b14
--- /dev/null
+++ b/uTPro/Feature/uTPro.Feature.SimpleForm/wwwroot/App_Plugins/simple-form/lang/en-us.js
@@ -0,0 +1,5 @@
+export default {
+ simpleForm: {
+ title: "Form Builder",
+ }
+}
diff --git a/uTPro/Feature/uTPro.Feature.SimpleForm/wwwroot/App_Plugins/simple-form/styles.js b/uTPro/Feature/uTPro.Feature.SimpleForm/wwwroot/App_Plugins/simple-form/styles.js
new file mode 100644
index 0000000..5827a80
--- /dev/null
+++ b/uTPro/Feature/uTPro.Feature.SimpleForm/wwwroot/App_Plugins/simple-form/styles.js
@@ -0,0 +1,336 @@
+import { css } from '@umbraco-cms/backoffice/external/lit';
+
+export const dashboardStyles = css`
+ :host { display: block; padding: 20px; }
+
+ /* ── Toolbar ── */
+ .toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; gap: 12px; flex-wrap: wrap; }
+ .toolbar h2 { margin: 0; font-size: 1.3rem; }
+ .toolbar-right { display: flex; align-items: center; gap: 8px; margin-left: auto; }
+
+ /* ── Messages ── */
+ .msg { padding: 8px 14px; border-radius: 4px; margin-bottom: 10px; font-size: 0.9rem; }
+ .error { background: #fde8e8; color: #c0392b; }
+ .success { background: #e8fde8; color: #27ae60; }
+
+ /* ── States ── */
+ .loading { display: flex; justify-content: center; padding: 40px; }
+ .empty { text-align: center; padding: 40px; color: #888; font-style: italic; }
+
+ /* ── List view ── */
+ .link { color: var(--uui-color-interactive, #1b264f); cursor: pointer; font-weight: 500; text-decoration: none; }
+ .link:hover { text-decoration: underline; }
+ .badge { padding: 2px 8px; border-radius: 10px; font-size: 0.8rem; font-weight: 500; }
+ .badge.on { background: #e8fde8; color: #27ae60; }
+ .badge.off { background: #fde8e8; color: #c0392b; }
+ .action-cell { display: flex; gap: 4px; }
+ code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 0.85rem; }
+ uui-table { width: 100%; }
+
+ /* ── Form editor grid ── */
+ .form-grid {
+ display: grid; grid-template-columns: 1fr 1fr; gap: 12px;
+ margin-bottom: 20px; padding: 16px;
+ background: var(--uui-color-surface-alt, #f9f9f9); border-radius: 6px;
+ }
+ .form-grid label { display: flex; flex-direction: column; gap: 4px; font-size: 0.85rem; font-weight: 500; }
+ .check-label { flex-direction: row !important; align-items: center; gap: 8px !important; }
+
+ /* ── Section headers ── */
+ .section-header { display: flex; justify-content: space-between; align-items: center; margin: 16px 0 8px; }
+ .section-header h3 { margin: 0; }
+
+ /* ── Field cards ── */
+ .field-card {
+ border: 1px solid var(--uui-color-border, #ddd); border-radius: 6px;
+ overflow: hidden; margin-bottom: 10px;
+ }
+ .field-card.field-hidden {
+ opacity: 0.5; border-style: dashed;
+ }
+ .field-header {
+ display: flex; align-items: center; gap: 8px; padding: 10px 14px;
+ background: var(--uui-color-surface-alt, #f4f4f4); flex-wrap: wrap;
+ }
+ .field-num { font-weight: 600; color: #888; min-width: 30px; }
+ .field-header select, .field-body select {
+ padding: 4px 8px; border: 1px solid #ccc; border-radius: 4px;
+ font-size: 0.9rem; background: #fff; height: 32px; box-sizing: border-box;
+ }
+ .field-actions { margin-left: auto; display: flex; gap: 4px; }
+
+ /* ── Type picker button ── */
+ .type-picker-btn {
+ padding: 4px 12px; border: 1px solid #ccc; border-radius: 4px;
+ font-size: 0.9rem; background: #fff; height: 32px; box-sizing: border-box;
+ cursor: pointer; display: flex; align-items: center; gap: 6px;
+ transition: border-color 0.2s;
+ }
+ .type-picker-btn:hover { border-color: #888; }
+
+ /* ── Type picker dialog ── */
+ .type-picker-dialog {
+ background: var(--uui-color-surface, #fff); border-radius: 8px;
+ width: 400px; max-width: 90vw; height: 480px; display: flex; flex-direction: column;
+ box-shadow: 0 8px 32px rgba(0,0,0,0.3);
+ }
+ .type-picker-header {
+ display: flex; justify-content: space-between; align-items: center;
+ padding: 14px 18px; border-bottom: 1px solid #e0e0e0;
+ }
+ .type-picker-header h3 { margin: 0; font-size: 1rem; }
+ .type-picker-search { padding: 12px 18px; border-bottom: 1px solid #f0f0f0; }
+ .type-picker-search uui-input { width: 100%; }
+ .type-picker-list { overflow-y: auto; flex: 1; padding: 6px 0; }
+ .type-picker-option {
+ display: flex; align-items: center; justify-content: space-between;
+ width: 100%; padding: 7px 18px; border: none; background: none;
+ cursor: pointer; font-size: 0.85rem; text-align: left;
+ transition: background 0.1s;
+ }
+ .type-picker-option:hover { background: var(--uui-color-surface-alt, #f4f4f4); }
+ .type-picker-option.active {
+ background: var(--uui-color-surface-alt, #f3f3f5);
+ font-weight: 600;
+ border-left: 3px solid var(--uui-color-interactive, #1b264f);
+ }
+ .type-picker-label { flex: 1; }
+ .type-picker-type { color: #999; font-size: 0.8rem; font-family: monospace; }
+ .type-picker-empty { padding: 20px; text-align: center; color: #888; font-style: italic; }
+ .field-body {
+ display: grid; grid-template-columns: 1fr 1fr; gap: 10px; padding: 14px;
+ }
+ .field-body label { display: flex; flex-direction: column; gap: 4px; font-size: 0.8rem; font-weight: 500; min-width: 0; overflow: hidden; }
+ .field-body uui-input { width: 100%; min-width: 0; }
+ .field-body select { width: 100%; }
+
+ /* ── Options (select/radio/checkbox) ── */
+ .options-section { padding: 0 14px 14px; }
+
+ /* ── Type-specific attributes ── */
+ .field-attrs {
+ display: flex; gap: 10px; padding: 8px 14px;
+ background: var(--uui-color-surface-alt, #fafafa);
+ border-top: 1px solid #eee; flex-wrap: wrap;
+ }
+ .field-attrs label {
+ display: flex; flex-direction: column; gap: 4px;
+ font-size: 0.8rem; font-weight: 500; flex: 1; min-width: 120px;
+ }
+ .field-attrs uui-input { width: 100%; }
+ .div-content-label { grid-column: 1 / -1; }
+ .div-content-editor {
+ width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 4px;
+ font-family: monospace; font-size: 0.85rem; resize: vertical;
+ background: #fff; box-sizing: border-box;
+ }
+
+ /* ── Settings panel ── */
+ .settings-panel {
+ margin-bottom: 16px; padding: 16px;
+ background: var(--uui-color-surface-alt, #f0f4ff);
+ border: 1px solid #c8d6f0; border-radius: 6px;
+ }
+ .settings-header { margin-bottom: 12px; }
+ .settings-header h3 { margin: 0 0 4px; font-size: 1rem; }
+ .settings-hint { font-size: 0.8rem; color: #888; }
+ .settings-body {
+ display: flex; flex-wrap: wrap; gap: 10px 20px;
+ }
+ .settings-col-item {
+ min-width: 140px; padding: 4px 8px;
+ background: #fff; border: 1px solid #e0e0e0; border-radius: 4px;
+ cursor: grab; user-select: none; transition: box-shadow 0.15s, opacity 0.15s;
+ }
+ .settings-col-item:active { cursor: grabbing; }
+ .settings-col-item.dragging { opacity: 0.4; }
+ .settings-col-item.drag-over { box-shadow: inset 0 0 0 2px var(--uui-color-interactive, #1b264f); }
+ .drag-handle { color: #aaa; margin-right: 4px; font-size: 0.8rem; }
+ .option-row { display: flex; gap: 8px; margin-bottom: 6px; align-items: center; }
+ .option-row uui-input { flex: 1; }
+
+ /* ── Embed info ── */
+ .embed-render-row {
+ display: flex; align-items: center; gap: 12px; margin: 6px 0;
+ }
+ .embed-render-row code {
+ flex: 1; display: inline-block; padding: 6px 10px;
+ background: #fff; border: 1px solid #e0e0e0; border-radius: 4px;
+ font-size: 0.85rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
+ }
+
+ /* ── Entries ── */
+ .filter-bar {
+ display: flex; align-items: center; gap: 12px; margin-bottom: 16px;
+ padding: 10px 14px; background: var(--uui-color-surface-alt, #f9f9f9);
+ border-radius: 6px; flex-wrap: wrap;
+ }
+ .filter-search { flex: 1; min-width: 200px; }
+ .filter-dates { display: flex; align-items: center; gap: 10px; margin-left: auto; }
+ .filter-date-label {
+ display: flex; align-items: center; gap: 4px; font-size: 0.85rem; font-weight: 500;
+ }
+ .filter-date-label input[type="date"] {
+ padding: 4px 8px; border: 1px solid #ccc; border-radius: 4px;
+ font-size: 0.85rem; background: #fff; height: 32px; box-sizing: border-box;
+ }
+ .pagination { display: flex; justify-content: center; align-items: center; gap: 12px; margin-top: 16px; }
+ .page-info { color: #888; font-size: 0.9rem; }
+ .cell-truncate { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+ .row-selected { background: var(--uui-color-surface-alt, #f0f4ff) !important; }
+
+ /* ── Detail overlay ── */
+ .overlay {
+ position: fixed; top: 0; left: 0; right: 0; bottom: 0;
+ background: rgba(0,0,0,0.5); z-index: 9999;
+ display: flex; justify-content: center; align-items: center;
+ }
+ .overlay.overlay-top { z-index: 10001; }
+ .detail-panel {
+ background: var(--uui-color-surface, #fff); border-radius: 8px;
+ width: 600px; max-width: 90vw; max-height: 80vh; display: flex; flex-direction: column;
+ box-shadow: 0 8px 32px rgba(0,0,0,0.3);
+ }
+ .detail-header {
+ display: flex; justify-content: space-between; align-items: center;
+ padding: 16px 20px; border-bottom: 1px solid #e0e0e0;
+ }
+ .detail-header h3 { margin: 0; }
+ .detail-body { padding: 20px; overflow-y: auto; flex: 1; }
+ .detail-row {
+ display: flex; padding: 10px 0; border-bottom: 1px solid #f0f0f0;
+ }
+ .detail-label { font-weight: 600; min-width: 120px; color: #555; font-size: 0.9rem; }
+ .detail-value { flex: 1; word-break: break-word; white-space: pre-wrap; }
+ .detail-footer { padding: 12px 20px; border-top: 1px solid #e0e0e0; display: flex; justify-content: flex-end; }
+
+ /* ── Group cards ── */
+ .group-card {
+ border: 2px solid var(--uui-color-border, #ccc); border-radius: 8px;
+ margin-bottom: 16px; overflow: hidden;
+ background: var(--uui-color-surface, #fff);
+ }
+ .group-header {
+ display: flex; align-items: center; gap: 12px; padding: 12px 16px;
+ background: var(--uui-color-surface-alt, #f0f0f5); flex-wrap: wrap;
+ }
+ .group-num { font-weight: 700; font-size: 1rem; color: #555; min-width: 80px; }
+ .group-settings { display: flex; gap: 10px; flex: 1; flex-wrap: wrap; align-items: flex-end; }
+ .group-setting-label {
+ display: flex; flex-direction: column; gap: 3px;
+ font-size: 0.8rem; font-weight: 500; min-width: 100px;
+ }
+ .group-setting-label uui-input { width: 100%; min-width: 80px; }
+ .group-actions { display: flex; gap: 4px; margin-left: auto; }
+ .group-preview {
+ padding: 10px 16px; border-bottom: 1px solid #eee;
+ background: var(--uui-color-surface-alt, #fafafa);
+ }
+ .group-preview-label { font-size: 0.8rem; color: #666; margin-bottom: 6px; display: block; }
+ .group-col-preview { min-height: 32px; }
+ .group-col-cell {
+ background: #e0e0e0; border-radius: 4px; padding: 6px 10px;
+ text-align: center; font-size: 0.8rem; font-weight: 600; color: #555;
+ }
+ .group-grid-empty {
+ grid-column: 1 / -1; text-align: center; color: #aaa;
+ font-size: 0.8rem; font-style: italic; padding: 4px;
+ }
+ .group-columns-container {
+ display: flex; gap: 12px; padding: 12px 16px; flex-wrap: wrap;
+ }
+
+ /* ── Column cards within a group ── */
+ .col-card {
+ min-width: 200px; box-sizing: border-box;
+ border: 1px dashed var(--uui-color-border, #ccc); border-radius: 6px;
+ background: var(--uui-color-surface-alt, #fafafa);
+ }
+ .col-header {
+ display: flex; align-items: center; gap: 8px; padding: 8px 12px;
+ background: var(--uui-color-surface-alt, #f0f0f0);
+ border-bottom: 1px solid #e0e0e0;
+ }
+ .col-num { font-weight: 600; font-size: 0.85rem; color: #555; }
+ .col-width-label {
+ display: flex; align-items: center; gap: 4px;
+ font-size: 0.8rem; font-weight: 500;
+ }
+ .col-width-label uui-input { width: 55px; }
+ .col-actions { margin-left: auto; }
+ .col-fields { padding: 8px 10px; }
+ .col-add-field {
+ text-align: center; padding: 6px 0; margin-top: 4px;
+ border-top: 1px dashed #ddd;
+ }
+
+ /* ── Move to select ── */
+ .move-to-select {
+ padding: 3px 6px; border: 1px solid #ccc; border-radius: 4px;
+ font-size: 0.78rem; background: #fff; height: 33px; box-sizing: border-box;
+ cursor: pointer; color: #555;
+ }
+ .move-to-select:hover { border-color: #888; }
+
+ /* ── Column drag & drop ── */
+ .col-drag-handle {
+ cursor: grab; color: #aaa; font-size: 0.9rem; user-select: none;
+ }
+ .col-drag-handle:active { cursor: grabbing; }
+ .col-card.col-dragging { opacity: 0.4; }
+ .col-card.col-drag-over {
+ box-shadow: inset 0 0 0 2px var(--uui-color-interactive, #1b264f);
+ border-color: var(--uui-color-interactive, #1b264f);
+ }
+
+ /* ── Compact field card ── */
+ .fc {
+ display: flex; align-items: center; gap: 6px; padding: 6px 8px;
+ border: 1px solid #e0e0e0; border-radius: 4px; margin-bottom: 4px;
+ background: #fff; cursor: grab; user-select: none;
+ transition: box-shadow 0.15s, opacity 0.15s;
+ }
+ .fc:hover { border-color: #bbb; }
+ .fc-hidden { opacity: 0.45; border-style: dashed; }
+ .fc-dragging { opacity: 0.3; }
+ .fc-drag-over { box-shadow: inset 0 0 0 2px var(--uui-color-interactive, #1b264f); }
+ .fc-type {
+ font-size: 0.7rem; font-weight: 600; color: #888; text-transform: uppercase;
+ background: #f0f0f0; padding: 1px 5px; border-radius: 3px; white-space: nowrap;
+ }
+ .fc-label { flex: 1; font-size: 0.85rem; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+ .fc-req { color: #e53935; font-weight: 700; font-size: 1rem; }
+ .fc-actions { display: flex; gap: 2px; margin-left: auto; flex-shrink: 0; }
+ .fc-btn {
+ border: none; background: none; cursor: pointer; padding: 2px 4px;
+ font-size: 0.75rem; color: #888; border-radius: 3px;
+ }
+ .fc-btn:hover { background: #f0f0f0; color: #333; }
+ .fc-btn:disabled { opacity: 0.3; cursor: default; }
+ .fc-btn-danger:hover { background: #fde8e8; color: #c0392b; }
+
+ /* ── Field settings dialog ── */
+ .field-dialog {
+ background: var(--uui-color-surface, #fff); border-radius: 8px;
+ width: 640px; max-width: 90vw; max-height: 85vh; display: flex; flex-direction: column;
+ box-shadow: 0 8px 32px rgba(0,0,0,0.3);
+ }
+ .field-dialog-header {
+ display: flex; justify-content: space-between; align-items: center;
+ padding: 14px 20px; border-bottom: 1px solid #e0e0e0;
+ }
+ .field-dialog-header h3 { margin: 0; font-size: 1rem; }
+ .field-dialog-body { padding: 16px 20px; overflow-y: auto; flex: 1; }
+ .field-dialog-footer { padding: 12px 20px; border-top: 1px solid #e0e0e0; display: flex; justify-content: flex-end; }
+ .fd-row { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
+ .fd-label { font-weight: 600; font-size: 0.85rem; min-width: 80px; }
+ .fd-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 12px; }
+ .fd-grid label { display: flex; flex-direction: column; gap: 3px; font-size: 0.8rem; font-weight: 500; }
+ .fd-toggles { display: flex; gap: 16px; margin-bottom: 12px; flex-wrap: wrap; }
+ .fd-options { margin-top: 12px; }
+ .fd-html-textarea {
+ width: 100%; min-height: 120px; padding: 8px; border: 1px solid #ccc; border-radius: 4px;
+ font-family: monospace; font-size: 0.85rem; resize: vertical; box-sizing: border-box;
+ background: #fff; margin-top: 4px;
+ }
+`;
diff --git a/uTPro/Feature/uTPro.Feature.SimpleForm/wwwroot/App_Plugins/simple-form/umbraco-package.json b/uTPro/Feature/uTPro.Feature.SimpleForm/wwwroot/App_Plugins/simple-form/umbraco-package.json
new file mode 100644
index 0000000..54c40f5
--- /dev/null
+++ b/uTPro/Feature/uTPro.Feature.SimpleForm/wwwroot/App_Plugins/simple-form/umbraco-package.json
@@ -0,0 +1,31 @@
+{
+ "name": "uTPro.SimpleForm",
+ "version": "1.0.0",
+ "extensions": [
+ {
+ "type": "dashboard",
+ "alias": "uTPro.SimpleForm.Dashboard",
+ "name": "Simple Form Builder",
+ "element": "/App_Plugins/simple-form/index.js",
+ "elementName": "utpro-simple-form-dashboard",
+ "weight": 15,
+ "meta": {
+ "label": "#simpleForm_title",
+ "pathname": "simple-form"
+ },
+ "conditions": [
+ {
+ "alias": "Umb.Condition.SectionAlias",
+ "match": "Umb.Section.Settings"
+ }
+ ]
+ },
+ {
+ "type": "localization",
+ "alias": "uTPro.SimpleForm.Localize.EnUS",
+ "name": "Simple Form English",
+ "js": "/App_Plugins/simple-form/lang/en-us.js",
+ "meta": { "culture": "en-US" }
+ }
+ ]
+}
diff --git a/uTPro/Feature/uTPro.Feature.SimpleForm/wwwroot/App_Plugins/simple-form/views/detail-view.js b/uTPro/Feature/uTPro.Feature.SimpleForm/wwwroot/App_Plugins/simple-form/views/detail-view.js
new file mode 100644
index 0000000..78c5d5d
--- /dev/null
+++ b/uTPro/Feature/uTPro.Feature.SimpleForm/wwwroot/App_Plugins/simple-form/views/detail-view.js
@@ -0,0 +1,44 @@
+import { html, nothing } from '@umbraco-cms/backoffice/external/lit';
+
+/**
+ * Renders the entry detail overlay.
+ * @param {object} host - the dashboard element
+ */
+export function renderDetail(host) {
+ const s = host._detailEntry;
+ if (!s) return nothing;
+
+ const isAdmin = host._permissions?.isAdmin;
+ const entries = Object.entries(s.data || {});
+
+ return html`
+ { if (e.target === e.currentTarget) host._closeDetail(); }}>
+
+
+
+
+ Date
+ ${new Date(s.createdUtc).toLocaleString()}
+
+
+ IP Address
+ ${s.ipAddress || 'N/A'}
+
+ ${entries.map(([k, v]) => html`
+
+ ${k}
+ ${v || ''}
+
+ `)}
+
+ ${isAdmin ? html`
+
+ ` : nothing}
+
+
`;
+}
diff --git a/uTPro/Feature/uTPro.Feature.SimpleForm/wwwroot/App_Plugins/simple-form/views/editor-view.js b/uTPro/Feature/uTPro.Feature.SimpleForm/wwwroot/App_Plugins/simple-form/views/editor-view.js
new file mode 100644
index 0000000..49f5a01
--- /dev/null
+++ b/uTPro/Feature/uTPro.Feature.SimpleForm/wwwroot/App_Plugins/simple-form/views/editor-view.js
@@ -0,0 +1,401 @@
+import { html, nothing } from '@umbraco-cms/backoffice/external/lit';
+
+// ── Main editor ──
+export function renderEditor(host) {
+ const f = host._editForm;
+ if (!f) return nothing;
+ const showSettings = host._showColumnSettings;
+ if (!f.groups) f.groups = [];
+
+ return html`
+
+
+ ${showSettings && f.id ? html`
+ ${_renderEmbedSettings(host, f)}
+ ${_renderGeneralSettings(host, f)}
+ ${_renderColumnSettings(host, f)}
+ ` : nothing}
+ ${!f.id ? _renderGeneralSettings(host, f) : nothing}
+
+ ${f.groups.length === 0 ? html`No groups yet. Add a group to organise fields.
` : nothing}
+ ${f.groups.map((group, gIdx) => _renderGroupCard(host, group, gIdx))}
+
+ ${host._typePickerIdx >= 0 ? _renderTypePicker(host) : nothing}
+ ${host._fieldSettingsLoc ? _renderFieldSettingsDialog(host) : nothing}`;
+}
+
+// ── Group card ──
+function _renderGroupCard(host, group, gIdx) {
+ const f = host._editForm;
+ if (!group.columns) group.columns = [];
+ const totalWidth = group.columns.reduce((sum, c) => sum + (c.width || 1), 0);
+ return html`
+
+
+
+
+
+
+ ${group.columns.map((col, cIdx) => _renderColumnCard(host, col, gIdx, cIdx, group.columns.length))}
+
+
`;
+}
+
+// ── Column card (draggable) ──
+function _renderColumnCard(host, col, gIdx, cIdx, totalCols) {
+ const widthPct = ((col.width || 12) / 12 * 100).toFixed(2);
+ return html`
+ { e.dataTransfer.setData('application/col-drag', JSON.stringify({ gIdx, cIdx })); e.dataTransfer.effectAllowed = 'move'; e.currentTarget.classList.add('col-dragging'); }}
+ @dragend=${(e) => { e.currentTarget.classList.remove('col-dragging'); }}
+ @dragover=${(e) => { if (e.dataTransfer.types.includes('application/col-drag')) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; e.currentTarget.classList.add('col-drag-over'); } }}
+ @dragleave=${(e) => { e.currentTarget.classList.remove('col-drag-over'); }}
+ @drop=${(e) => {
+ e.preventDefault(); e.currentTarget.classList.remove('col-drag-over');
+ try {
+ const from = JSON.parse(e.dataTransfer.getData('application/col-drag'));
+ if (from.gIdx === gIdx && from.cIdx !== cIdx) {
+ const cols = host._editForm.groups[gIdx].columns; const [moved] = cols.splice(from.cIdx, 1); cols.splice(cIdx, 0, moved); host.requestUpdate();
+ } else if (from.gIdx !== gIdx) { host._moveColumnTo(from.gIdx, from.cIdx, gIdx); }
+ } catch { }
+ }}>
+
+
+
+
+
+
+ ${col.fields.map((field, fIdx) => _renderFieldCompact(host, field, fIdx, { gIdx, cIdx }))}
+
+ host._addFieldToColumn(gIdx, cIdx)}>+ Add Field
+
+
+
`;
+}
+
+// ── Compact field card (summary only) ──
+function _renderFieldCompact(host, field, fIdx, loc) {
+ const typeLabel = host._fieldTypes.find(ft => ft.type === field.type)?.label || field.type;
+ const label = field.label || field.name || '(no label)';
+ const totalFields = host._editForm.groups[loc.gIdx].columns[loc.cIdx].fields.length;
+
+ return html`
+ { host._fieldSettingsLoc = { ...loc, fIdx }; host.requestUpdate(); }}
+ @dragstart=${(e) => { e.dataTransfer.setData('application/field-drag', JSON.stringify({ ...loc, fIdx })); e.dataTransfer.effectAllowed = 'move'; e.currentTarget.classList.add('fc-dragging'); }}
+ @dragend=${(e) => { e.currentTarget.classList.remove('fc-dragging'); }}
+ @dragover=${(e) => { if (e.dataTransfer.types.includes('application/field-drag')) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; e.currentTarget.classList.add('fc-drag-over'); } }}
+ @dragleave=${(e) => { e.currentTarget.classList.remove('fc-drag-over'); }}
+ @drop=${(e) => {
+ e.preventDefault(); e.currentTarget.classList.remove('fc-drag-over');
+ try {
+ const from = JSON.parse(e.dataTransfer.getData('application/field-drag'));
+ if (from.gIdx === loc.gIdx && from.cIdx === loc.cIdx && from.fIdx !== fIdx) {
+ const arr = host._editForm.groups[loc.gIdx].columns[loc.cIdx].fields;
+ const [moved] = arr.splice(from.fIdx, 1); arr.splice(fIdx, 0, moved);
+ arr.forEach((f, i) => f.sortOrder = i); host.requestUpdate();
+ } else if (from.gIdx !== loc.gIdx || from.cIdx !== loc.cIdx) {
+ const srcArr = host._editForm.groups[from.gIdx].columns[from.cIdx].fields;
+ const [moved] = srcArr.splice(from.fIdx, 1);
+ const destArr = host._editForm.groups[loc.gIdx].columns[loc.cIdx].fields;
+ moved.sortOrder = fIdx; destArr.splice(fIdx, 0, moved);
+ destArr.forEach((f, i) => f.sortOrder = i); host.requestUpdate();
+ }
+ } catch { }
+ }}>
+
${label} ${field.required ? html`*` : nothing}
+
+
+
+
+
+
+
+
`;
+}
+
+// ── Field settings dialog ──
+function _renderFieldSettingsDialog(host) {
+ const loc = host._fieldSettingsLoc;
+ if (!loc) return nothing;
+ const field = host._editForm.groups?.[loc.gIdx]?.columns?.[loc.cIdx]?.fields?.[loc.fIdx];
+ if (!field) return nothing;
+
+ const needsOptions = ['select', 'radio', 'checkbox'].includes(field.type);
+ const currentTypeLabel = host._fieldTypes.find(ft => ft.type === field.type)?.label || field.type;
+ const updateFn = (key, val) => { host._updateFieldInColumn(loc.gIdx, loc.cIdx, loc.fIdx, key, val); };
+ const close = () => { host._fieldSettingsLoc = null; host.requestUpdate(); };
+
+ return html`
+ { if (e.target === e.currentTarget) close(); }}>
+
+
+
+
+
+
+
+
+ ${_renderMoveToInDialog(host, loc)}
+
+
+ ${field.type !== 'div' && field.type !== 'step' ? html`
+
+
+
+
+
+
+
+
+
+
+ ` : html`
+
+
+
+
+
+ `}
+
+
+ ${_renderTypeAttributes(host, field, loc.fIdx, loc)}
+
+
+ ${needsOptions ? html`
+
+
+ ${(field.options || []).map((opt, oIdx) => html`
+
+ { opt.text = e.target.value; host.requestUpdate(); }}>
+ { opt.value = e.target.value; host.requestUpdate(); }}>
+ host._removeOptionInColumn(loc.gIdx, loc.cIdx, loc.fIdx, oIdx)}>✕
+
`)}
+
+ ` : nothing}
+
+
+
+
+
`;
+}
+
+function _renderMoveToInDialog(host, loc) {
+ const f = host._editForm;
+ const destinations = [];
+ (f.groups || []).forEach((g, gi) => {
+ (g.columns || []).forEach((c, ci) => {
+ if (gi === loc.gIdx && ci === loc.cIdx) return;
+ const gName = g.name || `Group #${gi + 1}`;
+ const colLabel = g.columns.length > 1 ? ` / Col ${ci + 1}` : '';
+ destinations.push({ label: `${gName}${colLabel}`, gIdx: gi, cIdx: ci });
+ });
+ });
+ if (destinations.length === 0) return nothing;
+ return html`
+ `;
+}
+
+// ── Shared helpers ──
+
+function _renderEmbedSettings(host, f) {
+ return html`
+
+
+
POST /api/utpro/simple-form/submit { "alias": "${f.alias}", "data": { ... } }
+
+ { f.enableRenderApi = e.target.checked; host.requestUpdate(); }} label=${f.enableRenderApi ? 'Enabled' : 'Disabled'}>
+ GET /api/utpro/simple-form/render/${f.alias}
+
+
+ { f.enableEntriesApi = e.target.checked; host.requestUpdate(); }} label=${f.enableEntriesApi ? 'Enabled' : 'Disabled'}>
+ GET /api/utpro/simple-form/entries/${f.alias}
+
+
`;
+}
+
+function _renderGeneralSettings(host, f) {
+ return html`
+
+
+
+
+
+
+
+
+
+
+
`;
+}
+
+function _renderColumnSettings(host, f) {
+ const allFieldNames = (f.groups || []).flatMap(g => (g.columns || []).flatMap(c => c.fields.map(field => field.name).filter(n => n)));
+ let orderedNames;
+ if (f.visibleColumns && f.visibleColumns.length > 0) {
+ const checked = f.visibleColumns.filter(n => allFieldNames.includes(n));
+ const unchecked = allFieldNames.filter(n => !f.visibleColumns.includes(n));
+ orderedNames = [...checked, ...unchecked];
+ } else { orderedNames = [...allFieldNames]; }
+ return html`
+
+
+
+ ${allFieldNames.length === 0 ? html`
No fields yet.
` : nothing}
+ ${orderedNames.map((name, idx) => {
+ const isVisible = f.visibleColumns == null ? true : f.visibleColumns.includes(name);
+ return html`
`;
+ })}
+
+
`;
+}
+
+function _renderTypePicker(host) {
+ const search = (host._typePickerSearch || '').toLowerCase();
+ const filtered = host._fieldTypes.filter(ft => ft.label.toLowerCase().includes(search) || ft.type.toLowerCase().includes(search));
+ const idx = host._typePickerIdx, gIdx = host._typePickerGroupIdx, cIdx = host._typePickerColIdx ?? -1;
+ let currentType;
+ if (gIdx >= 0 && cIdx >= 0) currentType = host._editForm?.groups?.[gIdx]?.columns?.[cIdx]?.fields?.[idx]?.type;
+ return html`
+ { if (e.target === e.currentTarget) { host._typePickerIdx = -1; host.requestUpdate(); } }}>
+
+
+
{ host._typePickerSearch = e.target.value; host.requestUpdate(); }}>
+
+ ${filtered.map(ft => html`
`)}
+ ${filtered.length === 0 ? html`
No matching types
` : nothing}
+
+
+
`;
+}
+
+function _renderTypeAttributes(host, field, idx, loc) {
+ const t = field.type; if (!field.attributes) field.attributes = {};
+ const a = field.attributes, s = (k, v) => { field.attributes[k] = v; host.requestUpdate(); };
+ if (t === 'number') return html``;
+ if (t === 'date') return html``;
+ if (t === 'time') return html``;
+ if (t === 'textarea') return html``;
+ if (t === 'file') return html``;
+ if (t === 'range') return html``;
+ if (t === 'accept') return html``;
+ if (t === 'step') return html``;
+ return nothing;
+}
+
+function _renderColMoveToSelect(host, gIdx, cIdx) {
+ const f = host._editForm;
+ if (!f.groups || f.groups.length < 2) return nothing;
+ const dests = f.groups.map((g, gi) => ({ label: g.name || `Group #${gi + 1}`, gi })).filter(d => d.gi !== gIdx);
+ if (!dests.length) return nothing;
+ return html``;
+}
diff --git a/uTPro/Feature/uTPro.Feature.SimpleForm/wwwroot/App_Plugins/simple-form/views/entries-view.js b/uTPro/Feature/uTPro.Feature.SimpleForm/wwwroot/App_Plugins/simple-form/views/entries-view.js
new file mode 100644
index 0000000..a298c69
--- /dev/null
+++ b/uTPro/Feature/uTPro.Feature.SimpleForm/wwwroot/App_Plugins/simple-form/views/entries-view.js
@@ -0,0 +1,100 @@
+import { html, nothing } from '@umbraco-cms/backoffice/external/lit';
+
+/**
+ * Renders the entries list view with search, date filter, and checkboxes.
+ * @param {object} host - the dashboard element
+ */
+export function renderEntries(host) {
+ const isAdmin = host._permissions?.isAdmin;
+ const form = host._forms.find(f => f.id === host._viewFormId);
+ const formName = form?.name || 'Form';
+ const pages = Math.max(1, Math.ceil(host._entryTotal / 20));
+ const page = Math.floor(host._entrySkip / 20) + 1;
+ const allDataKeys = [...new Set(host._entries.flatMap(s => Object.keys(s.data || {})))];
+ const visibleCols = form?.visibleColumns;
+ const allKeys = visibleCols && visibleCols.length > 0
+ ? allDataKeys.filter(k => visibleCols.includes(k))
+ : allDataKeys;
+ const allSelected = host._entries.length > 0 && host._selectedEntries.length === host._entries.length;
+
+ return html`
+
+
+
+
+
+
+
+
+ ${!host._entries.length ? html`No entries yet
` : html`
+
+
+
+ host._toggleSelectAll()} />
+
+ Date
+ IP
+ ${allKeys.map(k => html`${k}`)}
+ Actions
+
+ ${host._entries.map(s => html`
+
+
+ host._toggleEntrySelect(s.id)} />
+
+ ${new Date(s.createdUtc).toLocaleString()}
+ ${s.ipAddress || ''}
+ ${allKeys.map(k => html`${s.data?.[k] || ''}`)}
+
+ host._viewDetail(s)} title="View">☰
+ ${isAdmin ? html`
+ host._deleteEntry(s.id)} title="Delete">✕
+ ` : nothing}
+
+ `)}
+
+ ${host._entryTotal > 20 ? html`
+ ` : nothing}
+ `}
+ `;
+}
diff --git a/uTPro/Feature/uTPro.Feature.SimpleForm/wwwroot/App_Plugins/simple-form/views/list-view.js b/uTPro/Feature/uTPro.Feature.SimpleForm/wwwroot/App_Plugins/simple-form/views/list-view.js
new file mode 100644
index 0000000..81da4cf
--- /dev/null
+++ b/uTPro/Feature/uTPro.Feature.SimpleForm/wwwroot/App_Plugins/simple-form/views/list-view.js
@@ -0,0 +1,53 @@
+import { html, nothing } from '@umbraco-cms/backoffice/external/lit';
+
+/**
+ * Renders the form list view.
+ * - Admin: can create, edit, delete forms
+ * - Non-admin: can only view list and access entries
+ * @param {object} host - the dashboard element
+ */
+export function renderList(host) {
+ const isAdmin = host._permissions?.isAdmin;
+
+ return html`
+
+
+
Form Builder
+ ${isAdmin ? html`
+ host._newForm()}>+ New Form
+ ` : nothing}
+
+ ${host._loading ? html`
` : nothing}
+ ${!host._forms.length && !host._loading ? html`No forms yet.${isAdmin ? ' Create one!' : ''}
` : nothing}
+ ${host._forms.length ? html`
+
+
+ Name
+ Alias
+ Fields
+ Status
+ Actions
+
+ ${host._forms.map(f => html`
+
+
+ ${isAdmin
+ ? html` host._editExisting(f.id)}>${f.name}`
+ : html`${f.name}`}
+
+ ${f.alias}
+ ${f.fields?.length || 0}
+ ${f.isEnabled ? html`Active` : html`Disabled`}
+
+ ${isAdmin ? html`
+ host._editExisting(f.id)}>Edit
+ ` : nothing}
+ host._viewEntries(f.id)}>Entries
+ ${isAdmin ? html`
+ host._deleteForm(f.id)}>Delete
+ ` : nothing}
+
+ `)}
+ ` : nothing}
+ `;
+}
diff --git a/uTPro/Feature/uTPro.Feature.SimpleForm/wwwroot/uTPro/simple-form/css/simple-form.css b/uTPro/Feature/uTPro.Feature.SimpleForm/wwwroot/uTPro/simple-form/css/simple-form.css
new file mode 100644
index 0000000..886945e
--- /dev/null
+++ b/uTPro/Feature/uTPro.Feature.SimpleForm/wwwroot/uTPro/simple-form/css/simple-form.css
@@ -0,0 +1,31 @@
+/* ── SimpleForm Frontend Styles ── */
+.sf { max-width: 100%; }
+.sf .sf-error { color: #e53935; font-size: 0.8rem; display: none; }
+.sf .sf-error:not(:empty) { display: block; }
+.sf .sf-required { color: #e53935; }
+.sf-actions { display: flex; gap: 12px; margin-top: 20px; }
+.sf-btn {
+ padding: 14px 40px; font-size: 14px; font-weight: 700; letter-spacing: 2px;
+ text-transform: uppercase; border: 2px solid #555; border-radius: 4px; cursor: pointer;
+ transition: all 0.2s;
+}
+.sf-message { margin-top: 16px; padding: 12px; border-radius: 4px; }
+.sf-success { background: #e8fde8; color: #27ae60; }
+.sf-fail { background: #fde8e8; color: #c0392b; }
+
+/* ── Groups / Fieldsets ── */
+.sf-group { border: none; padding: 0; margin: 0 0 1.5em 0; }
+.sf-group-title { font-weight: 700; font-size: 1.1rem; margin-bottom: 0.75em; padding: 0; }
+.sf-group-grid { width: 100%; }
+.sf-group-field { min-width: 0; }
+
+/* ── Content Block ── */
+.sf-content-block { margin: 8px 0; }
+
+/* ── Step divider ── */
+.sf-step { margin: 16px 0 8px; }
+.sf-step-title { font-size: 1.1rem; margin: 0 0 8px; }
+.sf-step-divider { border: none; border-top: 1px solid #ddd; }
+
+/* ── Range ── */
+.sf-range-value { font-weight: 600; margin-left: 8px; }
diff --git a/uTPro/Feature/uTPro.Feature.SimpleForm/wwwroot/uTPro/simple-form/js/simple-form.js b/uTPro/Feature/uTPro.Feature.SimpleForm/wwwroot/uTPro/simple-form/js/simple-form.js
new file mode 100644
index 0000000..70dbd76
--- /dev/null
+++ b/uTPro/Feature/uTPro.Feature.SimpleForm/wwwroot/uTPro/simple-form/js/simple-form.js
@@ -0,0 +1,93 @@
+/**
+ * SimpleForm — client-side validation & submission handler.
+ * Reads config from data attributes on the
+
+@if (Context.Items["sf-assets-loaded"] is not true)
+{
+ Context.Items["sf-assets-loaded"] = true;
+ uTPro.Feature.SimpleForm.Helpers.SimpleFormAssets.Resolve(
+ Context.RequestServices.GetRequiredService());
+
+
+}
diff --git a/uTPro/Project/uTPro.Project.Web/Views/Partials/SimpleForm/Fields/_Default.cshtml b/uTPro/Project/uTPro.Project.Web/Views/Partials/SimpleForm/Fields/_Default.cshtml
new file mode 100644
index 0000000..43486fe
--- /dev/null
+++ b/uTPro/Project/uTPro.Project.Web/Views/Partials/SimpleForm/Fields/_Default.cshtml
@@ -0,0 +1,47 @@
+@* ═══════════════════════════════════════════════════════════════
+ DEFAULT FIELD PARTIAL — handles: text, email, tel, number, date, url, password
+
+ To add a CUSTOM field type, create a new file in this folder:
+ Fields/{YourType}.cshtml (e.g. Fields/turnstile.cshtml)
+ It receives:
+ @model FormFieldViewModel
+ ViewData["FormId"] — the form element id
+ ViewData["CultureDictionary"] — ICultureDictionary for multi-language lookups
+
+ Use FieldHelper for consistent rendering:
+ @{ var h = new FieldHelper(Model, ViewData); }
+ @h.Label() —