Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions examples/polls/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# SPDX-License-Identifier: Apache-2.0
#
# polls — rung 3 of the application ladder (examples/polls/README.md).
# All target wiring lives in morph_add_rung() (cmake/morph_add_rung.cmake);
# this file only pulls in polls-specific sources it doesn't know about, then
# calls it.

cmake_minimum_required(VERSION 3.25)

morph_add_rung(NAME polls)

# morph_add_rung() only globs src/models/*.cpp, src/db/*.cpp and
# src/app/*.cpp into ladder_polls_lib (cmake/morph_add_rung.cmake:91-92)
# — it does not know about this rung's src/auth/ (Tasks 1-10's
# PollsAuthorizer), so without an explicit target_sources() call the rung
# fails to link with undefined polls::auth::PollsAuthorizer symbols.
# Mirrors bookmarks' own CMakeLists.txt treatment of src/import/ and src/dto/.
# (src/db/schema.cpp needs no equivalent line here -- the glob above already
# covers src/db/*.cpp.)
if(TARGET ladder_polls_lib)
target_sources(ladder_polls_lib PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/src/auth/polls_authorizer.cpp")
endif()

# ladder_polls_lib is native-only (morph_add_rung.cmake's own comment:
# "ladder_<rung>_gui_wasm never links ladder_<rung>_lib — so this target
# genuinely never needs to build under Emscripten at all"), so the
# target_sources() call above silently no-ops under EMSCRIPTEN. Nothing in
# ladder_polls_gui_wasm references PollsAuthorizer today, so this has not
# yet produced bookmarks' identical undefined-symbol link failure — but the
# same trap is there the moment it does. polls_authorizer.cpp has no
# persistence dependency, so it is equally at home in ladder_polls_gui_lib,
# which does build under Emscripten and is what ladder_polls_gui_wasm links.
# Mirrors bookmarks' own CMakeLists.txt treatment of the identical gap for
# src/dto/auth_dto.cpp.
if(TARGET ladder_polls_gui_lib AND NOT TARGET ladder_polls_lib)
target_sources(ladder_polls_gui_lib PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/src/auth/polls_authorizer.cpp")
endif()

# ── The WASM client's server url ────────────────────────────────────────────
# Same mechanism as pastebin's/bookmarks' own CMakeLists.txt — see either
# file's comment. Port 8767 matches ladder_polls_server's own compiled-in
# default (src/server/main.cpp), the next free port after pastebin's 8765 and
# bookmarks' 8766.
if(TARGET ladder_polls_gui_wasm)
if(NOT DEFINED MORPH_LADDER_POLLS_WASM_SERVER_URL)
set(MORPH_LADDER_POLLS_WASM_SERVER_URL "ws://127.0.0.1:8767" CACHE STRING
"URL polls' WASM client connects to; must be a reachable ladder_polls_server.")
endif()
target_compile_definitions(ladder_polls_gui_wasm PRIVATE
MORPH_LADDER_POLLS_WASM_SERVER_URL="${MORPH_LADDER_POLLS_WASM_SERVER_URL}"
)
endif()
487 changes: 487 additions & 0 deletions examples/polls/README.md

Large diffs are not rendered by default.

202 changes: 202 additions & 0 deletions examples/polls/gui/qml/CreatePollView.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
// SPDX-License-Identifier: Apache-2.0
//
// The organizer's create-poll screen. Native-client-only (Main.qml only ever
// pushes this behind its nativeClient gate) — see examples/polls/README.md's
// resolved design decision 6.
//
// CreatePoll::options is std::vector<CreatePollOption> -- a JSON array of
// *objects*, not the array-of-strings DynamicForm's array-field control
// (src/qt/forms/qml/DynamicForm.qml's arrayJsonLiteral) supports — this
// whole screen is therefore driven by hand, not by a DynamicForm at all,
// exactly like rung 2's BulkEdit workaround: a plain
// title TextField plus a small hand-written option-label list editor (add/
// remove rows), submitted via PollBridge::createPoll(title, optionLabels)
// directly. See poll_schemas.hpp's own doc comment.
//
// `pollBridge` defaults to null so this same file also loads with nothing
// wired up, which is exactly what the offscreen engine-load smoke test
// (tests/test_gui_qml_smoke.cpp) does.

pragma ComponentBehavior: Bound

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

Item {
id: page

property var pollBridge: null

/// Emitted when the organizer chooses to go straight to the freshly
/// created poll's vote view. Main.qml listens and pushes VoteView.
signal openRequested(string pollId)

property string titleText: ""
property var optionLabels: ["", ""] // CreatePoll requires 2-20 options
property var lastResult: null // {pollId, adminToken, participantToken}
property string status: ""
property bool statusIsError: false

readonly property bool canSubmit: page.pollBridge !== null
&& page.titleText.trim() !== ""
&& page.optionLabels.length >= 2
&& page.optionLabels.every(function (label) { return label.trim() !== "" })

function addOption() {
page.optionLabels = page.optionLabels.concat([""])
}

function removeOption(index) {
if (page.optionLabels.length <= 2)
return
const next = page.optionLabels.slice()
next.splice(index, 1)
page.optionLabels = next
}

function setOption(index, text) {
const next = page.optionLabels.slice()
next[index] = text
page.optionLabels = next
}

Connections {
target: page.pollBridge

function onCreated(result) {
page.lastResult = result
page.status = "poll created — copy the admin token before leaving this screen"
page.statusIsError = false
}

function onFailed(message) {
page.status = message
page.statusIsError = true
}
}

ColumnLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 8

RowLayout {
Layout.fillWidth: true
Button {
text: "< Back"
onClicked: page.StackView.view.pop()
}
Label {
Layout.fillWidth: true
font.bold: true
text: "Create a poll"
}
}

Label {
Layout.fillWidth: true
visible: page.status !== ""
wrapMode: Text.Wrap
color: page.statusIsError ? "#d33" : palette.text
text: page.status
}

ColumnLayout {
Layout.fillWidth: true
visible: page.lastResult === null
spacing: 6

Label { text: "Title" }
TextField {
Layout.fillWidth: true
placeholderText: "e.g. Team offsite"
onTextChanged: page.titleText = text
}

Label { text: "Candidate dates/options (2-20)" }

Repeater {
model: page.optionLabels

delegate: RowLayout {
id: row
required property string modelData
required property int index
Layout.fillWidth: true

TextField {
Layout.fillWidth: true
placeholderText: "e.g. 2026-09-01"
text: row.modelData
onTextChanged: page.setOption(row.index, text)
}

Button {
text: "remove"
enabled: page.optionLabels.length > 2
onClicked: page.removeOption(row.index)
}
}
}

Button {
text: "+ add option"
enabled: page.optionLabels.length < 20
onClicked: page.addOption()
}

Button {
Layout.fillWidth: true
text: "Create poll"
enabled: page.canSubmit
onClicked: page.pollBridge.createPoll(page.titleText, page.optionLabels)
}
}

ColumnLayout {
Layout.fillWidth: true
visible: page.lastResult !== null
spacing: 6

Label {
Layout.fillWidth: true
text: "Poll id (share this link's id with participants):"
}
TextField {
Layout.fillWidth: true
readOnly: true
selectByMouse: true
text: page.lastResult ? page.lastResult.pollId : ""
}

Label {
Layout.fillWidth: true
text: "Admin token (keep this — needed to finalize the poll):"
}
TextField {
Layout.fillWidth: true
readOnly: true
selectByMouse: true
text: page.lastResult ? page.lastResult.adminToken : ""
}

Label {
Layout.fillWidth: true
text: "Participant token (goes out with the shared link):"
}
TextField {
Layout.fillWidth: true
readOnly: true
selectByMouse: true
text: page.lastResult ? page.lastResult.participantToken : ""
}

Button {
Layout.fillWidth: true
text: "Open this poll now"
onClicked: page.openRequested(page.lastResult.pollId)
}
}
}
}
Loading
Loading