Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5.2] [Guided tours] Auto start tours - full functionality #3261

Closed
jgerman-bot opened this issue Aug 31, 2024 · 1 comment · Fixed by #3264
Closed

[5.2] [Guided tours] Auto start tours - full functionality #3261

jgerman-bot opened this issue Aug 31, 2024 · 1 comment · Fixed by #3264

Comments

@jgerman-bot
Copy link

New language relevant PR in upstream repo: joomla/joomla-cms#43814 Here are the upstream changes:

Click to expand the diff!
diff --git a/administrator/components/com_actionlogs/config.xml b/administrator/components/com_actionlogs/config.xml
index 93af8df4ded39..59c301dbecd62 100644
--- a/administrator/components/com_actionlogs/config.xml
+++ b/administrator/components/com_actionlogs/config.xml
@@ -30,7 +30,7 @@
 			label="COM_ACTIONLOGS_LOG_EXTENSIONS_LABEL"
 			multiple="true"
 			layout="joomla.form.field.list-fancy-select"
-			default="com_banners,com_cache,com_categories,com_checkin,com_config,com_contact,com_content,com_fields,com_installer,com_media,com_menus,com_messages,com_modules,com_newsfeeds,com_plugins,com_redirect,com_scheduler,com_tags,com_templates,com_users"
+			default="com_banners,com_cache,com_categories,com_checkin,com_config,com_contact,com_content,com_fields,com_guidedtours,com_installer,com_media,com_menus,com_messages,com_modules,com_newsfeeds,com_plugins,com_redirect,com_scheduler,com_tags,com_templates,com_users"
 		/>
 		<field
 			name="loggable_api"
diff --git a/administrator/components/com_admin/sql/updates/mysql/5.2.0-2024-07-19.sql b/administrator/components/com_admin/sql/updates/mysql/5.2.0-2024-07-19.sql
new file mode 100644
index 0000000000000..284938a777e2d
--- /dev/null
+++ b/administrator/components/com_admin/sql/updates/mysql/5.2.0-2024-07-19.sql
@@ -0,0 +1,7 @@
+--
+-- Add the Guided Tours selectable option to the User Action Logs
+--
+INSERT INTO `#__action_logs_extensions` (`extension`) VALUES ('com_guidedtours');
+
+INSERT INTO `#__action_log_config` (`type_title`, `type_alias`, `id_holder`, `title_holder`, `table_name`, `text_prefix`) VALUES
+('guidedtour', 'com_guidedtours.state', 'id', 'title', '#__guidedtours', 'PLG_ACTIONLOG_JOOMLA');
diff --git a/administrator/components/com_admin/sql/updates/postgresql/5.2.0-2024-07-19.sql b/administrator/components/com_admin/sql/updates/postgresql/5.2.0-2024-07-19.sql
new file mode 100644
index 0000000000000..479c6b1fc2729
--- /dev/null
+++ b/administrator/components/com_admin/sql/updates/postgresql/5.2.0-2024-07-19.sql
@@ -0,0 +1,7 @@
+--
+-- Add the Guided Tours selectable option to the User Action Logs
+--
+INSERT INTO "#__action_logs_extensions" ("extension") VALUES ("com_guidedtours");
+
+INSERT INTO "#__action_log_config" ("type_title", "type_alias", "id_holder", "title_holder", "table_name", "text_prefix") VALUES
+('guidedtour', 'com_guidedtours.state', 'id', 'title', '#__guidedtours', 'PLG_ACTIONLOG_JOOMLA');
diff --git a/administrator/components/com_guidedtours/config.xml b/administrator/components/com_guidedtours/config.xml
index b387dae127f66..2ff0681eaa56b 100644
--- a/administrator/components/com_guidedtours/config.xml
+++ b/administrator/components/com_guidedtours/config.xml
@@ -1,5 +1,28 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <config>
+	<fieldset name="guidedtours_config" label="COM_GUIDEDTOURS">
+		<field
+			name="allowTourAutoStart"
+			type="radio"
+			label="COM_GUIDEDTOURS_CONFIG_USER_ALLOWTOURAUTOSTART_LABEL"
+			description="COM_GUIDEDTOURS_CONFIG_USER_ALLOWTOURAUTOSTART_DESCRIPTION"
+			layout="joomla.form.field.radio.switcher"
+			default="1"
+			>
+			<option value="0">JNO</option>
+			<option value="1">JYES</option>
+		</field>
+
+		<field
+			name="delayed_time"
+			type="text"
+			label="COM_GUIDEDTOURS_CONFIG_DELAYED_TIME_LABEL"
+			description="COM_GUIDEDTOURS_CONFIG_DELAYED_TIME_DESCRIPTION"
+			default="60"
+			size="small"
+			showon="allowTourAutoStart:1"
+		/>
+	</fieldset>
 	<fieldset
 		name="permissions"
 		label="JCONFIG_PERMISSIONS_LABEL"
