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';
export interface HalResourceLink {
href:string;
title:string;
}
export type InAppNotificationFormat = 'markdown'|'custom';
import {
HalResourceLink,
HalResourceLinks,
CustomText,
} from 'core-app/core/state/hal-resource';
export const NOTIFICATIONS_MAX_SIZE = 100;
export interface InAppNotificationDetail {
format:InAppNotificationFormat;
raw:string|null;
html:string;
export interface InAppNotificationHalResourceLinks extends HalResourceLinks {
actor:HalResourceLink;
project:HalResourceLink;
resource:HalResourceLink;
activity:HalResourceLink;
}
export interface InAppNotification {
@ -23,16 +23,11 @@ export interface InAppNotification {
readIAN: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".
keep?:boolean;
// Show message of a notification?
expanded:boolean;
_links:{
actor?:HalResourceLink,
project?:HalResourceLink,
resource?:HalResourceLink,
activity?:HalResourceLink,
};
_links:InAppNotificationHalResourceLinks;
}

@ -1,20 +1,32 @@
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 interface ProjectHalResourceLinks extends HalResourceLinks {
categories:HalResourceLink;
delete:HalResourceLink;
parent:HalResourceLink;
self:HalResourceLink;
status:HalResourceLink;
schema:HalResourceLink;
}
export interface Project {
id:ID;
identifier:string;
name:string;
public:boolean;
active:boolean;
statusExplanation:CustomText;
description:CustomText;
createdAt:string;
updatedAt:string;
_links:{
actor?:HalResourceLink,
project?:HalResourceLink,
resource?:HalResourceLink,
activity?:HalResourceLink,
};
_links:ProjectHalResourceLinks;
}

@ -163,7 +163,7 @@ export class BcfWpAttributeGroupComponent extends UntilDestroyedMixin implements
}
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.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
// ('bim.partitioned.split')
window.location.href = this.pathHelper.bimDetailsPath(
idFromLink(workPackage.project.href),
String(idFromLink(workPackage.project.href)),
workPackage.id!,
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()!;
return decodeURIComponent(idPart);
return parseInt(decodeURIComponent(idPart), 10);
}

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

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

@ -5,7 +5,7 @@ import {
OnInit,
} from '@angular/core';
import { combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { map, tap } from 'rxjs/operators';
import { StateService } from '@uirouter/core';
import idFromLink from 'core-app/features/hal/helpers/id-from-link';
import { I18nService } from 'core-app/core/i18n/i18n.service';
@ -58,11 +58,20 @@ export class IanMenuComponent implements OnInit {
];
notificationsByProject$ = this.ianMenuService.query.notificationsByProject$.pipe(
map((items) => items.map(item => ({
...item,
title: item.value,
href: this.getHrefForFilters({ filter: 'project', name: idFromLink(item._links.valueLink[0].href) }),
}))),
map((items) => items
.map(item => ({
...item,
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(

@ -1,15 +1,47 @@
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 { ProjectsResourceService } from 'core-app/core/state/projects/projects.service';
import { InAppNotification } from 'core-app/core/state/in-app-notifications/in-app-notification.model';
import { selectCollectionAsEntities$ } from 'core-app/core/state/collection-store';
import { Project } from 'core-app/core/state/projects/project.model';
import { collectionKey } from 'core-app/core/state/collection-store';
import {
IanMenuState,
IanMenuStore,
} from './ian-menu.store';
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');
constructor(

@ -2,18 +2,15 @@ import {
Injectable,
Injector,
} from '@angular/core';
import { switchMap } from 'rxjs/operators';
import {
markNotificationsAsRead,
notificationsMarkedRead,
} from 'core-app/core/state/in-app-notifications/in-app-notifications.actions';
import { notificationsMarkedRead } from 'core-app/core/state/in-app-notifications/in-app-notifications.actions';
import {
EffectCallback,
EffectHandler,
} from 'core-app/core/state/effects/effect-handler.decorator';
import { ActionsService } from 'core-app/core/state/actions/actions.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 { ProjectsResourceService } from 'core-app/core/state/projects/projects.service';
import { IanMenuQuery } from './ian-menu.query';
@ -52,13 +49,15 @@ export class IanMenuService {
public reload() {
this.ianResourceService.fetchNotifications(IAN_MENU_PROJECT_FILTERS)
.subscribe((data) => {
this.store.update({ notificationsByProject: data.groups });
/*
this.projectsResourceService.fetchProjects({
const projectsFilter:Apiv3ListParameters = {
pageSize: 100,
filters: [['id', '=', data.groups!.map(group => idFromLink(group._links.valueLink[0].href))]],
}).subscribe();
*/
filters: [['id', '=', data.groups!.map(group => String(idFromLink(group._links.valueLink[0].href)))]],
};
this.store.update({
notificationsByProject: data.groups,
projectsFilter,
});
this.projectsResourceService.fetchProjects(projectsFilter).subscribe();
});
this.ianResourceService.fetchNotifications(IAN_MENU_REASON_FILTERS)
.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 {
value:string;
count:number;
project?:Project;
projectHasParent?:boolean;
_links:{
valueLink:{
href:string;
@ -17,6 +17,7 @@ export interface IanMenuGroupingData {
export interface IanMenuState {
notificationsByProject:IanMenuGroupingData[],
notificationsByReason:IanMenuGroupingData[],
projectsFilter: Apiv3ListParameters,
}
export const IAN_MENU_PROJECT_FILTERS:Apiv3ListParameters = {
@ -35,6 +36,7 @@ export function createInitialState():IanMenuState {
return {
notificationsByProject: [],
notificationsByReason: [],
projectsFilter: {},
};
}

@ -1,4 +1,8 @@
import { Query } from '@datorama/akita';
import {
map,
switchMap,
} from 'rxjs/operators';
import {
IAN_FACET_FILTERS,
IanCenterState,
@ -6,10 +10,6 @@ import {
} 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 { 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 { 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';

@ -27,10 +27,8 @@ import { take } from 'rxjs/internal/operators/take';
import { StateService } from '@uirouter/angular';
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 {
InAppNotification,
InAppNotificationDetail,
} from 'core-app/core/state/in-app-notifications/in-app-notification.model';
import { CustomText } from 'core-app/core/state/hal-resource';
import { InAppNotification } 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';
@Component({
@ -52,10 +50,10 @@ export class InAppNotificationEntryComponent implements OnInit {
loading$ = this.storeService.query.selectLoading();
// Formattable body, if any
body:InAppNotificationDetail[];
body:CustomText[];
// custom rendered details, if any
details:InAppNotificationDetail[];
details:CustomText[];
// Whether body and details are empty
unexpandable = false;
@ -215,7 +213,7 @@ export class InAppNotificationEntryComponent implements OnInit {
if (project) {
this.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) =>
currentUserService.hasCapabilities$(
'memberships/create',
idFromLink(control.value.href)
String(idFromLink(control.value.href)),
).pipe(
take(1),
map((isAllowed) => (isAllowed ? null : { lackingPermission: true })),

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

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

@ -212,7 +212,7 @@ export class WorkPackageCreateService extends UntilDestroyedMixin {
const hasChanges = !change.isEmpty();
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)) {
return Promise.resolve(change);

@ -190,7 +190,7 @@ export class WorkPackageSingleViewComponent extends UntilDestroyedMixin implemen
this.projectContext = { matches: false, href: null };
} else {
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,
};
}
@ -261,7 +261,7 @@ export class WorkPackageSingleViewComponent extends UntilDestroyedMixin implemen
}
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 project = `<a href="${projectPath}">${this.workPackage.project.name}<a>`;
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 {
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) => {
this.recentWorkPackageIds = collection
.elements
.map((timeEntry) => idFromLink(timeEntry.workPackage.href))
.map((timeEntry) => String(idFromLink(timeEntry.workPackage.href)))
.filter((v, i, a) => a.indexOf(v) === i);
return this.fetchAllowedValueQuery(query);

@ -63,7 +63,7 @@ export class WidgetNewsComponent extends AbstractWidgetComponent implements OnIn
}
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) {

Loading…
Cancel
Save