Skip to content

Commit

Permalink
improve script selection functionality
Browse files Browse the repository at this point in the history
- Improve validation for configuration import
- Refactor SaveSelectionButton.vue implementation
- Add floppy-disk-gear icon for SaveSelectionButton
  • Loading branch information
e-salad committed Nov 23, 2024
1 parent 2c2c742 commit 7aa31e5
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 21 deletions.
2 changes: 2 additions & 0 deletions src/infrastructure/Dialog/Browser/FileSaverDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,6 @@ const MimeTypes: Record<FileType, string> = {
// otherwise they ignore extension and save the file as text.
[FileType.BatchFile]: 'application/bat', // https://en.wikipedia.org/wiki/Batch_file
[FileType.ShellScript]: 'text/x-shellscript', // https://de.wikipedia.org/wiki/Shellskript#MIME-Typ
[FileType.Json]: 'application/json', // https://en.wikipedia.org/wiki/JSON

} as const;
4 changes: 4 additions & 0 deletions src/presentation/assets/icons/floppy-disk-gear.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<IconButton
text="Save Selection"
icon-name="content-save"
icon-name="floppy-disk-gear"
@click="saveSelection"
/>
</template>
Expand All @@ -11,6 +11,13 @@ import { defineComponent } from 'vue';
import { injectKey } from '@/presentation/injectionSymbols';
import { FileType } from '@/presentation/common/Dialog';
import IconButton from '../IconButton.vue';
import { createScriptErrorDialog } from '../ScriptErrorDialog';
interface SelectionConfig {
version: string;
timestamp: string;
selectedScripts: string[];
}
export default defineComponent({
components: {
Expand All @@ -20,25 +27,28 @@ export default defineComponent({
const { currentSelection } = injectKey((keys) => keys.useUserSelectionState);
const { dialog } = injectKey((keys) => keys.useDialog);
const { projectDetails } = injectKey((keys) => keys.useApplication);
const { scriptDiagnosticsCollector } = injectKey((keys) => keys.useScriptDiagnosticsCollector);
async function saveSelection() {
// Create a config object with the current selection state
const config = {
version: projectDetails.version,
selectedScripts: currentSelection.value.scripts.selectedScripts.map((script) => script.id),
const config: SelectionConfig = {
version: projectDetails.version.toString(),
timestamp: new Date().toISOString(),
selectedScripts: currentSelection.value.scripts.selectedScripts.map((script) => script.id),
};
const configJson = JSON.stringify(config, null, 2);
const { success, error } = await dialog.saveFile(
configJson,
JSON.stringify(config, null, 2),
'privacy-selection.json',
FileType.Json,
);
if (!success && error) {
console.error('Failed to save selection:', error);
if (!success) {
dialog.showError(...(await createScriptErrorDialog({
errorContext: 'save',
errorType: error.type,
errorMessage: error.message,
isFileReadbackError: error.type === 'FileReadbackVerificationError',
}, scriptDiagnosticsCollector)));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,18 @@
<TooltipWrapper>
<MenuOptionListItem
label="Import"
:enabled="true"
:enabled="!isImporting"
@click="loadFromFile"
/>
<template #tooltip>
<RecommendationDocumentation
:privacy-rating="0"
description="Restores a previously saved script selection from a JSON file."
recommendation="..."
:considerations="[
'All current selections will be cleared before import',
'Only .json files exported by privacy.sexy are supported',
]"
/>
</template>
</TooltipWrapper>
Expand All @@ -102,7 +106,7 @@

<script lang="ts">
import {
defineComponent, computed,
defineComponent, computed, ref,
} from 'vue';
import { injectKey } from '@/presentation/injectionSymbols';
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
Expand All @@ -114,6 +118,8 @@ import { RecommendationStatusType } from './RecommendationStatusType';
import RecommendationDocumentation from './RecommendationDocumentation.vue';
interface SavedSelection {
version?: string;
timestamp?: string;
selectedScripts: string[];
}
Expand All @@ -129,6 +135,7 @@ export default defineComponent({
currentSelection, modifyCurrentSelection,
} = injectKey((keys) => keys.useUserSelectionState);
const { currentState } = injectKey((keys) => keys.useCollectionState);
const { dialog } = injectKey((keys) => keys.useDialog);
const currentCollection = computed<ICategoryCollection>(() => currentState.value.collection);
Expand All @@ -142,6 +149,8 @@ export default defineComponent({
},
});
const isImporting = ref(false);
function selectRecommendationStatusType(type: RecommendationStatusType) {
if (currentRecommendationStatusType.value === type) {
return;
Expand All @@ -155,13 +164,16 @@ export default defineComponent({
}
async function loadFromFile() {
if (isImporting.value) {
return;
}
try {
// Use file input to load the JSON file
isImporting.value = true;
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
// Create a promise to handle the file selection
const file = await new Promise<File>((resolve, reject) => {
input.onchange = (event) => {
const { files } = (event.target as HTMLInputElement);
Expand All @@ -174,23 +186,53 @@ export default defineComponent({
input.click();
});
// Read the file content
const content = await new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = () => reject(reader.error);
reader.onerror = () => reject(new Error('Failed to read file'));
reader.readAsText(file);
});
const savedSelection = JSON.parse(content) as SavedSelection;
let savedSelection: SavedSelection;
try {
savedSelection = JSON.parse(content) as SavedSelection;
if (!Array.isArray(savedSelection.selectedScripts)) {
throw new Error('Invalid file format: missing or invalid scripts array');
}
} catch (parseError) {
dialog.showError('Import Error', 'The selected file is not a valid selection file.');
return;
}
// Update the current selection state
modifyCurrentSelection((selection) => {
await modifyCurrentSelection((selection) => {
// First deselect all scripts
selection.scripts.deselectAll();
// Then select all scripts from the saved selection
savedSelection.selectedScripts.forEach((scriptId) => {
// Validate and apply each script selection
const validScripts = savedSelection.selectedScripts.filter(
(scriptId) => {
try {
return currentCollection.value.getScript(scriptId) !== undefined;
} catch {
return false;
}
},
);
if (validScripts.length === 0) {
throw new Error('No valid scripts found in the imported selection');
}
if (validScripts.length !== savedSelection.selectedScripts.length) {
dialog.showError(
'Import Warning',
'Some scripts from the imported selection were not found in the current collection.',
);
}
// Apply valid script selections
validScripts.forEach((scriptId) => {
selection.scripts.processChanges({
changes: [{
scriptId,
Expand All @@ -203,7 +245,12 @@ export default defineComponent({
});
});
} catch (error) {
console.error('Failed to load selection:', error);
if (error instanceof Error && error.message !== 'No file selected') {
dialog.showError('Import Error', `Failed to import selection: ${error.message}`);
console.error('Failed to load selection:', error);
}
} finally {
isImporting.value = false;
}
}
Expand All @@ -212,6 +259,7 @@ export default defineComponent({
currentRecommendationStatusType,
selectRecommendationStatusType,
loadFromFile,
isImporting,
};
},
});
Expand Down
1 change: 1 addition & 0 deletions src/presentation/components/Shared/Icon/IconName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const IconNames = [
'left-right',
'file-arrow-down',
'floppy-disk',
'floppy-disk-gear',
'play',
'lightbulb',
'square-check',
Expand Down

0 comments on commit 7aa31e5

Please sign in to comment.