Skip to content

Releases: digitonehr/digitone-events

v0.9.5

Choose a tag to compare

@github-actions github-actions released this 14 May 14:04

Added — Manual mapping fallback for Excel import

When the uploaded workbook is what we shipped as the template, auto-detection picks up every sheet and every column on its own and nothing changes about the existing flow. But when an external partner sends an Excel with sheet names like "Speakers Q1" or columns like "Surname" / "Email" that don't match the schema, the 0.9.0 behaviour was to skip them with a warning and never show that data. This release adds an inline mapping UI that surfaces only when something didn't auto-match.

Flow

  1. Click "Preview import" on an .xlsx whose sheet names or column headers don't all map cleanly.
  2. Instead of jumping to preview, a yellow-bordered mapping panel slides in below the upload row with one card per sheet in the workbook:
    • Sheet header line: name in your file, row count, "Maps to: ▾" dropdown of every entity (or "skip this sheet"). Green left border if mapped, amber if not.
    • Configure columns expander opens a two-column table: your sheet's header text in one column, a dropdown of valid fields in the other. Auto-detected columns are pre-selected; unmapped columns wear an amber "unmapped" pill.
  3. "Apply mapping & preview" locks the choices in (mappingExplicitlyOk flag) and re-runs the preview through the same applyMapping()excel_preview path.
  4. The mapping carries forward to commit — the same overrides go to excel_commit so what you preview is exactly what you get.

Workbooks that auto-detect cleanly skip this UI entirely

If every sheet matched an entity and every header matched a field, runPreview proceeds straight to the preview table just like before. The mapping UI is only revealed when there's something to fix, so the happy path (you used the template) stays a single click.

Smart defaults

  • Changing the entity dropdown re-renders the column table for that entity (different entities have different columns).
  • If you change the entity to one that wasn't the auto-detected one, columns auto-re-resolve against the new entity's column list (so "name" still finds the name field on either Roles or Titles, for instance).
  • File picker change wipes the cached mapping — switching to a different .xlsx always restarts with fresh auto-detection.
  • Switching between Incremental and Full mode while a mapping is set keeps the mapping (it doesn't depend on mode) and re-runs preview with new mode.

Internal

  • autoMap() was split into extractStructure( wb ) (reads sheet names + headers + rows once) and applyMapping( structure, schema, overrides ) (turns structure into the backend-shaped payload given an optional override map). Splitting let us cache the parsed workbook between mapping re-renders — re-applying with different overrides doesn't re-parse the .xlsx.
  • New mappingWarnings() builds the same "Sheet X — columns ignored: …" message the old autoMap returned, so the preview-summary warning row keeps its informational role for sheets the user chose to skip.
  • The mapping container [data-de-mapping-ui] is built imperatively inside showMappingUI() so the UI matches the workbook structure (no hard-coded list of sheets). hideMappingUI() clears it; both resetUi() and the file-input change listener call it.

v0.9.4

Choose a tag to compare

@github-actions github-actions released this 14 May 13:56

Added — Excel Full overwrite mode + JSON backup integration

The 0.9.0 release shipped Excel import in incremental mode only — rows that already existed (matched by natural key) got skipped. This release fills in the other half of the promise: a Full overwrite mode that wipes every existing row of the entity types in the workbook before inserting from it. Useful when an event's data has drifted from the master spreadsheet and the cleanest fix is "throw it all away, re-import from scratch."

UI flow

A new mode selector sits inside the upload column with two radio cards:

  • Incremental (default) — what 0.9.x has been doing. Safe to re-run any number of times; only inserts what's new.
  • Full overwrite — danger styling, destructive. Insertion happens after a clean wipe.

The Preview table gains a "Will delete" column when full mode is selected. Counts come from the same run( $payload, $event_id, $mode, dry_run=true ) call the incremental mode uses — single source of truth.

The big change is the danger dialog in front of the actual commit. When the user is in full mode and clicks "Replace event data…":

  1. A red-bordered <dialog> opens listing exactly what will be deleted ("57 sessions will be deleted", "159 speakers will be deleted", …) pulled from the preview totals.
  2. A prominent "Download JSON backup" button (using the existing admin-post.php?action=digitone_events_export&format=json&event_id=… nonce-protected URL) opens the JSON export in a new tab. JSON-format event snapshots are round-trippable via the existing Import-as-new-event panel, so the user has a real safety net.
  3. An "I have a backup and understand this cannot be undone." checkbox below; the "Replace everything" button stays disabled until it's ticked.
  4. Only when both conditions are met does the commit fire.

Switching from "Full overwrite" back to "Incremental" with a preview already on screen re-runs the preview automatically so the displayed numbers stay honest.

Internal

  • DigitOne_Events_Export_Import_Excel::run() was extended with two helpers:
    • count_existing( $entity_key, $event_id ) — used in dry-run + full mode so the preview shows accurate "will delete" totals before any DB write happens.
    • wipe_entity( $entity_key, $event_id ) — used in commit + full mode. Iterates and calls each repo's delete() (which handles its own junction-table cleanup, so we don't need raw SQL). Called in reverse of IMPORT_ORDER: sessions first (because everything references them), titles last (because nothing references titles).
  • prime_caches() now takes a $mode argument. In full mode it returns empty buckets (no by_nk pre-population, no next_order / color_idx from existing rows) so the import behaves as if the slate were already wiped. That means the same code path produces correct results for both dry-run-on-full-mode (cache empty, no dedup happens, FK lookups grow as imports happen) and commit-on-full-mode (DB really is empty, same behaviour).
  • import_entity() already skipped dedup in non-incremental modes; no changes there.
  • AJAX excel_preview / excel_commit lost the "Full mode is not yet enabled" guard from 0.9.0 and now pass mode through directly.

