Improved golden layout config persistence. A prompt is now shown when the user tries to leave the page after making changes to the current quest. Set production log level to INFO.

This commit is contained in:
Daan Vanden Bosch 2019-12-22 15:30:16 +01:00
parent 5522e7c6af
commit 33026ce015
12 changed files with 165 additions and 79 deletions

View File

@ -1,2 +1,2 @@
LOG_LEVEL=WARN LOG_LEVEL=INFO
PUBLIC_URL=/assets PUBLIC_URL=/assets

View File

@ -33,6 +33,8 @@ export abstract class Persister {
private server_key(server: Server, key: string): string { private server_key(server: Server, key: string): string {
let k = key + "."; let k = key + ".";
// Do this manually per server type instead of just appending e.g. `Server[server]` to
// ensure the persisted key never changes.
switch (server) { switch (server) {
case Server.Ephinea: case Server.Ephinea:
k += "Ephinea"; k += "Ephinea";

View File

@ -1,4 +1,4 @@
import { Persister } from "../../core/persistence"; import { Persister } from "../../core/Persister";
import { Server } from "../../core/model"; import { Server } from "../../core/model";
import { HuntMethodModel } from "../model/HuntMethodModel"; import { HuntMethodModel } from "../model/HuntMethodModel";
import { Duration } from "luxon"; import { Duration } from "luxon";

View File

@ -1,5 +1,5 @@
import { Server } from "../../core/model"; import { Server } from "../../core/model";
import { Persister } from "../../core/persistence"; import { Persister } from "../../core/Persister";
import { WantedItemModel } from "../model"; import { WantedItemModel } from "../model";
import { ItemTypeStore } from "../../core/stores/ItemTypeStore"; import { ItemTypeStore } from "../../core/stores/ItemTypeStore";
import { ServerMap } from "../../core/stores/ServerMap"; import { ServerMap } from "../../core/stores/ServerMap";

View File

@ -10,7 +10,7 @@ import { WebGLRenderer } from "three";
import { DisposableThreeRenderer } from "./core/rendering/Renderer"; import { DisposableThreeRenderer } from "./core/rendering/Renderer";
Logger.useDefaults({ Logger.useDefaults({
defaultLevel: (Logger as any)[process.env["LOG_LEVEL"] ?? "OFF"], defaultLevel: (Logger as any)[process.env["LOG_LEVEL"] ?? "INFO"],
}); });
function create_three_renderer(): DisposableThreeRenderer { function create_three_renderer(): DisposableThreeRenderer {

View File

@ -1,8 +1,7 @@
import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { create_element, disposable_listener, el } from "../../core/gui/dom"; import { create_element, el } from "../../core/gui/dom";
import { QuestEditorToolBar } from "./QuestEditorToolBar"; import { QuestEditorToolBar } from "./QuestEditorToolBar";
import GoldenLayout, { Container, ContentItem, ItemConfigType } from "golden-layout"; import GoldenLayout, { Container, ContentItem, ItemConfigType } from "golden-layout";
import { quest_editor_ui_persister } from "../persistence/QuestEditorUiPersister";
import { QuestInfoView } from "./QuestInfoView"; import { QuestInfoView } from "./QuestInfoView";
import "golden-layout/src/css/goldenlayout-base.css"; import "golden-layout/src/css/goldenlayout-base.css";
import "../../core/gui/golden_layout_theme.css"; import "../../core/gui/golden_layout_theme.css";
@ -24,6 +23,7 @@ import { EntityImageRenderer } from "../rendering/EntityImageRenderer";
import { AreaAssetLoader } from "../loading/AreaAssetLoader"; import { AreaAssetLoader } from "../loading/AreaAssetLoader";
import { EntityAssetLoader } from "../loading/EntityAssetLoader"; import { EntityAssetLoader } from "../loading/EntityAssetLoader";
import { DisposableThreeRenderer } from "../../core/rendering/Renderer"; import { DisposableThreeRenderer } from "../../core/rendering/Renderer";
import { QuestEditorUiPersister } from "../persistence/QuestEditorUiPersister";
import Logger = require("js-logger"); import Logger = require("js-logger");
const logger = Logger.get("quest_editor/gui/QuestEditorView"); const logger = Logger.get("quest_editor/gui/QuestEditorView");
@ -56,8 +56,6 @@ export class QuestEditorView extends ResizableWidget {
{ name: string; create(): ResizableWidget } { name: string; create(): ResizableWidget }
>; >;
private readonly view_white_list: readonly string[];
private readonly tool_bar: QuestEditorToolBar; private readonly tool_bar: QuestEditorToolBar;
private readonly layout_element = create_element("div", { class: "quest_editor_gl_container" }); private readonly layout_element = create_element("div", { class: "quest_editor_gl_container" });
@ -74,6 +72,7 @@ export class QuestEditorView extends ResizableWidget {
area_asset_loader: AreaAssetLoader, area_asset_loader: AreaAssetLoader,
entity_asset_loader: EntityAssetLoader, entity_asset_loader: EntityAssetLoader,
entity_image_renderer: EntityImageRenderer, entity_image_renderer: EntityImageRenderer,
private readonly quest_editor_ui_persister: QuestEditorUiPersister,
create_three_renderer: () => DisposableThreeRenderer, create_three_renderer: () => DisposableThreeRenderer,
) { ) {
super(); super();
@ -163,10 +162,6 @@ export class QuestEditorView extends ResizableWidget {
}); });
} }
this.view_white_list = [...this.view_map.values()]
.map(({ name }) => name)
.filter(name => name !== "quest_runner");
this.tool_bar = this.disposable( this.tool_bar = this.disposable(
new QuestEditorToolBar(gui_store, area_store, quest_editor_store), new QuestEditorToolBar(gui_store, area_store, quest_editor_store),
); );
@ -213,14 +208,6 @@ export class QuestEditorView extends ResizableWidget {
} }
} }
}), }),
disposable_listener(window, "beforeunload", e => {
if (quest_editor_store.quest_runner.running.val) {
quest_editor_store.quest_runner.stop();
e.preventDefault();
e.returnValue = false;
}
}),
); );
this.finalize_construction(); this.finalize_construction();
@ -251,24 +238,31 @@ export class QuestEditorView extends ResizableWidget {
private async init_golden_layout(): Promise<GoldenLayout> { private async init_golden_layout(): Promise<GoldenLayout> {
const default_layout_content = this.get_default_layout_content(); const default_layout_content = this.get_default_layout_content();
const content = await quest_editor_ui_persister.load_layout_config(
this.view_white_list,
default_layout_content,
);
try { try {
return this.attempt_gl_init({ const content = await this.quest_editor_ui_persister.load_layout_config(
...DEFAULT_LAYOUT_CONFIG, default_layout_content,
content, );
});
if (content) {
const gl = this.attempt_gl_init({
...DEFAULT_LAYOUT_CONFIG,
content,
});
logger.info("Instantiated golden layout with persisted layout.");
return gl;
}
} catch (e) { } catch (e) {
logger.warn("Couldn't instantiate golden layout with persisted layout.", e); logger.warn("Couldn't instantiate golden layout with persisted layout.", e);
return this.attempt_gl_init({
...DEFAULT_LAYOUT_CONFIG,
content: default_layout_content,
});
} }
logger.info("Instantiating golden layout with default layout.");
return this.attempt_gl_init({
...DEFAULT_LAYOUT_CONFIG,
content: default_layout_content,
});
} }
private attempt_gl_init(config: GoldenLayout.Config): GoldenLayout { private attempt_gl_init(config: GoldenLayout.Config): GoldenLayout {
@ -298,7 +292,7 @@ export class QuestEditorView extends ResizableWidget {
} }
layout.on("stateChanged", () => { layout.on("stateChanged", () => {
quest_editor_ui_persister.persist_layout_config(layout.toConfig().content); this.quest_editor_ui_persister.persist_layout_config(layout.toConfig().content);
}); });
layout.on("stackCreated", (stack: ContentItem) => { layout.on("stackCreated", (stack: ContentItem) => {

View File

@ -8,6 +8,7 @@ import { HttpClient } from "../core/HttpClient";
import { EntityImageRenderer } from "./rendering/EntityImageRenderer"; import { EntityImageRenderer } from "./rendering/EntityImageRenderer";
import { EntityAssetLoader } from "./loading/EntityAssetLoader"; import { EntityAssetLoader } from "./loading/EntityAssetLoader";
import { DisposableThreeRenderer } from "../core/rendering/Renderer"; import { DisposableThreeRenderer } from "../core/rendering/Renderer";
import { QuestEditorUiPersister } from "./persistence/QuestEditorUiPersister";
export function initialize_quest_editor( export function initialize_quest_editor(
http_client: HttpClient, http_client: HttpClient,
@ -23,6 +24,9 @@ export function initialize_quest_editor(
const quest_editor_store = new QuestEditorStore(gui_store, area_store); const quest_editor_store = new QuestEditorStore(gui_store, area_store);
const asm_editor_store = new AsmEditorStore(quest_editor_store); const asm_editor_store = new AsmEditorStore(quest_editor_store);
// Persisters
const quest_editor_ui_persister = new QuestEditorUiPersister();
// Entity Image Renderer // Entity Image Renderer
const entity_image_renderer = new EntityImageRenderer(entity_asset_loader); const entity_image_renderer = new EntityImageRenderer(entity_asset_loader);
@ -35,6 +39,7 @@ export function initialize_quest_editor(
area_asset_loader, area_asset_loader,
entity_asset_loader, entity_asset_loader,
entity_image_renderer, entity_image_renderer,
quest_editor_ui_persister,
create_three_renderer, create_three_renderer,
); );
} }

View File

@ -1,74 +1,141 @@
import { Persister } from "../../core/persistence"; import { Persister } from "../../core/Persister";
import { throttle } from "lodash"; import { throttle } from "lodash";
import GoldenLayout from "golden-layout"; import { ComponentConfig, ItemConfigType } from "golden-layout";
const LAYOUT_CONFIG_KEY = "QuestEditorUiPersister.layout_config"; const LAYOUT_CONFIG_KEY = "QuestEditorUiPersister.layout_config";
export class QuestEditorUiPersister extends Persister { export class QuestEditorUiPersister extends Persister {
persist_layout_config = throttle( persist_layout_config = throttle(
(config: any) => { (config: ItemConfigType[]) => {
this.persist(LAYOUT_CONFIG_KEY, config); this.persist(LAYOUT_CONFIG_KEY, this.to_persisted_item_config(config));
}, },
500, 500,
{ leading: true, trailing: true }, { leading: false, trailing: true },
); );
async load_layout_config( async load_layout_config(
components: readonly string[], default_config: ItemConfigType[],
default_config: GoldenLayout.ItemConfigType[], ): Promise<ItemConfigType[] | undefined> {
): Promise<any> { const config = await this.load<ItemConfigType[]>(LAYOUT_CONFIG_KEY);
const config = await this.load<GoldenLayout.ItemConfigType[]>(LAYOUT_CONFIG_KEY);
if (config && this.verify_layout_config(config, components)) { if (config) {
return config; const components = this.extract_components(default_config);
} else { const verified_config = this.sanitized_layout_config(
return default_config; this.from_persisted_item_config(config),
} components,
} );
private verify_layout_config( if (verified_config) {
config: GoldenLayout.ItemConfigType[], return verified_config;
components: readonly string[],
): boolean {
const set = new Set(components);
for (const child of config) {
if (!this.verify_layout_child(child, set, new Set(), true)) {
return false;
} }
} }
return true; return undefined;
} }
private verify_layout_child( private sanitized_layout_config(
config: GoldenLayout.ItemConfigType, config: ItemConfigType[],
components: Set<string>, components: Map<string, ComponentConfig>,
): ItemConfigType[] | undefined {
const found = new Set<string>();
const sanitized_config = config.map(child =>
this.sanitize_layout_child(child, components, found),
);
if (found.size !== components.size) {
// A component was added, use default layout instead of persisted layout.
return undefined;
}
// Filter out removed components.
return sanitized_config.filter(item => item) as ItemConfigType[];
}
/**
* Removed old components and adds titles and ids to current components.
* Modifies the given ItemConfigType object.
*/
private sanitize_layout_child(
config: ItemConfigType,
components: Map<string, ComponentConfig>,
found: Set<string>, found: Set<string>,
first: boolean, ): ItemConfigType | undefined {
): boolean {
if (!config) { if (!config) {
return false; return undefined;
} }
if ("componentName" in config) { if (config.type === "component" && "componentName" in config) {
if (!components.has(config.componentName)) { const component = components.get(config.componentName);
return false;
} else { // Remove deprecated components.
found.add(config.componentName); if (!component) {
return undefined;
} }
found.add(config.componentName);
config.id = component.id;
config.title = component.title;
} }
if (config.content) { if (config.content) {
for (const child of config.content) { config.content = config.content
if (!this.verify_layout_child(child, components, found, false)) { .map(child => this.sanitize_layout_child(child, components, found))
return false; .filter(item => item) as ItemConfigType[];
} }
return config;
}
private extract_components(
config: ItemConfigType[],
map: Map<string, ComponentConfig> = new Map(),
): Map<string, ComponentConfig> {
for (const child of config) {
if ("componentName" in child) {
map.set(child.componentName, child);
}
if (child.content) {
this.extract_components(child.content, map);
} }
} }
return first ? components.size === found.size : true; return map;
}
private to_persisted_item_config(config: ItemConfigType[]): PersistedItemConfig[] {
return config.map(item => ({
id: item.id,
type: item.type,
componentName: "componentName" in item ? item.componentName : undefined,
width: item.width,
height: item.height,
content: item.content && this.to_persisted_item_config(item.content),
}));
}
/**
* This simply makes a copy to ensure legacy properties are removed.
*/
private from_persisted_item_config(config: PersistedItemConfig[]): ItemConfigType[] {
return config.map(item => ({
id: item.id,
type: item.type,
componentName: item.componentName,
width: item.width,
height: item.height,
content: item.content && this.from_persisted_item_config(item.content),
isClosable: false,
}));
} }
} }
export const quest_editor_ui_persister = new QuestEditorUiPersister(); type PersistedItemConfig = {
id?: string | string[];
type: string;
componentName?: string;
width?: number;
height?: number;
content?: PersistedItemConfig[];
};

View File

@ -17,7 +17,7 @@ import { Kind, OP_BB_MAP_DESIGNATE, Opcode, OPCODES_BY_MNEMONIC } from "./opcode
import { AssemblyLexer, IdentToken, TokenType } from "./AssemblyLexer"; import { AssemblyLexer, IdentToken, TokenType } from "./AssemblyLexer";
Logger.useDefaults({ Logger.useDefaults({
defaultLevel: (Logger as any)[process.env["LOG_LEVEL"] || "OFF"], defaultLevel: (Logger as any)[process.env["LOG_LEVEL"] || "INFO"],
}); });
const ctx: Worker = self as any; const ctx: Worker = self as any;

View File

@ -18,6 +18,7 @@ import LocationLink = languages.LocationLink;
import IModelContentChange = editor.IModelContentChange; import IModelContentChange = editor.IModelContentChange;
import { Breakpoint } from "../scripting/vm/Debugger"; import { Breakpoint } from "../scripting/vm/Debugger";
import { QuestEditorStore } from "./QuestEditorStore"; import { QuestEditorStore } from "./QuestEditorStore";
import { disposable_listener } from "../../core/gui/dom";
const assembly_analyser = new AssemblyAnalyser(); const assembly_analyser = new AssemblyAnalyser();
@ -119,6 +120,13 @@ export class AsmEditorStore implements Disposable {
assembly_analyser.issues.observe(({ value }) => this.update_model_markers(value), { assembly_analyser.issues.observe(({ value }) => this.update_model_markers(value), {
call_now: true, call_now: true,
}), }),
disposable_listener(window, "beforeunload", e => {
if (this.undo.can_undo.val) {
e.preventDefault();
e.returnValue = false;
}
}),
); );
} }

View File

@ -30,6 +30,7 @@ import { WritableProperty } from "../../core/observable/property/WritablePropert
import { QuestRunner } from "../QuestRunner"; import { QuestRunner } from "../QuestRunner";
import { AreaStore } from "./AreaStore"; import { AreaStore } from "./AreaStore";
import Logger = require("js-logger"); import Logger = require("js-logger");
import { disposable_listener } from "../../core/gui/dom";
const logger = Logger.get("quest_editor/gui/QuestEditorStore"); const logger = Logger.get("quest_editor/gui/QuestEditorStore");
@ -80,6 +81,15 @@ export class QuestEditorStore implements Disposable {
this.set_selected_entity(undefined); this.set_selected_entity(undefined);
} }
}), }),
disposable_listener(window, "beforeunload", e => {
this.quest_runner.stop();
if (this.undo.can_undo.val) {
e.preventDefault();
e.returnValue = false;
}
}),
); );
} }

View File

@ -1,7 +1,7 @@
const Logger = require("js-logger"); const Logger = require("js-logger");
require('dotenv').config({ path: ".env.test" }) require('dotenv').config({ path: ".env.test" })
const log_level = process.env["LOG_LEVEL"] || "OFF"; const log_level = process.env["LOG_LEVEL"] || "WARN";
Logger.useDefaults({ Logger.useDefaults({
defaultLevel: Logger[log_level], defaultLevel: Logger[log_level],