diff --git a/package.json b/package.json index 2680bae..d929e00 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "ng": "ng", "start": "ng serve", "build": "ng build", - "build:prod": "ng build --prod --base-href /life-qa/", + "build:prod": "ng build --prod", "format:fix": "pretty-quick --staged", "precommit": "run-s format:fix lint", "format:check": "prettier --config ./.prettierrc --list-different \"src/{app,environments,assets}/**/*{.ts,.js,.json,.css,.scss}\"", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 744701e..c741b78 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -21,6 +21,7 @@ import { TasksComponent } from './components/pages/page/tower/tasks/tasks.compon import { ColorPipe } from './pipes/color.pipe'; import { BlocksComponent } from './components/modal/modals/blocks/blocks.component'; import { FormatDatePipe } from './pipes/format-date.pipe'; +import { HttpClientModule } from '@angular/common/http'; @NgModule({ declarations: [ @@ -43,7 +44,7 @@ import { FormatDatePipe } from './pipes/format-date.pipe'; BlocksComponent, FormatDatePipe ], - imports: [BrowserModule, AppRoutingModule, FormsModule, BrowserAnimationsModule, DragDropModule], + imports: [BrowserModule, AppRoutingModule, FormsModule, BrowserAnimationsModule, DragDropModule, HttpClientModule], providers: [], bootstrap: [AppComponent] }) diff --git a/src/app/components/modal/modals/settings/settings.component.html b/src/app/components/modal/modals/settings/settings.component.html index 72f00b5..cd67f6c 100644 --- a/src/app/components/modal/modals/settings/settings.component.html +++ b/src/app/components/modal/modals/settings/settings.component.html @@ -14,4 +14,8 @@

There can be a maximum of 5 towers on each page.

