summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorかっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>2024-01-28 19:22:38 +0900
committerGitHub <noreply@github.com>2024-01-28 19:22:38 +0900
commitb62d9f3920d94f68a6e0339c7d73659bbf5a0150 (patch)
tree8a97a7fd9b36c900084028fd325f753b67df4801
parentUpdate CHANGELOG.md (diff)
downloadmisskey-b62d9f3920d94f68a6e0339c7d73659bbf5a0150.tar.gz
misskey-b62d9f3920d94f68a6e0339c7d73659bbf5a0150.tar.bz2
misskey-b62d9f3920d94f68a6e0339c7d73659bbf5a0150.zip
feat(frontend/nirax): リダイレクトを設定できるように (#13030)
* feat(frontend/nirax): リダイレクトを設定できるように * revert demonstrative changes * fix * revert unrelated changes * リダイレクトの際にパスが変わらない問題を修正 * リダイレクトが必要なrouteを設定 * fix lint * router向けe2eテストの追加 * fix --------- Co-authored-by: syuilo <Syuilotan@yahoo.co.jp> Co-authored-by: samunohito <46447427+samunohito@users.noreply.github.com>
-rw-r--r--cypress/e2e/router.cy.js30
-rw-r--r--packages/frontend/src/components/MkPageWindow.vue7
-rw-r--r--packages/frontend/src/global/router/definition.ts19
-rw-r--r--packages/frontend/src/nirax.ts91
4 files changed, 131 insertions, 16 deletions
diff --git a/cypress/e2e/router.cy.js b/cypress/e2e/router.cy.js
new file mode 100644
index 0000000000..81f497b5b8
--- /dev/null
+++ b/cypress/e2e/router.cy.js
@@ -0,0 +1,30 @@
+describe('Router transition', () => {
+ describe('Redirect', () => {
+ // サーバの初期化。ルートのテストに関しては各describeごとに1度だけ実行で十分だと思う(使いまわした方が早い)
+ before(() => {
+ cy.resetState();
+
+ // インスタンス初期セットアップ
+ cy.registerUser('admin', 'pass', true);
+
+ // ユーザー作成
+ cy.registerUser('alice', 'alice1234');
+
+ cy.login('alice', 'alice1234');
+
+ // アカウント初期設定ウィザード
+ // 表示に時間がかかるのでデフォルト秒数だとタイムアウトする
+ cy.get('[data-cy-user-setup] [data-cy-modal-window-close]', { timeout: 12000 }).click();
+ cy.wait(500);
+ cy.get('[data-cy-modal-dialog-ok]').click();
+ });
+
+ it('redirect to user profile', () => {
+ // テストのためだけに用意されたリダイレクト用ルートに飛ぶ
+ cy.visit('/redirect-test');
+
+ // プロフィールページのURLであることを確認する
+ cy.url().should('include', '/@alice')
+ });
+ });
+});
diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index 28058c338b..ccd9df83ed 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -93,6 +93,13 @@ windowRouter.addListener('push', ctx => {
history.value.push({ path: ctx.path, key: ctx.key });
});
+windowRouter.addListener('replace', ctx => {
+ history.value.pop();
+ history.value.push({ path: ctx.path, key: ctx.key });
+});
+
+windowRouter.init();
+
provide('router', windowRouter);
provideMetadataReceiver((info) => {
pageMetadata.value = info;
diff --git a/packages/frontend/src/global/router/definition.ts b/packages/frontend/src/global/router/definition.ts
index 0333770a64..241b4fbcc7 100644
--- a/packages/frontend/src/global/router/definition.ts
+++ b/packages/frontend/src/global/router/definition.ts
@@ -4,6 +4,7 @@
*/
import { App, AsyncComponentLoader, defineAsyncComponent, provide } from 'vue';
+import type { RouteDef } from '@/nirax.js';
import { IRouter, Router } from '@/nirax.js';
import { $i, iAmModerator } from '@/account.js';
import MkLoading from '@/pages/_loading_.vue';
@@ -16,7 +17,7 @@ const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({
errorComponent: MkError,
});
-const routes = [{
+const routes: RouteDef[] = [{
path: '/@:initUser/pages/:initPageName/view-source',
component: page(() => import('@/pages/page-editor/page-editor.vue')),
}, {
@@ -333,8 +334,7 @@ const routes = [{
component: page(() => import('@/pages/registry.vue')),
}, {
path: '/install-extentions',
- // Note: This path is kept for compatibility. It may be deleted.
- component: page(() => import('@/pages/install-extensions.vue')),
+ redirect: '/install-extensions',
loginRequired: true,
}, {
path: '/install-extensions',
@@ -558,6 +558,11 @@ const routes = [{
component: $i ? page(() => import('@/pages/timeline.vue')) : page(() => import('@/pages/welcome.vue')),
globalCacheKey: 'index',
}, {
+ // テスト用リダイレクト設定。ログイン中ユーザのプロフィールにリダイレクトする
+ path: '/redirect-test',
+ redirect: $i ? `@${$i.username}` : '/',
+ loginRequired: true,
+}, {
path: '/:(*)',
component: page(() => import('@/pages/not-found.vue')),
}];
@@ -575,8 +580,6 @@ export function setupRouter(app: App) {
const mainRouter = createRouterImpl(location.pathname + location.search + location.hash);
- window.history.replaceState({ key: mainRouter.getCurrentKey() }, '', location.href);
-
window.addEventListener('popstate', (event) => {
mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key);
});
@@ -585,5 +588,11 @@ export function setupRouter(app: App) {
window.history.pushState({ key: ctx.key }, '', ctx.path);
});
+ mainRouter.addListener('replace', ctx => {
+ window.history.replaceState({ key: ctx.key }, '', ctx.path);
+ });
+
+ mainRouter.init();
+
setMainRouter(mainRouter);
}
diff --git a/packages/frontend/src/nirax.ts b/packages/frontend/src/nirax.ts
index a56aa6419e..ddb2a085db 100644
--- a/packages/frontend/src/nirax.ts
+++ b/packages/frontend/src/nirax.ts
@@ -9,16 +9,25 @@ import { Component, onMounted, shallowRef, ShallowRef } from 'vue';
import { EventEmitter } from 'eventemitter3';
import { safeURIDecode } from '@/scripts/safe-uri-decode.js';
-export type RouteDef = {
+interface RouteDefBase {
path: string;
- component: Component;
query?: Record<string, string>;
loginRequired?: boolean;
name?: string;
hash?: string;
globalCacheKey?: string;
children?: RouteDef[];
-};
+}
+
+interface RouteDefWithComponent extends RouteDefBase {
+ component: Component,
+}
+
+interface RouteDefWithRedirect extends RouteDefBase {
+ redirect: string | ((props: Map<string, string | boolean>) => string);
+}
+
+export type RouteDef = RouteDefWithComponent | RouteDefWithRedirect;
type ParsedPath = (string | {
name: string;
@@ -48,7 +57,19 @@ export type RouterEvent = {
same: () => void;
}
-export type Resolved = { route: RouteDef; props: Map<string, string | boolean>; child?: Resolved; };
+export type Resolved = {
+ route: RouteDef;
+ props: Map<string, string | boolean>;
+ child?: Resolved;
+ redirected?: boolean;
+
+ /** @internal */
+ _parsedRoute: {
+ fullPath: string;
+ queryString: string | null;
+ hash: string | null;
+ };
+};
function parsePath(path: string): ParsedPath {
const res = [] as ParsedPath;
@@ -81,6 +102,11 @@ export interface IRouter extends EventEmitter<RouterEvent> {
currentRoute: ShallowRef<RouteDef>;
navHook: ((path: string, flag?: any) => boolean) | null;
+ /**
+ * ルートの初期化(eventListenerの定義後に必ず呼び出すこと)
+ */
+ init(): void;
+
resolve(path: string): Resolved | null;
getCurrentPath(): any;
@@ -156,12 +182,13 @@ export interface IRouter extends EventEmitter<RouterEvent> {
export class Router extends EventEmitter<RouterEvent> implements IRouter {
private routes: RouteDef[];
public current: Resolved;
- public currentRef: ShallowRef<Resolved> = shallowRef();
- public currentRoute: ShallowRef<RouteDef> = shallowRef();
+ public currentRef: ShallowRef<Resolved>;
+ public currentRoute: ShallowRef<RouteDef>;
private currentPath: string;
private isLoggedIn: boolean;
private notFoundPageComponent: Component;
private currentKey = Date.now().toString();
+ private redirectCount = 0;
public navHook: ((path: string, flag?: any) => boolean) | null = null;
@@ -169,13 +196,24 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
super();
this.routes = routes;
+ this.current = this.resolve(currentPath)!;
+ this.currentRef = shallowRef(this.current);
+ this.currentRoute = shallowRef(this.current.route);
this.currentPath = currentPath;
this.isLoggedIn = isLoggedIn;
this.notFoundPageComponent = notFoundPageComponent;
- this.navigate(currentPath, null, false);
+ }
+
+ public init() {
+ const res = this.navigate(this.currentPath, null, false);
+ this.emit('replace', {
+ path: res._parsedRoute.fullPath,
+ key: this.currentKey,
+ });
}
public resolve(path: string): Resolved | null {
+ const fullPath = path;
let queryString: string | null = null;
let hash: string | null = null;
if (path[0] === '/') path = path.substring(1);
@@ -188,6 +226,12 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
path = path.substring(0, path.indexOf('?'));
}
+ const _parsedRoute = {
+ fullPath,
+ queryString,
+ hash,
+ };
+
if (_DEV_) console.log('Routing: ', path, queryString);
function check(routes: RouteDef[], _parts: string[]): Resolved | null {
@@ -238,6 +282,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
route,
props,
child,
+ _parsedRoute,
};
} else {
continue forEachRouteLoop;
@@ -263,6 +308,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
return {
route,
props,
+ _parsedRoute,
};
} else {
if (route.children) {
@@ -272,6 +318,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
route,
props,
child,
+ _parsedRoute,
};
} else {
continue forEachRouteLoop;
@@ -290,7 +337,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
return check(this.routes, _parts);
}
- private navigate(path: string, key: string | null | undefined, emitChange = true) {
+ private navigate(path: string, key: string | null | undefined, emitChange = true, _redirected = false): Resolved {
const beforePath = this.currentPath;
this.currentPath = path;
@@ -300,6 +347,20 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
throw new Error('no route found for: ' + path);
}
+ if ('redirect' in res.route) {
+ let redirectPath: string;
+ if (typeof res.route.redirect === 'function') {
+ redirectPath = res.route.redirect(res.props);
+ } else {
+ redirectPath = res.route.redirect + (res._parsedRoute.queryString ? '?' + res._parsedRoute.queryString : '') + (res._parsedRoute.hash ? '#' + res._parsedRoute.hash : '');
+ }
+ if (_DEV_) console.log('Redirecting to: ', redirectPath);
+ if (_redirected && this.redirectCount++ > 10) {
+ throw new Error('redirect loop detected');
+ }
+ return this.navigate(redirectPath, null, emitChange, true);
+ }
+
if (res.route.loginRequired && !this.isLoggedIn) {
res.route.component = this.notFoundPageComponent;
res.props.set('showLoginPopup', true);
@@ -321,7 +382,11 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
});
}
- return res;
+ this.redirectCount = 0;
+ return {
+ ...res,
+ redirected: _redirected,
+ };
}
public getCurrentPath() {
@@ -345,7 +410,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
const res = this.navigate(path, null);
this.emit('push', {
beforePath,
- path,
+ path: res._parsedRoute.fullPath,
route: res.route,
props: res.props,
key: this.currentKey,
@@ -353,7 +418,11 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
}
public replace(path: string, key?: string | null) {
- this.navigate(path, key);
+ const res = this.navigate(path, key);
+ this.emit('replace', {
+ path: res._parsedRoute.fullPath,
+ key: this.currentKey,
+ });
}
}