Note

The danger dialog uses the native <dialog> element. Modern browsers (Chrome/Firefox/Safari/Edge 2022+) support it natively; the fallback setAttribute('open', '') keeps it functional for older builds but without the proper modal backdrop. WP admin is already a modern-browser-only environment so this is a non-issue in practice.

v0.9.3

Choose a tag to compare

@github-actions github-actions released this 14 May 13:10

Added — Now / Next widget on the agenda

A live ticker that appears at the top of [digitone_events_agenda] when the device's local date is within the event window:

  • Now (red, pulsing) — the session currently in progress, with "X min in" remaining.
  • Up next (blue) — the earliest upcoming session for today, with "in X min".
  • Pre-start — before the conference starts, shows "Conference starts on Monday, 18 May."
  • Post-event — widget hides itself after the last event day passes.

The widget ticks every 30 s. Today's day-nav button gets a primary-coloured ring (.is-today). The currently-running session on the grid + mobile list gets a "NOW" badge with a red outline so attendees scanning the schedule visually see where they should be.

Time source is browser local time. On-site attendees with phones set to the venue timezone get correct results; remote viewers in other timezones will see drift relative to the venue, but the schedule itself is unaffected.

Added — Per-session deep links

Every session gets a stable URL via the hash: …/programme/#session-<uuid>.

  • Opening that URL anywhere — pasted into a browser tab, sent via WhatsApp, embedded in an email — auto-opens the session modal on page load via hashchange-aware boot.
  • When a user opens a session by clicking on the grid, the URL hash updates to match, so the address bar always reflects what's currently visible. They can copy it manually too.
  • A new Copy link button in the modal action row puts that URL on the clipboard (with the standard browser fallback for non-navigator.clipboard environments). "Copy link" → flashes "Copied!" → reverts to "Copy link" after 1.5 s.

Added — QR code per session

In the same action row, a QR code button toggles a QR canvas inside the modal. The QR encodes the deep-link URL, so anyone who scans it lands on the session page with the modal already open.

qrcode-generator is lazy-loaded from cdnjs on first click (~10 KB, then cached) and the QR is rendered once per session and kept around (data-rendered flag) so repeated toggles are instant.

Internal

  • agenda.php emits day_date per session in de-fe-session-data so the Now/Next widget can filter to today without a separate fetch.
  • setupSessionModal() now exposes open() on the schedule element (schedule.deOpenSession) so the Now/Next widget can trigger the modal directly without going through hash + hashchange round-trips.
  • wireModalExtras() wires Copy link + QR toggle on each modal open. QR canvas uses qr.createImgTag(5, 12) for a 5×-cellsize image with a 12 px quiet zone — good scan reliability without dominating the modal.
  • Block highlighting uses .is-now on .de-fe-block and .de-fe-mobile-item. The "NOW" badge is a ::before pseudo-element so it doesn't disturb the existing block layout.

v0.9.2