diff --git a/administrator/components/com_guidedtours/src/Controller/AjaxController.php b/administrator/components/com_guidedtours/src/Controller/AjaxController.php
new file mode 100644
index 0000000000000..031a4af78e3ed
--- /dev/null
+++ b/administrator/components/com_guidedtours/src/Controller/AjaxController.php
@@ -0,0 +1,124 @@
+<?php
+
+/**
+ * @package     Joomla.Administrator
+ * @subpackage  com_guidedtours
+ *
+ * @copyright   (C) 2024 Open Source Matters, Inc. <https://www.joomla.org>
+ * @license     GNU General Public License version 2 or later; see LICENSE.txt
+ */
+
+namespace Joomla\Component\Guidedtours\Administrator\Controller;
+
+use Joomla\CMS\Event\AbstractEvent;
+use Joomla\CMS\Language\Text;
+use Joomla\CMS\MVC\Controller\BaseController;
+use Joomla\CMS\Plugin\PluginHelper;
+use Joomla\CMS\Response\JsonResponse;
+
+// phpcs:disable PSR1.Files.SideEffects
+\defined('_JEXEC') or die;
+// phpcs:enable PSR1.Files.SideEffects
+
+/**
+ * The guided tours controller for ajax requests.
+ *
+ * @since  __DEPLOY_VERSION__
+ */
+class AjaxController extends BaseController
+{
+    /**
+     * Ajax call used when cancelling, skipping or completing a tour.
+     * It allows:
+     * - the trigering of before and after events the user state is recorded
+     * - the recording of the user behavior in the action logs
+     */
+    public function fetchUserState()
+    {
+        $user = $this->app->getIdentity();
+
+        $tourId     = $this->app->input->getInt('tid', 0);
+        $stepNumber = $this->app->input->getString('sid', '');
+        $context    = $this->app->input->getString('context', '');
+
+        if ($user != null && $user->id > 0) {
+            $actionState = '';
+
+            switch ($context) {
+                case 'tour.complete':
+                    $actionState = 'completed';
+                    break;
+                case 'tour.cancel':
+                    $actionState = 'delayed';
+                    break;
+                case 'tour.skip':
+                    $actionState = 'skipped';
+                    break;
+            }
+
+            PluginHelper::importPlugin('guidedtours');
+
+            // event onBeforeTourSaveUserState before save user tour state
+            $beforeEvent = AbstractEvent::create(
+                'onBeforeTourSaveUserState',
+                [
+                    'subject'     => new \stdClass(),
+                    'tourId'      => $tourId,
+                    'actionState' => $actionState,
+                    'stepNumber'  => $stepNumber,
+                ]
+            );
+
+            $this->app->getDispatcher()->dispatch('onBeforeTourSaveUserState', $beforeEvent);
+
+            // Save the tour state only when the tour auto-starts.
+            $tourModel = $this->getModel('Tour', 'Administrator');
+            if ($tourModel->isAutostart($tourId)) {
+                $result = $tourModel->saveTourUserState($tourId, $actionState);
+                if ($result) {
+                    $message = Text::sprintf('COM_GUIDEDTOURS_USERSTATE_STATESAVED', $user->id, $tourId);
+                } else {
+                    $message = Text::sprintf('COM_GUIDEDTOURS_USERSTATE_STATENOTSAVED', $user->id, $tourId);
+                }
+            } else {
+                $result  = false;
+                $message = Text::sprintf('COM_GUIDEDTOURS_USERSTATE_STATENOTSAVED', $user->id, $tourId);
+            }
+
+            // event onAfterTourSaveUserState after save user tour state (may override message)
+            $afterEvent = AbstractEvent::create(
+                'onAfterTourSaveUserState',
+                [
+                    'subject'     => new \stdClass(),
+                    'tourId'      => $tourId,
+                    'actionState' => $actionState,
+                    'stepNumber'  => $stepNumber,
+                    'result'      => $result,
+                    'message'     => &$message,
+                ]
+            );
+
+            $this->app->getDispatcher()->dispatch('onAfterTourSaveUserState', $afterEvent);
+
+            // Construct the response data
+            $data = [
+                'tourId'  => $tourId,
+                'stepId'  => $stepNumber,
+                'context' => $context,
+                'state'   => $actionState,
+            ];
+            echo new JsonResponse($data, $message);
+            $this->app->close();
+        } else {
+            // Construct the response data
+            $data = [
+                'success' => false,
+                'tourId'  => $tourId,
+            ];
+
+            $message = Text::_('COM_GUIDEDTOURS_USERSTATE_CONNECTEDONLY');
+            echo new JsonResponse($data, $message, true);
+            $this->app->close();
+        }
+    }
+}
diff --git a/administrator/components/com_guidedtours/src/Model/TourModel.php b/administrator/components/com_guidedtours/src/Model/TourModel.php
index edee94fc6bbbe..8cc0d50181c2d 100644
--- a/administrator/components/com_guidedtours/src/Model/TourModel.php
+++ b/administrator/components/com_guidedtours/src/Model/TourModel.php
@@ -10,6 +10,7 @@
 
 namespace Joomla\Component\Guidedtours\Administrator\Model;
 
+use Joomla\CMS\Date\Date;
 use Joomla\CMS\Factory;
 use Joomla\CMS\Language\Text;
 use Joomla\CMS\Log\Log;
@@ -530,20 +531,28 @@ public function setAutostart($id, $autostart)
     /**
      * Retrieve a tour's autostart value
      *
-     * @param   string  $uid  the uid of a tour
+     * @param   string  $pk  the id or uid of a tour
+     *
+     * @return  boolean
      *
      * @since  5.1.0
      */
-    public function isAutostart($uid)
+    public function isAutostart($pk): bool
     {
         $db = $this->getDatabase();
 
         $query = $db->getQuery(true)
             ->select($db->quoteName('autostart'))
             ->from($db->quoteName('#__guidedtours'))
-            ->where($db->quoteName('published') . ' = 1')
-            ->where($db->quoteName('uid') . ' = :uid')
-            ->bind(':uid', $uid, ParameterType::STRING);
+            ->where($db->quoteName('published') . ' = 1');
+
+        if (\is_integer($pk)) {
+            $query->where($db->quoteName('id') . ' = :id')
+                ->bind(':id', $pk, ParameterType::INTEGER);
+        } else {
+            $query->where($db->quoteName('uid') . ' = :uid')
+                ->bind(':uid', $pk, ParameterType::STRING);
+        }
 
         $db->setQuery($query);
 
@@ -558,4 +567,64 @@ public function isAutostart($uid)
 
         return $result;
     }
+
+    /**
+     * Save a tour state for a specific user.
+     *
+     * @param   int      $id       The id of the tour
+     * @param   string   $state    The label of the state to be saved (completed, delayed or skipped)
+     *
+     * @return  boolean
+     *
+     * @since  __DEPLOY_VERSION__
+     */
+    public function saveTourUserState($id, $state = ''): bool
+    {
+        $user = $this->getCurrentUser();
+        $db   = $this->getDatabase();
+
+        $profileKey = 'guidedtour.id.' . $id;
+
+        // Check if the profile key already exists.
+        $query = $db->getQuery(true)
+            ->select($db->quoteName('profile_value'))
+            ->from($db->quoteName('#__user_profiles'))
+            ->where($db->quoteName('user_id') . ' = :user_id')
+            ->where($db->quoteName('profile_key') . ' = :profileKey')
+            ->bind(':user_id', $user->id, ParameterType::INTEGER)
+            ->bind(':profileKey', $profileKey, ParameterType::STRING);
+
+        try {
+            $result = $db->setQuery($query)->loadResult();
+        } catch (\Exception $e) {
+            return false;
+        }
+
+        $tourState = [];
+
+        $tourState['state'] = $state;
+        if ($state === 'delayed') {
+            $tourState['time'] = Date::getInstance();
+        }
+
+        $profileObject = (object)[
+            'user_id'       => $user->id,
+            'profile_key'   => $profileKey,
+            'profile_value' => json_encode($tourState),
+            'ordering'      => 0,
+        ];
+
+        if (!\is_null($result)) {
+            $values = json_decode($result, true);
+
+            // The profile is updated only when delayed. 'Completed' and 'Skipped' are final
+            if (!empty($values) && $values['state'] === 'delayed') {
+                $db->updateObject('#__user_profiles', $profileObject, ['user_id', 'profile_key']);
+            }
+        } else {
+            $db->insertObject('#__user_profiles', $profileObject);
+        }
+
+        return true;
+    }
 }
