diff --git a/e2e-tests/fixtures/ExternalSources.ts b/e2e-tests/fixtures/ExternalSources.ts index ea6e27dcf3..8b90960788 100644 --- a/e2e-tests/fixtures/ExternalSources.ts +++ b/e2e-tests/fixtures/ExternalSources.ts @@ -49,10 +49,13 @@ export class ExternalSources { } async deleteSource(sourceName: string) { - await this.selectSource(sourceName); - await this.deleteSourceButton.click(); - await this.deleteSourceButtonConfirmation.click(); - await expect(this.externalSourcesTable.getByText(sourceName)).not.toBeVisible(); + // Only delete a source if its visible in the table + if (await this.page.getByRole('gridcell', { name: sourceName }).first().isVisible()) { + await this.selectSource(sourceName); + await this.deleteSourceButton.click(); + await this.deleteSourceButtonConfirmation.click(); + await expect(this.externalSourcesTable.getByText(sourceName)).not.toBeVisible(); + } } async fillInputFile(externalSourceFilePath: string) { @@ -88,10 +91,10 @@ export class ExternalSources { await expect(this.page.getByRole('button', { exact: true, name: sourceTypeName })).toBeVisible(); } - async selectEvent() { + async selectEvent(eventName: string, sourceName: string = 'example-external-source.json') { // Assumes the selected source was the test source, and selects the specific event from it // NOTE: This may not be the case, and should be re-visited when we implement deletion for External Sources! - await this.selectSource(); + await this.selectSource(sourceName); await this.page.getByRole('gridcell', { name: 'ExampleEvent:1/sc/sc1:1' }).click(); } diff --git a/e2e-tests/tests/external-sources.test.ts b/e2e-tests/tests/external-sources.test.ts index 44ce76ef87..4cc04a6c4a 100644 --- a/e2e-tests/tests/external-sources.test.ts +++ b/e2e-tests/tests/external-sources.test.ts @@ -37,7 +37,7 @@ test.describe.serial('External Sources', () => { }); test('External event form should be shown when an event is selected', async () => { - await externalSources.selectEvent(); + await externalSources.selectEvent('ExampleEvent:1/sc/sc1:1'); await expect(externalSources.inputFile).not.toBeVisible(); }); @@ -54,7 +54,7 @@ test.describe.serial('External Sources', () => { }); test('External event deselection should be shown when a source is selected', async () => { - await externalSources.selectEvent(); + await externalSources.selectEvent('ExampleEvent:1/sc/sc1:1'); await expect(page.getByLabel('Deselect event')).toBeVisible(); }); diff --git a/e2e-tests/tests/plan-external-source.test.ts b/e2e-tests/tests/plan-external-source.test.ts index 36bccb305e..f531459a1e 100644 --- a/e2e-tests/tests/plan-external-source.test.ts +++ b/e2e-tests/tests/plan-external-source.test.ts @@ -50,25 +50,11 @@ test.afterAll(async () => { await externalSources.goto(); // Cleanup all test files that *may* have been uploaded - if (await page.getByRole('gridcell', { name: externalSources.externalSourceFileName }).first().isVisible()) { - await externalSources.deleteSource(externalSources.externalSourceFileName); - } - - if (await page.getByRole('gridcell', { name: externalSources.derivationTestFileKey1 }).first().isVisible()) { - await externalSources.deleteSource(externalSources.derivationTestFileKey1); - } - - if (await page.getByRole('gridcell', { name: externalSources.derivationTestFileKey2 }).first().isVisible()) { - await externalSources.deleteSource(externalSources.derivationTestFileKey2); - } - - if (await page.getByRole('gridcell', { name: externalSources.derivationTestFileKey3 }).first().isVisible()) { - await externalSources.deleteSource(externalSources.derivationTestFileKey3); - } - - if (await page.getByRole('gridcell', { name: externalSources.derivationTestFileKey4 }).first().isVisible()) { - await externalSources.deleteSource(externalSources.derivationTestFileKey4); - } + await externalSources.deleteSource(externalSources.externalSourceFileName); + await externalSources.deleteSource(externalSources.derivationTestFileKey1); + await externalSources.deleteSource(externalSources.derivationTestFileKey2); + await externalSources.deleteSource(externalSources.derivationTestFileKey3); + await externalSources.deleteSource(externalSources.derivationTestFileKey4); await page.close(); await context.close(); diff --git a/src/components/external-source/ExternalSourceManager.svelte b/src/components/external-source/ExternalSourceManager.svelte index e903a5d02f..2cf3258604 100644 --- a/src/components/external-source/ExternalSourceManager.svelte +++ b/src/components/external-source/ExternalSourceManager.svelte @@ -37,7 +37,7 @@ import { parseJSONStream } from '../../utilities/generic'; import { permissionHandler } from '../../utilities/permissionHandler'; import { featurePermissions } from '../../utilities/permissions'; - import { convertUTCtoMs, formatDate, getIntervalInMs } from '../../utilities/time'; + import { convertUTCToMs, formatDate, getIntervalInMs } from '../../utilities/time'; import { showFailureToast } from '../../utilities/toast'; import { tooltip } from '../../utilities/tooltip'; import { required, timestamp } from '../../utilities/validators'; @@ -296,7 +296,7 @@ ...externalEventsDB, duration_ms: getIntervalInMs(externalEventsDB.duration), event_type: externalEventsDB.pkey.event_type_name, - start_ms: convertUTCtoMs(externalEventsDB.start_time), + start_ms: convertUTCToMs(externalEventsDB.start_time), }; })), ); @@ -530,7 +530,6 @@ -
- + {#if selectedSourceLinkedDerivationGroupsPlans.length > 0} + {#each selectedSourceLinkedDerivationGroupsPlans as linkedPlanDerivationGroup} + + + {$plans.find(plan => { + return linkedPlanDerivationGroup.plan_id === plan.id; + })?.name} + + + {/each} + {:else} + Not used in any plans + {/if} + + +
+
- -
{:else} @@ -762,7 +759,7 @@ {columnDefs} hasDeletePermission={hasDeleteExternalSourcePermissionOnRow} singleItemDisplayText="External Source" - pluralItemDisplayText="External Source" + pluralItemDisplayText="External Sources" {filterExpression} items={$externalSources.map(externalSource => { return { ...externalSource, id: getExternalSourceSlimRowId(externalSource) }; @@ -815,9 +812,9 @@ /> {:else if $externalSources.length} -

Select a source to view contents.

+

Select a source to view contents.

{:else} -

No External Sources present.

+

No External Sources present.

{/if} @@ -901,4 +898,12 @@ .selected-source-forms { height: 100%; } + + .selected-source-prompt { + padding-left: 4px; + } + + .selected-source-delete { + padding-top: 12px; + } diff --git a/src/components/external-source/ExternalSourcePanelEntry.svelte b/src/components/external-source/ExternalSourcePanelEntry.svelte index d333bc06eb..0f4da353d8 100644 --- a/src/components/external-source/ExternalSourcePanelEntry.svelte +++ b/src/components/external-source/ExternalSourcePanelEntry.svelte @@ -116,7 +116,6 @@
{/each} (); let mappedSources: { [sourceType: string]: { [derivationGroup: string]: ExternalSourceSlim[] } } = {}; - let hasAcknowledgePermission: boolean = false; + let hasUpdatePermission: boolean = false; $: if ($plan !== null) { - hasAcknowledgePermission = featurePermissions.derivationGroupAcknowledgement.canUpdate(user, $plan); + hasUpdatePermission = featurePermissions.derivationGroupAcknowledgement.canUpdate(user, $plan); } $: sources.forEach(source => { @@ -95,7 +95,8 @@ class="st-button secondary hover-fix" on:click={() => dispatch('dismiss')} use:permissionHandler={{ - hasPermission: hasAcknowledgePermission, + hasPermission: hasUpdatePermission, + permissionError: "You do not have permission to acknowledge this external source." }} > Dismiss diff --git a/src/components/external-source/ExternalSourcesPanel.svelte b/src/components/external-source/ExternalSourcesPanel.svelte index 894ff5866c..672873bb0a 100644 --- a/src/components/external-source/ExternalSourcesPanel.svelte +++ b/src/components/external-source/ExternalSourcesPanel.svelte @@ -82,7 +82,6 @@ {}, ); } - planDerivationGroupLinks.subscribe(_ => (mappedDerivationGroups = {})); // clear the map... $: filteredDerivationGroups.forEach(group => { // ...and repopulate it every time the links change. this handles deletion correctly if (group.source_type_name) { diff --git a/src/components/modals/CreateGroupsOrTypesModal.svelte b/src/components/modals/CreateGroupsOrTypesModal.svelte index b984eb5c86..0a1b328fbe 100644 --- a/src/components/modals/CreateGroupsOrTypesModal.svelte +++ b/src/components/modals/CreateGroupsOrTypesModal.svelte @@ -115,6 +115,7 @@ on:click|preventDefault={onCreateDerivationGroup} use:permissionHandler={{ hasPermission: hasCreateDerivationGroupPermission, + permissionError: "You do not have permission to create a derivation group." }} > Create @@ -142,6 +143,7 @@ on:click|preventDefault={onCreateExternalSourceType} use:permissionHandler={{ hasPermission: hasCreateExternalSourceTypePermission, + permissionError: "You do not have permission to create an external source type." }} > Create @@ -169,6 +171,7 @@ on:click|preventDefault={onCreateExternalEventType} use:permissionHandler={{ hasPermission: hasCreateExternalEventTypePermission, + permissionError: "You do not have permission to create an external event type." }} > Create diff --git a/src/components/modals/ManageGroupsAndTypesModal.svelte b/src/components/modals/ManageGroupsAndTypesModal.svelte index 7f3c34ed5a..a20c3e2a42 100644 --- a/src/components/modals/ManageGroupsAndTypesModal.svelte +++ b/src/components/modals/ManageGroupsAndTypesModal.svelte @@ -100,25 +100,7 @@ headerName: 'External Source Type', resizable: true, sortable: true, - }, - { - filter: 'number', - headerName: 'Associated External Sources', - sortable: true, - valueFormatter: params => { - const associatedSources = getAssociatedExternalSourcesBySourceType(params.data?.name); - return `${associatedSources.length}`; - }, - }, - { - filter: 'number', - headerName: 'Associated Derivation Groups', - sortable: true, - valueFormatter: params => { - const associatedDerivationGroups = getAssociatedDerivationGroupsBySourceTypeName(params.data?.name); - return `${associatedDerivationGroups.length}`; - }, - }, + } ]; const externalEventTypeBaseColumnDefs: DataGridColumnDef[] = [ { @@ -127,28 +109,7 @@ headerName: 'External Event Type', resizable: true, sortable: true, - }, - { - filter: 'number', - headerName: 'Associated External Sources', - sortable: true, - valueFormatter: params => { - let associatedDerivationGroups = getAssociatedDerivationGroupsByEventType(params.data?.name); - const sourceMap = associatedDerivationGroups.flatMap(derivationGroup => derivationGroup.sources.size); - const numOfSources = - sourceMap.length > 0 ? sourceMap.reduce((acc, derivationGroupSize) => acc + derivationGroupSize) : 0; - return `${numOfSources}`; - }, - }, - { - filter: 'number', - headerName: 'Associated Derivation Groups', - sortable: true, - valueFormatter: params => { - const associatedDerivationGroups = getAssociatedDerivationGroupsByEventType(params.data?.name); - return `${associatedDerivationGroups.length}`; - }, - }, + } ]; let derivationGroupColumnsDef: DataGridColumnDef[] = derivationGroupBaseColumnDefs; @@ -233,6 +194,24 @@ $: externalSourceTypeColumnDefs = [ ...externalSourceTypeBaseColumnDefs, + { + filter: 'number', + headerName: 'Associated External Sources', + sortable: true, + valueFormatter: params => { + const associatedSources = getAssociatedExternalSourcesBySourceType(params.data?.name); + return `${associatedSources.length}`; + }, + }, + { + filter: 'number', + headerName: 'Associated Derivation Groups', + sortable: true, + valueFormatter: params => { + const associatedDerivationGroups = getAssociatedDerivationGroupsBySourceTypeName(params.data?.name); + return `${associatedDerivationGroups.length}`; + }, + }, { cellClass: 'action-cell-container', cellRenderer: (params: ModalCellRendererParams) => { @@ -271,6 +250,27 @@ $: externalEventTypeColumnDefs = [ ...externalEventTypeBaseColumnDefs, + { + filter: 'number', + headerName: 'Associated External Sources', + sortable: true, + valueFormatter: params => { + let associatedDerivationGroups = getAssociatedDerivationGroupsByEventType(params.data?.name); + const sourceMap = associatedDerivationGroups.flatMap(derivationGroup => derivationGroup.sources.size); + const numOfSources = + sourceMap.length > 0 ? sourceMap.reduce((acc, derivationGroupSize) => acc + derivationGroupSize) : 0; + return `${numOfSources}`; + }, + }, + { + filter: 'number', + headerName: 'Associated Derivation Groups', + sortable: true, + valueFormatter: params => { + const associatedDerivationGroups = getAssociatedDerivationGroupsByEventType(params.data?.name); + return `${associatedDerivationGroups.length}`; + }, + }, { cellClass: 'action-cell-container', cellRenderer: (params: ModalCellRendererParams) => { @@ -500,7 +500,6 @@ {/each} {/each} onUpdateDerivationGroups(selectedDerivationGroups)} use:permissionHandler={{ hasPermission: hasUpdateDerivationGroupLinkPermission, + permissionError: "You do not have permission to update this derivation group/plan link." }} > Update diff --git a/src/stores/external-event.ts b/src/stores/external-event.ts index 1dfb1b3f66..42e0fb4e84 100644 --- a/src/stores/external-event.ts +++ b/src/stores/external-event.ts @@ -4,7 +4,7 @@ import { derived, writable, type Readable, type Writable } from 'svelte/store'; import type { ExternalEvent, ExternalEventDB, ExternalEventId, ExternalEventType } from '../types/external-event'; import { getExternalEventWholeRowId } from '../utilities/externalEvents'; import gql from '../utilities/gql'; -import { convertDoyToYmd, convertUTCtoMs, getIntervalInMs } from '../utilities/time'; +import { convertDoyToYmd, convertUTCToMs, getIntervalInMs } from '../utilities/time'; import { selectedPlanDerivationGroupNames } from './external-source'; import { plan } from './plan'; import { gqlSubscribable } from './subscribable'; @@ -33,10 +33,10 @@ export const externalEvents: Readable = derived( const completeExternalEvents: ExternalEvent[] = []; if ($externalEventsRaw !== null && $externalEventsRaw !== undefined) { // get plan bounds in an easily comparable format. The explicit strings are extreme bounds in case of a null plan. - const planStartTime = convertUTCtoMs( + const planStartTime = convertUTCToMs( convertDoyToYmd($plan?.start_time_doy ?? '1970-001T00:00:00Z') ?? '1970-01-01T00:00:00Z', ); - const planEndTime = convertUTCtoMs( + const planEndTime = convertUTCToMs( convertDoyToYmd($plan?.end_time_doy ?? '2100-001T00:00:00Z') ?? '2100-01-01T00:00:00Z', ); @@ -45,7 +45,7 @@ export const externalEvents: Readable = derived( const externalEvent = e.external_event; // check event is in plan bounds - const externalEventStartTime = convertUTCtoMs(externalEvent.start_time); + const externalEventStartTime = convertUTCToMs(externalEvent.start_time); const externalEventEndTime = externalEventStartTime + getIntervalInMs(externalEvent.duration); if ( @@ -62,7 +62,7 @@ export const externalEvents: Readable = derived( source_key: externalEvent.source_key, }, properties: externalEvent.properties, - start_ms: convertUTCtoMs(externalEvent.start_time), + start_ms: convertUTCToMs(externalEvent.start_time), start_time: externalEvent.start_time, }); } diff --git a/src/stores/external-source.ts b/src/stores/external-source.ts index 6c6217bc13..ab17138db2 100644 --- a/src/stores/external-source.ts +++ b/src/stores/external-source.ts @@ -4,8 +4,10 @@ import { type DerivationGroup, type ExternalSourceSlim, type ExternalSourceType, + type ExternalSourceWithId, type PlanDerivationGroup, } from '../types/external-source'; +import { getExternalSourceSlimRowId } from '../utilities/externalEvents'; import gql from '../utilities/gql'; import { planId } from './plan'; import { gqlSubscribable } from './subscribable'; @@ -60,6 +62,7 @@ export const selectedPlanDerivationGroupNames: Readable = derived( ([$planDerivationGroupLinks, $planId]) => $planDerivationGroupLinks.filter(link => link.plan_id === $planId).map(link => link.derivation_group_name), ); + export const selectedPlanDerivationGroupEventTypes: Readable = derived( [derivationGroups, selectedPlanDerivationGroupNames], ([$derivationGroups, $selectedPlanDerivationGroupIds]) => { @@ -77,6 +80,15 @@ export const selectedPlanDerivationGroupEventTypes: Readable = derived }, ); +export const externalSourcesWithIds: Readable = derived( + [externalSources], + ([$externalSources]) => { + $externalSources.map(externalSource => { + return { ...externalSource, id: getExternalSourceSlimRowId(externalSource) }; + }); + }, +); + /* Helper Functions. */ export function resetExternalSourceStores(): void { createExternalSourceError.set(null); diff --git a/src/types/external-source.ts b/src/types/external-source.ts index 5d330cf544..c17d0e248e 100644 --- a/src/types/external-source.ts +++ b/src/types/external-source.ts @@ -39,6 +39,9 @@ export type ExternalSourceJson = { // For use in retrieval of source information sans bulky items like metadata and event lists (see stores) export type ExternalSourceSlim = Omit; +// For use in table-related activities that require an 'id' field +export type ExternalSourceWithId = ExternalSourceSlim & { id: string }; + // Similar to ExternalSourceDB, but uses ExternalSourcePkey to represent the primary key (key, derivation_group_name) export type ExternalSource = Omit & { pkey: ExternalSourcePkey }; diff --git a/src/types/permissions.ts b/src/types/permissions.ts index 39e95d2641..994b709bec 100644 --- a/src/types/permissions.ts +++ b/src/types/permissions.ts @@ -1,5 +1,4 @@ import type { User, UserId, UserRole } from './app'; -import type { DerivationGroup, ExternalSource } from './external-source'; import type { Model } from './model'; import type { Plan } from './plan'; @@ -11,10 +10,6 @@ export type AssetWithAuthor = Partial & { author: UserId; }; -export type ExternalSourceWithOwner = Pick; - -export type DerivationGroupWithOwner = Pick; - export type ModelWithOwner = Pick; export type PermissibleQueriesMap = Record; diff --git a/src/utilities/effects.ts b/src/utilities/effects.ts index 1e34dc74ad..95888a7612 100644 --- a/src/utilities/effects.ts +++ b/src/utilities/effects.ts @@ -245,7 +245,7 @@ import { compareEvents } from './simulation'; import { pluralize } from './text'; import { convertDoyToYmd, - convertUTCtoMs, + convertUTCToMs, getDoyTime, getDoyTimeFromInterval, getIntervalFromDoyRange, @@ -2376,6 +2376,8 @@ const effects = { // If the return was null, do nothing - only act on success or non-null if (sourceDissociation) { showSuccessToast('Derivation Group Disassociated Successfully'); + } else { + showFailureToast('Derivation Group Disassociation Failed'); } } else { throw Error('Plan is not defined.'); @@ -3754,7 +3756,7 @@ const effects = { source_key: event.source_key, }, properties: event.properties, - start_ms: convertUTCtoMs(event.start_time), + start_ms: convertUTCToMs(event.start_time), start_time: event.start_time, }); } @@ -4803,6 +4805,8 @@ const effects = { // If the return was null, do nothing - only act on success or non-null if (sourceAssociation !== null) { showSuccessToast('Derivation Group Linked Successfully'); + } else { + showFailureToast('Derivation Group Link Failed'); } } else { throw Error('Plan is not defined.'); diff --git a/src/utilities/gql.ts b/src/utilities/gql.ts index 8800697401..fc932d2dea 100644 --- a/src/utilities/gql.ts +++ b/src/utilities/gql.ts @@ -939,18 +939,18 @@ const gql = { `, DELETE_DERIVATION_GROUP: `#graphql - mutation DeleteDerivationGroup($name: String!) { - deleteDerivationGroupForPlan: ${Queries.DELETE_PLAN_DERIVATION_GROUP}(where: { derivation_group_name: { _eq: $name }}) { - returning { - derivation_group_name + mutation DeleteDerivationGroup($name: String!) { + deleteDerivationGroupForPlan: ${Queries.DELETE_PLAN_DERIVATION_GROUP}(where: { derivation_group_name: { _eq: $name }}) { + returning { + derivation_group_name + } } - } - deleteDerivationGroup: ${Queries.DELETE_DERIVATION_GROUP}(where: { name: { _eq: $name } }) { - returning { - name + deleteDerivationGroup: ${Queries.DELETE_DERIVATION_GROUP}(where: { name: { _eq: $name } }) { + returning { + name + } } } - } `, DELETE_EXPANSION_RULE: `#graphql @@ -1480,36 +1480,36 @@ const gql = { `, GET_EXTERNAL_EVENTS: `#graphql - query GetExternalEvents( - $sourceKey: String!, - $derivationGroupName: String! - ) { - ${Queries.EXTERNAL_EVENT}( - where: { - source_key: {_eq: $sourceKey}, - derivation_group_name: {_eq: $derivationGroupName} - } + query GetExternalEvents( + $sourceKey: String!, + $derivationGroupName: String! ) { - properties - event_type_name - key - duration - start_time - source_key + ${Queries.EXTERNAL_EVENT}( + where: { + source_key: {_eq: $sourceKey}, + derivation_group_name: {_eq: $derivationGroupName} + } + ) { + properties + event_type_name + key + duration + start_time + source_key + } } - } `, GET_EXTERNAL_EVENT_BY_EVENT_TYPE: `#graphql - query GetExternalEventByEventType($event_type_name: String!) { - ${Queries.EXTERNAL_EVENT}(where: {event_type_name: { _eq: $event_type_name }}) { - key - event_type_name - start_time - duration - properties - } - } + query GetExternalEventByEventType($event_type_name: String!) { + ${Queries.EXTERNAL_EVENT}(where: {event_type_name: { _eq: $event_type_name }}) { + key + event_type_name + start_time + duration + properties + } + } `, GET_EXTERNAL_EVENT_TYPES: `#graphql diff --git a/src/utilities/permissions.ts b/src/utilities/permissions.ts index c8328083aa..f3bc79c6d8 100644 --- a/src/utilities/permissions.ts +++ b/src/utilities/permissions.ts @@ -4,14 +4,12 @@ import type { User, UserRole } from '../types/app'; import type { ReqAuthResponse } from '../types/auth'; import type { ConstraintDefinition, ConstraintMetadata, ConstraintRun } from '../types/constraint'; import type { ExpansionRule, ExpansionSequence, ExpansionSet } from '../types/expansion'; -import type { DerivationGroup, ExternalSourceSlim } from '../types/external-source'; +import type { DerivationGroup, ExternalSource, ExternalSourceSlim } from '../types/external-source'; import type { Model } from '../types/model'; import type { AssetWithAuthor, AssetWithOwner, CreatePermissionCheck, - DerivationGroupWithOwner, - ExternalSourceWithOwner, ModelWithOwner, PermissionCheck, PlanWithOwners, @@ -358,7 +356,18 @@ const queryPermissions: Record b return isUserAdmin(user) || getPermission([Queries.INSERT_EXTERNAL_EVENT_TYPE_ONE], user); }, CREATE_EXTERNAL_SOURCE: (user: User | null): boolean => { - return isUserAdmin(user) || getPermission([Queries.INSERT_EXTERNAL_SOURCE], user); + return ( + isUserAdmin(user) || + getPermission( + [ + Queries.INSERT_EXTERNAL_SOURCE, + Queries.INSERT_EXTERNAL_EVENT_TYPE, + Queries.INSERT_EXTERNAL_SOURCE_TYPE, + Queries.INSERT_DERIVATION_GROUP, + ], + user, + ) + ); }, CREATE_EXTERNAL_SOURCE_TYPE: (user: User | null): boolean => { return isUserAdmin(user) || getPermission([Queries.INSERT_EXTERNAL_SOURCE_TYPE], user); @@ -511,10 +520,11 @@ const queryPermissions: Record b (isPlanOwner(user, plan) || isPlanCollaborator(user, plan))) ); }, - DELETE_DERIVATION_GROUP: (user: User | null, derivationGroup: DerivationGroupWithOwner): boolean => { + DELETE_DERIVATION_GROUP: (user: User | null, derivationGroup: AssetWithOwner): boolean => { return ( isUserAdmin(user) || - (getPermission([Queries.DELETE_DERIVATION_GROUP], user) && isUserOwner(user, derivationGroup)) + (getPermission([Queries.DELETE_DERIVATION_GROUP, Queries.DELETE_PLAN_DERIVATION_GROUP], user) && + isUserOwner(user, derivationGroup)) ); }, DELETE_EXPANSION_RULE: (user: User | null, expansionRule: AssetWithOwner): boolean => { @@ -539,7 +549,7 @@ const queryPermissions: Record b DELETE_EXTERNAL_EVENT_TYPE: (user: User | null): boolean => { return isUserAdmin(user) || getPermission([Queries.DELETE_EXTERNAL_EVENT_TYPE], user); }, - DELETE_EXTERNAL_SOURCES: (user: User | null, externalSources: ExternalSourceWithOwner[]): boolean => { + DELETE_EXTERNAL_SOURCES: (user: User | null, externalSources: AssetWithOwner[]): boolean => { return ( isUserAdmin(user) || (getPermission([Queries.DELETE_EXTERNAL_SOURCE], user) && @@ -577,7 +587,7 @@ const queryPermissions: Record b DELETE_PLAN_DERIVATION_GROUP: (user: User | null, plan: PlanWithOwners): boolean => { return ( isUserAdmin(user) || - (getPermission([Queries.DELETE_PLAN_DERIVATION_GROUP], user) && + (getPermission([Queries.DELETE_PLAN_DERIVATION_GROUP, Queries.DELETE_SCHEDULING_SPECIFICATION], user) && (isPlanOwner(user, plan) || isPlanCollaborator(user, plan))) ); }, @@ -1378,7 +1388,7 @@ const featurePermissions: FeaturePermissions = { canCreate: user => queryPermissions.CREATE_PLAN_DERIVATION_GROUP(user), canDelete: user => queryPermissions.DELETE_PLAN_DERIVATION_GROUP(user), canRead: user => queryPermissions.SUB_PLAN_DERIVATION_GROUP(user), - canUpdate: () => false, // this is not a feature TODO should it be, instead of create/delete? + canUpdate: () => false, // this is not a feature }, expansionRules: { canCreate: user => queryPermissions.CREATE_EXPANSION_RULE(user), @@ -1564,3 +1574,4 @@ export { isUserOwner, queryPermissions, }; + diff --git a/src/utilities/time.test.ts b/src/utilities/time.test.ts index bcd5adb0e3..d0caff8477 100644 --- a/src/utilities/time.test.ts +++ b/src/utilities/time.test.ts @@ -5,7 +5,7 @@ import { convertDurationStringToInterval, convertDurationStringToUs, convertUsToDurationString, - convertUTCtoMs, + convertUTCToMs, getActivityDirectiveStartTimeMs, getBalancedDuration, getDaysInMonth, @@ -41,21 +41,21 @@ test('convertDurationStringToUs', () => { `); }); -test('convertUTCtoMs', () => { +test('convertUTCToMs', () => { // standard date conversion - expect(convertUTCtoMs('2024-01-01T00:00:00Z')).toEqual(1704067200000); + expect(convertUTCToMs('2024-01-01T00:00:00Z')).toEqual(1704067200000); // DOY doesn't work - expect(convertUTCtoMs('2024-001T00:00:00Z')).toEqual(NaN); + expect(convertUTCToMs('2024-001T00:00:00Z')).toEqual(NaN); // conversion to DOY is fine if the time zone ("Z") is excluded - expect(convertUTCtoMs(convertDoyToYmd('2024-001T00:00:00') ?? '')).toEqual(1704067200000); + expect(convertUTCToMs(convertDoyToYmd('2024-001T00:00:00') ?? '')).toEqual(1704067200000); - // conversion without a timezone in the input - expect(convertUTCtoMs('2024-01-01 00:00:00')).toEqual(new Date('2024-01-01 00:00:00').getTime()); + // conversion without a timezone in the input - this is compared to a new Date object in order to use the test runner's machine's timezone (as the result of convertUTCToMs should follow *that* timezone) + expect(convertUTCToMs('2024-01-01 00:00:00')).toEqual(new Date('2024-01-01 00:00:00').getTime()); // any other string fails - expect(convertUTCtoMs('not a date')).toEqual(NaN); + expect(convertUTCToMs('not a date')).toEqual(NaN); }); test('convertDurationStringToInterval', () => { diff --git a/src/utilities/time.ts b/src/utilities/time.ts index 583c92a1fe..47626952f5 100644 --- a/src/utilities/time.ts +++ b/src/utilities/time.ts @@ -427,7 +427,7 @@ export function convertDurationStringToInterval(durationString: string): string * @param date The date as a string to convert. * @returns The number of milliseconds since epoch start for this date. */ -export function convertUTCtoMs(date: string): number { +export function convertUTCToMs(date: string): number { const d = new Date(date); return d.getTime(); }