Skip to content

Commit

Permalink
Organization List Component, as well as user data retrieval from back…
Browse files Browse the repository at this point in the history
…end (#3)

* Suite of changes to enable the organization selector, as well as user data retrieval from the v3_users_current endpoint

* remove nav service

* use user attributes for simplicity

* do not include an unused response type

* do not include an unused response type

* remove some red underlines

* fix some additional linter errors

* Organization updates

---------

Co-authored-by: Alex Swindler <[email protected]>
  • Loading branch information
crutan and axelstudios authored Jan 30, 2025
1 parent 71fa046 commit 14c7f2f
Show file tree
Hide file tree
Showing 27 changed files with 340 additions and 84 deletions.
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ export default tseslint.config(
...angular.configs.templateAccessibility,
],
rules: {
'@angular-eslint/template/prefer-control-flow': 'error',
// TODO
'@angular-eslint/template/click-events-have-key-events': 'off',
'@angular-eslint/template/interactive-supports-focus': 'off',
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@
"watch": "ng build --watch -c development",
"test": "ng test",
"eslint": "ng lint",
"eslint:fix": "run-s eslint -- --fix",
"eslint:fix": "prettier -w \"src/**/*.ts\" && npm run eslint -- --fix",
"lint": "run-p -c eslint prettier stylelint",
"lint:fix": "run-p -c eslint:fix prettier:fix stylelint:fix",
"prettier": "prettier -c \"src/**/*.html\"",
"prettier:fix": "run-s prettier -- -w",
"prettier:fix": "npm run prettier -- -w",
"stylelint": "stylelint \"src/**/*.scss\"",
"stylelint:fix": "run-s stylelint -- --fix",
"stylelint:fix": "npm run stylelint -- --fix",
"update-translations": "node --env-file=.env --import=tsx update-translations.mts"
},
"dependencies": {
Expand Down
36 changes: 36 additions & 0 deletions src/@seed/api/column/column.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
export type Column = {
id: number;
name: string;
organization_id: number;
table_name: 'PropertyState' | 'TaxLotState';
merge_protection: 'Favor New' | 'Favor Existing';
shared_field_type: 'None' | 'Public';
column_name: string;
is_extra_data: boolean;
unit_name: null;
unit_type: null;
display_name: string;
data_type:
| 'number'
| 'float'
| 'integer'
| 'string'
| 'geometry'
| 'datetime'
| 'date'
| 'boolean'
| 'area'
| 'eui'
| 'ghg_intensity'
| 'ghg'
| 'wui'
| 'water_use';
is_matching_criteria: boolean;
is_updating: boolean;
geocoding_order: number;
recognize_empty: boolean;
comstock_mapping: string | null;
column_description: string;
derived_column: number | null;
is_excluded_from_hash: boolean;
}
1 change: 1 addition & 0 deletions src/@seed/api/column/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './column.types'
18 changes: 18 additions & 0 deletions src/@seed/api/cycle/cycle.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export type Cycle = {
name: string;
start: string;
end: string;
organization: number;
user: number | null;
id: number;
}

export type ListCyclesResponse = {
status: string;
cycles: Cycle[];
}

export type GetCycleResponse = {
status: string;
cycles: Cycle;
}
1 change: 1 addition & 0 deletions src/@seed/api/cycle/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './cycle.types'
2 changes: 2 additions & 0 deletions src/@seed/api/organization/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './organization.service'
export * from './organization.types'
36 changes: 36 additions & 0 deletions src/@seed/api/organization/organization.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { HttpClient } from '@angular/common/http'
import { inject, Injectable } from '@angular/core'
import type { Observable } from 'rxjs'
import { map, ReplaySubject, tap } from 'rxjs'
import { naturalSort } from '../../utils'
import type { BriefOrganization, Organization, OrganizationsResponse } from './organization.types'

@Injectable({ providedIn: 'root' })
export class OrganizationService {
private _httpClient = inject(HttpClient)
private _organizations: ReplaySubject<BriefOrganization[]> = new ReplaySubject<BriefOrganization[]>(1)
organizations$ = this._organizations.asObservable()

get(): Observable<Organization[]> {
return this._get(false) as Observable<Organization[]>
}

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

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(
map(({ organizations }) => {
return organizations.toSorted((a, b) => naturalSort(a.name, b.name))
}),
tap((organizations) => {
// TODO not sure if we actually want to cache this in the replaySubject
if (brief) {
this._organizations.next(organizations)
}
}),
)
}
}
78 changes: 78 additions & 0 deletions src/@seed/api/organization/organization.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { UserRole } from '@seed/api/user'
import type { Column } from '../column'

type OrgCycle = {
name: string;
cycle_id: number;
num_properties: number;
num_taxlots: number;
}

type OrgUser = {
first_name: string;
last_name: string;
email: string;
id: number;
}

export type BriefOrganization = {
name: string;
org_id: number;
parent_id: number | null;
is_parent: boolean;
id: number;
user_role: UserRole;
display_decimal_places: number;
salesforce_enabled: boolean;
access_level_names: string[];
audit_template_conditional_import: boolean;
property_display_field: string;
taxlot_display_field: string;
}

export type Organization = BriefOrganization & {
number_of_users: number;
user_is_owner: boolean;
owners: OrgUser[];
sub_orgs: (Organization & { is_parent: false })[];
parent_id: number;
display_units_eui: string;
display_units_ghg: string;
display_units_ghg_intensity: string;
display_units_water_use: string;
display_units_wui: string;
display_units_area: string;
cycles: OrgCycle[];
created: string;
mapquest_api_key: string;
geocoding_enabled: boolean;
better_analysis_api_key: string;
better_host_url: string;
display_meter_units: Record<string, string>;
display_meter_water_units: Record<string, string>;
thermal_conversion_assumption: number;
comstock_enabled: boolean;
new_user_email_from: string;
new_user_email_subject: string;
new_user_email_content: string;
new_user_email_signature: string;
at_organization_token: string;
at_host_url: string;
audit_template_user: string;
audit_template_password: string;
audit_template_city_id: number | null;
audit_template_report_type: string;
audit_template_status_types: string;
audit_template_sync_enabled: boolean;
ubid_threshold: number;
inventory_count: number;
public_feed_enabled: boolean;
public_feed_labels: boolean;
public_geojson_enabled: boolean;
default_reports_x_axis_options: Column[];
default_reports_y_axis_options: Column[];
require_2fa: boolean;
}
export type OrganizationsResponse = {
organizations: (BriefOrganization | Organization)[];
}
2 changes: 2 additions & 0 deletions src/@seed/api/user/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './user.service'
export * from './user.types'
42 changes: 42 additions & 0 deletions src/@seed/api/user/user.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { HttpClient } from '@angular/common/http'
import { inject, Injectable } from '@angular/core'
import type { Observable } from 'rxjs'
import { ReplaySubject, switchMap, take, tap } from 'rxjs'
import type { CurrentUser, SetDefaultOrganizationResponse } from '@seed/api/user'

@Injectable({ providedIn: 'root' })
export class UserService {
private _httpClient = inject(HttpClient)
private _currentUser: ReplaySubject<CurrentUser> = new ReplaySubject<CurrentUser>(1)
currentUser$ = this._currentUser.asObservable()

/**
* Get the current signed-in user data
*/
getCurrentUser(): Observable<CurrentUser> {
return this._httpClient.get<CurrentUser>('/api/v3/users/current/').pipe(
tap((user: CurrentUser) => {
this._currentUser.next(user)
}),
)
}

/**
* Set default org
*/
setDefaultOrganization(organizationId: number): Observable<SetDefaultOrganizationResponse> {
return this.currentUser$.pipe(
take(1),
switchMap(({ id: userId }) => {
return this._httpClient.put<SetDefaultOrganizationResponse>(
`/api/v3/users/${userId}/default_organization/?organization_id=${organizationId}`,
{},
)
}),
tap(() => {
// Refresh user info after changing the organization
this.getCurrentUser().subscribe()
}),
)
}
}
30 changes: 30 additions & 0 deletions src/@seed/api/user/user.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export type UserRole = 'viewer' | 'member' | 'owner'

export type CurrentUser = {
org_id: number;
org_name: string;
org_role: UserRole;
ali_name: string;
ali_id: number;
is_ali_root: boolean;
is_ali_leaf: boolean;
pk: number;
id: number;
first_name: string;
last_name: string;
email: string;
username: string;
is_superuser: boolean;
api_key: string;
}

export type SetDefaultOrganizationResponse = {
status: string;
user: {
id: number;
access_level_instance: {
id: number;
name: string;
};
};
}
1 change: 1 addition & 0 deletions src/@seed/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './exact-match-options'
export * from './natural-sort'
export * from './open-in-new-tab'
export * from './random-id'
export * from './sha256'
Expand Down
1 change: 1 addition & 0 deletions src/@seed/utils/natural-sort.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const { compare: naturalSort } = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })
6 changes: 5 additions & 1 deletion src/app/app.resolvers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { inject } from '@angular/core'
import { forkJoin } from 'rxjs'
import { ConfigService } from '@seed/api/config'
import { OrganizationService } from '@seed/api/organization/organization.service'
import { UserService } from '@seed/api/user'
import { VersionService } from '@seed/api/version'

