Finish project store, update hal resource typing, update idFromLink typing

pull/9581/head
Benjamin Bädorf 3 years ago
parent 8f54d68124
commit 4ba1a0f340
No known key found for this signature in database
GPG Key ID: 069CA2D117AB5CCF
  1. 19
      frontend/src/app/core/state/hal-resource.ts
  2. 29
      frontend/src/app/core/state/in-app-notifications/in-app-notification.model.ts
  3. 32
      frontend/src/app/core/state/projects/project.model.ts
  4. 2
      frontend/src/app/features/bim/bcf/bcf-wp-attribute-group/bcf-wp-attribute-group.component.ts
  5. 2
      frontend/src/app/features/bim/ifc_models/ifc-viewer/ifc-viewer.service.ts
  6. 4
      frontend/src/app/features/hal/helpers/id-from-link.ts
  7. 2
      frontend/src/app/features/hal/resources/hal-resource.ts
  8. 4
      frontend/src/app/features/hal/resources/relation-resource.ts
  9. 21
      frontend/src/app/features/in-app-notifications/center/menu/menu.component.ts
  10. 38
      frontend/src/app/features/in-app-notifications/center/menu/state/ian-menu.query.ts
  11. 23
      frontend/src/app/features/in-app-notifications/center/menu/state/ian-menu.service.ts
  12. 4
      frontend/src/app/features/in-app-notifications/center/menu/state/ian-menu.store.ts
  13. 8
      frontend/src/app/features/in-app-notifications/center/state/ian-center.query.ts
  14. 12
      frontend/src/app/features/in-app-notifications/entry/in-app-notification-entry.component.ts
  15. 2
      frontend/src/app/features/invite-user-modal/project-selection/project-allowed.validator.ts
  16. 2
      frontend/src/app/features/user-preferences/notifications-settings/table/notification-settings-table.component.ts
  17. 2
      frontend/src/app/features/work-packages/components/wp-activity/activity-entry.component.ts
  18. 2
      frontend/src/app/features/work-packages/components/wp-new/wp-create.service.ts
  19. 4
      frontend/src/app/features/work-packages/components/wp-single-view/wp-single-view.component.ts
  20. 2
      frontend/src/app/shared/components/dynamic-forms/components/dynamic-inputs/select-input/select-input.component.ts
  21. 2
      frontend/src/app/shared/components/fields/edit/field-types/te-work-package-edit-field.component.ts
  22. 2
      frontend/src/app/shared/components/grids/widgets/news/news.component.ts

@ -0,0 +1,19 @@
export interface HalResourceLink {
href:string;
title:string;
}
/*
* TODO: This typing is not perfect, since overriding with more specific keys that are optional
* e.g. `project?:HalResourceLink;` is not possible. This would result in a possible undefined type.
* I'm not sure how to fix this, but it works for now.
*/
export type HalResourceLinks = Record<string, HalResourceLink|HalResourceLink[]>;
export type CustomTextFormat = 'markdown'|'custom';
export interface CustomText {
format:CustomTextFormat;
raw:string;
html:string;
}

