Merge pull request #676 from vector-im/ts-conversion-domain-navigation
Convert /domain/navigation to typescript
This commit is contained in:
commit
b40ce6137e
10 changed files with 268 additions and 165 deletions
|
@ -16,10 +16,11 @@ limitations under the License.
|
||||||
|
|
||||||
import {Options as BaseOptions, ViewModel} from "./ViewModel";
|
import {Options as BaseOptions, ViewModel} from "./ViewModel";
|
||||||
import {Client} from "../matrix/Client.js";
|
import {Client} from "../matrix/Client.js";
|
||||||
|
import {SegmentType} from "./navigation/index";
|
||||||
|
|
||||||
type Options = { sessionId: string; } & BaseOptions;
|
type Options = { sessionId: string; } & BaseOptions;
|
||||||
|
|
||||||
export class LogoutViewModel extends ViewModel<Options> {
|
export class LogoutViewModel extends ViewModel<SegmentType, Options> {
|
||||||
private _sessionId: string;
|
private _sessionId: string;
|
||||||
private _busy: boolean;
|
private _busy: boolean;
|
||||||
private _showConfirm: boolean;
|
private _showConfirm: boolean;
|
||||||
|
@ -41,7 +42,7 @@ export class LogoutViewModel extends ViewModel<Options> {
|
||||||
return this._busy;
|
return this._busy;
|
||||||
}
|
}
|
||||||
|
|
||||||
get cancelUrl(): string {
|
get cancelUrl(): string | undefined {
|
||||||
return this.urlCreator.urlForSegment("session", true);
|
return this.urlCreator.urlForSegment("session", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,17 +27,19 @@ import type {Platform} from "../platform/web/Platform";
|
||||||
import type {Clock} from "../platform/web/dom/Clock";
|
import type {Clock} from "../platform/web/dom/Clock";
|
||||||
import type {ILogger} from "../logging/types";
|
import type {ILogger} from "../logging/types";
|
||||||
import type {Navigation} from "./navigation/Navigation";
|
import type {Navigation} from "./navigation/Navigation";
|
||||||
import type {URLRouter} from "./navigation/URLRouter";
|
import type {SegmentType} from "./navigation/index";
|
||||||
|
import type {IURLRouter} from "./navigation/URLRouter";
|
||||||
|
|
||||||
export type Options = {
|
export type Options<T extends object = SegmentType> = {
|
||||||
platform: Platform
|
platform: Platform;
|
||||||
logger: ILogger
|
logger: ILogger;
|
||||||
urlCreator: URLRouter
|
urlCreator: IURLRouter<T>;
|
||||||
navigation: Navigation
|
navigation: Navigation<T>;
|
||||||
emitChange?: (params: any) => void
|
emitChange?: (params: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ViewModel<O extends Options = Options> extends EventEmitter<{change: never}> {
|
|
||||||
|
export class ViewModel<N extends object = SegmentType, O extends Options<N> = Options<N>> extends EventEmitter<{change: never}> {
|
||||||
private disposables?: Disposables;
|
private disposables?: Disposables;
|
||||||
private _isDisposed = false;
|
private _isDisposed = false;
|
||||||
private _options: Readonly<O>;
|
private _options: Readonly<O>;
|
||||||
|
@ -47,7 +49,7 @@ export class ViewModel<O extends Options = Options> extends EventEmitter<{change
|
||||||
this._options = options;
|
this._options = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
childOptions<T extends Object>(explicitOptions: T): T & Options {
|
childOptions<T extends Object>(explicitOptions: T): T & Options<N> {
|
||||||
return Object.assign({}, this._options, explicitOptions);
|
return Object.assign({}, this._options, explicitOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,9 +60,9 @@ export class ViewModel<O extends Options = Options> extends EventEmitter<{change
|
||||||
return this._options[name];
|
return this._options[name];
|
||||||
}
|
}
|
||||||
|
|
||||||
observeNavigation(type: string, onChange: (value: string | true | undefined, type: string) => void): void {
|
observeNavigation<T extends keyof N>(type: T, onChange: (value: N[T], type: T) => void): void {
|
||||||
const segmentObservable = this.navigation.observe(type);
|
const segmentObservable = this.navigation.observe(type);
|
||||||
const unsubscribe = segmentObservable.subscribe((value: string | true | undefined) => {
|
const unsubscribe = segmentObservable.subscribe((value: N[T]) => {
|
||||||
onChange(value, type);
|
onChange(value, type);
|
||||||
});
|
});
|
||||||
this.track(unsubscribe);
|
this.track(unsubscribe);
|
||||||
|
@ -135,11 +137,12 @@ export class ViewModel<O extends Options = Options> extends EventEmitter<{change
|
||||||
return this.platform.logger;
|
return this.platform.logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
get urlCreator(): URLRouter {
|
get urlCreator(): IURLRouter<N> {
|
||||||
return this._options.urlCreator;
|
return this._options.urlCreator;
|
||||||
}
|
}
|
||||||
|
|
||||||
get navigation(): Navigation {
|
get navigation(): Navigation<N> {
|
||||||
return this._options.navigation;
|
// typescript needs a little help here
|
||||||
|
return this._options.navigation as unknown as Navigation<N>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,8 @@ import {StartSSOLoginViewModel} from "./StartSSOLoginViewModel.js";
|
||||||
import {CompleteSSOLoginViewModel} from "./CompleteSSOLoginViewModel.js";
|
import {CompleteSSOLoginViewModel} from "./CompleteSSOLoginViewModel.js";
|
||||||
import {LoadStatus} from "../../matrix/Client.js";
|
import {LoadStatus} from "../../matrix/Client.js";
|
||||||
import {SessionLoadViewModel} from "../SessionLoadViewModel.js";
|
import {SessionLoadViewModel} from "../SessionLoadViewModel.js";
|
||||||
|
import {SegmentType} from "../navigation/index";
|
||||||
|
|
||||||
import type {PasswordLoginMethod, SSOLoginHelper, TokenLoginMethod, ILoginMethod} from "../../matrix/login";
|
import type {PasswordLoginMethod, SSOLoginHelper, TokenLoginMethod, ILoginMethod} from "../../matrix/login";
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
|
@ -29,7 +31,7 @@ type Options = {
|
||||||
loginToken?: string;
|
loginToken?: string;
|
||||||
} & BaseOptions;
|
} & BaseOptions;
|
||||||
|
|
||||||
export class LoginViewModel extends ViewModel<Options> {
|
export class LoginViewModel extends ViewModel<SegmentType, Options> {
|
||||||
private _ready: ReadyFn;
|
private _ready: ReadyFn;
|
||||||
private _loginToken?: string;
|
private _loginToken?: string;
|
||||||
private _client: Client;
|
private _client: Client;
|
||||||
|
|
|
@ -16,27 +16,49 @@ limitations under the License.
|
||||||
|
|
||||||
import {BaseObservableValue, ObservableValue} from "../../observable/ObservableValue";
|
import {BaseObservableValue, ObservableValue} from "../../observable/ObservableValue";
|
||||||
|
|
||||||
export class Navigation {
|
|
||||||
constructor(allowsChild) {
|
type AllowsChild<T> = (parent: Segment<T> | undefined, child: Segment<T>) => boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OptionalValue is basically stating that if SegmentType[type] = true:
|
||||||
|
* - Allow this type to be optional
|
||||||
|
* - Give it a default value of undefined
|
||||||
|
* - Also allow it to be true
|
||||||
|
* This lets us do:
|
||||||
|
* const s: Segment<SegmentType> = new Segment("create-room");
|
||||||
|
* instead of
|
||||||
|
* const s: Segment<SegmentType> = new Segment("create-room", undefined);
|
||||||
|
*/
|
||||||
|
export type OptionalValue<T> = T extends true? [(undefined | true)?]: [T];
|
||||||
|
|
||||||
|
export class Navigation<T extends object> {
|
||||||
|
private readonly _allowsChild: AllowsChild<T>;
|
||||||
|
private _path: Path<T>;
|
||||||
|
private readonly _observables: Map<keyof T, SegmentObservable<T>> = new Map();
|
||||||
|
private readonly _pathObservable: ObservableValue<Path<T>>;
|
||||||
|
|
||||||
|
constructor(allowsChild: AllowsChild<T>) {
|
||||||
this._allowsChild = allowsChild;
|
this._allowsChild = allowsChild;
|
||||||
this._path = new Path([], allowsChild);
|
this._path = new Path([], allowsChild);
|
||||||
this._observables = new Map();
|
|
||||||
this._pathObservable = new ObservableValue(this._path);
|
this._pathObservable = new ObservableValue(this._path);
|
||||||
}
|
}
|
||||||
|
|
||||||
get pathObservable() {
|
get pathObservable(): ObservableValue<Path<T>> {
|
||||||
return this._pathObservable;
|
return this._pathObservable;
|
||||||
}
|
}
|
||||||
|
|
||||||
get path() {
|
get path(): Path<T> {
|
||||||
return this._path;
|
return this._path;
|
||||||
}
|
}
|
||||||
|
|
||||||
push(type, value = undefined) {
|
push<K extends keyof T>(type: K, ...value: OptionalValue<T[K]>): void {
|
||||||
return this.applyPath(this.path.with(new Segment(type, value)));
|
const newPath = this.path.with(new Segment(type, ...value));
|
||||||
|
if (newPath) {
|
||||||
|
this.applyPath(newPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
applyPath(path) {
|
applyPath(path: Path<T>): void {
|
||||||
// Path is not exported, so you can only create a Path through Navigation,
|
// Path is not exported, so you can only create a Path through Navigation,
|
||||||
// so we assume it respects the allowsChild rules
|
// so we assume it respects the allowsChild rules
|
||||||
const oldPath = this._path;
|
const oldPath = this._path;
|
||||||
|
@ -60,7 +82,7 @@ export class Navigation {
|
||||||
this._pathObservable.set(this._path);
|
this._pathObservable.set(this._path);
|
||||||
}
|
}
|
||||||
|
|
||||||
observe(type) {
|
observe(type: keyof T): SegmentObservable<T> {
|
||||||
let observable = this._observables.get(type);
|
let observable = this._observables.get(type);
|
||||||
if (!observable) {
|
if (!observable) {
|
||||||
observable = new SegmentObservable(this, type);
|
observable = new SegmentObservable(this, type);
|
||||||
|
@ -69,9 +91,9 @@ export class Navigation {
|
||||||
return observable;
|
return observable;
|
||||||
}
|
}
|
||||||
|
|
||||||
pathFrom(segments) {
|
pathFrom(segments: Segment<any>[]): Path<T> {
|
||||||
let parent;
|
let parent: Segment<any> | undefined;
|
||||||
let i;
|
let i: number;
|
||||||
for (i = 0; i < segments.length; i += 1) {
|
for (i = 0; i < segments.length; i += 1) {
|
||||||
if (!this._allowsChild(parent, segments[i])) {
|
if (!this._allowsChild(parent, segments[i])) {
|
||||||
return new Path(segments.slice(0, i), this._allowsChild);
|
return new Path(segments.slice(0, i), this._allowsChild);
|
||||||
|
@ -81,12 +103,12 @@ export class Navigation {
|
||||||
return new Path(segments, this._allowsChild);
|
return new Path(segments, this._allowsChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
segment(type, value) {
|
segment<K extends keyof T>(type: K, ...value: OptionalValue<T[K]>): Segment<T> {
|
||||||
return new Segment(type, value);
|
return new Segment(type, ...value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function segmentValueEqual(a, b) {
|
function segmentValueEqual<T>(a?: T[keyof T], b?: T[keyof T]): boolean {
|
||||||
if (a === b) {
|
if (a === b) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -103,24 +125,29 @@ function segmentValueEqual(a, b) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Segment {
|
|
||||||
constructor(type, value) {
|
export class Segment<T, K extends keyof T = any> {
|
||||||
this.type = type;
|
public value: T[K];
|
||||||
this.value = value === undefined ? true : value;
|
|
||||||
|
constructor(public type: K, ...value: OptionalValue<T[K]>) {
|
||||||
|
this.value = (value[0] === undefined ? true : value[0]) as unknown as T[K];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Path {
|
class Path<T> {
|
||||||
constructor(segments = [], allowsChild) {
|
private readonly _segments: Segment<T, any>[];
|
||||||
|
private readonly _allowsChild: AllowsChild<T>;
|
||||||
|
|
||||||
|
constructor(segments: Segment<T>[] = [], allowsChild: AllowsChild<T>) {
|
||||||
this._segments = segments;
|
this._segments = segments;
|
||||||
this._allowsChild = allowsChild;
|
this._allowsChild = allowsChild;
|
||||||
}
|
}
|
||||||
|
|
||||||
clone() {
|
clone(): Path<T> {
|
||||||
return new Path(this._segments.slice(), this._allowsChild);
|
return new Path(this._segments.slice(), this._allowsChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
with(segment) {
|
with(segment: Segment<T>): Path<T> | undefined {
|
||||||
let index = this._segments.length - 1;
|
let index = this._segments.length - 1;
|
||||||
do {
|
do {
|
||||||
if (this._allowsChild(this._segments[index], segment)) {
|
if (this._allowsChild(this._segments[index], segment)) {
|
||||||
|
@ -132,10 +159,10 @@ class Path {
|
||||||
index -= 1;
|
index -= 1;
|
||||||
} while(index >= -1);
|
} while(index >= -1);
|
||||||
// allow -1 as well so we check if the segment is allowed as root
|
// allow -1 as well so we check if the segment is allowed as root
|
||||||
return null;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
until(type) {
|
until(type: keyof T): Path<T> {
|
||||||
const index = this._segments.findIndex(s => s.type === type);
|
const index = this._segments.findIndex(s => s.type === type);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
return new Path(this._segments.slice(0, index + 1), this._allowsChild)
|
return new Path(this._segments.slice(0, index + 1), this._allowsChild)
|
||||||
|
@ -143,11 +170,11 @@ class Path {
|
||||||
return new Path([], this._allowsChild);
|
return new Path([], this._allowsChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
get(type) {
|
get(type: keyof T): Segment<T> | undefined {
|
||||||
return this._segments.find(s => s.type === type);
|
return this._segments.find(s => s.type === type);
|
||||||
}
|
}
|
||||||
|
|
||||||
replace(segment) {
|
replace(segment: Segment<T>): Path<T> | undefined {
|
||||||
const index = this._segments.findIndex(s => s.type === segment.type);
|
const index = this._segments.findIndex(s => s.type === segment.type);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
const parent = this._segments[index - 1];
|
const parent = this._segments[index - 1];
|
||||||
|
@ -160,10 +187,10 @@ class Path {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
get segments() {
|
get segments(): Segment<T>[] {
|
||||||
return this._segments;
|
return this._segments;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -172,43 +199,49 @@ class Path {
|
||||||
* custom observable so it always returns what is in navigation.path, even if we haven't emitted the change yet.
|
* custom observable so it always returns what is in navigation.path, even if we haven't emitted the change yet.
|
||||||
* This ensures that observers of a segment can also read the most recent value of other segments.
|
* This ensures that observers of a segment can also read the most recent value of other segments.
|
||||||
*/
|
*/
|
||||||
class SegmentObservable extends BaseObservableValue {
|
class SegmentObservable<T extends object> extends BaseObservableValue<T[keyof T] | undefined> {
|
||||||
constructor(navigation, type) {
|
private readonly _navigation: Navigation<T>;
|
||||||
|
private _type: keyof T;
|
||||||
|
private _lastSetValue?: T[keyof T];
|
||||||
|
|
||||||
|
constructor(navigation: Navigation<T>, type: keyof T) {
|
||||||
super();
|
super();
|
||||||
this._navigation = navigation;
|
this._navigation = navigation;
|
||||||
this._type = type;
|
this._type = type;
|
||||||
this._lastSetValue = navigation.path.get(type)?.value;
|
this._lastSetValue = navigation.path.get(type)?.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
get() {
|
get(): T[keyof T] | undefined {
|
||||||
const path = this._navigation.path;
|
const path = this._navigation.path;
|
||||||
const segment = path.get(this._type);
|
const segment = path.get(this._type);
|
||||||
const value = segment?.value;
|
const value = segment?.value;
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
emitIfChanged() {
|
emitIfChanged(): void {
|
||||||
const newValue = this.get();
|
const newValue = this.get();
|
||||||
if (!segmentValueEqual(newValue, this._lastSetValue)) {
|
if (!segmentValueEqual<T>(newValue, this._lastSetValue)) {
|
||||||
this._lastSetValue = newValue;
|
this._lastSetValue = newValue;
|
||||||
this.emit(newValue);
|
this.emit(newValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type {Path};
|
||||||
|
|
||||||
export function tests() {
|
export function tests() {
|
||||||
|
|
||||||
function createMockNavigation() {
|
function createMockNavigation() {
|
||||||
return new Navigation((parent, {type}) => {
|
return new Navigation((parent, {type}) => {
|
||||||
switch (parent?.type) {
|
switch (parent?.type) {
|
||||||
case undefined:
|
case undefined:
|
||||||
return type === "1" || "2";
|
return type === "1" || type === "2";
|
||||||
case "1":
|
case "1":
|
||||||
return type === "1.1";
|
return type === "1.1";
|
||||||
case "1.1":
|
case "1.1":
|
||||||
return type === "1.1.1";
|
return type === "1.1.1";
|
||||||
case "2":
|
case "2":
|
||||||
return type === "2.1" || "2.2";
|
return type === "2.1" || type === "2.2";
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -216,7 +249,7 @@ export function tests() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function observeTypes(nav, types) {
|
function observeTypes(nav, types) {
|
||||||
const changes = [];
|
const changes: {type:string, value:any}[] = [];
|
||||||
for (const type of types) {
|
for (const type of types) {
|
||||||
nav.observe(type).subscribe(value => {
|
nav.observe(type).subscribe(value => {
|
||||||
changes.push({type, value});
|
changes.push({type, value});
|
||||||
|
@ -225,6 +258,12 @@ export function tests() {
|
||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SegmentType = {
|
||||||
|
"foo": number;
|
||||||
|
"bar": number;
|
||||||
|
"baz": number;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"applying a path emits an event on the observable": assert => {
|
"applying a path emits an event on the observable": assert => {
|
||||||
const nav = createMockNavigation();
|
const nav = createMockNavigation();
|
||||||
|
@ -242,18 +281,18 @@ export function tests() {
|
||||||
assert.equal(changes[1].value, 8);
|
assert.equal(changes[1].value, 8);
|
||||||
},
|
},
|
||||||
"path.get": assert => {
|
"path.get": assert => {
|
||||||
const path = new Path([new Segment("foo", 5), new Segment("bar", 6)], () => true);
|
const path = new Path<SegmentType>([new Segment("foo", 5), new Segment("bar", 6)], () => true);
|
||||||
assert.equal(path.get("foo").value, 5);
|
assert.equal(path.get("foo")!.value, 5);
|
||||||
assert.equal(path.get("bar").value, 6);
|
assert.equal(path.get("bar")!.value, 6);
|
||||||
},
|
},
|
||||||
"path.replace success": assert => {
|
"path.replace success": assert => {
|
||||||
const path = new Path([new Segment("foo", 5), new Segment("bar", 6)], () => true);
|
const path = new Path<SegmentType>([new Segment("foo", 5), new Segment("bar", 6)], () => true);
|
||||||
const newPath = path.replace(new Segment("foo", 1));
|
const newPath = path.replace(new Segment("foo", 1));
|
||||||
assert.equal(newPath.get("foo").value, 1);
|
assert.equal(newPath!.get("foo")!.value, 1);
|
||||||
assert.equal(newPath.get("bar").value, 6);
|
assert.equal(newPath!.get("bar")!.value, 6);
|
||||||
},
|
},
|
||||||
"path.replace not found": assert => {
|
"path.replace not found": assert => {
|
||||||
const path = new Path([new Segment("foo", 5), new Segment("bar", 6)], () => true);
|
const path = new Path<SegmentType>([new Segment("foo", 5), new Segment("bar", 6)], () => true);
|
||||||
const newPath = path.replace(new Segment("baz", 1));
|
const newPath = path.replace(new Segment("baz", 1));
|
||||||
assert.equal(newPath, null);
|
assert.equal(newPath, null);
|
||||||
}
|
}
|
|
@ -14,28 +14,55 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export class URLRouter {
|
import type {History} from "../../platform/web/dom/History.js";
|
||||||
constructor({history, navigation, parseUrlPath, stringifyPath}) {
|
import type {Navigation, Segment, Path, OptionalValue} from "./Navigation";
|
||||||
|
import type {SubscriptionHandle} from "../../observable/BaseObservable";
|
||||||
|
|
||||||
|
type ParseURLPath<T> = (urlPath: string, currentNavPath: Path<T>, defaultSessionId?: string) => Segment<T>[];
|
||||||
|
type StringifyPath<T> = (path: Path<T>) => string;
|
||||||
|
|
||||||
|
export interface IURLRouter<T> {
|
||||||
|
attach(): void;
|
||||||
|
dispose(): void;
|
||||||
|
pushUrl(url: string): void;
|
||||||
|
tryRestoreLastUrl(): boolean;
|
||||||
|
urlForSegments(segments: Segment<T>[]): string | undefined;
|
||||||
|
urlForSegment<K extends keyof T>(type: K, ...value: OptionalValue<T[K]>): string | undefined;
|
||||||
|
urlUntilSegment(type: keyof T): string;
|
||||||
|
urlForPath(path: Path<T>): string;
|
||||||
|
openRoomActionUrl(roomId: string): string;
|
||||||
|
createSSOCallbackURL(): string;
|
||||||
|
normalizeUrl(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class URLRouter<T extends {session: string | boolean}> implements IURLRouter<T> {
|
||||||
|
private readonly _history: History;
|
||||||
|
private readonly _navigation: Navigation<T>;
|
||||||
|
private readonly _parseUrlPath: ParseURLPath<T>;
|
||||||
|
private readonly _stringifyPath: StringifyPath<T>;
|
||||||
|
private _subscription?: SubscriptionHandle;
|
||||||
|
private _pathSubscription?: SubscriptionHandle;
|
||||||
|
private _isApplyingUrl: boolean = false;
|
||||||
|
private _defaultSessionId?: string;
|
||||||
|
|
||||||
|
constructor(history: History, navigation: Navigation<T>, parseUrlPath: ParseURLPath<T>, stringifyPath: StringifyPath<T>) {
|
||||||
this._history = history;
|
this._history = history;
|
||||||
this._navigation = navigation;
|
this._navigation = navigation;
|
||||||
this._parseUrlPath = parseUrlPath;
|
this._parseUrlPath = parseUrlPath;
|
||||||
this._stringifyPath = stringifyPath;
|
this._stringifyPath = stringifyPath;
|
||||||
this._subscription = null;
|
|
||||||
this._pathSubscription = null;
|
|
||||||
this._isApplyingUrl = false;
|
|
||||||
this._defaultSessionId = this._getLastSessionId();
|
this._defaultSessionId = this._getLastSessionId();
|
||||||
}
|
}
|
||||||
|
|
||||||
_getLastSessionId() {
|
private _getLastSessionId(): string | undefined {
|
||||||
const navPath = this._urlAsNavPath(this._history.getLastSessionUrl() || "");
|
const navPath = this._urlAsNavPath(this._history.getLastSessionUrl() || "");
|
||||||
const sessionId = navPath.get("session")?.value;
|
const sessionId = navPath.get("session")?.value;
|
||||||
if (typeof sessionId === "string") {
|
if (typeof sessionId === "string") {
|
||||||
return sessionId;
|
return sessionId;
|
||||||
}
|
}
|
||||||
return null;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
attach() {
|
attach(): void {
|
||||||
this._subscription = this._history.subscribe(url => this._applyUrl(url));
|
this._subscription = this._history.subscribe(url => this._applyUrl(url));
|
||||||
// subscribe to path before applying initial url
|
// subscribe to path before applying initial url
|
||||||
// so redirects in _applyNavPathToHistory are reflected in url bar
|
// so redirects in _applyNavPathToHistory are reflected in url bar
|
||||||
|
@ -43,12 +70,12 @@ export class URLRouter {
|
||||||
this._applyUrl(this._history.get());
|
this._applyUrl(this._history.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose() {
|
dispose(): void {
|
||||||
this._subscription = this._subscription();
|
if (this._subscription) { this._subscription = this._subscription(); }
|
||||||
this._pathSubscription = this._pathSubscription();
|
if (this._pathSubscription) { this._pathSubscription = this._pathSubscription(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
_applyNavPathToHistory(path) {
|
private _applyNavPathToHistory(path: Path<T>): void {
|
||||||
const url = this.urlForPath(path);
|
const url = this.urlForPath(path);
|
||||||
if (url !== this._history.get()) {
|
if (url !== this._history.get()) {
|
||||||
if (this._isApplyingUrl) {
|
if (this._isApplyingUrl) {
|
||||||
|
@ -60,7 +87,7 @@ export class URLRouter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_applyNavPathToNavigation(navPath) {
|
private _applyNavPathToNavigation(navPath: Path<T>): void {
|
||||||
// this will cause _applyNavPathToHistory to be called,
|
// this will cause _applyNavPathToHistory to be called,
|
||||||
// so set a flag whether this request came from ourselves
|
// so set a flag whether this request came from ourselves
|
||||||
// (in which case it is a redirect if the url does not match the current one)
|
// (in which case it is a redirect if the url does not match the current one)
|
||||||
|
@ -69,21 +96,21 @@ export class URLRouter {
|
||||||
this._isApplyingUrl = false;
|
this._isApplyingUrl = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_urlAsNavPath(url) {
|
private _urlAsNavPath(url: string): Path<T> {
|
||||||
const urlPath = this._history.urlAsPath(url);
|
const urlPath = this._history.urlAsPath(url);
|
||||||
return this._navigation.pathFrom(this._parseUrlPath(urlPath, this._navigation.path, this._defaultSessionId));
|
return this._navigation.pathFrom(this._parseUrlPath(urlPath, this._navigation.path, this._defaultSessionId));
|
||||||
}
|
}
|
||||||
|
|
||||||
_applyUrl(url) {
|
private _applyUrl(url: string): void {
|
||||||
const navPath = this._urlAsNavPath(url);
|
const navPath = this._urlAsNavPath(url);
|
||||||
this._applyNavPathToNavigation(navPath);
|
this._applyNavPathToNavigation(navPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
pushUrl(url) {
|
pushUrl(url: string): void {
|
||||||
this._history.pushUrl(url);
|
this._history.pushUrl(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
tryRestoreLastUrl() {
|
tryRestoreLastUrl(): boolean {
|
||||||
const lastNavPath = this._urlAsNavPath(this._history.getLastSessionUrl() || "");
|
const lastNavPath = this._urlAsNavPath(this._history.getLastSessionUrl() || "");
|
||||||
if (lastNavPath.segments.length !== 0) {
|
if (lastNavPath.segments.length !== 0) {
|
||||||
this._applyNavPathToNavigation(lastNavPath);
|
this._applyNavPathToNavigation(lastNavPath);
|
||||||
|
@ -92,8 +119,8 @@ export class URLRouter {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
urlForSegments(segments) {
|
urlForSegments(segments: Segment<T>[]): string | undefined {
|
||||||
let path = this._navigation.path;
|
let path: Path<T> | undefined = this._navigation.path;
|
||||||
for (const segment of segments) {
|
for (const segment of segments) {
|
||||||
path = path.with(segment);
|
path = path.with(segment);
|
||||||
if (!path) {
|
if (!path) {
|
||||||
|
@ -103,29 +130,29 @@ export class URLRouter {
|
||||||
return this.urlForPath(path);
|
return this.urlForPath(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
urlForSegment(type, value) {
|
urlForSegment<K extends keyof T>(type: K, ...value: OptionalValue<T[K]>): string | undefined {
|
||||||
return this.urlForSegments([this._navigation.segment(type, value)]);
|
return this.urlForSegments([this._navigation.segment(type, ...value)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
urlUntilSegment(type) {
|
urlUntilSegment(type: keyof T): string {
|
||||||
return this.urlForPath(this._navigation.path.until(type));
|
return this.urlForPath(this._navigation.path.until(type));
|
||||||
}
|
}
|
||||||
|
|
||||||
urlForPath(path) {
|
urlForPath(path: Path<T>): string {
|
||||||
return this._history.pathAsUrl(this._stringifyPath(path));
|
return this._history.pathAsUrl(this._stringifyPath(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
openRoomActionUrl(roomId) {
|
openRoomActionUrl(roomId: string): string {
|
||||||
// not a segment to navigation knowns about, so append it manually
|
// not a segment to navigation knowns about, so append it manually
|
||||||
const urlPath = `${this._stringifyPath(this._navigation.path.until("session"))}/open-room/${roomId}`;
|
const urlPath = `${this._stringifyPath(this._navigation.path.until("session"))}/open-room/${roomId}`;
|
||||||
return this._history.pathAsUrl(urlPath);
|
return this._history.pathAsUrl(urlPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
createSSOCallbackURL() {
|
createSSOCallbackURL(): string {
|
||||||
return window.location.origin;
|
return window.location.origin;
|
||||||
}
|
}
|
||||||
|
|
||||||
normalizeUrl() {
|
normalizeUrl(): void {
|
||||||
// Remove any queryParameters from the URL
|
// Remove any queryParameters from the URL
|
||||||
// Gets rid of the loginToken after SSO
|
// Gets rid of the loginToken after SSO
|
||||||
this._history.replaceUrlSilently(`${window.location.origin}/${window.location.hash}`);
|
this._history.replaceUrlSilently(`${window.location.origin}/${window.location.hash}`);
|
|
@ -14,18 +14,36 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Navigation, Segment} from "./Navigation.js";
|
import {Navigation, Segment} from "./Navigation";
|
||||||
import {URLRouter} from "./URLRouter.js";
|
import {URLRouter} from "./URLRouter";
|
||||||
|
import type {Path, OptionalValue} from "./Navigation";
|
||||||
|
|
||||||
export function createNavigation() {
|
export type SegmentType = {
|
||||||
|
"login": true;
|
||||||
|
"session": string | boolean;
|
||||||
|
"sso": string;
|
||||||
|
"logout": true;
|
||||||
|
"room": string;
|
||||||
|
"rooms": string[];
|
||||||
|
"settings": true;
|
||||||
|
"create-room": true;
|
||||||
|
"empty-grid-tile": number;
|
||||||
|
"lightbox": string;
|
||||||
|
"right-panel": true;
|
||||||
|
"details": true;
|
||||||
|
"members": true;
|
||||||
|
"member": string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createNavigation(): Navigation<SegmentType> {
|
||||||
return new Navigation(allowsChild);
|
return new Navigation(allowsChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createRouter({history, navigation}) {
|
export function createRouter({history, navigation}: {history: History, navigation: Navigation<SegmentType>}): URLRouter<SegmentType> {
|
||||||
return new URLRouter({history, navigation, stringifyPath, parseUrlPath});
|
return new URLRouter(history, navigation, parseUrlPath, stringifyPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
function allowsChild(parent, child) {
|
function allowsChild(parent: Segment<SegmentType> | undefined, child: Segment<SegmentType>): boolean {
|
||||||
const {type} = child;
|
const {type} = child;
|
||||||
switch (parent?.type) {
|
switch (parent?.type) {
|
||||||
case undefined:
|
case undefined:
|
||||||
|
@ -45,8 +63,9 @@ function allowsChild(parent, child) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeRoomFromPath(path, roomId) {
|
export function removeRoomFromPath(path: Path<SegmentType>, roomId: string): Path<SegmentType> | undefined {
|
||||||
const rooms = path.get("rooms");
|
let newPath: Path<SegmentType> | undefined = path;
|
||||||
|
const rooms = newPath.get("rooms");
|
||||||
let roomIdGridIndex = -1;
|
let roomIdGridIndex = -1;
|
||||||
// first delete from rooms segment
|
// first delete from rooms segment
|
||||||
if (rooms) {
|
if (rooms) {
|
||||||
|
@ -54,22 +73,22 @@ export function removeRoomFromPath(path, roomId) {
|
||||||
if (roomIdGridIndex !== -1) {
|
if (roomIdGridIndex !== -1) {
|
||||||
const idsWithoutRoom = rooms.value.slice();
|
const idsWithoutRoom = rooms.value.slice();
|
||||||
idsWithoutRoom[roomIdGridIndex] = "";
|
idsWithoutRoom[roomIdGridIndex] = "";
|
||||||
path = path.replace(new Segment("rooms", idsWithoutRoom));
|
newPath = newPath.replace(new Segment("rooms", idsWithoutRoom));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const room = path.get("room");
|
const room = newPath!.get("room");
|
||||||
// then from room (which occurs with or without rooms)
|
// then from room (which occurs with or without rooms)
|
||||||
if (room && room.value === roomId) {
|
if (room && room.value === roomId) {
|
||||||
if (roomIdGridIndex !== -1) {
|
if (roomIdGridIndex !== -1) {
|
||||||
path = path.with(new Segment("empty-grid-tile", roomIdGridIndex));
|
newPath = newPath!.with(new Segment("empty-grid-tile", roomIdGridIndex));
|
||||||
} else {
|
} else {
|
||||||
path = path.until("session");
|
newPath = newPath!.until("session");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return path;
|
return newPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
function roomsSegmentWithRoom(rooms, roomId, path) {
|
function roomsSegmentWithRoom(rooms: Segment<SegmentType, "rooms">, roomId: string, path: Path<SegmentType>): Segment<SegmentType, "rooms"> {
|
||||||
if(!rooms.value.includes(roomId)) {
|
if(!rooms.value.includes(roomId)) {
|
||||||
const emptyGridTile = path.get("empty-grid-tile");
|
const emptyGridTile = path.get("empty-grid-tile");
|
||||||
const oldRoom = path.get("room");
|
const oldRoom = path.get("room");
|
||||||
|
@ -87,28 +106,28 @@ function roomsSegmentWithRoom(rooms, roomId, path) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function pushRightPanelSegment(array, segment, value = true) {
|
function pushRightPanelSegment<T extends keyof SegmentType>(array: Segment<SegmentType>[], segment: T, ...value: OptionalValue<SegmentType[T]>): void {
|
||||||
array.push(new Segment("right-panel"));
|
array.push(new Segment("right-panel"));
|
||||||
array.push(new Segment(segment, value));
|
array.push(new Segment(segment, ...value));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addPanelIfNeeded(navigation, path) {
|
export function addPanelIfNeeded<T extends SegmentType>(navigation: Navigation<T>, path: Path<T>): Path<T> {
|
||||||
const segments = navigation.path.segments;
|
const segments = navigation.path.segments;
|
||||||
const i = segments.findIndex(segment => segment.type === "right-panel");
|
const i = segments.findIndex(segment => segment.type === "right-panel");
|
||||||
let _path = path;
|
let _path = path;
|
||||||
if (i !== -1) {
|
if (i !== -1) {
|
||||||
_path = path.until("room");
|
_path = path.until("room");
|
||||||
_path = _path.with(segments[i]);
|
_path = _path.with(segments[i])!;
|
||||||
_path = _path.with(segments[i + 1]);
|
_path = _path.with(segments[i + 1])!;
|
||||||
}
|
}
|
||||||
return _path;
|
return _path;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseUrlPath(urlPath, currentNavPath, defaultSessionId) {
|
export function parseUrlPath(urlPath: string, currentNavPath: Path<SegmentType>, defaultSessionId?: string): Segment<SegmentType>[] {
|
||||||
// substr(1) to take of initial /
|
// substring(1) to take of initial /
|
||||||
const parts = urlPath.substr(1).split("/");
|
const parts = urlPath.substring(1).split("/");
|
||||||
const iterator = parts[Symbol.iterator]();
|
const iterator = parts[Symbol.iterator]();
|
||||||
const segments = [];
|
const segments: Segment<SegmentType>[] = [];
|
||||||
let next;
|
let next;
|
||||||
while (!(next = iterator.next()).done) {
|
while (!(next = iterator.next()).done) {
|
||||||
const type = next.value;
|
const type = next.value;
|
||||||
|
@ -170,9 +189,9 @@ export function parseUrlPath(urlPath, currentNavPath, defaultSessionId) {
|
||||||
return segments;
|
return segments;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stringifyPath(path) {
|
export function stringifyPath(path: Path<SegmentType>): string {
|
||||||
let urlPath = "";
|
let urlPath = "";
|
||||||
let prevSegment;
|
let prevSegment: Segment<SegmentType> | undefined;
|
||||||
for (const segment of path.segments) {
|
for (const segment of path.segments) {
|
||||||
switch (segment.type) {
|
switch (segment.type) {
|
||||||
case "rooms":
|
case "rooms":
|
||||||
|
@ -205,9 +224,15 @@ export function stringifyPath(path) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tests() {
|
export function tests() {
|
||||||
|
function createEmptyPath() {
|
||||||
|
const nav: Navigation<SegmentType> = new Navigation(allowsChild);
|
||||||
|
const path = nav.pathFrom([]);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"stringify grid url with focused empty tile": assert => {
|
"stringify grid url with focused empty tile": assert => {
|
||||||
const nav = new Navigation(allowsChild);
|
const nav: Navigation<SegmentType> = new Navigation(allowsChild);
|
||||||
const path = nav.pathFrom([
|
const path = nav.pathFrom([
|
||||||
new Segment("session", 1),
|
new Segment("session", 1),
|
||||||
new Segment("rooms", ["a", "b", "c"]),
|
new Segment("rooms", ["a", "b", "c"]),
|
||||||
|
@ -217,7 +242,7 @@ export function tests() {
|
||||||
assert.equal(urlPath, "/session/1/rooms/a,b,c/3");
|
assert.equal(urlPath, "/session/1/rooms/a,b,c/3");
|
||||||
},
|
},
|
||||||
"stringify grid url with focused room": assert => {
|
"stringify grid url with focused room": assert => {
|
||||||
const nav = new Navigation(allowsChild);
|
const nav: Navigation<SegmentType> = new Navigation(allowsChild);
|
||||||
const path = nav.pathFrom([
|
const path = nav.pathFrom([
|
||||||
new Segment("session", 1),
|
new Segment("session", 1),
|
||||||
new Segment("rooms", ["a", "b", "c"]),
|
new Segment("rooms", ["a", "b", "c"]),
|
||||||
|
@ -227,7 +252,7 @@ export function tests() {
|
||||||
assert.equal(urlPath, "/session/1/rooms/a,b,c/1");
|
assert.equal(urlPath, "/session/1/rooms/a,b,c/1");
|
||||||
},
|
},
|
||||||
"stringify url with right-panel and details segment": assert => {
|
"stringify url with right-panel and details segment": assert => {
|
||||||
const nav = new Navigation(allowsChild);
|
const nav: Navigation<SegmentType> = new Navigation(allowsChild);
|
||||||
const path = nav.pathFrom([
|
const path = nav.pathFrom([
|
||||||
new Segment("session", 1),
|
new Segment("session", 1),
|
||||||
new Segment("rooms", ["a", "b", "c"]),
|
new Segment("rooms", ["a", "b", "c"]),
|
||||||
|
@ -239,13 +264,15 @@ export function tests() {
|
||||||
assert.equal(urlPath, "/session/1/rooms/a,b,c/1/details");
|
assert.equal(urlPath, "/session/1/rooms/a,b,c/1/details");
|
||||||
},
|
},
|
||||||
"Parse loginToken query parameter into SSO segment": assert => {
|
"Parse loginToken query parameter into SSO segment": assert => {
|
||||||
const segments = parseUrlPath("?loginToken=a1232aSD123");
|
const path = createEmptyPath();
|
||||||
|
const segments = parseUrlPath("?loginToken=a1232aSD123", path);
|
||||||
assert.equal(segments.length, 1);
|
assert.equal(segments.length, 1);
|
||||||
assert.equal(segments[0].type, "sso");
|
assert.equal(segments[0].type, "sso");
|
||||||
assert.equal(segments[0].value, "a1232aSD123");
|
assert.equal(segments[0].value, "a1232aSD123");
|
||||||
},
|
},
|
||||||
"parse grid url path with focused empty tile": assert => {
|
"parse grid url path with focused empty tile": assert => {
|
||||||
const segments = parseUrlPath("/session/1/rooms/a,b,c/3");
|
const path = createEmptyPath();
|
||||||
|
const segments = parseUrlPath("/session/1/rooms/a,b,c/3", path);
|
||||||
assert.equal(segments.length, 3);
|
assert.equal(segments.length, 3);
|
||||||
assert.equal(segments[0].type, "session");
|
assert.equal(segments[0].type, "session");
|
||||||
assert.equal(segments[0].value, "1");
|
assert.equal(segments[0].value, "1");
|
||||||
|
@ -255,7 +282,8 @@ export function tests() {
|
||||||
assert.equal(segments[2].value, 3);
|
assert.equal(segments[2].value, 3);
|
||||||
},
|
},
|
||||||
"parse grid url path with focused room": assert => {
|
"parse grid url path with focused room": assert => {
|
||||||
const segments = parseUrlPath("/session/1/rooms/a,b,c/1");
|
const path = createEmptyPath();
|
||||||
|
const segments = parseUrlPath("/session/1/rooms/a,b,c/1", path);
|
||||||
assert.equal(segments.length, 3);
|
assert.equal(segments.length, 3);
|
||||||
assert.equal(segments[0].type, "session");
|
assert.equal(segments[0].type, "session");
|
||||||
assert.equal(segments[0].value, "1");
|
assert.equal(segments[0].value, "1");
|
||||||
|
@ -265,7 +293,8 @@ export function tests() {
|
||||||
assert.equal(segments[2].value, "b");
|
assert.equal(segments[2].value, "b");
|
||||||
},
|
},
|
||||||
"parse empty grid url": assert => {
|
"parse empty grid url": assert => {
|
||||||
const segments = parseUrlPath("/session/1/rooms/");
|
const path = createEmptyPath();
|
||||||
|
const segments = parseUrlPath("/session/1/rooms/", path);
|
||||||
assert.equal(segments.length, 3);
|
assert.equal(segments.length, 3);
|
||||||
assert.equal(segments[0].type, "session");
|
assert.equal(segments[0].type, "session");
|
||||||
assert.equal(segments[0].value, "1");
|
assert.equal(segments[0].value, "1");
|
||||||
|
@ -275,7 +304,8 @@ export function tests() {
|
||||||
assert.equal(segments[2].value, 0);
|
assert.equal(segments[2].value, 0);
|
||||||
},
|
},
|
||||||
"parse empty grid url with focus": assert => {
|
"parse empty grid url with focus": assert => {
|
||||||
const segments = parseUrlPath("/session/1/rooms//1");
|
const path = createEmptyPath();
|
||||||
|
const segments = parseUrlPath("/session/1/rooms//1", path);
|
||||||
assert.equal(segments.length, 3);
|
assert.equal(segments.length, 3);
|
||||||
assert.equal(segments[0].type, "session");
|
assert.equal(segments[0].type, "session");
|
||||||
assert.equal(segments[0].value, "1");
|
assert.equal(segments[0].value, "1");
|
||||||
|
@ -285,7 +315,7 @@ export function tests() {
|
||||||
assert.equal(segments[2].value, 1);
|
assert.equal(segments[2].value, 1);
|
||||||
},
|
},
|
||||||
"parse open-room action replacing the current focused room": assert => {
|
"parse open-room action replacing the current focused room": assert => {
|
||||||
const nav = new Navigation(allowsChild);
|
const nav: Navigation<SegmentType> = new Navigation(allowsChild);
|
||||||
const path = nav.pathFrom([
|
const path = nav.pathFrom([
|
||||||
new Segment("session", 1),
|
new Segment("session", 1),
|
||||||
new Segment("rooms", ["a", "b", "c"]),
|
new Segment("rooms", ["a", "b", "c"]),
|
||||||
|
@ -301,7 +331,7 @@ export function tests() {
|
||||||
assert.equal(segments[2].value, "d");
|
assert.equal(segments[2].value, "d");
|
||||||
},
|
},
|
||||||
"parse open-room action changing focus to an existing room": assert => {
|
"parse open-room action changing focus to an existing room": assert => {
|
||||||
const nav = new Navigation(allowsChild);
|
const nav: Navigation<SegmentType> = new Navigation(allowsChild);
|
||||||
const path = nav.pathFrom([
|
const path = nav.pathFrom([
|
||||||
new Segment("session", 1),
|
new Segment("session", 1),
|
||||||
new Segment("rooms", ["a", "b", "c"]),
|
new Segment("rooms", ["a", "b", "c"]),
|
||||||
|
@ -317,7 +347,7 @@ export function tests() {
|
||||||
assert.equal(segments[2].value, "a");
|
assert.equal(segments[2].value, "a");
|
||||||
},
|
},
|
||||||
"parse open-room action changing focus to an existing room with details open": assert => {
|
"parse open-room action changing focus to an existing room with details open": assert => {
|
||||||
const nav = new Navigation(allowsChild);
|
const nav: Navigation<SegmentType> = new Navigation(allowsChild);
|
||||||
const path = nav.pathFrom([
|
const path = nav.pathFrom([
|
||||||
new Segment("session", 1),
|
new Segment("session", 1),
|
||||||
new Segment("rooms", ["a", "b", "c"]),
|
new Segment("rooms", ["a", "b", "c"]),
|
||||||
|
@ -339,7 +369,7 @@ export function tests() {
|
||||||
assert.equal(segments[4].value, true);
|
assert.equal(segments[4].value, true);
|
||||||
},
|
},
|
||||||
"open-room action should only copy over previous segments if there are no parts after open-room": assert => {
|
"open-room action should only copy over previous segments if there are no parts after open-room": assert => {
|
||||||
const nav = new Navigation(allowsChild);
|
const nav: Navigation<SegmentType> = new Navigation(allowsChild);
|
||||||
const path = nav.pathFrom([
|
const path = nav.pathFrom([
|
||||||
new Segment("session", 1),
|
new Segment("session", 1),
|
||||||
new Segment("rooms", ["a", "b", "c"]),
|
new Segment("rooms", ["a", "b", "c"]),
|
||||||
|
@ -361,7 +391,7 @@ export function tests() {
|
||||||
assert.equal(segments[4].value, "foo");
|
assert.equal(segments[4].value, "foo");
|
||||||
},
|
},
|
||||||
"parse open-room action setting a room in an empty tile": assert => {
|
"parse open-room action setting a room in an empty tile": assert => {
|
||||||
const nav = new Navigation(allowsChild);
|
const nav: Navigation<SegmentType> = new Navigation(allowsChild);
|
||||||
const path = nav.pathFrom([
|
const path = nav.pathFrom([
|
||||||
new Segment("session", 1),
|
new Segment("session", 1),
|
||||||
new Segment("rooms", ["a", "b", "c"]),
|
new Segment("rooms", ["a", "b", "c"]),
|
||||||
|
@ -377,82 +407,83 @@ export function tests() {
|
||||||
assert.equal(segments[2].value, "d");
|
assert.equal(segments[2].value, "d");
|
||||||
},
|
},
|
||||||
"parse session url path without id": assert => {
|
"parse session url path without id": assert => {
|
||||||
const segments = parseUrlPath("/session");
|
const path = createEmptyPath();
|
||||||
|
const segments = parseUrlPath("/session", path);
|
||||||
assert.equal(segments.length, 1);
|
assert.equal(segments.length, 1);
|
||||||
assert.equal(segments[0].type, "session");
|
assert.equal(segments[0].type, "session");
|
||||||
assert.strictEqual(segments[0].value, true);
|
assert.strictEqual(segments[0].value, true);
|
||||||
},
|
},
|
||||||
"remove active room from grid path turns it into empty tile": assert => {
|
"remove active room from grid path turns it into empty tile": assert => {
|
||||||
const nav = new Navigation(allowsChild);
|
const nav: Navigation<SegmentType> = new Navigation(allowsChild);
|
||||||
const path = nav.pathFrom([
|
const path = nav.pathFrom([
|
||||||
new Segment("session", 1),
|
new Segment("session", 1),
|
||||||
new Segment("rooms", ["a", "b", "c"]),
|
new Segment("rooms", ["a", "b", "c"]),
|
||||||
new Segment("room", "b")
|
new Segment("room", "b")
|
||||||
]);
|
]);
|
||||||
const newPath = removeRoomFromPath(path, "b");
|
const newPath = removeRoomFromPath(path, "b");
|
||||||
assert.equal(newPath.segments.length, 3);
|
assert.equal(newPath?.segments.length, 3);
|
||||||
assert.equal(newPath.segments[0].type, "session");
|
assert.equal(newPath?.segments[0].type, "session");
|
||||||
assert.equal(newPath.segments[0].value, 1);
|
assert.equal(newPath?.segments[0].value, 1);
|
||||||
assert.equal(newPath.segments[1].type, "rooms");
|
assert.equal(newPath?.segments[1].type, "rooms");
|
||||||
assert.deepEqual(newPath.segments[1].value, ["a", "", "c"]);
|
assert.deepEqual(newPath?.segments[1].value, ["a", "", "c"]);
|
||||||
assert.equal(newPath.segments[2].type, "empty-grid-tile");
|
assert.equal(newPath?.segments[2].type, "empty-grid-tile");
|
||||||
assert.equal(newPath.segments[2].value, 1);
|
assert.equal(newPath?.segments[2].value, 1);
|
||||||
},
|
},
|
||||||
"remove inactive room from grid path": assert => {
|
"remove inactive room from grid path": assert => {
|
||||||
const nav = new Navigation(allowsChild);
|
const nav: Navigation<SegmentType> = new Navigation(allowsChild);
|
||||||
const path = nav.pathFrom([
|
const path = nav.pathFrom([
|
||||||
new Segment("session", 1),
|
new Segment("session", 1),
|
||||||
new Segment("rooms", ["a", "b", "c"]),
|
new Segment("rooms", ["a", "b", "c"]),
|
||||||
new Segment("room", "b")
|
new Segment("room", "b")
|
||||||
]);
|
]);
|
||||||
const newPath = removeRoomFromPath(path, "a");
|
const newPath = removeRoomFromPath(path, "a");
|
||||||
assert.equal(newPath.segments.length, 3);
|
assert.equal(newPath?.segments.length, 3);
|
||||||
assert.equal(newPath.segments[0].type, "session");
|
assert.equal(newPath?.segments[0].type, "session");
|
||||||
assert.equal(newPath.segments[0].value, 1);
|
assert.equal(newPath?.segments[0].value, 1);
|
||||||
assert.equal(newPath.segments[1].type, "rooms");
|
assert.equal(newPath?.segments[1].type, "rooms");
|
||||||
assert.deepEqual(newPath.segments[1].value, ["", "b", "c"]);
|
assert.deepEqual(newPath?.segments[1].value, ["", "b", "c"]);
|
||||||
assert.equal(newPath.segments[2].type, "room");
|
assert.equal(newPath?.segments[2].type, "room");
|
||||||
assert.equal(newPath.segments[2].value, "b");
|
assert.equal(newPath?.segments[2].value, "b");
|
||||||
},
|
},
|
||||||
"remove inactive room from grid path with empty tile": assert => {
|
"remove inactive room from grid path with empty tile": assert => {
|
||||||
const nav = new Navigation(allowsChild);
|
const nav: Navigation<SegmentType> = new Navigation(allowsChild);
|
||||||
const path = nav.pathFrom([
|
const path = nav.pathFrom([
|
||||||
new Segment("session", 1),
|
new Segment("session", 1),
|
||||||
new Segment("rooms", ["a", "b", ""]),
|
new Segment("rooms", ["a", "b", ""]),
|
||||||
new Segment("empty-grid-tile", 3)
|
new Segment("empty-grid-tile", 3)
|
||||||
]);
|
]);
|
||||||
const newPath = removeRoomFromPath(path, "b");
|
const newPath = removeRoomFromPath(path, "b");
|
||||||
assert.equal(newPath.segments.length, 3);
|
assert.equal(newPath?.segments.length, 3);
|
||||||
assert.equal(newPath.segments[0].type, "session");
|
assert.equal(newPath?.segments[0].type, "session");
|
||||||
assert.equal(newPath.segments[0].value, 1);
|
assert.equal(newPath?.segments[0].value, 1);
|
||||||
assert.equal(newPath.segments[1].type, "rooms");
|
assert.equal(newPath?.segments[1].type, "rooms");
|
||||||
assert.deepEqual(newPath.segments[1].value, ["a", "", ""]);
|
assert.deepEqual(newPath?.segments[1].value, ["a", "", ""]);
|
||||||
assert.equal(newPath.segments[2].type, "empty-grid-tile");
|
assert.equal(newPath?.segments[2].type, "empty-grid-tile");
|
||||||
assert.equal(newPath.segments[2].value, 3);
|
assert.equal(newPath?.segments[2].value, 3);
|
||||||
},
|
},
|
||||||
"remove active room": assert => {
|
"remove active room": assert => {
|
||||||
const nav = new Navigation(allowsChild);
|
const nav: Navigation<SegmentType> = new Navigation(allowsChild);
|
||||||
const path = nav.pathFrom([
|
const path = nav.pathFrom([
|
||||||
new Segment("session", 1),
|
new Segment("session", 1),
|
||||||
new Segment("room", "b")
|
new Segment("room", "b")
|
||||||
]);
|
]);
|
||||||
const newPath = removeRoomFromPath(path, "b");
|
const newPath = removeRoomFromPath(path, "b");
|
||||||
assert.equal(newPath.segments.length, 1);
|
assert.equal(newPath?.segments.length, 1);
|
||||||
assert.equal(newPath.segments[0].type, "session");
|
assert.equal(newPath?.segments[0].type, "session");
|
||||||
assert.equal(newPath.segments[0].value, 1);
|
assert.equal(newPath?.segments[0].value, 1);
|
||||||
},
|
},
|
||||||
"remove inactive room doesn't do anything": assert => {
|
"remove inactive room doesn't do anything": assert => {
|
||||||
const nav = new Navigation(allowsChild);
|
const nav: Navigation<SegmentType> = new Navigation(allowsChild);
|
||||||
const path = nav.pathFrom([
|
const path = nav.pathFrom([
|
||||||
new Segment("session", 1),
|
new Segment("session", 1),
|
||||||
new Segment("room", "b")
|
new Segment("room", "b")
|
||||||
]);
|
]);
|
||||||
const newPath = removeRoomFromPath(path, "a");
|
const newPath = removeRoomFromPath(path, "a");
|
||||||
assert.equal(newPath.segments.length, 2);
|
assert.equal(newPath?.segments.length, 2);
|
||||||
assert.equal(newPath.segments[0].type, "session");
|
assert.equal(newPath?.segments[0].type, "session");
|
||||||
assert.equal(newPath.segments[0].value, 1);
|
assert.equal(newPath?.segments[0].value, 1);
|
||||||
assert.equal(newPath.segments[1].type, "room");
|
assert.equal(newPath?.segments[1].type, "room");
|
||||||
assert.equal(newPath.segments[1].value, "b");
|
assert.equal(newPath?.segments[1].value, "b");
|
||||||
},
|
},
|
||||||
|
|
||||||
}
|
}
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {ViewModel} from "../ViewModel";
|
import {ViewModel} from "../ViewModel";
|
||||||
import {addPanelIfNeeded} from "../navigation/index.js";
|
import {addPanelIfNeeded} from "../navigation/index";
|
||||||
|
|
||||||
function dedupeSparse(roomIds) {
|
function dedupeSparse(roomIds) {
|
||||||
return roomIds.map((id, idx) => {
|
return roomIds.map((id, idx) => {
|
||||||
|
@ -185,7 +185,7 @@ export class RoomGridViewModel extends ViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
import {createNavigation} from "../navigation/index.js";
|
import {createNavigation} from "../navigation/index";
|
||||||
import {ObservableValue} from "../../observable/ObservableValue";
|
import {ObservableValue} from "../../observable/ObservableValue";
|
||||||
|
|
||||||
export function tests() {
|
export function tests() {
|
||||||
|
|
|
@ -21,7 +21,7 @@ import {InviteTileViewModel} from "./InviteTileViewModel.js";
|
||||||
import {RoomBeingCreatedTileViewModel} from "./RoomBeingCreatedTileViewModel.js";
|
import {RoomBeingCreatedTileViewModel} from "./RoomBeingCreatedTileViewModel.js";
|
||||||
import {RoomFilter} from "./RoomFilter.js";
|
import {RoomFilter} from "./RoomFilter.js";
|
||||||
import {ApplyMap} from "../../../observable/map/ApplyMap.js";
|
import {ApplyMap} from "../../../observable/map/ApplyMap.js";
|
||||||
import {addPanelIfNeeded} from "../../navigation/index.js";
|
import {addPanelIfNeeded} from "../../navigation/index";
|
||||||
|
|
||||||
export class LeftPanelViewModel extends ViewModel {
|
export class LeftPanelViewModel extends ViewModel {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
|
|
|
@ -18,7 +18,7 @@ export {Platform} from "./platform/web/Platform.js";
|
||||||
export {Client, LoadStatus} from "./matrix/Client.js";
|
export {Client, LoadStatus} from "./matrix/Client.js";
|
||||||
export {RoomStatus} from "./matrix/room/common";
|
export {RoomStatus} from "./matrix/room/common";
|
||||||
// export main view & view models
|
// export main view & view models
|
||||||
export {createNavigation, createRouter} from "./domain/navigation/index.js";
|
export {createNavigation, createRouter} from "./domain/navigation/index";
|
||||||
export {RootViewModel} from "./domain/RootViewModel.js";
|
export {RootViewModel} from "./domain/RootViewModel.js";
|
||||||
export {RootView} from "./platform/web/ui/RootView.js";
|
export {RootView} from "./platform/web/ui/RootView.js";
|
||||||
export {SessionViewModel} from "./domain/session/SessionViewModel.js";
|
export {SessionViewModel} from "./domain/session/SessionViewModel.js";
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
|
|
||||||
// import {RecordRequester, ReplayRequester} from "./matrix/net/request/replay";
|
// import {RecordRequester, ReplayRequester} from "./matrix/net/request/replay";
|
||||||
import {RootViewModel} from "../../domain/RootViewModel.js";
|
import {RootViewModel} from "../../domain/RootViewModel.js";
|
||||||
import {createNavigation, createRouter} from "../../domain/navigation/index.js";
|
import {createNavigation, createRouter} from "../../domain/navigation/index";
|
||||||
// Don't use a default export here, as we use multiple entries during legacy build,
|
// Don't use a default export here, as we use multiple entries during legacy build,
|
||||||
// which does not support default exports,
|
// which does not support default exports,
|
||||||
// see https://github.com/rollup/plugins/tree/master/packages/multi-entry
|
// see https://github.com/rollup/plugins/tree/master/packages/multi-entry
|
||||||
|
|
Reference in a new issue