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

View File

@ -33,6 +33,8 @@ export abstract class Persister {
private server_key(server: Server, key: string): string {
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) {
case Server.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 { HuntMethodModel } from "../model/HuntMethodModel";
import { Duration } from "luxon";

View File

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

View File

@ -10,7 +10,7 @@ import { WebGLRenderer } from "three";
import { DisposableThreeRenderer } from "./core/rendering/Renderer";
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 {

View File

@ -1,8 +1,7 @@
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 GoldenLayout, { Container, ContentItem, ItemConfigType } from "golden-layout";
import { quest_editor_ui_persister } from "../persistence/QuestEditorUiPersister";
import { QuestInfoView } from "./QuestInfoView";
import "golden-layout/src/css/goldenlayout-base.css";
import "../../core/gui/golden_layout_theme.css";
@ -24,6 +23,7 @@ import { EntityImageRenderer } from "../rendering/EntityImageRenderer";
import { AreaAssetLoader } from "../loading/AreaAssetLoader";
import { EntityAssetLoader } from "../loading/EntityAssetLoader";
import { DisposableThreeRenderer } from "../../core/rendering/Renderer";
import { QuestEditorUiPersister } from "../persistence/QuestEditorUiPersister";
import Logger = require("js-logger");
const logger = Logger.get("quest_editor/gui/QuestEditorView");
@ -56,8 +56,6 @@ export class QuestEditorView extends ResizableWidget {
{ name: string; create(): ResizableWidget }
>;
private readonly view_white_list: readonly string[];
private readonly tool_bar: QuestEditorToolBar;
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,
entity_asset_loader: EntityAssetLoader,
entity_image_renderer: EntityImageRenderer,
private readonly quest_editor_ui_persister: QuestEditorUiPersister,
create_three_renderer: () => DisposableThreeRenderer,
) {
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(
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();
@ -251,25 +238,32 @@ export class QuestEditorView extends ResizableWidget {
private async init_golden_layout(): Promise<GoldenLayout> {
const default_layout_content = this.get_default_layout_content();
const content = await quest_editor_ui_persister.load_layout_config(
this.view_white_list,
try {
const content = await this.quest_editor_ui_persister.load_layout_config(
default_layout_content,
);
try {
return this.attempt_gl_init({
if (content) {
const gl = this.attempt_gl_init({
...DEFAULT_LAYOUT_CONFIG,
content,
});
logger.info("Instantiated golden layout with persisted layout.");
return gl;
}
} catch (e) {
logger.warn("Couldn't instantiate golden layout with persisted layout.", e);
}
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 {
const layout = new GoldenLayout(config, this.layout_element);
@ -298,7 +292,7 @@ export class QuestEditorView extends ResizableWidget {
}
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) => {

View File

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

View File

@ -1,74 +1,141 @@
import { Persister } from "../../core/persistence";
import { Persister } from "../../core/Persister";
import { throttle } from "lodash";
import GoldenLayout from "golden-layout";
import { ComponentConfig, ItemConfigType } from "golden-layout";
const LAYOUT_CONFIG_KEY = "QuestEditorUiPersister.layout_config";
export class QuestEditorUiPersister extends Persister {
persist_layout_config = throttle(
(config: any) => {
this.persist(LAYOUT_CONFIG_KEY, config);
(config: ItemConfigType[]) => {
this.persist(LAYOUT_CONFIG_KEY, this.to_persisted_item_config(config));
},
500,
{ leading: true, trailing: true },
{ leading: false, trailing: true },
);
async load_layout_config(
components: readonly string[],
default_config: GoldenLayout.ItemConfigType[],
): Promise<any> {
const config = await this.load<GoldenLayout.ItemConfigType[]>(LAYOUT_CONFIG_KEY);
default_config: ItemConfigType[],
): Promise<ItemConfigType[] | undefined> {
const config = await this.load<ItemConfigType[]>(LAYOUT_CONFIG_KEY);
if (config && this.verify_layout_config(config, components)) {
return config;
} else {
return default_config;
if (config) {
const components = this.extract_components(default_config);
const verified_config = this.sanitized_layout_config(
this.from_persisted_item_config(config),
components,
);
if (verified_config) {
return verified_config;
}
}
private verify_layout_config(
config: GoldenLayout.ItemConfigType[],
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 undefined;
}
return true;
private sanitized_layout_config(
config: ItemConfigType[],
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;
}
private verify_layout_child(
config: GoldenLayout.ItemConfigType,
components: Set<string>,
// 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>,
first: boolean,
): boolean {
): ItemConfigType | undefined {
if (!config) {
return false;
return undefined;
}
if ("componentName" in config) {
if (!components.has(config.componentName)) {
return false;
} else {
found.add(config.componentName);
if (config.type === "component" && "componentName" in config) {
const component = components.get(config.componentName);
// Remove deprecated components.
if (!component) {
return undefined;
}
found.add(config.componentName);
config.id = component.id;
config.title = component.title;
}
if (config.content) {
for (const child of config.content) {
if (!this.verify_layout_child(child, components, found, false)) {
return false;
config.content = config.content
.map(child => this.sanitize_layout_child(child, components, found))
.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";
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;

View File

@ -18,6 +18,7 @@ import LocationLink = languages.LocationLink;
import IModelContentChange = editor.IModelContentChange;
import { Breakpoint } from "../scripting/vm/Debugger";
import { QuestEditorStore } from "./QuestEditorStore";
import { disposable_listener } from "../../core/gui/dom";
const assembly_analyser = new AssemblyAnalyser();
@ -119,6 +120,13 @@ export class AsmEditorStore implements Disposable {
assembly_analyser.issues.observe(({ value }) => this.update_model_markers(value), {
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 { AreaStore } from "./AreaStore";
import Logger = require("js-logger");
import { disposable_listener } from "../../core/gui/dom";
const logger = Logger.get("quest_editor/gui/QuestEditorStore");
@ -80,6 +81,15 @@ export class QuestEditorStore implements Disposable {
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");
require('dotenv').config({ path: ".env.test" })
const log_level = process.env["LOG_LEVEL"] || "OFF";
const log_level = process.env["LOG_LEVEL"] || "WARN";
Logger.useDefaults({
defaultLevel: Logger[log_level],