Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Loading