diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml
index 8f9c940..fe63ac6 100644
--- a/.github/workflows/build-and-deploy.yml
+++ b/.github/workflows/build-and-deploy.yml
@@ -1,27 +1,18 @@
-name: Build and Deploy to GitHub Pages
+name: CI Validate Pull Request
on:
- push:
- branches:
- - main
pull_request:
branches:
- main
permissions:
contents: read
- pages: write
- id-token: write
-
-concurrency:
- group: pages
- cancel-in-progress: false
jobs:
- build:
- name: Build Application
+ validate:
+ name: Build and Test
runs-on: ubuntu-latest
-
+
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -57,24 +48,8 @@ jobs:
echo "Server did not respond after 60s"
exit 1
- - name: Build application
- run: npm run build
+ - name: Run E2E tests
+ run: npx cucumber-js
- - name: Upload artifact
- uses: actions/upload-pages-artifact@v3
- with:
- path: ./dist
-
- deploy:
- name: Deploy to GitHub Pages
- if: github.ref == 'refs/heads/main' && github.event_name == 'push'
- needs: build
- runs-on: ubuntu-latest
- environment:
- name: github-pages
- url: ${{ steps.deployment.outputs.page_url }}
-
- steps:
- - name: Deploy to GitHub Pages
- id: deployment
- uses: actions/deploy-pages@v4
+ - name: Build application
+ run: npx vite build
diff --git a/.github/workflows/ci-dev.yml b/.github/workflows/ci-dev.yml
new file mode 100644
index 0000000..04a447c
--- /dev/null
+++ b/.github/workflows/ci-dev.yml
@@ -0,0 +1,105 @@
+name: CI – Dev Branch
+
+on:
+ push:
+ branches:
+ - Dev
+
+permissions:
+ contents: read
+
+jobs:
+ # ------------------------------------------------------------------
+ # 1. Run all E2E tests
+ # ------------------------------------------------------------------
+ test:
+ name: E2E Tests
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Install Playwright browser and system dependencies
+ run: npx playwright install --with-deps chromium
+
+ - name: Start dev server
+ run: nohup npx vite --host > /tmp/vite.log 2>&1 &
+
+ - name: Wait for dev server
+ run: |
+ echo "Waiting for Vite dev server..."
+ for i in $(seq 1 30); do
+ if curl -sf http://localhost:5173/SolutionInventory/ > /dev/null 2>&1; then
+ echo "Server is up after ${i} attempts"
+ exit 0
+ fi
+ echo "Attempt $i failed, retrying in 2s..."
+ sleep 2
+ done
+ echo "--- Vite log ---"
+ cat /tmp/vite.log
+ echo "Server did not respond after 60s"
+ exit 1
+
+ - name: Run E2E tests
+ run: npx cucumber-js
+
+ # ------------------------------------------------------------------
+ # 2. Verify Electron app builds (no release, no publish)
+ # ------------------------------------------------------------------
+ build-electron-check:
+ name: Electron Build Check (${{ matrix.artifact_name }})
+ needs: test
+ runs-on: ${{ matrix.os }}
+
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - os: ubuntu-latest
+ artifact_name: linux-x64
+ electron_flags: --linux --x64
+ - os: windows-latest
+ artifact_name: windows-x64
+ electron_flags: --win --x64
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: npm
+
+ - name: Install system dependencies (Ubuntu)
+ if: matrix.os == 'ubuntu-latest'
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y \
+ libx11-xcb1 libxrandr2 libxcomposite1 libxcursor1 libxdamage1 \
+ libxfixes3 libxi6 libgtk-3-0t64 libatk1.0-0t64 libcairo-gobject2 \
+ libgdk-pixbuf-2.0-0 libasound2t64 libgtk-4-1 libvulkan1 libopus0 \
+ libgstreamer1.0-0 libgstreamer-plugins-base1.0-0 \
+ libgstreamer-plugins-bad1.0-0 libflite1 libwebp7 libharfbuzz-icu0 \
+ libwayland-server0 libmanette-0.2-0 libenchant-2-2 libgbm1 libdrm2 \
+ libhyphen0 libgles2 rpm fakeroot dpkg
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Build Vite + Electron app (no publish)
+ run: npx vite build --mode electron && npx electron-builder ${{ matrix.electron_flags }} --publish never
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/electron-release.yml b/.github/workflows/electron-release.yml
index 09747aa..70ae11c 100644
--- a/.github/workflows/electron-release.yml
+++ b/.github/workflows/electron-release.yml
@@ -1,28 +1,87 @@
-name: Build and Release Electron App
+name: Release on Merge to Main
on:
push:
branches:
- - dev
- tags:
- - 'v*'
- workflow_dispatch:
+ - main
permissions:
contents: write
+ pages: write
+ id-token: write
+
+concurrency:
+ group: release-${{ github.run_id }}
+ cancel-in-progress: false
jobs:
- build-and-release:
- name: Build and Release
+ # ------------------------------------------------------------------
+ # 1. Bump the minor version, commit it back to main and create a tag
+ # ------------------------------------------------------------------
+ version:
+ name: Bump Version
+ runs-on: ubuntu-latest
+ # Prevent infinite loop: skip if the commit was created by this workflow
+ if: "!contains(github.event.head_commit.message, '[skip ci]')"
+ outputs:
+ version: ${{ steps.bump.outputs.version }}
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Configure Git identity
+ run: |
+ git config user.email "ci@github-actions"
+ git config user.name "GitHub Actions"
+
+ - name: Bump minor version and push
+ id: bump
+ run: |
+ npm version minor --no-git-tag-version
+ VERSION=$(node -p "require('./package.json').version")
+ echo "version=${VERSION}" >> $GITHUB_OUTPUT
+ git add package.json package-lock.json
+ git commit -m "chore: release v${VERSION} [skip ci]"
+ git tag "v${VERSION}"
+ git push origin main --follow-tags
+
+ # ------------------------------------------------------------------
+ # 2. Build the Electron app (parallel matrix: Linux + Windows)
+ # ------------------------------------------------------------------
+ build-electron:
+ name: Build Electron (${{ matrix.artifact_name }})
+ needs: version
runs-on: ${{ matrix.os }}
-
+
strategy:
+ fail-fast: false
matrix:
- os: [macos-latest, ubuntu-latest, windows-latest]
-
+ include:
+ - os: ubuntu-latest
+ artifact_name: linux-x64
+ electron_flags: --linux --x64
+ - os: windows-latest
+ artifact_name: windows-x64
+ electron_flags: --win --x64
+
steps:
- - name: Checkout repository
+ - name: Checkout tagged release
uses: actions/checkout@v4
+ with:
+ ref: v${{ needs.version.outputs.version }}
- name: Setup Node.js
uses: actions/setup-node@v4
@@ -30,62 +89,32 @@ jobs:
node-version: 20
cache: npm
- - name: Install system dependencies for Playwright (Ubuntu)
+ - name: Install system dependencies (Ubuntu)
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y \
- libx11-xcb1 \
- libxrandr2 \
- libxcomposite1 \
- libxcursor1 \
- libxdamage1 \
- libxfixes3 \
- libxi6 \
- libgtk-3-0t64 \
- libatk1.0-0t64 \
- libcairo-gobject2 \
- libgdk-pixbuf-2.0-0 \
- libasound2t64 \
- libgtk-4-1 \
- libvulkan1 \
- libopus0 \
- libgstreamer1.0-0 \
- libgstreamer-plugins-base1.0-0 \
- libgstreamer-plugins-bad1.0-0 \
- libflite1 \
- libwebp7 \
- libharfbuzz-icu0 \
- libwayland-server0 \
- libmanette-0.2-0 \
- libenchant-2-2 \
- libgbm1 \
- libdrm2 \
- libhyphen0 \
- libgles2
+ libx11-xcb1 libxrandr2 libxcomposite1 libxcursor1 libxdamage1 \
+ libxfixes3 libxi6 libgtk-3-0t64 libatk1.0-0t64 libcairo-gobject2 \
+ libgdk-pixbuf-2.0-0 libasound2t64 libgtk-4-1 libvulkan1 libopus0 \
+ libgstreamer1.0-0 libgstreamer-plugins-base1.0-0 \
+ libgstreamer-plugins-bad1.0-0 libflite1 libwebp7 libharfbuzz-icu0 \
+ libwayland-server0 libmanette-0.2-0 libenchant-2-2 libgbm1 libdrm2 \
+ libhyphen0 libgles2 rpm fakeroot dpkg
- name: Install dependencies
run: npm ci
- - name: Build Electron app
- run: npm run electron:build
+ - name: Build Vite + Electron app
+ run: npx vite build --mode electron && npx electron-builder ${{ matrix.electron_flags }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- - name: Upload artifacts (macOS)
- if: matrix.os == 'macos-latest'
- uses: actions/upload-artifact@v4
- with:
- name: macos-release
- path: |
- release/*.dmg
- release/*.zip
-
- - name: Upload artifacts (Ubuntu)
+ - name: Upload artifacts (Linux)
if: matrix.os == 'ubuntu-latest'
uses: actions/upload-artifact@v4
with:
- name: linux-release
+ name: ${{ matrix.artifact_name }}
path: |
release/*.AppImage
release/*.deb
@@ -94,16 +123,17 @@ jobs:
if: matrix.os == 'windows-latest'
uses: actions/upload-artifact@v4
with:
- name: windows-release
- path: |
- release/*.exe
-
+ name: ${{ matrix.artifact_name }}
+ path: release/*.exe
+
+ # ------------------------------------------------------------------
+ # 3. Create a GitHub Release with all Electron artifacts
+ # ------------------------------------------------------------------
create-release:
name: Create GitHub Release
- needs: build-and-release
+ needs: [version, build-electron]
runs-on: ubuntu-latest
- if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
-
+
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
@@ -111,11 +141,54 @@ jobs:
path: artifacts
- name: Create Release
- uses: softprops/action-gh-release@v1
+ uses: softprops/action-gh-release@v2
with:
- files: |
- artifacts/**/*
- draft: false
+ tag_name: v${{ needs.version.outputs.version }}
+ name: "Release v${{ needs.version.outputs.version }}"
+ body: |
+ ?? **Release v${{ needs.version.outputs.version }}**
+
+ Automatically built and released on merge to `main`.
+ Commit: ${{ github.sha }}
prerelease: false
+ files: artifacts/**/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ # ------------------------------------------------------------------
+ # 4. Build the web app and deploy to GitHub Pages
+ # ------------------------------------------------------------------
+ deploy-pages:
+ name: Deploy GitHub Pages
+ needs: version
+ runs-on: ubuntu-latest
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+
+ steps:
+ - name: Checkout tagged release
+ uses: actions/checkout@v4
+ with:
+ ref: v${{ needs.version.outputs.version }}
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Build web application
+ run: npx vite build
+
+ - name: Upload Pages artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: ./dist
+
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/README.md b/README.md
index 7caa862..f9276e4 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,9 @@
# Solution Inventory
+
+
+
+
Vue 3 + Vuetify application for documenting solution questionnaires across multiple projects. Available as both a Progressive Web App (PWA) and an Electron desktop application. It uses a project tree for navigation, questionnaire tabs for editing, and a configuration editor in a dialog.
## Features
diff --git a/electron/main.js b/electron/main.js
index e25e056..4ac3e3a 100644
--- a/electron/main.js
+++ b/electron/main.js
@@ -1,36 +1,94 @@
-const { app, BrowserWindow, ipcMain } = require('electron');
+const { app, BrowserWindow, ipcMain, dialog } = require('electron');
const path = require('path');
const fs = require('fs');
-// Handle creating/removing shortcuts on Windows when installing/uninstalling.
-if (require('electron-squirrel-startup')) {
- app.quit();
+const DATA_FILE_NAME = 'solution-inventory-data.json';
+
+function getConfigPath() {
+ return path.join(app.getPath('userData'), 'app-config.json');
+}
+
+function readConfig() {
+ try {
+ const configPath = getConfigPath();
+ if (fs.existsSync(configPath)) {
+ return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
+ }
+ } catch (e) {
+ console.error('Error reading config:', e);
+ }
+ return {};
+}
+
+function writeConfig(config) {
+ try {
+ fs.writeFileSync(getConfigPath(), JSON.stringify(config, null, 2));
+ } catch (e) {
+ console.error('Error writing config:', e);
+ }
}
let mainWindow;
+let splashWindow;
+
+function getLogoPath(size) {
+ if (size === 'icon') {
+ return app.isPackaged
+ ? path.join(__dirname, '../dist/icon.ico')
+ : path.join(__dirname, '../public/icon.ico');
+ }
+ const name = size === 'small' ? 'Logo-small.png' : 'Logo-Large.png';
+ return app.isPackaged
+ ? path.join(__dirname, '../dist', name)
+ : path.join(__dirname, '../public', name);
+}
+
+function createSplashWindow() {
+ splashWindow = new BrowserWindow({
+ width: 360,
+ height: 320,
+ frame: false,
+ transparent: false,
+ resizable: false,
+ center: true,
+ skipTaskbar: true,
+ icon: process.platform === 'win32' ? getLogoPath('icon') : getLogoPath('large')
+ });
+ splashWindow.loadFile(path.join(__dirname, 'splash.html'), {
+ query: { logo: getLogoPath('large') }
+ });
+}
function createWindow() {
- // Create the browser window.
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
+ show: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
},
- icon: path.join(__dirname, '../public/icon.png')
+ icon: process.platform === 'win32' ? getLogoPath('icon') : getLogoPath('large')
});
// In production, load the built files
if (app.isPackaged) {
mainWindow.loadFile(path.join(__dirname, '../dist/index.html'));
} else {
- // In development, load from Vite dev server
mainWindow.loadURL('http://localhost:5173');
mainWindow.webContents.openDevTools();
}
+ mainWindow.webContents.once('did-finish-load', () => {
+ if (splashWindow && !splashWindow.isDestroyed()) {
+ splashWindow.close();
+ splashWindow = null;
+ }
+ mainWindow.show();
+ mainWindow.focus();
+ });
+
mainWindow.on('closed', () => {
mainWindow = null;
});
@@ -39,6 +97,7 @@ function createWindow() {
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
app.whenReady().then(() => {
+ createSplashWindow();
createWindow();
app.on('activate', () => {
@@ -57,42 +116,53 @@ app.on('window-all-closed', () => {
}
});
-// IPC handlers for file operations
-ipcMain.handle('save-file', async (event, { filePath, data }) => {
+// IPC handlers for workspace directory management
+ipcMain.handle('get-workspace-dir', async () => {
+ const config = readConfig();
+ return config.workspaceDir || null;
+});
+
+ipcMain.handle('set-workspace-dir', async (event, dirPath) => {
+ const config = readConfig();
+ config.workspaceDir = dirPath;
+ writeConfig(config);
+ return { success: true };
+});
+
+ipcMain.handle('select-workspace-dir', async () => {
+ const result = await dialog.showOpenDialog(mainWindow, {
+ title: 'Select Workspace Directory',
+ properties: ['openDirectory', 'createDirectory'],
+ buttonLabel: 'Use as Workspace'
+ });
+ if (result.canceled || result.filePaths.length === 0) return null;
+ return result.filePaths[0];
+});
+
+// IPC handlers for data file I/O in the workspace directory
+ipcMain.handle('read-data-file', async () => {
try {
- const userDataPath = app.getPath('userData');
- const fullPath = path.join(userDataPath, filePath);
- const dir = path.dirname(fullPath);
-
- // Create directory if it doesn't exist
- if (!fs.existsSync(dir)) {
- fs.mkdirSync(dir, { recursive: true });
- }
-
- fs.writeFileSync(fullPath, JSON.stringify(data, null, 2));
- return { success: true, path: fullPath };
+ const config = readConfig();
+ if (!config.workspaceDir) return { success: false, error: 'No workspace dir configured' };
+ const filePath = path.join(config.workspaceDir, DATA_FILE_NAME);
+ if (!fs.existsSync(filePath)) return { success: false, error: 'File not found' };
+ const content = fs.readFileSync(filePath, 'utf-8');
+ return { success: true, data: JSON.parse(content) };
} catch (error) {
- console.error('Error saving file:', error);
+ console.error('Error reading data file:', error);
return { success: false, error: error.message };
}
});
-ipcMain.handle('load-file', async (event, filePath) => {
+ipcMain.handle('write-data-file', async (event, jsonString) => {
try {
- const userDataPath = app.getPath('userData');
- const fullPath = path.join(userDataPath, filePath);
-
- if (fs.existsSync(fullPath)) {
- const data = fs.readFileSync(fullPath, 'utf-8');
- return { success: true, data: JSON.parse(data) };
- }
- return { success: false, error: 'File not found' };
+ const config = readConfig();
+ if (!config.workspaceDir) return { success: false, error: 'No workspace dir configured' };
+ const filePath = path.join(config.workspaceDir, DATA_FILE_NAME);
+ fs.writeFileSync(filePath, jsonString);
+ return { success: true, path: filePath };
} catch (error) {
- console.error('Error loading file:', error);
+ console.error('Error writing data file:', error);
return { success: false, error: error.message };
}
});
-
-ipcMain.handle('get-user-data-path', async () => {
- return app.getPath('userData');
-});
diff --git a/electron/preload.js b/electron/preload.js
index ae41bb3..a537500 100644
--- a/electron/preload.js
+++ b/electron/preload.js
@@ -4,7 +4,11 @@ const { contextBridge, ipcRenderer } = require('electron');
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld('electronAPI', {
isElectron: true,
- saveFile: (filePath, data) => ipcRenderer.invoke('save-file', { filePath, data }),
- loadFile: (filePath) => ipcRenderer.invoke('load-file', filePath),
- getUserDataPath: () => ipcRenderer.invoke('get-user-data-path')
+ // Workspace directory management
+ getWorkspaceDir: () => ipcRenderer.invoke('get-workspace-dir'),
+ setWorkspaceDir: (dirPath) => ipcRenderer.invoke('set-workspace-dir', dirPath),
+ selectWorkspaceDir: () => ipcRenderer.invoke('select-workspace-dir'),
+ // Data file I/O (replaces localStorage in Electron mode)
+ readDataFile: () => ipcRenderer.invoke('read-data-file'),
+ writeDataFile: (jsonString) => ipcRenderer.invoke('write-data-file', jsonString),
});
diff --git a/electron/splash.html b/electron/splash.html
new file mode 100644
index 0000000..10d64f9
--- /dev/null
+++ b/electron/splash.html
@@ -0,0 +1,83 @@
+
+
+
+
+
+ Solution Inventory
+
+
+
+
+
![Solution Inventory]()
+
Solution Inventory
+
Loading…
+
+
+
+
+
diff --git a/index.html b/index.html
index fca17c4..b9b2aef 100644
--- a/index.html
+++ b/index.html
@@ -1,9 +1,10 @@
-
+
Solution Inventory
+
diff --git a/package.json b/package.json
index 29c866c..0d8bb02 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "solution-inventory-pwa",
- "version": "0.1.0",
+ "version": "0.9.0",
"description": "Solution Inventory - A questionnaire-based application for project assessment",
"author": "Hendrik Lösch ",
"private": true,
@@ -35,6 +35,7 @@
"build": {
"appId": "com.solutioninventory.app",
"productName": "Solution Inventory",
+ "icon": "public/Logo-Large",
"files": [
"dist/**/*",
"electron/**/*"
@@ -50,6 +51,7 @@
]
},
"win": {
+ "icon": "public/icon.ico",
"target": [
"nsis",
"portable"
diff --git a/public/Logo-Large.png b/public/Logo-Large.png
new file mode 100644
index 0000000..af6d769
Binary files /dev/null and b/public/Logo-Large.png differ
diff --git a/public/Logo-small.png b/public/Logo-small.png
new file mode 100644
index 0000000..d4cad93
Binary files /dev/null and b/public/Logo-small.png differ
diff --git a/public/icon.ico b/public/icon.ico
new file mode 100644
index 0000000..0e1769d
Binary files /dev/null and b/public/icon.ico differ
diff --git a/public/pwa-192.png b/public/pwa-192.png
deleted file mode 100644
index d252638..0000000
--- a/public/pwa-192.png
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/public/pwa-512.png b/public/pwa-512.png
deleted file mode 100644
index d252638..0000000
--- a/public/pwa-512.png
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/App.vue b/src/App.vue
index ec6e30d..1c38d96 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -5,6 +5,14 @@
mdi-menu
Toggle sidebar
+
Solution Inventory
@@ -46,6 +54,47 @@
+
+
+
+
+ Set Up Workspace
+
+
+
+ Please choose a directory where your workspace data will be stored.
+ The data will be saved as solution-inventory-data.json in that directory.
+
+
+
+
+ mdi-folder-open
+ Choose directory
+
+
+
+
+
+
+
+
+ Confirm
+
+
+
+
@@ -95,11 +144,25 @@ export default {
document.removeEventListener('mouseup', stopResize)
})
const store = useWorkspaceStore()
- const { lastSaved } = storeToRefs(store)
+ const { lastSaved, workspaceDirNeeded } = storeToRefs(store)
+
+ const isElectron = !!(window.electronAPI)
+ const baseUrl = import.meta.env.BASE_URL
+ const workspaceSetupDir = ref('')
+
+ async function selectDirectory() {
+ const dir = await window.electronAPI.selectWorkspaceDir()
+ if (dir) workspaceSetupDir.value = dir
+ }
+
+ async function confirmWorkspace() {
+ if (!workspaceSetupDir.value) return
+ await store.setWorkspaceDir(workspaceSetupDir.value)
+ }
// Beim Start versuchen, gespeicherte Daten zu laden
- onMounted(() => {
- store.initFromStorage()
+ onMounted(async () => {
+ await store.initFromStorage()
store.startAutoSave()
})
@@ -109,7 +172,13 @@ export default {
drawerOpen,
drawerWidth,
startResize,
- workspaceConfigOpen
+ workspaceConfigOpen,
+ isElectron,
+ baseUrl,
+ workspaceDirNeeded,
+ workspaceSetupDir,
+ selectDirectory,
+ confirmWorkspace
}
}
}
diff --git a/src/components/TreeNav.vue b/src/components/TreeNav.vue
index a1bd0a3..6defe43 100644
--- a/src/components/TreeNav.vue
+++ b/src/components/TreeNav.vue
@@ -563,7 +563,12 @@ export default {
categories: item.categories
}
})
- store.importProject(projectName, questionnaires)
+ const radarData = {
+ radarRefs: Array.isArray(data?.project?.radarRefs) ? data.project.radarRefs : [],
+ radarOverrides: Array.isArray(data?.project?.radarOverrides) ? data.project.radarOverrides : [],
+ radarCategoryOrder: Array.isArray(data?.project?.radarCategoryOrder) ? data.project.radarCategoryOrder : []
+ }
+ store.importProject(projectName, questionnaires, radarData)
closeImportDialog()
} catch (err) {
importError.value = `Import failed: ${err.message}`
diff --git a/src/stores/workspaceStore.js b/src/stores/workspaceStore.js
index 4c4f1fb..7f1a72f 100644
--- a/src/stores/workspaceStore.js
+++ b/src/stores/workspaceStore.js
@@ -42,6 +42,7 @@ export const useWorkspaceStore = defineStore('workspace', () => {
const lastSaved = ref('')
const autoSaveStarted = ref(false)
const pendingNavigation = ref(null) // { questionnaireId, categoryId, entryId } | null
+ const workspaceDirNeeded = ref(false)
const activeQuestionnaire = computed(() => {
return workspace.value.questionnaires.find((item) => item.id === activeQuestionnaireId.value) || null
@@ -86,40 +87,71 @@ export const useWorkspaceStore = defineStore('workspace', () => {
return [...projectTabs, ...questionnaireTabs]
})
- function initFromStorage() {
+ function applyStoredData(data) {
+ if (data.version === STORAGE_VERSION && data.workspace) {
+ workspace.value = data.workspace
+ activeQuestionnaireId.value = ''
+ openQuestionnaireIds.value = []
+ activeWorkspaceTabId.value = ''
+ openProjectSummaryIds.value = []
+ hydrateLastSaved(data.timestamp)
+ return true
+ }
+ if (data.version === STORAGE_VERSION && data.categories) {
+ const initialQuestionnaire = createQuestionnaire('Current questionnaire', data.categories)
+ workspace.value = createWorkspace([], [initialQuestionnaire])
+ activeQuestionnaireId.value = ''
+ openQuestionnaireIds.value = []
+ activeWorkspaceTabId.value = ''
+ openProjectSummaryIds.value = []
+ hydrateLastSaved(data.timestamp)
+ return true
+ }
+ return false
+ }
+
+ async function initFromStorage() {
+ // --- Electron: file-based storage ---
+ if (window.electronAPI) {
+ const dir = await window.electronAPI.getWorkspaceDir()
+ if (!dir) {
+ workspaceDirNeeded.value = true
+ return
+ }
+ workspaceDirNeeded.value = false
+ const result = await window.electronAPI.readDataFile()
+ if (result.success) {
+ try {
+ if (!applyStoredData(result.data)) seedWorkspace()
+ } catch (error) {
+ console.error('Error applying stored data:', error)
+ seedWorkspace()
+ }
+ } else {
+ seedWorkspace()
+ }
+ return
+ }
+
+ // --- Web: localStorage ---
const saved = localStorage.getItem(STORAGE_KEY)
if (!saved) {
seedWorkspace()
return
}
-
try {
const data = JSON.parse(saved)
- if (data.version === STORAGE_VERSION && data.workspace) {
- workspace.value = data.workspace
- activeQuestionnaireId.value = ''
- openQuestionnaireIds.value = []
- activeWorkspaceTabId.value = ''
- openProjectSummaryIds.value = []
- hydrateLastSaved(data.timestamp)
- return
- }
-
- if (data.version === STORAGE_VERSION && data.categories) {
- const initialQuestionnaire = createQuestionnaire('Current questionnaire', data.categories)
- workspace.value = createWorkspace([], [initialQuestionnaire])
- activeQuestionnaireId.value = ''
- openQuestionnaireIds.value = []
- activeWorkspaceTabId.value = ''
- openProjectSummaryIds.value = []
- hydrateLastSaved(data.timestamp)
- return
- }
+ if (!applyStoredData(data)) seedWorkspace()
} catch (error) {
console.error('Error loading from localStorage:', error)
+ seedWorkspace()
}
+ }
- seedWorkspace()
+ async function setWorkspaceDir(dirPath) {
+ if (!window.electronAPI) return
+ await window.electronAPI.setWorkspaceDir(dirPath)
+ await initFromStorage()
}
function seedWorkspace() {
@@ -132,31 +164,46 @@ export const useWorkspaceStore = defineStore('workspace', () => {
openProjectSummaryIds.value = []
}
+ let persistDebounceTimer = null
+
function startAutoSave() {
if (autoSaveStarted.value) return
autoSaveStarted.value = true
watch(
() => [workspace.value, activeQuestionnaireId.value, openQuestionnaireIds.value, activeWorkspaceTabId.value, openProjectSummaryIds.value],
- () => persist(),
+ () => {
+ clearTimeout(persistDebounceTimer)
+ persistDebounceTimer = setTimeout(() => persist(), 500)
+ },
{ deep: true }
)
}
- function persist() {
- try {
- const dataToSave = {
- version: STORAGE_VERSION,
- timestamp: new Date().toISOString(),
- workspace: workspace.value,
- activeQuestionnaireId: activeQuestionnaireId.value,
- openQuestionnaireIds: openQuestionnaireIds.value,
- activeWorkspaceTabId: activeWorkspaceTabId.value,
- openProjectSummaryIds: openProjectSummaryIds.value
+ async function persist() {
+ const dataToSave = {
+ version: STORAGE_VERSION,
+ timestamp: new Date().toISOString(),
+ workspace: workspace.value,
+ activeQuestionnaireId: activeQuestionnaireId.value,
+ openQuestionnaireIds: openQuestionnaireIds.value,
+ activeWorkspaceTabId: activeWorkspaceTabId.value,
+ openProjectSummaryIds: openProjectSummaryIds.value
+ }
+
+ if (window.electronAPI) {
+ try {
+ await window.electronAPI.writeDataFile(JSON.stringify(dataToSave, null, 2))
+ hydrateLastSaved(dataToSave.timestamp)
+ } catch (error) {
+ console.error('Error saving to file:', error)
+ }
+ } else {
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(dataToSave))
+ hydrateLastSaved(dataToSave.timestamp)
+ } catch (error) {
+ console.error('Error saving to localStorage:', error)
}
- localStorage.setItem(STORAGE_KEY, JSON.stringify(dataToSave))
- hydrateLastSaved(dataToSave.timestamp)
- } catch (error) {
- console.error('Error saving to localStorage:', error)
}
}
@@ -292,7 +339,7 @@ export const useWorkspaceStore = defineStore('workspace', () => {
return questionnaire.id
}
- function importProject(projectName, questionnaires) {
+ function importProject(projectName, questionnaires, radarData = {}) {
const name = String(projectName || '').trim()
if (!name) return
const projectId = addProject(name)
@@ -307,6 +354,9 @@ export const useWorkspaceStore = defineStore('workspace', () => {
? [...project.questionnaireIds, created.id]
: [created.id]
})
+ if (Array.isArray(radarData.radarRefs)) project.radarRefs = radarData.radarRefs
+ if (Array.isArray(radarData.radarOverrides)) project.radarOverrides = radarData.radarOverrides
+ if (Array.isArray(radarData.radarCategoryOrder)) project.radarCategoryOrder = radarData.radarCategoryOrder
}
function deleteQuestionnaire(questionnaireId) {
@@ -415,7 +465,10 @@ export const useWorkspaceStore = defineStore('workspace', () => {
const exportData = {
project: {
id: project.id,
- name: project.name
+ name: project.name,
+ radarRefs: Array.isArray(project.radarRefs) ? project.radarRefs : [],
+ radarOverrides: Array.isArray(project.radarOverrides) ? project.radarOverrides : [],
+ radarCategoryOrder: Array.isArray(project.radarCategoryOrder) ? project.radarCategoryOrder : []
},
questionnaires
}
@@ -743,11 +796,13 @@ export const useWorkspaceStore = defineStore('workspace', () => {
activeWorkspaceTabId,
openProjectSummaryIds,
lastSaved,
+ workspaceDirNeeded,
activeQuestionnaire,
activeCategories,
openTabs,
workspaceTabs,
initFromStorage,
+ setWorkspaceDir,
startAutoSave,
persist,
setActiveQuestionnaire,
diff --git a/tests/step_definitions/export-import.steps.js b/tests/step_definitions/export-import.steps.js
index dd6d8a8..b86198e 100644
--- a/tests/step_definitions/export-import.steps.js
+++ b/tests/step_definitions/export-import.steps.js
@@ -79,7 +79,12 @@ When('I import the {string} file', async function (filename) {
name: q.name || 'Imported questionnaire',
categories: q.categories,
}))
- store.importProject(data.project.name, questionnaires)
+ const radarData = {
+ radarRefs: Array.isArray(data?.project?.radarRefs) ? data.project.radarRefs : [],
+ radarOverrides: Array.isArray(data?.project?.radarOverrides) ? data.project.radarOverrides : [],
+ radarCategoryOrder: Array.isArray(data?.project?.radarCategoryOrder) ? data.project.radarCategoryOrder : [],
+ }
+ store.importProject(data.project.name, questionnaires, radarData)
}, importData)
})
diff --git a/vite.config.js b/vite.config.js
index 6496c94..ef2489b 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,12 +1,18 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { VitePWA } from 'vite-plugin-pwa'
+import { readFileSync } from 'fs'
+
+const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'))
export default defineConfig(({ mode }) => {
const isElectron = mode === 'electron';
return {
base: isElectron ? './' : '/SolutionInventory/',
+ define: {
+ __APP_VERSION__: JSON.stringify(pkg.version)
+ },
plugins: [
vue(),
// Only include PWA plugin for web build
@@ -19,8 +25,7 @@ export default defineConfig(({ mode }) => {
display: 'standalone',
background_color: '#ffffff',
icons: [
- { src: '/SolutionInventory/pwa-192.png', sizes: '192x192', type: 'image/png' },
- { src: '/SolutionInventory/pwa-512.png', sizes: '512x512', type: 'image/png' }
+ { src: '/SolutionInventory/Logo-Large.png', sizes: '512x512', type: 'image/png' }
]
}
})