diff --git a/administrator/components/com_users/forms/user.xml b/administrator/components/com_users/forms/user.xml
index 1946e3f3ca643..199662a2c8207 100644
--- a/administrator/components/com_users/forms/user.xml
+++ b/administrator/components/com_users/forms/user.xml
@@ -164,6 +164,18 @@
 				<option value="dark">COM_USERS_USER_COLORSCHEME_OPTION_DARK</option>
 			</field>
 
+			<field
+				name="allowTourAutoStart"
+				type="list"
+				label="COM_USERS_USER_ALLOWTOURAUTOSTART_LABEL"
+				default=""
+				validate="options"
+				>
+				<option value="">JOPTION_USE_DEFAULT</option>
+				<option value="0">JNO</option>
+				<option value="1">JYES</option>
+			</field>
+
 			<field
 				name="admin_language"
 				type="language"
diff --git a/administrator/language/en-GB/com_guidedtours.ini b/administrator/language/en-GB/com_guidedtours.ini
index e45fbb95000c9..627baef477610 100644
--- a/administrator/language/en-GB/com_guidedtours.ini
+++ b/administrator/language/en-GB/com_guidedtours.ini
@@ -6,6 +6,10 @@
 COM_GUIDEDTOURS="Guided Tours"
 COM_GUIDEDTOURS_AUTOSTART_DESC="Start the tour automatically when a user reaches the context in which the tour should be displayed."
 COM_GUIDEDTOURS_AUTOSTART_LABEL="Auto Start"
+COM_GUIDEDTOURS_CONFIG_DELAYED_TIME_DESCRIPTION="The amount of time (in minutes) a tour is delayed after being cancelled by the user and until it is shown again (only when a tour is set to start automatically).<br>For instance, enter 60 for 1 hour, 1440 for 24 hours, 10080 for 1 week."
+COM_GUIDEDTOURS_CONFIG_DELAYED_TIME_LABEL="Auto Start Time Delay (in minutes)"
+COM_GUIDEDTOURS_CONFIG_USER_ALLOWTOURAUTOSTART_DESCRIPTION="Turn on or off the auto starting functionality of tours."
+COM_GUIDEDTOURS_CONFIG_USER_ALLOWTOURAUTOSTART_LABEL="Allow Auto Starting Tours"
 COM_GUIDEDTOURS_CONFIGURATION="Guided Tours: Options"
 COM_GUIDEDTOURS_DESCRIPTION="Description"
 COM_GUIDEDTOURS_DESCRIPTION_TRANSLATION="Description (%s)"
@@ -90,4 +94,7 @@ COM_GUIDEDTOURS_TYPE_REDIRECT_URL_DESC="Enter the relative URL of the page you w
 COM_GUIDEDTOURS_TYPE_REDIRECT_URL_LABEL="Relative URL"
 COM_GUIDEDTOURS_URL_LABEL="Relative URL"
 COM_GUIDEDTOURS_URL_DESC="Enter the relative URL of the page from where you want to Start the tour, e.g administrator/index.php?option=com_guidedtours&view=tours for the tours' list page."
+COM_GUIDEDTOURS_USERSTATE_CONNECTEDONLY="Tour User state action is only for connected users."
+COM_GUIDEDTOURS_USERSTATE_STATENOTSAVED="Tour User state not saved for user %1$s tour %2$s."
+COM_GUIDEDTOURS_USERSTATE_STATESAVED="Tour User state saved for user %1$s tour %2$s."
 COM_GUIDEDTOURS_XML_DESCRIPTION="Component for managing Guided Tours functionality."
diff --git a/administrator/language/en-GB/com_users.ini b/administrator/language/en-GB/com_users.ini
index be813a51a60ca..4120dbfd6deaf 100644
--- a/administrator/language/en-GB/com_users.ini
+++ b/administrator/language/en-GB/com_users.ini
@@ -394,6 +394,7 @@ COM_USERS_USERS_N_ITEMS_DELETED="%d users deleted."
 COM_USERS_USERS_N_ITEMS_DELETED_1="User deleted."
 COM_USERS_USERS_TABLE_CAPTION="Users"
 COM_USERS_USER_ACCOUNT_DETAILS="Account Details"
+COM_USERS_USER_ALLOWTOURAUTOSTART_LABEL="Allow Auto Starting Tours"
 COM_USERS_USER_BACKUPCODE="Backup Code"
 COM_USERS_USER_BACKUPCODES="Backup Codes"
 COM_USERS_USER_BACKUPCODES_CAPTIVE_PROMPT="If you do not have access to your usual Multi-factor Authentication method use any of your Backup Codes in the field below. Please remember that this emergency backup code cannot be reused."
diff --git a/administrator/language/en-GB/plg_actionlog_joomla.ini b/administrator/language/en-GB/plg_actionlog_joomla.ini
index 408931eb795d5..69d026dbc27e8 100644
--- a/administrator/language/en-GB/plg_actionlog_joomla.ini
+++ b/administrator/language/en-GB/plg_actionlog_joomla.ini
@@ -63,3 +63,7 @@ PLG_ACTIONLOG_JOOMLA_EXTENSION_INSTALLED="User <a href='{accountlink}'>{username
 PLG_ACTIONLOG_JOOMLA_EXTENSION_UNINSTALLED="User <a href='{accountlink}'>{username}</a> uninstalled the {type} {extension_name}"
 PLG_ACTIONLOG_JOOMLA_EXTENSION_UPDATED="User <a href='{accountlink}'>{username}</a> updated the {type} {extension_name}"
 PLG_ACTIONLOG_JOOMLA_PLUGIN_INSTALLED="User <a href='{accountlink}'>{username}</a> installed the plugin <a href='index.php?option=com_plugins&task=plugin.edit&extension_id={id}'>{extension_name}</a>"
