diff --git a/package.json b/package.json index 15de499..14dd72e 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "@angular/pwa": "^0.1001.7", "@angular/router": "~10.1.5", "@angular/service-worker": "~10.1.5", - "@circlon/angular-tree-component": "^10.0.0", "@ckeditor/ckeditor5-angular": "^2.0.1", "@ckeditor/ckeditor5-build-decoupled-document": "^27.0.0", "@convergencelabs/monaco-collab-ext": "^0.3.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 20cd819..28777f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,7 +7,6 @@ dependencies: '@angular/pwa': 0.1001.7 '@angular/router': 10.1.6_2d3b53e7e463c932a6e73e0760b7a0a2 '@angular/service-worker': 10.1.6_04f723395c0c28a9d9b8a4ace7178ad2 - '@circlon/angular-tree-component': 10.0.2_04f723395c0c28a9d9b8a4ace7178ad2 '@ckeditor/ckeditor5-angular': 2.0.1_11b9a698fa893a2ce33633beb1aae14b '@ckeditor/ckeditor5-build-decoupled-document': 27.0.0 '@convergencelabs/monaco-collab-ext': 0.3.2 @@ -1411,19 +1410,6 @@ packages: dev: true resolution: integrity: sha512-tNMDjcv/4DIcHxErTgwB9q2ZcYyN0sUfgGKUK/mm1FJK7Wz+KstoEekxrl/tBiNDgLK1HGi+sppj1An/1DR4fQ== - /@circlon/angular-tree-component/10.0.2_04f723395c0c28a9d9b8a4ace7178ad2: - dependencies: - '@angular/common': 10.1.6_@angular+core@10.1.6+rxjs@6.6.3 - '@angular/core': 10.1.6_rxjs@6.6.3+zone.js@0.10.3 - lodash-es: 4.17.20 - mobx: 4.14.1 - tslib: 2.1.0 - dev: false - peerDependencies: - '@angular/common': '>=10.0.0 <11.0.0' - '@angular/core': '>=10.0.0 <11.0.0' - resolution: - integrity: sha512-N8KyIQ89fGEO8OKYYgFtY/PbPhHqs4DK5kNPmpJt4KPssvQMF1gP6m+RGVDKlDVH933aOPsrHQax87Fk6dI8gA== /@ckeditor/ckeditor5-adapter-ckfinder/27.0.0: dependencies: ckeditor5: 27.0.0 @@ -7011,10 +6997,6 @@ packages: node: '>=8' resolution: integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - /lodash-es/4.17.20: - dev: false - resolution: - integrity: sha512-JD1COMZsq8maT6mnuz1UMV0jvYD0E0aUsSOdrr1/nAG3dhqQXwRRgeW0cSqH1U43INKcqxaiVIQNOUDld7gRDA== /lodash-es/4.17.21: dev: false resolution: @@ -7495,10 +7477,6 @@ packages: hasBin: true resolution: integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== - /mobx/4.14.1: - dev: false - resolution: - integrity: sha512-Oyg7Sr7r78b+QPYLufJyUmxTWcqeQ96S1nmtyur3QL8SeI6e0TqcKKcxbG+sVJLWANhHQkBW/mDmgG5DDC4fdw== /moment/2.29.1: dev: false resolution: @@ -11611,7 +11589,6 @@ specifiers: '@angular/pwa': ^0.1001.7 '@angular/router': ~10.1.5 '@angular/service-worker': ~10.1.5 - '@circlon/angular-tree-component': ^10.0.0 '@ckeditor/ckeditor5-angular': ^2.0.1 '@ckeditor/ckeditor5-build-decoupled-document': ^27.0.0 '@convergencelabs/monaco-collab-ext': ^0.3.2 diff --git a/src/app/app.component.html b/src/app/app.component.html index a45ec3e..0a0e424 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -19,7 +19,7 @@ - + @@ -30,16 +30,20 @@ - - - - -
{{ node.data.name }}
-
-
-
+
+ Menu diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 6d321f6..800844e 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -12,8 +12,7 @@ import { SplashScreen } from '@ionic-native/splash-screen/ngx'; import { StatusBar } from '@ionic-native/status-bar/ngx'; import { ApiService } from './service/api.service'; import { Router } from '@angular/router'; -import {TREE_ACTIONS, TreeComponent} from '@circlon/angular-tree-component'; -import {BehaviorSubject, fromEvent, Observable} from 'rxjs'; +import {BehaviorSubject, Observable} from 'rxjs'; import {OptionPickerComponent} from './components/option-picker/option-picker.component'; import {OptionMenuComponent} from './components/option-menu/option-menu.component'; import {SelectorComponent} from './components/sharing/selector/selector.component'; @@ -26,6 +25,7 @@ import {EditorService} from './service/editor.service'; import {debug} from './utility'; import {AuthService} from './service/auth.service'; import {OpenerService} from './service/opener.service'; +import {TreeRootComponent} from './components/tree-root/tree-root.component'; @Component({ selector: 'app-root', @@ -33,82 +33,15 @@ import {OpenerService} from './service/opener.service'; styleUrls: ['app.component.scss'] }) export class AppComponent implements OnInit { - @ViewChild('menuTree') menuTree: TreeComponent; + @ViewChild('menuTree') menuTree: TreeRootComponent; public readonly ready$: BehaviorSubject = new BehaviorSubject(false); public addChildTarget: any = false; public deleteTarget: any = false; public menuTarget: any = false; public refreshingMenu = false; - public lastClickEvent: Array = []; + public lastClickEvent?: {event: MouseEvent, item: any}; public nodes = []; - public currentPageId: string; public virtualRootPageId?: string; - public options = { - isExpandedField: 'expanded', - animateExpand: false, - scrollOnActivate: false, - allowDrag: true, - allowDrop: (element, { parent, index }) => { - return ( - !this.api.isOffline - && (element.data.type === 'page' || element.data.type === 'form') - && (parent.data.type === 'page' || parent.data.userRootPage) - && !element.data.userRootPage - && element.data.id !== parent.data.id - ); - }, - actionMapping: { - mouse: { - dblClick: (tree, node, $event) => { - this.navigateEditorToNode(node); - }, - click: (tree, node, $event) => { - TREE_ACTIONS.FOCUS(tree, node, $event); - TREE_ACTIONS.EXPAND(tree, node, $event); - - this.addChildTarget = false; - this.deleteTarget = false; - this.menuTarget = false; - - if ( !node.data.noChildren && (!node.data.level || node.data.level === 'manage') ) { - this.addChildTarget = node; - } - - if ( !node.data.noDelete && (!node.data.level || node.data.level === 'manage') ) { - this.deleteTarget = node; - } - - this.menuTarget = node; - this.lastClickEvent = [tree, node, $event]; - }, - contextMenu: (tree, node, event) => { - event.preventDefault(); - event.stopPropagation(); - - TREE_ACTIONS.FOCUS(tree, node, event); - TREE_ACTIONS.EXPAND(tree, node, event); - - this.addChildTarget = false; - this.deleteTarget = false; - this.menuTarget = false; - - if ( !node.data.noChildren && (!node.data.level || node.data.level === 'manage') ) { - this.addChildTarget = node; - } - - if ( !node.data.noDelete && (!node.data.level || node.data.level === 'manage') ) { - this.deleteTarget = node; - } - - this.menuTarget = node; - this.lastClickEvent = [tree, node, event]; - - this.onNodeMenuClick(event); - }, - } - } - }; - public typeIcons = NodeTypeIcons; public get appName(): string { @@ -121,14 +54,12 @@ export class AppComponent implements OnInit { protected versionInterval?: any; protected showedNewVersionAlert = false; protected showedOfflineAlert = false; - protected backbuttonSubscription: any; constructor( private platform: Platform, private splashScreen: SplashScreen, private statusBar: StatusBar, public readonly api: ApiService, - protected navCtrl: NavController, protected router: Router, protected alerts: AlertController, protected popover: PopoverController, @@ -143,6 +74,55 @@ export class AppComponent implements OnInit { protected opener: OpenerService, ) { } + public onMenuItemClick(event: {event: MouseEvent, item: any}) { + this.addChildTarget = false; + this.deleteTarget = false; + this.menuTarget = false; + + if ( !event.item.noChildren && (!event.item.level || event.item.level === 'manage') ) { + this.addChildTarget = event.item; + } + + if ( !event.item.noDelete && (!event.item.level || event.item.level === 'manage') ) { + this.deleteTarget = event.item; + } + + this.menuTarget = event.item; + this.lastClickEvent = event; + } + + public onMenuItemActivate(event: {event: MouseEvent, item: any}) { + this.navigateEditorToNode(event.item); + } + + public onMenuItemRightClick(event: {event: MouseEvent, item: any}) { + event.event.preventDefault(); + event.event.stopPropagation(); + + this.addChildTarget = false; + this.deleteTarget = false; + this.menuTarget = false; + + if ( !event.item.noChildren && (!event.item.level || event.item.level === 'manage') ) { + this.addChildTarget = event.item; + } + + if ( !event.item.noDelete && (!event.item.level || event.item.level === 'manage') ) { + this.deleteTarget = event.item; + } + + this.menuTarget = event.item; + this.lastClickEvent = event; + + this.onNodeMenuClick(event.event, true); + } + + public onMenuFilterChange(event) { + const filterValue = event?.detail?.value; + debug('Filtering tree:', filterValue); + this.menuTree?.filterTree(filterValue); + } + async checkNewVersion() { if ( !this.showedNewVersionAlert && await this.session.newVersionAvailable() ) { const toast = await this.toasts.create({ @@ -236,15 +216,15 @@ export class AppComponent implements OnInit { } } - async onNodeMenuClick($event) { - let canManage = this.menuTarget.data.level === 'manage'; + async onNodeMenuClick($event, fromContextMenu = false) { + let canManage = this.menuTarget.level === 'manage'; if ( !canManage ) { - if ( !this.menuTarget.data.level ) { + if ( !this.menuTarget.level ) { canManage = true; } } - if ( !this.menuTarget.data.id ) { + if ( !this.menuTarget.id ) { return; } @@ -255,10 +235,12 @@ export class AppComponent implements OnInit { ]; const manageOptions = [ + ...(fromContextMenu ? this.getCreateNodeMenuItems() : []), {name: 'Share Sub-Tree', icon: 'fa fa-share-alt', value: 'share'}, + {name: 'Delete Sub-Tree', icon: 'fa fa-trash noded-danger', value: 'delete'}, ]; - if ( this.menuTarget.data.bookmark ) { + if ( this.menuTarget.bookmark ) { options.push({name: 'Remove Bookmark', icon: 'fa fa-star', value: 'bookmark_remove'}); } else { options.push({name: 'Bookmark', icon: 'fa fa-star', value: 'bookmark_add'}); @@ -280,7 +262,7 @@ export class AppComponent implements OnInit { component: SelectorComponent, cssClass: 'modal-med', componentProps: { - node: this.menuTarget.data, + node: this.menuTarget, } }).then(modal => { const modalState = { @@ -302,6 +284,14 @@ export class AppComponent implements OnInit { this.addBookmark(); } else if ( result.data === 'bookmark_remove' ) { this.removeBookmark(); + } else if ( result.data === 'top-level' ) { + this.onTopLevelCreate(); + } else if ( result.data === 'child' ) { + this.onChildCreate(); + } else if ( result.data === 'form' ) { + this.onChildCreate('form'); + } else if ( result.data === 'delete' ) { + this.onDeleteClick(); } }); @@ -309,9 +299,9 @@ export class AppComponent implements OnInit { } async setVirtualRoot() { - if ( this.menuTarget && this.menuTarget.data?.type === 'page' ) { + if ( this.menuTarget && this.menuTarget?.type === 'page' ) { debug('virtual root menu target', this.menuTarget); - this.virtualRootPageId = this.menuTarget.data.id; + this.virtualRootPageId = this.menuTarget.id; this.reloadMenuItems().subscribe(); } } @@ -325,7 +315,7 @@ export class AppComponent implements OnInit { const exportRecord: any = await new Promise((res, rej) => { const reqData = { format: 'html', - PageId: this.menuTarget.data.id, + PageId: this.menuTarget.id, }; this.api.post(`/exports/subtree`, reqData).subscribe({ @@ -343,8 +333,8 @@ export class AppComponent implements OnInit { addBookmark() { const bookmarks = this.session.get('user.preferences.bookmark_page_ids') || []; - if ( !bookmarks.includes(this.menuTarget.data.id) ) { - bookmarks.push(this.menuTarget.data.id); + if ( !bookmarks.includes(this.menuTarget.id) ) { + bookmarks.push(this.menuTarget.id); } this.session.set('user.preferences.bookmark_page_ids', bookmarks); @@ -353,35 +343,39 @@ export class AppComponent implements OnInit { removeBookmark() { let bookmarks = this.session.get('user.preferences.bookmark_page_ids') || []; - bookmarks = bookmarks.filter(x => x !== this.menuTarget.data.id); + bookmarks = bookmarks.filter(x => x !== this.menuTarget.id); this.session.set('user.preferences.bookmark_page_ids', bookmarks); this.session.save().then(() => this.navService.requestSidebarRefresh({ quiet: true })); } - async onCreateClick($event: MouseEvent) { - const menuItems = [ + getCreateNodeMenuItems() { + return [ { - name: 'Top-Level Note', + name: 'Create Top-Level Note', icon: 'fa fa-sticky-note noded-note', value: 'top-level', title: 'Create a new top-level note page', }, ...(this.addChildTarget ? [ { - name: 'Child Note', + name: 'Create Child Note', icon: 'fa fa-sticky-note noded-note', value: 'child', title: 'Create a note page as a child of the given note', }, { - name: 'Form', + name: 'Create Form', icon: 'fa fa-clipboard-list noded-form', value: 'form', title: 'Create a new form page as a child of the given note', }, ] : []), ]; + } + + async onCreateClick($event: MouseEvent) { + const menuItems = this.getCreateNodeMenuItems(); const popover = await this.popover.create({ event: $event, @@ -459,16 +453,11 @@ export class AppComponent implements OnInit { handler: async args => { args = { name: args.name, - parentId: this.addChildTarget.data.id, + parentId: this.addChildTarget.id, pageType, }; this.api.post('/page/create-child', args).subscribe(res => { this.reloadMenuItems().subscribe(() => { - TREE_ACTIONS.EXPAND( - this.lastClickEvent[0], - this.lastClickEvent[1], - this.lastClickEvent[2] - ); this.router.navigate(['/editor', { id: res.data.UUID }]); }); }); @@ -484,7 +473,7 @@ export class AppComponent implements OnInit { const alert = await this.alerts.create({ header: 'Delete page?', message: - 'Deleting this page will make its contents and all of its children inaccessible. Are you sure you want to continue?', + 'Deleting this page will make its contents and all of its children inaccessible. Are you sure you want to continue?', buttons: [ { text: 'Keep It', @@ -494,9 +483,9 @@ export class AppComponent implements OnInit { text: 'Delete It', handler: async () => { this.api - .post(`/page/delete/${this.deleteTarget.data.id}`) + .post(`/page/delete/${this.deleteTarget.id}`) .subscribe(res => { - if ( this.opener.currentPageId === this.deleteTarget.data.id ) { + if ( this.opener.currentPageId === this.deleteTarget.id ) { this.router.navigate(['/home']); } @@ -717,7 +706,7 @@ export class AppComponent implements OnInit { setTimeout(() => { this.loader.dismiss(); - this.menuTree?.treeModel?.expandAll(); + // this.menuTree?.treeModel?.expandAll(); }, 10); if ( !this.versionInterval ) { diff --git a/src/app/app.module.ts b/src/app/app.module.ts index ca795c7..39f7eeb 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -10,7 +10,6 @@ import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { HttpClientModule } from '@angular/common/http'; import { ComponentsModule } from './components/components.module'; -import { TreeModule } from '@circlon/angular-tree-component'; import {AgGridModule} from 'ag-grid-angular'; import {MonacoEditorModule} from 'ngx-monaco-editor'; import { APP_BASE_HREF, PlatformLocation } from '@angular/common'; @@ -47,7 +46,6 @@ export function getBaseHref(platformLocation: PlatformLocation): string { AppRoutingModule, HttpClientModule, ComponentsModule, - TreeModule, AgGridModule.withComponents([]), MonacoEditorModule.forRoot(), HighlightModule, diff --git a/src/app/components/components.module.ts b/src/app/components/components.module.ts index 755a8e4..26caf4a 100644 --- a/src/app/components/components.module.ts +++ b/src/app/components/components.module.ts @@ -26,6 +26,7 @@ import {DatetimeRendererComponent} from './editor/database/renderers/datetime-re import {CurrencyRendererComponent} from './editor/database/renderers/currency-renderer.component'; import {BooleanRendererComponent} from './editor/database/renderers/boolean-renderer.component'; import {SearchComponent} from './search/Search.component'; +import {TreeRootComponent} from './tree-root/tree-root.component'; import {NormComponent} from './nodes/norm/norm.component'; import {MarkdownComponent as MarkdownEditorComponent} from './nodes/markdown/markdown.component'; @@ -74,6 +75,7 @@ import {FileBoxPageComponent} from './nodes/file-box/file-box-page.component'; CurrencyRendererComponent, BooleanRendererComponent, SearchComponent, + TreeRootComponent, NormComponent, MarkdownEditorComponent, @@ -131,6 +133,7 @@ import {FileBoxPageComponent} from './nodes/file-box/file-box-page.component'; CurrencyRendererComponent, BooleanRendererComponent, SearchComponent, + TreeRootComponent, NormComponent, MarkdownEditorComponent, @@ -173,6 +176,7 @@ import {FileBoxPageComponent} from './nodes/file-box/file-box-page.component'; CurrencyRendererComponent, BooleanRendererComponent, SearchComponent, + TreeRootComponent, NormComponent, MarkdownEditorComponent, diff --git a/src/app/components/tree-root/tree-root.component.html b/src/app/components/tree-root/tree-root.component.html new file mode 100644 index 0000000..2aed149 --- /dev/null +++ b/src/app/components/tree-root/tree-root.component.html @@ -0,0 +1,37 @@ +
+ +
+ + + + {{ item[displayField] }} +
+
+ +
+
+
diff --git a/src/app/components/tree-root/tree-root.component.scss b/src/app/components/tree-root/tree-root.component.scss new file mode 100644 index 0000000..0f5fd05 --- /dev/null +++ b/src/app/components/tree-root/tree-root.component.scss @@ -0,0 +1,38 @@ +.container { + display: flex; + flex-direction: column; +} + +.nested-level { + margin-left: 20px; +} + +.item { + padding: 10px; + border-bottom: 1px solid #cccccc; + border-left: 1px solid #cccccc; + user-select: none; + + i { + padding-right: 10px; + + &.handle { + color: #888888; + } + } + + &:hover, &.activated { + background-color: #f0f0f0; + cursor: pointer; + } +} + +.container.dark { + .item { + border-color: #555555; + + &:hover, &.activated { + background-color: #333333; + } + } +} diff --git a/src/app/components/tree-root/tree-root.component.ts b/src/app/components/tree-root/tree-root.component.ts new file mode 100644 index 0000000..172dc0f --- /dev/null +++ b/src/app/components/tree-root/tree-root.component.ts @@ -0,0 +1,253 @@ +import {Component, EventEmitter, Input, OnInit, Output, ViewChildren} from '@angular/core'; +import {debug} from '../../utility'; + +/** + * A recursive tree listing component. + */ +@Component({ + selector: 'app-tree-root', + templateUrl: './tree-root.component.html', + styleUrls: ['./tree-root.component.scss'], +}) +export class TreeRootComponent implements OnInit { + /** The immediate children of this component. */ + @ViewChildren('childComponents') + protected childComponents; + + /** How deeply nested this component is in the tree. */ + @Input() + public nestingLevel = 0; + + /** Field on the listing record with the display string. */ + @Input() + public displayField = 'name'; + + /** Field on the listing record that contains the children of an item. */ + @Input() + public childrenField = 'children'; + + /** Optionally, field on the listing record that contains the icon class. */ + @Input() + public iconClassField?: string; + + /** The nested listing items. */ + @Input() + public items: any[] = []; + + /** @private Set by the parent component when recursively nested. */ + @Input() + public parent?: TreeRootComponent; + + /** @private Item object that created this listing when recursively nested. */ + @Input() + public parentItem?: any; + + /** Emits the selected item, when top-level tree. */ + @Output() + public itemSelected: EventEmitter<{ event: MouseEvent, item: any }> = new EventEmitter<{event: MouseEvent; item: any}>(); + + /** Emits the activated item, when top-level tree. */ + @Output() + public itemActivated: EventEmitter<{ event: MouseEvent, item: any }> = new EventEmitter<{event: MouseEvent; item: any}>(); + + /** Emits the right-clicked item, when top-level tree. */ + @Output() + public itemRightClicked: EventEmitter<{ event: MouseEvent, item: any }> = new EventEmitter<{event: MouseEvent; item: any}>(); + + /** The selected item record at this level, if one exists. */ + public selectedItem?: any; + + /** The current tree filter value, if one exists. */ + protected filterValue?: string; + + /** Called on initialization. */ + ngOnInit() { + if ( !this.parent ) { + debug('Top-level tree root component:', this); + } + } + + /** True if we're in dark mode. */ + public isDark() { + return document.body.classList.contains('dark'); + } + + /** Recursively filter the tree using the given quick-filter string. */ + public filterTree(filterValue?: string) { + this.setFilterRecursively(filterValue); + } + + /** Emit an item selected event, or bubble up to the parent. */ + public emitItemSelected(event: MouseEvent, item: any) { + if ( this.parent ) { + return this.parent.emitItemSelected(event, item); + } + + this.itemSelected.emit({ + event, + item, + }); + } + + /** Emit an item activated event, or bubble up to the parent. */ + public emitItemActivated(event: MouseEvent, item: any) { + if ( this.parent ) { + return this.parent.emitItemActivated(event, item); + } + + this.itemActivated.emit({ + event, + item, + }); + } + + /** Emit an item right-clicked event, or bubble up to the parent. */ + public emitItemRightClicked(event: MouseEvent, item: any) { + if ( this.parent ) { + return this.parent.emitItemRightClicked(event, item); + } + + this.itemRightClicked.emit({ + event, + item, + }); + } + + /** Wrapper for checking arrays for use in NG templates. */ + public isArray(something: unknown): boolean { + return Array.isArray(something); + } + + /** + * Returns true if an item has nested children. + * @param item + * @param includeHidden - if true, will include children hidden by the quick-filter + */ + public hasNesting(item: any, includeHidden = false): boolean { + const hasUnfilteredNesting = item && this.isArray(item[this.childrenField]) && item[this.childrenField].length; + if ( !this.filterValue || includeHidden ) { + return hasUnfilteredNesting; + } + + return hasUnfilteredNesting && item[this.childrenField].some(child => !child.__app_tree_comp_is_hidden); + } + + /** Called when an item expand/collapse arrow is clicked. */ + public onItemHandleClick(event: MouseEvent, item: any) { + if ( this.hasNesting(item) ) { + this.setExpansionRecursively(item, !item.__app_tree_comp_is_collapsed); + } + } + + /** Called when an item is clicked. */ + public onItemClick(event: MouseEvent, item: any) { + this.clearSelection(); + this.selectedItem = item; + this.emitItemSelected(event, item); + } + + /** Called when an item is double-clicked. */ + public onItemDoubleClick(event: MouseEvent, item: any) { + this.emitItemActivated(event, item); + } + + /** Called when an item is right-clicked. */ + public onItemRightClick(event: MouseEvent, item: any) { + this.clearSelection(); + this.selectedItem = item; + this.emitItemRightClicked(event, item); + } + + /** Recursively clear the selection from the entire tree, calling children and bubbling up to parent. */ + public clearSelection(from?: TreeRootComponent) { + this.selectedItem = undefined; + + if ( this.childComponents ) { + for ( const child of this.childComponents ) { + if ( child === from ) { + continue; + } + + child.clearSelection(this); + } + } + + if ( this.parent ) { + if ( this.parent !== from ) { + this.parent.clearSelection(this); + } + } + } + + /** + * Recursively set the filter on this subtree. + * Returns true if any of the children of this level match the filter. + * @param filterValue + * @param from + * @protected + */ + protected setFilterRecursively(filterValue?: string, from?: TreeRootComponent): boolean { + this.filterValue = filterValue; + + let anyChildMatchesFilter = false; + for ( const item of this.items ) { + item.__app_tree_comp_is_hidden = this.getFilterHiddenStatusForItem(item); + anyChildMatchesFilter = anyChildMatchesFilter || !item.__app_tree_comp_is_hidden; + } + + return anyChildMatchesFilter; + } + + /** + * Returns true if the given item should be hidden with the current quick-filter. + * @param item + * @protected + */ + protected getFilterHiddenStatusForItem(item: any): boolean { + const itemMatchesFilter = ( + !this.filterValue + || ( + item[this.displayField]?.trim()?.toLowerCase()?.includes(this.filterValue?.trim()?.toLowerCase()) + ) + ); + + if ( !this.hasNesting(item, true) ) { + return !itemMatchesFilter; + } + + let anyChildMatchesFilter = false; + const childTree = this.getChildComponentForItem(item); + if ( childTree ) { + anyChildMatchesFilter = childTree.setFilterRecursively(this.filterValue, this); + } else { + debug('Unable to find childTree for item:', item[this.displayField]); + } + + return !(!this.filterValue || itemMatchesFilter || anyChildMatchesFilter); + } + + /** + * Get the TreeRootComponent instance for some item's children, if it exists. + * @param item + * @protected + */ + protected getChildComponentForItem(item: any): TreeRootComponent | undefined { + return [...(this.childComponents || [])]?.find(child => { + return child.parentItem === item; + }); + } + + /** + * Expand or collapse an item recursively. + * @param item + * @param isCollapsed + */ + public setExpansionRecursively(item: any, isCollapsed: boolean) { + item.__app_tree_comp_is_collapsed = isCollapsed; + if ( this.hasNesting(item, true) && isCollapsed ) { + for ( const child of item.children ) { + this.setExpansionRecursively(child, isCollapsed); + } + } + } +} diff --git a/src/app/service/api.service.ts b/src/app/service/api.service.ts index 7874eb4..c191932 100644 --- a/src/app/service/api.service.ts +++ b/src/app/service/api.service.ts @@ -15,6 +15,7 @@ import {Page} from './db/Page'; import {PageNode} from './db/PageNode'; import {debug} from '../utility'; import HostRecord from '../structures/HostRecord'; +import {NodeTypeIcons} from '../structures/node-types'; export class ResourceNotAvailableOfflineError extends Error { constructor(msg = 'This resource is not yet available offline on this device.') { @@ -558,7 +559,7 @@ export class ApiService { const tree: any[] = await new Promise(res2 => { this.get(loadUrl).subscribe({ next: async result => { - const nodes = result.data as any[]; + const nodes = this.setMenuItemIconKeys(result.data as any[]); const items = MenuItem.deflateTree(nodes); // Update the locally stored nodes @@ -577,6 +578,21 @@ export class ApiService { }); } + protected setMenuItemIconKeys(nodes: any[]): any[] { + return nodes.map(node => { + node.faIconClass = NodeTypeIcons[node.type]; + if ( !node.faIconClass ) { + debug('Unable to map type icon class for menu item type:', node.faIconClass, node); + } + + if ( Array.isArray(node.children) ) { + node.children = this.setMenuItemIconKeys(node.children); + } + + return node; + }); + } + public getSessionData(): Promise { return new Promise(async (res, rej) => { const sessionKV = await this.db.getKeyValue('session_data'); diff --git a/src/app/service/db/MenuItem.ts b/src/app/service/db/MenuItem.ts index 683c9f6..0437f87 100644 --- a/src/app/service/db/MenuItem.ts +++ b/src/app/service/db/MenuItem.ts @@ -13,6 +13,7 @@ export interface IMenuItem { shared?: boolean; needsServerUpdate?: boolean; offlineUpdatedAt?: string; + faIconClass?: string; } export class MenuItem extends Model implements IMenuItem { @@ -27,13 +28,15 @@ export class MenuItem extends Model implements IMenuItem { shared?: boolean; needsServerUpdate?: boolean; offlineUpdatedAt?: string; + faIconClass?: string; public static getTableName() { return 'menuItems'; } public static getSchema() { - return '++id, serverId, name, childIds, noDelete, noChildren, virtual, type, shared, needsServerUpdate, offlineUpdatedAt'; + // tslint:disable-next-line:max-line-length + return '++id, serverId, name, childIds, noDelete, noChildren, virtual, type, shared, needsServerUpdate, offlineUpdatedAt, faIconClass'; } public static deflateTree(nodes: any[]): MenuItem[] { @@ -41,7 +44,17 @@ export class MenuItem extends Model implements IMenuItem { for ( const node of nodes ) { const childIds = node.children ? node.children.map(x => x.id) : []; - const item = new MenuItem(node.name, node.id, childIds, node.noDelete, node.noChildren, node.virtual, node.type, node.shared); + const item = new MenuItem( + node.name, + node.id, + childIds, + node.noDelete, + node.noChildren, + node.virtual, + node.type, + node.shared, + node.faIconClass + ); items.push(item); if ( node.children ) { @@ -114,6 +127,7 @@ export class MenuItem extends Model implements IMenuItem { shared?: boolean, needsServerUpdate?: boolean, offlineUpdatedAt?: string, + faIconClass?: string, id?: number ) { super(); @@ -152,6 +166,8 @@ export class MenuItem extends Model implements IMenuItem { this.offlineUpdatedAt = offlineUpdatedAt; } + this.faIconClass = faIconClass; + if ( id ) { this.id = id; } @@ -174,6 +190,7 @@ export class MenuItem extends Model implements IMenuItem { ...(typeof this.shared !== 'undefined' ? { shared: this.shared } : {}), ...(typeof this.needsServerUpdate !== 'undefined' ? { needsServerUpdate: this.needsServerUpdate } : {}), ...(typeof this.offlineUpdatedAt !== 'undefined' ? { offlineUpdatedAt: this.offlineUpdatedAt } : {}), + ...(typeof this.faIconClass !== 'undefined' ? { faIconClass: this.faIconClass } : {}), }; } } diff --git a/src/global.scss b/src/global.scss index 05715d6..c18da98 100644 --- a/src/global.scss +++ b/src/global.scss @@ -25,7 +25,6 @@ @import "~@ionic/angular/css/text-transformation.css"; @import "~@ionic/angular/css/flex-utils.css"; -@import '~@circlon/angular-tree-component/css/angular-tree-component.css'; @import "~ag-grid-community/dist/styles/ag-grid.css"; @import "~ag-grid-community/dist/styles/ag-theme-balham.css"; @import "~ag-grid-community/dist/styles/ag-theme-balham-dark.css"; @@ -61,6 +60,8 @@ --noded-background-form: #F2C57C; --noded-color-form: white; --noded-background-form-hover: #F8DEB5; + + --noded-color-danger: #dd2222 } .noded-note { @@ -91,6 +92,10 @@ color: var(--noded-background-form); } +.noded-danger { + color: var(--noded-color-danger); +} + div.picker-wrapper { border: 2px solid lightgrey !important; border-radius: 7px !important; @@ -173,6 +178,13 @@ ionic-selectable-modal { } } +.modal-tall { + .modal-wrapper { + min-height: calc(100vh - 30px); + min-width: calc(100vw - 200px); + } +} + .modal-med { .modal-wrapper { min-height: calc(100vh - 100px); diff --git a/src/polyfills.ts b/src/polyfills.ts index 14769a1..f1852ba 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -64,3 +64,4 @@ import 'zone.js/dist/zone'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ +(window as any).global = window;