Skip to content

Commit 0174c13

Browse files
authored
Merge pull request #3489 from SeedCompany/project-member/inactive-filters
2 parents 2093388 + e411590 commit 0174c13

File tree

9 files changed

+101
-36
lines changed

9 files changed

+101
-36
lines changed

src/components/progress-report/workflow/progress-report-workflow.repository.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
createRelationships,
1717
currentUser,
1818
merge,
19+
path,
1920
sorting,
2021
} from '~/core/database/query';
2122
import { ProgressReport, type ProgressReportStatus as Status } from '../dto';
@@ -133,16 +134,23 @@ export class ProgressReportWorkflowRepository extends DtoRepository(
133134
const query = this.db
134135
.query()
135136
.match([
136-
node('report', 'ProgressReport', { id: reportId }),
137+
node('', 'ProgressReport', { id: reportId }),
137138
relation('in', '', ACTIVE),
138-
node('engagement', 'Engagement'),
139+
node('', 'Engagement'),
139140
relation('in', '', 'engagement', ACTIVE),
140-
node('project', 'Project'),
141+
node('', 'Project'),
141142
relation('out', '', 'member', ACTIVE),
142143
node('member', 'ProjectMember'),
143144
relation('out', '', 'user', ACTIVE),
144145
node('user', 'User'),
145146
])
147+
.where(
148+
path([
149+
node('member'),
150+
relation('out', '', 'inactiveAt', ACTIVE),
151+
node('', 'Property', { value: null }),
152+
]),
153+
)
146154
.match([
147155
node('user'),
148156
relation('out', '', 'email', ACTIVE),

src/components/project/dto/list-projects.dto.ts

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { InputType, ObjectType } from '@nestjs/graphql';
22
import { Type } from 'class-transformer';
33
import { ValidateNested } from 'class-validator';
4+
import { set } from 'lodash';
45
import {
56
DateFilter,
67
DateTimeFilter,
@@ -14,6 +15,7 @@ import {
1415
type Sensitivity,
1516
SortablePaginationInput,
1617
} from '~/common';
18+
import { Transform } from '~/common/transform.decorator';
1719
import { LocationFilters } from '../../location/dto';
1820
import { PartnershipFilters } from '../../partnership/dto';
1921
import { ProjectMemberFilters } from '../project-member/dto';
@@ -88,21 +90,17 @@ export abstract class ProjectFilters {
8890
@ValidateNested()
8991
readonly mouEnd?: DateFilter;
9092

91-
@OptionalField({
92-
description: 'only mine',
93-
deprecationReason: 'Use `isMember` instead.',
94-
})
95-
readonly mine?: boolean;
96-
97-
@OptionalField({
98-
description: 'Only projects that the requesting user is a member of',
99-
})
100-
readonly isMember?: boolean;
101-
10293
@FilterField(() => ProjectMemberFilters, {
10394
description:
10495
"Only projects with the requesting user's membership that matches these filters",
10596
})
97+
@Transform(({ value, obj }) => {
98+
// Only ran when GQL specifies membership
99+
if (value.active == null && (obj.mine || obj.isMember)) {
100+
value.active = true;
101+
}
102+
return value;
103+
})
106104
readonly membership?: ProjectMemberFilters & {};
107105

108106
@FilterField(() => ProjectMemberFilters, {
@@ -138,6 +136,28 @@ export abstract class ProjectFilters {
138136
readonly primaryLocation?: LocationFilters & {};
139137
}
140138

139+
Object.defineProperty(ProjectFilters.prototype, 'mine', {
140+
set(value: boolean) {
141+
// Ensure this is set when membership has not been declared
142+
value && !this.membership && set(this, 'membership.active', true);
143+
},
144+
});
145+
OptionalField(() => Boolean, {
146+
description: 'only mine',
147+
deprecationReason: 'Use `isMember` instead.',
148+
})(ProjectFilters.prototype, 'mine');
149+
150+
Object.defineProperty(ProjectFilters.prototype, 'isMember', {
151+
set(value: boolean) {
152+
// Ensure this is set when membership has not been declared
153+
value && !this.membership && set(this, 'membership.active', true);
154+
},
155+
});
156+
OptionalField(() => Boolean, {
157+
description:
158+
'Only projects that the requesting user is an active member of. false does nothing.',
159+
})(ProjectFilters.prototype, 'isMember');
160+
141161
@InputType()
142162
export class ProjectListInput extends SortablePaginationInput<keyof IProject>({
143163
defaultSort: 'name',

src/components/project/dto/project.dto.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,10 +177,11 @@ class Project extends Interfaces {
177177

178178
readonly rootDirectory: Secured<LinkTo<'Directory'> | null>;
179179

180-
@Field({
181-
description: 'Is the requesting user a member of this project?',
182-
})
183-
readonly isMember: boolean;
180+
/** The current user's membership, if any. */
181+
readonly membership:
182+
| (LinkTo<'ProjectMember'> &
183+
Pick<UnsecuredDto<ProjectMember>, 'roles' | 'inactiveAt'>)
184+
| null;
184185

185186
@Field({
186187
description: stripIndent`

src/components/project/project-filters.query.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,6 @@ export const projectFilters = filter.define(() => ProjectFilters, {
3030
modifiedAt: filter.dateTimeProp(),
3131
mouStart: filter.dateTimeProp(),
3232
mouEnd: filter.dateTimeProp(),
33-
mine: filter.pathExistsWhenTrue([
34-
currentUser,
35-
relation('in', '', 'user'),
36-
node('', 'ProjectMember'),
37-
relation('in', '', 'member', ACTIVE),
38-
node('node'),
39-
]),
4033
languageId: filter.pathExists((id) => [
4134
node('node'),
4235
relation('out', '', 'engagement', ACTIVE),
@@ -51,13 +44,6 @@ export const projectFilters = filter.define(() => ProjectFilters, {
5144
relation('out', '', 'partner', ACTIVE),
5245
node('', 'Partner', { id }),
5346
]),
54-
isMember: filter.pathExistsWhenTrue([
55-
currentUser,
56-
relation('in', '', 'user'),
57-
node('', 'ProjectMember'),
58-
relation('in', '', 'member', ACTIVE),
59-
node('node'),
60-
]),
6147
membership: filter.sub(() => projectMemberFilters)((sub) =>
6248
sub.match([
6349
currentUser,

src/components/project/project-member/project-member.repository.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,13 @@ export class ProjectMemberRepository extends DtoRepository(ProjectMember) {
181181
relation('out', '', 'user', ACTIVE),
182182
node('user', 'User'),
183183
])
184-
.apply(projectMemberFilters({ project: { id: project }, roles }))
184+
.apply(
185+
projectMemberFilters({
186+
project: { id: project },
187+
roles,
188+
active: true,
189+
}),
190+
)
185191
.with('user')
186192
.optionalMatch([
187193
node('user'),

src/components/project/project.gel.repository.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ const hydrate = e.shape(e.Project, (project) => ({
3838
__typename: project.__type__.name.slice(9, null),
3939

4040
rootDirectory: true,
41+
membership: {
42+
id: true,
43+
roles: true,
44+
inactiveAt: true,
45+
},
4146
primaryPartnership: e
4247
.select(project.partnerships, (p) => ({
4348
filter: e.op(p.primary, '=', true),
@@ -172,7 +177,8 @@ export class ProjectGelRepository
172177
e.op(project.modifiedAt, '<=', input.modifiedAt.beforeInclusive),
173178
]
174179
: []),
175-
input.isMember != null && e.op(project.isMember, '=', input.isMember),
180+
input.membership != null && e.op('exists', project.membership),
181+
input.membership?.active && e.op(project.membership.active, '?=', true),
176182
input.pinned != null && e.op(project.pinned, '=', input.pinned),
177183
input.languageId &&
178184
e.op(

src/components/project/project.repository.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@ import { CommonRepository, OnIndex, UniquenessError } from '~/core/database';
1515
import { type ChangesOf, getChanges } from '~/core/database/changes';
1616
import {
1717
ACTIVE,
18+
collect,
1819
createNode,
1920
createRelationships,
21+
currentUser,
2022
defineSorters,
2123
FullTextIndex,
2224
matchChangesetAndChangedProps,
2325
matchProjectSens,
26+
matchProps,
2427
matchPropsAndProjectSensAndScopedRoles,
2528
merge,
2629
paginate,
@@ -89,6 +92,19 @@ export class ProjectRepository extends CommonRepository {
8992
relation('out', '', 'rootDirectory', ACTIVE),
9093
node('rootDirectory', 'Directory'),
9194
])
95+
.subQuery('node', (sub) =>
96+
sub
97+
.match([
98+
node('node'),
99+
relation('out', '', 'member', ACTIVE),
100+
node('membership'),
101+
relation('out', '', 'user'),
102+
currentUser,
103+
])
104+
.apply(matchProps({ nodeName: 'membership', outputVar: 'dto' }))
105+
.with(collect('dto').as('dtos'))
106+
.return('dtos[0] as membership'),
107+
)
92108
.optionalMatch([
93109
node('node'),
94110
relation('out', '', 'partnership', ACTIVE),
@@ -134,7 +150,7 @@ export class ProjectRepository extends CommonRepository {
134150
merge('props', 'changedProps', {
135151
type: 'node.type',
136152
pinned,
137-
isMember: '"member:true" in props.scope',
153+
membership: 'membership',
138154
rootDirectory: 'rootDirectory { .id }',
139155
primaryPartnership: 'primaryPartnership { .id }',
140156
primaryLocation: 'primaryLocation { .id }',

src/components/project/project.resolver.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
ListArg,
1919
mapSecuredValue,
2020
NotFoundException,
21+
OptionalField,
2122
SecuredDateRange,
2223
} from '~/common';
2324
import { Loader, type LoaderOf } from '~/core';
@@ -83,6 +84,14 @@ class ModifyOtherLocationArgs {
8384
locationId: ID;
8485
}
8586

87+
@ArgsType()
88+
class IsMemberArgs {
89+
@OptionalField({
90+
description: 'Consider inactive memberships as well',
91+
})
92+
includeInactive?: boolean;
93+
}
94+
8695
@Resolver(IProject)
8796
export class ProjectResolver {
8897
constructor(private readonly projectService: ProjectService) {}
@@ -148,6 +157,19 @@ export class ProjectResolver {
148157
return list;
149158
}
150159

160+
@ResolveField(() => Boolean, {
161+
description: 'Is the requesting user a member of this project?',
162+
})
163+
isMember(
164+
@Parent() project: Project,
165+
@Args() { includeInactive }: IsMemberArgs,
166+
): boolean {
167+
return (
168+
!!project.membership &&
169+
(includeInactive ? true : !project.membership.inactiveAt)
170+
);
171+
}
172+
151173
@ResolveField(() => String, { nullable: true })
152174
avatarLetters(@Parent() project: Project): string | undefined {
153175
return project.name.canRead && project.name.value

src/core/database/query/match-project-based-props.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export const matchProjectScopedRoles =
5858
.match([
5959
[
6060
node(projectVar),
61-
relation('out', '', 'member'),
61+
relation('out', '', 'member', ACTIVE),
6262
node('projectMember'),
6363
relation('out', '', 'user'),
6464
currentUser,

0 commit comments

Comments
 (0)