+; Guided Tours
+PLG_ACTIONLOG_JOOMLA_GUIDEDTOURS_TOURCOMPLETED="User <a href='{accountlink}'>{username}</a> completed the tour '{title}'"
+PLG_ACTIONLOG_JOOMLA_GUIDEDTOURS_TOURDELAYED="User <a href='{accountlink}'>{username}</a> delayed the tour '{title}'"
+PLG_ACTIONLOG_JOOMLA_GUIDEDTOURS_TOURSKIPPED="User <a href='{accountlink}'>{username}</a> skipped the tour '{title}'"
diff --git a/administrator/language/en-GB/plg_system_guidedtours.ini b/administrator/language/en-GB/plg_system_guidedtours.ini
index b812525479907..97ca225da8871 100644
--- a/administrator/language/en-GB/plg_system_guidedtours.ini
+++ b/administrator/language/en-GB/plg_system_guidedtours.ini
@@ -8,7 +8,10 @@ PLG_SYSTEM_GUIDEDTOURS_BACK="Back"
 PLG_SYSTEM_GUIDEDTOURS_COMPLETE="Complete"
 PLG_SYSTEM_GUIDEDTOURS_COULD_NOT_LOAD_THE_TOUR="Could not start the tour. No valid ID given."
 PLG_SYSTEM_GUIDEDTOURS_DESCRIPTION="System Guided Tour Plugin"
+PLG_SYSTEM_GUIDEDTOURS_HIDE_FOREVER="Hide Forever"
 PLG_SYSTEM_GUIDEDTOURS_NEXT="Next"
 PLG_SYSTEM_GUIDEDTOURS_START="Start"
 PLG_SYSTEM_GUIDEDTOURS_STEP_NUMBER_OF="Step {number} of {total}" ; Do not translate the text between the {}
 PLG_SYSTEM_GUIDEDTOURS_TOUR_ERROR="Oh no! It looks like your tour is off track. The tour exited and cannot run."
+PLG_SYSTEM_GUIDEDTOURS_TOUR_ERROR_RESPONSE="Something went wrong while saving tour state information! Response error."
+PLG_SYSTEM_GUIDEDTOURS_TOUR_INVALID_RESPONSE="Something went wrong while saving tour state information! Response is invalid."
diff --git a/build/media_source/plg_system_guidedtours/js/guidedtours.es6.js b/build/media_source/plg_system_guidedtours/js/guidedtours.es6.js
index 9cd2e75e3ed5d..9756d2344d03c 100644
--- a/build/media_source/plg_system_guidedtours/js/guidedtours.es6.js
+++ b/build/media_source/plg_system_guidedtours/js/guidedtours.es6.js
@@ -15,6 +15,71 @@ function emptyStorage() {
   sessionStorage.removeItem('tourId');
   sessionStorage.removeItem('tourToken');
   sessionStorage.removeItem('previousStepUrl');
+  sessionStorage.removeItem('skipTour');
+  sessionStorage.removeItem('autoTourId');
+}
+
+/**
+  Synchronize tour state for this user in their account/profile
+  tid = tour ID
+  sid = step number (the step the user is on)
+  state = state of the tour (completed, skipped, cancelled)
+*/
+function fetchTourState(tid, sid, context) {
+  const fetchUrl = 'index.php?option=com_guidedtours&task=ajax.fetchUserState&format=json';
+  Joomla.request({
+    url: `${fetchUrl}&tid=${tid}&sid=${sid}&context=${context}`,
+    method: 'GET',
+    perform: true,
+    onSuccess: (response) => {
+      try {
+        JSON.parse(response);
+      } catch (e) {
+        Joomla.renderMessages({ error: [Joomla.Text._('PLG_SYSTEM_GUIDEDTOURS_TOUR_INVALID_RESPONSE')] }, 'gt');
+        return false;
+      }
+      return true;
+    },
+    onError: () => {
+      Joomla.renderMessages({ error: [Joomla.Text._('PLG_SYSTEM_GUIDEDTOURS_TOUR_ERROR_RESPONSE')] });
+      return false;
+    },
+  });
+}
+
+/**
+ Stop tour on some specific context
+  - tour.complete
+  - tour.cancel
+  - tour.skip       Only autostart tours, to never display again
+*/
+function stopTour(tour, context) {
+  const tid = sessionStorage.getItem('tourId');
+  let sid = sessionStorage.getItem('currentStepId');
+
+  if (sid === 'tourinfo') {
+    sid = 1;
+  } else {
+    sid = Number(sid) + 1;
+  }
+
+  let trueContext = context;
+  if (context === 'tour.cancel' && sessionStorage.getItem('skipTour') === 'true') {
+    trueContext = 'tour.skip';
+  }
+
+  if (trueContext === 'tour.cancel' || trueContext === 'tour.skip' || trueContext === 'tour.complete') {
+    // ajax call to set the user state
+    fetchTourState(tid, sid, trueContext);
+
+    // close the tour
+    emptyStorage();
+    tour.steps = [];
+
+    return true; // cf. https://docs.shepherdpro.com/api/tour/classes/tour/#cancel
+  }
+
+  return false; // wrong context
 }
 
 function getTourInstance() {
@@ -35,9 +100,10 @@ function getTourInstance() {
   });
 
   tour.on('cancel', () => {
-    emptyStorage();
-
-    tour.steps = [];
+    // Test that a tour exists still, it may have already been emptied when skipping the tour
+    if (sessionStorage.getItem('tourId')) {
+      stopTour(tour, 'tour.cancel');
+    }
   });
 
   return tour;
@@ -70,6 +136,7 @@ function enableButton(eventElement) {
   element.removeAttribute('disabled');
   element.classList.remove('disabled');
 }