@ -1,17 +1,17 @@
import { ID } from '@datorama/akita'; import { ID } from '@datorama/akita';
import {
export interface HalResourceLink { HalResourceLink,
href:string; HalResourceLinks,
title:string; CustomText,
} } from 'core-app/core/state/hal-resource';
export type InAppNotificationFormat = 'markdown'|'custom';
export const NOTIFICATIONS_MAX_SIZE = 100; export const NOTIFICATIONS_MAX_SIZE = 100;
export interface InAppNotificationDetail { export interface InAppNotificationHalResourceLinks extends HalResourceLinks {
format:InAppNotificationFormat; actor:HalResourceLink;
raw:string|null; project:HalResourceLink;
html:string; resource:HalResourceLink;
activity:HalResourceLink;
} }
export interface InAppNotification { export interface InAppNotification {
@ -23,16 +23,11 @@ export interface InAppNotification {
readIAN:boolean|null; readIAN:boolean|null;
readEmail:boolean|null; readEmail:boolean|null;
details?:InAppNotificationDetail[]; details?:CustomText[];
// Mark a notification to be kept in the center even though it was saved as "read". // Mark a notification to be kept in the center even though it was saved as "read".
keep?:boolean; keep?:boolean;
// Show message of a notification? // Show message of a notification?
expanded:boolean; expanded:boolean;
_links:{ _links:InAppNotificationHalResourceLinks;
actor?:HalResourceLink,
project?:HalResourceLink,
resource?:HalResourceLink,
activity?:HalResourceLink,
};
} }

@ -1,20 +1,32 @@
import { ID } from '@datorama/akita'; import { ID } from '@datorama/akita';
import {
HalResourceLink,
HalResourceLinks,
CustomText,
} from 'core-app/core/state/hal-resource';
export interface HalResourceLink {
href:string;
title:string;
}
export const PROJECTS_MAX_SIZE = 100; export const PROJECTS_MAX_SIZE = 100;
export interface ProjectHalResourceLinks extends HalResourceLinks {
categories:HalResourceLink;
delete:HalResourceLink;
parent:HalResourceLink;
self:HalResourceLink;
status:HalResourceLink;
schema:HalResourceLink;
}
export interface Project { export interface Project {
id:ID; id:ID;
identifier:string;
name:string;
public:boolean;
active:boolean;
statusExplanation:CustomText;
description:CustomText;
createdAt:string; createdAt:string;
updatedAt:string; updatedAt:string;
_links:{ _links:ProjectHalResourceLinks;
actor?:HalResourceLink,
project?:HalResourceLink,
resource?:HalResourceLink,
activity?:HalResourceLink,
};
} }

@ -163,7 +163,7 @@ export class BcfWpAttributeGroupComponent extends UntilDestroyedMixin implements
} }
async initialize(workPackage:WorkPackageResource) { async initialize(workPackage:WorkPackageResource) {
this.projectId = idFromLink(workPackage.project.href); this.projectId = String(idFromLink(workPackage.project.href));
this.viewAllowed = await this.bcfAuthorization.isAllowedTo(this.projectId, 'project_actions', 'viewTopic'); this.viewAllowed = await this.bcfAuthorization.isAllowedTo(this.projectId, 'project_actions', 'viewTopic');
this.createAllowed = await this.bcfAuthorization.isAllowedTo(this.projectId, 'topic_actions', 'createViewpoint'); this.createAllowed = await this.bcfAuthorization.isAllowedTo(this.projectId, 'topic_actions', 'createViewpoint');

@ -167,7 +167,7 @@ export class IFCViewerService extends ViewerBridgeService {
// and redirect to a route with a place to show viewer // and redirect to a route with a place to show viewer
// ('bim.partitioned.split') // ('bim.partitioned.split')
window.location.href = this.pathHelper.bimDetailsPath( window.location.href = this.pathHelper.bimDetailsPath(
idFromLink(workPackage.project.href), String(idFromLink(workPackage.project.href)),
workPackage.id!, workPackage.id!,
index, index,
); );

@ -1,4 +1,4 @@
export default function idFromLink(href:string|null):string { export default function idFromLink(href:string|null):number {
const idPart = (href || '').split('/').pop()!; const idPart = (href || '').split('/').pop()!;
return decodeURIComponent(idPart); return parseInt(decodeURIComponent(idPart), 10);
} }

@ -146,7 +146,7 @@ export class HalResource {
return this.$source.id.toString(); return this.$source.id.toString();
} }
const id = idFromLink(this.href); const id = String(idFromLink(this.href));
if (/^\d+$/.exec(id)) { if (/^\d+$/.exec(id)) {
return id; return id;
} }

@ -118,8 +118,8 @@ export class RelationResource extends HalResource {
*/ */
public get ids() { public get ids() {
return { return {
from: idFromLink(this.from.href!), from: String(idFromLink(this.from.href!)),
to: idFromLink(this.to.href!), to: String(idFromLink(this.to.href!)),
}; };
} }

@ -5,7 +5,7 @@ import {
OnInit, OnInit,
} from '@angular/core'; } from '@angular/core';
import { combineLatest } from 'rxjs'; import { combineLatest } from 'rxjs';
import { map } from 'rxjs/operators'; import { map, tap } from 'rxjs/operators';
import { StateService } from '@uirouter/core'; import { StateService } from '@uirouter/core';
import idFromLink from 'core-app/features/hal/helpers/id-from-link'; import idFromLink from 'core-app/features/hal/helpers/id-from-link';
import { I18nService } from 'core-app/core/i18n/i18n.service'; import { I18nService } from 'core-app/core/i18n/i18n.service';
@ -58,11 +58,20 @@ export class IanMenuComponent implements OnInit {
]; ];
notificationsByProject$ = this.ianMenuService.query.notificationsByProject$.pipe( notificationsByProject$ = this.ianMenuService.query.notificationsByProject$.pipe(
map((items) => items.map(item => ({ map((items) => items
...item, .map(item => ({
title: item.value, ...item,
href: this.getHrefForFilters({ filter: 'project', name: idFromLink(item._links.valueLink[0].href) }), title: (item.projectHasParent ? '...' : '') + item.value,
}))), href: this.getHrefForFilters({ filter: 'project', name: String(idFromLink(item._links.valueLink[0].href)) }),
}))
.sort((a, b) => {
if (b.projectHasParent && !a.projectHasParent) {
return -1;
}
return a.value.toLowerCase().localeCompare(b.value.toLowerCase());
}),
),
); );
notificationsByReason$ = this.ianMenuService.query.notificationsByReason$.pipe( notificationsByReason$ = this.ianMenuService.query.notificationsByReason$.pipe(

@ -1,15 +1,47 @@
import { Query } from '@datorama/akita'; import { Query } from '@datorama/akita';
import { combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import idFromLink from 'core-app/features/hal/helpers/id-from-link';
import { InAppNotificationsResourceService } from 'core-app/core/state/in-app-notifications/in-app-notifications.service'; import { InAppNotificationsResourceService } from 'core-app/core/state/in-app-notifications/in-app-notifications.service';
import { ProjectsResourceService } from 'core-app/core/state/projects/projects.service'; import { ProjectsResourceService } from 'core-app/core/state/projects/projects.service';
import { InAppNotification } from 'core-app/core/state/in-app-notifications/in-app-notification.model'; import { Project } from 'core-app/core/state/projects/project.model';
import { selectCollectionAsEntities$ } from 'core-app/core/state/collection-store'; import { collectionKey } from 'core-app/core/state/collection-store';
import { import {
IanMenuState, IanMenuState,
IanMenuStore, IanMenuStore,
} from './ian-menu.store'; } from './ian-menu.store';
export class IanMenuQuery extends Query<IanMenuState> { export class IanMenuQuery extends Query<IanMenuState> {
notificationsByProject$ = this.select('notificationsByProject'); projectsFilter$ = this.select('projectsFilter');
projectsForNotifications$ = combineLatest([
this.projectsFilter$,
this.projectsResourceService.query.select(),
]).pipe(
map(([filterParams, collectionData]) => {
const key = collectionKey(filterParams);
const collection = collectionData.collections[key];
const ids = collection?.ids || [];
return ids
.map((id:string) => this.projectsResourceService.query.getEntity(id))
.filter((item:Project|undefined) => !!item) as Project[];
}),
);
notificationsByProject$ = combineLatest([
this.select('notificationsByProject'),
this.projectsForNotifications$,
]).pipe(
map(([notifications, projects]) => notifications.map(notification => {
const project = projects.find(project => project.id === idFromLink(notification._links.valueLink[0].href));
return {
...notification,
projectHasParent: project?._links.parent.href ? true : false,
};
})),
);
notificationsByReason$ = this.select('notificationsByReason'); notificationsByReason$ = this.select('notificationsByReason');
constructor( constructor(

@ -2,18 +2,15 @@ import {
Injectable, Injectable,
Injector, Injector,
} from '@angular/core'; } from '@angular/core';
import { switchMap } from 'rxjs/operators'; import { notificationsMarkedRead } from 'core-app/core/state/in-app-notifications/in-app-notifications.actions';
import {
markNotificationsAsRead,
notificationsMarkedRead,
} from 'core-app/core/state/in-app-notifications/in-app-notifications.actions';
import { import {
EffectCallback, EffectCallback,
EffectHandler, EffectHandler,
} from 'core-app/core/state/effects/effect-handler.decorator'; } from 'core-app/core/state/effects/effect-handler.decorator';
import { ActionsService } from 'core-app/core/state/actions/actions.service'; import { ActionsService } from 'core-app/core/state/actions/actions.service';
import { APIV3Service } from 'core-app/core/apiv3/api-v3.service'; import { APIV3Service } from 'core-app/core/apiv3/api-v3.service';
// import idFromLink from 'core-app/features/hal/helpers/id-from-link'; import { Apiv3ListParameters } from 'core-app/core/apiv3/paths/apiv3-list-resource.interface';
import idFromLink from 'core-app/features/hal/helpers/id-from-link';
import { InAppNotificationsResourceService } from 'core-app/core/state/in-app-notifications/in-app-notifications.service'; import { InAppNotificationsResourceService } from 'core-app/core/state/in-app-notifications/in-app-notifications.service';
import { ProjectsResourceService } from 'core-app/core/state/projects/projects.service'; import { ProjectsResourceService } from 'core-app/core/state/projects/projects.service';
import { IanMenuQuery } from './ian-menu.query'; import { IanMenuQuery } from './ian-menu.query';
@ -52,13 +49,15 @@ export class IanMenuService {
public reload() { public reload() {
this.ianResourceService.fetchNotifications(IAN_MENU_PROJECT_FILTERS) this.ianResourceService.fetchNotifications(IAN_MENU_PROJECT_FILTERS)
.subscribe((data) => { .subscribe((data) => {
this.store.update({ notificationsByProject: data.groups }); const projectsFilter:Apiv3ListParameters = {
/*
this.projectsResourceService.fetchProjects({
pageSize: 100, pageSize: 100,
filters: [['id', '=', data.groups!.map(group => idFromLink(group._links.valueLink[0].href))]], filters: [['id', '=', data.groups!.map(group => String(idFromLink(group._links.valueLink[0].href)))]],
}).subscribe(); };
*/ this.store.update({
notificationsByProject: data.groups,
projectsFilter,
});
this.projectsResourceService.fetchProjects(projectsFilter).subscribe();
}); });
this.ianResourceService.fetchNotifications(IAN_MENU_REASON_FILTERS) this.ianResourceService.fetchNotifications(IAN_MENU_REASON_FILTERS)
.subscribe((data) => this.store.update({ notificationsByReason: data.groups })); .subscribe((data) => this.store.update({ notificationsByReason: data.groups }));

@ -6,7 +6,7 @@ import { Project } from 'core-app/core/state/projects/project.model';
export interface IanMenuGroupingData { export interface IanMenuGroupingData {
value:string; value:string;
count:number; count:number;
project?:Project; projectHasParent?:boolean;
_links:{ _links:{
valueLink:{ valueLink:{
href:string; href:string;
@ -17,6 +17,7 @@ export interface IanMenuGroupingData {
export interface IanMenuState { export interface IanMenuState {
notificationsByProject:IanMenuGroupingData[], notificationsByProject:IanMenuGroupingData[],
notificationsByReason:IanMenuGroupingData[], notificationsByReason:IanMenuGroupingData[],
projectsFilter: Apiv3ListParameters,
} }
export const IAN_MENU_PROJECT_FILTERS:Apiv3ListParameters = { export const IAN_MENU_PROJECT_FILTERS:Apiv3ListParameters = {
@ -35,6 +36,7 @@ export function createInitialState():IanMenuState {
return { return {
notificationsByProject: [], notificationsByProject: [],
notificationsByReason: [], notificationsByReason: [],
projectsFilter: {},
}; };
} }

@ -1,4 +1,8 @@
import { Query } from '@datorama/akita'; import { Query } from '@datorama/akita';
import {
map,
switchMap,
} from 'rxjs/operators';
import { import {
IAN_FACET_FILTERS, IAN_FACET_FILTERS,
IanCenterState, IanCenterState,
@ -6,10 +10,6 @@ import {
} from 'core-app/features/in-app-notifications/center/state/ian-center.store'; } from 'core-app/features/in-app-notifications/center/state/ian-center.store';
import { ApiV3ListFilter } from 'core-app/core/apiv3/paths/apiv3-list-resource.interface'; import { ApiV3ListFilter } from 'core-app/core/apiv3/paths/apiv3-list-resource.interface';
import { InAppNotificationsResourceService } from 'core-app/core/state/in-app-notifications/in-app-notifications.service'; import { InAppNotificationsResourceService } from 'core-app/core/state/in-app-notifications/in-app-notifications.service';
import {
map,
switchMap,
} from 'rxjs/operators';
import { Apiv3ListParameters } from 'core-app/core/apiv3/paths/apiv3-list-resource.interface'; import { Apiv3ListParameters } from 'core-app/core/apiv3/paths/apiv3-list-resource.interface';
import { InAppNotification } from 'core-app/core/state/in-app-notifications/in-app-notification.model'; import { InAppNotification } from 'core-app/core/state/in-app-notifications/in-app-notification.model';
import { INotificationPageQueryParameters } from 'core-app/features/in-app-notifications/in-app-notifications.routes'; import { INotificationPageQueryParameters } from 'core-app/features/in-app-notifications/in-app-notifications.routes';

@ -27,10 +27,8 @@ import { take } from 'rxjs/internal/operators/take';
import { StateService } from '@uirouter/angular'; import { StateService } from '@uirouter/angular';
import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import { BackRouteOptions } from 'core-app/features/work-packages/components/back-routing/back-routing.service'; import { BackRouteOptions } from 'core-app/features/work-packages/components/back-routing/back-routing.service';
import { import { CustomText } from 'core-app/core/state/hal-resource';
InAppNotification, import { InAppNotification } from 'core-app/core/state/in-app-notifications/in-app-notification.model';
InAppNotificationDetail,
} from 'core-app/core/state/in-app-notifications/in-app-notification.model';
import { IanCenterService } from 'core-app/features/in-app-notifications/center/state/ian-center.service'; import { IanCenterService } from 'core-app/features/in-app-notifications/center/state/ian-center.service';
@Component({ @Component({
@ -52,10 +50,10 @@ export class InAppNotificationEntryComponent implements OnInit {
loading$ = this.storeService.query.selectLoading(); loading$ = this.storeService.query.selectLoading();
// Formattable body, if any // Formattable body, if any
body:InAppNotificationDetail[]; body:CustomText[];
// custom rendered details, if any // custom rendered details, if any
details:InAppNotificationDetail[]; details:CustomText[];
// Whether body and details are empty // Whether body and details are empty
unexpandable = false; unexpandable = false;
@ -215,7 +213,7 @@ export class InAppNotificationEntryComponent implements OnInit {
if (project) { if (project) {
this.project = { this.project = {
...project, ...project,
showUrl: this.pathHelper.projectPath(idFromLink(project.href)), showUrl: this.pathHelper.projectPath(String(idFromLink(project.href))),
}; };
} }
} }

@ -8,7 +8,7 @@ export const ProjectAllowedValidator = (currentUserService:CurrentUserService) =
(control:AbstractControl) => (control:AbstractControl) =>
currentUserService.hasCapabilities$( currentUserService.hasCapabilities$(
'memberships/create', 'memberships/create',
idFromLink(control.value.href) String(idFromLink(control.value.href)),
).pipe( ).pipe(
take(1), take(1),
map((isAllowed) => (isAllowed ? null : { lackingPermission: true })), map((isAllowed) => (isAllowed ? null : { lackingPermission: true })),

@ -48,7 +48,7 @@ export class NotificationSettingsTableComponent {
) {} ) {}
projectLink(href:string) { projectLink(href:string) {
return this.pathHelper.projectPath(idFromLink(href)); return this.pathHelper.projectPath(String(idFromLink(href)));
} }
addProjectSettings(project:HalSourceLink):void { addProjectSettings(project:HalSourceLink):void {

@ -56,7 +56,7 @@ export class ActivityEntryComponent implements OnInit {
} }
ngOnInit() { ngOnInit() {
this.projectId = idFromLink(this.workPackage.project.href); this.projectId = String(idFromLink(this.workPackage.project.href));
this.activityType = this.activity._type; this.activityType = this.activity._type;
} }

@ -212,7 +212,7 @@ export class WorkPackageCreateService extends UntilDestroyedMixin {
const hasChanges = !change.isEmpty(); const hasChanges = !change.isEmpty();
const typeEmpty = !changeType && !type; const typeEmpty = !changeType && !type;
const typeMatches = type && changeType && idFromLink(changeType.href) === type.toString(); const typeMatches = type && changeType && idFromLink(changeType.href) === type;
if (hasChanges && (typeEmpty || typeMatches)) { if (hasChanges && (typeEmpty || typeMatches)) {
return Promise.resolve(change); return Promise.resolve(change);

@ -190,7 +190,7 @@ export class WorkPackageSingleViewComponent extends UntilDestroyedMixin implemen
this.projectContext = { matches: false, href: null }; this.projectContext = { matches: false, href: null };
} else { } else {
this.projectContext = { this.projectContext = {
href: this.PathHelper.projectWorkPackagePath(idFromLink(resource.project.href), this.workPackage.id!), href: this.PathHelper.projectWorkPackagePath(String(idFromLink(resource.project.href)), this.workPackage.id!),
matches: resource.project.href === this.currentProject.apiv3Path, matches: resource.project.href === this.currentProject.apiv3Path,
}; };
} }
@ -261,7 +261,7 @@ export class WorkPackageSingleViewComponent extends UntilDestroyedMixin implemen
} }
public get projectContextText():string { public get projectContextText():string {
const id = idFromLink(this.workPackage.project.href); const id = String(idFromLink(this.workPackage.project.href));
const projectPath = this.PathHelper.projectPath(id); const projectPath = this.PathHelper.projectPath(id);
const project = `<a href="${projectPath}">${this.workPackage.project.name}<a>`; const project = `<a href="${projectPath}">${this.workPackage.project.name}<a>`;
return this.I18n.t('js.project.work_package_belongs_to', { projectname: project }); return this.I18n.t('js.project.work_package_belongs_to', { projectname: project });

@ -12,7 +12,7 @@ export class SelectInputComponent extends FieldType implements OnInit {
public ngOnInit():void { public ngOnInit():void {
if (this.model?.project) { if (this.model?.project) {
this.projectId = idFromLink(this.model.project?.href); this.projectId = String(idFromLink(this.model.project?.href));
} }
} }
} }

@ -93,7 +93,7 @@ export class TimeEntryWorkPackageEditFieldComponent extends WorkPackageEditField
.then((collection) => { .then((collection) => {
this.recentWorkPackageIds = collection this.recentWorkPackageIds = collection
.elements .elements
.map((timeEntry) => idFromLink(timeEntry.workPackage.href)) .map((timeEntry) => String(idFromLink(timeEntry.workPackage.href)))
.filter((v, i, a) => a.indexOf(v) === i); .filter((v, i, a) => a.indexOf(v) === i);
return this.fetchAllowedValueQuery(query); return this.fetchAllowedValueQuery(query);

@ -63,7 +63,7 @@ export class WidgetNewsComponent extends AbstractWidgetComponent implements OnIn
} }
public newsProjectPath(news:NewsResource) { public newsProjectPath(news:NewsResource) {
return this.pathHelper.projectPath(idFromLink(news.project?.href)); return this.pathHelper.projectPath(String(idFromLink(news.project?.href)));
} }
public newsProjectName(news:NewsResource) { public newsProjectName(news:NewsResource) {

Loading…
Cancel
Save