Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions client/dive-common/components/ConfigurationEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import ActionShortcuts from './ActionEditors/ActionShortcuts.vue';
import TrackFilter from './ActionEditors/TrackFilter.vue';
import ConfigurationSettings from './configurationEditors/configurationSettings.vue';
import GeneralConfiguration from './configurationEditors/generalConfiguration.vue';
import LaunchConfigs from './configurationEditors/LaunchConfigs.vue';
import TimelineSettings from './configurationEditors/TimelineSettings.vue';
import UiSettings from './configurationEditors/uiSettings.vue';

Expand All @@ -21,6 +22,7 @@ export default defineComponent({
ActionEditorSettings,
ActionShortcuts,
TimelineSettings,
LaunchConfigs,
},
props: {
buttonOptions: {
Expand Down Expand Up @@ -116,6 +118,9 @@ export default defineComponent({
<v-row>
<timeline-settings :disabled="!hasConfig" />
</v-row>
<v-row>
<launch-configs :disabled="!hasConfig" />
</v-row>
</v-card-text>
</v-card>
</template>
Expand Down
21 changes: 14 additions & 7 deletions client/dive-common/components/PrevNext.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
import TooltipButton from 'vue-media-annotator/components/TooltipButton.vue';
import { useConfiguration, useHandler } from 'vue-media-annotator/provides';
import { useRouter } from 'vue-router/composables';
import { readConfigFolderIdFromUrl } from 'vue-media-annotator/use/configFolderParam';

export default defineComponent({
name: 'PrevNext',
Expand Down Expand Up @@ -41,20 +42,26 @@ export default defineComponent({
next.value = configMap.prevNext.value?.next;
});

const gotoId = (id: string) => {
router.push({ name: 'viewer', params: { id } });
};

const queryStringParams = ref('');
watch(getDiveMetadataRootId, (newval) => {
diveMetadataRootId.value = newval;
const params: string[] = [];
if (diveMetadataRootId.value) {
queryStringParams.value = `?diveMetadataRootId=${diveMetadataRootId.value}`;
} else {
queryStringParams.value = '';
params.push(`diveMetadataRootId=${diveMetadataRootId.value}`);
}
const configFolderId = readConfigFolderIdFromUrl();
if (configFolderId) {
params.push(`configFolderId=${encodeURIComponent(configFolderId)}`);
}
queryStringParams.value = params.length ? `?${params.join('&')}` : '';
}, { immediate: true });

const gotoId = (id: string) => {
// Keep session overrides (configFolderId, diveMetadataRootId) on keyboard
// Prev/Next the same way click navigation does via :to + queryStringParams.
router.push(`/viewer/${id}${queryStringParams.value}`);
};

return {
previous,
next,
Expand Down
36 changes: 35 additions & 1 deletion client/dive-common/components/Viewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ import SlicerTaskRunnerVue from 'platform/web-girder/views/SlicerTaskRunner.vue'
import useURLParameters from 'vue-media-annotator/use/useURLParameters';
import useUINotifications from 'platform/web-girder/UIControls';
import { getLatestRevision } from 'platform/web-girder/api/annotation.service';
import { getDiveConfiguration } from 'platform/web-girder/api/dataset.service';
import { readConfigFolderIdFromUrl } from 'vue-media-annotator/use/configFolderParam';
import { putDiveMetadataLastModified } from 'platform/web-girder/api/divemetadata.service';
import { OverlayPreferences } from 'vue-media-annotator/types';
import { useRoute } from 'vue-router/composables';
Expand Down Expand Up @@ -653,6 +655,8 @@ export default defineComponent({
ctx.emit('change-camera', camera);
};

/** Folder id from ?configFolderId= that overrides the hierarchy base config for this session */
const configFolderOverride: Ref<string | null> = ref(null);
const initializeConfig = (config: { metadata: DatasetMeta & DatasetMetaMutable;
diveConfig: DiveConfiguration & {metadata: DatasetMetaMutable}; }) => {
const meta = config.metadata;
Expand All @@ -672,7 +676,10 @@ export default defineComponent({
);
visualMaskManager.load(config.diveConfig.metadata.configuration.visualMasks);

if (config.diveConfig.metadata.configuration.general?.baseConfiguration) {
if (configFolderOverride.value) {
// Session-only override: edits target the override folder, stored base is untouched
configurationManager.setConfigurationId(configFolderOverride.value);
} else if (config.diveConfig.metadata.configuration.general?.baseConfiguration) {
configurationManager.setConfigurationId(
config.diveConfig.metadata.configuration.general.baseConfiguration,
);
Expand All @@ -693,6 +700,33 @@ export default defineComponent({
// Close and reset sideBar
context.resetActive();
let config = await loadMetadata(datasetId.value);
configFolderOverride.value = readConfigFolderIdFromUrl();
if (configFolderOverride.value) {
try {
const override = (await getDiveConfiguration(configFolderOverride.value)).data;
if (!override.metadata?.configuration) {
throw new Error('Target folder has no configuration');
}
// Only overlay the config folder's metadata; prevNext / hierarchy / configOwners
// stay those of the dataset being viewed
config = {
...config,
diveConfig: {
...config.diveConfig,
metadata: override.metadata as typeof config.diveConfig.metadata,
},
};
} catch (err) {
// eslint-disable-next-line no-console
console.warn('configFolderId override failed, using default configuration', err);
configFolderOverride.value = null;
prompt({
title: 'Config folder unavailable',
text: 'The requested configuration folder could not be loaded (missing, no configuration, or no access). Using the default configuration.',
positiveButton: 'OK',
});
}
}
if (props.revision > 0) {
latestRevisionId.value = props.revision;
} else {
Expand Down
168 changes: 168 additions & 0 deletions client/dive-common/components/configurationEditors/LaunchConfigs.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
<script lang="ts">
import { defineComponent, ref } from 'vue';
import { cloneDeep } from 'lodash';
import { useConfiguration } from 'vue-media-annotator/provides';
import { LaunchConfig } from 'vue-media-annotator/ConfigurationManager';
import {
DEFAULT_LAUNCH_COLOR,
LaunchConfigRow,
normalizeLaunchColor,
normalizeLaunchConfigs,
} from 'vue-media-annotator/launchConfigs';
import { getFolder } from 'platform/web-girder/api/girder.service';
import { invalidateLaunchConfigs } from 'platform/web-girder/use/useLaunchConfigs';
import LaunchConfigsEditor from './LaunchConfigsEditor.vue';

export default defineComponent({
name: 'LaunchConfigs',
components: { LaunchConfigsEditor },
props: {
disabled: {
type: Boolean,
default: false,
},
},
setup() {
const { configuration, saveConfiguration, configurationId } = useConfiguration();
const dialog = ref(false);
const rows = ref<LaunchConfigRow[]>([]);
const saving = ref(false);

const launchEditor = () => {
rows.value = cloneDeep(normalizeLaunchConfigs(configuration.value?.launchConfigs)).map(
(r) => ({ ...r, color: normalizeLaunchColor(r.color) }),
);
dialog.value = true;
};
const addRow = () => {
rows.value.push({
id: `${Date.now()}${rows.value.length}`,
label: '',
configurationFolderId: '',
color: DEFAULT_LAUNCH_COLOR,
order: rows.value.length,
});
};
const removeRow = (index: number) => rows.value.splice(index, 1);

/** The target must be an accessible folder that has a stored configuration */
const validateRow = async (row: LaunchConfigRow) => {
let error = '';
let folderName = '';
if (!row.label.trim()) {
error = 'Label is required';
} else if (!row.configurationFolderId.trim()) {
error = 'Folder id is required';
} else {
try {
const folder = (await getFolder(row.configurationFolderId.trim())).data;
if (!folder.meta?.configuration) {
error = 'Folder has no configuration';
}
folderName = folder.name;
} catch (err) {
error = 'Folder not found or no access';
}
}
const target = row;
target.error = error;
target.folderName = folderName;
return !error;
};

const saveChanges = async () => {
saving.value = true;
const results = await Promise.all(rows.value.map(validateRow));
if (results.every((r) => r)) {
const launchConfigs: LaunchConfig[] = rows.value.map((r, order) => ({
id: r.id,
label: r.label.trim(),
tooltip: r.tooltip || undefined,
color: r.color || undefined,
configurationFolderId: r.configurationFolderId.trim(),
order,
}));
await saveConfiguration(configurationId.value, { launchConfigs });
// Keep the in-memory configuration in sync so reopening the editor
// (which reads from `configuration`) shows the saved values instead
// of stale/deleted entries.
configuration.value = { ...configuration.value, launchConfigs };
invalidateLaunchConfigs();
dialog.value = false;
}
saving.value = false;
};

return {
dialog,
rows,
saving,
launchEditor,
addRow,
removeRow,
saveChanges,
validateRow,
};
},
});
</script>

<template>
<div class="ma-2">
<v-btn
:disabled="disabled"
@click="launchEditor"
>
<span>Launch Configs<br></span>
<v-icon class="ml-2">
mdi-rocket-launch
</v-icon>
</v-btn>
<v-dialog
v-model="dialog"
max-width="900"
>
<v-card>
<v-card-title>
Launch Configs
<v-spacer />
<v-btn
icon
small
@click="dialog = false"
>
<v-icon small>
mdi-close
</v-icon>
</v-btn>
</v-card-title>
<v-card-text>
<LaunchConfigsEditor
:rows="rows"
description="Each entry adds a launch button that opens the viewer using the configuration of another folder (?configFolderId=). Empty list shows the default Launch Annotator button."
@add="addRow"
@remove="removeRow"
@validate="validateRow"
/>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
text
depressed
@click="dialog = false"
>
Cancel
</v-btn>
<v-btn
color="primary"
:loading="saving"
@click="saveChanges"
>
Save
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
Loading
Loading