+
 function disableButton(eventElement) {
   const element = eventElement instanceof Event ? document.querySelector(`.step-next-button-${eventElement.currentTarget.step_id}`) : eventElement;
   element.setAttribute('disabled', 'disabled');
@@ -297,36 +364,57 @@ function addStepToTourButton(tour, stepObj, buttons) {
   tour.addStep(step);
 }
 
+function addStartButton(tour, buttons, label) {
+  buttons.push({
+    text: label,
+    classes: 'btn btn-primary shepherd-button-primary',
+    action() {
+      return this.next();
+    },
+  });
+}
+
+function addSkipButton(tour, buttons) {
+  buttons.push({
+    text: Joomla.Text._('PLG_SYSTEM_GUIDEDTOURS_HIDE_FOREVER'),
+    classes: 'btn btn-secondary shepherd-button-secondary',
+    action() {
+      sessionStorage.setItem('skipTour', 'true');
+      return this.cancel();
+    },
+  });
+}
+
 function showTourInfo(tour, stepObj) {
+  const buttons = [];
+  if (sessionStorage.getItem('autoTourId') === sessionStorage.getItem('tourId')) {
+    addSkipButton(tour, buttons);
+  }
+  addStartButton(tour, buttons, stepObj.start_label);
+
   tour.addStep({
     title: stepObj.title,
     text: stepObj.description,
     classes: 'shepherd-theme-arrows',
-    buttons: [
-      {
-        classes: 'btn btn-primary shepherd-button-primary',
-        action() {
-          return this.next();
-        },
-        text: Joomla.Text._('PLG_SYSTEM_GUIDEDTOURS_START'),
-      },
-    ],
+    buttons,
     id: 'tourinfo',
     when: {
       show() {
         sessionStorage.setItem('currentStepId', 'tourinfo');
+        sessionStorage.setItem('skipTour', 'false');
         addProgressIndicator(this.getElement(), 1, sessionStorage.getItem('stepCount'));
       },
     },
   });
 }
 
-function pushCompleteButton(buttons) {
+function pushCompleteButton(tour, buttons) {
   buttons.push({
     text: Joomla.Text._('PLG_SYSTEM_GUIDEDTOURS_COMPLETE'),
     classes: 'btn btn-primary shepherd-button-primary',
     action() {
-      return this.cancel();
+      stopTour(tour, 'tour.complete');
+      return this.complete();
     },
   });
 }
@@ -529,7 +617,7 @@ function startTour(obj) {
         pushNextButton(buttons, obj.steps[index]);
       }
     } else {
-      pushCompleteButton(buttons);
+      pushCompleteButton(obj, buttons);
     }
 
     addStepToTourButton(tour, obj.steps[index], buttons);
@@ -576,23 +664,23 @@ function loadTour(tourId) {
 }
 
 // Opt-in Start buttons
-document.querySelector('body').addEventListener('click', ({ target }) => {
+document.querySelector('body').addEventListener('click', (event) => {
   // Click somewhere else
-  if (!target || !target.classList.contains('button-start-guidedtour')) {
+  if (!event.target || !event.target.classList.contains('button-start-guidedtour')) {
     return;
   }
 
   // Click button but missing data-id
   if (
-    (!target.hasAttribute('data-id') || target.getAttribute('data-id') <= 0)
-  && (!target.hasAttribute('data-gt-uid') || target.getAttribute('data-gt-uid') === '')
+    (!event.target.hasAttribute('data-id') || event.target.getAttribute('data-id') <= 0)
+  && (!event.target.hasAttribute('data-gt-uid') || event.target.getAttribute('data-gt-uid') === '')
   ) {
     Joomla.renderMessages({ error: [Joomla.Text._('PLG_SYSTEM_GUIDEDTOURS_COULD_NOT_LOAD_THE_TOUR')] });
     return;
   }
 
   sessionStorage.setItem('tourToken', String(Joomla.getOptions('com_guidedtours.token')));
-  loadTour(target.getAttribute('data-id') || target.getAttribute('data-gt-uid'));
+  loadTour(event.target.getAttribute('data-id') || event.target.getAttribute('data-gt-uid'));
 });
 
 // Start a given tour
@@ -601,6 +689,7 @@ let tourId = sessionStorage.getItem('tourId');
 // Autostart tours have priority
 if (Joomla.getOptions('com_guidedtours.autotour', '') !== '') {
   sessionStorage.setItem('tourToken', String(Joomla.getOptions('com_guidedtours.token')));
+  sessionStorage.setItem('autoTourId', String(Joomla.getOptions('com_guidedtours.autotour')));
   tourId = Joomla.getOptions('com_guidedtours.autotour');
 }
 
diff --git a/components/com_users/forms/frontend.xml b/components/com_users/forms/frontend.xml
index 95a4ec7d3aff1..61bc7ac9969b6 100644
--- a/components/com_users/forms/frontend.xml
+++ b/components/com_users/forms/frontend.xml
@@ -44,6 +44,18 @@
 				<option value="light">COM_USERS_USER_COLORSCHEME_OPTION_LIGHT</option>
 				<option value="dark">COM_USERS_USER_COLORSCHEME_OPTION_DARK</option>
 			</field>
+
+			<field
+				name="allowTourAutoStart"
+				type="list"
+				label="COM_USERS_USER_ALLOWTOURAUTOSTART_LABEL"
+				default=""
+				validate="options"
+				>
+				<option value="">JOPTION_USE_DEFAULT</option>
+				<option value="0">JNO</option>
+				<option value="1">JYES</option>
+			</field>
 		</fieldset>
 	</fields>
 </form>
diff --git a/installation/sql/mysql/base.sql b/installation/sql/mysql/base.sql
index 15e323420cae8..6301dfdc950c9 100644
--- a/installation/sql/mysql/base.sql
+++ b/installation/sql/mysql/base.sql
@@ -188,7 +188,7 @@ INSERT INTO `#__extensions` (`package_id`, `name`, `type`, `element`, `folder`,
 (0, 'com_fields', 'component', 'com_fields', '', 1, 1, 1, 0, 1, '', '', ''),
 (0, 'com_associations', 'component', 'com_associations', '', 1, 1, 1, 0, 1, '', '', ''),
 (0, 'com_privacy', 'component', 'com_privacy', '', 1, 1, 1, 0, 1, '', '', ''),
-(0, 'com_actionlogs', 'component', 'com_actionlogs', '', 1, 1, 1, 0, 1, '', '{"ip_logging":0,"csv_delimiter":",","loggable_extensions":["com_banners","com_cache","com_categories","com_checkin","com_config","com_contact","com_content","com_fields","com_installer","com_media","com_menus","com_messages","com_modules","com_newsfeeds","com_plugins","com_redirect","com_scheduler","com_tags","com_templates","com_users"]}', ''),
+(0, 'com_actionlogs', 'component', 'com_actionlogs', '', 1, 1, 1, 0, 1, '', '{"ip_logging":0,"csv_delimiter":",","loggable_extensions":["com_banners","com_cache","com_categories","com_checkin","com_config","com_contact","com_content","com_fields","com_guidedtours","com_installer","com_media","com_menus","com_messages","com_modules","com_newsfeeds","com_plugins","com_redirect","com_scheduler","com_tags","com_templates","com_users"]}', ''),
 (0, 'com_workflow', 'component', 'com_workflow', '', 1, 1, 0, 1, 1, '', '{}', ''),
 (0, 'com_mails', 'component', 'com_mails', '', 1, 1, 1, 1, 1, '', '', ''),
 (0, 'com_scheduler', 'component', 'com_scheduler', '', 1, 1, 1, 0, 1, '', '{}', ''),
diff --git a/installation/sql/mysql/extensions.sql b/installation/sql/mysql/extensions.sql
index b1a8b198cedbf..015ff1a1d79c2 100644
--- a/installation/sql/mysql/extensions.sql
+++ b/installation/sql/mysql/extensions.sql
@@ -831,7 +831,8 @@ INSERT INTO `#__action_logs_extensions` (`id`, `extension`) VALUES
 (17, 'com_users'),
 (18, 'com_checkin'),
 (19, 'com_scheduler'),
-(20, 'com_fields');
+(20, 'com_fields'),
+(21, 'com_guidedtours');
 
 -- --------------------------------------------------------
 
@@ -871,7 +872,8 @@ INSERT INTO `#__action_log_config` (`id`, `type_title`, `type_alias`, `id_holder
 (18, 'banner_client', 'com_banners.client', 'id', 'name', '#__banner_clients', 'PLG_ACTIONLOG_JOOMLA'),
 (19, 'application_config', 'com_config.application', '', 'name', '', 'PLG_ACTIONLOG_JOOMLA'),
 (20, 'task', 'com_scheduler.task', 'id', 'title', '#__scheduler_tasks', 'PLG_ACTIONLOG_JOOMLA'),
-(21, 'field', 'com_fields.field', 'id', 'title', '#__fields', 'PLG_ACTIONLOG_JOOMLA');
+(21, 'field', 'com_fields.field', 'id', 'title', '#__fields', 'PLG_ACTIONLOG_JOOMLA'),
+(22, 'guidedtour', 'com_guidedtours.state', 'id', 'title', '#__guidedtours', 'PLG_ACTIONLOG_JOOMLA');
 
 
 -- --------------------------------------------------------
diff --git a/installation/sql/postgresql/base.sql b/installation/sql/postgresql/base.sql
index f0f97b6150acb..9ade1f50e49a8 100644
--- a/installation/sql/postgresql/base.sql
+++ b/installation/sql/postgresql/base.sql
@@ -194,7 +194,7 @@ INSERT INTO "#__extensions" ("package_id", "name", "type", "element", "folder",
 (0, 'com_fields', 'component', 'com_fields', '', 1, 1, 1, 0, 1, '', '', '', 0, 0),
 (0, 'com_associations', 'component', 'com_associations', '', 1, 1, 1, 0, 1, '', '', '', 0, 0),
 (0, 'com_privacy', 'component', 'com_privacy', '', 1, 1, 1, 0, 1, '', '', '', 0, 0),
-(0, 'com_actionlogs', 'component', 'com_actionlogs', '', 1, 1, 1, 0, 1, '', '{"ip_logging":0,"csv_delimiter":",","loggable_extensions":["com_banners","com_cache","com_categories","com_checkin","com_config","com_contact","com_content","com_fields","com_installer","com_media","com_menus","com_messages","com_modules","com_newsfeeds","com_plugins","com_redirect","com_scheduler","com_tags","com_templates","com_users"]}', '', 0, 0),
+(0, 'com_actionlogs', 'component', 'com_actionlogs', '', 1, 1, 1, 0, 1, '', '{"ip_logging":0,"csv_delimiter":",","loggable_extensions":["com_banners","com_cache","com_categories","com_checkin","com_config","com_contact","com_content","com_fields","com_guidedtours","com_installer","com_media","com_menus","com_messages","com_modules","com_newsfeeds","com_plugins","com_redirect","com_scheduler","com_tags","com_templates","com_users"]}', '', 0, 0),
 (0, 'com_workflow', 'component', 'com_workflow', '', 1, 1, 0, 1, 1, '', '{}', '', 0, 0),
 (0, 'com_mails', 'component', 'com_mails', '', 1, 1, 1, 1, 1, '', '', '', 0, 0),
 (0, 'com_scheduler', 'component', 'com_scheduler', '', 1, 1, 1, 0, 1, '', '{}', '', 0, 0),
diff --git a/installation/sql/postgresql/extensions.sql b/installation/sql/postgresql/extensions.sql
index 5b0bda3f1bc49..af15b88b5cd0d 100644
--- a/installation/sql/postgresql/extensions.sql
+++ b/installation/sql/postgresql/extensions.sql
@@ -789,9 +789,10 @@ INSERT INTO "#__action_logs_extensions" ("id", "extension") VALUES
 (17, 'com_users'),
 (18, 'com_checkin'),
 (19, 'com_scheduler'),
-(20, 'com_fields');
+(20, 'com_fields'),
+(21, 'com_guidedtours');
 
-SELECT setval('#__action_logs_extensions_id_seq', 21, false);
+SELECT setval('#__action_logs_extensions_id_seq', 22, false);
 -- --------------------------------------------------------
 
 --
@@ -832,10 +833,11 @@ INSERT INTO "#__action_log_config" ("id", "type_title", "type_alias", "id_holder
 (18, 'banner_client', 'com_banners.client', 'id', 'name', '#__banner_clients', 'PLG_ACTIONLOG_JOOMLA'),
 (19, 'application_config', 'com_config.application', '', 'name', '', 'PLG_ACTIONLOG_JOOMLA'),
 (20, 'task', 'com_scheduler.task', 'id', 'title', '#__scheduler_tasks', 'PLG_ACTIONLOG_JOOMLA'),
-(21, 'field', 'com_fields.field', 'id', 'title', '#__fields', 'PLG_ACTIONLOG_JOOMLA');
+(21, 'field', 'com_fields.field', 'id', 'title', '#__fields', 'PLG_ACTIONLOG_JOOMLA'),
+(22, 'guidedtour', 'com_guidedtours.state', 'id', 'title', '#__guidedtours', 'PLG_ACTIONLOG_JOOMLA');
 
 
-SELECT setval('#__action_log_config_id_seq', 22, false);
+SELECT setval('#__action_log_config_id_seq', 23, false);
 
 --
 -- Table structure for table `#__action_logs_users`
diff --git a/language/en-GB/com_users.ini b/language/en-GB/com_users.ini
index b14aaf81e4f45..050ef805a6777 100644
--- a/language/en-GB/com_users.ini
+++ b/language/en-GB/com_users.ini
@@ -140,6 +140,7 @@ COM_USERS_RESET_REQUEST_ERROR="Error requesting password reset."
 COM_USERS_RESET_REQUEST_FAILED="Reset password failed: %s"
 COM_USERS_RESET_REQUEST_LABEL="Please enter the email address for your account. A verification code will be sent to you. Once you have received the verification code, you will be able to choose a new password for your account."
 COM_USERS_SETTINGS_FIELDSET_LABEL="Basic Settings"
+COM_USERS_USER_ALLOWTOURAUTOSTART_LABEL="Allow Auto Starting Tours"
 COM_USERS_USER_BACKUPCODE="Backup Code"
 COM_USERS_USER_BACKUPCODES="Backup Codes"
 COM_USERS_USER_BACKUPCODES_CAPTIVE_PROMPT="If you do not have access to your usual Multi-factor Authentication method use any of your Backup Codes in the field below. Please remember that this emergency backup code cannot be reused."
diff --git a/plugins/actionlog/joomla/src/Extension/Joomla.php b/plugins/actionlog/joomla/src/Extension/Joomla.php
index 360cab89df5aa..4c407485209e3 100644
--- a/plugins/actionlog/joomla/src/Extension/Joomla.php
+++ b/plugins/actionlog/joomla/src/Extension/Joomla.php
@@ -11,6 +11,7 @@
 namespace Joomla\Plugin\Actionlog\Joomla\Extension;
 
 use Joomla\CMS\Component\ComponentHelper;
+use Joomla\CMS\Event\AbstractEvent;
 use Joomla\CMS\Event\Application;
 use Joomla\CMS\Event\Cache;
 use Joomla\CMS\Event\Checkin;
@@ -131,6 +132,7 @@ public static function getSubscribedEvents(): array
             'onUserAfterResetRequest'   => 'onUserAfterResetRequest',
             'onUserAfterResetComplete'  => 'onUserAfterResetComplete',
             'onUserBeforeSave'          => 'onUserBeforeSave',
+            'onBeforeTourSaveUserState' => 'onBeforeTourSaveUserState',
         ];
     }
 
@@ -1301,4 +1303,56 @@ public function onUserBeforeSave(User\BeforeSaveEvent $event): void
             $session->set('block', $blockunblock);
         }
     }
+
+    /**
+     * Method is called when a user cancels, completes or skips a tour
+     *
+     * @param   AbstractEvent $event The event instance.
+     *
+     * @return  void
+     *
+     * @since  __DEPLOY_VERSION__
+     */
+    public function onBeforeTourSaveUserState(AbstractEvent $event): void
+    {
+        $option = $this->getApplication()->getInput()->get('option');
+
+        if (!$this->checkLoggable($option)) {
+            return;
+        }
+
+        $tourId     = $event->getArgument('tourId');
+        $state      = $event->getArgument('actionState');
+        $stepNumber = $event->getArgument('stepNumber');
+
+        switch ($state) {
+            case 'skipped':
+                $messageLanguageKey = 'PLG_ACTIONLOG_JOOMLA_GUIDEDTOURS_TOURSKIPPED';
+                break;
+            case 'completed':
+                $messageLanguageKey = 'PLG_ACTIONLOG_JOOMLA_GUIDEDTOURS_TOURCOMPLETED';
+                break;
+            default:
+                $messageLanguageKey = 'PLG_ACTIONLOG_JOOMLA_GUIDEDTOURS_TOURDELAYED';
+        }
+
+        // Get the tour from the model to fetch the translated title of the tour
+        $factory   = $this->getApplication()->bootComponent('com_guidedtours')->getMVCFactory();
+        $tourModel = $factory->createModel(
+            'Tour',
+            'Administrator',
+            ['ignore_request' => true]
+        );
+
+        $tour = $tourModel->getItem($tourId);
+
+        $message = [
+            'id'    => $tourId,
+            'title' => $tour->title_translation,
+            'state' => $state,
+            'step'  => $stepNumber,
+        ];
+
+        $this->addLog([$message], $messageLanguageKey, 'com_guidedtours.state');
+    }
 }
diff --git a/plugins/system/guidedtours/services/provider.php b/plugins/system/guidedtours/services/provider.php
index 8e30afd6b0b47..d372a3a0c2de9 100644
--- a/plugins/system/guidedtours/services/provider.php
+++ b/plugins/system/guidedtours/services/provider.php
@@ -14,6 +14,7 @@
 use Joomla\CMS\Factory;
 use Joomla\CMS\Plugin\PluginHelper;
 use Joomla\CMS\WebAsset\WebAssetRegistry;
+use Joomla\Database\DatabaseInterface;
 use Joomla\DI\Container;
 use Joomla\DI\ServiceProviderInterface;
 use Joomla\Event\DispatcherInterface;
@@ -43,6 +44,7 @@ function (Container $container) {
                 );
 
                 $plugin->setApplication($app);
+                $plugin->setDatabase($container->get(DatabaseInterface::class));
 
                 $wa = $container->get(WebAssetRegistry::class);
 
diff --git a/plugins/system/guidedtours/src/Extension/GuidedTours.php b/plugins/system/guidedtours/src/Extension/GuidedTours.php
index 4bcc765ad39fc..76844f7d61811 100644
--- a/plugins/system/guidedtours/src/Extension/GuidedTours.php
+++ b/plugins/system/guidedtours/src/Extension/GuidedTours.php
@@ -10,12 +10,17 @@
 
 namespace Joomla\Plugin\System\GuidedTours\Extension;
 
+use Joomla\CMS\Component\ComponentHelper;
+use Joomla\CMS\Date\Date;
+use Joomla\CMS\Language\Multilanguage;
 use Joomla\CMS\Language\Text;
 use Joomla\CMS\Object\CMSObject;
 use Joomla\CMS\Plugin\CMSPlugin;
 use Joomla\CMS\Session\Session;
 use Joomla\Component\Guidedtours\Administrator\Extension\GuidedtoursComponent;
 use Joomla\Component\Guidedtours\Administrator\Model\TourModel;
+use Joomla\Database\DatabaseAwareTrait;
+use Joomla\Database\ParameterType;
 use Joomla\Event\DispatcherInterface;
 use Joomla\Event\Event;
 use Joomla\Event\SubscriberInterface;
@@ -31,6 +36,8 @@
  */
 final class GuidedTours extends CMSPlugin implements SubscriberInterface
 {
+    use DatabaseAwareTrait;
+
     /**
      * A mapping for the step types
      *
@@ -141,41 +148,106 @@ public function onBeforeCompileHead()
         $user = $app->getIdentity();
 
         if ($user != null && $user->id > 0) {
-            // Load plugin language files
+            // Load plugin language files.
             $this->loadLanguage();
 
             Text::script('JCANCEL');
             Text::script('PLG_SYSTEM_GUIDEDTOURS_BACK');
             Text::script('PLG_SYSTEM_GUIDEDTOURS_COMPLETE');
             Text::script('PLG_SYSTEM_GUIDEDTOURS_COULD_NOT_LOAD_THE_TOUR');
+            Text::script('PLG_SYSTEM_GUIDEDTOURS_HIDE_FOREVER');
             Text::script('PLG_SYSTEM_GUIDEDTOURS_NEXT');
-            Text::script('PLG_SYSTEM_GUIDEDTOURS_START');
             Text::script('PLG_SYSTEM_GUIDEDTOURS_STEP_NUMBER_OF');
             Text::script('PLG_SYSTEM_GUIDEDTOURS_TOUR_ERROR');
+            Text::script('PLG_SYSTEM_GUIDEDTOURS_TOUR_ERROR_RESPONSE');
+            Text::script('PLG_SYSTEM_GUIDEDTOURS_TOUR_INVALID_RESPONSE');
 
             $doc->addScriptOptions('com_guidedtours.token', Session::getFormToken());
+            $doc->addScriptOptions('com_guidedtours.autotour', '');
 
-            // Load required assets
+            // Load required assets.
             $doc->getWebAssetManager()
                 ->usePreset('plg_system_guidedtours.guidedtours');
 
-            // Temporary solution to auto-start the welcome tour
-            if ($app->getInput()->getCmd('option', 'com_cpanel') === 'com_cpanel') {
-                $factory = $app->bootComponent('com_guidedtours')->getMVCFactory();
+            $params = ComponentHelper::getParams('com_guidedtours');
 
-                $tourModel = $factory->createModel(
-                    'Tour',
-                    'Administrator',
-                    ['ignore_request' => true]
-                );
+            // Check if the user has opted out of auto-start
+            $userAuthorizedAutostart = $user->getParam('allowTourAutoStart', $params->get('allowTourAutoStart', 1));
+            if (!$userAuthorizedAutostart) {
+                return;
+            }
 
-                if ($tourModel->isAutostart('joomla-welcome')) {
-                    $tour = $this->getTour('joomla-welcome');
+            // The following code only relates to the auto-start functionality.
+            // First, we get the tours for the context.
 
-                    $doc->addScriptOptions('com_guidedtours.autotour', $tour->id);
+            $factory = $app->bootComponent('com_guidedtours')->getMVCFactory();
+
+            $toursModel = $factory->createModel(
+                'Tours',
+                'Administrator',
+                ['ignore_request' => true]
+            );
+
+            $toursModel->setState('filter.extension', $app->getInput()->getCmd('option', 'com_cpanel'));
+            $toursModel->setState('filter.published', 1);
+            $toursModel->setState('filter.access', $user->getAuthorisedViewLevels());
 
-                    // Set autostart to '0' to avoid it to autostart again
-                    $tourModel->setAutostart($tour->id, 0);
+            if (Multilanguage::isEnabled()) {
+                $toursModel->setState('filter.language', ['*', $app->getLanguage()->getTag()]);
+            }
+
+            $tours = $toursModel->getItems();
+            foreach ($tours as $tour) {
+                // Look for the first autostart tour, if any.
+                if ($tour->autostart) {
+                    $db         = $this->getDatabase();
+                    $profileKey = 'guidedtour.id.' . $tour->id;
+
+                    // Check if the tour state has already been saved some time before.
+                    $query = $db->getQuery(true)
+                        ->select($db->quoteName('profile_value'))
+                        ->from($db->quoteName('#__user_profiles'))
+                        ->where($db->quoteName('user_id') . ' = :user_id')
+                        ->where($db->quoteName('profile_key') . ' = :profileKey')
+                        ->bind(':user_id', $user->id, ParameterType::INTEGER)
+                        ->bind(':profileKey', $profileKey, ParameterType::STRING);
+
+                    try {
+                        $result = $db->setQuery($query)->loadResult();
+                    } catch (\Exception $e) {
+                        // Do not start the tour.
+                        continue;
+                    }
+
+                    // A result has been found in the user profiles table
+                    if (!\is_null($result)) {
+                        $values = json_decode($result, true);
+
+                        if (empty($values)) {
+                            // Do not start the tour.
+                            continue;
+                        }
+
+                        if ($values['state'] === 'skipped' || $values['state'] === 'completed') {
+                            // Do not start the tour.
+                            continue;
+                        }
+
+                        if ($values['state'] === 'delayed') {
+                            $delay       = $params->get('delayed_time', '60');
+                            $currentTime = Date::getInstance();
+                            $loggedTime  = new Date($values['time']['date']);
+
+                            if ($loggedTime->add(new \DateInterval('PT' . $delay . 'M')) > $currentTime) {
+                                // Do not start the tour.
+                                continue;
+                            }
+                        }
+                    }
+
+                    // We have a tour to auto start. No need to go any further.
+                    $doc->addScriptOptions('com_guidedtours.autotour', $tour->id);
+                    break;
                 }
             }
         }
@@ -259,6 +331,9 @@ private function processTour($item)
         // Replace 'images/' to '../images/' when using an image from /images in backend.
         $temp->description = preg_replace('*src\=\"(?!administrator\/)images/*', 'src="../images/', $temp->description);
 
+        // Set the start label for the tour.
+        $temp->start_label = Text::_('PLG_SYSTEM_GUIDEDTOURS_START');
+
         $tour->steps[] = $temp;
 
         foreach ($steps as $i => $step) {
@zero-24
Copy link
Member

zero-24 commented Aug 31, 2024

PR #3264

@zero-24 zero-24 closed this as completed Aug 31, 2024
zero-24 added a commit that referenced this issue Aug 31, 2024
* add new strings

* fix #3261

translation

* Update administrator/language/de-DE/plg_system_guidedtours.ini

---------

Co-authored-by: Tobias Zulauf <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging a pull request may close this issue.

4 participants