Choose a tag to compare

@github-actions github-actions released this 14 May 12:54

Fixed — Excel preview falsely flagged every speaker and session as "will insert"

Three separate bugs were stacking so that the incremental-mode dedup never matched existing speakers (159 false positives + 4 errors) or existing sessions (57 false positives):

  1. Speakers natural-key format mismatch. prime_caches() was building keys as "ana veršić" (space-separated full name), but the dedup check via natural_key() was producing "ana|veršić" (pipe-separated, per the natural_key schema). They could never match.
  2. Sessions cache was never primed. There was no loop populating caches['sessions']['by_nk'] from existing sessions — so every imported session looked brand new.
  3. Time-format mismatch. Existing sessions hold start_time as HH:MM:SS (MySQL TIME), the Excel template uses HH:MM. Even with the cache primed, the keys wouldn't have matched.

Replaced the ad-hoc key building with a single source of truth, natural_key_for( $entity_key, $row ), used both when priming caches from the DB and when checking imported rows. The function handles per-field normalisation (currently: trimming :SS off session times) so the two sides always speak the same language. Speakers also keep a separate by_name cache (space-separated, both orderings) that the sessions importer uses for FK lookup of speakers by full name — that was the original purpose of the space-keyed cache and it's still needed.

Added — Auto-fill for blank sort_order and color columns

If the Excel cell is blank or 0, the importer now fills these in instead of inserting a row with defaults that look broken:

  • sort_order: assigned the next free integer (max existing + 1, then +2, +3…). Insertion order in Excel becomes display order, which is what you'd expect.
  • color (Roles and Session-Types only): cycles through an 8-colour palette, picking up where the existing data left off so new entries land in unused palette slots first.

If you do fill the cell in Excel, your value wins — auto-fill only kicks in for blank cells. The icon column for Session-Types is left blank if not provided; pick one later from the Session Types admin tab.

v0.9.1

Choose a tag to compare

@github-actions github-actions released this 14 May 12:36

Fixed — Excel snapshot / preview / commit threw PHP fatal

DigitOne_Events_Plugin::instance()->module( 'session-types' ) returned null because the module is registered under the underscore slug session_types (in class-plugin.php), not the hyphenated directory name. ->repo() on null hit a fatal in class-export-import-excel.php; the fatal printed as HTML, the AJAX response stopped being JSON, and the front-end threw JSON.parse: unexpected character at line 1 column 1. Fixed three call sites in that file. Other module slugs in the codebase are all single-word so this only bit session_types.

v0.9.0

Choose a tag to compare

@github-actions github-actions released this 14 May 12:31

Added — Excel bulk import / export on the Export/Import page

The Sessions-page CSV importer from 0.8.9 (which expected semicolon-separated speakers in a single column) was hard to use for non-developers. Replaced with a workbook-based flow on the Export / Import admin page:

