diff --git a/claims/claim-utils/claimContextSearchForm.component.ts b/claims/claim-utils/claimContextSearchForm.component.ts index 95289d97..d48540e8 100644 --- a/claims/claim-utils/claimContextSearchForm.component.ts +++ b/claims/claim-utils/claimContextSearchForm.component.ts @@ -7,8 +7,8 @@ import {LoginErrorCodes} from '../../login/utils/guardHelper.class'; import {EnvProperties} from '../../utils/properties/env-properties'; import {Subscriber} from "rxjs"; import {OpenaireEntities} from "../../utils/properties/searchFields"; -import {CommunityService} from "../../connect/community/community.service"; import {CommunitiesService} from "../../connect/communities/communities.service"; +import {UserManagementService} from "../../services/user-management.service"; declare var UIkit: any; @@ -49,9 +49,14 @@ export class ClaimContextSearchFormComponent { keyword = ""; subscriptions = []; communityLogos = {}; + communityIds = []; + user = null; ngOnInit() { this.entitiesSelectOptions = this.showOptions.selectOptions; //get community logos + this.subscriptions.push(this.userManagementService.getUserInfo().subscribe(user => { + this.user = user; + this.subscriptions.push(this._communitiesService.getCommunities(this.properties, this.properties.communityAPI+"/communities/").subscribe( communitiesResults => { if(communitiesResults!=null) { @@ -61,6 +66,13 @@ export class ClaimContextSearchFormComponent { this.communityLogos[community.communityId] = community; } } + this.communityIds = communitiesResults.filter(community => { + return community.claim == "all" || + Session.isCommunityCurator(this.user) || + Session.isClaimsCurator(this.user) || + (community.claim == "membersOnly" && Session.isSubscribedTo("community", community.communityId,this.user)) || + (community.claim == "managersOnly" && Session.isManager("community", community.communityId,this.user)) + }).map(community => community.communityId); this.getCommunities(); } }, @@ -68,6 +80,10 @@ export class ClaimContextSearchFormComponent { this.getCommunities(); } )); + + }, error => { + + })); } ngOnDestroy() { @@ -77,7 +93,7 @@ export class ClaimContextSearchFormComponent { } }); } - constructor(private _contextService: ContextsService, private router: Router, private _communitiesService: CommunitiesService) { + constructor(private _contextService: ContextsService, private router: Router, private _communitiesService: CommunitiesService, private userManagementService: UserManagementService,) { } @@ -172,15 +188,16 @@ export class ClaimContextSearchFormComponent { } else { this.loading = true; - this.subscriptions.push(this._contextService.getPublicCommunitiesByState().subscribe( + this.subscriptions.push(this._contextService.getCommunitiesByState().subscribe( data => { - this.communities = data; - console.log(this.communities) + this.communities = data.filter(community => { + return this.communityIds.indexOf(community.id) != -1 + }); if (this.communities.length > 0) { - this.communities.sort((n1, n2) => n1.label > n2.label); + this.communities.sort((n1, n2) => n1.title > n2.title); } this.loading = false; - if (this.communityId != null) { + if (this.communityId != null && this.communityIds.indexOf(this.communityId) != -1) { //preselect community this.selectedCommunityId = this.communityId; for (let i = 0; i < this.communities.length; i++) { diff --git a/connect/communities/communities.service.ts b/connect/communities/communities.service.ts index 299cfe32..05f427a6 100644 --- a/connect/communities/communities.service.ts +++ b/connect/communities/communities.service.ts @@ -56,12 +56,11 @@ export class CommunitiesService { result['description'] = resData.description; result['date'] = resData.creationDate; result['status'] = 'all'; + result['claim'] = resData.claim; + result['membership'] = resData.membership; if (resData.hasOwnProperty('status')) { result['status'] = resData.status; - const status = ['all', 'hidden', 'manager']; - if (status.indexOf(result['status']) === -1) { - result['status'] = 'hidden'; - } + result.validateStatus(); } if (resData.type != null) { result['type'] = resData.type; diff --git a/connect/community/community.service.ts b/connect/community/community.service.ts index d8fe6975..06d222e0 100644 --- a/connect/community/community.service.ts +++ b/connect/community/community.service.ts @@ -98,14 +98,14 @@ export class CommunityService { community.description = resData.description; community.date = resData.creationDate; community.zenodoCommunity = resData.zenodoCommunity; - community.status = 'all'; + community.status = 'PUBLIC'; + community.claim = resData.claim; + community.membership = resData.membership; community.type = resData.type; + community.otherZenodoCommunities = resData.otherZenodoCommunities; if (resData.hasOwnProperty('status')) { community.status = resData.status; - const status = ['all', 'hidden', 'manager']; - if (status.indexOf(community['status']) === -1) { - community.status = 'hidden'; - } + community.validateStatus(); } if (resData.subjects != null) { community.subjects = Array.isArray(resData.subjects)?resData.subjects:[resData.subjects]; diff --git a/connect/community/communityInfo.ts b/connect/community/communityInfo.ts index 231a642d..1185f8d1 100644 --- a/connect/community/communityInfo.ts +++ b/connect/community/communityInfo.ts @@ -14,8 +14,11 @@ export class CommunityInfo { managers: string[]; date:Date; subjects: string[]; - status:string; + status:"all" | "manager" | "hidden" | "PUBLIC" | "RESTRICTED" | "PRIVATE"; + claim: "all" | "managersOnly" | "membersOnly"; + membership: "open" | "byInvitation"; zenodoCommunity:string; + otherZenodoCommunities: string[]; isUpload: boolean; isSubscribed: boolean; isManager: boolean; @@ -33,5 +36,19 @@ export class CommunityInfo { } return response; } + public isPublic(){ + return this.status == "all" || this.status == "PUBLIC"; + } + public isRestricted(){ + return this.status == "manager" || this.status == "RESTRICTED"; + } + public isPrivate(){ + return this.status == "hidden" || this.status == "PRIVATE"; + } + public validateStatus(){ + if(!(this.isPrivate() || this.isRestricted() || this.isPublic())){ + this.status = "PRIVATE"; + } + } } // export const prodReadyCommunities = ["dh-ch", "ee", "fam", "mes", "ni", "covid-19", "dariah", "epos", "egi"]; diff --git a/connect/connectHelper.ts b/connect/connectHelper.ts index 1fbbc59a..0e65047a 100644 --- a/connect/connectHelper.ts +++ b/connect/connectHelper.ts @@ -54,6 +54,6 @@ export class ConnectHelper { public static isPrivate(community, user) { - return community && (community.status == "hidden" || (community.status == "manager" && !(Session.isPortalAdministrator(user) || Session.isCommunityCurator(user) || Session.isManager("community", community.communityId, user)))) + return community && (community.isPrivate() || (community.isRestricted() && !(Session.isPortalAdministrator(user) || Session.isCommunityCurator(user) || Session.isManager("community", community.communityId, user)))) } } diff --git a/connect/projects/searchProjects.service.ts b/connect/projects/searchProjects.service.ts index c580d190..e86fd1a8 100644 --- a/connect/projects/searchProjects.service.ts +++ b/connect/projects/searchProjects.service.ts @@ -7,15 +7,25 @@ import {map} from "rxjs/operators"; export class SearchCommunityProjectsService { constructor(private http: HttpClient ) {} - searchProjects (properties:EnvProperties, pid: string):any { - let url = properties.communityAPI+pid+"/projects"; - + searchProjects (properties:EnvProperties, pid: string, page=1, size=500):any { + return this.searchProjectsWithPaging(properties,pid,page, size, null, null); + } + searchProjectsWithPaging (properties:EnvProperties, pid: string, page=1, size=500, searchFilter, funder, orderBy = "name"):any { + let params = funder ? ["funder="+ funder] :[]; + if (searchFilter) { + params.push("searchFilter="+ searchFilter) + } + params.push("orderBy="+ orderBy); + let url = properties.communityAPI+pid+"/projects/"+ (page-1) + "/" + size + (params.length > 0?"?" + params.join("&"):""); return this.http.get((properties.useCache)? (properties.cacheUrl+encodeURIComponent(url)): url); - //.map(res => res.json()) } countTotalProjects(properties:EnvProperties,pid:string) { - let url = properties.communityAPI+pid+"/projects"; + let url = properties.communityAPI+pid+"/projects/0/1"; return this.http.get((properties.useCache) ? (properties.cacheUrl + encodeURIComponent(url)) : url) - .pipe(map(res => res['length'])); + .pipe(map(res => res['totalElements'])); + } + getProjectFunders(properties:EnvProperties,pid:string) { + let url = properties.communityAPI+pid+"/funders"; + return this.http.get((properties.useCache) ? (properties.cacheUrl + encodeURIComponent(url)) : url); } } diff --git a/connect/zenodoCommunities/searchZenodoCommunities.service.ts b/connect/zenodoCommunities/searchZenodoCommunities.service.ts deleted file mode 100644 index 02fb8fda..00000000 --- a/connect/zenodoCommunities/searchZenodoCommunities.service.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {Injectable} from '@angular/core'; -import {HttpClient} from "@angular/common/http"; -import{EnvProperties} from '../../utils/properties/env-properties'; - -@Injectable() -export class SearchZenodoCommunitiesService { - constructor(private http: HttpClient ) {} - - searchZCommunities (properties:EnvProperties, pid: string):any { - let url = properties.communityAPI+pid+"/zenodocommunities"; - - return this.http.get((properties.useCache)? (properties.cacheUrl+encodeURIComponent(url)): url); - //.map(res => res.json()) - } -} diff --git a/connect/zenodoCommunities/searchZenodoCommunitiesService.module.ts b/connect/zenodoCommunities/searchZenodoCommunitiesService.module.ts deleted file mode 100644 index 704bfe82..00000000 --- a/connect/zenodoCommunities/searchZenodoCommunitiesService.module.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { NgModule} from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; - -import {SearchZenodoCommunitiesService} from './searchZenodoCommunities.service'; - - - -@NgModule({ - imports: [ - CommonModule, FormsModule - ], - declarations: [ - ], - providers:[ - SearchZenodoCommunitiesService -], - exports: [ - ] -}) -export class SearchZenodoCommunitiesServiceModule { } diff --git a/connect/zenodoCommunities/zenodo-communities.service.ts b/connect/zenodoCommunities/zenodo-communities.service.ts index bf790f27..5268a079 100644 --- a/connect/zenodoCommunities/zenodo-communities.service.ts +++ b/connect/zenodoCommunities/zenodo-communities.service.ts @@ -16,12 +16,11 @@ export class ZenodoCommunitiesService { //.map(res => res.json()) .pipe(map(res => [this.parseZenodoCommunities(res['hits'].hits),res['hits'].total])); } - getZenodoCommunityById(properties:EnvProperties, url: string, openaireId:string) { + getZenodoCommunityById(properties:EnvProperties, url: string) { return this.http.get((properties.useLongCache)? (properties.cacheUrl+encodeURIComponent(url)) : url) //.map(res => res.json()) .pipe(map(res => { var community = this.parseZenodoCommunity(res); - community["openaireId"]=openaireId; return community; })); } diff --git a/connect/zenodoCommunities/zenodoCommunityInfo.ts b/connect/zenodoCommunities/zenodoCommunityInfo.ts index 491bec39..5fb7df3f 100644 --- a/connect/zenodoCommunities/zenodoCommunityInfo.ts +++ b/connect/zenodoCommunities/zenodoCommunityInfo.ts @@ -6,5 +6,4 @@ export class ZenodoCommunityInfo { logoUrl: string; date: Date; page: string; - openaireId:string; } diff --git a/fos/fos.component.ts b/fos/fos.component.ts index c02b6bed..043e7bd5 100644 --- a/fos/fos.component.ts +++ b/fos/fos.component.ts @@ -216,6 +216,7 @@ export class FosComponent implements OnInit, OnDestroy { } public buildFosQueryParam(fos) { - return {'foslabel': this.urlEncodeAndQuote(fos.id+"||"+fos.label)}; + // return {'foslabel': this.urlEncodeAndQuote(fos.id+"||"+fos.label)}; + return (properties.environment !== 'production' ? ({'foslabel': this.urlEncodeAndQuote(fos.id+"||"+fos.label)}) : ({'fos': this.urlEncodeAndQuote(fos.id)})); } } diff --git a/landingPages/landing-utils/entity-metadata.component.ts b/landingPages/landing-utils/entity-metadata.component.ts index aba2fc82..caad8627 100644 --- a/landingPages/landing-utils/entity-metadata.component.ts +++ b/landingPages/landing-utils/entity-metadata.component.ts @@ -1,4 +1,4 @@ -import {Component, Input} from "@angular/core"; +import {Component, Input, ViewChild} from "@angular/core"; import {EnvProperties} from "../../utils/properties/env-properties"; import {properties} from "../../../../environments/environment"; import {OpenaireEntities} from "../../utils/properties/searchFields"; @@ -151,12 +151,32 @@ import {RouterHelper} from "../../utils/routerHelper.class"; Thematic - - {{projectNames.slice(0,3).join(', ')}} + + {{showInline ? projectNames.join(', ') : projectNames.slice(0, projectsLimit).join(', ')}} + + + +{{projects.length - projectsLimit | number}} projects + + + View less + + - - {{organizationNames.slice(0, 3).join(', ')}} + + {{showInline ? organizationNames.join(', ') : organizationNames.slice(0, organizationsLimit).join(', ')}} + + + +{{organizations.length - organizationsLimit | number}} partners + + + View less + + @@ -169,10 +189,37 @@ import {RouterHelper} from "../../utils/routerHelper.class"; {{relationName}} + + + + +
+ +
+ {{item.name}}{{i == organizations.length - 1 ? '' : ','}} +
+
+
+
+ + +
+ +
+ {{item['funderShortname'] ? item['funderShortname'] : item['funderName']}} + [no funder available] + | {{ item['acronym'] ? item['acronym'] : item['title']}} + {{i == projects.length - 1 ? '' : ','}} +
+
+
+
`, styleUrls: ['entity-metadata.component.less'] }) export class EntityMetadataComponent { + @Input() isMobile: boolean = false; @Input() entityType: string; @Input() types: string[]; @Input() year: string; // search result @@ -204,6 +251,14 @@ export class EntityMetadataComponent { @Input() subjects: string[]; @Input() prevPath: string = ""; + @ViewChild('partnersModal') partnersModal; + @ViewChild('projectsModal') projectsModal; + + organizationsLimit: number = 5; + projectsLimit: number = 3; + showInline: boolean = false; + lessBtn: boolean = false; + properties: EnvProperties = properties; public openaireEntities = OpenaireEntities; public routerHelper: RouterHelper = new RouterHelper(); @@ -260,4 +315,48 @@ export class EntityMetadataComponent { } return obj; } -} + + public viewAllPartnersClick() { + if(this.organizations.length <= this.organizationsLimit * 2) { + this.showInline = true; + this.lessBtn = true; + } else { + this.openPartnersModal(); + } + } + + public openPartnersModal() { + if (this.isMobile) { + this.partnersModal.okButton = false; + this.partnersModal.title = "Partners"; + this.partnersModal.open(); + } else { + this.partnersModal.cancelButton = false; + this.partnersModal.okButton = false; + this.partnersModal.alertTitle = "Partners"; + this.partnersModal.open(); + } + } + + public viewAllProjectsClick() { + if(this.projects.length <= this.projectsLimit * 2) { + this.showInline = true; + this.lessBtn = true; + } else { + this.openProjectsModal(); + } + } + + public openProjectsModal() { + if (this.isMobile) { + this.projectsModal.okButton = false; + this.projectsModal.title = "Projects"; + this.projectsModal.open(); + } else { + this.projectsModal.cancelButton = false; + this.projectsModal.okButton = false; + this.projectsModal.alertTitle = "Projects"; + this.projectsModal.open(); + } + } +} \ No newline at end of file diff --git a/landingPages/landing-utils/fos.component.ts b/landingPages/landing-utils/fos.component.ts index 4dfc59e6..ba95bdc8 100644 --- a/landingPages/landing-utils/fos.component.ts +++ b/landingPages/landing-utils/fos.component.ts @@ -121,10 +121,12 @@ export class FosComponent { } public buildFosQueryParam(fos) { - return {'foslabel': this.urlEncodeAndQuote(fos.id+"||"+fos.label)}; + // return {'foslabel': this.urlEncodeAndQuote(fos.id+"||"+fos.label)}; + return (properties.environment !== 'production' ? ({'foslabel': this.urlEncodeAndQuote(fos.id+"||"+fos.label)}) : ({'fos': this.urlEncodeAndQuote(fos.id)})); } public buildFosHrefParam(fos): string { - return ('foslabel='+this.urlEncodeAndQuote(fos.id+"||"+fos.label)); + // return ('foslabel='+this.urlEncodeAndQuote(fos.id+"||"+fos.label)); + return (properties.environment !== 'production' ? ('foslabel='+this.urlEncodeAndQuote(fos.id+"||"+fos.label)) : ('fos='+this.urlEncodeAndQuote(fos.id))); } } diff --git a/landingPages/landing-utils/parsingFunctions.class.ts b/landingPages/landing-utils/parsingFunctions.class.ts index fd2c3b12..faeaaada 100644 --- a/landingPages/landing-utils/parsingFunctions.class.ts +++ b/landingPages/landing-utils/parsingFunctions.class.ts @@ -514,7 +514,8 @@ export class ParsingFunctions { let identifiers = new Map(); if (pid.hasOwnProperty("classid") && pid['classid'] != "") { - if (pid.classid == "doi" || pid.classid == "pmc" || pid.classid == "handle" || pid.classid == "pmid" || pid.classid == "re3data") { + if (pid.classid == "doi" || pid.classid == "pmc" || pid.classid == "handle" || pid.classid == "pmid" || pid.classid == "re3data" + || pid.classid == "swhid") { if (!identifiers.has(pid.classid)) { identifiers.set(pid.classid, new Array()); } @@ -522,7 +523,8 @@ export class ParsingFunctions { } } else { for (let i = 0; i < pid.length; i++) { - if (pid[i].classid == "doi" || pid[i].classid == "pmc" || pid[i].classid == "handle" || pid[i].classid == "pmid" || pid[i].classid == "re3data") { + if (pid[i].classid == "doi" || pid[i].classid == "pmc" || pid[i].classid == "handle" || pid[i].classid == "pmid" || pid[i].classid == "re3data" + || pid[i].classid == "swhid") { if (!identifiers.has(pid[i].classid)) { identifiers.set(pid[i].classid, new Array()); } diff --git a/landingPages/landing-utils/showIdentifiers.component.ts b/landingPages/landing-utils/showIdentifiers.component.ts index e4644c34..8a0b4680 100644 --- a/landingPages/landing-utils/showIdentifiers.component.ts +++ b/landingPages/landing-utils/showIdentifiers.component.ts @@ -26,7 +26,7 @@ import {properties} from "../../../../environments/environment"; {{key}}: - {{item}} @@ -124,6 +124,8 @@ export class ShowIdentifiersComponent implements AfterViewInit { return properties.handleURL; } else if(key == "re3data") { return properties.r3DataURL; + } else if(key == "swhid") { + return properties.swhURL; } } diff --git a/landingPages/result/resultLanding.component.ts b/landingPages/result/resultLanding.component.ts index 932901d7..9ba1309b 100644 --- a/landingPages/result/resultLanding.component.ts +++ b/landingPages/result/resultLanding.component.ts @@ -825,6 +825,8 @@ export class ResultLandingComponent { return this.properties.pmidURL + id.value; } else if (id.type === "handle") { return this.properties.handleURL + id.value; + } else if (id.type === "swhid") { + return this.properties.swhURL + id.value; } else { return null; } @@ -839,6 +841,8 @@ export class ResultLandingComponent { return 'PubMed'; } else if (id.type === "handle") { return 'Handle.NET'; + } else if(id.type == "swhid") { + return 'Software Heritage'; } else { return null; } diff --git a/login/user.component.html b/login/user.component.html index a69412e4..45cb0d51 100644 --- a/login/user.component.html +++ b/login/user.component.html @@ -17,13 +17,13 @@
Roles {{getTheRolesFormatted(user.role)}}
- diff --git a/login/user.component.ts b/login/user.component.ts index e279b7b6..f95adf67 100644 --- a/login/user.component.ts +++ b/login/user.component.ts @@ -100,12 +100,9 @@ export class UserComponent { return formattedRoles.join(", "); } - get isCurator() { - return Session.isPortalAdministrator(this.user) || Session.isMonitorCurator(this.user); + get isPortalAdministrator() { + return Session.isPortalAdministrator(this.user); } - get isUserManager() { - return Session.isUserManager(this.user); - } } diff --git a/searchPages/searchUtils/portal-search-result.component.html b/searchPages/searchUtils/portal-search-result.component.html index 9c202293..7be77b69 100644 --- a/searchPages/searchUtils/portal-search-result.component.html +++ b/searchPages/searchUtils/portal-search-result.component.html @@ -8,11 +8,11 @@ Member
-
+
restricted
- + private diff --git a/utils/authors/showAuthors.component.ts b/utils/authors/showAuthors.component.ts index 12a8784a..837bbbbf 100644 --- a/utils/authors/showAuthors.component.ts +++ b/utils/authors/showAuthors.component.ts @@ -109,7 +109,7 @@ import {properties} from "../../../../environments/environment"; +{{authors.length - authorsLimit | number}} more - + +{{authors.length - authorsLimit | number}} Authors diff --git a/utils/entities/resultLandingInfo.ts b/utils/entities/resultLandingInfo.ts index 0ae3866f..30e1e76b 100644 --- a/utils/entities/resultLandingInfo.ts +++ b/utils/entities/resultLandingInfo.ts @@ -8,7 +8,7 @@ import { } from "../result-preview/result-preview"; export interface Id { - type: "pmid" | "doi" | "pmc" | "handle" | "openaire"; + type: "pmid" | "doi" | "pmc" | "handle" | "openaire" | "swhid"; value: string; trust: number } diff --git a/utils/properties/env-properties.ts b/utils/properties/env-properties.ts index af367504..4a2ccd68 100644 --- a/utils/properties/env-properties.ts +++ b/utils/properties/env-properties.ts @@ -44,6 +44,7 @@ export interface EnvProperties { cordisURL?: string; openDoarURL?: string; r3DataURL?: string; + swhURL?: string; fairSharingURL?: string, eoscMarketplaceURL?: string, sherpaURL?: string; diff --git a/utils/properties/searchFields.ts b/utils/properties/searchFields.ts index 7ae0fcda..65ca6a4b 100644 --- a/utils/properties/searchFields.ts +++ b/utils/properties/searchFields.ts @@ -14,7 +14,7 @@ export class SearchFields { // Remove Collected From Filter "collectedfrom","collectedfrom" public RESULT_REFINE_FIELDS = [ - "resultbestaccessright", "instancetypename", "foslabel", "relfunder", + "resultbestaccessright", "instancetypename", properties.environment!='production'?"foslabel":'fos', "relfunder", "relfundinglevel0_id", "relfundinglevel1_id", "relfundinglevel2_id", "relproject", "sdg", "country", "resultlanguagename", "resulthostingdatasource", "community"]; diff --git a/utils/string-utils.class.ts b/utils/string-utils.class.ts index d05bc57e..dff363ff 100644 --- a/utils/string-utils.class.ts +++ b/utils/string-utils.class.ts @@ -149,7 +149,7 @@ export class DOI { } export class Identifier { - class: "doi" | "pmc" | "pmid" | "handle" | "ORCID" | "re3data" = null; + class: "doi" | "pmc" | "pmid" | "handle" | "ORCID" | "re3data" | "swhid" = null; id: string; public static getDOIsFromString(str: string): string[] { @@ -202,13 +202,15 @@ export class Identifier { return {"class": "handle", "id": pid}; } else if (Identifier.isValidRe3Data(pid)) { return {"class": "re3data", "id": pid}; + } else if (Identifier.isValidSwhId(pid)) { + return {"class": "swhid", "id": pid}; } //set it as a doi, to catch the case that doi has not valid format return (strict?null:{"class": "doi", "id": pid}); } public static getPIDFromIdentifiers(identifiers: Map): Identifier { - let classes:string [] = ["doi", "handle", "pmc", "pmid", "re3data"]; + let classes:string [] = ["doi", "handle", "pmc", "pmid", "re3data", "swhid"]; if(identifiers) { for (let cl of classes) { if (identifiers.get(cl)) { @@ -257,6 +259,12 @@ export class Identifier { let exp = /(r3d[1-9]\d{0,8})/g; return str.match(exp) != null; } + + public static isValidSwhId(str: string): boolean { + // let exp = /swh:1:(snp|rel|rev|dir|cnt):[0-9a-f]{40}/g; + let exp = /swh:1:snp:[0-9a-f]{40}/g; + return str.match(exp) != null; + } } export class StringUtils {