fix: support radio inputs in admin system config form #724
@@ -120,3 +120,6 @@ prime/
|
||||
|
||||
# A Makefile for custom make targets
|
||||
Makefile.local
|
||||
|
||||
# Local clone of the MCP server (separate repo, not a submodule of this project)
|
||||
/mcp-mokogitea-api/
|
||||
|
||||
@@ -47,6 +47,15 @@ jobs:
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
# This RC flow drives a Joomla-style update stream (updates.xml). Repos that don't ship
|
||||
# one (e.g. generic Go/TS) have nothing to package here, so no-op cleanly instead of
|
||||
# aborting under `set -e` when the file is absent.
|
||||
if [ ! -f updates.xml ]; then
|
||||
echo "has_updates=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No updates.xml in this repo — skipping RC update-stream packaging"
|
||||
exit 0
|
||||
fi
|
||||
echo "has_updates=true" >> "$GITHUB_OUTPUT"
|
||||
BASE_VERSION=$(sed -n 's/.*<version>\(.*\)<\/version>.*/\1/p' updates.xml | head -1)
|
||||
[ -z "$BASE_VERSION" ] && BASE_VERSION="04.00.00"
|
||||
RC_VERSION="${BASE_VERSION}-rc.${PR_NUMBER}"
|
||||
@@ -56,7 +65,7 @@ jobs:
|
||||
echo "RC version: $RC_VERSION (tag: $RC_TAG)"
|
||||
|
||||
- name: Update updates.xml RC channel
|
||||
if: steps.guard.outputs.skip != 'true'
|
||||
if: steps.guard.outputs.skip != 'true' && steps.version.outputs.has_updates == 'true'
|
||||
env:
|
||||
RC_VERSION: ${{ steps.version.outputs.version }}
|
||||
RC_TAG: ${{ steps.version.outputs.tag }}
|
||||
@@ -106,7 +115,7 @@ jobs:
|
||||
PYEOF
|
||||
|
||||
- name: Create RC release
|
||||
if: steps.guard.outputs.skip != 'true'
|
||||
if: steps.guard.outputs.skip != 'true' && steps.version.outputs.has_updates == 'true'
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
RC_TAG: ${{ steps.version.outputs.tag }}
|
||||
@@ -153,7 +162,7 @@ jobs:
|
||||
PYEOF
|
||||
|
||||
- name: Commit updates.xml
|
||||
if: steps.guard.outputs.skip != 'true'
|
||||
if: steps.guard.outputs.skip != 'true' && steps.version.outputs.has_updates == 'true'
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
@@ -47,15 +47,15 @@ jobs:
|
||||
fi
|
||||
;;
|
||||
fix/*|bugfix/*)
|
||||
if [ "$BASE" != "dev" ]; then
|
||||
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
|
||||
ALLOWED=false
|
||||
REASON="Fix branches must target 'dev', not '${BASE}'"
|
||||
REASON="Fix branches must target 'dev' or 'main', not '${BASE}'"
|
||||
fi
|
||||
;;
|
||||
patch/*)
|
||||
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
|
||||
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ] && [ "$BASE" != "main" ]; then
|
||||
ALLOWED=false
|
||||
REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
|
||||
REASON="Patch branches must target 'dev', 'rc', or 'main', not '${BASE}'"
|
||||
fi
|
||||
;;
|
||||
hotfix/*)
|
||||
@@ -86,7 +86,8 @@ jobs:
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`fix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`patch/*\` → \`dev\`, \`rc\`, or \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -147,10 +148,14 @@ jobs:
|
||||
- name: Detect platform
|
||||
id: platform
|
||||
run: |
|
||||
# Read platform from XML manifest (<platform> tag) or plain text fallback
|
||||
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
|
||||
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
|
||||
[ -z "$PLATFORM" ] && PLATFORM="generic"
|
||||
# Read platform from XML manifest (<platform> tag) if present; otherwise default to generic.
|
||||
# manifest.xml was removed in favor of the metadata API, so guard for its absence to avoid
|
||||
# aborting under `set -e` when the file is missing.
|
||||
PLATFORM="generic"
|
||||
if [ -f .mokogitea/manifest.xml ]; then
|
||||
DETECTED=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml | head -1)
|
||||
[ -n "$DETECTED" ] && PLATFORM="$DETECTED"
|
||||
fi
|
||||
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Setup PHP
|
||||
|
||||
@@ -57,6 +57,11 @@
|
||||
- Cherry-pick upstream v1.26.4: walk git log context error handling — regression fix (#38185)
|
||||
|
||||
### Fixed
|
||||
- Admin config form: radio inputs (e.g. instance landing page Mode) no longer throw "Unsupported config form value mapping", which had aborted all JS init on the admin settings page
|
||||
- PR check branch policy: allow `fix/*` → `main` and `patch/*` → `main` to match documented policy (was rejecting fix/patch PRs to main)
|
||||
- PR check platform detection: guard for missing `.mokogitea/manifest.xml` so the Validate PR job no longer aborts under `set -e` (manifest replaced by metadata API)
|
||||
- Remove dangling `mcp-mokogitea-api` submodule gitlink (no `.gitmodules` entry) that broke `submodule update --init` at checkout, failing all PR build/release jobs; ignore the local clone path
|
||||
- PR RC Release workflow: no-op cleanly when `updates.xml` is absent (generic repos) instead of aborting the "Determine RC version" step under `set -e`
|
||||
- PR check: platform detection now queries metadata API instead of removed manifest.xml
|
||||
- Cherry-pick upstream v1.26.2: handle empty pull request files view to allow reviews (#37783)
|
||||
- Cherry-pick upstream v1.26.2: fix "run as root" check with snap container detection (#37622)
|
||||
|
||||
Submodule mcp-mokogitea-api deleted from dbaf91546e
@@ -21,6 +21,17 @@ test('ConfigFormValueMapper', () => {
|
||||
<input name="struct.SubBoolean" type="checkbox" data-config-value-type="boolean">
|
||||
<input name="struct.SubTimestamp" type="datetime-local" data-config-value-type="timestamp">
|
||||
<textarea name="struct.NewKey">new-value</textarea>
|
||||
|
||||
<!-- radio group (sub key): only the option matching the config value should be checked and collected -->
|
||||
<input type="hidden" data-config-dyn-key="landing" data-config-value-json='{"Mode": "explore"}'>
|
||||
<input name="landing.Mode" type="radio" value="home">
|
||||
<input name="landing.Mode" type="radio" value="explore">
|
||||
<input name="landing.Mode" type="radio" value="login">
|
||||
|
||||
<!-- radio group with empty value: the server-rendered default (home) must be preserved, not unchecked -->
|
||||
<input type="hidden" data-config-dyn-key="landingDefault" data-config-value-json='{"Mode": ""}'>
|
||||
<input name="landingDefault.Mode" type="radio" value="home" checked>
|
||||
<input name="landingDefault.Mode" type="radio" value="explore">
|
||||
</form>
|
||||
`;
|
||||
|
||||
@@ -44,5 +55,7 @@ test('ConfigFormValueMapper', () => {
|
||||
'k-flipped-true': 'true',
|
||||
'repository.open-with.editor-apps': '[{"DisplayName":"a","OpenURL":"b"}]', // TODO: OPEN-WITH-EDITOR-APP-JSON: it must match backend
|
||||
'struct': '{"SubBoolean":true,"SubTimestamp":123456780,"OtherKey":"other-value","NewKey":"new-value"}',
|
||||
'landing': '{"Mode":"explore"}',
|
||||
'landingDefault': '{"Mode":"home"}',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -99,6 +99,10 @@ export class ConfigFormValueMapper {
|
||||
if (el.matches('[type="checkbox"]')) {
|
||||
if (valType !== 'boolean') requireExplicitValueType(el);
|
||||
el.checked = Boolean(val ?? el.checked);
|
||||
} else if (el.matches('[type="radio"]')) {
|
||||
// a radio group shares one name; check only the option whose value equals the config value.
|
||||
// when the value is empty (unset), leave the server-rendered default selection untouched.
|
||||
if (String(val) !== '') el.checked = el.value === String(val);
|
||||
} else if (el.matches('[type="datetime-local"]')) {
|
||||
if (valType !== 'timestamp') requireExplicitValueType(el);
|
||||
if (val) el.value = toDatetimeLocalValue(val);
|
||||
@@ -120,6 +124,9 @@ export class ConfigFormValueMapper {
|
||||
// it needs to iterate the "namedElems" to find all the checkboxes with the same name and collect values accordingly,
|
||||
// and set the namedElems[matchedIdx] to null to avoid duplicate processing.
|
||||
val = collectCheckboxBooleanValue(el);
|
||||
} else if (el.matches('[type="radio"]')) {
|
||||
// only the checked radio of a group reaches here (callers skip unchecked ones); its value is the selection
|
||||
val = el.value;
|
||||
} else if (el.matches('[type="datetime-local"]')) {
|
||||
if (valType !== 'timestamp') requireExplicitValueType(el);
|
||||
val = Math.floor(new Date(el.value).getTime() / 1000) ?? 0; // NaN is fine to JSON.stringify, it becomes null.
|
||||
@@ -139,6 +146,12 @@ export class ConfigFormValueMapper {
|
||||
if (!el) continue;
|
||||
const subKey = extractElemConfigSubKey(el, dynKey);
|
||||
if (!subKey) continue; // if not match, skip
|
||||
// a radio group has N elements sharing the same name; only the checked one carries the value.
|
||||
// drop the unchecked ones so they neither overwrite the selection here nor leak into the fallback loop.
|
||||
if (el.matches('[type="radio"]') && !el.checked) {
|
||||
namedElems[idx] = null;
|
||||
continue;
|
||||
}
|
||||
cfgVal[subKey] = this.collectConfigValueFromElement(el);
|
||||
namedElems[idx] = null;
|
||||
}
|
||||
@@ -194,6 +207,7 @@ export class ConfigFormValueMapper {
|
||||
// "foo.enabled" => "true"
|
||||
for (const el of namedElems) {
|
||||
if (!el) continue;
|
||||
if (el.matches('[type="radio"]') && !el.checked) continue; // skip unchecked radios of a top-level group
|
||||
const dynKey = el.name;
|
||||
const newVal = this.collectConfigValueFromElement(el);
|
||||
formData.append('key', dynKey);
|
||||
|
||||
Reference in New Issue
Block a user