diff --git a/Core/GDCore/Project/EventsFunction.cpp b/Core/GDCore/Project/EventsFunction.cpp
index 7c941232e3b1..4f5caf09776a 100644
--- a/Core/GDCore/Project/EventsFunction.cpp
+++ b/Core/GDCore/Project/EventsFunction.cpp
@@ -75,6 +75,9 @@ void EventsFunction::SerializeTo(SerializerElement& element) const {
if (isAsync) {
element.SetBoolAttribute("async", isAsync);
}
+ if (!helpUrl.empty()) {
+ element.SetAttribute("helpUrl", helpUrl);
+ }
events.SerializeTo(element.AddChild("events"));
gd::String functionTypeStr = "Action";
@@ -116,6 +119,7 @@ void EventsFunction::UnserializeFrom(gd::Project& project,
getterName = element.GetStringAttribute("getterName");
isPrivate = element.GetBoolAttribute("private");
isAsync = element.GetBoolAttribute("async");
+ helpUrl = element.GetStringAttribute("helpUrl");
events.UnserializeFrom(project, element.GetChild("events"));
gd::String functionTypeStr = element.GetStringAttribute("functionType");
diff --git a/Core/GDCore/Project/EventsFunction.h b/Core/GDCore/Project/EventsFunction.h
index 886b7401759c..9f1d8a9dac4c 100644
--- a/Core/GDCore/Project/EventsFunction.h
+++ b/Core/GDCore/Project/EventsFunction.h
@@ -223,6 +223,19 @@ class GD_CORE_API EventsFunction {
return *this;
}
+ /**
+ * \brief Get the help URL for this function.
+ */
+ const gd::String& GetHelpUrl() const { return helpUrl; }
+
+ /**
+ * \brief Set the help URL for this function.
+ */
+ EventsFunction& SetHelpUrl(const gd::String& helpUrl_) {
+ helpUrl = helpUrl_;
+ return *this;
+ }
+
/**
* \brief Return the events.
*/
@@ -304,6 +317,7 @@ class GD_CORE_API EventsFunction {
gd::ObjectGroupsContainer objectGroups;
bool isPrivate = false;
bool isAsync = false;
+ gd::String helpUrl;
};
} // namespace gd
diff --git a/Core/tests/EventsFunction.cpp b/Core/tests/EventsFunction.cpp
new file mode 100644
index 000000000000..bbbdbda17703
--- /dev/null
+++ b/Core/tests/EventsFunction.cpp
@@ -0,0 +1,88 @@
+/*
+ * GDevelop Core
+ * Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
+ * reserved. This project is released under the MIT License.
+ */
+/**
+ * @file Tests covering EventsFunction
+ */
+#include "GDCore/Project/EventsFunction.h"
+#include "GDCore/Project/Project.h"
+#include "GDCore/Serialization/SerializerElement.h"
+#include "catch.hpp"
+
+TEST_CASE("EventsFunction", "[common]") {
+ SECTION("Basic properties") {
+ gd::EventsFunction eventsFunction;
+
+ eventsFunction.SetName("MyFunction");
+ eventsFunction.SetFullName("My Function");
+ eventsFunction.SetDescription("A test function");
+ eventsFunction.SetGroup("Test Group");
+ eventsFunction.SetSentence("Do something with _PARAM1_");
+
+ REQUIRE(eventsFunction.GetName() == "MyFunction");
+ REQUIRE(eventsFunction.GetFullName() == "My Function");
+ REQUIRE(eventsFunction.GetDescription() == "A test function");
+ REQUIRE(eventsFunction.GetGroup() == "Test Group");
+ REQUIRE(eventsFunction.GetSentence() == "Do something with _PARAM1_");
+ }
+
+ SECTION("Help URL") {
+ gd::EventsFunction eventsFunction;
+
+ // Default should be empty
+ REQUIRE(eventsFunction.GetHelpUrl() == "");
+
+ // Can set a help URL
+ eventsFunction.SetHelpUrl("https://example.com/help");
+ REQUIRE(eventsFunction.GetHelpUrl() == "https://example.com/help");
+
+ // Can clear the help URL
+ eventsFunction.SetHelpUrl("");
+ REQUIRE(eventsFunction.GetHelpUrl() == "");
+ }
+
+ SECTION("Serialization with help URL") {
+ gd::Project project;
+
+ gd::EventsFunction eventsFunction;
+ eventsFunction.SetName("MyFunction");
+ eventsFunction.SetFullName("My Function");
+ eventsFunction.SetDescription("A test function");
+ eventsFunction.SetHelpUrl("https://example.com/custom-help");
+
+ gd::SerializerElement element;
+ eventsFunction.SerializeTo(element);
+
+ gd::EventsFunction eventsFunction2;
+ eventsFunction2.UnserializeFrom(project, element);
+
+ REQUIRE(eventsFunction2.GetName() == "MyFunction");
+ REQUIRE(eventsFunction2.GetFullName() == "My Function");
+ REQUIRE(eventsFunction2.GetDescription() == "A test function");
+ REQUIRE(eventsFunction2.GetHelpUrl() == "https://example.com/custom-help");
+ }
+
+ SECTION("Serialization without help URL") {
+ gd::Project project;
+
+ gd::EventsFunction eventsFunction;
+ eventsFunction.SetName("MyFunction");
+ eventsFunction.SetFullName("My Function");
+ eventsFunction.SetDescription("A test function");
+ // No help URL set
+
+ gd::SerializerElement element;
+ eventsFunction.SerializeTo(element);
+
+ gd::EventsFunction eventsFunction2;
+ eventsFunction2.UnserializeFrom(project, element);
+
+ REQUIRE(eventsFunction2.GetName() == "MyFunction");
+ REQUIRE(eventsFunction2.GetFullName() == "My Function");
+ REQUIRE(eventsFunction2.GetDescription() == "A test function");
+ REQUIRE(eventsFunction2.GetHelpUrl() == "");
+ }
+}
+
diff --git a/GDJS/GDJS/Events/CodeGeneration/MetadataDeclarationHelper.cpp b/GDJS/GDJS/Events/CodeGeneration/MetadataDeclarationHelper.cpp
index a443eae049e9..65d48f2c594f 100644
--- a/GDJS/GDJS/Events/CodeGeneration/MetadataDeclarationHelper.cpp
+++ b/GDJS/GDJS/Events/CodeGeneration/MetadataDeclarationHelper.cpp
@@ -534,6 +534,9 @@ MetadataDeclarationHelper::DeclareExpressionMetadata(
expressionAndCondition.AddCodeOnlyParameter("currentScene", "");
DeclareEventsFunctionParameters(freeEventsFunctions, eventsFunction,
expressionAndCondition, 0);
+ if (!eventsFunction.GetHelpUrl().empty()) {
+ expressionAndCondition.SetHelpPath(eventsFunction.GetHelpUrl());
+ }
expressionAndConditions.push_back(expressionAndCondition);
return expressionAndConditions.back();
} else {
@@ -555,6 +558,9 @@ MetadataDeclarationHelper::DeclareExpressionMetadata(
expression.AddCodeOnlyParameter("currentScene", "");
DeclareEventsFunctionParameters(freeEventsFunctions, eventsFunction,
expression, 0);
+ if (!eventsFunction.GetHelpUrl().empty()) {
+ expression.SetHelpPath(eventsFunction.GetHelpUrl());
+ }
return expression;
}
}
@@ -580,6 +586,9 @@ gd::InstructionMetadata &MetadataDeclarationHelper::DeclareInstructionMetadata(
condition.AddCodeOnlyParameter("currentScene", "");
DeclareEventsFunctionParameters(freeEventsFunctions, eventsFunction,
condition, 0);
+ if (!eventsFunction.GetHelpUrl().empty()) {
+ condition.SetHelpPath(eventsFunction.GetHelpUrl());
+ }
return condition;
} else if (functionType == gd::EventsFunction::ActionWithOperator) {
if (freeEventsFunctions.HasEventsFunctionNamed(
@@ -606,6 +615,9 @@ gd::InstructionMetadata &MetadataDeclarationHelper::DeclareInstructionMetadata(
action.AddCodeOnlyParameter("currentScene", "");
DeclareEventsFunctionParameters(freeEventsFunctions, eventsFunction,
action, 0);
+ if (!eventsFunction.GetHelpUrl().empty()) {
+ action.SetHelpPath(eventsFunction.GetHelpUrl());
+ }
return action;
} else {
@@ -620,6 +632,9 @@ gd::InstructionMetadata &MetadataDeclarationHelper::DeclareInstructionMetadata(
action.AddCodeOnlyParameter("currentScene", "");
DeclareEventsFunctionParameters(freeEventsFunctions, eventsFunction,
action, 0);
+ if (!eventsFunction.GetHelpUrl().empty()) {
+ action.SetHelpPath(eventsFunction.GetHelpUrl());
+ }
return action;
}
} else {
@@ -633,6 +648,9 @@ gd::InstructionMetadata &MetadataDeclarationHelper::DeclareInstructionMetadata(
action.AddCodeOnlyParameter("currentScene", "");
DeclareEventsFunctionParameters(freeEventsFunctions, eventsFunction,
action, 0);
+ if (!eventsFunction.GetHelpUrl().empty()) {
+ action.SetHelpPath(eventsFunction.GetHelpUrl());
+ }
return action;
}
}
@@ -725,6 +743,9 @@ MetadataDeclarationHelper::DeclareBehaviorExpressionMetadata(
GetExtensionIconUrl(extension));
DeclareEventsFunctionParameters(eventsBasedBehavior.GetEventsFunctions(),
eventsFunction, expressionAndCondition, 2);
+ if (!eventsFunction.GetHelpUrl().empty()) {
+ expressionAndCondition.SetHelpPath(eventsFunction.GetHelpUrl());
+ }
expressionAndConditions.push_back(expressionAndCondition);
return expressionAndConditions.back();
} else {
@@ -750,6 +771,9 @@ MetadataDeclarationHelper::DeclareBehaviorExpressionMetadata(
GetExtensionIconUrl(extension));
DeclareEventsFunctionParameters(eventsBasedBehavior.GetEventsFunctions(),
eventsFunction, expression, 2);
+ if (!eventsFunction.GetHelpUrl().empty()) {
+ expression.SetHelpPath(eventsFunction.GetHelpUrl());
+ }
return expression;
}
}
@@ -778,6 +802,9 @@ MetadataDeclarationHelper::DeclareBehaviorInstructionMetadata(
GetExtensionIconUrl(extension), GetExtensionIconUrl(extension));
DeclareEventsFunctionParameters(eventsBasedBehavior.GetEventsFunctions(),
eventsFunction, condition, 2);
+ if (!eventsFunction.GetHelpUrl().empty()) {
+ condition.SetHelpPath(eventsFunction.GetHelpUrl());
+ }
return condition;
} else if (functionType == gd::EventsFunction::ActionWithOperator) {
auto &eventsFunctionsContainer = eventsBasedBehavior.GetEventsFunctions();
@@ -803,6 +830,9 @@ MetadataDeclarationHelper::DeclareBehaviorInstructionMetadata(
DeclareEventsFunctionParameters(eventsBasedBehavior.GetEventsFunctions(),
eventsFunction, action, 2);
+ if (!eventsFunction.GetHelpUrl().empty()) {
+ action.SetHelpPath(eventsFunction.GetHelpUrl());
+ }
return action;
} else {
auto &action = behaviorMetadata.AddScopedAction(
@@ -816,6 +846,9 @@ MetadataDeclarationHelper::DeclareBehaviorInstructionMetadata(
DeclareEventsFunctionParameters(eventsBasedBehavior.GetEventsFunctions(),
eventsFunction, action, 2);
+ if (!eventsFunction.GetHelpUrl().empty()) {
+ action.SetHelpPath(eventsFunction.GetHelpUrl());
+ }
return action;
}
} else {
@@ -833,6 +866,9 @@ MetadataDeclarationHelper::DeclareBehaviorInstructionMetadata(
DeclareEventsFunctionParameters(eventsBasedBehavior.GetEventsFunctions(),
eventsFunction, action, 2);
+ if (!eventsFunction.GetHelpUrl().empty()) {
+ action.SetHelpPath(eventsFunction.GetHelpUrl());
+ }
return action;
}
}
@@ -899,6 +935,9 @@ MetadataDeclarationHelper::DeclareObjectExpressionMetadata(
DeclareEventsFunctionParameters(eventsBasedObject.GetEventsFunctions(),
eventsFunction, expressionAndCondition, 1);
+ if (!eventsFunction.GetHelpUrl().empty()) {
+ expressionAndCondition.SetHelpPath(eventsFunction.GetHelpUrl());
+ }
expressionAndConditions.push_back(expressionAndCondition);
return expressionAndConditions.back();
} else {
@@ -925,6 +964,9 @@ MetadataDeclarationHelper::DeclareObjectExpressionMetadata(
DeclareEventsFunctionParameters(eventsBasedObject.GetEventsFunctions(),
eventsFunction, expression, 1);
+ if (!eventsFunction.GetHelpUrl().empty()) {
+ expression.SetHelpPath(eventsFunction.GetHelpUrl());
+ }
return expression;
}
}
@@ -954,6 +996,9 @@ MetadataDeclarationHelper::DeclareObjectInstructionMetadata(
DeclareEventsFunctionParameters(eventsBasedObject.GetEventsFunctions(),
eventsFunction, condition, 1);
+ if (!eventsFunction.GetHelpUrl().empty()) {
+ condition.SetHelpPath(eventsFunction.GetHelpUrl());
+ }
return condition;
} else if (functionType == gd::EventsFunction::ActionWithOperator) {
auto &eventsFunctionsContainer = eventsBasedObject.GetEventsFunctions();
@@ -978,6 +1023,9 @@ MetadataDeclarationHelper::DeclareObjectInstructionMetadata(
DeclareEventsFunctionParameters(eventsBasedObject.GetEventsFunctions(),
eventsFunction, action, 1);
+ if (!eventsFunction.GetHelpUrl().empty()) {
+ action.SetHelpPath(eventsFunction.GetHelpUrl());
+ }
return action;
} else {
auto &action = objectMetadata.AddScopedAction(
@@ -990,6 +1038,9 @@ MetadataDeclarationHelper::DeclareObjectInstructionMetadata(
DeclareEventsFunctionParameters(eventsBasedObject.GetEventsFunctions(),
eventsFunction, action, 1);
+ if (!eventsFunction.GetHelpUrl().empty()) {
+ action.SetHelpPath(eventsFunction.GetHelpUrl());
+ }
return action;
}
} else {
@@ -1007,6 +1058,9 @@ MetadataDeclarationHelper::DeclareObjectInstructionMetadata(
DeclareEventsFunctionParameters(eventsBasedObject.GetEventsFunctions(),
eventsFunction, action, 1);
+ if (!eventsFunction.GetHelpUrl().empty()) {
+ action.SetHelpPath(eventsFunction.GetHelpUrl());
+ }
return action;
}
}
diff --git a/GDevelop.js/Bindings/Bindings.idl b/GDevelop.js/Bindings/Bindings.idl
index 5ddbe2d01686..087a3b9c1864 100644
--- a/GDevelop.js/Bindings/Bindings.idl
+++ b/GDevelop.js/Bindings/Bindings.idl
@@ -3165,6 +3165,8 @@ interface EventsFunction {
boolean IsPrivate();
[Ref] EventsFunction SetAsync(boolean isAsync);
boolean IsAsync();
+ [Ref] EventsFunction SetHelpUrl([Const] DOMString helpUrl);
+ [Const, Ref] DOMString GetHelpUrl();
boolean IsAction();
boolean IsExpression();
boolean IsCondition();
diff --git a/GDevelop.js/__tests__/Core.js b/GDevelop.js/__tests__/Core.js
index 07ea513fea09..6eb5d907053e 100644
--- a/GDevelop.js/__tests__/Core.js
+++ b/GDevelop.js/__tests__/Core.js
@@ -4579,6 +4579,15 @@ describe('libGD.js', function () {
expect(eventsFunction.getDescription()).toBe('My description');
eventsFunction.delete();
});
+ it('can have a help URL', function () {
+ const eventsFunction = new gd.EventsFunction();
+ expect(eventsFunction.getHelpUrl()).toBe('');
+ eventsFunction.setHelpUrl('https://example.com/help');
+ expect(eventsFunction.getHelpUrl()).toBe('https://example.com/help');
+ eventsFunction.setHelpUrl('');
+ expect(eventsFunction.getHelpUrl()).toBe('');
+ eventsFunction.delete();
+ });
});
describe('gd.EventsFunctionsExtension', () => {
diff --git a/GDevelop.js/__tests__/MetadataDeclarationHelper.js b/GDevelop.js/__tests__/MetadataDeclarationHelper.js
index 5388f624dd4c..0daeea381f35 100644
--- a/GDevelop.js/__tests__/MetadataDeclarationHelper.js
+++ b/GDevelop.js/__tests__/MetadataDeclarationHelper.js
@@ -43,6 +43,72 @@ describe('MetadataDeclarationHelper', () => {
project.delete();
});
+ it('propagates helpUrl to action metadata', () => {
+ const extension = new gd.PlatformExtension();
+ const project = new gd.Project();
+
+ const eventExtension = project.insertNewEventsFunctionsExtension(
+ 'MyExtension',
+ 0
+ );
+ const freeEventsFunctions = eventExtension.getEventsFunctions();
+ const eventFunction = freeEventsFunctions.insertNewEventsFunction(
+ 'MyFunction',
+ 0
+ );
+ eventFunction.setFunctionType(gd.EventsFunction.Action);
+ eventFunction.setHelpUrl('https://example.com/custom-help');
+
+ const metadataDeclarationHelper = new gd.MetadataDeclarationHelper();
+ metadataDeclarationHelper.generateFreeFunctionMetadata(
+ project,
+ extension,
+ eventExtension,
+ eventFunction
+ );
+ metadataDeclarationHelper.delete();
+
+ expect(extension.getAllActions().has('MyFunction')).toBe(true);
+ const action = extension.getAllActions().get('MyFunction');
+ expect(action.getHelpPath()).toBe('https://example.com/custom-help');
+
+ extension.delete();
+ project.delete();
+ });
+
+ it('does not set helpPath when helpUrl is empty', () => {
+ const extension = new gd.PlatformExtension();
+ const project = new gd.Project();
+
+ const eventExtension = project.insertNewEventsFunctionsExtension(
+ 'MyExtension',
+ 0
+ );
+ const freeEventsFunctions = eventExtension.getEventsFunctions();
+ const eventFunction = freeEventsFunctions.insertNewEventsFunction(
+ 'MyFunction',
+ 0
+ );
+ eventFunction.setFunctionType(gd.EventsFunction.Action);
+ // No helpUrl set (empty by default)
+
+ const metadataDeclarationHelper = new gd.MetadataDeclarationHelper();
+ metadataDeclarationHelper.generateFreeFunctionMetadata(
+ project,
+ extension,
+ eventExtension,
+ eventFunction
+ );
+ metadataDeclarationHelper.delete();
+
+ expect(extension.getAllActions().has('MyFunction')).toBe(true);
+ const action = extension.getAllActions().get('MyFunction');
+ expect(action.getHelpPath()).toBe('');
+
+ extension.delete();
+ project.delete();
+ });
+
it('can create metadata for free actions with an underscore and unicode characters', () => {
const extension = new gd.PlatformExtension();
const project = new gd.Project();
@@ -114,6 +180,78 @@ describe('MetadataDeclarationHelper', () => {
project.delete();
});
+ it('can create metadata for free actions with a help URL', () => {
+ const extension = new gd.PlatformExtension();
+ const project = new gd.Project();
+
+ const eventExtension = project.insertNewEventsFunctionsExtension(
+ 'MyExtension',
+ 0
+ );
+ const freeEventsFunctions = eventExtension.getEventsFunctions();
+ const eventFunction = freeEventsFunctions.insertNewEventsFunction(
+ 'MyFunction',
+ 0
+ );
+ eventFunction.setFunctionType(gd.EventsFunction.Action);
+ eventFunction.setFullName('My function');
+ eventFunction.setDescription('My function description.');
+ eventFunction.setSentence('My function sentence');
+ eventFunction.setHelpUrl('https://example.com/help');
+
+ const metadataDeclarationHelper = new gd.MetadataDeclarationHelper();
+ metadataDeclarationHelper.generateFreeFunctionMetadata(
+ project,
+ extension,
+ eventExtension,
+ eventFunction
+ );
+ metadataDeclarationHelper.delete();
+
+ expect(extension.getAllActions().has('MyFunction')).toBe(true);
+ const action = extension.getAllActions().get('MyFunction');
+ expect(action.getHelpPath()).toBe('https://example.com/help');
+
+ extension.delete();
+ project.delete();
+ });
+
+ it('does not set help path when help URL is empty', () => {
+ const extension = new gd.PlatformExtension();
+ const project = new gd.Project();
+
+ const eventExtension = project.insertNewEventsFunctionsExtension(
+ 'MyExtension',
+ 0
+ );
+ const freeEventsFunctions = eventExtension.getEventsFunctions();
+ const eventFunction = freeEventsFunctions.insertNewEventsFunction(
+ 'MyFunction',
+ 0
+ );
+ eventFunction.setFunctionType(gd.EventsFunction.Action);
+ eventFunction.setFullName('My function');
+ eventFunction.setDescription('My function description.');
+ eventFunction.setSentence('My function sentence');
+ // Help URL is not set (empty by default)
+
+ const metadataDeclarationHelper = new gd.MetadataDeclarationHelper();
+ metadataDeclarationHelper.generateFreeFunctionMetadata(
+ project,
+ extension,
+ eventExtension,
+ eventFunction
+ );
+ metadataDeclarationHelper.delete();
+
+ expect(extension.getAllActions().has('MyFunction')).toBe(true);
+ const action = extension.getAllActions().get('MyFunction');
+ expect(action.getHelpPath()).toBe('');
+
+ extension.delete();
+ project.delete();
+ });
+
it('can create metadata for free expressions', () => {
const extension = new gd.PlatformExtension();
const project = new gd.Project();
diff --git a/GDevelop.js/types.d.ts b/GDevelop.js/types.d.ts
index 4f40c764e896..d343540ff1f5 100644
--- a/GDevelop.js/types.d.ts
+++ b/GDevelop.js/types.d.ts
@@ -2277,6 +2277,8 @@ export class EventsFunction extends EmscriptenObject {
isPrivate(): boolean;
setAsync(isAsync: boolean): EventsFunction;
isAsync(): boolean;
+ setHelpUrl(helpUrl: string): EventsFunction;
+ getHelpUrl(): string;
isAction(): boolean;
isExpression(): boolean;
isCondition(): boolean;
diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/EventsFunctionConfigurationEditor/EventsFunctionPropertiesEditor.js b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsFunctionConfigurationEditor/EventsFunctionPropertiesEditor.js
index 3ebc98b31ba8..998a9d27bd47 100644
--- a/newIDE/app/src/EventsFunctionsExtensionEditor/EventsFunctionConfigurationEditor/EventsFunctionPropertiesEditor.js
+++ b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsFunctionConfigurationEditor/EventsFunctionPropertiesEditor.js
@@ -24,6 +24,10 @@ import useForceUpdate from '../../Utils/UseForceUpdate';
import Checkbox from '../../UI/Checkbox';
import { type ExtensionItemConfigurationAttribute } from '../../EventsFunctionsExtensionEditor';
import { useResponsiveWindowSize } from '../../UI/Responsive/ResponsiveWindowMeasurer';
+import {
+ isRelativePathToDocumentationRoot,
+ isDocumentationAbsoluteUrl,
+} from '../../Utils/HelpLink';
const gd: libGDevelop = global.gd;
@@ -446,6 +450,40 @@ export const EventsFunctionPropertiesEditor = ({
/>
)}
+
+ {(() => {
+ const helpUrl = eventsFunction.getHelpUrl();
+ const isValidHelpUrl =
+ !helpUrl ||
+ isDocumentationAbsoluteUrl(helpUrl) ||
+ isRelativePathToDocumentationRoot(helpUrl);
+ return (
+ Help page URL}
+ translatableHintText={t`Enter a URL to a help page for this action/condition/expression`}
+ helperMarkdownText={i18n._(
+ t`Optional. Enter a full URL (starting with https://) to a help page. A help icon will appear next to the action/condition/expression title in the editor, allowing users to quickly access documentation.`
+ )}
+ errorText={
+ !isValidHelpUrl ? (
+
+ This is not a URL starting with "http://" or
+ "https://".
+
+ ) : null
+ }
+ fullWidth
+ value={helpUrl}
+ onChange={text => {
+ eventsFunction.setHelpUrl(text);
+ if (onConfigurationUpdated) onConfigurationUpdated();
+ forceUpdate();
+ }}
+ />
+ );
+ })()}
+
{type === gd.EventsFunction.ActionWithOperator ? (
Help
) : isCondition ? (
diff --git a/newIDE/app/src/EventsSheet/InstructionEditor/InstructionParametersEditor.js b/newIDE/app/src/EventsSheet/InstructionEditor/InstructionParametersEditor.js
index 1ab2d124b092..a46bd606c2e8 100644
--- a/newIDE/app/src/EventsSheet/InstructionEditor/InstructionParametersEditor.js
+++ b/newIDE/app/src/EventsSheet/InstructionEditor/InstructionParametersEditor.js
@@ -10,6 +10,11 @@ import { mapFor } from '../../Utils/MapFor';
import EmptyMessage from '../../UI/EmptyMessage';
import ParameterRenderingService from '../ParameterRenderingService';
import HelpButton from '../../UI/HelpButton';
+import HelpIcon from '../../UI/HelpIcon';
+import {
+ isRelativePathToDocumentationRoot,
+ isDocumentationAbsoluteUrl,
+} from '../../Utils/HelpLink';
import { type ResourceManagementProps } from '../../ResourcesList/ResourceSource';
import { Column, Line, Spacer } from '../../UI/Grid';
import AlertMessage from '../../UI/AlertMessage';
@@ -306,9 +311,14 @@ const InstructionParametersEditor = React.forwardRef<
}}
/>
-
- {instructionMetadata.getDescription()}
-
+
+
+ {instructionMetadata.getDescription()}
+
+ {helpPage && isDocumentationAbsoluteUrl(helpPage) && (
+
+ )}
+
{instructionExtraInformation && (
@@ -464,18 +474,20 @@ const InstructionParametersEditor = React.forwardRef<
)}
- {!noHelpButton && helpPage && (
- Help for this condition
- ) : (
- Help for this action
- )
- }
- />
- )}
+ {!noHelpButton &&
+ helpPage &&
+ isRelativePathToDocumentationRoot(helpPage) && (
+ Help for this condition
+ ) : (
+ Help for this action
+ )
+ }
+ />
+ )}
diff --git a/newIDE/app/src/EventsSheet/InstructionEditor/SelectorListItems/SelectorInstructionOrExpressionListItem.js b/newIDE/app/src/EventsSheet/InstructionEditor/SelectorListItems/SelectorInstructionOrExpressionListItem.js
index 91cedf98c88f..28b809666d9c 100644
--- a/newIDE/app/src/EventsSheet/InstructionEditor/SelectorListItems/SelectorInstructionOrExpressionListItem.js
+++ b/newIDE/app/src/EventsSheet/InstructionEditor/SelectorListItems/SelectorInstructionOrExpressionListItem.js
@@ -2,10 +2,12 @@
import * as React from 'react';
import { ListItem } from '../../../UI/List';
import ListIcon from '../../../UI/ListIcon';
+import HelpIcon from '../../../UI/HelpIcon';
import { type EnumeratedInstructionOrExpressionMetadata } from '../../../InstructionOrExpression/EnumeratedInstructionOrExpressionMetadata';
import { getInstructionListItemValue, getInstructionListItemKey } from './Keys';
import { type SearchMatch } from '../../../UI/Search/UseSearchStructuredItem';
import HighlightedText from '../../../UI/Search/HighlightedText';
+import { isDocumentationAbsoluteUrl } from '../../../Utils/HelpLink';
type Props = {|
id?: string,
@@ -40,6 +42,9 @@ export const renderInstructionOrExpressionListItem = ({
return text;
};
+ const helpPath = instructionOrExpressionMetadata.metadata.getHelpPath();
+ const hasCustomHelpUrl = helpPath && isDocumentationAbsoluteUrl(helpPath);
+
return (
+ {getRenderedText('displayedName')}
+ {hasCustomHelpUrl && (
+
+ )}
+
+ }
secondaryText={getRenderedText('fullGroupName')}
leftIcon={
= {|
instructionTreeNode: InstructionOrExpressionTreeNode,
@@ -53,10 +55,21 @@ export const renderInstructionOrExpressionTree = <
const instructionMetadata: T = instructionOrGroup;
const value = getInstructionListItemValue(instructionOrGroup.type);
const selected = selectedValue === value;
+ const helpPath = instructionMetadata.metadata.getHelpPath();
+ const hasCustomHelpUrl = helpPath && isDocumentationAbsoluteUrl(helpPath);
return (
+ {instructionMetadata.displayedName}
+
+
+ ) : (
+ instructionMetadata.displayedName
+ )
+ }
selected={selected}
id={
// TODO: This id is used by in app tutorials. When in app tutorials
diff --git a/newIDE/app/src/UI/HelpIcon/index.js b/newIDE/app/src/UI/HelpIcon/index.js
index 488b6def52f0..8ba4ad4f7d1d 100644
--- a/newIDE/app/src/UI/HelpIcon/index.js
+++ b/newIDE/app/src/UI/HelpIcon/index.js
@@ -39,7 +39,10 @@ const HelpIcon = (props: PropsType) => {
return (
Window.openExternalURL(getHelpLink(helpPagePath))}
+ onClick={event => {
+ event.stopPropagation();
+ Window.openExternalURL(getHelpLink(helpPagePath));
+ }}
disabled={props.disabled}
style={props.style}
size={props.size}
diff --git a/newIDE/app/src/Utils/HelpLink.spec.js b/newIDE/app/src/Utils/HelpLink.spec.js
new file mode 100644
index 000000000000..7e583305a7db
--- /dev/null
+++ b/newIDE/app/src/Utils/HelpLink.spec.js
@@ -0,0 +1,76 @@
+// @flow
+import {
+ isRelativePathToDocumentationRoot,
+ isDocumentationAbsoluteUrl,
+ getHelpLink,
+} from './HelpLink';
+
+describe('HelpLink', () => {
+ describe('isRelativePathToDocumentationRoot', () => {
+ it('returns true for paths starting with /', () => {
+ expect(isRelativePathToDocumentationRoot('/test')).toBe(true);
+ expect(isRelativePathToDocumentationRoot('/all-features/audio')).toBe(
+ true
+ );
+ });
+
+ it('returns false for absolute URLs', () => {
+ expect(isRelativePathToDocumentationRoot('https://example.com')).toBe(
+ false
+ );
+ expect(isRelativePathToDocumentationRoot('http://example.com')).toBe(
+ false
+ );
+ });
+
+ it('returns false for empty string', () => {
+ expect(isRelativePathToDocumentationRoot('')).toBe(false);
+ });
+ });
+
+ describe('isDocumentationAbsoluteUrl', () => {
+ it('returns true for https URLs', () => {
+ expect(isDocumentationAbsoluteUrl('https://example.com/help')).toBe(true);
+ expect(isDocumentationAbsoluteUrl('https://wiki.gdevelop.io')).toBe(true);
+ });
+
+ it('returns true for http URLs', () => {
+ expect(isDocumentationAbsoluteUrl('http://example.com/help')).toBe(true);
+ });
+
+ it('returns false for relative paths', () => {
+ expect(isDocumentationAbsoluteUrl('/test')).toBe(false);
+ expect(isDocumentationAbsoluteUrl('/all-features/audio')).toBe(false);
+ });
+
+ it('returns false for empty string', () => {
+ expect(isDocumentationAbsoluteUrl('')).toBe(false);
+ });
+ });
+
+ describe('getHelpLink', () => {
+ it('returns wiki link for relative paths', () => {
+ expect(getHelpLink('/test')).toBe(
+ 'https://wiki.gdevelop.io/gdevelop5/test?utm_source=gdevelop&utm_medium=help-link'
+ );
+ });
+
+ it('returns wiki link with anchor for relative paths', () => {
+ expect(getHelpLink('/test', 'section1')).toBe(
+ 'https://wiki.gdevelop.io/gdevelop5/test?utm_source=gdevelop&utm_medium=help-link#section1'
+ );
+ });
+
+ it('returns the absolute URL as-is', () => {
+ expect(getHelpLink('https://example.com/custom-help')).toBe(
+ 'https://example.com/custom-help'
+ );
+ });
+
+ it('returns empty string for invalid paths', () => {
+ expect(getHelpLink('')).toBe('');
+ expect(getHelpLink('invalid')).toBe('');
+ });
+ });
+});
+
diff --git a/newIDE/app/src/stories/everything-else.stories.js b/newIDE/app/src/stories/everything-else.stories.js
index 7dda5c2577ce..57faba46229f 100644
--- a/newIDE/app/src/stories/everything-else.stories.js
+++ b/newIDE/app/src/stories/everything-else.stories.js
@@ -72,6 +72,7 @@ import InstructionEditorMenu from '../EventsSheet/InstructionEditor/InstructionE
import { PopoverButton } from './PopoverButton';
import MiniToolbar, { MiniToolbarText } from '../UI/MiniToolbar';
import { Column, Line } from '../UI/Grid';
+import { ListItem } from '../UI/List';
import DragAndDropTestBed from './DragAndDropTestBed';
import EditorMosaic from '../UI/EditorMosaic';
import FlatButton from '../UI/FlatButton';
@@ -914,7 +915,21 @@ storiesOf('UI Building Blocks/HelpButton', module)
storiesOf('UI Building Blocks/HelpIcon', module)
.addDecorator(paperDecorator)
- .add('default', () => );
+ .add('default (wiki path)', () => )
+ .add('absolute URL (custom help)', () => (
+
+ ))
+ .add('small size', () => )
+ .add('in ListItem context', () => (
+
+ My Custom Action
+
+
+ }
+ />
+ ));
storiesOf('PropertiesEditor', module)
.addDecorator(paperDecorator)