Initial commit: qvw-preserve-interface skill
This commit is contained in:
commit
a3cb35f078
5 changed files with 330 additions and 0 deletions
172
SKILL.md
Normal file
172
SKILL.md
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
---
|
||||||
|
name: qvw-preserve-interface
|
||||||
|
description: >
|
||||||
|
Protect QVW interface edits (sheets, tableboxes, charts, listboxes) from being
|
||||||
|
destroyed by reload-save cycles. Sets up local git tracking of the -prj/ folder,
|
||||||
|
enables QlikView's native auto-backup, installs a rolling 20-slot QVW backup,
|
||||||
|
and wraps the reload BAT with pre/post-reload git commits so every interface
|
||||||
|
edit is recoverable even if the QVW binary gets clobbered. Use this skill when
|
||||||
|
the user says "my interface disappeared", "reload overwrote my edits",
|
||||||
|
"protect my QVW objects", "version-control the QlikView app",
|
||||||
|
"set up git for the QVW project", or mentions losing tableboxes/charts
|
||||||
|
after a reload. Also apply proactively when scaffolding any new QVW project
|
||||||
|
that will be reloaded repeatedly. Uses the COM GetProperties/SetProperties
|
||||||
|
pattern (NOT SetScript — which doesn't exist on modern QV Document).
|
||||||
|
---
|
||||||
|
|
||||||
|
# QVW Interface Preservation (git-backed)
|
||||||
|
|
||||||
|
You are setting up a git-based safety net for QlikView Desktop projects so
|
||||||
|
that **every interface edit survives reload cycles**. Without this, QV's
|
||||||
|
default behaviour can silently drop charts/tableboxes whose source fields
|
||||||
|
change during a reload.
|
||||||
|
|
||||||
|
## Why this is needed (the failure mode)
|
||||||
|
|
||||||
|
1. User opens `app.qvw` in QlikView, adds a tablebox or chart, saves.
|
||||||
|
2. Reload BAT fires (`qv.exe /r app.qvw`) — QV re-executes the script,
|
||||||
|
which may rename/drop fields, then saves.
|
||||||
|
3. Objects referencing dropped/renamed fields are **silently removed** from
|
||||||
|
the saved `.qvw` binary.
|
||||||
|
4. QV's native ring-buffer backup (`BackupKeepCopies=4` by default) gets
|
||||||
|
evicted within a day of reload cycles → user's edit is **unrecoverable**.
|
||||||
|
|
||||||
|
The fix: put every `-prj/` XML under git **before** each reload.
|
||||||
|
|
||||||
|
## Step 1: Gather inputs
|
||||||
|
|
||||||
|
Ask the user for:
|
||||||
|
1. **Project root** — e.g. `C:\Users\...\MyProject\`. Must contain the QVW and the
|
||||||
|
reload BAT.
|
||||||
|
2. **QVW filename** — e.g. `MyProject_v3.qvw`. (If multiple QVWs, ask which one.)
|
||||||
|
3. **Reload BAT filename** — e.g. `Reload_MyProject.bat`. (Often in `8.BAT\`.)
|
||||||
|
|
||||||
|
Read the project layout to confirm those exist before proceeding.
|
||||||
|
|
||||||
|
## Step 2: Enable QlikView's native auto-backup
|
||||||
|
|
||||||
|
Check `%APPDATA%\QlikTech\QlikView\Settings.ini` for these keys:
|
||||||
|
```ini
|
||||||
|
[Settings 7]
|
||||||
|
BackupBeforeReload=1
|
||||||
|
BackupEnable=1
|
||||||
|
BackupKeepCopies=4 ; QV default; user can raise
|
||||||
|
AlwaysGenerateProject=1 ; required so -prj/ is populated on save
|
||||||
|
```
|
||||||
|
If any are missing, add them. Explain to the user that
|
||||||
|
`AlwaysGenerateProject=1` is essential — without it the `-prj/` folder
|
||||||
|
doesn't get the XML files that git tracks.
|
||||||
|
|
||||||
|
## Step 3: Initialize git in the project root
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd <PROJECT_ROOT>
|
||||||
|
git init
|
||||||
|
git config user.email "<user email>"
|
||||||
|
git config user.name "<user name>"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 4: Write the `.gitignore`
|
||||||
|
|
||||||
|
Critical: **Windows git is case-insensitive by default**, so `*.qvw` would
|
||||||
|
match the folder `4.Apps.QVW/` and ignore everything inside it (including
|
||||||
|
the precious `-prj/` subfolders). Use the `.gitignore` template
|
||||||
|
`templates/.gitignore` from this skill, which has the case-insensitive fix:
|
||||||
|
|
||||||
|
- Ignores `**/*.qvw`, `**/*.qvf`, `**/*.qvd`, `**/*.qvw.bak*` (actual binary files)
|
||||||
|
- Ignores `/4.Apps.QVW/backups/**` and `/4.Apps.QVW/_recovery/**`
|
||||||
|
- **Re-includes** `/4.Apps.QVW/**/*-prj/` and its contents (the `!` rules come
|
||||||
|
AFTER the ignore rules)
|
||||||
|
- Ignores logs, `.DS_Store`, `Thumbs.db`, `*.tmp`
|
||||||
|
- Ignores `4.Apps.QVW/Version * of *` (QV's native backup files, too big)
|
||||||
|
|
||||||
|
Copy the template to `<PROJECT_ROOT>/.gitignore` and adapt paths if the
|
||||||
|
project doesn't follow the QQinfo `4.Apps.QVW/` convention — ask the user.
|
||||||
|
|
||||||
|
## Step 5: Install `Backup_QVW.ps1` (rolling buffer)
|
||||||
|
|
||||||
|
Copy `templates/Backup_QVW.ps1` into `<PROJECT_ROOT>/8.BAT/` (or wherever
|
||||||
|
BATs live). Default retention is **20 slots** (parameter `-Keep`), so a day
|
||||||
|
of reloads can't evict a good edit. This runs BEFORE each reload as a
|
||||||
|
fallback safety net in case git isn't enough (e.g. if the user forgot
|
||||||
|
to enable `AlwaysGenerateProject`).
|
||||||
|
|
||||||
|
## Step 6: Wrap the reload BAT with git commits
|
||||||
|
|
||||||
|
The reload BAT must do, in order:
|
||||||
|
1. **Pre-reload `git add -A && git commit`** — captures whatever manual
|
||||||
|
edits the user made since the last reload.
|
||||||
|
2. Backup QVW to rolling buffer (`Backup_QVW.ps1 -Keep 20`).
|
||||||
|
3. `qv.exe /r <APP_PATH>` — the actual reload.
|
||||||
|
4. **Post-reload `git add -A && git commit`** — captures script-side
|
||||||
|
changes so you can diff "user edits" vs "reload output".
|
||||||
|
|
||||||
|
Use the template `templates/Reload_wrapped.bat`. Substitute:
|
||||||
|
- `%PROJECT_ROOT%` — the project root
|
||||||
|
- `%APP_PATH%` — the QVW to reload
|
||||||
|
- `%LOG_FILE%` — where to append reload log (usually `10.QQlog\`)
|
||||||
|
|
||||||
|
Warn user: all `git commit` commands use `--allow-empty` so they never
|
||||||
|
fail even when nothing changed.
|
||||||
|
|
||||||
|
## Step 7: Changing the script include path (if needed)
|
||||||
|
|
||||||
|
If the QVW's embedded script points at an old `9.QVSvN` folder and needs
|
||||||
|
to target a new one (e.g. during a version bump), use COM script
|
||||||
|
injection via `GetProperties / SetProperties`. The legacy `SetScript()`
|
||||||
|
method does NOT exist on modern QV Document COM. Template at
|
||||||
|
`templates/inject_include.ps1`. Usage:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -File inject_include.ps1 `
|
||||||
|
-QvwPath "C:\path\to\app.qvw" `
|
||||||
|
-NewInclude "C:\path\to\9.QVSv9\0100.MustInclude.qvs"
|
||||||
|
```
|
||||||
|
|
||||||
|
The script calls `$doc.GetProperties()` → sets `.Script` → `$doc.SetProperties($props)` → `.Save()`.
|
||||||
|
|
||||||
|
## Step 8: Make the initial commit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd <PROJECT_ROOT>
|
||||||
|
git add -A
|
||||||
|
git commit -m "Initial commit: QVS/config/-prj tracked; binaries excluded"
|
||||||
|
git tag initial
|
||||||
|
```
|
||||||
|
|
||||||
|
Then verify with `git log --oneline` and confirm the `-prj/*.xml` files
|
||||||
|
made it into the commit (not just `.qvs`/`.csv`). Run
|
||||||
|
`git check-ignore -v 4.Apps.QVW/<app>-prj/TB01.xml` to confirm NOT ignored.
|
||||||
|
|
||||||
|
## Step 9: Tell the user what to do when disaster strikes
|
||||||
|
|
||||||
|
If a reload destroys an object:
|
||||||
|
1. `git log --oneline --all -- 4.Apps.QVW/<app>-prj/` — find the last
|
||||||
|
commit that had it.
|
||||||
|
2. `git show <commit>:4.Apps.QVW/<app>-prj/TB05.xml` — recover the XML.
|
||||||
|
3. Paste the XML back into the `-prj/` folder and "Import from project"
|
||||||
|
in QlikView, OR restore the whole QVW from the rolling buffer:
|
||||||
|
`cp 4.Apps.QVW/backups/<name>_<ts>.qvw 4.Apps.QVW/<name>.qvw`.
|
||||||
|
|
||||||
|
## Step 10: Post-install verification
|
||||||
|
|
||||||
|
Run the reload BAT once end-to-end. Confirm:
|
||||||
|
- Two new git commits appear (`pre-reload` + `post-reload`).
|
||||||
|
- A new `.qvw` lands in `4.Apps.QVW/backups/`.
|
||||||
|
- The `-prj/` folder is populated with fresh XML timestamps.
|
||||||
|
- `git check-ignore -v` on any `-prj/*.xml` returns empty (NOT ignored).
|
||||||
|
|
||||||
|
If any of those fail, diagnose before declaring success.
|
||||||
|
|
||||||
|
## Common gotchas
|
||||||
|
|
||||||
|
- **Filename with spaces/typos** (e.g. `QQpo_v2 inerface preps.qvw`) — QV COM
|
||||||
|
`OpenDoc` returns null silently. Rename to a clean path before scripting.
|
||||||
|
- **`.qvw.bak.qvf`** is QV's native one-off backup (same content, renamed).
|
||||||
|
Safe to keep — gitignore already excludes.
|
||||||
|
- **`Version N of <app>.qvw`** — also QV native backups. Gitignored.
|
||||||
|
- **Tableboxes in "show all fields of table" mode** self-heal after field
|
||||||
|
renames. Tableboxes with explicit field lists do NOT — user has to re-edit.
|
||||||
|
- **Branch protection** — the local-only setup has no remote by design.
|
||||||
|
If the user wants off-site backup, run `git remote add origin ...` as a
|
||||||
|
follow-up step (Gitea / GitHub / Forgejo).
|
||||||
46
templates/.gitignore
vendored
Normal file
46
templates/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
# ===== QVW project — preserve interface via git (-prj/ tracked, binaries ignored) =====
|
||||||
|
# NOTE: on Windows, git is case-insensitive by default — a bare *.qvw would match
|
||||||
|
# the folder "4.Apps.QVW" and ignore everything inside it. So ignores are scoped
|
||||||
|
# to leaf filenames via explicit directory globs.
|
||||||
|
|
||||||
|
# QVW/QVF/QVD files — only as files, never folders
|
||||||
|
**/*.qvw
|
||||||
|
**/*.qvf
|
||||||
|
**/*.qvw.bak*
|
||||||
|
**/*.qvd
|
||||||
|
|
||||||
|
# Logs & scratch
|
||||||
|
*.log
|
||||||
|
**/*.qvs.log
|
||||||
|
*.tmp
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Explicit data folders to ignore (no content tracked)
|
||||||
|
/0.Sources.CSV/**
|
||||||
|
/1.Init.QVD/**
|
||||||
|
/3.Intermed.QVD/**
|
||||||
|
/4.Apps.QVW/backups/**
|
||||||
|
/4.Apps.QVW/_recovery/**
|
||||||
|
/10.LOG/**
|
||||||
|
/10.QQlog/**
|
||||||
|
|
||||||
|
# "Version N of ..." — Qlik native backups next to QVW
|
||||||
|
4.Apps.QVW/Version */
|
||||||
|
|
||||||
|
# Re-include key folders that the case-insensitive QVW pattern accidentally blanketed.
|
||||||
|
# Order: un-ignore folder, then un-ignore contents.
|
||||||
|
!/4.Apps.QVW/
|
||||||
|
!/4.Apps.QVW/*
|
||||||
|
!/4.Apps.QVW/**/*-prj/
|
||||||
|
!/4.Apps.QVW/**/*-prj/**
|
||||||
|
!/1.Init.QVD/
|
||||||
|
!/3.Intermed.QVD/
|
||||||
|
|
||||||
|
# But re-apply binary ignore inside the re-included 4.Apps.QVW
|
||||||
|
/4.Apps.QVW/*.qvw
|
||||||
|
/4.Apps.QVW/*.qvf
|
||||||
|
/4.Apps.QVW/*.qvw.bak*
|
||||||
|
/4.Apps.QVW/*.qvd
|
||||||
|
/4.Apps.QVW/backups/
|
||||||
|
/4.Apps.QVW/_recovery/
|
||||||
28
templates/Backup_QVW.ps1
Normal file
28
templates/Backup_QVW.ps1
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Backup_QVW.ps1 — rolling ring of pre-reload QVW snapshots.
|
||||||
|
# Usage: powershell -File Backup_QVW.ps1 -QvwPath "..\4.Apps.QVW\MyApp.qvw" [-Keep 20]
|
||||||
|
# Default retention 20 slots (a day of reloads won't evict good edits).
|
||||||
|
# NOTE: this is a FALLBACK. Primary safety net is git commits in Reload_*.bat.
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)][string]$QvwPath,
|
||||||
|
[int]$Keep = 20
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
if (-not (Test-Path $QvwPath)) { Write-Host "No QVW to back up: $QvwPath"; exit 0 }
|
||||||
|
|
||||||
|
$dir = Split-Path $QvwPath -Parent
|
||||||
|
$name = [System.IO.Path]::GetFileNameWithoutExtension($QvwPath)
|
||||||
|
$ext = [System.IO.Path]::GetExtension($QvwPath)
|
||||||
|
$backupDir = Join-Path $dir "backups"
|
||||||
|
if (-not (Test-Path $backupDir)) { New-Item -ItemType Directory -Path $backupDir -Force | Out-Null }
|
||||||
|
|
||||||
|
$stamp = Get-Date -Format "yyyyMMdd_HHmmss"
|
||||||
|
$dest = Join-Path $backupDir ($name + "_" + $stamp + $ext)
|
||||||
|
Copy-Item $QvwPath $dest -Force
|
||||||
|
Write-Host "Backed up: $dest"
|
||||||
|
|
||||||
|
# Prune: keep only $Keep most-recent backups matching "$name_*.qvw"
|
||||||
|
Get-ChildItem $backupDir -Filter ($name + "_*" + $ext) -File |
|
||||||
|
Sort-Object LastWriteTime -Descending |
|
||||||
|
Select-Object -Skip $Keep |
|
||||||
|
ForEach-Object { Remove-Item $_.FullName -Force; Write-Host "Pruned: $($_.Name)" }
|
||||||
43
templates/Reload_wrapped.bat
Normal file
43
templates/Reload_wrapped.bat
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
@echo off
|
||||||
|
SETLOCAL ENABLEDELAYEDEXPANSION
|
||||||
|
|
||||||
|
REM ============================================================
|
||||||
|
REM Reload wrapped with pre/post git commits + rolling QVW backup.
|
||||||
|
REM Substitute PROJECT_ROOT and APP_PATH for your project.
|
||||||
|
REM ============================================================
|
||||||
|
|
||||||
|
SET "PROJECT_ROOT=<<PROJECT_ROOT>>"
|
||||||
|
SET "APP_PATH=%PROJECT_ROOT%\4.Apps.QVW\<<APP_NAME>>.qvw"
|
||||||
|
SET "LOG_DIR=%PROJECT_ROOT%\10.QQlog"
|
||||||
|
IF NOT EXIST "%LOG_DIR%" MKDIR "%LOG_DIR%"
|
||||||
|
SET "LOG_FILE=%LOG_DIR%\QQreload_monitor_<<APP_NAME>>.txt"
|
||||||
|
|
||||||
|
FOR /F %%a IN ('powershell -NoProfile -Command "Get-Date -Format yyyyMMdd_HHmmss"') DO SET "TS=%%a"
|
||||||
|
|
||||||
|
REM --- Pre-reload: snapshot current -prj state to git ---
|
||||||
|
PUSHD "%PROJECT_ROOT%"
|
||||||
|
git add -A >> "%LOG_FILE%" 2>&1
|
||||||
|
git commit -m "pre-reload %TS% (user edits captured)" --allow-empty >> "%LOG_FILE%" 2>&1
|
||||||
|
POPD
|
||||||
|
|
||||||
|
REM --- Rolling QVW backup (fallback safety net) ---
|
||||||
|
powershell -ExecutionPolicy Bypass -File "%~dp0Backup_QVW.ps1" -QvwPath "%APP_PATH%" -Keep 20 >> "%LOG_FILE%" 2>&1
|
||||||
|
|
||||||
|
SET "QV_EXE="
|
||||||
|
FOR /F "tokens=2*" %%A IN ('REG QUERY "HKLM\SOFTWARE\QlikTech\QlikView" /v InstallDir 2^>nul') DO SET "QV_EXE=%%B\qv.exe"
|
||||||
|
IF NOT DEFINED QV_EXE IF EXIST "C:\Program Files\QlikView\qv.exe" SET "QV_EXE=C:\Program Files\QlikView\qv.exe"
|
||||||
|
IF NOT DEFINED QV_EXE (ECHO qv.exe not found >> "%LOG_FILE%" & EXIT /B 2)
|
||||||
|
|
||||||
|
ECHO ============================================================ >> "%LOG_FILE%"
|
||||||
|
ECHO [%date% %time%] reload start >> "%LOG_FILE%"
|
||||||
|
"%QV_EXE%" /r "%APP_PATH%"
|
||||||
|
SET "RC=%ERRORLEVEL%"
|
||||||
|
ECHO [%date% %time%] reload end rc=%RC% >> "%LOG_FILE%"
|
||||||
|
|
||||||
|
REM --- Post-reload: capture script-side -prj changes to git ---
|
||||||
|
PUSHD "%PROJECT_ROOT%"
|
||||||
|
git add -A >> "%LOG_FILE%" 2>&1
|
||||||
|
git commit -m "post-reload %TS% (rc=%RC%)" --allow-empty >> "%LOG_FILE%" 2>&1
|
||||||
|
POPD
|
||||||
|
|
||||||
|
EXIT /B %RC%
|
||||||
41
templates/inject_include.ps1
Normal file
41
templates/inject_include.ps1
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# inject_include.ps1 — rewrite a QVW's embedded Script to include a different
|
||||||
|
# MustInclude .qvs file (typical use: version bump from 9.QVSv8 -> 9.QVSv9).
|
||||||
|
#
|
||||||
|
# IMPORTANT: The legacy $doc.SetScript($s) method does NOT exist on modern
|
||||||
|
# QlikView Document COM (>= QV11). Use GetProperties() -> .Script -> SetProperties().
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# powershell -ExecutionPolicy Bypass -File inject_include.ps1 `
|
||||||
|
# -QvwPath "C:\path\to\app.qvw" `
|
||||||
|
# -NewInclude "C:\path\to\9.QVSv9\0100.MustInclude.qvs"
|
||||||
|
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)][string]$QvwPath,
|
||||||
|
[Parameter(Mandatory=$true)][string]$NewInclude
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
if (-not (Test-Path $QvwPath)) { throw "QVW not found: $QvwPath" }
|
||||||
|
if (-not (Test-Path $NewInclude)) { throw "Include file not found: $NewInclude" }
|
||||||
|
|
||||||
|
$qv = New-Object -ComObject QlikTech.QlikView
|
||||||
|
$doc = $qv.OpenDoc($QvwPath, "", "")
|
||||||
|
if (-not $doc) { throw "OpenDoc returned null for $QvwPath (check filename for spaces/typos)" }
|
||||||
|
Start-Sleep -Seconds 3
|
||||||
|
|
||||||
|
$newScript = @"
|
||||||
|
SET ErrorMode = 0;
|
||||||
|
SET DateFormat='YYYY-MM-DD';
|
||||||
|
`$(Include=$NewInclude);
|
||||||
|
"@
|
||||||
|
|
||||||
|
$props = $doc.GetProperties()
|
||||||
|
Write-Host "Current script head: $($props.Script.Substring(0, [Math]::Min(120, $props.Script.Length)))"
|
||||||
|
|
||||||
|
$props.Script = $newScript
|
||||||
|
$doc.SetProperties($props)
|
||||||
|
$doc.Save()
|
||||||
|
Write-Host "Saved $QvwPath with include -> $NewInclude"
|
||||||
|
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
|
$qv.Quit()
|
||||||
Loading…
Reference in a new issue