Skip to content

Commit

Permalink
Filter scheduling goals and conditions by plan scheduling specificati…
Browse files Browse the repository at this point in the history
…on id (#1072)

* Differentiate between all scheduling goals/conditions and those specifically related to the plan specification id
* Use correct property when destructing createSchedulingSpecGoal response
* Handle goals not associated with plans in the ScheduingGoals table
* Reworked permission checking for scheduling goals and conditions to handle orphaned entities
  • Loading branch information
AaronPlave authored Jan 22, 2024
1 parent 59449e4 commit 54db44c
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 90 deletions.
37 changes: 18 additions & 19 deletions src/components/scheduling/Scheduling.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<svelte:options immutable={true} />

<script lang="ts">
import { schedulingColumns, schedulingConditions, schedulingGoals } from '../../stores/scheduling';
import { schedulingColumns, schedulingConditionsAll, schedulingGoalsAll } from '../../stores/scheduling';
import type { User } from '../../types/app';
import type { DataGridRowSelection } from '../../types/data-grid';
import type { PlanSchedulingSpec } from '../../types/plan';
Expand All @@ -22,14 +22,14 @@
let editorTitle: string = 'Scheduling';
$: if (selectedCondition !== null) {
const found = $schedulingConditions.findIndex(condition => condition.id === selectedCondition?.id);
const found = $schedulingConditionsAll.findIndex(condition => condition.id === selectedCondition?.id);
if (found === -1) {
selectedCondition = null;
}
}
$: if (selectedGoal !== null) {
const found = $schedulingGoals.findIndex(goal => goal.id === selectedGoal?.id);
const found = $schedulingGoalsAll.findIndex(goal => goal.id === selectedGoal?.id);
if (found === -1) {
selectedGoal = null;
}
Expand All @@ -44,7 +44,7 @@
const success = await effects.deleteSchedulingCondition(condition, plan, user);
if (success) {
schedulingConditions.filterValueById(condition.id);
schedulingConditionsAll.filterValueById(condition.id);
if (condition.id === selectedCondition?.id) {
selectedCondition = null;
Expand All @@ -55,33 +55,32 @@
async function deleteGoal(goal: SchedulingGoalSlim) {
const { scheduling_specification_goal } = goal;
const specification_id = scheduling_specification_goal.specification_id;
const plan = plans?.find(plan => plan.scheduling_specifications[0]?.id === specification_id);
if (plan) {
const success = await effects.deleteSchedulingGoal(goal, plan, user);
if (success) {
schedulingGoals.filterValueById(goal.id);
let plan = null;
if (scheduling_specification_goal) {
const specification_id = scheduling_specification_goal.specification_id;
plan = plans?.find(plan => plan.scheduling_specifications[0]?.id === specification_id) ?? null;
}
const success = await effects.deleteSchedulingGoal(goal, plan, user);
if (success) {
schedulingGoalsAll.filterValueById(goal.id);
if (goal.id === selectedGoal?.id) {
selectedGoal = null;
}
if (goal.id === selectedGoal?.id) {
selectedGoal = null;
}
}
}
function deleteConditionContext(event: CustomEvent<number>) {
const id = event.detail;
const condition = $schedulingConditions.find(s => s.id === id);
const condition = $schedulingConditionsAll.find(s => s.id === id);
if (condition) {
deleteCondition(condition);
}
}
function deleteGoalContext(event: CustomEvent<number>) {
const id = event.detail;
const goal = $schedulingGoals.find(s => s.id === id);
const goal = $schedulingGoalsAll.find(s => s.id === id);
if (goal) {
deleteGoal(goal);
}
Expand Down Expand Up @@ -129,7 +128,7 @@
<SchedulingGoals
{plans}
{selectedGoal}
schedulingGoals={$schedulingGoals}
schedulingGoals={$schedulingGoalsAll}
{user}
on:deleteGoal={deleteGoalContext}
on:rowSelected={toggleGoal}
Expand All @@ -138,7 +137,7 @@
<SchedulingConditions
{plans}
{selectedCondition}
schedulingConditions={$schedulingConditions}
schedulingConditions={$schedulingConditionsAll}
{user}
on:deleteCondition={deleteConditionContext}
on:rowSelected={toggleCondition}
Expand Down
26 changes: 12 additions & 14 deletions src/components/scheduling/conditions/SchedulingConditions.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -121,24 +121,22 @@
editCondition({ id: event.detail[0] as number });
}
function hasDeletePermission(condition: SchedulingCondition) {
const { scheduling_specification_conditions } = condition;
const specification_id = scheduling_specification_conditions[0].specification_id;
const plan = plans?.find(plan => plan.scheduling_specifications[0]?.id === specification_id);
if (plan) {
return featurePermissions.schedulingConditions.canDelete(user, plan);
function getPlanForCondition(condition: SchedulingCondition): PlanSchedulingSpec | null {
// If there is no spec id attached to the goal, the goal is not associated with a plan
let plan = null;
if (condition.scheduling_specification_conditions.length > 0) {
const specification_id = condition.scheduling_specification_conditions[0].specification_id;
plan = plans?.find(plan => plan.scheduling_specifications[0]?.id === specification_id) ?? null;
}
return false;
return plan;
}
function hasDeletePermission(condition: SchedulingCondition) {
return featurePermissions.schedulingConditions.canDelete(user, getPlanForCondition(condition), condition);
}
function hasEditPermission(condition: SchedulingCondition) {
const { scheduling_specification_conditions } = condition;
const specification_id = scheduling_specification_conditions[0].specification_id;
const plan = plans?.find(plan => plan.scheduling_specifications[0]?.id === specification_id);
if (plan) {
return featurePermissions.schedulingConditions.canUpdate(user, plan);
}
return false;
return featurePermissions.schedulingConditions.canUpdate(user, getPlanForCondition(condition), condition);
}
function hasCreatePermission(user: User): boolean {
Expand Down
30 changes: 14 additions & 16 deletions src/components/scheduling/goals/SchedulingGoals.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -150,26 +150,24 @@
editGoal({ id: event.detail[0] as number });
}
function hasDeletePermission(goal: SchedulingGoal) {
const {
scheduling_specification_goal: { specification_id },
} = goal;
const plan = plans?.find(plan => plan.scheduling_specifications[0]?.id === specification_id);
if (plan) {
return featurePermissions.schedulingGoals.canDelete(user, plan);
function getPlanForGoal(goal: SchedulingGoal): PlanSchedulingSpec | null {
// If there is no spec id attached to the goal, the goal is not associated with a plan
let plan = null;
if (goal.scheduling_specification_goal) {
const {
scheduling_specification_goal: { specification_id },
} = goal;
plan = plans?.find(plan => plan.scheduling_specifications[0]?.id === specification_id) ?? null;
}
return false;
return plan;
}
function hasDeletePermission(goal: SchedulingGoal) {
return featurePermissions.schedulingGoals.canDelete(user, getPlanForGoal(goal), goal);
}
function hasEditPermission(goal: SchedulingGoal) {
const {
scheduling_specification_goal: { specification_id },
} = goal;
const plan = plans?.find(plan => plan.scheduling_specifications[0]?.id === specification_id);
if (plan) {
return featurePermissions.schedulingGoals.canUpdate(user, plan);
}
return false;
return featurePermissions.schedulingGoals.canUpdate(user, getPlanForGoal(goal), goal);
}
function hasCreatePermission(user: User): boolean {
Expand Down
116 changes: 96 additions & 20 deletions src/stores/scheduling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,42 +25,118 @@ export const selectedSpecId = derived(plan, $plan => $plan?.scheduling_specifica

/* Subscriptions. */

export const schedulingConditions = gqlSubscribable<SchedulingCondition[]>(gql.SUB_SCHEDULING_CONDITIONS, {}, [], null);
export const schedulingConditionsAll = gqlSubscribable<SchedulingCondition[]>(
gql.SUB_SCHEDULING_CONDITIONS,
{},
[],
null,
);

export const schedulingGoals = gqlSubscribable<SchedulingGoalSlim[]>(gql.SUB_SCHEDULING_GOALS, {}, [], null);
export const schedulingConditions = derived(
[selectedSpecId, schedulingConditionsAll],
([$selectedSpecId, $schedulingConditionsAll]) => {
return $schedulingConditionsAll.map(schedulingSpecCondition => {
return {
...schedulingSpecCondition,
scheduling_specification_conditions: schedulingSpecCondition.scheduling_specification_conditions.filter(
condition => condition.specification_id === $selectedSpecId,
),
};
});
},
);

export const schedulingGoalsAll = gqlSubscribable<SchedulingGoalSlim[]>(gql.SUB_SCHEDULING_GOALS, {}, [], null);

export const schedulingGoals = derived(
[selectedSpecId, schedulingGoalsAll],
([$selectedSpecId, $schedulingGoalsAll]) => {
return $schedulingGoalsAll
.filter(goal => goal.scheduling_specification_goal?.specification_id === $selectedSpecId)
.map(goal => {
return {
...goal,
analyses: goal.analyses.filter(analysis => analysis.request.specification_id === $selectedSpecId),
};
});
},
);

export const schedulingSpecConditions = gqlSubscribable<SchedulingSpecCondition[]>(
export const schedulingSpecConditionsAll = gqlSubscribable<SchedulingSpecCondition[]>(
gql.SUB_SCHEDULING_SPEC_CONDITIONS,
{ specification_id: selectedSpecId },
[],
null,
);

export const schedulingSpecGoals = gqlSubscribable<SchedulingSpecGoal[]>(
export const schedulingSpecConditions = derived(
[selectedSpecId, schedulingSpecConditionsAll],
([$selectedSpecId, $schedulingSpecConditionsAll]) => {
return $schedulingSpecConditionsAll
.filter(schedulingSpecCondition => schedulingSpecCondition.specification_id === $selectedSpecId)
.map(schedulingSpecCondition => {
return {
...schedulingSpecCondition,
condition: {
...schedulingSpecCondition.condition,
scheduling_specification_conditions:
schedulingSpecCondition.condition.scheduling_specification_conditions.filter(
condition => condition.specification_id === $selectedSpecId,
),
},
};
});
},
);

export const schedulingSpecGoalsAll = gqlSubscribable<SchedulingSpecGoal[]>(
gql.SUB_SCHEDULING_SPEC_GOALS,
{ specification_id: selectedSpecId },
[],
null,
);

export const latestAnalyses = derived(schedulingSpecGoals, $schedulingSpecGoals => {
const analysisIdToSpecGoalMap: Record<number, SchedulingGoalAnalysis[]> = {};
let latestAnalysisId = -1;

$schedulingSpecGoals.forEach(schedulingSpecGoal => {
schedulingSpecGoal.goal.analyses.forEach(analysis => {
if (!analysisIdToSpecGoalMap[analysis.analysis_id]) {
analysisIdToSpecGoalMap[analysis.analysis_id] = [];
}
analysisIdToSpecGoalMap[analysis.analysis_id].push(analysis);
if (analysis.analysis_id > latestAnalysisId) {
latestAnalysisId = analysis.analysis_id;
}
export const schedulingSpecGoals = derived(
[selectedSpecId, schedulingSpecGoalsAll],
([$selectedSpecId, $schedulingSpecGoalsAll]) => {
return $schedulingSpecGoalsAll
.filter(schedulingSpecGoal => schedulingSpecGoal.specification_id === $selectedSpecId)
.map(specGoal => {
return {
...specGoal,
goal: {
...specGoal.goal,
analyses: specGoal.goal.analyses.filter(analysis => analysis.request.specification_id === $selectedSpecId),
},
};
});
},
);

export const latestAnalyses = derived(
[selectedSpecId, schedulingSpecGoals],
([$selectedSpecId, $schedulingSpecGoals]) => {
const analysisIdToSpecGoalMap: Record<number, SchedulingGoalAnalysis[]> = {};
let latestAnalysisId = -1;

$schedulingSpecGoals.forEach(schedulingSpecGoal => {
schedulingSpecGoal.goal.analyses.forEach(analysis => {
if (analysis.request.specification_id !== $selectedSpecId) {
return;
}
if (!analysisIdToSpecGoalMap[analysis.analysis_id]) {
analysisIdToSpecGoalMap[analysis.analysis_id] = [];
}
analysisIdToSpecGoalMap[analysis.analysis_id].push(analysis);
if (analysis.analysis_id > latestAnalysisId) {
latestAnalysisId = analysis.analysis_id;
}
});
});
});

return analysisIdToSpecGoalMap[latestAnalysisId] || [];
});
return analysisIdToSpecGoalMap[latestAnalysisId] || [];
},
);

export const schedulingGoalCount = derived(latestAnalyses, $latestAnalyses => Object.keys($latestAnalyses).length);
export const satisfiedSchedulingGoalCount = derived(
Expand Down
4 changes: 4 additions & 0 deletions src/types/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ export type AssetWithOwner<T = any> = Partial<T> & {
owner: UserId;
};

export type AssetWithAuthor<T = any> = Partial<T> & {
author: UserId;
};

export type ModelWithOwner = Pick<Model, 'id' | 'owner'>;

export type PermissibleQueriesMap = Record<string, true>;
Expand Down
3 changes: 2 additions & 1 deletion src/types/scheduling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export type SchedulingGoal = {
revision: number;
scheduling_specification_goal: {
specification_id: number;
};
} | null;
tags: { tag: Tag }[];
};

Expand All @@ -40,6 +40,7 @@ export type SchedulingCondition = {

export type SchedulingGoalAnalysis = {
analysis_id: number;
request: { specification_id: number };
satisfied: boolean;
satisfying_activities: { activity_id: number }[];
};
Expand Down
16 changes: 10 additions & 6 deletions src/utilities/effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1197,9 +1197,9 @@ const effects = {
}

const data = await reqHasura<SchedulingSpecGoal>(gql.CREATE_SCHEDULING_SPEC_GOAL, { spec_goal }, user);
const { createSchedulingGoal } = data;
if (createSchedulingGoal != null) {
const { specification_id } = createSchedulingGoal;
const { createSchedulingSpecGoal } = data;
if (createSchedulingSpecGoal != null) {
const { specification_id } = createSchedulingSpecGoal;
return specification_id;
} else {
throw Error('Unable to create a scheduling spec goal');
Expand Down Expand Up @@ -1974,11 +1974,11 @@ const effects = {

async deleteSchedulingCondition(
condition: SchedulingCondition,
plan: PlanSchedulingSpec,
plan: PlanSchedulingSpec | null,
user: User | null,
): Promise<boolean> {
try {
if (!queryPermissions.DELETE_SCHEDULING_CONDITION(user, plan)) {
if (!queryPermissions.DELETE_SCHEDULING_CONDITION(user, plan, condition)) {
throwPermissionError('delete this scheduling condition');
}

Expand Down Expand Up @@ -2006,7 +2006,11 @@ const effects = {
}
},

async deleteSchedulingGoal(goal: SchedulingGoalSlim, plan: PlanSchedulingSpec, user: User | null): Promise<boolean> {
async deleteSchedulingGoal(
goal: SchedulingGoalSlim,
plan: PlanSchedulingSpec | null,
user: User | null,
): Promise<boolean> {
try {
if (!queryPermissions.DELETE_SCHEDULING_GOAL(user, plan)) {
throwPermissionError('delete this scheduling goal');
Expand Down
Loading

0 comments on commit 54db44c

Please sign in to comment.