export const configResolver = () => {
Expand All @@ -9,8 +11,10 @@ export const configResolver = () => {
}

export const initialDataResolver = () => {
const organizationService = inject(OrganizationService)
const userService = inject(UserService)
const versionService = inject(VersionService)

// Fork join multiple API endpoint calls to wait on all of them to finish
return forkJoin([versionService.get()])
return forkJoin([versionService.get(), userService.getCurrentUser(), organizationService.getBrief()])
}
5 changes: 1 addition & 4 deletions src/app/core/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { inject, Injectable } from '@angular/core'
import { Router } from '@angular/router'
import type { Observable } from 'rxjs'
import { map, of, tap, throwError } from 'rxjs'
import { UserService } from '@seed/api/user'
import { AuthUtils } from 'app/core/auth/auth.utils'
import { UserService } from 'app/core/user/user.service'
import type { TokenResponse } from './auth.types'

@Injectable({ providedIn: 'root' })
Expand Down Expand Up @@ -70,9 +70,6 @@ export class AuthService {

// Set the authenticated flag to true
this._authenticated = true

// Store the user on the user service
this._userService.user = AuthUtils.tokenUser(this.accessToken)
}

signOut(): Observable<boolean> {
Expand Down
3 changes: 0 additions & 3 deletions src/app/core/auth/auth.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,4 @@ export type UserToken = {
iat: number;
jti: string;
user_id: number;
name: string;
username: string;
email: string;
}
11 changes: 0 additions & 11 deletions src/app/core/auth/auth.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,9 @@
// https://github.com/auth0/angular2-jwt
// -----------------------------------------------------------------------------------------------------
import { jwtDecode } from 'jwt-decode'
import type { User } from '../user/user.types'
import type { UserToken } from './auth.types'

export class AuthUtils {
static tokenUser(token: string): User {
const { email, name, user_id, username } = this._decodeToken(token)
return {
id: user_id,
name,
username,
email,
}
}

static isTokenExpired(token: string): boolean {
// Return if there is no token
if (!token) {
Expand Down
Loading

0 comments on commit 14c7f2f

Please sign in to comment.