@@ -37,6 +37,8 @@ class NavigationManager implements INavigationManager {
3737 protected bool $ init = false ;
3838 /** User defined app order (cached for the `add` function) */
3939 private ?array $ customAppOrder = null ;
40+ /** List of loaded app info */
41+ private array $ loadedAppInfo = [];
4042
4143 public function __construct (
4244 protected IAppManager $ appManager ,
@@ -56,7 +58,7 @@ public function add(array|callable $entry): void {
5658 $ this ->closureEntries [] = $ entry ;
5759 return ;
5860 }
59- $ this ->init (false );
61+ $ this ->init ();
6062
6163 $ id = $ entry ['id ' ];
6264
@@ -99,7 +101,7 @@ private function updateDefaultEntries(): void {
99101
100102 #[Override]
101103 public function getAll (string $ type = 'link ' ): array {
102- $ this ->init ();
104+ $ this ->resolveAppNavigationEntries ();
103105
104106 $ result = $ this ->entries ;
105107 if ($ type !== 'all ' ) {
@@ -180,7 +182,16 @@ public function getActiveEntry(): ?string {
180182 return $ this ->activeEntry ;
181183 }
182184
183- private function init (bool $ resolveClosures = true ): void {
185+ /**
186+ * Initialize the internal state.
187+ * This loads the default app mapping and user mapping for app ordering.
188+ */
189+ private function init (): void {
190+ if ($ this ->init ) {
191+ return ;
192+ }
193+ $ this ->init = true ;
194+
184195 if ($ this ->customAppOrder === null ) {
185196 if ($ this ->userSession ->isLoggedIn ()) {
186197 $ user = $ this ->userSession ->getUser ();
@@ -189,21 +200,23 @@ private function init(bool $resolveClosures = true): void {
189200 $ this ->customAppOrder = [];
190201 }
191202 }
203+ }
192204
193- if ($ resolveClosures ) {
194- while ($ c = array_pop ($ this ->closureEntries )) {
195- $ this ->add ($ c ());
196- }
205+ /**
206+ * Resolve the app navigation entries from closures and info.xml files.
207+ */
208+ private function resolveAppNavigationEntries (): void {
209+ // Resolve app navigation closures
210+ while ($ c = array_pop ($ this ->closureEntries )) {
211+ $ this ->add ($ c ());
197212 }
198213
199- if ($ this ->init ) {
200- return ;
214+ // Resolve dynamically added navigation entries via event listeners
215+ if ($ this ->loadedAppInfo === []) {
216+ $ this ->eventDispatcher ->dispatchTyped (new LoadAdditionalEntriesEvent ());
201217 }
202- $ this ->init = true ;
203-
204- $ l = $ this ->l10nFac ->get ('lib ' );
205- $ this ->eventDispatcher ->dispatchTyped (new LoadAdditionalEntriesEvent ());
206218
219+ // Resolve classic info.xml based navigation entries
207220 if ($ this ->userSession ->isLoggedIn ()) {
208221 $ user = $ this ->userSession ->getUser ();
209222 $ apps = $ this ->appManager ->getEnabledAppsForUser ($ user );
@@ -212,6 +225,11 @@ private function init(bool $resolveClosures = true): void {
212225 }
213226
214227 foreach ($ apps as $ app ) {
228+ // skip already loaded apps
229+ if (in_array ($ app , $ this ->loadedAppInfo )) {
230+ continue ;
231+ }
232+
215233 // load plugins and collections from info.xml
216234 $ info = $ this ->appManager ->getAppInfo ($ app );
217235 if (!isset ($ info ['navigations ' ]['navigation ' ])) {
@@ -230,7 +248,6 @@ private function init(bool $resolveClosures = true): void {
230248 if ($ role === 'admin ' && !$ this ->isAdmin ()) {
231249 continue ;
232250 }
233- $ l = $ this ->l10nFac ->get ($ app );
234251 $ id = $ nav ['id ' ] ?? $ app . ($ key === 0 ? '' : $ key );
235252 $ order = $ nav ['order ' ] ?? 100 ;
236253 $ type = $ nav ['type ' ];
@@ -249,7 +266,14 @@ private function init(bool $resolveClosures = true): void {
249266 if ($ icon === null ) {
250267 $ icon = $ this ->urlGenerator ->imagePath ('core ' , 'places/default-app-icon.svg ' );
251268 }
269+ if ($ type === 'link ' && $ route === '' ) {
270+ // This means either the route is invalid in the info.xml or the app was not year loaded by the router
271+ $ this ->logger ->debug ('Missing or invalid navigation route for app ' . $ app , ['entry ' => $ nav ]);
272+ continue ;
273+ }
252274
275+ $ l = $ this ->l10nFac ->get ($ app );
276+ $ this ->loadedAppInfo [] = $ app ;
253277 $ this ->add (array_merge ([
254278 // Navigation id
255279 'id ' => $ id ,
@@ -287,13 +311,13 @@ public function setUnreadCounter(string $id, int $unreadCounter): void {
287311
288312 #[Override]
289313 public function get (string $ id ): ?array {
290- $ this ->init ();
314+ $ this ->resolveAppNavigationEntries ();
291315 return $ this ->entries [$ id ];
292316 }
293317
294318 #[Override]
295319 public function getDefaultEntryIdForUser (?IUser $ user = null , bool $ withFallbacks = true ): string {
296- $ this ->init ();
320+ $ this ->resolveAppNavigationEntries ();
297321 // Disable fallbacks here, as we need to override them with the user defaults if none are configured.
298322 $ defaultEntryIds = $ this ->getDefaultEntryIds (false );
299323
@@ -335,7 +359,7 @@ public function getDefaultEntryIdForUser(?IUser $user = null, bool $withFallback
335359
336360 #[Override]
337361 public function getDefaultEntryIds (bool $ withFallbacks = true ): array {
338- $ this ->init ();
362+ $ this ->resolveAppNavigationEntries ();
339363 $ storedIds = explode (', ' , $ this ->config ->getSystemValueString ('defaultapp ' , $ withFallbacks ? 'dashboard,files ' : '' ));
340364 $ ids = [];
341365 $ entryIds = array_keys ($ this ->entries );
@@ -349,7 +373,7 @@ public function getDefaultEntryIds(bool $withFallbacks = true): array {
349373
350374 #[Override]
351375 public function setDefaultEntryIds (array $ ids ): void {
352- $ this ->init ();
376+ $ this ->resolveAppNavigationEntries ();
353377 $ entryIds = array_keys ($ this ->entries );
354378
355379 foreach ($ ids as $ id ) {
0 commit comments