Merge pull request #676 from vector-im/ts-conversion-domain-navigation

Convert /domain/navigation to typescript
This commit is contained in:
Bruno Windels 2022-07-29 14:21:17 +00:00 committed by GitHub
commit b40ce6137e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 268 additions and 165 deletions

View File

@ -16,10 +16,11 @@ limitations under the License.
import {Options as BaseOptions, ViewModel} from "./ViewModel";
import {Client} from "../matrix/Client.js";
import {SegmentType} from "./navigation/index";
type Options = { sessionId: string; } & BaseOptions;
export class LogoutViewModel extends ViewModel<Options> {
export class LogoutViewModel extends ViewModel<SegmentType, Options> {
private _sessionId: string;
private _busy: boolean;
private _showConfirm: boolean;
@ -41,7 +42,7 @@ export class LogoutViewModel extends ViewModel<Options> {
return this._busy;
}
get cancelUrl(): string {
get cancelUrl(): string | undefined {
return this.urlCreator.urlForSegment("session", true);
}

View File

@ -27,17 +27,19 @@ import type {Platform} from "../platform/web/Platform";
import type {Clock} from "../platform/web/dom/Clock";
import type {ILogger} from "../logging/types";
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 = {
platform: Platform
logger: ILogger
urlCreator: URLRouter
navigation: Navigation
emitChange?: (params: any) => void
export type Options<T extends object = SegmentType> = {
platform: Platform;
logger: ILogger;
urlCreator: IURLRouter<T>;
navigation: Navigation<T>;
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 _isDisposed = false;
private _options: Readonly<O>;
@ -47,7 +49,7 @@ export class ViewModel<O extends Options = Options> extends EventEmitter<{change
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);
}
@ -58,9 +60,9 @@ export class ViewModel<O extends Options = Options> extends EventEmitter<{change
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 unsubscribe = segmentObservable.subscribe((value: string | true | undefined) => {
const unsubscribe = segmentObservable.subscribe((value: N[T]) => {
onChange(value, type);
});
this.track(unsubscribe);
@ -135,11 +137,12 @@ export class ViewModel<O extends Options = Options> extends EventEmitter<{change
return this.platform.logger;
}
get urlCreator(): URLRouter {
get urlCreator(): IURLRouter<N> {
return this._options.urlCreator;
}
get navigation(): Navigation {
return this._options.navigation;
get navigation(): Navigation<N> {
// typescript needs a little help here
return this._options.navigation as unknown as Navigation<N>;
}
}

View File

@ -21,6 +21,8 @@ import {StartSSOLoginViewModel} from "./StartSSOLoginViewModel.js";
import {CompleteSSOLoginViewModel} from "./CompleteSSOLoginViewModel.js";
import {LoadStatus} from "../../matrix/Client.js";
import {SessionLoadViewModel} from "../SessionLoadViewModel.js";
import {SegmentType} from "../navigation/index";
import type {PasswordLoginMethod, SSOLoginHelper, TokenLoginMethod, ILoginMethod} from "../../matrix/login";
type Options = {
@ -29,7 +31,7 @@ type Options = {
loginToken?: string;
} & BaseOptions;
export class LoginViewModel extends ViewModel<Options> {
export class LoginViewModel extends ViewModel<SegmentType, Options> {
private _ready: ReadyFn;
private _loginToken?: string;
private _client: Client;

View File

@ -16,27 +16,49 @@ limitations under the License.
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._path = new Path([], allowsChild);
this._observables = new Map();
this._pathObservable = new ObservableValue(this._path);
}
get pathObservable() {
get pathObservable(): ObservableValue<Path<T>> {
return this._pathObservable;
}
get path() {
get path(): Path<T> {
return this._path;
}
push(type, value = undefined) {
return this.applyPath(this.path.with(new Segment(type, value)));
push<K extends keyof T>(type: K, ...value: OptionalValue<T[K]>): void {
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,
// so we assume it respects the allowsChild rules
const oldPath = this._path;
@ -60,7 +82,7 @@ export class Navigation {
this._pathObservable.set(this._path);
}
observe(type) {
observe(type: keyof T): SegmentObservable<T> {
let observable = this._observables.get(type);
if (!observable) {
observable = new SegmentObservable(this, type);
@ -69,9 +91,9 @@ export class Navigation {
return observable;
}
pathFrom(segments) {
let parent;
let i;
pathFrom(segments: Segment<any>[]): Path<T> {
let parent: Segment<any> | undefined;
let i: number;
for (i = 0; i < segments.length; i += 1) {
if (!this._allowsChild(parent, segments[i])) {
return new Path(segments.slice(0, i), this._allowsChild);
@ -81,12 +103,12 @@ export class Navigation {
return new Path(segments, this._allowsChild);
}
segment(type, value) {
return new Segment(type, value);
segment<K extends keyof T>(type: K, ...value: OptionalValue<T[K]>): Segment<T> {
return new Segment(type, ...value);
}
}
function segmentValueEqual(a, b) {
function segmentValueEqual<T>(a?: T[keyof T], b?: T[keyof T]): boolean {
if (a === b) {
return true;
}
@ -103,24 +125,29 @@ function segmentValueEqual(a, b) {
return false;
}
export class Segment {
constructor(type, value) {
this.type = type;
this.value = value === undefined ? true : value;
export class Segment<T, K extends keyof T = any> {
public value: T[K];
constructor(public type: K, ...value: OptionalValue<T[K]>) {
this.value = (value[0] === undefined ? true : value[0]) as unknown as T[K];
}
}
class Path {
constructor(segments = [], allowsChild) {
class Path<T> {
private readonly _segments: Segment<T, any>[];
private readonly _allowsChild: AllowsChild<T>;
constructor(segments: Segment<T>[] = [], allowsChild: AllowsChild<T>) {
this._segments = segments;
this._allowsChild = allowsChild;
}
clone() {
clone(): Path<T> {
return new Path(this._segments.slice(), this._allowsChild);
}
with(segment) {
with(segment: Segment<T>): Path<T> | undefined {
let index = this._segments.length - 1;
do {
if (this._allowsChild(this._segments[index], segment)) {
@ -132,10 +159,10 @@ class Path {
index -= 1;
} while(index >= -1);
// 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);
if (index !== -1) {
return new Path(this._segments.slice(0, index + 1), this._allowsChild)
@ -143,11 +170,11 @@ class Path {
return new Path([], this._allowsChild);
}
get(type) {
get(type: keyof T): Segment<T> | undefined {
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);
if (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;
}
}
@ -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.
* This ensures that observers of a segment can also read the most recent value of other segments.
*/
class SegmentObservable extends BaseObservableValue {
constructor(navigation, type) {
class SegmentObservable<T extends object> extends BaseObservableValue<T[keyof T] | undefined> {
private readonly _navigation: Navigation<T>;
private _type: keyof T;
private _lastSetValue?: T[keyof T];
constructor(navigation: Navigation<T>, type: keyof T) {
super();
this._navigation = navigation;
this._type = type;
this._lastSetValue = navigation.path.get(type)?.value;
}
get() {
get(): T[keyof T] | undefined {
const path = this._navigation.path;
const segment = path.get(this._type);
const value = segment?.value;
return value;
}
emitIfChanged() {
emitIfChanged(): void {
const newValue = this.get();
if (!segmentValueEqual(newValue, this._lastSetValue)) {
if (!segmentValueEqual<T>(newValue, this._lastSetValue)) {
this._lastSetValue = newValue;
this.emit(newValue);
}
}
}
export type {Path};
export function tests() {
function createMockNavigation() {
return new Navigation((parent, {type}) => {
switch (parent?.type) {
case undefined:
return type === "1" || "2";
return type === "1" || type === "2";
case "1":
return type === "1.1";
case "1.1":
return type === "1.1.1";
case "2":
return type === "2.1" || "2.2";
return type === "2.1" || type === "2.2";
default:
return false;
}
@ -216,7 +249,7 @@ export function tests() {
}
function observeTypes(nav, types) {
const changes = [];
const changes: {type:string, value:any}[] = [];
for (const type of types) {
nav.observe(type).subscribe(value => {
changes.push({type, value});
@ -225,6 +258,12 @@ export function tests() {
return changes;
}
type SegmentType = {
"foo": number;
"bar": number;
"baz": number;
}
return {
"applying a path emits an event on the observable": assert => {
const nav = createMockNavigation();
@ -242,18 +281,18 @@ export function tests() {
assert.equal(changes[1].value, 8);
},
"path.get": assert => {
const path = new Path([new Segment("foo", 5), new Segment("bar", 6)], () => true);
assert.equal(path.get("foo").value, 5);
assert.equal(path.get("bar").value, 6);
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("bar")!.value, 6);
},
"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));
assert.equal(newPath.get("foo").value, 1);
assert.equal(newPath.get("bar").value, 6);
assert.equal(newPath!.get("foo")!.value, 1);
assert.equal(newPath!.get("bar")!.value, 6);
},
"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));
assert.equal(newPath, null);
}

View File

@ -14,28 +14,55 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
export class URLRouter {
constructor({history, navigation, parseUrlPath, stringifyPath}) {
import type {History} from "../../platform/web/dom/History.js";
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._navigation = navigation;
this._parseUrlPath = parseUrlPath;
this._stringifyPath = stringifyPath;
this._subscription = null;
this._pathSubscription = null;
this._isApplyingUrl = false;
this._defaultSessionId = this._getLastSessionId();
}
_getLastSessionId() {
private _getLastSessionId(): string | undefined {
const navPath = this._urlAsNavPath(this._history.getLastSessionUrl() || "");
const sessionId = navPath.get("session")?.value;
if (typeof sessionId === "string") {
return sessionId;
}
return null;
return undefined;
}
attach() {
attach(): void {
this._subscription = this._history.subscribe(url => this._applyUrl(url));
// subscribe to path before applying initial url
// so redirects in _applyNavPathToHistory are reflected in url bar
@ -43,12 +70,12 @@ export class URLRouter {
this._applyUrl(this._history.get());
}
dispose() {
this._subscription = this._subscription();
this._pathSubscription = this._pathSubscription();
dispose(): void {
if (this._subscription) { this._subscription = this._subscription(); }
if (this._pathSubscription) { this._pathSubscription = this._pathSubscription(); }
}
_applyNavPathToHistory(path) {
private _applyNavPathToHistory(path: Path<T>): void {
const url = this.urlForPath(path);
if (url !== this._history.get()) {
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,
// 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)
@ -69,21 +96,21 @@ export class URLRouter {
this._isApplyingUrl = false;
}
_urlAsNavPath(url) {
private _urlAsNavPath(url: string): Path<T> {
const urlPath = this._history.urlAsPath(url);
return this._navigation.pathFrom(this._parseUrlPath(urlPath, this._navigation.path, this._defaultSessionId));
}
_applyUrl(url) {
private _applyUrl(url: string): void {
const navPath = this._urlAsNavPath(url);
this._applyNavPathToNavigation(navPath);
}
pushUrl(url) {
pushUrl(url: string): void {
this._history.pushUrl(url);
}
tryRestoreLastUrl() {
tryRestoreLastUrl(): boolean {
const lastNavPath = this._urlAsNavPath(this._history.getLastSessionUrl() || "");
if (lastNavPath.segments.length !== 0) {
this._applyNavPathToNavigation(lastNavPath);
@ -92,8 +119,8 @@ export class URLRouter {
return false;
}
urlForSegments(segments) {
let path = this._navigation.path;
urlForSegments(segments: Segment<T>[]): string | undefined {
let path: Path<T> | undefined = this._navigation.path;
for (const segment of segments) {
path = path.with(segment);
if (!path) {
@ -103,29 +130,29 @@ export class URLRouter {
return this.urlForPath(path);
}
urlForSegment(type, value) {
return this.urlForSegments([this._navigation.segment(type, value)]);
urlForSegment<K extends keyof T>(type: K, ...value: OptionalValue<T[K]>): string | undefined {
return this.urlForSegments([this._navigation.segment(type, ...value)]);
}
urlUntilSegment(type) {
urlUntilSegment(type: keyof T): string {
return this.urlForPath(this._navigation.path.until(type));
}
urlForPath(path) {
urlForPath(path: Path<T>): string {
return this._history.pathAsUrl(this._stringifyPath(path));
}
openRoomActionUrl(roomId) {
openRoomActionUrl(roomId: string): string {
// not a segment to navigation knowns about, so append it manually
const urlPath = `${this._stringifyPath(this._navigation.path.until("session"))}/open-room/${roomId}`;
return this._history.pathAsUrl(urlPath);
}
createSSOCallbackURL() {
createSSOCallbackURL(): string {
return window.location.origin;
}
normalizeUrl() {
normalizeUrl(): void {
// Remove any queryParameters from the URL
// Gets rid of the loginToken after SSO
this._history.replaceUrlSilently(`${window.location.origin}/${window.location.hash}`);

View File

@ -14,18 +14,36 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {Navigation, Segment} from "./Navigation.js";
import {URLRouter} from "./URLRouter.js";
import {Navigation, Segment} from "./Navigation";
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);
}
export function createRouter({history, navigation}) {
return new URLRouter({history, navigation, stringifyPath, parseUrlPath});
export function createRouter({history, navigation}: {history: History, navigation: Navigation<SegmentType>}): URLRouter<SegmentType> {
return new URLRouter(history, navigation, parseUrlPath, stringifyPath);
}
function allowsChild(parent, child) {
function allowsChild(parent: Segment<SegmentType> | undefined, child: Segment<SegmentType>): boolean {
const {type} = child;
switch (parent?.type) {
case undefined:
@ -45,8 +63,9 @@ function allowsChild(parent, child) {
}
}
export function removeRoomFromPath(path, roomId) {
const rooms = path.get("rooms");
export function removeRoomFromPath(path: Path<SegmentType>, roomId: string): Path<SegmentType> | undefined {
let newPath: Path<SegmentType> | undefined = path;
const rooms = newPath.get("rooms");
let roomIdGridIndex = -1;
// first delete from rooms segment
if (rooms) {
@ -54,22 +73,22 @@ export function removeRoomFromPath(path, roomId) {
if (roomIdGridIndex !== -1) {
const idsWithoutRoom = rooms.value.slice();
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)
if (room && room.value === roomId) {
if (roomIdGridIndex !== -1) {
path = path.with(new Segment("empty-grid-tile", roomIdGridIndex));
newPath = newPath!.with(new Segment("empty-grid-tile", roomIdGridIndex));
} 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)) {
const emptyGridTile = path.get("empty-grid-tile");
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(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 i = segments.findIndex(segment => segment.type === "right-panel");
let _path = path;
if (i !== -1) {
_path = path.until("room");
_path = _path.with(segments[i]);
_path = _path.with(segments[i + 1]);
_path = _path.with(segments[i])!;
_path = _path.with(segments[i + 1])!;
}
return _path;
}
export function parseUrlPath(urlPath, currentNavPath, defaultSessionId) {
// substr(1) to take of initial /
const parts = urlPath.substr(1).split("/");
export function parseUrlPath(urlPath: string, currentNavPath: Path<SegmentType>, defaultSessionId?: string): Segment<SegmentType>[] {
// substring(1) to take of initial /
const parts = urlPath.substring(1).split("/");
const iterator = parts[Symbol.iterator]();
const segments = [];
const segments: Segment<SegmentType>[] = [];
let next;
while (!(next = iterator.next()).done) {
const type = next.value;
@ -170,9 +189,9 @@ export function parseUrlPath(urlPath, currentNavPath, defaultSessionId) {
return segments;
}
export function stringifyPath(path) {
export function stringifyPath(path: Path<SegmentType>): string {
let urlPath = "";
let prevSegment;
let prevSegment: Segment<SegmentType> | undefined;
for (const segment of path.segments) {
switch (segment.type) {
case "rooms":
@ -205,9 +224,15 @@ export function stringifyPath(path) {
}
export function tests() {
function createEmptyPath() {
const nav: Navigation<SegmentType> = new Navigation(allowsChild);
const path = nav.pathFrom([]);
return path;
}
return {
"stringify grid url with focused empty tile": assert => {
const nav = new Navigation(allowsChild);
const nav: Navigation<SegmentType> = new Navigation(allowsChild);
const path = nav.pathFrom([
new Segment("session", 1),
new Segment("rooms", ["a", "b", "c"]),
@ -217,7 +242,7 @@ export function tests() {
assert.equal(urlPath, "/session/1/rooms/a,b,c/3");
},
"stringify grid url with focused room": assert => {
const nav = new Navigation(allowsChild);
const nav: Navigation<SegmentType> = new Navigation(allowsChild);
const path = nav.pathFrom([
new Segment("session", 1),
new Segment("rooms", ["a", "b", "c"]),
@ -227,7 +252,7 @@ export function tests() {
assert.equal(urlPath, "/session/1/rooms/a,b,c/1");
},
"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([
new Segment("session", 1),
new Segment("rooms", ["a", "b", "c"]),
@ -239,13 +264,15 @@ export function tests() {
assert.equal(urlPath, "/session/1/rooms/a,b,c/1/details");
},
"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[0].type, "sso");
assert.equal(segments[0].value, "a1232aSD123");
},
"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[0].type, "session");
assert.equal(segments[0].value, "1");
@ -255,7 +282,8 @@ export function tests() {
assert.equal(segments[2].value, 3);
},
"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[0].type, "session");
assert.equal(segments[0].value, "1");
@ -265,7 +293,8 @@ export function tests() {
assert.equal(segments[2].value, "b");
},
"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[0].type, "session");
assert.equal(segments[0].value, "1");
@ -275,7 +304,8 @@ export function tests() {
assert.equal(segments[2].value, 0);
},
"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[0].type, "session");
assert.equal(segments[0].value, "1");
@ -285,7 +315,7 @@ export function tests() {
assert.equal(segments[2].value, 1);
},
"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([
new Segment("session", 1),
new Segment("rooms", ["a", "b", "c"]),
@ -301,7 +331,7 @@ export function tests() {
assert.equal(segments[2].value, "d");
},
"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([
new Segment("session", 1),
new Segment("rooms", ["a", "b", "c"]),
@ -317,7 +347,7 @@ export function tests() {
assert.equal(segments[2].value, "a");
},
"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([
new Segment("session", 1),
new Segment("rooms", ["a", "b", "c"]),
@ -339,7 +369,7 @@ export function tests() {
assert.equal(segments[4].value, true);
},
"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([
new Segment("session", 1),
new Segment("rooms", ["a", "b", "c"]),
@ -361,7 +391,7 @@ export function tests() {
assert.equal(segments[4].value, "foo");
},
"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([
new Segment("session", 1),
new Segment("rooms", ["a", "b", "c"]),
@ -377,82 +407,83 @@ export function tests() {
assert.equal(segments[2].value, "d");
},
"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[0].type, "session");
assert.strictEqual(segments[0].value, true);
},
"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([
new Segment("session", 1),
new Segment("rooms", ["a", "b", "c"]),
new Segment("room", "b")
]);
const newPath = removeRoomFromPath(path, "b");
assert.equal(newPath.segments.length, 3);
assert.equal(newPath.segments[0].type, "session");
assert.equal(newPath.segments[0].value, 1);
assert.equal(newPath.segments[1].type, "rooms");
assert.deepEqual(newPath.segments[1].value, ["a", "", "c"]);
assert.equal(newPath.segments[2].type, "empty-grid-tile");
assert.equal(newPath.segments[2].value, 1);
assert.equal(newPath?.segments.length, 3);
assert.equal(newPath?.segments[0].type, "session");
assert.equal(newPath?.segments[0].value, 1);
assert.equal(newPath?.segments[1].type, "rooms");
assert.deepEqual(newPath?.segments[1].value, ["a", "", "c"]);
assert.equal(newPath?.segments[2].type, "empty-grid-tile");
assert.equal(newPath?.segments[2].value, 1);
},
"remove inactive room from grid path": assert => {
const nav = new Navigation(allowsChild);
const nav: Navigation<SegmentType> = new Navigation(allowsChild);
const path = nav.pathFrom([
new Segment("session", 1),
new Segment("rooms", ["a", "b", "c"]),
new Segment("room", "b")
]);
const newPath = removeRoomFromPath(path, "a");
assert.equal(newPath.segments.length, 3);
assert.equal(newPath.segments[0].type, "session");
assert.equal(newPath.segments[0].value, 1);
assert.equal(newPath.segments[1].type, "rooms");
assert.deepEqual(newPath.segments[1].value, ["", "b", "c"]);
assert.equal(newPath.segments[2].type, "room");
assert.equal(newPath.segments[2].value, "b");
assert.equal(newPath?.segments.length, 3);
assert.equal(newPath?.segments[0].type, "session");
assert.equal(newPath?.segments[0].value, 1);
assert.equal(newPath?.segments[1].type, "rooms");
assert.deepEqual(newPath?.segments[1].value, ["", "b", "c"]);
assert.equal(newPath?.segments[2].type, "room");
assert.equal(newPath?.segments[2].value, "b");
},
"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([
new Segment("session", 1),
new Segment("rooms", ["a", "b", ""]),
new Segment("empty-grid-tile", 3)
]);
const newPath = removeRoomFromPath(path, "b");
assert.equal(newPath.segments.length, 3);
assert.equal(newPath.segments[0].type, "session");
assert.equal(newPath.segments[0].value, 1);
assert.equal(newPath.segments[1].type, "rooms");
assert.deepEqual(newPath.segments[1].value, ["a", "", ""]);
assert.equal(newPath.segments[2].type, "empty-grid-tile");
assert.equal(newPath.segments[2].value, 3);
assert.equal(newPath?.segments.length, 3);
assert.equal(newPath?.segments[0].type, "session");
assert.equal(newPath?.segments[0].value, 1);
assert.equal(newPath?.segments[1].type, "rooms");
assert.deepEqual(newPath?.segments[1].value, ["a", "", ""]);
assert.equal(newPath?.segments[2].type, "empty-grid-tile");
assert.equal(newPath?.segments[2].value, 3);
},
"remove active room": assert => {
const nav = new Navigation(allowsChild);
const nav: Navigation<SegmentType> = new Navigation(allowsChild);
const path = nav.pathFrom([
new Segment("session", 1),
new Segment("room", "b")
]);
const newPath = removeRoomFromPath(path, "b");
assert.equal(newPath.segments.length, 1);
assert.equal(newPath.segments[0].type, "session");
assert.equal(newPath.segments[0].value, 1);
assert.equal(newPath?.segments.length, 1);
assert.equal(newPath?.segments[0].type, "session");
assert.equal(newPath?.segments[0].value, 1);
},
"remove inactive room doesn't do anything": assert => {
const nav = new Navigation(allowsChild);
const nav: Navigation<SegmentType> = new Navigation(allowsChild);
const path = nav.pathFrom([
new Segment("session", 1),
new Segment("room", "b")
]);
const newPath = removeRoomFromPath(path, "a");
assert.equal(newPath.segments.length, 2);
assert.equal(newPath.segments[0].type, "session");
assert.equal(newPath.segments[0].value, 1);
assert.equal(newPath.segments[1].type, "room");
assert.equal(newPath.segments[1].value, "b");
assert.equal(newPath?.segments.length, 2);
assert.equal(newPath?.segments[0].type, "session");
assert.equal(newPath?.segments[0].value, 1);
assert.equal(newPath?.segments[1].type, "room");
assert.equal(newPath?.segments[1].value, "b");
},
}

View File

@ -15,7 +15,7 @@ limitations under the License.
*/
import {ViewModel} from "../ViewModel";
import {addPanelIfNeeded} from "../navigation/index.js";
import {addPanelIfNeeded} from "../navigation/index";
function dedupeSparse(roomIds) {
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";
export function tests() {

View File

@ -21,7 +21,7 @@ import {InviteTileViewModel} from "./InviteTileViewModel.js";
import {RoomBeingCreatedTileViewModel} from "./RoomBeingCreatedTileViewModel.js";
import {RoomFilter} from "./RoomFilter.js";
import {ApplyMap} from "../../../observable/map/ApplyMap.js";
import {addPanelIfNeeded} from "../../navigation/index.js";
import {addPanelIfNeeded} from "../../navigation/index";
export class LeftPanelViewModel extends ViewModel {
constructor(options) {

View File

@ -18,7 +18,7 @@ export {Platform} from "./platform/web/Platform.js";
export {Client, LoadStatus} from "./matrix/Client.js";
export {RoomStatus} from "./matrix/room/common";
// 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 {RootView} from "./platform/web/ui/RootView.js";
export {SessionViewModel} from "./domain/session/SessionViewModel.js";

View File

@ -17,7 +17,7 @@ limitations under the License.
// import {RecordRequester, ReplayRequester} from "./matrix/net/request/replay";
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,
// which does not support default exports,
// see https://github.com/rollup/plugins/tree/master/packages/multi-entry