Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Organizations pages ui and routing #1

Merged
merged 42 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
69d9f09
basic docker config
Jan 16, 2025
6a4fe7f
organizations routing base
Jan 22, 2025
79a9ffa
initial organizations pages ui
Jan 23, 2025
30c7819
fetch current org
Jan 23, 2025
699c8a1
non inventory org pages
Jan 23, 2025
ae7b3cc
inventory specific pages
Jan 24, 2025
ee38c2c
Merge branch 'main' into feature/organizations-ui
perryr16 Feb 3, 2025
fb20549
remove docker config
perryr16 Feb 3, 2025
e5dd452
remove duplicate host
perryr16 Feb 3, 2025
30ed831
lint
perryr16 Feb 3, 2025
064a240
prettier
perryr16 Feb 3, 2025
d667d8b
moved navigation to sidebar
crutan Feb 3, 2025
3ab79bf
remove org id from org rotues
perryr16 Feb 3, 2025
dbffdeb
prevent nav collapse on /taxlots
perryr16 Feb 3, 2025
64f6faa
organization backend now active
crutan Feb 4, 2025
46b8c37
now with async pipe for simpler componentry
crutan Feb 4, 2025
ea4c14f
lint - convert from ngIf to control-flow style w/ @if
crutan Feb 4, 2025
be13214
basic docker config
Jan 16, 2025
0214315
organizations routing base
Jan 22, 2025
5adce69
initial organizations pages ui
Jan 23, 2025
20d8fc0
fetch current org
Jan 23, 2025
02e165e
non inventory org pages
Jan 23, 2025
d9ca2ea
inventory specific pages
Jan 24, 2025
637d304
remove docker config
perryr16 Feb 3, 2025
90e7864
remove duplicate host
perryr16 Feb 3, 2025
03835bf
lint
perryr16 Feb 3, 2025
1684162
prettier
perryr16 Feb 3, 2025
56f62db
moved navigation to sidebar
crutan Feb 3, 2025
85438f7
remove org id from org rotues
perryr16 Feb 3, 2025
e201d40
prevent nav collapse on /taxlots
perryr16 Feb 3, 2025
53496a7
organization backend now active
crutan Feb 4, 2025
ab12411
now with async pipe for simpler componentry
crutan Feb 4, 2025
496cd2d
lint - convert from ngIf to control-flow style w/ @if
crutan Feb 4, 2025
c90d15d
revert collapsible component changes for now
crutan Feb 4, 2025
c21752c
org cycles page
perryr16 Feb 4, 2025
1fac901
re-add forwardRef calls to make the nav components work again
crutan Feb 4, 2025
9c4af32
cycles header
perryr16 Feb 4, 2025
ba47045
merge branches
perryr16 Feb 4, 2025
982dabf
lint
perryr16 Feb 4, 2025
e318361
linter fix for isActive link in the vertical component
crutan Feb 4, 2025
17b97af
update column mapping icon
kflemin Feb 5, 2025
e9d145d
lint
kflemin Feb 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/@seed/api/cycle/cycle.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { HttpClient } from '@angular/common/http'
import { inject, Injectable } from '@angular/core'
import { BehaviorSubject, map } from 'rxjs'
import { OrganizationService } from '@seed/api/organization'
import type { Cycle, ListCyclesResponse } from './cycle.types'

@Injectable({ providedIn: 'root' })
export class CycleService {
private _httpClient = inject(HttpClient)
private _organizationService = inject(OrganizationService)
private _cycles = new BehaviorSubject<Cycle[]>([])

cycles$ = this._cycles.asObservable()

get(): void {
// fetch current organization
this._organizationService.currentOrganization$.subscribe(({ org_id }) => {
const url = `/api/v3/cycles/?organization_id=${org_id}`
// fetch cycles
this._httpClient
.get<ListCyclesResponse>(url)
.pipe(map((response) => response.cycles))
.subscribe({
next: (cycles) => {
this._cycles.next(cycles)
},
error: (error) => {
console.error('Error fetching cycles:', error)
},
})
})
}
}
41 changes: 37 additions & 4 deletions src/@seed/api/organization/organization.service.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,57 @@
import type { HttpErrorResponse } from '@angular/common/http'
import { HttpClient } from '@angular/common/http'
import { inject, Injectable } from '@angular/core'
import type { Observable } from 'rxjs'
import { map, ReplaySubject, tap } from 'rxjs'
import { catchError, map, of, ReplaySubject, Subject, takeUntil, tap } from 'rxjs'
import { naturalSort } from '../../utils'
import type { BriefOrganization, Organization, OrganizationsResponse } from './organization.types'
import { UserService } from '../user'
import type { BriefOrganization, Organization, OrganizationResponse, OrganizationsResponse } from './organization.types'

