Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
82e228c
fix(smoke): align /abap redirect seed + smoke probes to live behavior
jung-thomas Oct 20, 2026
27a15fe
Merge pull request #2854 from sap-tutorials/fix/2855-redirect-signal-…
jung-thomas Oct 20, 2026
3da534f
feat(#2811): load tutorial-variables island in QA preview
jung-thomas Oct 20, 2026
5277fa3
Merge pull request #2855 from sap-tutorials/qa-tutorial-variables-island
jung-thomas Oct 20, 2026
a81acc0
feat(#2852): validate value + display fields on tutorial variables
jung-thomas Oct 20, 2026
1c94307
feat(#2852): thread value/display through compose + source type
jung-thomas Oct 20, 2026
5e92034
test(#2852): guard value/display reach Hugo frontmatter (verbatim emi…
jung-thomas Oct 20, 2026
3e51243
feat(#2852): island hides non-displayed vars, value overrides default
jung-thomas Oct 20, 2026
685d843
docs(#2852): document value/display fields for tutorial variables
jung-thomas Oct 20, 2026
1c5b07b
test(#2852): cover displayed variable pre-filled with literal value (…
jung-thomas Oct 20, 2026
6a0eaeb
Merge pull request #2856 from sap-tutorials/feat/2852-hidden-tutorial…
jung-thomas Oct 20, 2026
1b4b6a3
fix(options): dedent OPTION-group trailing content to col 0 (#2820)
jung-thomas Oct 20, 2026
e0d941f
Merge pull request #2858 from sap-tutorials/worktree-fix-2820-option-…
jung-thomas Oct 20, 2026
bf10e01
fix(app-space): read eventId query param case-insensitively
jung-thomas Oct 20, 2026
fc0fbc6
Merge pull request #2859 from sap-tutorials/fix/appspace-eventid-case…
jung-thomas Oct 20, 2026
a5d1d1f
docs: app-space checkpoint prize-barcode design spec
jung-thomas Oct 20, 2026
5bc5cdf
docs: correct binding/verification note in checkpoint barcode design
jung-thomas Oct 20, 2026
6896f84
docs: app-space checkpoint prize-barcode implementation plan
jung-thomas Oct 20, 2026
2657651
fix(app-space): resolve CHECKPOINT title from checkpointTitle
jung-thomas Oct 20, 2026
fdfa143
feat(app-space): authenticated prize-barcode QR endpoint
jung-thomas Oct 20, 2026
29cd317
feat(app-space): render checkpoint barcode on track completion
jung-thomas Oct 20, 2026
5578fbc
fix(app-space): remove unlock-rule duplication in checkpoint-barcode …
jung-thomas Oct 20, 2026
db01830
feat(scanner): resolve by user UUID, return event-scoped claimed count
jung-thomas Oct 20, 2026
2b3aed1
feat(scanner): atomic capped prize handover (max 2 per user/event)
jung-thomas Oct 20, 2026
aca2143
fix(scanner): update stale auth test to use UUID accountNumber (Task …
jung-thomas Oct 20, 2026
f3f79ca
docs(scanner): clarify atomic-cap forUpdate transaction boundary comment
jung-thomas Oct 20, 2026
513d662
fix(app-space): expand GROUP path items + use per-path description
jung-thomas Oct 20, 2026
b4218df
Merge pull request #2860 from sap-tutorials/fix/app-space-group-expan…
jung-thomas Oct 20, 2026
d049f58
feat(scanner): show N-of-2 count and capped handover button
jung-thomas Oct 20, 2026
297dfce
fix(test): reset-tutorial getContestant calls use UUID accountNumber …
jung-thomas Oct 20, 2026
309af3d
fix(scanner): update UI5 scanner to new UUID barcode contract + fix h…
jung-thomas Oct 20, 2026
d0f11d2
Merge remote-tracking branch 'origin/DEV' into worktree-app-space-che…
jung-thomas Oct 20, 2026
0cba439
Merge pull request #2861 from sap-tutorials/worktree-app-space-checkp…
jung-thomas Oct 20, 2026
f953808
fix(app-space): render tutorial time as minutes, not seconds
jung-thomas Oct 20, 2026
b1818bd
Merge pull request #2862 from sap-tutorials/worktree-fix-appspace-tim…
jung-thomas Oct 20, 2026
d9ad24f
fix(#2861): resolve App Space prize-barcode 401 for logged-in users
jung-thomas Oct 20, 2026
6667cb0
Merge pull request #2863 from sap-tutorials/worktree-fix-prize-barcod…
jung-thomas Oct 20, 2026
74ccd12
docs(#2864): design + implementation plan for mission disable toggle
jung-thomas Oct 20, 2026
691e181
fix(#2861): resolve prize-barcode identity from cds.context.user (rea…
jung-thomas Oct 20, 2026
da4f966
feat(#2866): gate App Space tutorial completions to the event window
jung-thomas Oct 20, 2026
adb80ce
Merge pull request #2867 from sap-tutorials/worktree-fix-2861-prize-b…
jung-thomas Oct 20, 2026
8742d65
feat(#2864): add temporarilyUnavailable + unavailableReason to Missions
jung-thomas Oct 20, 2026
7a026ae
feat(#2866): show distinct redo state for out-of-window App Space com…
jung-thomas Oct 20, 2026
4d36750
feat(#2864): carry temporarilyUnavailable through /build/catalog + Mi…
jung-thomas Oct 21, 2026
c851fdd
docs(#2866): add App Space completion-window design + implementation …
jung-thomas Oct 21, 2026
8f4a685
feat(#2864): expose Temporarily Unavailable toggle in Mission Editor
jung-thomas Oct 21, 2026
f0fac77
feat(#2864): Temporarily Unavailable badge on mission cards
jung-thomas Oct 21, 2026
cab3cee
Merge remote-tracking branch 'origin/DEV' into worktree-issue-2866-ap…
jung-thomas Oct 21, 2026
80b86d0
Merge pull request #2868 from sap-tutorials/worktree-issue-2866-appsp…
jung-thomas Oct 21, 2026
646d9b9
feat(#2864): mission-detail feed returns no items for disabled missions
jung-thomas Oct 21, 2026
549cf43
test(#2864): seed path+item on disabled mission so suppression is act…
jung-thomas Oct 21, 2026
867a9f4
feat(#2864): navigator suppresses links for disabled missions
jung-thomas Oct 21, 2026
bad6118
build(db): regenerate migration tables for #2864
jung-thomas Oct 21, 2026
d6a600d
Merge pull request #2869 from sap-tutorials/worktree-2864-mission-dis…
jung-thomas Oct 21, 2026
5dfd034
chore(release): 1.40.3
jung-thomas Oct 21, 2026
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
2 changes: 1 addition & 1 deletion .deploy/mta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ ID: tutorials-ims
# Bump this on each release you deploy — it's the version shown by `cf mtas`
# and in the mtar filename (tutorials-ims_<version>.mtar). Deploy is manual:
# `cd .deploy && mbt build && cf deploy mta_archives/tutorials-ims_<version>.mtar -e ../deploy/<env>.mtaext -f`.
version: 1.40.2
version: 1.40.3

# Top-level parameters (overridable per-env via deploy/<env>.mtaext).
parameters:
Expand Down
5 changes: 5 additions & 0 deletions app/admin-annotations.cds
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ annotate AdminService.Missions with {
missionType @Common.Label: 'Type' @Common.ValueListWithFixedValues;
event @Common.Label: 'Event';
published @Common.Label: 'Published';
temporarilyUnavailable @Common.Label: 'Temporarily Unavailable';
unavailableReason @Common.Label: 'Unavailable Reason' @UI.MultiLineText;
status @Common.Label: 'Status' @Common.ValueListWithFixedValues;
averageTimeToComplete @Common.Label: 'Avg Time (min)';
qaPreviewUrl @Common.Label: 'QA Preview' @Common.FieldControl: #ReadOnly;
Expand All @@ -149,6 +151,7 @@ annotate AdminService.Missions with @UI: {
{ Value: experienceTag },
{ Value: primaryTagRef.name, Label: 'Primary Tag' },
{ Value: published, @UI.Importance: #High },
{ Value: temporarilyUnavailable },
{ Value: status }
],
Facets: [
Expand All @@ -166,6 +169,8 @@ annotate AdminService.Missions with @UI: {
{ Value: experienceTag },
{ Value: missionType },
{ Value: published, @UI.FieldControl: publishedFieldControl },
{ Value: temporarilyUnavailable },
{ Value: unavailableReason },
{ Value: status },
{ $Type: 'UI.DataFieldWithUrl', Value: qaPreviewLabel, Url: qaPreviewUrl, Label: 'QA Preview' },
{ $Type: 'UI.DataFieldWithUrl', Value: mainPreviewLabel, Url: mainPreviewUrl, Label: 'Live Mission' }
Expand Down
34 changes: 19 additions & 15 deletions app/scanner/webapp/controller/App.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ sap.ui.define([

var CONTESTANT_DEFAULTS = {
uid: null,
recordId: null,
status: null,
eventId: null,
claimedCount: 0,
tutorialsCompleted: "",
groupsCompleted: "",
missionsCompleted: "",
Expand All @@ -33,18 +33,20 @@ sap.ui.define([
this._oContestantModel.setData(Object.assign({}, CONTESTANT_DEFAULTS));
},

_getContestant: async function (uid) {
_getContestant: async function (uid, eventId) {
this._oContestantModel.setProperty("/loading", true);
try {
var oModel = this.getView().getModel();
var oContext = oModel.bindContext("/getContestant(...)");
oContext.setParameter("accountNumber", uid);
oContext.setParameter("eventId", eventId);
await oContext.execute();
var result = oContext.getBoundContext().getObject();
this._oContestantModel.setProperty("/tutorialsCompleted", result.tutorialsCompleted || "");
this._oContestantModel.setProperty("/groupsCompleted", result.groupsCompleted || "");
this._oContestantModel.setProperty("/missionsCompleted", result.missionsCompleted || "");
this._oContestantModel.setProperty("/prizeRecords", result.prizeRecords || "");
this._oContestantModel.setProperty("/claimedCount", result.claimedCount || 0);
this._oContestantModel.setProperty("/loaded", true);
this._oContestantModel.setProperty("/loading", false);
} catch (e) {
Expand Down Expand Up @@ -74,12 +76,14 @@ sap.ui.define([

var payload = scanResult.payload || {};
this._oContestantModel.setProperty("/uid", scanResult.uid || null);
this._oContestantModel.setProperty("/recordId", payload.recordId || null);
this._oContestantModel.setProperty("/status", payload.status || null);
this._oContestantModel.setProperty("/eventId", payload.eventId ? Number(payload.eventId) : null);
this._oContestantModel.setProperty("/loaded", false);

try {
await this._getContestant(this._oContestantModel.getProperty("/uid"));
await this._getContestant(
this._oContestantModel.getProperty("/uid"),
this._oContestantModel.getProperty("/eventId")
);
} catch (e) {
MessageBox.error("Could not load contestant: " + (e.message || "Unknown error"));
this._resetContestantModel();
Expand Down Expand Up @@ -118,11 +122,12 @@ sap.ui.define([

onClaimPrize: async function () {
var oContestantModel = this._oContestantModel;
var recordId = oContestantModel.getProperty("/recordId");
var uid = oContestantModel.getProperty("/uid");
var eventId = oContestantModel.getProperty("/eventId");
var claimedCount = oContestantModel.getProperty("/claimedCount");

if (!recordId) {
MessageBox.warning("No prize record found. Please re-scan.");
if (claimedCount >= 2) {
MessageBox.warning("Cap reached (2 of 2). No further prizes can be recorded.");
return;
}

Expand All @@ -131,16 +136,15 @@ sap.ui.define([

try {
var oModel = this.getView().getModel();
var oContext = oModel.bindContext("/claimPrize(...)");
oContext.setParameter("recordId", recordId);
// #889: accountNumber is required so the server can verify
// the prize belongs to the scanned contestant.
var oContext = oModel.bindContext("/claimPrizeForContestant(...)");
oContext.setParameter("accountNumber", uid);
oContext.setParameter("eventId", eventId);
await oContext.execute();
MessageToast.show("Prize claimed!", { duration: 2000 });
var result = oContext.getBoundContext().getObject();
MessageToast.show(result.message || "Prize recorded!", { duration: 2000 });

try {
await this._getContestant(uid);
await this._getContestant(uid, eventId);
} catch (e) {
MessageBox.error("Could not refresh contestant: " + (e.message || "Unknown error"));
oContestantModel.setProperty("/loading", false);
Expand Down
10 changes: 3 additions & 7 deletions app/scanner/webapp/view/App.view.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,6 @@
<VBox id="vboxIdentity" class="sapUiSmallMarginBeginEnd sapUiSmallMarginTopBottom">
<Title id="resultUid" text="{contestant>uid}" level="H5"
class="sapUiTinyMarginBottom" />
<Text id="resultIdentitySub"
text="{= ${contestant>recordId} + ' · ' + ${contestant>status} }"
wrapping="false"
class="sapUiTinyMarginBottom" />
</VBox>

<!-- Prizes Claimed — highlighted row -->
Expand All @@ -56,7 +52,7 @@
justifyContent="SpaceBetween"
alignItems="Center">
<Label id="lblPrizesClaimed" text="Prizes Claimed" design="Bold" />
<Text id="prizeRecords" text="{contestant>prizeRecords}" />
<Text id="prizeCount" text="{= ${contestant>claimedCount} + ' of 2' }" />
</HBox>

<!-- Remaining stats as a list -->
Expand Down Expand Up @@ -89,11 +85,11 @@
<ToolbarSpacer id="footerSpacer" />
<Button
id="btnClaimPrize"
text="Claim Prize"
text="Record prize handover"
icon="sap-icon://gift"
type="Emphasized"
press="onClaimPrize"
enabled="{contestant>loaded}" />
enabled="{= ${contestant>loaded} &amp;&amp; ${contestant>claimedCount} &lt; 2 }" />
</OverflowToolbar>
</footer>

Expand Down
2 changes: 1 addition & 1 deletion db/data/com.sap.developers.ims-LegacyRedirects.csv
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ ID;fromPath;toPath;statusCode;isPattern;isActive
66333900-1eaa-0001-0001-000000000015;^/(de|es|zh)$;/;301;true;true
66333900-1eaa-0001-0001-000000000016;^/(de|es|zh)/(.*)$;/$2;301;true;true
66333900-1eaa-0001-0001-000000000017;/cloud-sdk;/topics/cloud-sdk.html;301;false;true
66333900-1eaa-0001-0001-000000000018;/abap;/topics/abap-platform.html;301;false;true
66333900-1eaa-0001-0001-000000000018;/abap;https://pages.community.sap.com/topics/abap;301;false;true
66333900-1eaa-0001-0001-000000000019;/abapxml;/topics/abap-platform.html;301;false;true
66333900-1eaa-0001-0001-000000000020;/datahub;/group.datahub-docker.html;301;false;true
66333900-1eaa-0001-0001-000000000021;/tutorials/ml-fs-sapui5-series-changepoint-detection.html;/group.ml-fs-api-hub.html;301;false;true
Expand Down
12 changes: 12 additions & 0 deletions db/last-dev/csn.json
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,18 @@
},
"@cds.persistence.name": "PUBLISHED"
},
"temporarilyUnavailable": {
"type": "cds.Boolean",
"default": {
"val": false
},
"@cds.persistence.name": "TEMPORARILYUNAVAILABLE"
},
"unavailableReason": {
"type": "cds.String",
"length": 500,
"@cds.persistence.name": "UNAVAILABLEREASON"
},
"group_ID": {
"type": "cds.String",
"length": 36,
Expand Down
2 changes: 2 additions & 0 deletions db/schema.cds
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ entity Missions : TaskBase, LinkHealth {
// toggles published to true. Replaces AEM's role of curating which missions
// surface on the website. Write-guard at admin-service.js:788. Issue #348.
published : Boolean default false;
temporarilyUnavailable : Boolean default false;
unavailableReason : String(500);
primaryTagRef : Association to Tags;
group : Association to Groups;
event : Association to Events;
Expand Down
8 changes: 7 additions & 1 deletion db/src/com.sap.developers.ims.Missions.hdbmigrationtable
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
== version=8
== version=9
COLUMN TABLE com_sap_developers_ims_Missions (
ID NVARCHAR(36) NOT NULL,
createdAt TIMESTAMP,
Expand All @@ -23,12 +23,18 @@ COLUMN TABLE com_sap_developers_ims_Missions (
sourceKgCommunityFingerprint NVARCHAR(64),
missionType NVARCHAR(20),
published BOOLEAN DEFAULT FALSE,
temporarilyUnavailable BOOLEAN DEFAULT FALSE,
unavailableReason NVARCHAR(500),
group_ID NVARCHAR(36),
event_ID NVARCHAR(36),
groupOrder INTEGER DEFAULT 0,
PRIMARY KEY(ID)
)

== migration=9
-- generated by cds-compiler version 7.0.1
ALTER TABLE com_sap_developers_ims_Missions ADD (temporarilyUnavailable BOOLEAN DEFAULT FALSE, unavailableReason NVARCHAR(500));

== migration=8
-- generated by cds-compiler version 7.0.1
ALTER TABLE com_sap_developers_ims_Missions ADD (linkStatus NVARCHAR(8) DEFAULT 'UNKNOWN', lastChecked TIMESTAMP, linkStatusOverride NVARCHAR(8));
Expand Down
4 changes: 3 additions & 1 deletion db/views.cds
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ view NavigatorCatalog as
item.itemOrder,
item.taskType
}
where item.taskType = 'TUTORIAL' and tut.slug is not null and mission.published = true;
where item.taskType = 'TUTORIAL' and tut.slug is not null
and mission.published = true
and (mission.temporarilyUnavailable is null or mission.temporarilyUnavailable = false);

view SearchableItems as
SELECT from ims.Tutorials as t
Expand Down
18 changes: 17 additions & 1 deletion docs/authors/writing-tutorials.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ author_profile: https://github.com/janedoe
| `auto_validation` | No | Set `true` if the tutorial has quiz rules in the `-Contribution` repo. |
| `author_name`, `author_profile` | Recommended | Shown on the tutorial page. |
| `video` | No | Optional intro video shown as a styled player at the top of the steps. See [§3.3 Adding a video](#_3-3-adding-a-video). |
| `variables` | No | Array of per-reader values (e.g. a workshop group number) the reader fills in once and the platform substitutes live throughout the tutorial. See [§3.7 Personalize with variables](#_3-7-personalize-with-variables). |
| `variables` | No | Array of substituted values — either per-reader (e.g. a workshop group number the reader fills in) or author-fixed (`value:`, optionally `display: false` to hide). The platform substitutes them live throughout the tutorial. See [§3.7 Personalize with variables](#_3-7-personalize-with-variables). |

A canonical taxonomy of allowed `primary_tag` and `tags` values is maintained in the platform's `Tags` entity. To add a new tag or category, see [Center Admin](center-admin.md) § "Import a new tag".

Expand Down Expand Up @@ -356,6 +356,22 @@ Optionally pre-fill the value from the reader's identity with a `default` regex
pattern: 'da164-(\d+)@' # capture group 1 → default value
```
#### Author-fixed values and hidden variables
Sometimes the value is the **same for every reader** but repeated throughout the tutorial — a product name, a release version, a service URL. Set it **once** with `value:` and reference `{{var:<name>}}` everywhere; change it in one place and it updates across the whole tutorial. Add `display: false` so the reader never sees an input for it:

```yaml
variables:
- name: productVersion
type: string
value: "MyApp 2.0" # author-fixed literal — substituted everywhere
display: false # hidden: no input shown to the reader
```

- `display` is **optional and defaults to `true`** (shown). Only `display: false` hides the input. A hidden variable needs no `label`.
- `value:` overrides a `default:` regex. Precedence for the substituted value is: **`value` → `default` (regex) → what the reader types → empty**.
- A displayed variable (`display: true` or omitted) with a `value:` is **pre-filled** with that value but the reader can still edit it.

Key behaviors:

- **Build-enforced.** A malformed `variables:` block, an invalid `type`, a `default.from` off the allowlist `{email, id, givenName, familyName}`, an uncompilable `default.pattern`, or a `{{var:<name>}}` token referencing an undeclared name is a **hard build failure** — it can never reach readers. (A declared-but-unused variable is a soft warning.)
Expand Down
Loading
Loading