+ + + + diff --git a/src/app/components/modal/modals/settings/settings.component.scss b/src/app/components/modal/modals/settings/settings.component.scss index 4c05a7d..2bb765b 100644 --- a/src/app/components/modal/modals/settings/settings.component.scss +++ b/src/app/components/modal/modals/settings/settings.component.scss @@ -31,4 +31,12 @@ p { font-size: var(--medium-font-size); } + + input[type='text'] { + text-align: center; + } + + button { + display: block; + } } diff --git a/src/app/components/modal/modals/settings/settings.component.ts b/src/app/components/modal/modals/settings/settings.component.ts index b39886c..a460c82 100644 --- a/src/app/components/modal/modals/settings/settings.component.ts +++ b/src/app/components/modal/modals/settings/settings.component.ts @@ -1,28 +1,56 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { ModalService } from '../../../../services/modal.service'; import { DataService } from '../../../../services/data.service'; import { Page } from '../../../../model/page'; +import { Data } from '../../../../model/data'; +import { Subscription } from 'rxjs/internal/Subscription'; +import { MapStoreService } from '../../../../services/map-store.service'; @Component({ selector: 'app-settings', templateUrl: './settings.component.html', styleUrls: ['./settings.component.scss'] }) -export class SettingsComponent implements OnInit { +export class SettingsComponent implements OnInit, OnDestroy { + data: Data; page: Page; - constructor(public modalService: ModalService, public dataService: DataService) {} + + private dataSubscription: Subscription; + private pageSubscription: Subscription; + + token: string; + + constructor(public modalService: ModalService, private store: MapStoreService) { + this.token = store.userToken; + } ngOnInit() { - this.modalService.active.input.subscribe(p => (this.page = p)); + const { data$, page$ } = this.modalService.active.input; + + this.dataSubscription = data$.subscribe(d => (this.data = d)); + this.pageSubscription = page$.subscribe(p => (this.page = p)); } async deletePage() { try { await this.modalService.showRemovePage(this.page.name); - this.dataService.removePage(this.page); + this.data.removePage(this.page); this.modalService.submit(); } catch { // pass } } + + setNewToken() { + this.store.userToken = this.token; + } + + ngOnDestroy() { + if (this.dataSubscription) { + this.dataSubscription.unsubscribe(); + } + if (this.pageSubscription) { + this.pageSubscription.unsubscribe(); + } + } } diff --git a/src/app/components/pages/page/tower/tower.component.ts b/src/app/components/pages/page/tower/tower.component.ts index e7717db..2a705d3 100644 --- a/src/app/components/pages/page/tower/tower.component.ts +++ b/src/app/components/pages/page/tower/tower.component.ts @@ -36,9 +36,7 @@ export class TowerComponent implements OnInit { return this.styledBlocks.filter(b => b.shouldDraw); } - public constructor(private modalService: ModalService, private changeDetection: ChangeDetectorRef) { - console.log('oo'); - } + public constructor(private modalService: ModalService, private changeDetection: ChangeDetectorRef) {} ngOnInit() { this.tower$.subscribe(value => { diff --git a/src/app/components/pages/pages.component.ts b/src/app/components/pages/pages.component.ts index 30f0312..8fdc4eb 100644 --- a/src/app/components/pages/pages.component.ts +++ b/src/app/components/pages/pages.component.ts @@ -4,6 +4,8 @@ import { DataService } from '../../services/data.service'; import { ModalService } from '../../services/modal.service'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; import { Observable } from 'rxjs/internal/Observable'; +import { Data } from '../../model/data'; +import { of } from 'rxjs/internal/observable/of'; const USER_DATA_KEY = 'life-towers.user-data.v.2'; @@ -17,6 +19,7 @@ export class PagesComponent implements OnInit { @ViewChild('page') page: ElementRef; @ViewChild('bottom') bottom: ElementRef; + data: Data; pages: Array; isDragHappening = false; @@ -44,8 +47,10 @@ export class PagesComponent implements OnInit { } ngOnInit() { - this.dataService.children$.subscribe(pages => { - if (pages) { + this.dataService.children$.subscribe(dataContainer => { + if (dataContainer && dataContainer.length > 0) { + this.data = dataContainer[0]; + const pages = this.data.pages; if (this.pages && !pages.includes(this._selectedPage.getValue().latestVersion)) { this.selectedPageName = null; } @@ -82,7 +87,7 @@ export class PagesComponent implements OnInit { if (this.pages && name) { if (!this.pageNames.includes(name)) { - this.dataService.addPage(name); + this.data.addPage(name); } const index = this.pageNames.indexOf(name); @@ -95,7 +100,10 @@ export class PagesComponent implements OnInit { async openSettings() { try { - await this.modalService.showSettings(this.selectedPage$); + await this.modalService.showSettings({ + page$: this.selectedPage$, + data$: of(this.data) + }); } catch { // pass } finally { diff --git a/src/app/interfaces/persistance/block.ts b/src/app/interfaces/persistance/block.ts index 5021ea3..8cf78d1 100644 --- a/src/app/interfaces/persistance/block.ts +++ b/src/app/interfaces/persistance/block.ts @@ -1,4 +1,6 @@ -export interface IBlock { +import { IUnique } from './unique'; + +export interface IBlock extends IUnique { created: Date; tag: string; isDone: boolean; diff --git a/src/app/interfaces/persistance/data.ts b/src/app/interfaces/persistance/data.ts new file mode 100644 index 0000000..df92560 --- /dev/null +++ b/src/app/interfaces/persistance/data.ts @@ -0,0 +1,6 @@ +import { IPage } from './page'; +import { IUnique } from './unique'; + +export interface IData extends IUnique { + pages: Array; +} diff --git a/src/app/interfaces/persistance/page.ts b/src/app/interfaces/persistance/page.ts index 75d9bf7..a096fae 100644 --- a/src/app/interfaces/persistance/page.ts +++ b/src/app/interfaces/persistance/page.ts @@ -1,12 +1,13 @@ import { ITower } from './tower'; import { Range } from '../range'; +import { IUnique } from './unique'; -export interface IPage { +export interface IPage extends IUnique { name: string; towers: ITower[]; userData: { - hideCreateTowerButton?: boolean; - defaultDateRange?: Range; + hideCreateTowerButton: boolean; + defaultDateRange: Range; }; } diff --git a/src/app/interfaces/persistance/tower.ts b/src/app/interfaces/persistance/tower.ts index 4776161..05d0a51 100644 --- a/src/app/interfaces/persistance/tower.ts +++ b/src/app/interfaces/persistance/tower.ts @@ -1,7 +1,8 @@ import { IBlock } from './block'; import { IColor } from '../color'; +import { IUnique } from './unique'; -export interface ITower { +export interface ITower extends IUnique { name: string; blocks: IBlock[]; baseColor: IColor; diff --git a/src/app/interfaces/persistance/unique.ts b/src/app/interfaces/persistance/unique.ts new file mode 100644 index 0000000..e41e7c2 --- /dev/null +++ b/src/app/interfaces/persistance/unique.ts @@ -0,0 +1,3 @@ +export interface IUnique { + id?: string; +} diff --git a/src/app/interfaces/serializable.ts b/src/app/interfaces/serializable.ts new file mode 100644 index 0000000..3a23338 --- /dev/null +++ b/src/app/interfaces/serializable.ts @@ -0,0 +1,3 @@ +export interface ISerializable { + serialize(referenceSerializer: (ref: object) => any): object; +} diff --git a/src/app/model/block.ts b/src/app/model/block.ts index 74ba9ee..e54cc32 100644 --- a/src/app/model/block.ts +++ b/src/app/model/block.ts @@ -1,23 +1,36 @@ -import { Serializable } from './serializable'; import { IBlock } from '../interfaces/persistance/block'; -import { InnerNodeState } from '../store/inner-node'; +import { InnerNode, InnerNodeState } from '../store/inner-node'; export interface BlockState extends IBlock, InnerNodeState {} -export class Block extends Serializable implements IBlock, BlockState { - readonly created: Date; - readonly isDone: boolean; - readonly description: string; +export class Block extends InnerNode implements IBlock, BlockState { readonly tag: string; + readonly description: string; + readonly isDone: boolean; + readonly created: Date; - constructor(props: IBlock) { + constructor(props: IBlock, referenceDeserializer: (from: any) => any = e => e) { + super([], props.id); if (props.created.constructor.name !== 'Date') { props.created = new Date(props.created); } - super(props, 'Block'); + this.tag = props.tag; + this.description = props.description; + this.isDone = props.isDone; + this.created = props.created; } changeKeys(props: Partial): this { return super.changeKeys(props); } + + serialize(referenceSerializer: (ref: object) => any): IBlock { + return { + ...super.serialize(referenceSerializer), + tag: this.tag, + description: this.description, + isDone: this.isDone, + created: this.created + }; + } } diff --git a/src/app/model/data.ts b/src/app/model/data.ts new file mode 100644 index 0000000..89b60dc --- /dev/null +++ b/src/app/model/data.ts @@ -0,0 +1,44 @@ +import { InnerNode, InnerNodeState } from '../store/inner-node'; +import { IData } from '../interfaces/persistance/data'; +import { Page } from './page'; + +export interface DataState extends IData, InnerNodeState {} + +export class Data extends InnerNode implements IData, DataState { + constructor(props: IData, referenceDeserializer: (from: any) => any = e => e) { + super(props.pages.map(p => new Page(referenceDeserializer(p), referenceDeserializer)), props.id); + } + + get pages(): Array { + return this.children as Array; + } + + addPage(name: string) { + const page = new Page({ + name, + userData: { + hideCreateTowerButton: false, + defaultDateRange: { + from: null, + to: null + } + }, + towers: [] + }); + this.addChildren([page]); + page.addTower(); + } + + removePage(page: Page) { + this.changeKeys({ + children: this.children.filter(c => c !== page) + }); + } + + serialize(referenceSerializer: (ref: object) => any): IData { + return { + ...super.serialize(referenceSerializer), + pages: this.pages.map(referenceSerializer) + }; + } +} diff --git a/src/app/model/page.ts b/src/app/model/page.ts index 142e4f4..76ea7d3 100644 --- a/src/app/model/page.ts +++ b/src/app/model/page.ts @@ -1,25 +1,24 @@ -import { Serializable } from './serializable'; import { IPage } from '../interfaces/persistance/page'; +import { Range } from '../interfaces/range'; import { Tower } from './tower'; -import { InnerNodeState } from '../store/inner-node'; +import { InnerNode, InnerNodeState } from '../store/inner-node'; export interface PageState extends InnerNodeState, IPage { towers: Array; } -export class Page extends Serializable implements IPage, PageState { +export class Page extends InnerNode implements IPage, PageState { readonly name: string; readonly userData: { hideCreateTowerButton: boolean; - defaultDateRange: { - from: Date; - to: Date; - }; + defaultDateRange: Range; }; - constructor(props: IPage) { - super(props, 'Page', props.towers.map(t => new Tower(t))); + constructor(props: IPage, referenceDeserializer: (from: any) => any = e => e) { + super(props.towers.map(t => new Tower(referenceDeserializer(t), referenceDeserializer)), props.id); + this.name = props.name; + this.userData = props.userData; } get towers(): Array { @@ -84,4 +83,13 @@ export class Page extends Serializable implements IPage, PageState { towers: this.towers.filter(t => t !== tower) }); } + + serialize(referenceSerializer: (ref: object) => any): IPage { + return { + ...super.serialize(referenceSerializer), + name: this.name, + userData: this.userData, + towers: this.towers.map(referenceSerializer) + }; + } } diff --git a/src/app/model/serializable.ts b/src/app/model/serializable.ts deleted file mode 100644 index e44de18..0000000 --- a/src/app/model/serializable.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { InnerNode } from '../store/inner-node'; - -export class Serializable extends InnerNode { - static childrenMap: { - [type: string]: { - childrenConstructor: typeof Serializable; - childrenListName: string; - childrenType: string; - }; - }; - - private static propertyList: any = {}; - protected type: string; - - protected constructor(properties: any, type: string, children: Array = []) { - super(children); - - const compiledType = this.constructor.name; - if (!Serializable.propertyList.hasOwnProperty(compiledType)) { - Serializable.propertyList[compiledType] = []; - } - for (const property in properties) { - if (properties.hasOwnProperty(property)) { - if (property !== Serializable.childrenMap[type].childrenListName) { - this[property] = properties[property]; - } - - if (!Serializable.propertyList[compiledType].includes(property)) { - Serializable.propertyList[compiledType].push(property); - } - } - } - } - - toJSON(): object { - return Serializable.propertyList[this.constructor.name].reduce( - (object, property) => ({ - [property]: this[property], - ...object - }), - {} - ); - } -} diff --git a/src/app/model/tower.ts b/src/app/model/tower.ts index f40d76d..0f85056 100644 --- a/src/app/model/tower.ts +++ b/src/app/model/tower.ts @@ -1,10 +1,9 @@ import { ITower } from '../interfaces/persistance/tower'; import { lighten } from '../utils/color'; import { Block } from './block'; -import { Serializable } from './serializable'; import { hash } from '../utils/hash'; import { IColor } from '../interfaces/color'; -import { InnerNodeState } from '../store/inner-node'; +import { InnerNode, InnerNodeState } from '../store/inner-node'; export type ColoredBlock = Block & { color: IColor }; @@ -12,14 +11,16 @@ export interface TowerState extends ITower, InnerNodeState { blocks: Array; } -export class Tower extends Serializable implements ITower, TowerState { - tags: string[]; +export class Tower extends InnerNode implements ITower, TowerState { readonly name: string; - coloredBlocks: Array; readonly baseColor: IColor; + tags: string[]; + coloredBlocks: Array; - constructor(props: ITower) { - super(props, 'Tower', props.blocks.map(b => new Block(b))); + constructor(props: ITower, referenceDeserializer: (from: any) => any = e => e) { + super(props.blocks.map(b => new Block(referenceDeserializer(b), referenceDeserializer)), props.id); + this.name = props.name; + this.baseColor = props.baseColor; this.onAfterClone(); } @@ -52,6 +53,15 @@ export class Tower extends Serializable implements ITower, TowerState { return lighten((hash(tag) - 0.5) * 50, this.baseColor); } + serialize(referenceSerializer: (ref: object) => any): ITower { + return { + ...super.serialize(referenceSerializer), + name: this.name, + baseColor: this.baseColor, + blocks: this.blocks.map(referenceSerializer) + }; + } + protected onAfterClone() { this.blocks.sort((a, b) => { return a.created.getTime() - b.created.getTime(); diff --git a/src/app/services/api.service.ts b/src/app/services/api.service.ts new file mode 100644 index 0000000..d76001f --- /dev/null +++ b/src/app/services/api.service.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Unique } from '../store/unique'; + +const API_URI = 'https://schmelczer.dev/api/store/'; + +@Injectable({ + providedIn: 'root' +}) +export class ApiService { + constructor(private http: HttpClient) {} + + private static getAuthorizationHeader(id: string): HttpHeaders { + return new HttpHeaders().set('Authorization', `life-towers-v3 ${id}`); + } + + async track(id: string): Promise { + await this.http.post(`${API_URI}me`, {}, { headers: ApiService.getAuthorizationHeader(id) }).toPromise(); + } + + async register(id: string): Promise { + await this.http.post(API_URI, { token: id }).toPromise(); + } + + async getObject(userId: string, objectId: string): Promise { + return await this.http + .get(`${API_URI}me/${objectId}`, { headers: ApiService.getAuthorizationHeader(userId) }) + .toPromise(); + } + + async postObject(userId: string, objectId: string, serializedObject: string): Promise { + await this.http + .post( + `${API_URI}me/${objectId}`, + { data: serializedObject }, + { headers: ApiService.getAuthorizationHeader(userId) } + ) + .toPromise(); + } + + async getRootId(userId: string): Promise { + return await this.http + // @ts-ignore + .get(`${API_URI}me/root`, { headers: ApiService.getAuthorizationHeader(userId), responseType: 'text' }) + .toPromise(); + } + + async setRootId(userId: string, rootId: string): Promise { + await this.http + .put(`${API_URI}me/root`, { root_id: rootId }, { headers: ApiService.getAuthorizationHeader(userId) }) + .toPromise(); + } +} diff --git a/src/app/services/data.service.ts b/src/app/services/data.service.ts index b9da30e..7e1dcef 100644 --- a/src/app/services/data.service.ts +++ b/src/app/services/data.service.ts @@ -1,72 +1,35 @@ import { Injectable } from '@angular/core'; -import { StoreService } from './store.service'; -import { Page } from '../model/page'; import { Root } from '../store/root'; -import { Serializable } from '../model/serializable'; -import { Tower } from '../model/tower'; -import { Block } from '../model/block'; -import { IPage } from '../interfaces/persistance/page'; +import { MapStoreService } from './map-store.service'; +import { Data } from '../model/data'; @Injectable({ providedIn: 'root' }) -export class DataService extends Root { - constructor(private storeService: StoreService>) { +export class DataService extends Root { + private shouldSave = true; + constructor(private store: MapStoreService) { super(); - this.init().catch(); - } - get pages(): Array { - return this.children; - } - - save(timeout: number) { - this.storeService.scheduleSave(this.pages, timeout); - } - - addPage(name: string) { - const page = new Page({ - name, - userData: {}, - towers: [] - }); - this.addChildren([page]); - page.addTower(); - } - - removePage(page: Page) { - this.changeKeys({ - children: this.children.filter(c => c !== page) - }); - } - - private async init() { - const pages = await this.storeService.load(); - Serializable.childrenMap = { - Page: { - childrenListName: 'towers', - childrenConstructor: Tower, - childrenType: 'Tower' - }, - Tower: { - childrenListName: 'blocks', - childrenConstructor: Block, - childrenType: 'Block' - }, - Block: { - childrenListName: null, - childrenConstructor: null, - childrenType: null + this.store.data.subscribe(d => { + if (d) { + this.shouldSave = false; + this.changeKeys({ children: [d] }); } - }; + }); + this.children$.subscribe(_ => { this.log(); }); - this.addChildren(pages.map(p => new Page(p))); - - this.children$.subscribe(_ => { - this.save(0); + this.children$.subscribe(data => { + if (data && data.length && data[0]) { + if (!this.shouldSave) { + this.shouldSave = true; + return; + } + this.store.save(data[0]); + } }); } } diff --git a/src/app/services/map-store.service.ts b/src/app/services/map-store.service.ts index 6858286..322806c 100644 --- a/src/app/services/map-store.service.ts +++ b/src/app/services/map-store.service.ts @@ -1,35 +1,191 @@ import { Injectable } from '@angular/core'; +import * as uuid from 'uuid'; +import { Data } from '../model/data'; +import { ITower } from '../interfaces/persistance/tower'; +import { IPage } from '../interfaces/persistance/page'; +import { IData } from '../interfaces/persistance/data'; +import { IUnique } from '../interfaces/persistance/unique'; +import { ApiService } from './api.service'; +import { Unique } from '../store/unique'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { Observable } from 'rxjs/internal/Observable'; const LOCAL_STORAGE_KEY = 'life-towers.data.v.3'; +interface LifeTowersData { + token: string; + root: string; + objects: { + [id: string]: IUnique; + }; +} + @Injectable({ providedIn: 'root' }) -export class MapStoreService { - readonly storage: { - [id: number]: T; - } = {}; +export class MapStoreService { + private state: LifeTowersData; + private canSaveTrigger: () => void; + private canSave = new Promise(r => (this.canSaveTrigger = r)); + private dataToSave: Data; - constructor() { - const localStorageData = localStorage.getItem(LOCAL_STORAGE_KEY); - if (localStorageData) { - this.storage = JSON.parse(localStorageData) as { - [id: number]: T; + private saveEverything = false; + + private readonly _data: BehaviorSubject = new BehaviorSubject(null); + readonly data: Observable = this._data.asObservable(); + + constructor(private api: ApiService) { + const storedData: string = localStorage.getItem(LOCAL_STORAGE_KEY); + + if (storedData) { + this.state = JSON.parse(storedData); + this.initWithLoad().catch(); + } else { + this.initWithRegister().catch(); + } + this.api.track(this.state.token).catch(); + } + + private static getSeed(): LifeTowersData { + const towerId = uuid.v4(); + const tower: ITower = { + id: towerId, + name: null, + blocks: [], + baseColor: { h: 0, s: 100, l: 50 } + }; + + const pageId = uuid.v4(); + const page: IPage = { + id: pageId, + name: 'My first page', + towers: [towerId], + userData: { + hideCreateTowerButton: false, + defaultDateRange: { + from: null, + to: null + } + } + }; + + const dataId = uuid.v4(); + const data: IData = { + id: dataId, + pages: [pageId] + }; + + return { + token: uuid.v4(), + root: dataId, + objects: { + [dataId]: data, + [pageId]: page, + [towerId]: tower + } + }; + } + + save(root: Data) { + this.dataToSave = root; + + setTimeout(() => { + if (this.dataToSave === root) { + this.realSave(root).catch(); + } + }, 750); + } + + private get root(): Data { + return new Data(this.state.objects[this.state.root] as IData, id => this.state.objects[id]); + } + + get userToken(): string { + return this.state.token; + } + + set userToken(value: string) { + this.state.token = value; + this.initWithLoad().catch(); + } + + private async realSave(root: Data): Promise { + await this.canSave; + + const waiting: Array = [root]; + const referenceSerializer = (e: Unique): string => { + waiting.push(e); + return e.id; + }; + + while (waiting.length > 0) { + const candidate = waiting.pop(); + if (!this.saveEverything && this.state.objects.hasOwnProperty(candidate.id)) { + continue; + } + + const serialized = candidate.serialize(referenceSerializer); + + this.state.objects[candidate.id] = serialized; + this.api.postObject(this.state.token, candidate.id, JSON.stringify(serialized)).catch(); + } + + this.api.setRootId(this.state.token, root.id).catch(); + this.state.root = root.id; + + localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(this.state)); + } + + private async initWithRegister(id?: string) { + this.state = MapStoreService.getSeed(); + if (id) { + this.state.token = id; + } + this._data.next(this.root); + + await this.api.register(this.state.token).catch(); + this.canSaveTrigger(); + + this.saveEverything = true; + await this.realSave(this.root); + this.saveEverything = false; + } + + private async initWithLoad() { + this.canSave = new Promise(r => (this.canSaveTrigger = r)); + + let realRoot: string; + try { + realRoot = await this.api.getRootId(this.state.token).catch(); + } catch { + this.initWithRegister(this.state.token).catch(); + return; + } + + if (this.state.root !== realRoot) { + this.state.root = realRoot; + const root = await this.api.getObject(this.state.token, realRoot); + this.state.objects[this.state.root] = root; + + const getUnknowns = async (element: any) => { + const childrenAliases = ['pages', 'towers', 'blocks']; + + for (const childrenAlias of childrenAliases) { + if (element.hasOwnProperty(childrenAlias)) { + for (const p of element[childrenAlias]) { + if (!this.state.objects.hasOwnProperty(p)) { + const unknown = await this.api.getObject(this.state.token, p); + this.state.objects[p] = unknown; + await getUnknowns(unknown); + } + } + } + } }; - } - } - get(id: number) { - if (this.storage.hasOwnProperty(id)) { - return this.storage[id]; + await getUnknowns(root); } - } - - add(id: number, value: T) { - if (this.storage.hasOwnProperty(id)) { - throw new Error('Key already set.'); - } - - this.storage[id] = value; + this._data.next(this.root); + this.canSaveTrigger(); } } diff --git a/src/app/services/modal.service.ts b/src/app/services/modal.service.ts index c376087..0f1d3e9 100644 --- a/src/app/services/modal.service.ts +++ b/src/app/services/modal.service.ts @@ -5,6 +5,7 @@ import { CancelService } from './cancel.service'; import { Page } from '../model/page'; import { Observable } from 'rxjs/internal/Observable'; import { Block } from '../model/block'; +import { Data } from '../model/data'; export enum ModalType { blocks, @@ -33,8 +34,8 @@ export class ModalService { return this.createPromiseAndPushToStack(input, ModalType.blocks); } - showSettings(selectedPage: Observable): Promise { - return this.createPromiseAndPushToStack(selectedPage, ModalType.settings); + showSettings(options: { page$: Observable; data$: Observable }): Promise { + return this.createPromiseAndPushToStack(options, ModalType.settings); } showRemoveTower(tower: Tower): Promise { diff --git a/src/app/services/store.service.ts b/src/app/services/store.service.ts deleted file mode 100644 index be3c4e0..0000000 --- a/src/app/services/store.service.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { Injectable } from '@angular/core'; - -const LOCAL_STORAGE_KEY = 'life-towers.data.v.2'; - -@Injectable({ - providedIn: 'root' -}) -export class StoreService { - private saveScheduled = false; - private dataToSave: T; - private storedData: T; - private mockData: string = JSON.stringify([ - { - name: 'Work & life', - userData: {}, - towers: [ - { - name: 'work', - baseColor: { h: 0, s: 100, l: 50 }, - blocks: [ - { - created: new Date(2015, 2, 13), - tag: 'a', - description: 'done it', - isDone: true - }, - { - created: new Date(2016, 2, 15), - tag: 'go to school', - description: 'done it', - isDone: false - }, - { - created: new Date(2017, 2, 15), - tag: 'go to work', - isDone: true - }, - { - created: new Date(2018, 2, 13), - tag: 'go to work', - description: 'done it', - isDone: true - }, - { - created: new Date(2019, 3, 13), - tag: 'go to work', - isDone: false - }, - { - created: new Date(2019, 3, 15), - tag: 'go to school', - description: 'done it', - isDone: true - }, - { - created: new Date(2019, 3, 15, 19), - tag: 'go to school', - isDone: false - } - ] - }, - { - baseColor: { h: 180, s: 100, l: 50 }, - name: 'life', - blocks: [ - { - created: new Date(2019, 3, 13), - tag: 'go home', - description: 'done it', - isDone: false - }, - { - created: new Date(2019, 4, 13), - tag: 'go home', - isDone: false - }, - { - created: new Date(2019, 4, 15), - tag: 'go to work', - description: 'done it', - isDone: false - }, - { - created: new Date(2019, 4, 15, 14), - tag: 'go to work', - isDone: false - } - ] - } - ] - } - ]); - - constructor() { - const localStorageData = localStorage.getItem(LOCAL_STORAGE_KEY); - this.storedData = JSON.parse(localStorageData ? localStorageData : this.mockData) as T; - } - - scheduleSave(data: T, timeout: number) { - this.dataToSave = data; - if (!this.saveScheduled) { - this.saveScheduled = true; - setTimeout(() => { - this.saveScheduled = false; - this.save(this.dataToSave).catch(); - }, timeout); - } - } - - async load(): Promise { - console.log('load', this.storedData); - return this.storedData; - } - - async save(data: T): Promise { - this.storedData = data; - const stringified = JSON.stringify(this.storedData, null, 2); - console.log('save'); - // console.log('save', stringified); - window.localStorage.setItem(LOCAL_STORAGE_KEY, stringified); - } -} diff --git a/src/app/store/inner-node.ts b/src/app/store/inner-node.ts index e5f6c57..e742ab1 100644 --- a/src/app/store/inner-node.ts +++ b/src/app/store/inner-node.ts @@ -10,8 +10,8 @@ export class InnerNode extends Node implements InnerNodeState { private nextVersion: this = null; readonly children: Array; - constructor(children: Array = []) { - super(children); + protected constructor(children: Array = [], id?: string) { + super(children, id); this.children = children; } diff --git a/src/app/store/node.ts b/src/app/store/node.ts index 2dae062..35f0a45 100644 --- a/src/app/store/node.ts +++ b/src/app/store/node.ts @@ -8,8 +8,8 @@ export interface NodeState { export abstract class Node extends Unique implements NodeState { abstract readonly children: Array; - protected constructor(children: Array = []) { - super(); + protected constructor(children: Array = [], id?: string) { + super(id); children.forEach(c => (c.parent = this)); } protected abstract changeKeys(props: Partial): this; @@ -32,7 +32,7 @@ export abstract class Node extends Unique implements NodeState { protected _log(indent = ''): string { const basicInfo = `${indent} - ${this.constructor.name}, #${this.id}`; - let response = `${basicInfo}${' '.repeat(25 - basicInfo.length)}copies: ${this.copies}\n`; + let response = `${basicInfo}${' '.repeat(70 - basicInfo.length)}copies: ${this.copies}\n`; for (const c of this.children) { response += `${c._log(indent + ' ')}`; } diff --git a/src/app/store/store.ts b/src/app/store/store.ts deleted file mode 100644 index e13f0b0..0000000 --- a/src/app/store/store.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { uuidv4 } from 'uuid'; - -class Store { - private readonly onSet: (id: string, element: any) => void; - private readonly elements: { - [id: string]: any; - }; - - constructor( - elements: { - [id: string]: any; - }, - onSet: (id: string, element: any) => void - ) { - this.elements = elements; - this.onSet = onSet; - } - - add(element: any): number { - const id = uuidv4(); - this.elements[id] = element; - this.onSet(id, element); - return id; - } - - get(id: string): any { - return this.elements[id]; - } -} diff --git a/src/app/store/unique.ts b/src/app/store/unique.ts index f2c7510..1202c09 100644 --- a/src/app/store/unique.ts +++ b/src/app/store/unique.ts @@ -1,18 +1,27 @@ -export class Unique { - private static nextId = 0; - private static store; +import * as uuid from 'uuid'; +import { ISerializable } from '../interfaces/serializable'; +import { IUnique } from '../interfaces/persistance/unique'; - constructor() { - this.setUniqueness(); +export class Unique implements ISerializable, IUnique { + private static count = 0; + + constructor(id?: string) { + if (id) { + this._id = id; + console.log('got id ' + id); + } else { + this.setUniqueness(); + console.log('unique ' + this.id); + } } static get ObjectCount(): number { - return Unique.nextId; + return Unique.count; } - private _id: number; + private _id: string; - get id(): number { + get id(): string { return this._id; } @@ -23,7 +32,14 @@ export class Unique { } protected setUniqueness() { - this._id = Unique.nextId++; + this._id = uuid.v4(); + Unique.count++; this._copies++; } + + serialize(referenceSerializer: (ref: object) => any): object { + return { + id: this.id + }; + } }