@Injectable({ providedIn: 'root' })
export class OrganizationService {
private _httpClient = inject(HttpClient)
private _userService = inject(UserService)
private _organizations = new ReplaySubject<BriefOrganization[]>(1)
private _currentOrganization = new ReplaySubject<Organization>(1)
private readonly _unsubscribeAll$ = new Subject<void>()

organizations$ = this._organizations.asObservable()
currentOrganization$ = this._currentOrganization.asObservable()

constructor() {
// Fetch current org data whenever user org id changes
this._userService.currentOrganizationId$.pipe(takeUntil(this._unsubscribeAll$)).subscribe((organizationId) => {
this.getById(organizationId).subscribe()
})
}

get(): Observable<Organization[]> {
return this._get(false) as Observable<Organization[]>
get(org_id?: number): Observable<Organization[]> | Observable<Organization> {
if (org_id) {
return this.getById(org_id)
} else {
return this._get(false) as Observable<Organization[]>
}
}

getBrief(): Observable<BriefOrganization[]> {
return this._get(true)
}

getById(org_id: number): Observable<Organization> {
const url = `/api/v3/organizations/${org_id}/`
return this._httpClient.get<OrganizationResponse>(url).pipe(
map((or) => {
this._currentOrganization.next(or.organization)
return or.organization
}),
catchError((error: HttpErrorResponse) => {
// TODO need to figure out error handling
console.error('Error occurred fetching organization: ', error.error)
return of({} as Organization)
}),
)
}

private _get(brief = false): Observable<(BriefOrganization | Organization)[]> {
const url = brief ? '/api/v3/organizations/?brief=true' : '/api/v3/organizations/'
return this._httpClient.get<OrganizationsResponse>(url).pipe(
Expand Down
4 changes: 4 additions & 0 deletions src/@seed/api/organization/organization.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ export type Organization = BriefOrganization & {
default_reports_y_axis_options: Column[];
require_2fa: boolean;
}
export type OrganizationResponse = {
status: string;
organization: Organization;
}
export type OrganizationsResponse = {
organizations: (BriefOrganization | Organization)[];
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { NgClass, NgTemplateOutlet } from '@angular/common'
import type { OnDestroy, OnInit } from '@angular/core'
import { booleanAttribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, input, viewChild } from '@angular/core'
import {
booleanAttribute,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
forwardRef,
inject,
input,
viewChild,
} from '@angular/core'
import { MatIconModule } from '@angular/material/icon'
import type { MatMenu } from '@angular/material/menu'
import { MatMenuModule } from '@angular/material/menu'
Expand All @@ -18,6 +27,7 @@ import type { NavigationItem } from '@seed/components/navigation/navigation.type
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
HorizontalNavigationBasicItemComponent,
forwardRef(() => HorizontalNavigationBranchItemComponent),
HorizontalNavigationDividerItemComponent,
MatIconModule,
MatMenuModule,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { BooleanInput } from '@angular/cdk/coercion'
import { NgClass } from '@angular/common'
import type { OnDestroy, OnInit } from '@angular/core'
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, inject, input } from '@angular/core'
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, HostBinding, inject, input } from '@angular/core'
import { MatIconModule } from '@angular/material/icon'
import { MatTooltipModule } from '@angular/material/tooltip'
import { NavigationEnd, Router } from '@angular/router'
Expand All @@ -26,12 +27,15 @@ import { exactMatchOptions, subsetMatchOptions } from '@seed/utils'
MatTooltipModule,
MatIconModule,
VerticalNavigationBasicItemComponent,
forwardRef(() => VerticalNavigationCollapsibleItemComponent),
VerticalNavigationDividerItemComponent,
VerticalNavigationGroupItemComponent,
VerticalNavigationSpacerItemComponent,
],
})
export class VerticalNavigationCollapsibleItemComponent implements OnInit, OnDestroy {
static ngAcceptInputType_autoCollapse: BooleanInput

private _changeDetectorRef = inject(ChangeDetectorRef)
private _router = inject(Router)
private _navigationService = inject(SeedNavigationService)
Expand All @@ -45,6 +49,9 @@ export class VerticalNavigationCollapsibleItemComponent implements OnInit, OnDes
private _verticalNavigationComponent: VerticalNavigationComponent
private readonly _unsubscribeAll$ = new Subject<void>()

/**
* Host binding for component classes
*/
@HostBinding('class') get classList(): Record<string, boolean> {
return {
'seed-vertical-navigation-item-collapsed': this.isCollapsed,
Expand All @@ -61,7 +68,7 @@ export class VerticalNavigationCollapsibleItemComponent implements OnInit, OnDes
this.expand()
} else {
// If the autoCollapse is on, collapse...
if (this.autoCollapse()) {
if (this.autoCollapse) {
this.collapse()
}
}
Expand All @@ -80,7 +87,7 @@ export class VerticalNavigationCollapsibleItemComponent implements OnInit, OnDes
})

// Listen for the onCollapsibleItemExpanded from the service if the autoCollapse is on
if (this.autoCollapse()) {
if (this.autoCollapse) {
this._verticalNavigationComponent.onCollapsibleItemExpanded.pipe(takeUntil(this._unsubscribeAll$)).subscribe((expandedItem) => {
// Check if the expanded item is null
if (expandedItem === null) {
Expand Down Expand Up @@ -119,7 +126,7 @@ export class VerticalNavigationCollapsibleItemComponent implements OnInit, OnDes
this.expand()
} else {
// If the autoCollapse is on, collapse...
if (this.autoCollapse()) {
if (this.autoCollapse) {
this.collapse()
}
}
Expand Down Expand Up @@ -222,7 +229,7 @@ export class VerticalNavigationCollapsibleItemComponent implements OnInit, OnDes
}

// Check if the child has a link and is active
if (child.link && this._router.isActive(child.link, child.exactMatch ? exactMatchOptions : subsetMatchOptions)) {
if (child.link && this._router.isActive(child.id, child.exactMatch ? exactMatchOptions : subsetMatchOptions)) {
return true
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NgClass } from '@angular/common'
import type { OnDestroy, OnInit } from '@angular/core'
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, input } from '@angular/core'
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, inject, input } from '@angular/core'
import { MatIconModule } from '@angular/material/icon'
import { Subject, takeUntil } from 'rxjs'
import type { NavigationItem, VerticalNavigationComponent } from '@seed/components'
Expand All @@ -22,6 +22,7 @@ import {
VerticalNavigationBasicItemComponent,
VerticalNavigationCollapsibleItemComponent,
VerticalNavigationDividerItemComponent,
forwardRef(() => VerticalNavigationGroupItemComponent),
VerticalNavigationSpacerItemComponent,
],
})
Expand Down
6 changes: 6 additions & 0 deletions src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ export const appRoutes: Route[] = [
},
{ path: 'contact', title: 'Contact', component: ContactComponent },
{ path: 'about', title: 'About', component: AboutComponent },
{ path: 'contact', title: 'Contact', component: ContactComponent },
{ path: 'documentation', title: 'Documentation', component: DocumentationComponent },
{
path: 'organizations',
loadChildren: () => import('app/modules/organizations/organizations.routes'),
},
// 404, redirect to dashboard
{ path: '**', redirectTo: 'dashboard' },
],
Expand Down
78 changes: 77 additions & 1 deletion src/app/core/navigation/navigation.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,85 @@ export class NavigationService {
{
id: 'organizations',
title: 'Organizations',
type: 'basic',
type: 'collapsible',
icon: 'fa-solid:users',
link: '/organizations',
children: [
{
id: 'organizations/settings',
link: 'organizations/settings',
title: 'Settings',
icon: 'fa-solid:gears',
type: 'basic',
},
{
id: 'organizations/access-level-tree',
link: '/organizations/access-level-tree',
title: 'Access Level Tree',
icon: 'fa-solid:sitemap',
type: 'basic',
},
{
id: 'organizations/column-mappings',
link: '/organizations/column-mappings/properties',
title: 'Column mappings',
icon: 'fa-solid:right-left',
type: 'basic',
regexMatch: /^\/organizations\/column-mappings\/(properties|taxlots)/,
},
{
id: 'organizations/column-settings',
link: '/organizations/column-settings/properties',
title: 'Column Settings',
icon: 'fa-solid:sliders',
type: 'basic',
regexMatch: /^\/organizations\/column-settings\/(properties|taxlots)/,
},
{
id: 'organizations/cycles',
link: '/organizations/cycles',
title: 'Cycles',
icon: 'fa-solid:calendar-days',
type: 'basic',
},
{
id: 'organizations/data-quality',
link: '/organizations/data-quality/properties',
title: 'Data Quality',
icon: 'fa-solid:flag',
type: 'basic',
regexMatch: /^\/organizations\/data-quality\/(properties|taxlots|goal)/,
},
{
id: 'organizations/derived-columns',
link: '/organizations/derived-columns/properties',
title: 'Derived Columns',
icon: 'fa-solid:calculator',
type: 'basic',
regexMatch: /^\/organizations\/derived-columns\/(properties|taxlots)/,
},
{
id: 'organizations/email-templates',
link: '/organizations/email-templates',
title: 'Email Templates',
icon: 'fa-solid:envelope',
type: 'basic',
},
{
id: 'organizations/labels',
link: '/organizations/labels',
title: 'Labels',
icon: 'fa-solid:tags',
type: 'basic',
},
{
id: 'organizations/members',
link: '/organizations/members',
title: 'Members',
icon: 'fa-solid:user',
type: 'basic',
},
],
},
{
id: 'insights',
Expand Down
2 changes: 1 addition & 1 deletion src/app/layout/layouts/main/main.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export class MainLayoutComponent implements OnInit, OnDestroy {
this.isScreenSmall = !matchingAliases.includes('md')

// Change the navigation appearance
this.navigationAppearance = this.isScreenSmall ? 'default' : 'dense'
this.navigationAppearance = this.isScreenSmall ? 'dense' : 'default'
})

this._versionService.version$.pipe(takeUntil(this._unsubscribeAll$)).subscribe(({ version, sha }) => {
Expand Down
12 changes: 12 additions & 0 deletions src/app/modules/organizations/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export * from './organizations-access-level-tree/organizations-access-level-tree.component'
export * from './organizations-column-mappings/organizations-column-mappings.component'
export * from './organizations-column-settings/organizations-column-settings.component'
export * from './organizations-cycles/organizations-cycles.component'
export * from './organizations-data-quality/organizations-data-quality.component'
export * from './organizations-derived-columns/organizations-derived-columns.component'
export * from './organizations-email-templates/organizations-email-templates.component'
export * from './organizations-labels/organizations-labels.component'
export * from './organizations-members/organizations-members.component'
export * from './organizations-list/organizations-list.component'
export * from './organizations-nav/organizations-nav.component'
export * from './organizations-settings/organizations-settings.component'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class="prose">Access Level Tree Content</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { OnInit } from '@angular/core'
import { Component } from '@angular/core'
import { MatIconModule } from '@angular/material/icon'

@Component({
selector: 'seed-organizations-access-level-tree',
templateUrl: './organizations-access-level-tree.component.html',
imports: [MatIconModule],
})
export class OrganizationsAccessLevelTreeComponent implements OnInit {
ngOnInit(): void {
console.log('organizations access level tree')
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<div class="flex min-w-0 flex-auto flex-col" *transloco="let t">
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to create a reusable component to handle the tab logic. You'll see this code repeat a few times

<div class="flex space-x-1">
@for (tab of tabs; track tab) {
<div
class="bg-default relative flex cursor-pointer self-start overflow-hidden rounded-t-xl border border-b-0 pb-1 pl-5 pr-4 pt-2"
[ngClass]="type === tab ? 'z-2' : ['bg-slate-50', 'dark:bg-slate-700']"
(click)="toggleInventoryType(tab)"
matRipple
>
<div class="overflow-hidden">
<div class="truncate font-medium leading-6">{{ t(tab) }}</div>
</div>
</div>
}
</div>

<div class="z-1 -mt-px flex-auto border-t pt-4 sm:pt-6">
<div class="mx-auto w-full max-w-screen-xl">{{ t(type) }} column mappings table</div>
</div>
</div>
Loading