Workflow

  1. Get a workbook: "Download empty template" (an .xlsx with one sheet per entity, only the header row populated) or "Download current data" (same shape but pre-filled with the active event's existing rows). Both are generated client-side via SheetJS, lazy-loaded from cdnjs the first time a button is clicked.
  2. Edit in Excel / Google Sheets / LibreOffice: each sheet has clearly named columns. Speakers list multiple roles via ; separation; sessions list multiple speakers via ; separation. Sheet names and column headers tell you exactly what each field is, no documentation needed.
  3. Upload .xlsx: SheetJS parses the workbook in the browser, auto-maps sheet names to entities and column headers to fields (case-insensitive, normalized — "Session Types", "session_types", "SESSION-TYPES" all match), and POSTs the resulting JSON to the preview endpoint.
  4. Preview: per-entity table of will insert / will skip / errors, plus a detail table of every skipped row with the reason (missing field, unknown venue / type / role, bad date format…). Sheets the auto-mapper didn't recognize and columns it didn't map are listed as warnings.
  5. Confirm import: same JSON re-POSTs to the commit endpoint, which runs the exact same code path (run() with dry_run=false) so what you preview is what you get.

Entities supported, in dependency order

Titles → Roles → Session-Types → Venues → Sub-Venues → Days → Speakers → Sessions. Within Speakers, the title and roles columns reference the previously-imported Titles and Roles by name; within Sessions, day_date, venue, sub_venue, session_type, speakers, and default_role resolve against everything earlier.

Mode (this release)

Incremental only: rows whose natural key already exists (name for taxonomies, first_name + last_name for speakers, day_date + start_time + title for sessions, name + parent_venue for sub-venues) are skipped; only new rows are inserted. Full / overwrite mode lands in 0.9.1, together with a JSON backup-download integration before the destructive step.

Auto-mapping fallback

This release only does auto-detection. If a sheet's columns don't match the schema, that sheet's rows are skipped and a warning shown; user can fix headers in Excel and re-upload. Manual mapping UI (drag-and-drop column-to-field for the edge case) is scheduled for 0.9.2.

Internal

  • New DigitOne_Events_Export_Import_Excel class in modules/export-import/class-export-import-excel.php. Defines the schema once (ENTITIES const), provides snapshot() for export-side data shaping and run() for the import side. run() is the single source of truth for both preview and commit — preview just passes dry_run=true, which still walks every row, validates FKs, populates the natural-key cache so later entities (Sessions referencing Speakers, Sub-Venues referencing Venues) can resolve correctly during the preview pass.
  • Three new AJAX endpoints in class-export-import-ajax.php:
    • digitone_events_excel_snapshot — returns { data, schema } for client-side workbook building (mode=full for export, mode=empty for template).
    • digitone_events_excel_preview — dry-run with full validation, returns the per-entity report.
    • digitone_events_excel_commit — actually inserts. Same payload shape as preview.
  • JS in assets/js/modules/export-import.js — auto-enqueued by hook detection (the existing class-assets.php maps the page hook digitone-events_page_digitone-events-export-import to module slug export-import).
  • CSS in assets/css/modules/export-import.css (panel layout + result tables).

Removed

  • Sessions-page Import CSV… button + CSV modal + handler + AJAX endpoint + class-sessions-csv-importer.php. The Excel workflow on Export / Import supersedes it entirely (and supports more entities, not just sessions).

v0.8.10

Choose a tag to compare

@github-actions github-actions released this 14 May 12:06

Fixed — "Import CSV…" button did nothing

The CSV block in sessions.js was placed at the IIFE's top level instead of inside initSessionsTab. That meant it ran at script-parse time (before DOMContentLoaded), the [data-de-csv-modal] query returned null because the dialog wasn't in the DOM yet, and the page reference threw under strict mode and aborted the block silently. Net effect: the click handler was never bound, the button looked alive but did nothing.

Moved the entire CSV setup inside initSessionsTab so it's called from the DOMContentLoaded pass with page already resolved and the dialog already in the DOM.

v0.8.9

Choose a tag to compare

@github-actions github-actions released this 14 May 12:00

Added — Bulk CSV import for sessions

Sessions admin page → header → new "Import CSV…" button next to "+ Add session". Opens a modal that takes a UTF-8 CSV with a header row and bulk-creates one session per data row in the active event.

Columns

day, start_time, end_time, title, session_type, venue, sub_venue, speakers, default_role, description

  • day is YYYY-MM-DD (also accepts dd.mm.yyyy and dd/mm/yyyy); must match an existing day for the active event.
  • start_time / end_time are HH:MM (24-hour).
  • title is required; other columns may be blank.
  • session_type, venue, sub_venue, default_role are name-based lookups against the event's own data. Case-insensitive on a UTF-8 fold (so "lecture" matches "Lecture", "Hala A" matches "hala a", etc.). Anything that doesn't resolve marks the row as skipped with a reason.
  • speakers is semicolon-separated First Last (or Last First — both orderings match). Unknown names abort the row with the unresolved names listed.
  • sub_venue is optional but if given must exist under the named venue.
  • All imported sessions are master-level. Child/parent relationships are not handled here — set those up by editing the individual session after import.

Flow

  1. Pick a .csv file (max 5 MB).
  2. Click Import. The whole file is parsed server-side, every row is resolved against name-to-id caches built once per import, and valid rows are inserted via the standard sessions_repo->save() (so uuid generation, speaker junction-table sync, time validation all run unchanged).
  3. The modal switches to a result panel: N imported, M skipped (of T rows) with a table of skipped-row reasons. If anything was imported, closing the modal triggers a list reload so the new sessions appear.

The modal also has a "↓ Download sample CSV" button that produces a client-side blob with three example rows, including a multi-speaker example. No server round-trip for the template.

Internal

  • New class DigitOne_Events_Sessions_Csv_Importer in modules/sessions/class-sessions-csv-importer.php — autoloaded by name. Parses CSV via PHP's fgetcsv over php://memory so quoted fields with embedded commas work. Detects ; vs , delimiter from the header line. Strips UTF-8 BOM if present.
  • New AJAX endpoint digitone_events_session_csv_import in modules/sessions/class-sessions-ajax.php. Caps-check + nonce, 5 MB upload limit, .csv extension only. Returns { imported, skipped, errors:[{row,reason}], created:[ids] }.
  • Modal HTML appended to modules/sessions/views/tab-sessions.php; JS lives at the end of initSessionsTab() in assets/js/modules/sessions.js; CSS in assets/css/modules/sessions.css.

v0.8.8

Choose a tag to compare

@github-actions github-actions released this 14 May 11:48

Changed — Speaker picker in session modal is now an autocomplete

The old picker was a flat checkbox grid of every speaker on the event. Workable with 10 speakers, painful with 50, unusable with 163 (the IAAS 2026 roster). Picking "Karlović Zoran" meant scrolling alphabetically through a grid that didn't fit the modal. Replaced with:

  • Type-to-find input with a dropdown of matching speakers (top 10). Matches both first last and last first orderings, plus title, all UTF-8 lowercased on the server so the loop is a substring check.
  • Selected speakers render as removable chips above the input. Each chip carries a hidden <input name="speaker_ids[]"> so the existing form-submit harvest in sessions.js just keeps working — the rest of the create/edit flow didn't have to change.
  • Keyboard nav: ↓/↑ moves through the dropdown, Enter selects the highlighted match, Esc closes the dropdown, Backspace on an empty input removes the last chip.
  • Edit mode pre-fills existing speakers as chips. New session starts with no chips.

Internal

  • The server still queries all speakers for the event (cheap, one query, already needed for the legacy grid). Instead of rendering them as 163 <label> elements it emits one <script type="application/json"> with { id, name, search } per speaker. The autocomplete reads that blob into memory once.
  • The default_role_id dropdown beside the picker is untouched — same behaviour: pick a role, it applies to every selected speaker on save (per-speaker overrides remain in the original session-roles flow).

Note — Active Event dropdown (B-bundle item #1)

That feature has been shipping since v0.2.0. The partial at admin/views/partials/active-event-bar.php is rendered by Speakers, Days, Venues, Sessions, and Export-Import admin pages; when more than one event exists, the bar shows a <select> and admin-common.js calls digitone_events_event_set_active on change, then reloads. If you're only seeing the static "Active event: …" line, it's because there's currently only one event in the database — create a second one and the dropdown appears.

v0.8.7

Choose a tag to compare

@github-actions github-actions released this 13 May 13:21

Added — Speakers shortcode now has search + role filter (A-bundle finale)

The [digitone_events_speakers] shortcode used to render a plain grid of cards in alphabetical order. With ~160 speakers in a real event that's a long scroll. This release adds two filter dimensions plus a live result counter:

  • Search box — type a name (or part of one) and matching cards stay visible while the rest hide. Input is debounced 200 ms; matches first name, last name, both orderings, title, and role names. UTF-8 lowercasing happens server-side (so mb_strtolower runs once per page render, not per keystroke), baked into a data-search-text attribute on each card. Croatian diacritics (Veršić, Sršen, Lešić, etc.) match correctly.
  • Role filter pills — one pill per role that's actually in use, each in the role's own colour. "All roles" pill resets. The count badge on each pill shows how many speakers carry that role. Pills are role-coloured when active.
  • Result counter — header shows 42 / 163 speakers when a filter is active, updating live as you type.
  • Empty state — "No speakers match your search." appears when the combined filter set returns zero results.

Server already sorts speakers by last_name → first_name in the repository, so alphabetical-by-surname order was free and the rendered DOM is the source of truth — JS never reorders.

Internal

  • Per-card metadata: data-search-text (searchable strings, UTF-8 lowercased) and data-role-ids (comma-separated role UUIDs). Filter loop is substring + set-intersection, no per-keystroke normalization.
  • Combined predicate: matchesSearch AND matchesRole. Cards toggle via the hidden attribute (cleaner than display: none — preserves layout for screen readers).
  • New setupSpeakers() initialiser wired into the same DOMContentLoaded pass that boots the schedule shortcodes.